Подтвердить что ты не робот

Методология структурирования высокоразмерных данных в R и MATLAB

Вопрос

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


Описание

Я люблю функцию R анализа и синтаксис (и ошеломляющие графики) гораздо лучше, чем MATLAB, и упорно работал, чтобы реорганизовать свой материал снова. Тем не менее, я все время зацикливаюсь на том, как организованы данные в моей работе.

MATLAB

Для меня типично работать с многомерными временными рядами, повторяющимися во многих испытаниях, которые хранятся в большом массиве matrix ранг-3 тензор многомерного массива SERIESxSAMPLESxTRIALS. Это время от времени поддается какой-то хорошей линейной алгебре, но неуклюже, когда дело касается другой переменной, а именно CLASS. Обычно метки классов хранятся в другом векторе размера 1x TRIALS.

Когда дело доходит до анализа, я в основном задумываюсь как можно меньше, потому что требуется много работы, чтобы собрать действительно хороший сюжет, который учит вас многому о данных в MATLAB. (Я не единственный, кто так чувствует себя).

R

В R я придерживался как можно ближе к структуре MATLAB, но все становится раздражающе сложным, пытаясь сохранить разметку класса отдельно; Мне пришлось бы продолжать передавать метки в функции, хотя я использую только их атрибуты. Итак, я сделал отдельный массив в списке массивов с помощью CLASS. Это добавляет сложности ко всем моим apply() функциям, но, кажется, стоит того, чтобы поддерживать согласованность (и ошибки).

С другой стороны, R просто не кажется дружелюбным с тензорами/многомерными массивами. Чтобы работать с ними, вам нужно захватить библиотеку abind. Документация на многомерный анализ, как этот пример, кажется, работает в предположении, что у вас есть огромная 2-D таблица данных, например какая-то длинная средневековая прокрутка кадр данных и не упоминает, как получить "там", откуда я.

Как только я добираюсь до построения и классификации обработанных данных, это не такая большая проблема, так как к тому времени я проделал свой путь к структурам, дружественным к фреймворкам, с такими формами, как TRIALSxFEATURES (melt помог много с это). С другой стороны, если я хочу быстро создать матрицу рассеянного экрана или гистограмму решетки для исследовательской фазы (т.е. статистические моменты, разделение, дисперсия между классами, гистограммы и т.д.), Я должен остановиться и выяснить, как Я собираюсь apply() эти огромные многомерные массивы во что-то, что понимают эти библиотеки.

Если я продолжаю стучать в джунглях, придумывая специальные решения для этого, я либо никогда не поправимся, либо у меня появятся мои собственные странные волшебные способы сделать это, которые не делают смысл кому-либо.

Итак, какой правильный способ структурировать многомерные данные с категориальными метками, накопленными в ходе повторных испытаний для исследовательского анализа в R? Пожалуйста, я не хочу возвращаться к MATLAB.

Бонус: Я склонен повторять эти анализы по идентичным структурам данных для нескольких субъектов. Есть ли лучший общий способ, чем обертывание фрагментов кода в циклы for?

4b9b3361

Ответ 1

Может быть dplyr:: tbl_cube?

Работая с отличным ответом @BrodieG, я думаю, что вам может показаться полезным взглянуть на новую функциональность, доступную с dplyr::tbl_cube. Это, по сути, многомерный объект, который вы можете легко создать из списка массивов (как вы сейчас используете), который имеет некоторые действительно хорошие функции для подмножества, фильтрации и подведения итогов, которые (что важно, я думаю,) последовательно используются в "куб" и "табличный" вид данных.

require(dplyr)

Пара предостережений:


Это ранний выпуск: все проблемы, которые идут вместе с этим
Рекомендуется, чтобы эта версия выгружала plyr при загрузке dplyr.

Загрузка массивов в кубы

Здесь пример, использующий arr, как определено в другом ответе:

# using arr from previous example
# we can convert it simply into a tbl_cube
arr.cube<-as.tbl_cube(arr)

arr.cube  
#Source: local array [24 x 3]  
#D: ser [chr, 3]  
#D: smp [chr, 2]  
#D: tr [chr, 4]  
#M: arr [dbl[3,2,4]]

Итак, обратите внимание, что D означает Dimensions и M Measures, и вы можете иметь столько, сколько хотите.

Простое преобразование из многомерного в плоское

