Какой самый сжатый способ в Python группировать и суммировать список объектов по одному и тому же свойству

У меня есть список объектов типа C, где тип C состоит из свойств X, Y, Z, например, c.X, c.Y, c.Z

Теперь я хочу выполнить следующую задачу:

  • Суммировать свойство Z тех объектов, которые имеют одинаковое значение для свойства Y
  • Вывести список кортежей (Y, сумма Zs с этим Y)

Какой самый сжатый способ?

Ответ 1

Подход defaultdict, вероятно, лучше, если предположить, что c.Y hashable, но здесь другой способ:

from itertools import groupby
from operator import attrgetter
get_y = attrgetter('Y')
tuples = [(y, sum(c.Z for c in cs_with_y) for y, cs_with_y in 
           groupby(sorted(cs, key=get_y), get_y)]

Чтобы быть более конкретным в отношении различий:

  • Этот подход требует создания сортированной копии cs, которая берет O (n log n) и O (n) дополнительное пространство. В качестве альтернативы вы можете сделать cs.sort(key=get_y) для сортировки cs на месте, что не требует дополнительного места, но изменяет список cs. Обратите внимание, что groupby возвращает итератор, чтобы там не было лишних накладных расходов. Если значения c.Y не hashable, тем не менее это работает, тогда как подход defaultdict будет вызывать TypeError.

    Но будьте осторожны - в последних Pythons он поднимет TypeError, если там есть какие-либо сложные числа, и, возможно, в других случаях. Возможно, эту работу можно выполнить с помощью соответствующей функции key - key=lambda e: (e.real, e.imag) if isinstance(e, complex) else e, похоже, работает на все, что я пробовал против нее прямо сейчас, хотя, конечно, пользовательские классы, которые переопределяют оператор __lt__, чтобы поднять исключение по-прежнему не идут. Возможно, вы могли бы определить более сложную ключевую функцию, которая проверяет это, и т.д.

    Конечно, все, о чем мы заботимся здесь, это то, что равные вещи находятся рядом друг с другом, а не столько, что они действительно сортировались, и вы могли бы написать функцию O (n ^ 2), чтобы сделать это, а не сортировать, если вы так желательно. Или функция, которая O (num_hashable + num_nonhashable ^ 2). Или вы могли бы написать версию O (n ^ 2)/O (num_hashable + num_nonhashable ^ 2) groupby, которая делает эти два вместе.

  • sblom answer работает для атрибутов hashable c.Y, с минимальным дополнительным пространством (потому что он вычисляет суммы напрямую).

  • philhag answer в основном совпадает с sblom, но использует дополнительную вспомогательную память, создавая список каждого из c - эффективно делая то, что groupby, но с хешированием вместо предположения, что он отсортирован и с фактическими списками вместо итераторов.

Итак, если вы знаете, что ваш атрибут c.Y hashable и нужны только суммы, используйте sblom's; если вы знаете, что это hashable, но хотите, чтобы они были сгруппированы для чего-то еще, используйте philhag's; если они не могут быть хешируемыми, используйте это (с дополнительным беспокойством, как отмечено, если они могут быть сложными или настраиваемый тип, который переопределяет __lt__).

Ответ 2

from collections import defaultdict
totals = defaultdict(int)
for c in cs:
  totals[c.Y] += c.Z

tuples = totals.items()

Ответ 3

Вы можете использовать collections.defaultdict, чтобы сгруппировать список по значениям y, а затем суммировать их значения z:

import collections
ymap = collections.defaultdict(list)
for c in listOfCs:
  ymap[c.Y].append(c)
print ([(y, sum(c.Z for c in clist)) for y,clist in ymap.values()])

Ответ 4

С pandas может быть что-то вроде:

df.groupby('Y')['Z'].sum()

Пример

>>> import pandas
>>> df = pandas.DataFrame(dict(X=[1,2,3], Y=[1,-1,1], Z=[3,4,5]))
>>> df
   X  Y   Z
0  1  1   3
1  2  -1  4
2  3  1   5
>>> df.groupby('Y')['Z'].sum()
Y
-1    4
1     8
>>> 

Ответ 5

Вы можете использовать Counter

from collections import Counter
cnt = Counter()
for c in cs:
  cnt[c.Y] += c.Z


print cnt