CHAPTER 10 ・ 総合演習 / Lesson 38

経費データを点検する

上限超え・重複・休日の申請・摘要の不備。ルールを関数にして、疑わしい行に理由を付けて出します。

目安 35分 ・ インストール不要

このレッスンが終わったら

「点検ルールを1つずつ関数にして組み合わせ、疑わしい行に理由を付けて一覧にできる」と言える。

STEP 1

課題とデータを見る

月末、総務や経理には、社員から経費の申請がたくさん届きます。1件ずつ目で確かめていると、見落としが出ますし、人によって基準がぶれることもあります。そこで、決まったルールで機械的に点検し、「疑わしい行」だけを人が見る形にします。

10月分の経費申請は30件です。

expenses.csvサンプルファイル
申請ID,申請日,申請者,部署,区分,金額,摘要,領収書
E001,2025-10-01,田中,営業,交通費,1240,客先訪問 東京→横浜,あり
E002,2025-10-01,佐藤,営業,会議費,3600,打合せ コーヒー,あり
E003,2025-10-02,鈴木,企画,消耗品費,8800,コピー用紙,あり
E004,2025-10-02,高橋,企画,接待交際費,42000,取引先 会食,あり
E005,2025-10-03,田中,営業,交通費,1240,客先訪問 東京→横浜,あり
E006,2025-10-04,伊藤,総務,消耗品費,15400,文房具まとめ買い,なし
E007,2025-10-05,佐藤,営業,交通費,980,客先訪問,あり
E008,2025-10-06,鈴木,企画,会議費,2400,,あり
E009,2025-10-07,高橋,企画,接待交際費,98000,取引先 会食 5名,あり
E010,2025-10-07,田中,営業,交通費,15600,大阪出張 新幹線,あり
E011,2025-10-08,伊藤,総務,通信費,5500,携帯電話,あり
E012,2025-10-08,佐藤,営業,会議費,4800,打合せ 軽食,あり
E013,2025-10-09,鈴木,企画,交通費,2100,セミナー参加,あり
E014,2025-10-09,高橋,企画,消耗品費,1980,USBケーブル,あり
E015,2025-10-10,田中,営業,接待交際費,36000,取引先 会食,あり
E016,2025-10-11,伊藤,総務,消耗品費,3300,ゴミ袋,あり
E017,2025-10-12,佐藤,営業,交通費,1240,客先訪問 東京→横浜,あり
E018,2025-10-14,鈴木,企画,会議費,52000,社内懇親会,あり
E019,2025-10-14,高橋,企画,交通費,760,客先訪問,なし
E020,2025-10-15,田中,営業,交通費,1240,客先訪問 東京→横浜,あり
E021,2025-10-15,田中,営業,交通費,1240,客先訪問 東京→横浜,あり
E022,2025-10-16,伊藤,総務,消耗品費,6600,トナー,あり
E023,2025-10-17,佐藤,営業,接待交際費,24000,取引先 会食,あり
E024,2025-10-18,鈴木,企画,交通費,1800,打合せ,あり
E025,2025-10-20,高橋,企画,会議費,3000,打合せ お茶,あり
E026,2025-10-21,伊藤,総務,通信費,5500,携帯電話,あり
E027,2025-10-22,田中,営業,交通費,-1240,取消 客先訪問,あり
E028,2025-10-23,佐藤,営業,会議費,5200,打合せ 軽食,あり
E029,2025-10-24,鈴木,企画,消耗品費,9900,書籍,あり
E030,2025-10-27,高橋,企画,交通費,980,客先訪問,あり

点検するルールは、次の6つです。上限や基準の金額は、このレッスン用の仮の数字です。実際の仕事では、社内の経費規程に合わせてください。

ルール基準(このレッスンの仮の基準)
① 区分ごとの上限交通費・消耗品費は2万円、会議費・通信費は1万円、接待交際費は5万円まで
② 重複同じ申請者・同じ申請日・同じ金額が、2件以上ある
③ 休日の申請申請日が土日、または holidays.csv にある祝日
④ 領収書なし領収書が「なし」で、金額が3,000円以上
⑤ 摘要が空欄空欄、または空白だけ
⑥ マイナス金額が0より小さい
STEP 2

設計:ルールを「部品」にする

ルールは、あとから増えたり、基準が変わったりします。そこで、ルール1つを関数1つにして、同じ形にそろえます。

