from __future__ import annotations import pandas as pd import numpy as np from . import storage # 可能なら Prophet / NeuralProphet を使用(無ければフォールバック) try: from prophet import Prophet except Exception: Prophet = None try: from neuralprophet import NeuralProphet except Exception: NeuralProphet = None class SeasonalityModel: def __init__(self, campaign_id: str): self.campaign_id = campaign_id self.model = None self.model_type = "none" self.global_mean = 0.05 # データが乏しいときの既定CTR def fit(self): # イベントから時系列(1時間粒度のCTR)を作る with storage.get_conn() as con: df = pd.read_sql_query( "SELECT ts, event_type FROM events WHERE campaign_id=?", con, params=(self.campaign_id,), ) if df.empty: self.model_type = "none" return df["ts"] = pd.to_datetime(df["ts"], errors="coerce") df = df.dropna(subset=["ts"]) df["hour"] = df["ts"].dt.floor("h") agg = ( df.pivot_table( index="hour", columns="event_type", values="ts", aggfunc="count" ) .fillna(0) ) if "impression" not in agg: agg["impression"] = 0 if "click" not in agg: agg["click"] = 0 ctr = np.where( agg["impression"] > 0, agg["click"] / agg["impression"], np.nan ) if np.all(np.isnan(ctr)): self.model_type = "none" return self.global_mean = float(np.nanmean(ctr)) # Prophet / NeuralProphet の学習データ ds = agg.index.to_series().reset_index(drop=True) train = pd.DataFrame({"ds": ds, "y": pd.Series(ctr).fillna(self.global_mean).values}) try: if Prophet is not None: m = Prophet(weekly_seasonality=True, daily_seasonality=True) m.fit(train) self.model = m self.model_type = "prophet" elif NeuralProphet is not None: m = NeuralProphet(weekly_seasonality=True, daily_seasonality=True) m.fit(train, freq="H") self.model = m self.model_type = "neuralprophet" else: self.model_type = "none" except Exception: # 失敗時はフォールバック self.model_type = "none" def expected_ctr(self, context: dict) -> float: hour = int(context.get("hour", 12)) # モデルが無い場合は簡易ヒューリスティック if self.model_type in {None, "none"}: base = self.global_mean if 11 <= hour <= 13: return min(0.99, base * 1.1) if 20 <= hour <= 23: return min(0.99, base * 1.15) return max(0.01, base) # モデルあり:当日・指定時間の1点予測 now_ds = pd.Timestamp.utcnow().floor("D") + pd.Timedelta(hours=hour) if self.model_type == "prophet": yhat = float(self.model.predict(pd.DataFrame({"ds": [now_ds]}))["yhat"].iloc[0]) else: # neuralprophet yhat = float(self.model.predict(pd.DataFrame({"ds": [now_ds]}))["yhat1"].iloc[0]) return max(0.01, min(0.99, yhat))