J'aimerais identifier des groupes de nombres continus dans une liste, de sorte que:
myfunc([2, 3, 4, 5, 12, 13, 14, 15, 16, 17, 20])
Résultats:
[(2,5), (12,17), 20]
Et je me demandais quelle était la meilleure façon de procéder (en particulier si quelque chose était intégré à Python).
Edit: Note: Au départ, j'avais oublié de mentionner que les numéros individuels devaient être renvoyés sous forme de nombres individuels et non de plages.
more_itertools.consecutive_groups
a été ajouté à la version 4.0.
Démo
import more_itertools as mit
iterable = [2, 3, 4, 5, 12, 13, 14, 15, 16, 17, 20]
[list(group) for group in mit.consecutive_groups(iterable)]
# [[2, 3, 4, 5], [12, 13, 14, 15, 16, 17], [20]]
Code
En appliquant cet outil, nous créons une fonction génératrice qui recherche des plages de nombres consécutifs.
def find_ranges(iterable):
"""Yield range of consecutive numbers."""
for group in mit.consecutive_groups(iterable):
group = list(group)
if len(group) == 1:
yield group[0]
else:
yield group[0], group[-1]
iterable = [2, 3, 4, 5, 12, 13, 14, 15, 16, 17, 20]
list(find_ranges(iterable))
# [(2, 5), (12, 17), 20]
L'implémentation source émule une recette classique } (comme l'a démontré @Nadia Alramli).
Remarque: more_itertools
est un package tiers installable via pip install more_itertools
.
EDIT 2: Répondre à la nouvelle exigence du PO
ranges = []
for key, group in groupby(enumerate(data), lambda (index, item): index - item):
group = map(itemgetter(1), group)
if len(group) > 1:
ranges.append(xrange(group[0], group[-1]))
else:
ranges.append(group[0])
Sortie:
[xrange(2, 5), xrange(12, 17), 20]
Vous pouvez remplacer xrange par range ou n’importe quelle autre classe personnalisée.
Les documents Python ont une recette très nette pour cela:
from operator import itemgetter
from itertools import groupby
data = [2, 3, 4, 5, 12, 13, 14, 15, 16, 17]
for k, g in groupby(enumerate(data), lambda (i,x):i-x):
print map(itemgetter(1), g)
Sortie:
[2, 3, 4, 5]
[12, 13, 14, 15, 16, 17]
Si vous voulez obtenir exactement le même résultat, procédez comme suit:
ranges = []
for k, g in groupby(enumerate(data), lambda (i,x):i-x):
group = map(itemgetter(1), g)
ranges.append((group[0], group[-1]))
sortie:
[(2, 5), (12, 17)]
EDIT: L'exemple est déjà expliqué dans la documentation mais je devrais peut-être l'expliquer davantage:
La clé de la solution est différenciation avec une plage telle que les numéros consécutifs apparaissent tous dans le même groupe.
Si les données étaient: [2, 3, 4, 5, 12, 13, 14, 15, 16, 17]
Alors groupby(enumerate(data), lambda (i,x):i-x)
est équivalent à ce qui suit:
groupby(
[(0, 2), (1, 3), (2, 4), (3, 5), (4, 12),
(5, 13), (6, 14), (7, 15), (8, 16), (9, 17)],
lambda (i,x):i-x
)
La fonction lambda soustrait l'index de l'élément de la valeur de l'élément. Donc, lorsque vous appliquez le lambda sur chaque élément. Vous obtiendrez les clés suivantes pour groupby:
[-2, -2, -2, -2, -8, -8, -8, -8, -8, -8]
groupby regroupe les éléments par valeur de clé égale, ainsi les 4 premiers éléments seront regroupés, et ainsi de suite.
J'espère que cela le rend plus lisible.
La version python 3
peut être utile pour les débutants
importer les bibliothèques nécessaires en premier
from itertools import groupby
from operator import itemgetter
ranges =[]
for k,g in groupby(enumerate(data),lambda x:x[0]-x[1]):
group = (map(itemgetter(1),g))
group = list(map(int,group))
ranges.append((group[0],group[-1]))
La solution "naïve" que je trouve au moins lisible.
x = [2, 3, 4, 5, 12, 13, 14, 15, 16, 17, 22, 25, 26, 28, 51, 52, 57]
def group(L):
first = last = L[0]
for n in L[1:]:
if n - 1 == last: # Part of the group, bump the end
last = n
else: # Not part of the group, yield current group and start a new
yield first, last
first = last = n
yield first, last # Yield the last group
>>>print list(group(x))
[(2, 5), (12, 17), (22, 22), (25, 26), (28, 28), (51, 52), (57, 57)]
En supposant que votre liste soit triée:
>>> from itertools import groupby
>>> def ranges(lst):
pos = (j - i for i, j in enumerate(lst))
t = 0
for i, els in groupby(pos):
l = len(list(els))
el = lst[t]
t += l
yield range(el, el+l)
>>> lst = [2, 3, 4, 5, 12, 13, 14, 15, 16, 17]
>>> list(ranges(lst))
[range(2, 6), range(12, 18)]
Voici quelque chose qui devrait fonctionner, sans aucune importation nécessaire:
def myfunc(lst):
ret = []
a = b = lst[0] # a and b are range's bounds
for el in lst[1:]:
if el == b+1:
b = el # range grows
else: # range ended
ret.append(a if a==b else (a,b)) # is a single or a range?
a = b = el # let's start again with a single
ret.append(a if a==b else (a,b)) # corner case for last single/range
return ret
Veuillez noter que le code utilisant groupby
ne fonctionne pas comme indiqué dans Python 3, utilisez donc ceci.
for k, g in groupby(enumerate(data), lambda x:x[0]-x[1]):
group = list(map(itemgetter(1), g))
ranges.append((group[0], group[-1]))
Cela n’utilise pas de fonction standard, c’est juste l’initialisation de la saisie, mais ça devrait marcher:
def myfunc(l):
r = []
p = q = None
for x in l + [-1]:
if x - 1 == q:
q += 1
else:
if p:
if q > p:
r.append('%s-%s' % (p, q))
else:
r.append(str(p))
p = q = x
return '(%s)' % ', '.join(r)
Notez que cela nécessite que l'entrée ne contienne que des nombres positifs par ordre croissant. Vous devez valider la saisie, mais ce code est omis pour plus de clarté.
Voici la réponse que je suis venu avec. J'écris le code pour que les autres comprennent, alors je suis assez bavard avec des noms de variables et des commentaires.
D'abord une fonction d'aide rapide:
def getpreviousitem(mylist,myitem):
'''Given a list and an item, return previous item in list'''
for position, item in enumerate(mylist):
if item == myitem:
# First item has no previous item
if position == 0:
return None
# Return previous item
return mylist[position-1]
Et puis le code actuel:
def getranges(cpulist):
'''Given a sorted list of numbers, return a list of ranges'''
rangelist = []
inrange = False
for item in cpulist:
previousitem = getpreviousitem(cpulist,item)
if previousitem == item - 1:
# We're in a range
if inrange == True:
# It's an existing range - change the end to the current item
newrange[1] = item
else:
# We've found a new range.
newrange = [item-1,item]
# Update to show we are now in a range
inrange = True
else:
# We were in a range but now it just ended
if inrange == True:
# Save the old range
rangelist.append(newrange)
# Update to show we're no longer in a range
inrange = False
# Add the final range found to our list
if inrange == True:
rangelist.append(newrange)
return rangelist
Exemple d'exécution:
getranges([2, 3, 4, 5, 12, 13, 14, 15, 16, 17])
résultats:
[[2, 5], [12, 17]]
import numpy as np
myarray = [2, 3, 4, 5, 12, 13, 14, 15, 16, 17, 20]
sequences = np.split(myarray, np.array(np.where(np.diff(myarray) > 1)[0]) + 1)
l = []
for s in sequences:
if len(s) > 1:
l.append((np.min(s), np.max(s)))
else:
l.append(s[0])
print(l)
Sortie:
[(2, 5), (12, 17), 20]
Une solution courte qui fonctionne sans importations supplémentaires. Il accepte toutes les itérations, trie les entrées non triées et supprime les éléments en double:
def ranges(nums):
nums = sorted(set(nums))
gaps = [[s, e] for s, e in Zip(nums, nums[1:]) if s+1 < e]
edges = iter(nums[:1] + sum(gaps, []) + nums[-1:])
return list(Zip(edges, edges))
Exemple:
>>> ranges([2, 3, 4, 7, 8, 9, 15])
[(2, 4), (7, 9), (15, 15)]
>>> ranges([-1, 0, 1, 2, 3, 12, 13, 15, 100])
[(-1, 3), (12, 13), (15, 15), (100, 100)]
>>> ranges(range(100))
[(0, 99)]
>>> ranges([0])
[(0, 0)]
>>> ranges([])
[]
C’est la même chose que la solution de @ dansalmo que j’ai trouvée incroyable, mais un peu difficile à lire et à appliquer (car elle n’est pas donnée en tant que fonction).
Notez qu'il pourrait facilement être modifié pour cracher des plages ouvertes "traditionnelles" [start, end)
, par exemple. modifier l'instruction de retour:
return [(s, e+1) for s, e in Zip(edges, edges)]
J'ai copié cette réponse de une autre question qui était marquée comme une copie de celle-ci dans le but de la rendre plus facile à trouver (après que je venais de chercher à nouveau ce sujet, ne trouvant que la question ici au début satisfait des réponses données).
Utilisation de numpy + listes de compréhension:
Avec la fonction numpy diff, il est possible d’identifier les entrées de vecteur d’entrée consécutives pour lesquelles leur différence n’est pas égale à un. Le début et la fin du vecteur d’entrée doivent être pris en compte.
import numpy as np
data = np.array([2, 3, 4, 5, 12, 13, 14, 15, 16, 17, 20])
d = [i for i, df in enumerate(np.diff(data)) if df!= 1]
d = np.hstack([-1, d, len(data)-1]) # add first and last elements
d = np.vstack([d[:-1]+1, d[1:]]).T
print(data[d])
Sortie:
[[ 2 5]
[12 17]
[20 20]]
Remarque: La demande selon laquelle les numéros individuels doivent être traités différemment ((renvoyée en tant que valeur individuelle, pas de plage) a été omise. Ceci peut être atteint en post-traitant davantage les résultats. Habituellement, cela rend les choses plus complexes sans gagner aucun avantage.
Utiliser groupby
et count
à partir de itertools
nous donne une solution courte. L'idée est que, dans un ordre croissant, la différence entre l'index et la valeur reste la même.
Afin de garder une trace de l'index, nous pouvons utiliser un itertools.count , ce qui rend le code plus propre en utilisant enumerate
:
from itertools import groupby, count
def intervals(data):
out = []
counter = count()
for key, group in groupby(data, key = lambda x: x-next(counter)):
block = list(group)
out.append([block[0], block[-1]])
return out
Quelques exemples de sortie:
print(intervals([0, 1, 3, 4, 6]))
# [[0, 1], [3, 4], [6, 6]]
print(intervals([2, 3, 4, 5]))
# [[2, 5]]