pythonのpandasによる簡単な統計処理:第1回 DataFrameへのmean,describeの適用

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

2017/10/29

 当シリーズでは、pythonのpandasを使って、
簡単な統計処理を試みます。

 日本語MS-Windowsの環境下で、Excelファイルを素材にします。
文字コードは cp932 (Shift_JISの拡張版)を前提にします。

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

    


《このページの目次》


    

はじめに

 pythonというプログラミング言語は、たまに使ってみる程度でしたが、
今更ながら pandas, scipy.stats などのライブラリを知って驚きました。

 統計解析ソフトRで処理できることの多くがpythonでも可能なようです。

 少なくとも私がやろうとすることくらいは、pythonで十分できそうです。

 ということで、pythonのpandasを試した過程を参考として記します。

    

◇ python, pandas を使う際にこだわった点

 私がこだわったのは次の2点です。

    

◇ python, pandas のバージョンと新たに導入したライブラリ

 pythonは、ver 2.7.10, 3.5.2 の2種類を使いました。

 どちらも WinPython をインストールし、
必要に応じてライブラリを更新したり、
新たにインストールしたりしました。

 Excelファイルを書き出すためのライブラリ xlwt をインストールしました。
これ、WinPythonには入っていないようです。
(XlsxWriter が入ってるようです ← お手軽ではないが高機能)

 pandas は Version: 0.20.3 を利用。

python -m pip install pandas --upgrade  [enter]
python -m pip install xlwt --upgrade  [enter]

 上のようにすると pandas, xlwt を更新または導入できます。

 --upgrade は、既に導入済みであってもバージョンアップするよ、というものです。

 ここで掲げるスクリプトは、python 2.7, 3.5 の両バージョンで
同じように動作するよう工夫したつもりです。

 「ExcelVBAとピボットテーブル」というシリーズで取り上げた処理を
〈忠実にではないにしても〉なぞる形にしました。

    

◇ 素材となるソースデータ

 ソースデータとして pt_source.xls を用います。
「ExcelVBAとピボットテーブル」で使ったのと同じもの。

 中身は下のとおり。

ID 性別 身長 体重
C3 女性 159.1 57.8
W5 男性 163.8 78.2
W11 女性 162.7 59.5
H1 女性 157 59.6

 上の形式で Sheet1 に 400人分のデータが書かれています。

 セル範囲でいえば A1:D401 にデータが書かれています。

 IDの列には空欄がありませんが、性別、身長、体重の列には少数ながら空欄があります。つまり、記載のない空白セルがあります。

目次に戻る


1. 男女別に身長と体重の平均値を算出

 最初に pythonスクリプトの全体像を掲げ、
その後、初心者の私が引っかかった点を記します。

(1) スクリプト pd01.py の全体像

 スクリプトの前に、まずは処理結果として得られる表を掲げます。

性別 平均身長 平均体重
男性 166.7 66.4
女性 160.1 55.5
記載なし 164.2 67.4

    

 次に pythonスクリプトの全体像です。

 私にとって分かりやすいとおもった形、
結果、スマートさには欠ける形になっています。

 1# pd01.py (coding: cp932)
 2import os, sys
 3import pandas as pd
 4import xlwt
 5
 6from platform import python_version
 7if int(python_version()[0]) < 3:  # python ver 2 の場合
 8    reload(sys)
 9    sys.setdefaultencoding('cp932')  # デフォルト文字コードを変更
10
11xls_file = "pt_source.xls"
12dtf = pd.read_excel(xls_file)
13dtf[u'性別'] = dtf[u'性別'].fillna(u'記載なし')  # 性別の欠損値に名前付け
14dgb = dtf.groupby(u'性別')  # 性別を手がかりに分類
15
16dtf2 = pd.DataFrame()  # からのデータフレームを用意
17dtf2[u'平均身長'] = dgb[u'身長'].mean().round(1)
18dtf2[u'平均体重'] = dgb[u'体重'].mean().round(1)
19dtf2 = dtf2.ix[[u'男性', u'女性', u'記載なし']]  # 行の順序入れ替え
20dtf2.index.name = u'性別'  # 左上端セルへの文字書き入れ
21dtf2.to_excel("pd01.xls")

 以降で、初心者の私が引っかかった点を記します。

目次に戻る


(2) 文字コード

 pythonでは、日本語のようなマルチバイト文字をスクリプト中に書き入れる場合、
