AI-OMS-Analyze / scripts /forecast.py
kawaiipeace's picture
Update Function
cc2e1db
raw
history blame
55.4 kB
import pandas as pd
import numpy as np
from pathlib import Path
import warnings
try:
from prophet import Prophet
PROPHET_AVAILABLE = True
except Exception:
PROPHET_AVAILABLE = False
try:
import tensorflow as tf
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import LSTM, Bidirectional, GRU, Dense, Dropout
from sklearn.preprocessing import MinMaxScaler
TF_AVAILABLE = True
except Exception:
TF_AVAILABLE = False
def prepare_multivariate_timeseries(df: pd.DataFrame, date_col: str = 'OutageDateTime') -> pd.DataFrame:
"""Prepare multivariate time series with multiple features"""
df = df.copy()
df['dt'] = pd.to_datetime(df[date_col], format='%d-%m-%Y %H:%M:%S', errors='coerce')
df = df.dropna(subset=['dt'])
df['day'] = df['dt'].dt.floor('D')
# Aggregate daily data
daily_data = df.groupby('day').agg({
'EventNumber': 'count', # daily count
'Load(MW)': lambda x: pd.to_numeric(x, errors='coerce').mean(),
'Capacity(kVA)': lambda x: pd.to_numeric(x, errors='coerce').mean(),
'AffectedCustomer': lambda x: pd.to_numeric(x, errors='coerce').sum(),
'OpDeviceType': lambda x: x.mode().iloc[0] if len(x) > 0 else 'Unknown',
'Owner': lambda x: x.mode().iloc[0] if len(x) > 0 else 'Unknown',
'Weather': lambda x: x.mode().iloc[0] if len(x) > 0 else 'Unknown',
'EventType': lambda x: x.mode().iloc[0] if len(x) > 0 else 'Unknown'
}).reset_index()
# Rename columns
daily_data = daily_data.rename(columns={
'day': 'ds',
'EventNumber': 'daily_count',
'Load(MW)': 'avg_load_mw',
'Capacity(kVA)': 'avg_capacity_kva',
'AffectedCustomer': 'total_affected_customers'
})
# Calculate duration if available
if 'LastRestoDateTime' in df.columns:
df['last_dt'] = pd.to_datetime(df.get('LastRestoDateTime'), format='%d-%m-%Y %H:%M:%S', errors='coerce')
df['duration_min'] = (df['last_dt'] - df['dt']).dt.total_seconds() / 60.0
duration_agg = df.groupby('day')['duration_min'].sum().reset_index()
duration_agg = duration_agg.rename(columns={'day': 'ds', 'duration_min': 'total_downtime_min'})
daily_data = daily_data.merge(duration_agg, on='ds', how='left')
daily_data['total_downtime_min'] = daily_data['total_downtime_min'].fillna(0)
else:
daily_data['total_downtime_min'] = 0
# Add time features
daily_data['ds'] = pd.to_datetime(daily_data['ds'])
daily_data['day_of_week'] = daily_data['ds'].dt.dayofweek
daily_data['month'] = daily_data['ds'].dt.month
daily_data['is_weekend'] = daily_data['day_of_week'].isin([5, 6]).astype(int)
# Fill missing numeric values
numeric_cols = ['avg_load_mw', 'avg_capacity_kva', 'total_affected_customers', 'total_downtime_min']
for col in numeric_cols:
daily_data[col] = pd.to_numeric(daily_data[col], errors='coerce').fillna(daily_data[col].mean())
# Encode categorical variables
categorical_cols = ['OpDeviceType', 'Owner', 'Weather', 'EventType']
for col in categorical_cols:
daily_data[col] = daily_data[col].fillna('Unknown')
# Simple frequency encoding
freq_map = daily_data[col].value_counts().to_dict()
daily_data[f'{col}_freq'] = daily_data[col].map(freq_map)
return daily_data
def prepare_multivariate_timeseries(df: pd.DataFrame, date_col: str = 'OutageDateTime', target_metric: str = 'count') -> pd.DataFrame:
"""
Prepare multivariate time series data with multiple features per day.
Args:
df: Input dataframe
date_col: Date column name
target_metric: Target metric ('count' or 'downtime_minutes')
Returns:
DataFrame with daily aggregated features
"""
df = df.copy()
# Convert data types properly
df['dt'] = pd.to_datetime(df[date_col], format='%d-%m-%Y %H:%M:%S', errors='coerce')
df = df.dropna(subset=['dt'])
df['day'] = df['dt'].dt.floor('D')
# Convert numeric columns
numeric_cols = ['Load(MW)', 'Capacity(kVA)', 'AffectedCustomer', 'FirstStepDuration', 'LastStepDuration']
for col in numeric_cols:
if col in df.columns:
df[col] = pd.to_numeric(df[col], errors='coerce')
# Target variable
if target_metric == 'count':
daily_data = df.groupby('day').size().rename('daily_count').reset_index()
elif target_metric == 'downtime_minutes':
df['last_dt'] = pd.to_datetime(df.get('LastRestoDateTime'), format='%d-%m-%Y %H:%M:%S', errors='coerce')
df['duration_min'] = (df['last_dt'] - df['dt']).dt.total_seconds() / 60.0
daily_data = df.groupby('day')['duration_min'].sum().rename('total_downtime_min').reset_index()
else:
raise ValueError('Unsupported target_metric')
# Additional features - aggregate per day
# Numeric features
numeric_agg = df.groupby('day').agg({
'Load(MW)': 'mean',
'Capacity(kVA)': 'mean',
'AffectedCustomer': 'sum',
'FirstStepDuration': 'mean',
'LastStepDuration': 'mean'
}).reset_index()
# Time features
time_features = df.groupby('day').agg({
'dt': ['count', lambda x: x.dt.hour.mean(), lambda x: x.dt.weekday.mean()]
}).reset_index()
time_features.columns = ['day', 'event_count', 'avg_hour', 'avg_weekday']
# Categorical features - take most common per day
categorical_features = df.groupby('day').agg({
'OpDeviceType': lambda x: x.mode().iloc[0] if len(x.mode()) > 0 else 'Unknown',
'Owner': lambda x: x.mode().iloc[0] if len(x.mode()) > 0 else 'Unknown',
'Weather': lambda x: x.mode().iloc[0] if len(x.mode()) > 0 else 'Unknown',
'EventType': lambda x: x.mode().iloc[0] if len(x.mode()) > 0 else 'Unknown'
}).reset_index()
# Merge all features
daily_data = daily_data.merge(numeric_agg, on='day', how='left')
daily_data = daily_data.merge(time_features, on='day', how='left')
daily_data = daily_data.merge(categorical_features, on='day', how='left')
# Fill missing values
daily_data = daily_data.fillna({
'Load(MW)': daily_data['Load(MW)'].mean(),
'Capacity(kVA)': daily_data['Capacity(kVA)'].mean(),
'AffectedCustomer': 0,
'FirstStepDuration': daily_data['FirstStepDuration'].mean(),
'LastStepDuration': daily_data['LastStepDuration'].mean(),
'avg_hour': 12,
'avg_weekday': 3
})
# Rename day column to ds for consistency
daily_data = daily_data.rename(columns={'day': 'ds'})
return daily_data
def prepare_timeseries(df: pd.DataFrame, date_col: str = 'OutageDateTime', metric: str = 'count') -> pd.DataFrame:
"""Prepare univariate time series data (original function for backward compatibility)"""
# date_col is in format DD-MM-YYYY HH:MM:SS
df = df.copy()
df['dt'] = pd.to_datetime(df[date_col], format='%d-%m-%Y %H:%M:%S', errors='coerce')
df = df.dropna(subset=['dt'])
df['day'] = df['dt'].dt.floor('D')
if metric == 'count':
ts = df.groupby('day').size().rename('y').reset_index()
elif metric == 'downtime_minutes':
# need LastRestoDateTime
df['last_dt'] = pd.to_datetime(df.get('LastRestoDateTime'), format='%d-%m-%Y %H:%M:%S', errors='coerce')
df['duration_min'] = (df['last_dt'] - df['dt']).dt.total_seconds() / 60.0
ts = df.groupby('day')['duration_min'].sum().rename('y').reset_index()
else:
raise ValueError('Unsupported metric')
ts = ts.rename(columns={'day':'ds'})
return ts
def forecast_prophet(ts: pd.DataFrame, periods: int = 7, freq: str = 'D', hyperparams: dict = None) -> pd.DataFrame:
if not PROPHET_AVAILABLE:
raise RuntimeError('Prophet not available')
# Set default hyperparameters
if hyperparams is None:
hyperparams = {}
changepoint_prior_scale = hyperparams.get('changepoint_prior_scale', 0.05)
seasonality_prior_scale = hyperparams.get('seasonality_prior_scale', 10.0)
seasonality_mode = hyperparams.get('seasonality_mode', 'additive')
m = Prophet(
changepoint_prior_scale=changepoint_prior_scale,
seasonality_prior_scale=seasonality_prior_scale,
seasonality_mode=seasonality_mode
)
m.fit(ts)
future = m.make_future_dataframe(periods=periods, freq=freq)
fcst = m.predict(future)
return fcst[['ds','yhat','yhat_lower','yhat_upper']]
def forecast_naive(ts: pd.DataFrame, periods: int = 7) -> pd.DataFrame:
# naive: use rolling mean of last 7 days as forecast
last_mean = ts['y'].tail(7).mean() if len(ts) >= 7 else ts['y'].mean()
last_date = ts['ds'].max()
future_dates = pd.date_range(start=last_date + pd.Timedelta(days=1), periods=periods, freq='D')
return pd.DataFrame({'ds': future_dates, 'yhat': [last_mean]*periods, 'yhat_lower':[np.nan]*periods, 'yhat_upper':[np.nan]*periods})
def create_sequences(data, seq_length):
"""Create sequences for time series forecasting"""
X, y = [], []
for i in range(len(data) - seq_length):
X.append(data[i:(i + seq_length)])
y.append(data[i + seq_length])
return np.array(X), np.array(y)
def forecast_lstm(ts: pd.DataFrame, periods: int = 7, seq_length: int = 7, hyperparams: dict = None) -> pd.DataFrame:
"""Forecast using LSTM model"""
if not TF_AVAILABLE:
raise RuntimeError('TensorFlow not available')
# Set default hyperparameters
if hyperparams is None:
hyperparams = {}
seq_length = hyperparams.get('seq_length', seq_length) # Use hyperparams seq_length if provided
epochs = hyperparams.get('epochs', 100)
batch_size = hyperparams.get('batch_size', 16)
learning_rate = hyperparams.get('learning_rate', 0.001)
units = hyperparams.get('units', 100)
dropout_rate = hyperparams.get('dropout_rate', 0.2)
# Prepare data
data = ts['y'].values.reshape(-1, 1)
scaler = MinMaxScaler(feature_range=(0, 1))
scaled_data = scaler.fit_transform(data)
# Create sequences
X, y = create_sequences(scaled_data, seq_length)
if len(X) < 10: # Not enough data
return forecast_naive(ts, periods)
# Split data
train_size = int(len(X) * 0.8)
X_train, X_test = X[:train_size], X[train_size:]
y_train, y_test = y[:train_size], y[train_size:]
# Reshape for LSTM [samples, time steps, features]
X_train = X_train.reshape((X_train.shape[0], X_train.shape[1], 1))
X_test = X_test.reshape((X_test.shape[0], X_test.shape[1], 1))
# Build LSTM model
model = Sequential([
LSTM(units, activation='relu', return_sequences=True, input_shape=(seq_length, 1)),
Dropout(dropout_rate),
LSTM(units//2, activation='relu'),
Dropout(dropout_rate),
Dense(1)
])
optimizer = tf.keras.optimizers.Adam(learning_rate=learning_rate)
model.compile(optimizer=optimizer, loss='mse')
# Train model
model.fit(X_train, y_train, epochs=epochs, batch_size=batch_size, verbose=0, validation_data=(X_test, y_test))
# Make predictions
predictions = []
current_sequence = scaled_data[-seq_length:].reshape(1, seq_length, 1)
for _ in range(periods):
pred = model.predict(current_sequence, verbose=0)
predictions.append(pred[0][0])
# Update sequence for next prediction
current_sequence = np.roll(current_sequence, -1, axis=1)
current_sequence[0, -1, 0] = pred[0][0]
# Inverse transform predictions
predictions = scaler.inverse_transform(np.array(predictions).reshape(-1, 1)).flatten()
# Create forecast dataframe
last_date = ts['ds'].max()
future_dates = pd.date_range(start=last_date + pd.Timedelta(days=1), periods=periods, freq='D')
return pd.DataFrame({
'ds': future_dates,
'yhat': predictions,
'yhat_lower': predictions * 0.8, # Simple confidence intervals
'yhat_upper': predictions * 1.2
})
def forecast_bilstm(ts: pd.DataFrame, periods: int = 7, seq_length: int = 7, hyperparams: dict = None) -> pd.DataFrame:
"""Forecast using Bi-LSTM model"""
if not TF_AVAILABLE:
raise RuntimeError('TensorFlow not available')
# Set default hyperparameters
if hyperparams is None:
hyperparams = {}
seq_length = hyperparams.get('seq_length', seq_length) # Use hyperparams seq_length if provided
epochs = hyperparams.get('epochs', 50)
batch_size = hyperparams.get('batch_size', 16)
learning_rate = hyperparams.get('learning_rate', 0.001)
units = hyperparams.get('units', 50)
dropout_rate = hyperparams.get('dropout_rate', 0.2)
# Prepare data
data = ts['y'].values.reshape(-1, 1)
scaler = MinMaxScaler(feature_range=(0, 1))
scaled_data = scaler.fit_transform(data)
# Create sequences
X, y = create_sequences(scaled_data, seq_length)
if len(X) < 10: # Not enough data
return forecast_naive(ts, periods)
# Split data
train_size = int(len(X) * 0.8)
X_train, X_test = X[:train_size], X[train_size:]
y_train, y_test = y[:train_size], y[train_size:]
# Reshape for Bi-LSTM [samples, time steps, features]
X_train = X_train.reshape((X_train.shape[0], X_train.shape[1], 1))
X_test = X_test.reshape((X_test.shape[0], X_test.shape[1], 1))
# Build Bi-LSTM model
model = Sequential([
Bidirectional(LSTM(units, activation='relu'), input_shape=(seq_length, 1)),
Dropout(dropout_rate),
Dense(1)
])
optimizer = tf.keras.optimizers.Adam(learning_rate=learning_rate)
model.compile(optimizer=optimizer, loss='mse')
# Train model
model.fit(X_train, y_train, epochs=epochs, batch_size=batch_size, verbose=0, validation_data=(X_test, y_test))
# Make predictions
predictions = []
current_sequence = scaled_data[-seq_length:].reshape(1, seq_length, 1)
for _ in range(periods):
pred = model.predict(current_sequence, verbose=0)
predictions.append(pred[0][0])
# Update sequence for next prediction
current_sequence = np.roll(current_sequence, -1, axis=1)
current_sequence[0, -1, 0] = pred[0][0]
# Inverse transform predictions
predictions = scaler.inverse_transform(np.array(predictions).reshape(-1, 1)).flatten()
# Create forecast dataframe
last_date = ts['ds'].max()
future_dates = pd.date_range(start=last_date + pd.Timedelta(days=1), periods=periods, freq='D')
return pd.DataFrame({
'ds': future_dates,
'yhat': predictions,
'yhat_lower': predictions * 0.8,
'yhat_upper': predictions * 1.2
})
def forecast_gru(ts: pd.DataFrame, periods: int = 7, seq_length: int = 7, hyperparams: dict = None) -> pd.DataFrame:
"""Forecast using GRU model"""
if not TF_AVAILABLE:
raise RuntimeError('TensorFlow not available')
# Set default hyperparameters
if hyperparams is None:
hyperparams = {}
seq_length = hyperparams.get('seq_length', seq_length) # Use hyperparams seq_length if provided
epochs = hyperparams.get('epochs', 50)
batch_size = hyperparams.get('batch_size', 16)
learning_rate = hyperparams.get('learning_rate', 0.001)
units = hyperparams.get('units', 50)
dropout_rate = hyperparams.get('dropout_rate', 0.2)
# Prepare data
data = ts['y'].values.reshape(-1, 1)
scaler = MinMaxScaler(feature_range=(0, 1))
scaled_data = scaler.fit_transform(data)
# Create sequences
X, y = create_sequences(scaled_data, seq_length)
if len(X) < 10: # Not enough data
return forecast_naive(ts, periods)
# Split data
train_size = int(len(X) * 0.8)
X_train, X_test = X[:train_size], X[train_size:]
y_train, y_test = y[:train_size], y[train_size:]
# Reshape for GRU [samples, time steps, features]
X_train = X_train.reshape((X_train.shape[0], X_train.shape[1], 1))
X_test = X_test.reshape((X_test.shape[0], X_test.shape[1], 1))
# Build GRU model
model = Sequential([
GRU(units, activation='relu', input_shape=(seq_length, 1)),
Dropout(dropout_rate),
Dense(1)
])
optimizer = tf.keras.optimizers.Adam(learning_rate=learning_rate)
model.compile(optimizer=optimizer, loss='mse')
# Train model
model.fit(X_train, y_train, epochs=epochs, batch_size=batch_size, verbose=0, validation_data=(X_test, y_test))
# Make predictions
predictions = []
current_sequence = scaled_data[-seq_length:].reshape(1, seq_length, 1)
for _ in range(periods):
pred = model.predict(current_sequence, verbose=0)
predictions.append(pred[0][0])
# Update sequence for next prediction
current_sequence = np.roll(current_sequence, -1, axis=1)
current_sequence[0, -1, 0] = pred[0][0]
# Inverse transform predictions
predictions = scaler.inverse_transform(np.array(predictions).reshape(-1, 1)).flatten()
# Create forecast dataframe
last_date = ts['ds'].max()
future_dates = pd.date_range(start=last_date + pd.Timedelta(days=1), periods=periods, freq='D')
return pd.DataFrame({
'ds': future_dates,
'yhat': predictions,
'yhat_lower': predictions * 0.8,
'yhat_upper': predictions * 1.2
})
"""Forecast using multivariate LSTM model"""
if not TF_AVAILABLE:
raise RuntimeError('TensorFlow not available')
# Select features for multivariate forecasting
feature_cols = [col for col in ts.columns if col not in ['ds', 'OpDeviceType', 'Owner', 'Weather', 'EventType']]
if target_col not in feature_cols:
raise ValueError(f"Target column '{target_col}' not found in features")
# Prepare data
data = ts[feature_cols].values
scaler = MinMaxScaler(feature_range=(0, 1))
scaled_data = scaler.fit_transform(data)
# Create sequences
X, y = create_sequences(scaled_data, seq_length)
if len(X) < 10: # Not enough data
return forecast_naive(ts[['ds', target_col]].rename(columns={target_col: 'y'}), periods)
# Split data
train_size = int(len(X) * 0.8)
X_train, X_test = X[:train_size], X[train_size:]
y_train, y_test = y[:train_size], y[train_size:]
# Reshape for LSTM [samples, time steps, features]
X_train = X_train.reshape((X_train.shape[0], X_train.shape[1], len(feature_cols)))
X_test = X_test.reshape((X_test.shape[0], X_test.shape[1], len(feature_cols)))
# Build multivariate LSTM model
model = Sequential([
LSTM(100, activation='relu', return_sequences=True, input_shape=(seq_length, len(feature_cols))),
Dropout(0.2),
LSTM(50, activation='relu'),
Dropout(0.2),
Dense(1)
])
model.compile(optimizer='adam', loss='mse')
# Train model
model.fit(X_train, y_train, epochs=100, batch_size=16, verbose=0, validation_data=(X_test, y_test))
# Make predictions
predictions = []
current_sequence = scaled_data[-seq_length:].reshape(1, seq_length, len(feature_cols))
for _ in range(periods):
pred = model.predict(current_sequence, verbose=0)
predictions.append(pred[0][0])
# Update sequence for next prediction (use predicted value for target, keep other features)
new_row = current_sequence[0, -1, :].copy()
new_row[feature_cols.index(target_col)] = pred[0][0] # Update target with prediction
current_sequence = np.roll(current_sequence, -1, axis=1)
current_sequence[0, -1, :] = new_row
# Inverse transform predictions (only for target column)
target_scaler = MinMaxScaler(feature_range=(0, 1))
target_scaler.fit(data[:, feature_cols.index(target_col)].reshape(-1, 1))
predictions = target_scaler.inverse_transform(np.array(predictions).reshape(-1, 1)).flatten()
# Create forecast dataframe
last_date = ts['ds'].max()
future_dates = pd.date_range(start=last_date + pd.Timedelta(days=1), periods=periods, freq='D')
return pd.DataFrame({
'ds': future_dates,
'yhat': predictions,
'yhat_lower': predictions * 0.8,
'yhat_upper': predictions * 1.2
})
"""Forecast using LSTM model"""
if not TF_AVAILABLE:
raise RuntimeError('TensorFlow not available')
# Prepare data
data = ts['y'].values.reshape(-1, 1)
scaler = MinMaxScaler(feature_range=(0, 1))
scaled_data = scaler.fit_transform(data)
# Create sequences
X, y = create_sequences(scaled_data, seq_length)
if len(X) < 10: # Not enough data
return forecast_naive(ts, periods)
# Split data
train_size = int(len(X) * 0.8)
X_train, X_test = X[:train_size], X[train_size:]
y_train, y_test = y[:train_size], y[train_size:]
# Reshape for LSTM [samples, time steps, features]
X_train = X_train.reshape((X_train.shape[0], X_train.shape[1], 1))
X_test = X_test.reshape((X_test.shape[0], X_test.shape[1], 1))
# Build LSTM model
model = Sequential([
LSTM(50, activation='relu', input_shape=(seq_length, 1)),
Dropout(0.2),
Dense(1)
])
model.compile(optimizer='adam', loss='mse')
# Train model
model.fit(X_train, y_train, epochs=50, batch_size=16, verbose=0, validation_data=(X_test, y_test))
# Make predictions
predictions = []
current_sequence = scaled_data[-seq_length:].reshape(1, seq_length, 1)
for _ in range(periods):
pred = model.predict(current_sequence, verbose=0)
predictions.append(pred[0][0])
# Update sequence for next prediction
current_sequence = np.roll(current_sequence, -1, axis=1)
current_sequence[0, -1, 0] = pred[0][0]
# Inverse transform predictions
predictions = scaler.inverse_transform(np.array(predictions).reshape(-1, 1)).flatten()
# Create forecast dataframe
last_date = ts['ds'].max()
future_dates = pd.date_range(start=last_date + pd.Timedelta(days=1), periods=periods, freq='D')
return pd.DataFrame({
'ds': future_dates,
'yhat': predictions,
'yhat_lower': predictions * 0.8, # Simple confidence intervals
'yhat_upper': predictions * 1.2
})
def forecast_bilstm_multivariate(ts: pd.DataFrame, periods: int = 7, seq_length: int = 7, target_col: str = 'daily_count', hyperparams: dict = None) -> pd.DataFrame:
"""Forecast using multivariate Bi-LSTM model"""
if not TF_AVAILABLE:
raise RuntimeError('TensorFlow not available')
# Set default hyperparameters
if hyperparams is None:
hyperparams = {}
epochs = hyperparams.get('epochs', 100)
batch_size = hyperparams.get('batch_size', 16)
learning_rate = hyperparams.get('learning_rate', 0.001)
units = hyperparams.get('units', 100)
dropout_rate = hyperparams.get('dropout_rate', 0.2)
# Select features for multivariate forecasting
feature_cols = [col for col in ts.columns if col not in ['ds', 'OpDeviceType', 'Owner', 'Weather', 'EventType']]
if target_col not in feature_cols:
raise ValueError(f"Target column '{target_col}' not found in features")
# Prepare data
data = ts[feature_cols].values
scaler = MinMaxScaler(feature_range=(0, 1))
scaled_data = scaler.fit_transform(data)
# Create sequences
X, y = create_sequences(scaled_data, seq_length)
if len(X) < 10: # Not enough data
return forecast_naive(ts[['ds', target_col]].rename(columns={target_col: 'y'}), periods)
# Split data
train_size = int(len(X) * 0.8)
X_train, X_test = X[:train_size], X[train_size:]
y_train, y_test = y[:train_size], y[train_size:]
# Reshape for Bi-LSTM [samples, time steps, features]
X_train = X_train.reshape((X_train.shape[0], X_train.shape[1], len(feature_cols)))
X_test = X_test.reshape((X_test.shape[0], X_test.shape[1], len(feature_cols)))
# Build multivariate Bi-LSTM model
model = Sequential([
Bidirectional(LSTM(units, activation='relu', return_sequences=True), input_shape=(seq_length, len(feature_cols))),
Dropout(dropout_rate),
Bidirectional(LSTM(units//2, activation='relu')),
Dropout(dropout_rate),
Dense(1)
])
optimizer = tf.keras.optimizers.Adam(learning_rate=learning_rate)
model.compile(optimizer=optimizer, loss='mse')
# Train model
model.fit(X_train, y_train, epochs=epochs, batch_size=batch_size, verbose=0, validation_data=(X_test, y_test))
# Make predictions
predictions = []
current_sequence = scaled_data[-seq_length:].reshape(1, seq_length, len(feature_cols))
for _ in range(periods):
pred = model.predict(current_sequence, verbose=0)
predictions.append(pred[0][0])
# Update sequence for next prediction
new_row = current_sequence[0, -1, :].copy()
new_row[feature_cols.index(target_col)] = pred[0][0]
current_sequence = np.roll(current_sequence, -1, axis=1)
current_sequence[0, -1, :] = new_row
# Inverse transform predictions
target_scaler = MinMaxScaler(feature_range=(0, 1))
target_scaler.fit(data[:, feature_cols.index(target_col)].reshape(-1, 1))
predictions = target_scaler.inverse_transform(np.array(predictions).reshape(-1, 1)).flatten()
# Create forecast dataframe
last_date = ts['ds'].max()
future_dates = pd.date_range(start=last_date + pd.Timedelta(days=1), periods=periods, freq='D')
return pd.DataFrame({
'ds': future_dates,
'yhat': predictions,
'yhat_lower': predictions * 0.8,
'yhat_upper': predictions * 1.2
})
"""Forecast using Bi-LSTM model"""
if not TF_AVAILABLE:
raise RuntimeError('TensorFlow not available')
# Prepare data
data = ts['y'].values.reshape(-1, 1)
scaler = MinMaxScaler(feature_range=(0, 1))
scaled_data = scaler.fit_transform(data)
# Create sequences
X, y = create_sequences(scaled_data, seq_length)
if len(X) < 10: # Not enough data
return forecast_naive(ts, periods)
# Split data
train_size = int(len(X) * 0.8)
X_train, X_test = X[:train_size], X[train_size:]
y_train, y_test = y[:train_size], y[train_size:]
# Reshape for Bi-LSTM [samples, time steps, features]
X_train = X_train.reshape((X_train.shape[0], X_train.shape[1], 1))
X_test = X_test.reshape((X_test.shape[0], X_test.shape[1], 1))
# Build Bi-LSTM model
model = Sequential([
Bidirectional(LSTM(50, activation='relu'), input_shape=(seq_length, 1)),
Dropout(0.2),
Dense(1)
])
model.compile(optimizer='adam', loss='mse')
# Train model
model.fit(X_train, y_train, epochs=50, batch_size=16, verbose=0, validation_data=(X_test, y_test))
# Make predictions
predictions = []
current_sequence = scaled_data[-seq_length:].reshape(1, seq_length, 1)
for _ in range(periods):
pred = model.predict(current_sequence, verbose=0)
predictions.append(pred[0][0])
# Update sequence for next prediction
current_sequence = np.roll(current_sequence, -1, axis=1)
current_sequence[0, -1, 0] = pred[0][0]
# Inverse transform predictions
predictions = scaler.inverse_transform(np.array(predictions).reshape(-1, 1)).flatten()
# Create forecast dataframe
last_date = ts['ds'].max()
future_dates = pd.date_range(start=last_date + pd.Timedelta(days=1), periods=periods, freq='D')
return pd.DataFrame({
'ds': future_dates,
'yhat': predictions,
'yhat_lower': predictions * 0.8,
'yhat_upper': predictions * 1.2
})
def forecast_multivariate_lstm(ts: pd.DataFrame, target_col: str = 'target_count', periods: int = 7, seq_length: int = 7) -> pd.DataFrame:
"""Forecast using multivariate LSTM model"""
if not TF_AVAILABLE:
raise RuntimeError('TensorFlow not available')
# Prepare data - exclude date column and target
feature_cols = [col for col in ts.columns if col not in ['ds', target_col]]
target_data = ts[target_col].values.reshape(-1, 1)
# Handle categorical features - simple label encoding for demo
ts_encoded = ts.copy()
for col in feature_cols:
if ts[col].dtype == 'object':
# Simple label encoding
unique_vals = ts[col].unique()
val_to_int = {val: i for i, val in enumerate(unique_vals)}
ts_encoded[col] = ts[col].map(val_to_int)
feature_data = ts_encoded[feature_cols].values
# Scale features and target separately
feature_scaler = MinMaxScaler(feature_range=(0, 1))
target_scaler = MinMaxScaler(feature_range=(0, 1))
scaled_features = feature_scaler.fit_transform(feature_data)
scaled_target = target_scaler.fit_transform(target_data)
# Combine features and target for sequences
combined_data = np.column_stack([scaled_features, scaled_target])
# Create sequences
X, y = create_sequences(combined_data, seq_length)
if len(X) < 10: # Not enough data
# Fallback to univariate naive
univariate_ts = ts[['ds', target_col]].rename(columns={target_col: 'y'})
return forecast_naive(univariate_ts, periods)
# Split data
train_size = int(len(X) * 0.8)
X_train, X_test = X[:train_size], X[train_size:]
y_train, y_test = y[:train_size], y[train_size:]
# X shape: [samples, time_steps, features]
n_features = combined_data.shape[1]
# Build multivariate LSTM model
model = Sequential([
LSTM(64, activation='relu', input_shape=(seq_length, n_features), return_sequences=True),
Dropout(0.2),
LSTM(32, activation='relu'),
Dropout(0.2),
Dense(16, activation='relu'),
Dense(1)
])
model.compile(optimizer='adam', loss='mse')
# Train model
model.fit(X_train, y_train, epochs=50, batch_size=16, verbose=0, validation_data=(X_test, y_test))
# Make predictions
predictions = []
current_sequence = combined_data[-seq_length:].reshape(1, seq_length, n_features)
for _ in range(periods):
pred = model.predict(current_sequence, verbose=0)
predictions.append(pred[0][0])
# For next prediction, we need to estimate future features
# For simplicity, use the last known feature values
next_features = current_sequence[0, -1, :-1] # All features except target
next_sequence = np.column_stack([next_features, pred[0][0]]) # Add predicted target
# Update sequence
current_sequence = np.roll(current_sequence, -1, axis=1)
current_sequence[0, -1, :] = next_sequence
# Inverse transform predictions
predictions = target_scaler.inverse_transform(np.array(predictions).reshape(-1, 1)).flatten()
# Create forecast dataframe
last_date = ts['ds'].max()
future_dates = pd.date_range(start=last_date + pd.Timedelta(days=1), periods=periods, freq='D')
return pd.DataFrame({
'ds': future_dates,
'yhat': predictions,
'yhat_lower': predictions * 0.8,
'yhat_upper': predictions * 1.2
})
def forecast_multivariate_gru(ts: pd.DataFrame, target_col: str = 'target_count', periods: int = 7, seq_length: int = 7) -> pd.DataFrame:
"""Forecast using multivariate GRU model"""
if not TF_AVAILABLE:
raise RuntimeError('TensorFlow not available')
# Similar to multivariate LSTM but using GRU layers
feature_cols = [col for col in ts.columns if col not in ['ds', target_col]]
target_data = ts[target_col].values.reshape(-1, 1)
# Handle categorical features
ts_encoded = ts.copy()
for col in feature_cols:
if ts[col].dtype == 'object':
unique_vals = ts[col].unique()
val_to_int = {val: i for i, val in enumerate(unique_vals)}
ts_encoded[col] = ts[col].map(val_to_int)
feature_data = ts_encoded[feature_cols].values
# Scale data
feature_scaler = MinMaxScaler(feature_range=(0, 1))
target_scaler = MinMaxScaler(feature_range=(0, 1))
scaled_features = feature_scaler.fit_transform(feature_data)
scaled_target = target_scaler.fit_transform(target_data)
combined_data = np.column_stack([scaled_features, scaled_target])
# Create sequences
X, y = create_sequences(combined_data, seq_length)
if len(X) < 10:
univariate_ts = ts[['ds', target_col]].rename(columns={target_col: 'y'})
return forecast_naive(univariate_ts, periods)
# Split data
train_size = int(len(X) * 0.8)
X_train, X_test = X[:train_size], X[train_size:]
y_train, y_test = y[:train_size], y[train_size:]
n_features = combined_data.shape[1]
# Build multivariate GRU model
model = Sequential([
GRU(64, activation='relu', input_shape=(seq_length, n_features), return_sequences=True),
Dropout(0.2),
GRU(32, activation='relu'),
Dropout(0.2),
Dense(16, activation='relu'),
Dense(1)
])
model.compile(optimizer='adam', loss='mse')
# Train model
model.fit(X_train, y_train, epochs=50, batch_size=16, verbose=0, validation_data=(X_test, y_test))
# Make predictions (same logic as LSTM)
predictions = []
current_sequence = combined_data[-seq_length:].reshape(1, seq_length, n_features)
for _ in range(periods):
pred = model.predict(current_sequence, verbose=0)
predictions.append(pred[0][0])
next_features = current_sequence[0, -1, :-1]
next_sequence = np.column_stack([next_features, pred[0][0]])
current_sequence = np.roll(current_sequence, -1, axis=1)
current_sequence[0, -1, :] = next_sequence
# Inverse transform predictions
predictions = target_scaler.inverse_transform(np.array(predictions).reshape(-1, 1)).flatten()
# Create forecast dataframe
last_date = ts['ds'].max()
future_dates = pd.date_range(start=last_date + pd.Timedelta(days=1), periods=periods, freq='D')
return pd.DataFrame({
'ds': future_dates,
'yhat': predictions,
'yhat_lower': predictions * 0.8,
'yhat_upper': predictions * 1.2
})
def run_forecast(df: pd.DataFrame, metric: str = 'count', periods: int = 7, model_type: str = 'prophet', multivariate: bool = False, target_col: str = 'daily_count', hyperparams: dict = None):
"""
Run forecasting with specified model type.
Args:
df: Input dataframe
metric: 'count' or 'downtime_minutes' (for univariate)
periods: Number of periods to forecast
model_type: 'prophet', 'lstm', 'bilstm', 'gru', or 'naive'
multivariate: Whether to use multivariate forecasting
target_col: Target column for multivariate forecasting ('daily_count' or 'total_downtime_min')
hyperparams: Dictionary of hyperparameters for the model
"""
if multivariate:
ts = prepare_multivariate_timeseries(df, target_metric=metric)
# Map metric to target column
if metric == 'count':
target_col = 'daily_count'
elif metric == 'downtime_minutes':
target_col = 'total_downtime_min'
else:
target_col = 'daily_count'
if model_type == 'lstm':
if TF_AVAILABLE and len(ts) >= 14:
try:
fcst = forecast_lstm_multivariate(ts, periods=periods, target_col=target_col, hyperparams=hyperparams)
return ts, fcst
except Exception as e:
warnings.warn(f'Multivariate LSTM failed: {e}, falling back to univariate')
# Fallback to univariate
univariate_ts = prepare_timeseries(df, metric=metric)
fcst = forecast_naive(univariate_ts, periods=periods)
return univariate_ts, fcst
elif model_type == 'bilstm':
if TF_AVAILABLE and len(ts) >= 14:
try:
fcst = forecast_bilstm_multivariate(ts, periods=periods, target_col=target_col, hyperparams=hyperparams)
return ts, fcst
except Exception as e:
warnings.warn(f'Multivariate Bi-LSTM failed: {e}, falling back to univariate')
# Fallback to univariate
univariate_ts = prepare_timeseries(df, metric=metric)
fcst = forecast_naive(univariate_ts, periods=periods)
return univariate_ts, fcst
elif model_type == 'gru':
if TF_AVAILABLE and len(ts) >= 14:
try:
fcst = forecast_gru_multivariate(ts, periods=periods, target_col=target_col, hyperparams=hyperparams)
return ts, fcst
except Exception as e:
warnings.warn(f'Multivariate GRU failed: {e}, falling back to univariate')
# Fallback to univariate
univariate_ts = prepare_timeseries(df, metric=metric)
fcst = forecast_naive(univariate_ts, periods=periods)
return univariate_ts, fcst
else:
# For prophet and other models, fall back to univariate
if multivariate:
warnings.warn(f'Model {model_type} does not support multivariate forecasting. Using univariate {model_type} instead.')
univariate_ts = prepare_timeseries(df, metric=metric)
if model_type == 'prophet':
if PROPHET_AVAILABLE and len(univariate_ts) >= 14:
try:
fcst = forecast_prophet(univariate_ts, periods=periods, hyperparams=hyperparams)
return univariate_ts, fcst
except Exception:
warnings.warn('Prophet failed, falling back to naive')
fcst = forecast_naive(univariate_ts, periods=periods)
else:
fcst = forecast_naive(univariate_ts, periods=periods)
return univariate_ts, fcst
else:
# Use univariate approach (original logic)
ts = prepare_timeseries(df, metric=metric)
if model_type == 'prophet':
if PROPHET_AVAILABLE and len(ts) >= 14:
try:
fcst = forecast_prophet(ts, periods=periods, hyperparams=hyperparams)
return ts, fcst
except Exception:
warnings.warn('Prophet failed, falling back to naive')
fcst = forecast_naive(ts, periods=periods)
elif model_type == 'lstm':
if TF_AVAILABLE and len(ts) >= 14:
try:
fcst = forecast_lstm(ts, periods=periods, hyperparams=hyperparams)
return ts, fcst
except Exception as e:
warnings.warn(f'LSTM failed: {e}, falling back to naive')
fcst = forecast_naive(ts, periods=periods)
elif model_type == 'bilstm':
if TF_AVAILABLE and len(ts) >= 14:
try:
fcst = forecast_bilstm(ts, periods=periods, hyperparams=hyperparams)
return ts, fcst
except Exception as e:
warnings.warn(f'Bi-LSTM failed: {e}, falling back to naive')
fcst = forecast_naive(ts, periods=periods)
elif model_type == 'gru':
if TF_AVAILABLE and len(ts) >= 14:
try:
fcst = forecast_gru(ts, periods=periods, hyperparams=hyperparams)
return ts, fcst
except Exception as e:
warnings.warn(f'GRU failed: {e}, falling back to naive')
fcst = forecast_naive(ts, periods=periods)
else: # naive or unknown model_type
fcst = forecast_naive(ts, periods=periods)
return ts, fcst
def forecast_gru_multivariate(ts: pd.DataFrame, periods: int = 7, seq_length: int = 7, target_col: str = 'daily_count', hyperparams: dict = None) -> pd.DataFrame:
"""Forecast using multivariate GRU model"""
if not TF_AVAILABLE:
raise RuntimeError('TensorFlow not available')
# Set default hyperparameters
if hyperparams is None:
hyperparams = {}
epochs = hyperparams.get('epochs', 100)
batch_size = hyperparams.get('batch_size', 16)
learning_rate = hyperparams.get('learning_rate', 0.001)
units = hyperparams.get('units', 100)
dropout_rate = hyperparams.get('dropout_rate', 0.2)
# Select features for multivariate forecasting
feature_cols = [col for col in ts.columns if col not in ['ds', 'OpDeviceType', 'Owner', 'Weather', 'EventType']]
if target_col not in feature_cols:
raise ValueError(f"Target column '{target_col}' not found in features")
# Prepare data
data = ts[feature_cols].values
scaler = MinMaxScaler(feature_range=(0, 1))
scaled_data = scaler.fit_transform(data)
# Create sequences
X, y = create_sequences(scaled_data, seq_length)
if len(X) < 10: # Not enough data
return forecast_naive(ts[['ds', target_col]].rename(columns={target_col: 'y'}), periods)
# Split data
train_size = int(len(X) * 0.8)
X_train, X_test = X[:train_size], X[train_size:]
y_train, y_test = y[:train_size], y[train_size:]
# Reshape for GRU [samples, time steps, features]
X_train = X_train.reshape((X_train.shape[0], X_train.shape[1], len(feature_cols)))
X_test = X_test.reshape((X_test.shape[0], X_test.shape[1], len(feature_cols)))
# Build multivariate GRU model
model = Sequential([
GRU(units, activation='relu', return_sequences=True, input_shape=(seq_length, len(feature_cols))),
Dropout(dropout_rate),
GRU(units//2, activation='relu'),
Dropout(dropout_rate),
Dense(1)
])
optimizer = tf.keras.optimizers.Adam(learning_rate=learning_rate)
model.compile(optimizer=optimizer, loss='mse')
# Train model
model.fit(X_train, y_train, epochs=epochs, batch_size=batch_size, verbose=0, validation_data=(X_test, y_test))
# Make predictions
predictions = []
current_sequence = scaled_data[-seq_length:].reshape(1, seq_length, len(feature_cols))
for _ in range(periods):
pred = model.predict(current_sequence, verbose=0)
predictions.append(pred[0][0])
# Update sequence for next prediction
new_row = current_sequence[0, -1, :].copy()
new_row[feature_cols.index(target_col)] = pred[0][0]
current_sequence = np.roll(current_sequence, -1, axis=1)
current_sequence[0, -1, :] = new_row
# Inverse transform predictions
target_scaler = MinMaxScaler(feature_range=(0, 1))
target_scaler.fit(data[:, feature_cols.index(target_col)].reshape(-1, 1))
predictions = target_scaler.inverse_transform(np.array(predictions).reshape(-1, 1)).flatten()
# Create forecast dataframe
last_date = ts['ds'].max()
future_dates = pd.date_range(start=last_date + pd.Timedelta(days=1), periods=periods, freq='D')
return pd.DataFrame({
'ds': future_dates,
'yhat': predictions,
'yhat_lower': predictions * 0.8,
'yhat_upper': predictions * 1.2
})
"""Forecast using GRU model (univariate)"""
if not TF_AVAILABLE:
raise RuntimeError('TensorFlow not available')
# Prepare data
data = ts['y'].values.reshape(-1, 1)
scaler = MinMaxScaler(feature_range=(0, 1))
scaled_data = scaler.fit_transform(data)
# Create sequences
X, y = create_sequences(scaled_data, seq_length)
if len(X) < 10: # Not enough data
return forecast_naive(ts, periods)
# Split data
train_size = int(len(X) * 0.8)
X_train, X_test = X[:train_size], X[train_size:]
y_train, y_test = y[:train_size], y[train_size:]
# Reshape for GRU [samples, time steps, features]
X_train = X_train.reshape((X_train.shape[0], X_train.shape[1], 1))
X_test = X_test.reshape((X_test.shape[0], X_test.shape[1], 1))
# Build GRU model
model = Sequential([
GRU(50, activation='relu', input_shape=(seq_length, 1)),
Dropout(0.2),
Dense(1)
])
model.compile(optimizer='adam', loss='mse')
# Train model
model.fit(X_train, y_train, epochs=50, batch_size=16, verbose=0, validation_data=(X_test, y_test))
# Make predictions
predictions = []
current_sequence = scaled_data[-seq_length:].reshape(1, seq_length, 1)
for _ in range(periods):
pred = model.predict(current_sequence, verbose=0)
predictions.append(pred[0][0])
# Update sequence for next prediction
current_sequence = np.roll(current_sequence, -1, axis=1)
current_sequence[0, -1, 0] = pred[0][0]
# Inverse transform predictions
predictions = scaler.inverse_transform(np.array(predictions).reshape(-1, 1)).flatten()
# Create forecast dataframe
last_date = ts['ds'].max()
future_dates = pd.date_range(start=last_date + pd.Timedelta(days=1), periods=periods, freq='D')
return pd.DataFrame({
'ds': future_dates,
'yhat': predictions,
'yhat_lower': predictions * 0.8,
'yhat_upper': predictions * 1.2
})
def forecast_lstm_multivariate(ts: pd.DataFrame, periods: int = 7, seq_length: int = 7, target_col: str = 'daily_count', hyperparams: dict = None) -> pd.DataFrame:
"""Forecast using multivariate LSTM model"""
if not TF_AVAILABLE:
raise RuntimeError('TensorFlow not available')
# Set default hyperparameters
if hyperparams is None:
hyperparams = {}
seq_length = hyperparams.get('seq_length', seq_length) # Use hyperparams seq_length if provided
epochs = hyperparams.get('epochs', 100)
batch_size = hyperparams.get('batch_size', 16)
learning_rate = hyperparams.get('learning_rate', 0.001)
units = hyperparams.get('units', 100)
dropout_rate = hyperparams.get('dropout_rate', 0.2)
# Select features for multivariate forecasting
feature_cols = [col for col in ts.columns if col not in ['ds', 'OpDeviceType', 'Owner', 'Weather', 'EventType']]
if target_col not in feature_cols:
raise ValueError(f"Target column '{target_col}' not found in features")
# Prepare data
data = ts[feature_cols].values
scaler = MinMaxScaler(feature_range=(0, 1))
scaled_data = scaler.fit_transform(data)
# Create sequences
X, y = create_sequences(scaled_data, seq_length)
if len(X) < 10: # Not enough data
return forecast_naive(ts[['ds', target_col]].rename(columns={target_col: 'y'}), periods)
# Split data
train_size = int(len(X) * 0.8)
X_train, X_test = X[:train_size], X[train_size:]
y_train, y_test = y[:train_size], y[train_size:]
# Reshape for LSTM [samples, time steps, features]
X_train = X_train.reshape((X_train.shape[0], X_train.shape[1], len(feature_cols)))
X_test = X_test.reshape((X_test.shape[0], X_test.shape[1], len(feature_cols)))
# Build multivariate LSTM model
model = Sequential([
LSTM(units, activation='relu', return_sequences=True, input_shape=(seq_length, len(feature_cols))),
Dropout(dropout_rate),
LSTM(units//2, activation='relu'),
Dropout(dropout_rate),
Dense(1)
])
optimizer = tf.keras.optimizers.Adam(learning_rate=learning_rate)
model.compile(optimizer=optimizer, loss='mse')
# Train model
model.fit(X_train, y_train, epochs=epochs, batch_size=batch_size, verbose=0, validation_data=(X_test, y_test))
# Make predictions
predictions = []
current_sequence = scaled_data[-seq_length:].reshape(1, seq_length, len(feature_cols))
for _ in range(periods):
pred = model.predict(current_sequence, verbose=0)
predictions.append(pred[0][0])
# Update sequence for next prediction (use predicted value for target, keep other features)
new_row = current_sequence[0, -1, :].copy()
new_row[feature_cols.index(target_col)] = pred[0][0] # Update target with prediction
current_sequence = np.roll(current_sequence, -1, axis=1)
current_sequence[0, -1, :] = new_row
# Inverse transform predictions (only for target column)
target_scaler = MinMaxScaler(feature_range=(0, 1))
target_scaler.fit(data[:, feature_cols.index(target_col)].reshape(-1, 1))
predictions = target_scaler.inverse_transform(np.array(predictions).reshape(-1, 1)).flatten()
# Create forecast dataframe
last_date = ts['ds'].max()
future_dates = pd.date_range(start=last_date + pd.Timedelta(days=1), periods=periods, freq='D')
return pd.DataFrame({
'ds': future_dates,
'yhat': predictions,
'yhat_lower': predictions * 0.8,
'yhat_upper': predictions * 1.2
})
def forecast_bilstm_multivariate(ts: pd.DataFrame, periods: int = 7, seq_length: int = 7, target_col: str = 'daily_count', hyperparams: dict = None) -> pd.DataFrame:
"""Forecast using multivariate Bi-LSTM model"""
if not TF_AVAILABLE:
raise RuntimeError('TensorFlow not available')
# Set default hyperparameters
if hyperparams is None:
hyperparams = {}
epochs = hyperparams.get('epochs', 100)
batch_size = hyperparams.get('batch_size', 16)
learning_rate = hyperparams.get('learning_rate', 0.001)
units = hyperparams.get('units', 100)
dropout_rate = hyperparams.get('dropout_rate', 0.2)
# Select features for multivariate forecasting
feature_cols = [col for col in ts.columns if col not in ['ds', 'OpDeviceType', 'Owner', 'Weather', 'EventType']]
if target_col not in feature_cols:
raise ValueError(f"Target column '{target_col}' not found in features")
# Prepare data
data = ts[feature_cols].values
scaler = MinMaxScaler(feature_range=(0, 1))
scaled_data = scaler.fit_transform(data)
# Create sequences
X, y = create_sequences(scaled_data, seq_length)
if len(X) < 10: # Not enough data
return forecast_naive(ts[['ds', target_col]].rename(columns={target_col: 'y'}), periods)
# Split data
train_size = int(len(X) * 0.8)
X_train, X_test = X[:train_size], X[train_size:]
y_train, y_test = y[:train_size], y[train_size:]
# Reshape for Bi-LSTM [samples, time steps, features]
X_train = X_train.reshape((X_train.shape[0], X_train.shape[1], len(feature_cols)))
X_test = X_test.reshape((X_test.shape[0], X_test.shape[1], len(feature_cols)))
# Build multivariate Bi-LSTM model
model = Sequential([
Bidirectional(LSTM(units, activation='relu', return_sequences=True), input_shape=(seq_length, len(feature_cols))),
Dropout(dropout_rate),
Bidirectional(LSTM(units//2, activation='relu')),
Dropout(dropout_rate),
Dense(1)
])
optimizer = tf.keras.optimizers.Adam(learning_rate=learning_rate)
model.compile(optimizer=optimizer, loss='mse')
# Train model
model.fit(X_train, y_train, epochs=epochs, batch_size=batch_size, verbose=0, validation_data=(X_test, y_test))
# Make predictions
predictions = []
current_sequence = scaled_data[-seq_length:].reshape(1, seq_length, len(feature_cols))
for _ in range(periods):
pred = model.predict(current_sequence, verbose=0)
predictions.append(pred[0][0])
# Update sequence for next prediction
new_row = current_sequence[0, -1, :].copy()
new_row[feature_cols.index(target_col)] = pred[0][0]
current_sequence = np.roll(current_sequence, -1, axis=1)
current_sequence[0, -1, :] = new_row
# Inverse transform predictions
target_scaler = MinMaxScaler(feature_range=(0, 1))
target_scaler.fit(data[:, feature_cols.index(target_col)].reshape(-1, 1))
predictions = target_scaler.inverse_transform(np.array(predictions).reshape(-1, 1)).flatten()
# Create forecast dataframe
last_date = ts['ds'].max()
future_dates = pd.date_range(start=last_date + pd.Timedelta(days=1), periods=periods, freq='D')
return pd.DataFrame({
'ds': future_dates,
'yhat': predictions,
'yhat_lower': predictions * 0.8,
'yhat_upper': predictions * 1.2
})
def forecast_gru_multivariate(ts: pd.DataFrame, periods: int = 7, seq_length: int = 7, target_col: str = 'daily_count', hyperparams: dict = None) -> pd.DataFrame:
"""Forecast using multivariate GRU model"""
if not TF_AVAILABLE:
raise RuntimeError('TensorFlow not available')
# Set default hyperparameters
if hyperparams is None:
hyperparams = {}
epochs = hyperparams.get('epochs', 100)
batch_size = hyperparams.get('batch_size', 16)
learning_rate = hyperparams.get('learning_rate', 0.001)
units = hyperparams.get('units', 100)
dropout_rate = hyperparams.get('dropout_rate', 0.2)
# Select features for multivariate forecasting
feature_cols = [col for col in ts.columns if col not in ['ds', 'OpDeviceType', 'Owner', 'Weather', 'EventType']]
if target_col not in feature_cols:
raise ValueError(f"Target column '{target_col}' not found in features")
# Prepare data
data = ts[feature_cols].values
scaler = MinMaxScaler(feature_range=(0, 1))
scaled_data = scaler.fit_transform(data)
# Create sequences
X, y = create_sequences(scaled_data, seq_length)
if len(X) < 10: # Not enough data
return forecast_naive(ts[['ds', target_col]].rename(columns={target_col: 'y'}), periods)
# Split data
train_size = int(len(X) * 0.8)
X_train, X_test = X[:train_size], X[train_size:]
y_train, y_test = y[:train_size], y[train_size:]
# Reshape for GRU [samples, time steps, features]
X_train = X_train.reshape((X_train.shape[0], X_train.shape[1], len(feature_cols)))
X_test = X_test.reshape((X_test.shape[0], X_test.shape[1], len(feature_cols)))
# Build multivariate GRU model
model = Sequential([
GRU(units, activation='relu', return_sequences=True, input_shape=(seq_length, len(feature_cols))),
Dropout(dropout_rate),
GRU(units//2, activation='relu'),
Dropout(dropout_rate),
Dense(1)
])
optimizer = tf.keras.optimizers.Adam(learning_rate=learning_rate)
model.compile(optimizer=optimizer, loss='mse')
# Train model
model.fit(X_train, y_train, epochs=epochs, batch_size=batch_size, verbose=0, validation_data=(X_test, y_test))
# Make predictions
predictions = []
current_sequence = scaled_data[-seq_length:].reshape(1, seq_length, len(feature_cols))
for _ in range(periods):
pred = model.predict(current_sequence, verbose=0)
predictions.append(pred[0][0])
# Update sequence for next prediction
new_row = current_sequence[0, -1, :].copy()
new_row[feature_cols.index(target_col)] = pred[0][0]
current_sequence = np.roll(current_sequence, -1, axis=1)
current_sequence[0, -1, :] = new_row
# Inverse transform predictions
target_scaler = MinMaxScaler(feature_range=(0, 1))
target_scaler.fit(data[:, feature_cols.index(target_col)].reshape(-1, 1))
predictions = target_scaler.inverse_transform(np.array(predictions).reshape(-1, 1)).flatten()
# Create forecast dataframe
last_date = ts['ds'].max()
future_dates = pd.date_range(start=last_date + pd.Timedelta(days=1), periods=periods, freq='D')
return pd.DataFrame({
'ds': future_dates,
'yhat': predictions,
'yhat_lower': predictions * 0.8,
'yhat_upper': predictions * 1.2
})