43号線を西へ東へ

フリーランスの備忘録、アウトプットの実験場

Pythonでグラフを作成:ランニング統計情報

このブログはプロモーションリンクを含むときがあります


Pythonコードのアップデートをしたので、ランニングデータの集計と分析方法についてまとめておきます。

長らく走っていなかったメタボのアラフィフおじさんのランニング記録です。走り始めて79日、走った回数は32回になりました。

歩いているのか走っているのか、微妙なデータが並ぶことをご了承ください。

データ収集から分析まで

Apple Watchで収集したランニングデータを、iPhoneからCSVエクスポートして、PCのPythonで集計しています。

flowchart TB
  node_1["走る"]
  node_2["iPhoneフィットネスアプリ"]
  node_3["Runmeter"]
  node_4["OneDrive"]
  node_5["Jupter Lab"]
  node_6["グラフ作成"]
  node_7["Googleカレンダー"]
  node_1 =="Apple Watchで計測"==> node_2
  node_2 =="インポート"==> node_3
  node_3 =="統計情報をエクスポート"==> node_4
  node_4 ==> node_5
  node_5 =="CSVに統合・グラフ作成"==> node_6
  node_3 =="カレンダー同期"==> node_7

週/月ごとの下記のデータを継続的に追っています。

  • 合計距離
  • 平均距離
  • ランニング時間

関連性を見るための二軸性のグラフはiPhoneのアプリでは表示できないので、Pythonでグラフを作成しています。

Jupyter Lab(Python)でグラフと表を作成

Pythonのコード作成はChatGPTを使用しています。使いながら少しづつ改変しています。今回は下記の変更を行いました。

  • 作成したグラフのPNG書き出し
  • 週間・月間の分析を同時に実施
  • データをMarkdownの表形式でテキストファイルに書き出し

Jupyter Lab画面

ランニングのデータを月ごと・週ごとにグラフ化・テーブル化するPythonコード

import pandas as pd
import matplotlib.pyplot as plt
from matplotlib.font_manager import FontProperties
import os
from datetime import datetime
from tabulate import tabulate

# CSVファイルのパス
file_path = 'your_data_path/run2024.csv'

# 日本語フォントのパスを設定
font_path = 'your_font_path/ipaexg.ttf'
japanese_font = FontProperties(fname=font_path)

# データの読み込み
running_data = pd.read_csv(file_path)

# 重複行を削除する前の行数
initial_row_count = len(running_data)

# 重複行を削除する
running_data.drop_duplicates(inplace=True)

# 重複行を削除した後の行数
final_row_count = len(running_data)

# 重複行が削除されたか確認
if final_row_count < initial_row_count:
    # 現在のタイムスタンプを取得
    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")

    # バックアップファイルのパスを作成
    backup_file_path = f"{file_path.rsplit('.', 1)[0]}_{timestamp}.csv"

    # 元のCSVファイルをバックアップファイルにコピー
    os.rename(file_path, backup_file_path)

    # 新しいCSVファイルとして保存
    running_data.to_csv(file_path, index=False)
    print(f"重複行を削除し、新しいファイルを作成しました。元のファイルは {backup_file_path} にバックアップされました。")
else:
    print("重複行は見つかりませんでした。バックアップファイルは作成されません。")

# 時間列をdatetime形式に変換してインデックスに設定
running_data['時間'] = pd.to_datetime(running_data['時間'])
running_data.set_index('時間', inplace=True)

# ランタイムを秒から分に変換し、1kmあたりのペースを計算
running_data['ランタイム (分)'] = running_data['ランタイム (秒)'] / 60
running_data['ペース (分/km)'] = running_data['ランタイム (分)'] / running_data['距離 (km)']

