""" 医疗病例数据 API 路由 提供门诊和住院数据的统计、趋势、区域分布等接口 """ from fastapi import APIRouter, HTTPException, Query from pydantic import BaseModel from typing import Optional from datetime import datetime, date import pandas as pd import json from data.case_loader import load_data, get_combined_data, get_outpatient_data, get_inpatient_data, WUHAN_DISTRICTS, DATE_PATTERN router = APIRouter(prefix="/api/cases", tags=["cases"]) # ============== Response Models ============== class StatsResponse(BaseModel): """统计数据响应""" total_outpatient: int total_inpatient: int date_range: dict top_districts: list top_diagnoses: list class TrendPoint(BaseModel): """趋势数据点""" date: str outpatient: int inpatient: int total: int class TrendResponse(BaseModel): """趋势数据响应""" trend: list[TrendPoint] summary: dict class DistrictData(BaseModel): """区域数据""" district: str outpatient: int inpatient: int total: int outpatient_ratio: float inpatient_ratio: float class DistrictsResponse(BaseModel): """区域分布响应""" districts: list[DistrictData] total: int class RealtimeData(BaseModel): """实时数据""" today_outpatient: int today_inpatient: int today_total: int last_7d_avg: int change_ratio: float status: str # ============== API Endpoints ============== @router.get("/stats", response_model=StatsResponse, summary="获取病例统计数据") async def get_cases_stats( diagnosis: Optional[str] = Query(None, description="Filter to single disease stats"), ): """ 获取病例总体统计信息 - 总门诊量、总住院量 - 数据日期范围 - 就诊量前 10 的区域 - 最常见诊断前 10 """ load_data() df_out = get_outpatient_data() df_in = get_inpatient_data() # 诊断过滤 if diagnosis: df_out = df_out[df_out['初诊'].str.contains(diagnosis, na=False, case=False)] df_in = df_in[df_in['诊断名称'].str.contains(diagnosis, na=False, case=False)] # 计算统计 total_outpatient = len(df_out) total_inpatient = len(df_in) # 日期范围 min_date = min(df_out['date'].min(), df_in['date'].min()) max_date = max(df_out['date'].max(), df_in['date'].max()) # 区域统计 out_districts = df_out[df_out['district'] != '未知']['district'].value_counts().head(10) in_districts = df_in[df_in['district'] != '其他']['district'].value_counts().head(10) combined_districts = pd.concat([out_districts, in_districts]).groupby(level=0).sum().nlargest(10) top_districts = [{"district": d, "count": int(c)} for d, c in combined_districts.items()] # 诊断统计 out_diagnoses = df_out['初诊'].value_counts().head(10) in_diagnoses = df_in['诊断名称'].value_counts().head(10) top_diagnoses = [ {"diagnosis": str(d), "outpatient": int(out_diagnoses.get(d, 0)), "inpatient": int(in_diagnoses.get(d, 0))} for d in set(list(out_diagnoses.index[:5]) + list(in_diagnoses.index[:5])) ][:10] return StatsResponse( total_outpatient=total_outpatient, total_inpatient=total_inpatient, date_range={ "start": min_date.strftime("%Y-%m-%d"), "end": max_date.strftime("%Y-%m-%d") }, top_districts=top_districts, top_diagnoses=top_diagnoses ) @router.get("/trend", response_model=TrendResponse, summary="获取病例趋势数据") async def get_cases_trend( start_date: Optional[str] = Query(None, description="开始日期 (YYYY-MM-DD)"), end_date: Optional[str] = Query(None, description="结束日期 (YYYY-MM-DD)"), group_by: str = Query("day", description="分组粒度:day, week, month"), diagnosis: Optional[str] = Query(None, description="Filter by diagnosis name"), ): """ 获取病例时间趋势数据 - 支持按日、周、月分组 - 可指定日期范围 - 返回门诊、住院、总计趋势 """ if start_date and not DATE_PATTERN.match(start_date): raise HTTPException(status_code=400, detail="Invalid start_date format. Use YYYY-MM-DD") if end_date and not DATE_PATTERN.match(end_date): raise HTTPException(status_code=400, detail="Invalid end_date format. Use YYYY-MM-DD") df = get_combined_data() # 日期过滤 if start_date: df = df[df['date'] >= pd.to_datetime(start_date)] if end_date: df = df[df['date'] <= pd.to_datetime(end_date)] # 诊断过滤 if diagnosis: df = df[df['diagnosis'].str.contains(diagnosis, na=False, case=False)] # 分组 if group_by == "week": df['period'] = df['date'].dt.to_period('W').dt.start_time elif group_by == "month": df['period'] = df['date'].dt.to_period('M').dt.start_time else: df['period'] = df['date'].dt.date # 聚合 out_trend = df[df['type'] == 'outpatient'].groupby('period').size() in_trend = df[df['type'] == 'inpatient'].groupby('period').size() periods = sorted(set(out_trend.index.tolist() + in_trend.index.tolist())) trend = [] total_out = total_in = 0 for p in periods: out_count = int(out_trend.get(p, 0)) in_count = int(in_trend.get(p, 0)) total_out += out_count total_in += in_count trend.append(TrendPoint( date=pd.Timestamp(p).strftime("%Y-%m-%d"), outpatient=out_count, inpatient=in_count, total=out_count + in_count )) return TrendResponse( trend=trend, summary={ "total_outpatient": total_out, "total_inpatient": total_in, "period_count": len(periods), "avg_daily_outpatient": round(total_out / max(len(periods), 1), 2), "avg_daily_inpatient": round(total_in / max(len(periods), 1), 2), } ) @router.get("/districts", response_model=DistrictsResponse, summary="获取区域分布数据") async def get_cases_districts( case_type: Optional[str] = Query(None, description="病例类型:outpatient, inpatient, all"), min_count: int = Query(10, description="最小病例数过滤"), diagnosis: Optional[str] = Query(None, description="Filter by diagnosis name"), ): """ 获取病例区域分布数据 - 支持按病例类型筛选 - 可设置最小病例数过滤 - 返回各区门诊、住院量及占比 """ df = get_combined_data() # 诊断过滤 if diagnosis: df = df[df['diagnosis'].str.contains(diagnosis, na=False, case=False)] # 类型过滤 if case_type == "outpatient": df = df[df['type'] == 'outpatient'] elif case_type == "inpatient": df = df[df['type'] == 'inpatient'] # 过滤未知区域 df = df[(df['district'] != '未知') & (df['district'] != '其他')] # 聚合 district_stats = df.groupby(['district', 'type']).size().unstack(fill_value=0) if 'outpatient' not in district_stats.columns: district_stats['outpatient'] = 0 if 'inpatient' not in district_stats.columns: district_stats['inpatient'] = 0 district_stats['total'] = district_stats['outpatient'] + district_stats['inpatient'] # 过滤 district_stats = district_stats[district_stats['total'] >= min_count] district_stats = district_stats.sort_values('total', ascending=False) total = int(district_stats['total'].sum()) districts = [] for district, row in district_stats.iterrows(): districts.append(DistrictData( district=district, outpatient=int(row['outpatient']), inpatient=int(row['inpatient']), total=int(row['total']), outpatient_ratio=round(row['outpatient'] / row['total'] * 100, 2) if row['total'] > 0 else 0, inpatient_ratio=round(row['inpatient'] / row['total'] * 100, 2) if row['total'] > 0 else 0 )) return DistrictsResponse(districts=districts, total=total) @router.get("/realtime", response_model=RealtimeData, summary="获取实时数据") async def get_cases_realtime(): """ 获取实时病例数据 - 今日就诊量 - 近 7 日平均值 - 变化率 - 状态评估 (正常/偏高/偏低) """ df = get_combined_data() today = pd.Timestamp.today().normalize() last_7d = today - pd.Timedelta(days=7) # 今日数据 today_data = df[df['date'] >= today] today_total = len(today_data) today_out = len(today_data[today_data['type'] == 'outpatient']) today_in = len(today_data[today_data['type'] == 'inpatient']) # 近 7 日平均 last_7d_data = df[(df['date'] >= last_7d) & (df['date'] < today)] last_7d_avg = round(len(last_7d_data) / 7, 2) if len(last_7d_data) > 0 else 0 # 变化率 if last_7d_avg > 0: change_ratio = round((today_total - last_7d_avg) / last_7d_avg * 100, 2) else: change_ratio = 0.0 # 状态评估 if change_ratio > 20: status = "偏高" elif change_ratio < -20: status = "偏低" else: status = "正常" return RealtimeData( today_outpatient=today_out, today_inpatient=today_in, today_total=today_total, last_7d_avg=last_7d_avg, change_ratio=change_ratio, status=status ) class DiagnosesResponse(BaseModel): """诊断列表响应""" diagnoses: list[str] @router.get("/diagnoses", response_model=DiagnosesResponse, summary="获取所有诊断名称列表") async def get_diagnoses(): """Returns deduplicated, sorted list of unique diagnosis names""" df = get_combined_data() diagnoses = sorted(df['diagnosis'].dropna().unique().tolist()) return DiagnosesResponse(diagnoses=diagnoses)