Jak zastosować funkcję do dwóch kolumn ramki danych Pandas

368

Załóżmy, że mam dfco ma kolumny 'ID', 'col_1', 'col_2'. I definiuję funkcję:

f = lambda x, y : my_function_expression.

Teraz chcę zastosować fdo df„s dwie kolumny 'col_1', 'col_2'do elementu mądry obliczy nową kolumnę 'col_3', trochę jak:

df['col_3'] = df[['col_1','col_2']].apply(f)  
# Pandas gives : TypeError: ('<lambda>() takes exactly 2 arguments (1 given)'

Jak zrobić ?

** Dodaj próbkę szczegółową jak poniżej ***

import pandas as pd

df = pd.DataFrame({'ID':['1','2','3'], 'col_1': [0,2,3], 'col_2':[1,4,5]})
mylist = ['a','b','c','d','e','f']

def get_sublist(sta,end):
    return mylist[sta:end+1]

#df['col_3'] = df[['col_1','col_2']].apply(get_sublist,axis=1)
# expect above to output df as below 

  ID  col_1  col_2            col_3
0  1      0      1       ['a', 'b']
1  2      2      4  ['c', 'd', 'e']
2  3      3      5  ['d', 'e', 'f']
wielki robak
źródło
4
czy możesz zastosować f bezpośrednio do kolumn: df ['col_3'] = f (df ['col_1'], df ['col_2'])
btel
1
przydałoby się wiedzieć, co fsię dzieje
tehmisvh,
2
nie, df ['col_3'] = f (df ['col_1'], df ['col_2']) nie działa. Dla f akceptuje tylko dane skalarne, a nie wektorowe. OK, możesz założyć f = lambda x, y: x + y. (oczywiście, mój prawdziwy f nie jest taki prosty, w przeciwnym razie mogę bezpośrednio df ['col_3'] = df ['col_1'] + df ['col_2'])
bigbug
1
Znalazłem powiązane pytania i odpowiedzi pod poniższym adresem URL, ale moim problemem jest obliczenie nowej kolumny na podstawie dwóch istniejących kolumn, a nie 2 z 1. stackoverflow.com/questions/12356501/…
bigbug
Myślę, że moja odpowiedź stackoverflow.com/a/52854800/5447172 odpowiada na to w najbardziej Pythoński / Pandaniczny sposób, bez obejść ani indeksowania numerycznego. Wytwarza dokładnie dane wyjściowe wymagane w twoim przykładzie.
ajrwhite

Odpowiedzi:

291

Oto przykład użycia applyw ramce danych, z którą dzwonię axis = 1.

Zauważ, że różnica polega na tym, że zamiast próbować przekazać dwie wartości do funkcji f, przepisz funkcję, aby zaakceptować obiekt serii pandy, a następnie indeksuj serię, aby uzyskać potrzebne wartości.

In [49]: df
Out[49]: 
          0         1
0  1.000000  0.000000
1 -0.494375  0.570994
2  1.000000  0.000000
3  1.876360 -0.229738
4  1.000000  0.000000

In [50]: def f(x):    
   ....:  return x[0] + x[1]  
   ....:  

In [51]: df.apply(f, axis=1) #passes a Series object, row-wise
Out[51]: 
0    1.000000
1    0.076619
2    1.000000
3    1.646622
4    1.000000

W zależności od przypadku użycia czasem pomocne jest utworzenie groupobiektu pandy , a następnie użycie applygo w grupie.

Mężczyzna
źródło
Tak, próbowałem użyć Apply, ale nie mogę znaleźć prawidłowego wyrażenia składniowego. A jeśli każdy wiersz df jest unikalny, nadal używasz groupby?
bigbug
Dodałem przykład do mojej odpowiedzi, mam nadzieję, że zrobi to, czego szukasz. Jeśli nie, podaj bardziej szczegółową przykładową funkcję, ponieważ sumzostała pomyślnie rozwiązana za pomocą dowolnej z sugerowanych dotychczas metod.
Aman
1
Czy mógłbyś wkleić swój kod? Przepisuję funkcję: def get_sublist (x): return mylist [x [1]: x [2] + 1] i df ['col_3'] = df.apply (get_sublist, axis = 1) daje 'ValueError: operandy mogą nie będą nadawane razem z kształtami (2) (3) '
bigbug
3
@Aman: w Pandach w wersji 0.14.1 (i prawdopodobnie wcześniejszych), use może również używać wyrażenia lambda. Podaj dfzdefiniowany obiekt, inne podejście (z równoważnymi wynikami) to df.apply(lambda x: x[0] + x[1], axis = 1).
Jubbles,
2
@CanCeylan możesz po prostu użyć nazw kolumn w funkcji zamiast indeksów, wtedy nie musisz się martwić o zmianę kolejności lub uzyskać indeks według nazwy, np. Patrz stackoverflow.com/questions/13021654/…
Davos
165

