使用尴尬过滤嵌套字典中锯齿状列表中的元素

filtered elements in jagged list in nested dictionary using awkward

我有一个带有锯齿状列表的大型嵌套字典 'c' :

x = {'first_block': 
     {'unit1': {'a': (3,5,4), 'b': 23, 'c': [10]}, 
      'unit2': {'a': (5,8,7), 'b': 15, 'c': [20,10]}, 
      'unit10k': {'a': (2,4,9), 'b': 10, 'c': [6,10,20,5]}},
     
      'second_block': 
       {'unit1' : {'a': (8,20,14), 'b': 10, 'c': [17,12,9]}, 
        'unit2' : {'a': (9,25,50), 'b': 15, 'c': [17,15,9,4,12]}, 
        'unit12k': {'a': (12,24,9), 'b': 23, 'c': [12,22,15,4]}},
     
      'millionth_block': 
      {'unit1': {'a': (35,64,85), 'b': 64, 'c': [50]}, 
       'unit2': {'a': (56,23,34), 'b': 55, 'c': [89,59,77]},
       'unit5k': {'a': (90,28,12), 'b': 85, 'c': [48,90,27,59]}}}  

'c'的元素是点标签。

对于 'c' 中的每个唯一点标签,我想生成 'b'、

中相应值的过滤列表

例如 'first_block' 在 'c' 中有独特的元素:5, 6, 10, 20

并且我想 obtain/extract 每个 'block' 的以下列表,以列出 'b' 的每个值与 'c' 中的特定值相关联,例如

first_block:
5: [10]
6: [10]
10: [10,15,23]
20: [10,15]
second_block:
4: [15,23]
9: [10,15]
12: [10,15,23]
15: [15,23]
17: [10,15]
22: [23]
etc.

考虑到 'c' 是参差不齐的,关于如何创建此结果有任何想法吗?

一直在尝试通过转换为 Awkward 数组来做到这一点,但文档目前很少,而且真的不明白如何在 Awkward 中做到这一点。

也接受不涉及尴尬的pythonic建议

试试这个,它会准确再现您想要的内容(包括排序)

x = {'first_block': 
     {'unit1': {'a': (3,5,4), 'b': 23, 'c': [10]}, 
      'unit2': {'a': (5,8,7), 'b': 15, 'c': [20,10]}, 
      'unit10k': {'a': (2,4,9), 'b': 10, 'c': [6,10,20,5]}},
     
      'second_block': 
       {'unit1' : {'a': (8,20,14), 'b': 10, 'c': [17,12,9]}, 
        'unit2' : {'a': (9,25,50), 'b': 15, 'c': [17,15,9,4,12]}, 
        'unit12k': {'a': (12,24,9), 'b': 23, 'c': [12,22,15,4]}},
     
      'millionth_block': 
      {'unit1': {'a': (35,64,85), 'b': 64, 'c': [50]}, 
       'unit2': {'a': (56,23,34), 'b': 55, 'c': [89,59,77]},
       'unit5k': {'a': (90,28,12), 'b': 85, 'c': [48,90,27,59]}}}  

results = {}

for key in x.keys(): # Block level key
    results[key] = {}

    for unit in x[key].keys(): # Unit level key in subdict
        for value in x[key][unit]['c']: #List of values in c
            if value not in results[key].keys():
                #You assign a c level key, create a list
                results[key][value] = []

            #And append values from b
            results[key][value].append(x[key][unit]['b'])

    #You sort your dict by key/item
    results[key] = dict(sorted(results[key].items()))

for key in results:
    print (key)
    for value in results[key].keys():
        print (value,results[key][value])

输出:

first_block
5 [10]
6 [10]
10 [23, 15, 10]
20 [15, 10]
second_block
4 [15, 23]
9 [10, 15]
12 [10, 15, 23]
15 [15, 23]
17 [10, 15]
22 [23]
millionth_block
27 [85]
48 [85]
50 [64]
59 [55, 85]
77 [55]
89 [55]
90 [85]
x = {'first_block': 
 {'unit1': {'a': (3,5,4), 'b': 23, 'c': [10]}, 
  'unit2': {'a': (5,8,7), 'b': 15, 'c': [20,10]}, 
  'unit10k': {'a': (2,4,9), 'b': 10, 'c': [6,10,20,5]}},
 
  'second_block': 
   {'unit1' : {'a': (8,20,14), 'b': 10, 'c': [17,12,9]}, 
    'unit2' : {'a': (9,25,50), 'b': 15, 'c': [17,15,9,4,12]}, 
    'unit12k': {'a': (12,24,9), 'b': 23, 'c': [12,22,15,4]}},
 
  'millionth_block': 
  {'unit1': {'a': (35,64,85), 'b': 64, 'c': [50]}, 
   'unit2': {'a': (56,23,34), 'b': 55, 'c': [89,59,77]},
   'unit5k': {'a': (90,28,12), 'b': 85, 'c': [48,90,27,59]}}}  
   
result = {};
for blocks in x.keys():
    cs = {};
    for unit in x[blocks].keys():
       for c in x[blocks][unit]['c']:
            #retrieve array of b value for the c key or take [], then concat the b value, then apply set function to remove double, then convert to list
            cs[str(c)] = list(set(((cs.get(str(c)) or []) + [x[blocks][unit]['b']])))
    result[blocks] = cs;

print(result);

我会尝试给出一个笨拙的数组解决方案。

首先,我们需要知道您的数据集中的哪些内容真正在扩大规模。我知道你可能有数百万个块,但这意味着你不应该用具有唯一字符串值键的字典来表示它们。创建字符串、比较字符串和按字符串查找内容的成本(尽管字典的散列 table 对这有很大帮助)并不是扩展的好方法,特别是如果这些字符串中的唯一信息是订购。

在我看来,您的单元数量也是可变的(因为最后一个单元被命名为“unit10k”、“unit12k”和“unit5k”)。

最后,我把你的问题陈述重新看了好几次,但似乎名为“a”的字段没有进入问题。我会忽略它。

因此,我认为您的数据结构会更好:

as_python = [
    # first block
    [
        # unit 1
        {"b": 23, "c": [10]},
        # unit 2
        {"b": 15, "c": [20, 10]},
        # ...
        # unit 10k
        {"b": 10, "c": [6, 10, 20, 5]},
    ],
    # second block
    [
        # unit 1
        {"b": 10, "c": [17, 12, 9]},
        # unit 2
        {"b": 15, "c": [17, 15, 9, 4, 12]},
        # ...
        # unit 12k
        {"b": 23, "c": [12, 22, 15, 4]},
    ],
    # ...
    # millionth block
    [
        # unit 1
        {"b": 64, "c": [50]},
        # unit 2
        {"b": 55, "c": [89, 59, 77]},
        # ...
        # unit 15k
        {"b": 85, "c": [48, 90, 27, 59]},
    ],
]

对此的 Pythonic 解决方案是

results = []
for block in as_python:
    block_result = {}
    for unit in block:
        b = unit["b"]    # only look up b once
        for c in unit["c"]:
            if c not in block_result:
                block_result[c] = []
            block_result[c].append(b)
    results.append(block_result)

这导致

[
    {10: [23, 15, 10], 20: [15, 10], 6: [10], 5: [10]},
    {17: [10, 15], 12: [10, 15, 23], 9: [10, 15], 15: [15, 23], 4: [15, 23], 22: [23]},
    {50: [64], 89: [55], 59: [55, 85], 77: [55], 48: [85], 90: [85], 27: [85]},
]

应该很快。 (对于未编译的动态类型虚拟机,仅使用内置 Python 类型的短循环出奇地快。根据我的经验,仅使用内置 Python 类型是关键。)

至于 Awkward Array,在调用 Numba(JIT 编译的 for 循环)之前,我只能使用矢量化(即“NumPy 类”)操作完成一半,这表明库需要一个“ groupby”函数。

当然,您可以在 Numba 中完成所有事情,这往往是总体上最快的解决方案,但这个问题主要取决于分组和唯一性,这在 Python 中最自然地使用字典或集,但很难让 Numba 推断出纯数字和数组以外的任何类型。 (为了 JIT 编译代码并使其更快,Numba 必须知道它们的类型,但是 Python 类型注释足够新,它们仍然被合并到 Numba 中。)

