佐渡トライアスロン2026の応援トラッカーを作りました。3年目でようやく「使われるもの」になった話

結論から言うと#

佐渡国際トライアスロン2026に合わせて、応援する人のためのライブトラッカー「佐渡トラッカー2026」を作りました。

sado-tracker-2026.teraren.com佐渡トラッカー 2026

佐渡国際トライアスロンの応援トラッカー。ブックマークした選手の現在地、順位、ゴール予想タイムがひと目でわかります。

佐渡トラッカー 2026

私は毎年、大会が近づくと「理想の応援トラッカーとは何か」を考えて、実装しては作り直しています。2024年はフィルタできる一覧を手書きで作り、そのあとAIにも作らせてみました。2025年はトラッカーではなく、レースのリザルト分析ツールを作りました。トラッカーはレースのその日にしか使われませんが、分析ツールなら振り返りでずっと使えるからです。2026年版はその積み重ねの結果です。

正直に言うと、これまでの反応は今ひとつでした。今年は違いました。FacebookとXでたくさんのコメントをいただき、Googleアナリティクスを見ると大会当日だけで992人が使ってくれていました。

この記事では、次の3つを書きます。

  • なぜ作ったのか、何を目指したのか
  • Googleアナリティクスで見た、どのくらいの人がどう使ったか
  • ソフトウェアアーキテクチャと、こだわった工夫。特に、レース1日分を2分で再生して見た目を確かめたリプレイ基盤

過去のトラッカーの話は以下の記事に書いています。

作った意図#

公式トラッカーが答えてくれない3つの質問#

佐渡の公式計測サイトは、各選手の通過タイムをそのまま表示してくれます。データとしては十分ですし、これがあるからこそトラッカーが作れます。

ただ、応援する側が知りたいのは通過タイムそのものではありません。知りたいのは次の3つです。

  1. いまどこにいるのか
  2. 調子はどうなのか。速いのか遅いのか、同じ年代の中で何番目なのか
  3. いつゴールするのか。ゴール地点に何時に行けばいいのか

2024年版のトラッカーは、この1番目だけを少し楽にしたものでした。名前やゼッケンで絞り込めて、ラップタイムが見える。それだけでも応援はだいぶ楽になったのですが、2番目と3番目には答えられていませんでした。

2026年版は、この3つの質問に「数字で」答えることを目標にしました。

もう1つ、作ってみて気づいたことがあります。選手がいまどこで、どんな調子で、いつ帰ってくるかが分かると、応援する人は選手と同じような状況を体感できます。何より一体感が生まれます。ただ待つのではなく、一緒にレースをしている感覚で応援できる。これがこのトラッカーの本当の価値だと思っています。

「数字」を製品にする#

もう1つ決めたことがあります。表示する数字は全部、テストで検証された関数から出す、ということです。

応援している人にとって「ゴール予想20:33」は、その時刻にゴール地点で待つ根拠になります。順位や予想が間違っていたら、そのトラッカーは無い方がましです。なので、正しさを「たぶん合っている」ではなく「公式の値と突き合わせて一致した」で担保することにしました。詳しくはアーキテクチャの章で書きます。

今年のトラッカーでできること#

以下のスクリーンショットは、2025年大会のデータをリプレイモードで再生したものです。選手名はテスト用の架空の名前に置き換えています。

ブックマークした選手をカードで追う#

ブックマークした選手のカード。通過した計測地点、種目ごとのタイムと順位、コース上の推定位置、ゴール予想時刻が1枚に収まっている

ゼッケン番号か名前で選手を探してブックマークすると、カードが並びます。カードには最後に通過した計測地点、種目ごとのタイムと順位、コース上の推定位置、ゴール予想時刻が入っています。

ブックマークはブラウザに保存するだけで、アカウントは要りません。

選手ページで「調子」を見る#

選手ページ。推定位置バー、ゴール予想時刻と予想の幅、総合・男子・エイジの3つの順位、種目別タイム、同じ年代の前後の選手のコース上の位置、順位の推移グラフ

選手ページには、応援する人が「調子はどう?」に答えるための材料をまとめました。

  • 総合・性別・年代の3つの順位。それぞれ「288/719」のように母数付きで表示します
  • 種目ごとのタイム、ペース、順位
  • 同じ年代の前後5人ずつの推定位置
  • 計測地点ごとの順位の推移グラフ

順位を計測地点ごとに出して、その入れ替わりを推移グラフにしたのには理由があります。通過タイムだけを見ていても、その選手がペースを落としているのか、キープできているのかは判断が非常に難しいのです。同じ地点を通過した人の中で何番目かが地点ごとに並ぶと、追い上げているのか、抜かれていっているのかが一目で分かります。

