跳转到内容

3.3.2 Pandas 核心数据结构

Pandas DataFrame 结构图

  • 理解 Pandas 在数据分析中的地位
  • 掌握 Series 的创建和基本操作
  • 掌握 DataFrame 的创建和基本属性
  • 理解索引(Index)机制

第一次学 Pandas,最稳的顺序不是“直接背所有方法”,而是先看清:

flowchart LR
A["Series:一列有标签的数据"] --> B["DataFrame:很多列组合成的表"]
B --> C["Index:行列的定位系统"]
C --> D["后面所有筛选、清洗、聚合都靠它们展开"]

所以这节真正想解决的是:

  • 为什么 Pandas 不只是“Python 版 Excel”
  • 为什么 Series / DataFrame / Index 会成为后面整章的底座

如果说 NumPy 是 Python 数据科学的引擎,那 Pandas 就是方向盘和仪表盘——它让你能方便地操控和观察数据。

flowchart LR
A["原始数据<br/>CSV / Excel / JSON / SQL"] --> B["Pandas<br/>读取 · 清洗 · 分析"]
B --> C["分析结果<br/>统计 · 可视化 · 报告"]
style B fill:#ff9800,color:#fff,stroke:#e65100

Pandas 的核心能力:

能力说明
数据读写一行代码读取 CSV、Excel、JSON、SQL
数据清洗处理缺失值、重复值、异常值
数据筛选像 SQL 一样灵活地过滤和查询数据
分组统计groupby 比纯 Python 循环快几十倍
数据合并像 SQL JOIN 一样合并多张表

你可以把 Pandas 理解成:

  • 一个会记住“行和列标签”的智能表格

普通 list 更像:

  • 没有列名的原始数据堆

NumPy 更像:

  • 适合高速数值计算的矩阵引擎

Pandas 则更像:

  • 你真的开始在处理“字段、行记录、表结构”的地方

还记得第 1 章的预热练习吗?75 行纯 Python 代码做的事情,Pandas 5 行就搞定了。现在让我们正式学习它。

import pandas as pd
import numpy as np
print(pd.__version__) # 如 2.2.0

Series 是 Pandas 最基本的数据结构——你可以把它理解为一个带标签的 NumPy 数组

第一次看 Series,最该先抓住什么?

Section titled “第一次看 Series,最该先抓住什么?”

最值得先抓住的是这句:

Series = 一列数据 + 一套标签。

只要这句稳住了,后面你看:

  • 按标签取值
  • 按位置取值
  • 对整列做运算

都会顺很多。

import pandas as pd
# 从列表创建(自动生成 0, 1, 2... 索引)
s1 = pd.Series([85, 92, 78, 95, 88])
print(s1)
# 0 85
# 1 92
# 2 78
# 3 95
# 4 88
# dtype: int64
# 指定索引
s2 = pd.Series(
[85, 92, 78, 95, 88],
index=["语文", "数学", "英语", "物理", "化学"]
)
print(s2)
# 语文 85
# 数学 92
# 英语 78
# 物理 95
# 化学 88
# dtype: int64
# 从字典创建(键自动成为索引)
scores = {"语文": 85, "数学": 92, "英语": 78, "物理": 95}
s3 = pd.Series(scores)
print(s3)
索引 (Index) 值 (Values)
─────────── ──────────
语文 85
数学 92
英语 78
物理 95
化学 88

每个 Series 都由两部分组成:

  • 索引(Index):标签,用来定位数据
  • 值(Values):实际数据,底层是 NumPy 数组

一个很适合初学者先记的对照表

Section titled “一个很适合初学者先记的对照表”
你现在看到的东西可以先把它想成什么
Series一列带标签的数据
Index这列数据的“行名”
Values真正存着的数据本体

这个表很适合新人,因为它能把抽象名词重新压回成几个更直白的角色。

s = pd.Series([85, 92, 78], index=["语文", "数学", "英语"])
print(s.index) # Index(['语文', '数学', '英语'], dtype='object')
print(s.values) # [85 92 78] ← 这是一个 NumPy 数组!
print(s.dtype) # int64
print(s.shape) # (3,)
print(len(s)) # 3
s = pd.Series([85, 92, 78, 95], index=["语文", "数学", "英语", "物理"])
# 用标签索引
print(s["数学"]) # 92
# 用位置索引
print(s.iloc[1]) # 92
# 切片
print(s["语文":"英语"]) # 标签切片(包含末尾!)
# 语文 85
# 数学 92
# 英语 78
# 布尔索引
print(s[s >= 90])
# 数学 92
# 物理 95
s = pd.Series([85, 92, 78, 95], index=["语文", "数学", "英语", "物理"])
# 向量化运算(和 NumPy 一样)
print(s + 5) # 每科加 5 分
print(s * 1.1) # 每科乘以 1.1
print(s.mean()) # 87.5 平均分
print(s.max()) # 95 最高分
print(s.describe()) # 一键生成描述性统计

