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 图谱构建流程总览
下图展示了从原始交易数据到最终加权图谱的完整构建流程:
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 类,支持增量更新。每次新增交易数据时,只需增量添加节点和边,并更新受影响节点的动态特征,避免全量重建带来的性能开销。