ゴール予想には「?」ボタンがあり、押すと予想の根拠が出ます。何人の過去データから予想したか、その人たちの残りタイムの中央値と四分位、そしてこの計測地点での予想がどのくらい当たるのかのバックテスト結果です。予想は予想でしかないので、信頼していい度合いも一緒に見せるようにしました。

自分で使っていちばん良かったのは「いまどこ」の推定#

自分で実際に使ってみて、いちばん良いと感じたのが選手の現在地を知る機能です。

公式の計測が教えてくれるのは、直前の計測地点を何時に通過したかまでです。そこから先、選手がいまどこを走っているかは、見る側が頭の中で計算するしかありませんでした。

このトラッカーは、通過タイムをそのまま出すのではなく、今の速度を推測して現在地を出します。速度は3段構えです。その選手が直前の区間で出した速度があればそれを使い、まだ無ければ同じタイプの選手たちがその区間で出している速度の中央値、それも無ければ過去大会の中央値の順に当てはめます。最後の通過地点からどこまで進んだかを「バイク 約123.2 / 190 km」のように表示し、時間が経てば数字も点も進みます。

応援する側には、この一行があるだけで「次の地点まであとどのくらいか」「今から移動して間に合うか」が決められます。仕組みの詳細は後半のアーキテクチャの章に書きます。

同じ発想のゴール予想は、必ず「幅」と一緒に#

現在地と同じ考え方で作ったのがゴール予想で、こちらも自分で使っていて素晴らしいと感じた機能です。

予想の元になるのは過去大会のデータです。同じタイプの過去の完走者の中から、その時点までのペースの組み合わせが似ている選手を20人探し、その人たちが同じ計測地点からゴールまでに実際に要した時間の中央値を、いまの経過時間に足します。それがゴール予想時刻です。似た選手が5人に満たないうちは、いまの速度で残り距離を割る単純な外挿に切り替えます。

ただし予想には必ず誤差があります。そこで、予想時刻だけを大きく出すのではなく、「20:33頃」の横に「予想の幅 20:10〜20:54」のように幅を同時に表示しています。「?」を押すと、何人の過去データを使ったか、この計測地点の予想がどのくらい当たるのかも見えます。バイクの途中では幅が広く、ランの後半に入ると狭まっていくので、応援する側は「そろそろゴール地点に移動していい」という判断を、予想の確かさ込みでできます。

全体マップで「どのあたり」を見る#

全体マップ。横軸がコース、縦軸が先頭からの順で、Aタイプ全員の推定位置が点で並ぶ

Aタイプ全員をコース上に並べたビューです。横軸がコース、縦軸は先頭からの順です。計測済みの位置は塗りつぶした点、推定位置は破線の点で描き分けています。

種目別順位と総合トップ#

種目別順位。バイクを走り終えた選手の順位表で、1位との差も出る

種目別順位はA・B・リレーの各タイプで、スイム・バイク・ラン・総合の順位表を出します。年代で絞ることもでき、1位との差が見えます。

このほかに、ブックマークした選手が計測地点を通過したときにベルで知らせる通知、佐渡市の天気予報と相川の実況、X・Facebook・LINEへの共有ボタンがあります。

反応#

これまで作ってきたトラッカーへの反応は、正直なところ今ひとつでした。

今年はXとFacebookに投稿したところ、たくさんのコメントをいただきました。

Googleアナリティクスの参照元を見ると、この反応は数字にも出ています。大会期間中のセッション約2,000のうち、X経由(t.co)が425、Facebook経由が約520、直接アクセスが約900でした。SNSからの流入と、URLを直接開く人がほぼ半々です。

Googleアナリティクスで見た使われ方#

大会前日の9月5日から9月8日までの数字です。アナリティクスを入れたのが大会当日の朝なので、当日より前のアクセスは含まれていません。

指標
アクティブユーザー(期間合計)1,149人
うち大会当日(9月6日)992人
セッション2,054
表示回数21,257
1人あたりの表示回数18.5回
1人あたりの平均エンゲージメント時間3分36秒
エンゲージメント率60.7%

エントリーが約1,900人の大会で、応援する側が当日約1,000人でした。

当初の見込みは、もっと多い数字でした。参加者1人につき応援する人が2人くらいはいるだろうと考えて、ユニークユーザーは2,000人から3,000人を期待していました。結果は約1,000人で、期待の半分から3分の1です。参加者の半数に1人ずつ、と考えると、届いた範囲がどのくらいかが分かります。

当日は朝5時から夜11時まで使われ続けた#

