Julia — Повторные записи вектора другим вектором (внутренним)

#julia

Вопрос:

У меня есть массив x , и я хотел бы повторить каждую запись x несколько раз, указанное соответствующими записями другого массива y , той же длины 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]
 

Есть ли способ сделать это в Julia?

Комментарии:

1. О, по-видимому, можно сделать vcat(fill.(x, y)...) , хотя я не совсем понимаю, как это работает

Ответ №1:

Ваши x и y векторы представляют собой то, что называется кодированием вектора по длине [1, 1, 1, 2, 2, 3, 4, 4, 5, 5, 5] . Итак, если вы возьмете обратную кодировку длины выполнения, вы получите искомый вектор. Пакет StatsBase.jl содержит функции rle и inverse_rle . Мы можем использовать inverse_rle так:

 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
 

Комментарии:

1. Это определенно лучший вариант здесь. Быстрый (немного превосходит даже мой ответ «ручным способом»), простой в понимании и обслуживании — вполне оправдывает стоимость одной (довольно стандартной) зависимости пакета.

2. @sundar-RememberMonica Просто просматривает исходный код, он выглядит довольно близко к вашему «ручному способу», вложенный цикл написан немного по-другому, но, насколько я могу судить, он выполняет те же итерации.

3. @BatWannaBe Да, и поучительно увидеть различия, которые компенсируют эти последние различия в производительности. Избегание enumerate , не отбрасывая length результат вверху, кажется большим, но маленькие, такие как выведение индексации массива из внутреннего цикла, также, похоже, складываются.

Ответ №2:

Вы дали то, что я бы предложил в качестве ответа, уже в своем комментарии:

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

Как это работает? Начните с 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.
 

Это немного сложнее, чем нужно для нашего случая (где у нас есть только одно измерение для заполнения), поэтому давайте рассмотрим простой пример:

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

это fill(1, 3) просто означает «возьмите число один и поместите это число в одномерный массив 3 раза».

Это, конечно, именно то, что мы хотим сделать здесь: для каждого элемента in x нам нужен массив, который содержит этот элемент несколько раз, с кратным, заданным соответствующим элементом in y . Поэтому мы могли бы перебирать x и y и делать что-то вроде:

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

Теперь этот цикл ничего не возвращает, поэтому нам придется предварительно выделить некоторое хранилище и назначить его внутри цикла. Более кратким способом записи этого при автоматическом возврате объекта было бы понимание:

 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]
 

и еще более кратко, мы можем просто использовать широковещательную передачу:

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

итак, из понимания или трансляции мы получаем вектор векторов, каждый вектор является элементом x повторяющихся y времен. Теперь все, что остается, это объединить их в один вектор, объединив их по вертикали:

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

Здесь мы используем splatting, чтобы по существу сделать:

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

Обратите внимание, что splatting может иметь неоптимальную производительность для массивов переменной длины, поэтому лучшим способом является использование reduce , которое специально предназначено для этого и даст тот же результат:

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

Комментарии:

1. Не могли бы вы подробнее прокомментировать, почему разбиение больших массивов (или, возможно, в более общем смысле, итераций) может иметь плохую производительность? Моим единственным предположением была бы задержка при компиляции метода для каждой длины, но я думал, что Julia не специализировалась автоматически на VarArgs именно по этой причине (упомянуто в советах по производительности).

2. Это справедливый момент, мой ответ был сформулирован там несколько вводящим в заблуждение — это более переменная длина, чем длина как таковая. Конечно, в подобном случае это на самом деле не имеет значения, это будет иметь значение только в том случае, если vcat(fill.(x, y)...) произойдет в горячем цикле с изменением x и y длиной.

3. Я внимательно изучил его в советах по производительности и форумах и нашел пару вещей. Кто-то утверждает, что специализация переменной длины является проблемой, и в советах по производительности говорится, что VarArg сделает метод специализированным, если он не просто передается другому методу (не уверен, как, поскольку все является вызовом метода). Кто-то также указал, что переменные аргументы (даже если они разделены из ленивой итерации) упаковываются в тип кортежа, зависящий от длины, так что это явная стоимость времени и памяти. В любом случае, правильный способ — это цикл или сокращение с помощью двоичных методов, как в вашем ответе.

Ответ №3:

Если производительность является приоритетом, вы также можете сделать это длинным, ручным способом:

 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

 

Это выполняется примерно в 12 раз быстрее, чем vcat fill метод на основе / для заданных данных, вероятно, из-за того, что не нужно создавать все промежуточные fill векторы редактирования.

Вы также можете вместо этого использовать fill! в предварительно @view выделенных результатах s, заменив цикл в приведенном выше коде на:

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

который имеет сопоставимую производительность.

Ответ №4:

Кроме того, для полноты понимания для этого может быть весьма удобно. И это быстрее, чем fill и 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