pandasのMultiIndexで階層ごとのサンプル数と統計量を出す方法|size・count・agg・標準誤差の使い分け

pandasでMultiIndexの階層(レベル)ごとのサンプル数を出すには df.groupby(level="学年").size()(行数)か .count()(欠損値を除いた件数)を使い、平均や標準偏差などの統計量は groupby(level=...).agg() でサンプル数と一緒に1つの表にまとめるのが確実です。

平均値だけを見ると、サンプル数が1〜2件しかないグループの値に引っぱられて判断を誤ることがあります。この記事では、MultiIndexの階層ごとに「何件のデータから出した統計量なのか」を必ず並べて表示する方法を、欠損値を含むデータで確認します。コードはpandas 3.0.6(2026年10月16日時点の最新版、PyPIで確認)とPython 3.12で実行しています。

サンプルデータ(学年×クラスのテスト結果)

学年とクラスの2階層をインデックスにした、生徒ごとのテストの点数です。欠席などで点数がない生徒は NaN(欠損値)にしています。

import pandas as pd
import numpy as np

df = pd.DataFrame({
    "学年":   ["1年", "1年", "1年", "1年", "1年", "2年", "2年", "2年", "2年", "2年", "2年"],
    "クラス": ["A", "A", "A", "B", "B", "A", "A", "A", "A", "B", "B"],
    "点数":   [72, 85, np.nan, 64, 90, 58, 77, 81, 69, 88, np.nan],
    "提出":   [1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 1],
}).set_index(["学年", "クラス"])
print(df)

実行結果:

          点数  提出
学年 クラス          
1年 A    72.0   1
   A    85.0   1
   A     NaN   0
   B    64.0   1
   B    90.0   1
2年 A    58.0   1
   A    77.0   1
   A    81.0   0
   A    69.0   1
   B    88.0   1
   B     NaN   1

MultiIndexの作り方そのものはpandasのMultiIndexの使い方、特定の階層の行を取り出す方法はpandasのMultiIndexから行・列を選択する方法で解説しています。

サンプル数を数える:size()とcount()の違い

階層ごとの件数を数える方法は2つあり、欠損値の扱いが違います。

メソッド 数えるもの 欠損値(NaN)
size() グループに属する行の数 数える
count() 列ごとの、値が入っている件数 数えない
g = df.groupby(level=["学年", "クラス"])
print(g.size())
print()
print(g["点数"].count())

実行結果:

学年  クラス
1年  A      3
    B      2
2年  A      4
    B      2
dtype: int64

学年  クラス
1年  A      2
    B      2
2年  A      4
    B      1
Name: 点数, dtype: int64

1年Aクラスは3行ありますが、点数が入っているのは2件です。平均点の計算に使われるのは count() のほうの2件なので、統計量と並べるサンプル数は count() を使うのが正確です。size() は「名簿上の人数」、count() は「計算に使われた件数」と考えると区別しやすくなります。

上の階層だけで数えたいときは、level に1つのレベル名を渡します。DataFrame全体に count() を使うと、列ごとに件数が出ます。

g = df.groupby(level="学年")
print(g.size())
print()
print(g.count())

実行結果:

学年
1年    5
2年    6
dtype: int64

    点数  提出
学年        
1年   4   5
2年   5   6

同じ1年でも、「提出」列は5件、「点数」列は4件です。列によってサンプル数が違うことがあるので、統計量を出す列の件数を確認してください。

サンプル数と統計量を1つの表にする:agg()

agg() に「新しい列名=集計方法」の形で渡すと、サンプル数・平均・標準偏差などを1回でまとめられます(名前付き集計)。

stats = df.groupby(level=["学年", "クラス"])["点数"].agg(
    サンプル数="count",
    平均="mean",
    標準偏差="std",
    標準誤差="sem",
)
print(stats.round(2))

実行結果:

        サンプル数     平均   標準偏差   標準誤差
学年 クラス                            
1年 A        2  78.50   9.19   6.50
   B        2  77.00  18.38  13.00
2年 A        4  71.25  10.14   5.07
   B        1  88.00    NaN    NaN

2年Bクラスは点数のあるデータが1件しかないため、標準偏差と標準誤差が NaN になっています。pandasの std() はデフォルトで不偏標準偏差(ddof=1、n−1で割る)を計算するので、データが1件だと計算できないためです。NaNはエラーではなく「サンプル数が足りない」というサインとして読みます。

