Система типов: dtypes, nullable и category
Тип столбца определяет, что с ним можно делать, сколько он ест памяти и как ведут себя пропуски.
dtype — тип данных столбца. От него зависят доступные операции, расход памяти и поведение при отсутствующих значениях.
Базовые типы pandas
Большинство столбцов получают один из «классических» numpy-типов:
| dtype | Что это |
int64 | целые числа без пропусков |
float64 | дробные; сюда же «съезжают» целые при появлении NaN |
bool | True/False (без пропусков) |
object | обычно строки, но вообще «любой питоновский объект» |
datetime64[ns] | метки времени |
category | категориальные значения с фиксированным набором |
object заслуживает осторожности: это «свалка», куда pandas складывает всё, что не уложилось в числовой тип. Строки хранятся как object, и числовой столбец, в который затесалась строка "—", тоже станет object — и арифметика по нему сломается.
Проблема классических пропусков
У numpy-целых нет способа обозначить «отсутствует»: NaN существует только для float. Поэтому появление пропуска в целочисленном столбце повышает его до float64 — и вы теряете «целочисленность» (id товара становится 1003.0), а у bool пропуск превращает столбец в object. Это историческая болячка pandas.
Nullable-типы: Int64 и boolean
Чтобы хранить целые и булевы с пропусками, pandas ввёл nullable-типы с заглавной буквы: Int64, boolean, Float64, string. Они используют отдельный маркер пропуска pd.NA и сохраняют исходный тип:
import pandas as pd
# обычный int + пропуск → float64 (id испорчен)
s1 = pd.Series([1, 2, None])
print(s1.dtype) # float64
print(s1[0]) # 1.0
# nullable Int64 + пропуск → тип сохранён
s2 = pd.Series([1, 2, None], dtype="Int64")
print(s2.dtype) # Int64
print(s2[0]) # 1
print(s2[2]) # <NA>
print(s2[2] is pd.NA) # True
В pandas 2.x nullable-типы — рекомендуемый путь, когда в целочисленных или булевых данных бывают пропуски. По умолчанию они не включаются: тип нужно указать явно (dtype="Int64") или получить через convert_dtypes().
Отдельно стоит знать про pyarrow-бэкенд и nullable-строки. Исторически строки в pandas хранились как object — это медленно и расходует много памяти (каждая строка — отдельный объект Python). В современных версиях появился специализированный тип string (а под капотом может использоваться Apache Arrow через dtype="string[pyarrow]" или ArrowDtype), который хранит текст компактно и быстрее обрабатывает .str-операции. Для новых проектов с большим объёмом текста это заметное улучшение. Все nullable-типы (Int64, boolean, string, Arrow-типы) объединяет единый маркер пропуска pd.NA вместо разнобоя NaN/None/NaT.
category: когда экономит память и время
Если в строковом столбце мало уникальных значений, но много строк (пол, город, статус заказа, тип устройства), хранить миллионы копий строки «Москва» расточительно. category хранит список уникальных значений один раз (словарь категорий) плюс компактные целочисленные коды-ссылки. Экономия памяти бывает в разы, а группировки и сравнения ускоряются.
Идею легко показать вручную — это «факторизация»: уникальные значения отдельно, данные становятся индексами в этот список:
data = ["Москва", "Сочи", "Москва", "Москва", "Сочи", "Казань", "Москва"]
# словарь категорий: уникальные значения по порядку появления
categories = []
for v in data:
if v not in categories:
categories.append(v)
# коды: каждое значение заменяем индексом в словаре категорий
codes = [categories.index(v) for v in data]
print("категории:", categories)
print("коды: ", codes)
print("строк:", len(data), "уникальных:", len(categories))
Вывод:
категории: ['Москва', 'Сочи', 'Казань'] коды: [0, 1, 0, 0, 1, 2, 0] строк: 7 уникальных: 3
Вместо семи строк храним три уникальных плюс семь маленьких целых кодов. На реальных данных (миллионы строк, десятки категорий) это и есть та самая экономия. В pandas достаточно df["город"] = df["город"].astype("category").
datetime: время как полноценный тип
Даты, прочитанные из файла, по умолчанию приходят строками (object). Преобразование в datetime64[ns] через pd.to_datetime открывает арифметику дат, доступ к компонентам через .dt (год, месяц, день недели) и временные ряды (resample, rolling) из раздела 6. Без этого «01.05.2024» — просто строка, по которой нельзя сортировать как по дате или вычесть из другой даты.
Почему типы вообще так важны? Тип определяет три практичные вещи. Во-первых, какие операции допустимы: вычесть одну дату из другой можно только у datetime, посчитать среднее — только у числового. Во-вторых, корректность сортировки и сравнения: строки "10" и "9" сортируются лексикографически ("10" < "9"!), а числа — по величине. В-третьих, память и скорость: category и компактные числовые типы экономят гигабайты на больших данных. Поэтому проверка df.dtypes сразу после загрузки — не формальность, а способ заранее увидеть, где данные «не те, чем кажутся».
Как менять и проверять типы
df["id"] = df["id"].astype("Int64") # явное приведение
df["город"] = df["город"].astype("category") # к категории
df["дата"] = pd.to_datetime(df["дата"]) # к datetime
df2 = df.convert_dtypes() # авто-подбор nullable-типов
df.dtypes # типы всех столбцов
df.memory_usage(deep=True) # память по столбцам
Подводные камни
- object — красный флаг для чисел и дат. Если ожидаемое число лежит как
object, данные грязные: пробелы, разделители, нечисловые маркеры пропусков. - category не бесплатна при многих уникальных значениях. Если уникальных почти столько же, сколько строк (id, email), category только добавит накладные расходы.
- Смешивание nullable и обычных типов. Операции между
Int64иint64могут давать неожиданный итоговый тип; в одном проекте лучше держаться одной системы. - astype не чистит данные.
astype(int)упадёт на строке"—"; для устойчивого приведения с пропусками нуженpd.to_numeric(..., errors="coerce")(раздел 3).
Лучшие практики
- Задавайте типы как можно раньше — в идеале прямо при чтении файла через
dtype=иparse_dates=. - Для категориальных столбцов с низкой кардинальностью используйте
category— выигрыш в памяти и скорости. - Для целых и булевых данных с пропусками берите nullable
Int64/boolean, чтобы не терять тип. - Регулярно сверяйтесь с
df.dtypes: дешевле поймать «уплывший» тип сразу, чем искать причину NaN позже.
Итог
- dtype управляет операциями, памятью и поведением пропусков.
- Классические int/bool не умеют хранить пропуски — для этого есть nullable Int64/boolean.
- category экономит память при малом числе уникальных значений.
- datetime превращает строки-даты в полноценное время с арифметикой и .dt.