https://github.com/rsharp-lang/R-sharp/tree/master/studio/RData

在最近的工作中,需要将Docker容器内的R环境之中的数据集无缝的串流到下游的.NET Core数据分析环境之中,基于.NET Core代码库进行数据可视化之类的操作。目前在R环境与.NET Core环境之间进行交互仅存在有一个比较出名的R.NET项目。但是对于使用R.NET项目而言,我们只能够在.NET Core环境之中调用R环境做数据分析,并不能够实现R环境调用.NET Core数据分析环境。并且R.NET项目必须要依赖于R环境对应的库文件,所以使用R.NET并不能够满足我们在Docker容器间进行R数据分析环境与.Net Core数据分析环境之间的无缝衔接。

随着自己开发的R#数据分析环境的日益成熟,最近又实现了在.NET Core环境中对R语言的RData数据集的读取代码模块。所以目前完成的开发工作已经足以支撑构建出一个比较平滑的R#/R混合型的数据分析流程框架。在这里主要是想为大家详细的说明一下RData文件的数据格式,以及对应的VisualBasic.NET代码对RData读取的实现。

对于RData数据集格式的讲解,我将会分开为独立的几篇文章进行讲解:

RData文件格式

在开始讲解RData文件格式之前,我们需要记住一个非常重要的文件格式要点:RData数据文件是以二叉树+链表的形式进行数据存储,并且每一个在链表之中的数据节点对象都有自己的属性信息,类型信息等元数据。明确了RData的链表存储形式的概念之后,对于下文的数据读取的代码理解将会稍微要容易一些了。

首先来一张图用于尽可能简单的为大家展示一个标准的RData数据文件的文件内容:

RData数据编码基础:XDR编码

在RData数据集之中,仅支持Integer和Double这两种基础数值类型。而这两种数值类型都是以XDR编码方案存储的。

XDR编码方案是 1987 年 6 月由 Sun Microsystems,Inc. 编写的 RFC 1014中描述的外部数据表示标准(External Data Representation)。XDR提供了一种与体系结构无关的表示数据,解决了数据字节排序的差异、数据字节大小、数据表示和数据对准的方式。使用XDR的应用程序,可以在异构硬件系统上交换数据。

对于RData数据集,由于我们可能会将RData放在对应的R package中发布给其他人使用。所以为了达到数据编码在不同的计算机硬件和操作系统平台之间的兼容性,在RData数据集中广泛的使用XDR编码方案来解决平台之间的兼容性问题。在进行RData数据文件的读取操作的时候,会需要使用到两个重要的数据类型的XDR解码函数:

对于进行32位整形数的解码操作,我们可以通过XDRParser.UnpackInteger实现。UnpackInteger函数的底层实现是依赖于一个名字叫做DecodeInt32的函数来完成:

''' <summary>
''' Decodes the Int32.
''' http://tools.ietf.org/html/rfc4506#section-4.1
''' </summary>
Public Function DecodeInt32(r As IByteReader) As Integer
    If r.EndOfStream Then
        ' Return 0
        Throw New InvalidProgramException
    Else
        ' 20211203
        ' default in VB.NET is byte shift
        ' should be convert to integer at first
        Dim H18 = CInt(r.Read) << &H18
        Dim H10 = CInt(r.Read) << &H10
        Dim H8 = CInt(r.Read) << &H8
        Dim H0 = CInt(r.Read)

        Return H18 Or H10 Or H8 Or H0
    End If
End Function

对于双精度类型,我们可以通过XDRParser.UnpackDouble函数来完成解码。与Integer不同的是,Double类型浮点数的解码是构建在64位整型数的基础上完成的。所以我们可以有下面所示的底层解码代码:

''' <summary>
''' Decodes the Double.
''' http://tools.ietf.org/html/rfc4506#section-4.7
''' </summary>
Public Function DecodeDouble(r As IByteReader) As Double
    Dim num As Long = Xdr.XdrEncoding.DecodeInt64(r)
    Return unsafeDouble(num)
End Function

''' <summary>
''' Decodes the Int64.
''' http://tools.ietf.org/html/rfc4506#section-4.5
''' </summary>
Public Function DecodeInt64(r As IByteReader) As Long
    Return (CLng(r.Read()) << 56) _
        Or (CLng(r.Read()) << 48) _
        Or (CLng(r.Read()) << 40) _
        Or (CLng(r.Read()) << 32) _
        Or (CLng(r.Read()) << 24) _
        Or (CLng(r.Read()) << 16) _
        Or (CLng(r.Read()) << 8) _
        Or (CLng(r.Read()))
End Function

