4. 数据探索与可视化(结合cuML)

拿到数据之后,别急着上模型。我个人习惯先跟数据“聊聊天”。

聊天的方式就是探索性数据分析。说白了,你得知道数据长什么样,有没有脏数据,特征之间有没有关联。这一环节做扎实了,后面的特征工程和模型训练才能少跳坑。我见过不少项目,上来就调参,结果跑出来模型精度奇低,回头一查,原来是特征分布偏到离谱——这就是跳过探索的代价。

经验之谈: 在GPU上做EDA和CPU不太一样。cuML虽然强在建模,但数据清洗和可视化依然需要和cuDF、cuPy、以及Python可视化库配合。你要学会在这个组合里找到平衡点。

4.1 数据加载与初步探查

所有工作开始于读取数据。我们用RAPIDS的cuDF来做这一步。它跟Pandas API几乎一模一样,但背后是GPU在跑。你只需要改个import,速度就能快几十倍。

import cudf
import cupy as cp
import matplotlib.pyplot as plt
import seaborn as sns

# 读取csv文件,直接加载到GPU
df = cudf.read_csv('credit_card_data.csv')
print('行数、列数:', df.shape)
print('前5行样例:')
print(df.head())

嗯,这里要注意一个坑:cuDF的read_csv目前对某些编码格式支持不如Pandas那么强。我曾经遇到一个UTF-16编码的文件,结果直接报错。解决方案很简单,先用Pandas的read_csv把它读进来,再用cudf.from_pandas()转过去就好。

import pandas as pd
pdf = pd.read_csv('weird_encoding.csv', encoding='utf-16')
df = cudf.from_pandas(pdf)

数据到手后,第一件事就是看“描述性统计”。用describe()可以快速了解数值列的分布情况。

# 描述性统计
desc = df.describe()
print(desc)

# 单独查看某列的分布特征
print('特征A的偏度:', df['feature_A'].skew())
print('特征B的峰度:', df['feature_B'].kurtosis())

我个人特别喜欢看分位数。中位数和均值相差大,说明分布有偏。如果某个特征的25%分位数和75%分位数差距过小,那这个特征可能没什么区分能力——我通常直接考虑过滤掉。你想想看,特征之间拉不开距离,模型自然学不到东西。

统计指标说明你该关注什么
均值 vs 中位数如果差距大,分布偏斜严重考虑做对数变换或Box-Cox
标准差数据分散程度过大的标准差可能含异常值
偏度/峰度分布形态偏度绝对值>1,建议做归一化
缺失值占比列中有多少空值>20%的列,我倾向于直接丢弃

4.2 单变量与双变量分析

4.2.1 直方图与分布

只看统计数字还不够。你需要用眼睛看。GPU加速下,生成直方图非常快。

import cupy as cp

# 从cuDF列获取cupy数组
feature_cp = df['feature_A'].to_cupy()

# 直方图计算(在GPU上完成)
hist, bin_edges = cp.histogram(feature_cp, bins=50)

# 用matplotlib绘图
plt.figure(figsize=(10,4))
plt.bar(bin_edges[:-1], hist.get(), width=(bin_edges[1]-bin_edges[0]).get())
plt.title('特征A的分布 - GPU加速直方图')
plt.show()

为什么我强调在GPU上计算直方图?因为数据量一大,CPU版本的numpy.histogram确实会卡。1000万行数据,cuPy只需几毫秒。我做过一场对比测试,结果差距超过20倍。说白了,用GPU不是为了炫技,而是为了不让等待消耗你的注意力。

你还可以加上KDE平滑来观察分布趋势。不过KDE计算在cuML里没有直接实现,需要借助cuDF+scipy的组合——我建议你直接取直方图的中心点,再插值一下效果也差不多。

4.2.2 异常值与箱线图

箱线图是发现异常值的好工具。在cuDF里实现IQR法,效率极高。

# 用cuDF计算四分位距
Q1 = df['feature_A'].quantile(0.25)
Q3 = df['feature_A'].quantile(0.75)
IQR = Q3 - Q1

lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR

# 筛选出异常值
outliers = df[(df['feature_A'] < lower_bound) | (df['feature_A'] > upper_bound)]
print(f'异常值数量:{len(outliers)}')

我曾经在一个金融风控项目里,直接用这种方法发现了2000多个异常交易。但这些异常并不都是噪音——其中有几笔是系统Bug导致的重复交易,还有几笔是真实的欺诈样本。嗯,如果你是做异常检测,这个步骤还能帮你标注一部分训练数据。

4.2.3 双变量散点图

双变量关系,我最喜欢用散点图。但要注意,GPU的显存有限,几百万个点全画上去,不是显存撑不住,就是看图完全糊成一团。

我建议采样。

# 采样10万行,放在CPU内存中绘图
sample = df.sample(n=100000).to_pandas()

plt.figure(figsize=(8,6))
plt.scatter(sample['feature_A'], sample['feature_B'], s=1, alpha=0.3)
plt.xlabel('feature_A')
plt.ylabel('feature_B')
plt.title('特征A与特征B的关系')
plt.show()

alpha=0.3可以让重叠的点显示出密度感。你想想看,如果两个特征完全线性相关,那散点图应该呈现一条斜线。如果呈球形,那关系就不强。如果出现弧形或分叉,你就要考虑交互特征或者分段建模了。

显存警告: 在GPU设备上一次性把整个DataFrame转为numpy再画图,可能会导致显存溢出。正确的做法是先采样,再.to_pandas(),然后绘制。如果你坚持全量绘制,请把图放在CPU端。

