Как я могу улучшить этот цикл for для индексации определенных строк вектора с большим набором данных?

Приношу извинения, если это неправильно отформатировано или мне не хватает какой-либо полезной информации. Я пытаюсь запустить цикл for с вложенным оператором if для нескольких больших наборов данных. Код:

ID_index <- data.frame()
for (x in 1:length(peaks)) {
  z <- ((mass_combo - peaks[x])/mass_combo)*10^6
  if (length(which(abs(z) < 1)) > 0) {
    ID_index[nrow(ID_index)+1, 1] <- peaks[x]
    ID_index[nrow(ID_index), 2] <- toString(which(abs(z) < 1))
  }
}

Векторные пики представляют собой вектор длиной ~130 000 числовых компонент. Вектор Mass_combo — это числовой вектор, содержащий более 150 000 000 компонентов. Я пытаюсь индексировать, находится ли каждое экспериментальное значение (пики) в пределах заданной погрешности (z) вектора возможных теоретических значений (mass_combo). Теоретические значения были предварительно рассчитаны путем взятия всех возможных комбинаций компонентов с помощью функции expand.grid.

У меня этот цикл работал примерно 2 часа, и цикл прошел всего 5700 итераций. Пример ввода и вывода:

peaks <- c(178.1161, 182.0530, 186.1223)
mass_combo <- c(154.1161, 166.1161, 178.1161, 190.1161, 202.1161, 214.1161, 226.1161, 170.053, 
                182.053, 194.053, 206.053, 709.0452, 721.0452, 182.0530, 194.0530, 186.1223, 
                198.1223, 210.1223)

ID_index
V1          V2
178.1161    3
182.0530    9, 14
186.1223    16

Есть ли лучший метод, который я могу использовать для индексации и определения совпадений теоретических значений для каждого экспериментального результата? Любая помощь или предложения будут с благодарностью приняты.

Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
2
0
119
4
Перейти к ответу Данный вопрос помечен как решенный

Ответы 4

Достаточно хорошо документировано, что выращивание объектов в цикле for очень неэффективно и является самым большим источником неэффективности вашего кода.

Вот несколько вещей, которые я бы сделал, чтобы улучшить скорость вашего цикла for:

  1. Предварительно выделите память для назначения индексов в пределах вашего допуска.
    • Вы уже знаете размер выходного целевого вектора: length(peaks). Используйте это для инициализации вектора правильного размера. Таким образом, вы не будете выращивать его внутри цикла.
  2. Не дублируйте вычисления
    • В вашем примере вы запускаете which(abs(z) < 1) дважды. Один раз в операторе if и еще раз для тех, кто передает ваш условный оператор.
  3. Не назначайте, если вам не нужно
    • Первый столбец в создаваемом вами фрейме данных — это просто peaks. Нет необходимости создавать это в своем цикле for, просто создайте его постфактум.

Например,

for (x in seq_along(peaks)) {
  z <- ((mass_combo - peaks[x])/mass_combo)*10^6
  idx <- which(abs(z) < 1)
  if (length(idx) > 0) {
    tol_idx[x] <- toString(idx)
  }
}
ID_index <- data.frame(peaks, tol_idx)

Отсюда вы можете продолжать совершенствоваться, рассматривая, какие оставшиеся выражения необходимы. Например, вам нужен оператор if?

Если ни один из mass_combo не находится в пределах вашего допуска, то which() вернет пустой вектор, а если передать его toString(), то вернет пустую строку: "". Это то же значение, которое инициализируется vector():

vector(mode = "character", length = 1L)
# [1] ""

which(c(F, F))
# integer(0)

toString(which(c(F, F)))
# [1] ""

Это позволяет вам полностью удалить это условие и повысить производительность:

tol_idx <- vector(mode = "character", length = length(peaks))
for (x in seq_along(peaks)) {
  z <- ((mass_combo - peaks[x])/mass_combo)*10^6
  tol_idx[x] <-  toString(which(abs(z) < 1))
}

ID_index <- data.frame(peaks, tol_idx)

