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:
@@ -116,7 +116,6 @@ class KeyValueCount(BaseModel):
|
|||||||
class InpatientKpis(BaseModel):
|
class InpatientKpis(BaseModel):
|
||||||
total_admissions: int
|
total_admissions: int
|
||||||
median_los_days: float
|
median_los_days: float
|
||||||
mean_cost: float
|
|
||||||
cure_rate: float
|
cure_rate: float
|
||||||
emergency_admit_ratio: float
|
emergency_admit_ratio: float
|
||||||
|
|
||||||
@@ -129,17 +128,6 @@ class LosByDisease(BaseModel):
|
|||||||
n: int
|
n: int
|
||||||
|
|
||||||
|
|
||||||
class CostByDisease(BaseModel):
|
|
||||||
diagnosis: str
|
|
||||||
mean_cost: float
|
|
||||||
n: int
|
|
||||||
|
|
||||||
|
|
||||||
class CostVsLos(BaseModel):
|
|
||||||
los: int
|
|
||||||
cost: float
|
|
||||||
|
|
||||||
|
|
||||||
class LabelCount(BaseModel):
|
class LabelCount(BaseModel):
|
||||||
outcome: Optional[str] = None
|
outcome: Optional[str] = None
|
||||||
route: Optional[str] = None
|
route: Optional[str] = None
|
||||||
@@ -168,9 +156,6 @@ class InpatientClinicalResponse(BaseModel):
|
|||||||
kpis: InpatientKpis
|
kpis: InpatientKpis
|
||||||
los_histogram: list[KeyValueCount]
|
los_histogram: list[KeyValueCount]
|
||||||
los_by_disease: list[LosByDisease]
|
los_by_disease: list[LosByDisease]
|
||||||
cost_histogram: list[KeyValueCount]
|
|
||||||
cost_by_disease: list[CostByDisease]
|
|
||||||
cost_vs_los: list[CostVsLos]
|
|
||||||
outcome_counts: list[OutcomeCount]
|
outcome_counts: list[OutcomeCount]
|
||||||
admission_route_counts: list[RouteCount]
|
admission_route_counts: list[RouteCount]
|
||||||
bmi_by_age_band: list[BmiByAge]
|
bmi_by_age_band: list[BmiByAge]
|
||||||
@@ -252,11 +237,10 @@ class TemporalResponse(BaseModel):
|
|||||||
def _empty_inpatient_clinical() -> InpatientClinicalResponse:
|
def _empty_inpatient_clinical() -> InpatientClinicalResponse:
|
||||||
return InpatientClinicalResponse(
|
return InpatientClinicalResponse(
|
||||||
kpis=InpatientKpis(
|
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,
|
cure_rate=0.0, emergency_admit_ratio=0.0,
|
||||||
),
|
),
|
||||||
los_histogram=[], los_by_disease=[], cost_histogram=[],
|
los_histogram=[], los_by_disease=[], outcome_counts=[],
|
||||||
cost_by_disease=[], cost_vs_los=[], outcome_counts=[],
|
|
||||||
admission_route_counts=[], bmi_by_age_band=[],
|
admission_route_counts=[], bmi_by_age_band=[],
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -287,8 +271,6 @@ def _compute_inpatient_clinical() -> InpatientClinicalResponse:
|
|||||||
|
|
||||||
total = len(df)
|
total = len(df)
|
||||||
median_los = float(df_los["los"].median()) if len(df_los) else 0.0
|
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("未知")
|
outcome = df["出院情况"].fillna("未知")
|
||||||
cure_n = int(outcome.isin(["治愈", "好转"]).sum())
|
cure_n = int(outcome.isin(["治愈", "好转"]).sum())
|
||||||
@@ -301,7 +283,6 @@ def _compute_inpatient_clinical() -> InpatientClinicalResponse:
|
|||||||
kpis = InpatientKpis(
|
kpis = InpatientKpis(
|
||||||
total_admissions=total,
|
total_admissions=total,
|
||||||
median_los_days=round(median_los, 2),
|
median_los_days=round(median_los, 2),
|
||||||
mean_cost=round(mean_cost, 2),
|
|
||||||
cure_rate=round(cure_rate, 4),
|
cure_rate=round(cure_rate, 4),
|
||||||
emergency_admit_ratio=round(emerg_ratio, 4),
|
emergency_admit_ratio=round(emerg_ratio, 4),
|
||||||
)
|
)
|
||||||
@@ -328,39 +309,6 @@ def _compute_inpatient_clinical() -> InpatientClinicalResponse:
|
|||||||
n=int(len(grp)),
|
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
|
||||||
outcome_counts = [
|
outcome_counts = [
|
||||||
OutcomeCount(outcome=str(k), count=int(v))
|
OutcomeCount(outcome=str(k), count=int(v))
|
||||||
@@ -399,9 +347,6 @@ def _compute_inpatient_clinical() -> InpatientClinicalResponse:
|
|||||||
kpis=kpis,
|
kpis=kpis,
|
||||||
los_histogram=los_histogram,
|
los_histogram=los_histogram,
|
||||||
los_by_disease=los_by_disease,
|
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,
|
outcome_counts=outcome_counts,
|
||||||
admission_route_counts=admission_route_counts,
|
admission_route_counts=admission_route_counts,
|
||||||
bmi_by_age_band=bmi_by_age_band,
|
bmi_by_age_band=bmi_by_age_band,
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ const CLINICAL_FIXTURE = {
|
|||||||
kpis: {
|
kpis: {
|
||||||
total_admissions: 5822,
|
total_admissions: 5822,
|
||||||
median_los_days: 4,
|
median_los_days: 4,
|
||||||
mean_cost: 6294,
|
|
||||||
cure_rate: 0.991,
|
cure_rate: 0.991,
|
||||||
emergency_admit_ratio: 0.47,
|
emergency_admit_ratio: 0.47,
|
||||||
},
|
},
|
||||||
@@ -23,19 +22,6 @@ const CLINICAL_FIXTURE = {
|
|||||||
{ diagnosis: '肺炎', p25: 3, median: 5, p75: 7, n: 800 },
|
{ diagnosis: '肺炎', p25: 3, median: 5, p75: 7, n: 800 },
|
||||||
{ diagnosis: '支气管炎', p25: 2, median: 4, p75: 6, n: 600 },
|
{ 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_counts: [
|
||||||
{ outcome: '治愈', count: 3474 },
|
{ outcome: '治愈', count: 3474 },
|
||||||
{ outcome: '好转', count: 2298 },
|
{ outcome: '好转', count: 2298 },
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { memo } from 'react';
|
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 { StatCard } from '@/components/StatCard';
|
||||||
import { TESTIDS } from '@/utils/testids';
|
import { TESTIDS } from '@/utils/testids';
|
||||||
import type { InpatientClinicalResponse } from '@/services/api';
|
import type { InpatientClinicalResponse } from '@/services/api';
|
||||||
@@ -8,12 +8,12 @@ interface ClinicalKpiRowProps {
|
|||||||
kpis: InpatientClinicalResponse['kpis'];
|
kpis: InpatientClinicalResponse['kpis'];
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 住院临床 5 项核心指标。375px 下 2 列,sm 起 5 列。 */
|
/** 住院临床 4 项核心指标。375px 下 2 列,sm 起 4 列。 */
|
||||||
export const ClinicalKpiRow = memo(function ClinicalKpiRow({ kpis }: ClinicalKpiRowProps) {
|
export const ClinicalKpiRow = memo(function ClinicalKpiRow({ kpis }: ClinicalKpiRowProps) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-testid={TESTIDS.clinicalKpis}
|
data-testid={TESTIDS.clinicalKpis}
|
||||||
className="grid grid-cols-2 sm:grid-cols-5 gap-3"
|
className="grid grid-cols-2 sm:grid-cols-4 gap-3"
|
||||||
>
|
>
|
||||||
<StatCard
|
<StatCard
|
||||||
icon={<Users className="w-4 h-4 text-primary" />}
|
icon={<Users className="w-4 h-4 text-primary" />}
|
||||||
@@ -25,11 +25,6 @@ export const ClinicalKpiRow = memo(function ClinicalKpiRow({ kpis }: ClinicalKpi
|
|||||||
label="中位住院日"
|
label="中位住院日"
|
||||||
value={`${kpis.median_los_days} 天`}
|
value={`${kpis.median_los_days} 天`}
|
||||||
/>
|
/>
|
||||||
<StatCard
|
|
||||||
icon={<Wallet className="w-4 h-4 text-primary" />}
|
|
||||||
label="人均费用"
|
|
||||||
value={`¥${Math.round(kpis.mean_cost).toLocaleString()}`}
|
|
||||||
/>
|
|
||||||
<StatCard
|
<StatCard
|
||||||
icon={<HeartPulse className="w-4 h-4 text-success" />}
|
icon={<HeartPulse className="w-4 h-4 text-success" />}
|
||||||
label="治愈好转率"
|
label="治愈好转率"
|
||||||
|
|||||||
@@ -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 <div className="text-center py-8 text-text-muted text-sm">暂无数据</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const chartData = [...data]
|
|
||||||
.sort((a, b) => a.mean_cost - b.mean_cost)
|
|
||||||
.map((d) => ({ ...d, displayName: truncate(d.diagnosis, 8) }));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ResponsiveContainer width="100%" height={Math.max(240, chartData.length * 34)}>
|
|
||||||
<BarChart
|
|
||||||
data={chartData}
|
|
||||||
layout="vertical"
|
|
||||||
margin={{ top: 5, right: 20, left: 12, bottom: 5 }}
|
|
||||||
>
|
|
||||||
<CartesianGrid strokeDasharray="3 3" stroke={CLINICAL_COLORS.grid} horizontal={false} />
|
|
||||||
<XAxis
|
|
||||||
type="number"
|
|
||||||
tick={{ fontSize: 10, fill: CLINICAL_COLORS.axis }}
|
|
||||||
tickFormatter={(v: number) => `¥${(v / 1000).toFixed(0)}k`}
|
|
||||||
/>
|
|
||||||
<YAxis
|
|
||||||
type="category"
|
|
||||||
dataKey="displayName"
|
|
||||||
tick={{ fontSize: 10, fill: CLINICAL_COLORS.axisLabel }}
|
|
||||||
width={72}
|
|
||||||
axisLine={false}
|
|
||||||
tickLine={false}
|
|
||||||
/>
|
|
||||||
<Tooltip
|
|
||||||
contentStyle={TOOLTIP_STYLE}
|
|
||||||
formatter={(v: number) => [`¥${Math.round(v).toLocaleString()}`, '人均费用']}
|
|
||||||
/>
|
|
||||||
<Bar dataKey="mean_cost" fill={CLINICAL_COLORS.cost} barSize={16} radius={[0, 3, 3, 0]} />
|
|
||||||
</BarChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
@@ -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 <div className="text-center py-8 text-text-muted text-sm">暂无数据</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ResponsiveContainer width="100%" height={300}>
|
|
||||||
<ScatterChart margin={{ top: 10, right: 16, left: 6, bottom: 16 }}>
|
|
||||||
<CartesianGrid strokeDasharray="3 3" stroke={CLINICAL_COLORS.grid} />
|
|
||||||
<XAxis
|
|
||||||
type="number"
|
|
||||||
dataKey="los"
|
|
||||||
name="住院天数"
|
|
||||||
unit="天"
|
|
||||||
tick={{ fontSize: 10, fill: CLINICAL_COLORS.axis }}
|
|
||||||
/>
|
|
||||||
<YAxis
|
|
||||||
type="number"
|
|
||||||
dataKey="cost"
|
|
||||||
name="费用"
|
|
||||||
tick={{ fontSize: 10, fill: CLINICAL_COLORS.axis }}
|
|
||||||
width={52}
|
|
||||||
tickFormatter={(v: number) => `¥${(v / 1000).toFixed(0)}k`}
|
|
||||||
/>
|
|
||||||
<Tooltip
|
|
||||||
contentStyle={TOOLTIP_STYLE}
|
|
||||||
cursor={{ strokeDasharray: '3 3' }}
|
|
||||||
formatter={(value: number, name: string) =>
|
|
||||||
name === '费用'
|
|
||||||
? [`¥${value.toLocaleString()}`, name]
|
|
||||||
: [`${value} 天`, name]
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Scatter data={data} fill={CLINICAL_COLORS.scatter} fillOpacity={0.5} />
|
|
||||||
</ScatterChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
@@ -13,11 +13,11 @@ import { CLINICAL_COLORS, TOOLTIP_STYLE } from './chartColors';
|
|||||||
interface HistogramChartProps {
|
interface HistogramChartProps {
|
||||||
data: { bin_label: string; count: number }[];
|
data: { bin_label: string; count: number }[];
|
||||||
color?: string;
|
color?: string;
|
||||||
/** tooltip 中数量的标签,如 "住院天数" / "费用区间"。 */
|
/** tooltip 中数量的标签,如 "住院天数"。 */
|
||||||
countLabel?: string;
|
countLabel?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 通用直方图。复用于「住院天数分布」与「住院费用分布」。 */
|
/** 通用直方图。用于「住院天数分布」。 */
|
||||||
export const HistogramChart = memo(function HistogramChart({
|
export const HistogramChart = memo(function HistogramChart({
|
||||||
data,
|
data,
|
||||||
color = CLINICAL_COLORS.los,
|
color = CLINICAL_COLORS.los,
|
||||||
|
|||||||
@@ -5,8 +5,6 @@
|
|||||||
export const CLINICAL_COLORS = {
|
export const CLINICAL_COLORS = {
|
||||||
primary: '#2563EB', // primary
|
primary: '#2563EB', // primary
|
||||||
los: '#2563EB',
|
los: '#2563EB',
|
||||||
cost: '#0891B2', // cyan — 费用维度
|
|
||||||
scatter: '#7C3AED', // violet — 散点
|
|
||||||
box: '#3B82F6', // 箱体填充
|
box: '#3B82F6', // 箱体填充
|
||||||
boxMedian: '#1D4ED8', // 中位刻度
|
boxMedian: '#1D4ED8', // 中位刻度
|
||||||
grid: '#E2E8F0',
|
grid: '#E2E8F0',
|
||||||
|
|||||||
@@ -7,8 +7,6 @@ import { TESTIDS } from '@/utils/testids';
|
|||||||
import { ClinicalKpiRow } from '@/components/clinical/ClinicalKpiRow';
|
import { ClinicalKpiRow } from '@/components/clinical/ClinicalKpiRow';
|
||||||
import { HistogramChart } from '@/components/clinical/HistogramChart';
|
import { HistogramChart } from '@/components/clinical/HistogramChart';
|
||||||
import { BoxPlotRows, type BoxRow } from '@/components/clinical/BoxPlotRows';
|
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 { DonutChart, type DonutSlice } from '@/components/clinical/DonutChart';
|
||||||
import { CLINICAL_COLORS } from '@/components/clinical/chartColors';
|
import { CLINICAL_COLORS } from '@/components/clinical/chartColors';
|
||||||
|
|
||||||
@@ -58,7 +56,7 @@ export function ClinicalAnalysis() {
|
|||||||
住院临床分析
|
住院临床分析
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-[12px] text-text-secondary">
|
<p className="text-[12px] text-text-secondary">
|
||||||
住院天数、费用、出院结局与入院途径等临床特征分析
|
住院天数、出院结局、入院途径与年龄别 BMI 等临床特征分析
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -140,21 +138,6 @@ export function ClinicalAnalysis() {
|
|||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 费用:分布 + 各病种平均费用 */}
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
||||||
<Card title="住院费用分布">
|
|
||||||
<HistogramChart data={data.cost_histogram ?? []} color={CLINICAL_COLORS.cost} countLabel="人次" />
|
|
||||||
</Card>
|
|
||||||
<Card title="各病种平均费用">
|
|
||||||
<CostByDiseaseChart data={data.cost_by_disease ?? []} />
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 费用 vs 住院天数 散点 */}
|
|
||||||
<Card title="费用 vs 住院天数">
|
|
||||||
<CostVsLosScatter data={data.cost_vs_los ?? []} />
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* 出院结局 + 入院途径 双环 */}
|
{/* 出院结局 + 入院途径 双环 */}
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
<Card title="出院结局构成">
|
<Card title="出院结局构成">
|
||||||
|
|||||||
@@ -348,15 +348,11 @@ export interface InpatientClinicalResponse {
|
|||||||
kpis: {
|
kpis: {
|
||||||
total_admissions: number;
|
total_admissions: number;
|
||||||
median_los_days: number;
|
median_los_days: number;
|
||||||
mean_cost: number;
|
|
||||||
cure_rate: number;
|
cure_rate: number;
|
||||||
emergency_admit_ratio: number;
|
emergency_admit_ratio: number;
|
||||||
};
|
};
|
||||||
los_histogram: { bin_label: string; count: number }[];
|
los_histogram: { bin_label: string; count: number }[];
|
||||||
los_by_disease: { diagnosis: string; p25: number; median: number; p75: number; n: 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 }[];
|
outcome_counts: { outcome: string; count: number }[];
|
||||||
admission_route_counts: { route: 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 }[];
|
bmi_by_age_band: { age_band: string; p25: number; median: number; p75: number; n: number }[];
|
||||||
|
|||||||
Reference in New Issue
Block a user