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:
315
backend/models.py
Normal file
315
backend/models.py
Normal file
@@ -0,0 +1,315 @@
|
||||
"""
|
||||
Pydantic models for CBPOA risk assessment API
|
||||
Aligned with frontend types from CBPOA/frontend/src/types/index.ts
|
||||
"""
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional, List, Literal
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class GridRisk(BaseModel):
|
||||
"""Grid risk data for map visualization"""
|
||||
grid_id: str = Field(..., description="Grid identifier")
|
||||
latitude: float = Field(..., description="Latitude coordinate")
|
||||
longitude: float = Field(..., description="Longitude coordinate")
|
||||
risk_value: float = Field(..., description="Risk value (0-1)")
|
||||
risk_level: Literal['high', 'medium_high', 'medium', 'medium_low', 'low'] = Field(..., description="Risk level classification")
|
||||
|
||||
|
||||
class GridDetail(GridRisk):
|
||||
"""Detailed grid information with environmental factors"""
|
||||
region: str = Field(..., description="Administrative region")
|
||||
street: str = Field(..., description="Street name")
|
||||
population_density: float = Field(..., description="Population density per km²")
|
||||
nearby_schools: int = Field(..., description="Number of nearby schools")
|
||||
nearby_schools_distance: float = Field(..., description="Distance to nearest school (km)")
|
||||
nearby_hospitals: int = Field(..., description="Number of nearby hospitals")
|
||||
nearby_hospitals_distance: float = Field(..., description="Distance to nearest hospital (km)")
|
||||
traffic_flow: str = Field(..., description="Traffic flow level")
|
||||
green_coverage: float = Field(..., description="Green coverage percentage")
|
||||
building_density: float = Field(..., description="Building density percentage")
|
||||
air_quality: str = Field(..., description="Air quality description")
|
||||
humidity: float = Field(..., description="Humidity percentage")
|
||||
wind_speed: float = Field(..., description="Wind speed (m/s)")
|
||||
temperature: float = Field(..., description="Temperature (°C)")
|
||||
trend: str = Field(..., description="Risk trend")
|
||||
forecast_1day: float = Field(..., description="1-day forecast risk value")
|
||||
forecast_3day: float = Field(..., description="3-day forecast risk value")
|
||||
forecast_7day: float = Field(..., description="7-day forecast risk value")
|
||||
timestamp: str = Field(..., description="Data timestamp")
|
||||
|
||||
|
||||
class RiskMapResponse(BaseModel):
|
||||
"""Response for risk map data"""
|
||||
grids: List[GridRisk] = Field(..., description="List of grid risk data")
|
||||
total_count: int = Field(..., description="Total number of grids")
|
||||
timestamp: str = Field(..., description="Response timestamp")
|
||||
|
||||
|
||||
class GridDetailResponse(BaseModel):
|
||||
"""Response for grid detail with history"""
|
||||
grid: GridDetail = Field(..., description="Grid detail information")
|
||||
history_risk: List[dict[str, str | float]] = Field(..., description="Historical risk data")
|
||||
|
||||
|
||||
class Alert(BaseModel):
|
||||
"""Health alert for high-risk area"""
|
||||
alert_id: str = Field(..., description="Alert identifier")
|
||||
grid_id: str = Field(..., description="Grid identifier")
|
||||
region: str = Field(..., description="Administrative region")
|
||||
street: str = Field(..., description="Street name")
|
||||
latitude: float = Field(..., description="Latitude coordinate")
|
||||
longitude: float = Field(..., description="Longitude coordinate")
|
||||
risk_value: float = Field(..., description="Risk value")
|
||||
risk_level: Literal['high', 'medium_high', 'medium', 'medium_low', 'low'] = Field(..., description="Risk level")
|
||||
priority: Literal['P1', 'P2'] = Field(..., description="Alert priority")
|
||||
reason: str = Field(..., description="Alert reason")
|
||||
timestamp: str = Field(..., description="Alert timestamp")
|
||||
forecast_time: str = Field(..., description="Forecast time")
|
||||
|
||||
|
||||
class AlertResponse(BaseModel):
|
||||
"""Response for alerts list"""
|
||||
alerts: List[Alert] = Field(..., description="List of alerts")
|
||||
total: int = Field(..., description="Total number of alerts")
|
||||
timestamp: str = Field(..., description="Response timestamp")
|
||||
|
||||
|
||||
class Stats(BaseModel):
|
||||
"""Risk statistics summary"""
|
||||
total_grids: int = Field(..., description="Total number of grids")
|
||||
avg_risk: float = Field(..., description="Average risk value")
|
||||
distribution: dict[str, int] = Field(..., description="Risk level distribution")
|
||||
high_risk_count: int = Field(..., description="Count of high risk grids")
|
||||
timestamp: str = Field(..., description="Stats timestamp")
|
||||
|
||||
|
||||
class HistoryPoint(BaseModel):
|
||||
"""Single point in risk history"""
|
||||
date: str = Field(..., description="Date string")
|
||||
risk_value: float = Field(..., description="Risk value")
|
||||
|
||||
|
||||
class RiskHistoryResponse(BaseModel):
|
||||
"""Response for risk history"""
|
||||
grid_id: str = Field(..., description="Grid identifier")
|
||||
history: List[HistoryPoint] = Field(..., description="Historical risk data")
|
||||
|
||||
|
||||
ForecastDay = Literal[0, 1, 3, 7]
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Insights Models
|
||||
# ============================================================================
|
||||
|
||||
class InsightTrendItem(BaseModel):
|
||||
"""Single trend data point for insights"""
|
||||
date: str = Field(..., description="Date string")
|
||||
value: float = Field(..., description="Risk value")
|
||||
change: float = Field(default=0, description="Change from previous day")
|
||||
|
||||
|
||||
class InsightTrend(BaseModel):
|
||||
"""Trend analysis for insights"""
|
||||
period: str = Field(..., description="Time period (e.g., '7d', '30d')")
|
||||
data: List[InsightTrendItem] = Field(..., description="Trend data points")
|
||||
direction: Literal["up", "down", "stable"] = Field(..., description="Overall trend direction")
|
||||
avg_change: float = Field(..., description="Average daily change percentage")
|
||||
|
||||
|
||||
class InsightHotspot(BaseModel):
|
||||
"""Hotspot area for insights"""
|
||||
grid_id: str = Field(..., description="Grid identifier")
|
||||
latitude: float = Field(..., description="Latitude coordinate")
|
||||
longitude: float = Field(..., description="Longitude coordinate")
|
||||
risk_value: float = Field(..., description="Current risk value")
|
||||
risk_level: Literal['high', 'medium_high', 'medium', 'medium_low', 'low'] = Field(..., description="Risk level")
|
||||
region: str = Field(..., description="Administrative region")
|
||||
street: str = Field(..., description="Street name")
|
||||
population_density: float = Field(..., description="Population density")
|
||||
days_in_high_risk: int = Field(..., description="Consecutive days in high risk")
|
||||
|
||||
|
||||
class InsightCorrelation(BaseModel):
|
||||
"""Correlation factor for insights"""
|
||||
factor: str = Field(..., description="Factor name (e.g., 'temperature', 'PM2.5')")
|
||||
correlation: float = Field(..., description="Correlation coefficient (-1 to 1)")
|
||||
significance: Literal["high", "medium", "low"] = Field(..., description="Statistical significance")
|
||||
description: str = Field(..., description="Factor description")
|
||||
impact: Literal["positive", "negative", "neutral"] = Field(..., description="Impact direction")
|
||||
|
||||
|
||||
class InsightDemographic(BaseModel):
|
||||
"""Demographic breakdown for insights"""
|
||||
age_group: str = Field(..., description="Age group (e.g., '0-14', '15-64', '65+')")
|
||||
case_count: int = Field(..., description="Number of cases")
|
||||
percentage: float = Field(..., description="Percentage of total cases")
|
||||
risk_ratio: float = Field(..., description="Risk ratio compared to baseline")
|
||||
|
||||
|
||||
class InsightsResponse(BaseModel):
|
||||
"""Response for comprehensive insights"""
|
||||
trend: InsightTrend = Field(..., description="Risk trend analysis")
|
||||
hotspots: List[InsightHotspot] = Field(..., description="Top hotspot areas")
|
||||
correlations: List[InsightCorrelation] = Field(..., description="Key correlation factors")
|
||||
demographics: List[InsightDemographic] = Field(..., description="Demographic breakdown")
|
||||
summary: str = Field(..., description="AI-generated summary of insights")
|
||||
timestamp: str = Field(..., description="Response timestamp")
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Reports Models
|
||||
# ============================================================================
|
||||
|
||||
class ReportSection(BaseModel):
|
||||
"""Single section of a report"""
|
||||
title: str = Field(..., description="Section title")
|
||||
content: str = Field(..., description="Section content")
|
||||
charts: List[str] = Field(default=[], description="Chart identifiers for this section")
|
||||
|
||||
|
||||
class ReportMetadata(BaseModel):
|
||||
"""Metadata for a report"""
|
||||
report_id: str = Field(..., description="Report identifier")
|
||||
title: str = Field(..., description="Report title")
|
||||
type: Literal["daily", "weekly", "monthly", "custom"] = Field(..., description="Report type")
|
||||
generated_at: str = Field(..., description="Generation timestamp")
|
||||
period_start: str = Field(..., description="Report period start date")
|
||||
period_end: str = Field(..., description="Report period end date")
|
||||
author: str = Field(default="CBPOA System", description="Report author")
|
||||
|
||||
|
||||
class ReportSummary(BaseModel):
|
||||
"""Summary statistics for a report"""
|
||||
total_cases: int = Field(..., description="Total cases in period")
|
||||
avg_risk: float = Field(..., description="Average risk level")
|
||||
peak_risk_date: str = Field(..., description="Date of peak risk")
|
||||
peak_risk_value: float = Field(..., description="Peak risk value")
|
||||
high_risk_areas: int = Field(..., description="Number of high risk areas")
|
||||
trend_direction: Literal["improving", "stable", "worsening"] = Field(..., description="Overall trend")
|
||||
|
||||
|
||||
class ReportRecommendation(BaseModel):
|
||||
"""Recommendation from report"""
|
||||
priority: Literal["high", "medium", "low"] = Field(..., description="Recommendation priority")
|
||||
category: Literal["prevention", "monitoring", "intervention", "resource_allocation"] = Field(..., description="Recommendation category")
|
||||
title: str = Field(..., description="Recommendation title")
|
||||
description: str = Field(..., description="Detailed recommendation")
|
||||
target_areas: List[str] = Field(default=[], description="Target grid IDs or regions")
|
||||
|
||||
|
||||
class ReportResponse(BaseModel):
|
||||
"""Response for full report"""
|
||||
metadata: ReportMetadata = Field(..., description="Report metadata")
|
||||
summary: ReportSummary = Field(..., description="Report summary")
|
||||
sections: List[ReportSection] = Field(..., description="Report sections")
|
||||
recommendations: List[ReportRecommendation] = Field(..., description="Recommendations")
|
||||
attachments: List[str] = Field(default=[], description="Attachment file paths")
|
||||
timestamp: str = Field(..., description="Response timestamp")
|
||||
|
||||
|
||||
class ReportListResponse(BaseModel):
|
||||
"""Response for list of reports"""
|
||||
reports: List[ReportMetadata] = Field(..., description="List of report metadata")
|
||||
total: int = Field(..., description="Total number of reports")
|
||||
timestamp: str = Field(..., description="Response timestamp")
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Grid Data Models (Wave 2 - Task 9)
|
||||
# ============================================================================
|
||||
|
||||
class GridFeature(BaseModel):
|
||||
"""Single grid cell with features for model input"""
|
||||
grid_id: str = Field(..., description="Grid identifier (e.g., 'r100_c200')")
|
||||
latitude: float = Field(..., description="Center latitude")
|
||||
longitude: float = Field(..., description="Center longitude")
|
||||
dem: float = Field(..., description="Digital elevation model (meters)")
|
||||
population_density: float = Field(..., description="Population density per km²")
|
||||
district: Optional[str] = Field(None, description="District name")
|
||||
|
||||
|
||||
class WeatherFeature(BaseModel):
|
||||
"""Weather features for a grid cell"""
|
||||
grid_id: str
|
||||
AQI: float
|
||||
PM25: float
|
||||
PM10: float
|
||||
SO2: float
|
||||
NO2: float
|
||||
O3: float
|
||||
CO: float
|
||||
|
||||
|
||||
class CaseFeature(BaseModel):
|
||||
"""Case features for a grid cell"""
|
||||
grid_id: str
|
||||
outpatient_count: int = Field(default=0, description="Outpatient count")
|
||||
inpatient_count: int = Field(default=0, description="Inpatient count")
|
||||
total_cases: int = Field(default=0, description="Total case count")
|
||||
|
||||
|
||||
class GridPrediction(BaseModel):
|
||||
"""Prediction result for a single grid cell"""
|
||||
grid_id: str
|
||||
latitude: float
|
||||
longitude: float
|
||||
risk_1day: float = Field(..., description="1-day risk prediction (0-1)")
|
||||
risk_3day: float = Field(..., description="3-day risk prediction (0-1)")
|
||||
risk_7day: float = Field(..., description="7-day risk prediction (0-1)")
|
||||
risk_level: Literal['high', 'medium_high', 'medium', 'medium_low', 'low']
|
||||
confidence: Optional[float] = Field(None, description="Prediction confidence")
|
||||
|
||||
|
||||
class MultiDayPredictionRequest(BaseModel):
|
||||
"""Request for multi-day grid predictions"""
|
||||
date: str = Field(..., description="Start date (YYYY-MM-DD)")
|
||||
days: int = Field(default=7, ge=1, le=14, description="Number of days to predict")
|
||||
district: Optional[str] = Field(None, description="Filter by district")
|
||||
|
||||
|
||||
class MultiDayPredictionResponse(BaseModel):
|
||||
"""Response for multi-day grid predictions"""
|
||||
predictions: List[GridPrediction] = Field(..., description="Grid predictions")
|
||||
total_grids: int = Field(..., description="Total grids predicted")
|
||||
date_range: tuple[str, str] = Field(..., description="Prediction date range")
|
||||
model_version: str = Field(default="1.3.7", description="Model version")
|
||||
timestamp: str = Field(..., description="Response timestamp")
|
||||
partial: bool = Field(default=False, description="True if some dates failed to generate")
|
||||
warnings: List[str] = Field(default_factory=list, description="Warnings from partial failures")
|
||||
|
||||
|
||||
class HistoricalAggregationRequest(BaseModel):
|
||||
"""Request for historical data aggregation"""
|
||||
start_date: str = Field(..., description="Start date (YYYY-MM-DD)")
|
||||
end_date: str = Field(..., description="End date (YYYY-MM-DD)")
|
||||
aggregation: Literal['daily', 'weekly', 'monthly'] = Field(default='daily', description="Aggregation level")
|
||||
district: Optional[str] = Field(None, description="Filter by district")
|
||||
|
||||
|
||||
class DistrictAggregation(BaseModel):
|
||||
"""Aggregated data for a district"""
|
||||
district: str
|
||||
date: str
|
||||
total_cases: int
|
||||
outpatient_count: int
|
||||
inpatient_count: int
|
||||
avg_AQI: float
|
||||
avg_PM25: float
|
||||
avg_PM10: float
|
||||
|
||||
|
||||
class HistoricalAggregationResponse(BaseModel):
|
||||
"""Response for historical data aggregation"""
|
||||
aggregations: List[DistrictAggregation] = Field(..., description="Aggregated data")
|
||||
total_records: int = Field(..., description="Total records")
|
||||
date_range: tuple[str, str] = Field(..., description="Data date range")
|
||||
timestamp: str = Field(..., description="Response timestamp")
|
||||
|
||||
|
||||
class GridGeoJSONResponse(BaseModel):
|
||||
"""Response for grid data as GeoJSON"""
|
||||
type: Literal['FeatureCollection'] = 'FeatureCollection'
|
||||
features: List[dict] = Field(..., description="GeoJSON features")
|
||||
timestamp: str = Field(..., description="Response timestamp")
|
||||
Reference in New Issue
Block a user