4. 图谱数据构建:从原始交易数据构建图谱、节点特征提取、边权重计算

图谱构建是反欺诈图谱分析的基础环节。原始交易数据通常是扁平的表格结构,需要经过实体抽取、关系映射、特征工程三个步骤,才能转化为可供图算法使用的节点-边结构。本节将详细拆解这一过程,并给出可落地的 Python 实现方案。

核心原则:图谱构建的质量直接决定后续告警阈值的有效性。节点特征缺失或边权重失真,会导致社区发现、异常检测等算法产生系统性偏差。

4.1 实体抽取与节点定义

从原始交易数据中识别出具有反欺诈意义的实体,每个实体对应图中的一个节点。常见的反欺诈实体包括:

实体类型 数据来源字段 节点ID生成规则 反欺诈意义
用户账户 payer_id, payee_id account_{id} 识别羊毛党、盗号账户
设备指纹 device_id, imei device_{hash} 发现设备农场、模拟器
IP地址 client_ip, server_ip ip_{address} 检测代理IP、异常地域
银行卡号 card_no, bank_account card_{masked} 追踪洗钱路径
手机号 phone, contact_phone phone_{number} 关联虚假注册
import pandas as pd
import hashlib

def extract_nodes(transactions: pd.DataFrame) -> dict:
    """
    从交易数据中提取所有实体节点,返回 {node_type: set(node_ids)} 字典。
    """
    nodes = {
        'account': set(),
        'device': set(),
        'ip': set(),
        'card': set(),
        'phone': set()
    }
    
    # 账户节点
    nodes['account'].update(transactions['payer_id'].dropna().apply(lambda x: f"account_{x}"))
    nodes['account'].update(transactions['payee_id'].dropna().apply(lambda x: f"account_{x}"))
    
    # 设备节点(对设备ID做哈希脱敏)
    for dev in transactions['device_id'].dropna().unique():
        dev_hash = hashlib.sha256(str(dev).encode()).hexdigest()[:12]
        nodes['device'].add(f"device_{dev_hash}")
    
    # IP节点
    nodes['ip'].update(transactions['client_ip'].dropna().apply(lambda x: f"ip_{x}"))
    
    # 银行卡节点(掩码处理)
    for card in transactions['card_no'].dropna().unique():
        masked = str(card)[:4] + '****' + str(card)[-4:]
        nodes['card'].add(f"card_{masked}")
    
    # 手机号节点
    nodes['phone'].update(transactions['phone'].dropna().apply(lambda x: f"phone_{x}"))
    
    return nodes

4.2 关系映射与边定义

实体之间的交互行为构成图的边。每条边需要明确:源节点、目标节点、关系类型、时间戳、交易金额等属性。反欺诈图谱中常见的边类型包括:

  • 交易边:payer → payee,属性包含金额、时间、交易类型
  • 设备共用边:account ↔ device,表示账户登录/使用某设备
  • IP关联边:account ↔ ip,表示账户从某IP发起交易
  • 资金循环边:account → account,表示短时间内资金回流
  • 社交关系边:account ↔ phone,表示账户绑定了某手机号
def build_edges(transactions: pd.DataFrame) -> pd.DataFrame:
    """
    从交易数据构建边DataFrame,包含源节点、目标节点、边类型、权重基础字段。
    """
    edges = []
    
    # 交易边
    for _, row in transactions.iterrows():
        edges.append({
            'source': f"account_{row['payer_id']}",
            'target': f"account_{row['payee_id']}",
            'edge_type': 'transfer',
            'amount': row['amount'],
            'timestamp': row['timestamp'],
            'txn_id': row['txn_id']
        })
        
        # 设备关联边
        if pd.notna(row.get('device_id')):
            dev_hash = hashlib.sha256(str(row['device_id']).encode()).hexdigest()[:12]
            edges.append({
                'source': f"account_{row['payer_id']}",
                'target': f"device_{dev_hash}",
                'edge_type': 'uses_device',
                'amount': 0,
                'timestamp': row['timestamp'],
                'txn_id': row['txn_id']
            })
        
        # IP关联边
        if pd.notna(row.get('client_ip')):
            edges.append({
                'source': f"account_{row['payer_id']}",
                'target': f"ip_{row['client_ip']}",
                'edge_type': 'from_ip',
                'amount': 0,
                'timestamp': row['timestamp'],
                'txn_id': row['txn_id']
            })
    
    return pd.DataFrame(edges)
