2012-07-02 61 views
26

Tôi đang cố hợp nhất nhật ký từ một số máy chủ. Mỗi nhật ký là một danh sách các bộ dữ liệu (date, count). date có thể xuất hiện nhiều lần và tôi muốn từ điển kết quả giữ tổng của tất cả các tính từ tất cả các máy chủ.Python: Từ điển hợp nhất các từ điển với tổng() của các giá trị

Đây là nỗ lực của tôi, với một số dữ liệu ví dụ:

from collections import defaultdict 

a=[("13.5",100)] 
b=[("14.5",100), ("15.5", 100)] 
c=[("15.5",100), ("16.5", 100)] 
input=[a,b,c] 

output=defaultdict(int) 
for d in input: 
     for item in d: 
      output[item[0]]+=item[1] 
print dict(output) 

Mà cho:

{'14.5': 100, '16.5': 100, '13.5': 100, '15.5': 200} 

Đúng như dự đoán.

Tôi sắp đi chuối vì một đồng nghiệp đã xem mã. Cô khẳng định rằng phải có một cách Pythonic và thanh lịch hơn để làm điều đó, mà không có lồng nhau cho các vòng. Bất kỳ ý tưởng?

+4

sử dụng 'Counter() ' –

+2

@AshwiniChaudhary:' Counter() 'chỉ số lần xuất hiện, và khi các giá trị đã được điền trước, nó sẽ không hoạt động cho kịch bản này. –

+0

@ChristianWitts xem giải pháp của tôi bên dưới. –

Trả lời

30

Không có được đơn giản hơn này, tôi nghĩ:

a=[("13.5",100)] 
b=[("14.5",100), ("15.5", 100)] 
c=[("15.5",100), ("16.5", 100)] 
input=[a,b,c] 

from collections import Counter 

print sum(
    (Counter(dict(x)) for x in input), 
    Counter()) 

Lưu ý rằng Counter (còn được gọi là một MultiSet) là cấu trúc dữ liệu tự nhiên nhất cho dữ liệu của bạn (một loại bộ mà các yếu tố có thể thuộc nhiều hơn một lần, hoặc tương đương - một bản đồ với ngữ nghĩa Element -> OccurrenceCount. Bạn có thể đã sử dụng nó ở nơi đầu tiên, thay vì danh sách các bộ dữ liệu.


Cũng có thể:

from collections import Counter 
from operator import add 

print reduce(add, (Counter(dict(x)) for x in input)) 

Sử dụng reduce(add, seq) thay vì sum(seq, initialValue) nói chung là linh hoạt hơn và cho phép bạn bỏ qua đi qua các giá trị ban đầu không cần thiết.

Lưu ý rằng bạn cũng có thể sử dụng operator.and_ để tìm giao điểm của các số nhiều thay vì tổng.


Biến thể trên là chậm chạp, vì Bộ đếm mới được tạo trên mỗi bước. Hãy sửa lỗi đó.

Chúng tôi biết rằng Counter+Counter trả lại Counter mới bằng dữ liệu đã hợp nhất.Điều này là ổn, nhưng chúng tôi muốn tránh tạo thêm. Hãy sử dụng Counter.update thay vì:

cập nhật (tự, iterable = None, ** kwds) phương pháp collections.Counter cởi

Giống như dict.update() nhưng thêm tội thay vì thay thế chúng. Nguồn có thể là một lần lặp lại, từ điển hoặc một phiên bản Bộ đếm khác.

Đó là những gì chúng tôi muốn. Hãy quấn nó với một chức năng tương thích với reduce và xem những gì sẽ xảy ra.

def updateInPlace(a,b): 
    a.update(b) 
    return a 

print reduce(updateInPlace, (Counter(dict(x)) for x in input)) 

Tốc độ này chỉ chậm hơn so với giải pháp của OP.

Benchmark: http://ideone.com/7IzSx(Cập nhật ngày với chưa một giải pháp, nhờ astynax)

(Ngoài ra: Nếu bạn rất muốn một one-liner, bạn có thể thay updateInPlace bởi lambda x,y: x.update(y) or x mà công trình cùng một cách và thậm chí chứng minh là một giây nhanh hơn, nhưng không thể đọc được. Đừng :-))

+2

+1 Tôi thực sự thích giải pháp này. – sloth

+1

Điều gì về thời gian phức tạp? Có hiệu quả hơn mã OP không – jerrymouse

