Files
CA/backend/routers/geocoded.py

179 lines
5.7 KiB
Python
Raw Normal View History

"""
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
from functools import lru_cache
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"
@lru_cache(maxsize=1)
def _load_csv(path: Path) -> pd.DataFrame:
return pd.read_csv(path)
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 = _load_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 = _load_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.to_dict('records'):
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 = _load_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")