CHAPTER 2 ・ データ構造を使いこなす / Lesson 07

集合で、突き合わせる

請求した先と入金のあった先の、差を取る。「あるのにない」「ないのにある」を一瞬で見つけます。

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

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

「集合は重複のない箱。引き算(-)や共通部分(&)で、2つの一覧の差を一瞬で出せる」と言える。

STEP 1

請求したのに、入金がない先はどこ?

月末になると、経理では「請求書を出した先」と「実際にお金が入った先」を見比べます。2つの一覧を照らし合わせて、食い違いを探す作業を、突合(とつごう)と呼びます。

invoices_billed.csv は請求の一覧(12件)、payments.csv は銀行の入金明細(10行)です。入金の「摘要」の先頭に、請求番号が書かれています(INV-2025-0001 アオイデンキ のように)。この番号を取り出して、未入金の請求を探してみましょう。基礎編の for と in を使うと、こう書けます。

リストと for で、未入金を探すサンプルファイル付き・書き換えOK
Ctrl / ⌘ + Enter でも実行

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

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

split() は、空白で文字を区切ります。[0] は、その先頭、つまり請求番号の部分です。また startswith("INV-") で、請求番号の書かれていない入金(摘要が「ウケトリキン…」など)を除いています。

未入金は4件と分かりました。動きはしますが、気になる点があります。入金は9件なのに、請求は12件です。9件の中に、同じ番号が2回出てくるものがあるようです(二重入金)。また、「リストを for で回し、if で探す」書き方は、データが数万行になると遅くなります。

このような「2つの一覧の差」を扱うのに向いているのが、集合(set)です。

STEP 2

集合に直して、引き算する

リストを set(...) で包むと、重複のない箱になります。2つの箱は、-(引き算)で、「片方にあって、もう片方にないもの」を一瞬で出せます。

集合の引き算で、未入金を出すサンプルファイル付き・書き換えOK
Ctrl / ⌘ + Enter でも実行

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

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

さきほどと同じ4件が、for も if もなしで出ました。{ ... for ... } は、Lesson 01 の内包表記の「集合版」です({ } の中に「キー: 値」ではなく、値だけを書くと、集合になります)。

入金の件数が、9件から8件に減っていることにも注目してください。二重入金だった INV-2025-0008 が、1つにまとまりました。

STEP 3

仕組みを読む

sorted(billed - paid) の部品
billed請求した番号の集合
-「あって、もう片方にない」
paid入金のあった番号の集合
sorted( )集合は順番を持たないので、並べ直して表示

集合には、4つの記号があります。2つの集合を a・b とします。

集合の4つの記号
a & bどちらにもある(共通部分)
a | bどちらかにある(合わせたもの)
a - ba にあって、b にない
a ^ b片方だけにある(どちらにもあるものは除く)

日本語にすると

billed - paid は、「請求した番号のうち、入金のあった番号に入っていないもの」。つまり、未入金です。
STEP 4

変えて、試す

① 4つの記号を、小さな例で。 社員名簿と、研修の受講者名簿を比べます。

名簿と受講者の突き合わせコードは書き換えられます
Ctrl / ⌘ + Enter でも実行
結果
▶ 実行を押すと、ここに結果が出ます

「名簿にない受講者」(山田)は、名簿の漏れか、入力の間違いかもしれません。引き算は、向きを入れ替えると、別の問題の答えになります。両方向を調べるのが、突合の基本です。

② 重複を除く。順番は保つ。 set で重複は消えますが、順番も失われます。順番を残したいときは、dict.fromkeys(...) を使います(辞書のキーは重複できないので、結果的に重複が消え、最初に出た順番が残ります)。

重複を除く2つの方法サンプルファイル付き・書き換えOK
Ctrl / ⌘ + Enter でも実行

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

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

list(set(paid)) と書くと、並びが実行のたびに変わることがあります。順番が大事なときは dict.fromkeys、並べ替えてよいときは sorted(set(...)) にしましょう。

③ 同じものが2回出たかを見つける。 「見たものを seen(見た)という集合に覚えておき、すでに入っていたら重複」という型です。

二重入金の請求番号を探すサンプルファイル付き・書き換えOK
Ctrl / ⌘ + Enter でも実行

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

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

自分で書く

お題① 未入金の請求番号を、番号順のリストで表示してください。入金のうち請求番号のあるもの(INV- で始まる摘要)の、先頭の番号を集合にして使います。

お題①:未入金の請求番号サンプルファイル付き・書き換えOK
Ctrl / ⌘ + Enter でも実行

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

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

paid = {row["摘要"].split()[0] for row in pay_rows if row["摘要"].startswith("INV-")} で入金側の集合ができます。sorted(billed - paid) を print します。

答えの例を見る
import csv

with open("invoices_billed.csv", encoding="utf-8") as f:
  billed = {row["請求番号"] for row in csv.DictReader(f)}

with open("payments.csv", encoding="utf-8") as f:
  pay_rows = list(csv.DictReader(f))

paid = {row["摘要"].split()[0] for row in pay_rows if row["摘要"].startswith("INV-")}
print(sorted(billed - paid))

お題② 入金に、請求にない番号が含まれていないか確かめます(向きを入れ替えた引き算です)。ない場合は、空の集合が表示されます。

お題②:請求にない入金サンプルファイル付き・書き換えOK
Ctrl / ⌘ + Enter でも実行

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

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

paid - billed です。引き算は、向きで意味が変わります。結果が空なら、set() と表示されます。

答えの例を見る
import csv

with open("invoices_billed.csv", encoding="utf-8") as f:
  billed = {row["請求番号"] for row in csv.DictReader(f)}

with open("payments.csv", encoding="utf-8") as f:
  paid = {row["摘要"].split()[0] for row in csv.DictReader(f) if row["摘要"].startswith("INV-")}

print(paid - billed)

お題③ expenses.csv から、「同じ申請者・同じ申請日・同じ金額」の申請が2回目以降に出てきたものの、申請IDを表示してください。3つの値を (申請者, 申請日, 金額) の組にして、seen に覚えます。

お題③:重複した経費申請サンプルファイル付き・書き換えOK
Ctrl / ⌘ + Enter でも実行

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

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

seen = set() を用意し、for で回して key = (row["申請者"], row["申請日"], row["金額"]) を作ります。key が seen に入っていれば申請IDをリストに足し、最後に seen.add(key) します。

答えの例を見る
import csv

with open("expenses.csv", encoding="utf-8") as f:
  rows = list(csv.DictReader(f))

seen = set()
dup = []
for row in rows:
  key = (row["申請者"], row["申請日"], row["金額"])
  if key in seen:
      dup.append(row["申請ID"])
  seen.add(key)
print(dup)
STEP 6

仕事ならどう使う?

理解チェックa = {1, 2, 3}、b = {3, 4} のとき、a - b の結果は?

理解チェック社員名簿 members と、研修の受講者 trained があります。「名簿にない人が、受講者に紛れていないか」を調べる式は?