#performance #r #for-loop #dataframe
#Производительность #r #цикл for #фрейм данных
Вопрос:
Я написал некоторый код, используемый для организации выборки данных с разной частотой, но я широко использовал циклы for, которые значительно замедляют работу кода при большом наборе данных. Я просматривал свой код, находя способы удаления циклов for, чтобы ускорить его, но один из циклов поставил меня в тупик.
В качестве примера предположим, что данные были отобраны с частотой 3 Гц, поэтому я получаю три строки за каждую секунду данных. Однако переменные A, B и C дискретизируются с частотой 1 Гц каждая, поэтому я буду получать одно значение каждые три строки для каждой из них. Выборка переменных выполняется последовательно в течение одной секунды, что приводит к диагональному характеру данных.
Чтобы еще больше усложнить ситуацию, иногда строка теряется в исходном наборе данных.
Моя цель такова: определив строки, которые я хочу сохранить, я хочу переместить значения, отличные от NA, из последующих строк вверх в строки хранителя. Если бы не проблема с потерянными данными, я бы всегда сохранял строку, содержащую значение для первой переменной, но если одна из этих строк будет потеряна, я сохраню следующую строку.
В приведенном ниже примере шестой образец и десятый образец теряются.
A <- c(1, NA, NA, 4, NA, 7, NA, NA, NA, NA)
B <- c(NA, 2, NA, NA, 5, NA, 8, NA, 11, NA)
C <- c(NA, NA, 3, NA, NA, NA, NA, 9, NA, 12)
test_df <- data.frame(A = A, B = B, C = C)
test_df
A B C
1 1 NA NA
2 NA 2 NA
3 NA NA 3
4 4 NA NA
5 NA 5 NA
6 7 NA NA
7 NA 8 NA
8 NA NA 9
9 NA 11 NA
10 NA NA 12
keep_rows <- c(1, 4, 6, 9)
После того, как я переместил значения в строки хранителя, я удалю промежуточные строки, что приведет к следующему:
test_df <- test_df[keep_rows, ]
test_df
A B C
1 1 2 3
2 4 5 NA
3 7 8 9
4 NA 11 12
В конце концов, мне нужна только одна строка для каждой секунды данных, а значения NA должны оставаться только там, где была потеряна строка исходных данных.
Есть ли у кого-нибудь идеи о том, как перемещать данные вверх без использования цикла for? Я был бы признателен за любую помощь! Извините, если этот вопрос слишком многословен; Я хотел ошибиться в сторону слишком большого количества информации, а не недостаточно.
Комментарии:
1. Чтобы уточнить: могут ли быть потеряны две или более последовательных строк? Если, скажем, вместо удаления 6 и 10 вы сбросили 4, 5 и 6, как бы вы определили, что это произошло?
2. Да, несколько последовательных строк могут быть потеряны. Я определил, какие строки следует сохранить, приняв это во внимание в другом месте моего кода, и в итоге получилось бы что-то вроде переменной «rows_to_keep», которую я привел в своем примере, генерируемой кодом, а не определяемой пользователем. Я не уверен, что это вызовет проблему в приведенных решениях, поскольку переменная «rows_to_keep» не была реализована.
Ответ №1:
Это должно сделать это:
test_df = with(test_df, cbind(A[1:(length(A)-2)], B[2:(length(B)-1)], C[3:length(C)]))
test_df = data.frame(test_df[!apply(test_df, 1, function(x) all(is.na(x))), ])
colnames(test_df) = c('A', 'B', 'C')
> test_df
A B C
1 1 2 3
2 4 5 NA
3 7 8 9
4 NA 11 12
И если вы хотите что-то еще быстрее:
test_df = data.frame(test_df[rowSums(is.na(test_df)) != ncol(test_df), ])
Комментарии:
1. Цикл не является a
for
, но это все равно цикл.2. См. Редактирование. Это всегда будет цикл, но, по крайней мере, этот цикл весь в коде C.
3. Это не «всегда» будет цикл. Я уверен, что есть векторизованное решение. Я напишу это сегодня вечером, если никто меня не опередит.
4. @goodside Отлично, я хотел бы знать, есть ли. Второй метод, описанный выше, занимает всего 50 мс для 10 ^ 6 строк на моей машине, но вы можете сравнить его с тем, что вы делаете на своей.
5. Спасибо, Джон. Это решение работает для меня! Мне нужно лучше ознакомиться с функциями «with» и «apply», чтобы я мог сам придумать ответ такого типа.
Ответ №2:
Основываясь на замечательном ответе @John Colby, мы можем избавиться от шага apply и ускорить его совсем немного (примерно в 20 раз):
# Create a bigger test set
A <- c(1, NA, NA, 4, NA, 7, NA, NA, NA, NA)
B <- c(NA, 2, NA, NA, 5, NA, 8, NA, 11, NA)
C <- c(NA, NA, 3, NA, NA, NA, NA, 9, NA, 12)
n=1e6; test_df = data.frame(A=rep(A, len=n), B=rep(B, len=n), C=rep(C, len=n))
# John Colby's method, 9.66 secs
system.time({
df1 = with(test_df, cbind(A[1:(length(A)-2)], B[2:(length(B)-1)], C[3:length(C)]))
df1 = data.frame(df1[!apply(df1, 1, function(x) all(is.na(x))), ])
colnames(df1) = c('A', 'B', 'C')
})
# My method, 0.48 secs
system.time({
df2 = with(test_df, data.frame(A=A[1:(length(A)-2)], B=B[2:(length(B)-1)], C=C[3:length(C)]))
df2 = df2[is.finite(with(df2, A|B|C)),]
row.names(df2) <- NULL
})
identical(df1, df2) # TRUE
… Хитрость здесь в том, что A|B|C
это только NA
в том случае, если все значения NA
. Это оказывается намного быстрее, чем вызов all(is.na(x))
каждой строки матрицы с помощью apply
.
EDIT у @John другой подход, который также ускоряет процесс. Я добавил некоторый код, чтобы превратить результат в data.frame с правильными именами и синхронизировать его. Похоже, это почти такая же скорость, как и мое решение.
# John's method, 0.50 secs
system.time({
test_m = with(test_df, cbind(A[1:(length(A)-2)], B[2:(length(B)-1)], C[3:length(C)]))
test_m[is.na(test_m)] <- -1
test_m <- test_m[rowSums(test_m) > -3,]
test_m[test_m == -1] <- NA
df3 <- data.frame(test_m)
colnames(df3) = c('A', 'B', 'C')
})
identical(df1, df3) # TRUE
ОТРЕДАКТИРУЙТЕ ЕЩЕ РАЗ …и обновленный ответ @John Colby еще быстрее!
# John Colby's method, 0.39 secs
system.time({
df4 = with(test_df, cbind(A[1:(length(A)-2)], B[2:(length(B)-1)], C[3:length(C)]))
df4 = data.frame(df4[rowSums(is.na(df4)) != ncol(df4), ])
colnames(df4) = c('A', 'B', 'C')
})
identical(df1, df4) # TRUE
Комментарии:
1. Я не знал о функции system.time, но я собираюсь извлечь из нее много пользы! Спасибо за это и за предложенный метод.
Ответ №3:
Итак, ваш вопрос был просто о движении вверх без цикла. Итак, очевидно, вы уже решили первый шаг.
> test_m <- with( test_df, cbind(A[1:(length(A)-2)], B[2:(length(B)-1)], C[3:length(C)]) )
> test_m
[,1] [,2] [,3]
[1,] 1 2 3
[2,] NA NA NA
[3,] NA NA NA
[4,] 4 5 NA
[5,] NA NA NA
[6,] 7 8 9
[7,] NA NA NA
[8,] NA 11 12
Который теперь является матрицей. Вы можете легко исключить строки, для которых теперь нет точки данных, без цикла. Если вы хотите вернуть его в data.frame, вы можете использовать другой метод, но этот будет работать быстрее всего для большого массива данных. Мне нравится просто делать NA невозможным значением … возможно, -1, но вы будете лучше знать свои данные … возможно, -pi.
test_m[is.na(test_m)] <- -1
А теперь просто выберите строки для свойства этих невозможных чисел
test_m <- test_m[rowSums(test_m) > -3,]
И, если вы хотите, вы можете вернуть NA обратно.
test_m[test_m == -1] <- NA
test_m
[,1] [,2] [,3]
[1,] 1 2 3
[2,] 4 5 NA
[3,] 7 8 9
[4,] NA 11 12
Цикла ( for
или apply
) нет, а функция one, применяемая к строкам матрицы, специально оптимизирована и выполняется очень быстро (rowSums).
Комментарии:
1. Спасибо, Джон. Метод перемещения в NA и из NA, который вы предложили здесь, несомненно, пригодится мне в будущем.