u'性別' とか u"性別" のように書くのが無難なようです。

 u を付けないと、単なるバイト列と見なされて
エラー発生につながることがあります。

 cp932, Shift_JIS の場合、漢字等は1文字が2バイトで構成されますが、
2バイト目に半角英数記号がくるケースがままあります。

 2バイト目が引用符の " とか、円記号の \ だと、
単なるバイト列と見なされるとトラブルにつながります。

 そこで、「ここはマルチバイト文字だよ」というのを
pythonに理解してもらうため u を前置するようです。

    

 それから、python2 と python3 ではデフォルト文字コードが異なるようです。

 python2 では ascii、python3 では utf-8 です。

 で、python2 でデフォルト文字コードをasciiのままにしておくと、
エラーになるケースがあります。

 pd01.pyを、Excelファイルでなく csvファイルを書き出すように変更すると、
デフォルト文字コードがasciiのままだと、下のエラーが発生します。

UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-1:
ordinal not in range(128)

 そこで、python2 の場合はデフォルト文字コードを変更するようにしました。

from platform import python_version
if int(python_version()[0]) < 3:
    reload(sys)
    sys.setdefaultencoding('cp932')

 上記のコードは、pythonのバージョンが3よりも前のものなら
デフォルト文字コードをcp932に変更する、という意味です。

 pd01.py のように Excelファイルに書き出す場合は、
デフォルトの文字コードを変更しなくてもエラーになりません。
でも、どこでエラーになるか分からないので念のため変更しておきます。

目次に戻る


(3) Excelファイルの読み込みとDataFrame

 Excelファイルの読み込みは簡単で、
dtf = pd.read_excel("test.xls") のようにすれば読み込めます。

 得られる結果は pandas の DataFrame です。

 DataFrameは、単純化していえば行列(マトリクス)ですが、
行単位・列単位で操作するための仕組みがいろいろ用意されています。

 Excelファイルの第1行目に書かれている「ID、性別、身長、体重」は、
それぞれ列ラベルる名になります。

 Excelファイルに「行ラベル」に該当するものはありませんが、
pandasのDataFrameでは 0 から始まる通し番号が割り当てられます。

 Excelファイルの第1行目は列ラベルなので、
その下の2行目が0番、3行目が1番です。

 変数dtfにセットされたDataFrameをExcelファイルとして書き出すと、
一番左の列に 0, 1, 2, 3, …… が縦に並ぶ形で挿入されます。
それより右側は、もともとのExcelファイルの内容と基本的に同じです。

目次に戻る


(4) DataFrameにまつわる操作

 ここで DataFrameの解説をするつもりはありません。
私にその力量はありません。

 単に「こんなスクリプトを書いたら、こんなふうになった」ということで、
私が気にとめた点を記します。

    

◇ 欠損値に名前をつける(fillna)

 変数 dtf にDataFrameがセットされている場合、
dtf[u'性別'] とすれば、「性別」の列(一つの列)を参照できます。

 この一つの列は、DataFrameではなく series という型になるようです。

 DataFrameが2次元のマトリクスに該当するとすれば、
seriesは1次元のベクトルに該当します。

 それはともかく、この列に含まれている欠損値に値を割り当てるとき、
fillna() を使います。

dtf[u'性別'] = dtf[u'性別'].fillna(u'記載なし')

 上のようにすると、欠損値に「記載無し」という文字が割り当てられて、
「性別」の内訳が「女性、男性、記載なし」の3種類になります。

 欠損値のままにしておくと、いろいろな分類や集計の際に無視されます。

    

◇ groupbyによる分類

 dgb = dtf.groupby(u'性別') とすると、
「女性、男性、記載なし」の3種類に分類されます。

 変数dgbにセットされる分類結果は、DataFrameではなく
GroupBy という独特の型のようです。

 この変数 dgb を用いて
 ser1 = dgb[u'身長'].mean() とすれば、
変数ser1には「身長」の平均値がセットされます。

 「女性、男性、記載なし」の三つの要素(平均値)からなるseriesです。

 この場合、「女性、男性、記載なし」は行ラベル。

 このようにして得られた「身長」のseriesと、それから「体重」のseriesを
新しく設けた空の dtf2 に記録します。
スクリプトの該当箇所は下のとおり。

