做者: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數據科學,查看更多精彩內容。