Python / LESSON 18 / PY18

Python:総合演習:業務データ集計スクリプト

1学習の目的

2基礎解説

工程使う道具学んだ章
① 取り込むopen / csv / read_csvPY11・PY16
② 検証するif / raise / try-exceptPY04・PY10
③ 集計する辞書 / groupby / datetimePY07・PY15・PY16
④ 出力するf-string / json / ファイル書き込みPY03・PY11・PY15
✅ 実務スクリプトの設計原則
  1. 「取り込む → 検証する → 集計する → 出力する」の4工程に分ける。1つの関数に全部詰め込まない。どこで壊れたかが分かる構造にする。
  2. 外部から来るデータは必ず汚れていると考える。1行の不正で全体を止めず、スキップして件数と理由を記録する。
  3. 計算の途中結果は変数に名前を付けて残す。1行に詰め込むと、検算もデバッグもできなくなる。
  4. 変わりうる値(税率・しきい値・パス)は先頭に定数としてまとめる。仕様変更のとき探し回らずに済む。
  5. 集計の要になる関数にはテストを書く。特に境界値と空データ。合計が合わない事故はここで防ぐ。

この章の題材:受注データのCSVを読み込み、不正行を弾いて、商品別・月別・部門別に集計し、レポートをJSONで保存する——実際の業務でそのまま使える形のスクリプトを完成させます。

# 実務スクリプトの骨格
CSV_PATH = "orders.csv"        # ① 定数は先頭に

rows = load(CSV_PATH)          # ② 取り込む
orders, errors = validate(rows)  # ③ 検証する(不正はerrorsへ)
report = summarize(orders)     # ④ 集計する
output(report)                 # ⑤ 出力する

# 各工程を関数に分けておけば、
# 「CSVではなくAPIから取る」に変わっても load() だけ差し替えれば済む

3基本ドリル(10問)

PY18-D01 rows = ["A,100", "B,250", "C,80"] の各行をカンマで分割し、金額の合計を「合計:430」の形式で出力せよ。 コーディング★☆☆無料
期待される結果

出力 == 合計:430

ヒント

r.split(",")[1] で金額部分の文字列が取れる。int() で数値に変換してから合計する。

模範解答
rows = ["A,100", "B,250", "C,80"]

total = sum(int(r.split(",")[1]) for r in rows)

print(f"合計:{total}")
解説

CSV1行を分解して数値化する最小形。取り込んだ値は必ず文字列なので、計算の前に int() が必要になる。

PY18-D02 外部から届いたCSVを集計するとき、最初にやるべきことはどれか。
A. いきなり合計を計算する B. データが想定どおりの形式か検証する C. グラフを描く D. ファイルを削除する
選択★☆☆無料
期待される結果

解答 == B

ヒント

汚れたデータで計算すると何が起きるか。

模範解答
B
解説

検証を飛ばすと間違った数字が正しい顔をして出てくるのが最悪のパターン。エラーで止まるほうがまだましで、静かに間違うのが一番怖い。

PY18-D03 orders = [("A", 100), ("B", 200), ("A", 150)] から商品別の合計を辞書で求め、そのまま出力せよ。 コーディング★☆☆無料
期待される結果

出力 == {'A': 250, 'B': 200}

ヒント

空の辞書を用意し、totals[item] = totals.get(item, 0) + amount で足し込む。

模範解答
orders = [("A", 100), ("B", 200), ("A", 150)]

totals = {}
for item, amount in orders:
    totals[item] = totals.get(item, 0) + amount

print(totals)
解説

PY07で学んだ集計イディオム。業務集計の9割はこの3行で書ける。キーを変えれば月別にも部門別にもなる。

PY18-D04 100以上の売上だけを取り出すコードを完成させよ。 穴埋め★☆☆無料
コード
sales = [120, 45, 300, 80]
big = [s ____ s in sales if s >= 100]

print(big)
期待される結果

出力 == [120, 300]

ヒント

内包表記の語順。角カッコの中で使う繰り返しのキーワード。

