在python或javascript中正确使用fold或reduce函数来处理从长到宽的数据?
试着多学一点函数式程序员的思维方式——我想用我认为是折叠或缩减操作的方式来转换数据集。在R中,我会认为这是一个重塑操作,但我不知道如何翻译这种想法 我的数据是一个json字符串,如下所示:在python或javascript中正确使用fold或reduce函数来处理从长到宽的数据?,javascript,python,functional-programming,reshape,fold,Javascript,Python,Functional Programming,Reshape,Fold,试着多学一点函数式程序员的思维方式——我想用我认为是折叠或缩减操作的方式来转换数据集。在R中,我会认为这是一个重塑操作,但我不知道如何翻译这种想法 我的数据是一个json字符串,如下所示: s = '[ {"query":"Q1", "detail" : "cool", "rank":1,"url":"awesome1"}, {"query&qu
s =
'[
{"query":"Q1", "detail" : "cool", "rank":1,"url":"awesome1"},
{"query":"Q1", "detail" : "cool", "rank":2,"url":"awesome2"},
{"query":"Q1", "detail" : "cool", "rank":3,"url":"awesome3"},
{"query":"Q#2", "detail" : "same", "rank":1,"url":"newurl1"},
{"query":"Q#2", "detail" : "same", "rank":2,"url":"newurl2"},
{"query":"Q#2", "detail" : "same", "rank":3,"url":"newurl3"}
]'
我想把它变成这样,其中query是定义“行”的主键,嵌套与“rank”值和“url”字段相对应的唯一“行”:
我知道我可以迭代,但我怀疑有一个函数操作可以完成这个转换,对吗
我也很想知道如何获得更像这样的版本2:
'[
{ "query" : "Q1",
"Common to all results" : [
{"detail" : "cool"}
],
"results" : [
{"rank" : 1, "url": "awesome1"},
{"rank" : 2, "url": "awesome2"},
{"rank" : 3, "url": "awesome3"}
]},
{ "query" : "Q#2",
"Common to all results" : [
{"detail" : "same"}
],
"results" : [
{"rank" : 1, "url": "newurl1"},
{"rank" : 2, "url": "newurl2"},
{"rank" : 3, "url": "newurl3"}
]}
]'
在第二个版本中,我希望将同一查询下重复的所有数据放入一个“其他内容”容器中,其中“rank”下唯一的所有项都位于“results”容器中
我正在mongodb中处理json对象,可以使用python或javascript来尝试这种转换
任何建议,如此转换的正确名称,在大型数据集上执行此操作的最快方法,都将不胜感激
编辑
结合下面@abarnert的优秀解决方案,尝试为处理同类问题的任何其他人获取上面的版本2,需要在一个级别下分叉一些键,在另一个级别下分叉其他键
以下是我尝试过的:
from functools import partial
groups = itertools.groupby(initial, operator.itemgetter('query'))
def filterkeys(d,mylist):
return {k: v for k, v in d.items() if k in mylist}
results = ((key, map(partial(filterkeys, mylist=['rank','url']),group)) for key, group in groups)
other_stuff = ((key, map(partial(filterkeys, mylist=['detail']),group)) for key, group in groups)
???
哦,不 我知道这不是您所要求的折叠式解决方案,但我会使用
itertools
,它同样具有功能性(除非您认为Haskell的功能不如Lisp…),而且可能是解决此问题最具python风格的方法
其思想是将序列视为一个惰性列表,并对其应用一系列惰性转换,直到获得所需的列表
这里的关键步骤是:
你可以一步看出我们已经很接近了
要重新构造每个密钥,请将密钥对分组为所需的dict格式:
>>> groups = itertools.groupby(initial, operator.itemgetter('query'))
>>> print([{"query": key, "results": list(group)} for key, group in groups])
[{'query': 'Q1',
'results': [{'detail': 'cool',
'query': 'Q1',
'rank': 1,
'url': 'awesome1'},
{'detail': 'cool',
'query': 'Q1',
'rank': 2,
'url': 'awesome2'},
{'detail': 'cool',
'query': 'Q1',
'rank': 3,
'url': 'awesome3'}]},
{'query': 'Q#2',
'results': [{'detail': 'same',
'query': 'Q#2',
'rank': 1,
'url': 'newurl1'},
{'detail': 'same',
'query': 'Q#2',
'rank': 2,
'url': 'newurl2'},
{'detail': 'same',
'query': 'Q#2',
'rank': 3,
'url': 'newurl3'}]}]
但是,等等,还有一些额外的字段需要清除。简单:
>>> groups = itertools.groupby(initial, operator.itemgetter('query'))
>>> def filterkeys(d):
... return {k: v for k, v in d.items() if k in ('rank', 'url')}
>>> filtered = ((key, map(filterkeys, group)) for key, group in groups)
>>> print([{"query": key, "results": list(group)} for key, group in filtered])
[{'query': 'Q1',
'results': [{'rank': 1, 'url': 'awesome1'},
{'rank': 2, 'url': 'awesome2'},
{'rank': 3, 'url': 'awesome3'}]},
{'query': 'Q#2',
'results': [{'rank': 1, 'url': 'newurl1'},
{'rank': 2, 'url': 'newurl2'},
{'rank': 3, 'url': 'newurl3'}]}]
唯一要做的就是调用json.dumps
,而不是print
在后续操作中,您希望使用相同的
查询
获取每行中相同的所有值,并将它们分组到其他内容
,然后列出结果中剩余的内容
因此,对于每个组,我们首先要获得公共密钥。我们可以通过迭代组中任何成员的键来实现这一点(不在第一个成员中的任何内容都不能在所有成员中),因此:
或者,或者……如果我们将每个成员转换为键值对的集合,而不是dict,那么我们就可以将它们全部相交。这意味着我们终于可以使用reduce
,所以让我们试试:
def common_fields(group):
return dict(functools.reduce(set.intersection, (set(d.items()) for d in group)))
我认为dict
和set
之间的来回转换可能会降低可读性,这也意味着您的值必须是可散列的(对于示例数据来说不是问题,因为这些值都是字符串)…但它肯定更简洁
当然,这将始终包括query
作为一个公共字段,但我们将在后面处理它。(另外,您希望otherstuff
成为一个带有一个dict
的list
,因此我们将在其周围加上一对括号)
同时,结果
与上述相同,只是filterkeys
过滤掉所有公共字段,而不是过滤掉除rank
和url
之外的所有内容。综合起来:
def process_group(group):
group = list(group)
common = dict(functools.reduce(set.intersection, (set(d.items()) for d in group)))
def filterkeys(member):
return {k: v for k, v in member.items() if k not in common}
results = list(map(filterkeys, group))
query = common.pop('query')
return {'query': query,
'otherstuff': [common],
'results': list(results)}
现在我们只使用这个函数:
>>> groups = itertools.groupby(initial, operator.itemgetter('query'))
>>> print([process_group(group) for key, group in groups])
[{'otherstuff': [{'detail': 'cool'}],
'query': 'Q1',
'results': [{'rank': 1, 'url': 'awesome1'},
{'rank': 2, 'url': 'awesome2'},
{'rank': 3, 'url': 'awesome3'}]},
{'otherstuff': [{'detail': 'same'}],
'query': 'Q#2',
'results': [{'rank': 1, 'url': 'newurl1'},
{'rank': 2, 'url': 'newurl2'},
{'rank': 3, 'url': 'newurl3'}]}]
这显然不像最初的版本那么简单,但希望它仍然有意义。只有两个新技巧。首先,我们必须多次迭代组
(一次查找公共密钥,然后再次提取剩余密钥)您在这里提出了两个不同的问题。你想知道功能性折叠风格的实现方式,还是最快的实现方式?我更喜欢两者,但会权衡:如果不是太慢,如果它们是相反的目标,我想学习功能性风格?它们不一定是相反的目标,但它们是完全不同的目标,至少可能相互矛盾。此外,要求、甚至思考最快的做事方式几乎从来都不是一个好主意。首先,获得正确且可读的内容,除了避免明显的big-O算法问题外,不要担心性能。然后,如果您的代码太慢,请对其进行分析,然后重写瓶颈以加快速度,让其余代码尽可能可读。这很有意义,谢谢。我很感激你的守则和建议。我可能错误地认为功能性解决方案通常比手工更快?但我完全理解,即使这是一个个案的事情,我应该把问题分开一点更好!对于大多数函数算法,有一个命令式版本具有相同的big-O复杂性,反之亦然。但是在Python中,由于其相对较高的解释器开销,一些函数解决方案通常更快(在调用map
的内部循环,或者列表理解比显式for
循环更快),一些通常更慢(尾部递归与迭代),还有一些差不多。而且有些本来就更容易或更难在其他方面进行优化(例如,转换不可变对象,而不是就地改变对象,这意味着您可以进行CPU并行化)。因此,这肯定是正确的答案,但我可以问一个后续问题,以正确的格式获得我所问的版本2,扩展您提供的优秀解决方案吗?编辑原始问题。请求似乎不明确。在您的示例中,所有行都有相同的额外字段
def common_fields(group):
return dict(functools.reduce(set.intersection, (set(d.items()) for d in group)))
def process_group(group):
group = list(group)
common = dict(functools.reduce(set.intersection, (set(d.items()) for d in group)))
def filterkeys(member):
return {k: v for k, v in member.items() if k not in common}
results = list(map(filterkeys, group))
query = common.pop('query')
return {'query': query,
'otherstuff': [common],
'results': list(results)}
>>> groups = itertools.groupby(initial, operator.itemgetter('query'))
>>> print([process_group(group) for key, group in groups])
[{'otherstuff': [{'detail': 'cool'}],
'query': 'Q1',
'results': [{'rank': 1, 'url': 'awesome1'},
{'rank': 2, 'url': 'awesome2'},
{'rank': 3, 'url': 'awesome3'}]},
{'otherstuff': [{'detail': 'same'}],
'query': 'Q#2',
'results': [{'rank': 1, 'url': 'newurl1'},
{'rank': 2, 'url': 'newurl2'},
{'rank': 3, 'url': 'newurl3'}]}]