交叉和合并许多词典的最快方法

Fastest way to intersect and merge many dictionaries

我有 1000 部词典(grid_1grid_2 ... grid_1000)存储为 pickle 对象(由之前的某个过程生成)和一部参考词典。我需要将每个 pickled 词典与参考文献进行比较,然后合并结果。

输入词典可能如下所示:

grid_1 = {'143741': {'467457':1,'501089':2,'903718':1,'999216':5,'1040952':2},'281092':{'1434': 67,'3345': 345}, '33123': {'4566':5,'56788':45}}

grid_2 = {'143741': {'467457':5,'501089':7,'1040952':9},'281092':{'1434': 67,'3345': 20}, '33123': {'4566':7,'56788':38}}

参考词典可能如下所示:

grid_density_original = {'143741': {'467457':1,'501089':2,'903718':1,'999216':5,'9990':4},'281092':{'1434': 60,'3345': 3,'9991': 43}, '33123': {'56788':4}}

在第一步中,我们应该将单个 grid_n 指令相交,例如:

# intersection of grid_1 and grid_density_original
assert intersect_1 == {'143741': {'467457':1,'501089':2,'903718':1,'999216':5},'281092':{'1434': 67,'3345': 345}, '33123': {'56788':45}
# intersection of grid_2 and grid_density_original
assert intersect_2 == {'143741': {'467457':5,'501089':7},'281092':{'1434': 67,'3345': 20}, '33123': {'56788':38}}

那么这些结果应该合并起来,如下:

assert combine12 == {'143741': {'467457':[1,5],'501089':[2,7],'903718':[1,99999],'999216':[5,99999]},'281092':{'1434': [67,67],'3345': [345,20]}, '33123': {'56788':[45,38]}

这似乎是缓慢的部分,因为每次添加新 intersect_n 时内部列表的大小都会增加。

这是我目前拥有的代码。我的实际字典有大约 10,000 个键,这段代码需要大约 4 天才能 运行。

from collections import defaultdict
from collections import Counter
import pickle
import gc
import copy
import pickle
import scipy.stats as st
from collections import defaultdict


# grid_density_orignal is original nested dictionary we compare each of 1000 grids to: 
with open('path\grid_density_original_intercountry.pickle','rb') as handle:
    grid_density_orignal = pickle.load(handle,encoding ='latin-1')  

    
# Previous process generated 1000 grids and dump them as .pickle files : grid_1,grid_2....grid_1000 

for iteration in range(1,1001):
    # load each grid i.e.grid_1,grid_2...grid_1000 into memory sequentially 
    filename = 'path\grid_%s' %iteration
    with open(filename,'rb') as handle:
        globals()['dictt_%s' % iteration] = pickle.load(handle,encoding ='latin-1') 
        
    # Counter to store grid-grids densities: same dictionary structure as grid_density_orignal
    globals()['g_den_%s' % iteration] = defaultdict(list)
    for k,v in globals()['dictt_%s' % iteration].items():
        globals()['g_den_%s' % iteration][k] = Counter(v)
     
    # here we find the common grid-grid connections between grid_density_orignal and each of the 1000 grids
    globals()['intersect_%s' % iteration] = defaultdict(list)
    for k,v in grid_density_orignal.items():
        pergrid = defaultdict(list)
        common_grid_ids = v.keys() & globals()['g_den_%s' % iteration][k].keys()
        for gridid in common_grid_ids:
            pergrid[gridid] = globals()['g_den_%s' % iteration][k][gridid]
        globals()['intersect_%s' % iteration][k] = pergrid

    
print('All 1000 grids intersection done')


# From previous code we now have 1000 intersection grids : intersect_1,intersect_2 ...... intersect_1000
for iteration in range(1,1000):

    itnext = iteration +1         # to get next intersect out of 1000
    globals()['combine_%s%s' %(iteration,itnext)] = defaultdict(list)  # dictionary to store intermediate combine step results between 2 intersects : intersect_x and intersect_x+1
    
    
  
   
    for k,v in globals()['intersect_%s' %iteration].items():
        innr = []
        combine = defaultdict(list)
        for key in set(list(globals()['intersect_%s' % iteration][k].keys())+ list(globals()['intersect_%s' % itnext][k].keys())):  # key in the union of intersect_1 , intersect_2
            
            if (isinstance(globals()['intersect_%s' % iteration][k].get(key,99999), int) and isinstance(globals()['intersect_%s' % itnext][k].get(key,99999), int)): # get key value if exists, if for e.g. a certain grid doesnt exist in intersect_1, intersect_2 we give it default of 99999 as placeholder, alos check if value is an instance of int or list as in intial step it is an int but later we get lists after combining every 2 intersects 
                
                
                combine[key] = [globals()['intersect_%s' % iteration][k].get(key,99999)] + [globals()['intersect_%s' % itnext][k].get(key,99999)]   # combine into list intersect_1, intersect_2
                
            
            if (isinstance(globals()['intersect_%s' % iteration][k].get(key,99999), list) and isinstance(globals()['intersect_%s' % itnext][k].get(key,99999), int)): # this condition will be reached after initial step of intersect_1 + intersect_2
                
                combine[key] = globals()['intersect_%s' % iteration][k].get(key,99999) + [globals()['intersect_%s' % itnext][k].get(key,99999)]    # combine into list intersect_1, intersect_2

                
        globals()['combine_%s%s' %(iteration,itnext)][k] = combine
        
    globals()['intersect_%s' % itnext] = copy.copy(globals()['combine_%s%s' %(iteration,itnext)])   # copy combine dict onto intersect dict so we can continue combining this dict with the next iteration

    print('2 combined: ',iteration,itnext)
    del globals()['intersect_%s' % iteration]                      # delete older intersect, combine as we dont need them and may cause memory overflow when more dicts are in memory
    del globals()['combine_%s%s' %(iteration,itnext)]
    gc.collect()                                                   # explicitly call the garbage collector as too big for ram

            
   
print('intersection and combine done')  # at the end we have intersect_1000 with is a dict with all grid_id ids as keys and a list of densities (list size is 1000 corresponding to 1000 grids)

如何提高代码的性能?

我将专注于第二个循环(合并 intersect_n 指令),提供一些一般性建议,其余的作为练习。我的最终结果是 so 比原来的要短得多,我觉得有必要把这个过程分解成几个步骤。希望您在此过程中学到很多有用的技术。

在删除空白行、注释(我们稍后会重写这些)和调试跟踪并清理一些空白之后,我们开始:

for iteration in range(1, 1000):
    itnext = iteration + 1
    globals()['combine_%s%s' % (iteration, itnext)] = defaultdict(list)
    for k, v in globals()['intersect_%s' % iteration].items():
        innr = []
        combine = defaultdict(list)
        for key in set(list(globals()['intersect_%s' % iteration][k].keys()) + list(globals()['intersect_%s' % itnext][k].keys())):
            if (isinstance(globals()['intersect_%s' % iteration][k].get(key, 99999), int) and isinstance(globals()['intersect_%s' % itnext][k].get(key, 99999), int)):
                combine[key] = [globals()['intersect_%s' % iteration][k].get(key, 99999)] + [globals()['intersect_%s' % itnext][k].get(key, 99999)]
            if (isinstance(globals()['intersect_%s' % iteration][k].get(key, 99999), list) and isinstance(globals()['intersect_%s' % itnext][k].get(key, 99999), int)):
                combine[key] = globals()['intersect_%s' % iteration][k].get(key, 99999) + [globals()['intersect_%s' % itnext][k].get(key, 99999)]
        globals()['combine_%s%s' % (iteration, itnext)][k] = combine
    globals()['intersect_%s' % itnext] = copy.copy(globals()['combine_%s%s' % (iteration, itnext)])
    del globals()['intersect_%s' % iteration]
    del globals()['combine_%s%s' % (iteration, itnext)]
    gc.collect()

现在我们可以开始工作了。

1。正确构建数据

Trying to create variable variables is generally a bad idea。它还具有性能成本:

$ python -m timeit -s "global x_0; x_0 = 'test'" "globals()['x_%s' % 0]"
2000000 loops, best of 5: 193 nsec per loop
$ python -m timeit -s "global x; x = ['test']" "x[0]"
10000000 loops, best of 5: 29.1 nsec per loop

是的,我们谈论的是纳秒,但现有代码几乎每次访问都在不断地这样做。但更重要的是,视觉上简化的代码更易于分析以进行后续改进。

显然我们已经知道如何操作嵌套数据结构;增加一层嵌套不是问题。要存储 intersect_n 结果,而不是拥有 1000 个动态命名的变量,显而易见的解决方案是只制作一个包含 1000 个元素的列表,其中每个元素都是这些结果之一。 (当然,请注意,我们将从 0 而不是 1 开始计算它们。)至于 globals()['combine_%s%s' % (iteration, itnext)] - 这没有意义;我们不需要每次都创建一个新的变量名,因为无论如何我们都会在循环结束时丢弃这些数据。所以让我们只使用一个常量名称。

一旦修改了第一个循环以提供正确的数据(这部分看起来也会简单得多),这里的访问看起来就简单多了:

for iteration in range(999):
    itnext = iteration + 1
    combine_overall = defaultdict(list)
    for k, v in intersect[iteration].items():
        combine = defaultdict(list)
        for key in set(list(intersect[iteration][k].keys()) + list(intersection[itnext][k].keys())):
            if (isinstance(intersect[iteration][k].get(key, 99999), int) and isinstance(intersect[itnext][k].get(key, 99999), int)):
                combine[key] = [intersect[iteration][k].get(key, 99999)] + [intersect[itnext][k].get(key, 99999)]
            if (isinstance(intersect[iteration][k].get(key, 99999), list) and isinstance(intersect[itnext][k].get(key, 99999), int)):
                combine[key] = intersect[iteration][k].get(key, 99999) + [intersect[itnext][k].get(key, 99999)]
        combine_overall[k] = combine
    intersect[itnext] = copy.copy(combine_overall)

你会注意到我在最后删除了内存管理的东西。稍后我将讨论更好的方法。 iteration 值的 del 会搞乱对该列表的迭代,我们不需要删除 combine_overall 因为我们将用一个新的空 defaultdict 替换它。我还偷偷删除了 innr = [],因为这个值根本就没有用过。就像我说的:视觉上更简单的代码更容易分析。

2。不必要的类型检查

所有这些 isinstance 内容很难阅读,而且很耗时,尤其是考虑到所有重复访问:

$ python -m timeit -s "global x; x = {'a': {'b': {'c': 0}}}" "isinstance(x['a']['b'].get('c', 0), int)"
2000000 loops, best of 5: 138 nsec per loop
$ python -m timeit -s "global x; x = {'a': {'b': {'c': 0}}}" "x['a']['b'].get('c', 0)"
5000000 loops, best of 5: 83.9 nsec per loop

我们知道 intersect[itnext][k].get(key, 99999) 应该是 int 的确切条件:总是,否则数据只是损坏了。 (我们可以稍后再担心这个问题,并且可能通过在调用代码中进行异常处理来解决。)我们知道 intersect[iteration][k].get(key, 99999) 应该是 intlist 的条件:它将是第一次通过 int(或缺失),每隔一次 list(或缺失)。解决这个问题也会让下一步更容易理解。

for iteration in range(999):
    itnext = iteration + 1
    combine_overall = defaultdict(list)
    for k, v in intersect[iteration].items():
        combine = defaultdict(list)
        for key in set(list(intersect[iteration][k].keys()) + list(intersection[itnext][k].keys())):
            if iteration == 0:
                combine[key] = [intersect[iteration][k].get(key, 99999)] + [intersect[itnext][k].get(key, 99999)]
            else:
                combine[key] = intersect[iteration][k].get(key, [99999]) + [intersect[itnext][k].get(key, 99999)]
        combine_overall[k] = combine
    intersect[itnext] = copy.copy(combine_overall)

请注意,当键是列表或缺失时,我们如何使用列表作为默认值。这就是保持类型一致性并使以这种方式编写代码成为可能的技巧。

3。不必要的复制和不必要的 pair-wise 迭代

由于 combine_overall 未在其他任何地方引用,我们实际上不需要将其复制到 intersect[itnext] 值上 - 我们可以重新分配它而不会出现任何别名问题。但更好的是 将其留在原处 。我们没有考虑成对合并在一起的相邻 iteration 值对,我们只是将所有内容合并到 combine_overall,一次一个(并设置初始 defaultdict 一次而不是覆盖它) .

这确实意味着我们必须做一些设置工作 - 而不是 special-casing 第一次合并,我们将“合并” intersect[0] 本身到 [=25 的初始状态=].

combine_overall = defaultdict(list)
for k, v in intersect[0].items():
    combine = defaultdict(list)
    for key, value in v.keys():
        combine[key] = [value]
    combine_overall[k] = combine

for iteration in range(999):
    itnext = iteration + 1
    for k, v in combine_overall.items():
        combine = defaultdict(list)
        for key in set(list(combine_overall[k].keys()) + list(intersection[itnext][k].keys())):
            combine[key] = combine_overall[k].get(key, [99999]) + [intersect[itnext][k].get(key, 99999)]
        combine_overall[k] = combine

请注意我们可以更简单地完成初始步骤 - 我们知道我们正在使用哪些键,因此不需要 .get;而且只有一个字典,所以不需要合并 key-sets。但我们还没有完成...

4。一些杂项清理

看这个版本,我们更容易注意到:

  • iteration 循环根本不使用 iteration,而只使用 itnext - 所以我们可以解决这个问题。此外,没有理由在简单循环中使用这样的索引 - 我们 should directly iterate over elements.
  • combine_overall 将保存字典,而不是列表(因为我们从 combine 分配值,这是一个 defaultdict);所以 defaultdict(list) 没有意义。
  • 我们可以直接修改 combine_overall[k],而不是使用临时 combine 来构建 combine_overall[k] 的替代品,然后将其分配回去。这样,我们实际上可以从 defaultdict 行为中获益。我们实际上希望默认值本身就是 defaultdicts - 不是完全简单,但非常可行。
  • 由于我们不再需要区分整体组合结果和单个结果,我们可以将 combine_overall 重命名为 combine 以看起来更简洁。
combine = defaultdict(lambda: defaultdict(list))
for k, v in intersect[0].items():
    for key, value in v.keys():
        combine[k][key] = [value]

for to_merge in intersect[1:]:
    for k, v in combine.items():
        for key in set(list(combine[k].keys()) + list(to_merge[k].keys())):
            combine[k][key] = combine[k].get(key, [99999]) + [to_merge[k].get(key, 99999)]

5。糟糕,一直有一个错误。此外,“特殊情况不足以违反规则”

希望您觉得这有点奇怪。为什么我们在 defaultdict 上使用 .get?为什么我们会有这个single-item 占位符值,而不是空列表?为什么我们必须对可能使用的 key 进行这种复杂的检查?我们真的需要以不同方式处理第一个 intersect 值吗?

考虑以下数据会发生什么(使用原始命名约定):

intersect_1 = {'1': {'1': 1}}
intersect_2 = {'1': {'1': 1}}
intersect_3 = {'1': {'1': 1, '2': 1}}

使用原来的方法,我得到如下结果:

$ python -i tmp.py 
>>> intersect_3
defaultdict(<class 'list'>, {'1': defaultdict(<class 'list'>, {'2': [99999, 1], '1': [1, 1, 1]})})

糟糕。 intersect_3['1']['2'] 只有两个元素 ([99999, 1]),因此与 intersect_3['1']['1'] 不匹配。这违背了 99999 占位符值的目的。问题是,因为该值在开始时多次丢失,所以应该插入多个 99999 值,但只有一个是 - 来自创建初始列表的那个,当 isinstance当它用 .get 检索 99999 时,检查报告了一个 int 而不是一个列表。丢失的信息:我们无法区分丢失的 int 和丢失的列表。

我们如何解决这个问题?简单:我们每次都使用相同的 overall 键集——应该存在的 total 组键,我们从 grid_density_original[k].每当任何 intersect 结果中缺少其中一个条目时,我们都会写入占位符值。现在我们也以相同的方式处理每个 intersect 结果——不是用第一个值进行特殊设置,然后合并其他所有内容,而是将 所有内容 合并到一个 初始状态。

我们没有遍历 combine.items(并期望 to_merge 具有相同的键),而是遍历 to_merge,这使得更多感觉。我们没有为 combine[k][key] 创建和分配一个列表,而是简单地将一个值附加到现有列表(我们知道有一个可用的,因为我们正在正确使用 defaultdicts 现在)。

因此:

combine = defaultdict(lambda: defaultdict(list))
for to_merge in intersect:
    for k, v in to_merge.items():
        # BTW, you typo'd this as "orignal" in the original code
        for key in grid_density_original[k].keys():
            combine[k][key].append(v.get(key, 99999))

(这确实意味着,如果 intersect 字典中的 none 包含特定的 key,结果将包含一个列表1000 99999 个值,而不是完全省略密钥。我希望这不是问题。)

6.好的,但是你不打算对内存使用做些什么吗?

哦,对了。请花点时间写下第一个循环的相应代码。

明白了吗?好的,现在。先设置combine;然后每次计算 intersect 的 would-be 元素之一时, 将其合并到 中(使用此处显示的两个内部循环)而不是构建实际的 intersect列表。

哦,我想你会发现 - 因为我们要遍历 grid_density_original[k].keys() 无论如何 - 从 中删除其他键的预处理=84=] 结果 现在根本不需要