Skip to content

Latest commit

 

History

History
563 lines (419 loc) · 24.1 KB

File metadata and controls

563 lines (419 loc) · 24.1 KB

eventstats 仕様書

1. システム概要

FGO イベント報告データを集計し、アイテムドロップ率を算出するシステム。

  • データソース: FGO Harvest の JSON API
  • バックエンド: AWS Lambda (Python) による定期データ取得・集計
  • 公開画面: 静的サイト (Harvest とは独立) による集計結果表示
  • 管理画面: イベント管理・報告データ除外指定

2. アーキテクチャ

┌─────────────┐    定期実行     ┌──────────────┐
│ EventBridge │───────────────→│ 集計 Lambda  │
└─────────────┘                └──────┬───────┘
                                      │
                       ┌──────────────┼──────────────┐
                       ↓              ↓              ↓
                  events.json    Harvest API     中間JSON
                  (S3)           (データ取得)    (S3: 出力)

┌─────────────────┐         ┌──────────────┐
│    管理画面      │────────→│  管理API     │
│   (静的サイト)   │←────────│ (API Gateway │
└─────────────────┘         │  + Lambda)   │
                            └──────┬───────┘
                                   ↓
                            events.json
                            exclusions.json
                            (S3)

┌──────────────────────────────────────────────┐
│  公開画面 (静的サイト)                        │
│  中間JSON + exclusions.json → 集計表示       │
└──────────────────────────────────────────────┘

2.1 集計 Lambda

  • EventBridge で定期実行 (2時間ごと)
  • S3 上の events.json を読み取り、現在日時がいずれかのイベント期間内かを判定
  • 期間内のイベントがない場合は即終了 (コスト最小化)
  • 期間内のイベントがある場合、対象クエストの報告データを Harvest API から取得し中間 JSON を S3 に出力

2.2 管理 API

  • API Gateway + Lambda で構成
  • イベント CRUD: events.json の読み書き
  • 除外リスト管理: exclusions.json の読み書き
  • 認証方式は Cognito

2.3 公開画面

  • S3 上の中間 JSON と exclusions.json を fetch して表示
  • 除外リストを適用して最終的な集計値を算出
  • イベントのアイテム構成に依存しない汎用的な UI

2.4 管理画面

  • 静的サイトとしてホスティング (公開画面と同一オリジンまたは別パス)
  • 管理 API を呼び出してイベント・除外リストを操作

3. 管理データ

3.1 イベント定義 (events.json)

管理画面から 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 のみを使用する

3.2 除外リスト (exclusions.json)

管理画面から操作される。公開画面が読み取る。

{
  "XCtBEoEwgr6R": [
    {
      "reportId": "e7f8543e-0a4c-453a-93e1-00fa9f8726a0",
      "reason": "データ不整合 (イベントアイテムの報告形式が不正)"
    },
    {
      "reportId": "faf16796-3346-4681-ba8b-bd670aad47f0",
      "reason": "ぐん肥/のび肥/すく肥の値が明らかに異常"
    }
  ]
}
  • クエスト ID をキーとして、除外対象の報告 ID と理由を記載
  • 除外は報告単位 (report ID 単位)
  • 除外された報告は集計値に含めないが、公開画面上では「除外済み」として確認可能

4. Harvest API レスポンス

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

items の値に関する注意事項

  • 全ての値は文字列型で格納されている ("33", "NaN" 等)
  • "NaN" が含まれる場合がある (報告者が未計測の項目)

5. アイテム分類とデータ処理ルール

5.1 アイテムのカテゴリ

items のキー名により以下のカテゴリに分類する。

(a) 枠数報告アイテム (イベントアイテム)

キー名に (x数値) を含むもの。

例: ぐん肥(x3), のび肥(x3), すく肥(x3)

  • 集計対象とする
  • 値は「枠数 (何枠ドロップしたか)」を表す
  • 1回の周回で 1枠あたり x3 個ドロップする場合、実際の獲得数は 値 * 3

(b) 添字なしイベントアイテム

枠数報告と同じベース名だが (x数値) を含まないもの。

例: ぐん肥, のび肥, すく肥

  • 集計対象としない(素材としても集計しない)
  • 実数報告((xN) キーが一切ない報告)か混合報告((xN) キーと添字なしイベントアイテムが共存する報告)かを問わず、常に除外する

(c) ポイントアイテム

キー名が ポイント(+数値) にマッチするもの。