模範解答
for
解説

絞り込みの if は for の後ろ(PY09)。この1行で「条件に合うデータだけ」を作れる。

PY18-D05 5,000行のCSVのうち1行だけ金額が壊れていた。最も適切な対応はどれか。
A. プログラムを止めて全体を中断する B. その行だけスキップし、件数と理由を記録して残りを処理する C. 0として集計に含める D. 例外を握りつぶして何も記録しない
選択★☆☆無料
期待される結果

解答 == B

ヒント

残り4,999行の価値と、後から原因を追えることの両方を考える。

模範解答
B
解説

Cは集計結果が静かに狂うので最悪。Dは記録が残らず原因追跡ができない。飛ばす。ただし必ず記録するが実務の答え。

PY18-D06 商品名と金額を持つ Order クラスを定義し、[Order("A", 100), Order("B", 250)] の金額合計を出力せよ。 コーディング★★☆無料
期待される結果

出力 == 350

ヒント

sum(o.amount for o in orders) と書ける。

模範解答
class Order:
    def __init__(self, item, amount):
        self.item = item
        self.amount = amount


orders = [Order("A", 100), Order("B", 250)]

print(sum(o.amount for o in orders))
解説

タプルのままでも集計はできるが、クラスにすると o.amount と名前で読める。項目が5個6個と増えたときに効いてくる。

PY18-D07 日付文字列から「年月」を取り出すコードを完成させよ。 穴埋め★★☆無料
コード
from datetime import datetime

d = datetime.____("2026-01-15", "%Y-%m-%d")

print(d.strftime("%Y年%m月"))
期待される結果

出力 == 2026年01月

ヒント

文字列を解析して日時にするほう(PY15)。

模範解答
strptime
解説

文字列 → 日時 → 別の書式の文字列という変換が、月次集計のキーを作る基本手順。

PY18-D08 同じ集計処理を3つのスクリプトから使いたい。最も適切な方法はどれか。
A. 3箇所にコピーする B. 関数にして共通モジュールに置き import する C. 毎回書き直す D. グローバル変数に入れる
選択★★☆無料
期待される結果

解答 == B

ヒント

仕様が変わったとき、直す場所は何箇所であるべきか。

模範解答
B
解説

コピーは直し忘れが必ず起きる。PY08の関数化とPY12のモジュール分割は、まさにこの問題を解くための道具。

PY18-D09 data = [("2026-01-05", 100), ("2026-02-10", 200), ("2026-01-20", 150)] から月別の合計を辞書で求め、そのまま出力せよ(キーは "2026-01" の形式)。 コーディング★★☆無料
期待される結果

出力 == {'2026-01': 250, '2026-02': 200}

ヒント

strptime で日時に変換してから strftime("%Y-%m") で月のキーを作り、D03と同じ方法で足し込む。

模範解答
from datetime import datetime

data = [("2026-01-05", 100), ("2026-02-10", 200), ("2026-01-20", 150)]

monthly = {}
for date, amount in data:
    key = datetime.strptime(date, "%Y-%m-%d").strftime("%Y-%m")
    monthly[key] = monthly.get(key, 0) + amount

print(monthly)
解説

集計キーを作ってから辞書に足す——月別も四半期別も曜日別も、キーの作り方を変えるだけで対応できる。

PY18-D10 文字列を金額に変換する関数 parse_amount を定義せよ。数値でなければ ValueError(int() が自動で出す)、マイナスなら raise ValueError("金額がマイナスです") とすること。assert で正常系を確認し、"-5" で例外が出たら「テスト成功」と出力せよ。 コーディング★★☆無料
期待される結果

出力 == テスト成功

ヒント

assert parse_amount("100") == 100 で正常系を確認し、異常系は try/except ValueError: で受け止める。

模範解答
def parse_amount(text):
    value = int(text)

    if value < 0:
        raise ValueError("金額がマイナスです")

    return value


assert parse_amount("100") == 100

try:
    parse_amount("-5")
    print("NG:例外が出ませんでした")
