StatsFragments

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

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

R dplyr, tidyr でのグルーピング/集約/変換処理まとめ

これの続き。よく使う集約/変換処理もまとめておく。

準備

library(dplyr)
library(tidyr)

(df <- dplyr::tbl_df(iris))
# Source: local data frame [150 x 5]
# 
#    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
# ..          ...         ...          ...         ...     ...

グルーピング/集約

ある列の値ごとに集計

Species 列ごとに Sepal.Length 列の合計を算出する場合、

df %>%
  dplyr::group_by(Species) %>%
  dplyr::summarise(Sepal.Length.Total = sum(Sepal.Length))
# Source: local data frame [3 x 2]
# 
#      Species Sepal.Length.Total
# 1     setosa              250.3
# 2 versicolor              296.8
# 3  virginica              329.4

# Standard Evaluation
df %>%
  dplyr::group_by_('Species') %>%
  dplyr::summarise_('Sepal.Length.Total' = 'sum(Sepal.Length)')
# 略

同一の集計を 複数列に対して行う場合は dplyr::summarise_eachが便利。全列の合計を取得するなら、

df %>%
  dplyr::group_by(Species) %>%
  dplyr::summarise_each(dplyr::funs(sum))
# Source: local data frame [3 x 5]
# 
#      Species Sepal.Length Sepal.Width Petal.Length Petal.Width
# 1     setosa        250.3       171.4         73.1        12.3
# 2 versicolor        296.8       138.5        213.0        66.3
# 3  virginica        329.4       148.7        277.6       101.3

vars 引数で集計対象列を指定することもできる。通常版 (Non-standard evaluation/NSE) と Standard evaluation で列名のつき方が違う、、、。

df %>%
  dplyr::group_by(Species) %>%
  dplyr::summarise_each(dplyr::funs(sum),
  vars = c(Petal.Width = Petal.Width, Petal.Length = Petal.Length))
# Source: local data frame [3 x 3]
# 
#      Species vars.Petal.Width vars.Petal.Length
# 1     setosa             12.3              73.1
# 2 versicolor             66.3             213.0
# 3  virginica            101.3             277.6

# Standard evaluation
df %>%
  dplyr::group_by_('Species') %>%
  dplyr::summarise_each_(dplyr::funs(sum), vars = c('Petal.Width', 'Petal.Length'))
# Source: local data frame [3 x 5]
# 
#      Species Petal.Width_sum Petal.Length_sum Petal.Width_mean Petal.Length_mean
# 1     setosa            12.3             73.1            0.246             1.462
# 2 versicolor            66.3            213.0            1.326             4.260
# 3  virginica           101.3            277.6            2.026             5.552

関数名を文字列渡しする場合は dplyr::funs_

df %>%
  dplyr::group_by(Species) %>%
  dplyr::summarise_each(dplyr::funs_('sum'),
                        vars=c(Petal.Width = Petal.Width, Petal.Length = Petal.Length))
# 略

# Standard evaluation
df %>%
  dplyr::group_by_('Species') %>%
  dplyr::summarise_each_(dplyr::funs_('sum'), vars=c('Petal.Width', 'Petal.Length'))
# 略

また、集約関数は複数渡すこともできる。

df %>%
  dplyr::group_by(Species) %>%
  dplyr::summarise_each(funs = c(dplyr::funs(sum), dplyr::funs(mean)),
                         vars=c(Petal.Width = Petal.Width, Petal.Length = Petal.Length))
# Source: local data frame [3 x 5]
# 
#      Species vars.Petal.Width_sum vars.Petal.Length_sum vars.Petal.Width_mean vars.Petal.Length_mean
# 1     setosa                 12.3                  73.1                 0.246                  1.462
# 2 versicolor                 66.3                 213.0                 1.326                  4.260
# 3  virginica                101.3                 277.6                 2.026                  5.552

# Standard evaluation
df %>%
  dplyr::group_by_('Species') %>%
  dplyr::summarise_each_(funs = c(dplyr::funs(sum), dplyr::funs(mean)),
                         vars=c('Petal.Width', 'Petal.Length'))
# Source: local data frame [3 x 5]
# 
#      Species Petal.Width_sum Petal.Length_sum Petal.Width_mean Petal.Length_mean
# 1     setosa            12.3             73.1            0.246             1.462
# 2 versicolor            66.3            213.0            1.326             4.260
# 3  virginica           101.3            277.6            2.026             5.552

