Урок курса

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

NumPy и pandas: практический тренажёр

В числовых массивах NumPy пропуск в рассмотренном float-сценарии представлен как np.nan, и там его проверяют np.isnan(). Pandas работает с таблицами, где в одном DataFrame могут соседствовать числа, строки и булевы значения — и каждый тип хранит «отсутствие данных» по-своему: np.nan, None или pd.NA. Поэтому pandas предлагает единый интерфейс — isna/notna — который скрывает эти различия и даёт одинаковую маску вне зависимости от dtype столбца.

Обнаружение пропусков: isna, notna и подсчёт через sum

Первый шаг при работе с любыми реальными данными — понять, где именно чего-то не хватает. df.isna() возвращает DataFrame той же формы, но заполненный булевыми значениями: True там, где значение пропущено, False — где данные есть. df.notna() — прямая инверсия: True означает наличие данных.

Оба метода работают одинаково для DataFrame и для Series — форма результата всегда совпадает с формой исходного объекта.

Возьмём таблицу с явными пропусками в разных столбцах:

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.isna())

Результат:

     age  salary   dept
0  False   False  False
1   True    True   True
2  False   False  False

Здесь вся вторая строка пропущена. Но в реальных данных пропуски рассеяны, и смотреть на полную матрицу неудобно. Чтобы получить сводку по столбцам, достаточно применить .sum() — булевые значения трактуются как 0/1, и сумма даёт количество пропусков в каждом столбце:

print(df.isna().sum())
age       1
salary    1
dept      1
dtype: int64

Если хочется знать долю пропусков, а не абсолютное число, достаточно разделить на длину таблицы:

print(df.isna().sum() / len(df))

Это стандартный способ быстро оценить «засорённость» данных до того, как принимать решение — удалять строки или заполнять.

Ещё один полезный приём — df.isna().any(): возвращает Series с True по тем столбцам, где есть хоть один пропуск. Удобно для быстрой проверки, а не полного подсчёта:

print(df.isna().any())
age       True
salary    True
dept      True
dtype: bool

На уровне отдельного столбца series.isna() возвращает обычную булеву Series, которую можно сразу использовать для фильтрации. Например, чтобы посмотреть только строки, где salary пропущен:

print(df[df['salary'].isna()])

В результате получаем только строки с пропуском в salary — это удобно для ручной проверки или для отладки логики заполнения.

Диагностика через isna().sum() всегда должна быть первым шагом: она показывает масштаб проблемы и помогает решить, какой инструмент применять дальше — удалять записи или заполнять пропуски.

Удаление строк и столбцов с пропусками через dropna

Когда диагностика через isna().sum() показывает, что пропусков немного и строки с ними можно просто выбросить без потери смысла, на сцену выходит dropna.

По умолчанию df.dropna() удаляет любую строку, в которой есть хотя бы один пропуск. Для нашей таблицы это означает, что строка с индексом 1 — где пропущены age, salary и dept — исчезнет из результата:

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']
})

clean = df.dropna()
print(clean)
   age   salary dept
0   25  50000.0   IT
2   32  75000.0   HR

dropna возвращает новый DataFrame — исходный df при этом не меняется, и строка с индексом 1 в нём по-прежнему присутствует. Необратимой потерей данных это становится только если вы перезаписываете исходную переменную (df = df.dropna()) или теряете ссылку на оригинальный объект. Пока df жив, данные можно достать.

Параметр how

Если нужно удалять строку только тогда, когда в ней пустые абсолютно все поля, используется how='all':

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

print(df2.dropna(how='all'))
   age   salary     dept
0   25  50000.0       IT
1  <NA>      NaN  Finance

Строка с индексом 1 содержит частичные данные (dept есть), поэтому how='all' её сохраняет. Строка 2 полностью пустая — она исчезает.

Параметр subset

Часто имеет смысл смотреть на пропуски только в ключевых столбцах. Если пропуск в dept допустим, а пропуск в salary — нет, указываем subset:

print(df.dropna(subset=['salary']))
   age   salary dept
0   25  50000.0   IT
2   32  75000.0   HR

Здесь строки отфильтрованы только по столбцу salary. Если бы пропуск был только в dept, строка бы осталась.

Параметр axis=1

Если пропуски сосредоточены в целом столбце — например, признак вообще не заполнялся — удобнее выбросить столбец, а не строки:

df3 = pd.DataFrame({
    'age':    [25, 30, 32],
    'salary': [50000.0, float('nan'), 75000.0],
    'score':  [float('nan'), float('nan'), float('nan')]
})