Редактировать

Судя по комментарию @ThomasIsCoding, неопределенность в размере вашего вывода может быть связана только с желанием сохранить совпадающие строки. Я думаю, что более эффективно хранить все наблюдения, а затем фильтровать фрейм данных постфактум:

ID_index <- data.frame(peaks, tol_idx)

# remove rows where tol_idx is ""
ID_index <- ID_index[nzchar(ID_index$tol_idx),] 

Контрольный показатель

Ваш пример небольшой, но экономия времени будет зависеть от размера ваших реальных данных. В этом небольшом примере ваш код занимает примерно на 66% больше времени:

microbenchmark::microbenchmark(
  `LMc if` = {
    tol_idx <- vector(mode = "character", length = length(peaks))
    for (x in seq_along(peaks)) {
      z <- ((mass_combo - peaks[x])/mass_combo)*10^6
      idx <- which(abs(z) < 1)
      if (length(idx) > 0) {
        tol_idx[x] <- toString(idx)
      }
    }
    ID_index <- data.frame(peaks, tol_idx)
  },
  `LMc no if` = {
    tol_idx <- vector(mode = "character", length = length(peaks))
    for (x in seq_along(peaks)) {
      z <- ((mass_combo - peaks[x])/mass_combo)*10^6
      tol_idx[x] <-  toString(which(abs(z) < 1))
    }
    
    ID_index <- data.frame(peaks, tol_idx)
  },
  `TCB at EU` = {
    ID_index <- data.frame()
    for (x in 1:length(peaks)) {
      z <- ((mass_combo - peaks[x])/mass_combo)*10^6
      if (length(which(abs(z) < 1)) > 0) {
        ID_index[nrow(ID_index)+1, 1] <- peaks[x]
        ID_index[nrow(ID_index), 2] <- toString(which(abs(z) < 1))
      }
    }
  },
  unit = "relative"
)
Unit: relative
      expr      min       lq     mean   median       uq       max neval cld
    LMc if 1.218349 1.217380 1.561912 1.220759 1.213983 15.954036   100  ab
 LMc no if 1.000000 1.000000 1.000000 1.000000 1.000000  1.000000   100  a 
 TCB at EU 1.712652 1.705199 1.664062 1.702189 1.661490  1.357403   100   b

Если прироста скорости, полученного в результате этих рекомендаций, все еще недостаточно, вы можете рассмотреть возможность параллельной обработки ваших итераций, которые также обеспечат большой прирост производительности.

Я думаю, вам нужно опустить строки с пустым tol_idx, т. е. "", в соответствии с форматом вывода OP. В любом случае, хороший ответ с обоснованной ссылкой, +1!

ThomasIsCoding 10.06.2024 23:48

Привет @LMc Спасибо за отзывы и предложения. Я самоучка, и кажется, что некоторые методы, которые я использовал для написания кода, оказались неэффективными. Вероятно, я этого не заметил, поскольку раньше не работал с такими большими наборами данных. Я приму ваши предложения и постараюсь реализовать их, чтобы улучшить свой код! Метод, предоставленный @ThomasIsCoding, оказался самым быстрым методом, когда я масштабировал объекты до полного mass_combo размера и использовал более крупные входные данные для peaks. Можно ли задать вопрос завтра в этой теме, если у меня будут продолжения?

TCB at EU 11.06.2024 02:12

Это здорово, и я надеюсь, что вы чему-то научились. У @ThomasIsCoding всегда есть быстрые ответы, из которых вы можете многому научиться, внимательно их изучив. Если у вас есть дополнительные сведения, связанные с этим конкретным вопросом или решениями, приведенными здесь, это нормально, но если ваш вопрос немного отличается от содержания этого сообщения, я бы рекомендовал опубликовать новый вопрос.

LMc 11.06.2024 02:40

Учитывая, что вы имеете дело с большими данными, вам, возможно, придется перебирать каждый peaks для получения желаемых индексов (в противном случае векторизованный подход может вызвать некоторые проблемы с нехваткой памяти).

