65.9K
CodeProject 正在变化。 阅读更多。
Home

R 统计语言 API 到 VB.NET 语言

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.60/5 (3投票s)

2016年3月8日

CPOL

6分钟阅读

viewsIcon

18316

用于 .NET 语言与 R 语言的混合编程技术

引言

在我最近的工作中,有一个需求是需要一个库来绘制细菌基因组中基因与表型之间的相关性关系。热图是一个不错的选择,而且 R 语言中已经有很多优秀的库了,所以我的研究工作需要一种混合编程技术。

RDotNET 项目使得 R 和 .NET 语言之间的混合编程成为可能,但它在编程方面仍然不够方便。所以我决定开发这个项目,以实现更高效的 R 混合编程。

访问 RDotNET 主页:https://rdotnet.codeplex.com/

声明 R API

R API 与 Win32 API 不同

如果我们定义一个 Win32API,那么将使用 DllImport 属性,例如

<DllImport("kernel32.dll", EntryPoint:="LoadLibrary", SetLastError:=True)> _
Private Shared Function InternalLoadLibrary(
              <MarshalAs(UnmanagedType.LPStr)> lpFileName As String) As IntPtr
End Function

或者在 vb6 旧风格中

Public Declare Function InternalLoadLibrary _
       Lib "kernel32.dll" _
       Alias "LoadLibrary" _
     (<MarshalAs(UnmanagedType.LPStr)> lpFileName As String) As IntPtr

但 R 的情况不同,R 中的函数是一个对象,就像 VisualBasic 中的方法也是一个对象一样,或者说一切都是对象。

一个简单的 R API 示例

一个不带任何参数的基本 R API 可以是一个空类对象,例如

<RFunc("heatmap.2")>
Public Class heatmap2 : Inherits IRToken
End Class

这样我们就可以从 R 中定义一个函数 API 入口点,例如

heatmap.2()

如果我们想为 API 添加一些参数,只需在你的类中添加一个属性即可

<RFunc("heatmap.2")>
Public Class heatmap2 : Inherits IRToken
    Public Property x As RExpression
    Public Property Rowv As Boolean = True
    Public Property Colv As RExpression = [TRUE]
    Public Property col As RExpression = "rev(brewer.pal(10,""RdYlBu""))"
    Public Property revC As RExpression = [TRUE]
    Public Property scale As RExpression = "row"
    Public Property margins As RExpression = c(15, 15)
    Public Property key As Boolean = True
    <Parameter("density.info")>
    Public Property densityInfo As String = Rstring("none")

End Class

这样,最终 R 中的这个函数 API 看起来就像

heatmap.2(x, 
rowv = TRUE, 
colv =TRUE, 
col= rev(brewer.pal(10,""RdYlBu"")), 
revC = TRUE, 
scale="row", 
margins=c(15,15),
key=TRUE, 
density.info="none")

API 详情

API 入口点

RFunc 属性用于定义 R API 入口点,就像 Win32API 的 DllImport 一样,使用这个 RFunc 属性,我们可以声明一个带有点的函数名,这在 VisualBasic 标识符命名中是不允许的。

对名称的调整

如果参数名中包含点,而点字符在 VisualBasic 标识符中是不允许的,那么你可以使用 Parameter 属性来声明参数名的别名。

此外,如果你的 API 的属性不是参数,那么你可以使用 Ignored 属性来屏蔽该属性,使其不被 API 构建器处理。

IRToken 包装器

最后,你的 R API 类可以选择继承 IRToken 对象,因为 R 脚本的一组扩展方法已经为 IRToken 包装器对象定义好了。最后,API 构建器可以通过使用扩展方法将你的 API 序列化为 R 脚本。

Me.GetScript(Me.GetType)

' Or more simple

' If your R API have the inherits relationship, then this generic method is not 
' recommended used in base class as the generic method always using the type 
' Information of your base class. This case bugs.

Me.GetScript

为什么选择类作为 API?

