6  Reporting, MLflow, and the CLI

Each experiment gets a Markdown model card. A shared standalone HTML comparison dashboard and cross-config comparison table collect the two main fitted models from both experiments. Reports and MLflow use raw run predictions, as in the prediction-scale convention.

This optional operations recipe refits its own candidates. It creates every report it logs; no workbook or fitted model from another chapter is required. For a brief handoff, generate the cards and comparison below and stop there.

import yaml

from azoic.mlops import log_run

portfolio = load_portfolio()
glm_config, gbm_config = make_experiment_configs(str(PORTFOLIO_PATH))
glm_run = run_experiment(glm_config)
gbm_run = run_experiment(gbm_config)
runs = [glm_run, gbm_run]
metrics = comparison_table(runs)
glm_card_path = ARTIFACT_DIR / "model_card_tariff_structured.md"
gbm_card_path = ARTIFACT_DIR / "model_card_raw_feature.md"
dashboard_path = ARTIFACT_DIR / "comparison_dashboard.html"

glm_card_path.write_text(model_card(glm_run), encoding="utf-8")
gbm_card_path.write_text(model_card(gbm_run), encoding="utf-8")
dashboard_path.write_text(comparison_dashboard(runs), encoding="utf-8")
4860582
Verify report artifacts and display raw metrics
assert metrics["model"].nunique() == 2
assert glm_card_path.stat().st_size > 0
assert gbm_card_path.stat().st_size > 0
assert dashboard_path.stat().st_size > 0
print("Held-out comparison — raw rates")
display(metrics[["config", "model", "gini_test", "op_ratio_test", "deviance_test"]])
Held-out comparison — raw rates
config model gini_test op_ratio_test deviance_test
0 fremtpl2-tariff-structured direct-tweedie-glm 0.309850 1.022067 75.338331
1 fremtpl2-raw-feature tweedie-lightgbm 0.341982 1.178104 74.721321

6.1 Log runs to local MLflow

MLflow uses a local SQLite backend. Its artifact root is also placed under the tutorial artifact directory. Each log_run call records the data fingerprint, experiment and model parameters, all finite metrics, and the generated artifacts. setup_mlflow_tracking is a tutorial helper in _shared.qmd; log_run is the Azoic API. Only reports written above are attached, so an older tariff workbook cannot be mistaken for an artifact of these new runs.

import mlflow

tracking_uri, experiment_name = setup_mlflow_tracking()
tracking_database = (ARTIFACT_DIR / "mlflow.db").resolve()

artifacts = [glm_card_path, gbm_card_path, dashboard_path]
mlflow_run_ids = [
    log_run(
        run,
        tracking_uri=tracking_uri,
        experiment_name=experiment_name,
        artifacts=artifacts,
    )
    for run in runs
]
Verify logged metrics and artifacts
client = mlflow.MlflowClient(tracking_uri=tracking_uri)
logged = [client.get_run(run_id) for run_id in mlflow_run_ids]

assert glm_run.data_fingerprint == gbm_run.data_fingerprint
assert all(
    entry.data.params["experiment.data_fingerprint"] == glm_run.data_fingerprint
    for entry in logged
)
assert all(
    f"{model_name}.gini_test" in entry.data.metrics
    for entry, run in zip(logged, runs, strict=True)
    for model_name in run.models
)
assert all(
    {Path(item.path).name for item in client.list_artifacts(run_id, path="artifacts")}
    == {Path(path).name for path in artifacts}
    for run_id in mlflow_run_ids
)

pd.DataFrame(
    {
        "run_id": mlflow_run_ids,
        "tracking_database": [str(tracking_database.relative_to(ROOT))] * len(mlflow_run_ids),
        "artifact_count": [len(artifacts)] * len(mlflow_run_ids),
    }
)
run_id tracking_database artifact_count
0 992f11c9696e42f7b99bc56cb633f2da examples/_artifacts/fremtpl2/mlflow.db 3
1 7d4754a5f2674fb1a813582a9bc01ab7 examples/_artifacts/fremtpl2/mlflow.db 3

To inspect the run later, start the UI manually; rendering never starts a server:

uv run mlflow ui \
  --backend-store-uri sqlite:///examples/_artifacts/fremtpl2/mlflow.db

6.2 Drive the same workflow from the CLI

Save the two experiment configurations as YAML, then run these commands from the repository root. They write into the tutorial artifacts directory. The render executes each command; only its result is shown below the command.

glm_yaml = ARTIFACT_DIR / "glm_config.yaml"
gbm_yaml = ARTIFACT_DIR / "gbm_config.yaml"
glm_yaml.write_text(
    yaml.safe_dump(glm_config.model_dump(exclude_none=True)), encoding="utf-8"
)
gbm_yaml.write_text(
    yaml.safe_dump(gbm_config.model_dump(exclude_none=True)), encoding="utf-8"
)
assert glm_yaml.stat().st_size > 0 and gbm_yaml.stat().st_size > 0
print("Wrote glm_config.yaml and gbm_config.yaml")
Wrote glm_config.yaml and gbm_config.yaml
uv run azoic profile --data examples/_artifacts/fremtpl2/portfolio_v3.parquet \
  --target claim_amount --exposure exposure --claim-count claim_count \
  --out examples/_artifacts/fremtpl2/cli_screening.csv
       column action                            reason
    policy_id    bin numeric high cardinality (667673)
     exposure    bin    numeric high cardinality (106)
  claim_count   keep                               NaN
 claim_amount    bin  numeric high cardinality (11943)
   driver_age    bin     numeric high cardinality (83)
  vehicle_age    bin     numeric high cardinality (78)
  bonus_malus    bin    numeric high cardinality (115)
      density    bin   numeric high cardinality (1607)
       region  group categorical high cardinality (22)
