からあげ博士の日常と研究と

博士課程を満期退学した人が好きなことを好きなままに書くところ。

pandasによる要約統計量(平均、分散、中央値、四分位数など)の求め方 ―― irisデータセットを例にして

 こんにちは。からあげ博士(@phd_karaage)です。今回もirisデータセットシリーズ。とはいえそんな難しいことはしません。単純な要約統計量を求めるだけのお話です。要約統計量と書くとあまりなじみがないかもしれませんが、平均や分散、中央値、四分位数などの値になります。こう書かれると分かりやすいと思います。

 そんな要約統計量ですが、Excelでも単純に求めることもできるしわざわざpythonでやらんでも……という声も聞こえてきそうですね。ただデータ解析をしていくにあたってこれら統計量は切っても切り離せない上に、python上で解析しているならこれらも一緒にやっちゃえばいいじゃん。という訳でpythonでどう書くと楽かな、という観点で書いていこうと思います。

 今回使うのはirisデータセット。以前紹介したデータセットですね。

www.phd-karaage.com

 こういうトイデータって、広く手法を紹介するのに便利。

目次

まずはirisデータセットを用意する

from sklearn.datasets import load_iris
import pandas as pd

iris_dataset = load_iris()

data_array = iris_dataset["data"]
line_num = iris_dataset["target"]
line = iris_dataset["target_names"]
feature = iris_dataset["feature_names"]

df_data = pd.DataFrame(data_array, columns = feature)
df_line = pd.DataFrame(line_num)
df = pd.concat([df_data, df_line], axis = 1)
df = df.rename(columns = {0: "Target"})

print(df.shape)
print(df.head())

 まずはirisデータセットを用意します。1つのデータフレームの中にすべてのデータをまとめたいので、あれこれくっつけて最終的にdfというデータフレームに格納しています。最後にdf.shapeを確認することで、このデータが150行、5列からなることを確認しています。ついでにdf.head()で先頭5行のデータを表示させてデータフレーム内にどのようにデータが格納されているか確認しています。うまくいけばこのように返ってくるはずです。

f:id:phd_karaage:20220119225611p:plain

 このデータの中身がどんなものは以前紹介した時の記事をご覧くださいませ。

 とりあえずこれで、dfの中にirisデータセットの各データが格納されたことになります。

平均を求めよう

 それではさっそく平均値を求めましょう。平均値の定義はどんなものでしたでしょうか。平均値を\mu、各データをx_{n}、データの個数をnとすると

\mu = \frac{1}{n}\sum^{n}_{k = 1} x_{k} = \frac{x_{1} + x_{2} + x_{3} + ...... + x_{n}}{n}

 と表すことができますね。単純にすべてのデータを足して、その個数で割るだけです。ちょっと単純にpythonで実装してみましょうか。とりあえずsepal length(がく片の長さ)について平均を求めてみましょう。

x_sum = 0
for i in range(len(df["sepal length (cm)"])):
    x = df["sepal length (cm)"][i]
    x_sum = x_sum + x
ave = x_sum/len(df["sepal length (cm)"])

print(ave)

 は?こんなもん見たくてこの記事を探しに来たんじゃないよ。というツッコミが来そうですね。とりあえずこのコードの中身について解説しておくと、x_sum = 0として合計値を0に初期化しておいて、あとはfor文で150個のデータを1つ1つ足して、最後に合計値をデータの個数で割っています。データの個数はlen()で求めることができます。定義をそのまま実装した感じですね。もう少し洗練した書き方をしましょうか。

x_sum_2 = sum(df["sepal length (cm)"])
ave_2 = x_sum_2/len(df["sepal length (cm)"])

print(ave_2)

 コードの行数が半分になりましたね!pythonのベース関数であるsum()を使って合計値を求めて、あとはデータの個数で割るだけです。ただ、pandasにはもっと便利な関数があります。.mean()関数です。

ave_3 = df["sepal length (cm)"].mean()
print(ave_3)

 平均を求めたいデータフレームの列を選択し、その後ろに.mean()をつけてあげることで平均値を求めることができます。ここですべての結果を見てみましょう。

f:id:phd_karaage:20220119235316p:plain

 どれも同じ値を取っています。これで異なる値を取っていたらなにかのミスを疑うところですが、同じ値ということであれば問題はなさそうですね。

 この.mean()はデータフレーム全体に適用することができます。

ave_all = df.mean()
print(ave_all)

 返り値を見てみると、各列に対して平均を求めてくれていることが分かります。

f:id:phd_karaage:20220119235706p:plain

 これを見ると、Targetは0~2のカテゴリカルデータが50個ずつ含まれていることから平均値が1.0となるのも当然、というのが分かります。

 品種ごとの平均を求めるにはどうすればよいでしょうか?これも.mean()の引数を変えてあげることで計算することができます。ただしデータフレームのインデックスをちょっと工夫してあげる必要があります。

df_line = df.set_index([df.index, df.Target])
print(df_line.head())

ave_4 = df_line["sepal length (cm)"].mean(level = 1)
print(ave_4)

 まずは、インデックスに系統情報も含むようにインデックスを書き換えます。書き換えたデータフレームをdf_lineとして保存し、これに対して.mean()を実行してあげる訳です。この時、.mean()の中にlevel = 1という引数を入れてあげることで、インデックスの2つ目、つまり系統情報に対して平均値を求めてくれます。ここで返り値を見てみるとどうでしょう。

