Files
CA/backend/routers/cases.py

319 lines
9.8 KiB
Python
Raw Normal View History

"""
医疗病例数据 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)