# 週単位の分析
running_data['ISO週番号'] = running_data.index.isocalendar().week
weekly_data = running_data.groupby('ISO週番号').agg({
    'ペース (分/km)': 'mean',
    '距離 (km)': ['sum', 'count']
})
weekly_data.columns = ['週間ペース (分/km)', '週間総走行距離', '週間ワークアウト回数']
weekly_data['週間平均走行距離'] = weekly_data['週間総走行距離'] / weekly_data['週間ワークアウト回数']

# 月単位の分析
running_data['年月'] = running_data.index.to_period('M')
monthly_data = running_data.groupby('年月').agg({
    'ペース (分/km)': 'mean',
    '距離 (km)': ['sum', 'count']
})
monthly_data.columns = ['月間ペース (分/km)', '月間総走行距離', '月間ワークアウト回数']
monthly_data['月間平均走行距離'] = monthly_data['月間総走行距離'] / monthly_data['月間ワークアウト回数']

# 日本語フォントの設定
plt.rcParams['font.family'] = japanese_font.get_name()

# 週単位のグラフ作成
fig, ax1 = plt.subplots(figsize=(16, 9))
ax1.set_xlabel('ISO週番号', fontproperties=japanese_font)
ax1.set_ylabel('走行距離 (km)', fontproperties=japanese_font)
ax1.bar(weekly_data.index, weekly_data['週間総走行距離'], color='lightblue', width=0.4, label='週間総走行距離', align='center', zorder=1)
ax1.bar(weekly_data.index + 0.4, weekly_data['週間平均走行距離'], color='green', width=0.4, label='週間平均走行距離', align='center', zorder=2)
ax1.tick_params(axis='x', labelsize=8)
ax1.tick_params(axis='y')
ax2 = ax1.twinx()
ax2.set_ylabel('週間ペース (分/km)', fontproperties=japanese_font)
ax2.plot(weekly_data.index, weekly_data['週間ペース (分/km)'], color='red', label='週間ペース (分/km)', zorder=3)
ax2.tick_params(axis='y', labelcolor='red')
fig.legend(loc='upper left', bbox_to_anchor=(0.1, 0.9), prop=japanese_font)
plt.title('ISO週番号別 ランニングパフォーマンス', fontproperties=japanese_font)
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
weekly_output_path = f"WeeklyAnalysis-{timestamp}.png"
plt.savefig(weekly_output_path)
plt.show()

# 月単位のグラフ作成
fig, ax1 = plt.subplots(figsize=(16, 9))
bar_width = 0.35
months = range(len(monthly_data))
bars1 = ax1.bar(months, monthly_data['月間総走行距離'], width=bar_width, color='lightblue', label='月間総走行距離', align='center')
bars2 = ax1.bar([x + bar_width for x in months], monthly_data['月間平均走行距離'], width=bar_width, color='green', label='月間平均走行距離', align='center')
for bar in bars1:
    yval = bar.get_height()
    ax1.text(bar.get_x() + bar.get_width()/2, yval + 0.5, round(yval, 1), va='bottom', ha='center', fontsize=12, color='black')
for bar in bars2:
    yval = bar.get_height()
    ax1.text(bar.get_x() + bar.get_width()/2, yval + 0.5, round(yval, 1), va='bottom', ha='center', fontsize=12, color='black')
month_labels = [f'{m+1}月' for m in monthly_data.index.month - 1]
ax1.set_xticks([x + bar_width / 2 for x in months])
ax1.set_xticklabels(month_labels)
ax1.set_xlabel('月', fontproperties=japanese_font)
ax1.set_ylabel('走行距離 (km)', fontproperties=japanese_font)
ax1.tick_params(axis='x', labelsize=8)
ax1.tick_params(axis='y')
ax2 = ax1.twinx()
ax2.set_ylabel('月間ペース (分/km)', fontproperties=japanese_font)
ax2.plot(months, monthly_data['月間ペース (分/km)'], color='red', label='月間ペース (分/km)')
ax2.tick_params(axis='y', labelcolor='red')
fig.legend(loc='upper left', bbox_to_anchor=(0.1, 0.9), prop=japanese_font)
plt.title('年月別 ランニングパフォーマンス', fontproperties=japanese_font)
monthly_output_path = f"MonthlyAnalysis-{timestamp}.png"
plt.savefig(monthly_output_path)
plt.show()

