做者:xiaoyupython
微信公众号:Python数据科学算法
知乎:python数据分析师bash
直方图是一个能够快速展现数据几率分布的工具,直观易于理解,并深受数据爱好者的喜好。你们平时可能见到最多就是 matplotlib
,seaborn
等高级封装的库包,相似如下这样的绘图。微信
本篇博主将要总结一下使用Python绘制直方图的全部方法,大体可分为三大类(详细划分是五类,参照文末总结):数据结构
Numpy
来建立直方图总结数据matplotlib
,pandas
,seaborn
绘制直方图下面,咱们来逐一介绍每种方法的前因后果。dom
当准备用纯Python来绘制直方图的时候,最简单的想法就是将每一个值出现的次数以报告形式展现。这种状况下,使用 字典
来完成这个任务是很是合适的,咱们看看下面代码是如何实现的。函数
>>> 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()
返回了一个字典,字典里出现的键为目标列表里面的全部惟一数值,而值为全部数值出现的频率次数。hist[i] = hist.get(i, 0) + 1
实现了每一个数值次数的累积,每次加一。工具
实际上,这个功能能够用一个Python的标准库 collection.Counter
类来完成,它兼容Pyhont 字典并覆盖了字典的 .update()
方法。学习
>>> from collections import Counter
>>> recounted = Counter(a)
>>> recounted
Counter({0: 1, 1: 3, 3: 1, 2: 1, 7: 2, 23: 1})
复制代码
能够看到这个方法和前面咱们本身实现的方法结果是同样的,咱们也能够经过 collection.Counter
来检验两种方法获得的结果是否相等。ui
>>> recounted.items() == counted.items()
True
复制代码
咱们利用上面的函数从新再造一个轮子 ASCII_histogram
,并最终经过Python的输出格式format
来实现直方图的展现,代码以下:
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]
。
>>> import random
>>> random.seed(1)
>>> vals = [1, 3, 4, 6, 8, 9, 10]
>>> # `vals` 里面的数字将会出现5到15次
>>> 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
内的数值是不重复的,而且每一个数值出现的频数是由咱们本身定义的,在5和15
之间随机选择。而后运用咱们上面封装的函数,就获得了纯Python版本的直方图展现。
总结:纯python实现频数表(非标准直方图),可直接使用collection.Counter方法实现。
以上是使用纯Python来完成的简单直方图,可是从数学意义上来看,直方图是分箱到频数的一种映射,它能够用来估计变量的几率密度函数的。而上面纯Python实现版本只是单纯的频数统计,不是真正意义上的直方图。
所以,咱们从上面实现的简单直方图继续往下进行升级。一个真正的直方图首先应该是将变量分区域(箱)的,也就是分红不一样的区间范围,而后对每一个区间内的观测值数量进行计数。恰巧,Numpy
的直方图方法就能够作到这点,不只仅如此,它也是后面将要提到的matplotlib和pandas使用的基础。
举个例子,来看一组从拉普拉斯分布上提取出来的浮点型样本数据。这个分布比标准正态分布拥有更宽的尾部,并有两个描述参数(location和scale):
>>> import numpy as np
>>> 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])
复制代码
因为这是一个连续型的分布,对于每一个单独的浮点值(即全部的无数个小数位置)并不能作很好的标签(由于点实在太多了)。可是,你能够将数据作 分箱 处理,而后统计每一个箱内观察值的数量,这就是真正的直方图所要作的工做。
下面咱们看看是如何用Numpy来实现直方图频数统计的。
>>> 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)
复制代码
那问题来了,Numpy究竟是如何进行分箱的呢?只是经过简单的 np.histogram()
就能够完成了,但具体是如何实现的咱们仍然全然不知。下面让咱们来将 np.histogram()
的内部进行解剖,看看究竟是如何实现的(以最前面提到的a列表为例)。
>>> # 取a的最小值和最大值
>>> first_edge, last_edge = a.min(), a.max()
>>> n_equal_bins = 10 # NumPy得默认设置,10个分箱
>>> 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. ])
复制代码
解释一下:首先获取a列表的最小值和最大值,而后设置默认的分箱数量,最后使用Numpy的 linspace
方法进行数据段分割。分箱区间的结果也正好与实际吻合,0到23均等分为10份,23/10,那么每份宽度为2.3。
除了np.histogram以外,还存在其它两种能够达到一样功能的方法:np.bincount()
和 np.searchsorted()
,下面看看代码以及比较结果。
>>> 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}
复制代码
总结:经过Numpy实现直方图,可直接使用np.histogram()或者np.bincount()。
从上面的学习,咱们看到了如何使用Python的基础工具搭建一个直方图,下面咱们来看看如何使用更为强大的Python库包来完成直方图。Matplotlib
基于Numpy的histogram进行了多样化的封装并提供了更加完善的可视化功能。
import matplotlib.pyplot as plt
# matplotlib.axes.Axes.hist() 方法的接口
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()
# 设置y轴的上限
plt.ylim(ymax=np.ceil(maxfreq / 10) * 10 if maxfreq % 10 else maxfreq + 10)
复制代码
bins='auto'
自动在写好的两个算法中择优选择并最终算出最适合的分箱数。这里,算法的目的就是选择出一个合适的区间(箱)宽度,并生成一个最能表明数据的直方图来。
若是使用Python的科学计算工具实现,那么可使用Pandas的 Series.histogram()
,并经过 matplotlib.pyplot.hist()
来绘制输入Series的直方图,以下代码所示。
import pandas as pd
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()
的用法与Series是同样的,但生成的是对DataFrame数据中的每一列的直方图。
总结:经过pandas实现直方图,可以使用Seris.plot.hist(),DataFrame.plot.hist(),matplotlib实现直方图能够用matplotlib.pyplot.hist()。
KDE(Kernel density estimation)是核密度估计的意思,它用来估计随机变量的几率密度函数,能够将数据变得更平缓。
使用Pandas库的话,你可使用 plot.kde()
建立一个核密度的绘图,plot.kde()
对于 Series和DataFrame数据结构都适用。可是首先,咱们先生成两个不一样的数据样本做为比较(两个正太分布的样本):
>>> # 两个正太分布的样本
>>> 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轴上绘制每一个直方图以及对应的kde,使用pandas的plot.kde()
的好处就是:它会自动的将全部列的直方图和kde都显示出来,用起来很是方便,具体代码以下:
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')
复制代码
一个更高级可视化工具就是Seaborn
,它是在matplotlib的基础上进一步封装的强大工具。对于直方图而言,Seaborn有 distplot()
方法,能够将单变量分布的直方图和kde同时绘制出来,并且使用及其方便,下面是实现代码(以上面生成的d为例):
import seaborn as sns
sns.set_style('darkgrid')
sns.distplot(d)
复制代码
distplot
方法默认的会绘制kde,而且该方法提供了
fit
参数,能够根据数据的实际状况自行选择一个特殊的分布来对应。
sns.distplot(d, fit=stats.laplace, kde=False)
复制代码
注意这两个图微小的区别。第一种状况你是在估计一个未知的几率密度函数(PDF),而第二种状况是你是知道分布的,并想知道哪些参数能够更好的描述数据。
总结:经过seaborn实现直方图,可以使用seaborn.distplot(),seaborn也有单独的kde绘图seaborn.kde()。
除了绘图工具外,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) # 边界
>>> 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
代码来完成,在运行速度的效果上也是很是快的。
总结:其它实现直方图的方法,可以使用.value_counts()和pandas.cut()。
至此,咱们了解了不少种方法来实现一个直方图。可是它们各自有什么有缺点呢?该如何对它们进行选择呢?固然,一个方法解决全部问题是不存在的,咱们也须要根据实际状况而考虑如何选择,下面是对一些状况下使用方法的一个推荐,仅供参考。
参考:https://realpython.com/python-histograms/
关注微信公众号:Python数据科学,查看更多精彩内容。