feat: Initial CBPOA commit — 武汉儿童呼吸疾病风险评估系统
Context: Build a spatial risk assessment system correlating air quality data with children's respiratory disease incidence across Wuhan. Approach: FastAPI backend serving PostGIS spatial queries, React frontend with Deck.gl maps, and a PyTorch SpatialTemporalGCN pipeline for multi-day (1d/3d/7d) risk prediction. Changes: - backend/ — FastAPI API with auth (JWT), alerts, risk analysis, geocoded case data, grid statistics, and report endpoints - frontend/ — React dashboard with interactive risk maps, alert monitoring, district comparison charts, and timeline player - models/ — SpatialTemporalGCN model with trained weights and ONNX export for inference - scripts/ — ETL pipeline for weather + medical data, grid generation, feature engineering, training, and daily inference - deploy/ — Docker Compose configs for backend, frontend, and MLflow - docs/ — API docs, deployment guide, user guide, and code review Impact: Enables spatial risk visualization, alert monitoring, and ML-driven health risk forecasting for environmental health teams.
This commit is contained in:
172
backend/routers/geocoded.py
Normal file
172
backend/routers/geocoded.py
Normal file
@@ -0,0 +1,172 @@
|
||||
"""
|
||||
Router for geocoded case data and grid aggregated data
|
||||
"""
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel
|
||||
from typing import List, Optional
|
||||
import logging
|
||||
import pandas as pd
|
||||
from pathlib import Path
|
||||
|
||||
logger = logging.getLogger("cbpoa.geocoded")
|
||||
|
||||
router = APIRouter(prefix="/api/geocoded", tags=["geocoded"])
|
||||
|
||||
PROJECT_ROOT = Path(__file__).parent.parent.parent
|
||||
DATA_DIR = PROJECT_ROOT / "outputs"
|
||||
|
||||
class GridCaseData(BaseModel):
|
||||
"""Grid case data for visualization"""
|
||||
grid_id: int
|
||||
latitude: float
|
||||
longitude: float
|
||||
total_cases: int
|
||||
outpatient_cases: int
|
||||
inpatient_cases: int
|
||||
case_density: float
|
||||
risk_index: float
|
||||
risk_level: str
|
||||
|
||||
class GridCaseResponse(BaseModel):
|
||||
grids: List[GridCaseData]
|
||||
total_count: int
|
||||
total_cases: int
|
||||
|
||||
class GeocodedCaseData(BaseModel):
|
||||
"""Individual geocoded case"""
|
||||
case_id: str
|
||||
case_type: str
|
||||
latitude: float
|
||||
longitude: float
|
||||
district: str
|
||||
street: Optional[str]
|
||||
geocode_method: str
|
||||
confidence: float
|
||||
|
||||
class GeocodedResponse(BaseModel):
|
||||
cases: List[GeocodedCaseData]
|
||||
total_count: int
|
||||
|
||||
@router.get("/grid", response_model=GridCaseResponse, summary="Get aggregated grid case data")
|
||||
async def get_grid_cases():
|
||||
"""
|
||||
Get 100x100m grid aggregated case data for high-resolution visualization.
|
||||
|
||||
Returns grid cells with case counts, density, and risk indices.
|
||||
"""
|
||||
grid_file = DATA_DIR / "grid_risk_summary.csv"
|
||||
|
||||
if not grid_file.exists():
|
||||
raise HTTPException(status_code=404, detail="Grid data not found")
|
||||
|
||||
try:
|
||||
df = pd.read_csv(grid_file)
|
||||
|
||||
grids = []
|
||||
for _, row in df.iterrows():
|
||||
grids.append(GridCaseData(
|
||||
grid_id=int(row['grid_id']),
|
||||
latitude=float(row['center_y']),
|
||||
longitude=float(row['center_x']),
|
||||
total_cases=int(row['total_cases']),
|
||||
outpatient_cases=int(row['outpatient_cases']),
|
||||
inpatient_cases=int(row['inpatient_cases']),
|
||||
case_density=float(row['cases_per_km2']),
|
||||
risk_index=float(row['risk_index']),
|
||||
risk_level=str(row['risk_level'])
|
||||
))
|
||||
|
||||
total_cases = int(df['total_cases'].sum())
|
||||
|
||||
return GridCaseResponse(
|
||||
grids=grids,
|
||||
total_count=len(grids),
|
||||
total_cases=total_cases
|
||||
)
|
||||
except Exception as e:
|
||||
logger.exception("Error loading grid case data")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
@router.get("/geocoded", response_model=GeocodedResponse, summary="Get geocoded case data")
|
||||
async def get_geocoded_cases(
|
||||
limit: int = 1000,
|
||||
district: Optional[str] = None,
|
||||
):
|
||||
"""
|
||||
Get individual geocoded case data.
|
||||
|
||||
Args:
|
||||
limit: Maximum number of cases to return (for performance)
|
||||
district: Filter by district name
|
||||
"""
|
||||
cases_file = DATA_DIR / "geocoded_all_cases.csv"
|
||||
|
||||
if not cases_file.exists():
|
||||
raise HTTPException(status_code=404, detail="Geocoded data not found")
|
||||
|
||||
try:
|
||||
df = pd.read_csv(cases_file)
|
||||
|
||||
# Drop rows with missing coordinates
|
||||
df = df.dropna(subset=['latitude', 'longitude'])
|
||||
|
||||
# Fix swapped lat/lon (Wuhan: lat ~29.9-31.4, lon ~113.7-115.1)
|
||||
swapped = df['latitude'] > 50 # longitude values are >113
|
||||
df.loc[swapped, ['latitude', 'longitude']] = df.loc[swapped, ['longitude', 'latitude']].values
|
||||
|
||||
# Filter by district if specified
|
||||
if district:
|
||||
df = df[df['district'] == district]
|
||||
|
||||
# Limit for performance
|
||||
df = df.head(limit)
|
||||
|
||||
cases = []
|
||||
for _, row in df.iterrows():
|
||||
street_val = row.get('street')
|
||||
if pd.isna(street_val):
|
||||
street_val = None
|
||||
district_val = row.get('district', '')
|
||||
if pd.isna(district_val):
|
||||
district_val = '未知'
|
||||
cases.append(GeocodedCaseData(
|
||||
case_id=str(row['case_id']),
|
||||
case_type=str(row['case_type']),
|
||||
latitude=float(row['latitude']),
|
||||
longitude=float(row['longitude']),
|
||||
district=str(district_val),
|
||||
street=street_val,
|
||||
geocode_method=str(row.get('geocode_method', 'unknown')),
|
||||
confidence=float(row.get('confidence', 0) or 0) if not pd.isna(row.get('confidence')) else 0.0
|
||||
))
|
||||
|
||||
return GeocodedResponse(
|
||||
cases=cases,
|
||||
total_count=len(cases)
|
||||
)
|
||||
except Exception as e:
|
||||
logger.exception("Error loading geocoded case data")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
@router.get("/geocoded/count", summary="Get geocoded case count")
|
||||
async def get_geocoded_count():
|
||||
"""Get total count of geocoded cases."""
|
||||
cases_file = DATA_DIR / "geocoded_all_cases.csv"
|
||||
|
||||
if not cases_file.exists():
|
||||
raise HTTPException(status_code=404, detail="Geocoded data not found")
|
||||
|
||||
try:
|
||||
df = pd.read_csv(cases_file)
|
||||
street_matched = len(df[df['geocode_method'] == 'street'])
|
||||
district_fallback = len(df[df['geocode_method'] == 'district'])
|
||||
|
||||
return {
|
||||
"total": len(df),
|
||||
"street_matched": street_matched,
|
||||
"district_fallback": district_fallback,
|
||||
"match_rate": round(street_matched / len(df) * 100, 1)
|
||||
}
|
||||
except Exception as e:
|
||||
logger.exception("Error counting geocoded cases")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
Reference in New Issue
Block a user