pythonから統計解析ソフトRを利用する方法・第1回

2018/02/18

 統計解析ソフトR(以下、統計R)をpythonから利用する場合、
rpy2 とか pyper というライブラリを利用できます。

 ただ、マルチバイト文字の日本語を扱おうとすると苦労します。

 そこで、rpy2 を利用しつつ、日本語を扱いやすくするため
pyrcmd.py を作りました。

 また、統計処理した結果をmarkdown経由でhtmlなどに変換するのに
ちょっと便利そうな関数をpyrcmd.pyに組み込みました。

 それについて解説したいとおもいますが、
今回は前提として pythonライブラリ rpy2 の簡単な使い方に触れます。

 なお、当サイトで紹介したスクリプトは
pyrcmd01.zip に入れてあります。


《このページの目次》


    

はじめに

 まず、必要な環境について書いて置きます。

(1) 統計Rに関連する環境

 何はともあれ、統計Rが動作する環境が必要です。

The Comprehensive R Archive Network からインストールに必要なファイルをダウンロードできます。

 インストールしたあと、Windowsでいえば R.exe にpathを通しておきます。

 それから、次の三つの環境変数を設定します。

 上は Windowsにおける例です。

 ディレクトリ名や統計Rのバージョン番号は、適宜 変更します。

 R_USER には、OS起動時に指定する
ユーザー名をセットしておけば大丈夫だとおもいます。

 Windowsの場合、環境変数 USERNAME にそれがセットされています。

 ちなみに、当サイトに掲げるスクリプトは、linux, FreeBSD などの
UNIX系OSでも使えるとおもいますが、確認はしてません。

    

(2) pythonのバージョンと必須のライブラリ

 当サイトに掲げるスクリプトは、
python2.7.13, python3.6.4 の両方で動作確認しました。

 pyrcmd.pyにとって必須の pythonライブラリは次の三つです(標準ライブラリ以外)。

 参考まで、私が用いたバージョン番号を付記しておきます。

 pandas, chardet は、次のようにしてインストールできます。

python -m pip install pandas --upgrade
python -m pip install chardet --upgrade

    

 rpy2の場合、Windowsではインストールに whl ファイルが必要になるので、
Python Extension Packages for Windows - Christoph Gohlke から自分の環境に合った whl ファイルをダウンロードしてインストールします。

 2018/02/11 現在でいうと

 python2.7, Windows 32bitに対応したものだと
rpy2-2.7.8-cp27-none-win32.whl をダウンロードして

python -m pip install rpy2-2.7.8-cp27-none-win32.whl --upgrade

 上のように実行します。

 python3.6, Windows 32bit なら
rpy2-2.8.6-cp36-cp36m-win32.whl です。

    

(3) サンプルスクリプトで用いているpythonライブラリ

 出力を html, Excel, docx(Wordファイル)にするため
下の pythonライブラリを使う場合があります。

 いずれも pandas と同じように pip install …… でインストールできます。

 外部コマンド pandoc は、Windowsの場合、
Releases - jgm/pandoc - GitHubから
pandoc-2.0.3-windows.msi をダウンロードしてインストールできます。

 2018/02/11 現在、 ver 2.1.1 が最新のようですが、
私のところでは正常に動作しなかったので 2.0.3 を使いました。
ver 2.0.6 も私のところではダメです。なぜなんだろう?

目次に戻る


1. rpy2 のごく基本的な使い方

 pyrcmd.py の説明に入る前提として、rpy2 について触れておきます。

 rpy2 は pythonのライブラリで、統計Rとの間を取り持つものです。

 統計Rのスクリプトを実行したり、
統計R側の変数の値を取り出したりできます。

 他にもいろんな機能がありますが、ここではその二つを取り上げます。

    

(1) 統計Rのスクリプトの実行と変数の参照

 まず、rpy2 を使うためには pythonスクリプトの先頭の方に次ぎの1行を置きます。

from rpy2.robjects import r

 そうすると、r という変数(?)を使って
rpy2 の機能を利用できるようになります。

 統計Rのスクリプトを実行するときは、普通の括弧を使って
