在python或javascript中正确使用fold或reduce函数来处理长到宽的数据?

时间:2013-04-22 19:10:54

标签: javascript python functional-programming reshape fold

尝试学习像功能程序员一样思考 - 我想用我认为的折叠或减少操作来转换数据集。在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)

???

哦不!

1 个答案:

答案 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)))

我认为在dictset之间来回转换可能会降低可读性,这也意味着您的值必须是可清除的(对于您的示例数据来说不是问题,因为值都是字符串)...但它肯定更简洁。

当然,这将始终包含query作为一个共同字段,但我们稍后会处理。 (另外,您希望otherstuff成为一个list dict,因此我们会在其周围添加一对括号。)

与此同时,results与上述内容相同,只是filterkeys过滤掉了所有常用字段,而不是过滤掉除rankurl之外的所有内容。把它放在一起:

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(一次找到公共密钥,然后再次提取剩余的密钥)