Урок по треку Python для анализа данных 7 мин

Пропуски, дубли и типы: чистка данных в pandas

NaN и None, isna, fillna, dropna, drop_duplicates, astype, nullable-типы, строки через .str.

Реальные данные грязные: пропуски, дубли, числа строками, «Москва» в пяти написаниях. На собесе аналитика об этом спрашивают почти всегда, а в тестовом задании чистка занимает половину времени.

Пропуски: NaN, None, pd.NA

  • np.nan это float, пропуск в числовых колонках.
  • None в числовой колонке pandas превращает в NaN.
  • pd.NA пропуск nullable-типов (Int64, boolean, string).

Проверять пропуски только через isna() / notna(): NaN не равен даже самому себе, поэтому x == np.nan всегда ложно.

df.isna().sum()      # число пропусков по колонкам
df.isna().mean()     # доля пропусков
df["age"].count()    # число непустых значений

Агрегаты пропускают NaN: mean считает по непустым, sum пустого набора равна 0. Последнее коварно: «выручка 0» вместо «нет данных». Параметр min_count=1 заставит sum вернуть NaN, если значений нет.

Удалить или заполнить

df.dropna()                     # строки, где пусто хоть что-то
df.dropna(subset=["email"])     # только по нужным колонкам
df.dropna(how="all")            # строки, где пусто всё
df["city"].fillna("неизвестно")
df["price"].fillna(df["price"].median())
df["price"] = df.groupby("category")["price"].transform(lambda s: s.fillna(s.median()))
df["value"].ffill()             # протянуть последнее известное (временные ряды)

Выбор зависит от смысла пропуска. Если пропуски случайны и их мало, можно удалить. Если они зависят от других признаков (молодые реже указывают доход), заполняют по группам. Если от самого значения (богатые скрывают доход), любое заполнение смещает выводы, честнее добавить флаг «был пропуск». Для моделей статистики заполнения считают только на train, иначе утечка.

Типы

После чтения CSV числа часто оказываются строками. Смотрим df.dtypes и приводим:

df["price"] = pd.to_numeric(df["price"], errors="coerce")   # мусор -> NaN
df["date"] = pd.to_datetime(df["date"], format="%Y-%m-%d", errors="coerce")
df["city"] = df["city"].astype("category")

После errors="coerce" проверьте, сколько значений стало NaN: это строки, которые не распарсились, их стоит посмотреть глазами.

astype(int) на колонке с пропусками падает: в обычном int64 нет места для NaN. Варианты: сначала заполнить пропуски или перейти на nullable astype("Int64").

Строки

Аксессор .str применяет строковые операции векторно:

df["city"] = df["city"].str.strip().str.lower()
df["phone"] = df["phone"].str.replace(r"\D", "", regex=True)
df["price"] = (df["price"].str.replace(r"[\s ₽]", "", regex=True)
                          .str.replace(",", ".", regex=False))

Помните про неразрывный пробел  , который приходит из Excel: обычный пробел в регулярке его не уберёт, а \s уберёт.

Дубли

df.duplicated().sum()                          # полные дубли
df.duplicated(subset=["user_id"]).sum()        # дубли по ключу
df = df.drop_duplicates(subset=["user_id"], keep="first")

По умолчанию keep="first": первое вхождение остаётся, повторы удаляются. Чтобы оставить самую свежую версию записи, сначала сортируют по времени:

latest = (df.sort_values("updated_at", kind="stable")
            .drop_duplicates("user_id", keep="last"))

groupby().first() для этой задачи не подходит: он берёт первое непустое значение по каждой колонке отдельно и может склеить поля из разных версий.

Ищите дубли после нормализации строк, иначе " Москва" и "москва" останутся разными.

Проверки после чистки

  • Число строк до и после каждого шага.
  • Ключ уникален: df["id"].is_unique.
  • Значения в разумных диапазонах: возраст от 0 до 120, суммы неотрицательные.
  • Итоговые суммы сходятся с источником.

Частые ошибки

  • dropna() без subset на широкой таблице выкидывает почти все строки.
  • Заполнение нулём там, где ноль это реальное значение.
  • Заполнение средним до разбиения на train и test.
  • astype(float) на грязной колонке вместо to_numeric(errors="coerce").
  • Удаление дублей до нормализации строк.

Что спросят на собесе

  • Чем NaN отличается от None и pd.NA, как найти пропуски?
  • Как выбрать стратегию работы с пропусками, когда заполнение средним вредно?
  • Как оставить последнюю версию записи по ключу?
  • Почему astype(int) падает и что такое nullable-типы?
  • Какие проверки вы делаете после чистки данных?

Закрепите за 5 минут

После урока в тренажёре 3-5 проверочных вопросов, дальше модуль уходит в повторения.