109
55

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ホタルイカの身投げ量を機械学習で予測するWebサイトを作ってみた(Next.js + Go)

109
Last updated at Posted at 2025-10-20

目次

1. はじめに
2. アプリ概要
3. 使用技術
4. アプリケーション構成図
5. 開発の流れ
6. 機械学習モデルの訓練
7. 予測 API の開発
8. Web サイトの構築
9. 課題点や懸念点
10. 終わりに

はじめに

ホタルイカの身投げについて

初めまして!ホタルイカの身投げについてご存知でしょうか?

身投げとは、ホタルイカが産卵のために富山県の海岸近くまで押し寄せる現象を指します。この現象は、特定の自然条件が揃ったときに発生し、身投げ量が多い時には以下のように海岸が青白い光で埋め尽くされる幻想的な光景を見ることができます。

毎年春になると、富山県の海岸に県内、県外関わらず多くの人が押し寄せホタルイカ掬いを楽しみます。

image.png

身投げが起きやすい条件

身投げは主に、以下の条件が重なった時に起こりやすいと言われています。

  • 月齢: 新月の前後、月明かりが少ない夜。
  • 潮汐: 満潮に近い時間帯。
  • 天候: 晴れて波が穏やかな日。
  • 風向: 南風が吹き、沖合のホタルイカが岸に寄せられるとき。

しかし、これらの条件が揃っても、1 匹も湧かないこともあります。

この身投げ量を予測する Web サイトを作ってみました。

アプリ概要

アプリ URL

ホタルイカのオフシーズン中は予報や詳細情報を表示されません。シーズン中の表示は、デモデータを用いた以下のプレビューページで確認できます。

機能

  • 身投げ量予報: 先 7 日間の予測身投げ量を指数で確認できる。
  • 詳細情報: 日付ごとの時間帯別の天気、波の高さ、潮位、月齢などの詳細なデータを確認できる。
  • 掲示板機能: 現地の最新情報やホタルイカに関する話題を共有・交換できる。

作成動機

  • 富山出身でホタルイカ掬いにたまに行く。
  • 現地に行ってもホタルイカが湧かないことも多く、予測できたら便利だと思った。
  • ホタルイカ掬いは最新のリアルタイムな情報が重要になる中で現状あまり情報を共有する場が少なく、掲示板機能を持ったサイトを自分で作りたくなった。

使用技術

アプリ全体で使用している技術です。

サーバーサイド

  • Go(サイト)
  • FastAPI(予測 API)

フロントエンド

  • TypeScript (Next.js)
  • Tailwind CSS
  • shadcn/ui

データベース・ストレージ

  • Supabase

デプロイ先

  • Vercel
  • GCP(Cloud Run)

機械学習

  • LightGBM

その他

  • Git
  • Github
  • GitHub Actions
  • Docker
  • Docker Compose
  • Google Cloud Scheduler

アプリ構成図

以下はこのアプリの流れを示した図になります。

image.png

開発の流れ

1. データセットの構築
過去のホタルイカの身投げ量(実績値)と、それに対応する日の 1 時間ごとの気象データ、潮汐データ、月齢などのデータを収集する。

2. 機械学習モデルの訓練
構築したデータセットを利用して、LightGBM でホタルイカの身投げ量を予測するためのモデルを学習させる。

3. 予測 API の開発
学習済みのモデルを組み込み、未来の天気予報や月齢などを入力として受け取り、身投げ量を予測し、予測した身投げ量を JSON 形式で返す API を作成する。

4. Web サイトの構築
作成した予測 API を用いて Web サイトを構築する。

機械学習モデルの訓練

私は、機械学習などの分野は全くの未経験だったので AI に聞きながら行いました。これが正しい方法だったかは分かりません。

予測モデル構築のステップ

予測モデルの構築は、大きく以下のステップで進めました。

1. データ読み込みと前処理: JSON 形式のデータを読み込み、扱いやすい形に整形
2. 特徴量エンジニアリング: 予測に役立ちそうな新しい特徴量を作成
3. モデル学習: LightGBM を使ってモデルを学習
4. 評価と可視化: モデルの精度を評価し、結果を可視化して考察

