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=True、normalize="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() は、件数表を作って終わりではなく、合計や割合を確認すると傾向が見えやすくなります。
基本の流れは、次のとおりです。
value_counts()で1列の件数を確認するcrosstab()で2つの列の組み合わせを集計するmargins=Trueで合計を見るnormalizeで割合を見る
まずは、件数表を作る → 合計を見る → 割合を見る の順番で確認すると、結果を読み取りやすくなります。
まとめ
この記事では、pandasの crosstab() を使って、2つの列を組み合わせたクロス集計表を作る方法を解説しました。
ポイントを整理します。
crosstab()は、2つの列の組み合わせを件数表にしたいときに使う- 1列の件数確認なら
value_counts()が向いている - 2列の組み合わせを見るなら
crosstab()がわかりやすい - 数値列の合計や平均を出したい場合は、
groupby()やpivot_table()が向いている margins=Trueで合計行・合計列を追加できるnormalizeを使うと、件数を割合で表示できるnormalize=True、normalize="index"、normalize="columns"は分母が違う- 欠損値がある場合は、集計前に
isnull()で確認すると安全
なお、values や aggfunc を使う発展的な集計もありますが、初心者はまず「2つの列の件数表を作る」基本形を押さえれば十分です。
crosstab() を使えるようになると、単に1列の件数を見るだけでなく、2つの列の関係を表で確認できるようになります。
「地域×購入有無」「年代×満足度」など、2つの列の組み合わせを表で確認できるため、value_counts() の次に覚えておきたいメソッドです。
次に読みたい関連記事
crosstab() を理解したら、次の記事に進むと、データ分析の流れがつながりやすくなります。
pandas value_counts()の使い方|件数集計・割合表示・欠損値の数え方を解説
1列の件数集計を復習したい場合におすすめです。Pandas groupby×aggの使い方|基本の集計とaggの書き方を例で解説
カテゴリ別に売上合計や平均を出したい場合に役立ちます。Pandas pivotとpivot_tableの違い|重複データ対応と集計方法
表の形を変えながら、より柔軟に集計したい場合におすすめです。pandas melt()の使い方|横持ちデータを縦持ちに変換する方法
集計しやすい縦長データに整える方法を学べます。欠損値を可視化して攻略!Pandas isnullとヒートマップ活用術
集計前に欠損値を確認したい場合におすすめです。pandas fillna()の使い方|欠損値を0・平均値・中央値・最頻値で埋める方法を初心者向けに解説
欠損値をどう扱うかを学びたい場合に役立ちます。
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=True、normalize="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() を使います。
コメント