kronector / tests /test_feature_engineering.py
Prathamesh Bhamare
Initial commit: KRONECTOR MLOps & Multi-Agent AI system
2532605
Raw
History Blame Contribute Delete
8.12 kB
"""
Tests for Week 2 feature engineering.
Run: python -m pytest tests/test_feature_engineering.py -v
"""
import numpy as np
import pandas as pd
import pytest
from ml.feature_engineering import (
add_driver_form,
add_era_normalized_sector_times,
create_time_series_splits,
load_encoders,
prepare_model_data,
save_encoders,
validate_input_schema,
)
@pytest.fixture
def sample_race_df():
return pd.DataFrame(
{
"season": [2021, 2021, 2022, 2022, 2023, 2023, 2024, 2024],
"round": [1, 2, 1, 2, 1, 2, 1, 2],
"driver_id": ["VER", "HAM", "VER", "HAM", "VER", "HAM", "VER", "HAM"],
"driver_name": [
"Max Verstappen",
"Lewis Hamilton",
"Max Verstappen",
"Lewis Hamilton",
"Max Verstappen",
"Lewis Hamilton",
"Max Verstappen",
"Lewis Hamilton",
],
"team": [
"Red Bull Racing",
"Mercedes",
"Red Bull Racing",
"Mercedes",
"Red Bull Racing",
"Mercedes",
"Red Bull Racing",
None,
],
"grid_position": [1, 2, 1, 3, 1, 4, 2, 1],
"finish_position": [1, 2, 1, 3, 2, 1, 1, 2],
"circuit_id": [
"Monaco Grand Prix",
"Silverstone Grand Prix",
"Bahrain Grand Prix",
"Australian Grand Prix",
"Singapore Grand Prix",
"Italian Grand Prix",
"Canadian Grand Prix",
"Las Vegas Grand Prix",
],
"regulation_era": [
"hybrid_era",
"hybrid_era",
"ground_effect_era",
"ground_effect_era",
"ground_effect_era",
"ground_effect_era",
"ground_effect_era",
"ground_effect_era",
],
"track_type": [
"street",
"permanent",
"permanent",
"hybrid",
"street",
"permanent",
"hybrid",
"street",
],
"sector_1_time": [28.0, np.nan, 29.0, 30.0, 28.5, 29.2, 28.8, 29.1],
"sector_2_time": [35.0, 35.5, 36.0, np.nan, 35.8, 36.1, 35.6, 35.9],
"sector_3_time": [22.0, 22.2, np.nan, 22.8, 22.5, 22.7, 22.4, 22.6],
"avg_lap_time_practice": [93.0, np.nan, 94.0, 95.0, 93.5, 94.2, 93.8, 94.1],
"tire_compound": [0, 1, 0, 1, np.nan, 1, 2, 0],
"tire_age_laps": [14, 12, 18, 16, 20, np.nan, 15, 17],
"fresh_tire": [1, 1, 1, 0, 1, 1, np.nan, 0],
"pit_stop_count": [2, 1, 2, 2, 3, 2, 1, np.nan],
"team_pit_speed": [np.nan] * 8,
"weather_temp_track": [30.0, 28.0, 31.0, 27.0, 29.0, 32.0, 26.0, 25.0],
"weather_rainfall": [0, 0, 0, 1, 0, 0, 1, 0],
"championship_standing": [1, 2, 1, 3, 1, 2, 1, 2],
"driver_form_last3": [np.nan, np.nan, 1.0, 2.0, 1.0, 2.5, 1.3, 2.3],
"safety_car_probability": [0.2, 0.0, 0.1, 0.3, 0.4, 0.1, 0.2, 0.5],
"telemetry_available": [True] * 8,
"win_probability": [1, 0, 1, 0, 0, 1, 1, 0],
}
)
def test_validate_input_schema_rejects_missing_required_column(sample_race_df):
broken = sample_race_df.drop(columns=["driver_id"])
with pytest.raises(ValueError, match="driver_id"):
validate_input_schema(broken)
def test_prepare_model_data_imputes_missing_values(sample_race_df):
bundle, encoders = prepare_model_data(sample_race_df)
assert not bundle.X.isna().any().any()
assert len(bundle.X) == len(sample_race_df)
assert len(bundle.y) == len(sample_race_df)
assert set(encoders) == {"team", "track_type", "regulation_era"}
def test_prepare_model_data_excludes_leakage_columns(sample_race_df):
bundle, _ = prepare_model_data(sample_race_df)
assert "finish_position" not in bundle.feature_columns
assert "driver_id" not in bundle.feature_columns
assert "win_probability" not in bundle.feature_columns
assert "round" not in bundle.feature_columns
assert "round" in bundle.metadata.columns
def test_prepare_model_data_label_encodes_categoricals(sample_race_df):
bundle, encoders = prepare_model_data(sample_race_df)
assert "team" in bundle.feature_columns
assert "track_type" in bundle.feature_columns
assert "regulation_era" in bundle.feature_columns
assert "unknown" in encoders["team"].classes_
assert pd.api.types.is_integer_dtype(bundle.X["team"])
def test_era_normalized_sector_times_center_within_era(sample_race_df):
normalized = add_era_normalized_sector_times(sample_race_df)
for era, group in normalized.groupby("regulation_era"):
mean_value = group["sector_1_time_era_norm"].mean()
assert mean_value == pytest.approx(0.0, abs=1e-12), era
def test_time_series_splits_are_chronological(sample_race_df):
bundle, _ = prepare_model_data(sample_race_df)
splits = list(create_time_series_splits(bundle.X, n_splits=3))
assert len(splits) == 3
for train_idx, test_idx in splits:
assert train_idx.max() < test_idx.min()
def test_prepare_model_data_can_accept_fastf1_only_shape(sample_race_df):
fastf1_only = sample_race_df.drop(
columns=[
"regulation_era",
"track_type",
"championship_standing",
"driver_form_last3",
"safety_car_probability",
"win_probability",
]
)
bundle, _ = prepare_model_data(fastf1_only)
assert "win_probability" not in bundle.feature_columns
assert set(bundle.y.unique()) <= {0, 1}
assert "safety_car_probability" in bundle.feature_columns
def test_prepare_model_data_reuses_fitted_encoders_for_inference(sample_race_df):
train_df = sample_race_df.iloc[:6].copy()
inference_df = sample_race_df.iloc[6:].copy()
inference_df.loc[:, "team"] = "New Team"
_, encoders = prepare_model_data(train_df)
inference_bundle, reused_encoders = prepare_model_data(inference_df, encoders)
unknown_code = encoders["team"].transform(["unknown"])[0]
assert reused_encoders is encoders
assert (inference_bundle.X["team"] == unknown_code).all()
def test_add_driver_form_uses_only_prior_races():
df = pd.DataFrame(
{
"driver_id": ["VER", "VER", "VER", "VER"],
"season": [2023, 2023, 2023, 2023],
"round": [1, 2, 3, 4],
"finish_position": [1, 3, 5, 7],
}
)
result = add_driver_form(df)
assert pd.isna(result.loc[0, "driver_form_last3"])
assert result.loc[1, "driver_form_last3"] == pytest.approx(1.0)
assert result.loc[2, "driver_form_last3"] == pytest.approx(2.0)
assert result.loc[3, "driver_form_last3"] == pytest.approx(3.0)
def test_prepare_model_data_computes_driver_form_only_when_missing(sample_race_df):
missing_form = sample_race_df.copy()
missing_form["driver_form_last3"] = np.nan
bundle, _ = prepare_model_data(missing_form)
assert not bundle.X["driver_form_last3"].isna().any()
def test_championship_standing_uses_season_max_fill(sample_race_df):
df = sample_race_df.copy()
df.loc[(df["season"] == 2023) & (df["driver_id"] == "HAM"), "championship_standing"] = np.nan
bundle, _ = prepare_model_data(df)
row = bundle.metadata[
(bundle.metadata["season"] == 2023) & (bundle.metadata["driver_id"] == "HAM")
].index[0]
assert bundle.X.loc[row, "championship_standing"] == 1
def test_encoder_persistence_round_trip(sample_race_df, tmp_path):
_, encoders = prepare_model_data(sample_race_df)
path = tmp_path / "label_encoders.pkl"
save_encoders(encoders, str(path))
loaded = load_encoders(str(path))
assert set(loaded) == set(encoders)
assert list(loaded["team"].classes_) == list(encoders["team"].classes_)