Rの隅(pipe)

健康診断

健康診断の季節がやってきた。今年はコロナの影響で集団検診はなるべく辞退して、かかりつけ医の所で個別に受けて欲しいとかの案内が同封されてた。それと、お決まりの案内。

検査の前夜は、なるべく夕食を早めに済まして下さい。油ギッシュな物とアルコールはお控えくださいですって。これって、良い記録を出す為の方法だな。オイラーはは、こんなの頑無視。 いつもの状態で受診してこそ、意味が有ると思うから。 まあ、当局は、記録更新に躍起となってるんでしょうな。サツの点数稼ぎ取り締まりと一緒だ。

健康診断のハイライトは、血液検査。血管って体内を巡るBusだと思うぞ。これ、電気屋の発想ね。bus driverは、心臓デバイス、大気とのI/Oには肺が使われる。勿論、中央処理装置兼主記憶兼連想記憶は脳みそデバイスだ。他にも多数のデバイスがぶら下がっている。

化学屋なら、ガスパイプ兼燃料パイプ兼下水管ぐらいに思っているのでしょう。更にウィルスとかの微妙な物質の拡散路も兼ねてるな。

だから、血液を分析すれば、体の状態を余すところなく把握出来る。

砂糖漬けになると、血管が劣化して、やがては破裂するよ。中性脂肪が多いと、粘性が増して、詰まっちゃうよ。だから、きちんと把握して整備しましょって訳だ。

でも、血糖値が幾つだと、どのぐらいで劣化するの? そんなの医者がエィヤァと決める訳にはいかない。個体によって特性は違ってくるし、今日明日の問題とも言い難い。

よって、統計の出番だ。医者の統計好きってよく言われるけど、メスと同様に重要な医者の道具。

オイラーへの審判の日は何時だ? それまで、統計データを揃えて、医者と検討出来るようにしておくかな。こういうの、地に足が付いた統計の利用法だな。python+GPGPUで、猫の画像分類をやるより、よっぽどか実用的だと思われ。

ああ血管で思い出した。低気圧になると頭が痛くなる理由を、気象予報士の方が説明してた。 気圧が低くなると、体への大気の締め付けが緩む。そうすると血管が膨張する。それによって周りの神経が圧迫されて、頭痛になると言う事らしい。

with pipe

と言う訳で、以前やった血圧データの分析をまたやってみる。今回は、csvファイルの内容を R内のpipeを使ってsedで変形したい。ちょいとすっきりsedで書けるようになったから。

ob$ sed -E 's#(..)(..)(..)(.*)#20\1,\2,\3,\4#' current.csv
2012,01,01,04,118,73,57
2012,01,01,21,109,70,71
2012,01,02,04,128,82,60
2012,01,02,21,105,62,67

元データが、yymmddhh,xxx,xx,xx ってなってるのを分解するやつ。-E を付けると、正規表現のグループを表す括弧の前に付けるバックスリャシュが不要になってすっきりする。 後はこれを埋め込むだけ。

 > con <- pipe("sed -E 's#(..)(..)(..)(.*)#20\1,\2,\3,\4#' current.csv")
 + bld <- read.csv(con, header=F)
 +
!> bld
       V1   V2   V3   V4
 1 20\001 \002 \003 \004
 2 20\001 \002 \003 \004
 3 20\001 \002 \003 \004
 4 20\001 \002 \003 \004

期待してたのと全く違う。でも、なんか、正規表現のリプレース部が化けている風に取れるな。 そもそも、pipeは何よ。helpすると、

For 'pipe' the description is the command line to be piped to or
from.  This is run in a shell, on Windows that specified by the
'COMSPEC' environment variable.

# convert decimal point to comma in output: see also write.table
# both R strings and (probably) the shell need \ doubled
zzfil <- tempfile("outfile")
zz <- pipe(paste("sed s/\\\\./,/ >", zzfil), "w")

嫌らしい注意書きが付いた例が載ってた。どうやらバックスラッシュが喰われてしまうようである。んで、バックスラッシュを追加して、保護してやる。

 > con <- pipe("sed -E 's#(..)(..)(..)(.*)#20\\1,\\2,\\3,\\4#' current.csv")
 + bld <- read.csv(con, header=F)
 +
