Guía de repaso · Parcial 1

Ingeniería de Datos (UCU, 2026). Cubre las clases 1 a 6 (UT1 y UT2), el caso de estudio y las prácticas del sitio del profe.

Armada con las diapositivas, el syllabus, la consigna del parcial y tu avance de Olist

Cómo es el parcial

Viernes 18/09, 8:05. Llevá la laptop cargada y con acceso a Google Colab y WebAsignatura. Si llegás tarde, tenés solo el tiempo que quede.

ParteTiempoPuntosQué es
1 · Cuestionario40 min50Unas 20 preguntas de múltiple opción sobre las clases 1 a 6: fundamentos, exploración, integración, calidad, escalado y ética. Una respuesta por pregunta y no resta por error, así que contestá todas.
2 · Cuaderno de Colab80 min50Ej. 1 · viajes (25): integrar fuentes, verificar los cruces y hacer un resumen fundamentado.
Ej. 2 · viviendas (25): diagnosticar, comparar dos preparaciones y justificar la elección con una evaluación válida.
3 · Revisión y entrega5 min–Revisá que el cuaderno corra en orden y que las salidas queden guardadas. Entregás las respuestas del cuestionario y el .ipynb ejecutado, con tu nombre y las conclusiones adentro.

Qué se evalúa

Se evalúan las decisiones, los resultados y la evidencia que los respalda. No alcanza con mostrar una tabla o una métrica: tenés que explicar qué permite concluir y qué limitaciones tiene. No hay una única solución ni un umbral de error que superar.

Qué podés usar

Apuntes, prácticas, documentación y páginas web comunes. No podés usar ChatGPT, Claude, Gemini, Copilot, Perplexity, NotebookLM, la IA de Colab ni el autocompletado generativo. Apagá el autocompletado de IA de Colab antes de empezar.

Cómo conviene encarar el cuadernoUn error puntual de código no invalida todo el ejercicio: cuenta lo que hayas logrado demostrar. Si algo no te corre, escribí en una celda de texto qué querías hacer y por qué. Podés preguntar qué pide una consigna, pero no cómo resolverla.

La nota del Parcial 1 vale el 25% del curso. Según el syllabus se reparte así: 40% preguntas de las unidades, 10% preguntas del caso y 50% mini-lab individual.

Mapa del temario

Si tenés poco tiempo, empezá por lo marcado con ★: es lo que se usa en el cuaderno.

Clase 1

Roles, pipeline de preparación, ETL, herramientas.

Clase 2

Las 4 habilidades, el proceso de análisis, los principios de Google, distribuciones y outliers con Titanic.

Clase 3 ★

Datos fragmentados, formatos de carga, tipos de join, indicator y validate.

Clase 4 ★

MCAR, MAR y MNAR, imputación, anti-leakage, outliers con IQR y z-score.

Clase 5 ★

Escalado (MinMax, Standard, Robust), log1p, leakage y split correcto.

Clase 6

Desbalance, tipos de sesgo, drift, Boston B, métricas de fairness y Fairlearn.

Prácticas del sitio que entran: P1 Iris, P2 Netflix, P3 multifuentes y joins (NYC Taxi) ★, P5 Missing Data Detective ★, P6 Feature Scaling y Anti-Leakage (Ames Housing) ★ y P7 Fairlearn. UT3 en adelante no entra.

Regla de las unidades: UT1 es explorar y entender (no se limpia ni se imputa). UT2 es limpiar y transformar.

Clase 1 · Fundamentos UT1

Qué es la ingeniería de datos y dónde encaja.

Los tres roles

Data AnalystData EngineerData Scientist
FocoInterpretar datos para encontrar insights y apoyar decisionesConstruir y mantener pipelines e infraestructura de datosUsar los datos para construir modelos y predecir resultados
SkillsSQL, Excel, Power BI/Tableau, estadística básicaPython, herramientas ETL, Big Data, plataformas cloudPython/R, Machine Learning, estadística, visualización
TareasReportes y dashboards, analizar tendencias y patronesLimpiar y transformar datos crudos, montar data warehouses y data lakesCrear modelos de ML, analítica predictiva y prescriptiva

Lo que tienen en común: los tres trabajan con datos y necesitan que sean confiables. La diferencia: el ingeniero hace que los datos lleguen bien y a tiempo, el analista los interpreta y el científico modela y predice.

Pipeline de preparación de datos

Data Collection→Data Formatting→Data Cleaning→Data Preparation→Data Transformation→Feature Engineering→Feature Selection→Data Splitting

Es un ciclo: cada etapa puede obligarte a volver a una anterior.

ETL: Extract, Transform, Load

Fuentes: bases de datos, web, apps, ERP, CRM, SCM…→Extract→Transform→Load→Insights→Decisiones basadas en datos
  • Extract: sacar los datos de sus fuentes originales.
  • Transform: limpiar, unificar tipos y formatos, unir y agregar.
  • Load: guardar el resultado en el destino (un data warehouse, por ejemplo) para analizarlo.

Herramientas

No todas son aplicaciones listas para usar: muchas son librerías y frameworks sobre los que se construyen soluciones.

Datos y cálculo

pandas, NumPy, SciPy, Dask (pandas en paralelo o con datos más grandes que la memoria), PySpark

Visualización

Matplotlib, Seaborn, Plotly, Bokeh

ML y modelado

scikit-learn, statsmodels, TensorFlow, PyTorch, Keras

Entorno y orquestación

Jupyter, Google Colab, Conda, papermill, Prefect (flujos con reintentos)

Formatos de UT1: CSV (texto plano, universal), JSON (estructura anidada, típico de APIs), SQLite (base relacional en un archivo, se consulta con SQL) y Parquet (columnar y comprimido, eficiente para datos grandes).

Resultados esperados de UT1: cargar y explorar datos en distintos formatos, hacer EDA con pandas, visualizar con matplotlib y seaborn, documentar con MkDocs, trabajar con GitHub y comparar pandas con PySpark.

Clase 2 · Principios de EDA UT1

Cómo hacer un análisis creíble, con el Titanic como ejemplo.

Las 4 habilidades del científico de datos (Brust)

1. Programación

Pensamiento computacional: partir problemas complejos en pasos. Herramientas: Python, pandas, seaborn.

2. Estadística

Media, mediana y desviación estándar. Sorprende lo mucho que se logra con rudimentos. Sirve para extraer conocimiento de los datos.

3. Comunicación

Explicar procesos complejos con visualizaciones que "hablen", para público técnico y no técnico.

4. Conocimiento del dominio

Experiencia en el área. Pregunta: ¿estos resultados tienen sentido? y ¿qué preguntas deberíamos hacer?

El proceso de análisis de datos

Importar→Ordenar→Comprender: Transformar ↔ Visualizar ↔ Modelar→Comunicar

La parte de comprender es un ciclo: transformás, visualizás, modelás y volvés a empezar.

Principios del buen análisis (Google, Good Data Analysis)

Un analista creíble es el que se gana la reputación de hacer afirmaciones confiables a partir de los datos. La guía se organiza en tres partes:

Técnico

Ideas y técnicas para manipular y examinar datos.

  • Mirá las distribuciones
  • Considerá los outliers
  • Reportá el ruido o la incertidumbre
  • Mirá ejemplos concretos
  • Segmentá los datos (slicing)
  • Considerá la significancia práctica
  • Revisá la consistencia en el tiempo
  • Contá lo que filtrás
  • Los ratios tienen que tener numerador y denominador claros

Proceso

Cómo abordar los datos, qué preguntas hacer y qué verificar.

  • Separar validación, descripción y evaluación
  • Confirmar cómo se recolectaron los datos
  • Chequear los "signos vitales"
  • Primero lo estándar, después lo personalizado
  • Medir dos veces o más
  • Verificar la reproducibilidad
  • Plantear hipótesis y buscar evidencia

Mentalidad

Cómo trabajar con otros y comunicar resultados.

  • El análisis empieza con preguntas, no con datos ni con una técnica
  • Ser escéptico y a la vez defensor de los resultados
  • Correlación no es causalidad
  • Compartir primero con pares
  • Aceptar la ignorancia y los errores

Principio 1 · Consultá tus distribuciones

Lo que NO hay que hacerResumir una variable solo con media y mediana. En Titanic, age tiene media 29,7 y mediana 28, y eso no dice nada de su forma, del rango ni de los faltantes.

Lo que SÍ hay que hacer: combinar varias representaciones.

sns.histplot(df['age'])      # distribución completa
sns.boxplot(df['age'])       # detectar outliers
df['age'].describe()         # resumen numérico
sns.countplot(x='pclass', hue='sex', data=df)   # categóricas combinadas

missing_pct = (df.isnull().sum() / len(df) * 100).round(1)
missing_pct[missing_pct > 0]  # deck 77.2 · age 19.9 · embarked 0.2

Después se miran las tasas por grupo con sns.barplot(x=..., y='survived', estimator='mean'):

CorteQué se vio
SexoSobrevivió cerca del 74% de las mujeres y cerca del 19% de los hombres
Clase1ª ≈ 63% · 2ª ≈ 47% · 3ª ≈ 24%
PuertoC (Cherbourg) tiene la tasa más alta, alrededor de 55%
Tamaño de familia (sibsp + parch + 1)Pico en familias de 4. Los que viajaban solos y las familias grandes sobrevivieron menos. Los grupos grandes tienen muy pocos casos (barras de error enormes).
Solo vs acompañadoAcompañados ≈ 50% · solos ≈ 30%
Sexo × clase (pd.crosstab + heatmap)Mujeres de 1ª: 96,8% · hombres de 3ª: 13,5%. Es el privilegio múltiple: los factores interactúan.

