估计阅读时长: 12 分钟

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

如果我们需要将上游的R数据分析环境之中的数据集串流至下游的R#数据分析环境之中,构建出一个不同的数据分析环境混合在一块的自动化数据分析流程。我们一般会需要将上游的R环境之中的数据符号对象以RData的格式串流到下游环境中,下游环境进行反序列化加载数据到环境中执行相应的分析。例如在下游执行定制化程度更高的数据作图,将数据以在上游R环境中比较困难实现的其他二进制文件格式进行保存,或者进行分布式的跨物理机的集群化计算,等等用于实现单纯依靠R环境所比较困难实现的功能。

从上一篇博客文章之中我们比较下详细的了解了RData数据文件的文件格式以及对应的读取操作。在这篇文章之中我们来了解如何基于我们通过对RData文件读取操作所获取得到的链表数据进行反序列化操作,将R环境之中的数据集串流加载到下游的R#数据分析环境之中。

RData反序列化基础

链表访问

我们在前面也了解到过,对于RData数据集而言,其格式为一个链表+二叉树的结构。所以在这里我们必须要通过特定的方法代码进行链表的访问,才可以正确的从整个链表中读取得到所有原始数据。假若我们需要获取得到当前的RObject数据中的某一个节点值的话,我们会需要通过一个While循环进行链表的递归访问和查找。下面的函数演示了如何在RData二叉树链表中查找对应键名的值:

<Extension>
Public Function LinkVisitor(robj As RObject, key As String) As RObject
    Dim tag As RObject

    Do While Not robj Is Nothing
        tag = robj.tag

        If tag Is Nothing AndAlso robj.referenced_object Is Nothing Then
            Return Nothing
        End If

        If tag.characters = key Then
            Return robj
        End If
        If Not tag.referenced_object Is Nothing AndAlso tag.referenced_object.characters = key Then
            Return robj
        End If

        robj = robj.value.CDR
    Loop

    Return Nothing
End Function

因为在二叉树链表之中,CAR属性都是存储的当前节点的数据的链表;CDR属性都是存储相邻的数据符号的链表。所以我们需要查找某一个特定的符号名称的值的话,在这里需要访问的是CDR属性。对于的符号名称值,是存储在数据对象的tag属性或者referenced_object属性之中。

上面的函数使用方法非常的简单,假若我们需要获取得到数据框对象的row.names属性值,我们可以通过上面的函数访问当前节点后续的row.names节点即可,例如:

data = attrs.LinkVisitor("row.names")

字符串解码

在RData数据集之中,所有的字符串数据都是以字节向量的形式存储在一个RObject节点之中。所以我们只需要将目标数据节点之中的所有字节向量数据拿出来,然后以特定的编码格式进行解码为字符串即可完成字符串的解码操作。

<Extension>
Public Function DecodeCharacters(r_char As RObject) As String
    If r_char.value Is Nothing Then
        Return ""
    ElseIf r_char.info.type = RObjectType.CHAR Then
        Dim bytes As Byte() = DirectCast(r_char.value.data, Byte())
        Dim encoding As Encoding = Encoding.UTF8

        If r_char.info.gp And CharFlags.UTF8 Then
            encoding = Encoding.UTF8
        ElseIf r_char.info.gp And CharFlags.LATIN1 Then
            encoding = Encoding.Latin1
        ElseIf r_char.info.gp And CharFlags.ASCII Then
            encoding = Encoding.ASCII
        ElseIf r_char.info.gp And CharFlags.BYTES Then
            encoding = Encoding.ASCII
        End If

        Return encoding.GetString(bytes)
    Else
        Return ""
    End If
End Function

对于编码信息,可以通过每一个Robject节点对象的元数据信息里面的gp属性进行计算获取得到。我们需要将gp属性与对应的编码格式的枚举值Integer数进行与运算。如果与运算的结果等于1,则表示TRUE,表示对应的枚举值的编码格式即为我们所需要用于进行字符串解码所需的格式。下面列举出了在RData之中所支持的所有的文本编码格式的枚举值:

Public Enum CharFlags
    HAS_HASH = 1
    BYTES    = 1 << 1
    LATIN1   = 1 << 2
    UTF8     = 1 << 3
    CACHED   = 1 << 5
    ASCII    = 1 << 6
End Enum

向量读取

在R环境之中,向量是最基础的数据类型。在R环境之中,所有的数据类型都是基于向量来实现的,例如list列表,列表中的元素最基本不可分的单元就是向量元素。对于dataframe数据类型而言,每一列数据就是等长的向量。从上一篇文章中可以了解到,向量都是以连续分布的线性格式存储在RData之中。所以我们解析RData数据集的时候,向量就可以在R#环境中转换为一个数组对象,存储在链表节点的data属性中。基于此,在这里我们就可以通过相应的方法得到链表节点中的向量数据:

Public Class RStreamReader

    <MethodImpl(MethodImplOptions.AggressiveInlining)>
    Public Shared Function ReadString(robj As RObject) As String
        Return robj.DecodeCharacters
    End Function

    <MethodImpl(MethodImplOptions.AggressiveInlining)>
    Public Shared Function ReadNumbers(robj As RObject) As Double()
        Return REnv.asVector(Of Double)(robj.value.data)
    End Function

    <MethodImpl(MethodImplOptions.AggressiveInlining)>
    Public Shared Function ReadIntegers(robj As RObject) As Long()
        Return REnv.asVector(Of Long)(robj.value.data)
    End Function

    <MethodImpl(MethodImplOptions.AggressiveInlining)>
    Public Shared Function ReadLogicals(robj As RObject) As Boolean()
        Return REnv.asVector(Of Boolean)(robj.value.data)
    End Function

    ''' <summary>
    ''' read R vector in any element data type
    ''' </summary>
    ''' <param name="robj"></param>
    ''' <returns></returns>
    Public Shared Function ReadVector(robj As RObject) As Array
        Select Case robj.info.type
            Case RObjectType.CHAR : Return ReadString(robj).ToArray
            Case RObjectType.STR : Return ReadStrings(robj)
            Case RObjectType.INT : Return ReadIntegers(robj)
            Case RObjectType.REAL : Return ReadNumbers(robj)
            Case RObjectType.LGL : Return ReadLogicals(robj)
            Case Else
                Throw New NotImplementedException(robj.info.ToString)
        End Select
    End Function
End Class

对于字符串向量的读取操作,要稍微麻烦一些。因为在RData之中,字符串向量一般存在有两种形式:CHAR向量和STR向量。因为二者表示的含义有一些差别,所以我们不得不通过一个额外的方法进行字符串向量的读取操作。

  • 对于CHAR向量,其真实数据为字符集合,在RData之中是以字节数组的形式存储,其也是STR向量的底层数据类型
  • 对于STR向量,其为字符串集合,其建立在CHAR向量的基础上,STR向量是基于多个CHAR向量堆叠而成的向量数据。虽然STR向量有点类似于复合数据类型,但是在R环境中其任然被认为是一种不可分的基元类型

虽然二者在RData数据底层中的表示方法不一致,但是在R数据分析环境之中,二者存在等价性。所以因为这个特点我们会需要使用一个额外的函数用于STR向量的读取:

Friend Shared Function ReadStrings(robj As Object) As String()
    If TypeOf robj Is RList Then
        Dim rlist As RList = DirectCast(robj, RList)

        If rlist.nodeType = ListNodeType.LinkedList Then
            Return ReadStrings(rlist.CAR)
        Else
            Return DirectCast(rlist.data, RObject()) _
                .Select(AddressOf ReadString) _
                .ToArray
        End If

    ElseIf DirectCast(robj, RObject).info.type = RObjectType.LIST Then
        Return ReadStrings(DirectCast(robj, RObject).value)
    Else
        Dim obj As RObject = DirectCast(robj, RObject)

        If obj.info.type = RObjectType.STR Then
            Return DirectCast(obj.value.data, RObject()) _
                .Select(AddressOf ReadString) _
                .ToArray
        Else
            Return {ReadString(obj)}
        End If
    End If
End Function

到这里为止,我们已经具备了进行反序列化操作所需要的所有的基础代码。现在我们开始来详细的讲解在RData中我们主要使用到的,对vector,list和dataframe类型的数据的反序列化操作。

R数据类型反序列化

反序列化vector

在RData之中,仅存在有有限的几种基础数据类型可以以向量的形式存储。在下面的代码之中,我们列举出来了所有支持向量存储的基础数据类型:

' R storage units (nodes)
' Two types: SEXPREC(non-vectors) And VECTOR_SEXPREC(vectors)

' Node: VECTOR_SEXPREC
' The vector types are RAWSXP, CHARSXP, LGLSXP, INTSXP, REALSXP, CPLXSXP, STRSXP, VECSXP, EXPRSXP And WEAKREFSXP.
Friend ReadOnly elementVectorFlags As Index(Of RObjectType) = {
    RObjectType.CPLX,
    RObjectType.EXPR,
    RObjectType.INT,
    RObjectType.LGL,
    RObjectType.RAW,
    RObjectType.REAL,
    RObjectType.CHAR,
    RObjectType.STR,
    RObjectType.WEAKREF
}

在前面我们已经知道了基础数据类型的向量数据是如何进行读取的。所以在这里我们直接使用前面所提到的方法进行向量的建立即可。对于在.NET Core环境中创建一个R#环境中的向量类型,我们可以从RObject中等价的获取得到对应的元素值类型,向量值数组等信息。然后基于这些数据创建出一个在.NET Core环境中的向量数据对象:

<Extension>
Private Function CreateRVector(robj As RObject) As vector
    Dim type As RType = robj.info.GetRType
    Dim data As Array = RStreamReader.ReadVector(robj)
    Dim factor As factor = If(RObjectSignature.HasFactor(robj), robj.attributes.value.CreateFactor, Nothing)
    Dim vec As New vector(data, type) With {
        .factor = factor
    }

    Return vec
End Function

在R环境之中,除了上面所提到的基础数据类型。还存在有一种字符串数据类型的变种:factor值。所以对于向量中的值,我们还需要进行factor值的额外检测。对于RObject对象之中是否存在factor值,我们只需要检查attributes之中是否存在有一个名称为"levels"的RObject对象即可。如果存在的话,将对应的factor levels字符串向量读取出来,然后做factor枚举与原始字符串数组之间的相互转换。

factor值的读取,就是对一个字符串数组的读取操作:

<Extension>
Private Function CreateFactor(robj As RList) As factor
    Dim data = robj.CAR
    Dim levels As String() = RStreamReader.ReadStrings(data)
    Dim factor As factor = factor.CreateFactor(levels, ordered:=True)

    Return factor
End Function

对于将factor类型的向量,重新转换为原始的字符串向量,我们可以在代码里面执行:

strs = factor.asCharacter(vector)

上面的代码的效果等价于在R环境之中执行as.vector函数进行factor转换。



在VisualStudio之中调试我们的代码,可以看见,向量基础数据已经可以被我们正确的反序列解码出来了。

反序列化list

对于list列表对象的反序列化所需要的元素值数据,在RData之中都是以连续分布的RObject数组的方式存储的。所以我们在进行RData文件读取操作的时候,就可以很方便的将list中的元素值列表存储进入list对象的链表节点的data数组属性之中。在进行对应的反序列化操作之中,我们就可以直接对列表的链表节点中的data进行递归反序列操作即可。整个过程非常的简单

Dim elements As RObject() = robj.value.data
Dim list As New list

For i As Integer = 0 To names.Length - 1
    Call list.add(names(i), ConvertToR.PullRObject(elements(i), Nothing))
Next

Return list

在上面的过程中,由于list列表之中的元素可能不是基础不可分的vector数据类型,所以我们需要进行递归反序列化操作。整个反序列化操作过程通过对ConvertToR.PullRObject函数的递归调用来完成。

对于list列表中的每一个元素的name属性值,则是存储于attributes节点之中,我们找到名称为names的链表数据节点,然后读取出对应的names字符串向量值即可。

If attrTags IsNot Nothing AndAlso attrTags.tag IsNot Nothing Then
    Dim tag As RObject = attrTags.tag

    If tag.characters = "names" Then
        names = RStreamReader.ReadStrings(attrTags.value)
    ElseIf tag.referenced_object IsNot Nothing AndAlso tag.referenced_object.characters = "names" Then
        names = RStreamReader.ReadStrings(attrTags.value)
    End If
End If



反序列化dataframe

最后就是在R数据分析环境之中非常常用的dataframe对象的反序列操作了。dataframe对象中的数据存储格式与list类型的格式是一样的。但是在attributes属性之中存在有row.names属性值,所以我们只需要通过判断Robject节点之中是否存在有row.names属性值,即可判断目标RObject是一个list列表对象还是dataframe数据框对象。

Public Shared Function IsPairList(robj As RObject) As Boolean
    Dim attrs As RObject = robj.attributes

    If attrs Is Nothing Then
        If robj.info.type Like ConvertToR.elementVectorFlags Then
            Return False
        Else
            Return True
        End If
    End If

    If attrs.LinkVisitor("row.names") IsNot Nothing Then
        Return False
    End If

    Return True
End Function

上面的函数用于判断目标RObject是否一个列表类型。因为dataframe和list二者在数据存储结构上一致的原因,并且dataframe的数据组织维度要高于list类型数据。所以我们需要在list函数之中优先判断目标是否为数据框对象。

在判断完目标对象类型为数据框类型之后,我们只需要按照与list类型相同的方式进行反序列读取操作即可。数据框之中的colnames属性值就是list之中的names属性值:

<Extension>
Private Function CreateRTable(robj As RObject) As dataframe
    Dim columns As RObject() = robj.value.data
    Dim colnames As String() = robj.readColumnNames
    Dim vector As vector
    Dim table As New dataframe With {
        .columns = New Dictionary(Of String, Array),
        .rownames = robj.readRowNames
    }

    For i As Integer = 0 To colnames.Length - 1
        vector = columns(i).CreateRVector

        If vector.factor Is Nothing Then
            table.columns(colnames(i)) = vector.data
        Else
            table.columns(colnames(i)) = factor.asCharacter(vector)
        End If
    Next

    Return table
End Function

对于rownames属性值,我们只需要找到名称为row.names字符串值的RObject数据节点,然后读取里面的字符串向量值即可。



获取所有符号列表

在RData数据集之中,一般是保存有多个数据符号。这些数据符号以二叉树+链表的形式存储在RData之中。所以我们需要以特定的方法进行RData数据链表的访问,在反序列化RData数据集到.NET Core数据分析环境之中的时候一次性的获取得到所有的数据符号对象。

根据链表节点的属性值的赋值状态,我们一般可以将RObject节点分为三种类型:

Public Class RList

    Public ReadOnly Property nodeType As ListNodeType
        Get
            If data Is Nothing AndAlso CAR Is Nothing AndAlso CDR Is Nothing Then
                Return ListNodeType.NA
            ElseIf Not data Is Nothing Then
                Return ListNodeType.Vector
            Else
                Return ListNodeType.LinkedList
            End If
        End Get
    End Property

End Class
  • 所有的属性值都是空值,则是空值NA状态
  • 当data属性值不为空的时候,CAR和CDR两个属性节点一定为空值。这个时候的RObject是二叉树中的叶子节点,仅存储有向量数据
  • 当data属性值为空的时候,CAR和CDR二者肯定有一个属性不为空值。这个时候RObject节点为一个链表中的链接节点

所以我们可以依照RObject上面所描述的三种状态,编写出对应的访问二叉树+链表的数据结构的访问代码用于读取整个RData对象树完成反序列化加载操作:

''' <summary>
''' Pull all R# object from the RData linked list
''' </summary>
''' <param name="rdata"></param>
''' <returns></returns>
''' 
<Extension>
Private Function PullRObject(rdata As RObject, list As Dictionary(Of String, Object)) As Object
    Dim value As RList = rdata.value
    Dim car As RObject = value.CAR

    If value.nodeType = ListNodeType.NA Then
        Return Nothing
    ElseIf value.nodeType = ListNodeType.Vector Then
        ' 已经没有数据了,结束递归
        If RObjectSignature.IsPairList(rdata) Then
            Return rdata.CreatePairList
        ElseIf RObjectSignature.IsDataFrame(rdata) Then
            Return rdata.CreateRTable
        Else
            Return rdata.CreateRVector
        End If
    Else
        ' CAR为当前节点的数据
        ' 获取节点数据,然后继续通过CDR进行链表的递归访问
        Dim current As Object = PullRObject(car, list)
        Dim currentName As String = rdata.tag.characters
        Dim CDR As RObject = value.CDR

        ' pull an object
        Call list.Add(currentName, current)

        If CDR Is Nothing Then
            Return current
        Else
            ' 产生一个列表
            Return PullRObject(CDR, list)
        End If
    End If
End Function

可以看得到,在上面的代码之中,我们根据当前的RObject节点的状态值,分别进行三种对应的操作:

  • NA状态的时候,已经到了叶节点处,并且叶节点中没有存储任何数据。则直接返回空值,退出整个递归
  • 处于Vector状态的时候,也属于访问到了整个链表中的叶节点处。但是当前的RObject节点之中存储有数组或者向量数据,所以从这里开始我们可以进行上面所描述的向量类型,列表类型以及数据框类型的反序列化读取操作
  • 最后,处于LinkedList链表状态的时候,说明在当前的RObject节点之后还存在有其他的符号对象值。我们在读取完当前节点的对象值之后,还需要访问CDR节点的值,进行整个二叉树链表的递归访问。

这样,我们通过上面所展示的方法,就可以递归的访问完整个RData二叉树链表中的所有的数据对象了。

R#脚本DEMO测试

我已经将上面所提到的RData数据集反序列化过程开放为一个在R#基础环境之中的api函数,用于支持进行在R#环境之中对R环境中的数据读取操作:

<ExportAPI("readRData")>
Public Function parseRData(file As String) As Object
    Using buffer As Stream = file.Open(FileMode.Open, doClear:=False, [readOnly]:=True)
        Dim obj As Struct.RData = Reader.ParseData(buffer)
        Dim symbols As list = ConvertToR.ToRObject(obj.object)

        Return symbols
    End Using
End Function

假设我们现在有上面所示的4个对象的数据集需要从上游的R环境之中串流至下游的R#数据分析环境之中进行作图或者报告输出等操作。这个时候,我们可以在R#脚本中进行对readRData函数的调用,读取通过RStudio所保存的RData文件即可:

list = base::readRData("multiple_object.rda");

print("get all symbols in target RData dataset:");
print(names(list));

print("===============================================");

for(name in names(list)) {
    print("symbol name:");
    print(name);

    cat("\n");

    if (typeof list[[name]] is "data.frame") {
        print(list[[name]]);
    } else {
        str(list[[name]]);
    }

    cat("\n\n");
}

执行一下,可以看得到,在R#环境之中的RData数据集加载结果与R环境中的原始数据之间保持一致。

Attachments

3 Responses

Leave a Reply

Your email address will not be published.