富山県ホタルイカ身投げAI予測モデルを作った (地域別推定)

背景

富山県から新潟県西部の沿岸で、春に産卵のために浮上したホタルイカが沿岸に打ち上げられる現象は「ホタルイカの身投げ」と呼ばれます。この接岸したホタルイカを掬うべく、大勢の人間が真夜中の海岸に集結するわけですが、全くホタルイカが湧かないという日も珍しくないようです。

ホタルイカが湧く条件というのは経験則な面も多く、一般的に言われているのは、

  • 新月前後の夜
  • 月明かりがない
  • 波が穏やか
  • 雨が降っていない(淡水を嫌う?)

…など。

これを機械学習でモデル化し、未来の3日間において、ホタルイカが湧く/湧かないを予測するモデルを作りました。富山県沿岸を4地域に分割し、その地域における湧き確率を推定します。

実際に3日予想を推定 & 公開しているサイトはこちらになります: https://hotaruika.tatuiyo.xyz

ホタルイカ出現データ集め

スクレイピング

大抵のホタルイカハンターは、この掲示板を利用しています。そして「今日はこの場所で取れた/取れない」という情報に翻弄され、現地で右往左往しているようです。

https://rara.jp/hotaruika-toyama

なかなか古風な、香ばしい感じの掲示板ですが、ここが一番活発です。

この掲示板に投稿された全投稿をスクレイピングし、学習データとしました。この掲示板自体は2014年から運用されており、前身の掲示板もあるようですが、そちらはWayback Machineにも残っていませんでした。

2ch等も探してみましたが、あまり使えそうなデータはなさそうです。そもそもホタルイカ掬いが活発になり始めたのは2014年前後からのようなので、それ以上過去のデータは見つからなさそうですね。

投稿データの整形

LLMを用い、各投稿データから、ホタルイカが採集された場所とその量を定量的に集計しました。具体的には、以下のようなプロンプトで、またAPI料金がもったいないので研究室のGPUで勝手に動かしているローカルのQwen3.5-122B-A10Bを用いました。

みんな量を適当に申告する上に、地名は伏せ字にしたりしやがるのでなかなか正確に集計するのは難しいです。最終的には用いませんでしたが、投稿に気象情報が含まれている場合は同時に集計させました。

SYSTEM_PROMPT = """あなたは富山県のホタルイカ身投げ情報を分析するアシスタントです。

1. reason(判定理由):
   なぜその対象日、湧きレベル、天候数値にしたのか、その3点を必ず含めた判断根拠を簡潔に記述してください。

2. datetime(対象となるホタルイカ日付):
   ホタルイカの身投げは、およそ 21:00 から 翌 4:00 に行われ、日付をまたぎます。日付をまたいだ釣果報告の場合は、カレンダー上は前の日付として記述しなさい。(例:3/16 23:00 から 3/17 4:00 まで身投げが発生したとしても、すべて 3/16 とします。)
   21:00 から 4:00 の身投げ時間以外の投稿も注意すること。例えば、3/17 の昼や夕方に「今日は〇匹採れた」という報告があれば、文脈を読み、3/16 としてください。

3. location(場所):
   テキストから地名を抽出してください。伏字や通称も文脈から推測できれば抽出。
   ※場所の記載が一切ない場合は null にしてください。

4. catch_level(湧き量レベル):
   以下の基準で 0 〜 6 の数値で定量化してください。ホタルイカの単位は「匹」および「杯」で、ホタルイカ 1 匹のことを 1 杯と呼びます。バケツ 1 杯という意味ではないので注意しなさい。
    - 0: なし/ゼロ(イカなし、気配なし、ボウズ)
    - 1: 少量(1-9 匹 または 1-9 杯):はぐれイカ、偵察隊レベル
    - 2: 少し(10-49 匹 または 10-49 杯):ポツポツ掬える程度
    - 3: 通常 / プチ湧き(50-199 匹 / 50-199 杯 / 約 350g〜1.4kg):飽きない程度に獲れる
    - 4: 多い / チョイ湧き(200-599 匹 / 200-599 杯 / 約 1.4〜4.2kg):バケツ半分程度
    - 5: 非常に多い / 湧き(600-1499 匹 / 600-1499 杯 / 約 4.2〜10kg):クーラーボックス半分程度
    - 6: 爆湧き(約 1500 匹〜 / 約 10kg〜):クーラーボックス満タン〜複数
   ※釣果への言及がなく、気象条件のみの投稿(例:「波が高い」「風がない」)の場合は null にしてください。


5. weather_and_sea(環境条件):
   テキスト内に現地で確認した事実として言及がある場合のみ抽出してください。言及がない項目や「天気予報では〜」は null にしてください。
   - wave: 0(なし/穏やか), 1(少しあり), 2(高い/荒れている)
   - wind: 0(無風/微風), 1(少しあり), 2(強い/暴風)
   - turbidity: 0(なし/澄んでいる), 1(少しあり/笹濁り), 2(酷い/激濁り/コーヒー色)


【重要な注意事項】
1. 未来の意気込みや願望:「〜する予定」「〜したい」「クーラー満タンにして帰るぞ」など、事実ではないものは null にしてください。
2. 累計釣果:「今シーズン累計で〇匹」「〇回参戦して〇匹」など、その日単体の釣果ではないものは null にしてください。
3. 無関係な話題:ツアー告知、解禁日の解説、単なる挨拶、雑談、スパムなどは null にしてください。


【出力フォーマット】
以下の形式に従い、必ず JSON 配列として出力してください。
{
    "original_text": "日付、投稿時間を含めた投稿の元のテキスト",
    "reason": "対象日、湧きレベル、天候数値の判定理由",
    "datetime": "YYYY-MM-DD",
    "location": "地名または null",
    "catch_level": 数値または null,
    "weather_and_sea": {
    "wave": 数値または null,
    "wind": 数値または null,
    "turbidity": 数値または null
    }
}
"""