ルール関数の形
def check_limitルールの名前
(row, ctx)1行分のデータと、全体の情報を受け取る
return "上限超え…"問題があれば、理由の文を返す
return ""問題がなければ、空文字を返す

全部のルールが同じ形なら、リストに並べて、順番に当てられます。ルールを足すときは、関数を1つ書いて、リストに1行足すだけです。

  1. ① 読み込む経費と祝日
  2. ② 全体の情報重複の数え上げなど
  3. ③ ルールを当てる全行×全ルール
  4. ④ 理由をつなぐ複数なら「 / 」で連結
  5. ⑤ 出力人が確認するExcel
ルールは③の中に並びます。ルールの数が増えても、流れは変わりません。

ctx は「コンテキスト(周りの情報)」の略です。重複のように、1行だけを見ても分からないルールのために、全体から先に数えておいた情報を渡します。

STEP 3

部品を1つずつ作る

読み込みと、全体の情報。 金額は、計算や比較ができるよう、数字(int)に直しておきます(Lesson 09 の通り、直せない値があれば、ここで気付けます)。

読み込みと、重複の数え上げサンプルファイル付き・書き換えOK
Ctrl / ⌘ + Enter でも実行

この欄では、サンプルファイルが使えます。実行のたびに、ファイルは最初の状態に戻ります。

結果
▶ 実行を押すと、ここに結果が出ます

田中さんの 10/15・1,240円が2件ある、と分かります。E001 と E005 も田中さんの 1,240円ですが、日付が違うので、重複には数えません。

単純な4つのルール。 1行だけで決まるルールを、テストと一緒に作ります。ちょうど基準の金額のときは、どちらになるのかが、いちばん間違えやすいところです。

上限・領収書・摘要・マイナスのルールとテストコードは書き換えられます
Ctrl / ⌘ + Enter でも実行
結果
▶ 実行を押すと、ここに結果が出ます

「上限ちょうどは問題なし」「3,000円ちょうどは対象」のように、境界をどちらにするかを、テストの形で書き残しています。基準を決めた人以外が読んでも、意図が伝わります。

休日のルール。 日付の文字を date に直し、曜日を調べます(Lesson 03)。祝日は、holidays.csv から作った辞書(日付 → 名前)で調べます。

休日のルールとテストサンプルファイル付き・書き換えOK
Ctrl / ⌘ + Enter でも実行

この欄では、サンプルファイルが使えます。実行のたびに、ファイルは最初の状態に戻ります。

結果
▶ 実行を押すと、ここに結果が出ます

weekday() は、月曜が0、日曜が6です。5以上なら土日になります。10月の経費の中には、祝日の申請はありませんが、2025-10-13(スポーツの日)で試すと、ちゃんと見つかります。使われていないルールこそ、テストで動作を確かめておく価値があります。

ルールを並べて、まとめて当てる。 作ったルールを RULES というリストに並べ、1行に全部を当てて、理由のある分だけを残します。

ルールをリストに並べて、1行に当てるコードは書き換えられます
Ctrl / ⌘ + Enter でも実行
結果
▶ 実行を押すと、ここに結果が出ます

1行目は問題がなく、空のリストです。2行目は、理由が2つ付きました。ルールを足すときは、関数を書いて RULES に名前を足すだけです。

STEP 4

組み合わせて完成させる

6つのルールをリストに並べ、全行に当てて、理由をつなぎます。重複のルールには、先に数えた情報を渡します。最後に、確認用のExcelを書き出します。

完成:経費の点検(Excel出力つき)pandas・サンプルファイル付き
Ctrl / ⌘ + Enter でも実行

この欄では、サンプルファイルが使えます。実行のたびに、ファイルは最初の状態に戻ります。

結果
▶ 実行を押すと、ここに結果が出ます
ルールを当てる部分を、日本語にする
RULES = [check_limit, check_duplicate, ...]点検ルールの関数を、名前のまま、リストに並べる
messages = [rule(row, ctx) for rule in RULES]1行に、全部のルールを順番に当てる(理由の文か、空文字が返る)
return [m for m in messages if m]空文字(問題なし)を捨てて、理由だけを残す
" / ".join(...)理由が複数あれば、「 / 」でつないで1つの文にする
row | {"理由": ...}元の行に「理由」を足した、新しい辞書を作る(元の行は書き換えない)

