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

Ког­да‑то это был язык для работы со ста­тис­тикой, но сей­час его впол­не мож­но счи­тать язы­ком обще­го наз­начения (хотя основную свою нап­равлен­ность он сох­ранил). Кто не верит, может заг­лянуть на стра­нич­ку про­екта Shiny, с помощью которо­го любой может соз­давать пол­ноцен­ные веб‑при­ложе­ния на R.

Ну да лад­но, нас язык R инте­ресу­ет имен­но в той области, где он дей­стви­тель­но хорош. В этой статье я рас­ска­жу о самых базовых объ­ектах в язы­ке и его осо­бен­ностях. Кто‑то муд­рый дав­но заметил, что самое хорошее в язы­ке R — это то, что он был соз­дан спе­циалис­тами по ста­тис­тике, а самое пло­хое — то, что он был соз­дан спе­циалис­тами по ста­тис­тике :).

Опус­тив воп­росы уста­нов­ки R, с которы­ми доволь­но лег­ко спра­вить­ся (тем более что про это я нем­ного уже писал в прош­лой статье), прис­тупим к изу­чению язы­ка.

 

Все, что вы хотели знать о функции, но боялись спросить

В язы­ке R дос­тупна очень боль­шая инфраструк­тура пакетов и, как следс­твие, совер­шенно неверо­ятное количес­тво фун­кций для пов­седнев­ного исполь­зования. Что делать, если ты зна­ешь наз­вание, но не пом­нишь пра­виль­ное упот­ребле­ние фун­кции? Решени­ем ста­нет очень при­лич­ный help, пра­виль­ным обра­зом встро­енный в сис­тему. Для того что­бы получить справ­ку об исполь­зовании той или иной фун­кции, дос­таточ­но наб­рать ?имя_функции.

 

Векторы

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

В язы­ке R пол­но сюр­при­зов, и пер­вый из них зак­люча­ется в том, что при­митив­ным объ­ектом в R явля­ется век­тор, то есть совокуп­ность зна­чений одной при­роды. К при­меру, век­тор вещес­твен­ных чисел. Хочет­ся спро­сить, а как быть с обыч­ными чис­лами? Ска­жем, с чис­лом 10. Ответ на этот воп­рос доволь­но прост — это век­тор из одно­го эле­мен­та.

Век­торы быва­ют сле­дующих типов:

  • це­лые;
  • чис­ловые (вещес­твен­ные);
  • сим­воль­ные;
  • ком­плексные;
  • ло­гичес­кие.

По умол­чанию чис­ла в R — это вещес­твен­ные чис­ла, то есть чис­ла с пла­вающей запятой. Для того что­бы явно ука­зать R, что чис­ло, с которым ты собира­ешь­ся иметь дела, целое, нуж­но добавить суф­фикс L. Нап­ример, 10L. Это лег­ко иллюс­три­рует сле­дующий код:

> x <- 1
> typeof(x)
[1] "double"
> y <- 1L
> typeof(y)
[1] "integer"

Здесь будет исполь­зовать­ся сим­вол приг­лашения >, что­бы отли­чать код от отве­та сис­темы. В работе с чис­лами сущес­тву­ет спе­циаль­ный сим­вол Inf для пред­став­ления бес­конеч­ности.

Сле­дует обра­тить вни­мание на опе­ратор прис­ваива­ния <-. Рас­смот­рим сле­дующий фраг­мент:

> x <- 10
> x # печатаем x
[1] 10
> print(x) # еще раз печатаем
[1] 10

В этом, казалось бы, оче­вид­ном фраг­менте кода есть два важ­ных момен­та: печатать мож­но, прос­то ука­зывая имя перемен­ной в стро­ке или исполь­зуя фун­кцию, пред­назна­чен­ную для печати. Это доволь­но харак­терно для всех язы­ков, в которых есть инте­рак­тивный интер­пре­татор REPL (Read — Evaluate — Print Loop). Фун­кция print ско­рее исполь­зует­ся для печати внут­ри дру­гих фун­кций для отладки или прос­то для вывода какой‑либо информа­ции. Что более важ­но и менее оче­вид­но, стро­ка [1] 10, выводи­мая в качес­тве резуль­тата в R, говорит, что это пер­вый (и единс­твен­ный) эле­мент век­тора.