たとえば次のようにします。

from rpy2.robjects import r
r('x <- 123')

 これで統計Rの側の変数 x に 123 を代入できます。

 そして、その変数 x の値をpython側で取得したい場合は、
角括弧を使って下のようにします。

x = r['x']

 スクリプトを実行するときは普通の括弧、
変数を取り出すときは角括弧です。

 ただし、これで python側の変数 x に数値 123 が代入されるかというと、 そうではありません。

    

 python側の x は、FloatVector という独特の型になっています。

 これは rpy2 が提供する独自の型です。

 print(x) として x を表示させると
[1] 123 という統計Rと類似の表示が行われます。

 では、数値 123 を取り出したいときはどうするか……

x2 = x[0]

 上のようにすると、変数 x2 に 123.0 が代入されます。

 x2 の型は、pythonの一般的な float型です。

目次に戻る


(2) 複数の要素からなるベクトルを扱う場合

 次は、統計Rのスクリプトで、複数の値を保持するベクトルを扱ってみます。

 pythonスクリプトは下のとおり。

---- ここから
from rpy2.robjects import r
rscript = 'x <- c("cat", "dog", "fox")'
r(rscript)
x = r['x']
print(x)
print(type(x))
x2 = list(x)
print(x2)
print(type(x2))
-------- ここまで

 今度は x2 = x[0] ではなく
x2 = list(x) としています。

 複数の要素からなるベクトルを扱うので list() を使います。

 実行結果(表示)は下のとおり。

[1] "cat" "dog" "fox"
<class 'rpy2.robjects.vectors.StrVector'>
['cat', 'dog', 'fox']
<type 'list'>

    

 今回、変数 x の型は StrVector です。

 これに list() を適用すると、全要素とも pythonの文字列型になります。

 そうではなく、rpy2独自の Vector型のままで要素を一つずつ参照したいなら
rpy2が提供する rx2() を使います。

 具体的には下のとおり。

-------- ここから
from rpy2.robjects import r
rscript = 'x <- c("cat", "dog", "fox")'
r(rscript)
x = r['x']
for i in range(1, len(x)+1):
    x2 = x.rx2(i)
    print(x2)
-------- ここまで

 rx2() の引数は、0からではなく 1から始まります。

 実行結果(空白行を除いて表示)は次のとおり。

[1] "cat"
[1] "dog"
[1] "fox"

目次に戻る


(3) 名前付きベクトルを扱う

 統計Rではベクトルに名前が付いているものがあります。

 100個の数値からなるベクトルを summary() にかけると
最小値、平均値、最大値などを得ることができますが、
summary() が返すのが名前付きベクトルです。

 乱数発生により 100個の数値からなるベクトルを生成し、
その summary の結果を python側で pandas.Series に変換する例を掲げます。

-------- ここから
import pandas as pd
from rpy2.robjects import r
from collections import OrderedDict

rscript = '''\
height <- rnorm(100, 165.0, 8.0)
height_summary <- summary(height)
'''
r(rscript)
height_summary = r['height_summary']
keys = list(height_summary.names)
values = list(height_summary)
ser = pd.Series(OrderedDict(zip(keys, values)))
print(ser)
-------- ここまで

 python側の変数 height_summary の型は FloatVector ですが、
height_summary.names によってベクトルに付いている名前を取得できます。

 実行結果の表示例は下のとおり。

Min.       138.0
1st Qu.    158.9
Median     165.0
Mean       164.8
3rd Qu.    169.9
Max.       185.9
dtype: float64

目次に戻る


(4) DataFrameの処理方法その1

 統計Rのデータフレームを rpy2 で取り込んだ場合、
行ラベルは xx.rownames、列ラベルは xx.colnames で取得できます。

 そして、各列をベクトルとして得るときは xx.rx2(……) を使えます。

 「……」は、番号でも列名でもどちらでもかまいません。

 ということを利用すれば、前述の Series 生成の方法を応用して
