尝试学习像功能程序员一样思考 - 我想用我认为的折叠或减少操作来转换数据集。在R中,我认为这是一个重塑操作,但我不确定如何翻译这种想法。
我的数据是一个json字符串,如下所示:
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是定义'row'的主键,嵌套对应于“rank”值和“url”字段的唯一“行”:
'[
{ "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"},
]}
]'
我知道我可以迭代,但我怀疑有一个功能操作可以进行这种转换,对吗?
也很想知道如何获得更像这样的东西,版本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)
???
哦不!
答案 0 :(得分:2)
我知道这不是你要求的折叠式解决方案,但我会用itertools
执行此操作,这同样具有功能性(除非你认为Haskell功能不如Lisp ......),也可能是解决这个问题的最恐怖的方式。
我们的想法是将您的序列视为一个惰性列表,并对其应用一系列延迟转换,直到您获得所需的列表。
这里的关键步骤是groupby
:
>>> initial = json.loads(s)
>>> groups = itertools.groupby(initial, operator.itemgetter('query'))
>>> print([key, list(group) for key, group in groups])
[('Q1',
[{'detail': 'cool', 'query': 'Q1', 'rank': 1, 'url': 'awesome1'},
{'detail': 'cool', 'query': 'Q1', 'rank': 2, 'url': 'awesome2'},
{'detail': 'cool', 'query': 'Q1', 'rank': 3, 'url': 'awesome3'}]),
('Q#2',
[{'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'}])]
您只需一步即可看到我们已经有多接近。
要将每个键,组对重组为您想要的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
。
对于您的后续操作,您希望将具有相同query
的每一行中的所有值相同,并将它们分组到otherstuff
,然后列出results
中剩余的任何内容。
因此,对于每个组,首先我们要获取公共密钥。我们可以通过迭代组中任何成员的键来做到这一点(第一个成员中没有的任何东西都不能在所有成员中),所以:
def common_fields(group):
def in_all_members(key, value):
return all(member[key] == value for member in group[1:])
return {key: value for key, value in group[0].items() if in_all_members(key, value)}
或者,或者......如果我们将每个成员转换为set
键值对而不是dict,那么我们就可以intersect
全部。这意味着我们最终会使用reduce
,所以让我们试试:
def common_fields(group):
return dict(functools.reduce(set.intersection, (set(d.items()) for d in group)))
我认为在dict
和set
之间来回转换可能会降低可读性,这也意味着您的值必须是可清除的(对于您的示例数据来说不是问题,因为值都是字符串)...但它肯定更简洁。
当然,这将始终包含query
作为一个共同字段,但我们稍后会处理。 (另外,您希望otherstuff
成为一个list
dict
,因此我们会在其周围添加一对括号。)
与此同时,results
与上述内容相同,只是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'}]}]
这显然不如原始版本那么简单,但希望这一切仍然有意义。只有两个新技巧。首先,我们必须多次迭代groups
(一次找到公共密钥,然后再次提取剩余的密钥)