PDEP-12: 紧凑且可逆的 JSON 接口

摘要

摘要

问题描述

当前的 JSON 接口没有明确考虑 dtype 和“Python 类型”。

因此,JSON 接口并不总是可逆的,并且在考虑 dtype 时存在不一致之处。

另一个结果是在 orient="table" 选项中部分应用 Table Schema 规范(24 个定义的 Table Schema 数据类型中只考虑了 6 个)。

一些 JSON 接口问题在 链接的 Notebook 中有详细说明。

功能描述

为了获得一个简单、紧凑且可逆的解决方案,我建议使用 JSON-NTV 格式(命名和类型值) - 它集成了类型的概念 - 以及它的 JSON-TAB 变体用于表格数据(JSON-NTV 格式在 IETF Internet-Draft 中定义(尚未成为 RFC !!))。

此解决方案允许包含大量类型(不一定为 pandas dtype),从而允许拥有

全局 JSON 接口示例

在下面的示例中,一个包含多种数据类型的 DataFrame 被转换为 JSON。

从该 JSON 生成的 DataFrame 与初始 DataFrame 相同(可逆性)。

使用现有的 JSON 接口,这种转换是不可能的。

此示例使用在 ntv-pandas 仓库 中定义的 ntv_pandas 模块。

数据示例

In [1]: from shapely.geometry import Point
        from datetime import date
        import pandas as pd
        import ntv_pandas as npd

In [2]: data = {'index':           [100, 200, 300, 400, 500, 600],
                'dates::date':     [date(1964,1,1), date(1985,2,5), date(2022,1,21), date(1964,1,1), date(1985,2,5), date(2022,1,21)],
                'value':           [10, 10, 20, 20, 30, 30],
                'value32':         pd.Series([12, 12, 22, 22, 32, 32], dtype='int32'),
                'res':             [10, 20, 30, 10, 20, 30],
                'coord::point':    [Point(1,2), Point(3,4), Point(5,6), Point(7,8), Point(3,4), Point(5,6)],
                'names':           pd.Series(['john', 'eric', 'judith', 'mila', 'hector', 'maria'], dtype='string'),
                'unique':          True }

In [3]: df = pd.DataFrame(data).set_index('index')

In [4]: df
Out[4]:       dates::date  value  value32  res coord::point   names  unique
        index
        100    1964-01-01     10       12   10  POINT (1 2)    john    True
        200    1985-02-05     10       12   20  POINT (3 4)    eric    True
        300    2022-01-21     20       22   30  POINT (5 6)  judith    True
        400    1964-01-01     20       22   10  POINT (7 8)    mila    True
        500    1985-02-05     30       32   20  POINT (3 4)  hector    True
        600    2022-01-21     30       32   30  POINT (5 6)   maria    True

JSON 表示

In [5]: df_to_json = npd.to_json(df)
        pprint(df_to_json, width=120)
Out[5]: {':tab': {'coord::point': [[1.0, 2.0], [3.0, 4.0], [5.0, 6.0], [7.0, 8.0], [3.0, 4.0], [5.0, 6.0]],
                  'dates::date': ['1964-01-01', '1985-02-05', '2022-01-21', '1964-01-01', '1985-02-05', '2022-01-21'],
                  'index': [100, 200, 300, 400, 500, 600],
                  'names::string': ['john', 'eric', 'judith', 'mila', 'hector', 'maria'],
                  'res': [10, 20, 30, 10, 20, 30],
                  'unique': [True, True, True, True, True, True],
                  'value': [10, 10, 20, 20, 30, 30],
                  'value32::int32': [12, 12, 22, 22, 32, 32]}}

可逆性

In [5]: df_from_json = npd.read_json(df_to_json)
        print('df created from JSON is equal to initial df ? ', df_from_json.equals(df))
Out[5]: df created from JSON is equal to initial df ?  True

链接的 NoteBook 中提供了其他几个示例

Table Schema JSON 接口示例

在下面的示例中,一个包含多种 Table Schema 数据类型的 DataFrame 被转换为 JSON。

从该 JSON 生成的 DataFrame 与初始 DataFrame 相同(可逆性)。

使用现有的 Table Schema JSON 接口,这种转换是不可能的。

In [1]: from shapely.geometry import Point
        from datetime import date

