1. 程式人生 > >RCaller 無法返回複雜資料的研究以及解決方案

RCaller 無法返回複雜資料的研究以及解決方案

在 Java呼叫基於 R 的 One-Way ANOVA檢測 文章裡,通過 cbind 我們可以返回多個數據,但是裡面的資料都是簡單型別,所有能正常工作,但是我在做 Propensity Score Match 的時候呼叫 MatchIt 函式,我需要將分析結果資料全部返回,下面是資料在 RGui 裡面的樣子:


獲取各個 Matrix 的函式:


我的程式是這樣的:

RCaller caller = initRCaller(new RCallerTemplate() {
	@Override
	public void addRCode(RCode code) {	
		code.addRCode("df <- read.csv('C:/Users/Lenovo/Desktop/ccc.csv', header=TRUE)");
		code.addRCode("library(MatchIt)"); 			
		code.addRCode("fm <- matchit(censor ~ covariate1 + covariate2, method='nearest', data=df)");
		code.addRCode("result <- summary(fm)");				
		code.addRCode("sa <- result$sum.all");
		code.addRCode("mat <- result$sum.matched");
		code.addRCode("red <- result$reduction");
		code.addRCode("ss <- result$nn");
		code.addRCode("mData <- match.data(fm)");
		code.addRCode("out <- list(sum, mat, red, ss, mData)");
	}
});
caller.runAndReturnResult("out");

double[] result = caller.getParser().getAsDoubleArray("out");

程式執行時,RCaller 報 Rcaller Error in strsplit(names, "\\.")  錯誤,逐行註釋除錯,如果直接獲取  sa , 報 

com.github.rcaller.exception.ParseException: Variable sa not found
但是獲取 ss 是沒有問題的,能正確取得值,只能進入 RCaller 裡面除錯原始碼了。大致得出下面 3點:

1. RCaller 把R程式碼寫入檔案中,呼叫Rscript執行;
2. 執行之後RCaller通過試圖把R結果轉成xml格式,返回給Java;我們的R程式碼沒有問題,問題出在轉xml的部分;
3.

RCaller轉xml的時候,它預設每個資料(包括list),每一列都要有名字,而我們java程式碼裡邊的out變數,沒有名字,所以objnames<-names(obj)為空,導致執行出錯;

makexml<-function(obj,name=""){
    xmlcode<-"<?xml version=\"1.0\"?>\n<root>\n"
    if(!is.list(obj)){
        print( cat( "not list:", name ) )
        xmlcode<-makevectorxml(xmlcode,obj,name)
    }else{
        objnames<-names(obj)
        for (i in 1:length(obj)){
            print( cat( "not list:", objnames[[i]] ) )
            xmlcode<-makevectorxml(xmlcode,obj[[i]],cleanNames(objnames[[i]]))
        }
    }
    xmlcode<-paste(xmlcode,"</root>\n",sep="")
    return(xmlcode)
}

嘗試著給每個列都取名字, 把Java程式碼寫成這樣:

output <- list( o1=sa, o2=mat, o3=red, o4=ss, o5=mData ) 

又會遇到 RCaller 的第二個問題,資料型別支援不全:

makevectorxml<-function(code,objt,name=""){
    xmlcode<-code
    if(name==""){
        varname<-cleanNames(deparse(substitute(obj)))
    }else{
        varname<-name
    }   
    obj<-objt
    n <- 0; m <- 0
    mydim <- dim(obj)
    if(!is.null(mydim)){
        n <- mydim[1]; m <- mydim[2]
    }else{
        n <- length(obj); m <- 1
    }   
    if(is.matrix(obj)) obj<-as.vector(obj)
    if(typeof(obj)=="language") obj<-toString(obj)
    if(typeof(obj)=="logical") obj<-as.character(obj)
    if(is.vector(obj) && is.numeric(obj)){
        xmlcode<-paste(xmlcode,"<variable name=\"",varname,"\" type=\"numeric\" n=\"", n, "\"  m=\"", m, "\">",sep="")
        for (i in obj){
            xmlcode<-paste(xmlcode,"<v>", toString(i), "</v>",sep="")
        }
        xmlcode<-paste(xmlcode,"</variable>\n",sep="")
    }
    if(is.vector(obj) && is.character(obj)){
        xmlcode<-paste(xmlcode,"<variable name=\"",varname,"\" type=\"character\">\n",sep="")
        for (i in obj){
            xmlcode<-paste(xmlcode,"<v>",toString(i),"</v>",sep="")
        }
        xmlcode<-paste(xmlcode,"</variable>\n")
    }
    return(xmlcode)
}

