Комментарии 23
Великолепная статья! Пожалуй, лучший пример нейтрального сравнения разных подходов.
Сам после 7 лет использования R решил изучить Python (базовый, numpy, pandas, вот это вот всё). Для себя выделил три самых "сложных" отличия (с точки зрения "запоминания" до уровня комфортного использования):
- Изменение объекта напрямую вместо создания копии. В R такое практически не практикуется ('data.table' одно из исключений), а вот в Python встречается часто. Правда, pandas тоже предпочитает создавать копии, но следует быть аккуратным. Больше всего проблем с этим у меня при работе с датафреймами внутри функции:
def foo(df):
df["z"] = 100
return None
data = pd.DataFrame({"a": [1]})
foo(data)
# В `data` добавлен столбец 'z'. В аналогичном коде R `data` не изменяется
data
>>> a z
>>> 0 1 100
- Фильтрация по индексу элемента. Речь не столько о том, что индексы в Python начинаются с 0, а в R с 1. Концептуально сложнее для меня почему-то две другие вещи. Во-первых, трактовка отрицательных индексов:
x[-1]
в R это всё кроме первого элемента, а в Python — один последний элемент. Во-вторых, конструкцияx[i:j]
не включает элемент с индексомj
в Python, а в R включает. - Наличие строковых и столбцовых индексов в DataFrame. Для меня это самое важное и большое концептуальное отличие tidyverse и pandas. В tidyverse настаивается на том, чтобы все данные хранились в ячейках таблицы. В pandas же часто правильно выбранный индекс (обычно строковый) упрощает работу с данными. Примером здесь служит выравнивание по индексу при использовании различных операций на паре датафреймов.
В целом, сейчас уже могу полностью согласиться с КДПВ: в каждом языке свои подходы со своими плюсами и минусами, которые важно знать и грамотно использовать.
Благодарю за комментарий. И отдельное спасибо за то, что напомнили про индексы, сейчас добавлю эту информацию в статью.
В python конструкция x[n] означает доступ к конкретному элементу, поэтому оно не возвращает вместо элемента список в некоторых случаях. И это лучше с точки зрения предсказуемости. Не получишь внезапно список объектов вместо объекта, если явно не просил.
А по конструкции x[i:j] тоже просто — она означает "срез" (slice). В данном случае индексируются не сами элементы, а промежутки между ними. И вы как бы "отрезаете" себе сколько надо элементов по этим, скажем, тонким линиям отрезов между ними. Поэтому и называется "срез". :)
То есть в R вы говорите "мне отрезок, в котором с такого по такой элемент" а в Python "а давай-ка перережем полоску вот тут между элементами и вот тут". И у каждого этого "между" есть свой индекс, который начинается до нулевого элемента и заканчивается за последним. Поэтому индексов срезов всегда на один больше, чем элементов в списке.
В R, если x — список, то x[n] всегда вернет список. Никаких "некоторых случаев" нет. Если надо получить один элемент, то пишем x[[n]] — и всегда будет один элемент, без исключений.
Абстракция с промежутками между элементами на мой взгляд, начинает протекать, когда мы берем элементы с конца, т. е. с отрицательными индексами.
В data.table больше нравится то, что фильтрация с изменением данных позволяет не отбрасывать все остальные данные.
Вот, что я имею ввиду:
dat <- tibble(a = 1:4, b = 1:4)
dat <- dat %>%
filter(a > 1) %>%
mutate(b = b * 2)
dat
# A tibble: 3 x 2
a b
1 2 4
2 3 6
3 4 8
dat <- data.table(a = 1:4, b = 1:4)
dat[a > 1, b := b * 2]
dat
# a b
1: 1 1
2: 2 4
3: 3 6
4: 4 8
Аналогичный результат в 'dplyr' можно получить используя ifelse()
:
dat %>%
mutate(b = ifelse(a > 1, b * 2, b))
#> # A tibble: 4 x 2
#> a b
#> <int> <dbl>
#> 1 1 1
#> 2 2 4
#> 3 3 6
#> 4 4 8
Как по мне, такой подход более явно демонстрирует алгоритм вычисления b
, чем с 'data.table'.
Как-то так:
dat %>%
mutate(b = ifelse(a > 1, b * 2, b),
c = ifelse(a > 1, c * 2, c),
d = ifelse(a > 1, d * 2, d))
по мне наоборот, элегантности в таком мало + фильтрация происходит каждый раз заново.
Тогда уж лучше:
dat[dat$a > 1, ] <- dat %>%
filter(a > 1) %>%
mutate(b = b * 2)
dat
# A tibble: 4 x 2
a b
1 1 1
2 2 4
3 3 6
4 4 8
здесь будет в любом случае не больше двух фильтраций
Да, подход с присваиванием подмножеству строк в данном случае более удобен с точки зрения количества изменений столбцов, но менее гибкий с точки зрения универсальности правила обновления. В частности, он не работает, если правило обновления столбцов зависит от всех строк (хотя и применить его нужно только для подмножества). Например, если вместо 2 нужно взять минимум столбца a
по всему датафрейму. Здесь исходный подход 'data.table' тоже оказывается не рабочим.
Кстати, для создания многих столбцов по одному правилу пока что есть mutate_at()
:
library(tidyverse)
dat <- tibble(a = 1:4, b = 1:4, c = 1:4, d = 1:4)
dat %>%
mutate_at(vars(b, c, d), ~ifelse(a > 1, 2*., .))
#> # A tibble: 4 x 4
#> a b c d
#> <int> <dbl> <dbl> <dbl>
#> 1 1 1 1 1
#> 2 2 4 4 4
#> 3 3 6 6 6
#> 4 4 8 8 8
Преимуществом здесь считается более "выразительный" код, который "читается как обычный текст": "взять dat
, произвести обновление b, c, d по следующему правилу". Но это больше обусловлено привычкой читать код.
И да, каждый раз фильтрация будет заново вычисляться. Это минус, согласен. Его, конечно, можно обойти, но это если сильно важна производительность.
ООП освещен, однако, совсем слабо. А как же R6?
Спасибо.
Про S4 и R6 я отдельную публикацию планирую, просто пока руки не дошли.
Цель этой статьи была упростить миграцию между Python и К в плане синтаксиса, т.е. не столько сравнение языков и их производительности, как миграция между ними.
Так много людей расплываются мыслью по древу — пока уследишь, забудешь о чём вообще речь шла…
Спасибо за комментарий, циклы в R, да и в Python вроде тоже, довольно медленные.
Попробуйте вместо циклов в R использовать что то из семества функций apply()
, sapply()
, lapply()
, vapply()
. Или пакет purrr
.
В pandas
тоже есть аналоги, apply()
, map()
, applymap()
.
И в теле цикла не используйте операции вертикального объединения типа rbind()
и bind_rows()
, лучше результат каждой итерации добавлять в заранее определённый список, и по завершению работы цикла привести его уже с помощью того же bind_rows()
в табличный вид.
Думаю такой подход ускорит ваш код.
В R, что for loop, что *apply функции примерно одинаково быстры (медленны), явный цикл даже получше. Есть нюансы, например, apply на матрицах, которые делать не стоит. В остальном *apply, map, for полностью взаимно заменяемы по скорости. sapply еще делает пару операций под капотом: as.vector(unlist(...)), которые замедляют.
Советую посмотреть пару тредов на stackexchange на эту тему, освежает ум:
stackoverflow.com/questions/5533246/why-is-apply-method-slower-than-a-for-loop-in-r
stackoverflow.com/questions/42393658/lapply-vs-for-loop-performance-r
Закон большого пальца для R: все, что можно векторизовать, надо векторизовать. Циклы по элементам вектора и строкам dataframe нам не нужны, например.
И в теле цикла не используйте операции вертикального объединения типа rbind() и bind_rows()
Вот это правильно. Там под капотом оверхед из-за копирования объекта, что точно давит на память.
Спасибо за комментарий, рад, что статья оказалась для вас полезной.
А какие задачи планируете решать с помощью R, в какой сфере работаете? Академические исследования, биоинформатика, интернет — маркетинг, или ещё что-то другое?
Понял.
Если вы сейчас берётесь за активное изучение R, то есть пара хороших, и при этом бесплатных курсов на stepic.org.
Также могу порекомендовать книгу Хедли Викхема "Язык R в задачах науки о данных". В ней довольно подробна описана инфраструктура tidyverse
. Если позволяет уровень английского, то она есть в бесплатном онлайн доступе на англ языке — https://r4ds.had.co.nz/. Перевод тоже есть, но платный.
Какой язык выбрать для работы с данными — R или Python? Оба! Мигрируем с pandas на tidyverse и data.table и обратно