geekdoc-python-zh/docs/realpython/python-histograms.md

22 KiB
Raw Blame History

Python 直方图绘制:NumPy、Matplotlib、Pandas 和 Seaborn

原文:https://realpython.com/python-histograms/

*立即观看**本教程有真实 Python 团队创建的相关视频课程。配合文字教程一起看,加深理解: Python 直方图绘制:NumPy、Matplotlib、Pandas & Seaborn

在本教程中,您将具备制作产品质量、演示就绪的 Python 直方图的能力,并拥有一系列选择和功能。

如果您对 Python 和 statistics 有入门中级知识,那么您可以将本文作为使用 Python 科学堆栈中的库(包括 NumPy、Matplotlib、 Pandas 和 Seaborn)在 Python 中构建和绘制直方图的一站式商店。

直方图是一个很好的工具可以快速评估几乎所有观众都能直观理解的概率分布。Python 为构建和绘制直方图提供了一些不同的选项。大多数人通过直方图的图形表示来了解直方图,它类似于条形图:

Histogram of commute times for 1000 commuters

这篇文章将引导你创建类似上面的情节以及更复杂的情节。以下是您将涉及的内容:

  • 用纯 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 产生相同的“随机”数据。

Remove ads

从基础开始构建:以 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)

Histogram

如前所述,直方图在 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)

Histogram of commute times for 1000 commuters

pandas.DataFrame.histogram()类似,但为数据帧中的每列数据生成一个直方图。

Remove ads

绘制核密度估计值(KDE)

在本教程中,从统计学的角度来说,您一直在使用样本。无论数据是离散的还是连续的,它都被假定为来自一个总体,该总体具有仅由几个参数描述的真实、精确的分布。

核密度估计(KDE)是一种估计样本中随机变量的概率密度函数(PDF)的方法。KDE 是一种数据平滑的手段。

坚持使用熊猫库,你可以使用plot.kde()创建和覆盖密度图,它对SeriesDataFrame对象都可用。但首先,让我们生成两个不同的数据样本进行比较:

>>> # 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')

Histogram

这些方法利用了 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)

Chart

这是一个更大的代码块,所以让我们花点时间来了解几个关键行:

  • 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)

Seaborn's distplot

上面的调用产生了一个 KDE。还可以选择为数据拟合特定的分布。这不同于 KDE它由通用数据的参数估计和指定的分布名称组成:

sns.distplot(d, fit=stats.laplace, kde=False)

Histogram with fitted laplace distribution

同样,请注意细微的差别。在第一种情况下,你估计一些未知的 PDF 在第二种情况下,你需要一个已知的分布,并根据经验数据找出最能描述它的参数。

Remove ads

熊猫里的其他工具

除了它的绘图工具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()
熊猫的SeriesDataFrame对象中的表格数据。 熊猫法如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**