1. データ読み込みと前処理

まず、様々なデータソースから収集・統合した JSON ファイルを読み込みます。

このファイルには、日ごとのホタルイカの平均身投げ量と、その日に対応する 1 時間ごとの気象データ・潮位データ・月齢データが含まれています。

対象期間は 2〜5 月の 10 年間で、計 1220 日分のデータがあります。

2. 特徴量エンジニアリング

今回は「周期性」と「過去の情報」の 2 つの観点を工夫し特徴量を作成しました。

sin/cos 変換で周期性を表現

月齢(約 29.5 日周期)や日付(365 日周期)といった周期的なデータは、そのまま数値として扱うと周期の終わりと始まり(例: 12 月 31 日と 1 月 1 日)の関係性をうまく表現できません。

そこでsin/cos 変換を行い、周期的なデータを円周上の点として表現し、連続値として学習できるようにしました。

ラグ特徴量で過去の情報を利用

ラグ特徴量も作成しました。これは、過去のデータを当日の特徴量として利用する手法です。今回は 1 日前と 2 日前の気象データなどを特徴量に加えました。

3. モデル学習

今回は高精度で計算も高速な LightGBM を使用しました。

時系列データの交差検証とハイパーパラメータチューニング

時系列データを扱う上で重要なのは、未来のデータを使って過去を予測しないことです。
これを防ぐため、交差検証に TimeSeriesSplit を使用し、常に過去のデータで学習し、未来のデータで評価を行うようにしています。

from sklearn.model_selection import TimeSeriesSplit, GridSearchCV
import lightgbm as lgb

# 特徴量Xと目的変数yを準備
features = [col for col in df.columns if col not in ['date', 'avg_amount']]
X = df[features]
y = df['avg_amount']

# 時系列データ用の交差検証を設定
tscv = TimeSeriesSplit(n_splits=5)

# LightGBMモデルとチューニングしたいパラメータ範囲を定義
lgb_model = lgb.LGBMRegressor(random_state=42)
param_grid = {
    'n_estimators': [300, 500],
    'learning_rate': [0.01, 0.05],
    'num_leaves': [20, 31, 40],
}

# グリッドサーチで最適なパラメータを探索
gs = GridSearchCV(lgb_model, param_grid, cv=tscv, scoring='r2')
gs.fit(X, y)

# 最も性能の良かったモデルを取得
best_model = gs.best_estimator_

4. 評価と可視化

モデルの精度を評価します。

ここでは、学習には使っていない 全データの最後の 20% を「テストデータ」として使用し、予測値と実際の値を比較しました。

評価指標は以下の通りです。今回は、0 から 1 の範囲に正規化された値で評価指標を計算します。

R² (決定係数): 1 に近いほど良いモデル。
MAE (平均絶対誤差): 予測値と実測値の誤差の平均。小さいほど良い。
RMSE (二乗平均平方根誤差): MAE と同様に誤差の指標。大きな誤差をより重視する。

算出された評価指数

このようになりました。

決定係数 (R²): 0.7008
平均絶対誤差 (MAE): 0.0721
二乗平均平方根誤差 (RMSE): 0.1013

特徴量の重要度

特徴量の重要度の上位には、temperature_stdprecipitation_sumday_of_year_sinmoon_age_coswind_speed_std_lag1temperature_std_lag1 などがあり、季節、気温、月齢、降水量などが特に身投げ量に影響することがわかりました。

予測 API の開発

作成した API の概要

前回で学習させたモデルを使用して予測値を返す API を開発しました。フレームワークは FastAPI、GCP(Cloud Run)上にデプロイしています。

API の構造

API は以下の流れで動作します。リクエストを受けると、外部の API から最新の予報を取得し、学習済みの機械学習モデルを使って予測値を算出し、予測値を返します。

image.png

使用した外部 API

非営利なら無料で使用できるこれらの API を使用しました。

時間の基準