# Markdownのコード形式で表をテキストファイルに書き出し
weekly_markdown = tabulate(weekly_data, headers='keys', tablefmt='pipe', showindex=True)
monthly_markdown = tabulate(monthly_data, headers='keys', tablefmt='pipe', showindex=True)
markdown_output_path = f"Markdown-table-{timestamp}.txt"

with open(markdown_output_path, 'w', encoding='utf-8') as f:
    f.write("### 週ごとのランニングデータ\n")
    f.write(weekly_markdown)
    f.write("\n\n### 月ごとのランニングデータ\n")
    f.write(monthly_markdown)

print("\n### 週ごとのランニングデータ\n")
print(weekly_markdown)

print("\n### 月ごとのランニングデータ\n")
print(monthly_markdown)

print(f"週単位のグラフは {weekly_output_path} に保存されました。")
print(f"月単位のグラフは {monthly_output_path} に保存されました。")
print(f"Markdown形式の表は {markdown_output_path} に保存されました。")

作成したグラフ

データを蓄積させて、Pythonのファイルを実行すれば自動的に下記のグラフが作成されます。

週ごとのグラフ

月ごとのグラフ

Markdownで表を作成

こちらもPythonコードを実行すれば、自動的にテキストファイルに書き出すように変更しました。

週ごとのランニングデータ

ISO週番号 週間ペース (分/km) 週間総走行距離 週間ワークアウト回数 週間平均走行距離
9 8.24618 1.09 1 1.09
11 10.5731 1.55 1 1.55
12 8.54601 1.92 1 1.92
13 8.91069 2.09 1 2.09
14 9.27627 1.84 1 1.84
15 9.65168 4.5 3 1.5
16 8.66516 9.15 5 1.83
17 8.37914 11.94 6 1.99
18 8.43324 14.97 6 2.495
19 8.61574 12.35 5 2.47
20 8.20327 5.24 2 2.62

月ごとのランニングデータ

年月 月間ペース (分/km) 月間総走行距離 月間ワークアウト回数 月間平均走行距離
2024-02 8.24618 1.09 1 1.09
2024-03 9.34327 5.56 3 1.85333
2024-04 8.7799 32.26 17 1.89765
2024-05 8.42348 27.73 11 2.52091

現状

心拍数をコントロールする必要がなくなった

3~4週前は走る距離を伸ばすために心拍数を上げすぎないように走っていました。息が上がってしんどかったからですが、この2週間は息が上がるよりも、ふくらはぎの張りの有無がランニングの距離とペースを左右するようになっている。

上記のグラフでは折れ線グラフのランニングペースが早くなっているものの、心拍数(最大平均)も下がっている。客観的にも心肺機能への負担が減っているが、筋肉の張りのようなランニングフォーム由来の問題が大きくなってきた。

走り方を考えよう

今後は負担が少ないランニングフォームを考えていきたいと思います。

まとめ

ランニングのログを可視化するためのPythonコードをアップデートしました。1コード1機能で運用していましたが、機能も固まってきたので複数の機能を統合したコードに変更しました。

ちょうどChatGPTもGPT-4oにバージョンアップしたところで、長いコードも途切れることなく作ってくれるようになりました。

遅々としているもののランニング能力は改善してています。ただ体重がほとんど変わりません。これは10年前のマラソンの時もそうでした。

これまでは食いしん坊のせいかと思っていましたが、昨日男性ホルモン(テストステロン)の分泌量が人よりも少ない可能性が出てきました。男性ホルモンが減ると太りやすくなるそうです。

男性ホルモン走ることに加えて、筋トレも組み合わせていきたいですが、ブログにランニング、トレーニングまで追加すると時間が問題になりますね。