例: ポイント(+600), ポイント(+700), ポイント(+1200)

  • 集計対象とする
  • 値は「枠数 (何枠ドロップしたか)」を表す
  • 1周あたり期待値: Σ (1周あたり枠数 * ポイント量)

(d) QP アイテム

キー名が QP(+数値) にマッチするもの。

例: QP(+150000), QP(+175000), QP(+3400000)

  • 集計対象とする
  • ポイントアイテムと同じ仕様で処理する

(e) 通常アイテム (素材・スキル石等)

上記いずれにも該当しないもの。

例: 礼装, 心臓, , 殺秘, 殺魔, 殺輝, 殺モ

  • 集計対象とする
  • 値はドロップ個数そのもの

5.2 NaN の扱い

  • items の値が "NaN" の場合、そのアイテムは当該報告において未計測として扱う
  • 当該報告のそのアイテムを集計から除外する (報告全体を除外するわけではない)
  • 中間 JSON には null として保持し、フロントエンドでも参照可能とする

5.3 集計方法

各アイテムについて:

  • 合計ドロップ数 = 有効な報告の値の総和
  • 合計周回数 = 有効な報告の runcount の総和 (アイテムごとに異なりうる。NaN 報告を除くため)
  • ドロップ率 = 合計ドロップ数 / 合計周回数

5.4 アイテム表示優先度 (item_list_priority.json)

viewer/src/data/item_list_priority.json に配置され、viewer が TypeScript import でビルド時バンドルする。gen_item_list_priority.pymake gen-priority-file)で生成する。

各エントリのフィールド:

フィールド 説明
id number アイテム ID
rarity number レアリティ
shortname string アイテム名(報告 JSON の items キーと完全一致)
dropPriority number 表示優先度(大きいほど上位)

表示順序

dropPriority 降順 → 同値の場合は id 降順でソートする。

フィルタリング(集計テーブルへの適用)

以下をすべて満たすアイテムは集計テーブル(素材・イベントアイテム・ポイント・QP)の表示対象にならない:

  1. このリストに shortname が存在しない
  2. 添字つきイベントアイテム (xN) でない
  3. ポイント ポイント(+N) でない
  4. QP QP(+N) でない

報告一覧のカラムはこのフィルタの影響を受けない(未知アイテムもカラムとして表示される)。

報告者サマリ アコーディオン明細

未知アイテム(上記フィルタ対象)は既知アイテムの後ろに順不同で末尾表示する。

6. 中間 JSON 出力フォーマット

集計 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:ぐん肥,のび肥,すく肥(実数報告のため除外)"]
    }
  ]
}

6.1 中間 JSON の設計方針

  • 報告単位のデータを保持する: 集計済みサマリーだけでなく、個別報告を全て含める
  • 生キーを保持する: イベントアイテム (xN) やポイント (+N)、QP (+N) のキー名をそのまま保持し、集約・合算は行わない
    • 例: ぐん肥(x3): 1099 → そのまま保持
    • 例: ポイント(+600): 448 → そのまま保持
    • 集約・期待値の計算はフロントエンド側で行う
  • 値の型変換のみ行う: 文字列 → 数値変換、"NaN"null 変換
  • 添字なしイベントアイテムは除外する: 実数報告・混合報告を問わず、ベース名(添字なし)のイベントアイテムは items に含めず、warnings に除外理由を記録する
    • 実数報告((xN) キーなし)の場合: (実数報告のため除外)
    • (xN) キーと添字なしイベントアイテムが混在する場合: (添字なしイベントアイテムのため除外)
  • warnings フィールド: 処理中に検出された注意事項を記録 (フロントエンドでの表示に使える)

6.2 items のキー名

中間 JSON では、元データのキー名をそのまま保持する。

元データ 中間 JSON のキー
ぐん肥(x3) ぐん肥(x3) (そのまま)
ポイント(+600) ポイント(+600) (そのまま)
QP(+150000) QP(+150000) (そのまま)
心臓 心臓 (そのまま)

7. データ上の注意すべきパターン

実データ (XCtBEoEwgr6R.json) から確認できたイレギュラーケース:

7.1 実数報告と枠数報告の混在

同一クエストに対して、イベントアイテムを枠数 (ぐん肥(x3)) で報告する人と実数 (ぐん肥) で報告する人が混在する。

  • 枠数報告のみを集計対象とする
  • 実数報告((xN) キーが一切ない報告)では、イベントアイテムを集計から除外するが、通常アイテム (素材等) は集計に含める