標準誤差(sem)でサンプル数の少なさを読む

表の「標準誤差」は、平均値がどのくらいぶれうるかを表す指標です。総務省統計局の「なるほど統計学園」では、標準誤差を推定量の標準偏差と説明し、標本平均の標準誤差は不偏分散の正の平方根 s と標本の大きさ n から求められ、標本の大きさが大きくなるほど標準誤差は小さくなると説明しています(統計的推定と統計的仮説検定)。pandasの sem() はこの s/√n を計算します。

上の結果では、1年Bクラス(n=2)の標準誤差13.00は、2年Aクラス(n=4)の5.07よりずっと大きくなっています。1年Bの平均77.0は、2件だけから出した、ぶれの大きい値だということが数字でわかります。

「サンプル数」と「サンプルサイズ」

統計の文脈では、1つのグループに含まれるデータの件数を「標本の大きさ(サンプルサイズ)」と呼び、上記の統計局の解説も n を「標本の大きさ」としています。日常的には「サンプル数」と呼ぶことも多く、この記事でもグループごとの件数をサンプル数と書いていますが、論文やレポートでは「n=4」のように件数をそのまま書くと誤解がありません。

describe()で要約統計量をまとめて出す

四分位数まで一度に見たいときは、groupby() のあとに describe() を付けます。先頭の count 列がそのままサンプル数になります。

print(df.groupby(level="学年")["点数"].describe().round(1))

実行結果:

    count  mean   std   min   25%   50%   75%   max
学年                                                 
1年    4.0  77.8  11.9  64.0  70.0  78.5  86.2  90.0
2年    5.0  74.6  11.5  58.0  69.0  77.0  81.0  88.0

表示が細かすぎる場合は round() で丸めます。describe() の見方はpandasのdescribe()の使い方で詳しく解説しています。

下の階層・上の階層・全体を並べて比べる

クラス別・学年別・全体の3段階で集計すると、どの階層で差が出ているかがわかります。level に渡すレベルを変えるだけです。

by_class = df.groupby(level=["学年", "クラス"])["点数"].agg(["count", "mean"])
by_grade = df.groupby(level="学年")["点数"].agg(["count", "mean"])
whole = df["点数"].agg(["count", "mean"])
print(by_class.round(1))
print()
print(by_grade.round(1))
print()
print(whole.round(1))

実行結果:

        count  mean
学年 クラス             
1年 A        2  78.5
   B        2  77.0
2年 A        4  71.2
   B        1  88.0

    count  mean
学年             
1年      4  77.8
2年      5  74.6

count     9.0
mean     76.0
Name: 点数, dtype: float64

「クラス平均の平均」は学年平均と一致しない

よくある間違いが、下の階層の平均をもう一度平均して上の階層の値にしてしまうことです。サンプル数がグループごとに違うと、結果がずれます。

cls_mean = df.groupby(level=["学年", "クラス"])["点数"].mean()
print("クラス平均の平均:", round(cls_mean.groupby(level="学年").mean()["2年"], 2))
print("2年全体の平均  :", round(df.loc["2年", "点数"].mean(), 2))

実行結果:

クラス平均の平均: 79.62
2年全体の平均  : 74.6

2年はAクラス4件・Bクラス1件なので、クラス平均どうしを平均すると、1件しかないBクラスの88点が半分の重みを持ってしまいます。上の階層の値は、元のデータから groupby(level="学年") で計算し直してください。

グループの数・値ごとの出現回数を数える

「各学年にクラスがいくつあるか」のように、下の階層の種類の数を知りたいときは nunique() を使います。インデックスのままでは扱いにくいので、reset_index() で列に戻してから数えます。レベルの値ごとの出現回数は get_level_values() と value_counts() で出せます。

n_class = df.reset_index().groupby("学年")["クラス"].nunique()
print(n_class)
print()
print(df.index.get_level_values("クラス").value_counts())

実行結果:

学年
1年    2
2年    2
Name: クラス, dtype: int64

クラス
A    7
B    4
Name: count, dtype: int64

サンプル数が少ないグループを除いて集計する

グループごとの件数を元の行に付けたいときは transform("count")、件数が一定未満のグループを除くときは filter() を使います。

