资讯专栏INFORMATION COLUMN

5种方法教你用Python玩转histogram直方图

simpleapples / 715人阅读

摘要:使用实现以上是使用纯来完成的简单直方图,但是从数学意义上来看,直方图是分箱到频数的一种映射,它可以用来估计变量的概率密度函数的。第一种情况你是在估计一个未知的概率密度函数,而第二种情况是你是知道分布的,并想知道哪些参数可以更好的描述数据。

作者:xiaoyu

微信公众号:Python数据科学

知乎:python数据分析师


直方图是一个可以快速展示数据概率分布的工具,直观易于理解,并深受数据爱好者的喜爱。大家平时可能见到最多就是 matplotlibseaborn 等高级封装的库包,类似以下这样的绘图。

本篇博主将要总结一下使用Python绘制直方图的所有方法,大致可分为三大类(详细划分是五类,参照文末总结):

纯Python实现直方图,不使用任何第三方库

使用Numpy来创建直方图总结数据

使用matplotlibpandasseaborn绘制直方图

下面,我们来逐一介绍每种方法的来龙去脉。

纯Python实现histogram

当准备用纯Python来绘制直方图的时候,最简单的想法就是将每个值出现的次数以报告形式展示。这种情况下,使用 字典 来完成这个任务是非常合适的,我们看看下面代码是如何实现的。

</>复制代码

  1. >>> a = (0, 1, 1, 1, 2, 3, 7, 7, 23)
  2. >>> def count_elements(seq) -> dict:
  3. ... """Tally elements from `seq`."""
  4. ... hist = {}
  5. ... for i in seq:
  6. ... hist[i] = hist.get(i, 0) + 1
  7. ... return hist
  8. >>> counted = count_elements(a)
  9. >>> counted
  10. {0: 1, 1: 3, 2: 1, 3: 1, 7: 2, 23: 1}

我们看到,count_elements() 返回了一个字典,字典里出现的键为目标列表里面的所有唯一数值,而值为所有数值出现的频率次数。hist[i] = hist.get(i, 0) + 1 实现了每个数值次数的累积,每次加一。

实际上,这个功能可以用一个Python的标准库 collection.Counter 类来完成,它兼容Pyhont 字典并覆盖了字典的 .update() 方法。

</>复制代码

  1. >>> from collections import Counter
  2. >>> recounted = Counter(a)
  3. >>> recounted
  4. Counter({0: 1, 1: 3, 3: 1, 2: 1, 7: 2, 23: 1})

可以看到这个方法和前面我们自己实现的方法结果是一样的,我们也可以通过 collection.Counter 来检验两种方法得到的结果是否相等。

</>复制代码

  1. >>> recounted.items() == counted.items()
  2. True

我们利用上面的函数重新再造一个轮子 ASCII_histogram,并最终通过Python的输出格式format来实现直方图的展示,代码如下:

</>复制代码

  1. def ascii_histogram(seq) -> None:
  2. """A horizontal frequency-table/histogram plot."""
  3. counted = count_elements(seq)
  4. for k in sorted(counted):
  5. print("{0:5d} {1}".format(k, "+" * counted[k]))

这个函数按照数值大小顺序进行绘图,数值出现次数用 (+) 符号表示。在字典上调用 sorted() 将会返回一个按键顺序排列的列表,然后就可以获取相应的次数 counted[k]

</>复制代码

  1. >>> import random
  2. >>> random.seed(1)
  3. >>> vals = [1, 3, 4, 6, 8, 9, 10]
  4. >>> # `vals` 里面的数字将会出现5到15次
  5. >>> freq = (random.randint(5, 15) for _ in vals)
  6. >>> data = []
  7. >>> for f, v in zip(freq, vals):
  8. ... data.extend([v] * f)
  9. >>> ascii_histogram(data)
  10. 1 +++++++
  11. 3 ++++++++++++++
  12. 4 ++++++
  13. 6 +++++++++
  14. 8 ++++++
  15. 9 ++++++++++++
  16. 10 ++++++++++++

这个代码中,vals内的数值是不重复的,并且每个数值出现的频数是由我们自己定义的,在5和15之间随机选择。然后运用我们上面封装的函数,就得到了纯Python版本的直方图展示。

总结:纯python实现频数表(非标准直方图),可直接使用collection.Counter方法实现。

使用Numpy实现histogram

以上是使用纯Python来完成的简单直方图,但是从数学意义上来看,直方图是分箱到频数的一种映射,它可以用来估计变量的概率密度函数的。而上面纯Python实现版本只是单纯的频数统计,不是真正意义上的直方图。

