Pandas 系列的接口不一致;产生对底层数据的访问

Inconsistent interface of Pandas Series; yielding access to underlying data

虽然自 pandas 0.15.0 以来的新分类系列支持非常棒,但我对他们如何决定让基础数据无法访问(除非通过带下划线的变量)感到有点恼火。考虑以下代码:

import numpy as np
import pandas as pd

x = np.empty(3, dtype=np.int64)

s = pd.DatetimeIndex(x, tz='UTC')

x
Out[17]: array([140556737562568,        55872352,              32])

s[0]
Out[18]: Timestamp('1970-01-02 15:02:36.737562568+0000', tz='UTC')

x[0] = 0

s[0]
Out[20]: Timestamp('1970-01-01 00:00:00+0000', tz='UTC')

y = s.values

y[0] = 5

x[0]
Out[23]: 5

s[0]
Out[24]: Timestamp('1970-01-01 00:00:00.000000005+0000', tz='UTC')

我们可以看到,无论是在构建过程中还是在询问基础值时,都没有在此 DatetimeIndex 中对其基础数据进行深拷贝。这不仅在效率方面可能有用,而且如果您使用 DataFrame 作为缓冲区,这也很棒。您可以轻松获取包含底层数据的 numpy 原语,从那里获取指向原始数据的指针,一些低级 C 例程可以使用它从某个内存块复制到其中。

现在让我们看看新分类系列的行为。底层数据当然不是级别,而是代码。

x2 = np.zeros(3, dtype=np.int64)

s2 = pd.Categorical.from_codes(x2, ["hello", "bye"])

s2
Out[27]: 
[hello, hello, hello]
Categories (2, object): [hello, bye]

x2[0] = 1

s2[0]
Out[29]: 'hello'

y2 = s2.codes

y2[0] = 1
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-31-0366d645c98d> in <module>()
----> 1 y2[0] = 1

ValueError: assignment destination is read-only

y2 = s2._codes

y2[0] = 1

s2[0]
Out[34]: 'bye'

此行为的最终结果是,作为开发人员,对 Categoricals 基础数据的高效操作不是接口的一部分。同样作为用户,from_codes 构造函数很慢,因为它深度复制代码,这通常是不必要的。至少应该有一个选项。

但是 codes 是一个只读变量并且需要使用 _codes 的事实让我感到更糟。为什么 .codes 不会给出与 .values 相同的行为?除了代码是 "private" 的概念之外,是否还有其他理由可以证明这一点?我希望 Whosebug 上的一些 pandas 专家可以对此有所启发。

Categorical类型与几乎所有其他类型的不同之处在于,它是一种复合类型,其数据之间具有一定的保证。也就是说,代码提供了级别的因式分解。

因此,反对可变性的论点是,很容易破坏代码-类别映射,而且可能性能不佳。当然,可以通过检查 setitem 来减轻这些问题(但会增加一些代码复杂性)。

绝大多数用户不会直接操作 codes/categories(并且只会使用暴露的方法),因此这确实是一种防止意外破坏这些保证的保护措施。

如果需要高效的操作底层数据,best/easiest就是简单的把codes/categories拉出来。改变它们,然后创建一个新的分类(如果已经提供了 codes/categories,这很便宜)。

例如

In [3]: s2 = pd.Categorical.from_codes(x2, ["hello", "bye"])

In [4]: s2
Out[4]: 
[hello, hello, hello]
Categories (2, object): [hello, bye]

In [5]: s2.codes
Out[5]: array([0, 0, 0], dtype=int8)

In [6]: pd.Categorical(s2.codes+1,s2.categories,fastpath=True)
Out[6]: 
[bye, bye, bye]
Categories (2, object): [hello, bye]

当然这很危险,如果你在表达式中加 2 就会爆炸。直接操作代码简直就是买家当心。