f:id:phd_karaage:20220120001245p:plain

 系統番号0~2に対してそれぞれ平均値を求めてくれることが分かりますね。もちろんdf_line.mean(level = 1)で各列に対してまとめて計算することもできます。

分散を求めよう

 分散も同様に求めていきましょう。分散とはデータの散らばり具合を表したもので、平均値からの偏差の2乗の平均によって表すことができます。分散をs^{2}として、

s^{2} = \frac{1}{n}\sum^{n}_{k = 1} (x_{k} - \mu)

 と表すことができますね。

 平均値が.mean()で計算できたことから、察しのいい人ならなんとなく書き方が思いつくのではないでしょうか?

var = df["sepal length (cm)"].var()
print(var)

 という感じで、これも一撃で求めることができます。英語でvarianceだからvar()という関数。単純ですね。同様にlevel = 1と指定してあげると

var_line = df_line["sepal length (cm)"].var(level = 1)
print(var_line)

f:id:phd_karaage:20220120002745p:plain

 系統ごとの分散を求めることができます。

中央値を求めよう

 要約統計量の中には中央値もあります。平均値とどう違うねん、という話ですが、下位、あるいは上位の値から数えて、データの半分のところにある値というべきでしょうか。厳密な定義はググってくれ。

 平均だけではなく中央値も求める理由ですが、年収200万円の人が33人、年収300万円の人が33人、年収400万円の人が33人。年収1億円の人が1人いたら、このときの平均年収はどうなるでしょうか?397万円になりますね。しかしこの平均値はこのデータを平均的に表しているでしょうか?年収1億円の人が1人含まれているおかげで、かなり平均年収が高めになっていることが分かります。そこで中央値を考えてあげると、300万円となる訳ですから、なにか乖離のあるデータが含まれていると考えることができる、という話です。

 中央値もpandasの関数を使うことで簡単に求めることができます。中央値は英語でmedianですから、これも簡単に.median()で求められます。

median = df["sepal length (cm)"].median()
print(median)

 この関数も同様に、level = 1とすることで系統ごとの中央値を求めることができます。

med_line = df_line["sepal length (cm)"].median(level = 1)
print(med_line)

f:id:phd_karaage:20220120004350p:plain

四分位数を求めよう

 四分位数は中央値と似たようなもの、というより、第2四分位数は中央値を表しています。第1四分位数が下側から25%のデータ点、第3四分位数が上側から25%のデータ点となる訳ですね。これによってデータの分布がどうなっているかを見ることができる訳です。また箱ひげ図を書くときなんかもよく出てくるものですね。

 四分位範囲の外側を外れ値とする場合もあり、外れ値検出にも使うことができます。

 分位数という英語がquantileであることから、これも.quantile()という関数によって求めることができます。

quant = df["sepal length (cm)"].quantile(0.25)
print(quant)

 ここで他の関数と違うところは、quantile()の中に引数としてどのデータ点を取るかを入力することですね。今回0.25を入れていますが、これは25%データ点、つまり、第1四分位数を求めてくれと言っている訳です。ここには任意の値( 0 \leqq x \leqq 1)を入れて、各分位数を求めることができます。第3四分位数であれば0.75ですね。

 そしてこの関数ではlevelの引数を取ることができないので、各系統ごとに分位数を求めることができません。一方でデータフレーム全体に対して.quantile()を使うことは可能なので、列ごとに分位数を求めることは可能です。

quant_all = df_line.quantile(0.25)
print(quant_all)

f:id:phd_karaage:20220120005736p:plain

系統ごとに分位数を求めるには?

 level = 1で系統ごとの分位数を求めることができないという話でしたね。では系統ごとの分位数が知りたければどのように求めればよいでしょうか?それはある意味簡単で、系統ごとのデータフレームに分けてあげればよい訳です。ちょっとやってみましょう。

df_0 = df[df.Target == 0]
print(df_0.shape)
quant_0 = df_0["sepal length (cm)"].quantile(0.25)
print(quant_0)

 まずはもともとのデータフレームから、系統ごとのデータフレームを抽出します。df.Targetに系統情報が格納されていて、その値は0,1,2でしたね。そこで今回はとりあえず系統0について抽出しました。抽出したデータフレーム、df_0のshapeを確認するとちゃんと50行5列のデータが抽出されています。このデータフレームに.quantile()を適用してあげるとうまく値が出てきますね。

要約統計量は簡単に求まる

 pandasのデータフレームに対しては、関数を使うことで簡単に要約統計量を求めることができます。データ解析を行うときにデータを眺めることが非常に重要ですが、こうした要約統計量からデータを眺めてあげることで何かを見つけることができます。単純に平均だけを求めておしまい、ではなく様々な要約統計量を求めてあげることで見えてくるデータもあるはずです。また外れ値なんかを見つけて誤った結論に至らないためにも、こうした統計量を活用していきましょう。

 平均値のところで定義に従って求めてみましたが、もし時間があるなら定義通りに計算してみるということもコードを書く練習や統計量そのものの理解につながります。ぜひトライしてみてください。