ねこすたっと

ねこの気持ちと統計について悩む筆者の備忘録的ページ。

分散分析(ANOVA):球面性仮定

分散分析で満たすべき仮定として球面性の仮定(the assumption of sphericity)*1をよく目にしますが、きちんと勉強したことがなかったので(わかるところだけ)読んでみました。

分散分析に必要な仮定

異なる対象者同士で比較する被験者間分散分析(between-subjects ANOVA)には、

  • 観測が互いに独立である(independence of observations)
  • 各群内で正規分布している(normality)
  • 各群の分散が等しい(homogeneity of variance)

の3つの仮定が必要です。

同じ対象者に対して観察された値同士を比較する 被験者内分散分析(within-subjects ANOVA) 、あるいは反復測定分散分析(repeated measures ANOVA) では、上記に加え 球面性の仮定(the assumption of sphericity) が必要になります。

球面性の仮定は反復測定ANOVAにおいてF値が正確なF分布に従うための必要十分条件で、これが満たされていないと第1種過誤が増加してしまい*2、ANOVAの結果が不適切になってしまいます。

球面性の仮定とは

Lane (2016) の導入で、球面性について2通りの定義を紹介していました。

  • 2水準間の差の分散に基づいた定義方法
  • 直交対比(orthogonal contrast)の分散・共分散に基づいた定義方法

両者は同値ですが、前者の方が説明が分かりやすく、後者の方が一般化して複雑なデザインにも対応できます。

2水準間の差の分散に基づいた定義方法

要因に含まれる全ての2水準間について対象者内の差を計算し、その分散が全て等しいとき、球面性が満たされていると定義します。 下の表では、T1〜T3の3つの水準の差(T3-T2, T3-T1, T2-T1)を計算して、その分散が全て等しい値(=8)になっていることを示しています。 ちなみに分散分析では各群の分散が等しくなければいけないので、T1〜T3の分散も全て等しい値(=10)になっていることを示しています。

]

水準が2つしかない場合(例えば前値・後値を測定)、水準間差は1つしか存在しないので、「水準間によって分散が異なっている」という事態は発生せず球面性は常に成り立ちます。

直交対比の分散・共分散に基づいた定義方法

多重比較の手順の1つであるSchefféの方法で対比(contrast)が登場します。複数の平均値(例えば \mu_1, \mu_2, \mu_3, \mu_4)を比べるとき、任意の平均の比較は \mu_1, \mu_2, \mu_3, \mu_4の線型結合が0であるという形式で表すことができます。

例えば「1番目と2番目の平均が等しい」、つまり  \mu_1 = \mu_2 という仮説は、

 1 \times \mu_1 + (-1) \times \mu_2 + 0 \times \mu_3 +  0 \times \mu_4 = 0

と書けますし、「1番目の平均と他の3つの平均の平均が等しい」、つまり  \mu_1 = \frac{\mu_2 + \mu_3 + \mu_4}{3} という仮説は、

 1 \times \mu_1 + \left( -\frac{1}{3} \right) \times \mu_2 +  \left( -\frac{1}{3} \right)  \times \mu_3 +   \left( -\frac{1}{3} \right)  \times \mu_4 = 0

と書けます。

この線型結合の係数を 対比(contrast) といいます(各係数を要素に持つベクトルです)。

2つの対比が直交する(=内積が0)とき、この2つの対比は直交対比(orthogonal contrast)であると言います。 分散分析の帰無仮説である「p個の平均が全て等しい」、つまり \mu_1 = \mu_2 = ... = \mu_pという仮説は、「p-1個の直交対比が全て0である」という仮説に置き換えることができます(p-1個の直交対比の選び方は無数にありますが、互いに直交する対比は常にp-1個しか作れません)。

このp-1個の直交対比ベクトル(←各々がp個の要素からなる)を並べた(p-1)×p行列を直交対比行列(orthogonal matrix)と言います。 さらに、

  • 各行の和 = 0
  • 各行の長さ(平方和)= 1

という条件が追加されたものを正規直交対比行列(orthonormal contrast matrix)と言います。

元のデータの共分散行列  \Sigma を正規直交対比行列  C を使って、  C \Sigma C^{T} と変換したものが、対角成分が全て等しい対角行列  \lambda Iになっているというのが球面性の仮定です。

 C \Sigma C^{T} =  \lambda I

 \Sigma = E(X^{T} X) を鑑みてさらに読み砕くと、

元データXを正規直交対比行列で変換した X C^{T} の分散が等しく、かつ互いに相関がない というのが球面性の仮定の意味するところです。

