Pythonでのパレート図の作り方

paleto chart 4

今回はmatplotlibを使用してパレート図を作成する方法を紹介します。

実はよく使われる業界や分野もあるようなので、axesの使い方の学習も兼ねて流れを見ていきましょう。

パレート図について

パレート図は、発生頻度や割合が大きな要素から、それらが全体に対し占める割合を可視化するのに利用される図で、値が大きなものから並べた棒グラフと、データの累積分布の割合を示す折れ線グラフから構成されます。

パレート図は「QC7つ道具 (QC: Quality Control)」と呼ばれる品質管理の7つの手法のうちの一つとして、製造業をはじめとして、分野によりますが、意外と広く知られています。

もしかすると、パレートの法則という言葉なら聞いたことがあるかもしれませんね。

パレートの法則は、全体の数値の大部分は一部の要素によりもたらされていることを示す法則で、80:20の法則と呼ばれることもあります。

ちなみにパレートというのは人の名前で、パレート図はその名前からつけられた名称のようです。

Pythonライブラリ

パレート図を作成できるライブラリとしては、paretochartがあります。

ただし、今回はこちらは使用しません。

ドキュメントをみたところ、自分が描きたい図と少し違う図ができるというのが理由です。

ですが、サクッと描きたいときにこういうライブラリがあるのは非常に助かりますね。

