高校生物553211 views
高校日本史191026 views
小学算数1203731 views
数学講師2913151 views
Computer370048 views
小学理科721729 views
高校化学2933198 views
教育150077 views
高校倫理1444503 views
いろは3025218 views
Help
Tools

English

NumPy と pandas で分散の答えが違う!pandas の要約統計量

同じ 10 個の数から、NumPy の np.var は 3.6 を返し、pandas の .var() は 4.0 を返します。

計算がちがうわけではありません。既定の ddof が、片方は 、もう片方は だからです[1]。

pandas で要約統計量を出すときは、この既定を頭に置いておく必要があります。

Series と DataFrame

1 列ぶんのデータが Series、表になったものが DataFrame です。

import numpy as np
import pandas as pd

data = pd.Series([4, 8, 5, 9, 6, 7, 3, 8, 6, 4])

Series は値の並びに見出しがついたものと考えると分かりやすい。見出しを指定しなければ から順に振られます。

DataFrame は、その Series を列として横に並べたものです。列ごとに型が違ってよいので、数値と文字列を同じ表に置けます。

基本の統計量

メソッドとして呼びます。出力を右にコメントで添えました。

print(data.mean())              # 6.0
print(data.var())               # 4.0
print(data.var(ddof=0))         # 3.6
print(data.std())               # 2.0
print(np.var(data.to_numpy()))  # 3.6

pandas の .var() は既定で で割ります[1]。不偏分散のほうです。

最後の行で同じデータを NumPy に渡すと 3.6 が返りました。既定が で割るほうだからです。

pandas の既定

ddof=1。 で割る不偏分散

NumPy の既定

ddof=0。 で割る標本分散

どちらも引数で切りかえられます。混ぜて使うときは、どちらの既定に合わせるかを先に決めておく。

同じ集計を 2 つの道具で書き直したら値が変わった、という事故はここから起きます。

describe で一度に出す

DataFrame には、主な統計量をまとめて返すメソッドがあります。

frame = pd.DataFrame({
	'国語': [62, 72, 66, 70, 58],
	'数学': [68, 75, 64, 80, 55],
	'組': ['A', 'B', 'A', 'B', 'A'],
})

print(frame.describe())

出力はこうなります。

国語         数学
count   5.000000   5.000000
mean   65.600000  68.400000
std     5.727128   9.710819
min    58.000000  55.000000
25%    62.000000  64.000000
50%    66.000000  68.000000
75%    70.000000  75.000000
max    72.000000  80.000000

数値の列だけが選ばれ、文字列の「組」は落ちています。数値の列がまったくない場合だけ、文字列の列が対象になります[2]。

count は欠測を除いた個数です。std はここでも で割った値になります。

50% の行が中央値です。国語の中央値 66.0 は平均 65.6 とほぼ同じで、大きな偏りはありません。

数学のほうは std が 9.71 と大きく、min 55 から max 80 まで開いています。同じ 5 人でも、科目によって散らばりが違う。

型を確かめる

集計の前に、各列がどう読みこまれたかを見ておきます。

print(frame.dtypes)
国語    int64
数学    int64
組       str
dtype: object

数値の列が int64、文字列の列が str になっています。ここが意図と違っていると、集計の結果もおかしくなる。

外部のファイルから読みこんだ場合、数値のはずの列が文字列になっていることがあります。空白や単位の記号が 1 つ混じっているだけで、列全体が文字列と判定される。

平均が出ないとき、まず疑うのはこの型です。エラーにならずに文字列の連結が起きる場合もあり、そのほうがたちが悪い。

行と列のどちらへ集計するか

DataFrame のメソッドは、既定で列ごとに集計します。

print(frame[['国語', '数学']].median())
print(frame[['国語', '数学']].mean(axis=1))
国語    66.0
数学    68.0
dtype: float64
0    65.0
1    73.5
2    65.0
3    75.0
4    56.5
dtype: float64

上は科目ごとの中央値、下は 1 人ごとの 2 科目の平均です。axis=1 を渡すと横方向に集計されます。

科目ごとの平均を出したいのか、生徒ごとの平均を出したいのか。同じデータでも、集計の向きで答える問いが変わります。

群ごとに集計する

列の値でグループに分け、それぞれの統計量を出せます。

