Pythonで二重ドーナツグラフをpie chartで書く

みんなに嫌われ者の"円グラフ"をPythonで書いてみる。
しかも、内側の円グラフにgender、外側の円グラフにgenerationが並ぶ"二重の円グラフ"（二重のpie_chart）を書く。
こんな感じ↓
例によって、matplotlibの引数がややこしいのでメモとして残します。

![pie_chart_#1.png](https://qiita-image-store.s3.amazonaws.com/0/100780/7970bde1-3a1c-4405-8ae8-d48b19ca8cfa.png)


# 円グラフ化するデータ

例えばこんなJSONデータがあったとします

```json:インプットデータ
data={
      "cluster": 1, 
      "member_cnt": 21343, 
      "generation": {"11-15": 284, "81-85": 216, "0-5": 10, 
                     "76-80": 486, "16-20": 840, "56-60": 1938, 
                     "unknown": 558, "61-65": 1561, "86-90": 51, 
                     "51-55": 2504, "46-50": 2639, "6-10": 18, 
                     "31-35": 1501, "41-45": 2410, "36-40": 1954, 
                     "66-70": 1486, "91 over": 23, "26-30": 989, 
                     "21-25": 976, "71-75": 899},
      "gender": {"M": 11652, "F": 9691}
     }
```

- cluster: クラスター番号（ここでは1番）
- member_cnt: 当該のクラスター全体の人数
- generation: 年齢層とそこに所属する人数はdictionary型で格納（足し上げるとmemberの数になる）
- gender: 性別とそれぞれの人数をdictionary型で格納（足し上げるとmemberの数になる）



# プロットする

作図の流れとしては
1. 外側の円グラフ（generation）を作成
2. 内側の円グラフ（gender）を作成
3. 最も内側に真っ白な円を作成
という感じです。

```Python:モジュールの読み込みと初期設定的なもの
# -*- coding: utf-8 -*-
#jupyter notebook用
from IPython.display import display, HTML
%matplotlib inline 

# pie_chart用
import numpy as np
import matplotlib.pyplot as plt

#後で使うカラーをまとめて先に指定しておく（自分が好きな色を指定）
c_cycle=("#3498db","#51a62d","#1abc9c","#9b59b6","#f1c40f",
         "#7f8c8d","#34495e","#446cb3","#d24d57","#27ae60",
         "#663399","#f7ca18","#bdc3c7","#2c3e50","#d35400",
         "#9b59b6","#ecf0f1","#ecef57","#9a9a00","#8a6b0e")
```



```Python:スクリプト本体
cluster_num = data['cluster']
member_cnt = data['member_cnt']

'''''''''''''''''''''''''''''''''
generationについての外側の円グラフを作成する
'''''''''''''''''''''''''''''''''

generation_labels = list()
generation_values = list()

for gen_cat,cnt in sorted(data['generation'].items(),reverse=True,key=lambda x:x[1]): #所属人数(cnt)が多い順にkey(gen_cat)をソートする
    generation_labels.append(gen_cat)
    generation_values.append(cnt)

plt.figure(figsize=(10,8)) #適当な大きさのウィンドウを用意
x = np.array(generation_values) #円グラフ描写のために値をnp.array型に代入

plt.pie(x,
       labels=generation_labels, #円グラフのラベルを指定
       colors=c_cycle, #円グラフの色を指定（上記で先に設定したもの）
       wedgeprops={'linewidth': 2,'edgecolor':"white"}, # 太さ2で円グラフに白フチを付ける
       textprops={'color': "white", 'weight': "bold"}, # 文字の色と太さ
       pctdistance=0.85, # 構成割合の数値を中心から円周の間の 85% の位置に出力（テキスト表示位置の微調整）
       startangle=90, #円の向きを90度回転
       counterclock=False, #falseで反時計回りに出力
       autopct=lambda p: '{:.1f}%'.format(p) if p >= 2.5 else '' #2.5%以下の構成要素は数値を非表示（後述）
      )

plt.axis('equal') #歪み無く円グラフを出力するのに必要。これを指定しないと楕円形になる


'''''''''''''''''''''''''''''
genderについての内側の円グラフを作成
'''''''''''''''''''''''''''''

gender_labels = list()
gender_values = list()

for gender,cnt in data['gender'].items():
    gender_labels.append(gender)
    gender_values.append(cnt)
y = np.array(gender_values)

plt.pie(y,
        labels=gender_labels,
        colors=("#00a2ad","#ffcccc"), # Male,Female用の色の割当
        wedgeprops={'linewidth': 2,'edgecolor':"white"}, # 太さ2で白フチを付ける
        textprops={'color': "#2c3e50", 'weight': "bold"}, # 文字の色と太さ
        radius=0.7, #70%の大きさの円を書く。genderは内側の円グラフになるため少し小さめに出力する。
        labeldistance=0.8, #ラベルの表示位置を円周から80%の位置に表示
        autopct="%1.1f%%", # 構成割合を小数点以下1桁で出力
        pctdistance=0.6, # 構成割合を中心から円周の間の 60% の位置に出力
        startangle=90, #円の向きを90度回転
        counterclock=False #falseで反時計回りに出力
       )
plt.axis('equal') #アスペクト比を補正。歪み無く円グラフを出力するのに必要


'''''''''''''''''''''''''''''
最も内側に白の円グラフを書く（ドーナツ円グラフっぽく見せる）
'''''''''''''''''''''''''''''

center_circle = plt.Circle((0,0),0.4,color='white', fc='white',linewidth=1.25) #中心(0,0)に40%の大きさで円を描画
fig = plt.gcf()
fig.gca().add_artist(center_circle)

# generation+genderの凡例の表示
lgnd=plt.legend(bbox_to_anchor=(1.0, 0.25, 1.55, 0.5), loc="center left", borderaxespad=0.)

# 図のタイトルの表示
plt.suptitle('cluster#%s (UU=%s)' % (cluster_num,member_cnt),fontsize=25)


#円グラフの保存
# bbox~~というパラメータが無いとsaveした画像からlegendがはみ出る
#plt.savefig('./hoge/pie_chart_#{}.png'.format(cluster_num),bbox_exstra_artists=(lgnd),bbox_inches='tight') 
plt.show()
```

# 説明

**色をおしゃれ風にする**

- [こちら](http://qiita.com/Hyperion13fleet/items/dee6cfb61959a7767d2b)を参考にさせていただきました
	- `plt.pie`のデフォルトの色ではドギツイ感じがするので[ここ](https://flatuicolors.com/)や[ここ](http://www.flatuicolorpicker.com/)や[ここ](http://colorpalettes.net/)を参考に素敵な色を見つけてきて`c_cycle`として設定しています

**要は、円グラフ化したい値をnp.arrayに、ラベルをlist型にいれる**

- 例えば"generation"の円グラフの場合、
	- ラベルでは`generation_labels.append(gen_cat)`でリスト型に格納
	- 値では一旦`generation_values.append(cnt)`でリスト型に格納した後、`x = np.array(generation_values) `でarray型に変換している箇所に該当
	- それらのデータをplt.pieに放り込めばおｋ

**matplotlib.pyplotのpie関数はデータを自動で割合値に変換してくれるらしい**

- コードを追ってもらうとわかりますが、`data`として読み込んだ実数値を（割合に算出する処理を行わないまま）array型で格納し、plt.pieの引数として渡すと割合値として描写されるようです
	- じゃあ、割合にしないで入力値そのままで円グラフを書きたい場合は？ → すみません、しらべてません。。。

**割合が小さい要素を非表示にして、見栄えを整える**

- `\#2.5%以下の構成要素は数値を非表示`という箇所の処理に該当
	- 割合が小さな要素周辺は値のテキストが重なってしまい見栄えが悪い
	- なんとかテキストが重ならないように表示しようとしたがうまい方法が見つからなかったため、いっそ割合が小さい要素（ここでは適当に2.5%以下とした）を非表示にしています


他のplt.pieのパラメーターに関しては、コード中のコメントアウトを読んでもらったり、実際に該当箇所をコメントアウトで描写がどう変わるかを確認してもらうと理解できると思います


# ちょっとずるしているところ

"generation"の円グラフを書いているところで、`textprops={'color': "white", 'weight': "bold"}`の"white"を"black"にすると、実はこんな図になっています↓

![pie_chart2_#1.png](https://qiita-image-store.s3.amazonaws.com/0/100780/0757071b-88d0-c2fa-b437-d0c417c03fa5.png)


この重なったラベルもなんとかうまく表示するor削除したいのですが、ただの文字列データなので上記の「2.5%以下は非表示」のように条件をつけて非表示にすることが出来ない状態です（自分はうまい方法を見つけられなかった）。このあたり、良い方法をご存知の方がいれば教えていただきたい...
ということで、この部分はとりあえず白文字にして背景に溶かしてごまかしてる感じです。


# 参考

- [matplotlib (Python) で円グラフ](https://mkacky.wordpress.com/2014/09/28/matplotlib-python-%E3%81%A7%E5%86%86%E3%82%B0%E3%83%A9%E3%83%95/)
- [matplotlib で円グラフ (pie chart) を描く](http://pythondatascience.plavox.info/matplotlib/%E5%86%86%E3%82%B0%E3%83%A9%E3%83%95/)
- [matplotlibで円グラフを書く](http://d.hatena.ne.jp/a-hisame/20150424/1429875458)
- [Pythonで円グラフ](http://akiyoko.hatenablog.jp/entry/2013/06/22/001523)
- [Python Matplotlib figure title overlaps axes label when using twiny](http://stackoverflow.com/questions/12750355/python-matplotlib-figure-title-overlaps-axes-label-when-using-twiny)
- [matplotlib savefig() size control](http://stackoverflow.com/questions/13073045/matplotlib-savefig-size-control)