派遣で働くエンジニアのスキルアップを応援するサイト

PRODUCED BY RECRUIT

"続けた日"が光る!Pythonで簡単カレンダーヒートマップ

「Pythonに興味がある」「プログラミングを勉強してみたい」そんな方におすすめの本連載。今回は継続記録が一目瞭然のカレンダーヒートマップを作ります!Excelと組み合わせて、楽しくはじめられる内容です。

手順の紹介だけではなく、読むだけでも「なるほど!」と思っていただけるよう、ポイントもやさしく解説しています。

今回つくるもの
◆毎日の記録をカレンダーヒートマップに!頑張り可視化ツール

Excelは日々の学習記録やダイエット記録などをつけるのに、もってこいのツールです。せっかくつけた記録をカレンダーヒートマップで可視化して「サボっていないか?」「がんばれているか?」を見てみましょう。

自分で作成したカレンダーヒートマップをSNSに投稿することで、ちょっとした注目を集められるかも?

 
INDEX
  1. カレンダーヒートマップでモチベーションを維持しよう
    ・カレンダーヒートマップとは?
    ・カレンダーヒートマップはモチベーション維持に最適
  2. Pythonプログラミングの準備
    ・Pythonプログラミング環境の構築
    ・必要なライブラリのインストール
  3. サンプルのExcelファイルについて
    ・Excel関数で日付を作成する
  4. Python×Excelで実践!
    ・データを読み込む
    ・相関係数で色を付ける
    ・データのレベル分けをする
    ・グリッドデータをつくる
    ・グラフにプロットする
  5. 全体のコードはこちらから

1.カレンダーヒートマップでモチベーションを維持しよう

■カレンダーヒートマップとは?

カレンダーヒートマップとは、1年間の各日付をマス目として並べ、日ごとの数値(学習時間・歩数など)を色の濃淡で表すグラフです。

IT業界ではGitHubがリポジトリへの貢献度可視化のために導入したことでよく知られています。緑の濃い日が多いと「活動的に見える」あのグラフです。

この記事では以下のカレンダーヒートマップを作成します。

上記のカレンダーヒートマップを見ると、月曜から金曜まで何もやっていないけれど、土日はコンスタントに作業していた、ということが一目瞭然ですね。土日だけだとしても、何かを1年ずっとやり続けられるのはすごいですよね。

次の図はどうでしょうか?

このマップは、お正月にやる気が出て3月までは頑張っていたけど、新年度からぱったりしなくなっちゃったのかな?と、見える分布になっています。

このように、カレンダーヒートマップを見ると、自分のカレンダーの感覚と照らし合わせてみることができるので、活動の情景がよく伝わってきます。

■カレンダーヒートマップはモチベーション維持に最適

カレンダー形式で活動状況を可視化することによって、継続的な取り組みへの意欲が自然と高まってきます。資格勉強時間や受験勉強時間といった学習系はもちろん、筋トレ時間やウォーキング時間といったレコーディングダイエットとして使うのも最適です。

連続的に色がついていくカレンダーヒートマップを見ていると、空白を埋めるために「今日もちょっとだけやってみようかな?」という気分になります。

IT業界では緑色のヒートマップを用いたカレンダーヒートマップが有名で、俗に「草を生やす」と呼ばれることがあります。皆さんも、まずはExcelに記録されたデータを用いてカレンダーヒートマップで「大草原」をつくってみましょう!

2. Pythonプログラミングの準備

■Pythonプログラミング環境の構築

Pythonプログラミングの環境は「最速でPython環境を構築してプログラミングをはじめよう」を参照してください。リンク先にはPythonのインストール方法、プログラムの記述・実行方法、外部ライブラリのインストール方法をまとめています。

▼第0回:最速でPython環境を構築してプログラミングをはじめよう

POINT
上記の記事からはじめる方は、JupyterLabを立ち上げる前の状態にして、次の「必要なライブラリのインストール」に進みましょう(仮想環境を使わない場合は5節、仮想環境を使う場合は7節の、いずれもjupyter labコマンドを実行する手前まで行う)。

■必要なライブラリのインストール

以下の3つの外部ライブラリをインストールしておきましょう。

(1)Pandas

PandasはPythonでExcelを扱うときに便利な外部ライブラリです。次のコマンドでインストールしましょう。 Pandasをインストールすると、依存ライブラリであるNumPyもインストールされます。
pip install pandas