出力フォーマットも一応工夫しており、”original_text”で投稿データをまず復唱させたのち、”reason”を先に吐かせることで、矛盾のない判定を引き出すことができます。

ホタルイカ掬いは真夜中に行われるので、投稿日時が日付をまたぐと日付管理が面倒なことになります。上の指示にはどのように日付を扱うか書いていますが、少し複雑なのでLLMは混乱して間違えた日付を吐くことがありました。
仕方ないので、もう一度LLMを通し、正確な日付を出力させました。

BATCH_PROMPT = """以下の{count}件の投稿データがあります。

各投稿について、設定されている日付が正しいか、以下の判断基準に基づき判定してください。

ホタルイカの身投げは、およそ 21:00 から 翌 4:00 に行われ、日付をまたぎます。日付をまたいだ釣果報告の場合は、カレンダー上は前の日付として記述しなさい。(例:3/16 23:00 から 3/17 4:00 まで身投げが発生したとしても、すべて 3/16 とします。)
21:00 から 4:00 の身投げ時間以外の投稿も注意すること。例えば、3/17 の昼や夕方に「今日は〇匹採れた」という報告があれば、文脈を読み、3/16 としてください。

【投稿一覧】
{posts_text}

【出力形式】
以下の JSON 配列で回答してください。各投稿の順序は入力と同じにすること。
各項目は以下の順で出力:
1. original_text: 投稿の元のテキスト(入力と同じ)
2. reason: 日付判定の理由
3. correct_datetime: 正しい日付(YYYY-MM-DD)

[
    {{
        "original_text": "投稿テキスト",
        "reason": "判断理由",
        "correct_datetime": "YYYY-MM-DD"
    }},
    ...
]
"""

そして、地名の抽出も怪しい。最初の指示では、とりあえず投稿内で出現した地名を全てリストアップしてしまっているので、結局どこで取れたのかよくわかりません。更にもう一度LLMを通します。

SYSTEM_PROMPT = """あなたはホタルイカ釣果報告から地名を抽出するアシスタントです。

【出力項目】
1. original_text: 投稿の元のテキスト(そのままコピー)
2. reason(判断理由):なぜその判断をしたか
3. location_primary(主要地名):釣りが行われた場所、または「行った」「釣れた」「採れた」など活動が発生した場所
4. location_secondary(二次地名):比較や言及だけされている場所(例:「四方は釣れたが岩瀬はダメ」の場合、岩瀬)

【出力形式(JSON 配列のみ)】
[
    {{
        "original_text": "投稿の元のテキスト",
        "reason": "判断理由",
        "location_primary": "地名または null",
        "location_secondary": ["地名 1", "地名 2"]
    }}
]

【注意事項】
- 地名の記載が一切ない場合は location_primary を null にすること
- original_text は入力テキストをそのままコピーすること
- 入力の順序と同じ順番で JSON オブジェクトを出力すること
- 現在の location 抽出結果を参考にし、より正確に判断すること"""

