Pandas 数据结构
第 26 章的 NumPy 把「同质数值 + 广播」玩得很转:一个数组里全是数字,运算又快又省心。可随手打开一张真实业务表——比如一份员工花名册——里面是姓名、部门、职级这样的文字,混着工龄、薪资这样的数字,还零星缺着几…
第 26 章的 NumPy 把「同质数值 + 广播」玩得很转:一个数组里全是数字,运算又快又省心。可随手打开一张真实业务表——比如一份员工花名册——里面是姓名、部门、职级这样的文字,混着工龄、薪资这样的数字,还零星缺着几个格子。硬塞进 ndarray,整表都会被「升格」成最宽容的 object 类型,NumPy 引以为傲的速度优势瞬间清零。
Pandas 就是来补这块短板的:它在 NumPy 之上加了一层「标签 + 异构」的能力,让 Python 拥有接近 Excel、SQL 的表格操作体验。本章解决三件事——认识 Series 与 DataFrame 两种核心结构,学会把文件和数据库里的数据装进来,再把一张表拆开、看清它的内部构造。
# 数据实践部分的依赖一次装齐(本章只用到前两个,其余是后续章节的)
# pip install pandas openpyxl
import numpy as np
import pandas as pd
1. Pandas 在工具链里的位置
先划清边界,免得学混:
- NumPy 管「数」:单一类型的数值数组、向量化运算、广播——上一章的主场;
- Pandas 管「表」:不同类型的列拼在一起、行列都有名字、缺失值有统一的表示(NaN)。
两者不是竞争关系:Pandas 的底层大量复用 ndarray,你在 Series 上调用的很多方法,最终仍会走上 NumPy 的向量化快车道。可以说 Pandas = ndarray 的计算能力 + 表格的标签体系。
顺带认识几位同行:Polars(Rust 内核、天生多线程)、DuckDB(直接用 SQL 查文件)、Modin 和 Dask(让 Pandas 代码扩展到多核或集群)。它们各有所长,但 API 设计全都或多或少借鉴 Pandas——先把 Pandas 用熟,将来换工具几乎零成本。
2. Series:带标签的一维数据
2.1 最小可运行例子
想象书店里四本书的定价(元):
prices = pd.Series([89.0, 45.5, 128.0, 39.9])
prices
0 89.0
1 45.5
2 128.0
3 39.9
dtype: float64
左边那列 0~3 是自动分配的整数索引(index)——每个值的「门牌号」。右边是数据和整体的 dtype。
2.2 门牌号可以自己起
整数编号在数学计算里方便,业务里却常常不如有意义的标签好用——比如直接用书名:
prices = pd.Series(
[89.0, 45.5, 128.0, 39.9],
index=['Python从入门到实践', '算法图解', '深度学习入门', 'SQL必知必会'],
)
prices
Python从入门到实践 89.0
算法图解 45.5
深度学习入门 128.0
SQL必知必会 39.9
dtype: float64
2.3 从字典创建
字典本身就是「标签 → 值」的映射,天然适合造 Series——键自动成为索引:
stock = pd.Series({'Python从入门到实践': 42, '算法图解': 130, 'SQL必知必会': 67})
stock
Python从入门到实践 42
算法图解 130
SQL必知必会 67
dtype: int64
2.4 拆开看:values 与 index
一个 Series 由两块零件组成,分别摸一下:
print(prices.values) # [ 89. 45.5 128. 39.9] → <class 'numpy.ndarray'>
print(prices.index) # Index(['Python从入门到实践', ...], dtype='object')
print(prices.dtype) # float64
print(prices.shape) # (4,)
values 就是上一章的老朋友 ndarray——所以 Series 的本质是一维 ndarray 配上一个 Index 标签层。理解这一点,后面很多行为都好解释了。
3. DataFrame:把多个 Series 拼成一张表
3.1 从字典创建(最常用)
键当列名,列表当列数据——这是最贴近直觉的造表方式:
books = pd.DataFrame({
'书名': ['算法图解', 'Python从入门到实践', 'SQL进阶', '数据可视化实战'],
'类别': ['计算机', '计算机', '数据库', '计算机'],
'单价': [45.5, 89.0, 69.0, 79.8],
'月销量': [320, 150, 88, 46],
})
books
书名 类别 单价 月销量
0 算法图解 计算机 45.5 320
1 Python从入门到实践 计算机 89.0 150
2 SQL进阶 数据库 69.0 88
3 数据可视化实战 计算机 79.8 46
3.2 从嵌套列表创建
换成「行思维」:外层列表一行一个记录,内层列表就是每行的字段,列名用 columns 另行指定:
pd.DataFrame(
[
['算法图解', '计算机', 45.5, 320],
['Python从入门到实践', '计算机', 89.0, 150],
['SQL进阶', '数据库', 69.0, 88],
],
columns=['书名', '类别', '单价', '月销量'],
)
3.3 从 NumPy 数组创建
把上一章的 ndarray 直接喂进来(纯数值表特别合适):
matrix = np.array([
[45.5, 320],
[89.0, 150],
[69.0, 88],
])
pd.DataFrame(matrix, columns=['单价', '月销量'])
单价 月销量
0 45.5 320.0
1 89.0 150.0
2 69.0 88.0
注意一个小细节:由于 ndarray 只能装同一种 dtype,整数列 月销量 也被「就高」成了 float64——这正是异构数据需要 DataFrame 的原因。
3.4 表与列的关系
type(books) # <class 'pandas.core.frame.DataFrame'>
type(books['单价']) # <class 'pandas.core.series.Series'>
取一列得到的正是 Series。DataFrame 可以理解为「若干个共享同一套行索引的 Series 纵向拼起来」——这个视角在后面学习按列操作、分组聚合时非常有用。
4. 把文件里的数据装进来
手工造表只够教学,真实数据得从文件读。本章起会用一份贯穿两章的示例数据:data/staff.csv——一家虚构公司的员工花名册,字段为员工号、姓名、部门、职级、工作年限、月薪、绩效,共 37 行。它是「故意弄脏」的:各部门上报格式不一,混着各种写法,正好留给下一章练手。
路径
data/staff.csv相对课程根目录,请从Python与数据实践/目录启动 Python(或把工作目录切到这里)再运行本章代码。
4.1 CSV:pd.read_csv
staff = pd.read_csv('data/staff.csv')
staff.head() # 默认看前 5 行,快速确认读对了
员工号 姓名 部门 职级 工作年限 月薪 绩效
0 E001 陈明远 技术部 P7 8年 42k A
1 E002 林晓彤 技术部 P6 5年 26k A+
2 E003 赵启航 技术研发 P6 4年 25k B+
3 E004 孙一诺 市场部 M1 3年 18k B
4 E005 周慕云 人事部 M1级 6年 16k B
大文件、怪文件场景下,这几个参数出场率最高:
| 参数 | 作用 | 示例 |
|---|---|---|
encoding |
指定编码(老系统导出的 CSV 常是 GBK) | encoding='gbk' |
header |
哪一行当表头;None 表示没有表头 |
header=None |
index_col |
把某一列设为行索引 | index_col='员工号' |
usecols |
只读指定列(省内存) | usecols=['姓名', '月薪'] |
nrows |
只读前 N 行(先探路再全量读) | nrows=100 |
4.2 Excel:pd.read_excel
读写 .xlsx 需要额外装 openpyxl(课程环境已包含)。Excel 是「工作簿 → 多个工作表」的两层结构,所以多了一个 sheet_name 参数:
# 先把花名册另存为 Excel
staff.to_excel('data/staff.xlsx', index=False) # index=False:别把行号写进文件
# 再读回来;sheet_name 默认取第一张表,也可以按名字或序号指定
back = pd.read_excel('data/staff.xlsx', sheet_name='Sheet1')
back.shape # (37, 7),与原表一致
4.3 其他格式
Pandas 的读取家族几乎覆盖所有常见格式,套路一致:
# logs = pd.read_json('data/logs.json') # JSON
# big = pd.read_parquet('data/orders.parquet') # 列式存储,大数据量首选(需 pyarrow)
5. 把数据库里的数据装进来:pd.read_sql
业务系统的数据通常躺在数据库里。以 Python 内置的 SQLite 演示(无需安装任何服务),建一张书店订单表再查出来:
import sqlite3
conn = sqlite3.connect(':memory:') # 内存数据库,脚本结束即消失
conn.execute('''
CREATE TABLE orders (
book TEXT, qty INT, amount FLOAT
)
''')
conn.executemany(
'INSERT INTO orders VALUES (?, ?, ?)',
[('算法图解', 3, 136.5), ('Python从入门到实践', 2, 178.0), ('SQL进阶', 5, 345.0)],
)
conn.commit()
orders = pd.read_sql('SELECT * FROM orders', conn)
conn.close()
orders
book qty amount
0 算法图解 3 136.5
1 Python从入门到实践 2 178.0
2 SQL进阶 5 345.0
SQL 查询的结果直接变成 DataFrame,数值列也会保留正确的类型(qty 是 int64、amount 是 float64)。
💡 真实场景:公司数据库多是 MySQL / PostgreSQL。配好 SQLAlchemy 引擎后,
pd.read_sql('SELECT ...', engine)同样一行拉数进 DataFrame 分析,省去「先导出 CSV 再读」的中间环节。
6. 行索引:不只是序号
默认情况下行索引是 RangeIndex——自动编号 0, 1, 2, …:
staff.index # RangeIndex(start=0, stop=37, step=1)
但表格的主键往往自带业务含义(比如员工号)。用 set_index 把某列「提拔」为行索引,之后就能用标签直接定位:
staff_idx = staff.set_index('员工号')
# 用工号查一个人的完整档案(loc 的完整用法见下一章)
staff_idx.loc['E007']
姓名 郑安然
部门 财务部
职级 M1
工作年限 7年
月薪 20w/年
绩效 B+
Name: E007, dtype: object
(顺带一提:这份表里 E029 出现了两次,loc['E029'] 会一次查出两行——重复数据的问题,下一章开头就处理它。)
reset_index 是它的逆操作——索引退回成普通列,行索引恢复默认编号:
staff_idx.reset_index().head(2)
员工号 姓名 部门 职级 工作年限 月薪 绩效
0 E001 陈明远 技术部 P7 8年 42k A
1 E002 林晓彤 技术部 P6 5年 26k A+
7. 解剖一个 DataFrame
拿 staff 做标本,把 DataFrame 的五脏六腑挨个看一遍。先上一张结构图:
columns(列标签)
┌─────────────────────────────┐
index │ 员工号 姓名 部门 … │
(行标签) ├─────────────────────────────┤
0 → │ E001 陈明远 技术部 … │
1 → │ E002 林晓彤 技术部 … │
⋮ │ ⋮ ⋮ ⋮ │
└─────────────────────────────┘
values:剥掉两层标签后剩下的二维 ndarray
逐个属性核对:
print(staff.columns) # Index(['员工号', '姓名', '部门', '职级', '工作年限', '月薪', '绩效'], dtype='object')
print(staff.index) # RangeIndex(start=0, stop=37, step=1)
print(staff.shape) # (37, 7) → (行数, 列数)
print(staff.size) # 259 → 元素总数 = 37 × 7
print(staff.ndim) # 2 → 二维
values(新版本更推荐写 to_numpy())剥掉标签,露出二维 ndarray 本体:
staff.to_numpy()[:2]
array([['E001', '陈明远', '技术部', 'P7', '8年', '42k', 'A'],
['E002', '林晓彤', '技术部', 'P6', '5年', '26k', 'A+']], dtype=object)
最后看 dtypes——每列各自的类型,这是读表后要做的第一项检查:
staff.dtypes
员工号 object
姓名 object
部门 object
职级 object
工作年限 object
月薪 object
绩效 object
dtype: object
问题来了:工作年限、月薪明明是数字,怎么全是 object(字符串)?因为表里混着 20w/年、10+年 这类写法,Pandas 只能整列按字符串收下。数值被当成文本,意味着没法求和、没法求均值、没法比较大小——这就是数据清洗要解决的头号问题,下一章用它开刀。
小贴士:pandas 3.0 起字符串列默认显示为
str,旧版本显示object——两者在这里是同一件事,别被名字唬住。
8. 小结
| Series | DataFrame | |
|---|---|---|
| 维度 | 一维 | 二维 |
| 直观类比 | Excel 里的一列 | Excel 里的一张表 |
| 标签 | index |
index + columns |
| 本质 | 一维 ndarray + 标签 | 共享行索引的一组 Series |
这一章的三条主线:
- 两种结构:Series 是带标签的一维数组,DataFrame 是带行列标签的二维表,取 DataFrame 的一列就是 Series;
- 数据装载:
read_csv/read_excel/read_sql一行把外部数据变 DataFrame,参数应对各种怪文件; - 内部构成:
index、columns、values、dtypes、shape五个属性就是 DataFrame 的「体检单」,其中dtypes最值得反复看。
我们还埋了一个悬念:staff 花名册里全是 object 列、藏着一个重复员工、月薪写法五花八门。下一章就把它彻底洗干净,让每个字段都变得「能算」。
下一章:28 · Pandas 数据清洗