Пропуски и дубликаты
Реальные данные всегда содержат дыры и повторы — от того, как вы их обработаете, напрямую зависит корректность анализа.
Пропуск — отсутствующее значение (
NaNдля float,NaTдля дат,pd.NAдля nullable-типов). Дубликат — строка, полностью или частично совпадающая с другой.
Как pandas обозначает пропуски
Маркер пропуска зависит от типа столбца: для обычных числовых — numpy.nan (он же тянет тип в float), для дат — NaT, а для nullable-типов (Int64, boolean, string) — единый pd.NA, который сохраняет исходный тип. Главное свойство любого пропуска: он не равен ничему, даже самому себе. Поэтому NaN == NaN даёт False, и проверять пропуски сравнением нельзя — для этого есть isna().
import pandas as pd, numpy as np
df.isna() # таблица True/False: где пропуски
df.isna().sum() # сколько пропусков в каждом столбце — главный отчёт
df.notna() # обратное: где значения есть
df.isna().sum() — первое, что стоит сделать с новыми данными: он мгновенно показывает, какие столбцы дырявые и насколько.
Важно различать почему данные пропущены — это влияет на выбор стратегии. Пропуск может означать «значение не измерили» (датчик не сработал), «значение неприменимо» (дата увольнения у работающего сотрудника) или «значение равно нулю, но записано как пусто». Это три разные ситуации: в первой уместна оценка/интерполяция, во второй пропуск осмыслен и удалять строку нельзя, в третьей нужно явно заполнить нулём. Механически «убрать все NaN» — частая ошибка новичков, которая искажает данные. Сначала поймите природу дыр, потом выбирайте инструмент.
Три стратегии: удалить, заполнить, интерполировать
Универсального решения нет — выбор зависит от того, почему данные пропущены и что вы будете считать дальше.
dropna: удалить строки или столбцы с пропусками
df.dropna() # убрать строки, где есть хоть один NaN
df.dropna(subset=["цена"]) # убрать строки, где NaN именно в 'цена'
df.dropna(axis=1) # убрать СТОЛБЦЫ с пропусками
df.dropna(thresh=3) # оставить строки, где ≥ 3 непустых значений
Удаление уместно, когда пропусков мало и они «случайны». Опасно, если пропусков много: можно выбросить половину данных. subset ограничивает проверку нужными столбцами — обычно так и надо.
fillna: заполнить значением
df["цена"] = df["цена"].fillna(0) # константой
df["цена"] = df["цена"].fillna(df["цена"].median()) # медианой
df["товар"] = df["товар"].fillna("неизвестно") # для строк
df["x"] = df["x"].ffill() # протянуть предыдущее значение вперёд
df["x"] = df["x"].bfill() # протянуть следующее значение назад
Чем заполнять — содержательный вопрос. Медиана устойчивее среднего к выбросам. Для временных рядов часто логичен ffill (последнее известное значение «держится» до следующего). Заполнять нулём цену — почти всегда ошибка: ноль исказит средние и суммы.
interpolate: восстановить по соседям
Для упорядоченных числовых данных (особенно временных рядов) пропуски можно оценить интерполяцией — по линии между соседними известными точками:
values = [10, None, None, 40, 50]
# линейная интерполяция: заполняем дыры по прямой между известными точками
result = list(values)
i = 0
while i < len(result):
if result[i] is None:
start = i - 1
end = i
while end < len(result) and result[end] is None:
end += 1
left, right = result[start], result[end]
steps = end - start
for k in range(start + 1, end):
result[k] = left + (right - left) * (k - start) / steps
i = end
else:
i += 1
print(result)
Вывод:
[10, 20.0, 30.0, 40, 50]
Ровно это делает df["x"].interpolate(): две дыры между 10 и 40 заполнились значениями 20 и 30 по прямой. Метод подходит, когда между точками есть осмысленная непрерывность (температура, цена по времени), и не подходит для категорий.
Дубликаты: найти и удалить
Повторяющиеся строки появляются из-за двойной загрузки, ошибок join или экспорта. Их ищут через duplicated() (булева маска: True у повторов) и удаляют через drop_duplicates().
df.duplicated() # True для строк-повторов (кроме первой)
df.duplicated().sum() # сколько дубликатов всего
df.drop_duplicates() # оставить уникальные строки
df.drop_duplicates(subset=["email"]) # уникальность по email
df.drop_duplicates(subset=["email"], keep="last") # оставить последний
Параметр subset критичен: «дубликат по email» и «дубликат по всем столбцам» — разные вещи. keep решает, какую из повторяющихся строк сохранить: первую ("first", по умолчанию), последнюю ("last") или ни одной (False — удалить все повторы целиком).
Логику легко повторить на словаре «виденных» ключей:
rows = [
{"email": "[email protected]", "имя": "Аня"},
{"email": "[email protected]", "имя": "Боря"},
{"email": "[email protected]", "имя": "Аня (повтор)"},
]
seen = set()
unique = []
for r in rows:
if r["email"] not in seen: # keep="first": берём первое появление
seen.add(r["email"])
unique.append(r)
for r in unique:
print(r["email"], r["имя"])
Вывод:
[email protected] Аня [email protected] Боря
Множество seen запоминает уже встреченные ключи — так drop_duplicates(subset=["email"], keep="first") и отбрасывает второе появление [email protected].
Перед удалением дубликатов почти всегда нужна нормализация, иначе «почти одинаковые» строки проскользнут. "[email protected]" и "[email protected] " для drop_duplicates — разные значения, хотя для человека это один email. Поэтому типичный пайплайн такой: сначала привести ключ к каноничному виду (df["email"] = df["email"].str.strip().str.lower()), и только потом искать дубликаты. То же касается чисел-строк с разными разделителями и дат в разных форматах. Дедупликация без предварительной нормализации даёт ложное чувство чистоты.
Подводные камни
- Сравнение с NaN всегда False. Проверяйте пропуски через
isna(), а не== np.nan. - fillna(0) для денег и метрик. Ноль — это значение, а не «нет данных»; он испортит средние, суммы и доли. Думайте, что значит пропуск.
- inplace и Copy-on-Write. При CoW
df["x"].fillna(0, inplace=True)не изменит df; переприсваивайте:df["x"] = df["x"].fillna(0). - drop_duplicates без subset требует совпадения по всем столбцам; «почти одинаковые» строки (разный регистр, пробелы) он не поймает — сперва нормализуйте.
Лучшие практики
- Начинайте с диагностики:
df.isna().sum()иdf.duplicated().sum(). - Выбирайте стратегию пропусков осознанно: удалять, заполнять осмысленным значением или интерполировать — в зависимости от природы данных.
- Для дубликатов всегда уточняйте
subsetиkeep— это и есть бизнес-логика «что считать повтором». - Перед поиском дубликатов нормализуйте строки (регистр, пробелы), иначе часть повторов проскользнёт.
Итог
- Пропуски:
NaN/NaT/pd.NA; ищите черезisna(), считайте черезisna().sum(). - Три стратегии:
dropna,fillna/ffill/bfill,interpolate. - Дубликаты:
duplicated()находит,drop_duplicates(subset, keep)удаляет. - Главное — содержательный выбор стратегии, а не механическое заполнение нулём.