hackathon-ENGIN/final_pipeline.py
Jakub Famulski 2 8e6dcb750d
All checks were successful
ENGIN CI / Build, test and smoke (push) Successful in 2m58s
prod: load versioned model artifact at runtime
2026-08-25 13:06:14 +02:00

201 lines
6.5 KiB
Python

"""Train the selected ENGIN models and create a validated submission.
Selected, leakage-safe architecture:
* label: signed/absolute leave-one-cylinder-out deviations, ratio deltas and
summary statistics with balanced Logistic Regression,
* severity: leave-one-cylinder-out deviation features with Extra Trees.
The script trains only on labeled ``val.csv``. The unlabeled archive is not
used because its benefit has not been established in grouped validation.
"""
from __future__ import annotations
import argparse
from pathlib import Path
import pandas as pd
from sklearn.linear_model import LogisticRegression
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from benchmark_grouped import validate_data
from engin.config import FAULT_LABELS, LABELS, NOT_APPLICABLE, SEVERITIES
from engin.features import SeverityFeatures
from engin.inference import (
KEY_COLUMNS,
OOD_OK_TO_UNKNOWN_RATIO_THRESHOLD,
OOD_OK_TO_UNKNOWN_THRESHOLD_MV,
DiagnosticModels,
apply_ood_override,
predict_test,
validate_inference_data,
validate_submission,
)
from severity_benchmark import (
CANDIDATE_BY_ID,
fit_candidate,
prepare_labeled_frame,
)
__all__ = [
"DiagnosticModels",
"OOD_OK_TO_UNKNOWN_RATIO_THRESHOLD",
"OOD_OK_TO_UNKNOWN_THRESHOLD_MV",
"apply_ood_override",
"predict_test",
"run_pipeline",
"train_models",
"validate_inference_data",
"validate_submission",
]
LABEL_MODEL_NAME = "deviation_logistic_c10"
LABEL_FEATURE_SET = "deviation"
SEVERITY_CANDIDATE_ID = "deviation_extra_trees_mf03"
def make_final_label_pipeline(random_state: int = 42) -> Pipeline:
"""Build the multi-seed winner used for final label predictions."""
return Pipeline(
[
("features", SeverityFeatures(LABEL_FEATURE_SET)),
("scale", StandardScaler()),
(
"model",
LogisticRegression(
C=10.0,
class_weight="balanced",
max_iter=5_000,
random_state=random_state,
),
),
]
)
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--val", type=Path, default=Path("val.csv"))
parser.add_argument("--test", type=Path, default=Path("test.csv"))
parser.add_argument(
"--sample-submit", type=Path, default=Path("sample_submit.csv")
)
parser.add_argument("--output", type=Path, default=Path("predictions.csv"))
parser.add_argument(
"--diagnostics",
type=Path,
default=Path("prediction_diagnostics.csv"),
help="Auxiliary model scores and anomaly data for the application.",
)
parser.add_argument("--random-state", type=int, default=42)
parser.add_argument("--n-jobs", type=int, default=-1)
return parser.parse_args()
def validate_sample_keys(sample: pd.DataFrame, test: pd.DataFrame) -> None:
missing = sorted(set(KEY_COLUMNS).difference(sample.columns))
if missing:
raise ValueError(f"Sample submission is missing key columns: {missing}")
if sample.duplicated(KEY_COLUMNS).any():
raise ValueError("Sample submission contains duplicate keys.")
sample_keys = set(map(tuple, sample[KEY_COLUMNS].to_numpy()))
test_keys = set(map(tuple, test[KEY_COLUMNS].to_numpy()))
if sample_keys != test_keys or len(sample) != len(test):
raise ValueError("sample_submit.csv keys do not match test.csv.")
def train_models(
val: pd.DataFrame,
random_state: int = 42,
n_jobs: int = -1,
) -> DiagnosticModels:
validate_data(val, n_splits=2)
y_label = val["label"].reset_index(drop=True)
y_severity = val["severity"].reset_index(drop=True)
label_pipeline = make_final_label_pipeline(random_state=random_state).fit(
val, y_label
)
fault_mask = y_label.isin(FAULT_LABELS).to_numpy()
observed_severities = set(y_severity.to_numpy()[fault_mask])
if observed_severities != set(SEVERITIES):
raise ValueError(
"Training faults must contain every allowed severity; "
f"observed={sorted(observed_severities)}"
)
candidate = CANDIDATE_BY_ID[SEVERITY_CANDIDATE_ID]
val_with_labels = prepare_labeled_frame(
val, y_label.to_numpy(dtype=object)
)
transformer, estimator = fit_candidate(
candidate=candidate,
X_train_full=val_with_labels,
train_fault_mask=fault_mask,
y_severity_fault=y_severity.to_numpy(dtype=object)[fault_mask],
y_fault_label=y_label.to_numpy(dtype=object)[fault_mask],
random_state=random_state + 1_002,
n_jobs=n_jobs,
)
return DiagnosticModels(
label_pipeline=label_pipeline,
severity_transformer=transformer,
severity_estimator=estimator,
)
def run_pipeline(
val: pd.DataFrame,
test: pd.DataFrame,
sample_submit: pd.DataFrame | None = None,
random_state: int = 42,
n_jobs: int = -1,
) -> tuple[pd.DataFrame, pd.DataFrame]:
if sample_submit is not None:
validate_sample_keys(sample_submit, test)
models = train_models(val, random_state=random_state, n_jobs=n_jobs)
submission, diagnostics = predict_test(models, test)
validate_submission(submission, test)
return submission, diagnostics
def main() -> None:
args = parse_args()
val = pd.read_csv(args.val).reset_index(drop=True)
test = pd.read_csv(args.test).reset_index(drop=True)
sample_submit = pd.read_csv(args.sample_submit).reset_index(drop=True)
submission, diagnostics = run_pipeline(
val=val,
test=test,
sample_submit=sample_submit,
random_state=args.random_state,
n_jobs=args.n_jobs,
)
args.output.parent.mkdir(parents=True, exist_ok=True)
args.diagnostics.parent.mkdir(parents=True, exist_ok=True)
submission.to_csv(args.output, index=False)
diagnostics.to_csv(args.diagnostics, index=False)
print(f"Saved validated submission: {args.output.resolve()}")
print(f"Saved application diagnostics: {args.diagnostics.resolve()}")
print(f"Rows: {len(submission)} | engines: {submission['engine_id'].nunique()}")
print("\nLabel distribution:")
print(submission["label"].value_counts().reindex(LABELS, fill_value=0).to_string())
print("\nSeverity distribution:")
print(
submission["severity"]
.value_counts()
.reindex([NOT_APPLICABLE, *SEVERITIES], fill_value=0)
.to_string()
)
if __name__ == "__main__":
main()