摘要
pandas 是 Python 数据分析中最常用的表格处理工具。理解 Series 和 DataFrame 的结构、索引、列、数据类型和对齐规则,是后续数据读取、清洗、筛选、分组和合并的基础。
本文从一维 Series 和二维 DataFrame 开始,介绍创建、查看、索引、切片、修改、类型转换和数据对齐,并通过订单数据示例建立一套可复用的基本操作习惯。
一、背景与问题
业务数据通常不是单一的数字数组,而是包含多个字段的表格:
| order_id | customer | amount | status |
|---|---|---|---|
| A001 | Alice | 1200 | paid |
| A002 | Bob | 800 | pending |
使用嵌套列表处理这类数据时,需要手动维护列名、数据类型和行位置,代码容易出现以下问题:
- 通过位置访问字段,可读性差。
- 插入或删除列后,索引关系容易错乱。
- 缺失值和不同类型混在一起,判断复杂。
- 多张表关联需要手动编写循环。
pandas 用带标签的数据结构表达表格,使列名、索引和数据类型成为代码的一部分。
二、核心概念
1. Series
Series 是带索引的一维数据。可以把它理解为"带标签的数组":
text
index value
east 1200
south 800
north 1560
Series 的核心属性包括:
| 属性 | 说明 |
|---|---|
index |
每个元素对应的标签 |
values |
底层值 |
dtype |
数据类型 |
name |
Series 名称 |
shape |
数据形状 |
2. DataFrame
DataFrame 是由多个 Series 按索引对齐组成的二维表格。每一列是一个 Series,所有列共享行索引。
text
DataFrame
├─ index
├─ Series: order_id
├─ Series: customer
├─ Series: amount
└─ Series: status
不同列可以使用不同数据类型,但同一列通常应保持类型稳定。
3. Index
Index 是 pandas 数据结构中的标签系统。它既可以是默认整数索引,也可以是字符串、日期或多级索引。
索引主要用于:
- 定位数据。
- 对齐不同 Series。
- 连接和合并表格。
- 表示时间序列位置。
索引不是普通的数据列。需要将索引转换为列时,可以使用 reset_index()。
4. 标签对齐
pandas 运算默认按标签对齐,而不是单纯按位置:
python
import pandas as pd
left = pd.Series([10, 20], index=["a", "b"])
right = pd.Series([1, 2], index=["b", "c"])
print(left + right)
结果中 a 和 c 因为缺少对应标签会得到缺失值。标签对齐很方便,但也要求在运算前确认索引含义。
5. 数据类型
常见类型包括:
- 整数和浮点数。
- 字符串。
- 布尔值。
- 日期时间。
- 分类类型。
- 可空整数和可空布尔类型。
类型会影响筛选、排序、聚合、内存和输出格式,不能只看表格显示结果判断类型。
三、工作原理
1. DataFrame 的列式结构
DataFrame 以列为核心组织数据。不同列可能拥有不同类型,因此处理单列通常比把所有数据转换成同一种 NumPy 类型更自然。
text
读取表格
→ 建立列名和索引
→ 推断或指定 dtype
→ 通过标签选择数据
→ 返回 Series 或 DataFrame
使用链式操作时,要注意每一步返回的是原对象、视图还是新对象,避免隐式修改带来的错误。
2. loc 与 iloc
loc按标签选择。iloc按整数位置选择。
python
df.loc[0, "amount"]
df.iloc[0, 2]
当索引不是连续整数时,loc[0] 和 iloc[0] 的含义可能完全不同。
3. 视图与副本
对 DataFrame 进行切片后再修改,可能产生副本修改警告或无法修改原数据。更明确的方式是使用 .loc 直接赋值,或者在需要独立数据时显式 .copy()。
4. 缺失值
pandas 通常使用 NaN、NaT 或可空类型表示缺失。缺失值具有特殊传播规则:
python
import pandas as pd
series = pd.Series([10, None, 30])
print(series.isna())
print(series.fillna(0))
缺失值处理不能只做全局填充,应根据字段含义和业务规则决定。
四、实战示例
1. 创建 Series
python
import pandas as pd
amounts = pd.Series(
[1200.0, 800.0, 1560.0],
index=["A001", "A002", "A003"],
name="amount",
)
print(amounts)
print(amounts["A002"])
print(amounts.dtype)
2. 创建 DataFrame
python
orders = pd.DataFrame(
{
"order_id": ["A001", "A002", "A003", "A004"],
"customer": ["Alice", "Bob", "Carol", "David"],
"amount": [1200.0, 800.0, 1560.0, 620.0],
"status": ["paid", "pending", "paid", "cancelled"],
}
)
print(orders)
print(orders.shape)
print(orders.columns.tolist())
print(orders.dtypes)
3. 查看数据
python
print(orders.head(2))
print(orders.tail(2))
print(orders.info())
print(orders.describe(numeric_only=True))
print(orders["status"].value_counts(dropna=False))
info() 适合检查列名、非空数量和类型,value_counts() 适合查看分类字段的分布。
4. 选择列
python
amount = orders["amount"]
selected = orders[["order_id", "amount", "status"]]
print(type(amount))
print(type(selected))
选择单列通常返回 Series,选择多列返回 DataFrame。
5. 使用 loc 和 iloc
python
first_order = orders.loc[0]
first_amount = orders.loc[0, "amount"]
first_two = orders.iloc[0:2, 0:3]
print(first_order)
print(first_amount)
print(first_two)
使用标签选择时,代码表达的是业务字段;使用位置选择时,代码依赖列和行的位置。
6. 条件筛选
python
paid_orders = orders.loc[orders["status"] == "paid"]
large_orders = orders.loc[orders["amount"] >= 1000]
selected_orders = orders.loc[
(orders["status"] == "paid")
& (orders["amount"] >= 1000),
["order_id", "amount"],
]
print(selected_orders)
多个条件需要使用 &、| 和 ~,并为每个条件加括号。不能直接使用 Python 的 and 和 or 处理 Series。
7. 新增与修改列
python
orders = orders.copy()
orders["is_high_value"] = orders["amount"] >= 1000
orders["amount_with_tax"] = orders["amount"] * 1.06
orders.loc[orders["status"] == "pending", "status"] = "待支付"
print(orders)
通过 .loc 修改满足条件的单元格,可以避免链式赋值带来的不确定性。
8. 类型转换
python
orders["amount"] = pd.to_numeric(
orders["amount"],
errors="coerce",
)
orders["order_date"] = pd.to_datetime(
["2026-09-01", "2026-09-02", "2026-09-03", "2026-09-04"],
errors="coerce",
)
orders["status"] = orders["status"].astype("string")
errors="coerce" 会把无法转换的值变成缺失值,转换后必须继续检查缺失数量,不能把异常静默吞掉。
9. 重置索引
python
filtered = orders.loc[orders["amount"] >= 1000].copy()
filtered = filtered.reset_index(drop=True)
print(filtered)
筛选后保留原索引有时很有用,例如追踪原始行;需要生成连续展示序号时再重置索引。
10. 导入和导出
python
orders.to_csv(
"output/orders.csv",
index=False,
encoding="utf-8-sig",
)
loaded = pd.read_csv("output/orders.csv")
导出时是否保留索引、使用什么编码、是否包含缺失值,都应成为明确的接口约定。
五、常见问题与实践建议
1. loc 和 iloc 该怎么选?
当代码表达业务字段时优先使用 loc;当处理固定位置、矩阵切片或底层数组时可以使用 iloc。避免用位置索引表达容易变化的业务列。
2. 为什么筛选后修改会出现警告?
常见原因是对一个可能由切片产生的对象继续赋值。可以使用:
python
result = orders.loc[orders["amount"] > 1000].copy()
result.loc[:, "level"] = "high"
3. 为什么字符串列有时是 object?
旧版本或混合内容可能让 pandas 使用 object 表示字符串列。需要稳定处理时,可以显式转换为 string,并检查其中是否混入数字、列表或其他对象。
4. DataFrame 复制会不会很慢?
大表复制确实会增加内存和时间。先明确是否需要独立副本,避免无意义地反复 copy();但在需要安全修改筛选结果时,显式复制往往比隐式共享更可靠。
5. 如何处理重复索引?
先判断重复索引是否具有业务意义:
python
print(orders.index.is_unique)
print(orders.index[orders.index.duplicated()])
如果索引应该唯一,应在读取或转换后立即校验;如果允许重复,则避免把索引唯一性当作前提。
六、进阶思考
1. 数据类型与内存
大数据表可以从以下方面优化:
- 整数列使用合适的位宽。
- 重复字符串转换为
category。 - 日期列转换为
datetime64。 - 不要让数字列长期保持字符串。
优化前后应比较内存和计算结果,不能只追求占用更小。
2. 链式方法与可读性
pandas 支持链式处理:
python
summary = (
orders.loc[orders["status"] == "paid"]
.assign(amount_with_tax=lambda frame: frame["amount"] * 1.06)
.loc[:, ["order_id", "amount_with_tax"]]
)
链式代码可以减少中间变量,但过长时应拆成具名函数,优先保证可读性和可调试性。
3. DataFrame 与业务模型
DataFrame 适合数据处理,不一定适合直接作为整个业务系统的数据模型。进入数据库、API 或领域服务前,应明确字段类型、空值语义和主键规则。
4. 从单表操作进入分析流程
掌握 Series 与 DataFrame 后,下一步可以学习:
text
读取数据
→ 清洗类型
→ 条件筛选
→ 分组聚合
→ 多表合并
→ 透视与可视化
每一步都应保留数据行数、列名和关键指标的检查。
结论
Series 是带标签的一维数据,DataFrame 是由多个 Series 按索引组织起来的二维表格。loc、iloc、索引对齐、缺失值、类型转换和 .copy() 是日常 pandas 开发最重要的基础。
后续数据读取、清洗和统计分析都建立在这些结构之上。先把数据结构和索引关系理解清楚,再扩展到更复杂的业务处理。
参考资料
- pandas 官方文档:https://pandas.pydata.org/docs/
- pandas 用户指南:https://pandas.pydata.org/docs/user_guide/
- NumPy 官方文档:https://numpy.org/doc/