Excel y documentos de trabajo

Convertir registros de trabajo en una tabla y gráfico de resumen mensual

Valida las fechas y las horas de trabajo de un CSV y después guarda los recuentos mensuales de registros y las horas totales como 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. English

Para quién esPersonal de oficina que quiere gestionar resúmenes mensuales recurrentes de trabajo con código Python sencillo

Preparación
  • Ten preparados Python 3.12 y una terminal. Comprueba la versión con python --version.
  • Extrae por completo el ZIP descargado y abre una terminal en la carpeta que contiene example.py.
  • Empieza con los datos de práctica incluidos y después compara los resultados con los archivos originales.
  • Este ejemplo utiliza work_log.csv y Matplotlib. El CSV debe usar encoding UTF-8 o UTF-8 BOM.

01Empezar definiendo qué significa una fila

Trata cada fila como un registro de trabajo y suma hours al mes que contiene su date. record_count es el número de filas, no el número de personas ni de días laborables. hours utiliza horas decimales. 1.50 significa 1 hora y 30 minutos, no 1 hora y 50 minutos.

Los datos incluidos contienen 9 registros de trabajo ficticios de enero a marzo de 2026. Se trata de un ejercicio de suma de números existentes por mes, no de una herramienta para medir el tiempo de trabajo ni evaluar el rendimiento.

02Comprobar las columnas y valores de entrada

ColumnaFormatoRegla de validación
record_idTexto como R001Debe ser único dentro del archivo
date2026-01-06YYYY-MM-DD; una fecha que exista realmente
task자료 정리No debe estar vacío
hours2.50Un número finito de 0 o más, con hasta dos decimales

No añadas title rows, total rows ni números con comas. Si el script encuentra un duplicate ID, se detiene para evitar doble conteo. Los records con IDs diferentes se cuentan por separado aunque sus task descriptions coincidan.

03Preparar los archivos y ejecutar el ejemplo

  1. Extrae el ZIP y comprueba que el input file indicado en README.txt esté en la misma carpeta que example.py. No ejecutes el ejemplo desde dentro del ZIP archive.
  2. Ejecuta los commands siguientes, una línea cada vez, en la carpeta que contiene example.py. En Windows, si python no está disponible pero sí py, sustituye python en cada command por py -3.12.
  3. Cuando aparezca el completion message, abre la nueva carpeta outputs. Si outputs ya existe, cámbiale primero el nombre para conservar los resultados anteriores y después vuelve a ejecutar el ejemplo.
bash
python --version
python -m pip install -r requirements.txt
python example.py

La instalación requiere conexión a internet. BASE en el código establece los paths de input y output relativos a example.py, no a la terminal folder actual.

04Código completo y método de agregación

El código valida las fechas con date.fromisoformat y crea month keys con formato YYYY-MM. Suma hours con Decimal y los escribe en la tabla con dos decimales. Los values se convierten a ordinary floating-point numbers únicamente para plotting. Agg mode guarda un PNG en lugar de abrir una chart window.

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 de resumen y el gráfico

El script crea outputs/monthly_summary.csv y outputs/monthly_hours.png. El chart muestra únicamente los months presentes en el input, ordenados por year y month. Los axis labels en inglés permiten ejecutar el ejemplo sin instalar una Korean font separada.

monthrecord_counttotal_hours
2026-0136.50
2026-0237.50
2026-0339.50
Total general para comprobación923.50
Monthly hours chart for fictional work records. January: 6.5 hours; February: 7.5 hours; March: 9.5 hours.
Monthly hours chart for fictional work records. January: 6.5 hours; February: 7.5 hours; March: 9.5 hours.

06Comprobar los totales y los límites entre meses

  1. En el summary CSV, comprueba que enero sume 2.50 + 1.00 + 3.00 = 6.50 hours.
  2. Comprueba que la suma de los valores mensuales de record_count coincida con los 9 input records.
  3. Comprueba que total_hours sume 23.50 y compara el valor de cada mes con el número de su chart bar.
  4. Copia la practice folder, cambia la date del último record a 2026-04-01 y vuelve a ejecutar. Comprueba que las hours se dividan en 5.50 para marzo y 4.00 para abril.

07Cuando una fecha o un número detiene el script

ProblemaCómo corregirlo
duplicate record_idComprueba si el mismo record se introdujo dos veces.
day is out of range / Invalid isoformatComprueba si hay fechas imposibles como 2026-02-30 o un date format incorrecto.
hours must be a numberIntroduce únicamente un número como 2.5 para hours. Elimina unit labels y thousands separators.
hours must be finite…Comprueba si hay valores negativos, NaN, Infinity o más de dos decimales.
Korean text appears garbledVuelve a guardar el CSV original en UTF-8.
outputs already existsConserva los resultados anteriores y después cambia el nombre de la result folder.

08Conocer los límites antes de utilizar los resultados en un informe

  • Los records que contienen solo dates se agrupan por calendar month. No se gestionan start and end times, night shifts, time zones ni tasks que crucen medianoche.
  • Los months sin input no se añaden como filas con 0 hours. Los missing records deben distinguirse de 0 hours reales.
  • Solo se bloquean duplicate IDs. Comprueba en los datos originales si la misma task se introdujo dos veces con IDs distintos.
  • El summary CSV y el PNG reflejan los datos en el momento de la ejecución. Después de editar el CSV, vuelve a ejecutar el script para obtener nuevos resultados.

Registro de ejecución y verificación

Windows 11 (10.0.26200), CPython 3.12.14 (64-bit). openpyxl 3.1.5, Matplotlib 3.10.8. Las bibliotecas utilizadas por cada ejemplo están fijadas en requirements.txt.

  • Se confirmaron 3 filas mensuales, 23.50 hours y 9 records
  • Se verificaron la creación del PNG y los resultados después de cambiar un record a través de un month boundary
  • Se comprobaron fechas no válidas, duplicate IDs, negative/NaN hours y header errors
  • Se confirmaron los hashes originales sin cambios y la conservación de los resultados existentes
Límites de la verificación
  • La correspondencia con las aggregation rules de sistemas reales de HR o attendance está fuera del alcance de la verificación.

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.