(2)openpyxl

openpyxlはPandasでExcelを読み込むときに必要です。以下のコマンドでインストールしましょう。
pip install openpyxl

(3)Matplotlib

MatplotlibはPythonでグラフや図を描画するのに便利な外部ライブラリです。次のコマンドでインストールしましょう。
pip install matplotlib

外部ライブラリのインストールが終了したら、以下のコマンドを入力して、JupyterLabを立ち上げましょう。 立ち上がったら、Notebookと書かれている下にあるPython 3のアイコンをクリックします。

3. サンプルExcelファイルについて

■Excel関数で日付を作成する

カレンダーヒートマップを書くためのデータ例を紹介します。まずExcelを起動し、A1セルに「日付」、B1セルに「値」と入力しましょう。

次に、A2セルに「2025/1/1」のような日付を書きます。そのままセルの右下の■をドラッグしてフィル操作をすると、連続した日付が自動的に計算されます(うるう年なども考慮されます)。

何日からはじめても構いませんが、この記事で紹介するコードは単一の年に限定した簡易版とします。複数年をまたぐ場合はコードを修正する必要があるので、あらかじめご了承ください。

あとは「値」の欄であるB2以降のセルに自分の好きな項目を仮定して、数値を入れていきましょう。

これから、このExcelとPythonを使ってカレンダーヒートマップを作成するコードを紹介します。まだ記録するデータが思いつかない場合は、筆者がサンプルデータを用意しましたので、次のExcelファイルをダウンロードして使ってください。

▼クリックするとファイルがダウンロードされます。

https://www.r-staffing.co.jp/rasisa/wp-content/uploads/2026/01/record_data_calendar.xlsx

4. Python×Excelで実践!

■データを読み込む

まずはExcelファイルのデータを読み込む部分を実装しましょう。次のコードを書きます。

import pandas as pd

def load_daily_series(excel_path, year):
    """Excelから日付と値を読み込み、指定年の全日Seriesにそろえて返す関数"""

    # Excelに書いたヘッダー名
    date_col = "日付"
    value_col = "値"

    # PandasでExcelを開く
    df = pd.read_excel(excel_path)
    df[date_col] = pd.to_datetime(df[date_col])
    df[value_col] = pd.to_numeric(df[value_col]).fillna(0)
    df = df.dropna(subset=[date_col])

    # 1年間のデータを作成(欠けている日があった場合に0で埋める)
    day_index = pd.date_range(f"{year}-01-01", f"{year}-12-31", freq="D")
    series = (
        df.set_index(date_col)[value_col]
        .reindex(day_index)
        .fillna(0)
    )

    return series, year


# ファイルパスと年の設定
excel_path = "record_data_calendar.xlsx"
year = 2025

# ①日次データを読み込み
daily_series, year = load_daily_series(excel_path, year)

# ①の出力を確認
print(daily_series)
print(year)

コードを実行して左に年月日、右にExcelに記録されたデータが出力され、最後に指定した年(ここでは2025)も出力されていれば正常です。

2025-01-01    0.0
2025-01-02    0.0
2025-01-03    0.0
2025-01-04    1.0
2025-01-05    2.0
             ... 
2025-12-27    1.3
2025-12-28    1.8
2025-12-29    0.0
2025-12-30    0.0
2025-12-31    0.0
Freq: D, Name: 値, Length: 365, dtype: float64
2025
 
POINT
10行目〜14行目:PandasでExcelを開く
Pandasは表形式のデータをDataFrame型として扱います。pd.read_excelでExcelファイルを読み込み、DataFrame型のデータとして取得しています。
 
pd.to_datetimeは日付部分をdatetime型(日付を扱う型)に変換しています。Excelには2025/1/1や2025年1月1日といったさまざまな形式で日付が書かれる場合がありますが、それらをすべて自動判定してPandasで扱えるdatetime型に統一します。
 
そしてpd.to_numericは値列の数をfloat型に変換しています。そして、数値にできないものはfillna(0)で0にします。
 
さらに、df.dropnaは日付が空欄で値が入っている行があった場合(欠損値の場合)に行を削除します。この辺はデータの前処理としてよく使われる内容なので、ぜひご自身のデータを分析する時などに参考にしてください。
 
