feat: type the pandas boundary and enforce 100% public-API docstrings #95
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI | |
| on: | |
| push: | |
| pull_request: | |
| workflow_dispatch: | |
| jobs: | |
| quality: | |
| name: Lint, format, and type checks on Python ${{ matrix.python-version }} | |
| runs-on: ubuntu-latest | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| python-version: ["3.10", "3.11", "3.12"] | |
| steps: | |
| - name: Check out repository | |
| uses: actions/checkout@v4 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: ${{ matrix.python-version }} | |
| cache: "pip" | |
| - name: Install quality tools | |
| run: | | |
| python -m pip install --upgrade pip | |
| pip install -r requirements-dev.txt | |
| - name: Ruff (lint) | |
| run: ruff check underwriting app tests | |
| - name: Black (format check) | |
| run: black --check underwriting app tests | |
| - name: Mypy (type check) | |
| run: mypy | |
| - name: Interrogate (docstring coverage) | |
| run: interrogate underwriting | |
| test-and-smoke: | |
| name: Test and smoke workflow on Python ${{ matrix.python-version }} | |
| runs-on: ubuntu-latest | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| python-version: ["3.10", "3.11", "3.12"] | |
| steps: | |
| - name: Check out repository | |
| uses: actions/checkout@v4 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: ${{ matrix.python-version }} | |
| cache: "pip" | |
| - name: Install dependencies | |
| run: | | |
| python -m pip install --upgrade pip | |
| pip install -r requirements.txt | |
| pip install 'coverage[toml]==7.14.3' | |
| - name: Check source files compile | |
| run: | | |
| python -m compileall underwriting app tests | |
| - name: Run unit tests with coverage | |
| run: | | |
| coverage run -m unittest discover -s tests -v | |
| coverage report | |
| - name: Run pipeline smoke workflow | |
| run: | | |
| python -m underwriting.pipeline \ | |
| --input data/raw/loanapproval.csv \ | |
| --out-dir ci_outputs \ | |
| --figures-dir ci_figures \ | |
| --target-coverage 0.70 | |
| - name: Verify expected outputs | |
| run: | | |
| test -f ci_outputs/metrics_overall.json | |
| test -f ci_outputs/baseline_metrics.json | |
| test -f ci_outputs/evaluation_summary.json | |
| test -f ci_outputs/policy_variants.json | |
| test -f ci_outputs/slice_report.csv | |
| test -f ci_outputs/slice_report.json | |
| test -f ci_outputs/slice_summary.json | |
| test -f ci_outputs/coverage_curve.csv | |
| test -f ci_outputs/abstention_policy.json | |
| test -f ci_outputs/test_predictions.csv | |
| test -f ci_outputs/model.joblib | |
| test -f ci_outputs/policy_card.md | |
| test -f ci_outputs/data_quality.json | |
| test -f ci_figures/confusion_matrix.png | |
| test -f ci_figures/reliability_diagram.png | |
| test -f ci_figures/probability_histograms.png | |
| test -f ci_figures/precision_recall_curve.png | |
| test -f ci_figures/coverage_vs_performance.png | |
| test -f ci_figures/slice_review_rates.png | |
| test -f ci_figures/slice_error_rates.png | |
| - name: Validate smoke output contracts | |
| run: | | |
| python - <<'PY' | |
| import json | |
| from pathlib import Path | |
| import pandas as pd | |
| metrics = json.loads(Path("ci_outputs/metrics_overall.json").read_text()) | |
| policy = json.loads(Path("ci_outputs/abstention_policy.json").read_text()) | |
| baselines = json.loads(Path("ci_outputs/baseline_metrics.json").read_text()) | |
| variants = json.loads(Path("ci_outputs/policy_variants.json").read_text()) | |
| summary = json.loads(Path("ci_outputs/evaluation_summary.json").read_text()) | |
| slice_summary = json.loads(Path("ci_outputs/slice_summary.json").read_text()) | |
| predictions = pd.read_csv("ci_outputs/test_predictions.csv") | |
| coverage = pd.read_csv("ci_outputs/coverage_curve.csv") | |
| slice_report = pd.read_csv("ci_outputs/slice_report.csv") | |
| required_metrics = {"accuracy", "f1", "brier", "roc_auc", "average_precision", "ece"} | |
| missing_metrics = required_metrics - set(metrics) | |
| if missing_metrics: | |
| raise AssertionError(f"Missing metrics: {missing_metrics}") | |
| if not 0 <= metrics["brier"] <= 1: | |
| raise AssertionError("Brier score should be between 0 and 1") | |
| if not 0 <= metrics["ece"] <= 1: | |
| raise AssertionError("ECE should be between 0 and 1") | |
| if not 0 <= metrics["average_precision"] <= 1: | |
| raise AssertionError("Average precision should be between 0 and 1") | |
| required_policy = { | |
| "recommended_threshold", | |
| "expected_coverage", | |
| "expected_accuracy_auto", | |
| "expected_f1_auto", | |
| } | |
| missing_policy = required_policy - set(policy) | |
| if missing_policy: | |
| raise AssertionError(f"Missing policy keys: {missing_policy}") | |
| required_prediction_cols = {"p_approve", "p_reject", "confidence", "auto_decide"} | |
| missing_cols = required_prediction_cols - set(predictions.columns) | |
| if missing_cols: | |
| raise AssertionError(f"Missing prediction columns: {missing_cols}") | |
| if not predictions["p_approve"].between(0, 1).all(): | |
| raise AssertionError("p_approve must be between 0 and 1") | |
| if not coverage["coverage"].between(0, 1).all(): | |
| raise AssertionError("Coverage values must be between 0 and 1") | |
| required_baselines = {"majority_class", "empirical_prior", "stratified_random"} | |
| if required_baselines - set(baselines): | |
| raise AssertionError("Missing baseline metrics") | |
| required_variants = {"target_coverage", "quality_first", "high_coverage", "balanced", "conservative_review"} | |
| if required_variants - set(variants): | |
| raise AssertionError("Missing policy variants") | |
| for key in ["model_metrics", "baseline_metrics", "recommended_policy", "policy_variants", "slice_summary", "slice_artifacts"]: | |
| if key not in summary: | |
| raise AssertionError(f"Missing evaluation summary key: {key}") | |
| required_slice_cols = {"slice_feature", "slice_value", "n", "review_rate", "error_rate", "ece"} | |
| missing_slice_cols = required_slice_cols - set(slice_report.columns) | |
| if missing_slice_cols: | |
| raise AssertionError(f"Missing slice report columns: {missing_slice_cols}") | |
| if slice_report.empty: | |
| raise AssertionError("Slice report should not be empty") | |
| if slice_summary.get("n_slices", 0) <= 0: | |
| raise AssertionError("Slice summary should report at least one slice") | |
| print("Smoke output contracts OK") | |
| PY | |
| - name: Upload generated artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: underwriting-decision-safety-artifacts-python-${{ matrix.python-version }} | |
| path: | | |
| ci_outputs/ | |
| ci_figures/ |