【Python】カレンダー形式のヒートマップ(祝日表示つき)

# この記事の目的

以下のような, 祝日表示付きのカレンダー形式のヒートマップを作成する. 

![calendar.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/540956/d03c5a05-d680-00a9-4f70-b7e0fc77ea05.png)

ちなみにデータには, Qiita APIを使って取得した @yaotti さんの記事投稿日を1投稿＝1イベントとして使わせていただいています. 

# 似た可視化の方法

- Contributions Calendar
- Contribution Graph
- 草
- 芝生

など, いろいろな名前で呼ばれていますが, GitHubの以下の図もシンプルで良いですね. 

![contributions_graph.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/540956/7c36eee2-e2bd-7967-ed61-8da5c34a0887.png)
出典：[GitHubヘルプ プロフィールでコントリビューションを表示する](https://help.github.com/ja/github/setting-up-and-managing-your-github-profile/viewing-contributions-on-your-profile#contributions-calendar)

Pythonだと```calmap```というライブラリで似たような図が作成できるみたいです. 

![image02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/540956/5f2e0752-d3dc-03fb-7530-ca3ebdaebbdc.png)

GitHub：[martijnvermaat/calmap](https://github.com/martijnvermaat/calmap)

また, Pythonでは見つけられませんでしたが, Rなら, ```openair```というライブラリで月別のカレンダー表示ができます. 
openair：[calendarPlot()](http://davidcarslaw.github.io/openair/reference/calendarPlot.html)


![image03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/540956/b975d7b3-4c5f-0190-bf18-b98b69aeba0e.png)
出典：[RPubs Plotting with openair](https://rpubs.com/NateByers/Openair)


しかし,  

- カレンダー形式
- 日付表示がある
- 祝日表示がある

を満たすものは, 調べた限り見つかりませんでした...



# 解説
ということで, 自分でコードを書きました！

## 準備
まずは, ライブラリを準備します. 

```python
!pip install jpholiday #祝日情報を取得するため
!pip install japanize_matplotlib #pltの日本語化のため
```


```python
import numpy as np
import pandas as pd
import requests
import json
from pandas.io.json import json_normalize
import datetime
import jpholiday

import seaborn as sns
import matplotlib.pyplot as plt
import japanize_matplotlib
import matplotlib.patches as patches #長方形の描画

```

## データの用意

適当なイベントデータを用意します. ここでは, [最近Qiita APIを使ってみた](https://qiita.com/yuto16/items/ca4ba95d6d2b4aa2d11e)ので, Qiita記事の投稿をイベントと考えて, 投稿日の可視化を行います. 

取得した投稿データは以下のような形式です. タイムスタンプの列があれば, 他のどんなデータでも大丈夫です. 今回は, ```created_at```という列にタイムスタンプ情報が入っています. 
![image04.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/540956/a9ab3cc5-7345-0407-4b42-88ce40bf05af.jpeg)

まず, タイムスタンプの列に文字列として入っている値を```datetime.datetime型```というものに変換します. 
```format```は適宜変更する必要があります. 

```python
df["created_at"] = pd.to_datetime(df["created_at"], format='%Y-%m-%dT%H:%M:%S+09:00')
```

続いてここから, 年や月などの情報を抽出します. 

```python
df["year"] = df["created_at"].dt.year
df["month"] = df["created_at"].dt.month
df["day"] = df["created_at"].dt.day
```

さらに, 自作関数の```american_calendar```というもので, アメリカ式の週番号と曜日情報を取得します. 
詳細はこちら：[アメリカ式の週番号の取得](https://qiita.com/yuto16/items/337c9b2f596394326b26)

<details><summary>def american_calendar</summary><div>

```python
def american_calendar(year, month, day):
    #入力年のdatetime.date型データを作成
    inp = datetime.date(year=year, month=month, day=day)
    #入力年の元日のdatetime.date型データを作成
    first = datetime.date(year=year, month=1, day=1)

    #まず曜日の計算
    inp_how = (inp.weekday()+1) % 7 #+1は曜日を日曜始まりに変更するため
    first_how = (first.weekday()+1)%7
    
    #以下, 週番号の計算
    #カレンダーの左上(最初の日曜日)の日付を取得
    upper_left = first - datetime.timedelta(days=first_how)

    #基準日との日数差を計算して週番号を取得
    inp_week = (inp - upper_left).days // 7
    
    return year, inp_week, inp_how
```
</div></details>



```python
cal = np.array([american_calendar(ymd[0], ymd[1], ymd[2]) for ymd in df.loc[:,["year","month","day"]].values])

df["week"] = cal[:,1]
df["dayofweek"] = cal[:,2]
```

ここまでくるとデータは以下のようになっているはずです. 

```python
df.loc[:,["created_at", "year", "month", "day", "week", "dayofweek"]].head()
```

![image06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/540956/58e2f29f-de35-e8f8-9a2f-884ff62ba8e6.png)

ここで```week```が週番号で```dayofweek```が曜日になっています. 
注意として, ここでの```dayofweek```は```0```が日曜日, ```1```が月曜日...と続きます. 

## イベントの集計

ここで```year```を指定して, データを絞り込み, 日付別に集計するために```pivot_table```を使います. 

```python
year = 2012

idx_name = "week"
col_name = "dayofweek"

tmp_df = df[df["year"]==year]
pv = tmp_df.pivot_table(index=idx_name, columns=col_name, values="body", aggfunc="count") #bodyは適当なんでもOK
pv = pd.DataFrame(pv.values, columns=pv.columns.values ,index=pv.index.values)
pv
```
![image07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/540956/86b3033b-dc89-0a48-a481-086c9f3e1d35.png)

このデータフレーム```pv```は, イベントが起きない週がまったく表示されおらず, このままでは使えません. 
そこで, あらかじめカレンダーのような54×7マスのデータフレーム```mat```を作成し, そこに値を追加することで, 整形します. 

```python
pv.columns = [int(num) for num in pv.columns]
pv.index = [int(num) for num in pv.index]
pv = pv.fillna(0)

mat = pd.DataFrame(index=list(range(54)), columns=list(range(7)))
mat = mat.fillna(0)

mat = mat.add(pv, fill_value=0)
mat = mat.applymap(lambda x: int(x))
mat
```
![image08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/540956/a0469ad1-4e4a-a8fd-4377-78f480c2abbf.png)

綺麗になりました. (本当は54行まであります)

続いて, カレンダーに表示する日付を準備します. 

```python
mat_date = pd.DataFrame(index=list(range(54)), columns=list(range(7)))
mat_date = mat_date.fillna("")

tmp = datetime.date(year=year, month=1, day=1)

for i in range(366):
    if tmp.year==year:
        y,week,how = american_calendar(tmp.year, tmp.month, tmp.day)
        mat_date.loc[week,how] = str(tmp.month) +"/"+ str(tmp.day)
    tmp+=datetime.timedelta(days=1)
    
lab = mat_date.values
lab
```

![image09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/540956/f1d1584f-8a58-9143-b287-2a055a994229.png)
上のような, 日付の文字列を要素とした, 54×7の配列ができます. 
はじめの2つの要素が空文字なので, この年は火曜から始まるということになります. 

## 可視化

最後に```sns.heatmap```を使ってカレンダーを描きます. 
祝日表示については, ```jpholiday.year_holidays(year)```に特定年の祝日一覧が入っているので, その情報から, ```patches.Rectangle```で赤い正方形を１つ１つ描画しています. 

```python
plt.figure(figsize=(10,30))
ax = sns.heatmap(mat, annot=lab, square=True, fmt="", cmap="Greens", cbar=False, linewidths=0.1, linecolor="silver")
ax.set_xticklabels(["日","月","火","水","木","金","土"])

for holiday in jpholiday.year_holidays(year):
    tmp = holiday[0]
    y,week,how = american_calendar(tmp.year, tmp.month, tmp.day)

    r = patches.Rectangle(xy=(how, week), width=1, height=1, edgecolor='red', fill=False, linewidth=1)
    ax.add_patch(r)

ax.tick_params(right=False, top=True, labelright=False, labeltop=True)
plt.xlim((-0.1,7.1))
plt.title("Calendar Heatmap (year:{0})".format(year))
plt.show()
```

![image11.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/540956/7861e315-5c11-f426-866b-f278c8dd40a7.jpeg)

以上！

# コード一覧
<details>
<summary>一覧を表示</summary><div>

```python
import numpy as np
import pandas as pd
import requests
import json
from pandas.io.json import json_normalize
import datetime
import jpholiday

import seaborn as sns
import matplotlib.pyplot as plt
import japanize_matplotlib
import matplotlib.patches as patches #長方形の描画

# データ準備
df["created_at"] = pd.to_datetime(df["created_at"], format='%Y-%m-%dT%H:%M:%S+09:00')
df["year"] = df["created_at"].dt.year
df["month"] = df["created_at"].dt.month
df["day"] = df["created_at"].dt.day

cal = np.array([american_calendar(ymd[0], ymd[1], ymd[2]) for ymd in df.loc[:,["year","month","day"]].values])
df["week"] = cal[:,1]
df["dayofweek"] = cal[:,2]

# データ集計
year = 2012
idx_name = "week"
col_name = "dayofweek"

tmp_df = df[df["year"]==year]
pv = tmp_df.pivot_table(index=idx_name, columns=col_name, values="body", aggfunc="count") #bodyは適当なんでもOK
pv = pd.DataFrame(pv.values, columns=pv.columns.values ,index=pv.index.values)

# 集計データの整形
pv.columns = [int(num) for num in pv.columns]
pv.index = [int(num) for num in pv.index]
pv = pv.fillna(0)
mat = pd.DataFrame(index=list(range(54)), columns=list(range(7)))
mat = mat.fillna(0)
mat = mat.add(pv, fill_value=0)
mat = mat.applymap(lambda x: int(x))

#日付ラベル作成
mat_date = pd.DataFrame(index=list(range(54)), columns=list(range(7)))
mat_date = mat_date.fillna("")

tmp = datetime.date(year=year, month=1, day=1)

for i in range(366):
    if tmp.year==year:
        y,week,how = american_calendar(tmp.year, tmp.month, tmp.day)
        mat_date.loc[week,how] = str(tmp.month) +"/"+ str(tmp.day)
    tmp+=datetime.timedelta(days=1)

lab = mat_date.values


# 可視化
plt.figure(figsize=(10,30))
ax = sns.heatmap(mat, annot=lab, square=True, fmt="", cmap="Greens", cbar=False, linewidths=0.1, linecolor="silver")
ax.set_xticklabels(["日","月","火","水","木","金","土"])

for holiday in jpholiday.year_holidays(year):
    tmp = holiday[0]
    y,week,how = american_calendar(tmp.year, tmp.month, tmp.day)

    r = patches.Rectangle(xy=(how, week), width=1, height=1, edgecolor="red", fill=False, linewidth=1)
    ax.add_patch(r)

ax.tick_params(right=False, top=True, labelright=False, labeltop=True)
plt.xlim((-0.1,7.1))
plt.title("Calendar Heatmap (year:{0})".format(year))
plt.show()

```
</div></details>



# 参考
GitHubヘルプ：[ プロフィールでコントリビューションを表示する](https://help.github.com/ja/github/setting-up-and-managing-your-github-profile/viewing-contributions-on-your-profile#contributions-calendar)
GitHub：[martijnvermaat/calmap](https://github.com/martijnvermaat/calmap)
openair：[calendarPlot()](http://davidcarslaw.github.io/openair/reference/calendarPlot.html)
RPubs：[Plotting with openair](https://rpubs.com/NateByers/Openair)
GitHub：[日本の祝日を取得するライブラリ jpholiday](https://github.com/Lalcs/jpholiday)
stack overflow：[How to Add Text plus Value in Python Seaborn Heatmap](https://stackoverflow.com/questions/41164710/how-to-add-text-plus-value-in-python-seaborn-heatmap)