except ValueError:
    print("テスト成功")
解説

検証関数を作り、それをテストするという実務の型。この小さな関数が、後の集計全体の正しさを支える土台になる。

4実践シナリオ(5問)

PY18-S01 【工程①②:取り込みと検証】次の8行の受注データを1行ずつ検証せよ。
raw = [
  "2026-01-05,ノートPC,法人,128000",
  "2026-01-08,マウス,個人,3200",
  "2026-01-12,ノートPC,法人,abc",
  "2026-01-15,キーボード,個人",
  "2026-01-20,モニター,法人,45000",
  "2026-02-03,マウス,法人,3200",
  "2026-02-10,ノートPC,法人,128000",
  ",サーバー,法人,300000",
]


形式は「日付,商品,部門,金額」。検証ルール:項目が4つ未満 →「項目数が不足しています」/日付が空 →「日付が空です」/金額が数値でない →「金額が不正です」。
正常なら「[1] OK 2026-01-05 ノートPC 128,000円」、不正なら「[3] NG 金額が不正です」の形式で出力し、最後に「有効:5件 / スキップ:3件」と出力せよ。
コーディング★★☆無料
期待される結果

出力9行が完全一致

ヒント

enumerate(raw, 1) で行番号を振り、split(",") で分解。検証は raise ValueError(...) で通知し、except ValueError as e: でまとめて受け止める。OKの後ろは半角スペース3つ、NGの後ろも3つで揃える。

模範解答
raw = [
    "2026-01-05,ノートPC,法人,128000",
    "2026-01-08,マウス,個人,3200",
    "2026-01-12,ノートPC,法人,abc",
    "2026-01-15,キーボード,個人",
    "2026-01-20,モニター,法人,45000",
    "2026-02-03,マウス,法人,3200",
    "2026-02-10,ノートPC,法人,128000",
    ",サーバー,法人,300000",
]

valid = []
skipped = 0

for i, row in enumerate(raw, 1):
    parts = row.split(",")

    try:
        if len(parts) < 4:
            raise ValueError("項目数が不足しています")

        if not parts[0].strip():
            raise ValueError("日付が空です")

        try:
            amount = int(parts[3])
        except ValueError:
            raise ValueError("金額が不正です")

    except ValueError as e:
        print(f"[{i}] NG   {e}")
        skipped += 1
        continue

    valid.append((parts[0], parts[1], parts[2], amount))
    print(f"[{i}] OK   {parts[0]} {parts[1]} {amount:,}円")

print(f"有効:{len(valid)}件 / スキップ:{skipped}件")
解説

検証は raise、対応は except、不正行は continue(PY10)。ここで作った valid のリストが、以降すべての集計の入力になる。汚れたデータを最初の関門で止めるのが、信頼できる集計の絶対条件。

PY18-S02 【クラスで表現する】受注1件を表す Order クラス(date / item / dept / amount)を定義し、__str__ で「2026-01-05 ノートPC(法人)128,000円」の形式にせよ。
S01で有効だった5件を Order のリストにして1行ずつ出力し、最後に「合計:307,400円」と出力すること。
コーディング★★☆無料
期待される結果

出力6行が完全一致

ヒント

有効データは (日付, 商品, 部門, 金額) のタプルのリストとして与えられている前提でよい。合計は sum(o.amount for o in orders)。

模範解答
class Order:
    def __init__(self, date, item, dept, amount):
        self.date = date
        self.item = item
        self.dept = dept
        self.amount = amount

    def __str__(self):
        return f"{self.date} {self.item}({self.dept}){self.amount:,}円"


rows = [
    ("2026-01-05", "ノートPC", "法人", 128000),
    ("2026-01-08", "マウス", "個人", 3200),
    ("2026-01-20", "モニター", "法人", 45000),
    ("2026-02-03", "マウス", "法人", 3200),
    ("2026-02-10", "ノートPC", "法人", 128000),
]

orders = [Order(*r) for r in rows]

for o in orders:
    print(o)

