3. 数据清洗基础:缺失值处理、异常值检测、重复数据去重、数据类型统一

说实话,很多做金融图分析的朋友,一上来就急着建模、跑算法。结果呢?图建出来全是噪声,分析结果根本没法看。

我个人的经验是:数据清洗占整个项目 70% 的时间,一点都不夸张。金融网络数据尤其脏——你想想看,不同系统导出来的数据,格式五花八门,字段名对不上,还有各种空值和乱码。如果不把这些坑填平,后面的图构建就是空中楼阁。

核心观点:数据清洗不是苦力活,而是决定图分析质量的基石。脏数据进,脏结果出。干净数据进,洞察自然来。

金融网络数据清洗 缺失值处理 异常值检测 重复数据去重 数据类型统一 删除 / 填充 / 插值 IQR / Z-Score / 业务规则 精确匹配 / 模糊匹配 字符串 / 数值 / 日期 / 枚举

3.1 缺失值处理——别急着删,先问问为什么

我在项目中遇到过最典型的场景:从银行核心系统导出的交易流水,有 5% 的「对手方账号」字段是空的。很多人第一反应就是「删掉」。等等,先别急。

缺失值分三种:

  • 完全随机缺失(MCAR): 比如系统偶尔抽风没记上,概率很低。这种可以直接删。
  • 随机缺失(MAR): 缺失跟其他字段有关。比如大额交易更容易缺失备注信息。这种需要谨慎。
  • 非随机缺失(MNAR): 缺失本身就有含义。比如某些高风险交易故意不填对手方。这种最危险。

我的经验:金融数据中,缺失往往不是意外,而是信号。我曾经处理过一批网贷数据,发现「借款人收入」字段缺失率高达 30%。后来一查,缺失的恰恰是那些收入不稳定的用户。如果直接删掉,模型就只看到「好客户」了。

处理策略(按优先级排序):

  1. 业务填充: 比如「交易对手」缺失,可以查关联交易补上。我习惯先做这一步。
  2. 统计填充: 均值、中位数、众数。注意:金融数据偏态严重,用中位数比均值靠谱。
  3. 模型预测填充: 用 KNN 或回归模型预测缺失值。适合关键字段。
  4. 直接删除: 只有缺失率低(<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 类字段我都强制转成字符串。

好了,数据清洗这块就聊到这儿。记住一个原则:清洗不是目的,让数据变得可用才是。你花在清洗上的每一分钟,都会在图分析和建模阶段加倍回报给你。


专注资料整理