El dataset tiene 891 filas y 15 columnas.

Principio 2 · Considerá los valores atípicos

Los outliers son como el canario en la mina: avisan temprano que puede haber un problema más profundo en los datos, en el sistema o en el proceso.

  • fare: máximo £512,33 contra una mediana de £14,45. Se ve mejor con plt.yscale('log') y comparando por survived.
  • age: va de 0,42 a 80. Los puntos por encima de ~65 aparecen como outliers en el boxplot, pero son edades reales.
Qué se logra con métodoValidación (encontrar faltantes críticos) · Contexto (el protocolo histórico explica los patrones, como "mujeres y niños primero") · Outliers (tarifas extremas para investigar) · Interacciones (privilegio múltiple). Sin método se pierden los sesgos en los faltantes, la importancia de los outliers, las interacciones y la explicación histórica.

Clase 3 · Multifuentes y joins UT1 · ★ Ej. 1

Integrar fuentes sin perder ni multiplicar filas. Es la base del Ejercicio 1.

Por qué los datos están fragmentados

El escenario

Empresa de transporte. El CEO pregunta qué zonas son más rentables, si los eventos especiales impactan en los ingresos y si conviene cambiar tarifas por zona. Los datos están repartidos en sistemas distintos: viajes, geografía, eventos y finanzas.

Las causas

  • Sistemas legacy: cada área armó el suyo
  • Especialización: cada sistema optimizado para una función
  • Escalabilidad: es más fácil mantener sistemas chicos
  • Seguridad: distintos niveles de acceso
  • Proveedores externos

Caso NYC Taxi (TLC): 240.000 viajes por día en una ciudad de 8,4 millones de personas. Afecta a pasajeros, conductores, la ciudad y los reguladores. Las preguntas de negocio: zonas desatendidas (rutas), tarifas dinámicas en eventos (precios), fraude por zona (compliance) y equidad entre barrios (responsabilidad social).

Carga de formatos

trips = pd.read_csv(url, parse_dates=['pickup_datetime'])  # CSV con fechas parseadas
trips = pd.read_parquet(url)                                # Parquet: más eficiente con datos grandes
calendar = pd.read_json(url)                                # JSON
calendar['date'] = pd.to_datetime(calendar['date']).dt.date # string → fecha sin hora
con = sqlite3.connect('db.sqlite'); df = pd.read_sql('SELECT * FROM t', con)  # SQLite

Simulador de joins (el ejemplo de la clase)

ventas izquierda
idproducto_idcantidad
112
221
331
413
productos derecha
idnombreprecio
1Laptop1000
2Mouse25
5Tablet500

TipoQué conservaCuándo usarlo
innerSolo las filas con coincidencia en ambos ladosCuando solo te importan los casos completos. Puede perder filas sin avisar.
leftTodas las filas de la izquierda. Si no hay coincidencia, NaN.El caso más común: tabla de hechos + catálogo (viajes + zonas, pedidos + sucursales). Conserva la unidad de observación.
rightTodas las filas de la derechaEs un left con las tablas al revés
outerTodas las filas de ambos ladosPara auditar qué falta en cada lado

Auditar un cruce: indicator y validate

n0 = len(pedidos)
assert pedidos['pedido_id'].is_unique            # la clave de hechos es única
assert sucursales['sucursal_id'].is_unique       # la clave del catálogo es única

m = pedidos.merge(sucursales, on='sucursal_id', how='left',
                  validate='many_to_one',        # falla si la derecha tiene claves repetidas
                  indicator='cruce_sucursal')    # both / left_only / right_only
assert len(m) == n0                              # no se perdieron ni multiplicaron pedidos
print(m['cruce_sucursal'].value_counts())
print(m.loc[m['cruce_sucursal']=='left_only', 'sucursal_id'].unique())  # claves sin match

resumen = (m.groupby('region', dropna=False)     # dropna=False conserva "sin región"
            .agg(pedidos=('pedido_id','count'), monto_prom=('monto','mean')))
validate=Comprueba que…
'one_to_one'la clave es única en ambas tablas
'one_to_many'la clave es única en la izquierda
'many_to_one'la clave es única en la derecha (el caso de hechos + catálogo)
'many_to_many'no chequea nada

Si falla, pandas lanza un MergeError. Es un control que falla a propósito cuando los datos no cumplen lo esperado.

Trampas típicas de un join
  • Filas que aumentan después de un left join: la clave de la derecha tiene duplicados y se multiplicaron filas.
  • Cero coincidencias con fechas que "se ven iguales": tipos distintos (datetime64, date o str), horas incluidas o períodos que no se solapan. Compará min() y max() de las dos columnas antes de unir.
  • Claves con distinto tipo (int contra str): unificalas con astype antes del merge.
  • Un NaN después del join no siempre es "falso": puede significar "no informado". No lo rellenes con False sin pensarlo.
  • groupby descarta los NaN por defecto: usá dropna=False para no esconder filas.

Lo que hace la práctica P3 (NYC Taxi)

  1. Carga viajes (Parquet, ~3M de filas), zonas (CSV) y calendario (JSON).
  2. Normaliza: columnas en minúscula, crea pickup_date = tpep_pickup_datetime.dt.date y revisa que los tipos coincidan.
  3. Optimiza memoria (int16, int8) y elimina filas sin ubicación.
  4. Hace un left join de viajes con zonas (pulocationid ↔ locationid) y cuenta los viajes sin borough.
  5. Hace un left join con el calendario por fecha y crea is_special_day = special.fillna(False).
  6. Agrupa por borough con count, mean, std y median, y calcula métricas de negocio: ingreso por milla, % de propina y participación de mercado.
  7. Compara días normales y especiales, arma muestras con sample(n, random_state=42) para graficar, calcula la tasa de coincidencia y la cobertura de zonas, y hace el análisis por hora y las correlaciones.

Bonus Prefect: @task(retries=2) reintenta automáticamente si falla, por ejemplo una URL caída. @flow conecta las tasks en un pipeline con logs.

Clase 4 · Faltantes y outliers UT2 · ★ Ej. 2

Un dataset es un rompecabezas con piezas que faltan: por azar, por un patrón o por algo oculto.

  • Un solo NA puede cambiar un promedio o romper un modelo.
  • Antes de imputar, entendé por qué falta.
  • Que un dato falte también puede ser información.

Tipos de faltantes (ejemplo de la clase: edad e IQ)

TipoDe qué depende que falteEjemploQué hacer
MCAR
Missing Completely At Random
De nada: es puro azarQue falte un IQ no depende ni de la edad ni del propio IQImputación simple (media, mediana o moda). Suele estar bien.
MAR
Missing At Random
De otra variable observadaLos IQ faltantes se concentran en ciertos rangos de edad. La probabilidad de faltar depende de la edad, no del IQ real.Imputar condicionando en esa variable (por ejemplo, mediana por grupo de edad).
MNAR
Missing Not At Random
Del propio valor que falta (o de una causa no observada)Justo los de IQ bajo no lo reportan. Aunque controles por edad, la falta sigue atada al valor.No inventar: dejar NaN y agregar una bandera, o usar una categoría especial.
Cómo distinguirlos en la prácticaCompará el % de faltantes entre grupos de otra variable (df.groupby('grupo')['col'].apply(lambda s: s.isna().mean())). Si cambia mucho entre grupos, no es MCAR. MAR y MNAR no se pueden separar solo mirando los datos: hace falta conocer el proceso que los generó (conocimiento del dominio).

Imputación: reglas simples

Rellenar no es gratis: hacelo columna por columna y documentá cada decisión.

  • Numéricas: mediana si la distribución tiene cola pesada o outliers, media si es aproximadamente normal.
  • Categóricas: la moda o "Unknown", con una bandera si sospechás MNAR.
  • Series de tiempo: ffill o bfill con cuidado, para no propagar errores.
from sklearn.impute import SimpleImputer
SimpleImputer(strategy='median')            # 'mean' | 'median' | 'most_frequent' | 'constant'
SimpleImputer(strategy='constant', fill_value='Unknown')
SimpleImputer(strategy='median', add_indicator=True)   # agrega columna "faltaba"
df['col_faltaba'] = df['col'].isna().astype(int)       # bandera manual
df.isna().mean().sort_values(ascending=False) * 100    # % faltante por columna

Imputar con la media o la mediana reduce la varianza y genera un pico artificial en ese valor. Eso se ve en un histograma de antes y después.

Anti-leakage básico

No espíes el examenHacé fit solo con TRAIN y transform en VALID y TEST.
1) Split en X_train, X_valid y X_test · 2) imputer.fit(X_train) y transform al resto · 3) Nunca recalcular con valid o test.

Outliers

Son rebeldes: pueden ser errores o casos valiosos (como un fraude). Un outlier es un valor muy lejos del grupo. Decidí qué hacer con ellos antes de modelar.

IQR (caja y bigotes)

IQR = Q3 − Q1. Es sospechoso si
x < Q1 − 1,5·IQR o x > Q3 + 1,5·IQR.
Usa la mediana y los cuartiles. Sirve para distribuciones asimétricas o con colas pesadas. Da una lista de puntos para revisar con contexto.

