Python for Data Analysis权威指南——超清英文版带书签目录
简介:《Python for Data Analysis》是数据科学领域的经典著作,由Wes McKinney撰写,系统讲解了使用Python进行高效数据处理、清洗、分析与可视化的全流程。本书重点介绍Pandas、NumPy、SciPy、Matplotlib和Seaborn等核心库的应用,涵盖数据导入导出、数据清洗、分组聚合、时间序列分析及基础统计方法等内容。全书结合大量实例,适合数据分析师、数据科学家和开发人员学习实践,帮助读者掌握Python在真实数据分析场景中的应用技巧,提升数据处理效率与洞察力。 
1. Python基础语法入门
Python作为数据科学领域的核心编程语言,其简洁清晰的语法结构为数据分析工作奠定了坚实基础。本章将系统讲解Python的基本语法要素,包括变量定义、数据类型(整型、浮点型、字符串、布尔值)、运算符使用、流程控制语句(条件判断与循环结构)以及函数的定义与调用机制。通过实际代码示例展示如何编写可读性强、逻辑清晰的数据处理脚本,并介绍模块化编程思想在数据分析项目中的初步应用。
# 示例:使用for循环与条件判断统计列表中偶数个数
numbers = [1, 2, 3, 4, 5, 6]
count_even = 0
for num in numbers:
if num % 2 == 0:
count_even += 1
print(f"偶数个数:{count_even}") # 输出:偶数个数:3
此外,还将涵盖Python标准库中与数据操作密切相关的内置函数和工具,如 range() 、 zip() 、 enumerate() 等,帮助读者建立扎实的编程基础,为后续深入学习高级数据结构和分析技术提供支撑。
2. NumPy多维数组(ndarray)创建与数学运算
2.1 NumPy数组对象ndarray的核心特性
2.1.1 数组的维度、形状与数据类型
在科学计算和数据分析领域,数据通常以多维形式存在。NumPy中的核心数据结构 ndarray (N-dimensional array)正是为高效处理这类数据而设计。理解其基本属性——维度(dimension)、形状(shape)和数据类型(dtype),是掌握NumPy操作的基础。
维度 是指数组中轴的数量。例如,一个标量是0维,一维数组类似于列表,二维数组类似表格(行×列),三维及以上则可看作“立方体”或更高维空间的数据堆叠。通过 .ndim 属性可以查看数组的维度数:
import numpy as np
# 创建不同维度的数组
scalar = np.array(5) # 0维
vector = np.array([1, 2, 3]) # 1维
matrix = np.array([[1, 2], [3, 4]]) # 2维
tensor = np.array([[[1, 2], [3, 4]],
[[5, 6], [7, 8]]]) # 3维
print(f"标量维度: {scalar.ndim}") # 输出: 0
print(f"向量维度: {vector.ndim}") # 输出: 1
print(f"矩阵维度: {matrix.ndim}") # 输出: 2
print(f"张量维度: {tensor.ndim}") # 输出: 3
逻辑分析 :
-np.array()用于创建数组,输入参数决定了初始结构。
-.ndim返回整数值,表示当前数组有多少个轴。
- 即使是单个元素,只要封装进np.array(),就成为一个0维数组(注意不是Python原生标量)。
形状 由 .shape 属性给出,是一个元组,描述每个维度上的大小。例如,一个 3×4 的二维数组,其形状为 (3, 4) ,分别对应行数和列数。对于上面的例子:
print(f"向量形状: {vector.shape}") # (3,)
print(f"矩阵形状: {matrix.shape}") # (2, 2)
print(f"张量形状: {tensor.shape}") # (2, 2, 2)
注意到一维数组的形状显示为 (n,) ,末尾有一个逗号,这是Python中单元素元组的标准表示法。
数据类型 决定了数组中每个元素占用多少字节以及如何解释这些字节。NumPy支持丰富的数据类型,如 int32 、 float64 、 bool_ 、 complex128 等。可通过 .dtype 查看:
arr_float = np.array([1.0, 2.5, 3.7])
arr_int = np.array([1, 2, 3], dtype=np.int8)
print(f"浮点数组类型: {arr_float.dtype}") # float64
print(f"整型数组类型: {arr_int.dtype}") # int8
参数说明 :
- 默认情况下,NumPy会根据输入自动推断最合适的类型。
- 显式指定dtype可用于节省内存或满足特定算法需求(如GPU加速常需float32)。
下面用表格总结常见属性及其含义:
| 属性 | 类型 | 含义 | 示例 |
|---|---|---|---|
.ndim |
整数 | 维度数量 | 2 表示二维 |
.shape |
元组 | 各维度大小 | (3, 4) 表示3行4列 |
.size |
整数 | 总元素个数 | 对 (3,4) 为12 |
.dtype |
数据类型对象 | 存储元素的类型 | float64 , int32 |
.itemsize |
整数 | 每个元素占字节数 | float64 为8 |
此外,我们可以使用Mermaid流程图来展示从原始数据到ndarray的构建过程及其关键属性提取路径:
graph TD
A[原始数据] --> B{选择创建方式}
B --> C[arange/zeros/ones]
B --> D[嵌套列表转换]
B --> E[文件加载]
C --> F[生成ndarray]
D --> F
E --> F
F --> G[访问属性]
G --> H[".ndim → 维度"]
G --> I[".shape → 形状"]
G --> J[".dtype → 类型"]
G --> K[".size → 总元素数"]
该流程体现了从多种源头构建数组后统一进行属性解析的过程,有助于理解NumPy抽象模型的一致性。
2.1.2 ndarray与Python原生列表的性能对比
尽管Python内置的 list 也能存储多个值,但在数值计算场景下, ndarray 相比原生列表具有显著优势,主要体现在 内存效率 、 访问速度 和 向量化运算能力 三个方面。
内存效率比较
Python列表本质上是对象指针数组,每个元素都是指向实际对象(如整数、字符串等)的引用。这意味着即使所有元素都是整数,也会因PyObject头部开销导致内存浪费。而 ndarray 采用连续内存块存储固定类型的原始数据,极大减少内存碎片和额外开销。
以下代码演示两者的内存占用差异:
import sys
import numpy as np
py_list = list(range(1000))
np_array = np.arange(1000, dtype='int32')
print(f"Python列表内存占用: {sys.getsizeof(py_list)} 字节")
print(f"NumPy数组内存占用: {np_array.nbytes} 字节")
输出可能为:
Python列表内存占用: 9032 字节
NumPy数组内存占用: 4000 字节
逻辑分析 :
-sys.getsizeof()测量容器本身大小,包含指针和管理信息。
-np_array.nbytes仅统计实际数据所占空间(1000 × 4 bytes = 4000)。
- 列表每个整数作为独立对象存储,加上指针(约8字节)和PyObject头(约28字节),总开销远高于ndarray。
访问速度测试
由于 ndarray 的数据布局紧凑且类型一致,CPU缓存命中率高,访问速度更快。我们通过时间测量验证这一点:
import time
# 大规模数据测试
size = 10_000_000
py_list = list(range(size))
np_arr = np.arange(size)
# 测试列表求和
start = time.time()
sum(py_list)
list_time = time.time() - start
# 测试数组求和
start = time.time()
np_arr.sum()
numpy_time = time.time() - start
print(f"Python列表求和耗时: {list_time:.4f}s")
print(f"NumPy数组求和耗适: {numpy_time:.4f}s")
典型结果:
Python列表求和耗时: 0.3215s
NumPy数组求和耗时: 0.0187s
扩展说明 :
- NumPy底层用C语言实现,避免了解释器循环开销。
- 向量化操作无需Python层面的for-loop,直接调用高度优化的BLAS/LAPACK库。
为了更系统地对比,下表列出关键性能指标:
| 特性 | Python列表 | NumPy ndarray |
|---|---|---|
| 内存布局 | 不连续(指针数组) | 连续原始数据块 |
| 数据类型 | 可变(异构) | 固定(同构) |
| 支持向量化运算 | 否(需循环) | 是(ufunc) |
| 索引速度 | 较慢(动态查找) | 极快(偏移计算) |
| 聚合函数性能 | 低效(纯Python) | 高效(C级实现) |
| 扩展性 | 支持任意对象 | 限于数值/布尔等基础类型 |
由此可见,在涉及大量数值运算的任务中,使用 ndarray 不仅是编程便利性的提升,更是性能瓶颈突破的关键。
2.1.3 内存布局与数组视图/副本机制
NumPy对内存的精细控制使其既能保证高性能,又能灵活应对复杂操作。其中,“视图(view)”与“副本(copy)”的区别尤为关键,直接影响程序行为和资源消耗。
当执行切片操作时,NumPy默认返回原数组的 视图 ,即共享同一块内存区域的新数组对象。修改视图会影响原始数组:
original = np.array([1, 2, 3, 4, 5])
view = original[1:4] # 切片产生视图
view[0] = 99
print(original) # 输出: [1 99 3 4 5]
逻辑分析 :
-view并未复制数据,而是记录了起始地址、步长、形状等元信息。
- 修改view[0]实际修改了original[1]所在内存位置的值。
相反,若需要完全独立的副本,则应显式调用 .copy() 方法:
copy_arr = original[1:4].copy()
copy_arr[0] = 88
print(original) # 输出: [1 99 3 4 5] —— 原数组不变
print(copy_arr) # 输出: [88 3 4]
这种机制允许开发者根据需求权衡性能与安全性。
内存布局:C-order vs Fortran-order
NumPy数组在内存中按行优先(C-order)或列优先(Fortran-order)排列。这影响迭代顺序和某些操作的效率。
# 创建C顺序数组(默认)
c_order = np.array([[1, 2], [3, 4]], order='C')
f_order = np.array([[1, 2], [3, 4]], order='F')
print("C-order flatten:", c_order.flatten(order='C')) # [1 2 3 4]
print("F-order flatten:", f_order.flatten(order='F')) # [1 3 2 4]
参数说明 :
-order='C':按行遍历,适合大多数现代硬件。
-order='F':按列遍历,适用于某些线性代数库(如LAPACK)。
可通过 .flags 查看数组内存特性:
print(c_order.flags)
输出包含:
C_CONTIGUOUS : True
F_CONTIGUOUS : False
OWNDATA : True
C_CONTIGUOUS:是否行连续存储F_CONTIGUOUS:是否列连续存储OWNDATA:是否拥有底层数据所有权
非连续数组(如转置后的二维数组)可能导致后续操作降级为逐元素处理,影响性能。此时可用 .copy() 强制重建连续内存:
A = np.random.rand(3, 4)
B = A.T # 转置后可能非C连续
C = B.copy(order='C') # 强制转为C连续副本
下面用Mermaid图展示视图与副本的关系:
graph LR
Original((原始数组)) -- 切片 --> View((视图))
Original -- copy() --> Copy((副本))
style View fill:#e1f5fe,stroke:#03a9f4
style Copy fill:#f0f4c3,stroke:#c0ca33
style Original fill:#ffe0b2,stroke:#ff8f00
subgraph "内存共享"
Original
View
end
subgraph "独立内存"
Copy
end
该图清晰表明:视图与原数组共用数据区,任何一方修改都会反映到另一方;而副本则开辟新空间,实现完全隔离。
综上所述,深入理解 ndarray 的内存模型,有助于编写既高效又安全的科学计算代码。尤其是在处理大规模数据集或进行复杂索引变换时,合理利用视图可节省内存,而适时创建副本则能防止意外副作用。
2.2 多维数组的构建与初始化方法
2.2.1 使用arange、zeros、ones、eye等函数创建数组
NumPy提供了多种便捷函数用于快速生成常用结构的数组,极大提升了开发效率。这些函数不仅能简化编码,还能确保生成的数组具备最优内存布局和正确数据类型。
np.arange() :生成等差序列
np.arange(start, stop, step, dtype=None) 功能类似于Python内置的 range() ,但返回的是 ndarray ,支持浮点步长:
# 整数序列
arr1 = np.arange(0, 10, 2) # [0 2 4 6 8]
# 浮点序列(注意精度问题)
arr2 = np.arange(0.5, 2.0, 0.3) # [0.5 0.8 1.1 1.4 1.7]
# 指定类型
arr3 = np.arange(5, dtype=np.float32)
逻辑分析 :
- 参数顺序为起始、终止(不包含)、步长。
- 浮点arange可能存在舍入误差,建议对精确间隔使用linspace。
-dtype可显式设定输出类型,避免隐式转换带来的性能损耗。
np.zeros() 和 np.ones() :全零/全一初始化
在机器学习、图像处理等领域,常需构造特定形状的初始化矩阵:
# 创建全零数组
zero_2d = np.zeros((3, 4)) # 3×4 的浮点零矩阵
zero_3d = np.zeros((2, 3, 4), dtype=int) # 三维整型数组
# 创建全一数组
ones_row = np.ones(5) # [1. 1. 1. 1. 1.]
# 创建与某数组同形的零阵
prototype = np.array([[1,2],[3,4]])
zeros_like = np.zeros_like(prototype)
参数说明 :
- 形状以元组传入,如(rows, cols)。
- 默认dtype=float64,可根据需要调整。
-*_like系列函数(如zeros_like,ones_like)基于已有数组结构复制模板。
np.eye() 与 np.identity() :单位矩阵
线性代数中常用的单位矩阵可通过以下方式生成:
I3 = np.eye(3) # 标准3×3单位阵
I4_shift = np.eye(4, k=1) # 主对角线上方一条对角线为1
I_rect = np.eye(3, 5) # 非方阵情况下的矩形单位阵
print(I3)
# [[1. 0. 0.]
# [0. 1. 0.]
# [0. 0. 1.]]
参数说明 :
-k=0为主对角线;k>0为上方偏移;k<0为下方。
-identity(n)只能生成方阵且k不可调,灵活性较低。
综合对比表
| 函数 | 用途 | 典型应用场景 |
|---|---|---|
arange |
等差序列 | 循环索引、坐标轴生成 |
linspace |
等距点(含端点) | 函数绘图x轴 |
zeros |
全零初始化 | 缓冲区、权重初始化 |
ones |
全一初始化 | 偏置项、计数器 |
full |
自定义填充 | 初始化为特定常数 |
eye |
单位矩阵 | 线性变换、正则化 |
empty |
未初始化数组 | 临时变量(追求极致速度) |
实际应用案例:构建神经网络权重矩阵
在深度学习中,权重初始化至关重要。以下是一个模拟全连接层权重初始化的片段:
input_dim = 784 # 输入特征数(如MNIST图像展平)
output_dim = 10 # 输出类别数
# Xavier初始化近似实现
scale = np.sqrt(2.0 / (input_dim + output_dim))
W = np.random.normal(loc=0.0, scale=scale, size=(input_dim, output_dim))
b = np.zeros(output_dim) # 偏置初始化为0
print(f"权重矩阵形状: {W.shape}, 均值≈{W.mean():.3f}, 标准差≈{W.std():.3f}")
扩展讨论 :
- 此处结合了zeros和随机生成(见下一节),体现组合使用的灵活性。
- 正确的初始化有助于缓解梯度消失问题。
(继续撰写其余章节内容……)
3. Pandas DataFrame构建与数据操作
在现代数据分析流程中,结构化数据的组织、访问和变换是核心任务之一。Pandas作为Python生态系统中最强大的数据分析工具库,提供了两种关键数据结构—— Series 和 DataFrame ,它们为高效处理表格型或时间序列类数据奠定了坚实基础。其中, DataFrame 作为一种二维带标签的数据容器,具备灵活的列类型支持、自动索引对齐机制以及丰富的操作接口,使其成为数据清洗、探索性分析乃至建模准备阶段不可或缺的核心组件。
本章将深入剖析 Pandas 的核心数据结构设计原理,并系统讲解如何从多种来源创建 DataFrame ,掌握行与列的精细化控制方法。进一步地,通过条件筛选、函数映射与字符串向量化操作等技术,实现复杂的数据转换逻辑。最后引入层次化索引(MultiIndex)的概念,展示其在多维数据组织中的强大表达能力,为后续高阶聚合与透视分析打下基础。
3.1 Pandas核心数据结构:Series与DataFrame
Pandas 的两大支柱—— Series 与 DataFrame ,分别对应一维和二维的数据抽象模型。理解它们的设计哲学与内在行为机制,是构建可靠数据处理流水线的前提。
3.1.1 Series的一维标签化数据表示
Series 是一种带有索引的一维数组,可以看作是一个增强版的 NumPy 一维数组。它不仅支持数值类型,还能容纳字符串、日期甚至更复杂的 Python 对象。最重要的是,每个元素都关联一个显式的“标签”——即索引(index),这使得数据访问不再依赖位置顺序,而是基于语义化的名称进行定位。
例如,在分析某城市每日气温时,使用整数索引 [0, 1, 2] 不如使用日期作为索引直观:
import pandas as pd
import numpy as np
dates = pd.date_range('2025-04-01', periods=5)
temperatures = pd.Series([20, 22, 19, 24, 23], index=dates, name="Temperature")
print(temperatures)
输出:
2025-04-01 20
2025-04-02 22
2025-04-03 19
2025-04-04 24
2025-04-05 23
Name: Temperature, dtype: int64
代码逻辑逐行解读:
- 第3行:利用
pd.date_range()创建连续的日期序列,避免手动输入; - 第4行:构造
Series,传入数据列表和自定义索引dates,并指定name属性用于标识该序列含义; - 输出结果展示了索引与值的配对关系,且保留了数据类型信息。
这种基于标签的访问方式极大提升了代码可读性。例如可通过 temperatures['2025-04-02'] 直接获取第二天温度,而不必记忆具体位置。
此外, Series 支持自动对齐运算。假设有另一个表示湿度的序列,即使两者索引不完全一致,Pandas 也会按索引匹配后执行操作:
humidity = pd.Series([60, 65, 70, 68], index=dates[:4])
combined = temperatures + humidity
print(combined)
结果中缺失值自动以 NaN 填充,体现了 Pandas 在处理现实世界非对齐数据时的强大鲁棒性。
| 特性 | 描述 |
|---|---|
| 数据类型 | 支持混合类型,但推荐单一类型以提升性能 |
| 索引唯一性 | 允许重复索引,但在某些操作中可能引发警告 |
| 缺失值处理 | 天然支持 NaN ,多数方法提供 skipna 参数控制是否忽略 |
| 向量化操作 | 所有算术、统计、布尔操作均可直接作用于整个序列 |
graph TD
A[原始数据] --> B{是否有序?}
B -->|是| C[使用DatetimeIndex]
B -->|否| D[使用字符串/整数索引]
C --> E[创建Series]
D --> E
E --> F[支持切片、过滤、函数应用]
该流程图说明了从原始数据到 Series 构建的决策路径及其后续操作可能性。
3.1.2 DataFrame的二维表格结构与列索引机制
如果说 Series 是带标签的一维数组,那么 DataFrame 就是一个由多个 Series 组成的二维表格,每列共享相同的行索引。其结构类似于电子表格或SQL表,但在灵活性和编程接口上远超传统工具。
创建一个简单的学生成绩表如下:
data = {
'Name': ['Alice', 'Bob', 'Charlie', 'Diana'],
'Math': [85, 90, 78, 92],
'English': [88, 85, 91, 87],
'Science': [90, 82, 85, 95]
}
df = pd.DataFrame(data)
df.index.name = 'ID'
print(df)
输出:
Name Math English Science
ID
0 Alice 85 88 90
1 Bob 90 85 82
2 Charlie 78 91 85
3 Diana 92 87 95
参数说明与逻辑分析:
data字典的键成为列名,值构成各列数据;pd.DataFrame()自动推断数据类型,并分配默认整数索引;- 设置
df.index.name提升元数据清晰度,便于后续分组或导出。
DataFrame 的每一列都是一个 Series ,可通过 df['Math'] 或 df.Math 访问。而每一行则需借助 .loc 或 .iloc 实现精确提取。
更重要的是, DataFrame 列之间可异构:允许同时包含字符串、整数、浮点数甚至布尔值。这种灵活性特别适用于真实业务场景中混合属性的数据集。
下表对比了 DataFrame 与传统数据库表的关键特性差异:
| 特性 | DataFrame | SQL 表 |
|---|---|---|
| 类型动态性 | ✅ 列类型可在运行期变更 | ❌ 通常需要预定义 schema |
| 索引灵活性 | ✅ 支持多级、非唯一索引 | ⚠️ 主键唯一,索引受限 |
| 缺失值支持 | ✅ 天然支持 NaN |
✅ 但需显式处理 NULL |
| 内存驻留 | ✅ 整体加载至内存 | ❌ 一般流式查询 |
| 运算向量化 | ✅ 所有操作默认向量化 | ❌ 需依赖窗口函数扩展 |
这一对比凸显了 Pandas 在交互式分析中的优势:无需持久化存储即可快速试验多种变换策略。
3.1.3 数据对齐与自动索引匹配特性
Pandas 最具特色的行为之一是 基于索引的数据对齐机制 。无论是在两个 Series 之间相加,还是多个 DataFrame 进行合并,Pandas 都会首先根据索引标签进行对齐,再执行运算。
考虑以下案例:
s1 = pd.Series([10, 20, 30], index=['a', 'b', 'c'])
s2 = pd.Series([5, 15], index=['b', 'd'])
result = s1 + s2
print(result)
输出:
a NaN
b 25.0
c NaN
d NaN
dtype: float64
尽管 s1 和 s2 长度不同、索引不完全重叠,Pandas 仍能正确识别交集部分(仅 'b' 匹配),并对其他位置补 NaN 。这是因为在内部,Pandas 执行了类似集合交集的操作来确定对齐轴。
同样适用于 DataFrame :
df1 = pd.DataFrame({'A': [1, 2], 'B': [3, 4]}, index=[0, 1])
df2 = pd.DataFrame({'B': [10, 20], 'C': [30, 40]}, index=[1, 2])
result_df = df1 + df2
print(result_df)
输出:
A B C
0 NaN NaN NaN
1 NaN 14 NaN
2 NaN NaN NaN
只有当行列索引均匹配时才会计算结果,其余均为缺失值。这种机制确保了跨数据源合并的安全性,防止因位置错位导致错误拼接。
该特性广泛应用于:
- 多时间段指标合并;
- 不同采样频率传感器数据融合;
- 分布式采集系统的后期整合。
其底层依赖于高效的哈希表查找算法,保证即使面对数百万级别的索引也能快速完成对齐。
flowchart LR
A[输入 DataFrame/Series] --> B{是否存在共同索引?}
B -->|是| C[按索引对齐数据]
B -->|否| D[生成全为空的结果]
C --> E[执行元素级运算]
E --> F[返回新对象,原数据不变]
此流程图揭示了 Pandas 如何在每次二元操作中保障语义一致性,体现了“以数据意义为中心”的设计理念。
3.2 数据的导入与结构化构建
构建有效的 DataFrame 是任何数据分析工作的起点。除了从外部文件(如 CSV、Excel)读取外,更多时候我们需要从程序内生数据结构(如字典、列表、NumPy 数组)初始化 DataFrame ,并对其进行结构调整。
3.2.1 从字典、列表、NumPy数组构造DataFrame
最常见的构建方式是从字典出发,其中每个键代表一列,对应的值应为等长的可迭代对象:
import numpy as np
import pandas as pd
# 方法1:从字典构建
dict_data = {
'Product': ['Laptop', 'Mouse', 'Keyboard'],
'Price': [1200, 25, 75],
'Stock': [10, 150, 80]
}
df_from_dict = pd.DataFrame(dict_data)
# 方法2:从嵌套列表构建(需配合 columns 指定列名)
list_data = [
['Laptop', 1200, 10],
['Mouse', 25, 150],
['Keyboard', 75, 80]
]
df_from_list = pd.DataFrame(list_data, columns=['Product', 'Price', 'Stock'])
# 方法3:从 NumPy 数组构建
np_array = np.array([[1200, 10], [25, 150], [75, 80]])
df_from_numpy = pd.DataFrame(np_array,
columns=['Price', 'Stock'],
index=['Laptop', 'Mouse', 'Keyboard'])
逻辑分析与参数说明:
columns参数用于命名列,若未提供则默认为RangeIndex;index可自定义行标签,常用于产品 ID、时间戳等有意义标识;- 当输入为 NumPy 数组时,所有数据必须同质(同一 dtype),而字典允许列间类型不同。
值得注意的是,Pandas 在构建过程中会尝试统一列的数据类型。例如,若某列中混入字符串与数字,整列将被转为 object 类型,影响后续向量化效率。因此建议提前清理或显式转换类型。
| 构造方式 | 优点 | 缺点 |
|---|---|---|
| 字典 | 结构清晰,支持异构列 | 要求所有值长度一致 |
| 嵌套列表 | 简单直观 | 易出错,缺乏列语义 |
| NumPy 数组 | 性能最优,适合大规模数值数据 | 不支持混合类型 |
实际开发中推荐优先使用字典形式,因其最贴近“字段-值”的自然认知模式。
3.2.2 列访问、添加、删除与重命名操作
一旦 DataFrame 被创建,常见的维护操作包括查看、新增、修改和移除列。
查看与选择列
# 获取单列(返回 Series)
price_series = df['Price']
# 获取多列(返回 DataFrame)
subset = df[['Product', 'Stock']]
注意:单列用一对方括号返回 Series ;双层方括号返回 DataFrame ,保持二维结构。
添加新列
# 添加总价列:单价 × 库存
df['Total_Value'] = df['Price'] * df['Stock']
此操作利用广播机制,逐行计算每项商品总价值,体现 Pandas 的向量化优势。
删除列
# 删除临时列
df.drop('Total_Value', axis=1, inplace=True)
axis=1表示按列操作;inplace=True修改原对象,否则返回副本。
重命名列
df.rename(columns={'Price': 'Unit_Price'}, inplace=True)
支持映射字典批量重命名,极大简化 ETL 流程中的字段标准化工作。
3.2.3 行索引设置与标签化选取(loc/iloc)
Pandas 提供两套主要的索引器: .loc (基于标签)和 .iloc (基于位置)。
# 设置自定义索引
df.set_index('Product', inplace=True)
# 使用 .loc 按标签选取
mouse_row = df.loc['Mouse']
# 使用 .iloc 按位置选取
first_row = df.iloc[0]
# 切片操作
top_two = df.iloc[0:2] # 注意:iloc 左闭右开
| 索引器 | 依据 | 是否包含终点 | 示例 |
|---|---|---|---|
.loc |
标签名 | ✅ 包含 | df.loc['A':'C'] |
.iloc |
整数位置 | ❌ 不包含 | df.iloc[0:2] → 0,1 行 |
强烈建议在索引稳定后使用 .loc ,因为它更具可读性和健壮性。例如:
# 条件筛选 + loc 联合使用
high_value_items = df.loc[df['Unit_Price'] > 100]
这条链式操作先生成布尔掩码,再通过 .loc 提取满足条件的行,构成了 Pandas 中极为常用的“过滤模式”。
graph TB
Start[开始选择数据] --> Condition{按标签还是位置?}
Condition -->|标签| UseLoc[使用 .loc]
Condition -->|位置| UseIloc[使用 .iloc]
UseLoc --> Example1[df.loc['Bob']]
UseIloc --> Example2[df.iloc[1]]
Example1 --> End[返回结果]
Example2 --> End
该流程图指导用户在不同情境下做出正确的索引选择。
3.3 数据选择、过滤与变换技术
数据操作的本质是对原始信息进行提炼与重构。Pandas 提供了多层次的选择、过滤与转换工具,使开发者能够以声明式语法实现复杂逻辑。
3.3.1 条件表达式与布尔掩码的应用
布尔掩码是 Pandas 中实现条件筛选的核心机制。通过比较操作生成 True/False 数组,再将其传递给 .loc 完成子集提取。
mask = df['Unit_Price'] >= 80
filtered_df = df.loc[mask]
复合条件可用 & (且)、 | (或)、 ~ (非)连接,注意括号优先级:
high_stock_expensive = df.loc[(df['Unit_Price'] > 50) & (df['Stock'] > 50)]
也可用于赋值更新:
df.loc[df['Unit_Price'] > 100, 'Category'] = 'Premium'
此句将高价产品标记为“Premium”,展示了“条件定位 + 赋值”的典型用法。
| 运算符 | 含义 | 示例 |
|---|---|---|
== |
等于 | df['A'] == 5 |
!= |
不等于 | df['A'] != 'X' |
> < |
大于小于 | df['Score'] > 90 |
isin() |
成员判断 | df['Name'].isin(['Alice']) |
str.contains() |
字符串匹配 | df['Name'].str.contains('a') |
这些工具组合起来可用于实现复杂的业务规则引擎。
3.3.2 apply()与map()函数实现自定义转换
当内置操作无法满足需求时,可通过 apply() 和 map() 注入自定义逻辑。
# map(): 适用于 Series,一对一映射
grade_map = {85: 'B', 90: 'A', 78: 'C', 92: 'A'}
df['Grade'] = df['Unit_Price'].map(grade_map)
# apply(): 可作用于行或列
def price_category(row):
if row['Unit_Price'] > 100:
return 'High'
elif row['Unit_Price'] > 50:
return 'Medium'
else:
return 'Low'
df['Level'] = df.apply(price_category, axis=1)
map()更快,适合简单查表;apply()更灵活,可访问整行/列,但性能较低,慎用于大表。
对于纯元素级操作,推荐使用向量化替代:
# 更高效的方式
df['Efficient_Level'] = pd.cut(df['Unit_Price'],
bins=[0, 50, 100, np.inf],
labels=['Low', 'Medium', 'High'])
pd.cut() 是专为区间分类设计的向量化函数,执行速度远超循环式 apply 。
3.3.3 向量化字符串操作:str访问器详解
Pandas 提供 .str 访问器,封装了大量高效的字符串处理方法,全部支持向量化执行。
names = pd.Series([' Alice ', 'BOB', 'charlie!'])
cleaned = (names
.str.strip() # 去空格
.str.replace('!', '') # 替换字符
.str.title()) # 首字母大写
print(cleaned)
输出:
0 Alice
1 Bob
2 Charlie
dtype: object
常用方法包括:
| 方法 | 功能 |
|---|---|
.str.lower() / .upper() |
大小写转换 |
.str.startswith(prefix) |
判断前缀 |
.str.extract(r'(\d+)') |
正则提取数字 |
.str.split(expand=True) |
分割成多列 |
例如从地址中提取邮编:
addresses = pd.Series(['Beijing 100084', 'Shanghai 200001'])
zip_codes = addresses.str.extract(r'(\d{6})')
正则捕获组 (\\d{6}) 匹配六位数字,返回一个新的 DataFrame 。
graph LR
Raw[String Column] --> Strip[.str.strip()]
Strip --> Lower[.str.lower()]
Lower --> Replace[.str.replace()]
Replace --> Extract[.str.extract(regex)]
Extract --> Cleaned[Clean Output]
该流程图描绘了一个典型的文本清洗管道,展示了 .str 链式调用的强大表现力。
3.4 层次化索引与多级数据组织
当数据具有多个分类维度(如地区+年份+产品类别),扁平索引难以有效组织。此时,层次化索引(MultiIndex)提供了高维数据建模的能力。
3.4.1 MultiIndex的创建与层级操作
arrays = [
['North', 'North', 'South', 'South'],
['2023', '2024', '2023', '2024']
]
index = pd.MultiIndex.from_arrays(arrays, names=['Region', 'Year'])
sales = pd.Series([100, 120, 90, 110], index=index)
print(sales)
输出:
Region Year
North 2023 100
2024 120
South 2023 90
2024 110
dtype: int64
通过 from_arrays() 创建两级索引, names 参数赋予语义标签。
访问方式也相应升级:
# 获取某区域所有年份
north_sales = sales.loc['North']
# 获取特定年份所有区域
year_2023 = sales.xs('2023', level='Year')
xs() (cross-section)方法专门用于跨层级提取切片。
3.4.2 分层索引下的数据透视与重排
MultiIndex 常由 pivot() 或 groupby() 自动生成。例如:
df_wide = df.reset_index().pivot(index='Region', columns='Year', values='Sales')
可将长格式转为宽格式报表。反之, .stack() 和 .unstack() 可在多级结构间自由切换。
最终形成的结构支持高级聚合、分组统计与可视化联动,是构建商业智能仪表盘的基础。
flowchart TD
Long[Long Format] --> Pivot[pivot_table()]
Pivot --> Wide[Wide Format]
Wide --> Unstack[.unstack()]
Unstack --> Hierarchical[MultiIndex DataFrame]
Hierarchical --> Plot[Plotting]
整个流程展现了从原始记录到多维分析视图的完整转化链条。
4. 数据清洗与预处理关键技术
在真实世界的数据分析项目中,原始数据往往充满噪声、不完整或结构混乱。据行业统计,数据科学家平均花费60%以上的时间用于数据清洗与预处理工作。高质量的建模和分析结果高度依赖于前期对数据的规范化处理。本章系统阐述Pandas中实现数据质量提升的核心技术路径,涵盖缺失值管理、异常值识别、重复记录去重以及数据类型标准化等关键环节。通过结合理论推导与实战代码示例,展示如何构建鲁棒性强、可复用的数据预处理流程。
4.1 缺失值的识别与处理策略
缺失数据是数据集中最常见的质量问题之一。它可能源于采集设备故障、人为录入疏漏或系统集成错误。若直接忽略缺失值而进行建模,可能导致偏差估计、模型过拟合甚至计算中断。因此,必须首先明确缺失值的存在形式,并选择合理的填补或删除策略。
4.1.1 isna()与notna()检测缺失模式
Pandas将 NaN (Not a Number)作为浮点型缺失值的标准表示,同时支持 None 和 NaT (Not a Time)用于对象类型和时间类型的空值。使用 isna() 函数可以返回一个布尔型DataFrame,标记每个位置是否为缺失值。
import pandas as pd
import numpy as np
# 构造含缺失值的示例数据
data = {
'name': ['Alice', 'Bob', None, 'David', 'Eva'],
'age': [25, np.nan, 30, 35, np.nan],
'salary': [50000, 60000, np.nan, 80000, 70000],
'department': ['HR', 'IT', 'Finance', None, 'IT']
}
df = pd.DataFrame(data)
# 检测缺失值
missing_mask = df.isna()
print(missing_mask)
输出如下:
name age salary department
0 False False False False
1 False True False False
2 True False True False
3 False False False True
4 False True False False
逐行解析:
- 第1–5行:导入必要的库。
- 第8–12行:构造包含字符串、整数、浮点等多种类型的字典数据,其中显式插入 None 和 np.nan 模拟缺失。
- 第15行:调用 df.isna() 生成布尔掩码,True表示该位置为空。
- 输出显示每列的具体缺失分布,便于后续统计。
进一步可统计各列缺失数量:
missing_count = df.isna().sum()
print("每列缺失数:\n", missing_count)
结果:
每列缺失数:
name 1
age 2
salary 1
department 1
dtype: int64
此方法适用于快速诊断数据完整性问题,尤其在大规模数据集中可用于自动化预警机制。
4.1.2 删除含缺失记录的dropna()方法
当缺失比例较低且样本充足时,最简单的处理方式是直接删除含有缺失值的行或列。Pandas提供 dropna() 函数,支持多种删除逻辑。
# 删除任意含缺失的行
df_cleaned_any = df.dropna()
# 仅当所有字段都为空时才删除
df_cleaned_all = df.dropna(how='all')
# 删除特定列中存在缺失的行
df_cleaned_subset = df.dropna(subset=['age', 'salary'])
# 删除整列为缺失的列
df_cleaned_axis = df.dropna(axis=1, how='all')
| 参数 | 含义 | 示例 |
|---|---|---|
how='any' |
只要任一元素为空就删除 | df.dropna() |
how='all' |
所有元素均为空才删除 | df.dropna(how='all') |
subset=['col1'] |
仅检查指定列是否缺失 | df.dropna(subset=['age']) |
axis=1 |
按列方向操作(即删除列) | df.dropna(axis=1) |
参数说明:
- how 控制删除条件;
- subset 允许限定作用域;
- axis 决定操作维度(0=行,1=列)。
该方法简单高效,但需谨慎评估信息损失风险。通常建议先分析缺失机制(MCAR、MAR、MNAR),再决定是否删除。
4.1.3 插值填充与均值/众数替换技巧
对于高价值特征或小样本场景,应优先考虑填补而非删除。常用方法包括常量填充、统计量替换和插值法。
# 使用均值填充数值型变量
df['age'].fillna(df['age'].mean(), inplace=True)
# 使用众数填充分类变量
mode_dept = df['department'].mode()[0] # 获取首个众数
df['department'].fillna(mode_dept, inplace=True)
# 使用前向填充(适用于时间序列)
df['salary'].fillna(method='ffill', inplace=True)
# 多列批量填充不同策略
fill_rules = {
'age': df['age'].median(),
'salary': df['salary'].mean(),
'name': 'Unknown',
'department': 'Other'
}
df.fillna(value=fill_rules, inplace=True)
逻辑分析:
- fillna() 支持标量、字典、方法等多种输入;
- method='ffill' 实现向前传播非空值;
- 字典型 value 参数允许差异化填充规则。
此外,还可利用线性插值提升连续变量的合理性:
df_interpolated = df.copy()
df_interpolated['age'] = df_interpolated['age'].interpolate(method='linear')
注意: 插值适用于有序数据(如时间序列),随机缺失情况下慎用。
graph TD
A[原始数据] --> B{是否存在缺失?}
B -- 否 --> C[继续分析]
B -- 是 --> D[评估缺失比例]
D -- 高缺失率 --> E[考虑删除变量]
D -- 低缺失率 --> F[选择填补策略]
F --> G[均值/中位数填充]
F --> H[众数填充]
F --> I[插值法]
F --> J[机器学习预测填补]
G --> K[更新数据集]
H --> K
I --> K
J --> K
上述流程图展示了从缺失检测到最终修复的整体决策路径,体现了工程实践中“先诊断、后干预”的原则。
4.2 异常值检测与修正机制
异常值(Outliers)是指明显偏离整体分布趋势的极端观测点。它们可能是测量误差、输入错误或罕见事件的结果。若不加以处理,异常值会对均值、方差等统计量造成显著干扰,进而影响回归、聚类等模型性能。
4.2.1 基于统计方法(Z-score、IQR)识别离群点
Z-Score 方法
Z-score衡量某点距离均值的标准差倍数。一般认为|Z| > 3的数据为异常值。
from scipy import stats
# 计算Z-score
z_scores = np.abs(stats.zscore(df[['age', 'salary']]))
outliers_z = (z_scores > 3).any(axis=1)
print("Z-score异常点索引:", df[outliers_z].index.tolist())
解释:
- stats.zscore() 对每列标准化;
- np.abs() 取绝对值;
- .any(axis=1) 判断任一特征超标即视为异常。
四分位距法(IQR)
IQR基于四分位数,更具抗噪性,适合偏态分布。
Q1 = df['salary'].quantile(0.25)
Q3 = df['salary'].quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
outliers_iqr = df[(df['salary'] < lower_bound) | (df['salary'] > upper_bound)]
| 统计量 | 公式 | 用途 |
|---|---|---|
| Q1 | 第25百分位数 | 下界基准 |
| Q3 | 第75百分位数 | 上界基准 |
| IQR | Q3 - Q1 | 衡量数据散布程度 |
| 异常区间 | [Q1−1.5×IQR, Q3+1.5×IQR] | 定义正常范围 |
该方法广泛应用于箱线图绘制,是金融风控、用户行为分析中的基础工具。
4.2.2 可视化手段辅助异常判断(箱线图、散点图)
图形化方式能直观揭示潜在异常。以Matplotlib和Seaborn为例:
import matplotlib.pyplot as plt
import seaborn as sns
fig, axes = plt.subplots(1, 2, figsize=(12, 5))
# 箱线图
sns.boxplot(y=df['salary'], ax=axes[0])
axes[0].set_title('Salary Boxplot')
# 散点图(年龄 vs 薪资)
sns.scatterplot(x=df['age'], y=df['salary'], ax=axes[1])
axes[1].set_title('Age vs Salary Scatter Plot')
plt.tight_layout()
plt.show()
这些图表帮助确认数值型变量之间的关系及孤立点分布。例如,在薪资散点图中,若出现“30岁领取200万年薪”这类极端组合,虽未必是错误,但仍需业务验证。
4.2.3 异常值截断或替换的实际案例
一旦确认异常值为噪声,常见处理方式包括:
- 截断(Winsorization): 将超出边界值的数据压缩至上下限。
- 替换为NA后填补: 视为缺失值处理。
- 单独建模分析: 若代表特殊群体,则另作分类。
# Winsorize处理:将top/bottom 1%替换为边界值
from scipy.stats.mstats import winsorize
df['salary_winsorized'] = winsorize(df['salary'], limits=[0.01, 0.01])
# 或手动截断
df['salary_clipped'] = df['salary'].clip(lower=lower_bound, upper=upper_bound)
flowchart LR
Start[开始] --> Load[加载数据]
Load --> Detect[检测异常值]
Detect --> Method{选择方法}
Method --> Z[Z-score]
Method --> I[IQR]
Method --> V[可视化]
Z --> Action[标记或处理]
I --> Action
V --> Action
Action --> Decide[是否保留?]
Decide -->|否| Correct[修正/剔除]
Decide -->|是| Keep[纳入分析]
Correct --> End[完成]
Keep --> End
此流程强调“识别—评估—决策”闭环,避免盲目清除异常,确保数据分析的科学性与业务贴合度。
4.3 重复数据的发现与去重操作
重复记录会扭曲频率统计、误导聚合分析并浪费存储资源。尤其是在多源数据合并过程中,极易产生完全重复或部分字段重复的冗余条目。
4.3.1 duplicated()与drop_duplicates()函数详解
duplicated() 返回布尔Series,标识当前行是否为此前已出现过的重复项(默认保留首次出现为False)。
# 标记重复行
df['is_duplicate'] = df.duplicated()
# 查看重复记录
duplicates = df[df['is_duplicate']]
print(duplicates)
# 删除重复项(保留第一条)
df_unique = df.drop_duplicates()
关键参数说明:
| 参数 | 默认值 | 功能 |
|---|---|---|
subset |
None | 指定用于比较的列子集 |
keep |
‘first’ | {‘first’, ‘last’, False},控制保留策略 |
inplace |
False | 是否原地修改 |
例如,若只想根据姓名和部门判断重复:
df.drop_duplicates(subset=['name', 'department'], keep='first', inplace=True)
4.3.2 基于子集字段的重复判定逻辑
在实际业务中,“全表重复”较少见,更多是关键主键冲突。例如员工编号重复、订单ID重复等。
# 假设添加employee_id字段
df_with_id = pd.DataFrame({
'emp_id': [101, 102, 101, 103, 104],
'name': ['Alice', 'Bob', 'Alice', 'David', 'Eva'],
'dept': ['HR', 'IT', 'HR', 'Finance', 'IT']
})
# 按emp_id判断唯一性
dup_by_id = df_with_id.duplicated(subset='emp_id', keep=False)
print("按ID重复的记录:\n", df_with_id[dup_by_id])
输出:
emp_id name dept
0 101 Alice HR
2 101 Alice HR
此时可根据业务规则选择保留最新记录或报错提醒。
| 场景 | 子集字段 | 处理策略 |
|---|---|---|
| 用户注册日志 | user_id | 保留最早注册 |
| 交易流水 | transaction_id | 不允许重复,抛出异常 |
| 设备上报数据 | device_id + timestamp | 组合键去重 |
通过合理设置 subset 和 keep 参数,可在保证数据完整性的同时消除冗余。
graph TB
A[读取原始数据] --> B[执行duplicated()]
B --> C{是否存在重复?}
C -- 否 --> D[无需去重]
C -- 是 --> E[分析重复原因]
E --> F[确定去重维度]
F --> G[调用drop_duplicates()]
G --> H[输出清洗后数据]
该流程强调去重不仅是技术操作,更是数据治理的一部分,需结合元数据管理和业务规则共同设计。
4.4 数据类型转换与标准化流程
正确的数据类型不仅能提高运算效率,还能防止类型错误引发的逻辑漏洞。例如将字符串型数字参与数学运算会导致异常。
4.4.1 astype()进行显式类型转换
Pandas允许使用 astype() 强制转换列的数据类型。
# 示例数据(存在类型混杂)
raw_data = {
'id': ['1', '2', '3', '4'],
'score': ['85.5', '90', 'invalid', '78'],
'enrolled': ['True', 'False', 'True', 'False']
}
df_raw = pd.DataFrame(raw_data)
# 类型转换
df_raw['id'] = df_raw['id'].astype(int)
df_raw['enrolled'] = df_raw['enrolled'].astype(bool)
# 浮点转换需先处理无效值
df_score_clean = pd.to_numeric(df_raw['score'], errors='coerce')
df_raw['score'] = df_score_clean.astype('float')
参数说明:
- errors='coerce' :将无法解析的值转为NaN;
- astype('category') :节省内存;
- astype('datetime64') :时间类型转换。
4.4.2 类别型数据(category)优化内存使用
对于低基数分类变量(如性别、省份),使用 category 类型可大幅降低内存占用。
# 转换为category类型
df['department'] = df['department'].astype('category')
# 查看内存使用变化
print("Department列内存占用:", df['department'].memory_usage(deep=True))
| 类型 | 示例 | 内存优势 |
|---|---|---|
| object | ‘Sales’, ‘HR’ | 存储指针,开销大 |
| category | 编码为整数 | 显著节省空间 |
特别在百万级以上数据集中,类别化可带来数倍性能提升。
4.4.3 数据归一化与标准化(MinMaxScaler、StandardScaler)
在机器学习建模前,常需对数值特征进行尺度统一。
from sklearn.preprocessing import MinMaxScaler, StandardScaler
scaler_minmax = MinMaxScaler()
scaler_standard = StandardScaler()
# 归一化:缩放到[0,1]
df[['salary_scaled']] = scaler_minmax.fit_transform(df[['salary']])
# 标准化:零均值单位方差
df[['age_standardized']] = scaler_standard.fit_transform(df[['age']])
| 方法 | 公式 | 适用场景 |
|---|---|---|
| Min-Max | ( \frac{x - x_{min}}{x_{max} - x_{min}} ) | 神经网络、图像处理 |
| Z-score | ( \frac{x - \mu}{\sigma} ) | 回归、SVM等距离敏感算法 |
两者均通过Sklearn接口实现管道化处理,便于集成进ML流水线。
graph LR
Start --> LoadData
LoadData --> CheckTypes
CheckTypes --> ConvertTypes
ConvertTypes --> HandleCategorical
HandleCategorical --> ScaleNumerical
ScaleNumerical --> OutputCleaned
整个标准化流程应作为数据预处理的标准步骤嵌入自动化脚本中,保障模型输入的一致性和稳定性。
5. Pandas groupby机制与聚合分析实战
在现代数据分析流程中,数据往往以“分组-聚合”的形式揭示其内在规律。Pandas 的 groupby 机制正是为此类任务设计的核心工具之一,它允许用户基于一个或多个分类变量对数据进行切片、分别处理,并最终整合结果。这种操作模式不仅广泛应用于业务报表生成、统计摘要计算,也在机器学习的特征工程阶段发挥着重要作用。理解 groupby 的底层逻辑及其灵活应用方式,是构建高效、可解释性强的数据分析流水线的关键一环。
本章将深入剖析 Pandas 中 groupby 操作的运行机制,从理论范式到实际编码实践,层层递进地讲解如何利用该功能完成复杂的数据聚合任务。我们将首先解析 Split-Apply-Combine 范式的数学本质与执行路径,随后探讨单级与多级分组的应用场景,接着展示内置与自定义聚合函数的使用技巧,最后拓展至 transform 、 filter 和 pivot_table 等高级功能,帮助读者掌握全面而系统的分组分析能力。
5.1 分组操作的核心原理与执行流程
Pandas 的 groupby 并非简单的“按列分类求和”,而是一种高度抽象化的数据操作范式,其背后遵循的是著名的 Split-Apply-Combine 模型。这一模型由 Hadley Wickham 在其关于数据重塑的研究中提出,已成为现代数据分析语言(如 R、Python、Julia)实现分组操作的标准框架。
5.1.1 Split-Apply-Combine范式的理论解析
Split-Apply-Combine 是一种三步式的数据处理策略:
- Split(拆分) :根据指定的分组键(key),将原始数据集划分为若干互不重叠的子集;
- Apply(应用) :对每个子集独立应用某个函数(如求均值、计数、拟合模型等);
- Combine(合并) :将各子集的计算结果整合为统一的数据结构输出。
这一过程可以用如下 Mermaid 流程图清晰表达:
graph TD
A[原始DataFrame] --> B{Split}
B --> C[Group 1]
B --> D[Group 2]
B --> E[Group n]
C --> F[Apply Function]
D --> G[Apply Function]
E --> H[Apply Function]
F --> I[Combine Results]
G --> I
H --> I
I --> J[Result: Series or DataFrame]
例如,在销售数据集中,若我们希望查看“不同地区每月销售额的平均值”,则系统会先按“地区”和“月份”两个维度进行 Split;然后 Apply mean() 函数于每组的“销售额”列;最后 Combine 成一张新的汇总表。
值得注意的是,Split 阶段并不立即执行任何计算,而是返回一个特殊的中间对象—— DataFrameGroupBy 或 SeriesGroupBy ,这体现了 Pandas 的 惰性计算(lazy evaluation)特性 。只有当调用聚合方法时,真正的 Apply-Combine 才会被触发。
示例代码演示 Split-Apply-Combine 过程
import pandas as pd
# 构造示例销售数据
data = {
'region': ['North', 'South', 'East', 'West', 'North', 'South'],
'product': ['A', 'B', 'A', 'B', 'B', 'A'],
'sales': [100, 150, 200, 130, 170, 110],
'profit': [20, 30, 40, 26, 34, 22]
}
df = pd.DataFrame(data)
# Step 1: Split - 创建 groupby 对象
grouped = df.groupby('region')
# Step 2 & 3: Apply + Combine - 计算每组 sales 的均值
result = grouped['sales'].mean()
print(result)
输出:
region
East 200.0
North 135.0
South 130.0
West 130.0
Name: sales, dtype: float64
逐行逻辑分析与参数说明
- 第 6–9 行:构造包含
region,product,sales,profit四个字段的字典,并转换为DataFrame,便于后续操作。 - 第 12 行:
df.groupby('region')表示以'region'列作为分组键进行 Split。此时并未真正分割数据,仅创建了一个DataFrameGroupBy对象。 - 第 15 行:
.mean()触发 Apply-Combine 阶段。Pandas 自动遍历每个分组,提取'sales'列并计算其算术平均值,最后 Combine 成一个Series,索引为region值。
此模式的优势在于解耦了“如何分组”与“做什么运算”,使得同一分组可以反复用于多种不同的 Apply 操作,提升代码复用性和执行效率。
此外,Split 可基于多种类型的关键字:
- 单列名(字符串)
- 多列列表
- 函数(作用于索引)
- 数组或 Series(长度需匹配)
这些将在后续章节进一步展开。
5.1.2 groupby对象的结构与惰性计算特性
groupby 返回的对象是一个特殊类型的容器,其本身不存储具体数值,而是保存了关于如何划分数据的信息以及原始数据的引用。这种设计带来了显著的性能优势,尤其是在大数据集上避免不必要的内存复制。
groupby对象的核心属性
| 属性/方法 | 功能描述 |
|---|---|
.groups |
字典结构,键为分组名,值为对应行索引列表 |
.ngroups |
分组总数 |
.size() |
各组元素数量统计 |
.get_group(name) |
提取特定名称的子组 |
.apply(func) |
应用函数到每个子组 |
下面我们通过一个完整示例来观察这些属性的行为:
# 继续使用上文 df 数据
grouped_multi = df.groupby(['region', 'product'])
# 查看分组信息
print("Groups Dictionary:")
print(grouped_multi.groups)
print("\nNumber of groups:", grouped_multi.ngroups)
print("\nGroup sizes:")
print(grouped_multi.size())
print("\nExtract 'North-A' group:")
print(grouped_multi.get_group(('North', 'A')))
输出:
Groups Dictionary:
{('East', 'A'): Int64Index([2], dtype='int64'),
('North', 'A'): Int64Index([0], dtype='int64'),
('North', 'B'): Int64Index([4], dtype='int64'),
('South', 'A'): Int64Index([5], dtype='int64'),
('South', 'B'): Int64Index([1], dtype='int64'),
('West', 'B'): Int64Index([3], dtype='int64')}
Number of groups: 6
Group sizes:
region product
East A 1
North A 1
B 1
South A 1
B 1
West B 1
dtype: int64
Extract 'North-A' group:
region product sales profit
0 North A 100 20
逻辑分析与参数说明
grouped_multi.groups返回一个嵌套元组作为键的字典,每个键对应一组数据所在的原始索引位置。这对于调试和验证分组逻辑非常有用。ngroups快速获取总共有多少个独立分组,常用于判断数据离散程度。.size()返回各组的样本数,等价于grouped.count().iloc[:,0],但更高效。.get_group()允许直接访问某一分组的数据块,适合做局部探索性分析。
更重要的是,由于 groupby 是惰性的,以下操作不会引发任何计算开销:
g = df.groupby('region')
# 此时尚未执行任何聚合,只是记录了分组规则
直到你调用 .sum() , .agg() , .transform() 等方法时,Pandas 才真正进入 Apply 阶段。
惰性计算的实际意义
假设我们有一个千万级记录的日志数据集,需要按“用户ID”分组后分别计算多个指标(点击次数、会话时长、转化率)。如果我们每次重新分组,会导致重复扫描整个数据集。而采用惰性模式:
user_groups = df.groupby('user_id')
clicks = user_groups['clicks'].sum()
duration = user_groups['session_duration'].mean()
conversion = user_groups['converted'].sum() / user_groups.size()
上述三个操作共享同一个分组结构,只需一次 Split 即可完成所有 Apply,极大提升了运行效率。
这也意味着我们可以将 groupby 对象作为函数参数传递,实现模块化编程:
def compute_kpi(grouped):
return pd.DataFrame({
'total_sales': grouped['sales'].sum(),
'avg_profit': grouped['profit'].mean(),
'order_count': grouped.size()
})
result_df = compute_kpi(df.groupby('region'))
print(result_df)
输出:
total_sales avg_profit order_count
region
East 200 40.0 1
North 270 27.0 2
South 260 26.0 2
West 130 26.0 1
综上所述, groupby 的核心价值不仅在于语法简洁,更在于其背后的 Split-Apply-Combine 范式与惰性计算机制共同构成了高性能数据分析的基础架构。
5.2 单级与多级分组的应用场景
分组操作的灵活性体现在它可以适应不同粒度的分析需求。无论是单一维度的粗略划分,还是多维交叉的精细切片,Pandas 都能提供一致的接口支持。
5.2.1 按单一列或多列进行数据切片分组
最基础的分组方式是基于单个列进行分类,适用于宏观趋势分析。例如,分析“各产品类别的总销量”。
# 单列分组
single_group = df.groupby('product')['sales'].sum()
print(single_group)
输出:
product
A 410
B 450
Name: sales, dtype: int64
而当需要更细粒度洞察时,应采用多列联合分组。例如,“每个区域中每种产品的销售表现”:
# 多列分组
multi_group = df.groupby(['region', 'product'])['sales'].sum()
print(multi_group)
输出:
region product
East A 200
North A 100
B 170
South A 110
B 150
West B 130
Name: sales, dtype: int64
此时结果为一个具有 MultiIndex(层次化索引)的 Series,可通过 .unstack() 转换为二维表格:
pivot_style = multi_group.unstack(fill_value=0)
print(pivot_style)
输出:
product A B
region
East 200 0
North 100 170
South 110 150
West 0 130
这种方式已接近透视表的功能,体现了分组与重塑之间的紧密联系。
性能对比:单列 vs 多列分组
尽管语法相似,但多列分组的计算成本更高。我们可通过以下实验验证:
import numpy as np
# 生成大规模测试数据
large_df = pd.DataFrame({
'cat1': np.random.choice(['X','Y','Z'], 1_000_000),
'cat2': np.random.choice(['P','Q','R'], 1_000_000),
'value': np.random.randn(1_000_000)
})
%timeit large_df.groupby('cat1')['value'].mean() # 单列分组
%timeit large_df.groupby(['cat1','cat2'])['value'].mean() # 双列分组
通常情况下,双列分组耗时约为单列的 1.8~2.5 倍,主要因哈希组合键的复杂度增加所致。
因此,在满足业务需求的前提下,应优先选择最小必要分组维度。
5.2.2 分组键的函数映射与索引级别指定
除了直接使用列名外, groupby 还支持更高级的分组键定义方式,包括:
- 使用函数动态生成分组标签
- 基于索引层级进行分组(尤其适用于 MultiIndex)
函数映射分组
假设我们要按“销售额是否高于平均水平”来分组:
mean_sales = df['sales'].mean()
def high_low(s):
return 'High' if s > mean_sales else 'Low'
# 使用 map 将函数应用于 sales 列作为分组键
grouped_by_func = df.groupby(df['sales'].map(high_low))
result = grouped_by_func[['sales', 'profit']].mean()
print(result)
输出:
sales profit
Low 126.0 25.2
High 180.0 36.0
这里 df['sales'].map(high_low) 生成了一个临时 Series,作为分组依据。这种方法非常适合基于条件逻辑的动态分组。
索引级别分组
当 DataFrame 具有层次化索引时,可直接按某一层级分组:
# 设置 MultiIndex
df_indexed = df.set_index(['region', 'product'])
# 按第一层索引(region)分组
result = df_indexed.groupby(level=0)['sales'].sum()
print(result)
输出:
region
East 200
North 270
South 260
West 130
Name: sales, dtype: int64
level=0 表示最外层索引, level=1 表示内层。这在处理时间序列或面板数据时极为常见。
结合以上技术,可实现高度定制化的分组逻辑,充分释放 Pandas 的表达能力。
5.3 聚合函数与自定义聚合逻辑
聚合是 groupby 最常见的应用场景,但其潜力远不止于 sum() 或 mean() 。
5.3.1 内置聚合方法(sum、count、mean、size)
Pandas 提供了一系列优化过的内置聚合函数,执行速度快且语义明确:
| 方法 | 说明 |
|---|---|
.sum() |
数值列求和 |
.mean() |
算术平均 |
.count() |
非空值计数 |
.size() |
包括缺失值在内的总行数 |
.max() / .min() |
极值 |
.std() / .var() |
标准差与方差 |
示例:
summary = df.groupby('region').agg({
'sales': ['sum', 'mean', 'std'],
'profit': ['count', 'max']
})
print(summary)
输出:
sales profit
sum mean std count max
region
East 200 200.0 NaN 1 40
North 270 135.0 49.497475 2 34
South 260 130.0 14.142136 2 30
West 130 130.0 NaN 1 26
注意: size() 不接受字段选择,必须单独调用。
5.3.2 使用agg()实现多函数组合统计
agg() 支持传入函数列表或字典,实现混合聚合:
result = df.groupby('region')['sales'].agg(
['sum', 'mean', lambda x: x.max() - x.min()]
).rename(columns={'<lambda>': 'range'})
print(result)
输出:
sum mean range
region
East 200 200.0 0
North 270 135.0 70
South 260 130.0 40
West 130 130.0 0
5.3.3 自定义函数传入agg进行复杂指标计算
可封装复杂逻辑,如计算变异系数(CV):
def cv(x):
return x.std() / x.mean() if x.mean() != 0 else np.nan
result = df.groupby('region')['sales'].agg(cv)
print(result)
输出:
region
East NaN
North 0.3666
South 0.1088
West NaN
Name: sales, dtype: float64
这展示了如何突破内置函数限制,实现领域特定的统计建模。
5.4 变换、过滤与透视表扩展功能
5.4.1 transform()保持维度一致的组内计算
transform 返回与原数据同形状的结果,常用于标准化:
df['sales_z'] = df.groupby('region')['sales'].transform(
lambda x: (x - x.mean()) / x.std()
)
print(df)
可用于去中心化处理,保留原始结构。
5.4.2 filter()基于条件筛选分组结果
仅保留满足条件的分组:
filtered = df.groupby('region').filter(
lambda g: g['sales'].sum() > 150
)
print(filtered)
返回总销售额大于150的 region 所有行。
5.4.3 pivot_table()实现高维数据透视分析
高层封装,等价于复杂 groupby 操作:
pd.pivot_table(df, values='sales', index='region',
columns='product', aggfunc='sum', fill_value=0)
生成二维交叉报表,直观展现分布关系。
6. 时间序列处理与可视化分析集成
6.1 时间数据的解析与标准化表示
在数据分析项目中,时间序列数据广泛存在于金融、物联网、日志监控等领域。正确解析和标准化时间数据是构建可靠分析流程的第一步。Pandas 提供了强大的 pd.to_datetime() 函数,能够自动识别多种日期格式并转换为统一的 datetime64[ns] 类型。
import pandas as pd
# 示例:解析混合格式的时间字符串
date_strings = [
'2023-01-15', 'Jan 20, 2023', '15/03/2023',
'April 10, 2023', '2023.05.20', '20230630'
]
parsed_dates = pd.to_datetime(date_strings)
print(parsed_dates)
输出结果:
DatetimeIndex(['2023-01-15', '2023-01-20', '2023-03-15', '2023-04-10',
'2023-05-20', '2023-06-30'],
dtype='datetime64[ns]', freq=None)
该函数支持参数如 format 显式指定格式以提升性能,也可设置 errors='coerce' 将非法值转为 NaT(Not a Time):
pd.to_datetime(['2023-01-01', 'invalid_date'], errors='coerce')
对于带有时区信息的数据,可使用 tz_localize() 和 tz_convert() 进行处理:
# 创建本地化时间戳
utc_time = pd.Timestamp('2023-07-01 12:00:00', tz='UTC')
beijing_time = utc_time.tz_convert('Asia/Shanghai')
print(beijing_time) # 2023-07-01 20:00:00+08:00
| 原始字符串 | 解析后时间 | 时区 |
|---|---|---|
| 2023-01-15 | 2023-01-15 00:00:00 | None |
| Jan 20, 2023 | 2023-01-20 00:00:00 | None |
| 15/03/2023 | 2023-03-15 00:00:00 | None |
| April 10, 2023 | 2023-04-10 00:00:00 | None |
| 2023.05.20 | 2023-05-20 00:00:00 | None |
| 20230630 | 2023-06-30 00:00:00 | None |
| 2023-07-01T12:00:00Z | 2023-07-01 12:00:00 | UTC |
| 2023-07-01T20:00:00+08:00 | 2023-07-01 20:00:00 | Asia/Shanghai |
6.2 时间序列的索引与重采样技术
将时间列设为索引后,即可利用 DatetimeIndex 实现高效的时间切片查询:
# 构造示例时间序列数据
dates = pd.date_range('2023-01-01', periods=100, freq='D')
data = pd.DataFrame({'value': range(100)}, index=dates)
# 时间切片查询
jan_data = data['2023-01']
mid_jan = data['2023-01-10':'2023-01-15']
resample() 是 Pandas 中用于频率转换的核心方法,支持上采样(upsampling)与下采样(downsampling):
# 按周进行下采样,计算每周最大值
weekly_max = data.resample('W').max()
# 按小时上采样(原为日频),填充缺失值
hourly = data.resample('H').ffill(limit=23) # 向前填充最多23小时
在金融领域,OHLC(Open-High-Low-Close)重采样尤为常见:
# 模拟股票分钟级交易数据
minutes = pd.date_range('2023-01-01 09:30', periods=390, freq='T') # A股交易分钟数
price = pd.DataFrame({
'close': (100 + pd.np.random.randn(390).cumsum()).round(2)
}, index=minutes)
# 转换为5分钟K线
ohlc_rule = price.resample('5T').agg({
'close': ['first', 'max', 'min', 'last']
})
ohlc_rule.columns = ['open', 'high', 'low', 'close']
下表展示不同重采样频率及其典型应用场景:
| 频率字符串 | 描述 | 应用场景 |
|---|---|---|
| ‘D’ | 每日 | 日销售额统计 |
| ‘W’ | 每周 | 周报生成 |
| ‘M’ | 每月 | 财务报表汇总 |
| ‘Q’ | 每季度 | 季度业绩分析 |
| ‘H’ | 每小时 | 监控系统日志 |
| ‘T’ 或 ‘min’ | 每分钟 | 金融高频交易 |
| ‘S’ | 每秒 | 物联网传感器数据 |
| ‘5T’ | 每5分钟 | K线图绘制 |
| ‘15T’ | 每15分钟 | 网络流量聚合 |
| ‘AS-JAN’ | 每年1月开始 | 年度预算周期 |
mermaid 流程图描述 resample 执行逻辑如下:
graph TD
A[原始时间序列] --> B{目标频率?}
B -->|更高频率| C[上采样: 插值或填充]
B -->|更低频率| D[下采样: 分组聚合]
C --> E[ffill / bfill / interpolate]
D --> F[mean / sum / ohlc / custom]
E --> G[新频率时间序列]
F --> G
6.3 移动窗口与滚动统计分析
移动窗口分析用于揭示时间序列中的趋势与波动特征。Pandas 的 rolling() 方法提供滑动窗口操作:
# 计算7天移动平均
data['ma_7'] = data['value'].rolling(window=7).mean()
# 计算30天滚动标准差(波动率)
data['volatility_30'] = data['value'].rolling(window=30).std()
expanding() 则实现从起点到当前点的累积计算:
# 累积最大值
data['cummax'] = data['value'].expanding().max()
# 累积均值
data['cummean'] = data['value'].expanding().mean()
支持链式调用与自定义函数:
# 自定义滚动指标:移动变异系数
def coef_var(x):
return x.std() / x.mean() if x.mean() != 0 else 0
data['cv_14'] = data['value'].rolling(14).apply(coef_var)
参数说明:
- window : 窗口大小(整数)
- min_periods : 最少观测数(默认等于 window)
- center : 是否居中对齐结果(布尔值)
- win_type : 窗口类型(如 ‘hamming’)
以下为一个完整的滚动分析案例:
# 模拟带噪声的趋势数据
trend = pd.Series(100 + 0.5 * range(200) + np.random.normal(0, 5, 200))
analysis = pd.DataFrame({
'original': trend,
'ma_10': trend.rolling(10).mean(),
'ma_30': trend.rolling(30).mean(),
'ewm_0.1': trend.ewm(alpha=0.1).mean(), # 指数加权
'vol_20': trend.rolling(20).std()
})
6.4 数据可视化与分析报告输出
Matplotlib 与 Seaborn 可无缝集成 Pandas 时间序列绘图。基础折线图可通过 .plot() 快速生成:
import matplotlib.pyplot as plt
import seaborn as sns
sns.set_style("whitegrid")
plt.figure(figsize=(12, 6))
data[['value', 'ma_7', 'ma_30']].plot(title="Time Series with Moving Averages")
plt.ylabel("Value")
plt.xlabel("Date")
plt.legend(['Raw', '7-day MA', '30-day MA'])
plt.show()
Seaborn 支持更高级的统计图表:
# 热力图:相关性矩阵
corr_matrix = analysis.corr()
sns.heatmap(corr_matrix, annot=True, cmap='coolwarm')
# 分布图
sns.histplot(analysis['original'], kde=True)
# 回归图
sns.regplot(x=analysis.index.astype(int), y='original', data=analysis)
多子图布局增强可读性:
fig, axes = plt.subplots(2, 2, figsize=(14, 10))
analysis['original'].plot(ax=axes[0,0], title="Original Series")
analysis['ma_7'].plot(ax=axes[0,1], title="7-Day Moving Average")
analysis['vol_20'].plot(ax=axes[1,0], title="20-Day Volatility")
sns.boxplot(x=analysis.index.month, y='original', data=analysis, ax=axes[1,1])
axes[1,1].set_title("Seasonal Box Plot")
plt.tight_layout()
plt.show()
通过 pandas.plotting 模块还可实现:
- lag_plot() :滞后图检测自相关
- autocorrelation_plot() :自相关系数可视化
- bootstrap_plot() :自助法分布估计
这些工具共同构成了从原始时间数据到洞察输出的完整分析闭环。
简介:《Python for Data Analysis》是数据科学领域的经典著作,由Wes McKinney撰写,系统讲解了使用Python进行高效数据处理、清洗、分析与可视化的全流程。本书重点介绍Pandas、NumPy、SciPy、Matplotlib和Seaborn等核心库的应用,涵盖数据导入导出、数据清洗、分组聚合、时间序列分析及基础统计方法等内容。全书结合大量实例,适合数据分析师、数据科学家和开发人员学习实践,帮助读者掌握Python在真实数据分析场景中的应用技巧,提升数据处理效率与洞察力。
更多推荐



所有评论(0)