NSEの dplyr::groupby に文字列を渡した場合、特にエラーが出ず全集約されるので注意。

# NG!
df %>%
  dplyr::group_by('Species') %>%
  dplyr::summarise_each_(funs = c(dplyr::funs(sum), dplyr::funs(mean)),
                         vars=c('Petal.Width', 'Petal.Length'))
# Source: local data frame [1 x 5]
# 
#   "Species" Petal.Width_sum Petal.Length_sum Petal.Width_mean Petal.Length_mean
# 1   Species           179.9            563.7         1.199333             3.758

行持ち / 列持ち変換

複数列持ちの値を行持ちに展開 (unpivot / melt)

複数列で持っている値を行持ちに展開する処理は、reshape2::melt から名前が変わって tidyr::gather になった。この処理の名前として一般的なのは unpivot ではないのか、、。

tidyr でも Standard evaluationが使える。

df %>%
  tidyr::gather(variable, value, c(Sepal.Length, Sepal.Width, Petal.Length, Petal.Width))
# Source: local data frame [600 x 3]
# 
#    Species     variable value
# 1   setosa Sepal.Length   5.1
# 2   setosa Sepal.Length   4.9
# 3   setosa Sepal.Length   4.7
# ..     ...          ...   ...

# Standard evaluation
df %>%
  tidyr::gather_('variable', 'value', c('Sepal.Length', 'Sepal.Width', 'Petal.Length', 'Petal.Width'))
# 略

複数行持ちの値を列持ちに変換 (pivot)

tidyr::spreadspreadでは集計処理は入らない (行列の入替のみ) ので、まず 行/列として展開する 2列がユニークとなるサンプルデータを用意する。

# spreadへの入力データ。Species (列にする値) と variable (行にする値) の組がユニークでないとダメ。
(unpivot <- df %>%
  tidyr::gather(variable, value, c(Sepal.Length, Sepal.Width, Petal.Length, Petal.Width)) %>%
  dplyr::group_by(Species, variable) %>%
  dplyr::summarise(Total = sum(value)))
# Source: local data frame [12 x 3]
# Groups: Species
# 
#       Species     variable Total
# 1      setosa Sepal.Length 250.3
# 2      setosa  Sepal.Width 171.4
# 3      setosa Petal.Length  73.1
# 4      setosa  Petal.Width  12.3
# 5  versicolor Sepal.Length 296.8
# 6  versicolor  Sepal.Width 138.5
# 7  versicolor Petal.Length 213.0
# 8  versicolor  Petal.Width  66.3
# 9   virginica Sepal.Length 329.4
# 10  virginica  Sepal.Width 148.7
# 11  virginica Petal.Length 277.6
# 12  virginica  Petal.Width 101.3

unpivot %>%
  data.frame %>%
  tidyr::spread(Species, Total)
#       variable setosa versicolor virginica
# 1 Sepal.Length  250.3      296.8     329.4
# 2  Sepal.Width  171.4      138.5     148.7
# 3 Petal.Length   73.1      213.0     277.6
# 4  Petal.Width   12.3       66.3     101.3

# Standard evaluation
unpivot %>%
  data.frame %>%
  tidyr::spread_('Species', 'Total')
# 略

(たぶんバグだと思うが) grouped_dfspread に渡すと以下の通りエラーになるので、上では spread の前に data.frame への変換を挟んでいる。

→ 10/16 追記: バグでしたdplyr 3.1 で修正予定。

class(unpivot)
# [1] "grouped_df" "tbl_df"     "tbl"        "data.frame"

# NG!
unpivot %>%
  tidyr::spread(Species, Total)
#  エラー: index out of bounds

列の分割 / 結合

列の値を複数列に分割

上で作ったデータから variable 列の値をドットで区切って二列に分けたい、なんてときには tidyr::separate。NSE の場合も into に渡す値は文字列 vector でないとダメっぽい (新しい列名を作る処理になるからかなあ、 dplyr::rename では作成する列名もシンボルで渡せるのに)。

df %>%
  tidyr::gather(variable, value, c(Sepal.Length, Sepal.Width, Petal.Length, Petal.Width)) %>%
  tidyr::separate(variable, into = c('Parts', 'Scale'), sep='\\.')
# Source: local data frame [600 x 4]
# 
#    Species Parts  Scale value
# 1   setosa Sepal Length   5.1
# 2   setosa Sepal Length   4.9
# 3   setosa Sepal Length   4.7
# ..     ...   ...    ...   ...

# Standard evaluation
df %>%
  tidyr::gather_('variable', 'value', c('Sepal.Length', 'Sepal.Width', 'Petal.Length', 'Petal.Width')) %>%
  tidyr::separate_('variable', into = c('Parts', 'Scale'), sep='\\.')
# 略

正規表現でグルーピングした値で列分割したい場合は tidyr::extract

df %>%
  tidyr::gather(variable, value, c(Sepal.Length, Sepal.Width, Petal.Length, Petal.Width)) %>%
  tidyr::extract(variable, into = c('Parts', 'Scale'), regex='(.+)\\.(.+)')
# Source: local data frame [600 x 4]
# 
#    Species Parts  Scale value
# 1   setosa Sepal Length   5.1
# 2   setosa Sepal Length   4.9
# 3   setosa Sepal Length   4.7
# ..     ...   ...    ...   ...

# Standard evaluation
df %>%
  tidyr::gather_('variable', 'value', c('Sepal.Length', 'Sepal.Width', 'Petal.Length', 'Petal.Width')) %>%
  tidyr::extract_('variable', into = c('Parts', 'Scale'), regex='(.+)\\.(.+)')
# 略

複数列の値を一列に結合

tidyr::separateと逆の操作はtidyr::uniteでできる。

df %>%
  tidyr::gather(variable, value, c(Sepal.Length, Sepal.Width, Petal.Length, Petal.Width)) %>%
  tidyr::separate(variable, into = c('Parts', 'Scale'), sep='\\.') %>%
  tidyr::unite('variable', c(Parts, Scale), sep='.')
# Source: local data frame [600 x 3]
# 
#    Species     variable value
# 1   setosa Sepal.Length   5.1
# 2   setosa Sepal.Length   4.9
# 3   setosa Sepal.Length   4.7
# ..     ...          ...   ...  

# Standard evaluation
df %>%
  tidyr::gather_('variable', 'value', c('Sepal.Length', 'Sepal.Width', 'Petal.Length', 'Petal.Width')) %>%
  tidyr::separate_('variable', into = c('Parts', 'Scale'), sep='\\.') %>%
  tidyr::unite_('variable', c('Parts', 'Scale'), sep='.')
# 略

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

Rの data.table と data.frame を dplyr で区別なく扱う

R を使っていると、組み込み型の data.frame と大規模データ用パッケージである data.table の差異で思わずはまることがあるので使い方をまとめる。どちらか一方しか使わないようにすれば 差異を気にする必要はないのかも知れないが、、。

基本的には

データ操作用パッケージ dplyrdata.framedata.table 両方に対して同じように使えるので、できるだけ dplyr を使って操作するのがよい。

ある程度 複雑な操作であれば最初から dplyr を使うと思うが、列選択, 行選択, 代入など 比較的シンプルな操作はつい 通常の書式で書いてしまう (そしてはまる、、)。また、列名を文字列に入れて処理するなど、dplyr 0.2以前では(シンプルには)書けない処理もあった。

dplyr 0.3でこのあたりの処理が素直に書けるようになっているので、その方法と 通常の記法で書いた場合にひっかかるポイントをまとめてみた。

まずdplyrのwrapperに渡す

何か処理を始める前には、data.frame, data.tabledplyr で定義された wrapperオブジェクトに渡すこと。それぞれ、dplyr::tbl_df, dplyr::tbl_dt を通しておけば、表示フォーマットなんかをあわせてくれる。特に tbl_dfdata.frame の表示を既定で省略/列選択時の挙動を一貫したものにしてくれる(下記 補足参照)ので必須。

※代入式を括弧で囲んでいるのは 代入 + printするため。通常は必要ない。

(df <- dplyr::tbl_df(iris))
# Source: local data frame [150 x 5]
# 
#    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
# 3           4.7         3.2          1.3         0.2
# ..          ...         ...          ...         ...

(dt <- dplyr::tbl_dt(data.table::data.table(iris)))
# Source: local data table [150 x 5]
# 
#    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
# 3           4.7         3.2          1.3         0.2
# ..          ...         ...          ...         ...

以降のサンプルで、 dfdata.frame, dtdata.table をあらわす。また、返ってくるデータ形式に特に差異がない場合の出力は適宜 省略する。

