class PM:
    codes = [
        '''

# Import necessary libraries
import pandas as pd
import numpy as np
from sklearn.preprocessing import MinMaxScaler
from scipy import stats
from statsmodels.tsa.seasonal import seasonal_decompose
import matplotlib.pyplot as plt
import seaborn as sns

# Display settings
pd.set_option('display.float_format', lambda x: '%.2f' % x)

# ==========================================================
# STEP 1: LOAD / CREATE SAMPLE DATASET
# ==========================================================

data = {
    'Date': pd.date_range(start='2021-01-01', periods=24, freq='M'),
    'Sales': [100, 120, np.nan, 130, 150, np.nan, 200, 210, 190, 400, 410, 430,
              np.nan, 450, 460, 470, 480, 490, np.nan, 500, 510, 515, 520, np.nan],
    'Customers': [20, 22, 21, 23, np.nan, 26, 28, 29, 31, 60, 62, 63,
                  np.nan, 65, 66, 67, np.nan, 69, 70, 72, 73, 74, 75, np.nan]
}
df = pd.DataFrame(data)

print(" Original Data:")
display(df.head())

# ==========================================================
# STEP 2: MISSING VALUE IMPUTATION
# ==========================================================

print("\n Missing Values Before Imputation:")
display(df.isnull().sum())

# Method 1: Fill with Mean or Median
df['Sales'].fillna(df['Sales'].median(), inplace=True)
df['Customers'].fillna(df['Customers'].mean(), inplace=True)

# Method 2 (Alternative): Linear Interpolation (for time-series)
# df['Sales'] = df['Sales'].interpolate(method='linear')

print("\n After Missing Value Imputation:")
display(df.head())

# ==========================================================
# STEP 3: NORMALIZATION / STANDARDIZATION
# ==========================================================

scaler = MinMaxScaler()
df[['Sales_norm', 'Customers_norm']] = scaler.fit_transform(df[['Sales', 'Customers']])

print("\n After Normalization:")
display(df[['Sales', 'Customers', 'Sales_norm', 'Customers_norm']].head())

# Visualize normalization
plt.figure(figsize=(8,4))
sns.lineplot(x=df.index, y=df['Sales_norm'], label='Sales_norm')
sns.lineplot(x=df.index, y=df['Customers_norm'], label='Customers_norm')
plt.title("Normalized Features")
plt.legend()
plt.show()

# ==========================================================
# STEP 4: OUTLIER DETECTION AND TREATMENT
# ==========================================================

# Detect Outliers using Z-score
z_scores = np.abs(stats.zscore(df[['Sales', 'Customers']]))
threshold = 3
outliers = (z_scores > threshold)

print("\n⚠️ Outlier Locations:")
print(np.where(outliers))

# Visualize Outliers Before Treatment
plt.figure(figsize=(6,4))
sns.boxplot(data=df[['Sales', 'Customers']])
plt.title("Before Outlier Treatment")
plt.show()

# Replace Outliers with Median
for col in ['Sales', 'Customers']:
    median_val = df[col].median()
    df.loc[np.abs(stats.zscore(df[col])) > threshold, col] = median_val

# Visualize After Treatment
plt.figure(figsize=(6,4))
sns.boxplot(data=df[['Sales', 'Customers']])
plt.title("After Outlier Treatment")
plt.show()

print("\n After Outlier Treatment:")
display(df[['Sales', 'Customers']].head())

# ==========================================================
# STEP 5: TREND, SEASONALITY, AND CYCLIC COMPONENT TREATMENT
# ==========================================================

# Set Date column as index
df.set_index('Date', inplace=True)

# Decompose time series
result = seasonal_decompose(df['Sales'], model='additive', period=6)

# Plot decomposition
result.plot()
plt.suptitle("Sales Trend, Seasonality, and Residuals", fontsize=14)
plt.show()

# Detrending and Deseasonalizing
df['Sales_detrended'] = df['Sales'] - result.trend
df['Sales_deseasonalized'] = df['Sales'] - result.seasonal

print("\n After Trend & Seasonality Treatment:")
display(df[['Sales', 'Sales_detrended', 'Sales_deseasonalized']].head())

# Visualize Detrended & Deseasonalized Data
plt.figure(figsize=(8,4))
plt.plot(df.index, df['Sales'], label='Original Sales', marker='o')
plt.plot(df.index, df['Sales_detrended'], label='Detrended', linestyle='--')
plt.plot(df.index, df['Sales_deseasonalized'], label='Deseasonalized', linestyle='-.')
plt.title("Sales - Trend & Seasonality Removed")
plt.legend()
plt.show()

# ==========================================================
# STEP 6: FINAL CLEANED DATA
# ==========================================================

print("\n🏁 Final Preprocessed Data:")
display(df.head())

        ''',
        '''
# ==============================================================
#  LINEAR & LOGISTIC REGRESSION IMPLEMENTATION & INTERPRETATION
# ==============================================================
# - Linear Regression → Predict continuous outcome (house price)
# - Logistic Regression → Predict categorical outcome (diabetes)
# - Evaluate using RMSE, R², and Accuracy
# ==============================================================

# Import required libraries
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns

from sklearn.model_selection import train_test_split
from sklearn.linear_model import LinearRegression, LogisticRegression
from sklearn.metrics import mean_squared_error, r2_score, accuracy_score, confusion_matrix, classification_report

# Set visual style
sns.set(style="whitegrid", palette="pastel")

# ==============================================================
# PART 1️ : LINEAR REGRESSION - Predicting Continuous Outcome
# ==============================================================

# Dataset: California Housing dataset
from sklearn.datasets import fetch_california_housing
housing = fetch_california_housing(as_frame=True)
df_housing = housing.frame

print(" California Housing Dataset Sample:")
display(df_housing.head())

# Define features (X) and target (y)
X = df_housing.drop(columns='MedHouseVal')  # independent variables
y = df_housing['MedHouseVal']               # dependent variable (median house value)

# Split into train/test
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

# Create & train model
lin_reg = LinearRegression()
lin_reg.fit(X_train, y_train)

# Predict
y_pred = lin_reg.predict(X_test)

# Evaluate Model
rmse = np.sqrt(mean_squared_error(y_test, y_pred))
r2 = r2_score(y_test, y_pred)

print("\n LINEAR REGRESSION RESULTS")
print(f"Root Mean Squared Error (RMSE): {rmse:.3f}")
print(f"R² Score: {r2:.3f}")

# Visualize Actual vs Predicted
plt.figure(figsize=(6,5))
sns.scatterplot(x=y_test, y=y_pred, alpha=0.5)
plt.xlabel("Actual House Value")
plt.ylabel("Predicted House Value")
plt.title("Linear Regression: Actual vs Predicted")
plt.show()

# Interpret Coefficients
coef_df = pd.DataFrame({
    'Feature': X.columns,
    'Coefficient': lin_reg.coef_
}).sort_values(by='Coefficient', ascending=False)

print("\nFeature Importance in Linear Regression:")
display(coef_df)

# ==============================================================
# PART 2️ : LOGISTIC REGRESSION - Predicting Categorical Outcome
# ==============================================================

from sklearn.datasets import load_diabetes
diabetes = load_diabetes(as_frame=True)
df_diabetes = diabetes.frame

print("\n Diabetes Dataset Sample:")
display(df_diabetes.head())

# Create a binary target:
# 1 → High disease progression (>= median)
# 0 → Low disease progression (< median)
median_val = df_diabetes['target'].median()
df_diabetes['Severity'] = (df_diabetes['target'] >= median_val).astype(int)

# Define features and new binary target
X = df_diabetes.drop(columns=['target', 'Severity'])
y = df_diabetes['Severity']

# Split dataset
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

# Train Logistic Regression Model
log_reg = LogisticRegression(max_iter=1000)
log_reg.fit(X_train, y_train)

# Predictions
y_pred = log_reg.predict(X_test)

# Evaluate
accuracy = accuracy_score(y_test, y_pred)
cm = confusion_matrix(y_test, y_pred)

print("\n LOGISTIC REGRESSION RESULTS (Diabetes Classification)")
print(f"Accuracy: {accuracy:.3f}")
print("\nConfusion Matrix:")
print(cm)

# Confusion Matrix Heatmap
plt.figure(figsize=(4,4))
sns.heatmap(cm, annot=True, fmt='d', cmap='Blues', cbar=False)
plt.title("Confusion Matrix - Diabetes Classification")
plt.xlabel("Predicted")
plt.ylabel("Actual")
plt.show()

# Classification Report
print("\nClassification Report:")
print(classification_report(y_test, y_pred, target_names=['Low Severity', 'High Severity']))

# Interpret coefficients
coef_df = pd.DataFrame({
    'Feature': X.columns,
    'Coefficient': log_reg.coef_[0]
}).sort_values(by='Coefficient', ascending=False)

print("\n Feature Influence in Logistic Regression (Diabetes):")
display(coef_df)
        ''',
        '''
# ============================================================
# LAB: Decision Tree & Random Forest for Classification & Regression
# ============================================================

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.datasets import make_classification, make_regression
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier, DecisionTreeRegressor, plot_tree
from sklearn.ensemble import RandomForestClassifier, RandomForestRegressor
from sklearn.metrics import (
    accuracy_score, classification_report, precision_score, recall_score, f1_score,
    mean_squared_error, r2_score
)

# ------------------------------------------------------------
# Helper function: Run classification experiment
# ------------------------------------------------------------
def run_classification_experiment(X, y, feature_names=None):
    print("\n🔹 Running Classification Experiment...")

    # Split dataset
    X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)

    # Initialize models
    dt = DecisionTreeClassifier(random_state=0)
    rf = RandomForestClassifier(n_estimators=100, random_state=0)

    # Train
    dt.fit(X_train, y_train)
    rf.fit(X_train, y_train)

    # Predictions
    y_pred_dt = dt.predict(X_test)
    y_pred_rf = rf.predict(X_test)

    # Evaluation (multi-class compatible)
    metrics = {}
    for name, model, y_pred in [('Decision Tree', dt, y_pred_dt), ('Random Forest', rf, y_pred_rf)]:
        acc = accuracy_score(y_test, y_pred)
        prec = precision_score(y_test, y_pred, average='weighted')
        rec = recall_score(y_test, y_pred, average='weighted')
        f1 = f1_score(y_test, y_pred, average='weighted')
        metrics[name] = {'Accuracy': acc, 'Precision': prec, 'Recall': rec, 'F1-score': f1}

        print(f"\n📘 {name} Classification Report:")
        print(classification_report(y_test, y_pred))

    # Visualization
    plt.figure(figsize=(14, 5))

    # Decision Tree visualization
    plt.subplot(1, 2, 1)
    plot_tree(dt, filled=True, feature_names=feature_names, class_names=True, max_depth=3)
    plt.title("Decision Tree (Depth ≤ 3)")

    # Feature importance comparison
    plt.subplot(1, 2, 2)
    dt_importances = dt.feature_importances_
    rf_importances = rf.feature_importances_
    indices = np.arange(len(dt_importances))

    plt.bar(indices - 0.2, dt_importances, 0.4, label="Decision Tree")
    plt.bar(indices + 0.2, rf_importances, 0.4, label="Random Forest")
    plt.title("Feature Importances Comparison")
    plt.xlabel("Features")
    plt.ylabel("Importance")
    plt.legend()
    plt.tight_layout()
    plt.show()

    return metrics


# ------------------------------------------------------------
# Helper function: Run regression experiment
# ------------------------------------------------------------
def run_regression_experiment(X, y, feature_names=None):
    print("\n🔹 Running Regression Experiment...")

    # Split dataset
    X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)

    # Initialize models
    dt = DecisionTreeRegressor(random_state=0)
    rf = RandomForestRegressor(n_estimators=100, random_state=0)

    # Train
    dt.fit(X_train, y_train)
    rf.fit(X_train, y_train)

    # Predictions
    y_pred_dt = dt.predict(X_test)
    y_pred_rf = rf.predict(X_test)

    # Evaluation
    metrics = {}
    for name, y_pred in [('Decision Tree', y_pred_dt), ('Random Forest', y_pred_rf)]:
        mse = mean_squared_error(y_test, y_pred)
        r2 = r2_score(y_test, y_pred)
        metrics[name] = {'MSE': mse, 'R²': r2}

        print(f"\n📘 {name} Regression Metrics:")
        print(f"Mean Squared Error: {mse:.3f}")
        print(f"R² Score: {r2:.3f}")

    # Visualization: Actual vs Predicted
    plt.figure(figsize=(12, 5))
    plt.subplot(1, 2, 1)
    plt.scatter(y_test, y_pred_dt, alpha=0.6)
    plt.title("Decision Tree Regression: Actual vs Predicted")
    plt.xlabel("Actual Values")
    plt.ylabel("Predicted Values")

    plt.subplot(1, 2, 2)
    plt.scatter(y_test, y_pred_rf, alpha=0.6, color='orange')
    plt.title("Random Forest Regression: Actual vs Predicted")
    plt.xlabel("Actual Values")
    plt.ylabel("Predicted Values")

    plt.tight_layout()
    plt.show()

    return metrics


# ------------------------------------------------------------
# 1️⃣ Classification Demo
# ------------------------------------------------------------
Xc, yc = make_classification(
    n_samples=1000, n_features=10, n_informative=6,
    n_redundant=2, n_classes=3, random_state=42
)
feature_names_c = [f"Feature_{i}" for i in range(Xc.shape[1])]
cls_metrics = run_classification_experiment(Xc, yc, feature_names_c)
print("\nClassification Metrics Summary:\n", pd.DataFrame(cls_metrics).T)

# ------------------------------------------------------------
# 2️⃣ Regression Demo
# ------------------------------------------------------------
Xr, yr = make_regression(
    n_samples=800, n_features=8, noise=0.4, random_state=42
)
feature_names_r = [f"Feature_{i}" for i in range(Xr.shape[1])]
reg_metrics = run_regression_experiment(Xr, yr, feature_names_r)
print("\nRegression Metrics Summary:\n", pd.DataFrame(reg_metrics).T)
        ''',
        '''
# ------------------------------------------------------------
# LAB: Support Vector Machine (SVM) for Binary Classification
# ------------------------------------------------------------

from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split, GridSearchCV
from sklearn.preprocessing import StandardScaler
from sklearn.svm import SVC
from sklearn.metrics import accuracy_score, classification_report
import matplotlib.pyplot as plt

# Step 1: Generate high-dimensional synthetic data
X, y = make_classification(
    n_samples=500, n_features=15, n_informative=8,
    n_redundant=3, n_classes=2, random_state=42
)

# Step 2: Split and scale data
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)
scaler = StandardScaler()
X_train = scaler.fit_transform(X_train)
X_test = scaler.transform(X_test)

# Step 3: Train base SVM model
svm = SVC(kernel='rbf', random_state=42)
svm.fit(X_train, y_train)
print("Base Accuracy:", accuracy_score(y_test, svm.predict(X_test)))

# Step 4: Hyperparameter tuning
params = {'C': [0.1, 1, 10], 'gamma': ['scale', 0.01, 0.001], 'kernel': ['linear', 'rbf', 'poly']}
grid = GridSearchCV(SVC(), params, cv=5)
grid.fit(X_train, y_train)

best = grid.best_estimator_
print("\nBest Parameters:", grid.best_params_)
print("Tuned Accuracy:", accuracy_score(y_test, best.predict(X_test)))
print("\nClassification Report:\n", classification_report(y_test, best.predict(X_test)))

# Step 5: Compare kernels
kernels = ['linear', 'poly', 'rbf', 'sigmoid']
acc = []
for k in kernels:
    model = SVC(kernel=k).fit(X_train, y_train)
    acc.append(accuracy_score(y_test, model.predict(X_test)))

plt.bar(kernels, acc, color='skyblue')
plt.title("SVM Kernel Comparison")
plt.ylabel("Accuracy")
plt.show()
        ''',
        '''
# Time Series Visualization and Decomposition Example
# Requirements: pandas, matplotlib, seaborn, statsmodels
# pip install pandas matplotlib seaborn statsmodels

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from statsmodels.tsa.seasonal import seasonal_decompose

# -----------------------------
# 1. Generate or Load Time Series Data
# -----------------------------
# Example: synthetic monthly sales data
np.random.seed(42)
date_rng = pd.date_range(start='2018-01-01', end='2023-12-31', freq='M')
sales = 200 + 10*np.arange(len(date_rng)) + 40*np.sin(2*np.pi*date_rng.month/12) + np.random.normal(0, 25, len(date_rng))
df = pd.DataFrame({'Date': date_rng, 'Sales': sales})

# -----------------------------
# 2. Basic Line Plot
# -----------------------------
plt.figure(figsize=(10,4))
plt.plot(df['Date'], df['Sales'], color='tab:blue')
plt.title('Monthly Sales Over Time')
plt.xlabel('Date')
plt.ylabel('Sales')
plt.grid(True)
plt.show()

# -----------------------------
# 3. Moving Average (Trend Smoothing)
# -----------------------------
df['Sales_MA3'] = df['Sales'].rolling(window=3).mean()   # 3-month moving average
df['Sales_MA12'] = df['Sales'].rolling(window=12).mean() # 12-month moving average

plt.figure(figsize=(10,4))
plt.plot(df['Date'], df['Sales'], label='Original', alpha=0.6)
plt.plot(df['Date'], df['Sales_MA3'], label='3-Month MA', linewidth=2)
plt.plot(df['Date'], df['Sales_MA12'], label='12-Month MA', linewidth=2)
plt.title('Trend Visualization using Moving Averages')
plt.xlabel('Date'); plt.ylabel('Sales')
plt.legend(); plt.grid(True); plt.show()

# -----------------------------
# 4. Seasonal Decomposition
# -----------------------------
df.set_index('Date', inplace=True)
decomposition = seasonal_decompose(df['Sales'], model='additive', period=12)

# Plot trend, seasonal, and residual components
decomposition.plot()
plt.suptitle('Time Series Decomposition', fontsize=14)
plt.show()

# -----------------------------
# 5. Yearly and Monthly Patterns (Seasonality)
# -----------------------------
df['Year'] = df.index.year
df['Month'] = df.index.month_name()

plt.figure(figsize=(10,4))
sns.boxplot(x='Month', y='Sales', data=df, order=['January','February','March','April','May','June',
                                                  'July','August','September','October','November','December'])
plt.title('Seasonality by Month')
plt.xticks(rotation=45)
plt.show()

plt.figure(figsize=(8,4))
sns.lineplot(x='Year', y='Sales', data=df, marker='o')
plt.title('Yearly Average Sales Trend')
plt.grid(True)
plt.show()

# -----------------------------
# 6. Lag Plot and Autocorrelation
# -----------------------------
from pandas.plotting import lag_plot, autocorrelation_plot

plt.figure(figsize=(4,4))
lag_plot(df['Sales'])
plt.title('Lag Plot')
plt.show()

plt.figure(figsize=(8,4))
autocorrelation_plot(df['Sales'])
plt.title('Autocorrelation Plot')
plt.show()

print("✅ Time Series Visualization Complete!")
        ''',
        '''
# Time Series Forecasting using ARIMA and SARIMA
# Requirements: pandas, numpy, matplotlib, statsmodels, sklearn
# Install if needed: pip install pandas numpy matplotlib statsmodels scikit-learn

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from statsmodels.tsa.arima.model import ARIMA
from statsmodels.tsa.statespace.sarimax import SARIMAX
from sklearn.metrics import mean_absolute_error, mean_squared_error
import warnings
warnings.filterwarnings("ignore")

# -----------------------------
# 1. Generate or Load Time Series Data
# -----------------------------
np.random.seed(42)
date_rng = pd.date_range(start='2018-01-01', end='2023-12-31', freq='M')
sales = 200 + 10*np.arange(len(date_rng)) + 40*np.sin(2*np.pi*date_rng.month/12) + np.random.normal(0, 25, len(date_rng))
df = pd.DataFrame({'Date': date_rng, 'Sales': sales})
df.set_index('Date', inplace=True)

# -----------------------------
# 2. Train-Test Split
# -----------------------------
train_size = int(len(df) * 0.8)
train, test = df.iloc[:train_size], df.iloc[train_size:]

plt.figure(figsize=(10,4))
plt.plot(train, label='Train')
plt.plot(test, label='Test')
plt.title('Train-Test Split')
plt.legend()
plt.show()

# -----------------------------
# 3. ARIMA Model (Non-seasonal)
# -----------------------------
# (p, d, q) = (autoregressive order, differencing order, moving average order)
arima_model = ARIMA(train['Sales'], order=(1,1,1))
arima_result = arima_model.fit()

# Forecast for test period
arima_forecast = arima_result.forecast(steps=len(test))

# -----------------------------
# 4. SARIMA Model (Seasonal ARIMA)
# -----------------------------
# (p,d,q)(P,D,Q,s) - s=12 for monthly data
sarima_model = SARIMAX(train['Sales'], order=(1,1,1), seasonal_order=(1,1,1,12))
sarima_result = sarima_model.fit()

sarima_forecast = sarima_result.forecast(steps=len(test))

# -----------------------------
# 5. Evaluation Metrics
# -----------------------------
def evaluate_forecast(true, predicted):
    mae = mean_absolute_error(true, predicted)
    rmse = np.sqrt(mean_squared_error(true, predicted))
    mape = np.mean(np.abs((true - predicted) / true)) * 100
    return mae, rmse, mape

arima_mae, arima_rmse, arima_mape = evaluate_forecast(test['Sales'], arima_forecast)
sarima_mae, sarima_rmse, sarima_mape = evaluate_forecast(test['Sales'], sarima_forecast)

print("🔹 ARIMA Model Performance:")
print(f"MAE: {arima_mae:.2f}, RMSE: {arima_rmse:.2f}, MAPE: {arima_mape:.2f}%")

print("\n🔹 SARIMA Model Performance:")
print(f"MAE: {sarima_mae:.2f}, RMSE: {sarima_rmse:.2f}, MAPE: {sarima_mape:.2f}%")

# -----------------------------
# 6. Visualization of Forecast vs Actual
# -----------------------------
plt.figure(figsize=(10,4))
plt.plot(train.index, train['Sales'], label='Train Data')
plt.plot(test.index, test['Sales'], label='Actual Test Data', color='black')
plt.plot(test.index, arima_forecast, label='ARIMA Forecast', linestyle='--')
plt.plot(test.index, sarima_forecast, label='SARIMA Forecast', linestyle='--')
plt.title('ARIMA vs SARIMA Forecast Comparison')
plt.xlabel('Date'); plt.ylabel('Sales')
plt.legend(); plt.grid(True)
plt.show()

# -----------------------------
# 7. Forecast into the Future
# -----------------------------
future_steps = 12  # forecast 12 months ahead
sarima_future = sarima_result.forecast(steps=future_steps)

# Create future date range
future_dates = pd.date_range(start=df.index[-1] + pd.offsets.MonthEnd(), periods=future_steps, freq='M')
future_df = pd.DataFrame({'Date': future_dates, 'Forecasted_Sales': sarima_future})
future_df.set_index('Date', inplace=True)

plt.figure(figsize=(10,4))
plt.plot(df['Sales'], label='Historical Data')
plt.plot(future_df['Forecasted_Sales'], label='Future Forecast', linestyle='--', color='red')
plt.title('12-Month Future Sales Forecast (SARIMA)')
plt.xlabel('Date'); plt.ylabel('Sales')
plt.legend(); plt.grid(True)
plt.show()

print("✅ Time Series Forecasting Complete!")
        ''',
        '''
# Time Series Forecasting using Machine Learning Models
# Requirements: pandas, numpy, matplotlib, scikit-learn
# Optional: xgboost (pip install xgboost)

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_absolute_error, mean_squared_error
from sklearn.linear_model import LinearRegression
from sklearn.ensemble import RandomForestRegressor

# Optional: XGBoost
try:
    from xgboost import XGBRegressor
    xgb_available = True
except:
    xgb_available = False

# ---------------------------------------
# 1. Generate or Load Time Series Data
# ---------------------------------------
np.random.seed(42)
date_rng = pd.date_range(start='2018-01-01', end='2023-12-31', freq='M')
sales = 200 + 10*np.arange(len(date_rng)) + 40*np.sin(2*np.pi*date_rng.month/12) + np.random.normal(0, 25, len(date_rng))

df = pd.DataFrame({'Date': date_rng, 'Sales': sales})
df.set_index('Date', inplace=True)

plt.plot(df['Sales'])
plt.title("Original Time Series")
plt.show()

# ---------------------------------------
# 2. Feature Engineering for ML
# ---------------------------------------
data = df.copy()

# Create Lag Features
data['Lag1'] = data['Sales'].shift(1)
data['Lag2'] = data['Sales'].shift(2)
data['Lag3'] = data['Sales'].shift(3)

# Moving Averages
data['MA3'] = data['Sales'].rolling(window=3).mean()
data['MA6'] = data['Sales'].rolling(window=6).mean()

# Time-based Features
data['Month'] = data.index.month
data['Year'] = data.index.year

# Drop missing rows
data = data.dropna()

# ---------------------------------------
# 3. Train-Test Split
# ---------------------------------------
train_size = int(len(data) * 0.8)
train, test = data.iloc[:train_size], data.iloc[train_size:]

X_train = train.drop(['Sales'], axis=1)
y_train = train['Sales']

X_test = test.drop(['Sales'], axis=1)
y_test = test['Sales']

# ---------------------------------------
# 4. Machine Learning Models
# ---------------------------------------

# 📌 Model 1: Linear Regression
lr_model = LinearRegression()
lr_model.fit(X_train, y_train)
lr_pred = lr_model.predict(X_test)

# 📌 Model 2: Random Forest
rf_model = RandomForestRegressor(n_estimators=200, random_state=42)
rf_model.fit(X_train, y_train)
rf_pred = rf_model.predict(X_test)

# 📌 Model 3: XGBoost (optional)
if xgb_available:
    xgb_model = XGBRegressor(n_estimators=300, learning_rate=0.05)
    xgb_model.fit(X_train, y_train)
    xgb_pred = xgb_model.predict(X_test)
else:
    xgb_pred = None

# ---------------------------------------
# 5. Evaluation Function
# ---------------------------------------
def evaluate(true, pred):
    mae = mean_absolute_error(true, pred)
    rmse = np.sqrt(mean_squared_error(true, pred))
    return mae, rmse

lr_mae, lr_rmse = evaluate(y_test, lr_pred)
rf_mae, rf_rmse = evaluate(y_test, rf_pred)

print("🔹 Linear Regression → MAE:", round(lr_mae,2), "RMSE:", round(lr_rmse,2))
print("🔹 Random Forest     → MAE:", round(rf_mae,2), "RMSE:", round(rf_rmse,2))

if xgb_pred is not None:
    xgb_mae, xgb_rmse = evaluate(y_test, xgb_pred)
    print("🔹 XGBoost           → MAE:", round(xgb_mae,2), "RMSE:", round(xgb_rmse,2))

# ---------------------------------------
# 6. Plot Predictions vs Actual
# ---------------------------------------
plt.figure(figsize=(10,4))
plt.plot(y_test.index, y_test, label='Actual', color='black')
plt.plot(y_test.index, lr_pred, label='Linear Regression', linestyle='--')
plt.plot(y_test.index, rf_pred, label='Random Forest', linestyle='--')

if xgb_pred is not None:
    plt.plot(y_test.index, xgb_pred, label='XGBoost', linestyle='--')

plt.title("Machine Learning Forecasting Comparison")
plt.xlabel("Date"); plt.ylabel("Sales")
plt.legend(); plt.grid(True)
plt.show()

# ---------------------------------------
# 7. Future Forecast Using Best Model (Random Forest)
# ---------------------------------------
future_steps = 12
future_dates = pd.date_range(start=df.index[-1] + pd.offsets.MonthEnd(), periods=future_steps, freq='M')

last_data = data.iloc[-3:]  # last few rows for creating lags dynamically
future_df = []

prev_values = list(last_data['Sales'])

for i in range(future_steps):
    lag1 = prev_values[-1]
    lag2 = prev_values[-2]
    lag3 = prev_values[-3]

    new_row = {
        'Lag1': lag1,
        'Lag2': lag2,
        'Lag3': lag3,
        'MA3': np.mean(prev_values[-3:]),
        'MA6': np.mean(prev_values[-6:]) if len(prev_values) >= 6 else np.mean(prev_values),
        'Month': future_dates[i].month,
        'Year': future_dates[i].year
    }

    pred = rf_model.predict(pd.DataFrame([new_row]))[0]
    future_df.append(pred)
    prev_values.append(pred)  # update list

future_df = pd.DataFrame({'Forecast': future_df}, index=future_dates)

plt.figure(figsize=(10,4))
plt.plot(df['Sales'], label='History')
plt.plot(future_df['Forecast'], label='Future Forecast', linestyle='--', color='red')
plt.title("12-Month Future Forecast (Random Forest)")
plt.legend(); plt.grid(True)
plt.show()

print("✅ ML-based Time Series Forecasting Complete!")
        ''',
        '''
"""
Time Series Forecasting using Signal-Processing Decomposition Techniques
- STL decomposition (trend / seasonal / residual)
- FFT low-pass smoothing (optional)
- Component forecasting:
    * Trend: linear extrapolation (numpy.polyfit)
    * Seasonal: seasonal-naive (repeat last yearly cycle)
    * Residual: ARIMA(1,0,0) on residuals (fallback to mean if ARIMA fails)
- Backtesting (holdout) and plotting
Requirements: numpy, pandas, matplotlib, statsmodels
Install if needed: pip install numpy pandas matplotlib statsmodels
"""

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from statsmodels.tsa.seasonal import STL
from statsmodels.tsa.arima.model import ARIMA
import os

# ---------------------------
# Helpers
# ---------------------------
def generate_synthetic_series(n_days=365*3, seed=42):
    """Generate synthetic daily time series with trend + yearly + weekly + noise."""
    np.random.seed(seed)
    n = n_days
    date_index = pd.date_range(start='2018-01-01', periods=n, freq='D')
    trend = 0.01 * np.arange(n)                          # small linear trend
    seasonal_yearly = 10 * np.sin(2 * np.pi * np.arange(n) / 365.25)
    weekly = 2 * np.sin(2 * np.pi * np.arange(n) / 7)
    noise = np.random.normal(scale=2.0, size=n)
    y = 50 + trend + seasonal_yearly + weekly + noise
    return pd.Series(y, index=date_index, name='value')

def fft_lowpass_filter(series, cutoff_freq_ratio=0.02):
    """Simple FFT low-pass filter. cutoff_freq_ratio between 0 and 1 (smaller -> smoother)."""
    x = series.values
    n = len(x)
    fft_coef = np.fft.rfft(x)
    freqs = np.fft.rfftfreq(n)
    cutoff = cutoff_freq_ratio * freqs.max()
    mask = freqs <= cutoff
    fft_filt = fft_coef * mask
    x_smooth = np.fft.irfft(fft_filt, n=n)
    return pd.Series(x_smooth, index=series.index)

def forecast_components_stl(ts, stl_result, forecast_horizon=120, season_length=365):
    """
    Given ts (pd.Series) and stl_result (statsmodels STL results),
    forecast next `forecast_horizon` days by component:
      - trend: linear fit (polyfit degree=1)
      - seasonal: repeat last season_length days
      - resid: ARIMA(1,0,0) on residuals (fallback to mean or zeros)
    Returns forecast_series (pd.Series).
    """
    trend_comp = stl_result.trend
    seasonal_comp = stl_result.seasonal
    resid_comp = stl_result.resid

    # future index
    last_date = ts.index[-1]
    future_index = pd.date_range(start=last_date + pd.Timedelta(days=1), periods=forecast_horizon, freq='D')

    # Trend forecast using linear polynomial fit
    x = np.arange(len(trend_comp))
    # polyfit returns [slope, intercept] for deg=1
    coef = np.polyfit(x, trend_comp.values, deg=1)
    future_x = np.arange(len(trend_comp), len(trend_comp) + forecast_horizon)
    trend_forecast = np.polyval(coef, future_x)

    # Seasonal forecast: repeat last year's seasonal cycle (or seasonal mean if not enough data)
    if len(seasonal_comp) >= season_length:
        last_season = seasonal_comp[-season_length:].values
        repeats = int(np.ceil(forecast_horizon / season_length))
        seasonal_future = np.tile(last_season, repeats)[:forecast_horizon]
    else:
        seasonal_future = np.full(forecast_horizon, seasonal_comp.mean())

    # Residual forecast using ARIMA(1,0,0) if possible
    resid = resid_comp.dropna()
    if len(resid) >= 10:
        try:
            arima_model = ARIMA(resid, order=(1,0,0)).fit()
            resid_forecast = arima_model.forecast(steps=forecast_horizon)
            # Ensure index alignment
            resid_forecast = pd.Series(resid_forecast, index=future_index).values
        except Exception as e:
            # fallback to mean residual
            resid_forecast = np.full(forecast_horizon, resid.mean())
    else:
        resid_forecast = np.zeros(forecast_horizon)

    forecast_values = trend_forecast + seasonal_future + resid_forecast
    forecast_series = pd.Series(forecast_values, index=future_index, name='forecast')
    return forecast_series

def backtest_decomposition(ts, backtest_h=120, season_length=365):
    """
    Backtest decomposition forecasting by using all data except the last backtest_h days as train,
    forecasting those last days and returning actual vs predicted series and RMSE.
    """
    train = ts[:-backtest_h]
    test = ts[-backtest_h:]

    stl_train = STL(train, period=season_length, robust=True).fit()
    pred_bt = forecast_components_stl(train, stl_train, forecast_horizon=backtest_h, season_length=season_length)

    # align predicted series index with test
    pred_bt = pred_bt[:len(test)]
    pred_bt.index = test.index

    mse = np.mean((test.values - pred_bt.values)**2)
    rmse = np.sqrt(mse)
    return test, pred_bt, rmse

# ---------------------------
# Main flow
# ---------------------------
def main():
    # 1) Generate data
    ts = generate_synthetic_series(n_days=365*3, seed=42)

    # 2) Plot raw series
    plt.figure(figsize=(12,3))
    plt.plot(ts)
    plt.title('Synthetic Time Series (3 years, daily)')
    plt.xlabel('Date')
    plt.ylabel('Value')
    plt.tight_layout()
    plt.show()

    # 3) STL decomposition
    season_length = 365
    stl = STL(ts, period=season_length, robust=True)
    stl_res = stl.fit()

    # Plot decomposition
    fig = stl_res.plot()
    fig.set_size_inches(12,8)
    plt.tight_layout()
    plt.show()

    # 4) FFT smoothing example
    smoothed = fft_lowpass_filter(ts, cutoff_freq_ratio=0.02)
    plt.figure(figsize=(12,3))
    plt.plot(ts, label='Original', alpha=0.8)
    plt.plot(smoothed, label='FFT low-pass (smoothed)', linewidth=2)
    plt.title('FFT Low-pass Smoothing (signal-processing approach)')
    plt.legend()
    plt.tight_layout()
    plt.show()

    # 5) Forecast next horizon by components
    forecast_horizon = 120
    forecast_series = forecast_components_stl(ts, stl_res, forecast_horizon=forecast_horizon, season_length=season_length)

    # Plot last 365 days + forecast
    plt.figure(figsize=(12,4))
    plt.plot(ts[-365:], label='Last 365 days (actual)')
    plt.plot(forecast_series, label=f'Forecast (next {forecast_horizon} days)')
    plt.axvline(ts.index[-1], linestyle='--', color='gray')
    plt.legend()
    plt.title('Actual (last year) + Forecast')
    plt.tight_layout()
    plt.show()

    # 6) Backtest (holdout)
    test, pred_bt, rmse = backtest_decomposition(ts, backtest_h=forecast_horizon, season_length=season_length)
    print(f"Backtest RMSE on last {forecast_horizon} days: {rmse:.4f}")

    # Plot backtest actual vs predicted
    plt.figure(figsize=(12,4))
    plt.plot(test, label='Actual (test period)')
    plt.plot(pred_bt, label='Predicted (decomposition forecast)')
    plt.legend()
    plt.title('Backtest: Actual vs Predicted (holdout)')
    plt.tight_layout()
    plt.show()

    # 7) Comparison DataFrame (first 10 rows)
    compare_df = pd.DataFrame({
        'actual': test.values,
        'predicted': pred_bt.values,
        'error': test.values - pred_bt.values
    }, index=test.index)
    print("\nHoldout sample (first 10 rows):")
    print(compare_df.head(10))

    # 8) Future forecast snippet
    print("\nFuture forecast (first 10 rows):")
    print(forecast_series.head(10))

    # 9) Save actual + forecast to CSV
    # We'll save actual series and future forecast in one CSV with columns: date, type, value
    actual_df = ts.reset_index()
    actual_df.columns = ['date', 'value']
    actual_df['type'] = 'actual'
    forecast_df = forecast_series.reset_index()
    forecast_df.columns = ['date', 'value']
    forecast_df['type'] = 'forecast'
    out_df = pd.concat([actual_df, forecast_df], ignore_index=True)
    save_path = os.path.join(os.getcwd(), 'decomposition_forecast_output.csv')
    out_df.to_csv(save_path, index=False)
    print(f"\nSaved combined actual+forecast CSV to: {save_path}")

    # 10) Short textual summary (useful for students)
    print("\n--- Summary ---")
    print("1) Decompose the series with STL to get trend, seasonal, residual.")
    print("2) Optionally smooth using FFT low-pass to remove high-frequency noise.")
    print("3) Forecast components separately (trend: linear, seasonal: seasonal-naive, residual: ARIMA).")
    print("4) Recombine component forecasts to produce the final forecast and evaluate with backtesting.")

if __name__ == "__main__":
    main()
        ''',
    ]

    @staticmethod
    def text(index):
        """Fetch a specific code based on the index."""
        try:
            return PM.codes[index - 1]
        except IndexError:
            return f"Invalid code index. Please choose a number between 1 and {len(PM.codes)}."