Aller au contenu

Guides

Tutoriel : un classifieur et un régresseur

Un parcours exécutable pour trois modèles prédictifs évalués en ligne de commande : un classifieur binaire d'attrition, un routeur de tickets à trois classes évalué une classe à la fois, et un régresseur de délai de livraison noté par l'erreur absolue dans une plage déclarée. Chacun s'exécute en local sans fournisseur, sans clé et sans réseau, et chacun se termine par une modification candidate mesurée par rapport à la référence.

Cette page est le compagnon pratique de Classifieurs et régresseurs, qui explique chaque champ ; lisez cette page-là pour la référence et celle-ci pour tout faire une fois de bout en bout. Les termes tels qu'intervalle, état de décision et action de publication sont définis dans Concepts.

Ce qu'Oloproof mesure pour un modèle prédictif, et ce qu'il ne mesure pas

ModèleCe que vous obtenezCe que vous n'obtenez pas
Classifieur binaireexactitude, rappel, précision, score de Brier, perte logarithmique, ROC-AUC et précision moyenne, plus les comptes de confusion, une table de calibration et un balayage de seuilsun seuil recommandé
Classifieur multiclasseun rappel et une précision par classe, chacun étant une métrique binaire dont la classe positive est cette classe (une contre les autres)une métrique de moyenne macro ou micro
Régresseurl'erreur absolue moyenne, bornée par une target_range que vous déclarezl'erreur quadratique, le R carré, ou toute erreur sans plage déclarée

Le modèle lui-même reste le vôtre. Oloproof appelle une fonction Python que vous lui indiquez, lit la prédiction qu'elle renvoie, et ne voit jamais les variables, les poids ni les internes.

Prérequis

  • Python 3.11 ou ultérieur et pip install oloproof dans un environnement virtuel, comme dans le démarrage rapide.
  • Les fichiers d'exemple, livrés avec le paquet. Copiez-les dans un nouveau répertoire pour que les magasins des exécutions y soient créés :
oloproof init --example predictive ~/oloproof-predictive
cd ~/oloproof-predictive

Chaque commande ci-dessous s'exécute depuis l'un de ses trois sous-répertoires. Chaque exécution écrit ses preuves dans un répertoire .oloproof/ à côté du oloproof.yaml qu'elle a lu.

predictive/
  binary/        app.py  oloproof.yaml  candidate.yaml  release.yaml  comparison.yaml  data/accounts.jsonl
  multiclass/    app.py  oloproof.yaml  candidate.yaml  release.yaml  data/tickets.jsonl
  regression/    app.py  oloproof.yaml  candidate.yaml  release.yaml  data/orders.jsonl

Partie 1 : un classifieur binaire

L'adaptateur

binary/app.py contient le modèle et l'adaptateur dans un seul fichier. Le modèle est le même modèle d'attrition déterministe que l'exemple churn_model (oloproof init --example churn_model) ; l'adaptateur est la fonction décorée qu'Oloproof appelle :

from oloproof import system


@system(name="churn-model", version="baseline")
def run(account):
    score = churn_score(account)
    return {"label": score >= 0.5, "score": round(score, 4)}


@system(name="churn-model", version="candidate-cutoff-0.4")
def run_candidate(account):
    score = churn_score(account)
    return {"label": score >= 0.4, "score": round(score, 4)}

Pour évaluer votre propre modèle, gardez la forme et remplacez churn_score par un appel à celui-ci, par exemple model.predict_proba([features(account)])[0][1] pour un modèle scikit-learn que vous chargez une fois à l'import. Changez version chaque fois que le modèle ou son seuil change : la version fait partie de la clé de cache, donc un modèle réentraîné sous la même version réutiliserait les anciennes prédictions.

Formes d'entrée et de sortie

Une ligne de data/accounts.jsonl est un cas :

{"expected": {"label": false}, "id": "account_000", "input": {"recent_upgrade": true, "support_contacts": 0, "tenure_months": 0}, "metadata": {"plan": "enterprise"}}
PartieFormeQui la lit
inputl'objet que reçoit votre fonctionvotre adaptateur
expected.labeltrue ou false, la véritéles évaluateurs
metadata.plann'importe quel JSONles segments uniquement
label renvoyétrue ou false, la prédictionles évaluateurs
score renvoyéun nombre dans [0, 1], la probabilité de la classe positiveBrier, perte logarithmique, classement, calibration, balayage

