панды & ldquo; isin & rdquo; намного медленнее, чем numpy & ldquo; in1d & rdquo;

Python: Mutable Default Argument

Аргументы по умолчанию получают оценку во время компиляции функции в объект функции. При использовании этой функции, несколько раз с помощью этой функции, они являются и остаются одним и тем же объектом.

Когда они изменяются, когда они мутируются (например, добавляя к нему элемент), они остаются мутированными по последовательным вызовам.

Они остаются мутированными, потому что каждый раз они являются одним и тем же объектом .

Эквивалентный код:

Поскольку список связан с функцией, когда объект функции компилируется и инстанцируется, это:

def foo(mutable_default_argument=[]): # make a list the default argument
    """function that uses a list"""

почти точно эквивалентно этому:

_a_list = [] # create a list in the globals

def foo(mutable_default_argument=_a_list): # make it the default argument
    """function that uses a list"""

del _a_list # remove globals name binding

Демонстрация

Вот демонстрация - вы можете проверить, что они являются одним и тем же объектом каждый раз, когда они ссылаются на

  • , увидев, что список создан до того, как функция завершила компиляцию объекту функции,
  • , заметив, что идентификатор один и тот же каждый раз, когда список ссылается,
  • , наблюдая, что список остается, когда функция, которая его использует, называется второй раз,
  • , наблюдая порядок, в котором вывод печатается из источника (который я удобно пронумеровал для вас):

example.py

print('1. Global scope being evaluated')

def create_list():
    '''noisily create a list for usage as a kwarg'''
    l = []
    print('3. list being created and returned, id: ' + str(id(l)))
    return l

print('2. example_function about to be compiled to an object')

def example_function(default_kwarg1=create_list()):
    print('appending "a" in default default_kwarg1')
    default_kwarg1.append("a")
    print('list with id: ' + str(id(default_kwarg1)) + 
          ' - is now: ' + repr(default_kwarg1))

print('4. example_function compiled: ' + repr(example_function))


if __name__ == '__main__':
    print('5. calling example_function twice!:')
    example_function()
    example_function()

и работает это с python example.py:

1. Global scope being evaluated
2. example_function about to be compiled to an object
3. list being created and returned, id: 140502758808032
4. example_function compiled: 
5. calling example_function twice!:
appending "a" in default default_kwarg1
list with id: 140502758808032 - is now: ['a']
appending "a" in default default_kwarg1
list with id: 140502758808032 - is now: ['a', 'a']

Означает ли это нарушение принципа «Наименьшее удивление»?

Этот порядок выполнения часто путает новых пользователей Python. Если вы понимаете модель исполнения Python, это становится вполне ожидаемым.

Обычная инструкция для новых пользователей Python:

Но поэтому обычная инструкция для новых пользователей заключается в том, чтобы вместо этого создать свои аргументы по умолчанию:

def example_function_2(default_kwarg=None):
    if default_kwarg is None:
        default_kwarg = []

Это использует None singleton как объект-дозор, чтобы сообщить функции, есть ли у нас аргумент, отличный от значения по умолчанию. Если мы не получаем никакого аргумента, то мы действительно хотим использовать новый пустой список [] в качестве значения по умолчанию.

Как говорится в разделе в разделе управления потоком :

Если вы не хотите, чтобы по умолчанию были разделены между последующими вызовами, вы можете написать такую ​​функцию, как это:

def f(a, L=None):
    if L is None:
        L = []
    L.append(a)
    return L
2
задан mclafee 26 March 2019 в 14:52
поделиться

1 ответ

Numpy и Pandas используют разные алгоритмы для isin. В некоторых случаях версия Numpy быстрее, а для некоторых панд. Для вашего теста NumPy кажется быстрее.

Версия Pandas, однако, имеет лучшее асимптотическое время выполнения, она выиграет для больших наборов данных.


Предположим, что в серии данных есть n элементов (в вашем примере df) и в запросе m (в вашем примере vals).

Обычно алгоритм Numpy выполняет следующие действия:

  • Используйте np.unique(..), чтобы найти все уникальные элементы в ряду. Таким образом, выполняется сортировка, т. Е. O(n*log(n)), может быть N<=n уникальных элементов.
  • Для каждого элемента используйте бинарный поиск, чтобы определить, входит ли элемент в ряд, то есть O(m*log(N)) в целом.

Что приводит к общему времени работы O(n*log(n) + m*log(N)).

Существуют некоторые жестко запрограммированные оптимизации для случаев, когда vals только несколько элементов, и для этих случаев действительно блестит numpy.

Панды делают что-то другое:

  • Заполняет хеш-карту (обернутая khash -функция), чтобы найти все уникальные элементы, которая занимает O(n).
  • Поиск в хэш-карте в O(1) для каждого запроса, т. Е. O(m) в целом.

В целом, время работы составляет O(n)+O(m), что намного лучше, чем у Нампи.

Однако, для меньших входных данных постоянные факторы, а не асимптотическое поведение - это то, что имеет значение, и это просто намного лучше для Numpy. Есть и другие соображения, такие как потребление памяти (которое выше у панд), которое может сыграть свою роль.

Но если мы возьмем больший набор запросов, ситуация будет совершенно другой:

import pandas as pd
import numpy as np
df = pd.DataFrame(np.random.randint(0,10,(10**6),dtype='int8'),columns=['A'])
vals = np.array([5,7],dtype='int64')
vals2 = np.random.randint(0,10,(10**6),dtype='int64')

И теперь:

%timeit df['A'].isin(vals)    # 17.0 ms 
%timeit df['A'].isin(vals2)   # 16.8 ms

%timeit pd.np.in1d(df['A'],vals)    # 1.36
%timeit pd.np.in1d(df['A'],vals2)   # 82.9 ms

Numpy действительно сдает позиции, пока есть больше запросы. Также можно видеть, что создание хеш-карты является узким местом для панд, а не для запросов.

В конце концов, не имеет большого смысла (даже если бы я это сделал!) Оценивать производительность только для одного размера ввода - это должно быть сделано для диапазона размеров ввода - есть некоторые сюрпризы, которые нужно обнаружить!

Например, Интересный факт: если вы возьмете

df = pd.DataFrame(np.random.randint(0,10,(10**6+1), dtype='int8'),columns=['A'])

, т.е. 10^6+1 вместо 10^6, панды прибегнут к алгоритму numpy (который, на мой взгляд, не очень умен) и станут лучше для маленьких входов, но хуже для больших:

%timeit df['A'].isin(vals)    # 6ms  was 17.0 ms 
%timeit df['A'].isin(vals2)   # 100ms was 16.8 ms
0
ответ дан ead 26 March 2019 в 14:52
поделиться
Другие вопросы по тегам:

Похожие вопросы: