使用尴尬过滤嵌套字典中锯齿状列表中的元素
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 的概念本身相当新。
我没有对这些提议的解决方案中的任何一个进行性能测试。
我有一个带有锯齿状列表的大型嵌套字典 '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 的概念本身相当新。
我没有对这些提议的解决方案中的任何一个进行性能测试。