7.2 NaN を含む報告

"NaN" は未計測を意味する。アイテム単位で除外し、報告全体は除外しない。

例:

  • 礼装: "NaN" — 礼装のドロップを計測していない報告が複数存在
  • ぐん肥: "NaN" 等 — イベントアイテム全てが NaN のケース (report_id: 69d4e11f)

7.3 明らかなデータ異常

report_id: faf16796 — イベントアイテムの値が同一報告内で大きくばらつく。

ぐん肥: 501 (~4.0/周), のび肥: 933 (~7.5/周), すく肥: 2202 (~17.6/周)

他の報告者は 3種の値がほぼ均等 (~33/周) であり、明らかなデータ入力ミスと推定される。このような報告は除外リストで対応する。

7.4 イベントアイテム未報告

report_id: 605fc0f1items にイベントアイテム (ぐん肥/のび肥/すく肥) が存在しない。

通常アイテムのみの集計には使えるが、イベントアイテムについては当然ながら集計対象外となる。

7.5 枠数報告での一部キー未入力

同一 baseName のキー(例: ミトン(x1)ミトン(x3))のうち、一方しか入力せずに報告されるケースがある。

  • 未入力のキーは items に存在しないため、aggregate() がその報告でそのキーを null 扱いにする
  • 結果として同じ baseName 内でもキーによって totalRuns が異なる
  • イベントアイテムサマリの「周回数」列にはこの差異が反映される。baseName グループ内でずれが生じた場合は最大値を採用する

7.6 混合報告((xN) と添字なしイベントアイテムの共存)

(xN) キーを持つアイテムと、添字なしイベントアイテム(例: 三角巾)が同一報告内に混在するケース。

  • (xN) キーが存在するため実数報告とは判定されない
  • しかし添字なしイベントアイテムはやはり集計に含めるべきでない(枠数として解釈できないため)
  • → 実数報告かどうかに関わらず、ベース名がイベントアイテムと一致するキーは常に除外する (→ 5.1 (b))

7.7 Harvest ページ分割 (1:N ソースマッピング)

同一クエストのデータが Harvest 上で複数のページ ID に分割されている場合がある(例: イベント期間途中でページが分割されたケース)。

このような場合は events.jsonquests[].sourceQuestIds に全ページ ID を列挙することで対応する:

{
  "questId": "XCtBEoEwgr6R",
  "name": "...",
  "level": "90+",
  "ap": 40,
  "sourceQuestIds": ["XCtBEoEwgr6R", "Ab12CdEfGhIj"]
}
  • 集計 Lambda は sourceQuestIds の各 ID から報告を取得し、重複排除後にマージして1つの中間 JSON を生成する
  • 出力ファイルパスは questId を使用 ({eventId}/{questId}.json)
  • 公開画面のルーティングや除外リストの管理は変わらない
  • sourceQuestIds 省略または空配列の場合は questId のみを使用する(後方互換)

7.8 同一報告者の複数報告

同一の reporter_id で複数の報告が存在する (例: 豆ぽ 5件、シェリル 6件)。これらは別々の周回期間の報告であり、それぞれ独立した報告として集計する。

8. Lambda 実行スケジュール

8.1 方式

  • EventBridge のスケジュールルールで定期実行 (2時間ごと)
  • Lambda は起動時に S3 の events.json を読み取る
  • 現在日時が period.startperiod.end の範囲内にあるイベントを抽出
  • 対象イベントがなければ即終了

8.2 運用フロー

  1. イベント開始前: 管理画面からイベント情報 (期間・クエスト一覧) を登録
  2. イベント期間中: Lambda が自動的にデータ取得・中間 JSON 更新
  3. イベント終了後: 期間が過ぎれば Lambda は自動的にスキップ (手動操作不要)

8.3 コスト

  • 即終了の Lambda 実行 (128MB, ~100ms) は実質無料
  • データ処理ありの場合も、1時間1回程度であればフリーティア内に収まる見込み

9. 管理画面

