pandas crosstab()の使い方|2つの列をクロス集計・割合表示する方法

CSVを読み込んだあと、「地域ごとに購入あり・購入なしの件数を見たい」「年代ごとに満足度の分布を確認したい」と思うことがあります。

このように、2つの列の組み合わせごとの件数を表で見たいときに便利なのが、pandasの crosstab() です。

crosstab() を使うと、たとえば「地域×購入有無」「年代×満足度」のようなクロス集計表を短いコードで作れます。

基本形は次のとおりです。


pd.crosstab(df["行にしたい列"], df["列にしたい列"])

この記事では、pandasの crosstab() を使って、2つの列を組み合わせたクロス集計表を作る方法を解説します。あわせて、value_counts()groupby()pivot_table() との違い、割合表示、合計行・合計列も整理します。

この記事でわかること

この記事では、crosstab() で2つの列の組み合わせを集計する方法を学びます。

具体的には、次の内容を扱います。

  • pd.crosstab() の基本的な使い方
  • 1列だけを見る value_counts() との違い
  • 2つの列を組み合わせたクロス集計表の作り方
  • 合計行・合計列を追加する方法
  • 件数を割合で表示する方法
  • 欠損値があるときの注意点
  • crosstab() が役立つ実践的な場面

crosstab() は、value_counts() で1列の件数を確認したあとに、2つの列の関係を表で見たいときに使うメソッドです。

crosstab()が役立つ場面

crosstab() は、2つの列の組み合わせごとの件数を見たい場面で役立ちます。

場面 見たい組み合わせ
購入データの確認 地域 × 購入有無
アンケート分析 年代 × 満足度
A/Bテストの確認 パターンA/B × 申込有無

この記事では、まず 地域×購入有無年代×満足度 の例で、基本的なクロス集計を確認します。

サンプルデータを作成する

ここでは、地域・年代・商品カテゴリ・購入有無・満足度を含む小さなデータを使います。

実務ではCSVから読み込むことが多いですが、この記事ではGoogle Colabでそのまま試せるように、DataFrameを直接作成します。


import pandas as pd

df = pd.DataFrame({
    "地域": ["東京", "東京", "東京", "大阪", "大阪", "大阪", "名古屋", "名古屋", "福岡", None],
    "年代": ["20代", "20代", "30代", "40代", "40代", "30代", "20代", "30代", "40代", "30代"],
    "商品カテゴリ": ["PC", "PC", "文房具", "家具", "家具", "文房具", "PC", "文房具", "家具", "PC"],
    "購入有無": ["購入あり", "購入あり", "購入あり", "購入なし", "購入なし", "購入なし", "購入あり", "購入なし", "購入なし", "購入あり"],
    "満足度": ["高い", "高い", "普通", "低い", "低い", "普通", "高い", "普通", "低い", None]
})

df
地域 年代 商品カテゴリ 購入有無 満足度
0 東京 20代 PC 購入あり 高い
1 東京 20代 PC 購入あり 高い
2 東京 30代 文房具 購入あり 普通
3 大阪 40代 家具 購入なし 低い
4 大阪 40代 家具 購入なし 低い
5 大阪 30代 文房具 購入なし 普通
6 名古屋 20代 PC 購入あり 高い
7 名古屋 30代 文房具 購入なし 普通
8 福岡 40代 家具 購入なし 低い
9 None 30代 PC 購入あり None

このデータでは、クロス集計後の傾向が見えやすいように、次のような特徴を入れています。

列名 使いどころ
地域 東京は購入ありが多く、大阪は購入なしが多い例に使う
年代 20代は満足度が高く、40代は低い例に使う
商品カテゴリ カテゴリ列の例として使う
購入有無 地域ごとの購入状況を比較する
満足度 年代ごとの満足度分布を比較する

地域満足度 には、あえて欠損値も入れています。
あとで、欠損値がある場合の注意点も確認します。

1列だけならvalue_counts()で確認する

crosstab() は2つの列の組み合わせを見る方法です。

一方、1つの列だけの件数を見たい場合は、value_counts() のほうが簡単です。

まずは、地域 列だけの件数を確認してみます。


df["地域"].value_counts()
count
東京 3
大阪 3
名古屋 2
福岡 1

