Как стать автором
Обновить

Комментарии 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]] — и всегда будет один элемент, без исключений.
Абстракция с промежутками между элементами на мой взгляд, начинает протекать, когда мы берем элементы с конца, т. е. с отрицательными индексами.

Пользуюсь tidyverse (точнее dplyr, так как всё из tidyverse не нужно) и data.table.
В 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'.

Конечно с ifelse получить можно, но вот если колонок модифицируется несколько, то и ifelse начнут плодиться :)
Как-то так:
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 и К в плане синтаксиса, т.е. не столько сравнение языков и их производительности, как миграция между ними.

Я понимаю, что объем про одни только классы R6 будет большой, но сам вердикт я бы сформулировал так, что на R можно писать в стиле ООП на более крутом уровне, чем в S3, хотя функциональность более родна этому языку.

Согласен, подредактирую пункт про ООП в статье.

Офигенская статья, коротко и по делу.
Так много людей расплываются мыслью по древу — пока уследишь, забудешь о чём вообще речь шла…

Спасибо за комментарий.

Спасибо, довольно полезно. Подумываю время от времени мигрировать на Python, но когда я его использую, каждый раз для вроде бы простых операций надо писать свои функции и циклы. Подзадалбывает, но зато на R я довольно часто пишу код с плохой производительностью, потому что тяну какие-то жирные функции, там где можно было обойтись простым циклом.

Спасибо за комментарий, циклы в 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. Статья действительно помогла немного разобраться в основных моментах в аналогии с уже знакомым мне Python. Я сэкономила кучу времени!

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


А какие задачи планируете решать с помощью R, в какой сфере работаете? Академические исследования, биоинформатика, интернет — маркетинг, или ещё что-то другое?

Аналитика данных, разработка проектных решений инструментами DS

Понял.


Если вы сейчас берётесь за активное изучение R, то есть пара хороших, и при этом бесплатных курсов на stepic.org.


Также могу порекомендовать книгу Хедли Викхема "Язык R в задачах науки о данных". В ней довольно подробна описана инфраструктура tidyverse. Если позволяет уровень английского, то она есть в бесплатном онлайн доступе на англ языке — https://r4ds.had.co.nz/. Перевод тоже есть, но платный.

Спасибо! Как раз изучаю все эти источники :)
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации