FGO イベント報告データを集計し、アイテムドロップ率を算出するシステム。
- データソース: FGO Harvest の JSON API
- バックエンド: AWS Lambda (Python) による定期データ取得・集計
- 公開画面: 静的サイト (Harvest とは独立) による集計結果表示
- 管理画面: イベント管理・報告データ除外指定
┌─────────────┐ 定期実行 ┌──────────────┐
│ EventBridge │───────────────→│ 集計 Lambda │
└─────────────┘ └──────┬───────┘
│
┌──────────────┼──────────────┐
↓ ↓ ↓
events.json Harvest API 中間JSON
(S3) (データ取得) (S3: 出力)
┌─────────────────┐ ┌──────────────┐
│ 管理画面 │────────→│ 管理API │
│ (静的サイト) │←────────│ (API Gateway │
└─────────────────┘ │ + Lambda) │
└──────┬───────┘
↓
events.json
exclusions.json
(S3)
┌──────────────────────────────────────────────┐
│ 公開画面 (静的サイト) │
│ 中間JSON + exclusions.json → 集計表示 │
└──────────────────────────────────────────────┘
- EventBridge で定期実行 (2時間ごと)
- S3 上の
events.jsonを読み取り、現在日時がいずれかのイベント期間内かを判定 - 期間内のイベントがない場合は即終了 (コスト最小化)
- 期間内のイベントがある場合、対象クエストの報告データを Harvest API から取得し中間 JSON を S3 に出力
- API Gateway + Lambda で構成
- イベント CRUD:
events.jsonの読み書き - 除外リスト管理:
exclusions.jsonの読み書き - 認証方式は Cognito
- S3 上の中間 JSON と
exclusions.jsonを fetch して表示 - 除外リストを適用して最終的な集計値を算出
- イベントのアイテム構成に依存しない汎用的な UI
- 静的サイトとしてホスティング (公開画面と同一オリジンまたは別パス)
- 管理 API を呼び出してイベント・除外リストを操作
管理画面から CRUD 操作される。集計 Lambda が読み取る。
{
"events": [
{
"eventId": "2026-02-valentines",
"name": "バレンタインイベント 2026",
"period": {
"start": "2026-01-29T18:00:00+09:00",
"end": "2026-02-12T12:59:59+09:00"
},
"quests": [
{
"questId": "XCtBEoEwgr6R",
"name": "花畑作り クワ振りの極意を学ぼう",
"level": "90+",
"ap": 40
}
]
}
]
}| フィールド | 型 | 説明 |
|---|---|---|
eventId |
string | イベントの一意識別子 (管理用) |
name |
string | イベント名 |
period.start |
string (ISO 8601) | イベント開始日時 |
period.end |
string (ISO 8601) | イベント終了日時 |
quests |
array | 集計対象クエストの一覧 |
quests[].questId |
string | Harvest 上のクエスト ID (ページ ID)。公開画面の URL キーおよび中間 JSON のファイルキーに使われる |
quests[].name |
string | クエスト名 |
quests[].level |
string | 推奨レベル |
quests[].ap |
number | 消費 AP |
quests[].sourceQuestIds |
string[] (省略可) | 集計元の Harvest ページ ID リスト。複数ページに分割されているクエストを統合する際に指定する。省略または空配列の場合は questId のみを使用する |
管理画面から操作される。公開画面が読み取る。
{
"XCtBEoEwgr6R": [
{
"reportId": "e7f8543e-0a4c-453a-93e1-00fa9f8726a0",
"reason": "データ不整合 (イベントアイテムの報告形式が不正)"
},
{
"reportId": "faf16796-3346-4681-ba8b-bd670aad47f0",
"reason": "ぐん肥/のび肥/すく肥の値が明らかに異常"
}
]
}- クエスト ID をキーとして、除外対象の報告 ID と理由を記載
- 除外は報告単位 (report ID 単位)
- 除外された報告は集計値に含めないが、公開画面上では「除外済み」として確認可能
https://fgojunks.max747.org/harvest/contents/quest/<questId>.json
レスポンスは報告オブジェクトの配列。各報告の構造:
| フィールド | 型 | 説明 |
|---|---|---|
id |
string (UUID) | 報告の一意識別子 |
report_id |
string (UUID) | id と同値 |
reporter |
string | 報告者のアカウント名 |
reporter_id |
string (UUID) | 報告者 ID (匿名報告の場合は空文字列) |
reporter_name |
string | 報告者の表示名 |
runcount |
number | 周回数 |
items |
object | アイテム名をキー、ドロップ数 (文字列) を値とするマップ |
note |
string | 報告者によるメモ |
timestamp |
string (ISO 8601) | 報告日時 |
quest_id |
string | クエスト ID |
- 全ての値は文字列型で格納されている (
"33","NaN"等) "NaN"が含まれる場合がある (報告者が未計測の項目)
items のキー名により以下のカテゴリに分類する。
キー名に (x数値) を含むもの。
例: ぐん肥(x3), のび肥(x3), すく肥(x3)
- 集計対象とする
- 値は「枠数 (何枠ドロップしたか)」を表す
- 1回の周回で 1枠あたり x3 個ドロップする場合、実際の獲得数は
値 * 3
枠数報告と同じベース名だが (x数値) を含まないもの。
例: ぐん肥, のび肥, すく肥
- 集計対象としない(素材としても集計しない)
- 実数報告(
(xN)キーが一切ない報告)か混合報告((xN)キーと添字なしイベントアイテムが共存する報告)かを問わず、常に除外する
キー名が ポイント(+数値) にマッチするもの。
例: ポイント(+600), ポイント(+700), ポイント(+1200)
- 集計対象とする
- 値は「枠数 (何枠ドロップしたか)」を表す
- 1周あたり期待値:
Σ (1周あたり枠数 * ポイント量)
キー名が QP(+数値) にマッチするもの。
例: QP(+150000), QP(+175000), QP(+3400000)
- 集計対象とする
- ポイントアイテムと同じ仕様で処理する
上記いずれにも該当しないもの。
例: 礼装, 心臓, 灰, 殺秘, 殺魔, 殺輝, 殺モ
- 集計対象とする
- 値はドロップ個数そのもの
itemsの値が"NaN"の場合、そのアイテムは当該報告において未計測として扱う- 当該報告のそのアイテムを集計から除外する (報告全体を除外するわけではない)
- 中間 JSON には
nullとして保持し、フロントエンドでも参照可能とする
各アイテムについて:
合計ドロップ数 = 有効な報告の値の総和合計周回数 = 有効な報告の runcount の総和(アイテムごとに異なりうる。NaN 報告を除くため)ドロップ率 = 合計ドロップ数 / 合計周回数
viewer/src/data/item_list_priority.json に配置され、viewer が TypeScript import でビルド時バンドルする。gen_item_list_priority.py(make gen-priority-file)で生成する。
各エントリのフィールド:
| フィールド | 型 | 説明 |
|---|---|---|
id |
number | アイテム ID |
rarity |
number | レアリティ |
shortname |
string | アイテム名(報告 JSON の items キーと完全一致) |
dropPriority |
number | 表示優先度(大きいほど上位) |
dropPriority 降順 → 同値の場合は id 降順でソートする。
以下をすべて満たすアイテムは集計テーブル(素材・イベントアイテム・ポイント・QP)の表示対象にならない:
- このリストに
shortnameが存在しない - 添字つきイベントアイテム
(xN)でない - ポイント
ポイント(+N)でない - QP
QP(+N)でない
報告一覧のカラムはこのフィルタの影響を受けない(未知アイテムもカラムとして表示される)。
未知アイテム(上記フィルタ対象)は既知アイテムの後ろに順不同で末尾表示する。
集計 Lambda が S3 に出力する。クエストごとに 1ファイル。
ファイルパス: <eventId>/<questId>.json
{
"quest": {
"questId": "XCtBEoEwgr6R",
"name": "花畑作り クワ振りの極意を学ぼう",
"level": "90+",
"ap": 40
},
"lastUpdated": "2026-02-08T17:30:00+09:00",
"reports": [
{
"id": "1572863d-39ab-46f9-b70b-8a8b557b3c6d",
"reporter": "max747_fgo",
"reporterName": "まっくす",
"runcount": 100,
"timestamp": "2026-02-08T16:17:03+09:00",
"note": "...",
"items": {
"礼装": 3,
"心臓": 33,
"灰": 101,
"殺秘": 27,
"殺魔": 16,
"殺輝": 16,
"殺モ": 32,
"ポイント(+600)": 448,
"ポイント(+700)": 394,
"ポイント(+1200)": 201,
"ぐん肥(x3)": 1099,
"のび肥(x3)": 1132,
"すく肥(x3)": 1115
},
"warnings": []
},
{
"id": "a2b8329f-09bf-4390-aaeb-70b83d306f7a",
"reporter": "jackalfgo",
"reporterName": "じゃっかる",
"runcount": 500,
"timestamp": "2026-02-08T12:36:05+09:00",
"note": "心臓泥UP %",
"items": {
"礼装": null,
"心臓": 154,
"灰": 472,
"殺秘": 145,
"殺魔": 72,
"殺輝": 90,
"殺モ": 160
},
"warnings": ["excluded_items:ぐん肥,のび肥,すく肥(実数報告のため除外)"]
}
]
}- 報告単位のデータを保持する: 集計済みサマリーだけでなく、個別報告を全て含める
- 生キーを保持する: イベントアイテム
(xN)やポイント(+N)、QP(+N)のキー名をそのまま保持し、集約・合算は行わない- 例:
ぐん肥(x3): 1099→ そのまま保持 - 例:
ポイント(+600): 448→ そのまま保持 - 集約・期待値の計算はフロントエンド側で行う
- 例:
- 値の型変換のみ行う: 文字列 → 数値変換、
"NaN"→null変換 - 添字なしイベントアイテムは除外する: 実数報告・混合報告を問わず、ベース名(添字なし)のイベントアイテムは
itemsに含めず、warningsに除外理由を記録する- 実数報告(
(xN)キーなし)の場合:(実数報告のため除外) (xN)キーと添字なしイベントアイテムが混在する場合:(添字なしイベントアイテムのため除外)
- 実数報告(
- warnings フィールド: 処理中に検出された注意事項を記録 (フロントエンドでの表示に使える)
中間 JSON では、元データのキー名をそのまま保持する。
| 元データ | 中間 JSON のキー |
|---|---|
ぐん肥(x3) |
ぐん肥(x3) (そのまま) |
ポイント(+600) |
ポイント(+600) (そのまま) |
QP(+150000) |
QP(+150000) (そのまま) |
心臓 |
心臓 (そのまま) |
実データ (XCtBEoEwgr6R.json) から確認できたイレギュラーケース:
同一クエストに対して、イベントアイテムを枠数 (ぐん肥(x3)) で報告する人と実数 (ぐん肥) で報告する人が混在する。
- 枠数報告のみを集計対象とする
- 実数報告(
(xN)キーが一切ない報告)では、イベントアイテムを集計から除外するが、通常アイテム (素材等) は集計に含める
"NaN" は未計測を意味する。アイテム単位で除外し、報告全体は除外しない。
例:
礼装: "NaN"— 礼装のドロップを計測していない報告が複数存在ぐん肥: "NaN"等 — イベントアイテム全てが NaN のケース (report_id: 69d4e11f)
report_id: faf16796 — イベントアイテムの値が同一報告内で大きくばらつく。
ぐん肥: 501 (~4.0/周), のび肥: 933 (~7.5/周), すく肥: 2202 (~17.6/周)
他の報告者は 3種の値がほぼ均等 (~33/周) であり、明らかなデータ入力ミスと推定される。このような報告は除外リストで対応する。
report_id: 605fc0f1 — items にイベントアイテム (ぐん肥/のび肥/すく肥) が存在しない。
通常アイテムのみの集計には使えるが、イベントアイテムについては当然ながら集計対象外となる。
同一 baseName のキー(例: ミトン(x1) と ミトン(x3))のうち、一方しか入力せずに報告されるケースがある。
- 未入力のキーは
itemsに存在しないため、aggregate()がその報告でそのキーをnull扱いにする - 結果として同じ baseName 内でもキーによって
totalRunsが異なる - イベントアイテムサマリの「周回数」列にはこの差異が反映される。baseName グループ内でずれが生じた場合は最大値を採用する
(xN) キーを持つアイテムと、添字なしイベントアイテム(例: 三角巾)が同一報告内に混在するケース。
(xN)キーが存在するため実数報告とは判定されない- しかし添字なしイベントアイテムはやはり集計に含めるべきでない(枠数として解釈できないため)
- → 実数報告かどうかに関わらず、ベース名がイベントアイテムと一致するキーは常に除外する (→ 5.1 (b))
同一クエストのデータが Harvest 上で複数のページ ID に分割されている場合がある(例: イベント期間途中でページが分割されたケース)。
このような場合は events.json の quests[].sourceQuestIds に全ページ ID を列挙することで対応する:
{
"questId": "XCtBEoEwgr6R",
"name": "...",
"level": "90+",
"ap": 40,
"sourceQuestIds": ["XCtBEoEwgr6R", "Ab12CdEfGhIj"]
}- 集計 Lambda は
sourceQuestIdsの各 ID から報告を取得し、重複排除後にマージして1つの中間 JSON を生成する - 出力ファイルパスは
questIdを使用 ({eventId}/{questId}.json) - 公開画面のルーティングや除外リストの管理は変わらない
sourceQuestIds省略または空配列の場合はquestIdのみを使用する(後方互換)
同一の reporter_id で複数の報告が存在する (例: 豆ぽ 5件、シェリル 6件)。これらは別々の周回期間の報告であり、それぞれ独立した報告として集計する。
- EventBridge のスケジュールルールで定期実行 (2時間ごと)
- Lambda は起動時に S3 の
events.jsonを読み取る - 現在日時が
period.start〜period.endの範囲内にあるイベントを抽出 - 対象イベントがなければ即終了
- イベント開始前: 管理画面からイベント情報 (期間・クエスト一覧) を登録
- イベント期間中: Lambda が自動的にデータ取得・中間 JSON 更新
- イベント終了後: 期間が過ぎれば Lambda は自動的にスキップ (手動操作不要)
- 即終了の Lambda 実行 (128MB, ~100ms) は実質無料
- データ処理ありの場合も、1時間1回程度であればフリーティア内に収まる見込み
- イベントの追加・編集・削除
- イベントごとにクエスト一覧を設定
- 必要な入力項目: イベント名、期間 (開始・終了)、クエスト (ID・名前・推奨レベル・消費AP)
- クエスト入力補助: Harvest の
all.json(https://fgojunks.max747.org/harvest/contents/quest/all.json) からクエスト候補を取得is_freequest = falseかつsinceがイベント期間内にあるものをフィルタ- 候補から選択すると、クエスト ID と名前が自動入力される
- 推奨レベルと消費 AP は
all.jsonに含まれないため手動入力
- 中間 JSON の報告一覧を表示
- 各報告に対して除外/除外解除を操作
- 除外理由を記入
- 操作結果は
exclusions.jsonに保存
| メソッド | パス | 説明 |
|---|---|---|
| GET | /events |
イベント一覧取得 |
| POST | /events |
イベント追加 |
| PUT | /events/{eventId} |
イベント編集 |
| DELETE | /events/{eventId} |
イベント削除 |
| GET | /exclusions/{questId} |
除外リスト取得 |
| PUT | /exclusions/{questId} |
除外リスト更新 |
- 管理画面・管理 API へのアクセスには認証が必要
- Amazon Cognito User Pool を使用
- 管理者アカウントを1つ作成し、ユーザー名/パスワードでログイン
- API Gateway の Cognito オーソライザーで管理 API を保護
- フロントエンド側では AWS Amplify Auth (または amazon-cognito-identity-js) でトークン管理
- API キーのような手動管理が不要で、トークンの取得・リフレッシュは SDK が自動処理
- 中間 JSON を fetch して報告一覧と集計値を表示
exclusions.jsonを fetch して除外を適用し最終集計を算出- アイテム名を動的にカラム化し、イベントごとの UI 変更を不要にする
- イベント選択、クエスト選択 (推奨レベル順、デフォルトは最高レベル)
- 選択中のイベント名を見出し表示、集計期間を併記
- クエスト情報 (名前、推奨レベル、消費 AP)
- 更新日時、有効報告数、合計周回数 (カード表示)、データ更新間隔の注記
- 素材テーブル: 合計ドロップ、合計周回数、ドロップ率、95% 信頼区間幅 (Wilson スコア法)
- イベントアイテムテーブル: 合計ドロップ、合計周回数、1周あたり枠数
- イベントアイテム獲得数期待値表: ベースアイテム単位で +0 〜 +12 ボーナス時の1周あたり獲得数期待値
- ポイント / QP テーブル: 合計、合計周回数、1周あたり枠数、1周あたり期待値 (枠数 × ボーナス値)、合計行
- 個別報告一覧 (除外状態を含む)
- 報告者・周回数・日時でソート可能
- 報告者名は fgodrop へのリンク、メモ内の fgosccnt URL は自動リンク化
- 異常値検出: 各セルの1周あたりの値が全体から大きく乖離している場合にハイライト表示
- z-score の絶対値が 3.0 を超えるセルを薄い赤背景 (
#fde8e8) で強調 - ホバー時に z-score を title 属性で表示
- 検出対象: イベントアイテム
(xN)・ポイント(+N)・QP(+N)は常に対象、通常アイテムはドロップ率 50% 以上のもののみ - 有効報告数が 500 件未満の場合は検出を抑制
- 周回数が 10 以下の報告は検出対象外
- 標準偏差がほぼ 0 の場合(全員同じ値)は検出しない
- 除外済み報告のセルはハイライト対象外
- 見出しとテーブルの間に色付きセルの凡例を表示
- z-score の絶対値が 3.0 を超えるセルを薄い赤背景 (
- 全クエストを横断してイベントアイテムの獲得数期待値を比較できるビュー
- アイテムベース名ごとにセクションを分けて表示
- 各セクション内のテーブル: 行=クエスト (Lv 表示のみ)、列=合計枠数 / 周回数 / 枠数 / +0 〜 +12
- 「合計枠数」は baseName グループ全キーの枠数合計
- 「周回数」はその baseName グループを有効に集計できた報告の合計周回数
- 同一クエスト内でも baseName が異なれば周回数が異なりうる (一部報告で対応するキーが未入力の場合)
- baseName グループ内で複数キーの周回数がずれる場合は最大値を採用する (→ 7.5)
- 該当クエストにそのアイテムが存在しない場合は "-" を表示
- 全クエスト横断で報告者単位に集約
- 報告者数、総報告数、総周回数のヘッダー表示
- 報告者ごとの報告数、合計周回数 (ソート可能、デフォルトは合計周回数の降順)
- アコーディオン展開で報告明細を確認可能
- 報告者名から fgodrop / X へのリンク
パスベースのパーマリンクにより、各画面に固有の URL を持たせる。
| パス | 表示内容 |
|---|---|
/eventstats/ |
最新イベントの最高難度クエストへリダイレクト |
/eventstats/events/:eventId |
そのイベントの最高難度クエストへリダイレクト |
/eventstats/events/:eventId/quests/:questId |
クエスト詳細 |
/eventstats/events/:eventId/reporters |
報告者サマリ |
/eventstats/events/:eventId/event-items |
イベントアイテムサマリ |
react-router-domのcreateBrowserRouterを使用 (basename: "/eventstats")- ブラウザの戻る/進むが機能する
- GitHub Pages SPA 対応: ビルド時に
index.htmlを404.htmlにコピー
- GitHub Pages でホスティング (GitHub Actions で自動デプロイ)
- Harvest とは独立して運用
- React + TypeScript (Vite)
- react-router-dom (クライアントサイドルーティング)
| レイヤー | 技術 |
|---|---|
| フロントエンド (管理画面・公開画面) | React |
| バックエンド (集計 Lambda・管理 API Lambda) | Python |
| インフラ定義 | Terraform |
| 認証 | Amazon Cognito User Pool |
| API | Amazon API Gateway |
| ストレージ | Amazon S3 |
| スケジューラ | Amazon EventBridge |