dtf2 = pd.DataFrame()  # からのデータフレームを用意
dtf2[u'平均身長'] = dgb[u'身長'].mean().round(1)
dtf2[u'平均体重'] = dgb[u'体重'].mean().round(1)

 身長と体重を別々に処理しているあたりがスマートでないところですが、
私には分かりやすいのでこうしました。

    

◇ 行ラベルの順番の入れ替え、行名の指定

 行ラベルは「女性、男性、記載なし」になっています。

 これを「男性、女性、記載なし」の順番に変更します。

 それには ix[……] を使うようです。

dtf2 = dtf2.ix[[u'男性', u'女性', u'記載なし']]

 上のようにすると意図した順番になります。

 ix[……] は、DataFrameから行単位、または列単位で
データ(series)を取り出すためのもののようです。

 それから、行に対して「性別」という名前を付けます。

dtf2.index.name = u'性別'

 上のようにすると、「男性」の真上のセルに「性別」が入ります。

 上の記述がなくても「性別」が既にセットされているとおもいますが、
念のため書いておきました。

 ここではやりませんが、DataFrameの行と列を入れ替えると、
このindex.nameが空白になったりするので、
必要に応じてセットすることになります。

    

◇ Excelファイルの書き出し

 Excelファイルの書き出しは to_excel() で行います。

 サンプルでは dtf2.to_excel("pd01.xls") と、
最も簡単な使い方をしています。

 第2引数に文字列を与えると、それがワークシート名になります。

 Excel2007用の xlsxファイルを書き出すこともできます。

 この1行こっきりの記述は、ある種の省略形です。

 dtf1, dtf2 の二つのDataFrameを書き出したいときは
正式な手順に沿って下のように書きます。

writer = pd.ExcelWriter("test.xlsx")
dtf1.to_excel(writer, 'Sheet1')
dtf2.to_excel(writer, 'Sheet2')
writer.save()

目次に戻る


2. 男女全体の平均値をDataFrameに追加

 男女別に身長と体重の平均値を算出しましたが、
せっかくなので男女全体の平均値も加えることにします。

 主題は、DataFrameに seriesを追加する、列ラベルを変更する、
といったところでしょうか。

 作成する表は次のとおり。

性別 平均身長 平均体重
男性 166.7 66.4
女性 160.1 55.5
記載なし 164.2 67.4
全体 163.5 61.2

 上の表を書き出すスクリプト pd01_2.py は、zip圧縮ファイルに入っています。

    

(1) 男女別および男女全体の平均値をえる

 先の pd01.py では、平均値を得るのに身長と体重を別々に扱いました。

 なので、mean() を2度呼び出しています。

 これを一度で済ませることができます。

 Excelファイルを読み取った結果が変数 dtf にセットされている場合、

dgb = dtf.groupby(u'性別')  # 性別を手がかりに分類
dtf2 = dgb[[u'身長', u'体重']].mean().round(1)

 上のようにすると、変数 dtf2 には身長と体重の平均値が入ります。

 もちろん、男女別になっています。

 dtf2を表として書き出すと次のとおり。

性別 身長 体重
女性 160.1 55.5
男性 166.7 66.4
記載なし 164.2 67.4

    

 一方、男女全体の平均値は下の記述で得ることができます。

ser = dtf[[u'身長', u'体重']].mean().round(1)

 変数serにセットされるのは DataFrame でなく series です。

 この ser を dtf2 に最後の行として追加します。

dtf2 = dtf2.append(ser, ignore_index=True)

 上の記述で追加できます。

 ただ、dtf2の行ラベルが「女性、男性、記載なし」ではなく
0, 1, 2, 3 の通し番号になってしまいます。

 append() を呼び出す際、ignore_index=True を指定したためです。

 でも、これを指定しないとエラーになります。

 ということで、serを追加する前の dtf2の行ラベルを記録しておいて、
追加後、改めて行ラベルを通し番号から文字に変更します。

目次に戻る


(2) 行ラベル、列ラベルの調整

 男女別&身長&体重の平均値が入っている dtf2 の行ラベルは、
ilist = list(dtf2.index) として取得できます。

 dtf2.index だけだと、pythonでいうリストとして扱えません。

 「女性、男性、記載無し」に後で「全体」を追加する関係で、
ここはリストに変換しておきます。

 変数 ilist に「全体」を追加するには
ilist.append(u'全体') とします。

  dtf2 = dtf2.append(ser, ignore_index=True)
