Spaces:
Running
Running
| import numpy as np | |
| import pandas as pd | |
| import logging | |
| import subprocess | |
| import os | |
| import sys | |
| import requests | |
| # Add the project root to the path so we can import from src | |
| sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '../..'))) | |
| from src.models.train import load_real_data | |
| logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') | |
| DRIFT_THRESHOLD = 0.05 # Significance level (alpha = 0.05) for Kolmogorov-Smirnov test | |
| def send_alert(message): | |
| """ | |
| Sends a notification alert if a WEBHOOK_URL is provided in the environment. | |
| """ | |
| webhook_url = os.environ.get("ALERT_WEBHOOK_URL") | |
| if webhook_url: | |
| try: | |
| payload = {"text": f"π’ *MLOps Alert*: {message}"} | |
| requests.post(webhook_url, json=payload, timeout=5) | |
| logging.info("Alert notification sent successfully.") | |
| except Exception as e: | |
| logging.error(f"Failed to send alert: {e}") | |
| else: | |
| logging.info("No ALERT_WEBHOOK_URL found. Skipping notification.") | |
| def fetch_latest_data(simulate_shock=False): | |
| """ | |
| Simulates fetching the latest 7 days of data from the scraper. | |
| """ | |
| logging.info("Fetching latest market data...") | |
| dates = pd.date_range(start='2026-01-01', periods=7, freq='D') | |
| # Baseline latest price would normally be around 250 (from the end of 2025 trend) | |
| base_price = 250 | |
| if simulate_shock: | |
| logging.warning("Simulating an economic shock (+30% inflation) in the incoming data!") | |
| # Artificially inflate by 30% | |
| base_price = base_price * 1.30 | |
| noise = np.random.normal(0, 5, 7) | |
| prices = base_price + noise | |
| df = pd.DataFrame({'Date': dates, 'Price': prices}) | |
| df.set_index('Date', inplace=True) | |
| return df | |
| def calculate_drift(new_data, baseline_df): | |
| """ | |
| Calculates statistical concept drift using the Kolmogorov-Smirnov (KS) test. | |
| The KS test checks whether the distribution of recent prices has drifted | |
| significantly compared to the baseline training distribution. | |
| """ | |
| from scipy.stats import ks_2samp | |
| new_prices = new_data['Price'].values | |
| baseline_prices = baseline_df['Price'].values | |
| # Run Kolmogorov-Smirnov test comparing the two price distributions | |
| ks_stat, p_value = ks_2samp(baseline_prices, new_prices) | |
| latest_mean = new_prices.mean() | |
| drift_detected = bool(p_value < DRIFT_THRESHOLD) | |
| return float(ks_stat), float(p_value), drift_detected, latest_mean | |
| def main(simulate_shock=False): | |
| # 1. Get Baseline Data | |
| logging.info("Loading baseline training data to calculate baseline mean...") | |
| baseline_df = load_real_data() | |
| # Exclude test set (last 20%) to match training data | |
| train_size = int(len(baseline_df) * 0.8) | |
| train_df = baseline_df.iloc[:train_size] | |
| baseline_mean = train_df['Price'].mean() | |
| logging.info(f"Baseline Mean Price (Training Data): {baseline_mean:.2f} LKR") | |
| # 2. Fetch Latest Data | |
| latest_data = fetch_latest_data(simulate_shock=simulate_shock) | |
| # 3. Calculate Drift | |
| ks_stat, p_value, drift_detected, latest_mean = calculate_drift(latest_data, train_df) | |
| logging.info(f"Latest 7-Day Mean Price: {latest_mean:.2f} LKR") | |
| logging.info(f"KS Statistic: {ks_stat:.4f}, p-value: {p_value:.4f}") | |
| logging.info(f"Concept Drift Detected: {drift_detected}") | |
| # 4. Save drift status to JSON for Streamlit dashboard | |
| import json | |
| from datetime import datetime | |
| retrain_triggered = drift_detected | |
| status_data = { | |
| "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), | |
| "baseline_mean": round(float(baseline_mean), 2), | |
| "latest_mean": round(float(latest_mean), 2), | |
| "drift_score": round(float(ks_stat), 4), | |
| "p_value": round(float(p_value), 4), | |
| "drift_threshold": round(float(DRIFT_THRESHOLD), 4), | |
| "drift_detected": drift_detected, | |
| "retrain_triggered": retrain_triggered, | |
| "simulation_mode": simulate_shock | |
| } | |
| os.makedirs("data/processed", exist_ok=True) | |
| try: | |
| with open("data/processed/drift_status.json", "w") as f: | |
| json.dump(status_data, f, indent=4) | |
| logging.info("Drift status successfully written to data/processed/drift_status.json") | |
| except Exception as e: | |
| logging.error(f"Failed to write drift status: {e}") | |
| # 5. Check against threshold | |
| if drift_detected: | |
| alert_msg = f"π¨ Concept Drift Detected! p-value ({p_value:.4f}) is below significance threshold ({DRIFT_THRESHOLD})." | |
| logging.error(alert_msg) | |
| send_alert(alert_msg) | |
| logging.info("Initiating automatic model retraining pipeline...") | |
| # Trigger retraining | |
| train_script_path = os.path.join(os.path.dirname(__file__), 'train.py') | |
| # Setting CWD for subprocess to project root so MLFlow logs to the correct mlruns folder | |
| project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '../..')) | |
| try: | |
| subprocess.run([sys.executable, train_script_path], cwd=project_root, check=True) | |
| logging.info("β Model retraining completed successfully. New model logged to MLflow.") | |
| send_alert("β Model retraining completed successfully. New model is live on DagsHub.") | |
| except subprocess.CalledProcessError as e: | |
| err_msg = f"β Model retraining failed: {e}" | |
| logging.error(err_msg) | |
| send_alert(err_msg) | |
| else: | |
| logging.info(f"β Data is stable. p-value ({p_value:.4f}) is above significance threshold ({DRIFT_THRESHOLD}). No retraining required.") | |
| if __name__ == "__main__": | |
| # Check if we should simulate a shock via command line arg | |
| simulate_shock = "--simulate-shock" in sys.argv | |
| main(simulate_shock=simulate_shock) | |