export const MODEL_CATEGORIES = [ { id: "std", name: "标准A卡" }, { id: "bai", name: "白户A卡" }, { id: "big", name: "大额A卡" }, { id: "afd", name: "反欺诈评分" }, ] as const; export type ModelCategoryId = (typeof MODEL_CATEGORIES)[number]["id"]; export type ModelGrade = "A" | "B" | "C"; export type AbnormalLevel = "三级" | "二级" | "一级" | "正常"; export type ModelStatus = "正常" | "陪跑" | "陪跑结束" | "下线"; export type RankingStatus = "相符" | "不符" | "—"; export type ModelRecord = { bank: string; category: ModelCategoryId; name: string; modelId: string; version: string; status: ModelStatus; iteratedAt: string; ranking: RankingStatus; ks: number; psi: number; ksDrop: number; secondaryHits: number; wuji: boolean; processedAt: string | null; previousAdvice: string; commonModel: string | null; }; export type MonitoringRow = ModelRecord & { monitorMonth: string; }; export const MONITOR_MONTHS = [ "2026-02", "2026-03", "2026-04", "2026-05", "2026-06", "2026-07", ] as const; // 业务数据全部来自后端。以下空集合仅保留页面类型和计算函数的接口, // 在真实接口补齐对应字段前不再提供本地样例数据。 export const MODELS: ModelRecord[] = []; export const FEATURE_METRICS: Array<{ key: string; name: string; iv: number; ivDrop: number; csi: number; csiRise: number; ksContribution: number; psiContribution: number; }> = []; export const SORTING_DISTRIBUTION = { bins: [] as string[], counts: [] as number[], badRates: [] as number[], }; export const BANK_AVERAGE_CYCLE: Record = {}; export type ModelLifecycle = { onlineAt: string | null; escortStartAt: string | null; escortEndAt: string | null; offlineAt: string | null; developer: string; developmentKs: number; developmentPsi: number; maxLift: number; }; export const MODEL_LIFECYCLE: Record = {}; export function categoryName(category: ModelCategoryId): string { return MODEL_CATEGORIES.find((item) => item.id === category)?.name ?? category; } export function abnormalLevelOf(model: Pick): AbnormalLevel { const { ranking, ks, psi, ksDrop } = model; if (ranking === "—") return "正常"; if (ranking === "不符" && ks < 40 && psi > 10) return "三级"; if (ranking === "相符" && ks < 40 && psi > 25) return "三级"; if (ranking === "不符" && ks >= 40 && psi > 25) return "二级"; if (ranking === "不符" && ks >= 40 && psi > 10 && psi <= 25 && ksDrop > 20) return "二级"; if (ranking === "不符" && ks < 40 && psi <= 10) return "二级"; if (ranking === "相符" && ks < 40 && psi <= 25) return "二级"; if (ranking === "相符" && ks >= 40 && psi > 25 && ksDrop > 20) return "二级"; if (ranking === "不符" && ks >= 40 && psi > 10 && psi <= 25 && ksDrop <= 20) return "一级"; if (ranking === "不符" && ks >= 40 && psi <= 10) return "一级"; if (ranking === "相符" && ks >= 40 && psi > 25 && ksDrop <= 20) return "一级"; if (ranking === "相符" && ks >= 40 && psi > 10 && psi <= 25) return "一级"; if (ranking === "相符" && ks >= 40 && psi <= 10 && ksDrop > 20) return "一级"; return "正常"; } export function gradeOf(model: Pick): ModelGrade { if (model.status === "下线") return "A"; const abnormal = abnormalLevelOf(model); if (abnormal === "三级") return "C"; if (abnormal === "二级" && model.secondaryHits >= 4) return "C"; if (abnormal === "正常") return "A"; return "B"; } export function abnormalReasonOf(model: ModelRecord | MonitoringRow): string { const abnormal = abnormalLevelOf(model); if (abnormal === "正常") return "各项指标均在阈值内"; const reasons: string[] = []; if (model.ranking === "不符") reasons.push("排序性不符"); if (model.ks < 40) reasons.push("KS 低于 40%"); if (model.psi > 25) reasons.push("PSI 高于 25%"); else if (model.psi > 10) reasons.push("PSI 处于 10%–25%"); if (model.ksDrop > 20) reasons.push("KS 环比降幅高于 20%"); if (abnormal === "二级") { const result = model.secondaryHits >= 4 ? ",已触发监控结果升级为 C" : ",未达到累计升级条件"; return `${reasons.join("、")};近 6 个月累计 ${model.secondaryHits} 次二级异常${result}`; } return reasons.join("、"); } export function modelTrend( _model: ModelRecord, _metric: "ks" | "psi", _months: readonly string[] = MONITOR_MONTHS, ): number[] { return []; } export function monitoringRows(_source: ModelRecord[] = MODELS): MonitoringRow[] { return []; } export function monthsBetween(from: string, to: string): string[] { const [fromYear, fromMonth] = from.split("-").map(Number); const [toYear, toMonth] = to.split("-").map(Number); if (!fromYear || !fromMonth || !toYear || !toMonth) return [...MONITOR_MONTHS]; const start = fromYear * 12 + fromMonth - 1; const end = toYear * 12 + toMonth - 1; if (start > end || end - start > 23) return [...MONITOR_MONTHS]; return Array.from({ length: end - start + 1 }, (_, index) => { const value = start + index; const year = Math.floor(value / 12); const month = value % 12 + 1; return `${year}-${String(month).padStart(2, "0")}`; }); } export function average(values: number[]): number { return values.length ? values.reduce((sum, value) => sum + value, 0) / values.length : 0; } export function categoryTrend( _category: ModelCategoryId, _metric: "ks" | "psi", _months: string[], _source: ModelRecord[] = MODELS, ): number[] { return []; } export function bankTrend( _bank: string, _metric: "ks" | "psi", _months: string[], _source: ModelRecord[] = MODELS, ): number[] { return []; } export function modelsTrend( _models: ModelRecord[], _metric: "ks" | "psi", _months: string[], ): number[] { return []; } export function latestIterationDate(models: ModelRecord[]): string { return models.map((model) => model.iteratedAt).filter(Boolean).sort().at(-1) ?? "—"; } export function modelIterationCycleMonths(model: ModelRecord): number | null { const lifecycle = MODEL_LIFECYCLE[model.modelId]; const start = lifecycle?.onlineAt ?? lifecycle?.escortStartAt; if (!start || !model.iteratedAt) return null; const [startYear, startMonth] = start.slice(0, 7).split("-").map(Number); const [endYear, endMonth] = model.iteratedAt.slice(0, 7).split("-").map(Number); if (![startYear, startMonth, endYear, endMonth].every(Number.isFinite)) return null; return Math.max(0, (endYear * 12 + endMonth) - (startYear * 12 + startMonth)); } export function averageIterationCycle(models: ModelRecord[]): number | null { const values = models.map(modelIterationCycleMonths).filter((value): value is number => value !== null); return values.length ? Number(average(values).toFixed(1)) : null; } export function averageIterationMonths(models: ModelRecord[]): number { const months = models .filter((model) => model.status !== "下线") .map((model) => { const [year, month] = model.iteratedAt.slice(0, 7).split("-").map(Number); return (2026 * 12 + 7) - (year * 12 + month); }) .filter(Number.isFinite); return Number(average(months).toFixed(1)); }