11  ペンギンの体の大きさを比べる

これまでに学んだデータの読み込み,可視化,代表値,ばらつき,相関を組み合わせて,一つのデータを分析しよう.本章の問いは,ペンギンの種類や性別によって,体の大きさはどのように違うのかである.

まず,記録の内容と欠損値を確認する.次に,ひれの長さの分布と体重の中心・ばらつきを種類ごとに比べる.最後に,性別による違いや,くちばしの長さと厚さの関係を調べる.全体をまとめて見たときと,グループに分けて見たときの違いに注目しよう.

11.1 どのようなデータか

南極で調査された 3 種類のペンギン(図 11.1)のデータを使う (Gorman ほか 2014年; Horst ほか 2020年).1 行が 1 羽のペンギンに対応し,種類,島,体の大きさ,性別,調査年が記録されている.体の大きさは,くちばしの長さ・厚さとひれの長さを mm,体重を g で表している.各列の意味とデータの詳しい出典は,セクション A.2.2 と 表 A.3 を参照すること.

(a) アデリーペンギン(Adelie)
(b) ヒゲペンギン(Chinstrap)
(c) ジェンツーペンギン(Gentoo)
図 11.1: データに含まれる 3 種類のペンギン.写真:Andrew Shiva / Wikipedia,CC BY-SA 4.0.Wikimedia Commons の写真(a,b,c)をトリミングして使用した.

penguins.csv をダウンロードし,チャプター 4 と同じ手順でアップロードする.セクション 4.10.1 の方法で,https://zi-ang-liu.github.io/data-science-book/download/penguins.csv から直接読み込んでもよい.本章のコードは,上から順に実行する.

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

penguins = pd.read_csv("penguins.csv")
penguins.head()
species island bill_length_mm bill_depth_mm flipper_length_mm body_mass_g sex year
0 Adelie Torgersen 39.1 18.7 181.0 3750.0 male 2007
1 Adelie Torgersen 39.5 17.4 186.0 3800.0 female 2007
2 Adelie Torgersen 40.3 18.0 195.0 3250.0 female 2007
3 Adelie Torgersen NaN NaN NaN NaN NaN 2007
4 Adelie Torgersen 36.7 19.3 193.0 3450.0 female 2007

11.1.1 行数・列数とデータ型

print(penguins.shape)
print(penguins.dtypes)
(344, 8)
species               object
island                object
bill_length_mm       float64
bill_depth_mm        float64
flipper_length_mm    float64
body_mass_g          float64
sex                   object
year                   int64
dtype: object

344 羽について,8 項目が記録されている.species(種類),island(島),sex(性別)には文字列が入っており,そのほかの列は数値である.

11.2 分析に使える記録はどれか

head() の表示にも,NaN と表示された値があった.各列の欠損値の数を調べよう.

penguins.isna().sum()
species               0
island                0
bill_length_mm        2
bill_depth_mm         2
flipper_length_mm     2
body_mass_g           2
sex                  11
year                  0
dtype: int64

体の大きさを表す 4 列ではそれぞれ 2 件,性別 sex では 11 件の欠損値がある.どの行で何が記録されていないのかを,条件を使って確かめる.penguins["body_mass_g"].isna() は,体重が欠損値の行で True となる.

missing_mass = penguins[penguins["body_mass_g"].isna()]
missing_mass
species island bill_length_mm bill_depth_mm flipper_length_mm body_mass_g sex year
3 Adelie Torgersen NaN NaN NaN NaN NaN 2007
271 Gentoo Biscoe NaN NaN NaN NaN NaN 2009

体重が記録されていない 2 羽は,くちばしとひれの大きさ,性別も記録されていない.この 2 羽については,種類,島,調査年しかわからない.

性別が記録されていない 11 羽についても,同じように調べる.取り出した表に isna().sum() を使うと,その中の欠損値の数がわかる.

missing_sex = penguins[penguins["sex"].isna()]
missing_sex.isna().sum()
species               0
island                0
bill_length_mm        2
bill_depth_mm         2
flipper_length_mm     2
body_mass_g           2
sex                  11
year                  0
dtype: int64