所以让我们从将上面的数据结构变成一个笨拙的数组开始。根据您的数据源,有不同的方法来执行此操作(this part of the documentation 已完成),但我会让 ak.Array 迭代上面的 Python 数据结构。

>>> import awkward as ak
>>> as_awkward = ak.Array(as_python)

通常,如果我们有一个“X 和 Y 的所有组合”问题,我们要使用 ak.cartesian。我们不能在这里马上使用它,因为“b”数据和“c”数据的深度不同:

>>> as_awkward = ak.Array(as_python)
>>> as_awkward.b
<Array [[23, 15, 10], ... 23], [64, 55, 85]] type='3 * var * int64'>
>>> as_awkward.c
<Array [[[10], [20, 10], ... [48, 90, 27, 59]]] type='3 * var * var * int64'>
>>> as_awkward.type
3 * var * {"b": int64, "c": var * int64}
>>> as_awkward.b.type
3 * var * int64
>>> as_awkward.c.type
3 * var * var * int64

为了使它们匹配,我们可以使用 np.newaxis 创建一个新轴(a.k.a。None,但我喜欢明确):

>>> import numpy as np
>>> bprime = as_awkward.b[:, :, np.newaxis]
>>> bprime
<Array [[[23], [15], [10, ... 64], [55], [85]]] type='3 * var * 1 * int64'>
>>> bprime.type
3 * var * 1 * int64
>>> as_awkward.c.type
3 * var * var * int64

现在我们可以用每个单元中的笛卡尔积来制作 b-c 对列表 (axis=2)。

>>> combinations = ak.cartesian({"c": as_awkward.c, "b": bprime}, axis=2)
>>> combinations
<Array [[[{c: 10, b: 23}], ... c: 59, b: 85}]]] type='3 * var * var * {"c": int6...'>
>>> combinations.type
3 * var * var * {"c": int64, "b": int64}
>>> combinations.tolist()
[
    [
        [
            [{"c": 10, "b": 23}]
        ],
        [
            [{"c": 20, "b": 15}],
            [{"c": 10, "b": 15}]],
        [
            [{"c": 6, "b": 10}],
            [{"c": 10, "b": 10}],
            [{"c": 20, "b": 10}],
            [{"c": 5, "b": 10}],
        ],
    ],
    [
        [
            [{"c": 17, "b": 10}],
            [{"c": 12, "b": 10}],
            [{"c": 9, "b": 10}]
        ],
        [
            [{"c": 17, "b": 15}],
            [{"c": 15, "b": 15}],
            [{"c": 9, "b": 15}],
            [{"c": 4, "b": 15}],
            [{"c": 12, "b": 15}],
        ],
        [
            [{"c": 12, "b": 23}],
            [{"c": 22, "b": 23}],
            [{"c": 15, "b": 23}],
            [{"c": 4, "b": 23}],
        ],
    ],
    [
        [
            [{"c": 50, "b": 64}]
        ],
        [
            [{"c": 89, "b": 55}],
            [{"c": 59, "b": 55}],
            [{"c": 77, "b": 55}]
        ],
        [
            [{"c": 48, "b": 85}],
            [{"c": 90, "b": 85}],
            [{"c": 27, "b": 85}],
            [{"c": 59, "b": 85}],
        ],
    ],
]

现在我们的结构太多了:只要“b”和“c”值正确连接,你想在一个块中混合所有单元,因为你对每个单元中“c”的唯一性感兴趣.

