Reshape и merge в pandas
Переформатирование таблиц - melt, pivot, concat - проверка на то, умеешь ли ты приводить данные к форме, удобной для задачи. Вопрос-детектор звучит просто: «когда держать данные в long, а когда в wide». За ним стоит половина ежедневной работы с табличками.
Типовые формулировки: «разверни таблицу по месяцам», «почему pivot упал?», «чем concat отличается от merge?».
// Правило большого пальца, которое стоит проговорить: считаешь и джойнишь - держи в long, показываешь человеку - разворачивай в wide.
Long и wide на одной табличке
Wide - привычный человеку вид: строка это юзер, а колонки это январь, февраль, март со значениями внутри. Помещается в презентацию, читается глазами, ложится в матричные методы вроде корреляций.
Long - вид для машины: строка это одно наблюдение, то есть тройка (юзер, месяц, значение). Тех же данных получается втрое больше строк и втрое меньше колонок. Зато по нему естественно делать groupby, джойны и фильтры, и почти все библиотеки графиков ждут именно его.
melt переводит wide в long: колонки-метрики схлопываются в две - имя и значение. Параметр id_vars перечисляет то, что должно остаться идентификатором строки и не расплавиться.
// Симптом, что ты воюешь с форматом вместо работы: пытаешься сделать groupby «по колонкам» или пишешь цикл по названиям колонок. Значит, данные надо было сначала расплавить в long, и задача исчезнет сама.
- наблюдение
- один замер одной величины в одних условиях. В long-формате каждому наблюдению отведена ровно одна строка, и в этом весь смысл формата
pivot, pivot_table и молчаливое среднее
pivot идёт обратным путём, из long в wide, и у него есть жёсткое требование: пара «строка плюс колонка» обязана быть уникальной. Два наблюдения с одинаковым ключом - и pivot падает с ValueError «Index contains duplicate entries, cannot reshape».
pivot_table дубли переживает, потому что их агрегирует. И вот здесь ловушка: если aggfunc не задан, он молча берёт среднее. Значения 1 и 3 по одному ключу превратятся в 2.0, никто ничего не скажет, а в отчёт уйдёт число, которого в данных не было. Задавай aggfunc явно - сумма, максимум, количество, - потому что выбор агрегата это содержательное решение.
stack и unstack делают то же самое через уровни индекса: переносят уровень между строками и колонками. После groupby по нескольким ключам возникает MultiIndex, индекс из нескольких уровней. reset_index() возвращает уровни в обычные колонки, а as_index=False прямо в groupby не даёт им туда попасть вовсе.
// Забытый MultiIndex - типичный источник «странных джойнов»: merge выравнивается по индексу, о существовании которого ты уже не помнишь, и соединяет данные не по тем ключам.
- MultiIndex
- индекс из нескольких уровней, например (город, месяц). Появляется сам после группировки по нескольким ключам и потом удивляет
concat против merge
concat склеивает таблицы по оси. С axis=0 они встают одна под другую: набор колонок объединяется, недостающие клетки заполняются пропусками. С axis=1 - бок о бок, и вот тут прячется главное: выравнивание идёт по индексу, а не по порядку строк.
Две таблицы по две строки с индексами [0, 1] и [5, 6] после concat(axis=1) дадут не две строки, а четыре, наполовину пустые: индексы не пересеклись, и pandas честно вывел объединение. Лечится reset_index(drop=True) перед склейкой или честным merge по ключу.
Разделение смыслов простое: merge - про ключи, concat - про оси. Путаница между ними даёт либо декартово произведение, либо решето из пропусков, и оба случая обнаруживаются не сразу.
// explode разворачивает список внутри ячейки в отдельные строки - обычный шаг после разбора вложенного JSON (JavaScript object notation). Обратно всё собирается через groupby с agg(list).
Как отвечать: «Когда данные держать в long, а когда в wide?»
Long - формат вычислений: строка равна наблюдению, на нём естественно ложатся groupby, джойны, фильтры, и графические библиотеки ждут именно его. Wide - формат представления: метрики разложены по колонкам, удобно человеку и матричным методам вроде корреляций. Практически я держу весь пайплайн в long до последнего шага и разворачиваю в wide только для отчёта, через pivot_table с явно заданным aggfunc, потому что дубли ключа иначе молча усреднятся и в отчёт уедет число, которого в данных не было. Обратный путь - melt, он нужен постоянно: чужие экспорты почти всегда приходят в wide.
Назван критерий выбора, направление пайплайна и конкретная ловушка с дублями. Это ответ практика, а не пересказ документации.
На чём валят
- −pivot на дублях ключа падает с ValueError, а pivot_table на тех же данных молча усредняет - и это тоже надо знать заранее.
- −concat(axis=1) с несовпадающими индексами даёт решето из пропусков вместо склейки бок о бок.
- −Мучить groupby «по колонкам» в wide-формате там, где данные просились в long через melt.
- −Забыть reset_index после агрегации и получить странные джойны по MultiIndex.
- −Путать concat и merge: первый про оси, второй про ключи, и последствия у ошибки разные.
Проверьте себя
Пять вопросов из банка по этой подтеме. Всего их 9, остальные разбираются в тренажёре.
- После merge боишься неожиданного размножения строк из-за дублей ключа. Как поймать это заранее, а не постфактум?A)Просто сравнивать число строк до и после каждого merge вручную и ловить раздувание постфактумB)merge(validate='one_to_one'/...) проверяет кардинальность ключа и бросает MergeErrorC)Ставить how='inner', тогда дублей ключа не возникнетD)Дубликаты ключа в merge заранее не проверить
показать ответ и разбор
+B)merge(validate='one_to_one'/...) проверяет кардинальность ключа и бросает MergeError// разбор: Параметр validate заставляет pandas проверить ожидаемую связь по ключу: 'one_to_one' требует уникальности с обеих сторон, 'one_to_many'/'many_to_one' — с одной. Если реальность не совпадает (например, дубли там, где ждали уникальность), merge сразу бросает MergeError — до того как таблица молча раздуется декартовым фрагментом. Это дешёвая страховка от классического fan-out при джойнах.
- Нужно во всей строковой колонке привести к нижнему регистру и найти вхождение подстроки. Как правильно в pandas?A)Аксессор .str: col.str.lower(), col.str.contains('abc') — векторно по колонкеB)Питоновский цикл с str.lower() по каждому элементуC)Применить обычный lower() прямо к Series целикомD)Строковые операции в pandas делают исключительно через регулярные выражения внутри вызова apply
показать ответ и разбор
+A)Аксессор .str: col.str.lower(), col.str.contains('abc') — векторно по колонке// разбор: Аксессор .str даёт векторные версии строковых методов: .str.lower(), .str.contains(pat, na=False), .str.replace, .str.split, .str.extract (с regex) и т.д. Они работают по всей колонке разом и аккуратно обходят NaN (параметр na). Это чище и быстрее цикла или .apply(str.lower), а вызвать lower() прямо на Series нельзя — метода объекта Series такого нет.
- Нужно поэлементно выбрать значение по условию: где cond истинно — из x, иначе из y. Чем это делают векторно?A)Питоновским тернарником x if cond else y прямо над SeriesB)Циклом с if по строкамC)Только через.apply со сложной лямбдой, перебирающей условие по каждому элементу SeriesD)np.where(cond,x,y) — векторный выбор; Series.where/mask — условная замена
показать ответ и разбор
+D)np.where(cond,x,y) — векторный выбор; Series.where/mask — условная замена// разбор: np.where(cond, x, y) возвращает массив, беря x там, где cond истинно, и y иначе — быстрый векторный аналог поэлементного if-else. Для случая «оставить исходные значения, а заменить только там, где условие» удобнее Series.where(cond, other): сохраняет значение, где cond True, и ставит other, где False; Series.mask — противоположная логика. Питоновский тернарник над Series не векторизуется и даст ошибку неоднозначности.
- Нужна доля каждой категории ВНУТРИ группы (по каждому городу — распределение тарифов). Чем считать?A)df.pivot_table(index='city', columns='tariff', aggfunc='mean')B)df.groupby('city')['tariff'].value_counts(normalize=True)C)df.groupby('city')['tariff'].apply(lambda s: s / s.sum())D)df['tariff'].value_counts() / len(df)
показать ответ и разбор
+B)df.groupby('city')['tariff'].value_counts(normalize=True)// разбор: value_counts(normalize=True) внутри groupby считает частоты категорий и делит их на размер своей группы — ровно то, что нужно. Тот же вызов без groupby дал бы доли по всей таблице. pivot_table агрегирует числовую колонку, а не частоты категорий; делить серию категорий на её сумму бессмысленно, там строки.
- После df.groupby('city')['revenue'].sum() город оказался индексом. Как вернуть его обычной колонкой?A)df.reset_index() — индекс переедет в колонкуB)df.set_index('city') — вернёт колонку на местоC)df.T — транспонирование поднимет индекс наверхD)Никак: после агрегации индекс закреплён за результатом навсегда
показать ответ и разбор
+A)df.reset_index() — индекс переедет в колонку// разбор: groupby по умолчанию делает ключ группировки индексом результата. reset_index() сбрасывает его обратно в обычную колонку и нумерует строки заново — без этого шага merge по city или запись в CSV поведут себя не так, как ждёшь. Тот же эффект даёт groupby('city', as_index=False): агрегат сразу вернётся плоским DataFrame. set_index делает ровно обратное — превращает колонку в индекс.
дальше
Теорию прочитали. Навык ставится повторением
В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.