4.4 导入与导出表格数据
文件并不是一个等待打开的 DataFrame。它由字节与格式约定组成,解析过程会把字节转换成列、值、缺失标记与 dtype。因此,每个解析器默认值都是一项决策。可靠分析会显式声明重要决策,并在读取后验证结果。
完整载入之前先检查边界
导入陌生文件之前,先确认:
- 文件格式与分隔符。
- 字符编码,通常是 UTF-8,但不能想当然。
- 是否存在表头,以及列名是否唯一。
- 哪些字符串代表缺失值。
- 小数、千位、日期与时区约定。
- 标识符是否含前导零,或是否超出安全数值范围。
- 预期列、行数规模与畸形行处理策略。
应使用文本工具预览原始内容。不要把不可信文件当作可执行代码打开,也不要为了发现表头就完整加载数 GB 的文件。
对于下面的 CSV 内容:
order_id,ordered_at,zone,minutes,debug_note
00127,2026-08-01,North,31,ok
00128,2026-08-02,South,UNKNOWN,late scan
00129,2026-08-03,West,44,ok应定义导入契约:
import pandas as pd
required = ["order_id", "ordered_at", "zone", "minutes"]
deliveries = pd.read_csv(
"orders.csv",
usecols=required,
dtype={
"order_id": "string",
"zone": "string",
},
parse_dates=["ordered_at"],
date_format="%Y-%m-%d",
na_values={"minutes": ["UNKNOWN"]},
encoding="utf-8",
on_bad_lines="error",
)
deliveries["minutes"] = deliveries["minutes"].astype("Int64")usecols 限制导入边界,也记录了哪些字段重要。把 order_id 声明为字符串,可以保留 00127;若它先被解析成整数 127,就无法恢复原始标识符格式。parse_dates 标记需要解析的日期列,date_format 消除歧义,也可能提升解析性能。按列设置 na_values,可以避免把其他文本列中的 UNKNOWN 也误判成缺失。
on_bad_lines="error" 让畸形行保持可见。跳过畸形数据可以是一项明确的隔离策略,但静默丢弃不属于数据清洗。应保存被拒绝的行及其数量,以供审计。
解析实验是一张边界解剖台,而不是一组预设选项。你要直接编辑原始 CSV 与解析清单,再运行分隔符和引号扫描。前导零丢失、畸形记录、数值列文本污染、接受行数与推断 dtype 都由你写下的文件和契约真实计算。
成功解析只是第一个检查点
读取后应立即检查并验证:
print(deliveries.shape)
print(deliveries.head())
print(deliveries.dtypes)
print(deliveries.isna().sum())
assert list(deliveries.columns) == required
assert deliveries["order_id"].notna().all()
assert deliveries["order_id"].is_unique
assert deliveries["ordered_at"].notna().all()
assert deliveries["minutes"].dropna().between(0, 24 * 60).all()这些检查回答不同问题:
- 结构:必需列是否存在?名称与顺序是否符合预期?
- 身份:订单键是否存在且唯一?
- 类型:日期与可空整数是否按预期解析?
- 有效性:数值是否满足合理范围与类别规则?
- 完整性:解析在哪里引入或保留了缺失值?
解析器完全可以成功生成一张所有用时都是文本、所有日期都是缺失,或两笔订单共享标识符的 DataFrame。“没有异常”不是质量标准。
对于大型文件,可以用 nrows 抽样检查结构,用 chunksize 迭代处理可控大小的数据块:
for chunk in pd.read_csv("orders.csv", chunksize=100_000, **read_options):
validate_chunk(chunk)
process_chunk(chunk)分块会改变算法。全局重复检查、分位数或排序无法在每个数据块内独立验证,必须维护跨块状态,或使用适合该操作的存储引擎。第 11 章会在时间有序数据中再次讨论流式思想。
导出是一项接口契约
CSV 具有广泛互操作性。当默认位置索引没有业务含义时,应显式省略:
deliveries.to_csv(
"orders_clean.csv",
index=False,
encoding="utf-8",
na_rep="UNKNOWN",
date_format="%Y-%m-%d",
)如果没有 index=False,再次读取时往往会看到 Unnamed: 0 这样的额外列。如果索引是真正的业务键,就应给它命名,并决定接收系统是否把它作为字段使用。不要让序列化默认值替你作出决定。
CSV 保存字符,不保存完整 pandas 模式。重新导入时,字符串标识符可能被推断成整数,可空整数可能变成浮点数,分类列会丢失分类定义,时区细节也需要显式处理。因此,对应的读取参数也是交换契约的一部分。
对于分析存储,Parquet 是一种列式格式(columnar format),通常能保留更多类型信息,也支持选择性读取列与压缩:
deliveries.to_parquet(
"orders_clean.parquet",
index=False,
)
restored = pd.read_parquet(
"orders_clean.parquet",
columns=required,
)Parquet 需要 PyArrow 等兼容引擎。引擎版本与跨系统类型映射仍然会影响结果,因此要验证恢复的数据,不要假定它在所有系统间完美移植。格式选择取决于消费者:电子表格和简单集成常用 CSV,分析管道则能从保留模式的列式存储中受益。
不要用 pickle 交换不可信数据。加载 pickle 可能执行代码,而且 pickle 与 Python 对象版本紧密相关。应选择可互操作的数据格式并进行验证。
测试往返,而不只是写入调用
往返(round trip)是指先写出数据,再按记录好的契约读取,并比较恢复结果与源数据:
from io import StringIO
from pandas.testing import assert_frame_equal
buffer = StringIO()
deliveries.to_csv(
buffer,
index=False,
na_rep="UNKNOWN",
date_format="%Y-%m-%d",
)
buffer.seek(0)
restored = pd.read_csv(
buffer,
dtype={"order_id": "string", "zone": "string"},
parse_dates=["ordered_at"],
date_format="%Y-%m-%d",
na_values={"minutes": ["UNKNOWN"]},
)
restored["minutes"] = restored["minutes"].astype("Int64")
assert_frame_equal(deliveries[required], restored[required])默认情况下,assert_frame_equal 会检查值、标签、顺序与 dtype。若交换契约允许某种差异,例如无害的浮点舍入,应只放宽对应比较,并记录容差。
往返实验使用对抗式属性测试。你可以编辑源记录与交换契约,再生成含前导零标识符、超大数字、电子表格公式和分隔符文本的边界行。值、dtype、列集合与电子表格安全分别测试,避免一次“写入成功”伪装成真正等价。
安全写出生产结果
生产导出还需要操作层面的保障:
- 在目标文件系统中写入临时文件,验证后再在系统支持时原子替换最终路径。
- 不要用转换结果覆盖源数据的唯一副本。
- 当可审计性重要时,记录源版本、转换版本、行数、模式与校验和。
- 为电子表格用户导出不可信文本时,应转义或消除公式前缀。
- 保护凭据与敏感字段;方便的导出仍可能构成数据泄露。
- 测试真实消费路径,包括编码、换行符、时区与缺失标记。
第 4 章现场检查表
把 DataFrame 交给下一阶段之前,确认:
- 行身份、索引唯一性、列含义与 dtype 都已明确。
- 标签对齐出于明确意图,不匹配标签在解决前保持可见。
.loc与.iloc符合预期坐标系统和结果维数。
- 筛选对缺失值有命名策略,排序有确定规则。
- 在写时复制机制下,赋值通过一次操作指向一个明确对象。
- 文件解析、验证、导出与往返行为共同构成一份记录好的契约。
第 5 章将处理这些检查暴露的问题:缺失观测、重复实体、不一致的字符串与日期、混合单位和异常值。目标不是让警告消失,而是在保留证据的同时作出可辩护的清洗决策。