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èle | Ce que vous obtenez | Ce que vous n'obtenez pas |
|---|---|---|
| Classifieur binaire | exactitude, 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 seuils | un seuil recommandé |
| Classifieur multiclasse | un 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égresseur | l'erreur absolue moyenne, bornée par une target_range que vous déclarez | l'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-predictiveChaque 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.jsonlPartie 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"}}| Partie | Forme | Qui la lit |
|---|---|---|
| input | l'objet que reçoit votre fonction | votre adaptateur |
| expected.label | true ou false, la vérité | les évaluateurs |
| metadata.plan | n'importe quel JSON | les segments uniquement |
| label renvoyé | true ou false, la prédiction | les évaluateurs |
| score renvoyé | un nombre dans [0, 1], la probabilité de la classe positive | Brier, 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 runExtrait :
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 523 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_candidateoloproof run --config candidate.yamlGate: 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.yamlComparison 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 runGate: 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 228 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 runGate: 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_000output: {
"days": 6.1
}
judgments:
days_error: score 4.9Le 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.yamlGate: 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ôme | Cause 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écution | target_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érence | Les 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 faux | Votre 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
- Classifieurs et régresseurs est la référence champ par champ.
- Comparer un candidat à une référence et Règles de comparaison couvrent le flux de comparaison.
- Segments couvre les tranches confidence: et le support des segments.
- Porte de CI liste les codes de sortie.