!> bld
     V1 V2 V3 V4  V5 V6 V7
 1 2012  1  1  4 118 73 57
 2 2012  1  1 21 109 70 71
 3 2012  1  2  4 128 82 60
 4 2012  1  2 21 105 62 67

やれやれである。所で、バックスラッシュは何処で食べられてしまうのだろう? いきなりksh上でsedコマンドを実行した時は、バックスラッシュ1個で良かった。そうだよな。バッククォートで保護してるから、kshは余計なちょっかいを出さないはずだからね。

そう考えると、バックスラッシュを喰っちゃうのはRの文字列解析系の仕業だろうか? 考えていてもしょうが無い。

do_pipe , pipe_open

Rのユーザーからはpipeコマンドが見えて、Rの実行系では、それが do_pipe となって実行される。定義場所は、connections.c の中。

実際にgdbで追ってみてもSEXPいわゆるS式に阻まれて、文字列を探し出すのは容易にはいかない。そこで、もっと下位の pipe_open に目を付ける。

(gdb) bt 3
#0  pipe_open (con=0x63851092000) at connections.c:1495
#1  0x00000635674d9fe7 in do_open (call=<optimized out>, op=<optimized out>, args=<optimized out>, env=<optimized out>) at ../../src/include/Rinlinedfuns.h:779
#2  0x0000063567538565 in bcEval (body=body@entry=0x637b19d6ac0, rho=rho@entry=0x6381cbc75b0, useCache=useCache@entry=TRUE) at eval.c:5252

数行実行してくと、

          fp = R_popen(con->description, mode);
      if(!fp) {
          warning(_("cannot open pipe() cmd '%s': %s"), con->description,
                  strerror(errno));

(gdb) p con->description
$1 = 0x637e8706c40 "sed -E 's#(..)(..)(..)(.*)#20\\1,\\2,\\3,\\4#' current.csv"
(gdb) p mode
$2 = "r\000"

こんなのに出くわしたので、エラー(ワーニング)メッセージを頼りに、送り出しているであろう文字列を検証した。ここでは、まだそのままの正しいコードになってるな。

更にしつこく追う。今度はpopenにBPを置く。

(gdb) bt 3
#0  _libc_popen (program=0x63778b4bec0 "sed -E 's#(..)(..)(..)(.*)#20\\1,\\2,\\3,\\4#' current.csv", type=type@entry=0x7f7ffffc4f45 "r") at /usr/src/lib/libc/gen/popen.c:57
#1  0x0000063567616b95 in R_popen (command=<optimized out>, type=type@entry=0x7f7ffffc4f45 "r") at ../../src/include/Rinlinedfuns.h:481

やっぱり、ここまでで、そのまま渡ってる。更にコードを追うと、vforkしてから子に

execl(_PATH_BSHELL, "sh", "-c", program, (char *)NULL);

をやらせていた。

popenの裏を取れ

ここまでで、事情は分かった。でもlispに組み込まれてちゃ、見通しが悪い。popenを純粋培養して確認してみる。フィッシャー先生の実験計画法の何節かにあったね。一つだけの事項を実験せよと。C語でサンプル実験。

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char** argv) {
    FILE* fp;
    char  buf[1024];
    char* cmd = "sed -E 's#(..)(..)(..)(.*)#20\1,\2,\3,\4#' current.csv";

    if ((fp = popen(cmd, "r")) != NULL) {
        while (fgets(buf, sizeof(buf), fp) != NULL) {
            printf("%s", buf);
        }
        pclose(fp);
        return 0;
    }
    return -1;
}

popenでsedを実行し、それを親側で受け取って表示する、スッキリサッパリな奴だ。

ob$ ./a.out
20,,,
20,,,
20,,,
20,,,
ob$ ./a.out | hexdump -C
00000000  32 30 01 2c 02 2c 03 2c  04 0a 32 30 01 2c 02 2c  |20.,.,.,..20.,.,|
00000010  03 2c 04 0a 32 30 01 2c  02 2c 03 2c 04 0a 32 30  |.,..20.,.,.,..20|
00000020  01 2c 02 2c 03 2c 04 0a                           |.,.,.,..|
00000028

普通に実行するとRでの化け方とは違った。よってバイナリアンになって確認。カンマの間に否表示(いわゆるバイナリ)のデータが混じっている。Rは気を利かせて、オクタル表示してくれているんだな。カンマは、read.csvの倣いにより取り除かれてしまうので、表示はされないとな。

