R сделать круг / диаграмму аккордов с циклизацией из данных

Я хотел бы сделать диаграмму аккордов, используя пакет circlize. У меня есть датафрейм, содержащий автомобили с четырьмя столбцами. Во 2 первых столбцах содержится информация о принадлежащем автомобильной группе и модели, а в следующих двух столбцах указывается марка и модель, на которую переместился респондент.

Вот простой пример кадра данных:

   Brand_from model_from Brand_to Model_to
1:      VOLVO        s80      BMW  5series
2:        BMW    3series      BMW  3series
3:      VOLVO        s60    VOLVO      s60
4:      VOLVO        s60    VOLVO      s80
5:        BMW    3series     AUDI       s4
6:       AUDI         a4      BMW  3series
7:       AUDI         a5     AUDI       a5

Было бы здорово иметь возможность сделать это в диаграмме аккордов. Я нашел пример в справке, которая сработала, но я не смог преобразовать мои данные в правильный формат для построения графика. Этот код из справки в пакете circlize. Это производит один слой, я думаю, мне нужно два, бренд и модель.

mat = matrix(1:18, 3, 6)
rownames(mat) = paste0("S", 1:3)
colnames(mat) = paste0("E", 1:6)

rn = rownames(mat)
cn = colnames(mat)
factors = c(rn, cn)
factors = factor(factors, levels = factors)
col_sum = apply(mat, 2, sum)
row_sum = apply(mat, 1, sum)
xlim = cbind(rep(0, length(factors)), c(row_sum, col_sum))

par(mar = c(1, 1, 1, 1))
circos.par(cell.padding = c(0, 0, 0, 0))
circos.initialize(factors = factors, xlim = xlim)
circos.trackPlotRegion(factors = factors, ylim = c(0, 1), bg.border = NA,
                       bg.col = c("red", "green", "blue", rep("grey", 6)), track.height = 0.05,
                       panel.fun = function(x, y) {
                         sector.name = get.cell.meta.data("sector.index")
                         xlim = get.cell.meta.data("xlim")
                         circos.text(mean(xlim), 1.5, sector.name, adj = c(0.5, 0))
})

col = c("#FF000020", "#00FF0020", "#0000FF20")
for(i in seq_len(nrow(mat))) {
  for(j in seq_len(ncol(mat))) {
    circos.link(rn[i], c(sum(mat[i, seq_len(j-1)]), sum(mat[i, seq_len(j)])),
                cn[j], c(sum(mat[seq_len(i-1), j]), sum(mat[seq_len(i), j])),
                col = col[i], border = "white")
  }
}
circos.clear()

Этот код создает следующий график:

Идеальный результат был бы как этот пример, но вместо континентов я бы хотел марки автомобилей, а по кругу - модели автомобилей этой марки.

3 ответа

Решение

Поскольку я немного обновил пакет, теперь есть более простой способ сделать это. Я дам здесь другой ответ, если кто-то заинтересован в этом.

В последних нескольких версиях, chordDiagram() принимает в качестве входных данных и матрицу смежности, и список смежности, что означает, что теперь вы можете предоставить фрейм данных, который содержит парное отношение к функции. Также есть highlight.sector() функция, которая может выделять или отмечать более одного сектора одновременно.

Я реализую сюжет, который я показал ранее, но с более коротким кодом:

df = read.table(textConnection("
 brand_from model_from brand_to model_to
      VOLVO        s80      BMW  5series
        BMW    3series      BMW  3series
      VOLVO        s60    VOLVO      s60
      VOLVO        s60    VOLVO      s80
        BMW    3series     AUDI       s4
       AUDI         a4      BMW  3series
       AUDI         a5     AUDI       a5
"), header = TRUE, stringsAsFactors = FALSE)

brand = c(structure(df$brand_from, names=df$model_from),
          structure(df$brand_to,names= df$model_to))
brand = brand[!duplicated(names(brand))]
brand = brand[order(brand, names(brand))]
brand_color = structure(2:4, names = unique(brand))
model_color = structure(2:8, names = names(brand))

Значение для brand, brand_color а также model_color являются:

> brand
     a4      a5      s4 3series 5series     s60     s80
 "AUDI"  "AUDI"  "AUDI"   "BMW"   "BMW" "VOLVO" "VOLVO"
> brand_color
 AUDI   BMW VOLVO
    2     3     4
> model_color
     a4      a5      s4 3series 5series     s60     s80
      2       3       4       5       6       7       8

На этот раз мы добавим только один дополнительный трек, в котором будут указаны линии и названия брендов. А также вы можете найти входную переменную на самом деле фрейм данных (df[, c(2, 4)]).

library(circlize)
gap.degree = do.call("c", lapply(table(brand), function(i) c(rep(2, i-1), 8)))
circos.par(gap.degree = gap.degree)

chordDiagram(df[, c(2, 4)], order = names(brand), grid.col = model_color,
    directional = 1, annotationTrack = "grid", preAllocateTracks = list(
        list(track.height = 0.02))
)

Как и прежде, названия моделей добавляются вручную:

circos.trackPlotRegion(track.index = 2, panel.fun = function(x, y) {
    xlim = get.cell.meta.data("xlim")
    ylim = get.cell.meta.data("ylim")
    sector.index = get.cell.meta.data("sector.index")
    circos.text(mean(xlim), mean(ylim), sector.index, col = "white", cex = 0.6, facing = "inside", niceFacing = TRUE)
}, bg.border = NA)

В конце мы добавляем линии и названия брендов по highlight.sector() функция. Здесь значение sector.index может быть вектором длиной более 1, и линия (или тонкий прямоугольник) будет охватывать все указанные сектора. Метка будет добавлена ​​в середину секторов, а радикальная позиция контролируется text.vjust вариант.

for(b in unique(brand)) {
  model = names(brand[brand == b])
  highlight.sector(sector.index = model, track.index = 1, col = brand_color[b], 
    text = b, text.vjust = -1, niceFacing = TRUE)
}

circos.clear()

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

df = read.table(textConnection("
 Brand_from model_from Brand_to Model_to
      VOLVO        s80      BMW  5series
        BMW    3series      BMW  3series
      VOLVO        s60    VOLVO      s60
      VOLVO        s60    VOLVO      s80
        BMW    3series     AUDI       s4
       AUDI         a4      BMW  3series
       AUDI         a5     AUDI       a5
"), header = TRUE, stringsAsFactors = FALSE)

from = paste(df[[1]], df[[2]], sep = ",")
to = paste(df[[3]], df[[4]], sep = ",")

mat = matrix(0, nrow = length(unique(from)), ncol = length(unique(to)))
rownames(mat) = unique(from)
colnames(mat) = unique(to)
for(i in seq_along(from)) mat[from[i], to[i]] = 1

Ценность mat является

> mat
            BMW,5series BMW,3series VOLVO,s60 VOLVO,s80 AUDI,s4 AUDI,a5
VOLVO,s80             1           0         0         0       0       0
BMW,3series           0           1         0         0       1       0
VOLVO,s60             0           0         1         1       0       0
AUDI,a4               0           1         0         0       0       0
AUDI,a5               0           0         0         0       0       1

Затем отправьте матрицу chordDiagram с указанием order а также directional, Ручная спецификация order чтобы убедиться, что одни и те же бренды сгруппированы вместе.

par(mar = c(1, 1, 1, 1))
chordDiagram(mat, order = sort(union(from, to)), directional = TRUE)
circos.clear()

Чтобы сделать фигуру более сложной, вы можете создать трек для названий брендов, трек для идентификации брендов, трек для названий моделей. Также мы можем установить разрыв между брендами больше, чем внутри каждого бренда.

1 комплект gap.degree

circos.par(gap.degree = c(2, 2, 8, 2, 8, 2, 8))

2 перед тем как нарисовать диаграмму аккордов, мы создаем две пустые дорожки, одну для торговых марок, одну для идентификационных линий. preAllocateTracks аргумент.

par(mar = c(1, 1, 1, 1))
chordDiagram(mat, order = sort(union(from, to)),
    direction = TRUE, annotationTrack = "grid", preAllocateTracks = list(
        list(track.height = 0.02),
        list(track.height = 0.02))
)

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

circos.trackPlotRegion(track.index = 3, panel.fun = function(x, y) {
    xlim = get.cell.meta.data("xlim")
    ylim = get.cell.meta.data("ylim")
    sector.index = get.cell.meta.data("sector.index")
    model = strsplit(sector.index, ",")[[1]][2]
    circos.text(mean(xlim), mean(ylim), model, col = "white", cex = 0.8, facing = "inside", niceFacing = TRUE)
}, bg.border = NA)

4 добавить линию идентификации бренда. Поскольку бренд охватывает более одного сектора, нам необходимо вручную рассчитать начальный и конечный градусы для линии (дуги). В следующем, rou1 а также rou2 высота двух бордюров во втором треке. Линии идендификации прорисованы во втором треке.

all_sectors = get.all.sector.index()
rou1 = get.cell.meta.data("yplot", sector.index = all_sectors[1], track.index = 2)[1]
rou2 = get.cell.meta.data("yplot", sector.index = all_sectors[1], track.index = 2)[2]

start.degree = get.cell.meta.data("xplot", sector.index = all_sectors[1], track.index = 2)[1]
end.degree = get.cell.meta.data("xplot", sector.index = all_sectors[3], track.index = 2)[2]
draw.sector(start.degree, end.degree, rou1, rou2, clock.wise = TRUE, col = "red", border = NA)

5 сначала получим координату текста в полярной системе координат, затем отобразим в систему координат данных reverse.circlize, Обратите внимание, что ячейка, на которую вы отображаете координаты, и ячейка, которую вы рисуете, должны быть той же самой ячейкой

m = reverse.circlize( (start.degree + end.degree)/2, 1, sector.index = all_sectors[1], track.index = 1)
circos.text(m[1, 1], m[1, 2], "AUDI", cex = 1.2, facing = "inside", adj = c(0.5, 0), niceFacing = TRUE, 
    sector.index = all_sectors[1], track.index = 1)

Для двух других брендов, с одинаковым кодом.

start.degree = get.cell.meta.data("xplot", sector.index = all_sectors[4], track.index = 2)[1]
end.degree   = get.cell.meta.data("xplot", sector.index = all_sectors[5], track.index = 2)[2]
draw.sector(start.degree, end.degree, rou1, rou2, clock.wise = TRUE, col = "green", border = NA)
m = reverse.circlize( (start.degree + end.degree)/2, 1, sector.index = all_sectors[1], track.index = 1)
circos.text(m[1, 1], m[1, 2], "BMW", cex = 1.2, facing = "inside", adj = c(0.5, 0), niceFacing = TRUE, 
    sector.index = all_sectors[1], track.index = 1)

start.degree = get.cell.meta.data("xplot", sector.index = all_sectors[6], track.index = 2)[1]
end.degree  = get.cell.meta.data("xplot", sector.index = all_sectors[7], track.index = 2)[2]
draw.sector(start.degree, end.degree, rou1, rou2, clock.wise = TRUE, col = "blue", border = NA)
m = reverse.circlize( (start.degree + end.degree)/2, 1, sector.index = all_sectors[1], track.index = 1)
circos.text(m[1, 1], m[1, 2], "VOLVO", cex = 1.2, facing = "inside", adj = c(0.5, 0), niceFacing = TRUE, 
    sector.index = all_sectors[1], track.index = 1)

circos.clear()

Если вы хотите установить цвета, перейдите к виньетке пакета, если вы хотите, вы также можете использовать circos.axis добавить оси на сюжет.

Считайте ваши данные, используя read.table, в результате чего получится файл data.frame размером 7x4 (brand.txt должен быть разделен табуляцией).

mt <- read.table("//your-path/brand.txt",header=T,sep="\t",na.string="NA")

Ваши имена переменных (mt): "Brand_from", "model_from", "Brand_to" и "Model_to". Выберите две интересующие вас переменные, например:

mat <- table(mt$Brand_from, mt$model_from)

Это приводит к следующей таблице:

# >mat
#        3series a4 a5 s60 s80
# AUDI        0  1  1   0   0
# BMW         2  0  0   0   0
# VOLVO       0  0  0   2   1

Затем вы можете запускать все то же самое из "rn = rownames(mat)", как вы указали в своем скрипте circlize.

Другие вопросы по тегам