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 行快速预览大文件结构
我个人习惯: 在读取大 CSV 时,先用 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)
我曾经踩过一个坑: 某次从 SQL Server 拉数据,字段名里有空格,Pandas 直接报错。后来我把查询中的列名用 AS 重命名,去掉特殊字符。从数据库读取,先把「列名友好化」这一步在 SQL 里完成,别交到 Python 层再处理。

4.5 数据类型查看与转换

读取完数据,第一件事就是看 dtypes。

print(df.dtypes)

你可能会发现:批次号(BatchID)变成了 int64,日期列是 object,温度列混进了字符串。如果不处理,后面的统计 mean()、std() 全部失效。

类型转换三板斧:

  1. astype():最常用,但容易踩。如果列中存在无法转换的值,会直接抛异常。
  2. pd.to_numeric():配合 errors='coerce',把非法值变成 NaN,这是我最推荐的方式。
  3. 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)
我曾经见过团队直接对整个 DataFrame 用 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

把上面这些步骤串成一个流程,你会发现数据清洗不再是头痛医头。下面这张图展示了我每次拿到新批次数据后的操作顺序:

数据读取与清洗四阶段流程 1. 数据读取 2. 类型检查 3. 清洗操作 4. 过滤 读取方式 · read_csv · read_excel · read_sql 类型检查 · dtypes 查看 · to_numeric · to_datetime 清洗操作 · 缺失值处理 · 重复值删除 · 统一格式 布尔 过滤 isin query 可用 DataFrame 核心原则:清洗在前,分析在后 每一步都用 .shape 和 .info() 确认效果

看到上图了吗?四阶段不是线性的,清洗操作有时候需要往返:过滤完发现还有缺失,回头再补一轮填充,这在真实项目中太常见了。不要觉得一次就能搞定,迭代两次才算完成。

现在,你可以用 Pandas 把任意一种原始批次数据——CSV 也好,Excel 也好,数据库也好——完整地读进来,并被清洗成一个结构清晰、类型正确、无缺无重的 DataFrame。有了它,下一章的那些质控图、控制限计算才能真实地反映生产过程。(当然,那是后话,先把这个清洗功夫练扎实。)

练习: 找一个你手头的批次 Excel 文件,尝试用本节的方法读取、查看数据类型、处理缺失值和重复值,然后过滤出「温度在 20~25℃ 之间且浓度大于 0」的有效记录。坚持三步走:读 → 看 → 洗。一周练三遍,你立刻成为团队里最会洗数据的人。