Вы можете легко сделать данные табличными, вернув его как data.frame(который вы можете просто преобразовать в таблицу данных, если вам нужны функциональные возможности и преимущества производительности позже)

head(as.data.frame(arr.cube))
#    ser   smp   tr       arr
#1 ser 1 smp 1 tr 1 0.6656456
#2 ser 2 smp 1 tr 1 0.6181301
#3 ser 3 smp 1 tr 1 0.7335676
#4 ser 1 smp 2 tr 1 0.9444435
#5 ser 2 smp 2 tr 1 0.8977054
#6 ser 3 smp 2 tr 1 0.9361929

Подменит

Вы могли бы сгладить все данные для каждой операции, но это имеет много последствий для производительности и полезности. Я думаю, что реальная выгода от этого пакета заключается в том, что вы можете "предварительно разбить" куб для данных, которые вам нужны, прежде чем преобразовать их в табличный формат, который является ggplot-friendly, например. простая фильтрация для возврата только серии 1:

arr.cube.filtered<-filter(arr.cube,ser=="ser 1")
as.data.frame(arr.cube.filtered)
#    ser   smp   tr       arr
#1 ser 1 smp 1 tr 1 0.6656456
#2 ser 1 smp 2 tr 1 0.9444435
#3 ser 1 smp 1 tr 2 0.4331116
#4 ser 1 smp 2 tr 2 0.3916376
#5 ser 1 smp 1 tr 3 0.4669228
#6 ser 1 smp 2 tr 3 0.8942300
#7 ser 1 smp 1 tr 4 0.2054326
#8 ser 1 smp 2 tr 4 0.1006973

tbl_cube в настоящее время работает с dplyr функциями summarise(), select(), group_by() и filter(). Целесообразно вы можете связать их вместе с оператором %.%.

Для остальных примеров я собираюсь использовать встроенный объект nasa tbl_cube, который содержит кучу метеорологических данных (и демонстрирует множество измерений и мер):

Группировка и сводные меры

nasa
#Source: local array [41,472 x 4]
#D: lat [dbl, 24]
#D: long [dbl, 24]
#D: month [int, 12]
#D: year [int, 6]
#M: cloudhigh [dbl[24,24,12,6]]
#M: cloudlow [dbl[24,24,12,6]]
#M: cloudmid [dbl[24,24,12,6]]
#M: ozone [dbl[24,24,12,6]]
#M: pressure [dbl[24,24,12,6]]
#M: surftemp [dbl[24,24,12,6]]
#M: temperature [dbl[24,24,12,6]]

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

plot_data<-as.data.frame(          # as.data.frame so we can see the data
filter(nasa,long<(-70)) %.%        # filter long < (-70) (arbitrary!)
group_by(lat,long) %.%             # group by lat/long combo
summarise(p.max=max(pressure),     # create summary measures for each group
          o.avg=mean(ozone),
          c.all=(cloudhigh+cloudlow+cloudmid)/3)
)

head(plot_data)

#       lat   long p.max    o.avg    c.all
#1 36.20000 -113.8   975 310.7778 22.66667
#2 33.70435 -113.8   975 307.0833 21.33333
#3 31.20870 -113.8   990 300.3056 19.50000
#4 28.71304 -113.8  1000 290.3056 16.00000
#5 26.21739 -113.8  1000 282.4167 14.66667
#6 23.72174 -113.8  1000 275.6111 15.83333

Согласованная нотация для структур данных n-d и 2-d

К сожалению, функция mutate() еще не реализована для tbl_cube, но похоже, что это будет просто вопрос (не много). Вы можете использовать его (и все другие функции, которые работают на кубе) по табулярному результату, хотя и с точно такой же нотацией. Например:

plot_data.mod<-filter(plot_data,lat>25) %.%    # filter out lat <=25
mutate(arb.meas=o.avg/p.max)                   # make a new column

head(plot_data.mod)

#       lat      long p.max    o.avg    c.all  arb.meas
#1 36.20000 -113.8000   975 310.7778 22.66667 0.3187464
#2 33.70435 -113.8000   975 307.0833 21.33333 0.3149573
#3 31.20870 -113.8000   990 300.3056 19.50000 0.3033389
#4 28.71304 -113.8000  1000 290.3056 16.00000 0.2903056
#5 26.21739 -113.8000  1000 282.4167 14.66667 0.2824167
#6 36.20000 -111.2957   930 313.9722 20.66667 0.3376045