避坑提示:不要将所有关系都无差别加入图谱。例如,同一IP下数百个账户同时交易,如果全部建边会导致图密度过高,社区发现算法失效。建议对高频IP做聚合或设置最小交易次数阈值。

4.3 节点特征提取

节点特征分为静态属性特征和动态行为特征。静态特征来自注册信息、实名认证等级等;动态特征需要从交易历史中聚合计算。

特征类别 特征名称 计算方式 数据类型
静态 账户年龄 当前时间 - 注册时间(天) float
静态 实名等级 0=未实名, 1=初级, 2=高级 int
动态 交易频次 过去24小时交易笔数 int
动态 平均交易金额 过去7天交易金额均值 float
动态 设备切换次数 过去24小时使用不同设备数 int
动态 IP地域分散度 过去24小时IP所属城市数量 int
图特征 出度/入度 节点连接的边数 int
图特征 PageRank NetworkX PageRank算法 float
def extract_node_features(transactions: pd.DataFrame, nodes: dict) -> pd.DataFrame:
    """
    为每个节点提取特征,返回 node_id -> feature_vector 的映射表。
    """
    features = []
    
    # 按账户聚合交易行为
    account_txns = transactions.groupby('payer_id')
    
    for node_id in nodes['account']:
        account_id = node_id.replace('account_', '')
        if account_id in account_txns.groups:
            txns = account_txns.get_group(account_id)
            features.append({
                'node_id': node_id,
                'node_type': 'account',
                'txn_count_24h': (txns['timestamp'] > (pd.Timestamp.now() - pd.Timedelta(hours=24))).sum(),
                'avg_amount_7d': txns[txns['timestamp'] > (pd.Timestamp.now() - pd.Timedelta(days=7))]['amount'].mean(),
                'device_switch_24h': txns[txns['timestamp'] > (pd.Timestamp.now() - pd.Timedelta(hours=24))]['device_id'].nunique(),
                'ip_city_count': txns[txns['timestamp'] > (pd.Timestamp.now() - pd.Timedelta(hours=24))]['ip_city'].nunique()
            })
        else:
            # 只有收款记录的账户
            features.append({
                'node_id': node_id,
                'node_type': 'account',
                'txn_count_24h': 0,
                'avg_amount_7d': 0,
                'device_switch_24h': 0,
                'ip_city_count': 0
            })
    
    return pd.DataFrame(features)

4.4 边权重计算

边权重反映两个节点之间关联的强度或异常程度。在反欺诈场景中,权重越高通常意味着关系越可疑。常用的权重计算策略包括:

  • 交易频次权重:w = log(1 + 交易次数),高频交易获得更高权重
  • 金额异常权重:w = (交易金额 - 均值) / 标准差,偏离正常范围越大权重越高
  • 时间集中度权重:w = 1 / (时间间隔标准差 + 1),短时间内密集交易权重更高
  • 设备/IP共享权重:w = 1 + 共享设备数,多账户共用同一设备时权重增加
  • 综合权重:w = α·频次 + β·金额异常 + γ·时间集中度
