Различие между типами str и object в Pandas
Я столкнулся с проблемой различия типов в 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 ответ(ов)
Строковые типы данных 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, которые ведут себя более интуитивно для большинства пользователей.
Если вы сюда попали, чтобы узнать о различиях между типами данных '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
Объединение двух столбцов текста в DataFrame pandas
Фильтрация DataFrame pandas по критериям подстроки
Создание нового столбца на основе значений других столбцов / Применение функции к нескольким столбцам построчно в Pandas
Преобразование DataFrame Pandas в массив NumPy
Pandas read_csv: Опции low_memory и dtype