大会当日の時間帯別アクティブユーザー数の棒グラフ。朝5時から増え始め、9時から20時まで100人を超え、13時台の169人が最多

時間帯別に見ると、Aタイプがスタートする6時半の前から人が増え始め、Bタイプがスタートする8時を過ぎると一気に増えて、9時から20時までは毎時100人以上が見ていました。ピークは13時台の169人です。Aタイプの先頭がバイクを終えてランに入り、Bタイプのゴールが続く時間帯なので、納得の分布です。

ほぼ全員がスマートフォン#

端末ユーザー割合平均エンゲージメント時間
スマートフォン1,064人92.6%3分26秒
PC69人6.0%7分01秒
タブレット17人1.5%31秒

9割以上がスマートフォンでした。沿道で使う道具なので当然ではありますが、390px幅を基準にデザインしたのは正解でした。一方でPCの人は1人あたり7分と、スマートフォンの2倍の時間を使っています。自宅のPCで腰を据えて見ていた人がいたのだと思います。

どのページが見られたか#

ページ表示回数ユーザー1人あたりの平均エンゲージメント時間
総合トップ8,4941,005人1分23秒
ブックマーク1,561141人3分34秒
Aタイプ 種目別順位1,21974人1分59秒
全体マップ27870人1分41秒

ほとんどの人は総合トップで名前を検索し、選手ページを開いて閉じる、という使い方でした。選手ページは550以上の別々のページが開かれています。

一方で、ブックマークを使った141人は1人あたり3分34秒とトップページの2倍以上の時間を使っています。ブックマークの追加は255回ありました。この人たちが、私が想定していた「特定の誰かを1日追い続ける応援者」です。全体の1割強でしたが、その人たちには深く使われました。

面白いところでは、ChatGPT経由のアクセスが9人ありました。AIに聞いた人に、このサイトが案内されていたようです。

ソフトウェアアーキテクチャ#

ソースコードは公開しています。

github.com/matsubo/sado-tracker-2026

全体像#

  • Next.js 16(App Router)、TypeScript strict、React 19
  • パッケージ管理はBun、実行はNode 24
  • Dockerコンテナ1つを自前のサーバーのCoolifyで動かし、前段にCloudflare
  • テストはVitest(ユニット・統合)とPlaywright(E2E)、静的検査はBiomeとtsc

コミット数は30余り、srcのTypeScriptが約12,000行、テストが約6,400行です。設計書を先に書いてから、Claude Codeと一緒に実装しました。リポジトリの最初のコミットは大会前日の9月5日です。

データの取り方#

公式計測サイトには、全選手・全タイプを1つのCSVで返すエクスポートがあります。ページをスクレイピングする必要はなく、これを60秒に1回取りに行くだけです。

ただしこのCSVには癖があります。

  • 文字コードがShift_JIS
  • 各計測地点のタイム列の隣に、ミリ秒だけの列がある
  • 列数が年によって違う(2022年は41列、2026年は48列)
  • 年代区分の表記が年によって違う(「40-44男子」「40-44歳男子」「M40-44」)

そこで、列はインデックスではなくヘッダー名で読み、ミリ秒列はタイム列と結合して1つのエポックミリ秒にし、年ごとの表記揺れは年別の設定ファイルに正規化表として持たせました。2022年から2026年まで、同じパーサーで読めます。

計算は全部、純粋関数#

順位、ペース、推定位置、ゴール予想といった計算は、すべて (スナップショット, 設定, 現在時刻) → 結果 の形の純粋関数です。I/Oはなく、Date.now() も呼びません。現在時刻は外から渡します。

これがテストのしやすさを決めました。時刻を渡せるので「13時05分にこの選手はどこにいるはずか」をそのままテストに書けます。

レース当日を2分で再生するリプレイ基盤#

今回いちばんのキーポイントだと思っているのがこれです。

このサービスの本物の表示は、大会当日にしか出ません。計測データがリアルタイムに流れてきて、選手が少しずつ進み、順位や予想が刻々と変わる。その画面がどう見えるのかは、大会が始まってからでないと分からない。でも当日に初めて見て直す時間はありません。

そこで、時間を自動で進めながら「そのときの画面」を再現するリプレイ基盤を最初から組み込みました。環境変数で開始時刻と倍速を指定すると、2025年大会のCSVを読み込み、仮想の時計を進めながら、その時刻までに起きた通過だけを見せます。計算が現在時刻を外から受け取る純粋関数になっているので、時計を差し替えるだけで済みました。

