Saltar al contenido

lección 7

Que el comité clínico pueda decidir con esto

Montar las figuras y la tabla de decisión en lenguaje de pacientes, escribir el README, y defender el punto de equilibrio con la curva de coste.

⏱ 55 min

## Viernes, 10:00. Dos horas para el comité

A las doce presentas ante Beatriz y Tomás, y lo que lleves tiene que poder entenderse sin una sola palabra de jerga. Beatriz no quiere oír «recall 0,90»: quiere oír «de cada 100 lesiones graves, el sistema pesca 90 y se le escapan 10, frente a las 58 que se escapaban con el ajuste de fábrica». Tomás no quiere oír «795 falsos positivos»: quiere oír «generas unas 500 consultas de más al mes, y aquí tienes el margen que tengo para absorberlas». Tu trabajo del viernes es traducir toda la semana a ese lenguaje, con tres figuras que hablen solas y una tabla de decisión que cualquiera pueda leer.

Las tres figuras son las que has ido construyendo: la curva de coste frente al umbral con el mínimo marcado, que enseña por qué 0,12 y no 0,5; la curva de calibración antes y después, que enseña por qué las probabilidades ahora son de fiar; y la tabla de decisión en lenguaje de pacientes, que pone cara a cada número. Y al lado de cada regla, una referencia: lo que costaría derivar a todas. La regla manual del fundador nunca se midió; en el informe lo dices así, y propones medirla con los mismos datos antes de compararla.

## La ratio no es tuya: tres filas para el comité

Toda la semana has usado 20:1. Esa cifra no sale de los datos ni la elige el científico de datos: cuánto vale un grave perdido frente a una consulta de más es una decisión clínica, y la firma el comité de Beatriz. Lo que sí te toca es enseñarle qué cambia si mueve la ratio. Así que llevas tres filas: 10:1, 20:1 y 40:1. Para cada una, el umbral se elige en validación y se mide en la prueba. La cuenta teórica te dice hacia dónde se moverá: 1/11 ≈ 0,09 con 10:1, 1/21 ≈ 0,05 con 20:1 y 1/41 ≈ 0,02 con 40:1.

Lo que vas a ver es incómodo y útil. Con 10:1 el modelo ahorra frente a derivar a todas (1110 frente a 1256 en la prueba). Con 20:1 queda cerca de esa regla trivial, incluso algo por encima en la prueba. Con 40:1 el umbral baja a 0,03 y la regla es, en la práctica, derivar a todas. Es decir: cuanto más cara pone el comité la vida de un grave perdido, menos sitio queda para el modelo y más consultas tiene que absorber Tomás. Esa conversación es la que el comité necesita tener, y tu tabla es la que la hace posible.

La regla manual del fundador nunca se había medido: es lo primero que hay que medir para poder compararla. El salto del 42 % al 90 % de graves pescados no vino de un modelo mejor, sino del umbral por coste.
La misma tabla sirve a los tres: Beatriz mira cuántos graves se escapan, Tomás cuántas consultas de más, y el comité el coste total. La línea de abajo es la que no se puede esconder: con 20:1, derivar a todas cuesta menos que esta regla en la prueba.

## ¿Funciona igual para todos? Una tabla por grupo

Antes de llevar una regla de triaje a una clínica hay una pregunta que el comité te va a hacer, y con razón: ¿se le escapan más graves a algún grupo de pacientes? Una regla que va bien de media puede ir peor con las mujeres, o con los mayores. Los datos traen sexo y edad, así que puedes mirarlo: en la prueba, con el bosque y el umbral 0,12, cuentas por grupo los graves, los que se escapan y las consultas de más.

