首页/Python 与数据实践/Pandas 数据结构
Python 与数据实践 · 第 27 章

Pandas 数据结构

第 26 章的 NumPy 把「同质数值 + 广播」玩得很转:一个数组里全是数字,运算又快又省心。可随手打开一张真实业务表——比如一份员工花名册——里面是姓名、部门、职级这样的文字,混着工龄、薪资这样的数字,还零星缺着几…

约 17 分钟6,611 字31 个代码示例

第 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

这一章的三条主线:

  1. 两种结构:Series 是带标签的一维数组,DataFrame 是带行列标签的二维表,取 DataFrame 的一列就是 Series;
  2. 数据装载:read_csv / read_excel / read_sql 一行把外部数据变 DataFrame,参数应对各种怪文件;
  3. 内部构成:index、columns、values、dtypes、shape 五个属性就是 DataFrame 的「体检单」,其中 dtypes 最值得反复看。

我们还埋了一个悬念:staff 花名册里全是 object 列、藏着一个重复员工、月薪写法五花八门。下一章就把它彻底洗干净,让每个字段都变得「能算」。


下一章:28 · Pandas 数据清洗