3. CSV文件读写与校验

CSV(Comma-Separated Values)是科学计算中最常见的数据交换格式之一:它结构简单、通用性强,几乎任何工具都能读写。但“简单”不等于“省心”——编码、分隔符、缺失值、类型推断和格式异常,往往才是实际工作中真正消耗时间的地方。本节围绕两条主线展开:用标准库 csv 模块做精细控制,用 pandas 做高效批量读取与校验,并重点讲清楚缺失值处理、数据类型推断和格式校验这三个最容易出问题的环节。

一、csv 模块:底层可控的读写方式

csv 模块把每一行当作字符串列表处理,不做任何类型推断,因此适合需要逐行处理、字段顺序敏感或数据格式不规整的场景。读取时推荐使用 csv.DictReader,它把表头映射为字典键,代码可读性更好:

import csv

with open("measurements.csv", newline="", encoding="utf-8") as f:
    reader = csv.DictReader(f)
    rows = list(reader)

print(reader.fieldnames)   # ['sample_id', 'temperature', 'pressure', 'note']
print(rows[0])             # {'sample_id': 'S001', 'temperature': '25.3', ...}

这里有两个细节必须注意。第一,open 时要加 newline="",否则在 Windows 上可能出现空行;第二,encoding 要显式指定,中文环境常见 utf-8 与 gbk 混用,读错编码会直接抛 UnicodeDecodeError 或产生乱码。写入时用 csv.DictWriter 并调用 writeheader(),可以保证列顺序与字段名一致。需要强调的是:csv 模块读出的所有值都是字符串,"25.3" 不会自动变成浮点数,后续计算前必须自行转换,这也是它“可控但需要自己兜底”的体现。

二、pandas 读取 CSV:效率与类型推断

pandas.read_csv 在读取的同时完成类型推断,把整列看起来像数字的字段转成数值类型,看起来像日期的转成 datetime64。核心参数包括 sep(分隔符)、encoding、dtype(强制指定类型)、na_values(自定义缺失标记)、usecols(只读需要的列)和 parse_dates(解析日期列)。

import pandas as pd

df = pd.read_csv(
    "measurements.csv",
    encoding="utf-8",
    na_values=["", "NA", "N/A", "-"],
    dtype={"sample_id": "string"},
    parse_dates=["record_time"],
)
print(df.dtypes)
print(df.isna().sum())

类型推断并非总是可靠。典型陷阱是:某列本应是数值,但因为混入了一个 "abc" 或 "1,234"(带千分位逗号),整列会被推断为 object 字符串类型,后续求均值会报错或得到意外结果。因此读取后第一件事应是检查 df.dtypes,对关键数值列用 dtype 显式声明,或读取后用 pd.to_numeric(..., errors="coerce") 强制转换并把无法转换的值变为 NaN,从而把“脏数据”暴露出来而不是悄悄污染计算。

三、缺失值处理与格式校验

缺失值处理要先“看见”再“决定”。df.isna().sum() 给出每列缺失数量,df.isna().mean() 给出缺失比例。处理策略取决于业务含义:数值列可用均值、中位数或前向填充 ffill;分类列可填固定值如 "unknown";若某列缺失比例过高(例如超过 40%)或该字段是主键,则更合理的做法是删除对应行 dropna(subset=[...])。切忌不加判断地 fillna(0),因为 0 在温度、压力等物理量中可能是合法值,会掩盖真实缺失。

格式校验的目标是把“结构正确”与“内容合理”分开检查。结构层面校验列名是否齐全、行数是否异常、是否存在重复主键;内容层面校验数值范围、枚举取值、日期可解析性。下面这段校验逻辑可直接复用:

required = {"sample_id", "temperature", "pressure"}
missing_cols = required - set(df.columns)
assert not missing_cols, f"缺少列: {missing_cols}"

df["temperature"] = pd.to_numeric(df["temperature"], errors="coerce")
bad_range = df["temperature"].notna() & ~df["temperature"].between(-50, 200)
print("温度越界行数:", bad_range.sum())

dup = df["sample_id"].duplicated().sum()
print("重复主键数:", dup)

把上述检查组织成“读取 → 类型确认 → 缺失统计 → 范围/枚举校验 → 输出问题清单”的固定顺序,就能在数据进入计算流程前拦住大部分问题。下图概括了这条校验链中各环节的判断关系与分支走向。

CSV 读取与校验链路 读取 CSV encoding / sep / na_values 类型推断检查 df.dtypes / dtype 指定 缺失值统计 isna().sum() / mean() 关键列类型 是否可靠? 强制转换 to_numeric(errors="coerce") 无法转换 → NaN 暴露 否 格式校验 范围 / 枚举 / 日期解析 重复主键检查 是 缺失值处理决策 填充 / 删除 / 标记 unknown 输出清洗后数据 + 问题清单

最后补充一个实用习惯:把校验结果写成结构化的问题清单(列名、问题类型、涉及行号、示例值),而不是只打印一句“数据有问题”。这样在数据量大、需要回溯时,能快速定位到具体行,也便于把校验逻辑沉淀为可复用的函数,在每次读取新批次数据时自动执行。