Julia - Repeat entries of a vector by another vector (inner)

Viewed 233

I have an array x and I would like to repeat each entry of x a number of times specified by the corresponding entries of another array y, of the same length of x.

x = [1, 2, 3, 4, 5]    # Array to be repeated
y = [3, 2, 1, 2, 3]    # Repetitions for each element of x
# result should be [1, 1, 1, 2, 2, 3, 4, 4, 5, 5, 5]

Is there a way to do this in Julia?

4 Answers

You've given what I would have suggested as the answer already in your comment:

vcat(fill.(x, y)...)

How does this work? Start with fill:

help?> fill

  fill(x, dims::Tuple)
  fill(x, dims...)

  Create an array filled with the value x. For example, fill(1.0, (5,5)) returns a 5×5 array of floats, with each element initialized to 1.0.

This is a bit more complicated than it needs to be for our case (where we only have one dimension to fill into), so let's look at a simple example:

julia> fill(1, 3)
3-element Vector{Int64}:
 1
 1
 1

so fill(1, 3) just means "take the number one, and put this number into a one-dimensional array 3 times."

This of course is exactly what we want to do here: for every element in x, we want an array that holds this element multiple times, with the multiple given by the corresponding element in y. We could therefore loop over x and y and do something like:

julia> for (xᵢ, yᵢ) ∈ zip(x, y)
           fill(xᵢ, yᵢ)
       end

Now this loop doesn't return anything, so we'd have to preallocate some storage and assign to that within the loop. A more concise way of writing this while automatically returning an object would be a comprehension:

julia> [fill(xᵢ, yᵢ) for (xᵢ, yᵢ) ∈ zip(x, y)]
5-element Vector{Vector{Int64}}:
 [1, 1, 1]
 [2, 2]
 [3]
 [4, 4]
 [5, 5, 5]

and even more concisely, we can just use broadcasting:

julia> fill.(x, y)
5-element Vector{Vector{Int64}}:
 [1, 1, 1]
 [2, 2]
 [3]
 [4, 4]
 [5, 5, 5]

so from the comprehension or the broadcast we are getting a vector of vectors, each vector being an element of x repeated y times. Now all that remains is to put these together into a single vector by concatenating them vertically:


julia> vcat(fill.(x, y)...)
11-element Vector{Int64}:
 1
 1
 1
 2
 2
 3
 4
 4
 5
 5
 5

Here we are using splatting to essentially do:

z = fill.(x, y)
vcat(z[1], z[2], z[3], z[4], z[5])

Note that splatting can have suboptimal performance for arrays of variable length, so a better way is to use reduce which is special cased for this and will give the same result:

reduce(vcat, fill.(x, y))

Your x and y vectors constitute what is called a run-length encoding of the vector [1, 1, 1, 2, 2, 3, 4, 4, 5, 5, 5]. So if you take the inverse of the run-length encoding, you will get the vector you are looking for. The StatsBase.jl package contains the rle and inverse_rle functions. We can use inverse_rle like this:

julia> using StatsBase

julia> x = [1, 2, 3, 4, 5];

julia> y = [3, 2, 1, 2, 3];

julia> inverse_rle(x, y)
11-element Vector{Int64}:
 1
 1
 1
 2
 2
 3
 4
 4
 5
 5
 5

If performance is a priority, you can also do it the long, manual way:

function runlengthdecode(vals::Vector{T}, reps::Vector{<:Integer}) where T
  length(vals) == length(reps) || throw(ArgumentError("Same number of values and counts expected"))

  result = Vector{T}(undef, sum(reps))
  resind = 1
  for (valind, numrep) in enumerate(reps)
    for i in 1:numrep
      @inbounds result[resind] = vals[valind]
      resind += 1
    end
  end

  result
end

This runs about 12 times faster than the vcat/fill based method for the given data, likely because of avoiding creating all the intermediate filled vectors.

You can also instead use fill! on the preallocated result's @views, by replacing the loop in above code with:

  for (val, numrep) in zip(vals, reps)
    fill!(@view(result[resind:resind + numrep - 1]), val)
    resind += numrep
  end

which has comparable performance.

Also, for completeness, a comprehension can be quite handy for this. And it's faster than fill and vcat.

julia> [x[i] for i=1:length(x) for j=1:y[i]]
11-element Vector{Int64}:
 1
 1
 1
 2
 2
 3
 4
 4
 5
 5
 5
Related