Guías
Tutorial: un clasificador y un regresor
Un recorrido ejecutable por tres modelos predictivos evaluados desde la línea de comandos: un clasificador binario de abandono, un enrutador de tickets de tres clases evaluado una clase cada vez y un regresor del tiempo de entrega puntuado por el error absoluto dentro de un rango declarado. Cada uno se ejecuta en local sin proveedor, sin clave y sin red, y cada uno termina con un cambio candidato medido frente a la línea base.
Esta página es la compañera práctica de Clasificadores y regresores, que explica cada campo; lea esa página como referencia y esta para hacerlo una vez de principio a fin. Términos como intervalo, estado de decisión y acción de publicación se definen en Conceptos.
Qué mide Oloproof para un modelo predictivo, y qué no
| Modelo | Qué obtiene | Qué no obtiene |
|---|---|---|
| Clasificador binario | exactitud, recall, precisión, puntuación de Brier, log loss, ROC-AUC y precisión media, además de recuentos de confusión, una tabla de calibración y un barrido de umbrales | un umbral recomendado |
| Clasificador multiclase | un recall y una precisión por clase, cada uno una métrica binaria cuya clase positiva es esa clase (una contra el resto) | una métrica de media macro o micro |
| Regresor | error absoluto medio, acotado por un target_range que usted declara | error cuadrático, R cuadrado, o cualquier error sin un rango declarado |
El modelo sigue siendo suyo. Oloproof llama a una función de Python que usted le indica, lee la predicción que devuelve y nunca ve las variables, los pesos ni los detalles internos.
Requisitos previos
- Python 3.11 o posterior y pip install oloproof en un entorno virtual, como en la guía rápida.
- Los archivos de ejemplo, que vienen con el paquete. Cópielos en un directorio nuevo para que los almacenes de las ejecuciones queden allí:
oloproof init --example predictive ~/oloproof-predictive
cd ~/oloproof-predictiveCada comando de abajo se ejecuta desde uno de sus tres subdirectorios. Cada ejecución escribe su evidencia en un directorio .oloproof/ junto al oloproof.yaml que leyó.
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 clasificador binario
El adaptador
binary/app.py contiene el modelo y el adaptador en un solo archivo. El modelo es el mismo modelo determinista de abandono que el del ejemplo churn_model (oloproof init --example churn_model); el adaptador es la función decorada a la que llama Oloproof:
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 evaluar su propio modelo, conserve la forma y sustituya churn_score por una llamada a él, por ejemplo model.predict_proba([features(account)])[0][1] para un modelo de scikit-learn que cargue una vez al importar. Cambie version cada vez que cambien el modelo o su punto de corte: la versión forma parte de la clave de caché, así que un modelo reentrenado con la misma versión reutilizaría las predicciones antiguas.
Formas de la entrada y la salida
Una línea de data/accounts.jsonl es un caso:
{"expected": {"label": false}, "id": "account_000", "input": {"recent_upgrade": true, "support_contacts": 0, "tenure_months": 0}, "metadata": {"plan": "enterprise"}}| Parte | Forma | Quién la lee |
|---|---|---|
| input | el objeto que recibe su función | su adaptador |
| expected.label | true o false, la verdad | los evaluadores |
| metadata.plan | cualquier JSON | solo los segmentos |
| label devuelto | true o false, la predicción | los evaluadores |
| score devuelto | un número en [0, 1], la probabilidad de la clase positiva | Brier, log loss, ranking, calibración, barrido |
Los evaluadores, y por qué estos
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- La exactitud, el recall y la precisión son tres tasas sobre tres conjuntos de filas distintos: todas las cuentas, las cuentas que abandonaron y las cuentas que el modelo señaló. Un modelo de abandono necesita las tres porque alrededor de un tercio de las cuentas abandonan, así que un modelo que predice que nadie abandona acierta un 68% y no encuentra a nadie.
- Brier y log loss puntúan la probabilidad que hay detrás de la etiqueta. Log loss necesita clip, porque de lo contrario un solo error confiado es infinito.
- predictive_ranking más dos entradas de metrics: dan ROC-AUC y la precisión media. Responden a lo bien que el modelo ordena las cuentas, con independencia de dónde esté el punto de corte.
- El bloque predictive: indica dónde están la etiqueta, la puntuación y la verdad, y produce los recuentos de confusión, la tabla de calibración y el barrido de umbrales junto a las métricas.
La política
binary/release.yaml pone un mínimo a cada tasa:
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 regla pasa cuando el límite inferior del intervalo supera el mínimo, falla cuando el límite superior está por debajo y, en otro caso, se lee INSUFFICIENT_EVIDENCE.
Ejecútelo
cd binary
oloproof runRecortado:
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 │Cómo leerlo:
- Gate: ALLOW (exit 0) significa que ninguna regla llegó a un estado en el que la política bloquea. No afirma que el modelo sea bueno más allá de los tres mínimos que usted escribió.
- Los recuentos excluded son los denominadores en acción: el recall se mide sobre las 64 cuentas que abandonaron, así que las 136 que no lo hicieron quedan excluidas de él, no contadas como fallos.
- pr_auc no tiene intervalo. Con 200 filas el motor retiene un límite que no puede respaldar; una regla sobre ella se leería INSUFFICIENT_EVIDENCE con interval_unavailable.
Debajo de las métricas, la misma salida imprime los recuentos de confusión, la tabla de calibración y el barrido de umbrales:
│ 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 │Los recuentos no son tasas y ninguna regla puede nombrarlos. Las filas de calibración dicen que las probabilidades del modelo están desviadas: en la banda de 0.2 a 0.3 afirma alrededor de un abandono de cada cuatro y ninguna de las 34 cuentas abandonó. El barrido se titula Thresholds (exploratory; recommends nothing): muestra lo que cada punto de corte habría medido y le deja la elección a usted, porque solo usted sabe lo que cuesta un abandono no detectado frente a una llamada de retención desperdiciada.
Inspeccionar los fallos
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 es el id de la primera línea de la salida de la ejecución. oloproof inspect RUN_ID --case account_037 muestra la entrada, el valor esperado, la salida y todos los juicios de un caso. Varios abandonos no detectados quedan justo por debajo del corte de 0.5 (0.37, 0.43), que es lo que la fila 0.4 del barrido ya sugería.
Una siguiente acción con sentido se desprende de lo que usted ve, no del gate: aquí los fallos se concentran por debajo del corte, así que el candidato prueba uno más bajo. Si hubieran sido fallos confiados (0.07), la siguiente acción serían las variables del modelo, y ningún punto de corte ayudaría.
Un cambio candidato, y la comparación
binary/candidate.yaml es oloproof.yaml con dos líneas cambiadas:
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 │Exactamente la fila 0.4 del barrido: el recall sube y la precisión baja. La salida 3 es INSUFFICIENT_EVIDENCE, no FAIL: los mínimos están dentro de los intervalos, así que 200 cuentas no pueden decir de qué lado está el candidato. Las puntuaciones no cambiaron, así que Brier, log loss y ROC-AUC son idénticos.
Ahora compare las dos ejecuciones caso a caso. binary/comparison.yaml contiene reglas de comparación:
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)Lea con atención la línea de la precisión. La precisión propia del candidato bajó del 82.5% al 62.6%, y sin embargo la diferencia emparejada es +0.0 sobre 63 pares. Una comparación empareja filas: una diferencia de precisión se mide solo sobre las cuentas que ambos modelos señalaron, y en esas 63 los dos acertaron. Las 28 cuentas adicionales que señaló el candidato, 23 de ellas falsas alarmas, quedan fuera de ese conjunto. Por eso comparison.yaml protege la exactitud y no la precisión para un cambio de punto de corte; Reglas de comparación describe los tipos de regla.
La decisión es la parte útil: se demuestra que el recall no es peor, y no se puede demostrar que la exactitud esté dentro de cinco puntos; la línea de consejo dice que más datos la empujarían hacia FAIL. Si cambiar nueve puntos de exactitud por ocho de recall merece la pena es una decisión de negocio que el gate ha hecho visible.
Parte 2: un clasificador multiclase, una clase cada vez
multiclass/app.py envía un ticket de soporte a una de tres colas, y tiene un defecto deliberado: cualquier ticket enviado desde la aplicación móvil 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"}}No hay una métrica multiclase que activar. Cada clase recibe su propio bloque binario: el recall de billing es el recall binario cuya clase positiva es 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 no calcula una media macro ni micro sobre estas. Si la necesita, es un número que usted deriva de los recuentos por clase, y ninguna regla puede hacer gate sobre él. La política pone un mínimo a las clases que importan, porque un enrutador puede ser exacto en conjunto y perder una cola:
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 salida 1 significa que al menos una regla es FAIL. Cada clase tiene su propio denominador: 78 tickets se clasificaron como technical, así que ese es el denominador de la precisión para technical. La tabla de segmentos apunta a la causa; los segmentos son exploratorios y nunca hacen gate, pero un segmento marcado es una pista que vale la pena leer:
│ 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
...El candidato (route_candidate, ejecutado con oloproof run --config candidate.yaml) elimina el atajo por canal. Con estos datos sintéticos enruta correctamente todos los tickets y el gate lo 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 aquí exactamente como en la Parte 1, una diferencia por cada métrica de clase.
Parte 3: un regresor, puntuado por el error absoluto
regression/app.py estima los días de entrega e ignora si el artículo está en stock:
@system(name="delivery-estimator", version="baseline")
def estimate(order):
return {"days": round(base_days(order), 1)}Un caso, con la verdad como número:
{"expected": {"days": 11}, "id": "order_000", "input": {"distance_km": 468, "express": false, "in_stock": false}, "metadata": {"in_stock": false}}El único evaluador de regresión es el error absoluto, y necesita el rango en el que se encuentra cada objetivo. Un error absoluto es una media acotada cuyo intervalo solo es válido dentro de ese rango, así que se declara, nunca se toma por defecto. Aquí las entregas tardan de 0 a 20 días:
evaluators:
- type: predictive_absolute_error
criterion: days_error
field: days
expected_field: days
target_range: [0, 20]
slices: [metadata.in_stock]La política es un presupuesto, una regla max:: pasa cuando el límite superior del intervalo está en él o por debajo.
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 incluso el límite inferior del intervalo supera el presupuesto de dos días. Un evaluador de puntuación no tiene aprobado ni fallo por caso, así que oloproof inspect RUN_ID --failures no lista ninguno; lea un caso en su lugar:
oloproof inspect RUN_ID --case order_000output: {
"days": 6.1
}
judgments:
days_error: score 4.9El segmento dice dónde mirar: los pedidos sin stock se desvían cinco días. El candidato (estimate_candidate) añade cinco días para un artículo sin 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 │Resolución de problemas
| Síntoma | Causa y solución |
|---|---|
| Configuration error: evaluator 'recall' counts 'churned' as the positive class, and no case's 'label' is 'churned' | positive: nombra un valor que ningún caso tiene. Use el valor exactamente como aparece bajo expected, incluida la diferencia entre true y "true". |
| release rule ... refers to unknown metric 'false_positives' | Los recuentos de confusión no son métricas. Haga gate sobre el recall o la precisión. |
| Un evaluador de regresión se rechaza antes de la ejecución | Falta target_range o está vacío. Declare el rango que los objetivos pueden tomar realmente; un rango más amplio da un intervalo más amplio. |
| Una regla de ranking se lee INSUFFICIENT_EVIDENCE (interval_unavailable) | La suite es demasiado pequeña para el intervalo de esa estadística, normalmente pr_auc. Haga gate sobre roc_auc, o añada casos. |
| El candidato muestra las cifras de la línea base | Las dos ejecuciones comparten una version, así que se reutilizaron las predicciones en caché. Dé a cada cambio su propia versión. |
| Un caso está missing en lugar de equivocado | Su función lanzó una excepción, o el campo que lee el evaluador falta o no es un número. oloproof inspect RUN_ID --case ID muestra el error. |
Limitaciones
- Ningún umbral recomendado. El barrido informa de lo que cada punto de corte declarado habría medido.
- Ninguna métrica de media macro o micro para multiclase, y ningún soporte multietiqueta más allá de un bloque por etiqueta.
- La regresión es solo error absoluto dentro de un target_range declarado: sin error cuadrático, R cuadrado ni error no acotado.
- La calibración se muestra como una tabla, sin gate y sin corrección.
- Una diferencia de precisión emparejada cubre solo las filas que ambos modelos señalaron, como muestra la Parte 1.
- Los ejemplos son deterministas y sintéticos. Sus decisiones muestran la mecánica, no cómo se comporta un modelo real con datos reales.
Adónde ir después
- Clasificadores y regresores es la referencia campo por campo.
- Comparar un candidato con una línea base y Reglas de comparación cubren el flujo de comparación.
- Segmentos cubre las bandas confidence: y el soporte de los segmentos.
- Gates en CI enumera los códigos de salida.