最後に、出現した地名を集め、手動でグループ化します。富山県西部から5グループに分け、番号を振りました。これが一番大変!

# 地域グループ定義(地名→グループ番号)
LOCATION_GROUPS = {
    # 西部から
    # アメダス伏木
    "国分": 1,
    "伏木": 1,
    "新湊": 1,

    # アメダス富山
    "海老江": 2,
    "足洗": 2,
    "八重津": 2,
    "打出": 2,
    "四方": 2,
    "岩瀬": 2,
    "浜黒": 2,
    "水橋": 2,

    # アメダス魚津
    "滑川": 3,
    "ホタルイカ": 3,
    "ほたるいか": 3,
    "ミュージアム": 3,
    "魚津": 3,
    "補助港": 3,
    "黒部": 3,
    "石田": 3,
    "生地": 3,

    # アメダス朝日
    "入善": 4,
    "朝日": 4,
    "宮崎": 4,
    "市振": 4,

    # アメダス糸魚川
    "姫川": 5,
    "糸魚川": 5,
    "ヒスイ": 5,

    # 細かな表記ゆれ
    "I 浜": 2,
    "I浜": 2,
    "あさひ": 4,
    "いわせ": 2,
    "うお": 3,
    "うちいで": 2,
    "えび": 2,
    "おさかなランド": 3,
    "けんか山": 1,
    "しんきろうロード": 3,
    "はまくろ": 2,
    "ひすい": 5,
    "ほたみゅー": 3,
    "ホタミュー": 3,
    "ミユージアム": 3,
    "やいづ": 2,
    "やえず": 2,
    "ヤエズ": 2,
    "よかた": 2,
    "キャンプ場": 2,
    "ダイヤ海岸": 4,
    "ハマクロ": 2,
    "ホテル古志": 2,
    "マリーナ": 2,
    "フィッシャリーナ": 2,
    "万葉": 1,
    "八重": 2,
    "八幡": 1,
    "上市川": 3,
    "下新川": 4,
    "古志": 2,
    "呉西": 1,
    "呉東": 3,
    "園": 4,
    "境海岸": 4,
    "大屋海岸": 4,
    "宮﨑": 4,
    "富山市": 2,
    "富山新港": 1,
    "射水": 1,
    "岩" : 2,
    "庄川": 1,
    "小矢部川": 1,
    "新川": 4,
    "新港": 1,
    "東部": 4,
    "東側": 4,
    "氷見": 1,
    "浜○崎": 2,
    "海の駅": 3,
    "海浜公園": 3,
    "海老": 2,
    "火力": 2,
    "発電所": 2,
    "県境": 4,
    "西部": 1,
    "神通川": 2,
    "蜃気楼": 3,
    "雨晴": 1,
    "親知": 5,
    "青海": 5,
    "高岡": 1,
    "高月": 3,
    "黒瀬": 2,
    "瀬浜": 2,
    "浜崎": 2,
    "西の": 2,
    "灯台": 2,
    "新漁港": 1,
    "新潟": 5
}

位置的にはこんな感じ。

最終的に、「日付, 場所, ホタルイカ採取量レベル」が各投稿から得られます。

ただし、ホタルイカ掬いは心理戦なので、掲示板に嘘投稿を流して動揺させる手法が存在します。そのような投稿を除外するため、同じ日・同じ地域の投稿群から90パーセンタイルで最終的な身投げレベルを確定しました。

学習データの補完

大雨や暴風の日は、誰も海岸に行かないのでデータが集まりません。が、このような日には明らかにホタルイカが取れるはずもないので、ホタルイカ湧きレベル0として学習データに追加します。

