Commit 35170610 by luoqi

feat(persona): 治疗史特征(C.3.1,读 canonical category)

- treatment_history 多标签:implant→种植史/orthodontic→正畸史/prosthodontic·cosmetic→修复史/
  periodontic→牙周治疗史。不标记 surgical/restorative/endodontic 等基础治疗。
-  spec 用病历/结算关键词匹配(数仓原始文本);PAC canonical 层已归一成 category →
  直接读,无关键词/无新摄入/无迁移。canonical 层价值体现。
- 注册表 spec/enum/label。本地 773 患者:牙周596/修复195/正畸165/种植120,多标签生效。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
parent 57838efe
...@@ -9,6 +9,7 @@ import { AcquisitionChannelFeatureExtractor } from './acquisition-channel.featur ...@@ -9,6 +9,7 @@ import { AcquisitionChannelFeatureExtractor } from './acquisition-channel.featur
import { FamilyStructureFeatureExtractor } from './family-structure.feature'; import { FamilyStructureFeatureExtractor } from './family-structure.feature';
import { ReferralChampionFeatureExtractor } from './referral-champion.feature'; import { ReferralChampionFeatureExtractor } from './referral-champion.feature';
import { LifecycleStageFeatureExtractor } from './lifecycle-stage.feature'; import { LifecycleStageFeatureExtractor } from './lifecycle-stage.feature';
import { TreatmentHistoryFeatureExtractor } from './treatment-history.feature';
/** /**
* FeatureRegistry — 收集所有 PersonaFeature 提取器。 * FeatureRegistry — 收集所有 PersonaFeature 提取器。
...@@ -30,9 +31,10 @@ export class FeatureRegistry { ...@@ -30,9 +31,10 @@ export class FeatureRegistry {
family: FamilyStructureFeatureExtractor, family: FamilyStructureFeatureExtractor,
referral: ReferralChampionFeatureExtractor, referral: ReferralChampionFeatureExtractor,
lifecycle: LifecycleStageFeatureExtractor, lifecycle: LifecycleStageFeatureExtractor,
treatmentHistory: TreatmentHistoryFeatureExtractor,
dnc: DoNotContactStatusFeatureExtractor, dnc: DoNotContactStatusFeatureExtractor,
entitlement: EntitlementStatusFeatureExtractor, entitlement: EntitlementStatusFeatureExtractor,
) { ) {
this.extractors = [rfm, age, gender, acquisition, family, referral, lifecycle, dnc, entitlement]; this.extractors = [rfm, age, gender, acquisition, family, referral, lifecycle, treatmentHistory, dnc, entitlement];
} }
} }
import { Injectable } from '@nestjs/common';
import { PersonaFeatureKey, FactType, FactKind } from '@pac/types';
import type {
ActiveFact,
FeatureExtractor,
FeatureExtractorContext,
PersonaFeatureDraft,
} from './feature.interface';
/**
* treatment_history 治疗史(C.3.1)— 规则层,snapshot · 多标签(允许并列)
*
* 标签:种植史 / 正畸史 / 修复史 / 牙周治疗史。历史核心治疗类型,影响后续推荐与维护。
*
* ⭐ spec 用"病历处置/结算关键词"匹配,因数仓是原始文本;PAC canonical 层已把治疗归一成
* treatment_record.content.category(单一收口)→ 直接读 category,无需关键词/新摄入:
* implant→种植史 / orthodontic→正畸史 / prosthodontic·cosmetic→修复史(冠桥/贴面/嵌体)/
* periodontic→牙周治疗史。
* 不标记:surgical(拔牙)/ restorative(充填)/ endodontic(根管)/ preventive·review·pediatric
* —— 基础治疗或非核心,对后续推荐影响有限(同 spec)。
* 仅看 actual(已做);四类全不命中 → 不打标签。
*/
@Injectable()
export class TreatmentHistoryFeatureExtractor implements FeatureExtractor {
readonly key = PersonaFeatureKey.TREATMENT_HISTORY;
// category → { 标签 code, 中文 };一个 code 可由多 category 命中(修复史=修复+美学)
private static readonly MAP: Record<string, { code: string; zh: string }> = {
implant: { code: 'implant_history', zh: '种植史' },
orthodontic: { code: 'ortho_history', zh: '正畸史' },
prosthodontic: { code: 'prostho_history', zh: '修复史' },
cosmetic: { code: 'prostho_history', zh: '修复史' },
periodontic: { code: 'perio_history', zh: '牙周治疗史' },
};
// 输出稳定顺序
private static readonly ORDER = ['implant_history', 'ortho_history', 'prostho_history', 'perio_history'];
private static readonly ZH: Record<string, string> = {
implant_history: '种植史',
ortho_history: '正畸史',
prostho_history: '修复史',
perio_history: '牙周治疗史',
};
extract(ctx: FeatureExtractorContext): PersonaFeatureDraft | null {
const txs = (ctx.factsByType.get(FactType.TREATMENT_RECORD) ?? []) as ActiveFact[];
const hit = new Set<string>();
const evidenceIds: string[] = [];
for (const f of txs) {
if (f.kind !== FactKind.ACTUAL) continue;
const cat = String((f.content as Record<string, unknown>).category ?? '');
const m = TreatmentHistoryFeatureExtractor.MAP[cat];
if (!m) continue;
hit.add(m.code);
evidenceIds.push(f.id);
}
if (hit.size === 0) return null;
const codes = TreatmentHistoryFeatureExtractor.ORDER.filter((c) => hit.has(c));
const labels = codes.map((c) => TreatmentHistoryFeatureExtractor.ZH[c]!);
return {
key: this.key,
description: labels.join(' / '),
score: null,
data: { types: codes, labels },
evidence: { factIds: evidenceIds },
};
}
}
...@@ -11,6 +11,7 @@ import { AcquisitionChannelFeatureExtractor } from './features/acquisition-chann ...@@ -11,6 +11,7 @@ import { AcquisitionChannelFeatureExtractor } from './features/acquisition-chann
import { FamilyStructureFeatureExtractor } from './features/family-structure.feature'; import { FamilyStructureFeatureExtractor } from './features/family-structure.feature';
import { ReferralChampionFeatureExtractor } from './features/referral-champion.feature'; import { ReferralChampionFeatureExtractor } from './features/referral-champion.feature';
import { LifecycleStageFeatureExtractor } from './features/lifecycle-stage.feature'; import { LifecycleStageFeatureExtractor } from './features/lifecycle-stage.feature';
import { TreatmentHistoryFeatureExtractor } from './features/treatment-history.feature';
@Module({ @Module({
controllers: [PersonaController], controllers: [PersonaController],
...@@ -25,6 +26,7 @@ import { LifecycleStageFeatureExtractor } from './features/lifecycle-stage.featu ...@@ -25,6 +26,7 @@ import { LifecycleStageFeatureExtractor } from './features/lifecycle-stage.featu
FamilyStructureFeatureExtractor, FamilyStructureFeatureExtractor,
ReferralChampionFeatureExtractor, ReferralChampionFeatureExtractor,
LifecycleStageFeatureExtractor, LifecycleStageFeatureExtractor,
TreatmentHistoryFeatureExtractor,
DoNotContactStatusFeatureExtractor, DoNotContactStatusFeatureExtractor,
EntitlementStatusFeatureExtractor, EntitlementStatusFeatureExtractor,
], ],
......
...@@ -388,6 +388,7 @@ export const PersonaFeatureKey = { ...@@ -388,6 +388,7 @@ export const PersonaFeatureKey = {
FAMILY_STRUCTURE: 'family_structure', // 家庭构成(单身/两口/多口/多代;PatientRelation 反推) FAMILY_STRUCTURE: 'family_structure', // 家庭构成(单身/两口/多口/多代;PatientRelation 反推)
REFERRAL_CHAMPION: 'referral_champion', // 转介绍达人(家庭型/社交型;recommend_num+转化) REFERRAL_CHAMPION: 'referral_champion', // 转介绍达人(家庭型/社交型;recommend_num+转化)
LIFECYCLE_STAGE: 'lifecycle_stage', // 生命周期(潜客..流失客;时间+消费规则) LIFECYCLE_STAGE: 'lifecycle_stage', // 生命周期(潜客..流失客;时间+消费规则)
TREATMENT_HISTORY: 'treatment_history', // 治疗史(种植/正畸/修复/牙周;读 treatment category)
// v1 候选(规则路径,业务方反馈后逐步上) // v1 候选(规则路径,业务方反馈后逐步上)
ENTITLEMENT_STATUS: 'entitlement_status', // 权益身份(商保直付 / 医保 / 储值 / 私行;事实投影型,史+最近日期) ENTITLEMENT_STATUS: 'entitlement_status', // 权益身份(商保直付 / 医保 / 储值 / 私行;事实投影型,史+最近日期)
......
...@@ -45,6 +45,7 @@ export const PERSONA_FEATURE_META: Record<string, { label: string; tone: Tone }> ...@@ -45,6 +45,7 @@ export const PERSONA_FEATURE_META: Record<string, { label: string; tone: Tone }>
[PersonaFeatureKey.FAMILY_STRUCTURE]: { label: '家庭构成', tone: 'amber' }, [PersonaFeatureKey.FAMILY_STRUCTURE]: { label: '家庭构成', tone: 'amber' },
[PersonaFeatureKey.REFERRAL_CHAMPION]: { label: '转介绍达人', tone: 'rose' }, [PersonaFeatureKey.REFERRAL_CHAMPION]: { label: '转介绍达人', tone: 'rose' },
[PersonaFeatureKey.LIFECYCLE_STAGE]: { label: '生命周期', tone: 'teal' }, [PersonaFeatureKey.LIFECYCLE_STAGE]: { label: '生命周期', tone: 'teal' },
[PersonaFeatureKey.TREATMENT_HISTORY]: { label: '治疗史', tone: 'amber' },
[PersonaFeatureKey.VALUE]: { label: '患者价值', tone: 'indigo' }, [PersonaFeatureKey.VALUE]: { label: '患者价值', tone: 'indigo' },
[PersonaFeatureKey.TREATMENT_CHAIN_STATUS]: { label: '治疗链状态', tone: 'amber' }, [PersonaFeatureKey.TREATMENT_CHAIN_STATUS]: { label: '治疗链状态', tone: 'amber' },
[PersonaFeatureKey.RECALL_RISK]: { label: '流失风险', tone: 'emerald' }, [PersonaFeatureKey.RECALL_RISK]: { label: '流失风险', tone: 'emerald' },
......
...@@ -195,4 +195,22 @@ export const PERSONA_FEATURE_SPECS: Record<string, PersonaFeatureSpec> = { ...@@ -195,4 +195,22 @@ export const PERSONA_FEATURE_SPECS: Record<string, PersonaFeatureSpec> = {
owner: 'pac-algo', owner: 'pac-algo',
version: 1, version: 1,
}, },
// ── C.3.1 治疗史(业务 CDP 口径;PAC 读 canonical category)──
treatment_history: {
key: 'treatment_history',
nameZh: '治疗史',
tier: 'rule',
timeSemantics: 'lifetime',
labelValues: ['种植史', '正畸史', '修复史', '牙周治疗史'],
dataSource: 'PAC treatment_record.content.category(canonical 已归一;spec 的"病历处置/结算关键词"在摄入层已收敛成 category)',
dataFields: ['medical_record_tag', 'settlement_records'],
meaning: '客户历史接受过的主要治疗类型,影响后续推荐与维护策略;允许并列,仅保留核心治疗类型',
algorithm: [
'implant→种植史;orthodontic→正畸史;prosthodontic/cosmetic→修复史(冠桥/贴面/嵌体);periodontic→牙周治疗史。',
'不标记 surgical(拔牙)/restorative(充填)/endodontic(根管)等基础治疗。仅 actual;全不命中→不打标签。',
].join('\n'),
owner: 'pac-algo',
version: 1,
},
}; };
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment