Apuntes DAM
Volver al inicio

Informe de ventas a partir de un CSV

Ejercicio de PythonDifícilUnos 70 minutos

Lee las ventas de una tienda en formato CSV, descarta las líneas incorrectas y saca un informe: total, ventas y porcentaje por categoría, producto más vendido, mejor día y un gráfico de barras por mes. Diccionarios, cálculos en céntimos y formato de importes en español.

  • Diccionarios
  • Funciones
  • try/except
  • sorted y min con key
  • Formato de números
  • Céntimos en lugar de decimales

Enunciado

Una papelería exporta las ventas de su caja registradora a un fichero CSV, con una línea por venta: fecha, producto, categoría, unidades y precio unitario. El dueño quiere un informe rápido sin abrir una hoja de cálculo.

El fichero lo genera una caja antigua y a veces trae líneas rotas: columnas que faltan, unidades escritas con letras o negativas. Esas líneas no deben estropear el informe: se descartan y se dice cuáles eran.

Para que los totales cuadren al céntimo, el informe se calcula en céntimos (números enteros) y solo se convierte a euros al escribirlo. Sumar precios con decimales como 0.1 + 0.2 en coma flotante da errores pequeños que, con muchas ventas, acaban notándose.

Qué tiene que hacer el programa

  1. La primera línea es la cabecera fecha,producto,categoria,unidades,precio y no se procesa. Las líneas vacías se ignoran sin más.
  2. Una línea es válida si tiene exactamente 5 campos separados por comas, las unidades son un entero mayor que 0 y el precio es un número no negativo. El importe de la línea es unidades × precio. Las fechas tienen el formato AAAA-MM-DD.
  3. Escribe Ventas totales: X € y, bajo Por categoría:, una línea por categoría de mayor a menor importe (a igualdad, alfabético): dos espacios, el nombre en 16 caracteres a la izquierda, el importe en 12 caracteres a la derecha, dos espacios y el porcentaje sobre el total con un decimal en 5 caracteres a la derecha, seguido de %.
  4. Producto más vendido: producto (N unidades): el de más unidades vendidas en total (a igualdad, el primero alfabéticamente; «unidad» en singular si es 1). Mejor día: fecha (X €): el día con más importe (a igualdad, el más antiguo).
  5. Bajo Por mes:, una línea por mes en orden cronológico: dos espacios, el mes (AAAA-MM), dos espacios, el importe en 12 caracteres a la derecha, un espacio y una almohadilla # por cada 50 € completos (sin espacios al final de la línea).
  6. Por último, Líneas descartadas: N y, si hay alguna, sus números de línea entre paréntesis separados por coma y espacio (la cabecera es la línea 1).
  7. Los importes se escriben con punto de millar, coma decimal, dos decimales y el símbolo € (por ejemplo 1.234,50 €). Si no hay ninguna venta válida, escribe solo No hay ventas válidas.

Entrada

Un fichero CSV: la cabecera y una venta por línea, con los campos separados por comas y el precio con punto decimal.

Ejemplos de ejecución

Tu programa debe escribir exactamente esta salida para estas entradas. Las pruebas del editor incluyen estos ejemplos y otros casos ocultos.

Una semana de ventas

Entrada

fecha,producto,categoria,unidades,precio
2026-08-28,Cuaderno A4,Papelería,10,2.50
2026-08-28,Ratón inalámbrico,Informática,2,18.90
2026-08-31,Bolígrafo azul,Papelería,25,0.80
2026-09-01,Cuaderno A4,Papelería,15,2.50
2026-09-01,Memoria USB 64 GB,Informática,4,12.00
2026-09-02,Mochila,Complementos,3,34.95
2026-09-02,Bolígrafo azul,Papelería,40,0.80
2026-09-03,Ratón inalámbrico,Informática,1,18.90

Salida por consola

Ventas totales: 324,05 €
Por categoría:
  Papelería           114,50 €   35,3 %
  Complementos        104,85 €   32,4 %
  Informática         104,70 €   32,3 %
Producto más vendido: Bolígrafo azul (65 unidades)
Mejor día: 2026-09-02 (136,85 €)
Por mes:
  2026-08       82,80 € #
  2026-09      241,25 € ####
Líneas descartadas: 0

Con líneas rotas

Entrada

fecha,producto,categoria,unidades,precio
2026-09-10,Cuaderno A4,Papelería,4,2.50
2026-09-10,Grapadora,Papelería,dos,6.40
2026-09-11,Auriculares,Informática,1,29.99
2026-09-11,Funda portátil,Complementos,1
2026-09-12,Bolígrafo azul,Papelería,-3,0.80
2026-09-12,Bolígrafo azul,Papelería,12,0.80

2026-09-13,Auriculares,Informática,1,29.99

Salida por consola

Ventas totales: 79,58 €
Por categoría:
  Informática          59,98 €   75,4 %
  Papelería            19,60 €   24,6 %
Producto más vendido: Bolígrafo azul (12 unidades)
Mejor día: 2026-09-11 (29,99 €)
Por mes:
  2026-09       79,58 € #
Líneas descartadas: 3 (3, 5, 6)

Guía paso a paso

Intenta resolverlo por tu cuenta y abre un paso solo cuando te atasques: cada uno te acerca a la solución sin dártela entera.

1. Una función para cada línea

Escribe leer_venta(linea) que devuelva una tupla con los datos ya convertidos o None si la línea no vale. Así el bucle principal queda limpio y la validación se prueba por separado. int() y float() lanzan ValueError si el texto no es un número: captúralo con try/except.

2. Trabaja en céntimos

Convierte el precio a céntimos al leerlo y opera siempre con enteros. Solo al escribir divides entre 100.

python
precio_cent = round(float(precio) * 100)
importe = unidades * precio_cent
3. Un diccionario por cada agrupación

Usa cuatro diccionarios: importe por categoría, unidades por producto, importe por día e importe por mes (el mes son los 7 primeros caracteres de la fecha). d.get(clave, 0) + valor suma sin tener que comprobar si la clave existe.

4. Ordenar y elegir con empates

sorted(d.items(), key=lambda c: (-c[1], c[0])) ordena de mayor a menor importe y, a igualdad, por nombre. Para quedarte solo con el mejor, min con esa misma clave te da el primero de esa ordenación.

5. Formatear importes en español

Python escribe 1,234.50 con f"{x:,.2f}"; para el formato español hay que intercambiar comas y puntos. Hazlo en tres pasos con un carácter intermedio para que un reemplazo no deshaga el otro.

python
texto = f"{centimos / 100:,.2f}"
texto.replace(",", "X").replace(".", ",").replace("X", ".")

Resuélvelo aquí

El editor trae el esqueleto del programa. Pulsa «Ejecutar» para comprobarlo con los ejemplos y con 2 casos ocultos que buscan los errores típicos.

🐍PythonInforme de ventas a partir de un CSVDifícil

Ejemplo

Entrada (lo que se escribe por teclado)
fecha,producto,categoria,unidades,precio
2026-08-28,Cuaderno A4,Papelería,10,2.50
2026-08-28,Ratón inalámbrico,Informática,2,18.90
2026-08-31,Bolígrafo azul,Papelería,25,0.80
2026-09-01,Cuaderno A4,Papelería,15,2.50
2026-09-01,Memoria USB 64 GB,Informática,4,12.00
2026-09-02,Mochila,Complementos,3,34.95
2026-09-02,Bolígrafo azul,Papelería,40,0.80
2026-09-03,Ratón inalámbrico,Informática,1,18.90
Salida esperada
Ventas totales: 324,05 €
Por categoría:
  Papelería           114,50 €   35,3 %
  Complementos        104,85 €   32,4 %
  Informática         104,70 €   32,3 %
Producto más vendido: Bolígrafo azul (65 unidades)
Mejor día: 2026-09-02 (136,85 €)
Por mes:
  2026-08       82,80 € #
  2026-09      241,25 € ####
Líneas descartadas: 0
⏳
Test oculto #3
⏳
Test oculto #4
0/4 tests pasados · pulsa un test para ver su entrada y su salida esperada

Solución explicada

Ver la solución completa
python
1import sys
2
3
4def euros(centimos):
5    """1234.5 € → «1.234,50 €»: puntos de millar y coma decimal."""
6    texto = f"{centimos / 100:,.2f}"
7    return texto.replace(",", "X").replace(".", ",").replace("X", ".") + " €"
8
9
10def leer_venta(linea):
11    """Devuelve (fecha, producto, categoría, unidades, importe en céntimos) o None si la línea no es válida."""
12    partes = [p.strip() for p in linea.split(",")]
13    if len(partes) != 5:
14        return None
15    fecha, producto, categoria, unidades, precio = partes
16    try:
17        unidades = int(unidades)
18        precio_cent = round(float(precio) * 100)
19    except ValueError:
20        return None
21    if unidades <= 0 or precio_cent < 0:
22        return None
23    return fecha, producto, categoria, unidades, unidades * precio_cent
24
25
26def main():
27    lineas = sys.stdin.read().split("\n")
28    por_categoria, por_producto, por_dia, por_mes = {}, {}, {}, {}
29    total = 0
30    descartadas = []
31    for num, linea in enumerate(lineas[1:], start=2):
32        if not linea.strip():
33            continue
34        venta = leer_venta(linea)
35        if venta is None:
36            descartadas.append(num)
37            continue
38        fecha, producto, categoria, unidades, importe = venta
39        total += importe
40        por_categoria[categoria] = por_categoria.get(categoria, 0) + importe
41        por_producto[producto] = por_producto.get(producto, 0) + unidades
42        por_dia[fecha] = por_dia.get(fecha, 0) + importe
43        mes = fecha[:7]
44        por_mes[mes] = por_mes.get(mes, 0) + importe
45
46    if total == 0 and not por_producto:
47        print("No hay ventas válidas.")
48        return
49
50    print(f"Ventas totales: {euros(total)}")
51    print("Por categoría:")
52    for categoria, importe in sorted(por_categoria.items(), key=lambda c: (-c[1], c[0])):
53        porcentaje = f"{100 * importe / total:.1f}".replace(".", ",")
54        print(f"  {categoria:<16}{euros(importe):>12}  {porcentaje:>5} %")
55
56    producto, unidades = min(por_producto.items(), key=lambda p: (-p[1], p[0]))
57    print(f"Producto más vendido: {producto} ({unidades} {'unidad' if unidades == 1 else 'unidades'})")
58    dia, importe = min(por_dia.items(), key=lambda d: (-d[1], d[0]))
59    print(f"Mejor día: {dia} ({euros(importe)})")
60
61    print("Por mes:")
62    for mes in sorted(por_mes):
63        barra = "#" * (por_mes[mes] // 5000)   # una almohadilla por cada 50 €
64        print(f"  {mes}  {euros(por_mes[mes]):>12} {barra}".rstrip())
65
66    lista = f" ({', '.join(map(str, descartadas))})" if descartadas else ""
67    print(f"Líneas descartadas: {len(descartadas)}{lista}")
68
69
70main()

La validación vive en leer_venta, que convierte los campos y devuelve None ante cualquier problema. El bucle principal solo decide qué hacer con el resultado: descartar la línea o acumularla.

Todo se acumula en céntimos enteros. Así 0,80 × 40 es exactamente 3200 céntimos y las sumas no arrastran errores de coma flotante; la conversión a euros ocurre una sola vez, en euros.

Cada pregunta del informe es un diccionario: por categoría, por producto, por día y por mes. Una sola pasada por las ventas basta para rellenarlos todos.

Los empates se resuelven con claves de ordenación en forma de tupla: el primer elemento es el importe en negativo (para ir de mayor a menor) y el segundo el nombre o la fecha. min con esa clave devuelve directamente el ganador.

Para ir más allá

  • Usa el módulo csv para admitir productos con comas en el nombre entre comillas.
  • Añade las ventas por día de la semana (datetime.date.fromisoformat(fecha).weekday()).
  • Escribe el informe en un fichero HTML con una tabla y barras dibujadas con CSS.

Dónde se explica