具体的には、雨量が 30 mm 超 または 平均風速が 8 m/s 以上 の悪条件日を対象としました。

学習データ数

ここまでの操作の結果、最終データの内訳は、正例(湧き) 785 件、負例(湧かない) 996 件となりました。地域別では Location 1 が 285 件、Location 2が 721 件、Location 3 が 438 件、Location 4 が 279 件、Location 5 が 58 件です。思ったより少なくなりました…

気象データの収集

次に、学習に用いる気象データを収集します。

気象データは、各 Location において一番近い気象観測所のデータを収集しました。

https://www.data.jma.go.jp/stats/etrn/index.php

Location No.代表地名使用気象データ
1雨晴、伏木、新湊アメダス伏木
2海老江、八重津、岩瀬、浜黒崎、水橋アメダス富山
3滑川、魚津、黒部、生地アメダス魚津
4入善、朝日、市振アメダス朝日
5糸魚川周辺アメダス糸魚川

これらのデータから、気温、風速、風向、気圧、降水量、日照時間が使えそうです。

また、追加で潮位、波浪、海水温データを収集しました。これは全ての Location で共通の値とします。

波浪データは NOWPHAS の「伏木富山港伏木」および「伏木富山港富山」を使用しました。 2024 年以降は確定値が揃っていないため、利用可能な速報値を採用しています。

https://nowphas.mlit.go.jp/pastdata_select

潮位データは気象庁の富山における潮汐観測資料を使用しました。

https://www.data.jma.go.jp/kaiyou/db/tide/genbo/format.html#hry

海水温データは気象庁の富山湾沿岸域海面水温データを使用しました。

https://www.data.jma.go.jp/kaiyou/data/db/kaikyo/series/engan/engan318.html

学習モデルと特徴量エンジニアリング

機械学習モデルは決定木の LightGBM を用いました。学習データが少なすぎるので、深層学習は不可能です。

特徴量は、以下を用いました。

暦・月

  • 年内の日付を周期変数に変換(sin/cos)
  • 月齢を 29.53 日周期で変換(sin/cos)
  • 富山湾の海水温

LightGBMは日付をそのまま扱えないし、ホタルイカの出現には季節性があるので、円周として周期性を表現します。

気象

  • 当日の降水量合計
  • 当日の気温の平均と標準偏差
  • 当日の風速の平均と標準偏差
  • 当日の気圧の平均と標準偏差
  • 当日の日照時間合計
  • 当日の港ごとの向かい風方向を使ったベクトルスコア
  • 当日20時から翌5時までの晴れ時間

「当日の港ごとの向かい風方向を使ったベクトルスコア」というのは、向かい風なら岸にホタルイカが押し寄せるはずという(根拠のない)推測に基づいた特徴量です。
具体的には、各 Location で「だいたいその方向から吹くと向かい風になる」とみなした角度に対して風速 × cos(風向 - 目標方向) で計算しています。上の地図を眺めながら、それっぽい角度を設定しました。現在の角度設定は、Location 1: 15 deg, Location 2: 0 deg, Location 3: 292.5 deg, Location 4: 337.5 deg, Location 5: 337.5 deg です。

「当日20時から翌5時までの晴れ時間」は、ホタルイカが湧く条件として有名な「月明かりがない日」の特徴を表現するために導入しました。

潮位・波浪

  • 当日24時間内の潮位差
  • 当日の夜間帯の相対潮位スコア
  • 当日の有義波高の平均と標準偏差
  • 当日の有義波周期の平均と標準偏差

「相対潮位スコア」は、ホタルイカの身投げが起きる夜間帯が、一日のうち干潮寄りなのか満潮寄りなのかを示す値です。

潮位スコア =
(夜間帯 20:00-翌4:00 の平均潮位 - その日の最低潮位)
/ (その日の最高潮位 - その日の最低潮位)

夜間帯が干潮寄りなら 0 に近く、満潮寄りなら 1 に近い値になります。

前日のラグ特徴量

気象、潮位、波浪データは前日のラグ特徴量も用います。前日の天候が大荒れだと少なからず影響ありそうなので。

location_group

後述する「統合モデル」においては、location_group 特徴量を用います。

いざ、学習