統計Rのデータフレームをpythonの pandas.DataFrame に変換できます。

 pythonスクリプトの例を掲げてみます。

 男女それぞれ 10人の身長を乱数発生で生成し、
それをデータフレームにしたあと、行ラベルとして a, b, c, …… を付加します。

 そのデータフレームをpython側で pandas.DataFrame にするというものです。

-------- ここから
import pandas as pd
from rpy2.robjects import r
from collections import OrderedDict

rscript = '''\
male <-  round(rnorm(10, 165.0, 8.0), 1)
female <- round(rnorm(10, 161.0, 9.0), 1)
rdtf <- data.frame(male, female)
rownames(rdtf) <- c("a", "b", "c", "d", "e", "f", "g", "h", "i", "j")
'''
r(rscript)
rdtf = r['rdtf']
rownames = list(rdtf.rownames)
colnames = list(rdtf.colnames)
pod = OrderedDict()
for name in colnames:
    cobj = rdtf.rx2(name)
    pod[name] = list(cobj)
pdtf = pd.DataFrame(pod)
pdtf.index = rownames
print(pdtf)
-------- ここまで

 今回は違いますが、統計Rの側で rownames を設定しない場合、
rownames は 1 から始まる通し番号になります。

 一方、pythonでは、普通 0 から始まる通し番号が使われるので
統計Rの rownames をそのまま用いるかどうか悩ましいところです。

目次に戻る


(5) DataFrameの処理方法その2

 データフレームの処理方法をもう一つ。

 こちらの方が正統派のやり方だとおもいます。

 rpy2.robjects から r の他に pandas2ri というのも import します。

from rpy2.robjects import r, pandas2ri

 こうしておけば、rpy2のDataFrameオブジェクトを
簡単に pandas.DataFrame に変換できます。

-------- ここから
import pandas as pd
from rpy2.robjects import r, pandas2ri

rscript = '''\
male <-  round(rnorm(10, 165.0, 8.0), 1)
female <- round(rnorm(10, 161.0, 9.0), 1)
rdtf <- data.frame(male, female)
rownames(rdtf) <- c("a", "b", "c", "d", "e", "f", "g", "h", "i", "j")
'''
r(rscript)
rdtf = r['rdtf']
pdtf = pandas2ri.ri2py(rdtf)
print(pdtf)
-------- ここまで

 前の pythonスクリプトに比べると、ずいぶん簡単になりました。

 ただ、日本語のようなマルチバイト文字を扱えません。

 前に掲げた長いスクリプトもマルチバイト文字を扱えませんが、
骨格はそのままにして、あれこれ工夫するとマルチバイト文字に対応できます。

 pandas2ri を使った方は、工夫を施すのが難しいとおもいます。

 とはいえ、マルチバイト文字を使わないなら pandas2ri が断然 便利です。

目次に戻る


(6) ListVectorの扱い

 統計Rには list というのがあります。pythonの list と同じような感じです。

 第1要素に文字列、第2要素に数値、第3要素にはベクトルなどと、
いろいろな型のデータを保持できます。

 便利な反面、プログラムを書く立場からは扱いにくい。

 そして、困ったことに、ほとんどの統計検定の結果が list として返されます。

 F検定、t検定、カイ2乗検定、回帰分析、ANOVA ……
いずれも list を返します。

 統計Rの list を rpy2 で受け取ると、ListVector という型のデータになります。

 rpy2を実用的に使うなら、ListVector をどうにか処理できないと……

 これが pyrcmd.py を作った動機の一つです。

 それはともかく、簡単な ListVector の処理サンプルを掲げます。

 文字列、数値、ベクトルを保持する list に対応します。

 listの要素に名前が付いている場合と付いていない場合がありますが、
付いている場合に備えて、pythonの OrderedDict に変換します。

-------- ここから
import pandas as pd
from rpy2.robjects import r
from collections import OrderedDict

def lvt2od(robj):
    names = robj.names
    if names.__class__.__name__ == 'RNULLType':  # without name
        names = range(1, len(robj)+1)
    pod = OrderedDict()
    for name in names:
        pobj = list(robj.rx2(name))
        if len(pobj) == 1:
            pobj = pobj[0]
        pod[name] = pobj
    return pod

