Crear una tabla y un gráfico mensuales a partir de registros de trabajo
Valida las fechas y las horas de trabajo del CSV, y guarda el número de registros y las horas totales de cada mes en un CSV y un gráfico de barras.
Contenido revisado 2026.09.19Incluye archivos de ejemplo
Ver el índice
La traducción se ha realizado con IA. Comprueba el código, las unidades y los valores junto con el original. La revisión por hablantes nativos de cada idioma aún no se ha completado. 한국어
Para quién esPersonal administrativo que quiere automatizar los resúmenes mensuales recurrentes con código sencillo de Python
Preparación
Prepara Python 3.12 y una terminal. Comprueba la versión con python --version.
Extrae todo el ZIP descargado y abre la terminal en la carpeta que contiene example.py.
Ejecuta primero con los datos de práctica incluidos y compara el resultado con el original.
Se usan work_log.csv y Matplotlib. El CSV debe tener codificación UTF-8 o UTF-8 BOM.
01Empezar por definir qué representa una fila
Cada fila se considera un registro de trabajo y sus hours se suman al mes de date. record_count cuenta filas, no personas ni días trabajados. hours expresa horas decimales: 1.50 significa 1 hora y 30 minutos, no 1 hora y 50 minutos.
Los datos incluidos son 9 registros de trabajo ficticios de los meses 1~3 de 2026. No es una herramienta para medir el tiempo trabajado ni evaluar el rendimiento, sino una práctica para sumar por mes cifras ya introducidas.
02Comprobar las columnas y los valores de entrada
Columna
Formato
Criterio de comprobación
record_id
Texto como R001
Debe ser único dentro del archivo
date
2026-01-06
YYYY-MM-DD, una fecha real
task
자료 정리
No puede estar vacío
hours
2.50
Número finito mayor o igual que 0, con un máximo de dos decimales
No incluyas filas de título, filas de totales ni números con comas. Si se detecta un ID duplicado, el programa se detiene para evitar un doble recuento. Aunque la tarea sea la misma, los ID distintos se suman como registros diferentes.
03Preparar los archivos y ejecutar
Extrae el ZIP y comprueba que el archivo de entrada indicado en README.txt esté en la misma ubicación que example.py. No ejecutes directamente dentro del ZIP.
Ejecuta los siguientes comandos uno por uno desde la carpeta que contiene example.py. Si en Windows no tienes el comando python pero sí py, sustituye python por py -3.12 en cada comando.
Cuando aparezca el mensaje de finalización, abre la nueva carpeta outputs. Si ya existe outputs, guárdala primero con otro nombre y vuelve a ejecutar.
La instalación requiere conexión a Internet. BASE establece las rutas de entrada y salida a partir de la ubicación de example.py, no de la ubicación actual de la terminal.
04Código completo y método de resumen
Las fechas se validan con date.fromisoformat y se crea una clave mensual en formato YYYY-MM. Las horas se suman con Decimal y se registran en la tabla con dos decimales. Solo se convierten a números de coma flotante al dibujar el gráfico. Se usa el modo Agg, por lo que se guarda un PNG en lugar de abrir una ventana de gráfico.
example.py
"""Summarize fictional work-log records by calendar month."""
import csv
import math
from collections import defaultdict
from datetime import date
from decimal import Decimal, InvalidOperation
from pathlib import Path
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
BASE = Path(__file__).resolve().parent
HEADERS = ["record_id", "date", "task", "hours"]
def main():
destination = BASE / "outputs"
if destination.exists():
raise ValueError("outputs already exists; rename it before running again.")
totals = defaultdict(lambda: {"records": 0, "hours": Decimal("0")})
seen = set()
with (BASE / "work_log.csv").open(encoding="utf-8-sig", newline="") as file:
reader = csv.DictReader(file)
if reader.fieldnames != HEADERS:
raise ValueError(f"Expected headers: {HEADERS}")
for line, row in enumerate(reader, start=2):
if None in row or any(value is None or not value.strip() for value in row.values()):
raise ValueError(f"Row {line}: missing or extra field.")
if row["record_id"] in seen:
raise ValueError(f"Row {line}: duplicate record_id.")
seen.add(row["record_id"])
parsed_date = date.fromisoformat(row["date"])
if parsed_date.isoformat() != row["date"]:
raise ValueError(f"Row {line}: use YYYY-MM-DD dates.")
try:
hours = Decimal(row["hours"])
except InvalidOperation as error:
raise ValueError(f"Row {line}: hours must be a number.") from error
if not hours.is_finite() or hours < 0 or hours.as_tuple().exponent < -2:
raise ValueError(f"Row {line}: hours must be finite, nonnegative, with at most 2 decimals.")
month = parsed_date.strftime("%Y-%m")
totals[month]["records"] += 1
totals[month]["hours"] += hours
if not totals:
raise ValueError("No work records were found.")
months = sorted(totals)
figure, axis = plt.subplots(figsize=(8, 4.6), layout="constrained")
values = [float(totals[month]["hours"]) for month in months]
if not all(math.isfinite(value) and math.isfinite(value * 1.2) for value in values):
plt.close(figure)
raise ValueError("Monthly total is too large to plot as a finite number.")
bars = axis.bar(months, values, color="#2B6578", width=0.55)
axis.bar_label(bars, labels=[f"{value:.2f}" for value in values], padding=4)
axis.set(title="Monthly work hours | Fictional data", xlabel="Calendar month", ylabel="Hours")
axis.set_ylim(0, max(values) * 1.2 if max(values) > 0 else 1)
axis.spines[["top", "right"]].set_visible(False)
axis.set_axisbelow(True)
axis.grid(axis="y", alpha=0.2)
destination.mkdir(exist_ok=False)
with (destination / "monthly_summary.csv").open("x", encoding="utf-8-sig", newline="") as file:
writer = csv.writer(file)
writer.writerow(["month", "record_count", "total_hours"])
for month in months:
writer.writerow([month, totals[month]["records"], f"{totals[month]['hours']:.2f}"])
figure.savefig(destination / "monthly_hours.png", dpi=160)
plt.close(figure)
total_hours = sum((values["hours"] for values in totals.values()), Decimal("0"))
print(f"Records: {len(seen)}; months: {len(months)}; hours: {total_hours:.2f}")
print("Created outputs/monthly_summary.csv and outputs/monthly_hours.png.")
if __name__ == "__main__":
try:
main()
except (OSError, ValueError, csv.Error) as error:
raise SystemExit(f"Stopped: {error}") from error
05Leer la tabla y el gráfico de resumen
Se crean outputs/monthly_summary.csv y outputs/monthly_hours.png. El gráfico muestra solo los meses con datos de entrada, ordenados por año y mes. Se han elegido ejes en inglés para poder ejecutar sin instalar fuentes coreanas adicionales.
month
record_count
total_hours
2026-01
3
6.50
2026-02
3
7.50
2026-03
3
9.50
Total general de comprobación
9
23.50
Gráfico de horas mensuales de registros de trabajo ficticios. Mes 1: 6.5 horas; mes 2: 7.5 horas; mes 3: 9.5 horas.
06Comprobar los totales y el cambio de mes
Comprueba en el CSV de resumen que el mes 1 sume 2.50 + 1.00 + 3.00 = 6.50 horas.
Comprueba que la suma mensual de record_count coincida con los 9 registros de entrada.
Comprueba que la suma de total_hours sea 23.50 y contrástala con los valores de las barras del gráfico.
Copia la carpeta de práctica, cambia la fecha del último registro a 2026-04-01 y vuelve a ejecutar. Comprueba que se reparta en 5.50 horas para el mes 3 y 4.00 horas para el mes 4.
07Cuando el programa se detiene por fechas o números
Problema
Cómo corregirlo
duplicate record_id
Comprueba si has introducido el mismo registro dos veces.
day is out of range / Invalid isoformat
Revisa si hay fechas imposibles, como 2026-02-30, o formatos de fecha incorrectos.
hours must be a number
Introduce solo números para las horas, como 2.5. Elimina las unidades de tiempo y las comas de separación de miles.
hours must be finite…
Busca valores negativos, NaN, Infinity o valores con tres o más decimales.
Los caracteres coreanos se muestran mal
Vuelve a guardar el CSV original en UTF-8.
outputs already exists
Conserva los resultados anteriores y cambia el nombre de la carpeta de resultados.
08Qué debes saber antes de incluirlo en un informe
Se agrupan por meses naturales registros que solo tienen fecha. No se procesan horas de inicio o fin, turnos nocturnos, zonas horarias ni tareas que cruzan la medianoche.
Los meses sin datos no se convierten en filas de 0 horas, porque hay que distinguir entre registros ausentes y un valor real de 0 horas.
Solo se bloquean los ID duplicados. Si la misma tarea se ha introducido dos veces con ID diferentes, hay que revisar los datos originales.
El CSV de resumen y el PNG reflejan el momento de la ejecución. Si modificas el CSV, debes volver a ejecutar para obtener resultados nuevos.
Registro de ejecución y verificación
Windows 11 (10.0.26200), CPython 3.12.14 (64 bits). openpyxl 3.1.5, Matplotlib 3.10.8. Las versiones de las bibliotecas usadas en cada ejemplo están fijadas en requirements.txt.
Comprobadas 3 filas mensuales, 23.50 horas y 9 registros
Verificadas la creación del PNG y los resultados al cambiar un registro de mes
Comprobados errores de fechas incorrectas, ID duplicados, horas negativas o NaN y cabeceras
Comprobado que el hash del original no cambia y que se conservan los resultados existentes
Límites de la verificación
No se verifica si las reglas coinciden con las de sistemas reales de recursos humanos o control de asistencia.
El código de ejemplo, los nombres de archivo y las claves de entrada se mantienen como en el original. Consulta también los comandos y los pasos de comprobación del texto traducido.
Material de práctica original · Guarda los archivos originales por separado antes de ejecutarlo.
Fuentes de referencia
Las explicaciones y los ejemplos son de elaboración propia. Puedes consultar los comportamientos y conceptos relacionados en las siguientes fuentes oficiales.
Busca filas con las cuatro columnas idénticas y reúnelas en un nuevo archivo de Excel junto con sus números de fila originales. Revisa cada grupo de duplicados incluyendo su primera aparición.