本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:《Python for Data Analysis》是数据科学领域的经典著作,由Wes McKinney撰写,系统讲解了使用Python进行高效数据处理、清洗、分析与可视化的全流程。本书重点介绍Pandas、NumPy、SciPy、Matplotlib和Seaborn等核心库的应用,涵盖数据导入导出、数据清洗、分组聚合、时间序列分析及基础统计方法等内容。全书结合大量实例,适合数据分析师、数据科学家和开发人员学习实践,帮助读者掌握Python在真实数据分析场景中的应用技巧,提升数据处理效率与洞察力。
Python for Data Analysis-带书签目录超清文字版英文版

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 是一种三步式的数据处理策略:

  1. Split(拆分) :根据指定的分组键(key),将原始数据集划分为若干互不重叠的子集;
  2. Apply(应用) :对每个子集独立应用某个函数(如求均值、计数、拟合模型等);
  3. 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() :自助法分布估计

这些工具共同构成了从原始时间数据到洞察输出的完整分析闭环。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:《Python for Data Analysis》是数据科学领域的经典著作,由Wes McKinney撰写,系统讲解了使用Python进行高效数据处理、清洗、分析与可视化的全流程。本书重点介绍Pandas、NumPy、SciPy、Matplotlib和Seaborn等核心库的应用,涵盖数据导入导出、数据清洗、分组聚合、时间序列分析及基础统计方法等内容。全书结合大量实例,适合数据分析师、数据科学家和开发人员学习实践,帮助读者掌握Python在真实数据分析场景中的应用技巧,提升数据处理效率与洞察力。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