Private Function unsafeDouble(x As Long) As Double
    Dim bytes = BitConverter.GetBytes(x)
    Dim dbl As Double = BitConverter.ToDouble(bytes, 0)

    Return dbl
End Function

在VisualBasic中解析RData数据集

在假设大家都已经明白了上面所讲解的进行RData数据集解析所必须的一些基础知识之后,我们下面就可以开始学习对RData数据文件的详细解析代码的实现了。

文件级别的解析代码

对于RData数据集而言,可能存在有bz2,gzip,xz这三种文件压缩格式。也可能是没有进行压缩的,直接暴露在外面的RData裸数据文件。所以在数据集读取的最开始阶段,我们就需要根据数据流之中的幻数头进行文件压缩类型的判断。下面的代码给出了在RData之中的文件类型的幻数头常数:

ReadOnly magic_dict As New Dictionary(Of FileTypes, Byte()) From {
    {FileTypes.bzip2, bytes("\x42\x5a\x68")},
    {FileTypes.gzip, bytes("\x1f\x8b")},
    {FileTypes.xz, bytes("\xFD7zXZ\x00")},
    {FileTypes.rdata_binary_v2, bytes("RDX2\n")},
    {FileTypes.rdata_binary_v3, bytes("RDX3\n")}
}

根据不同的幻数检查结果,我们使用对应的数据压缩格式进行数据流的解压缩之后再进行XDR文件读取即可。具体的解压缩代码在这里不在做赘述。

在完成了最外层的数据流解压缩操作之后,整个RData数据集就直接暴露在外面了。现在我们再根据RData格式类型的幻数头进行对应版本的RData读取模块的调用。进行RData格式的幻数头的检查与之前的文件压缩幻数头检查的操作是一样的。下面的代码给出了在RData之中的文件格式的幻数常数定义:

ReadOnly format_dict As New Dictionary(Of RdataFormats, Byte()) From {
    {RdataFormats.XDR, bytes("X\n")},
    {RdataFormats.ASCII, bytes("A\n")},
    {RdataFormats.binary, bytes("B\n")}
}

可以看得到,在RData之中存在有三个版本的编码格式:默认的XDR编码格式,ASCII编码格式以及二进制编码格式。在这里我们主要学习的是对基于XDR编码格式的RData文件读取的代码。

文件元数据读取

在RData文件数据之中,仅存在有两个元数据信息:R环境版本以及文本编码。我们在进行读取的时候,需要先读取版本信息,在读取文本编码信息,最后进行具体的R数据的读取操作。对应的读取函数如下所示:

''' <summary>
''' Parse the versions header.
''' </summary>
''' <returns></returns>
Public Function parse_versions() As RVersions
    Dim format_version = parse_int()
    Dim r_version = parse_int()
    Dim minimum_r_version = parse_int()

    Static supportedVer As New Index(Of Integer) From {2, 3}

    If Not format_version Like supportedVer Then
        Throw New NotImplementedException($"Format version {format_version} unsupported")
    End If

    Return New RVersions With {
        .format = format_version,
        .serialized = r_version,
        .minimum = minimum_r_version
    }
End Function

''' <summary>
''' Parse the versions header.
''' </summary>
''' <param name="versions"></param>
''' <returns></returns>
Public Function parse_extra_info(versions As RVersions) As RExtraInfo
    Dim encoding As String = Nothing
    Dim encoding_len As Integer

    If versions.format >= 3 Then
        encoding_len = parse_int()
        encoding = parse_string(encoding_len).decode(Encodings.ASCII)
    End If

    Dim extract_info = New RExtraInfo With {.encoding = encoding}

    Return extract_info
End Function

可以看得到,RData版本号是由三个Integer数构成的。进行Integer数读取的parse_int函数就是我们进行Integer值的XDR解码函数调用。文本字符串编码信息,则是以字符串长度作为前缀的ASCII字符串的形式保存在RData文件之中。

解析RObject对象

前面我们提到,在RData数据集之中,所有的数据都是以二叉树加链表的形式存储在一个二进制文件之中。在我们所读取的链表中,数据单元被称作为RObject对象。下面所展示的代码,为大家展示了RData之中的基本单元里面具体包含有哪些数据信息:

''' <summary>
''' Representation of a R object.
''' </summary>
Public Class RObject

    Public Property info As RObjectInfo
    Public Property value As RList
    Public Property attributes As RObject
    Public Property tag As RObject
    Public Property referenced_object As RObject

End Class

Public Class RList
    Public Property CAR As RObject
    Public Property CDR As RObject
    Public Property data As Array
End Class

