You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.25 17:37:34