Preparar datos: limpieza, variables y pipelines

Tema 10 · Fase 2, Aprender de ejemplos · Toda la ruta

En esta ficha · 5 secciones
  1. Antes de empezar
  2. Ejercicios
  3. Mini-proyecto: del CSV sucio a la predicción
  4. Errores comunes
  5. ¿Lo lograste?

Antes de empezar

Varios ejercicios usan lo que dejó la teoría: el bloque final, «El pipeline del tema 10» (agregar_variables, crear_pipeline_titanic, NUMERICAS, CATEGORICAS, titanic con las variables nuevas y la división X_ent, X_prueba, y_ent, y_prueba), y también pliegues, prep_numericas y prep_categoricas. Abre un cuaderno nuevo en Colab, pega y ejecuta en orden los bloques de la ficha Código y después trabaja debajo. Intenta cada ejercicio antes de abrir la respuesta.

Ejercicios

1. ¿Por qué falta?

Para cada caso, di si el dato falta al azar, por algo que sí vemos en otra columna, o por el propio valor que falta. Después piensa qué problema traería rellenarlo con la mediana.

  1. Una encuesta de ingresos en la que las familias con ingresos más altos dejan la pregunta en blanco más seguido.
  2. Un registro de notas en el que se mojó una hoja y se perdieron las notas de diez estudiantes, sin importar quiénes eran.
  3. Una base de pacientes donde a los que llegaron de noche no se les midió la presión, porque el aparato estaba en otra sala. La hora de llegada sí está anotada.
Respuesta
  1. Por el propio valor: los que faltan son justamente los ingresos altos. Rellenar con la mediana los convierte en ingresos normales y el promedio de la encuesta queda más bajo que el real. Ningún relleno lo arregla del todo; al menos conviene marcar con una columna aparte (add_indicator=True) que el dato faltaba, porque eso ya es información.
  2. Al azar: que falte no dice nada del estudiante. Rellenar con la mediana es razonable; solo hace la columna un poco más «picuda».
  3. Por algo que sí vemos: depende de la hora de llegada. Si los pacientes de la noche son distintos (por ejemplo, llegan más graves), rellenar con la mediana de todos los hace parecer más sanos. Una mejora es rellenar con la mediana de los pacientes de su mismo turno, siempre calculada solo con el entrenamiento.

2. ¿Media o mediana?

Los sueldos mensuales de seis personas de una oficina (datos inventados) son 3000, 3200, 3500, 3800, 4000 y 45 000 bolivianos. A una séptima persona le falta el sueldo. Calcula la media y la mediana sin computadora, decide con cuál rellenarías y comprueba con SimpleImputer.

Respuesta
  • Media: (3000 + 3200 + 3500 + 3800 + 4000 + 45 000) / 6 = 62 500 / 6 ≈ 10 417 Bs.
  • Mediana: con seis valores ordenados, el promedio de los dos del medio, (3500 + 3800) / 2 = 3650 Bs.

Rellenaría con la mediana. El sueldo de 45 000 (quizás el del jefe) arrastra la media hasta casi el triple de lo que gana cualquiera de los otros cinco: la media no describe a nadie de la oficina.

sueldos = pd.DataFrame(
    {"sueldo": [3000, 3200, 3500, 3800, 4000, 45000, np.nan]})
for estrategia in ["mean", "median"]:
    imp = SimpleImputer(strategy=estrategia).fit(sueldos)
    relleno = imp.transform(sueldos)[-1, 0]
    print(f"{estrategia:6}: rellena con {relleno:.0f}")
mean  : rellena con 10417
median: rellena con 3650

SimpleImputer ignora el faltante al calcular, como hiciste a mano.

3. ¿One-hot o número?

Di cómo convertirías cada columna en números y por qué:

  1. El departamento de Bolivia donde vive cada estudiante.
  2. La talla de una polera: S, M, L o XL.
  3. El color favorito.
  4. El curso: de 1.º a 6.º de secundaria.
Respuesta
  1. One-hot: los departamentos no tienen orden. Numerarlos de 1 a 9 diría, por ejemplo, que Pando está «más lejos» de La Paz que Oruro solo por el número que les tocó. Son 9 columnas nuevas.
  2. Número: S, M, L y XL tienen un orden claro (variable ordinal), así que 1, 2, 3 y 4 respetan algo real. One-hot también funcionaría, pero perdería ese orden.
  3. One-hot: nominal, sin orden.
  4. Número: es ordinal y además los pasos son parejos (de un curso al siguiente hay un año), así que 1 a 6 funciona muy bien.