方便与朋友分享你的脚本或归档脚本模型

由于有时绘制 R 图片需要大量的参数调整,你想在调整完参数后与朋友分享 R 脚本,你只需要将脚本序列化为一个 JSON 文件,然后通过电子邮件发送给你的朋友,你的朋友只需通过 JSON 反序列化加载你的脚本并进行进一步的调整。

使 Visual Basic 编程更加容易

正如你所见,大多数 R 函数都有很多可调整的参数,所以当你用 R 编程时,如果 API 是以函数对象的形式编写的,你需要在程序中定义很多参数。

对我而言,我更喜欢使用一个类来传递多个参数给多个函数参数。

    ' Not a Convenient style with a lot of parameter
    Function example(path, format, blablabla...) As Type
    End Function

    ' Great and convenient style with just some parameter
    ' All of the blablabla parameter are passing from the RAPI object.
    Function example(path, format, RAPI) As Type

    End Function

其中 RAPI 是一个类,这个类中的属性就是上面函数中的多个参数,等等。

API 可以继承其他 API,这使得在 R 中定义 API 的一些重载函数更加容易

例如,在 RgrDevices 命名空间中有一些图像格式 API,如 bmp、jpeg、png 和 tiff,这些 R 函数在绘制图像时有一些共同的参数,所以在设计这个 API 时,你只需要为一个公共参数声明一个基类,为独特参数声明子类,这种继承关系使你的程序更加清晰和简单。

是的,来自 R 的类类型函数 API 使你的程序结构更加清晰!

API 构建器

R 脚本 Token

这里我定义了一组 abstract 类作为 R API Token,其中包含一系列包装器扩展方法。

''' <summary>
''' 一个提供脚本语句的最基本的抽象对象
''' </summary>
''' <remarks>就只通过一个函数来提供脚本执行语句</remarks>
Public MustInherit Class IRProvider
    Implements IScriptProvider

    Dim __requires As String()

    ''' <summary>
    ''' The package names that required of this script file.
    ''' (需要加载的R的包的列表)
    ''' </summary>
    ''' <returns></returns>
    <Ignored> Public Overridable Property Requires As String()
        Get
            Return __requires
        End Get
        Protected Set(value As String())
            __requires = value
        End Set
    End Property

    ''' <summary>
    ''' Get R Script text from this R script object build model.
    ''' </summary>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public MustOverride Function RScript() As String Implements IScriptProvider.RScript

    Public Overrides Function ToString() As String
        Return RScript()
    End Function

    Public Shared Narrowing Operator CType(R As IRProvider) As String
        Return R.RScript
    End Operator
End Class

''' <summary>
''' R之中的单步函数调用
''' </summary>
Public Class IRToken : Inherits IRProvider
    Implements IScriptProvider

    ''' <summary>
    ''' 
    ''' </summary>
    ''' <returns>由于这个对象只是对一个表达式的抽象,最常用的是对一个函数调用的抽象,
    ''' 所以library在这里不可以自动添加,需要自己在后面手工添加</returns>
    Public Overrides Function RScript() As String
        Return Me.GetScript([GetType])
    End Function

    Public Overloads Shared Narrowing Operator CType(token As IRToken) As String
        Return token.RScript
    End Operator

    Public Shared Operator &(token As IRToken, script As String) As String
        Return token.RScript & script
    End Operator

    Public Shared Operator &(script As String, token As IRToken) As String
        Return script & token.RScript
    End Operator
End Class

构建器 API

从类对象构建 R API 基于 System.Reflection 方法,因此需要两个基本的 API 构建器参数

   <Extension>
   Public Function GetScript(token As Object, Optional type As Type = Nothing) As String
       If token Is Nothing Then
            Throw New NullReferenceException("Script tokens is nothing!")
       End If

       If type Is Nothing Then
            type = token.GetType
       End If

       Return __getScript(token, type)

   End Function

首先,token 参数提供 R 函数对象实例,即我们在上一节定义的类对象,R 函数入口点。

