4. Pandas 数据读取与清洗:CSV、Excel、数据库与脏数据治理
这一章,我们来解决一个非常现实的问题:数据不会自己收拾干净等着你。我在项目里遇到过太多次了,实验室给了个 Excel,打开一看,缺值、重复行、列名乱码、日期格式五花八门……你想想看,如果连基础数据都脏,后面那堆统计模型全都是白搭。
所以,数据清洗不是「锦上添花」,而是「雪中送炭」。一个批次质量控制流程,80% 的坑都埋在原始数据里。我们把 Pandas 读取数据的底层逻辑理清楚,再把缺失值、重复值、数据类型这三座大山翻过去,后续的质控分析才能真正跑起来。
4.1 数据读取:从文件到 DataFrame
Pandas 为我们准备了三种最常见的读取方式:CSV、Excel、SQL 数据库。记住,读取时就把类型、缺失值标记、日期格式指定好,后面能省一大半力气。这是我的核心习惯——不要等清洗阶段再去猜列的类型,一开始就给它定调。
4.2 读取 CSV:不只是 pd.read_csv
CSV 是批次质控中最常见的物料清单 / 检测结果存储格式。很多人只会传一个文件路径,但实战中,CSV 往往混杂着中文字段、千分符、空行和注释行。
下面这个例子是我在一个批次检测项目中收上来的实验汇总表:
import pandas as pd
df = pd.read_csv(
'batch_result.csv',
encoding='utf-8-sig', # 处理中文 BOM
sep=',', # 明确分隔符,别用默认赌运气
header=0, # 第一行是列名
dtype={'BatchID': str, 'Temp': float},
parse_dates=['MeasureDate'],
na_values=['--', 'N/A', '空', '']
)
print(df.head())
关键参数速查表:
| 参数 | 作用 | 实战场景 |
|---|---|---|
| encoding | 指定字符编码 | utf-8-sig 解决 Excel 另存的 CSV BOM 问题 |
| sep | 分隔符 | 遇到 Tab 分隔就传 sep='\t' |
| dtype | 列类型显式声明 | 避免 BatchID 被读成数字 |
| parse_dates | 指定日期列 | 把字符串日期转成 datetime 类型 |
| na_values | 自定义缺失值标记 | -- / N/A / 空 都映射成 NaN |
| skiprows | 跳过前 N 行 | 文件头有几行备注时使用 |
| nrows | 只读前 N 行 | 快速预览大文件结构 |
nrows=100 预览一次,确认列名、分隔符、编码都对了,再全量读取。这样不会因为一个解析报错浪费五分钟。
4.3 读取 Excel:多 Sheet 处理与 xlrd/openpyxl 之争
Excel 在批次管理里比 CSV 更顽固。一个 workbook 里可能藏着「原材料检测」、「成品测试」、「备注」三个 sheet。我建议你一次性把 Sheet 名称都打印出来,再决定读哪个。
# 先看所有 Sheet 名
xl_file = pd.ExcelFile('batch_202401.xlsx')
print(xl_file.sheet_names)
# 按需读取
df_raw = pd.read_excel(
xl_file,
sheet_name='原材料检测',
header=0,
dtype={'SupplierID': str},
engine='openpyxl' # .xlsx 推荐
)
Excel 读取时特别注意三点:
- 合并单元格: 读取时自动填充上层值?不会。你需要手动
ffill()或提前拆开。 - 日期格式: Excel 的日期序列值(如 44927)会被解析为整数;设置
converters或者读取后用pd.to_datetime转。 - 空行: 很多 Excel 表格末尾有空白行,读取后记得
dropna(how='all')。
4.4 读取数据库:SQLAlchemy + read_sql
当批次数据量大到几十万行,或者需要从 LIMS(实验室信息管理系统)直接取数时,CSV / Excel 就不够看了。直接用 read_sql 连数据库,一步到位。
from sqlalchemy import create_engine
engine = create_engine(
'mysql+pymysql://user:pass@host:3306/lims_db'
)
query = """
SELECT batch_id, measure_time, temperature, humidity
FROM batch_measurements
WHERE measure_date >= '2024-01-01'
"""
df_sql = pd.read_sql(query, engine, dtype={'batch_id': str})
print(df_sql.shape)
4.5 数据类型查看与转换
读取完数据,第一件事就是看 dtypes。
print(df.dtypes)
你可能会发现:批次号(BatchID)变成了 int64,日期列是 object,温度列混进了字符串。如果不处理,后面的统计 mean()、std() 全部失效。
类型转换三板斧:
astype():最常用,但容易踩。如果列中存在无法转换的值,会直接抛异常。pd.to_numeric():配合errors='coerce',把非法值变成 NaN,这是我最推荐的方式。pd.to_datetime():日期字符串转 datetime,同样支持errors='coerce'。
# 安全的数值转换
df['Temperature'] = pd.to_numeric(df['Temperature'], errors='coerce')
# 日期转换
df['MeasureDate'] = pd.to_datetime(df['MeasureDate'], errors='coerce')
# 字符串列统一 astype
df['BatchID'] = df['BatchID'].astype(str)
errors='coerce'? 因为批次数据里经常混入「仪器报错」、"NaN"、"-" 等非数值内容。一棍子全转成 NaN,再做下一步填充或删除,远比程序崩溃强得多。
4.6 缺失值处理:Drop 还是 Fill?
缺失值处理没有银弹。我习惯先问自己三个问题:
- 这个缺失值占整列的比例是多少?
- 这个特征对后续质控有多重要?
- 缺失的模式是随机缺失,还是系统性缺失(比如某台仪器坏了)?
删除缺失:
# 删除任何包含缺失的行(慎用,可能删掉一半数据)
df.dropna(inplace=True)
# 只删除关键列(如浓度、温度)缺失的行
df.dropna(subset=['Concentration', 'Temperature'], inplace=True)
填充缺失:
# 固定值填充(例如未检测到的浓度填 0)
df['Concentration'].fillna(0, inplace=True)
# 前向填充,适用于时间序列中临时缺失
df['Temperature'].ffill(inplace=True)
# 均值填充(谨慎使用,会拉低方差)
mean_temp = df['Temperature'].mean()
df['Temperature'].fillna(mean_temp, inplace=True)
fillna(0),结果把日期列也填成了 0。 填缺失值之前,先用 df.isnull().sum() 列出每列的缺失计数,按列分别处理,不要无脑全部填充。
4.7 重复值:真假之分
重复行可能是仪器误报、人工重复录入,也可能是真实的平行样。先看看再说:
# 检查完全重复行
dups = df.duplicated()
print(dups.sum())
# 只看关键字段的重复(比如批次号 + 时间)
key_dups = df.duplicated(subset=['BatchID', 'MeasureDate'])
print(key_dups.sum())
如果确认是误录,直接删除:
df.drop_duplicates(subset=['BatchID', 'MeasureDate'], keep='first', inplace=True)
参数 keep 的含义:
| keep 值 | 保留哪个 | 批次场景 |
|---|---|---|
| 'first' | 保留第一条重复 | 默认,一般为第一次录入 |
| 'last' | 保留最后一条重复 | 最后修改的记录更准确 |
| False | 全删,一条不留 | 只保留独立样本,极为严格 |
4.8 过滤:布尔索引与 query
清洗完缺值和重复,下一步就是筛选有效批次。我常用三种方式:
# 方式一:布尔索引(最快,可读性好)
df_valid = df[(df['Temperature'] >= 15) & (df['Temperature'] <= 30)]
# 方式二:query 方法(适合复杂条件,字符串友好)
df_valid = df.query('15 <= Temperature <= 30 and Concentration > 0')
# 方式三:isin 筛选特定批次
valid_batches = ['B001', 'B003', 'B012']
df_filtered = df[df['BatchID'].isin(valid_batches)]
query。不推荐链式赋值(如 df[df.a > 0]['b'] = 1),容易触发 SettingWithCopyWarning,我吃过这个亏。
4.9 综合流程:从读取到可用 DataFrame
把上面这些步骤串成一个流程,你会发现数据清洗不再是头痛医头。下面这张图展示了我每次拿到新批次数据后的操作顺序:
看到上图了吗?四阶段不是线性的,清洗操作有时候需要往返:过滤完发现还有缺失,回头再补一轮填充,这在真实项目中太常见了。不要觉得一次就能搞定,迭代两次才算完成。
现在,你可以用 Pandas 把任意一种原始批次数据——CSV 也好,Excel 也好,数据库也好——完整地读进来,并被清洗成一个结构清晰、类型正确、无缺无重的 DataFrame。有了它,下一章的那些质控图、控制限计算才能真实地反映生产过程。(当然,那是后话,先把这个清洗功夫练扎实。)