StatsFragments

Python, R, Rust, 統計, 機械学習とか

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

まずは普通の groupbyDataFramegroupby すると、結果は 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.yeardt.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で周期的なグルーピング

数日おきなど、特定の周期/間隔でグループ分けしければ Grouperfreqキーワードの記法は Documentation 参照。

Grouperdatetime64 をグループ分けした場合、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 での集約には二つ既知の バグがあるので注意。

  1. グルーピング対象の列の値に NaTが入っているとき var, std, mean で集約できない
  2. 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 に変換する

pandasDataFrame を R へ渡す/また R から Python へデータを戻す方法について、本家のドキュメント が書きかけなのでよくわからない。ということで 以前 下の文書を書いたので訳してみる。

DOC: Complete R interface section by sinhrks · Pull Request #7309 · pydata/pandas · GitHub

rpy2 を使うと pandas (Python) <-> R 間のデータの相互変換を以下 2通りの方法で行うことができる。

  1. R の関数を Python に loadし、Python名前空間上で操作する

    • pandas で作ったデータを rpy2 形式に変換
    • R の 関数/処理を Python の関数として load
    • Python に読み込まれた R の関数を使って、rpy2 形式のデータを Python 上で操作する
  2. Python のデータを R に渡し、 R の名前空間上で操作する

    • pandas で作ったデータを rpy2 形式に変換 (ここは一緒)
    • rpy2 を使って、このデータを R の名前空間に転送
    • rpy2 で R へコマンドを送って、 R 上のデータ/関数を操作する
    • 結果を R から Python名前空間へ戻す

何をどちらに読み込ませるかという話なので、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_dataframeSeries に対応していないので、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()

f:id:sinhrks:20141013120538p:plain

補足

とはいえ、いちいち pandas -> rpy2 形式へ変換するのは面倒なので、robjects.r で直接 pandas.DataFrame を受け渡しできるとうれしい。やり方は、

ENH: automatic rpy2 instance conversion by sinhrks · Pull Request #7385 · pydata/pandas · GitHub

Pythonによるデータ分析入門 ―NumPy、pandasを使ったデータ処理

Pythonによるデータ分析入門 ―NumPy、pandasを使ったデータ処理

Python traits で型強制 + traitsui でカンタン GUI 作成

Python の Canopy ディストリビューションで有名な Enthought.inc が作っている traits, traitsui というモジュールが結構便利なのだが、日本語の情報がないのでメモ。

概要

  • traitsPython のクラスプロパティに特定の型を強制できるモジュール
  • traitsuitraits の定義に従って、wxPython, PyQt, PysideGUI を簡単にデザインできるモジュール

インストール

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()

f:id:sinhrks:20141013185835p:plain

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()

f:id:sinhrks:20141013191414p:plain

このGUIからユーザがプロパティを変更した場合、traits で行った定義に従って入力値のチェックや Handler の実行なんかが自動的に行われる。たとえば Int で定義された年齢フィールドに文字列を入力しようとすると、エラーとなり入力が許可されない。

f:id:sinhrks:20141014213926p:plain

使い方 (PyQtへの埋め込み)

traits, traitsui でデザインした GUIPyQt のウィンドウ / ダイアログに組み込むことができる。ので 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_())

f:id:sinhrks:20141013191423p:plain

まとめ

ちょっとした GUI のついたツールが作りたいとき、traits, traitsui を使うとラクだし、設計もシンプルにできる。

  • モデル/コントローラの各パーツ部分は traits で作る。Handler も traits で定義しておく。
  • traitsui で各パーツの GUI のスタイル/レイアウトを決める。
  • 作ったパーツを PyQt 上に配置する

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.meltDataFrame.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.splitreturn_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を使ったデータ処理

Pythonによるデータ分析入門 ―NumPy、pandasを使ったデータ処理

簡単なデータ操作を 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を使ったデータ処理

Pythonによるデータ分析入門 ―NumPy、pandasを使ったデータ処理

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()

こんな感じで順番にプロットされる。

f:id:sinhrks:20141006221706p:plain