Z-score

z = (x − media) / desvío. Es sospechoso si |z| > 3.
Sirve cuando la forma es aproximadamente de campana y sin colas raras. Si hay colas, aplicá log primero o usá la versión robusta con la mediana. Los propios outliers inflan la media y el desvío.

Ver es creer

Mirá el boxplot, el histograma o el scatter antes de decidir. Un punto puede ser normal en una variable y raro en la relación entre dos.

q1, q3 = s.quantile([.25, .75]); iqr = q3 - q1
mask_iqr = (s < q1 - 1.5*iqr) | (s > q3 + 1.5*iqr)
z = (s - s.mean()) / s.std()
mask_z = z.abs() > 3
print(mask_iqr.mean()*100, '% outliers por IQR')

Ejemplo con la edad del Titanic: Q1 = 20,1 y Q3 = 38, así que IQR ≈ 17,9 y el bigote superior queda en 38 + 1,5·17,9 ≈ 64,8. Por eso las edades mayores a ~65 aparecen como puntos sueltos en el boxplot. Son raras, pero reales.

DecisiónCuándo
EliminarError claro, menos del 5% de los datos e impacto bajo
TransformarCuando distorsiona (log, winsorizar o recortar a un límite)
MantenerCuando lo raro es justamente la señal (fraude, tarifas de primera clase)
Superar un límite estadístico no alcanza para borrar un datoPrimero preguntá si el valor es posible en el dominio: un auto de 200.000 km es posible, uno con −5 km no. Documentá qué hiciste y cuántas filas afecta.

Lo que hace la práctica P5 (Missing Data Detective)

Toma Ames Housing y le genera faltantes sintéticos de cada tipo. Después analiza patrones (% por columna, faltantes por grupo), clasifica MCAR, MAR y MNAR, detecta outliers con IQR y z-score, los visualiza, compara estrategias de imputación, hace imputación "inteligente" según el tipo, aplica anti-leakage (fit en train) y mide el impacto de imputar en las distribuciones.

Clase 5 · Escalado y leakage UT2 · ★ Ej. 2

Si mezclás columnas con números gigantes (salario) y chicos (edad), la gigante tapa a las demás.

  • Las distancias y los gradientes sí dependen de la escala: KNN, K-means, SVM, regresión con descenso por gradiente o con regularización, redes neuronales y PCA.
  • Los árboles (árbol de decisión, Random Forest, gradient boosting) cortan por umbrales y casi no se ven afectados por el escalado.
  • Ejemplo: 35 años contra $80.000. Mandan los $80.000.
  • Normalizar pone todas las columnas en la misma escala para comparar peras con peras. No crea información nueva ni arregla sesgos.

Los tres scalers

ScalerFórmulaResultadoCuándo usarlo
MinMaxScaler
"aprieta y estira"
(x − min) / (max − min)Rango [0, 1]Datos limpios y acotados, o cuando necesitás un rango fijo. Con outliers se rompe: todo el resto queda aplastado.
StandardScaler
(z-score) "centra en 0 y ajusta el tamaño"
(x − media) / desvíoMedia 0 y desvío 1, sin rango fijoEl caso general. Ayuda a los optimizadores por gradiente. La clase dice que tolera mejor los valores raros que MinMax, aunque la media y el desvío igual se ven afectados.
RobustScaler
"usa el valor típico e ignora los atípicos"
(x − mediana) / IQRCentrado en la mediana, sin rango fijoOutliers fuertes o colas pesadas. Pero si los outliers son la señal (fraude), puede "suavizarlos" demasiado.

Regla corta de la clase: ¿outliers fuertes? → Robust · ¿necesitás el rango [0,1]? → MinMax · ¿caso general o gradiente? → Standard.

Probalo: el mismo dato con cada scaler

Valores: 10, 12, 13, 15, 16, 18 y un outlier que podés mover. Todos los scalers se muestran sobre el mismo eje, de −2 a +3. Subí el outlier y fijate que con MinMax y con Standard los valores normales quedan amontonados, mientras que con Robust siguen separados (el outlier se sale del eje, pero no arrastra al resto). El orden de los datos nunca cambia, porque las tres transformaciones son lineales.

Log transform: aplastar la cola

Si la variable tiene una cola larga a la derecha (ingresos, precios, superficies), np.log1p(x) la vuelve más simétrica y reduce el peso de los valores gigantes. log1p calcula log(1 + x), así que acepta ceros. Con negativos no sirve: ahí se usa PowerTransformer (Yeo-Johnson). Es una transformación fila por fila: no aprende parámetros, así que no genera leakage.

Data leakage

Split correcto, paso a paso1) Split en train, valid y test · 2) fit del scaler solo en train · 3) transform de valid y test con ese mismo scaler.

Si escalás o imputás antes del split, la media, el desvío o la mediana incluyen información de test y la métrica sale optimista (mejor de lo que va a ser en la realidad).

Hay otro tipo de leakage que conviene tener presente: el leakage de variables. Pasa cuando usás una columna que no existe todavía en el momento de predecir, o que se calcula a partir del target. Siempre preguntate: ¿este dato lo tengo antes del evento que quiero predecir?

Pipeline y ColumnTransformer (patrón del simulacro y la P6)

from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import StandardScaler, RobustScaler, OneHotEncoder

num = Pipeline([('imputar', SimpleImputer(strategy='median')),
                ('escalar', RobustScaler())])
pre = ColumnTransformer([
    ('num', num, ['Kilometraje', 'Potencia']),
    ('cat', OneHotEncoder(handle_unknown='ignore'), ['Marca']),   # sin orden impuesto
])
X_ref = pre.fit_transform(referencia[cols])   # aprende SOLO con referencia/train
X_new = pre.transform(nuevos[cols])           # aplica las mismas reglas
pre.named_transformers_['num'].named_steps['imputar'].statistics_  # evidencia: medianas aprendidas
  • OneHotEncoder crea una columna 0/1 por categoría y no impone un orden, a diferencia de codificar Toyota=1, VW=2.
  • handle_unknown='ignore': si en el lote nuevo aparece una categoría que no estaba en el ajuste, la fila queda en ceros en lugar de dar error.
  • get_feature_names_out() devuelve los nombres de las columnas de salida.

Comparar dos preparaciones con una evaluación válida (Ej. 2 del parcial)

from sklearn.model_selection import train_test_split, cross_val_score, KFold
from sklearn.neighbors import KNeighborsRegressor

X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.2, random_state=42)
cv = KFold(n_splits=5, shuffle=True, random_state=42)       # MISMOS folds para ambas

for nombre, prep in {'A: mediana+Standard': pre_a, 'B: mediana+Robust': pre_b}.items():
    pipe = Pipeline([('prep', prep), ('modelo', KNeighborsRegressor(n_neighbors=7))])
    s = -cross_val_score(pipe, X_tr, y_tr, cv=cv, scoring='neg_mean_absolute_error')
    print(nombre, s.mean().round(1), '±', s.std().round(1))
  • Mismo modelo, mismos folds, mismo scoring: lo único que cambia es la preparación.
  • La preparación va dentro del Pipeline, así cross_val_score la ajusta de nuevo en cada fold, solo con los datos de entrenamiento de ese fold.
  • Elegís con la CV sobre train. El test se usa una sola vez al final. Si comparás decisiones usando el test, ese número pasa a ser de validación.
  • Compará contra un baseline (predecir la media, con DummyRegressor) y mirá la dispersión: si la diferencia entre A y B es menor que el desvío, no hay un ganador claro. Decilo así.
  • Métricas: MAE (error promedio en las unidades del target, menos es mejor), RMSE (penaliza más los errores grandes) y R² (proporción de varianza explicada, más es mejor).

Lo que hace la práctica P6 (Ames Housing)

Identifica columnas con escalas muy distintas (por ejemplo Lot Area, Year Built y SalePrice) calculando los ratios max/min. Hace el split antes de escalar y estudia si detectar outliers antes o después de escalar cambia el resultado. Aplica log a la columna más sesgada (scipy.stats.skew, con |skew| > 2 como "muy sesgada") y prueba un transformador avanzado: PowerTransformer, QuantileTransformer, MaxAbsScaler, Normalizer o FunctionTransformer. El gran experimento compara tres métodos: (1) escalar todo y después hacer el split, con leakage; (2) hacer el split y ajustar en train, sin leakage; (3) usar Pipeline con cross_val_score, que es lo recomendado. Regla de oro de la práctica: "si hay duda, usá Pipeline con StandardScaler y validá con cross-validation".

Dato sobre el escalado y los outliersStandardScaler, MinMaxScaler y RobustScaler son transformaciones lineales. No cambian el orden de los datos, así que el método IQR marca exactamente los mismos puntos antes y después de escalar. Lo que sí cambia qué puntos quedan como outliers es el log: mantiene el orden, pero comprime las distancias en la cola.

Clase 6 · Sesgo, ética y fairness UT2

Los datos reflejan el mundo, con todos sus sesgos.

Desbalances

  • En la variable objetivo: en Titanic, alrededor del 62% no sobrevivió y el 38% sí. Si un modelo predice siempre "no sobrevive", acierta el 62% sin aprender nada. Por eso la accuracy sola engaña.
  • En los atributos: 65% hombres y 35% mujeres, y la 3ª clase es la más numerosa. Los grupos chicos tienen estimaciones menos confiables.

