Excel・文書業務

業務記録を月別の集計表とグラフにする

CSVの日付と作業時間を検証し、月別の記録数と合計時間をCSVと棒グラフに保存します。

目次を表示

この翻訳はAIで作成しました。コード、単位、数値は原文と併せて確認してください。各言語のネイティブ話者による校閲は、まだ完了していません。 한국어

対象読者繰り返し発生する月次の業務集計を、簡単なPythonコードで処理したい事務担当者

準備するもの
  • Python 3.12とターミナルを用意します。python --versionでバージョンを確認します。
  • ダウンロードしたZIPをすべて展開し、example.pyがあるフォルダーでターミナルを開きます。
  • 最初は同梱の練習データで実行し、結果と元のファイルを比較します。
  • work_log.csvとMatplotlibを使います。CSVの文字コードはUTF-8またはUTF-8 BOMである必要があります。

01まず一行の意味を決める

業務記録の一行を一つの作業記録と見なし、dateが属する月にhoursを加算します。record_countは行数であり、人数や勤務日数ではありません。hoursは十進表記の時間です。1.50は1時間30分であり、1時間50分ではありません。

同梱データは、2026年1~3月の架空の業務記録9件です。作業時間を測定したり成果を評価したりするツールではなく、入力済みの数値を月別に合計する練習です。

02入力の列と値を確認する

形式確認基準
record_idR001のようなテキストファイル内で一意であること
date2026-01-06YYYY-MM-DD形式の実在する日付
task자료 정리空の値は不可
hours2.500以上の有限の数値で、小数点以下は二桁まで

タイトル行、合計行、カンマを含む数値は入れません。IDの重複が見つかると、二重集計を防ぐため停止します。作業内容が同じでも、IDが異なれば別の記録として合算します。

03ファイルを準備して実行する

  1. ZIPを展開し、README.txtに記載された入力ファイルがexample.pyと同じ場所にあるか確認します。ZIPの中から直接実行しないでください。
  2. example.pyがあるフォルダーで、以下のコマンドを一行ずつ実行します。Windowsでpythonコマンドがなくpyがある場合は、各コマンドのpythonをpy -3.12に置き換えます。
  3. 完了メッセージが表示されたら、新しいoutputsフォルダーを開きます。既存のoutputsがある場合は、先に別名で保管してから再実行します。
bash
python --version
python -m pip install -r requirements.txt
python example.py

インストールにはインターネット接続が必要です。コードのBASEは、現在のターミナル位置ではなくexample.pyの位置を基準に入力と結果のパスを決めます。

04コード全体と集計方法

date.fromisoformatで日付を検証し、YYYY-MM形式の月キーを作ります。Decimalで時間を合計し、表には小数点以下二桁まで記録します。通常の浮動小数点数に変換するのはグラフを描くときだけです。Aggモードを使うため、グラフのウィンドウは開かず、PNGを保存します。

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

05集計表とグラフを読む

outputs/monthly_summary.csvとoutputs/monthly_hours.pngが作られます。グラフは入力のある月だけを、年・月の順に表示します。軸を英語にしたのは、韓国語フォントを別途インストールせずに実行するためです。

monthrecord_counttotal_hours
2026-0136.50
2026-0237.50
2026-0339.50
確認用の総合計923.50
架空の業務記録の月別時間グラフ。1月6.5時間、2月7.5時間、3月9.5時間。
架空の業務記録の月別時間グラフ。1月6.5時間、2月7.5時間、3月9.5時間。

06合計と月の切り替わりを確認する

  1. 集計CSVで、1月が2.50 + 1.00 + 3.00 = 6.50時間になっているか確認します。
  2. 月別のrecord_countの合計が、入力記録9件と一致するか確認します。
  3. total_hoursの合計が23.50になっているか確認し、グラフの各棒の数値と照合します。
  4. 練習フォルダーをコピーし、最後の記録の日付を2026-04-01に変更して再実行します。3月が5.50時間、4月が4.00時間に分かれるか確認します。

07日付や数値が原因で停止した場合

問題修正方法
duplicate record_id同じ記録を二回入力していないか確認します。
day is out of range / Invalid isoformat2026-02-30のような存在しない日付や、日付の形式を確認します。
hours must be a number時間には2.5のような数値だけを入力します。時間の単位や桁区切りのカンマは除いてください。
hours must be finite…負の値、NaN、Infinity、小数点以下三桁以上の値がないか確認します。
韓国語の文字が正しく表示されない元のCSVをUTF-8で保存し直します。
outputs already exists前回の結果を保管してから、結果フォルダーの名前を変更します。

08報告書に載せる前に知っておく範囲

  • 日付だけを持つ記録を、暦の月ごとに集計します。開始・終了時刻、夜勤、タイムゾーン、日付をまたぐ作業は処理しません。
  • 入力がない月について、0時間の行は作りません。記録の欠落と実際の0時間を区別する必要があるためです。
  • 重複IDだけをブロックします。別々のIDで同じ作業を二回入力した場合は、元のデータの確認が必要です。
  • 集計CSVとPNGは、実行時点の結果です。CSVを変更して新しい結果を得るには、再実行する必要があります。

実行・検証の記録

Windows 11 (10.0.26200)、CPython 3.12.14(64ビット)。openpyxl 3.1.5、Matplotlib 3.10.8。各例で使用するライブラリのバージョンはrequirements.txtで固定しています。

  • 月別の3行、23.50時間、9件の記録を確認
  • PNGの作成と、月をまたぐ日付変更後の結果を検証
  • 不正な日付、重複ID、負の時間・NaN、ヘッダーエラーを検査
  • 元のファイルのハッシュ不変と、既存の結果の保全を確認
検証範囲の限界
  • 実際の人事・勤怠システムの集計ルールとの一致は、検証対象ではありません。

サイト全体の執筆・検証方針

自分で実行するためのサンプルファイル

コード、入力データ、実行手順が含まれています。展開して、まずREADME.txtを読んでください。

サンプルZIPをダウンロード

サンプルコード、ファイル名、入力キーは原文のままです。翻訳本文のコマンドと確認手順も併せて参照してください。

独自に作成した練習用資料 · 元のファイルを別に保管してから実行してください。

参考資料

説明と例は独自に作成しました。関連する動作や概念は、以下の公式資料で確認できます。