0

Различие между типами str и object в Pandas

12

Я столкнулся с проблемой различия типов в Numpy и Pandas. В Numpy четко разграничиваются типы str и object. Например, при выполнении следующих команд:

import pandas as pd
import numpy as np
np.dtype(str)  # dtype('S')
np.dtype(object)  # dtype('O')

мы видим, что dtype('S') соответствует типу str, а dtype('O') — типу object.

Однако, в Pandas такого различия не наблюдается: строки автоматически преобразуются в тип 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')

Как видно, после преобразования типа str и object, оба возвращают dtype('O').

Попытка принудительно задать тип dtype('S') также не дает результата:

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

Не могу понять, почему такое поведение имеет место. Есть ли объяснение этому?

2 ответ(ов)

0

Строковые типы данных NumPy не являются обычными строками Python.

Поэтому библиотека pandas намеренно использует нативные строки Python, которые требуют использования объекта dtype.

Прежде всего, позвольте проиллюстрировать, что я имею в виду, когда говорю, что строки 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 (строка фиксированной длины, подобная 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 не могут корректно обрабатывать юникод, хотя существует также строковый тип фиксированной длины для юникода. Я пропущу этот пример на данный момент.

Наконец, строки 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. Вместо этого pandas всегда использует нативные строки Python, которые ведут себя более интуитивно для большинства пользователей.

0

Если вы сюда попали, чтобы узнать о различиях между типами данных 'string' и object в pandas, то на версии pandas 1.5.3 можно выделить два основных отличия.

1. Обработка null-значений

Тип данных object может хранить не только строки, но и смешанные типы данных. Поэтому, если вы хотите привести значения к строковому типу, следует использовать метод astype(str). Однако этот метод преобразует все значения в строки, включая NaN, которые превращаются в строку 'nan'. Тип 'string' являетсяnullable (может содержать null-значения), поэтому при приведении к типу 'string' NaN сохраняются как пустые значения.

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.int или numpy.bool, в то время как те же операции для типа 'string' возвращают nullable типы pd.Int64 или pd.Boolean. В частности, сравнения NaN возвращают False для столбцов типа object, поскольку NaN не равно ни одному значению, тогда как pd.NA остается pd.NA для сравнений с типом 'string'.

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' работа с null-значениями более гибкая, так как вы можете вызывать методы, такие как 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')      # выбирает только строковый столбец

    A
0   a
1   b
2   c

df = pd.DataFrame({'A': ['a', 'b', 'c'], 'B': [[1], [2,3], [4,5]]})
df.select_dtypes('object')      # также выбирает смешанный тип колонок

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

3. Эффективность использования памяти

Тип '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.contains, str.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
Чтобы ответить на вопрос, пожалуйста, войдите или зарегистрируйтесь