POINT
16行目〜22行目:1年間のデータを作成(欠けている日があった場合に0で埋める)
カレンダーヒートマップは毎日のマスが必要であるため、Excelで欠けている日付(記録がされなかった日)があるとつくれません。
 
pd.date_rangeは対象年すべての日付をつくり、つくった日付と元からある日付のデータを並べ替え、欠けている日があったら先ほどと同様にfillna(0)で0にします。これも予想外のデータが来たときの予防策として書いておくと安心です。

■データのレベル分けをする

次に、データ(学習時間や歩数など、Excelの「値」欄に記載した数値)をレベル分けします。

先ほどのコードに追加(コード内に「追加」のコメントがあります)を行いましょう。

import pandas as pd
import numpy as np    # 追加

def load_daily_series(excel_path, year):
    """Excelから日付と値を読み込み、指定年の全日Seriesにそろえて返す関数"""
    省略
    return series, year

def to_levels_fixed(values, bins):                   # ここから追加
    """数値配列を固定しきい値に基づき0〜4レベルに変換する関数"""

    v = np.asarray(values, float)
    bounds = [0.0] + list(bins)
    idx = np.digitize(v, bounds, right=True)
    levels = np.clip(idx, 0, 4)

    return levels.astype(int)                       # ここまで追加


# ファイルパスと年の設定
excel_path = "record_data_calendar.xlsx"
year = 2025

# ①日次データを読み込み
daily_series, year = load_daily_series(excel_path, year)

# ①の出力を確認
print(daily_series)
print(year)

# ②レベルを設定する                          # ここから追加
fixed_bins = (1, 2, 3, 4)
level_values = to_levels_fixed(daily_series.values, bins=fixed_bins)
level_series = pd.Series(level_values, index=daily_series.index)

# ②の出力を確認
print(level_series)                        # ここまで追加

このコードを実行すると、日付と値(時間など)だった最初のデータが、日付とレベルのデータに変換されていることが確認できます。

2025-01-01    0
2025-01-02    0
2025-01-03    0
2025-01-04    1
2025-01-05    2
             ...
2025-12-27    2
2025-12-28    2
2025-12-29    0
2025-12-30    0
2025-12-31    0
Freq: D, Length: 365, dtype: int64
POINT
50行目、27行目〜35行目:レベルの設定
fixed_bins = (1, 2, 3, 4)はレベルの設定です。今回は5段階のレベル分けを行います。つまり先ほどのfixed_binsの設定の場合、1未満の値は0、4以上の場合は4と0, 1, 2, 3, 4の5段階にレベル分けするための設定です。
 
そしてto_levels_fixed関数で実際にレベル分類を行います。この関数はデータをNumPy配列に変換した後、np.digitizeでデータがどの区間に入るかを判定しています。そして最後にnp.clipで結果を0〜4に制限しています。
 
例えば、この処理のおかげで10といった値になっても4に丸められます。

■グリッドデータをつくる

次はグリッドデータをつくります。カレンダーヒートマップのデータの並び順が週と曜日のグリッドになっているのでグリッドデータと呼んでいます。

先ほどのコードに次の追加を行いましょう。

import pandas as pd
import numpy as np

def load_daily_series(excel_path, year):
    """Excelから日付と値を読み込み、指定年の全日Seriesにそろえて返す関数"""
    省略
    return series, year

def to_levels_fixed(values, bins):
    """数値配列を固定しきい値に基づき0〜4レベルに変換する関数"""
    省略
    return levels.astype(int)

def build_calendar_grid(level_series, year):      # ここから追加
    """レベルSeriesをカレンダー形式の週×曜日グリッドに変換する関数"""

    start = pd.Timestamp(f"{year}-01-01")
    end = pd.Timestamp(f"{year}-12-31")

    start_monday = start - pd.Timedelta(days=start.weekday())
    end_sunday = end + pd.Timedelta(days=(6 - end.weekday()))

    all_days = pd.date_range(start_monday, end_sunday, freq="D")
    num_weeks = len(all_days) // 7

    # 行=Mon..Sun, 列=週
    grid = np.zeros((7, num_weeks), int) 

    for i, d in enumerate(all_days):
        week = i // 7
        dow = d.weekday()  # 0=Mon..6=Sun
        grid[dow, week] = int(level_series.get(d, 0))

    return grid, num_weeks, start_monday          # ここまで追加


# ファイルパスと年の設定
excel_path = "record_data_calendar.xlsx"
year = 2025

