All checks were successful
ENGIN CI / Build, test and smoke (push) Successful in 2m58s
201 lines
6.5 KiB
Python
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()
|