In [2]: df = pd.DataFrame({
            'end february::date': ['date(2023,2,28)', 'date(2024,2,29)', 'date(2025,2,28)'],
            'coordinates::point': ['Point([2.3, 48.9])', 'Point([5.4, 43.3])', 'Point([4.9, 45.8])'],
            'contact::email':     ['[email protected]', '[email protected]', '[email protected]']
            })

In [3]: df
Out[3]: end february::date coordinates::point             contact::email
        0         2023-02-28   POINT (2.3 48.9)         john.doe@table.com
        1         2024-02-29   POINT (5.4 43.3)    lisa.minelli@schema.com
        2         2025-02-28   POINT (4.9 45.8)  walter.white@breaking.com

JSON 表示

In [4]: df_to_table = npd.to_json(df, table=True)
        pprint(df_to_table, width=140, sort_dicts=False)
Out[4]: {'schema': {'fields': [{'name': 'index', 'type': 'integer'},
                               {'name': 'end february', 'type': 'date'},
                               {'name': 'coordinates', 'type': 'geopoint', 'format': 'array'},
                               {'name': 'contact', 'type': 'string', 'format': 'email'}],
                    'primaryKey': ['index'],
                    'pandas_version': '1.4.0'},
         'data': [{'index': 0, 'end february': '2023-02-28', 'coordinates': [2.3, 48.9], 'contact': '[email protected]'},
                  {'index': 1, 'end february': '2024-02-29', 'coordinates': [5.4, 43.3], 'contact': '[email protected]'},
                  {'index': 2, 'end february': '2025-02-28', 'coordinates': [4.9, 45.8], 'contact': '[email protected]'}]}

可逆性

In [5]: df_from_table = npd.read_json(df_to_table)
        print('df created from JSON is equal to initial df ? ', df_from_table.equals(df))
Out[5]: df created from JSON is equal to initial df ?  True

链接的 NoteBook 中提供了其他几个示例

范围

目标是为任何类型的数据以及 orient="table" 选项或新的 orient="ntv" 选项提供建议的 JSON 接口。

建议的接口与现有数据兼容。

动机

为什么要将 orient=table 选项扩展到其他数据类型?

为什么拥有紧凑且可逆的 JSON 接口很重要?

考虑扩展类型是否相关?

这是否只对 pandas 有用?

描述

提出的解决方案基于以下几个关键点:

数据类型

数据类型在 NTV 项目中定义和管理(名称、JSON 编码器和解码器)。

Pandas dtype 与 NTV 类型兼容

pandas dtype NTV 类型
intxx intxx
uintxx uintxx
floatxx floatxx
datetime[ns] datetime
datetime[ns, ] datetimetz
timedelta[ns] durationiso
string string
boolean boolean

注意

JSON 类型(隐式或显式)在 dtype 中转换,遵循 pandas JSON 接口

JSON 类型 pandas dtype
number int64 / float64
string string / object
array object
object object
true, false boolean
null NaT / NaN / None

注意

其他 NTV 类型与 object dtype 相关联。

TableSchema 与 pandas 之间的对应关系

TableSchema 类型由两个属性 formattype 携带。

下表显示了 TableSchema 格式/类型与 pandas NTVtype/dtype 之间的对应关系

format / type NTV type / dtype
default / datetime / datetime64[ns]
default / number / float64
default / integer / int64
default / boolean / bool
default / string / object
default / duration / timedelta64[ns]
email / string email / string
uri / string uri / string
default / object object / object
default / array array / object
default / date 日期 / 对象
默认 / 时间 时间 / 对象
默认 / 年份 年份 / int64
默认 / 年月 月份 / int64
数组 / 地理点 点 / 对象
默认 / GeoJSON GeoJSON / 对象

注意

JSON 格式

TableSchema 接口的 JSON 格式是现有的。

Global 接口的 JSON 格式在 JSON-TAB 规范中定义。它包括最初在 JSON-ND 项目 中定义的命名规则,并支持分类数据。该规范需要更新以包含稀疏数据。

转换

当数据与非object dtype 关联时,使用 pandas 转换方法。否则,使用 NTV 转换。

pandas -> JSON

JSON -> pandas

使用和影响

使用

在我看来,这个提案解决了重要问题

兼容性