rscript = '''\
lx1 = list("color", c("black", "#000000"), c("white", "#ffffff"))
lx2 <- list(fruit="apple", price=450)
'''
r(rscript)
lx1 = r['lx1']
lx2 = r['lx2']
for lx in [lx1, lx2]:
    lod = lvt2od(lx)
    for key in lod.keys():
        print(key)
        print(lod[key])
        print('')
-------- ここまで

 変数 lx1 が名前なし、lx2 の方は名前付きの list です。

 名前なしの場合、1 から始まる通し番号を名前の代用にしています。

 実践への応用からは ほど遠いですが、ごく簡単ながら
一つの処理方針の提示にはなっているのでは?とおもいます。

    

[注] 統計Rの list では、名前付きの要素と名前なしの要素が混在できます。

 前掲のスクリプトは、そうした混在の list には対応しません。

 サンプルとしては煩雑で長くなるのでやめました。

目次に戻る


2. 日本語にまつわるトラブル

 これまで掲げてきた pythonスクリプトは、残念ながら
データ中に日本語が入ると正しく処理されません。

 python2 では文字化けくらいで済みますが、
python3 だとエラー発生によりスクリプトを実行できません。

    

(1) バイト列からunicode文字列への変換

 とりあえず python2 での実行を考えます。

 また、Windows(日本語版)での実行を仮定します。

 下の pythonスクリプトは、統計Rのベクトルを
pandas.Series に変換するものですが、文字化けになります。

-------- ここから
# encoding: utf-8
import pandas as pd
from rpy2.robjects import r

rscript = u'''\
options(encoding="utf-8")
vect <- c("パイソン(カタカナ)", "python")
'''
r(rscript)
vect = r['vect']
lvect = list(vect)
ser = pd.Series(lvect)
print(ser)
-------- ここまで

    

 実は、Seriesに変換せず print(lvect[0]) とすれば
日本語が文字化けせずに表示されます。

 rpy2のStrVector型を list() で変換すると、
python2 では、文字列がバイト列に変換されます。
(unicode文字列ではありません。)

 一方、pandas の Series, DataFrame では、バイト列を
unicode文字列に変換した上で記録するようです。

 pd.Series() が暗黙のうちに行う unicode変換に任せるのでなく
自前で unicode変換した上で Series() に引き渡せば文字化けしません。

 具体的には、上記スクリプトの最後の方を次ぎのようにします。

-------- ここから
  (前略)
lvect = list(vect)
for i in range(0, len(lvect)):
    lvect[i] = lvect[i].decode('cp932')
ser = pd.Series(lvect)
print(ser)
-------- ここまで

    

 肝は lvect[i].decode('cp932') です。

 今回のスクリプトは、統計R, python ともに utf-8 で書いています。

 にもかかわらず、unicode変換するときに cp932 を指定しないとダメです。

 これは、Windows(日本語版)の R.exe が低レベルのところで
cp932 を基盤にしているからだと推測します。
(おそらく OS の locale を参照している。)

 そのため、rpy2のオブジェクトが cp932 のバイト列になってしまう ……

 UNIX系OSでは、たぶん事情は違います。

 ともあれ、このことが様々なトラブルの要因になります。

 python3 の場合は、lvect = list(vect) のところで、
バイト列を暗黙のうちに unicode変換しようとします。

 unicode変換の前提となるエンコーディングは utf-8 です。
(スクリプトのエンコーディングとは関係ない。)

 でも、実態は cp932 のバイト列なので正しく unicode変換できません。

 結果、エラーが発生してしまいます。

 UNIX系OSであれば、スクリプトを utf-8 で書いているかぎり、
こうしたトラブルに巻き込まれることはないかもしれません。

 このトラブルに対応するには、
1) 統計Rの低レベルのエンコーディングを utf-8 に変更する。
2) python3 の暗黙の unicode変換時のエンコーディングを cp932 に変更する。

 このどちらかです。

 でも、残念ながらどちらも対応方法が分かりませんでした。