print(frame.groupby('組')['数学'].mean())
組
A    62.333333
B    77.500000
Name: 数学, dtype: float64

複数の統計量をまとめて出すこともできます。

print(frame.groupby('組')['数学'].agg(['count', 'mean', 'std']))
count       mean       std
組
A      3  62.333333  6.658328
B      2  77.500000  3.535534

A 組が 3 人、B 組が 2 人です。この人数を見ずに平均だけを比べると、判断を誤ります。

B 組の std が 3.54 と小さく見えますが、2 人しかいないので自由度は 1 しかありません。この値から母集団のばらつきを語る根拠は薄い。

groupby の出力には count を必ず添えておくと、あとで読み返したときに人数が分かります。

列どうしの関係

数値の列だけをとり出して、相関行列や共分散行列を出せます。

print(frame[['国語', '数学']].corr())
print(frame[['国語', '数学']].cov())
国語        数学
国語  1.000000  0.875661
数学  0.875661  1.000000

      国語    数学
国語  32.8  48.7
数学  48.7  94.3

対角線が になるのは、自分自身との相関だからです。共分散行列のほうは、対角線に各列の分散が並びます。

国語の分散 32.8 と数学の分散 94.3。数学のほうが 3 倍近く散らばっています。

相関 0.876 は強い正の関係ですが、5 人ぶんしかありません。この人数では、相関係数の値そのものが大きく振れます。

欠測値は既定で落ちる

pandas の集計は、既定で欠測を無視します[1]。

data = pd.Series([4.0, 8.0, np.nan, 9.0, 6.0])

print(data.mean())          # 6.75
print(data.count())         # 4
print(len(data))            # 5
print(data.isna().sum())    # 1

.mean() が 6.75 を返しました。欠測を除いた 4 個で計算しています。

NumPy が nan を返したのとは反対の設計です。pandas のほうが便利ですが、欠測があることに気づかないまま進む危険もある。

.count() は欠測を除いた個数、len() はもとの長さです。この 2 つがずれていたら、欠測があるということ。

落とし穴は合計を自分で個数で割ったときです。.sum() は欠測を飛ばすのに、len() は飛ばしません。

この例では になり、正しい 6.75 とずれます。割る相手は .count() のほう。

欠測をどう扱うか

集計が通ってしまうぶん、欠測の扱いは自分で決める必要があります。

まず isna().sum() で何個あるかを数える

無作為に抜けているかどうかを考える

落とすか埋めるかを決めて、決めたことを記録する

大きい値だけが抜けている場合、残りで計算した平均は母平均の推定になりません。関数は落としてくれますが、落としてよいかは判断できない。

報告するときは、何個のうち何個で計算したかを一緒に書きます。describe の count がその役目を果たします。

pandas の Series に .var() を当てたら 4.0、np.var で同じデータを計算したら 3.6 でした。原因はどれですか。

  • 既定の ddof が pandas は 1、NumPy は 0 だから
  • pandas が欠測を落としているから
  • 計算の精度が違うから
__RESULT__

pandas は で割る不偏分散、NumPy は で割る標本分散を既定にしています。この例に欠測はなく、精度の問題でもありません。data.var(ddof=0) とすれば 3.6 で一致します。

describe を 1 度呼べば、count と mean と std がまとめて出ます。あとは ddof の既定と、count と len のずれだけ確かめる。

型を見て、向きを決めて、欠測を数える。集計そのものより、その前後のほうに手間をかける価値があります。

参考

pandas.Series.var. pandas documentation.
pandas.DataFrame.describe. pandas documentation.
numpy.var. NumPy Reference.
同じ 10 個から np.var は 3.6、pandas の .var() は 4.0。既定の ddof が $0$ と $1$ で違うためです。describe を 1 度呼べば count、mean、std、四分位数までまとまって出て、文字列の列は自動で落ちます。dtypes で型を確かめ、axis で集計の向きを決め、groupby には count を添える。A 組 3 人と B 組 2 人の平均を並べるとき、人数を見ずに比べると判断を誤ります。欠測は既定で無視されるので .mean() は通りますが、.sum() を len() で割ると 6.75 が 5.4 になる。実行して確かめた出力とともに、集計の前後で見るところを並べます。