カテゴリー名: [pandasによる簡単な統計処理]
当シリーズでは、pythonのpandasを使って、
簡単な統計処理を試みます。
日本語MS-Windowsの環境下で、Excelファイルを素材にします。
文字コードは cp932 (Shift_JISの拡張版)を前提にします。
当Webページで紹介するスクリプトや素材データ一式は、
pandas02.zip という圧縮ファイルに同梱しておきます。
今回は、身長(数値)を「160〜170」のようにカテゴリ化し、
性別とのクロス集計を行います。
pythonをめぐる環境は「第1回」のときと同じですが、念のため記します。
素材となるソースデータは pt_source.xls です。
四つの列(ID、性別、身長、体重)からなるデータですが、
今回、体重は扱いません。
ID | 性別 | 身長 | 体重 |
C3 | 女性 | 159.1 | 57.8 |
W5 | 男性 | 163.8 | 78.2 |
W11 | 女性 | 162.7 | 59.5 |
H1 | 女性 | 157 | 59.6 |
上の形式で 400人分のデータが書かれています。
IDには欠損値がありませんが、性別、身長、体重には
僅かですが欠損値があります。
身長(数値データ)を「〜150、150〜160、160〜170、170〜180、180〜」
のようにカテゴリ化して、各カテゴリに所属する人数をカウントします。
この項で作成する表は次のとおり。
身長区分 | 人数 | 構成比 |
〜150 | 13 | 3.2 |
150〜160 | 106 | 26.5 |
160〜170 | 206 | 51.5 |
170〜180 | 61 | 15.2 |
180〜 | 6 | 1.5 |
不明 | 8 | 2 |
合計 | 400 | 100 |
変数 dtf に Excelファイルを読み込んだ結果が入っているものとします。
dtf は、DataFrame です。
この場合、身長をカテゴリ化するには次のようにします。
bins = [0.0, 150.0, 160.0, 170.0, 180.0, 500.0]
labels = [u'〜150', u'150〜160', u'160〜170', u'170〜180', u'180〜']
dtf[u'身長区分'] = pd.cut(dtf[u"身長"], bins, right=False, labels=labels)
上を実行すると、「身長区分」という列が新たにできます。
身長 159.1 の人の「身長区分」は「150〜160」になり、
167.3 の人なら「160〜170」になります。
いわば数値データに名前を付けたことになります。
こうしておけば、「150〜160」などの名前によって分類できます。
cut()
の第2引数 binsは、カテゴリに分割するときの区切りの値です。
bins = [0.0, 150.0, 160.0, 170.0, 180.0, 500.0]
上の指定は、「0〜150、150〜160、160〜170、170〜180、180〜500」に
分割することを指示します。
第2引数 right=False
は、各カテゴリの上限値(右側の値)の人を
カテゴリに含めないようにするための指定です。
False にしておくと、「150〜160」に 160.0 の人は含まれません。
カテゴリが「150以上・160未満」の意味になります。
デフォルトは right=True
です。
第3引数の labels は、各カテゴリの名前を指定するものです。
labels = [u'〜150', u'150〜160', u'160〜170', u'170〜180', u'180〜']
上のリストで指定した名前がカテゴリ名になります。
身長の列には欠損値があるのですが、それに「不明」と名前を付けるとすれば、
欠損値に便宜上 999.0 を割り当てることにして、たとえば下のようにします。
bins = [0.0, 150.0, 160.0, 170.0, 180.0, 500.0, 1000.0]
labels = [u'〜150', u'150〜160', u'160〜170', u'170〜180', u'180〜', u'不明']
dtf[u'身長区分'] = pd.cut(dtf[u"身長"].fillna(999.0),
bins, right=False, labels=labels)
新たに「身長区分」という列が加わった DataFrame を材料にして、
カテゴリごとの人数をカウントします。
また、各カテゴリの合計値も追加します。
「身長区分」による分類は、groupby()
で行うことにします。
dgb = dtf.groupby(u'身長区分')
上を実行すると、身長区分ごとに分類された DataFrame ができます。
「不明」を入れると6分類になるので
「ID、性別、身長、体重、身長区分」の5列から構成される
DataFrame が6個できることになります。
変数dgbに、その6個の DataFrame が記録される訳です。
次に、各カテゴリに所属する人の人数をカウントします。
ser = dgb[u'身長区分'].size()
上のようにすると、各カテゴリの人数が series として取得できます。
変数 ser を print()
で表示させると次のとおり。
身長区分
〜150 13
150〜160 106
160〜170 206
170〜180 61
180〜 6
不明 8
Name: 身長区分, dtype: int64
初心者の私は、ここで引っかかりました。
series の合計値は ser.sum()
のようにして求めることができます。
だとすれば、下の2行で変数serに合計値を追加できるのでは?と考えました。
xsum = ser.sum()
ser[u'合計'] = xsum
ところが、これだとエラーが発生します。
変数serの行ラベルは、「〜150、150〜160、……」ですが、
これが CategoricalIndex という特殊な型のようで、
TypeError: cannot insert an item into a CategoricalIndex
that is not already an existing category
上のエラーが起きます。
そこで、ser.index = list(ser.index)
として、
serの行ラベルを付け直してみます。
その上で合計値を追加すると、正常に処理できました。
合計値に対する各カテゴリの構成比(パーセンテージ)は、
ser2 = ser / xsum * 100.0
上の1行で求めることができます。
人数の ser、パーセンテージの ser2 が得られれば、
この項の冒頭に掲げた表を出力できます。
ここで、身長区分ごとの人数と構成比を
Excelファイルに書き出すスクリプトを掲載しておきます。
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) 13 14bins = [0.0, 150.0, 160.0, 170.0, 180.0, 500.0, 1000.0] 15labels = [u'〜150', u'150〜160', u'160〜170', u'170〜180', u'180〜', u'不明'] 16dtf[u'身長区分'] = pd.cut(dtf[u"身長"].fillna(999.0), 17 bins, right=False, labels=labels) 18dgb = dtf.groupby(u'身長区分') 19ser = dgb[u'身長区分'].size() # 身長区分ごとの人数を得る 20ser.index = list(ser.index) # CategoricalIndexを通常のindexに 21xsum = ser.sum() # 人数の合計値 22ser[u'合計'] = xsum 23dtf2 = pd.DataFrame() 24dtf2[u'人数'] = ser 25dtf2[u'構成比'] = (ser / xsum * 100.0).round(1) 26dtf2.index.name = u'身長区分' 27dtf2.to_excel("pd01.xls")
性別と身長区分とのクロス集計を行います。
作成する表は次のとおり(人数の表)。
〜150 | 150〜160 | 160〜170 | 170〜180 | 180〜 | 不明 | 合計 | |
男性 | 0 | 25 | 117 | 50 | 6 | 6 | 204 |
女性 | 13 | 81 | 87 | 11 | 0 | 2 | 194 |
記載なし | 0 | 0 | 2 | 0 | 0 | 0 | 2 |
合計 | 13 | 106 | 206 | 61 | 6 | 8 | 400 |
これまで用いてきた groupby()
を二段構えで行えば、
クロス集計の表を得ることができます。
でも、pivot_table()
を使えば、より簡単です。
ただ、簡単とはいえ引っかかったところがあるので、それを中心に記します。
変数dtf(DataFrame型)に、既に「身長区分」の列が設けられているものとします。
そのとき、下のように pivot_table()
を呼び出します。
dtf2 = pd.pivot_table(dtf, values=[u'ID'], index=u'性別',
columns=u'身長区分', aggfunc='count')
どの項目とどの項目をクロスさせるかは、
index と columns で指定します。
index=u'性別'
は、性別の内訳(女性、男性、記載なし)を
縦方向に並べるための指定です。
columns=u'身長区分'
は、身長区分の内訳を横方向に並べる指定。
最後の引数 aggfunc は、計算処理の種類を指定するものです。
aggfunc='count'
とすれば、度数(人数・個数)の数え上げです。
これを省略すると、'mean'
の平均値算出になります。
私が引っかかったのは values の指定です。
これは、どの項目に対して aggfunc の計算処理を適用するかを指定するものです。
上記では values=[u'ID']
としました。
IDの列には欠損値がないので、取りこぼしなく数え上げができるからです。
他に、性別と身長区分にも実質的に欠損値がない状態になっているので、
values=[u'性別']
とか values=[u'身長区分']
も試してみましたが、
どちらもエラーになりました。
引数 index, columns で指定した項目は、
values で指定できない(?)のだろうとおもいます。
dtf2 = pd.pivot_table(dtf, values=[u'ID'], index=u'性別',
columns=u'身長区分', aggfunc='count')
上記で、引数 values を省略したとすると、
「ID、身長、体重」の三つについて
「性別×身長区分」のクロス集計表が生成されます。
戻り値が代入される変数 dtf2 には、この三つの集計表(DataFrame)が入ります。
当然、dtf2は、MultiIndexの構造になります。
では、ちゃんと values を指定した場合にどうなるかというと、
やはり MultiIndex になります。
大枠の階層には「ID」の一つしか要素がありませんが、
それでも MultiIndex です。
通常の2次元マトリクス的な DataFrame にしたいときは次のようにします。
dtf2 = dtf2[u'ID']
ここまでの処理で、変数 dtf2 に「性別×身長区分」のクロス集計表が入りました。
ただ、「合計」を追加する場合、扱いにくい点があります。
「合計」追加の下準備を含めて少し書きます。
dtf2の列ラベル「〜150、150〜160、……」は CategoricalIndex です。
dtf2に「合計」を付加するとき、このことが障壁になります。
なので、下のように CategoricalIndex型を通常型に変換します。
dtf2.columns = list(dtf2.columns)
dtf2 には欠損値が含まれています。
性別の「記載なし」は全部で2人だけなので、数え上げできなう箇所が生じ
クロス集計表内に空欄セルができます。
その空欄セルが欠損値です。
欠損値のままでもいいのですが、数値 0 に置き換えることにします。
dtf2.fillna(0)
とすれば、欠損値を置換できるかとおもいましたが、
ダメなようです。
なので、次のように列ごとに置換することにします。
for col in dtf2.columns: # 欠損値を0にする
dtf2[col] = dtf2[col].fillna(0)
これで「合計」を追加する準備ができました。
series に「合計」を追加するのは簡単でしたが、
DataFrame の場合は少々面倒です。
私には整理してロジックをたどるのが難しいので、
「こう書けば、こうなる」という慣用句のように使うことにしました。
結局、次のようにすれば行と列の両方の「合計」を追加できます。
dtf2 = pd.concat([dtf2, pd.DataFrame(dtf2.sum(axis=0), columns=[u’合計’]).T]) # 最後の行として合計を追加 dtf2 = pd.concat([dtf2, pd.DataFrame(dtf2.sum(axis=1), columns=[u’合計’])], axis=1) # 最後の列として合計を追加
これで変数 dtf2 に縦・横両方の「合計」を追加できます。
ここで、「性別×身長区分」のクロス集計表を
Excelファイルに書き出すスクリプトを掲げておきます。
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'記載なし') 14 15bins = [0.0, 150.0, 160.0, 170.0, 180.0, 500.0, 1000.0] 16labels = [u'〜150', u'150〜160', u'160〜170', u'170〜180', u'180〜', u'不明'] 17dtf[u'身長区分'] = pd.cut(dtf[u"身長"].fillna(999.0), 18 bins, right=False, labels=labels) 19dtf2 = pd.pivot_table(dtf, values=[u'ID'], index=u'性別', 20 columns=u'身長区分', aggfunc='count') 21dtf2 = dtf2[u'ID'] # MultiIndex構造から通常のDataFrameへ 22dtf2.columns = list(dtf2.columns) # CategoricalIndex型を通常型へ 23for col in dtf2.columns: # 欠損値を0にする 24 dtf2[col] = dtf2[col].fillna(0) 25dtf2 = dtf2.ix[[u'男性', u'女性', u'記載なし']] 26dtf2 = pd.concat([dtf2, pd.DataFrame(dtf2.sum(axis=0), 27 columns=[u'合計']).T]) # 最後の行として合計を追加 28dtf2 = pd.concat([dtf2, pd.DataFrame(dtf2.sum(axis=1), 29 columns=[u'合計'])], axis=1) # 最後の列として合計を追加 30dtf2.to_excel("pd02.xls")
度数表から構成比の表を生成し、
その二つの表を2種類の形でくっつけます。
二つの表を単純に縦にくっつけるやり方と、
度数と構成比を隣接させて一つの表にまとめるやり方です。
男女それぞれ別個に、身長区分別のパーセンテージを算出します。
具体的には次の表を作成します。
〜150 | 150〜160 | 160〜170 | 170〜180 | 180〜 | 不明 | 合計 | |
男性 | 0.0 | 12.3 | 57.4 | 24.5 | 2.9 | 2.9 | 100.0 |
女性 | 6.7 | 41.8 | 44.8 | 5.7 | 0.0 | 1.0 | 100.0 |
記載なし | 0.0 | 0.0 | 100.0 | 0.0 | 0.0 | 0.0 | 100.0 |
合計 | 3.2 | 26.5 | 51.5 | 15.2 | 1.5 | 2.0 | 100.0 |
度数表(人数の表)が既に変数 dtf2 にセットされているものとします。
dtf2 は DataFrame です。
DataFrameから series を取り出すには、列単位の方がやりやすいので、
まず、行と列を入れ替えて「男性、女性、……」を列にします。
そうすれば、男女それぞれの身長区分別人数を series として取り出せます。
series の最後の要素が「合計」なので、
各要素を「合計」で割り算し、100を掛ければOKです。
変数 ser に series が代入されている場合、
ser[-1]
で最後の要素の値を参照できます。
ということで、度数表が入っている dtf2 を構成比表にするための
スクリプト pd03.py の該当箇所は下のとおり。
dtf2 = dtf2.T # 男性、女性、……を行から列へ
for col in dtf2.columns: # 男女別にパーセンテージを算出
xsum = dtf2[col][-1] # 最後の要素「合計」の値
dtf2[col] = (dtf2[col] / xsum * 100.0).round(1) # 構成比の算出
dtf2 = dtf2.T # 行と列を元に戻す
dtf2.to_excel("pd03.xls", u'性別×身長区分のパーセンテージ')
度数表が変数 dtf_frq に入っていて、
構成比表が dtf_pct に入っているとすれば、
それを縦にくっつけるのは次のスクリプトで可能です。
dtf2 = pd.concat([dtf_frq, dtf_pct], axis=0,
keys=[u'度数(人)', u'構成比(%)']) # 度数表と構成比表を縦にくっつける
この項の肝は上記でおわりですが、
ちょっと引っかかった点があるので記します。
DataFrame の代入処理に関するものです。
変数 dtf2 にクロス集計のための処理とか、
「合計」不可の処理を施して度数表に仕上げたとします。
そこで dtf_frq = dtf2
として度数表を保存します。
その上で、今度は dtf2 に加工処理を施して構成比表に仕立て上げます。
すると、変数 dtf_frq が度数表でなく構成比表になってしまいます。
この場合、dtf_frq = dtf2
とするのでなく
dtf_frq = dtf2.copy()
としなければダメなようです。
DataFrame の =
による単なる代入処理は、
「値渡し」でなく「参照渡し」に該当するようです。
pd04.py から該当箇所を抜粋しておきます。
(前略)
dtf2 = pd.concat([dtf2, pd.DataFrame(dtf2.sum(axis=0),
columns=[u'合計']).T]) # 最後の行として合計を追加
dtf2 = pd.concat([dtf2, pd.DataFrame(dtf2.sum(axis=1),
columns=[u'合計'])], axis=1) # 最後の列として合計を追加
dtf_frq = dtf2.copy() # 度数表として保存
dtf2 = dtf2.T # 男性、女性、……を行から列へ
for col in dtf2.columns: # 男女別にパーセンテージを算出
xsum = dtf2[col][-1] # 最後の要素「合計」の値
dtf2[col] = (dtf2[col] / xsum * 100.0).round(1) # 構成比の算出
dtf_pct = dtf2.T # 行と列を元に戻して構成比の表に
dtf2 = pd.concat([dtf_frq, dtf_pct], axis=0,
keys=[u'度数(人)', u'構成比(%)']) # 度数表と構成比表を縦にくっつける
dtf2.to_excel("pd04.xls")
ここで作る表は下のようになります。
〜150 | 150〜160 | 160〜170 | 170〜180 | 180〜 | 不明 | 合計 | ||
男性 | 人数 | 0 | 25 | 117 | 50 | 6 | 6 | 204 |
構成比 | 0.0 | 12.3 | 57.4 | 24.5 | 2.9 | 2.9 | 100.0 | |
女性 | 人数 | 13 | 81 | 87 | 11 | 0 | 2 | 194 |
構成比 | 6.7 | 41.8 | 44.8 | 5.7 | 0.0 | 1.0 | 100.0 | |
記載なし | 人数 | 0 | 0 | 2 | 0 | 0 | 0 | 2 |
構成比 | 0.0 | 0.0 | 100.0 | 0.0 | 0.0 | 0.0 | 100.0 | |
合計 | 人数 | 13 | 106 | 206 | 61 | 6 | 8 | 400 |
構成比 | 3.2 | 26.5 | 51.5 | 15.2 | 1.5 | 2.0 | 100.0 |
上の表とは行と列が逆転した状態の 度数表と 構成比表が
それぞれ dtf_frq, dtf_pct に入っているとします。
男性の人数と構成比の二つを新しい DataFrame に記録するには次のようにします。
dtfx = pd.DataFrame() # 空のDataFrameを設ける
ser1 = dtf_frq[u'男性'] # 男性の度数に関するseries
ser2 = dtf_pct[u'男性'] # 男性の構成比に関するseries
dtfx[u'人数'] = ser1
dtfx[u'構成比'] = ser2
上の処理を「女性、記載なし、合計」についても行うと、
人数と構成比の2列からなる新しい DataFrame が4個できます。
最後に、その四つの DataFrame を pd.concat()
で結合し、
行と列を入れ替えれば目的の DataFrame が得られます。
行が MultiIndexの構造になっている DataFrame です。
スクリプト pd05.py から該当箇所を抜粋しておきます。
(前略)
dtf2 = pd.concat([dtf2, pd.DataFrame(dtf2.sum(axis=0),
columns=[u'合計']).T]) # 最後の行として合計を追加
dtf2 = pd.concat([dtf2, pd.DataFrame(dtf2.sum(axis=1),
columns=[u'合計'])], axis=1) # 最後の列として合計を追加
dtf2 = dtf2.T # 男性、女性、……を行から列へ
dtf_frq = dtf2.copy() # 度数表として保存
for col in dtf2.columns: # 男女別にパーセンテージを算出
xsum = dtf2[col][-1] # 最後の要素「合計」の値
dtf2[col] = (dtf2[col] / xsum * 100.0).round(1) # 構成比の算出
dtf_pct = dtf2.copy() # 構成比として保存
# 人数と構成比を隣接させ、一つの表にする
clist = list(dtf_frq.columns) # 列名:「男性、女性、……」
dlist = list()
for col in clist:
dtfx = pd.DataFrame()
dtfx[u'人数'] = dtf_frq[col]
dtfx[u'構成比'] = dtf_pct[col]
dlist.append(dtfx)
dtf2 = pd.concat(dlist, axis=1, keys=clist)
dtf2 = dtf2.T # 「男性、女性、……」を列から行に
dtf2.to_excel("pd05.xls")
今回は これで終了です。
体重については取り上げませんでしたが、身長と同じように処理可能です。
Copyright (C) T. Yoshiizumi, 2017 All rights reserved.