因此,我们从上面实现的简单直方图继续往下进行升级。一个真正的直方图首先应该是将变量分区域(箱)的,也就是分成不同的区间范围,然后对每个区间内的观测值数量进行计数。恰巧,Numpy的直方图方法就可以做到这点,不仅仅如此,它也是后面将要提到的matplotlib和pandas使用的基础。

举个例子,来看一组从拉普拉斯分布上提取出来的浮点型样本数据。这个分布比标准正态分布拥有更宽的尾部,并有两个描述参数(location和scale):

</>复制代码

  1. >>> import numpy as np
  2. >>> np.random.seed(444)
  3. >>> np.set_printoptions(precision=3)
  4. >>> d = np.random.laplace(loc=15, scale=3, size=500)
  5. >>> d[:5]
  6. array([18.406, 18.087, 16.004, 16.221, 7.358])

由于这是一个连续型的分布,对于每个多带带的浮点值(即所有的无数个小数位置)并不能做很好的标签(因为点实在太多了)。但是,你可以将数据做 分箱 处理,然后统计每个箱内观察值的数量,这就是真正的直方图所要做的工作。

下面我们看看是如何用Numpy来实现直方图频数统计的。

</>复制代码

  1. >>> hist, bin_edges = np.histogram(d)
  2. >>> hist
  3. array([ 1, 0, 3, 4, 4, 10, 13, 9, 2, 4])
  4. >>> bin_edges
  5. array([ 3.217, 5.199, 7.181, 9.163, 11.145, 13.127, 15.109, 17.091,
  6. 19.073, 21.055, 23.037])

这个结果可能不是很直观。来说一下,np.histogram() 默认地使用10个相同大小的区间(箱),然后返回一个元组(频数,分箱的边界),如上所示。要注意的是:这个边界的数量是要比分箱数多一个的,可以简单通过下面代码证实。

</>复制代码

  1. >>> hist.size, bin_edges.size
  2. (10, 11)

那问题来了,Numpy到底是如何进行分箱的呢?只是通过简单的 np.histogram() 就可以完成了,但具体是如何实现的我们仍然全然不知。下面让我们来将 np.histogram() 的内部进行解剖,看看到底是如何实现的(以最前面提到的a列表为例)。

</>复制代码

  1. >>> # 取a的最小值和最大值
  2. >>> first_edge, last_edge = a.min(), a.max()
  3. >>> n_equal_bins = 10 # NumPy得默认设置,10个分箱
  4. >>> bin_edges = np.linspace(start=first_edge, stop=last_edge,
  5. ... num=n_equal_bins + 1, endpoint=True)
  6. ...
  7. >>> bin_edges
  8. array([ 0. , 2.3, 4.6, 6.9, 9.2, 11.5, 13.8, 16.1, 18.4, 20.7, 23. ])

解释一下:首先获取a列表的最小值和最大值,然后设置默认的分箱数量,最后使用Numpy的 linspace 方法进行数据段分割。分箱区间的结果也正好与实际吻合,0到23均等分为10份,23/10,那么每份宽度为2.3。

除了np.histogram之外,还存在其它两种可以达到同样功能的方法:np.bincount()np.searchsorted(),下面看看代码以及比较结果。

</>复制代码

  1. >>> bcounts = np.bincount(a)
  2. >>> hist, _ = np.histogram(a, range=(0, a.max()), bins=a.max() + 1)
  3. >>> np.array_equal(hist, bcounts)
  4. True
  5. >>> # Reproducing `collections.Counter`
  6. >>> dict(zip(np.unique(a), bcounts[bcounts.nonzero()]))
  7. {0: 1, 1: 3, 2: 1, 3: 1, 7: 2, 23: 1}

总结:通过Numpy实现直方图,可直接使用np.histogram()或者np.bincount()。

使用Matplotlib和Pandas可视化Histogram

从上面的学习,我们看到了如何使用Python的基础工具搭建一个直方图,下面我们来看看如何使用更为强大的Python库包来完成直方图。Matplotlib基于Numpy的histogram进行了多样化的封装并提供了更加完善的可视化功能。

</>复制代码

  1. import matplotlib.pyplot as plt
  2. # matplotlib.axes.Axes.hist() 方法的接口
  3. n, bins, patches = plt.hist(x=d, bins="auto", color="#0504aa",
  4. alpha=0.7, rwidth=0.85)
  5. plt.grid(axis="y", alpha=0.75)
  6. plt.xlabel("Value")
  7. plt.ylabel("Frequency")
  8. plt.title("My Very Own Histogram")
  9. plt.text(23, 45, r"$mu=15, b=3$")
  10. maxfreq = n.max()
  11. # 设置y轴的上限
  12. plt.ylim(ymax=np.ceil(maxfreq / 10) * 10 if maxfreq % 10 else maxfreq + 10)