性別が記録されていない 11 羽のうち,体重などの欠損値は 2 件だけであり,残りの 9 羽は体の大きさが記録されている.体の大きさを調べるときに,性別が欠損しているという理由でこの 9 羽まで除くと,使えるデータが減ってしまう.欠損値をどう扱うかは,何を分析するかによって決めよう.

11.3 ひれの長さに二つの山があるのはなぜか

11.3.1 種類ごとの羽数

種類ごとの表を用意し,まず羽数を確かめる.

adelie = penguins[penguins["species"] == "Adelie"]
chinstrap = penguins[penguins["species"] == "Chinstrap"]
gentoo = penguins[penguins["species"] == "Gentoo"]

以下では,この三つの表を使って,種類ごとの違いを調べる.取り出した表の行数は,len() で求められる.

species_names = ["Adelie", "Chinstrap", "Gentoo"]
counts = [len(adelie), len(chinstrap), len(gentoo)]
print(counts)
[152, 68, 124]

この羽数を棒グラフで比べる.

plt.figure(figsize=(6, 4))
plt.bar(species_names, counts, color="steelblue")
plt.xlabel("Species")
plt.ylabel("Number of penguins")
plt.tight_layout()
plt.show()
Adelie 152羽,Chinstrap 68羽,Gentoo 124羽の棒グラフ.
図 11.2: ペンギンの種類ごとの羽数.

図 11.2 から,Adelie が 152 羽と最も多く,Gentoo が 124 羽,Chinstrap が 68 羽であることがわかる.種類の名前には順序がない.このような項目の値を比べるときも,棒グラフを使う.

11.3.2 ひれの長さの分布

次に,ひれの長さ flipper_length_mm の分布を,区間の幅を 5 mm にしたヒストグラムで調べる.

bins = [170, 175, 180, 185, 190, 195, 200, 205, 210, 215, 220, 225, 230, 235]

plt.figure(figsize=(7, 4))
plt.hist(penguins["flipper_length_mm"], bins=bins, color="steelblue", edgecolor="white")
plt.xlabel("Flipper length (mm)")
plt.ylabel("Number of penguins")
plt.xticks(bins)
plt.tight_layout()
plt.show()
190以上195未満が62羽で最も多い山と,210以上215未満の35羽,215以上220未満の36羽からなる山の,二つの山がある.その間の200以上210未満の二つの区間は,それぞれ19羽.単位はミリメートル.
図 11.3: ペンギン 342 羽のひれの長さ.区間の幅を 5 mm にしたヒストグラム.

ひれの長さが記録されていない 2 羽は,plt.hist() で自動的に除かれる.そのため,図 11.3 には 342 羽分の分布が描かれている.

図 11.3 には,190 mm 以上 195 mm 未満の区間を頂点とする山と,210 mm 以上 220 mm 未満あたりの山の,二つの山がある.その間の 200 mm 以上 210 mm 未満では,羽数が少ない.山が二つある分布は,性質の異なるグループが混ざっているときに現れやすい.このデータには 3 種類のペンギンが含まれているので,種類による違いが考えられる.

11.3.3 種類ごとに色を分けて描く

山が二つある理由を確かめるため,種類ごとに色を分けたヒストグラムを描く.plt.hist() に,種類ごとのデータをリストにまとめて渡し,stacked=True を指定すると,種類ごとの度数を積み重ねて描ける.label には,凡例に表示する種類の名前を,データと同じ順に指定する.

plt.figure(figsize=(7, 4))
plt.hist(
    [
        adelie["flipper_length_mm"],
        chinstrap["flipper_length_mm"],
        gentoo["flipper_length_mm"],
    ],
    bins=bins,
    stacked=True,
    label=species_names,
    edgecolor="white",
)
plt.xlabel("Flipper length (mm)")
plt.ylabel("Number of penguins")
plt.xticks(bins)
plt.legend()
plt.tight_layout()
plt.show()
左の山は主にAdelieとChinstrapからなり,190ミリメートル前後に集まる.右の山は主にGentooからなり,210から220ミリメートルあたりに集まる.Gentooのひれは203から231ミリメートルの範囲にある.
図 11.4: ペンギンのひれの長さ.種類ごとの度数を積み重ねたヒストグラム.

各区間の棒全体の高さは,図 11.3 と同じである.図 11.4 から,左の山は主に Adelie と Chinstrap,右の山は主に Gentoo からなることがわかる.Gentoo のひれは,ほかの 2 種類より長い.