paretochart
Pareto chart for python (similar to Matlab's, but much more flexible)

コード

という訳で、今回はmatplotlibを駆使してパレート図を作成したいと思います。

目標と方針

今回最終的に作成するパレート図を先に示しておきます。
paleto chart 4

ポイントとして、下記の3点を挙げておきます。

  • 棒グラフ同士は隣り合う
  • 折れ線グラフは0%から100%まで
  • 折れ線グラフが1番初めの棒グラフの右肩を通る

よくみるパレート図の形ですが、単純にグラフを同じ表に書き込むだけでは実現できません。
いくつか細かな設定が必要になってきます。

ここでは、解説の都合上、以下の3段階に分けて説明します。

  1. データの準備
  2. シンプルなパレート図の作り方
  3. 目標とするパレート図の作り方

それでは、一つずつ見ていきましょう。

データの準備

まずはデータを準備します。今回は、説明のために適当にデータを作成しました。

サンプルデータの作成

作図に使用するデータを準備します。ラベルと値は適当に準備しました。

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
%matplotlib inline
plt.rcParams['font.size'] = 12

sample_label = ["a", "b", "c", "d", "e", "f", "g"]
np.random.seed(seed=42)
sample_data = np.random.randint(0, 100, size=7)
print(sample_data)

今回はこのようなデータを使います。

[51 92 14 71 60 20 82]

一応ポイントとしては、

  • 後のためにフォントのサイズを変えておく
  • np.random.randint()の値を再現するために、np.random.seed()を入れておく。

といったことをしています。

ラベル名は適当につけましたが、実用例としては、品質不具合の要素や、アンケートで集まった不満点などになるでしょうか。

データフレームの作成

次に、データフレームを作成しておきます。

sample_df = pd.DataFrame({"label": sample_label, "data":sample_data}, columns=["label", "data"])
sample_df
labeldata
0a51
1b92
2c14
3d71
4e60
5f20
6g82

また、データを降順に並べ替えておきましょう。

sample_df = sample_df.sort_values(by="data", ascending=False)
sample_df
labeldata
1b92
6g82
3d71
4e60
0a51
5f20
2c14

これで、サンプルデータの準備ができました。

累積和をとる

続いて累積和をとります。listなどを使用して単純に足し合わせてもいいですが、numpy.cumsum()を使うと楽です。
numpy cumsum

sample_df["accum"] = np.cumsum(sample_df["data"])
sample_df
labeldataaccum
1b9292
6g82174
3d71245
4e60305
0a51356
5f20376
2c14390

累積和の割合の計算

次は累積和の割合です。こちらも単純に計算するだけです。今回は、各値をカウント数のsumで割ることにしました。

# パーセント表示の割合を作成する
sample_df["accum_percent"] = sample_df["accum"] / sum(sample_df["data"]) * 100
sample_df
labeldataaccumaccum_percent
1b929223.589744
6g8217444.615385
3d7124562.820513
4e6030578.205128
0a5135691.282051
5f2037696.410256
2c14390100.000000

特に難しいことはありませんね。これで描画に必要なデータが準備できました。

シンプルなパレート図の作り方

それでは、単純なパレート図を作成してみましょう。

ここでのポイントは一つだけです。

  • 1つの図に縦軸が異なる2つのグラフを書くために、axes.twinx()を使う。

axes.twinx()は、x軸を共有させて作図をするために使用します。
シンプルなパレート図を作るだけならこれだけで問題ありません。

なお、今回に限らず、matplollibでの描画では、pltではなくaxesを使った方が描画の対象を意識しやすく、応用も楽だと思います。

それではコードを見てみましょう。

fig, ax1 = plt.subplots(figsize=(6,4))
data_num = len(sample_df)

ax1.bar(range(data_num), sample_df["data"])
ax1.set_xticks(range(data_num))
ax1.set_xticklabels(sample_df["label"].tolist())
ax1.set_xlabel("label")
ax1.set_ylabel("counts")

ax2 = ax1.twinx()
ax2.plot(range(data_num), sample_df["accum_percent"], c="k", marker="o")
ax2.set_ylim([0, 100])

ax2.grid(True, which='both', axis='y')

ax1.set_title("PARETO_CHART")

plt.savefig("../output/pareto_chart1.png", bbox_inches="tight")

simple_paleto_cart1

見やすいグラフではありませんが、一応パレート図としての役割は果たすことができています。

さすがに見辛いので、以下の3点の対応をしておきましょう。

  • 棒グラフの縦軸の範囲を広げる。ここでは一旦[0, 200]にする。
  • 右側の縦軸を20ごとではなく10ごとに変更し、%も表示させる。
  • 右側の縦軸に10ごとにlabelを表示させる。

1点目の対応はaxes.set_ylim([ymin, ymax])でOKですね。

2点目と3点目には、axes.set_yticks()とaxes.set_yticklabels()を使います。
前者で目盛のための場所を準備し、後者でラベルを表示させるイメージですね。

では、コードを確認しましょう。

fig, ax1 = plt.subplots(figsize=(6,4))
data_num = len(sample_df)

# ラベルの準備
percent_labels = [str(i) + "%" for i in np.arange(0, 100+1, 10)]

ax1.bar(range(data_num), sample_df["data"])
ax1.set_xticks(range(data_num))
ax1.set_xticklabels(sample_df["label"].tolist())
ax1.set_ylim([0, 200])
ax1.set_xlabel("label")
ax1.set_ylabel("counts")

ax2 = ax1.twinx()
ax2.plot(range(data_num), sample_df["accum_percent"], c="k", marker="o")
ax2.set_ylim([0, 100])
ax2.set_yticks(np.arange(0, 100+1, 10))
ax2.set_yticklabels(percent_labels)

ax2.grid(True, which='both', axis='y')

ax1.set_title("PARETO_CHART")

plt.savefig("../output/pareto_chart2.png", bbox_inches="tight")

paleto chart 1

これで大分見やすいグラフになったかと思います。

最低限のグラフとしてはここまでで十分ですね。ここからは表示の仕方を工夫していきます。

目標とするパレート図の作り方

今回目標とするパレート図のポイントは下記の3点でした。

  • 棒グラフ同士は隣り合う
  • 折れ線グラフは0%から100%まで
  • 折れ線グラフが1番初めの棒グラフの右肩を通る

この条件を満たすためのポイントが3つあります。

まず初めの2つを見ていきましょう。

axes.bar()のオプション”width”と”align”

目的とするグラフ作成のために、axesの”width”と”align”を活用します。

widthは棒の幅を割合で示していて、width = 1の時に幅がぴったりになるようになっています。

alignは棒グラフと描画点の位置関係を表し、”center”か”edge”のどちらかを指定します。デフォルトは”center”です。
“edge”を指定すると、棒グラフの端は右側になります。つまり、棒グラフが点の左側に来る、ということです。

後述しますが、今回は描画点の右側に描画させます。こは、widthに負の値を渡すことで達成されます。

なので、今回は (width=-1, align=”edge”)を引数として渡します。

折れ線グラフを0%から表示させる

折れ線グラフを0%から表示させたい時の、問題点は、棒グラフよりも折れ線グラフの描画点が1つ大きくなることです。

この問題は、棒グラフを折れ線グラフの描画点の間に表示させることで解決できます。

先ほどの話と絡めると、棒グラフを描画点の右側に配置させれば達成できます。

ただし、これだとラベルの表示がずれてしまうので、ラベルの表示には軸のminorを利用します。

具体的には、下記ような作業をします。
– 棒グラフは描画点からみて右側に描画する(棒グラフの端を左側にする)。軸はmajor。
– 棒グラフ、折れ線グラフ共に原点から描画する(描画点は折れ線グラフの方が1つ多くなる)。軸はmajor。
– 棒グラフのラベルを描画点の間に配置する。軸はminor。

minorはmajorの間に入るように設定します。

コードを確認しましょう。

fig, ax1 = plt.subplots(figsize=(6,4))
data_num = len(sample_df)

# 折れ線グラフを0%から表示させるためのデータの準備
accum_to_plot = [0] + sample_df["accum_percent"].tolist()

percent_labels = [str(i) + "%" for i in np.arange(0, 100+1, 10)]

ax1.bar(range(1, data_num + 1), sample_df["data"], align="edge", width=-1, edgecolor='k')

# 棒グラフのラベルは0.5の位置に設定
ax1.set_xticks([0.5 + i for i in range(data_num)], minor=True)
ax1.set_xticklabels(sample_df["label"].tolist(), minor=True)

# x軸のmajorは描画のためだけなので隠す
ax1.tick_params(axis="x", which="major", direction="in")
ax1.set_ylim([0, 200])
ax1.set_xlabel("label")
ax1.set_ylabel("counts")

ax2 = ax1.twinx()
ax2.set_xticks(range(data_num+1))
ax2.plot(range(data_num+1), accum_to_plot, c="k", marker="o")
ax2.set_xticklabels([])
ax2.set_xlim([0,data_num])
ax2.set_ylim([0, 100])
ax2.set_yticks(np.arange(0, 100+1, 10))
ax2.set_yticklabels(percent_labels)

ax2.grid(True, which='both', axis='y')

ax1.set_title("PARETO_CHART")

plt.savefig("../output/pareto_chart3.png", bbox_inches="tight")

paleto chart 3

本筋のところは先に記述した通りです。
細かなポイントとして、グラフに余計なラベルが表示されないようにax1.tick_params()で目盛を隠したり、set_xticks([])で不要なラベルを消すようにしています。

折れ線グラフが1番初めの棒グラフの右肩を通るようにする

最後に、折れ線グラフが1番目の棒グラフの右肩を通過するようにしましょう。

と言ってもこれは簡単で、棒グラフの縦軸の最大値をデータの総数にするだけです。

言い換えると、折れ線と同様に、全体に対する割合を示すように縦軸を調整します。

コードとしては、棒グラフの表示範囲をax1.set_ylim([0, sum(sample_df[“data”])])とすればOKです。

fig, ax1 = plt.subplots(figsize=(6,4))
data_num = len(sample_df)

accum_to_plot = [0] + sample_df["accum_percent"].tolist()

percent_labels = [str(i) + "%" for i in np.arange(0, 100+1, 10)]

ax1.bar(range(1, data_num + 1), sample_df["data"], align="edge", width=-1, edgecolor='k')

ax1.set_xticks([0.5 + i for i in range(data_num)], minor=True)
ax1.set_xticklabels(sample_df["label"].tolist(), minor=True)
ax1.tick_params(axis="x", which="major", direction="in")
# 変更はここだけ
ax1.set_ylim([0, sum(sample_df["data"])])
ax1.set_xlabel("label")
ax1.set_ylabel("counts")

ax2 = ax1.twinx()
ax2.set_xticks(range(data_num+1))
ax2.plot(range(data_num+1), accum_to_plot, c="k", marker="o")
ax2.set_xticklabels([])
ax2.set_xlim([0,data_num])
ax2.set_ylim([0, 100])
ax2.set_yticks(np.arange(0, 100+1, 10))
ax2.set_yticklabels(percent_labels)

ax2.grid(True, which='both', axis='y')

ax1.set_title("PARETO_CHART")

plt.savefig("../output/pareto_chart4.png", bbox_inches="tight")

paleto chart 4

これで目的とするグラフが作成できました。

まとめ

今回はmatplotlibでのパレート図の作成方法を扱いました。

統計や機械学習の勉強をしていても、パレート図を見かけることはあまりないような気もしますが、知っていると意外と役に立つかもしれません。

やっていること自体はすごくシンプルなので、matplotlibに不慣れな方は、勉強がてら自分で作成してみるのもいいと思います。

参考資料

Wikipedia パレート図
numpy cumsum
matplotlib api example code: two_scales.py
matplotlib.axes.Axes.bar
paretochart

コメント