Добавление столбца в фрейм данных, в котором значение количества дубликатов каждой строки занимает слишком много времени

#python #pandas #dataframe #numpy

Вопрос:

Я читал сообщения SOF о том, как создать поле, содержащее количество дубликатов, которые содержит строка в фрейме данных pandas. Не используя никаких других библиотек, я попытался написать функцию, которая делает это, и она работает с небольшими объектами фреймов данных; однако для больших объектов это занимает слишком много времени и потребляет слишком много памяти.

Это и есть функция:

 def count_duplicates(dataframe):
    function = lambda x: dataframe.to_numpy().tolist().count(x.to_list()) - 1
    return dataframe.apply(function, axis=1)   
 

Я сделал dir в массив numpy из DataFrame.to_numpy функции, и я не видел функции, похожей на эту list.count функцию. Причина, по которой это занимает так много времени, заключается в том, что для каждой строки необходимо сравнить строку со всеми строками в массиве numpy. Я хотел бы гораздо более эффективный способ сделать это, даже если он не использует фрейм данных pandas. Я чувствую, что должен быть простой способ сделать это с помощью numpy , но я просто недостаточно знаком. Я уже некоторое время тестирую различные подходы, и это приводит к множеству ошибок. Я собираюсь продолжать тестировать различные подходы, но почувствовал, что сообщество может предложить лучший способ.

Спасибо вам за вашу помощь.

Вот пример фрейма данных:

    one  two
0    1    1
1    2    2
2    3    3
3    1    1
 

Я бы использовал его вот так:

 d['duplicates'] = count_duplicates(d)
 

Результирующий кадр данных является:

    one  two  duplicates
0    1    1           1
1    2    2           0
2    3    3           0
3    1    1           1
 

Проблема в том, что фактический кадр данных будет содержать 1,4 миллиона строк, и каждая лямбда занимает в среднем 0,148558 секунды, что при умножении на 1,4 миллиона строк составляет около 207981,459 секунды или 57,772 часа. Мне нужен гораздо более быстрый способ сделать это.

Еще раз спасибо вам.

Я обновил функцию, которая ускоряет процесс:

 def _counter(series_to_count, list_of_lists):
    return list_of_lists.count(series_to_count.to_list()) - 1

def count_duplicates(dataframe):
    df_list = dataframe.to_numpy().tolist()
    return dataframe.apply(_counter, args=(df_list,), axis=1)
 

Это занимает всего 29,487 секунды. Узким местом было преобразование фрейма данных при каждом вызове функции.

Я все еще заинтересован в оптимизации этого. Я бы хотел сократить это до 2-3 секунд, если это вообще возможно. Может быть, это и не так, но я хотел бы убедиться, что это произойдет как можно быстрее.

Еще раз спасибо вам.

Комментарии:

1. не могли бы вы, пожалуйста, предоставить образец фрейма данных и желаемый результат.

2. Я привел пример и некоторые данные о текущем времени выполнения и расчетном времени выполнения с текущим кодом.

3. Я отредактировал функцию, и она оптимизировала ее, но для завершения 1,4 миллиона строк все равно требуется около 29 секунд. Я все еще ищу лучший способ, если вы можете помочь.

Ответ №1:

Вот векторизованный способ сделать это. Для 1,4 миллиона строк, в среднем 140 дубликатов для каждой строки, это занимает менее 0,05 секунды. Когда дубликатов вообще нет, это занимает около 0,4 секунды.

 d['duplicates'] = d.groupby(['one', 'two'], sort=False)['one'].transform('size') - 1
 

На вашем примере:

 >>> d
   one  two  duplicates
0    1    1           1
1    2    2           0
2    3    3           0
3    1    1           1
 

Скорость

Относительно высокая частота дубликатов:
 n = 1_400_000
d = pd.DataFrame(np.random.randint(0, 100, size=(n, 2)), columns='one two'.split())
%timeit d.groupby(['one', 'two'], sort=False)['one'].transform('size') - 1
# 48.3 ms ± 110 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

# how many duplicates on average?
>>> (d.groupby(['one', 'two'], sort=False)['one'].transform('size') - 1).mean()
139.995841

# (as expected: n / 100**2)
 
Никаких дубликатов
 n = 1_400_000
d = pd.DataFrame(np.arange(2 * n).reshape(-1, 2), columns='one two'.split())
%timeit d.groupby(['one', 'two'], sort=False)['one'].transform('size') - 1
# 389 ms ± 1.55 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)