在 R 中绘制李克特变量的堆积条形图
Plot stacked bar chart of likert variables in R
假设我有一个如下所示的数据框:
P Q1 Q2 ...
1 1 4 1
2 2 3 4
3 1 1 4
其中的列告诉我哪个人相应地回答了问题 q1、q2...中的哪些。这些问题需要 4 点李克特量表的答案(例如,“赞同”表示 1,“略微赞同”表示 2,依此类推)。我如何绘制例如这两个问题都会生成堆积条形图(以 % 为单位)?
它看起来应该有点像 this。
我在网上找到的都是非常复杂的代码,我无法处理或无法理解...难道只有一个简单的函数可以满足我的需求吗?
谢谢!
我相信我不是唯一对你问题的这一部分有异议的人:
All I find online is very complex code I can't handle or fail to understand ... Isn't there just a simple function that does what I want?
“非常复杂的代码”是相当主观的。然而,我能理解学习代码并试图弄清楚如何做你想做的事情(起初看起来很简单)可能会令人生畏和沮丧。我将尝试向您展示如何以非常合乎逻辑和清晰的方式处理此问题,以便您可以理解此处显示的代码实际上并不太复杂。
数据集
OP 没有提供数据集,但我将在这里随机演示一个。这也是展示如何通过代码生成此类数据(并使其可扩展)的好机会。假设我们有 20 个人回答 20 个问题。我将首先只提供一列人员,然后向其添加 20 列问题,从而在数据框架结构中创建数据。问题答案的每个单元格将随机 select 一个从 1 到 5 的答案。
library(dplyr)
library(tidyr)
library(ggplot2)
# make the dataset
set.seed(8675309)
questions <- data.frame(Person = 1:20)
for (i in 1:20) {
questions[[paste0('Q',i)]] <- sample(1:5, 20, replace=TRUE)
}
这为我们提供了一个 20 行 21 列的数据框(1 列用于人员 + 20 列用于问题)。
准备数据
准备生成绘图时,您几乎总是必须以某种方式准备数据。在我们绘制之前,我只想先在这里做两件事。第一步是将我们的数据转换成一种格式,称为 Tidy Data。在我们现在的格式中......可以在 Excel 中绘制,但如果我们想要有一种组织和总结这些数据的高质量方式,我们希望将它组织成“更长”的格式table 格式。我们需要的是以一种将列组织为:
的方式进行组织
Person | Question_num | Answer
您可以通过几种方式做到这一点。在这里,我使用 dplyr
和 tidyr
包以及 gather()
函数,但还存在其他方法(即使用 pivot_longer()
):
questions <- questions %>% gather(key='Question_num', value='Answer', -Person)
我想在这里做的最后一件事是将我们的列 questions$Answer
转换为分类变量,而不是连续数字。为什么?那么,参与者只能回答 1、2、3、4 或 5。回答“3.4”没有意义,因此我们的数据应该是离散的,而不是连续的。我们将通过将 questions$Answer
转换为一个因子来做到这一点。这也允许我们同时做两件在这里非常有用的事情:
- 设置
levels
- 这表示您想要因子水平的顺序。
- 设置
labels
- 这允许您将 1
重新映射为 "Approve"
并将 2
重新映射为 "Slightly Approve"
等等。
然后您可以检查数据,看到 questions$Answer
列现在由我们的 labels()
值组成,而不是数字。
questions$Answer <- factor(questions$Answer,
levels=1:5,
labels=c('Approve','Slightly Approve','Neutral','Slightly Disapprove','Disapprove'))
制作情节
然后我们可以使用 ggplot2
包制作绘图。 GGplot 使用 geoms
将数据绘制到绘图区域。在这种情况下,我们可以使用 geom_bar()
来绘制条形图(将每个项目的 number/count 加起来),并且只需要 x
美学。如果我们将每个条形的 fill
颜色设置为等于 Answer
列,那么它将 color-code 条形与每个问题的每个答案的数量相关联。默认情况下,条形图按照我们之前为 questions$Answer
列的 levels
参数设置的顺序堆叠在一起。
ggplot(questions, aes(x=Question_num)) +
geom_bar(aes(fill=Answer))
这个情节有很多地方是对的,总体布局看起来不错。剩下的就是以几种方式改变外观。我们可以通过扩展我们的情节代码来改变情节的那些方面来做到这一点。即,我想执行以下操作:
- 添加标题并更改一些轴标签
- 将配色方案更改为其中一种 Brewer 标度
- 去掉y轴的空白
- 简化主题并将图例移动到不同的位置
完整的情节代码现在如下所示。您应该能够确定代码的哪些部分正在执行上面提到的每件事。
ggplot(questions, aes(x=Question_num)) +
geom_bar(aes(fill=Answer)) +
scale_fill_brewer(palette='Spectral', direction=-1) +
scale_y_continuous(expand=expansion(0)) +
labs(
title='My Likert Plot', subtitle='Twenty Questions!',
x='Questions', y='Number Answered'
) +
theme_classic() +
theme(legend.position='top')
很酷,嗯?
至于“有没有一个简单的函数可以满足我的需求?”。答案是不”。你可以写一个,但这可能取决于你的数据最初是如何格式化的。如果您需要经常绘制这些图,请设置一个 R 脚本来自动为您执行此操作:)。
编辑:可能是百分比???
OP 在评论中要求通过百分比显示相同的信息。这也很容易做到,而且通常是人们想用李克特图做的事情……所以让我们开始吧!我们将分两个阶段将计数转换为百分比。首先,我们将设置轴和条来执行此操作。其次,我们将在每个栏的顶部叠加文本,以显示每个问题以这种方式回答的百分比。
首先,让我们将条形和 y 轴设置为百分比,而不是计数。我们绘制条形图的线是 geom_bar(aes(fill=Answer))
。 position = "stack"
在该函数中也有一个隐藏的默认值(w不必指定)。 position
参数涉及 ggplot
应该如何处理需要在该特定 x 值处绘制多个柱的情况。在这种情况下,它确定如何处理与每个问题对应的 questions$Answer
每个值对应的 5 个条。
“堆叠”,如您所想,只是将它们堆叠在一起。由于我们有 20 个人回答了每个问题,因此我们所有的条形图对于每个问题的总高度 (20) 都相同。如果只有 19 个人回答问题 #3 会怎么样?好吧,总条形高度会比其他部分短。
一般情况下,李克特图都显示相同高度的条,因为它们是按照占整体的比例堆叠的。在这种情况下,我们希望每堆条形的总和为 1。这意味着 10 个人以一种方式回答应该映射到 0.5 (50%) 的条形高度。
这是其他 position
值发挥作用的地方。我们想用position = "fill"
来引用我们想要在相同的x轴位置需要绘制的条进行堆叠...但不是根据它们的值,而是根据总值的比例为x轴位置。
最后,我们要修复我们的规模。如果我们只使用 position="fill"
,我们的 y 轴刻度将具有“0、0.25、0.50、0.75 和 1.0”或类似值。我们希望它看起来像“0%、25%、50%、75%、100%”。您可以在 scale_y_continuous()
函数中执行此操作并指定 labels
参数。在这种情况下,scales
包有一个方便的 percent_format()
函数就是为了这个目的。将这些放在一起,您将得到以下内容:
ggplot(questions, aes(x=Question_num)) +
geom_bar(aes(fill=Answer), position="fill") +
scale_fill_brewer(palette='Spectral', direction=-1) +
scale_y_continuous(expand=expansion(0), labels=scales::percent_format()) +
labs(
title='My Likert Plot', subtitle='Twenty Questions!',
x='Questions', y='Number Answered'
) +
theme_classic() +
theme(legend.position='top')
让文字置顶
要将文本以百分比形式放在首位,不幸的是,这并不那么简单。为此,我们需要汇总数据,在这种情况下,最简单的方法是事先在单独的数据集中进行汇总,然后使用映射到我们的汇总数据框的文本 geom 来标记文本。
通过指定我们希望如何将数据分组在一起,然后分配 n()
或每个答案的计数作为 freq
列值来创建摘要数据框。
questions_summary <- questions %>%
group_by(Question_num, Answer) %>%
summarize(freq = n()) %>% ungroup()
然后我们使用它来映射到一个新的 geom:geom_text
。 y
值需要再次表示为比例。就像 geom_bar
和上面的原因一样,我们必须使用 "fill"
位置。我还想确保每个条的位置都设置为垂直“中间”,因此我们必须使用 position_fill(vjust=0.5)
而不是仅 "fill"
.
来进一步指定
您会注意到最后一个关键部分是我们正在使用 group
美学。这是非常重要的。对于文本几何,ggplot
需要知道如何对数据进行分组。在条形图的情况下,很“明显”(so-to-speak),因为条形的颜色不同,所以条形的每种颜色都是分隔。对于文本,这总是需要指定(如何拆分值),我们通过 group
美学来做到这一点。
ggplot(questions, aes(x=Question_num)) +
geom_bar(aes(fill=Answer), position="fill") +
geom_text(
data=questions_summary,
aes(y=freq, label=percent(freq/20,1), group=Answer),
position=position_fill(vjust=0.5),
color='gray25', size=3.5
) +
scale_fill_brewer(palette='Spectral', direction=-1) +
scale_y_continuous(expand=expansion(0), labels=scales::percent_format()) +
labs(
title='My Likert Plot', subtitle='Twenty Questions!',
x='Questions', y='Number Answered'
) +
theme_classic() +
theme(legend.position='top')
瞧!
没有代表发表评论,但只想补充给定的答案。要为每个问题的答案数量不同的数据添加百分比标签(在顶部获取文本),请使用以下代码(而不是给定的代码)获取 questions_summary
questions_summary <- questions %>%
group_by(Question_num, Answer) %>%
dplyr::summarize(freq = length(Person)) %>%
ungroup %>% group_by(Question_num) %>%
mutate(proportion = freq / sum(freq))
然后,将geom_text()中的label=percent(freq/20,1)
改为label=percent(proportion)
如下:
ggplot(questions, aes(x=Question_num)) +
geom_bar(aes(fill=Answer), position="fill") +
geom_text(
data=questions_summary,
aes(y=freq, label=percent(proportion), group=Answer),
position=position_fill(vjust=0.5),
color='gray25', size=3.5
) +
scale_fill_brewer(palette='Spectral', direction=-1) +
scale_y_continuous(expand=expansion(0), labels=scales::percent_format()) +
labs(
title='My Likert Plot', subtitle='Twenty Questions!',
x='Questions', y='Number Answered'
) +
theme_classic() +
theme(legend.position='top')
此外,如果您的数据中有 NA,但您不想在图表中显示,只需使用
questions <- na.omit(questions)
在准备数据时将答案转换为因素之前。
假设我有一个如下所示的数据框:
P Q1 Q2 ...
1 1 4 1
2 2 3 4
3 1 1 4
其中的列告诉我哪个人相应地回答了问题 q1、q2...中的哪些。这些问题需要 4 点李克特量表的答案(例如,“赞同”表示 1,“略微赞同”表示 2,依此类推)。我如何绘制例如这两个问题都会生成堆积条形图(以 % 为单位)?
它看起来应该有点像 this。
我在网上找到的都是非常复杂的代码,我无法处理或无法理解...难道只有一个简单的函数可以满足我的需求吗?
谢谢!
我相信我不是唯一对你问题的这一部分有异议的人:
All I find online is very complex code I can't handle or fail to understand ... Isn't there just a simple function that does what I want?
“非常复杂的代码”是相当主观的。然而,我能理解学习代码并试图弄清楚如何做你想做的事情(起初看起来很简单)可能会令人生畏和沮丧。我将尝试向您展示如何以非常合乎逻辑和清晰的方式处理此问题,以便您可以理解此处显示的代码实际上并不太复杂。
数据集
OP 没有提供数据集,但我将在这里随机演示一个。这也是展示如何通过代码生成此类数据(并使其可扩展)的好机会。假设我们有 20 个人回答 20 个问题。我将首先只提供一列人员,然后向其添加 20 列问题,从而在数据框架结构中创建数据。问题答案的每个单元格将随机 select 一个从 1 到 5 的答案。
library(dplyr)
library(tidyr)
library(ggplot2)
# make the dataset
set.seed(8675309)
questions <- data.frame(Person = 1:20)
for (i in 1:20) {
questions[[paste0('Q',i)]] <- sample(1:5, 20, replace=TRUE)
}
这为我们提供了一个 20 行 21 列的数据框(1 列用于人员 + 20 列用于问题)。
准备数据
准备生成绘图时,您几乎总是必须以某种方式准备数据。在我们绘制之前,我只想先在这里做两件事。第一步是将我们的数据转换成一种格式,称为 Tidy Data。在我们现在的格式中......可以在 Excel 中绘制,但如果我们想要有一种组织和总结这些数据的高质量方式,我们希望将它组织成“更长”的格式table 格式。我们需要的是以一种将列组织为:
的方式进行组织Person | Question_num | Answer
您可以通过几种方式做到这一点。在这里,我使用 dplyr
和 tidyr
包以及 gather()
函数,但还存在其他方法(即使用 pivot_longer()
):
questions <- questions %>% gather(key='Question_num', value='Answer', -Person)
我想在这里做的最后一件事是将我们的列 questions$Answer
转换为分类变量,而不是连续数字。为什么?那么,参与者只能回答 1、2、3、4 或 5。回答“3.4”没有意义,因此我们的数据应该是离散的,而不是连续的。我们将通过将 questions$Answer
转换为一个因子来做到这一点。这也允许我们同时做两件在这里非常有用的事情:
- 设置
levels
- 这表示您想要因子水平的顺序。 - 设置
labels
- 这允许您将1
重新映射为"Approve"
并将2
重新映射为"Slightly Approve"
等等。
然后您可以检查数据,看到 questions$Answer
列现在由我们的 labels()
值组成,而不是数字。
questions$Answer <- factor(questions$Answer,
levels=1:5,
labels=c('Approve','Slightly Approve','Neutral','Slightly Disapprove','Disapprove'))
制作情节
然后我们可以使用 ggplot2
包制作绘图。 GGplot 使用 geoms
将数据绘制到绘图区域。在这种情况下,我们可以使用 geom_bar()
来绘制条形图(将每个项目的 number/count 加起来),并且只需要 x
美学。如果我们将每个条形的 fill
颜色设置为等于 Answer
列,那么它将 color-code 条形与每个问题的每个答案的数量相关联。默认情况下,条形图按照我们之前为 questions$Answer
列的 levels
参数设置的顺序堆叠在一起。
ggplot(questions, aes(x=Question_num)) +
geom_bar(aes(fill=Answer))
这个情节有很多地方是对的,总体布局看起来不错。剩下的就是以几种方式改变外观。我们可以通过扩展我们的情节代码来改变情节的那些方面来做到这一点。即,我想执行以下操作:
- 添加标题并更改一些轴标签
- 将配色方案更改为其中一种 Brewer 标度
- 去掉y轴的空白
- 简化主题并将图例移动到不同的位置
完整的情节代码现在如下所示。您应该能够确定代码的哪些部分正在执行上面提到的每件事。
ggplot(questions, aes(x=Question_num)) +
geom_bar(aes(fill=Answer)) +
scale_fill_brewer(palette='Spectral', direction=-1) +
scale_y_continuous(expand=expansion(0)) +
labs(
title='My Likert Plot', subtitle='Twenty Questions!',
x='Questions', y='Number Answered'
) +
theme_classic() +
theme(legend.position='top')
很酷,嗯?
至于“有没有一个简单的函数可以满足我的需求?”。答案是不”。你可以写一个,但这可能取决于你的数据最初是如何格式化的。如果您需要经常绘制这些图,请设置一个 R 脚本来自动为您执行此操作:)。
编辑:可能是百分比???
OP 在评论中要求通过百分比显示相同的信息。这也很容易做到,而且通常是人们想用李克特图做的事情……所以让我们开始吧!我们将分两个阶段将计数转换为百分比。首先,我们将设置轴和条来执行此操作。其次,我们将在每个栏的顶部叠加文本,以显示每个问题以这种方式回答的百分比。
首先,让我们将条形和 y 轴设置为百分比,而不是计数。我们绘制条形图的线是 geom_bar(aes(fill=Answer))
。 position = "stack"
在该函数中也有一个隐藏的默认值(w不必指定)。 position
参数涉及 ggplot
应该如何处理需要在该特定 x 值处绘制多个柱的情况。在这种情况下,它确定如何处理与每个问题对应的 questions$Answer
每个值对应的 5 个条。
“堆叠”,如您所想,只是将它们堆叠在一起。由于我们有 20 个人回答了每个问题,因此我们所有的条形图对于每个问题的总高度 (20) 都相同。如果只有 19 个人回答问题 #3 会怎么样?好吧,总条形高度会比其他部分短。
一般情况下,李克特图都显示相同高度的条,因为它们是按照占整体的比例堆叠的。在这种情况下,我们希望每堆条形的总和为 1。这意味着 10 个人以一种方式回答应该映射到 0.5 (50%) 的条形高度。
这是其他 position
值发挥作用的地方。我们想用position = "fill"
来引用我们想要在相同的x轴位置需要绘制的条进行堆叠...但不是根据它们的值,而是根据总值的比例为x轴位置。
最后,我们要修复我们的规模。如果我们只使用 position="fill"
,我们的 y 轴刻度将具有“0、0.25、0.50、0.75 和 1.0”或类似值。我们希望它看起来像“0%、25%、50%、75%、100%”。您可以在 scale_y_continuous()
函数中执行此操作并指定 labels
参数。在这种情况下,scales
包有一个方便的 percent_format()
函数就是为了这个目的。将这些放在一起,您将得到以下内容:
ggplot(questions, aes(x=Question_num)) +
geom_bar(aes(fill=Answer), position="fill") +
scale_fill_brewer(palette='Spectral', direction=-1) +
scale_y_continuous(expand=expansion(0), labels=scales::percent_format()) +
labs(
title='My Likert Plot', subtitle='Twenty Questions!',
x='Questions', y='Number Answered'
) +
theme_classic() +
theme(legend.position='top')
让文字置顶
要将文本以百分比形式放在首位,不幸的是,这并不那么简单。为此,我们需要汇总数据,在这种情况下,最简单的方法是事先在单独的数据集中进行汇总,然后使用映射到我们的汇总数据框的文本 geom 来标记文本。
通过指定我们希望如何将数据分组在一起,然后分配 n()
或每个答案的计数作为 freq
列值来创建摘要数据框。
questions_summary <- questions %>%
group_by(Question_num, Answer) %>%
summarize(freq = n()) %>% ungroup()
然后我们使用它来映射到一个新的 geom:geom_text
。 y
值需要再次表示为比例。就像 geom_bar
和上面的原因一样,我们必须使用 "fill"
位置。我还想确保每个条的位置都设置为垂直“中间”,因此我们必须使用 position_fill(vjust=0.5)
而不是仅 "fill"
.
您会注意到最后一个关键部分是我们正在使用 group
美学。这是非常重要的。对于文本几何,ggplot
需要知道如何对数据进行分组。在条形图的情况下,很“明显”(so-to-speak),因为条形的颜色不同,所以条形的每种颜色都是分隔。对于文本,这总是需要指定(如何拆分值),我们通过 group
美学来做到这一点。
ggplot(questions, aes(x=Question_num)) +
geom_bar(aes(fill=Answer), position="fill") +
geom_text(
data=questions_summary,
aes(y=freq, label=percent(freq/20,1), group=Answer),
position=position_fill(vjust=0.5),
color='gray25', size=3.5
) +
scale_fill_brewer(palette='Spectral', direction=-1) +
scale_y_continuous(expand=expansion(0), labels=scales::percent_format()) +
labs(
title='My Likert Plot', subtitle='Twenty Questions!',
x='Questions', y='Number Answered'
) +
theme_classic() +
theme(legend.position='top')
瞧!
没有代表发表评论,但只想补充给定的答案。要为每个问题的答案数量不同的数据添加百分比标签(在顶部获取文本),请使用以下代码(而不是给定的代码)获取 questions_summary
questions_summary <- questions %>%
group_by(Question_num, Answer) %>%
dplyr::summarize(freq = length(Person)) %>%
ungroup %>% group_by(Question_num) %>%
mutate(proportion = freq / sum(freq))
然后,将geom_text()中的label=percent(freq/20,1)
改为label=percent(proportion)
如下:
ggplot(questions, aes(x=Question_num)) +
geom_bar(aes(fill=Answer), position="fill") +
geom_text(
data=questions_summary,
aes(y=freq, label=percent(proportion), group=Answer),
position=position_fill(vjust=0.5),
color='gray25', size=3.5
) +
scale_fill_brewer(palette='Spectral', direction=-1) +
scale_y_continuous(expand=expansion(0), labels=scales::percent_format()) +
labs(
title='My Likert Plot', subtitle='Twenty Questions!',
x='Questions', y='Number Answered'
) +
theme_classic() +
theme(legend.position='top')
此外,如果您的数据中有 NA,但您不想在图表中显示,只需使用
questions <- na.omit(questions)
在准备数据时将答案转换为因素之前。