因為 sa, mat 的型別都是 list,而 ss 是其支援的型別,不然根本進入不了 if 語句裡,原樣返回了xmlcode而已,所以Java裡只能看到ss,呼叫獲取別的都出錯。

> is.matrix( mat )
[1] FALSE
> is.numeric( mat )
[1] FALSE
> is.vector( mat )
[1] FALSE
> is.character(mat)
[1] FALSE
> typeof(mat)
[1] "list"

所以,對於複雜的型別返回,就不要使用 RCaller, 但是 RCaller 的 API 友好性真的不錯,如果分析的結果比較簡單,還是喜歡使用這個工具。


=================更新 @ 2017/09/26=====================

既然 RCaller 支援的型別比較有限,那麼為了返回結果能夠進入這些 if  語句塊,我們是不是可以對結果資料進行轉型,比如 mat 是一個 list 型別,我們是否有辦法將其轉型為 vector, 這樣不就可以了。google 了下,發現了 Better way to convert list to vector? 裡面提到了 

unlist(myList, use.names=FALSE)
將轉型和對 out 裡面的每個返回值賦予一個名字,終於出結果了。但是發現了另外一個問題,具體描述見  difference between as.data.frame and read.csv in R

其實,真正寫程式的時候,是頁面傳變數的ID/Name, 後臺查詢資料庫得出值,因為不知道變數返回值的型別,我統一使用 String 接收,然後放入 RCaller 的 RCode裡,生成 R 的完整程式裡,可以看到每個變數的值都是字串。把程式抓出來放到 RGui 裡面,列印data.frame並不能看出其型別,但是可以通過 sapply 函式得知:

> sapply(df, class)
PERSON_ID   OUTCOME       tnb       gxy      AGE1 
 "factor"  "factor"  "factor"  "factor"  "factor"
由於是 factor 型別,表示是離散值,非數字, matchit 不會把他們當數字處理,只羅列所有離散值。

解決辦法有兩個,一個就是在輸入 RCode 之前,就將變數值轉成數字型。另一個就是在 R 執行 matchit 之前,對 data.frame 的列進行轉型:

newdf = transform( df, tnb = as.numeric( tnb ), AGE1=as.numeric( AGE1 ) )
sapply( newdf, class )
PERSON_ID   OUTCOME       tnb       gxy      AGE1 
 "factor"  "factor" "numeric"  "factor" "numeric" 

最終,我採取了在傳入 RCode 之前,就對資料轉型成數字,然後傳入 RCode。首先我的 R 程式的生成檔案在 C:\Users\Lenovo\AppData\Local\Temp 路徑下 (Lenovo是我的機器名),完整程式碼如下:

cleanNames<-function(names){
  cln<-paste(unlist(strsplit(names,"\\.")),collapse="_")
  cln<-paste(unlist(strsplit(cln,"<")),collapse="")
  cln<-paste(unlist(strsplit(cln,">")),collapse="")
  cln<-paste(unlist(strsplit(cln," ")),collapse="")
  cln<-paste(unlist(strsplit(cln,"\\(")),collapse="")
  cln<-paste(unlist(strsplit(cln,"\\)")),collapse="")
  cln<-paste(unlist(strsplit(cln,"\\[")),collapse="")
  cln<-paste(unlist(strsplit(cln,"\\]")),collapse="")
  cln<-paste(unlist(strsplit(cln,"\\*")),collapse="")
  cln<-paste(unlist(strsplit(cln,"&")),collapse="")
  return(cln)
}

replaceXMLchars <- function(aStr){
  cln <-paste(unlist(strsplit(aStr,"&")),collapse="&")
  cln <-paste(unlist(strsplit(cln,"<")),collapse="<")
  cln <-paste(unlist(strsplit(cln,">")),collapse=">")
  cln <-paste(unlist(strsplit(cln,"'")),collapse="'")
  return(cln)	
}

makevectorxml<-function(code,objt,name=""){
  xmlcode<-code
  if(name==""){
    varname<-cleanNames(deparse(substitute(obj)))
  }else{
    varname<-name
  }
  obj<-objt  
  n <- 0; m <- 0
  mydim <- dim(obj)
  if(!is.null(mydim)){
    n <- mydim[1]; m <- mydim[2]
  }else{
    n <- length(obj); m <- 1
  }
  if(is.matrix(obj)) obj<-as.vector(obj)
  if(typeof(obj)=="language") obj<-toString(obj)
  if(typeof(obj)=="logical") obj<-as.character(obj)
  if(class(obj)=="factor") obj<-as.vector(obj)
  if(is.vector(obj) && is.numeric(obj)){
    xmlcode<-paste(xmlcode,"<variable name=\"",varname,"\" type=\"numeric\" n=\"", n, "\"  m=\"", m, "\">",sep="")
    s <- sapply(X=obj, function(str){
      return(
        paste("<v>",iconv(replaceXMLchars(toString(str)), to="UTF-8"),"</v>",sep="")
      )})
    xmlcode<-paste(xmlcode,paste(s, collapse=""),"</variable>\n")
  }
  if(is.vector(obj) && is.character(obj)){
    xmlcode<-paste(xmlcode,"<variable name=\"",varname,"\" type=\"character\">\n",sep="")
    s <- sapply(X=obj, function(str){
                            return(
                              paste("<v>",iconv(replaceXMLchars(toString(str)), to="UTF-8"),"</v>",sep="")
                             )})
    xmlcode<-paste(xmlcode,paste(s, collapse=""),"</variable>\n")
  }
  return(xmlcode)
}


