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 })