print(f"合計:{sum(o.amount for o in orders):,}円")
解説

Order(*r) はタプルを展開して引数に渡す書き方。タプルの3番目、ではなく o.dept と書けるようになり、以降のコードが一気に読みやすくなる(PY13)。

PY18-S03 【工程③:3つの軸で集計】S02の5件の受注データについて、商品別(金額の多い順)・月別(昇順)・部門別(昇順)の合計を集計して出力せよ。
「[商品別]」「[月別]」「[部門別]」の見出しを付け、商品別は件数も表示すること(「ノートPC:256,000円(2件)」)。
コーディング★★☆無料
期待される結果

出力10行が完全一致

ヒント

3つの辞書を用意し、1回のループで同時に足し込む。月のキーは strptime → strftime("%Y-%m")。商品別は sorted(..., key=lambda x: -x[1])、他は sorted() で並べる。

模範解答
from datetime import datetime

rows = [
    ("2026-01-05", "ノートPC", "法人", 128000),
    ("2026-01-08", "マウス", "個人", 3200),
    ("2026-01-20", "モニター", "法人", 45000),
    ("2026-02-03", "マウス", "法人", 3200),
    ("2026-02-10", "ノートPC", "法人", 128000),
]

by_item = {}
by_count = {}
by_month = {}
by_dept = {}

for date, item, dept, amount in rows:
    by_item[item] = by_item.get(item, 0) + amount
    by_count[item] = by_count.get(item, 0) + 1

    key = datetime.strptime(date, "%Y-%m-%d").strftime("%Y-%m")
    by_month[key] = by_month.get(key, 0) + amount

    by_dept[dept] = by_dept.get(dept, 0) + amount

print("[商品別]")
for item, amount in sorted(by_item.items(), key=lambda x: -x[1]):
    print(f"{item}:{amount:,}円({by_count[item]}件)")

print("[月別]")
for month, amount in sorted(by_month.items()):
    print(f"{month}:{amount:,}円")

print("[部門別]")
for dept, amount in sorted(by_dept.items()):
    print(f"{dept}:{amount:,}円")
解説

1回のループで4つの辞書を同時に育てるのが要点。データを4周する必要はない。集計軸を増やしたければ辞書を1つ足すだけで済む構造になっている。

PY18-S04 【工程④:レポートの保存と読み返し】集計結果を辞書にまとめ、"report.json" に保存(ensure_ascii=False)してから読み返して内容を表示せよ。
保存する項目:total(総売上)/ count(件数)/ average(平均単価・整数)/ by_month(月別の辞書)。
「保存しました:report.json」に続けて、読み返した各項目を「total: 307400」の形式で4行出力すること。
コーディング★★★無料
期待される結果

出力5行が完全一致

ヒント

平均は int(total / count)。json.dump でファイルに保存し、json.load で読み返す(s が付かないほう)。表示は for key, value in loaded.items(): で回す。

模範解答
import json

rows = [
    ("2026-01-05", "ノートPC", "法人", 128000),
    ("2026-01-08", "マウス", "個人", 3200),
    ("2026-01-20", "モニター", "法人", 45000),
    ("2026-02-03", "マウス", "法人", 3200),
    ("2026-02-10", "ノートPC", "法人", 128000),
]

total = sum(r[3] for r in rows)

report = {
    "total": total,
    "count": len(rows),
    "average": int(total / len(rows)),
    "by_month": {"2026-01": 176200, "2026-02": 131200},
}

with open("report.json", "w", encoding="utf-8") as f:
    json.dump(report, f, ensure_ascii=False)

print("保存しました:report.json")

with open("report.json", encoding="utf-8") as f:
    loaded = json.load(f)

for key, value in loaded.items():
    print(f"{key}: {value}")
解説

JSONなら数値も辞書も型のまま復元できる(PY15)。翌月このファイルを読み込めば前月比が計算できるし、他のシステムへの受け渡しにもそのまま使える。

