Python 数据可视化进阶:如何使用 Matplotlib 计算并绘制累积分布函数(CDF)

在数据分析和统计建模的世界里,仅仅知道平均值和中位数往往是不够的。当我们想要深入理解数据的分布形态,或者回答诸如“有多少比例的数据低于某个特定值?”这类问题时,累积分布函数(CDF)就是我们要找的强力工具。

在这篇文章中,我们将深入探讨如何使用 Python 中的 Matplotlib 以及其他核心科学计算库来计算和绘制 CDF。我们将从最基础的概念出发,逐步通过实战代码示例,探索多种不同的实现方法——从原生 Python 逻辑到高级的统计库应用。无论你是进行数据探索性分析(EDA),还是为机器学习模型做准备,掌握这些技巧都将让你的数据可视化能力更上一层楼。

什么是累积分布函数(CDF)?

在开始编写代码之前,让我们先通过一个直观的例子来理解 CDF。想象一下,你刚刚拿到了全班期末考试的数学成绩。

  • 概率密度函数 (PDF) 或直方图会告诉你:“得 80 分左右的有多少人”。它关注的是某个特定值的频率。
  • 累积分布函数 (CDF) 则告诉你:“得分低于 80 分的学生占总人数的百分比是多少”。它关注的是累积的概率。

CDF 的核心定义是:对于一个随机变量 $X$,其 CDF 值 $F(x)$ 等于 $X$ 小于或等于 $x$ 的概率。在图表上,它通常表现为一条从 0(或最小值)开始,最终上升到 1(或 100%)的 S 形曲线。

方法一:使用 NumPy 进行基础计算与绘制

这是最“硬核”但也最透明的方法。不依赖复杂的第三方统计库,我们仅利用 NumPy 进行排序和数组操作,然后使用 Matplotlib 绘图。这种方法的核心逻辑是:先将数据排序,然后为每个数据点分配一个累积的概率值。

#### 核心逻辑

假设我们有 $N$ 个数据点。我们将它们从小到大排序。排序后的第 $i$ 个数据点,其对应的 CDF 值(经验概率)通常计算为 $i / N$ 或 $(i + 1) / N$。

#### 实战代码

让我们生成一些服从标准正态分布的随机数据,并手动计算其 CDF。

import numpy as np
import matplotlib.pyplot as plt

# 设置随机种子以保证结果可复述
np.random.seed(42)

# 1. 生成 500 个来自标准正态分布(均值=0,标准差=1)的随机数据点
data = np.random.randn(500)

# 2. 计算步骤
# 先对数据进行升序排序,这是计算经验 CDF 的前提
sorted_data = np.sort(data)

# 计算累积概率:生成从 1/500 到 500/500 的均匀间隔值
# len(sorted_data) 是数据总数,这里我们使用 (i+1)/N 的形式
# 这意味着第 1 个点对应 1/500,最后一个点对应 500/500 (即 1.0)
cumulative_probs = np.arange(1, len(sorted_data) + 1) / len(sorted_data)

# 3. 绘图步骤
plt.figure(figsize=(10, 6))
# 使用散点图绘制,蓝点代表实际观测到的数据点
plt.plot(sorted_data, cumulative_probs, marker=‘.‘, linestyle=‘none‘, color=‘blue‘, alpha=0.5)

plt.xlabel(‘Data Values (数据数值)‘, fontsize=12)
plt.ylabel(‘CDF (累积概率)‘, fontsize=12)
plt.title(‘Calculating CDF via Sorting and np.arange‘, fontsize=14)
plt.grid(True, linestyle=‘--‘, alpha=0.7)
plt.show()

#### 代码解析

  • 数据生成 (np.random.randn):我们创建了 500 个数据点。你可以将其替换为你自己的真实数据集,比如从 CSV 文件中读取的列。
  • 排序 (np.sort):CDF 的单调递增性质基于数据的排序。未排序的数据无法形成有效的分布曲线。
  • 概率计算 (INLINECODE4bc82631):这里有一个巧妙的设计。INLINECODEe8ebc20f 生成了 1 到 500 的整数,除以总数 500 后,我们就得到了每个点对应的百分比排名。
  • 可视化 (INLINECODE9f116472):使用点图而不是线图,是因为我们的数据是离散的。如果你喜欢平滑的曲线,可以将 INLINECODEe9357957 改为 ‘-‘,但这可能会在数据稀疏的区域产生误导性的线性连接。

方法二:使用 Statsmodels 的 ECDF 类

如果你希望代码更加简洁且符合统计学的标准定义,INLINECODE929be895 库提供了一个专门为此设计的类:INLINECODE99e4095e(Empirical Cumulative Distribution Function,经验累积分布函数)。这通常是专业数据科学家的首选。

#### 为什么选择 Statsmodels?

它封装了所有的排序和计算逻辑,返回一个可调用的函数对象。这意味着,一旦你计算了 ECDF,就可以像查询字典一样,输入任意值 $x$,立即得到其对应的累积概率。

import numpy as np
import matplotlib.pyplot as plt
from statsmodels.distributions.empirical_distribution import ECDF

np.random.seed(42)
data = np.random.randn(500)

# 1. 创建 ECDF 对象
# 这一步会自动处理排序和概率计算
ecdf_func = ECDF(data)

# 2. 你可以直接使用 ecdf_func 来查询特定值的概率
# 例如,查询数值 0 对应的累积概率(对于标准正态分布,应该接近 0.5)
print(f"小于等于 0 的概率: {ecdf_func(0)}") 

# 3. 绘图
plt.figure(figsize=(10, 6))
# 使用 step 函数绘制阶梯图,这是 CDF 的标准视觉表达
# ecdf_func.x 包含排序后的数据,ecdf_func.y 包含对应的累积概率
plt.step(ecdf_func.x, ecdf_func.y, where=‘post‘, color=‘green‘, linewidth=2)

plt.xlabel(‘Data Values (数据数值)‘, fontsize=12)
plt.ylabel(‘CDF (累积概率)‘, fontsize=12)
plt.title(‘CDF using Statsmodels ECDF Class‘, fontsize=14)
plt.grid(True, linestyle=‘--‘, alpha=0.7)

# 添加一条辅助线看中位数
plt.axhline(y=0.5, color=‘gray‘, linestyle=‘--‘, linewidth=1)
plt.text(min(data), 0.52, ‘Median (50%)‘, color=‘gray‘)
plt.show()

#### 关键点解析

  • 阶梯图 (INLINECODE46081a1c):CDF 理论上是离散跳跃的。INLINECODE26dd8585 参数表示只有在数据点之后,概率才会发生跳变,这在数学上是更准确的画法。
  • 对象化设计:INLINECODE5290ff80 现在是一个函数。你可以将一组全新的数据 INLINECODE0ba9d26b 传给它:ecdf_func(new_data),它会立即返回新数据在旧数据分布下的位置。这对于对比训练集和测试集的分布非常有用。

方法三:使用 SciPy 的 cumfreq(分箱法)

前面的两种方法是基于原始数据的“精确”计算。然而,当你拥有数百万个数据点时,绘制每一个点可能会导致图表变得非常缓慢且模糊。这时,分箱 技术就派上用场了。

scipy.stats.cumfreq 计算的是经过分箱处理后的累积频率。这类似于绘制直方图,但我们累加的是每一个柱子的高度。

import numpy as np
import matplotlib.pyplot as plt
from scipy.stats import cumfreq

np.random.seed(42)
data = np.random.randn(10000) # 增加数据量以体现分箱的优势

# 1. 计算累积频率
# numbins=25 意味着我们将数据范围切分为 25 个区间
res = cumfreq(data, numbins=25)

# 2. 提取绘图所需的数据
# res.cumcount 是每个 bin 的累积计数(非归一化)
# res.lowerlimit 是第一个 bin 的起始位置
# res.binsize 是每个 bin 的宽度

# 计算 X 轴坐标(bin 的边缘)
x = res.lowerlimit + np.linspace(0, res.binsize * res.cumcount.size, res.cumcount.size)

# 归一化:将累积计数转换为概率 (0-1)
cdf_vals = res.cumcount / res.cumcount[-1]

# 3. 绘图
plt.figure(figsize=(10, 6))
plt.plot(x, cdf_vals, color=‘purple‘, linewidth=2, marker=‘o‘)

plt.xlabel(‘Data Values (Bin Centers)‘, fontsize=12)
plt.ylabel(‘CDF (Normalized Cumulative Frequency)‘, fontsize=12)
plt.title(‘CDF via SciPy cumfreq (Binning Method)‘, fontsize=14)
plt.grid(True, linestyle=‘--‘, alpha=0.7)
plt.show()

#### 适用场景与局限

  • 优势:极大地减少了绘制点的数量,适合大数据集。它能提供更平滑的视觉概览。
  • 劣势:损失了精度。你无法看到数据中的微小抖动或异常值的具体位置,因为它们被“合并”到了 bin 中。

方法四:结合直方图与 CDF(PDF + CDF)

有时候,我们需要同时对比概率密度(数据分布的“高度”)和累积分布(数据的“覆盖范围”)。使用 Matplotlib 的 twinx() 功能,我们可以将这两个概念叠加在同一张图上。

