StatsFragments

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

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