Zastosować funkcję pandy do kolumny, aby utworzyć wiele nowych kolumn?

215

Jak to zrobić w pandach:

Mam funkcję extract_text_featuresw jednej kolumnie tekstowej, zwracającą wiele kolumn wyjściowych. W szczególności funkcja zwraca 6 wartości.

Funkcja działa, jednak wydaje się, że nie ma żadnego poprawnego typu zwracanego (pandy DataFrame / numpy array / Python list), aby można było poprawnie przypisać dane wyjściowe df.ix[: ,10:16] = df.textcol.map(extract_text_features)

Więc myślę, że muszę wrócić do iteracji df.iterrows(), zgodnie z tym ?

AKTUALIZACJA: Iteracja z df.iterrows()jest co najmniej 20x wolniejsza, więc poddałem się i podzieliłem funkcję na sześć różnych .map(lambda ...)wywołań.

AKTUALIZACJA 2: to pytanie zostało zadane około v0.11.0 . Dlatego też większość pytań i odpowiedzi nie jest zbyt trafna.

smci
źródło
1
Nie sądzę, że można to zrobić Multiple cesji sposób masz napisane: df.ix[: ,10:16]. Myślę, że będziesz musiał do mergeswoich funkcji do zestawu danych.
Zelazny7
1
Dla tych, którzy chcą znacznie bardziej wydajnego rozwiązania, sprawdź to poniżej, które nie używaapply
Ted Petrou
Większość operacji numerycznych z pandami można wektoryzować - oznacza to, że są one znacznie szybsze niż konwencjonalna iteracja. OTOH, niektóre operacje (takie jak string i regex) są z natury trudne do wektoryzacji. W tym przypadku ważne jest, aby zrozumieć, jak zapętlić dane. Więcej informacji na temat tego, kiedy i jak należy zapętlać dane, przeczytaj artykuł Pętle z Pandami - kiedy powinno mnie to obchodzić? .
cs95
@coldspeed: głównym problemem nie było wybranie, która z kilku opcji była lepsza, walczyła ze składnią pand, aby w ogóle działała, około v0.11.0 .
smci
Rzeczywiście, komentarz jest przeznaczony dla przyszłych czytelników, którzy szukają iteracyjnych rozwiązań, którzy albo nie wiedzą nic lepszego, albo wiedzą, co robią.
cs95

Odpowiedzi:

109

Opierając się na odpowiedzi użytkownika 1827356, możesz wykonać zadanie w jednym przebiegu, używając df.merge:

df.merge(df.textcol.apply(lambda s: pd.Series({'feature1':s+1, 'feature2':s-1})), 
    left_index=True, right_index=True)

    textcol  feature1  feature2
0  0.772692  1.772692 -0.227308
1  0.857210  1.857210 -0.142790
2  0.065639  1.065639 -0.934361
3  0.819160  1.819160 -0.180840
4  0.088212  1.088212 -0.911788

EDYCJA: Należy pamiętać o ogromnym zużyciu pamięci i niskiej prędkości: https://ys-l.github.io/posts/2015/08/28/how-not-to-use-pandas-apply/ !

Zelazny7
źródło
2
tylko z ciekawości, czy w ten sposób zużyjesz dużo pamięci? Robię to na ramce danych, która zawiera 2,5 mil wierszy, i prawie wpadłem na problemy z pamięcią (również jest to znacznie wolniejsze niż zwracanie tylko 1 kolumny).
Jeffrey04,
2
'df.join (df.textcol.apply (lambda s: pd.Series ({' feature1 ': s + 1,' feature2 ': s-1})))' byłoby lepszym rozwiązaniem.
Shivam K. Thakkar
@ShivamKThakkar, dlaczego uważasz, że twoja sugestia byłaby lepszym rozwiązaniem? Czy myślicie, że byłoby bardziej wydajne lub miałoby mniejszy koszt pamięci?
tsando
1
Proszę wziąć pod uwagę szybkość i wymaganą pamięć: ys-l.github.io/posts/2015/08/28/how-not-to-use-pandas-apply
Make42
190

Zwykle robię to za pomocą zip:

>>> df = pd.DataFrame([[i] for i in range(10)], columns=['num'])
>>> df
    num
0    0
1    1
2    2
3    3
4    4
5    5
6    6
7    7
8    8
9    9

>>> def powers(x):
>>>     return x, x**2, x**3, x**4, x**5, x**6

>>> df['p1'], df['p2'], df['p3'], df['p4'], df['p5'], df['p6'] = \
>>>     zip(*df['num'].map(powers))

>>> df
        num     p1      p2      p3      p4      p5      p6
0       0       0       0       0       0       0       0
1       1       1       1       1       1       1       1
2       2       2       4       8       16      32      64
3       3       3       9       27      81      243     729
4       4       4       16      64      256     1024    4096
5       5       5       25      125     625     3125    15625
6       6       6       36      216     1296    7776    46656
7       7       7       49      343     2401    16807   117649
8       8       8       64      512     4096    32768   262144
9       9       9       81      729     6561    59049   531441
ostrokach
źródło
8
Ale co zrobisz, jeśli dodasz 50 kolumn w ten sposób, a nie 6?
maks.
14
@maxtemp = list(zip(*df['num'].map(powers))); for i, c in enumerate(columns): df[c] = temp[c]
ostrokach
8
@ostrokach Myślę, że miałeś na myśli for i, c in enumerate(columns): df[c] = temp[i]. Dzięki temu naprawdę mam cel enumerate: D
rocarvaj
4
To zdecydowanie najbardziej eleganckie i czytelne rozwiązanie. Jeśli nie występują problemy z wydajnością, idiom zip(*df['col'].map(function))jest prawdopodobnie dobrym rozwiązaniem.
François Leblanc
1
@XiaoyuLu Zobacz stackoverflow.com/questions/3394835/args-and-kwargs
ostrokach 18.10.2018
84

Tak robiłem w przeszłości

df = pd.DataFrame({'textcol' : np.random.rand(5)})

df
    textcol
0  0.626524
1  0.119967
2  0.803650
3  0.100880
4  0.017859

df.textcol.apply(lambda s: pd.Series({'feature1':s+1, 'feature2':s-1}))
   feature1  feature2
0  1.626524 -0.373476
1  1.119967 -0.880033
2  1.803650 -0.196350
3  1.100880 -0.899120
4  1.017859 -0.982141

Edycja dla kompletności

pd.concat([df, df.textcol.apply(lambda s: pd.Series({'feature1':s+1, 'feature2':s-1}))], axis=1)
    textcol feature1  feature2
0  0.626524 1.626524 -0.373476
1  0.119967 1.119967 -0.880033
2  0.803650 1.803650 -0.196350
3  0.100880 1.100880 -0.899120
4  0.017859 1.017859 -0.982141
użytkownik1827356
źródło
concat () wygląda na prostsze niż merge () do połączenia nowych kols z oryginalną ramką danych.
kminek
2
ładna odpowiedź, nie musisz używać dyktowania ani scalania, jeśli podasz kolumny poza aplikacjądf[['col1', 'col2']] = df['col3'].apply(lambda x: pd.Series('val1', 'val2'))
Matt
66

Jest to poprawny i najłatwiejszy sposób na osiągnięcie tego w 95% przypadków użycia:

>>> df = pd.DataFrame(zip(*[range(10)]), columns=['num'])
>>> df
    num
0    0
1    1
2    2
3    3
4    4
5    5

>>> def example(x):
...     x['p1'] = x['num']**2
...     x['p2'] = x['num']**3
...     x['p3'] = x['num']**4
...     return x

>>> df = df.apply(example, axis=1)
>>> df
    num  p1  p2  p3
0    0   0   0    0
1    1   1   1    1
2    2   4   8   16
3    3   9  27   81
4    4  16  64  256
Michael David Watson
źródło
nie powinieneś pisać: df = df.apply (przykład (df), axis = 1) popraw mnie, jeśli się mylę, jestem tylko nowicjuszem
użytkownik299791
1
@ user299791, Nie, w tym przypadku traktujesz przykład jako obiekt pierwszej klasy, więc przekazujesz samą funkcję. Ta funkcja zostanie zastosowana do każdego wiersza.
Michael David Watson,
cześć Michael, twoja odpowiedź pomogła mi w moim problemie. Zdecydowanie twoje rozwiązanie jest lepsze niż metoda df.assign () oryginalnej pandy, ponieważ jest to jeden raz na kolumnę. Korzystając z funkcji assign (), jeśli chcesz utworzyć 2 nowe kolumny, musisz użyć df1 do pracy na df, aby uzyskać nową kolumnę1, a następnie użyj df2 do pracy na df1, aby utworzyć drugą nową kolumnę ... jest to dość monotonne. Ale twoja metoda uratowała mi życie !!! Dzięki!!!
commentallez-vous
1
Czy to nie uruchamia kodu przypisania kolumny raz na wiersz? Czy nie byłoby lepiej zwrócić pd.Series({k:v})i serializować przypisanie kolumny jak w odpowiedzi Ewana?
Denis de Bernardy
Jeśli to pomaga komukolwiek, a to podejście jest poprawne, a także najprostsze ze wszystkich przedstawionych rozwiązań, bezpośrednie aktualizowanie wiersza w ten sposób okazało się zaskakująco wolne - o rząd wielkości wolniejsze niż zastosowanie z rozwiązaniami „expand” + pd.concat
Dmytro Bugayev
31

W 2018 używam apply()z argumentamiresult_type='expand'

>>> appiled_df = df.apply(lambda row: fn(row.text), axis='columns', result_type='expand')
>>> df = pd.concat([df, appiled_df], axis='columns')
Ben
źródło
6
Tak właśnie to robicie w dzisiejszych czasach!
Make42
1
To zadziałało po wyjęciu z pudełka w 2020 r., Podczas gdy wiele innych pytań nie. Nie działa również, pd.Series co zawsze jest miłe z
punktu widzenia
1
To dobre rozwiązanie. Jedynym problemem jest to, że nie można wybrać nazwy dla 2 nowo dodanych kolumn. Musisz później zrobić df.rename (kolumny = {0: 'col1', 1: 'col2'})
pedram bashiri
2
@pedrambashiri Jeśli funkcja, którą przekazujesz, df.applyzwraca a dict, wyjdą kolumny o nazwach zgodnych z kluczami.
Seb
25

Po prostu użyj result_type="expand"

df = pd.DataFrame(np.random.randint(0,10,(10,2)), columns=["random", "a"])
df[["sq_a","cube_a"]] = df.apply(lambda x: [x.a**2, x.a**3], axis=1, result_type="expand")
Abhishek
źródło
4
Pomaga wskazać, że opcja jest nowa w 0.23 . Pytanie zadano ponownie w dniu 0.11
smci
Fajnie, to jest proste i nadal działa dobrze. Tego szukałem. Dzięki
Izaak Sim
Duplikuje wcześniejszą odpowiedź: stackoverflow.com/a/52363890/823470
tar
22

Podsumowanie: Jeśli chcesz utworzyć tylko kilka kolumn, użyjdf[['new_col1','new_col2']] = df[['data1','data2']].apply( function_of_your_choosing(x), axis=1)

W tym rozwiązaniu liczba tworzonych nowych kolumn musi być równa liczbie kolumn używanych jako dane wejściowe do funkcji .apply (). Jeśli chcesz zrobić coś innego, spójrz na inne odpowiedzi.

Detale Załóżmy, że masz dwukolumnową ramkę danych. Pierwsza kolumna to wzrost osoby, która ma 10 lat; drugi to wzrost osoby, która ma 20 lat.

Załóżmy, że musisz obliczyć zarówno średnią wysokość każdej osoby, jak i sumę wysokości każdej osoby. To dwie wartości na każdy wiersz.

Możesz to zrobić za pomocą następującej funkcji, która zostanie wkrótce zastosowana:

def mean_and_sum(x):
    """
    Calculates the mean and sum of two heights.
    Parameters:
    :x -- the values in the row this function is applied to. Could also work on a list or a tuple.
    """

    sum=x[0]+x[1]
    mean=sum/2
    return [mean,sum]

Możesz użyć tej funkcji w następujący sposób:

 df[['height_at_age_10','height_at_age_20']].apply(mean_and_sum(x),axis=1)

(Dla jasności: ta funkcja wprowadzania pobiera wartości z każdego wiersza w podzestawowej ramce danych i zwraca listę.)

Jeśli jednak to zrobisz:

df['Mean_&_Sum'] = df[['height_at_age_10','height_at_age_20']].apply(mean_and_sum(x),axis=1)

utworzysz 1 nową kolumnę, która zawiera listy [średnia, suma], których prawdopodobnie chcesz uniknąć, ponieważ wymagałoby to kolejnej Lambda / Apply.

Zamiast tego chcesz rozbić każdą wartość na osobną kolumnę. Aby to zrobić, możesz utworzyć dwie kolumny jednocześnie:

df[['Mean','Sum']] = df[['height_at_age_10','height_at_age_20']]
.apply(mean_and_sum(x),axis=1)
Evan W.
źródło
4
W przypadku pand 0.23 musisz użyć składni:df["mean"], df["sum"] = df[['height_at_age_10','height_at_age_20']] .apply(mean_and_sum(x),axis=1)
SummerEla
Ta funkcja może zgłaszać błąd. Funkcja powrotu musi być return pd.Series([mean,sum])
Kanishk Mair
22

Dla mnie to zadziałało:

Wpisz df

df = pd.DataFrame({'col x': [1,2,3]})
   col x
0      1
1      2
2      3

Funkcjonować

def f(x):
    return pd.Series([x*x, x*x*x])

Utwórz 2 nowe kolumny:

df[['square x', 'cube x']] = df['col x'].apply(f)

Wynik:

   col x  square x  cube x
0      1         1       1
1      2         4       8
2      3         9      27
Joe
źródło
13

Szukałem kilku sposobów na zrobienie tego, a pokazana tutaj metoda (zwracanie serii pand) nie wydaje się być najbardziej wydajna.

Jeśli zaczniemy od dużej ramki danych losowych:

# Setup a dataframe of random numbers and create a 
df = pd.DataFrame(np.random.randn(10000,3),columns=list('ABC'))
df['D'] = df.apply(lambda r: ':'.join(map(str, (r.A, r.B, r.C))), axis=1)
columns = 'new_a', 'new_b', 'new_c'

Przykład pokazany tutaj:

# Create the dataframe by returning a series
def method_b(v):
    return pd.Series({k: v for k, v in zip(columns, v.split(':'))})
%timeit -n10 -r3 df.D.apply(method_b)

10 pętli, najlepiej 3: 2,77 s na pętlę

Alternatywna metoda:

# Create a dataframe from a series of tuples
def method_a(v):
    return v.split(':')
%timeit -n10 -r3 pd.DataFrame(df.D.apply(method_a).tolist(), columns=columns)

10 pętli, najlepiej 3: 8,85 ms na pętlę

Według moich obliczeń, o wiele bardziej wydajne jest pobranie serii krotek, a następnie przekonwertowanie ich na ramkę danych. Chciałbym usłyszeć, jak ludzie myślą, jeśli w mojej pracy wystąpi błąd.

RFox
źródło
To jest naprawdę przydatne! Mam 30-krotne przyspieszenie w porównaniu z metodami zwracającymi funkcje.
Pushkar Nimkar
9

Przyjęte rozwiązanie będzie bardzo powolne dla wielu danych. Rozwiązanie z największą liczbą głosów upvotes jest trochę trudne do odczytania, a także powolne w przypadku danych liczbowych. Jeśli każdą nową kolumnę można obliczyć niezależnie od innych, po prostu przypisałbym każdą z nich bezpośrednio, bez użycia apply.

Przykład z fałszywymi danymi postaci

Utwórz 100 000 ciągów w ramce danych

df = pd.DataFrame(np.random.choice(['he jumped', 'she ran', 'they hiked'],
                                   size=100000, replace=True),
                  columns=['words'])
df.head()
        words
0     she ran
1     she ran
2  they hiked
3  they hiked
4  they hiked

Powiedzmy, że chcieliśmy wyodrębnić niektóre funkcje tekstu, tak jak w pierwotnym pytaniu. Na przykład wyodrębnijmy pierwszy znak, policzmy występowanie litery „e” i wielką frazę.

df['first'] = df['words'].str[0]
df['count_e'] = df['words'].str.count('e')
df['cap'] = df['words'].str.capitalize()
df.head()
        words first  count_e         cap
0     she ran     s        1     She ran
1     she ran     s        1     She ran
2  they hiked     t        2  They hiked
3  they hiked     t        2  They hiked
4  they hiked     t        2  They hiked

Czasy

%%timeit
df['first'] = df['words'].str[0]
df['count_e'] = df['words'].str.count('e')
df['cap'] = df['words'].str.capitalize()
127 ms ± 585 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

def extract_text_features(x):
    return x[0], x.count('e'), x.capitalize()

%timeit df['first'], df['count_e'], df['cap'] = zip(*df['words'].apply(extract_text_features))
101 ms ± 2.96 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

Zaskakujące jest to, że można uzyskać lepszą wydajność, zapętlając każdą wartość

%%timeit
a,b,c = [], [], []
for s in df['words']:
    a.append(s[0]), b.append(s.count('e')), c.append(s.capitalize())

df['first'] = a
df['count_e'] = b
df['cap'] = c
79.1 ms ± 294 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

Kolejny przykład z fałszywymi danymi liczbowymi

Utwórz 1 milion liczb losowych i przetestuj powersfunkcję z góry.

df = pd.DataFrame(np.random.rand(1000000), columns=['num'])


def powers(x):
    return x, x**2, x**3, x**4, x**5, x**6

%%timeit
df['p1'], df['p2'], df['p3'], df['p4'], df['p5'], df['p6'] = \
       zip(*df['num'].map(powers))
1.35 s ± 83.6 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

Przypisanie każdej kolumny jest 25 razy szybsze i bardzo czytelne:

%%timeit 
df['p1'] = df['num'] ** 1
df['p2'] = df['num'] ** 2
df['p3'] = df['num'] ** 3
df['p4'] = df['num'] ** 4
df['p5'] = df['num'] ** 5
df['p6'] = df['num'] ** 6
51.6 ms ± 1.9 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

Udzieliłem podobnej odpowiedzi, podając więcej szczegółów na temat tego, dlaczego applyzazwyczaj nie jest to dobra droga.

Ted Petrou
źródło
8

Opublikowałem tę samą odpowiedź w dwóch innych podobnych pytaniach. Preferuję to, aby zawijać zwracane wartości funkcji w szeregu:

def f(x):
    return pd.Series([x**2, x**3])

A następnie użyj zastosować w następujący sposób, aby utworzyć osobne kolumny:

df[['x**2','x**3']] = df.apply(lambda row: f(row['x']), axis=1)
Dmytro Bugayev
źródło
1

możesz zwrócić cały wiersz zamiast wartości:

df = df.apply(extract_text_features,axis = 1)

gdzie funkcja zwraca wiersz

def extract_text_features(row):
      row['new_col1'] = value1
      row['new_col2'] = value2
      return row
Saket Bajaj
źródło
Nie, nie chcę stosować się extract_text_featuresdo każdej kolumny df, tylko do kolumny tekstowejdf.textcol
smci
-2
def myfunc(a):
    return a * a

df['New Column'] = df['oldcolumn'].map(myfunc))

To zadziałało dla mnie. Nowa kolumna zostanie utworzona na podstawie przetworzonych danych starych kolumn.

użytkownik2902302
źródło
2
To nie zwraca „wielu nowych kolumn”
pedram bashiri
To nie zwraca „wielu nowych kolumn”, więc nie odpowiada na pytanie. Czy możesz to usunąć?
smci