PY18-S05 【集計処理をテストする】受注のリストから商品別合計を返す関数 summarize_by_item(rows) を定義し、次の4観点のテストを書け(test_summary.py として pytest で実行)。
① 通常の集計 ② 同じ商品はまとめられる ③ 空のリストなら空の辞書 ④ 1件だけの場合
コーディング★★★無料
期待される結果

pytest 実行で 4 passed

ヒント

③の「空のとき」は忘れられがちだが、月初など実データで普通に起きる。ゼロ除算や KeyError の温床なので必ずテストする。

模範解答
def summarize_by_item(rows):
    totals = {}
    for _, item, _, amount in rows:
        totals[item] = totals.get(item, 0) + amount
    return totals


def test_通常の集計():
    rows = [
        ("2026-01-05", "A", "法人", 100),
        ("2026-01-06", "B", "個人", 200),
    ]
    assert summarize_by_item(rows) == {"A": 100, "B": 200}


def test_同じ商品はまとめられる():
    rows = [
        ("2026-01-05", "A", "法人", 100),
        ("2026-01-06", "A", "個人", 150),
    ]
    assert summarize_by_item(rows) == {"A": 250}


def test_空のリストなら空の辞書():
    assert summarize_by_item([]) == {}


def test_1件だけの場合():
    rows = [("2026-01-05", "A", "法人", 100)]
    assert summarize_by_item(rows) == {"A": 100}
解説

集計関数にテストが付いていれば、合計が合わない事故は起きない。特に③のような空データのテストは、実装を書き換えたときに真価を発揮する(PY17)。

5仕上げ課題

PY18-FINAL 【受注データ月次集計システム】これまでの全工程を1本のスクリプトに統合せよ。Python編の卒業課題

RAW = [
  "2026-01-05,ノートPC,法人,128000",
  "2026-01-08,マウス,個人,3200",
  "2026-01-12,ノートPC,法人,abc",
  "2026-01-15,キーボード,個人",
  "2026-01-20,モニター,法人,45000",
  "2026-02-03,マウス,法人,3200",
  "2026-02-10,ノートPC,法人,128000",
  ",サーバー,法人,300000",
]


要件
① Order クラス(date / item / dept / amount)と、月キーを返すメソッド month() を定義
validate(raw) … 行を検証して (Orderのリスト, エラーのリスト) を返す。エラーは (行番号, 理由) のタプル
 検証ルールはS01と同じ(項目数不足/日付が空/金額が不正)
summarize(orders) … 商品別(金額の多い順)・月別(昇順)・部門別(昇順)・総売上・平均単価・最高額の取引をまとめた辞書を返す
④ 集計結果を画面に出力し、report.json に保存(ensure_ascii=False)

出力仕様(23行)
=== 受注データ月次集計 ===
[取り込み]
有効:5件 / スキップ:3件
- 3行目:金額が不正です
- 4行目:項目数が不足しています
- 8行目:日付が空です
---
[商品別]
ノートPC:256,000円(2件)
モニター:45,000円(1件)
マウス:6,400円(2件)
---
[月別]
2026-01:176,200円
2026-02:131,200円
---
[部門別]
個人:3,200円
法人:304,200円
---
総売上:307,400円 / 平均単価:61,480円
最高額:ノートPC(128,000円)
レポートを report.json に保存しました
期待される結果

出力23行が完全一致

ヒント

工程ごとに関数を分けるのが最大のコツ。validate は検証だけ、summarize は集計だけを担当させ、print は最後にまとめて行う。最高額の取引は max(orders, key=lambda o: o.amount) で取れる。平均単価は int(総売上 / 件数)。商品別の並べ替えは sorted(..., key=lambda x: -x[1])。

模範解答
import json
from datetime import datetime

REPORT_PATH = "report.json"

RAW = [
    "2026-01-05,ノートPC,法人,128000",
    "2026-01-08,マウス,個人,3200",
    "2026-01-12,ノートPC,法人,abc",
    "2026-01-15,キーボード,個人",
    "2026-01-20,モニター,法人,45000",
    "2026-02-03,マウス,法人,3200",
    "2026-02-10,ノートPC,法人,128000",
    ",サーバー,法人,300000",
]