4. Estandarizar y normalizar a mano

Para los valores 2, 4, 6, 8 y 30:

  1. Calcula la media y la desviación estándar (divide por 5, como StandardScaler), y estandariza.
  2. Normaliza al rango de 0 a 1.
  3. ¿Cómo quedan repartidos los cuatro primeros valores en cada caso? Compruébalo con scikit-learn.
Respuesta
  1. Media = 50 / 5 = 10. Las diferencias con la media son −8, −6, −4, −2 y 20; sus cuadrados suman 64 + 36 + 16 + 4 + 400 = 520, la varianza es 520 / 5 = 104 y la desviación √104 ≈ 10,2. Estandarizados: −0,78, −0,59, −0,39, −0,20 y 1,96.
  2. Mínimo 2 y máximo 30, así que se divide por 28: 0, 0,07, 0,14, 0,21 y 1.
  3. Con la normalización, los cuatro primeros quedan apretados entre 0 y 0,21: el 30, un atípico, se queda con casi todo el rango. Con la estandarización también quedan juntos, pero al menos centrados cerca de 0 y sin un techo fijo.
v = np.array([[2], [4], [6], [8], [30]])
print("Estandarizados:", StandardScaler().fit_transform(v).ravel().round(2))
print("Normalizados:  ", MinMaxScaler().fit_transform(v).ravel().round(2))
Estandarizados: [-0.78 -0.59 -0.39 -0.2   1.96]
Normalizados:   [0.   0.07 0.14 0.21 1.  ]

5. Encuentra la fuga: escalar primero

Una compañera escribió esto para medir los vecinos cercanos en el Titanic. ¿Dónde está la fuga? Corrígelo.

from sklearn.neighbors import KNeighborsClassifier

datos = titanic[["pclass", "age", "sibsp", "parch", "fare"]]
datos = datos.fillna(datos.median())
datos = (datos - datos.mean()) / datos.std()
D_ent, D_prueba, d_ent, d_prueba = train_test_split(
    datos, titanic["survived"], test_size=0.25,
    stratify=titanic["survived"], random_state=0)
knn = KNeighborsClassifier(n_neighbors=5).fit(D_ent, d_ent)
print(f"Exactitud: {knn.score(D_prueba, d_prueba):.3f}")
Exactitud: 0.704
Respuesta

La mediana con que rellena y la media y la desviación con que estandariza se calculan con todos los pasajeros, incluidos los de prueba, y recién después divide. La prueba influyó en la preparación. La corrección es dividir primero y meter la preparación en un pipeline, que la ajusta solo con el entrenamiento:

columnas_num = ["pclass", "age", "sibsp", "parch", "fare"]
knn_bien = Pipeline([
    ("imputar", SimpleImputer(strategy="median")),
    ("escalar", StandardScaler()),
    ("modelo", KNeighborsClassifier(n_neighbors=5))])
knn_bien.fit(X_ent[columnas_num], y_ent)
exactitud = knn_bien.score(X_prueba[columnas_num], y_prueba)
print(f"Exactitud: {exactitud:.3f}")
Exactitud: 0.709

Esta vez la diferencia es chica (y hasta a favor de la versión correcta), como viste en la teoría: con 891 pasajeros, una mediana y una media casi no cambian. Pero la primera versión no es confiable por cómo está hecha, no por el número que dio, y el mismo hábito con un paso que mira la respuesta infla el puntaje muchísimo.

6. Encuentra la fuga: una columna sospechosa

Otro estudiante quiso usar «todas las columnas que sirvan» y obtuvo un modelo casi perfecto. ¿Qué pasó?

cols_todas = ["pclass", "sex", "age", "fare", "embarked", "alive"]
T_ent, T_prueba, t_ent, t_prueba = train_test_split(
    titanic[cols_todas], titanic["survived"], test_size=0.25,
    stratify=titanic["survived"], random_state=0)
prep_todas = ColumnTransformer([
    ("num", prep_numericas, ["pclass", "age", "fare"]),
    ("cat", prep_categoricas, ["sex", "embarked", "alive"])])
