はじめに
現役の高校教員(数学)です。エンジニアではありません。
前回、マネーフォワードのCSVを分析して固定費の計上漏れを見つけた話 を書きました。その続きで、今度はAmazonの注文履歴をエクスポートして分析してみました。
対象は 2009年10月〜2026年6月の788件。ほぼ17年分です。
結論から書くと、技術的に一番の学びは家計の話ではなく、こちらでした。
同じ商品でも、Amazonの「商品名」は年月とともに変わる。だから商品名で集計してはいけない。
商品名で素朴に集計すると「最多41回」に見えた商品が、ASINで集計し直したら96回でした。2.3倍の差です。
※実際の購入データを扱っているため、商品の一部と一部の金額は伏せています。集計処理そのものは変えていません。
環境
- macOS
- Claude Code
- Python 3.9(Macに最初から入っているもの)
- Amazonからエクスポートした
Order History.csv
追加インストールはしていません。
1. Amazonから注文履歴を出す
Amazonの注文履歴は、画面から17年分をさかのぼるのは現実的ではありません。データのエクスポートを申請します。
アカウントサービス → データをリクエストする → 「注文」を選択 → 申請
申請するとメールが届き、承認するとzipがダウンロードできます。即時ではなく、数時間〜数日かかります。私の場合は当日中に届きました。
中身のうち、今回使ったのは Order History.csv の1つだけです。列は28個ありました。
ASIN, Billing Address, Carrier Name & Tracking Number, Currency,
Order Date, Order ID, Order Status, Product Name, Total Amount, ...
2. 最初の壁:今度はBOMだった
前回のマネーフォワードのCSVは Shift-JIS(cp932)でした。今回は違いました。
# 前回の経験でこう書いたら、今度は逆に失敗する
open("Order History.csv", encoding="cp932")
Amazonのそれは BOM付きUTF-8 です。普通に utf-8 で開くと、1列目のヘッダーが ASIN ではなく ASIN になります。
厄介なのは、エラーで落ちないことです。読み込みは成功して、r["ASIN"] だけが KeyError になります。原因が見えません。
# NG:エラーは出ないが、1列目のキーだけ取れない
csv.DictReader(open("Order History.csv", encoding="utf-8"))
# OK:BOMを食わせる
csv.DictReader(open("Order History.csv", encoding="utf-8-sig"))
utf-8-sig の sig は signature(BOM)のことです。これを知らないと詰みます。
Claude Codeには「1列目だけKeyErrorになる」と伝えただけで、BOMを疑うところから入ってくれました。
3. キャンセル分を外す
Order Status 列を見ると、キャンセル済みの注文が混ざっています。
rows = [r for r in rows if r["Order Status"] == "Closed"]
788件のうち 63件(8.0%)がキャンセルでした。これを外さないと支出が水増しされます。前回の「振替」列と同じ種類の罠です。
4. ここが本題:商品名で集計してはいけない
「何を一番たくさん買っているか」を知りたくて、まず素朴にこう書きました。
import collections
c = collections.Counter(r["Product Name"] for r in rows)
print(c.most_common(3))
結果、最多は 41回。それなりの回数ですが、まあこんなものかと思いました。
ところが、ユニーク数を数えて違和感が出ます。
print(len({r["Product Name"] for r in rows})) # 562
print(len({r["ASIN"] for r in rows})) # 532
725件の注文に対して、商品名は562種類あるのに、ASINは532個しかない。商品名の方が30種類多い。同じ商品が複数の名前で記録されている、ということです。
該当商品を全部並べたら、正体が分かりました。
41回 [トクホ]サントリー 伊右衛門 特茶 お茶 500ml×24本
25回 [トクホ]サントリー緑茶 伊右衛門特茶 500ml×24本
11回 [トクホ] [2CS]サントリー緑茶 伊右衛門特茶 (500ml×24本)×2箱
10回 サントリー緑茶 伊右衛門特茶 500mlペット×24本×2ケース(48本)
4回 [2CS] サントリー SUNTORY 伊右衛門 特茶 TOKUCHA ... 合計48本 トクチャ トクホ
3回 [2CS] サントリー SUNTORY 伊右衛門 特茶 TOKUCHA ... トクチャ トクホ 緑茶
1回 [2CS] サントリー SUNTORY 伊右衛門 特茶 TOKUCHA ... トクホ 緑茶 ペットボトル PET
1回 〔飲料〕 サントリー 伊右衛門 特茶 500ml PET 2ケース …
全部お茶です。同じお茶です。
しかも下の3つは、パッと見ほとんど同じに見えます。実際は末尾が違うだけでした。
print(len(name_a), len(name_b), len(name_c))
# 87 90 101
前方は完全一致していて、後ろにキーワードが足されているだけ。出品者が検索対策で商品名にワードを追加していった結果、購入時期によって記録される名前が変わっていたわけです。全角スペース版まで混ざっていました。
「不可視文字が紛れているのでは」と最初は疑ったのですが、実際は単に商品名そのものが年々伸びていたというだけの話でした。
正しい集計キーは ASIN
Amazonの商品には ASIN(Amazon Standard Identification Number)という不変のIDが振られています。CSVの1列目がそれです。
c = collections.Counter(r["ASIN"] for r in rows)
こう書き直したら、8つの商品名は 4つのASIN に集約されました。合計 96回。
商品名で数えた「41回」との差は 55回。半分以上を取りこぼしていたことになります。
さらに、単一のASIN 1個だけで 66回 買っていました。
5. 家計として何が分かったか
ASINで集計し直して初めて、金額が見えました。
| 分析期間 | 2009年10月 〜 2026年6月(約16年8ヶ月) |
| 有効注文 | 725件(キャンセル63件を除外) |
| 累計 | 約326万円 |
| 1件あたり中央値 | 2,498円 |
| 1件あたり平均 | 4,489円 |
中央値2,498円に対して平均4,489円。平均が中央値の1.8倍です。ごく少数の高額商品が平均を引っ張っているということで、「普段の買い物の感覚」は中央値の方に近い。家計を平均で語ると実感とズレるのは、これが理由でした。
そして例のお茶は、2015年から2026年まで11年間で96回、累計約115万円でした。17年間の総額のおよそ3分の1が、この1商品です。
もちろん飲んでいるので無駄遣いというわけではありません。ただ、自分がこの支出を「大きな支出」として認識したことは一度もありませんでした。1回あたり1万円ちょっとの、ごく普通の注文が11年積み上がっただけです。
前回の記事で「固定費の計上漏れ」を見つけましたが、あれは家計簿に載っていなかった話でした。今回は家計簿にはずっと載っていたのに、粒度が細かすぎて塊として見えなかった話です。逆方向の見落としでした。
6. ハマったポイントまとめ
| ハマり | 対処 |
|---|---|
1列目だけ KeyError(エラーは出ない) |
encoding="utf-8-sig" で開く(BOM付きUTF-8) |
| 支出が水増しされる |
Order Status == "Closed" で絞る(キャンセル8%) |
| 同じ商品の回数が過小に出る | 商品名ではなく ASIN で集計する |
| 商品名の表記ゆれが正規化できない | 出品者がSEOワードを追記していくため、後方一致もできない。名寄せは諦めてIDを使う |
| 平均額が実感と合わない | 中央値も出す |
7. 非エンジニアがやってみて思ったこと
前回書いた「Claude Codeはデータを外に出さなくていい」という点は、今回さらに強く感じました。Amazonの注文履歴には住所・氏名・カード下4桁の列がそのまま入っています。これをどこかにアップロードするのは、正直かなり抵抗があります。手元で読んで手元で集計できるのは大きい。
技術的な学びとしては、「集計キーに何を使うか」で結論が2.3倍変わるという体験が一番大きかったです。
集計そのものは Counter を1行書くだけで、難しいことは何もしていません。難しいのは「その集計、キーが正しいのか」に気づくことでした。そして今回それに気づけたのは、私が賢かったからではなく、ユニーク数を数えて数が合わないことに違和感を持ったからです。
データ分析の初心者向けTipsとして、これだけは書いておきます。
集計する前に、
len(set(...))を2種類以上のキーで比べる。
数が合わなければ、そこに見落としがあります。
おわりに
「何を買ったか」は覚えていなくても、記録は17年分残っています。それをどう集計するかで見える景色が変わる、というのが今回の結論でした。
非エンジニアにとっての価値は、Pythonが書けるようになることではなく、手元のデータに対して「本当にそうか?」を確かめる手段を持てることだと思っています。
次はMarkdownからPDFを自動生成する話(日本語フォントでハマった話)を書く予定です。