補足: data.frame では 列選択時に結果が 一列になった場合は vector, 複数列の場合は data.frame を返すという謎の挙動をする。tbl_dfはこれを抑制し、常に data.frame を返してくれる (v0.3.0以降)。

# 素の data.frame
# 結果が複数列の場合は data.frame
iris[, c(FALSE, FALSE, FALSE, TRUE, TRUE)]
#     Petal.Width    Species
# 1           0.2     setosa
# 2           0.2     setosa
# 3           0.2     setosa

# 結果が一列の場合に vectorになる
iris[, c(FALSE, FALSE, FALSE, FALSE, TRUE)]
#   [1] setosa     setosa     setosa     setosa     setosa     setosa     setosa     setosa
# ...
# Levels: setosa versicolor virginica

# tbl_df なら 結果が一列でも data.frame
df[, c(FALSE, FALSE, FALSE, FALSE, TRUE)]
# Source: local data frame [150 x 1]
# 
#    Species
# 1   setosa
# 2   setosa
# 3   setosa
# ..     ...

# data.tableは一貫して data.table
data.table(iris)[, c(FALSE, FALSE, FALSE, FALSE, TRUE), with = FALSE]
# 略 (data.table)

dt[, c(FALSE, FALSE, FALSE, FALSE, TRUE), with = FALSE]
# 略 (data.table)

補足2 一方、data.table[, ]記法で 列名を変数名で指定した場合は vector、文字列で指定した場合は data.tableと挙動が違うので注意。

dt[, Species]
#   [1] setosa     setosa     setosa     setosa     setosa     setosa     setosa     setosa
# ...
# Levels: setosa versicolor virginica

dt[, 'Species', with = FALSE]
# Source: local data table [150 x 1]
# 
#    Species
# 1   setosa
# 2   setosa
# 3   setosa
# .   ...

なんにせよ、一列を選択する処理では常に戻り値の型を意識しないと つい以下のような処理を書いてしまう。

# OK
is.factor(dt[, Species])
# [1] TRUE

# NG!
is.factor(dt[, 'Species', with = FALSE])
# [1] FALSE

列操作

列名操作

参照は names, colnamesが共通の動作をするのでどちらでもよい。

names(df)
# [1] "Sepal.Length" "Sepal.Width"  "Petal.Length" "Petal.Width"  "Species"     

names(dt)
# 略

colnames(df)
# 略
     
colnames(dt)
# 略

列名変更は dplyr::rename (v0.3.0以降)。

dplyr::rename(df, newcol = Species)
# Source: local data frame [150 x 5]
# 
#    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
# 3           4.7         3.2          1.3         0.2 setosa
# ..          ...         ...          ...         ...    ...

dplyr::rename(dt, newcol = Species)
# 略

列名を文字列で渡したい場合は dplyr::rename_rename_renameStandard evaluation 版。(select_, group_by_など、様々な dplyr関数にv0.3.0で追加された)で、渡した文字列を関数/列名として評価するもの。

dplyr::rename_(df, 'newcol' = 'Species')
# 略

dplyr::rename_(dt, 'newcol' = 'Species')
# 略

すべての列名をまとめて変更したい場合は上の記法で個々に指定してもよいが、 dplyr::rename_(.dots)を使うと便利。dotsに渡したリストは引数として展開された後、Standard Evaluationされる。この記法で可変長の文字列引数を dplyr へ渡せる ( Python でいうと **kwargs 記法のような感じ )。

# .dotsに引数として渡すリスト
setNames(names(df), c('col1', 'col2', 'col3', 'col4', 'col5'))
#           col1           col2           col3           col4           col5 
# "Sepal.Length"  "Sepal.Width" "Petal.Length"  "Petal.Width"      "Species" 

dplyr::rename_(df, .dots = setNames(names(df), c('col1', 'col2', 'col3', 'col4', 'col5')))
# Source: local data frame [150 x 5]
# 
#    col1 col2 col3 col4   col5
# 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
# ..  ...  ...  ...  ...    ...

dplyr::rename_(dt, .dots = setNames(names(dt), c('col1', 'col2', 'col3', 'col4', 'col5')))
# 略

補足 dplyr::renamedata.tableに対して裏側で高速化された data.table::setnames を使ってくれている。setnamesは破壊的だが renameは非破壊的。

names(dt)
# "Sepal.Length" "Sepal.Width"  "Petal.Length" "Petal.Width"  "Species"     

names(dplyr::rename(dt, newcol = Species))
# [1] "Sepal.Length" "Sepal.Width"  "Petal.Length" "Petal.Width"  "newcol"      

# rename適用しても元データは変更されない
names(dt)
# [1] "Sepal.Length" "Sepal.Width"  "Petal.Length" "Petal.Width"  "Species"     

# setnamesでは変更される
data.table::setnames(dt, c('col1', 'col2', 'col3', 'col4', 'col5'))
names(dt)
# [1] "col1" "col2" "col3" "col4" "col5"

※ここで列名変更した場合、以降のスクリプトの前に列名を元に戻すこと。

変数名を用いて、列をベクトルとして選択する

dplyr::select もしくは $ を使う。selectは戻り値が data.frameもしくは data.table になるので、結果を vector としてほしい場合は列番号指定が必要なことに注意。

dplyr::select(df, Species)
# Source: local data frame [150 x 1]
# 
#    Species
# 1   setosa
# 2   setosa
# 3   setosa
# ..     ...

dplyr::select(df, Species)[[1]]
#   [1] setosa     setosa     setosa     setosa     setosa     setosa     setosa     setosa
# ...
# Levels: setosa versicolor virginica

df$Species
#   [1] setosa     setosa     setosa     setosa     setosa     setosa     setosa     setosa
# ...
# Levels: setosa versicolor virginica

dt$Species
# 略

補足 data.tableでは[ , ]記法の際に変数名で列選択できるが、data.frame では使えない。

df[, Species]
# Error in `[.tbl_df`(df, , Species) : object 'Species' not found

dt[, Species]
#   [1] setosa     setosa     setosa     setosa     setosa     setosa     setosa     setosa
# ...
# Levels: setosa versicolor virginica

補足2 data.frame[, ] で変数名から列選択したいときは substitute %>% deparse すればできる。ただし戻り値は data.frame になるので、vector がほしければ列指定する。

df[, deparse(substitute(Species))][[1]]
#   [1] setosa     setosa     setosa     setosa     setosa     setosa     setosa     setosa
# ...
# Levels: setosa versicolor virginica
変数名を用いて、列を data.frame/data.table として選択する

dplyr::select を使う。

dplyr::select(df, Petal.Length, Petal.Width)
# Source: local data frame [150 x 2]
# 
#    Petal.Length Petal.Width
# 1           1.4         0.2
# 2           1.4         0.2
# 3           1.3         0.2
# ..          ...         ...

dplyr::select(dt, Petal.Length, Petal.Width)
# 略
文字列を用いて、列をベクトルとして選択する

