1. はじめに:市民ランナーが直面する「データ死蔵」と「一般論AI」の限界
以前、Qiitaに「マラソン大会のGPXデータをGitHubで公開」という記事を投稿しました。
その後、改めて感じたことがあります。
市民ランナーにとって最も重要で、最も難しいテーマは、単に「速く走ること」ではありません。
本当に難しいのは、次の2つです。
- 怪我を予防すること
- 日々の疲労状態を見ながら、練習量を絶妙に調整すること
いわゆる「匙加減」です。
現在、多くのランナーがGarmin、COROS、Polar、Apple Watchなどの高性能GPSウォッチを使い、ランニングデータをGarmin ConnectやStravaに同期しています。
また、生成AIの普及により、ChatGPTやGeminiに、
目標達成のための3ヶ月練習プランを作ってください
と相談しているランナーも増えてきました。
しかし、実際に使ってみると、そこには大きな壁がありました。
課題1:せっかくの客観ログがLLMに渡っていない
手元には、距離、ペース、心拍数、時間、獲得標高などのリッチなデータが毎日蓄積されています。
しかし、LLMに相談するときは、
今日は10km走りました。少し疲れています。
と手入力するだけになりがちです。
これでは、せっかく蓄積されたランニングログの価値が活かされません。
課題2:過去の実績が文脈化されていない
AIにその日の相談をしても、過去数週間の走行距離、疲労の蓄積、ポイント練習の頻度、休養日の取り方などが文脈として渡っていなければ、返ってくるのは一般論になりがちです。
疲れているなら休みましょう
無理せずジョグにしましょう
週末にロング走を入れましょう
正しいけれど、自分専用ではない。
そこに限界を感じました。
課題3:リアルな専門家や生活制約が反映されない
市民ランナーの練習は、机上の理想プランだけでは回りません。
私たちの周りには、フォームを見てくれるクラブチームの監督やコーチ、身体をケアしてくれる整骨院の先生、練習仲間、ペーサーがいます。
さらに、仕事の繁忙期、出張、旅行、家族予定、怪我の違和感、天候といった現実の制約もあります。
AIがどれだけ美しいプランを出しても、それが現実の生活や身体の状態に合わなければ実行できません。
そこで私は、
Stravaの客観ログ × 今の主観コンディション × レースや生活制約 × AI相談
を統合し、AIと一緒に練習判断を育てる仕組みとして、
Strava連携AIランニングカルテ
を作りました。
コンセプトは、
自由に走って、強くなる。
です。
2. 私のバックグラウンド:コードは書けない。仕様を書き、テストして、走る。
少し、私自身のバックグラウンドをお話しします。
普段の私は、前職では地理空間データプラットフォームや、現職ではエンタープライズ向けのデータストレージ / データインフラ領域で、アライアンス構築や戦略的パートナーシップに関わる仕事をしています。
位置情報、API、データ活用、インフラの世界には長く関わってきました。
ただし、私は一般的なIT知識はあるものの、いわゆる「ガリガリとコードを書けるエンジニア」ではありません。
それでも、今回のシステムを作ることができました。
なぜか。
それは、AI時代の開発において重要なのは、必ずしも最初からコードを書けることだけではないからです。
重要なのは、
- 何を解決したいのか
- どんなデータを使うのか
- どんな操作体験にしたいのか
- どこで人間が判断すべきなのか
- どこまでAIに任せ、どこから人間が引き取るのか
を具体的に設計することです。
私はその仕様を書き、実際に走ってテストし、エラーを見つけ、また直す、という流れを繰り返しました。
GeminiとChatGPTによるペアプロ開発
今回の開発では、2つのLLMを役割分担して使いました。
| 役割 | 主に使ったLLM | 内容 |
|---|---|---|
| 仕様整理・初稿コード作成 | Gemini in Canvas | 全体構造、シート設計、GAS初稿作成 |
| レビュー・バグ修正・リファクタリング | ChatGPT | GASの実行時エラー確認、列ズレ、スマホQOL、JSONサニタイズ、設計整理 |
Geminiで大きな骨格を作り、ChatGPTで細かいバグや設計の粗さを潰していく。
この「2つのLLMを相手にしたペアプロ」によって、コードを書けない私でも、Google Apps Scriptを使った実用的なシステムを構築できました。
3. なぜアプリではなく「Googleスプレッドシート × 自由なLLM」なのか?
世の中には、AIを使ったランニングアプリや、トレーニング管理アプリがたくさんあります。
それらは便利です。
ただし、多くは固定されたクローズドな仕組みです。
- どのAIモデルが使われているかわからない
- 自分でプロンプトを調整できない
- 自分のデータがどこに保存されているかわかりにくい
- アプリの思想に合わせて、自分の練習管理を変える必要がある
- コーチや自分の感覚を柔軟に差し込めない
私が作りたかったのは、そういう「閉じたAIアプリ」ではありません。
もっと自由で、もっと自分の手元にあり、もっと人間の判断を大切にする仕組みです。
LLMモデルを自由に選べる
このシステムでは、AIモデルを固定しません。
Googleスプレッドシート上でAI相談用プロンプトを生成し、それを自分の好きなLLMに投げます。
例えば、
- ChatGPT
- Gemini
- Claude
- その他のLLM
どれを使ってもよい。
さらに、納得がいかなければ、そのままチャット上で壁打ちできます。
この提案は少し攻めすぎでは?
週末は出張があるので調整してください
コーチに相談するなら、どこを論点にすべきですか?
もう少し守りのプランにしてください
このように、AIアプリの固定画面ではなく、LLMとの自然な対話をそのまま練習計画に接続できます。
データを自分で管理できる
ランニングログは、単なる運動データではありません。
心拍、疲労、体調、怪我の兆候、生活リズム、レース目標。
かなり個人的なライフログです。
だからこそ、自分のデータをどこに置くかは重要です。
このシステムでは、データは自分のGoogleドライブ上のスプレッドシートに保存されます。
つまり、
自分のデータを、自分の管理下に置く
という形にできます。
これは、いわばランニングデータにおけるデータ・ソブリンティです。
無料枠で運用できる
この仕組みは、基本的に以下で構成されています。
- Googleスプレッドシート
- Google Apps Script
- Strava API
- 任意のLLM
専用サーバーは不要です。
月額課金のバックエンドも不要です。
GoogleアカウントとStravaがあれば、かなりの部分を無料枠で運用できます。
もちろん本格的に大人数で運用するなら、API制限や権限管理、データ保護設計を考える必要があります。
しかし、個人やチーム内の検証であれば、十分に現実的な構成です。
AIは支配者ではなく、補助ヘルパー
このシステムで一番大事にしているのは、AIとの距離感です。
AIにカレンダーを支配させません。
AIが勝手にTraining_Planを書き換えることもありません。
AIは提案するだけです。
最終的には、人間がDashboardで確認し、必要ならAI案をコピーし、微修正してから、Training_Planへ反映します。
つまり、
AIは副コーチ。
正本を更新するのは人間。
という思想です。
これが、Strava連携AIランニングカルテの一番大事な設計思想です。
4. なぜGarmin直接接続ではなく、Stravaをデータハブにしたのか?
当初は、GarminユーザーなのだからGarmin Connectから直接データを取るのが自然だと考えていました。
しかし、調べていくと、個人開発で扱うにはハードルが高いことがわかりました。
Garminの公式APIは、一般個人が気軽にOAuth連携してアクティビティデータを取得する用途には向いていません。
一方で、周囲のランナーを見ると、使っているデバイスはバラバラです。
- Garmin
- COROS
- Polar
- Apple Watch
- Suunto
など、さまざまです。
そこで、メーカーごとのAPIに依存するのではなく、Stravaをデータハブにする方針にしました。
多くのGPSウォッチはStravaに自動同期できます。
つまり、
各種GPSウォッチ
↓
Strava
↓
Google Apps Script
↓
Googleスプレッドシート
↓
LLM相談
という流れにすれば、デバイス依存をかなり減らせます。
Strava API連携部分では、Qiita上の先行記事も参考にしました。(感謝)
-StravaのAPIにアクセスしてアカウント情報を取得する
OAuth2.0認証、リフレッシュトークン、アクティビティ取得などをGoogle Apps Script上に組み込み、スプレッドシートへRunアクティビティを同期する構成にしています。
5. 全体アーキテクチャ
Strava連携AIランニングカルテは、以下のような構成です。
[GPS Watch]
Garmin / COROS / Apple Watch など
│
▼
[Strava]
│ Strava API
▼
[Google Apps Script]
│
▼
[Google Sheets]
- Dashboard
- Training_Plan
- Training_Master
- Daily_Summary
- Race_Calendar
- Generated_Plan
│
▼
[LLM]
ChatGPT / Gemini / Claude など
重要なのは、Googleスプレッドシートを単なる表ではなく、軽量なデータベース兼UIとして使っている点です。
6. シート設計
主なシートの役割は以下です。
| シート名 | 役割 |
|---|---|
| Dashboard | 日常操作の中心。AIプロンプト生成、JSON貼り戻し、AI提案レビュー、現行プラン更新を行う |
| Training_Plan | 直近3ヶ月の正本カレンダー。予定と実績をすべて保持する |
| Training_Master | Training_PlanとStrava実績の突合DB |
| Daily_Summary | 日次集計。総距離、総時間、アクティビティ数、心拍、判定コメントなど |
| Race_Calendar | レース、練習会、出張、旅行、休養など、長期の目標・制約を管理 |
| Generated_Plan | AI提案の一時保存。正本ではない |
| Strava_Activities | Strava APIから取得したアクティビティログ |
7. Human-in-the-Loop:AIに丸投げしない設計
このシステムの基本思想は、
AIに練習を丸投げしない
ことです。
AIはあくまでアドバイザーです。
正本であるTraining_Planを更新するのは、人間です。
Dashboardでは、AI提案と現行プランを横並びで見ます。
現行プラン
↓
AI提案を確認
↓
必要ならAIコピー
↓
人間が微修正
↓
現行プランを更新
↓
Training_Planへ反映
Dashboardのレビュー画面は、12列構成にしています。
| 列 | 内容 |
|---|---|
| A | 日付 |
| B | 現行 種別 |
| C | 現行 メニュー |
| D | 現行 距離 |
| E | 現行 ペース |
| F | AIコピー |
| G | AI種別 |
| H | AIメニュー |
| I | AI距離 |
| J | AIペース |
| K | AI理由 |
| L | 更新対象 |
F列の「AIコピー」は、AI提案を現行欄へコピーするだけです。
Training_Planにはまだ反映されません。
人間がB〜E列を確認・微修正し、L列がONになった行だけを「現行プランを更新」でTraining_Planへ反映します。
8. Dashboardに全部出さない理由
Dashboardには、すべての日程を出しません。
表示対象は以下です。
1. 今日〜14日以内の予定は必ず表示
2. 15〜30日以内は、具体的なAI提案がある日だけ表示
3. 過去日は表示しない
4. 31日以降は表示しない
理由は、スマホで運用するためです。
30日分すべてを毎回表示すると、スクロールが長くなり、判断すべき日が埋もれます。
一方で、直近14日分は常に見たい。
そこで、
直近14日は必ず表示し、15〜30日はAIが触れた日だけ表示
という仕様にしました。
日付順は必ず維持します。
AI提案のある日を最上部に持ち上げると、カレンダーとしての視認性が悪くなるためです。
AI提案ありの日は、日付順の中で背景色によって強調します。
9. Training_Plan:3ヶ月固定の正本カレンダー
Training_Planは、直近3ヶ月の正本カレンダーです。
期間は、
先月1日〜翌月末
です。
例えば今日が2026年6月なら、
2026/05/01〜2026/07/31
を保持します。
Training_Planは1日1行を原則とします。
A〜M列が予定、N〜W列が実績です。
A〜M:予定
N〜W:実績
二部練や複数アクティビティは、Training_Plan上では1日1行に集約します。
実績側では、日次合算と代表Stravaを保持します。
10. Planned IDのライフサイクル管理
Training_Planでは、各予定にPlanned IDを持たせています。
空予定の初期行は、
CAL-yyyyMMdd-1
です。
人間が具体的な予定を書き込むと、
MAN-yyyyMMdd-xxxxxxxx
に自動昇格します。
逆に、予定を消して白紙に戻すと、再びCAL形式に戻します。
この仕組みにより、スプレッドシートを手で触っても、ゴミIDが増え続けることを防げます。
概念的には、以下のような状態遷移です。
空予定
CAL-yyyyMMdd-1
│ 予定を入力
▼
手動予定
MAN-yyyyMMdd-xxxx
│ 予定を削除
▼
空予定
CAL-yyyyMMdd-1
イメージコードです。
function hasRealPlan_(type, menu, pace, dist) {
return (
type && type !== '未定' ||
menu && menu !== '未定' ||
pace && pace !== '-' ||
Number(dist) > 0
);
}
実際のコードでは、Training_Planが直接編集されたときに、予定の中身を見てIDとステータスを自動補正します。
11. Strava実績の突合
Stravaから取得したRunアクティビティは、Training_MasterでTraining_Planと突合します。
主な判定は以下です。
| 状態 | 突合ステータス | 判定コメント |
|---|---|---|
| 予定あり・実績あり | 正常 | 計画通り / 走りすぎ注意 / 距離不足 |
| 空予定・実績あり | 実績のみあり | 予定外の実施 |
| 予定あり・実績なし | 未実施 | 未実施 |
| 取消予定・実績あり | 取消予定の実施 | 取消予定の実施 |
実績はTraining_PlanのN〜W列へ書き戻します。
| 列 | 内容 |
|---|---|
| N | 実績距離 |
| O | 実績時間 |
| P | 実績アクティビティ数 |
| Q | 実績ペース |
| R | 平均心拍 |
| S | 最大心拍 |
| T | 突合ステータス |
| U | 判定コメント |
| V | 代表Strava URL |
| W | 実績更新日時 |
12. 複数アクティビティから代表Stravaを選ぶ
1日に複数のStravaアクティビティがあることがあります。
例えば、
- 朝ジョグ
- 夜のポイント練習
- アップジョグ
- レース本体
- ダウンジョグ
このとき、単純に距離が長いものを代表にすると、本当に負荷の高かった練習が埋もれることがあります。
そこで、代表Stravaは以下の簡易負荷で選びます。
平均心拍 × 実績時間(分)
イメージコードです。
function selectRepresentativeActivity_(activities) {
if (!activities || activities.length === 0) return null;
if (activities.length === 1) return activities[0];
let representative = activities[0];
let maxLoad = -1;
activities.forEach(act => {
const heartRate = act.average_heartrate || 120;
const durationMin = (act.moving_time || 0) / 60;
const load = heartRate * durationMin;
if (load > maxLoad) {
maxLoad = load;
representative = act;
}
});
return representative;
}
厳密なTRIMPではありませんが、日次レビュー用の代表選定としては十分実用的です。
13. 実績分類:Runを全部ジョグ扱いしない
Stravaのtype=Runを、そのまま全部「ジョグ」にしてしまうと、分析が弱くなります。
そこで、以下を組み合わせて実績分類します。
- Stravaの
workout_type - タイトル
- 距離
- 平均心拍
- 最大心拍
- 予定種別
例えば、
| 条件 | 分類 |
|---|---|
| workout_typeがRecovery | リカバリー |
| workout_typeがWorkout | ポイント練習候補 |
| workout_typeがRace | レース |
| 距離15km以上 | ポイント練習候補 |
| 平均心拍145以上かつ30分以上 | ポイント練習候補 |
| 最大心拍170以上 | ポイント練習候補 |
workout_typeは常に入るとは限らないため、補助判定として扱います。
14. Race_Calendarは「長期の地図」
Race_Calendarはレース専用ではありません。
レースだけでなく、練習計画に影響する制約も入れます。
| 列 | 内容 |
|---|---|
| 日付 | イベント日 |
| イベント名 | レース名、出張名、練習会名など |
| イベント種別 | レース / 練習会 / 制約 / 休養 |
| 目標/制約 | 目標タイム、走れない理由、注意点 |
| 優先度 | A / B / C / 未定 |
| メモ | 補足 |
これにより、AIは単なる練習メニューだけでなく、生活上の制約も考慮できます。
15. JSON貼り戻しと自動サニタイズ
LLMの回答は、JSON形式でA13へ貼り戻します。
しかし、LLMの出力は揺らぎます。
- 全角引用符が混じる
- 末尾カンマが入る
- 文字列内に生改行が入る
- 軽微なカンマ抜けがある
そこで、GAS側でsanitizeJson_()を通し、できる範囲で自動修復してからJSON.parse()します。
A13に貼ると、以下が自動で走ります。
A13へJSON貼付
↓
onDashboardEdit
↓
sanitizeJson_
↓
JSON.parse
↓
Generated_Planへ保存
↓
Dashboardへ表示
↓
A13を自動クリア
↓
行高を再固定
16. スマホ運用へのこだわり
このシステムは、PCだけでなくほぼスマホで使う前提です。
そのため、以下をかなり重視しました。
- 操作メニューを上部とレビュー直上の2箇所に置く
- A11はコピー専用セルにする
- A13はJSON貼り戻し専用セルにする
- A13は自動クリアする
- A11/A13の行高を固定する
- Dashboardは12列に絞る
- 表示対象を今日〜14日+AI提案あり30日以内に絞る
毎日使うものなので、UIの小さなストレスを潰すことが重要でした。
17. 実際の運用例
例えば、25kmの距離走を実施した直後に、左シンスプリント(ランニングを繰り返すことで、すねの内側に痛みが生じるスポーツ障害)気味の違和感があったとします。
Dashboardに主観情報として、
25km走は完遂。
大きなダメージはないが、左足のすね周りに少し違和感あり。
水曜のチーム練習をどうするか迷っている。
と入力します。
データ更新後、A11のプロンプトをLLMに貼ります。
AIは、Strava実績、直近14日間の負荷、Training_Plan、Race_Calendar、今の主観コンディションをもとに提案します。
例えば、
6/10の3SHINEチーム練習は、800mインターバルを回避し、
後半の5kmペース走のみ安全に合流する案を推奨。
と出てきます。
Dashboard上でそのAI提案を確認し、F列のAIコピーを押す。
すると、AI案が現行プラン編集欄にコピーされます。
そこで人間が、
5kmだけなら行けそう
ただしペースはBクラス設定に落とそう
と微修正します。
最後に「現行プランを更新」を実行すると、Training_Planへ反映されます。
この流れにより、AIの提案をそのまま鵜呑みにするのではなく、自分の身体感覚とコーチへの相談を前提に、現実的な予定へ落とし込めます。
18. 結論:AIを伴走者にする
Strava連携AIランニングカルテを作って感じたのは、AIは「答えを出す存在」というより、良い問いを立てるための伴走者だということです。
AIがいることで、
- 休む勇気を持てる
- 練習を減らす根拠を持てる
- 監督やコーチに相談する論点が明確になる
- 練習計画の意図が言語化される
- 自分の疲労を客観視できる
ようになります。
そしてもう一つ伝えたいことがあります。
プログラムが書けないからといって、システム開発を諦める必要はありません。
自分の課題を深く理解し、仕様を考え、実際の現場でテストできるなら、GeminiやChatGPTはそれを動く形にしてくれます。
これからの個人開発では、ドメイン知識と仕様設計力がますます重要になるはずです。
まだ一般公開できる段階ではありませんが、チームメンバーで興味がある方々にも協力してもらいながら、今後もテストを続けていきます。
自分のスポーツログとLLMを組み合わせて、自分専用のAIコーチを召喚する。
そんな時代が、もう来ています。
自由に走って、強くなる。
私たちの限界は、まだまだ先にあるはずです。
