この記事でわかること

  • 明細の2つの列の組み合わせごとに、件数を数える方法
  • 合計の行と列、店舗ごとの割合の付け方
  • 空欄の行が件数に入らないことの確かめ方
  • 並び順・表記の揺れ・書き出したときの見出しのつまずき

注文の明細から「どの店舗で、どの商品の注文が何件あったか」を一覧にしたいことがあります。Excelなら、店舗と商品の組み合わせごとにCOUNTIFSの式を並べますが、店舗や商品が増えると式も増えます。この記事では、Pythonのpandasのcrosstabで、店舗を行・商品を列にした件数の表を作り、合計と割合を足して別のファイルに書き出すまでをまとめます。

調べたのは、pandas 3.1.0の公式の資料(crosstabと、ユーザーガイドのcrosstabの節)で、2026年11月1日に原文に当たりました。表の店舗と商品は、確かめるために作った架空のものです。

先に結論: crosstabに行と列の2つの列を渡す

crosstabに、行に並べる列(店舗)と、列に並べる列(商品)を渡すと、組み合わせごとの件数の表ができます。 合計はmargins、割合はnormalizeで足します。この記事で使う指定は次の3つです。

指定 働き 記事のコード
margins 合計の行と列を足す True
margins_name 合計の行と列の名前 "合計"
normalize 件数を割合に直す "index"(店舗ごと)

crosstabのしくみ

pandasの資料では、crosstabは2つ(またはそれ以上)の項目のクロス集計を作る関数で、値と集計の方法を渡さなければ、件数の表を作るとしています。注文の1行が1件として数えられ、明細に1行もない組み合わせのマスは0になります。

margins=Trueを付けると、合計の行と列が足されます。名前は、margins_nameで決めます。資料では、初期の名前は「All」です。

normalizeは、件数を割合に直す指定です。資料では、"index"なら行ごと、"columns"なら列ごと、Trueなら全体で割るとしています。"index"を渡すと、店舗ごとに、どの商品の注文が多いかを比べられます。

手順: 店舗と商品の件数表を書き出す

次のコードは、架空の注文の表 orders.xlsx(注文番号・店舗・商品の3列)を読み、店舗と商品の組み合わせごとの件数と、店舗ごとの割合を、orders_counts.xlsx の2つのシートに書き出します。orders.xlsx には書き込みません。列の名前は、自分の表に合わせて書き換えます。

import pandas as pd

# orders.xlsx は読み込むだけで、書き換えない
orders = pd.read_excel("orders.xlsx")

# 店舗と商品の組み合わせごとの件数に、合計の行と列を付ける
count_table = pd.crosstab(orders["店舗"], orders["商品"], margins=True, margins_name="合計")

# 店舗ごとに、商品の件数の割合を出す(各行を足すと1になる)
share_table = pd.crosstab(orders["店舗"], orders["商品"], normalize="index").round(3)

# 空欄の行は数えられないので、表の行数と数えた件数を見比べる
print("表の行数:", len(orders), "数えた件数:", count_table.loc["合計", "合計"])
print(count_table)

with pd.ExcelWriter("orders_counts.xlsx") as writer:
    count_table.to_excel(writer, sheet_name="件数")
    share_table.to_excel(writer, sheet_name="店舗ごとの割合")

pandasのほかに、xlsxを読み書きするopenpyxlも入れておきます。店舗の名前が行の見出しになるので、to_excelではindex=Falseを付けていません。

試した結果

架空の注文の表(12行)で動かしました。店舗は新宿店・渋谷店・池袋店の3つ、商品はノート・ペン・消しゴムの3つで、渋谷店の1行だけ商品を空欄にしています。

  • 新宿店はノート2件・ペン2件・消しゴム0件で、合計4件でした。明細に1行もない組み合わせは0になりました
  • 渋谷店は4行ありますが、合計は3件でした。商品が空欄の1行は数えられていません
  • 表示は「表の行数: 12 数えた件数: 11」で、空欄の1行の分だけ合わないことが、この1行で分かりました
  • 店舗ごとの割合では、池袋店がノート0.5・ペン0.25・消しゴム0.25になり、どの店舗も足すと1でした
  • orders_counts.xlsx には「件数」と「店舗ごとの割合」の2つのシートができ、元の orders.xlsx のハッシュ値は実行の前後で同じでした

使ったのは Mac の作業PCで、Python は3.12.15、pandas は3.1.0、openpyxl は3.1.5でした。できたファイルの見た目は、Excelのアプリでは開いて確かめていません。

自分で直すか、相談するかの判断

目安は、「表の行数と、数えた件数の合計が同じか」です。同じなら、取りこぼしはありません。合わないときは、店舗か商品が空欄の行があります。その行を数えに入れるか、外すかは、表を受け取る人の決まりで変わります。

キャンセルした注文を数えるか、1つの注文に商品が2つあるときに2件と数えるか、といった数え方の決まりも、コードでは決められません。数える前に、表を受け取る人と決めておきます。

つまずきやすいところ

  • 空欄の行を数えたい: 試したときも、商品が空欄の行は件数に入りませんでした。先に orders["商品"].fillna("(空欄)") のように文字で埋めてから渡すと、「(空欄)」の列ができ、渋谷店に1件と数えられました
  • 並び順が明細と違う: 試した表では、店舗が新宿店・池袋店・渋谷店の順に並び、明細に出てきた順(新宿店・渋谷店・池袋店)になりませんでした。行や列は文字の順に並ぶので、決まった順にしたいときは、数えた後に並べ替えます
  • 同じ店舗が2行に分かれる: 店舗の名前の後ろに空白が付いた「新宿店 」があると、「新宿店」とは別の行として数えられました。数える前に、前後の空白を取ってそろえます
  • 書き出すと「商品」の見出しが消える: 試したときは、件数のシートの左上に「店舗」だけが入り、列の見出しの名前の「商品」は残りませんでした。必要なら、シートの名前やファイルの説明で補います
  • 件数ではなく金額を集計したい: 金額を店舗と月のような2つの項目で合計する方法は、PythonでExcelのピボットテーブルのように集計する手順で紹介しています
  • 金額を範囲に分けてから数えたい: 金額に範囲の名前を付ける方法は、PythonでExcelの数値を区切って範囲ごとの件数を数える手順にまとめています。範囲の列を作ってから、crosstabの列に渡せます
  • 割合を明細の行ごとに付けたい: 表にまとめるのではなく、元の行に「店舗の中での割合」の列を足したいときは、PythonでExcelのグループごとの割合の列を作る手順が参考になります

まとめ

  • crosstabに行と列の2つの列を渡すと、組み合わせごとの件数の表ができます
  • margins=Trueで合計の行と列、normalize="index"で行ごとの割合を足します
  • 空欄の行は数えられないので、表の行数と合計の件数を見比べます
  • 並び順や表記の揺れは、数えた後と数える前に整えます