feat: add analysis pages and raster risk map
Ship a new app version with broader analytics, restructured dashboards, and a server-rendered risk map. Frontend: - Add Overview, Demographic, Disease, and Environmental Health analysis pages - Add AnomalyMarkers, CalendarHeatmap, and MetricHeatmapTable components - Rebuild Alerts map onto server-rendered raster risk tiles; expand Monitoring, Trend, and District Comparison views - Extend API client, stores, and TypeScript types Backend: - Add environment router (pollutants, lag correlations) - Add risk_raster util serving XYZ 100m risk tiles - Expand cases endpoints (demographics, seasonality, diagnoses) and insights; harden auth and file-based loaders Data & tooling: - Add processed outpatient/inpatient/combined case parquet (LFS) - Add nested CLAUDE.md guides, pyrightconfig, and test updates
This commit is contained in:
@@ -4,6 +4,7 @@ Provides comprehensive analytics, trends, hotspots, and correlations
|
||||
"""
|
||||
from fastapi import APIRouter, HTTPException, Query
|
||||
from datetime import datetime, timedelta
|
||||
from functools import lru_cache
|
||||
import random
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
@@ -20,6 +21,13 @@ from models import (
|
||||
)
|
||||
from utils.date_helpers import get_latest_date
|
||||
from utils.geojson import parse_geojson_file, load_districts
|
||||
|
||||
|
||||
@lru_cache(maxsize=8)
|
||||
def _cached_parquet(path_str: str):
|
||||
"""Load a parquet file once and reuse it (read-only) across requests."""
|
||||
import pandas as pd
|
||||
return pd.read_parquet(path_str)
|
||||
from utils.geo import point_in_polygon
|
||||
from utils.risk import calculate_trend as calculate_trend_direction
|
||||
|
||||
@@ -41,6 +49,7 @@ class InsightCardResponse(BaseModel):
|
||||
warning_count: int
|
||||
info_count: int
|
||||
success_count: int
|
||||
danger_count: int
|
||||
cards: list[InsightCardItem]
|
||||
|
||||
|
||||
@@ -482,7 +491,7 @@ async def get_insights_cards():
|
||||
|
||||
cases_path = PROJECT_ROOT / "processed" / "cases_by_district_daily.parquet"
|
||||
if cases_path.exists():
|
||||
cases_df = pd.read_parquet(cases_path)
|
||||
cases_df = _cached_parquet(str(cases_path))
|
||||
latest_case_date = cases_df["date"].max()
|
||||
latest_cases = cases_df[cases_df["date"] == latest_case_date].copy()
|
||||
latest_cases["base_district"] = latest_cases["district"].str.replace("区", "")
|
||||
@@ -533,7 +542,7 @@ async def get_insights_cards():
|
||||
grids_df["col"] = ((grids_df["longitude"] - MIN_LON) / STEP).astype(int)
|
||||
grids_df["grid_id"] = "r" + grids_df["row"].astype(str) + "_c" + grids_df["col"].astype(str)
|
||||
|
||||
mapping = pd.read_parquet(mapping_path)
|
||||
mapping = _cached_parquet(str(mapping_path))
|
||||
merged = grids_df.merge(mapping, on="grid_id", how="inner")
|
||||
|
||||
if len(merged) > 0:
|
||||
@@ -574,7 +583,7 @@ async def get_insights_cards():
|
||||
|
||||
weather_path = PROJECT_ROOT / "processed" / "weather" / "station_daily_2022.parquet"
|
||||
if weather_path.exists():
|
||||
weather_df = pd.read_parquet(weather_path)
|
||||
weather_df = _cached_parquet(str(weather_path))
|
||||
daily_wx = weather_df.groupby("date").agg(
|
||||
AQI=("AQI", "mean"), PM25=("PM25", "mean"), PM10=("PM10", "mean"),
|
||||
).reset_index()
|
||||
@@ -664,11 +673,13 @@ async def get_insights_cards():
|
||||
warning_count = sum(1 for c in cards if c.type == "warning")
|
||||
info_count = sum(1 for c in cards if c.type == "info")
|
||||
success_count = sum(1 for c in cards if c.type == "success")
|
||||
danger_count = sum(1 for c in cards if c.type == "danger")
|
||||
|
||||
return InsightCardResponse(
|
||||
total_insights=len(cards),
|
||||
warning_count=warning_count,
|
||||
info_count=info_count,
|
||||
success_count=success_count,
|
||||
danger_count=danger_count,
|
||||
cards=cards,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user