表结构与数据读写
本章目录
本章学习目标
- 解释 Series、DataFrame 和索引
- 带类型和日期要求读写 CSV
- 检查输入表的粒度与结构
从数组到有标签的表
DataFrame 是二维的有标签表格,每列可以有不同类型,行索引和列名共同定位数据。Series 是一维带索引的数据,df['amount'] 返回金额列这个 Series,df[['amount']] 则仍是只有一列的 DataFrame。两者外观看起来近似,但维数、传给后续函数的行为和返回结构不同。shape 报告行列数,columns 报告列标签,dtypes 报告各列类型。
不要把索引默认看成主键。读取 CSV 后默认得到从零开始的行编号,这只是当前展示和位置,排序或过滤后不会自动表达用户身份。本课程用户表每行一个 user_id,订单表每行一个 order_id,订单明细每行一个 item_id。将这些键列保留下来,便于连接、唯一性检查和输出交换。先描述每行代表的对象,才能正确解释表的大小。
读入时建立数据合同
CSV 保存字符,没有完整的数据库类型约束。读取器通过内容推断类型,空 user_id 可能让本来整数的列变成浮点数;编号含前导零时也可能丢失格式。对需要为空的整数使用 Int64,对编号字符串显式使用 string,对时间显式调用 to_datetime 或指定 parse_dates。金额在数据库为精确小数,CSV 中可读为字符串再转 Decimal 或整数分,避免把浮点近似当作财务精度。
每次读取后检查必需列、主键、日期范围、空值和行数。不能只看 head 五行便宣布整个文件正确。缺少列通常应立即停止;额外列可以先保留并记录。不同来源时间可能带时区,业务日期必须确定统一时区后再提取日历日。教学数据使用统一的无时区业务日期,不代表现实数据也如此简单。
小数据逐步读写
考虑两行文本:订单一、用户一、金额十元;订单二、用户为空、金额二十元。读入以后先检查 shape 为两行,接着看 user_id 的类型和空值位置。设置可空整数后,缺失表示为 pandas 的缺失标记,而不是虚构用户零。order_date 被解析成日期类型,才能可靠排序并按月份汇总。若解析遇到不合法日期,默认报错;使用 errors='coerce' 会转成 NaT,因此必须另外统计新增缺失,不能认为错误消失了。
示例把订单写到 outputs 下,使用 index=False 避免把内部行索引多写为一列。再次读取时恢复日期和可空用户键,并断言行数、订单编号和顺序一致。一次成功往返只验证本次的数据合同,若之后加入新的列或时区,需要更新读写参数与断言。
格式、路径与可复现输出
本课程脚本通过自身路径定位课程根目录,读取统一数据目录,输出到 outputs,因此从任意工作目录运行不会误写到桌面或原始 CSV。to_csv 的默认值会写索引,教学输出显式关闭;中文文本使用 UTF-8,给某些桌面软件展示时可额外导出 UTF-8 BOM 版本。不要用字符串手工拆分 CSV,因为带逗号的引号字段会被错误截断。
大量数据需要只读必要列、分块读入或使用保留类型的列式格式。CSV 便于查看和 SQL 导入,适合本课程的小规模交换;Parquet 保存类型且适合列读取,但通常需要额外引擎。代码应明确依赖,而不是假定每台机器都有相同引擎。读取失败时保留原文件和错误位置,不自动覆盖输入。
解释结果与常见错误
运行结果中的实际 shape 取决于配套数据,重点是列名、日期类型和可空用户键满足合同。head 是抽样展示,info 和缺失统计才能揭示更完整结构。对空表也应能够说明列与类型要求,读入后无记录不等同于文件不存在。SQL 表通过主键、约束和类型限制写入,DataFrame 需要你主动断言这些条件;它不会阻止重复订单编号进入表。
典型错误是覆盖源文件后才发现数据解析错误,或把内部索引输出再读回变成 Unnamed 列。修正方式是输出独立文件,并做往返检查。练习中先预测缺失键会是什么类型,再运行 dtypes;再试着省略 index=False,观察额外列从哪里来,最后恢复规范输出。
版本与复现约定
本课程 pandas 示例以 2.3.3 为验证基线,页面运行记录给出实际版本。pandas 3.x 默认使用 Copy-on-Write:从表中取出的对象发生修改时,不据此假定原表同步变化。跨版本都应使用一次完整的 loc 赋值表达式;需要独立工作表时显式 copy。阅读网上的旧示例时,先核对版本、输入类型和索引,再判断结果能否复现。
完整示例与实际输出
from pathlib import Path
import pandas as pd
import numpy as np
from _shared import load
ROOT = Path(__file__).resolve().parents[1]
OUT = ROOT / "outputs"
OUT.mkdir(exist_ok=True)
users = load("users")
orders = load("orders")
assert {"user_id","signup_date","country","channel"} <= set(users.columns)
assert users["user_id"].is_unique
copy_path = OUT / "orders_roundtrip.csv"
orders.to_csv(copy_path,index=False,encoding="utf-8")
restored = pd.read_csv(copy_path,dtype={"user_id":"Int64"},parse_dates=["order_date"])
assert len(restored) == len(orders)
assert restored["order_id"].tolist() == orders["order_id"].tolist()
print("pandas",pd.__version__)
print("orders shape:",orders.shape)
print(restored.head(4).to_string(index=False))
print(restored.dtypes.to_string())
实际运行通过CPU Python execution.
pandas 2.3.3
orders shape: (60, 6)
order_id user_id order_date status amount refund_amount
1 1 2026-01-02 01:00:00 paid 48.0 0.0
2 2 2026-01-03 02:00:00 paid 240.0 0.0
3 3 2026-01-05 03:00:00 paid 12.5 0.0
4 4 2026-01-06 04:00:00 paid 48.0 0.0
order_id int64
user_id Int64
order_date datetime64[ns]
status object
amount float64
refund_amount float64五道自测:先回答,再核对
01 · df['x'] 与 df[['x']] 有什么不同?
查看提示
看维数。
查看参考答案
前者是 Series,后者是 DataFrame。
02 · 为什么 CSV 读入的整数键可能变浮点?
查看提示
检查缺失。
查看参考答案
传统整数 dtype 无法表示缺失,推断可能采用浮点;显式使用可空 Int64。
03 · 保存 CSV 如何避免多出索引列?
查看提示
查看 to_csv 参数。
查看参考答案
使用 index=False;索引若有业务意义,应先明确转成命名列。
04 · 解析日期使用 coerce 后还需做什么?
查看提示
错误值去哪了?
查看参考答案
统计新增 NaT 并定位原值,决定修正还是排除,而非静默接受。
05 · head 能保证全表键唯一吗?
查看提示
抽样与全集不同。
查看参考答案
不能;需用 is_unique 或 duplicated 对全部键检查。
两项动手任务
任务 1 · 完成订单 CSV 往返并检查主键顺序。
提示
输出独立文件,恢复日期类型。
完整参考解答与检查方式
from pathlib import Path
import pandas as pd
import numpy as np
from _shared import load
ROOT = Path(__file__).resolve().parents[1]
OUT = ROOT / "outputs"
OUT.mkdir(exist_ok=True)
users = load("users")
orders = load("orders")
assert {"user_id","signup_date","country","channel"} <= set(users.columns)
assert users["user_id"].is_unique
copy_path = OUT / "orders_roundtrip.csv"
orders.to_csv(copy_path,index=False,encoding="utf-8")
restored = pd.read_csv(copy_path,dtype={"user_id":"Int64"},parse_dates=["order_date"])
assert len(restored) == len(orders)
assert restored["order_id"].tolist() == orders["order_id"].tolist()
print("pandas",pd.__version__)
print("orders shape:",orders.shape)
print(restored.head(4).to_string(index=False))
print(restored.dtypes.to_string())
任务 2 · 读取用户表,显示每列缺失数并断言 user_id 非空唯一。
提示
isna 和 is_unique。
完整参考解答与检查方式
from pathlib import Path
import pandas as pd
import numpy as np
from _shared import load
ROOT = Path(__file__).resolve().parents[1]
OUT = ROOT / "outputs"
OUT.mkdir(exist_ok=True)
users = load("users")
assert users["user_id"].notna().all() and users["user_id"].is_unique
print(users.isna().sum().to_string())
print(users.head().to_string(index=False))
官方资料与掌握状态
完成两项任务后,按独立完成程度记录。状态会尽量保存在当前浏览器,可导出备份。
动手任务记录
掌握状态与复习
关掉资料独立完成后记 A;提示后完成记 B;需要重学记 C。
记录尽量保存在当前浏览器,建议定期导出备份。