データは時系列順に 80% を学習、20% を検証に使い、 検証データで閾値と精度を決めます。欠損値は 0 埋めせず、LightGBM に NaN のまま渡しています。Early Stopping は 50 ラウンド。

結論から、特徴量を入れたり抜いたり、違う地域の特徴量を使ったり諸々やった結果、以下のような結論が得られました。
前提として、ホタルイカの湧き量の具体的な推定は全く精度が出なかったため、湧く/湧かないの二値分類としました。

  • Location 5 は、学習データの少なさから精度検証が不能
  • Location 1, 3, 4 は、全Locationデータで学習した「統合モデル」を用いると精度が良い
  • Location 2 は、Location 2のみのデータで学習した「専用モデル」を用いると精度が良い
  • Location 4 は、その地域に最も近い「アメダス朝日」よりも「アメダス富山」の気象データを用いたほうが精度が良い

つまり、こうです

Location No.モデル使用気象データ
1統合モデル伏木アメダス
2専用モデル富山アメダス
3統合モデル魚津アメダス
4統合モデル富山アメダス

その条件において、精度は以下のようになりました。

Location No.閾値湧く Precision湧かない PrecisionF1Accuracy
10.500.82350.88890.82350.8636
20.600.86540.75270.75000.7931
30.360.71430.70210.67570.7073
40.500.74290.76040.70270.7535

湧く Precision は、湧くと判定して実際に湧く確率です。

閾値は、検証データ上で「湧く Precision」と「湧かない Precision」を閾値をスイープしながら比較し、バランスの良い閾値の値を(人為的に)採用しました。

Location 3 はちょっと微妙ですが、他はそれなりの精度が得られています。

実運用時に用いる気象予報データの取得

実運用時に用いる予報データは、以下より取得しました。

項目取得元補足
気象Open-MeteoLocation 1 は伏木アメダス観測所座標、Location 2, 4 は富山アメダス座標、Location 3 は魚津アメダス座標の気象予報
潮位気象庁 富山 潮位予報予測潮位を使用
波浪気象庁 富山 波浪予報有義波高・有義波周期を使用
海水温気象庁 富山湾 海水温対象日以前の最新観測値を使用

気象データは、学習時と同じ観測点の座標における予報を使っています。

実運用上は、波浪予報の取得範囲が 3 日程度であるため、予報期間も 今日・明日・明後日の 3 日までに制限しています。(ここがネック!)

海水温の変化は緩やかであるため、最新の当日の海水温データを用いて 3 日間の予報を行っています。

これでホタルイカ湧き推定に必要な気象予報データは揃いましたが、予報データを用いた場合の精度は、学習(検証)時の精度よりも当然下がるはず。どの程度下がるかは定かではありませんが、今後は予測的中率を出して明らかにしていきたいと思います。

ホタルイカ出現確率への変換

「明日はホタルイカが湧きます/湧きません」のみでは味気がありません。湧く確率が見たいですよね。

しかし、LightGBM が出す生スコアは、そのまま 「40% なら 10 回に 4 回湧く」と読めるとは限りません。そこで、isotonic regression による確率校正を行いました。以下の記事が参考になります。なかなか難しいですが、ライブラリもあるしCodexがやってくれるので理解しなくても組めました。

https://qiita.com/dai08srhg/items/eb08fc98e7149748a9d5

これで、出現確率の推定値が得られました。

実運用

3時間毎にCrontabを回して最新の気象情報を取得し、都度推論します。N100ミニPCでも推論は一瞬でした。

決定木の説明可能性という最大の利点を生かし、モデル別に重視される特徴量や、推定結果の詳細としてプラス/マイナスに効いた特徴量も表示させます。

統合モデルでは、「当日降水量」、「月齢」、「当日日照時間」、「当日夜の晴れ時間」が重視されるようです。ただし、プラスに効いた要因の「当日夜の晴れ時間」は6時間となっていて、むしろ夜に晴れている方が湧きやすいという推定結果になっていました。通説とは逆ですね。

同様に、向かい風なら岸にホタルイカが押し寄せるはずという推測に基づいた「向かい風スコア」も、逆に点数が高いとマイナスに効いていました。