value_counts() では、地域ごとの件数を確認できます。

ただし、これだけでは「地域ごとに購入あり・購入なしが何件あるか」は見えません。

1列だけを見るなら value_counts()、2つの列の組み合わせを見るなら crosstab() と考えるとわかりやすいです。

2つの列を組み合わせたクロス集計表を作る

ここからは、pd.crosstab() を使って実際にクロス集計表を作ります。

まずは、地域購入有無 を指定して、地域ごとの購入状況を集計します。

地域×購入有無を集計する

「地域ごとに、購入あり・購入なしが何件あるか」を確認する例です。


pd.crosstab(df["地域"], df["購入有無"])
購入あり 購入なし
名古屋 1 1
大阪 0 3
東京 3 0
福岡 0 1

この結果を見ると、行に 地域、列に 購入有無 が並びます。

たとえば、このサンプルデータでは、東京は「購入あり」が多く、大阪は「購入なし」が多いことがわかります。

crosstab() は元のDataFrameを書き換える処理ではなく、集計結果として新しい表を作る処理です。

行と列を入れ替えると見え方が変わる

crosstab() では、最初に指定した列が行、2つ目に指定した列が列になります。

次のコードでは、先ほどとは逆に、購入有無 を行、地域 を列に置いてみます。


pd.crosstab(df["購入有無"], df["地域"])
名古屋 大阪 東京 福岡
購入あり 1 0 3 0
購入なし 1 3 0 1

集計している内容は同じでも、pd.crosstab() に指定する列の順番を変えると、行と列が入れ替わります。

初心者のうちは、「比較したいグループ」を1つ目の引数、つまり行側に置くと読みやすくなります。

年代×満足度を集計する

同じ考え方で、年代満足度 の組み合わせも集計できます。

これは、アンケート集計のように「年代ごとに、満足度がどのように分布しているか」を確認する例です。


pd.crosstab(df["年代"], df["満足度"])
低い 普通 高い
20代 0 0 3
30代 0 3 0
40代 3 0 0

この表では、年代ごとに「高い」「普通」「低い」が何件あるかを確認できます。

このサンプルデータでは、20代は「高い」が多く、40代は「低い」が多いことがわかります。

このように、単に満足度全体の件数を見るだけでなく、年代別に分けると傾向が見えやすくなります。
crosstab() は、アンケート集計の入口としても使いやすい方法です。

合計行・合計列を追加する

クロス集計表では、行ごとの合計や列ごとの合計も見たいことがあります。

その場合は、margins=True を指定します。


pd.crosstab(df["地域"], df["購入有無"], margins=True)
購入あり 購入なし All
名古屋 1 1 2
大阪 0 3 3
東京 3 0 3
福岡 0 1 1
All 4 5 9

margins=True を使うと、合計行・合計列が追加されます。

指定 意味
margins=True 行方向・列方向の合計を追加する
All 合計を表す行または列

全体の件数も同時に確認したいときに便利です。

件数を割合で表示する

件数だけでなく、割合で見たい場合は normalize を使います。

normalize は、何を分母にして割合を出すかを指定する引数です。

指定 意味 使う場面
normalize=True 全体に対する割合 全体の中でどれくらいかを見たい
normalize="index" 行ごとの割合 地域ごとの購入有無の割合を見たい
normalize="columns" 列ごとの割合 購入有無ごとの地域構成を見たい

全体に対する割合を見る

まずは、全体に対する割合を見てみます。


# 全体に対する割合を表示する
pd.crosstab(df["地域"], df["購入有無"], normalize=True)
購入あり 購入なし
名古屋 0.1111111111111111 0.1111111111111111
大阪 0.0 0.3333333333333333
東京 0.3333333333333333 0.0
福岡 0.0 0.1111111111111111

この表では、全体を1として、それぞれの組み合わせがどれくらいの割合かを表しています。

実務でよく使うのは、次の normalize="index" です。
たとえば、地域ごとに「購入あり」「購入なし」の割合を見たいときに使います。

行ごとの割合を見る

normalize="index" は、行ごとの合計を1として割合を出します。

次の例では、地域ごとに「購入あり」「購入なし」の割合を確認します。