Tipos de sesgo

SesgoQué es
HistóricoLos datos reflejan políticas o realidades del pasado que no queremos perpetuar
Representación / muestreoGrupos sub- o sobrerrepresentados (selection bias, survivorship bias)
MediciónErrores o diferencias sistemáticas al medir o registrar (instrumentos, encuestas, OCR)
EtiquetadoSesgo de quienes anotan o de las guías de rotulado (labeling bias)
ProxyVariables que funcionan como sustitutas de un atributo sensible (la ubicación puede reflejar raza o ingresos)
AgregaciónPromedios globales que ocultan disparidades entre grupos (falacia ecológica)
AlgorítmicoEl objetivo, la función de pérdida o la búsqueda de performance redistribuyen el error entre grupos
Despliegue y feedbackEl uso del sistema altera los datos futuros (feedback loops)

Deriva (drift)

Data drift

Cambia la distribución de las entradas X, aunque la regla para predecir no cambie (o cambie poco).
Ejemplos: el público ahora es más joven, cambió la cámara y las fotos tienen otra iluminación, se recalibró un sensor y las lecturas vienen en otro rango.

Concept drift

Cambia la relación X → y.
Ejemplos: los spammers cambian de táctica y las palabras sospechosas ya no son las mismas; después de la pandemia la gente compra distinto con los mismos datos de entrada.

Boston Housing y la variable B

Es un dataset clásico de los años 70 con variables urbanas y sociodemográficas. La variable B = 1000·(Bk − 0,63)², donde Bk es la proporción de población negra por zona, introduce composición racial y con eso el riesgo de perpetuar sesgos. El objetivo del caso es analizar el sesgo y discutir la mitigación y la ética, no "mejorar el precio".

Mitigación

  • Excluir B/Bk, analizando el impacto en el desempeño
  • Descorrelación: proyectar las features para quitarles la componente correlacionada con el atributo sensible A (ortogonalización)
  • Re-weighting por grupo para equilibrar su peso en el entrenamiento
  • Post-processing: ajustar interceptos o umbrales de decisión, documentando los riesgos

Siempre comparar el trade-off: error global contra brechas entre grupos.

Errores comunes y buenas prácticas

  • No usar B para ganar performance sin justificar el impacto social
  • Evitar leakage entre splits al transformar B o Bk
  • Reportar la incertidumbre (intervalos o errores estándar) por grupo
  • Visualizar relaciones no lineales (scatter con suavizado) y densidades por grupo
  • Sacar el atributo sensible no alcanza si quedan proxies

Métricas de fairness (A = atributo sensible)

MétricaQué pide
Demographic Parity (DP)Que la probabilidad de recibir el resultado positivo (la decisión favorable) sea parecida entre grupos. No mira el valor real.
Equal Opportunity (EO)Que la TPR sea parecida entre grupos: de los que realmente son positivos, detectar la misma proporción en cada grupo.
Equalized Odds (EOd)Que la TPR y la FPR sean parecidas entre grupos: igual tasa de aciertos y de falsos positivos. Es más estricta que EO.
Calibración por grupoSi el modelo dice "70%", que en cada grupo alrededor del 70% sean positivos de verdad.

TPR = TP / (TP + FN), la sensibilidad o recall. FPR = FP / (FP + TN).

Fairlearn es una librería para medir y mitigar disparidades entre grupos, en clasificación y regresión. Se usa cuando hay atributos sensibles y posibles impactos desiguales. No garantiza justicia: ayuda a hacer explícitos los trade-offs y a documentarlos. En la P7 se usan MetricFrame para medir por grupo y ExponentiatedGradient con una restricción (por ejemplo DemographicParity) para corregir, en Boston (B) y en Titanic (sexo y clase).

Caso de estudio 1 · "Datos que merezcan confianza"

El 10% del parcial son preguntas sobre el caso. Tenés que poder explicar la consigna y las decisiones de tu equipo.

Pregunta central: ¿Qué decisiones puede sostener este dataset, qué problemas reducen su confiabilidad y qué controles permiten detectar si esos problemas reaparecen? Cada equipo la baja a 2 o 3 preguntas propias. No hay variable objetivo obligatoria ni se pide entrenar un modelo.

Requisitos del dataset: que represente un proceso real con procedencia documentada, tenga al menos 8 variables útiles de distintos tipos, presente decisiones reales de integración o calidad, permita uso académico sin datos sensibles y no se haya usado como práctica de la materia. Opciones de referencia: Olist, Online Retail II y Beijing Air Quality.

Las 5 etapas

EtapaQué pide
1. ComprenderInventario de archivos, formatos, tamaños, períodos y procedencia. Unidad de análisis, claves y relaciones. Conservar la fuente original. Antes de explorar, predecir los 2 principales problemas de calidad y después contrastarlos con la evidencia.
2. ExplorarTipos, cobertura, distribuciones, faltantes y relaciones. Cada hallazgo importante responde: qué evidencia hay, qué podría explicarla, qué decisión provoca y qué todavía no se puede concluir.
3. DiagnosticarPerfil reproducible con las 7 dimensiones (abajo). Reglas y umbrales justificables. No todo valor raro es un error.
4. Curar y controlarVersión curada con grano y contrato explícitos. Decisiones documentadas sobre tipos, faltantes, duplicados, categorías, fechas, unidades, claves y uniones. Controles ejecutables que fallen ante datos incompatibles. Comparación de antes y después. La fuente nunca se sobrescribe.
5. Desafiar y concluirProbar el flujo con casos que violen al menos 3 reglas y explicar qué detecta y qué no cada control. Cerrar con los usos que el dataset habilita, los que requieren revisión y los que no se recomiendan.

Las 7 dimensiones de calidad

DimensiónPreguntaEjemplo
Completitud¿Falta algo?% de nulos por columna
Unicidad¿Hay duplicados donde no debería?Una clave primaria repetida
Validez¿El valor está en el dominio o rango permitido?Precio negativo, puntaje 7 en una escala de 1 a 5
Consistencia¿Los campos son coherentes entre sí?Entregado antes de ser despachado
Integridad entre fuentes¿Las claves foráneas existen en la otra tabla?Un pedido con una sucursal que no está en el catálogo
Cobertura temporal¿El período está completo y parejo?Meses casi vacíos al principio o al final
Procedencia¿Se sabe de dónde viene y cómo se obtuvo?Fuente, licencia, fecha de descarga

Clasificar lo "raro": no todo es un error

Dato inválido

Imposible en el dominio (peso 0, fechas en orden imposible). Se marca o se corrige.

Evento poco frecuente

Es raro pero real (un voucher de valor 0, una tarifa altísima). Se mantiene.

Ausencia con significado

Falta porque el proceso no llegó a esa etapa (un pedido cancelado sin fecha de entrega). No se imputa.

Problema de integración

Aparece al unir fuentes (claves compartidas, categorías sin traducción).

Limitación de la fuente

Es una característica de cómo se publicó el dataset (geolocalización con muchas filas por prefijo).

Entregables y reglas

  • Informe PDF de hasta 6 páginas sin contar referencias, y notebook ejecutable de principio a fin con instrucciones para obtener los datos.
  • No subir los datos crudos al repo. Cada gráfico lleva título, unidades, población e interpretación.
  • Avance: 13/09. Entrega final: 23/11, que es la base de la modificación individual de la Prueba Final.
  • Uso de IA: se sigue la secuencia predecir → intentar → consultar → verificar → declarar, registrando herramienta, para qué, qué se modificó y cómo se validó. No se le pasan datos sensibles ni credenciales, y cada integrante tiene que poder explicar y modificar el trabajo.
  • Referencias sugeridas: pandas merge, pandas missing data y Pandera (esquemas de DataFrame, útiles para los controles ejecutables).

Tu caso: Olist (equipo 6)

Lo que tenés que poder explicar con tus palabras. Todo sale de tu informe de avance y del registro de decisiones.

El dataset

Unos 100.000 pedidos de marketplaces de Brasil entre 2016 y 2018, publicados por Olist en Kaggle con licencia CC BY-NC-SA 4.0. Son datos anonimizados: IDs con hash, ubicación solo como prefijo de CEP. 9 tablas: orders, items, payments, reviews, customers, products, sellers, geolocation y translation.

Sus preguntas

P1: ¿en qué estados conviene revisar la logística? (% de entregas tarde por estado).
P2: ¿el puntaje de la reseña sirve como indicador de satisfacción con la entrega?
P3: ¿el valor pagado sirve para medir la facturación por categoría?

TablaGrano (una fila es…)Clave
ordersun pedido (99.441)order_id
itemsun ítem de un pedidoorder_id + order_item_id
paymentsun pago de un pedidoorder_id + payment_sequential
reviewsuna reseña asociada a un pedidoreview_id + order_id (porque review_id solo se repite)
geolocationuna coordenada observada para un prefijoninguna

Predicciones y qué pasó

  1. Fechas del ciclo faltantes o incoherentes con el estado. ✔ Se confirmó: los faltantes se concentran en los estados que no llegaron a esa etapa. También aparecieron casos imposibles: 8 delivered sin fecha de entrega y 23 con la entrega al transportista posterior a la entrega al cliente.
  2. Reseñas y pagos no son uno a uno con el pedido. ✔ Se confirmó en parte: 547 pedidos tienen 2 o 3 reseñas, 789 review_id se comparten entre pedidos y el 3% de los pedidos tiene varios pagos. Lo pagado coincide con precio + flete en el 99,75% de los casos.

