Пропуски в таблице: обнаружение, 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.
