29
48

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-03-27

この記事の目的

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

calendar.png

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

似た可視化の方法

  • Contributions Calendar
  • Contribution Graph
  • 芝生

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

contributions_graph.png
出典:GitHubヘルプ プロフィールでコントリビューションを表示する

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

image02.png

GitHub:martijnvermaat/calmap

また, Pythonでは見つけられませんでしたが, Rなら, openairというライブラリで月別のカレンダー表示ができます.
openair:calendarPlot()

image03.png
出典: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という列にタイムスタンプ情報が入っています.
image04.jpg

まず, タイムスタンプの列に文字列として入っている値を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()

image06.png

ここでweekが週番号でdayofweekが曜日になっています.
注意として, ここでのdayofweek0が日曜日, 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

image07.png

このデータフレーム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

image08.png

綺麗になりました. (本当は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

image09.png
上のような, 日付の文字列を要素とした, 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()

image11.jpg

以上!

コード一覧

一覧を表示
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

29
48
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
29
48

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?