Mam pandy DataFrame, df_test
. Zawiera kolumnę „rozmiar”, która reprezentuje rozmiar w bajtach. Obliczyłem KB, MB i GB za pomocą następującego kodu:
df_test = pd.DataFrame([
{'dir': '/Users/uname1', 'size': 994933},
{'dir': '/Users/uname2', 'size': 109338711},
])
df_test['size_kb'] = df_test['size'].astype(int).apply(lambda x: locale.format("%.1f", x / 1024.0, grouping=True) + ' KB')
df_test['size_mb'] = df_test['size'].astype(int).apply(lambda x: locale.format("%.1f", x / 1024.0 ** 2, grouping=True) + ' MB')
df_test['size_gb'] = df_test['size'].astype(int).apply(lambda x: locale.format("%.1f", x / 1024.0 ** 3, grouping=True) + ' GB')
df_test
dir size size_kb size_mb size_gb
0 /Users/uname1 994933 971.6 KB 0.9 MB 0.0 GB
1 /Users/uname2 109338711 106,776.1 KB 104.3 MB 0.1 GB
[2 rows x 5 columns]
Uruchomiłem to na 120000 wierszy, a czas zajmuje około 2,97 sekundy na kolumnę * 3 = ~ 9 sekund zgodnie z% timeit.
Czy w ogóle mogę to przyspieszyć? Na przykład, czy zamiast zwracać jedną kolumnę na raz po zastosowaniu i uruchamianiu jej 3 razy, czy mogę zwrócić wszystkie trzy kolumny w jednym przebiegu, aby wstawić je z powrotem do oryginalnej ramki danych?
Wszystkie inne pytania, które znalazłem, chcą przyjąć wiele wartości i zwrócić jedną wartość . Chcę wziąć jedną wartość i zwrócić wiele kolumn .
rows_list
w tej odpowiedzi?pd.Series(data, index=...)
. W przeciwnym razie podczas próby przypisania wyniku z powrotem do nadrzędnej ramki danych pojawią się tajemnicze błędy.Użyj aplikacji, a zamek błyskawiczny będzie 3 razy szybszy niż sposób serii.
def sizes(s): return locale.format("%.1f", s / 1024.0, grouping=True) + ' KB', \ locale.format("%.1f", s / 1024.0 ** 2, grouping=True) + ' MB', \ locale.format("%.1f", s / 1024.0 ** 3, grouping=True) + ' GB' df_test['size_kb'], df_test['size_mb'], df_test['size_gb'] = zip(*df_test['size'].apply(sizes))
Wynik testu to:
Separate df.apply(): 100 loops, best of 3: 1.43 ms per loop Return Series: 100 loops, best of 3: 2.61 ms per loop Return tuple: 1000 loops, best of 3: 819 µs per loop
źródło
Niektóre z obecnych odpowiedzi działają dobrze, ale chcę zaoferować inną, może bardziej „spandyfikowaną” opcję. To działa dla mnie z obecnymi pandami 0.23 (nie jestem pewien, czy zadziała w poprzednich wersjach):
import pandas as pd df_test = pd.DataFrame([ {'dir': '/Users/uname1', 'size': 994933}, {'dir': '/Users/uname2', 'size': 109338711}, ]) def sizes(s): a = locale.format("%.1f", s['size'] / 1024.0, grouping=True) + ' KB' b = locale.format("%.1f", s['size'] / 1024.0 ** 2, grouping=True) + ' MB' c = locale.format("%.1f", s['size'] / 1024.0 ** 3, grouping=True) + ' GB' return a, b, c df_test[['size_kb', 'size_mb', 'size_gb']] = df_test.apply(sizes, axis=1, result_type="expand")
Zwróć uwagę, że sztuczka dotyczy
result_type
parametru ofapply
, który rozszerzy wynik do postaci,DataFrame
którą można bezpośrednio przypisać do nowych / starych kolumn.źródło
Po prostu inny czytelny sposób. Ten kod doda trzy nowe kolumny i ich wartości, zwracając serie bez użycia parametrów w funkcji Apply.
def sizes(s): val_kb = locale.format("%.1f", s['size'] / 1024.0, grouping=True) + ' KB' val_mb = locale.format("%.1f", s['size'] / 1024.0 ** 2, grouping=True) + ' MB' val_gb = locale.format("%.1f", s['size'] / 1024.0 ** 3, grouping=True) + ' GB' return pd.Series([val_kb,val_mb,val_gb],index=['size_kb','size_mb','size_gb']) df[['size_kb','size_mb','size_gb']] = df.apply(lambda x: sizes(x) , axis=1)
Ogólny przykład z: https://pandas.pydata.org/pandas-docs/stable/generated/pandas.DataFrame.apply.html
df.apply(lambda x: pd.Series([1, 2], index=['foo', 'bar']), axis=1) #foo bar #0 1 2 #1 1 2 #2 1 2
źródło
Naprawdę fajne odpowiedzi! Dzięki Jesse i jaumebonet! Tylko kilka obserwacji dotyczących:
zip(* ...
... result_type="expand")
Chociaż expand jest bardziej elegancki ( pandify ), zip jest co najmniej ** 2x szybszy . Na tym prostym przykładzie poniżej osiągnąłem 4x szybciej .
import pandas as pd dat = [ [i, 10*i] for i in range(1000)] df = pd.DataFrame(dat, columns = ["a","b"]) def add_and_sub(row): add = row["a"] + row["b"] sub = row["a"] - row["b"] return add, sub df[["add", "sub"]] = df.apply(add_and_sub, axis=1, result_type="expand") # versus df["add"], df["sub"] = zip(*df.apply(add_and_sub, axis=1))
źródło
Wyniki najlepszych odpowiedzi są znacznie zróżnicowane, a Jesse i famaral42 już o tym dyskutowali, ale warto podzielić się uczciwym porównaniem najlepszych odpowiedzi i rozwinąć subtelny, ale ważny szczegół odpowiedzi Jessego: argument przekazany do wpływa również na wydajność .
(Python 3.7.4, Pandas 1.0.3)
import pandas as pd import locale import timeit def create_new_df_test(): df_test = pd.DataFrame([ {'dir': '/Users/uname1', 'size': 994933}, {'dir': '/Users/uname2', 'size': 109338711}, ]) return df_test def sizes_pass_series_return_series(series): series['size_kb'] = locale.format_string("%.1f", series['size'] / 1024.0, grouping=True) + ' KB' series['size_mb'] = locale.format_string("%.1f", series['size'] / 1024.0 ** 2, grouping=True) + ' MB' series['size_gb'] = locale.format_string("%.1f", series['size'] / 1024.0 ** 3, grouping=True) + ' GB' return series def sizes_pass_series_return_tuple(series): a = locale.format_string("%.1f", series['size'] / 1024.0, grouping=True) + ' KB' b = locale.format_string("%.1f", series['size'] / 1024.0 ** 2, grouping=True) + ' MB' c = locale.format_string("%.1f", series['size'] / 1024.0 ** 3, grouping=True) + ' GB' return a, b, c def sizes_pass_value_return_tuple(value): a = locale.format_string("%.1f", value / 1024.0, grouping=True) + ' KB' b = locale.format_string("%.1f", value / 1024.0 ** 2, grouping=True) + ' MB' c = locale.format_string("%.1f", value / 1024.0 ** 3, grouping=True) + ' GB' return a, b, c
Oto wyniki:
# 1 - Accepted (Nels11 Answer) - (pass series, return series): 9.82 ms ± 377 µs per loop (mean ± std. dev. of 7 runs, 100 loops each) # 2 - Pandafied (jaumebonet Answer) - (pass series, return tuple): 2.34 ms ± 48.6 µs per loop (mean ± std. dev. of 7 runs, 100 loops each) # 3 - Tuples (pass series, return tuple then zip): 1.36 ms ± 62.8 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each) # 4 - Tuples (Jesse Answer) - (pass value, return tuple then zip): 752 µs ± 18.5 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
Wskazówki jak powrocie krotki to najszybsza metoda, ale to, co jest przekazywane w jako argument, również wpływa na wydajność. Różnica w kodzie jest niewielka, ale poprawa wydajności jest znacząca.
Test nr 4 (zaliczenie pojedynczej wartości) jest dwukrotnie szybszy niż test nr 3 (zaliczenie serii), mimo że wykonywana operacja jest pozornie identyczna.
Ale jest więcej ...
# 1a - Accepted (Nels11 Answer) - (pass series, return series, new columns exist): 3.23 ms ± 141 µs per loop (mean ± std. dev. of 7 runs, 100 loops each) # 2a - Pandafied (jaumebonet Answer) - (pass series, return tuple, new columns exist): 2.31 ms ± 39.3 µs per loop (mean ± std. dev. of 7 runs, 100 loops each) # 3a - Tuples (pass series, return tuple then zip, new columns exist): 1.36 ms ± 58.4 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each) # 4a - Tuples (Jesse Answer) - (pass value, return tuple then zip, new columns exist): 694 µs ± 3.9 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
W niektórych przypadkach (# 1a i # 4a) zastosowanie funkcji do ramki DataFrame, w której kolumny wyjściowe już istnieją, jest szybsze niż utworzenie ich z funkcji.
Oto kod do uruchamiania testów:
# Paste and run the following in ipython console. It will not work if you run it from a .py file. print('\nAccepted Answer (pass series, return series, new columns dont exist):') df_test = create_new_df_test() %timeit result = df_test.apply(sizes_pass_series_return_series, axis=1) print('Accepted Answer (pass series, return series, new columns exist):') df_test = create_new_df_test() df_test = pd.concat([df_test, pd.DataFrame(columns=['size_kb', 'size_mb', 'size_gb'])]) %timeit result = df_test.apply(sizes_pass_series_return_series, axis=1) print('\nPandafied (pass series, return tuple, new columns dont exist):') df_test = create_new_df_test() %timeit df_test[['size_kb', 'size_mb', 'size_gb']] = df_test.apply(sizes_pass_series_return_tuple, axis=1, result_type="expand") print('Pandafied (pass series, return tuple, new columns exist):') df_test = create_new_df_test() df_test = pd.concat([df_test, pd.DataFrame(columns=['size_kb', 'size_mb', 'size_gb'])]) %timeit df_test[['size_kb', 'size_mb', 'size_gb']] = df_test.apply(sizes_pass_series_return_tuple, axis=1, result_type="expand") print('\nTuples (pass series, return tuple then zip, new columns dont exist):') df_test = create_new_df_test() %timeit df_test['size_kb'], df_test['size_mb'], df_test['size_gb'] = zip(*df_test.apply(sizes_pass_series_return_tuple, axis=1)) print('Tuples (pass series, return tuple then zip, new columns exist):') df_test = create_new_df_test() df_test = pd.concat([df_test, pd.DataFrame(columns=['size_kb', 'size_mb', 'size_gb'])]) %timeit df_test['size_kb'], df_test['size_mb'], df_test['size_gb'] = zip(*df_test.apply(sizes_pass_series_return_tuple, axis=1)) print('\nTuples (pass value, return tuple then zip, new columns dont exist):') df_test = create_new_df_test() %timeit df_test['size_kb'], df_test['size_mb'], df_test['size_gb'] = zip(*df_test['size'].apply(sizes_pass_value_return_tuple)) print('Tuples (pass value, return tuple then zip, new columns exist):') df_test = create_new_df_test() df_test = pd.concat([df_test, pd.DataFrame(columns=['size_kb', 'size_mb', 'size_gb'])]) %timeit df_test['size_kb'], df_test['size_mb'], df_test['size_gb'] = zip(*df_test['size'].apply(sizes_pass_value_return_tuple))
źródło
Uważam, że wersja 1.1 łamie zachowanie sugerowane w pierwszej odpowiedzi tutaj.
import pandas as pd def test_func(row): row['c'] = str(row['a']) + str(row['b']) row['d'] = row['a'] + 1 return row df = pd.DataFrame({'a': [1, 2, 3], 'b': ['i', 'j', 'k']}) df.apply(test_func, axis=1)
Powyższy kod uruchomiony na pandach 1.1.0 zwraca:
a b c d 0 1 i 1i 2 1 1 i 1i 2 2 1 i 1i 2
W pandach 1.0.5 powrócił:
a b c d 0 1 i 1i 2 1 2 j 2j 3 2 3 k 3k 4
Co myślę, że jest to, czego można się spodziewać.
Nie jestem pewien, jak informacje o wydaniu wyjaśniają to zachowanie, jednak jak wyjaśniono tutaj, uniknięcie mutacji oryginalnych wierszy przez ich skopiowanie powoduje przywrócenie starego zachowania. to znaczy:
def test_func(row): row = row.copy() # <---- Avoid mutating the original reference row['c'] = str(row['a']) + str(row['b']) row['d'] = row['a'] + 1 return row
źródło
Generalnie, aby zwrócić wiele wartości, robię to
def gimmeMultiple(group): x1 = 1 x2 = 2 return array([[1, 2]]) def gimmeMultipleDf(group): x1 = 1 x2 = 2 return pd.DataFrame(array([[1,2]]), columns=['x1', 'x2']) df['size'].astype(int).apply(gimmeMultiple) df['size'].astype(int).apply(gimmeMultipleDf)
Ostatecznie zwrócenie ramki danych ma swoje zalety, ale czasami nie jest wymagane. Możesz spojrzeć na co
apply()
zwraca i pobawić się trochę funkcjami;)źródło
Daje nową ramkę danych z dwiema kolumnami z oryginalnej.
import pandas as pd df = ... df_with_two_columns = df.apply(lambda row:pd.Series([row['column_1'], row['column_2']], index=['column_1', 'column_2']),axis = 1)
źródło