Семестр 3 · Модуль 2 · Неделя 5
Очистка данных
Пропуски · дубликаты · типы · .str · map/apply · выбросы
2 ч теории. Презентация — 30 мин мини-лекция, далее live-coding
и задачи семинара.
Прежде чем начать: представьте, что заказчик прислал файл, где
у половины сотрудников нет возраста, зарплата записана как «90k», а название отдела
«разными буквами». Ваша задача сегодня — сделать так, чтобы с этим файлом можно
было работать.
Зачем вам очистка на 2–3 курсе
- Очистка данных — 80% работы data scientist. Без неё не работает
ни один курс с реальными данными: ML, статистика, NLP.
- Вы не получите от заказчика чистый файл — он всегда будет «таким»:
пропуски, опечатки, «120 000» вместо числа, кракозябры из-за кодировки.
- Навык «привести данные в порядок» — главный фильтр при приёме на junior-позиции
аналитиком.
- В проекте модуля (неделя 6) очистка — обязательный этап: без неё выводы неверны.
Что произойдёт с результатом анализа, если пропуски и опечатки не обработать?
С чего начинается очистка: аудит качества
Прежде чем чистить — понять, что «сломано». Три вопроса к каждому датасету:
df = pd.read_csv('dirty_salary.csv')
df.dtypes # типы колонок
df.isna().sum() # пропуски
df.duplicated().sum()# дубликаты
employee_id 0
name 0
department 1
salary 2
age 5
hire_date 0
План очистки строится по этой сводке: колонка → проблема →
стратегия → проверка.
Какие три типа проблем вы уже видите в этой сводке?
Пропуски: isna, dropna, fillna
# удалить строки
df.dropna() # любые NaN
df.dropna(subset=['salary'])
df.dropna(thresh=3) # ≥3 непустых
# заполнить
df.fillna(0)
df['age'].fillna(df['age'].median())
df.fillna(method='ffill') # временные ряды
Выбор стратегии по смыслу: числовые — среднее/медиана,
категориальные — мода или «unknown», временные ряды — ffill/bfill.
Почему для зарплаты лучше медиана, а не среднее? А для категории — «unknown»?
Ошибка недели: dropna/fillna без присваивания
Методы Pandas возвращают новый объект и не меняют исходный:
df.dropna() # результат НЕ присвоен → пропал
df.isna().sum() # пропуски никуда не делись!
df = df.dropna() # ← присвоить!
# или inplace=True (не рекомендуется):
df.dropna(inplace=True)
Правило: не уверен, что в таблице — выведи
(`.head()`, `.shape`, `.isna().sum()`), а не гадай.
Почему после `df.fillna(0)` колонка может остаться с NaN? Что забыли?
Дубликаты: duplicated и drop_duplicates
df.duplicated() # True/False по строкам
df.drop_duplicates() # удалить полные копии
df.drop_duplicates(subset=['employee_id'], keep='first')
Вопрос — не «как удалить», а «что считать дубликатом»:
- Настоящий дубликат — повторная покупка / повторная запись (нужен
keep).
- Артефакт импорта — одна и та же строка скопирована дважды (удалять по
ключу, например
employee_id).
Чем отличается drop_duplicates() от drop_duplicates(subset=['employee_id'])?
Типы данных: astype, to_numeric, to_datetime
Главная проблема после чтения CSV: число записано строкой.
df['salary'].dtype # object (строки!)
df['salary'].mean() # NaN — строки не складываются
pd.to_numeric(df['salary'], errors='coerce')
pd.to_datetime(df['hire_date'], format='%Y-%m-%d', errors='coerce')
df['age'].astype('int64') # после очистки пропусков
errors='coerce' превращает нераспарсенные значения в
NaN, а не бросает исключение — потом решаем, что с ними делать.
Почему df['salary'].mean() на строках даёт NaN? Что делает errors='coerce'?
Нормализация «число-строк»: str.replace
Зарплаты приходят в виде «120 000», «90k», «12,5» — сначала привести к
«чистым» строкам, потом в число:
s = df['salary']
s = s.str.replace(' ', '', regex=False) # "120 000" → "120000"
s = s.str.replace('k', '000', regex=False) # "90k" → "90000"
s = s.str.replace(',', '.', regex=False) # "12,5" → "12.5"
pd.to_numeric(s, errors='coerce') # показаны первые 6 из 16
0 120000.0
1 90000.0
2 75500.0
3 120000.0
4 145000.0
5 88000.0
dtype: float64
Почему нельзя сразу сделать pd.to_numeric('120 000')? Что «починит» replace?
String methods: аксессор .str
Строковые методы вызываются через df['col'].str.* — они
векторизованы, т.е. работают сразу над всеми значениями:
df['name'].str.lower() # регистр
df['name'].str.strip() # пробелы по краям
df['name'].str.contains('ова', na=False)
df['name'].str.startswith('И')
df['salary_raw'].str.extract(r'([\d,]+)') # числа из строк
df['name'].str.split(' ')
str.extract принимает регулярное выражение — к
regex вернёмся на неделе 9.
Что вернёт '12,5 кг'.str.extract(r'([\d,]+)') — и как превратить результат в число?
map и apply: когда нужны, а когда нет
# map — словарь/функция на Series
gender = {'male': 'М', 'female': 'Ж'}
df['sex'].map(gender)
# apply — произвольная функция
df['age'].apply(lambda a: 'взрослый' if a > 18 else 'ребёнок')
Главное правило: map/apply — для
поштучных преобразований. Там, где есть векторизация (np.where,
.str-методы, арифметика колонок), она быстрее и чище.
Какой способ быстрее для salary * 1.1: apply(lambda x: x*1.1) или просто df['salary']*1.1?
Выбросы: IQR и clip
Выброс — значение, далеко от основной массы. Стандартный
метод — IQR (из NumPy, неделя 2):
q1, q3 = s.quantile(0.25), s.quantile(0.75)
iqr = q3 - q1
lower, upper = q1 - 1.5*iqr, q3 + 1.5*iqr
s[(s < lower) | (s > upper)] # выбросы
s.clip(lower, upper) # «каппировать»
Решение — не «удалить», а обосновать: удалять,
каппировать или помечать отдельной колонкой.
Почему после очистки «12,5» (опечатка) оказывается выбросом по IQR, хотя это «всего одно число»?
Конвейер очистки: шаги и контрольные точки
Очистка — это процесс с проверками, а не одна магическая функция:
сырой CSV → осмотр (info, isna().sum())
→ решение по каждой колонке (пропуски, дубликаты, типы, текст)
→ проверка: 0 пропусков, верные dtypes, нет дубликатов
→ clean.csv
После каждого шага — «как проверить»: .head(),
.isna().sum(), .dtypes, .shape.
Зачем проверять качество ДО и ПОСЛЕ очистки (одной функцией data_quality)?
Типичные ошибки недели 5
- Мутация без присваивания:
df.dropna() /
df.fillna(0) «не сработали» — результат не присвоен.
apply там, где достаточно map или векторизации
(лишние циклы по элементам).
to_datetime без format на нестандартных строках —
парсится неверно; нужен явный формат и errors='coerce'.
- Не задан
na_values при чтении — строки «N/A», «-» не
распознаются как NaN.
drop_duplicates() без subset — дубликат по
ключу с другими полями не удалится.
Студент сделал df.fillna(0) и удивлён: пропуски остались. Что он забыл?
Вопросы для проверки понимания
- Когда выбрать
dropna, а когда fillna (среднее/медиана/ffill)?
- Чем
map отличается от apply на Series и когда что быстрее?
- Как извлечь год из строки «2024-05-01» через
str.extract?
- Почему после
df.fillna(0) колонка может не измениться?
Ответьте себе письменно за 2 минуты — это и есть мини-самооценка перед семинаром.
Переход: на семинаре вы почистите «грязный» датасет зарплат и
убедитесь, что каждая стратегия имеет обоснование.
Что дальше
- Семинар недели 5: аудит качества, стратегии для пропусков,
чистка текста, функция
data_quality — три уровня задач.
- ДЗ недели 5: очистить «грязный» датасет до состояния
«0 пропусков, 0 дубликатов, корректные типы» +
clean.csv.
- Неделя 6: группировка и сводные таблицы — на уже чистых
данных. Чистота сегодня — фундамент группировок завтра.
Готовы? Откройте семинар-5.md и dirty_salary.csv.