3. 探索性数据分析(EDA):描述性统计、数据可视化、相关性分析
说实话,很多同学拿到环境数据的第一反应就是直接上模型。我当年也犯过这个错——数据一丢进去就跑XGBoost,结果模型效果差得离谱。后来才明白,EDA才是整个建模流程中最值钱的一步。你想想看,连数据长什么样都不知道,怎么可能选对特征、调好参数?
核心观点:EDA不是走过场,它是你和数据之间的第一次深度对话。花在EDA上的时间,会在后续建模中十倍百倍地回报你。
3.1 描述性统计:先摸清数据的底细
我个人习惯,拿到任何环境数据集,第一件事就是跑df.describe()。这玩意儿能告诉你每个特征的均值、标准差、最小值、最大值、四分位数。说白了,就是给数据做个全面体检。
import pandas as pd
import numpy as np
# 加载环境监测数据
df = pd.read_csv('air_quality.csv')
print(df.head())
# 描述性统计
desc = df.describe()
print(desc)
# 检查缺失值
print(df.isnull().sum())
# 数据类型检查
print(df.dtypes)
我在项目中遇到过一件事:有一次分析某城市的PM2.5数据,describe()一看,最大值居然显示9999。嗯,这明显是传感器故障或者传输错误。如果不做这一步,直接拿这个异常值去训练模型,后果可想而知。
这里有几个关键点要盯住:
- 均值 vs 中位数:如果两者差距很大,说明数据有偏态分布。比如PM2.5数据,均值远大于中位数,意味着存在一些极端高值在拉高平均。
- 标准差:数值太大说明数据波动剧烈,可能需要做标准化处理。
- 缺失值比例:超过30%的缺失,我建议直接考虑删除这个特征,或者用更复杂的插补方法。
我的小技巧:除了describe(),我还会用df.info()快速查看数据类型和内存占用。环境数据经常有日期时间列,记得先转成datetime类型,不然后面画图会出问题。
3.2 数据可视化:一张好图胜过千言万语
数字是冷冰冰的,但图表会说话。我个人认为,EDA阶段至少要做三种图:箱线图、直方图、散点图矩阵。每种图解决不同的问题。
3.2.1 箱线图:揪出异常值的神器
箱线图能直观展示数据的分布和异常值。我曾经用箱线图发现过一个有趣的现象:某监测站点的SO2数据,每年冬季都会出现一批异常高值,后来查出来是附近工厂在夜间偷排。
import matplotlib.pyplot as plt
import seaborn as sns
# 设置中文字体
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False
# 绘制箱线图
fig, axes = plt.subplots(2, 3, figsize=(15, 10))
features = ['PM2.5', 'PM10', 'SO2', 'NO2', 'CO', 'O3']
for i, feature in enumerate(features):
row = i // 3
col = i % 3
sns.boxplot(y=df[feature], ax=axes[row, col])
axes[row, col].set_title(f'{feature} 箱线图')
plt.tight_layout()
plt.show()
看箱线图时,重点关注那些「飞出去」的点——也就是超出上下须的点。这些点可能是:
- 传感器故障导致的错误数据
- 真实但罕见的极端事件(比如沙尘暴期间的PM10暴增)
- 数据录入错误
避坑指南:我曾经直接把所有异常值都删掉了,结果模型在预测极端天气时完全失效。后来学乖了——先判断异常值是「噪声」还是「信号」。如果是真实事件,建议保留并单独标记。
3.2.2 直方图:看数据长什么样
直方图告诉你数据的分布形态。是正态分布?偏态分布?还是双峰分布?这直接决定了你后续用什么模型。
# 绘制直方图
fig, axes = plt.subplots(2, 3, figsize=(15, 10))
for i, feature in enumerate(features):
row = i // 3
col = i % 3
axes[row, col].hist(df[feature].dropna(), bins=50, alpha=0.7, edgecolor='black')
axes[row, col].set_title(f'{feature} 分布')
axes[row, col].set_xlabel(feature)
axes[row, col].set_ylabel('频数')
plt.tight_layout()
plt.show()
你想想看,如果PM2.5的分布是严重右偏的,那用线性回归去拟合就会出问题。这时候我通常会考虑做对数变换或者Box-Cox变换,让数据更接近正态分布。
3.2.3 散点图矩阵:一眼看出变量关系
散点图矩阵能同时展示多个变量两两之间的关系。这是特征选择前必做的功课。
# 散点图矩阵
sns.pairplot(df[features].dropna(), diag_kind='kde')
plt.show()
看散点图矩阵时,我主要关注三点:
- 线性关系:点大致呈一条直线,说明两个变量线性相关
- 非线性关系:点呈曲线或U形,说明可能需要做特征工程
- 聚类现象:点分成几堆,说明数据可能有分组结构
经验之谈:如果散点图矩阵太大(比如超过10个特征),我会先做一次初步筛选。不然图会密密麻麻,什么都看不清。我一般先保留5-8个最有可能相关的特征来画矩阵。
3.3 相关性分析:量化变量间的亲疏关系
可视化能给你直觉,但相关性分析给你数字。我常用的方法有三种:Pearson相关系数、Spearman秩相关系数、热力图。
3.3.1 Pearson相关系数
这是最常用的方法,衡量两个连续变量之间的线性相关程度。取值范围[-1, 1],绝对值越大相关性越强。
# 计算Pearson相关系数
corr_matrix = df[features].corr(method='pearson')
print(corr_matrix)
# 热力图可视化
plt.figure(figsize=(10, 8))
sns.heatmap(corr_matrix, annot=True, cmap='coolwarm', fmt='.2f',
linewidths=0.5, square=True)
plt.title('环境特征相关性热力图')
plt.show()
看热力图时,我有个习惯:先找绝对值大于0.7的相关系数。这些是强相关特征,需要特别注意:
- 正相关(>0.7):比如PM2.5和PM10通常高度正相关,因为它们来源相似。这种情况下,我可能会只保留其中一个,避免多重共线性。
- 负相关(<-0.7):比如温度和湿度往往负相关。这种关系在环境数据中很常见。
- 弱相关(接近0):说明这两个变量基本没关系,可以考虑删除其中一个作为特征。
3.3.2 Spearman秩相关系数
当数据不满足正态分布,或者存在明显的非线性关系时,我会改用Spearman相关系数。它基于数据的排名而不是原始数值,对异常值不那么敏感。
# Spearman相关系数
spearman_corr = df[features].corr(method='spearman')
print(spearman_corr)
我记得有一次分析臭氧数据,Pearson相关系数显示温度和臭氧只有0.3的相关性,但Spearman系数却高达0.6。为什么?因为臭氧和温度的关系不是线性的——温度超过某个阈值后,臭氧浓度会急剧上升。这种情况下,Spearman更能捕捉到这种单调关系。
关键决策点:如果两个特征的相关系数绝对值大于0.8,我建议只保留其中一个。比如PM2.5和PM10,保留PM2.5就够了,因为PM2.5对健康影响更大,而且模型输入更简洁。
3.4 EDA知识体系总览
下面这张图是我自己总结的EDA核心流程,每次做项目前我都会瞄一眼,确保不遗漏关键步骤。
3.5 实战中的常见坑与对策
做了这么多年环境数据分析,我踩过的坑真不少。这里分享几个最常见的:
| 常见问题 | 表现 | 我的对策 |
|---|---|---|
| 异常值误判 | 把真实极端事件当成噪声删除 | 结合气象日志和专家经验判断,不盲目删除 |
| 多重共线性 | 模型系数不稳定,预测波动大 | 用VIF(方差膨胀因子)检测,保留VIF<10的特征 |
| 忽略时间依赖性 | 把时间序列数据当成独立样本处理 | 画时间序列图,检查自相关函数(ACF) |
| 数据泄露 | 用未来数据预测过去 | 严格按时间顺序划分训练集和测试集 |
特别提醒:环境数据往往有空间相关性。比如一个站点的PM2.5浓度,和它周围站点的数据高度相关。如果你忽略这一点,模型可能会学到「伪相关」。我建议在做EDA时,把站点位置信息也纳入分析范围。
好了,EDA这部分就聊到这儿。记住一句话:EDA做得越扎实,后面的模型就越靠谱。别急着跑模型,先和数据好好聊聊天。
公众号:蓝海资料掘金营,微信deep3321