全体のヒストグラムだけでは,山が二つある理由はわからなかった.種類ごとに分けると,このデータでは種類による違いが二つの山に対応していることがわかる.体の大きさを比べるときは,全体の分布だけでなく,種類ごとの分布にも注目しよう.

11.4 種類によって体重はどのくらい違うか

11.4.1 重いペンギンはどの種類か

体重が 5000 g 以上のペンギンを取り出し,そのうち何羽が Gentoo(ジェンツーペンギン)かを数える.

heavy = penguins[penguins["body_mass_g"] >= 5000]
heavy_gentoo = heavy[heavy["species"] == "Gentoo"]
print("5000 g 以上の羽数:", len(heavy))
print("そのうち Gentoo の羽数:", len(heavy_gentoo))
5000 g 以上の羽数: 67
そのうち Gentoo の羽数: 67

5000 g 以上の 67 羽は,すべて Gentoo である.体重が欠損している行は,条件を満たさないものとして扱われ,取り出されない.ひれの長さだけでなく,体重にも種類による違いがありそうだ.次に,箱ひげ図と数値で,体重の中心とばらつきを比べよう.

11.4.2 箱ひげ図で比べる

体重 body_mass_g には欠損値が 2 件ある.np.percentile() は,欠損値を含むデータに使うと結果が nan となり,箱ひげ図も正しく描けない.そこで,dropna() を使い,体重の欠損値を除いた Series を用意する.性別が欠損していても,体重が記録されている行は使う.

adelie_mass = adelie["body_mass_g"].dropna()
chinstrap_mass = chinstrap["body_mass_g"].dropna()
gentoo_mass = gentoo["body_mass_g"].dropna()
plt.figure(figsize=(6, 4))
plt.boxplot(
    [adelie_mass, chinstrap_mass, gentoo_mass],
    tick_labels=["Adelie", "Chinstrap", "Gentoo"],
)
plt.ylabel("Body mass (g)")
plt.grid(axis="y", alpha=0.3)
plt.tight_layout()
plt.show()
Adelieの箱は3350から4000,中央値は3700.Chinstrapの箱は3487.5から3950,中央値は3700で,2700と4800に点がある.Gentooの箱は4700から5500,中央値は5000.単位はグラム.
図 11.5: ペンギンの種類ごとの体重の箱ひげ図.

図 11.5 では,Gentoo の箱が,ほかの 2 種類より高い位置にある.一方,Adelie と Chinstrap の中央値の線は,同じ高さにある.

11.4.3 数値で比べる

種類ごとに,中央値,四分位範囲,標準偏差を求める.ここでは,母集団のばらつきを標本から推定するときによく使われる,不偏分散の平方根を標準偏差として用いる.そのため,np.std() には ddof=1 を指定する.三つの Series をリストにまとめ,for 文で順に計算する.

masses = [adelie_mass, chinstrap_mass, gentoo_mass]

for i in range(3):
    mass = masses[i]
    q1 = np.percentile(mass, 25)
    q3 = np.percentile(mass, 75)
    std = np.std(mass, ddof=1)
    print(species_names[i])
    print("  中央値:", mass.median())
    print("  四分位範囲:", q3 - q1)
    print("  標準偏差:", round(std, 1))
Adelie
  中央値: 3700.0
  四分位範囲: 650.0
  標準偏差: 458.6
Chinstrap
  中央値: 3700.0
  四分位範囲: 462.5
  標準偏差: 384.3
Gentoo
  中央値: 5000.0
  四分位範囲: 800.0
  標準偏差: 504.1

結果を表にまとめると,次のようになる.

指標 Adelie Chinstrap Gentoo
中央値 3700 g 3700 g 5000 g
四分位範囲 650 g 462.5 g 800 g
標準偏差(ddof=1) 458.6 g 384.3 g 504.1 g

Gentoo の中央値は 5000 g で,ほかの 2 種類より 1300 g 大きい.Adelie と Chinstrap の中央値はどちらも 3700 g だが,四分位範囲も標準偏差も Chinstrap の方が小さい.Chinstrap の体重は,Adelie よりも中央値の近くに集まっている.中心が同じでも,ばらつきが同じとは限らない.

