pandas: типы столбцов и очистка по правилу

Пропуски в таблице: обнаружение, dropna, fillna и nullable dtype

Содержание курса

Nullable dtype: Int64, Float64, boolean, StringDtype и поведение при fillna

Когда пропуски заполнены, возникает вопрос, который легко проигнорировать: а какой тип данных теперь у столбца? Ответ зависит от того, каким был dtype до заполнения — и здесь кроется принципиальная разница между NumPy-backed типами и nullable-типами pandas.

Проблема с обычным int64

В NumPy целочисленный массив (int64) не умеет хранить пропуск — у целых чисел просто нет специального sentinel-значения вроде float('nan'). Поэтому когда в столбце с целыми числами появляется пропуск, pandas вынужден переводить его в float64, где пропуск представлен NaN. Это происходит во многих, но не во всех сценариях — конкретное поведение зависит от операции и версии pandas.

Результат: данные, которые были целыми числами, начинают выглядеть как 25.0, 32.0, и агрегаты могут вести себя иначе, чем ожидалось.

Nullable-типы как решение

Pandas предоставляет альтернативу — nullable-типы, которые поддерживают pd.NA рядом с обычными значениями без потери типа:

  • Int64 (заглавная I) — целые числа с пропусками
  • Float64 (заглавная F) — вещественные числа с пропусками
  • boolean — булевы значения с пропусками
  • pd.StringDtype() — строки с пропусками

Задаются через astype:

import pandas as pd

df = pd.DataFrame({
    'age':    pd.array([25, None, 32], dtype='Int64'),
    'salary': [50000.0, float('nan'), 75000.0],
    'dept':   ['IT', None, 'HR']
})

print(df['age'].dtype)    # Int64
print(df['salary'].dtype) # float64

# Переводим salary в nullable Float64
df['salary'] = df['salary'].astype('Float64')
print(df['salary'].dtype) # Float64

# Переводим dept в StringDtype
df['dept'] = df['dept'].astype(pd.StringDtype())
print(df['dept'].dtype)   # string

Столбец age уже создан с dtype='Int64' явно — None в нём хранится как pd.NA, а не превращается в NaN с потерей целочисленного типа.

Что происходит с dtype после fillna

Если заполнять nullable-столбец значением совместимого типа, dtype сохраняется:

filled_age = df['age'].fillna(0)
print(filled_age.dtype)    # Int64 — тип не изменился

filled_salary = df['salary'].fillna(0.0)
print(filled_salary.dtype) # Float64 — тип не изменился

Для сравнения — обычный float64 после fillna остаётся float64, потому что NaN в нём изначально тот же float. Но если попытаться создать столбец как обычный int64 и добавить в него пропуск через операцию, поведение может оказаться неожиданным: pandas нередко повышает тип до float64.

Как проверить dtype после операций

Привычка проверять dtype после заполнения — полезная. Особенно если столбец участвует в дальнейших вычислениях:

print(df.dtypes)
age       Int64
salary    Float64
dept      string
dtype: object

Если dtype изменился неожиданно (например, Int64 стал object), значит где-то в пайплайне значение оказалось несовместимого типа.

Сквозной пример: от сырых данных до заполненного nullable-DataFrame

import pandas as pd

df = pd.DataFrame({
    'a': pd.array([1, None, 3], dtype='Int64'),
    'b': [10.0, float('nan'), 30.0],
    'c': ['x', None, 'z']
})

# Переводим все столбцы в nullable-типы
df['b'] = df['b'].astype('Float64')
df['c'] = df['c'].astype(pd.StringDtype())

# Проверяем пропуски
print(df.isna().sum())

# Заполняем
df_filled = df.fillna({'a': 0, 'b': 0.0, 'c': 'Unknown'})
print(df_filled)
print(df_filled.dtypes)
a    1
b    1
c    1
dtype: int64

   a     b        c
0  1  10.0        x
1  0   0.0  Unknown
2  3  30.0        z

a      Int64
b    Float64
c     string
dtype: object

Все три столбца сохранили свои nullable-типы после fillna — именно это и отличает Int64/Float64/string от NumPy-backed аналогов. Ключевое правило: nullable dtype сохраняется после fillna, если значение-заменитель совместимо с типом столбца; если передать несовместимый тип, pandas может повысить dtype до object.