sospechoso = Pipeline([("preparar", prep_todas),
                       ("modelo", LogisticRegression(max_iter=1000))])
sospechoso.fit(T_ent, t_ent)
print(f"Exactitud: {sospechoso.score(T_prueba, t_prueba):.3f}")
Exactitud: 1.000
Respuesta

La columna alive es la respuesta escrita con palabras: vale "yes" exactamente cuando survived vale 1. El modelo no aprendió nada del Titanic, solo a leer la respuesta en otra columna. Se comprueba en una línea:

print(pd.crosstab(titanic["alive"], titanic["survived"]))
survived    0    1
alive             
no        549    0
yes         0  342

Esta es la fuga por la respuesta (en inglés, target leakage): una columna que en la vida real no conocerías al momento de predecir, porque se anota después o es una copia de lo que quieres predecir. Pasa también con columnas menos obvias, como «días de internación» para predecir si un paciente se enfermará. Un resultado demasiado bueno para ser cierto casi siempre es una fuga. La corrección es sacar alive de las columnas.

7. Encuentra la fuga: elegir con la prueba

Este código busca el mejor número de vecinos. ¿Por qué el 0,816 que imprime al final no es una buena estimación de cómo le irá al modelo con pasajeros nuevos?

mejor_k, mejor_puntaje = None, 0
for k in [1, 3, 5, 9, 15, 25, 41]:
    m = crear_pipeline_titanic(KNeighborsClassifier(n_neighbors=k))
    puntaje = m.fit(X_ent, y_ent).score(X_prueba, y_prueba)
    if puntaje > mejor_puntaje:
        mejor_k, mejor_puntaje = k, puntaje
print(f"Mejor k = {mejor_k}, exactitud = {mejor_puntaje:.3f}")
Mejor k = 5, exactitud = 0.816
Respuesta

El pipeline está bien armado, pero el conjunto de prueba se usó para elegir k: se probaron siete modelos y se quedó el que mejor salió en la prueba. Parte de esa ventaja es suerte con esos 223 pasajeros, así que el 0,816 es optimista. Es la fuga del tema 7: la prueba se usa una sola vez, al final. La corrección es elegir k con validación cruzada sobre el entrenamiento y recién después mirar la prueba:

buscar_k = GridSearchCV(
    crear_pipeline_titanic(KNeighborsClassifier()),
    {"modelo__n_neighbors": [1, 3, 5, 9, 15, 25, 41]}, cv=pliegues)
buscar_k.fit(X_ent, y_ent)
print("k elegido con validación:", buscar_k.best_params_)
print(f"Validación: {buscar_k.best_score_:.3f}"
      f"   prueba: {buscar_k.score(X_prueba, y_prueba):.3f}")
k elegido con validación: {'modelo__n_neighbors': 25}
Validación: 0.814   prueba: 0.794

La validación elige otro k, y la prueba, usada una sola vez, da un número honesto. Que el número honesto sea más bajo no es un error: es la verdad que el primer código escondía.

8. Escribe un pipeline completo

Arma desde cero, sin copiar crear_pipeline_titanic, un pipeline para el Titanic que:

  1. use las columnas pclass, sex, age, sibsp, parch, fare, embarked y además deck;
  2. impute la edad con la mediana y agregue una columna que diga si faltaba;
  3. rellene la cubierta faltante con el texto "sin dato" (strategy="constant"), porque que falte puede decir algo (los pasajeros de tercera casi nunca tienen cubierta anotada);
  4. termine en una regresión logística.

Mide su exactitud con validación cruzada en el entrenamiento y compárala con la de crear_pipeline_titanic(LogisticRegression(max_iter=1000)).

Respuesta
X_deck = agregar_variables(titanic)[
    ["pclass", "sex", "age", "sibsp", "parch", "fare", "embarked",
     "deck"]]
# Las mismas filas de entrenamiento que en la teoría
Xd_ent = X_deck.loc[X_ent.index]
# deck es de tipo categoría; como texto, admite el relleno nuevo
Xd_ent = Xd_ent.astype({"deck": "object"})