makexml<-function(obj,name=""){
  xmlcode<-"<?xml version=\"1.0\"?>\n<root>\n"
  if(!is.list(obj)){
    xmlcode<-makevectorxml(xmlcode,obj,cleanNames(name))
  }else{
    objnames<-names(obj)
    for (i in 1:length(obj)){
      xmlcode<-makevectorxml(xmlcode,obj[[i]],cleanNames(objnames[[i]]))
    }
  }
  xmlcode<-paste(xmlcode,"</root>\n",sep="")
  return(xmlcode)
}



PERSON_ID<-c(166532, 166551, 166640, 166651, 166668, 166705, 166736, 166745, 166806, 166822);
OUTCOME<-c(1, 1, 1, 1, 1, 0, 0, 0, 0, 0);
tnb<-c(48.0, 48.0, 49.0, 48.0, 48.0, 49.0, 48.0, 48.0, 49.0, 48.0);
gxy<-c(48.0, 48.0, 49.0, 49.0, 48.0, 49.0, 49.0, 48.0, 49.0, 48.0);
AGE1<-c(76.0, 81.0, 74.0, 72.0, 73.0, 73.0, 81.0, 74.0, 74.0, 85.0);
matrix <- cbind(PERSON_ID,OUTCOME,tnb,gxy,AGE1)
df <- as.data.frame(matrix)
library(MatchIt)
fm <- matchit(OUTCOME ~ tnb + gxy + AGE1, data = df, method = "nearest", replace = TRUE, ratio = 1)
result <- summary(fm)
sum <- result$sum.all
sa_0_distance <- unlist(sum[1, 1:7], use.names=FALSE)
sa_1_tnb <- unlist(sum[2, 1:7], use.names=FALSE)
sa_2_gxy <- unlist(sum[3, 1:7], use.names=FALSE)
sa_3_AGE1 <- unlist(sum[4, 1:7], use.names=FALSE)
mat <- result$sum.matched
mat_0_distance <- unlist(mat[1, 1:7], use.names=FALSE)
mat_1_tnb <- unlist(mat[2, 1:7], use.names=FALSE)
mat_2_gxy <- unlist(mat[3, 1:7], use.names=FALSE)
mat_3_AGE1 <- unlist(mat[4, 1:7], use.names=FALSE)
red <- result$reduction
red_0_distance <- unlist(red[1, 1:4], use.names=FALSE)
red_1_tnb <- unlist(red[2, 1:4], use.names=FALSE)
red_2_gxy <- unlist(red[3, 1:4], use.names=FALSE)
red_3_AGE1 <- unlist(red[4, 1:4], use.names=FALSE)
ss <- result$nn
mData <- unlist(match.data(fm)[1], use.names=FALSE)
out <- list(sa_distance = sa_0_distance, mat_distance = mat_0_distance, red_distance = red_0_distance, sa_tnb = sa_1_tnb, mat_tnb = mat_1_tnb, red_tnb = red_1_tnb, sa_gxy = sa_2_gxy, mat_gxy = mat_2_gxy, red_gxy = red_2_gxy, sa_AGE1 = sa_3_AGE1, mat_AGE1 = mat_3_AGE1, red_AGE1 = red_3_AGE1, size = ss, ids = mData)
cat(makexml(obj=out, name="out"), file="C:/Users/Lenovo/AppData/Local/Temp/ROutput8334329596424358515")

為了方便顯示,對每個變數我只截取了十個元素。從上面可以看到,我幹了四件事:

1. 傳入的資料必須根據實際情況轉型成需要的型別,比如這裡所有的變數值都是數字

2. 將每個變數的值陣列直接傳入 R 的數組裡,然後使用 as.data.frame 轉成 data.frame, 這樣就省去了些 CSV 檔案的麻煩,這個在之前的一個文章裡提到了

3. 將需要返回的複雜型別(比如 list)轉成 vector,使用 unlist 函式

4. 所有放入最終返回型別的 out 裡面的元素都必須有一個名字