この記事の目的
以下のような, 祝日表示付きのカレンダー形式のヒートマップを作成する.
ちなみにデータには, Qiita APIを使って取得した @yaotti さんの記事投稿日を1投稿=1イベントとして使わせていただいています.
似た可視化の方法
- Contributions Calendar
- Contribution Graph
- 草
- 芝生
など, いろいろな名前で呼ばれていますが, GitHubの以下の図もシンプルで良いですね.
出典:GitHubヘルプ プロフィールでコントリビューションを表示する
Pythonだとcalmap
というライブラリで似たような図が作成できるみたいです.
GitHub:martijnvermaat/calmap
また, Pythonでは見つけられませんでしたが, Rなら, openair
というライブラリで月別のカレンダー表示ができます.
openair:calendarPlot()
出典:RPubs Plotting with openair
しかし,
- カレンダー形式
- 日付表示がある
- 祝日表示がある
を満たすものは, 調べた限り見つかりませんでした...
解説
ということで, 自分でコードを書きました!
準備
まずは, ライブラリを準備します.
!pip install jpholiday #祝日情報を取得するため
!pip install japanize_matplotlib #pltの日本語化のため
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を使ってみたので, Qiita記事の投稿をイベントと考えて, 投稿日の可視化を行います.
取得した投稿データは以下のような形式です. タイムスタンプの列があれば, 他のどんなデータでも大丈夫です. 今回は, created_at
という列にタイムスタンプ情報が入っています.
まず, タイムスタンプの列に文字列として入っている値をdatetime.datetime型
というものに変換します.
format
は適宜変更する必要があります.
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
さらに, 自作関数のamerican_calendar
というもので, アメリカ式の週番号と曜日情報を取得します.
詳細はこちら:アメリカ式の週番号の取得
def american_calendar
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
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]
ここまでくるとデータは以下のようになっているはずです.
df.loc[:,["created_at", "year", "month", "day", "week", "dayofweek"]].head()
ここでweek
が週番号でdayofweek
が曜日になっています.
注意として, ここでのdayofweek
は0
が日曜日, 1
が月曜日...と続きます.
イベントの集計
ここでyear
を指定して, データを絞り込み, 日付別に集計するためにpivot_table
を使います.
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
このデータフレームpv
は, イベントが起きない週がまったく表示されおらず, このままでは使えません.
そこで, あらかじめカレンダーのような54×7マスのデータフレームmat
を作成し, そこに値を追加することで, 整形します.
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
綺麗になりました. (本当は54行まであります)
続いて, カレンダーに表示する日付を準備します.
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
上のような, 日付の文字列を要素とした, 54×7の配列ができます.
はじめの2つの要素が空文字なので, この年は火曜から始まるということになります.
可視化
最後にsns.heatmap
を使ってカレンダーを描きます.
祝日表示については, jpholiday.year_holidays(year)
に特定年の祝日一覧が入っているので, その情報から, patches.Rectangle
で赤い正方形を1つ1つ描画しています.
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()
以上!
コード一覧
一覧を表示
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()
参考
GitHubヘルプ: プロフィールでコントリビューションを表示する
GitHub:martijnvermaat/calmap
openair:calendarPlot()
RPubs:Plotting with openair
GitHub:日本の祝日を取得するライブラリ jpholiday
stack overflow:How to Add Text plus Value in Python Seaborn Heatmap