Les évaluateurs, et pourquoi ceux-ci

binary/oloproof.yaml :

version: 1
project: churn-tutorial
dataset: data/accounts.jsonl
system:
  name: churn-model
  version: baseline
  callable: app:run
evaluators:
  - {type: predictive_correct, criterion: accuracy}
  - {type: predictive_recall, criterion: recall}
  - {type: predictive_precision, criterion: precision}
  - {type: predictive_brier, criterion: brier}
  - {type: predictive_log_loss, criterion: log_loss, clip: 0.02}
  - {type: predictive_ranking, criterion: rank}
metrics:
  - {id: roc_auc, type: ranking, criterion: rank, statistic: roc_auc}
  - {id: pr_auc, type: ranking, criterion: rank, statistic: average_precision}
predictive:
  label_field: label
  score_field: score
  expected_field: label
  positive: true
  calibration_bins: 10
  thresholds: [0.3, 0.4, 0.5, 0.6, 0.7]
slices: [metadata.plan, "confidence:0.5"]
min_slice_support: 20
  • L'exactitude, le rappel et la précision sont trois taux sur trois ensembles de lignes différents : tous les comptes, les comptes partis, et les comptes que le modèle a signalés. Un modèle d'attrition a besoin des trois, car environ un tiers des comptes partent, si bien qu'un modèle qui prédit que personne ne part est exact à 68 % et ne trouve personne.
  • Brier et la perte logarithmique notent la probabilité derrière l'étiquette. La perte logarithmique a besoin de clip, car une seule erreur confiante serait sinon infinie.
  • predictive_ranking plus deux entrées metrics: donnent la ROC-AUC et la précision moyenne. Elles mesurent la qualité avec laquelle le modèle ordonne les comptes, indépendamment de l'emplacement du seuil.
  • Le bloc predictive: indique où se trouvent l'étiquette, le score et la vérité, et produit les comptes de confusion, la table de calibration et le balayage de seuils à côté des métriques.

La politique

binary/release.yaml impose un plancher à chaque taux :

version: 1
confidence_level: 0.95
block_on: [FAIL, INSUFFICIENT_EVIDENCE, MANUAL_REVIEW]
rules:
  - {id: accuracy-floor, metric: accuracy, min: 0.75}
  - {id: recall-floor, metric: recall, min: 0.60}
  - {id: precision-floor, metric: precision, min: 0.60}

Une règle passe quand la borne inférieure de l'intervalle franchit le plancher, échoue quand la borne supérieure est en dessous, et vaut sinon INSUFFICIENT_EVIDENCE.

L'exécuter

cd binary
oloproof run

Extrait :

Run run_... [DECIDED/COMPLETE]
Gate: ALLOW (exit 0)
│ accuracy-floor  │ accuracy  │ PASS  │ lower_bound_meets_minimum │
│ recall-floor    │ recall    │ PASS  │ lower_bound_meets_minimum │
│ precision-floor │ precision │ PASS  │ lower_bound_meets_minimum │

│ accuracy  │ 88.5%    │ [83.2%, 92.6%]  │ 177 / 200 observed · 0 missing · 0 excluded                                │
│ recall    │ 81.2%    │ [69.5%, 90.0%]  │ 52 / 64 observed · 0 missing · 136 excluded                                │
│ precision │ 82.5%    │ [70.9%, 91.0%]  │ 52 / 63 observed · 0 missing · 137 excluded                                │
│ brier     │ 0.120    │ [0.094, 0.154]  │ mean of 200 observed · 0 missing · 0 excluded                              │
│ log_loss  │ 0.389    │ [0.323, 0.499]  │ mean of 200 observed · 0 missing · 0 excluded                              │
│ roc_auc   │ 92.3%    │ [69.3%, 100.0%] │ roc_auc over 64 positive · 136 negative · 0 missing · 0 excluded           │
│ pr_auc    │ 86.5%    │                 │ average_precision over 64 positive · 136 negative · 0 missing · 0 excluded │