直交対比に基づいた球面性の仮定については、名古屋大学大学院 教育発達科学研究科 心理発達科学専攻 計量心理学領域 石井研究室が公開している資料集を参考にしました。非常に分かりやすい資料でとても勉強になりましした。

検定方法

Mauchly(モクリー)の球面性検定がよく知られています。Mauchlyの統計量Wは0(球面性が成り立たない)〜1(完全に球面性OK)の値を取り、仮説が棄却された(P値が小さい)場合は球面性が成り立っていないと判断します。

もし被験者内要因(=反復測定要因)に加えて、被験者間要因を含むデザインでは、共分散行列が群ごとで等質かどうかを検証しなくてはなりません。Mauchly検定はこれに対応していません。球面性と共分散の等質性を一度に検定する方法としてMendozaの多標本球面性検定があります。

これについては井関先生が詳しく解説されています(井関先生が作成されたR関数ANOVA君ではMendoza検定がデフォルトになっているとのこと)。

riseki.php.xdomain.jp

球面性仮定が満たされなかったときの補正方法

球面性仮定が満たされていない場合、計算されたF値がF分布に従うとみなせなくなるので調整が必要です。ε統計量を使って分散分析の自由度を調整します。

データから計算されたF値をF(df1, df2)分布に当てはめてP値を計算するのではなく、F(ε×df1, ε×df2)分布に当てはめて計算します。当てはめるF分布の自由度が小さいほど計算されるP値は大きくなります。

ε統計量の計算方法はいくつかあります。いずれも0〜1の範囲で求められ、球面性が成り立っていないときほど0に近い値になるので、自由度は調整前よりも小さくなり、P値は調整前よりも大きくなります。

  • 下限値:εの取りうる理論的下限値です。1/(水準数-1)で計算できるのでPC不要ですが、保守的過ぎるので通常使いません。
  • Greenhouse-Geisser法*3:ε≧0.75のときには厳し過ぎるとのこと
  • Huynh-Feldt法*4:計算上1を超えることがありますが、その場合は1を用います。Lecoutre(ルクルト)による修正版が用いられていることが多く、Huynh-Feldt-Lecoutre法と呼ぶ方が良いかもしれません。
  • Chi-Muller法:計算上1を超えることも、下限値よりも小さい値になることもあるとのこと。GG法、HFL法の使い分けを気にせず、広い状況で優れている?

使い分けの詳細はまだ勉強しきれていません。ひとまずは

  • ε<0.75 → Greenhouse-Geisser法
  • ε≧0.75 → Huynh-Feldt-Lecoutre法

とざっくり覚えておきたいと思います。

おわりに

  • 理解という意味ではまだまだですが、出力された表に何が書いてあるかは分かるようになりました。
  • 猫の爪研ぎタワーから毎日大量のゴミが出ます。麻紐がとれて芯が剥き出しです。

参考資料

  • Lane, D. M. (2016). The assumption of sphericity in repeated-measures designs: what it means and what to do when it is violated. The Quantitative Methods for Psychology, 12(2), 114–122. doi:10.20982/tqmp.12.2.p114

  • 千野直仁. 行動研究における反復測定デザインANOVAの誤用 - I. 愛知学院大学心身科学部紀要第9号(45-52)(2013).

  • EZR, SPSSでの実行方法なども紹介されています。

toukeier.hatenablog.com

*1:千野(2013)によると「球面性」ではなく「球形」とすべきと言われています。「表面だけでなく球内部も分布しているから」という理由で納得しましたが、ここでは検索へのかかりやすさの観点から「球面性」としました。

*2:差がないのに有意差ありとしてしまう

*3:グリーンハウス・ガイサーと読むそうです

*4:フィン・フェルトと読むそうです

チュートリアル拾い読み (12):Pandas公式チュートリアル (7/7) [Python]

Rもたいして使いこなせてないのにPythonの勉強を始めてみました。色々と素晴らしいチュートリアルはありそうですが、目移りしてしまうので公式チュートリアルを拾い読みしていきます。

NumPyのあとPandasチートシート(PDF)を中心に読んできましたが、今回で最後です。

前回:

necostat.hatenablog.jp

最終回は「データの要約と可視化」です。Pandas公式チュートリアル拾い読みの初回で紹介した、irisデータを例に使います。

import pandas as pd
df = pd.read_csv("iris.csv")

データの形状を調べる

.shape()メソッドでdfの形状を表示します。

df.shape  # 出力: (150, 5)

150行5列のデータであることがわかります。

行数を知りたいときは、len()関数を使うこともできます。

len(df)  # 出力: 150

連続変数の要約統計量を計算する

dfに含まれる連続変数の要約値をまとめて調べたいときは.describe()というメソッドを使います。

