Opanuj funkcje niestandardowe Pandas, aby w pełni wykorzystać jego potencjał. Przewodnik omawia różnice, wydajność i zastosowania apply(), map() oraz applymap() w analizie danych.
Opanowanie Pandas: Dogłębne Spojrzenie na Funkcje Niestandardowe z apply(), map() i applymap()
W świecie nauki o danych i analizy, biblioteka Pandas w Pythonie jest niezastąpionym narzędziem. Dostarcza potężne, elastyczne i wydajne struktury danych zaprojektowane tak, aby praca ze ustrukturyzowanymi danymi była zarówno łatwa, jak i intuicyjna. Chociaż Pandas zawiera bogaty zestaw wbudowanych funkcji do agregacji, filtrowania i transformacji, w podróży każdego profesjonalisty zajmującego się danymi nadchodzi moment, gdy te funkcje nie wystarczają. Musisz zastosować własną niestandardową logikę, unikalną regułę biznesową lub złożoną transformację, która nie jest łatwo dostępna.Właśnie tutaj zdolność do stosowania funkcji niestandardowych staje się supermocą. Jednak Pandas oferuje kilka sposobów na osiągnięcie tego, głównie poprzez metody apply(), map() i applymap(). Dla nowicjusza te funkcje mogą wydawać się myląco podobne. Której z nich użyć? Kiedy? I jakie są konsekwencje wydajnościowe Twojego wyboru?
Ten obszerny przewodnik odczaruje te potężne metody. Omówimy każdą z nich szczegółowo, zrozumiemy ich konkretne przypadki użycia, a co najważniejsze, nauczymy się, jak wybrać odpowiednie narzędzie do zadania, aby napisać czysty, wydajny i czytelny kod Pandas. Omówimy:
- Metoda
map(): Idealna do transformacji element po elemencie na pojedynczej Series. - Metoda
apply(): Wszechstronny „koń roboczy” do operacji wierszowych lub kolumnowych na DataFrame. - Metoda
applymap(): Specjalista do operacji element po elemencie w całym DataFrame. - Kwestie wydajności: Krytyczna różnica między tymi metodami a prawdziwą wektoryzacją.
- Najlepsze praktyki: Ramy decyzyjne, które pomogą Ci wybrać najbardziej efektywną metodę za każdym razem.
Przygotowanie gruntu: Nasz przykładowy zbiór danych
Aby nasze przykłady były praktyczne i jasne, będziemy pracować z spójnym, globalnie istotnym zbiorem danych. Stworzymy przykładowy DataFrame reprezentujący dane dotyczące sprzedaży online fikcyjnej międzynarodowej firmy e-commerce.
import pandas as pd
import numpy as np
data = {
'OrderID': [1001, 1002, 1003, 1004, 1005, 1006, 1007, 1008],
'Product': ['Laptop', 'Mouse', 'Keyboard', 'Monitor', 'Webcam', 'Headphones', 'Docking Station', 'Mouse'],
'Category': ['Electronics', 'Accessories', 'Accessories', 'Electronics', 'Accessories', 'Audio', 'Electronics', 'Accessories'],
'Price_USD': [1200, 25, 75, 300, 50, 150, 250, 30],
'Quantity': [1, 2, 1, 2, 1, 1, 1, 3],
'Country': ['USA', 'Canada', 'USA', 'Germany', 'Japan', 'Canada', 'Germany', np.nan]
}
df = pd.DataFrame(data)
print(df)
Ten DataFrame zapewnia nam dobrą mieszankę typów danych (numerycznych, tekstowych, a nawet brakującą wartość), aby zademonstrować pełne możliwości naszych docelowych funkcji.
Metoda `map()`: Transformacja element po elemencie dla Series
Czym jest `map()`?
Metoda map() to Twoje specjalistyczne narzędzie do modyfikowania wartości w pojedynczej kolumnie (Pandas Series). Działa na zasadzie element po elemencie. Pomyśl o tym, jak o stwierdzeniu: "Dla każdego elementu w tej kolumnie, wyszukaj go w słowniku lub przepuść przez tę funkcję i zastąp go wynikiem."
Jest używana głównie do dwóch zadań:
- Podstawiania wartości na podstawie słownika (mapowania).
- Stosowania prostej funkcji do każdego elementu.
Przypadek użycia 1: Mapowanie wartości za pomocą słownika
Jest to najczęstsze i najbardziej efektywne zastosowanie map(). Wyobraź sobie, że chcemy stworzyć szerszą 'Department' (Dział) kolumnę na podstawie naszej kolumny 'Category' (Kategoria). Możemy zdefiniować mapowanie w słowniku Pythona i użyć map(), aby je zastosować.
category_to_department = {
'Electronics': 'Technology',
'Accessories': 'Peripherals',
'Audio': 'Technology'
}
df['Department'] = df['Category'].map(category_to_department)
print(df[['Category', 'Department']])
Wynik:
Category Department
0 Electronics Technology
1 Accessories Peripherals
2 Accessories Peripherals
3 Electronics Technology
4 Accessories Peripherals
5 Audio Technology
6 Electronics Technology
7 Accessories Peripherals
Zauważ, jak elegancko to działa. Każda wartość w Series 'Category' jest wyszukiwana w słowniku `category_to_department`, a odpowiadająca jej wartość jest używana do wypełnienia nowej kolumny 'Department'. Jeśli klucz nie zostanie znaleziony w słowniku, map() wygeneruje wartość NaN (Not a Number), co jest często pożądanym zachowaniem dla niezmapowanych kategorii.
Przypadek użycia 2: Stosowanie funkcji z `map()`
Możesz również przekazać funkcję (w tym funkcję lambda) do map(). Funkcja zostanie wykonana dla każdego elementu w Series. Stwórzmy nową kolumnę, która poda nam opisową etykietę dla ceny.
def price_label(price):
if price > 200:
return 'High-Value'
elif price > 50:
return 'Mid-Value'
else:
return 'Low-Value'
df['Price_Label'] = df['Price_USD'].map(price_label)
# Using a lambda function for a simpler task:
# df['Product_Length'] = df['Product'].map(lambda x: len(x))
print(df[['Product', 'Price_USD', 'Price_Label']])
Wynik:
Product Price_USD Price_Label
0 Laptop 1200 High-Value
1 Mouse 25 Low-Value
2 Keyboard 75 Mid-Value
3 Monitor 300 High-Value
4 Webcam 50 Low-Value
5 Headphones 150 Mid-Value
6 Docking Station 250 High-Value
7 Mouse 30 Low-Value
Kiedy używać `map()`: Krótkie podsumowanie
- Pracujesz na pojedynczej kolumnie (Series).
- Musisz podstawić wartości na podstawie słownika lub innej Series. To jest jego główna siła.
- Musisz zastosować prostą funkcję element po elemencie do pojedynczej kolumny.
Metoda `apply()`: Wszechstronny Koń Roboczy
Czym jest `apply()`?
Jeśli map() jest specjalistą, apply() jest wszechstronną potęgą. Jest bardziej elastyczna, ponieważ może działać zarówno na Series, jak i na DataFrames. Kluczem do zrozumienia apply() jest parametr axis, który kieruje jej działaniem:
- Na Series: Działa element po elemencie, bardzo podobnie do
map(). - Na DataFrame z
axis=0(domyślnie): Stosuje funkcję do każdej kolumny. Funkcja otrzymuje każdą kolumnę jako Series. - Na DataFrame z
axis=1: Stosuje funkcję do każdego wiersza. Funkcja otrzymuje każdy wiersz jako Series.
`apply()` na Series
Użyta na Series, apply() zachowuje się bardzo podobnie do map(). Stosuje funkcję do każdego elementu. Na przykład, moglibyśmy powtórzyć nasz przykład etykiety ceny.
df['Price_Label_apply'] = df['Price_USD'].apply(price_label)
print(df['Price_Label_apply'].equals(df['Price_Label'])) # Output: True
Chociaż tutaj wydają się być zamienne, map() jest często nieco szybsza dla prostych podstawień słownikowych i operacji element po elemencie na Series, ponieważ ma bardziej zoptymalizowaną ścieżkę dla tych konkretnych zadań.
`apply()` na DataFrame (kolumnowo, `axis=0`)
Jest to domyślny tryb dla DataFrame. Funkcja, którą dostarczysz, jest wywoływana raz dla każdej kolumny. Jest to przydatne do agregacji lub transformacji kolumnowych.
Znajdźmy różnicę między wartością maksymalną a minimalną (zakres) dla każdej z naszych kolumn numerycznych.
numeric_cols = df[['Price_USD', 'Quantity']]
def get_range(column_series):
return column_series.max() - column_series.min()
column_ranges = numeric_cols.apply(get_range, axis=0)
print(column_ranges)
Wynik:
Price_USD 1175.0
Quantity 2.0
dtype: float64
Tutaj funkcja get_range najpierw otrzymała Series 'Price_USD', obliczyła jej zakres, następnie otrzymała Series 'Quantity' i zrobiła to samo, zwracając nową Series z wynikami.
`apply()` na DataFrame (wierszowo, `axis=1`)
Jest to prawdopodobnie najpotężniejszy i najczęstszy przypadek użycia apply(). Gdy musisz obliczyć nową wartość na podstawie wielu kolumn w tym samym wierszu, apply() z axis=1 jest Twoim podstawowym rozwiązaniem.
Funkcja, którą przekażesz, otrzyma każdy wiersz jako Series, gdzie indeksami są nazwy kolumn. Obliczmy całkowity koszt dla każdego zamówienia.
def calculate_total_cost(row):
# 'row' is a Series representing a single row
price = row['Price_USD']
quantity = row['Quantity']
return price * quantity
df['Total_Cost'] = df.apply(calculate_total_cost, axis=1)
print(df[['Product', 'Price_USD', 'Quantity', 'Total_Cost']])
Wynik:
Product Price_USD Quantity Total_Cost
0 Laptop 1200 1 1200
1 Mouse 25 2 50
2 Keyboard 75 1 75
3 Monitor 300 2 600
4 Webcam 50 1 50
5 Headphones 150 1 150
6 Docking Station 250 1 250
7 Mouse 30 3 90
To jest coś, czego map() po prostu nie może zrobić, ponieważ jest ograniczona do jednej kolumny. Zobaczmy bardziej złożony przykład. Chcemy skategoryzować priorytet wysyłki każdego zamówienia na podstawie jego kategorii i kraju.
def assign_shipping_priority(row):
if row['Category'] == 'Electronics' and row['Country'] == 'USA':
return 'High Priority'
elif row['Total_Cost'] > 500:
return 'High Priority'
elif row['Country'] == 'Japan':
return 'Medium Priority'
else:
return 'Standard'
df['Shipping_Priority'] = df.apply(assign_shipping_priority, axis=1)
print(df[['Category', 'Country', 'Total_Cost', 'Shipping_Priority']])
Kiedy używać `apply()`: Krótkie podsumowanie
- Gdy Twoja logika zależy od wielu kolumn w wierszu (użyj
axis=1). To jest jego kluczowa cecha. - Gdy musisz zastosować funkcję agregującą w dół kolumn lub w poprzek wierszy.
- Jako narzędzie do ogólnego stosowania funkcji, gdy
map()nie pasuje.
Specjalna wzmianka: Metoda `applymap()`
Czym jest `applymap()`?
Metoda applymap() to kolejny specjalista, ale jej domeną jest cały DataFrame. Stosuje funkcję do każdego pojedynczego elementu DataFrame. Nie działa na Series – to metoda wyłącznie dla DataFrame.
Pomyśl o tym jak o uruchamianiu map() na każdej kolumnie jednocześnie. Jest przydatna do szerokich, kompleksowych transformacji, takich jak formatowanie lub konwersja typów, we wszystkich komórkach.
DataFrame.applymap() jest oznaczana jako przestarzała. Nowym zalecanym sposobem jest użycie DataFrame.map(). Funkcjonalność jest taka sama. Będziemy używać applymap() tutaj dla kompatybilności, ale bądź świadomy tej zmiany dla przyszłego kodu.
Praktyczny przykład
Powiedzmy, że mamy sub-DataFrame tylko z naszymi kolumnami numerycznymi i chcemy sformatować je wszystkie jako ciągi walutowe dla raportu.
numeric_df = df[['Price_USD', 'Quantity', 'Total_Cost']]
# Using a lambda function to format each number
formatted_df = numeric_df.applymap(lambda x: f'${x:,.2f}')
print(formatted_df)
Wynik:
Price_USD Quantity Total_Cost
0 $1,200.00 $1.00 $1,200.00
1 $25.00 $2.00 $50.00
2 $75.00 $1.00 $75.00
3 $300.00 $2.00 $600.00
4 $50.00 $1.00 $50.00
5 $150.00 $1.00 $150.00
6 $250.00 $1.00 $250.00
7 $30.00 $3.00 $90.00
Innym częstym zastosowaniem jest czyszczenie DataFrame z danych tekstowych, na przykład poprzez konwersję wszystkiego na małe litery.
string_df = df[['Product', 'Category', 'Country']].copy() # Create a copy to avoid SettingWithCopyWarning
# Ensure all values are strings to prevent errors
string_df = string_df.astype(str)
lower_df = string_df.applymap(str.lower)
print(lower_df)
Kiedy używać `applymap()`: Krótkie podsumowanie
- Gdy musisz zastosować jedną, prostą funkcję do każdego elementu w DataFrame.
- Do zadań takich jak konwersja typów danych, formatowanie ciągów znaków lub proste transformacje matematyczne w całym DataFrame.
- Pamiętaj o jego deprecjacji na rzecz
DataFrame.map()w nowszych wersjach Pandas.
Głębsze spojrzenie na wydajność: Wektoryzacja kontra iteracja
"Ukryta" pętla
To jest najważniejsza koncepcja do zrozumienia, jeśli chodzi o pisanie wysokowydajnego kodu Pandas. Chociaż apply(), map() i applymap() są wygodne, w istocie są tylko fantazyjnymi opakowaniami wokół pętli Pythona. Kiedy używasz df.apply(..., axis=1), Pandas iteruje przez Twój DataFrame wiersz po wierszu, przekazując każdy z nich do Twojej funkcji. Ten proces wiąże się ze znacznym narzutem i jest znacznie wolniejszy niż operacje zoptymalizowane w C lub Cythonie.
Siła wektoryzacji
Wektoryzacja to praktyka wykonywania operacji na całych tablicach (lub Series) jednocześnie, zamiast na pojedynczych elementach. Pandas i jego podstawowa biblioteka, NumPy, są specjalnie zaprojektowane, aby być niezwykle szybkimi w operacjach wektoryzowanych.
Wróćmy do naszego obliczenia 'Total_Cost'. Użyliśmy apply(), ale czy istnieje sposób wektoryzowany?
# Method 1: Using apply() (Iteration)
df['Total_Cost'] = df.apply(lambda row: row['Price_USD'] * row['Quantity'], axis=1)
# Method 2: Vectorized Operation
df['Total_Cost_Vect'] = df['Price_USD'] * df['Quantity']
# Check if the results are the same
print(df['Total_Cost'].equals(df['Total_Cost_Vect'])) # Output: True
Druga metoda jest wektoryzowana. Bierze całą Series 'Price_USD' i mnoży ją przez całą Series 'Quantity' w jednej, wysoko zoptymalizowanej operacji. Gdybyś zmierzył czas tych dwóch metod na dużym DataFrame (miliony wierszy), podejście wektoryzowane byłoby nie tylko szybsze – byłoby rzędami wielkości szybsze. Mówimy o sekundach kontra minuty, lub minutach kontra godziny.
Kiedy `apply()` jest nieuniknione?
Skoro wektoryzacja jest tak znacznie szybsza, to dlaczego istnieją te inne metody? Ponieważ czasami Twoja logika jest zbyt złożona, aby ją zwektoryzować. apply() jest niezbędnym i prawidłowym narzędziem, gdy:
- Złożona logika warunkowa: Twoja logika obejmuje skomplikowane instrukcje `if/elif/else`, które zależą od wielu kolumn, jak w naszym przykładzie `assign_shipping_priority`. Chociaż część tego można osiągnąć za pomocą `np.select()`, może to stać się nieczytelne.
- Funkcje bibliotek zewnętrznych: Musisz zastosować funkcję z biblioteki zewnętrznej do swoich danych. Na przykład, zastosowanie funkcji z biblioteki geoprzestrzennej do obliczenia odległości na podstawie kolumn szerokości i długości geograficznej, lub funkcji z biblioteki przetwarzania języka naturalnego (takiej jak NLTK) do przeprowadzenia analizy sentymentu na kolumnie tekstowej.
- Procesy iteracyjne: Obliczenie dla danego wiersza zależy od wartości obliczonej w poprzednim wierszu (chociaż jest to rzadkie i często jest sygnałem, że potrzebna jest inna struktura danych).
Najlepsza praktyka: Najpierw wektoryzacja, potem `apply()`
Prowadzi to do złotej zasady wydajności Pandas:
Zawsze najpierw szukaj rozwiązania wektoryzowanego. Używaj `apply()` jako potężnego, elastycznego rozwiązania awaryjnego, gdy wektoryzowane rozwiązanie nie jest praktyczne lub możliwe.
Podsumowanie i kluczowe wnioski: Wybór odpowiedniego narzędzia
Skonsolidujmy naszą wiedzę w przejrzyste ramy decyzyjne. Kiedy stajesz przed zadaniem niestandardowej transformacji, zadaj sobie te pytania:
Tabela porównawcza
| Metoda | Działa na | Zakres działania | Funkcja otrzymuje | Główny przypadek użycia |
|---|---|---|---|---|
| Wektoryzacja | Series, DataFrame | Cała tablica jednocześnie | N/A (operacja jest bezpośrednia) | Operacje arytmetyczne, logiczne. Najwyższa wydajność. |
.map() |
Tylko Series | Element po elemencie | Pojedynczy element | Podstawianie wartości ze słownika. |
.apply() |
Series, DataFrame | Wiersz po wierszu lub kolumna po kolumnie | Series (wiersz lub kolumna) | Złożona logika wykorzystująca wiele kolumn w wierszu. |
.applymap() |
Tylko DataFrame | Element po elemencie | Pojedynczy element | Formatowanie lub transformowanie każdej komórki w DataFrame. |
Schemat podejmowania decyzji
- Czy moja operacja może być wyrażona za pomocą podstawowych operatorów arytmetycznych (+, -, *, /) lub logicznych (&, |, ~) na całych kolumnach?
→ Tak? Użyj podejścia wektoryzowanego. To jest najszybsze. (np. `df['col1'] * df['col2']`) - Czy pracuję tylko na jednej kolumnie, a moim głównym celem jest podstawienie wartości na podstawie słownika?
→ Tak? UżyjSeries.map(). Jest to zoptymalizowane do tego celu. - Czy muszę zastosować funkcję do każdego pojedynczego elementu w całym moim DataFrame?
→ Tak? UżyjDataFrame.applymap()(lubDataFrame.map()w nowszych wersjach Pandas). - Czy moja logika jest złożona i wymaga wartości z wielu kolumn w każdym wierszu do obliczenia pojedynczego wyniku?
→ Tak? UżyjDataFrame.apply(..., axis=1). To jest Twoje narzędzie do złożonej logiki wierszowej.
Wnioski
Nawigacja po opcjach stosowania funkcji niestandardowych w Pandas to rytuał przejścia dla każdego praktyka danych. Chociaż na pierwszy rzut oka mogą wydawać się wymienne, map(), apply() i applymap() to odrębne narzędzia, każde z własnymi mocnymi stronami i idealnymi przypadkami użycia. Rozumiejąc ich różnice, możesz pisać kod, który jest nie tylko poprawny, ale także bardziej czytelny, łatwiejszy w utrzymaniu i znacznie wydajniejszy.
Pamiętaj o hierarchii: preferuj wektoryzację ze względu na jej surową szybkość, używaj map() do efektywnego podstawiania Series, wybierz applymap() do transformacji obejmujących cały DataFrame, a także wykorzystaj moc i elastyczność apply() do złożonej logiki wierszowej lub kolumnowej, której nie można zwektoryzować. Uzbrojony w tę wiedzę, jesteś teraz lepiej przygotowany do sprostania każdemu wyzwaniu manipulacji danymi, przekształcając surowe dane w potężne wnioski z umiejętnością i wydajnością.