開発中は、2025年のスタートから夜10時までを約2分で再生する設定を使いました。画面を眺めていると、スイムからバイクに移る時間帯、Bタイプのゴールが続く時間帯、夜になって完走者が並ぶ時間帯と、レース1日分の表示の変化が2分で目の前を流れていきます。これを何度も繰り返して、見た目やレイアウトを調整しました。気になる時間帯があれば倍速を落として、13時なら13時の画面をじっくり見られます。

自動テストも同じ基盤の上に乗せています。PlaywrightのE2Eテストはリプレイモードで起動したサーバーに対して、選手をブックマークして通知が来るところまでを通します。この記事のスクリーンショットも、すべてこのリプレイモードで撮ったものです。

正しさは、公式の値と突き合わせて決める#

「数字を製品にする」と決めたので、正しさはテストで主張するのではなく、公式の値との一致で確かめることにしました。

  • 2023年から2025年の全完走者について、私が計算した FINISH − START が公式の総合記録と一致することをテストで確認しています。3,820行が全件一致しました。この確認で、ミリ秒の丸め方が「四捨五入ではなく切り捨て」だと分かりました
  • 順位には必ず母数を付けます。「288/719」の719は、その計測地点を通過した同じタイプの選手の数です。まだ通過していない選手を分母に含めると順位の意味が変わってしまうので、母数は計測地点ごとに取り直しています

推定位置は「次の計測地点の手前」で止める#

推定位置は、最後に通過した計測地点から、その選手自身の直近の区間速度で前進させます。自分の速度がまだ無いときは、同じタイプの選手がその区間で出している速度の中央値(5人以上いるとき)、それも無ければ過去大会の中央値を使います。

工夫したのは上限です。推定位置は次の計測地点の手前で止めます。もし次の計測地点を通過していれば記録が残っているはずなので、記録が無い以上そこは越えていないからです。上限に当たった選手は「次の計測待ち」と表示します。

ゴール予想は「似た過去の選手」から#

ゴール予想は、2023年から2025年の同じタイプの完走者の中から、同じ計測地点でのペースの組み合わせが最も似ている20人を探し、その20人が実際に要した残りタイムの中央値を足します。幅として四分位も出します。似た選手が5人に満たないときは、いまの速度で残り距離を割る外挿に切り替え、どちらの方法で出したかも「?」に表示します。

さらに起動時にバックテストを回します。2025年の完走者を2023年と2024年のデータから予想し、計測地点ごとの誤差を記録しておいて、画面の「?」で見せます。

計測地点中央絶対誤差±25分に収まる割合
Aタイプ 住吉(バイク100km)26分49%
Aタイプ ランスタート24分52%
Aタイプ ラン20km9分92%
Aタイプ ラン34km3分100%
Bタイプ ランスタート24分54%
Bタイプ ラン14km3分100%

バイクの途中で出す予想は±25分に収まるのが半分程度で、正直あまり当てになりません。ランの後半になると急に当たるようになります。この「当たらなさ」を隠さずに見せるのが、今年こだわった点の1つです。

2026年の実際のゴールで答え合わせ#

上の表は過去の年での検証ですが、2026年の大会が終わったので、本番の予想が実際のゴールとどれだけ違ったかを答え合わせしました。方法は本番と同じで、その計測地点より後の通過を隠した状態で予想を出し、実際のゴール時刻と比べています。

まず、Aタイプの完走者から無作為に3人選んで、バイクの途中(住吉)、バイク終了(ランスタート)、ランの中間(20km)、ランの最後の計測地点(39km)での予想を並べました。

選手住吉(バイク100km)ランスタートラン20kmラン39km実際のゴール
選手A18:32(+44分)18:27(+39分)17:46(−1分)17:48(±0)17:48
選手B19:03(+32分)19:12(+41分)18:16(−15分)18:31(±0)18:31
選手C20:07(+43分)19:38(+15分)19:19(−4分)19:24(±0)19:24

括弧の中は予想と実際の差で、プラスは予想が遅かったことを意味します。3人ともバイク中の予想は30分から40分ほど遅めに出て、ランの中間で数分から15分に縮まり、最後の計測地点では一致しました。

3人だけでは偏るので、完走者全員でも集計しました。

計測地点人数誤差の中央値(符号付き)誤差の中央値(絶対値)±25分に収まる割合実際のゴールが予想の幅に入る割合
A 住吉812+5分27分47%39%
A ランスタート813+8分26分49%36%
A ラン20km813−6分10分89%36%
A ラン39km811±00分100%46%
B 住吉665+28分33分39%36%
B ランスタート665+9分17分69%45%
B ラン10km665+2分5分100%44%
B ラン19km665±00分100%42%

