barplot - Obteniendo los primeros n elementos únicos de la lista de Python
pandas plot (10)
¡Hay respuestas realmente sorprendentes para esta pregunta, que son rápidas, compactas y brillantes! La razón por la que estoy poniendo este código aquí es porque creo que hay muchos casos en los que no le importa perder ni un microsegundo ni una sola vez en su código para resolver una sola tarea una sola vez.
a = [1,2,2,3,3,4,5,6]
res = []
for x in a:
if x not in res: # yes, not optimal, but doesnt need additional dict
res.append(x)
if len(res) == 5:
break
print(res)
Tengo una lista de python donde los elementos se pueden repetir.
>>> a = [1,2,2,3,3,4,5,6]
Quiero obtener los primeros n
elementos únicos de la lista. Entonces, en este caso, si quiero los primeros 5 elementos únicos, serían:
[1,2,3,4,5]
He encontrado una solución usando generadores:
def iterate(itr, upper=5):
count = 0
for index, element in enumerate(itr):
if index==0:
count += 1
yield element
elif element not in itr[:index] and count<upper:
count += 1
yield element
En uso:
>>> i = iterate(a, 5)
>>> [e for e in i]
[1,2,3,4,5]
Tengo dudas de que esta sea la solución más óptima. ¿Existe una estrategia alternativa que pueda implementar para escribirla de una manera más pitónica y eficiente?
¿Por qué no usar algo como esto?
>>> a = [1, 2, 2, 3, 3, 4, 5, 6]
>>> list(set(a))[:5]
[1, 2, 3, 4, 5]
Aquí hay un enfoque de Pythonic que usa itertools.takewhile()
:
In [95]: from itertools import takewhile
In [96]: seen = set()
In [97]: set(takewhile(lambda x: seen.add(x) or len(seen) <= 4, a))
Out[97]: {1, 2, 3, 4}
Puede usar OrderedDict
o, desde Python 3.7, un dict
ordinario, ya que se implementan para preservar el orden de inserción. Tenga en cuenta que esto no funcionará con conjuntos.
N = 3
a = [1, 2, 2, 3, 3, 3, 4]
d = {x: True for x in a}
list(d.keys())[:N]
Puedes adaptar la popular receta de itertools
unique_everseen
:
def unique_everseen_limit(iterable, limit=5):
seen = set()
seen_add = seen.add
for element in iterable:
if element not in seen:
seen_add(element)
yield element
if len(seen) == limit:
break
a = [1,2,2,3,3,4,5,6]
res = list(unique_everseen_limit(a)) # [1, 2, 3, 4, 5]
Alternativamente, como lo sugiere @Chris_Rands, puede usar itertools.islice
para extraer un número fijo de valores de un generador no limitado:
from itertools import islice
def unique_everseen(iterable):
seen = set()
seen_add = seen.add
for element in iterable:
if element not in seen:
seen_add(element)
yield element
res = list(islice(unique_everseen(a), 5)) # [1, 2, 3, 4, 5]
Tenga en cuenta que la receta unique_everseen
está disponible en bibliotecas de terceros a través de more_itertools.unique_everseen
o toolz.unique
, por lo que podría usar:
from itertools import islice
from more_itertools import unique_everseen
from toolz import unique
res = list(islice(unique_everseen(a), 5)) # [1, 2, 3, 4, 5]
res = list(islice(unique(a), 5)) # [1, 2, 3, 4, 5]
Si sus objetos son hashable (los int
s son hashable), puede escribir la función de utilidad utilizando el método fromkeys
de collections.OrderedDict
(o comenzando desde Python3.7 un dict
simple, ya que se ordenaron officially ) como
from collections import OrderedDict
def nub(iterable):
"""Returns unique elements preserving order."""
return OrderedDict.fromkeys(iterable).keys()
y luego la implementación de iterate
se puede simplificar para
from itertools import islice
def iterate(itr, upper=5):
return islice(nub(itr), upper)
O si quieres siempre una list
como salida.
def iterate(itr, upper=5):
return list(nub(itr))[:upper]
Mejoras
Como @Chris_Rands mencionó, esta solución recorre toda la colección y podemos mejorar esto escribiendo la utilidad nub
en una forma de generator como otros ya lo hicieron:
def nub(iterable):
seen = set()
add_seen = seen.add
for element in iterable:
if element in seen:
continue
yield element
add_seen(element)
Suponiendo que los elementos estén ordenados como se muestra, esta es una oportunidad para divertirse con la función groupby en itertools:
from itertools import groupby, islice
def first_unique(data, upper):
return islice((key for (key, _) in groupby(data)), 0, upper)
a = [1, 2, 2, 3, 3, 4, 5, 6]
print(list(first_unique(a, 5)))
Actualizado para usar islice
lugar de enumerate
por @ juanpa.arrivillaga. Ni siquiera necesita un set
para realizar un seguimiento de los duplicados.
Usaría un set
para recordar lo que se vio y regresaría del generador cuando haya seen
suficiente:
a = [1,2,2,3,3,4,5,6]
def get_unique_N(iterable, N):
"""Yields (in order) the first N unique elements of iterable.
Might yield less if data too short."""
seen = set()
for e in iterable:
if e in seen:
continue
seen.add(e)
yield e
if len(seen) == N:
return
k = get_unique_N([1,2,2,3,3,4,5,6], 4)
print(list(k))
Salida:
[1,2,3,4]
De acuerdo con PEP-479 , debería return
de los generadores, no raise StopIteration
(gracias a @khelwood y @iBug por ese comentario), uno nunca aprende.
Con 3.6 obtienes una advertencia desaprobada, con 3.7 da RuntimeErrors: Plan de transición si aún utilizas la función raise StopIteration
Su solución que usa el elif element not in itr[:index] and count<upper:
usa las búsquedas O(k)
, siendo k
la longitud de la porción; el uso de un conjunto reduce esto a las búsquedas O(1)
pero usa más memoria porque El set también debe ser mantenido. Es un intercambio de velocidad y memoria: lo que es mejor depende de la aplicación / datos.
Considere [1,2,3,4,4,4,4,5]
vs [1]*1000+[2]*1000+[3]*1000+[4]*1000+[5]*1000+[6]
:
Para 6 unicos (en lista más larga):
- tendría búsquedas de
O(1)+O(2)+...+O(5001)
- el mío tendría
5001*O(1)
búsqueda + memoria para elset( {1,2,3,4,5,6})
Utilizando set
con sorted+ key
sorted(set(a), key=list(a).index)[:5]
Out[136]: [1, 2, 3, 4, 5]
Dado
import itertools as it
a = [1, 2, 2, 3, 3, 4, 5, 6]
Código
Una simple lista de comprensión (similar a la respuesta de @cdlane).
[k for k, _ in it.groupby(a)][:5]
# [1, 2, 3, 4, 5]
Alternativamente, en Python 3.6+:
list(dict.fromkeys(a))[:5]
# [1, 2, 3, 4, 5]