У меня есть список словарей, например:
[{'user': '123456', 'db': 'db1', 'size': '8628'}
{'user': '123456', 'db': 'db1', 'size': '7168'}
{'user': '123456', 'db': 'db1', 'size': '38160'}
{'user': '222345', 'db': 'db3', 'size': '8628'}
{'user': '222345', 'db': 'db3', 'size': '8628'}
{'user': '222345', 'db': 'db5', 'size': '840'}
{'user': '34521', 'db': 'db6', 'size': '12288'}
{'user': '34521', 'db': 'db6', 'size': '476'}
{'user': '2345156', 'db': 'db7', 'size': '5120'}.....]
Этот список содержит миллионы записей. Каждый пользователь может быть найден в нескольких БД, каждый пользователь может иметь несколько целых в одной БД. Я хочу подвести итог, сколько занимает каждый пользователь на каждый db. Я не хочу использовать панд. На данный момент делаю так:
result = []
for user in unique_users:
for db in unique_dbs:
total_size = 0
for i in big_list:
if (i['user'] == user and i['db'] == db):
total_size += float(i['size'])
if (total_size) > 0:
row = {}
row['user'] = user
row['db'] = db
row['size'] = total_size
result.append(row)
Проблема в том, что этот тройной цикл for превращается в нечто очень большое (сотни миллиардов итераций), которому требуется целая вечность, чтобы подвести итог. Если big_list маленький, это работает очень хорошо.
Как мне подойти к этому, чтобы сделать его быстрым и простым? Большое спасибо!
как бы я это сделал?
Почему вы не хотите использовать панд? Это было бы банально...
Отсортированы ли данные по пользователю и БД?
«Я хочу подвести итог, сколько места занимает каждый пользователь на каждую базу данных». Я не совсем понимаю. Для показанного здесь ввода, каким должен быть вывод?