W Pandach istnieje czysty, jednowierszowy sposób:

df['col_3'] = df.apply(lambda x: f(x.col_1, x.col_2), axis=1)

Pozwala fto być funkcją zdefiniowaną przez użytkownika z wieloma wartościami wejściowymi i używa (bezpiecznych) nazw kolumn zamiast (niebezpiecznych) indeksów numerycznych w celu uzyskania dostępu do kolumn.

Przykład z danymi (na podstawie oryginalnego pytania):

import pandas as pd

df = pd.DataFrame({'ID':['1', '2', '3'], 'col_1': [0, 2, 3], 'col_2':[1, 4, 5]})
mylist = ['a', 'b', 'c', 'd', 'e', 'f']

def get_sublist(sta,end):
    return mylist[sta:end+1]

df['col_3'] = df.apply(lambda x: get_sublist(x.col_1, x.col_2), axis=1)

Wyjście print(df):

  ID  col_1  col_2      col_3
0  1      0      1     [a, b]
1  2      2      4  [c, d, e]
2  3      3      5  [d, e, f]

Jeśli nazwy kolumn zawierają spacje lub mają wspólną nazwę z istniejącym atrybutem ramki danych, możesz indeksować za pomocą nawiasów kwadratowych:

df['col_3'] = df.apply(lambda x: f(x['col 1'], x['col 2']), axis=1)
ajrwhite
źródło
2
Uwaga: jeśli używasz, axis=1a twoja kolumna jest nazywana name, tak naprawdę nie zwróci danych kolumny, ale index. Podobne do uzyskiwania namew groupby(). Rozwiązałem to, zmieniając nazwę mojej kolumny.
Tom Hemmes
2
TO JEST TO! Po prostu nie zdawałem sobie sprawy, że możesz wstawić funkcje lambda z wieloma parametrami wejściowymi do lambdas. Należy zauważyć (myślę), że używasz DF.apply () zamiast Series.apply (). Pozwala to na indeksowanie pliku df przy użyciu dwóch kolumn, które chcesz, i przekazanie całej kolumny do funkcji, ale ponieważ używasz funkcji Apply (), stosuje funkcję w sposób elementarny wzdłuż całej kolumny. Znakomity! Dziękujemy za wysłanie wiadomości!
Data-phile
1
WRESZCIE! Uratowałeś mi dzień!
Mysterio
Myślę, że sugerowanym sposobem jest zrobienie tego df.loc [:, 'new col'] = df.apply .....
valearner
@valearner Nie wydaje mi się, żeby .locw tym przykładzie istniał jakiś powód . Może to być potrzebne, jeśli dostosujesz to do innego ustawienia problemu (np. Praca z plasterkami).
ajrwhite
86

Prostym rozwiązaniem jest:

df['col_3'] = df[['col_1','col_2']].apply(lambda x: f(*x), axis=1)
sjm
źródło
1
czym różni się ta odpowiedź od podejścia w pytaniu: df ['col_3'] = df [['' col_1 ',' col_2 ']]. zastosuj (f) tylko w celu potwierdzenia, podejście w pytaniu nie zadziałało, ponieważ plakat nie określił tej osi = 1, domyślnie oś = 0?
Lost1
1
Ta odpowiedź jest porównywalna z odpowiedzią @ Anman, ale nieco bardziej płynna. Konstruuje anonimową funkcję, która przyjmuje iterowalną i rozpakowuje ją przed przekazaniem jej do funkcji f.
tiao,
39

Ciekawe pytanie! moja odpowiedź jak poniżej:

import pandas as pd

def sublst(row):
    return lst[row['J1']:row['J2']]

df = pd.DataFrame({'ID':['1','2','3'], 'J1': [0,2,3], 'J2':[1,4,5]})
print df
lst = ['a','b','c','d','e','f']

df['J3'] = df.apply(sublst,axis=1)
print df

Wynik:

  ID  J1  J2
0  1   0   1
1  2   2   4
2  3   3   5
  ID  J1  J2      J3
0  1   0   1     [a]
1  2   2   4  [c, d]
2  3   3   5  [d, e]

Zmieniłem nazwę kolumny na ID, J1, J2, J3, aby zapewnić ID <J1 <J2 <J3, więc kolumna wyświetla się we właściwej kolejności.

Jeszcze jedna krótka wersja:

import pandas as pd

df = pd.DataFrame({'ID':['1','2','3'], 'J1': [0,2,3], 'J2':[1,4,5]})
print df
lst = ['a','b','c','d','e','f']

df['J3'] = df.apply(lambda row:lst[row['J1']:row['J2']],axis=1)
print df

źródło
23

Metoda, której szukasz, to Series.combine. Wydaje się jednak, że należy zachować ostrożność przy typach danych. W twoim przykładzie zadzwoniłbyś (tak jak ja podczas testowania odpowiedzi) naiwnie

df['col_3'] = df.col_1.combine(df.col_2, func=get_sublist)

Powoduje to jednak błąd:

ValueError: setting an array element with a sequence.

Domyślam się, że oczekuje, że wynik będzie tego samego typu, co seria wywołująca metodę (tutaj df.col_1). Jednak następujące prace:

df['col_3'] = df.col_1.astype(object).combine(df.col_2, func=get_sublist)

df

   ID   col_1   col_2   col_3
0   1   0   1   [a, b]
1   2   2   4   [c, d, e]
2   3   3   5   [d, e, f]
JoeCondron
źródło
12

Sposób, w jaki napisałeś, wymaga dwóch danych wejściowych. Jeśli spojrzysz na komunikat o błędzie, to znaczy, że nie podajesz dwóch danych wejściowych dla f, tylko jedno. Komunikat o błędzie jest poprawny.
Niedopasowanie jest spowodowane tym, że df [['col1', 'col2']] zwraca pojedynczą ramkę danych z dwiema kolumnami, a nie dwiema osobnymi kolumnami.

Musisz zmienić swój f, aby wymagał pojedynczego wejścia, zachowaj powyższą ramkę danych jako wejście, a następnie podziel ją na x, y wewnątrz ciała funkcji. Następnie zrób wszystko, czego potrzebujesz i zwróć jedną wartość.

Potrzebujesz tej sygnatury funkcji, ponieważ składnia to .apply (f) Więc f musi wziąć jedną rzecz = ramkę danych, a nie dwie rzeczy, czego oczekuje twój obecny f.

Ponieważ nie podałeś jeszcze części f, nie mogę pomóc w szczegółach - ale powinno to dać wyjście bez fundamentalnej zmiany kodu lub użycia innych metod zamiast zastosowania

Nitin
źródło
12

Zagłosuję na np. Wektor. Pozwala strzelać ponad x liczby kolumn i nie zajmować się ramką danych w funkcji, więc świetnie nadaje się do funkcji, których nie kontrolujesz lub nie robisz czegoś takiego jak wysyłanie 2 kolumn i stałej do funkcji (np. Col_1, col_2, 'bla').

import numpy as np
import pandas as pd

df = pd.DataFrame({'ID':['1','2','3'], 'col_1': [0,2,3], 'col_2':[1,4,5]})
mylist = ['a','b','c','d','e','f']

def get_sublist(sta,end):
    return mylist[sta:end+1]

#df['col_3'] = df[['col_1','col_2']].apply(get_sublist,axis=1)
# expect above to output df as below 

df.loc[:,'col_3'] = np.vectorize(get_sublist, otypes=["O"]) (df['col_1'], df['col_2'])


df

ID  col_1   col_2   col_3
0   1   0   1   [a, b]
1   2   2   4   [c, d, e]
2   3   3   5   [d, e, f]
Trae Wallace
źródło
1
To tak naprawdę nie odpowiada na pytanie za pomocą pand.
mnky9800n
18
Pytanie brzmi: „Jak zastosować funkcję do dwóch kolumn ramki danych Pandas”, a nie „Jak zastosować funkcję do dwóch kolumn ramki danych Pandas tylko przy użyciu metod Pandas”, a numpy jest zależnością Pandas, więc trzeba ją mimo wszystko zainstalować, więc wydaje się to dziwnym zastrzeżeniem.
Trae Wallace
12

Zwracanie listy applyjest niebezpieczną operacją, ponieważ nie można zagwarantować, że wynikowy obiekt będzie Serią lub ramką danych. W niektórych przypadkach mogą pojawić się wyjątki. Przejdźmy przez prosty przykład:

df = pd.DataFrame(data=np.random.randint(0, 5, (5,3)),
                  columns=['a', 'b', 'c'])
df
   a  b  c
0  4  0  0
1  2  0  1
2  2  2  2
3  1  2  2
4  3  0  0

Istnieją trzy możliwe wyniki przy zwrocie listy apply

1) Jeśli długość zwracanej listy nie jest równa liczbie kolumn, zwracana jest seria list.

df.apply(lambda x: list(range(2)), axis=1)  # returns a Series
0    [0, 1]
1    [0, 1]
2    [0, 1]
3    [0, 1]
4    [0, 1]
dtype: object

2) Gdy długość zwracanej listy jest równa liczbie kolumn, wówczas zwracana jest ramka danych i każda kolumna otrzymuje odpowiednią wartość z listy.

df.apply(lambda x: list(range(3)), axis=1) # returns a DataFrame
   a  b  c
0  0  1  2
1  0  1  2
2  0  1  2
3  0  1  2
4  0  1  2

3) Jeśli długość zwracanej listy jest równa liczbie kolumn dla pierwszego wiersza, ale ma co najmniej jeden wiersz, w którym lista ma inną liczbę elementów niż liczba kolumn, wywoływany jest błąd ValueError.

i = 0
def f(x):
    global i
    if i == 0:
        i += 1
        return list(range(3))
    return list(range(4))

df.apply(f, axis=1) 
ValueError: Shape of passed values is (5, 4), indices imply (5, 3)

Rozwiązanie problemu bez zastosowania

Korzystanie applyz osi = 1 jest bardzo wolne. Możliwe jest uzyskanie znacznie lepszej wydajności (szczególnie w przypadku większych zestawów danych) za pomocą podstawowych metod iteracyjnych.

Utwórz większą ramkę danych

df1 = df.sample(100000, replace=True).reset_index(drop=True)

Czasy

# apply is slow with axis=1
%timeit df1.apply(lambda x: mylist[x['col_1']: x['col_2']+1], axis=1)
2.59 s ± 76.8 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

# zip - similar to @Thomas
%timeit [mylist[v1:v2+1] for v1, v2 in zip(df1.col_1, df1.col_2)]  
29.5 ms ± 534 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

@Thomas answer

%timeit list(map(get_sublist, df1['col_1'],df1['col_2']))
34 ms ± 459 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
Ted Petrou
źródło
1
Miło jest zobaczyć tak szczegółowe odpowiedzi, z których można się nauczyć.
Andrea Moro
7

Jestem pewien, że nie jest to tak szybkie, jak rozwiązania wykorzystujące operacje Pandas lub Numpy, ale jeśli nie chcesz przepisać swojej funkcji, możesz użyć mapy. Wykorzystując oryginalne przykładowe dane -

import pandas as pd

df = pd.DataFrame({'ID':['1','2','3'], 'col_1': [0,2,3], 'col_2':[1,4,5]})
mylist = ['a','b','c','d','e','f']

def get_sublist(sta,end):
    return mylist[sta:end+1]

df['col_3'] = list(map(get_sublist,df['col_1'],df['col_2']))
#In Python 2 don't convert above to list

W ten sposób moglibyśmy przekazać dowolną liczbę argumentów do funkcji. Wynik jest tym, czego chcieliśmy

ID  col_1  col_2      col_3
0  1      0      1     [a, b]
1  2      2      4  [c, d, e]
2  3      3      5  [d, e, f]
Tomasz
źródło
1
Jest to w rzeczywistości znacznie szybsze odpowiedzi, które używają applyzaxis=1
Ted Petrou
2

Mój przykład na twoje pytania:

def get_sublist(row, col1, col2):
    return mylist[row[col1]:row[col2]+1]
df.apply(get_sublist, axis=1, col1='col_1', col2='col_2')
Qing Liu
źródło
2

Jeśli masz duży zestaw danych, możesz użyć łatwego, ale szybszego (czasu wykonania) sposobu, aby to zrobić za pomocą przełącznika szybszego:

import pandas as pd
import swifter

def fnc(m,x,c):
    return m*x+c

df = pd.DataFrame({"m": [1,2,3,4,5,6], "c": [1,1,1,1,1,1], "x":[5,3,6,2,6,1]})
df["y"] = df.swifter.apply(lambda x: fnc(x.m, x.x, x.c), axis=1)
durjoy
źródło
1

Przypuszczam, że nie chcesz zmieniać get_sublistfunkcji, a po prostu chcesz użyć applymetody DataFrame do wykonania zadania. Aby uzyskać pożądany rezultat, napisałem dwie funkcje pomocy: get_sublist_listi unlist. Jak sugeruje nazwa funkcji, najpierw uzyskaj listę podlisty, a następnie wypakuj podlistę z tej listy. Na koniec musimy wywołać applyfunkcję, aby następnie zastosować te dwie funkcje do df[['col_1','col_2']]DataFrame.

import pandas as pd

df = pd.DataFrame({'ID':['1','2','3'], 'col_1': [0,2,3], 'col_2':[1,4,5]})
mylist = ['a','b','c','d','e','f']

def get_sublist(sta,end):
    return mylist[sta:end+1]

def get_sublist_list(cols):
    return [get_sublist(cols[0],cols[1])]

def unlist(list_of_lists):
    return list_of_lists[0]

df['col_3'] = df[['col_1','col_2']].apply(get_sublist_list,axis=1).apply(unlist)

df

Jeśli nie użyjesz []do załączenia get_sublistfunkcji, get_sublist_listfunkcja zwróci prostą listę, podniesie się ValueError: could not broadcast input array from shape (3) into shape (2), jak wspomniał @Ted Petrou.

allenyllee
źródło