4.1 Series、DataFrame、索引与模式
NumPy 数组为我们提供了形状、编号轴,以及每个数组一个 dtype。真实分析表还有一项要求:数值需要标签来保留每行每列的含义。pandas 在 NumPy 数组模型之上附加标签,并允许每一列使用适合自身字段的 dtype。
按照惯例导入 pandas:
import pandas as pdSeries:一个带标签的维度
pandas 的 Series 是一维数值序列,每个值都与一条索引(Index)标签配对:
minutes = pd.Series(
[31, 52, 44],
index=["A104", "A105", "A106"],
name="delivery_minutes",
dtype="Int64",
)数值表示配送用时,索引标签是订单编号,Series 名称描述被测变量。三种角色不能混淆:A104 用于识别观测,而 31 是可以进行算术运算的数量。
应直接检查结构,不要只依赖 Notebook 的显示结果:
print(minutes.index)
print(minutes.name)
print(minutes.dtype)
print(minutes.shape) # (3,)首字母大写的 Int64 是 pandas 的可空整数类型(nullable integer dtype)。它可以同时表示整数与 pd.NA,无需把标识符或计数转换为浮点数。由于不同构造函数和 pandas 版本的 dtype 推断可能不同,重要边界处应显式声明类型。
DataFrame:带行列标签的异构表
DataFrame 是二维带标签表。与二维 NumPy 数组不同,它的各列可以采用不同 dtype:
order_id = pd.Index(
["A104", "A105", "A106", "A107"],
name="order_id",
)
deliveries = pd.DataFrame(
{
"zone": pd.Series(
["North", "South", "West", "South"],
index=order_id,
dtype="string",
),
"minutes": pd.Series(
[31, 52, None, 44],
index=order_id,
dtype="Int64",
),
"delivered": pd.Series(
[True, True, False, True],
index=order_id,
dtype="boolean",
),
},
index=order_id,
)这张表有两条带标签的轴:
- 第 0 轴是行索引,包含订单编号。
- 第 1 轴是列索引,包含变量名称。
每一列同时也是与 DataFrame 行索引对齐的 Series。这就是上面每个 Series 都传入 index=order_id 的原因。如果某个 Series 使用不同索引,pandas 会按标签对齐,而不会直接按位置复制数值。
第一次检查表格时,可以使用:
print(deliveries.shape)
print(deliveries.columns)
print(deliveries.index.name)
print(deliveries.dtypes)
print(deliveries.head())
print(deliveries.info())head 只预览记录,不能验证完整表格;info 汇总非缺失数量、dtype 和内存用量,但业务检查仍应写成明确断言。
模式实验提供一张可编辑表格和一份可编辑的 schema manifest。你可以修改任意单元格、添加记录、移动索引声明或改变可空性与 dtype。验证会遍历整张表,因此可以亲手制造外观正常的预览无法发现的重复键和类型污染。
索引是对齐键,不是行号
默认 RangeIndex 使用 0, 1, 2, ... 作为标签。当行身份不重要,或身份已经保存在普通列中时,它很方便。如果 order_id 这样的业务键稳定、非缺失且唯一,就可以把它设为索引:
deliveries = raw.set_index("order_id", verify_integrity=True)verify_integrity=True 会在操作过程中拒绝重复键。对于已有索引,也可以自行验证:
assert deliveries.index.notna().all()
assert deliveries.index.is_unique
assert deliveries.index.name == "order_id"并非每个 pandas 对象的索引都必须唯一。重复标签对某些分组结构或时间序列很有用。但如果契约规定“一笔订单一行”,重复标签就是数据错误:选择一个订单标签时,可能意外返回多行。
不要为了显示美观而随意把字段放进索引。索引标签会参与对齐、选择、连接和序列化。如果一个字段不是稳定身份或坐标,通常更适合留在普通列中。
算术先对齐标签,再计算数值
考虑标签集合与顺序都不同的需求和运力 Series:
demand = pd.Series(
{"North": 12, "South": 8, "West": 5},
dtype="Int64",
)
capacity = pd.Series(
{"South": 10, "North": 15, "Central": 4},
dtype="Int64",
)
surplus = capacity - demandpandas 会先取得标签并集,对齐两个操作数,然后才做减法。North 和 South 两边都有数值;Central 没有需求,West 没有运力,因此它们的余量是缺失值。
这种行为避免了位置错误:运力的第一个值属于 South,而需求的第一个值属于 North。若像 NumPy 那样直接按当前位置相减,就会组合两个无关区域。
可以直接检查对齐过程:
aligned_capacity, aligned_demand = capacity.align(
demand,
join="outer",
)当业务契约明确规定“没有记录等于零”时,命名算术方法可以接受 fill_value:
surplus_assuming_zero = capacity.subtract(demand, fill_value=0)这是业务假设,不是无害的数据清理。缺失运力可能表示“尚未上报”,而不是运力为零。在含义确定之前,应让缺失结果保持可见。
对齐实验允许你把两条带标签的 Series 都写成 label:value 记录,再执行对齐表达式。重排、改名、增删标签后,可以直接检查计算得到的标签并集;随后还能显式声明缺失政策,观察用零填充何时会把结构不匹配变成业务断言。
把模式当成可执行契约
数据模式(schema)描述数据集应有的结构和含义,包括必需列、dtype、键、可空性、范围、单位,有时还包括字段间关系。pandas 没有唯一通用的模式声明方式,但普通断言足以保护 Notebook 或管道边界:
required = {"zone", "minutes", "delivered"}
assert required <= set(deliveries.columns)
assert deliveries.index.is_unique
assert deliveries["zone"].dtype == pd.StringDtype()
assert str(deliveries["minutes"].dtype) == "Int64"
assert deliveries["minutes"].dropna().between(0, 24 * 60).all()只有当列顺序属于外部文件或 API 契约时,才应检查完全一致的列顺序。对于普通分析,集合包含关系通常更稳健。同样,dtype 检查只能证明存储类型,不能证明业务值有效:负数配送用时仍然可以放入整数 dtype。
下一节会把这些带标签结构转化为精确选择契约。你将按标签、按位置和按布尔条件选择行列,同时避免混淆不同坐标系统。