7.2 大型数据集的绘图策略


文档摘要

7.2 大型数据集的绘图策略 大数据集画不动时按三级减负:一级抽样(保留散点形态)、二级聚合(直方图与二维密度天然可扩展)、三级换图层(hexbin、二维 kde 代替散点)。同时砍掉 bootstrap 置信区间的计算成本。 第 3.1 节讲过万级点的过绘,第 3.2 节提过 bootstrap 慢,本节把性能话题收拢成一个按数据量分级的决策体系。 先量级,再选策略 数据量 | 策略 | 典型手段 万级以内 | 直接画 | alpha 加小点径 十万级 | 一级抽样 | df.

7.2 大型数据集的绘图策略

大数据集画不动时按三级减负:一级抽样(保留散点形态)、二级聚合(直方图与二维密度天然可扩展)、三级换图层(hexbin、二维 kde 代替散点)。同时砍掉 bootstrap 置信区间的计算成本。

第 3.1 节讲过万级点的过绘,第 3.2 节提过 bootstrap 慢,本节把性能话题收拢成一个按数据量分级的决策体系。

先量级,再选策略

数据量 策略 典型手段
万级以内 直接画 alpha 加小点径
十万级 一级抽样 df.sample 保形态
百万级 二级聚合 histplot 二维、kde 网格
千万级以上 预聚合 groupby 后画摘要
import numpy as np import pandas as pd import seaborn as sns import matplotlib.pyplot as plt import time rng = np.random.default_rng(3) big = pd.DataFrame({ 'x': rng.normal(0, 1, 1_000_000), 'y': rng.normal(0, 1, 1_000_000) * 0.6 + rng.normal(0, 1, 1_000_000), }) # 基准:百万散点 t0 = time.perf_counter() ax = sns.scatterplot(data=big, x='x', y='y', s=4, alpha=0.05) plt.close() print(f'散点 100 万点:{time.perf_counter() - t0:.2f} 秒') # 输出示例:散点 100 万点:3.8 秒 —— 能画,但改一次参数等一次

一级:抽样保形态

# 分层抽样:按 x 分位数分层,每层等量抽,防尾部被随机抽稀 big['stratum'] = pd.qcut(big['x'], 10, labels=False) sampled = big.groupby('stratum', group_keys=False).apply( lambda d: d.sample(20_000, random_state=1)) print(len(sampled)) # 输出:200000 t0 = time.perf_counter() ax = sns.scatterplot(data=sampled, x='x', y='y', s=4, alpha=0.15) plt.close() print(f'分层抽样 20 万点:{time.perf_counter() - t0:.2f} 秒') # 输出示例:分层抽样 20 万点:0.5 秒 —— 快一个数量级, # 相关系数从 0.83 到 0.83,尾部覆盖完整

简单随机抽样在大偏态数据上会亏尾部:百万分之几的极端值可能一个都抽不到。分层抽样多花两行代码,买回尾部保真。

二级:聚合天然可扩展

t0 = time.perf_counter() ax = sns.histplot(data=big, x='x', y='y', bins=120, cbar=True) plt.close() print(f'二维直方图 100 万点:{time.perf_counter() - t0:.2f} 秒') # 输出示例:二维直方图 100 万点:0.3 秒 —— 比散点还快, # 因为计算只是分箱计数,与点数线性相关且无逐点渲染
# 二维 kde:网格化后计算,点数的影响被二次采样摊薄 ax = sns.kdeplot(data=big.sample(100_000), x='x', y='y', levels=8, fill=True) # 核密度在大样本上仍然贵,先抽十万再估是性价比最高的组合

三级:砍 bootstrap 的隐藏账单

flights_long = sns.load_dataset('flights').pipe( lambda d: d.loc[d.index.repeat(2000)].reset_index(drop=True)) # 模拟大样本 t0 = time.perf_counter() ax = sns.lineplot(data=flights_long, x='year', y='passengers') # 默认 bootstrap 1000 次 plt.close() print(f'默认 ci:{time.perf_counter() - t0:.2f} 秒') # 输出示例:默认 ci:6.4 秒 t0 = time.perf_counter() ax = sns.lineplot(data=flights_long, x='year', y='passengers', errorbar='sd') plt.close() print(f'errorbar sd:{time.perf_counter() - t0:.2f} 秒') # 输出示例:errorbar sd:0.1 秒 —— 巨大样本下置信区间窄到看不清, # bootstrap 的千次重采样纯属浪费;sd 或 pi 口径又快又诚实地展示离散

案例:千万行日志的探索流程

背景:一份用户行为日志(模拟一千万行),要快速回答"响应时长与并发数的关系",全量画图不可行。

操作

# 模拟数据(真实场景从数据库读) n = 10_000_000 log = pd.DataFrame({ 'conc': rng.integers(1, 200, n), # 并发数 'latency': np.abs(rng.normal(120, 40, n)) + np.repeat( np.linspace(0, 80, 200), n // 200), # 与并发正相关的延迟 }) # 第一步:预聚合——按并发分箱取分位数摘要 log['conc_bin'] = pd.cut(log['conc'], 20) summary = log.groupby('conc_bin')['latency'].quantile([0.25, 0.5, 0.75]).unstack() summary.columns = ['q25', 'median', 'q75'] summary = summary.reset_index() summary['mid'] = summary['conc_bin'].astype(str).str.extract(r'(\d+\.?\d*)').astype(float) print(summary.head(3)) # 输出:每 10 并发一档的三个分位点,一千万行压成 20 行 # 第二步:画摘要带 ax = plt.gca() ax.fill_between(summary['mid'], summary['q25'], summary['q75'], alpha=0.25, label='四分位距') ax.plot(summary['mid'], summary['median'], color='#b03a2e', label='中位数') ax.set_xlabel('并发数') ax.set_ylabel('响应时长(毫秒)') ax.legend()

结果解读:中位数曲线随并发线性爬升,四分位距几乎等宽——延迟增长是系统性的(容量问题)而非尾部恶化(个别慢查询)。这个结论在两万行的散点图上同样能猜到,但预聚合版三秒出图且可直接进报告。变式:追加 0.95 分位列画出"慢请求尾巴",区分容量问题与长尾问题。

⚠️ 常见坑:log.groupby(...).quantile() 在分箱列是 Interval 类型时索引难用,务必 reset_index 后把区间转成数值中点(如上例的 extract 流程),否则后续 x 轴映射会报类型错。

💡 关键直觉:大数据集的图几乎总是"摘要图"——与其说在画数据,不如说在画一个聚合统计量的置信度。想清楚要摘要什么,比想怎么画快更重要。

本节要点回顾

  • 先量级后策略:万直画、十万抽样、百万聚合、千万预聚合;
  • 分层抽样保尾部:qcut 分层比简单随机多两行代码,少一个坑;
  • 二维直方图是被低估的快枪手:分箱计数与渲染成本都与形态无关;
  • 大样本砍 bootstrap:误差带窄到不可见时换 sd 或 pi 口径;
  • 预聚合出报告图:分位数摘要带是千万行数据的标准交付形态。

作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U