lección 9
Proyecto: script que procesa un CSV de ventas
Todo junto: lees un CSV real, lo limpias, calculas métricas y generas un reporte JSON. Tu primer pipeline de datos completo.
⏱ 60 min
Ha llegado el momento de unir todo lo que has aprendido en un proyecto real. Imagina este escenario: trabajas en el equipo de datos de una tienda online. Cada día a las 7:00 de la mañana, el sistema de ventas genera un archivo CSV con todas las transacciones del día anterior. Tu trabajo es procesar ese archivo y generar un resumen ejecutivo que el CEO revisa antes de la reunión de las 9:00.
El CSV viene "sucio" — tiene filas con datos faltantes, precios con formato incorrecto, fechas inconsistentes. Tu script tiene que ser robusto: no puede explotar porque una fila entre miles esté mal. Tiene que limpiar, calcular y reportar. Este es TU primer pipeline ETL (Extract-Transform-Load) completo.
### El problema de negocio
- ENTRADA: CSV con ventas del día (fecha, producto, categoría, cantidad, precio_unitario, cliente)
- PROCESO: Limpiar datos sucios, calcular totales, agrupar por categoría
- SALIDA: Archivo JSON con el resumen ejecutivo del día
- RESTRICCIÓN: El script debe reportar errores sin detenerse (tolerancia a fallos)
### Paso 1: Generar datos de ejemplo
En un escenario real, el CSV lo genera otro sistema. Para este proyecto, vamos a crear un CSV de ejemplo con datos realistas — incluyendo errores intencionales para probar nuestra robustez.
1# generar_datos.py — Crea el CSV de ventas con datos sucios2import csv3import os45os.makedirs("data", exist_ok=True)67ventas = [8 ["2024-03-15", "Laptop Pro 15", "electrónica", "2", "1299.99", "Ana García"],9 ["2024-03-15", "Monitor 4K", "electrónica", "1", "449.00", "Luis Pérez"],10 ["2024-03-15", "Camiseta Algodón", "ropa", "5", "24.99", "María López"],11 ["2024-03-15", "Teclado Mecánico", "electrónica", "3", "89.99", "Pedro Ruiz"],12 ["2024-03-15", "Pantalón Slim", "ropa", "2", "59.99", "Sara Torres"],13 ["2024-03-15", "Auriculares BT", "electrónica", "4", "N/A", "Jorge Díaz"], # precio inválido14 ["2024-03-15", "Zapatillas Run", "calzado", "1", "129.99", "Ana García"],15 ["2024-03-15", "", "ropa", "3", "34.99", "Carlos Ruiz"], # producto vacío16 ["2024-03-15", "Mochila Pro", "accesorios", "2", "79.99", "Luis Pérez"],17 ["2024-03-15", "Portátil Air", "electrónica", "1", "999.00", "María López"],18 ["2024-03-15", "Jersey Lana", "ropa", "4", "45.50", "Pedro Ruiz"],19 ["2024-03-15", "Ratón Gaming", "electrónica", "", "49.99", "Sara Torres"], # cantidad vacía20 ["2024-03-15", "Botas Montaña", "calzado", "1", "189.00", "Jorge Díaz"],21 ["2024-03-15", "USB-C Hub", "electrónica", "6", "35.99", "Ana García"],22 ["2024-03-15", "Sudadera Tech", "ropa", "2", "69.99", "Carlos Ruiz"],23]2425with open("data/ventas_dia.csv", "w", encoding="utf-8", newline="") as f:26 writer = csv.writer(f)27 writer.writerow(["fecha", "producto", "categoria", "cantidad", "precio_unitario", "cliente"])28 writer.writerows(ventas)2930print(f"✓ CSV generado: data/ventas_dia.csv ({len(ventas)} filas)")
Paso 1: Generamos el CSV con datos sucios (precios inválidos, campos vacíos).
### Paso 2: El pipeline principal
Ahora el script principal. Lo estructuramos en funciones con responsabilidades claras: una para extraer (leer), otra para transformar (limpiar y calcular) y otra para cargar (escribir el resultado). Es el patrón ETL que usarás en toda tu carrera.
1# pipeline_ventas.py — Tu primer pipeline ETL completo2import csv3import json4from datetime import datetime5from collections import defaultdict678# ═══ EXTRACT: leer datos ═══════════════════════════════════════9def extraer_ventas(ruta_csv):10 """Lee el CSV y devuelve una lista de diccionarios (datos crudos)."""11 with open(ruta_csv, "r", encoding="utf-8") as f:12 reader = csv.DictReader(f)13 return list(reader)141516# ═══ TRANSFORM: limpiar y calcular ═════════════════════════════17def transformar_ventas(ventas_raw):18 """Limpia datos sucios y calcula totales. Devuelve (limpias, errores)."""19 limpias = []20 errores = []2122 for i, venta in enumerate(ventas_raw):23 try:24 # Validar producto no vacío25 if not venta.get("producto", "").strip():26 raise ValueError("Producto vacío")2728 # Convertir cantidad (default 1 si vacío)29 cantidad_raw = venta.get("cantidad", "").strip()30 cantidad = int(cantidad_raw) if cantidad_raw else 13132 # Convertir precio33 precio = float(venta["precio_unitario"])3435 # Calcular total36 total = cantidad * precio3738 # Registro limpio39 limpias.append({40 "fecha": venta["fecha"],41 "producto": venta["producto"].strip(),42 "categoria": venta["categoria"].strip(),43 "cantidad": cantidad,44 "precio_unitario": precio,45 "total": round(total, 2),46 "cliente": venta["cliente"].strip(),47 })48 except (ValueError, TypeError) as e:49 errores.append({50 "fila": i + 2, # +2 por header y 0-index51 "error": str(e),52 "datos": dict(venta),53 })5455 return limpias, errores565758# ═══ ANALYZE: generar métricas ═════════════════════════════════59def generar_resumen(ventas_limpias):60 """Genera el resumen ejecutivo a partir de datos limpios."""61 if not ventas_limpias:62 return {"error": "No hay datos válidos"}6364 # Métricas generales65 total_facturado = sum(v["total"] for v in ventas_limpias)66 num_transacciones = len(ventas_limpias)67 ticket_medio = total_facturado / num_transacciones6869 # Por categoría70 por_categoria = defaultdict(lambda: {"total": 0, "transacciones": 0})71 for v in ventas_limpias:72 cat = v["categoria"]73 por_categoria[cat]["total"] += v["total"]74 por_categoria[cat]["transacciones"] += 17576 # Top productos77 productos_total = defaultdict(float)78 for v in ventas_limpias:79 productos_total[v["producto"]] += v["total"]8081 top_productos = sorted(productos_total.items(), key=lambda x: x[1], reverse=True)[:3]8283 # Clientes únicos84 clientes = set(v["cliente"] for v in ventas_limpias)8586 return {87 "fecha_reporte": datetime.now().strftime("%Y-%m-%d %H:%M"),88 "total_facturado": round(total_facturado, 2),89 "num_transacciones": num_transacciones,90 "ticket_medio": round(ticket_medio, 2),91 "clientes_unicos": len(clientes),92 "por_categoria": dict(por_categoria),93 "top_3_productos": [94 {"producto": p, "total": round(t, 2)} for p, t in top_productos95 ],96 }979899# ═══ LOAD: escribir resultados ═════════════════════════════════100def guardar_resultado(resumen, errores, ruta_salida, ruta_errores):101 """Escribe el resumen y los errores en archivos JSON."""102 with open(ruta_salida, "w", encoding="utf-8") as f:103 json.dump(resumen, f, indent=2, ensure_ascii=False)104105 if errores:106 with open(ruta_errores, "w", encoding="utf-8") as f:107 json.dump(errores, f, indent=2, ensure_ascii=False)
El pipeline completo: extraer, transformar, analizar y cargar.
### Paso 3: El orquestador (main)
1# ═══ MAIN: orquestar el pipeline ══════════════════════════════2def main():3 """Punto de entrada del pipeline."""4 print("=" * 50)5 print(" PIPELINE DE VENTAS DIARIAS")6 print("=" * 50)78 # Configuración9 ruta_entrada = "data/ventas_dia.csv"10 ruta_salida = "data/resumen_dia.json"11 ruta_errores = "data/errores_dia.json"1213 # 1. EXTRACT14 print("\n[1/4] Extrayendo datos...")15 ventas_raw = extraer_ventas(ruta_entrada)16 print(f" Leídos: {len(ventas_raw)} registros")1718 # 2. TRANSFORM19 print("[2/4] Limpiando y transformando...")20 ventas_limpias, errores = transformar_ventas(ventas_raw)21 tasa_exito = len(ventas_limpias) / len(ventas_raw) * 10022 print(f" Válidos: {len(ventas_limpias)} ({tasa_exito:.0f}%)")23 print(f" Errores: {len(errores)}")2425 # 3. ANALYZE26 print("[3/4] Generando resumen ejecutivo...")27 resumen = generar_resumen(ventas_limpias)28 print(f" Total facturado: {resumen['total_facturado']}€")2930 # 4. LOAD31 print("[4/4] Guardando resultados...")32 guardar_resultado(resumen, errores, ruta_salida, ruta_errores)33 print(f" Resumen: {ruta_salida}")34 if errores:35 print(f" Errores: {ruta_errores}")3637 # Reporte final38 print("\n" + "=" * 50)39 print(" RESUMEN EJECUTIVO")40 print("=" * 50)41 print(f" Facturación total: {resumen['total_facturado']}€")42 print(f" Ticket medio: {resumen['ticket_medio']}€")43 print(f" Transacciones: {resumen['num_transacciones']}")44 print(f" Clientes únicos: {resumen['clientes_unicos']}")45 print(f"\n Top 3 productos:")46 for item in resumen["top_3_productos"]:47 print(f" • {item['producto']}: {item['total']}€")48 print("=" * 50)495051if __name__ == "__main__":52 main()
El main orquesta todo: extrae → transforma → analiza → guarda.
### Paso 4: Ejecutar el pipeline
1# 🟦 Windows (PowerShell)2python generar_datos.py3python pipeline_ventas.py45# 🍎 Mac (Terminal) — dentro del venv6python generar_datos.py7python pipeline_ventas.py89# Verifica los archivos generados:10# data/resumen_dia.json ← resumen ejecutivo11# data/errores_dia.json ← registros que fallaron
Primero genera los datos, luego ejecuta el pipeline.
Consejo de senior: este patrón de "generar datos de prueba" + "ejecutar pipeline" es exactamente cómo se desarrolla en producción. Siempre tienes un script de datos de prueba para verificar que tu pipeline funciona antes de apuntar a datos reales.
### Lo que acabas de construir
Haz una pausa y mira lo que has logrado. En un solo proyecto has usado:
- Módulo csv para leer datos tabulares
- Módulo json para escribir resultados estructurados
- Listas y diccionarios para representar tablas de datos
- List comprehensions para filtrar y transformar
- try/except para tolerar datos sucios
- Funciones con responsabilidades claras (Extract, Transform, Load)
- defaultdict de collections para agrupar
- pathlib/os para crear directorios
- datetime para timestamps
- El patrón if __name__ == "__main__" para scripts ejecutables
Esto es un pipeline ETL real en miniatura. Los mismos principios aplican cuando procesas 15 millones de filas en vez de 15 — solo cambias las herramientas (Pandas en vez de csv, PySpark en vez de Pandas), pero la ESTRUCTURA es idéntica: extraer, transformar, cargar, reportar.
Este proyecto procesa 15 filas en milisegundos. En producción procesarás millones. El PATRÓN es idéntico, pero las herramientas cambian: Pandas para cientos de miles de filas, PySpark para millones. Primero domina el patrón con Python puro — luego escalar es fácil.
### Siguientes pasos
Has completado Python desde cero. Ahora dominas los fundamentos: estructuras de datos, I/O de archivos, módulos, errores, pip y el patrón ETL. En las próximas skills vas a escalar todo esto:
- Skill 4 (Fundamentos de datos): entenderás los formatos (CSV vs JSON vs Parquet) y por qué importa elegir bien.
- Skill 5-6 (SQL): aprenderás a hablar con bases de datos — donde viven la mayoría de datos empresariales.
- Skill 7 (Python para ingeniería): Pandas, APIs, testing — Python con esteroides para datos.
- Skill 10 (PySpark): cuando tus datos no caben en un solo ordenador.
Lo que le diría a mi yo del pasado: no te saltes la práctica. Lee código de otros. Rompe cosas. Los mejores ingenieros de datos que conozco no son los que más saben — son los que más han practicado y más errores han cometido temprano (en desarrollo, no en producción).
## ejercicios
Pipeline básico: leer y resumir
Lee el CSV de ventas, filtra las ventas de electrónica y calcula el total facturado en esa categoría. Escribe el resultado como JSON.
Ranking de clientes
El CEO quiere saber quiénes son los top 3 clientes que más han gastado. Lee las ventas, agrupa por cliente, calcula el gasto total de cada uno y muestra el ranking.
Reporte por categorías en JSON
Genera un reporte JSON con el desglose por categoría: número de productos, total vendido y porcentaje sobre el total general.
Validador de datos con reporte
Escribe una función que valide un CSV completo: verifica que no hay campos vacíos, que los numéricos son válidos y que las fechas tienen formato correcto. Genera un reporte de calidad.
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...