Чис­ло 1 в квад­ратных скоб­ках выводит­ся для удобс­тва чте­ния. К при­меру, если век­тор не вле­зает в ширину экра­на, то он раз­бива­ется на стро­ки и чис­ла перед каж­дой стро­кой — это индекс эле­мен­та век­тора, с которо­го начина­ется дан­ная стро­ка.

Для соз­дания обыч­ного век­тора исполь­зует­ся фун­кция c:

> x <- c(1,2,3)
> x
[1] 1 2 3

Так же как и во мно­гих дру­гих язы­ках, мож­но соз­давать век­торы, ука­зывая интервал зна­чений:

> x <- c(1:3)
> x
[1] 1 2 3

Ка­залось бы, резуль­тат тот же, одна­ко все не сов­сем так. Так как это целочис­ленный интервал, содер­жимое вто­рого век­тора — это целые чис­ла, а пер­вого — вещес­твен­ные. Что лег­ко про­верить с помощью фун­кции typeof. В пос­леднем слу­чае еще мож­но писать прос­то x <- 1:3. Зна­чения булева типа в язы­ке R выг­лядят как TRUE и FALSE или прос­то T и F. Для того что­бы соз­дать пус­той век­тор нуж­ного типа, необ­ходимо исполь­зовать фун­кцию vector.

> x <- vector("numeric", length = 10)
> x
[1] 0 0 0 0 0 0 0 0 0 0
> length(x)
[1] 10

Не­явное пре­обра­зова­ние типов в R хорошо иллюс­три­рует­ся сле­дующим при­мером:

> x <- c("a", TRUE, 1.3)
> x
[1] "a" "TRUE" "1.3"
> y <- c(2, TRUE, FALSE)
> y
[1] 2 1 0

Час­то быва­ет необ­ходимо вос­поль­зовать­ся явным пре­обра­зова­нием типов. Рас­смот­рим при­мер:

> as(TRUE, "character")
[1] "TRUE"
> as.character(TRUE)
[1] "TRUE"

Два строч­ки дела­ют в точ­ности то же самое, одна­ко с точ­ки зре­ния чита­емос­ти кода
вто­рой под­ход выг­лядит более пред­почти­тель­ным. Вооб­ще, фун­кции соз­дания, про­вер­ки и пре­обра­зова­ния типов лег­ко запом­нить сле­дующим обра­зом. Для соз­дания (пус­того век­тора строк) исполь­зует­ся character(length=5), где length — количес­тво эле­мен­тов, is.character исполь­зует­ся для срав­нения, а as.character для пре­обра­зова­ния. Ког­да пре­обра­зова­ние невоз­можно, то его резуль­татом будет спе­циаль­ное зна­чение NA.

Эле­мен­ты век­тора могут быть заиме­нова­ны, это мож­но сде­лать сле­дующим обра­зом:

> v <- c(x = 1.0, y = 2.5, z = -0.1)
> v
x y z
1.0 2.5 -0.1

или так:

> u <- c(1.0, -0.5, -0.5)
> names(u) <- c("x", "y", "z")
 

Матрицы

С век­торами все доволь­но прос­то, давай теперь поп­робу­ем разоб­рать­ся с дру­гой полез­ной струк­турой дан­ных — мат­рицами. Для соз­дания мат­рицы есть спе­циаль­ная фун­кция matrix:

> m <- matrix(nrow = 2, ncol = 3)
> m
[,1] [,2] [,3]
[1,] NA NA NA
[2,] NA NA NA

