Python数据分析实战:用Seaborn和Scipy绘制CDF图的5个实用技巧

Python数据分析实战:用Seaborn和Scipy绘制CDF图的5个实用技巧

在数据探索和结果汇报中,直方图和箱线图固然经典,但累积分布函数图(CDF)往往能以一种更直观、更具统计洞察力的方式,揭示数据背后的完整故事。它不只是一个单调递增的曲线,更是理解数据分布形态、比较不同群体、识别异常阈值和评估模型性能的利器。对于需要向非技术背景的决策者解释复杂数据分布,或者需要在多个模型输出间进行精细对比的数据分析师而言,掌握CDF图的进阶绘制技巧,意味着能从图表中挖掘出更深层次、更具说服力的信息。

这篇文章不是另一个简单的“如何画出一条CDF曲线”的教程。我们将直接切入实战,聚焦于Seaborn和Scipy这两个在Python数据分析生态中举足轻重的库,分享五个能立刻提升你图表专业性和分析深度的技巧。无论你是希望让报告图表更具美感,还是需要处理真实世界中不那么“干净”的数据,亦或是想通过CDF进行严谨的统计推断,下面的内容都将提供清晰的路径和可复现的代码。

1. 从混乱到清晰:数据预处理与CDF的稳健性构建

绘制CDF的第一步永远不是plt.plot(),而是审视你的数据。真实世界的数据集常常伴随着缺失值、极端异常值以及非数值型条目,这些都会让最基础的CDF计算函数“卡壳”或产生误导性结果。

1.1 处理缺失值与异常值

Seaborn的ecdfplot()在遇到NaN时会直接报错。一个健壮的工作流始于数据清洗。

import pandas as pd
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt

# 模拟一个包含缺失值和异常值的真实数据集
np.random.seed(42)
data = pd.DataFrame({
    'group_A': np.concatenate([np.random.normal(100, 15, 950), [np.nan]*30, [1000, -500]]), # 包含NaN和极端值
    'group_B': np.random.exponential(50, 1000)
})

print(f"原始数据形状: {data.shape}")
print(f"Group A 缺失值数量: {data['group_A'].isna().sum()}")
print(f"Group A 描述统计:\n{data['group_A'].describe()}")

# 技巧1:选择性清洗而非全局删除
# 对于CDF,我们通常更关心数据的“主体”分布,对极端异常值敏感
# 方法A:基于分位数过滤(适用于对称分布)
Q1 = data['group_A'].quantile(0.25)
Q3 = data['group_A'].quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR

data_clean_iqr = data[(data['group_A'] >= lower_bound) & (data['group_A'] <= upper_bound)].copy()
data_clean_iqr = data_clean_iqr.dropna(subset=['group_A']) # 移除该列的NaN

# 方法B:基于绝对中位差(MAD)过滤(对异常值更稳健)
from scipy.stats import median_abs_deviation
median_val = data['group_A'].median()
mad = median_abs_deviation(data['group_A'], nan_policy='omit')
# 通常使用3倍MAD作为阈值
data_clean_mad = data[np.abs(data['group_A'] - median_val) / mad <= 3].copy()
data_clean_mad = data_clean_mad.dropna(subset=['group_A'])

print(f"\nIQR法清洗后数据量: {len(data_clean_iqr)}")
print(f"MAD法清洗后数据量: {len(data_clean_mad)}")

注意:选择哪种清洗方法取决于你的分析目标。如果目标是研究包括尾部在内的完整分布(如金融风险中的极端损失),则可能需保留异常值,但需在图中明确标注。如果目标是了解主体数据的典型行为,则过滤是必要的。

1.2 数据转换与CDF

某些数据(如收入、网页访问时长)通常呈重尾分布。直接绘制CDF可能导致曲线在低值区域过于陡峭,在高值区域拉得过长,难以观察细节。此时,对数转换(或其它幂变换)能有效“拉伸”分布。

# 原始数据 - 指数分布,典型的重尾
data_raw = np.random.exponential(scale=100, size=1000)

# 对数据取对数
# 添加一个小常数避免log(0)
data_log = np.log1p(data_raw)  # log1p = log(1 + x)

fig, axes = plt.subplots(1, 2, figsize=(12, 4))