Decisiones clave (las más preguntables)

IDDecisiónPor qué
D01Los datos crudos quedan en data/raw sin editar; lo transformado va a interimLa consigna prohíbe sobrescribir la fuente
D02El prefijo de CEP se lee como textoEs un código: como número pierde los ceros iniciales
D03to_datetime(errors='coerce') contando los que fallanPara que un valor inválido no se vuelva NaN sin que nadie lo note
D04Grano: una fila por pedidoLa reseña es del pedido. A grano ítem, los pedidos con varios ítems cuentan varias veces (el promedio pasa de 4,086 a 4,033).
D05Agregar las tablas hijas antes de unir y quedarse con la reseña más reciente, marcando los pedidos con variasEvita multiplicar pedidos. Se controla con validate y con un assert len == 99.441.
D07Marcar en vez de borrar (sin_items, sin_pago, sin_resena, varias_resenas)Son ausencias con significado: de los 775 pedidos sin ítems, 603 son unavailable y 164 canceled
D08P1 y P2 solo con pedidos delivered que tienen fecha de entregaSe define la población explícitamente: 96.470 pedidos en P1 y 95.824 en P2
D09Análisis mensual solo de enero de 2017 a agosto de 2018Cobertura temporal despareja en 2016 y a fines de 2018
D11Facturación medida con price y no con payment_valueEl pago incluye cuotas e intereses y está a nivel de pedido
  • Umbrales: 0% para unicidad, validez, consistencia, integridad y cobertura (cada caso se revisa). 5% para completitud, que es el criterio del curso. 1% con tolerancia de 1 BRL para pago contra precio + flete. Resultado: 49 reglas, 36 OK y 13 para revisar.
  • Outliers de precio y flete (7,5% y 10,8% por IQR): se tratan como la cola de una distribución asimétrica y no se eliminan.
  • Faltantes: la fecha de entrega es MAR respecto del estado (ausencia con significado, no se imputa). review_comment_message falta 23,5% con puntaje 1 y 64,1% con puntaje 5, así que es MAR respecto del puntaje o posiblemente MNAR: no se imputa y se usa el indicador tiene_comentario.
  • P2: los pedidos a tiempo tienen puntaje promedio 4,29 y los que llegaron tarde 2,27. Es una asociación, no prueba causalidad.
  • Falta para la final: contrato y controles ejecutables (Pandera), comparación de antes y después, integrar sellers y geolocation, P3 a grano ítem, prueba con 3 o más reglas violadas y conclusiones de uso.

Gráficos paso a paso ★ los dos ejercicios

Los dos ejercicios piden gráficos y se evalúa que respondan una pregunta, no que existan. Todo el código de acá está probado y solo usa matplotlib y pandas.

La receta, siempre igual

  1. Escribí la pregunta que el gráfico tiene que responder ("¿el monto promedio cambia entre regiones?"). Si no podés escribirla, el gráfico sobra.
  2. Preparate el dato: una serie (para distribuciones) o una tabla ya agregada con groupby (para comparaciones). El gráfico no calcula nada.
  3. Elegí el tipo con la tabla de abajo.
  4. Rotulá: título con la población (n=…), eje x y eje y con unidades. Ordená las barras y rotá las etiquetas si no entran.
  5. Interpretá abajo, en una celda de texto: qué se ve, qué decisión provoca y qué no se puede concluir.
Si la pregunta es…GráficoDato que le pasás
¿Cómo se reparte esta variable numérica? ¿Es simétrica o tiene cola?HistogramaUna serie numérica
¿Hay valores extremos? ¿Cómo se compara una numérica entre grupos?BoxplotUna serie, o una numérica + una categórica
¿Cómo se compara un total o un promedio entre categorías?Barras (horizontales si los nombres son largos)Tabla de groupby().agg()
¿Cómo se relacionan dos variables numéricas?ScatterDos columnas numéricas
¿Cómo evoluciona esto en el tiempo?Líneagroupby por fecha, ordenado
¿Qué cambió después de imputar o escalar?Dos histogramas con los mismos binsSerie antes y serie después

1 · Histograma — distribución de una variable

Cuándo: primer paso del diagnóstico, para ver forma, rango, asimetría y candidatos a extremos.

s = autos['Kilometraje'].dropna()

fig, ax = plt.subplots(figsize=(7,4))
ax.hist(s, bins=30, edgecolor='white')
ax.set_title(f'Distribución de Kilometraje (n={len(s)} autos con dato)')
ax.set_xlabel('Kilometraje (km)'); ax.set_ylabel('Cantidad de autos')
plt.tight_layout(); plt.show()

Qué escribir: "La distribución es asimétrica a la derecha, con la mayoría entre X e Y y una cola hasta Z. Eso justifica imputar con la mediana y usar RobustScaler. Quedan afuera los N autos sin dato, que no aparecen en el gráfico."

2 · Boxplot — extremos y comparación entre grupos

Cuándo: para mostrar la regla IQR, o para comparar una numérica entre categorías.

# una sola variable
fig, ax = plt.subplots(figsize=(5,4))
ax.boxplot(autos['Precio'].dropna())
ax.set_title(f'Precio (n={autos["Precio"].notna().sum()} autos)')
ax.set_ylabel('Precio (pesos)')
plt.tight_layout(); plt.show()

# por grupo, con pandas (evita problemas de versión de matplotlib)
ax = autos.boxplot(column='Precio', by='Marca', figsize=(7,4), grid=False)
ax.set_title(f'Precio por marca (n={len(autos)} autos)'); plt.suptitle('')
ax.set_xlabel('Marca'); ax.set_ylabel('Precio (pesos)')
plt.tight_layout(); plt.show()

Qué escribir: "Los puntos por encima del bigote son el X% de las filas. Son valores posibles en el dominio (autos nuevos y caros), así que los mantengo y elijo un escalado robusto en lugar de borrarlos."

3 · Barras — comparar un agregado entre categorías

Cuándo: es el gráfico que pide el Ejercicio 1, después del groupby.

res = (m.groupby('region', dropna=False)
        .agg(pedidos=('pedido_id','count'), monto_prom=('monto','mean'))
        .sort_values('monto_prom'))
etiq = ['Sin región' if pd.isna(i) else i for i in res.index]   # el NaN también se nombra

fig, ax = plt.subplots(figsize=(7,4))
ax.barh(etiq, res['monto_prom'])
ax.set_title(f'Monto promedio por región (n={len(m)} pedidos)')
ax.set_xlabel('Monto promedio (pesos)'); ax.set_ylabel('Región')
for y, v in enumerate(res['monto_prom']):
    ax.text(v, y, f' {v:,.0f}', va='center')      # el valor al lado de la barra
plt.tight_layout(); plt.show()
Dos cuidadosUn promedio no muestra cuántos casos hay detrás: poné el n de cada grupo en la tabla o en la etiqueta, porque un grupo con 5 filas no se compara con uno de 300. Y si un grupo es "sin categoría", nombralo en el eje en lugar de dejar "nan".

4 · Scatter — relación entre dos numéricas

Cuándo: para mostrar una relación, o para detectar puntos raros que solo se ven en dos dimensiones.

d = autos[['Kilometraje','Precio']].dropna()

fig, ax = plt.subplots(figsize=(7,4))
ax.scatter(d['Kilometraje'], d['Precio'], s=12, alpha=.5)
ax.set_title(f'Precio vs Kilometraje (n={len(d)} autos)')
ax.set_xlabel('Kilometraje (km)'); ax.set_ylabel('Precio (pesos)')
plt.tight_layout(); plt.show()

print(d.corr().round(3))     # el número que acompaña al gráfico

Qué escribir: "Se ve una relación negativa (r = −0,X): a más kilómetros, menor precio. Es una asociación, no una prueba de causa, y hay mucha dispersión, así que el kilometraje solo no explica el precio."

Si dos variables dan una correlación cercana a 1, sospechá: puede ser la misma información dos veces, o una columna calculada a partir de la otra (leakage).

5 · Línea — evolución en el tiempo

Cuándo: cobertura temporal, estacionalidad, o para mostrar que faltan períodos.

serie = m.groupby(m['fecha_pedido'].dt.date)['monto'].sum()   # o .size() para cantidad

fig, ax = plt.subplots(figsize=(8,4))
ax.plot(serie.index, serie.values, marker='o')
ax.set_title(f'Monto total por día ({serie.index.min()} a {serie.index.max()})')
ax.set_xlabel('Fecha'); ax.set_ylabel('Monto total (pesos)')
plt.xticks(rotation=45, ha='right'); plt.tight_layout(); plt.show()

Por mes: m.groupby(m['fecha_pedido'].dt.to_period('M'))['monto'].sum(). Si el primer o el último período están casi vacíos, decilo: suele ser el arranque de la operación o el corte de la extracción, y conviene excluirlos de las comparaciones mensuales.

Bonus · Antes y después (lo pide el Ejercicio 2)

antes   = autos['Kilometraje']
despues = pd.Series(SimpleImputer(strategy='median')
                    .fit_transform(autos[['Kilometraje']]).ravel())