Как вид­но, изна­чаль­но соз­дает­ся пус­тая мат­рица. Для того что­бы получить раз­меры мат­рицы, сущес­тву­ет спе­циаль­ный атри­бут dim:

> dim(m)
[1] 2 3
> attributes(m)
$dim
[1] 2 3

Сле­дует отме­тить, что в смыс­ле хра­нения дву­мер­ных объ­ектов (мас­сивов, мат­риц) все язы­ки делят­ся на две груп­пы: те, что хра­нят мат­рицы по стро­кам, такие как C и Java, и те, что хра­нят по стол­бцам, — это, к при­меру, FORTRAN и R. В том, что это имен­но так, лег­ко убе­дить­ся сле­дующим обра­зом:

> m <- matrix(1:6, nrow = 2, ncol = 3)
> m
[,1] [,2] [,3]
[1,] 1 3 5
[2,] 2 4 6

При­чем задавать дву­мер­ную струк­туру мож­но, прос­то добав­ляя атри­бут dim к век­тору:

> v <- 1:6
> dim(v) <- c(2, 3)
> v
[,1] [,2] [,3]
[1,] 1 3 5
[2,] 2 4 6

Сверх того, стро­кам и колон­кам мат­рицы мож­но давать име­на:

> m <- matrix(1:4, nrow=2, ncol=2)
> dimnames(m) <- list(c("a", "b"), c("c", "d"))
> m
c d
a 1 3
b 2 4

В язы­ке R сущес­тву­ет так­же механизм соз­дания дву­мер­ных струк­тур из одно­мер­ных с помощью опе­раций при­соеди­нения стро­ки или стол­бца:

> x <- 1:3
> y <- 11:13
> cbind(x, y)
x y
[1,] 1 11
[2,] 2 12
[3,] 3 13
> rbind(x, y)
[,1] [,2] [,3]
x 1 2 3
y 11 12 13

О том, как работать с отдель­ными эле­мен­тами струк­тур дан­ных, я напишу чуть поз­же, а пока про­дол­жим раз­бирать­ся с самими струк­турами.

 

Списки и факторы

В этом раз­деле рас­смот­рим еще две полез­ные струк­туры дан­ных. Пер­вая — это, конеч­но, спи­сок. Дело в том, что час­то при­ходит­ся хра­нить дан­ные раз­ного типа в одном мес­те.

Как мы зна­ем, век­тор здесь не под­ходит, потому что его эле­мен­ты дол­жны быть одно­го типа, поэто­му в R пре­дус­мотре­ны спис­ки:

> lst <- list("hello", 1.5, TRUE, 1+2i)
> lst
[[1]]
[1] "hello"
[[2]]
[1] 1.5
[[3]]
[1] TRUE
[[4]]
[1] 1+2i

Как вид­но, в спис­ке содер­жится четыре эле­мен­та, и все они раз­ного типа: стро­ка, вещес­твен­ное чис­ло, булево зна­чение и ком­плексное чис­ло. Эле­мен­ты спис­ка мож­но име­новать, как и эле­мен­ты век­тора:

> l <- list(a="test", b=3.14)
> l
$a
[1] "test"
$b
[1] 3.14

Во мно­гих язы­ках прог­рамми­рова­ния есть перечис­лимый тип дан­ных. Он нужен для того, что­бы работать с дан­ными, в качес­тве зна­чения которых могут выс­тупать эле­мен­ты конеч­ного мно­жес­тва. Ког­да такого типа в язы­ке нет, этот воп­рос реша­ется с помощью кон­стант для соот­ветс­тву­юще­го набора зна­чений. В язы­ке R для обес­печения подоб­ной фун­кци­ональ­нос­ти есть спе­циаль­ный тип — фак­тор:

> x <- factor(c("yes", "no", "yes", "no", "no"))
> x
[1] yes no yes no no
Levels: no yes

Здесь соз­дает­ся век­тор фак­торов с дву­мя воз­можны­ми зна­чени­ями: yes и no. Мож­но, к при­меру, под­счи­тать, сколь­ко соот­ветс­тву­ющих зна­чений есть в нашем век­торе:

> table(x)
x
no yes
3 2
 

Фрейм данных (Data Frame)

Фрейм дан­ных — один из самых полез­ных типов дан­ных в R. Ког­да мы работа­ем с реаль­ными таб­личны­ми дан­ными, имен­но этот тип пред­став­ляет таб­лицы. В отли­чие от мат­риц, дан­ный тип поз­воля­ет хра­нить раз­личные типы дан­ных в раз­ных колон­ках. С точ­ки зре­ния хра­нения этот тип дан­ных мож­но пред­ста­вить как спи­сок спе­циаль­ного вида, где эле­мен­тами спис­ка явля­ются спис­ки оди­нако­вой дли­ны (колон­ки). Для заг­рузки фрей­ма дан­ных из CSV-фай­ла сущес­тву­ет фун­кция read.csv, которая уже встре­чалась нам в пре­дыду­щей статье этой серии.

Мож­но соз­дать фрейм дан­ных вруч­ную, нап­ример так:

> x <- data.frame(a=c(F, F, T, T), b=c(F, T, F, T), or=c(F, T, T, T))
> x
a b or
1 FALSE FALSE FALSE
2 FALSE TRUE TRUE
3 TRUE FALSE TRUE
4 TRUE TRUE TRUE

По­мимо атри­бута names, для фрей­ма дан­ных так­же есть row.names:

> names(x)
[1] "a" "b" "or"
> row.names(x)
[1] "1" "2" "3" "4"
 

Доступ к элементам

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

> x <- c(11, 21, 31, 41, 11, 21, 31)
> x[1]
[1] 11
> x[2]
[1] 21
> x[x > 21]
[1] 31 41 31
> j <- x > 21
> j
[1] FALSE FALSE TRUE TRUE FALSE FALSE TRUE
> x[j]
[1] 31 41 31
> x[1:3]
[1] 11 21 31

На­вер­ное, единс­твен­ный ком­мента­рий, который тре­бует­ся к дан­ному при­меру, — это то, что опе­рация > работа­ет как век­торная опе­рация и резуль­татом ее выпол­нения будет век­тор булевых зна­чений, этот век­тор может быть исполь­зован для выбор­ки дан­ных из век­тора.

При работе с мат­рицами так­же не воз­ника­ет никаких слож­ностей, нуж­ные стро­ки и стол­бцы мы можем получить лег­ко и неп­ринуж­денно. К при­меру, запись x[2,3] — это эле­мент во вто­рой стро­ке и в треть­ем стол­бце, а x[1,] и z[,2] — это пер­вая стро­ка и вто­рой стол­бец соот­ветс­твен­но. По умол­чанию эти опе­рации воз­вра­щают век­тор, а не мат­рицу, в которой одна стро­ка или стол­бец, и если мы хотим, что­бы резуль­татом выпол­нения дан­ной опе­рации была все‑таки мат­рица, пусть и дру­гого раз­мера, то нуж­но исполь­зовать допол­нитель­ный параметр x[1, ,drop=FALSE].

С дос­тупом к эле­мен­там спис­ка все нем­ного слож­нее. Рас­смот­рим сле­дующий при­мер:

> l <- list(a=0.5, b=1:3)
> l$a
[1] 0.5
> l$b
[1] 1 2 3
> x <- l[2]
> x
$b
[1] 1 2 3
> typeof(x)
[1] "list"
> y <- l[[2]]
> typeof(y)
[1] "integer"
> y
[1] 1 2 3

На этом при­мере дос­таточ­но хорошо вид­ны осо­бен­ности дос­тупа к эле­мен­там в R.

Как мож­но заметить, исполь­зование [[]] не гаран­тиру­ет соот­ветс­твие типа воз­вра­щаемо­го зна­чения изна­чаль­ному, а в слу­чае оди­нар­ных ско­бок [] воз­вра­щаемое зна­чение так­же явля­ется спис­ком.

