Какие методы можно использовать для измерения производительности панд /numpy решений
Вопрос
Как измерить производительность различных функций ниже в сжатой и всеобъемлющей форме.
пример
Рассмотрим фрейм данных df
df = pd.DataFrame({
'Group': list('QLCKPXNLNTIXAWYMWACA'),
'Value': [29, 52, 71, 51, 45, 76, 68, 60, 92, 95,
99, 27, 77, 54, 39, 23, 84, 37, 99, 87]
})
Я хочу суммировать столбец Value
сгруппированный по отдельным значениям в Group
. У меня есть три способа сделать это.
import pandas as pd
import numpy as np
from numba import njit
def sum_pd(df):
return df.groupby('Group').Value.sum()
def sum_fc(df):
f, u = pd.factorize(df.Group.values)
v = df.Value.values
return pd.Series(np.bincount(f, weights=v).astype(int), pd.Index(u, name='Group'), name='Value').sort_index()
@njit
def wbcnt(b, w, k):
bins = np.arange(k)
bins = bins * 0
for i in range(len(b)):
bins[b[i]] += w[i]
return bins
def sum_nb(df):
b, u = pd.factorize(df.Group.values)
w = df.Value.values
bins = wbcnt(b, w, u.size)
return pd.Series(bins, pd.Index(u, name='Group'), name='Value').sort_index()
Они одинаковы?
print(sum_pd(df).equals(sum_nb(df)))
print(sum_pd(df).equals(sum_fc(df)))
True
True
Как быстро они?
%timeit sum_pd(df)
%timeit sum_fc(df)
%timeit sum_nb(df)
1000 loops, best of 3: 536 µs per loop
1000 loops, best of 3: 324 µs per loop
1000 loops, best of 3: 300 µs per loop
Ответы
Ответ 1
Они могут не классифицироваться как "простые фреймворки", потому что они являются сторонними модулями, которые необходимо установить, но я часто использую две фреймворки:
Например, библиотека simple_benchmark
позволяет декорировать функции для тестирования:
from simple_benchmark import BenchmarkBuilder
b = BenchmarkBuilder()
import pandas as pd
import numpy as np
from numba import njit
@b.add_function()
def sum_pd(df):
return df.groupby('Group').Value.sum()
@b.add_function()
def sum_fc(df):
f, u = pd.factorize(df.Group.values)
v = df.Value.values
return pd.Series(np.bincount(f, weights=v).astype(int), pd.Index(u, name='Group'), name='Value').sort_index()
@njit
def wbcnt(b, w, k):
bins = np.arange(k)
bins = bins * 0
for i in range(len(b)):
bins[b[i]] += w[i]
return bins
@b.add_function()
def sum_nb(df):
b, u = pd.factorize(df.Group.values)
w = df.Value.values
bins = wbcnt(b, w, u.size)
return pd.Series(bins, pd.Index(u, name='Group'), name='Value').sort_index()
Также украсьте функцию, которая производит значения для теста:
from string import ascii_uppercase
def creator(n): # taken from another answer here
letters = list(ascii_uppercase)
np.random.seed([3,1415])
df = pd.DataFrame(dict(
Group=np.random.choice(letters, n),
Value=np.random.randint(100, size=n)
))
return df
@b.add_arguments('Rows in DataFrame')
def argument_provider():
for exponent in range(4, 22):
size = 2**exponent
yield size, creator(size)
И тогда все, что вам нужно для запуска теста:
r = b.run()
После этого вы можете просмотреть результаты в виде графика (для этого вам нужна библиотека matplotlib
):
r.plot()
![enter image description here]()
В случае, если функции очень похожи во время выполнения, процентная разница вместо абсолютных чисел может быть более важной:
r.plot_difference_percentage(relative_to=sum_nb)
![enter image description here]()
Или получите время для теста как DataFrame
(для этого нужны pandas
)
r.to_pandas_dataframe()
sum_pd sum_fc sum_nb
16 0.000796 0.000515 0.000502
32 0.000702 0.000453 0.000454
64 0.000702 0.000454 0.000456
128 0.000711 0.000456 0.000458
256 0.000714 0.000461 0.000462
512 0.000728 0.000471 0.000473
1024 0.000746 0.000512 0.000513
2048 0.000825 0.000515 0.000514
4096 0.000902 0.000609 0.000640
8192 0.001056 0.000731 0.000755
16384 0.001381 0.001012 0.000936
32768 0.001885 0.001465 0.001328
65536 0.003404 0.002957 0.002585
131072 0.008076 0.005668 0.005159
262144 0.015532 0.011059 0.010988
524288 0.032517 0.023336 0.018608
1048576 0.055144 0.040367 0.035487
2097152 0.112333 0.080407 0.072154
Если вам не нравятся декораторы, вы также можете настроить все за один вызов (в этом случае вам не нужны декораторы BenchmarkBuilder
и add_function
/add_arguments
):
from simple_benchmark import benchmark
r = benchmark([sum_pd, sum_fc, sum_nb], {2**i: creator(2**i) for i in range(4, 22)}, "Rows in DataFrame")
Здесь perfplot
предлагает очень похожий интерфейс (и результат):
import perfplot
r = perfplot.bench(
setup=creator,
kernels=[sum_pd, sum_fc, sum_nb],
n_range=[2**k for k in range(4, 22)],
xlabel='Rows in DataFrame',
)
import matplotlib.pyplot as plt
plt.loglog()
r.plot()
![enter image description here]()
Ответ 2
Термин для этого - "сравнительный бенчмаркинг", и, как и во всех бенчмарках, важно указать (даже если это только для вас), что вы хотите для бенчмаркинга. Также плохой тест хуже, чем отсутствие тестов вообще. Таким образом, любая структура должна быть тщательно отрегулирована в зависимости от ваших настроек.
Обычно, когда вы анализируете алгоритмы, вас интересует "порядок роста". Поэтому, как правило, вы хотите сравнить алгоритм с различными длинами входных данных (но могут быть важны и другие метрики, такие как "количество дубликатов" при создании set
или начальный порядок при сравнении алгоритмов sort
). Но важны не только асимптотические характеристики, но и постоянные факторы (особенно если это постоянные факторы для членов более высокого порядка).
Что касается предисловия, я часто сам использую какую-то "простую структуру":
# Setup
import pandas as pd
import numpy as np
from numba import njit
@njit
def numba_sum(arr):
return np.sum(arr)
# Timing setup
timings = {sum: [], np.sum: [], numba_sum: []}
sizes = [2**i for i in range(1, 20, 2)]
# Timing
for size in sizes:
func_input = np.random.random(size=size)
for func in timings:
res = %timeit -o func(func_input) # if you use IPython, otherwise use the "timeit" module
timings[func].append(res)
Это все, что нужно, чтобы сделать некоторые ориентиры. Более важный вопрос - как их визуализировать. Один из подходов, который я обычно использую, состоит в том, чтобы построить их логарифмически. Таким образом, вы можете увидеть постоянные факторы для небольших массивов, а также увидеть, как асимптотически они работают:
%matplotlib notebook
import matplotlib.pyplot as plt
import numpy as np
fig = plt.figure(1)
ax = plt.subplot(111)
for func in timings:
ax.plot(sizes,
[time.best for time in timings[func]],
label=str(func)) # you could also use "func.__name__" here instead
ax.set_xscale('log')
ax.set_yscale('log')
ax.set_xlabel('size')
ax.set_ylabel('time [seconds]')
ax.grid(which='both')
ax.legend()
plt.tight_layout()
![enter image description here]()
Но другой подход заключается в том, чтобы найти базовый уровень и построить относительную разницу:
%matplotlib notebook
import matplotlib.pyplot as plt
import numpy as np
fig = plt.figure(1)
ax = plt.subplot(111)
baseline = sum_nb # choose one function as baseline
for func in timings:
ax.plot(sizes,
[time.best / ref.best for time, ref in zip(timings[func], timings[baseline])],
label=str(func)) # you could also use "func.__name__" here instead
ax.set_yscale('log')
ax.set_xscale('log')
ax.set_xlabel('size')
ax.set_ylabel('time relative to {}'.format(baseline)) # you could also use "func.__name__" here instead
ax.grid(which='both')
ax.legend()
plt.tight_layout()
![enter image description here]()
Легенда может нуждаться в дополнительной работе... уже поздно... надеюсь, это пока понятно.
Просто несколько дополнительных случайных замечаний:
-
Документация timeit.Timer.repeat
содержит очень важное примечание:
Соблазнительно рассчитать среднее и стандартное отклонение от вектора результатов и сообщить о них. Однако это не очень полезно. В типичном случае самое низкое значение дает нижнюю границу для того, насколько быстро ваша машина может выполнить данный фрагмент кода; более высокие значения в векторе результатов, как правило, вызваны не изменчивостью скорости Питона, а другими процессами, влияющими на точность синхронизации. Таким образом, min() результата, вероятно, является единственным числом, которое вас должно заинтересовать. После этого вы должны смотреть на весь вектор и применять здравый смысл, а не статистику.
Это означает, что mean
может быть предвзятым, а следовательно, и sum
. Вот почему я использовал .best
из %timeit
результата. Это "мин". Конечно, минимум тоже не полная правда, просто убедитесь, что min
и mean
(или sum
) не показывают разные тенденции.
-
Я использовал графики log-log выше. Это позволяет легко интерпретировать общую производительность ("x быстрее, чем y, если он длиннее 1000 элементов"), но затрудняет количественную оценку (например, "это в 3 раза быстрее, чем x, чем y"). Так что в некоторых случаях другие виды визуализации могут быть более подходящими.
-
%timeit
- это здорово, потому что он вычисляет повторы, так что для каждого теста требуется примерно 1-3 секунды. Однако в некоторых случаях явные повторы могут быть лучше.
-
Всегда проверяйте правильность времени! Будьте особенно осторожны при выполнении операций, которые изменяют глобальное состояние или изменяют ввод. Например, для синхронизации сортировки на месте требуется шаг настройки перед каждым тестом, в противном случае вы сортируете уже отсортированную вещь (что является лучшим вариантом для нескольких алгоритмов сортировки).
Ответ 3
Фреймворк
Люди ранее просили меня об этом. Так что я просто публикую это как Q & A в надежде, что другие найдут это полезным.
Я приветствую все отзывы и предложения.
Варьируемый размер
Первым приоритетом для вещей, которые я обычно проверяю, является то, насколько быстрыми являются решения по сравнению с различными размерами входных данных. Это не всегда очевидно, как мы должны масштабировать "размер" данных.
Мы инкапсулируем эту концепцию с помощью функции creator
которая принимает один параметр n
который определяет размер. В этом случае creator
создает кадр данных длиной n
с двумя столбцами Group
и Value
from string import ascii_uppercase
def creator(n):
letters = list(ascii_uppercase)
np.random.seed([3,1415])
df = pd.DataFrame(dict(
Group=np.random.choice(letters, n),
Value=np.random.randint(100, size=n)
))
return df
Размеры
Я хочу протестировать различные размеры, указанные в списке
sizes = [1000, 3000, 10000, 30000, 100000]
методы
Я хочу список функций для тестирования. Каждая функция должна принимать один вход, который является выходом от creator
.
У нас есть функции от OP
import pandas as pd
import numpy as np
from numba import njit
def sum_pd(df):
return df.groupby('Group').Value.sum()
def sum_fc(df):
f, u = pd.factorize(df.Group.values)
v = df.Value.values
return pd.Series(np.bincount(f, weights=v).astype(int), pd.Index(u, name='Group'), name='Value').sort_index()
@njit
def wbcnt(b, w, k):
bins = np.arange(k)
bins = bins * 0
for i in range(len(b)):
bins[b[i]] += w[i]
return bins
def sum_nb(df):
b, u = pd.factorize(df.Group.values)
w = df.Value.values
bins = wbcnt(b, w, u.size)
return pd.Series(bins, pd.Index(u, name='Group'), name='Value').sort_index()
methods = [sum_pd, sum_fc, sum_nb]
тестер
Наконец, мы строим нашу функцию tester
import pandas as pd
from timeit import timeit
def tester(sizes, methods, creator, k=100, v=False):
results = pd.DataFrame(
index=pd.Index(sizes, name='Size'),
columns=pd.Index([m.__name__ for m in methods], name='Method')
)
methods = {m.__name__: m for m in methods}
for n in sizes:
x = creator(n)
for m in methods.keys():
stmt = '%s(x)' % m
setp = 'from __main__ import %s, x' % m
if v:
print(stmt, setp, n)
t = timeit(stmt, setp, number=k)
results.set_value(n, m, t)
return results
Мы фиксируем результаты с
results = tester(sizes, methods, creator)
print(results)
Method sum_pd sum_fc sum_nb
Size
1000 0.0632993 0.0316809 0.0364261
3000 0.0596143 0.031896 0.0319997
10000 0.0609055 0.0324342 0.0363031
30000 0.0646989 0.03237 0.0376961
100000 0.0656784 0.0363296 0.0331994
И мы можем построить с
results.plot()
![enter image description here]()