Saltar al contenido

lección 3

Limpiar y construir features justas

Limpiar y construir las features que miden a un jugador: el volumen bruto engaña, así que las estadísticas se trabajan por 90 minutos.

⏱ 55 min

## Martes, 10:00. El valor viene como texto

Ayer ya sabes agrupar y contar; hoy toca convertir datos sucios en features que un modelo pueda usar. La mancha estrella del caso está en el fichero de valores: el valor de mercado no viene como número, viene como texto escrito a mano por quien exportó el extracto. Verás «16,1 M EUR», «800 K», «7.600.000 €» y mezclas de todas ellas. Mientras el valor sea texto, no puedes hacer nada con él: ni sumarlo, ni promediarlo, ni —lo más importante— usarlo como target. Lo primero del día es convertir ese texto a un número en euros.

Parsear ese texto tiene su truco, y conviene verlo con calma porque un error aquí envenena el target y la feature más fuerte a la vez. Hay que quitar el símbolo de euro y los espacios, interpretar la «M» como multiplicar por un millón y la «K» por mil, y entender que la coma es el separador decimal (16,1 son dieciséis millones cien mil) mientras que el punto, cuando no hay letra, es el separador de miles (7.600.000 son siete millones seiscientos mil). Dos símbolos parecidos que significan cosas opuestas: ese es el tipo de detalle que, si se te escapa, hace que un jugador de 16 millones figure como 161.

1def parsear_valor(texto):
2 """'16,1 M EUR' -> 16100000.0 ; '800 K' -> 800000.0 ; '7.600.000 €' -> 7600000.0"""
3 s = str(texto).upper().replace('EUR', '').replace('€', '').strip().replace(' ', '')
4 if s.endswith('M'): # millones: la coma es decimal
5 return float(s[:-1].replace('.', '').replace(',', '.')) * 1_000_000
6 if s.endswith('K'): # miles
7 return float(s[:-1].replace('.', '').replace(',', '.')) * 1_000
8 return float(s.replace('.', '').replace(',', '.')) # el punto es separador de miles

La función que convierte el valor de texto a euros. Fíjate en el último caso: cuando no hay letra, el punto es separador de miles y se quita, no es un decimal. Interpretar mal ese punto es el bug más fácil de cometer y el que más distorsiona el target.

## La idea del día: por qué el volumen bruto engaña

Aquí está la idea que hace bueno o malo tu modelo, y es de fútbol, no de código. Imagina dos delanteros: uno jugó la temporada entera (3.000 minutos) y marcó 20 goles; otro entró de suplente casi siempre (600 minutos) y marcó 8. El primero tiene más goles en bruto, pero ¿es mejor delantero? El segundo marcó un gol cada 75 minutos; el primero, uno cada 150. Por ritmo, el suplente es más letal. Si le das al modelo los goles en bruto, premiará al que más jugó, que es una mezcla de lo bueno que es y de cuánto confió en él el entrenador. Lo que quieres medir es lo bueno que es por tiempo en el campo.

Por eso en el fútbol se trabaja con estadísticas por 90 minutos: se divide cada estadística de volumen entre el número de partidos completos equivalentes que jugó el jugador. goles_90 = goles / (minutos / 90). Así, los 20 goles en 3.000 minutos son 0,6 goles por 90, y los 8 en 600 minutos son 1,2 goles por 90: el suplente duplica al titular, que es la verdad futbolística que el volumen tapaba. Esto se llama normalizar por exposición, y es la ingeniería de features propia del dominio. No se te ocurra meter los goles en bruto: estarías midiendo cuánto jugó cada uno, no cómo de bueno es.

Por qué se normaliza por 90 minutos. En bruto, el titular parece mucho mejor porque jugó cinco veces más. Al dividir por el tiempo jugado, el suplente resulta ser más letal. El modelo debe aprender de la fila de abajo: cómo de bueno es por minuto, no cuánto jugó.

Los ratios por 90 se disparan con pocos minutos. Un jugador que jugó 30 minutos y marcó 1 gol tiene goles_90 = 3, un disparate que lo pondría por encima de cualquier estrella. Por eso hay que fijar un mínimo de minutos por debajo del cual el jugador no entra en el modelo (o se trata aparte). Dónde pones ese corte es una decisión tuya: justifícala. Un corte demasiado bajo mete ruido de muestras minúsculas; uno demasiado alto tira a jugadores jóvenes con pocos minutos pero reales.

## Los jugadores que aparecen dos veces

La otra limpieza importante son los duplicados. Ayer viste que hay más filas que jugadores: los que ficharon en enero aparecen dos veces, una con cada equipo, en la misma temporada. Si los dejas así, el modelo ve a esos jugadores partidos en dos mitades, cada una con menos minutos y stats, y los ratios por 90 salen deformados. Hay que consolidarlos a una fila por jugador-temporada: las estadísticas de volumen y los minutos se suman (jugó esos goles y esos minutos en total, repartidos entre dos equipos), y los ratios por 90 se recalculan después, sobre el total consolidado. Un ratio nunca se promedia entre las dos filas: se vuelve a calcular con los totales, o sale mal.

La posición hay que normalizarla también, pero por una razón distinta: no es que esté mal, es que está escrita de veinte formas para cuatro posiciones reales. Se pasa todo a minúsculas, se quitan los espacios y se mapea cada variante a su posición canónica (portero, defensa, centrocampista, delantero). Y ojo, la posición no es un número: es una categoría. No se escala como las stats; se codifica, convirtiéndola en columnas de ceros y unos (una por posición) con lo que se llama codificación one-hot. Un delantero y un central no se valoran por las mismas estadísticas, y el modelo necesita saber de qué posición es cada jugador sin inventarse que «delantero = 3» es más que «defensa = 1».

Consejo de senior: cuando construyas una feature nueva, compárala con la columna de la que sale antes de fiarte. Mira a tu mejor jugador conocido —aquí, Dani Olarra— y comprueba a mano que su goles_90 tiene sentido. Olarra marcó 34 goles en 2.950 minutos: eso son unos 1,04 goles por 90, cifra de delantero de élite. Si te saliera 0,1 o 15, sabrías al instante que has dividido mal. Validar una feature contra un caso que conoces cuesta diez segundos y caza bugs que el modelo nunca te señalaría.

Cuando tengas todo limpio, cruzas las dos tablas por jugador y temporada y montas la tabla de modelado: una fila por jugador-temporada, con las features (las por 90, la edad, los minutos, la posición codificada, el valor actual…) y el target (valor_siguiente). Esa tabla es lo que alimentará el modelo mañana. Y aquí va una advertencia que arma lo que viene: vas a meter muchas columnas, y la tentación natural es meterlas todas, «cuantas más mejor». Mételas. Mañana, con el modelo delante, veremos si todas aportan o si algunas solo estorban. Hoy, tu trabajo es que la tabla esté limpia y las features sean justas.

Regístrate para guardar tu progreso.