1prueba = X_te.copy()
2prueba['grave'] = y_te
3prueba['urgente'] = (rf.predict_proba(X_te)[:, 1] >= 0.12).astype(int)
4prueba['sexo'] = np.where(prueba['sexo_F'] == 1, 'F', 'M')
5prueba['edad_grupo'] = pd.cut(prueba['edad'], [0, 40, 60, 120], right=False,
6 labels=['<40', '40-59', '60+'])
7for col in ['sexo', 'edad_grupo']:
8 for grupo, g in prueba.groupby(col, observed=True):
9 graves = int(g['grave'].sum())
10 pescados = int(((g['urgente'] == 1) & (g['grave'] == 1)).sum())
11 de_mas = int(((g['urgente'] == 1) & (g['grave'] == 0)).sum())
12 print(f"{col}={grupo}: lesiones={len(g)} graves={graves} "
13 f"escapados={graves - pescados} recall={pescados / graves:.2f} "
14 f"consultas_de_mas={de_mas}")
15
16# sexo=F: lesiones=660 graves=133 escapados=14 recall=0.89 consultas_de_mas=329
17# sexo=M: lesiones=912 graves=183 escapados=18 recall=0.90 consultas_de_mas=466
18# edad_grupo=<40: lesiones=472 graves=56 escapados=6 recall=0.89 consultas_de_mas=193
19# edad_grupo=40-59: lesiones=733 graves=161 escapados=19 recall=0.88 consultas_de_mas=360
20# edad_grupo=60+: lesiones=367 graves=99 escapados=7 recall=0.93 consultas_de_mas=242

Rendimiento por grupo en la prueba (bosque, umbral 0,12, corte por paciente). Va a continuación del código de la tabla de decisión, con el mismo rf y la misma prueba.

En estos datos no se ve una diferencia clara: el recall va de 0,88 a 0,93 en todos los grupos. Pero léelo con prudencia. Los grupos son pequeños (56 graves en los menores de 40), así que un par de graves arriba o abajo mueve mucho la cifra. Y falta el grupo que más importa en dermatología: el tipo de piel (fototipo). Los datos no lo traen, y los modelos de piel se suelen entrenar con pocas pieles oscuras. Sin esa columna no puedes saber si la regla falla más con ellas, y el informe tiene que decirlo como límite.

## El README: el proyecto se convierte en portfolio

A lo largo de la semana has ido montando, pieza a pieza, un repositorio completo: cinco scripts numerados en src/, las consultas en sql/, los datos en datos/, y el requirements.txt con las dependencias. Lo único que falta es el README, y lo escribes tú. No es un trámite: es la pieza que explica qué hace el proyecto, cómo se ejecuta, y —lo más importante— cuál fue el hallazgo. Es lo que un reclutador o un compañero leen primero, y lo que demuestra que entiendes la diferencia entre un modelo con buen número y una decisión que se puede firmar.

1cribado-dermia/
2├── README.md ← lo escribes HOY (el hallazgo del caso)
3├── requirements.txt ← pandas, numpy, scikit-learn, matplotlib, duckdb
4├── datos/ ← lesiones.csv, desenlaces.csv, tabla_modelado.csv
5├── sql/ ← 01_desbalanceo.sql, 02_calibracion_color.sql
6└── src/
7 ├── 01_explorar.py ← carga, prevalencia, encuadre de costes
8 ├── 02_features.py ← limpieza y tabla de modelado
9 ├── 03_modelar.py ← dos clasificadores, AUC, matriz a 0,5
10 ├── 04_decidir.py ← umbral por coste, calibración, remate
11 └── 05_informe.py ← figuras y tabla de decisión

El repositorio completo que has montado sin darte cuenta, un ejercicio cada vez. Al juntar las soluciones en orden, cada script de src/ se ejecuta de principio a fin fuera del navegador. Es tu pieza de portfolio: un proyecto de cribado con decisión por coste, de verdad.

El arco completo del caso. El miércoles creías haber terminado con un buen AUC; el jueves descubriste que el umbral de fábrica dejaba escapar la mitad de los graves y que las probabilidades no eran de fiar. El informe del viernes cuenta ese viaje.

Consejo de senior: en el README, el hallazgo va antes que el método. No empieces por «entrené una logística y un bosque»; empieza por «el ajuste de fábrica dejaba escapar a más de la mitad de los graves, y el umbral por coste lo arregla». Quien lee un README quiere saber qué descubriste, no qué botones pulsaste. El cómo viene después, para quien quiera reproducirlo.

Regístrate para guardar tu progreso.