DataFrame 是 Pandas 的核心——你可以把它理解为一张Excel 表格,或者一个由多个 Series 组成的字典

第一次看 DataFrame,最该先记什么?

Section titled “第一次看 DataFrame,最该先记什么?”

最值得先记住的是:

DataFrame = 多列 Series 按同一套行索引拼起来的一张表。

你可以先把它理解成:

  • 一张真正有列名和行号的数据表

而不是一堆数组拼在一起。

# 方法 1:从字典创建(最常用)
data = {
"姓名": ["张三", "李四", "王五", "赵六", "钱七"],
"年龄": [22, 25, 23, 28, 21],
"城市": ["北京", "上海", "广州", "深圳", "杭州"],
"薪资": [15000, 22000, 18000, 25000, 16000]
}
df = pd.DataFrame(data)
print(df)
# 姓名 年龄 城市 薪资
# 0 张三 22 北京 15000
# 1 李四 25 上海 22000
# 2 王五 23 广州 18000
# 3 赵六 28 深圳 25000
# 4 钱七 21 杭州 16000
# 方法 2:从列表的列表创建
data = [
["张三", 22, "北京"],
["李四", 25, "上海"],
["王五", 23, "广州"]
]
df = pd.DataFrame(data, columns=["姓名", "年龄", "城市"])
# 方法 3:从 NumPy 数组创建
rng = np.random.default_rng(seed=42)
arr = rng.integers(60, 100, size=(5, 3))
df = pd.DataFrame(arr, columns=["语文", "数学", "英语"])
# 方法 4:从 Series 字典创建
df = pd.DataFrame({
"数学": pd.Series([90, 85, 78], index=["张三", "李四", "王五"]),
"英语": pd.Series([88, 92, 75], index=["张三", "李四", "王五"])
})
组成部分示例
行索引01234
列名姓名年龄城市薪资
一行数据张三22北京15000

DataFrame = 行索引(Index) + 列名(Columns) + 数据(Values)

data = {
"姓名": ["张三", "李四", "王五", "赵六", "钱七"],
"年龄": [22, 25, 23, 28, 21],
"城市": ["北京", "上海", "广州", "深圳", "杭州"],
"薪资": [15000, 22000, 18000, 25000, 16000]
}
df = pd.DataFrame(data)
print(df.shape) # (5, 4) → 5 行 4 列
print(df.columns) # Index(['姓名', '年龄', '城市', '薪资'], dtype='object')
print(df.index) # RangeIndex(start=0, stop=5, step=1)
print(df.dtypes)
# 姓名 object ← 字符串
# 年龄 int64
# 城市 object
# 薪资 int64
print(df.size) # 20 → 5 × 4 = 20 个元素
print(len(df)) # 5 → 行数
# 前 3 行
print(df.head(3))
# 后 2 行
print(df.tail(2))
# 基本信息
print(df.info())
# <class 'pandas.core.frame.DataFrame'>
# RangeIndex: 5 entries, 0 to 4
# Data columns (total 4 columns):
# # Column Non-Null Count Dtype
# --- ------ -------------- -----
# 0 姓名 5 non-null object
# 1 年龄 5 non-null int64
# 2 城市 5 non-null object
# 3 薪资 5 non-null int64
# 数值列的统计摘要
print(df.describe())
# 年龄 薪资
# count 5.000000 5.000000
# mean 23.800000 19200.000000
# std 2.774887 4147.288271
# min 21.000000 15000.000000
# 25% 22.000000 16000.000000
# 50% 23.000000 18000.000000
# 75% 25.000000 22000.000000
# max 28.000000 25000.000000

一个新人可直接照抄的“拿到新表先做什么”顺序

Section titled “一个新人可直接照抄的“拿到新表先做什么”顺序”

更稳的顺序通常是:

  1. 先看 df.head()
  2. 再看 df.info()
  3. 再看 df.describe()
  4. 最后再开始筛选和清洗

这样会比一上来就直接写复杂操作更不容易迷路。

