Съединяване: merge
Накратко: merge в pandas съединява две таблици по общ ключ, както JOIN в SQL, а how решава кои редове остават.
Проблем
„Коя категория носи най-много пари?“
Управителят пита коя категория продава най-много. Редовете на поръчките казват колко бройки от кой продукт са купени и на каква цена:
Категория обаче там няма. Има само product_id, номерът на продукта. Името и категорията са в products.csv, а името на категорията е в трета таблица, categories.csv.
Данните в магазин почти винаги са разделени така: всяко нещо е записано на едно място, а другите таблици сочат към него с номер. За отговора трябва да слепиш таблиците в една.
Идеята
Слепване по ключ: merge
merge взима всеки ред от лявата таблица, търси в дясната реда със същата стойност в колоната ключ и слепва двата реда в един. Това се казва съединяване (join), а колоната, по която се търси, е ключ. В SQL същото е JOIN.
Тук ключът се казва различно в двете таблици: product_id в редовете, id в продуктите. left_on и right_on казват коя колона от коя страна е:
Редовете остават 152, а колоните стават 10: четирите от items и шестте от products. Колоните product_id и id са едно и също число, вече два пъти.
Затова по-често първо преименуваш ключа, за да се казва еднакво, и съединяваш с on. Взимаш и само колоните, които ти трябват:
Виж го
Как merge намира двойките
Ето същото на малки таблици: 5 реда от поръчки и ценоразпис с 4 реда. В ценоразписа кафето (17) е два пъти, със старата и с новата цена, а продукт 26 още го няма. С how="left" всеки ред от поръчките остава:
import pandas as pd
lines = pd.DataFrame({ 'order_id': [7, 7, 8, 8, 9], 'product_id': [1, 17, 5, 1, 26], 'quantity': [2, 1, 3, 1, 2],})catalog = pd.DataFrame({ 'product_id': [1, 5, 17, 17], 'name': ['Бял хляб', 'Прясно мляко', 'Кафе на зърна', 'Кафе на зърна'], 'price': [1.2, 1.6, 13.9, 14.9],})both = lines.merge(catalog, on='product_id', how='left')print(len(lines), len(both))bothСтъпка по стъпка, с думи
- lines са 5 реда от поръчки. product_id казва кой продукт е купен.
- catalog е ценоразписът: 4 реда. Продукт 17 е в него два пъти, със старата и с новата цена.
- Ред 1: product_id 1. merge търси всички редове с 1 в catalog: Бял хляб. Двойката става ред в both.
- Ред 2: product_id 17 е два пъти в catalog, с 13,90 € и 14,90 €. Двете двойки стават два реда.
- Ред 3: product_id 5, това е Прясно мляко.
- Ред 4: product_id 1, пак Бял хляб. Един ред от catalog може да е двойка на много редове.
- Ред 5: продукт 26 го няма в catalog. С how='left' редът остава, а name и price са NaN.
- 5 реда влязоха, 6 излязоха: повтореният ключ удвои реда. Затова броиш len преди и след.
- Всеки ред от поръчките търси всички редове в ценоразписа със същия
product_id. - Редът с кафето намира два и става два реда. Петте реда стават 6.
- Продукт 26 не намира нищо. С
how="left"редът остава, а колоните от ценоразписа са NaN.
Пробвай
Колко реда?
Идеята
Кои редове остават: how
Не всеки ред има двойка. Двама от 20-те клиенти още не са поръчвали нищо, а поръчките са 60. Параметърът how решава какво става с редовете без двойка:
inner(по подразбиране): само двойките. 60 реда, по един за всяка поръчка. Клиентите без поръчки ги няма.left: всички редове отляво, с NaN, където няма двойка. 62 реда: 60 поръчки и двамата клиенти без поръчки.right: всички редове отдясно. Тук е 60, защото всяка поръчка има клиент.outer: всички редове от двете страни.
Кой ред откъде е, казва indicator=True. Той добавя колона _merge със стойности both, left_only и right_only:
Двамата клиенти без поръчки са с id 19 и 20. right_only е 0: няма поръчка на клиент, който го няма в таблицата. Ако имаше, това щеше да е грешка в данните и indicator щеше да я покаже.
По-точно, за любопитните
indicator е най-бързата проверка след съединяване. value_counts() на _merge веднага казва колко реда не са намерили двойка. Ако очакваш 0, а са 300, ключовете не съвпадат: например единият е текст '17', а другият е число 17.
Пробвай
Продукти без отзив
Кои продукти още нямат нито един отзив? Съедини products с отзивите (reviews) така, че да останат всички продукти, и запиши в no_review само редовете без двойка.
Решение
import pandas as pd
products = pd.read_csv('products.csv')
reviews = pd.read_csv('reviews.csv')
both = products.merge(reviews[['product_id', 'rating']], left_on='id', right_on='product_id', how='left', indicator=True)
no_review = both[both['_merge'] == 'left_only']
print(len(no_review))
no_review['name']С how="left" и indicator=True всички 25 продукта остават, а тези без отзив са с _merge == "left_only". Такива са 12, сред тях кифлата с масло, баницата и подаръчната карта. С вътрешно съединяване (inner) продуктите без отзив изчезват още при слепването и left_only е празно.
Идеята
Когато ключът се повтаря
Сега управителят иска до всеки ред от поръчка да стои оценката на продукта. Отзивите са в reviews.csv и изглежда естествено да ги съединиш по product_id:
Редовете станаха 167 от 152, а сборът на поръчките скочи от 1 963,50 € на 2 485,60 €. Кафето има 3 отзива, затова всеки ред с кафе се слепи с трите и стана три реда. Тиганът (22) има 2 отзива. Нищо не гърми, но числата вече са грешни.
Затова броиш редовете преди и след всяко съединяване. Ако дясната таблица трябва да има по един ред на ключ, кажи го на pandas с validate:
validate="many_to_one" значи „отляво ключът може да се повтаря, отдясно трябва да е уникален“. Ако не е, merge спира с MergeError и ти казва кой ключ се повтаря. Има и "one_to_one" и "one_to_many".
Пробвай
Оценка до всеки ред
Сложи до всеки ред от поръчките средната оценка на продукта. Редовете трябва да останат колкото са (152), а продуктите без отзив да са с NaN.
Решение
import pandas as pd
items = pd.read_csv('order_items.csv')
reviews = pd.read_csv('reviews.csv')
avg = reviews.groupby('product_id', as_index=False)['rating'].mean()
with_rating = items.merge(avg, on='product_id', how='left', validate='many_to_one')
print(len(items), len(with_rating))
with_rating.head(3)Първо reviews.groupby("product_id", as_index=False)["rating"].mean() дава по един ред на продукт: 13 продукта, кафето е с 3,33. Сега ключът отдясно е уникален и validate="many_to_one" минава, а редовете остават 152. 75 от тях са на продукти без отзив и имат NaN. С вътрешно съединяване щяха да изчезнат, а без groupby редовете щяха да станат 167.
Идеята
Две колони name: suffixes
Следва категорията. И продуктите, и категориите имат колони id и name. След съединяването pandas пази и двете и им слага окончания _x (отляво) и _y (отдясно). С suffixes ги избираш сам:
suffixes=("", "_category") оставя колоните на продукта както са, а на категорията добавя _category. Подаръчната карта е без категория, затова name_category е NaN.
Тук е капанът на урока. Ако забравиш how="left", вътрешното съединяване изхвърля подаръчната карта без нито едно предупреждение:
152 реда стават 148. Четири продажби на подаръчна карта, общо 240 €, изчезват от отчета по категории. С how="left" редовете остават 152, а липсващата категория се вижда като NaN.
Идеята
Една под друга: concat
Понякога таблиците не трябва да се слепват една до друга, а една под друга. Магазинът сменил системата: поръчките до края на 2025 са изтеглени от старата, а тези от 2026 от новата, с едни и същи колони. pd.concat ги нарежда в една таблица:
21 + 39 = 60 реда. ignore_index=True номерира редовете наново от 0. Без него индексите от двете таблици се запазват и може да се повтарят.
Датите тук са текст във вида „2025-09-01“, затова сравнението < '2026-01-01' по азбучен ред съвпада с реда във времето. Истинските дати са в урока „Дати и времеви редове“.
По-точно, за любопитните
merge търси двойки по ключ и слага колоните една до друга. concat не търси нищо: нарежда редовете (или с axis=1 колоните) по реда на таблиците. Ако колоните не съвпадат, concat пак работи, но липсващите клетки стават NaN, затова преди него сравни df.columns на двете таблици.
Упражнение
Отчетът по категории
Отговори на управителя: колко е оборотът на всяка категория, без отказаните поръчки? Съедини редовете с поръчките (за status), с продуктите (за category_id) и с категориите, така че след всяко съединяване редовете да са пак 152. Продуктите без категория да са под „Без категория“. Запиши резултата в by_category.
Решение
import pandas as pd
items = pd.read_csv('order_items.csv')
orders = pd.read_csv('orders.csv').rename(columns={'id': 'order_id'})
products = pd.read_csv('products.csv').rename(columns={'id': 'product_id'})
categories = pd.read_csv('categories.csv').rename(columns={'id': 'category_id', 'name': 'category'})
lines = items.merge(orders[['order_id', 'status']], on='order_id', how='left', validate='many_to_one')
lines = lines.merge(products[['product_id', 'category_id']], on='product_id', how='left', validate='many_to_one')
lines = lines.merge(categories[['category_id', 'category']], on='category_id', how='left', validate='many_to_one')
print(len(items), len(lines))
lines['category'] = lines['category'].fillna('Без категория')
sold = lines[lines['status'] != 'cancelled']
sold['revenue'] = sold['quantity'] * sold['unit_price']
by_category = sold.groupby('category')['revenue'].sum().round(2).sort_values(ascending=False)
by_categoryС how="left" и validate="many_to_one" и трите съединявания пазят 152 реда. fillna("Без категория") дава име на подаръчната карта. Без отказаните поръчки остават 131 реда за общо 1 697,00 €. Най-много носи „Кухня“ с 516,90 €, а подаръчната карта е 160,00 €. С вътрешно съединяване картата изчезва, а ако не махнеш отказаните, „Кухня“ излиза 601,50 €.
Тест
Провери се
Свързани уроци
Раздел „Свързани уроци“- Нужно отпреди: Филтри и подреждане · Нови колони · Типове и липсващи стойности · Групи: groupby
- Продължава в: Широко и дълго: pivot_table и melt · Дати и времеви редове · Първата графика · Коя графика за какъв въпрос
- Задачи по темата: Подреждане на данни