From fe8bed58f5818d5959a5b65bf3aa1e115d5a7b19 Mon Sep 17 00:00:00 2001 From: Akiba So Date: Sun, 21 Jun 2026 21:50:30 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20remove=20cost/=E8=B4=B9=E7=94=A8=20stat?= =?UTF-8?q?istics=20from=20clinical=20analytics?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- backend/routers/statistics.py | 59 +----------------- frontend/e2e/clinical.spec.ts | 14 ----- .../components/clinical/ClinicalKpiRow.tsx | 11 +--- .../clinical/CostByDiseaseChart.tsx | 62 ------------------- .../components/clinical/CostVsLosScatter.tsx | 55 ---------------- .../components/clinical/HistogramChart.tsx | 4 +- .../src/components/clinical/chartColors.ts | 2 - frontend/src/pages/ClinicalAnalysis.tsx | 19 +----- frontend/src/services/api.ts | 4 -- 9 files changed, 8 insertions(+), 222 deletions(-) delete mode 100644 frontend/src/components/clinical/CostByDiseaseChart.tsx delete mode 100644 frontend/src/components/clinical/CostVsLosScatter.tsx diff --git a/backend/routers/statistics.py b/backend/routers/statistics.py index f260dca..e3376bc 100644 --- a/backend/routers/statistics.py +++ b/backend/routers/statistics.py @@ -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, diff --git a/frontend/e2e/clinical.spec.ts b/frontend/e2e/clinical.spec.ts index 958833f..a04c1de 100644 --- a/frontend/e2e/clinical.spec.ts +++ b/frontend/e2e/clinical.spec.ts @@ -10,7 +10,6 @@ const CLINICAL_FIXTURE = { kpis: { total_admissions: 5822, median_los_days: 4, - mean_cost: 6294, cure_rate: 0.991, emergency_admit_ratio: 0.47, }, @@ -23,19 +22,6 @@ const CLINICAL_FIXTURE = { { diagnosis: '肺炎', p25: 3, median: 5, p75: 7, n: 800 }, { diagnosis: '支气管炎', p25: 2, median: 4, p75: 6, n: 600 }, ], - cost_histogram: [ - { bin_label: '0-3k', count: 1800 }, - { bin_label: '3k-6k', count: 2200 }, - ], - cost_by_disease: [ - { diagnosis: '肺炎', mean_cost: 7200, n: 800 }, - { diagnosis: '支气管炎', mean_cost: 5100, n: 600 }, - ], - cost_vs_los: [ - { los: 3, cost: 5000 }, - { los: 5, cost: 7200 }, - { los: 7, cost: 9100 }, - ], outcome_counts: [ { outcome: '治愈', count: 3474 }, { outcome: '好转', count: 2298 }, diff --git a/frontend/src/components/clinical/ClinicalKpiRow.tsx b/frontend/src/components/clinical/ClinicalKpiRow.tsx index b49c442..cc86811 100644 --- a/frontend/src/components/clinical/ClinicalKpiRow.tsx +++ b/frontend/src/components/clinical/ClinicalKpiRow.tsx @@ -1,5 +1,5 @@ import { memo } from 'react'; -import { Users, CalendarDays, Wallet, HeartPulse, Siren } from 'lucide-react'; +import { Users, CalendarDays, HeartPulse, Siren } from 'lucide-react'; import { StatCard } from '@/components/StatCard'; import { TESTIDS } from '@/utils/testids'; import type { InpatientClinicalResponse } from '@/services/api'; @@ -8,12 +8,12 @@ interface ClinicalKpiRowProps { kpis: InpatientClinicalResponse['kpis']; } -/** 住院临床 5 项核心指标。375px 下 2 列,sm 起 5 列。 */ +/** 住院临床 4 项核心指标。375px 下 2 列,sm 起 4 列。 */ export const ClinicalKpiRow = memo(function ClinicalKpiRow({ kpis }: ClinicalKpiRowProps) { return (
} @@ -25,11 +25,6 @@ export const ClinicalKpiRow = memo(function ClinicalKpiRow({ kpis }: ClinicalKpi label="中位住院日" value={`${kpis.median_los_days} 天`} /> - } - label="人均费用" - value={`¥${Math.round(kpis.mean_cost).toLocaleString()}`} - /> } label="治愈好转率" diff --git a/frontend/src/components/clinical/CostByDiseaseChart.tsx b/frontend/src/components/clinical/CostByDiseaseChart.tsx deleted file mode 100644 index 81e1bf5..0000000 --- a/frontend/src/components/clinical/CostByDiseaseChart.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { memo } from 'react'; -import { - BarChart, - Bar, - XAxis, - YAxis, - CartesianGrid, - Tooltip, - ResponsiveContainer, -} from 'recharts'; -import { CLINICAL_COLORS, TOOLTIP_STYLE } from './chartColors'; - -interface CostByDiseaseChartProps { - data: { diagnosis: string; mean_cost: number; n: number }[]; -} - -function truncate(s: string, max: number): string { - return s.length > max ? s.slice(0, max) + '…' : s; -} - -/** 各病种平均费用横向柱状图。 */ -export const CostByDiseaseChart = memo(function CostByDiseaseChart({ - data, -}: CostByDiseaseChartProps) { - if (!data || data.length === 0) { - return
暂无数据
; - } - - const chartData = [...data] - .sort((a, b) => a.mean_cost - b.mean_cost) - .map((d) => ({ ...d, displayName: truncate(d.diagnosis, 8) })); - - return ( - - - - `¥${(v / 1000).toFixed(0)}k`} - /> - - [`¥${Math.round(v).toLocaleString()}`, '人均费用']} - /> - - - - ); -}); diff --git a/frontend/src/components/clinical/CostVsLosScatter.tsx b/frontend/src/components/clinical/CostVsLosScatter.tsx deleted file mode 100644 index d5007ea..0000000 --- a/frontend/src/components/clinical/CostVsLosScatter.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { memo } from 'react'; -import { - ScatterChart, - Scatter, - XAxis, - YAxis, - CartesianGrid, - Tooltip, - ResponsiveContainer, -} from 'recharts'; -import { CLINICAL_COLORS, TOOLTIP_STYLE } from './chartColors'; - -interface CostVsLosScatterProps { - data: { los: number; cost: number }[]; -} - -/** 费用 vs 住院天数散点。 */ -export const CostVsLosScatter = memo(function CostVsLosScatter({ data }: CostVsLosScatterProps) { - if (!data || data.length === 0) { - return
暂无数据
; - } - - return ( - - - - - `¥${(v / 1000).toFixed(0)}k`} - /> - - name === '费用' - ? [`¥${value.toLocaleString()}`, name] - : [`${value} 天`, name] - } - /> - - - - ); -}); diff --git a/frontend/src/components/clinical/HistogramChart.tsx b/frontend/src/components/clinical/HistogramChart.tsx index a06b9f2..6fb795d 100644 --- a/frontend/src/components/clinical/HistogramChart.tsx +++ b/frontend/src/components/clinical/HistogramChart.tsx @@ -13,11 +13,11 @@ import { CLINICAL_COLORS, TOOLTIP_STYLE } from './chartColors'; interface HistogramChartProps { data: { bin_label: string; count: number }[]; color?: string; - /** tooltip 中数量的标签,如 "住院天数" / "费用区间"。 */ + /** tooltip 中数量的标签,如 "住院天数"。 */ countLabel?: string; } -/** 通用直方图。复用于「住院天数分布」与「住院费用分布」。 */ +/** 通用直方图。用于「住院天数分布」。 */ export const HistogramChart = memo(function HistogramChart({ data, color = CLINICAL_COLORS.los, diff --git a/frontend/src/components/clinical/chartColors.ts b/frontend/src/components/clinical/chartColors.ts index 67d2762..155d5c6 100644 --- a/frontend/src/components/clinical/chartColors.ts +++ b/frontend/src/components/clinical/chartColors.ts @@ -5,8 +5,6 @@ export const CLINICAL_COLORS = { primary: '#2563EB', // primary los: '#2563EB', - cost: '#0891B2', // cyan — 费用维度 - scatter: '#7C3AED', // violet — 散点 box: '#3B82F6', // 箱体填充 boxMedian: '#1D4ED8', // 中位刻度 grid: '#E2E8F0', diff --git a/frontend/src/pages/ClinicalAnalysis.tsx b/frontend/src/pages/ClinicalAnalysis.tsx index 8a48f0c..68c7412 100644 --- a/frontend/src/pages/ClinicalAnalysis.tsx +++ b/frontend/src/pages/ClinicalAnalysis.tsx @@ -7,8 +7,6 @@ import { TESTIDS } from '@/utils/testids'; import { ClinicalKpiRow } from '@/components/clinical/ClinicalKpiRow'; import { HistogramChart } from '@/components/clinical/HistogramChart'; import { BoxPlotRows, type BoxRow } from '@/components/clinical/BoxPlotRows'; -import { CostVsLosScatter } from '@/components/clinical/CostVsLosScatter'; -import { CostByDiseaseChart } from '@/components/clinical/CostByDiseaseChart'; import { DonutChart, type DonutSlice } from '@/components/clinical/DonutChart'; import { CLINICAL_COLORS } from '@/components/clinical/chartColors'; @@ -58,7 +56,7 @@ export function ClinicalAnalysis() { 住院临床分析

- 住院天数、费用、出院结局与入院途径等临床特征分析 + 住院天数、出院结局、入院途径与年龄别 BMI 等临床特征分析

); @@ -140,21 +138,6 @@ export function ClinicalAnalysis() { - {/* 费用:分布 + 各病种平均费用 */} -
- - - - - - -
- - {/* 费用 vs 住院天数 散点 */} - - - - {/* 出院结局 + 入院途径 双环 */}
diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index 61e8053..b1f6cf6 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -348,15 +348,11 @@ export interface InpatientClinicalResponse { kpis: { total_admissions: number; median_los_days: number; - mean_cost: number; cure_rate: number; emergency_admit_ratio: number; }; los_histogram: { bin_label: string; count: number }[]; los_by_disease: { diagnosis: string; p25: number; median: number; p75: number; n: number }[]; - cost_histogram: { bin_label: string; count: number }[]; - cost_by_disease: { diagnosis: string; mean_cost: number; n: number }[]; - cost_vs_los: { los: number; cost: number }[]; outcome_counts: { outcome: string; count: number }[]; admission_route_counts: { route: string; count: number }[]; bmi_by_age_band: { age_band: string; p25: number; median: number; p75: number; n: number }[];