Подтвердить что ты не робот

Какие методы можно использовать для измерения производительности панд /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
4b9b3361

Ответ 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