Попробуйте сопоставить пользователя с БД с общим размером в словаре. Это потребует дополнительной памяти, но должно быть быстрее для доступа и требует только одного прохода через данные:
user_to_db_to_size = {}
for entry in unique_users:
user = entry['user']
db = entry['db']
size = int(entry['size'])
if user not in user_to_db_to_size:
user_to_db_to_size[user] = {}
if db not in user_to_db_to_size[user]:
user_to_db_to_size[user][db] = 0
user_to_db_to_size[user][db] += size
print(user_to_db_to_size)
Для ваших образцов данных он производит:
{'123456': {'db1': 53956}, '222345': {'db3': 17256, 'db5': 840}, '34521': {'db6': 12764}, '2345156': {'db7': 5120}}
И теперь вы можете получить доступ к общему размеру на пользователя/БД с помощью:
print(user_to_db_to_size['123456']['db1']) # 53956
Просто примечание: кеширование поиска по базовому словарю даст вам 5%-е улучшение скорости. Я включил некоторые тесты ниже.
В текущем подходе есть две основные проблемы: неэффективный алгоритм и неэффективная структура данных.
Во-первых, используемый алгоритм явно неэффективен, так как он много раз выполняет итерацию по большому списку. Нет необходимости перебирать весь список, чтобы отфильтровать уникального пользователя и базу данных. Вы можете выполнить итерацию по большому списку один раз и агрегировать данные с помощью словаря. Ключ целевого словаря — это просто кортеж (user, db). Значение словаря total_size. Вот непроверенный пример:
# Aggregation part
# Note: a default dict can be used instead to make the code possibly simpler
aggregate_dict = dict()
for i in big_list:
key = (i['user'], i['db'])
value = float(i['size'])
if key in aggregate_dict:
aggregate_dict[key] += value
else:
aggregate_dict[key] = value
# Fast creation of `result`
result = []
for user in unique_users:
for db in unique_dbs:
total_size = aggregate_dict.get((user, key))
if total_size is not None and total_size > 0:
result.append({'user': user, 'db': db, 'size': total_size})
Другая проблема — неэффективная структура данных: для каждой строки ключи реплицируются, а вместо них могут использоваться кортежи. На самом деле лучшая структура данных — хранить словарь (column, items) ключей и значений, где items — это список элементов для целевого столбца. Этот способ хранения данных называется фреймом данных. Это примерно то, что Pandas использует внутри (за исключением того, что это массив Numpy, который даже лучше, поскольку он более компактен и, как правило, более эффективен, чем список для большинства операций). Использование этой структуры данных как для ввода, так и для вывода должно привести к значительному ускорению (в сочетании с Numpy) и меньшему объему памяти.
Обратите внимание, что создание окончательных результатов в виде понимания списка примерно на 8% быстрее, поэтому [{'user': user, 'db': db, 'size': total_size} for user in unique_users for db in unique_dbs if (total_size := aggregate_dict.get((user, key)))]. Обратите внимание, что если предположить, что размеры положительны, я считаю, что if total_size is not None and total_size > 0 можно упростить до if total_size, поскольку как общий размер None, так и ноль будут оцениваться как false. Я включу тестовый код ниже для справки. Тайминги немного неустойчивы, но понимание списка постоянно быстрее.
@Джон М. Действительно для понимания списка. Для размеров они, безусловно, положительны, но могут быть и не строго положительными (т. е., возможно, нулевыми). Спасибо, что указали на это и сделали тест.
Пожалуйста. Хотя ты проделал всю тяжелую работу. Я полагаю, что Null все равно будет оцениваться как false.
На самом деле, я думал, что 8% — это неправильно. Ключ поиска в aggregate_dict.get((user, key)) должен быть aggregate_dict.get((user, db)). Ошибка означала, что результирующий список был пуст, поскольку он фактически искал кортеж (user, (key, value)). Замена на db ускоряет понимание списка на 38%.
Если вы используете Counter и создаете кортежи пар значений (user, db) в качестве ключей, то:
from collections import Counter
data = [{'user': '123456', 'db': 'db1', 'size': '8628'},
{'user': '123456', 'db': 'db1', 'size': '7168'},
{'user': '123456', 'db': 'db1', 'size': '38160'},
{'user': '222345', 'db': 'db3', 'size': '8628'},
{'user': '222345', 'db': 'db3', 'size': '8628'},
{'user': '222345', 'db': 'db5', 'size': '840'},
{'user': '34521', 'db': 'db6', 'size': '12288'},
{'user': '34521', 'db': 'db6', 'size': '476'},
{'user': '2345156', 'db': 'db7', 'size': '5120'}]
print(sum((Counter({(d['user'], d['db']): int(d['size'])}) for d in data), start=Counter()))
Counter({('123456', 'db1'): 53956, ('222345', 'db3'): 17256, ('34521', 'db6'): 12764, ('2345156', 'db7'): 5120, ('222345', 'db5'): 840})
Таким образом, глядя на три предложенных решения этой проблемы, подход от rdas, вероятно, является победителем. С парой модификаций оно на 57 % быстрее, чем решение для Жерома, и в 180 раз быстрее, чем исходный код. Решение Сергея было примерно в 350 раз медленнее на значительно урезанном подмножестве результатов (1000 записей); он также, казалось, очень плохо масштабировался, и у меня не было времени ждать результатов от полного набора данных.
Тайминги следующие:
В ответе Жерома окончательный расчет результатов можно улучшить, заменив окончательную конструкцию списка на понимание списка. Глядя на изменение понимания изолированного списка, код обновления работает примерно на 38% быстрее. Стоит отметить, что это не является основным фактором общего времени.
[{'user': user, 'db': db, 'size': total_size} for user in unique_users for db in unique_dbs if (total_size := aggregate_dict.get((user, db)))]
сравнение до (Метод1) и после (Метод2) за несколько итераций дает:
Method1: 6.447344800049905
Method2: 4.851344000024255
Method1: 6.546811900043394
Method2: 4.816081999975722
Method1: 6.863985500007402
Method2: 4.755402299982961
Method1: 6.8024070000392385
Method2: 4.738170499971602
Method1: 6.524162200046703
Method2: 4.755566900013946
Первое обновление ответа rdas заключалось в том, чтобы избежать поиска в нескольких словарях путем кэширования поиска первого словаря:
if not (user_dict := user_to_db_to_size.get(user)):
user_to_db_to_size[user] = { db: size }
elif not db in user_dict:
user_dict[db] = size
else:
user_dict[db] += size
впоследствии это было снова улучшено путем замены на collections.defaultdict, что устраняет дополнительные дорогостоящие поиски в словаре:
user_to_db_to_size = defaultdict(lambda: defaultdict(int))
for entry in big_list:
user = entry['user']
db = entry['db']
size = int(entry['size'])
user_to_db_to_size[user][db] += size
return user_to_db_to_size
Это код, используемый для запуска тестов:
import random
import timeit
from collections import defaultdict
random.seed(1)
test_iterations = 100
big_list = [{'user': random.randint(0, 100), 'db': f'db{random.randint(1, 10)}', 'size': f'{random.randint(100, 90000)}' } for i in range(10000)]
unique_users = { i['user'] for i in big_list }
unique_dbs = { i['db'] for i in big_list }
def method0():
result = []
for user in unique_users:
for db in unique_dbs:
total_size = 0
for i in big_list:
if (i['user'] == user and i['db'] == db):
total_size += float(i['size'])
if (total_size) > 0:
row = {}
row['user'] = user
row['db'] = db
row['size'] = total_size
result.append(row)
return result
def method1():
aggregate_dict = dict()
for i in big_list:
key = (i['user'], i['db'])
value = float(i['size'])
if key in aggregate_dict:
aggregate_dict[key] += value
else:
aggregate_dict[key] = value
result = []
for user in unique_users:
for db in unique_dbs:
total_size = aggregate_dict.get((user, db))
if total_size is not None and total_size > 0:
result.append({'user': user, 'db': db, 'size': total_size})
return result
def method2():
aggregate_dict = dict()
for i in big_list:
key = (i['user'], i['db'])
value = float(i['size'])
if key in aggregate_dict:
aggregate_dict[key] += value
else:
aggregate_dict[key] = value
return [{'user': user, 'db': db, 'size': total_size} for user in unique_users for db in unique_dbs if (total_size := aggregate_dict.get((user, db)))]
def method3():
user_to_db_to_size = {}
for entry in big_list:
user = entry['user']
db = entry['db']
size = int(entry['size'])
if user not in user_to_db_to_size:
user_to_db_to_size[user] = {}
if db not in user_to_db_to_size[user]:
user_to_db_to_size[user][db] = 0
user_to_db_to_size[user][db] += size
return user_to_db_to_size
def method4():
user_to_db_to_size = {}
for entry in big_list:
user = entry['user']
db = entry['db']
size = int(entry['size'])
if not (user_dict := user_to_db_to_size.get(user)):
user_to_db_to_size[user] = { db: size }
elif not db in user_dict:
user_dict[db] = size
else:
user_dict[db] += size
return user_to_db_to_size
def method5():
user_to_db_to_size = defaultdict(lambda: defaultdict(int))
for entry in big_list:
user = entry['user']
db = entry['db']
size = int(entry['size'])
user_to_db_to_size[user][db] += size
return user_to_db_to_size
assert (m0 := method0()) == method1()
print('Method1 OK')
assert m0 == method2()
print('Method2 OK')
assert all(v['size'] == method3()[v['user']][v['db']] for v in m0)
print('Method3 OK')
assert all(v['size'] == method4()[v['user']][v['db']] for v in m0)
print('Method4 OK')
assert all(v['size'] == method5()[v['user']][v['db']] for v in m0)
print('Method5 OK')
t0 = timeit.timeit(method0, number=test_iterations)
t1 = timeit.timeit(method1, number=test_iterations)
t2 = timeit.timeit(method2, number=test_iterations)
t3 = timeit.timeit(method3, number=test_iterations)
t4 = timeit.timeit(method4, number=test_iterations)
t5 = timeit.timeit(method5, number=test_iterations)
tmin = min((t0, t1, t2, t3, t4, t5))
print(f'| Method | Time | Relative |')
print(f'|------------------|----------------------|')
print(f'| Original | {t0} | {t0 / tmin} |')
print(f'| Jérôme | {t1} | {t1 / tmin} |')
print(f'| Jérôme (updated) | {t2} | {t2 / tmin} |')
print(f'| rdas | {t3} | {t3 / tmin} |')
print(f'| rdas (updated) | {t4} | {t4 / tmin} |')
print(f'| defaultdict | {t5} | {t5 / tmin} |')
приводит к:
Method1 OK
Method2 OK
Method3 OK
Method4 OK
Method4 OK
| Method | Time | Relative |
|------------------|----------------------|
| Original | 102.83720700000413 | 180.4580695623077 |
| Jérôme | 0.8722491000080481 | 1.5306171118096032 |
| Jérôme (updated) | 0.9000219000154175 | 1.5793526426731528 |
| rdas | 0.6866130999987945 | 1.2048642527015463 |
| rdas (updated) | 0.6580462999991141 | 1.1547354157572032 |
| defaultdict | 0.5698675999883562 | 1.0 |
Примечание. Метод Сергея примерно эквивалентен:
def c():
tmp = Counter()
for d in big_list:
tmp = tmp + Counter(d)
return tmp
так эффективно создается новый словарь с каждой итерацией в цикле. Для каждой итерации в цикле создаваемый словарь будет больше. Я подозреваю, что это масштабируется как N ** 2 или хуже.
Почему бы просто не создать карту пользователя для списка баз данных и их соответствующего размера?