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.
| Parte | Tiempo | Puntos | Qué es |
|---|---|---|---|
| 1 · Cuestionario | 40 min | 50 | Unas 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 Colab | 80 min | 50 | Ej. 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 entrega | 5 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.
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 Analyst | Data Engineer | Data Scientist | |
|---|---|---|---|
| Foco | Interpretar datos para encontrar insights y apoyar decisiones | Construir y mantener pipelines e infraestructura de datos | Usar los datos para construir modelos y predecir resultados |
| Skills | SQL, Excel, Power BI/Tableau, estadística básica | Python, herramientas ETL, Big Data, plataformas cloud | Python/R, Machine Learning, estadística, visualización |
| Tareas | Reportes y dashboards, analizar tendencias y patrones | Limpiar y transformar datos crudos, montar data warehouses y data lakes | Crear 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
Es un ciclo: cada etapa puede obligarte a volver a una anterior.
ETL: Extract, Transform, Load
- 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
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
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'):
| Corte | Qué se vio |
|---|---|
| Sexo | Sobrevivió cerca del 74% de las mujeres y cerca del 19% de los hombres |
| Clase | 1ª ≈ 63% · 2ª ≈ 47% · 3ª ≈ 24% |
| Puerto | C (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ñado | Acompañ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 conplt.yscale('log')y comparando porsurvived.age: va de 0,42 a 80. Los puntos por encima de ~65 aparecen como outliers en el boxplot, pero son edades reales.
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)
| id | producto_id | cantidad |
|---|---|---|
| 1 | 1 | 2 |
| 2 | 2 | 1 |
| 3 | 3 | 1 |
| 4 | 1 | 3 |
| id | nombre | precio |
|---|---|---|
| 1 | Laptop | 1000 |
| 2 | Mouse | 25 |
| 5 | Tablet | 500 |
| Tipo | Qué conserva | Cuándo usarlo |
|---|---|---|
inner | Solo las filas con coincidencia en ambos lados | Cuando solo te importan los casos completos. Puede perder filas sin avisar. |
left | Todas 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. |
right | Todas las filas de la derecha | Es un left con las tablas al revés |
outer | Todas las filas de ambos lados | Para 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.
- 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,dateostr), horas incluidas o períodos que no se solapan. Comparámin()ymax()de las dos columnas antes de unir. - Claves con distinto tipo (
intcontrastr): unificalas conastypeantes del merge. - Un NaN después del join no siempre es "falso": puede significar "no informado". No lo rellenes con False sin pensarlo.
groupbydescarta los NaN por defecto: usádropna=Falsepara no esconder filas.
Lo que hace la práctica P3 (NYC Taxi)
- Carga viajes (Parquet, ~3M de filas), zonas (CSV) y calendario (JSON).
- Normaliza: columnas en minúscula, crea
pickup_date = tpep_pickup_datetime.dt.datey revisa que los tipos coincidan. - Optimiza memoria (
int16,int8) y elimina filas sin ubicación. - Hace un left join de viajes con zonas (
pulocationid↔locationid) y cuenta los viajes sin borough. - Hace un left join con el calendario por fecha y crea
is_special_day = special.fillna(False). - Agrupa por borough con
count,mean,stdymedian, y calcula métricas de negocio: ingreso por milla, % de propina y participación de mercado. - 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)
| Tipo | De qué depende que falte | Ejemplo | Qué hacer |
|---|---|---|---|
| MCAR Missing Completely At Random | De nada: es puro azar | Que falte un IQ no depende ni de la edad ni del propio IQ | Imputación simple (media, mediana o moda). Suele estar bien. |
| MAR Missing At Random | De otra variable observada | Los 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. |
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:
ffillobfillcon 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
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 six < 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ón | Cuándo |
|---|---|
| Eliminar | Error claro, menos del 5% de los datos e impacto bajo |
| Transformar | Cuando distorsiona (log, winsorizar o recortar a un límite) |
| Mantener | Cuando lo raro es justamente la señal (fraude, tarifas de primera clase) |
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
| Scaler | Fórmula | Resultado | Cuá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ío | Media 0 y desvío 1, sin rango fijo | El 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) / IQR | Centrado en la mediana, sin rango fijo | Outliers 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
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
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
OneHotEncodercrea 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_scorela 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".
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
| Sesgo | Qué es |
|---|---|
| Histórico | Los datos reflejan políticas o realidades del pasado que no queremos perpetuar |
| Representación / muestreo | Grupos sub- o sobrerrepresentados (selection bias, survivorship bias) |
| Medición | Errores o diferencias sistemáticas al medir o registrar (instrumentos, encuestas, OCR) |
| Etiquetado | Sesgo de quienes anotan o de las guías de rotulado (labeling bias) |
| Proxy | Variables que funcionan como sustitutas de un atributo sensible (la ubicación puede reflejar raza o ingresos) |
| Agregación | Promedios globales que ocultan disparidades entre grupos (falacia ecológica) |
| Algorítmico | El objetivo, la función de pérdida o la búsqueda de performance redistribuyen el error entre grupos |
| Despliegue y feedback | El 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étrica | Qué 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 grupo | Si 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
| Etapa | Qué pide |
|---|---|
| 1. Comprender | Inventario 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. Explorar | Tipos, 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. Diagnosticar | Perfil reproducible con las 7 dimensiones (abajo). Reglas y umbrales justificables. No todo valor raro es un error. |
| 4. Curar y controlar | Versió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 concluir | Probar 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ón | Pregunta | Ejemplo |
|---|---|---|
| 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?
| Tabla | Grano (una fila es…) | Clave |
|---|---|---|
| orders | un pedido (99.441) | order_id |
| items | un ítem de un pedido | order_id + order_item_id |
| payments | un pago de un pedido | order_id + payment_sequential |
| reviews | una reseña asociada a un pedido | review_id + order_id (porque review_id solo se repite) |
| geolocation | una coordenada observada para un prefijo | ninguna |
Predicciones y qué pasó
- 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
deliveredsin fecha de entrega y 23 con la entrega al transportista posterior a la entrega al cliente. - 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_idse 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)
| ID | Decisión | Por qué |
|---|---|---|
| D01 | Los datos crudos quedan en data/raw sin editar; lo transformado va a interim | La consigna prohíbe sobrescribir la fuente |
| D02 | El prefijo de CEP se lee como texto | Es un código: como número pierde los ceros iniciales |
| D03 | to_datetime(errors='coerce') contando los que fallan | Para que un valor inválido no se vuelva NaN sin que nadie lo note |
| D04 | Grano: una fila por pedido | La 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). |
| D05 | Agregar las tablas hijas antes de unir y quedarse con la reseña más reciente, marcando los pedidos con varias | Evita multiplicar pedidos. Se controla con validate y con un assert len == 99.441. |
| D07 | Marcar 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 |
| D08 | P1 y P2 solo con pedidos delivered que tienen fecha de entrega | Se define la población explícitamente: 96.470 pedidos en P1 y 95.824 en P2 |
| D09 | Análisis mensual solo de enero de 2017 a agosto de 2018 | Cobertura temporal despareja en 2016 y a fines de 2018 |
| D11 | Facturación medida con price y no con payment_value | El 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_messagefalta 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 indicadortiene_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
- 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.
- Preparate el dato: una serie (para distribuciones) o una tabla ya agregada con
groupby(para comparaciones). El gráfico no calcula nada. - Elegí el tipo con la tabla de abajo.
- 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. - 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áfico | Dato que le pasás |
|---|---|---|
| ¿Cómo se reparte esta variable numérica? ¿Es simétrica o tiene cola? | Histograma | Una serie numérica |
| ¿Hay valores extremos? ¿Cómo se compara una numérica entre grupos? | Boxplot | Una 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? | Scatter | Dos columnas numéricas |
| ¿Cómo evoluciona esto en el tiempo? | Línea | groupby por fecha, ordenado |
| ¿Qué cambió después de imputar o escalar? | Dos histogramas con los mismos bins | Serie 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()
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."
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 deshow(), siempre.- Etiquetas largas:
plt.xticks(rotation=45, ha='right')o pasate abarh. - Cola muy larga:
ax.set_yscale('log')oset_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."
- Guardá
n0 = len(tabla_principal). - Chequeá unicidad de las claves y que los tipos coincidan.
how='left'desde la tabla de hechos, convalidate='many_to_one'eindicator.assert len(resultado) == n0y mostrá elvalue_counts()del indicador.- 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."
groupby('region', dropna=False)— sin eso, los sin categoría desaparecen del resumen.- Agregá cantidad y promedio juntos (y la mediana si hay outliers).
- Ordená por lo que querés comparar y graficá (sección de gráficos, punto 3).
- 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?"
- Compará las coberturas:
min()ymax()de la fecha en cada tabla. - Cruzá por fecha con
indicatory contá cuántas filas encontraron pareja. - Si no hay coincidencias o son parciales, decí qué falta exactamente: el calendario del año o del rango que sí cubren tus datos.
- 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."
- Faltantes:
df.isna().mean()*100y, para cada columna con faltantes, si el porcentaje cambia entre grupos (MCAR contra MAR). - Escalas:
describe().Tmirandomin,maxy el cociente entre ellos. - Extremos: IQR o z-score, más un boxplot o un histograma.
- 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?"
- Preguntate, columna por columna: ¿este dato existe en el momento en que quiero predecir?
- Sospechá de cualquier columna con correlación altísima con el target.
- 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."
- Definí las listas
numycata partir del diagnóstico, no al azar. - Numéricas:
SimpleImputer(mediana si hay cola o extremos) + scaler (Robust si hay extremos, Standard en el caso general). - Categórica:
OneHotEncoder(handle_unknown='ignore'), que no impone orden. - 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."
fit_transformsolo con referencia (o train);transformcon el lote nuevo.- Controles: mismas filas, mismas columnas, cero nulos en la salida.
- 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."
- Serie antes (con NaN) y serie después (imputada o escalada), con los mismos bins.
- Contá el
nde cada lado en el título. - 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."
- Armá A y B cambiando una sola cosa (el imputador o el scaler).
- Mismo modelo, mismos folds (
KFold(shuffle=True, random_state=…)), misma métrica, todo dentro de unPipeline. - Reportá media ± desvío y compará contra un baseline (
DummyRegressor). - 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?"
- Calculá el % de faltantes por grupo de otra variable. Si cambia mucho, no es MCAR.
- Pensá el proceso: ¿el dato falta porque la etapa no ocurrió, porque depende de otra variable, o por su propio valor?
- 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."
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
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)
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
Marcalo rompe. set_output(transform='pandas')te ahorra elpd.DataFrame(..., columns=get_feature_names_out()).- Si usás
set_output, elOneHotEncodernecesitasparse_output=Falseo tira "Pandas output does not support sparse data". make_column_selectorelige 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)
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.
| Par | Cómo los separás en una pregunta |
|---|---|
inner vs left | Inner 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 indicator | validate controla la cardinalidad de la clave y falla con un error; indicator solo informa qué filas encontraron pareja. |
size vs count | size cuenta todas las filas del grupo; count no cuenta los NaN. |
| Media vs mediana | La media se corre con los extremos; la mediana no. Cola pesada u outliers → mediana. |
| MAR vs MNAR | MAR: 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 MAR | Si el % de faltantes cambia entre grupos, ya no es MCAR. |
| Imputar vs marcar | Imputá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 normalizar | Standard: 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 Standard | Robust usa mediana e IQR → no se mueve con los outliers. Standard usa media y desvío → sí. |
fit vs transform | fit aprende parámetros (mediana, media, categorías) y solo se hace con train o referencia. transform los aplica. |
| Leakage de proceso vs de variable | De 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 test | Con 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 drift | Data: cambia la distribución de X. Concept: cambia la relación X → y. |
| DP vs EO vs EOd | DP: misma tasa de positivos. EO: misma TPR. EOd: misma TPR y FPR. |
| Sesgo histórico vs de representación | Histórico: el mundo que generó los datos ya era desigual. Representación: un grupo aparece sub o sobrerrepresentado en la muestra. |
| Proxy vs atributo sensible | El proxy no es el atributo sensible, pero lo transporta (ubicación, código postal). Por eso sacar el atributo no alcanza. |
| Correlación vs causalidad | Si 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 clase | Con clases desbalanceadas, la accuracy alta puede ser el modelo que siempre predice la clase mayoritaria. |
| Outlier vs dato inválido | Outlier: raro pero posible en el dominio. Inválido: imposible (km negativo, puntaje 7 en escala de 1 a 5). |
| Grano pedido vs grano ítem | Cambiar 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é significa | Qué escribís |
|---|---|---|
_merge: muchos left_only | Claves 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 both | Tipos 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 join | Clave 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 0 | Centinela: "sin dato" cargado como cero | "Trato los ceros como faltantes y lo documento; son N filas." |
max/min de varios órdenes entre columnas | Problema de escala | "Hay que escalar antes de cualquier método basado en distancias." |
| Boxplot con muchos puntos arriba | Cola, 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 grupos | No es MCAR | "Lo trato como MAR respecto de [variable] e imputo condicionando." |
| Histograma con un pico nuevo tras imputar | El 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ío | Empate 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… sospechosamente | Posible leakage | "Reviso si alguna columna es posterior al evento o deriva del target." |
| Correlación ≈ 1 entre dos columnas | Informació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.
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
| Error | Causa típica | Salida |
|---|---|---|
MergeError: Merge keys are not unique | La clave se repite del lado que declaraste único | Agregá la tabla hija antes de unir, o cambiá el validate y justificalo |
KeyError | Nombre con espacios o mayúsculas distintas | df.columns = df.columns.str.strip().str.lower() |
ValueError: could not convert string to float | Una categórica entró al bloque numérico | Revisá las listas num y cat del ColumnTransformer |
Input contains NaN | Falta el imputador o una columna quedó afuera | El SimpleImputer va antes del scaler, dentro del Pipeline |
Found unknown categories | OneHotEncoder sin handle_unknown | OneHotEncoder(handle_unknown='ignore') |
Pandas output does not support sparse data | set_output('pandas') con one-hot disperso | Agregá sparse_output=False |
SettingWithCopyWarning | Estás escribiendo sobre una vista | Trabajá sobre df = original.copy() |
| Números que no cierran entre celdas | Ejecutaste fuera de orden | Reiniciar y ejecutar todo antes de entregar |
Si te quedan 15 minutos y falta la mitad
- 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.
- Cerrá con números. Un
printcon las filas conservadas, las que no encontraron pareja y el % de faltantes vale más que un gráfico lindo a medio hacer. - Una conclusión por ejercicio, aunque sea corta: qué permite concluir y qué no.
- Contestá las preguntas de cierre: son puntos que no dependen de que el código corra.
- Guardá y entregá con las salidas visibles. Un cuaderno entregado a medias puntúa; uno perfecto sin entregar, no.
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
- Revisá la unicidad de cada clave antes de unir (
is_unique,duplicated().sum()). - Revisá tipos y rangos de las claves y las fechas (
dtypes,min(),max()). - Usá left join desde la tabla de hechos, con
validate='many_to_one'eindicator. assert len(después) == len(antes): la unidad de observación es una fila por viaje o pedido.- Reportá cuántas filas no encontraron pareja y qué claves eran.
- Agrupá con
dropna=Falsey graficá con título, unidades y población. - Concluí qué se puede afirmar y qué no. "No informado" no es lo mismo que "falso".
Ejercicio 2 · Preparación
- Diagnosticá: % de faltantes,
describe(), escalas, boxplots y outliers por IQR. - Revisá qué columnas existen antes del momento de predecir. Excluí las que se calculan con el target o después.
- Split o referencia primero. Todo
fitse hace solo con train. - Usá
Pipeline+ColumnTransformer: imputar, escalar yOneHotEncoder(handle_unknown='ignore'). - Compará A contra B con el mismo modelo y la misma CV. Reportá la media ± el desvío y un baseline.
- Controles: mismas filas, cero nulos en la salida, mismas columnas en ambos lotes y los parámetros aprendidos (
statistics_,center_). - 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).
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.
Flashcards
Tocá una tarjeta para darla vuelta.