feat: remove cost/费用 statistics from clinical analytics

Per request — drop all monetary statistics (住院费用 is sensitive).
- Backend statistics.py: remove mean_cost KPI + cost_histogram / cost_by_disease
  / cost_vs_los from /inpatient-clinical (models, computation, response)
- Frontend: drop 人均费用 KPI card (now 4 KPIs), 住院费用分布, 各病种平均费用,
  费用×住院天数散点; delete CostByDiseaseChart + CostVsLosScatter components;
  trim statsApi type + e2e fixture + chartColors

Clinical page now: KPI(总人次/中位住院日/治愈好转率/急诊占比) + LOS dist + LOS-by-disease
box + outcome donut + admission-route donut + age-band BMI box.

Gates: backend 106 pytest · tsc 0 · build ok · clinical+user-flows e2e 19/19 ·
live endpoint confirmed cost-free

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-21 21:50:30 +08:00
parent 4df6c71628
commit fe8bed58f5
9 changed files with 8 additions and 222 deletions

View File

@@ -116,7 +116,6 @@ class KeyValueCount(BaseModel):
class InpatientKpis(BaseModel):
total_admissions: int
median_los_days: float
mean_cost: float
cure_rate: float
emergency_admit_ratio: float
@@ -129,17 +128,6 @@ class LosByDisease(BaseModel):
n: int
class CostByDisease(BaseModel):
diagnosis: str
mean_cost: float
n: int
class CostVsLos(BaseModel):
los: int
cost: float
class LabelCount(BaseModel):
outcome: Optional[str] = None
route: Optional[str] = None
@@ -168,9 +156,6 @@ class InpatientClinicalResponse(BaseModel):
kpis: InpatientKpis
los_histogram: list[KeyValueCount]
los_by_disease: list[LosByDisease]
cost_histogram: list[KeyValueCount]
cost_by_disease: list[CostByDisease]
cost_vs_los: list[CostVsLos]
outcome_counts: list[OutcomeCount]
admission_route_counts: list[RouteCount]
bmi_by_age_band: list[BmiByAge]
@@ -252,11 +237,10 @@ class TemporalResponse(BaseModel):
def _empty_inpatient_clinical() -> InpatientClinicalResponse:
return InpatientClinicalResponse(
kpis=InpatientKpis(
total_admissions=0, median_los_days=0.0, mean_cost=0.0,
total_admissions=0, median_los_days=0.0,
cure_rate=0.0, emergency_admit_ratio=0.0,
),
los_histogram=[], los_by_disease=[], cost_histogram=[],
cost_by_disease=[], cost_vs_los=[], outcome_counts=[],
los_histogram=[], los_by_disease=[], outcome_counts=[],
admission_route_counts=[], bmi_by_age_band=[],
)
@@ -287,8 +271,6 @@ def _compute_inpatient_clinical() -> InpatientClinicalResponse:
total = len(df)
median_los = float(df_los["los"].median()) if len(df_los) else 0.0
cost = pd.to_numeric(df["住院总费用"], errors="coerce")
mean_cost = float(cost.mean()) if cost.notna().any() else 0.0
outcome = df["出院情况"].fillna("未知")
cure_n = int(outcome.isin(["治愈", "好转"]).sum())
@@ -301,7 +283,6 @@ def _compute_inpatient_clinical() -> InpatientClinicalResponse:
kpis = InpatientKpis(
total_admissions=total,
median_los_days=round(median_los, 2),
mean_cost=round(mean_cost, 2),
cure_rate=round(cure_rate, 4),
emergency_admit_ratio=round(emerg_ratio, 4),
)
@@ -328,39 +309,6 @@ def _compute_inpatient_clinical() -> InpatientClinicalResponse:
n=int(len(grp)),
))
# Cost histogram: 0-2k,2-4k,4-6k,6-8k,8-10k,10k+
cost_valid = cost.dropna()
cost_bins = [(0, 2000, "0-2k"), (2000, 4000, "2-4k"), (4000, 6000, "4-6k"),
(6000, 8000, "6-8k"), (8000, 10000, "8-10k")]
cost_histogram: list[KeyValueCount] = []
for lo, hi, label in cost_bins:
cost_histogram.append(KeyValueCount(
bin_label=label, count=int(((cost_valid >= lo) & (cost_valid < hi)).sum())))
cost_histogram.append(KeyValueCount(bin_label="10k+", count=int((cost_valid >= 10000).sum())))
# Cost by disease (top 8 by n)
cost_by_disease: list[CostByDisease] = []
df_cost = df[cost.notna()].copy()
df_cost["_cost"] = cost[cost.notna()]
if len(df_cost):
top_cd = df_cost["诊断名称"].value_counts().head(8).index.tolist()
for d in top_cd:
grp = df_cost[df_cost["诊断名称"] == d]["_cost"]
cost_by_disease.append(CostByDisease(
diagnosis=str(d),
mean_cost=round(float(grp.mean()), 2),
n=int(len(grp)),
))
# cost vs los scatter (up to 500 points)
cost_vs_los: list[CostVsLos] = []
scatter_df = df_los[cost.reindex(df_los.index).notna()].copy()
scatter_df["_cost"] = cost.reindex(scatter_df.index)
if len(scatter_df) > 500:
scatter_df = scatter_df.sample(n=500, random_state=42)
for _, r in scatter_df.iterrows():
cost_vs_los.append(CostVsLos(los=int(r["los"]), cost=round(float(r["_cost"]), 2)))
# outcome counts
outcome_counts = [
OutcomeCount(outcome=str(k), count=int(v))
@@ -399,9 +347,6 @@ def _compute_inpatient_clinical() -> InpatientClinicalResponse:
kpis=kpis,
los_histogram=los_histogram,
los_by_disease=los_by_disease,
cost_histogram=cost_histogram,
cost_by_disease=cost_by_disease,
cost_vs_los=cost_vs_los,
outcome_counts=outcome_counts,
admission_route_counts=admission_route_counts,
bmi_by_age_band=bmi_by_age_band,