ホタルイカの身投げは主に 22:00〜翌 4:00 ごろ に発生します。
そのため、このサイトでは 1 日の切り替え時刻を 5:00 に設定しています。

たとえば「4/10 の予報」は、実際には「4/10 の 22:00 〜 4/11 の 4:00 ごろ」までの身投げ量を指しています。この設定は機械学習モデルの学習段階から一貫して適用しています。

Web サイトの開発のセクションで後述しますが、この API には、2:00, 5:00, 8:00, 11:00, 14:00, 17:00, 20:00, 23:00にリクエストが送信されます。

そこで問題なのが2:00 のアクセスです。例えば「4/11 の 2:00」にリクエストが送られた場合、サイト上では「4/10」が当日扱いになるはずですが、実際には「4/11 から一週間分のデータ」が返ってきてしまいます。

この問題を解決するために、API 側のタイムゾーンを日本時間より 4 時間遅め に設定しています。これにより「2:00 のアクセス」は内部的に「前日の 22:00」として扱われ、期待どおり「4/10 から一週間分のデータ」を取得できるようになります。

# Cloud Run にデプロイする際の設定例
gcloud run deploy [SERVICE_NAME] \
  --image [IMAGE_URL] \
  --set-env-vars "TZ=Etc/GMT-13"

大変だったこと

「学習時と予測時でデータの前処理を完全に一致させること」がとても大変でした。

学習時に実施した欠損値補完・スケーリング・特徴量生成などの前処理を、外部 API から取得したデータに対しても API 側でも厳密に再現する必要がありました。

API で予測時に使用した特徴量の値をログで出して、実際の値との違いを比べるといった作業を何回も行いました。

Web サイトの構築

作成した予測 API を用いて、Web サイトを作成しました。
アプリケーション全体構成の中の、この赤枠の部分です。

image.png

アプリケーション構成

  • フロントエンド: Next.js (TypeScript), shadcn/ui, Tailwind CSS
  • バックエンド: Go
  • データベース: PostgreSQL

バックエンド (Go)

主な API エンドポイント

  • 予報関連
    • GET /api/prediction: キャッシュにある予測身投げ量を返す。
    • GET /api/detail/{date}: キャッシュにある詳細な気象・潮汐データを返す。
    • POST /api/tasks/refresh-cache:外部データにアクセスし、新しいデータで、予測データと詳細データのキャッシュを強制的に更新する。
  • 口コミ (Posts) 関連
    • GET POST /api/posts: 口コミの一覧取得、新規作成。
    • GET POST DELETE /api/posts/{id}/...: 特定の口コミに対する操作(削除、返信、リアクション)。
  • 管理者関連
    • POST /api/admin/login: 管理者としてログインし、JWT を発行。
    • POST /api/admin/logout: ログアウト。

キャッシュ

外部 API(気象情報、潮汐情報、先ほど解説した予測 API)へユーザーが直接ブラウザからアクセスすると、コストと時間がかかります。

そこで、アプリの構成図のように、頻繁にアクセスされるデータをメモリ上にキャッシュするように実装しました。

backend/internal/cache/cache.go
// 予測データと詳細データのキャッシュを管理
type CacheManager struct {
	logger        *slog.Logger
	predictionURL string
	predictionCache struct {
		sync.RWMutex
		data []byte
	}
	detailCache struct {
		sync.RWMutex
		data map[string][]byte
	}
}

// 予測データを取得しキャッシュする
func (c *CacheManager) FetchAndCachePredictionData() {
    // ... 外部APIからデータを取得 ...
	c.predictionCache.Lock()
	c.predictionCache.data = body
	c.predictionCache.Unlock()
}