勿論、バックスラッシュをバックスラッシュでエスケープしてあげれば、ちゃんとCSVデータが表示されるよ。ああ、すっきりした、と同時に知見が増えたわい。

更に考察

上でちょいとした実験コードなんだけど、sedにどんな引数が渡ってる? 確認したい。sedを新たにコンパイルして、引数を表示させるようにする? そんな事しなくても、echoで十分じゃん。

char* cmd = "echo  's#(..)(..)(..)(.*)#20\1,\2,\3,\4#' current.csv";

に変更してから実行。(-Eはechoには無いので、あらかじめ除去)

ob$ ./a.out
s#(..)(..)(..)(.*)#20,,,# current.csv
ob$ ./a.out | hexdump -C
00000000  73 23 28 2e 2e 29 28 2e  2e 29 28 2e 2e 29 28 2e  |s#(..)(..)(..)(.|
00000010  2a 29 23 32 30 01 2c 02  2c 03 2c 04 23 20 63 75  |*)#20.,.,.,.# cu|
00000020  72 72 65 6e 74 2e 63 73  76 0a                    |rrent.csv.|

sedに希望通りの引数が渡っていないじゃん。だから、あんな挙動になったんだ。

初心に帰って、man sh すると

A group of characters can be enclosed within double quotes (") to quote
every character within the quotes except a backquote (`) or a dollar sign
($), both of which retain their special meaning.  A backslash (\) within
double quotes retains its special meaning, 

こんな事が説明されてた。-cでコマンド文字列をダブルクォーテーションで囲んでいるから、バックスラッシュの効力を発揮したわけね。

ob$ echo  's#(..)(..)(..)(.*)#20\1,\2,\3,\4#' current.csv
s#(..)(..)(..)(.*)#20\1,\2,\3,\4# current.csv

この挙動を見て、シングルクォーテーションで囲んでるから、文字列は保護されるって、勝手に解釈してたわい。分かってしまえば、どうって事ないけど、ちょっと苦労したな。

%>% の正体

R言語でパイプを検索すると、上でやったpipeはさっぱり出てこなくて、%>% ばかりが目立つ。 dplyrを入れると自然に付いてくるので、気にする事なく使ってると思うが、dplyrとは全く別のパッケージだ。

magrittrのvignetteの訳 変形版が有るけど、覚える必要はないかな。 Rプログラマーのための関数型プログラミングの学び方 にもパイプの話が出てきてた。そういう見方も出来るね。

> library(magrittr)
> %>%
Error: unexpected SPECIAL in "%>%"
> '%>%'
[1] "%>%"
> help(%>%)
Error: unexpected SPECIAL in "help(%>%"
> help('%>%')

なかなか正体を現さない君。最後のhelpで、やっと解説を読めたぞ。 ソースを見るには、ソースを取り寄せろ?

> debug('%>%')
> 1.23456 %>% round(2)
debugging in: 1.23456 %>% round(2)
debug: {
    parent <- parent.frame()
    env <- new.env(parent = parent)
    chain_parts <- split_chain(match.call(), env = env)
    pipes <- chain_parts[["pipes"]]
    rhss <- chain_parts[["rhss"]]
    lhs <- chain_parts[["lhs"]]
     :

これで白昼の元に晒す事が出来た。まあ、R語だけで書いてあれば、、、ですが。

debug: chain_parts <- split_chain(match.call(), env = env)
Browse[2]> n
debug: pipes <- chain_parts[["pipes"]]
Browse[2]> chain_parts
$rhss
$rhss[[1]]
round(., 2)

$pipes
$pipes[[1]]
`%>%`

$lhs
[1] 1.23456

ふむ、分解してるんだな。そろりそろりと歩を進めると、最後は、

debug: result[["value"]]
Browse[2]>
exiting from: 1.23456 %>% round(2)
[1] 1.23

新しい環境を作って、そこに中間値をハッシュして、演算を進めているな。[[って奴だな。

[[> ## check $ and [[ for environments
[[> e1 <- new.env()

[[> e1$a <- 10

[[> e1[["a"]]
[1] 10

[[> e1[["b"]] <- 20

[[> e1$b
[1] 20

[[> ls(e1)
[1] "a" "b"

侮れない、小粋な機能だ箏。ソースを見ると、こういう楽しい発見が有って得した気分。

> 123 %>%
+ '+'(456) %>%
+ sqrt()
Browse[2]> chain_parts
$rhss
$rhss[[1]]
. + 456

$rhss[[2]]
sqrt(.)

$pipes
$pipes[[1]]
`%>%`

$pipes[[2]]
`%>%`

$lhs
[1] 123
Browse[2]> c
exiting from: 123 %>% +456 %>% sqrt()
[1] 24.06242

この表現、面白いな。どことなく関数型を思い出すぞ。

purrr

思い出した。

> library(purrr)
>
> c(2, 4, 6, 8, 10) %>%
+   keep(~. <= 8) %>%
+   map(~. ** 2) %>%
+   keep(~. >= 20) %>%
+   reduce(`*`)
[1] 2304

以前、Rを関数型風に使うってのでpurrrを見つけておいた。これもパイプ演算子を使えるように なっている。それとRに元々ある関数群は、第一引数に関数が来る事が多いんだけど、パイプ演算子との兼ね合いでベクトルを受けるようになってる。第二引数は関数名なんだけど、いちいちfunctionと書くのは煩わしい。そこで、第二引数にチルダ~で始まるラムダ式(~. <= 8など)を書けるようになっている(勿論、内部で変換はしてる)。

> c('ichigo', 'aoi', 'ran') %>% purrr::map(~paste(., 'chan'))
[[1]]
[1] "ichigo chan"

[[2]]
[1] "aoi chan"

[[3]]
[1] "ran chan"

making R on OpenBSD(i386)

デフォで供給されてるRはデバッグ情報が落とされているみたい。ならば自前でってんで、i386なOpenBSDでコンパイルを始めてみたんだ。そしたら、不可解なエラーが。。。

gmake[5]: Entering directory '/tmp/R-3.6.3/src/library/tools/src'  
  :
gcc -shared -fPIC -L/usr/local/lib -o tools.so text.o init.o Rmd5.o md5.o signals.o install.o getfmts.o http.o gramLatex.o gramRd.o
ld: error: gramRd.c:(.debug_info+0x14562): has non-ABS relocation R_386_GOTOFF against symbol '.LC26'
collect2: error: ld returned 1 exit status

リンクで失敗してる。それもエラーメッセージからするとdebug絡みっぽい。こういう事が有るんで、デバッグを切っているのかな? メッセージを頼りにggしたら、とあるパッケージのMakefileに、意味深なやつを発見。

.if ${MACHINE_ARCH} == i386
# i386 issue linking libasteriskpj.so.0.0:
# ld: error: cli_telnet.c:(.debug_info+0x1DF087): has non-ABS relocation R_386_GOTOFF against symbol 'CR_LF.6215'
USE_LLD=		No
.endif

リンカーがおかしいから、使用禁止を申し渡すとな。

手っ取り早く、CC=cc gmake してみたんだけど、相変わらずgccが使われている。だったらMakefileの中に CC=gcc って書いて有るのかな? どうもそうでは無い。目を皿にして探したら、 Makefileの補助が出てた。

その場所は、 topdir/Makeconf と、 topdir/etc/Makeconf だ。同じ事を2度書いているのかな? だとしたら、集中の原則を忘れてるよ。

で、コンフィグ時に –disable-java しておかないと、javaと密になろうとして失敗する。 なんやかんやで、18分程で、Rが出来た。

コンパイル作業は、SSDの負担にならないようにRAM-DISK(/tmp)上で行った。コンパイルが済んだら、ソースは適当な所に配置。そうなると、コンパイル時とgdb使用時のソースの在処が異なる事になる。

gdbには、それを吸収してくれるdirってコマンドが用意されてる。gdbが使うソースの在処を指定出来るんだ。

(gdb) dir
Reinitialize source path to empty? (y or n) n
Source directories searched: $cdir:$cwd

デフォでは、上記のようになっている。PATHの設定宜しく、コロン区切りで複数指定出来る。だから、ソースファイルが複数のdirに分散してても大丈夫。

今回はgdbを起動する前に、cd ~/src/R-3.6.3/src/main して、ソースの場所に移動。これで、何事もなく、ソースを参照してくれた。

先程 doas syspatch したら、新しいのが落ちて来た。新しいVerが出るんで、それのバックポートかしら? 新しいsshもこの間出た事だし、いよいよな段階に入ったと思われ。


This year's Index

Home