(495) 925-0049, ITShop интернет-магазин 229-0436, Учебный Центр 925-0049
  Главная страница Карта сайта Контакты
Поиск
Вход
Регистрация
Рассылки сайта
 
 
 
 
 

Статистическое программирование на R: Часть 3. Повторное использование кода и объектное программирование (исходники)

Девид Мертц (David Mertz)

Первые две статьи этой серии (часть 1, часть 2) рассматривали R в применении к "реальному миру". Мы исследовали различные возможности статистического анализа и графического отображения, используя большие наборы данных о температуре, которые собрал один из авторов этих статей. Как было упомянуто в предыдущих статьях, мы фактически обследовали лишь малую часть всей глубины и богатства статистических библиотек R.

В этой статье я хочу отойти от дальнейшего статистического анализа как такового (в основном потому, что я сам не имею необходимых знаний в области статистики, чтобы выбрать наиболее подходящий метод; и мой соавтор Брэд Хантинг, и многие читатели знают намного больше в этой области). В дополнение к богатству статистических понятий, предложенных в первых двух статьях, я ознакомлю читателя с некоторыми тонкостями, лежащими в основе языка программирования R. Предыдущие статьи рассказывали о функционально-ориентированном программировании в R; я подозреваю, что многим читателям будут больше близки процедурные и объектно-ориентированные языки программирования.

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

Назад к основам

Перед рассмотрением объектной модели R давайте изучим и уясним объекты и функции R. Главное, что нужно запомнить об объектах R - это то, что "все - вектор". Даже объекты, которые на первый взгляд отличаются от векторов - матрицы, массивы, структуры данных и остальное - на самом деле векторы с дополнительными (измененными) атрибутами, которые "указывают" R обрабатывать их специальным образом.

Массив (обозначаемый dim) - это один из наиболее важных атрибутов, которые имеют (некоторые) вектора R. Функции matrix(), array() и dim() - просто удобные функции для установки размерности вектора. Система ООП (объектно-ориентированного программирования) R также уходит корнями к атрибутам объектных (или других) классов.

Давайте рассмотрим объявление массива на примере кода в Листинге 1.

Листинг 1. Создание векторов и установка размерности

> v = 1:1000
> typeof(v)
[1] "integer"
> attributes(v)
NULL
> dim(v) = c(10,10,10)  # Установка размерности
> attributes(v)
$dim
[1] 10 10 10
> v2 = matrix(1:1000, nrow=100, ncol=10)
> typeof(v2)
[1] "integer"
> attributes(v2)
$dim
[1] 100  10
> attr(v2,'dim') = c(10,10,10)  # Изменение размерности
> attributes(v2)
$dim
[1] 10 10 10

Кратко говоря, существуют различные синтаксические приемы установки атрибута dim вектору - но фактически все эти синтаксические ухищрения делают одно и то же.

Единственное, что может смутить в положении "все - вектор" - это построчные и постолбцовые операции, которые могут быть не так интуитивно понятны. Например, достаточно легко создать двумерный массив (матрицу) и поработать с одним столбцом или строкой:

Листинг 2. Построчные операции над матрицей

> m = matrix(1:12, nrow=3, ncol=4)
> m
     [,1] [,2] [,3] [,4]
[1,]    1    4    7   10
[2,]    2    5    8   11
[3,]    3    6    9   12
> sum(m)  # суммирование всех элементов матрицы m
[1] 78
> sum(m[1,])  # суммирование по первой строке
[1] 22

Но если бы вы захотели создать вектор сумм по строкам, вам, возможно, захотелось бы сделать что-либо вроде следующего:

Листинг 3. Неправильный способ выполнения построчных операций

> sum(m[c(1,2,3),])  # это НЕ суммирование строк
[1] 78

Здесь можно было бы использовать цикл, но это идет вразрез с функциональными и векторно-ориентированными операциями R. Правильное решение - использовать функцию apply():

Листинг 4. Функция apply() для построчных операций

> apply(m, 1, sum) # по строке
[1] 22 26 30
> apply(m, 2, sum) # по столбцу
[1]  6 15 24 33
> apply(m, c(1,2), sum) # по столбцу и строке (суммирование каждой отдельной ячейки)
     [,1] [,2] [,3] [,4]
[1,]    1    4    7   10
[2,]    2    5    8   11
[3,]    3    6    9   12
# Бесполезно суммировать каждую отдельную ячейку, но можно сделать так:
> a = array(1:24,c(3,4,2))
> apply(a, c(1,2), sum)  # суммирование вглубь в 3-х мерном массиве
     [,1] [,2] [,3] [,4]
[1,]   14   20   26   32
[2,]   16   22   28   34
[3,]   18   24   30   36
> apply(a, c(3), sum)    # суммирование по каждому направлению вглубь
[1]  78 222

Бесконечная последовательность

Иногда, из чисто практических соображений, полезно использовать такую конструкцию, как бесконечная числовая последовательность. Например, мой соавтор по предыдущей статье делал некий анализ с помощью метода интегрирования Монте-Карло, и для его задачи было полезно использовать бесконечно длинную последовательность случайных чисел. Необходимо понять, что бесконечная последовательность - это не просто возможность генерации нового числа в случае необходимости; это также необходимость иметь возможность обращаться к любому предшествующему элементу и получать его прежнее значение.

Очевидно, нет ни компьютерного языка, ни компьютера, способного хранить бесконечную последовательность - все, что они могут хранить, - это "ленивые" ( lazy ) и неограниченные ( unbounded ) последовательности. Новые элементы добавляются к уже реализованному списку тогда и только тогда, когда это необходимо. Например, в Python можно выполнить такое создание спископодобных объектов методом .__getitem__(), который расширяет внутренние списки по необходимости. В Haskell "ленивые" последовательности встроены глубоко внутрь языка - в результате все последовательности являются "ленивыми". В моем руководстве по Haskell (Ресурсы) я использовал пример создания списка всех простых чисел:

Листинг 5. Список простых чисел в Haskell, полученных с помощью решета Эратосфена

primes :: [Int]
primes = sieve [2 .. ]
sieve (x:xs) = x : sieve [y / y <- xs, (y `rem` x)/=0]

В том, что касается бесконечности, R ближе к Python, чем к Haskell. Вам необходимо явно создавать новые элементы в случае необходимости. Подождем до раздела ООП, где мы увидим, как работает скрытый механизм векторного индексирования; здесь еще не так все запутано.

Листинг 6. Объявление вектора и способа его динамического расширения

inf_vector = rnorm(10, 0, 1)   # произвольный старт w/ ,
                               # инициализация 10 элементов
assure <- function(index) {
  extend_by = max(index-length(inf_vector), 0)
  extras = rnorm(extend_by, 0, 1)
  v <- c(inf_vector, extras)
  assign("inf_vector", v, env=.GlobalEnv)
  return(index)
}
getRand <- function(index) {
  assure(index)
  return(inf_vector[index])
}

Вероятно, предпочтительнее получать значение элемента через функцию-контейнер getRand(). Заметим, что вы совершенно свободны в использовании срезов, вычисляемых значений либо отдельных индексов:

Листинг 7. Использование функции-контейнера в качестве посредника для бесконечного виртуального вектора

> getRand(3)                # единичный индекс
[1] 0.5557101
> getRand(1:5)              # диапазон
[1] -0.05472011 -0.30419695  0.55571013  0.91667175 -0.40644081
> getRand(sqrt(c(4,16)))    # расчет индекса коллекции
[1] -0.3041970  0.9166717
> getRand(100)              # принудительное расширение вектора
[1] 0.6577079

Если необходимо, перед использованием элементов можно создать достаточно большой вектор с помощью функции assure():

Листинг 8. Расширение вектора (при необходимости) перед работой с ним

> assure(2000)
[1] 2000
> inf_vector[1500]
[1] 1.267652

Объектно-ориентированное программирование в R

R полностью отвечает требованиям объектно-ориентированного программирования, но чтобы понять это, нужно вспомнить о том, что такое ООП. Пользователи таких языков, как Java и C++ и даже Python, Ruby или Smalltalk, могут иметь несколько ограниченное представление об объектно-ориентированном программировании. Не ошибочное, но ограниченное одной моделью.

Принципы ООП в R в большей степени основываются на обобщенных функциях, чем на иерархиях классов. Эта концепция будет близка читателям, использующим CLOS Lisp или тем, кто читал мои дискуссии по множественной диспетчеризации в Python. К сожалению, подход R - это единичная диспетчеризация; в этом отношении он эквивалентен "традиционным" языкам, упомянутым ранее (C++, Java и другие).

Необходимо заметить, хотя это и не будет обсуждаться подробно в данной статье, что наиболее новая версия R сопровождается пакетом methods, который определяет и использует так называемые "формальные методы". Применение этих формальных методов во многом накладывает некую дисциплину (и ограничения), знакомые по традиционным языкам ООП. В любом случае формальное ООП в R строится поверх "неформального ООП", о котором будет рассказано в этой статье. Насколько я знаю, пакет methods не имеет окончательного статуса, но некая слегка модифицированная версия этого пакета, по-видимому, будет сохранена в будущих версиях R.

Необходимо помнить, что суть концепции ООП на самом деле не в наследовании, а в более общем принципе - решениях по диспетчеризации . Например, вызов obj.method() в традиционных языках ООП будет использовать порядок разрешения методов ( method resolution order - MRO) объекта для поиска "первого" класса-предка obj, который имеет метод .method().