>>> flattened = ak.flatten(combinations, axis=-1)
>>> flattened
<Array [[{c: 10, b: 23}, ... c: 59, b: 85}]] type='3 * var * {"c": int64, "b": i...'>
>>> flattened.type
3 * var * {"c": int64, "b": int64}
>>> flattened.tolist()
[
    [
        {"c": 10, "b": 23},
        {"c": 20, "b": 15},
        {"c": 10, "b": 15},
        {"c": 6, "b": 10},
        {"c": 10, "b": 10},
        {"c": 20, "b": 10},
        {"c": 5, "b": 10},
    ],
    [
        {"c": 17, "b": 10},
        {"c": 12, "b": 10},
        {"c": 9, "b": 10},
        {"c": 17, "b": 15},
        {"c": 15, "b": 15},
        {"c": 9, "b": 15},
        {"c": 4, "b": 15},
        {"c": 12, "b": 15},
        {"c": 12, "b": 23},
        {"c": 22, "b": 23},
        {"c": 15, "b": 23},
        {"c": 4, "b": 23},
    ],
    [
        {"c": 50, "b": 64},
        {"c": 89, "b": 55},
        {"c": 59, "b": 55},
        {"c": 77, "b": 55},
        {"c": 48, "b": 85},
        {"c": 90, "b": 85},
        {"c": 27, "b": 85},
        {"c": 59, "b": 85},
    ],
]

排序和唯一性处于 Awkward Array 功能的前沿;有些函数是在内部编写的,只是没有在 Python 中公开,更不用说记录了。幸运的是,ak.sort and ak.argsort 已记录在案。我们最终想要将这些 b-c 对按 c 分组,我们至少可以对它们进行排序:

>>> sorted = flattened[ak.argsort(flattened.c, axis=-1)]
>>> sorted
<Array [[{c: 5, b: 10}, ... {c: 90, b: 85}]] type='3 * var * {"c": int64, "b": i...'>
>>> sorted.type
3 * var * {"c": int64, "b": int64}
>>> sorted.tolist()
[
    [
        {"c": 5, "b": 10},
        {"c": 6, "b": 10},
        {"c": 10, "b": 23},
        {"c": 10, "b": 15},
        {"c": 10, "b": 10},
        {"c": 20, "b": 15},
        {"c": 20, "b": 10},
    ],
    [
        {"c": 4, "b": 15},
        {"c": 4, "b": 23},
        {"c": 9, "b": 10},
        {"c": 9, "b": 15},
        {"c": 12, "b": 10},
        {"c": 12, "b": 15},
        {"c": 12, "b": 23},
        {"c": 15, "b": 15},
        {"c": 15, "b": 23},
        {"c": 17, "b": 10},
        {"c": 17, "b": 15},
        {"c": 22, "b": 23},
    ],
    [
        {"c": 27, "b": 85},
        {"c": 48, "b": 85},
        {"c": 50, "b": 64},
        {"c": 59, "b": 55},
        {"c": 59, "b": 85},
        {"c": 77, "b": 55},
        {"c": 89, "b": 55},
        {"c": 90, "b": 85},
    ],
]

现在我们真的非常希望我们有一个“groupby”功能,但现在没有。这将是一个自然的添加,也许应该是 feature request.

所以在这一点上,我们切换到 Numba。这个问题比原来的问题简单得多,因为要分组的数据已经排序:我们只需要查看值何时更改以插入边界。笨拙的数组可以作为参数传递给 Numba,但它们是 immutable。要创建新数组,请使用 ak.ArrayBuilder。注意:当开发这样的函数时,请分步进行并删除 @nb.njit 以在没有 JIT 编译的情况下尝试它(在尝试解决类型错误之前确保您做的是正确的事情)。

import numba as nb

@nb.njit
def groupby(input, output):
    for block in input:
        output.begin_list()
        output.begin_list()
        last = block[0].c  # note: assumes that len(block) >= 1
        for unit in block:
            if unit.c != last:
                output.end_list()
                output.begin_list()
            output.append(unit)
            last = unit.c
        output.end_list()
        output.end_list()
    return output

input就是我们刚刚做的数组(sorted),output是一个ArrayBuilder,用snapshot变成一个真正的数组(在Numba).