В этом смыс­ле $ и [[]] работа­ют очень похоже. Хотя есть некото­рая осо­бен­ность — зна­чение в двой­ных квад­ратных скоб­ках может быть вычис­лено, а имя пос­ле зна­ка $ — нет.

Спис­ки быва­ют вло­жен­ными, и дос­туп к их эле­мен­там осу­щест­вля­ется с помощью вло­жен­ных ско­бок, как и полага­ется: x[[1]][[3]], одна­ко мож­но сде­лать запись чуть более понят­ной, исполь­зуя фун­кцию c. К при­меру, пос­леднее выраже­ние мож­но записать как x[[c(1, 3)]]. При­чем исполь­зовать селек­тор с одной скоб­кой не получит­ся (подумай почему).

 

Управляющие структуры

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

Нач­нем с условно­го опе­рато­ра. Надо ска­зать, что здесь в R нет поч­ти ничего осо­бен­ного, но все‑таки:

if (x > 0) { y <- x } else { y <- -x }

Ни­чего нового тут нет, и else может быть опу­щен. Хотя и тут не обош­лось без нес­коль­ко необыч­ного поведе­ния. В фун­кци­ональ­ных язы­ках, таких как Haskell, конс­трук­ция if явля­ется выраже­нием, а не опе­рато­ром, то есть воз­вра­щает зна­чение, а не изме­няет сос­тояние. В язы­ке R эта идея так­же наш­ла себе мес­то в сле­дующей конс­трук­ции:

y <- if (x > 0) { x } else { -x } # фигурные скобки можно опустить

Пос­ледняя конс­трук­ция дела­ет то же самое, что и пре­дыду­щая, толь­ко в фун­кци­ональ­ном сти­ле. Одна­ко в фун­кци­ональ­ных язы­ках выраже­ние дол­жно быть опре­деле­но и поэто­му наличие вет­ви else обя­затель­но. Здесь же это не так, конс­трук­ция z <- if (x < 0) -x впол­не допус­тима, но зна­чени­ем это­го выраже­ния при x > 0 будет спе­циаль­ное зна­чение NULL. Для срав­нения с этим зна­чени­ем мож­но исполь­зовать фун­кцию is.null.

Те­перь сто­ит ска­зать пару слов о цик­лах. Цик­лы в R работа­ют мед­ленно, но ког­да мы име­ем срав­нитель­но неболь­шой объ­ем дан­ных, то исполь­зование цик­лов может быть впол­не допус­тимо и даже удоб­но. С этой точ­ки зре­ния R так­же мало отли­чает­ся от дру­гих язы­ков прог­рамми­рова­ния. Нач­нем с цик­ла for, который реали­зует извес­тную парадиг­му for-in.

x <- c("a", "b", "c", "d", "e")
for(i in 1:5) {
print(x[i])
}
for(ch in x) print(ch)

Как и полага­ется, цикл for-in реали­зует про­цесс ите­рации по век­тору или пос­ледова­тель­нос­ти, пос­ледний вари­ант более лакони­чен, одна­ко если нам каким‑либо обра­зом нуж­ны индексы, то хорошо бы иметь воз­можность соз­давать пос­ледова­тель­ность, соот­ветс­тву­ющую задан­ному век­тору.

Для это­го в R пре­дус­мотре­на спе­циаль­ная фун­кция seq_along, при­нима­ющая в качес­тве аргу­мен­та век­тор или спи­сок, для которых стро­ится пос­ледова­тель­ность индексов. Таким обра­зом, пер­вый цикл мож­но было бы перепи­сать в виде for(i in seq_along(x)) { ... }. Для того что­бы сге­нери­ровать пос­ледова­тель­ность задан­ной дли­ны, мож­но вос­поль­зовать­ся фун­кци­ей seq_len.

