¿Uso correcto de una función de plegado o reducción para datos de largo a ancho en python o javascript?

Tratar de aprender a pensar como un progtwigdor funcional un poco más. Me gustaría transformar un conjunto de datos con lo que creo que es una operación de pliegue o de reducción. En R, pensaría en esto como una operación de remodelación, pero no estoy seguro de cómo traducir ese pensamiento.

Mis datos son una cadena json que se ve así:

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"} ]' 

Me gustaría convertirlo en algo como esto, donde la consulta es la clave maestra que define la “fila”, anidando las “filas” únicas correspondientes a los valores de “rango” y los campos de “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"}, ]} ]' 

Sé que puedo recorrerlo, pero sospecho que hay una operación funcional que realiza esta transformación, ¿verdad?

También sería curioso saber cómo llegar a algo más como esto, Versión 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"} ]} ]' 

En esta segunda versión, me gustaría tomar todos los datos que se repiten en la misma consulta, y colocarlos en un contenedor de “otras cosas”, donde todos los elementos únicos en “rango” estarían en el contenedor de “resultados”.

Estoy trabajando en los objetos json en mongodb, y puedo usar python o javascript para probar esta transformación.

Cualquier consejo, como el nombre propio de esta transformación, ¡cuál podría ser la forma más rápida de hacer esto en un gran conjunto de datos, se agradece!

EDITAR

Incorporando la excelente solución de @abarnert a continuación, tratando de obtener mi Versión 2 anterior para cualquier otra persona que trabaje en el mismo tipo de problema, lo que requiere bifurcar algunas claves en un nivel, otras claves en otro …

Esto es lo que intenté:

 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) ??? 

¡Oh no!

Sé que esta no es la solución de estilo plegable que estaba pidiendo, pero lo haría con itertools , que es igual de funcional (a menos que piense que Haskell es menos funcional que Lisp …), y también es probablemente la forma más pythonica de hacerlo. resuelve esto.

La idea es pensar en su secuencia como una lista perezosa, y aplicarle una cadena de transformaciones perezosas hasta que obtenga la lista que desea.

El paso clave aquí es 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'}])] 

Puedes ver lo cerca que ya estamos, en un solo paso.

Para reestructurar cada clave, agrupe el par en el formato dict que desee:

 >>> 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'}]}] 

Pero espera, todavía hay esos campos adicionales de los que quieres deshacerte. Fácil:

 >>> 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'}]}] 

Lo único que queda por hacer es llamar a json.dumps lugar de print .


Para su seguimiento, desea tomar todos los valores que sean idénticos en todas las filas con la misma query y agruparlos en otras otherstuff , y luego enumerar lo que quede en los results .

Entonces, para cada grupo, primero queremos obtener las claves comunes. Podemos hacerlo mediante la iteración de las claves de cualquier miembro del grupo (cualquier cosa que no esté en el primer miembro no puede estar en todos los miembros), por lo que:

 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)} 

O, alternativamente … si convertimos a cada miembro en un set de pares clave-valor, en lugar de un dictado, podemos intersect todos. Y esto significa que finalmente podemos usar reduce , así que intentemos eso:

 def common_fields(group): return dict(functools.reduce(set.intersection, (set(d.items()) for d in group))) 

Creo que la conversión entre dict y set puede hacer que esto sea menos legible, y también significa que sus valores deben ser hashable (no es un problema para sus datos de muestra, ya que los valores son todas cadenas) … pero ciertamente es más conciso .

Esto, por supuesto, siempre incluirá la query como un campo común, pero trataremos eso más adelante. (Además, deseaba que otherstuff fueran una list con un solo dict , así que otherstuff un par de soportes adicionales a su alrededor).

Mientras tanto, los results son los mismos que los anteriores, excepto que las filterkeys filtro filterkeys todos los campos comunes, en lugar de filtrar todo menos el rank y la url . Poniendo todo junto:

 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)} 

Entonces, ahora solo usamos esa función:

 >>> 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'}]}] 

Obviamente, esto no es tan trivial como la versión original, pero ojalá todo tenga sentido. Solo hay dos nuevos trucos. Primero, tenemos que iterar sobre los groups varias veces (una vez para encontrar las claves comunes y luego otra vez para extraer las claves restantes)