>>> grouped = groupby(sorted, ak.ArrayBuilder()).snapshot()
>>> grouped
<Array [[[{c: 5, b: 10}], ... {c: 90, b: 85}]]] type='3 * var * var * {"c": int6...'>
>>> grouped.type
3 * var * var * {"c": int64, "b": int64}
>>> grouped.tolist()
[
    [
        [{"c": 5, "b": 10}],
        [{"c": 6, "b": 10}],
        [{"c": 10, "b": 23}, {"c": 10, "b": 15}, {"c": 10, "b": 10}],
        [{"c": 20, "b": 15}, {"c": 20, "b": 10}],
    ],
    [
        [{"c": 4, "b": 15}, {"c": 4, "b": 23}],
        [{"c": 9, "b": 10}, {"c": 9, "b": 15}],
        [{"c": 12, "b": 10}, {"c": 12, "b": 15}, {"c": 12, "b": 23}],
        [{"c": 15, "b": 15}, {"c": 15, "b": 23}],
        [{"c": 17, "b": 10}, {"c": 17, "b": 15}],
        [{"c": 22, "b": 23}],
    ],
    [
        [{"c": 27, "b": 85}],
        [{"c": 48, "b": 85}],
        [{"c": 50, "b": 64}],
        [{"c": 59, "b": 55}, {"c": 59, "b": 85}],
        [{"c": 77, "b": 55}],
        [{"c": 89, "b": 55}],
        [{"c": 90, "b": 85}],
    ],
]

然后您可以 fiddle 使用该输出来获得所需的结构。如果您希望每个列表“b”都有一个标量“c”,您需要使用 depth_limit 来防止 ak.zip 将其广播到最深层次。

>>> ak.zip({"c": grouped.c[:, :, 0], "b": grouped.b}, depth_limit=2).tolist()
[
    [
        {"c": 5, "b": [10]},
        {"c": 6, "b": [10]},
        {"c": 10, "b": [23, 15, 10]},
        {"c": 20, "b": [15, 10]},
    ],
    [
        {"c": 4, "b": [15, 23]},
        {"c": 9, "b": [10, 15]},
        {"c": 12, "b": [10, 15, 23]},
        {"c": 15, "b": [15, 23]},
        {"c": 17, "b": [10, 15]},
        {"c": 22, "b": [23]},
    ],
    [
        {"c": 27, "b": [85]},
        {"c": 48, "b": [85]},
        {"c": 50, "b": [64]},
        {"c": 59, "b": [55, 85]},
        {"c": 77, "b": [55]},
        {"c": 89, "b": [55]},
        {"c": 90, "b": [85]},
    ],
]

如果您想直接在 Numba 中构建该类型的报告,您可以这样做。 (这只是为了说明“begin_list”等正在做什么——这就像“打印出”一个结构,想象 output.begin_list() 打印一个 "["output.begin_record() 到打印一个"{",等等)

@nb.njit
def groupby(input, output):
    for block in input:
        output.begin_list()
        output.begin_record()
        output.field("c").integer(block[0].c)
        output.field("b")
        output.begin_list()
        last = block[0].c
        for unit in block:
            if unit.c != last:
                output.end_list()
                output.end_record()
                output.begin_record()
                output.field("c").integer(unit.c)
                output.field("b")
                output.begin_list()
            output.integer(unit.b)
            last = unit.c
        output.end_list()
        output.end_record()
        output.end_list()
    return output

>>> grouped = groupby(sorted, ak.ArrayBuilder()).snapshot()
>>> grouped
<Array [[{c: 5, b: [10]}, ... c: 90, b: [85]}]] type='3 * var * {"c": int64, "b"...'>
>>> grouped.type
3 * var * {"c": int64, "b": var * int64}
>>> grouped.tolist()
# it's the same

正如我上面所说,绝对最快的解决方案可能是在 Numba 中完成所有事情。笛卡尔积、展平和排序都创建了部分新数组(尽可能多地重用输入,这就是为什么笨拙的数组必须是 immutable 的原因),这涉及分配和多次传递数据。但是很难在 Numba 中表达涉及字典、列表和集合的问题,因为它需要类型化的字典、列表和集合。我尝试使用 Numba's typed dict,但它抱怨 dict 的值是不可散列的列表。 (Dict values 不需要是可哈希的,所以我想知道那里发生了什么。)正如唯一性和排序是 Awkward Array 的前沿一样,键入字典是 Numba 的前沿因为输入 Python 的概念本身相当新。

我没有对这些提议的解决方案中的任何一个进行性能测试。