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 sucios
2import csv
3import os
4
5os.makedirs("data", exist_ok=True)
6
7ventas = [
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álido
14 ["2024-03-15", "Zapatillas Run", "calzado", "1", "129.99", "Ana García"],
15 ["2024-03-15", "", "ropa", "3", "34.99", "Carlos Ruiz"], # producto vacío
16 ["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ía
20 ["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]
24
25with 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)
29
30print(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 completo
2import csv
3import json
4from datetime import datetime
5from collections import defaultdict
6
7
8# ═══ 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)
14
15
16# ═══ TRANSFORM: limpiar y calcular ═════════════════════════════
17def transformar_ventas(ventas_raw):
18 """Limpia datos sucios y calcula totales. Devuelve (limpias, errores)."""
19 limpias = []
20 errores = []
21
22 for i, venta in enumerate(ventas_raw):
23 try:
24 # Validar producto no vacío
25 if not venta.get("producto", "").strip():
26 raise ValueError("Producto vacío")
27
28 # Convertir cantidad (default 1 si vacío)
29 cantidad_raw = venta.get("cantidad", "").strip()
30 cantidad = int(cantidad_raw) if cantidad_raw else 1
31
32 # Convertir precio
33 precio = float(venta["precio_unitario"])
34
35 # Calcular total
36 total = cantidad * precio
37
38 # Registro limpio
39 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-index
51 "error": str(e),
52 "datos": dict(venta),
53 })
54
55 return limpias, errores
56
57
58# ═══ 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"}
63
64 # Métricas generales
65 total_facturado = sum(v["total"] for v in ventas_limpias)
66 num_transacciones = len(ventas_limpias)
67 ticket_medio = total_facturado / num_transacciones
68
69 # Por categoría
70 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"] += 1
75
76 # Top productos
77 productos_total = defaultdict(float)
78 for v in ventas_limpias:
79 productos_total[v["producto"]] += v["total"]
80
81 top_productos = sorted(productos_total.items(), key=lambda x: x[1], reverse=True)[:3]
82
83 # Clientes únicos
84 clientes = set(v["cliente"] for v in ventas_limpias)
85
86 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_productos
95 ],
96 }
97
98
99# ═══ 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)
104
105 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)
7
8 # Configuración
9 ruta_entrada = "data/ventas_dia.csv"
10 ruta_salida = "data/resumen_dia.json"
11 ruta_errores = "data/errores_dia.json"
12
13 # 1. EXTRACT
14 print("\n[1/4] Extrayendo datos...")
15 ventas_raw = extraer_ventas(ruta_entrada)
16 print(f" Leídos: {len(ventas_raw)} registros")
17
18 # 2. TRANSFORM
19 print("[2/4] Limpiando y transformando...")
20 ventas_limpias, errores = transformar_ventas(ventas_raw)
21 tasa_exito = len(ventas_limpias) / len(ventas_raw) * 100
22 print(f" Válidos: {len(ventas_limpias)} ({tasa_exito:.0f}%)")
23 print(f" Errores: {len(errores)}")
24
25 # 3. ANALYZE
26 print("[3/4] Generando resumen ejecutivo...")
27 resumen = generar_resumen(ventas_limpias)
28 print(f" Total facturado: {resumen['total_facturado']}€")
29
30 # 4. LOAD
31 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}")
36
37 # Reporte final
38 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)
49
50
51if __name__ == "__main__":
52 main()

El main orquesta todo: extrae → transforma → analiza → guarda.

### Paso 4: Ejecutar el pipeline

1# 🟦 Windows (PowerShell)
2python generar_datos.py
3python pipeline_ventas.py
4
5# 🍎 Mac (Terminal) — dentro del venv
6python generar_datos.py
7python pipeline_ventas.py
8
9# Verifica los archivos generados:
10# data/resumen_dia.json ← resumen ejecutivo
11# 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.

Tu primer pipeline ETL real. Este patrón te acompañará toda tu carrera.

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

[01]

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.

Cargando editor...
[02]

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.

Cargando editor...
[03]

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.

Cargando editor...
[04]

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.

Cargando editor...

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...