R语言ggplot2绘制Likert图:布局与百分比显示问题求助
解决Likert图的布局与标注问题
我有一个R数据框df,结构如下:
df # A tibble: 90 × 3 # Groups: item [18] item Response Percentage <chr> <fct> <dbl> 1 A Very Dissatisfied 0 2 A Dissatisfied 0 3 A Average 33.3 4 A Satisfied 11.1 5 A Very Satisfied 55.6 6 B Very Dissatisfied 0 7 B Dissatisfied 0 8 B Average 44.4 9 B Satisfied 0 10 B Very Satisfied 55.6 # ℹ 80 more rows # ℹ Use `print(n = ...)` to see more rows
其中Response是包含5个等级的Likert量表字段。我用ggplot2绘制Likert图的代码如下:
# 创建响应与数值的映射 response_mapping <- c("Very Dissatisfied" = 1, "Dissatisfied" = 2, "Average" = 3, "Satisfied" = 4, "Very Satisfied" = 5) # 应用映射并计算符号 data_f_sum <- df %>% ungroup() %>% mutate(res.sgn = sign(response_mapping[as.character(Response)] - 3)) %>% summarise(sum.prcnt = sum(Percentage), .by = c(item, res.sgn)) likert_levels = c("Very Dissatisfied", "Dissatisfied" , "Average" , "Satisfied", "Very Satisfied") df = df%>% mutate(Response = factor(Response , levels = likert_levels)) ggplot(data = df, aes(Percentage, item, fill = Response)) + geom_col(position = position_likert()) + scale_x_continuous(breaks = seq(-1, 1, 0.5), labels = ggstats::label_percent_abs()) + geom_label(data = data_f_sum, aes(label = sprintf("%.1f", sum.prcnt), y = item, x = res.sgn * 0.5), alpha = 0.3, inherit.aes = FALSE) + scale_fill_brewer(type = "div", palette = "RdYlGn") + theme_bw()
现在图表有两个问题需要解决:
- 将
Average(中等)等级固定在图表中间,从0%位置起始; - 在每个
item的Average区域中间显示该等级的百分比,左侧显示Very Dissatisfied和Dissatisfied的百分比总和,右侧显示Satisfied和Very Satisfied的百分比总和。
完整数据结构:
structure(list(item = c("A", "A", "A", "A", "A", "B", "B", "B", "B", "B", "C", "C", "C", "C", "C", "D", "D", "D", "D", "D", "E", "E", "E", "E", "E", "F", "F", "F", "F", "F", "G", "G", "G", "G", "G", "H", "H", "H", "H", "H", "I", "I", "I", "I", "I", "J", "J", "J", "J", "J", "K", "K", "K", "K", "K", "L", "L", "L", "L", "L", "M", "M", "M", "M", "M", "N", "N", "N", "N", "N", "O", "O", "O", "O", "O", "P", "P", "P", "P", "P", "Q", "Q", "Q", "Q", "Q", "R", "R", "R", "R", "R"), Response = structure(c(4L, 2L, 1L, 3L, 5L, 4L, 2L, 1L, 3L, 5L, 4L, 2L, 1L, 3L, 5L, 4L, 2L, 1L, 3L, 5L, 4L, 2L, 1L, 3L, 5L, 4L, 2L, 1L, 3L, 5L, 4L, 2L, 1L, 3L, 5L, 4L, 2L, 1L, 3L, 5L, 4L, 2L, 1L, 3L, 5L, 4L, 2L, 1L, 3L, 5L, 4L, 2L, 1L, 3L, 5L, 4L, 2L, 1L, 3L, 5L, 4L, 2L, 1L, 3L, 5L, 4L, 2L, 1L, 3L, 5L, 4L, 2L, 1L, 3L, 5L, 4L, 2L, 1L, 3L, 5L, 4L, 2L, 1L, 3L, 5L, 4L, 2L, 1L, 3L, 5L), levels = c("Average", "Dissatisfied", "Satisfied", "Very Dissatisfied", "Very Satisfied"), class = "factor"), Percentage = c(0, 0, 33.3, 11.1, 55.6, 0, 0, 44.4, 0, 55.6, 0, 0, 22.2, 33.3, 44.4, 0, 0, 33.3, 11.1, 55.6, 0, 22.2, 11.1, 11.1, 55.6, 0, 0, 44.4, 11.1, 44.4, 0, 0, 11.1, 33.3, 55.6, 0, 0, 33.3, 22.2, 44.4, 0, 0, 11.1, 33.3, 55.6, 0, 0, 22.2, 22.2, 55.6, 0, 0, 11.1, 11.1, 77.8, 0, 0, 11.1, 33.3, 55.6, 0, 0, 33.3, 0, 66.7, 0, 0, 33.3, 11.1, 55.6, 0, 11.1, 0, 33.3, 55.6, 0, 0, 22.2, 22.2, 55.6, 0, 22.2, 22.2, 11.1, 44.4, 0, 11.1, 22.2, 11.1, 55.6)), class = c("grouped_df", "tbl_df", "tbl", "data.frame"), row.names = c(NA, -90L), groups = structure(list( item = c("A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R"), .rows = structure(list( 1:5, 6:10, 11:15, 16:20, 21:25, 26:30, 31:35, 36:40, 41:45, 46:50, 51:55, 56:60, 61:65, 66:70, 71:75, 76:80, 81:85, 86:90), ptype = integer(0), class = c("vctrs_list_of", "vctrs_vctr", "list"))), class = c("tbl_df", "tbl", "data.frame" ), row.names = c(NA, -18L), .drop = TRUE))
解决方案
1. 调整数据格式,实现Average居中
先对数据的百分比做符号调整:负面等级设为负数,正面等级保持正数,Average等级数值不变,这样能确保它从x=0位置开始绘制。
library(dplyr) library(tidyr) library(ggplot2) likert_levels = c("Very Dissatisfied", "Dissatisfied", "Average", "Satisfied", "Very Satisfied") df_processed <- df %>% ungroup() %>% mutate( Response = factor(Response, levels = likert_levels), # 为不同类别设置百分比符号 Percentage_adj = case_when( Response %in% c("Very Dissatisfied", "Dissatisfied") ~ -Percentage, Response == "Average" ~ Percentage, TRUE ~ Percentage ), # 计算每个item的正负最大占比,用于设置x轴范围 total_neg = sum(Percentage[Response %in% c("Very Dissatisfied", "Dissatisfied")], .by = item), total_pos = sum(Percentage[Response %in% c("Satisfied", "Very Satisfied")], .by = item), max_range = pmax(total_neg, total_pos) )
2. 准备标注数据
生成每个item的三个标注内容及对应x轴位置:
label_data <- df_processed %>% group_by(item) %>% summarise( left_sum = sum(Percentage[Response %in% c("Very Dissatisfied", "Dissatisfied")]), avg_val = Percentage[Response == "Average"], right_sum = sum(Percentage[Response %in% c("Satisfied", "Very Satisfied")]), # 计算标注的x轴位置 left_x = -left_sum / 2, avg_x = avg_val / 2, right_x = right_sum / 2, .groups = "drop" ) %>% pivot_longer( cols = c(left_sum, avg_val, right_sum), names_to = "label_type", values_to = "label_value" ) %>% mutate( x_pos = case_when( label_type == "left_sum" ~ left_x, label_type == "avg_val" ~ avg_x, label_type == "right_sum" ~ right_x ) )
3. 最终绘图代码
ggplot(df_processed, aes(x = Percentage_adj, y = item, fill = Response)) + geom_col(position = position_stack(reverse = FALSE)) + scale_x_continuous( limits = function(x) c(-max(df_processed$max_range), max(df_processed$max_range)), breaks = seq(-100, 100, 25), labels = abs, name = "Percentage" ) + geom_text( data = label_data, aes(x = x_pos, y = item, label = sprintf("%.1f", label_value)), inherit.aes = FALSE, size = 3.5 ) + scale_fill_brewer(type = "div", palette = "RdYlGn") + theme_bw() + theme(legend.position = "bottom")
这段代码会实现:
Average类别从x=0位置起始,左侧为负面等级,右侧为正面等级;- 每个item的左侧显示负面等级百分比总和,中间显示Average的百分比,右侧显示正面等级百分比总和。
内容的提问来源于stack exchange,提问作者Homer Jay Simpson
相关产品推荐
相关产品推荐

