web-dev-qa-db-fra.com

Pas à pas avec plusieurs valeurs lors du découpage d'un tableau en Python

J'essaie d'obtenir les valeurs m tout en parcourant tous les éléments n d'un tableau. Par exemple, pour m = 2 et n = 5, et étant donné

a = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

Je veux récupérer

b = [1, 2, 6, 7]

Y at-il un moyen de faire cela en utilisant le découpage? Je peux le faire en utilisant une compréhension de liste imbriquée, mais je me demandais s'il était possible de le faire en utilisant uniquement les index. Pour référence, le moyen de compréhension de liste est:

 b = [k for j in [a[i:i+2] for i in range(0,len(a),5)] for k in j]
25
Paul W

Je suis d'accord avec wim que vous ne pouvez pas le faire avec juste le découpage en tranches. Mais vous pouvez le faire avec une seule compréhension de liste:

>>> [x for i,x in enumerate(a) if i%n < m]
[1, 2, 6, 7]
24
Kevin

Non, ce n'est pas possible avec le découpage en tranches. Le découpage en tranches prend uniquement en charge les opérations start, stop et step - il n'y a aucun moyen de représenter une progression avec des "groupes" de taille supérieure à 1.

6
wim

En bref, non, vous ne pouvez pas. Mais vous pouvez utiliser itertools pour supprimer le besoin de listes intermédiaires:

from itertools import chain, islice

res = list(chain.from_iterable(islice(a, i, i+2) for i in range(0, len(a), 5)))

print(res)

[1, 2, 6, 7]

Selon la logique de @ Kevin, si vous voulez une solution vectorisée pour éviter une boucle for, vous pouvez utiliser la bibliothèque tierce numpy:

import numpy as np

m, n = 2, 5
a = np.array(a)  # convert to numpy array
res = a[np.where(np.arange(a.shape[0]) % n < m)]
5
jpp

Il existe d'autres moyens de le faire, qui présentent tous des avantages dans certains cas, mais aucun ne consiste à "trancher".


La solution la plus générale consiste probablement à grouper vos entrées, à les diviser en tranches, puis à les aplatir. L'un des avantages de cette solution est que vous pouvez le faire paresseusement, sans construire de grandes listes intermédiaires, et vous pouvez le faire à tout moment, y compris un itérateur paresseux, pas seulement une liste.

# from itertools recipes in the docs
def grouper(iterable, n, fillvalue=None):
    "Collect data into fixed-length chunks or blocks"
    # grouper('ABCDEFG', 3, 'x') --> ABC DEF Gxx"
    args = [iter(iterable)] * n
    return itertools.Zip_longest(*args, fillvalue=fillvalue)
groups = grouper(a, 5)
truncated = (group[:2] for group in groups)
b = [elem for group in truncated for elem in group]

Et vous pouvez convertir cela en une simple ligne, bien que vous ayez toujours besoin de la fonction grouper:

b = [elem for group in grouper(a, 5) for elem in group[:2]]

Une autre option consiste à créer une liste d’index et à utiliser itemgetter pour récupérer toutes les valeurs. Cela pourrait être plus lisible pour une fonction plus compliquée que "les 2 premiers sur 5", mais c'est probablement moins lisible pour quelque chose d'aussi simple que votre utilisation:

indices = [i for i in range(len(a)) if i%5 < 2]
b = operator.itemgetter(*indices)(a)

… Qui peut être transformé en une ligne:

b = operator.itemgetter(*[i for i in range(len(a)) if i%5 < 2])(a)

Et vous pouvez combiner les avantages des deux approches en écrivant votre propre version de itemgetter qui prend un itérateur d'index paresseux - ce que je ne montrerai pas, car vous pouvez aller encore mieux en écrivant une qui prend à la place une fonction de filtre d'index:

def indexfilter(pred, a):
    return [elem for i, elem in enumerate(a) if pred(i)]
b = indexfilter((lambda i: i%5<2), a)

(Pour rendre indexfilter paresseux, remplacez simplement les crochets par des parenthèses.)

… Ou comme one-liner:

b = [elem for i, elem in enumerate(a) if i%5<2]

Je pense que ce dernier pourrait être le plus lisible. Et cela fonctionne avec n'importe quelle liste, plutôt que de simples listes, et il peut être rendu paresseux (encore une fois, remplacez simplement les crochets par des parenthèses). Mais je ne pense toujours pas que ce soit plus simple que votre compréhension initiale, et que ce n’est pas seulement trancher.

3
abarnert

La question state array, et par là si nous parlons de tableaux NumPy, nous pouvons sûrement utiliser quelques astuces évidentes pour NumPy et quelques astuces moins évidentes. Nous pouvons sûrement utiliser slicing pour obtenir une vue 2D dans l'entrée sous certaines conditions.

Maintenant, en fonction de la longueur du tableau, appelons-le l et m, nous aurions trois scénarios:

Scénario 1: l est divisible par n

Nous pouvons utiliser le découpage en tranches et le remodelage pour obtenir une vue dans le tableau en entrée et ainsi obtenir une exécution constante.

Vérifiez le concept de vue:

In [108]: a = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])

In [109]: m = 2; n = 5

In [110]: a.reshape(-1,n)[:,:m]
Out[110]: 
array([[1, 2],
       [6, 7]])

In [111]: np.shares_memory(a, a.reshape(-1,n)[:,:m])
Out[111]: True

Vérifiez les timings sur un très grand tableau et par conséquent une revendication d'exécution constante:

In [118]: a = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])

In [119]: %timeit a.reshape(-1,n)[:,:m]
1000000 loops, best of 3: 563 ns per loop

In [120]: a = np.arange(10000000)

