22 KiB
Python 直方图绘制:NumPy、Matplotlib、Pandas 和 Seaborn
*立即观看**本教程有真实 Python 团队创建的相关视频课程。配合文字教程一起看,加深理解: Python 直方图绘制:NumPy、Matplotlib、Pandas & Seaborn
在本教程中,您将具备制作产品质量、演示就绪的 Python 直方图的能力,并拥有一系列选择和功能。
如果您对 Python 和 statistics 有入门中级知识,那么您可以将本文作为使用 Python 科学堆栈中的库(包括 NumPy、Matplotlib、 Pandas 和 Seaborn)在 Python 中构建和绘制直方图的一站式商店。
直方图是一个很好的工具,可以快速评估几乎所有观众都能直观理解的概率分布。Python 为构建和绘制直方图提供了一些不同的选项。大多数人通过直方图的图形表示来了解直方图,它类似于条形图:
这篇文章将引导你创建类似上面的情节以及更复杂的情节。以下是您将涉及的内容:
- 用纯 Python 构建直方图,不使用第三方库
- 使用 NumPy 构建直方图以汇总底层数据
- 使用 Matplotlib、Pandas 和 Seaborn 绘制结果直方图
**免费奖金:**时间短?点击这里获得一份免费的两页 Python 直方图备忘单,它总结了本教程中解释的技术。
纯 Python 的直方图
当您准备绘制直方图时,最简单的方法是不要考虑柱,而是报告每个值出现的次数(频率表)。Python 字典非常适合这项任务:
>>> # Need not be sorted, necessarily
>>> a = (0, 1, 1, 1, 2, 3, 7, 7, 23)
>>> def count_elements(seq) -> dict:
... """Tally elements from `seq`."""
... hist = {}
... for i in seq:
... hist[i] = hist.get(i, 0) + 1
... return hist
>>> counted = count_elements(a)
>>> counted
{0: 1, 1: 3, 2: 1, 3: 1, 7: 2, 23: 1}
count_elements()返回一个字典,将序列中的唯一元素作为键,将它们的频率(计数)作为值。在seq的循环中,hist[i] = hist.get(i, 0) + 1说,“对于序列中的每个元素,将它在hist中的对应值增加 1。”
事实上,这正是 Python 标准库中的collections.Counter类所做的,该类的子类化了一个 Python 字典并覆盖了它的.update()方法:
>>> from collections import Counter
>>> recounted = Counter(a)
>>> recounted
Counter({0: 1, 1: 3, 3: 1, 2: 1, 7: 2, 23: 1})
通过测试两者之间的相等性,您可以确认您的手工函数实际上做了与collections.Counter相同的事情:
>>> recounted.items() == counted.items()
True
技术细节:上面count_elements()的映射默认为一个更加高度优化的 C 函数,如果它可用的话。在 Python 函数count_elements()中,你可以做的一个微优化是在 for 循环之前声明get = hist.get。这将把一个方法绑定到一个变量,以便在循环中更快地调用。
作为理解更复杂的函数的第一步,从头构建简化的函数可能会有所帮助。让我们利用 Python 的输出格式进一步重新发明一个 ASCII 直方图:
def ascii_histogram(seq) -> None:
"""A horizontal frequency-table/histogram plot."""
counted = count_elements(seq)
for k in sorted(counted):
print('{0:5d} {1}'.format(k, '+' * counted[k]))
该函数创建一个排序频率图,其中计数表示为加号(+)的计数。在字典上调用 sorted() 会返回一个排序后的键列表,然后使用counted[k]访问每个键对应的值。要了解这一点,您可以使用 Python 的random模块创建一个稍大的数据集:
>>> # No NumPy ... yet
>>> import random
>>> random.seed(1)
>>> vals = [1, 3, 4, 6, 8, 9, 10]
>>> # Each number in `vals` will occur between 5 and 15 times.
>>> freq = (random.randint(5, 15) for _ in vals)
>>> data = []
>>> for f, v in zip(freq, vals):
... data.extend([v] * f)
>>> ascii_histogram(data)
1 +++++++
3 ++++++++++++++
4 ++++++
6 +++++++++
8 ++++++
9 ++++++++++++
10 ++++++++++++
在这里,您模拟从vals开始拨弦,频率由freq(一个发生器表达式)给出。产生的样本数据重复来自vals的每个值一定的次数,在 5 到 15 之间。
注意 : random.seed() 用于播种或初始化random使用的底层伪随机数发生器( PRNG )。这听起来可能有点矛盾,但这是一种让随机数据具有可重复性和确定性的方法。也就是说,如果你照原样复制这里的代码,你应该得到完全相同的直方图,因为在播种生成器之后第一次调用random.randint()将使用 Mersenne Twister 产生相同的“随机”数据。
从基础开始构建:以 NumPy 为单位的直方图计算
到目前为止,您一直在使用最好称为“频率表”的东西。但是从数学上来说,直方图是区间(区间)到频率的映射。更专业的说,可以用来近似基础变量的概率密度函数( PDF )。
从上面的“频率表”开始,真正的直方图首先“分类”值的范围,然后计算落入每个分类的值的数量。这就是 NumPy 的 histogram()函数所做的事情,它也是你稍后将在 Python 库中看到的其他函数的基础,比如 Matplotlib 和 Pandas。
考虑从拉普拉斯分布中抽取的一个浮点样本。该分布比正态分布具有更宽的尾部,并且具有两个描述性参数(位置和比例):
>>> import numpy as np
>>> # `numpy.random` uses its own PRNG.
>>> np.random.seed(444)
>>> np.set_printoptions(precision=3)
>>> d = np.random.laplace(loc=15, scale=3, size=500)
>>> d[:5]
array([18.406, 18.087, 16.004, 16.221, 7.358])
在这种情况下,您处理的是一个连续的分布,单独计算每个浮点数,直到小数点后无数位,并没有多大帮助。相反,您可以对数据进行分类或“分桶”,并对落入每个分类中的观察值进行计数。直方图是每个条柱内值的结果计数:
>>> hist, bin_edges = np.histogram(d)
>>> hist
array([ 1, 0, 3, 4, 4, 10, 13, 9, 2, 4])
>>> bin_edges
array([ 3.217, 5.199, 7.181, 9.163, 11.145, 13.127, 15.109, 17.091,
19.073, 21.055, 23.037])
这个结果可能不是直接直观的。 np.histogram() 默认使用 10 个大小相等的仓,并返回频率计数和相应仓边的元组。它们是边缘,在这种意义上,将会比直方图的成员多一个箱边缘:
>>> hist.size, bin_edges.size
(10, 11)
技术细节:除了最后一个(最右边的)箱子,其他箱子都是半开的。也就是说,除了最后一个 bin 之外的所有 bin 都是[包含,不包含],最后一个 bin 是[包含,不包含]。
NumPy 如何构造的一个非常简洁的分类如下:
>>> # The leftmost and rightmost bin edges
>>> first_edge, last_edge = a.min(), a.max()
>>> n_equal_bins = 10 # NumPy's default
>>> bin_edges = np.linspace(start=first_edge, stop=last_edge,
... num=n_equal_bins + 1, endpoint=True)
...
>>> bin_edges
array([ 0\. , 2.3, 4.6, 6.9, 9.2, 11.5, 13.8, 16.1, 18.4, 20.7, 23\. ])
上面的例子很有意义:在 23 的峰峰值范围内,10 个等间距的仓意味着宽度为 2.3 的区间。
从那里,该功能委托给 np.bincount() 或 np.searchsorted() 。bincount()本身可以用来有效地构建您在这里开始的“频率表”,区别在于包含了零出现的值:
>>> bcounts = np.bincount(a)
>>> hist, _ = np.histogram(a, range=(0, a.max()), bins=a.max() + 1)
>>> np.array_equal(hist, bcounts)
True
>>> # Reproducing `collections.Counter`
>>> dict(zip(np.unique(a), bcounts[bcounts.nonzero()]))
{0: 1, 1: 3, 2: 1, 3: 1, 7: 2, 23: 1}
注意 : hist这里实际上使用的是宽度为 1.0 的面元,而不是“离散”计数。因此,这只对计算整数有效,而不是像[3.9, 4.1, 4.15]这样的浮点数。
用 Matplotlib 和 Pandas 可视化直方图
现在你已经看到了如何用 Python 从头开始构建直方图,让我们看看其他的 Python 包如何为你完成这项工作。 Matplotlib 通过围绕 NumPy 的histogram()的通用包装器,提供开箱即用的可视化 Python 直方图的功能:
import matplotlib.pyplot as plt
# An "interface" to matplotlib.axes.Axes.hist() method
n, bins, patches = plt.hist(x=d, bins='auto', color='#0504aa',
alpha=0.7, rwidth=0.85)
plt.grid(axis='y', alpha=0.75)
plt.xlabel('Value')
plt.ylabel('Frequency')
plt.title('My Very Own Histogram')
plt.text(23, 45, r'$\mu=15, b=3$')
maxfreq = n.max()
# Set a clean upper y-axis limit.
plt.ylim(ymax=np.ceil(maxfreq / 10) * 10 if maxfreq % 10 else maxfreq + 10)
如前所述,直方图在 x 轴上使用其条边,在 y 轴上使用相应的频率。在上图中,传递bins='auto'在两个算法之间选择,以估计“理想”的箱数。在高层次上,该算法的目标是选择一个能够生成最忠实的数据表示的条柱宽度。关于这个主题的更多信息,可能会变得非常专业,请查看 Astropy 文档中的选择直方图仓。
在 Python 的科学堆栈中,熊猫的Series.histogram() 使用matplotlib.pyplot.hist() 绘制输入序列的 Matplotlib 直方图:
import pandas as pd
# Generate data on commute times.
size, scale = 1000, 10
commutes = pd.Series(np.random.gamma(scale, size=size) ** 1.5)
commutes.plot.hist(grid=True, bins=20, rwidth=0.9,
color='#607c8e')
plt.title('Commute Times for 1,000 Commuters')
plt.xlabel('Counts')
plt.ylabel('Commute Time')
plt.grid(axis='y', alpha=0.75)
pandas.DataFrame.histogram()类似,但为数据帧中的每列数据生成一个直方图。
绘制核密度估计值(KDE)
在本教程中,从统计学的角度来说,您一直在使用样本。无论数据是离散的还是连续的,它都被假定为来自一个总体,该总体具有仅由几个参数描述的真实、精确的分布。
核密度估计(KDE)是一种估计样本中随机变量的概率密度函数(PDF)的方法。KDE 是一种数据平滑的手段。
坚持使用熊猫库,你可以使用plot.kde()创建和覆盖密度图,它对Series和DataFrame对象都可用。但首先,让我们生成两个不同的数据样本进行比较:
>>> # Sample from two different normal distributions
>>> means = 10, 20
>>> stdevs = 4, 2
>>> dist = pd.DataFrame(
... np.random.normal(loc=means, scale=stdevs, size=(1000, 2)),
... columns=['a', 'b'])
>>> dist.agg(['min', 'max', 'mean', 'std']).round(decimals=2)
a b
min -1.57 12.46
max 25.32 26.44
mean 10.12 19.94
std 3.94 1.94
现在,要在相同的 Matplotlib 轴上绘制每个直方图:
fig, ax = plt.subplots()
dist.plot.kde(ax=ax, legend=False, title='Histogram: A vs. B')
dist.plot.hist(density=True, ax=ax)
ax.set_ylabel('Probability')
ax.grid(axis='y')
ax.set_facecolor('#d8dcd6')
这些方法利用了 SciPy 的 gaussian_kde() ,从而产生看起来更平滑的 PDF。
如果您仔细观察这个函数,您会发现对于 1000 个数据点的相对较小的样本,它是多么接近“真实”的 PDF。下面,你可以先用scipy.stats.norm()构建“解析”分布。这是一个类实例,封装了统计标准正态分布、其矩和描述函数。它的 PDF 是“精确的”,因为它被精确地定义为norm.pdf(x) = exp(-x**2/2) / sqrt(2*pi)。
在此基础上,您可以从该分布中随机抽取 1000 个数据点,然后尝试使用scipy.stats.gaussian_kde()返回 PDF 的估计值:
from scipy import stats
# An object representing the "frozen" analytical distribution
# Defaults to the standard normal distribution, N~(0, 1)
dist = stats.norm()
# Draw random samples from the population you built above.
# This is just a sample, so the mean and std. deviation should
# be close to (1, 0).
samp = dist.rvs(size=1000)
# `ppf()`: percent point function (inverse of cdf — percentiles).
x = np.linspace(start=stats.norm.ppf(0.01),
stop=stats.norm.ppf(0.99), num=250)
gkde = stats.gaussian_kde(dataset=samp)
# `gkde.evaluate()` estimates the PDF itself.
fig, ax = plt.subplots()
ax.plot(x, dist.pdf(x), linestyle='solid', c='red', lw=3,
alpha=0.8, label='Analytical (True) PDF')
ax.plot(x, gkde.evaluate(x), linestyle='dashed', c='black', lw=2,
label='PDF Estimated via KDE')
ax.legend(loc='best', frameon=False)
ax.set_title('Analytical vs. Estimated PDF')
ax.set_ylabel('Probability')
ax.text(-2., 0.35, r'$f(x) = \frac{\exp(-x^2/2)}{\sqrt{2*\pi}}$',
fontsize=12)
这是一个更大的代码块,所以让我们花点时间来了解几个关键行:
- SciPy 的
stats子包允许您创建表示分析分布的 Python 对象,您可以从中采样以创建实际数据。所以dist = stats.norm()代表一个正常的连续随机变量,你用dist.rvs()从中生成随机数。 - 为了评估分析 PDF 和高斯 KDE,您需要一个分位数数组
x(高于/低于平均值的标准偏差,表示正态分布)。stats.gaussian_kde()表示一个估计的 PDF,在这种情况下,您需要对一个数组进行评估,以产生视觉上有意义的东西。 - 最后一行包含一些 LaTex ,它与 Matplotlib 很好地集成在一起。
与 Seaborn 的别样选择
让我们再加入一个 Python 包。Seaborn 有一个displot()函数,可以一步绘制出单变量分布的直方图和 KDE。使用早期的 NumPy 数组d:
import seaborn as sns
sns.set_style('darkgrid')
sns.distplot(d)
上面的调用产生了一个 KDE。还可以选择为数据拟合特定的分布。这不同于 KDE,它由通用数据的参数估计和指定的分布名称组成:
sns.distplot(d, fit=stats.laplace, kde=False)
同样,请注意细微的差别。在第一种情况下,你估计一些未知的 PDF 在第二种情况下,你需要一个已知的分布,并根据经验数据找出最能描述它的参数。
熊猫里的其他工具
除了它的绘图工具,Pandas 还提供了一个方便的.value_counts()方法来计算 Pandas 的非空值的直方图Series:
>>> import pandas as pd
>>> data = np.random.choice(np.arange(10), size=10000,
... p=np.linspace(1, 11, 10) / 60)
>>> s = pd.Series(data)
>>> s.value_counts()
9 1831
8 1624
7 1423
6 1323
5 1089
4 888
3 770
2 535
1 347
0 170
dtype: int64
>>> s.value_counts(normalize=True).head()
9 0.1831
8 0.1624
7 0.1423
6 0.1323
5 0.1089
dtype: float64
在其他地方, pandas.cut() 是将值绑定到任意区间的便捷方式。假设您有一些关于个人年龄的数据,并希望明智地对它们进行分类:
>>> ages = pd.Series(
... [1, 1, 3, 5, 8, 10, 12, 15, 18, 18, 19, 20, 25, 30, 40, 51, 52])
>>> bins = (0, 10, 13, 18, 21, np.inf) # The edges
>>> labels = ('child', 'preteen', 'teen', 'military_age', 'adult')
>>> groups = pd.cut(ages, bins=bins, labels=labels)
>>> groups.value_counts()
child 6
adult 5
teen 3
military_age 2
preteen 1
dtype: int64
>>> pd.concat((ages, groups), axis=1).rename(columns={0: 'age', 1: 'group'})
age group
0 1 child
1 1 child
2 3 child
3 5 child
4 8 child
5 10 child
6 12 preteen
7 15 teen
8 18 teen
9 18 teen
10 19 military_age
11 20 military_age
12 25 adult
13 30 adult
14 40 adult
15 51 adult
16 52 adult
令人高兴的是,这两个操作最终都利用了 Cython 代码,这使它们在速度上具有竞争力,同时保持了灵活性。
好吧,那我应该用哪个?
至此,您已经看到了许多用于绘制 Python 直方图的函数和方法可供选择。他们如何比较?简而言之,没有“一刀切”以下是到目前为止您所涉及的函数和方法的回顾,所有这些都与用 Python 分解和表示分布有关:
| 你有/想 | 考虑使用 | 注意事项 |
|---|---|---|
| 包含在列表、元组或集合等数据结构中的简单整数数据,并且您希望在不导入任何第三方库的情况下创建 Python 直方图。 | Python 标准库中的 collections.Counter() 提供了一种从数据容器中获取频率计数的快速而简单的方法。 |
这是一个频率表,所以它不像“真正的”直方图那样使用宁滨的概念。 |
| 大量数据,并且您想要计算表示仓和相应频率的“数学”直方图。 | NumPy 的 np.histogram() 和 np.bincount() 对数值计算直方图值和相应的面元边缘很有用。 |
更多信息,请查看 np.digitize() 。 |
熊猫的Series或DataFrame对象中的表格数据。 |
熊猫法如Series.plot.hist()DataFrame.plot.hist()Series.value_counts()cut(),还有Series.plot.kde()DataFrame.plot.kde()。 |
查看熊猫可视化文档获取灵感。 |
| 从任何数据结构创建高度可定制、微调的图。 | pyplot.hist() 是一个广泛使用的直方图绘制函数,使用np.histogram(),是熊猫绘制函数的基础。 |
Matplotlib,尤其是它的面向对象框架,非常适合微调直方图的细节。这个界面可能需要一点时间来掌握,但最终可以让您非常精确地安排任何可视化。 |
| 预先封装的设计和集成。 | Seaborn 的 distplot() ,用于结合直方图和 KDE 图或绘制分布拟合图。 |
本质上是“包装器周围的包装器”,它在内部利用 Matplotlib 直方图,而 Matplotlib 直方图又利用 NumPy。 |
**免费奖金:**时间短?点击这里获得一份免费的两页 Python 直方图备忘单,它总结了本教程中解释的技术。
您也可以在真正的 Python 材料页面上找到这篇文章的代码片段,它们都在一个脚本中。
至此,祝你在野外创建直方图好运。希望上面的某个工具能满足你的需求。无论你做什么,只是不要用饼状图。
立即观看本教程有真实 Python 团队创建的相关视频课程。配合文字教程一起看,加深理解: Python 直方图绘制:NumPy、Matplotlib、Pandas & Seaborn**