# 访问单列 → 返回 Series
print(df["姓名"])
# 0 张三
# 1 李四
# ...
# 也可以用点语法(列名不含空格且不与方法冲突时)
print(df.年龄)
# 访问多列 → 返回 DataFrame
print(df[["姓名", "薪资"]])
# 姓名 薪资
# 0 张三 15000
# 1 李四 22000
# ...

为什么“先学会看列”这么重要?

Section titled “为什么“先学会看列”这么重要?”

因为后面大多数 Pandas 工作都在做三件事:

  • 选列
  • 改列
  • 基于列做统计和组合

所以第一次学 Pandas,与其急着记很多高阶方法, 不如先把“我怎么找到这列、它是什么类型、我能对它做什么”打稳。

# 添加新列
df["税后薪资"] = df["薪资"] * 0.85
print(df[["姓名", "薪资", "税后薪资"]])
# 基于条件添加列
df["薪资等级"] = np.where(df["薪资"] >= 20000, "", "")
print(df[["姓名", "薪资", "薪资等级"]])
# 删除列
df = df.drop(columns=["税后薪资"]) # 返回新 DataFrame
# 或者
# df.drop(columns=["税后薪资"], inplace=True) # 原地修改

索引是 Pandas 区别于 NumPy 的关键特性。

df = pd.DataFrame({
"姓名": ["张三", "李四", "王五"],
"年龄": [22, 25, 23],
"薪资": [15000, 22000, 18000]
})
# 把"姓名"列设为索引
df_indexed = df.set_index("姓名")
print(df_indexed)
# 年龄 薪资
# 姓名
# 张三 22 15000
# 李四 25 22000
# 王五 23 18000
# 通过索引访问
print(df_indexed.loc["李四"])
# 年龄 25
# 薪资 22000
# 重置索引
df_reset = df_indexed.reset_index()
print(df_reset) # 和原来一样

Pandas 的运算会自动按索引对齐——这是一个非常强大的特性:

s1 = pd.Series({"语文": 85, "数学": 92, "英语": 78})
s2 = pd.Series({"数学": 88, "英语": 82, "物理": 90})
# 自动按索引对齐相加
result = s1 + s2
print(result)
# 数学 180.0
# 物理 NaN ← s1 没有物理,结果为 NaN
# 英语 160.0
# 语文 NaN ← s2 没有语文,结果为 NaN

特性SeriesDataFrame
维度一维二维
类比Excel 的一列整张 Excel 表格
创建pd.Series([1,2,3])pd.DataFrame({"a":[1,2]})
访问列df["列名"] 返回 Series
索引一个 Index行索引 + 列索引

学完这一页,至少保留这张证据卡:

数据框状态
列、数据类型、行数、缺失值和样本行
操作
读/写、select/filter、清洗、转换、groupby、merge,或时间序列步骤
输出
结果表、保存的文件、聚合、连接结果,或时间索引视图
失败检查
dtype 不匹配、缺失数据、重复键、链式赋值或时间频率错误
期望产出
前后对比表格样本,以及转换原因
root((Pandas 核心数据结构))
Series
带标签的一维数组
index + values
像增强版的字典
DataFrame
带标签的二维表格
index + columns + values
像 Excel 表格
Index 索引
自动对齐
set_index / reset_index
标签访问
常用方法
head / tail
info / describe
shape / dtypes / columns

# 创建一个 Series 表示一周每天的步数
# 索引用 "周一" 到 "周日"
# 1. 打印平均步数
# 2. 找出步数最多的一天
# 3. 找出步数超过 8000 的天数
# 创建一个功能进度 DataFrame,包含:
# 功能、负责人、计划小时、实际小时、状态 五列,至少 5 条任务
# 1. 添加一列"工时差"
# 2. 添加一列"是否超时"
# 3. 添加一列"风险"(阻塞 -> 高,评审中 -> 中,已完成 -> 低,其他 -> 观察)
# 4. 用 describe() 查看数值列的统计信息
# 使用练习 2 的 DataFrame
# 1. 把"功能"设为索引
# 2. 通过功能名查找某条进度记录
# 3. 重置索引
参考实现与讲解
  • 一周步数 Series 应把星期名设为 index,然后计算均值、最高步数日 idxmax,并用布尔条件筛出高步数日。
  • 功能进度 DataFrame 先增加 工时差是否超时,再根据规则用函数、mappd.cut 创建 风险 列。describe() 是有用证据,但不是完整分析。
  • 索引练习要同时展示 set_indexreset_index。好的答案会说明什么时候用 .loc 的标签查找比 .iloc 的位置查找更清楚。