''' <summary>
''' Internal attributes of a R object.
''' </summary>
Public Class RObjectInfo

    Public type As RObjectType
    Public [object] As Boolean
    Public attributes As Boolean
    Public tag As Boolean
    Public gp As Integer
    Public reference As Integer

End Class

从上面的对象定义之中可以看得到,RObject对象就是在RData数据集之中的基本数据单元。无论是数据对象本身还是数据对象其所具有的属性,标签等元数据信息,都是RObject对象。对于链表+二叉树结构的实现,则是基于一个名称为RList的数据类型来实现的。

在RList之中,CAR属性或者data属性为在链表中当前的数据节点中的数据存储区。其中data属性用于存储向量数据,而CAR属性则用于存储组合类型的复杂数据关系,例如list,dataframe等结构化的非基础类型数据元件之间的关联信息。CDR则是链表之中的链接信息,通过CDR属性我们可以从当前节点跳转到下一个RObject数据节点进行数据读取操作。因为在RList之中,仅存在有CAR和CDR这两个属性进行链表的延申,所以基于这两个属性,由产生了一个二叉树的结构。为大家总结的进行理解RData文件格式的重点:

  • CAR为当前的符号对象的值存储链,读取当前的符号值所有的数据都可以通过读取CAR路径来实现
  • CDR则链接到了下一个符号对象的数据存储链,如果要读取其他的符号数据,则必须要通过CDR路径来实现

就这样,在RData之中,基于上面所展示的RObject链表+二叉树的结构,我们就可以在二进制数据流这样子的一维线性空间之中描述出list,dataframe这样子的结构化的多维度数据了。

RObject元数据读取

在学习RData文件格式的时候,最让我惊讶的是在RData之中对数据空间的利用最大化。例如对于上面所示的RObjectInfo元数据对象,看着里面那么多的属性信息,大家一般会首先想到的是通过多个integer或者bytes数据进行信息的存储。但是在RData之中并不是这样浪费空间,RData之中通过非常精妙的利用Integer的32为比特空间进行上面所展示的元数据信息的存储。是的,仅使用一个Integer数就可以保存上面的所有元数据信息:

''' <summary>
''' Parse the internal information of an object.
''' </summary>
''' <param name="info_int"></param>
''' <returns></returns>
Public Function parse_r_object_info(info_int As Integer) As RObjectInfo
    Dim type_exp As RObjectType = bits(info_int, 0, 8)
    Dim reference = 0
    Dim object_flag As Boolean
    Dim attributes As Boolean
    Dim tag As Boolean
    Dim gp As Integer

    If is_special_r_object_type(type_exp) Then
        object_flag = False
        attributes = False
        tag = False
        gp = 0
    Else
        object_flag = CBool(bits(info_int, 8, 9))
        attributes = CBool(bits(info_int, 9, 10))
        tag = CBool(bits(info_int, 10, 11))
        gp = bits(info_int, 12, 28)
    End If

    If type_exp = RObjectType.REF Then
        reference = bits(info_int, 8, 32)
    End If

    Return New RObjectInfo With {
        .type = type_exp,
        .[object] = object_flag,
        .attributes = attributes,
        .tag = tag,
        .gp = gp,
        .reference = reference
    }
End Function

从上面的代码中可以看得到,通过对不同的比特位的运算,我们就可以存储上面所展示的所有的元数据信息了。在RData格式中为什么会进行这样子的编码操作呢?我总结了一下,大致可能是下面的一个原因:必须要节省数据空间!

因为对于R环境而言,一般用RData文件保存大量的科学计算数据结果。因为RData是通过链表的形式来组成的,而在这个链表之中,一般会存在有非常多的RObject节点,这些Robject节点,有的用于存储真正的数据,有的用于存储元数据信息,并且每一个RObject节点,都有其各自的RObjectInfo元数据信息。所以必须要尽量的利用上每一个比特的空间。如果直接使用多个Byte或者Integer来存储元数据信息的话,随着所需要保存的数据量的上升,RData数据集里面的RObject对象的数量也会随着增加非常多,这样子光元数据信息的存储都将会占用非常大的空间。所以基于上面的原因,在RData之中,必须要基于比特位的运算充分的将所有数据空间利用起来以节省内存空间和硬盘空间。

RObject对象值类型

在下面的代码中定义了RObject所有可能的值类型:

' r object
NIL = 0, SYM = 1, LIST = 2, CLO = 3, ENV = 4, PROM = 5, LANG = 6, SPECIAL = 7, BUILTIN = 8
' element vector
CHAR = 9, LGL = 10, INT = 13, REAL = 14, CPLX = 15, STR = 16
' r language
DOT = 17, ANY = 18, VEC = 19, EXPR = 20, BCODE = 21, EXTPTR = 22, WEAKREF = 23, RAW = 24, S4 = 25
' alternative flags
ALTREP = 238, EMPTYENV = 242, GLOBALENV = 253, NILVALUE = 254, REF = 255

