【Python】【Pandas】数値データの頻度(割合)をヒストグラムで表示する

■ やりたいこと

具体的には、

100個のデータがあり、
各階級の値が「50, 20, 10, 10, 5, 5」であったとき、
単にこの数値を描画するのではなく、
「50/100, 20/100, 10/100, 10/100, 5/100, 5/100」
を描画したい。

プロットライブラリはseabornを使う。

■ まずseaborn.displotしよう

「seaborn ヒストグラム」で検索すると、まずこのメソッドが出る。
早速使ってみよう。

import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
sns.set()

# 適当に配列作る
l = np.random.randint(1, 100, (1, 100)).tolist()[0]

# distplot をデフォルトで実行
sns.distplot(l)
plt.show()

デフォルト状態だとこんな感じで描画される。

f:id:massox:20200618125913p:plain

ヒストグラムと一緒に描画された折れ線グラフは何?

seaborn.pydata.org

この折れ線はKDEという。
KDEって何?:Kernel density estimation - Wikipedia

要は、あるデータが与えられたときにそいつの確率密度関数をノンパラメトリックに推定する手法ということ。
ノンパラメトリックとは、母数に依らないって意味。
母数とは、パラメータ。例えば、正規分布の母数は平均μと分散σ2
※サンプルサイズを母数と誤って理解しているケースが多いので注意したい
つまり、描画されるのは確率密度関数である。
これを任意の定義域で定積分することで確率になる。 -∞ ~ +∞ の範囲で積分すると1になる。

つまり、自分の欲しいグラフではないということ。
欲しいグラフはただの頻度(割合表示)なので。

■ 一旦、戻る。何をプロットしたいんだっけ?

シンプルに頻度データを割合にしたいだけ。
データ列⇒頻度データ(カウント)
⇒各頻度をデータ総数で割る⇒頻度データ(割合)

という流れを自分で作ることにする。
もしかしたら、seabornを調査したら楽に描画できる方法があるかもしれないが、
一旦やめておこう。

調べると、
pandas.DataFrameのvalue_countsを使えばできそうということが分かった。

Pandasでヒストグラムの作成や頻度を出力する方法 - DeepAge


単純にvalue_countsを使うだけだと、ユニークデータのカウントになってしまうので、
引数をちょっと調整する必要がありそう。
ということで、ドキュメントを見る。

pandas.Series.value_counts — pandas 1.0.4 documentation

以下の引数を調整すれば良さそう。
normalize を True に
bins を グルーピングしたい数に
設定してデータを作り、プロットすれば良さそう。

以下がそのスクリプト

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
sns.set()

# 適当に配列作る
l = np.random.randint(1, 100, (1, 100)).tolist()[0]

# 頻度データ(割合)を作る
sr = pd.Series(l)
vc_sr = sr.value_counts(normalize=True, bins=15)

# - ラベルをきれいに
vc_sr.index = [f'{x.mid:.2f}' for x in vc_sr.index]

# 頻度データ(割合)をプロットする
vc_sr.plot()
plt.show()


プロットした結果
f:id:massox:20200618150914p:plain

■ できたと思ったが……

実は、自分が欲しいグラフとは異なるグラフを描画していることが判明
自分が欲しかったグラフのイメージは、

f:id:massox:20200618145626p:plain
でも、実際は

f:id:massox:20200618145642p:plain
ということで、ここんとこ修正していきます。

■ 階級でソートする

公式ドキュメントを見る。

pandas.Series.value_counts — pandas 1.0.4 documentation
sort フラグがデフォルトでTrueになっている。
このせいで、頻度によってソートされてしまう。こいつをFalseにしてやるだけ。

すると、結果はこうなる。

f:id:massox:20200618151117p:plain

これでおっけい。

■ 補足:pd.DataFrame.value_counts と Interval オブジェクト

value_counts の戻り値は頻度データを保持する Series である。
この Series のインデックス(pd.Series.index でアクセス可能)は Interval 型である。

ここらへん初めて知ったのでメモしておく。
Interval型について、以下を参照

pandas.Interval — pandas 1.0.4 documentation


これは、数学で言うところの 区間 を表現するオブジェクトである。
詳細は、ドキュメントに譲るとする。

今回の場合、何も考えずに plt.plot() すると、
X軸ラベルにIntervalオブジェクト、具体的には以下のようなものが描画される。

( 0.1, 0.5 ] ※0.1より大、0.5以下の意

これがラベルに表示されると、
ぐちゃぐちゃになるので、
区間の代表値をラベルに表示したいと思った。
方法は簡単で、Interval.mid 属性を使うだけ。

それが、上記コードのこの部分
midプロパティで取得した値を f-string で文字列化してあげて、小数以下2桁で丸めている。

# - ラベルをきれいに
vc_sr.index = [f'{x.mid:.2f}' for x in vc_sr.index]

以上。