def compute_edge_weights(edges: pd.DataFrame, transactions: pd.DataFrame) -> pd.DataFrame:
    """
    为每条边计算综合权重,基于频次、金额异常度和时间集中度。
    """
    # 按源-目标-类型聚合
    edge_groups = edges.groupby(['source', 'target', 'edge_type'])
    
    weighted_edges = []
    for (src, tgt, etype), group in edge_groups:
        txn_ids = group['txn_id'].tolist()
        related_txns = transactions[transactions['txn_id'].isin(txn_ids)]
        
        # 频次权重
        freq_weight = np.log1p(len(group))
        
        # 金额异常权重(Z-score)
        amounts = related_txns['amount'].values
        if len(amounts) > 1:
            amount_z = np.abs((amounts.mean() - amounts.mean()) / (amounts.std() + 1e-8))
            amount_weight = np.mean(amount_z)
        else:
            amount_weight = 0.5  # 单笔交易默认中等异常度
        
        # 时间集中度(时间戳标准差越小越集中)
        timestamps = related_txns['timestamp'].values.astype(np.int64)
        if len(timestamps) > 1:
            time_std = timestamps.std()
            time_weight = 1.0 / (time_std / 1e9 + 1)  # 转换为秒
        else:
            time_weight = 0.3
        
        # 综合权重(α=0.4, β=0.4, γ=0.2)
        combined_weight = 0.4 * freq_weight + 0.4 * amount_weight + 0.2 * time_weight
        
        weighted_edges.append({
            'source': src,
            'target': tgt,
            'edge_type': etype,
            'weight': round(combined_weight, 4),
            'txn_count': len(group)
        })
    
    return pd.DataFrame(weighted_edges)
权重归一化:不同边类型的权重量级可能差异很大(如交易边权重可达100+,而设备关联边通常小于5)。在输入图算法前,建议按边类型做 min-max 归一化或 z-score 标准化,避免量级大的边类型主导模型。

4.5 图谱构建流程总览

下图展示了从原始交易数据到最终加权图谱的完整构建流程:

图谱数据构建流程 原始交易数据 CSV / Parquet / DB 实体抽取 账户/设备/IP/卡/手机 关系映射 交易/设备/IP关联 节点特征提取 静态+动态+图特征 边权重计算 频次+金额+时间 图谱融合 节点+边+特征 → NetworkX / Neo4j 加权反欺诈图谱 用于社区发现/异常检测 ⚠ 关键检查点 节点去重 / 边方向确认 权重归一化 / 孤立节点处理

4.6 图谱质量验证

构建完成后,需要对图谱进行质量检查,确保数据可用性:

检查项 预期指标 异常处理
节点总数 与原始数据实体数一致 检查实体抽取逻辑是否遗漏
孤立节点比例 < 5% 孤立节点过多说明关系映射不完整
边权重分布 近似正态或右偏 权重全为0或全相同需检查计算逻辑
图密度 0.001 ~ 0.1 密度过高需考虑边采样或聚合
特征缺失率 < 10% 缺失率高的特征需用中位数填充
def validate_graph(G, node_features: pd.DataFrame):
    """
    对构建好的图进行质量验证,输出关键指标。
    """
    import networkx as nx
    
    print(f"节点总数: {G.number_of_nodes()}")
    print(f"边总数: {G.number_of_edges()}")
    
    # 孤立节点
    isolated = list(nx.isolates(G))
    print(f"孤立节点数: {len(isolated)}, 占比: {len(isolated)/G.number_of_nodes():.2%}")
    
    # 图密度
    density = nx.density(G)
    print(f"图密度: {density:.4f}")
    
    # 边权重分布
    weights = [d['weight'] for _, _, d in G.edges(data=True) if 'weight' in d]
    if weights:
        print(f"边权重 - 均值: {np.mean(weights):.2f}, 标准差: {np.std(weights):.2f}")
        print(f"边权重 - 最小值: {min(weights):.2f}, 最大值: {max(weights):.2f}")
    
    # 特征缺失率
    if node_features is not None:
        missing_rate = node_features.isnull().sum() / len(node_features)
        print(f"\n特征缺失率:\n{missing_rate[missing_rate > 0]}")
    
    return {
        'n_nodes': G.number_of_nodes(),
        'n_edges': G.number_of_edges(),
        'isolated_ratio': len(isolated) / G.number_of_nodes(),
        'density': density
    }
最佳实践:建议将图谱构建封装为可复用的 Pipeline 类,支持增量更新。每次新增交易数据时,只需增量添加节点和边,并更新受影响节点的动态特征,避免全量重建带来的性能开销。