pythonのpandasによる簡単な統計処理:第4回 データの抽出と日本語処理

カテゴリー名: [pandasによる簡単な統計処理

2017/12/02

 次回、身長と体重の間に相関がみられるかをチェックしようとおもいますが、
前提として、身長と体重の両方ともそろっている人
(欠損値がない人)を抽出しなければなりません。

 そこで、DataFrameからデータ抽出する方法をいくつか確認します。

 関連して、dplython というライブラリの使い方も取り上げます。

 当Webページで紹介するスクリプトや素材データ一式は、
pandas04.zip という圧縮ファイルに同梱しておきます。

 素材データは、「第1回」〜「第3回」と同じ
pt_source.xls です。

 「ID、性別、身長、体重」の4列からなる 400人分のデータです。

    


《このページの目次》


    

はじめに

 pythonをめぐる環境は、基本的に「第3回」までと同じですが、
dplython というライブラリが加わります。

    

 DataFrame から特定のデータを取り出すのに
query() の利用も試みます。

 ただ、このメソッドは、python2の場合に列ラベルが日本語だと
うまく使えないようです。

 python3では日本語を使える範囲が広がっているものの、万全とはいえません。

 そこで、半分だけ日本語対応した query2() を設けてみます。

 「日本語にこだわる」といいながら、完全に日本語対応させる力量がないので
ちょっと妥協します。

目次に戻る


1. 真偽値からなるSeriesを利用したデータ抽出

 400人のデータのうち、身長と体重の両方ともそろっているのは 388人です。

 その 388人の抽出方法をみます。

    

(1) 真偽値からなるSeriesの生成

 Excelファイルを読み込んだ結果が変数 dtf に代入されているとします。

 dtf は DataFrame です。

 ser = dtf[u'身長'] とすれば、
変数 ser に身長のデータ(数値)が入ります。

 400人分のデータですが、欠損値が含まれています。

 ここで notnull() を適用して
ser = dtf[u'身長'].notnull() とすればどうなるかというと、
ser が True または False を要素とする series になります。

 要素の個数は 400個です。

 身長のデータのうち、欠損値でなければ True になり、
欠損値だと False になります。

 この真偽値からなる ser が手に入れば、
身長が欠損値でない人を抽出できます。

ser = dtf[u'身長'].notnull()
new_dtf = dtf[ser]

 上のようにすると、True に当たる行が抽出されて、
変数 new_dtf には身長が欠損していない 392人のデータが入ります。

 new_dtf は、「身長」だけのデータフレームではなく、
「ID、性別、身長、体重」の4列からなっています。

目次に戻る


(2) 真偽値からなる series の論理積・論理和

 真偽値からなる series が二つあるとき(要素個数は同じ)、
ser = ser1 & ser2 とか ser = ser1 | ser2 のようにして
論理積、あるいは論理和を得ることができます。

 各要素ごとに「積」 「和」を計算します。

 結果を受け取った変数 ser の要素個数は、ser1, ser2 と同じです。

 身長と体重の両方ともそろっているデータを抽出するなら次のとおり。

ser1 = dtf[u'身長'].notnull()
ser2 = dtf[u'体重'].notnull()
new_dtf = dtf[ser1 & ser2]

 変数 new_dtf には 388人分のデータが入ります。

 わざわざ3行で書かなくても、下の1行でも同じ結果を得られます。

new_dtf = dtf[dtf[u'身長'].notnull() & dtf[u'体重'].notnull()]

    

 ここで、身長と体重のデータがそろっている人を抽出するための
pd01.py を掲げます。

# pd01.py (coding: cp932)
import os, sys
import pandas as pd
import xlwt

from platform import python_version
if int(python_version()[0]) < 3:  # python ver 2 ??
    reload(sys)
    sys.setdefaultencoding('cp932')  # ftHgR[h?X

xls_file = "pt_source.xls"
dtf = pd.read_excel(xls_file)
new_dtf = dtf[dtf[u'身長'].notnull() & dtf[u'体重'].notnull()]
new_dtf.to_excel("pd01.xls", index=False)

目次に戻る


(3) 真偽値の反転

 ser が真偽値からなる series である場合、
ser = ~ser として True, False を反転させることができます。

 そして、notnull() の逆の意味合いを持つ isnull() があります。

 ということで、次のような書き方もできます。

new_dtf = dtf[~dtf[u'身長'].isnull() & ~dtf[u'体重'].isnull()]

 変数 new_dtf には身長と体重の両方がそろっている 388人のデータが入ります。

    

 なお、私が使っている pandas よりも新しいバージョン
ver 0.21.0 のドキュメントでは
notnull でなく notna が、isnull でなく isna が紹介されています。

 新しいバージョンを使うときは notna, isna を使う方がいいのかもしれません。

 私が使っている ver 0.20.3 では notna, isna が使えないようですが……

目次に戻る


(4) いろいろな条件指定

 身長が 170以上の人を抽出したいときは次のようにします。

    ser = dtf[u'身長'] >= 170.0
new_dtf = dtf[ser]

 上記は new_dtf = dtf[dtf[u'身長'] >= 170.0] とも書けます。

 性別が女性の人を抽出する場合は下のとおり。

new_dtf = dtf[dtf[u'性別'] == u'女性']

 身長が 170以上の女性なら

new_dtf = dtf[(dtf[u'身長'] >= 170.0) & (dtf[u'性別'] == u'女性')]

 条件式を でつなぐ場合は、
それぞれの条件式を括弧でくくらないとエラーになるようです。

 データフレーム名の dtf が繰り返し出てくるのが少々煩わしいですが、
400個もある要素を一つずつチェックすることなく、簡単に記述できます。

目次に戻る


(5) 部分的な文字を手掛かりにした抽出

 素材データの「ID」の列は、先頭の1文字が半角英字で、それに数字が続きます。

 「C3」とか「W11」といった具合です。

 先頭のアルファベットは C, H, K, R, W の5つあります。

 仮に、これが地域を示すとすれば、400人を地域別に分類できます。

 変数 s に文字列が代入されているとき、その第1文字目は
s[0] で参照します。

 そのことを利用すれば、「地域」の列を次のようにして新設できます。

area = list()
for s in dtf[u'ID']:
    area.append(s[0])
dtf[u'地域'] = area

 上記は、series の apply() と、名なしの簡易関数 lambda を使うと

dtf[u'地域'] = dtf[u'ID'].apply(lambda s: s[0])

 上の1行で実現できます。

 こうしておけば、new_dtf = dtf[dtf[u'地域'] == 'H'] のようにして
特定の地域を抽出できるほか、groupby() で地域別の分類も用意です。

    

 分類する必要はなく、地域 R だけを抽出できればいいという場合は
series の str.contains() を使うことも可能です。

new_dtf = dtf[dtf[u'ID'].str.contains('^R')]

 上のようにすれば、new_dtf に地域Rの人のデータだけがセットされます。

 str.contains() は、seriesの各要素について
引数で指定したパターン文字列に マッチするか否かで True, False のどちらかの値を返します。

 引数は、正規表現として解釈されるようです。

 str.contains('^R', case=False) のように caseオプションを指定すると、
半角アルファベットの大文字・小文字を区別しません。

 正規表現を使える点が魅力です。

目次に戻る


(6) python2 と python3 の全角文字の扱いの相違

 列ラベルが半角英字の場合、

 「ID、性別、身長、体重」 → 「ID, gender, height, weight」

 dtf['height']dtf.height と書くことができます。

new_dtf = dtf[dtf.height.notnull() & dtf.weight.notnull()]

 上記は、身長と体重の両方がそろっている人の抽出です。

 python2, python3 とも同じ書き方ができます。

    

 では、列ラベルが日本語だとどうなるかというと……

 python2 では dtf.身長 という書き方ができません。

 それに対して python3 ではその記述が可能です。

 python3 の場合は下のように書くことができます。

new_dtf = dtf[dtf.身長.notnull() & dtf.体重.notnull()]

 ただ、dtf.身長, dtf.体重 は大丈夫なのですが、
dtf.ID はダメです。理由は分かりません。

 dtf['ID'] と書けばOKです。

 全角の「ID」のエラー発生について、
スクリプトの文字コードを cp932 から utf-8 に変更して試してみましたが、
それでもダメでした。

 この制約は、この後で述べる query() にも当てはまります。

new_dtf = dtf.query('身長 >= 170')

 上の記述はOKですが、下の記述はダメです。

new_dtf = dtf.query('ID == "C1"')

 ともあれ、python3 でも全角文字を使うときは、注意が必要ということです。

目次に戻る


2. queryメソッドによるデータ抽出

 pandas の DataFrame には query() というメソッドがあります。

 DataFrame の列ラベルが半角英字であれば、たとえば下のような記述が可能です。

new_dtf = dtf.query(height >= 170.0)

 身長が 170以上の人の抽出です。

 簡単に書けて、読みやすくもあります。

    

(1) 半分だけ日本語対応

 queryメソッドは便利ですが、残念なことに
python2 では列ラベルが日本語だとエラーになります。

 python3 の場合も、全角文字が使えないケースがたまにあります。

new_dtf = dtf.query('身長 >= 170.0')  # ← python2ではエラー

 ということで、ちょっと工夫してみました。

 一時的な列ラベルの変更です。

    

 一時的に列ラベルを日本語から英字に変更し、
queryを実行した後で元に戻す方法を考えました。

 具体的には次のとおり。

asc_col = ['ID', 'gender', 'height', 'weight']  # 英字の列ラベル
org_col = list(dtf.columns)  # もともとの列ラベルを記録
undo_dict = dict(zip(asc_col, org_col))  # 列ラベル復元用の辞書
dtf.columns = asc_col  # 列ラベルを英字に変更
new_dtf = dtf.query('height >= 170.0')
new_dtf = new_dtf.rename(columns = undo_dict)
dtf.columns = org_col  # 日本語列ラベルに戻す

 ただ、毎回 上記のような処理手続きを書くのは大変です。

 そこで、事前・事後の列ラベル変更の処理を組み込んだ関数
query2() を設けて、pandas.DataFrame に組み込む方法を考えました。

 それについて、次の項で述べます。

目次に戻る


(2) query2をDataFrameクラスに組み込む

 列ラベルを英字に変更する事前処理と、
列ラベルを日本語に戻す事後処理を組み込んだ
query2() は、次のように呼び出します。

asc_col = ['ID', 'gender', 'height', 'weight']
new_dtf = dtf.query2(asc_col, 'height >= 170.0')

 上のようにすると、変数new_dtf には身長が 170以上の人のデータが入ります。

 変数 dtf, new_dtf の両方とも、列ラベルは日本語です。

    

◇ query2 の中身

 query2() は、pdex.py というファイルに書き入れました。

 その具体的な記述は下のとおり。

 クラスに組み込むので、オブジェクト自身を示す self が出てきます。

def query2(self, asc_col, x, inplace=False, **kwargs):
    org_col = list(self.columns)
    if len(org_col) != len(asc_col):
        sys.stderr.write("Error in 'query2': 1st parameter(%s) "
          "is undue length!\n" % asc_col)
        return False
    undo_dict = dict(zip(asc_col, org_col))
    self.columns = asc_col
    if inplace == True:
        self.query(x, inplace=True, **kwargs)
        self.rename(columns = undo_dict, inplace=True)
        res = None
    else:
        res = self.query(x, inplace=False, **kwargs)
        if res.__class__.__name__ == 'DataFrame':
            res = res.rename(columns = undo_dict)
        elif res.__class__.__name__ == 'Series' and \
             res.name in undo_dict.keys():
            res.name = undo_dict[res.name]
        self.columns = org_col
    return res

    

 dtf.query2(……) のように呼び出した場合は、
self が dtf を指し示します。

 本来の query() は、戻り値として DataFrame を返すようですが、
念のため、series が返された場合に備えて
query2 では、seriesの name を英字から日本語に変更するようにもしました。

 inplaceオプションが True だと、オブジェクト自身が変更されて、
戻り値は None になります。

 query2 でもその仕様は同じです。

    

◇ query2を組み込む際の留意点

 別ファイルの pdex.py に書かれている関数は、
一つのモジュール(一つの独立ワールド)に所属します。

 それを他のファイル(スクリプト)からアクセスする場合、
モジュール名に続けてピリオド記号を書き、
その後に関数名を記述します。

 import pdex as px とすれば、
pdex.py をpxというモジュール名で取り込むことになります。

 取り込んだ後で pd.DataFrame.query2 = px.query2 とすれば、
pandas.DataFrame に query2 を組み込むことができます。

 しかし、今回はこの方法だと不十分です。

 query() が呼び出し元の変数を参照するため、
別のモジュールの関数だと対応しきれないからです(scopeが異なる)。

 仕方ないので、関数のソースコードを取得して、
当該ファイル内にそれが書かれているかのような状態にします。

 具体的には次のとおり(該当箇所のみ)。

import pandas as pd
import inspect
import pdex as px  # pdex.py を px という名前で取り込む
exec( inspect.getsource(px.query2) )  # query2のソースコードを取り込む
pd.DataFrame.query2 = query2  # query2をDataFrameクラスに組み込む

 inspect.getsource() は、ソースコードを文字列として取得するもの、
 exec() は、与えられた文字列をスクリプトとして実行するものです。

 もっとスマートに scope の食い違いを処理する方法があるはずですが、
分からなかったので上記の方法を採りました。

    

 なお、pdex.py には add_query2() を設けてあり、
次の2行で query2 を組み込むことができます。

import pdex as px
exec(px.add_query2())

 もし pandas を ‘pdxx’ という名前で取り込んでいるなら
exec(px.add_query2('pdxx')) のようにします。

    

◇ query2を利用するスクリプトの例

 pdex.py を取り込んで、query2() を利用するスクリプト pd02.py を掲げます。

 1# pd02.py (coding: cp932)
 2import os, sys
 3import pandas as pd
 4import xlwt
 5import inspect
 6import pdex as px  # pdex.py を px という名前で取り込む
 7exec( inspect.getsource(px.query2) )  # query2のソースコードを取り込む
 8pd.DataFrame.query2 = query2  # query2をDataFrameクラスに組み込む
 9
10from platform import python_version
11if int(python_version()[0]) < 3:  # python ver 2 ??
12    reload(sys)
13    sys.setdefaultencoding('cp932')  # ftHgR[h?X
14
15xls_file = "pt_source.xls"
16dtf = pd.read_excel(xls_file)
17
18asc_col = ['ID', 'gender', 'height', 'weight']  # 英字の列ラベル
19new_dtf = dtf.query2(asc_col, 'height >= 170.0')
20new_dtf.to_excel("pd02.xls", index=False)

目次に戻る


(3) query における条件式あれこれ

 query() には、引数として文字列を渡します。

 データ抽出のための条件式を文字列で渡す訳ですが、
既に値が代入されている変数を条件式の中で参照する場合は、
@xx のようにアットマークを前置します。

 たとえば下のとおり。

xx = 170.0
dtf.query('height > @xx')

(この仕組みがあるので、query2 を組み込むときに
 scope の食い違いを避ける必要がある訳です。)

    

◇ 欠損値を取り除く

 まず、今回の本丸である欠損値を取り除く方法です。

new_dtf1 = dtf.query('height == height')
new_dtf2 = dtf.query('height == height & weight == weight')

 new_dtf1 には身長が欠損していない人のデータが入ります。

 new_dtf2 の方は、身長と体重の両方とも欠損していない人です。

 'height == height' は、常に True になりそうな気がしますが、
欠損値 NaN の場合は True になりません。

 そのことを利用した「裏技」のような記述です。

 でも、書きやすいので多用してしまいそう……

    

 欠損値を取り除く方法をもう一つ。こちらの方が素直かも。

nan = np.NaN
new_dtf1 = dtf.query('height not in [@nan]')
new_dtf2 = dtf.query('height not in [@nan] & weight not in [@nan]')

 上のように書いても欠損値を取り除くことができます。

 条件式の中に直接 np.NaN を書くことはできないようです。

 欠損値を取り除くサンプルスクリプト pd03.py, pd03_2.py が
zip圧縮ファイルに入っています。

    

◇ 身長が平均値以上の女性の抽出

 query を利用する場合、列ラベルが日本語だと適正に処理されませんが、
「女性」 「男性」などのデータそのものは日本語でも大丈夫です。

 身長が平均値以上の女性の抽出は次のとおり(pd04.py の抜粋)。

asc_col = ['ID', 'gender', 'height', 'weight']  # 英字の列ラベル
female = dtf.query2(asc_col, 'gender == u"女性"')  # 女性の抽出
mean = female[u'身長'].mean()  # 女性の平均身長
new_dtf = female.query2(asc_col, 'height >= @mean')

writer = pd.ExcelWriter("pd04.xls")
new_dtf.to_excel(writer, index=False, startrow=1, startcol=0)
worksheet = writer.sheets['Sheet1']
worksheet.write(0, 0,  # 引数は row, column の順番
  u"女性の平均身長(%.1f)より背の高い女性" % mean)
writer.save()

 Excelファイルの出力部分が長くなっていますが、
1行目にタイトルみたいなものを書き出し、
2行目以降にデータフレームの中身を書き出しています。

    

◇ 真偽値の series を併用

 「ID」の先頭文字が ‘C’ であって、
身長と体重の両方ともそろっている人を抽出します。

 series の str.contains() を使えば
「ID」の先頭文字が ‘C’ の人を抽出できます。

 ser = dtf[u'ID'].str.contains('^C') とすれば
変数 ser に真偽値からなる series がセットされます。

 この ser を query2() の引数に盛り込むことができます。

 以下、pd05.py の抜粋を掲げます。

asc_col = ['ID', 'gender', 'height', 'weight']  # 英字の列ラベル
ser = dtf[u'ID'].str.contains('^C')  # 先頭文字が C のデータ
qstr = '@ser & height == height & weight == weight'
new_dtf = dtf.query2(asc_col, qstr)

writer = pd.ExcelWriter("pd05.xls")
new_dtf.to_excel(writer, index=False, startrow=1, startcol=0)
worksheet = writer.sheets['Sheet1']
worksheet.write(0, 0,  # 引数は row, column の順番
  u"地域Cの人で,身長と体重に欠損値がない人")
writer.save()

目次に戻る


3. evalを試してみる

 pandas.DataFrame の evalメソッドを利用する前提として、
to_asc, to_org という関数を紹介しておきます。

(1) 半角に変更する to_asc、全角に復元する to_org

 pdex.py には to_asc()to_org() という関数が書かれています。

 前者は列ラベルをアスキー文字に変更するもの、
後者は列ラベルを復元するものです。

 たとえば次のようにします。

asc_col = ['ID', 'gender', 'height', 'weight']  # 英字の列ラベル
px.to_asc(dtf, asc_col)
……(何か処理)
px.to_org(dtf)

 to_asc と to_org の間の処理では、半角英字の列ラベルを使えます。

 なお、to_asc, to_org にはそれぞれ do, undo という別名を割り当てています。

目次に戻る


(2) evalでBMIを算出

 pandas.DataFrame には eval() というメソッドがあります。

 ある列と別の列を材料にして計算処理するようなときに利用します。

 データの抽出とはちょっと違いますが、
列ラベルを半角英字にすると使いやすくなるので取り上げます。

 身長と体重が分かれば BMI(体格指数)を計算できます。

 BMIが 18.5 を下回ると「ちょっと痩せすぎ?」みたいです。

 BMI=体重÷(身長×身長) ← 身長はメートル

 上の式で BMI を算出します。

    

 eval() の利用例 pd06.py から該当箇所を抜粋すると下のとおり。

asc_col = ['ID', 'gender', 'height', 'weight']  # 英字の列ラベル
px.to_asc(dtf, asc_col)  # 列ラベルをasciiに
dtf2 = dtf.query('height == height & weight == weight')  # 欠損値の除去
dtf2 = dtf2.reset_index(drop=True)  # 行ラベルを付け替え
ser = dtf2.eval('weight / (height / 100.0)**2')  # BMIを算出
dtf2['BMI'] = ser
dtf3 = dtf2.query('BMI < 18.5')  # 痩せすぎの人を抽出
dtf3 = dtf3.reset_index(drop=True)  # 行ラベルを付け替え
px.to_org(dtf, dtf2, dtf3)

    

 今回のケースでは、eval が series を返します。

 その series を「BMI」という列ラベルで dtf2 に登録しています。

 変数 dtf3 には、BMI から「痩せすぎ」の人を抽出して代入しました。

 処理の過程で dtf から dtf2, dtf3 を生成した訳ですが、
三つとも列ラベルがアスキー文字です。

 そこで、px.to_org(dtf, dtf2, dtf3) として
三つの DataFrame の列ラベルを日本語に復元します。

 復元のときは asc_col を渡す必要がありません。

 モジュール px の側に保持されているからです。

 文字されているのを初期化したいなら px.clear_vars() とします。

 初期化すると、to_org() は機能しなくなります。

 なお、保持されている英字の列ラベルは px.asc_col で参照できます。

目次に戻る


(3) 警告メッセージ出力への対応

 dtf2, dtf3 について、reset_index() でその行ラベルを付け替えています。

 この処理は、なくてもかまいません。Excelに行ラベルは出力しないので。

 ただ、この処理を外すと下のような警告メッセージが出ます。

pd06.py:22: SettingWithCopyWarning: 
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: http://pandas.pydata.org/pandas-docs/stable/indexing.html#indexing-view-versus-copy
  dtf2['BMI'] = ser
(以下、省略)

 警告が出ても実行に支障はありません。

 でも、気分がよくないので書き入れました。

 実のところ、今回のケースで、なぜ警告が出るのか
私にはよく分かりません。

 reset_index() を適用すればいいということでもなさそうですが……

 実行に支障のない警告メッセージの出力を抑制する方法があるようですが、
今回は「そこまでする必要もないかな」ということでパスしました。

目次に戻る


4. dplythonを試してみる

 dplython というライブラリがあります。

 pandas.DataFrame を機能拡張するライブラリです。

 統計解析ソフトRには dplyr というライブラリがありますが、
それと類似の機能を提供します。

 データフレームをSQLで操作できるような感じ(?)
というのが特徴のようです。

 また、下のようなリダイレクションに似た記述ができて、おもしろいです。

new_dpl = dpl >> select(X.gender, X.height) >> head(5)

 このライブラリを使う場合、
X.height といった記述形式を用いずに
X['height'] という書き方を使えば、
列ラベルが全角文字でも支障ありません。

 でも、列ラベルがアスキー文字だと記述が簡単になるので
pdex.py の to_asc, to_org を利用します。

    

(1) dplythonの簡単な利用例

 dplythonのインストールから順を追手説明します。

◇ インストール

 dplython のインストールは次のようにして行えます。

python -m pip install dplython --upgrade

 私のところでは ver 0.0.7 が入っています。

    

◇ 使い始める前の「おまじない」

 使い始める前に、下の3行を記述します。

from dplython import (DplyFrame, X, diamonds, select, sift,
  sample_n, sample_frac, head, arrange, mutate, group_by,
  summarize, DelayFunction)

 diamonds はサンプルデータだとおもいます。

 記述しなくてもいいかもしれませんが念のため書きました。

    

◇ サンプルスクリプト pd07.py

 まず、スクリプトの実行結果ですが、下のようになります。

 もともとのデータフレームから「性別、身長」の2列を抜き出し、
かつ、先頭の5行だけを取り出します。

性別 身長
女性 159.1
男性 163.8
女性 162.7
女性 157
男性 167.3

 スクリプト全体を下に掲げますが、
肝になるのは次の1行です。

new_dpl = dpl >> select(X.gender, X.height) >> head(5)

 以下、pd07.py です。

 1# pd07.py (coding: cp932)
 2import os, sys
 3import pandas as pd
 4import numpy as np
 5import xlwt
 6import pdex as px
 7from dplython import (DplyFrame, X, diamonds, select, sift,
 8  sample_n, sample_frac, head, arrange, mutate, group_by,
 9  summarize, DelayFunction)
10
11from platform import python_version
12if int(python_version()[0]) < 3:  # python ver 2 ??
13    reload(sys)
14    sys.setdefaultencoding('cp932')  # ftHgR[h?X
15
16xls_file = "pt_source.xls"
17dtf = pd.read_excel(xls_file)
18dpl = DplyFrame(dtf)  # DataFrame → DplyFrame
19asc_col = ['ID', 'gender', 'height', 'weight']  # 英字の列ラベル
20px.do(dpl, asc_col)  # 列ラベルをasciiに
21new_dpl = dpl >> select(X.gender, X.height) >> head(5)
22px.undo(new_dpl)
23new_dpl.to_excel("pd07.xls", index=False)

    

◇ DplyFrameオブジェクトおよびシンボルとしてのX

 変数 dtf は、おなじみの pandas.DataFrame オブジェクトです。

 dpl = DplyFrame(dtf) とすることによって
dplython の DplyFrame オブジェクトに変換しています。

 dpl は、基本的に dtf と同じように使えますが、
>> を適用できるなどの拡張が施されています。

 select は、特定の列を抜き出すもの、
 head は、先頭から指定行数を抜き出すものです。

 select(X.gender, X.height) の大文字 X は、
DplyFrame オブジェクトを参照するためのある種のシンボルです。

 X.height の代わりに X['height'] と書いてもOKです。

 ただし、X が DplyFrame オブジェクトそのものを指し示すわけではないようで、
オブジェクト全体は、アンダーライン記号を使って X._ と記述します。

 行と列の入れ替え(縦・横の入れ替え)は X._.T と記述する旨
ドキュメントに書かれています。

目次に戻る


(2) フィルタリングのsift、列新設のmutate

 sift は「ふるいにかける」の意味、
 mutate は「変化させる」の意味のようです。

 sift(X.height >= 170.0) だと、身長が 170以上の人の抽出になり、
 mutate(BMI = X.weight / (X.height/100.0)**2) であれば
新たに BMI の列を設けることになります。

 身長と体重の両方のデータがそろっている人を抽出し、
BMI の列を新設した後で、
標準体重の人を抜き出す処理は次のとおり。

new_dpl = (dpl >>
  sift(X.height == X.height, X.weight == X.weight) >>  # 欠損値のない人
  mutate(BMI = X.weight / (X.height/100.0)**2) >>  # BMIの列を新設
  sift(X.BMI >= 18.5, X.BMI < 25.0)  # 標準体重の人
)

 >> を同じ行の中に何度も書くと読みにくくなるので、
全体を括弧でくくって複数行にするのが一般的なようです。

    

◇ siftの条件式の書き方

 sift() ではカンマで区切って条件式を複数記述できます。

 複数の条件式は、 でつながれて評価されます。

 論理和の でつなぎたいときは、
sift((X.BMI < 18.5) | (X.BMI >= 25.0)) のように書きます。

 それぞれの条件式を括弧でくくるようです。

 なお、X.height というのは pandas.Series オブジェクトです。

 なので、notnull() とか str.contains() を適用できます。

sift(X.height.notnull(), X.weight.notnull())

 上のような記述が可能です。

    

◇ mutateと警告メッセージ

 mutate() を利用する際ですが、
今回、python2 では警告メッセージが出力されてしまいます。
(おそらく BMI の算出式を直接 書き入れているため?)

 python3 だと出力されません。

 また、python2 でも、mutateを使えば必ず警告メッセージが出る
という訳ではありません。

 で、警告メッセージが出ても実行に支障がある訳ではありませんが、
なんか気分がよくないので出力を抑制することにしました。

 次の2行を書き加えると抑制できます。

import warnings
warnings.filterwarnings('ignore')

    

◇ サンプルスクリプト pd08.py

 今回、列ラベルを英字にする際、大着をして頭文字だけにしてみました。

 「ID, gender, height, weight」 → 「I, g, h, w」

 1# pd08.py (coding: cp932)
 2import os, sys
 3import pandas as pd
 4import numpy as np
 5import xlwt
 6import pdex as px
 7from dplython import (DplyFrame, X, diamonds, select, sift,
 8  sample_n, sample_frac, head, arrange, mutate, group_by,
 9  summarize, DelayFunction)
10
11from platform import python_version
12if int(python_version()[0]) < 3:  # python ver 2 ??
13    reload(sys)
14    sys.setdefaultencoding('cp932')  # ftHgR[h?X
15    import warnings
16    warnings.filterwarnings('ignore')  # 警告の出力を抑制
17
18xls_file = "pt_source.xls"
19dtf = pd.read_excel(xls_file)
20dpl = DplyFrame(dtf)  # DataFrame → DplyFrame
21
22px.do(dpl, list('Ighw'))  # 列ラベルを英単語の頭文字に
23new_dpl = (dpl >>
24  sift(X.h == X.h, X.w == X.w) >>  # 身長・体重の欠損値を除去
25  mutate(BMI = X.w / (X.h/100.0)**2) >>  # BMIの列を新設
26  sift(X.BMI >= 18.5, X.BMI < 25.0)  # 標準体重の人を抽出
27)
28px.undo(new_dpl)
29
30writer = pd.ExcelWriter("pd08.xls")
31new_dpl.to_excel(writer, index=False, startrow=1, startcol=0)
32worksheet = writer.sheets['Sheet1']
33worksheet.write(0, 0,  # 引数は row, column の順番
34  u"標準体重の人(%d人)" % len(new_dpl))
35writer.save()

目次に戻る


(3) 分類のgroup_by、要約のsummarize

 group_by は、グループに分類するためのメソッドです。

 group_by(X.gender) とすれば男女別に分類することになります。

 summarize は、データの要約を設定するメソッドです。

 summarize(avg_height = X.height.mean()) とすれば
平均身長の設定です。

 avg_height は列ラベルになります。

 実は、この二つのメソッドの詳しい挙動は分かりません。

 「こういうスクリプトを書けば、こうなる」ということしか分かりません。

 なので、サンプルスクリプト pd09.py の抜粋を掲げてお茶を濁します。

    

 まず、スクリプトの実行結果は次のとおり。

性別 平均身長 平均体重
女性 160.1 55.5
男性 166.7 66.4

 次に、スクリプトです。

    (前略)
dpl = DplyFrame(dtf)  # DataFrame → DplyFrame
asc_col = ['ID', 'gender', 'height', 'weight']  # 英字の列ラベル
px.do(dpl, asc_col)  # 列ラベルをasciiに
new_dpl = (dpl >>
  group_by(X.gender) >>  # 性別による分類
  summarize(avg_height = X.height.mean(),  # 平均身長の列を設定
    avg_weight = X.weight.mean())  # 平均体重の列を設定
)
px.undo_dict.update({'avg_height': u'平均身長',
  'avg_weight': u'平均体重'})  # 復元用の列ラベルを追加
px.undo(new_dpl)
new_dpl.to_excel("pd09.xls", index=False, float_format='%.1f')

    

 group_by(X.gender) は男女別の分類ですが、
これにより最終的な DplyFrame に「性別」という列が設けられる
ということのようです。

 summarize(……) では平均身長と平均体重を設定していますが、
これも最終的な DplyFrame に「平均身長」と「平均体重」の列を設ける
という作用があるようです。

 そもそもは 400人分のデータが保持されていた DplyFrame ですが、
summarize を適用すると2行×3列の小さいフレームになってしまいました。

目次に戻る


(4) ソートのarrange、無作為抽出のsample_n と sample_frac

 行の並べ替え(ソート)には arrange を使います。

 arrange(X.height) とすれば、身長の小さい値の順番でソートされます。

new_dpl = dpl >> arrange(X.height)

 上のようにすると、new_dpl にはソートされた DplyFrame が代入されます。

 身長の大きい順でソートするなら arrange(-X.height) と書きます。

    

 sample_n, sample_frac は無作為抽出を行うメソッドです。

 sample_n(5) とすると、5行分が無作為抽出されます。

 行の順番はランダムです。

 sample_frac(0.1) だと、10分の1が抽出されます。

 400行の DplyFrame からは 40行の DplyFrame が生成されます。

 sample_frac(0.5) なら半分の 200行の抽出です。

 記述例を掲げると下のとおり。

new_dpl = (dpl >>
  sample_n(200) >>
  sample_frac(0.1) >>
  arrange(-X.height)
)

    

 ここで、group_by の作用を確認しておきたいとおもいます。

new_dpl = (dpl >>
  group_by(X.gender) >>
  sample_n(5)
)

 上のようにすると、男女それぞれ5人ずつが抽出され、
全体では 10人が抽出されます。

 sample_n, sample_frac は、統計処理を実験的に試してみるのに便利な機能です。

目次に戻る


(5) 特有のデコレータ DelayFunction

 自作した関数を >> の連鎖の中で使いたい場合、
DelayFunction を利用するようです。

 python のデコレータといわれる仕組みにかかわる事柄です。

 仮に test() という関数を自作したとして、
 test = DelayFunction(test) という1行を書くと、
test関数が >> の連鎖の中で使えるように変身する
(機能が付け加えられる)ということのようです。

 デコレータですから「返信」というよりは「飾りつけ」でしょうか。

    

◇ 戻り値として series を返す関数の場合

 サンプル pd01.py を書いてみました。

 先述の pd08.py と行うことは同じです。

 身長と体重を材料にして BMI を算出する関数 BMIcalc() を設けます。

 BMIcalc 関数は、BMI が記録された series を返します。

 今回は python2 でも警告メッセージが出力されません。

 BMIcalc の関数部分を掲げます。

def BMIcalc(height, weight):  # BMIの算出
    index = height.index
    ser = pd.Series(w/(h/100)**2 for h, w in zip(height, weight))
    ser.index = index
    return ser
BMIcalc = DelayFunction(BMIcalc)

 最後の BMIcalc = DelayFunction(BMIcalc) は、
BMIcalc を >> の連鎖の中で使えるようにするための記述です。

 この BMIcalc = DelayFunction(BMIcalc) という1行を書く代わりに
@DelayFunction という行を関数定義の直前に書くというのでもいいようです。

 というか、その方が一般的なのかもしれません。

@DelayFunction
def BMIcalc(height, weight):
    …………

 上のような形です。

 一方、サンプルの pd10.py の >> 連鎖部分は下のとおり。

new_dpl = (dpl >>
  sift(X.height == X.height, X.weight == X.weight) >>  # 欠損値の除去
  mutate(BMI = BMIcalc(X.height, X.weight)) >>  # BMIの列を新設
  sift(X.BMI >= 25.0, X.BMI < 30.0)  # やや肥満の人を抽出
)

 BMIcalc は series を返すので、
new_obj = dpl >> BMIcalc(X.height, X.weight) とすれば、
new_obj は BMI を記録した series になります。

    

◇ 戻り値として DplyFrame を返す関数の場合

 BMIcalc関数は、BMI を記録した series を返すので
mutate() の括弧内で用いました。

 今度は DplyFrame を返す関数 BMIappend を設けて
mutate を使わずに済ませようとおもいます。

 zip圧縮ファイルに入っている pd11.py がそれです。

 BMIappend関数は次のとおり。

@DelayFunction
def BMIappend(dobj):  # BMIの列を追加
    ser = pd.Series(w/(h/100)**2 for h, w in zip(dobj.height, dobj.weight))
    ser.index = dobj.index
    dobj['BMI'] = ser
    return dobj

 BMIappend を呼び出す側は下のようになります。

new_dpl = (dpl >>
  sift(X.height == X.height, X.weight == X.weight) >>  # 欠損値の除去
  BMIappend(X._) >>  # BMIの列を新設
  sift(X.BMI >= 25.0, X.BMI < 30.0)  # やや肥満の人を抽出
)

 BMIappend関数にパラメータとして X._ を渡しています。

 X._ は、DplyFrame オブジェクト全体を指し示すものです。

 でも、>> の連鎖の中では暗黙のうちに
DplyFrame オブジェクトが引き渡されているはずなので、
X._ をわざわざ引き渡すというのは
どうもぶすいな漢字です。

 悔しいので引数無しで済ませる方法を調べたり試したりしましたが、
よく分かりませんでした。

    

◇ 補足

 BMIcalc = DelayFunction(BMIcalc) とすれば
>> の連鎖の中で BMIcalc を使えるようになるわけですが、
bcalc = DelayFunction(BMIcalc) のように異なる名前にすると、
>> の連鎖の中では bcalc の方が使えるようになります。

 もし BMIcalc を >> の連鎖の外でも使おうという場合は、
異なる名前にしておくのが無難かもしれません。

 なお、DelayFunction() の引数には様々なオブジェクトを渡せるようです。

 グラフなどを描くためのクラスに ggplot というのがありますが、
ggplot = DelayFunction(ggplot) とすれば、
ggplotに付随する各種メソッドが >> の連鎖の中で使えるようになるようです。

 私はまだ試していませんが……

    

 今回は これで終了です。

Copyright (C) T. Yoshiizumi, 2017 All rights reserved.