Р | За кулисами | Векторы

Как R обрабатывает векторы?

С легкой, волшебной любовью конечно!

R — очень простой язык для изучения. Это причина того, что он является лидером в области науки о данных: люди могут быстро приступить к работе. Векторы — это один из способов, с помощью которого R помогает упростить и ускорить работу. Но что именно происходит за кулисами, чтобы это произошло?

Что такое Вектор?

Векторы являются основой многих операций обработки данных в R. Они невероятно быстры по сравнению с циклами for или while. Это увеличение скорости связано с тем, что векторы могут хранить только один тип данных. Это означает, что если вы хотите создать вектор, вы не можете поместить в него как символьные, так и числовые переменные.

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



Векторизованные операции

Обычная техника, используемая в программировании для работы с несколькими объектами, состоит в том, чтобы перебирать их в цикле. Я уверен, что вы знакомы со скромным циклом for. Вы указываете ему, где начать и где остановиться, и он снова и снова будет выполнять какой-то код от начала до конца.

Давайте рассмотрим пример с использованием набора данных cars. Он включает в себя скорость (миль в час) и тормозной путь (футы) автомобилей. Если мы хотим рассчитать среднюю скорость в наборе данных, мы можем использовать цикл for. Мы создаем speed_sum как число, чтобы отслеживать сумму наших переменных скорости, затем мы перебираем каждое наблюдение в столбце speed фрейма данных cars. Наконец, мы делим сумму speed на количество строк в наборе данных для нашего ответа: 15.4.

speed_sum <- 0
for (i in 1:nrow(cars)) {
    speed_sum <- speed_sum + cars$speed[i]
}
speed_sum / nrow(cars)
[1] 15.4

Вот одна из замечательных особенностей векторов: многие функции оптимизированы для выполнения операций над ними. Это означает, что векторизованная функция может быть намного быстрее, чем цикл for.

Возьмем функцию mean. Он вычисляет среднее значение любого числового вектора, который мы ему даем. Для столбца speed числа cars это снова 15,4.

mean(cars$speed)
[1] 15.4

Насколько быстрее получается среднее значение с помощью цикла for по сравнению с использованием векторизованной функции mean? Ну и времени на написание цикла может быть много, в зависимости от задачи. Даже наш быстрый цикл for для нахождения среднего значения занимал 5 строк. Чем больше строк мы добавим, тем больше сложности нам потребуется для нашей задачи, тем больше у нас шансов накосячить и потратить время на отладку. С векторизованной функцией мы экономим много времени, потому что знаем, что функция берет вектор и выполняет вычисления.

Как насчет времени вычисления цикла по сравнению с векторизованной функцией? Что ж, на моем компьютере (Ubuntu 20.04, R 3.6.3, Ryzen 5 5600G, 16 ГБ ОЗУ, 256 ГБ SSD, RTX 3090) векторизация этой довольно простой операции может привести к значительному улучшению скорости выполнения. Мы видим, что с пакетом rbenchmark он может работать в 185 раз быстрее!

library(rbenchmark)
benchmark(
    "Loop" = {
        speed_sum <- 0
        for (i in 1:nrow(cars)) {
            speed_sum <- speed_sum + cars$speed[i]
        }
        speed_sum / nrow(cars)
    },
    "Vectorized" = {
        mean(cars$speed)
    }
)
     test     reps elapsed relative user.self sys.self user.child
1    Loop     100   0.185    185     0.185        0        0
2 Vectorized  100   0.001     1      0.001        0        0


Скалярные операции

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

Все еще в наборе данных cars, возможно, вы из разумной страны, где вы используете метрическую систему. Переменная speed записывается в милях в час. Вместо того, чтобы создавать цикл для умножения каждого числа в векторе на 1,609 для преобразования в километры в час, вы можете просто умножить вектор на число, а R позаботится обо всем остальном.

cars$speed * 1.609
# Note: only the first 5 numbers from the output are shown here
[1] 6.436  6.436 11.263 11.263 12.872 ...

Хотите объединить еще больше скалярных операций? Действуй! Как человек из сумасшедшей страны, где мы используем шкалу Фаренгейта для измерения температуры, данные по Цельсию не имеют для меня особого смысла. Набор данных beaver1 (встроенный в R) содержит записи температуры тела бобра в градусах Цельсия. С помощью некоторых скалярных операций я могу преобразовать их в единицы, которые лучше понимаю.

(beaver1$temp  * (9/5)) + 32
# Note: only the first 5 numbers from the output are shown here
[1] 97.394 97.412 97.430 97.556 97.790 ...

Вау, это горячий бобер! 🦫

2 вектора

Вычисление с двумя векторами вместе довольно просто, если они имеют одинаковую длину. Представьте, что я ищу автомобиль 1974 года. В наборе данных mtcars есть статистика по 32 автомобилям этого года. Я хочу что-то с большой мощностью, но не большим весом. Я могу вычислить отношение лошадиных сил (hp) к весу (wt), разделив два вектора.