Построение графика - в качестве примера функции R, которая "любит" плоские данные

Затем вы можете построить с помощью ggplot(), используя преимущества сглаженных данных:

# plot as you like:
ggplot(plot_data.mod) +
  geom_point(aes(lat,long,size=c.all,color=c.all,shape=cut(p.max,6))) +
  facet_grid( lat ~ long ) +
  theme(axis.text.x = element_text(angle = 90, hjust = 1))

enter image description here

Использование таблицы данных в результирующих плоских данных

Я не собираюсь расширять использование data.table здесь, как это хорошо сделано в предыдущем ответе. Очевидно, есть много веских причин использовать data.table - для любой ситуации здесь вы можете вернуть один путем простого преобразования data.frame:

data.table(as.data.frame(your_cube_name))

Работа динамически с вашим кубом

Еще одна вещь, которая, на мой взгляд, великолепна, - это возможность добавлять меры (срезы/сценарии/сдвиги, все, что вы хотите назвать), к вашему кубу. Я думаю, что это будет хорошо соответствовать методу анализа, описанному в вопросе. Вот простой пример с arr.cube - добавление дополнительной меры, которая сама является (по общему признанию, простой) функцией предыдущей меры. Вы получаете доступ/обновление мер через синтаксис yourcube $mets[$...]

head(as.data.frame(arr.cube))

#    ser   smp   tr       arr
#1 ser 1 smp 1 tr 1 0.6656456
#2 ser 2 smp 1 tr 1 0.6181301
#3 ser 3 smp 1 tr 1 0.7335676
#4 ser 1 smp 2 tr 1 0.9444435
#5 ser 2 smp 2 tr 1 0.8977054
#6 ser 3 smp 2 tr 1 0.9361929

arr.cube$mets$arr.bump<-arr.cube$mets$arr*1.1  #arb modification!

head(as.data.frame(arr.cube))

#    ser   smp   tr       arr  arr.bump
#1 ser 1 smp 1 tr 1 0.6656456 0.7322102
#2 ser 2 smp 1 tr 1 0.6181301 0.6799431
#3 ser 3 smp 1 tr 1 0.7335676 0.8069244
#4 ser 1 smp 2 tr 1 0.9444435 1.0388878
#5 ser 2 smp 2 tr 1 0.8977054 0.9874759
#6 ser 3 smp 2 tr 1 0.9361929 1.0298122

Размеры - или нет...

Я немного поиграл с попыткой динамически добавлять совершенно новые измерения (эффективно увеличивая существующий куб с дополнительными размерами и клонируя или изменяя исходные данные с помощью yourcube $dims[$...]), но обнаружил, что поведение немного непоследовательно, Наверное, лучше всего этого избежать, и сначала сконструируйте свой куб, прежде чем манипулировать им. Будут держать вас в курсе, если я где-нибудь.

Настойчивость

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

tempfilename<-gsub("[ :-]","",paste0("DBX",(Sys.time()),".cub"))
# save:
save(arr.cube,file=tempfilename)
# load:
load(file=tempfilename)

Надеюсь, что это поможет!

Ответ 2

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

Вот пример, чтобы вы начали с data.table и ggplot.

Массив → Таблица данных

Сначала давайте сделаем некоторые данные в вашем формате:

series <- 3
samples <- 2
trials <- 4

trial.labs <- paste("tr", seq(len=trials))
trial.class <- sample(c("A", "B"), trials, rep=T)

arr <- array(
  runif(series * samples * trials), 
  dim=c(series, samples, trials),
  dimnames=list(
    ser=paste("ser", seq(len=series)), 
    smp=paste("smp", seq(len=samples)), 
    tr=trial.labs
  )
)
# , , tr = Trial 1
#        smp
# ser         smp 1     smp 2
#   ser 1 0.9648542 0.4134501
#   ser 2 0.7285704 0.1393077
#   ser 3 0.3142587 0.1012979
#
# ... omitted 2 trials ...
# 
# , , tr = Trial 4
#        smp
# ser         smp 1     smp 2
#   ser 1 0.5867905 0.5160964
#   ser 2 0.2432201 0.7702306
#   ser 3 0.2671743 0.8568685

