Identifier des groupes de nombres continus dans une liste

89

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

Retour:

[(2,5), (12,17), 20]

Et je me demandais quelle était la meilleure façon de le faire (en particulier s'il y a quelque chose d'incorporé dans Python).

Edit: Notez que j'ai initialement oublié de mentionner que les nombres individuels doivent être renvoyés sous forme de nombres individuels, pas de plages.

Mikemaccana
la source
3
Cette valeur de retour est-elle une chaîne?
Mark Byers
Idéalement, préférerait quelque chose qui utilise un type distinct pour les plages par rapport aux nombres autonomes.
mikemaccana

Réponses:

50

more_itertools.consecutive_groups a été ajouté dans 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 de générateur qui trouve 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 démontré par @Nadia Alramli).

Remarque: more_itertoolsest un package tiers installable via pip install more_itertools.

pylang
la source
119

EDIT 2: Pour 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])

Production:

[xrange(2, 5), xrange(12, 17), 20]

Vous pouvez remplacer xrange par range ou toute autre classe personnalisée.


Les documents Python ont une recette très soignée 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)

Production:

[2, 3, 4, 5]
[12, 13, 14, 15, 16, 17]

Si vous souhaitez obtenir exactement la même sortie, vous pouvez le faire:

ranges = []
for k, g in groupby(enumerate(data), lambda (i,x):i-x):
    group = map(itemgetter(1), g)
    ranges.append((group[0], group[-1]))

production:

[(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 la différenciation avec une plage afin que les nombres consécutifs apparaissent tous dans le même groupe.

Si les données étaient: [2, 3, 4, 5, 12, 13, 14, 15, 16, 17] Then groupby(enumerate(data), lambda (i,x):i-x)équivaut à 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. Ainsi, 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, de sorte que les 4 premiers éléments seront regroupés et ainsi de suite.

J'espère que cela le rend plus lisible.

python 3 la version peut être utile pour les débutants

importez d'abord les bibliothèques requises

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]))
Nadia Alramli
la source
4
fonctionne presque dans py3k, sauf que cela nécessite lambda x:x[0]-x[1].
SilentGhost
Pourriez-vous utiliser des noms de variables à plusieurs caractères? Pour quelqu'un qui n'est pas familier avec map () ou groupby (), les significations de kg, i et x ne sont pas claires.
mikemaccana
1
Cela a été copié à partir des documentations Python avec les mêmes noms de variables. J'ai changé les noms maintenant.
Nadia Alramli
1
Vous devrez incrémenter le 2ème nombre dans xrange / range car il n'est pas inclusif. En d'autres termes [2,3,4,5] == xrange(2,6), non xrange(2,5). Il peut être utile de définir un nouveau type de données de plage inclusif.
IceArdor
10
Python 3 renvoie une erreur de syntaxe sur le premier exemple. Voici les 2 premières lignes mises à jour pour fonctionner sur python 3:for key, group in groupby(enumerate(data), lambda i: i[0] - i[1]): group = list(map(itemgetter(1), group))
derek73
16

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)]
truppo
la source
J'aime beaucoup cette réponse car elle est laconique mais lisible. Cependant, les nombres qui sont en dehors des plages doivent être imprimés sous forme de chiffres uniques, pas de tuples (car je formaterai la sortie et
aurai des
4
L'autre réponse était belle et intelligente, mais celle-ci est plus compréhensible pour moi et a permis à un débutant comme moi de l'élargir en fonction de mes besoins.
Benny
Pourrait utiliser une compréhension de liste pour imprimer les tuples hors plage sous forme de chiffres uniques: print([i if i[0] != i[1] else i[0] for i in group(x)])
Nexus
14

En supposant que votre liste est 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)]
SilentGhost
la source
2
[j - i for i, j in enumerate(lst)]est intelligent :-)
Jochen Ritzel
9

Ici, c'est 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
Andrea Ambu
la source
6

Veuillez noter que le code utilisant groupbyne fonctionne pas comme indiqué dans Python 3, alors utilisez 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]))
Mark Lawrence
la source
3

Cela n'utilise pas de fonction standard - il ne fait qu'itérer sur l'entrée, mais cela devrait fonctionner:

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 contienne uniquement des nombres positifs dans l'ordre croissant. Vous devez valider l'entrée, mais ce code est omis pour plus de clarté.

Mark Byers
la source
1

Voici la réponse que j'ai trouvée. J'écris le code pour que d'autres personnes le comprennent, donc je suis assez bavard avec les noms de variables et les 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 réel:

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

Retour:

[[2, 5], [12, 17]]
Mikemaccana
la source
>>> getranges([2, 12, 13]) Les sorties: [[12, 13]] . Était-ce intentionnel?
SilentGhost
Oui, je dois corriger les numéros individuels (pour la plupart des réponses sur la page). J'y travaille maintenant.
mikemaccana
En fait, je préfère la réponse de Nadia, groupby () semble être la fonction standard que je voulais.
mikemaccana
1
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)

Production:

[(2, 5), (12, 17), 20]

la source
1

Utiliser groupbyet countde itertoolsnous donne une solution courte. L'idée est que, dans une séquence croissante, la différence entre l'indice et la valeur restera 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]]
Thierry Lathuille
la source
0

Utilisation des listes numpy + compréhension:
Avec la fonction numpy diff, les entrées vectorielles d'entrée conséquentes dont la différence n'est pas égale à un peuvent être identifiées. 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])

Production:

 [[ 2  5]   
  [12 17]   
  [20 20]]

Remarque: La demande selon laquelle les numéros individuels doivent être traités différemment (renvoyés en tant qu'individus et non par plages) a été omise. Ceci peut être atteint par un post-traitement supplémentaire des résultats. Habituellement, cela rendra les choses plus complexes sans en tirer aucun avantage.

Nir
la source
0

Une solution courte qui fonctionne sans importations supplémentaires. Il accepte tout itérable, 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, bien qu'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), en modifiant par exemple l'instruction return:

    return [(s, e+1) for s, e in zip(edges, edges)]

J'ai copié cette réponse à partir d' une autre question qui a été marquée comme un double de celle-ci dans le but de la rendre plus facile à trouver (après avoir récemment cherché à nouveau ce sujet, trouvant seulement la question ici au début et n'étant pas satisfait des réponses donné).

coldfix
la source
0

Les versions de Mark Byers , Andrea Ambu , SilentGhost , Nadia Alramli et truppo sont simples et rapides. La version 'truppo' m'a encouragé à écrire une version qui conserve le même comportement agile tout en gérant des tailles de pas autres que 1 (et des listes comme des éléments singletons qui ne s'étendent pas plus d'un pas avec une taille de pas donnée). Il est donné ici .

>>> list(ranges([1,2,3,4,3,2,1,3,5,7,11,1,2,3]))
[(1, 4, 1), (3, 1, -1), (3, 7, 2), 11, (1, 3, 1)]
smichr
la source