В вашем коде та часть, которая итеративно обновляет фрейм данных путем добавления новых строк, является медленной операцией.

ID_index[nrow(ID_index)+1, 1] <- peaks[x]
ID_index[nrow(ID_index), 2] <- toString(which(abs(z) < 1))

который на самом деле можно оптимизировать, избегая динамического обновления.


Пример сравнительного анализа

set.seed(0)
peaks <- rnorm(1e3, 200)
mass_combo <- rnorm(1e5, 200)


f1 <- \() {
  ID_index <- data.frame()
  for (x in 1:length(peaks)) {
    z <- ((mass_combo - peaks[x]) / mass_combo) * 10^6
    if (length(which(abs(z) < 1)) > 0) {
      ID_index[nrow(ID_index) + 1, 1] <- peaks[x]
      ID_index[nrow(ID_index), 2] <- toString(which(abs(z) < 1))
    }
  }
  ID_index
}

f2 <- \() {
  subset(
    data.frame(
      V1 = peaks,
      V2 = unlist(
        lapply(
          peaks,
          function(x) {
            toString(which(abs(1 - x / mass_combo) < 1e-6))
          }
        ), FALSE, FALSE
      )
    ),
    nzchar(V2)
  )
}

microbenchmark(
  f1 = f1(),
  f2 = f2(),
  times = 10L,
  check = "equivalent",
  unit = "relative"
)

который показывает

Unit: relative
 expr      min       lq     mean   median       uq      max neval
   f1 2.434582 2.506911 2.504454 2.474551 2.579847 2.497963    10
   f2 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000    10

Привет @ThomasIsCoding, спасибо за ответ и за предложения. Как я уже говорил @LMc, я довольно дилетант в вопросах оптимизации кода и не имею формального образования. Я ценю ваше предложение и код, который вы рекомендовали. Использование предоставленного вами subset дало самый быстрый код и результаты сопоставления, когда я масштабировал данные до более крупного набора данных (peaks с 30 значениями и mass_combo с полным списком). Я опробую этот код в полном списке peaks, когда завтра приду на работу. Можно ли продолжить эту тему завтра, если у меня возникнут какие-либо проблемы?

TCB at EU 11.06.2024 02:18

@TCBatEU, без проблем. Если ваши последующие вопросы сильно отличаются, я предлагаю вам открыть новую заявку на вопросы.

ThomasIsCoding 11.06.2024 09:39
Ответ принят как подходящий

После некоторой алгебры, чтобы перестроить сравнение, операцию можно реализовать как data.table неэквисоединение, что будет намного быстрее, чем цикл.

Демонстрируем на примере данных:

data.table(x = mass_combo)[,`:=`(min = x - x/1e6, max = x + x/1e6, r = .I)][
  data.table(y = peaks), .(peak = y, r), on = .(min < y, max > y)
][,.(idx = .(r)), peak]
#>        peak    idx
#>       <num> <list>
#> 1: 178.1161      3
#> 2: 182.0530   9,14
#> 3: 186.1223     16

Или чтобы получить значения mass_combo вместо индексов, согласно комментариям:

data.table(x = mass_combo)[,`:=`(min = x - x/1e6, max = x + x/1e6)][
  data.table(y = peaks), .(peak = y, x), on = .(min < y, max > y)
][,.(mass_combo = .(x)), peak]
#>        peak      mass_combo
#>       <num>          <list>
#> 1: 178.1161        178.1161
#> 2: 182.0530 182.053,182.053
#> 3: 186.1223        186.1223

Синхронизация набора данных с приблизительными размерами ваших фактических данных:

peaks <- runif (13e4)
mass_combo <- runif (15e7)

system.time(
  ID_index <-
    data.table(x = mass_combo)[,`:=`(min = x - x/1e6, max = x + x/1e6, r = .I)][
      data.table(y = peaks), .(peak = y, r), on = .(min < y, max > y)
    ][,.(idx = .(r)), peak]
)
#>    user  system elapsed 
#>   71.22    4.47   25.64

