Guias
Tutorial: um classificador e um regressor
Um passo a passo executável para três modelos preditivos avaliados pela linha de comando: um classificador binário de churn, um roteador de tickets de três classes avaliado uma classe por vez e um regressor de prazo de entrega pontuado pelo erro absoluto dentro de uma faixa declarada. Cada um roda localmente sem provedor, sem chave e sem rede, e cada um termina com uma mudança candidata medida contra a linha de base.
Esta página é a companheira prática de Classificadores e regressores, que explica cada campo; leia aquela página como referência e esta para fazer tudo uma vez de ponta a ponta. Termos como intervalo, estado de decisão e ação de lançamento são definidos em Conceitos.
O que o Oloproof mede para um modelo preditivo, e o que não mede
| Modelo | O que você recebe | O que você não recebe |
|---|---|---|
| Classificador binário | acurácia, recall, precisão, score de Brier, log loss, ROC-AUC e precisão média, além de contagens de confusão, uma tabela de calibração e uma varredura de limiares | um limiar recomendado |
| Classificador multiclasse | um recall e uma precisão por classe, cada um uma métrica binária cuja classe positiva é aquela classe (uma contra as demais) | uma métrica de média macro ou micro |
| Regressor | erro absoluto médio, limitado por uma target_range que você declara | erro quadrático, R quadrado ou qualquer erro sem uma faixa declarada |
O modelo continua sendo seu. O Oloproof chama uma função Python que você indica, lê a predição que ela retorna e nunca vê features, pesos ou detalhes internos.
Pré-requisitos
- Python 3.11 ou posterior e pip install oloproof em um ambiente virtual, como no quickstart.
- Os arquivos de exemplo, que acompanham o pacote. Copie-os para um novo diretório para que os stores das execuções fiquem lá:
oloproof init --example predictive ~/oloproof-predictive
cd ~/oloproof-predictiveTodo comando abaixo roda a partir de um dos seus três subdiretórios. Cada execução grava sua evidência em um diretório .oloproof/ ao lado do oloproof.yaml que leu.
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: um classificador binário
O adaptador
binary/app.py contém o modelo e o adaptador em um só arquivo. O modelo é o mesmo modelo de churn determinístico do exemplo churn_model (oloproof init --example churn_model); o adaptador é a função decorada que o Oloproof chama:
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)}Para avaliar o seu próprio modelo, mantenha o formato e substitua churn_score por uma chamada a ele, por exemplo model.predict_proba([features(account)])[0][1] para um modelo scikit-learn que você carrega uma vez na importação. Mude version sempre que o modelo ou o seu ponto de corte mudar: a versão faz parte da chave de cache, então um modelo retreinado sob a mesma versão reutilizaria as predições antigas.
Formatos de entrada e saída
Uma linha de data/accounts.jsonl é um caso:
{"expected": {"label": false}, "id": "account_000", "input": {"recent_upgrade": true, "support_contacts": 0, "tenure_months": 0}, "metadata": {"plan": "enterprise"}}| Parte | Formato | Quem lê |
|---|---|---|
| input | o objeto que a sua função recebe | o seu adaptador |
| expected.label | true ou false, a verdade | os avaliadores |
| metadata.plan | qualquer JSON | apenas os segmentos |
| label retornado | true ou false, a predição | os avaliadores |
| score retornado | um número em [0, 1], a probabilidade da classe positiva | Brier, log loss, ranking, calibração, varredura |
Os avaliadores, e por que estes
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- Acurácia, recall e precisão são três taxas sobre três conjuntos diferentes de linhas: todas as contas, as contas que deram churn e as contas que o modelo sinalizou. Um modelo de churn precisa das três porque cerca de um terço das contas dá churn, então um modelo que prevê que ninguém dá churn tem 68% de acurácia e não encontra ninguém.
- Brier e log loss pontuam a probabilidade por trás do rótulo. Log loss precisa de clip, porque um erro confiante seria, de outra forma, infinito.
- predictive_ranking mais duas entradas em metrics: dão ROC-AUC e precisão média. Elas respondem quão bem o modelo ordena as contas, separadamente de onde fica o ponto de corte.
- O bloco predictive: diz onde ficam o rótulo, o score e a verdade, e produz as contagens de confusão, a tabela de calibração e a varredura de limiares ao lado das métricas.
A política
binary/release.yaml coloca um piso em cada taxa:
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}Uma regra passa quando o limite inferior do intervalo supera o piso, falha quando o limite superior está abaixo dele e, caso contrário, mostra INSUFFICIENT_EVIDENCE.
Execute
cd binary
oloproof runResumido:
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 │Como ler:
- Gate: ALLOW (exit 0) significa que nenhuma regra chegou a um estado em que a política bloqueia. Não é uma afirmação de que o modelo é bom além dos três pisos que você escreveu.
- As contagens excluded são os denominadores em ação: o recall é medido sobre as 64 contas que deram churn, então as 136 que não deram são excluídas dele, e não contadas como falhas.
- pr_auc não tem intervalo. Com 200 linhas o motor retém um limite que não consegue sustentar; uma regra sobre ela mostraria INSUFFICIENT_EVIDENCE com interval_unavailable.
Abaixo das métricas, a mesma saída imprime as contagens de confusão, a tabela de calibração e a varredura de limiares:
│ 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 │As contagens não são taxas e nenhuma regra pode nomeá-las. As linhas de calibração dizem que as probabilidades do modelo estão erradas: na faixa de 0.2 a 0.3 ele afirma cerca de um churner em cada quatro e nenhuma das 34 deu churn. A varredura tem o título Thresholds (exploratory; recommends nothing): ela mostra o que cada ponto de corte teria medido e deixa a escolha com você, porque só você sabe quanto custa um churner não detectado comparado a uma ligação de retenção desperdiçada.
Inspecione as falhas
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 é o id na primeira linha da saída da execução. oloproof inspect RUN_ID --case account_037 mostra a entrada de um caso, o valor esperado, a saída e todo julgamento. Vários churners não detectados ficam logo abaixo do ponto de corte de 0.5 (0.37, 0.43), que é o que a linha 0.4 da varredura já sugeria.
Uma próxima ação significativa decorre do que você vê, não do gate: aqui os erros se concentram abaixo do ponto de corte, então o candidato tenta um mais baixo. Se fossem erros confiantes (0.07), a próxima ação seriam as features do modelo, e nenhum ponto de corte ajudaria.
Uma mudança candidata, e a comparação
binary/candidate.yaml é o oloproof.yaml com duas linhas alteradas:
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 │Exatamente a linha 0.4 da varredura: recall para cima, precisão para baixo. O exit 3 é INSUFFICIENT_EVIDENCE, não FAIL: os pisos estão dentro dos intervalos, então 200 contas não conseguem dizer de que lado o candidato está. Os scores não mudaram, então Brier, log loss e ROC-AUC são idênticos.
Agora compare as duas execuções caso a caso. binary/comparison.yaml contém regras de comparação:
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)Leia a linha de precisão com atenção. A precisão do próprio candidato caiu de 82.5% para 62.6%, mas a diferença pareada é +0.0 sobre 63 pares. Uma comparação pareia linhas: uma diferença de precisão é medida apenas sobre as contas que ambos os modelos sinalizaram, e nessas 63 ambos acertaram. As 28 contas a mais que o candidato sinalizou, 23 delas alarmes falsos, ficam fora desse conjunto. É por isso que comparison.yaml protege a acurácia em vez da precisão para uma mudança de ponto de corte; Regras de comparação traz os tipos de regra.
A decisão é a parte útil: mostra-se que o recall não é pior, e não se consegue mostrar que a acurácia fica dentro de cinco pontos; a linha de orientação diz que mais dados a empurrariam para FAIL. Se trocar nove pontos de acurácia por oito de recall vale a pena é uma decisão de negócio que o gate tornou visível.
Parte 2: um classificador multiclasse, uma classe por vez
multiclass/app.py encaminha um ticket de suporte para uma de três filas e tem uma falha proposital: qualquer ticket enviado pelo app móvel vai para technical.
@system(name="ticket-router", version="baseline")
def route(ticket):
if ticket["channel"] == "app":
return {"queue": "technical"}
return {"queue": classify(str(ticket["subject"]))}Um caso:
{"expected": {"queue": "billing"}, "id": "ticket_000", "input": {"channel": "email", "subject": "update the card on file"}, "metadata": {"channel": "email"}}Não há uma métrica multiclasse para ativar. Cada classe recebe o seu próprio bloco binário: o recall de billing é o recall binário cuja 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]O Oloproof não calcula uma média macro ou micro sobre elas. Se você precisar de uma, é um número que você deriva por conta própria das contagens por classe, e nenhuma regra pode usá-lo como gate. A política coloca um piso nas classes que importam, porque um roteador pode ser preciso no geral e perder uma fila:
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 │O exit 1 significa que pelo menos uma regra é FAIL. Cada classe tem o seu próprio denominador: 78 tickets foram classificados como technical, então esse é o denominador da precisão de technical. A tabela de segmentos aponta a causa; segmentos são exploratórios e nunca funcionam como gate, mas um segmento marcado é uma pista que vale ler:
│ 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
...O candidato (route_candidate, executado com oloproof run --config candidate.yaml) remove o atalho por canal. Nestes dados sintéticos ele encaminha todo ticket corretamente e o gate permite:
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 funciona aqui exatamente como na Parte 1, com uma diferença por métrica de classe.
Parte 3: um regressor, pontuado pelo erro absoluto
regression/app.py estima os dias de entrega e ignora se o item está em estoque:
@system(name="delivery-estimator", version="baseline")
def estimate(order):
return {"days": round(base_days(order), 1)}Um caso, com a verdade como um número:
{"expected": {"days": 11}, "id": "order_000", "input": {"distance_km": 468, "express": false, "in_stock": false}, "metadata": {"in_stock": false}}O único avaliador de regressão é o erro absoluto, e ele precisa da faixa em que todo alvo está. Um erro absoluto é uma média limitada cujo intervalo só vale dentro dessa faixa, então ela é declarada, nunca assumida por padrão. As entregas aqui levam de 0 a 20 dias:
evaluators:
- type: predictive_absolute_error
criterion: days_error
field: days
expected_field: days
target_range: [0, 20]
slices: [metadata.in_stock]A política é um orçamento, uma regra max:: ela passa quando o limite superior do intervalo está nele ou abaixo dele.
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 porque até o limite inferior do intervalo está acima do orçamento de dois dias. Um avaliador de score não tem aprovação ou reprovação por caso, então oloproof inspect RUN_ID --failures não lista nenhum; leia um caso em vez disso:
oloproof inspect RUN_ID --case order_000output: {
"days": 6.1
}
judgments:
days_error: score 4.9O segmento diz onde olhar: pedidos sem estoque erram por cinco dias. O candidato (estimate_candidate) acrescenta cinco dias para um item sem estoque:
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 │Solução de problemas
| Sintoma | Causa e correção |
|---|---|
| Configuration error: evaluator 'recall' counts 'churned' as the positive class, and no case's 'label' is 'churned' | positive: nomeia um valor que nenhum caso tem. Use o valor exatamente como aparece em expected, incluindo true versus "true". |
| release rule ... refers to unknown metric 'false_positives' | Contagens de confusão não são métricas. Use recall ou precisão como gate. |
| Um avaliador de regressão é recusado antes da execução | target_range está ausente ou vazia. Declare a faixa que os alvos podem realmente assumir; uma faixa mais larga dá um intervalo mais largo. |
| Uma regra de ranking mostra INSUFFICIENT_EVIDENCE (interval_unavailable) | A suíte é pequena demais para o intervalo dessa estatística, tipicamente pr_auc. Use roc_auc como gate, ou adicione casos. |
| O candidato reporta os números da linha de base | As duas execuções compartilham uma version, então as predições em cache foram reutilizadas. Dê a cada mudança a sua própria versão. |
| Um caso está missing em vez de errado | A sua função lançou uma exceção, ou o campo que o avaliador lê está ausente ou não é um número. oloproof inspect RUN_ID --case ID mostra o erro. |
Limitações
- Nenhum limiar recomendado. A varredura reporta o que cada ponto de corte declarado teria medido.
- Nenhuma métrica de média macro ou micro para multiclasse, e nenhum suporte multirrótulo além de um bloco por rótulo.
- Regressão é apenas erro absoluto dentro de uma target_range declarada: sem erro quadrático, R quadrado ou erro ilimitado.
- A calibração é mostrada como uma tabela, não funciona como gate e não é corrigida.
- Uma diferença de precisão pareada cobre apenas as linhas que ambos os modelos sinalizaram, como mostra a Parte 1.
- Os exemplos são determinísticos e sintéticos. As suas decisões mostram a mecânica, não como um modelo real se comporta com dados reais.
Para onde ir em seguida
- Classificadores e regressores é a referência campo a campo.
- Comparando um candidato com uma linha de base e Regras de comparação cobrem o fluxo de comparação.
- Segmentos cobre as faixas confidence: e o suporte de segmentos.
- Gate no CI lista os códigos de saída.