Нашли ошибку?
Скачать чистый
урок 1 из 7≈ 20 минупражнения: 0 из 4

Пропуски

Что такое NaN, как найти и посчитать пропуски, как они ведут себя в суммах, средних и сравнениях, когда строки с пропусками удалять, а когда заполнять — и чем опасно заполнение нулём.

Python запустится при первом выполнении

До сих пор данные были чистыми. В жизни так не бывает: часть значений обычно неизвестна. Служба доставки «Кофейной лавки» прислала выгрузку за первое полугодие — по строке на заказ: номер, срок доставки в днях (days) и оценка покупателя от 1 до 5 (rating).

[ ]Доставка: первое полугодие
import pandas as pd

delivery = pd.read_csv("data/delivery_h1.csv")
delivery.head()
Выводсохранённый · нажмите «Выполнить», чтобы получить свой
order_id days rating
0 10001 4.0 NaN
1 10002 4.0 4.0
2 10003 2.0 5.0
3 10004 3.0 5.0
4 10005 4.0 3.0

В первой же строке вместо оценки стоит NaN (not a number, «не число»). Так pandas обозначает пропуск — значение, которого нет: в файле на этом месте было пусто. Покупатель не поставил оценку.

Заметьте: сроки и оценки показаны как 4.0, хотя в файле целые числа. NaN — особое дробное значение, и столбец с ним становится дробным (float64). Как вернуть целые, разберём в следующем уроке.

Найти и посчитать

Метод isna («is NA» — «отсутствует ли») возвращает маску: True там, где пропуск.

[ ]Маска пропусков
delivery["rating"].isna().head()
Выводсохранённый · нажмите «Выполнить», чтобы получить свой
0     True
1    False
2    False
3    False
4    False
Name: rating, dtype: bool

У таблицы isna возвращает таблицу масок, а sum по ней считает пропуски в каждом столбце — это первое, что стоит сделать с любой новой выгрузкой:

[ ]Сколько пропусков в каждом столбце
delivery.isna().sum()
Выводсохранённый · нажмите «Выполнить», чтобы получить свой
order_id      0
days         36
rating      287
dtype: int64

Оценки нет у 287 заказов, срок не записан у 36. Обратный метод вы уже знаете: notna — «значение есть». А метод count считает именно непустые значения — в отличие от len, который считает строки:

[ ]Строк 816, оценок 529
print(len(delivery))
print(delivery["rating"].count())
delivery.count()
Выводсохранённый · нажмите «Выполнить», чтобы получить свой
816
529
order_id    816
days        780
rating      529
dtype: int64
[ ]Упражнение 1Доля пропусков решено

Сохраните в gap_share долю пропусков в каждом столбце таблицы delivery, а в no_rating — строки, где оценка пропущена.

gap_share = ...
no_rating = ...

NaN не равен ничему

Сам пропуск в коде пишут как np.nan — значение из библиотеки NumPy. У него необычное свойство: он не равен ничему, даже самому себе. И любая арифметика с ним даёт снова NaN:

[ ]Свойства NaN
import numpy as np

print(np.nan == np.nan)
print((delivery["rating"] == np.nan).sum())
print(np.nan + 1)
Выводсохранённый · нажмите «Выполнить», чтобы получить свой
False
0
nan

Пропуски в вычислениях

sum, mean, min, max и другие итоги пропускают NaN и считают по тем значениям, что есть:

[ ]Сумма, среднее и неверное среднее
print(delivery["rating"].sum())
print(delivery["rating"].mean())
print(delivery["rating"].sum() / len(delivery))
Выводсохранённый · нажмите «Выполнить», чтобы получить свой
1953.0
3.6918714555765595
2.3933823529411766

Средняя оценка 3.69 — это 1953 балла, делённые на 529 поставленных оценок. Третья строка делит на все 816 заказов и получает 2.39: такой расчёт считает, будто не оценившие поставили ноль.

Удобно, но есть оборотная сторона: вы можете не заметить, что среднее посчитано по половине таблицы. Поэтому сначала — isna().sum().

