Система типов: dtypes, nullable и category

Тип столбца определяет, что с ним можно делать, сколько он ест памяти и как ведут себя пропуски.

dtype — тип данных столбца. От него зависят доступные операции, расход памяти и поведение при отсутствующих значениях.

Базовые типы pandas

Большинство столбцов получают один из «классических» numpy-типов:

dtypeЧто это
int64целые числа без пропусков
float64дробные; сюда же «съезжают» целые при появлении NaN
boolTrue/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.
Проверьте себя
1. Зачем нужен nullable-тип Int64 (с заглавной буквы)?
AОн работает быстрее обычного int64
BОн позволяет хранить целые числа вместе с пропусками без повышения до float
CОн занимает меньше памяти
DОн автоматически удаляет пропуски
2. В каком случае тип category даёт наибольший выигрыш?
AКогда почти все значения уникальны (например, id)
BКогда столбец числовой
CКогда много строк, но мало уникальных значений (пол, город, статус)
DКогда в столбце только пропуски
3. Столбец с числами после чтения CSV имеет dtype object. О чём это говорит?
AЭто нормально для чисел
BВ данных есть нечисловые значения (пробелы, символы, неверный разделитель)
CФайл повреждён
Dpandas всегда читает числа как object
Поддержать проект