第1章:数据获取与清洗——用Pandas读取CSV/Excel环境数据,处理缺失值与异常值
大家好,我是老张。干环境数据分析这行十几年了,今天咱们聊聊数据获取与清洗。说实话,很多新手一上来就急着建模、跑预测,结果模型跑出来一塌糊涂。为什么?因为数据本身就有问题。你想想看,垃圾数据进去,再牛的算法也白搭。
我个人习惯,拿到任何环境数据,第一件事不是分析,而是先看看数据长什么样、有没有坑。今天这章,我就把这些年踩过的坑、总结的经验,一次性讲清楚。
1.1 环境数据的常见来源与格式
环境数据来源五花八门。我接触过的就有:
- 气象站数据:温度、湿度、风速、气压,通常是CSV格式
- 空气质量监测站:PM2.5、PM10、SO₂、NO₂,Excel或CSV居多
- 水文监测数据:水位、流量、水质指标,有时是数据库导出的文本文件
- 遥感数据:卫星影像反演的地表温度、植被指数,格式更复杂
咱们课程主要用CSV和Excel。为什么?因为这两种格式最常见,也最容易上手。我在项目中遇到过,有些合作方给的数据是Excel里套着Excel,一个文件几十个sheet,光找数据就花了半天。嗯,这种情况咱们后面会讲怎么处理。
1.2 使用Pandas读取CSV文件
Pandas是Python里处理表格数据的利器。读取CSV,一行代码搞定:
import pandas as pd
# 读取CSV文件
df = pd.read_csv('air_quality_beijing.csv')
print(df.head()) # 看看前5行
但实际项目中,CSV文件往往没那么规矩。我遇到过编码问题、分隔符问题、表头问题。来看几个常见场景:
| 问题 | 解决方案 | 代码示例 |
|---|---|---|
| 文件编码是GBK | 指定encoding参数 | pd.read_csv('data.csv', encoding='gbk') |
| 分隔符是分号 | 指定sep参数 | pd.read_csv('data.csv', sep=';') |
| 没有表头 | 设置header=None | pd.read_csv('data.csv', header=None) |
| 跳过前几行注释 | 设置skiprows | pd.read_csv('data.csv', skiprows=2) |
nrows=100 参数就行。
1.3 读取Excel文件
Excel文件比CSV复杂一些,因为一个文件可能有多个sheet。我建议这样操作:
# 读取Excel文件
df = pd.read_excel('water_quality.xlsx', sheet_name='监测数据')
# 如果不知道sheet名字,先看看有哪些sheet
xls = pd.ExcelFile('water_quality.xlsx')
print(xls.sheet_names)
# 读取所有sheet
all_sheets = pd.read_excel('water_quality.xlsx', sheet_name=None)
这里有个坑。我曾经处理过一个Excel文件,里面有些单元格是合并的,读进来全是NaN。后来发现,合并单元格在Pandas里默认只保留左上角的值,其他位置都是空值。解决办法是提前在Excel里取消合并,或者用 fillna(method='ffill') 向前填充。
1.4 数据概览与基本信息
数据读进来了,先别急着分析。花30秒看看数据的基本情况:
# 查看数据形状
print(f"数据有 {df.shape[0]} 行,{df.shape[1]} 列")
# 查看列名和数据类型
print(df.dtypes)
# 查看统计摘要
print(df.describe())
# 查看缺失值情况
print(df.isnull().sum())
我个人习惯,拿到数据先看三样东西:数据量够不够、有没有缺失值、数据类型对不对。比如时间列应该是datetime类型,如果读进来是object,那后面做时间序列分析就会出问题。
pd.to_datetime() 转换,否则后续的resample、shift等操作都会报错。
1.5 处理缺失值
环境数据里缺失值太常见了。仪器故障、网络中断、人为漏记,原因五花八门。处理缺失值,我一般分三步走:
1.5.1 查找缺失值
# 查看每列缺失值数量
missing = df.isnull().sum()
print(missing[missing > 0])
# 查看缺失值占比
missing_pct = df.isnull().mean() * 100
print(missing_pct[missing_pct > 0])
1.5.2 决定处理策略
缺失值怎么处理,取决于数据的重要性和缺失比例。我常用的策略:
- 缺失比例 < 5%:直接删除,用
df.dropna() - 缺失比例 5%-20%:用均值/中位数/前向填充
- 缺失比例 > 20%:需要慎重,可能要考虑插值或模型预测
# 删除缺失值(谨慎使用)
df_clean = df.dropna()
# 用均值填充
df['PM2.5'] = df['PM2.5'].fillna(df['PM2.5'].mean())
# 前向填充(时间序列常用)
df['温度'] = df['温度'].fillna(method='ffill')
# 线性插值
df['风速'] = df['风速'].interpolate(method='linear')
1.6 处理异常值
异常值比缺失值更隐蔽,也更危险。它不会报错,但会悄悄扭曲你的分析结果。我见过最离谱的,一个温度传感器坏了,连续三天输出-999,有人没处理直接拿去建模,结果预测值全是负数。
1.6.1 识别异常值的方法
常用的方法有三种:
| 方法 | 适用场景 | 代码示例 |
|---|---|---|
| Z-score法 | 数据近似正态分布 | z = np.abs(stats.zscore(df['PM2.5'])) |
| IQR四分位距法 | 数据偏态分布 | Q1, Q3 = df['温度'].quantile([0.25, 0.75]) |
| 领域知识法 | 有明确的物理范围 | df[(df['风速'] >= 0) & (df['风速'] <= 50)] |
# IQR法示例
Q1 = df['温度'].quantile(0.25)
Q3 = df['温度'].quantile(0.75)
IQR = Q3 - Q1
# 定义异常值范围
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
# 找出异常值
outliers = df[(df['温度'] < lower_bound) | (df['温度'] > upper_bound)]
print(f"发现 {len(outliers)} 个异常值")
1.6.2 处理异常值的策略
找到异常值后,怎么处理?我的建议是:
- 明显错误值(比如负数的PM2.5):直接删除或设为NaN
- 极端但可能真实的值(比如沙尘暴时的PM2.5爆表):保留,但要标记
- 不确定的异常值:用前后值的均值替换
1.7 数据清洗的完整流程
说了这么多,咱们来总结一下。我一般按这个流程走:
# 完整的数据清洗流程
def clean_environment_data(df):
# 1. 备份原始数据
df_raw = df.copy()
# 2. 转换时间列
df['时间'] = pd.to_datetime(df['时间'])
df = df.set_index('时间')
# 3. 处理缺失值(前向填充)
df = df.fillna(method='ffill')
# 4. 处理异常值(IQR法)
for col in df.select_dtypes(include=[np.number]).columns:
Q1 = df[col].quantile(0.25)
Q3 = df[col].quantile(0.75)
IQR = Q3 - Q1
lower = Q1 - 3 * IQR # 用3倍IQR,更宽松
upper = Q3 + 3 * IQR
df[col] = df[col].clip(lower, upper)
# 5. 检查最终数据
print(f"清洗完成,剩余 {len(df)} 条记录")
return df
这个流程我用了好几年,基本能覆盖90%的环境数据问题。当然,具体项目还要具体调整。
1.8 本章知识体系
下面这张图,是我画的数据获取与清洗的核心流程。你看一眼,心里就有数了:
这张图把咱们今天讲的内容串起来了。从数据获取开始,到概览、缺失值、异常值、数据转换,最后得到干净的数据。每一步都有对应的Pandas方法,你照着做就行。
1.9 写在最后
数据清洗这件事,说白了就是「磨刀不误砍柴工」。我见过太多人急着跑模型,结果被脏数据坑得怀疑人生。你花30分钟把数据洗干净,后面建模可能省3个小时。这笔账,你自己算算。
下一章,咱们会讲时间序列数据的可视化。到时候,你会看到清洗前后的数据,画出来的图完全是两个世界。嗯,今天就到这儿,有问题随时交流。
公众号:蓝海资料掘金营,微信deep3321