Pandy: utwórz dwie nowe kolumny w ramce danych z wartościami obliczonymi z poprzedniej kolumny

100

Pracuję z biblioteką pandy i chcę dodać dwie nowe kolumny do ramki danych dfz n kolumnami (n> 0).
Te nowe kolumny wynikają z zastosowania funkcji do jednej z kolumn w ramce danych.

Funkcja do zastosowania jest następująca:

def calculate(x):
    ...operate...
    return z, y

Jedną z metod tworzenia nowej kolumny dla funkcji zwracającej tylko wartość jest:

df['new_col']) = df['column_A'].map(a_function)

Więc to, czego chcę i próbowałem bezskutecznie (*), to coś takiego:

(df['new_col_zetas'], df['new_col_ys']) = df['column_A'].map(calculate)

Jaki mógłby być najlepszy sposób na osiągnięcie tego? Zeskanowałem dokumentację bez pojęcia.

** df['column_A'].map(calculate)zwraca serię pand, każdy element składający się z krotki z, y. Próba przypisania tego do dwóch kolumn ramek danych generuje błąd ValueError. *

joaquin
źródło

Odpowiedzi:

119

Po prostu użyłbym zip:

In [1]: from pandas import *

In [2]: def calculate(x):
   ...:     return x*2, x*3
   ...: 

In [3]: df = DataFrame({'a': [1,2,3], 'b': [2,3,4]})

In [4]: df
Out[4]: 
   a  b
0  1  2
1  2  3
2  3  4

In [5]: df["A1"], df["A2"] = zip(*df["a"].map(calculate))

In [6]: df
Out[6]: 
   a  b  A1  A2
0  1  2   2   3
1  2  3   4   6
2  3  4   6   9
DSM
źródło
Dzięki, świetnie, działa. Nie znalazłem nic takiego w dokumentach dla 0.8.1 ... Przypuszczam, że zawsze powinienem myśleć o Seriach jako o listach krotek ...
joaquin
Czy zamiast tego jest jakaś różnica w wydajności? zip (* map (oblicz, df ["a"])) zamiast zip (* df ["a"]. map (oblicz)), co daje również (jak wyżej) [(2, 4, 6), ( 3, 6, 9)]?
ekta
1
Podczas tworzenia nowej kolumny w ten sposób pojawia się następujące ostrzeżenie: „SettingWithCopyWarning: Próba ustawienia wartości na kopii wycinka z DataFrame. Spróbuj użyć .loc [row_indexer, col_indexer] = value.” Powinienem się tym martwić? pandy wer. 0,15
taras
47

Moim zdaniem najlepsza odpowiedź jest błędna. Miejmy nadzieję, że nikt nie importuje masowo wszystkich pand do swojej przestrzeni nazw z from pandas import *. Ponadto mapmetoda powinna być zarezerwowana dla tych czasów, gdy przekazujesz jej słownik lub serię. Może przyjąć funkcję, ale do tego applysłuży.

Jeśli więc musisz zastosować powyższe podejście, napisałbym to w ten sposób

df["A1"], df["A2"] = zip(*df["a"].apply(calculate))

Właściwie nie ma powodu, aby używać tutaj zip. Możesz to po prostu zrobić:

df["A1"], df["A2"] = calculate(df['a'])

Ta druga metoda jest również znacznie szybsza w przypadku większych ramek DataFrame

df = pd.DataFrame({'a': [1,2,3] * 100000, 'b': [2,3,4] * 100000})

DataFrame utworzona z 300 000 wierszy

%timeit df["A1"], df["A2"] = calculate(df['a'])
2.65 ms ± 92.4 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

%timeit df["A1"], df["A2"] = zip(*df["a"].apply(calculate))
159 ms ± 5.24 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

60x szybciej niż zip


Ogólnie rzecz biorąc, unikaj stosowania aplikacji

Stosowanie jest ogólnie niewiele szybsze niż iterowanie po liście w Pythonie. Przetestujmy wydajność pętli for, aby zrobić to samo, co powyżej

%%timeit
A1, A2 = [], []
for val in df['a']:
    A1.append(val**2)
    A2.append(val**3)

df['A1'] = A1
df['A2'] = A2

298 ms ± 7.14 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

Jest to więc dwukrotnie wolniejsze, co nie jest straszną regresją wydajności, ale jeśli zcytonujemy powyższe, otrzymamy znacznie lepszą wydajność. Zakładając, że używasz ipython:

%load_ext cython

%%cython
cpdef power(vals):
    A1, A2 = [], []
    cdef double val
    for val in vals:
        A1.append(val**2)
        A2.append(val**3)

    return A1, A2

%timeit df['A1'], df['A2'] = power(df['a'])
72.7 ms ± 2.16 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

Bezpośrednie przypisywanie bez zastosowania

Możesz uzyskać jeszcze większą poprawę szybkości, jeśli używasz bezpośrednich operacji wektoryzowanych.

%timeit df['A1'], df['A2'] = df['a'] ** 2, df['a'] ** 3
5.13 ms ± 320 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

Wykorzystuje to niezwykle szybkie operacje wektoryzacji NumPy zamiast naszych pętli. Mamy teraz 30-krotne przyspieszenie w stosunku do oryginału.


Najprostszy test szybkości z apply

Powyższy przykład powinien jasno pokazać, jak wolno applymoże być, ale właśnie dlatego jego wyjątkowo jasny, spójrzmy na najbardziej podstawowy przykład. Wyrównajmy do kwadratu serię 10 milionów liczb z zastosowaniem i bez zastosowania

s = pd.Series(np.random.rand(10000000))

%timeit s.apply(calc)
3.3 s ± 57.4 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

Bez aplikacji jest 50x szybsze

%timeit s ** 2
66 ms ± 2 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
Ted Petrou
źródło
1
To naprawdę świetna odpowiedź. Chciałem zapytać: co myślisz o applymapprzypadku, gdy musisz zaimplementować określoną funkcję do każdego elementu ramki danych?
David
3
Chociaż w tej odpowiedzi jest kilka dobrych rad, uważam, że główna rada, której należy użyć func(series)zamiast, series.apply(func)ma zastosowanie tylko wtedy, gdy funkcja jest w całości zdefiniowana przy użyciu operacji, które zachowują się podobnie zarówno na indywidualnej wartości, jak i na serii. Tak jest w przykładzie w pierwszej odpowiedzi, ale tak nie jest w pytaniu PO, który pyta bardziej ogólnie o zastosowanie funkcji do kolumn. 1/2
Graham Lea
1
Na przykład, jeśli df jest: DataFrame({'a': ['Aaron', 'Bert', 'Christopher'], 'b': ['Bold', 'Courageous', 'Distrusted']})i calcjest: def calc(x): return x[0], len(x)to tdf.a.apply(calc))i calc(tdf.a)zwraca bardzo różne rzeczy.
Graham Lea