Python:総合演習:業務データ集計スクリプト
1学習の目的
- PY01〜PY17で学んだ道具を組み合わせ、汚れた実データを取り込んで集計しレポートを出力する一連の処理を、自力で設計・実装できるようになる。
- 「どの場面でどの道具を使うか」を判断できるようになる。文法を知っていることと、仕事で使えることの差がここで埋まる。
2基礎解説
| 工程 | 使う道具 | 学んだ章 |
|---|---|---|
| ① 取り込む | open / csv / read_csv | PY11・PY16 |
| ② 検証する | if / raise / try-except | PY04・PY10 |
| ③ 集計する | 辞書 / groupby / datetime | PY07・PY15・PY16 |
| ④ 出力する | f-string / json / ファイル書き込み | PY03・PY11・PY15 |
- 「取り込む → 検証する → 集計する → 出力する」の4工程に分ける。1つの関数に全部詰め込まない。どこで壊れたかが分かる構造にする。
- 外部から来るデータは必ず汚れていると考える。1行の不正で全体を止めず、スキップして件数と理由を記録する。
- 計算の途中結果は変数に名前を付けて残す。1行に詰め込むと、検算もデバッグもできなくなる。
- 変わりうる値(税率・しきい値・パス)は先頭に定数としてまとめる。仕様変更のとき探し回らずに済む。
- 集計の要になる関数にはテストを書く。特に境界値と空データ。合計が合わない事故はここで防ぐ。
この章の題材:受注データの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問)
出力 == 合計: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() が必要になる。
A. いきなり合計を計算する B. データが想定どおりの形式か検証する C. グラフを描く D. ファイルを削除する 選択★☆☆無料
解答 == B
汚れたデータで計算すると何が起きるか。
B
検証を飛ばすと間違った数字が正しい顔をして出てくるのが最悪のパターン。エラーで止まるほうがまだましで、静かに間違うのが一番怖い。
出力 == {'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行で書ける。キーを変えれば月別にも部門別にもなる。
sales = [120, 45, 300, 80] big = [s ____ s in sales if s >= 100] print(big)
出力 == [120, 300]
内包表記の語順。角カッコの中で使う繰り返しのキーワード。
for
絞り込みの if は for の後ろ(PY09)。この1行で「条件に合うデータだけ」を作れる。
A. プログラムを止めて全体を中断する B. その行だけスキップし、件数と理由を記録して残りを処理する C. 0として集計に含める D. 例外を握りつぶして何も記録しない 選択★☆☆無料
解答 == B
残り4,999行の価値と、後から原因を追えることの両方を考える。
B
Cは集計結果が静かに狂うので最悪。Dは記録が残らず原因追跡ができない。飛ばす。ただし必ず記録するが実務の答え。
出力 == 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個と増えたときに効いてくる。
from datetime import datetime
d = datetime.____("2026-01-15", "%Y-%m-%d")
print(d.strftime("%Y年%m月"))
出力 == 2026年01月
文字列を解析して日時にするほう(PY15)。
strptime
文字列 → 日時 → 別の書式の文字列という変換が、月次集計のキーを作る基本手順。
A. 3箇所にコピーする B. 関数にして共通モジュールに置き import する C. 毎回書き直す D. グローバル変数に入れる 選択★★☆無料
解答 == B
仕様が変わったとき、直す場所は何箇所であるべきか。
B
コピーは直し忘れが必ず起きる。PY08の関数化とPY12のモジュール分割は、まさにこの問題を解くための道具。
出力 == {'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)
集計キーを作ってから辞書に足す——月別も四半期別も曜日別も、キーの作り方を変えるだけで対応できる。
出力 == テスト成功
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問)
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 のリストが、以降すべての集計の入力になる。汚れたデータを最初の関門で止めるのが、信頼できる集計の絶対条件。
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)。
「[商品別]」「[月別]」「[部門別]」の見出しを付け、商品別は件数も表示すること(「ノート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つ足すだけで済む構造になっている。
保存する項目: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)。翌月このファイルを読み込めば前月比が計算できるし、他のシステムへの受け渡しにもそのまま使える。
① 通常の集計 ② 同じ商品はまとめられる ③ 空のリストなら空の辞書 ④ 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仕上げ課題
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で問題を解ける人です。次に学ぶべきものは、あなたが解きたい問題が決めてくれます。お疲れさまでした。