之前我们的做法是,在x轴上定义了分箱边界,y轴是相对应的频数,不难发现我们都是手动定义了分箱的数目。但是在以上的高级方法中,我们可以通过设置 bins="auto" 自动在写好的两个算法中择优选择并最终算出最适合的分箱数。这里,算法的目的就是选择出一个合适的区间(箱)宽度,并生成一个最能代表数据的直方图来。

如果使用Python的科学计算工具实现,那么可以使用Pandas的 Series.histogram() ,并通过 matplotlib.pyplot.hist() 来绘制输入Series的直方图,如下代码所示。

</>复制代码

  1. import pandas as pd
  2. size, scale = 1000, 10
  3. commutes = pd.Series(np.random.gamma(scale, size=size) ** 1.5)
  4. commutes.plot.hist(grid=True, bins=20, rwidth=0.9,
  5. color="#607c8e")
  6. plt.title("Commute Times for 1,000 Commuters")
  7. plt.xlabel("Counts")
  8. plt.ylabel("Commute Time")
  9. plt.grid(axis="y", alpha=0.75)


pandas.DataFrame.histogram() 的用法与Series是一样的,但生成的是对DataFrame数据中的每一列的直方图。

总结:通过pandas实现直方图,可使用Seris.plot.hist(),DataFrame.plot.hist(),matplotlib实现直方图可以用matplotlib.pyplot.hist()。

绘制核密度估计(KDE)

KDE(Kernel density estimation)是核密度估计的意思,它用来估计随机变量的概率密度函数,可以将数据变得更平缓。

使用Pandas库的话,你可以使用 plot.kde() 创建一个核密度的绘图,plot.kde() 对于 Series和DataFrame数据结构都适用。但是首先,我们先生成两个不同的数据样本作为比较(两个正太分布的样本):

</>复制代码

  1. >>> # 两个正太分布的样本
  2. >>> means = 10, 20
  3. >>> stdevs = 4, 2
  4. >>> dist = pd.DataFrame(
  5. ... np.random.normal(loc=means, scale=stdevs, size=(1000, 2)),
  6. ... columns=["a", "b"])
  7. >>> dist.agg(["min", "max", "mean", "std"]).round(decimals=2)
  8. a b
  9. min -1.57 12.46
  10. max 25.32 26.44
  11. mean 10.12 19.94
  12. std 3.94 1.94

以上看到,我们生成了两组正态分布样本,并且通过一些描述性统计参数对两组数据进行了简单的对比。现在,我们可以在同一个Matplotlib轴上绘制每个直方图以及对应的kde,使用pandas的plot.kde()的好处就是:它会自动的将所有列的直方图和kde都显示出来,用起来非常方便,具体代码如下:

</>复制代码

  1. fig, ax = plt.subplots()
  2. dist.plot.kde(ax=ax, legend=False, title="Histogram: A vs. B")
  3. dist.plot.hist(density=True, ax=ax)
  4. ax.set_ylabel("Probability")
  5. ax.grid(axis="y")
  6. ax.set_facecolor("#d8dcd6")


总结:通过pandas实现kde图,可使用Seris.plot.kde(),DataFrame.plot.kde()。

使用Seaborn的完美替代

一个更高级可视化工具就是Seaborn,它是在matplotlib的基础上进一步封装的强大工具。对于直方图而言,Seaborn有 distplot() 方法,可以将单变量分布的直方图和kde同时绘制出来,而且使用及其方便,下面是实现代码(以上面生成的d为例):

</>复制代码

  1. import seaborn as sns
  2. sns.set_style("darkgrid")
  3. sns.distplot(d)


distplot方法默认的会绘制kde,并且该方法提供了 fit 参数,可以根据数据的实际情况自行选择一个特殊的分布来对应。

</>复制代码

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

</>复制代码

  1. 注意这两个图微小的区别。第一种情况你是在估计一个未知的概率密度函数(PDF),而第二种情况是你是知道分布的,并想知道哪些参数可以更好的描述数据。

总结:通过seaborn实现直方图,可使用seaborn.distplot(),seaborn也有多带带的kde绘图seaborn.kde()。

在Pandas中的其它工具

除了绘图工具外,pandas也提供了一个方便的.value_counts() 方法,用来计算一个非空值的直方图,并将之转变成一个pandas的series结构,示例如下:

</>复制代码

  1. >>> import pandas as pd
  2. >>> data = np.random.choice(np.arange(10), size=10000,
  3. ... p=np.linspace(1, 11, 10) / 60)
  4. >>> s = pd.Series(data)
  5. >>> s.value_counts()
  6. 9 1831
  7. 8 1624
  8. 7 1423
  9. 6 1323
  10. 5 1089
  11. 4 888
  12. 3 770
  13. 2 535
  14. 1 347
  15. 0 170
  16. dtype: int64
  17. >>> s.value_counts(normalize=True).head()
  18. 9 0.1831
  19. 8 0.1624
  20. 7 0.1423
  21. 6 0.1323
  22. 5 0.1089
  23. dtype: float64

此外,pandas.cut() 也同样是一个方便的方法,用来将数据进行强制的分箱。比如说,我们有一些人的年龄数据,并想把这些数据按年龄段进行分类,示例如下:

</>复制代码

  1. >>> ages = pd.Series(
  2. ... [1, 1, 3, 5, 8, 10, 12, 15, 18, 18, 19, 20, 25, 30, 40, 51, 52])
  3. >>> bins = (0, 10, 13, 18, 21, np.inf) # 边界
  4. >>> labels = ("child", "preteen", "teen", "military_age", "adult")
  5. >>> groups = pd.cut(ages, bins=bins, labels=labels)
  6. >>> groups.value_counts()
  7. child 6
  8. adult 5
  9. teen 3
  10. military_age 2
  11. preteen 1
  12. dtype: int64
  13. >>> pd.concat((ages, groups), axis=1).rename(columns={0: "age", 1: "group"})
  14. age group
  15. 0 1 child
  16. 1 1 child
  17. 2 3 child
  18. 3 5 child
  19. 4 8 child
  20. 5 10 child
  21. 6 12 preteen
  22. 7 15 teen
  23. 8 18 teen
  24. 9 18 teen
  25. 10 19 military_age
  26. 11 20 military_age
  27. 12 25 adult
  28. 13 30 adult
  29. 14 40 adult
  30. 15 51 adult
  31. 16 52 adult

除了使用方便外,更加好的是这些操作最后都会使用 Cython 代码来完成,在运行速度的效果上也是非常快的。

总结:其它实现直方图的方法,可使用.value_counts()和pandas.cut()。

该使用哪个方法?

至此,我们了解了很多种方法来实现一个直方图。但是它们各自有什么有缺点呢?该如何对它们进行选择呢?当然,一个方法解决所有问题是不存在的,我们也需要根据实际情况而考虑如何选择,下面是对一些情况下使用方法的一个推荐,仅供参考。

</>复制代码

  1. 参考:https://realpython.com/python...

关注微信公众号:Python数据科学,查看更多精彩内容。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/42006.html

相关文章

  • ☀️苏州程序大白一文从基础手把手教你Python数据可视化大佬☀️《❤️记得收藏❤️》

    ☀️苏州程序大白一文从基础手把手教你Python数据可视化大佬☀️《❤️记得收藏❤️》 目录 ?️‍?开讲啦!!!!?️‍?苏州程序大白?️‍??博主介绍前言数据关系可视化散点图 Scatter plots折线图强调连续性 Emphasizing continuity with line plots同时显示多了图表 数据种类的可视化 Plotting with categorical da...

    Drinkey 评论0 收藏0
  • opencv python 2D方图

    Histograms - 3 : 2D Histograms 我们已经计算并绘制了一维直方图,因为我们只考虑一个特征,即像素的灰度强度值.但在二维直方图中,需要考虑两个特征,通常,它用于查找颜色直方图,其中两个要素是每个像素的色调和饱和度值. OpenCV中的2D直方图 使用函数cv.calcHist(), 对于颜色直方图,我们需要将图像从BGR转换为HSV。 (请记住,对于1D直方图,我们从B...

    BlackFlagBin 评论0 收藏0
  • Programming Computer Vision with Python (学习笔记二)

    摘要:首先介绍跟图像处理显示有关两个库和,然后介绍增强图像对比度的实现原理。直方图均衡化就是为了达到这个目的,均衡化后的图像,像素落在每个灰度级上的个数是相等的。 首先介绍跟图像处理、显示有关两个库:NumPy和Matplotlib,然后介绍增强图像对比度的实现原理。 NumPy NumPy是Python用于科学计算的基础库,提供了一些很有用的概念,如:N维数组对象,可用于表示向量、矩阵、图...

    Berwin 评论0 收藏0

发表评论

0条评论

simpleapples

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<