mi_prep = ColumnTransformer([
    ("edad", Pipeline([
        ("imputar", SimpleImputer(strategy="median",
                                  add_indicator=True)),
        ("escalar", StandardScaler())]), ["age"]),
    ("num", Pipeline([
        ("imputar", SimpleImputer(strategy="median")),
        ("escalar", StandardScaler())]),
     ["pclass", "sibsp", "parch", "fare"]),
    ("cubierta", Pipeline([
        ("imputar", SimpleImputer(strategy="constant",
                                  fill_value="sin dato")),
        ("onehot", OneHotEncoder(handle_unknown="ignore"))]), ["deck"]),
    ("cat", Pipeline([
        ("imputar", SimpleImputer(strategy="most_frequent")),
        ("onehot", OneHotEncoder(handle_unknown="ignore"))]),
     ["sex", "embarked"])])
mi_pipe = Pipeline([("preparar", mi_prep),
                    ("modelo", LogisticRegression(max_iter=1000))])

mio = cross_val_score(mi_pipe, Xd_ent, y_ent, cv=pliegues)
tema = cross_val_score(
    crear_pipeline_titanic(LogisticRegression(max_iter=1000)),
    X_ent, y_ent, cv=pliegues)
print(f"Con deck e indicador: {mio.mean():.3f} ± {mio.std():.3f}")
print(f"Pipeline del tema:    {tema.mean():.3f} ± {tema.std():.3f}")
print(mi_pipe.fit(Xd_ent, y_ent).named_steps["preparar"]
      .get_feature_names_out()[:4])
Con deck e indicador: 0.807 ± 0.033
Pipeline del tema:    0.823 ± 0.032
['edad__age' 'edad__missingindicator_age' 'num__pclass' 'num__sibsp']

Fíjate en tres cosas: el indicador aparece como una columna más (edad__missingindicator_age); strategy="constant" convierte el faltante en una categoría más, que one-hot trata como cualquier otra; y la cubierta no mejoró la validación. Con la clase del pasaje ya en el modelo, saber si falta la cubierta agrega poco: los dos datos cuentan casi la misma historia.

9. Momentos estadísticos

Una fábrica (datos sintéticos) registra 50 lecturas de vibración de cada máquina. Las máquinas sanas vibran parejo; las que van a fallar tienen la misma vibración promedio, pero con sacudones de vez en cuando. Cada fila es una máquina y cada columna, una lectura.

rng = np.random.default_rng(1)
n_maquinas = 400
falla = rng.integers(0, 2, size=n_maquinas)
lecturas = rng.normal(0, 1, size=(n_maquinas, 50))
# Las que fallan: algunos sacudones fuertes, en momentos al azar
sacudon = rng.random((n_maquinas, 50)) < 0.06
lecturas = lecturas + falla[:, None] * sacudon * rng.normal(
    0, 4, size=(n_maquinas, 50))
print(lecturas.shape, "  fallan:", falla.sum())
(400, 50)   fallan: 205
  1. Mide con validación cruzada una regresión logística que usa las 50 lecturas tal cual.
  2. Crea, para cada máquina, cuatro variables: la media, la desviación estándar, el máximo del valor absoluto y la curtosis (scipy.stats.kurtosis, que mide qué tan «picuda» o con colas largas es una distribución). Mide el mismo modelo con esas cuatro.
Respuesta
from scipy.stats import kurtosis

modelo_l = Pipeline([
    ("escalar", StandardScaler()),
    ("modelo", LogisticRegression(max_iter=1000))])

crudas = cross_val_score(modelo_l, lecturas, falla, cv=pliegues)

momentos = np.column_stack([
    lecturas.mean(axis=1),
    lecturas.std(axis=1),
    np.abs(lecturas).max(axis=1),
    kurtosis(lecturas, axis=1)])
resumidas = cross_val_score(modelo_l, momentos, falla, cv=pliegues)
print(f"50 lecturas crudas:    {crudas.mean():.3f}")
print(f"4 momentos por fila:   {resumidas.mean():.3f}")
50 lecturas crudas:    0.500
4 momentos por fila:   0.840

Con las lecturas crudas, el modelo no aprende nada (acierta como una moneda): un sacudón puede caer en cualquier columna, y para la regresión logística cada columna es una variable distinta con su propio peso. Los momentos estadísticos resumen la forma de las 50 lecturas sin importar en qué momento pasó cada sacudón, y con eso el modelo acierta el 84 %, contra el 50 % de las lecturas crudas. Calcularlos no produce fuga: cada fila usa solo sus propias lecturas.

10. ¿Cuánto cuesta una búsqueda?

