海野さん卒論
をテンプレートにして作成
[
トップ
] [
新規
|
一覧
|
検索
|
最終更新
|
ヘルプ
]
開始行:
*引き継ぎ [#c96934e6]
[[引き継ぎ用ページ]]
*目次 [#title]
#CONTENTS
* アプリ概要 [#overview]
ホタルイカのオフシーズン中(2〜5月以外)は予報や詳細情報...
:アプリURL|
https://bakuwaki.jp/
:プレビューページ(シーズン中デモ)|
https://bakuwaki.jp/preview
:Webサイト構築コード(GitHub)|
https://github.com/yuchi1128/hotaruika-bakuwaki-forecast
**機能 [#features]
:身投げ量予報|先7日間の予測身投げ量を指数で確認できる。
:詳細情報|日付ごとの時間帯別の天気、波の高さ、潮位、月齢...
:掲示板機能|現地の最新情報やホタルイカに関する話題を共有...
**作成動機 [#motivation]
-富山出身でホタルイカ掬いにたまに行く。
-現地に行ってもホタルイカが湧かないことも多く、予測できた...
-既存で一番使われているホタルイカ掬い用の掲示板サイトが使...
*使用技術 [#tech_stack]
アプリ全体で使用している技術です。
***サーバーサイド [#server]
-Go(サイト)
-FastAPI(予測API)
***フロントエンド [#frontend]
-TypeScript (Next.js)
-Tailwind CSS
-shadcn/ui
***データベース・ストレージ [#db]
-Supabase
***デプロイ先 [#deploy]
-Vercel
-GCP(Cloud Run)
***機械学習 [#ml]
-LightGBM
***その他 [#others]
-Git
-GitHub
-GitHub Actions
-Docker
-Docker-Compose
-Google Cloud Scheduler
*アプリ構成図 [#architecture]
以下はこのアプリの流れを示した図になります。
#ref(アプリ構成図.png,,40%)
*開発の流れ [#dev_flow]
+''データセットの構築''
過去のホタルイカの身投げ量(実績値)と、それに対応する日...
+''機械学習モデルの訓練''
構築したデータセットを利用して、LightGBMでホタルイカの身...
+''予測APIの開発''
学習済みのモデルを組み込み、未来の天気予報や月齢などを入...
+''Webサイトの構築''
作成した予測APIを用いてWebサイトを構築。
ここからはこれらを順に解説していきます。
*データセットの構築 [#data_collection]
#ref(data_set.png,,25%)
***1. ホタルイカ身投げデータの取得 (スクレイピング) [#scr...
ホタルイカの身投げ量は公的なデータが存在しないため、掲示...
-取得元: [[ホタルイカ掲示板:https://rara.jp/hotaruika-toy...
-対象期間: 2015年〜2025年の10年間(各年2月〜5月)、約1300...
-処理内容:
++口コミ内容をスクレイピングで取得。
++内容から身投げ量を5段階評価し、0〜4の整数で数値化。
++日ごとの平均値を算出し、その日の「身投げ量」として日付...
***2. 過去の気象データの取得 [#weather_data]
[[気象庁公式サイト:https://www.data.jma.go.jp/risk/obsdl/...
※身投げデータが存在する日付に対応するデータを取得。
***3. 潮位データの取得 [#tide_data]
外部API([[tide736.net:https://tide736.net/#google_vignet...
***4. 月齢データの取得 [#moon_data]
西暦と日付から計算式を用いて算出しました。
***5. データの統合 [#data_integration]
上記で作成した複数のJSONファイルを、日付をキー(主キー)...
*機械学習モデルの訓練 [#ml_training]
私は、機械学習などの分野は全くの未経験だったのでAIに聞き...
***モデル構築のステップ [#ml_steps]
+データ読み込みと前処理: JSON形式のデータを読み込み、扱い...
+特徴量エンジニアリング: 予測に役立ちそうな新しい特徴量を...
+モデル学習: LightGBMを使ってモデルを学習
+評価と可視化: モデルの精度を評価し、結果を可視化して考察
以下はモデル構築の流れを示した図になります。
#ref(model_learn.png,,70%)
**1. データ読み込みと前処理 [#data_process]
まず、様々なデータソースから収集・統合した JSON ファイル...
このファイルには、日ごとのホタルイカの平均身投げ量と、そ...
対象期間は2〜5月の10年間で、計1220日分のデータがあります。
**2. 特徴量エンジニアリング [#feature_eng]
***使用した特徴量 (説明変数) [#features]
予測に使用した主な特徴量は以下の通りです。
:時間特徴量|年、月、日、曜日、年の第何週
:月齢 (sin/cos変換)|月齢(0〜29.53日周期)を円周上の点と...
--月齢 sin: 上弦の月で最大(1)、下弦の月で最小(-1)
--月齢 cos: 新月で最大(1)、満月で最小(-1)
:気温|平均、最大、最小、標準偏差。
--時間帯別(10-13時、14-17時、18-21時、22-0時、1-4時)の...
:降水量|合計降水量、降水の有無(バイナリ変数)。
--時間帯別の合計も算出。
:風速・風向|平均、最大、最小、標準偏差。
:潮汐データ|
--潮型: 若潮、長潮、小潮、中潮、大潮を数値化。
--夜間潮位: 夜間(21時〜5時)の満潮・干潮の高さ(平均、最...
:ラグ特徴量|過去の情報を特徴量として追加。
--気象・潮汐変数: 1日前、2日前の値。
--目的変数(avg_amount): 1日前、2日前、直近3日間の平均値。
***工夫した点 [#r5c8c58d]
「周期性」と「過去の情報」の2つの観点を工夫し特徴量を作成...
:sin/cos変換で周期性を表現|
月齢(約29.5日周期)や日付(365日周期)といった周期的なデ...
そこでsin/cos変換を行い、周期的なデータを円周上の点として...
# 月齢をsin/cosに変換
df['moon_age_sin'] = np.sin(2 * np.pi * df['moon_age'] /...
df['moon_age_cos'] = np.cos(2 * np.pi * df['moon_age'] /...
# 1年のうちの日付をsin/cosに変換
df['day_of_year_sin'] = np.sin(2 * np.pi * df['day_of_ye...
df['day_of_year_cos'] = np.cos(2 * np.pi * df['day_of_ye...
:ラグ特徴量で過去の情報を利用|
ラグ特徴量も作成しました。これは、過去のデータを当日の特...
# ラグ特徴量を作成したいカラムのリスト
cols_for_lag = [
'moon_age_sin', 'moon_age_cos', 'temperature_mean',
'precipitation_sum', 'wind_speed_mean', 'wind_direct...
]
# forループで1日前と2日前のデータ(ラグ)を新しい列とし...
for col in cols_for_lag:
df[f'{col}_lag1'] = df[col].shift(1) # 1日前の値
df[f'{col}_lag2'] = df[col].shift(2) # 2日前の値
**3. モデル学習 [#model_training]
今回は高精度で計算も高速なLightGBMを使用しました。
***時系列データの交差検証とハイパーパラメータチューニング...
時系列データを扱う上で最も重要なのは、未来のデータを使っ...
これを防ぐため、交差検証にTimeSeriesSplitを使用し、常に過...
from sklearn.model_selection import TimeSeriesSplit, Gri...
import lightgbm as lgb
# 特徴量Xと目的変数yを準備
features = [col for col in df.columns if col not in ['da...
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, scorin...
gs.fit(X, y)
# 最も性能の良かったモデルを取得
best_model = gs.best_estimator_
**4. 評価と可視化 [#evaluation]
モデルの精度を評価します。
ここでは、学習には使っていない 全データの最後の20% を「テ...
評価指標は以下の通りです。今回は、0から1の範囲に正規化さ...
-R² (決定係数): 1に近いほど良いモデル。
-MAE (平均絶対誤差): 予測値と実測値の誤差の平均。小さいほ...
-RMSE (二乗平均平方根誤差): MAEと同様に誤差の指標。大きな...
***算出された評価指数 [#score]
このようになりました。
-決定係数 (R²): 0.7008
-平均絶対誤差 (MAE): 0.0721
-二乗平均平方根誤差 (RMSE): 0.1013
***予測結果と実測値の比較 [#comparison]
学習済みモデルを使ってテストデータの身投げ量を予測し、実...
#ref(予測精度グラフ.png,,30%)
青色が実際の過去の身投げ量(正解値)で、赤色が予測された...
グラフを見ると、身投げ量の増減はある程度捉えられています...
***特徴量の重要度 [#importance]
特徴量の重要度の上位には、temperature_std、precipitation_...
*予測APIの開発 [#api_dev]
**作成したAPIの概要 [#api_overview]
前回で学習させたモデルを使用して予測値を返すAPIを開発しま...
**APIの構造 [#api_structure]
APIは以下の流れで動作します。リクエストを受けると、外部の...
#ref(API概要.png,,20%)
**使用した外部API [#external_api]
非営利なら無料で使用できるこれらのAPIを使用しました。
**APIエンドポイント [#endpoint]
1週間分(当日を含む7日間)のホタルイの予測身投げ量データ...
-エンドポイント: /predict/week
-HTTPメソッド: GET
-認証: 不要
***レスポンスボディの例 [#response_example]
(7日分のデータが配列で返されますが、ここでは1日分の例を...
[
{
"date": "2025-4-10",
"predicted_amount": 1.5,
"moon_age": 18.2,
"weather_code": 3,
"temperature_max": 18.5,
"temperature_min": 12.3,
"precipitation_probability_max": 20,
"dominant_wind_direction": 270
},
]
**時間の基準 [#time_standard]
ホタルイカの身投げは主に 22:00〜翌4:00ごろ に発生します。
そのため、このサイトでは 1日の切り替え時刻を 5:00 に設定...
たとえば「4/10 の予報」は、実際には「4/10の22:00 〜 4/11...
Webサイトの開発のセクションで後述しますが、このAPIには、2...
そこで問題なのが2:00のアクセスです。例えば「4/11の2:00」...
この問題を解決するために、API 側のタイムゾーンを日本時間...
# Cloud Run にデプロイする際の設定例
gcloud run deploy [SERVICE_NAME] \
--image [IMAGE_URL] \
--set-env-vars "TZ=Etc/GMT-13"
**大変だったこと [#api_challenges]
「学習時と予測時でデータの前処理を完全に一致させること」...
学習時に実施した欠損値補完・スケーリング・特徴量生成など...
APIで予測時に使用した特徴量の値をログで出して、実際の値と...
*Webサイトの構築 [#web_dev]
作成した予測APIを用いて、Webサイトを作成しました。
アプリケーション全体構成の中の、この赤枠の部分がこのセク...
#ref(アプリ構成図_web.png,,25%)
**アプリケーション構成 [#web_config]
-フロントエンド: Next.js (TypeScript), shadcn/ui, Tailwin...
-バックエンド: Go
-データベース: PostgreSQL
**バックエンド (Go) [#backend]
***主なAPIエンドポイント [#backend_endpoints]
-予報関連
--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: ログアウト。
***キャッシュ [#cache]
外部API(気象情報、潮汐情報、先ほど解説した予測API)へユ...
そこで、アプリの構成図のように、頻繁にアクセスされるデー...
backend/internal/cache/cache.go
// CacheManager は予測データと詳細データのキャッシュを管理
type CacheManager struct {
logger *slog.Logger
predictionURL string
predictionCache struct {
sync.RWMutex
data []byte
}
detailCache struct {
sync.RWMutex
data map[string][]byte
}
}
// FetchAndCachePredictionData は予測データを取得しキャ...
func (c *CacheManager) FetchAndCachePredictionData() {
// ... 外部APIからデータを取得 ...
c.predictionCache.Lock()
c.predictionCache.data = body
c.predictionCache.Unlock()
}
// GetPredictionData はキャッシュされた予測データを返す
func (c *CacheManager) GetPredictionData() []byte {
c.predictionCache.RLock()
defer c.predictionCache.RUnlock()
return c.predictionCache.data
}
/api/tasks/refresh-cacheにPOSTリクエストがあるとFetchAndC...
/api/prediction および /api/detail/{date} エンドポイント...
***管理者機能とJWT認証 [#admin_jwt]
口コミの削除など、特定の操作は管理者のみが行えるように制...
backend/internal/handler/admin.go
func (h *Handler) adminLoginHandler(w http.ResponseWrite...
// ... パスワード検証 ...
// JWT Claimsを設定
expirationTime := time.Now().Add(24 * time.Hour)
claims := &model.Claims{
Role: "admin",
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(expirationTime),
},
}
// トークンを生成し、署名
token := jwt.NewWithClaims(jwt.SigningMethodHS256, c...
tokenString, err := token.SignedString(h.jwtKey)
// ...
// Cookieにトークンをセット
http.SetCookie(w, &http.Cookie{
Name: "admin_token",
Value: tokenString,
Expires: expirationTime,
HttpOnly: true,
Path: "/",
SameSite: http.SameSiteNoneMode,
Secure: true,
})
}
ログインに成功すると、Role: "admin" という情報を含んだJWT...
**データベース (PostgreSQL & Supabase) [#database]
***テーブル設計 [#table_design]
このサイトではログイン機能を実装していないためUserテーブ...
#ref(DB構成図.png,,20%)
-posts: 口コミの投稿そのものを格納。
-replies: posts テーブルへの返信を格納。parent_reply_id ...
-reactions: 「good」「bad」といったリアクション情報を格納。
**フロントエンド (Next.js) [#frontend_js]
***ページ構成とデータ取得 [#page_structure]
-トップページ (/): クライアントコンポーネントとして実装。
-詳細ページ (/detail/[date]): サーバーコンポーネントとし...
***湧きレベル判定 [#waki_level]
このサイトでは、予測身投げ量を「湧きなし」「プチ湧き」「...
APIから予測身投げ量の数値が返ってくるのですが、その値は0~...
|~範囲 (x)|~評価|h
|x < 0.25|湧きなし|
|0.25 ≦ x < 0.5|プチ湧き|
|0.5 ≦ x < 0.75|チョイ湧き|
|0.75 ≦ x < 1|湧き|
|1 ≦ x < 1.25|大湧き|
|1.25 ≦ x|爆湧き|
としています。機械学習セクションでの予測結果と実測値の比...
***複雑な状態を管理するコンポーネント [#component]
CommentSection は、口コミの表示、投稿、フィルタリング、検...
frontend/components/Community/CommentSection.tsx
const CommentSection = ({ ... }: CommentSectionProps) => {
// 投稿内容、画像、ラベルなどの状態
const [newComment, setNewComment] = useState('');
const [selectedImages, setSelectedImages] = useState<F...
const [selectedLabel, setSelectedLabel] = useState<str...
// フィルタリング、検索、ソート、ページネーションの状態
const [selectedFilterLabel, setSelectedFilterLabel] = ...
const [searchQuery, setSearchQuery] = useState<string>...
const [sortOrder, setSortOrder] = useState<'newest' | ...
const [currentPage, setCurrentPage] = useState<number>...
// フィルタリングとソートを適用したメモ化済みのコメン...
const sortedComments = useMemo(() => {
// ... フィルタリングとソートのロジック ...
}, [comments, searchQuery, sortOrder]);
// ページネーションを適用した最終的な表示用コメントリ...
const paginatedComments = useMemo(() => sortedComments...
// ...
}
-状態管理: useState を多用して、ユーザーの入力や選択の状...
-パフォーマンス最適化: useMemo を活用し、フィルタリングや...
***ログイン機能無しでのリアクション管理 [#reaction]
本アプリケーションでは、ユーザーの利用開始時の負担を最小...
「good」「bad」のリアクション機能については、ユーザーが行...
***UI [#ui]
今回のアプリではUIも自分なりにこだわってみました。
全体的にモダンな雰囲気を目指し、ホタルイカの神秘的な世界...
また、深夜の時間帯に利用されることが多いと思うので、暗い...
デザイン面が苦手なので、最初に bolt.new に自分が想像して...
**開発環境と本番環境の構成 [#environments]
***ローカル開発環境 (Docker Compose) [#local_env]
開発環境では、docker-compose.yml を用いて frontend, backe...
docker-compose.yml
version: '3.8'
services:
frontend:
build:
context: ./frontend
ports:
- "3001:3000"
environment:
# コンテナ内からバックエンドサービスにアクセスする...
NEXT_PUBLIC_API_BASE_URL: http://backend:8080
depends_on:
- backend
backend:
build:
context: ./backend
ports:
- "8080:8080"
env_file:
- ./backend/.env # DB接続情報などをファイルから読...
depends_on:
db:
condition: service_healthy # DBが準備完了してか...
db:
image: postgres:14-alpine
healthcheck:
test: ["CMD-SHELL", "pg_isready -U user -d hotarui...
interval: 5s
timeout: 5s
retries: 5
# ...
***本番環境 (Cloud Run, Vercel, Supabase) [#prod_env]
本番環境では、以下のクラウドサービスを組み合わせて利用し...
-フロントエンド (Next.js): Vercel
-バックエンド (Go): Google Cloud Run
-データベース (PostgreSQL) および ストレージ: Supabase
*課題点や懸念点 [#issues]
**海岸ごとの予測ができていない [#issue_area]
現在のモデルでは、富山湾全体としてのホタルイカの湧き量(...
**身投げ量の予測精度が外部APIの精度に影響される [#issue_a...
このサイトでは、自作の機械学習モデルに外部APIから取得した...
**サイトを訪れるユーザーの母体が少ない [#issue_users]
サイトの主な利用者は最低でもホタルイカ掬いに関心のある人...
*終わりに [#conclusion]
なんとかアプリを形にすることができてよかったです。
終了行:
*引き継ぎ [#c96934e6]
[[引き継ぎ用ページ]]
*目次 [#title]
#CONTENTS
* アプリ概要 [#overview]
ホタルイカのオフシーズン中(2〜5月以外)は予報や詳細情報...
:アプリURL|
https://bakuwaki.jp/
:プレビューページ(シーズン中デモ)|
https://bakuwaki.jp/preview
:Webサイト構築コード(GitHub)|
https://github.com/yuchi1128/hotaruika-bakuwaki-forecast
**機能 [#features]
:身投げ量予報|先7日間の予測身投げ量を指数で確認できる。
:詳細情報|日付ごとの時間帯別の天気、波の高さ、潮位、月齢...
:掲示板機能|現地の最新情報やホタルイカに関する話題を共有...
**作成動機 [#motivation]
-富山出身でホタルイカ掬いにたまに行く。
-現地に行ってもホタルイカが湧かないことも多く、予測できた...
-既存で一番使われているホタルイカ掬い用の掲示板サイトが使...
*使用技術 [#tech_stack]
アプリ全体で使用している技術です。
***サーバーサイド [#server]
-Go(サイト)
-FastAPI(予測API)
***フロントエンド [#frontend]
-TypeScript (Next.js)
-Tailwind CSS
-shadcn/ui
***データベース・ストレージ [#db]
-Supabase
***デプロイ先 [#deploy]
-Vercel
-GCP(Cloud Run)
***機械学習 [#ml]
-LightGBM
***その他 [#others]
-Git
-GitHub
-GitHub Actions
-Docker
-Docker-Compose
-Google Cloud Scheduler
*アプリ構成図 [#architecture]
以下はこのアプリの流れを示した図になります。
#ref(アプリ構成図.png,,40%)
*開発の流れ [#dev_flow]
+''データセットの構築''
過去のホタルイカの身投げ量(実績値)と、それに対応する日...
+''機械学習モデルの訓練''
構築したデータセットを利用して、LightGBMでホタルイカの身...
+''予測APIの開発''
学習済みのモデルを組み込み、未来の天気予報や月齢などを入...
+''Webサイトの構築''
作成した予測APIを用いてWebサイトを構築。
ここからはこれらを順に解説していきます。
*データセットの構築 [#data_collection]
#ref(data_set.png,,25%)
***1. ホタルイカ身投げデータの取得 (スクレイピング) [#scr...
ホタルイカの身投げ量は公的なデータが存在しないため、掲示...
-取得元: [[ホタルイカ掲示板:https://rara.jp/hotaruika-toy...
-対象期間: 2015年〜2025年の10年間(各年2月〜5月)、約1300...
-処理内容:
++口コミ内容をスクレイピングで取得。
++内容から身投げ量を5段階評価し、0〜4の整数で数値化。
++日ごとの平均値を算出し、その日の「身投げ量」として日付...
***2. 過去の気象データの取得 [#weather_data]
[[気象庁公式サイト:https://www.data.jma.go.jp/risk/obsdl/...
※身投げデータが存在する日付に対応するデータを取得。
***3. 潮位データの取得 [#tide_data]
外部API([[tide736.net:https://tide736.net/#google_vignet...
***4. 月齢データの取得 [#moon_data]
西暦と日付から計算式を用いて算出しました。
***5. データの統合 [#data_integration]
上記で作成した複数のJSONファイルを、日付をキー(主キー)...
*機械学習モデルの訓練 [#ml_training]
私は、機械学習などの分野は全くの未経験だったのでAIに聞き...
***モデル構築のステップ [#ml_steps]
+データ読み込みと前処理: JSON形式のデータを読み込み、扱い...
+特徴量エンジニアリング: 予測に役立ちそうな新しい特徴量を...
+モデル学習: LightGBMを使ってモデルを学習
+評価と可視化: モデルの精度を評価し、結果を可視化して考察
以下はモデル構築の流れを示した図になります。
#ref(model_learn.png,,70%)
**1. データ読み込みと前処理 [#data_process]
まず、様々なデータソースから収集・統合した JSON ファイル...
このファイルには、日ごとのホタルイカの平均身投げ量と、そ...
対象期間は2〜5月の10年間で、計1220日分のデータがあります。
**2. 特徴量エンジニアリング [#feature_eng]
***使用した特徴量 (説明変数) [#features]
予測に使用した主な特徴量は以下の通りです。
:時間特徴量|年、月、日、曜日、年の第何週
:月齢 (sin/cos変換)|月齢(0〜29.53日周期)を円周上の点と...
--月齢 sin: 上弦の月で最大(1)、下弦の月で最小(-1)
--月齢 cos: 新月で最大(1)、満月で最小(-1)
:気温|平均、最大、最小、標準偏差。
--時間帯別(10-13時、14-17時、18-21時、22-0時、1-4時)の...
:降水量|合計降水量、降水の有無(バイナリ変数)。
--時間帯別の合計も算出。
:風速・風向|平均、最大、最小、標準偏差。
:潮汐データ|
--潮型: 若潮、長潮、小潮、中潮、大潮を数値化。
--夜間潮位: 夜間(21時〜5時)の満潮・干潮の高さ(平均、最...
:ラグ特徴量|過去の情報を特徴量として追加。
--気象・潮汐変数: 1日前、2日前の値。
--目的変数(avg_amount): 1日前、2日前、直近3日間の平均値。
***工夫した点 [#r5c8c58d]
「周期性」と「過去の情報」の2つの観点を工夫し特徴量を作成...
:sin/cos変換で周期性を表現|
月齢(約29.5日周期)や日付(365日周期)といった周期的なデ...
そこでsin/cos変換を行い、周期的なデータを円周上の点として...
# 月齢をsin/cosに変換
df['moon_age_sin'] = np.sin(2 * np.pi * df['moon_age'] /...
df['moon_age_cos'] = np.cos(2 * np.pi * df['moon_age'] /...
# 1年のうちの日付をsin/cosに変換
df['day_of_year_sin'] = np.sin(2 * np.pi * df['day_of_ye...
df['day_of_year_cos'] = np.cos(2 * np.pi * df['day_of_ye...
:ラグ特徴量で過去の情報を利用|
ラグ特徴量も作成しました。これは、過去のデータを当日の特...
# ラグ特徴量を作成したいカラムのリスト
cols_for_lag = [
'moon_age_sin', 'moon_age_cos', 'temperature_mean',
'precipitation_sum', 'wind_speed_mean', 'wind_direct...
]
# forループで1日前と2日前のデータ(ラグ)を新しい列とし...
for col in cols_for_lag:
df[f'{col}_lag1'] = df[col].shift(1) # 1日前の値
df[f'{col}_lag2'] = df[col].shift(2) # 2日前の値
**3. モデル学習 [#model_training]
今回は高精度で計算も高速なLightGBMを使用しました。
***時系列データの交差検証とハイパーパラメータチューニング...
時系列データを扱う上で最も重要なのは、未来のデータを使っ...
これを防ぐため、交差検証にTimeSeriesSplitを使用し、常に過...
from sklearn.model_selection import TimeSeriesSplit, Gri...
import lightgbm as lgb
# 特徴量Xと目的変数yを準備
features = [col for col in df.columns if col not in ['da...
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, scorin...
gs.fit(X, y)
# 最も性能の良かったモデルを取得
best_model = gs.best_estimator_
**4. 評価と可視化 [#evaluation]
モデルの精度を評価します。
ここでは、学習には使っていない 全データの最後の20% を「テ...
評価指標は以下の通りです。今回は、0から1の範囲に正規化さ...
-R² (決定係数): 1に近いほど良いモデル。
-MAE (平均絶対誤差): 予測値と実測値の誤差の平均。小さいほ...
-RMSE (二乗平均平方根誤差): MAEと同様に誤差の指標。大きな...
***算出された評価指数 [#score]
このようになりました。
-決定係数 (R²): 0.7008
-平均絶対誤差 (MAE): 0.0721
-二乗平均平方根誤差 (RMSE): 0.1013
***予測結果と実測値の比較 [#comparison]
学習済みモデルを使ってテストデータの身投げ量を予測し、実...
#ref(予測精度グラフ.png,,30%)
青色が実際の過去の身投げ量(正解値)で、赤色が予測された...
グラフを見ると、身投げ量の増減はある程度捉えられています...
***特徴量の重要度 [#importance]
特徴量の重要度の上位には、temperature_std、precipitation_...
*予測APIの開発 [#api_dev]
**作成したAPIの概要 [#api_overview]
前回で学習させたモデルを使用して予測値を返すAPIを開発しま...
**APIの構造 [#api_structure]
APIは以下の流れで動作します。リクエストを受けると、外部の...
#ref(API概要.png,,20%)
**使用した外部API [#external_api]
非営利なら無料で使用できるこれらのAPIを使用しました。
**APIエンドポイント [#endpoint]
1週間分(当日を含む7日間)のホタルイの予測身投げ量データ...
-エンドポイント: /predict/week
-HTTPメソッド: GET
-認証: 不要
***レスポンスボディの例 [#response_example]
(7日分のデータが配列で返されますが、ここでは1日分の例を...
[
{
"date": "2025-4-10",
"predicted_amount": 1.5,
"moon_age": 18.2,
"weather_code": 3,
"temperature_max": 18.5,
"temperature_min": 12.3,
"precipitation_probability_max": 20,
"dominant_wind_direction": 270
},
]
**時間の基準 [#time_standard]
ホタルイカの身投げは主に 22:00〜翌4:00ごろ に発生します。
そのため、このサイトでは 1日の切り替え時刻を 5:00 に設定...
たとえば「4/10 の予報」は、実際には「4/10の22:00 〜 4/11...
Webサイトの開発のセクションで後述しますが、このAPIには、2...
そこで問題なのが2:00のアクセスです。例えば「4/11の2:00」...
この問題を解決するために、API 側のタイムゾーンを日本時間...
# Cloud Run にデプロイする際の設定例
gcloud run deploy [SERVICE_NAME] \
--image [IMAGE_URL] \
--set-env-vars "TZ=Etc/GMT-13"
**大変だったこと [#api_challenges]
「学習時と予測時でデータの前処理を完全に一致させること」...
学習時に実施した欠損値補完・スケーリング・特徴量生成など...
APIで予測時に使用した特徴量の値をログで出して、実際の値と...
*Webサイトの構築 [#web_dev]
作成した予測APIを用いて、Webサイトを作成しました。
アプリケーション全体構成の中の、この赤枠の部分がこのセク...
#ref(アプリ構成図_web.png,,25%)
**アプリケーション構成 [#web_config]
-フロントエンド: Next.js (TypeScript), shadcn/ui, Tailwin...
-バックエンド: Go
-データベース: PostgreSQL
**バックエンド (Go) [#backend]
***主なAPIエンドポイント [#backend_endpoints]
-予報関連
--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: ログアウト。
***キャッシュ [#cache]
外部API(気象情報、潮汐情報、先ほど解説した予測API)へユ...
そこで、アプリの構成図のように、頻繁にアクセスされるデー...
backend/internal/cache/cache.go
// CacheManager は予測データと詳細データのキャッシュを管理
type CacheManager struct {
logger *slog.Logger
predictionURL string
predictionCache struct {
sync.RWMutex
data []byte
}
detailCache struct {
sync.RWMutex
data map[string][]byte
}
}
// FetchAndCachePredictionData は予測データを取得しキャ...
func (c *CacheManager) FetchAndCachePredictionData() {
// ... 外部APIからデータを取得 ...
c.predictionCache.Lock()
c.predictionCache.data = body
c.predictionCache.Unlock()
}
// GetPredictionData はキャッシュされた予測データを返す
func (c *CacheManager) GetPredictionData() []byte {
c.predictionCache.RLock()
defer c.predictionCache.RUnlock()
return c.predictionCache.data
}
/api/tasks/refresh-cacheにPOSTリクエストがあるとFetchAndC...
/api/prediction および /api/detail/{date} エンドポイント...
***管理者機能とJWT認証 [#admin_jwt]
口コミの削除など、特定の操作は管理者のみが行えるように制...
backend/internal/handler/admin.go
func (h *Handler) adminLoginHandler(w http.ResponseWrite...
// ... パスワード検証 ...
// JWT Claimsを設定
expirationTime := time.Now().Add(24 * time.Hour)
claims := &model.Claims{
Role: "admin",
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(expirationTime),
},
}
// トークンを生成し、署名
token := jwt.NewWithClaims(jwt.SigningMethodHS256, c...
tokenString, err := token.SignedString(h.jwtKey)
// ...
// Cookieにトークンをセット
http.SetCookie(w, &http.Cookie{
Name: "admin_token",
Value: tokenString,
Expires: expirationTime,
HttpOnly: true,
Path: "/",
SameSite: http.SameSiteNoneMode,
Secure: true,
})
}
ログインに成功すると、Role: "admin" という情報を含んだJWT...
**データベース (PostgreSQL & Supabase) [#database]
***テーブル設計 [#table_design]
このサイトではログイン機能を実装していないためUserテーブ...
#ref(DB構成図.png,,20%)
-posts: 口コミの投稿そのものを格納。
-replies: posts テーブルへの返信を格納。parent_reply_id ...
-reactions: 「good」「bad」といったリアクション情報を格納。
**フロントエンド (Next.js) [#frontend_js]
***ページ構成とデータ取得 [#page_structure]
-トップページ (/): クライアントコンポーネントとして実装。
-詳細ページ (/detail/[date]): サーバーコンポーネントとし...
***湧きレベル判定 [#waki_level]
このサイトでは、予測身投げ量を「湧きなし」「プチ湧き」「...
APIから予測身投げ量の数値が返ってくるのですが、その値は0~...
|~範囲 (x)|~評価|h
|x < 0.25|湧きなし|
|0.25 ≦ x < 0.5|プチ湧き|
|0.5 ≦ x < 0.75|チョイ湧き|
|0.75 ≦ x < 1|湧き|
|1 ≦ x < 1.25|大湧き|
|1.25 ≦ x|爆湧き|
としています。機械学習セクションでの予測結果と実測値の比...
***複雑な状態を管理するコンポーネント [#component]
CommentSection は、口コミの表示、投稿、フィルタリング、検...
frontend/components/Community/CommentSection.tsx
const CommentSection = ({ ... }: CommentSectionProps) => {
// 投稿内容、画像、ラベルなどの状態
const [newComment, setNewComment] = useState('');
const [selectedImages, setSelectedImages] = useState<F...
const [selectedLabel, setSelectedLabel] = useState<str...
// フィルタリング、検索、ソート、ページネーションの状態
const [selectedFilterLabel, setSelectedFilterLabel] = ...
const [searchQuery, setSearchQuery] = useState<string>...
const [sortOrder, setSortOrder] = useState<'newest' | ...
const [currentPage, setCurrentPage] = useState<number>...
// フィルタリングとソートを適用したメモ化済みのコメン...
const sortedComments = useMemo(() => {
// ... フィルタリングとソートのロジック ...
}, [comments, searchQuery, sortOrder]);
// ページネーションを適用した最終的な表示用コメントリ...
const paginatedComments = useMemo(() => sortedComments...
// ...
}
-状態管理: useState を多用して、ユーザーの入力や選択の状...
-パフォーマンス最適化: useMemo を活用し、フィルタリングや...
***ログイン機能無しでのリアクション管理 [#reaction]
本アプリケーションでは、ユーザーの利用開始時の負担を最小...
「good」「bad」のリアクション機能については、ユーザーが行...
***UI [#ui]
今回のアプリではUIも自分なりにこだわってみました。
全体的にモダンな雰囲気を目指し、ホタルイカの神秘的な世界...
また、深夜の時間帯に利用されることが多いと思うので、暗い...
デザイン面が苦手なので、最初に bolt.new に自分が想像して...
**開発環境と本番環境の構成 [#environments]
***ローカル開発環境 (Docker Compose) [#local_env]
開発環境では、docker-compose.yml を用いて frontend, backe...
docker-compose.yml
version: '3.8'
services:
frontend:
build:
context: ./frontend
ports:
- "3001:3000"
environment:
# コンテナ内からバックエンドサービスにアクセスする...
NEXT_PUBLIC_API_BASE_URL: http://backend:8080
depends_on:
- backend
backend:
build:
context: ./backend
ports:
- "8080:8080"
env_file:
- ./backend/.env # DB接続情報などをファイルから読...
depends_on:
db:
condition: service_healthy # DBが準備完了してか...
db:
image: postgres:14-alpine
healthcheck:
test: ["CMD-SHELL", "pg_isready -U user -d hotarui...
interval: 5s
timeout: 5s
retries: 5
# ...
***本番環境 (Cloud Run, Vercel, Supabase) [#prod_env]
本番環境では、以下のクラウドサービスを組み合わせて利用し...
-フロントエンド (Next.js): Vercel
-バックエンド (Go): Google Cloud Run
-データベース (PostgreSQL) および ストレージ: Supabase
*課題点や懸念点 [#issues]
**海岸ごとの予測ができていない [#issue_area]
現在のモデルでは、富山湾全体としてのホタルイカの湧き量(...
**身投げ量の予測精度が外部APIの精度に影響される [#issue_a...
このサイトでは、自作の機械学習モデルに外部APIから取得した...
**サイトを訪れるユーザーの母体が少ない [#issue_users]
サイトの主な利用者は最低でもホタルイカ掬いに関心のある人...
*終わりに [#conclusion]
なんとかアプリを形にすることができてよかったです。
ページ名: