Pular para o conteúdo

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

ModeloO que você recebeO que você não recebe
Classificador binárioacurá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 limiaresum limiar recomendado
Classificador multiclasseum 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
Regressorerro absoluto médio, limitado por uma target_range que você declaraerro 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-predictive

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

Parte 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"}}
ParteFormatoQuem lê
inputo objeto que a sua função recebeo seu adaptador
expected.labeltrue ou false, a verdadeos avaliadores
metadata.planqualquer JSONapenas os segmentos
label retornadotrue ou false, a prediçãoos avaliadores
score retornadoum número em [0, 1], a probabilidade da classe positivaBrier, 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 run

Resumido:

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

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

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

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

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

Solução de problemas

SintomaCausa 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çãotarget_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 baseAs 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 erradoA 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