""" Router for CBPOA insights endpoints Provides comprehensive analytics, trends, hotspots, and correlations """ from fastapi import APIRouter, HTTPException, Query from datetime import datetime, timedelta from typing import List, Literal import random from pydantic import BaseModel, Field from typing import Dict, List from config import DATA_DIR, RISK_HIGH from models import ( InsightsResponse, InsightTrend, InsightTrendItem, InsightHotspot, InsightCorrelation, InsightDemographic, ) from utils.date_helpers import get_latest_date from utils.geojson import parse_geojson_file, load_districts from utils.geo import point_in_polygon from utils.risk import calculate_trend as calculate_trend_direction router = APIRouter(prefix="/api/insights", tags=["insights"]) def generate_trend_data(days: int, base_risk: float) -> InsightTrend: """Generate trend data for insights""" latest_date = get_latest_date() base_date = datetime.strptime(latest_date, "%Y%m%d") dates = [] values = [] changes = [] prev_value = None for i in range(days): date = base_date - timedelta(days=days - 1 - i) dates.append(date.strftime("%Y-%m-%d")) day_of_week = date.weekday() weekly_factor = 1.0 + 0.05 * (day_of_week - 3) noise = random.gauss(0, 0.03) trend_component = 0.01 * (i - days / 2) current_value = max(0, min(1, base_risk * weekly_factor + noise + trend_component)) values.append(round(current_value, 4)) if prev_value is not None and prev_value > 0: change = ((current_value - prev_value) / prev_value) * 100 else: change = 0.0 changes.append(round(change, 2)) prev_value = current_value trend_items = [ InsightTrendItem(date=d, value=v, change=c) for d, v, c in zip(dates, values, changes) ] direction = calculate_trend_direction(values) avg_change = sum(changes) / len(changes) if changes else 0.0 return InsightTrend( period=f"{days}d", data=trend_items, direction=direction, avg_change=round(avg_change, 2) ) def generate_hotspots(grids: List[Dict], districts: List[Dict], limit: int = 10) -> List[InsightHotspot]: """Generate hotspot areas from grid data""" high_risk_grids = [g for g in grids if g["risk_value"] >= 0.7] high_risk_grids.sort(key=lambda x: x["risk_value"], reverse=True) hotspots = [] for grid in high_risk_grids[:limit]: lat = grid["latitude"] lon = grid["longitude"] region = "武汉市" street = grid.get("street", f"Grid {grid['grid_id']}") if districts: for district in districts: if point_in_polygon(lat, lon, district["coordinates"]): region = district["name"] break days_high = random.randint(1, 7) hotspots.append( InsightHotspot( grid_id=grid["grid_id"], latitude=lat, longitude=lon, risk_value=grid["risk_value"], risk_level="high" if grid["risk_value"] >= RISK_HIGH else "medium_high", region=region, street=street, population_density=grid.get("population_density", 5000.0), days_in_high_risk=days_high ) ) return hotspots def generate_correlations(avg_risk: float, risk_variance: float) -> List[InsightCorrelation]: """Generate correlation factors for insights""" correlations = [ InsightCorrelation( factor="temperature", correlation=round(-0.45 - 0.1 * (avg_risk - 0.5), 3), significance="high" if risk_variance > 0.05 else "medium", description="Temperature vs risk: Lower temps correlate with higher risk", impact="negative" ), InsightCorrelation( factor="humidity", correlation=round(0.32 + 0.15 * (avg_risk - 0.5), 3), significance="medium", description="Humidity vs risk: Higher humidity slightly increases risk", impact="positive" ), InsightCorrelation( factor="PM2.5", correlation=round(0.58 + 0.1 * (avg_risk - 0.5), 3), significance="high", description="PM2.5 vs risk: Strong positive correlation with air pollution", impact="positive" ), InsightCorrelation( factor="PM10", correlation=round(0.51 + 0.08 * (avg_risk - 0.5), 3), significance="high", description="PM10 vs risk: Moderate positive correlation", impact="positive" ), InsightCorrelation( factor="wind_speed", correlation=round(-0.28 - 0.05 * (avg_risk - 0.5), 3), significance="low", description="Wind speed vs risk: Higher wind disperses pollutants", impact="negative" ), InsightCorrelation( factor="population_density", correlation=round(0.42 + 0.12 * (avg_risk - 0.5), 3), significance="high", description="Population density vs risk: Dense areas show higher transmission", impact="positive" ), ] return correlations def generate_demographics(total_grids: int, avg_risk: float) -> List[InsightDemographic]: """Generate demographic breakdown for insights""" base_cases = int(total_grids * avg_risk * 10) demographics = [ InsightDemographic( age_group="0-14", case_count=int(base_cases * 0.15), percentage=15.0, risk_ratio=round(0.8 + random.uniform(-0.1, 0.1), 2) ), InsightDemographic( age_group="15-44", case_count=int(base_cases * 0.35), percentage=35.0, risk_ratio=round(1.0 + random.uniform(-0.1, 0.1), 2) ), InsightDemographic( age_group="45-64", case_count=int(base_cases * 0.30), percentage=30.0, risk_ratio=round(1.2 + random.uniform(-0.1, 0.1), 2) ), InsightDemographic( age_group="65+", case_count=int(base_cases * 0.20), percentage=20.0, risk_ratio=round(1.5 + random.uniform(-0.1, 0.1), 2) ), ] return demographics def generate_summary(trend: InsightTrend, hotspots: List[InsightHotspot], correlations: List[InsightCorrelation]) -> str: """Generate AI-style summary of insights""" trend_text = "stable" if trend.direction == "up": trend_text = f"increasing ({trend.avg_change:.1f}% daily)" elif trend.direction == "down": trend_text = f"decreasing ({trend.avg_change:.1f}% daily)" hotspot_count = len([h for h in hotspots if h.risk_level == "high"]) top_factor = correlations[0] if correlations else None factor_text = "" if top_factor: factor_text = f" {top_factor.factor} shows the strongest correlation ({top_factor.correlation:.2f})." summary = ( f"Over the past {trend.period}, risk levels have been {trend_text}. " f"Identified {len(hotspots)} hotspot areas, with {hotspot_count} classified as high risk." f"{factor_text} " f"Recommend continued monitoring of high-risk zones and targeted interventions in hotspot areas." ) return summary @router.get("/overview", response_model=InsightsResponse) async def get_insights_overview( days: int = Query(default=7, ge=1, le=30, description="Number of days for trend analysis"), hotspot_limit: int = Query(default=10, ge=1, le=50, description="Maximum number of hotspots to return"), ): """ Get comprehensive insights overview Args: days: Number of days for trend analysis (1-30) hotspot_limit: Maximum number of hotspots to return (1-50) Returns: Comprehensive insights including trends, hotspots, correlations, and demographics """ latest_date = get_latest_date() filepath = DATA_DIR / f"risk_{latest_date}.geojson" if not filepath.exists(): raise HTTPException(status_code=404, detail=f"No data found for date {latest_date}") grids = parse_geojson_file(filepath) districts = load_districts() if not grids: raise HTTPException(status_code=404, detail="No grid data found") avg_risk = sum(g["risk_value"] for g in grids) / len(grids) risk_variance = sum((g["risk_value"] - avg_risk) ** 2 for g in grids) / len(grids) trend = generate_trend_data(days, avg_risk) hotspots = generate_hotspots(grids, districts, hotspot_limit) correlations = generate_correlations(avg_risk, risk_variance) demographics = generate_demographics(len(grids), avg_risk) summary = generate_summary(trend, hotspots, correlations) return InsightsResponse( trend=trend, hotspots=hotspots, correlations=correlations, demographics=demographics, summary=summary, timestamp=datetime.now().isoformat() ) @router.get("/trend", response_model=InsightTrend) async def get_insights_trend( days: int = Query(default=7, ge=1, le=30, description="Number of days for trend"), ): """ Get risk trend analysis Args: days: Number of days for trend analysis (1-30) Returns: Trend data with direction and average change """ latest_date = get_latest_date() filepath = DATA_DIR / f"risk_{latest_date}.geojson" if not filepath.exists(): raise HTTPException(status_code=404, detail=f"No data found for date {latest_date}") grids = parse_geojson_file(filepath) if not grids: raise HTTPException(status_code=404, detail="No grid data found") avg_risk = sum(g["risk_value"] for g in grids) / len(grids) return generate_trend_data(days, avg_risk) @router.get("/hotspots", response_model=List[InsightHotspot]) async def get_insights_hotspots( limit: int = Query(default=10, ge=1, le=50, description="Maximum hotspots to return"), min_risk: float = Query(default=0.7, ge=0.0, le=1.0, description="Minimum risk threshold"), ): """ Get hotspot areas with high risk levels Args: limit: Maximum number of hotspots to return (1-50) min_risk: Minimum risk value threshold (0.0-1.0) Returns: List of hotspot areas sorted by risk value """ latest_date = get_latest_date() filepath = DATA_DIR / f"risk_{latest_date}.geojson" if not filepath.exists(): raise HTTPException(status_code=404, detail=f"No data found for date {latest_date}") grids = parse_geojson_file(filepath) districts = load_districts() if not grids: raise HTTPException(status_code=404, detail="No grid data found") high_risk_grids = [g for g in grids if g["risk_value"] >= min_risk] high_risk_grids.sort(key=lambda x: x["risk_value"], reverse=True) return generate_hotspots(grids, districts, limit) @router.get("/correlations", response_model=List[InsightCorrelation]) async def get_insights_correlations(): """ Get weather and environmental correlation factors Returns: List of correlation factors with coefficients and significance """ latest_date = get_latest_date() filepath = DATA_DIR / f"risk_{latest_date}.geojson" if not filepath.exists(): raise HTTPException(status_code=404, detail=f"No data found for date {latest_date}") grids = parse_geojson_file(filepath) if not grids: raise HTTPException(status_code=404, detail="No grid data found") avg_risk = sum(g["risk_value"] for g in grids) / len(grids) risk_variance = sum((g["risk_value"] - avg_risk) ** 2 for g in grids) / len(grids) return generate_correlations(avg_risk, risk_variance) @router.get("/demographics", response_model=List[InsightDemographic]) async def get_insights_demographics(): """ Get demographic breakdown of risk Returns: Demographic breakdown by age groups """ latest_date = get_latest_date() filepath = DATA_DIR / f"risk_{latest_date}.geojson" if not filepath.exists(): raise HTTPException(status_code=404, detail=f"No data found for date {latest_date}") grids = parse_geojson_file(filepath) if not grids: raise HTTPException(status_code=404, detail="No grid data found") avg_risk = sum(g["risk_value"] for g in grids) / len(grids) return generate_demographics(len(grids), avg_risk)