У меня есть панды DataFrame, df_test
. Он содержит столбец «размер», который представляет размер в байтах. Я рассчитал КБ, МБ и ГБ, используя следующий код:
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]
Я запустил это более 120000 строк, и время занимает около 2,97 секунды на столбец * 3 = ~ 9 секунд согласно% timeit.
Есть ли способ сделать это быстрее? Например, могу ли я вместо того, чтобы возвращать по одному столбцу за раз из приложения и запускать его 3 раза, могу ли я вернуть все три столбца за один проход, чтобы вставить обратно в исходный фрейм данных?
Все остальные вопросы, которые я нашел, хотят принимать несколько значений и возвращать одно значение . Я хочу взять одно значение и вернуть несколько столбцов .
rows_list
в этом ответе?pd.Series(data, index=...)
. В противном случае вы получите загадочные ошибки, когда попытаетесь вернуть результат в родительский фрейм данных.Используйте apply и zip в 3 раза быстрее, чем Series.
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))
Результатом тестирования являются:
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
источник
Некоторые из текущих ответов работают нормально, но я хочу предложить другой, может быть, более «пандифицированный» вариант. Это работает для меня с текущими пандами 0.23 (не уверен, будет ли это работать в предыдущих версиях):
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")
Обратите внимание, что трюк заключается в
result_type
параметреapply
, который расширит его результат до,DataFrame
который можно напрямую назначить новым / старым столбцам.источник
Еще один читаемый способ. Этот код добавит три новых столбца и их значения, возвращая серию без использования параметров в функции 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)
Общий пример из: 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
источник
Действительно классные ответы! Спасибо, Джесси и jaumebonet! Просто небольшое наблюдение в отношении:
zip(* ...
... result_type="expand")
Хотя расширение является более элегантным ( расширенным ), zip как минимум ** в 2 раза быстрее . В этом простом примере ниже я стал в 4 раза быстрее .
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))
источник
Эффективность между основными ответами значительно различается, и Джесси и famaral42 уже обсуждали это, но стоит поделиться справедливым сравнением между основными ответами и подробно остановиться на тонкой, но важной детали ответа Джесси: аргумент передан в функция, также влияет на производительность .
(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
Вот результаты:
# 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)
Обратите внимание, что возврат кортежей является самым быстрым методом, но то, что передается в качестве аргумента, также влияет на производительность. Разница в коде невелика, но производительность значительно улучшилась.
Тест №4 (передача одного значения) в два раза быстрее, чем тест №3 (проходящий последовательно), хотя выполняемая операция якобы идентична.
Но это еще не все ...
# 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)
В некоторых случаях (# 1a и # 4a) применение функции к DataFrame, в котором уже существуют выходные столбцы, происходит быстрее, чем их создание из функции.
Вот код для запуска тестов:
# 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))
источник
Я считаю, что версия 1.1 нарушает поведение, предложенное в верхнем ответе здесь.
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)
Приведенный выше код, запущенный на pandas 1.1.0, возвращает:
a b c d 0 1 i 1i 2 1 1 i 1i 2 2 1 i 1i 2
В pandas 1.0.5 он вернулся:
a b c d 0 1 i 1i 2 1 2 j 2j 3 2 3 k 3k 4
Я думаю, этого и следовало ожидать.
Не уверен, как в примечаниях к выпуску объясняется это поведение, однако, как объясняется здесь, предотвращение мутации исходных строк путем их копирования восстанавливает старое поведение. то есть:
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
источник
Как правило, чтобы вернуть несколько значений, это то, что я делаю
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)
Окончательный возврат фрейма данных имеет свои преимущества, но иногда и не требуется. Вы можете посмотреть, что
apply()
возвращается, и немного поиграться с функциями;)источник
Он дает новый фрейм данных с двумя столбцами от исходного.
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)
источник