df["グループのn"] = df.groupby(level=["学年", "クラス"])["点数"].transform("count")
print(df)
print()
enough = df.groupby(level=["学年", "クラス"]).filter(lambda x: x["点数"].count() >= 3)
print(enough.groupby(level=["学年", "クラス"])["点数"].mean().round(1))

実行結果:

          点数  提出  グループのn
学年 クラス                  
1年 A    72.0   1       2
   A    85.0   1       2
   A     NaN   0       2
   B    64.0   1       2
   B    90.0   1       2
2年 A    58.0   1       4
   A    77.0   1       4
   A    81.0   0       4
   A    69.0   1       4
   B    88.0   1       1
   B     NaN   1       1

学年  クラス
2年  A      71.2
Name: 点数, dtype: float64

ここでは点数のあるデータが3件以上のグループだけを残したので、2年Aクラスだけが集計対象になりました。何件以上あれば十分かはデータや目的によって変わるので、基準を決めたらレポートに明記しておくと後で困りません。groupby() の transform() と filter() はpandasのgroupbyの使い方でも解説しています。

クロス表にする・割合を出す

階層ごとのサンプル数を見やすい表にするには、unstack() で内側のレベルを列に展開します。0/1のフラグ列なら、sum() が件数、mean() が割合になります。

table = df.groupby(level=["学年", "クラス"])["点数"].count().unstack()
print(table)
print()
print(df.groupby(level="学年")["提出"].agg(["sum", "mean"]))

実行結果:

クラス  A  B
学年       
1年   2  2
2年   4  1

    sum      mean
学年               
1年    4  0.800000
2年    5  0.833333

「提出」の mean は提出率です。1年は5人中4人で0.8になっています。unstack() の詳しい使い方はpandas unstack()の使い方をご覧ください。

古い書き方 sum(level=…) は使えない

以前のpandasでは df.sum(level="学年") のように集計メソッドに level を渡す書き方がありましたが、pandas 2.0で削除され、groupby を使うように変わりました(What’s new in 2.0.0)。古い記事のコードを写すと次のエラーになります。

try:
    df.sum(level="学年")
except TypeError as e:
    print("TypeError:", e)

実行結果:

TypeError: sum() got an unexpected keyword argument 'level'

この場合は df.groupby(level="学年").sum() に書き換えます。

グループごとの統計量と標準誤差の考え方は、機械学習のデータ分析の前提になる知識です。テックジムのAIエンジニア養成コースでは、第6章「AIのための統計学入門」で推測統計・区間推定・不偏分散を学べます。

まとめ

  • 行数は size()、欠損値を除いた件数は count()。統計量と並べるのは count()
  • groupby(level=...).agg(サンプル数="count", 平均="mean", ...) で件数と統計量を1つの表に
  • 標準偏差・標準誤差がNaNならサンプル数が1件。標準誤差はサンプル数が少ないほど大きい
  • 上の階層の平均は、下の階層の平均を平均せずに元データから計算する
  • sum(level=...) はpandas 2.0で削除。groupby(level=...) を使う

よくある質問

Q. size()とcount()はどう違いますか?

size()はグループに属する行の数を数え、欠損値(NaN)の行も含みます。count()は列ごとに値が入っている件数を数え、欠損値は数えません。平均などの統計量と並べるサンプル数にはcount()を使います。

Q. グループごとの標準偏差がNaNになるのはなぜですか?

pandasのstd()はデフォルトでn−1で割る不偏標準偏差を計算するため、値が1件しかないグループでは計算できずNaNになります。サンプル数が足りないというサインとして扱います。

Q. サンプル数が違うグループの平均を比べるときの注意点は?

サンプル数が少ないグループの平均はぶれが大きくなります。sem()で標準誤差を並べて表示すると、どの平均がどのくらい不確かかを比べられます。

Q. df.sum(level=…) がエラーになります。

集計メソッドのlevel引数はpandas 2.0で削除されました。df.groupby(level=”学年”).sum() のようにgroupbyを使って書き換えてください。


pandas|抽出・選択の関連記事

loc・ilocや条件指定で、必要な行・列だけを取り出す操作の記事です。

pandas全体の使い方は「pandasの使い方を初心者向けに解説」でまとめています。

プログラミング学習に興味がある方は、まずは無料体験会へ

無料体験会・お申し込みはこちら