Comment le lire :

  • Gate: ALLOW (exit 0) signifie qu'aucune règle n'a atteint un état sur lequel la politique bloque. Ce n'est pas une affirmation que le modèle est bon au-delà des trois planchers que vous avez écrits.
  • Les comptes excluded sont les dénominateurs à l'œuvre : le rappel est mesuré sur les 64 comptes partis, donc les 136 autres en sont exclus, et non comptés comme des échecs.
  • pr_auc n'a pas d'intervalle. Avec 200 lignes, le moteur retient une borne qu'il ne peut pas justifier ; une règle sur cette métrique vaudrait INSUFFICIENT_EVIDENCE avec interval_unavailable.

Sous les métriques, la même sortie affiche les comptes de confusion, la table de calibration et le balayage de seuils :

│ actually positive │ 52                 │ 12                 │
│ actually negative │ 11                 │ 125                │

│ 0.2-0.3 │ 26.7%   │ 0.0%     │ 34 rows │
│ 0.5-0.6 │ 53.4%   │ 81.0%    │ 21 rows │

│ 0.3     │ 57.4%     │ 96.9%  │ 62/108 predicted positive · 62/64 actual positive │
│ 0.4     │ 62.6%     │ 89.1%  │ 57/91 predicted positive · 57/64 actual positive  │
│ 0.5     │ 82.5%     │ 81.2%  │ 52/63 predicted positive · 52/64 actual positive  │
│ 0.6     │ 83.3%     │ 54.7%  │ 35/42 predicted positive · 35/64 actual positive  │
│ 0.7     │ 100.0%    │ 37.5%  │ 24/24 predicted positive · 24/64 actual positive  │

Les comptes ne sont pas des taux et aucune règle ne peut les nommer. Les lignes de calibration indiquent que les probabilités du modèle sont décalées : dans la tranche de 0.2 à 0.3, il annonce environ un départ sur quatre et aucun des 34 comptes n'est parti. Le balayage s'intitule Thresholds (exploratory; recommends nothing) : il montre ce que chaque seuil aurait mesuré et vous laisse le choix, car vous seul savez ce que coûte un départ manqué face à un appel de fidélisation inutile.

Inspecter les échecs

oloproof inspect RUN_ID --failures --limit 5
23 of 200 cases failed, errored or did not finish

account_032
  output: {"label": false, "score": 0.07}
  accuracy: failed
  recall: failed

account_037
  output: {"label": false, "score": 0.37}
  accuracy: failed
  recall: failed
...

RUN_ID est l'identifiant sur la première ligne de la sortie de l'exécution. oloproof inspect RUN_ID --case account_037 montre l'entrée, la valeur attendue, la sortie et chaque jugement d'un cas. Plusieurs départs manqués se situent juste sous le seuil de 0.5 (0.37, 0.43), ce que la ligne 0.4 du balayage suggérait déjà.

Une action suivante pertinente découle de ce que vous voyez, pas de la porte : ici, les manqués se regroupent sous le seuil, donc le candidat en essaie un plus bas. S'il s'était agi de manqués confiants (0.07), l'action suivante porterait sur les variables du modèle, et aucun seuil n'aiderait.

Une modification candidate, et la comparaison

binary/candidate.yaml est oloproof.yaml avec deux lignes modifiées :

system:
  name: churn-model
  version: candidate-cutoff-0.4
  callable: app:run_candidate
oloproof run --config candidate.yaml
Gate: BLOCK (exit 3)
│ accuracy-floor  │ accuracy  │ INSUFFICIENT_EVIDENCE │ interval_overlaps_threshold │
│ recall-floor    │ recall    │ PASS                  │ lower_bound_meets_minimum   │
│ precision-floor │ precision │ INSUFFICIENT_EVIDENCE │ interval_overlaps_threshold │

│ accuracy  │ 79.5%    │ [73.2%, 84.9%]  │ 159 / 200 observed · 0 missing · 0 excluded                                │
│ recall    │ 89.1%    │ [78.7%, 95.5%]  │ 57 / 64 observed · 0 missing · 136 excluded                                │
│ precision │ 62.6%    │ [51.8%, 72.6%]  │ 57 / 91 observed · 0 missing · 109 excluded                                │

Exactement la ligne 0.4 du balayage : le rappel monte, la précision baisse. La sortie 3 est INSUFFICIENT_EVIDENCE, pas FAIL : les planchers sont à l'intérieur des intervalles, donc 200 comptes ne peuvent pas dire de quel côté se trouve le candidat. Les scores n'ont pas changé, donc Brier, la perte logarithmique et la ROC-AUC sont identiques.

Comparez maintenant les deux exécutions cas par cas. binary/comparison.yaml contient des règles de comparaison :

version: 1
confidence_level: 0.95
block_on: [FAIL, INSUFFICIENT_EVIDENCE, MANUAL_REVIEW]
rules:
  - {id: recall-no-worse, kind: non_inferiority, metric: recall, margin: 0.05}
  - {id: accuracy-no-worse, kind: non_inferiority, metric: accuracy, margin: 0.05}
oloproof compare CANDIDATE_RUN_ID BASELINE_RUN_ID --policy comparison.yaml
Comparison sha256:... of run_... against run_... · 200 paired cases
accuracy: -9.0 points [-17.9, -1.4] · 200 paired · 0 missing · 0 excluded
recall: +7.8 points [-2.8, +21.9] · 64 paired · 0 missing · 136 excluded
  excluded 136: not_a_positive_case
precision: +0.0 points [-8.3, +8.3] · 63 paired · 0 missing · 137 excluded
  excluded 137: not_predicted_positive
...
Decisions
  recall-no-worse  recall  non-inferiority, margin 5.0 points  PASS  lower_bound_above_margin
  accuracy-no-worse  accuracy  non-inferiority, margin 5.0 points  INSUFFICIENT_EVIDENCE  interval_overlaps_margin
    no sample size would make this PASS: the difference itself (-9.0 points) is outside the margin, so more cases would move it toward FAIL
Gate: BLOCK (exit 3)

Lisez attentivement la ligne de précision. La précision propre du candidat est tombée de 82.5 % à 62.6 %, et pourtant la différence appariée est +0.0 sur 63 paires. Une comparaison apparie des lignes : une différence de précision n'est mesurée que sur les comptes signalés par les deux modèles, et sur ces 63, les deux avaient raison. Les 28 comptes supplémentaires signalés par le candidat, dont 23 fausses alertes, sont hors de cet ensemble. C'est pourquoi comparison.yaml protège l'exactitude plutôt que la précision pour un changement de seuil ; Règles de comparaison présente les types de règles.

La décision est la partie utile : le rappel est démontré non inférieur, et l'exactitude ne peut pas être démontrée à moins de cinq points ; la ligne de conseil indique que davantage de données la pousseraient vers FAIL. Savoir si échanger neuf points d'exactitude contre huit de rappel en vaut la peine est une décision métier que la porte a rendue visible.

Partie 2 : un classifieur multiclasse, une classe à la fois

multiclass/app.py oriente un ticket de support vers l'une de trois files, et comporte un défaut délibéré : tout ticket envoyé depuis l'application mobile part vers technical.

@system(name="ticket-router", version="baseline")
def route(ticket):
    if ticket["channel"] == "app":
        return {"queue": "technical"}
    return {"queue": classify(str(ticket["subject"]))}

Un cas :

{"expected": {"queue": "billing"}, "id": "ticket_000", "input": {"channel": "email", "subject": "update the card on file"}, "metadata": {"channel": "email"}}

Il n'y a pas de métrique multiclasse à activer. Chaque classe a son propre bloc binaire : le rappel de billing est le rappel binaire dont la classe positive est billing. multiclass/oloproof.yaml :

evaluators:
  - {type: predictive_correct, criterion: accuracy, field: queue, expected_field: queue}
  - {type: predictive_recall, criterion: recall_billing, field: queue, expected_field: queue, positive: billing}
  - {type: predictive_precision, criterion: precision_billing, field: queue, expected_field: queue, positive: billing}
  - {type: predictive_recall, criterion: recall_technical, field: queue, expected_field: queue, positive: technical}
  - {type: predictive_precision, criterion: precision_technical, field: queue, expected_field: queue, positive: technical}
  - {type: predictive_recall, criterion: recall_account, field: queue, expected_field: queue, positive: account}
  - {type: predictive_precision, criterion: precision_account, field: queue, expected_field: queue, positive: account}