接口可以在没有 NTV 类型的情况下使用(与现有数据兼容 - 查看示例

如果接口可用,在 JSON 接口中添加一个新的 orient 选项,该功能的使用与其他功能解耦。

对 pandas 框架的影响

最初,影响非常有限

在后续阶段,可以考虑几个发展方向。

不做/不做风险

JSON-NTV 格式和 JSON-TAB 格式目前尚未被识别和使用。对于 pandas 来说,这种功能不被使用(没有功能影响)的风险是存在的。

另一方面,pandas 早期使用这种功能将有助于更好地考虑 pandas 的预期和需求,以及对 pandas 支持的类型演变的思考。

实现

模块

为 NTV 定义了两个模块。

pandas 对 JSON 接口的集成只需要导入 json-ntv 模块。

实现选项

该接口可以实现为 NTV 连接器(SeriesConnectorDataFrameConnector),也可以实现为新的 pandas JSON 接口 orient 选项。

pandas 的实现方式有多种。

  1. 外部

    在这种实现方式中,接口仅在 NTV 端可用。这意味着这种 JSON 接口的演变对 pandas 来说没有用处或战略意义。

  2. NTV 端

    在这种实现方式中,接口在两端都可用,转换位于 NTV 内部。这种方式可以最大程度地减少对 pandas 端的影响。

  3. pandas 端

    在这种实现方式中,接口在两端都可用,转换位于 pandas 内部。这种方式可以让 pandas 控制这种演变。

  4. pandas 受限

    在这种实现方式中,pandas 接口和转换位于 pandas 内部,并且仅适用于非对象 dtype。这种方式可以提供一个紧凑且可逆的接口,同时禁止引入与现有 dtype 不兼容的类型。

常见问题解答

问:orient="table" 是否已经实现了您提出的功能?

:原则上,是的,这个选项考虑了类型的概念。

但它非常有限(请参阅 笔记本 中添加的示例)。

当前接口与 table-schema 定义的数据结构不兼容。要实现兼容,需要集成一个“类型扩展”,类似于所提出的扩展(这在一定程度上已经通过接口中针对多种格式的 extDtype 概念实现了)。

问:一般来说,我们应该只在 pandas 的 read_json/to_json 中使用 1 个 "table" 格式。如果我们更改格式,还会存在向后兼容性问题。表格接口存在 bug 并不是添加新接口的理由(我更愿意修复这些 bug)。现有的格式能否以某种方式进行调整以解决类型问题/往返转换问题?

:我将补充两点说明。

我认为问题不能仅仅通过修复 bug 来解决,需要制定一个明确的 JSON 接口策略,特别是随着开放数据解决方案中逐渐放弃过时的 CSV 格式而转向 JSON 格式。

如前所述,提议的解决方案解决了当前接口的几个缺陷,并且可以轻松地集成到 pandas 环境中(另一个选择是考虑 Json 接口是 pandas 的外围功能,可以保持在 pandas 之外),无论 orient='table' 选项如何。

尽管如此,仍然可以合并提议的格式和 orient='table' 格式,以便对 extDtype 概念进行显式管理。

问:据我所知,JSON NTV 并非任何形式的标准化 JSON 格式。我认为 pandas(以及 geopandas,我从那里遇到了这个问题)应该尝试遵循事实上的或法定的标准,并且不选择目前没有任何社区支持的文件格式。这显然可以在未来改变,这就是应该修改此 PR 的地方。为什么 pandas 会使用此标准?

:如问题中所述(并在 附带的 Notebook 中详细说明),json 接口不可逆(to_json 然后 read_json 并不总是返回初始对象),并且存在几个缺陷和错误。这个问题的主要原因是数据类型在 JSON 格式中没有被考虑(或者在 orient='table' 选项中只被部分考虑)。

提出的方案解决了这个问题(Notebook 开头的示例 简单明了地说明了该方案的优势)。

关于底层的 JSON-NTV 格式,它对表格数据的影響很小(它仅限于在字段名称中添加类型)。然而,这个问题是相关的:JSON-NTV 格式(IETF 互联网草案)是一种共享的、有文档记录的、受支持的和已实现的格式,但实际上社区支持目前有限,但它只需要扩展!

综合

总之,

核心团队决定

投票从 9 月 11 日到 9 月 26 日开放。

反对意见 :

决定:

时间线

不适用

PDEP 历史