这个方法通过计算直方图的概率质量,然后对其进行求和来得到 CDF。

import numpy as np
import matplotlib.pyplot as plt

np.random.seed(42)
data = np.random.randn(1000)

# 1. 计算直方图相关信息
# density=True 将计数归一化为概率密度(面积和为1)
counts, bin_edges = np.histogram(data, bins=30, density=True)

# 计算每个 bin 的宽度
bin_width = bin_edges[1] - bin_edges[0]

# 计算每个 bin 中的概率质量(近似值)
# PDF * bin_width = 该区间内的概率
prob_mass = counts * bin_width

# 计算累积概率
# np.cumsum 沿着轴计算元素的累加和
cdf_vals = np.cumsum(prob_mass)

# 为了让图形闭合,我们在开头补一个 0
cdf_vals_plot = np.insert(cdf_vals, 0, 0)

# 2. 双轴绘图
fig, ax1 = plt.subplots(figsize=(12, 7))

# 左侧 Y 轴:绘制 PDF(直方图)
color = ‘tab:red‘
ax1.set_xlabel(‘Data Values‘, fontsize=12)
ax1.set_ylabel(‘PDF (Probability Density)‘, color=color, fontsize=12)
# 绘制直方图
ax1.hist(data, bins=30, density=True, color=color, alpha=0.3, label=‘PDF‘)
ax1.tick_params(axis=‘y‘, labelcolor=color)

# 右侧 Y 轴:绘制 CDF(折线图)
ax2 = ax1.twinx()  # 创建一个共享 X 轴的 Y 轴
color = ‘tab:blue‘
ax2.set_ylabel(‘CDF (Cumulative Probability)‘, color=color, fontsize=12)
# 使用 bin_edges 和 cdf_vals_plot 绘制
ax2.plot(bin_edges, cdf_vals_plot, color=color, linewidth=2, marker=‘o‘, label=‘CDF‘)
ax2.tick_params(axis=‘y‘, labelcolor=color)

plt.title(‘Overlaying PDF and CDF from Histogram‘, fontsize=14)
fig.tight_layout()  # 防止标签重叠
plt.show()

#### 视觉解读

在这张图中,你可以清晰地看到两者的对应关系:直方图(红色)最高的地方,对应的 CDF 曲线(蓝色)上升得最陡峭。 这意味着在数据最集中的区域,累积概率的增长速度最快。

常见陷阱与最佳实践

在实际开发中,你可能会遇到以下问题,这里有一些解决建议:

  • 如何处理重复数据?

在 INLINECODEb1dea65a 方法中,重复的数据点会在排序后挨在一起。由于我们使用 INLINECODE79372514 分配不同的概率值,这意味着两个数值完全相同的数据,在图上会有略微不同的 CDF 值(一个是 $i/N$,一个是 $i+1/N$)。这是“经验” CDF 的标准做法,数学上是合理的。

  • 绘制平滑曲线(外推)

如果你看到的 CDF 曲线看起来参差不齐,想要一条平滑的曲线,这通常意味着你假设数据服从某种特定分布(如正态分布)。你需要先拟合参数,然后生成理论上的 X 和 Y。

    # 简单示例:拟合正态分布并绘制理论 CDF
    from scipy.stats import norm
    mu, std = norm.fit(data) # 拟合参数
    x_smooth = np.linspace(min(data), max(data), 100)
    y_smooth = norm.cdf(x_smooth, mu, std) # 计算理论值
    plt.plot(x_smooth, y_smooth, label=‘Theoretical CDF‘)
    
  • 性能优化

对于超过 100 万行的数据,直接排序和绘图会消耗大量内存。建议使用 INLINECODE823d31e7 配合较大的 INLINECODE01c52f13 参数(如 Method 3 所示),先对数据进行降维处理,再绘制 CDF。

结语

在这篇文章中,我们并没有局限于某一种单一的方法,而是探索了四种不同层级的实现方式:从最基础的 NumPy 逻辑实现,到利用 Statsmodels 和 SciPy 的强大统计功能,再到直观的直方图叠加分析。

  • 如果你需要快速、无依赖的解决方案,Method 1 是你的不二之选。
  • 如果你正在做严谨的统计分析Method 2 (ECDF) 能提供最标准的工具。
  • 如果你面对的是海量数据Method 3 (cumfreq) 能在保持精度的同时优化性能。

希望这些技巧能帮助你更好地展示和分析数据!现在,当你拿到一组新的数据时,不妨试试画一张 CDF 图,看看它能揭示出什么被平均值掩盖的秘密。

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。如需转载,请注明文章出处豆丁博客和来源网址。https://shluqu.cn/43922.html
点赞
0.00 平均评分 (0% 分数) - 0