として、「全体」の行を追加した後で、
dtf2.index = ilist とすれば行ラベルが通し番号でなく文字になります。

 あと、行ラベルの順番を入れ替えます。

 今回は列ラベルの順番も念のため指定します。

dtf2 = dtf2.ix[[u'男性', u'女性', u'記載なし', u'全体'],
  [u'身長', u'体重']]

    

 ここまでの段階で、dtf2の列ラベルは「身長、体重」です。

 dtf2を表として書き出すと、「平均」という語が出てきません。

 そこで、dtf2の列ラベルを変更します。

 サンプルでは、ちょっと面倒な記述になってしまいますが、覚え書きとして
rename() を使ってみました。

 次のようにします。

dtf2.rename(columns={u'身長':u'平均身長', u'体重':u'平均体重'},
 inplace=True)

 inplace=True は、dtf2そのものに変更を加えるためのものです。

 これがないと、dtf2は変更されないので、
rename() の結果を変数で受け止めなければなりません。

 以上で「全体」の追加が完了です。

 もっと簡単にやれそうな気がしますが、方法が分かりませんでした。

目次に戻る


3. 分布の要約を得る(describeの利用)

 データを見るとき、平均値だけでは考察できません。

 最小値、最大値、ばらつきを見るための標準偏差、
あるいはパー線タイルの情報がほしいところです。

 pandasでは describe() を使うと、それらが簡単に得られます。

    

(1) 作成表のイメージと、これから行う操作

 describe() を使うと、通常は次の8項目が取得できます。

 これから作成する表は、上の8項目を日本語名にして、
下のような形にしたいとおもいます。

  身長              
  人数 平均値 標準偏差 最小値 1/4位点 中央値 3/4位点 最大値
男性 198 166.7156566 6.21548805 151.6 162.725 166.8 170.775 185.2
女性 192 160.0994792 6.374987916 144.8 156.075 160.1 164.4 179.8
記載なし 2 164.15 4.030508653 161.3 162.725 164.15 165.575 167

 上は身長に関するものですが、この右側に体重のデータが続きます。

 後の方になりますが、小数点以下が長いのもあるので、
項目別にround(まるめ処理)したいとおもいます。

 それと、更に後の方で、男女全体のデータを追加することも試みます。

目次に戻る


(2) describeを利用する簡易なスクリプト

 まるめ処理とか「全体」の追加をとりあえず横に置いて、
describeを利用する簡易なスクリプト pd02.py を掲げます。

 基本的には、平均値を求める pd01.py の mean を describe に置き換えるだけです。

 1# pd02.py (coding: cp932)
 2import os, sys
 3import pandas as pd
 4import xlwt
 5
 6from platform import python_version
 7if int(python_version()[0]) < 3:  # python ver 2 の場合
 8    reload(sys)
 9    sys.setdefaultencoding('cp932')  # デフォルト文字コードを変更
10
11xls_file = "pt_source.xls"
12dtf = pd.read_excel(xls_file)
13dtf[u'性別'] = dtf[u'性別'].fillna(u'記載なし')  # 性別の欠損値に名前付け
14dgb = dtf.groupby(u'性別')  # 性別を手がかりに分類
15dtf2 = dgb[[u'身長', u'体重']].describe()  # 男女別に分布状況を得る
16dtf2 = dtf2.ix[[u'男性', u'女性', u'記載なし'],
17  [u'身長', u'体重']]  # 行・列の順序入れ替え
18
19etoj = {'count': u'人数', 'mean': u'平均値', 'std': u'標準偏差',
20  'min': u'最小値', '25%': u'1/4位点', '50%': u'中央値',
21  '75%': u'3/4位点', 'max': u'最大値'}
22dtf2.rename(columns=etoj, inplace=True)  # 英語名を日本語に
23dtf2.to_excel("pd02.xls")

    

 上記スクリプトに関連して少し補足します。

◇ describeは数値データの列しか扱わない

dtf2 = dgb[[u'身長', u'体重']].describe()  # 男女別に分布状況を得る

 上は次のように簡略化して書いても結果は同じです。

dtf2 = dgb.describe()

 変数dgbには、身長と体重の他にIDと性別の情報も入っています。

 でも、IDと性別は数値データでないので describe() が処理対象にしません。

 結果、変数 dtf2 には身長と体重に関するデータだけが入ります。

 この事情は、mean() の場合も同じです。

    