4.3 关联性与相关性分析

4.3.1 Pearson vs Spearman

相关性矩阵是特征筛选的重要依据。cuML提供了corr()方法,你可以直接在GPU上算。

# 计算Pearson相关系数
pearson_corr = df.corr(method='pearson')
print(pearson_corr)

# 计算Spearman秩相关系数
spearman_corr = df.corr(method='spearman')
print(spearman_corr)

这里我要多说两句:

  • Pearson 只对线性关系敏感。如果两个特征是非线性但单调的(比如y = log(x)),Pearson会很低,但Spearman会接近1。我一开始也犯过这个错,只看Pearson矩阵,把一堆强单调关系的特征误判为不相关,白白浪费了信息。
  • Spearman 基于排名,对异常值不敏感。如果你数据里混了很多噪声,Spearman比Pearson可靠。

我建议两个都算一下,放在一张热力图里对比。

import matplotlib.pyplot as plt
import seaborn as sns

fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14,6))

# 将cuDF DataFrame转成pandas再画图
pearson_pd = pearson_corr.to_pandas()
spearman_pd = spearman_corr.to_pandas()

sns.heatmap(pearson_pd, annot=True, fmt='.2f', ax=ax1, cmap='RdBu_r')
ax1.set_title('Pearson相关系数')

sns.heatmap(spearman_pd, annot=True, fmt='.2f', ax=ax2, cmap='RdBu_r')
ax2.set_title('Spearman相关系数')

plt.tight_layout()
plt.show()

4.3.2 可视化热力图

热力图能让你一眼看出哪些特征群高度相关。如果两个特征相关系数绝对值超过0.9,我通常会二选一。为什么?因为共线性会让一些模型(比如线性回归、逻辑回归)的系数不稳定,甚至完全失效。树模型还好,但冗余特征也只会增加计算量,不会提升精度。

我个人开发了一个小策略:先跑Spearman相关系数,设定阈值0.95,对高度相关的特征只保留其中一个。

# 找出高度相关的特征对
high_corr_pairs = []
for i in range(len(spearman_pd.columns)):
    for j in range(i+1, len(spearman_pd.columns)):
        if abs(spearman_pd.iloc[i, j]) > 0.95:
            high_corr_pairs.append((spearman_pd.columns[i], spearman_pd.columns[j], spearman_pd.iloc[i, j]))

print(f'发现 {len(high_corr_pairs)} 对高度相关特征:')
for pair in high_corr_pairs:
    print(pair)

注意,这里有一个容易忽略的点:相关性不等于因果。即使两个特征高度相关,也不能说明它们表达同一个信息。我曾经在用户行为数据里发现“登录次数”和“购买金额”的相关系数是0.88,但事实上两者描述的是完全不同的行为模式。所以,删特征前一定要结合业务逻辑。

4.4 cuML集成可视化与性能洞察

4.4.1 使用cuML的预测可视化

cuML不直接提供画图功能,但它产生的预测结果可以很轻松地回流到可视化工具里。比如你做KMeans聚类,把聚类标签拿到后,用t-SNE降到2D来看效果。

from cuml.cluster import KMeans
from cuml.manifold import TSNE

# 用cuML训练KMeans
model = KMeans(n_clusters=5)
labels = model.fit_predict(df[['feature_A', 'feature_B', 'feature_C']])

# 用cuML的t-SNE降维
tsne = TSNE(n_components=2)
embedding = tsne.fit_transform(df[['feature_A', 'feature_B', 'feature_C']])

# 降维结果转pandas用于绘图
embedding_pd = embedding.to_pandas()
embedding_pd['cluster'] = labels.to_pandas().values

plt.figure(figsize=(10,8))
for cluster_id in range(5):
    subset = embedding_pd[embedding_pd['cluster'] == cluster_id]
    plt.scatter(subset[0], subset[1], s=10, label=f'Cluster {cluster_id}')
plt.title('KMeans聚类降维可视化')
plt.legend()
plt.show()

配合这个流程,你可以快速验证聚类效果的合理性。我一般在聚类后必做这一步——不然给业务方看一堆数字,他们根本不信。

4.4.2 性能对比与洞察

做探索的时候,我习惯记录一下耗时。因为在GPU上做EDA比在CPU上快多少,这本身就是一个有意思的洞察。你可以用Python的time模块简单地测一下。

import time

start = time.time()
# 可以替换为CPU版的Pandas操作
cpu_version = pdf.corr()
cpu_time = time.time() - start

start = time.time()
gpu_version = df.corr()
gpu_time = time.time() - start

print(f'CPU耗时:{cpu_time:.3f}s')
print(f'GPU耗时:{gpu_time:.3f}s')
print(f'加速比:{cpu_time/gpu_time:.1f}x')

跑过之后你会发现,当数据量超过100万行,GPU加速比通常在10倍以上。我自己在200万行×50个特征的数据集上测试过,相关矩阵计算加速了18倍。这就是我强调GPU做EDA的原因——你等得起吗?等不起就上GPU。

核心关键: 数据探索不是可选项,是必选项。先探索,再做特征工程,最后建模。这个顺序不要颠倒了。用cuML的家族做EDA,你不但跑得快,还能在探索过程中发现建模阶段可能出现的共线性、异常值和分布偏斜问题。习惯了之后,你会越来越依赖这样的速度。
数据探索与可视化决策流程 数据摄入 数据清洗 统计摘要 分布验证 相关性矩阵 可视化汇报 异常? 技术栈总结 cuDF 数据摄入和处理 cuPy 统计和直方图 cuML 相关性与降维