bins = np.histogram_bin_edges(pd.concat([antes.dropna(), despues]), bins=18)   # MISMOS bins
fig, axes = plt.subplots(1, 2, figsize=(10,4), sharex=True, sharey=True)
axes[0].hist(antes.dropna(), bins=bins, edgecolor='white')
axes[1].hist(despues, bins=bins, edgecolor='white', color='orange')
axes[0].set_title(f'Antes · n={antes.notna().sum()}')
axes[1].set_title(f'Después de imputar · n={len(despues)}')
for a in axes: a.set_xlabel('Kilometraje (km)'); a.set_ylabel('Cantidad')
plt.tight_layout(); plt.show()

Qué escribir: "Después de imputar no quedan faltantes y aparece un pico artificial en la mediana: la imputación completó las filas pero no recuperó la información que faltaba, y además achicó la varianza. Si el faltante no fuera MCAR, ese pico además introduce sesgo."

Detalles que suman

  • plt.tight_layout() antes de show(), siempre.
  • Etiquetas largas: plt.xticks(rotation=45, ha='right') o pasate a barh.
  • Cola muy larga: ax.set_yscale('log') o set_xscale('log'), y aclaralo en el título.
  • Guardar: plt.savefig('results/visualizaciones/01_monto_region.png', dpi=150, bbox_inches='tight').
  • Un gráfico por pregunta. Mejor tres gráficos que respondan algo que diez decorativos.

Si te preguntan… paso a paso ★ mini-lab

Cada consigna típica, con los pasos, el código mínimo y una plantilla de respuesta para completar con tus números. Salen del simulacro y de la consigna del parcial.

1 · "Integrá las fuentes conservando todas las filas, sin multiplicarlas. Mostrá controles."

  1. Guardá n0 = len(tabla_principal).
  2. Chequeá unicidad de las claves y que los tipos coincidan.
  3. how='left' desde la tabla de hechos, con validate='many_to_one' e indicator.
  4. assert len(resultado) == n0 y mostrá el value_counts() del indicador.
  5. Listá las claves que no encontraron pareja.

Respuesta modelo: "Uso left join desde pedidos porque la unidad de observación es un pedido y no quiero perder ninguno. validate='many_to_one' comprueba que la clave sea única en el catálogo y el assert confirma que sigo con N filas. Del indicador surge que K filas quedaron como left_only, correspondientes a las claves [ … ]."

2 · "Mostrá cantidad y promedio por categoría, incluyendo los que no tienen categoría."

  1. groupby('region', dropna=False) — sin eso, los sin categoría desaparecen del resumen.
  2. Agregá cantidad y promedio juntos (y la mediana si hay outliers).
  3. Ordená por lo que querés comparar y graficá (sección de gráficos, punto 3).
  4. Nombrá el grupo NaN como "sin región" en el gráfico.

Respuesta modelo: "El grupo 'sin región' tiene K pedidos, que no son un error sino pedidos de sucursales que no están en el catálogo. Los dejo visibles porque excluirlos cambiaría el total y ocultaría un problema de integridad entre fuentes."

3 · "¿La fuente X permite clasificar el período? ¿Qué información falta?"

  1. Compará las coberturas: min() y max() de la fecha en cada tabla.
  2. Cruzá por fecha con indicator y contá cuántas filas encontraron pareja.
  3. Si no hay coincidencias o son parciales, decí qué falta exactamente: el calendario del año o del rango que sí cubren tus datos.
  4. No rellenes los NaN con False: "no informado" no es "no es feriado".
print(pedidos['fecha_pedido'].min(), pedidos['fecha_pedido'].max())
print(feriados['date'].min(), feriados['date'].max())
print(m['cruce_feriado'].value_counts())

Respuesta modelo: "La fuente de feriados cubre [período] y los pedidos son de [otro período], así que solo K de N pedidos encuentran fecha. Con esta evidencia no se puede clasificar feriado contra día común; haría falta el calendario del período de los pedidos. Dejo la columna como 'no informado' en lugar de asumir que no son feriados."

4 · "Diagnosticá faltantes, escalas y valores extremos."

  1. Faltantes: df.isna().mean()*100 y, para cada columna con faltantes, si el porcentaje cambia entre grupos (MCAR contra MAR).
  2. Escalas: describe().T mirando min, max y el cociente entre ellos.
  3. Extremos: IQR o z-score, más un boxplot o un histograma.
  4. Valores centinela: mirá si hay ceros o mínimos repetidos que en realidad significan "sin dato" (un kilometraje 0, un monto que siempre es el mismo mínimo).

Respuesta modelo: "La columna A tiene X% de faltantes y además N ceros exactos, que probablemente sean 'no informado' cargado como cero: los trato aparte y lo documento. Las escalas van de [rango] a [rango], una diferencia de varios órdenes, así que hay que escalar antes de cualquier método basado en distancias. Por IQR hay un Y% de extremos, todos dentro de lo posible en el dominio, así que no los elimino."

5 · "¿Qué columnas se pueden usar antes del evento? ¿Cuáles excluirías?"

  1. Preguntate, columna por columna: ¿este dato existe en el momento en que quiero predecir?
  2. Sospechá de cualquier columna con correlación altísima con el target.
  3. Excluí las que se calculan después del evento o a partir del target, y decilo explícitamente.
autos[['Kilometraje','Potencia','Anio','Precio','TasacionSeguro']].corr()['Precio'].round(3)

Respuesta modelo: "Uso Marca, Año, Kilometraje y Potencia, que se registran antes de la venta. Excluyo la tasación del seguro: se incorpora después del cierre y toma el precio como insumo (su correlación con el precio es de casi 1). Incluirla daría un resultado excelente en el papel y sin valor real, porque en el momento de estimar el precio esa columna no existe. También excluyo el propio Precio, que es el objetivo."

6 · "Construí un preprocesamiento para una categórica y dos numéricas."

  1. Definí las listas num y cat a partir del diagnóstico, no al azar.
  2. Numéricas: SimpleImputer (mediana si hay cola o extremos) + scaler (Robust si hay extremos, Standard en el caso general).
  3. Categórica: OneHotEncoder(handle_unknown='ignore'), que no impone orden.
  4. Todo junto en ColumnTransformer. Justificá cada elección en una celda de texto.

Respuesta modelo: "Imputo con la mediana porque la distribución es asimétrica y la media se corre con los extremos. Escalo con RobustScaler porque hay outliers reales que no quiero eliminar y no quiero que definan la escala. Codifico la marca con one-hot para no inventar un orden entre marcas, con handle_unknown='ignore' para que una marca nueva no rompa la transformación."

7 · "Ajustá con referencia, transformá ambos lotes y explicá cómo evitaste aprender con el lote nuevo."

  1. fit_transform solo con referencia (o train); transform con el lote nuevo.
  2. Controles: mismas filas, mismas columnas, cero nulos en la salida.
  3. Evidencia: mostrá los parámetros aprendidos (medianas, centros) y que son los de referencia.
print(prep.named_transformers_['num'].named_steps['imp'].statistics_)   # medianas de referencia
print(referencia['Kilometraje'].median())                                # coinciden
assert len(X_new) == len(nuevos) and X_ref.columns.equals(X_new.columns)

Respuesta modelo: "Llamo fit una sola vez y con referencia. Las medianas y los centros guardados en el preprocesador coinciden con los de referencia, lo que muestra que el lote nuevo no participó del ajuste: solo se le aplicaron las reglas. Si hubiera ajustado con los dos lotes juntos, la métrica quedaría optimista porque el preprocesamiento habría visto datos que en producción todavía no existen."

8 · "Mostrá el antes y después y explicá qué no queda resuelto."

  1. Serie antes (con NaN) y serie después (imputada o escalada), con los mismos bins.
  2. Contá el n de cada lado en el título.
  3. Explicá el cambio y, sobre todo, el límite.

Respuesta modelo: "Después del tratamiento no quedan nulos y la escala pasa a [rango]. Aparece un pico en la mediana, que es artificial. Lo que la transformación no resuelve: la información que faltaba sigue sin estar, la varianza quedó más chica de lo real y, si el mecanismo de los faltantes no es MCAR, la imputación mete sesgo. Escalar tampoco corrige sesgos de cómo se generaron los datos."

9 · "Compará dos preparaciones y justificá la elección con una evaluación válida."

  1. Armá A y B cambiando una sola cosa (el imputador o el scaler).
  2. Mismo modelo, mismos folds (KFold(shuffle=True, random_state=…)), misma métrica, todo dentro de un Pipeline.
  3. Reportá media ± desvío y compará contra un baseline (DummyRegressor).
  4. Si la diferencia es menor que el desvío, decilo y elegí por otro criterio.

Respuesta modelo: "A da MAE X ± Y y B da MAE Z ± W, con el mismo modelo y los mismos folds sobre train; el baseline que predice la media da MAE B. Elijo [A/B] porque [la diferencia supera el desvío / son equivalentes y B es más robusta a los extremos]. La evaluación es válida porque la preparación va dentro del Pipeline y se reajusta en cada fold, así que ningún fold de validación participó del ajuste."

10 · "¿Qué tipo de join usarías y por qué?"

Left desde la tabla de hechos si querés conservar la unidad de observación (lo habitual). Inner solo si justificás perder filas. Outer si el objetivo es auditar qué falta de cada lado. Respuesta modelo: "Left, porque cada fila es un pedido y un pedido sin sucursal sigue siendo un pedido; con inner perdería K filas sin dejar rastro."