slices: [metadata.channel]

Oloproof ne calcule pas de moyenne macro ou micro sur celles-ci. Si vous en avez besoin, c'est un nombre que vous dérivez vous-même des comptes par classe, et aucune règle ne peut s'appuyer dessus. La politique impose un plancher aux classes qui comptent, car un routeur peut être exact dans l'ensemble et perdre une file :

rules:
  - {id: billing-recall-floor, metric: recall_billing, min: 0.80}
  - {id: account-recall-floor, metric: recall_account, min: 0.80}
  - {id: technical-precision-floor, metric: precision_technical, min: 0.80}
cd ../multiclass
oloproof run
Gate: BLOCK (exit 1)
│ billing-recall-floor      │ recall_billing      │ FAIL                  │ upper_bound_below_minimum   │
│ account-recall-floor      │ recall_account      │ INSUFFICIENT_EVIDENCE │ interval_overlaps_threshold │
│ technical-precision-floor │ precision_technical │ FAIL                  │ upper_bound_below_minimum   │

│ accuracy            │ 81.3%    │ [74.1%, 87.3%]  │ 122 / 150 observed · 0 missing · 0 excluded │
│ recall_billing      │ 66.0%    │ [51.2%, 78.8%]  │ 33 / 50 observed · 0 missing · 100 excluded │
│ precision_billing   │ 100.0%   │ [89.4%, 100.0%] │ 33 / 33 observed · 0 missing · 117 excluded │
│ recall_technical    │ 100.0%   │ [92.8%, 100.0%] │ 50 / 50 observed · 0 missing · 100 excluded │
│ precision_technical │ 64.1%    │ [52.4%, 74.7%]  │ 50 / 78 observed · 0 missing · 72 excluded  │
│ recall_account      │ 78.0%    │ [64.0%, 88.5%]  │ 39 / 50 observed · 0 missing · 100 excluded │
│ precision_account   │ 100.0%   │ [90.9%, 100.0%] │ 39 / 39 observed · 0 missing · 111 excluded │

La sortie 1 signifie qu'au moins une règle est FAIL. Chaque classe a son propre dénominateur : 78 tickets ont été classés technical, c'est donc le dénominateur de la précision pour technical. La table des segments désigne la cause ; les segments sont exploratoires et ne bloquent jamais, mais un segment marqué est une piste à lire :

│ metadata.channel=app   │ accuracy            │ 42.9%    │ [28.8%, 57.8%]  │ 21 / 49 observed · ... │ marked (p 0.0001)    │
oloproof inspect RUN_ID --failures --limit 2
28 of 150 cases failed, errored or did not finish

ticket_011
  output: {"queue": "technical"}
  accuracy: failed
  precision_technical: failed
  recall_account: failed
...

Le candidat (route_candidate, exécuté avec oloproof run --config candidate.yaml) supprime le raccourci par canal. Sur ces données synthétiques, il oriente correctement chaque ticket et la porte autorise :

Gate: ALLOW (exit 0)
│ billing-recall-floor      │ recall_billing      │ PASS  │ lower_bound_meets_minimum │
│ account-recall-floor      │ recall_account      │ PASS  │ lower_bound_meets_minimum │
│ technical-precision-floor │ precision_technical │ PASS  │ lower_bound_meets_minimum │

oloproof compare fonctionne ici exactement comme dans la partie 1, une différence par métrique de classe.

Partie 3 : un régresseur, noté par l'erreur absolue

regression/app.py estime les jours de livraison et ignore si l'article est en stock :

@system(name="delivery-estimator", version="baseline")
def estimate(order):
    return {"days": round(base_days(order), 1)}

Un cas, avec la vérité sous forme de nombre :

{"expected": {"days": 11}, "id": "order_000", "input": {"distance_km": 468, "express": false, "in_stock": false}, "metadata": {"in_stock": false}}

