SolveConPython

Python Reto #15 — Leer un CSV y calcular métricas (sin pandas)

Nivel: Intermedio
Tema: Archivos, CSV, parsing, conversión de tipos, agregaciones, validación de entradas, tests con pytest
Objetivo: Leer un archivo CSV usando solo la librería estándar y calcular métricas básicas (conteo, suma y promedio) de una columna numérica, ignorando filas inválidas.

Reto #15 Leer un CSV y calcular métricas (sin pandas)
Reto #15 Leer un CSV y calcular métricas (sin pandas)

Enunciado

Crea una función llamada metricas_csv(ruta_csv, columna) que:

  1. Reciba:
    • ruta_csv: ruta (string) a un archivo CSV con cabecera
    • columna: nombre (string) de una columna numérica
  2. Si ruta_csv es None o columna es None, lance TypeError.
  3. Si ruta_csv o columna no son str, lance TypeError.
  4. Si el archivo no existe, lance FileNotFoundError.
  5. Lea el CSV y calcule métricas para la columna indicada:
    • count: número de filas válidas (con valor numérico convertible)
    • sum: suma de los valores válidos
    • avg: promedio (si count es 0, avg debe ser 0)
  6. Debe ignorar filas donde:
    • falte la columna,
    • el valor esté vacío,
    • el valor no se pueda convertir a float.

Debe devolver un diccionario con esta forma:

{"count": int, "sum": float, "avg": float}

CSV de ejemplo

ventas.csv

producto,importe
A,10
B,20.5
C,abc
D,
E,5

metricas_csv("ventas.csv", "importe") debería devolver:

{"count": 3, "sum": 35.5, "avg": 11.833333333333334}

Pistas

  1. Usa csv.DictReader para leer filas como diccionarios.
  2. Convierte a número con float(valor).
  3. Maneja conversiones fallidas con try/except ValueError.
  4. Asegura casos borde: archivo vacío, columna inexistente, count == 0.

Solución explicada (paso a paso)

  1. Validamos tipos (ruta_csv y columna deben ser str).
  2. Abrimos el archivo en modo texto con newline="".
  3. Usamos csv.DictReader para obtener cada fila como diccionario.
  4. Para cada fila:
    • obtenemos el valor de la columna,
    • verificamos que exista y no esté vacío,
    • intentamos convertirlo a float,
    • si se puede, lo acumulamos en suma y aumentamos count.
  5. Calculamos promedio:
    • si count es 0 → avg = 0
    • si no → avg = total / count
  6. Devolvemos el diccionario con métricas.

Python
import csv
def metricas_csv(ruta_csv: str, columna: str) -> dict:
"""
Lee un CSV con cabecera y calcula métricas básicas (count, sum, avg)
de una columna numérica, ignorando filas inválidas.
Reglas:
- ruta_csv y columna deben ser str
- Si archivo no existe -> FileNotFoundError
- Ignora valores vacíos o no numéricos
"""
if ruta_csv is None or columna is None:
raise TypeError("Los parámetros 'ruta_csv' y 'columna' no pueden ser None.")
if not isinstance(ruta_csv, str) or not isinstance(columna, str):
raise TypeError("Los parámetros 'ruta_csv' y 'columna' deben ser cadenas (str).")
total = 0.0
count = 0
with open(ruta_csv, mode="r", encoding="utf-8", newline="") as f:
reader = csv.DictReader(f)
for fila in reader:
if not isinstance(fila, dict):
continue
if columna not in fila:
continue
valor_raw = fila.get(columna)
if valor_raw is None:
continue
valor_raw = valor_raw.strip()
if valor_raw == "":
continue
try:
valor = float(valor_raw)
except ValueError:
continue
total += valor
count += 1
avg = total / count if count > 0 else 0.0
return {"count": count, "sum": total, "avg": avg}

Python
import pytest
from reto_15_metricas_csv import metricas_csv
def test_metricas_csv_con_datos(tmp_path):
contenido = (
"producto,importe\n"
"A,10\n"
"B,20.5\n"
"C,abc\n"
"D,\n"
"E,5\n"
)
archivo = tmp_path / "ventas.csv"
archivo.write_text(contenido, encoding="utf-8")
resultado = metricas_csv(str(archivo), "importe")
assert resultado["count"] == 3
assert resultado["sum"] == 35.5
assert resultado["avg"] == 35.5 / 3
def test_archivo_no_existe():
with pytest.raises(FileNotFoundError):
metricas_csv("no_existe.csv", "importe")
def test_columna_no_existe(tmp_path):
contenido = "a,b\n1,2\n3,4\n"
archivo = tmp_path / "datos.csv"
archivo.write_text(contenido, encoding="utf-8")
resultado = metricas_csv(str(archivo), "importe")
assert resultado == {"count": 0, "sum": 0.0, "avg": 0.0}
def test_none_lanza_typeerror():
with pytest.raises(TypeError):
metricas_csv(None, "importe")
def test_tipo_incorrecto_lanza_typeerror():
with pytest.raises(TypeError):
metricas_csv(123, "importe")

Ejecuta:

  • pytest -q

Variantes para subir de nivel (opcional)

  1. Devolver también min/max de la columna
  2. Permitir separador configurable (, vs ;)
  3. Soportar decimales con coma (locale)
  4. Calcular métricas por grupo (por ejemplo, por producto o categoria)
  5. Validar que el CSV tenga cabecera (y manejar caso contrario)

Lo que aprendiste

  • Lectura de CSV con csv.DictReader
  • Manejo de datos “sucios” y filas inválidas
  • Agregaciones básicas (conteo, suma, promedio)
  • Tests con archivos temporales (tmp_path)

Accede al código completo y a los tests en GitHub para ejecutar y modificar la solución localmente.

Continúa con Reto #16 — Merge de dos listas ordenadas para practicar lógica de “dos punteros”, muy útil en algoritmos y rendimiento.