Files
CA/frontend/e2e/clinical.spec.ts
Akiba So 4df6c71628 feat: deep statistical analytics — clinical, symptoms, incidence, env correlation, weekday
Adds a substantial layer of data-backed statistics (all grounded in verified,
clean source data — no fabricated metrics).

Backend (new routers/statistics.py, prefix /api/stats; +106 pytest still green):
- /inpatient-clinical: LOS dist + by-disease quartiles, cost dist + by-disease +
  cost-vs-LOS, outcome counts, admission-route counts, BMI-by-age, KPIs
  (5822 admissions, median LOS 4d, mean ¥6294, cure 99.1%, emergency 47%)
- /symptoms: 主诉 keyword frequencies (发热/咳嗽/肺炎…) + revisit ratio (36%)
- /incidence-rate: per-10k-population standardized rate by district (cases ÷ pop)
- /env-correlation: pollutant×cases Pearson + 7×7 pairwise matrix + PM2.5 scatter
- /temporal: weekday distribution (+ month/yoy returned but UI omits them — data
  is December-only, so seasonality/YoY would be misleading)

Frontend:
- NEW 住院临床分析 page (/analysis/clinical, nav 临床分析): 9 charts + KPI row —
  LOS histogram + box-by-disease, cost histogram + scatter + by-disease, outcome
  donut (severity-colored), admission-route donut, age-band BMI box
- DiseaseAnalysis: 主诉症状词频 horizontal bar + revisit ratio
- DistrictComparison: 标化发病率(每万人)with 病例数↔发病率 toggle (rate is
  epidemiologically correct; raw counts mislead by population)
- EnvironmentalHealth: pollutant-cases correlation bar + 7×7 correlation heatmap +
  PM2.5×cases scatter with least-squares regression line
- TrendAnalysis: 星期就诊分布 + honest "data is December-only" note
- statsApi client + types

Gates: tsc 0 · build ok · functional e2e 43/43 (incl 2 new clinical) · verified
live against real backend data via dev proxy

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 21:42:52 +08:00

105 lines
3.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 住院临床分析页(/analysis/clinical验收测试。
* 与 user-flows.spec.ts 一致的鉴权策略addInitScript 注入 cbpoa_token
* 用 page.route 拦截 /api/**,对 inpatient-clinical 返回合法小样本,其余返回 {}。
*/
import { test, expect, Page } from '@playwright/test';
import { TESTIDS } from '../src/utils/testids';
const CLINICAL_FIXTURE = {
kpis: {
total_admissions: 5822,
median_los_days: 4,
mean_cost: 6294,
cure_rate: 0.991,
emergency_admit_ratio: 0.47,
},
los_histogram: [
{ bin_label: '1-2', count: 1200 },
{ bin_label: '3-4', count: 2100 },
{ bin_label: '5-7', count: 1500 },
],
los_by_disease: [
{ 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 },
{ outcome: '其他', count: 35 },
{ outcome: '未愈', count: 12 },
{ outcome: '死亡', count: 3 },
],
admission_route_counts: [
{ route: '急诊', count: 2700 },
{ route: '门诊', count: 3122 },
],
bmi_by_age_band: [
{ age_band: '0-2', p25: 14, median: 16, p75: 18, n: 400 },
{ age_band: '3-6', p25: 15, median: 17, p75: 19, n: 500 },
],
};
async function seedAuthAndMockApi(page: Page) {
await page.addInitScript(() => {
localStorage.setItem('cbpoa_token', 'e2e-test-token');
});
await page.route('/api/**', (route) => {
const url = route.request().url();
if (url.includes('/stats/inpatient-clinical')) {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(CLINICAL_FIXTURE),
});
return;
}
// 其余接口返回空对象,本页不依赖。
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({}),
});
});
}
test.describe('住院临床分析页', () => {
test.beforeEach(async ({ page }) => {
await seedAuthAndMockApi(page);
});
test('deep-link /analysis/clinical mounts page-clinical + clinical-kpis', async ({ page }) => {
await page.goto('/analysis/clinical');
await expect(page.locator(`[data-testid="${TESTIDS.pageClinical}"]`)).toBeVisible();
await expect(page.locator(`[data-testid="${TESTIDS.clinicalKpis}"]`)).toBeVisible();
});
test('no horizontal scroll at 375px', async ({ page }) => {
await page.setViewportSize({ width: 375, height: 812 });
await page.goto('/analysis/clinical');
await expect(page.locator(`[data-testid="${TESTIDS.pageClinical}"]`)).toBeVisible();
await expect(page.locator(`[data-testid="${TESTIDS.clinicalKpis}"]`)).toBeVisible();
const noHorizontalScroll = await page.evaluate(
() => document.documentElement.scrollWidth <= document.documentElement.clientWidth
);
expect(noHorizontalScroll).toBe(true);
});
});