аудит качества · пропуски · дубликаты · типы · текст и apply · выбросы
2 ч теории. Презентация — 30 мин мини-лекция, далее live-coding и задачи семинара.
df['Age'][df['Age'] > 30] = 99
не записывает значения? Как записать правильно? Сегодня продолжим тему «изменяю данные» —
но уже для целых колонок, и здесь тоже действует правило «присвой результат».dtypes/isna().sum()/duplicated().sum());dropna/fillna) с обоснованной стратегией;astype, to_numeric, to_datetime);.str, map/apply) и выбирать векторизацию;Аудит — это «карта ремонта»: колонка → проблема → стратегия. Три вызова заменяют просмотр 16 строк глазами; для 100 000 строк они незаменимы.
isna()/notna() дают маску True/FalseNaN (Not a Number) — отметка «данных нет». isna() — маска
True там, где пропуск; notna() — наоборот. .sum() по маске считает
число пропусков; .mean() — долю.
.sum() по маске isna() считает пропуски?dropna: удалять — только когда нужно (how/thresh/subset)dropna() без параметров удаляет все строки с хоть одним пропуском
(how='any') — у нас выбросило бы половину данных. subset=['col'] удаляет
только когда пропуск в важной колонке; thresh=N — оставляет строки с ≥ N
непустыми значениями. Выбор = вопрос «какие пропуски я могу себе позволить потерять?»
dropna() без параметров — плохая идея? Почему?fillna: чем заполнять — по смыслу колонкиЗаполняем по типу колонки: числовые — медиана (реже среднее),
категориальные — отдельная метка 'unknown' (а не мода!), временные ряды —
method='ffill'/'bfill' (следующее/предыдущее значение).
'unknown', а не мода?| Тип колонки | Стратегия | Почему |
|---|---|---|
| Числовая | медиана (устойчива), реже среднее | среднее «тянется» к выбросам |
| Категориальная | отдельная метка 'unknown' | мода искажает частоты; «нет данных» — честный ответ |
| Временной ряд | ffill/bfill (с limit) | соседние по времени значения ближе к пропущенному |
| «Ключевая» | dropna(subset=[...]) | если строки без неё бессмысленны (нет salary — нет анализа) |
| Большая доля | dropna/dropna(thresh=...) | заполнять 30% данных — значит выдумывать 30% данных |
Стратегия — это не «какая функция», а ответ на вопрос «что этот пропуск означает для анализа». Функцию выбираем последней.
dropna, fillna, drop_duplicates,
astype, set_index, reset_index) не меняют
таблицу, а возвращают новую копию.df = df.dropna() или df['col'] = ...),
он пропадёт, а исходная таблица останется как была.inplace=True существует, но не рекомендуется —
присваивание читается лучше и безопаснее..head()/.shape/.isna().sum()».df.dropna() сам по себе «ничего не делает»? Что обязательно нужно написать?df.dropna() «не сработала»: результат не присвоенdropna() вернула новую таблицу без пропусков — но мы её выбросили,
не присвоив. Исходный dirty не изменился. Это и есть правило слайда 10
в действии. В pandas 2.x та же история — не предупреждение, а молча «не сработало».
df.fillna(0) и пропуски остались. Что он забыл?duplicated()/drop_duplicates(keep, subset)duplicated() — маска True/False (как isna()).
drop_duplicates(subset=[ключ]) удаляет повторы по ключу; keep='first'/'last'
— какую строку оставить, keep=False — удалить все копии. Вопрос не «как удалить»,
а «что считать дубликатом».
drop_duplicates() отличается от drop_duplicates(subset=['employee_id'])?salary/age/hire_date)
→ удалять по ключу.keep, чтобы выбрать правильную
строку, или вообще не удалять.Повторилась ровно та же строка — это артефакт импорта, удаляем. Ключ тот же, а данные разные — это отдельные события, их нельзя удалять без осмысления.
salary — str, .mean() бросает TypeErrorВ pandas 2.x на object-колонке это же выражение возвращало NaN — тихо; в 3.0 ошибка явная.
Pandas не умеет складывать «120 000» и «90k»: это строки.
.mean() на строковой колонке падает с TypeError. Причина — в колонке есть
не-числа: пробелы, буква «k», запятая. Проверка — dtypes после чтения.
salary? Что внутри колонки мешает?astype: когда можно, а когда нетastype меняет тип, не меняя значения. Для age
(числа с плавающей точкой после fillna) — работает. Для salary («120 000») —
падает: это не число, а текст. Строки-числа переводятся через to_numeric
ПОСЛЕ нормализации формата.
astype('int64') сработал для age, но упал для salary?to_numeric(errors='coerce'): нераспарсенное → NaN, конвейер живётpd.to_numeric переводит строки в числа.
errors='coerce' — «мягкий» режим: что не распарсилось, становится NaN, и конвейер
не падает. Потом решаем: 2 строки без зарплаты — удалить (dropna(subset=['salary'])).
errors='coerce'? Зачем он, а не исключение?str.replace → to_numericСначала привести к единому виду строк (убрать пробелы, суффикс «k»,
заменить «,» на «.»), потом ОДИН раз перевести в число. to_numeric сразу на
«120 000» — ValueError: пробел не входит в число.
pd.to_numeric("120 000") сразу падает?to_datetime(format=, errors='coerce'): строки дат → datetime64to_datetime без format на нестандартных строках может
«угадать» неверно; задаём format явно. errors='coerce' —
мусорная строка станет NaN, а не уронит конвейер. Зачем даты? На неделе 6 из них будем
вытаскивать месяц и группировать.
to_datetime для строки «не дата» с errors='coerce'?.str-аксессор: векторизованные строковые методыВсе строковые методы вызываются через аксессор df['col'].str.* и
работают сразу над всеми значениями (векторизованно). contains(na=False) —
что делать с NaN: не падать, а считать False.
na=False в str.contains? Зачем он нужен?str.extract: вытащить число из «12,5 кг» (мост к regex)str.extract берёт из строки кусок, подходящий под шаблон — регулярное
выражение. Скобки (...) — «какую группу вернуть». Здесь — цифры и
запятая. Результат — строка; в число переводим to_numeric. Полноценно regex —
на неделе 9.
map: замена значений словарём/функциейmap на Series применяет к каждому значению словарь или функцию.
Словарь — для замены «код → название»; функция — для простого поштучного преобразования.
Если есть векторный способ (.str, арифметика, np.where) — он
быстрее и читается лучше.
apply на Series: произвольная функцияapply прогоняет каждый элемент через функцию — когда логику
не записать одной векторной операцией (if/else по условию с текстом). Но: если условие
простое, np.where(cond, 'взрослый', 'ребёнок') — то же самое и быстрее.
apply вызывает Python-функцию на каждый элемент — медленно на больших данных.
apply или np.where? Почему?apply на DataFrame (axis=1) и .map (экс-applymap) — обзорноapply(..., axis=1) — функция на каждую строку; на 100 тыс.
строк это медленно — почти всегда заменяется арифметикой колонок (price * qty).
Поэлементное применение по всему DataFrame — .map (в pandas 3.0 метод
applymap переименован в .map).
df_small.apply(lambda row: row['price']*row['qty'], axis=1) без apply?apply: быстрее и чище| Задача | Медленно | Векторизованно |
|---|---|---|
| «взрослый/ребёнок» по возрасту | ages.apply(lambda a: ...) | np.where(ages > 18, 'взр.', 'реб.') |
| выручка = цена × количество | df.apply(lambda r: r.price*r.qty, axis=1) | df['price'] * df['qty'] |
| нормализация текста | цикл по строкам | df['name'].str.strip().str.lower() |
| замена по словарю | df['col'].apply(lambda x: d[x]) | df['col'].map(d) |
apply/цикл — Python-функция на каждый элемент; векторная операция —
один проход на C-уровне. Где есть np.where, .str,
арифметика, map — они быстрее, короче и не ловят «классику» вроде iterrows.
salary * 1.1: apply(lambda x: x*1.1) или df['salary'] * 1.1?Числа: q1 = 77250.0, q3 = 107500.0, iqr = 30250.0, lower = 31875.0, upper = 152875.0.
Выброс — значение за границами q1 − 1.5·IQR / q3 + 1.5·IQR.
В наших зарплатах это 12.5 — опечатка «12,5», ставшая абсурдной зарплатой. IQR ловит именно
ошибки ввода: одно «далёкое» число сигналит об опечатке.
|z| > 2z-score — сколько стандартных отклонений значение отстоит от среднего.
Порог |z| > 2 (иногда 3) — выброс. Оба метода (IQR и z-score) поймали одно
и то же — 12.5. Когда методы согласны, решение принимается увереннее.
s.clip(lower, upper)) — «прижать» к границе;
уместно для честных, но экстремальных значений (зарплата топ-менеджера), когда не хотим
удалять данные.is_outlier = ...) — не терять
информацию, но не дать выбросу исказить статистики.Никогда не «удаляй по умолчанию». Сначала пойми природу выброса: ошибка ввода → удалить; честное экстремальное значение → каппировать или пометить. Решение записывается в markdown-обоснование.
На каждом шаге — «как проверить»: .head(), .isna().sum(),
.dtypes, .shape.
После каждого шага выводи результат и сверяй с контрольной точкой — не переходи дальше «вслепую».
data_quality(df) до и после очисткиdata_quality обобщает аудит в функцию, которую можно применить к
любому DataFrame: тип, число и доля пропусков, дубликаты. Вызов ДО и ПОСЛЕ очистки —
контрольная точка конвейера.
share_missing? При какой доле колонку обычно удаляют?clean(): порядок шагов и df.copy()Порядок шагов не случаен: сначала типы (str.replace →
to_numeric), потом заполнение пропусков, потом дубликаты и «безнадёжные» строки.
df.copy() в начале изолирует вход — функция чистая и воспроизводимая.
Итог: 16 строк → 13 (минус дубликат emp_001 и 2 строки без зарплаты).
df.copy() в начале clean()? Что случится без него?df.dropna() / df.fillna(0)
«не сработали» — результат не присвоен.apply там, где достаточно map или векторизации (лишние циклы по элементам).to_datetime без format на нестандартных строках — парсится
неверно; нужен явный формат и errors='coerce'.na_values при чтении — строки «N/A», «-» не распознаются как NaN.drop_duplicates() без subset — дубликат по ключу с другими
полями не удалится.df.fillna(0) и удивлён: пропуски остались. Что он забыл?clean.csv с обоснованиемclean.csv + ноутбук clean_data.ipynb; autograder
проверяет load_clean(path) -> pd.DataFrame (нет NaN, нет дубликатов,
верные dtype).clean_data.ipynb?dropna, а когда fillna (среднее/медиана/ffill)?map отличается от apply на Series и когда что быстрее?str.extract?df.fillna(0) колонка может не измениться?2 минуты письменно; соберите 1–2 ответа вслух.
clean() (5.4), challenge —
аудит + каппирование выбросов (5.5). Данные — dirty_salary.csv.clean.csv
с обоснованием каждого шага; autograder load_clean.dirty_salary.csv и проверьте на себе шаги 5.1–5.5.dtypes, isna().sum(), duplicated().sum().dropna/fillna).astype, to_numeric, to_datetime,
нормализация число-строк..str, map/apply, выбирать векторизацию.One-minute paper: главное, что вы поняли, и один оставшийся вопрос.