Первые 10 рядов:

ID_index[1:10]
#>           peak                                                       idx
#>          <num>                                                    <list>
#>  1: 0.54024270        267797,2733918,3335681,3499464,3800516,4405793,...
#>  2: 0.52608405        263261, 383398,1462884,1510015,2504774,2521597,...
#>  3: 0.41793366        847991,1156169,1213597,1489392,1680001,3838945,...
#>  4: 0.24143285        512967,1296863,3515676,4526957,5952832,6020254,...
#>  5: 0.89169524        285889, 785031, 829258, 966923,1044726,2564670,...
#>  6: 0.09785744 20626427,23309899,23498292,23712990,23870867,35958767,...
#>  7: 0.07118160   681958, 7164888,10311600,11386095,12753834,34130008,...
#>  8: 0.27893975       1378505,2577870,3153625,3320806,5095727,9098257,...
#>  9: 0.01651383  1503323, 5273696,27981236,30451611,42380615,42807730,...
#> 10: 0.61528940       2191889,2570098,2979545,3398146,4041039,4652609,...

Сравните с циклическим подходом к векторам, которые в 10 раз меньше:

peaks <- runif (13e3)
mass_combo <- runif (15e6)

system.time({ # from @ThomasIsCoding
  subset(
    data.frame(
      V1 = peaks,
      V2 = unlist(
        lapply(
          peaks,
          function(x) {
            toString(which(abs(1 - x / mass_combo) < 1e-6))
          }
        ), FALSE, FALSE
      )
    ),
    nzchar(V2)
  )
})
#>    user  system elapsed 
#>  920.64  542.20 1463.61

system.time(
  data.table(x = mass_combo)[,`:=`(min = x - x/1e6, max = x + x/1e6, r = .I)][
    data.table(y = peaks), .(peak = y, r), on = .(min < y, max > y)
  ][,.(idx = .(r)), peak]
)
#>    user  system elapsed 
#>    5.15    0.26    2.14

Привет @jblood94, спасибо и тебе за предложение. Ваше решение оказалось даже быстрее, чем предыдущие рекомендации, как вы также продемонстрировали. Спасибо за отзыв, я постараюсь реализовать его в своем коде!

TCB at EU 11.06.2024 16:27

Этот код работает очень хорошо при масштабировании до полного набора данных! Он может предоставить индексы для всего за очень короткий период, и я благодарю вас за это. Поскольку это сработало так хорошо, можно ли это изменить, чтобы вместо этого давать индексируемое значение? Итак, в приведенном вами примере вместо указания индекса в ID_index[,2] вместо этого указано индексированное значение?

TCB at EU 11.06.2024 19:33

Получить значения немного проще, чем получить индексы. См. второй блок кода в обновленном ответе.

jblood94 11.06.2024 21:45

Спасибо @jblood94, обе версии кода очень полезны! Я ценю дополнение, которое вы внесли в ответ.

TCB at EU 12.06.2024 22:05

Вот еще одно решение на базе R, которое интересно и увлекательно играть, но может подойти только для небольших или средних наборов данных.

Например, с данными peaks и mass_combo в вашем вопросе вы можете использовать outer + which + split, как показано ниже.

with(
    as.data.frame(which(abs(1 - outer(peaks, mass_combo, `/`)) < 1e-6, TRUE)),
    split(col, row)
)

который дает

$`1`
[1] 3

$`2`
[1]  9 14

$`3`
[1] 16

Предупреждение!!!

Описанный выше подход не применим к большим данным, поскольку outer требует больших затрат памяти, и у вас могут возникнуть проблемы с нехваткой памяти, если вы создадите титаническую матрицу. Поэтому НЕ РЕКОМЕНДУЕТСЯ для больших наборов данных.

Привет @ThomasIsCoding, спасибо за это дополнительное предложение! Как вы предупреждали, для большого набора данных это не сработало, но я могу реализовать это для другой части кода, над которым работаю. Спасибо.

TCB at EU 12.06.2024 22:06

Другие вопросы по теме