Excel y documentos de trabajo

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.

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

ColumnaFormatoCriterio de comprobación
record_idTexto como R001Debe ser único dentro del archivo
date2026-01-06YYYY-MM-DD, una fecha real
task자료 정리No puede estar vacío
hours2.50Nú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

  1. 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.
  2. 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.
  3. 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.
bash
python --version
python -m pip install -r requirements.txt
python example.py

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.

monthrecord_counttotal_hours
2026-0136.50
2026-0237.50
2026-0339.50
Total general de comprobación923.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.
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

  1. Comprueba en el CSV de resumen que el mes 1 sume 2.50 + 1.00 + 3.00 = 6.50 horas.
  2. Comprueba que la suma mensual de record_count coincida con los 9 registros de entrada.
  3. Comprueba que la suma de total_hours sea 23.50 y contrástala con los valores de las barras del gráfico.
  4. 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

ProblemaCómo corregirlo
duplicate record_idComprueba si has introducido el mismo registro dos veces.
day is out of range / Invalid isoformatRevisa si hay fechas imposibles, como 2026-02-30, o formatos de fecha incorrectos.
hours must be a numberIntroduce 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 malVuelve a guardar el CSV original en UTF-8.
outputs already existsConserva 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.

Criterios de redacción y verificación de todo el sitio

Archivos de ejemplo para ejecutar

Incluye código, datos de entrada e instrucciones de ejecución. Extrae el ZIP y lee primero README.txt.

Descargar ZIP de ejemplo

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.