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

辞書を使いこなす(集計・Counter)

「ないキーを聞いても止まらない」get、部署別の合計、出現回数の数え上げ。辞書は集計の道具です。

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

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

「辞書は、キーごとの集計表として使える。get や defaultdict、Counter を使うと、KeyError なしで数えられる」と言える。

STEP 1

集計しようとすると、止まってしまう

「商品ごとに、何件売れたか」「得意先ごとの売上は、いくらか」。このように、キーごとに数える・足す作業を、集計と呼びます。辞書は「名前(キー)と値の対応表」なので、集計表にぴったりの入れ物です。

sales_detail.csv(4〜9月の売上明細、128行)で、商品ごとの件数を数えてみましょう。まず、基礎編の知識だけで、素直に書いてみます。

商品ごとの件数を数える(そのまま書くと…)サンプルファイル付き・書き換えOK
Ctrl / ⌘ + Enter でも実行

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

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

KeyError: 'P05' と出て止まりました。KeyErrorは「その名前(キー)は、辞書にありません」という意味です。最初の明細の商品は P05 ですが、空の辞書には、まだ P05 の項目がありません。足し算(+= 1)をする前に、まず「今の値」を読もうとして、失敗しています。

基礎編の知識で直すなら、「なければ、先に 0 を入れる」と書きます(rows は上の Run と同じ、CSVを読んだ結果です)。

counts = {}
for row in rows:
    code = row["商品コード"]
    if code not in counts:
        counts[code] = 0
    counts[code] += 1

これで動きます。ただ、「なければ作る」の3行を、集計のたびに書くのは大変です。辞書には、この手間を減らす道具が用意されています。

STEP 2

3つの道具で、数えてみる

同じ「商品ごとの件数」を、3つの道具で数えます。結果は、すべて同じになります。

  • get:counts.get(code, 0) は、「code があればその値、なければ 0」を返します。KeyError は出ません。
  • defaultdict:ないキーを読んだとき、自動で初期値を作る辞書です。最初に初期値を決めておくと、+= 1 と書くだけで済みます。
  • Counter:数えるための専用の辞書です。数える対象を渡すだけで、数えてくれます。
get・defaultdict・Counter で、同じ集計をするサンプルファイル付き・書き換えOK
Ctrl / ⌘ + Enter でも実行

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

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

3つとも、P02 が43件、P03 が34件、…と同じ数字になりましたか。最後の行の most_common(2) は、Counter だけの機能で、多い順に2件を返します。(dict(by_default) と書いているのは、defaultdict という名札を外して、ふつうの辞書の形で表示するためです。)

STEP 3

仕組みを読む

まず、いちばん基本の get を、部品に分けて見ます。

counts[code] = counts.get(code, 0) + 1 の部品
counts[code]書き込む先(code の項目)
counts.get(辞書から読む(なくても止まらない)
code探すキー
0ないときに返す値
) + 1読んだ値に 1 を足す
get で数えるループを、1行ずつ日本語にしてみる
for row in rows:明細を1行ずつ取り出して
code = row["商品コード"]その行の商品コードを取り出し
counts[code] = counts.get(code, 0) + 1今の件数(なければ 0)に 1 を足して、書き戻します

日本語にすると

「商品コードごとの件数を、今の値(まだなければ 0)に 1 を足して、更新していく」

Step 1 の if code not in counts: と見比べてください。3行が1行になりました。in は「そのキーが辞書にあるか」を調べる書き方で、値ではなくキーを調べます。

STEP 4

変えて、試す

① 件数ではなく、金額を足して、上位を出す。 得意先ごとの売上は「数量 × 単価」の合計です。CSVの値は文字なので、int で数字にしてから掛けます。items() は、辞書を「(キー, 値)」の組として取り出します。これを sorted で金額順に並べます(key=lambda は Lesson 05 の書き方です)。最後に、明細の合計と一致するかの検算もします。

得意先ごとの売上合計、上位3社、検算サンプルファイル付き・書き換えOK
Ctrl / ⌘ + Enter でも実行

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

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

最後の2つの数字(辞書の合計と、明細から直接求めた合計)が同じなら、取りこぼしはありません。

② 名前を引く。 コードから名前を引く辞書は、実務で毎日のように使います。登録のないコードを [ ] で読むと KeyError です。get なら、代わりの値を返せます。

わざと間違える:登録のないコードを [ ] で読むコードは書き換えられます
Ctrl / ⌘ + Enter でも実行
結果
▶ 実行を押すと、ここに結果が出ます
get なら、代わりの値を返せるコードは書き換えられます
Ctrl / ⌘ + Enter でも実行
結果
▶ 実行を押すと、ここに結果が出ます

③ 辞書をまとめる。 | で、2つの辞書を1つにできます。同じキーがあれば、右側が優先されます。

辞書をまとめる(右側が優先)コードは書き換えられます
Ctrl / ⌘ + Enter でも実行
結果
▶ 実行を押すと、ここに結果が出ます
STEP 5

自分で書く

お題① sales_detail.csv で、商品コード P03 が何件あるかを、表示してください。

お題①:P03 の件数サンプルファイル付き・書き換えOK
Ctrl / ⌘ + Enter でも実行

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

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

Counter(row["商品コード"] for row in rows) で数えます。作った Counter から counts["P03"] で取り出します。

答えの例を見る
import csv
from collections import Counter

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

counts = Counter(row["商品コード"] for row in rows)
print(counts["P03"])

お題② 得意先ごとの売上合計(数量 × 単価)を求め、上位3社を [(得意先コード, 合計), ...] の形で表示してください。

お題②:売上上位3社サンプルファイル付き・書き換えOK
Ctrl / ⌘ + Enter でも実行

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

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

defaultdict(int) に、数量 × 単価を足していきます。sorted(totals.items(), key=lambda kv: kv[1], reverse=True) で並べ、[:3] で上位3つです。

答えの例を見る
import csv
from collections import defaultdict

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

totals = defaultdict(int)
for row in rows:
  totals[row["得意先コード"]] += int(row["数量"]) * int(row["単価"])

ranking = sorted(totals.items(), key=lambda kv: kv[1], reverse=True)
print(ranking[:3])

お題③ customers.csv から「得意先コード → 得意先名」の辞書を作り、C003 と、登録のない C999 の名前を、1行に並べて表示してください。C999 は「(未登録)」と表示します。

お題③:名前を引く(ないときは未登録)サンプルファイル付き・書き換えOK
Ctrl / ⌘ + Enter でも実行

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

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

print(names.get("C003", "(未登録)"), names.get("C999", "(未登録)")) の形です。print に2つ渡すと、間に空白が入ります。

答えの例を見る
import csv

with open("customers.csv", encoding="utf-8") as f:
  names = {row["得意先コード"]: row["得意先名"] for row in csv.DictReader(f)}

print(names.get("C003", "(未登録)"), names.get("C999", "(未登録)"))
STEP 6

仕事ならどう使う?

理解チェックd = {"A": 1} のとき、d.get("B", 0) + 1 の結果は?

理解チェックfrom collections import defaultdict して d = defaultdict(int) とし、d["X"] を print したあと、len(d) は?