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>
This commit is contained in:
104
frontend/e2e/clinical.spec.ts
Normal file
104
frontend/e2e/clinical.spec.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* 住院临床分析页(/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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user