◇ 行と列の入れ替え

 今回、得られる表は横に長いので、行と列を入れ替えて
縦長にしたいと考えたとします。

 その場合は、

dtf2.rename(columns=etoj, inplace=True)  # 英語名を日本語に

 上の1行を下の2行に置き換えます。

dtf2 = dtf2.T  # 行と列を入れ替え
dtf2.rename(index=etoj, inplace=True)  # 英語名を日本語に

 行と列を入れ替えは、大文字の T を適用しておこないます。

 なお、ここでは pandas ver 0.20.3 を前提にしていますが、
Ver 0.17.0 などでは describe の挙動(行と列の取り方)が
逆転していたような気がします。

    

◇ 複数のDataFrameを異なるExcelワークシートとして書き出す

 身長と体重を同じ表に盛り込む必要はないので、
別々のExcelワークシートに書き出したいと考えたとします。

 その場合は、スクリプトの最後の1行(to_excelの行)を下の4行に書き換えます。

writer = pd.ExcelWriter("pd02.xls")
dtf2[u'身長'].to_excel(writer, u'身長のシート')
dtf2[u'体重'].to_excel(writer, u'体重のシート')
writer.save()

 変数dtf2にセットされているのは
MultiIndex といわれる列構造を持つDataFrameです。

 単純な2次元マトリクスの DataFrame であれば
dtf2[u'身長'] で取り出せるのは series のはずですが、
今回は DataFrameが取り出されます。ちょっとだけ重厚な構成。

 これ以降で取り上げる操作では MultiIndex に少々手こずりました。

目次に戻る


(3) MultiIndexにおける小数点のまるめ処理

 ここでは、まるめ処理そのものに焦点を当てるのではなく、
MultiIndexの DataFrame の各列に対して、
どのようにまるめ処理を施すかが主題です。

 人数(count)は整数でいいでしょうし、
平均値など大半は小数点以下1桁で大丈夫だとおもいます。

 ただ、標準偏差は、小数点以下2桁か3桁くらい欲しいところです。

 いずれにしても、デフォルトのままだと桁数が多すぎます。

    

◇ MultiIndexのDataFrameからseriesを取り出す

 pd02.py で生成した dtf2 には必要なデータが一通り入っています。

 で、dtfx = dtf2[u'身長'] とすれば、 変数 dtfx には身長に関する DataFrame がセットされます。
男女別(3行)×平均値等の項目(8列)のデータが格納されています。

 そして、ser = dtfx[u'平均値'] とすれば、
変数 ser にseriesがセットされます。
「男性、女性、記載無し」の3種類の平均値です。

 ser = dtf2[u'身長'][u'平均値'] としても
おなじ series が得られます。

 series が得られれば round() を適用してまるめ処理を行うことができます。

    

◇ MultiIndexの場合の列ラベルの取り出し

 変数 dtf2 の列は、2段階の階層になっています。

 第1段階(大きい階層)は「身長、体重」です。

 それより細かい第2段階は、「人数、平均値、……」の8項目です。

 これら列ラベルの名前を得るには
dtf2.columns.levels[0] とか
dtf2.columns.levels[1] を用います。

    

◇ 二重のforループを使ったまるめ処理

 以上の事柄を基礎にして、実際のまるめ処理を行うと下のようになります。

 forループが入れ子になります。

for cname1 in dtf2.columns.levels[0]:
    dtfx = pd.DataFrame()
    for cname2 in dtf2.columns.levels[1]:
        if cname2 == u'人数':
            dtfx[cname2] = dtf2[cname1][cname2].astype(int)
        elif cname2 == u'標準偏差':
            dtfx[cname2] = dtf2[cname1][cname2].round(2)
        else:
            dtfx[cname2] = dtf2[cname1][cname2].round(1)
    dtf2[cname1] = dtfx
dtf2.to_excel("pd02_2.xls")

 「人数」のところで出てくる astype(int) は、値を整数値にするものらしいです。

 二重のforループがおわった後で、Excelファイルに書き出しています。

 実際の pd02_2.py では、書き出す前に行と列を入れ替えて
縦長にしています。

 入れ替えると、「列」ではなく「行」の方が2段階の階層を持つことになります。

目次に戻る


4. 男女全体の分布状況を追加

 describeで得られる平均値等の8項目の分布関連データに関して、