Теперь у нас есть трехмерный массив. Пусть melt и превратите его в data.table (примечание melt работает на data.frames, которые в основном data.table не имеют колоколов и свистов, поэтому мы должны сначала расплавиться, а затем конвертировать в data.table):

library(reshape2)
library(data.table)

dt.raw <- data.table(melt(arr), key="tr")  # we'll get to what the `key` arg is doing later
#       ser   smp   tr      value
#  1: ser 1 smp 1 tr 1 0.53178276
#  2: ser 2 smp 1 tr 1 0.28574271
#  3: ser 3 smp 1 tr 1 0.62991366
#  4: ser 1 smp 2 tr 1 0.31073376
#  5: ser 2 smp 2 tr 1 0.36098971
# ---                            
# 20: ser 2 smp 1 tr 4 0.38049334
# 21: ser 3 smp 1 tr 4 0.14170226
# 22: ser 1 smp 2 tr 4 0.63719962
# 23: ser 2 smp 2 tr 4 0.07100314
# 24: ser 3 smp 2 tr 4 0.11864134

Обратите внимание на то, насколько это было легко, и все наши метрики измерения просачивались в длинный формат. Один из звонков data.tables - это возможность делать индексированные слияния между data.table (так же, как MySQL индексированные объединения). Итак, мы сделаем это, чтобы привязать class к нашим данным:

dt <- dt.raw[J(trial.labs, class=trial.class)]  # on the fly mapping of trials to class
#          tr   ser   smp     value class
#  1: Trial 1 ser 1 smp 1 0.9648542     A
#  2: Trial 1 ser 2 smp 1 0.7285704     A
#  3: Trial 1 ser 3 smp 1 0.3142587     A
#  4: Trial 1 ser 1 smp 2 0.4134501     A
#  5: Trial 1 ser 2 smp 2 0.1393077     A
# ---                                    
# 20: Trial 4 ser 2 smp 1 0.2432201     A
# 21: Trial 4 ser 3 smp 1 0.2671743     A
# 22: Trial 4 ser 1 smp 2 0.5160964     A
# 23: Trial 4 ser 2 smp 2 0.7702306     A
# 24: Trial 4 ser 3 smp 2 0.8568685     A