然后,如果我们想使用反射操作,则需要一个反射源,这个源来自 type 参数,我们可以通过它获取属性信息和类信息。

获取 API 名称只需要获取我们在类定义中定义的 RFunc 自定义属性。

        ''' <summary>
        ''' GET API name
        ''' </summary>
        ''' <param name="type"></param>
        ''' <returns></returns>
        <Extension> Public Function GetAPIName(type As Type) As String
            Dim name As RFunc = type.GetAttribute(Of RFunc) ' Get function name

            If name Is Nothing Then
                Dim ex As New Exception(IsNotAFunc)
                ex = New Exception(type.FullName, ex)
                Throw ex
            Else
                Return name.Name
            End If

        End Function

由于所有 R 函数参数都以类属性的形式存在,构建器的下一步就是获取所有可读属性。

此外,如果我们想屏蔽属性不被构建器处理,我们应该跳过所有我们已定义了 ignore 属性的属性,这样可以使用 Linq 表达式来完成这项工作。

Dim props = (From prop As PropertyInfo In type.GetProperties
             Where prop.GetAttribute(Of Ignored) Is Nothing AndAlso
                  prop.CanRead
             Let param As Parameter = prop.GetAttribute(Of Parameter)
             Select prop,
                   func = prop.__getName(param),
                   param.__isOptional,
                   param
             Order By __isOptional Ascending)

关于数据类型的 IMPORTANT 通知

有一些数据类型需要注意。

1. Bool 逻辑值

R 语言中的布尔逻辑类型是全大写的 TRUE、FALSE 或 T、F,R 语言不像 VisualBasic 语言,R 语言是区分大小写的,所以我们应该对逻辑值进行映射。

  Public Structure RBoolean : Implements IScriptProvider

        Public Shared ReadOnly Property [TRUE] As New RBoolean(RScripts.TRUE)
        Public Shared ReadOnly Property [FALSE] As New RBoolean(RScripts.FALSE)

        ReadOnly __value As String

        Sub New(value As String)
            __value = value
        End Sub

        Public Function RScript() As String Implements IScriptProvider.RScript
            Return __value
        End Function

   End Structure

2. 字符串值类型

例如,VisualBasic 中的一个 string 值是

    Dim s As String = "abc"

当我们把这个变量写到文本文件时,内容只有 abc,两个引号字符消失了。所以当我们将 R 脚本写入时,情况也是一样的。

R 脚本中的函数需要一个 string 值,并且它用两个引号字符包裹 string,但当我们编写脚本时,这两个引号字符也消失了,所以写入脚本之前,需要对 string 类型进行处理。

Public Function Rstring(s As String) As String
        Return $"""{s}"""
End Function

3. 表达式作为参数

R 表达式我们可以直接使用 string 来表示。

4. 字符串作为文件路径

由于字符 \ 在 C/C++ 语言中是转义字符,所以在 R 语言中文件路径中的 \ 字符会导致错误,处理这种情况的一个简单方法是将所有的 \ 字符替换为 /。

    ''' <summary>
    ''' 
    ''' </summary>
    ''' <param name="file"></param>
    ''' <param name="extendsFull">是否转换为全路径?默认不转换</param>
    ''' <returns></returns>
    <Extension>
    Public Function UnixPath(file As String, Optional extendsFull As Boolean = False) As String
        If String.IsNullOrEmpty(file) Then
            Return ""
        End If
        If extendsFull Then
            file = FileIO.FileSystem.GetFileInfo(file).FullName
        End If
        Return file.Replace("\"c, "/"c)

    End Function

最后,我们可以编写一个函数来对不同数据类型的 API 构建器进行附加处理。

<Extension>
Private Function __getValue(type As Type, value As Object, valueType As ValueTypes) As String
    If value Is Nothing Then
        Return Nothing
    End If

    Select Case type

        Case GetType(String)

            If valueType = ValueTypes.Path Then
                  Return Rstring(Scripting.ToString(value).UnixPath)
             Else
                  Return Rstring(Scripting.ToString(value))
             End If
        Case GetType(Boolean)
             If True = DirectCast(value, Boolean) Then
                  Return RBoolean.TRUE.__value
             Else
                  Return RBoolean.FALSE.__value
             End If
        Case GetType(RExpression)
             Return DirectCast(value, RExpression).RScript
        Case Else
             Return Scripting.ToString(value)
    End Select
End Function

示例:在 VisualBasic 中绘制热图

使用 R 语言绘制热图,可以在 http://flowingdata.com/2010/01/21/how-to-make-a-heatmap-a-quick-and-easy-solution/ 找到一个例子。

所以,基于这个例子,我们可以创建一个 R API 包装器

Imports System.Text
Imports System.IO
Imports Microsoft.VisualBasic.DocumentFormat.Csv.DocumentStream.Tokenizer
Imports Microsoft.VisualBasic.Linq
Imports Microsoft.VisualBasic
Imports RDotNet.Extensions.VisualBasic
Imports RDotNet.Extensions.VisualBasic.utils.read.table
Imports RDotNet.Extensions.VisualBasic.stats
Imports RDotNet.Extensions.VisualBasic.Graphics
Imports RDotNet.Extensions.VisualBasic.grDevices

Public Class Heatmap : Inherits IRScript

    Const df As String = "df"

    ''' <summary>
    ''' Column name of the row factor in the csv file that represents the row name. 
    ''' Default is the first column.
    ''' </summary>
    ''' <returns></returns>
    Public Property rowNameMaps As String
    ''' <summary>
    ''' Csv文件的文件路径
    ''' </summary>
    ''' <returns></returns>
    Public Property dataset As readcsv
    Public Property heatmap As heatmap_plot
    ''' <summary>
    ''' tiff文件的输出路径
    ''' </summary>
    ''' <returns></returns>
    Public Property image As grDevice

    Sub New()
        Requires = {"RColorBrewer"}
    End Sub

    ''' <summary>
    ''' 
    ''' </summary>
    ''' <returns></returns>
    ''' <remarks>
    ''' http://joseph.yy.blog.163.com/blog/static/50973959201285102114376/
    ''' </remarks>
    Protected Overrides Function __R_script() As String
        Dim script As StringBuilder = New StringBuilder()
        Call script.AppendLine($"{df} <- " & dataset)
        Call script.AppendLine($"row.names({df}) <- {df}${__getRowNames()}")
        Call script.AppendLine($"{df}<-{df}[,-1]")
        Call script.AppendLine("df <- data.matrix(df)")

        heatmap.x = df

        If Not heatmap.Requires Is Nothing Then
            For Each ns As String In heatmap.Requires
                Call script.AppendLine(RScripts.library(ns))
            Next
        End If

        Call script.AppendLine(image.Plot("result <- " & heatmap))

        Return script.ToString
    End Function
End Class

通过使用这个 heatmap API,需要三个参数

1. 定义热图数据源,其数据源来自读取 csv 文件

Property dataset As readcsv

通过从一个位置读取数据,只需构建 read.csv API 类的对象实例,例如

dataset = New readcsv("http://datasets.flowingdata.com/ppg2008.csv")

2. 定义热图绘制方法,其可用的 API 可以在 gplots 或 stats 命名空间中找到

Property heatmap As heatmap_plot

3. 定义热图图像保存位置

Property image As grDevice

此外,在 RDotNET.Extensions.VisualBasic.grDevices 命名空间中已经定义了一组图像文件格式 API。

例如:grDevices .bmpgrDevices .jpeggrDevices .pnggrDevices .tiff

通过使用这个 R API,只需简单地构建一个对象实例,例如

Dim image As grDevice = New tiff("imagefile.tiff", 8000, 6500)

你可以从 github 下载这个示例

快去试试吧!

© . All rights reserved.