通过上面的代码对RObjectInfo元数据的读取,就可以得到对应的RObject的数据类型了。根据不同的数据类型,我们就需要不同的数据对象读取代码进行对应的读取操作。在这里,我仅仅为大家讲解RData之中的链表+二叉树数据结构的读取以及向量数据的读取操作的代码。其他的数据类型的RObject的读取操作,大家可以阅读R#的源代码文件。

链表结构的读取

在RData之中,目前只有LIST类型和LANG类型为链表结构。进行链表结构的数据,其实我们就只需要进行递归的进行RObject对象读取函数的调用即可。在读取的时候我们分别读取CAR和CDR节点的数据即可完成链表的构建以及二叉树的读取操作:

' parse_R_object
tag = Nothing

If info.attributes Then
    attributes = parse_R_object(reference_list)
    attributes_read = True
ElseIf info.tag Then
    tag = parse_R_object(reference_list)
    tag_read = True
End If

' Read CAR and CDR
' RData linked list
Dim car As RObject = parse_R_object(reference_list)
Dim cdr As RObject = parse_R_object(reference_list)

value = New RList With {.CAR = car, .CDR = cdr}

从上面的代码中我们可以看得到,我们在parse_R_object函数之中,递归的调用函数其自身进行CAR和CDR节点的读取,既可以构建出链表+二叉树的结构。

向量的读取

在RData之中的向量数据,我们可以通过一个通用的泛型函数进行相应的数据读取操作:

Private Function parseVector(Of T)(parse As Func(Of T)) As Array
    Dim length As Integer = parse_int()
    Dim value As T() = New T(length - 1) {}

    For i As Integer = 0 To length - 1
        value(i) = parse()
    Next

    Return value
End Function

对于RData之中的向量,一般是以一个用于表示向量长度的32位整数作为整个数据区的起始,然后向量元素值依次线性排布在后面。对于RData之中的向量,一般只有7种数据类型可以以向量的形式进行存储:

If info.type = RObjectType.LGL Then
    value = parseVector(AddressOf parse_bool)
ElseIf info.type = RObjectType.INT Then
    value = parseVector(AddressOf parse_int)
ElseIf info.type = RObjectType.REAL Then
    value = parseVector(AddressOf parse_double)
ElseIf info.type = RObjectType.CPLX Then
    value = parseVector(AddressOf parse_complex)
ElseIf info.type Like objType3 Then
    value = parseVector(Function() parse_R_object(reference_list))

上面所示的7种数据类型分别为:LGL逻辑值类型,INT整型数,REAL双精度浮点数,CPLX两个双精度浮点数组成的复数,STR字符串类型,EXPR表达式类型以及VEC数据类型。请注意,在这里的VEC类型并不是vector向量,而是元素数组的概念。例如,在list数据结构之中,元素值就是以数组类型存储的;在dataframe数据结构之中,列数据值也是以数组类型存储的。所以VEC在这里指的是list或者dataframe这类高维度数据的数据存储,而非向量这类基元类型的基础数据存储。

对于完整的RObject解析代码,大家可以阅读【RData/Reader.vb】源代码文件。

至此,整个RData文件已经被完全读取了,将我们所读取的元数据加RObject链表二叉树数据组装在一起,就可以得到RData对象数据了。

''' <summary>
''' Parse all the file.
''' </summary>
''' <returns></returns>
Public Function parse_all() As RData
    Dim versions As RVersions = parse_versions()
    Dim extra_info As RExtraInfo = parse_extra_info(versions)
    Dim obj As RObject = parse_R_object()

    Return New RData With {
        .versions = versions,
        .extra = extra_info,
        .[Object] = obj
    }
End Function

VisualBasic代码用例

在VisualBasic程序之中使用对应的API进行RData文件的读取操作非常的简单,只需要引用RData代码库项目之后,导入对应的数据集处理相关的命令空间,然后使用ParseData函数即可读取R语言的Rda文件:

Imports SMRUCC.Rsharp.RDataSet

Using buffer As Stream = file.Open(FileMode.Open, doClear:=False, [readOnly]:=True)
    Dim obj As Struct.RData = Reader.ParseData(buffer)
    ' blabla
End Using

可以看得见,我们通过执行上面的代码,成功的将目标rda数据文件进行读取,并且文件里面包含有一个变量名为test_dataframe的对象:

Latest posts by xie guigang (see all)

Attachments

One response

Leave a Reply

Your email address will not be published.