class Order:
    def __init__(self, date, item, dept, amount):
        self.date = date
        self.item = item
        self.dept = dept
        self.amount = amount

    def month(self):
        return datetime.strptime(self.date, "%Y-%m-%d").strftime("%Y-%m")

    def __str__(self):
        return f"{self.date} {self.item}({self.dept}){self.amount:,}円"


def validate(raw):
    orders = []
    errors = []

    for i, row in enumerate(raw, 1):
        parts = row.split(",")

        try:
            if len(parts) < 4:
                raise ValueError("項目数が不足しています")

            if not parts[0].strip():
                raise ValueError("日付が空です")

            try:
                amount = int(parts[3])
            except ValueError:
                raise ValueError("金額が不正です")

        except ValueError as e:
            errors.append((i, str(e)))
            continue

        orders.append(Order(parts[0], parts[1], parts[2], amount))

    return orders, errors


def summarize(orders):
    by_item = {}
    by_count = {}
    by_month = {}
    by_dept = {}

    for o in orders:
        by_item[o.item] = by_item.get(o.item, 0) + o.amount
        by_count[o.item] = by_count.get(o.item, 0) + 1
        by_month[o.month()] = by_month.get(o.month(), 0) + o.amount
        by_dept[o.dept] = by_dept.get(o.dept, 0) + o.amount

    total = sum(o.amount for o in orders)
    best = max(orders, key=lambda o: o.amount)

    return {
        "by_item": dict(sorted(by_item.items(), key=lambda x: -x[1])),
        "by_count": by_count,
        "by_month": dict(sorted(by_month.items())),
        "by_dept": dict(sorted(by_dept.items())),
        "total": total,
        "count": len(orders),
        "average": int(total / len(orders)),
        "best_item": best.item,
        "best_amount": best.amount,
    }


orders, errors = validate(RAW)
report = summarize(orders)

print("=== 受注データ月次集計 ===")
print("[取り込み]")
print(f"有効:{len(orders)}件 / スキップ:{len(errors)}件")

for line_no, reason in errors:
    print(f" - {line_no}行目:{reason}")

print("---")
print("[商品別]")
for item, amount in report["by_item"].items():
    print(f"{item}:{amount:,}円({report['by_count'][item]}件)")

print("---")
print("[月別]")
for month, amount in report["by_month"].items():
    print(f"{month}:{amount:,}円")

print("---")
print("[部門別]")
for dept, amount in report["by_dept"].items():
    print(f"{dept}:{amount:,}円")

print("---")
print(f"総売上:{report['total']:,}円 / 平均単価:{report['average']:,}円")
print(f"最高額:{report['best_item']}({report['best_amount']:,}円)")

with open(REPORT_PATH, "w", encoding="utf-8") as f:
    json.dump(report, f, ensure_ascii=False)

print(f"レポートを {REPORT_PATH} に保存しました")
解説

これがPython編の到達点です。

このスクリプト1本に、変数(PY02)・f-string(PY03)・条件分岐(PY04)・ループ(PY05)・リスト(PY06)・辞書(PY07)・関数(PY08)・内包表記(PY09)・例外処理(PY10)・ファイル操作(PY11)・クラス(PY13)・datetime と json(PY15)が全部入っています。18項目・270問を通して積み上げてきたものが、ここで1つの動くシステムになりました。

そして重要なのは、これが練習用のコードではないということです。RAW を実際のCSVファイル読み込みに差し替えれば、明日から業務で使えます。集計軸を追加したければ summarize に辞書を1つ足すだけ、出力先をExcelに変えたければ最後の数行を差し替えるだけ——変更に強い構造で書けているからです。

ここまで来たあなたは、もう「Pythonを勉強している人」ではありません。Pythonで問題を解ける人です。次に学ぶべきものは、あなたが解きたい問題が決めてくれます。お疲れさまでした。