Guide
Tutorial: un classificatore e un regressore
Un percorso eseguibile per tre modelli predittivi valutati dalla riga di comando: un classificatore binario di abbandono, un instradatore di ticket a tre classi valutato una classe alla volta, e un regressore dei tempi di consegna valutato con l'errore assoluto entro un intervallo di valori dichiarato. Ciascuno gira in locale senza provider, senza chiave e senza rete, e ciascuno termina con una modifica candidata misurata rispetto alla baseline.
Questa pagina è la compagna pratica di Classificatori e regressori, che spiega ogni campo; leggi quella pagina per il riferimento e questa per fare tutto una volta dall'inizio alla fine. Termini come intervallo, stato di decisione e azione di rilascio sono definiti in Concetti.
Che cosa misura Oloproof per un modello predittivo, e che cosa no
| Modello | Che cosa ottieni | Che cosa non ottieni |
|---|---|---|
| Classificatore binario | accuratezza, recall, precisione, Brier score, log loss, ROC-AUC e average precision, più i conteggi di confusione, una tabella di calibrazione e una scansione delle soglie | una soglia raccomandata |
| Classificatore multiclasse | una recall e una precisione per classe, ciascuna una metrica binaria la cui classe positiva è quella classe (una contro le altre) | una metrica di media macro o micro |
| Regressore | errore assoluto medio, limitato da un target_range che dichiari | errore quadratico, R quadro, o qualsiasi errore senza un intervallo di valori dichiarato |
Il modello resta tuo. Oloproof chiama una funzione Python a cui lo fai puntare, legge la previsione che restituisce, e non vede mai feature, pesi o dettagli interni.
Prerequisiti
- Python 3.11 o successivo e pip install oloproof in un ambiente virtuale, come nel quickstart.
- I file di esempio, distribuiti con il pacchetto. Copiali in una nuova directory così che gli archivi delle esecuzioni finiscano lì:
oloproof init --example predictive ~/oloproof-predictive
cd ~/oloproof-predictiveOgni comando qui sotto si esegue da una delle sue tre sottodirectory. Ogni esecuzione scrive la sua evidenza in una directory .oloproof/ accanto all'oloproof.yaml che ha letto.
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.jsonlParte 1: un classificatore binario
L'adattatore
binary/app.py contiene il modello e l'adattatore in un unico file. Il modello è lo stesso modello deterministico di abbandono dell'esempio churn_model (oloproof init --example churn_model); l'adattatore è la funzione decorata che Oloproof chiama:
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)}Per valutare il tuo modello, conserva la forma e sostituisci churn_score con una chiamata a esso, per esempio model.predict_proba([features(account)])[0][1] per un modello scikit-learn che carichi una volta all'import. Cambia version ogni volta che cambiano il modello o la sua soglia: la versione fa parte della chiave di cache, quindi un modello riaddestrato con la stessa versione riuserebbe le vecchie previsioni.
Forme di input e di output
Una riga di data/accounts.jsonl è un caso:
{"expected": {"label": false}, "id": "account_000", "input": {"recent_upgrade": true, "support_contacts": 0, "tenure_months": 0}, "metadata": {"plan": "enterprise"}}| Parte | Forma | Chi la legge |
|---|---|---|
| input | l'oggetto che la tua funzione riceve | il tuo adattatore |
| expected.label | true o false, la verità | i valutatori |
| metadata.plan | qualsiasi JSON | solo le slice |
| label restituito | true o false, la previsione | i valutatori |
| score restituito | un numero in [0, 1], la probabilità della classe positiva | Brier, log loss, ranking, calibrazione, scansione |
I valutatori, e perché questi
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- Accuratezza, recall e precisione sono tre tassi su tre insiemi diversi di righe: ogni account, gli account che hanno abbandonato, e gli account che il modello ha segnalato. Un modello di abbandono ha bisogno di tutti e tre perché circa un terzo degli account abbandona, quindi un modello che prevede che nessuno abbandoni è accurato al 68% e non trova nessuno.
- Brier e log loss valutano la probabilità dietro l'etichetta. La log loss richiede clip, perché altrimenti un solo errore sicuro di sé è infinito.
- predictive_ranking più due voci metrics: danno ROC-AUC e average precision. Rispondono a quanto bene il modello ordina gli account, indipendentemente da dove si trova la soglia.
- Il blocco predictive: dice dove si trovano etichetta, punteggio e verità, e produce i conteggi di confusione, la tabella di calibrazione e la scansione delle soglie accanto alle metriche.
La policy
binary/release.yaml mette una soglia minima su ciascun tasso:
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}Una regola passa quando il limite inferiore dell'intervallo supera la soglia minima, fallisce quando il limite superiore sta sotto di essa, e altrimenti riporta INSUFFICIENT_EVIDENCE.
Eseguila
cd binary
oloproof runRidotto:
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 │Come leggerlo:
- Gate: ALLOW (exit 0) significa che nessuna regola ha raggiunto uno stato su cui la policy blocca. Non è un'affermazione che il modello sia buono oltre le tre soglie che hai scritto.
- I conteggi excluded sono i denominatori all'opera: la recall è misurata sui 64 account che hanno abbandonato, quindi i 136 che non l'hanno fatto ne sono esclusi, non contati come fallimenti.
- pr_auc non ha intervallo. Con 200 righe il motore trattiene un limite che non può sostenere; una regola su di essa riporterebbe INSUFFICIENT_EVIDENCE con interval_unavailable.
Sotto le metriche, lo stesso output stampa i conteggi di confusione, la tabella di calibrazione e la scansione delle soglie:
│ 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 │I conteggi non sono tassi e nessuna regola può nominarli. Le righe di calibrazione dicono che le probabilità del modello sono sbagliate: nella fascia da 0.2 a 0.3 dichiara circa un abbandono su quattro e nessuno dei 34 ha abbandonato. La scansione è intitolata Thresholds (exploratory; recommends nothing): mostra che cosa avrebbe misurato ogni soglia e lascia a te la scelta, perché solo tu sai quanto costa un abbandono mancato rispetto a una chiamata di fidelizzazione sprecata.
Esamina i fallimenti
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 è l'id sulla prima riga dell'output dell'esecuzione. oloproof inspect RUN_ID --case account_037 mostra l'input di un caso, il valore atteso, l'output e ogni giudizio. Diversi abbandoni mancati stanno appena sotto la soglia di 0.5 (0.37, 0.43), cosa che la riga 0.4 della scansione già suggeriva.
Una prossima azione sensata deriva da ciò che vedi, non dal gate: qui gli errori si concentrano sotto la soglia, quindi il candidato ne prova una più bassa. Se fossero stati errori sicuri di sé (0.07), la prossima azione riguarderebbe le feature del modello, e nessuna soglia aiuterebbe.
Una modifica candidata, e il confronto
binary/candidate.yaml è oloproof.yaml con due righe cambiate:
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 │Esattamente la riga 0.4 della scansione: recall in salita, precisione in discesa. L'uscita 3 è INSUFFICIENT_EVIDENCE, non FAIL: le soglie minime sono dentro gli intervalli, quindi 200 account non possono dire da che parte stia il candidato. I punteggi non sono cambiati, quindi Brier, log loss e ROC-AUC sono identici.
Ora confronta le due esecuzioni caso per caso. binary/comparison.yaml contiene regole di confronto:
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)Leggi con attenzione la riga della precisione. La precisione del candidato è scesa da 82.5% a 62.6%, eppure la differenza appaiata è +0.0 su 63 coppie. Un confronto appaia le righe: una differenza di precisione è misurata solo sugli account che entrambi i modelli hanno segnalato, e su quei 63 entrambi avevano ragione. I 28 account in più segnalati dal candidato, 23 dei quali falsi allarmi, sono fuori da quell'insieme. Ecco perché comparison.yaml protegge l'accuratezza invece della precisione per un cambio di soglia; Regole di confronto riporta i tipi di regola.
La decisione è la parte utile: la recall risulta non peggiore, e l'accuratezza non può essere mostrata entro cinque punti; la riga di consiglio dice che più dati la spingerebbero verso FAIL. Se scambiare nove punti di accuratezza per otto di recall valga la pena è una decisione di business che il gate ha reso visibile.
Parte 2: un classificatore multiclasse, una classe alla volta
multiclass/app.py instrada un ticket di supporto verso una di tre code, e ha un difetto deliberato: qualsiasi ticket inviato dall'app mobile va a technical.
@system(name="ticket-router", version="baseline")
def route(ticket):
if ticket["channel"] == "app":
return {"queue": "technical"}
return {"queue": classify(str(ticket["subject"]))}Un caso:
{"expected": {"queue": "billing"}, "id": "ticket_000", "input": {"channel": "email", "subject": "update the card on file"}, "metadata": {"channel": "email"}}Non c'è alcuna metrica multiclasse da attivare. Ogni classe ha il proprio blocco binario: la recall per billing è la recall binaria la cui classe positiva è 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 non calcola una media macro o micro su queste. Se te ne serve una, è un numero che ricavi tu stesso dai conteggi per classe, e nessuna regola può fare da gate su di essa. La policy mette una soglia minima sulle classi che contano, perché un instradatore può essere accurato nel complesso e perdere una coda:
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 │L'uscita 1 significa che almeno una regola è FAIL. Ogni classe ha il proprio denominatore: 78 ticket sono stati assegnati a technical, quindi quello è il denominatore della precisione per technical. La tabella delle slice indica la causa; le slice sono esplorative e non fanno mai da gate, ma una slice segnalata è una pista che vale la pena leggere:
│ 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
...Il candidato (route_candidate, eseguito con oloproof run --config candidate.yaml) elimina la scorciatoia sul canale. Su questi dati sintetici instrada correttamente ogni ticket e il gate consente:
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 funziona qui esattamente come nella Parte 1, una differenza per ogni metrica di classe.
Parte 3: un regressore, valutato con l'errore assoluto
regression/app.py stima i giorni di consegna e ignora se l'articolo è disponibile in magazzino:
@system(name="delivery-estimator", version="baseline")
def estimate(order):
return {"days": round(base_days(order), 1)}Un caso, con la verità come numero:
{"expected": {"days": 11}, "id": "order_000", "input": {"distance_km": 468, "express": false, "in_stock": false}, "metadata": {"in_stock": false}}L'unico valutatore di regressione è l'errore assoluto, e richiede l'intervallo di valori in cui si trova ogni obiettivo. Un errore assoluto è una media limitata il cui intervallo vale solo entro quell'intervallo di valori, quindi viene dichiarato, mai assunto per impostazione predefinita. Qui le consegne richiedono da 0 a 20 giorni:
evaluators:
- type: predictive_absolute_error
criterion: days_error
field: days
expected_field: days
target_range: [0, 20]
slices: [metadata.in_stock]La policy è un budget, una regola max:: passa quando il limite superiore dell'intervallo è pari o inferiore a esso.
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 perché anche il limite inferiore dell'intervallo è sopra il budget di due giorni. Un valutatore a punteggio non ha successo o fallimento per caso, quindi oloproof inspect RUN_ID --failures non ne elenca nessuno; leggi invece un caso:
oloproof inspect RUN_ID --case order_000output: {
"days": 6.1
}
judgments:
days_error: score 4.9La slice dice dove guardare: gli ordini non disponibili in magazzino sbagliano di cinque giorni. Il candidato (estimate_candidate) aggiunge cinque giorni per un articolo non disponibile:
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 │Risoluzione dei problemi
| Sintomo | Causa e soluzione |
|---|---|
| Configuration error: evaluator 'recall' counts 'churned' as the positive class, and no case's 'label' is 'churned' | positive: nomina un valore che nessun caso ha. Usa il valore esattamente come compare sotto expected, distinguendo true da "true". |
| release rule ... refers to unknown metric 'false_positives' | I conteggi di confusione non sono metriche. Fai da gate su recall o precisione. |
| Un valutatore di regressione viene rifiutato prima dell'esecuzione | target_range manca o è vuoto. Dichiara l'intervallo di valori che gli obiettivi possono davvero assumere; un intervallo di valori più ampio dà un intervallo più ampio. |
| Una regola di ranking riporta INSUFFICIENT_EVIDENCE (interval_unavailable) | La suite è troppo piccola per l'intervallo di quella statistica, tipicamente pr_auc. Fai da gate su roc_auc, o aggiungi casi. |
| Il candidato riporta i numeri della baseline | Entrambe le esecuzioni condividono una version, quindi le previsioni in cache sono state riusate. Dai a ogni modifica la propria versione. |
| Un caso è missing invece che sbagliato | La tua funzione ha sollevato un'eccezione, o il campo che il valutatore legge è assente o non è un numero. oloproof inspect RUN_ID --case ID mostra l'errore. |
Limitazioni
- Nessuna soglia raccomandata. La scansione riporta che cosa avrebbe misurato ogni soglia dichiarata.
- Nessuna metrica di media macro o micro per il multiclasse, e nessun supporto multi-etichetta oltre un blocco per etichetta.
- La regressione è solo errore assoluto entro un target_range dichiarato: nessun errore quadratico, R quadro o errore non limitato.
- La calibrazione è mostrata come tabella, non fa da gate e non viene corretta.
- Una differenza di precisione appaiata copre solo le righe che entrambi i modelli hanno segnalato, come mostra la Parte 1.
- Gli esempi sono deterministici e sintetici. Le loro decisioni mostrano il meccanismo, non come si comporta un modello reale su dati reali.
Dove andare ora
- Classificatori e regressori è il riferimento campo per campo.
- Confrontare un candidato con una baseline e Regole di confronto coprono il flusso di confronto.
- Slice copre le fasce confidence: e il supporto delle slice.
- Gate in CI elenca i codici di uscita.