11 · "¿Es MCAR, MAR o MNAR?"

  1. Calculá el % de faltantes por grupo de otra variable. Si cambia mucho, no es MCAR.
  2. Pensá el proceso: ¿el dato falta porque la etapa no ocurrió, porque depende de otra variable, o por su propio valor?
  3. MAR y MNAR no se distinguen solo con los datos: usá el conocimiento del dominio y decilo así.

Respuesta modelo: "El faltante se concentra en [grupo] ([x%] contra [y%]), así que descarto MCAR. Por cómo se genera el dato lo trato como MAR respecto de [variable] e imputo condicionando. No puedo descartar MNAR solo con los datos, así que agrego además una bandera de faltante."

12 · "¿Eliminarías estos outliers?"

Respondé en tres pasos: cuántos son (%), si son posibles en el dominio, y qué hacés. Respuesta modelo: "Son el X% por IQR. Son valores posibles (autos de alta gama), así que los mantengo: superar un límite estadístico no vuelve inválido a un dato. Para que no dominen la escala uso RobustScaler. Si fueran imposibles —un kilometraje negativo— los marcaría como inválidos y los excluiría, documentando cuántas filas."

13 · Preguntas de cierre ("¿cuántas filas quedaron sin pareja y qué significa?")

Siempre con el mismo esqueleto: número → causa probable → consecuencia para el informe. Ejemplo: "K pedidos quedaron sin sucursal (sucursales [ids] que no están en el catálogo) y ningún pedido encontró fecha en el calendario. Para el informe significa que los totales por región tienen un grupo 'sin región' que hay que mostrar, y que no se puede analizar el efecto de los feriados con estas fuentes."

Patrón general para cualquier pregunta abiertaEvidencia (número o gráfico) → decisión que tomás → alternativa que descartaste y por qué → qué queda fuera del alcance de lo que mostraste.

Código extra ★ lo que no está en el resto

Pandas y sklearn que no aparecen en las otras secciones y que te puede hacer falta. Todo probado.

1 · Columnas derivadas

a = autos.copy()
a['antiguedad']  = 2026 - a['Anio']
a['km_por_anio'] = a['Kilometraje'] / a['antiguedad'].replace(0, np.nan)
a['km_faltante'] = a['Kilometraje'].isna().astype(int)        # bandera
a['segmento']    = np.where(a['Precio'] > a['Precio'].median(), 'caro', 'barato')
a['region']      = a['sucursal'].map({'Sucursal_1': 'Este'})  # recodificar
a['Marca']       = a['Marca'].replace({'VOLKSWAGEN': 'VW'})   # unificar categorías

2 · Filtrar y dejar la población por escrito

pob = a[(a['Anio'] >= 2015) & (a['Marca'].isin(['Ford','VW'])) & a['Precio'].between(10000, 30000)]
print(f'población: {len(pob)} de {len(a)} filas')

a.query("Anio >= 2015 and Marca in ['Ford','VW']")   # lo mismo, más corto
a[~a['Marca'].isin(['Ford'])]                         # ~ niega la condición
Cada tabla o gráfico del informe tiene que decir sobre qué filas se calculó. Imprimir el tamaño de la población es la forma más barata de dejarlo asentado.

3 · Fechas

m['mes']   = m['fecha_pedido'].dt.to_period('M')
m['dia']   = m['fecha_pedido'].dt.day_name()
m['finde'] = m['fecha_pedido'].dt.dayofweek >= 5
m['dias_entrega'] = (m['fecha_entrega'] - m['fecha_pedido']).dt.days
m['fecha'] = m['fecha_pedido'].dt.normalize()    # saca la hora, útil antes de un join

4 · Agregaciones que no están en las otras secciones

g = m.groupby('region', dropna=False).agg(
        size=('monto','size'),        # cuenta TODAS las filas del grupo
        count=('region','count'))     # NO cuenta los NaN

pd.pivot_table(m, index='region', columns='dia', values='monto', aggfunc='mean')
pd.crosstab(a['Marca'], a['segmento'], normalize='index').round(2)
a['Marca'].value_counts(normalize=True, dropna=False).round(3)   # proporciones
a.nlargest(5, 'Precio')                                          # top N
res.reset_index()                                                # el índice pasa a columna
size contra countEn el grupo "sin región", count sobre esa misma columna da 0 porque no cuenta NaN, y parece que el grupo está vacío. Para contar filas usá size, o contá una columna que sí tenga dato (('pedido_id','count')).

5 · Agrupar una numérica en rangos

a['rango_km'] = pd.cut(a['Kilometraje'], bins=[0, 50000, 100000, 150000, np.inf],
                       labels=['0-50k','50-100k','100-150k','150k+'],
                       include_lowest=True)     # sin esto, los ceros quedan NaN
a['cuartil'] = pd.qcut(a['Precio'], 4, labels=['Q1','Q2','Q3','Q4'])   # grupos del mismo tamaño
a['rango_km'].value_counts(dropna=False)
La trampa del cutSin include_lowest=True el borde inferior queda afuera: los valores exactamente iguales al primer corte se vuelven NaN y parece que faltan más datos de los que faltan. Verificalo comparando con isna().sum() de la columna original.

6 · Pipeline: lo que le falta al de la sección 7

from sklearn.compose import make_column_selector

prep = ColumnTransformer([
    ('num', Pipeline([('imp', SimpleImputer(strategy='median')),
                      ('sc',  StandardScaler())]),
             make_column_selector(dtype_include=np.number)),
    ('cat', Pipeline([('imp', SimpleImputer(strategy='most_frequent')),   # imputa la categórica
                      ('oh',  OneHotEncoder(handle_unknown='ignore',
                                            sparse_output=False))]),
             make_column_selector(dtype_include=object)),
]).set_output(transform='pandas')        # devuelve DataFrame con nombres, no array

X_ref = prep.fit_transform(referencia[cols])   # ya es DataFrame
X_ref.head()
  • Imputar también la categórica: el pipeline de la sección 7 solo imputa las numéricas, así que un NaN en Marca lo rompe.
  • set_output(transform='pandas') te ahorra el pd.DataFrame(..., columns=get_feature_names_out()).
  • Si usás set_output, el OneHotEncoder necesita sparse_output=False o tira "Pandas output does not support sparse data".
  • make_column_selector elige columnas por tipo, pero revisá qué agarró: puede colar una columna que no debería estar (el target o una posterior al evento).

7 · Duplicados y nombres de columnas

df.columns = df.columns.str.strip().str.lower()          # espacios y mayúsculas
df.duplicated(subset=['Marca','Anio','Kilometraje']).sum()  # duplicados por clave
df = df.drop_duplicates(subset=['pedido_id'], keep='first')
df['Marca'] = df['Marca'].str.strip().str.title()          # categorías con espacios

8 · Guardar y entregar desde Colab

from pathlib import Path
Path('results/visualizaciones').mkdir(parents=True, exist_ok=True)
resumen.to_csv('results/resumen_region.csv', index=False)
plt.savefig('results/visualizaciones/01_region.png', dpi=150, bbox_inches='tight')

from google.colab import files
files.upload()                       # subir los CSV
files.download('mi_parcial.ipynb')   # bajar el cuaderno ejecutado

pd.set_option('display.max_columns', None)    # que no te corte las tablas
pd.set_option('display.width', 140)
Si tuvieras que quedarte con tresDerivadas y filtros con la población escrita · size contra count en el groupby · el pipeline con la categórica imputada y set_output('pandas').

Pares que se confunden ★ cuestionario

El cuestionario vive de estos pares. La columna de la derecha es la frase que los separa.

ParCómo los separás en una pregunta
inner vs leftInner pierde filas sin avisar; left conserva la unidad de observación y deja NaN. Si la pregunta habla de "no perder pedidos/viajes" → left.
validate vs indicatorvalidate controla la cardinalidad de la clave y falla con un error; indicator solo informa qué filas encontraron pareja.
size vs countsize cuenta todas las filas del grupo; count no cuenta los NaN.
Media vs medianaLa media se corre con los extremos; la mediana no. Cola pesada u outliers → mediana.
MAR vs MNARMAR: la falta depende de otra variable observada (podés condicionar). MNAR: depende del valor que falta (no podés verlo en los datos, lo deducís del proceso).
MCAR vs MARSi el % de faltantes cambia entre grupos, ya no es MCAR.
Imputar vs marcarImputás cuando podés estimar el valor sin sesgar; marcás (bandera o categoría) cuando la ausencia significa algo o sospechás MNAR.
Estandarizar vs normalizarStandard: media 0 y desvío 1, sin rango fijo. MinMax: rango fijo [0,1]. "Normalizar" en la clase se usa como poner todo en la misma escala.
Robust vs StandardRobust usa mediana e IQR → no se mueve con los outliers. Standard usa media y desvío → sí.
fit vs transformfit aprende parámetros (mediana, media, categorías) y solo se hace con train o referencia. transform los aplica.
Leakage de proceso vs de variableDe proceso: hiciste fit antes del split. De variable: usaste una columna que no existe al predecir o que se calcula con el target.
Validación vs testCon validación (o CV sobre train) elegís. El test se toca una sola vez para reportar. Si elegís mirando el test, ese número es de validación.
Data drift vs concept driftData: cambia la distribución de X. Concept: cambia la relación X → y.
DP vs EO vs EOdDP: misma tasa de positivos. EO: misma TPR. EOd: misma TPR y FPR.
Sesgo histórico vs de representaciónHistórico: el mundo que generó los datos ya era desigual. Representación: un grupo aparece sub o sobrerrepresentado en la muestra.
Proxy vs atributo sensibleEl proxy no es el atributo sensible, pero lo transporta (ubicación, código postal). Por eso sacar el atributo no alcanza.
Correlación vs causalidadSi la pregunta dice "demuestra que X causa Y" y solo tenés una asociación observacional, la respuesta correcta es que no lo demuestra.
Accuracy vs métricas por claseCon clases desbalanceadas, la accuracy alta puede ser el modelo que siempre predice la clase mayoritaria.
Outlier vs dato inválidoOutlier: raro pero posible en el dominio. Inválido: imposible (km negativo, puntaje 7 en escala de 1 a 5).
Grano pedido vs grano ítemCambiar el grano cambia los promedios: a grano ítem, un pedido con varios ítems pesa varias veces.