df.describe()

# 出力: 
       sepal.length  sepal.width  petal.length  petal.width
count    150.000000   150.000000    150.000000   150.000000
mean       5.843333     3.057333      3.758000     1.199333
std        0.828066     0.435866      1.765298     0.762238
min        4.300000     2.000000      1.000000     0.100000
25%        5.100000     2.800000      1.600000     0.300000
50%        5.800000     3.000000      4.350000     1.300000
75%        6.400000     3.300000      5.100000     1.800000
max        7.900000     4.400000      6.900000     2.500000

個々の統計量を得たい場合は次のメソッドを使います。

  • .sum():合計
  • .count():観測個数
  • .median():中央値
  • .quantile([0.25,0.75]):四分位範囲
  • .min():最小値
  • .max():最大値
  • .mean()平均
  • .var():分散
  • .std():標準偏差

カテゴリー変数を要約する

カテゴリー変数に対して.nunique()メソッドを適用すれば、カテゴリー数がわかります。

df["variety"].nunique()  # 出力: 3

個々のカテゴリーに含まれる観察数を求めたい場合は、.value_counts()を使います。

df["variety"].value_counts()

# 出力:
Setosa        50
Versicolor    50
Virginica     50
Name: variety, dtype: int64

グループごとに要約する

例えば治療群など、ある変数のカテゴリー別に要約したいときには、.groupby()メソッドを使ってデータ内のグループ構造を指定します。

グループ分けに使う変数はbyで指定します(インデックス名を使って分けたいときはlevelを使うみたいです)。

df_grouped = df.groupby(by="variety")

各グループ毎に要約する

グループ化されたデータに.describe()メソッドを使うと、各グループ毎の要約が返されます。

df_grouped.describe()

# 出力: 
           sepal.length                                            (略)
                  count   mean       std  min    25%  50%  75%  max ~  
variety                                                               
Setosa             50.0  5.006  0.352490  4.3  4.800  5.0  5.2  5.8 ~  
Versicolor         50.0  5.936  0.516171  4.9  5.600  5.9  6.3  7.0 ~  
Virginica          50.0  6.588  0.635880  4.9  6.225  6.5  6.9  7.9 ~  

各グループの観察数を調べる

グループ化されたデータに.size()メソッドを使うと、各グループのデータ数が返されます。

df_grouped.size()

# 出力: 
variety
Setosa        50
Versicolor    50
Virginica     50
dtype: int64

グループ内の順位を返す

.rank()メソッドを使うと、それぞれのデータの各グループ内での順位が返されます。 主な引数は、以下のとおり。

  • method:同じ値(タイ)の扱い方を指定します。デフォルトはaverageで、タイには平均順位が割り振られます。
  • ascending:デフォルトはTrueで、小さい値に小さい順位が振られます。
  • pctTrueにすると0-1の範囲にスケーリングされます。
df_grouped.rank(method="average")

# 出力: 
     sepal.length  sepal.width  petal.length  petal.width
0            32.5         31.5          18.0         20.0
1            18.5          5.5          18.0         20.0
2            10.5         15.0           8.0         20.0
3             7.5         10.5          31.0         20.0
4            24.5         36.0          18.0         20.0
..            ...          ...           ...          ...
145          31.0         27.5          17.5         40.5
146          16.5          3.5           8.0         19.0
147          26.5         27.5          17.5         24.5
148          12.5         46.5          21.5         40.5
149           7.0         27.5          13.0         11.0

[150 rows x 4 columns]

プロットする

チートシートの扱いはかなり小さく、本格的なグラフ作成をするには(他のライブラリを含め)もう少しじっくりと勉強する必要がありそうです。 ここではチートシートに載っていたヒストグラムと散布図だけにとどめておきます。

.plot()メソッドの中に、グラフの種類を指定すればひとまず描けます。ヒストグラムならkind="hist"だそうです。

df["sepal.length"].plot(kind="hist")

.plotの後に.hist()を繋げても描けるようです。

df["sepal.length"].plot.hist()

散布図はx軸に持ってくる変数とy軸に持ってくる変数を指定する必要があります。

df.plot.scatter(x="sepal.length",y="sepal.width")

おわりに

今回学んだこと:

  • 代表的な要約統計量を計算する
  • グループ毎に計算する
  • 非常に簡単なプロット