mtcars$hp / mtcars$wt
# Note: only the first 5 numbers from the output are shown here
[1] 41.98473 38.26087 40.08621 34.21462 50.87209 ...

Довольно просто, правда? Что ж, мы только начинаем с двух векторов одинаковой длины. Поскольку эти векторы исходят из столбцов фрейма данных, мы можем использовать ту же концепцию для фильтрации нашего фрейма данных. Давайте отфильтруем автомобили с 6 и более цилиндрами. На этот раз наш результат будет включать все данные для выбранных строк.

mtcars[mtcars$cyl >= 6 ,]

Фильтр использует силу векторов, чтобы получить то, что мы хотим. Во-первых, он создает вектор TRUE и FALSE. Скалярные операции, о которых мы упоминали ранее, также могут быть логическими операторами! Таким образом, это утверждение действительно сравнивает каждый элемент вектора mtcars$cyl со скаляром 6 и дает вектор результатов.

mtcars$cyl >= 6
# Note: only the first 5 numbers from the output are shown here
[1] TRUE TRUE FALSE TRUE TRUE ...

Чтобы отфильтровать набор данных, мы можем использовать скобки и вектор того же размера из значений TRUE и FALSE, чтобы выбрать только то, что нам нужно. В нашем случае нам нужны строки, в которых наш вектор true/false говорит, что количество цилиндров больше или равно 6.

Давайте рассмотрим еще один векторный трюк с этими данными. Я хочу знать, сколько из этих автомобилей имеют 6 или более цилиндров. Для этого я могу использовать тот факт, что внутри R представляет TRUE как 1, а FALSE как 0.

Если я использую функцию суммирования для нашего вектора истинного/ложного, она суммирует истинные значения и сообщает мне, сколько автомобилей соответствует критериям. Оказывается, у 21 из 32 автомобилей было 6 и более цилиндров. Все, что потребовалось, это некоторое знание векторов, чтобы понять это!

sum(mtcars$cyl >= 6)
[1] 21

2 вектора разной длины

Здесь все становится сложнее, и где R действительно начинает демонстрировать магию и упрощение, которые он делает при работе с векторами. Скалярные операции — это частный случай векторов двух разных длин. По сути, вы говорите R выполнить операцию с вашим вектором и вектором длины 1. Как мы уже говорили, R будет повторно использовать одно значение и выполнять операцию над каждым элементом вектора. Но что, если мы сделаем наш второй вектор немного длиннее?

Этот пример будет немного более абстрактным. Это поможет вам понять магию, которую R использует с векторными операциями. Вернемся к столбцу speed в нашем наборе данных cars. На этот раз давайте разделим на вектор длины 2: c(2, 4). Я включу первые 5 чисел из столбца speed, чтобы вы могли понять, что происходит.

cars$speed[1:5]
[1] 4 4 7 7 8
cars$speed / c(2, 4)
# Note: Only the first 5 numbers of the output are shown here
[1] 2.00 1.00 3.50 1.75 4.00 ...

...Так что именно произошло? Ну, в основном то же самое происходит, когда вы используете скаляр или вектор длины 1. R будет повторно использовать каждый элемент вектора, пока не вычислит операцию для всего столбца speed. Давайте пройдемся по этим операциям:

  1. 1-й элемент speed, 4, делится на 1-й элемент c(2, 4), 2. Это дает нам 2.
  2. 2-й элемент speed, 4, делится на 2-й элемент c(2, 4), 4. Это дает нам 4.
  3. Третий элемент speed, 7, делится на 1-й элемент c(2, 4), 2. Это дает нам 3,5.
  4. И так до конца столбца speed.

Это может быть не самым практичным в реальном мире, но пока длина меньшего вектора кратна длине большего, это будет работать. Но что, если длины не кратны друг другу?

Попробуйте этот код. Столбец speed имеет длину 50. Этот новый вектор имеет длину 3. Поскольку 50 не кратно 3, R делает 2 вещи:

  1. Он пытается завершить операцию. Он следует той же схеме, что и в предыдущем примере.
  2. Он генерирует предупреждающее сообщение, говорящее нам, что более длинный вектор не кратен более короткому.
cars$speed / c(3, 6, 9)
# Note: only the first 5 numbers from the output are shown here
[1] 1.3333333 0.6666667 0.7777778 2.3333333 1.3333333 ...
Warning message:
In cars$speed/c(3, 6, 9) :
  longer object length is not a multiple of shorter object length

Ах, эта волшебная обработка векторов R делает. Он выполняет вычисления, но выдает небольшое предупреждающее сообщение, чтобы мы знали, что что-то не так.

Заключение

Итак, что мы узнали? Векторы быстрые. Они часто позволяют нам писать код быстрее, который работает лучше, чем сопоставимый цикл. Мы можем делать вычисления с векторами и одним числом. Есть функции, которые работают со всем вектором, чтобы получить результат, и мы можем выполнять операции с несколькими векторами, даже если векторы имеют разные размеры. Обработка векторов в R также позволяет нам узнать, когда что-то выглядит не совсем правильно, например, когда наши длины векторов не кратны друг другу.