Quieres buscar con GridSearchCV y validación cruzada de 5 pliegues sobre: C en 6 valores, la estrategia de imputación de la edad en 2 valores ("mean", "median") y el escalador en 3 opciones. ¿Cuántos modelos se entrenan? ¿Y con RandomizedSearchCV(n_iter=10)? Compruébalo con ParameterGrid.

Respuesta

La rejilla tiene 6 × 2 × 3 = 36 combinaciones; con 5 pliegues son 36 × 5 = 180 entrenamientos, más uno final con todo el entrenamiento y la mejor combinación (refit). La búsqueda aleatoria con n_iter=10 hace 10 × 5 = 50, más el final.

from sklearn.model_selection import ParameterGrid

rejilla_grande = {
    "modelo__C": [0.001, 0.01, 0.1, 1, 10, 100],
    "preparar__num__imputar__strategy": ["mean", "median"],
    "preparar__num__escalar": [StandardScaler(), MinMaxScaler(),
                               "passthrough"]}
combinaciones = len(ParameterGrid(rejilla_grande))
print("Combinaciones:", combinaciones,
      "  entrenamientos:", combinaciones * 5 + 1)
Combinaciones: 36   entrenamientos: 181

Cada hiperparámetro nuevo multiplica el costo de la rejilla. Por eso, cuando hay más de dos o tres, conviene la búsqueda aleatoria.

Mini-proyecto: del CSV sucio a la predicción

En el tema 4 conociste los pingüinos de Palmer. Ahora vas a predecir el sexo de cada pingüino a partir de sus medidas, su especie y su isla, con todo el proceso en un solo Pipeline.

  1. Del CSV a la tabla. Guarda el dataset en un CSV y vuelve a leerlo, como si te lo hubieran mandado:

    sns.load_dataset("penguins").to_csv("pinguinos.csv", index=False)
    pinguinos = pd.read_csv("pinguinos.csv")
  2. Mira qué falta. Cuenta los faltantes por columna. Las filas sin sexo no sirven para entrenar ni para evaluar (no se puede imputar la respuesta): sácalas. Las demás faltas, déjalas para el pipeline.

  3. Divide primero, con 25 % para prueba, estratificando por sexo y con random_state=0.

  4. Arma el pipeline: las cuatro medidas imputadas con la mediana y estandarizadas, species e island imputadas con la más frecuente y en one-hot, y al final un modelo.

  5. Busca hiperparámetros con GridSearchCV para dos modelos: regresión logística (C entre 0,01 y 100) y vecinos cercanos (k entre 1 y 25). Quédate con el que tenga mejor validación.

  6. Evalúa una sola vez en la prueba, con classification_report y la matriz de confusión del tema 9.

  7. Informe. En cinco líneas: qué hiciste con los faltantes, qué modelo ganó y con qué hiperparámetros, la exactitud en validación y en prueba, y una advertencia (¿sirve este modelo para pingüinos de otras islas o especies?).

Resultados de referencia, para comprobar los tuyos
from sklearn.metrics import confusion_matrix

sns.load_dataset("penguins").to_csv("pinguinos.csv", index=False)
pinguinos = pd.read_csv("pinguinos.csv")
print(pinguinos.isna().sum().to_string())
pinguinos = pinguinos.dropna(subset=["sex"])
print("Filas con sexo:", len(pinguinos))

medidas = ["bill_length_mm", "bill_depth_mm", "flipper_length_mm",
           "body_mass_g"]
P = pinguinos[medidas + ["species", "island"]]
s = pinguinos["sex"]
P_ent, P_prueba, s_ent, s_prueba = train_test_split(
    P, s, test_size=0.25, stratify=s, random_state=0)

prep_ping = ColumnTransformer([
    ("num", prep_numericas, medidas),
    ("cat", prep_categoricas, ["species", "island"])])
candidatos = {
    "logística": (LogisticRegression(max_iter=1000),
                  {"modelo__C": [0.01, 0.1, 1, 10, 100]}),
    "vecinos": (KNeighborsClassifier(),
                {"modelo__n_neighbors": [1, 3, 5, 9, 15, 25]})}
mejores = {}
for nombre, (modelo, grilla) in candidatos.items():
    pipe = Pipeline([("preparar", prep_ping), ("modelo", modelo)])
    b = GridSearchCV(pipe, grilla, cv=pliegues).fit(P_ent, s_ent)
    mejores[nombre] = b
    print(f"{nombre:10} {b.best_params_}  validación = "
          f"{b.best_score_:.3f}")