Le seul évaluateur de régression est l'erreur absolue, et il a besoin de la plage dans laquelle se trouve chaque cible. Une erreur absolue est une moyenne bornée dont l'intervalle ne tient que dans cette plage ; elle est donc déclarée, jamais prise par défaut. Les livraisons prennent ici de 0 à 20 jours :

evaluators:
  - type: predictive_absolute_error
    criterion: days_error
    field: days
    expected_field: days
    target_range: [0, 20]
slices: [metadata.in_stock]

La politique est un budget, une règle max: : elle passe quand la borne supérieure de l'intervalle est inférieure ou égale à celui-ci.

rules:
  - {id: error-budget, metric: days_error, max: 2.0}
cd ../regression
oloproof run
Gate: BLOCK (exit 1)
│ error-budget │ days_error │ FAIL  │ lower_bound_above_maximum │

│ days_error │ 2.66     │ [2.01, 3.57] │ mean of 120 observed · 0 missing · 0 excluded │

│ metadata.in_stock=false │ days_error │ 5.18     │ [4.60, 6.65] │ mean of 53 observed · ... │
│ metadata.in_stock=true  │ days_error │ 0.67     │ [0.51, 2.19] │ mean of 67 observed · ... │

FAIL, car même la borne inférieure de l'intervalle dépasse le budget de deux jours. Un évaluateur de score n'a ni réussite ni échec par cas, donc oloproof inspect RUN_ID --failures n'en liste aucun ; lisez plutôt un cas :

oloproof inspect RUN_ID --case order_000
output: {
  "days": 6.1
}
judgments:
  days_error: score 4.9

Le segment indique où chercher : les commandes en rupture de stock sont décalées de cinq jours. Le candidat (estimate_candidate) ajoute cinq jours pour un article en rupture de stock :

oloproof run --config candidate.yaml
Gate: ALLOW (exit 0)
│ error-budget │ days_error │ PASS  │ upper_bound_meets_maximum │
│ days_error │ 0.70     │ [0.56, 1.57] │ mean of 120 observed · 0 missing · 0 excluded │

Dépannage

SymptômeCause et correction
Configuration error: evaluator 'recall' counts 'churned' as the positive class, and no case's 'label' is 'churned'positive: nomme une valeur qu'aucun cas ne possède. Utilisez la valeur exactement telle qu'elle apparaît sous expected, y compris true par opposition à "true".
release rule ... refers to unknown metric 'false_positives'Les comptes de confusion ne sont pas des métriques. Appuyez la porte sur le rappel ou la précision.
Un évaluateur de régression est refusé avant l'exécutiontarget_range est absente ou vide. Déclarez la plage que les cibles peuvent réellement prendre ; une plage plus large donne un intervalle plus large.
Une règle de classement vaut INSUFFICIENT_EVIDENCE (interval_unavailable)La suite est trop petite pour l'intervalle de cette statistique, typiquement pr_auc. Appuyez la porte sur roc_auc, ou ajoutez des cas.
Le candidat rapporte les chiffres de la référenceLes deux exécutions partagent une version, donc les prédictions en cache ont été réutilisées. Donnez à chaque modification sa propre version.
Un cas est missing plutôt que fauxVotre fonction a levé une exception, ou le champ que lit l'évaluateur est absent ou n'est pas un nombre. oloproof inspect RUN_ID --case ID montre l'erreur.

Limites

  • Pas de seuil recommandé. Le balayage rapporte ce que chaque seuil déclaré aurait mesuré.
  • Pas de métrique de moyenne macro ou micro pour le multiclasse, et pas de prise en charge multi-étiquette au-delà d'un bloc par étiquette.
  • La régression se limite à l'erreur absolue dans une target_range déclarée : ni erreur quadratique, ni R carré, ni erreur non bornée.
  • La calibration est présentée sous forme de table, ni soumise à une porte ni corrigée.
  • Une différence de précision appariée ne couvre que les lignes signalées par les deux modèles, comme le montre la partie 1.
  • Les exemples sont déterministes et synthétiques. Leurs décisions montrent la mécanique, pas le comportement d'un vrai modèle sur de vraies données.

Pour aller plus loin