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.