「男性、女性、記載無し」に「全体」を追加します。

 身長と体重を一括して取り扱う方法を調べてみましたが、分かりませんでした。

 なので、身長と体重を別々に処理します。

 その上で、最後に身長のDataFrameと体重のDataFrameを
concat() によって一つのDataFrameに結合します。

    

(1) 材料となる二つのDataFrame

 材料となる一つ目のDataFrameを改めて掲げると下のとおりです。

  身長              
  count mean std min 25% 50% 75% max
女性 192 160.0994792 6.374987916 144.8 156.075 160.1 164.4 179.8
男性 198 166.7156566 6.21548805 151.6 162.725 166.8 170.775 185.2
記載なし 2 164.15 4.030508653 161.3 162.725 164.15 165.575 167

 上は身長に関するものですが、右側に体重の表がくっつきます。

 この DataFrame を得るためのスクリプト記述は下のとおり。
(変数dtfには Excelファイルを読み込んだ結果が入っている。)

dgb = dtf.groupby(u'性別')  # 性別を手がかりに分類
genders = dgb[[u'身長', u'体重']].describe()  # 男女別に分布状況を得る

 変数 genders を表として出力すると先の表になります。

    

 一方、「全体」の DataFrame は次のようになります。

  身長 体重
count 392 393
mean 163.4619898 61.156743
std 7.090396165 10.76329797
min 144.8 40.1
25% 158.675 53
50% 163.6 60.3
75% 168.025 68.8
max 185.2 95.3

 この DataFrame を得るためのスクリプト記述は下のとおり。

entire = dtf[[u'身長', u'体重']].describe()  # 男女全体の分布状況を得る

 変数 entire を表として出力すると先の表になります。

 この材料となる二つの DataFrame をどうやって結合するか、
あれこれ調べてみましたが、簡単に済ます方法は分かりませんでした。

目次に戻る


(2) 身長と体重を別個に扱って「全体」を追加

 変数 genders から身長に関する DataFrame を取り出すには
dtfx = genders[u'身長'] とします。

 一方、身長に関する「全体」のデータは
ser = entire[u'身長'] とすれば大丈夫です。

 変数 ser には series がセットされます。

 そして、DataFrame の dtfx の最後の行として ser を追加するには
dtfx = dtfx.append(ser, ignore_index=True) です。

    

 この段階で dtfx を表として書き出すと下のとおり。

  count mean std min 25% 50% 75% max
0 192 160.0994792 6.374987916 144.8 156.075 160.1 164.4 179.8
1 198 166.7156566 6.21548805 151.6 162.725 166.8 170.775 185.2
2 2 164.15 4.030508653 161.3 162.725 164.15 165.575 167
3 392 163.4619898 7.090396165 144.8 158.675 163.6 168.025 185.2

 一番左の列が通し番号 0〜3 になっていますが、
これは「女性、男性、記載無し、全体」を意味します。

 ここで述べた一連の手順は、describe() でなく
mean() の平均値を扱ったときと同じです。

 以上は身長のデータに「全体」を追加する方法ですが、
体重についても同じ方法を適用できます。

目次に戻る


(3) 身長・体重の二つの DataFrame を結合

 身長の DataFrame が変数 height に入っていて、
体重の DataFrame は weight に入っているとします。

 この二つを横方向に結合するには下のようにします。

dtfm = pd.concat([height, weight], axis=1, keys=[u'身長', u'体重'])

 axis は軸線の意味のようです。
結合の方向を横・縦のどちらにするかを指定するものらしいです。

 keys=[u'身長', u'体重'] の使い方は、正直なところ よく分かりませんが、
これを指定すると MultiIndex の大きな階層を設けることができます。

 変数 dtfm は、genders と同じ MultiIndex になります。

目次に戻る


(4) スクリプト pd02_3.py の全体像

 これまで細切れに書いてきたので全体像が見えにくいとおもいます。

 ちょっと長いですが、pd02_3.py を掲げます。

 なお、describe() で得られた DataFrame に対して、
英語の列ラベルを日本語に変更する処理と、小数点のまるめ処理を
関数 describe_arrange() にまとめました。

 MyFunc.py というファイルに関数定義を書き込み、それを読み込む形にしました。

 1# pd02_3.py (coding: cp932)
 2import os, sys
 3import pandas as pd
 4import xlwt
 5
 6from platform import python_version
 7if int(python_version()[0]) < 3:  # python ver 2 の場合
 8    reload(sys)
 9    sys.setdefaultencoding('cp932')  # デフォルト文字コードを変更
