import type { AnalysisContext } from '../types/analysis' import { getAnalysisDefinition } from '../config/analysis-config' import { buildAeMonthlyByOccurrence, buildComplaintPareto, buildComplianceMatching, buildGlobalMonthlyPpm, buildInjuryDeviceMatrix, buildProvinceAeCounts, buildPsurTopProducts, buildReportingLagDays, buildSaeMonthlyByOccurrence, buildSaeShareMonthly, buildTopDeviceFailures, isSaeRecord, } from './aggregate' import { buildBatchComplaintVsAeCounts, buildComplaintBatchTop, buildComplaintCloseCycleDays, buildComplaintMonthly, buildCompensationDist, buildDefectConfirmedProductFault, buildFaultByRegisterMonthMatrix, buildHospitalAeVsAdmissionScatter, buildHospitalComplaintVsAdmissionQty, buildHospitalProductComplaintRate, buildInvestigationConclusionDist, buildIsAeRateByProduct, buildProductComplaintHalfYearGrowth, buildProvinceComplaintHalfYearGrowth, buildTopFaultTypes, buildTopHospitalsComplaints, buildTopProductsComplaints, } from './quality-aggregate' import { buildDealerComplaintRatePer1000, buildDealerOpWrongRatePer1000, buildOpWrongDealerCounts, buildOpWrongHospitalTop, buildProductOpWrongSharePct, buildProvinceComplaintCounts, buildProvinceComplaintRatePer1000, } from './marketing-aggregate' export type InsightDimension = '合规' | '质量' | '营销' | '事件实质' export interface InsightRow { dimension: InsightDimension pageTitle: string chartId: string chartTitle: string bullets: string[] } const PAGE_ORDER: { key: string; dimension: InsightDimension }[] = [ { key: 'compliance-psur', dimension: '合规' }, { key: 'compliance-audit', dimension: '合规' }, { key: 'quality-concentration', dimension: '质量' }, { key: 'quality-trends', dimension: '质量' }, { key: 'quality-complaint-sales', dimension: '质量' }, { key: 'quality-complaint-investigation', dimension: '质量' }, { key: 'quality-complaint-batch', dimension: '质量' }, { key: 'quality-ae-cause', dimension: '质量' }, { key: 'quality-ae-only', dimension: '质量' }, { key: 'quality-ae-with-complaint', dimension: '质量' }, { key: 'marketing-operation-training', dimension: '营销' }, { key: 'marketing-region-dealer', dimension: '营销' }, { key: 'substance-matrix', dimension: '事件实质' }, { key: 'substance-seasonality', dimension: '事件实质' }, ] function seriesPeak(labels: T[], values: number[], unit: string): string[] { if (!labels.length) return ['当前序列无有效数据点。'] let maxI = 0 let minI = 0 for (let i = 1; i < values.length; i++) { if (values[i] > values[maxI]) maxI = i if (values[i] < values[minI]) minI = i } const sum = values.reduce((a, b) => a + b, 0) const avg = sum / values.length const bullets = [ `统计范围内合计 ${sum}${unit},覆盖 ${labels.length} 个序列点。`, `峰值出现在「${labels[maxI]}」(${values[maxI]}${unit});低谷为「${labels[minI]}」(${values[minI]}${unit})。`, ] if (values[maxI] > avg * 1.25) bullets.push('峰值明显高于均值,建议结合下钻维度核查是否存在集中爆发或外部因素。') else bullets.push('波动相对温和,可继续跟踪后续周期是否突破历史区间。') return bullets } type Builder = (ctx: AnalysisContext) => string[] const INSIGHTS: Record = { 'psur-monthly': (ctx) => { const t = buildAeMonthlyByOccurrence(ctx) return seriesPeak(t.months, t.values, ' 条 AE') }, 'psur-ppm': (ctx) => { const p = buildGlobalMonthlyPpm(ctx) const pairs = p.months.map((m, i) => ({ m, v: p.ppm[i] })) const valid = pairs.filter((x) => x.v != null) as { m: string; v: number }[] if (!valid.length) return ['多数月份缺少入院量分母,PPM 不可算;需检查入院数据覆盖。'] const vals = valid.map((x) => x.v) const maxI = vals.indexOf(Math.max(...vals)) return [ `可计算 PPM 的月份共 ${valid.length} 个;最高 PPM 出现在 ${valid[maxI].m}(${valid[maxI].v})。`, 'PPM 同时受分子(AE 条数)与分母(CY Qty)波动影响,解读时需双因子对照。', ] }, 'psur-device-top': (ctx) => { const top = buildTopDeviceFailures(ctx, 10) if (!top.labels.length) return ['暂无器械故障字段数据。'] return [ `报告条数最多的器械故障表现为「${top.labels[0]}」(${top.values[0]} 条)。`, `Top3 合计约占当期可分类故障的显著份额,适合作为 PSUR「常见原因」叙述抓手。`, ] }, 'psur-product-top': (ctx) => { const t = buildPsurTopProducts(ctx) if (!t.labels.length) return ['暂无产品维度 AE。'] return [`AE 条数最高的产品为「${t.labels[0]}」(${t.values[0]} 条)。`, '建议与投诉侧同产品趋势对照阅读,区分监管通道与市场通道信号。'] }, 'psur-sae-monthly': (ctx) => { const s = buildSaeMonthlyByOccurrence(ctx) return seriesPeak(s.months, s.values, ' 条 SAE') }, 'psur-sae-share': (ctx) => { const s = buildSaeShareMonthly(ctx) const avg = s.shares.reduce((a, b) => a + b, 0) / s.shares.length const maxI = s.shares.indexOf(Math.max(...s.shares)) return [ `SAE 占全部 AE 比例月均约 ${avg.toFixed(1)}%。`, `占比峰值出现在 ${s.months[maxI]}(${s.shares[maxI]}%),需关注是否伴随总量上升(量率齐升)。`, ] }, 'psur-province': (ctx) => { const p = buildProvinceAeCounts(ctx) if (!p.labels.length) return ['暂无省级 AE 分布。'] return [ `AE 报告条数最多的省份为「${p.labels[0]}」(${p.values[0]} 条)。`, '省级绝对量需结合人口与装机/入院暴露进一步做率化,再判「热区」。', ] }, 'psur-lag-histogram': (ctx) => { const lags = buildReportingLagDays(ctx).filter((d) => d >= 0) if (!lags.length) return ['缺少可计算的滞后天数(审核日或登记日为空)。'] const mean = lags.reduce((a, b) => a + b, 0) / lags.length const ok = lags.filter((d) => d <= 15).length return [ `有效样本 ${lags.length} 条,上报滞后(审核−登记)均值约 ${mean.toFixed(1)} 天。`, `其中 ≤15 天约占 ${((ok / lags.length) * 100).toFixed(1)}%(演示阈值)。`, ] }, 'audit-summary-bar': (ctx) => { const m = buildComplianceMatching(ctx) return [ `应报样本(投诉标记不良事件)共 ${m.flaggedTotal} 条,疑似漏报率 ${m.leakRate}%,匹配率 ${m.matchRate}%。`, `匹配子集上,按登记与按 C3 起算的 ≤15 天占比分别为 ${m.timelinessFromRegistrationPct}% 与 ${m.timelinessFromC3Pct}%。`, ] }, 'audit-counts-pie': (ctx) => { const m = buildComplianceMatching(ctx) return [`已匹配 ${m.matched} 条,疑似漏报 ${m.missed} 条。`, '饼图结构用于审计抽样框说明;漏报清单需进入逐条调查闭环。'] }, 'quality-pareto-chart': (ctx) => { const p = buildComplaintPareto(ctx) if (!p.labels.length) return ['暂无产品×故障组合数据。'] return [ `贡献最大的组合为「${p.labels[0]}」(${p.bars[0]} 条投诉)。`, `Top 组合累计占比曲线末点约 ${p.line[p.line.length - 1]}%,可据此划定「80% 投诉池」优先改进范围。`, ] }, 'quality-batch-chart': (ctx) => { const b = buildComplaintBatchTop(ctx, 8) if (!b.labels.length) return ['暂无批号信息。'] return [`投诉集中度最高的批号为「${b.labels[0]}」(${b.values[0]} 条)。`, '建议对 Top 批号启动批次质量回溯并与 AE 批号对照。'] }, 'quality-hospital-top': (ctx) => { const t = buildTopHospitalsComplaints(ctx, 8) return [`投诉条数最多的医院为「${t.labels[0]}」(${t.values[0]} 条)。`, '医院端集中可与培训、跟台及备件策略联动。'] }, 'quality-product-fault-top': (ctx) => { const t = buildTopFaultTypes(ctx, 8) return [`频次最高的故障类型为「${t.labels[0]}」(${t.values[0]} 次)。`, '一维故障分布可与 Pareto 组合图交叉验证。'] }, 'trend-monthly': (ctx) => { const t = buildComplaintMonthly(ctx) return seriesPeak(t.months, t.values, ' 条投诉') }, 'trend-product-half': (ctx) => { const g = buildProductComplaintHalfYearGrowth(ctx) return [ `按上下半年对比,满足「增速≥20%」预警规则的产品共 ${g.length} 个(演示规则)。`, g.length ? `预警列表首项产品为「${g[0].product}」,建议结合 Pareto 与批号视图复核。` : '当前样本未触发产品增速预警,可结合更长窗口观察。', ] }, 'trend-product-alert': (ctx) => { const g = buildProductComplaintHalfYearGrowth(ctx) return [ g.length ? `预警列表首位产品为「${g[0].product}」,建议优先安排工程/质量联合复盘。` : '未识别到达阈的增速异常产品(或样本不足)。', ] }, 'trend-province-half': (ctx) => { const rows = buildProvinceComplaintHalfYearGrowth(ctx) const hit = rows.filter((r) => r.hit) return [ `共统计 ${rows.length} 个省份上下半年投诉量。`, hit.length ? `其中 ${hit.length} 个省下半年相对上半年增幅≥20%,需关注区域侧原因。` : '未识别省级增速达阈信号。', ] }, 'qc-sales-scatter': (ctx) => { const pts = buildHospitalComplaintVsAdmissionQty(ctx) const hi = [...pts].sort((a, b) => b.complaints - a.complaints)[0] return [ `共 ${pts.length} 家医院同时具有投诉与入院量数据。`, hi ? `投诉条数最多的是「${hi.hospital}」(${hi.complaints} 条,cyQty 汇总 ${hi.cyQty})。` : '', '散点偏离带可提示「相对入院量投诉偏多」的医院,需结合床位与产品结构复核。', ].filter(Boolean) }, 'qc-sales-rate': (ctx) => { const rows = buildHospitalProductComplaintRate(ctx) const top = rows[0] if (!top) return ['暂无医院×产品投诉率数据。'] return [ `医院×产品维度投诉率(件/千件)最高为「${top.hospital} / ${top.product}」(${top.ratePer1000})。`, '率化结果对入院量分母敏感,脚注需固定为 CY Qty 与对齐键版本。', ] }, 'qc-inv-pie': (ctx) => { const d = buildInvestigationConclusionDist(ctx) const top = d[0] return top ? [`调查结论占比最高为「${top[0]}」(${top[1]} 条)。`, '结构变化适合按季度监控,识别「产品缺陷成立」是否抬升。'] : ['暂无调查结论数据。'] }, 'qc-defect-bar': (ctx) => { const d = buildDefectConfirmedProductFault(ctx) return d.labels.length ? [`「产品缺陷成立」场景下,最高频组合为「${d.labels[0]}」(${d.values[0]} 条)。`, '该池子应进入 CAPA/设计变更输入优先级队列。'] : ['暂无产品缺陷成立记录。'] }, 'qc-comp-pie': (ctx) => { const d = buildCompensationDist(ctx) const top = d[0] return top ? [`赔付方式占比最高为「${top[0]}」(${top[1]} 次)。`, '可与缺陷成立子集交叉,观察质量失败外显成本结构。'] : ['暂无赔付数据。'] }, 'qc-product-top': (ctx) => { const t = buildTopProductsComplaints(ctx, 8) return [`投诉条数最多的产品为「${t.labels[0]}」(${t.values[0]} 条)。`, '与调查/赔付饼图联读可快速定位「问题产品」。'] }, 'qc-batch-ae': (ctx) => { const rows = buildBatchComplaintVsAeCounts(ctx).filter((r) => r.complaints + r.aeReports > 0) if (!rows.length) return ['暂无可对照的批号双通道数据。'] const top = [...rows].sort((a, b) => b.complaints + b.aeReports - (a.complaints + a.aeReports))[0] return [ `批号「${top.batchNo}」投诉 ${top.complaints} 条、AE ${top.aeReports} 条,合计通道压力最大(演示口径)。`, '双通道同升批号应优先纳入监管沟通与质量联合调查。', ] }, 'qc-close-cycle': (ctx) => { const days = buildComplaintCloseCycleDays(ctx) if (!days.length) return ['缺少关闭日期或登记日期,无法计算周期。'] const mean = days.reduce((a, b) => a + b, 0) / days.length return [`投诉关闭周期(关闭−登记)均值约 ${mean.toFixed(1)} 天。`, '分布右尾偏长通常与补件、调查反复或跨部门协同有关。'] }, 'ae-scatter-hosp': (ctx) => { const pts = buildHospitalAeVsAdmissionScatter(ctx) const hi = [...pts].sort((a, b) => b.aeCount - a.aeCount)[0] return [ `共 ${pts.length} 家医院具备 AE 与入院量对照数据。`, hi ? `AE 条数最多的是「${hi.hospital}」(${hi.aeCount} 条)。` : '', '该图为报告行为异常筛查视角,不等同于医院医疗质量差。', ].filter(Boolean) }, 'ae-upgrade-rate': (ctx) => { const rows = buildIsAeRateByProduct(ctx) const top = [...rows].sort((a, b) => b.ratePct - a.ratePct)[0] return top ? [`升级率(是否不良事件=是)最高的产品为「${top.product}」(${top.ratePct}%)。`, '跨 BU 对比可反映上报文化与流程差异。'] : ['暂无升级率数据。'] }, 'ae-only-failure': (ctx) => { const top = buildTopDeviceFailures(ctx, 8) return [`未并发投诉的 AE 中,器械故障 Top1 为「${top.labels[0]}」。`, '用于理解「仅监管通道」事件形态。'] }, 'ae-only-monthly': (ctx) => { const t = buildAeMonthlyByOccurrence(ctx) return [`AE 仅报告维度下,月度峰值仍在「${t.months[t.values.indexOf(Math.max(...t.values))]}」。`, '与投诉月度曲线对照时注意双时间轴。'] }, 'ae-with-fault': (ctx) => { const sub = ctx.complaints.filter((c) => c.isAe) const m = new Map() sub.forEach((c) => m.set(c.faultType, (m.get(c.faultType) ?? 0) + 1)) const sorted = [...m.entries()].sort((a, b) => b[1] - a[1]) return sorted.length ? [`已升级投诉中最高频故障为「${sorted[0][0]}」。`, '可与 AE 伤害×故障矩阵叙事衔接。'] : ['暂无已升级投诉。'] }, 'ae-with-monthly': (ctx) => { const sub = ctx.complaints.filter((c) => c.isAe) return [`当前样本中已升级投诉共 ${sub.length} 条。`, '登记月趋势用于观察升级投诉是否与活动期/政策窗口重叠。'] }, 'mkt-op-hospital': (ctx) => { const d = buildOpWrongHospitalTop(ctx, 14) return d.labels.length ? [`操作不当投诉最多的医院为「${d.labels[0]}」(${d.values[0]} 条)。`, '适合作为院内操作培训与跟台加强的优先名单。'] : ['无操作不当子集样本。'] }, 'mkt-op-dealer-count': (ctx) => { const d = buildOpWrongDealerCounts(ctx, 14) return d.labels.length ? [`操作不当投诉条数最多的经销商为「${d.labels[0]}」(${d.values[0]} 条)。`, '渠道侧可安排联合巡院与标准操作复核。'] : ['无操作不当子集样本。'] }, 'mkt-op-dealer-rate': (ctx) => { const d = buildDealerOpWrongRatePer1000(ctx, 14) return d.labels.length ? [`操作不当率(件/千件)最高的经销商为「${d.labels[0]}」(${d.values[0]})。`, '率化结果提示相对销量下的使用风险,不等于经销商储运问题。'] : ['无法计算经销商操作不当率(分母或分子不足)。'] }, 'mkt-op-product-share': (ctx) => { const d = buildProductOpWrongSharePct(ctx, 12) return d.labels.length ? [`操作不当占该产品全部投诉比例最高为「${d.labels[0]}」(${d.values[0]}%)。`, '说明书与数字化指导素材可优先迭代该产品。'] : ['无操作不当占比数据。'] }, 'mkt-rg-province-rate': (ctx) => { const d = buildProvinceComplaintRatePer1000(ctx, 16) return d.labels.length ? [`省级投诉率(件/千件)最高为「${d.labels[0]}」(${d.values[0]})。`, '需与绝对量图对照,避免大省误判。'] : ['暂无省级投诉率数据。'] }, 'mkt-rg-dealer-rate': (ctx) => { const d = buildDealerComplaintRatePer1000(ctx, 16) return d.labels.length ? [`经销商维度投诉率(件/千件)最高为「${d.labels[0]}」(${d.values[0]})。`, '可作为渠道辅导与考核的量化参考之一。'] : ['暂无经销商投诉率数据。'] }, 'mkt-rg-province-count': (ctx) => { const d = buildProvinceComplaintCounts(ctx, 14) return d.labels.length ? [`省级投诉绝对量最高为「${d.labels[0]}」(${d.values[0]} 条)。`, '与省级率图并列阅读,区分总量大与相对率高。'] : ['暂无省级投诉分布。'] }, 'matrix-all': (ctx) => { const { cells } = buildInjuryDeviceMatrix(ctx, false) if (!cells.length) return ['暂无伤害×故障交叉数据。'] const top = [...cells].sort((a, b) => b.value - a.value)[0] return [ `全量 AE 下最高频组合为「${top.injury} × ${top.device}」(${top.value} 条)。`, '热力图用于 CAPA 优先级与医学叙述,不等同因果结论。', ] }, 'matrix-sae': (ctx) => { const rows = ctx.ae.filter((r) => isSaeRecord(r)) const { cells } = buildInjuryDeviceMatrix(ctx, true) if (!cells.length) return ['SAE 子集下暂无有效组合。'] const top = [...cells].sort((a, b) => b.value - a.value)[0] return [ `当前 SAE 子集共 ${rows.length} 条;最高频伤害-故障组合为「${top.injury} × ${top.device}」。`, '建议与严重医学评审及对外沟通材料要点对齐。', ] }, 'seasonality-heatmap': (ctx) => { const { cells } = buildFaultByRegisterMonthMatrix(ctx) if (!cells.length) return ['暂无故障×月份数据。'] const top = [...cells].sort((a, b) => b.value - a.value)[0] return [ `投诉侧「故障×登记月」强度最高单元为「${top.fault} / ${top.month}」(${top.value} 条)。`, '探索性结论需结合气候/医院排班等外部变量再验证。', ] }, 'seasonality-monthly-total': (ctx) => { const t = buildComplaintMonthly(ctx) const maxI = t.values.indexOf(Math.max(...t.values)) return [`投诉总量峰值月份为 ${t.months[maxI]}(${t.values[maxI]} 条)。`, '与热力图行合计应对齐,用于读整体季节性背景。'] }, } export function buildComprehensiveInsightRows(context: AnalysisContext): InsightRow[] { const rows: InsightRow[] = [] for (const { key, dimension } of PAGE_ORDER) { const def = getAnalysisDefinition(key) if (!def) continue for (const sec of def.sections) { for (const chart of sec.charts) { if (chart.placeholder) continue if (!chart.optionBuilder) continue const builder = INSIGHTS[chart.id] const bullets = builder ? builder(context) : ['该图表已接入数据视图;结论规则尚未注册,可在 comprehensive-insights.ts 中补充。'] rows.push({ dimension, pageTitle: def.title, chartId: chart.id, chartTitle: chart.title, bullets, }) } } } return rows } export function groupInsightsByDimension(rows: InsightRow[]): Record { const init: Record = { 合规: [], 质量: [], 营销: [], 事件实质: [] } for (const r of rows) init[r.dimension].push(r) return init }