保险反欺诈如何用图分析识别团伙网络

发布时间:2026/6/6 5:12:27

保险反欺诈如何用图分析识别团伙网络 1. 项目概述为什么保险反欺诈必须跳出表格思维我做风控建模快八年了前五年几乎全泡在SQL和Pandas里——写几百行JOIN查关联方、用窗口函数算滚动均值、给每个医生加个“历史拒赔率”标签。直到2021年接手一家车险公司的反欺诈二期项目才真正被现实打醒。他们当时上线的XGBoost模型AUC稳定在0.83左右但一线调查员反馈模型抓出来的“高风险单”里有近40%是孤例——单个保单确实可疑可查完所有关联人、维修厂、4S店、定损员的全部历史记录后发现根本构不成团伙作案。而真正被漏掉的反而是那些分散在不同分公司、由不同业务员承保、但共用同一套“修理-定损-理赔”链条的环形结构。这种结构在传统宽表里就是几行孤立数据在图数据库里却是一张咬合紧密的网。这就是图分析在保险反欺诈中不可替代的价值它不看单点是否异常而看关系是否违和。关键词里的“Towards AI - Medium”只是原始发布平台真正核心是“Insurance Fraud Detection with Graph Analytics”这个命题本身——它不是炫技而是解决一个行业级痛点当欺诈从“单点造假”进化到“网络协同”你的工具箱还停留在放大镜阶段对手早已用上了显微镜拓扑分析仪。我后来在三家保险公司落地过类似方案最深的体会是图特征不是锦上添花的附加项而是把“人如何协作造假”这个业务本质第一次真正编码进模型的能力。它让机器开始理解“关系即证据”——比如两个从未见过面的医生三年内共同出现在17份不同患者的住院清单上且所有患者都来自同一县域再比如某家汽修厂的5个合作定损员其名下理赔单的平均维修金额比行业均值高2.3倍但单张单子都在阈值内。这些模式在表格里是噪声在图里却是强信号。本文要讲的就是怎么把这种业务直觉变成可落地、可解释、可迭代的技术实现。不谈虚概念只拆真实代码、踩过的真实坑、调参时的真实纠结。2. 整体设计与思路拆解从“查关系”到“建关系网络”的范式转换2.1 为什么非得用图——传统方法的三个致命盲区很多人第一反应是“我用SQL也能查关联啊” 没错但请先看这三个场景场景一跨层级穿透你想查“和欺诈保单C4377同属一个政策持有人PH3759的所有理赔单”这很简单。但若想进一步查“PH3759的配偶、子女、常用联系人、共用银行卡的账户持有人以及这些人各自名下的所有理赔单”传统SQL需要嵌套6层LEFT JOIN性能断崖式下跌且一旦中间某层无数据比如配偶没投保整条链就断了。图查询一句MATCH (p:Policyholder)-[:HAS_CLAIM]-(c:Claim)-[:HAS_CLAIM]-(r:RelatedPerson) RETURN r就能拉出全网缺失节点自动跳过。场景二环形结构识别某欺诈团伙操作模式A医生开单→B医院收治→C定损员核价→D汽修厂维修→E车主提车→F业务员回佣→A医生返点。六个人形成闭环。在宽表里你得写6个字段的笛卡尔积去匹配计算量是O(n⁶)。图算法里find_cycles()或社区发现算法如Infomap能直接标出这个环时间复杂度是O(nm)m是边数。场景三权重动态聚合同一个医生给100个患者开单其中5个被证实欺诈那他的“欺诈倾向分”是5%。但如果这5个患者全集中在同一家医院、由同一个定损员处理、维修厂地址都在同一栋楼这个5%就该乘以一个“网络聚集系数”——比如3.2倍。表格无法天然表达这种“局部密度影响全局权重”的逻辑图的边权重节点中心性计算却能自然融合。所以图分析不是替代传统模型而是补上它缺失的“关系维度”。我的设计原则很朴素图负责定义“谁和谁有关联、关联有多强、关联是否异常”表格负责承载“每个人/每张单本身的静态属性”两者拼起来才是完整画像。这也是为什么原文强调“Augment machine learning model with graph features”——是增强augment不是取代。2.2 技术栈选型为什么用iGraph而不是Neo4j或NetworkX原文用了iGraph我实测后认为这是非常务实的选择尤其对中小团队iGraph vs NetworkXNetworkX在Python生态里名气大但它本质是内存图库所有计算都在RAM里跑。处理百万级节点时内存暴涨到32GB且很多算法如PageRank默认不支持边权重要自己重写。iGraph底层是C写的同样数据量内存占用低40%且原生支持加权图、有向图、多图等工业级特性。我拿医保数据集120万节点800万边测试iGraph构建图耗时23秒NetworkX要147秒且后者在计算社区发现时直接OOM。iGraph vs Neo4jNeo4j是图数据库适合在线查询和可视化。但反欺诈模型训练是离线批量任务需要的是高性能图计算引擎不是实时响应。把数据从数据库导出再计算IO开销巨大。iGraph直接读取CSV/Parquet内存计算省去ETL环节。我们线上环境是Spark集群iGraph能无缝集成PySpark UDF而Neo4j的JDBC驱动在分布式环境下稳定性差。关键决策点边权重的设计原文用groupby([Provider, AttendingPhysician]).size()生成权重这很合理但要注意权重不是越细越好。我最初尝试过用“单次诊疗费用均值”“诊断码相似度”“时间间隔标准差”等复合权重结果模型效果反而下降。原因在于噪声放大。医疗行为本身波动大用太精细的权重会把正常变异当成信号。最终我们回归到最朴素的计数权重——“两人合作次数”它鲁棒性强业务解释性好合作越频繁勾结嫌疑越大且和后续的度中心性、介数中心性等指标天然兼容。这个教训是图特征工程的第一原则是业务可解释性优先于数学复杂性。2.3 数据建模节点和边到底该定义成什么这是最容易踩坑的环节。原文把节点设为Provider医疗机构和AttendingPhysician主治医生边是他们的合作。这没错但实际落地时我扩展了三层第一层实体节点Entity NodesProvider,Physician,Patient,Claim,ICD_Code,CPT_Code,RepairShop,Appraiser... 所有业务实体都作为节点。注意Claim也设为节点很多人只把人设节点把保单当属性这会丢失关键路径。比如“同一患者在不同医院的多次就诊”这条链如果保单不是节点你就无法追踪患者-医院-医生的完整流转。第二层关系边Relationship Edges边类型必须带语义:TREATS医生治疗患者、:SUBMITS患者提交保单、:APPROVES定损员审批保单、:REPAIRS修理厂维修车辆。每条边存weight合作次数、first_date首次合作时间、last_date末次合作时间。这样MATCH (p:Physician)-[r:TREATS]-(t:Patient) WHERE r.weight 50就能直接找出“高频接诊医生”。第三层抽象节点Abstract Nodes这是提升效率的关键。比如把“同一县域内的所有修理厂”聚合成一个:REGIONAL_SHOP_CLUSTER节点把“所有使用相同ICD-9-CM-3手术编码的医生”聚合成:PROCEDURE_GROUP节点。这些抽象节点不存原始数据只存聚合特征如集群内平均欺诈率它们作为“超节点”连接到具体实体大幅压缩图规模。我们一个省级车险图原始节点280万加入抽象节点后降到110万计算速度提升3.7倍。这个三层建模法让图既能做深度穿透查具体人又能做宏观扫描查区域风险是平衡精度与性能的核心。3. 核心细节解析与实操要点从数据清洗到特征落地的硬核步骤3.1 数据清洗图分析对脏数据零容忍表格模型能容忍20%的缺失值图模型可能因为1个关键节点ID格式错误导致整张关系网断裂。我列几个血泪教训ID标准化是生死线医保数据里同一医生可能有多个IDDOC123系统ID、张三_省立医院姓名机构、ZS-SL拼音缩写。如果不统一图里会出现3个独立节点本该是一人的关系被割裂。我们的方案是建立主数据管理MDM规则库用正则模糊匹配如Levenshtein距离2归并ID。例如所有含“省立”“人民”“中心”字样的医院名强制映射到标准IDHOSP-001。这步必须在建图前完成否则后期修复成本极高。时间戳处理避免未来信息泄露原文用ClaimStartDt 2009-10-01做时间切分这正确。但要注意边的权重必须基于截止时间前的历史数据计算。比如计算医生A和B在2009年10月1日前的合作次数不能包含2009年10月2日发生的合作。我们曾因一个时间窗口bug导致测试集里混入了未来边AUC虚高0.12上线后模型完全失效。解决方案在构建边DataFrame时增加valid_until字段所有图计算只取valid_until cutoff_date的边。空值与默认值的哲学表格里常把缺失的AttendingPhysician填成UNKNOWN。但在图里UNKNOWN会成为一个超级节点连接所有无主治医生的保单瞬间污染整个网络。我们的做法是彻底删除含关键空值的边。比如Provider和AttendingPhysician任一为空这条边直接丢弃。宁可图稀疏也不要虚假连接。稀疏图还能用算法补全如基于相似度的链接预测虚假连接则永远是毒瘤。3.2 图特征工程6个必用指标的物理意义与计算陷阱原文列了Degree、Closeness、Infomap等我补充实战细节和避坑指南Degree度中心性物理意义节点的“社交广度”。医生合作的医院越多越可能是中介。但注意必须用加权度Strength而非简单度。G.strength(weightsG.es[Weight])计算的是所有邻边权重之和比G.degree()只数边数更能反映真实影响力。比如医生A和5家医院各合作1次度5强度5医生B和1家医院合作100次度1强度100后者风险显然更高。Betweenness Centrality介数中心性物理意义节点的“枢纽价值”。如果删除该节点网络连通性下降最多说明它是关键桥梁。计算陷阱默认算法对无向图假设所有路径等概率但保险网络有方向性如Physician-Patient-Claim是单向流。我们改用G.betweenness(directedTrue, weightsG.es[Weight])并过滤掉betweenness 0.001的节点避免数值噪声。Eigenvector Centrality特征向量中心性物理意义“朋友的质量比数量重要”。一个医生如果只和几个高欺诈率医生合作比和一堆低风险医生合作更危险。原文没提但我们强制要求计算前必须对节点做欺诈标签预标注哪怕只是粗筛的疑似标签否则特征向量中心性会收敛到随机值。我们用初始模型打分0.7的节点作为“种子欺诈节点”再迭代计算。Clustering Coefficient聚类系数物理意义节点邻居间的“抱团程度”。如果医生A合作的5家医院彼此之间也频繁合作说明存在封闭小圈子。计算时用G.transitivity_local_undirected(modezero)mode设为zero防止邻居数为0时报错。Community Detection社区发现原文用Infomap我们对比了Louvain、Label Propagation、Infomap三种Louvain速度快但对小社区敏感易把正常医疗联合体误判为欺诈圈。Label Propagation结果不稳定每次运行社区划分不同。Infomap最优。它最小化描述网络所需的“比特数”天然契合欺诈网络的“信息压缩”特性——欺诈团伙总试图用最少的沟通成本完成最多交易。我们固定trials10取最优解并用modularity值0.4作为有效社区阈值。Subgraph Density子图密度这是自研指标对每个社区计算边数 / (节点数 * (节点数-1))。密度0.3的社区标记为“高密欺诈嫌疑区”。它比单纯看社区大小更准——一个100节点的社区若只有50条边是松散联盟若900条边就是铁板一块。提示所有图特征计算后必须做Min-Max归一化。因为Degree可能从1到10000Closeness在0~1之间不归一化直接喂给LR模型小数值特征会被淹没。我们用sklearn.preprocessing.MinMaxScaler但fit只在训练集上做测试集用transform避免数据泄露。3.3 特征融合如何把图特征精准注入表格模型这是成败关键。原文简单pd.merge但实际要处理三个矛盾矛盾一粒度不匹配图特征是按Provider或Physician计算的而模型输入是按Claim保单的。一个保单对应一个医生、一个医院但图特征是医生的全局属性。解决方案在Claim表上做左连接用医生ID作为key。例如Claim表有AttendingPhysician_ID列图特征表有Physician_ID列直接merge即可。但要注意如果Claim表里医生ID是DOC123图特征表里是doc123必须先统一大小写。矛盾二时效性冲突图特征是截至2009-10-01计算的但Claim的ClaimStartDt是2009-09-15没问题如果Claim是2009-10-05它的图特征就不能用2009-10-01的快照。解决方案为每个Claim动态计算“截至其发生时间”的图特征。我们用Spark SQL写UDF对每个Claim取ClaimStartDt前所有合作记录重建子图再计算特征。虽然慢但杜绝了未来信息泄露。矛盾三高维稀疏性一个医生可能属于3个不同社区Infomap、Louvain、自定义地理社区每个社区有密度、规模、平均欺诈率等5个特征光社区相关特征就15维。直接拼接会让特征矩阵极度稀疏。解决方案用Target Encoding降维。例如对Physician_Community_ID计算该社区内历史欺诈保单占比作为单一数值特征。既保留业务含义又消除稀疏性。最终融合后的特征矩阵我们按重要性排序原始表格特征占40%、节点级图特征如医生度中心性、医院聚类系数占35%、社区级图特征如所在社区欺诈率、社区密度占25%。这个比例是通过Shapley值分析确定的不是拍脑袋。4. 实操过程与核心环节实现从零搭建可复现的端到端流程4.1 环境准备与依赖安装别跳过这步iGraph在不同系统编译差异很大。我们生产环境是CentOS 7 Python 3.8以下是经过千次验证的安装命令# 升级pip和setuptools避免编译失败 pip install --upgrade pip setuptools # 安装系统依赖CentOS sudo yum install -y gcc gcc-c make cmake git # 安装iGraph必须指定版本1.3.0以上有重大API变更 pip install igraph1.2.11 # 安装其他必要包 pip install pandas1.3.5 numpy1.21.6 scikit-learn1.0.2 matplotlib3.5.1注意如果用condaconda install -c conda-forge python-igraph更稳定但版本选择少。我们坚持pip因为能精确控制版本。4.2 数据加载与图构建完整可运行代码以下代码基于原文的Medicare数据集但增加了生产级健壮性处理import pandas as pd import numpy as np from igraph import Graph import warnings warnings.filterwarnings(ignore) # 1. 加载原始数据模拟 # Train_Inpatient.csv 和 Train_Outpatient.csv 结构相同Provider, AttendingPhysician, ClaimID, ClaimStartDt, ... Train_Inpatient pd.read_csv(data/Train_Inpatient.csv) Train_Outpatient pd.read_csv(data/Train_Outpatient.csv) # 2. 时间切分严格防止数据泄露 cutoff_date 2009-10-01 Train_Inpatient_G Train_Inpatient[Train_Inpatient[ClaimStartDt] cutoff_date][[Provider, AttendingPhysician, ClaimID]].copy() Train_Outpatient_G Train_Outpatient[Train_Outpatient[ClaimStartDt] cutoff_date][[Provider, AttendingPhysician, ClaimID]].copy() # 3. ID标准化关键步骤 def standardize_id(x): if pd.isna(x): return UNKNOWN # 去除空格、转小写、替换特殊字符 x str(x).strip().lower().replace( , ).replace(., ).replace(-, ) # 医生ID规则以DOC开头的保留否则加前缀 if x.startswith(doc): return x else: return fdoc_{x} Train_Inpatient_G[Provider] Train_Inpatient_G[Provider].apply(standardize_id) Train_Inpatient_G[AttendingPhysician] Train_Inpatient_G[AttendingPhysician].apply(standardize_id) Train_Outpatient_G[Provider] Train_Outpatient_G[Provider].apply(standardize_id) Train_Outpatient_G[AttendingPhysician] Train_Outpatient_G[AttendingPhysician].apply(standardize_id) # 4. 合并并去重防止同一对医生在不同保单重复计数 G_df pd.concat([Train_Inpatient_G, Train_Outpatient_G], ignore_indexTrue) G_df G_df.drop_duplicates(subset[Provider, AttendingPhysician, ClaimID]) # 5. 构建加权边 G_df G_df.groupby([Provider, AttendingPhysician]).size().reset_index(nameWeight) print(f原始边数: {len(G_df)}) # 过滤掉权重过低的边减少噪声 G_df G_df[G_df[Weight] 2].reset_index(dropTrue) print(f过滤后边数: {len(G_df)}) # 6. 创建图无向图因合作是双向关系 source Provider target AttendingPhysician weight_col Weight # 确保ID是字符串 G_df[source] G_df[source].astype(str) G_df[target] G_df[target].astype(str) # iGraph要求DataFrame只有三列source, target, weight edge_df G_df[[source, target, weight_col]] # 构建图 G Graph.DataFrame(edge_df, directedFalse) print(f图节点数: {G.vcount()}, 边数: {G.ecount()})这段代码跑通后你会得到一个Graph对象G它就是后续所有计算的基石。注意G.vcount()和G.ecount()的输出如果节点数远小于预期比如只有几百说明ID标准化出了问题要回头检查。4.3 图特征计算6个核心指标的完整实现# 1. 度中心性加权 degree pd.DataFrame({ Node: G.vs[name], Degree_Weighted: G.strength(weightsG.es[Weight]) }) # 2. 介数中心性有向加权 betweenness pd.DataFrame({ Node: G.vs[name], Betweenness_Weighted: G.betweenness(directedTrue, weightsG.es[Weight]) }) # 3. 特征向量中心性需预标注欺诈节点 # 假设我们有一个欺诈医生列表 fraud_physicians [doc_123, doc_456] fraud_mask [1 if node in fraud_physicians else 0 for node in G.vs[name]] eigenvector pd.DataFrame({ Node: G.vs[name], Eigenvector_Centrality: G.eigenvector_centrality(weightsG.es[Weight], algorithmarpack) # arpack更稳定 }) # 4. 聚类系数局部 clustering pd.DataFrame({ Node: G.vs[name], Clustering_Coefficient: G.transitivity_local_undirected(modezero) }) # 5. 社区发现Infomap communities G.community_infomap(edge_weightsG.es[Weight], trials10) community_membership communities.membership community_df pd.DataFrame({ Node: G.vs[name], Community_ID: community_membership, Community_Size: [communities.sizes()[i] for i in community_membership] }) # 6. 子图密度对每个社区计算 density_features [] for comm_id in set(community_membership): # 获取该社区所有节点 nodes_in_comm [G.vs[i][name] for i in range(len(G.vs)) if community_membership[i] comm_id] # 构建子图 subgraph_nodes [i for i in range(len(G.vs)) if community_membership[i] comm_id] subgraph G.induced_subgraph(subgraph_nodes) # 计算密度 if subgraph.vcount() 1: density subgraph.ecount() / (subgraph.vcount() * (subgraph.vcount() - 1)) else: density 0.0 density_features.append({ Community_ID: comm_id, Subgraph_Density: density, Subgraph_Edge_Count: subgraph.ecount() }) density_df pd.DataFrame(density_features) # 合并所有特征 graph_features degree.merge(betweenness, onNode).merge(eigenvector, onNode) graph_features graph_features.merge(clustering, onNode).merge(community_df, onNode) graph_features graph_features.merge(density_df, onCommunity_ID, howleft) # 归一化 from sklearn.preprocessing import MinMaxScaler scaler MinMaxScaler() feature_cols [Degree_Weighted, Betweenness_Weighted, Eigenvector_Centrality, Clustering_Coefficient, Subgraph_Density] graph_features[feature_cols] scaler.fit_transform(graph_features[feature_cols]) print(图特征计算完成形状:, graph_features.shape) print(graph_features.head())这段代码执行后graph_featuresDataFrame就是你的图特征宝库。注意Subgraph_Density是按社区聚合的所以它和Node不是一一对应要用Community_ID做join key。4.4 模型训练与评估超越AUC的实战指标原文只用AUC这在生产环境远远不够。我们增加三个关键指标PrecisionTopK取预测概率最高的前1000个保单其中真实欺诈的比例。这直接对应调查队的人力投入效率。RecallCost设定单次人工核查成本为$200计算模型节省的总成本。公式(真实欺诈数 - 模型漏报数) * 平均欺诈损失 - 预测数 * 200。Business F1给欺诈样本赋予权重w 平均欺诈金额 / 全局平均金额再算F1。让模型更关注“高损失欺诈”。from sklearn.linear_model import LogisticRegression from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import roc_auc_score, precision_score, recall_score, f1_score import numpy as np # 假设原始表格特征已加载为 train_table, test_table # 图特征已加载为 graph_features按Physician_ID索引 # 特征融合以Physician_ID为key merge train_enhanced train_table.merge(graph_features, left_onAttendingPhysician, right_onNode, howleft) test_enhanced test_table.merge(graph_features, left_onAttendingPhysician, right_onNode, howleft) # 填充NaN图特征缺失表示无合作记录设为0 train_enhanced train_enhanced.fillna(0) test_enhanced test_enhanced.fillna(0) # 准备X, y X_train train_enhanced.drop(columns[PotentialFraud, Node, AttendingPhysician]) y_train train_enhanced[PotentialFraud] X_test test_enhanced.drop(columns[PotentialFraud, Node, AttendingPhysician]) y_test test_enhanced[PotentialFraud] # 训练模型 lr LogisticRegression(penaltyl2, solversaga, random_state42, max_iter1000) rf RandomForestClassifier(n_estimators300, max_depth5, min_samples_leaf50, max_features0.3, random_state42, n_jobs-1) lr.fit(X_train, y_train) rf.fit(X_train, y_train) # 预测概率 pred_lr_proba lr.predict_proba(X_test)[:, 1] pred_rf_proba rf.predict_proba(X_test)[:, 1] # 评估 def business_metrics(y_true, y_pred_proba, top_k1000, avg_fraud_loss15000, audit_cost200): # PrecisionTopK top_k_idx np.argsort(y_pred_proba)[::-1][:top_k] precision_topk precision_score(y_true[top_k_idx], (y_pred_proba[top_k_idx] 0.5).astype(int)) # RecallCost # 假设模型预测所有prob0.3的为欺诈 pred_fraud (y_pred_proba 0.3) true_fraud y_true 1 detected np.logical_and(pred_fraud, true_fraud).sum() cost_saving detected * avg_fraud_loss - pred_fraud.sum() * audit_cost # Business F1 # 计算加权F1 from sklearn.metrics import fbeta_score # 权重w 欺诈损失 / 全局平均损失这里简化为w2.5因高损失欺诈占比高 w 2.5 f1_weighted fbeta_score(y_true, pred_fraud, beta1, pos_label1, sample_weight[w if x1 else 1 for x in y_true]) return precision_topk, cost_saving, f1_weighted # 对比结果 lr_prec, lr_cost, lr_f1 business_metrics(y_test, pred_lr_proba) rf_prec, rf_cost, rf_f1 business_metrics(y_test, pred_rf_proba) print( 模型对比报告 ) print(fLogistic Regression:) print(f Precision1000: {lr_prec:.4f}) print(f 成本节省: ${lr_cost:,.0f}) print(f Business F1: {lr_f1:.4f}) print(fRandom Forest:) print(f Precision1000: {rf_prec:.4f}) print(f 成本节省: ${rf_cost:,.0f}) print(f Business F1: {rf_f1:.4f})实测结果加入图特征后RF模型的Precision1000从0.31提升到0.47意味着调查队每查1000单多抓回160个真欺诈成本节省从$1.2M提升到$2.8M。这才是业务部门真正关心的数字。5. 常见问题与排查技巧实录那些文档里不会写的坑5.1 图构建失败常见报错与根因分析报错信息根本原因解决方案ValueError: Invalid vertex name节点名含非法字符如/,:, 空格或为None在standardize_id()函数中增加re.sub(r[^a-zA-Z0-9_], _, x)清洗MemoryErrorduringGraph.DataFrame()边DataFrame过大1000万行改用分块构建G Graph(); G.add_vertices(unique_nodes); G.add_edges(chunk)igraph.InternalError: Cannot compute betweenness on a graph with no edges过滤后边数为0如Weight2太严降低阈值或改用Weight1并在计算前加if G.ecount() 0: ...判断5.2 特征计算异常数值问题与业务逻辑错位问题Closeness出现inf或nan原因图不连通某些节点无法到达其他节点。Closeness定义是1/平均最短路径长度路径长度无穷大时结果为0倒数为inf。解决方案用G.closeness(modeall, weightsG.es[Weight], normalizedTrue)modeall会自动处理不连通分量normalizedTrue确保值在[0,1]。问题Community_ID全是0原因Infomap算法未收敛或图太稀疏边数/节点数 0.01。解决方案先用G.components()检查连通分量数量若1对每个分量单独运行Infomap或降低trials参数强制返回一个解。问题图特征和业务直觉相反例如一个已知欺诈医生的Degree_Weighted很低。根因该医生只和1家黑医院合作但合作了500次Weight是500Degree是1。Degree反映广度Strength反映深度。解决方案永远用Strength代替Degree并在特征命名时明确标注如Physician_Strength。5.3 模型效果不佳不是算法问题是数据问题如果加入图特征后AUC不升反降90%概率是以下三个问题数据泄露未切断检查cutoff_date是否严格应用在所有环节。一个典型错误是用全量数据计算图特征再用时间切分后的数据训练模型。解决方案写一个build_graph_up_to_date(data, cutoff)函数所有图计算都走这个入口。特征未对齐图特征按Physician_ID计算但模型输入表里AttendingPhysician列有NULL或格式不一致。解决方案在merge前用train_table[AttendingPhysician].value_counts(dropnaFalse)检查空值比例5%就要处理。业务逻辑未编码图特征是通用的但保险欺诈有领域知识。例如车险中“同一4S店同一定损员同一车型”的组合比单纯“同一4S店”更有意义。解决方案在图特征基础上叠加业务规则特征。如新增列Is_Same_Dealer_Appraiser布尔值再和图特征交叉。实操心得我有个硬性规定——任何新特征加入模型前必须用Excel手动验证10个样本。比如挑出Physician_Strength最高的5个医生查他们的真实理赔记录确认是否真有异常模式。如果人工都看不出规律机器更学不到。这一步花1小时能省下3天调参时间。5.4 性能优化从分钟级到秒级的提速技巧技巧1用induced_subgraph替代全图计算当只需计算某个医生的邻居特征时不要在整个图上跑G.neighborhood()而是neighbors G.neighbors(node_id); subgraph G.induced_subgraph(neighbors); result subgraph.closeness()。速度提升5-8倍。技巧2预计算并缓存图特征不随每次预测变化应离线计算好存入数据库。我们用Redis缓存{physician_id: {strength: 120, betweenness: 0.

相关新闻