10
11xls_file = "pt_source.xls"
12dtf = pd.read_excel(xls_file)
13dtf[u'性別'] = dtf[u'性別'].fillna(u'記載なし')  # 性別の欠損値に名前付け
14dgb = dtf.groupby(u'性別')  # 性別を手がかりに分類
15genders = dgb[[u'身長', u'体重']].describe()  # 男女別に分布状況を得る
16entire = dtf[[u'身長', u'体重']].describe()  # 男女全体の分布状況を得る
17ilist = list(genders.index)  # 「女性,男性,記載無し」の行ラベルを得る
18ilist.append(u'全体')
19
20    # 「全体」を追加:身長、体重を別々に扱う
21dlist = list()
22for cname in [u'身長', u'体重']:
23    ser = entire[cname]  # 身長または体重の「全体」を得る
24    dtfx = genders[cname].append(ser, ignore_index=True)  # 「全体」を追加
25    dtfx.index = ilist  # 行ラベルを通し番号から文字へ
26    dlist.append(dtfx)
27dtf2 = pd.concat(dlist, axis=1, keys=[u'身長', u'体重'])  # 身長と体重を結合
28dtf2 = dtf2.ix[[u'男性', u'女性', u'記載なし', u'全体'],
29  [u'身長', u'体重']]  # 行・列の順序入れ替え
30from MyFunc import describe_arrange
31describe_arrange(dtf2)
32dtf2 = dtf2.T  # 行と列を入れ替え
33dtf2.to_excel("pd02_3.xls")

目次に戻る


(5) GroupByオブジェクトの内容を辞書に入れて処理

 「全体」を追加する方法をもう一つ掲げてみます。

dgb = dtf.groupby(u'性別')  # 性別を手がかりに分類

 上のスクリプト記述で、変数dgbに GroupByオブジェクトが入ります。

 この dgb の内容を辞書(hash)にコピーするには下のようにします。

dct = dict()
for g_name, g_dtf in dgb:  # dgbを辞書に入れ替える
    dct[g_name] = g_dtf

 上記の処理がおわった後で
dct[u'全体'] = dtf とすれば、辞書に「全体」を追加できます。

 こうしておくと、dtfx = dct[u'女性'].describe() によって
女性の身長と体重のdescribe情報を取得できます。

 dtfx = dct[u'全体'].describe() だと「全体」の情報が得られます。

 身長の情報だけ取得するなら次のとおり。

ser = dct[u'女性'][u'身長'].describe()
ser = dct[u'全体'][u'身長'].describe()

 この場合は、得られるのが series です。

    

 以上のことを踏まえて「全体」を追加するスクリプトを書くと下のとおり。

 zip圧縮ファイルに入っている pd02_4.py から関連箇所を抜粋します。

dgb = dtf.groupby(u'性別')  # 性別を手がかりに分類
dct = dict()
for g_name, g_dtf in dgb:  # dgbを辞書に入れ替える
    dct[g_name] = g_dtf
dct[u'全体'] = dtf  # 辞書に「全体」を追加

ilist = [u'男性', u'女性', u'記載なし', u'全体']
clist = [u'身長', u'体重']
dlist = list()
for cname in clist:  # 身長と体重を別個に処理
    dtfx = pd.DataFrame()
    for idx in ilist:  # 男女別にdescribeの結果を得る
        ser = dct[idx][cname].describe()
        dtfx[idx] = ser
    dtfx = dtfx.T  # 「男性、女性、……」を列から行へ
    dlist.append(dtfx)  # 身長または体重のDataFrameを格納
dtf2 = pd.concat(dlist, axis=1, keys=clist)  # 身長と体重を結合

 「全体」が「男性、女性、……」と同列に扱えるようになり、
スクリプトが短くなるかなとおもいましたが、そうでもないですね。

 GroupByオブジェクトそのものに「全体」を追加できれば
ずっと短いスクリプトになるとおもいますが、
そんな方法があるんでしょうか?

    

 今回は、これで終了にします。

 得られる結果が簡素な割には手間がかかったという感触ですが、
pandas にもっと熟達すれば、短いスクリプトで済むのだとおもいます。

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