+0

Tôi không nghĩ vậy. Mã OP không tạo ra bất kỳ đối tượng trực tiếp nào, vì vậy nó thường hiệu quả hơn. – Kos

7

Bạn có thể sử dụng itertools' groupby:

from itertools import groupby, chain 

a=[("13.5",100)] 
b=[("14.5",100), ("15.5", 100)] 
c=[("15.5",100), ("16.5", 100)] 
input = sorted(chain(a,b,c), key=lambda x: x[0]) 

output = {} 
for k, g in groupby(input, key=lambda x: x[0]): 
    output[k] = sum(x[1] for x in g) 

print output 

Việc sử dụng groupby thay vì hai vòng và một defaultdict sẽ làm cho mã của bạn rõ ràng hơn.

+2

thay vì lambda, bạn cũng có thể thả vào' toán tử.itemgetter (0) ':) – Kos

+1

Sai:' groupby', như đã nói trong tài liệu bạn đề cập, cần phân loại trước! Ở đây, nó hoạt động vì 'b [1]' và 'c [0]' sẽ được liên tiếp trong 'chuỗi (a, b, c)' nhưng nếu bạn làm 'chuỗi (a, c, b)', kết quả là không đúng (bạn nhận được 100 thay vì 200 cho 'output ['15 .5 ']') ... – Emmanuel

+1

Tôi đoán sở thích cá nhân của nó, nhưng tôi thấy khó đọc hơn defaultdict, và cũng chậm hơn so với phương pháp OP – fraxel

8
from collections import Counter 


a = [("13.5",100)] 
b = [("14.5",100), ("15.5", 100)] 
c = [("15.5",100), ("16.5", 100)] 

inp = [dict(x) for x in (a,b,c)] 
count = Counter() 
for y in inp: 
    count += Counter(y) 
print(count) 

đầu ra:

Counter({'15.5': 200, '14.5': 100, '16.5': 100, '13.5': 100}) 

Edit: Như duncan gợi ý bạn có thể thay thế 3 dòng này với một dòng duy nhất:

count = Counter() 
    for y in inp: 
     count += Counter(y) 

thay thế bởi: count = sum((Counter(y) for y in inp), Counter())

+2

Bạn thậm chí có thể loại bỏ vòng lặp 'for' bằng cách sử dụng' sum': 'count = sum ((Counter (y) cho y inp), Counter())' – Duncan

+0

@Duncan cảm ơn Tôi chưa bao giờ biết điều đó . –

1

Bạn có thể sử dụng Counter hoặc defaultdict hoặc bạn có thể thử biến thể của mình:

def merge_with(d1, d2, fn=lambda x, y: x + y): 
    res = d1.copy() # "= dict(d1)" for lists of tuples 
    for key, val in d2.iteritems(): # ".. in d2" for lists of tuples 
     try: 
      res[key] = fn(res[key], val) 
     except KeyError: 
      res[key] = val 
    return res 

>>> merge_with({'a':1, 'b':2}, {'a':3, 'c':4}) 
{'a': 4, 'c': 4, 'b': 2} 

Hoặc thậm chí chung chung hơn:

def make_merger(fappend=lambda x, y: x + y, fempty=lambda x: x): 
    def inner(*dicts): 
     res = dict((k, fempty(v)) for k, v 
      in dicts[0].iteritems()) # ".. in dicts[0]" for lists of tuples 
     for dic in dicts[1:]: 
      for key, val in dic.iteritems(): # ".. in dic" for lists of tuples 
       try: 
        res[key] = fappend(res[key], val) 
       except KeyError: 
        res[key] = fempty(val) 
     return res 
    return inner 

>>> make_merger()({'a':1, 'b':2}, {'a':3, 'c':4}) 
{'a': 4, 'c': 4, 'b': 2} 

>>> appender = make_merger(lambda x, y: x + [y], lambda x: [x]) 
>>> appender({'a':1, 'b':2}, {'a':3, 'c':4}, {'b':'BBB', 'c':'CCC'}) 
{'a': [1, 3], 'c': [4, 'CCC'], 'b': [2, 'BBB']} 

Ngoài ra bạn có thể phân lớp các dict và thực hiện một phương pháp __add__:

+1

Cảm ơn! Tuy nhiên, nó có vẻ ít rõ ràng hơn mã gốc. –

Các vấn đề liên quan