А. Б. Шипунов, Е. М. Балдин, П. А. Волкова, А. И. Коробейников, С. А. Назарова
Скачать 3.04 Mb.
|
> state.x77[1:2, "Area"] Alabama Alaska 50708 566432 является вектором, а не матрицей. Если такое поведение нежела- тельно, то его можно переопределить заданием аргумента drop: > state.x77[1:2, "Area", drop=FALSE] Area Alabama 50708 Alaska 566432 Так как матрицы являются обычными векторами, то их элементы можно индексировать по порядку так же, как если бы измерения отсут- ствовали. Двумерные матрицы при этом индексируются по столбцам; многомерные — так, что первое измерение меняется наиболее часто, а последнее — наиболее редко: > m <- matrix(1:6, nrow = 3) > m [,1] [,2] [1,] 1 4 [2,] 2 5 [3,] 3 6 > m[2:4] [1] 2 3 4 Такая гибкость очень часто приводит к трудноуловимым ошибкам в случае пропуска запятых при индексировании. Объект типа data.frame является обычным списком из векторов, поэтому доступ к нему при помощи оператора [ аналогичен доступу к списку. Функции и аргументы 233 В.2.8. Пустые индексы Отдельные индексирующие векторы могут быть опущены. В таком случае выбирается все измерение и нет необходимости дополнительно узнавать его размер. Наиболее часто это используется при выборке от- дельных строк или столбцов из матрицы: > m <- matrix(1:6, nrow = 3) > m[c(2,3), ] [,1] [,2] [1,] 2 5 [2,] 3 6 > m[, 2] [1] 4 5 6 Пустой индекс можно использовать и с векторами (осторожно!): > v <- 1:10 > v[] <- 0 > v [1] 0 0 0 0 0 0 0 0 0 0 Заменяет все элементы вектора v на 0, но при этом сохраняет все атрибуты (например, имена отдельных элементов). В некоторых случа- ях это может быть более предпочтительно, чем, скажем, конструкция вида > v <- rep(0, length(v)) В.3. Функции и аргументы Функции являются объектами типа function. Единственное их от- личие, скажем, от вектора заключается в наличии в языке дополни- тельного оператора вызова функции (). Создать функцию можно при помощи вызова функции function(): function(arglist) expr Здесь arglist — список аргументов в виде имен или пар имя = зна- чение , а expr — объект типа expression, составляющий тело функ- ции. Выполнение функции происходит до вызова функции return(), аргумент которой становится возвращаемым значением, либо до вы- полнения последнего выражения в теле функции. В последнем случае именно значение последнего выражения будет возвращено из функции. Возвращаемое значение можно замаскировать при помощи функции invisible() 234 Основы программирования в R > norm <- function(x) sqrt(x%*%x) > fib <- function(n) + { + if (n<=2) { + if (n>=0) 1 else 0 + } + else { + return(Recall(n-1) + Recall(n-2)) + } + } Обратите внимание на использование фигурных скобок. Несмотря на то что во многих случаях они могут быть опущены, мы рекоменду- ем их использовать чаще, особенно для функций, циклов и условных конструкций. Наоборот, точка с запятой в конце строки, хотя и допус- кается, но совершенно не обязательна, и лучше ее опускать. Отметим еще использование конструкции Recall() во втором примере. Так как функции суть обычные переменные, то они могут быть переименованы естественным образом: > fibonacci <- fib; rm(fib) > fibonacci(10) Конструкция Recall() позволяет вызвать «текущую функцию» не- зависимо от используемого имени, тем самым сохраняя работоспособ- ность рекурсивных функций даже после переименования. Аргументы функций можно разделить на обязательные и необяза- тельные. Наличие возможности передавать необязательные аргументы (опции) является одной из отличительных черт языка R. Так как функции может быть передан не весь набор аргументов, то, естественно, должен существовать механизм назначения аргумен- тов . Так, аргументы могут передаваться по имени (и поскольку их чис- ло конечно, то стандартные правила сокращения имен до уникального префикса работают и здесь). Есть также способ передачи аргументов по их позиции в операторе вызова функции. Назначение значений аргументам происходит в три этапа: 1. Аргументы, переданные в виде пары имя = значение, имена ко- торых совпадают явно. 2. Аргументы, переданные в виде пары имя = значение, имена ко- торых совпали по уникальному префиксу. 3. Все остальные аргументы по порядку. Функции и аргументы 235 Ключевое слово «...» (ellipsis) позволяет создавать функции с про- извольным числом аргументов. Его наличие несколько усложняет про- цедуру назначения аргументов, так как происходит разделение аргу- ментов на те, что находятся до троеточия, и располагающихся после. Если к первым применяются стандартные правила назначения аргумен- тов, то последние можно назначить исключительно по полному имени, все неназначенные аргументы «съедаются» троеточием и доступны из- нутри функции в виде именованного списка. Как правило, аргументы функции, идущие после троеточия, в справ- ке представлены в виде пары имя = значение. Кроме того, так как ар- гументы назначаются по имени, то при вызове функции они могут на- ходиться на любой позиции, нет необходимости передавать их послед- ними. Рассмотрим простой пример: > f <- function(aa, bb, cc, ab, ..., arg1 = 5, arg2 = 10) { + print(c(aa, bb, cc, ab, arg1, arg2)); print(list(...)) + } > f(arg1 = 7, aa = 1, a = 2, ac = 3, 4, 5, 6) [1] 1 4 5 2 7 10 $ac [1] 3 [[2]] [1] 6 Назначение аргументов здесь происходит так: 1. Назначаются аргументы arg1 и aa, так как их имена совпадают целиком. 2. Значение аргументу aa уже было присвоено на предыдущем ша- ге, поэтому ab может быть назначен по совпадению префикса a, ставшего уникальным. 3. Вследствие совпадения имени для переданного аргумента ac про- исходит добавление аргумента к списку (. . . ). arg2 получает зна- чение по умолчанию. После этого этапа именованных аргументов не остается. 4. Аргументы bb и cc назначаются по порядку значениями 4 и 5 соответственно. Аргумент 6 добавляется к списку list(...). При отсутствии оператора троеточия вызов функции оканчивается ошибкой при попытке назначить аргументы на шагах 3 и 4. 236 Основы программирования в R Функция args(fun) выводит список всех аргументов функции fun. В заключение отметим еще одну важную особенность, отличающую R от многих других языков программирования: аргументы функций вы- числяются «лениво», то есть не в момент вызова функции, а в момент использования (этот факт объясняется просто: язык R является интер- претируемым). В связи с этим надо соблюдать большую осторожность, скажем, при передаче в качестве аргументов результатов вызовов функ- ций со сторонними эффектами: порядок их вызова может отличаться от ожидаемого. С другой стороны, такое поведение позволяет в некоторых случаях существенно упростить задание значений по умолчанию для аргумен- тов: f <- function(X, L = N %/% 2) { N <- length(X) do.something(X, L) } Здесь вычисление аргумента L произойдет где-то внутри функции do.something() . К этому моменту переменной N уже будет назначено значение, и выражение N %/% 2 будет корректно определено. В.4. Циклы и условные операторы Как и многие языки программирования, R предоставляет конструк- ции, позволяющие управлять исполнением программы в зависимости от внешних условий: операторы цикла и условия. Хотя, в отличие от «обычных» языков декларативного программирования (например, C), они используются существенно реже по причинам, которые будут разо- браны ниже. Условное выполнение кода производится при помощи оператора if: if (cond) cons.expr else alt.expr Здесь cond — логический вектор длины 1, cons.expr, alt.expr — выражения, которые будут выполнены в случае, если cond истинно или ложно соответственно. Значение NA для условия не допускается. Если длина условия больше 1, то используется только первый элемент век- тора и выдается предупреждение. Данный факт является источником многочисленных недоразуме- ний: оператор == работает покомпонентно, поэтому в конструкции вида R как СУБД 237 if (v1 == v2) do.something(v1, v2) будет происходить сравнение только первых элементов векторов, но не их содержимого целиком. Ожидаемого поведения можно достичь при помощи функций identical() или all.equal() в зависимости от того, требуется точное или приближенное равенство векторов. Циклы в R реализуются при помощи операторов while и for. Син- таксис первого следующий: while (cond) expr Здесь cond — условие выполнения тела цикла (правила вычисления условия совпадают с таковыми для оператора if), expr — собственно, тело цикла. Оператор for позволяет перечислить все элементы последователь- ности: for (idx in seq) expr Здесь idx — переменная цикла, seq — перечисляемая последователь- ность, а expr — тело цикла. Последовательность seq вычисляется до первого выполнения тела цикла, ее переопределение внутри тела цикла не влияет на число итераций, аналогично назначение какого-либо зна- чения переменной idx не влияет на следующие итерации цикла. Цикл можно прервать оператором break; закончить текущую итерацию и пе- рейти к следующей — оператором next. В.5. R как СУБД Несмотря на то что к R написаны интерфейсы ко многим системам управления базами данных (СУБД) и даже есть специальный пакет sqldf , который позволяет управлять таблицами данных посредством команд SQL, стоит обратить внимание и на базовые особенности R, позволяющие превратить его, практически не расширяя, в организатор связанных (реляционных) текстовых баз данных. Тем, кто знаком с основами языка SQL, могут показаться интерес- ными соответствия между командами и операторами этого языка и ко- мандами R. Некоторые из них приведены в табл. В.1. Соответствия эти не однозначные и не абсолютные, но хорошо вид- но, что операции SQL могут быть без особых проблем выполнены «из- нутри» R. Единственным серьезным недостатком является то, что мно- гие из этих функций выполняются весьма медленно. Грешит этим и очень важная функция merge(), которая позволяет связывать разные 238 Основы программирования в R SELECT [ и subset() JOIN merge() GROUP BY aggregate(), tapply() DISTINCT unique() и duplicated() ORDER BY order(), sort(), rev() WHERE which() , %in%, == LIKE grep() INSERT rbind() EXCEPT ! и - Таблица В.1. Некоторые (примерные) соответствия между операторами и командами SQL и функциями R таблицы на основании общей колонки («ключа» в терминологии баз данных). Вот пример пользовательской функции, которая работает быстрее: > recode <- function(var, from, to) + { + x <- as.vector(var) + x.tmp <- x + for (i in 1:length(from)) {x <- replace(x, x.tmp == from[i], + to[i])} + if(is.factor(var)) factor(x) else x + } Она делает то, чего не умеет делать встроенная функция replace(),— перекодирование всех значений по определенному правилу: > replace(rep(1:10,2), c(2,3,5), c("a","b","c")) [1] "1" "a" "b" "4" "c" "6" "7" "8" "9" "10" "1" "2" "3" "4" "5" [16] "6" "7" "8" "9" "10" > recode(rep(1:10,2), c(2,3,5), c("a","b","c")) [1] "1" "a" "b" "4" "c" "6" "7" "8" "9" "10" "1" "a" "b" "4" "c" [16] "6" "7" "8" "9" "10" R как СУБД 239 Как видите, replace() заменил только первые значения, в то время как recode() заменил их все. Теперь мы можем оперировать несколькими таблицами как одной. Это очень важно для иерархически организованных данных. Напри- мер, мы можем работать в разных регионах и всюду делать похожие операции (скажем, что-то измерять). Тогда удобнее иметь не одну таб- лицу, а две: в первой будут данные по регионам, а во второй — данные измерений объектов. Для связывания таблиц нужно, чтобы в каждой из них была одна и та же колонка, например номер региона. Вот как это можно организовать: > locations <- read.table("data/eq-l.txt", h=T, sep=";") > measurements <- read.table("data/eq-s.txt", h=T, sep=";") > head(locations) N.POP WHERE SPECIES 1 1 Tverskaja arvense 2 2 Tverskaja arvense 3 3 Tverskaja arvense 4 4 Tverskaja arvense 5 5 Tverskaja pratense 6 6 Tverskaja palustre > head(measurements) N.POP DL.R DIA.ST N.REB N.ZUB DL.OSN.Z DL.TR.V DL.BAZ DL.PER 1 1 424 2.3 13 12 2.0 5 3.0 25 2 1 339 2.0 11 12 1.0 4 2.5 13 3 1 321 2.5 15 14 2.0 5 2.3 13 4 1 509 3.0 14 14 1.5 5 2.2 23 5 1 462 2.5 12 13 1.1 4 2.1 12 6 1 350 1.8 9 9 1.1 4 2.0 15 > loc.N.POP <- recode(measurements$N.POP, locations$N.POP, + as.character(locations$SPECIES)) > head(cbind(species=loc.N.POP, measurements)) species N.POP DL.R DIA.ST N.REB N.ZUB DL.OSN.Z DL.TR.V DL.BAZ 1 arvense 1 424 2.3 13 12 2.0 5 3.0 2 arvense 1 339 2.0 11 12 1.0 4 2.5 3 arvense 1 321 2.5 15 14 2.0 5 2.3 4 arvense 1 509 3.0 14 14 1.5 5 2.2 5 arvense 1 462 2.5 12 13 1.1 4 2.1 6 arvense 1 350 1.8 9 9 1.1 4 2.0 240 Основы программирования в R Здесь показано, как работать с двумя связанными таблицами и ко- мандой recode(). В одной таблице записаны местообитания (locations), а в другой — измерения растений (measurements). Названия видов есть только в первой таблице. Если мы хотим узнать, каким видам какие признаки соответствуют (см. главу про многомерные данные), то на- до слить первую и вторую таблицы. Можно использовать для этого merge() , но recode() работает быстрее и эффективнее. Надо только помнить о типе данных, чтобы факторы не превратились в цифры. Ключом в этом случае является колонка N.POP (номер местообитания). В.6. Правила переписывания. Векторизация Встроенные операции языка R векторизованы, то есть выполняют- ся покомпонентно. В таком случае достаточно быстро встает вопрос, каким образом осуществляются операции в случае, если операнды име- ют разную длину (например, при сложении вектора длины 2 и длины 4 ). За это отвечают так называемые правила переписывания (recycling rules): 1. Длина результата совпадает с длиной операнда наибольшей дли- ны. 2. Если длина операнда меньшей длины делит длину второго опе- ранда, то такой операнд повторяется (переписывается) столько раз, сколько нужно до достижения длины второго операнда. Пос- ле этого операция производится покомпонентно над операндами одинаковой длины. 3. Если длина операнда меньшей длины не является делителем дли- ны второго операнда (то есть она не укладывается целое число раз в длину большего операнда), то такой операнд повторяется столько раз, сколько нужно для перекрытия длины второго опе- ранда. Лишние элементы отбрасываются, производится операция и выводится предупреждение. Как следствие этих правил, операции типа сложения числа (то есть вектора единичной длины) с вектором выполняются естественным об- разом: > 2 + c(3, 5, 7, 11) [1] 5 7 9 13 > c(1, 2) + c(3, 5, 7, 11) [1] 4 7 8 13 Правила переписывания. Векторизация 241 > c(1, 2, 3) + c(3, 5, 7, 11) [1] 4 7 10 12 Warning message: In c(1, 2, 3) + c(3, 5, 7, 11) : longer object length is not a multiple of shorter object length Большинство встроенных функций языка R так или иначе векто- ризованы, то есть выдают «естественный» результат при передаче в качестве аргумента вектора. К этому необходимо стремиться при напи- сании собственных функций, так как это, как правило, является клю- чевым фактором, влияющим на скорость выполнения программы. Раз- берем простой пример (написанный, очевидно, человеком, хорошо зна- комым с языком типа C, но малознакомым с R): > p <- 1:20 > lik <- 0 > for (i in 1:length(p)) + { + lik <- lik + log(p[i]) + } Это же самое действие можно реализовать существенно проще и короче (кроме того, обеспечив корректную работу в случае, если вектор p имел бы нулевую длину): > lik <- sum(log(p)) Отметим, что «проще» имеется в виду не только с точки зрения количества строк кода, но и вычислительной сложости: первый образец кода выполняется полностью на интерпретаторе, второй же использует эффективные и быстрые встроенные функции. Второй образец кода работает потому, что функции log() и sum() векторизованы. Функция log() векторизована в обычном смысле: ска- лярная функция применяется поочередно к каждому элементу вектора, таким образом результат log(c(1, 2)) идентичен результату c(log(1), log(2)) Функция sum() векторизована в несколько ином смысле: она берет на вход вектор и считает что-то, зависящее от вектора целиком. В дан- ном случае вызов sum(x) полностью эквивалентен выражению x[1] + x[2] + ... + x[length(x)] Очень часто векторизация кода появляется сама собой за счет нали- чия встроенных функций, правил переписывания для арифметических операций и т. п. Однако (особенно часто это происходит при переписы- 242 Основы программирования в R вании кода с других языков программирования) код следует изменить для того, чтобы он стал векторизованным. Например, код > v <- NULL > v2 <- 1:10 > for (i in 1:length(v2)) + { + if (v2[i] %% 2 == 0) + { + v <- c(v, v2[i]) # 7 строка + } + } плох сразу по двум причинам: он содержит цикл там, где его можно избежать, и, что совсем плохо, он содержит вектор, растущий внутри цикла. Среда R действительно прячет детали выделения и освобожде- ния памяти от пользователя, но это вовсе не значит, что о них не надо знать и их не надо учитывать. В данном случае в седьмой строке про- исходят выделение памяти под новый вектор v и копирование в этот новый вектор элементов из старого. Таким образом, вычислительные затраты этого цикла (в терминах числа копирования элементов) про- порциональны квадрату длины вектора v2! Оптимальное же решение в данном случае является очень простым: > v <- v2[v2 %% 2 == 0] Векторизацию отдельной функции можно произвести при помощи функции Vectorize(), однако не стоит думать, что это решение всех проблем — это исключительно изменение внешнего интерфейса функ- ции, внутри она по-прежнему будет вызываться для каждого элемента по отдельности (хотя в отдельных случаях такого решения оказывается достаточно). Стандартной проблемой при векторизации является оператор if. Один из вариантов замены его векторизации был рассмотрен выше, но очень часто подобного рода преобразования невозможны. Например, |