FRC 2026シーズン以降のChampionship出場権について
はじめに
まず、公式情報がこちらにあります。
https://www.firstinspires.org/resource-library/frc/championship-eligibility-criteria
また、The Blue Allianceという大会の成績等がとても見やすくまとまっているサイトがあります。
https://www.thebluealliance.com/
主に、これら2つのサイト (とそれにリンクされているサイト) から得た情報からこの記事は構成されています。
この記事の読み方
この記事は全部読もうとすると超ヘビーです。なので、いくつか読み方を紹介します。
要は、満足したら途中で読むのをやめてください。
なんとなくChampionship出場権に関するルールを知りたい人
- 「出場権の基本的な与えられ方」
- 「Championship出場基準について」
- 「それでは結局どうすればChampionshipに行きやすいのか?」の最初の1文
Championshipに出たい人
- 同上
- 「結局どうすればChampionshipに行きやすいのか?」
ポイント制について理解したい人
- 同上
- 「ポイントシステムの詳細」のざっくり解説
- 「大体のChampionship出場のパターン」
チームでの分析係
- 同上
- 「ケーススタディ」
マニア
- 同上
- 「ポイントシステムの詳細」の補足
- 「出場権に関する全体的な流れについて」
- 「Championship出場基準について」
- 「FIRSTは何がやりたいのか?」
- 「おまけ: もし2025シーズンが2026シーズンのルールだったら?」
- 「2025でどれだけChampionshipのレベルがあがったか?」
必要な前提知識
- FRCについて
- 特にImpactやEngineering Inspiration (以降EI) などの重要なAwardについての軽い知識
- FRCのシーズンの流れ (Regionalの場合)
- Week 1〜Week 6に分けられている、チームは何個でも出ていい
- 大会の簡単な流れ
- Qualification → Alliance Selection → Playoff
出場権の基本的な与えられ方
まず、予め出場権を得ているチーム (Pre-Qualified Teams) と シーズンを通して出場権を獲得するチーム (Merit-Based Qualifying Teams)があります。
Pre-Qualified Teams
基本的に、Championshipで優秀な成績を残したチームが該当します。
Merit-Based Qualifying Teams
シーズン中の成績、つまり予選大会での成績を元に出場権を得るチームです。
FRCの予選大会には2種類あります。District ModelとRegional Modelです。また、チームも住所によってDistrict TeamなのかRegional Teamなのかが自動的に割り振られます。
Regional Model
世界各地で大会が開催され、出場チームは大会での成績を元にChampionship出場権を得ます。
2025シーズンは69大会開催されています。
District Model (日本チームは今のところ関係ないので読み飛ばしても大丈夫です)
Districtは特定の地域に大量のチームがあるときに設けられます。
2026シーズン現在はChesapeake, California, Michigan, Texas, Indiana, Israel, Mid-Atlantic, North Carolina, South Carolina, Wisconsin, NE (New England), Ontario, Pacific Northwest, Peachtreeの14箇所がDistrictの地域に指定されています。(ちなみに2026シーズンにCaliforniaとWisconsinが追加されました。2023シーズンには11箇所, 2025シーズンには12箇所だったことを考えると着実に増えています。)
Championshipへの流れとしては、
- District Eventにいくつか出場
- そこでの成績に基づいてDistrict Championshipへの出場権を得る
- District Championshipでの成績に基づいてChampionshipへの出場権を得る
になります。
なお、Districtに指定された地域のチームはRegional Modelの大会に出場することはできますが、Championship出場権を得ることは出来ず、一部の賞も審査対象外となります。一方、RegionalチームはDistrictイベントに出場することもできないようです。
Championship出場基準について
2026シーズン
Pre-Qualified Teams
過去8年間のChampionshipでImpact Awardを受賞したチームです。
Regional Model
大会での成績を元に点数付けが行われ、まだ出場権を獲得していない、以下の数のチームが出場権を得ます。
- アメリカでのRegionalでは上位3チーム
- アメリカ以外でのRegionalでは上位4チーム
それ以外はまとめてRegional Poolに入れられます。この際、大会での成績を元に点数付けが行われ、順位付けが行われます。点数付けの仕組みはややこしいので後述します。
そして、Week 2以降1週間ごとに、決められた数のチームがPoolの上位から順にChampionshipへ招待されます。
結局どうすればChampionshipに行きやすいのか?
結局、
- 賞を狙えなくてもPlayoff優勝できそうなら好きなように!
- ImpactやEIを狙えて、Playoffに高順位で安定して出られるなら、Regionalは2つで受賞頑張ろう!
- ImpactやEIを狙えるけど、Playoff出場ギリギリそうなら、Regionalは1つで上振れを狙おう!
- 賞を狙える感じでないときはRegional1つでPlayoff3位以上を目指そう!
- なんにせよ、遅めのWeekのRegionalに出よう!(でも遅すぎるとギリギリになってしんどいよ!)
というのが結論です。
ざっくり
出場回数について
- ImpactやEIをガチで狙えるかつ、取れなくてもロボットでそれなりの成績を残せるのであれば2つ以上出場!
- ロボットで準優勝以上できる自信があるのであれば2つ以上出場!
- 安定して40ポイント以上取れる自信があるのであれば2つ出場!
- (上に内包される気はしますが)
- それ以外は1つ出場!
出場場所について
- 言語や、時期、渡航費、治安、会場位置が問題にならないのであればアメリカ以外の大会に出場!
- 英語圏でアメリカ以外: カナダ、オーストラリア
- 英語を使える人も多い(が、過度な期待は禁物): メキシコ
- どうやら外国人に優しいらしいところ: トルコ、台湾(?)
- その他: ブラジル、中国
- その他の場合は、治安や会場の位置、大会レベルを元にアメリカの大会に出場!
- 会場の位置は、都市部に近ければだいたい大丈夫だとは思います。交通の便や、治安的にはその方が安心です。あとは、空港との位置も大事です。
忘れているかもしれないので補足しますが、アメリカ以外の大会では大会からの出場枠が4つです。アメリカの大会は3つです。
ちなみに、日本のチームが良く出がちなハワイは会場が狭くなってしまったためにかなり枠が狭く、第一希望じゃないと入れないと思います。
出場するWeekについて
- 1つだけの場合、遅いほうが進出できる可能性が高くなります。Regional内での「まだ出場権を獲得していない上位3 or 4チームが出場権を獲得する」という性質上、出場権を得ているチームが多そうな遅いWeekの方が4〜6位のロボットや、あまりロボットが強くないチームのImpactやEIが出場できる可能性が高まります。
- 早いほうがRegional Poolの抽選回数は多くはあるのですが、結局「最後のWeekのボーダーラインがどこになるか」しか問題ではないので、Reginal Poolについては変わりません。一応、早いほうがChampionship確定が早くなる可能性があって準備時間も取りやすくなるというメリットもあります。
- 2つ以上の場合、2つ目はできるだけ遅らせたほうがいいです。1つ目でいい成績を残したときに2つ目で台無しにする可能性が低くなります。
Impact Award, EIについて
- 狙えるのであれば狙うべき。
- ただしImpactがダメでEIの場合、ロボットもそれなりの成績が必要なので気は抜けない。
- 2つ以上Regionalに出る場合は、本当にロボットにも気合を入れないといけない。
その他Awardについて
- それなりに強いロボットを作れていて、Playoffには確実に出場できそうだが、4位以上になれるかは自信がない時には積極的に狙うべき。この場合Regional Poolでの出場を目指す。
ロボットについて
- いずれにせよいい順位でPlayoff出場できるロボットを目指す。
- 可能であればキャプテンか1st pick、あるいはアライアンス1〜4の2nd pick (Qualのランキング的には上位16番以内になるのが望ましい)
- あわよくばFinal出場
ポイントシステムの詳細
まず各イベントでのポイントは6種類のポイントの合算値になります。
ポイント名称 | どういった成績に基づくか |
Qualification Round Performance | Qualificationを何位で終えたか |
ALLIANCE CAPTAINS | Playoffの何番目のアライアンスのCaptainか |
Draft Order Acceptance | Playoffのアライアンスに何番目にpickされたか |
Playoff Advancement | Playoffでどれだけすすんだか |
Judged Team Awards | どんな賞を受賞したか |
Team Age | 新人チームに対するボーナス |
そして、Regional Poolからの判定時には、最初の2つのイベントのポイントの合計値が使われます。1つしか出ていないときには、2つ目は予測値が用いられ、0.6 * (1つ目のイベントのポイント) + 14の切り上げで計算されます。
これが結構厄介で、「2つのイベントの成績が入れ替わるだけでChampionshipに出られるかどうかが変わる」「3つ目のイベントでいい成績を出したけど2つ目が悪かったから出られなかった」みたいなことが起きたりします。
ちなみに、1つ目のイベントが35ポイントより小さいなら予測値は1つ目のイベントより大きくなり、38ポイントより大きいなら予測値は小さくなります。(小数点以下切り上げの仕様により、35〜37ポイントは同一の値になります。)
ポイントをざっくり解説
細かな例外を除いたり、論理を簡単にして説明します。
Qualification Round Performance
Qualificationの順位に応じて点数が決まります。
簡単に言えば、1位が22ポイント、最下位が3ポイントで、その間がいい感じに割り振られます。大体の感覚としては、上位と下位は順位に応じて点数がまあまあ変わりがちで、中位は2, 3ランクくらいは同じ点数のものがある、という感じです。
ALLIANCE CAPTAINS, Draft Order Acceptance
ALLIANCE CAPTAINSとDraft Order Acceptanceについて表にまとめると以下のようになります。 (pickの時間制限を破らない限り)
ALLIANCE 1 | ALLIANCE 2 | ALLIANCE 3 | ALLIANCE 4 | ALLIANCE 5 | ALLIANCE 6 | ALLIANCE 7 | ALLIANCE 8 | |
captain | 16 | 15 | 14 | 13 | 12 | 11 | 10 | 9 |
1st pick | 16 | 15 | 14 | 13 | 12 | 11 | 10 | 9 |
2nd pick | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
Playoff Advancement
Playoffの所属アライアンスの成績に応じて、(ロボットの故障等で欠場していない限り) 以下のように与えられます。
- 優勝アライアンス: 30ポイント
- 準優勝アライアンス: 20ポイント
- 3位のアライアンス: 13ポイント
- 4位のアライアンス: 7ポイント
また、バックアップチームとして出場して、最終的にそのアライアンスがいい成績を収めた場合ポイントをちょっと貰えます。
Judged Team Awards
チームがもらった賞に応じて与えられます。複数受賞すればその分加算されます。
- Impact Award: 45ポイント
- Engineering Inspiration Award: 28ポイント
- Rookie All Star Award: 8ポイント
- その他のAward: 5ポイント
Team Age
単純です。1, 2年目のチームにはポイントが加算されます。
- ルーキーチーム: 10ポイント
- 昨年のルーキーチーム: 5ポイント
ポイントの補足
上記で省いた正確性等を補足します。
Qualification Round Performance
これが一番複雑です。逆にこれ以外は簡単なので安心してください。
簡単に言えば、最高22ポイント、最低3ポイントが以下の式を元にいい感じに割り振られます。
R: 最終的なQualificationでの順位
N: 出場チーム数
α: 1.07 (分布を正規化するための定数 ≒ 点数配分をいい感じにするための定数)
ここで使われているInvERFはERF (error function, 誤差関数)というものの逆関数です。細かいことは分かる必要はありませんが、要は「どれだけ平均から外れた実力を持っているか」を表してくれるものです。
大体の感覚としては、上位と下位は順位に応じて点数がまあまあ変わりがちで、中位は2, 3ランクくらいは同じ点数のものがある、という感じです。
ALLIANCE CAPTAINS
PlayoffのアライアンスのCaptainにのみ与えられるポイントです。
17 – (アライアンス番号)
で計算されます。
Alliance 1なら16ポイント、Alliance 8なら9ポイントです。
Draft Order Acceptance
Alliance Selectionでpickされ、受け入れたチームにのみ与えられるポイントです。
17 – (受け入れた順番)
で計算されます。
最初に受け入れたら(≒ Alliance 1の1st pickなら)、16ポイントです。
(2025シーズンから導入されたpickの時間制限を破らない限り、) 1st pickの場合はcaptainと同じポイントになり、2nd pickの場合はアライアンス番号と同じになります。
Playoff Advancementを説明するための補足
まず、試合の欠場に関するルールを紹介します。Playoff中、ロボットが壊れるなどして出場できなくなった場合はバックアップチームを起用することで、3台で戦うことをとりあえず継続できます。その後、ロボットが直ればバックアップチームをクビにしてまた試合に戻ることも可能です。
つまり、Playoffでアライアンスとして勝った回数 = Playoffでチームが勝った回数、とは限らないということです。
また、DE MATCH (Double Elimination MATCH) という概念があり、Playoffの試合のうち、Final以外のものを指します。
Playoff Advancement
まず、Playoffの所属アライアンスの成績に応じた基本ポイントがあります。
- 優勝アライアンス: 20ポイント
- 準優勝アライアンス: 20ポイント
- 3位のアライアンス: 13ポイント
- 4位のアライアンス: 7ポイント
そして、以下のように「DE Points」が計算されます。整数にならなかった場合は切り上げされます。
DE Points = (チームが出場して勝利したDE MATCH数) / (アライアンスが勝利したDE MATCH数) * (基本ポイント)
また、優勝したアライアンスのチームには、Finalで出場して勝利した試合の数 * 5ポイントが加算されます。
だいぶややこしいですが、以下に良い例があります。Northern Lights Regional 2025 のALLIANCE 2です。MATCH 8にてALLIANCE 2の5253のロボットが壊れて4360が出場しました。最終的にALLIANCE 2は優勝したのですが、
- ALLIANCEとしてはDEで3勝
- 4360起用は1試合のみで、そのとき勝利
- Finalで5253が入った状態で2勝
という状況で、以下のようにポイントが割り振られました。
- 111, 2240: 3/3 * 20 + 5 + 5 = 30ポイント
- 5253: 2/3 * 20 + 5 + 5 = 23.333.. → 24ポイント
- 4360: 1/3 * 20 = 6.666… → 7ポイント
Team Age
- ルーキーチーム: 10ポイント
- 昨年のルーキーチーム: 5ポイント
ポイントの割り振りに例外はありませんが、Districtとはちょっと足され方が違うよ、という話です。Districtはイベントのポイントには含まれず、2つのイベントの合算をした後に足される一方、Regionalではそれぞれのイベントに足される、という違いがあります。要は、Regionalの方が有利なシステムになっています。大会ごとに3つしかない枠においてルーキーを優遇するためだそうです。
大体のChampionship出場のパターン
1イベント41ポイント、2つ合計で80ポイントくらいを取れると安心です。
ロボットのみ
超単純に、AllianceのpickがQualificationのランキングの上から行われたとします。また、イベントに参加したチームが50チームと仮定します。それをもとにQualification Round PerformanceとALLIANCE CAPTAINとDraft Order Acceptanceの合計値は以下のようになります。
もちろん適当なシナリオではあるものの2つの仮定が行われていることには注意してください。特に、Hawaiiなど参加チーム数が大きく少ない (2025シーズンは33チーム) 場合は3位以降はここの点数より1〜3点低くなります。順位が低いほどその幅は広くなります。
順位 | 1位 | 2位 | 3位 | 4位 | 5位 | 6位 | 7位 | 8位 |
ポイント | 38 | 36 | 35 | 34 | 32 | 32 | 30 | 30 |
順位 | 9位 | 10位 | 11位 | 12位 | 13位 | 14位 | 15位 | 16位 |
ポイント | 28 | 28 | 27 | 26 | 25 | 25 | 23 | 23 |
順位 | 17位 | 18位 | 19位 | 20位 | 21位 | 22位 | 23位 | 24位 |
ポイント | 22 | 21 | 19 | 18 | 17 | 16 | 14 | 13 |
41ポイントをボーダーラインとすると、なんにせよPlayoffで4位以内に残るのは必須と言えます。
- 優勝 (30ポイント): どの順位でも
- 準優勝 (20ポイント): 18位以上
- 3位 (13ポイント): 10位以上
- 4位 (7ポイント): 4位以上
- それ以外 (0ポイント): どれでもだめ
Impact受賞
Impactは45ポイントなのでボーダー超えです。
ただし、RegionalではWeek 1からWeek 6を通して1度しかImpactを受賞できないので、もし2つRegionalに出るのであれば、それ以外で平均17.5ポイントを稼ぐ必要があります。上の表的には20位ですね。ギリギリPlayoff出場できるなら可能性がありそうです。
EI受賞
EIは28ポイントなので単体ではボーダーに届きません。上の表的には24位以上であればボーダーに届きます。
EIはImpact同様、1シーズン1度しか受賞できないので、2つRegionalに出るのであれば、それ以外で平均26ポイント稼ぐ必要があります。上の表的には12位です。結構強くないといけないですね。
あるいは、一回でも2nd pickで準優勝をすれば平均16ポイントで済むのでどうにかなるかももしれません。
Rookie All-Star Award受賞
Team Ageで10ポイント、RASで8ポイントなので合計18ポイントですが、ボーダーには全然届きません。ロボットで17位以上であればボーダーに届きます。ルーキーとしては結構大変です。
RASはImpactやEI同様、1シーズン1度しか受賞できないので、2つRegionalに出るのであれば、それ以外で平均26ポイント稼ぐ必要があります。(Team Ageはイベントごとに加算のためちょっと楽。) あとはEIと同じで、12位以上が望ましいです。
その他の賞受賞
地味に大きいかもしれなくて、賞を1個とると必要な順位が4〜6位ほど下がる感覚です。
ケーススタディ
Championshipの当落線上のケースや、多数のRegionalに出ているケースなど、様々取り扱います。
具体的なチームを例に上げて説明をしますが、貶める意図はなく、単なる例として紹介します。
ロボットの成績関係
基本的に2nd pickでWinnerのケースを取り上げて、如何に24位のロボットがChampionshipに出るのに運や上振れがないといけないかを説明します。
10598: Rookieかつそこそこのロボットの上振れや運でやっと
TBA: https://www.thebluealliance.com/team/10598/2025
Bosphorus Regional (Week 2)
Qualification | Alliance Selection | Playoff | Award | Team Age | 合計 | |
成績 | 26位/49チーム | Alliance 1の2nd pick | Winner | 無し | Rookie | |
ポイント | 12ポイント | 1ポイント | 30ポイント | 0ポイント | 10ポイント | 53ポイント |
Marmara Regional (Week 2)
Qualification | Alliance Selection | Playoff | Award | Team Age | 合計 | |
成績 | 10位/50チーム | Alliance 7のcaptain | round3で敗退 | 無し | Rookie | |
ポイント | 17ポイント | 10ポイント | 0ポイント | 0ポイント | 10ポイント | 37ポイント |
合計ポイント: 90ポイント (Week5でようやく通過)
分析
- Bosphorus RegionalでAlliance 1の2nd pickとかなりギリギリの選ばれ方をして優勝したが、ポイント的には8位で出場ならず。
- Marmara Regionalの方で、味方運もありRPを結構取れて順位が高くなりcaptainに繰り上がったおかげで結構稼げて、なんとかRegional Poolで出場
- Championshipでは70位/75チーム
6873: 2nd pick + awardでギリギリ
TBA: https://www.thebluealliance.com/team/6873/2025
New York City Regional (Week 6)
Qualification | Alliance Selection | Playoff | Award | Team Age | 合計 | |
成績 | 47位/53チーム | Alliance 1の2nd pick | Winner | Creativity Award | 8年目 | |
ポイント | 7ポイント | 1ポイント | 30ポイント | 5ポイント | 0ポイント | 43ポイント |
合計ポイント: (補正込みで) 83ポイント (Week6のボーダーラインピッタリ)
分析
- 正直47位で2nd pickされたのはかなりラッキーです。よくあるのは、なにかに特化している/captainと仲がいい/運が悪かっただけで実はそこそこの実力があるパターンです。
- 映像を確認した感じ、かなりゆっくりではあるものの、得点率は100%で一定の点数を取ることが出来ていました。また、他のロボットの邪魔をすることもなくかなり優秀な2nd pickロボットだったと思います。そこそこの実力があるパターンです。
- Creativity Award受賞のおかげでボーダーラインにギリギリねじ込むことができました。
- 受賞理由は定かではありませんが、インテイクの受けの広さによる安定性が評価されたのかもしれません。
- Championshipでは67位/75チーム
Award関係
706: EIとってもPlayoff出れてないと…
TBA: https://www.thebluealliance.com/team/706/2025
Seven Rivers Regional (Week 4)
Qualification | Alliance Selection | Playoff | Award | Team Age | 合計 | |
成績 | 21位/51チーム | なし | なし | EI | 25年目 | |
ポイント | 14ポイント | 0ポイント | 0ポイント | 28ポイント | 0ポイント | 42ポイント |
合計ポイント: (補正込みで) 82ポイント (Week6のボーダーラインに届かず)
分析
- 2025シーズンはEIは無条件出場だったので助かりましたが、2026シーズンだったら出場できていません
- Alliance Selectionで選ばれなかったのが一番大きいです。
- Qualの順位がPlayoffボーダーラインなのと、ロボットが重心が高く転倒リスクを懸念されたことが原因かもしれません。
Regionalの個数関係
9583: 3つ以上出ると…
TBA: https://www.thebluealliance.com/team/9583/2025
Bosphorus Regional (Week 2): 20ポイント
Haliç Regional (Week 2): 10ポイント
Ankara Regional (Week 3): 52ポイント (EI受賞)
合計ポイント: 30ポイント (当然出場届かず)
分析
- Regional Poolのためのポイントは最初の2つしか採用されないため、EIをとった3つ目のAnkara Regionalはポイントの観点では貢献しません。
- 2025シーズンはEIは無条件出場だったので助かりましたが、2026シーズンだったら出場できていません。
- Ankara Regionalでもポイントで5位で繰り下がりもなかったため、本当に出場できていませんでした。
6560: 最初のイベントでやらかすと…
TBA: https://www.thebluealliance.com/team/6560/2025
Hueneme Port Regional (Week 1)
Qualification | Alliance Selection | Playoff | Award | Team Age | 合計 | |
成績 | 22位/46チーム | なし | なし | なし | 9年目 | |
ポイント | 13ポイント | 0ポイント | 0ポイント | 0ポイント | 0ポイント | 13ポイント |
Arizona North Regional (Week 3)
Qualification | Alliance Selection | Playoff | Award | Team Age | 合計 | |
成績 | 3位/40チーム | Alliance 2のcaptain | Finalist | Innovation in Control Award | 9年目 | |
ポイント | 20ポイント | 15ポイント | 20ポイント | 5ポイント | 0ポイント | 60ポイント |
合計ポイント: 73ポイント (Week6のボーダーラインに届かず)
分析
- 仮にWeek3だけ出場だったら補正込みで110ポイントなので余裕でChampionship通過です。
- また、成績の順番が逆でもRegional Poolで通過です。
- Statboticsというサイトによると、Week 1と比較してWeek 3では全体的に伸びている & Endgameが特に伸びています。おそらく、ClimbをWeek 3で出来るようになったのが一番大きいです。Week 1に出るのは早すぎたかもしれません。
5705: 2つ目でやらかすと…
TBA: https://www.thebluealliance.com/team/5705/2025
Regional Monterrey (Week 1): 49ポイント (EI受賞)
Regional Laguna (Week 4): 24ポイント
合計ポイント: 73ポイント (Championship出場ならず)
分析
- 仮にWeek 1だけだとしたら、補正込みで93ポイントで、Week 4のボーダーラインを超えていたためChampionship出場できていました。
- 2025シーズンはEIは無条件出場だったので助かりましたが、2026シーズンだったら出場できていません。
6510: 2つめギリギリ耐えてる
TBA: https://www.thebluealliance.com/team/6510/2025
Southern Cross Regional (Week 2)
Qualification | Alliance Selection | Playoff | Award | Team Age | 合計 | |
成績 | 4位/41チーム | Alliance 3のcaptain | Round 5で敗退 | Innovation in Control Award | 9年目 | |
ポイント | 19ポイント | 14ポイント | 13ポイント | 5ポイント | 0ポイント | 51ポイント |
Colorado Regional (Week 4)
Qualification | Alliance Selection | Playoff | Award | Team Age | 合計 | |
成績 | 5位/50チーム | Alliance 4のcaptain | Round 3で敗退 | なし | 9年目 | |
ポイント | 19ポイント | 13ポイント | 0ポイント | 0ポイント | 0ポイント | 37ポイント |
合計ポイント: 88ポイント (Week5のボーダーライン超え)
分析
- Week 2の51ポイントは補正込みで96ポイントだったため、Week 4のボーダーラインを超えていました。結果としてWeek 4は出ても出ていなくてもChampionship出場にはなっていたわけです。
- なんにせよ、2つ出るならPlayoffは高順位で安定していないとリスキーということがわかると思います。
出場権に関する全体的な流れについて
Championshipの出場条件はしばしば批判の対象になってきました。
FIRSTはMore Than Robotsであり、FRCはロボットだけが評価対象ではありません。そのため、FIRSTの理念に沿って活動しているチームは表彰され、Championship出場において優遇されてきました。しかしその一方、ルールの仕組み上、「強くないロボット」が出場することがありました。
ロボットに力を入れていなくても、「社会的な影響が大きい」「プレゼン上手」というだけでも、運だけで強いチームにキャリーされるだけでも、Championshipに出場できてしまい、ロボット大会としてのレベルが下がっているのではないか、というのが批判の主な点です。
また、他にも様々な点が歪みを生み出していました。
こういう批判もあってか、FIRSTは出場権に関するルールの改定を2025シーズンから行っています。パット見は難解ですが、ちゃんと分析すると意外と単純なシステムかもしれません。
これまでのChampionship出場基準について
District Modelに関してはこれまで変更がなく、この記事を日本語で読んでいる時点で対象でないと思われるので割愛します。
2024シーズンまで
Pre-Qualified Teams
前年のChampionshipの
- Winner
- Impact AwardのFinalist
- Engineering Inspiration (以降EI) Award受賞チーム
- Impact Awardの受賞チーム
と、過去10年間のChampionshipでImpact Awardを受賞したチームです。
Regional Model
原則として、1つのRegionalイベントから、「新しく出場権を獲得するチームが最大で」5つ出ます。
以下のチームはChampionship出場権を得ます。
- Impact Award受賞チーム
- EI受賞チーム
- 優勝アライアンスのキャプテン
- 優勝アライアンスの1st pick
- 優勝アライアンスの2nd pick
ここまでは単純なのですが、既にChampionship出場権を得ているチームがいた場合「ワイルドカード」が発生することが話をちょっとややこしくします。ワイルドカードが発生すると、以下の優先順位で出場権が与えられます。
- Rookie All-Star Award受賞チーム
- 優勝アライアンスのbackupチーム (もしいれば)
- 準優勝アライアンスのキャプテン
- 準優勝アライアンスの1st pick
- 準優勝アライアンスの2nd pick
- 準優勝アライアンスのbackupチーム (もしいれば)
これが、「新しく出場権を獲得するチーム」が最大で5つ出る理由です。
またちょっとややこしい話なのですが、ImpactやEIは受賞チームがいない場合もあるそうで、その場合はワイルドカード等が発生しません。これが、新しく出場権を獲得するチームが「最大で」5つ出る理由です。例えばEIの受賞チームがいなければ4つになります。
Priority Waitlist
Regional出場チームにはさらなるチャンスがあります。
Rookie-All-Star Award受賞チームと優勝アライアンスのbackupチームがウェイトリストに加えられます。毎週、いい感じに調整されたランダムくじを元にChampionship出場の招待が届きます。
Regional Modelの仕組みにより出場チーム数が想定より少なくなったり、出場を辞退するチームもいたりすることで、Championshipの枠に空きが出ることの対策になっています。
2025シーズン
Pre-Qualified Teams
前年のChampionshipの
- Winner
- Impact AwardのFinalist
- Engineering Inspiration Award受賞チーム
- Impact Awardの受賞チーム
- (と、イスラエル・イラン紛争によって昨年出場できなかった9739)
と、過去10年間のChampionshipでImpact Awardを受賞したチームです。
Regional Model
まず、以下のチームは出場が確定します。
- Impact Award受賞チーム
- EI受賞チーム
- 優勝アライアンスのキャプテン
- 優勝アライアンスの1st pick
それ以外はまとめてRegional Poolに入れられます。この際、大会での成績を元に点数付けが行われ、順位付けが行われます。そして、Week2以降1週間ごとに、決められた数のチームがPoolの上位から順にChampionshipへ招待されます。
FIRSTは何がやりたいのか?
簡単に言うと
- Impact Award, 優勝アライアンスのキャプテンと1st pickは基本的に出場
- それ以外は総合的な成績を元に出場
- EIは優遇度は大きいが、ロボットの成績が良くないと出場できない場合もある
- Rookieはちょっと優遇されるが、ロボットの成績が良くないと出場できない
- 優勝できなくてもロボットの成績が良ければ出場できる場合もある
公式の詳細な説明
公式によって意図はここで説明されています。https://community.firstinspires.org/regional-advancement-task-force-update
FIRSTの言う事を訳しつつ、(筆者が勝手に想像した) 裏の意図を書くと以下のようになります。
- 全ての関係者を考慮に入れる
- FIRSTのCore Valueには沿いたい (Impact Awardも、レベルの高い大会にすることも重視する)
- Pre-qualifiedのコンセプトは維持したいが、枠は開けたい (Pre-Qualified Teamsの減少)
- ロボットとAwardのパフォーマンスのバランスを取りたい
- RegionalとDistrictのバランスを維持 (District, Regionalそれぞれの出場可能チーム数はあまり変化しない)
- Championshipに出場するチームが増えても大丈夫なように (Regional Poolで調整可能)
- 選考プロセスを増やさない
- 全てWeek6ではなく、シーズン中均等に出場権を出す (毎週のRegional Pool)
- リモート審査はしない
- 出場するチームすべてがロボットで戦える (「24番目のロボット」やRookieチームを安易に出場させない)
- アライアンスの仕組みやPlayoffを変更しない
- Impact Awardは基本的に出場可能
- EIは出場と相関関係が強い
また追加目標として、以下も掲げています。
- 一か八かではなく安定したパフォーマンスを出すインセンティブを高める (Awardに集中しすぎない、一発屋のようなロボットを作らない)
- 優勝アライアンスのキャプテンは基本的に出場可能
- 優勝アライアンスの1st pickは基本的に出場可能
- 運要素を減らす (実力がかなり反映されるポイント制)
- 24番目のロボットを作るインセンティブを下げ、3-6番目のロボットを作るインセンティブを高める
- 開催時期が早いイベントと遅いイベントを公平に (Regional Poolで毎週選出)
- 直感的でわかりやすいシステム
ここでいう「24番目のロボット」は、2024シーズンまでは出場権があった「優勝アライアンスの2nd pick」のことです。結局Alliance 1が優勝することが多いわけですが、Alliance Selectionにおいて最後に、24番目に選ばれるロボットはAlliance 1の2nd pickになるため、このように言われています。
おまけ: もし2025シーズンが2026シーズンのルールだったら?
Regional Poolの様子はこちらから確認できます
https://www.thebluealliance.com/events/regional#rankings
2025シーズンのChampionship出場のボーダーラインはは83ポイントです。
Week | ボーダーライン | ボーダーラインのチーム | 同率と差をつけた点 (わかれば) |
Week 2 | 99ポイント | 1732と6574がWeek 1で、8595と7536がWeek 2で53ポイント、補正込みで99ポイント | round5 (準決勝)まで残ったため (あと多分Alliance 3のため) |
Week 3 | 98ポイント | 4322がWeek 3で52ポイント、補正込みで98ポイントのため 10428がWeek2で49ポイントと39ポイント、合計で99ポイントのため |
なし (98ポイント勢全員招待のため) |
Week 4 | 91ポイント | 5847が Week 4で48ポイント、補正込みで91ポイントのため | Finalistのため (ちなみに1st pick) |
Week 5 | 87ポイント | 4009がWeek 1で26ポイント、Week 4で61ポイント、合わせて87ポイントのため | Finalistのため (ちなみにcaptain) |
Week 6 | 83ポイント | 7050がWeek 2で、5124がWeek 3で、117と6483がWeek 4で、10154と6837がWeek 6で43ポイント、補正込みで83ポイントのため 1625がWeek 5で29ポイント、Week 6で54ポイント、合計で83ポイントのため |
round5まで残ったため (Alliance的には一番低くてAlliance 5) (あとはWinnerの2nd pickが多かったです) |
さて、もしこれが2026シーズンだとどうなっていたかです。2025と2026で違うのは、ImpactとEI、優勝による確定枠がないのと、アメリカのRegionalでは3枠しか無いことです。(ちなみに、69あるRegional中54がアメリカのRegionalです。)
ざっくりカウントしてみましたが、2026シーズンだったらRegionalにて出場できなかったのは20チームほどありました。それがRegional Poolに分配されるのですが、おそらく最終ボーダーラインは80ポイントあたりになるでしょうか。1つしかRegionalに出ない場合は41ポイントです。
なお2026シーズンはDistrictが2つ増えましたが、regionalの枠が狭くなった一方、regionalのチーム数も減ったはずなので影響はあまりないはずです。逆に強いチームが抜ける可能性のほうが高いので楽になる可能性まであります。(でもそれ込みで枠が設定される気もするのでやっぱり関係ないかもしれません。)
2025でどれだけChampionshipのレベルがあがったか?
Statbotics (https://www.statbotics.io/) というサイトがあります。
ここのサイトでは、Expected Points Added (EPA) という指標を用いて大会でのロボットの成績を評価しています。EPAは要は、チームの得点能力をいい感じに評価する指標です。
これを使って計算してみたんですが、Championshipの出場チームの得点能力について、あんまり有意と言える向上は見られませんでした。(意外)
24番目のロボットは出場しなくなったものの、Impactはともかく依然としてEIが確定出場であったことや、そもそも強いチームはルール変更に依らず出場する (≒ 大半の出場層は変わらない) ことも原因としてあげられるかと思います。
ボーダーライン上のチームのレベルが上ったとは思われますが、それを計測するのは難しいため諦めました。(また、それを選ぶ際の基準もだいぶややこしそう)
おまけ
Qualification Round Performanceの計算プログラム
Pythonが動かせる人は下のプログラムで計算ができます。scipyのインストールが必要です。
実行の際は自己責任でお願いします。
import mathfrom scipy.special import erfinv # 逆誤差関数 InvERFdef qualification_points(R: int, N: int) -> int:“""QualificationPoints(R, N, α) を計算R: 順位 (1 = best)N: 全チーム数“""alpha=1.07numerator=N-2*R+2denominator=alpha*Ninner=numerator/denominatorval=erfinv(inner) * (10/erfinv(1/alpha)) +12return math.floor(val)if __name__ == “__main__":for i in range(1, 24+1):print(f"{i}位: {qualification_points(i, 50)}")
StatboticsでのChampionshipレベル解析プログラム
ChatGPT製です。いろんなパッケージのインストールが必要です。コピペの過程でインデントが崩れてますが、LLMに頼んで整形してもらってから実行してみてください。
実行の際は自己責任でお願いします。
#!/usr/bin/env python3# -*- coding: utf-8 -*-“""Championship (Regional-only teams) vs Regional Events — EPA Comparison– Events / TeamYears は Statbotics Python パッケージを使用– team_events だけ REST API を使用(/v3/team_events?event=…)、limit=1000でページング– Others(比較対象)は “regional" のみ(district / district_cmp は完全除外)– Championship 側は “Regionalチームのみ" かつ **下位 p%**(既定 10%)で評価– Regionalチーム定義は 2モード:strict : その年に district 無し(所属ベース)【既定】eligible : strict ∪ 「その年に一度でも regional に出場したチーム」(出場実績ベース)– 比較指標は avg / median / min / sd を選択可能(既定: avg)“""from __future__ import annotationsimport argparseimport jsonimport mathimport sysimport timefrom typing import Any, Dict, List, Optional, Tuple, Iterable, Setimport numpy as npimport pandas as pdimport requestsfrom cachecontrol import CacheControl # type: ignorefrom tqdm import tqdmfrom statbotics import StatboticsBASE_URL = “https://api.statbotics.io/v3"PAGE_SIZE = 1000 # RESTページサイズ# —————————–# ユーティリティ:型・タイプ正規化# —————————–def normalize_event_type(raw: Any) -> str:s=str(raw).strip().lower()fora, bin [(“-“, “_"), (" “, “_")]:s=s.replace(a, b)aliases= {“rgional": “regional",“regionals": “regional",“dist": “district",“dcmp": “district_cmp",“district_champ": “district_cmp",“districtcmp": “district_cmp",“cmp_div": “champs_div",“cmpdiv": “champs_div",“champsdiv": “champs_div",“cmp": “champs",“champ": “champs",“championship": “champs",“champsionship": “champs",“einsten": “einstein",}returnaliases.get(s, s)CHAMP_TYPES_FULL = {“champs", “champs_div", “cmp", “cmp_div", “einstein"}CHAMP_TYPES_NO_EINSTEIN = {“champs", “champs_div", “cmp", “cmp_div"}def is_championship_type(t_norm: str, include_einstein: bool) -> bool:returnt_normin (CHAMP_TYPES_FULLifinclude_einsteinelseCHAMP_TYPES_NO_EINSTEIN)# —————————–# REST (/team_events のみ)# —————————–def make_session() -> requests.Session:s=CacheControl(requests.Session())s.headers.update({“User-Agent": “champ-epa-regional-only/1.1"})returnsdef api_get_json(sess: requests.Session, path: str, params: Optional[Dict[str, Any]] = None, retries: int = 3) -> Any:url=BASE_URL+pathbackoff=1.0forattemptinrange(retries):try:r=sess.get(url, params=params, timeout=30)ifr.status_code==200:returnr.json()ifr.status_codein (429, 502, 503, 504):time.sleep(backoff); backoff*=2continuer.raise_for_status()exceptException:ifattempt<retries-1:time.sleep(backoff); backoff*=2continueraiseraiseRuntimeError(f"GET failed after retries: {url} params={params}")def fetch_team_events_for_event(sess: requests.Session, event_code: str, page_size: int = PAGE_SIZE) -> List[Dict[str, Any]]:rows: List[Dict[str, Any]] = []offset=0whileTrue:data=api_get_json(sess, “/team_events", {“event": event_code, “limit": page_size, “offset": offset})ifnotisinstance(data, list):raiseRuntimeError(f"Unexpected /team_events response for event={event_code}")rows.extend(data)iflen(data) <page_size:breakoffset+=page_sizereturnrows# —————————–# EPA & team id 抽出# —————————–def pick_epa_from_team_event(row: Dict[str, Any]) -> Optional[float]:epa=row.get(“epa")ifisinstance(epa, dict):tp=epa.get(“total_points")ifisinstance(tp, dict):v=tp.get(“mean")ifisinstance(v, (int, float)): returnfloat(v)br=epa.get(“breakdown")ifisinstance(br, dict):v=br.get(“total_points")ifisinstance(v, (int, float)): returnfloat(v)v=epa.get(“mean")ifisinstance(v, (int, float)): returnfloat(v)forkin (“total", “total_epa", “epa_total"):v=epa.get(k)ifisinstance(v, (int, float)): returnfloat(v)forkin (“epa_total_points", “epa_mean", “epa_total"):v=row.get(k)ifisinstance(v, (int, float)): returnfloat(v)ifisinstance(epa, (int, float)): returnfloat(epa)returnNonedef get_team_id_from_row(row: Dict[str, Any]) -> Optional[int]:if"team"inrow:try:returnint(row[“team"])exceptException:passkey=row.get(“team_key")ifisinstance(key, str) andkey.lower().startswith(“frc"):try:returnint(key[3:])exceptException:returnNonereturnNone# —————————–# 統計ユーティリティ# —————————–def percentile_rank(values: np.ndarray, target: float) -> float:returnfloat((values<=target).mean() *100.0)def cohens_d_single_vs_dist(single_val: float, dist_vals: np.ndarray) -> float:x1=np.array([single_val], dtype=float)x2=np.asarray(dist_vals, dtype=float)n2=len(x2)s2=np.std(x2, ddof=1) ifn2>1else0.0ifn2<=1ors2==0.0: returnfloat(“nan")return (x1.mean() -x2.mean()) /s2# 単一点 vs 分布 → zに一致def bootstrap_pvalue_greater(other_vals: np.ndarray, champ_val: float, n_boot: int = 5000, seed: int = 0) -> float:rng=np.random.default_rng(seed)x=np.asarray(other_vals, dtype=float)y=np.array([champ_val], dtype=float)diff_obs=y.mean() -x.mean()pool=np.concatenate([x, y])n_x, n_y=len(x), len(y)diffs= []for_inrange(n_boot):x_b=rng.choice(pool, size=n_x, replace=True)y_b=rng.choice(pool, size=n_y, replace=True)diffs.append(y_b.mean() -x_b.mean())diffs=np.array(diffs)returnfloat((diffs>=diff_obs).mean())# —————————–# チーム所属:Regional集合の構築# —————————–def is_non_district_team(year_row: Dict[str, Any]) -> bool:“""team_years 1行から 'districtに所属していない’ を判定。代表的キー: 'district’(None または " なら非district)フォールバック: 'district_key’ / 'district_name’ などが空/Noneでも非district扱い“""forkin (“district", “district_key", “district_name"):ifkinyear_row:v=year_row.get(k)ifvisNone: returnTrueifisinstance(v, str) andv.strip() =="": returnTruereturnFalse# 何もキーがないときは保守的に FalsereturnFalsedef make_events_dataframe(sb: Statbotics, year: int) -> pd.DataFrame:events: List[Dict[str, Any]] = []offset=0whileTrue:batch=sb.get_events(year=year, limit=PAGE_SIZE, offset=offset)ifnotisinstance(batch, list) orlen(batch) ==0:breakevents.extend(batch)iflen(batch) <PAGE_SIZE:breakoffset+=PAGE_SIZEifnotevents:raiseRuntimeError(f"No events found for {year}")df=pd.DataFrame(events)if"event"notindf.columns:forcandin (“key", “event_key", “code"):ifcandindf.columns:df=df.rename(columns={cand: “event"})breakif"event"notindf.columns:raiseRuntimeError(“イベントコード列(event/key/code)が見つかりません")df[“__type_norm"] =df[“type"].map(normalize_event_type)returndfdef build_regional_team_sets(sb: Statbotics, year: int, regional_events: List[str], sess: requests.Session) -> Tuple[Set[int], Set[int]]:“""戻り値:strict_set : 非district所属チーム(所属ベース)played_regio : その年に一度でも regional に出場したチーム(出場実績ベース)“""# strict_set:team_years から構築strict_set: Set[int] =set()offset=0whileTrue:batch=sb.get_team_years(year=year, limit=PAGE_SIZE, offset=offset)ifnotisinstance(batch, list) orlen(batch) ==0:breakforrowinbatch:try:team=int(row.get(“team"))exceptException:continueifis_non_district_team(row):strict_set.add(team)iflen(batch) <PAGE_SIZE:breakoffset+=PAGE_SIZE# played_regio:regional の team_events を総なめして収集played_regio: Set[int] =set()forevintqdm(regional_events, desc="Collecting teams that played regional (for eligible mode)"):try:te_rows=fetch_team_events_for_event(sess, ev, page_size=PAGE_SIZE)exceptExceptionase:print(f"[WARN] {ev}: /team_events failed while collecting regional teams: {e}", file=sys.stderr)continueforrowinte_rows:tid=get_team_id_from_row(row)iftidisnotNone:played_regio.add(tid)returnstrict_set, played_regio# —————————–# 下位 p% 抽出ヘルパ# —————————–def bottom_percent_values(vals: np.ndarray, percent: float) -> np.ndarray:“""配列 vals を昇順ソートして下位 p% を返す(少なくとも1件、端数は切り捨て)。"""ifvals.size==0:returnvalsk=int(np.floor(vals.size* (percent/100.0)))k=max(1, k)idx=np.argsort(vals, kind="mergesort") # 安定ソートreturnvals[idx][:k]# —————————–# メイン# —————————–def main():ap=argparse.ArgumentParser(description="Championship (Regional-only teams) vs Regional events — EPA comparison (Champ bottom p%)")ap.add_argument(“–year", type=int, default=2025, help="Season year (default: 2025)")ap.add_argument(“–exclude-einstein", action="store_true", help="Exclude Einstein from Championship set")ap.add_argument(“–metric", type=str, default="avg", choices=[“avg", “median", “min", “sd"], help="Comparison metric per event")ap.add_argument(“–regional-mode", type=str, default="strict", choices=[“strict", “eligible"],help="Regional team definition: strict(non-district only) or eligible(strict ∪ played regional). Default=strict")ap.add_argument(“–champ-bottom-percent", type=float, default=10.0,help="Championship は下位 p%% のチームのみで評価(既定: 10)")ap.add_argument(“–out-prefix", type=str, default="", help="Output filename prefix (optional)")args=ap.parse_args()year=args.yearinclude_einstein=notargs.exclude_einsteinmetric=args.metricregional_mode=args.regional_mode # 'strict’ (default) or 'eligible’champ_bottom_p=float(args.champ_bottom_percent)prefix= (args.out_prefix +"_") ifargs.out_prefix else""sb=Statbotics()sess=make_session()# 1) イベント一覧(Regional と Championship を抽出)ev_df=make_events_dataframe(sb, year)regional_events=ev_df.loc[ev_df[“__type_norm"] =="regional", “event"].astype(str).tolist()champ_df_meta=ev_df[ev_df[“__type_norm"].apply(lambdat: is_championship_type(t, include_einstein))].copy()ifchamp_df_meta.empty:raiseRuntimeError(“No Championship events detected with current settings (Einstein excluded? check).")champ_events=champ_df_meta[“event"].astype(str).tolist()# 2) Regionalチーム集合(strict / eligible)strict_set, played_regio=build_regional_team_sets(sb, year, regional_events, sess)allowed_teams: Set[int] =strict_setifregional_mode=="strict"else (strict_set|played_regio)# 3) Regional 各イベントの統計(allowed_teams 全体を使用)per_event_rows: List[Dict[str, Any]] = []forev_codeintqdm(regional_events, desc="Processing regional events"):try:te_rows=fetch_team_events_for_event(sess, ev_code, page_size=PAGE_SIZE)exceptExceptionase:print(f"[WARN] {ev_code}: /team_events failed: {e}", file=sys.stderr)continueepa_vals_all: List[float] = []forrowinte_rows:tid=get_team_id_from_row(row)iftidisNoneortidnotinallowed_teams:continuev=pick_epa_from_team_event(row)ifvisnotNoneandnp.isfinite(v):epa_vals_all.append(float(v))iflen(epa_vals_all) >=3:arr_all=np.array(epa_vals_all, dtype=float)n_used=arr_all.size# regional側は全量使用per_event_rows.append({“event": ev_code,“event_name": ev_df.loc[ev_df[“event"] ==ev_code, “name"].values[0] if"name"inev_df.columnselseev_code,“type": “regional",“is_championship": False,“n_teams_total_filtered": int(arr_all.size),“n_teams_used": int(n_used),“avg_team_epa": float(np.mean(arr_all)),“sd_team_epa": float(np.std(arr_all, ddof=1)) ifarr_all.size>1elsefloat(“nan"),“median_team_epa": float(np.median(arr_all)),“min_team_epa": float(np.min(arr_all)),})else:print(f"[WARN] {ev_code}: insufficient filtered teams (n={len(epa_vals_all)})", file=sys.stderr)# 4) Championship(Regional-only teams)→ **下位 p%** だけでイベント統計forev_codeintqdm(champ_events, desc=f"Processing championship events (regional teams; bottom {champ_bottom_p:.1f}%)"):try:te_rows=fetch_team_events_for_event(sess, ev_code, page_size=PAGE_SIZE)exceptExceptionase:print(f"[WARN] {ev_code}: /team_events failed: {e}", file=sys.stderr)continueepa_vals_all: List[float] = []forrowinte_rows:tid=get_team_id_from_row(row)iftidisNoneortidnotinallowed_teams:continuev=pick_epa_from_team_event(row)ifvisnotNoneandnp.isfinite(v):epa_vals_all.append(float(v))# まず Regional-only でフィルタした総数を確認iflen(epa_vals_all) >=3:arr_all=np.array(epa_vals_all, dtype=float)# 下位 p% を抽出arr_bot=bottom_percent_values(arr_all, champ_bottom_p)ifarr_bot.size>=3:per_event_rows.append({“event": ev_code,“event_name": ev_df.loc[ev_df[“event"] ==ev_code, “name"].values[0] if"name"inev_df.columnselseev_code,“type": “championship",“is_championship": True,“n_teams_total_filtered": int(arr_all.size),“n_teams_used": int(arr_bot.size), # ★下位p%の採用数“avg_team_epa": float(np.mean(arr_bot)),“sd_team_epa": float(np.std(arr_bot, ddof=1)) ifarr_bot.size>1elsefloat(“nan"),“median_team_epa": float(np.median(arr_bot)),“min_team_epa": float(np.min(arr_bot)),})else:print(f"[WARN] {ev_code}: bottom {champ_bottom_p}% too small after filtering (n={arr_bot.size})", file=sys.stderr)else:print(f"[WARN] {ev_code}: insufficient filtered teams (n={len(epa_vals_all)})", file=sys.stderr)ifnotper_event_rows:raiseRuntimeError(“No events with sufficient filtered teams to compute stats.")per_event_df=pd.DataFrame(per_event_rows)# 5) 指標の選択と集約定義metric_col= {“avg": “avg_team_epa",“median": “median_team_epa",“min": “min_team_epa",“sd": “sd_team_epa",}[metric]defchamp_aggregate(df: pd.DataFrame) -> float:ifdf.empty: returnfloat(“nan")ifmetric=="avg":returnfloat(np.average(df[metric_col].values, weights=df[“n_teams_used"].values))elifmetric=="sd":vals=df[metric_col].valuesns=df[“n_teams_used"].valuesweights=np.maximum(ns-1, 1) # (n-1)重みreturnfloat(np.average(vals, weights=weights))else:# median / min はイベント値の単純平均returnfloat(np.mean(df[metric_col].values))# 6) CSV 出力(イベントごとの統計)out_csv= (f"{prefix}event_epa_averages_{year}_regional_only_mode-{regional_mode}_"f"{metric}_champ-bottom-{int(champ_bottom_p)}.csv")per_event_df.sort_values([“is_championship", metric_col, “n_teams_used"], ascending=[False, False, False]).to_csv(out_csv, index=False)# 7) 比較:Champ(Regional-only bottom p%) vs Regional(others)champ_df=per_event_df[per_event_df[“is_championship"] ==True] # noqa: E712others_df=per_event_df[per_event_df[“is_championship"] ==False] # noqa: E712ifchamp_df.emptyorothers_df.empty:raiseRuntimeError(“Not enough events in either Championship or Regional to compare.")champ_val=champ_aggregate(champ_df)other_vals=others_df[metric_col].valuesmu=float(np.mean(other_vals))sd=float(np.std(other_vals, ddof=1)) iflen(other_vals) >1elsefloat(“nan")z= (champ_val-mu) /sdifsdandnp.isfinite(sd) andsd>0elsefloat(“nan")d=cohens_d_single_vs_dist(champ_val, other_vals)pct=percentile_rank(other_vals, champ_val)p_boot=bootstrap_pvalue_greater(other_vals, champ_val)summary= {“year": year,“comparison_metric": metric,“regional_mode": regional_mode, # strict (default) / eligible“include_einstein": include_einstein,“champ_bottom_percent": champ_bottom_p,“championship_value": champ_val,“others_mean": mu,“others_sd": sd,“z_score_vs_others": z,“cohens_d_vs_others": d,“percentile_rank_vs_others": pct,“bootstrap_p_one_sided": p_boot,“n_events_championship": int(champ_df.shape[0]),“n_events_regional": int(others_df.shape[0]),“outputs": {“per_event_csv": out_csv},}out_json= (f"{prefix}championship_vs_regional_summary_{year}_mode-{regional_mode}_"f"{metric}_champ-bottom-{int(champ_bottom_p)}.json")withopen(out_json, “w", encoding="utf-8″) asf:json.dump(summary, f, ensure_ascii=False, indent=2)# 8) コンソール要約print(f"\n=== Championship (Regional-only teams, bottom {champ_bottom_p:.1f}%) vs Regional — {year} ===")print(f"Mode = {regional_mode} | Metric = {metric} | Einstein = {'included’ifinclude_einsteinelse’excluded’}")print(f"Championship value (bottom {champ_bottom_p:.1f}%): {champ_val:.2f}")print(f"Regional mean: {mu:.2f} (sd={sd:.2f}, n={len(other_vals)})")print(f"Z-score (Champ vs Regional): {z:.2f}")print(f"Cohen’s d: {d:.2f}")print(f"Percentile of regional: {pct:.1f}%")print(f"Bootstrap p (one-sided, Champ>Reg): {p_boot:.4f}")print(f"\nSaved per-event table -> {out_csv}")print(f"Saved summary JSON -> {out_json}")if __name__ == “__main__":try:main()exceptExceptionase:print(f"\n[ERROR] {e}\n", file=sys.stderr)print(“Tips:")print(" – Regional集合は –regional-mode で切替(既定: strict)。")print(" – Championship側は –champ-bottom-percent でサブセット率を変更できます。")print(" – /team_events のEPAフィールドが合わない場合は pick_epa_from_team_event() を調整してください。")sys.exit(1)