CHAPTER 10 ・ 総合演習 / Lesson 39
月次レポートを自動で作る
読み込み・整形・集計・前月比・Excel出力を、関数でつないだ1本の流れにします。
このレッスンが終わったら
「月次の作業を、読み込み→整形→集計→出力の関数に分けて、1本の流れにできる。テストで結果を守れる」と言える。
課題とデータを見る
毎月、月が替わると、決まって頼まれる仕事があります。「先月の売上を、得意先別・商品別・地域別にまとめて、前の月と比べてください」。Excelで毎月ピボットテーブルを作り直している人も多いでしょう。同じ手順の繰り返しは、Pythonの得意分野です。
材料は、売上明細(sales_detail.csv)と、得意先・商品の一覧(customers.csv、products.csv)です。まず、明細の中身を確かめます。
この欄では、サンプルファイルが使えます。実行のたびに、ファイルは最初の状態に戻ります。
▶ 実行を押すと、ここに結果が出ます
2025年4月から9月までの明細が128行あります。1行は「伝票番号・日付・得意先コード・商品コード・数量・単価」で、得意先名や地域は、別のファイルにあります。
今回作るレポートの中身は、次の通りです。指定した月(例:2025-09)について、
- 得意先別・商品別・地域別の、当月の売上・前月の売上・前月比・構成比
- 売上の多い上位5社
- 金額は、数量×単価の合計(税抜)。棒グラフは使わず、Excelの表示形式(桁区切り、マイナスは赤字)で見やすくする
設計:4つの関数と、まとめ役
作業を、4つの関数と、それをつなぐ main に分けます。このような「小さな処理を順につないだ1本の流れ」を、パイプラインと呼ぶこともあります。
- load_data3つのCSVを読む
- clean売上と年月を作り、名前を付ける
- summarize当月・前月・前月比・構成比
- check_totals検算:合計が合うか
- write_reportExcelに書く
| 関数 | 受け取るもの | 返すもの |
|---|---|---|
load_data() | なし | 明細・得意先・商品の3つの表 |
clean(...) | 3つの表 | 売上と年月の列がある、名前付きの明細 |
summarize(df, key, month, prev) | 明細、集計の軸、対象月、前月 | 集計の表 |
write_report(path, tables) | 出力先、集計の表 | なし(Excelを書く) |
main(month, dry_run) | 対象月、書き込むかどうか | 集計の表(確認用) |
決めておくことが、3つあります。
- 対象月は引数で渡す:来月は、コードではなく、引数の
"2025-10"を変えるだけにする - 前月が無いとき、前月比は空欄にする:0で割れないので、「比べられない」ものとして扱う
dry_run:Trueのときは、集計と検算だけ行い、ファイルを書かない(Lesson 29)。本番の前に試せる
部品を1つずつ作る
前月の求め方。 「2025-01 の前月」は「2024-12」です。文字の計算では間違えやすいので、pandas の月の型 Period に任せます。
この欄では、サンプルファイルが使えます。実行のたびに、ファイルは最初の状態に戻ります。
▶ 実行を押すと、ここに結果が出ます
読み込みと整形。 売上を計算し、年月の列を作り、得意先名・地域・商品名を結合します(Lesson 23)。結合の前後で行数が変わっていないかを、必ず確かめます。
この欄では、サンプルファイルが使えます。実行のたびに、ファイルは最初の状態に戻ります。
▶ 実行を押すと、ここに結果が出ます
validate="many_to_one" は、「右の表(得意先・商品)に、同じコードが2行あったらエラー」という安全装置です。重複があると行が増え、売上が二重に数えられてしまうので、行数の確認と合わせて守ります。
集計。 当月と前月を並べて、前月比と構成比を求めます。まず、ごく小さなデータで手計算できる形にして、テストします。
この欄では、サンプルファイルが使えます。実行のたびに、ファイルは最初の状態に戻ります。
▶ 実行を押すと、ここに結果が出ます
ポイントは .where(out["前月"] > 0) です。「前月が0より大きいところだけ、比べる。それ以外は空欄(NaN)にする」という意味で、0で割る問題を避けています。B社のように前月はあったのに当月が0の得意先も、表に残ります。取引が途切れたことに気付けるからです。
Excelの見た目。 数字をそのまま書くと、桁区切りも%も付きません。Excelの表示形式を設定すると、値は変えずに、見た目だけを整えられます(Lesson 27)。
この欄では、サンプルファイルが使えます。実行のたびに、ファイルは最初の状態に戻ります。
▶ 実行を押すと、ここに結果が出ます
0.0%;[Red]-0.0% は、「プラスは普通に、マイナスは赤字で」という書式です。ここで確かめたのは、値が元の数字のままであることと、書式が設定されていることです。
組み合わせて完成させる
部品を main でつなぎます。ログ(Lesson 12)で、途中の様子を残します。最後に、本番の書き込み、書き出したExcelの読み直し、前月が無い月、データの無い月を試します。
この欄では、サンプルファイルが使えます。実行のたびに、ファイルは最初の状態に戻ります。
▶ 実行を押すと、ここに結果が出ます
結果を読みましょう。
- 9月の売上合計は 1,036,500円で、得意先別・商品別・地域別の3つの合計が、すべて明細と一致しました。一致しなければ、
check_totalsが止めてくれます - 前月(8月)と比べると、約66%の減少です(サンプルデータの都合です)。実際の仕事で、こんなに減っていたら、まずデータの取り込み漏れや、入力の遅れを疑います
- 得意先では、サクラ印刷が299,000円でトップ、アサヒ建材は前月の約4.6倍(前月比で約+360%)です。8月にあって9月が0円の得意先(スミレ運輸・カエデ設計)も、表に残っています
main("2025-04")は、前月(3月)のデータが無いので、前月比が空欄になります。エラーにはならず、レポートは作れますmain("2025-12")は、データが無い月なので、分かりやすい日本語のエラーで止まります(Lesson 10)
dry_run=True(既定)なら、集計と検算だけで、ファイルは作りません。main("2025-04") のログに、その表示が出ています。ブラウザではダウンロードできないので、書き出した 月次レポート_2025-09.xlsx を読み直して確かめました。自分のパソコンで動かせば、フォルダにファイルができます。
自分で書く
お題① 月を指定して、その月の売上合計を返す関数 total_of(df, month) を完成させてください。7月・8月の合計と、8月の前月比(小数1桁の%)を表示します。
この欄では、サンプルファイルが使えます。実行のたびに、ファイルは最初の状態に戻ります。
▶ 実行を押すと、ここに結果が出ます
ヒントを見る
df[df["年月"] == month]["売上"].sum() で合計が出ます。int(...) で整数にして、return で返します。
答えの例を見る
import pandas as pd
sales = pd.read_csv("sales_detail.csv")
df = sales.assign(売上=sales["数量"] * sales["単価"], 年月=sales["日付"].str[:7])
def total_of(df, month):
return int(df[df["年月"] == month]["売上"].sum())
jul = total_of(df, "2025-07")
aug = total_of(df, "2025-08")
print(jul, aug)
print(f"{(aug / jul - 1):.1%}" if jul else "前月なし")お題② 2025年8月の売上の上位3社の得意先名を、売上の多い順にリストで表示してください。(customers.csv の結合と、nlargest を使います)
この欄では、サンプルファイルが使えます。実行のたびに、ファイルは最初の状態に戻ります。
▶ 実行を押すと、ここに結果が出ます
ヒントを見る
df[df["年月"] == "2025-08"].groupby("得意先名")["売上"].sum() のあと、.nlargest(3).index.tolist() で、名前のリストになります。
答えの例を見る
import pandas as pd
sales = pd.read_csv("sales_detail.csv")
customers = pd.read_csv("customers.csv")
df = sales.assign(売上=sales["数量"] * sales["単価"], 年月=sales["日付"].str[:7])
df = df.merge(customers[["得意先コード", "得意先名"]], on="得意先コード", how="left", validate="many_to_one")
by_customer = df[df["年月"] == "2025-08"].groupby("得意先名")["売上"].sum()
print(by_customer.nlargest(3).index.tolist())お題③ 検算の関数 check_total(detail_total, table_total) を完成させてください。2つの合計が違うときは、ValueError("合計が合いません") を出します。
▶ 実行を押すと、ここに結果が出ます
ヒントを見る
if detail_total != table_total: のあとに、raise ValueError("合計が合いません") と書きます。
答えの例を見る
def check_total(detail_total, table_total):
if detail_total != table_total:
raise ValueError("合計が合いません")
for a, b in [(1036500, 1036500), (1036500, 1036000)]:
try:
check_total(a, b)
print("OK")
except ValueError as e:
print(e)振り返りと、実務に持ち帰る
import argparse
def parse_args(argv=None):
parser = argparse.ArgumentParser(description="月次の売上レポートを作ります")
parser.add_argument("--month", required=True, help="対象月(例 2025-09)")
parser.add_argument("--dry-run", action="store_true", help="ファイルを書き込まずに確認だけする")
return parser.parse_args(argv)
if __name__ == "__main__":
args = parse_args()
main(args.month, dry_run=args.dry_run)python report.py --month 2025-09 --dry-run
python report.py --month 2025-09理解チェックmain(month, dry_run) のように、対象月と dry_run を「引数」にしておく利点は?
理解チェック前月の売上が0円の商品について、前月比はどう扱うのが適切でしょう?