在数据分析和统计建模的世界里,仅仅知道平均值和中位数往往是不够的。当我们想要深入理解数据的分布形态,或者回答诸如“有多少比例的数据低于某个特定值?”这类问题时,累积分布函数(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 图,看看它能揭示出什么被平均值掩盖的秘密。