課題

  • 湧く/湧かないの二値分類で、湧き量の推定が不可能
  • 波浪予報の入手範囲がボトルネックで、今日・明日・明後日の 3 日予報のみな点
  • Location 5 (新潟の糸魚川) 学習データの少なさに起因する、推定精度の検証が不能の問題
  • Location 3 (滑川、魚津) の精度が低い問題

まとめ

AIホタルイカ身投げ予想は二番煎じですが、地域別の推定ができる点で有用なのではないでしょうか。 先行研究(?)の実用精度は不明ですが、同じあの掲示板を学習データとしているようなので大きく精度は変わらないはずです。

ちなみにまだ一度もホタルイカ掬いに行ったことがありません。

おすすめ

1件の返信

  1. GPT5.5-Thinking より:

    読みました。結論から言うと、**趣味記事としてはかなり良いですが、「予測精度がこの程度ある」と強く主張するには危ない点が多い**です。特に突っ込まれそうなのは、モデルよりも **教師データの作り方と検証方法** です。

    ## 一番大きい突っ込みどころ

    ### 1. 掲示板投稿ベースなので、「ホタルイカの発生」ではなく「人間の投稿行動」を学習している可能性がある

    掲示板の全投稿をスクレイピングし、LLMで「場所・量・日付」を抽出して教師データにしている構成ですが、これはかなり面白い一方で、データの性質としてはかなり歪みます。記事中でも、地名の伏字、投稿の曖昧さ、嘘投稿の存在を認めています。([たつろぐ | tatuiyoWebServer – 自宅鯖とかの技術系ブログ][1])

    特に問題なのは、

    * 採れた人ほど投稿しやすい
    * 採れなかった日は投稿されないことがある
    * 人気地点ほどデータが多い
    * 「湧いた」ではなく「誰かが行って採れた」を見ている
    * 嘘投稿や誇張投稿が混ざる

    という点です。

    そのため、厳密には「ホタルイカ身投げ発生モデル」ではなく、**掲示板上で報告される採集成功の予測モデル** に近いです。記事内でも Location 2 が721件、Location 5 が58件と地域差が大きいので、地域別比較もそのまま自然現象の差とは読めません。([たつろぐ | tatuiyoWebServer – 自宅鯖とかの技術系ブログ][1])

    ### 2. 嘘投稿対策に90パーセンタイルを使うのは、むしろ逆効果になり得る

    「同じ日・同じ地域の投稿群から90パーセンタイルで最終的な身投げレベルを確定」とありますが、これはかなり突っ込みどころです。([たつろぐ | tatuiyoWebServer – 自宅鯖とかの技術系ブログ][1])

    90パーセンタイルは、低めの外れ値を無視して「その日の良かった側」を拾うには向いています。しかし、記事中で問題にしているのが **嘘の大量報告・誇張投稿** なら、90パーセンタイルはむしろその影響を拾いやすいです。

    たとえば同じ日に、

    “`text
    0, 0, 1, 1, 6
    “`

    みたいな投稿があったとき、嘘の「爆湧き」投稿が上側にあると、90パーセンタイルはそれに引っ張られます。

    ここは、より安全にするなら、

    * 投稿数が少ない日は信頼度を下げる
    * median / trimmed mean / winsorize を比較する
    * 「採れた」「採れない」の二値は別集計する
    * 量レベルではなく「1件以上の肯定報告あり」に落とす
    * 同一IDや文体、連投、極端値を疑う

    あたりを書いておくと、かなり説得力が増します。

    ### 3. 悪天候日を「湧きレベル0」として追加するのは、便利だが少し循環している

    記事では「雨量30mm超または平均風速8m/s以上の日を湧きレベル0として追加」としています。([たつろぐ | tatuiyoWebServer – 自宅鯖とかの技術系ブログ][1])
    これは実務的にはかなり合理的ですが、機械学習の検証としては注意が必要です。

    なぜなら、**ラベルを気象条件から人工的に作って、その同じ気象条件で学習している** からです。モデルが「雨量30mm超なら0」「風速8m/s以上なら0」というルールを覚えれば、検証精度が上がります。

    実運用上は役に立つルールですが、精度評価では、

    * 人工追加データを除いた評価
    * 人工追加データだけの評価
    * 実投稿由来データだけの評価
    * 人工データに低い重みを付ける

    を分けた方がよいです。

    ここは記事中で自覚的に「これは実質ルールベース補完」と書いておくと、かなり突っ込まれにくくなります。

    ## 検証精度まわりの突っ込みどころ

    ### 4. 検証データを使いすぎていて、報告精度が楽観的になっている可能性が高い

    記事では、時系列順に80%を学習、20%を検証に使い、さらにその検証データで閾値と精度を決めています。加えて、特徴量を入れたり抜いたり、地域ごとのモデル選択や使用アメダス地点の選択もしています。([たつろぐ | tatuiyoWebServer – 自宅鯖とかの技術系ブログ][1])

    これは典型的に、**検証データに合わせ込んでいる** 状態です。

    たとえば、

    * Location 2 は専用モデルがよい
    * Location 4 は朝日アメダスより富山アメダスがよい
    * 閾値は検証データでスイープして決める
    * 特徴量の採否も試行錯誤で決める

    という判断を全部同じ検証データでやってから、その同じ検証データのAccuracyやPrecisionを出すと、数値は高めに出ます。

    公開記事としては、表の前に一文、

    > なお、以下の精度は特徴量・モデル・閾値選択にも使った検証データ上の値であり、完全な未知データに対する推定精度より楽観的である可能性があります。

    と入れるだけで、かなり誠実になります。

    本当に検証するなら、最後の1年または最後の1シーズンを完全に封印して、そこで一発評価するのがよいです。

    ### 5. 予報運用時の精度は、検証精度より確実に落ちる

    学習・検証では過去の実測気象データ、実運用ではOpen-Meteoの予報データや気象庁の波浪予報などを使っています。記事でも「予報データを用いた場合の精度は当然下がるはず」と書いており、ここは正しい認識です。([たつろぐ | tatuiyoWebServer – 自宅鯖とかの技術系ブログ][1])

    ただ、ここはもう少し強く書いてもよいです。

    実測値で作った特徴量と、予報値で作った特徴量は別物です。Open-Meteoは複数の数値予報モデルを組み合わせた時系列を返す仕組みで、日本ではJMA MSM/GSMなども選択肢に含まれますが、観測所の実測値そのものではありません。([Open Meteo][2])

    なので、過去検証で80%近いAccuracyが出ても、実運用では、

    * 降水量予報のズレ
    * 風向・風速予報のズレ
    * 波浪予報の粗さ
    * 雲量・晴れ時間のズレ
    * 観測所と海岸現地の差

    で普通に落ちます。

    ここは「バックテストには過去予報を使うべき」と書くと強いです。Open-Meteoにも過去の予報ランを検証・ML学習に使うためのHistorical Forecast APIやSingle Runs APIが案内されています。([Open Meteo][2])

    ## 特徴量まわり

    ### 6. 「晴れ時間」が通説と逆に効いたのは、モデルが因果ではなく相関を拾っている可能性が高い

    記事では、夜に晴れている方が湧きやすい方向に効いていて、通説と逆だと書かれています。([たつろぐ | tatuiyoWebServer – 自宅鯖とかの技術系ブログ][1])

    これはかなり面白いですが、解釈には注意が必要です。単純な「晴れ時間」は、月明かりそのものではありません。

    本来見たいのは、

    * 月齢
    * 月の高度
    * 月の出・月の入り
    * 夜間の雲量
    * 月が出ている時間帯だけの雲量
    * 人間が出撃しやすい天気

    あたりです。

    たとえば「新月付近で晴れている夜」は、月明かりが少なく、かつ人間も出撃しやすいので、掲示板上の成功報告は増える可能性があります。この場合、「晴れ」がホタルイカに効いているというより、**新月期の好天と観測・投稿のしやすさ** を拾っているかもしれません。

    ### 7. 向かい風スコアは発想は良いが、海岸線形状と風向の定義が粗い

    向かい風なら岸に寄るはず、という仮説で `風速 × cos(風向 – 目標方向)` を入れているのは良い特徴量設計です。ただし、目標方向を地図を見て手で設定しているので、かなり恣意的です。([たつろぐ | tatuiyoWebServer – 自宅鯖とかの技術系ブログ][1])

    また、気象庁などの風向は通常「風が吹いてくる方向」なので、式の符号を間違えると、向かい風と追い風が逆になります。記事の式だけでは、そこを読者が確認できません。

    ここは、

    > 風向は「風が吹いてくる方角」として扱い、各海岸の沖側方位との差を取った

    のように明記した方がよいです。

    ## 確率表示まわり

    ### 8. isotonic regression は良い選択だが、データが少ないと過学習しやすい

    LightGBMの生スコアをそのまま確率として読めないので、isotonic regressionで確率校正した、という流れ自体は妥当です。([たつろぐ | tatuiyoWebServer – 自宅鯖とかの技術系ブログ][1])

    ただし、scikit-learnの説明でも、isotonic regression は小さいデータでは過学習しやすく、十分なデータがある場合に向くとされています。([scikit-learn][3])

    今回、地域別に見ると Location 5 は58件しかなく、Location 1や4も数百件規模です。さらに時系列検証の後半20%だけで校正しているなら、校正用データはもっと少ないはずです。したがって、表示される「40%」「70%」は、かなり粗い目安として扱うべきです。

    記事には、

    > 表示確率は厳密な発生確率ではなく、過去データ上で校正した相対的な期待度です。

    と書いておくと安全です。

    ## 細かい表現・誤字・読者向けの直し

    * 「LightGBMは決定木」とありますが、より正確には **決定木ベースの勾配ブースティング** です。単体の決定木ではありません。LightGBM自体もtree based learning algorithmsの勾配ブースティング系フレームワークです。([LightGBM][4])
    * 「当日夜の腫れ時間」は誤字で、「晴れ時間」だと思います。([たつろぐ | tatuiyoWebServer – 自宅鯖とかの技術系ブログ][1])
    * 「深層学習は不可能」は少し強いです。「このデータ量では深層学習を使う利点が薄い」くらいが妥当です。
    * 「理解しなくても組めました」は記事としては面白いですが、確率校正の信頼性を語る節ではやや弱く見えます。ネタとして残すなら、その直後に「ただし校正用データが少ないため過信は禁物」と補うとよいです。
    * 「先行研究の実用精度は不明だが、同じ掲示板を学習データとしているようなので大きく精度は変わらないはず」は少し雑です。モデル、特徴量、ラベル化、検証方法でかなり変わるので、「同じデータ源なら似た課題を抱えているはず」くらいが安全です。([たつろぐ | tatuiyoWebServer – 自宅鯖とかの技術系ブログ][1])

    ## 追記するとかなり強くなる内容

    記事末尾か課題に、以下を入れると技術記事としてかなり締まります。

    “`text
    本モデルの限界:
    このモデルは自然現象としてのホタルイカ接岸そのものではなく、掲示板に投稿された採集報告を教師データとしている。そのため、投稿者数・地点人気・出撃しやすい天候・虚偽投稿・報告バイアスの影響を受ける。また、検証には実測気象値を用いているため、実際の予報値を用いた運用精度は表中の検証精度より低下する可能性がある。
    “`

    さらに強くするなら、

    “`text
    今後は、2026年シーズンの予測を事前に保存し、後から掲示板投稿と照合することで、完全な未来データに対する的中率を評価する。
    “`

    と書くとよいです。

    総評としては、**発想・実装・記事の面白さはかなり良い**です。ただし、技術的に厳しく見ると、現状の弱点は「LightGBMの性能」ではなく、**掲示板由来ラベルのバイアス、人工負例、検証データの使い回し、予報運用時の分布ズレ** です。ここを自覚的に書けば、かなり突っ込まれにくい記事になります。

    [1]: https://tatuiyo.xyz/?p=1722 “富山県ホタルイカ身投げAI予測モデルを作った (地域別推定) – たつろぐ | tatuiyoWebServer”
    [2]: https://open-meteo.com/en/docs “️ Docs | Open-Meteo.com”
    [3]: https://scikit-learn.org/stable/modules/calibration.html “1.16. Probability calibration — scikit-learn 1.8.0 documentation”
    [4]: https://lightgbm.readthedocs.io/?utm_source=chatgpt.com “Welcome to LightGBM’s documentation! — LightGBM 4.6.0 …”

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

Index