Со сравнениями хитрее. Сравнения >, <, == с пропуском дают False (и только != — True: пропуск «не равен» ничему):

[ ]Сколько заказов везли дольше 5 дней
print((delivery["days"] > 5).sum())
print((delivery["days"] > 5).mean())
Выводсохранённый · нажмите «Выполнить», чтобы получить свой
150
0.18382352941176472

150 опозданий — верно. А доля 18.4 % занижена: mean маски делит на все 816 строк, и 36 заказов с неизвестным сроком молча записаны в «доставленные вовремя». Честная доля — среди заказов, срок которых известен.

Вопрос

Что выведет print(pd.Series([1, np.nan, 3]).mean())?

[ ]Упражнение 2Честная доля опозданий решено

Сохраните в known_days сроки доставки без пропусков, а в late_share — долю заказов, которые везли дольше 5 дней, среди заказов с известным сроком.

known_days = ...
late_share = ...

Удалить: dropna

dropna убирает строки с пропусками. Без параметров — все строки, где пропущено хоть что-нибудь. Параметр subset ограничивает проверку указанными столбцами:

[ ]Полные строки и строки с известным сроком
print(len(delivery.dropna()))
print(len(delivery.dropna(subset=["days"])))
Выводсохранённый · нажмите «Выполнить», чтобы получить свой
505
780

Из 816 строк полностью заполнены 505. Если нас интересует только срок, выбрасывать 311 строк незачем — достаточно убрать 36.

Заполнить: fillna

fillna ставит на место пропусков указанное значение:

[ ]Ноль вместо пропуска
delivery["rating"].fillna(0).head()
Выводсохранённый · нажмите «Выполнить», чтобы получить свой
0    0.0
1    4.0
2    5.0
3    5.0
4    3.0
Name: rating, dtype: float64

Осторожно: выбранное значение становится «данными», и итоги начинают его учитывать.

[ ]Средняя оценка до и после заполнения нулём
print(delivery["rating"].mean())
print(delivery["rating"].fillna(0).mean())
Выводсохранённый · нажмите «Выполнить», чтобы получить свой
3.6918714555765595
2.3933823529411766

Чаще пропуски в числах заполняют типичным значением — средним или медианой. Медиана — значение посередине: половина данных меньше неё, половина больше. В describe она подписана 50%, отдельно её считает метод median:

[ ]Заполнить срок медианой
median_days = delivery["days"].median()
print(median_days)
print(delivery["days"].fillna(median_days).isna().sum())
Выводсохранённый · нажмите «Выполнить», чтобы получить свой
4.0
0

Как и остальные методы, dropna и fillna возвращают новый объект, а исходный не меняют.

Что выбрать — удалить или заполнить? Общего ответа нет, зависит от вопроса:

  • считаете итог по одному столбцу — ничего не делайте, mean и sum сами пропустят NaN;
  • для расчёта нужны полные строки — dropna(subset=[...]) по нужным столбцам;
  • значение нужно в каждой строке (для следующего шага, для графика) — fillna, и запомните, чем заполнили.
[ ]Упражнение 3Только оценённые решено

Сохраните в rated заказы, у которых есть оценка (срок при этом может быть пропущен), а в avg_rating — среднюю оценку.

rated = ...
avg_rating = ...

[ ]Упражнение 4Заполнить сроки решено

В таблице filled — копия delivery. Заполните в ней пропуски столбца days медианой этого столбца. В n_gaps_left сохраните, сколько пропусков осталось в filled["days"].

filled = delivery.copy()
# заполните пропуски в столбце days таблицы filled медианой
n_gaps_left = ...

Итоги

  • NaN — пропуск; isna() — маска пропусков, isna().sum() — сколько их в каждом столбце.
  • count() считает непустые значения, len — строки.
  • sum, mean и другие итоги пропускают NaN; сравнения >, <, == с NaN дают False.
  • dropna(subset=[...]) удаляет строки с пропусками, fillna(значение) заполняет.
  • Заполнение меняет итоги: ноль вместо пропуска — частая и тихая ошибка.

Подробнее в справочнике

Нашли ошибку?