Что такое "первый" - это более тонкий вопрос, чем кажется на первый взгляд. R принимает те же решения, но выворачивает идею наследования наизнанку - вместо набора классов , которые могут объявлять и аннулировать различные методы внутри себя, R порождает семейство обобщенных (generic) функций , которые имеют метки, указывающие тип объекта, которым они хотят оперировать.

Обобщенные функции

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

Листинг 9. Создание обобщенной функции и помеченных методов

#------------- Создание обобщенного  метода
> whoami <- function(x, ...) UseMethod("whoami")
> whoami.foo <- function(x) print("I am a foo")
> whoami.bar <- function(x) print("I am a bar")
> whoami.default <- function(x) print("I don't know who I am")

Ключевая идея состоит в том, что каждый объект в R может принадлежать нулю, одному или большему числу классов. MRO любого заданного объекта (относительно конкретного метода) - это просто вектор именованных классов (если они есть) в атрибуте его class. Например:

Листинг 10. Назначение объектам меток членства в классе

> a = 1:10
> b = 2:20
> whoami(a)                 # Нет соответствующего класса
[1] "I don't know who I am"
> attr(a,'class') <- 'foo'
> attr(b,'class') <- c('baz','bam','bar')
> whoami(a)
[1] "I am a foo"
> whoami(b)                 # поиск MRO для описываемого вектора
[1] "I am a bar"
> attr(a,'class') <- 'bar'  # изменение класса 'a'
> whoami(a)
[1] "I am a bar"

Как и в традиционных наследующих языках, объект не обязан использовать один и тот же класс для всех вызываемых методов. Традиционно, если Child наследуется от Mom и Dad, объект типа Child может использовать .meth1() от Mom и .meth2() от Dad. Все это можно сделать и в R, но Mom и Dad ничего не значат - это просто имена:

Листинг 11. Разрешение методов

> meth1 <- function(x) UseMethod("meth1")
> meth1.Mom <- function(x) print("Mom's meth1")
> meth1.Dad <- function(x) print("Dad's meth1")
> meth2 <- function(x) UseMethod("meth2")
> meth2.Dad <- function(x) print("Dad's meth2")
> attr(a,'class') <- c('Mom','Dad')
> meth1(a)  # несмотря на существование meth1.Dad, Mom используется первым для a
[1] "Mom's meth1"
> meth2(a)
[1] "Dad's meth2"

Включение предков

Необходимость явного указания MRO объекта вместо неявного разрешения через синтаксис наследования может показаться ограничивающей. В действительности можно легко реализовать основанный на наследовании синтаксис MRO, используя минимальное количество функций-контейнеров. MRO, используемый в Листинге 11, вероятно, не лучший из всех возможных, но он демонстрирует идею:

Листинг 12. Реализация основанного на наследовании MRO с использованием минимального количества функций-контейнеров

char0 = character(0)
makeMRO <- function(classes=char0, parents=char0) {
    # Создание MRO из опционального явного списка
    # и опционального списка предков
    mro <- c(classes)
    for (name in parents) {
        mro <- c(mro, name)
        ancestors <- attr(get(name),'class')
        mro <- c(mro, ancestors[ancestors != name])
    }
    return(mro)
}
NewInstance <- function(value=0, classes=char0, parents=char0) {
    # Создание нового объекта, основываясь на первоначальном значении,
    # явных классах и предках (все опционально)
    obj <- value
    attr(obj,'class') <- makeMRO(classes, parents)
    return(obj)
}
MaternalGrandma <- NewInstance()
PaternalGrandma <- NewInstance()
Mom <- NewInstance(classes='Mom', parents='MaternalGrandma')
Dad <- NewInstance(0, classes=c('Dad','Uncle'), 'PaternalGrandma')
Me <- NewInstance(value='Hello World', 'Me', c('Mom','Dad'))

В действии код Листинга 12 выглядит следующим образом:

Листинг 13. Объект с основанным на наследовании MRO

> print(Me)
[1] "Hello World"
attr(,"class")
[1] "Me"              "Mom"             "MaternalGrandma" "Dad"
[5] "Uncle"           "PaternalGrandma"

Если следовать традиционному подходу отношений класс/наследование, надо включать имя создаваемого класса (подобно Mom в аргументе classes). Фактически, учитывая, что каждый класс - это нормальный объект, рассмотренная выше система ближе к прототипному ООП, чем к основанному на классах.

Опять же, вся система обладает достаточной гибкостью для реализации всех вариантов. Можно при желании совершенно свободно отделить объекты классов от объектов экземпляров - можно различать классы по соглашениям именования (например, Mom в отличие от mom), присваивая другие атрибуты (например, type может быть class или instance; функция обработки должна проверять тип) или другими способами.

Снова о бесконечном векторе