Кста­ти, все эти воп­росы лег­ко решить извес­тны­ми средс­тва­ми, исполь­зуя лишь фун­кцию вычис­ления дли­ны length. Цикл while име­ет впол­не клас­сичес­кую фор­му while (cond) { ... }.

Ло­гичес­кие связ­ки в R так­же выг­лядят стан­дар­тным обра­зом: &&, || и !. В допол­нение к баналь­ному while(TRUE) в R при­сутс­тву­ет небаналь­ный repeat { ... }, выход из которо­го обес­печива­ет, как обыч­но, ком­бинация if и break. Для перехо­да к сле­дующей ите­рации пре­дус­мотрен опе­ратор с нес­коль­ко неожи­дан­ным наз­вани­ем next.

Как мож­но заметить, раз­делите­лей вро­де точ­ки с запятой меж­ду опе­рато­рами в R нет.

 

Функции

Как уж без фун­кций в при­лич­ном язы­ке прог­рамми­рова­ния? В самом общем виде опре­деле­ние фун­кции выг­лядит сле­дующим обра­зом:

f <- function(<args>) {
...
}

Как и в фун­кци­ональ­ных язы­ках, фун­кции в R явля­ются объ­екта­ми клас­са. Это озна­чает, что их мож­но передать в качес­тве аргу­мен­та в дру­гую фун­кцию и вер­нуть в качес­тве зна­чения. Ано­ним­ные (лям­бда) фун­кции так­же при­сутс­тву­ют:

f <- function(g) {
function(x) g(g(x))
}
y <- f(function(x) x * x)(5)

Здесь фун­кция f при­нима­ет в качес­тве аргу­мен­та фун­кцию g и воз­вра­щает фун­кцию, которая име­ет один фор­маль­ный параметр x и дваж­ды при­меня­ет к нему фун­кцию g. Так­же в коде мож­но уви­деть переда­чу ано­ним­ной фун­кции в качес­тве аргу­мен­та, а получен­ный резуль­тат (ком­позиция фун­кций g и самой себя) при­меня­ется к чис­лу 5. Таким обра­зом, чис­ло 5 будет дваж­ды воз­ведено в квад­рат.

По­рядок вычис­ления аргу­мен­тов в R явля­ется отло­жен­ным (lazy), то есть аргу­мент не вычис­ляет­ся, если он не нужен:

> f <- function(x, y) x * x
> f(3)
[1] 9
> f(3, 5/0)
[1] 9

Для того что­бы пра­виль­но работать с сос­тоянием в слу­чае замыка­ний, сущес­тву­ет опе­ратор <<-. Рас­смот­рим при­мер:

counter <- function() {
i <- 0
function() {
i <<- i + 1
i
}
}

Те­перь мы можем соз­дать счет­чик или даже два и про­верить, как все работа­ет:

> counter_one <- counter()
> counter_two <- counter()
> counter_one()
[1] 1
> counter_one()
[1] 2
> counter_two()
[1] 1

Как вид­но, все работа­ет штат­но, одна­ко если заменить опе­ратор <<- на обыч­ный опе­ратор прис­ваива­ния, то ничего работать не будет и счет­чик всег­да будет выдавать чис­ло 1.

Язык R дос­таточ­но гибок при работе с фор­маль­ными парамет­рами и аргу­мен­тами. В нем допус­кает­ся исполь­зование зна­чения по умол­чанию и вызов фун­кции с про­изволь­ным поряд­ком аргу­мен­тов (по име­ни):

> f <- function(x, y=1) x + y
> f(y=2, x=5)

Мно­гие фун­кции в R име­ют доволь­но боль­шой спи­сок фор­маль­ных парамет­ров и зна­чений по умол­чанию, и поэто­му переда­вать в них аргу­мен­ты удоб­нее по име­ни.

 

Продолжение следует

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

  • Подпишись на наc в Telegram!

    Только важные новости и лучшие статьи

    Подписаться

  • Подписаться
    Уведомить о
    0 комментариев
    Межтекстовые Отзывы
    Посмотреть все комментарии