dplyr::select_ もしくは [[

dplyr::select_(df, 'Species')[[1]]
#   [1] setosa     setosa     setosa     setosa     setosa     setosa     setosa     setosa
# ...
# Levels: setosa versicolor virginica

df[['Species']]
# 略

dt[['Species']]
# 略

また、多少長くなってもよければ通常のselectも使える (次セクション参照)。

dplyr::select(df, one_of(c('Species')))[[1]]
# 略
文字列vectorを用いて、列をdata.frame/data.tableとして選択する

dplyr::select + one_ofone_ofに渡しているものはデータと関係ない文字列vectorなので、select_ではなくselectでよい。

※以下の例の場合は stats_withでもOK.

dplyr::select(df, one_of(c('Petal.Length', 'Petal.Width')))
# Source: local data frame [150 x 2]
# 
#    Petal.Length Petal.Width
# 1           1.4         0.2
# 2           1.4         0.2
# 3           1.3         0.2
# ..          ...         ...

dplyr::select(dt, one_of(c('Petal.Length', 'Petal.Width')))
# 略

dplyr::select(df, starts_with('Petal'))
# 略

また、dplyr::select_(.dots)を使って以下のようにも書ける。

dplyr::select_(df, .dots = c('Petal.Width', 'Petal.Length'))
# 略

dplyr::select_(dt, .dots = c('Petal.Width', 'Petal.Length'))
# 略

補足 data.framedata.table それぞれの書式で書く場合は、data.tablewith = FALSEを指定しないと そのまま文字列vector として評価されてしまう。

# df[, c('Petal.Width', 'Species')]
# Source: local data frame [150 x 2]
# 
#    Petal.Width Species
# 1          0.2  setosa
# 2          0.2  setosa
# 3          0.2  setosa
# ..         ...     ...

# NG!
dt[, c('Petal.Length', 'Petal.Width')]
# [1] "Petal.Length" "Petal.Width" 

dt[, c('Petal.Length', 'Petal.Width'), with = FALSE]
# Source: local data table [150 x 2]
# 
#    Petal.Width Species
# 1          0.2  setosa
# 2          0.2  setosa
# 3          0.2  setosa
# ..         ...     ...
真偽値vectorを用いて、列をdata.frame/data.tableとして選択する

dplyr::select + one_of。ただし真偽値 vector はそのままでは selectに渡せないため、names をかませて列名を取る。

dplyr::select(df, one_of(names(df)[c(FALSE, FALSE, TRUE, TRUE, FALSE)]))
# Source: local data frame [150 x 2]
# 
#    Petal.Length Petal.Width
# 1           1.4         0.2
# 2           1.4         0.2
# 3           1.3         0.2
# ..          ...         ...

dplyr::select(dt, one_of(names(dt)[c(FALSE, FALSE, TRUE, TRUE, FALSE)]))
# 略

補足 通常の記法を使う際の with = FALSEは文字列vectorの場合と同様。

df[, c(FALSE, FALSE, TRUE, TRUE, FALSE)]
# 略

dt[, c(FALSE, FALSE, TRUE, TRUE, FALSE), with = FALSE]
# 略
列の属性/値が特定の条件に該当する列を、data.frame/data.tableとして選択する

型が数値の列のみ取り出したい、なんてときには dplyr::select + one_of + sapply

dplyr::select(df, one_of(names(df)[sapply(df, is.numeric)]))
# Source: local data frame [150 x 4]
# 
#    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
# 3           4.7         3.2          1.3         0.2
# ..          ...         ...          ...         ...

dplyr::select(dt, one_of(names(dt)[sapply(dt, is.numeric)]))
# 略

補足 通常の記法を使う際h(略)

df[, sapply(df, is.numeric)]
# 略

dt[, sapply(df, is.numeric), with = FALSE]
# 略

行操作

値が特定の条件を満たす行を抽出する

dplyr::filter

dplyr::filter(df, Species == 'virginica')
# Source: local data frame [50 x 5]
# 
#    Sepal.Length Sepal.Width Petal.Length Petal.Width   Species
# 1           6.3         3.3          6.0         2.5 virginica
# 2           5.8         2.7          5.1         1.9 virginica
# 3           7.1         3.0          5.9         2.1 virginica
# ..          ...         ...          ...         ...     ...

dplyr::filter(dt, Species == 'virginica')
# 略

列名、式を文字列として渡す場合は dplyr::filter_

dplyr::filter_(df, "Species == 'virginica'")
# 略

dplyr::filter_(dt, "Species == 'virginica'")
# 略

補足 通常の記法では data.frameで条件式後のカンマを忘れると悲惨なことになる。列選択との場合と違って、ここのカンマは忘れやすいと思う、、自分だけ? data.table ではカンマ有無どちらでも同じ挙動。

df[df$Species == 'virginica', ]
# Source: local data frame [50 x 5]
# 
#    Sepal.Length Sepal.Width Petal.Length Petal.Width   Species
# 1           6.3         3.3          6.0         2.5 virginica
# 2           5.8         2.7          5.1         1.9 virginica
# 3           7.1         3.0          5.9         2.1 virginica
# ..          ...         ...          ...         ... ...

# NG!
df[df$Species == 'virginica']
# Source: local data frame [150 x 50]

dt[dt$Species == 'virginica', ]
# Source: local data table [50 x 5]
# 
#    Sepal.Length Sepal.Width Petal.Length Petal.Width   Species
# 1           6.3         3.3          6.0         2.5 virginica
# 2           5.8         2.7          5.1         1.9 virginica
# 3           7.1         3.0          5.9         2.1 virginica
# ..          ...         ...          ...         ... ...

dt[dt$Species == 'virginica']
# Source: local data table [50 x 5]
# 
#    Sepal.Length Sepal.Width Petal.Length Petal.Width   Species
# 1           6.3         3.3          6.0         2.5 virginica
# 2           5.8         2.7          5.1         1.9 virginica
# 3           7.1         3.0          5.9         2.1 virginica
# ..          ...         ...          ...         ... ...
特定の行番号を抽出する

n行目を抽出する処理には dplyr::slice

# dplyr::slice(df, 2:4)
# Source: local data frame [3 x 5]
# 
#   Sepal.Length Sepal.Width Petal.Length Petal.Width Species
# 1          4.9         3.0          1.4         0.2  setosa
# 2          4.7         3.2          1.3         0.2  setosa
# 3          4.6         3.1          1.5         0.2  setosa

dplyr::slice(dt, 2:4)
# 略

ランダムサンプリングしたい場合は、それ用の関数 dplyr::sample_n で直接抽出できる。標準関数でやる場合のようにsampleでインデックスをランダム生成してスライシングする必要はない。

dplyr::sample_n(df, 5)
# Source: local data frame [5 x 5]
# 
#   Sepal.Length Sepal.Width Petal.Length Petal.Width    Species
# 1          4.8         3.4          1.6         0.2     setosa
# 2          5.0         2.3          3.3         1.0 versicolor
# 3          4.9         2.4          3.3         1.0 versicolor
# 4          4.8         3.4          1.9         0.2     setosa
# 5          6.2         3.4          5.4         2.3  virginica

dplyr::sample_n(dt, 5)
# Source: local data table [5 x 5]
# 
#   Sepal.Length Sepal.Width Petal.Length Petal.Width    Species
# 1          5.9         3.2          4.8         1.8 versicolor
# 2          6.1         3.0          4.6         1.4 versicolor
# 3          6.9         3.2          5.7         2.3  virginica
# 4          5.5         2.3          4.0         1.3 versicolor
# 5          4.8         3.4          1.6         0.2     setosa

代入

dplyr::mutate。計算結果を新しい列 "Petal.Mult" として追加するとき、

dplyr::mutate(df, Petal.Mult = Petal.Width * Petal.Length)
# Source: local data frame [150 x 6]
# 
#    Sepal.Length Sepal.Width Petal.Length Petal.Width Species Petal.Mult
# 1           5.1         3.5          1.4         0.2  setosa       0.28
# 2           4.9         3.0          1.4         0.2  setosa       0.28
# 3           4.7         3.2          1.3         0.2  setosa       0.26
# ..          ...         ...          ...         ...     ...        ...

dplyr::mutate(dt, Petal.Mult = Petal.Width * Petal.Length)
# 略

列の値、式が文字列の場合は dplyr::mutate_

dplyr::mutate_(df, 'Petal.Mult' = 'Petal.Width * Petal.Length')
# 略

dplyr::mutate_(dt, 'Petal.Mult' = 'Petal.Width * Petal.Length')
# 略

補足 data.tableの代入式はかっこいいが、列名、式を文字列で扱う場合の挙動が恐ろしすぎるので自分はあまり使っていない。

x = 'Petal.Mult'
y = 'Petal.Witdh * Petal.Length'

# NG!
(data.table(iris)[, x := y])
#      Sepal.Length Sepal.Width Petal.Length Petal.Width   Species                          x
#   1:          5.1         3.5          1.4         0.2    setosa Petal.Witdh * Petal.Length
#   2:          4.9         3.0          1.4         0.2    setosa Petal.Witdh * Petal.Length
#   3:          4.7         3.2          1.3         0.2    setosa Petal.Witdh * Petal.Length
#   ..          ...         ...          ...         ...    ...    ...

# これまでのように with = FALSE しても NG!
(data.table(iris)[, x := y, with = FALSE])
#      Sepal.Length Sepal.Width Petal.Length Petal.Width   Species                 Petal.Mult
#   1:          5.1         3.5          1.4         0.2    setosa Petal.Witdh * Petal.Length
#   2:          4.9         3.0          1.4         0.2    setosa Petal.Witdh * Petal.Length
#   3:          4.7         3.2          1.3         0.2    setosa Petal.Witdh * Petal.Length
#   ..          ...         ...          ...         ...    ...    ...

# OK
(data.table(iris)[, x := get('Petal.Width') * get('Petal.Length'), with = FALSE])
#      Sepal.Length Sepal.Width Petal.Length Petal.Width   Species Petal.Mult
#   1:          5.1         3.5          1.4         0.2    setosa       0.28
#   2:          4.9         3.0          1.4         0.2    setosa       0.28
#   3:          4.7         3.2          1.3         0.2    setosa       0.26
#   ..          ...         ...          ...         ...    ...          ...

まとめ

上述のような基本操作もdplyrによって data.frame, data.table共通の処理に置き換えられるようになった。特に Standard Evaluation関数(アンダースコア付の関数)によって変数名を文字列として扱うことで、かなり柔軟な処理が書けるようになっている。ということで dplyr 使っておけばいい。

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