Несколько вещей, чтобы понять:

  • J создает data.table из векторов
  • попытка подмножества строк одного data.table с другой таблицей данных (т.е. использование data.table в качестве первого аргумента после привязки в [.data.table) приводит к тому, что data.table остается влево (в языке MySQL) внешний table (dt в этом случае) во внутреннюю таблицу (созданную на лету J) в этом случае. Соединение выполняется в столбце внешнего data.table, который, как вы, возможно, заметили ранее на этапе преобразования melt/data.table.

Вам нужно будет прочитать документацию, чтобы полностью понять, что происходит, но подумайте, что J(trial.labs, class=trial.class) эффективно эквивалентно созданию data.table с data.table(trial.labs, class=trial.class), кроме J работает только при использовании внутри [.data.table.

Итак, теперь, за один простой шаг, у нас есть данные класса, привязанные к значениям. Опять же, если вам нужна матричная алгебра, сначала оперируйте свой массив, а затем в двух или трех простых командах вернитесь к длинному формату. Как отмечалось в комментариях, вы, вероятно, не хотите идти вперед и назад от форматов long to array, если у вас нет действительно веских оснований для этого.

Когда вещи находятся в data.table, вы можете легко группировать/сводить ваши данные (аналогично концепции стиля split-apply-comb). Предположим, мы хотим получить сводную статистику для каждой комбинации class - sample:

dt[, as.list(summary(value)), by=list(class, smp)]

#    class   smp    Min. 1st Qu. Median   Mean 3rd Qu.   Max.
# 1:     A smp 1 0.08324  0.2537 0.3143 0.4708  0.7286 0.9649
# 2:     A smp 2 0.10130  0.1609 0.5161 0.4749  0.6894 0.8569
# 3:     B smp 1 0.14050  0.3089 0.4773 0.5049  0.6872 0.8970
# 4:     B smp 2 0.08294  0.1196 0.1562 0.3818  0.5313 0.9063

Здесь мы просто даем data.table выражение (as.list(summary(value))) для каждого подмножества class, smp данных (как указано в выражении by). Нам нужно as.list, чтобы результаты были собраны data.table в виде столбцов.

Вы могли бы так же легко вычислить моменты (например, list(mean(value), var(value), (value - mean(value))^3) для любой комбинации переменных класса/образца/пробного/серийного номера.

Если вы хотите сделать простые преобразования для данных, это очень легко с помощью data.table:

dt[, value:=value * 10]  # modify in place with `:=`, very efficient
dt[1:2]                  # see, `value` now 10x    
#         tr   ser   smp    value class
# 1: Trial 1 ser 1 smp 1 9.648542     A
# 2: Trial 1 ser 2 smp 1 7.285704     A

Это трансформация на месте, поэтому копий памяти нет, что делает ее быстрой. Обычно data.table пытается максимально эффективно использовать память и как таковой является одним из самых быстрых способов сделать этот тип анализа.

Вывод из длинного формата

ggplot является фантастическим для построения данных в длинном формате. Я не буду вдаваться в подробности того, что происходит, но, надеюсь, изображения дадут вам представление о том, что вы можете сделать

library(ggplot2)
ggplot(data=dt, aes(x=ser, y=smp, color=class, size=value)) + 
  geom_point() +
  facet_wrap( ~ tr)

enter image description here

ggplot(data=dt, aes(x=tr, y=value, fill=class)) + 
  geom_bar(stat="identity") +
  facet_grid(smp ~ ser)

enter image description here

ggplot(data=dt, aes(x=tr, y=paste(ser, smp))) + 
  geom_tile(aes(fill=value)) + 
  geom_point(aes(shape=class), size=5) + 
  scale_fill_gradient2(low="yellow", high="blue", midpoint=median(dt$value))

enter image description here

Таблица данных → Массив → Таблица данных

Сначала нам нужно acast (из пакета reshape2) вернуть нашу таблицу данных в массив:

arr.2 <- acast(dt, ser ~ smp ~ tr, value.var="value")
dimnames(arr.2) <- dimnames(arr)  # unfortunately `acast` doesn't preserve dimnames properly
# , , tr = Trial 1
#        smp
# ser        smp 1    smp 2
#   ser 1 9.648542 4.134501
#   ser 2 7.285704 1.393077
#   ser 3 3.142587 1.012979
# ... omitted 3 trials ...

В этот момент arr.2 выглядит так же, как arr, за исключением значений, умноженных на 10. Обратите внимание, что нам пришлось отбросить столбец class. Теперь допустим некоторую тривиальную матричную алгебру

shuff.mat <- matrix(c(0, 1, 1, 0), nrow=2) # re-order columns
for(i in 1:dim(arr.2)[3]) arr.2[, , i] <- arr.2[, , i] %*% shuff.mat

Теперь вернемся к длинному формату с помощью melt. Обратите внимание на аргумент key:

dt.2 <- data.table(melt(arr.2, value.name="new.value"), key=c("tr", "ser", "smp"))

Наконец, присоединитесь назад dt и dt.2. Здесь вам нужно быть осторожным. Поведение data.table заключается в том, что внутренняя таблица будет соединена с внешней таблицей на основе всех ключей внутренней таблицы, если внешняя таблица не имеет ключей. Если внутренняя таблица имеет ключи, data.table присоединяется к ключу. Это проблема здесь, потому что наша предполагаемая внешняя таблица dt уже имеет ключ только tr от ранее, поэтому наше соединение будет происходить только в этом столбце. Из-за этого нам нужно либо сбросить ключ во внешней таблице, либо reset ключ (мы выбрали последнее здесь):

setkey(dt, tr, ser, smp)
dt[dt.2]
#          tr   ser   smp    value class new.value
#  1: Trial 1 ser 1 smp 1 9.648542     A  4.134501
#  2: Trial 1 ser 1 smp 2 4.134501     A  9.648542
#  3: Trial 1 ser 2 smp 1 7.285704     A  1.393077
#  4: Trial 1 ser 2 smp 2 1.393077     A  7.285704
#  5: Trial 1 ser 3 smp 1 3.142587     A  1.012979
# ---                                             
# 20: Trial 4 ser 1 smp 2 5.160964     A  5.867905
# 21: Trial 4 ser 2 smp 1 2.432201     A  7.702306
# 22: Trial 4 ser 2 smp 2 7.702306     A  2.432201
# 23: Trial 4 ser 3 smp 1 2.671743     A  8.568685
# 24: Trial 4 ser 3 smp 2 8.568685     A  2.671743

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