目次に戻る


(2) print文を利用した対応

 以下の話は、Windows(日本語版)で python3を使う場合は、
統計R, python の両方とも文字エンコーディングを
cp932 にしないと うまくいかないのでご注意!

 rpy2のオブジェクトは、 print文で出力できます。

 日本語が含まれていても print できます。
(list() による変換だとエラーになります。)

 であれば、一度 print文でファイルに書き出し、
それを読み取って pythonの通常のデータとして取り込む ……

 試してみると、python3 でも日本語を何とか扱えるようになりました。

 ファイル出力を少しでも効率的にするため
オンメモリーの StringIO を使います。

 また、いつも print文による出力をやっていたのでは効率がわるいので
list() の変換で済むならそちらを使います。

 といった処理をプログラムとして示すと煩雑になるので省略しますが、
pyrcmd.py にマルチバイト文字への対応を組み入れました。

 前述のスクリプトを pyrcmd.pi を使って書くとどうなるか掲げてみます。

-------- ここから
# encoding: cp932
import pandas as pd
from rpy2.robjects import r
import pyrcmd as prc
prc.set_charset('cp932')

rscript = u'''\
options(encoding="cp932")
vect <- c("パイソン(カタカナ)", "python")
'''
r(rscript)
vect = r['vect']
ser = prc.rp2pd(vect)
print(ser)
-------- ここまで

 pyrcmdの詳細については次回以降で述べますが、
上のスクリプトは、python2, python3 のどちらでも実行可能です。

 python3の場合、スクリプトのエンコーディングを
cp932にしないとエラーになるのは少々残念ですが。

 prc.rp2pd() という関数は、rpy2のオブジェクトを受け取って
極力 pandasの Series, DataFrame に変換するものです。

 引数として ListVector を渡したときは、OrderedDict を返します。

目次に戻る


3. まだpython2を使う理由

 ちょっと脱線の話題かもしれませんが少々。

 pyrcmd.py は、python2, python3 のどちらにも対応するように書きました。

 実のところ、初心者の私には相当 骨の折れる作業でした。

 既に python3 が主流なので、python2 への対応をやめてしまえば
かなり楽だとおもいます。

 でも、そうできない事情があります。

 私は、WinPythonをUSBメモリーに入れて持ち歩き、
常用のパソコン以外でも使う機会があります。

 Windows7 か Windows10 のパソコンですが、
中に Windows7 32bit のパソコンがあります。

 それで python3 を動かそうとするとエラーが発生します。

 次のようなエラーです。

python.exe - システム エラー
コンピューターに api-ms-win-crt-runtime-l1-1-0.dll がないため、
プログラムを開始できません。

 そこで、該当の dll がほんとにないかパソコン内を探してみると、
二つ見つかりました。

 それを検索pathの通ったフォルダにコピーして python3 を動かしてみると
今度は下のようなエラーです。

python.exe - エントリ ポイントが見つかりません
プロシージャ エントリ ポイント ucrtbase.abort が
ダイナミック リンク ライブラリ api-ms-win-crt-runtime-l1-1-0.dll
から見つかりませんでした。 

 見つかった二つの dll それぞれを試しましたが、どちらもダメです。

 それならということで、python3がちゃんと動作するパソコンの
該当 dll(32bit版)を持ち込んで試してもみましたが、あえなく失敗。

 Webで調べると、Visual Studio を更新すればいいみたいな記述がありますが、
自前のパソコンではないので勝手にやる訳にはいかず ……

 幸いというべきか、python2 ではそうしたトラブルを経験したことがありません。

 という訳で、python2 を今も使っています。

 python2 のサポート期間が 2020年まで延長されているみたいですが、
python2用の rpy2 のバージョンが 2.7.8 で止まっているようです。

 python3用の方は 2.8.6 です。

 この種のライブラリが増えていくのだろうとおもいます。

 まあ、新しければいいというものでもありませんが。

〜 以上 〜

Copyright (C) T. Yoshiizumi, 2018 All rights reserved.