# 行ごとの割合を表示する
# ここでは「地域ごとに、購入あり・購入なしの割合」を見る
pd.crosstab(df["地域"], df["購入有無"], normalize="index")
購入あり 購入なし
名古屋 0.5 0.5
大阪 0.0 1.0
東京 1.0 0.0
福岡 0.0 1.0

この表では、各地域の中で「購入あり」「購入なし」がどれくらいの割合かを確認できます。

normalize="index" は、行ごとの割合です。
「年代ごとの満足度割合」を見たい場合も同じ考え方で使えます。


# 行ごとの割合を表示する
# ここでは「年代ごとに、満足度の割合」を見る
pd.crosstab(df["年代"], df["満足度"], normalize="index")
低い 普通 高い
20代 0.0 0.0 1.0
30代 0.0 1.0 0.0
40代 1.0 0.0 0.0

このコードでは、年代ごとに満足度の割合を見ています。

「20代の中では高いが何割か」「30代の中では普通が何割か」のように、行ごとの分布を確認できます。

列ごとの割合を見る

normalize="columns" は、列ごとの合計を1として割合を出します。

次の例では、購入あり・購入なしごとに、地域の構成を確認します。


# 列ごとの割合を表示する
# ここでは「購入あり・購入なしごとに、地域の割合」を見る
pd.crosstab(df["地域"], df["購入有無"], normalize="columns")
購入あり 購入なし
名古屋 0.25 0.2
大阪 0.0 0.6
東京 0.75 0.0
福岡 0.0 0.2

この表では、列ごとの合計が1になります。

normalize=Truenormalize="index"normalize="columns" は、分母が違います。
割合を見るときは、「何を1としているのか」を必ず確認しましょう。

小数を見やすく丸める

割合表示では、小数が長く表示されることがあります。

最初は割合の意味を理解することが大切ですが、表として見やすくしたい場合は .round() を使うと便利です。


# 行ごとの割合を、小数第2位まで表示する
pd.crosstab(df["地域"], df["購入有無"], normalize="index").round(2)
購入あり 購入なし
名古屋 0.5 0.5
大阪 0.0 1.0
東京 1.0 0.0
福岡 0.0 1.0

この例では、小数第2位まで表示しています。

パーセント表記に整える方法もありますが、この記事では深入りしません。
まずは、normalize の種類によって分母が変わることを理解しておきましょう。

欠損値があるときの注意点

crosstab() で使う列に欠損値があると、その行は集計表に入らない場合があります。

まずは、どの列に欠損値があるかを確認します。


df.isnull().sum()
地域 1
年代 0
商品カテゴリ 0
購入有無 0
満足度 1

今回のデータでは、地域満足度 に欠損値があります。

ここでは、地域 が欠損している行を確認してみます。


df[df["地域"].isnull()]
地域 年代 商品カテゴリ 購入有無 満足度
9 None 30代 PC 購入あり None

元データは10行あります。


len(df)
10

次に、地域×購入有無 のクロス集計表に合計行・合計列を付けて確認します。


pd.crosstab(df["地域"], df["購入有無"], margins=True)
購入あり 購入なし All
名古屋 1 1 2
大阪 0 3 3
東京 3 0 3
福岡 0 1 1
All 4 5 9

クロス集計表の合計は9件です。

元データは10行ありますが、地域 が欠損している行は、東京・大阪・名古屋・福岡のどこにも分類できません。

そのため、地域×購入有無 のクロス集計表には入らず、合計が9件になります。

このように、crosstab() で使う列に欠損値があると、集計表の合計件数が元データの行数より少なく見えることがあります。

欠損値を「未回答」として集計したい場合は、fillna() で置き換えてから crosstab() を使います。


df_filled = df.copy()
df_filled["地域"] = df_filled["地域"].fillna("未回答")
df_filled["満足度"] = df_filled["満足度"].fillna("未回答")

pd.crosstab(df_filled["地域"], df_filled["購入有無"], margins=True)
購入あり 購入なし All
名古屋 1 1 2
大阪 0 3 3
未回答 1 0 1
東京 3 0 3
福岡 0 1 1
All 5 5 10

ここでは、欠損値を「未回答」として扱いました。