In [121]: %timeit a.reshape(-1,n)[:,:m]
1000000 loops, best of 3: 564 ns per loop

Pour obtenir une version aplatie:

Si nous avons pour obtenir un tableau aplati en sortie, nous devons simplement utiliser une opération d'aplatissement avec .ravel(), comme ceci -

In [127]: a = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])

In [128]: m = 2; n = 5

In [129]: a.reshape(-1,n)[:,:m].ravel()
Out[129]: array([1, 2, 6, 7])

Les timings montrent que ce n’est pas trop mal comparé aux versions numpy.where vectorisées et en boucle d’autres publications -

In [143]: a = np.arange(10000000)

# @Kevin's soln
In [145]: %timeit [x for i,x in enumerate(a) if i%n < m]
1 loop, best of 3: 1.23 s per loop

# @jpp's soln
In [147]: %timeit a[np.where(np.arange(a.shape[0]) % n < m)]
10 loops, best of 3: 145 ms per loop

In [144]: %timeit a.reshape(-1,n)[:,:m].ravel()
100 loops, best of 3: 16.4 ms per loop

Scénario 2: l n'est pas divisible par n, mais les groupes se terminent par un complet à la fin

Nous passons aux méthodes non évidentes de NumPy avec np.lib.stride_tricks.as_strided qui permet d'aller au-delà des limites du bloc mémoire (il faut donc faire attention ici pour ne pas écrire dans celles-ci) afin de faciliter une solution utilisant slicing. La mise en oeuvre ressemblerait à quelque chose comme ça -

def select_groups(a, m, n):
    a = np.asarray(a)
    strided = np.lib.stride_tricks.as_strided

    # Get params defining the lengths for slicing and output array shape    
    nrows = len(a)//n
    add0 = len(a)%n
    s = a.strides[0]
    out_shape = nrows+int(add0!=0),m

    # Finally stride, flatten with reshape and slice
    return strided(a, shape=out_shape, strides=(s*n,s))

Un exemple d’exécution pour vérifier que la sortie est une view -

In [151]: a = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13])

In [152]: m = 2; n = 5

In [153]: select_groups(a, m, n)
Out[153]: 
array([[ 1,  2],
       [ 6,  7],
       [11, 12]])

In [154]: np.shares_memory(a, select_groups(a, m, n))
Out[154]: True

Pour obtenir une version aplatie, ajoutez-la avec .ravel().

Voyons un peu la comparaison des temps -

In [158]: a = np.arange(10000003)

In [159]: m = 2; n = 5

# @Kevin's soln
In [161]: %timeit [x for i,x in enumerate(a) if i%n < m]
1 loop, best of 3: 1.24 s per loop

# @jpp's soln
In [162]: %timeit a[np.where(np.arange(a.shape[0]) % n < m)]
10 loops, best of 3: 148 ms per loop

In [160]: %timeit select_groups(a, m=m, n=n)
100000 loops, best of 3: 5.8 µs per loop

Si nous avons besoin d'une version aplatie, c'est quand même pas si mal -

In [163]: %timeit select_groups(a, m=m, n=n).ravel()
100 loops, best of 3: 16.5 ms per loop

Scénario 3: l n'est pas divisible par n et les groupes se terminent par un incomplet à la fin.

Pour ce cas, nous aurions besoin d'un découpage supplémentaire à la fin de ce que nous avions dans la méthode précédente, comme suit -

def select_groups_generic(a, m, n):
    a = np.asarray(a)
    strided = np.lib.stride_tricks.as_strided

    # Get params defining the lengths for slicing and output array shape    
    nrows = len(a)//n
    add0 = len(a)%n
    lim = m*(nrows) + add0
    s = a.strides[0]
    out_shape = nrows+int(add0!=0),m

    # Finally stride, flatten with reshape and slice
    return strided(a, shape=out_shape, strides=(s*n,s)).reshape(-1)[:lim]

Exemple de cycle -

In [166]: a = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11])

In [167]: m = 2; n = 5

In [168]: select_groups_generic(a, m, n)
Out[168]: array([ 1,  2,  6,  7, 11])

Horaires -

In [170]: a = np.arange(10000001)

In [171]: m = 2; n = 5

# @Kevin's soln
In [172]: %timeit [x for i,x in enumerate(a) if i%n < m]
1 loop, best of 3: 1.23 s per loop

# @jpp's soln
In [173]: %timeit a[np.where(np.arange(a.shape[0]) % n < m)]
10 loops, best of 3: 145 ms per loop

In [174]: %timeit select_groups_generic(a, m, n)
100 loops, best of 3: 12.2 ms per loop
2
Divakar

Avec itertools, vous pouvez obtenir un itérateur avec:

from itertools import compress, cycle

a = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
n = 5
m = 2

it = compress(a, cycle([1, 1, 0, 0, 0]))
res = list(it)
0
Jacques Gaudin

Je me rends compte que la récursion n'est pas populaire, mais est-ce que quelque chose comme cela fonctionnerait? De plus, il n'est pas certain que l'ajout de récursivité au mélange compte uniquement comme utilisant des tranches.

def get_elements(A, m, n):
    if(len(A) < m):
        return A
    else:
        return A[:m] + get_elements(A[n:], m, n)

A est le tableau, m et n sont définis comme dans la question. Le premier si couvre le cas de base, où vous avez un tableau avec une longueur inférieure au nombre d'éléments que vous essayez d'extraire, et le second si est le cas récursif. Je suis un peu nouveau sur Python, veuillez pardonner ma mauvaise compréhension de la langue si cela ne fonctionne pas correctement, même si je l’ai testé et que cela semble bien fonctionner.

0
GnoveltyGnome