如何将任意元数据分配给pyarrow.Table / Parquet列

时间:2019-04-06 04:52:39

标签: python pandas gis parquet pyarrow

用例

我将Apache Parquet文件用作快速的IO格式,用于处理我在GeoPandas中使用Python处理的大型空间数据。我将要素几何存储为WKB,并希望将坐标参考系统(CRS)记录为与WKB数据关联的元数据。

代码问题

我正在尝试将任意元数据分配给pyarrow.Field对象。

我尝试过的事情

假设table是从pyarrow.Table实例化到df的{​​{1}}:

pandas.DataFrame

根据df = pd.DataFrame({ 'foo' : [1, 3, 2], 'bar' : [6, 4, 5] }) table = pa.Table.from_pandas(df) 文档,列元数据包含在属于pyarrowsource)的field中,并且可以将可选元数据添加到{ {1}}(source)。

如果我尝试为schema属性分配值,则会引发错误:

field

如果我尝试将一个字段(具有通过metadata方法关联的元数据)分配给一个字段,则会返回错误:

>>> table.schema.field_by_name('foo').metadata = {'crs' : '4283'}
AttributeError: attribute 'metadata' of 'pyarrow.lib.Field' objects is not writable

>>> table.column(0).field.metadata = {'crs' : '4283'}
AttributeError: attribute 'metadata' of 'pyarrow.lib.Field' objects is not writable

我什至尝试将元数据分配给add_metadata对象,例如

>>> table.schema.field_by_name('foo') = (
           table.schema.field_by_name('foo').add_metadata({'crs' : '4283'})
           )
SyntaxError: can't assign to function call

>>> table.column(0).field = table.column(0).field.add_metadata({'crs' : '4283'})
AttributeError: attribute 'field' of 'pyarrow.lib.Column' objects is not writable

,但是在pandas.Series对象的df['foo']._metadata.append({'crs' : '4283'}) 属性上调用pandas_metadatadocs)方法时,该数据不会在元数据中返回。

研究

在stackoverflow上,this个问题仍未得到解答,与this相关的问题涉及Scala,而不是Python和schemaElsewhere我已经看到了与table对象相关联的元数据,但是仅仅是从头实例化了pyarrowpyarrow.Field对象。

PS

这是我第一次发布到stackoverflow,因此在此先感谢您为任何错误致歉。

2 个答案:

答案 0 :(得分:0)

“箭头”中的“所有内容”都是不可变的,因此,正如您所经历的那样,您不能简单地修改任何字段或架构的元数据。唯一的方法是使用添加的元数据创建一个“ new” 表。我将 new 放在引号之间,因为可以做到这一点而无需实际复制表,因为在幕后这只是移动指针。以下代码显示了如何在Arrow元数据中存储任意字典(只要它们是json可序列化的)以及如何检索它们:

def set_metadata(tbl, col_meta={}, tbl_meta={}):
    """Store table- and column-level metadata as json-encoded byte strings.

    Table-level metadata is stored in the table's schema.
    Column-level metadata is stored in the table columns' fields.

    To update the metadata, first new fields are created for all columns.
    Next a schema is created using the new fields and updated table metadata.
    Finally a new table is created by replacing the old one's schema, but
    without copying any data.

    Args:
        tbl (pyarrow.Table): The table to store metadata in
        col_meta: A json-serializable dictionary with column metadata in the form
            {
                'column_1': {'some': 'data', 'value': 1},
                'column_2': {'more': 'stuff', 'values': [1,2,3]}
            }
        tbl_meta: A json-serializable dictionary with table-level metadata.
    """
    # Create updated column fields with new metadata
    if col_meta or tbl_meta:
        fields = []
        for col in tbl.itercolumns():
            if col.name in col_meta:
                # Get updated column metadata
                metadata = col.field.metadata or {}
                for k, v in col_meta[col.name].items():
                    metadata[k] = json.dumps(v).encode('utf-8')
                # Update field with updated metadata
                fields.append(col.field.add_metadata(metadata))
            else:
                fields.append(col.field)

        # Get updated table metadata
        tbl_metadata = tbl.schema.metadata
        for k, v in tbl_meta.items():
            tbl_metadata[k] = json.dumps(v).encode('utf-8')

        # Create new schema with updated field metadata and updated table metadata
        schema = pa.schema(fields, metadata=tbl_metadata)

        # With updated schema build new table (shouldn't copy data)
        # tbl = pa.Table.from_batches(tbl.to_batches(), schema)
        tbl = pa.Table.from_arrays(list(tbl.itercolumns()), schema=schema)

    return tbl


def decode_metadata(metadata):
    """Arrow stores metadata keys and values as bytes.
    We store "arbitrary" data as json-encoded strings (utf-8),
    which are here decoded into normal dict.
    """
    if not metadata:
        # None or {} are not decoded
        return metadata

    decoded = {}
    for k, v in metadata.items():
        key = k.decode('utf-8')
        val = json.loads(v.decode('utf-8'))
        decoded[key] = val
    return decoded


def table_metadata(tbl):
    """Get table metadata as dict."""
    return decode_metadata(tbl.schema.metadata)


def column_metadata(tbl):
    """Get column metadata as dict."""
    return {col.name: decode_metadata(col.field.metadata) for col in tbl.itercolumns()}


def get_metadata(tbl):
    """Get column and table metadata as dicts."""
    return column_metadata(tbl), table_metadata(tbl)

简而言之,您可以使用添加的元数据创建新字段,将字段聚合到新架构中,然后根据现有表和新架构创建新表。一切都太漫长了。理想情况下,pyarrow具有方便的功能,可以用更少的代码行来完成此操作,但是最后我检查了这是执行此操作的唯一方法。

唯一的麻烦是元数据以字节形式存储在Arrow中,因此在上面的代码中,我将元数据存储为json可序列化字典,并在utf-8中进行了编码。

答案 1 :(得分:0)

以下是解决此问题的较简单方法:

import pandas as pd

df = pd.DataFrame({
        'foo' : [1, 3, 2],
        'bar' : [6, 4, 5]
        })

table = pa.Table.from_pandas(df)

your_schema = pa.schema([
    pa.field("foo", "int64", False, metadata={"crs": "4283"}),
    pa.field("bar", "int64", True)],
    metadata={"diamond": "under_pressure"})

table2 = table.cast(your_schema)

table2.field('foo').metadata[b'crs'] # => b'4283'

我还添加了一个架构元数据字段以显示其工作原理。

table2.schema.metadata[b'diamond'] # => b'under_pressure'

请注意,元数据键/值是字节字符串-这就是为什么它是b'under_pressure'而不是'under_pressure'的原因。因为Parquet是二进制文件格式,所以需要字节字符串。有关合并元数据架构的更多详细信息和其他方法,请参见this blog post