До сих пор данные были чистыми. В жизни так не бывает: часть значений обычно неизвестна. Служба доставки
«Кофейной лавки» прислала выгрузку за первое полугодие — по строке на заказ: номер, срок доставки в днях
(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, который считает строки:
print(len(delivery))
print(delivery["rating"].count())
delivery.count()816 529
order_id 816 days 780 rating 529 dtype: int64
Сохраните в gap_share долю пропусков в каждом столбце таблицы delivery, а в no_rating — строки, где
оценка пропущена.
delivery.isna().mean().delivery[delivery["rating"].isna()].Заготовка упражнения обновилась с тех пор, как вы сохранили свой код.
Вернуть заготовку? Ваш код в этом упражнении будет удалён.
gap_share = ...
no_rating = ...Упражнение пока не решено, поэтому для ячеек ниже выполнено эталонное решение — ваш код не изменён.
gap_share = delivery.isna().mean()
no_rating = delivery[delivery["rating"].isna()]NaN не равен ничему
Сам пропуск в коде пишут как np.nan — значение из библиотеки NumPy. У него необычное свойство: он не равен
ничему, даже самому себе. И любая арифметика с ним даёт снова 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: пропуск
«не равен» ничему):
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())?
mean пропускает NaN: остаются 1 и 3, их среднее — 2.0. Результат 1.33 получился бы, если бы пропуск считался нулём: (1 + 0 + 3) / 3.Сохраните в known_days сроки доставки без пропусков, а в late_share — долю заказов, которые везли дольше
5 дней, среди заказов с известным сроком.
delivery.loc[delivery["days"].notna(), "days"].known_days > 5.Заготовка упражнения обновилась с тех пор, как вы сохранили свой код.
Вернуть заготовку? Ваш код в этом упражнении будет удалён.
known_days = ...
late_share = ...Упражнение пока не решено, поэтому для ячеек ниже выполнено эталонное решение — ваш код не изменён.
known_days = delivery.loc[delivery["days"].notna(), "days"]
late_share = (known_days > 5).mean()Удалить: 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, и запомните, чем заполнили.
Сохраните в rated заказы, у которых есть оценка (срок при этом может быть пропущен), а в avg_rating —
среднюю оценку.
delivery.dropna(subset=["rating"]).Заготовка упражнения обновилась с тех пор, как вы сохранили свой код.
Вернуть заготовку? Ваш код в этом упражнении будет удалён.
rated = ...
avg_rating = ...Упражнение пока не решено, поэтому для ячеек ниже выполнено эталонное решение — ваш код не изменён.
rated = delivery.dropna(subset=["rating"])
avg_rating = rated["rating"].mean()В таблице filled — копия delivery. Заполните в ней пропуски столбца days медианой этого столбца.
В n_gaps_left сохраните, сколько пропусков осталось в filled["days"].
fillna возвращает новый Series — запишите его обратно: filled["days"] = filled["days"].fillna(...).filled["days"].median(); оставшиеся пропуски — filled["days"].isna().sum().Заготовка упражнения обновилась с тех пор, как вы сохранили свой код.
Вернуть заготовку? Ваш код в этом упражнении будет удалён.
filled = delivery.copy()
# заполните пропуски в столбце days таблицы filled медианой
n_gaps_left = ...Упражнение пока не решено, поэтому для ячеек ниже выполнено эталонное решение — ваш код не изменён.
filled = delivery.copy()
filled["days"] = filled["days"].fillna(filled["days"].median())
n_gaps_left = filled["days"].isna().sum()Итоги
NaN— пропуск;isna()— маска пропусков,isna().sum()— сколько их в каждом столбце.count()считает непустые значения,len— строки.sum,meanи другие итоги пропускаютNaN; сравнения>,<,==сNaNдаютFalse.dropna(subset=[...])удаляет строки с пропусками,fillna(значение)заполняет.- Заполнение меняет итоги: ноль вместо пропуска — частая и тихая ошибка.