Leer una salida y decir algo ★ los dos ejercicios

Lo que puntúa no es correr la celda, es lo que decís de lo que salió.

Si ves…Qué significaQué escribís
_merge: muchos left_onlyClaves que no están en el catálogo"K filas no encontraron pareja; las conservo como 'sin categoría' porque excluirlas cambiaría el total y ocultaría un problema de integridad."
_merge: cero bothTipos distintos o períodos que no se solapan"Las fuentes no se cruzan en este período, así que esa pregunta no se puede responder con estos datos."
Filas que aumentan tras el joinClave duplicada del lado derecho"Agrego la tabla hija antes de unir para mantener una fila por pedido."
Media >> mediana en describe()Cola larga a la derecha"Distribución asimétrica: imputo con mediana y considero log o RobustScaler."
min = 0 en una variable que no puede ser 0Centinela: "sin dato" cargado como cero"Trato los ceros como faltantes y lo documento; son N filas."
max/min de varios órdenes entre columnasProblema de escala"Hay que escalar antes de cualquier método basado en distancias."
Boxplot con muchos puntos arribaCola, no necesariamente errores"Son el X% por IQR y son posibles en el dominio: los mantengo y uso escalado robusto."
% de faltantes muy distinto entre gruposNo es MCAR"Lo trato como MAR respecto de [variable] e imputo condicionando."
Histograma con un pico nuevo tras imputarEl valor imputado"El pico es artificial; la imputación completó filas pero no recuperó información y achicó la varianza."
MAE de A y B que difieren menos que el desvíoEmpate estadístico"No hay ganador claro: elijo B por robustez a los extremos, no por la métrica."
Métrica mucho mejor que el baseline… sospechosamentePosible leakage"Reviso si alguna columna es posterior al evento o deriva del target."
Correlación ≈ 1 entre dos columnasInformación duplicada o derivada"Una se calcula a partir de la otra: excluyo la posterior al evento."

Números y umbrales para recordar

De la materia

  • Completitud: umbral del curso 5% de faltantes.
  • IQR: fuera de Q1 − 1,5·IQR y Q3 + 1,5·IQR.
  • Z-score: |z| > 3.
  • Eliminar outliers: error claro, < 5% e impacto bajo.
  • Sesgo fuerte: |skew| > 2 → log.
  • Split típico: 80/20; CV de 5 folds.
  • Unicidad, validez, consistencia, integridad, cobertura: umbral 0%, cada caso se revisa.

Del parcial

  • 40 min cuestionario · 80 min cuaderno · 5 min entrega.
  • ~20 preguntas, 50 puntos, sin descuento por error.
  • Ej. 1: 25 pts · Ej. 2: 25 pts.
  • El parcial vale el 25% del curso.
  • Clases que entran: 1 a 6.
Si te preguntan por un umbral que no recordás, respondé con el criterio: "depende del contexto, el curso usa 5% para completitud y 0% para las reglas estructurales, y cada caso raro se revisa antes de decidir".

P1 y P2 de UT1, en diez líneas

Las dos prácticas de UT1 que no se detallan en el resto de la guía. Entran en el cuestionario.

P1 · Iris (60 min)

  • Dataset chico y limpio: 150 filas, 4 medidas (largo y ancho de sépalo y pétalo) y la especie.
  • Se evalúa: datos y fuentes, perfilado, visualizaciones e insights.
  • Sirve para practicar el flujo: cargar → perfilar → graficar → escribir hallazgos.

P2 · Netflix (75–90 min)

  • EDA con pandas sobre el catálogo de Netflix.
  • Se evalúa: metodología de Google, validación de expectativas, visualizaciones con interpretación, y ydata-profiling con reflexión.
  • Validar expectativas = escribir antes qué esperás encontrar y después contrastarlo con la evidencia. Es lo mismo que pide el caso de estudio con la predicción.
  • ydata-profiling genera un reporte automático (tipos, faltantes, correlaciones, alertas). Ahorra tiempo, pero no decide: las decisiones y la interpretación siguen siendo tuyas.

Artefactos mínimos de UT1 (por si preguntan por la estructura del proyecto): carpeta results/ con perfiles/, visualizaciones/ y reportes/; el notebook corre de punta a punta; al menos 2 visualizaciones útiles, 3 insights, un diccionario de datos mínimo y las fuentes documentadas.

Plan B: si algo se rompe o te queda poco tiempo

Errores frecuentes

ErrorCausa típicaSalida
MergeError: Merge keys are not uniqueLa clave se repite del lado que declaraste únicoAgregá la tabla hija antes de unir, o cambiá el validate y justificalo
KeyErrorNombre con espacios o mayúsculas distintasdf.columns = df.columns.str.strip().str.lower()
ValueError: could not convert string to floatUna categórica entró al bloque numéricoRevisá las listas num y cat del ColumnTransformer
Input contains NaNFalta el imputador o una columna quedó afueraEl SimpleImputer va antes del scaler, dentro del Pipeline
Found unknown categoriesOneHotEncoder sin handle_unknownOneHotEncoder(handle_unknown='ignore')
Pandas output does not support sparse dataset_output('pandas') con one-hot dispersoAgregá sparse_output=False
SettingWithCopyWarningEstás escribiendo sobre una vistaTrabajá sobre df = original.copy()
Números que no cierran entre celdasEjecutaste fuera de ordenReiniciar y ejecutar todo antes de entregar

Si te quedan 15 minutos y falta la mitad

  1. Que corra. Comentá la celda rota y dejá escrito en markdown qué querías hacer y por qué: el error puntual no invalida el ejercicio, y la idea puntúa.
  2. Cerrá con números. Un print con las filas conservadas, las que no encontraron pareja y el % de faltantes vale más que un gráfico lindo a medio hacer.
  3. Una conclusión por ejercicio, aunque sea corta: qué permite concluir y qué no.
  4. Contestá las preguntas de cierre: son puntos que no dependen de que el código corra.
  5. Guardá y entregá con las salidas visibles. Un cuaderno entregado a medias puntúa; uno perfecto sin entregar, no.
Antes de entregar, mirá que no hayas hecho estoUsar una columna posterior al evento · fit con todos los datos · groupby sin dropna=False cuando hay grupos sin categoría · rellenar NaN con False o con 0 sin justificar · borrar outliers solo porque superan el bigote · gráficos sin título, unidades ni población · conclusiones sin el "qué no puedo afirmar".

Reglas de oro para el cuaderno

Lo que más suma en el mini-lab. Leelas el viernes antes de entrar.

Ejercicio 1 · Integración

  1. Revisá la unicidad de cada clave antes de unir (is_unique, duplicated().sum()).
  2. Revisá tipos y rangos de las claves y las fechas (dtypes, min(), max()).
  3. Usá left join desde la tabla de hechos, con validate='many_to_one' e indicator.
  4. assert len(después) == len(antes): la unidad de observación es una fila por viaje o pedido.
  5. Reportá cuántas filas no encontraron pareja y qué claves eran.
  6. Agrupá con dropna=False y graficá con título, unidades y población.
  7. Concluí qué se puede afirmar y qué no. "No informado" no es lo mismo que "falso".

Ejercicio 2 · Preparación

  1. Diagnosticá: % de faltantes, describe(), escalas, boxplots y outliers por IQR.
  2. Revisá qué columnas existen antes del momento de predecir. Excluí las que se calculan con el target o después.
  3. Split o referencia primero. Todo fit se hace solo con train.
  4. Usá Pipeline + ColumnTransformer: imputar, escalar y OneHotEncoder(handle_unknown='ignore').
  5. Compará A contra B con el mismo modelo y la misma CV. Reportá la media ± el desvío y un baseline.
  6. Controles: mismas filas, cero nulos en la salida, mismas columnas en ambos lotes y los parámetros aprendidos (statistics_, center_).
  7. Gráfico de antes y después con los mismos bins. Explicá qué cambió y qué no resuelve la transformación (por ejemplo, la imputación no recupera la información perdida).
Frases que suman puntos"Elegí X porque [evidencia: tabla o gráfico]. La alternativa era Y, que descarté porque… Esto permite concluir… pero no permite afirmar… porque [limitación]."

Autoevaluación

Preguntas de opción múltiple con el estilo del cuestionario. Al elegir una opción ves si acertaste y por qué. El "modo parcial" saca 20 al azar con un cronómetro de 40 minutos.

0 / 0

Flashcards

Tocá una tarjeta para darla vuelta.