11件が要確認になりました。目を通すと、いくつか気付くことがあります。

  • E006:理由が2つ付いています(土曜日の申請で、領収書もなし)。連結しているので、1行で分かります
  • E020 と E021:両方に印が付きます。どちらが元の申請かは、データだけでは分からないためです
  • E027:10/22 の取消(-1,240円)です。上の重複の片方を打ち消しているのかもしれません。つなげて考えるのは、人の仕事です

「要確認」シートには、空の「確認結果」列を付けました。確認した人が、ここに「問題なし」「差し戻し」などを書き込めます。ブラウザではダウンロードできないので、書き出したExcelを読み直して、シート名・行数・先頭行の理由を確かめています。自分のパソコンで動かせば、フォルダに 経費点検.xlsx ができます。

STEP 5

自分で書く

お題① ルールを足す。 区分に関係なく、50,000円以上の申請に「高額(承認の確認)」と出すルール check_big を完成させてください。該当する申請IDを表示します。

お題①:高額のルールを足すサンプルファイル付き・書き換えOK
Ctrl / ⌘ + Enter でも実行

この欄では、サンプルファイルが使えます。実行のたびに、ファイルは最初の状態に戻ります。

結果
▶ 実行を押すと、ここに結果が出ます
ヒントを見る

if row["金額"] >= 50000: return "高額(承認の確認)" と書き、最後に return "" を残します。理由の文が空でなければ、any(...) が真になります。

答えの例を見る
import csv

with open("expenses.csv", encoding="utf-8") as f:
  rows = list(csv.DictReader(f))
for row in rows:
  row["金額"] = int(row["金額"])


def check_big(row, ctx):
  if row["金額"] >= 50000:
      return "高額(承認の確認)"
  return ""


RULES = [check_big]
ids = [r["申請ID"] for r in rows if any(rule(r, {}) for rule in RULES)]
print(ids)

お題② 基準を変える。 領収書の基準を「3,000円以上」から「500円以上」に変えると、領収書なしの該当は何件になるでしょう。基準の数字を変えて、該当する申請IDを表示してください。

お題②:領収書の基準を500円にするサンプルファイル付き・書き換えOK
Ctrl / ⌘ + Enter でも実行

この欄では、サンプルファイルが使えます。実行のたびに、ファイルは最初の状態に戻ります。

結果
▶ 実行を押すと、ここに結果が出ます
ヒントを見る

変えるのは RECEIPT_MIN の数字1か所だけです。ルールの関数は、書き換えなくても動きます。

答えの例を見る
import csv

with open("expenses.csv", encoding="utf-8") as f:
  rows = list(csv.DictReader(f))
for row in rows:
  row["金額"] = int(row["金額"])

RECEIPT_MIN = 500


def check_receipt(row, ctx):
  if row["領収書"] == "なし" and row["金額"] >= RECEIPT_MIN:
      return f"領収書なし({RECEIPT_MIN:,}円以上)"
  return ""


print([r["申請ID"] for r in rows if check_receipt(r, {})])

お題③ ルールの結果を集計する。 休日の申請(check_holiday が理由を返したもの)の金額の合計を表示してください。

お題③:休日の申請の金額合計サンプルファイル付き・書き換えOK
Ctrl / ⌘ + Enter でも実行

この欄では、サンプルファイルが使えます。実行のたびに、ファイルは最初の状態に戻ります。

結果
▶ 実行を押すと、ここに結果が出ます
ヒントを見る

sum(r["金額"] for r in rows if check_holiday(r, ctx)) の形です。理由の文が空でなければ、if の条件は真になります。

答えの例を見る
import csv
from datetime import date

with open("expenses.csv", encoding="utf-8") as f:
  rows = list(csv.DictReader(f))
for row in rows:
  row["金額"] = int(row["金額"])
with open("holidays.csv", encoding="utf-8") as f:
  holidays = {r["日付"]: r["名称"] for r in csv.DictReader(f)}
ctx = {"holidays": holidays}


def check_holiday(row, ctx):
  d = date.fromisoformat(row["申請日"])
  if d.weekday() >= 5:
      return "休日の申請"
  if row["申請日"] in ctx["holidays"]:
      return "休日の申請"
  return ""


print(sum(r["金額"] for r in rows if check_holiday(r, ctx)))
STEP 6

振り返りと、実務に持ち帰る

理解チェック点検ルールを「同じ形の関数」にして、リストに並べる最大の利点は何でしょう?

理解チェック同じ人・同じ日・同じ金額の申請が2件あるとき、両方の行に「重複の疑い」の印を付けるのはなぜでしょう?