| """ |
| KRONECTOR - Drift Detection Pipeline |
| Monitors incoming F1 data for Concept and Data Drift using Evidently AI. |
| """ |
|
|
| import logging |
| import pandas as pd |
| from evidently.report import Report |
| from evidently.metric_preset import DataDriftPreset, TargetDriftPreset |
| from evidently import ColumnMapping |
|
|
| from ml.feature_engineering import prepare_model_data |
|
|
| logger = logging.getLogger(__name__) |
| logging.basicConfig(level=logging.INFO, format="%(asctime)s | %(name)s | %(levelname)s | %(message)s") |
|
|
| def detect_drift( |
| data_path: str = "data_output/fastf1_races.parquet", |
| current_season: int = 2026, |
| report_output_path: str = "data_output/drift_report.html" |
| ) -> bool: |
| """ |
| Detects drift between the reference dataset (historical) and the current dataset. |
| Returns True if significant drift is detected. |
| """ |
| try: |
| df = pd.read_parquet(data_path) |
| except FileNotFoundError: |
| logger.error(f"Data not found at {data_path}") |
| return False |
|
|
| |
| |
| logger.info("Preparing features for drift detection...") |
| bundle, _ = prepare_model_data(df) |
| |
| |
| eval_df = bundle.X.copy() |
| eval_df["season"] = bundle.metadata["season"] |
| eval_df["win_probability"] = bundle.y |
|
|
| |
| reference = eval_df[eval_df["season"] < current_season].copy() |
| current = eval_df[eval_df["season"] >= current_season].copy() |
| |
| |
| reference = reference.drop(columns=["season"]) |
| current = current.drop(columns=["season"]) |
|
|
| if current.empty: |
| logger.warning(f"No data available for current season {current_season}. Skipping drift detection.") |
| return False |
| |
| logger.info(f"Running drift detection: Reference ({len(reference)} rows) vs Current ({len(current)} rows)") |
|
|
| |
| target = "win_probability" |
| prediction = None |
| |
| column_mapping = ColumnMapping() |
| column_mapping.target = target |
| column_mapping.prediction = prediction |
| |
| |
| |
| numerical_features = [col for col in reference.columns if col != target] |
| column_mapping.numerical_features = numerical_features |
| column_mapping.categorical_features = [] |
|
|
| |
| report = Report(metrics=[ |
| DataDriftPreset(), |
| TargetDriftPreset(), |
| ]) |
|
|
| logger.info("Generating Evidently AI Drift Report...") |
| report.run(reference_data=reference, current_data=current, column_mapping=column_mapping) |
| |
| |
| import os |
| os.makedirs(os.path.dirname(report_output_path), exist_ok=True) |
| report.save_html(report_output_path) |
| logger.info(f"Drift report saved to {report_output_path}") |
|
|
| |
| result = report.as_dict() |
| |
| |
| data_drift = False |
| target_drift = False |
| |
| try: |
| |
| for metric in result["metrics"]: |
| if metric["metric"] == "DataDriftPreset": |
| if metric["result"]["dataset_drift"]: |
| data_drift = True |
| logger.warning("🚨 DATA DRIFT DETECTED!") |
| elif metric["metric"] == "TargetDriftPreset": |
| if metric["result"]["drift_detected"]: |
| target_drift = True |
| logger.warning("🚨 TARGET DRIFT DETECTED!") |
| except KeyError as e: |
| logger.warning(f"Could not parse drift results exactly: {e}") |
|
|
| return data_drift or target_drift |
|
|
| if __name__ == "__main__": |
| import argparse |
| parser = argparse.ArgumentParser(description="Run Drift Detection") |
| parser.add_argument("--data-path", default="data_output/fastf1_races.parquet") |
| parser.add_argument("--current-season", type=int, default=2026) |
| args = parser.parse_args() |
| |
| drift_detected = detect_drift( |
| data_path=args.data_path, |
| current_season=args.current_season |
| ) |
| print(f"\nFinal Result -> Drift Detected: {drift_detected}") |
|
|