Skip to content

Architecture

Pipeline

flowchart LR
    raw[(train.csv)] --> data["data<br/>load, validate,<br/>split, fingerprint"]
    data --> features["features<br/>engineered columns<br/>(optional)"]
    features --> preprocess["preprocess<br/>impute, encode, scale"]
    preprocess --> model["models<br/>linear / ridge /<br/>lasso / gbm"]
    model --> evaluate["evaluate<br/>metrics"]
    model --> bundle[("model bundle<br/>model.joblib<br/>metadata.json")]
    bundle --> cli["cli<br/>amesml predict"]
    bundle --> api["api<br/>FastAPI service"]
    config["config<br/>TrainConfig"] -.-> model

The features, preprocess and estimator steps form a single scikit-learn Pipeline, so a saved model applies exactly the same transformations to new data as it learned in training. Everything statistical (medians, category levels, scaling, the classification threshold) is fitted on the training rows only.

Modules

Module Responsibility
config TrainConfig: every training option, validated, loadable from YAML or JSON
data Ames schema, CSV conventions, validation, checksummed download, splitting, fingerprints
features Pure functions deriving total area, ages, bathrooms and porch area
preprocess Column typing and the ColumnTransformer (imputation, one-hot encoding, scaling)
models Estimators, training, the model bundle on disk, input alignment and prediction
evaluate Regression and classification metrics as typed dataclasses
plots Figures, drawn with matplotlib's object-oriented API
cli The amesml command (Typer)
api The REST service (FastAPI)
errors The exception hierarchy

Dependencies point one way: cli and api use models, which uses preprocess, features, data and evaluate. Only cli writes to the terminal; library code logs through logging.getLogger("amesml").

The preprocessing transformer

flowchart TB
    input[raw columns] --> num["numeric<br/>median imputation (+ scaling)"]
    input --> zero["numeric_zero_fill<br/>MasVnrArea, GarageYrBlt -> 0 (+ scaling)"]
    input --> cat["categorical<br/>missing -> 'Missing', one-hot"]
    input --> codes["categorical_codes<br/>MSSubClass, MoSold: mode, one-hot"]
    num & zero & cat & codes --> matrix[design matrix]

Scaling is applied only where it matters (the penalised and logistic models). The matrix is sparse for the linear models - scikit-learn's default, and the reason the defaults reproduce the notebook's figures - and dense for gradient boosting, which requires it.

A saved model

models/regression-gbm/
├── model.joblib     fitted pipeline, with task_, input_features_, feature_defaults_, ...
├── metadata.json    config, metrics, features, defaults, data fingerprint,
│                    library versions, SHA-256 of model.joblib
└── metrics.json     held-out metrics

load_model refuses a model.joblib whose checksum no longer matches its metadata. The API reads metadata.json for /model/info.

Serving

create_app() loads every model once at startup, keyed by task, and fails to start if any is missing or invalid, so a misconfigured container crashes immediately instead of failing on its first request. Requests are validated by Pydantic, aligned to the model's columns by records_to_frame, and scored by the same predict function the CLI uses.

Quality gates

Every push runs ruff, the ruff formatter, strict mypy, the test suite with coverage on Linux, macOS and Windows for Python 3.9, 3.11, 3.12 and 3.13, a distribution build with twine check, a Docker build with an HTTP smoke test, a strict documentation build, and CodeQL.