print(df3.dropna(axis=1, how='all'))
   age   salary
0   25  50000.0
1   30      NaN
2   32  75000.0

Столбец score полностью пустой — он удаляется. salary с одним пропуском остаётся, потому что how='all' требует, чтобы все значения в столбце были пропущены.

dropna — это инструмент для ситуаций, когда строка или столбец с пропуском не несёт полезной информации. Если данные можно восстановить или заменить осмысленным значением, следующий шаг — fillna и распространение значений.

Заполнение пропусков: константа через fillna и распространение через ffill/bfill

Там, где удалять строки нельзя — данных и без того мало, или строка ценна частично, — пропуск нужно заполнить. Два главных инструмента для этого: fillna для подстановки фиксированного значения и ffill/bfill для распространения соседних значений.

fillna(value) — константное заполнение

series.fillna(value) заменяет каждый пропуск в Series скалярным значением value. df.fillna(value) делает то же самое для всей таблицы сразу. Исходный объект при этом не меняется — метод возвращает новый объект:

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']
})

# Заполняем только salary
filled_salary = df['salary'].fillna(0.0)
print(filled_salary)
0    50000.0
1        0.0
2    75000.0
Name: salary, dtype: float64

Для разных столбцов можно передать словарь — тогда каждый столбец получит своё значение:

df_filled = df.fillna({'salary': 0.0, 'dept': 'Unknown'})
print(df_filled)
    age   salary     dept
0    25  50000.0       IT
1  <NA>      0.0  Unknown
2    32  75000.0       HR

Заметьте: age всё ещё <NA>, потому что в словаре мы его не указали.

Подбирать значение для заполнения нужно осмысленно: 0 в salary говорит «зарплата равна нулю», а это может исказить агрегаты. Иногда правильнее заполнить медианой или средним — но это уже задача анализа, а не синтаксиса fillna.

ffill и bfill — распространение соседних значений

Когда данные упорядочены по времени или по какой-то логике и пропуск — это просто «пропуск в записи, а не в реальности», удобнее взять ближайшее известное значение.

ffill() (forward fill) распространяет последнее известное значение вниз — каждый пропуск заменяется тем, что стоит выше него:

s = pd.Series([10.0, float('nan'), float('nan'), 40.0, float('nan')])
print(s.ffill())
0    10.0
1    10.0
2    10.0
3    40.0
4    40.0
dtype: float64

bfill() работает в обратном направлении — заполняет пропуск ближайшим значением снизу:

print(s.bfill())
0    10.0
1    40.0
2    40.0
3    40.0
4     NaN
dtype: float64

Оба метода имеют граничный случай, который легко упустить: если пропуск стоит в самом начале Series, ffill не может его заполнить — выше нет данных. Аналогично bfill не заполнит пропуск в самом конце — ниже ничего нет. В обоих случаях пропуск просто остаётся NaN.

В примере выше последний элемент после bfill так и остался NaN — именно по этой причине.

Оба метода применимы и к DataFrame — тогда заполнение происходит вдоль каждого столбца независимо:

print(df['salary'].ffill())
0    50000.0
1    50000.0
2    75000.0
Name: salary, dtype: float64

Здесь пропуск в строке 1 заполнился значением из строки 0 — 50000.0.

Выбор между fillna, ffill и bfill диктует природа данных: для временного ряда ffill часто уместен, для категориальной переменной — скорее константа или мода. Что именно происходит с 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.

Закрепите тему на задачах

Посчитайте пропуски до и после обработки. Не заменяйте отсутствие измерения нулём без объяснения смысла, а для ffill проверьте порядок строк и границы групп.

pandas dropna(subset): какие строки удалятся

Задача на pandas fillna и nullable dtype: обработка пропусков

pandas dropna: how, subset и форма таблицы

Откройте условие и решите задачу перед чтением следующей темы. В задачах на код проверяется программа, в тестовых заданиях — выбранный ответ. Для последовательного обучения вернитесь в программу бесплатного курса.

Задача 1

DataFrame df имеет столбцы 'a', 'b', 'c'. Строка с индексом 2 содержит пропуски в столбцах 'a' и 'c'; строка с индексом 5 содержит пропуск только в 'b'. Вызывается df.dropna(subset=['a', 'c']). Какие строки будут удалены?

Выберите один вариант
Без регистрации · результат не сохраняется

Задача 2

Series создан как:
import pandas as pd
s = pd.Series(pd.array([None, 10, None, 30, None], dtype='Int64'))
Что вернёт s.ffill().tolist()?