9.1 イベント管理

  • イベントの追加・編集・削除
  • イベントごとにクエスト一覧を設定
  • 必要な入力項目: イベント名、期間 (開始・終了)、クエスト (ID・名前・推奨レベル・消費AP)
  • クエスト入力補助: Harvest の all.json (https://fgojunks.max747.org/harvest/contents/quest/all.json) からクエスト候補を取得
    • is_freequest = false かつ since がイベント期間内にあるものをフィルタ
    • 候補から選択すると、クエスト ID と名前が自動入力される
    • 推奨レベルと消費 AP は all.json に含まれないため手動入力

9.2 データ管理 (除外指定)

  • 中間 JSON の報告一覧を表示
  • 各報告に対して除外/除外解除を操作
  • 除外理由を記入
  • 操作結果は exclusions.json に保存

9.3 管理 API エンドポイント

メソッド パス 説明
GET /events イベント一覧取得
POST /events イベント追加
PUT /events/{eventId} イベント編集
DELETE /events/{eventId} イベント削除
GET /exclusions/{questId} 除外リスト取得
PUT /exclusions/{questId} 除外リスト更新

9.4 認証

  • 管理画面・管理 API へのアクセスには認証が必要
  • Amazon Cognito User Pool を使用
    • 管理者アカウントを1つ作成し、ユーザー名/パスワードでログイン
    • API Gateway の Cognito オーソライザーで管理 API を保護
    • フロントエンド側では AWS Amplify Auth (または amazon-cognito-identity-js) でトークン管理
    • API キーのような手動管理が不要で、トークンの取得・リフレッシュは SDK が自動処理

10. 公開画面

10.1 基本機能

  • 中間 JSON を fetch して報告一覧と集計値を表示
  • exclusions.json を fetch して除外を適用し最終集計を算出
  • アイテム名を動的にカラム化し、イベントごとの UI 変更を不要にする

10.2 表示項目

  • イベント選択、クエスト選択 (推奨レベル順、デフォルトは最高レベル)
  • 選択中のイベント名を見出し表示、集計期間を併記
  • クエスト情報 (名前、推奨レベル、消費 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 の場合(全員同じ値)は検出しない
    • 除外済み報告のセルはハイライト対象外
    • 見出しとテーブルの間に色付きセルの凡例を表示

イベントアイテムサマリ (イベント全体)

  • 全クエストを横断してイベントアイテムの獲得数期待値を比較できるビュー
  • アイテムベース名ごとにセクションを分けて表示
  • 各セクション内のテーブル: 行=クエスト (Lv 表示のみ)、列=合計枠数 / 周回数 / 枠数 / +0 〜 +12
    • 「合計枠数」は baseName グループ全キーの枠数合計
    • 「周回数」はその baseName グループを有効に集計できた報告の合計周回数
    • 同一クエスト内でも baseName が異なれば周回数が異なりうる (一部報告で対応するキーが未入力の場合)
    • baseName グループ内で複数キーの周回数がずれる場合は最大値を採用する (→ 7.5)
  • 該当クエストにそのアイテムが存在しない場合は "-" を表示

報告者サマリ (イベント全体)

  • 全クエスト横断で報告者単位に集約
  • 報告者数、総報告数、総周回数のヘッダー表示
  • 報告者ごとの報告数、合計周回数 (ソート可能、デフォルトは合計周回数の降順)
  • アコーディオン展開で報告明細を確認可能
  • 報告者名から fgodrop / X へのリンク

10.3 URL ルーティング

パスベースのパーマリンクにより、各画面に固有の URL を持たせる。

パス 表示内容
/eventstats/ 最新イベントの最高難度クエストへリダイレクト
/eventstats/events/:eventId そのイベントの最高難度クエストへリダイレクト
/eventstats/events/:eventId/quests/:questId クエスト詳細
/eventstats/events/:eventId/reporters 報告者サマリ
/eventstats/events/:eventId/event-items イベントアイテムサマリ
  • react-router-domcreateBrowserRouter を使用 (basename: "/eventstats")
  • ブラウザの戻る/進むが機能する
  • GitHub Pages SPA 対応: ビルド時に index.html404.html にコピー

10.4 技術スタック

  • GitHub Pages でホスティング (GitHub Actions で自動デプロイ)
  • Harvest とは独立して運用
  • React + TypeScript (Vite)
  • react-router-dom (クライアントサイドルーティング)

11. 技術スタック

レイヤー 技術
フロントエンド (管理画面・公開画面) React
バックエンド (集計 Lambda・管理 API Lambda) Python
インフラ定義 Terraform
認証 Amazon Cognito User Pool
API Amazon API Gateway
ストレージ Amazon S3
スケジューラ Amazon EventBridge