Tengo datos (recuentos) indexados por user_id y analysis_type_id obtenidos de una base de datos. Es una lista de 3 tuplas. Data de muestra:

counts  = [(4, 1, 4), (3, 5, 4), (2, 10, 4), (2, 10, 5)]

Donde el primer elemento de cada tupla es el count, el segundo el analysis_type_id y el último el user_id.

Me gustaría colocar eso en un diccionario, para poder recuperar los recuentos rápidamente: dado un user_id y analysis_type_id. Tendría que ser un diccionario de dos niveles. ¿Hay alguna estructura mejor?

Para construir el diccionario de dos niveles "a mano", codificaría:

dict = {4:{1:4,5:3,10:2},5:{10:2}}

Donde user_id es el primer nivel de clave dict, analysis_type_id es la segunda (sub) clave, y count es el valor dentro del dict.

¿Cómo crearía la "doble profundidad" en las claves dict mediante la comprensión de la lista? ¿O necesito recurrir a un bucle for anidado, donde primero itero a través de valores únicos user_id, luego encuentro los analysis_type_id coincidentes y relleno los recuentos ... uno por uno en el dict?

6
Ant 1 nov. 2017 a las 04:42

5 respuestas

La mejor respuesta

Dos llaves de tupla

Sugeriría abandonar la idea de anidar diccionarios y simplemente usar dos tuplas como claves directamente. Al igual que:

d = { (user_id, analysis_type_id): count for count, analysis_type_id, user_id in counts}

El diccionario es una tabla hash. En python, cada una de las dos tuplas tiene un único valor hash (no dos valores hash) y, por lo tanto, cada una de las dos tuplas se busca en función de su hash (relativamente) único. Por lo tanto, esto es más rápido (2 veces más rápido, la mayoría de las veces) que buscar el hash de DOS teclas separadas (primero user_id, luego analysis_type_id).

Sin embargo, tenga cuidado con la optimización prematura. A menos que esté haciendo millones de búsquedas, es poco probable que el aumento del rendimiento del plano dict sea importante. La verdadera razón para favorecer el uso de las dos tuplas aquí es que la sintaxis y la legibilidad de una solución de dos tuplas es muy superior a otras soluciones, es decir, suponiendo que la gran mayoría de las veces querrá acceder a elementos basados en una par de valores y no grupos de elementos basados en un solo valor.

Considere usar un namedtuple

Puede ser conveniente crear una tupla con nombre para almacenar esas claves. Hazlo de esta manera:

from collections import namedtuple
IdPair = namedtuple("IdPair", "user_id, analysis_type_id")

Luego úsalo en la comprensión de tu diccionario:

d = { IdPair(user_id, analysis_type_id): count for count, analysis_type_id, user_id in counts}

Y acceda a un recuento que le interese así:

somepair = IdPair(user_id = 4, analysis_type_id = 1)
d[somepair]

La razón por la que esto a veces es útil es que puedes hacer cosas como esta:

user_id = somepair.user_id # very nice syntax

Algunas otras opciones útiles

Una desventaja de la solución anterior es el caso en que falla su búsqueda. En ese caso, solo obtendrá un rastreo como el siguiente:

>>> d[IdPair(0,0)]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyError: IdPair(user_id=0, analysis_type_id=0)

Esto no es muy útil; ¿Fue el user_id que no tenía comparación, o el analysis_type_id, o ambos?

Puede crear una herramienta mejor para usted creando su propio tipo dict que le brinda un buen rastreo con más información. Puede verse más o menos así:

class CountsDict(dict):
    """A dict for storing IdPair keys and count values as integers.

    Provides more detailed traceback information than a regular dict.
    """
    def __getitem__(self, k):
        try:
            return super().__getitem__(k)
        except KeyError as exc:
            raise self._handle_bad_key(k, exc) from exc
    def _handle_bad_key(self, k, exc):
        """Provides a custom exception when a bad key is given."""
        try:
            user_id, analysis_type_id = k
        except:
            return exc
        has_u_id = next((True for u_id, _ in self if u_id==user_id), False)
        has_at_id  = next((True for _, at_id in self if at_id==analysis_type_id), False)
        exc_lookup = {(False, False):KeyError(f"CountsDict missing pair: {k}"),
                      (True, False):KeyError(f"CountsDict missing analysis_type_id: "
                                             f"{analysis_type_id}"),
                      (False, True):KeyError(f"CountsDict missing user_id: {user_id}")}
        return exc_lookup[(user_id, analysis_type_id)]

Úselo como un dict normal.

Sin embargo, puede tener MÁS sentido simplemente agregar nuevos pares a su dict (con un recuento de cero) cuando intenta acceder a un par faltante. Si este es el caso, usaría un defaultdict y establecería el recuento en cero (usando el valor predeterminado de int como la función de fábrica) cuando se accede a una clave faltante. Al igual que:

from collections import defaultdict
my_dict = defaultdict(default_factory=int, 
                      ((user_id, analysis_type_id), count) for count, analysis_type_id, user_id in counts))

Ahora, si intenta acceder a una clave que falta, el recuento se establecerá en cero. Sin embargo, un problema con el método this es que TODAS las teclas se establecerán en cero:

value = my_dict['I'm not a two tuple, sucka!!!!'] # <-- will be added to my_dict

Para evitar esto, volvemos a la idea de hacer un CountsDict, excepto en este caso, su dict especial será una subclase de defaultdict. Sin embargo, a diferencia de un defaultdict normal, verificará que la clave sea válida antes de agregarla. Y como beneficio adicional, podemos asegurarnos de que CUALQUIER dos tuplas que se agreguen como clave se conviertan en IdPair.

from collections import defaultdict

class CountsDict(defaultdict):
    """A dict for storing IdPair keys and count values as integers.

    Missing two-tuple keys are converted to an IdPair. Invalid keys raise a KeyError.
    """
    def __getitem__(self, k):
        try:
            user_id, analysis_type_id = k
        except:
            raise KeyError(f"The provided key {k!r} is not a valid key.")
        else:
            # convert two tuple to an IdPair if it was not already
            k = IdPair(user_id, analysis_type_id)
        return super().__getitem__(k)

Úselo como el defaultdict normal:

my_dict = CountsDict(default_factory=int, 
                     ((user_id, analysis_type_id), count) for count, analysis_type_id, user_id in counts))

NOTA: en lo anterior no lo he hecho para que dos claves de tupla se conviertan en IdPair s tras la creación de la instancia (porque __setitem__ no se utiliza durante la creación de la instancia). Para crear esta funcionalidad, también necesitaríamos implementar una anulación del método __init__.

Envolver

De todos estos, la opción más útil depende completamente de su caso de uso.

6
Rick supports Monica 1 nov. 2017 a las 20:37

Este es un buen uso para el objeto defaultdict. Puede crear un defaultdict cuyos elementos son siempre dictados. Luego puede simplemente meter los recuentos en los dictados correctos, así:

from collections import defaultdict

counts  = [(4, 1, 4), (3, 5, 4), (2, 10, 4), (2, 10, 5)]
dct = defaultdict(dict)
for count, analysis_type_id, user_id in counts:
    dct[user_id][analysis_type_id]=count

dct
# defaultdict(dict, {4: {1: 4, 5: 3, 10: 2}, 5: {10: 2}})

# if you want a 'normal' dict, you can finish with this:
dct = dict(dct)

O simplemente puede usar dictados estándar con setdefault:

counts  = [(4, 1, 4), (3, 5, 4), (2, 10, 4), (2, 10, 5)]
dct = dict()
for count, analysis_type_id, user_id in counts:
    dct.setdefault(user_id, dict())
    dct[user_id][analysis_type_id]=count

dct
# {4: {1: 4, 5: 3, 10: 2}, 5: {10: 2}}

No creo que puedas hacer esto de manera ordenada con una comprensión de la lista, pero no hay que tener miedo de un bucle for para este tipo de cosas.

0
Matthias Fripp 1 nov. 2017 a las 02:02

Podrías usar la siguiente lógica. No es necesario importar ningún paquete, solo debemos usarlo para los bucles correctamente.

counts = [(4, 1, 4), (3, 5, 4), (2, 10, 4), (2, 10, 5)] dct = {x[2]:{y[1]:y[0] for y in counts if x[2] == y[2]} for x in counts }

La salida "" "será {4: {1: 4, 5: 3, 10: 2}, 5: {10: 2}}" ""

0
karthik reddy 1 nov. 2017 a las 02:36

Puede enumerar la comprensión de los bucles anidados con condición y usar uno o más de ellos para la selección de elementos:

# create dict with tuples
line_dict = {str(nest_list[0]) : nest_list[1:] for nest_list in nest_lists for elem in nest_list if elem== nest_list[0]}
print(line_dict)

 # create dict with list 
line_dict1 = {str(nest_list[0]) list(nest_list[1:]) for nest_list in nest_lists for elem in nest_list if elem== nest_list[0]}
print(line_dict1)

Example: nest_lists = [("a","aa","aaa","aaaa"), ("b","bb","bbb","bbbb") ("c","cc","ccc","cccc"), ("d","dd","ddd","dddd")]

Output: {'a': ('aa', 'aaa', 'aaaa'), 'b': ('bb', 'bbb', 'bbbb'), 'c': ('cc', 'ccc', 'cccc'), 'd': ('dd', 'ddd', 'dddd')}, {'a': ['aa', 'aaa', 'aaaa'], 'b': ['bb', 'bbb', 'bbbb'], 'c': ['cc', 'ccc', 'cccc'], 'd': ['dd', 'ddd', 'dddd']}
0
Mindaugas Vaitkus 10 jun. 2019 a las 15:24

La solución más legible utiliza un defaultdict que le ahorra bucles anidados y comprueba si ya existen claves:

from collections import defaultdict
dct = defaultdict(dict)  # do not shadow the built-in 'dict'
for x, y, z in counts:
    dct[z][y] = x
dct
# defaultdict(dict, {4: {1: 4, 5: 3, 10: 2}, 5: {10: 2}})

Si realmente desea una comprensión única, puede usar {{X0} } y esta torpeza:

from itertools import groupby
dct = {k: {y: x for x, y, _ in g} for k, g in groupby(sorted(counts, key=lambda c: c[2]), key=lambda c: c[2])}

Si sus datos iniciales ya están ordenados por user_id, puede guardar la clasificación.

2
schwobaseggl 1 nov. 2017 a las 02:01