// キャッシュされた予測データを返す
func (c *CacheManager) GetPredictionData() []byte {
	c.predictionCache.RLock()
	defer c.predictionCache.RUnlock()
	return c.predictionCache.data
}
  • /api/tasks/refresh-cacheに POST リクエストがあるとFetchAndCache... 関数群が実行されるようになっていて、キャッシュを最新の状態に更新します。本番環境では Google Cloud Scheduler を使用し2:00, 5:00, 8:00, 11:00, 14:00, 17:00, 20:00, 23:00に POST リクエストを送る設定にしています。
  • /api/prediction および /api/detail/{date} エンドポイントは、このキャッシュに保存されたデータを返します。ブラウザからはこのエンドポイントにアクセスするようにし、外部 API へのアクセスを最小限に抑え、高速なレスポンスを実現しました。

データベース (PostgreSQL & Supabase)

テーブル設計

image.png

フロントエンド (Next.js)

湧きレベル判定

このサイトでは、予測身投げ量を「湧きなし」「プチ湧き」「チョイ湧き」「湧き」「大湧き」「爆湧き」の 6 段階のレベルで予報します。

API から予測身投げ量の数値が返ってくるのですが、その値は 0~4 の値をとります。現時点ではその API から返ってくる値をxとした時、

範囲 (x) 評価
x < 0.25 湧きなし
0.25 ≦ x < 0.5 プチ湧き
0.5 ≦ x < 0.75 チョイ湧き
0.75 ≦ x < 1 湧き
1 ≦ x < 1.25 大湧き
1.25 ≦ x 爆湧き

としています。

機械学習セクションでの予測結果と実測値の比較グラフを見てこの基準にしました。

ログイン機能無しでのリアクション管理

本アプリケーションでは、ユーザーの利用開始時の負担を最小限に抑えるため、ログイン機能を実装していません。

「good」「bad」のリアクション機能については、ユーザーが行ったリアクションを記憶するために、ブラウザの localStorage を利用しました。

UI

今回のアプリでは UI も自分なりにこだわってみました。
全体的にモダンな雰囲気を目指し、ホタルイカの神秘的な世界観に合うよう意識しました。

また、深夜の時間帯に利用されることが多いと思うので、暗い環境でも目が疲れにくいように暗めのデザインにしました。また、スマートフォンからのアクセスがメインと思い、レスポンシブ対応にも気をつけて開発を進めました。

デザイン面が苦手なので、最初に bolt.new に自分が想像しているデザインを指示し、Next.js のプロジェクトコードを生成してもらい、それをダウンロードして開発を開始しました。

開発環境と本番環境の構成

ローカル開発環境 (Docker Compose)

開発環境では、docker-compose.yml を用いて frontend, backend, db の 3 つのサービスを定義し、連携させています。

本番環境 (Cloud Run, Vercel, Supabase)

本番環境では、以下のクラウドサービスを組み合わせて利用しています。

  • フロントエンド (Next.js): Vercel
  • バックエンド (Go): Google Cloud Run
  • データベース (PostgreSQL) および ストレージ: Supabase

課題点や懸念点

海岸ごとの予測ができていない

現在のモデルでは、富山湾全体としてのホタルイカの湧き量(身投げ量)を予測しています。そのため、特定の海岸ごとの違いまでは反映できていません。実際には、同じ日でも場所によって湧き量に差が出ることがあり、予測精度の低下やサイトの信頼性の低下つながることが心配です。

身投げ量の予測精度が外部 API の精度に影響される

このサイトでは、自作の機械学習モデルに外部 API から取得したデータを入力し、身投げ量を予測しています。なので、そもそも外部 API の予報精度が低い場合には、予測精度が落ちてしまいます。今後は、より精度の高い気象予報 API の使用を検討する必要があると感じています。

サイトを訪れるユーザーの母体が少ない

サイトの主な利用者は最低でもホタルイカ掬いに関心のある人に限られます。そのためユーザー母数が少なく、また利用が集中するのは 2〜5 月のシーズン期間のみです。オフシーズンにはアクセスが減少しやすく、ユーザーが離れてしまいます。季節外でも使ってもらえる工夫が必要だと感じています。

終わりに

なんとかアプリを形にすることができてよかったです。
2026 年のホタルイカシーズンの時に公開したいと思います。
大学生で時間があるうちに、もっと個人開発をしたいと思います!

109
55
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
109
55

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?