Теперь, имея некоторые механизмы ООП, можно намного удобнее работать с бесконечным вектором, который был описан ранее. Наше первое решение вполне работоспособно, но лучше было бы иметь еще более органичный и прозрачный бесконечный вектор.

Операторы в R - это просто сокращенный способ вызова функций; вы можете свободно дифференцировать поведение операторов на основе классов, так же, как и для вызовов других функций. Попутно исправим еще несколько недостатков первой системы:

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

Сделаем все это:

Листинг 14. Объявление индексируемого бесконечного случайного вектора

"[.infinite_random" <- function(v, index) {
    name <- attr(v, 'name')
    rfunc <- attr(v, 'rfunc')
    extend_by = max(index-length(v), 0)
    extras = rfunc(extend_by)
    new <- c(v, extras)
    makeInfiniteRandomVector(name, v=new, rfunc)
    return(new[index])
}
unitnorm <- function(n) return(rnorm(n,0,1))
empty <- vector('numeric', 0)
makeInfiniteRandomVector <- function(name, v=empty, rfunc=unitnorm) {
    # Создание бесконечного вектора
    # можно расширить существующий вектор, настраиваемая функция rand
    attr(v,'class') <- 'infinite_random'
    attr(v,'name') <- name
    attr(v,'rfunc') <- rfunc
    assign(name, v, env=.GlobalEnv)
}
makeInfiniteRandomVector('v')
# makeInfiniteRandomVector('inf_poisson', rfunc=my_poisson)
# Использование: v[1]; v[10]; v[9:12]; и так далее.

Индексирование уже определено в R как обобщенная функция, так что для его настройки не нужно вызывать метод UseMethod(); можно просто определить столько новых специализаций, сколько нужно. Аналогично, встроенная функция print() тоже является обобщенной. Ею можно воспользоваться так:

Листинг 15. Печать бесконечного вектора

print.infinite_random <- function(v) {
    a_few = 5
    len = length(v)
    end_range = (len-a_few)+1
    cat('* Infinite Random Vector *\n')
    cat('[1] ', v[1:a_few], '...\n')
    cat('[')
    cat(end_range)
    cat('] ', v[end_range:len], '...\n')
}

In action, the code produces the following:

Листинг 16. Пример печати бесконечного вектора

> v[1000]
[1] -1.341881
> print(v)
* Бесконечный случайный вектор *
[1]  -0.6247392 1.308057 1.654919 1.691754 -2.251065 ...
[996]  1.027440 0.8376 -0.7066545 -0.7778386 -1.341881 ...

Заключение

Для программирования функций общего назначения, объектов и классов в R нужно сделать шаг назад от подхода традиционных процедурных и объектно-ориентированных языков программирования. Первые две статьи демонстрировали примеры конкретного статистического анализа и не требовали отдельного обдумывания, но однажды, когда вам захочется создать повторно используемый код, придется понять концепцию обобщенных функций и "вывернутый наизнанку" объектно-ориентированный подход, на котором основано их применение (форма ООП "наизнанку" в действительности является более общей).

Вся хитрость такого понимания ООП в том, чтобы мыслить в терминах "какой код вызван?" и "как сделан выбор?". Не привязывайтесь к синтаксису, который применяется в конкретных языках - C++, Objective C, Java, Ruby или Python; сконцентрируйтесь на концепции диспетчеризации.



 Распечатать »
 Правила публикации »
  Написать редактору 
 Рекомендовать » Дата публикации: 23.09.2008 
 

Магазин программного обеспечения   WWW.ITSHOP.RU
TeeChart Pro VCL/FMX with source code single license
IBM Domino Enterprise Server Processor Value Unit (PVU) License + SW Subscription & Support 12 Months
Купить WinRAR : 5 : Академическая лицензия 1 лицензия
VMware Workstation 14 Pro for Linux and Windows, ESD
ABBYY Lingvo x6 Английская Профессиональная версия
 
Другие предложения...
 
Курсы обучения   WWW.ITSHOP.RU
 
Другие предложения...
 
Магазин сертификационных экзаменов   WWW.ITSHOP.RU
 
Другие предложения...
 
3D Принтеры | 3D Печать   WWW.ITSHOP.RU
 
Другие предложения...
 
Новости по теме
 
Рассылки Subscribe.ru
Информационные технологии: CASE, RAD, ERP, OLAP
Программирование на Microsoft Access
CASE-технологии
OS Linux для начинающих. Новости + статьи + обзоры + ссылки
СУБД Oracle "с нуля"
3D и виртуальная реальность. Все о Macromedia Flash MX.
Проект mic-hard - все об XP - новости, статьи, советы
 
Статьи по теме
 
Новинки каталога Download
 
Исходники
 
Документация
 
 



    
rambler's top100 Rambler's Top100