# 绘制原始数据CDF
sns.ecdfplot(data=data_raw, ax=axes[0])
axes[0].set_title('原始数据CDF (指数分布)')
axes[0].set_xlabel('原始值')
axes[0].grid(True, alpha=0.3)

# 绘制对数转换后数据CDF
sns.ecdfplot(data=data_log, ax=axes[1])
axes[1].set_title('对数转换后数据CDF')
axes[1].set_xlabel('log(1 + 值)')
axes[1].grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

对数转换后的CDF曲线形态更接近我们熟悉的S型,便于比较不同数据集在“尺度”上的差异,这在比较用户活跃度、城市规模等数据时特别有用。

2. 超越基础绘图:Seaborn ecdfplot的深度定制与美学提升

seaborn.ecdfplot() 是绘制经验CDF最快捷的工具,但它的默认输出可能过于“学术化”。要让图表在商业报告或文章中脱颖而出,需要进行深度定制。

2.1 多组数据对比与统计量标注

在A/B测试或不同群体对比中,我们常需要将多条CDF曲线放在一起。ecdfplothue参数是天然的多组对比工具,但如何清晰地传达关键统计量的差异?

# 生成模拟的A/B测试数据(例如,新旧版本页面停留时间)
np.random.seed(123)
n_samples = 500
group_a = np.random.gamma(shape=2, scale=30, size=n_samples)  # 版本A
group_b = np.random.gamma(shape=2.2, scale=28, size=n_samples) # 版本B (略有改进)

df_ab = pd.DataFrame({
    '停留时间(秒)': np.concatenate([group_a, group_b]),
    '版本': ['A'] * n_samples + ['B'] * n_samples
})

plt.figure(figsize=(10, 6))
ax = sns.ecdfplot(data=df_ab, x='停留时间(秒)', hue='版本',
                  palette={'A': '#2E86AB', 'B': '#A23B72'},  # 自定义颜色
                  linewidth=2.5,
                  stat='proportion',  # 纵轴显示比例而非计数
                  complementary=False)  # 绘制标准CDF,而非1-CDF

# 计算并标注中位数、第90百分位数等关键分位点
for version, color in [('A', '#2E86AB'), ('B', '#A23B72')]:
    data = df_ab[df_ab['版本'] == version]['停留时间(秒)'].values
    median = np.median(data)
    p90 = np.percentile(data, 90)

    # 在曲线上标注中位数
    # 找到最接近中位数的数据点索引(用于确定y值)
    sorted_data = np.sort(data)
    cdf_y = np.arange(1, len(sorted_data)+1) / len(sorted_data)
    idx_median = np.argmin(np.abs(sorted_data - median))
    y_median = cdf_y[idx_median]

    ax.plot(median, y_median, 'o', color=color, markersize=10, markeredgecolor='white', markeredgewidth=1.5)
    ax.vlines(x=median, ymin=0, ymax=y_median, color=color, linestyle='--', alpha=0.7, linewidth=1)
    ax.hlines(y=y_median, xmin=0, xmax=median, color=color, linestyle='--', alpha=0.7, linewidth=1)

    # 添加文本标注
    ax.text(median+2, y_median+0.02, f'中位数: {median:.1f}s', color=color, fontsize=10, va='bottom')
    ax.text(p90+2, 0.88, f'P90 ({version}): {p90:.1f}s', color=color, fontsize=9, alpha=0.8)

# 添加阴影区域表示差异(例如,在某个关键阈值以下的用户比例差异)
threshold = 60  # 假设我们关心60秒以下的用户
for version, color in [('A', '#2E86AB'), ('B', '#A23B72')]:
    data = df_ab[df_ab['版本'] == version]['停留时间(秒)'].values
    prop_below_threshold = np.mean(data <= threshold)
    ax.fill_betweenx(y=[0, prop_below_threshold], x1=0, x2=threshold, color=color, alpha=0.1)

ax.set_title('A/B测试:不同版本用户停留时间累积分布对比', fontsize=14, fontweight='bold', pad=20)
ax.set_ylabel('用户比例 (≤ X秒)', fontsize=12)
ax.set_xlabel('停留时间 (秒)', fontsiz
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值