また,四分位範囲で比べても,標準偏差で比べても,ばらつきは小さい方から Chinstrap,Adelie,Gentoo の順である.

11.5 離れた点は誤った記録なのか

図 11.5 では,Chinstrap に,ひげの外の点が二つある.体重が 4800 g と 2700 g の 2 羽である.この 2 羽の記録を確かめる.

heaviest = chinstrap[chinstrap["body_mass_g"] == 4800]
lightest = chinstrap[chinstrap["body_mass_g"] == 2700]
print(heaviest[["species", "body_mass_g", "sex"]])
print(lightest[["species", "body_mass_g", "sex"]])
       species  body_mass_g   sex
313  Chinstrap       4800.0  male
       species  body_mass_g     sex
314  Chinstrap       2700.0  female

4800 g の個体は雄(male),2700 g の個体は雌(female)である.Chinstrap の体重の中央値を,性別ごとに求めてみよう.

chinstrap_male = chinstrap[chinstrap["sex"] == "male"]
chinstrap_female = chinstrap[chinstrap["sex"] == "female"]
print("雄の中央値(g):", chinstrap_male["body_mass_g"].median())
print("雌の中央値(g):", chinstrap_female["body_mass_g"].median())
雄の中央値(g): 3950.0
雌の中央値(g): 3550.0

雄の中央値は 3950 g,雌の中央値は 3550 g であり,雄の方が重い.重い方の点が雄,軽い方の点が雌であることは,この性別による違いと合っている.体重のばらつきには,種類だけでなく性別も関係していそうだ.ただし,これだけで,二つの記録が正しいかどうかを判断することはできない.箱ひげ図でひげの外にある点も,直ちに誤った記録とみなすことはできない.外れ値の候補を見つけたら,削除する前に,その個体のほかの記録や調査方法を確かめよう.

11.6 全体と種類別で相関は同じか

ここまでは,一つの変数の分布を種類ごとに比べた.発展課題として,二つの変数の関係についても,全体と種類ごとの結果を比べてみよう.

練習 11.1 本章で読み込んだ penguins を使い,くちばしの長さ bill_length_mm と厚さ bill_depth_mm の関係を調べよ.

  1. すべてのペンギンについて,くちばしの長さを横軸,厚さを縦軸とする散布図を描き,相関係数を求めよ.
  2. 種類ごとに相関係数を求めよ.
  3. 1 と 2 の結果を比べ,わかることを説明せよ.

くちばしの大きさが記録されていない行は,あらかじめ除いておく.penguins.dropna(subset=["bill_length_mm", "bill_depth_mm"]) とすると,指定した 2 列のどちらかが欠損値である行を除いた表が得られる.

11.7 わかったことをまとめる

今回のデータから,次のことがわかった.

  • ひれの長さの分布には二つの山があり,左の山は主に Adelie と Chinstrap,右の山は主に Gentoo からなる.
  • Gentoo は,ほかの 2 種類より体重の中央値が大きい.Adelie と Chinstrap は中央値が同じでも,体重のばらつきが異なる.
  • Chinstrap では,雄の方が雌より体重の中央値が大きい.体重の違いを考えるには,種類だけでなく性別にも注目する必要がある.

これらは,このデータに記録された個体についての結果である.データに含まれる種類ごとの羽数から,南極全体に生息する羽数の割合まではわからない.どこで,いつ,どのように調査されたデータかを確かめ,結論の範囲を明確にしよう.

分析結果を説明するときは,「Gentoo は大きい」だけでなく,何を比べたのか,どの数値や図を根拠にしたのかを示す.例えば,「このデータでは,Gentoo の体重の中央値は 5000 g で,Adelie と Chinstrap より 1300 g 大きい」と書くと,違いが具体的に伝わる.

Gorman, Kristen B, Tony D Williams, と William R Fraser. 2014年. 「Ecological sexual dimorphism and environmental variability within a community of Antarctic penguins (genus Pygoscelis)」. PLoS One 9 (3): e90081. https://doi.org/10.1371/journal.pone.0090081.
Horst, Allison Marie, Alison Presmanes Hill, と Kristen B Gorman. 2020年. palmerpenguins: Palmer Archipelago (Antarctica) penguin data. https://doi.org/10.5281/zenodo.3960218.