Vai al contenuto

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

ModelloChe cosa ottieniChe cosa non ottieni
Classificatore binarioaccuratezza, recall, precisione, Brier score, log loss, ROC-AUC e average precision, più i conteggi di confusione, una tabella di calibrazione e una scansione delle soglieuna soglia raccomandata
Classificatore multiclasseuna 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
Regressoreerrore assoluto medio, limitato da un target_range che dichiarierrore 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-predictive

Ogni 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.jsonl

Parte 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"}}
ParteFormaChi la legge
inputl'oggetto che la tua funzione riceveil tuo adattatore
expected.labeltrue o false, la veritài valutatori
metadata.planqualsiasi JSONsolo le slice
label restituitotrue o false, la previsionei valutatori
score restituitoun numero in [0, 1], la probabilità della classe positivaBrier, 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 run

Ridotto:

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 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 è 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_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                                │

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.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)

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 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 │

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 2
28 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 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 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_000
output: {
  "days": 6.1
}
judgments:
  days_error: score 4.9

La 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.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 │

Risoluzione dei problemi

SintomoCausa 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'esecuzionetarget_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 baselineEntrambe 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 sbagliatoLa 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