3. 数据清洗基础:缺失值处理、异常值检测、重复数据去重、数据类型统一
说实话,很多做金融图分析的朋友,一上来就急着建模、跑算法。结果呢?图建出来全是噪声,分析结果根本没法看。
我个人的经验是:数据清洗占整个项目 70% 的时间,一点都不夸张。金融网络数据尤其脏——你想想看,不同系统导出来的数据,格式五花八门,字段名对不上,还有各种空值和乱码。如果不把这些坑填平,后面的图构建就是空中楼阁。
核心观点:数据清洗不是苦力活,而是决定图分析质量的基石。脏数据进,脏结果出。干净数据进,洞察自然来。
3.1 缺失值处理——别急着删,先问问为什么
我在项目中遇到过最典型的场景:从银行核心系统导出的交易流水,有 5% 的「对手方账号」字段是空的。很多人第一反应就是「删掉」。等等,先别急。
缺失值分三种:
- 完全随机缺失(MCAR): 比如系统偶尔抽风没记上,概率很低。这种可以直接删。
- 随机缺失(MAR): 缺失跟其他字段有关。比如大额交易更容易缺失备注信息。这种需要谨慎。
- 非随机缺失(MNAR): 缺失本身就有含义。比如某些高风险交易故意不填对手方。这种最危险。
我的经验:金融数据中,缺失往往不是意外,而是信号。我曾经处理过一批网贷数据,发现「借款人收入」字段缺失率高达 30%。后来一查,缺失的恰恰是那些收入不稳定的用户。如果直接删掉,模型就只看到「好客户」了。
处理策略(按优先级排序):
- 业务填充: 比如「交易对手」缺失,可以查关联交易补上。我习惯先做这一步。
- 统计填充: 均值、中位数、众数。注意:金融数据偏态严重,用中位数比均值靠谱。
- 模型预测填充: 用 KNN 或回归模型预测缺失值。适合关键字段。
- 直接删除: 只有缺失率低(<5%)且随机时才用。
# 我个人常用的缺失值处理流程(Python 伪代码)
import pandas as pd
import numpy as np
df = pd.read_csv('transactions.csv')
# 第一步:看看缺失情况
print(df.isnull().sum() / len(df) * 100)
# 第二步:业务规则填充
# 比如:如果交易类型是 '内部转账',对手方缺失就填 'INTERNAL'
df.loc[(df['tx_type'] == '内部转账') & (df['counterparty'].isnull()), 'counterparty'] = 'INTERNAL'
# 第三步:中位数填充(金融数据用中位数)
df['amount'].fillna(df['amount'].median(), inplace=True)
# 第四步:时间序列插值(适合时序数据)
df['balance'].interpolate(method='linear', inplace=True)
3.2 异常值检测——别把「异常」当「错误」
嗯,这里要注意。金融网络中的异常值,有时候是脏数据,有时候是真正的「黑天鹅」事件。我见过有人把一笔 1 个亿的转账当成异常删掉了——结果那是客户并购交易的付款。
常用的检测方法:
| 方法 | 适用场景 | 我的建议 |
|---|---|---|
| Z-Score(3σ 原则) | 正态分布的数据 | 金融数据很少正态,慎用 |
| IQR(箱线图) | 偏态分布 | 我比较推荐,鲁棒性好 |
| DBSCAN 聚类 | 多维异常检测 | 适合图数据的节点异常 |
| 业务规则 | 有明确上下限 | 最可靠,但需要业务知识 |
避坑指南:我曾经在反洗钱项目中,用 IQR 方法检测交易金额异常。结果发现所有跨境汇款都被标为异常——因为跨境汇款金额普遍偏高。后来我加了「按交易类型分组检测」的逻辑,才解决问题。记住:异常值检测一定要分组进行。
# IQR 方法示例(按交易类型分组)
def detect_outliers_iqr(df, column, group_col):
def iqr_func(x):
Q1 = x.quantile(0.25)
Q3 = x.quantile(0.75)
IQR = Q3 - Q1
lower = Q1 - 1.5 * IQR
upper = Q3 + 1.5 * IQR
return (x < lower) | (x > upper)
return df.groupby(group_col)[column].transform(iqr_func)
df['is_outlier'] = detect_outliers_iqr(df, 'amount', 'tx_type')
3.3 重复数据去重——图数据里,重复就是灾难
你想想看,如果同一个客户在图中出现两个节点,那图分析的结果就全乱了。中心度算不准,社区发现也会分裂。
重复数据的三种类型:
- 完全重复: 所有字段都一样。直接 drop_duplicates() 搞定。
- 关键字段重复: 比如身份证号相同,但姓名略有差异(「张三」vs「张 三」)。
- 业务含义重复: 比如同一个企业,在不同系统里注册名称不同(「阿里巴巴」vs「阿里集团」)。
我的做法:对于金融网络数据,我一般先做精确去重,再做模糊匹配。模糊匹配用 Levenshtein 距离或者 Jaccard 相似度。但要注意阈值设置——设太严漏掉重复,设太松误杀正常数据。
# 精确去重
df.drop_duplicates(subset=['customer_id'], keep='first', inplace=True)
# 模糊去重(使用 recordlinkage 库)
import recordlinkage
indexer = recordlinkage.Index()
indexer.block('city') # 先按城市分块,减少计算量
pairs = indexer.index(df, df)
compare = recordlinkage.Compare()
compare.string('name', 'name', method='jarowinkler', threshold=0.85)
compare.exact('phone', 'phone')
features = compare.compute(pairs, df, df)
# 找出相似度高的对
potential_duplicates = features[features.sum(axis=1) >= 2]
3.4 数据类型统一——别让「字符串」坑了你
说实话,数据类型问题是我见过最隐蔽的坑。明明看着是数字,一检查是字符串。排序的时候「100」排在「9」前面,你说气不气人?
金融数据常见的类型问题:
- 金额字段: 带逗号、带货币符号($1,234.56)、或者有空格
- 日期字段: 2023/01/01、2023-01-01、20230101、01/01/2023 混在一起
- 枚举字段: 「男」「M」「男性」「1」表示同一个意思
- ID 字段: 数字型 ID 和字符串型 ID 混用,join 的时候直接报错
# 数据类型统一的标准流程
# 1. 金额清洗
df['amount'] = df['amount'].str.replace(',', '').str.replace('$', '').astype(float)
# 2. 日期统一
df['tx_date'] = pd.to_datetime(df['tx_date'], format='mixed', dayfirst=False)
# 3. 枚举映射
gender_map = {'男': 'M', '男性': 'M', '1': 'M', '女': 'F', '女性': 'F', '0': 'F'}
df['gender'] = df['gender'].map(gender_map)
# 4. ID 统一为字符串(避免精度丢失)
df['customer_id'] = df['customer_id'].astype(str)
一个血的教训:我曾经在处理银行卡号时,直接用 int 类型读取。结果卡号末尾几位变成了 0(因为浮点数精度问题)。后来查了三天才发现是数据类型惹的祸。从此以后,所有 ID 类字段我都强制转成字符串。
好了,数据清洗这块就聊到这儿。记住一个原则:清洗不是目的,让数据变得可用才是。你花在清洗上的每一分钟,都会在图分析和建模阶段加倍回报给你。