Tengo datos que llegan como diccionarios de listas. De hecho, leí en una lista de ellos...
data = [ { 'key1': [101, 102, 103], 'key2': [201, 202, 203], 'key3': [301, 302, 303], }, { 'key2': [204], 'key3': [304, 305], 'key4': [404, 405, 406], }, { 'key1': [107, 108], 'key4': [407], }, ]Cada diccionario puede tener diferentes claves.
Cada tecla se asocia a una lista, de longitud variable.
Lo que me gustaría hacer es hacer un solo diccionario, concatenando las listas que comparten una clave...
desired_result = { 'key1': [101, 102, 103, 107, 108], 'key2': [201, 202, 203, 204], 'key3': [301, 302, 303, 304, 305], 'key4': [404, 405, 406, 407], }Notas:
Puedo hacer esto, con comprensiones, pero se siente muy torpe, y en realidad es muy lento (recorrer todas las teclas posibles para cada diccionario posible produce más 'errores' que 'aciertos')...
{ key: [ item for d in data if key in d for item in d[key] ] for key in set( key for d in data for key in d.keys() ) } # TimeIt gives 3.2, for this small data set Una opción más corta y más fácil de leer/mantener es simplemente recorrer todo. Pero el rendimiento aún apesta (¿posiblemente debido a la gran cantidad de llamadas a extend() , lo que obliga a la reasignación frecuente de memoria a medida que se llenan las listas sobreaprovisionadas?) ...
from collections import defaultdict result = defaultdict(list) for d in data: for key, val in d.items(): result[key].extend(val) # TimeIt gives 1.7, for this small data set¿Hay una mejor manera?
Alternativamente, ¿existe una estructura de datos más aplicable para este tipo de proceso?
Editar: se agregó el tiempo para el conjunto de datos pequeños
res = {} for d in data: for k, v in d.items(): # Adds `key1` to `res` with empty list, if `key1` is not there yet. # Extends list under `key1` with new portion of data contained in `v`. res.setdefault(k, []).extend(v)Resultado:
{'key1': [101, 102, 103, 107, 108], 'key2': [201, 202, 203, 204], 'key3': [301, 302, 303, 304, 305], 'key4': [404, 405, 406, 407]} ACTUALIZACIÓN: Me gustaría evitar comparar el rendimiento de setdefault y defaultdict , pero hubo una discusión en los comentarios e hice algunas pruebas para esos datos en particular, usando python 3.10.
TL; DR: Setdefault más rápido defaultdict alrededor del 13% para ese caso en particular.
El resultado de la prueba:
defaultldict [1.633565943, 1.590108738, 1.6549000220000005, 1.622328843, 1.6121867709999993] setdefault [1.4336988549999994, 1.4056579070000002, 1.4107502079999996, 1.408643755, 1.433823878]El código de la prueba:
import timeit from collections import defaultdict data = [ { 'key1': [101, 102, 103], 'key2': [201, 202, 203], 'key3': [301, 302, 303], }, { 'key2': [204], 'key3': [304, 305], 'key4': [404, 405, 406], }, { 'key1': [107, 108], 'key4': [407], }, ] def setdefault(): res = {} for d in data: for k, v in d.items(): res.setdefault(k, []).extend(v) def default(): res = defaultdict(list) for d in data: for k, v in d.items(): res[k].extend(v) if __name__ == '__main__': print('defaultldict', timeit.repeat(stmt=default, repeat=5, number=1000000, globals={'data': data})) print('setdefault', timeit.repeat(stmt=setdefault, repeat=5, number=1000000, globals={'data': data}))Tal vez podría usar la comprensión de listas y pandas.
No tengo idea si esta es una respuesta válida, o cómo funciona, pero funciona, para el pequeño conjunto de datos de ejemplo de todos modos.
import pandas as pd data = [ { "key1": [101, 102, 103], "key2": [201, 202, 203], "key3": [301, 302, 303], }, { "key2": [204], "key3": [304, 305], "key4": [404, 405, 406], }, { "key1": [107, 108], "key4": [407], }, ] dics = [pd.DataFrame.from_dict(el, orient="index") for el in data] dics_concat = pd.concat(dics).fillna("Empty") dics_concat["key"] = dics_concat.index dics_concat = dics_concat.groupby("key").agg(list) combined = dics_concat.apply( lambda row: sorted( [item for sublist in row for item in sublist if item != "Empty"] ), axis=1, ) print(combined.to_dict()) {'key1': [101, 102.0, 103.0, 107, 108.0], 'key2': [201, 202.0, 203.0, 204], 'key3': [301, 302.0, 303.0, 304, 305.0], 'key4': [404, 405.0, 406.0, 407]}Sin pd.concat.
df = pd.DataFrame(data).fillna("") combined = df.apply( lambda row: sorted( [item for sublist in row for item in sublist if item != "Empty"] ), axis=0, ) print(combined.to_dict())Tengo una solución que parece lograr buenos resultados cuando las listas en sus diccionarios son largas. Aunque no es el caso en su situación, todavía lo menciono.
La idea, como se menciona en mis comentarios, es usar append en lugar de extend en un primer ciclo y luego concatenar todas las listas en el diccionario resultante usando itertools.chain . Esta es la chain de funciones definida a continuación. También agregué el código de @ mrvol en mi respuesta para comparar.
Aquí está el código:
import timeit import itertools from collections import defaultdict REPEAT = 5 # parameter repeat of timeit NUMBER = 100 # parameter number of timeit DICTS = 100 # number of dictionaries in our data KEYS = 12 # size of the dictionaries in our data LEN = 1_000 # size of the lists in our dictionaries data = [ { f'key{x}': [x * 100 + y for y in range(LEN)] for x in range(KEYS) } for _ in range(DICTS) ] def setdefault(): res = {} for d in data: for k, v in d.items(): res.setdefault(k, []).extend(v) return res def default(): res = defaultdict(list) for d in data: for k, v in d.items(): res[k].extend(v) return res def chain(): res = dict() for d in data: for k, v in d.items(): res.setdefault(k, []).append(v) for key in res: res[key] = list(itertools.chain.from_iterable(res[key])) return res # check that all produce the same result assert chain() == default() assert setdefault() == default() if __name__ == '__main__': for name, fun in [ ('default', default), ('setdefault', setdefault), ('chain', chain) ]: print(name, timeit.repeat( stmt=fun, repeat=REPEAT, number=NUMBER, globals={'data': data} ))Todas las pruebas a continuación se han realizado con python 3.10.
Aquí están los resultados:
default [3.0608591459999843, 2.9533347530000356, 3.204700414999934, 2.934139603999938, 2.854463246000023] setdefault [2.7814459759999863, 2.801596405000055, 2.796927817000096, 2.797430740999971, 2.795393482999998] chain [2.336767712999972, 2.33148793700002, 2.3378432869999415, 2.3322470529999464, 2.3312841169999956] Si aumentamos LEN a 10_000 la diferencia es más impresionante:
default [33.63351462200012, 33.598145768999984, 33.83524595699987, 33.732721158000004, 33.785992579999856] setdefault [33.658237180000015, 33.51113319399997, 33.25321677000011, 33.23780467200004, 33.467723277999994] chain [23.47564513400016, 23.542697918999693, 23.520614959999875, 23.498439506000068, 23.582990831999723] Pero con LEN=100 , la función de chain es un poco más lenta:
default [0.20926385200004916, 0.23037391399998342, 0.21281876400007604, 0.21195233899993582, 0.21580142600009822] setdefault [0.22843905199988512, 0.2232434430000012, 0.2187928880000527, 0.22453147500004889, 0.21708852799997658] chain [0.24585279899997659, 0.23280389700016713, 0.2262972040000477, 0.24113659099998586, 0.2370573980001609] Entonces, una vez más, chain no debería ajustarse a sus necesidades, ya que sus listas tienden a ser pequeñas con docenas de elementos, pero menciono esta solución en aras de la exhaustividad.