lección 5
Día 3 — El pipeline completo
Construyes el pipeline raw→silver unificado, escribes tests de validación, abres tu primer PR en GitHub y sobrevives al code review de Elena.
⏱ 60 min
### 9:30 AM — Standup: Elena pregunta por el deadline
Miércoles. El día que marca el deadline de tu ticket DATA-147. Abres Slack con ese cosquilleo de "hoy toca entregar". El standup es breve y directo — Elena va al grano:
1Elena Torres 9:31 AM2Standup — miércoles.3tú, ¿cómo vas con DATA-147? Hoy era el deadline.45Tú 9:33 AM6La ingesta está hecha y la limpieza del verano también.7María me mandó ayer otro CSV (primavera) con formato diferente.8Estoy adaptando el script con un mapping de columnas como me dijiste.9¿El deadline sigue siendo hoy o se extiende por el cambio de scope?1011Elena Torres 9:35 AM12El deadline se mantiene para los datos de verano. Lo de primavera lo13puedes entregar mañana por la mañana. Pero necesito que hoy dejes la14tabla silver de verano LISTA y con tests básicos. ¿Puedes?1516Tú 9:36 AM17Sí, para las 14:00 debería estar.
Elena es directa: el deadline se mantiene, pero el scope se ajusta
Respiras. El scope se ha acotado a lo razonable: verano hoy con tests, primavera mañana. Esto es algo que aprenderás a lo largo de tu carrera: cuando los requisitos crecen a mitad de sprint, la conversación no es "no puedo" sino "¿cuál es la prioridad real?".
Consejo de senior: NUNCA aceptes un deadline sin negociar el scope. Si te piden más trabajo con el mismo plazo, estás aceptando hacer horas extra o entregar con menos calidad. Ambas opciones son malas. La negociación sana es: "Puedo entregar A para el miércoles o A+B para el jueves. ¿Qué prefieres?" Elena te enseñó esto sin decírtelo explícitamente.
### 10:00 AM — Tarea principal: el pipeline completo raw → silver
Hasta ahora has explorado datos, subido archivos a raw y escrito funciones de limpieza sueltas. Hoy toca ensamblar todo en un PIPELINE — un flujo completo que toma los datos crudos y produce datos limpios, validados y listos para consumir. Es como pasar de cocinar ingredientes sueltos a tener la receta completa documentada.
La estructura que vas a crear en el repositorio freshmart-data es profesional. No es un notebook desordenado ni un script monolítico de 500 líneas — es código modular que otro ingeniero pueda leer, ejecutar y mantener sin preguntarte.
1freshmart-data/2├── pipelines/3│ └── campaign_ingestion/4│ ├── __init__.py5│ ├── config.py # Mappings, constantes, enums6│ ├── clean.py # Funciones de limpieza7│ ├── validate.py # Validaciones de calidad8│ ├── run.py # Orquestación del pipeline9│ └── tests/10│ ├── __init__.py11│ ├── test_clean.py12│ └── test_validate.py13├── data/14│ ├── raw/ # CSVs originales (no se tocan)15│ └── silver/ # Parquet limpios (output)16└── README.md
Estructura profesional: cada archivo tiene UNA responsabilidad
### El archivo de configuración: config.py
El primer archivo que escribes es la configuración. Aquí van los mappings de columnas, las constantes y todo lo que cambia entre campañas. La idea es que cuando Marketing mande la campaña de Navidad, solo tengas que añadir una entrada aquí — no tocar la lógica de limpieza.
1# pipelines/campaign_ingestion/config.py2"""Configuración centralizada del pipeline de campañas."""3from enum import Enum4from pathlib import Path567class Campaign(Enum):8 """Campañas conocidas. Usar este Enum evita typos silenciosos."""9 VERANO = "verano"10 PRIMAVERA = "primavera"111213# Columnas obligatorias después del mapping14REQUIRED_COLUMNS = [15 "venta_id", "cliente_id", "producto_id", "fecha_compra",16 "cantidad", "precio", "descuento_pct", "tienda_id", "campana",17]1819# Mapping de columnas por campaña (origen → destino estándar)20COLUMN_MAPPINGS: dict[Campaign, dict[str, str]] = {21 Campaign.VERANO: {}, # Ya viene con nombres correctos22 Campaign.PRIMAVERA: {23 "id_transaccion": "venta_id",24 "id_cliente": "cliente_id",25 "id_producto": "producto_id",26 "fecha_venta": "fecha_compra",27 "unidades": "cantidad",28 "importe": "precio",29 "descuento": "descuento_pct",30 "almacen": "tienda_id",31 "promocion": "campana",32 },33}3435# Configuración específica de limpieza por campaña36CAMPAIGN_CONFIG: dict[Campaign, dict] = {37 Campaign.VERANO: {38 "precio_es_string": True,39 "fechas_mixtas": True,40 "anonimo_valor": None, # NULL = anónimo41 },42 Campaign.PRIMAVERA: {43 "precio_es_string": False,44 "fechas_mixtas": False,45 "anonimo_valor": -1, # -1 = anónimo46 },47}4849# Rutas50RAW_DIR = Path("data/raw")51SILVER_DIR = Path("data/silver")5253# Rango válido de fechas por campaña54DATE_RANGES: dict[Campaign, tuple[str, str]] = {55 Campaign.VERANO: ("2024-06-15", "2024-09-15"),56 Campaign.PRIMAVERA: ("2024-03-01", "2024-05-31"),57}
config.py — Todo lo configurable en un solo sitio. Añadir campaña = añadir entrada.
### Las funciones de limpieza: clean.py
El módulo de limpieza contiene funciones puras: reciben un DataFrame, lo transforman y lo devuelven. No leen archivos, no escriben a disco, no tienen efectos secundarios. Esto las hace fáciles de testear.
1# pipelines/campaign_ingestion/clean.py2"""Funciones de limpieza para datos de campañas de Marketing."""3import logging45import pandas as pd67from .config import Campaign, COLUMN_MAPPINGS, CAMPAIGN_CONFIG, REQUIRED_COLUMNS89logger = logging.getLogger(__name__)101112def apply_column_mapping(df: pd.DataFrame, campaign: Campaign) -> pd.DataFrame:13 """Renombra columnas según el mapping de la campaña."""14 mapping = COLUMN_MAPPINGS[campaign]15 if mapping:16 df = df.rename(columns=mapping)17 logger.info(f" Mapping aplicado: {len(mapping)} columnas renombradas")1819 # Verificar que todas las columnas obligatorias existen20 missing = set(REQUIRED_COLUMNS) - set(df.columns)21 if missing:22 raise ValueError(23 f"Columnas obligatorias ausentes después del mapping: {missing}"24 )25 return df262728def clean_dates(df: pd.DataFrame, campaign: Campaign) -> pd.DataFrame:29 """Normaliza la columna fecha_compra a datetime."""30 config = CAMPAIGN_CONFIG[campaign]31 try:32 if config["fechas_mixtas"]:33 df["fecha_compra"] = pd.to_datetime(34 df["fecha_compra"], format="mixed", dayfirst=True35 )36 else:37 df["fecha_compra"] = pd.to_datetime(38 df["fecha_compra"], dayfirst=True39 )40 except Exception as e:41 # No dejar que una fecha rota tumbe el pipeline entero42 logger.warning(f" Fechas no parseables encontradas: {e}")43 df["fecha_compra"] = pd.to_datetime(44 df["fecha_compra"], format="mixed", dayfirst=True, errors="coerce"45 )46 nulas = df["fecha_compra"].isna().sum()47 logger.warning(f" {nulas} fechas convertidas a NaT")4849 return df505152def clean_prices(df: pd.DataFrame, campaign: Campaign) -> pd.DataFrame:53 """Normaliza la columna precio a float."""54 config = CAMPAIGN_CONFIG[campaign]55 if config["precio_es_string"]:56 df["precio"] = (57 df["precio"]58 .astype(str)59 .str.replace("€", "", regex=False)60 .str.replace(",", ".")61 .astype(float)62 )63 logger.info(" Precios limpiados: eliminado €, coma → punto")64 else:65 df["precio"] = df["precio"].astype(float)66 return df676869def mark_anonymous(df: pd.DataFrame, campaign: Campaign) -> pd.DataFrame:70 """Marca ventas anónimas con una columna booleana."""71 config = CAMPAIGN_CONFIG[campaign]72 anonimo_valor = config["anonimo_valor"]7374 if anonimo_valor is None:75 df["es_anonimo"] = df["cliente_id"].isna()76 else:77 df.loc[df["cliente_id"] == anonimo_valor, "cliente_id"] = None78 df["es_anonimo"] = df["cliente_id"].isna()7980 n_anonimos = df["es_anonimo"].sum()81 logger.info(f" Ventas anónimas: {n_anonimos:,}")82 return df838485def remove_duplicates(df: pd.DataFrame) -> pd.DataFrame:86 """Elimina filas 100% duplicadas."""87 antes = len(df)88 df = df.drop_duplicates()89 eliminados = antes - len(df)90 if eliminados > 0:91 logger.info(f" Duplicados eliminados: {eliminados:,}")92 return df939495def filter_invalid_rows(df: pd.DataFrame) -> pd.DataFrame:96 """Filtra filas con cantidad <= 0 (errores de la app)."""97 antes = len(df)98 df = df[df["cantidad"] > 0]99 eliminados = antes - len(df)100 if eliminados > 0:101 logger.info(f" Filas con cantidad <= 0 eliminadas: {eliminados}")102 return df103104105def run_cleaning(df: pd.DataFrame, campaign: Campaign) -> pd.DataFrame:106 """Ejecuta el pipeline de limpieza completo."""107 logger.info(f"Limpiando campaña: {campaign.value} ({len(df):,} filas)")108109 df = apply_column_mapping(df, campaign)110 df = remove_duplicates(df)111 df = clean_dates(df, campaign)112 df = clean_prices(df, campaign)113 df = mark_anonymous(df, campaign)114 df = filter_invalid_rows(df)115116 logger.info(f" Resultado: {len(df):,} filas limpias")117 return df
clean.py — Funciones puras, composables y fáciles de testear
### Tests de validación: validate.py
Aquí es donde separamos a los juniors de los profesionales. Cualquiera puede escribir un script que limpie datos. Pero un ingeniero de datos de verdad escribe VALIDACIONES que verifican que el resultado es correcto. Es como el inspector de calidad al final de la línea de producción: antes de que el producto salga de la fábrica, alguien comprueba que no tiene defectos.
Las validaciones que vas a implementar cubren las 5 dimensiones clásicas de calidad de datos: completitud (¿hay NULLs donde no debería?), unicidad (¿hay duplicados?), validez (¿los valores tienen sentido?), consistencia (¿las fechas están en rango?) y frescura (¿son datos recientes?). No necesitas un framework externo para empezar — Python puro es suficiente.
1# pipelines/campaign_ingestion/validate.py2"""Validaciones de calidad para los datos de campaña."""3import logging4from dataclasses import dataclass56import pandas as pd78from .config import Campaign, DATE_RANGES910logger = logging.getLogger(__name__)111213@dataclass14class ValidationResult:15 """Resultado de una validación individual."""16 name: str17 passed: bool18 details: str192021def validate_not_empty(df: pd.DataFrame) -> ValidationResult:22 """El DataFrame debe tener al menos 1 fila."""23 passed = len(df) > 024 return ValidationResult(25 name="no_vacio",26 passed=passed,27 details=f"{len(df)} filas" if passed else "¡DataFrame vacío!",28 )293031def validate_no_null_required_fields(df: pd.DataFrame) -> ValidationResult:32 """Campos obligatorios no pueden ser NULL (excepto cliente_id en anónimos)."""33 required = ["venta_id", "producto_id", "fecha_compra", "precio", "cantidad"]34 nulls = {col: df[col].isna().sum() for col in required}35 problemas = {k: v for k, v in nulls.items() if v > 0}36 passed = len(problemas) == 037 return ValidationResult(38 name="campos_obligatorios_no_nulos",39 passed=passed,40 details=f"OK" if passed else f"NULLs: {problemas}",41 )424344def validate_positive_prices(df: pd.DataFrame) -> ValidationResult:45 """Todos los precios deben ser positivos."""46 negativos = (df["precio"] <= 0).sum()47 passed = negativos == 048 return ValidationResult(49 name="precios_positivos",50 passed=passed,51 details="OK" if passed else f"{negativos} precios <= 0",52 )535455def validate_dates_in_range(56 df: pd.DataFrame, campaign: Campaign57) -> ValidationResult:58 """Las fechas deben estar dentro del rango de la campaña."""59 date_min, date_max = DATE_RANGES[campaign]60 fuera_de_rango = (61 (df["fecha_compra"] < date_min) | (df["fecha_compra"] > date_max)62 ).sum()63 passed = fuera_de_rango == 064 return ValidationResult(65 name="fechas_en_rango",66 passed=passed,67 details="OK" if passed else f"{fuera_de_rango} fuera de [{date_min}, {date_max}]",68 )697071def validate_no_duplicates(df: pd.DataFrame) -> ValidationResult:72 """No debe haber filas 100% duplicadas."""73 dupes = df.duplicated().sum()74 passed = dupes == 075 return ValidationResult(76 name="sin_duplicados",77 passed=passed,78 details="OK" if passed else f"{dupes} filas duplicadas",79 )808182def run_validations(83 df: pd.DataFrame, campaign: Campaign84) -> list[ValidationResult]:85 """Ejecuta todas las validaciones y devuelve resultados."""86 results = [87 validate_not_empty(df),88 validate_no_null_required_fields(df),89 validate_positive_prices(df),90 validate_dates_in_range(df, campaign),91 validate_no_duplicates(df),92 ]9394 # Logging95 passed = sum(1 for r in results if r.passed)96 total = len(results)97 logger.info(f"Validaciones: {passed}/{total} pasaron")98 for r in results:99 icon = "✓" if r.passed else "✗"100 logger.info(f" {icon} {r.name}: {r.details}")101102 return results
validate.py — Validaciones claras, ejecutables y con resultados estructurados
ERROR COMÚN: no validar el DataFrame vacío. He visto pipelines en producción que procesan un CSV vacío (0 filas) y escriben una tabla silver vacía sin que nadie se entere. El dashboard muestra "0 ventas" y todo el mundo piensa que fue un mal día... hasta que alguien se da cuenta 3 días después de que el pipeline no estaba ingiriendo datos. La validación validate_not_empty parece obvia pero te puede salvar la vida.
### Los tests unitarios: test_validate.py
Las validaciones verifican los DATOS. Los tests unitarios verifican que las FUNCIONES de validación hacen lo que deben. Es meta — tests de los tests. Pero es necesario: si alguien modifica validate.py sin querer y ahora validate_positive_prices devuelve True siempre, tus datos basura pasarán sin que lo sepas.
1# pipelines/campaign_ingestion/tests/test_validate.py2"""Tests unitarios para las funciones de validación."""3import pandas as pd4import pytest56from campaign_ingestion.config import Campaign7from campaign_ingestion.validate import (8 validate_not_empty,9 validate_no_null_required_fields,10 validate_positive_prices,11 validate_dates_in_range,12 validate_no_duplicates,13)141516@pytest.fixture17def df_valido():18 """DataFrame que pasa TODAS las validaciones."""19 return pd.DataFrame({20 "venta_id": [1, 2, 3],21 "cliente_id": [100, None, 200], # None es anónimo, OK22 "producto_id": [2001, 2002, 2003],23 "fecha_compra": pd.to_datetime(["2024-07-01", "2024-07-15", "2024-08-01"]),24 "cantidad": [1, 2, 3],25 "precio": [2.45, 8.99, 1.20],26 "descuento_pct": [0.0, 10.0, 5.0],27 "tienda_id": [1, 2, 1],28 "campana": ["verano", "verano", "verano"],29 "es_anonimo": [False, True, False],30 })313233@pytest.fixture34def df_vacio():35 """DataFrame sin filas."""36 return pd.DataFrame(columns=["venta_id", "producto_id", "fecha_compra", "precio", "cantidad"])373839class TestValidateNotEmpty:40 def test_pasa_con_datos(self, df_valido):41 result = validate_not_empty(df_valido)42 assert result.passed is True4344 def test_falla_sin_datos(self, df_vacio):45 result = validate_not_empty(df_vacio)46 assert result.passed is False474849class TestValidatePositivePrices:50 def test_pasa_con_precios_positivos(self, df_valido):51 result = validate_positive_prices(df_valido)52 assert result.passed is True5354 def test_falla_con_precio_negativo(self, df_valido):55 df_valido.loc[0, "precio"] = -5.056 result = validate_positive_prices(df_valido)57 assert result.passed is False58 assert "-5" in result.details or "1" in result.details596061class TestValidateDatesInRange:62 def test_pasa_con_fechas_en_rango_verano(self, df_valido):63 result = validate_dates_in_range(df_valido, Campaign.VERANO)64 assert result.passed is True6566 def test_falla_con_fecha_fuera_de_rango(self, df_valido):67 df_valido.loc[0, "fecha_compra"] = pd.Timestamp("2024-01-01")68 result = validate_dates_in_range(df_valido, Campaign.VERANO)69 assert result.passed is False707172class TestValidateNoDuplicates:73 def test_pasa_sin_duplicados(self, df_valido):74 result = validate_no_duplicates(df_valido)75 assert result.passed is True7677 def test_falla_con_duplicados(self, df_valido):78 df_con_dupes = pd.concat([df_valido, df_valido.iloc[[0]]])79 result = validate_no_duplicates(df_con_dupes)80 assert result.passed is False
test_validate.py — Tests que verifican que tus validaciones funcionan
### 13:30 PM — Tu primer Pull Request en GitHub
Todo funciona en local: el pipeline limpia los datos de verano, las validaciones pasan y los tests están en verde. Es hora de abrir tu primer PR en el repositorio freshmart-data. Un Pull Request no es solo "subir código" — es una PROPUESTA de cambio que otro ingeniero va a revisar antes de que entre al código principal.
Primero, creas una branch con un nombre descriptivo. La convención del equipo es tipo/ticket-descripcion:
1# 🟦 Windows (PowerShell) / 🍎 Mac (Terminal)2git checkout -b feat/DATA-147-campaign-ingestion3git add pipelines/campaign_ingestion/4git commit -m "feat(DATA-147): pipeline de ingesta para campañas de Marketing56- Pipeline raw → silver para campañas de Marketing7- Mapping configurable para diferentes formatos (verano, primavera)8- Funciones de limpieza: fechas, precios, duplicados, anónimos9- Validaciones de calidad con resultados estructurados10- Tests unitarios con pytest"11git push -u origin feat/DATA-147-campaign-ingestion
Branch descriptiva, commit con contexto, push con tracking
Luego abres la PR en GitHub con una descripción que ayude al reviewer a entender QUÉ hiciste, POR QUÉ y CÓMO probarlo:
1╔══════════════════════════════════════════════════════════════════╗2║ PR #234: feat: campaign ingestion pipeline (DATA-147) ║3╠══════════════════════════════════════════════════════════════════╣4║ ║5║ ## Qué hace ║6║ Pipeline de ingesta para campañas de Marketing que: ║7║ - Lee CSVs de raw (originales sin tocar) ║8║ - Aplica mapping de columnas según la campaña ║9║ - Ejecuta limpieza (fechas, precios, duplicados, anónimos) ║10║ - Valida calidad de los datos resultantes ║11║ - Escribe el resultado en silver como Parquet ║12║ ║13║ ## Archivos nuevos ║14║ pipelines/campaign_ingestion/ (6 archivos, ~280 líneas) ║15║ ║16║ ## Cómo probar ║17║ cd pipelines/campaign_ingestion ║18║ python run.py --campaign verano ║19║ pytest tests/ -v ║20║ ║21║ ## Decisiones de diseño ║22║ - Enum para campañas (evita typos silenciosos) ║23║ - Funciones de limpieza puras (sin side effects) ║24║ - Validaciones como dataclass (resultado estructurado) ║25║ - Ventas anónimas se MARCAN, no se eliminan ║26║ ║27║ ## Contexto ║28║ María necesita estos datos para la presentación del jueves. ║29║ Los datos de primavera se añadirán mañana (mismo pipeline). ║30║ ║31╚══════════════════════════════════════════════════════════════════╝
Una buena PR tiene: qué, cómo probar, decisiones de diseño y contexto de negocio
Consejo de senior: la descripción de tu PR es tan importante como el código. Un reviewer que entiende tu contexto te da mejores comentarios. Uno que tiene que adivinar qué hiciste te hace preguntas innecesarias y la revisión tarda el doble. Invierte 10 minutos en una buena descripción — te ahorrará horas.
### 14:00 PM — El code review de Elena: 4 comentarios
Elena revisa tu PR en menos de una hora. Es rápida y detallista. Ves 4 comentarios en GitHub — y por un segundo sientes un pinchazo de "¿lo hice mal?". Respira. El code review NO es un juicio sobre ti como persona. Es el proceso normal por el que TODO el código pasa antes de entrar a main. Elena te está haciendo MEJOR con cada comentario.
1# ──────────────────────────────────────────────────────────────2# COMENTARIO 1 de Elena en clean.py, línea 42:3# ──────────────────────────────────────────────────────────────4# Tu código original:5df["fecha_compra"] = pd.to_datetime(6 df["fecha_compra"], infer_datetime_format=True7)89# Elena Torres comentó:10# ❌ No uses infer_datetime_format — está deprecated desde Pandas 2.0.11# Usa format='mixed' con dayfirst=True. Y pon un try/except por si12# hay fechas completamente inválidas — no quiero que el pipeline13# entero falle por UNA fecha rota. Usa errors='coerce' como fallback.
Comentario 1: deprecated API + falta de manejo de errores
1# ──────────────────────────────────────────────────────────────2# COMENTARIO 2 de Elena en run.py, línea 8:3# ──────────────────────────────────────────────────────────────4# Tu código original:5INPUT_PATH = "data/raw/marketing/campana_verano_ventas/2024/09/16/"6OUTPUT_PATH = "data/silver/campaign_ventas.parquet"78# Elena Torres comentó:9# ⚠️ El path está hardcodeado. En producción el bucket no se llama10# igual que en local. Usa variables de entorno o argumentos de CLI:11#12# import os13# INPUT_PATH = os.environ.get("RAW_PATH", "data/raw/")14# OUTPUT_PATH = os.environ.get("SILVER_PATH", "data/silver/")15#16# O mejor: usa argparse para recibir --input y --output como args.
Comentario 2: paths hardcodeados = bomba de tiempo en producción
1# ──────────────────────────────────────────────────────────────2# COMENTARIO 3 de Elena en validate.py, línea 31:3# ──────────────────────────────────────────────────────────────4# Elena Torres comentó:5# 👍 Bien las validaciones. Pero falta una CRÍTICA:6# ¿Qué pasa si el CSV viene vacío? Añade un check de que el7# DataFrame tiene al menos 1 fila ANTES de las demás validaciones.8# Te sorprendería cuántas veces un archivo llega vacío por un9# error en el sistema de origen y nadie se entera durante días.
Comentario 3: el caso edge que parece obvio pero que te puede morder
1# ──────────────────────────────────────────────────────────────2# COMENTARIO 4 de Elena en config.py, línea 2:3# ──────────────────────────────────────────────────────────────4# Tu código original:5COLUMN_MAPPINGS = {6 'verano': {},7 'primavera': {...},8}910# Elena Torres comentó:11# Nit: los nombres de campaña como keys del dict deberían ser un12# Enum o al menos constantes. Si alguien escribe 'Verano' en vez13# de 'verano' explota silenciosamente (el dict devuelve el valor14# por defecto sin avisar). Con un Enum te da un error claro.15#16# from enum import Enum17# class Campaign(Enum):18# VERANO = "verano"19# PRIMAVERA = "primavera"
Comentario 4: strings mágicas → Enum. Explícito mejor que implícito.
Cuatro comentarios. Ninguno es "esto es horrible" — todos son "esto puede ser mejor". El primero es un bug real (API deprecated). El segundo es experiencia de producción (paths hardcodeados). El tercero es un edge case que se te escapó. El cuarto es estilo/robustez. Todos te hacen mejor ingeniero.
### 14:30 PM — Iterar sobre el code review
No discutes los comentarios. No te pones a la defensiva. Los corriges. Uno por uno, con commits descriptivos que referencian el feedback:
1# Corrección 1: usar format='mixed' + try/except2git commit -m "fix: usar pd.to_datetime format=mixed (deprecated infer_datetime_format)34Ref: review comment de Elena — infer_datetime_format está deprecated5desde Pandas 2.0. Añadido try/except con errors='coerce' como fallback."67# Corrección 2: parametrizar paths8git commit -m "refactor: paths como argumentos de CLI en vez de hardcoded910Añadido argparse con --input y --output. Default apunta a local11para desarrollo pero permite override en producción."1213# Corrección 3: validación de DataFrame vacío14git commit -m "feat: añadir validate_not_empty como primera validación1516Check de que el DataFrame tiene ≥1 fila antes de ejecutar las17demás validaciones. Evita procesar archivos vacíos en silencio."1819# Corrección 4: Enum para campañas20git commit -m "refactor: Campaign Enum en vez de strings como keys2122Strings mágicas → Enum. Typos dan error explícito en vez de23devolver None silenciosamente."2425# Push de todas las correcciones26git push
Cada corrección en un commit separado — facilita el re-review
A las 16:30 Elena aprueba la PR:
1Elena Torres 16:32 PM2PR aprobada ✅3Buen primer pipeline. Mergea cuando quieras.4Mañana quiero que hagas lo mismo con primavera y que construyas5la tabla gold unificada (verano + primavera juntas).
Aprobada. "Buen primer pipeline" de Elena vale su peso en oro.
### 16:00 PM — 1:1 con Elena (feedback semanal)
El miércoles a las 16:00 es tu 1:1 semanal con Elena. Son 20 minutos donde ella te da feedback y tú puedes preguntar lo que quieras sin el contexto del equipo. Es un espacio seguro:
1Elena: ¿Qué tal la semana? ¿Algún bloqueante?23Tú: Bien, la verdad. Lo de María cambiando requisitos me pilló un poco4 desprevenido...56Elena: Bienvenido a la vida real. María siempre hace eso. El truco es:7 1. No digas que sí a todo sin preguntar primero8 2. Comunica el impacto en tiempo ("puedo hacerlo pero será 1 día más")9 3. Pon por escrito qué se acordó (Slack es tu amigo)1011Elena: Por lo demás, buen trabajo. Tu código es limpio para ser tu primera12 semana. Solo te falta rodaje con el stack. Mañana te voy a pedir13 algo más complejo — la tabla gold para Marketing.1415Tú: ¿Algo que debería leer o estudiar para prepararme?1617Elena: Lee cómo funcionan las agregaciones en Pandas y cómo se escribe a18 Parquet con particionado. Si no has usado groupby con múltiples19 funciones de agregación, repásalo esta noche. Mañana lo vas a necesitar.
El 1:1 es tu espacio para aprender sin presión del equipo
Consejo de senior: aprovecha los 1:1 para hacer preguntas que no harías en público. "¿Cómo priorizo cuando me llegan 3 peticiones a la vez?", "¿Cómo sé cuándo escalar un problema vs resolverlo solo?", "¿Qué debería estar leyendo para crecer más rápido?". Estas conversaciones son las que aceleran tu carrera más que cualquier curso online.
### Resumen del Día 3
- Pipeline completo raw → silver con código modular y bien estructurado
- Tests de validación que cubren: vacío, NULLs, precios positivos, fechas en rango, sin duplicados
- Tu primer PR en GitHub con descripción completa y profesional
- Code review de Elena: 4 comentarios constructivos, todos corregidos
- 1:1 semanal: feedback positivo y preparación para mañana (tabla gold)
## ejercicios
Escribir la función de validación completa
Implementa una función validate_campaign_data que reciba un DataFrame y devuelva un diccionario con los resultados de 5 validaciones: (1) no vacío, (2) campos obligatorios sin NULLs, (3) precios positivos, (4) fechas en rango Jun-Sep 2024, (5) sin duplicados. Cada key del dict debe ser el nombre de la validación y el value un booleano.
Corregir el code review de Elena: fechas deprecated
Elena te señaló que usas infer_datetime_format (deprecated en Pandas 2.0). Reescribe la función clean_dates para: (1) usar format="mixed" con dayfirst=True, (2) incluir un try/except que use errors="coerce" como fallback, (3) loggear cuántas fechas quedaron como NaT después del coerce.
Escribir tests unitarios para las validaciones
Escribe 4 tests con pytest que verifiquen: (1) validate_positive_prices pasa con un DataFrame de precios válidos, (2) validate_positive_prices falla si hay un precio negativo, (3) validate_no_duplicates pasa con datos únicos, (4) validate_no_duplicates falla con filas duplicadas.
Escribir la descripción del PR para tu pipeline
Escribe en formato Markdown la descripción de un Pull Request para tu pipeline de ingesta. Debe incluir: (1) sección "Qué hace" con 3-4 bullet points, (2) sección "Cómo probar" con los comandos exactos, (3) sección "Decisiones de diseño" explicando por qué usaste Enum y por qué no eliminas anónimos. No escribas código — escribe la descripción en Markdown como la pondrías en GitHub.
Regístrate para guardar tu progreso.
## comentarios
Reporta erratas, ayuda a otros o comparte tu opinión. Sé constructivo.
Inicia sesión para comentar y responder.
cargando comentarios...