3. 数据探索与可视化:描述性统计、分布分析、相关性分析、异常值检测、可视化实战
数据拿到手,第一件事是什么?
不是建模,不是调参。是看数据。
我见过太多人,上来就扔进模型,结果跑出来一堆垃圾。说白了,数据探索就像医生看病前的望闻问切。你连病人的体温、脉搏都不量,直接开药方?不靠谱。
这一章,我们就来聊聊怎么「看」储能数据。我会带你走一遍我自己的标准流程:描述性统计、分布分析、相关性分析、异常值检测,最后来一个完整的可视化实战。
核心观点:数据探索不是走过场,它是整个分析流程的基石。跳过这一步,后面全是坑。
3.1 描述性统计:先摸个底
我个人习惯,拿到数据第一件事,就是跑一个 df.describe()。别小看这行代码,它能告诉你很多信息。
import pandas as pd
import numpy as np
# 假设我们有一个储能电池的SOC(荷电状态)和温度数据
df = pd.DataFrame({
'soc': [85, 92, 78, 88, 95, 82, 90, 76, 84, 91, 87, 83, 89, 86, 94],
'temperature': [25.3, 26.1, 24.8, 25.9, 27.2, 25.0, 26.5, 24.2, 25.6, 26.8, 25.7, 25.1, 26.0, 25.4, 27.0],
'voltage': [3.65, 3.72, 3.58, 3.68, 3.78, 3.62, 3.70, 3.55, 3.66, 3.74, 3.69, 3.63, 3.71, 3.67, 3.76]
})
print(df.describe())
输出结果大概长这样:
| 统计量 | soc | temperature | voltage |
|---|---|---|---|
| count | 15.00 | 15.00 | 15.00 |
| mean | 86.27 | 25.77 | 3.67 |
| std | 5.42 | 0.89 | 0.06 |
| min | 76.00 | 24.20 | 3.55 |
| 25% | 83.00 | 25.10 | 3.63 |
| 50% | 87.00 | 25.70 | 3.68 |
| 75% | 90.00 | 26.50 | 3.71 |
| max | 95.00 | 27.20 | 3.78 |
你看,一眼就能看出:SOC均值86.27%,温度均值25.77°C,电压均值3.67V。标准差告诉你数据波动大不大。SOC的标准差5.42,说明电池之间的荷电状态差异不小。
我的经验:如果均值和中位数(50%)差距很大,说明数据有偏。比如SOC均值86.27,中位数87,差不多,说明分布还算对称。如果均值远大于中位数,那就有右偏,可能有极端高值。
3.2 分布分析:数据长什么样?
描述性统计只能给你几个数字,但数据长什么样,还得看图。
我习惯先画直方图。为什么?因为直方图能直观告诉你数据集中在哪,有没有双峰,有没有拖尾。
import matplotlib.pyplot as plt
import seaborn as sns
plt.figure(figsize=(12, 4))
plt.subplot(1, 3, 1)
sns.histplot(df['soc'], bins=8, kde=True)
plt.title('SOC分布')
plt.subplot(1, 3, 2)
sns.histplot(df['temperature'], bins=8, kde=True)
plt.title('温度分布')
plt.subplot(1, 3, 3)
sns.histplot(df['voltage'], bins=8, kde=True)
plt.title('电压分布')
plt.tight_layout()
plt.show()
嗯,这里要注意。KDE曲线(核密度估计)能帮你看出数据的大致概率密度。如果KDE曲线有两个峰,那你的数据可能来自两个不同的群体。我在一个储能项目中就遇到过——白天和夜晚的SOC分布完全不同,后来发现是充放电策略导致的。
箱线图也是我的必备工具。它能同时展示中位数、四分位数和异常值。
plt.figure(figsize=(8, 4))
sns.boxplot(data=df[['soc', 'temperature', 'voltage']])
plt.title('箱线图:SOC、温度、电压')
plt.show()
箱线图里,箱子中间的线是中位数,箱子的上下边界是Q1和Q3。箱子外面的「须」延伸到非异常值的范围。如果某个点超出了须的范围,那就是异常值候选。
注意:箱线图默认用1.5倍IQR(四分位距)来判定异常值。这个阈值不是万能的。对于储能数据,有时候3倍IQR更合适,因为电池数据本身波动就大。我曾经因为用了默认阈值,把正常的充放电峰值当成了异常值,差点误判。
3.3 相关性分析:谁和谁有关系?
储能系统里,变量之间往往相互影响。SOC和电压有关系吗?温度和SOC有关系吗?
相关性分析就是回答这个问题的。
# 计算皮尔逊相关系数
corr_matrix = df.corr()
print(corr_matrix)
输出:
| soc | temperature | voltage | |
|---|---|---|---|
| soc | 1.00 | 0.89 | 0.97 |
| temperature | 0.89 | 1.00 | 0.85 |
| voltage | 0.97 | 0.85 | 1.00 |
你看,SOC和电压的相关系数高达0.97,这很正常——电池的电压和荷电状态本来就是强相关的。温度和SOC也有0.89,说明温度高的时候SOC也高?不一定,可能是充放电过程中温度上升导致的。
相关性不等于因果。这个坑我踩过。有一次我看到温度和SOC强相关,就以为是温度影响了SOC,后来发现是充电时电流大导致温度升高,同时SOC也在上升。说白了,它们都是充电这个行为的「果」。
热力图是展示相关性的好方式:
plt.figure(figsize=(6, 5))
sns.heatmap(corr_matrix, annot=True, cmap='coolwarm', vmin=-1, vmax=1)
plt.title('相关性热力图')
plt.show()
颜色越深,相关性越强。红色正相关,蓝色负相关。一眼就能看出哪些变量关系紧密。
3.4 异常值检测:揪出捣乱分子
储能数据里,异常值很常见。传感器故障、通信丢包、电池单体不一致,都会产生异常值。如果不处理,模型会被带偏。
我常用的方法有三种:
- Z-score方法:假设数据服从正态分布,Z-score大于3或小于-3的视为异常值。
- IQR方法:小于Q1-1.5*IQR或大于Q3+1.5*IQR的视为异常值。
- DBSCAN聚类:基于密度的聚类,孤立点就是异常值。
from scipy import stats
# Z-score方法
z_scores = np.abs(stats.zscore(df['soc']))
outliers_z = df[z_scores > 3]
print(f"Z-score方法检测到 {len(outliers_z)} 个异常值")
# IQR方法
Q1 = df['soc'].quantile(0.25)
Q3 = df['soc'].quantile(0.75)
IQR = Q3 - Q1
outliers_iqr = df[(df['soc'] < Q1 - 1.5*IQR) | (df['soc'] > Q3 + 1.5*IQR)]
print(f"IQR方法检测到 {len(outliers_iqr)} 个异常值")
我的建议:不要只依赖一种方法。我通常先用IQR方法快速筛查,再用DBSCAN做二次验证。如果两种方法都标记为异常值,那基本可以确定是异常。如果只有一种方法标记,我会结合业务逻辑判断——比如SOC突然从80%跳到100%,可能是充电完成,不一定是异常。
3.5 可视化实战:把故事讲出来
好了,理论说完了。我们来一个完整的实战案例。
假设你拿到了一批储能电池的实时监控数据,包含SOC、温度、电压、电流、循环次数。你想看看这批电池的健康状态。
# 生成模拟数据
np.random.seed(42)
n = 200
data = {
'soc': np.random.normal(85, 10, n),
'temperature': np.random.normal(26, 3, n),
'voltage': np.random.normal(3.7, 0.1, n),
'current': np.random.normal(50, 15, n),
'cycle': np.random.randint(1, 1000, n)
}
df_real = pd.DataFrame(data)
# 1. 整体概览:pairplot
sns.pairplot(df_real, diag_kind='kde')
plt.suptitle('储能数据多维分布', y=1.02)
plt.show()
Pairplot能让你同时看到所有变量的两两关系和各自的分布。对角线是分布图,非对角线是散点图。如果某个散点图呈现明显的线性关系,说明这两个变量高度相关。
# 2. 时间序列可视化(假设按时间排序)
plt.figure(figsize=(12, 6))
plt.plot(df_real['soc'].values[:100], label='SOC', color='#4a90d9')
plt.plot(df_real['temperature'].values[:100], label='温度', color='#e57373')
plt.xlabel('时间点')
plt.ylabel('数值')
plt.title('SOC与温度随时间变化')
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()
时间序列图能帮你发现趋势和模式。比如SOC在某个时间段突然下降,可能是放电开始。温度跟着上升,说明电池在发热。
# 3. 异常值可视化:用散点图标记
plt.figure(figsize=(8, 6))
plt.scatter(df_real['soc'], df_real['voltage'], c='blue', alpha=0.6, label='正常数据')
# 假设我们标记出异常值
outliers = df_real[(df_real['soc'] < 60) | (df_real['soc'] > 100)]
plt.scatter(outliers['soc'], outliers['voltage'], c='red', s=100, marker='x', label='异常值')
plt.xlabel('SOC (%)')
plt.ylabel('电压 (V)')
plt.title('SOC vs 电压:异常值标记')
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()
你看,红色叉号标记的就是异常值。它们明显偏离了正常数据的分布范围。这种可视化比单纯看数字直观多了。
总结一下:数据探索不是一次性的工作。我通常会反复做几轮——先看描述性统计,再画分布图,然后做相关性分析,最后检测异常值。每一轮都会发现新的问题,然后回去修正数据。这个过程就像剥洋葱,一层一层剥开,直到看到数据的本质。
好了,这一章的内容就到这里。记住,数据探索花的时间越多,后面建模就越顺利。别急着往前冲,先把数据看透。
公众号:蓝海资料掘金营,微信deep3321