読み取れることは3つあります。

  • バイク中の予想は、Aタイプで誤差の中央値が26〜27分。過去年の検証(26分、24分)とほぼ同じで、本番でもその程度でした。無作為の3人がそろって30分以上遅かったのは、全体で見ると偶然の側に近いです
  • Bタイプの住吉(バイク18km地点)は、全体で28分も遅めに偏っていました。2026年はBタイプのスイムが1.0kmに短縮され、スタートも8時に動いたので、過去の完走者より体力を残してバイクに入った選手が多かったのだと推測しています。ここは来年直したい点です
  • 「予想の幅」は四分位なので、理屈の上では半分の人の実際のゴールがこの中に入るはずです。実際は36〜46%でした。幅がやや狭すぎる、つまり少し自信過剰です

ランの後半に入れば、予想は数分の誤差で当たります。逆に言うと、バイク中の予想は「ゴール地点に向かう時刻を決める材料」ではなく「だいたいの目安」として見てほしい数字です。

余談ですが、2025年のBタイプはスイムが1.35kmに短縮されていました。距離で割ってペースにしても他の年と揃わないので、Bタイプのスイムだけは絶対的なペースではなく「その年の中で何割の位置か」という百分位で比べています。年ごとの距離を設定に持っていたおかげで、表示するペースは正しいまま、予想の中の扱いだけを変えられました。

ページを再読み込みしない#

レース中に画面がリロードされるのは、応援している人にとってストレスです。ページは15秒に1回、小さな GET /api/race だけを見に行き、更新時刻が変わったときだけ本体のデータを取り直します。推定位置はサーバーの更新を待たず、ブラウザ側の時計に合わせて計算し直すので、点が少しずつ動きます。

通知もプッシュ通知ではなく画面内で完結させました。ブックマークした選手の通過イベントを集合として持ち、既読の集合との差分を未読にしています。差分で持つと、計測が遅れて届いた通過も取りこぼしません。

APIは自己記述的に#

APIはHAL形式で、すべてのリソースに _links を付けています。選手のレスポンスには自分自身、所属タイプの順位表、AI TRI+の選手ページへのリンクが入ります。順位表のページ送りも _linksnextprev で辿れます。クライアントがURLを組み立てなくて済むようにするためです。

公開リポジトリとプライバシー#

公式CSVには約1,900人分の実名が入っています。これはリポジトリに入れません。テスト用のCSVは、実データからタイムの分布を保ったまま名前を架空のものに置き換え、ゼッケンをシャッフルして生成しています。この記事のスクリーンショットに出ている「小木 湊」「松ケ崎 結衣」といった名前は、佐渡の地名から作った架空の名前です。

ブックマークはブラウザのlocalStorageにしか保存せず、サーバーには送りません。アナリティクスに送るのは追加した回数だけで、誰をブックマークしたかは送っていません。

反省と来年に向けて#

  • バイク中の予想が粗い。誤差の中央値が26分では「ゴール地点に何時に行くか」の判断に使えません。バイクの前半と後半で傾向が違うので、区間ごとの特徴量を増やしたいです
  • 天気の予報地点。当日は相川のアメダスで最大瞬間風速17.6m/sの強風でした。予報地点は内陸の佐和田なので、海岸の実況とは差が出ます。来年は予報地点を見直したいです
  • ブックマークの入口。深く使ってくれたのはブックマークを使った1割強の人でした。トップページで名前を検索した人を、もっと自然にブックマークへ誘導したいです
  • 当日前のアクセスが測れていない。アナリティクスを入れたのが当日の朝だったので、前日にどれだけ準備で見られたかが分かりません

まとめ#

  • 3年目の応援トラッカーは、大会当日に992人、期間合計で1,149人に使ってもらえました。参加者1人につき応援者2人と見込んだ2,000〜3,000人には届きませんでした
  • 自分で使っていちばん良かったのは現在地の推定です。直前の通過タイムではなく、推測した速度で進み続ける位置を出します
  • 9割以上がスマートフォン。9時から20時まで毎時100人以上が見ていて、13時台がピークでした
  • 順位・推定位置・ゴール予想はすべて純粋関数で計算し、3,820行の公式記録と突き合わせて検証しています
  • 大会当日にしか出ない画面を、2025年のレース1日分を2分で再生するリプレイ基盤で、事前に人の目で確かめて調整しました
  • ゴール予想の「当たらなさ」もバックテストで測って、隠さずに見せています
  • ソースコードは公開しています。来年はバイク中の予想精度と、ブックマークへの導線を改善します
この記事が役に立ったら
GitHub Sponsorsで応援できます

コメント