# ①日次データを読み込み
daily_series, year = load_daily_series(excel_path, year)

# ①の出力を確認
print(daily_series)
print(year)

# ②レベルを設定する
fixed_bins = (1, 2, 3, 4)
level_values = to_levels_fixed(daily_series.values, bins=fixed_bins)
level_series = pd.Series(level_values, index=daily_series.index)

# ②の出力を確認
print(level_series)

# ③カレンダーグリッドへ変換                    # ここから追加
grid, num_weeks, start_monday = build_calendar_grid(level_series, year)

# ③の出力を確認
print(grid)
print(num_weeks)
print(start_monday)                         # ここまで追加

このコードを実行して得られる結果はこちらです。

gridは2次元の行列で月曜から日曜まで、曜日ごとに行が分かれています。そして列方向が週です。今回使用したサンプルデータは土日だけ時間が書かれたExcelファイルを使っているので、土日だけレベル分けされた数値が入っていることがわかります。

num_weeksは週の合計、start_mondayはこのデータ範囲における最初の月曜日の日付です。

[[0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
  0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
 [0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
  0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
 [0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
  0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
 [0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
  0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
 [0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
  0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
 [1 3 4 2 4 2 2 2 4 4 4 4 4 3 3 3 3 4 2 2 3 2 2 2 1 1 4 3 2 3 4 4 3 4 3 4
  2 0 3 3 2 3 3 4 4 4 3 3 1 1 1 2 0]
 [2 4 4 3 3 3 2 4 4 4 2 4 4 1 3 3 3 2 1 3 3 2 1 2 1 4 3 2 3 3 4 3 4 4 3 3
  4 2 4 2 4 2 4 4 4 4 4 2 1 1 1 2 0]]
53
2024-12-30 00:00:00
POINT
37行目〜57行目:グリッドデータの作成
startendで指定した年の範囲をタイムスタンプで取得します。タイムスタンプにすることで演算が可能になります。また、start.weekday()は月曜が0、火曜が1...日曜が6になります。
 
pd.Timedelta(days=start.weekday())で月曜からの経過日数を表現しており、startから引くことでその週の月曜日を算出しています。
 
例えば、2025年の1月1日は水曜日なのでstart.weekday()の計算で2となります。2025年1月1日から2日間を引くと、その週の月曜日2024年12月30日が得られます。似たような計算でend_sundayはその年の最終日週の日曜日を計算しています。
 
pd.date_rangeは得られた開始日と終了日の間を頻度Dayfreq="D"で埋めることで、その期間内の全ての日付を取得しています。 後は週の数を計算し、グラフにプロットするためのグリッドデータをforループで作成します。

■グラフにプロットする

それでは最後の仕上げです。カレンダーヒートマップを描画するため、matplotlibをimportして、draw_calendar関数を追加、メイン文から呼び出して実行してみましょう。

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt                 # 追加
from matplotlib.colors import ListedColormap    # 追加

def load_daily_series(excel_path, year):
    """Excelから日付と値を読み込み、指定年の全日Seriesにそろえて返す関数"""
    省略
    return series, year

def to_levels_fixed(values, bins):
    """数値配列を固定しきい値に基づき0〜4レベルに変換する関数"""
    省略    
    return levels.astype(int)

def build_calendar_grid(level_series, year):
    """レベルSeriesをカレンダー形式の週×曜日グリッドに変換する関数"""
    省略
    return grid, num_weeks, start_monday

def draw_calendar(grid, save_path=None):        # ここから追加
    """週×曜日グリッドを、カレンダーヒートマップとして描画する関数"""

    # 0〜4のレベルを5色で描くシンプルなカラーマップ
    cmap = ListedColormap([
        "#ffffff",  # 0: no activity
        "#9be9a8",  # 1
        "#40c463",  # 2
        "#30a14e",  # 3
        "#216e39",  # 4
    ])

    fig, ax = plt.subplots(figsize=(10, 3))

    im = ax.imshow(
        grid,
        cmap=cmap,
        aspect="auto",
        interpolation="none",
        origin="upper"
    )

    # y軸に曜日ラベルだけ出す
    ax.set_yticks(np.arange(7))
    ax.set_yticklabels(["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"])
    plt.tight_layout()

    # 画像保存
    fig.savefig(save_path, dpi=200, bbox_inches="tight")

    plt.show()                                    # ここまで追加


# ファイルパスと年の設定
excel_path = "record_data_calendar.xlsx"
year = 2025

省略

# ④描画&保存                           # ここから追加
save_path = "calendar_heat_map.png"
draw_calendar(grid, save_path)         # ここまで追加

このコードを実行することで次の画像がプロジェクトフォルダ直下に保存されます。

すでにお気づきの方が多いと思いますが、この図はこの記事の冒頭で載せていた図とは異なります。まずは最初なので、できるだけシンプルな表示方法をさせてみました。

それでは少し見た目にこだわってみましょう。次のコードの追加・変更部分を反映してください。

微妙な位置や色の調整が多いので少々複雑なコードになっていますが、すべて覚える必要はありません。これくらいの調整であれば近年の生成AIに任せることも可能です。

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.colors import ListedColormap
from matplotlib.patches import Rectangle            # 追加

def load_daily_series(excel_path, year):
    """Excelから日付と値を読み込み、指定年の全日Seriesにそろえて返す関数"""
    省略
    return series, year

def to_levels_fixed(values, bins):
    """数値配列を固定しきい値に基づき0〜4レベルに変換する関数"""
    省略
    return levels.astype(int)

def build_calendar_grid(level_series, year):
    """レベルSeriesをカレンダー形式の週×曜日グリッドに変換する関数"""
    省略
    return grid, num_weeks, start_monday

def draw_calendar(grid, num_weeks, year, start_monday, save_path):    # ここから差し替え
    """週×曜日グリッドをカレンダーヒートマップとして描画・保存する関数"""

    # フォント設定
    plt.rcParams["font.family"] = "Times New Roman"

    # セルの色設定とサイズ
    cmap = ListedColormap(["#ffffff", "#9be9a8", "#40c463", "#30a14e", "#216e39"])
    edge_color = "#d0d7de"
    edge_width = 0.8
    cell_pad = 0.18

    # matplotlibのfig準備
    fig_w, fig_h = 10, 3
    fig, ax = plt.subplots(figsize=(fig_w, fig_h))

    # 0〜7 がデータ、7〜9 を Legend 用にするために余白を設ける
    ax.set_xlim(0, num_weeks)
    ax.set_ylim(9.0, -1.4)

    for sp in ax.spines.values():
        sp.set_visible(False)
    ax.set_xticks([])

    # 曜日ラベル
    yticks = [i + 0.5 for i in range(7)]
    ax.set_yticks(yticks)
    ax.set_yticklabels(["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"])

    # セル描画
    for dow in range(7):
        for w in range(num_weeks):
            lvl = int(np.clip(grid[dow, w], 0, 4))
            x = w + cell_pad / 2
            y = dow + cell_pad / 2
            size = 1.0 - cell_pad
            ax.add_patch(Rectangle(
                (x, y), size, size,
                facecolor=cmap.colors[lvl],
                edgecolor=edge_color,
                linewidth=edge_width,
                joinstyle="miter"
            ))

    # 月ラベル(上部)
    for m in range(1, 13):
        mfirst = pd.Timestamp(year=year, month=m, day=1)
        w = ((mfirst - start_monday).days) // 7
        if 0 <= w < num_weeks:
            ax.text(
                w + 0.5, -0.55, mfirst.strftime("%b"),
                ha="center", va="center", fontsize=10, color="#57606a"
            )

    # Legend(Less □□□□ More)
    legend_y = 7.8            # 色四角の高さ
    legend_text_y = legend_y  # テキストも同じ高さにして横並び
    box_w, box_h = 0.8, 0.8
    gap = 0.25

    text_gap_left = 0.6
    text_gap_right = 0.6
    less_est = 2.0
    more_est = 2.0

    boxes_width = 5 * box_w + 4 * gap
    legend_total = less_est + text_gap_left + boxes_width + text_gap_right + more_est

    legend_x0 = max(0, num_weeks - legend_total - 0.5)

    # Less テキスト
    less_x = legend_x0 + less_est
    ax.text(
        less_x, legend_text_y, "Less",
        ha="right", va="center", fontsize=10, color="#57606a"
    )

    # 色四角
    first_box_x = less_x + text_gap_left
    for i in range(5):
        bx = first_box_x + i * (box_w + gap)
        by = legend_y - box_h / 2
        ax.add_patch(Rectangle(
            (bx, by), box_w, box_h,
            facecolor=cmap.colors[i],
            edgecolor=edge_color,
            linewidth=edge_width
        ))

    # More テキスト
    more_x = first_box_x + boxes_width + text_gap_right
    ax.text(
        more_x, legend_text_y, "More",
        ha="left", va="center", fontsize=10, color="#57606a"
    )

    plt.tight_layout()

    # 保存
    fig.savefig(save_path, dpi=200, bbox_inches="tight")

    plt.show()                               # ここまで差し替え

# ファイルパスと年の設定
excel_path = "record_data_calendar.xlsx"
year = 2025

省略

# ④描画&保存
save_path = "calendar_heat_map.png"
draw_calendar(grid, num_weeks, year, start_monday, save_path)    # 変更

このコードを実行することで、冒頭の画像が保存されるようになります。

ちなみに、cmapを次のように変更すると赤系のヒートマップになります。

cmap = ListedColormap(["#ffffff", "#ff9999", "#ff4d4d", "#cc0000", "#660000"])

5. 全体のコードはこちらから

この記事ではExcelに記録された日々の頑張りを、カレンダーに対応させて可視化させるカレンダーヒートマップの描き方を紹介しました。

カレンダーヒートマップを描けるようになると、自分や会社の記録データを見てただモチベーションを上げるためだけにとどまりません。例えば、ソフトウェアの使用をユーザーごとに管理している場合、各ユーザーがどのようなログイン状況にあるかもカレンダーヒートマップを見れば一目瞭然です。
ぜひ、ご自身の環境における活用方法を試してみてください。

最後に、この記事で紹介した全体のコードを載せます。パソコンで開ける方はぜひコピペして遊んでみてください。

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.colors import ListedColormap
from matplotlib.patches import Rectangle

def load_daily_series(excel_path, year):
    """Excelから日付と値を読み込み、指定年の全日Seriesにそろえて返す関数"""

    # Excelに書いたヘッダー名
    date_col = "日付"
    value_col = "値"

    # PandasでExcelを開く
    df = pd.read_excel(excel_path)
    df[date_col] = pd.to_datetime(df[date_col])
    df[value_col] = pd.to_numeric(df[value_col]).fillna(0)
    df = df.dropna(subset=[date_col])

    # 1年間のデータを作成(欠けている日があった場合に0で埋める)
    day_index = pd.date_range(f"{year}-01-01", f"{year}-12-31", freq="D")
    series = (
        df.set_index(date_col)[value_col]
        .reindex(day_index)
        .fillna(0)
    )

    return series, year

def to_levels_fixed(values, bins):
    """数値配列を固定しきい値に基づき0〜4レベルに変換する関数"""

    v = np.asarray(values, float)
    bounds = [0.0] + list(bins)
    idx = np.digitize(v, bounds, right=True)
    levels = np.clip(idx, 0, 4)

    return levels.astype(int)

def build_calendar_grid(level_series, year):
    """レベルSeriesをカレンダー形式の週×曜日グリッドに変換する関数"""

    start = pd.Timestamp(f"{year}-01-01")
    end = pd.Timestamp(f"{year}-12-31")

    start_monday = start - pd.Timedelta(days=start.weekday())
    end_sunday = end + pd.Timedelta(days=(6 - end.weekday()))

    all_days = pd.date_range(start_monday, end_sunday, freq="D")
    num_weeks = len(all_days) // 7

    # 行=Mon..Sun, 列=週
    grid = np.zeros((7, num_weeks), int) 

    for i, d in enumerate(all_days):
        week = i // 7
        dow = d.weekday()  # 0=Mon..6=Sun
        grid[dow, week] = int(level_series.get(d, 0))

    return grid, num_weeks, start_monday

def draw_calendar(grid, num_weeks, year, start_monday, save_path):
    """週×曜日グリッドをカレンダーヒートマップとして描画・保存する関数"""

    # フォント設定
    plt.rcParams["font.family"] = "Times New Roman"

    # セルの色設定とサイズ
    cmap = ListedColormap(["#ffffff", "#9be9a8", "#40c463", "#30a14e", "#216e39"])
    edge_color = "#d0d7de"
    edge_width = 0.8
    cell_pad = 0.18

    # matplotlibのfig準備
    fig_w, fig_h = 10, 3
    fig, ax = plt.subplots(figsize=(fig_w, fig_h))

    # 0〜7 がデータ、7〜9 を Legend 用にするために余白を設ける
    ax.set_xlim(0, num_weeks)
    ax.set_ylim(9.0, -1.4)

    for sp in ax.spines.values():
        sp.set_visible(False)
    ax.set_xticks([])

    # 曜日ラベル
    yticks = [i + 0.5 for i in range(7)]
    ax.set_yticks(yticks)
    ax.set_yticklabels(["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"])

    # セル描画
    for dow in range(7):
        for w in range(num_weeks):
            lvl = int(np.clip(grid[dow, w], 0, 4))
            x = w + cell_pad / 2
            y = dow + cell_pad / 2
            size = 1.0 - cell_pad
            ax.add_patch(Rectangle(
                (x, y), size, size,
                facecolor=cmap.colors[lvl],
                edgecolor=edge_color,
                linewidth=edge_width,
                joinstyle="miter"
            ))

    # 月ラベル(上部)
    for m in range(1, 13):
        mfirst = pd.Timestamp(year=year, month=m, day=1)
        w = ((mfirst - start_monday).days) // 7
        if 0 <= w < num_weeks:
            ax.text(
                w + 0.5, -0.55, mfirst.strftime("%b"),
                ha="center", va="center", fontsize=10, color="#57606a"
            )

    # Legend(Less □□□□ More)
    legend_y = 7.8            # 色四角の高さ
    legend_text_y = legend_y  # テキストも同じ高さにして横並び
    box_w, box_h = 0.8, 0.8
    gap = 0.25

    text_gap_left = 0.6
    text_gap_right = 0.6
    less_est = 2.0
    more_est = 2.0

    boxes_width = 5 * box_w + 4 * gap
    legend_total = less_est + text_gap_left + boxes_width + text_gap_right + more_est

    legend_x0 = max(0, num_weeks - legend_total - 0.5)

    # Less テキスト
    less_x = legend_x0 + less_est
    ax.text(
        less_x, legend_text_y, "Less",
        ha="right", va="center", fontsize=10, color="#57606a"
    )

    # 色四角
    first_box_x = less_x + text_gap_left
    for i in range(5):
        bx = first_box_x + i * (box_w + gap)
        by = legend_y - box_h / 2
        ax.add_patch(Rectangle(
            (bx, by), box_w, box_h,
            facecolor=cmap.colors[i],
            edgecolor=edge_color,
            linewidth=edge_width
        ))

    # More テキスト
    more_x = first_box_x + boxes_width + text_gap_right
    ax.text(
        more_x, legend_text_y, "More",
        ha="left", va="center", fontsize=10, color="#57606a"
    )

    plt.tight_layout()

    # 保存
    fig.savefig(save_path, dpi=200, bbox_inches="tight")

    plt.show()

# ファイルパスと年の設定
excel_path = "record_data_calendar.xlsx"
year = 2025

# ①日次データを読み込み
daily_series, year = load_daily_series(excel_path, year)

# ①の出力を確認
print(daily_series)
print(year)

# ②レベルを設定する
fixed_bins = (1, 2, 3, 4)
level_values = to_levels_fixed(daily_series.values, bins=fixed_bins)
level_series = pd.Series(level_values, index=daily_series.index)

# ②の出力を確認
print(level_series)

# ③カレンダーグリッドへ変換
grid, num_weeks, start_monday = build_calendar_grid(level_series, year)

# ③の出力を確認
print(grid)
print(num_weeks)
print(start_monday)

# ④描画&保存
save_path = "calendar_heat_map.png"
draw_calendar(grid, num_weeks, year, start_monday, save_path)
 
【筆者】 watさん
メーカー勤務機械系エンジニア。WATLABブログ運営者。工学計算に関する知識の習得を目指し、Pythonの学習を2019年からはじめる。仕事以外にも、趣味のプログラミングやPythonコミュニティへの参加を行っている。また、月間数万PVのPythonブログ「WATLAB」を立ち上げ、初心者向けに図を多くしたわかりやすい記事を作成・公開している。著書は『いきなりプログラミング Python』(翔泳社)。
ブログ:https://watlab-blog.com/

※本記事に記載されている会社名、製品名はそれぞれ各社の商標および登録商標です。
※本稿に記載されている情報は2025年11月時点のものです。

リクルートスタッフィング