ただし、欠損値をすべて機械的に埋めればよいわけではありません。
未回答なのか、入力漏れなのか、分析対象外なのかを考えてから処理しましょう。

実務では、件数表から割合表示につなげる

crosstab() は、件数表を作って終わりではなく、合計や割合を確認すると傾向が見えやすくなります。

基本の流れは、次のとおりです。

  1. value_counts() で1列の件数を確認する
  2. crosstab() で2つの列の組み合わせを集計する
  3. margins=True で合計を見る
  4. normalize で割合を見る

まずは、件数表を作る → 合計を見る → 割合を見る の順番で確認すると、結果を読み取りやすくなります。

まとめ

この記事では、pandasの crosstab() を使って、2つの列を組み合わせたクロス集計表を作る方法を解説しました。

ポイントを整理します。

  • crosstab() は、2つの列の組み合わせを件数表にしたいときに使う
  • 1列の件数確認なら value_counts() が向いている
  • 2列の組み合わせを見るなら crosstab() がわかりやすい
  • 数値列の合計や平均を出したい場合は、groupby()pivot_table() が向いている
  • margins=True で合計行・合計列を追加できる
  • normalize を使うと、件数を割合で表示できる
  • normalize=Truenormalize="index"normalize="columns" は分母が違う
  • 欠損値がある場合は、集計前に isnull() で確認すると安全

なお、valuesaggfunc を使う発展的な集計もありますが、初心者はまず「2つの列の件数表を作る」基本形を押さえれば十分です。

crosstab() を使えるようになると、単に1列の件数を見るだけでなく、2つの列の関係を表で確認できるようになります。

「地域×購入有無」「年代×満足度」など、2つの列の組み合わせを表で確認できるため、value_counts() の次に覚えておきたいメソッドです。

次に読みたい関連記事

crosstab() を理解したら、次の記事に進むと、データ分析の流れがつながりやすくなります。

▲ ページトップへ戻る

value_counts()ではなくcrosstab()を使うのはどんなときですか?

value_counts() は、1つの列の値を数えるときに使います。
一方、crosstab() は、2つの列の組み合わせごとの件数を表にするときに使います。
たとえば、地域ごとの件数だけなら value_counts()、地域ごとの購入あり・購入なしを見たいなら crosstab() が向いています。

crosstab()で売上合計や平均値も出せますか?

2つの列の件数表を作りたいなら crosstab() がわかりやすいです。
一方、カテゴリ別に売上合計や平均値などの数値を集計したい場合は、groupby() が向いています。

pivot_table()ではなくcrosstab()を使うのはどんなときですか?

crosstab() は、2つの列の組み合わせを件数表にする用途で使いやすい方法です。
pivot_table() は、行・列・値・集計方法を指定して、より柔軟な集計表を作る方法です。
初心者はまず、2つの列の件数表なら crosstab()、数値列を使った集計なら pivot_table() と分けて考えると理解しやすいです。

crosstab()で割合を表示できますか?

できます。
normalize=Truenormalize="index"normalize="columns" を使うと、件数ではなく割合で表示できます。
normalize=True:全体に対する割合
・normalize="index":行ごとの割合
・normalize="columns":列ごとの割合

crosstab()で合計行や合計列を追加できますか?

できます。
margins=True を指定すると、合計行・合計列を追加できます。
pd.crosstab(df["地域"], df["購入有無"], margins=True)

valuesやaggfuncも使う必要がありますか?

初心者のうちは、まず pd.crosstab(df["行にしたい列"], df["列にしたい列"]) の基本形を押さえれば十分です。
values や aggfunc を使うと、件数ではなく数値を集計することもできます。
ただし、売上合計や平均値のような数値集計を本格的に行う場合は、groupby() や pivot_table() とあわせて学ぶほうが理解しやすいです。

欠損値がある場合、crosstab()の結果はどうなりますか?

欠損値がある行は、集計表に含まれない場合があります。
そのため、集計前に df.isnull().sum() で欠損値を確認しておくと安全です。
欠損値を「未回答」などのカテゴリとして扱いたい場合は、fillna() で置き換えてから crosstab() を使います。

コメント

タイトルとURLをコピーしました