Pandas 中 str 和 object 类型的区别

54
Numpy似乎区分strobject类型。例如,我可以这样做:
>>> import pandas as pd
>>> import numpy as np
>>> np.dtype(str)
dtype('S')
>>> np.dtype(object)
dtype('O')

dtype('S')dtype('O')分别对应于strobject

然而,Pandas似乎缺乏这种区分,并将str强制转换为object

>>> df = pd.DataFrame({'a': np.arange(5)})
>>> df.a.dtype
dtype('int64')
>>> df.a.astype(str).dtype
dtype('O')
>>> df.a.astype(object).dtype
dtype('O')

将类型强制转换为dtype('S')也没有帮助。

>>> df.a.astype(np.dtype(str)).dtype
dtype('O')
>>> df.a.astype(np.dtype('S')).dtype
dtype('O')

这种行为有什么解释吗?


4
简要解释一下,但不是完整的答案:如果您在numpy中使用字符串数据类型,则基本上是一个定长的 c 语言风格字符串。在 pandas 中,它们是“普通”的 Python 字符串,因此是对象类型。 - Joe Kington
2
这可能回答了您的问题 - https://dev59.com/J2Ei5IYBdhLWcg3wueN0 - 基本上它们存储对象ndarray而不是字符串ndarray。然而,我支持当涉及区分类型时他们可以更清晰 - 例如具有能够区分“str”和“mixed”列的能力,这些列也报告为“O”。 - Sereger
2个回答

54

Numpy的字符串数据类型不是Python字符串。

因此,pandas故意使用需要对象数据类型的原生Python字符串。

首先,让我演示一下什么是numpy字符串的不同之处:

In [1]: import numpy as np
In [2]: x = np.array(['Testing', 'a', 'string'], dtype='|S7')
In [3]: y = np.array(['Testing', 'a', 'string'], dtype=object)

现在,'x' 是一个numpy字符串dtype(固定宽度的类C字符串),而y是由原生Python字符串组成的数组。

如果我们尝试超过7个字符,我们将立即看到一个差异。字符串dtype版本将被截断:

In [4]: x[1] = 'a really really really long'
In [5]: x
Out[5]:
array(['Testing', 'a reall', 'string'],
      dtype='|S7')

虽然对象数据类型的版本可以是任意长度:

In [6]: y[1] = 'a really really really long'

In [7]: y
Out[7]: array(['Testing', 'a really really really long', 'string'], dtype=object)

接下来,|S 类型的字符串无法正确地存储Unicode字符,虽然存在一种指定长度的Unicode字符串类型。目前我会跳过一个例子。

最后,NumPy字符串实际上是可变的,而Python字符串不是。例如:

In [8]: z = x.view(np.uint8)
In [9]: z += 1
In [10]: x
Out[10]:
array(['Uftujoh', 'b!sfbmm', 'tusjoh\x01'],
      dtype='|S7')
对于这些原因,pandas选择不允许C语言风格的定长字符串作为数据类型。正如您所注意到的,试图将Python字符串强制转换为固定宽度的NumPy字符串在pandas中无法工作。相反,它始终使用本地Python字符串,对于大多数用户来说,这种方法更加直观。

10
实际上,Pandas确实允许类似于NumPy的定长字节串,尽管它们很少使用,例如 pd.Series(['a', 'b', 'c'], dtype='S1') - mdurant
@mdurant Pandas会接受该语句为有效,但数据类型将从'S1'更改为'O'(对象)。 - James Cropcho
以前可能是可以的,现在可能不行了。 - mdurant

7

如果您来到这里是想了解Pandas中'string'object数据类型之间的区别,请阅读以下内容。截至Pandas 1.5.3,这两种数据类型有两个主要区别。

1. 空值处理

object数据类型不仅可以存储字符串,还可以存储混合数据类型。因此,如果您想将值转换为字符串,则应使用astype(str)方法。但是,这会将所有值都转换为字符串,即使NaN也会变成字面上的'nan'字符串。而string是一种可空数据类型,因此将其转换为'string'会将NaN保留为null值。

x = pd.Series(['a', float('nan'), 1], dtype=object)
x.astype(str).tolist()          # ['a', 'nan', '1']
x.astype('string').tolist()     # ['a', <NA>, '1']

这导致对于 object 数据类型列进行的字符串操作(例如字符计数、比较),返回的结果是 numpy.intnumpy.bool 等,而对于 'string' 数据类型进行的相同操作则返回可为空的 pd.Int64pd.Boolean 数据类型。特别地,对于 object 数据类型进行的 NaN 比较返回 False(因为 NaN 不等于任何值),而对于 'string' 数据类型进行的比较仍然保持为 pd.NA
x = pd.Series(['a', float('nan'), 'b'], dtype=object)
x == 'a'

0     True
1    False
2    False
dtype: bool
    
    
y = pd.Series(['a', float('nan'), 'b'], dtype='string')
y == 'a'

0     True
1     <NA>
2    False
dtype: boolean

因此,使用'string'数据类型时,空值处理更加灵活,因为您可以调用fillna()等方法来处理空值,以任何您想要的方式进行处理。1

2. 'string'数据类型更清晰

如果一个pandas列是'object'数据类型,则其中的值可以被替换为任何内容。例如,其中的字符串可以被整数替换,这是可以接受的(例如下面的x)。但是,如果您期望其中的每个值都是字符串,则可能会在之后产生不必要的后果。'string'数据类型没有这个问题,因为一个字符串只能被另一个字符串替换(例如下面的y)。

x = pd.Series(['a', 'b'], dtype=str)
y = pd.Series(['a', 'b'], dtype='string')
x[1] = 3                        # OK
y[1] = 3                        # ValueError
y[1] = '3'                      # OK

这样的好处在于您可以使用 select_dtypes() 来选择仅字符串列。换句话说,对于 object 数据类型,没有办法识别字符串列,但对于 'string' 数据类型可以。

df = pd.DataFrame({'A': ['a', 'b', 'c'], 'B': [[1], [2,3], [4,5]]}).astype({'A': 'string'})
df.select_dtypes('string')      # only selects the string column


    A
0   a
1   b
2   c



df = pd.DataFrame({'A': ['a', 'b', 'c'], 'B': [[1], [2,3], [4,5]]})
df.select_dtypes('object')      # selects the mixed dtype column as well


    A   B
0   a   [1]
1   b   [2, 3]
2   c   [4, 5]

3. 内存效率

字符串 Dtype 'string' 有存储选项(Python 和 PyArrow),如果字符串较短,则 PyArrow 的效率非常高。请看以下示例:

lst = np.random.default_rng().integers(1000000, size=1000).astype(str).tolist()

x = pd.Series(lst, dtype=object)
y = pd.Series(lst, dtype='string[pyarrow]')
x.memory_usage(deep=True)       # 63041
y.memory_usage(deep=True)       # 10041

正如您所看到的,如果字符串很短(在上面的示例中最多为6个字符),pyarrow消耗的内存要少6倍以上。然而,正如下面的示例所示,如果字符串很长,则几乎没有任何区别。

z = x * 1000
w = (y.astype(str) * 1000).astype('string[pyarrow]')
z.memory_usage(deep=True)       # 5970128
w.memory_usage(deep=True)       # 5917128

1类似的直觉已经存在于str.containsstr.match等中。

x = pd.Series(['a', float('nan'), 'b'], dtype=object)
x.str.match('a', na=np.nan)

0     True
1      NaN
2    False
dtype: object

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接