ganador = max(mejores.values(), key=lambda b: b.best_score_)
pred = ganador.predict(P_prueba)
print(f"Prueba: {ganador.score(P_prueba, s_prueba):.3f}")
print(confusion_matrix(s_prueba, pred))
print(classification_report(s_prueba, pred))
species               0
island                0
bill_length_mm        2
bill_depth_mm         2
flipper_length_mm     2
body_mass_g           2
sex                  11
Filas con sexo: 333
logística  {'modelo__C': 10}  validación = 0.911
vecinos    {'modelo__n_neighbors': 5}  validación = 0.908
Prueba: 0.905
[[38  4]
 [ 4 38]]
              precision    recall  f1-score   support

      Female       0.90      0.90      0.90        42
        Male       0.90      0.90      0.90        42

    accuracy                           0.90        84
   macro avg       0.90      0.90      0.90        84
weighted avg       0.90      0.90      0.90        84
  • Faltantes: 11 pingüinos sin sexo (se sacan: es la respuesta) y 2 sin medidas. Esos dos también tienen el sexo vacío, así que al sacar las 11 filas ya no queda ningún faltante; el imputador del pipeline igual queda listo por si llega un pingüino incompleto.
  • Ganador: la regresión logística con C = 10, con 91,1 % en validación, casi empatada con los vecinos cercanos (k = 5, 90,8 %): una diferencia así no permite decir que un modelo sea mejor que el otro. En prueba acierta el 90,5 %, muy parecido a la validación, y se equivoca por igual con hembras y machos (4 de 42 en cada grupo). Con solo 84 pingüinos de prueba, cada uno vale más de un punto.
  • Advertencia: el modelo vio tres especies de tres islas de la Antártida. Con otra especie, de otro tamaño, las medidas que separan machos de hembras serían otras.

Errores comunes

  • Preparar antes de dividir. Imputar, escalar, elegir columnas o codificar con todos los datos mete información de la prueba en el entrenamiento. Divide primero y pon cada paso dentro del pipeline.
  • Llamar a fit sobre la prueba. El imputador y el escalador se ajustan una sola vez, con el entrenamiento; a la prueba solo se le aplica transform. Con un pipeline, esto ocurre solo.
  • Dejar una columna que contiene la respuesta. alive en el Titanic, una fecha de alta en datos de hospital. Si el modelo es casi perfecto, sospecha.
  • Elegir hiperparámetros mirando la prueba. Para elegir está la validación cruzada; la prueba se usa una vez, al final.
  • Numerar categorías sin orden. S = 0, C = 1, Q = 2 inventa un orden y unas distancias que no existen. Usa one-hot para las nominales.
  • Rellenar con la media una columna con atípicos. Unos pocos valores enormes arrastran la media. La mediana es la opción segura por defecto.
  • Imputar la respuesta. Las filas sin respuesta no sirven para entrenar ni para evaluar: se sacan.
  • Olvidar escalar antes de los vecinos cercanos o de un descenso de gradiente. La variable con números grandes manda. Los árboles del tema 11 son la excepción.
  • Quedarse con una variable nueva por una mejora menor que el vaivén entre pliegues. Mira el ± antes de celebrar.
  • No fijar semillas. Sin random_state en la división, los pliegues y la búsqueda aleatoria, cada corrida da otro número y no puedes comparar.

¿Lo lograste?

Al terminar este tema deberías poder hacer lo siguiente. Al lado, dónde lo practicaste.

  • Imputar faltantes con la media o la mediana y justificar la elección (ejercicios 1 y 2, paso 2 del mini-proyecto).
  • Codificar variables categóricas con one-hot y escalar las numéricas, estandarizando o normalizando (ejercicios 3 y 4).
  • Armar un Pipeline con ColumnTransformer y reconocer la fuga de datos (ejercicios 5, 6, 7 y 8, y pasos 3 y 4 del mini-proyecto).
  • Buscar hiperparámetros con GridSearchCV y RandomizedSearchCV (ejercicios 7 y 10, paso 5 del mini-proyecto).
  • Crear variables nuevas, como conteos, cocientes y momentos estadísticos, y medir si ayudan (ejercicios 8 y 9).