Выберите один вариант
Без регистрации · результат не сохраняется

Задача 3

Столбец df['count'] имеет dtype float64 и содержит пропуски (NaN). Разработчик хочет хранить целые числа вместе с пропусками. Какой nullable dtype нужно указать в astype(), чтобы столбец мог хранить целые числа вместе с pd.NA?

Выберите один вариант
Без регистрации · результат не сохраняется

Задача 4

Пропуски в журнале датчиков: isna, dropna, fillna и ffill за один проход

Что уже дано

Проверяющая система запускает вашу программу и подаёт данные через stdin. Никаких заранее созданных переменных нет — вы читаете всё сами.

Что нужно сделать

Напишите программу, которая обрабатывает журнал показаний датчика:

  1. Читает из stdin целое число n — количество строк журнала.
  2. Читает следующие n строк; каждая содержит два токена через пробел: hour (целое число от 0 до 23) и value (вещественное число или строка nan). Создаёт pd.DataFrame с именем df со столбцами 'hour' (dtype int64) и 'value' (dtype float64).
  3. Шаг A. Выводит одну строку: missing=<k>, где k — число пропущенных значений в столбце 'value' (через df['value'].isna().sum()).
  4. Шаг B. Удаляет строки, где 'value' пропущен, через df.dropna(subset=['value']). Сохраняет результат в df_clean. Выводит одну строку: clean_shape=<shape>, например clean_shape=(4, 2).
  5. Шаг C. Заполняет пропуски в df['value'] константой 0.0 через fillna. Сохраняет результат в s_filled. Выводит значения s_filled через пробел, округлив до одного знака.
  6. Шаг D. Заполняет пропуски в df['value'] методом ffill(). Сохраняет результат в s_ffill. Выводит значения s_ffill через пробел, округлив до одного знака. Если первый элемент оказался пропуском и ffill не смог его заполнить — выводит nan для этой позиции.

Во всех шагах исходный df не изменяется.

Ввод и вывод

Программа читает stdin и пишет в stdout.

Формат ввода

n
hour0 value0
hour1 value1
...

Токен nan в поле value означает пропуск.

Дополнительные ограничения: n >= 1; таблица содержит явно описанные в условии столбцы.

Формат вывода

Ровно четыре строки в порядке A → B → C → D.

РешениеPython

Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.

Отправьте решение, чтобы увидеть результаты тестов.

Задача 5

Nullable dtype и fillna: Int64, Float64, boolean после заполнения пропусков

Что уже дано

Проверяющая система запускает вашу программу и подаёт данные через stdin. Никаких заранее созданных переменных нет — вы читаете всё сами.

Что нужно сделать

Напишите программу, которая:

  1. Читает из stdin целое число n — количество строк таблицы.
  2. Читает следующие n строк; каждая содержит три токена через пробел:
  • qty — целое число или строка NA;
  • price — вещественное число или строка NA;
  • active — строка True, False или NA.
  1. Строит pd.DataFrame с именем df со столбцами:
  • 'qty' с dtype Int64 (nullable integer);
  • 'price' с dtype Float64 (nullable float);
  • 'active' с dtype boolean (nullable boolean).
  1. Шаг A. Выводит одну строку: isna_sum=<qty_na> <price_na> <active_na> — число пропусков по каждому столбцу через пробел.
  2. Шаг B. Заполняет пропуски в df['qty'] значением 0 через fillna(0), сохраняет в qty_filled. Выводит одну строку: qty_dtype=<dtype>, где <dtype> — dtype результата.
  3. Шаг C. Заполняет пропуски в df['price'] через ffill(), сохраняет в price_ffilled. Выводит значения через пробел, округлив до одного знака. Если первый элемент — пропуск и ffill не смог его заполнить, выводит NA для этой позиции.
  4. Шаг D. Заполняет пропуски в df['active'] значением False через fillna(False), сохраняет в active_filled. Выводит одну строку: active_dtype=<dtype>, где <dtype> — dtype результата.

Во всех шагах исходный df не изменяется.

Ввод и вывод

Программа читает stdin и пишет в stdout.

Формат ввода

n
qty0 price0 active0
qty1 price1 active1
...

Токен NA означает пропуск в соответствующем столбце.

Формат вывода

Ровно четыре строки в порядке A → B → C → D.

РешениеPython

Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.

Отправьте решение, чтобы увидеть результаты тестов.

Задача 6

dropna с how и subset: диагностика формы после каждого удаления

