この記事では,調査によって得られる質問表からのデータのクリーニングをPandasのメソッドチェーン(method chaining, 以下chaining)を用いるとコードの可読性が飛躍的に向上するお話と,クリーニングの際の現在の僕のベストプラクティスを紹介する.
今回触れる内容は,
- 欠損値が絡んだクロス集計.
- 欠損値が絡んだ連続値からカテゴリーへの変換.
- 欠損値が絡んだスケールのカテゴリーへの変換.
である.
Method chainingに関してはこのサイトが詳しい.この記事は実践編の立ち位置.
・Pythonでメソッドチェーンを改行して書く note.nkmk.me
データの準備
データはKaggleより,
・Depression and Academic performance of students
を用いる.全然有名なデータではないし,説明も少ないので例として用いるだけだ.
今回用いるコードは,https://github.com/akitoshiblog/pandas_method_chaining_cleaning からデータと共にダウンロード可能だ.
データを上のサイトからダウンロードしたら以下のコードを回しておく.
path = "Effects of Depression Anxiety on Academic Performance among the students (respuestas).xlsx" df = pd.read_excel(path) # Insert missing values np.random.seed(1234) for col in df.columns: df.loc[df.sample(frac=0.1).index, col] = np.nan
後半部分は欠損値をこちらから意図的に行列に差し込む方法だ.欠損値込みのデータセットを作成する際に便利なコードだ.
クロス集計
pd.crosstab
を用いれば良い.注意点としては欠損値は表れないのでreplaceをするか,いつも全体の数を出すように margins=True
にしておくよう心がけることである.
sex = "Gender:" age_ = "Age:" tab = pd.crosstab( df[sex].replace(np.nan,"missing"), df[age_].replace(np.nan,"missing"), margins=True) tab_per = ( pd.crosstab( df[sex].replace(np.nan,"missing"), df[age_].replace(np.nan,"missing"), margins=True, normalize="index" ).mul(100) .round(2) ) display(tab) display(tab_per)
これで数字と率が手に入る.注目すべきは率の算出の際に *100
ではなく .mul(100)
を用いることで .round(2)
までmethod chainingで繋げることが出来ることだ.また()で括ると\がいらなくなる.
連続値からカテゴリーへの変換
データに日付型が入ってしまっているので,今回はそれを欠損に変更しておく.実際の場面においては日付型が一つだけ混入しているなどはexcelの読み込みの問題が高いのでcsvに変換して,日付として読み込まれない工夫が必要である.
gpa = 'Your Last Semester GPA: ' df[gpa] = pd.to_numeric(df[gpa], errors='coerce') df[gpa].describe()
良く知られている方法は pd.cut
を用いる方法であろう.pd.cutを用いるとカテゴリー型を作成してくれるが,欠損値を違う名前に変更したいときにエラーが出る.解決方法としては型をstring型などにしてしまうことだ.
gpa_cate = "gpa_cate" labels = [">=1,<2", ">=2,<3",">=3,<=4"] df[gpa_cate] = (pd.cut(df[gpa], bins=[1,2,3,4.1], right=False, labels=labels) .astype(str) .replace("nan", "missing") .fillna("missing") ) df[gpa_cate].value_counts()
Pythonのバージョンによって.astype(str)の際のnp.nanの挙動が違うため,今回はどちらのケースでもカバー出来るように.replaceと.fillnaを用いている.
他にも2パターン同等の処理を叶える方法がある..mask
を用いる方法だ.Way1とWay2が書いてあるが比較演算子をmethodで行っているかどうかの違いである.
# Way 1 df[gpa_cate] = (df[gpa] .mask(df[gpa].isnull(),"missing") .mask(df[gpa] <= 4, labels[2]) .mask(df[gpa] < 3, labels[1]) .mask(df[gpa] < 2, labels[0]) ) # Way 2 df[gpa_cate] = (df[gpa] .mask(df[gpa].isnull(),"missing") .mask(df[gpa].le(4), labels[2]) .mask(df[gpa].lt(3), labels[1]) .mask(df[gpa].lt(2), labels[0]) ) df[gpa_cate].value_counts()
ミスが少なくなるのはどちらかと聞かれたら,実は.maskを用いる方法ではなかろうか? 確かにpd.cutは非常に多くの区分を設定する際には有用だが,今回のgpaの列に関しては,最低の2と最高の4を入れるために端の挙動について気を使う必要がある.また,欠損値処理のために.astypeや.replace, .fillnaが必要になる.
一方,maskによる方法はもっと直接的で明示的に処理が書かれる.この記法は構造化データの処理に特化したSASやSPSSといった統計ソフトに近くなることも特筆すべき点である.
この可読性はmethod chainingを用いることによって得られていることに気をつけること.
スケールからカテゴリー
7個の質問(答えは1~4を取る)に関して,3,4を答えたら1点,1,2だったら0点として得られるスケールを計算すると考えよう.
まず,項目を点数に変換するプロセスを行う.3通りの記法が考えられる.今回のケースだと取り扱う値が少ない(カテゴリー的)ので,.replaceを用いるのが最もsimpleだろう.
qs = {i : df.columns[i+2] for i in range(1,7)} # Way 1 dfM = df.copy() for i in range(1,7): c = qs[i] dfM[c] = dfM[c].replace({1:0,2:0,3:1,4:1}) # Way 2 dfM = df.copy() for i in range(1,7): c = qs[i] dfM[c] = (dfM[c] .mask(dfM[c].apply(lambda x: x in [1,2]), 0) .mask(dfM[c].apply(lambda x: x in [3,4]), 1) ) # Way 3 dfM = df.copy() for i in range(1,7): c = qs[i] dfM[c] = (dfM[c] .mask(dfM[c].eq(1) | dfM[c].eq(2), 0) .mask((dfM[c] == 3) | (dfM[c] == 4) , 1) )
次にこれらの点数を合計してカテゴリーにする.質問紙から得られるデータでよくあることだが,一つの項目だけ足りていない人でも,一番上のカテゴリーだと判断出来たり,一番下のカテゴリーに入れられると判断出来たりする.今回は6つの質問からなる得点を">=4", "2-3", "<=1" の3つのカテゴリーに変換することを考えよう.
cols = [qs[i] for i in range(1,7)] sum1 = dfM[cols].sum(axis=1, skipna=False) sum2 = dfM[cols].replace(np.nan,1).sum(axis=1, skipna=False) sum3 = dfM[cols].replace(np.nan,0).sum(axis=1, skipna=False) scale = (sum1 .mask(sum1.isnull(), "missing") .mask( (sum1 >=4 ) | (sum3 >= 4), ">=4") .mask( sum1 <= 3, "2-3") .mask( (sum1 <= 1) | (sum2 <= 1), "<=1") )
方針としては,欠損を0点にしたにも関わらず一番上のグループに入れば,その人は一番上だと判断出来るし,欠損を1点にしたにも関わらず一番下のグループに入れば,その人は一番下だと判断出来る.
注目して欲しいことは,このような処理はpd.cutでは達成出来ない,もしくは煩雑になることである.ところがmaskを用いたchianingを用いれば可読性を向上したコードを書くことが可能である.
今回のケースだとこのような処理をすれば,欠損を169件からカテゴリーにした際に85件まで減らすことが出来,データを最大限活用することが出来る.
----------雑感(`・ω・´)----------
今回の記法は,以下の本によって学んだ.他にも色々なTipsが眠っているので是非流し読みしてみてほしい.
今まで書いていた自分なりのコードよりも圧倒的に分かりやすくて頭がスッキリします.
コメント