Python pandas アクセサ / Grouperで少し高度なグルーピング/集計
日本語の説明がなさそうなので。
概要
pandas では groupby
メソッドを使って、指定したカラムの値でデータをグループ分けできる。ここでは少し凝った方法を説明。
※ dtアクセサ の追加、またグルーピング関連のバグ修正がいろいろ入っているので、0.15以降が必要。
※簡単な処理については下の記事でまとめ。
はじめに
例えばこんなデータがあったとして、
import pandas as pd import datetime df = pd.DataFrame({'dt1': [datetime.datetime(2014, 10, 1), datetime.datetime(2014, 10, 2), datetime.datetime(2014, 10, 3), datetime.datetime(2014, 10, 4), datetime.datetime(2014, 10, 5), datetime.datetime(2014, 10, 6), datetime.datetime(2014, 10, 7), datetime.datetime(2014, 11, 1), datetime.datetime(2014, 11, 2), datetime.datetime(2014, 11, 3), datetime.datetime(2014, 11, 4), datetime.datetime(2014, 11, 5), datetime.datetime(2014, 11, 6), datetime.datetime(2014, 11, 7)], 'col1': 'a b'.split() * 7}) # col1 dt1 num1 # 0 a 2014-10-01 3 # 1 b 2014-10-02 4 # 2 a 2014-10-03 2 # 3 b 2014-10-04 5 # 4 a 2014-10-05 1 # 5 b 2014-11-01 3 # 6 a 2014-11-02 2 # 7 b 2014-11-03 3 # 8 a 2014-11-04 5 # 9 b 2014-11-05 2
まずは普通の groupby
。DataFrame
を groupby
すると、結果は DataFrameGroupBy
インスタンスとして返ってくる。このとき、groups
プロパティに グループのラベル : グループに含まれる index からなる辞書が入っている。サンプルスクリプトでの結果は、この groups
プロパティを使って表示することにする。
下の結果は、上の DataFrame
をカラム "col1" の値でグループ分けした結果、グループ "a" に 0, 2, 4, 6, 8行目, グループ "b" に 1, 3, 5, 7, 9行目が含まれていることを示す。
df.groupby('col1').groups # {'a': [0L, 2L, 4L, 6L, 8L], # 'b': [1L, 3L, 5L, 7L, 9L]}
groupby へのリスト渡し
groupby
には 対象の DataFrame
と同じ長さのリスト, numpy.array
, pandas.Series
, pandas.Index
が渡せる。このとき、渡した リスト-likeなものをキーとしてグルーピングしてくれる。
grouper = ['g1', 'g2', 'g3', 'g4', 'g5'] * 2 df.groupby(grouper).groups # {'g5': [4L, 9L], # 'g4': [3L, 8L], # 'g3': [2L, 7L], # 'g2': [1L, 6L], # 'g1': [0L, 5L]}
そのため、一度 外部の関数を通せばどんな柔軟な処理でも書ける。"num1" カラムが偶数か奇数かでグループ分けしたければ、
grouper = df['num1'] % 2 df.groupby(grouper).groups # {0: [1L, 2L, 6L, 9L], # 1: [0L, 3L, 4L, 5L, 7L, 8L]}
アクセサ
でも わざわざ関数書くのめんどくさい、、、。そんなときの アクセサ。例えばカラムが datetime64
型のとき、dt
アクセサを利用して datetime64
の各プロパティにアクセスできる。
例えば "dt1" カラムの各日時から "月" を取得するには、
df['dt1'].dt.month # 0 10 # 1 10 # 2 10 # 3 10 # 4 10 # 5 11 # 6 11 # 7 11 # 8 11 # 9 11 # dtype: int64
これを使えば月次のグルーピングも簡単。
df.groupby(df['dt1'].dt.month).groups # {10: [0L, 1L, 2L, 3L, 4L], # 11: [5L, 6L, 7L, 8L, 9L]}
複数要素、例えば年月の組み合わせでグループ分けしたければ、 dt.year
と dt.month
をリストで渡す。
df.groupby([df['dt1'].dt.year, df['dt1'].dt.month]).groups # {(2014L, 11L): [5L, 6L, 7L, 8L, 9L], # (2014L, 10L): [0L, 1L, 2L, 3L, 4L]}
Grouperで周期的なグルーピング
数日おきなど、特定の周期/間隔でグループ分けしければ Grouper
。freq
キーワードの記法は Documentation 参照。
※ Grouper
で datetime64
をグループ分けした場合、groups
プロパティの読み方はちょっと難しくなる (辞書の値として、グループに含まれる indexではなくグループの切れ目になる index が入る)
df.groupby(pd.Grouper(key='dt1', freq='4d')).groups # {Timestamp('2014-10-09 00:00:00', offset='4D'): 5, # Timestamp('2014-11-02 00:00:00', offset='4D'): 10, # Timestamp('2014-10-13 00:00:00', offset='4D'): 5, # Timestamp('2014-10-01 00:00:00', offset='4D'): 4, # Timestamp('2014-10-05 00:00:00', offset='4D'): 5, # Timestamp('2014-10-25 00:00:00', offset='4D'): 5, # Timestamp('2014-10-29 00:00:00', offset='4D'): 6, # Timestamp('2014-10-17 00:00:00', offset='4D'): 5, # Timestamp('2014-10-21 00:00:00', offset='4D'): 5}
読めないので同じ処理をした結果を普通に print
。4日おきに グループ分けされていることがわかる。
対応する日付のない期間はパディングされる。
for name, group in df.groupby(pd.Grouper(key='dt1', freq='4d')): print(name) print(group) # 2014-10-01 00:00:00 # col1 dt1 num1 # 0 a 2014-10-01 3 # 1 b 2014-10-02 4 # 2 a 2014-10-03 2 # 3 b 2014-10-04 5 # 2014-10-05 00:00:00 # col1 dt1 num1 # 4 a 2014-10-05 1 # 2014-10-09 00:00:00 # Empty DataFrame # 2014-10-13 00:00:00 # Empty DataFrame # 2014-10-17 00:00:00 # Empty DataFrame # 2014-10-21 00:00:00 # Empty DataFrame # 2014-10-25 00:00:00 # Empty DataFrame # 2014-10-29 00:00:00 # col1 dt1 num1 # 5 b 2014-11-01 3 # 2014-11-02 00:00:00 # col1 dt1 num1 # 6 a 2014-11-02 2 # 7 b 2014-11-03 3 # 8 a 2014-11-04 5 # 9 b 2014-11-05 2
集計
上のようにグルーピングした後、集約関数、もしくは aggregate
で普通に集約してもよいが、
df.groupby(df['dt1'].dt.month)['num1'].sum() # 10 15 # 11 15 # Name: num1, dtype: int64
pandas.pivot_table
にも リスト-like、 Grouper
を渡して直接集約できる。例えば "dt1" の year を列, month を行として集計したければ、
pd.pivot_table(df, index=df['dt1'].dt.month, columns=df['dt1'].dt.year, values='num1', aggfunc=sum) # 2014 # 10 15 # 11 15
"col1" を列, "dt1"の値を4日おきに行として集計したければ、
pd.pivot_table(df, index=pd.Grouper(key='dt1', freq='4d'), columns='col1', values='num1', aggfunc=sum) # col1 a b # dt1 # 2014-10-01 5 9 # 2014-10-05 1 NaN # 2014-10-29 NaN 3 # 2014-11-02 7 5
補足 Grouper
での集約には二つ既知の バグがあるので注意。
- グルーピング対象の列の値に
NaT
が入っているときvar
,std
,mean
で集約できない first
,last
,nth
で返ってくる行が 通常のgroupby
と異なる。
まとめ
groupby
,pivot_table
にはリスト-likeが渡せる。複雑な処理を書きたければ 外部の関数で配列作成してグルーピング/集計する。- アクセサ,
Grouper
便利。
おまけ
groupby
には 関数、辞書も渡せる。
df.groupby(lambda x: x % 3).groups # {0: [0L, 3L, 6L, 9L], # 1: [1L, 4L, 7L], # 2: [2L, 5L, 8L]}
辞書渡しの場合、存在しないキーはフィルタされる。
grouper = {1: 'a', 2: 'b', 3: 'a'} df.groupby(by=grouper).groups # {'a': [1L, 3L], # 'b': [2L]} [asin:4873116554:detail]
Python rpy2 で pandas の DataFrame を R の data.frame に変換する
pandas の DataFrame
を R へ渡す/また R から Python へデータを戻す方法について、本家のドキュメント が書きかけなのでよくわからない。ということで 以前 下の文書を書いたので訳してみる。
DOC: Complete R interface section by sinhrks · Pull Request #7309 · pydata/pandas · GitHub
rpy2
を使うと pandas
(Python) <-> R 間のデータの相互変換を以下 2通りの方法で行うことができる。
何をどちらに読み込ませるかという話なので、1, 2を組み合わせて処理することもできる。
共通
rpy2 のインストール
pip install rpy2
準備
import numpy as np import pandas as pd # 表示する行数を指定 pd.options.display.max_rows = 5
R のデータセットを Pandas に読み込む
pandas.rpy.common.load_data
で、R のデータセットが pandas.DataFrame
に変換されて読み込まれる。
import pandas.rpy.common as com infert = com.load_data('infert') type(infert) # pandas.core.frame.DataFrame infert.head() # education age parity induced case spontaneous stratum pooled.stratum # 1 0-5yrs 26 6 1 1 2 1 3 # 2 0-5yrs 42 1 1 1 0 2 1 # 3 0-5yrs 39 6 2 1 0 3 4 # 4 0-5yrs 34 4 2 1 0 4 2 # 5 6-11yrs 35 3 1 1 1 5 32
pandas.DataFrame を R に渡せる形式 (rpy2) に変換する
pandas.DataFrame
から R に渡せる形式 (rpy2.robjects.DataFrame
) への変換は com.convert_to_r_dataframe
で行う。
# サンプルの DataFrame 作成 df = pd.DataFrame({'A': [1, 2, 3], 'B': [4, 5, 6], 'C':[7,8,9]}, index=["one", "two", "three"]) df # A B C # one 1 4 7 # two 2 5 8 # three 3 6 9 r_dataframe = com.convert_to_r_dataframe(df) type(r_dataframe) # rpy2.robjects.vectors.DataFrame # ipython でやる場合、print をかまさないと出力がうっとおしい print(r_dataframe) # A B C # one 1 4 7 # two 2 5 8 # three 3 6 9 # rpy2.objects.DataFrame に変換後も属性名の確認/操作はできる ( pandas でやっておけば必要ないが) print(r_dataframe.rownames) # [1] "one" "two" "three" In [20]: print(r_dataframe.colnames) [1] "A" "B" "C"
補足 R へ matrix
で渡したい場合は com.convert_to_r_matrix
。
r_matrix = com.convert_to_r_matrix(df) type(r_matrix) # rpy2.robjects.vectors.Matrix print(r_matrix) # A B C # one 1 4 7 # two 2 5 8 # three 3 6 9
rpy2 形式のデータ を pandas.DataFrame に変換する
上の逆変換は com.convert_robj
。rpy2 オブジェクト (rpy2.robjects.DataFrame
) を pandas.DataFrame
に戻す。
com.convert_robj(r_dataframe) # A B C # one 1 4 7 # two 2 5 8 # three 3 6 9 com.convert_robj(r_matrix) # A B C # one 1 4 7 # two 2 5 8 # three 3 6 9
1. R の関数を Python に loadし、Python の名前空間上で操作する
rpy2.robjects.r.__getitem__
で、R の名前空間のオブジェクトを Python の名前空間へ load できる。ここでは R の sum
関数をPython 上に読み出して使う。読み込んだ関数へ渡せるのは rpy2
のオブジェクトのみ。
# 処理するデータ print(r_dataframe) # A B C # one 1 4 7 # two 2 5 8 # three 3 6 9 import rpy2.robjects as robjects # R 上の sum 関数を rsum として Python の名前空間に load rsum = robjects.r['sum'] type(rsum) # rpy2.robjects.functions.SignatureTranslatedFunction # r_dataframe に対して関数適用 (Python の関数として使える) rsum_result = rsum(r_dataframe) # R の関数なので、結果は vector になる type(rsum_result) # rpy2.robjects.vectors.IntVector # 要素を取り出す場合はスライス rsum_result[0] # 45
2. Python のデータを R に渡し、 R の名前空間上で操作する
rpy2
のオブジェクトを R の名前空間へ渡す場合は robjects.r.assign
。
R で 実行する処理(コマンド)を R に渡す (Rに何か処理をさせる) 場合は robjects.r
。
# Python 上の r_dataframe が R 上で rdf という名前で転送される # セミコロンは ipython での出力省略のため robjects.r.assign('rdf', r_dataframe); # R 上で str(rdf) コマンドを実行 robjects.r('str(rdf)'); # 'data.frame': 3 obs. of 3 variables: # $ A:Class 'AsIs' int [1:3] 1 2 3 # $ B:Class 'AsIs' int [1:3] 4 5 6 # $ C:Class 'AsIs' int [1:3] 7 8 9
処理サンプル
これまでの処理の組み合わせで以下のようなことができる。
線形回帰
# データの準備 iris = com.load_data('iris') # setosa のデータをフィルタ setosa = iris[iris['Species'] == 'setosa'] setosa.head() # Sepal.Length Sepal.Width Petal.Length Petal.Width Species # 1 5.1 3.5 1.4 0.2 setosa # 2 4.9 3.0 1.4 0.2 setosa # 3 4.7 3.2 1.3 0.2 setosa # 4 4.6 3.1 1.5 0.2 setosa # 5 5.0 3.6 1.4 0.2 setosa # (ほか、R でやるには面倒な処理があれば pandas で実行... ) # rpy2 オブジェクトに変換 r_setosa = com.convert_to_r_dataframe(setosa) # R の名前空間に送る robjects.r.assign('setosa', r_setosa); # R の lm 関数を実行し、結果の summary を表示する robjects.r('result <- lm(Sepal.Length~Sepal.Width, data=setosa)'); print(robjects.r('summary(result)')) # Call: # lm(formula = Sepal.Length ~ Sepal.Width, data = setosa) # # Residuals: # Min 1Q Median 3Q Max # -0.52476 -0.16286 0.02166 0.13833 0.44428 # # Coefficients: # Estimate Std. Error t value Pr(>|t|) # (Intercept) 2.6390 0.3100 8.513 3.74e-11 *** # Sepal.Width 0.6905 0.0899 7.681 6.71e-10 *** # --- # Signif. codes: 0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1 # # Residual standard error: 0.2385 on 48 degrees of freedom # Multiple R-squared: 0.5514, Adjusted R-squared: 0.542 # F-statistic: 58.99 on 1 and 48 DF, p-value: 6.71e-10 # R 上の result オブジェクトを Python の名前空間に戻す result = robjects.r['result'] print(result.names) # [1] "coefficients" "residuals" "effects" "rank" # [5] "fitted.values" "assign" "qr" "df.residual" # [9] "xlevels" "call" "terms" "model" # 結果は名前付きリストになっている。各要素には .rx でアクセスできる print(result.rx('coefficients')) # $coefficients # (Intercept) Sepal.Width # 2.6390012 0.6904897 # 回帰式の切片, 係数を取得 intercept, coef1 = result.rx('coefficients')[0] intercept # 2.6390012498579694 coef1 # 0.6904897170776046
時系列処理
時系列の場合に気をつけるのは、
convert_to_r_dataframe
はSeries
に対応していないので、rpy2
で直接 ベクトル (ここではrpy2.FloatVector
) を作って渡す。詳細は rpy2 documentation: Vectors and arrays 。ts
オブジェクトへの変換は R で明示的に行う- R から
pandas.DataFrame
へ結果を戻した際に、日時の index を再度 付与する
# 2013年1月〜 月次 4年分のランダムデータを作成 idx = pd.date_range(start='2013-01-01', freq='M', periods=48) vts = pd.Series(np.random.randn(48), index=idx).cumsum() vts # 2013-01-31 0.801791 # ... # 2016-12-31 -3.391142 # Freq: M, Length: 48 # R へ渡す rpy2 オブジェクトを準備 r_values = robjects.FloatVector(vts.values) # R の名前空間へ転送 robjects.r.assign('values', r_values); # R の ts 関数で timeseries に変換 robjects.r('vts <- ts(values, start=c(2013, 1, 1), frequency=12)'); print(robjects.r['vts']) # Jan Feb Mar Apr May Jun # 2013 0.80179068 -0.04157987 -0.15779190 -0.02982779 -2.03214239 -0.09868078 # 2014 5.97378179 6.27023875 4.85958760 6.50728371 5.14595583 5.29780411 # 2015 1.04548632 0.60093762 0.13941486 0.56116450 -0.20040731 1.19696178 # 2016 -0.09101317 -0.79038658 -0.13305769 0.61016756 -0.13059757 -1.28190161 # Jul Aug Sep Oct Nov Dec # 2013 2.84555901 3.96259305 4.45565104 2.86998914 4.52347928 4.38237841 # 2014 5.16001952 3.44611678 3.49705824 2.37352719 0.75428874 1.62569642 # 2015 -0.03488274 0.13323226 -0.78262492 -0.75325348 -0.65414439 0.40700944 # 2016 -2.31702656 -1.78120320 -1.92904062 -0.83488094 -2.31609640 -3.39114197 # R の stl 関数を実行し、 時系列をトレンド/季節性/残差に分解 robjects.r('result <- stl(vts, s.window=12)'); # 結果を Python の名前空間に戻す result = robjects.r['result'] print(result.names) # [1] "time.series" "weights" "call" "win" "deg" # [6] "jump" "inner" "outer" # 結果の時系列を取得し、pandas.DataFrame へ変換 (index は数値型になってしまう) result_ts = result.rx('time.series')[0] converted = com.convert_robj(result_ts) converted.head() # seasonal trend remainder # 2013.000000 0.716947 -1.123112 1.207956 # 2013.083333 0.264772 -0.603006 0.296655 # 2013.166667 -0.165811 -0.082900 0.090919 # 2013.250000 0.528043 0.437206 -0.995077 # 2013.333333 -0.721440 0.938796 -2.249498 # index を再設定 converted.index = idx converted.head() # seasonal trend remainder # 2013-01-31 0.716947 -1.123112 1.207956 # 2013-02-28 0.264772 -0.603006 0.296655 # 2013-03-31 -0.165811 -0.082900 0.090919 # 2013-04-30 0.528043 0.437206 -0.995077 # 2013-05-31 -0.721440 0.938796 -2.249498
結果をプロットしてみる。
import matplotlib.pyplot as plt fig, axes = plt.subplots(4, 1) axes[0].set_ylabel('Original'); ax = vts.plot(ax=axes[0]); axes[1].set_ylabel('Trend'); ax = converted['trend'].plot(ax=axes[1]); axes[2].set_ylabel('Seasonal'); ax = converted['seasonal'].plot(ax=axes[2]); axes[3].set_ylabel('Residuals'); converted['remainder'].plot(ax=axes[3]) plt.show()
補足
とはいえ、いちいち pandas
-> rpy2
形式へ変換するのは面倒なので、robjects.r
で直接 pandas.DataFrame
を受け渡しできるとうれしい。やり方は、
ENH: automatic rpy2 instance conversion by sinhrks · Pull Request #7385 · pydata/pandas · GitHub
Pythonによるデータ分析入門 ―NumPy、pandasを使ったデータ処理
- 作者: Wes McKinney,小林儀匡,鈴木宏尚,瀬戸山雅人,滝口開資,野上大介
- 出版社/メーカー: オライリージャパン
- 発売日: 2013/12/26
- メディア: 大型本
- この商品を含むブログ (9件) を見る
Python traits で型強制 + traitsui でカンタン GUI 作成
Python の Canopy ディストリビューションで有名な Enthought.inc が作っている traits, traitsui というモジュールが結構便利なのだが、日本語の情報がないのでメモ。
概要
traits
は Python のクラスプロパティに特定の型を強制できるモジュールtraitsui
はtraits
の定義に従って、wxPython
,PyQt
,Pyside
の GUI を簡単にデザインできるモジュール
インストール
pip install traits traitsui
この記事の例ではPyQt を使うので入ってなければ入れる (pip では入らない)。Windows なら MSIインストーラがあるので楽。他OSならソースから build するか、各パッケージ管理で。
traits
Defining Traits: Initialization and Validation — Traits 4 User Manual
ほぼそのままですが。
import traits.api # class定義の際は HasTraits を継承させる class Person(traits.api.HasTraits): # traits を使うプロパティはクラス変数とし、許可される型を traits.api から設定 # 文字列 (str) のみ許可 name = traits.api.Str # 整数のみ許可 age = traits.api.Int # 'M', 'F' もしくは 'X'のみ許可 sex = traits.api.Enum('M', 'F', 'X') # インスタンス初期化 p = Person(name='John', age=22, sex='M') # Str 型が設定されたプロパティを書き換え。str では上書きできるが、別の型を入れるとエラー p.name = 'Mike' # NG! p.name = 0 # TraitError: The 'name' trait of a Person instance must be a string, but a value of 0 <type 'int'> was specified. # Int 型が設定されたプロパティを書き換え。int では上書きられるが、別の型を入れるとエラー p.age = 24 # NG! p.age = 2.0 # TraitError: The 'age' trait of a Person instance must be an integer (int or long), but a value of 2.0 <type 'float'> was specified. # Enum 型が設定されたプロパティを書き換え。初期化の際に許可した値以外はエラー # NG! p.sex = 'B' # TraitError: The 'sex' trait of a Person instance must be 'M' or 'F' or 'X', but a value of 'B' <type 'str'> was specified.
という感じで、クラス定義の際に設定しておけば、煩雑な入力値チェックを自分で書く必要がなくなる。より複雑な条件を設定したい場合は自分でチェック関数を書くこともできる。
さらに、ある変数の変更を検知する Handler や 読み取り専用のProperty (cache可) を定義したりもできる。
class Person2(Person): # Personを継承 # name + age を表示名にしてみる disp = traits.api.Property # _xxx_changed は プロパティ xxx の変更時に自動的に実行 def _name_changed(self, value): print('updated with ' + value) # _get_xxx は 自動的にプロパティ xxx の getter になる def _get_disp(self): return '{0} ({1})'.format(self.name, self.age) # 初期化やプロパティ設定でname を変更すると Person._name_changed が実行される p = Person2(name='John', age=22, sex='M') # updated with John p.name = 'Mike' # updated with Mike # disp を読み取ると Person._get_disp が実行される p.disp # Mike (22) # disp は上書きできない p.disp = 'overwrite' # TraitError: The 'disp' trait of a Person2 instance is 'read only'.
traitsui
traits
で定義したプロパティの型に応じて、適切な GUI を表示してくれる。さきほどの Person2
クラスで .configure_traits
メソッドを実行すると、自動でレイアウトされた GUI がポップアップしてくる。各フィールドはクラスで定義した型に応じてテキスト/プルダウンとして表示される。
p.configure_traits()
traitsui
を使うと、この GUI のレイアウトを変えられる。表示方法は HasTraits
を継承したクラスの traits_view
プロパティで設定する。
表示のレイアウト変更 + ラベル付与 + 性別をラジオボタン選択 + 表示名を読み取り専用にしてみる。どういった設定ができるかは膨大なので 下記ドキュメントを参照。
Introduction to Trait Editor Factories — TraitsUI 4 User Manual
from traitsui.api import View, VGroup, HGroup, Item class Person3(Person2): # VGroupは縦方向のレイアウト # HGroupは横方向のレイアウト traits_view = View(VGroup( HGroup(Item('name', label='氏名')), HGroup(Item('age', label='年齢'), Item('sex', label='性別', style='custom')), HGroup(Item('disp',label='表示名', style='readonly')))) p = Person3(name='John', age=22, sex='M') p.configure_traits()
このGUIからユーザがプロパティを変更した場合、traits
で行った定義に従って入力値のチェックや Handler の実行なんかが自動的に行われる。たとえば Int
で定義された年齢フィールドに文字列を入力しようとすると、エラーとなり入力が許可されない。
使い方 (PyQtへの埋め込み)
traits
, traitsui
でデザインした GUI は PyQt のウィンドウ / ダイアログに組み込むことができる。ので PyQt 上で 細かい GUI のレイアウトやイベント制御の処理を書かなくてすむようになる。
import sys # おまじない import sip sip.setapi('QString', 2) sip.setapi('QVariant', 2) import PyQt4.QtCore as QtCore import PyQt4.QtGui as QtGui class AppForm(QtGui.QMainWindow): def __init__(self, parent=None): QtGui.QMainWindow.__init__(self, parent) # ウィンドウタイトル / サイズを指定 self.setWindowTitle('Person Config') self.resize(400, 200) # メインの widget, layout を作成 self.main_frame = QtGui.QWidget() self.main_layout = QtGui.QVBoxLayout() p = Person3(name='John', age=22, sex='M') # Person3 の画面 widget ( GUI のレイアウト) を取得 control = p.edit_traits(parent=self, kind='subpanel').control # layout に widget 追加 self.main_layout.addWidget(control) # メインの領域に layout 追加 self.main_frame.setLayout(self.main_layout) self.setCentralWidget(self.main_frame) if __name__ == "__main__": app = QtGui.QApplication(sys.argv) form = AppForm() form.show() sys.exit(app.exec_())
まとめ
ちょっとした GUI のついたツールが作りたいとき、traits
, traitsui
を使うとラクだし、設計もシンプルにできる。
Python pandas でのグルーピング/集約/変換処理まとめ
これの pandas 版。
準備
サンプルデータは iris で。
補足 (11/26追記) rpy2
を設定している方は rpy2
から、そうでない方は こちら から .csv
でダウンロードして読み込み (もしくは read_csv
のファイルパスとして直接 URL 指定しても読める)。
import pandas as pd import numpy as np # 表示する行数を設定 pd.options.display.max_rows=5 # iris の読み込みはどちらかで # rpy2 経由で R から iris をロード # import pandas.rpy.common as com # iris = com.load_data('iris') # csv から読み込み # http://aima.cs.berkeley.edu/data/iris.csv names = ['Sepal.Length', 'Sepal.Width', 'Petal.Length', 'Petal.Width', 'Species'] iris = pd.read_csv('iris.csv', header=None, names=names) iris # Sepal.Length Sepal.Width Petal.Length Petal.Width Species # 1 5.1 3.5 1.4 0.2 setosa # 2 4.9 3.0 1.4 0.2 setosa # .. ... ... ... ... ... # 149 6.2 3.4 5.4 2.3 virginica # 150 5.9 3.0 5.1 1.8 virginica
グルーピング/集約
ある列の値ごとに集計
Species 列ごとに Sepal.Length 列の合計を算出する場合、
iris.groupby('Species')['Sepal.Length'].sum() # Species # setosa 250.3 # versicolor 296.8 # virginica 329.4 # Name: Sepal.Length, dtype: float64
全列の合計を取得する場合 DataFrame.groupby
から直接集約関数を呼べばよい。集約できない列は勝手にフィルタされる。
iris.groupby('Species').sum() # Sepal.Length Sepal.Width Petal.Length Petal.Width # Species # setosa 250.3 171.4 73.1 12.3 # versicolor 296.8 138.5 213.0 66.3 # virginica 329.4 148.7 277.6 101.3
集約対象列の指定は DataFrame
の列選択と同じ。すっきり。
iris.groupby('Species')[['Petal.Width', 'Petal.Length']].sum() # Petal.Width Petal.Length # Species # setosa 12.3 73.1 # versicolor 66.3 213.0 # virginica 101.3 277.6
メソッド呼び出しではなく、別に用意された集約関数を渡したい場合は .apply
。文字列で渡したいときは 渡す際に eval
。
iris.groupby('Species')[['Petal.Width', 'Petal.Length']].apply(np.sum) # Petal.Width Petal.Length # Species # setosa 12.3 73.1 # versicolor 66.3 213.0 # virginica 101.3 277.6 iris.groupby('Species')[['Petal.Width', 'Petal.Length']].apply(eval('np.sum')) # Petal.Width Petal.Length # Species # setosa 12.3 73.1 # versicolor 66.3 213.0 # virginica 101.3 277.6
また、集約関数を複数渡したい場合は .agg
。列名 : 集約関数の辞書を渡すので、列ごとに集約関数を変えることもできる。
iris.groupby('Species').agg({'Petal.Length': [np.sum, np.mean], 'Petal.Width': [np.sum, np.mean]}) # Petal.Length Petal.Width # sum mean sum mean # Species # setosa 73.1 1.462 12.3 0.246 # versicolor 213.0 4.260 66.3 1.326 # virginica 277.6 5.552 101.3 2.026
行持ち / 列持ち変換
複数列持ちの値を行持ちに展開 (unpivot / melt)
複数列で持っている値を行持ちに展開する処理は、pd.melt
。 DataFrame.melt
ではないので注意。
melted = pd.melt(iris, id_vars=['Species'], var_name='variable', value_name='value') melted # Species variable value # 0 setosa Sepal.Length 5.1 # 1 setosa Sepal.Length 4.9 # .. ... ... ... # 598 virginica Petal.Width 2.3 # 599 virginica Petal.Width 1.8 # # [600 rows x 3 columns]
複数行持ちの値を列持ちに変換 (pivot)
DataFrame.pivot
。集約処理付きの別関数 pd.pivot_table
もある。
# pivotするデータの準備。Species (列にする値) と variable (行にする値) の組がユニークでないとダメ。 unpivot = melted.groupby(['Species', 'variable']).sum() unpivot = unpivot.reset_index() unpivot # Species variable value # 0 setosa Petal.Length 73.1 # 1 setosa Petal.Width 12.3 # .. ... ... ... # 10 virginica Sepal.Length 329.4 # 11 virginica Sepal.Width 148.7 # # [12 rows x 3 columns] unpivot.pivot(index='variable', columns='Species', values='value') # Species setosa versicolor virginica # variable # Petal.Length 73.1 213.0 277.6 # Petal.Width 12.3 66.3 101.3 # Sepal.Length 250.3 296.8 329.4 # Sepal.Width 171.4 138.5 148.7
列の分割 / 結合
列の値を複数列に分割
pandas
には tidyr::separate
に直接対応する処理はない。.str.split
では分割された文字列が一つの列にリストとして格納されてしまう。そのため、分割結果のリストを個々の列に格納しなおす必要がある。
2014/11/17修正: v0.15.1 以降では str.split
の return_type='frame'
オプションを利用して簡単にできるようになったので修正。既定 ( return_type='series'
)では、split
されたリストが object
型として 1列に保存されてしまうので注意。
2015/01/16追記: v0.16.1 以降では return_type
オプションが deprecate され、 expand
オプションに置き換えられた。expand=True
を指定すれば同様の処理ができる。
melted2 = melted.copy() melted2 # Species variable value # 0 setosa Sepal.Length 5.1 # 1 setosa Sepal.Length 4.9 # .. ... ... ... # 598 virginica Petal.Width 2.3 # 599 virginica Petal.Width 1.8 # # [600 rows x 3 columns] melted2[['Parts', 'Scale']] = melted2['variable'].str.split('.', return_type ='frame') melted2 # Species variable value Parts Scale # 0 setosa Sepal.Length 5.1 Sepal Length # 1 setosa Sepal.Length 4.9 Sepal Length # .. ... ... ... ... ... # 598 virginica Petal.Width 2.3 Petal Width # 599 virginica Petal.Width 1.8 Petal Width # # [600 rows x 5 columns] # 不要な列を削除 melted2.drop('variable', axis=1) # Species value Parts Scale # 0 setosa 5.1 Sepal Length # 1 setosa 4.9 Sepal Length # .. ... ... ... ... # 598 virginica 2.3 Petal Width # 599 virginica 1.8 Petal Width # # [600 rows x 4 columns]
.str.extract
で正規表現を使ってもできる。
melted3 = melted.copy() melted3[['Parts', 'Scale']] = melted3['variable'].str.extract('(.+)\.(.+)') melted3 = melted3.drop('variable', axis=1) melted3 # Species value Parts Scale # 0 setosa 5.1 Sepal Length # 1 setosa 4.9 Sepal Length # .. ... ... ... ... # 598 virginica 2.3 Petal Width # 599 virginica 1.8 Petal Width # # [600 rows x 4 columns]
複数列の値を一列に結合
普通に文字列結合すればよい。
melted3['variable'] = melted3['Parts'] + '.' + melted3['Scale'] melted3 # Species value Parts Scale variable # 0 setosa 5.1 Sepal Length Sepal.Length # 1 setosa 4.9 Sepal Length Sepal.Length # .. ... ... ... ... ... # 598 virginica 2.3 Petal Width Petal.Width # 599 virginica 1.8 Petal Width Petal.Width # # [600 rows x 5 columns]
Pythonによるデータ分析入門 ―NumPy、pandasを使ったデータ処理
- 作者: Wes McKinney,小林儀匡,鈴木宏尚,瀬戸山雅人,滝口開資,野上大介
- 出版社/メーカー: オライリージャパン
- 発売日: 2013/12/26
- メディア: 大型本
- この商品を含むブログ (9件) を見る
簡単なデータ操作を Python pandas で行う
先ほどの R の記事と同じ操作を Python pandas でやる。
Python の場合は Rのようなシンボルの概念がないので、変数が評価される環境を意識する必要が(あまり)ない。
準備
サンプルデータは iris で。
補足 (11/26追記) rpy2
を設定している方は rpy2
から、そうでない方は こちら から .csv
でダウンロードして読み込み (もしくは read_csv
のファイルパスとして直接 URL 指定しても読める)。
import pandas as pd # 表示する行数を設定 pd.options.display.max_rows=5 # iris の読み込みはどちらかで # rpy2 経由で R から iris をロード # import pandas.rpy.common as com # iris = com.load_data('iris') # csv から読み込み # http://aima.cs.berkeley.edu/data/iris.csv names = ['Sepal.Length', 'Sepal.Width', 'Petal.Length', 'Petal.Width', 'Species'] iris = pd.read_csv('iris.csv', header=None, names=names) iris # Sepal.Length Sepal.Width Petal.Length Petal.Width Species # 1 5.1 3.5 1.4 0.2 setosa # 2 4.9 3.0 1.4 0.2 setosa # .. ... ... ... ... ... # 149 6.2 3.4 5.4 2.3 virginica # 150 5.9 3.0 5.1 1.8 virginica
列操作
列名操作
参照と変更。変更前後の列名は辞書で渡すので、可変長でも楽。
iris.columns # Index([u'Sepal.Length', u'Sepal.Width', u'Petal.Length', u'Petal.Width', u'Species', u'Petal.Mult'], dtype='object') iris.rename(columns={'Species': 'newcol'}) # Sepal.Length Sepal.Width Petal.Length Petal.Width newcol # 1 5.1 3.5 1.4 0.2 setosa # 2 4.9 3.0 1.4 0.2 setosa # .. ... ... ... ... ... # 149 6.2 3.4 5.4 2.3 virginica # 150 5.9 3.0 5.1 1.8 virginica
変数名を用いて列選択
iris['Species'] # 1 setosa # ... # 150 virginica # Name: Species, Length: 150, dtype: object
文字列リストを用いて、複数列を選択する
iris[['Petal.Length', 'Petal.Width']] # Petal.Length Petal.Width # 1 1.4 0.2 # 2 1.4 0.2 # .. ... ... # 149 5.4 2.3 # 150 5.1 1.8 # # [150 rows x 2 columns]
真偽値リストを用いて列選択する
これは R のほうがシンプル。
iris.loc[:,[False, False, True, True, False]] # Petal.Length Petal.Width # 1 1.4 0.2 # 2 1.4 0.2 # .. ... ... # 149 5.4 2.3 # 150 5.1 1.8 # # [150 rows x 2 columns]
列の属性/値が特定の条件に該当する列を選択する
型が float
の列のみ取り出す。
iris.loc[:,iris.dtypes == float] # Sepal.Length Sepal.Width Petal.Length Petal.Width # 1 5.1 3.5 1.4 0.2 # 2 4.9 3.0 1.4 0.2 # .. ... ... ... ... # 149 6.2 3.4 5.4 2.3 # 150 5.9 3.0 5.1 1.8 # # [150 rows x 4 columns]
public ではないが数値型の列のみ取り出すメソッドもある。
iris._get_numeric_data() # Sepal.Length Sepal.Width Petal.Length Petal.Width Petal.Mult # 1 5.1 3.5 1.4 0.2 0.28 # 2 4.9 3.0 1.4 0.2 0.28 # .. ... ... ... ... ... # 149 6.2 3.4 5.4 2.3 12.42 # 150 5.9 3.0 5.1 1.8 9.18 # # [150 rows x 5 columns]
行操作
値が特定の条件を満たす行を抽出する
iris[iris['Species'] == 'virginica'] # Sepal.Length Sepal.Width Petal.Length Petal.Width Species # 101 6.3 3.3 6.0 2.5 virginica # 102 5.8 2.7 5.1 1.9 virginica # .. ... ... ... ... ... # 149 6.2 3.4 5.4 2.3 virginica # 150 5.9 3.0 5.1 1.8 virginica # # [50 rows x 5 columns]
特定の行番号を抽出する
iris.loc[[2, 3, 4]] # Sepal.Length Sepal.Width Petal.Length Petal.Width Species # 2 4.9 3.0 1.4 0.2 setosa # 3 4.7 3.2 1.3 0.2 setosa # 4 4.6 3.1 1.5 0.2 setosa
ランダムサンプリングしたい場合は index
をサンプリングしてスライス。
import random iris.loc[random.sample(iris.index, 5)] # Sepal.Length Sepal.Width Petal.Length Petal.Width Species # 64 6.1 2.9 4.7 1.4 versicolor # 17 5.4 3.9 1.3 0.4 setosa # 14 4.3 3.0 1.1 0.1 setosa # 4 4.6 3.1 1.5 0.2 setosa # 146 6.7 3.0 5.2 2.3 virginica
代入
iris['Petal.Mult'] = iris['Petal.Width'] * iris['Petal.Length'] iris # Sepal.Length Sepal.Width Petal.Length Petal.Width Species \ # 1 5.1 3.5 1.4 0.2 setosa # 2 4.9 3.0 1.4 0.2 setosa # .. ... ... ... ... ... # 149 6.2 3.4 5.4 2.3 virginica # 150 5.9 3.0 5.1 1.8 virginica # # Petal.Mult # 1 0.28 # 2 0.28 # .. ... # 149 12.42 # 150 9.18 # # [150 rows x 6 columns]
まとめ
pandas
いいですよ。
Pythonによるデータ分析入門 ―NumPy、pandasを使ったデータ処理
- 作者: Wes McKinney,小林儀匡,鈴木宏尚,瀬戸山雅人,滝口開資,野上大介
- 出版社/メーカー: オライリージャパン
- 発売日: 2013/12/26
- メディア: 大型本
- この商品を含むブログ (9件) を見る
Pythonでdata.go.jpからデータを取得する
データカタログサイト data.go.jp が本稼働したので、そこからデータを pandasのデータフレームとして取得するモジュールを書いた。
data.go.jp に限らず data.go...系は CKAN で構築されていることが多いのだが、PyPI 上には使いやすそうなクライアントが現状見当たらない。
インストール
pip install pyopendata
ドキュメント
http://pyopendata.readthedocs.org/en/latest/
データの取得
サンプルとして 鉱工業指数の原指数を業種別/月次 で取ってくる。data.go.jp上のURLのうち、"meti_20140901_0895"がパッケージのID, "aad25837-7e83-4881-9372-1839ecb9b5eb"がリソースのIDになる。リソースのIDがわかればファイルが一意に特定される (ハズ)。
上記リンク先でデータがプレビュー/ダウンロードできるが、"生産", "出荷", "在庫", "在庫率" 4シートからなるExcelになっている。また、ヘッダは3行目から始まっているので冒頭2行は読み飛ばす必要がある。
※対象のファイルは pandasの read_excel, read_csvでパースできるものでないとだめ。
# おまじない from __future__ import unicode_literals import pyopendata as pyod import pandas as pd pd.options.display.mpl_style = 'default' import matplotlib.pyplot as plt plt.ioff() # DataStoreの初期化 store = pyod.CKANStore('http://www.data.go.jp/data') # 取得するリソースIDを指定 resource = store.get('aad25837-7e83-4881-9372-1839ecb9b5eb') # シート名: 不要カラムの辞書 sheets = {'生産': '付加生産ウエイト', '出荷': '出荷ウエイト', '在庫': '在庫ウエイト'} for sheet in sheets: # sheet名を指定して開く。最初の二行は読み飛ばす df = resource.read(sheetname=sheet, skiprows=[0, 1]) # 不要なカラムを削除 df = df.drop(['品目番号', sheets[sheet]], axis=1) # index設定 df = df.set_index('品目名称') # 転置して、時間が行として並ぶようにする df = df.T # 数値に変換できるものを変換 df = df.convert_objects(convert_numeric=True) # 適当な列をフィルタ df = df[['製造工業', '電気機械工業', '機械工具', '乗用車']] # プロット ax = df.plot() ax.set_title(sheet) plt.show()
こんな感じで順番にプロットされる。