Что уже дано

Проверяющая система запускает вашу программу и подаёт данные через stdin. Никаких заранее созданных переменных нет — вы читаете всё сами.

Что нужно сделать

Напишите программу, которая:

  1. Читает из stdin целое число n — количество строк таблицы.
  2. Читает следующие n строк; каждая содержит четыре токена через пробел:
  • id — целое число;
  • region — строка или NA (пропуск);
  • amount — вещественное число или NA (пропуск);
  • tag — строка или NA (пропуск).
  1. Строит pd.DataFrame с именем df со столбцами 'id' (int64), 'region' (object), 'amount' (float64), 'tag' (object). Токен NA заменяется на None для строковых столбцов и на float('nan') для числового.
  2. Шаг A. Вычисляет df.isna().sum() и выводит строку:
    isna_sum=<id> <region> <amount> <tag> — четыре числа через пробел.
  3. Шаг B. Применяет df.dropna(how='any'). Выводит drop_any=<shape>, например drop_any=(3, 4).
  4. Шаг C. Применяет df.dropna(subset=['region', 'amount'], how='all'). Выводит drop_subset_all=<shape>.
  5. Шаг D. Применяет df.dropna(axis=1, how='all'). Выводит drop_cols= и следом через пробел имена оставшихся столбцов. Если удалённых столбцов нет — выводит только drop_cols= и пустую строку (то есть drop_cols=\n).

Во всех шагах исходный df не изменяется.

Ввод и вывод

Программа читает stdin и пишет в stdout.

Формат ввода

n
id0 region0 amount0 tag0
id1 region1 amount1 tag1
...

Токен NA означает пропуск в соответствующем столбце.

Дополнительные ограничения: n >= 1; таблица содержит явно описанные в условии столбцы.

Формат вывода

Ровно четыре строки в порядке A → B → C → D.

РешениеPython

Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.

Отправьте решение, чтобы увидеть результаты тестов.

Задача 7

Найди и исправь: три ошибки в bfill, dropna(subset) и dtype после fillna

Что уже дано

Проверяющая система запускает вашу программу и подаёт данные через stdin. Никаких заранее созданных переменных нет.

В редакторе уже есть сломанный код. В нём ровно три ошибки в трёх независимых строках:

  1. Для заполнения пропусков в столбце 'temp' вперёд (каждый пропуск заменяется последним известным значением выше) используется bfill() — хотя нужен ffill().
  2. dropna вызывается с subset=['station'], хотя по условию нужно удалять строки, где пропущен именно столбец 'temp' — то есть subset=['temp'].
  3. Для заполнения пропусков в столбце 'qty' (dtype Int64) используется fillna(-1), но результат приводится через .astype('float64') — хотя nullable-тип Int64 сохраняется после fillna целым числом и приводить к float не нужно; выводить нужно dtype результата fillna без astype.

Каждая ошибка находится в отдельной строке кода. Исправьте все три.

Что нужно сделать

Исправьте три ошибки так, чтобы программа:

  1. Читала из stdin целое число n — количество строк таблицы.
  2. Читала следующие n строк; каждая содержит три токена через пробел: station (строка без пробелов), temp (вещественное число или nan), qty (целое число или NA). Создавала pd.DataFrame со столбцами 'station' (object), 'temp' (float64), 'qty' (Int64).
  3. Шаг A. Заполняла пропуски в df['temp'] методом forward fill и сохраняла в s_ff. Выводила значения s_ff через пробел, округлив до одного знака. Если первый элемент — пропуск и ffill не смог его заполнить — выводила nan.
  4. Шаг B. Применяла df.dropna(subset=['temp']) и выводила shape результата в виде drop_shape=<shape>.
  5. Шаг C. Заполняла пропуски в df['qty'] значением -1 через fillna(-1) и выводила qty_dtype=<dtype> — dtype результата (без astype).

Ввод и вывод

Программа читает stdin и пишет в stdout.

Формат ввода

n
station0 temp0 qty0
station1 temp1 qty1
...

Токен nan в поле temp означает пропуск (float NaN). Токен NA в поле qty означает пропуск (pd.NA).

Дополнительные ограничения: n >= 1; таблица содержит явно описанные в условии столбцы.

Формат вывода

Ровно три строки в порядке A → B → C.

РешениеPython

Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.

Отправьте решение, чтобы увидеть результаты тестов.

Задачи к уроку

Все задачи курса →

Продолжить с сохранением прогресса

Перейдите к уроку в учебном кабинете.

Перейти к интерактивному уроку