vehicle_brand  group categorical high cardinality (11)
    veh_power  group categorical high cardinality (12)
      veh_gas   keep                               NaN
         area   keep                               NaN
uv run azoic fit --config examples/_artifacts/fremtpl2/glm_config.yaml \
  --out examples/_artifacts/fremtpl2/cli_model_card.md --quiet
# Azoic model card -- fremtpl2-tariff-structured

## Experiment
- name: `fremtpl2-tariff-structured`
- data: `/home/runner/work/azoic/azoic/examples/_artifacts/fremtpl2/portfolio_v3.parquet`
- split: `random` (test_size=0.2, random_state=42)
- target: `claim_amount`  exposure: `exposure`
- data fingerprint: `5c765c822ca04a254727eee1fb48074e7cf38537a0e7070cb541ff91eed1c8b1`
- rows: 667673  (train 534138 / test 133535)
- features (9): `driver_age`, `vehicle_age`, `bonus_malus`, `density`, `region`, `vehicle_brand`, `veh_power`, `veh_gas`, `area`

### Model: `direct-tweedie-glm` (glm)

- params: 
uv run azoic compare examples/_artifacts/fremtpl2/glm_config.yaml \
  examples/_artifacts/fremtpl2/gbm_config.yaml \
  --out examples/_artifacts/fremtpl2/cli_comparison.csv
                    config              model kind  gini_train  gini_test  op_ratio_test  deviance_test  d2_test
fremtpl2-tariff-structured direct-tweedie-glm  glm    0.335709   0.309850       1.022067      75.338331 0.048954
      fremtpl2-raw-feature   tweedie-lightgbm  gbm    0.525721   0.341982       1.178104      74.721321 0.056743

Export the direct GLM with --no-recalibrate to preserve its raw prediction scale. The training multiplier shown in scoring remains external to this workbook.

Without this flag, CLI export recalibrates on the stored training rows only, keeping holdout claims out of the base. The earlier raw metrics still describe the unadjusted estimator; evaluate the adjusted workbook separately on the holdout.

uv run azoic export-tariff --config examples/_artifacts/fremtpl2/glm_config.yaml \
  --model direct-tweedie-glm --no-recalibrate \
  --out examples/_artifacts/fremtpl2/cli_glm_tariff.xlsx
Direct GLM workbook — raw rates
base_rate family link intercept_ recalibrated
0 48.05387 tweedie log 5.52521 False

6.2.1 Optional: export a distilled GBM

As in optional distillation, create a separate structured teacher by copying the raw GBM’s settings into the GLM’s preprocessing configuration. Save it before running the export command. The CLI fits this teacher and its student on training rows, reserving the test rows for fidelity evaluation.

teacher_config = glm_config.model_copy(update={
    "name": "fremtpl2-structured-teacher",
    "models": {"tweedie-lightgbm-tariff": gbm_config.models["tweedie-lightgbm"].model_copy(deep=True)},
})
teacher_yaml = ARTIFACT_DIR / "teacher_config.yaml"
teacher_yaml.write_text(
    yaml.safe_dump(teacher_config.model_dump(exclude_none=True)), encoding="utf-8"
)
978
uv run azoic export-tariff --config examples/_artifacts/fremtpl2/teacher_config.yaml \
  --model tweedie-lightgbm-tariff --distill --no-recalibrate \
  --out examples/_artifacts/fremtpl2/cli_distilled_tariff.xlsx
Distilled workbook — raw student rates
base_rate family link intercept_ recalibrated distilled_from teacher_objective teacher_student_deviance student_teacher_total_ratio
0 51.015377 tweedie log 5.225353 False RiskGBM tweedie 0.661562 1.003165

azoic tune is exercised end-to-end in the tuning chapter, using a bounded trial budget; its full search output is intentionally kept out of this page.

uv run azoic tune --help
                                                                                

 Usage: azoic tune [OPTIONS]                                                    

                                                                                

 Tune model hyperparameters with optuna, then write a model card of the best    

 fit.                                                                           

                                                                                

╭─ Options ────────────────────────────────────────────────────────────────────╮

│ *  --config                       <path>   ExperimentConfig YAML. [required] │

│    --trials                       <int>    Optuna trials per model;          │

│                                            overrides YAML (default: YAML or  │

│                                            20).                              │

│    --calibration-penalty          <float>  O/P penalty weight; overrides     │

│                                            YAML (default: YAML or 1.0).      │

│    --out                          <path>   Write the model card markdown     │

│                                            here.                             │

│    --quiet                -q               Do not print the card to stdout.  │

│    --help                                  Show this message and exit.       │

╰──────────────────────────────────────────────────────────────────────────────╯




6.3 Operational boundaries

This book estimates the capped historical pure-premium layer. A deployable price still needs decisions about large losses above the cap, claims development, inflation, expenses, commission, profit, taxes, and business constraints. The local MLflow store and workbook are review artifacts, not a deployed scoring service or policy-system integration. Keep preprocessing, model, prediction scale, and calibration factor together when handing work over.

Once these holdout results inform a change to features, tuning, factors, or calibration, they become development evidence. Evaluate the revised decision on fresh data or a reserved later period before claiming independent performance.