W każdym systemie e-commerce dane są rozproszone po różnych tabelach - zamówienia w jednej, klienci w drugiej, produkty w trzeciej, kategorie w czwartej. Klasyczny problem relacyjnych baz danych, który wymaga inteligentnego łączenia na podstawie wspólnych kluczy.
W świecie pandas odpowiedzią jest pd.merge() - funkcja, która pozwala łączyć DataFrame'y jak w SQL, ale z całą elastycznością Pythona. To narzędzie, które pozwala nam sprawniej i efektywniej operować zbiorami danych.
Czym jest pd.merge()?
pd.merge() to funkcja pandas służąca do łączenia DataFrame'ów na podstawie wspólnych kolumn lub indeksów, podobnie jak JOIN w SQL. W przeciwieństwie do pd.concat(), które "skleja" dane mechanicznie, merge łączy je inteligentnie - dopasowuje wiersze na podstawie wartości w określonych kolumnach.
import pandas as pd
# Podstawowa składnia
pd.merge(left, right, how='inner', on=None, left_on=None, right_on=None,
left_index=False, right_index=False, suffixes=('_x', '_y'))Podstawowe typy łączeń
Przejrzyjmy zatem podstawowe typy łączeń, dostępne w ramach funkcjonalności pd.merge.
Inner Join - tylko pasujące rekordy
Inner join zachowuje tylko te wiersze, które mają dopasowanie w obu DataFrame'ach. To najbezpieczniejszy typ łączenia, ale można stracić dane.
Przykład inner join:
# Tabela zamówień
orders = pd.DataFrame({
'order_id': [1001, 1002, 1003, 1004],
'customer_id': [501, 502, 503, 504],
'amount': [150.00, 89.50, 299.00, 75.25],
'product_id': ['P001', 'P002', 'P001', 'P003']
})
# Tabela klientów
customers = pd.DataFrame({
'customer_id': [501, 502, 503, 505], # Brak klienta 504, jest 505
'name': ['Jan Kowalski', 'Anna Nowak', 'Piotr Wiśniewski', 'Maria Dąbrowska'],
'city': ['Warszawa', 'Kraków', 'Gdańsk', 'Poznań']
})
# Inner join - tylko klienci którzy mają zamówienia i są w tabeli klientów
orders_with_customers = pd.merge(orders, customers, on='customer_id', how='inner')
print(f"Inner join: {len(orders_with_customers)} wierszy") # 3 wiersze (brak 504)Left Join - wszystkie rekordy z lewej tabeli
# Left join - wszystkie zamówienia, nawet bez danych klienta
all_orders = pd.merge(orders, customers, on='customer_id', how='left')
print(f"Left join: {len(all_orders)} wierszy") # 4 wiersze (zamówienie 504 z NaN w name/city)Left join to częsty wybór w analizach biznesowych - zachowujemy wszystkie zamówienia, nawet jeśli nie mamy pełnych danych o kliencie.
Right Join - wszystkie rekordy z prawej tabeli
# Right join - wszyscy klienci, nawet bez zamówień
all_customers = pd.merge(orders, customers, on='customer_id', how='right')
print(f"Right join: {len(all_customers)} wierszy") # 4 wiersze (klient 505 bez zamówienia)Right join przydaje się, gdy chcemy zobaczyć wszystkich klientów, w tym tych którzy jeszcze nie złożyli zamówienia.
Outer Join - wszystkie rekordy z obu tabel
# Outer join - wszystko
complete_picture = pd.merge(orders, customers, on='customer_id', how='outer')
print(f"Outer join: {len(complete_picture)} wierszy") # 5 wierszy (wszystkie kombinacje)Outer join daje kompletny obraz - wszystkie zamówienia i wszystkich klientów, z NaN tam gdzie brak dopasowania.
Łączenie po różnych nazwach kolumn
# Tabela produktów z inną nazwą klucza
products = pd.DataFrame({
'product_code': ['P001', 'P002', 'P003'], # Inna nazwa niż product_id
'product_name': ['iPhone 14', 'Samsung S23', 'MacBook Air'],
'category': ['Electronics', 'Electronics', 'Computers'],
'price': [999.99, 899.99, 1299.99]
})
# Łączenie po różnych nazwach kolumn
orders_with_products = pd.merge(orders, products,
left_on='product_id', right_on='product_code',
how='inner')left_on i right_on pozwalają łączyć DataFrame'y nawet gdy klucze mają różne nazwy.
Łączenie po wielu kolumnach
# Tabela z cenami historycznymi
historical_prices = pd.DataFrame({
'product_id': ['P001', 'P001', 'P002', 'P002'],
'date': ['2024-01-01', '2024-02-01', '2024-01-01', '2024-02-01'],
'price': [999.99, 1049.99, 899.99, 849.99]
})
# Zamówienia z datami
orders_with_dates = pd.DataFrame({
'order_id': [1001, 1002, 1003],
'product_id': ['P001', 'P002', 'P001'],
'date': ['2024-01-01', '2024-02-01', '2024-02-01']
})
# Łączenie po dwóch kolumnach jednocześnie
orders_with_historical_prices = pd.merge(orders_with_dates, historical_prices,
on=['product_id', 'date'],
how='inner')Przykłady biznesowe z e-commerce
Analiza zamówień z pełnymi informacjami
# Tabela zamówień - podstawowe informacje
orders = pd.DataFrame({
'order_id': [1001, 1002, 1003, 1004, 1005],
'customer_id': [501, 502, 503, 501, 504],
'product_id': ['P001', 'P002', 'P003', 'P002', 'P001'],
'quantity': [1, 2, 1, 1, 3],
'order_date': ['2024-01-15', '2024-01-16', '2024-01-17', '2024-01-18', '2024-01-19']
})
# Tabela klientów - dane demograficzne
customers = pd.DataFrame({
'customer_id': [501, 502, 503, 504],
'customer_name': ['Jan Kowalski', 'Anna Nowak', 'Piotr Wiśniewski', 'Maria Dąbrowska'],
'customer_segment': ['Premium', 'Standard', 'Premium', 'Standard'],
'city': ['Warszawa', 'Kraków', 'Gdańsk', 'Poznań'],
'registration_date': ['2023-01-01', '2023-06-15', '2022-11-30', '2024-01-10']
})
# Tabela produktów - informacje katalogowe
products = pd.DataFrame({
'product_id': ['P001', 'P002', 'P003'],
'product_name': ['iPhone 14', 'Samsung Galaxy S23', 'MacBook Pro'],
'category': ['Electronics', 'Electronics', 'Computers'],
'unit_price': [999.99, 899.99, 2299.99],
'cost': [600.00, 550.00, 1400.00]
})
# Pierwszy krok - zamówienia z informacjami o produktach
orders_products = pd.merge(orders, products, on='product_id', how='left')
orders_products['total_revenue'] = orders_products['quantity'] * orders_products['unit_price']
orders_products['total_cost'] = orders_products['quantity'] * orders_products['cost']
orders_products['margin'] = orders_products['total_revenue'] - orders_products['total_cost']
# Drugi krok - dodanie informacji o klientach
complete_orders = pd.merge(orders_products, customers, on='customer_id', how='left')
# Analiza marży per segment klienta
margin_by_segment = complete_orders.groupby('customer_segment').agg({
'total_revenue': 'sum',
'total_cost': 'sum',
'margin': 'sum',
'order_id': 'count'
}).round(2)
print("Analiza marży per segment:")
print(margin_by_segment)Budowanie kohort klientów
# Tabela pierwszych zamówień klientów
first_orders = pd.DataFrame({
'customer_id': [501, 502, 503, 504, 505],
'first_order_date': ['2023-01-15', '2023-06-20', '2022-12-01', '2024-01-10', '2024-02-01'],
'first_order_value': [150.00, 89.50, 299.00, 75.25, 120.00],
'acquisition_channel': ['Google', 'Facebook', 'Organic', 'Email', 'Referral']
})
# Tabela aktywności ostatnich 90 dni
recent_activity = pd.DataFrame({
'customer_id': [501, 502, 504, 505], # Brak klienta 503 - nieaktywny
'orders_90d': [5, 2, 1, 3],
'revenue_90d': [750.00, 180.00, 75.25, 360.00],
'last_order_date': ['2024-01-19', '2024-01-10', '2024-01-10', '2024-02-05']
})
# Łączenie dla analizy kohort
customer_cohort = pd.merge(first_orders, recent_activity,
on='customer_id', how='left')
# Identyfikacja klientów churn (bez aktywności w ostatnich 90 dniach)
customer_cohort['is_churned'] = customer_cohort['orders_90d'].isna()
customer_cohort['lifetime_value'] = customer_cohort['first_order_value'] + customer_cohort['revenue_90d'].fillna(0)
# Analiza churn per kanał akwizycji
churn_by_channel = customer_cohort.groupby('acquisition_channel').agg({
'is_churned': ['count', 'sum'],
'lifetime_value': 'mean'
}).round(2)Analiza cross-sell i up-sell
# Historia zakupów klientów
purchase_history = pd.DataFrame({
'customer_id': [501, 501, 501, 502, 502, 503, 503, 503],
'product_id': ['P001', 'P004', 'P005', 'P002', 'P006', 'P001', 'P002', 'P003'],
'purchase_date': ['2024-01-01', '2024-01-15', '2024-02-01',
'2024-01-10', '2024-01-25', '2024-01-05', '2024-01-20', '2024-02-10'],
'amount': [999.99, 49.99, 29.99, 899.99, 19.99, 999.99, 899.99, 2299.99]
})
# Kategorie produktów dla analizy cross-sell
product_categories = pd.DataFrame({
'product_id': ['P001', 'P002', 'P003', 'P004', 'P005', 'P006'],
'category': ['Phone', 'Phone', 'Laptop', 'Accessory', 'Accessory', 'Accessory'],
'subcategory': ['Smartphone', 'Smartphone', 'MacBook', 'Case', 'Cable', 'Charger']
})
# Łączenie historii z kategoriami
purchases_with_categories = pd.merge(purchase_history, product_categories,
on='product_id', how='left')
# Analiza cross-sell - ile kategorii kupuje każdy klient
cross_sell_analysis = purchases_with_categories.groupby('customer_id').agg({
'category': 'nunique',
'subcategory': 'nunique',
'amount': ['sum', 'count', 'mean']
}).round(2)
cross_sell_analysis.columns = ['categories_count', 'subcategories_count',
'total_spent', 'orders_count', 'avg_order_value']
print("Analiza cross-sell:")
print(cross_sell_analysis)Zaawansowane techniki
Obsługa duplikatów przy merge
# Problem: klient ma wiele adresów
customer_addresses = pd.DataFrame({
'customer_id': [501, 501, 502, 503], # Klient 501 ma 2 adresy
'address_type': ['home', 'work', 'home', 'home'],
'city': ['Warszawa', 'Kraków', 'Kraków', 'Gdańsk']
})
# Merge z duplikatami - powstanie więcej wierszy
orders_with_addresses = pd.merge(orders, customer_addresses, on='customer_id', how='left')
print(f"Zamówień przed merge: {len(orders)}")
print(f"Zamówień po merge: {len(orders_with_addresses)}") # Więcej przez duplikaty
# Rozwiązanie: filtrowanie do jednego adresu na klienta
primary_addresses = customer_addresses.drop_duplicates('customer_id', keep='first')
orders_with_primary = pd.merge(orders, primary_addresses, on='customer_id', how='left')Używanie suffixes dla kolumn o tych samych nazwach
# Dwie tabele z kolumną 'name'
customers_basic = pd.DataFrame({
'customer_id': [501, 502],
'name': ['Jan Kowalski', 'Anna Nowak'],
'created_date': ['2023-01-01', '2023-06-15']
})
customers_extended = pd.DataFrame({
'customer_id': [501, 502],
'name': ['J. Kowalski', 'A. Nowak'], # Inna forma tego samego
'updated_date': ['2024-01-15', '2024-01-16']
})
# Merge z suffixes
combined_customers = pd.merge(customers_basic, customers_extended,
on='customer_id', how='inner',
suffixes=('_original', '_updated'))Łączenie z agregacją
# Najpierw agregujemy dane zamówień per klient
customer_stats = orders.groupby('customer_id').agg({
'order_id': 'count',
'quantity': 'sum'
}).reset_index()
customer_stats.columns = ['customer_id', 'total_orders', 'total_items']
# Potem łączymy z danymi demograficznymi
customers_with_stats = pd.merge(customers, customer_stats,
on='customer_id', how='left')
# Wypełniamy NaN zerami dla klientów bez zamówień
customers_with_stats[['total_orders', 'total_items']] = customers_with_stats[['total_orders', 'total_items']].fillna(0)Najlepsze praktyki
Zawsze sprawdzaj wynik merge
# Przed merge
print(f"Zamówienia: {len(orders)} wierszy")
print(f"Klienci: {len(customers)} wierszy")
# Po merge
result = pd.merge(orders, customers, on='customer_id', how='left')
print(f"Wynik: {len(result)} wierszy")
# Sprawdzenie czy są brakujące dopasowania
missing_customers = result[result['name'].isna()]
if len(missing_customers) > 0:
print(f"Ostrzeżenie: {len(missing_customers)} zamówień bez danych klienta")Uważaj na typy danych w kluczach
# Problem: różne typy w kluczowych kolumnach
orders_str = pd.DataFrame({
'customer_id': ['501', '502', '503'], # string
'amount': [100, 200, 300]
})
customers_int = pd.DataFrame({
'customer_id': [501, 502, 503], # int
'name': ['Jan', 'Anna', 'Piotr']
})
# Nie będzie działać dobrze
# bad_result = pd.merge(orders_str, customers_int, on='customer_id')
# Rozwiązanie: konwersja typów przed merge
orders_str['customer_id'] = orders_str['customer_id'].astype(int)
good_result = pd.merge(orders_str, customers_int, on='customer_id')
Używaj validate do kontroli typu relacji
# Sprawdzenie czy relacja jest one-to-one, one-to-many, etc.
try:
result = pd.merge(orders, customers, on='customer_id',
how='left', validate='many_to_one')
print("Relacja many-to-one potwierdzona")
except ValueError as e:
print(f"Błąd walidacji: {e}")Częste pułapki i jak ich uniknąć
Problem z indeksami po merge
# Indeks może się zduplikować po merge
orders_indexed = orders.set_index('order_id')
result = pd.merge(orders_indexed, customers, on='customer_id', how='left')
# Rozwiązanie: reset indeksu jeśli potrzeba
result = result.reset_index()Nieoczekiwane duplikowanie wierszy
# Problem: tabela "jeden do wielu" w obu kierunkach
customers_multi = pd.DataFrame({
'customer_id': [501, 501, 502], # Klient 501 dwukrotnie
'segment': ['Premium', 'VIP', 'Standard']
})
orders_multi = pd.DataFrame({
'customer_id': [501, 501, 502], # Też duplikaty
'order_value': [100, 200, 150]
})
# Wynik będzie miał 5 wierszy zamiast 3!
result = pd.merge(orders_multi, customers_multi, on='customer_id')
print(f"Niespodziewanie {len(result)} wierszy") # 5 zamiast 3
# Rozwiązanie: deduplikacja przed merge
customers_dedup = customers_multi.drop_duplicates('customer_id')
safe_result = pd.merge(orders_multi, customers_dedup, on='customer_id')Wydajność dla dużych zbiorów danych
# Dla dużych DataFrame'ów warto sortować według klucza
large_orders = orders.sort_values('customer_id')
large_customers = customers.sort_values('customer_id')
# Merge będzie szybszy
result = pd.merge(large_orders, large_customers, on='customer_id')
# Dla bardzo dużych zbiorów rozważ użycie kategorii
orders['customer_id'] = orders['customer_id'].astype('category')
customers['customer_id'] = customers['customer_id'].astype('category')Porównanie z SQL
# SQL: SELECT * FROM orders o INNER JOIN customers c ON o.customer_id = c.customer_id
# Pandas:
sql_like = pd.merge(orders, customers, on='customer_id', how='inner')
# SQL: SELECT * FROM orders o LEFT JOIN customers c ON o.customer_id = c.customer_id
# Pandas:
sql_left = pd.merge(orders, customers, on='customer_id', how='left')
# SQL: SELECT * FROM orders o JOIN products p ON o.product_id = p.product_code
# Pandas:
sql_different_keys = pd.merge(orders, products,
left_on='product_id', right_on='product_code')
Praktyczne wskazówki
- pd.merge() to fundamentalne narzędzie analizy danych - swoisty most pomiędzy światem relacyjnych baz danych a elastycznym ekosystemem pandas.
- Kluczem do sukcesu jest zrozumienie różnych typów joinów i świadome wybieranie parametrów how w zależności od celu analizy. Zawsze sprawdzaj wynik merge pod kątem duplikatów i brakujących wartości. Pamiętaj o typach danych w kluczowych kolumnach i używaj validate do kontroli relacji między tabelami.
- To narzędzie, które transformuje płaskie, rozproszone dane w bogate, wielowymiarowe analizy biznesowe. Opanowanie pd.merge() otwiera drogę do zaawansowanych analiz wielotabelowych - od prostych raportów sprzedażowych po złożone modele segmentacji klientów.
FAQ - Najczęściej zadawane pytania
Jaka jest różnica między pd.merge() a pd.concat()?
pd.merge() łączy DataFrame'y na podstawie wspólnych wartości w kolumnach (jak JOIN w SQL), pd.concat() skleja je mechanicznie. Merge używasz gdy masz relacyjne dane z kluczami, concat gdy chcesz po prostu dodać wiersze lub kolumny.
Kiedy używać inner, left, right czy outer join?
Inner join gdy potrzebujesz tylko kompletnych rekordów. Left join gdy chcesz zachować wszystkie rekordy z głównej tabeli (np. wszystkie zamówienia). Right join rzadko używany. Outer join gdy potrzebujesz pełnego obrazu z obu tabel.
Dlaczego po merge mam więcej wierszy niż przed?
To znak relacji one-to-many lub many-to-many. Jeden rekord z lewej tabeli pasuje do wielu z prawej (lub odwrotnie). Sprawdź duplikaty w kluczowych kolumnach i zastanów się czy to oczekiwane zachowanie.
Jak łączyć po wielu kolumnach jednocześnie?
Użyj on=['kolumna1', 'kolumna2'] lub left_on=['kol1', 'kol2'], right_on=['kol3', 'kol4']. Przydatne przy złożonych kluczach biznesowych jak produkt + data czy klient + kanał.
Co oznacza błąd o duplikatach kolumn?
Obie tabele mają kolumny o tych samych nazwach (inne niż klucz łączenia). Użyj parametru suffixes=('_left', '_right') lub usuń zbędne kolumny przed merge.
Jak obsłużyć różne typy danych w kluczowych kolumnach?
Skonwertuj do tego samego typu przed merge: df['key'] = df['key'].astype(str) lub astype(int). Pandas nie połączy automatycznie '123' (string) z 123 (int).
Kiedy używać left_on/right_on zamiast on?
Gdy klucze mają różne nazwy w obu tabelach. Np. tabela zamówień ma 'customer_id', tabela klientów ma 'id' - wtedy left_on='customer_id', right_on='id'.
Czy merge zachowuje indeks oryginalnych DataFrame'ów?
Domyślnie tak, ale może powstać chaos. Często warto użyć reset_index() po merge lub ustawić left_index=True/right_index=True jeśli łączysz po indeksach.
Jak sprawdzić jakość wyniku merge?
Porównaj liczbę wierszy przed i po, sprawdź ile jest NaN w kluczowych kolumnach, użyj validate parameter do kontroli typu relacji (one_to_one, many_to_one, etc.).
Czy merge jest wydajny dla dużych danych?
Dla średnich zbiorów tak. Dla bardzo dużych rozważ sortowanie według klucza, użycie kategorii dla kluczowych kolumn, lub przejście na Dask/Polars dla danych większych niż RAM.
Podsumowanie
pd.merge() to potężna funkcja w bibliotece pandas, która umożliwia inteligentne łączenie tabel danych (DataFrame'ów) podobnie jak operacja JOIN w SQL. W przeciwieństwie do pd.concat(), która po prostu skleja dane, merge dopasowuje wiersze na podstawie wspólnych wartości w określonych kolumnach.
Funkcja oferuje cztery główne typy łączeń:
- Inner join - zachowuje tylko rekordy mające dopasowanie w obu tabelach
- Left join - zachowuje wszystkie rekordy z lewej tabeli, uzupełniając brakujące wartości NaN
- Right join - zachowuje wszystkie rekordy z prawej tabeli
- Outer join - zachowuje wszystkie rekordy z obu tabel
Merge pozwala na łączenie po różnych nazwach kolumn (left_on/right_on) oraz po wielu kolumnach jednocześnie, co jest nieocenione w analizach biznesowych. Funkcja umożliwia zaawansowane operacje, takie jak analiza kohort klientów, cross-sell, up-sell czy budowanie kompletnych raportów sprzedażowych.