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:
@@ -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 },
|
||||
|
||||
@@ -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 (
|
||||
<div
|
||||
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
|
||||
icon={<Users className="w-4 h-4 text-primary" />}
|
||||
@@ -25,11 +25,6 @@ export const ClinicalKpiRow = memo(function ClinicalKpiRow({ kpis }: ClinicalKpi
|
||||
label="中位住院日"
|
||||
value={`${kpis.median_los_days} 天`}
|
||||
/>
|
||||
<StatCard
|
||||
icon={<Wallet className="w-4 h-4 text-primary" />}
|
||||
label="人均费用"
|
||||
value={`¥${Math.round(kpis.mean_cost).toLocaleString()}`}
|
||||
/>
|
||||
<StatCard
|
||||
icon={<HeartPulse className="w-4 h-4 text-success" />}
|
||||
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 {
|
||||
data: { bin_label: string; count: number }[];
|
||||
color?: string;
|
||||
/** tooltip 中数量的标签,如 "住院天数" / "费用区间"。 */
|
||||
/** tooltip 中数量的标签,如 "住院天数"。 */
|
||||
countLabel?: string;
|
||||
}
|
||||
|
||||
/** 通用直方图。复用于「住院天数分布」与「住院费用分布」。 */
|
||||
/** 通用直方图。用于「住院天数分布」。 */
|
||||
export const HistogramChart = memo(function HistogramChart({
|
||||
data,
|
||||
color = CLINICAL_COLORS.los,
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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() {
|
||||
住院临床分析
|
||||
</h1>
|
||||
<p className="text-[12px] text-text-secondary">
|
||||
住院天数、费用、出院结局与入院途径等临床特征分析
|
||||
住院天数、出院结局、入院途径与年龄别 BMI 等临床特征分析
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
@@ -140,21 +138,6 @@ export function ClinicalAnalysis() {
|
||||
</Card>
|
||||
</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">
|
||||
<Card title="出院结局构成">
|
||||
|
||||
@@ -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 }[];
|
||||
|
||||
Reference in New Issue
Block a user