Répéter mais en morceaux de taille variable dans numpy

Aug 20 2020

J'ai un tableau qui est la concaténation de différents morceaux:

a = np.array([0, 1, 2, 10, 11, 20, 21, 22, 23])
#             >     <  >    <  >            <
chunks = np.array([3, 2, 4])
repeats = np.array([1, 3, 2])

Chaque segment commençant par une nouvelle décennie dans l'exemple ci-dessus est un «morceau» distinct que je voudrais répéter. La taille des morceaux et le nombre de répétitions sont connus pour chacun. Je ne peux pas faire de remodelage suivi de kronou repeatparce que les morceaux sont de tailles différentes.

Le résultat que je voudrais est

np.array([0, 1, 2, 10, 11, 10, 11, 10, 11, 20, 21, 22, 23, 20, 21, 22, 23])
# repeats:>  1  <  >         3          <  >              2             <

C'est facile à faire en boucle:

in_offset = np.r_[0, np.cumsum(chunks[:-1])]
out_offset = np.r_[0, np.cumsum(chunks[:-1] * repeats[:-1])]
output = np.zeros((chunks * repeats).sum(), dtype=a.dtype)
for c in range(len(chunks)):
    for r in range(repeats[c]):
        for i in range(chunks[c]):
            output[out_offset[c] + r * chunks[c] + i] = a[in_offset[c] + i]

Cela conduit à la vectorisation suivante:

regions = chunks * repeats
index = np.arange(regions.sum())

segments = np.repeat(chunks, repeats)
resets = np.cumsum(segments[:-1])
offsets = np.zeros_like(index)
offsets[resets] = segments[:-1]
offsets[np.cumsum(regions[:-1])] -= chunks[:-1]

index -= np.cumsum(offsets)

output = a[index]

Existe-t-il un moyen plus efficace de vectoriser ce problème? Pour être clair, je ne demande pas de révision de code. Je suis satisfait de la façon dont ces appels de fonction fonctionnent ensemble. Je voudrais savoir s'il existe une combinaison d'appels de fonction entièrement différente (plus efficace) que je pourrais utiliser pour obtenir le même résultat.

Cette question a été inspirée par ma réponse à cette question .

Réponses

1 AkshaySehgal Aug 20 2020 at 20:29

Une manière encore plus " numpythonique " de résoudre ce problème que l'autre réponse est -

np.concatenate(np.repeat(np.split(a, np.cumsum(chunks))[:-1], repeats))
array([ 0,  1,  2, 10, 11, 10, 11, 10, 11, 20, 21, 22, 23, 20, 21, 22, 23])

Remarquez, pas de boucles for explicites.

( np.splita une boucle implicite comme indiqué par @Divakar).


EDIT: Benchmarks (MacBook pro 13) -

La solution de Divakar s'adapte mieux aux tableaux plus grands, aux blocs et aux répétitions comme l'a souligné @Mad Physicist dans son article.

1 Valdi_Bo Aug 20 2020 at 19:35

Une manière plus numpythonique de faire votre tâche (que l'autre réponse) est:

result = np.concatenate([ np.tile(tbl, rpt) for tbl, rpt in
    zip(np.split(a, np.cumsum(chunks[:-1])), repeats) ])

Le résultat est:

array([ 0,  1,  2, 10, 11, 10, 11, 10, 11, 20, 21, 22, 23, 20, 21, 22, 23])
1 Divakar Aug 20 2020 at 20:06

Pour ces morceaux étant des tableaux de plage, nous pouvons travailler directement sur le tableau d'entrée et ainsi éviter l'étape finale d'indexation et cela devrait améliorer les choses -

# https://stackoverflow.com/a/47126435/ @Divakar
def create_ranges(starts, ends, l):
    clens = l.cumsum()
    ids = np.ones(clens[-1],dtype=int)
    ids[0] = starts[0]
    ids[clens[:-1]] = starts[1:] - ends[:-1]+1
    out = ids.cumsum()
    return out

s = np.r_[0,chunks.cumsum()]
starts = a[np.repeat(s[:-1],repeats)]
l = np.repeat(chunks, repeats)
ends = starts+l
out = create_ranges(starts, ends, l)
MadPhysicist Aug 20 2020 at 20:47

À titre informatif, j'ai comparé les solutions de travail ici:

def MadPhysicist1(a, chunks, repeats):
    in_offset = np.r_[0, np.cumsum(chunks[:-1])]
    out_offset = np.r_[0, np.cumsum(chunks[:-1] * repeats[:-1])]
    output = np.zeros((chunks * repeats).sum(), dtype=a.dtype)
    for c in range(len(chunks)):
        for r in range(repeats[c]):
            for i in range(chunks[c]):
                output[out_offset[c] + r * chunks[c] + i] = a[in_offset[c] + i]
    return output

def MadPhysicist2(a, chunks, repeats):
    regions = chunks * repeats
    index = np.arange(regions.sum())

    segments = np.repeat(chunks, repeats)
    resets = np.cumsum(segments[:-1])
    offsets = np.zeros_like(index)
    offsets[resets] = segments[:-1]
    offsets[np.cumsum(regions[:-1])] -= chunks[:-1]

    index -= np.cumsum(offsets)

    output = a[index]
    return output

def create_ranges(starts, ends, l):
    clens = l.cumsum()
    ids = np.ones(clens[-1],dtype=int)
    ids[0] = starts[0]
    ids[clens[:-1]] = starts[1:] - ends[:-1]+1
    out = ids.cumsum()
    return out

def Divakar(a, chunks, repeats):
    s = np.r_[0, chunks.cumsum()]
    starts = a[np.repeat(s[:-1], repeats)]
    l = np.repeat(chunks, repeats)
    ends = starts+l
    return create_ranges(starts, ends, l)

def Valdi_Bo(a, chunks, repeats):
    return np.concatenate([np.tile(tbl, rpt) for tbl, rpt in
                           zip(np.split(a, np.cumsum(chunks[:-1])), repeats)])

def AkshaySehgal(a, chunks, repeats):
    return np.concatenate(np.repeat(np.split(a, np.cumsum(chunks))[:-1], repeats))

J'ai regardé les horaires pour trois tailles d'entrée: ~ 100, ~ 1000 et ~ 10k éléments:

np.random.seed(0xA)
chunksA = np.random.randint(1, 10, size=20)   # ~100 elements
repeatsA = np.random.randint(1, 10, size=20)
arrA = np.random.randint(100, size=chunksA.sum())

np.random.seed(0xB)
chunksB = np.random.randint(1, 100, size=20)  # ~1000 elements
repeatsB = np.random.randint(1, 10, size=20)
arrB = np.random.randint(100, size=chunksB.sum())

np.random.seed(0xC)
chunksC = np.random.randint(1, 100, size=200)  # ~10000 elements
repeatsC = np.random.randint(1, 10, size=200)
arrC = np.random.randint(100, size=chunksC.sum())

Voici quelques résultats:

|               |    A    |    B    |    C    |
+---------------+---------+---------+---------+
| MadPhysicist1 | 1.92 ms |   16 ms |  159 ms |
| MadPhysicist2 | 85.5 µs |  153 µs |  744 µs |
| Divakar       | 75.9 µs | 95.9 µs |  312 µs |
| Valdi_Bo      |  370 µs |  369 µs |  3.4 ms |
| AkshaySehgal  |  163 µs |  165 µs | 1.24 ms |