Data_Analysis/analytics-demo-web/src/lib/comprehensive-insights.ts

410 lines
19 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.

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<T extends string | number>(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<string, Builder> = {
'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<string, number>()
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<InsightDimension, InsightRow[]> {
const init: Record<InsightDimension, InsightRow[]> = { : [], : [], : [], : [] }
for (const r of rows) init[r.dimension].push(r)
return init
}