チュートリアルは読んだけど取り上げなかったもの:

  • グループ毎に累積和などを計算する方法(.cumsum()など)
  • グループ毎に決まった数だけずらす方法(.shift()

次回:未定

チュートリアル拾い読み (11):Pandas公式チュートリアル (6/7) [Python]

Rもたいして使いこなせてないのにPythonの勉強を始めてみました。色々と素晴らしいチュートリアルはありそうですが、目移りしてしまうので公式チュートリアルを拾い読みしていきます。

NumPyのあとPandasチートシート(PDF)を中心に読んできましたが、そろそろ終盤です。

前回:

necostat.hatenablog.jp

今回はデータの結合です。Pandasライブラリを読み込んでおきます。

import pandas as pd

単純に結合する

pd.concate()という関数を使って結合します。引数としてaxis=0とすれば上下方向に、axis=1とすれば左右方向に結合します(デフォルトはaxis=0)。

上下に結合する

模式的に描くと次のような感じです。

次の2つのサンプルデータ(df1, df2)を上下に結合してみます。2つのdfでは、

  • 変数の並び順が異なっている
  • 重複データが含まれている

となるようにしてみました。

df1 = pd.DataFrame(
    {"patient":["A","B","C"],
     "time1":[10,20,30],
     "time2":[40,50,60]},
)

df2 = pd.DataFrame(
    {"time2":[40,25,35],
     "patient":["A","E","F"],
     "time1":[10,55,65]},
)
pd.concat([df1,df2],axis=0) 

# 出力:
  patient  time1  time2
0       A     10     40
1       B     20     50
2       C     30     60
0       A     10     40
1       E     55     25
2       F     65     35

列の順序が異なっていても列名が揃うように結合してくれています。また、単純に結合するだけなので重複は除かれていません。

左右に結合する

模式的に描くと次のような感じです。

今度は次の2つのサンプルデータ(df1, df3)を左右に結合してみます。2つのdfではpatientの並び順が異なっています。

df1 = pd.DataFrame(
    {"patient":["A","B","C"],
     "time1":[10,20,30],
     "time2":[40,50,60]},
)

df3 = pd.DataFrame(
    {"patient":["C","B","A"],
     "time3":[15,25,35],
     "time4":[45,55,65]},
)

pd.concat()axis=1を指定します。

pd.concat([df1,df3],axis=1) 

# 出力:
  patient  time1  time2 patient  time3  time4
0       A     10     40       C     15     45
1       B     20     50       B     25     55
2       C     30     60       A     35     65

並び順はそのままで結合されています。縦方向のときは列名を元にして対応が決められますが、横方向のときはどれが観測単位を識別する変数か分からないので勝手に揃えてはくれません。

次のようにpatientをインデックス名に指定すれば、

df1_new = df1.set_index("patient")
df3_new = df3.set_index("patient")
pd.concat([df1_new,df3_new],axis=1)

# 出力:
         time1  time2  time3  time4
patient                            
A           10     40     35     65
B           20     50     25     55
C           30     60     15     45

上手く揃えてくれました。でも、このように揃えて結合したいのであれば、次に紹介する方法を使う方が良さそうです。

キーで紐付けて結合する

キーで紐付けして結合する場合は、pd.merge()という関数を使います。指定する主な引数は、

  • how:どの行を残すのか(left, right, inner, outer)を指定します
  • on:紐付けに使うキーを指定します

左側のdfの行を残す

how=leftとすると、左側(1つ目に指定したdf)に含まれる対象者のみ残ります。右側(2つ目に指定したdf)にデータがない箇所はNaNになります。

試しに次の2つのdfを結合してみます。

df1 = pd.DataFrame(
    {"patient":["A","B","C"],
     "time1":[10,20,30],
     "time2":[40,50,60]},
)

df2 = pd.DataFrame(
    {"time2":[40,25,35],
     "patient":["A","E","F"],
     "time1":[10,55,65]},
)

df1が左側、df2が右側として、patientを紐付けキーにして結合します。

pd.merge(df1, df2, how="left", on="patient") 

# 出力:
  patient  time1_x  time2_x  time2_y  time1_y
0       A       10       40     40.0     10.0
1       B       20       50      NaN      NaN
2       C       30       60      NaN      NaN

右側のdfに含まれていない"B", "C"についてはデータが追加されずNaNとなっています。 また、左右で同じ列名がある場合、結合すると列名が重複してしまうので、左側のdfの列名には_x、右側のdfの列名には_yの接尾辞が付与されます。

右側のdfの行を残す

how=rightとすると、右側(2つ目に指定したdf)に含まれる対象者のみ残ります。

指定するdfの順序を入れ替えて、how=leftとしても一緒なので、あまり使う機会がありません。

どちらのdfにも含まれる行を残す

how=innerとすると、左右どちらのdfにも含まれている対象者(積集合)が残ります。

全ての行を残す

how=outerとすると、どちらかのdfに含まれている対象者(和集合)が残ります。

おわりに

今回学んだこと:

  • 2つのデータをキーで紐つけて結合する方法

チュートリアルは読んだけど取り上げなかったもの:

  • 今回もありません(チュートリアルを読む量が少ないだけ)。

次回:

necostat.hatenablog.jp

チュートリアル拾い読み (10):Pandas公式チュートリアル (5/7) [Python]

Rもたいして使いこなせてないのにPythonの勉強を始めてみました。色々と素晴らしいチュートリアルはありそうですが、目移りしてしまうので公式チュートリアルを拾い読みしていきます。

NumPyが終わってPandasに入っています。導入版(Intro to pandas)・簡易版(10 minutes to pandas)は物足りなず、本格版(User guideのIntro to data structures以降)は読みきれなかったので、チートシート(PDF)を中心に読んでいきます。

前回:

necostat.hatenablog.jp

今回はデータの縦横変形です。

横型データ・縦型データとは

同一対象者に対して複数回測定が行われた場合、データを保持する方法が2種類あります。 例えば、A, B, Cの3人に対して1990年と2000年にある項目を測定したデータシートを考えてみます。

横型データは、下のようにそれぞれの人について測定値を横に並べたデータです。よく見かける1行=1対象者の構造になっています。「Wide型」とか「横持ち」などと表現されることもあります。

これに対して縦型データは、それぞれの人について測定値を縦に並べたデータです。「Long型」とか「縦持ち」と表現されることがあります。

縦型データでは、値を格納する変数とその測定の属性(ここでは年)を格納する変数のペアとしてデータを保持します。これに加え、対象者を識別する変数(=ID)があり、同一対象者のデータはこれを元に関連づけられます。横型データが1行=1対象者であったのに対し、縦型データでは1行=1観察単位という構造になっています。

Pandasライブラリを読み込んで、サンプルデータを用意しておきます。

import pandas as pd
df_wide = pd.DataFrame(
    {"patient":["A","B","C"],
     "time1":[10,20,30],
     "time2":[40,50,60]},
)
df_wide

# 出力:
  patient  time1  time2
0       A     10     40
1       B     20     50
2       C     30     60

横型を縦型に変形する

.melt()というメソッドを使います。指定する主な引数は今のとおり。

  • id_vars:縦変換しない変数
  • value_vars:縦型に変換したい変数
  • var_name:ラベルを格納する列につける名前
  • value_name:値を格納する列につける名前
df_long = df_wide.melt(
    id_vars = "patient",
    value_vars = ["time1","time2"],
    var_name = "timing",
    value_name ="lab"
)
df_long

# 出力:
  patient timing  lab
0       A  time1   10
1       B  time1   20
2       C  time1   30
3       A  time2   40
4       B  time2   50
5       C  time2   60

縦型を横型に変形する

.pivot()というメソッドを使って、先程作成したデータ(df_long)を再度横型にしてみます。主な引数は以下のとおり。

  • index:新しいデータフレームのインデックス名に使用する変数
  • columns:横型に展開した部分の列名に使う変数
  • values:横型に展開した部分の値に使う変数
df_rewide = df_long.pivot(
    index = "patient",
    columns = "timing",
    values = "lab"
)
df_rewide

# 出力: 
timing   time1  time2
patient              
A           10     40
B           20     50
C           30     60

インデックスの階層構造を解きたいときは、.reset_index()を使います。

df_rewide.reset_index()

# 出力:
timing patient  time1  time2
0            A     10     40
1            B     20     50
2            C     30     60

おわりに

今回学んだこと:

  • データの縦横変形

チュートリアルは読んだけど取り上げなかったもの:

  • 今回はありません。

次回:

(未作成)

チュートリアル拾い読み (9):Pandas公式チュートリアル (4/7) [Python]

Rもたいして使いこなせてないのにPythonの勉強を始めてみました。色々と素晴らしいチュートリアルはありそうですが、目移りしてしまうので公式チュートリアルを拾い読みしていきます。

NumPyが終わってPandasに入っています。導入版(Intro to pandas)・簡易版(10 minutes to pandas)は物足りなず、本格版(User guideのIntro to data structures以降)は読みきれなかったので、チートシート(PDF)を中心に読んでいきます。

前回:

necostat.hatenablog.jp

前回からデータテーブルの一部の行(= 観察単位, observation)や列(= 変数, variable)、あるいは両者が交差する要素にアクセスする方法を確認しています。今回は列単位の抽出と、ついでに列に対する操作を確認します。

最初にPandasライブラリと、前回使用したirisデータセットを読み込んでおきます。

import pandas as pd
df = pd.read_csv("iris.csv")

列を抽出する

列名を指定して抽出する

[] の中に列名を書きます。複数の場合はリストで渡します。

df[["sepal.length", "sepal.width"]]

# 出力:
     sepal.length  sepal.width
0             5.1          3.5
1             4.9          3.0
2             4.7          3.2
3             4.6          3.1
4             5.0          3.6
..            ...          ...
145           6.7          3.0
146           6.3          2.5
147           6.5          3.0
148           6.2          3.4
149           5.9          3.0

[150 rows x 2 columns]

条件に合致する列を抽出する

.filter()を使います(Rでは行の部分抽出に使う関数なので間違えそうです)。列の名前に基づいた条件で選んでくることになるので、使いこなそうとすると正規表現(文字列のパターンを表すための決まりごと)を学ぶ必要がありそうです。

df.filter(regex="abc")とすれば、変数名にabcを含んでいるものだけが取り出されます。

例えば、irisデータの中で"al"が含まれるものは、

df.filter(regex="al")
# 出力:
     sepal.length  sepal.width  petal.length  petal.width
0             5.1          3.5           1.4          0.2
1             4.9          3.0           1.4          0.2
2             4.7          3.2           1.3          0.2
3             4.6          3.1           1.5          0.2
4             5.0          3.6           1.4          0.2
..            ...          ...           ...          ...
145           6.7          3.0           5.2          2.3
146           6.3          2.5           5.0          1.9
147           6.5          3.0           5.2          2.0
148           6.2          3.4           5.4          2.3
149           5.9          3.0           5.1          1.8

となります。

その他、頻用しそうなものとして、

  • ^abc:名前がabcで始まるもの
  • xyz$:名前がxyzで終わるもの

は覚えておきます。 もう少し複雑なもの(?!や記号のバックエスケープなど)はまた別の機会に...。

列を除外する

残す変数を選ぶのではなく、削除したい変数を指定する場合は、.drop()を使います。

df.drop("sepal.length", axis=1)

列に対する操作なのでaxis=1が必要です。インデックス名を使って行を削除するときはaxis=0とします。

.drop()は正規表現を直接受け付けてくれないようです。

列名を変更する

.rename()を使います。columns={ }のカッコ内に、"旧名":"新名"の要領で書きます。

df.rename(columns={"sepal.length":"S.L",
                   "sepal.width":"S.W",
                   "petal.length":"P.L",
                   "petal.width":"P.W"})
# 出力:
     S.L  S.W  P.L  P.W    variety
0    5.1  3.5  1.4  0.2     Setosa
1    4.9  3.0  1.4  0.2     Setosa
2    4.7  3.2  1.3  0.2     Setosa
3    4.6  3.1  1.5  0.2     Setosa
4    5.0  3.6  1.4  0.2     Setosa
..   ...  ...  ...  ...        ...
145  6.7  3.0  5.2  2.3  Virginica
146  6.3  2.5  5.0  1.9  Virginica
147  6.5  3.0  5.2  2.0  Virginica
148  6.2  3.4  5.4  2.3  Virginica
149  5.9  3.0  5.1  1.8  Virginica

列を作成する

1つずつ作成する

既存の変数をもとに、新たな変数を作成する場合は、df[]を使います。例えば、sepal.lengthsepal.widthの積を保持するsepal.areaという変数を作成する場合は次のように書きます。

df["sepal.area"] = df["sepal.length"]*df["sepal.width"]
# 出力: 
   sepal.length  sepal.width  petal.length  petal.width variety  sepal.area
0           5.1          3.5           1.4          0.2  Setosa       17.85
1           4.9          3.0           1.4          0.2  Setosa       14.70
2           4.7          3.2           1.3          0.2  Setosa       15.04
3           4.6          3.1           1.5          0.2  Setosa       14.26
4           5.0          3.6           1.4          0.2  Setosa       18.00

複数まとめて作成する

.assign()を使って、複数の列を一度に作成できます。

df = df.assign(sepal_area = df["sepal.length"]*df["sepal.width"],
               petal_area = df["petal.length"]*df["petal.width"])

作成した変数を保持するためにdfに再代入しました。

この方法だとピリオドが入った変数名で作成することができません(引数を辞書形式で渡せば出来るみたいです)。

おわりに

今回学んだこと:

  • 列の抽出・除外
  • 列名変更
  • 列の作成

チュートリアルは読んだけど取り上げなかったもの:

  • 正規表現
  • .assign()メソッドの中でラムダ式で変数を定義する方法

この2つはチートシート拾い読みで扱うには大きすぎます。

  • .clip()を使って指定した上下端でトリミングをする方法
  • .qcut()を使って連続変数をカテゴリー化する方法

この2つは知ってると便利そうだなと思いました。

次回:

necostat.hatenablog.jp

チュートリアル拾い読み (8):Pandas公式チュートリアル (3/7) [Python]

Rもたいして使いこなせてないのにPythonの勉強を始めてみました。色々と素晴らしいチュートリアルはありそうですが、目移りしてしまうので公式チュートリアルを拾い読みしていきます。

NumPyが終わってPandasに入っています。導入版(Intro to pandas)・簡易版(10 minutes to pandas)は物足りなず、本格版(User guideのIntro to data structures以降)は読みきれなかったので、チートシート(PDF)を中心に読んでいきます。

前回:

necostat.hatenablog.jp

前回からデータテーブルの一部の行(= 観察単位, observation)や列(= 変数, variable)、あるいは両者が交差する要素にアクセスする方法を確認しています。今回は行単位の抽出から。

最初にPandasライブラリと、前回使用したirisデータセットを読み込んでおきます。

import pandas as pd
df = pd.read_csv("iris.csv")

先頭・末尾の行を抽出する

先頭を取り出すには.head()、末尾を取り出すには.tail()というメソッドを使います。取り出したい行数を指定できます。

df.head(n=3)
# 出力:
   sepal.length  sepal.width  petal.length  petal.width variety
0           5.1          3.5           1.4          0.2  Setosa
1           4.9          3.0           1.4          0.2  Setosa
2           4.7          3.2           1.3          0.2  Setosa

ランダムに行を抽出する

.sample()でランダムに行を抽出します。 使う状況はデータが大きすぎるので一部でお試ししたいときなどでしょうか。

抽出する個数か割合のどちらかを指定します。

  • n:抽出する個数を指定
  • frac:元データに対する割合を指定

その他の引数として、

  • random_state:乱数シードを指定
  • replace = True:復元抽出(同じものを2回以上抽出可)をしたいとき

などが指定できます。

df.sample(n=3, random_state=1234)
# 出力:
     sepal.length  sepal.width  petal.length  petal.width     variety
91            6.1          3.0           4.6          1.4  Versicolor
63            6.1          2.9           4.7          1.4  Versicolor
103           6.3          2.9           5.6          1.8   Virginica

指定した列について最大・最小の行を抽出する

.nlargest()で最大の行を、.nsmallest()で最小の行を抽出します。 指定する引数は以下のとおり。

  • n:抽出する個数(割合は不可)
  • columns:対象の列
df.nlargest(n=3, columns="sepal.length")
# 出力:
     sepal.length  sepal.width  petal.length  petal.width    variety
131           7.9          3.8           6.4          2.0  Virginica
117           7.7          3.8           6.7          2.2  Virginica
118           7.7          2.6           6.9          2.3  Virginica

指定した列にもとづいて行を並べ替える

部分抽出ではないですが、.sort_values()を使えば同じことができるのでここで取り上げます。

.sort_values()に大小判定の基準にする変数名を渡します。昇順のときはascending=True、降順のときはascending=Falseとします。

.nlargest()sepal.lengthが大きいもの3つを取り出したいときは、

df.sort_values("sepal.length", ascending=False).head(n=3)

とすれば前述と同じ結果が得られます。

余談ですが、上記のようにメソッドを繋いで書いていけるのは便利ですね。"Method Chaining"と呼ぶみたいです。

条件に合致する行を抽出する

.query()を使います。使い方の注意は次のとおり。

  • カッコの中の条件は、全体を引用符で囲む
  • 変数名にスペースやアンダースコア以外の句読点(.,-,:,^,@など)が含まれているときはバックティック(``)で囲む
df.query("`sepal.length` > 7 & variety != 'Versicolor'")

条件を組み合わせるとき、それぞれの条件をカッコで囲む必要はないみたいですが、カッコをつけておく方が見やすい気がします。

# 出力:
     sepal.length  sepal.width  petal.length  petal.width    variety
102           7.1          3.0           5.9          2.1  Virginica
105           7.6          3.0           6.6          2.1  Virginica
107           7.3          2.9           6.3          1.8  Virginica
109           7.2          3.6           6.1          2.5  Virginica
117           7.7          3.8           6.7          2.2  Virginica
# 以下省略

条件式の中でデータフレーム外の変数を用いたい場合は@を使います。例えば、threshという名前で条件の基準を保持した場合、

thresh = 7
df.query("`sepal.length` > @thresh")

とすれば、sepal.length > 7と同じになります(出力は省略)。

.query()についてはAPIリファレンスを読みました。

pandas.pydata.org

重複している行を除く

.drop_duplicates()を使って、重複している行を除いて一意(unique)な行を抽出します。

df = pd.DataFrame(
    {"a":[10,20,10],
     "b":[40,50,40],
     "c":[70,80,70]},
    index = [1,2,3]
)

1行目と3行目が重複しています。

df.drop_duplicates()

# 出力:
    a   b   c
1  10  40  70
2  20  50  80

重複しているうち、最初に登場する1行目だけが残っています。

おわりに

今回学んだこと:

  • 条件に合致した行の抽出方法
  • Method chainingについて

チュートリアルは読んだけど取り上げなかったもの:

  • 条件式の書き方

次回:

necostat.hatenablog.jp

チュートリアル拾い読み (7):Pandas公式チュートリアル (2/7) [Python]

Rもたいして使いこなせてないのにPythonの勉強を始めてみました。色々と素晴らしいチュートリアルはありそうですが、目移りしてしまうので公式チュートリアルを拾い読みしていきます。

NumPyが終わって前回からはPandasに入っています。導入版(Intro to pandas)・簡易版(10 minutes to pandas)は物足りなず、本格版(User guideのIntro to data structures以降)は読みきれなかったので、チートシート(PDF)を中心に読んでいきます。

前回:

necostat.hatenablog.jp

今回からデータテーブルの一部の行(= 観察単位, observation)や列(= 変数, variable)、あるいは両者が交差する要素にアクセスする方法を確認していきます。

最初にPandasライブラリを読み込んで、サンプルデータを作成しておきます。

import pandas as pd
df = pd.DataFrame(
    {"c1":[10,20,30],
     "c2":[40,50,60],
     "c3":[70,80,90]},
    index = ["r1","r2","r3"]
)
# 出力:
    c1  c2  c3
r1  10  40  70
r2  20  50  80
r3  30  60  90

インデックス名と位置を区別しやすいように、前者は数字ではなく文字列にしました。

ラベル名を使ったアクセス

行の名前(インデックス名)や列の名前(変数名)で部分抽出する方法です。.loc[]を使います。

要素を抽出

.loc[]のカッコの中に、抽出したい行と列の名前を指定します。行名と列名の間は,で区切ります。

df.loc["r1","c1"]  # 出力: 10

複数の行・列を指定したいときは、[ ]を使って名前のリストとして渡します。

df.loc[["r1","r2"], ["c1","c3"]]

# 出力:
    c1  c3
r1  10  70
r2  20  80

複数の行・列は:を使って範囲として指定することができます(スライシングといいます)。ラベル名を使った指定では、開始点も終了点も含まれることに注意です。

df.loc["r1":"r2", "c2":"c3"]

# 出力:
    c2  c3
r1  40  70
r2  50  80

開始点、あるいは終了点だけを指定することもできます。両方指定しなければ「全て」ということになります。

行を抽出

列名を空欄にすれば、指定した行の全ての変数を取り出すことができます。

df.loc["r1",]

# 出力:
c1    10
c2    40
c3    70
Name: r1, dtype: int64

列名を空欄にする代わりに、スライシング(:)で開始・終了を書かないでおいても同じです。

df.loc["r1", :]  # 出力: 省略

列を抽出

行の抽出のときと違って、行名は空欄だとエラーになります。全てを抽出したい場合は:を書かなければいけません(何故なのか、その背景は分かりません)。

df.loc[:,"c2"]

# 出力:
r1    40
r2    50
r3    60

位置番号を使ったアクセス

行や列の位置を番号で指定する方法です。.iloc[]を使います。

要素を抽出

.loc[]のカッコの中に、行の番号・列の番号を指定します。Pythonでは0から数え始めることに注意!

df.iloc[0,0]  # 出力: 10

.iloc[]でスライシングを使うときは終了点を含みません。例えば、

df.iloc[0:2,0]

だと、行番号0, 1のみが抽出され、行番号2(=3行目)は含まれません。

# 出力:
r1    10
r2    20
Name: c1, dtype: int64

行・列を抽出

基本的に.loc[]と同じです。列番号は空欄に出来ますが、行番号は空欄は不可で、:を入れておく必要があります。

おわりに

今回学んだこと:

  • ラベル名と位置番号で部分抽出する方法
  • 上記の両者において、スライシングの終了点の扱いが異なる

チュートリアルは読んだけど取り上げなかったもの:

  • 部分抽出ではなく要素にアクセスする.at[], .iat[]については、同じような使い方であり.loc[], .iloc[]でも代用できると思ったので取り上げませんでした。

次回:

necostat.hatenablog.jp