Методология структурирования высокоразмерных данных в 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
?
Ответы
Ответ 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
.