diff --git a/README.md b/README.md
index e8645afd..b419b150 100644
--- a/README.md
+++ b/README.md
@@ -20,8 +20,18 @@
### V2 版本(2024春季学期)得到以下助教同学的大力协助:
+- 陈钟奇,南京师范大学心理学院博士生;
+- 冯雨萌,香港城市大学社会与行为科学系硕士生;
+- 张慧如,陕西师范大学心理学院博士生;
+- 徐方照,南昌大学公共政策管理学院心理学硕士生;
+- 郭泽敏,香港大学教育学院博士生;
+- 朱雪扬,南京师范大学心理学院硕士生;
+- 杨斌杰,温州医科大学本科生;
+- 司远宁,信阳师范大学教育科学学院硕士生;
+- 武婷婷,南京师范大学心理学院硕士生;
+- 温佳慧,南京师范大学心理学院硕士生;
- 亓鹤潼, 南京师范大学文学院硕士生;
-- ...
+- 陈逸群,南京师范大学心理学院硕士。
### V1 版本(2023春季学期)得到以下助教同学的大力协助:
diff --git a/bookdown_files/Books/Book/1008-lesson8_2024.Rmd b/bookdown_files/Books/Book/1008-lesson8_2024.Rmd
index e69de29b..a24b764f 100644
--- a/bookdown_files/Books/Book/1008-lesson8_2024.Rmd
+++ b/bookdown_files/Books/Book/1008-lesson8_2024.Rmd
@@ -0,0 +1,619 @@
+---
+editor_options:
+ markdown:
+ wrap: 72
+---
+# 第八讲:回归模型(一)
+我们之前讨论的内容主要分为两方面。首先是学习R语言的一些基本知识,另一方面则是使用R代码来帮助我们解决描述性统计问题。如果大家还记得心理统计学中的内容,它分为描述统计和推断统计两部分。我们使用数据来进行统计推断,而R语言则可以帮助我们更灵活地实现各种统计方法。
+
+
+```{r, echo=FALSE}
+# Packages
+if (!requireNamespace('pacman', quietly = TRUE)) {
+ install.packages('pacman')
+}
+
+pacman::p_load(
+ # 本节课需要用到的 packages
+ here, tidyverse, bruceR, DT, car)
+```
+
+```{r, echo=FALSE}
+# 改变R在显示大数字和小数字时是选择常规格式还是科学计数法的倾向
+options(scipen=999)
+
+# 还原设置 options(scipen = 0)
+```
+纯粹的R代码学习 → 使用R语言来实现统计知识。当然,这种灵活性既有好处也有坏处。好处是我们有很多选择,坏处则是面对众多选择,我们可能会不知道如何选择。但是,如果我们能够度过刚开始不知道如何选择的阶段,后面就能更好地运用统计知识。另外,在接下来的课程中,我们将重点讨论回归模型,例如回归模型一、回归模型二和回归模型三。
+
+我们之所以关注回归模型,是因为我们希望借此机会将大家在心理学、社会科学研究中常用的统计检验统一到回归框架下。这也是近年来一些研究者推荐的做法。实际上,心理学使用的统计方法与其他学科并没有本质区别,只是大家的偏好问题。在心理学领域中,最常用的方法便是各种回归模型的特例。
+
+
+## 研究问题
+
+我们从研究问题开始,先来回顾一下人类企鹅计划的关键变量。其中一个是恋爱状态,另一个是核心温度,还有一个是社交复杂程度,以及赤道距离。在刚开始进行数据分析时,我们可能没有特别明确的假设,但我们想要了解社会关系,尤其是亲密关系,是否会影响我们的体温,是否能帮助我们调节核心温度。
+
+我们可以使用我们知道的统计方法进行一些探索性分析,比如检验恋爱状态和赤道距离之间是否存在交互作用。大家还记得中介模型吗?其中一个变量是社交复杂程度,另一个是核心温度,中间有一个变量是赤道距离。研究发现,在不同的情侣关系群体中,这三个变量之间的关系是不一样的。
+
+在接下来的课程中,我们将回顾一些关键概念,并进行一些常用的统计检验,包括t检验和方差分析。我们还将介绍为什么t检验和方差分析实际上是线性回归的特例。关于第一个研究问题,我们主要关注恋爱状态是否会影响核心体温。我们会比较两组不同的人,看他们的核心体温是否有差异。
+
+基于恋爱状态,我们可以将数据分为两组:一组是处于恋爱或亲密关系中的人,另一组是没有处于亲密关系的人。我们想要比较这两组之间是否存在差异,通常会采用独立样本t检验。这是我们在学习过程中接触过的统计方法,大家应该都不陌生。
+
+
+```{r, echo = FALSE, fig.width = 3, fig.height = 2}
+knitr::include_graphics('pic/chp8/IJzerman2018fig.png')
+```
+(引自[IJzerman et al., 2018](https://doi.org/10.1525/collabra.165))
+
+## *t*-test作为回归模型的特例
+### 独立样本*t*检验(independent *t*-test)
+当然,我们需要了解一些基础知识,比如独立样本t检验。第一,我们需要满足正态性假设,即两个样本都来自正态总体。我们也知道,如果样本量足够大,即使不严格服从正态分布,使用t检验也是没有问题的。第二,我们还需要满足方差同质性假设,即两个样本的方差应该是类似的。第三,两个样本应该是独立的。
+
+在进行独立样本t检验时,我们的零假设是这两个独立样本在某个变量上的均值没有差异,即μ1等于μ2。而我们的备择假设则是它们的总体均值是有差异的,即μ1不等于μ2。接下来,我们需要计算t值,这是进行t检验时通常需要用到的一个公式。
+
+$$t = \frac{\bar{X}_1 - \bar{X}_2}{\sqrt{\frac{s_1^2}{n_1} + \frac{s_2^2}{n_2}}}$$
+
+在R语言中,如果我们要进行t检验,首先我们需要进行数据预处理。这包括清洗数据,处理缺失值,选择我们感兴趣的变量,以及生成新的变量等。
+
+首先,我们可能需要为数据集生成一些新的变量,比如在原始数据中可能没有被试编号,我们可以随机生成。然后,我们选择自己在分析中关心的问题,使用dplyr函数选择我们感兴趣的变量。
+
+在处理数据时,我们需要注意缺失值。在初步分析时,我们一般选择忽略缺失值,直接将其剔除,以便快速查看结果。此外,还需要将一些变量转换为因子,比如将是否处于亲密关系的变量转换为因子,并赋予两个水平:恋爱和单身。
+
+接下来,我们关注的因变量是被试的体温,我们想知道在两组之间是否有差异。在原始数据中,有两次测量体温的数据,我们可以选择将两次测量的平均值作为新的变量,这可以通过R语言中的行函数实现。具体来说,我们可以创建一个新的变量,这个变量是以temperature字符串开头的列的平均值。
+
+这些步骤都是数据预处理的一部分,是进行t检验之前必要的步骤。
+```{r preprocessing}
+df.penguin <- bruceR::import(here::here('data', 'penguin', 'penguin_rawdata.csv')) %>%
+ dplyr::mutate(subjID = row_number()) %>%
+ dplyr::select(subjID,Temperature_t1, Temperature_t2, socialdiversity,
+ Site, DEQ, romantic, ALEX1:ALEX16) %>% # 选择变量
+ dplyr::filter(!is.na(Temperature_t1) & !is.na(Temperature_t2) & !is.na(DEQ)) %>% # 处理缺失值
+ dplyr::mutate(romantic = factor(romantic, levels = c(1,2), labels = c("恋爱", "单身")), # 转化为因子
+ Temperature = rowMeans(select(., starts_with("Temperature"))), # 计算两次核心温度的均值
+ ALEX4 = case_when(TRUE ~ 6 - ALEX4),
+ ALEX12 = case_when(TRUE ~ 6 - ALEX12),
+ ALEX14 = case_when(TRUE ~ 6 - ALEX14),
+ ALEX16 = case_when(TRUE ~ 6 - ALEX16),
+ ALEX = rowSums(select(., starts_with("ALEX")))) # 反向计分后计算总分
+```
+
+```{r, echo=FALSE}
+DT::datatable(head(df.penguin),
+ fillContainer = TRUE, options = list(pageLength = 4))
+```
+
+要在R语言中进行t检验,我们可以使用自带的stats包中的t.test函数。这是一个非常常用的t检验函数。在使用t.test函数时,我们需要输入一些参数。第一个参数是经过筛选的数据框,第二个参数是我们感兴趣的自变量(如temp),第三个参数是分组变量(如romantic)。此外,我们还可以假定两组的方差是相等的。运行t.test函数后,我们可以得到结果,包括t值、自由度(df)和p值。在这个例子中,t值为0.34664,自由度为1425,而p值较大,表示在恋爱组和非恋爱组之间的体温差异不显著。从结果来看,恋爱状态对体温的影响似乎并不大。
+```{r ttest in stats}
+stats::t.test(data = df.penguin, # 数据框
+ Temperature ~ romantic, # 因变量~自变量
+ var.equal = TRUE) %>%
+ capture.output() # 将输出变整齐
+```
+
+当然,除了t.test函数,还有其他方法可以进行类似的分析。不过,在这个简单的示例中,t.test已经足够满足我们的需求。
+
+我们说t检验是一个特殊的线性回归模型,这是因为在R语言中,它们的编写方式非常相似。在回归模型中,我们将自变量放在前面,因变量放在后面。为了理解这个概念,我们需要简要回顾一下线性回归模型。
+
+线性回归模型是一种统计方法,用于研究一个或多个变量能否预测或解释另一个变量。我们将用来预测的变量称为预测变量或自变量,而被预测的变量称为因变量或响应变量。线性回归的核心是通过拟合一条直线来表示两个变量之间的关系,使得直线与每个数据点之间的距离最小。这样,我们便可以利用这条直线来进行预测。
+
+我们通常会用一个方程或等式来表示线性回归模型,如y = β0 + β1x1 + β2x2 + ... + ε。其中y是我们关心的因变量,x1、x2等是自变量,β0、β1、β2等是回归系数,而ε是误差项。
+
+$$y = \beta_0 + \beta_1 x_1 + \beta_2 x_2 + ... + \beta_p x_p + \epsilon$$
+
+在学习线性回归时,我们通常关注连续变量之间的关系。这意味着x和y都是连续变量。而在t检验中,我们关注的是分组变量,这似乎与线性回归有所不同。然而,在特定情况下,我们可以将t检验视为线性回归的一个特例。当我们分析的自变量是一个二分类变量时,线性回归模型实际上等同于独立样本t检验。所以,虽然它们在某些方面有所不同,但t检验确实可以看作是线性回归的一个特殊情况。
+
+### 线性回归(linear regression)
+在t检验中,我们的因变量,比如体温,是连续的,而自变量,比如是否处于亲密关系,是二分类的。这似乎与我们通常的线性回归模型有所不同,因为在线性回归中,我们通常关注的是连续变量之间的关系。然而,实际上,当我们的自变量是二分类变量时,我们也可以将其视为线性回归模型的一个特例。
+
+想象一下,我们在图上有两堆数据点,一堆是处于亲密关系的人的体温,另一堆是非恋爱状态的人的体温。在这两堆数据点之间,我们可以拟合出一条回归线,这条线可以帮助我们进行预测。当x=1(处于亲密关系)时,我们预测的y值(体温)是多少?当x=2(非恋爱状态)时,我们预测的y值是多少?
+
+在常规的线性回归中,x可以有很多值,y也可以有很多值。然而,在t检验中,x只有两个值,一个是1,一个是2(或者0和1)。我们要做的预测也是,当x=1时,y是多少?当x=2时,y是多少?这就是为什么我们说独立样本t检验是线性回归模型的一个特例,特别是当我们的自变量是二分变量时。
+
+在R语言中,我们可以通过代码来验证这个观点。无论我们是使用t.test函数进行t检验,还是使用lm函数进行线性回归,我们得到的结果应该是一样的。这是因为在这种特殊情况下,这两种方法本质上是在做同样的事情。
+
+
+
+实际上,在进行线性回归时,我们可以采用不同的编码方法来表示分类变量。比如,对于二分类变量,我们可以将其编码为0和1。当我们采用这种编码方式时,线性回归方程为y = β0 + β1x1。其中,x1只有两个可能的取值:0和1。当x1 = 0时,y = β0。当x1 = 1时,y = β0 + β1。这样,我们可以看到,β1实际上表示了两组之间的差异。当然,这只是一种方便我们理解的编码方式。在实际应用中,我们可能会遇到更复杂的编码方法,比如虚拟变量编码、效应编码等。这些编码方法在处理多分类变量时尤为有用。
+
+总之,独立样本t检验实际上可以看作是线性回归模型的一个特例,特别是当我们的自变量是二分变量时。无论我们是使用t.test函数进行t检验,还是使用lm函数进行线性回归,我们得到的结果应该是一样的。这是因为在这种特殊情况下,这两种方法本质上是在做同样的事情。
+
+首先,我们进行预处理数据。这个过程相对简单,大家应该都能理解。接下来,我们来看一下t检验。假设我们不对结果进行整理,而是直接查看原始输出,那么我们会看到以下内容:t值是多少,df值是多少,以及p值是多少。我们之前提到过,t检验实际上是一种特殊的回归。
+
+```{r ttest,results='hide'}
+# t检验
+stats::t.test(
+ data = df.penguin,
+ Temperature ~ romantic,
+ var.equal = TRUE) %>%
+ capture.output() # 将输出变整齐
+```
+
+
+
+
+
+我们继续讨论回归模型。在R语言中,线性回归模型的常用函数是lm。我们可以通过比较t检验和线性回归模型得到的t值、df值和p值来判断它们是否相同。
+
+在这个地方,大家可以看到我们使用的是stats包的lm代码,这个代码代表线性回归模型。我们在这里还是采用同样的数据,然后使用回归模型的公式。在这个公式中,temp是我们的因变量y,而romantics是我们的自变量x。这个x是一个因子,我们需要将其转换为因子。在编写这个公式时,我们只需在因变量前加上一个波浪号,然后加上自变量。接下来,lm函数会自动帮助我们处理因子变量在回归分析中的一些后续处理。最后,它会给我们一个结果。
+
+```{r ttest in lm,results='hide'}
+# 线性回归
+model.inde <- stats::lm(
+ data = df.penguin,
+ formula = Temperature ~ 1 + romantic
+ )
+summary(model.inde)
+```
+
+
+
+
+
+我们可以看到,这里的t值、p值与我们之前的结果基本相同(t值为0.347,p值为0.729)。这是因为我们主要关注的是单身对结果的影响。因此,我们可以得出这样的结论:两组之间的差异在t检验和回归模型中表现得相当一致。在回归模型中,可能还会有一个intercept,但我们通常不会关注它。斜率slope代表的含义是当x从一个单位变化到另一个单位时,我们的因变量y会发生多少变化。而对于我们这种只有二分变量的自变量的情况,就代表两组之间的差异。
+
+因此,我们可以说,二分变量的t检验与回归模型之间存在密切关系。我们关注的是回归系数,它的统计检验值与t检验中的值完全相同。这就是我们希望展示的结果。所以,在心理学中常用的t检验实际上是线性回归的一种特殊情况。
+
+那当然,这里涉及到一个知识点,我们没有详细展开讲,但大家以后感兴趣的话可以去了解。这个知识点就是编码方法。因为我们的数据有两个水平,可能有多种编码方法。例如,我们可以把一个水平编码为0,另一个水平编码为1。那么我们的公式可以表示为:y = β0 + β1 * x1。在这个情况下,x1只有两种取值:0和1。当x=0时,y等于β0;当x=1时,y等于β0 + β1。所以这个时候我们可以看出β1就是两组之间的区别。
+
+当然,这只是一种方便我们理解的方法。另外一种常用的编码方法是将x1编码为-0.5和0.5。在这种情况下,对β的解读就不一样了。大家可以将这种编码带入公式后,自己去观察一下。我们在这里就不展开详细讲解了,因为我们主要想向大家介绍这个概念。大家可以在R语言中多探索一下这个知识点,以便更好地理解和应用。
+
+
+### 单样本*t*检验(one sample *t*-test)
+那么同理,一系列的t检验其实都可以认为是回归模型的特例。例如,我们来看单样本t检验。虽然我们平时很少使用单样本t检验,但在某些情况下,它还是有用的。比如,我们想要判断某个班级的成绩是否属于全校成绩的总体。在这种情况下,我们实际上是在比较这个班级的成绩均值和全校的总体均值。
+
+在单样本t检验中,我们关心的是一个样本的均值是否等于某个特定值。例如,在“在penguin数据中,全体被试的核心体温(Temperature)是否等于36.6?”这个例子中,我们关心的是全体被试的核心温度是否等于正常人的温度。单样本t检验的计算公式可以不用深究,关键是理解其背后的逻辑。
+
+$$t = \frac{\bar{X} - \mu}{s / \sqrt{n}}$$
+
+
+
+
+$$y = \beta_0 + \beta_1 x_1 + \beta_2 x_2 + ... + \beta_p x_p + \epsilon$$
+
+单样本*t*检验中,仅截距不为0。此时公式为:
+
+$$y = \beta_0$$
+$$H_0: \beta_0 = 0$$
+如图所示,这里有一群点,我们想要比较的是这些点的均值(例如用菱形表示)与某条线(特定值)之间是否存在显著性差异,或者说这些点的均值与这条线是否来自同一个总体。在这种情况下,我们可以构建一个只包含截距项β0的回归模型,即y = β0。我们也可以使用回归模型进行统计检验。
+
+
+
+当我们进行单样本t检验时,我们需要稍微调整公式。我们将因变量(temperature)减去我们想要比较的特定值(例如30),然后考虑减去特定值后的因变量均值是否属于一个以0为均值的正态分布。在这个回归模型中,我们加入一个截距项,用于进行t检验。
+```{r,results='hide'}
+stats::t.test(
+ x = df.penguin$Temperature, # 核心体温均值
+ mu = 36.6)
+
+```
+
+
+
+
+当然,在实际应用中,我们可以直接使用单样本t检验,将观测值x与某个特定值进行比较。这样,我们就可以判断样本数据的均值是否与特定值之间存在显著性差异,从而得出结论。总之,无论是单样本t检验还是回归模型,它们都可以帮助我们解决实际问题。在具体分析过程中,我们可以根据需要选择合适的方法。
+
+```{r,results='hide'}
+model.single <- lm(
+ data = df.penguin,
+ formula = Temperature - 36.6 ~ 1
+ )
+summary(model.single)
+```
+
+
+
+
+### 配对样本*t*检验(paired *t*-test)
+
+那么,对于配对样本t检验,我们其实可以把它看作是单样本t检验。因为配对样本t检验中的数据是一一对应的,我们可以通过一个简单的操作,即将两列配对数据相减,得到一列新的数据。然后,我们可以将这列新数据进行单样本t检验。
+
+$$t = \frac{\bar{X} - \mu}{s / \sqrt{n}}$$
+这里的前提条件我们就不详细讲了。简单来说,对于配对样本t检验,我们可以先将两个值相减,得到他们的差值。然后,我们检验这个差值是否显著地不同于零。如果差值的分布明显不同于零,那么我们就可以认为两者之间存在显著的差异。
+$$y_1 - y_2 = \beta_0 $$
+$$H_0: \beta_0 = 0 $$
+我们之所以能这样做,是因为在配对样本t检验中,每个被试都有两个数据点,这两个数据点是一一对应的。但是,对于独立样本t检验,由于数据点之间没有形成配对关系,我们不能直接相减。
+
+
+
+同样,对于配对样本t检验,我们也可以使用回归方法进行检验。方法类似于独立样本t检验,我们只需将两个值相减,然后检验这个差值的均值是否等于零。在R语言中,我们可以使用t.test函数来进行配对样本t检验。我们只需将两列数据分别作为x和y输入,然后设置参数paired为TRUE,表示这两列数据是配对的,就可以进行配对样本t检验了。
+
+```{r,results='hide'}
+stats::t.test(
+ x = df.penguin$Temperature_t1,
+ y = df.penguin$Temperature_t2,
+ paired = TRUE
+ )
+```
+
+
+
+在前面我们讲了关于亲密关系和非亲密关系的两组人在体温上是否有区别的问题。我们可以使用配对样本t检验来进行检验,而配对样本t检验实际上是一个特殊的回归模型。同样地,对于所有其他类型的t检验,它们也都可以看作是特定的回归模型。因此,t检验与线性模型的本质是相同的。
+
+```{r,results='hide'}
+model.paired <- lm(
+ Temperature_t1 - Temperature_t2 ~ 1,
+ data = df.penguin
+ )
+summary(model.paired)
+```
+
+
+
+
+
+
+### bruceR::TTEST
+这里,我们可以介绍一下bruceR包中的t检验功能,它涵盖了我们前面提到的各种t检验,并且输出结果非常方便。如果大家下载了我们的课件,可以看到关于bruceR包中t检验各种参数的介绍。bruceR包基本上涵盖了所有类型的t检验,包括独立样本t检验、配对样本t检验和单样本t检验。它的输出结果通常是一个三线表,方便大家查看和理解。
+
+
+
+
+```{r,results='hide'}
+stats::t.test(
+ data = df.penguin,
+ Temperature ~ romantic,
+ var.equal = TRUE
+ )
+```
+
+
+
+```{r,results='hide'}
+bruceR::TTEST(
+ data = df.penguin, # 数据
+ y = "Temperature", # 因变量
+ x = "romantic" # 自变量
+)
+```
+
+
+
+[单样本t检验]
+```{r, results='hide'}
+stats::t.test(
+ x = df.penguin$Temperature,
+ mu = 36.6
+ )
+
+```
+
+
+
+```{r,results='hide'}
+bruceR::TTEST(
+ data = df.penguin, # 数据
+ y = "Temperature", # 确定变量
+ test.value = 36.6, # 固定值
+ test.sided = "=") # 假设的方向
+
+```
+
+
+
+
+[配对样本t检验]
+```{r,results='hide'}
+stats::t.test(
+ x = df.penguin$Temperature_t1, #第1次
+ y = df.penguin$Temperature_t2, #第2次
+ paired = TRUE)
+
+```
+
+
+
+
+```{r, results='hide'}
+bruceR::TTEST(
+ data = df.penguin, # 数据
+ y = c("Temperature_t1",
+ "Temperature_t2"), # 变量为两次核心体温
+ paired = T) # 配对数据,默认是FALSE
+```
+
+
+
+无论是使用bruceR包中的t检验功能,还是R语言自带的t检验功能,我们都可以发现它们得到的结果与线性回归模型的结果是一模一样的。这再次证实了t检验与线性回归模型之间的密切关系。在实际研究中,我们可以根据问题的具体情况选择合适的方法,无论是t检验还是线性回归模型。同时,理解它们之间的关系有助于我们更好地掌握统计分析的原理和技巧。
+
+### 总结
+我们来做一个简单的总结。对于t检验,我们可以使用R自带的t.test函数。虽然语法上略有不同,但实际上,线性模型和t检验在本质上是一致的。单样本t检验可以看作是一个仅有截距的线性模型;独立样本t检验是一个仅有截距和一个斜率的线性模型;而配对样本t检验则是一个基于差值的仅有截距的线性模型。
+
+独立样本t检验也可以看作是一个自变量为二分变量的线性回归模型。要理解为什么仅有截距的回归模型与t检验相关,我们可能需要回顾正态分布、抽样分布等概念,以及为什么我们使用t值这个分布来进行检验。然而,我们在这里没有足够的时间来展开这个话题。如果大家对此感兴趣,可以自行深入学习,也很推荐大家关注包寒吴霜老师的专栏。
+
+## ANOVA & linear regression
+接下来我们讨论方差分析,这也是心理学中常用的一种统计方法。在学习统计时,我们会花很多时间学习如何进行方差分解。
+
+### 研究问题
+以Penguin数据为例,我们刚才发现恋爱状态对体温没有太大影响。但我们可以进一步探讨它是否与距离赤道的距离有交互作用。因为我们知道,离赤道越远,气温就越寒冷。有时候,我们可能会发现某个自变量没有影响,但实际上它可能受到另一个自变量的调节。例如,在这个例子中,我们可以检验恋爱状态是否在较寒冷的地方影响体温,而在较暖和的地方则不需要调节。要检验这种交互作用,我们通常会想到使用双因素方差分析。
+
+### 代码实操 & 知识回顾
+双因素方差分析通常是我们在讨论方差分析时所指的完全随机方差分析。但在这里,我们需要注意我们使用的是双因素的重复测量方差分析,而不是完全随机的。也就是说,不同距离赤道的人以及处于不同恋爱状态的人并非随机分配。我们无法将某人随机分配到离赤道较近的恋爱状态这一组,因为这是一个横断数据。然而,我们仍然可以使用方差分析来进行数据分析。
+
+在学习方差分析时,我们可能会了解到它的历史以及它所检验的假设。对我们来说,检验的假设是关键。也就是说,在各因素的各个水平下,因变量的均值是否完全相同。原假设认为各因素各水平下的因变量均值完全相同,而备择假设则认为各因素各水平下的因变量均值并不完全相同。通常情况下,只要有一个水平的因变量均值不同,方差分析就能探测出来。
+
+当然,方差分析的使用也有一些前提假设,包括随机性、正态性、独立性和方差齐性等。这些内容我们在这里就不再详细展开了。
+
+### ANOVA代码实操|数据预处理
+在R中实现方差分析的关键是选择合适的函数。当我们想进行多因素方差分析时,R中有一些常用的函数可以帮助我们实现。在进行方差分析之前,我们需要对数据进行一些预处理。
+
+以距离赤道的距离为例,我们发现它是一个连续的数值变量。但为了进行方差分析,我们可以将其分为三个水平:热带、温带和寒温带。我们可以根据纬度的区别设定分隔点,例如,赤道到23.5度为热带,35度到40度为暖温带,40度到50度为中温带(统称为温带),50度以上为寒温带。我们可以使用R中的cut函数将连续性的数据划分为不同的段,并赋予不同的值。同时,我们可以为这个新变量命名为climate。
+[vars]
+
+```{r}
+summary(df.penguin$DEQ)
+```
+
+```{r}
+# 设定分割点
+# [0-23.5 热带, 23.5-35 亚热带], [35-40 暖温带, 40-50 中温带], [50-66.5 寒温带]
+breaks <- c(0, 35, 50, 66.5)
+
+# 设定相应的标签
+labels <- c('热带', '温带', '寒温带')
+
+# 创建新的变量
+df.penguin$climate <- cut(df.penguin$DEQ,
+ breaks = breaks,
+ labels = labels)
+summary(df.penguin$climate)
+```
+
+[tidy data]
+
+```{r}
+df <- df.penguin %>%
+ select(subjID, climate, romantic, Temperature)
+
+```
+
+```{r example of df, echo=FALSE}
+DT::datatable(head(df), fillContainer = TRUE)
+```
+
+
+### 代码实操|正态性检验
+在进行方差分析前,我们还需要检验数据的正态性。R中有一些方法可以帮助我们检验正态性,例如KS检验和QQ图。通过这些方法,我们可以观察数据的分布情况。虽然数据可能不是完全正态分布的,但接近正态分布的数据也是可以接受的。
+```{r}
+# 正态性检验-Kolmogorov-Smirnov检验
+# 若p >.05,不能拒绝数据符合正态分布的零假设
+ks.test(df$Temperature, 'pnorm')
+```
+
+```{r}
+# 进行数据转换,转换后仍非正态分布
+df$Temperature_log <- log(df$Temperature)
+ks.test(df$Temperature_log, 'pnorm')
+```
+
+[qq图]
+```{r, fig.width=7, fig.height=5}
+# 正态性检验-qq图
+qqnorm(df$Temperature)
+qqline(df$Temperature, col = "red") # 添加理论正态分布线
+```
+
+[直方图]
+```{r, fig.width=7, fig.height=5}
+ggplot(df, aes(Temperature)) +
+ geom_histogram(aes(y =..density..), color='black', fill='white', bins=30) +
+ geom_density(alpha=.5, fill='red')
+```
+
+
+### 代码实操|双因素被试间方差分析
+在R中实现方差分析,我们通常会用到aov函数,这是R自带的一个函数。实际上,这个函数来自于stats包,同样的,t.test函数和lm函数也是来源于这个包。在进行方差分析时,我们采用的语法与t检验非常相似,仍然是线性回归的写法。我们将因变量放在前面,自变量放在后面,中间用一个波浪号连接。然后,我们指定使用的数据。
+
+在方差分析的语法中,我们使用乘号 * 来表示主效应及其交互作用。例如,我们可以写成climate * romantic,这表示考虑climate和romantic的主效应以及它们之间的交互作用。在R中,这样的写法非常常见,前面的波浪号表示因变量与自变量之间的关系,而乘号表示考虑主效应及交互作用。
+
+实际上,这种线性方程有多种写法。例如,在t检验中,我们只关注romantic;而在方差分析中,我们加入了climate的主效应以及它们之间的交互作用。在R中最基础的包里,当我们进行方差分析时,实际上沿用了线性模型的语法。通过这种语法,我们可以得到方差分析的一些统计值,如F值、P值以及我们熟悉的平方和(SS,Sum of Squares)。
+
+如果我们对方差分析的结果使用summary函数进行查看,我们会看到熟悉的方差分析表。在这个表中,我们可以看到两个主效应(romantic和climate)以及它们之间的交互作用。同时,表格中还包含了残差(Residuals)信息。这些信息有助于我们了解各个因素对因变量的影响程度和显著性。
+
+```{r}
+aov1 <- stats::aov(Temperature ~ climate * romantic, data = df)
+summary(aov1)
+```
+
+在进行方差分析时,有时我们会发现,在R中进行的分析结果与在SPSS中进行的结果不完全一样。例如,climate的F值在R中为49.39,而在SPSS中为50.88。这种情况下,我们可能会感到困惑,不知道是否可以信任R的结果,甚至可能会认为R的分析方法不靠谱。
+
+
+[SPSS]
+```{r, echo = FALSE, fig.width = 4.5, fig.height = 3}
+knitr::include_graphics('pic/chp8/SPSS.png')
+```
+实际上,这种差异的出现很可能是因为在R和SPSS中使用的方法或某些设置不同。这可能导致在相同的数据和看似相同的方法下,得到不同的分析结果。在这里,我们需要讨论一个问题:为什么方差分析这样一个看似简单的方法(我们在本科阶段就学过并且很熟悉),会在不同软件中产生不同的结果呢?
+
+### 代码实操|平方和(SS)的计算
+在进行方差分析时,可能会涉及到平方和计算方法的问题。在平衡设计下,即每个条件下的样本量相同时,三类平方和的结果是没有差别的。但在不平衡设计下,即每个条件下的样本量不同时,就需要进行一些调整。
+
+这里的不平衡设计是指,比如我们有两个变量,climate有三个水平,romantic有两个水平,那么组合之后就有六个条件,但这六个条件下的被试数量可能是不同的。理论上,最好的情况是每个条件下的被试数量是一样的,这样的设计被称为平衡设计。在平衡设计中,三种类型的平方和的结果会很清晰,并且方差分析的结果独立于平方和的类型。但在实际情况中,这种设计可能较难实现,从而导致不平衡设计。
+
+在不平衡设计下,计算方差时就会出现问题,因为我们需要将不同组的方差进行合并。尤其是当各组样本量差距较大时,三种类型的平方和计算结果可能会不同。在这种情况下,我们需要进行一些调整,需要根据具体研究设计和问题来选择使用哪一种类型的平方和。这就产生了三类平方和。SPSS默认使用的是第三类平方和,而在R中,aov函数默认使用的是第一类平方和。因此,当我们使用R进行方差分析时,可能会发现结果与SPSS有所不同。
+
+这种情况下,我们需要更深入地理解平方和的计算方法,以便正确解读R的输出结果。这也是我们在使用R进行统计分析时,需要不断强化统计知识的一个重要原因。因为相比于SPSS这类点击式操作的软件,R提供了更多的选项,而正确理解和使用这些选项,就需要我们对统计学有更深入的理解。
+
+我们是否能得到完全相同的结果呢?实际上,这是完全可能的。例如,我们可以使用afex这个包,这是在2017或2018年后出现的一个包。afex是为了满足实验心理学家进行方差分析的需求而设计的,其中包含了各种方差分析的功能。如果我们按照这样的写法,即将被试的identity写在这里,然后将类型设置为三,我们就能得到完全相同的结果。我们也可以在其他地方使用同样的方法,同样能得到一模一样的结果。
+
+[car::Anova()]
+```{r}
+# 结果不一致,原因PPT显示不全,请回到rmd文档查看
+aov1 <- car::Anova(stats::aov(Temperature ~ climate * romantic, data = df))
+aov1
+```
+我们是否能得到完全相同的结果呢?实际上,这是完全可能的。例如,我们可以使用afex这个包,这是在2017或2018年后出现的一个包。afex是为了满足实验心理学家进行方差分析的需求而设计的,其中包含了各种方差分析的功能。如果我们按照这样的写法,即将被试的identity写在这里,然后将类型设置为三,我们就能得到完全相同的结果。我们也可以在其他地方使用同样的方法,同样能得到一模一样的结果。
+```{r}
+# 原因debug
+# 查看R的默认对比设置
+options("contrasts")
+# 从输出结果可知,无序默认为contr.treatment(),有序默认为contr.poly()
+# factor()函数来创建无序因子,ordered()函数创建有序因子
+
+is.factor(df$climate)
+is.ordered(df$climate)
+# climate是无序因子
+
+# 创建一个3水平的因子的基准对比
+c1 <- contr.treatment(3)
+
+# 创建一个新的对比,这个编码假设分类水平之间的差异被等分,每一个水平与总均值的差异等于1/3
+my.coding <- matrix(rep(1/3, 6), ncol=2)
+# 将对比调整为每个水平与第一个水平的振幅减去1/3
+# 可能的原因:除了关心每个水平对应的效果,同时也关心水平与水平之间的效果
+my.simple <- c1-my.coding
+my.simple
+
+# 更改climate的对比
+contrasts(df$climate) <- my.simple
+
+# 将数据集df的romantic列的对比设为等距对比,它假设分类水平之间的差异为等距离
+contrasts(df$romantic) <- contr.sum(2)/2
+
+# 方差分析
+aov1 <- car::Anova(lm(Temperature ~ climate * romantic, data = df),
+ type = 3)
+aov1
+```
+
+[afex::aov_ez()]
+```{r}
+afex::aov_ez(id = "subjID",
+ dv = "Temperature",
+ data = df,
+ between = c("climate", "romantic"),
+ type = 3)
+```
+
+```{r,results='hide'}
+# afex中的其他函数可以得到同样的结果
+afex::aov_car(Temperature ~ climate * romantic + Error(subjID), data = df, type = 3)
+afex::aov_4(Temperature ~ climate * romantic + (1|subjID), data = df)
+```
+也就是说,如果我们使用最常见的R代码,很可能会得到与SPSS不同的结果。同样的现象也可能发生在我们习惯使用AMOS进行结构方程模型(SEM)的情况下。当你使用R的某些包进行结构方程模型时,即使是同样的数据和结构,可能会得到不同的结果。这可能是因为它们内部设定的不同。因此,我们需要去了解它们内部的设定是什么。
+
+## 线性回归
+
+我们刚刚讨论的是t检验,它是一种特定的回归模型。而对于ANOVA(方差分析),从它的表示方法来看,我们可以很明显地看出它实际上也是一个回归模型。因为它的方程表示形式与线性模型非常相似,包括一些自变量以及它们之间是否存在交互作用。在实际应用中,我们经常会遇到单因素方差分析和多因素方差分析。
+
+在这种情况下,两因素方差分析也是一个特殊的回归模型。它的特殊之处在于我们的自变量是分类变量,也可以称为离散变量。因此,在进行方差分析时,我们需要注意处理这些分类变量或离散变量。
+
+假设我们这里有一个2×2的设计,也就是有四个条件,这四个条件可以视为四组。每一组被试在某一因变量上都会有自己的取值。我们需要检验的是这四组被试的均值是否完全相同,即它们是否有偏离总体均值的情况。这就是我们进行方差分析的一个核心目标。在线性模型上,我们也在检验同样的问题。
+
+比如说,我们可以通过某种方式对自变量进行编码,然后观察各组的均值与其他组之间的差异是否达到显著水平。举个例子,这里我们展示的是一种非常简单的编码方式,我们以某一条件下的值作为零点,比如第一组,然后其他所有的系数都是其他组的均值与这个零点的差异,也就是我们斜率的比较对象。
+
+可能这么说有点抽象,让我们以一个具体的例子来说明。假设我们有一个变量叫"romantic",我们可以把它编码为0和1。同样地,我们也可以把其他变量编码为0和1。如果我们简化这两种情况,就更容易理解了。
+
+在这里,我们可以看到如果有一个组合为0和0,那么在我们的回归模型中,这个部分就变成了β0。在这种编码方式下,β0代表的就是某个条件下的均值。通过这种方式,我们可以看到β1可能代表的是某种条件下的值,比如当某个变量取值为1或0时的情况。同样,β2可能代表的是另一个变量在取值为1、0或者两个变量都为1时的情况。
+
+所有这些回归系数都可以被理解为组间的差异值。我们的回归模型的目标就是去检验这些差异是否显著。在做回归模型时,我们检验显著性的方式可能有所不同。在这种最简化的情况下,我们可以看到,我们的2×2的两因素方差分析完全可以对应成为一个离散变量的回归模型。
+
+```{r, echo = FALSE, fig.width = 3.5, fig.height = 2}
+knitr::include_graphics('pic/chp8/aovLM.png')
+```
+
+如果我们采用ANOVA或者线性模型,并且我们的设定是一样的,那么得到的结果将会是完全一样的。可能我讲得有点快,大家可能需要回顾一下自己学的线性回归的知识。因为我不确定其他的统计老师在讲线性回归的时候是否已经将其与ANOVA或t检验进行了关联。但大家可以这样理解,它们本质上都是一样的。如果大家理解了,ANOVA其实就是线性回归的一个特定模式,那么你们就已经把握了其要点。
+
+
+```{r,results='hide'}
+aov1 <- car::Anova(
+ aov(Temperature ~ climate * romantic,
+ data = df),
+ type = 3
+ )
+aov1
+```
+
+![](./pic/chp8/compare8231.png)
+
+
+
+
+
+```{r,results='hide'}
+lm1 <- car::Anova(
+ lm(Temperature ~ climate * romantic,
+ data = df),
+ type = 3
+ )
+lm1
+```
+
+![](./pic/chp8/compare8232.png)
+
+
+在实际研究中,即使我们知道ANOVA与线性回归有相似之处,但我们仍然需要报告传统的方差分析结果。在这种情况下,我们推荐使用bruceR包的`mANOVA`函数,它是一个非常强大的函数,基本上涵盖了我们在心理学中常用的所有ANOVA类型。使用`mANOVA`函数可以大大简化心理学研究者使用这个方法的门槛。
+
+
+
+关于`mANOVA`函数的主要参数,我们不再详细讲解,大家可以查阅相关资料。该函数非常全面,而且有一个非常好的特点,就是它可以直接将结果保存为三线表格,方便我们将结果粘贴到Word文档中进行报告。
+
+
+
+
+以我们刚才讲解的例子为例,我们要对climate和romantic进行方差分析,我们可以调用`mANOVA`函数,设置dependent variable为temperature,参数设为between表示主间变量。这里有两个主间变量。大家可以看到,得到的结果与SPSS是完全相同的,如50.88。
+
+与SPSS不同之处在于`mANOVA`函数会输出Generalized Eta Squared(广义η²),这是方差分析的一个效应量指标,有助于我们更好地理解结果。关于效应量的指标,对于传统心理学来说,我们通常只关注p值。但实际上,最近的趋势是大家会更加关注效应量有多大,而不仅仅是p值是否显著。换句话说,我们不仅关心结果是否显著,还关心效应量是否足够大。
+
+在方差分析中,我们经常会进行进一步的简单效应分析和事后多重比较。这里的操作比较灵活。例如,如果你使用的是纯粹的重复测量设计,那么在没有交互作用的情况下,我们可能会进行多重比较,也就是比较某一条件下不同水平之间是否存在差异。然而,如果存在交互作用,我们可能需要重新检查在某一自变量的水平下,另一个自变量是否具有效应。这种情况下,我们称之为简单效应。简单效应分析可以帮助我们深入了解在某一特定条件下,不同水平之间的差异,以便更好地解释和理解我们的研究结果。
+
+
+```{r,results='hide'}
+res1 <- bruceR::MANOVA(data = df,
+ dv = "Temperature",
+ between = c("climate", "romantic"))
+```
+```{r,echo=FALSE}
+res1 %>%
+ capture.output()
+```
+
+在这个例子中,我们可以看到,我们对climate和romantic进行了方差分析,试图查看在不同的romantic条件下(即单身或非单身),climate是否有影响。这就是简单效应分析。
+
+在R中,我们可以看到mANOVA函数的输出结果。首先,它会输出描述性统计结果,包括climate和romantic的各个水平的均值、标准差和样本量。然后,它会给出方差分析表,包括climate的主效应、romantic的主效应和它们的交互作用的效应。我们可以看到,climate的主效应是显著的,这与我们的预期一致。即在不同气候带的人们体温存在差异,其p值非常小,F值也比较大。此外,我们也发现climate和romantic的交互作用是显著的,这也与我们的预期相符。
+
+原先,我们试图通过t检验来查看不同romantic状态下的人们体温是否有区别,但我们并没有发现显著差异。然后,我们怀疑这可能是因为人的体温受到climate的影响,这个效应可能掩盖了romantic的影响。因此,我们引入了climate这个因素,然后再次检验romantic和climate的交互作用。结果显示,这个交互作用是显著的,这也就验证了我们的猜想。
+
+在mANOVA中,它会输出各种效应量指标,如η²和广义η²(Generalized Eta Squared)。通常情况下,在做方差分析时,我们会报告广义η²。实际上,关于方差分析的效应量指标是一个重要且不容易掌握的知识点。在mANOVA中,它还会输出其他效应量指标,如Cohen's f和方差同质性检验结果。这些输出结果非常符合心理学研究者的使用习惯。
+
+
+
+当我们发现交互作用时,我们通常需要进一步了解这个交互作用代表什么。在这种情况下,我们可以使用`emmeans`(estimated marginal means)函数来查看不同条件下的结果。这个函数可以帮助我们查看在不同条件下,climate和romantic之间的简单效应。通过`emmeans`函数,我们可以得到一个三线表,展示了在不同romantic条件下,climate的简单效应。在这个表格中,我们可以看到,在不同的romantic条件下,climate都具有显著效应。这些信息有助于我们更深入地了解交互作用背后的实际情况。
+
+在这个分析过程中,我们首先在恋爱和单身的条件下分别看了climate的影响,并发现climate在两种情况下都有显著影响,虽然效应量可能会有所不同。然后,我们进一步进行了post-hoc比较,比较了恋爱和单身状态下,不同climate的体温差异,结果显示基本上所有的差异都是显著的。
+
+然后,我们通过改变分组条件,看了在不同climate下,恋爱关系是否有影响。结果显示,在热带和温带,恋爱关系没有显著影响,但在寒带,恋爱关系则有显著影响。这个结果与我们的预期相符,表明在较冷的气候下,恋爱关系对体温有调节作用。
+
+此外,我们还进行了F检验,实际上是在热带、温带和寒带三种条件下进行了单因素方差分析。由于只有两个水平(恋爱和单身),所以这个F检验的结果与后面的t检验结果基本一致。这是因为在只有两个水平的情况下,F检验和t检验的结果是一样的。如果你对这个有兴趣,你可以进一步查看t值和F值之间的关系。
+```{r, results='asis'}
+sim_eff <- res1 %>%
+ bruceR::EMMEANS("climate", by = "romantic") %>%
+ bruceR::EMMEANS("romantic", by = "climate")
+```
+
+
+## 知识延申|单因素方差分析示例
+在本节课中,我们讲解了单因素方差分析和多因素方差分析,并重点讲解了多因素方差分析。我们提到了R中自带的函数以及其他一些适合心理学研究者使用的包,如PR和fx。这些包的输出结果与SPSS的输出结果非常相似,因此可以让老师和同学们更加放心地使用R进行分析。
+```{r}
+# DEQ对Temperature的影响
+res2 <- bruceR::MANOVA(
+ data = df,
+ dv = "Temperature",
+ between = "climate")
+```
+
+
+## 知识延申|总结
+
+此外,我们还讨论了线性模型与t检验之间的关系。可能有些同学在理解这部分内容时会感到困惑,这时候建议大家回顾一下本科阶段的教材,加深对回归模型和t检验之间关系的理解。学习代码和应用是很重要的,但要正确地使用它们,还需要掌握背后的统计知识。
+
+总之,通过学习本节课的内容,我们可以更好地理解方差分析的原理和应用,并能够在R中使用相应的函数进行实际分析。同时,我们也要意识到,在学习和使用R进行统计分析时,理解和掌握背后的统计知识是非常重要的。
+
+
+
+课堂总结
+
+![](./pic/chp8/route.png)
+
+在今天的课程中,我们讨论了方差分析的原理和应用,并在R中使用了相关函数进行实际分析。我们还提到了一个国外博客,这个博客指出许多常见的统计检验都是线性模型的特例。这可以帮助大家更好地理解这些统计方法之间的联系。
+
+最后,我们留了一个思考题:对于重复测量设计(within-subject design),如何用回归模型进行分析?此外,我们还给出了一些课堂练习,建议大家尝试自己编写代码来完成这些练习,以加深对课程内容的理解。
+
+本节课在此结束。感谢大家的参与,如果有任何问题,请随时提问。
\ No newline at end of file
diff --git a/bookdown_files/Books/Book/_book/_main_files/figure-html/unnamed-chunk-20-1.png b/bookdown_files/Books/Book/_book/_main_files/figure-html/unnamed-chunk-20-1.png
new file mode 100644
index 00000000..232599a8
Binary files /dev/null and b/bookdown_files/Books/Book/_book/_main_files/figure-html/unnamed-chunk-20-1.png differ
diff --git a/bookdown_files/Books/Book/_book/_main_files/figure-html/unnamed-chunk-21-1.png b/bookdown_files/Books/Book/_book/_main_files/figure-html/unnamed-chunk-21-1.png
new file mode 100644
index 00000000..6bd23b3b
Binary files /dev/null and b/bookdown_files/Books/Book/_book/_main_files/figure-html/unnamed-chunk-21-1.png differ
diff --git a/bookdown_files/Books/Book/_book/pic/chp8/ANOVA data.png b/bookdown_files/Books/Book/_book/pic/chp8/ANOVA data.png
new file mode 100644
index 00000000..22f2186f
Binary files /dev/null and b/bookdown_files/Books/Book/_book/pic/chp8/ANOVA data.png differ
diff --git a/bookdown_files/Books/Book/_book/pic/chp8/EMMEANS.png b/bookdown_files/Books/Book/_book/pic/chp8/EMMEANS.png
new file mode 100644
index 00000000..bba19b5f
Binary files /dev/null and b/bookdown_files/Books/Book/_book/pic/chp8/EMMEANS.png differ
diff --git a/bookdown_files/Books/Book/_book/pic/chp8/IJzerman2018fig.png b/bookdown_files/Books/Book/_book/pic/chp8/IJzerman2018fig.png
new file mode 100644
index 00000000..583ffd4e
Binary files /dev/null and b/bookdown_files/Books/Book/_book/pic/chp8/IJzerman2018fig.png differ
diff --git a/bookdown_files/Books/Book/_book/pic/chp8/MANOVA.png b/bookdown_files/Books/Book/_book/pic/chp8/MANOVA.png
new file mode 100644
index 00000000..0ee27e86
Binary files /dev/null and b/bookdown_files/Books/Book/_book/pic/chp8/MANOVA.png differ
diff --git a/bookdown_files/Books/Book/_book/pic/chp8/SPSS.png b/bookdown_files/Books/Book/_book/pic/chp8/SPSS.png
new file mode 100644
index 00000000..d7fd9240
Binary files /dev/null and b/bookdown_files/Books/Book/_book/pic/chp8/SPSS.png differ
diff --git a/bookdown_files/Books/Book/_book/pic/chp8/TTEST.png b/bookdown_files/Books/Book/_book/pic/chp8/TTEST.png
new file mode 100644
index 00000000..0d233877
Binary files /dev/null and b/bookdown_files/Books/Book/_book/pic/chp8/TTEST.png differ
diff --git a/bookdown_files/Books/Book/_book/pic/chp8/aovLM.png b/bookdown_files/Books/Book/_book/pic/chp8/aovLM.png
new file mode 100644
index 00000000..8b44ae98
Binary files /dev/null and b/bookdown_files/Books/Book/_book/pic/chp8/aovLM.png differ
diff --git a/bookdown_files/Books/Book/_book/pic/chp8/compare1.1.png b/bookdown_files/Books/Book/_book/pic/chp8/compare1.1.png
new file mode 100644
index 00000000..5b3b2114
Binary files /dev/null and b/bookdown_files/Books/Book/_book/pic/chp8/compare1.1.png differ
diff --git a/bookdown_files/Books/Book/_book/pic/chp8/compare1.2.png b/bookdown_files/Books/Book/_book/pic/chp8/compare1.2.png
new file mode 100644
index 00000000..e4a27226
Binary files /dev/null and b/bookdown_files/Books/Book/_book/pic/chp8/compare1.2.png differ
diff --git a/bookdown_files/Books/Book/_book/pic/chp8/compare2.1.png b/bookdown_files/Books/Book/_book/pic/chp8/compare2.1.png
new file mode 100644
index 00000000..303cb0b3
Binary files /dev/null and b/bookdown_files/Books/Book/_book/pic/chp8/compare2.1.png differ
diff --git a/bookdown_files/Books/Book/_book/pic/chp8/compare2.2.png b/bookdown_files/Books/Book/_book/pic/chp8/compare2.2.png
new file mode 100644
index 00000000..d4e8c9dd
Binary files /dev/null and b/bookdown_files/Books/Book/_book/pic/chp8/compare2.2.png differ
diff --git a/bookdown_files/Books/Book/_book/pic/chp8/compare3.1.png b/bookdown_files/Books/Book/_book/pic/chp8/compare3.1.png
new file mode 100644
index 00000000..ad222ae5
Binary files /dev/null and b/bookdown_files/Books/Book/_book/pic/chp8/compare3.1.png differ
diff --git a/bookdown_files/Books/Book/_book/pic/chp8/compare3.2.png b/bookdown_files/Books/Book/_book/pic/chp8/compare3.2.png
new file mode 100644
index 00000000..69a0582b
Binary files /dev/null and b/bookdown_files/Books/Book/_book/pic/chp8/compare3.2.png differ
diff --git a/bookdown_files/Books/Book/_book/pic/chp8/compare8231.png b/bookdown_files/Books/Book/_book/pic/chp8/compare8231.png
new file mode 100644
index 00000000..134c3b30
Binary files /dev/null and b/bookdown_files/Books/Book/_book/pic/chp8/compare8231.png differ
diff --git a/bookdown_files/Books/Book/_book/pic/chp8/compare8232.png b/bookdown_files/Books/Book/_book/pic/chp8/compare8232.png
new file mode 100644
index 00000000..d892e8e7
Binary files /dev/null and b/bookdown_files/Books/Book/_book/pic/chp8/compare8232.png differ
diff --git a/bookdown_files/Books/Book/_book/pic/chp8/indet-lm.png b/bookdown_files/Books/Book/_book/pic/chp8/indet-lm.png
new file mode 100644
index 00000000..c5472704
Binary files /dev/null and b/bookdown_files/Books/Book/_book/pic/chp8/indet-lm.png differ
diff --git a/bookdown_files/Books/Book/_book/pic/chp8/leven.png b/bookdown_files/Books/Book/_book/pic/chp8/leven.png
new file mode 100644
index 00000000..9c987cb2
Binary files /dev/null and b/bookdown_files/Books/Book/_book/pic/chp8/leven.png differ
diff --git a/bookdown_files/Books/Book/_book/pic/chp8/pair.png b/bookdown_files/Books/Book/_book/pic/chp8/pair.png
new file mode 100644
index 00000000..d729b71b
Binary files /dev/null and b/bookdown_files/Books/Book/_book/pic/chp8/pair.png differ
diff --git a/bookdown_files/Books/Book/_book/pic/chp8/pairt-lm.png b/bookdown_files/Books/Book/_book/pic/chp8/pairt-lm.png
new file mode 100644
index 00000000..2d9a5bbb
Binary files /dev/null and b/bookdown_files/Books/Book/_book/pic/chp8/pairt-lm.png differ
diff --git a/bookdown_files/Books/Book/_book/pic/chp8/route.png b/bookdown_files/Books/Book/_book/pic/chp8/route.png
new file mode 100644
index 00000000..96c64a08
Binary files /dev/null and b/bookdown_files/Books/Book/_book/pic/chp8/route.png differ
diff --git a/bookdown_files/Books/Book/_book/pic/chp8/sample.png b/bookdown_files/Books/Book/_book/pic/chp8/sample.png
new file mode 100644
index 00000000..f53267df
Binary files /dev/null and b/bookdown_files/Books/Book/_book/pic/chp8/sample.png differ
diff --git a/bookdown_files/Books/Book/_book/pic/chp8/singlet-lm.png b/bookdown_files/Books/Book/_book/pic/chp8/singlet-lm.png
new file mode 100644
index 00000000..08a2279e
Binary files /dev/null and b/bookdown_files/Books/Book/_book/pic/chp8/singlet-lm.png differ
diff --git a/bookdown_files/Books/Book/_book/reference-keys.txt b/bookdown_files/Books/Book/_book/reference-keys.txt
index 38f40ce2..bd5fd17b 100644
--- a/bookdown_files/Books/Book/_book/reference-keys.txt
+++ b/bookdown_files/Books/Book/_book/reference-keys.txt
@@ -245,3 +245,26 @@ part3-正文的撰写
小练习
元分析简介
元分析实现
+warning程辑包tidyverse是用r版本4.3.3来建造的
+密度图
+第八讲回归模型一
+研究问题
+t-test作为回归模型的特例
+独立样本t检验independent-t-test
+线性回归linear-regression
+单样本t检验one-sample-t-test
+配对样本t检验paired-t-test
+brucerttest
+anova-linear-regression
+研究问题-1
+代码实操-知识回顾
+anova代码实操数据预处理
+代码实操正态性检验
+代码实操双因素被试间方差分析
+代码实操平方和ss的计算
+线性回归
+知识延申单因素方差分析示例
+知识延申总结
+可能遇到的问题
+回归-代码实现
+总结-3
diff --git a/bookdown_files/Books/Book/_book/search_index.json b/bookdown_files/Books/Book/_book/search_index.json
index ce12a43f..a9d6ece8 100644
--- a/bookdown_files/Books/Book/_book/search_index.json
+++ b/bookdown_files/Books/Book/_book/search_index.json
@@ -1 +1 @@
-[["index.html", "R语言在心理学研究中的应用: 从原始数据到可重复的论文手稿(V2) Chapter 1 教学内容与课时 1.1 目录", " R语言在心理学研究中的应用: 从原始数据到可重复的论文手稿(V2) 胡传鹏 2024年06月30日 Chapter 1 教学内容与课时 1.1 目录 第一讲:为什么要学习R 1.1 R在心理科学及社会科学中的运用 1.2 R语言使用的示例展示 1.3 课程安排 1.4 如何学好这门课 第二讲:如何开始使用R 2.1 要解决的数据分析问题简介[介绍我们的数据和拟解决的问题,对比R和传统flow] 2.1 如何安装? 2.2 如何方便使用?Rstudio的安装与界面介绍 第三章:如何使用本课件/电子书资源 3.1 Git与Github 3.2 项目、文件与代码的规范化 第四章:如何导入数据 4.1 路径与工作目录 4.2 读取数据 4.3 了解R里的数据 (R语言中的对象) 第五章:如何清理数据一 R语言编程基础 5.1 R对象的操控 5.2 逻辑运算 5.3 函数 第六章:如何清理数据二 数据的预处理 6.1 Tidyverse简介 6.2 问卷数据的预处理:基本 6.3 实验数据的预处理:提高 第七章:探索数据: 描述性统计与数据可视化基础 7.1 描述性统计 7.2 探索性数据分析(DataExplorer) 7.3 ggplot2基础 第八章:R语言中的统计分析: 线性模型1 (t-test、anova等) 8.1 语法实现 8.2 分析的流程 第九章:R语言中的统计分析: 线性模型2(rm-anova、层级模型) 9.1 语法实现 9.2 分析的流程 第十章:R语言中的统计分析: 线性模型3(GLM) 10.1 语法实现 10.2 分析的流程 第十一章:R语言中的统计分析: 线性模型4(中介效应模型) 11.1. 多种分析方法的实现 11.2 代码整合与规范化 第十二章: 如何得到可发表的图像: 数据可视化进阶(3学时) 12.1 ggplot2的图层与面板控制 12.2 ggplot2与其他工具的结合 第十三章:心理学研究中的网络分析 第十四章:心理学元分析入门 "],["lesson-1.html", "Chapter 2 第一讲:为什么要学习R 2.1 R在心理科学及社会科学中的运用 2.2 R语言使用的示例展示 2.3 现场运行代码 2.4 课程安排 2.5 如何学好这门课 2.6 课程总结与期望 2.7 推荐", " Chapter 2 第一讲:为什么要学习R 序 本课程主要面向具有心理学背景的学生,包括教育学、社会学等相近领域的同学。今天的第一堂课旨在让同学们了解这门课程的性质,判断它是否适合自己。这里我想提醒大家,虽然R语言编程语言非常有趣,但它并不适合所有人。有些同学上完课后,处理数据时仍然使用SPSS,这说明本课程对他们来说没有太大意义。如果只是因为学分而选择这门课,是没有必要的,你完全可以选择一些更有趣的课程。 在本课程的学习中,你可能会遇到一些情绪上的起伏,因为本课程与我们通常学习的知识不同,需要我们改变思考问题的方式。如果你不打算使用R语言进行数据分析,不改变思考问题的方式,那就没有必要选择这门课。当然,我也希望通过这门课程,向大家展示学习R语言编程语言的价值,让大家认为经历情绪的起伏是有值得的。 上本课的另一个动机是希望大家更多地使用开源软件,放弃使用商业软件,尤其是在国内存在盗版泛滥的情况下。学完本课程可以帮助你逐渐转向使用开源软件。此外,现在学习R语言比几年前更加轻松,因为现在有很多在线资料和代码可以参考。完成这门课程后,你可能至少会接触一些代码,这些代码可以直接应用于分析某些数据。 因此,这次课的主要目的在于帮助大家了解本课程的基本情况,为本课程做好心理准备。所以接下来,我们主要介绍为什么要开设这门课程、课程的内容是什么、需要做什么样的准备以及能收获什么。 2.1 R在心理科学及社会科学中的运用 2.1.1 数据科学 这门课的开设有其时代的大背景。作为在心理学院的课程,我们将这门课称为《R语言在心理学研究当中的应用》。但实际上,R语言是当前数据科学(data science)中主流的计算机语言之一。正是数据科学在各种学科中的渗透和普及,让我们开设这门课程显得非常重要。那么什么是data science呢? 数据科学是什么? 在科学研究中有人认为,科学的革命经过了几次范式转换(参考链接)。最早期的是”实验”科学,研究者通过设计和完成实验,一个一个地去检验假设。随后是理论科学,在实验基础上进行归纳。随着计算机越来越发达,我们进入了”计算”时代,通过用各种计算模型模拟的方法,帮助我们去理解世界。但是现在,随着数据越来越多,通过数据驱动的方式就能发现很多新的东西。最近这些年,很多在科技领域尤其是在计算机领域取得的重大突破和进展都是依赖于大量数据的,也就是通过对数据进行“提炼”从而得到新的发现。比如说2023年初非常火的ChatGPT。作为现在全球最火的科技界产品之一,它背后的模型叫做LLM,Large Language Model,就是一个大语言模型,它依靠的就是大量语言材料的训练。 数据科学的内容 大概10多年前,数据科学就已经出现。大家也许对“数据科学”这个术语已经不再陌生。数据科学里面既涉及到计算机编程,也包括数理统计。当讨论具体应用领域的数据科学,比如心理学的科研领域,数据科学也需要domain-specific的知识,也就是这个领域的特殊性知识。 [此处插入关于数据科学的Venn图] 这意味着什么? 意味着如果你仅仅懂计算机,那你不一定能懂data science的;如果你仅仅是懂数学和统计,那也不意味这你能解决一个data science的问题。必须要将计算、统计和领域特殊的知识进行结合。在心理学研究中,这对研究生提出一个新的要求。 2.1.2 数据科学的诞生——数字化时代 为什么会有data science? 大家应该能直观地感受到:随着个人电脑的普及,互联网越来越发达,整个社会所产生的数据呈现爆炸式的增长。下图是一个可视化的例子。我们可以看到,在计算机出现之前人类产生的数据是非常少的,而计算机出现之后产生的数据越来越多。 我们也有了越来越多的个人电子设备以及其他的先进设备,它们所观察到的、产生的数据也是非常大的。这个图片相信很多人在朋友圈都被刷屏过。这是人类所能观察到的一个划时代的新的图像,尽管我们作为外行可能不知道它具体的内涵是什么,但是都知道它很酷。 此外,我们国家是互联网普及最高的国家之一,我们现在有百分之七十四(可能现在有更多了)的人都已经开始接入到互联网。这么多的人接入到互联网,所产生的数据可想而知,一定是海量的。所以现在我们很多电商,像淘宝这样的各类购物平台,它们在中国做的是非常好的,这也得益于海量的数据。包括最近像拼多多,听说在美国也是势如破竹,态势很猛。还有像TikTok,就是字节跳动,前一段时间在美国甚至要被封杀了,原因就是年青人都很喜欢用它。字节跳动的一个特点就是它很好地利用了中国大量的网民产生的海量数据,通过网民不断地使用它们的产品,不断地进行迭代。所以当它能出海的时候,去给海外的用户提供服务的时候,它的迭代已经非常成熟了。当然迭代的过程中需要大量数据的产生和调试,产品才能越来越成熟。 数字化对心理学研究的影响 近年来,随着人工智能的兴起,数据科学变得越来越重要。我们生活在大数据时代,心理学或心理科学也在这个时代中发展起来。我们收集的数据越来越多,我们的生活方式也在逐渐数字化。因此,数字化成为了新时代的重要组成部分。在数字化时代,心理学不可避免地会受到数据化的影响。 其实很早就有人关注数字化我们心理学的影响了,国内的研究者也经常会提到心理学和大数据。我们这里做一个不完全的概括,主要是这三个方面的显著变化。 Big n (sample size) 首先就是样本量很大。现在的数字化平台生产的数据非常大。我们传统的行为学实验可能只有几十、几百人的数据,上千已经是很不错的了,上万就比较费劲了。但是如果说我们能从互联网上抓取数据的话,那动辄就是上万甚至是百万级别的。例如,与企业合作或者使用可穿戴设备,我们可以收集到大量的心理健康数据,这已经超越了传统心理学研究的范畴。 Big v (variables) 其次,我们现在面对的数据不仅仅是数量上的增加,还有数据维度的增加。以前我们可能只关注反应时间和准确率等少数几个变量,但现在我们可能需要处理大量的参数,比如个人的购物记录、聊天记录、身体活动数据等。比如我们使用手机,那其实我们产生了非常多的数据。你一天使用多久、点击多少次、点击了什么、在哪个地方、用的是什么APP,甚至包括你所处的地址,你在地球上的经纬度、当地的气温、湿度这些数据信息。这些数据的维度非常多,每个变量都可能对我们的研究产生影响。 Big t (time) 还有就是时间的跨度比较长。现在很多的APP一旦用户开始使用之后就会长期使用,如果能用于收集心理学的数据,就可以在很长的一段时间里记录很多的数据(Experience Sampling Method, ESM, or Ecological Momentary Assessment, EMA)。我们现在有了新的研究设备,比如通过手机应用程序不断追踪个体的心理状态,可能一天收集五六次,连续追踪一个月,这样就会收集到大量的数据。这些数据为我们提供了新的分析挑战,我们需要新的方法来处理这些数据。 这对于了解人类的心理和行为的规律来说其实是非常好的。对于发展心理学家来说,有这样的一个例子:有一个做语言发展的一个研究者他从自己的孩子出生开始就一直用视频记录孩子的成长(见science最近的文章)。儿童产生语言目前对于人类来说还是一个不能够完全理解的过程,从语言学上看这是一个很大的跳跃。这位研究者完全记录了孩子生长的过程,积累了大量的数据,并通过对视频数据的全方位分析,得到了以前心理语言学比较少能够得到的东西。 数字化时代的心理学研究 这里有几个例子。比如这个是利用手机里的数据预测人格,我们这里看到的纵轴就是人格的类型,不同的颜色表示使用不同的方法进行预测,横轴是相关系数。可以发现说手机里的一些数据和人格的倾向是密切相关的。 另外,像我们心理学的顶刊Psychological Science,上面也不定期的有研究是探索我们在数字的视觉中留下的痕迹与行为之间的关系。比如这篇文章发现我们个体在使用手机的行为上是非常的一致的。研究者通过大量的数据是得到了比较强的结论,尽管这个结论比较简单,但是基于数据量比较大,是比较有说服力。 我们也可以利用互联网的平台来收集行为的数据。传统上我们是把被试带到实验室做实验,但现在由于互联网的发达,我们可以进行线上实验。在疫情期间,不少同学可能尝试过JsPsych或者巴普洛夫这样的平台,把实验编写好放在网上,并通过链接给被试发放被试费。这样我们在短时间内可以收到比传统实验室更大量的数据。同时,因为我们使用的其实还是传统的实验任务,只要事先验证过在线的实验和实验室实验的可比性,就可以利用互联网在线去收集更大量的数据来研究我们感兴趣的问题。 比如这里列举的实验,去年的R课上我们以此作为了示例。它就是通过在线的平台收集的数据,收集到了很多变量。它的研究一个目的是为了探测self regulation,自我控制或者自我调节,不同的测量方法是不是一致的,哪一个能更好地预测生活中的一些行为,比方说这里就是看哪一些与饮食控制有更强的相关。 数字化时代心理学研究方式的变化 数字化时代不仅给心理学提供了新的数据,实际上也变革了我们做研究的方式。比如说合作。在疫情以前,研究者建立国际合作主要是通过导师的联系、邀请讲座或者是开会时建立联系。但在疫情期间,因为大家都困在家里同时又都使用很发达的互联网,所以就有研究者直接在社交媒体上发起合作的倡议。比如有的人说我有个想法需要在不同国家收数据,想要有人和我一起收集数据,最后可以一起发表文章。在疫情期间很多研究就是这么展开的,并且因为这样的研究往往样本量比较大,也能发到很好的期刊上去。同时,除了个人的发起,研究者们也成立了更多的学术组织,比方说Psychological Science Accelerator (https://psysciacc.org/)心理科学加速器,这个组织就是专门组织在全球范围通过互联网进行合作研究心理科学加速器,这个组织就是专门组织在全球范围通过互联网进行合作研究){.uri}。 此外,以前研究者需要去线下开会或者是参加工作坊,现在即便是疫情过去了,大家还是越来越习惯和更多地采用在线的方式进行学术讲座或是工作坊。所以呢互联网实际上是改变了我们心理学的方方面面。 2.1.3 为什么要学习R语言?{1-why-learn-R} 我个人总结了一些我们学习R语言或者说学习R,而不是Matlab或python的理由。 首先,学习R是一个大的背景。R语言是一个开源的一个软件,它跟python、junior一样,基本绝大部分的基于R语言的工具都是开源的免费的,也说你基本上都能够(只要你的互联网是畅通的话)免费得到所有的内容。 第二,它是一个高级的语言,不需要和计算机的硬件直接进行交流,和我们日常的英语差不多。 第三,它有一个强大的community。我们在选择工具时,不仅要考虑工具本身的功能,还要考虑使用这个工具的社区和支持。因为一个好的社区可以提供大量的资源和经验,帮助我们更好地使用这个工具。因为现在的所有的开源的语言,它依赖于有多少人在使用它、有多少人在不断的进行开发,尤其是谁在开发这些新的东西。对于我们心理学或者是社会科学而言,绝大部分是使用R做数据分析。简单来讲,我们一开始作为新手肯定不会去开发什么工具的,就必须要把别人开发的工具拿过来。那谁为我们开发呢?肯定是这个community里的人,这就需要我们有一个比较成熟和强大的社区。而R语言本身就是由统计学家所开发的,所以它就是为了做数据分析而生的一门语言。同时,在这么多年的发展当中有大量的研究者,尤其是社会科学的研究者不断加入这个community,从初学者变成使用者最后变成开发者。 从做研究的角度来说,R可以在这三个方面提供强大的支持。 科学性 使用R有助于增强计算的可重复性。如果我们能够精确地重复我们的分析,并且得到相同的结果,那么我们的研究就更加可信。在讨论心理学研究的可重复性时,我们发现即使是有了公开的数据和代码,也很难保证研究的精确重复。一个研究发现,在14篇文章中,只有一篇能够完全精确地重复出来(https://doi.org/10.1177/09567976221140828)。这表明我们在数据分析的过程中,很多微小的步骤如果没有被完整记录下来,就很难保证研究的可重复性。为了解决这个问题,现在越来越多的人鼓励使用编程语言,如R语言,来记录数据分析的每一步。这样,我们可以从原始数据开始,记录下所有的数据处理步骤,从而确保研究的可重复性。例如,我与合作者2020年发表的一篇文章中(https://doi.org/10.1525/collabra.301),我们公开了所有的数据和代码,并且有小组检查了这些数据和代码,发现能够得到与我们报告中大致相同的结果。 R会提供新的统计方法。IJzerman 2018年的Collabra: Psychology这篇文章(https://doi.org/10.1525/collabra.165),我也是合作者之一,当时也通过互联网来合作收集的数据。在这个文章当中,他就使用了机器学习的方法,叫做(条件)随机森林,叫做conditional random forest。它实际上是在机器学习里面非常常见的一个方法。它的特点就是说即便只有比较少的数据,也能够得到比较稳健的结果。当然这个小数据是相对于机器学习里面的小的数据,因为机器学习里面可能动则就是上十万百万的数据。相比而言,我们的数据其实每一个都是很小的,就几百人上千人。所以当拿到这1,000多人的数据之后,他想去探索这么多变量之间到底哪些变量之间有一个比较稳定的关系,他就采用了随机森林的方法,最后也发现他感兴趣的那个变量,就是身体的温度和这个社交网络的复杂程度是有关系的。 R会提供更合适的方法。比方说我们实验室实验当中非常常用的反应时间,它基本上都是偏态的分布,对于这种偏态分布的数据到底应该采用什么样的一个模型,到底是用传统的线性模型还是应该用广义的线性模型。如果使用r,那我们可以很灵活的使用r里面比较新的一些回归模型的包。在这包里面我们可以使用最适合这个模型的,比方说GLM。我们甚至可以通过模型比较的方式找到哪一个模型是最适合的。也就是说正是因为在R有一个很强大的community,然后这里面有众多可以选择的r的工具包。这样我们就能够不仅仅是使用新的方法,它也可以帮助我们不断的去选出更加适合的方法。 美观 R语言提供了强大的绘图功能,便于调整细节,可以帮助我们在数据分析的每个阶段进行可视化。如ggplot2,它允许用户调整图表的每一个细节,以更直观、美观的方式展示数据。 我们后面会讲可视化的进阶,那一章的时候我们会把ggplot这个最常见的画图软件里面的每一个细节都掰开讲,这里我们只是稍微展示一下。我们可以把原始数据和group level数据结合到一起,然后再把每个被试的数据,把它的分布画出来。 最近几年非常流行的雨云图。当然我们还可以把多个(图)进行叠加,像这种被试类的实验设计我们可以把每个点都连到一起,可以看到在不同之间的一个变化,它是不是完全具有一致性的。然后我们把把这个box plot也加上去,这样的话我们能够看到极端点。我们同时还把这个分布加上去,当然这个分布目前的α值比较高,我们还可以把它调的低一点,就是说让他透明度再低一点,让我们看到这个分布之间的一个叠加。这样我们就可以在一个图上看到非常丰富的信息。 当然还有一个叫做ggridges的一个图,这个上面我们不仅仅看到可视化的效果,还可以直接把值标到上面。这样一个图给我们的信息量就非常大,当然在画图的时候我们不是单纯的追求这个信息量很大,我们要美观。要有足够的信息量同时也能够让大家不会一看到之后就不想看了,而是说看到之后能够立刻get到你想要传达一个什么样的想法,这个是很重要的。所以可视化这一点上面说实话我们即便在这个课上有两次课,但是我们只能教大家一些方法,大家最后画图的实际效果要依赖自己的taste,就是自己的一个口味和不断提升的感觉。 我们也可以画地图。我们可以把一些相关的数据在地图上进行映射,随着我们越来越多的能够得到不同地区的数据,把这些数据映射到地图上的时候,我们就会发现很有价值的信息。这个图是我最近画的一个图,就是我们在分析大团队科学中的样本被试到底是不是真的具有代表性(https://doi.org/10.1038/s41562-024-01902-y)。因为很多做这种就是跨国的研究的研究者总是会claim我们的这个研究从几十个国家来的,那么这个数据是能够推广到全人类的。是不是真的如此呢?我们看一下被试在我们这个图当中(的位置),我们就这边是中国的人口的一个(分布)那边就是他的那个被试的群体在不同省份的一个分布图,我们可以看到其实他选取的样本主要就集中在这两个地方,一个是广西一个是上海,其他地方的话其实数据量非常少。 当然我们还可以从其他的维度对他的样本代表性分析,这里主要是展示我们可以把数据映射到地图上面,这样一眼就看到他的数据到底行不行。现在把数据映射到地图上是越来越多的使用在心理学的理念当中。 实用性 R的实用性之一:适用于数据分析的各个阶段。首先,几乎科研每个阶段中涉及到的数据处理,均有对应的R包:计算样本量、读取数据、清理数据、处理缺失值、可视化、统计分析、生成PDF、甚至生成PPT。当然要掌握这里面的每一个流程的话其实是要花很多时间的,但是呢我们可以找到一个对心理学研究者来说最快的(方式)。比方说我们这个课上就会把整个过程中所有心理学的数据整个演示一遍。大家就可以照着这个流程去走,就不需要再去重新探索,这是我们这门课的意义。 此外,R语言还支持将数据处理和分析过程自动化,从而直接生成报告或演示文稿,甚至建个人网站。这意味着从原始数据到最终结果的展示,都可以通过R语言来完成。 R的实用性之二:适应数字化时代的需求。我们现在越来越多的大的数据,所以我们要使用一些更加fashion的一些方法,机器学习、深度学习什么的。那么r语言现在已经有很多这种框架了,如果我们能够掌握r的知识以后我们后面去拓展到这些部分相对来说是容易的。因为里面已经有一些比较成熟的框架。那么就像我们能够调用Tidyverse,我们同样也能够去调用这些机器学习的包,只不过我们要真正的合理的使用还是需要去了解了解背后的一些知识,不能盲目的使用R语言。 R的实用性之三:代码复用。一旦你编写了一段处理数据的代码,你可以轻松地在不同的数据集上重复使用这段代码,只需要对代码进行少量的修改。这大大提高了工作效率,特别是当你需要处理大量数据时。 R的实用性之四:强大的社区、众多的教程。我们选择R而不是Python的原因之一就是,更多的心理学研究者在使用R。我们有一个非常强大的社区。意味着有很多人教你做各种各样的事情,也就是说你如果你想做什么东西,99.9%的情况下你不需要自己去真的去从原理到到实现全部实现全部去做一遍。而是去搜索前人是怎么解决的。比如说你要就要做meta analysis你就搜索meta analysis视频,然后你能得到很多的教程,这时候你就去找一个好的教程就可以了。或者比方说我们要做混合线性模型,你就搜索一下肯定又会得到很多教程。 2.1.3.1 心理学的可重复性危机 在我们心理学领域从2011年开始出现了一个比较大的问题,就是可重复性的问题。大量发表的研究的结果无法被其他的研究者独立重复。那这个问题到底有多严重呢?最有代表性的应该就是这篇文章。在2015年的时候,一篇Science的文章专门报道了整个心理学领域的可重复性的问题。Science是跨领域的多学科的一个综合的期刊,能够发表到Science这个期刊的文章都是能够引起广泛的兴趣的,也是对整个科学界来说都很重要的。在这个文章中当中100个团队重复了2008年发表在心理学顶刊上的100个研究。他们的分析的发现大概只有36%的结果是能够被重复出来的。2015年这个结果是引起了非常大的震撼也被Nature评价为2015年的年度的十大重要的论文之一。因为这个问题出现,研究者就做了很多的反思。当然我也是被这个问题所深深的震撼,现在还是在一直在寻求能够去做到更加严谨、可重复的透明的这种研究。这个是我在2016年的时候跟我们课题组的同学一起写的一篇对可重复性问题的一个介绍和思考,大家有兴趣的话可以看一下。 我这里没有去把这些心理学的计算上可重复的研究拿过来,有人对心理科学在Science这个期刊上面有公开数据的文章进行了计算的可重复性的检验,也就是说按照研究者描述的方法去做一遍分析,看能不能得到跟研究者一模一样的结果。大家猜一下这个比例大概有多高。做一个区间吧30%以下、30%到50%、50%到80%、还是80%到百分之百。约为30%以下的举手,30%-50%呢,50%到80%呢。大家都很乐观啊,80%我就不问了吧。如果说我们考虑完全能够重复的话,他们在14篇文章里面只有1篇能够重复,是1篇还是2篇能够完全重复。然后有的是在作者的协助之下都得不到原来的结果,所以这个问题并没有那么简单。 2.1.3.2 利用R语言增强计算的可重复性 既然有这么大的一个问题,那么为什么说R语言可以帮助我们解决计算的可重复性呢?首先是说可重复性是有多个层面的。大家可以可以想一下,如果说你的这个结果是可以重复的,那么最简单的一个可重复是什么?就是计算的可重复性(Computational reproducibility),这个computation reproducibility说的是什么?假如你有一个数据,然后你做了一套分析,你把它报告出来了,我拿到你这个数据,我按照你描述的方法,我能不能得到跟你一模一样的结果。假如说你的计算的过程当中,没有一些随机的生成的过程,全部都是说我们用的这种可以求到解析解的这种这种算法的话,那就意味着我不仅仅要跟你的结论是一致的,而且是在数值上应该是一模一样的。你原来得到比方说t = 2.1,那我应该也得到就是t=2.1,或者你得到的是F = 10,我应该也得到F = 10,不然就说明什么计算上是不可重复的。 2.1.3.2.1 记录数据分析的全过程 所以我们实际上按照传统的做数据分析的方法,都是用手动点击实现分析的对吧,尤其是前面那一部分。我们不是说你把数据录入到SPSS是以后的那部分,(说的是)比方说你用你在问卷上面,用问卷星收一批问卷,那里面可能有一些不太认真的吧,你要把它给删除掉对吧?有可能你会删除一些你认为是比较极端的也确实可能是极端的数据对吧?有可能你就是100个人里面或者是300个人里面你把某一两个(极端数据)你当时看到你就删除掉了,然后你最后认为你得到了一个干净的数据,你把它存起来以final或者是以最终数据作为后缀对吧,然后你就会基于那个数据把它打到SPSS里面对吧。但是如果说你要重复的话从前面到你那个最终数据你能够(重复出来吗?)可能一个月之后你就不一定记得为什么你删除某个数据了。那么这是很普遍的一个(原因)导致我们最后结果无法得到(重复结果)。如果我们用R语言编程语言来记录数据分析流程的话,就可以把我们整个数据分析的过程全部记录下来,也就说任何一个步骤出错了我们都可以找到,因为我们代码全部在那里。 我们这门课会从原始数据出发,从数据的预处理到后续的统计分析。我们会展示如何把每一个步骤都用代码记录下来,这样一个好处就是即便过了一两年之后,即便我们已完全忘记了当时是怎么处理的,但是代码还是可以告诉我们当时怎么做的,这一点就可以帮助我们去保证计算的可重复性。 2.1.3.2.2 跨机器的一致结果 另外一点,可以帮助我们达到跨系统或者是跨机器的结果。这个其实是在我们心理学的数据当中比如行为学的数据当中是不是很大的问题。为什么呢?因为我们行为数据的处理涉及到的步骤很少,即便里面包括一些随机化的过程,他的错误不会累积和放大。但是如果你们要去处理一些分析流程更长的一些数据,比方说像fMRI的数据,那么你在不同的机器之间的随机性或者浮点数据导致的这个差异,他就会随着你研究的步骤慢慢积累起来,也就是说即便你的这个系统刚开始的时候输了原始数据。经过了不同的系统不同的机器有不同的随机的非常微小的差异,经过一段时间之后也会累积成为很大的一个差异。我们后面会讲如何控制这种随机性导致的这个结果,如果我们使用比较好的使用包括像pandas?或其他的一些软件,我们实际上是能够达到某种程度上跨机器的一致性的。当然到了一个精度非常高的程度的话,其实就不是我们心理学家能够解决的问题,因为他涉及到一些计算机内部如何去控制浮点的精确度等一些技术细节的问题。但是我们可以怎么样呢?当我们学习了这些编程语言之后,我们能够去把计算机科学家在这方面做的改进纳入到我们的分析当中从而去改进我们自己的分析的结果。 那么在这个记录分析的全过程中我自己有一个例子。就是我们在2020年有一篇文章当中公开了数据。但是呢最后其实我们的数据跟统计的结果稍微有一点不一致,有一个读者他读了我们文章之后给我发邮件,我们后来是追溯到原因:之所以出问题就是我们用Excel进行数据预处理,在Excel操作中不小心删除了几行。所以如果我们使用R做全部的数据分析的话,应该就不会出现这个问题,这是说R可以做全过程的一个记录。 那么另外一个例子就是我自己在2020年另外一篇文章当中,因为他的数据和代码全部是公开的,所以有一个叫做reprohack的团队,他们对已经发表文章的结果的可重复性进行评估。他们选择了我的这个数据和代码进行了一次重复。完了之后他们说行为结果绝大部分是能够重复出来的,但是有一部分是没有重复出来,原因是因为他安装不了HDDM软件。这不是一个很小的问题啊,我们后来花了两年的时间去专门把那个软件打了一个docker的包(e.g., dockerHDDM),我们后面会学到的这个docker就是为了去让我们能够保证跨机器的一致性。这个看起来是个很简单的问题,你发了一个文章,然后你做了某一个分析,结果别人连你的软件都装不上。那么我们如何去提高自己的研究结果的可重复性,那当然就是我们把软件让它变得更好安装。那个重复的它是用python写的,不是用r语言写的。他们重复的R语言部分,因为comments都是非常完善,他们能够很好的读到我的注释,他也能够知道我在做什么。大家以后能够比较好的比较规范的写自己的r代码的话,那么同行的反应也是类似的,他会发现你的这个数据,第一个结构非常清晰,第二个你的代码非常清晰易懂,他很快就知道你在干什么,他也能够很快的重复你结果。至少这样的话,你就保证了自己的结果是非常稳定的也是非常靠谱的。 2.2 R语言使用的示例展示 我们已经讲完了第一部分关于为什么要学习的内容,希望大家在听完后仍然能够有学习的动力。接下来,我们将简单展示一些使用R语言的代表性情况。你现在看到的是其中最具代表性的情况之一,即遇到错误的情况。例如,在使用R语言时,实际上碰到错误的概率几乎是百分之百的,而即使你非常熟练,仍然可能出现很低级的错误,比如漏掉一个字符或者反引号。我们将在之后讲到数据类型时介绍不同数据类型需要对应的一些符号。 2.2.1 数据清洗 在数据清洗方面,我们一般会使用dplyr, 它是Data science里面非常常用的一个包,需要进行各种数据转换、分组等等操作。但是数据清洗通常是数据分析中耗时最长的一个过程,即使是简单的数据也需要花费相对较长的时间进行清洗。虽然在使用SPSS的过程中,我们已经形成了一个非常快速的数据分析思维,但是在使用计算机语言进行数据分析时,这个过程完全不同,需要花费大量时间进行数据清洗。即使是行为学数据,甚至是简单的反应时数据,也需要进行相对较长时间的数据清洗。 这里面可能有几个图没有截过来,在数据科学中,无论你从事哪个领域,完成一个数据分析项目的时间通常会包括从数据清洗到最终分析以及报告撰写。其中,数据清洗通常会占用至少60%的时间,这个过程可能需要反复查看和修改。传统的做研究的方式并不习惯分享数据,因为整理和清洗数据需要很长时间。即使你的文章已经发表,清洗数据所花费的时间也可能会被认为是浪费的,尽管有时会带来间接的回报,如其他人可能会重新使用你的数据或发现你的分析的可重复性很高。 2.2.2 ggplot2画图 另一个耗时的任务是画图,尽管这些图看起来很漂亮,但需要不断地调整和修改。有时,研究者会花很长时间去完美地绘制图表,而这些时间可以用来完成其他任务。经常会有研究在这个社交媒体上,他自己比方说做统计分析发了一分钟,然后画图画了两天的时间,就是不断的去调,并不是他不会画图,而是他总是觉得画出来这个图不满意,然后就不断的调整,不断调整最后发现,时间就没了,并且呢你也会发现呢,就是当你掌握了不同的这个,画图的这个方法以后呢你会不断的去想,我能不能找到一个更合适的方法,去对他进行更好的一个可视化,我没有来得及把我自己之前的一个画的图的这个历程贴上去,我刚开始就是最简单的这个,跟我们在,Excel里面画的那个图是一样的,就是一个直方图上面加一个error bar,这是我一开始用来画的用APA的格式,后来就变成那个带散点的,再后来变成了那个raincloud,然后就是raincloud加box,然后加这个distribution,然后最后又回到了这个散点的,加上这个主题,就是groups这些。所以其实这个画图的时间需要大家当你熟练了以后也需要适可而止,差不多能够传达你的这个信息就可以停止了,要不然的话,这个画图的提高是没有止境的,有的也可能可以看有的时候也看到,就比方说一些好的期刊,比如像经济学人他们涉及到数据的时候,像比方说, 我们说Nature、Science或PNAS对吧,Nature Communications,当他们涉及到数据的图的时候,都是非常漂亮的,包括他们配色包括的比例各方面,他实际上都是经过专业的人士进行调试的。那么有的时候其实我们要想要达到这个类似的效果的话,也需要花很长的时间去做这些细节的工作。但假如我们只是按照心理学传统的做法,Excel的那种一个bar图加一个error bar的那种非常快基本上几行代码就可以搞定。 2.2.3 心理学数据分析与结果汇报 针对于各种各样学科、心理学的这个数据的分析的包,也有心理学的研究者开发的包,例如包寒吴霜老师的BruceR包,该包专门针对心理学数据分析,并包含很多有用的功能,非常适合心理学学生使用。例如,对于做T检验的研究,可以使用该包的功能,我们可以用他的这个T-test对吧,它可以将结果输出成为一个简单的三线表格,并且可以直接在命令中加入指令将结果输出成为一个Word文档。这个word文档里面就有这么一个表。你就可以直接复制打开到你的文档里面,这个是非简单的。他也告诉你,你的零假设是什么,我们这个是单样本的t检验,那么他告诉你这个假设就是双侧的,然后这个叫做均值,他不等于700,那么类似的可能我们还有其他的,比方说我们用这种配对样本t检验对吧,他同样的也可以得到这种非常适合我们输出的这个结果,而且他最近把Bayes factor打包进去了,所以我们大家可以看到这里报告的信息肯定是比SPSS更加全面的。 2.2.4 Regression 那么另外就是关于这个regression这里面我们其实没有使用BruceR,如果我们使用BruceR的话,他有一些专门的回归模型,然后我们这里有个简单的回归模型,把回归模型直接做一个这样的输出,BruceR也是可以的。还有这种,这里应该也是一个简单的回归模型,还有就是我们也可以使用这个SEM,这个地方的话,我们可能要使用一个新的包,就不是BruceR,当然bruceR他也把SPSS的process插件整合进去了,这里我还没有尝试过,可能我们后面可以一起来探索。 2.3 现场运行代码 在今天的课程中,我们向大家展示了一段R语言代码。我之前在公众号或群组中提到过,所有的课件都会放在github同一个地方,供大家查看。比如,我最近在一个小时前更新的内容,包括第一节课的PPT。我通常会在上课前更新课件,以确保大家能够获取最新的信息。 去年的课程PPT,也就是2023年的课堂PPT,已经不再使用。虽然这些代码和PPT可能仍然有用,但与我们的课程配套的内容是我们当前每次课的课件,大家可以自己下载最新的课件。我刚才展示了一个简单的rmd,让大家看到如何进行简单的展示。如果你已经下载了,可以直接打开并查看。 我使用的电脑是我的助教提供的。在课程开始前,很多人还没有安装课件和代码,但我通过一个工具,将代码和课件下载到另一台电脑上,并成功打开。通过这个工具,我们可以看到各种信息,包括代码和文字信息。这些信息和代码可以混合编写,最终可以生成一个可以直接在电脑上运行的演示文稿。 在运行中,可能会遇到一些问题,比如某些包未安装。需要注意的是,R中的很多包看似独立,但实际上是基于其他包构建的。这意味着你需要安装所有依赖的包才能正常使用。特定的R包甚至可以帮助我们按照特定的排版格式生成符合学术标准的论文排版,符合一些学术杂志的要求。这个工具可以处理很多细节,比如作者信息、摘要、关键词、材料和方法等部分。你可以编写所有的细节,包括实验设计和数据处理过程。通过这种方式,我们可以确保我们的文档既符合学术标准,又能以一种清晰、易于理解的方式展示我们的研究内容和数据分析过程。 2.4 课程安排 我们这学期的课程的有三个原则:第一个就是即学即用,我希望我们在课堂上教的这些代码,大家都能够在自己的数据分析中使用而不是我们在这里学了一遍之后,自己还需要重新去学,或者这里面学的代码,都是大家用不上的,这个我们尽量避免。因为大家时间都很有限,如果能够帮助大家省一点时间的话我觉得或者少走一点弯路的话,减轻大家的心理负担。 第二个就是在做的过程中学习,我们会教大家怎么去安装,然后去了解这个里面的各种各样的功能,以及数据导入,以及各种各样的情况,基本上第二节课以后,我们就开始就直接就给大家讲这个代码。我们需要在做的过程中学习。 然后第三个就是逆向学习,逆向学习就是你先做,你先能够在哪里面实现这个东西,然后我在这个我演示的过程中,我用一个命令,比方说就是t-test或者F-test对吧,我能够得到这样的结果,大家首先说的就是你在你自己电脑上面,或者说你在这个云计算平台上面你能够实现这个功能,你能把这个代码抄下来,然后得到跟我一模一样的结果。然后你后面再去理解它,你先会做然后再去理解,这个是对于学习代码来说,我觉得是很好的一个做法。因为如果你看到一两本书之后,却没有写过几行代码,这个实际上是非常的浪费时间的一个事情,尤其现在大家的这个时间也比较紧张,我们的一个目标就是想要去压平这个学习曲线。因为如果大家知道学习,就是说以前大家认为这个R语言的学习曲线是非常陡峭的,就刚开始特别难,你要进步的话很慢,就是很难,也花很多时间。我们希望他尽量的快一点,然后可以慢慢的去后面的不断的学习。 那么参考教程的话,英文版的有这个(Naverro, Learning statistics with R: A tutorial for psychology students and other beginners. (Version0.6.1),https://learningstatisticswithr-bookdown.netlify.app),然后中文版的话,有一个叫做王敏杰老师的《数据科学中的r语言》,他实际上是一个公开的一个教程,那么大家可以把它当做参考书,因为这个书里面讲到了非常多的一些知识点。但是我们不会按照他的这个知识点进行讲解。然后另外一个叫做Tidyverse,张敬信老师的书(https://www.epubit.com/bookDetails?id=UB7db2c0db9f537),这个我最近也买了。那他实际上跟我们的课堂的这个内容契合度是非常高的,因为Tidyverse就是我们最常使用的一个工具包,那么课程的安排的话我们就是,我们课程的内容的话其实就基本上跟我们的研究密切相关,因为大家都是研究生对吧,所以我们是希望:首先是跟数据分析这部分密切相关,然后等我把这部分解决之后,我们看到后面进阶的话,在设计实验的时候怎么把这个部分做好,那么在数据分析的过程中的话,我们就会有一个完整的流程:我们从原始数据到清理,然后到数据的探索,统一分析,然后分析完了之后统一推断,然后把它结果去验证,然后撰写一个报告。 我自己本人也非常喜欢可重复性和开放科学,因为我觉得它是科研中很重要的一个方面,我们后面也整合了一些有关我们如何跟他人进行协作的内容,帮助他人一起共同的合作来完成任务。还有如何保证我们的可重复性,如何采用一些更加先进的计算机技术,来帮助我们更好的保证我们的这个计算的可重复性。然后以及如何直接能够从代码到数据,生成一个PDF文件或者word文档,能够生成一个直接可以提交的一个版本。 2.4.1 课程大纲 我们不会按照传统的这种方法介绍R:先介绍R里面有什么数据,有什么对象,有什么语法规则,这些通通都不介绍。就是直接数据拿过来,我们要怎么用,怎么分析,第一步做什么。我们从第二章开始,就是安装,假如我们现在有了一个数据,我们需要用r语言,我们第一个问题就是把r安装到自己的电脑上面去,如果使用的是windows系统的话,最好把自己的用户名改成英文或者是拼音,不要用中文作为用户名,因为R语言它是英语的使用者开发的,所以说他的这个编码可能会对中文不是很友好。我们之前碰到过一个问题,就是当我们使用中文作用户名的时候,可能没有办法画图。 然后我们会后面会帮助大家解决一些安装中的问题,我们会介绍安装之后的各个界面,我们也会介绍如何更加方便的使用r,如果我们是使用原生态的r的话会非常的难用,相当于只是给我们提供了一个引擎就是做R计算的一个引擎。我们还要需要有一个写代码的一个界面,更加方便我们进行交互,那么我们会使用Rstudio,那目前的话,Rstudio应该是使用最广泛的,当然越来越多的人也使用Microsoft的那个VS code,但是我们使用R语言,其实也是挺方便的。 然后我们介绍各个界面以及如何开始我们的数据分析。我们现在拿到一个数据之后,首先要把这个数据导入进去,那么在SPSS里面大家知道,就是一个File导入,那么在里面呢我们除了这种方式以外,我们有其他的方式,我们也会进行批量的导入,我们不仅要导入一个数据,我们可能需要导入多个数据,导入完数据之后呢我们就有东西了。 我们的数据进入到里面了,那么我们从现在就开始认识R里面的这些数据,就是R里面的这个对象,然后我们会以这个为基础来讲解R里面的各个对象有什么特点。 首先我们要讲解数据和处理,因为这个过程有点复杂,所以我们会分成两次来讲解。我们会首先讲解单个对象在R中如何操作,一个一个的我们在Rstuido中都是可以看到的,然后讲解一些运算规则。这时候,我们就把R的一些基础知识放在这里了。然后我们讲解每个对象的特点之后,会展开介绍一些函数和规则等等。然后我们就可以开始自己的处理工作了。我们会按照tidyverse的风格进行基本的操作,或者我们叫做比较管道的操作。在这个过程中,我们可能需要给大家演示一下。如果大家能理解了,我们就可以进入下一个阶段。但是,如果有些同学对编程没有基础的话,第四章和第五章可能会比较难理解。所以我们会多停留一段时间,确保大家能够理解。 然后我们已经导入了数据,接下来就可以进行一些探索。看一下我们数据长什么样,他有什么样的一个模式,特点,我们应该对他进行什么样的一些分析,这就是这就是去了解我们的数据。在传统的心理统计学和SPSS中,不知道老师有没有要求大家查看原始数据。实际上在我们用做数据分析的时候,我们一定要去看,并且是在开始的时多的去看原始数据他到底是什么样子。我们不能只看统计指标,我们一定要看数据他到底是什么样子,这样的话就避免让我们发生一些非常非常基本的错误。 那么这个数据探索的话其实他包括两个部分。一个部分就是进行描述。第二个部分就是进行一些基础的一些可视化,所以探索数据部分其实已经包含了我们两个部分的一个知识点:一个是描述统计,另一部分就是最粗糙的可视化。因为这个时候,我们只需要自己看就行了,我们不需要给别人看,我们也不需要去把它做的非常精美,所以这个时候是最基础的这个可视化,然后的话我们就会接下来几章我们就会告诉大家如何用r语言实现大家常用的一些统计分析,因为这个可能是我们心理学常用的。 所以我们先展示一下,然后他的这个分析的流程是什么样的。这几个部分呢,其实应该是可以并行展开的,但是我们没法进行并行展开,所以我们只能依次的介绍。 然后呢,介绍完了之后呢我们会介绍一个目前来说在国际心理学界比较流行的一个做法,或者说大家在推荐的一个做法,就是看我们的这个结果是不是稳健的,那么这个时候比如说我们同样一批数据,我们采用多个方法来分析它,最后我们得到的结论是不是一致的,这个称之为Multivese。在心理科学进展上最近有一个文章,介绍这个Multivese的。大家有兴趣话可以去看一下。 那么到这个时候的话,其实我们基本上统计分析就基本上,如果我们只做传统的这个数据分析的话,我们就其实就已经做完了。那么我们接下来就是,我们汇报结果,这个时候我们如何得到一个可发表的图像,或者可发表的是一个插图,那么这个时候我们会进一步的讲我们如何进行拼图,如何操作每一个每一个这个元素然后,如何把多个图拼到一起,让他更加的美观,以及他的比例等等。然后呢我们会讲一下,这个叫做文学编程literate programming就是,实际上他就是把我们前面讲到——就是在这一章以前,我们讲代码,所有的都是直接代码,那么我们在这个地方的话我们就开始介绍如何把r代码和文字进行混合,也就是说我们同一个文档里面既有文字描述也有代码,这个代码还是可以运行的对吧,他生成的图片还可以直接插到这个文档里面。 我们就要介绍这个Rmarkdown,它是用LaTex的语法,所以我们会介绍一些最基本的LaTex的语法,帮助大家进行排版,还有公式的撰写等等,然后到这里的话基本上就是我们主流的部分,一个人干活的话基本上就差不多了。但是我们知道,现在的已经都不是一个人干活对吧?所以我们要经常跟他人进行协作,所以我们后面介绍就是如何跟合作者或者导师进行协作那么这个地方就涉及到两个,一个是版本控制——其实一个人干活的话你版本控制也很重要,为什么呢?因为你可能前后代码有很多迭代对吧,你有的时候可能删除一个功能,但是你后来发现,这个删除功能它是有用的,但是如果你直接删除的话,你就找不到了,那么如何我们能够找到以前的版本,这个其实是,当大家写代码越来越多的时候很重要的,另外一个就是多人协作对吧,通过这个Github来进行多个人进行同时,完成一个数据分析,你完成这个t-test,我完成F-test,最后我们两个人,进行一起合并,这样的话我们能够更加快速,更加有效的进行协调。 然后,我们相当于是一个研究做完了,可以计划下一个研究,这里我们会介绍一些心理学常用的方法,包括meta-analysis元分析。元分析实际上就是我们把多个研究的这个效应量进行综合,综合起来之后我们就有一个用来估计样本量的一个东西,那么我们接下来就是做这个power-analysis。以及我们如何在没有任何数据的情况之下,我们就可以把自己的分析数据的代码先写出来,那么这就涉及到这个假的数据对吧,fake data或者叫做simulated data。 如果我们涉及到这个数据非常复杂的话,我们可能会考虑如何进行变形处理。就是说我们的数据量比较大的话,如果我们做模拟的话有可能他要花很长的时间。但是我们的CPU有多个核对吧,我们是不是能够把多个核都用上来,让他更加快速有效的进行这个模拟,当然这个可能是我们看情况啊,如果说有时间的话,最后大家自己去搜索就可以了,因为对于传统的行为学的这个研究来说,除非我们用贝叶斯的这个混合线性模型,要不然的话基本上都是在可预期的时间内能够完成的。 最后的话,我们基本上完成了整个分析。我们全部做完了对吧。假如我毕业了,那么师弟师妹他们如何来重复我的研究?或者导师如何能够重复我的研究?那我可能过了一段时间之后,这个版本有不断的更新的,那么别人如何能够还能够重复我的研究?这个时候我们如何把这个版本,这个包给记录下来发布?现在比较主流的这种计算机的方法就是容器技术。用这个帮助我们来更好的达到这个computational reproducibility,当然这个地方我可能只会做点介绍,大家如果真的要去完成这一点的话,可能也需要花点时间去琢磨。因为R语言他有的一些包可能会涉及到比较底层系统的交互,如果你使用的包是比较主流的,比较常规的,那可能,你把它打到docker里面是很容易的,如果你涉及到的包可能是比较新的,有可能会它需要一些编译的工作。 所以如果我们再回过头看一下,就是说我们整个教学大纲就是按照我们一个研究生,拿到数据之后会做什么一步一步的往下走,所以我们不会很系统的去介绍R语言里的知识,我们就是碰到了什么我们就讲什么,那么我们会用什么数据呢?我们的数据就是我之前的公开数据。这里面包括了2019年在scientific data发表了一个数据,那么它的数据呢以问卷为主,那么还有原研究,我们后面会把这个发给大家。另外一个是实验数据,也是我2020年发表的一篇文章,我们就会从这些数据开始,然后一步一步的进行处理。 那么当然如果大家说想就在这个学习的过程中,顺便把自己数据给处理了,可不可以呢?当然是可以的,你可以尝试,然后你碰到什么错误。如果说需要问的话呢,也可以问,但是不能太超纲了。 我们这里面大家可以注意到一点,就是我们对统计方法的讲解没有涉及到很深,因为我们是R语言课对吧,所以是以R语言本身的操作为主,就是比方说大家这个里面设计的很复杂,我有一个链式中介模型要试试,或者什么调节模型。如果说你碰到那种技术性的问题,我们可能不会回答因为这个完全超纲了,但你说我碰到了一个数据导入的问题,这个没有问题,我们可以解答。因为现在是说一个是知识一个是操作,我们教的主要是操作,因为实际上如果你懂了,这种比较复杂的SEM的知识的话呢,你要操作起来很简单,可能就一两行代码就能够解决了或者复杂一点就是十几行代码就能够解决了,但是,如何设置这个模型,如何解读模型的输出,这个不属于我们R课的内容,而是属于你的统计知识的内容,大家清楚了吗?所以我们这个课,你可以把自己数据拿过来,但是如果你的数据非常specific分析的话,我们可能不在我们这个课程所覆盖的范围之内,但是可以帮助大家去把前面这个部分解决。你可能原来完全不知道怎么使用R,那么我们现在就教你怎么把这个数据导入怎么使用,做基本的数据分析。 那么到后面,你可能很容易就找到适合你的这个数据分析的工具包,那么你需要去阅读这个工具包相关的知识点,去把它用于自己的数据。有时候我们会以这个原始数据——两个原始数据,一个是问卷数据,一个是反应时的数据为基础,然后一步一步的去走完整的过程,那么中间大家可能会有一些要抄写代码的地方,这个是就是大家可以在课堂上实现。那么我们可能会需要,为了保证大家能够不断的进行这个讨论和反馈,我们需要分组。那我们可以加到群里面,然后后面需要进行分组,大家小组长最好能够负责带一些插线板之类的,这样的话就保证每个人都能够有电,在上课的时候有电来进行后续的操作。 我的想法就是最开始的时候我们就分组,那么大家可以比方说根据自己的兴趣,或者是我没有兴趣的话,我们可以由助教来进行随机的分组,那么分组之后呢大家比方说碰到了问题大家相互讨论,我不知道大家以前在做数据分析的时候是不是相互讨论,但是在写代码的时候,相互讨论是一个非常有用的一个东西,为什么呢?可能你写了一段代码之后你犯了一个非常小的错误,然后导致你的代码没办法运行,你自己看不出来为什么,因为你太熟悉了这个代码,但是别人来看的话可能一眼就看出来了。 所以呢大家可能就可以形成小组,在上课的时候练习的阶段,大家就可以相互来进行这个相互进行检查,同时呢我们也有几位助教,我们分组讨论的时候呢,助教就会跟我们一起来解决。 我们会后面会有一个我们课程的PPT,完成之后会把它上传到这个Github,为什么我有这么多助教呢?有一部分助教是会把我们上课的录音进行一个,就讲的这个东西进行一个文字的整理,然后让他更加的符合逻辑,把它变成文字稿。 有些同学或者老师,特别想学,但是好像没有时间过来,你觉得参加小组讨论也太麻烦了,后面就可以看这个文字稿。或者这个书也可以电子书也也是可以的。这大概就是我们这个课程的安排。 这个课程的整个大纲啊,大家跟我就一起体验。如果大家感觉很很糟糕的话,也可以继续跟我反馈,我后面在不断的改进。我们每年的结构都是有调整的。 我们每一节课希望能够解决一个问题,那么在这个解决问题中的话,有的时候我PPT讲的内容不一定能够完全解决你的问题,因为你有可能在你的电脑上去解决这个问题的过程中你碰到了新的问题,那么这个时候我们有很重要就是小组讨论。我和助教来一起帮助大家解答疑惑,这个基本上就是我们这个课的安排。 2.4.2 成绩分配 那么选课的同学,可能就会涉及到这个成绩的问题了。首先是出勤,只要大家来的话,就有10%的分数;第二个的话就是3个小作业,是单人任务,占比15%*3;第三个是一次大作业,分组完成,占比45%。大作业要求学生找到一篇顶级期刊上发表的文章,该文章应公开数据,最好是原始数据,然后按照文章中描述的方法使用R语言进行数据分析,并制作一个报告。对于线上学习的学生,如果对大作业感兴趣,也可以尝试。我们会通过在线平台进行评估和反馈。 在课程中,我们还会教授如何使用云平台(和鲸)来提交作业。这样,我们就可以直接从平台上运行学生的作业,避免了下载和路径问题。对于线上和线下学生来说,使用云平台可以更方便地提交和评估作业。 2.5 如何学好这门课 最关键的就是不要害怕这个课程,这课程其实没有大家想象的那么难,为什么呢?因为我们不需要成为r开发者,我们也不需要懂很多很多r里面代码,我们只需要成为一个合格的调包侠就可以了。别人开发的包我们能够合理的使用,这就是我们这门课的目的,大家如果说有志于要成为大神,比方说我要开发某一个包,或者是我要后面所有的东西都用R解决,包括给自己建一个网站什么的,这个东西不在我们的这个课程范围之内,也说大家能够用r语言,第一个就是消除r语言的畏难心理。能够用r的这个生态里面的一些包帮助大家解决问题,这就是我们这门课要达到的一个,让大家入门的一个目标那么当然我们也希望通过这个入门让更多的人,能够成为长期的这个R语言的使用者,能够长期的在这个community里面活跃。甚至有一天能够为他人答疑解惑,或者是明年的时候来给我当助教。 第二个就是敢于尝试,一个就是说大家一定要去不断的犯错,另外一个就是说你可以借助一些比较新的一些东西来去帮助你解决这个问题,我们有些助教就是,非常熟练的使用ChatGPT。假如你没办法使用ChatGpt,是不是还可以使用Bing的相关功能?它也非常的强大。我们最好是用英文进行搜索,因为相对而言,英文的这个社区要比中文的社区要强大很多,把你的问题用英文描述出来,然后在Bing里面搜索,99.9%的这个情况之下你都能够找到答案,如果你的问题描述是正确的话。你也可以很简单就把那个报错最关键的地方放到这个搜索框里面,基本上你也能够找到答案。R语言他再怎么统计,他也本质上也是一门计算机的编程语所以他还是有编程的这些成分在里面,那么编程他最大的一个特点就是你要需要去勇敢的尝试,不断的犯错,犯的错越多的话,学习的越多,尤其在课上。假如说你犯了200个错误300个错误,那么你后面自己分析数据的时候可能还是会碰到这些错误,但你就知道怎么解决了。所以只有多犯错才能多学习。 我们需要以计算机的这种思维方式思考,就是计算机他是非常非常机械的,你告诉他什么,你输入什么指令,他就给你什么结果。所以如果出了错误,一定是我们的指令或者是哪个地方出错了,所以很多时候你需要把这个逻辑想清楚。特别是你有一个比较复杂的分析问题,你要想我第一步做什么,第二步做什么,第一步的这个输入变量是什么,输出变量是什么,这个输出的变量,他能不能进入到下一步作为输入。简单的这种机械的操作的思考,能够帮助我们去使用这个R语言。 第三个当然就是我们在小组的讨论中一定要相互的帮助,我有一些朋友,他是学习计算机本科的,他们的快乐之一就是上课的时候,相互debug,相互帮助,因为写代码写多了之后你会发现他是一个非常有及时反馈的一个事情,你输入一行代码,他给你一个正确反应非常开心,输一行代码错了,能够解决了也非常开心。但有一种情况对于初学者来说,就是你犯了一个错误,结果你两三天解决不了,就非常的头疼。那这种情况的话,如果是有人能够帮你的话呢,就能够其实极大的促进正反馈。还有一个就是,遇到比较复杂的代码的话他确实有可能起作用了,但是你可能也不知道为什么,他不起作用你也不知道为什么,但是我们需要尽量减少这种情况。 在学习R语言的过程中,我们鼓励大家即学即用,将学到的知识和技能应用到自己的研究中。这不仅有助于巩固学习成果,还能够提高数据分析的效率和准确性。对于那些刚开始接触R语言的同学,可能会觉得很难。但是,通过不断的练习和使用,你会逐渐熟悉R语言的语法和结构,从而能够更加高效地处理数据。 此外,我们鼓励大家在使用R语言时,不仅要学会如何使用各种函数和包,还要理解这些函数和包背后的原理。这样,你不仅能够解决问题,还能够更好地理解数据分析的过程和方法。 2.6 课程总结与期望 总的来说,R语言是一个强大的工具,它可以帮助我们更有效地进行数据处理和分析。通过学习和使用R语言,我们可以提高自己的数据分析能力,更好地服务于我们的研究和工作。 学习R语言并不是一件可怕的事情。每个人都能学会,只要你有足够的练习和尝试。在学习过程中,你可能会遇到一些挑战,比如数据预处理和统计思维的培养,但这些都可以通过持续的训练和实践来克服。 我们这门课的最主要意义在于让初学者从完全不会到能够不害怕使用R。在心理学研究当中R语言也是慢慢地变得越来越流行,一些比较新的期刊会发表很多教程性的文章,就是专门教大家如何去使用各种各样的新的方法。其中有多篇关于R语言的使用的。 数据分析是一个需要长期训练和实践的过程。我们需要学会用计算机的方式去思考问题,这需要我们有很强的逻辑思维能力。同时,我们也需要相互帮助和支持,共同进步。 2.7 推荐 公众号:统计之都,它提供了很多关于统计学的科普内容和思维方式,这对于数据分析的学习非常有帮助。 书籍:statistical rethinking "],["第二讲如何开始使用r.html", "Chapter 3 第二讲:如何开始使用R 3.1 数据分析的出发点——问题 3.2 如何安装R 3.3 如何安装Rstudio 3.4 如何通过和鲸(Model Whale)使用R", " Chapter 3 第二讲:如何开始使用R 大家好,下午好晚上好。现在我们开始第二节课。看起来上一节课还是起到了一些效果,人数减少了一些。我觉得这很好,这说明大家开始明白自己真正想要学习的是什么,这是一个非常好的开始。无论你们学得怎么样,最重要的是首先要知道自己是否真的想要这个。上一节课我们作为科普,讲解了为什么要学习,这是我们的核心目的。我们希望从零基础开始,让大家从完全不知道的状态,通过我们的课程开始了解并逐渐使用。今天这节课我们将从最基础的内容开始,讲解Rstudio的安装。我们之前已经把PPT发到群里了,所以大家可能都已经下载了。但今天我们还会简单地过一遍R的安装步骤和可能遇到的一些问题。那么,我们今天就开始使用它吧。 3.1 数据分析的出发点——问题 虽然我们这是一门介绍R语言的课程,但我们不能忘记我们为什么要学习数据分析。对吧,上次也有同学和老师在关注统计之都的公众号时,看到了他们最近发布的一篇关于统计学未来的文章,讨论了在人工智能发展的背景下,统计学应该如何发展。我们学习的心理统计学或者数据分析,都是与统计学密切相关的问题。我们的应用也是心理学的问题,在当今这个时代,心理学和数据分析应该如何教授和学习,这是一个值得思考的问题。 最近,我注意到越来越多的数据分析师、统计学家,甚至是各个领域的专家,都开始强调一个观点:做数据分析不仅仅是为了完成统计分析,它实际上是一个完整的工作过程。这个过程的起点是我们有一个问题,我们为什么要去分析数据。在我们的科研过程中,我们通常是因为要通过统计分析来帮助检验假设、估计我们感兴趣的效应量,或者探测某个效应是否存在。在心理学研究中,我们大部分情况下都在进行假设检验,我们都在使用p值。但最近有些反思认为,我们可能过于依赖假设检验,我们应该去做一些其他的事情,比如预测或者估计的工作。 在其他领域,比如医学,医学统计学也是非常重要的方法学部分。在医学中,更注重的是治疗效果本身量的大小,比如新的治疗方案是否有效,效果有多大,在多少人身上有效,以及它的成本和效益比是多少,这些是一个非常实用的、医学研究中需要关注的问题。与我们心理学非常接近的教育学,在进行干预时也需要关注效应量。例如,当我们提出一个新的教育干预手段或者一个新的教学方法的时候,我们需要评估它是否真的能提高学生的学习能力。我们最终关注的也是提升的效应量的大小,实际上就是一个估计的问题。有些同学或老师可能将来会进入企业工作,在那里,数据分析将用于帮助上司或公司做出决策,在企业中,数据分析通常用于解决决策性的问题。 因此,我们进行数据分析的出发点或起点,通常是我们面临的问题,无论是研究问题、实践问题,还是辅助决策的问题。由于我们是在心理学院,我们遇到的问题大多数是传统的问题。在整个课程中,我们会在课堂上使用两种常见的数据类型,这是心理学中经常遇到的两种类型:一种是问卷数据,另一种是在进行认知实验时收集的反应时间和反应选择的数据。 3.1.1 问题一: 人类企鹅计划(问卷数据) 问卷数据采用的是我之前参与过的一个项目中的数据。我上节课也跟大家提到过,是Hans IJzerman在2018年主持了一个大规模的合作项目。他研究的问题是关于人类的社交网络关系是否与我们的身体体温调节有关。下面这是这篇文章的信息: (参考文献:IJzerman, H., et al. (2018). The Human Penguin Project: Climate, Social Integration, and Core Body Temperature. Collabra: Psychology, 4(1): 37. DOI: https://doi.org/10.1525/collabra.165) 下面是这篇文章的数据。一般来说,我们做问卷调查都是在网上进行的,现在已经很少有人会拿着一堆问卷一个个发放了。基本上,我们都是在网络上收集问卷,然后导出的格式通常是CSV或者SAS格式。CSV是全文本格式,使用逗号分隔,而SAS是SAS格式的数据。所以我们可能会拿到这样的数据。 这两个数据我们后面都可以在课程资源里找到,我们课程的git hub上也直接放上去了。这个数据集最初是通过Qualtrics在线收集,现在已经公开可以使用(Hu et al., 2019, sci data)。 最后我们拿到了1,523条数据,这些数据可能会形成我们心理学常用的数据格式——每一行代表一个被试的数据,每一列代表这个被试在不同变量上的信息。比如说,我们有1,523个被试的数据,最后我们加上开头(title)、列的名字、或者变量名,就会有1,524行,最后形成一个很大的CSV文件。我们现在需要以它这个数据作为输入,然后去检验我们的假设,或者探讨我们想要探索的关系,就是人的社交网络与身体体温之间的关系。 这就涉及到很多变量,这里也会涉及到我们要用什么样的方法去探测。与机器学习相比,哦我们的变量谈不上大量,那么,在变量比较多的情况下,如何能够探测到或找到一个比较稳定的关系,可以作为我们学习的问题之一。 那么当我们拿到这个数据之后,我们可能还要经过一系列的预处理,我们要把它导入到R中进行清洗,把它变成干净的数据。当我们在做问卷的时候,通常拿到的分数实际上是对应于问卷条目上的一个得分。为了得到一个变量,或者说被试在某种心理变量上的得分,我们通常需要将这些得分转换成问卷得分,而问卷得分的计算方式可能有多种,最简单的方法就是计算总分或者平均分。我们是否能够在R中实现所有这些工作,如果可以的话,那就意味着我们可以在R中完成我们的所有分析工作。 在Hans(2018)的这篇论文中,作者构建了一个中介模型,研究了所处地理位置与赤道距离对体温的影响。这个距离代表了与赤道的距离,一般来说,你离赤道越远,你的地方的温度可能会有更大的变化,比如在冬季更冷,夏季更热。如果你的位置更靠近北方,比如北欧,那么全年温度通常都会比较低。物理环境的温度肯定会影响你的核心体温,也就是core body temperature(CBT)。研究发现,物理距离,或者说你所处环境的温度对身体温度的影响,实际上受到社会关系的调节。如果你的社交网络很强大、有多样的社交网络时,即使你处在寒冷的北方,你的核心温度也会很高。这说明社交网络在调节体温方面起到了作用,验证了研究开始的假设。 我们在课程中是否能够从原始数据中读取并解决这个问题,这也是我们课程想要实现的目标。 如果我们把这个流程看作一个整体,要解决这个问题,我们可能需要经过多个步骤。首先,我们需要把数据读取到R中,然后对数据进行清理。比如,去除那些只填写了一半的问卷。数据清理干净后,我们可能需要进行进一步的整理工作。比如,我们可能需要将每个被试的单一得分转换成变量的得分。在这个过程中,我们可能还需要查看描述性统计的一些统计值,比如问卷的信效度,特别是内部一致性指标,如Cronbach’s alpha系数。我们还需要检查问卷的结构效度,即使我们使用的问卷是比较成熟的,也有可能在这批被试上不适用。因此,我们也需要对问卷的结构进行检验。最近的一些心理学家提出了比较严谨的说法,我们在使用任何一个工具的时候,都需要报告它的信度、效度,以此来看测量工具合不合理。这就意味着我们要去对问卷本身的这个心理学的心理测量的特性进行报告,此外,一般还需要描述一些特定的统计数据指标,比如被试的平均年龄、年龄范围、男女比例等。问题1的解决流程思路图如下: 这样的话,我们可能需要做一些描述性统计。有时候,我们甚至需要进行数据可视化,帮助我们更清楚地看到数据的模式是什么。这就涉及到了探索性数据分析,我们通过可视化,通过各种各样的图表,让我们了解到底哪些变量之间存在关系,哪些变量可能不太可能有关系。这个时候,我们其实就已经开始涉及到数据的可视化了。因为这个研究本身就有一定的探索性,最后我们可能需要通过一些机器学习的方法来探索和找到变量之间的内部关系。 比如说,假如我们要完全按照Hans的研究当中的做法,我们去探索哪些变量与我们的体温有密切的关系。这个时候,因为你设计了探索性分析,就可能有假阳性的存在。为了避免假阳性的存在,他当时采取了一个在机器学习中常用的做法,就是把数据分成两半,用一半的数据来探索变量之间是否有关系,然后在另一半的数据中进行验证。我甚至可以不断地重复这个过程,这就是在机器学习中称为N-Fold Cross,即N折交叉验证。这个过程就会涉及到我们需要对数据进行随机的取样,将它们进行分组,然后把我们数据做一遍,再重复。这里面就会涉及到很多这种需要重复的、批量处理的一些工作。如果你手工做的话,就会非常的繁琐。 假如说你最后探索出了一个比较稳定的关系,而且在另一半的数据当中也能够得到充分的验证,你这个时候需要对这个结果进行报告。你需要对那些最能反映你结论的一些数据进行可视化,需要报告关键的统计指标,以及对这个结果的一些比较美观的可视化。所以我们可以认为,在这个过程中,可视化是一个操作,至少在两个步骤中涉及到:一个是探索,另外一个就是报告结果。这就是我们在这个简单的问题中可能会涉及到的流程。 当我们完成结果报告后,基本上就可以将其放入我们的文章中。当我们把文章整理好并提交出去时,我们的数据分析工作实际上就已经完成了。因此,从数据的导入到最后的可视化报告,我们基本上可以在R中完成整个过程,不需要借助第三方的工具。这是一个常见的问卷数据的分析。 3.1.2 问题二:认知决策任务(反应时和反应选择数据) 另一方面,我们还有行为任务的数据。通常,每个被试在完成实验后都会有一个自己的数据文件,比如E-Prime,Matlab,Psychopy或其他软件记录的数据,你需要做的是将这些结果文件合并。同样地,需要进行预处理。比如,在2020年的一个工作中,我们进行了一个简单的认知实验(下图是数据的形式)。研究假设是人们是否对自己相关的事情有更快、更优先的处理。心理学中已经有大量研究发现了这一点,从上世纪七十年代的研究就已经表明,如果将外界刺激与个人相关联,对这个刺激的记忆效果会比深度的记忆加工或加工他的物理知识等其他编码方式更好。之后也有大量的研究发现,如果你把一个自己跟自己关联后,你其实会更快地注意到。 我的博士导师也做了大量相关的工作。我在读博的时候,我对法律、道德比较感兴趣,所以当时我提出,实际上人的自我概念是否只有好的一面?我们大部分人其实都是普通人,有好的和坏的一面,但是当我们想到自我概念时,我们实际上可能只想到了自己好的一面。 (参考文献:Hu, C.-P., et al. (2020). Good Me Bad Me: Prioritization of the Good-Self During Perceptual Decision-Making. Collabra: Psychology, 6(1): 20. DOI: https://doi.org/10.1525/collabra.301) 因此,我做了一个简单的实验,将人们好的一面和坏的一面与不同的几何图形进行关联,然后让他们完成一个匹配任务,比如,圆形代表好的一面,正方形代表坏的一面,三角形代表另外好的一面,菱形代表另外一个坏的一面,等等。任务很简单,就是判断上方的图形和下方的文字标签是否匹配。匹配按一个键,不匹配按另一个键。我们做了一个分类任务,比如只展示一个图形,让人们判断它属于自我还是他人,或者判断它是好是坏。实验流程图如下: 这是实验所做的工作。通过这个实验我们想要通过不同条件之间的比较,我们想要验证的假设是,是否只有中性的刺激与好的自我概念连接时,人们才会对它进行又快又准的反应。最后,我们拿到了被试的反应时数据和正确率数据。通过正确率,我们还可以计算出d prime的数据,所以相当于我们最后有三种不同的因变量数据。最后,我们可能对这些数据进行一个多变量分析,并绘制结果。当然,由于这是一个被试内设计的实验,我们不仅需要使用传统的分析方法,还应该使用混合线性模型,这样更能将被试的个体差异纳入考虑。我们当时只使用了重复测量方差分析的方法,并对结果进行了可视化。 这就是我那篇文章中的一个图表,现在我们课题组对它进行了一些改进,比如让闪点在中间这条线的两边稍微有些偏离,这样可以看到点并不是完全重叠的。我们还使用了一些计算建模的方法,进行了一些参数估计。 在认知实验中,我们进行数据分析的流程基本上是相同的:数据导入、数据清理、描述性统计,这时我们可能更加关注的是平均值,比如,每个被试在不同条件下反应时的平均值和标准差,以及频次分布或百分比等。我们也会进行一些常规的统计分析,比如重复测量的方差分析,计算出信号检测论中的d prime。如果交互作用显著,我们还要进行简单效应分析。同样地,从数据的导入到分析,我们完整的流程都可以在R中完成。问题2的解决流程思路图如下: 我们把R和传统的方式比较一下:在我读硕士和博士的早期,基本上都是采用传统的方式,首先在Excel中手动清理数据,筛选并删除不需要的行,然后保存为新的文件。然后,我们在SPSS中进行分析,绘制结果时基本上是Excel加上Photoshop,或者使用Illustrator等软件。在心理学中,主要使用的可能还是Excel,我当年用excel画error bar(误差线)还挺熟的。在excel中绘制的误差线比在SPSS中更好看。有时候需要对图形进行拼接时,可能需要借助Photoshop。这是我们以前的工具套装。最后,我们在Word中撰写报告。 但如果我们有信心,或者有决心,我们可以在R中完成全套工作,不需要借助其他任何工具。R中已经有一个非常完备的工具套件,包括数据清洗(用tidyverse)、各种统计分析工具(有bruceR),以及非常多的其它的统计包。个人认为,最近比较好的、有系统的包是easystats,有可能成为和tidyverse一样的R包体系。Easystats的主要开发者现在在新加坡,应该是一个法国的研究者。 至于绘图,我们会花很多时间专门讲解ggplot2,它与tidyverse都是同一个体系,都属于tidy。输出方面,我们使用markdown,上节课已经展示过,它可以输出PDF文件,也可以输出Word文档。需要注意的一点,因为R语言的开发主要是以英文开发者为主,对中文的兼容存在一些问题。在使用R语言时,特别是在文字输出时,如果你有很强的debug能力,你可以选择使用中文,不然使用英文是很好的选择,这样会减少很多错误。 对于统计分析来说,R语言的长处就在统计。对于基础的统计方法,它有多个函数来实现,比如bruceR里的t-test(t检验),各种各样的t-test它可以一次性解决,各种各样的方差分析它也可以一次性解决。简单效应和多重效应的比较就是EMMEANS()。最近也有一些新的包专门用于查看回归模型中的交互作用。 如果我们想要解决心理学问题,无论是问卷还是实验数据作为输入,我们都可以在R中完整地实现整个分析流程。使用R的好处之一就是它可以保留完整的分析过程,包括数据的预处理。也就是说,从读取原始数据,将大量数据用代码进行预处理,直到完成数据分析,这些过程记录都可以被保存下来,保证每次读取原始数据和每次分析的结果都与第一次相同。另一个好处是你的代码可以复用。这是学习使用计算机语言来处理和分析数据的最大好处之一。刚开始学习时可能会觉得浪费时间,但一旦学会,后面会节省很多时间。每次你只需要做一点点小的改动,就可以将代码很好地衔接到其他的数据之中了。 既然大家已经选择了这门课程,那就证明大家希望能够采用R语言进行数据分析。那我们就开始了! 3.2 如何安装R 3.2.1 R的下载与安装 大家都安装R了吗?我们从最简单的内容开始。如果还没有安装R,就在Bing浏览器中直接在搜索栏输入R进行搜索。不推荐使用百度,因为R主要是英文开发者开发的,所以你可以在英文互联网上找到更好的结果。这里显示的是在清华的tuna镜像网站上下载R语言 ),下载以后你可以进行安装。对于苹果电脑系统,它也有相应的apple芯片(比如M1到M3芯片)的独立安装包。这也是R非常好的一点。因为它不像python的某些旧软件包,可能找不到苹果芯片兼容的版本,这就很麻烦。 安装时,一路点击“下一步”即可。但是,需要注意的是,如果你是Windows老用户,都会对硬盘进行分区,比如C\\D\\E\\F这样分,大家都希望不要将软件安装在C盘。如果你习惯这样,那么在安装R的时候路径中千万不要有中文目录,也不要把R安装在某一个中文的文件夹中里,否则可能会出现问题,因为R目前对中文的识别存在问题。另外,也不要将自己的window系统名字设置为中文,因为这也可能会有问题。比如,R在画图的时候需要调用系统名下面的一个文件,如果你的系统名设置的是中文,它有可能会因为无法找到对应的程序文件而出错。 一般来说,现在大部分计算机都是64位比特的CPU,所以安装64位的R就足够了。安装时全部采用默认选择,安装完成后,你就可以看到下面这个界面。我的电脑上也没有安装R,所以我会直接给大家演示一遍R的安装过程。这是一个比较老的Windows机器,我习惯将其设置为英文界面,因为有时候如果设置为中文,可能会出现一些问题。现在,我将演示安装R的过程。首先,我会直接选择安装路径,确保路径中没有中文名称。然后,安装过程很快,完成后,我们会看到R的Logo,点击它就可以打开R的界面。 3.2.2 R console(控制台) 对于那些习惯图形界面的用户来说,看到控制台这个界面可能会感到困惑,不知道接下来该做什么。因为在这个界面中,我们没有太多可以点击的地方。如下: 在控制台界面中,它上面显示R的版本信息,下面是输入代码的地方。我们需要使用原生态的控制台的话,就意味着我们需要知道很多东西。但这和之前说的不太一样,前面说R社区的开发者开发了很多配套使用的包。这就好比,如果一个东西不好用的话,就会有很多人给它修饰和功能,让它变得好用起来。 3.3 如何安装Rstudio 3.3.1 Rstuio的下载与安装 目前,使用最多的是Rstudio。其实我刚开始使用R的时候,大约在2015或2016年,Rstudio还没有这么绝对的统治地位。当时还有Red R,以及各种各样的为R提供额外功能的软件。但现在,基本上都是Rstudio一家独大。 安装完成后,我们会看到一个包含四个面板的界面: 1. 左上角(panel 1):这是代码编辑区域,也被称为面板一,实际上它是一个文本编辑器,我们可以在这里编写R代码。 2. 右上角(panel 2):这个区域包含一些标签,用于显示程序中被调用和创建的数据、运行代码的历史记录等。默认的标签是“environment”(环境),它表示你打开了R以后有没有一些临时的变量或文件保存在工作记忆(缓存)里;“history”(历史)表示过去在R中输入的命令留下的痕迹; “connection”是R和其它后台的一些连接;“tutorial”绝大多数时候都用不到,因为我们的tutorial一般是在网上找的。 3. 左下角(panel 3):这是控制台(Console),我们在这里输入R代码并运行它,R的输出结果和报错信息也会在这里显示。 4. 右下角(panel 4):这个区域包含了一些其他标签,如“Files”(文件)、“Plots”(画图)、“Packages”(安装包)和“Help”(帮助),还有“presentation”(演示),比如,我们之后的课件都是运用html文件呈现,也可以在这里展现出来。 3.3.2 测试Rstudio 安装了Rstudio后,可以用一个简单的函数测试一下是否会因为用户名中文导致做不出图。在R的命令行输入下面的函数代码运行(按面板一的“run”键或“Ctrl+Enter”,这与在面板三的控制台上输入代码按“Enter”键是一个效果): x <- rnorm(1000, mean = 0, sd = 1) hist(x, breaks = 30, main = “Normal Distribution”, xlab = “Value”, ylab = “Frequency”) 运行完之后可以看到右上角的environment多了一个x,有一千多个变量。这样可以看到左上角是写代码,左下角是交互结果——即左上角代码运行的结果,右上角是运行之后临时变量和数据的存放,右下角是输出的结果——图表展示。如果Rstudio安装配置成功,运行上面的函数命令后你将同样生成一个随机的正态数组,并且结果将显示在相同的地方。如下: 刚刚说到我们安装了Rstudio之后可以帮助我们更好地使用R。那么我们刚介绍了Rstudio打开之后的四个界面。大家可能会有个疑问,就说我打开之后怎么只有三个界面,对吧?我们刚刚说在这个地方,其实左上角,应该有一个脚本编辑的地方。所以一般来说,我们写代码的时候不会直接在这个控制台里面写脚本。而是会用R的脚本(script),我们后面会提到的rmarkdown,那么就在这个地方写代码,把你代码的全部能够保存下来。那当你新建一个脚本之后,这个时候他就会有一个我们刚才看到的四个窗口的界面。如果你有强迫症的话,就会把他拉得跟他一模一样的。 另外,大家可能看到有些人的Rstudio是黑色的。怎么设置呢?在工具栏里面有个 global options(全局选项),然后有个 appearance(外观),那么在这个外观里面,你可以选择这种黑色主题,这样看着更专业。然后我现在就随便选一个,那是不是觉得看起来更黑了? 这里是一个文本编辑器,每一行都对应着一个代码。在Rstudio界面中,你可以看到一个“run”按钮。将光标放在某一行上,点击“run”,就会在这一行的代码在工作台运行。这类似于将代码复制粘贴到工作台(console)按一下回车(Enter)。 我们这里进行的一个简单操作是:从一个均值为0、标准差为1的标准正态分布中随机抽取1,000个数据点,通过“rnorm” 函数生成1000个随机的数据点,然后将它们赋值给一个名为“x”的变量。在界面的右上角environment,你可以看到新出现的变量“x”。我们将在后面讨论RStudio的对象(object),但现在你可以至少完成这个操作。接下来,我们可以进行数据可视化,比如选择第二个代码并运行。如果你的画图功能正常,你应该能够看到一个频次分布图,看起来是比较正态分布的。 3.3.3 创建新项目 进行一个完整的数据分析通常涉及多个步骤,这意味着可能需要不同的脚本。如果你有很多脚本和不同的数据,你可能希望将所有相关的东西放在一个文件夹中,这样每次打开Rstudio时,它都会直接挑回到你上次工作的地方,这样你就可以接着往下工作。这在R studio里面,它提供了一个“项目”(project)的功能。 当你在一个新的工作环境中创建一个项目时,你可以选择在一个已经存在的文件夹中创建一个新的项目,或者在电脑上创建一个新的文件夹来存放你的数据和之前的工作。创建项目后,它会生成一个后缀名为“.Rproj”的文件。这个文件会在每次打开时加载你的整个项目,包括之前的工作,并设置为工作目录。这个功能非常方便,这也是为什么我要在这里插入讲一下新建项目。 那么,测试成功后,大家可能会好奇:这些代码代表什么意思?这些函数是从哪里来的呢?比如,我们在前面讲的,假如你运行测试代码——rnorm函数,它可以直接从正态分布中随机抽取1000个数据点,绘图的histgram这个函数从哪里来?包括还有t检验。那么,这些函数都从哪里来呢? 如果我们想要对两个随机生成的变量进行t检验,我们可能会想直接在R环境中输入“t.test(V1, V2)”。然而,它可能会告诉你没有这个函数。这时候你想,如果变成大写的TTEST会不会有,你会发现,这里还是没有这个(大写的)函数。但是,如果你是小写的,它这个地方就会有。所以你可能会困惑:我们的函数到底从哪里来?所以,这就涉及到我们在R中调用函数。很多使用R的人都是“调包侠”。我们调用各种各样的函数,而我们不需要自己开发这些包,那么,我们调的这些函数从哪里来呢?例如,如果我们想要找到“t.test”函数来自哪个包,我们可以使用“?t.test”来查看其帮助文档(在R的右下角的help)。在R的帮助文档中,我们可以看到“t.test”函数的详细介绍,包括大括号里说明了它来自“stats”包,还有description(描述)、usage(用法)和argument(参数对象)。 3.3.4 包(packages)的介绍与调用 这里涉及到之前说过的R的生态。之所以学习R语言,是因为我们不需要从头开始编写所有这些函数。回想一下在我们学习心理统计方法时,比如t test,我们需要学习T检验的计算公式。而在R中,这些经典的方法已经被包装成易于使用的函数,我们可以直接调用它们,而不需要手动完成计算过程。这就是很多函数的来源——大量存在的包(packages)。 我们在使用R的时候,不仅需要安装基础的R,比如Rstudio,R base,我们还需要安装很多其它的packages。在R语言中,packages把大量的函数(function)模块化方便大家的调用。 一般来说,packages里包含了函数,有输入,做了一定的处理后,给一个输出,还包括数据集、文档等其它元素。所有的packages都是依照一定的规范开发的,这样的话用户就能够比较好的来引用和使用package。我们所有前面说的这个说R有很强的功能都是在packages里实现的。 R包(package)大概可以分为两类:第一类是基础包,这些包在安装R时就已经自带了。比如我们之前提到的stats,它就是是R base软件里面自带的一个包,里面包含了一些基本函数;第二类是由整个R社区贡献的包,比如我们之后会大量使用的tidyverse,这些包通常是由一系列研究者遵循相同的风格和方式开发的一系列用来开发数据分析的。每一个包可能具有其他包无法替代的独特功能,因此它们可能会被特定的用户群体使用。每个包都可能只提供部分功能,但当它们结合在一起时,就形成了一个生态系统,可以完成包括数据可视化、数据分析方法(比如机器学习、自然语言处理、数据质量分析等)在内的各种任务。 我们之前推荐的两个系统,Tidyverse和easystats,都是遵循相同风格开发的包系列。Tidyverse包括了ggplot和DPLYR等包,这些在我们进行数据清洗时会大量使用。此外,还有其他专门处理特定任务的包,比如用于处理字符串的String包。 这个easystats在过去的三年到四年时间里,有很多社会科学、心理学和神经科学领域的开发者使用R语言进行数据分析,它非常适合我们心理学领域的研究。因为它解决了很多常见的问题,基本就是我们会常用的一些功能。例如,effect size 是用于计算效应大小的包,reports是用于生成报告的包,它可以将分析结果整理成文档形式。还有see,用于数据可视化的包,以及model base和model performance进行模型基础分析和模型评估与比较的包。还有correlation包,它几乎囊括了我们能够找到的所有计算相关的方法,比如要计算两个数列相关的时候,可以让它将各种各样的相关方法的结果输出,获得丰富而全面的分析结果。 还有一个R包列表,叫CRAN Task View。它是一个官方维护R包的系统,其中包含了关于特定统计分析或数据分析主题的包,这些包对于解决特定问题非常有帮助。比如,有的同学可能会做元分析,R语言提供了丰富多样的包来支持这种分析,包括基于平均值的、贝叶斯的、基于模型的SEM方法、常规的基于CONSE的方法,以及混合线性模型(mix model)等方法。此外,R语言还有处理缺失值的包、心理测量(psychometrics)的包,以及贝叶斯统计的。大家可以看到在task view里面其实有很多跟心理学相关的包,如果你要做某个特定主题的话,就可以在里面寻找适合解决自己问题的包。 学习这个包的使用,一开始是在课堂上模仿他人。你看到同学用了什么,课堂上用了什么,觉得可以拿过去用,就以这里为起点,后面能够用R解决一些基本的问题。碰到一些小的问题没办法解决的时候再进行搜索。 既然R包由社区所开发,就意味着也需要安装才能使用。一般的时候,首先需要去安装这些包。因此,这里涉及两个函数,一个是通过install.packages()安装包,一个是library()加载包。R是高级编程语言,函数与自然语言相似,我们说的自然语言一般指英语。比如,安装ggplot这个画图的工具包,包ggpplt放到函数的括号里——install.packages(),然后用library()加载这个包。如果说把一个包当作一个工具箱的话,那么library加载包意味着你要打开工具箱,可以使用里面各种各样的工具。每个工具都有自己独特的标签,使用标签时调用工具包里的函数。 install.packages(‘tidyverse’) library(tidyverse) 注意,使用R时一定要用英文符号,括号、引号必须是半角符号,这是初学者容易出错的点。比如,当你应该用英文的逗号时,你用的中文的,他们看起来还很像,可能你找半天也找不到错在哪儿。 在Panel 4的package界面中,你可以看到已安装的包: 3.3.5 镜像的选择 安装包时,如果发现安装特别慢,可以选择镜像。通常选择国内镜像安装会比较快,选择国外的会比较慢。一般来说,国内的镜像都比较稳定。有两种做法: (1)一种是可以使用如下代码更改全局镜像地址,以清华地址为例: options(repos = c(CRAN = “https://mirrors.tuna.tsinghua.edu.cn/CRAN/”)) 如果需要查询当前默认全局镜像,可以使用以下代码: getOption(“repos”) (2)还有一种做法是,在R的install语句中用额外的命令指定镜像。怎么使用在之后的打开Rstudio的时候我们再看(详见下一节3.6)。 3.3.6 安装Rtools(windows) 有些包需要Rtools,它是在windows下面的一个工具,因为有些包不是官方可以通过install package安装的,它需要重新编译,这时需要使用特定的工具。安装的办法有两种:第一种是直接在R里面用install语句安装。另外一种办法更简单些,直接下载Rtools43进行安装。其实你只要全部选择默认路径的话,那么其它地方都不需要修改。如果你是Mac系统的话,需要安装Xcode。Xcode是开发者的一个大工具包,如果你做任何的编程,包括github,可能都需要用到它。 install.packages(“installr”) library(installr) install.Rtools() 回到 Rstudio的界面,在右下角你会看到一个package的选项。点击它,你就能看到安装了哪些工具包。由于我的 Rstudio 是刚刚安装的,所以这里的包并不多。这些包是在安装Rstudio的基础包时自动带过来的。 在 studio 环境中,你会注意到它提供了一个很好的自动补全功能。比如,当你开始输入一个函数名的前几个字符时,它会自动显示以这些字符开头的所有函数名。这时,你只需要输入前几个字符,然后按 Tab 键,它就会自动补全剩余的部分。例如,我们一般都会安装 “tidyverse”这个包。如果你仔细观察,可以看到安装是从哪个地方进行的。你会注意到它从 “grand r studio .com” ,实际上,它是从这个 R 的官方包网址下载所有包。 另外,值得注意的是,当我们安装一个包时,虽然我这里只安装了一个,但实际上它会安装一系列相关的包。这是因为很多包的开发依赖于其他已经存在的包,要使这个包正常运行,必须安装这些依赖的包。当我们采用默认方式安装某个包时,它会安装所有依赖的包。通常情况下,在windows系统不会出错,但在某些特定的环境配置下,比如使用 Linuxs 作为数据配置环境,可能需要对系统打一些补丁才能安装某些包。 安装完成后,我们可以查看或更改 “install package” 里面的参数设置。现在我们这里的包已经安装完成了。当你使用install package 安装包时,你可能注意到了我们只输入了一个参数。如果你想了解这个函数或包的更多信息,可以在函数前面加上一个问号,这样就可以查询帮助文件,了解这个包的具体功能。 ?install.packages 例如,我们可以看到这个包属于 [utils] 这个包,它是专门用来安装R包的。这个函数的第一个参数是 “package”,即包的名称,第二个参数是 “library”,也是一个特定的参数。还有其他参数,比如 “repos”,它指定了安装的来源,比如可能需要从清华的镜像源安装,这个时候需要把这个参数也输入进来才能选择镜像: >install.packages(pkgs, lib, repos = getOption(“repos”) 虽然这个函数有很多参数,但作为新手,大多数情况下,可能不需要使用所有这些灵活的参数,只需要使用最简单的参数,大多数默认参数对于新手来说已经足够使用。关于在不同系统下安装Rtools的具体步骤,我们这里就不详细讲解了。如果你在使用过程中遇到任何问题,可以在群里随时交流、讨论。 3.4 如何通过和鲸(Model Whale)使用R 另外,关于提交作业,为了方便我们批改,今年我们采用了和鲸的云计算平台。下面我会简单演示如何在和鲸云计算平台上操作。 假如你已经注册一个和鲸的用户账户。注册并登录后,进入社区界面,在右上角会看到一个工作台的下拉菜单。点击这个三角形的下拉标志,选择基础版工作台。进入工作台后,你会看到一个界面,其中有一个加号按钮,用于新建项目。点击这个加号就可以建立一个新项目。比如,建一个名为 “test”的项目。 初始的数据语言设置十分重要。和鲸提供了三种开源的数学分析语言可供选择——Python、R 和 Julia。我们在这里选择R。不需要上传数据,就可以直接创建项目。创建完成后,就可以开始运行项目。点击进入项目,右上角有一个运行按钮。在运行前,你需要选择计算环境和计算时间。在设置中,选择合适的计算资源,比如一个两核 8G 内存的计算资源。这个计算资源相当于是一个远程的计算机硬件,而软件系统则是你所选择的运行环境。如果使用 R 语言,平台提供了多种公开的 R 环境供你选择,比如 R4.3.1的tidyverse的分析镜像。直接选择所需的环境,然后就可以开始运行你的项目了。 点击开始后,和鲸系统需要一点点时间拉取这个时间镜像。相当于它即时地给你生成一个工作环境。这个环境会包含 R、tidyverse。准备就绪后,你可以直接在这个环境中编写和运行代码。然后你也可以生成不同的版本。大家需要慢慢熟悉一下和鲸的平台。在和鲸云计算平台上写代码比自己搭建环境要简单,因为你只需要敲代码就可以了,其它的都不用管了。 我在这里给大家演示一下:注册账号并登录后,你会在右上角看到工作台选项。选择基础版工作台,你会看到一个界面,其中有一个加号按钮用于创建新项目。将鼠标放在加号上,可以看到新建项目的选项。创建一个名为 “test 2” 的项目,并进入该项目。项目默认是展开的,你可以将其关闭。如果没有特殊数据需求,可以直接创建。创建后,项目会自动打开。你也可以回到首页,在最新项目中找到 “test 2”,点击进入,浏览项目。 它实际上提供了一个 Notebook,在notebook里面,文字和代码可以混合编写。这里有一个“欢迎进入ModelWhale Notebook”的标题,双击标题符号可以编辑内容。如果发现无法编辑,可能是因为还没有运行项目的镜像。需要点击运行按钮来启动项目。在运行前,你可以点击齿轮图标进行设置,比如选择计算资源和时间。选择好后点击运行,那么它就开始给你打开一个云端的服务器,它需要一点的等待时间。当它可以用的时候,它上面会显示一个R、空心的圆,表示你可以用这个计算机资源了,计算机语言是R语言。 我们可以把刚刚在Rstudio里面运行的代码复制、粘贴到和鲸来,看看会不会得到一样的结果。在和鲸中新建一个代码框,点一下三角符号运行复制、粘贴过来的代码,得到一个跟刚刚类似的分布图。所以,和鲸这个平台就是为了避免大家自己管理工作环境而创建的,它适合一些特定的情境,比如不同的人需要完全一模一样的工作环境时,不同的人就可以在和鲸云计算平台上运行。 由于和鲸是一个商业组织,使用计算资源是需要付费的。对于我们班上的同学来说,因为大家后面要完成作业。和鲸有一个与我们心理学院合作的统计教学工作台,这是之前课程的平台,我们后面会邀请大家加入。还有,对于使用Windows系统的同学,如果发现很难将用户名从中文改为英文,但你又想学习R、尝试一下,你也可以使用和鲸系统来编写 R 代码。如果你需要改变计算环境,助教将会帮助大家配置。 刚刚在运行之前,我们会选择硬件——计算资源,另外一个选择是工作的环境——数据分析的镜像。相当于这里有一个打包好的系统,其中包含了各种数据分析工具,包括R、base 和各种package。如果课程需要的工具在镜像中缺失,我们会在镜像中安装并分享给大家,这是我们的助教会帮助来做的。 也就是说,在课程中,大家可以选择两种途径来学习:一种是在自己的电脑上安装 R 和 Rstudio,然后下载课件进行学习;另一种是只想学习R语言本身,不想学习各种各样的包,那么你可以选择在和鲸的云计算平台上使用和学习代码。它会有一些区别,因为我们的课件本身使用的是Rstudio里面的东西。OK!这节课相当于我们开始上路了!上R语言学习的路!下节课,我们会告诉大家如何使用我们提供的课程资源。除了前两节课使用的是PPT,后面的几章都是用的Rmarkdown。Rmarkdown就意味着我们所有使用的代码都可以直接拿去复用。那么,怎么才能最充分地使用这些公开分享的东西呢?推荐大家学会Github。所以下节课,我们会教会大家如何最基础地使用Github、如何运用我们的课件资源,以及在当中如何协作、贡献。比如,最后我们需要使用GitHub做小作业,如何运用GitHub协作,每个人都写一部分代码而不出错,等等。所以下节课,我们先把基础性的工作做好,教大家学会使用GitHub、如何使用我们的课件,然后开始正式地介绍R里面做数据分析的东西。前奏有点长,节奏现在比较舒缓,而一旦开始了可能就比较快了。 今天的课就到这里,谢谢大家。 "],["第三讲git-rstudio工作流.html", "Chapter 4 第三讲:Git & RStudio工作流 4.1 Files and Folders System 4.2 Git and Git Hub 4.3 Local Version Control 4.4 Remote Version Control 4.5 作业", " Chapter 4 第三讲:Git & RStudio工作流 4.1 Files and Folders System 第三节课将继续入门R语言的学习。本课程的核心目标是帮助大家开始使用R,即便是已经接触过R的同学,也可以通过这节课对R进行更系统的学习。去年我们的R课程中,Git的学习是在课程后期进行的,那时同学们已经接触了一定量的代码,意识到代码管理的重要性。但今年,我们选择提前介绍Git,因为我们的所有PPT和课件更新都将通过Git Hub进行。如果不熟悉Git Hub操作,同学们在获取最新课件时可能会面临反复下载和删除的问题,这不仅耗时,也不利于大家的学习。因此,我们决定将Git和Git Hub的学习放在课程的前面,让大家尽早掌握这些必要的技能。 本次课的内容主要分为四个方面:首先是数据分析中如何更好地管理数据文件;其次是Git Hub的基本介绍和主要特点;接着是Git的基本功能和版本控制原理;最后是Git的远程文件版本控制和如何在远程代码托管平台或云平台上进行交互。这些技能对于团队合作和项目管理非常重要,尤其是在大型项目中。 我们的目标有两个:一是提高大家的项目管理和版本控制技能,以增强学习效率、研究能力和团队协作能力;二是即便短期内不需要频繁编写代码的同学,也应该学会如何下载更新课件和完成第一次小作业。这些技能对于保持项目最新性和协作顺畅性至关重要。 在日常工作中,我们经常看到不规范的数据管理做法,如所有类型的文档和数据被随意放置在一个根目录下,没有分类。这种做法虽然能找到所需的数据和图表,但查找过程繁琐,效率低下。 为了解决这个问题,建议对文件夹进行分类管理,并根据数据和文件的不同目的和使用方式,将它们分成不同的类别。此外,添加一个README文件,描述每个文件夹的功能和内容,以便未来的自己或其他研究者理解。 我们的课题组采用了两种文件管理模式:针对实证研究的文件管理和针对元分析或元研究的文件管理。这些模式在我们的Git Hub仓库中有详细的模板,其他研究者可以参考。通过特定的网站,我们发现借鉴规范的文件夹结构模式对研究者有益,也可以向导师推荐。网址是:https://psych-transparency-guide.uni-koeln.de/folder-structure.html 在文件管理方面,我们发现一个常见问题是文件夹结构看起来整洁,但打开后发现文件组织混乱,包括文件命名问题。例如,对于毕业论文,应该在文件名中加上作者的名字,以便导师清楚地知道哪个文件是需要修改的。在数据分析过程中,版本控制非常重要,它可以帮助我们管理文件的各个版本,避免丢失重要的修改。 4.2 Git and Git Hub 版本控制是一种强大的文件管理工具,它让我们能够记录每次文件更改的详细信息,只更新变化的部分,并清晰地标记出哪些内容发生了变化,哪些保持不变。这在管理大量文件,尤其是每次修改内容较小时,显得尤为重要。版本控制工具帮助我们追踪文件修改的历史,保留各个版本,即使是一些看似不重要的更改。 现代版本控制工具,如Git,提供了更多功能。它们使我们能在本地电脑上管理文件的所有更改,并同步这些更改到远程服务器,如Git Hub。这样,我们既能跟踪项目在个人电脑上的所有变化,也能与他人协作,共同管理项目的发展。 版本控制系统让我们能轻松地回退到之前的版本,比较不同版本间的差异,甚至恢复已删除的文件。这些操作既可以在本地电脑上完成,也可以通过网络连接到远程服务器或云端。当我们提到“本地”时,我们指的是电脑内部的硬盘;而“远程”或“云端”则指的是连接到个人电脑之外的存储系统,如Git Hub这样的代码托管平台。 通过这些远程平台,我们可以创建所谓的“仓库”(repository),这是一个集中存放代码和文件的地方,方便他人与我们一起合作,共同开发项目。这种协作方式大大提高了工作效率和团队协作的便捷性。 版本控制系统的另一个重要特点是它支持多人同时对同一项目进行修改,这在团队协作中尤为重要。例如,团队成员A和团队成员B都可以在同一文件上进行修改,只要他们的修改不冲突,系统就可以自动合并这些更改。如果出现冲突,系统会提示两位团队成员在哪些具体部分出现了冲突,然后他们可以决定如何处理这些冲突,是保留A的更改、保留B的更改,还是尝试合并两者的工作。 这种机制允许不同的团队成员在不同时间和地点工作,同时维护同一个项目的最新状态。这对于国际团队合作尤为重要,因为团队成员可能分布在不同的时区,比如中国的团队成员在白天工作,而北美的团队成员在晚上工作。即便如此,他们也可以无缝地共享工作进展,并在适当的时候合并他们的更改。通过版本控制系统,团队成员可以清楚地看到项目的演变过程,了解彼此的更改和思考,从而更好地协同工作。这种协作方式不仅提高了工作效率,还促进了团队成员之间的沟通和协作。 4.2.1 Git 目前,Git是最广泛使用的版本控制系统,而Git Hub是基于Git的开源代码托管平台,提供了丰富的功能和界面,使得版本控制更加直观和易于使用。在Git Hub上,用户可以创建项目并从中创建分支来尝试新想法或进行修改。分支的作用在于,它允许你在不影响主线(或主分支)的情况下,独立地开发新功能或进行实验性修改。这样,你可以自由地探索新的思路,而不必担心这些未经验证的更改会破坏主项目的稳定性。一旦你完成实验或确认了新的修改是有效的,你可以将这些更改合并回主分支,从而推动项目向前发展。Git Hub提供了分支管理的图形界面,使得创建、合并和切换分支变得非常简单。此外,Git Hub还支持协作特性,如Pull Request,它允许贡献者提交他们所做的更改,供项目维护者审查和合并。 在团队协作中,每个成员都可以从主分支创建自己的分支,进行工作,然后将更改合并回主分支。这种工作模式促进了代码的迭代和团队之间的协作,同时也确保了项目的稳定性和可维护性。在教学或学习环境中,Git的下载和安装可能需要科学上网,Git网址是:https://Git-scm.com/downloads。 Git Hub既是一个平台,也是一个软件工具,它提供了版本管理的功能,同时支持通过终端(命令行)和图形界面两种方式进行操作。终端提供了强大的功能,允许用户通过输入命令来执行各种操作,比如创建仓库、分支、提交更改、合并请求等。终端的使用需要一定的命令行知识,但对于熟练的使用者来说,它提供了更高的灵活性和控制力。 Git Hub的终端在不同的操作系统中表现不同。在Linux和macOS系统中,用户可以使用内置的终端,如bash或zsh;在Windows系统中,用户可以使用命令提示符(Command Prompt)、Windows PowerShell或终端模拟器。此外,许多开发环境,如RStudio,也提供了内置的终端,允许用户直接在环境中使用Git命令。 图形界面则提供了更加直观的操作方式,适合不熟悉命令行的用户。Git Hub Desktop是Git Hub提供的桌面应用程序,它提供了一个简单的界面,允许用户通过点击来完成诸如克隆仓库、提交更改、创建分支和合并请求等操作。此外,还有一些第三方软件在Git Hub的基础上增加了图形界面,提供了更多的集成和自动化功能。 在教学环境中,通常会主要介绍通过RStudio中的Git面板来使用Git Hub,因为这种方式可以在RStudio环境中直接完成所有需要的Git功能,无需切换到其他终端或应用程序。这使得版本控制的教学和操作更加方便和集成。 4.2.2 Git Hub Git Hub确实是一个全球性的开源代码托管平台,它允许用户存储和管理自己的项目,并通过Git来进行版本控制。Git Hub提供了丰富的特性,如分支管理、合并请求(Pull Requests)、代码审查、以及issue跟踪等,这些都有助于促进项目的发展和团队合作。在2018年之前,Git Hub是完全开源的,由社区运营,并且是非盈利的。然而,2018年微软收购了Git Hub,这引发了一些关于开源和商业利益的关系的讨论。微软收购Git Hub后,有些人担心这可能会影响Git Hub的开放性和中立性,因为微软是一个盈利性企业。 关于微软使用Git Hub上的开源代码来训练AI助手Copilot的问题,这是一个敏感话题。Copilot是一个基于Git Hub上大量开源代码训练出来的AI工具,它可以帮助开发者编写代码。尽管Copilot提供了便利,但它的存在确实引发了一些争议。一些开发者担心,使用开源代码来训练商业产品可能违反了开源许可的精神,特别是当开源许可明确禁止将代码用于商业目的时。这些争议触及了知识产权、开源许可和商业伦理等领域。开源社区和开发者们对于如何正确使用和尊重开源代码的知识产权有着不同的观点和讨论。重要的是,使用开源代码时,用户应该遵守相应的开源许可协议,尊重原作者的意图和权利。 OpenAI 是一家总部位于美国的人工智能研究公司,它致力于推动人工智能的发展和应用。OpenAI 的某些做法确实引发了公众和媒体的讨论,特别是关于如何处理开源代码和知识产权的问题。OpenAI 使用大量开源代码和数据来训练其AI模型,而这些资源和数据的合法性和道德性有时会受到质疑。确实,OpenAI 和其他一些公司利用开源社区的贡献来开发商业产品和服务,这可能会与开源许可的精神产生冲突。开源许可通常要求用户在修改和重新分发代码时遵守特定的条件,有些许可明确规定了禁止将代码用于商业目的。尽管 OpenAI 声称其部分模型是开源的,但其核心技术和模型往往是不公开的,这引发了关于透明度和公平性的讨论。在商业实践方面,OpenAI 的行为有时也会受到批评。例如,出版商与其就使用版权材料进行训练的做法发生法律纠纷。这些纠纷反映了人工智能发展中的一些法律和伦理问题,包括如何平衡版权保护与技术发展之间的关系。 此外,一些Git Hub平替平台,如 Gitee 和 GitLab,也是值得关注的。Gitee 是中国的一个代码托管平台,它提供了一些与 Git Hub 类似的功能,但更加符合中国的互联网环境和文化。GitLab 则是一个企业级的代码管理平台,它提供了更丰富的功能,特别是针对企业的协作和安全管理需求。GitLab 在处理商业和政治敏感问题时采取了一些立场,例如在俄乌冲突期间,它停止了对俄罗斯的科学家和程序员的代码托管服务。Bitbucket 是由 Atlassian 公司开发的一个代码托管平台,它也提供了Git和Subversion的存储服务。Bitbucket 同样支持开源项目,并且提供了一些针对团队协作和企业使用的功能。与 Git Hub 类似,Bitbucket 允许用户创建自己的仓库,管理分支,以及通过 pull requests 进行代码审查。对于大多数开发者来说,Git Hub 是最受欢迎的选择之一,原因包括其庞大的用户社区、丰富的功能集、以及广泛的开源项目托管。Git Hub 的用户界面友好,功能直观,适合个人和团队项目。 然而,Git Hub 可能会遇到稳定性问题,这可能会影响开发者的体验。在某些情况下,Git Hub 的服务可能会出现中断或性能问题。因此,作为开发者,了解如何使用命令行操作 Git,即使在图形界面不可用或遇到问题时,也是一个有用的技能。开发者应该根据自己的需求和偏好来选择代码托管平台。如果是在中国地区,可能会考虑到网络连接的稳定性,选择如 Gitee 这样的国内平台。如果是在国际环境中,Git Hub 通常是一个不错的选择,因为它拥有更广泛的社区支持和丰富的生态系统。大家可以先注册一个Git Hub账号( https://Git Hub.com )。 接下来,我们将展示Git Hub的界面。假设你已经注册了一个Git Hub账号,接下来你会创建自己的repository,也就是仓库。在Git Hub上,你可以按照项目来管理你的代码,为每个项目创建一个独立的仓库。这样的好处是,每个仓库都可以针对特定的目的进行管理。例如,我们为这门课程创建了一个名为R4psy的仓库,另外还有一个R4psyBook仓库,这里面包含了我们将课程讲稿转换成文字的内容。 在Git Hub上创建仓库时,你可以给仓库命名,只要这个名字在你名下还未使用过。你可以选择仓库是公开的还是私密的。如果选择私密,只有你授权的人才能访问代码。你可以邀请合作者,这样你和合作者都可以访问仓库。创建仓库时,你可能需要添加一个readme文件,以便他人更快地了解仓库内容。你还可以选择添加一个.Gitignore文件,这将我们在后面讲解。此外,你可以选择一个license来规定你的代码的许可方式。尽管创建的license可能不会被每个人都遵守,但如果不创建,则默认没有人会遵守。例如,如果你希望研究中被广泛使用,你可以选择一个宽松的许可,允许他人修改和分发代码。如果你不希望代码被用于商业目的,你可以选择一个禁止商业用途的许可。对于心理学的同学或大部分研究者来说,选择一个常用的Creative Commons(CC)许可通常是较为合适的选择。 关于readme文件,它实际上是一个文本文件,但在Git Hub上,它可以被识别并展示为Markdown格式。Markdown是一种轻量级标记语言,允许使用简单的符号来格式化文本。例如,在Markdown中,你可以使用井号来创建一级标题,使用两个井号来创建二级标题,使用星号来加粗文本,使用一个星号来创建斜体文本。此外,Markdown还支持水平分割线、代码格式化(包括单行代码和多行代码)等。随着我们开始使用Markdown,大家会逐渐熟悉这些语法规则。至于我们课程的PPT,我们现在展示的内容实际上是在网页上展示的。这个PPT是通过一个名为xaringan的包生成的,这个包可能与动漫有关,它的名字听起来像是一个与动漫相关的词汇。这个xaringan包生成的PPT实际上是rmarkdown格式的。 当你第一次创建一个仓库并在其中添加了一个readme文件时,这个readme文件通常是空的。一旦你添加了内容或者对仓库进行了任何更改,Git Hub会提示你记录这些更改。这个过程通常被称为“提交”(commit)。你可以将这次更改命名为“first commit”,然后将其提交到Git Hub上。在这个过程中,你可以想象你的仓库就像是一个文件夹,而在文件夹中有一个名为readme的文件。当你第一次创建仓库时,readme文件是空的,但当你添加内容后,它就不再是空的了。你将这些更改记录下来,并将其提交到版本控制系统中。提交更改后,你的所有更改都会保存在远程仓库中。在Git Hub上,你可以看到你已经做了几次更改,以及每次更改的详细信息。这样,你就可以跟踪你的项目进度,并与他人协作时保持代码的历史记录。 在Git Hub中,你可以查看仓库中文件的完整历史记录。这意味着你可以追踪文件从创建之初到当前状态所经历的所有更改。每次更改都会被记录下来,包括更改的内容和日期。这就是版本控制系统的作用,它使我们能够有效地管理代码的变化,确保每次更改都可以被追踪和回溯。进行版本控制是我们维护代码库时非常重要的一部分,它帮助我们在开发过程中保持代码的整洁和可管理性。 在Git Hub上,不仅可以方便地查看代码的历史记录,而且还可以轻松地与他人合作。有了版本控制系统,合作变得格外简单。在Git Hub上,你可以直接fork他人的代码,这是一个非常直观的操作,特别是对于公开的repository。当你登录Git Hub并打开一个公开仓库时,你会看到一个fork按钮,它象征着将代码复制到你的个人账户中,形成一个新的仓库。 Fork操作就像是将一个代码仓库复制一份,创建到你自己的Git Hub账户下。这样,你就可以在这个复制过来的仓库中进行修改,而不会影响到原仓库的内容。这是一个理想的协作方式,因为它允许你在不影响他人工作的情况下,对代码进行个性化的修改或实验。 例如,如果你看到一个课件仓库,并希望对其进行个性化修改以适应自己的学习需求,你可以fork这个仓库到自己的账户中,然后在forked仓库中进行更改。这样,你的更改只会影响你的仓库,而不会影响到原始仓库的内容。这为开源项目的贡献者提供了一个安全的环境,让他们可以自由地创新和实验。 在Git Hub上,当你对forked仓库进行了更改,并希望将这些更改合并回原仓库时,你可以通过发起一个pull request来实现。pull request是一个请求,邀请原仓库的管理员或贡献者审查你的更改,并决定是否将这些更改合并(merge)到主分支。通过pull request,你可以通知原仓库的维护者你的更改,并提供详细的描述和原因。维护者可以审查你的代码,提出建议或直接合并你的更改。如果维护者认为你的更改不合适,他们可以拒绝合并请求。如果更改被接受,你将成为原仓库的贡献者之一,并在仓库的贡献者列表中看到你的头像和名字。 到目前为止,我们讨论的都是基于Git Hub的在线操作,包括注册账户、创建项目、fork项目以及通过pull request贡献代码。但实际开发过程中,我们还需要在本地进行代码的编写和版本控制。本地版本控制是开发者经常遇到的场景,它允许你在本地文件夹中进行操作,然后将更改推送到云端,与其他人分享和同步代码。 本地版本控制通常涉及到使用Git命令行工具或图形界面工具,如Git Hub Desktop,来克隆仓库、提交更改、创建分支、合并分支等。这样,你可以在本地测试和修改代码,确保一切按预期进行,然后再将更改推送到Git Hub上,与他人共享你的工作成果。 4.3 Local Version Control 我们将讨论本地的版本控制,特别是当你使用Git进行管理时。当你将文件夹转变为Git仓库,并在Git Hub上托管项目时,你可能会想知道每次更改是如何被记录的。实际上,在你的工作目录中,有一个隐藏的.Git文件夹,它包含了Git仓库的所有信息。这个文件夹通常是不可见的,除非你在文件资源管理器中启用了隐藏文件的显示。 在这个.Git文件夹中,有一个叫做暂存区(index)的区域,它用于记录你对文件的更改。当你在工作目录中修改文件后,你需要使用Git add命令来标记这些更改,以便它们被纳入下一次提交的范围内。如果你只是添加了一个新的文件,如一个readme文件,你会使用Git add来通知Git这个新文件的存在。一旦你标记了更改,你可以使用Git commit命令来提交这些更改。这个命令会为你提供的更改创建一个唯一的ID(通常是一个很长的数字哈希),并将它们保存到本地的Git仓库中。这样,每次提交都会有一个唯一的标识符,以及你为该次更改提供的注释。如果你想查看更改的历史,你可以使用Git log命令来查看所有提交的记录。这让你可以看到每次更改的内容,以及它们对应的标识符。 总结一下,本地版本控制的关键在于明确地告诉Git你做了哪些更改,无论是通过Git add还是Git commit命令。如果你没有记录这些更改,版本控制系统就不会知道你已经做了修改。因此,使用这些命令是确保你的更改被跟踪和保存的关键。 在我们本地的环境中,如果你想要完成版本控制的流程,你其实不需要编写代码,如果你不喜欢写代码的话。你可以完全使用RStudio来完成这个流程。在RStudio中,你只需要点击“文件”菜单,然后选择“新建文件”里的“新建项目”,也就是创建一个新的项目。在这个新的项目里面,你可以选择全新的在一个新的文件夹中进行实验,也可以在一个已经存在的文件夹中进行操作,将其转换为一个R项目。不管你选择哪个选项,最终都会打开一个名为“新建项目”的窗口。在这个窗口中,有一个高亮的勾选选项,询问你是否要将项目同时设置为Git仓库。如果你勾选了这个选项,RStudio会帮助你初始化一个新的Git仓库。当然,这要求你的计算机上已经安装了Git。如果你还没有安装Git,那么这个选项可能不会出现。一旦你安装了Git,RStudio内部会发生变化,开始兼容一些与Git相关的选项。在这种情况下,当你创建项目时,你的文件夹中就会有一个隐藏的.Git文件夹,它开始对你文件进行版本控制。这样,你就可以利用RStudio的图形界面来进行版本控制,而无需直接编写代码。 当你在RStudio中创建了一个新的Git仓库时,默认情况下,RStudio会创建一个隐藏的.Git文件夹来管理版本控制数据。在这个时候,你的RStudio界面上方,右上角,会有一个关于环境变量、历史记录、连接信息以及其他相关内容的菜单。其中, Git相关的窗口和标签会出现在界面上,允许你查看和管理Git仓库的信息。 一旦你安装了Git并且开始了版本控制,你可以在Git标签下看到你有哪些未提交的更改。RStudio会自动识别出这些新的更改,并允许你选择是否将它们暂存(stage)并提交(commit)。当你点击“提交”按钮时,RStudio会弹出一个窗口,让你输入提交信息并确认提交更改。 当你准备对代码进行提交时,你需要确定要 commit 的具体内容。比如,我们现在决定将这两个更改纳入 commit,这样做之后,它们将被提交到本地的 Git 仓库,并且被记录下来。这意味着,当你新建一个文件夹并决定将其作为一个 Git 项目来管理时,你可以通过创建一个新的 Git 仓库来将其变为一个 Git 项目。 在我们刚刚讨论的提交(commit)过程中,每次提交都会自动分配一个唯一的编号,也就是提交哈希(commit hash)。然而,仅仅分配一个编号是不够的,你还必须为这次更改附上一个描述性的信息,比如更改的主要内容或者目的。如果你不提供信息,Git 会报错,要求你必须留下一些备注。这是 Git 的一项基本要求,无论你是通过命令行操作还是使用图形界面,都需要提供这次提交的信息。 信息填写完成后,Git 会开始处理提交,可能会显示一些状态信息。这一切都是在本地进行的,你的更改还没有推送到远程仓库。 例如,我们可以在项目文件夹中新建一个名为 “read me” 的文件。这个文件可以是文本文件(TXT)或者 R Markdown 文件(RMD),取决于你想要记录的内容类型。在创建了这个新的 “read me” 文件之后,我们可以在这个文件中添加一些描述性的内容或者说明。 与 Git 刚才记录的文件夹状态相比,我们现在新增了一个 TXT 文件。Git 会有所察觉,并通知你新增了一个文件。此时,你可以将这个新的文件暂存(stage),这意味着你标记了它以便于下一次提交。然后,你可以再次执行提交(commit)命令,重复我们刚才的过程,这样新的 “read me” 文件就会被添加到 Git 仓库的历史记录中。这个过程不仅记录了文件的更改,也记录了文件的新增。 在你进行提交(commit)时,Git 会在提交界面的底部显示一个对比窗口,这里会比较你上一次提交和本次提交之间的差异。例如,如果你首先创建了一个测试仓库,并提交了初始状态,然后你修改了 “read me” 文件的内容,Git 会高亮显示删除的内容为红色,表示这些内容在本次更改中被移除了。同时,新增的内容会被高亮显示为绿色,这样你就能清楚地看到本次修改具体做了哪些改变。 完成修改后,你可以再次执行提交(commit)操作。通过这样的步骤,每次提交都会被记录下来,并且每个提交都会有一个独特的编号,这个编号通常是一个由字母和数字组成的 SHA-1 哈希值。这个哈希值不仅标识了提交的唯一性,而且还可以用来检索提交历史记录中的特定提交。 正如所演示的,从创建文件到第一次提交,再到添加和更新 “read me” 文件,每一次的修改都会被 Git 记录下来,并且每个文件的变化都会有一个清晰的记录。这样,你可以追踪项目的每一次变化,以及每一次变化的具体内容。 在我们上课的微信群中,有同学提问了一个问题:如果我已经在本地有一些 R 代码,但之前没有使用 Git 来管理这些代码,现在想要开始用 Git 来管理,应该怎么办? 答案其实很简单。首先,你需要将你的代码文件夹转换为一个 Git 仓库。我现在就直接在我的 RStudio 中进行操作。在 RStudio 中,我们点击“文件”(File)菜单,然后选择“新建项目”(New Project)。在这里,我们有几个选项: 1.创建一个新的目录(New Directory):这将创建一个新的项目和一个新的文件夹。 2.关联现有目录(Existing Directory):如果你已经有了一个正在编写代码的文件夹,你可以选择这个选项来关联现有的文件夹。 3.创建目录并启用版本控制(Create Project from Existing Code):这个选项实际上是让我们创建一个新的 Git 仓库,并将现有的代码文件夹作为 Git 仓库来使用。 由于我们已经有了一些代码,我们应该选择第二个选项——关联现有目录。选择后,RStudio 会提示你选择具体的文件夹。你可以浏览到你的代码文件夹,并选择它。一旦你选择了现有目录,你需要将所有现有的文件添加到 Git 仓库中。对于 Git 来说,它并不知道你之前的工作,所以你需要手动将这些文件添加进去。这相当于你第一次将这些文件加入 Git 版本控制。 在我们刚才的讨论中,我们深入探讨了一个对科研人员非常重要的概念:.gitignore文件。这个文件的主要作用是指导 Git 版本控制系统,忽略掉不需要进行版本控制的文件和目录。这对于协作项目和数据管理来说非常关键。例如,在使用 R 语言进行数据分析时,RStudio 开发环境会自动生成一些临时文件,比如 .Rproj 文件、.RData 文件和 .history 文件等。这些文件属于临时性质,每次打开 RStudio 或者运行 R 命令时都可能会发生变化。将这些文件纳入版本控制是没有必要的,因为它们并不能反映你的实际代码或数据变化。 此外,科研人员在进行研究时可能会涉及到一些含有敏感信息的原始数据,这类数据是不宜公开的。在这种情况下,通过在 .gitignore文件中明确指出要忽略的文件,科研人员可以避免这些敏感数据被上传到 Git Hub 等公开平台上。如果不幸地,某些敏感信息已经被上传到了版本控制系统中,那么需要立即采取措施,确保这些信息从所有历史提交中彻底删除。由于 Git 保留了每次提交的所有文件,因此这一步骤非常关键,以确保敏感信息不会被外部获取。 总的来说,.gitignore文件是一个强大的工具,可以帮助科研人员管理 Git 仓库,避免不必要的信息上传,并保护敏感数据。值得注意的是,一旦 .gitignore文件设定好后,Git 会自动忽略其中的文件,科研人员无需再手动排除这些文件。如果某些文件需要分享,但不宜通过 Git Hub 进行,可以选择其他方式,如直接分享文件或使用额外的共享服务。 关于 Git 的 .gitignore文件,它有自己的匹配规则,这些规则支持简单的模式匹配,功能类似于通配符,而不完全是传统的正则表达式。你可以指定哪些文件或目录应该被 Git 忽略,以确保不必要的文件不会被添加到版本控制中。例如,如果你想忽略所有扩展名为.TXT的文件,你可以在.gitignore文件中添加规则*.TXT。如果你只想忽略特定文件夹TXT3中的secret.TXT文件,你可以写成TXT3/secret.TXT。 这样的规则尤其有用当处理敏感数据。例如,在心理学研究中,可能需要保护包含被试个人信息的原始数据不被公开。在这种情况下,指定这些文件在.gitignore文件中可以防止它们被上传到Git Hub。需要注意的是,如果敏感信息已经被上传,仅仅更新.gitignore是不够的,因为这些信息仍然存在于历史提交中。在这种情况下,应采取措施从仓库的历史中彻底删除这些信息,例如使用git filter-branch或BFG Repo-Cleaner。 总之,.gitignore文件是管理 Git 仓库中不应跟踪的文件的重要工具。它不仅帮助保持仓库的整洁,还可以避免敏感信息的不必要泄露。设置好.gitignore文件后,Git 将自动忽略这些文件,减少了手动管理的需要。如果你需要共享某些文件但不通过Git Hub,考虑使用其他直接的文件分享方法或额外的共享服务。 关于.gitignore文件中的原则,其实它的语法相对简单。基本上,你需要合理使用通配符,并注意忽略特定的文件后缀。例如,如果你想在example文件夹中忽略所有.txt文件,但保留special.txt,你可以在.gitignore文件中这样写: example/*.txt !example/special.txt 这里,*表示匹配任何字符,*.txt表示匹配任何以.txt结尾的文件。使用!前缀可以指定不忽略的文件。此外,.gitignore文件支持添加注释,你可以使用井号#来开始一行,从而添加说明或备注,例如: # 这是一个注释行 在指定要忽略的目录或文件时,可以加上斜杠/来精确匹配路径的开始部分,确保只有指定路径下的文件或目录被忽略。.gitignore文件的管理相对直观,主要依靠通配符的使用和对特定文件的排除。有许多在线资源和工具可帮助你理解和生成这类文件,因此,你不需要特别记忆所有这些规则。 例如,根据我们之前的讨论,你们可以在自己的项目文件夹中创建三个特定的文件夹,在这些文件夹中,你们应该分别放置相应的文件。完成这些步骤后,你们可以测试.gitignore文件是否按照预期工作。换句话说,你们应该检查.gitignore文件是否正确地忽略了你们希望忽略的文件,同时确保它没有错误地忽略掉任何不应该被忽略的文件。这样,你们就可以验证.gitignore文件是否实现了你们想要的效果。 一旦你成功创建了.gitignore文件,并且其中的语法是正确的,你想要忽略的文件在 Git 仓库的界面上就不会再出现了。这是因为 Git 只会显示那些被追踪(即没有被忽略)并且有更改的文件。当你将文件添加到.gitignore文件中时,你实际上是在告诉 Git:“忽略这些文件的所有变化,不要将它们纳入版本控制。” 因此,这些文件的变化不会被 Git 记录,也就不会出现在 Git 仓库的列表中。这意味着,即使这些文件发生了变化,它们也不会影响你的提交历史。 有时候,你可能会发现你想忽略的文件并没有被成功忽略,这可能会让你感到困惑。在这种情况下,你可能需要仔细检查 .gitignore文件的语法和内容。这就像是编程时遇到的问题,当我们开始使用 R 语言分析数据时,我们可能会不断遇到各种小错误。我们写了一行代码,期望它能产生特定的结果,但最终却没有得到预期的结果。在这种情况下,需要保持冷静,因为我们在编写代码时经常会遇到这样的问题,比如看到代码中出现一连串红色错误或警告。首先,要保持冷静,认真查看错误信息,仔细检查 .gitignore文件中的每一项内容。同时,也要检查自己的文件,例如,如果你期望创建的是一个 .txt 文件,要确保没有拼写错误或其他小错误。 此外,你还可以查看 Git 的缓存(cache)来了解更多信息。在你注意到 Git 似乎已经识别出文件时,这意味着还没有使用 Git add 将其纳入暂存区,或者使用 Git commit 将其永久记录到版本管理系统中。但即使如此,Git 已经识别出了这个文件,这表明它没有被忽略。你可以查看缓存区,了解 Git 记录了哪些内容,以及这些内容与你想忽略的内容之间是否存在差异。有可能是这些小的差异导致了问题。你还可以使用 Git check-ignore 命令来检查 .gitignore文件是否被 Git 正确识别。通过这些步骤,你可以逐步定位并解决问题。 总的来说,当你发现 .gitignore文件没有按预期工作时,应该耐心地检查文件语法、检查文件内容和扩展名是否有误,以及查看 Git 的缓存和日志信息。这些步骤可以帮助你识别并解决忽略文件的问题。 让我们回顾一下在 RStudio 中建立本地 Git 仓库的过程。首先,我们创建了一个工作目录,并通过创建一个新的 R 项目将其转变为一个由 Git 进行版本控制的文件夹。在这个过程中,Git 创建了一个缓存区域,以及一个 Git 仓库的内在结构。我们通过 RStudio 中的右上角 Git 窗口来记录我们的更改,并为每一次更改添加一个描述性的名字,以便将其加入到版本控制系统里面。非常重要的一点是,我们需要确保 .gitignore文件被正确编写。我们需要清楚地定义哪些文件和文件夹应该被忽略,以免它们被错误地记录到版本控制系统中。这个文件对于保持仓库的整洁和避免不必要的文件被追踪至关重要。 我们还讲述了两个相关的概念:在 Git Hub 上创建一个项目,以及在本地进行版本控制管理。版本控制管理允许我们在本地文件夹中进行更改,并确保这些更改能够同步到远程的项目库中,如 Git Hub。 由于 Git 是一个分布式版本控制系统,它允许你在本地上拥有完整的项目历史和版本控制能力。每次你进行更改时,这些更改首先会被记录到你的本地仓库中。只要你执行了 Git add 和 Git commit 命令,这些更改就会被添加到你的本地提交历史中。然后,你可以通过 Git push 命令将这些更改推送到远程仓库中,如 Git Hub。这样,你的远程项目就会保持与本地仓库同步。当你需要在另一台电脑上工作,你可以使用 Git clone 命令从远程仓库克隆出一份本地副本。这样,你就可以继续工作,并且所有的更改都会被记录下来,就像你在原始电脑上一样。这样,无论你在哪里,你都可以访问你的项目并进行工作。 总结来说,通过在 RStudio 中使用 Git,我们可以轻松地管理我们的代码和项目历史。通过正确的使用 .gitignore文件,我们可以保持仓库的清洁,并避免不必要的信息被追踪。同时,通过 Git Hub 的远程同步,我们可以确保我们的工作在任何地方都能无缝继续。 4.4 Remote Version Control 在我们了解了如何在本地使用 Git 记录更改之后,我们将这些更改推送到云端仓库的过程实际上相当简单。我们最常使用的两个命令是 Git push 和 Git pull。 首先,当你将本地的文件夹与 Git Hub 上的仓库关联后,如果你在本地进行了更改并记录在了 Git 仓库中,你想要将这些更改同步到 Git Hub 上的云端仓库时,只需执行 Git push 命令。这个命令会将本地的更改推送到远程仓库中,更新云端的数据。另一方面,如果你与他人合作的一个项目中,你在一段时间内没有进行更改,但你的合作者做了更改,并且你想将这些更改下载到你的本地计算机上,你可以执行 Git pull 命令。这个命令会将远程仓库的最新更改拉取到你的本地仓库中。 当你第一次想要将一个项目变成你自己的项目时,你需要执行 fork 操作。这意味着你在 Git Hub 上创建了一个项目的新副本。之后,如果你的合作者进行了更改,你不需要再次 fork,而是可以使用 Git Hub 提供的 sync fork 功能,它会自动将合作者的更改同步到你的已 fork 的仓库中。然后,你可以将其拉取到你的本地计算机上。如果你的仓库是个人使用的,或者是一个私人的仓库,你没有与他人合作,只是为了在 Git Hub 上备份以防止本地硬盘出现问题,那么你只需要使用 Git push 和 Git pull 这两个命令。在这种情况下,不存在与别人合作的问题。 连接本地仓库到远程仓库的过程涉及几个步骤。这里有两种常见的方法来建立这种连接: 一是从远程创建并克隆到本地:这种方法是先在远程仓库(如 Git Hub)上创建一个新的项目仓库,然后直接从远程克隆这个仓库到你的本地机器。这样,你的本地仓库就会包含所有的 Git 版本控制信息。具体步骤如下:首先,在远程仓库(例如 Git Hub)上创建一个新的项目仓库。然后,使用 Git clone 命令将远程仓库克隆到你的本地机器。克隆完成后,你的本地仓库就会自动与远程仓库连接起来。 二是将本地仓库连接到远程仓库:如果你已经有了一个本地的 Git 仓库,并且想要将它连接到一个远程仓库(如 Git Hub),你可以使用以下步骤:首先,在远程仓库(例如 Git Hub)上创建一个新的空仓库。然后,在本地仓库中,打开终端或命令提示符,并使用 Git remote add 命令来添加一个新的远程仓库。例如:Git remote add origin <远程仓库的URL>。这里的 origin 是你为远程仓库指定的一个名字,你可以根据自己的喜好选择其他名字。最后,使用 Git push 命令将本地的更改推送到远程仓库。例如:Git push -u origin master。这里的 master 是你本地仓库的默认分支名,如果你的本地仓库使用的是其他分支,请相应地替换它。 在第二种方法中,你需要将远程仓库的地址添加到本地的 Git 配置文件中。这个配置文件是一个隐藏的文件,通常位于你的用户目录下的 .gitconfig文件中。你可以手动编辑这个文件来添加远程仓库的地址,或者使用 Git remote add 命令来自动添加。 无论使用哪种方法,一旦你的本地仓库与远程仓库建立了连接,你就可以使用 Git push 和 Git pull 命令来同步本地和远程仓库之间的更改了。 创建一个新的 Git Hub 仓库并将其与你的本地 Git 仓库连接起来是一个相对简单的过程。以下是使用第三种方法(直接在 Git Hub 上创建仓库并将其连接到本地仓库)的步骤: 1.在 Git Hub 上创建一个新的仓库: 访问 Git Hub 网站。点击右上角的 “+” 图标,选择 “New repository”。为仓库命名,并选择是否使其成为公共仓库。选择 “Initialize this repository with code” 选项,如果你想要一个空的仓库,可以选择 “This is a brand new repository”(这是一个全新的仓库)。点击 “Create repository” 按钮。 2.在本地仓库中添加远程仓库: 打开你的本地 Git 仓库所在的目录。在终端或命令提示符中,使用 Git remote add 命令来添加一个新的远程仓库。例如:Git remote add origin <远程仓库的URL>。这里的 origin 是你为远程仓库指定的名字,你可以根据自己的喜好选择其他名字。 3.将本地更改推送到远程仓库: 确保你的本地更改已经添加到暂存区,并提交到本地仓库。使用 Git push 命令将本地的更改推送到远程仓库。例如:Git push -u origin master。这里的 master 是你本地仓库的默认分支名,如果你的本地仓库使用的是其他分支,请相应地替换它。 在创建新的 Git Hub 仓库时,Git Hub 会提供一个新的 SSH 密钥或 HTTPS URL,你需要在本地仓库中使用这些信息来建立连接。如果你在创建仓库时选择了 “Initialize this repository with code”,Git Hub 会自动为你创建一个 README 文件,你可以随时添加其他文件或目录。 一旦你完成了这些步骤,你的本地仓库就会与 Git Hub 上的远程仓库连接起来,你就可以使用 Git push 和 Git pull 命令来管理本地和远程仓库之间的更改了。 当你在 Git Hub 上创建一个新的仓库并且选择了一个空仓库(“Initialize this repository with code”)时,Git Hub 会自动提供一个初始化的仓库地址。这个地址通常是一个 HTTPS URL,它看起来像这样: https://Git Hub.com/username/repository.git 这个 URL 就是你在本地 Git 仓库中添加远程仓库时需要使用的。你可以在创建仓库后的 Git Hub 页面中找到这个 URL,通常在仓库的设置或者仓库的 README 文件中的 “Code” 部分。 通过这个过程,你就可以将你的本地 Git 仓库与 Git Hub 上的远程仓库连接起来,并且可以开始进行代码的推送和拉取操作。 当然,你需要确保了解本地仓库的路径,因为这将决定你在本地创建的文件夹位置,以便与之对应的远程仓库能够正确地与之连接。一旦你确定了本地仓库的路径,你就可以轻松地将它与远程仓库连接起来。在创建远程仓库之后,你可能会发现本地仓库已经包含了一些文件,而 Git Hub 上的远程仓库却是空的。这种情况下,你需要将本地的更改记录到本地的 Git 系统中,并将其推送到远程仓库,以实现两者的同步。虽然这个过程听起来可能有些抽象,但实际上,只需要执行两行代码就可以完成操作。 那么,我们现在来详细讲解最关键的一点。实际上,核心要求就是希望各位同学能够执行一个操作:即将我们课程的课件fork到你个人的repository中。对于选修本课程的同学来说,有两个重要时刻会用到这一操作:首先,在完成第一次小作业时,你需要对课件进行修改并上传至你的仓库;其次,在提交大作业时,也必须将其提交至Git Hub仓库。此外,在课程进行期间,你需要不断地更新你的仓库。 关于课程的具体操作,我们已将网址发送给各位,你在登录个人的Git Hub账号后,可打开该网址,并会发现有一个“fork”的标志,点击即可完成操作。当你需要提交小作业时,你需要对Git Hub仓库进行一些更改,并将修改后的文件保存在你的远程Git Hub仓库中。具体来说,你需要在本地进行修改,并将更改push到远程仓库,随后再回到我们课程的中心仓库。例如,在浏览我们的仓库时,你可以获得该仓库的地址,并在页面上看到你的仓库来源。点击“code”,你会看到对应的地址,复制该地址后,在RStudio中粘贴,并使用此地址而不是新建文件的地址,同时为其命名并指定位置。此时,RStudio会帮助你将文件克隆到本地。 那么,当你完成了一定的内容并准备提交作业时,你需要在本地进行提交。打开Git界面后,它会提示你进行一些提交(commit)的操作。完成提交后,你需要进行推送(push)操作。如果一切顺利,你会看到类似这样的提示,表明你的更改已经被成功地推送到了远程仓库。 那么,如果你已经在线上了,也就是说在下一节课之前,我们作为更新助教团了下节课的课件,那么在你自己的repository中,你会看到一个提示,表明你的repository与中央repository之间存在差异。这是因为我们维护的中央仓库是官方的,当我们更新了课件,你的forked仓库与中央仓库之间就会产生分歧。 这种差异的出现是因为我们更新了下一节课的课件,而你还没有将这些更改合并到你的仓库中。为了解决这个问题,你只需执行一个同步操作,即点击“Sync fork-Update branch”按钮,Git Hub会自动帮你下载最新的更改。更新完成后,你可以使用“pull”命令将更新从你的forked仓库中拉取下来,这样你的本地仓库就会与中央仓库保持一致了。 当你在个人仓库中进行了更改,比如提交了一个作业,中央仓库中并不会自动显示出这些更改。这是因为中央仓库的官方版本并未包含你所做的修改。如果你希望将你的作业整合到中央仓库中,你需要发起一个pull request。具体操作是,你在Git Hub上找到你想要贡献代码的中央仓库,然后点击“New pull request”或者“Create pull request”按钮。在这个 pull request 中,你可以选择你的分支,并描述你的更改内容以及目的。提交后,其他贡献者或维护者可以审查你的代码,并决定是否将其合并到中央仓库中。这个过程是开放的,允许社区成员进行讨论和反馈。 如果助教在中央仓库中对第三章进行了更改,并创建了一个pull request,那么这些更改会被提交给你或其他相应的管理员进行审查。在Git Hub上,管理员会看到一个pull request的请求,他可以查看助教所做的更改,包括更改的细节、差异以及可能的影响。如果认为助教的更改是合适的,并且没有问题,就可以进行合并(merge)操作。 接下来我们给大家演示一下如何操作。由于我使用的这台电脑主要是用于教学,所以我通常是现场安装软件。因此,大家可以看到它处于一个非常原始的状态。在我们安装Git时,通常会一路点击“Next”按钮,无需进行其他设置。当然,安装过程中会有一些可选功能,比如将Git添加到Windows Timer,但这对于我们来说并没有太大用处,因此我们选择忽略这些选项。 安装完成后,我们可以关闭RStudio,然后重新打开它。大家可以看到,在我们刚才打开RStudio时,并没有看到Git相关的窗口,但现在已经有了。这个小的标签页显示了Git Hub的信息,这意味着Git Hub已经正确地被识别并配置好了。如果在不同的电脑上出现不同的表现,这可能是因为安装Git后没有正确地识别。为了解决这个问题,你可以在安装Git后退出R和RStudio,然后重新打开,看看是否能够解决问题。 现在,假设我们要fork一个Git Hub仓库,我们可以使用助教的账号登录,并尝试fork一个仓库。即使已经fork过,我们仍然可以看到“Fork”按钮是可以点击的。但是,如果你直接点击“Fork”,系统会要求你选择一个用户来作为该仓库的拥有者。如果你尝试选择自己的Git Hub账号,会发现无法选择,因为已经存在一个fork。在这种情况下,我们可以采取另一种方式进行演示,比如使用R4psyBook,这是我们课程的文字稿。在这种情况下,我们可以点击“Fork”按钮,通过这样的步骤,我们可以顺利完成fork操作,并继续进行版本控制的相关工作。 大家可以看到,在Git Hub上,我们有这个选项“o”,也就是我们自己的账号。即使我们使用与助教账号完全相同的名称,也不会遇到任何问题。此时,页面提示我们“Copy the main branch only”,这意味着我们只需复制主分支。由于我们的仓库不存在多个分支,因此选择这个选项并不会造成影响。 很快,fork操作就完成了。现在,我们需要将这个仓库的内容克隆到本地的文件夹中。对于一个新的项目,你可以按照以下步骤操作: 首先,由于我的电脑有些老旧,可能性能会有所影响,操作看起来不太正常。这里显得有些慢,我们可以尝试关闭并重新打开,以检查是否会有所改善。接下来,我们选择“Version Control”选项,然后从已有的仓库中复制。此时,我们可以看到,在我们复制后的仓库中,这里会有一个绿色的“Code”标签。点击“Code”,我们可以看到有几种克隆方式,包括HTTP、SSH,以及Git Hub。由于我们目前使用的是网络版RStudio,通常只需要使用网页版Git Hub即可。 大家可以看到,后面有一个复制的图标。点击复制后,我们将链接粘贴到本地仓库的位置。例如,在这里,我可以选择将新的文件夹创建在“文档”文件夹下,这样就完成了新仓库的创建。通过这样的步骤,我们可以顺利地将Git Hub上的仓库克隆到本地,并在RStudio中进行后续的操作。 首先,我要提醒大家,由于我使用的这台电脑主要用于教学,所以我们会现场安装软件。请大家看到这里很原始的状态。在安装Git时,我们通常会一路点击“Next”按钮,无需进行其他设置。当然,安装过程中会有一些可选功能,比如将Git添加到Windows Timer,但这对于我们来说并没有太大用处,因此我们选择忽略这些选项。 安装完成后,我们可以关闭RStudio,然后重新打开它。大家可以看到,在我们刚才打开RStudio时,并没有看到Git相关的窗口,但现在已经有了。这个小的标签页显示了Git Hub的信息,这意味着Git Hub已经正确地被识别并配置好了。 现在,假设我们要fork一个Git Hub仓库,我们可以使用助教的账号登录,并尝试fork一个仓库。即使已经fork过,我们仍然可以看到“Fork”按钮是可以点击的。但是,如果你直接点击“Fork”,系统会要求你选择一个用户来作为该仓库的拥有者。如果你尝试选择自己的Git Hub账号,会发现无法选择,因为已经存在一个fork。 在这种情况下,我们可以采取另一种方式进行演示,比如使用AlphaSealBook,这是我们课程的文字稿。在这种情况下,我们可以点击“Fork”按钮。接下来,我们需要将这个仓库的内容克隆到本地的文件夹中。对于一个新的项目,你可以按照以下步骤操作:选择“Version Control”选项→从已有的仓库中复制→复制链接后,粘贴到本地仓库的位置→在本地创建一个名为“homework”的文件夹→“homework”文件夹中,新建一个以学号最后三位和姓的拼音首字母命名的txt文件。 完成这些步骤后,你已经对本地文件夹进行了更改。在这个过程中,你可能会遇到一些问题,比如网络连接不稳定或者安全通道问题。对于这些问题,Git Hub提供了详细的文档和中文介绍,指导你如何在本地的文件夹和Git Hub之间建立一个安全的连接。 最后,如果你在提交作业时遇到任何问题,可以将错误信息截图发到群里,以便其他人帮助你解决问题。 4.5 作业 "],["第四讲如何导入数据.html", "Chapter 5 第四讲:如何导入数据 5.1 正式开始前的Tips 5.2 回顾与问题 5.3 数据导入 5.4 读取数据 5.5 数据类型 5.6 数据结构 5.7 数据索引 5.8 对象(object) 5.9 其它注意事项 5.10 final project(期末作业)", " Chapter 5 第四讲:如何导入数据 5.1 正式开始前的Tips 今天我们正式开始使用R语言来接触我们的数据。我们之前做了很多准备工作,我看这两天,绝大部分人已经提交了第一次作业。我不知道是否还有人没有提交。从大家的完成情况来看,绝大部分人还是可以的。大家可能会在GitHub的登录这个地方出一点小问题,但绝大部分都能够解决。大家应该也可以获得最新的课件,上课的所有代码都可以通过GitHub同步。既然我们要开始接触数据,这是有可能会在开始使用R之前遇到问题。例如,去年上这门课的时候,有同学遇到一个问题,就是报错时出现乱码,无法被识别出来,看不到具体内容。大家可以把自己的报错语言设置为英文,这样就不会出现乱码了。也就是说,如果你报错时使用的是系统默认的语言,有可能是中文,那么中文输出时可能会有一些乱码无法被识别出来。 # set local encoding to English(将当前系统编码设为英文) if (.Platform$OS.type == 'windows') { Sys.setlocale(category = 'LC_ALL','English_United States.1250') } else { Sys.setlocale(category = 'LC_ALL','en_US.UTF-8') } # set the feedback language to English(将报错语言设为英文) Sys.setenv(LANG = "en") 另外,在整个课程中,我们推荐大家使用一种方式来装载自己的包,就是使用叫做pacman的管理包。使用这个p_load,它会帮助你自动安装那些你没有安装的包。所以我们现在推荐大家使用这个包,非常方便。 if (!requireNamespace('pacman', quietly = TRUE)) { install.packages('pacman') } pacman::p_load(bruceR) 5.2 回顾与问题 之前提到,R作为一个工具,是帮助我们解决数据分析问题的关键。我们需要将R整合到,或者说使用R来完成我们整个数据分析的流程。实际上,在一个正常的数据分析流程中,第一步应该是获得数据。获得数据的途径在不同情境下是多样的。对于我们心理学的同学来说,大部分人获得数据的方式是通过自己做实验、发问卷来收集数据,然后数据通过问卷平台或实验收集工具将这些数据收集起来。在其他情况下,获得数据的方式可能不同。比如说,大家毕业后,大家可能会去不同的地方工作,那时你们遇到的数据可能不是通过实验收集的,而是需要通过网络或其他方式来收集,或者是别人提供的一些杂乱无章的数据。所以在拿到数据之前,可能还需要一些额外的步骤,比如通过网络爬虫来爬取数据,这是一种可能的数据获取方式。现在假设大家手头上已经有一些数据了,那么拿到数据后,接下来就是如何将其导入到数据分析软件中。大家在做本科毕业论文时,应该都使用过SPSS,那么SPSS是如何导入数据的呢? 我们其实也可以试一下,在RStudio里面我们是不是也可以用这种方式。比方说我们现在看到的这个R语言的RStudio界面,它这里有一个文件(file)选项,可以选择导入数据(import dataset)。这里有很多个选项,比如从Excel、SPSS、SAS和Stata等不同的软件里面导入数据。比如说,我们假定大家用的是txt格式的数据,然后我们现在想导入其中的一个数据。我们用这个人类企鹅计划(Human Penguin Project)的数据,我们实际上是可以做这个操作的对吧。大家可以看到这个时候,因为我们的数据文件是TXT格式,然后通过逗号进行分隔的,上面有输入(input),每一列对应着数字。这个时候,系统会询问你这个文件是否包含头文件(heading),也就是列名称(column name)。如果你选择“否”(no),系统会默认给每一列加上V1、V2、V3等列名,一直到V5,将第一行数据作为数据行处理。但如果你选择“是”(yes),系统会将第一行识别为列名称(column names),这样你的第一行就变成了列标题(headings),这里默认使用逗号进行分割。理论上,你可以直接这样导入数据,数据就会导入进来。这种情况可能让人觉得不需要学习代码。 我们再来看另一种情况,如果你遇到有20、30个的实验数据,一个个点显然是不现实的。所以我们首先可以看到,刚才我们点击导入数据后,系统会在控制台自动显示一行代码。它会告诉我们,我们刚才的点击操作实际上是在做什么。这样,系统自动地写出了一行代码,即使用文件名称作为一个变量,然后使用read.csv函数将其读入。这节课的主要目的就是要理解R这行代码是如何工作的。我们不仅要用鼠标点击,还要理解这个命令,然后让我们更加熟练地把它应用在更广泛的情形下。比如,当我们有很多个文件需要导入时,我们应该怎么办。 我们之前提到,在整个课程中,我们会使用两个数据集:一个是人类企鹅计划项目的数据,另一个是认知实验的数据。比如我们刚才通过点击操作导入的数据,然后查看数据的大致结构。那么,我们如何通过代码来完成这个操作?包括我们是否能尝试选择一些变量进行初步的统计,这就是我们在数据分析中遇到的第一个问题。这节课的主要内容就是解决数据导入这个问题。 5.3 数据导入 5.3.1 数据的“住址”——路径 (以Mac系统为例) 要解决这个问题,如果我们回到刚才的点击操作,我们会发现read.csv函数中有一串很长的字符,这串字符代表了导入数据时使用的一些参数和选项。在读取文件时,R语言需要知道文件所在的路径。当我们通过点击的方式打开文件时,会弹出一个窗口,我们可以一层层地选择文件夹。但当我们写代码时,我们需要自己找出文件夹或文件所在的位置,并使用代码来指定这个路径。这就涉及到了一个重要概念,即数据所在的路径,或者可以说是数据在电脑中的“住址”(address)。我们需要告诉R语言从哪里找到这个数据,如何定位它。 如果我们查看文件夹结构,在我们的R4Psy的文件中,它里面有一个名为“data”的文件夹。在这个“data”文件夹里面,有一个名为“penguin”的文件夹。我们想要找到的CSV文件就位于这个“penguin”文件夹内(见下图)。 也就是说,将这个顺序形成一个路径,它基本上可以说是“data”的一个斜线(或斜杠),然后是“Penguin”,接着是“Penguin”文件夹中的“rawdata.csv”文件。(“data/penguin/penguin_rawdata.csv”)但是,跟我们前面看到的在R里面显示的路径相比,它好像更长一些。为什么会更长呢?因为对于这个数据而言,它的完整路径是非常长的。这里,大家可能会了解到,不同的操作系统对文件的挂载或者说文件夹的组织方式是不一样的。在Mac系统下,它只有一个盘,在这个根目录下有几个文件夹,比如“users”——用户文件夹。然后在这个“users”里面,有一个以你自己的用户名为文件名的文件夹。在这个用户文件夹里面,通常会有一个名为“Documents”的文件夹。如果你是mac用户,那么绝大部分默认的文件和文件夹都会放在这个用户名下的“Documents”文件夹里,或者放在一个名为“home”的文件夹下面。然后,你再向下层层递进进行组织。比如说,我们把文件放在user文件夹下面自己的document的文件夹中,然后我们自己又起了个名字“Github”。想把所有的相关文件夹都放在里面。在这里面,就有一个名为“R4Psy”的文件夹,就是我们直接从“GitHub”里面克隆下来的。然后在这个“R4Psy”文件夹里面,就有我们刚才提到的完整路径。 文件我们电脑上的一个完整路径,实际上是从硬盘的根目录开始的。这个路径会包括从根目录开始,一直到我们想要访问的文件为止的所有文件夹和子文件夹。比如,“/Users/cz***/Documents/github/R4Psy/data/penguin/penguin_rawdata.csv”。在Mac系统下,我们通常只有一个硬盘,不会分多个盘。所以在这个硬盘的根目录下,就直接是目录结构,不会有盘符的概念。在这个根目录下,你会看到“用户”(Users)文件夹,然后是你的用户名字。这样一来,完整的路径就会变得非常地长、而且非常地复杂。 5.3.2 绝对路径/相对路径 这里涉及到两种展现路径的方式:(1)一种是“R4Psy”文件夹里面去看我们的数据所在的位置,我们称之为相对路径。相对路径是相对于你当前的工作目录(或父文件夹)来说的。你所在的这个父文件下可能有很多子文件夹,比如“data”、“homework”等。实际上,我们是相对“R4Psy”这个文件夹来说的。 例如,相对路径: “data/penguin/penguin_rawdata.csv” 或”./data/penguin/penguin_rawdata.csv”(“./”表示当前所在文件夹)。 (2)另一种是绝对路径,它是相对在整个硬盘中的位置。从硬盘的根目录开始,一直到文件所在的具体位置的完整路径。例如,“/Users/cz***/Documents/github/R4Psy/data/penguin/penguin_rawdata.csv”。 对于第一次接触代码和路径的同学来说,可能需要练习如何找到文件夹的相对路径和绝对路径,并且能够区分这两者。在Mac系统下,因为通常只有一个硬盘,会比较好找。使用相对路径会比较方便,因为你可以忽略掉前面的路径,只关注当前工作目录下的文件位置。如果我们把所有东西都放在一个特定的文件夹里面,比如“R4Psy”,并且在这里面保存和读取文件,这个文件夹就可以称为工作目录(working directory)。在英文中,这个工作目录也可以被称为“parent folder”,即父文件夹。 在R语言中,你可以使用getwd()命令来查看当前的工作目录是什么位置,以及使用setwd()命令来设置一个新的工作目录路径。此外,在RStudio中,你可以在“全局选项”(Global Options)中进行设置,指定每次打开RStudio时自动打开的文件夹作为工作目录。你可以在RStudio的设置中进行修改,选择一个合适的文件夹作为默认工作目录。需要注意的是,文件路径和文件夹名称应避免使用中文,因为不同的操作系统和编码方式可能会导致兼容性问题。建议使用拼音或英文来命名文件夹和文件,以确保跨平台的一致性和可靠性。比如说,将“学习”文件夹命名为“courses”或“learning”,然后在后面加上“r”,比如“learning_r”,这样每次打开RStudio时,它会自动打开这个文件夹。 (图. 在全局模式中设置默认工作目录) 大家可能不太明白我在说什么,那我们操作一下来演示。我们现在有一个RStudio窗口,如果我们想打开一个新的RStudio窗口,可以在session里面打开一个新的窗口。这时我们使用刚才提到的getwd()命令看看我们现在所在的工作目录是哪里,发现它还是在之前的路径里面。有可能虽然打开了一个新的窗口,但实际上它还是在原来的项目里面。我们可以把这个整个项目关掉(file-close project),关掉之后再查看当前工作目录,这个时候它就已经不再是之前的目录了。我们打开两个RStudio窗口进行对比一下。一个是在Onedrive里面,另外一个打开的时候,它是在用户名文件下的一个默认文件夹,即我的用户名加上“Documents”。这个地方就是我们之前提到的可以在全局变量中修改的默认工作目录(default working directory),在R的通用设置(general)里面。一般我们可能不会修改它,它默认就在用户名下面的“Documents”文件夹里。 大家也可以看到,我刚才打开了一个名为“R project”的项目,它就自动打开了这个项目所在的文件夹。这就是我们之前提到的,当大家使用GitHub上的RStudio仓库时,里面会有一个以“.rproj”结尾的文件。大家要使用GitHub上的仓库最方便的方式就是直接点击那个仓库(repository),选择打开方式为Rstudio。 打开RStudio时,比如在“R4Psy”这个文件夹里,有一个以“.rproj”结尾的文件。这个文件表示它是一个RStudio的项目文件,它把“project”后面的几个字母缩掉了。如果我们现在关闭所有的RStudio窗口,然后直接打开这个项目文件,它会将当前的文件夹设置为工作目录,这是一种很方便的方式。如果你有一个固定的项目,你可以在该文件夹中创建一个“.rproj”文件,这是最方便的方式。 我们刚才讨论的是在Mac系统下的情况。实际上,在Windows系统下,路径通常是可见的,你可以通过在文件夹上面点击来显示完整的路径(如下图)。 在这里需要注意一点,就是斜杠(/)的使用。在Windows系统中,路径是按照逆时针方向组织的,即从左到右();而在Mac和Linux系统中,路径是按照顺时针方向组织的,即从右到左(/)。如果你直接复制了Windows下的路径并粘贴到Mac或Linux系统中,可能会出现问题。因此注意不要直接复制Windows地址栏中的地址。 为了解决这个问题,你也可以通过某种方式,比如使用normalized_path,来转换路径格式。 ###### Run in WinOS !!! ##### # r语言中的地址 first_path <- getwd() cat(first_path,"\\n") # Windows的地址 normalized_path <- normalizePath(first_path, winslash = "\\\\") cat(normalized_path) 上面这种方式是将其反转,即将逆时针方向的斜杠转换为顺时针方向。另外,我个人使用较多的一种方式是直接让代码处理这个问题,而不去关心斜杠的方向。这实际上是一个字符识别的问题,无论是顺时针的斜杠还是逆时针的反斜杠,都可以让代码来处理。我通常使用一个名为here的包来处理路径问题。例如,在打开项目后,我可以直接使用here包来查看。 pacman::p_load(here) # here::here() here::here("data","penguin","penguin_rawdata.csv") [注:在Python及大多数编程语言中,路径都支持斜杠(/)] 在R中,两个冒号(::)表示包里面的一个函数。在这个包里面,它会找到名为data、Penguin的数据,然后将其所有的路径连起来,不管你在Windows、Mac还是Linux系统上,它都会自动找到正确的路径。因此,你不需要手动编写完整的和正确的路径。 5.3.3 设定工作目录 – 手动挡与自动挡 手动档 通常,我们在编写代码时,会手动设定自己的具体路径。例如,使用setwd()函数,或者在Windows系统上使用两个反斜杠(\\\\)。还有一种方式就是使用here包。还有一种方式是直接使用Rproject来设定你的工作目录。我推荐使用Rproject,因为它非常方便。 使用 setwd输入具体路径,例如: # setwd('D:/R4Psy/data/penguin/') # or # setwd('D:\\\\R4Psy\\\\data\\\\penguin') 半自动档 另外,还有Brucer这个包,它提供了一个半自动的方式。你可以在Brucer的WD函数中加上一个参数argument,值为ask = T,它会弹出一个交互窗口,让你选择你希望哪个文件夹作为你的工作目录。 # 两个函数等价,ask = T设置弹出交互式窗口选择文件夹 bruceR::set.wd(ask = T) bruceR::set_wd(ask = T) 手动点击 还有一种方式是直接手动点击RStudio窗口中的齿轮图标,进入设置,然后选择“复制”(Copy)或“打开”(Open)等功能,手动设置工作目录。这种方法是可行的,但我们不太推荐。在RStudio的设置中,有许多选项,你可以选择将当前文件夹的目录复制起来,或者打开文件夹等。很多时候,我们使用代码能够完成的事情,RStudio现在也通过点击的方式帮助我们实现。当然,我们的目的还是让大家能够用代码来编写。 我推荐将所有的数据、代码等放在一个工作目录下。这样,你后面读取时只需读取相对路径,不需要读取绝对路径。这种做法的好处是,你可以随意复制粘贴这个文件夹。比如,你在C盘底下创建了这样一个工作目录,然后你所有的代码都放在这个工作目录中,使用相对路径。当你把这个文件夹复制到D盘、E盘,或者复制到另一个同学的电脑上时,你打开这个文件夹,他还是可以继续读取这个相对路径里面的数据,不会出错。如果你写的是绝对路径,那么当你换一个地方时,这个绝对路径前面的部分就会发生变化,这会导致找不到文件。 5.4 读取数据 5.4.1 读取数据——手动档 OK,我们前面演示了如何通过点击的方式读取数据。那么我们自己能不能直接写代码来解决这个问题呢?其实也不难。我们给它命名为“penguin_data”,然后使用import函数,使用here包这种方式。这个import函数是我们之前提到的一个函数,用于读取特定目录下的文件。在这里,我们实际上使用的是Bruce包中的一个import函数。在点击导入时,我们也在RStudio中看到了它自动给出的是读取CSV(read.csv)函数。我们将完整的绝对路径写出来,然后它可以通过这个函数来读取CSV文件,并将其导入到我们的环境中。 # 读取数据,命名为penguin_data penguin_data = import(here::here('data', 'penguin', 'penguin_rawdata.csv')) # 查看头(head) 5 行,有头就有尾(tail) # head(penguin_data,n = 3) # tail(penguin_data,n = 3) 我们使用的这个函数是Brucer包中的import函数。为什么要用这个呢?因为它非常好用。read.csv函数只能读取CSV文件,或者类似的以逗号、点、或空格分隔的TXT文件。但是对于SPSS保存文件或Excel文件等,它可能无法读取,因为read.csv是专门为读取CSV文件而开发的函数。 在BruceR包中,它会整合多个读取文件的函数。它会根据文件的后缀和内容自动识别最适合的函数来读取文件。因此,我们通常推荐心理学同学直接使用BruceR包的import函数。至于路径,我们刚刚说到的,最好使用相对路径的写法。而且,使用here这个包可以避免一些小细节的问题,比如决定是使用两个反斜杠还是一个正斜杠这样让人头疼的细节。读取数据之后,我们实际上可以对它进行一些基本的查看,比如查看有多少行和多少列。 查看有多少列(column)、多少行(row): ncol(penguin_data) nrow(penguin_data) 我们通常使用一个已经加载的R包中的函数来进行这些操作。在上节课,我们讲过R包,即R里面有很多函数。这些函数的来源有两个:一个是base包提供的一些函数,另一个是各种各样的package包提供的函数。当我们需要使用某个特定包中的函数时,我们首先需要加载该包。我们使用library函数,后面跟上包的名字,这样就可以将函数加载出来。理论上,一旦加载了一个包,我们就可以使用该包中的所有函数。这是我们上节课涉及到的内容。 当我们已经import了某个包,例如BruceR包以后,该包中的所有函数都可以直接使用。因此,我们就可以直接这么写bruceR::import()可以简写为import()。我个人推荐,当涉及到使用较多的包时,最好将包的名字加到函数前面,而不是省略。这样做的好处是,当你写了几百行甚至上千行代码,并且使用了十几个包之后,你可能记不清某个函数是从哪个包中来的。你可能在编写时非常清楚,因为你可能几分钟前刚刚从搜索引擎或大语言模型中找到了这个函数。但可能半年后,你可能已经不记得了。通过这种方式,你可以知道每个函数是从哪个包中来的,这对于检查代码的可重复性性是非常有帮助的。 读取完数据之后,我们可以在R的environment环境中看到我们刚刚读取的数据。当我们给读取的数据打上标签时,这个标签通常是我们在之前定义的,例如“penguin_data”。这样,我们就可以在environment中找到它。如果我们回到刚才的操作中,假设我们刚开始打开的是一个空的environment,里面什么都没有。然后我们可能想要通过刚才介绍的方式去读取数据。考虑到我们仍然使用一个相对简单的方式来读取数据,我们仍然直接使用import函数。只要我们通过任何方式读取数据后,它都会出现在environment中。在开始时,environment是空的,但现在出现了一个名为“data”的目录,在“data”目录下有一个名为“penguin_data”的数据集。那么我们就相当于地将数据从我们的硬盘里读取到了R里面。这个过程相当于是我们在SPSS中打开Excel文件,并将数据读取到SPSS中。但是,在R读取之后,它不像SPSS那样直接显示所有数据。有时候,你可能无法立即看到数据。如果我们仔细看一下刚才的代码,实际上有两行。第一行是读取数据的代码,第二行是view函数,用于查看数据。当我们自己编写代码时,直接使用bruceR包的import函数来import某个CSV文件,它只会告诉我们已经import了数据,我们可以观察到的变化是在右上角的environment中的变化。 如果我们想要查看数据,我们必须要使用两种方式之一:(1)一种方式是手动的方式,即点击环境(environment)中的数据,鼠标放在数据上时,它会变成一个手形图标,点击一下就可以打开数据。(2)另一种方式是,当你点击了数据之后,RStudio会将view函数的代码显示出来。这有点像在SPSS中查看数据,你在这里面可以看到每一行有多少个数据,以及一些基本的数据。但这里有一个不同,你可能无法直接点击某个操作栏来进行数据的操作。在这种情况下,你必须理解数据是如何存放在R中的,以及R中有哪些数据,然后通过什么代码去操纵这些数据,以及进行什么样的统计分析。 例如,我们在PPT中显示了查看数据的基本信息。我们查看读取后的数据,看看它是否与我们的预期完全一样,行和列的信息是否对得上。我们在environment可以看到这个数据有1,523个观测值(observation),有247个变量(variables),这些变量在R中实际上相当于有247列。再比如,当你收集数据时,你收集了100个人,但读取出来时只有88个人,那可能就是某个地方出了问题。有时候会出现这种情况。 在R中,查看数据的一个常用方法是使用head()函数,它查看数据的前几行。现在RStudio的一个好处是,当你想要读取某个存在于环境中的变量名时,你输入前几个名字,它就会自动给出可能存在的变量名。然后你按下Tab键,它就会自动输入完整的变量名。 由于我们有247个列,只看head()也没什么太大意义。我们后面会告诉大家使用一些更有效的包来直接对这种较大的数据进行快速的探索。这就是我们最基本的读取数据的方法。 5.4.2 读取数据——自动挡(GUI点击操作) 这个地方就是我们前面说的,你自动去GUI点击操作。我们刚刚已经讲过了,它实际上跟代码基本上是一一对应的。 5.5 数据类型 那么我们读取到了这个数据后,接下来想要做什么?肯定不可能读取完了就完了,我们肯定要对他进行一些操作。操作的话,你就要知道,这个我们到底读取的一个什么东西。所以这个时候你可能需要了解一点,关于r里面如何存取数据的一些最基本的知识。那么这里面就会涉及到数据的类型。比方说我们在Excel里面处理数据的时候,我们会有一些,比方说它到底是常规的还是数字类型,还是货币,还是日期等等。那么在SPSS里面的话,我们应该也有不同的这种数据类型,到底是数字还是字符串,等等。实际上,为什么大家喜欢用SPSS,就是它跟我们心理统计上讲离散型数据、连续型数据都差不多。R里面它也有自己的规则,我们接下来可能会讲一下它的规则。 在这个我们刚才说了,就是说把数据导入给进去之后,我们就要对数据进行操作对吧。既然要操作这个数据的话,我们肯定要知道这个数据它有什么样的特点。那么在R里面的话,其实我们可能会有两类基本的数据类型。一个就是最简单的,这种叫做所谓的基本的数据类型,它非常简单的不能再简单的,就单个的东西,它不是由多个元素组成,它就只有一个元素。我们可以直接在这个Rstudio里面进行操作。比方说,在R这个命令窗框里面,你可以把R当做一个简单的计算器,直接在R这个窗框里面进行简单的一些运算,比方说,输入123乘以5,R就可以给你返回一个数字结果。那么其实这里面的123和5都是一个单独的一个元素,那么R会把它识别为一种特殊的数据类型,就是数字。 数值类又包括两种类型,一个就是包含小数点,一个是不包含小数点。包含小数点的类型在其他计算机语言里又被称之为浮点数据,叫double。然后还有一个就整形的数据,就是像12345678没有小数点的这种叫做integer。然后另外的话,单个的这个数字它有可能是一个字母,或者一个特殊的符号,那像这种的话,就是一般在其他的计算机语言里面可能叫string(字符串)。在R里面,字符串数据类型叫做character。然后是逻辑值。看它是“是”还是“否”。 使用class()函数可以查看数据类型。例如,查看当前工作目录的数据类型,其中,getwd是返回当前工作目录: #getwd 返回当前工作目录,返回的数据类型是? class(getwd()) 再例如,查看导入的penguin_data的数据类型: # 查看导入数据的类型 class(penguin_data) 结果显示:[1] “data.frame” 字符串的数字与数值型的数字,区分的关键在于引号的使用。加了引号就表示是字符串的数字(例如,“1”),而没有加引号的则带包数值型的数字(例如,1)。 # 字符串的数字与数值型的数字; # 注意区别== 与 = class('1' == 1) 在R语言中,==和=是两种不同的操作符,分别用于不同的目的: (1)==:这是比较操作符,用于比较两个同类对象的值是否相等。如果相等,返回TRUE,否则返回FALSE。例如: #判断1和2是否相等。 1 == 2 (2)=:这是赋值操作符,用于将一个值或表达式的结果赋给一个变量。例如: #`x`被赋值为10,`y`被赋值为"text"。 x = 10 y = "text" 总的来说,==用于比较,而=用于赋值。在编写R代码时,需要根据上下文正确使用这两个操作符。 5.6 数据结构 我们的数据很多时候是有很多不同的类型的。不同数据类型组合了之后,就会形成R里面比较复杂的一个数据结构。大家可以这么理解,如果数据类型由单个元素构成,那么就相当于是把R当一个计算器使用了,因为只需要再单个元素之间进行相互比较和运算。但是,如果有更加复杂的数据结构之后,我们就可以同时对各种组合的数据进行存储和运算。这样才能够满足我们对于比较复杂数据处理的需求。那么我们这里的在R里面,经常会碰到的几种复合的这种数据类型呢?就是向量(Vector)、矩阵(Matrix)、数组(Array),还有数据框(dataframe),我们刚刚看到的数据penguin_data就属于是dataframe。最后一个就是列表(List)。 在向量(vector)型数据中,逻辑型(logical)、字符串(character)、数值型(double和integer)这些称之为原子(Atomic),即基本的数据类型。把完全相同的基本的数据类型放到一起,多个元素叠加到一起,就形成了一个Vector(向量)数据。比方说,多个数字放在一起,就形成了一个数字向量;多个字符放在一起,就形成了一个字符向量。那么我们这个很多向量,它可以通过各种其他的方式进行组合,就形成我们更加复杂的这种数据结构。然后我们把有多个向量在一起的时候,它就可以形成一个矩阵(Matrix)。然后多个矩阵放到一起的时候,它就会形成一个数组(array)。 数据结构里面的每一个元素,都是用同一个颜色表示,这就意味着它在数据类型上面是属于同一个类型。一个向量里,每一个元素都是相通的。比方说,第一个元素是数值的话,那么第二个元素也必须是数值的,它不能说是一个字符的,它可以是逻辑的,它可以是强行转成一个数值的。这里面也涉及到字符的”1”和数字的1返回相等。像R这种高级的计算机语言,它会在内部会做一些自动化的处理。那么有时候这个自动化的处理是有帮助的,但有时候需要去避免这种自动化处理带来的问题,因为自动化处理有可能并不符合你的要求。 (图片来源: http://venus.ifca.unican.es/Rintro/dataStruct.html) 这里的向量(Vector)和矩阵(Matrix)和数组(Array),它都必须是同一个数据类型的。然后呢,它组成了这种不同的比较复杂的结构之后,它就形成了这样的一个类型。在dataframe数据中,每一个列使用的颜色不同(见上图)。这就表示每一个列可以是不同的数据类型。例如,第一列可以是数值,第二列可以是字符串,第三列可以是其他类型的。这样给我们提供了很大的自由度,有点相当于Excel。在Excel里,比方说,一列是数字,另一列可以是完全不同的东西。所以,这其实给我们提供了很大的一个灵活处理数据的自由度。列表(List)是更加灵活的一个数据类型。我们会发现,很多模型的结果,特别是那种比较复杂的模型的结果,一般都是以这种列表的方式来存储的,因为list里面可以存储各种各样很复杂的信息,也可以把我们的数据完全装进去。 我们最常碰到的这个数据类型是Dataframe,dataframe数据应该是在心理学研究当中最常出现的,也是最符合心理学研究的实际需求的。我们可以查看数据的每一行每一列的元素是什么,比方说,变量”anxiety”实际上就是不一样的一个数据类型,它属于有小数点的数值类型。当然我们这里还可以比方说,在我们自己处理的时候,我们可能还有被试编号,被试编号如果说我们是字母加数字的组合,那么这个时候它就变成了一个字符串了。 当处理复杂的数据时,有时候不仅仅需要知道数据类型是什么,还要把它提取出来。拿最常用的问卷数据来说,假如说我们得到了每个条目的一个分数之后,如果我们想要得到这个问卷的总分或者均值,然后把它进行下一步的处理。这个时候就意味着我们的第一步是提取出每一个条目(item)所对应的列。第二步,进行运算。 5.7 数据索引 5.7.1 数据索引(中括号) 所以第一点,我们首先要知道如何把需要的东西找出来,在数据处理里面,称之为index(索引),即在复杂数据结构里定位特殊的、感兴趣的变量或数据。基本上所有的这个数据处理的编程软件,都会涉及到索引的问题,不管是用Python还是用这个R,还是用MATLAB。在R里面,我们是用这个中括号这种方式来进行索引的。例如,dataframe的数据里有行和列,在中括号通过逗号分隔两个部分,逗号前面是行的信息,后面是列的信息。我们可以直接用行的数字提取它们。 # 选取前 2 行以及前 3 列数据 penguin_data[1:2,1:3] 结果显示: Site age sex 1 Tsinghua 1922 2 2 Oxford 1940 1 那么关于如何去提取、提取几行或几列、以及如何索引,大家可以后面去进一步搜索其它的方法,上面这种方法是一个最基本的索引方式,也就是直接用行和列的编号来定位索引。还有一种情况是,如果你在作为主试的过程中发现第100个被试(编号在第100行)的数据可能有问题,你想单独把他的数据提出来。那么,你可以直接把第100行提取出来,前面写100,后面直接不写,就表示把所有的列都提出来,看这一行的整个数据怎么样。 # 提取第100行的整个数据: penguin_data[100,] 另外一种方式是,可以用ncol函数和减号(-)提取dataframe数据的倒数几列。ncol是查看数据有多少个列。 # 提取penguin_data的最后4列数据的2行: penguin_data[1:2,-c(4:ncol(penguin_data))] 这是一种用传统的这种方式来提取某一个dataframe里面细节的例子,也是是最经典的方法。但有时候,你其实不知道想要的数据在多少行多少列,但是你知道这个数据的其他信息,比方,你知道被试的编号。而被试编号并不完全和行的数字编号对应。这是为什么呢?因为有可能你中间缺失了或者作废了一些编号,比如从第50个直接到了第53个,中间的51、52号根本都没有输入进去。这就是说被试的编号,并不是和行数完全对应的。定位可以通过一种挑选的方法实现,也就是说,找到被试编号等于某一个特定数值的数据。这里涉及到一个逻辑选择的问题。这里的代码会比较复杂,用tidyverse可以更简单地实现,等我们讲到数据清理的时候再说这个方法。 另外一种方式是,我们可以直接用列的名字提取。比如说,我们知道有哪些列需要提取出来,刚才是用序号来提,这里可以用列的名字直接来提取。 # 选取前 2 行以及前 4 列数据:: penguin_data[1:2,c('age','ALEX1','ALEX2')] 结果显示: age ALEX1 ALEX2 1 1922 2 2 2 1940 1 1 还有一个小tips,就是用BruceR里面的一个CC帮助省略每次使用字符串的时候都需要打的引号。使用bruceR::cc(),只需在首尾写引号: c('age','ALEX1','ALEX2') == cc('age,ALEX1,ALEX2') 5.7.2 数据索引($) 另外一个常见的方法是使用美元符号($)。当我直接去提取出某一列时,由于每一列都有自己的名字,R语言提供了一个特殊的方法,就是使用美元符号。在输入dataframe后按下美元符号,RStudio就会自动弹出所有列名的提示,您可以选择您想提取的列名,比如我们想要提取site列,直接输入site即可。这样您就可以提取出所有的site名称。如果您这样操作,就会得到很多site的名字。但是如果您不想查看所有的内容,只想看前面的一些,这个时候可以使用head函数,这是可行的。 这样做的好处是可以快速查看数据,比如对某一列进行精确的定位和运算。比如,如果想知道某个特定列,比如anxious(焦虑)的均值,我们就可以直接用美元符号提取这个anxious列。因为焦虑得分可能是在200多列中的某一项,可能不知道它的具体位置,如果一个个数又太慢了。通过使用美元符号,我们可以快速进行定位。 ## 根据列名进行索引 head(penguin_data$age) 结果显示:[1] 1922 1940 1945 1948 1948 1951 ## 如果数据类型的格式是 ***data.frame*** ## 则使用$提取和中括号提取是等价的 class(penguin_data$age) class(penguin_data[,1]) 这里面又会涉及到我们如何处理缺失值的问题。当你去提取某一列、对它做运算的时候,如果里面有一个缺失值,比如,有一个NA(缺失值)的话,那么系统就会默认把整个数据都把当做缺失值了。在这种情况下,需要加一些额外的对于缺失值的处理。比方说,需要把这些缺失值去掉,就相当于如果说他某一行缺失的话,你就不把他算入均值里面。这样的话你就能够快速地获得均值。这也是一种个人的使用经验,其实应该是最常见的一种提取列的方式。 5.7.3 数据索引(逻辑值) 还有一种就是根据逻辑值进行索引。当你只筛选特定的、符合某一个条件的一些数据出来。那么这个已经变成了数据预处理的部分,就是我们需要通过一个逻辑值挑选出一部分的数据,然后对它进行一些后续的处理。比方说,在做实验数据处理的时候,通常会有不同的实验条件。我们可能会想把不同实验条件的数据以及不同被试的数据分开进行处理,分别求他们的均值或者进行其他分析。这个时候我们就需要采用逻辑值进行索引与筛选。 首先,我们要输出逻辑词。比方说.我们想要提取出所有年龄大于1980年的数据,也就是1980年以后出生的人。如果我们采用这种方式操作,实际上会返回一串逻辑值,对于每一个年龄,比如1500多个数据点的年龄,它都会对应有一个TRUE或FALSE,表示这个年龄是否大于1980年。如果大于1980年,它会返回TRUE,如果不是,就会返回FALSE。然后,我们可以根据这一串TRUE和FALSE将它们作为一个筛选工具,用来提取数据。也就是说,我们可以只提取那些逻辑值为TRUE的数据。 ## 输出逻辑值 head(penguin_data$age >1980) 比方说,在下面这个代码的中括号里,前面部分的代码实际上就是在做判断,判断在这个DataFrame数据里,出生年份(age)大于1980年并且小于1990年的数据,即1981年到1989年间出生的人的数据,这就是我们想要筛选出来的数据。此时,我们进行了两个逻辑判断。第一个是年龄大于1980,第二个是小于1990,然后用一个逻辑值AND连接,同时满足这两个条件的数据,就把它判断出来。然后,把这个逻辑值放到前面,把它作为挑选行的一个标准。中括号前面是挑选行,后面是挑选列。 ## 筛选出生年份大于 1980 且(&)小于 1990 的数据 agedata = penguin_data[ penguin_data$age >1980 & penguin_data$age < 1990 ,] unique(agedata$age) {## 逻辑运算: 且(&)、或(|)、非(!)、%in%(属于)} 这个操作相当于是先生成了一个索引(index),然后提取索引。索引根据我们对年龄的逻辑判断生成。我们把年龄通过逻辑判断出来,也就是把所有符合“同时大于1980并且小于1990”这一年龄特征的行标识出来。比方说,第一行如果不在这个范围,它就变成FALSE。第二个是TRUE。于是,就会生成1,500多个TRUE和FALSE。如果我们把逻辑判断值放在行里作为挑选的索引时,那么它就会只把那些等于TRUE的保留下来,等于FALSE就全部被去掉了。这样一来,我们就可以按照逻辑的规则把我们感兴趣的数据挑选出来。 挑选出来之后,可以看一下挑选是否正确。此时,需要用到了一个函数,查看我们所挑选出来的数据的年龄的独特元素,这里用函数unique。unique是在检查数据时非常常用、非常好用的一个工具,它的功能是查看某一个对象(可能是一行,可能是一列)里有多少个独特的元素。在我们这个例子里,我们想要提取的就是大于1980、小于1990的这批人的年龄。我们提取完了之后,需要看看提取的内容有没有错误。比方说,我们想确保只有大于1980且小于1990的年龄被提取出来,而不希望大于1990或小于1980的年龄被包含在内。我们使用unique函数来查看提取的结果,就会发现基本上所有的年龄都在我们设定的提取范围之内。当然,如果有缺失值(NA),它们也可能被纳入进来,这也是一种提取的方式。 当然,我们也可以通过多种条件进行组合。比方说,我们不仅要对年龄进行筛选,还要对地点(site)进行筛选,比如,我们只想要来自中国的数据。这时,我们还可以在这个逻辑判断里面继续加入其他条件,不仅仅是加年龄的条件,也可以加其他条件。逻辑判断会针对每一行生成一个TRUE或FALSE,如果有1,500多行,它就会生成1,500多个TRUE和FALSE。根据生成的TRUE和FALSE进行后续的数据提取。 我们可以尝试一下提取全部在牛津收集的数据,然后我们再用unique来看数据是不是全部来自牛津(“oxford”)。我们可以用一个temp的数据。先打一个中括号,然后一个逗号,表示我们在前面这个地方提取。我们首先要去选择用哪一个(这里是site列)作为挑选的标准,然后让它等于“oxford”。这等于是在“oxford”的被试的数据全部提取出来。这里因为我们生产了一个临时变量,所以在environment里面又增加了一个新变量,就是叫temp,表示临时的一个数据。 然后,我们用unique函数去检查刚才提取得是否正确。如果我们提取的全部都是“oxford”的,那么这里是“site”就应该只有一个元素。如果结果不是只有一个的话,就说明提取有误。这种(代码运行后的)检查在我个人的经验当中是非常重要的,因为有时候,当我们我们自己写两行命令时,它并没有报错,我们可能会以为它做了我们想要做的事情,但实际上可能不一定。也就是说,在数据分析的时候,你一定要确认,你做的这个操作是不是真的得到了你想做的东西。所以你要有一个办法能够把它检测出来,这时候就可以用unique函数去确认。这是一定要经常去检查的。因为很容易就会出现没有报错,但他做的东西并不是你想要做的。 #示例,提取site为Oxford的数据,并判断是否为单一元素: temp= penguin_rawdata[penguin_rawdata\\$site==“oxford”,] unique(temp\\$site) 5.8 对象(object) 我们刚才所有说的这些东西,在R里面都是一个object(对象)。 那么大家可以想象,就是说我们在环境(environment)当中看到的对象,实际上就是我们在临时操纵、不断生成、不断变化的一个又一个object。比方说,我们现在用这个temp,临时地提取和检查一下刚刚用来检查提取是否正确的。可能很快地,我们又把temp赋值为另外一个值,比方说123。那么这个时候,在这个标签之下,大家可以看到,temp就已经变成了另外一个内容了,它变成了一个values(值)。然后这个”temp_123”跟刚才(penguin_data)就不一样了。 所有的这些临时存储数据、对象,还可以理解为,我们心理学当中的一个工作记忆。对R来说,R也需要工作记忆。在工作记忆里,我们对这些对象不断的进行操纵。那么每一个容器,或者每一个对象,它们就像是一个盒子,这个盒子上有个标签。但是你把什么东西扔进去,这取决于你最近的操作是什么。比方说,我们刚才做了两个操作。第一个是temp,我们从这个DataFrame里面提取出来的,属于所有在Oxford收集的数据。大家可以想象,我们刚刚作的操作实际上就是在R的工作记忆里找了一个叫做temp的盒子,把所有符合条件的数据,一整个dataframe都放进去了。我们的第二个操作是赋值给temp,把它叫”123”。这是我们把原来的那个扔掉了,又重新放了一个新的一个值,就是123这个数字。我们可能一会儿又要做别的操作,那么这个变量,就是这个盒子,它的标签虽然叫做temp,但它内容可能随时在变。所以这个时候,也是我们在后面处理数据的时候要注意的点,变量的命名要注意。因为一旦你的代码写长了之后,你原来的变量在中间经过很多操作之后,它有可能就变成了一个你不想要的内容,但是它又带着这个内容可以做后续操作,也不报错。这个时候,可能最后你的结果就很奇怪了。 那我们今天要讲的内容,差不多就是这么多。我们也没有把这个所有的知识点讲完了,因为这里面涉及到的知识点非常多。那么为什么我们没有讲呢?因为感觉其实讲了之后,大家可能听完了也忘记了。那么大家只有在自己在实际的过程中操作的时候,才能不断地去练习。我最近测试了一下,大语言模型的讲解非常的完善,就大家如果对R里面的某一个知识点,比如,在R里面向量是什么,在R里面Matrix是什么,大语言模型都会给出解释。这样的话,大家就可能更快地去了解这些细节性的知识。那么在我们课程上的话,我们主要就是以数据分析的整个流程为主。 5.9 其它注意事项 5.9.1 变量命名 大小写敏感 不能有空格,可以用下划线代替空格 开头不能是数字和一些特殊符号(如+-*/) 关于”.”:R语言是允许变量名中带”.”的,比如将数据命名为penguin.rawdata是合法的,但是”.”在许多编程语言中都具有特殊含义(如Python),所以更建议使用_来代替.。 如果一定要使用非法的变量名的话:将变量名使用il≤galvarname括起来: # 比如变量名以数字开头且包含空格: `1 首先` = 1 print(`1 首先`) # 从环境中删除该变量 rm(`1 首先`) 5.9.2 缺失值(NA) 空值(NULL) NA:代表缺失值,任何数值与NA进行计算都会得到NA,因此在计算或绘图中,需要移除缺失值或对其进行填补 unique(penguin_data\\$ALEX1) #直接计算会得到 NA mean(penguin_data$ALEX1) # 因此计算时需要移除 NA(remove): mean(penguin_data$ALEX1,na.rm = T) NULL表示空值,没有任何数据或内容,比如penguin_data$ALEX1 = NULL。会直接删除 ALEX1 这一列(注意:该操作不可逆) 5.9.3 数据类型的转换 R 语言中有一类函数以 as.开头,如as.numeric()、as.data.frame(),会对数据类型进行转换,比如: x = TRUE x = as.numeric(x) class(x) x = as.character(x) class(x) 类似地,也存在一类函数以is.开头,判断对象是否属于某一类型,返回逻辑值: is.character(x) 5.9.4 目录和文件管理函数: getwd()—返回当前工作目录。 setwd()—设置当前工作目录。 list.files()或dir()—查看目录中内容。 list.files(pattern=’.*[.]r$’)可以列出所有以“.r”结尾的文件。 file.path()—把目录和文件名组合得到文件路径。 file.info(filenames)—显示文件的详细信息。 file.exists()—查看文件是否存在。 file.access()—考察文件的访问权限。 create.dir()—新建目录。 file.create()—生成文件。 file.remove()或unlink()—删除文件。 unlink()可以删除目录。 file.rename()—为文件改名。 file.append()—把两个文件相连。 file.copy()—复制文件。 basename()和dirname()- 从一个全路径文件名获取文件名和目录。 5.9.5 对象与变量名 关于对象更加细节的内容,可以参考《R 语言入门与实践》(点击这里)。 R语言中,对象就是存储数据的容器,而变量名相当于容器的标签,通过标签来找到对应的数据:比如x = 1,y = x,y 的值也为 1,而’x’和’y’只是数值 1 的两个标签,二者访问的计算机内存地址相同; 但 R 语言中所使用的复制为浅拷贝(shallow copy):当对 y进行更改的时候并不会影响原本 x 的值(在 R 3.1.0之前为deep copies),而 y这个标签也被“贴在”了新的内存地址上(copy-on-modify)。 x = 1;y = x address(x) address(y) y = 2 address(y) 5.10 final project(期末作业) 最后的大作业,也就是说大家要做的东西,需要形成文字的,或者说要写的东西。一个是代码,最好是用R Markdown来写。然后呢,你可以比方说生成一个,如果你用这个R Markdown,就是我们心理学推荐的那个包的话,那么你就写markdown。它最后会什么呢,就直接给你生成一个文档。 第二个我们要交的东西就是文档,要么是PDF要么是word文档。那么还有一个就是第三个,就是你这个PPT,就是你最后做完了之后,你不仅仅要提交内容,你还要去展示。 对已发表论文的数据分析进行复现: 1、展示:待定,每小组约8分钟+2分钟提问 2、代码及报告提交:2024.6.30(待定) 3、需要上交如下内容: 代码(RMD格式,最好使用papaja进行准备) 文档(PDF或者Word,采用APA格式撰写,推荐由Papaja生成) 报告(PPT或者HTML在课堂上展示) 4、选题范围,以下期刊上发表有公开数据的论文: Nature Human Behaviour; Nature communications; elife; Psychological Science, Cognition, JEG:General; Collabra: Psychology; Communications Psychology 5.10.1 Requirements 5.10.1.1 代码要求: 1、数据预处理流程 2、完整的数据分析流程,包括: 描述性统计结果 + 可视化(集中量数,离散量数,相关等) 对原文数据分析结果的计算可重复性的评估(按照模板回答问题) 检验结果可视化 5.10.1.2 文档要求: 标题《对XXX的可重复性研究》及作者 小组成员与分工 引言(对选取研究的简述,包含其假设、数据、方法、结果等) 研究复现思路及流程 复现结果 对结果的讨论及结论 参考文献 附件:对计算可重复性模板中问题的逐条回复 5.10.1.3 报告要求: 标题,作者 成员信息、分工 所选研究简介,包含其前言、方法、结果三个部分 研究复现思路及流程 复现结果 对结果的讨论及结论 5.10.2 评分标准 好的,如果有什么问题的话,现在不问的话可以在群里随时问。我们今天就到这里,谢谢大家。 "],["lesson-5.html", "Chapter 6 第五讲:R语言中的对象2: 函数 6.1 加载数据 6.2 读取数据 6.3 赋值 6.4 数据类型 6.5 数据结构 6.6 数据框 6.7 矩阵与数组 6.8 列表 6.9 函数", " Chapter 6 第五讲:R语言中的对象2: 函数 今天我们正式开始用r来处理数据。记得去年教授这门课程时,有同学遇到了报错和乱码的问题,无法正常显示,这是因为部分语言设置不正确。为避免此类问题,大家可以将自己的编程语言设置为英文,这样就不会出现乱码了。也就是说,当你遇到报错时,如果系统默认语言是中文,那么在输出时可能会有乱码,无法被识别,这是一个小问题。将界面语言设置成英文的代码如下: # set local encoding to English if (.Platform$OS.type == 'windows') { Sys.setlocale(category = 'LC_ALL','English_United States.1250') } else { Sys.setlocale(category = 'LC_ALL','en_US.UTF-8') } # set the feedback language to English Sys.setenv(LANG = "en") 在开始分析数据数据之前,我们需要加载分析数据时所需要的包,一般使用library()函数。但更推荐大家使用名为packman(Package Management )的包来加载,使用pacman::p_load不仅可以批量加载包,而且遇到没有安装的包时会自动为我们安装。 if (!requireNamespace('pacman', quietly = TRUE)) { install.packages('pacman') } pacman::p_load(bruceR,here) 6.1 加载数据 既然我们要开始处理数据,而R作为一种工具,是解决数据分析问题的关键。我们需要将R整合到数据分析流程中,用R来完成整个数据分析过程。数据分析的第一步通常是获取数据。对于心理学专业的同学来说,大多数人获取数据的方式是通过自己进行实验和发放问卷来收集数据,并通过问卷平台或实验数据收集工具将数据汇总。而在其他情况下,获取数据的方式可能会有所不同。例如,你毕业后可能会在其他地方工作,此时你遇到的数据可能不是通过实验收集的,而是需要通过网络或其他途径来获取,或者可能是别人交给你的未经整理的数据。 在处理数据之前,你可能需要进行一些额外的步骤,比如通过网络爬虫技术来爬取数据,这是一种获取数据的方式。假设现在大家手头已经有了一些数据,接下来,我们要探讨的是如何将这些数据导入到我们的数据分析软件中。对于本科毕业论文,我们都知道SPSS是如何导入数据的。 同样,我们也可以尝试在R环境中进行类似的操作,比如在R Studio的界面中,我们通过File-Import Dataset选项中手动导入数据,选择数据打开后,在右上角的 Environment 界面中会显示已导入的数据的名称。但是如果我们要导入的数据非常多时,或者我们需要将许多分散的数据合并成一个数据时,使用手动点击的方式一个个导入就会非常费时费力,因此代码会是效率更高的选择。 在课程中,我们会遇到两个主要的数据集,一个是Human Penguin Project问卷数据集,另一个是Perceptual matching 实验数据集。我们刚才通过点击操作完成了数据导入并查看了问卷数据,现在则要通过代码来完成相同的操作,包括尝试选择一些变量进行初步统计。这就是我们在数据分析中遇到的第一个问题。本节课的主要目标就是要解决这一问题。 6.1.1 数据的“地址”——路径 如果我们希望通过代码来导入数据,那么首先需要告诉电脑数据“住在”哪里(address),即到哪个文件夹来寻找数据。如果只是在文件夹中以点击的方式来寻找我们想要的数据,比要在 R4Psy 中找到名为 penguin_rawdata.csv 的数据,则需要按照以下步骤进行点击(以 Macos 系统为例): 如果将点击文件夹的名称按顺序组合起来,并且中间使用斜杠(/)作为分隔符,就形成了计算机寻找文件的路径:“./data/penguin/penguin_rawdata.csv” 或 “data/penguin/penguin_rawdata.csv”(注意,路径的前后都需要有引号,单引号双引号都可以,具体原因会在这节课的结尾解释)。 但是很容易发现,在 R4Psy 文件之前仍然存在文件夹,如果一直追溯的话会发现路径会变得非常非常长: 完整的路径写成代码的话应该表示为:“/Users/cz***/Documents/github/R4Psy/data/penguin/penguin_rawdata.csv”。 6.1.2 绝对路径与相对路径 对于这两种路径,后者(即完整的路径)称为绝对路径,而前者(不完全的路径)称为相对路径,二者区别在于是否要设置一个文件夹所谓搜索的起始点,比如在上面例子中,相对路径设置了R4Psy作为搜索的起始点,而绝对路径则从硬盘所在的文件夹开始搜索。显然,就写法而言相对路径会更加轻松。而这个起始文件夹被称为工作路径(working directory,绝对路径 = 工作路径+相对路径),如果设定了工作路径,在工作路径之前的内容就可以省略不写。 在RStudio 的 global options-General中可以设置默认的工作路径;当然,也可以使用setwd()函数来手动修改,使用getwd()函数来查看当前的工作路径;另外, Rmarkdown(.Rmd) 和Rproject(.Rproj)这两种文件对于路径的处理会比较特殊,它们会默认将文件所在的地址作为工作路径,这非常有利于和别人分享你的结果:我们一般会将打包放在和 Rmarkdown 文件相同路径的文件夹中,如果你将 Rmarkdown 及数据打包分享给他人时,别人使用你的 Rmarkdown 来加载数据,比如 R4Psy 文件夹,尽管在别人的电脑中 R4Psy 之前的路径和你完全不同,但别人运行 Rmd 文件时完全不用对路径做任何修改,也不用重新修改默认的工作路径。 对于工作路径的设置,除了使用 setwd()函数外,还可使用 bruceR 包中的set_wd(ask = T)或set.wd(ask = T)进行设置,另参数 ask = T 可以调出可视化界面通过点击的方式进行选择。 在 Windows 系统中道理是一样的,但如果大家点击地址栏的话,会发现路径的分隔符使用的是反斜杠(\\)而非上里面例子中所展示的(/): <img src="pic_abspath.jpg" alt="abs_path" style="zoom:50%;"/> 在R语言中,路径分隔符要求为斜杠(/),注意不能直接从路径栏中直接复制地址。这是一个非常细节的问题,但也有更加方便的方式来解决这个问题,即使用here::here()函数,只需要在这个函数中按顺序依次输出,比如对于上面的相对路径就可以写为: here::here('data','penguin','penguin_rawdata.csv') 6.2 读取数据 6.2.1 手动导入 在设置好工作路径后,可以在 Files 窗口中直接点击数据,这个过程与 SPSS 或 Excel 里导入数据的操作类似。 6.2.2 代码导入 当然,我们更推荐使用代码的方式来导入数据: penguin_data = bruceR::import(here::here('data', 'penguin', 'penguin_rawdata.csv')) ncol(penguin_data) ## [1] 232 nrow(penguin_data) ## [1] 1517 head(penguin_data) tail(penguin_data) 在上面的代码中,我们使用了 bruceR 包中的 import 函数来导入 penguin_rawdata.csv文件,并将数据命名为penguin_data(即等号’=’);导入成功后,可以在 Environment 界面中看到这个名称,如果在 console 界面中输入penguin_data,就会返回具体的数据内容;使用 head()或tail()可以查看数据的前 5 行或最后 5 行。ncol()和nrow()分别查看数据的列数与行数。 (如果 bruceR包已经加载了,在代码中bruceR::部分是可以省略不写的,对于简单的数据处理来说没有问题,但随着数据处理与分析的难度增加,会用调用非常多的包,有时候不同包之间会存在有相同变量名的函数,如果不声明函数来源于哪个包的话,要么 R语言会提示函数存在冲突,要么本来想用 A 包中的函数,但实际上执行了 B 包中的同名函数,因而出现报错。因此建议大家现在就养成在函数声明其来源的好习惯) 如果大家之前使用点击的方式来导入数据,会发现在 console 中会显示导入数据的代码,代码中使用的是read.csv()函数,而我们更推荐使用import()函数作为替代,因为import()函数对于心理学常见数据几乎都是适用的(如txt,csv,sav,dta等等,可通过?bruceR::import()查看具体信息),而read.csv()只适用于 csv 类型的数据。 6.3 赋值 在导入数据时,我们使用了等号进行赋值。而在R中,赋值操作符可以使用“<-”,也可以使用“=”,二者是等价的。 x = 1 y = x + 5 ## cat函数将值的内容输出到屏幕上 x ## [1] 1 y ## [1] 6 ## 重新对 x 进行赋值 x = 1000 ## x x ## [1] 1000 y ## [1] 6 通过上面的例子可以发现,对 x 进行赋值,并利用 x 进行计算后,更改 x 的值并不会影响 y的结果。此外,如果仅仅只是利用数据进行计算而没有复制操作的话,计算结果并不会被保存: object <- 10 object ## [1] 10 # 利用 object 进行计算但不赋值 object + 2 ## [1] 12 # object 数值并没有发生改变 object ## [1] 10 此外需要注意的是,R语言中的变量名的命名有一系列的规则: 区分大小写,’Object’和’object’是两个不同的变量名; 变量名内不能使用空格,但可以用下划线代替空格; 变量名开头不能是数字和一些特殊符号(如+-*/) ; 6.4 数据类型 在读取数据后,我们需要知道数据中的具体内容有哪些,但这里就需要了解一些与数据类型相关的知识。在 SPSS 中的变量视图中,不同的变量可能会有不同的数据类型,如名义、标度、有序等,Excel 中对于单元格也可以去选择常规、数值、货币、日期等类型,R 语言也是一样。 可以观察一下我们导入的 pengui_data 数据,可以发现对于每个单元格来说,大概可以分为两类,一种是数字,一种是文字;而数字也有整数和小数之分。凭直觉来说,数字是可以进行加减乘除等运算的,而文字(如‘Oxyford’)不能(实际上也确实如此)。 6.4.1 单个元素 在R语言中,对于单个元素的数据类型(大概可以理解为 Excel 中的单元格),可以简单分为三类: 6.4.2 数值型(numeric) 包括浮点型(double,即小数)、整型(int)等,可以进行数学运算; # 简单做个加法 1 + 1 ## [1] 2 ## 直接输入pi会返回圆周率 pi ## [1] 3.1416 ## 幂运算 10^3 ## [1] 1000 ## 取对数 log(1) ## [1] 0 使用class()函数(即类)可以查看具体是什么数据类型,比如: class(pi) ## [1] "numeric" 6.4.3 字符串(character) 即文字内容,但需要注意的是,字符串的前后需要被英文的引号所包裹,单引号双引号都行。无论内容是什么,只要前后存在引号,R语言都会将其识别为字符,哪怕其中是乱码: class('1 + 1') ## [1] "character" class("ㄤ娇鐢ㄦ鏂囦欢锛") ## [1] "character" ## 之前提到的路径,其实也是字符串,所以需要加引号 class(getwd()) ## [1] "character" 6.4.4 逻辑值(logical) 即布尔值。大家肯定都听说过计算机使用二进制 0 和 1来表示“关”和“开”或“假”和“真”;逻辑值与此类似,即True 为真(对),False 为假(错),分别可以简写为 T 和 F。逻辑值通常是比较运算的结果: 2 > 1 ## [1] TRUE T == TRUE ## [1] TRUE T > F ## [1] TRUE class(2 > 1) ## [1] "logical" 常见的比较运算符如下图: 6.5 数据结构 之前的内容介绍了三种数据类型,相同或不同的数据类型之间可以相互组合,进而形成了丰富的数据结构: 6.5.1 向量(vector) 数值、字符串、逻辑值都可以各自组合在一起,可以分别组成数值向量、字符串向量、逻辑向量。可以使用c()来建立向量(c可以理解为 combine),但需要注意的是,向量里的类型必须相同,否则会按照一定规则进行强制转换。使用 class()来查询向量类型,由于向量里的数据类型是相同的,因而会返回向量中具体的元素类型,而不是 vector。 ## 数值型向量 v1 <- c(1,2,3,4,5) # 对于连续数字可以使用冒号简写,即从1到(:)5 # 在连续数字或单个元素时可以省略c() v1 = 1:5 ## 字符型向量 v2 <- c('apple','pear','banana','strawberry','lemon') # 每个元素都要写一遍引号非常麻烦 # 因此可以使用 bruceR 包中的 cc()函数,开头结尾有引号即可 v2 <- bruceR::cc('apple,pear,banana,strawberry,lemon') ## 逻辑型向量 v3 <- c(T,F,F,T,T) ## 使用 class查看,并不会返回vector class(v1) ## [1] "integer" 6.5.2 类型转换 刚才提到:向量里的类型必须相同,否则会按照一定规则进行强制转换,那么这些强制转换的规则是什么样的呢?尝试在一个向量中包含不同的数据类型: ## 数值和逻辑 x1 = c(T,2) class(x1) ## [1] "numeric" ## 数值和字符串 x2 = c(2,'a') class(x2) ## [1] "character" ## 逻辑值和字符串 x3 = c(T,'a') class(x3) ## [1] "character" ## 数值型、逻辑型和字符串 x4 = c(1,T,'a') class(x4) ## [1] "character" 可以发现,如果数值型和逻辑值同时出现,则会被强制转换为数值型向量;如果数值型或逻辑型与字符串同时出现,则会强制转换为字符串向量,这就是 R语言中数据类型的强制转换机制。当然这也提醒我们,不同类型之间的数据是可以相互转换的,而在 R 语言中也存在以 as.开头的函数执行转换操作: as.character(1:3) ## [1] "1" "2" "3" as.numeric(c('1','2','a')) ## [1] 1 2 NA as.numeric(c(T,F)) ## [1] 1 0 在上面例子中,需要注意第二个例子。字母本身并不能转换成数字,因而使用 NA 进行替换;而 NA(Not Available) 指数据中的缺失值。 和as.系列函数相类似的还有is.系列函数,用于判断数据是否是某个数据类型,比如 is.character(1) ## [1] FALSE is.na(1) ## [1] FALSE is.numeric(1) ## [1] TRUE 6.5.3 向量循环 在 R语言中,向量有着独特的运算方式: x = 1:6 x + 1 ## [1] 2 3 4 5 6 7 x * x ## [1] 1 4 9 16 25 36 可以发现,对 x 向量进行加一的运算会返回对x 中每一个元素进行加一运算;而向量之间的相乘会返回每个元素乘积所形成的向量,这种操作称为向量循环。 6.5.4 向量的索引 如果想要提取向量中的某个值,可以在中括号中输入数字向量进行索引: v2 <- c('apple','pear','banana','strawberry','lemon') ## 查找第 1 个元素 v2[1] ## [1] "apple" ## 查找第 2 和第 4 个元素 v2[c(2,4)] ## [1] "pear" "strawberry" ## 查找第 1 个到第 4 个元素 v2[1:4] ## [1] "apple" "pear" "banana" "strawberry" ## 负索引( = 删除元素) ## 删除前两个元素 v2[-c(1:2)] ## [1] "banana" "strawberry" "lemon" ## 也可以利用索引对元素进行更改 v2[1:2] = bruceR::cc('APPLE,PEAR') v2 ## [1] "APPLE" "PEAR" "banana" "strawberry" "lemon" 6.5.5 因子(factor) 对于心理学专业的同学来说,因子这个词应该非常熟悉了,因子分析是对于问卷数据来说是常用的方法之一,但这里的因子与因子分析中的因子概念并不相同。 在统计中,我们将数据分为称名数据、顺序数据、等距数据、等比数据四种类型,而因子这一数据结构(容器),专门用来存放称名数据和顺序数据。 相较于字符串,直接用字符向量也可以表示分类变量,但它只有字母顺序,不能规定想要的顺序,也不能表达有序分类变量。 x <- c('good','better','best','bad','worse','worst') # 使用sort函数进行排序,结果只会按照字母顺序排序 sort(x) ## [1] "bad" "best" "better" "good" "worse" "worst" ## 可以使用 factor 来创建因子,并使用 levels 参数来规定具体的顺序 x1 <- factor(x,levels = c('best','better','good','bad','worse','worst')) sort(x1)#排序 ## [1] best better good bad worse worst ## Levels: best better good bad worse worst 因子通常用于表示有限集合中的元素,但输入的类型可以是整型,也可以是字符串。 6.6 数据框 如果将向量视为一列,不同列拼接在一起就形成了我们常用的数据(比如 penguin_data),我们称之为数据框(dataframe),显然,数据框要求形状必须是方形,即每一列的长度必须相等。我们可以尝试手动定义一个数据框: ## 创建三个不同类型的向量 v1 <- c(1,2,3,4,5) v2 <- c('apple','pear','banana','strawberry','lemon') v3 <- c(T,F,F,T,T) ## 将三个向量打包成一个数据框 df1 = data.frame(col1 = v1,col2 = v2,col3 = v3) class(df1) ## [1] "data.frame" ## 查看数据框内容 df1 ## col1 col2 col3 ## 1 1 apple TRUE ## 2 2 pear FALSE ## 3 3 banana FALSE ## 4 4 strawberry TRUE ## 5 5 lemon TRUE 上面例子中,我们首先建立了三个等长但类型不同的向量,之后使用data.frame()函数将三个向量“打包”成一个数据框并赋值给df1,其中,col1、col2、col3 分别为 v1、v2、v3 的列名,当然列名也可以其对应的向量的名称重合,尝试一下将列名修改成与向量名一致: ## colnames()可以查看数据框列名 colnames(df1) ## [1] "col1" "col2" "col3" ## 对 colnames 进行赋值可以修改列名 ## 但在之前,介绍一个字符串的拼接函数 ## 将'v'与向量 1:3分别拼接,输出字符串向量 newname = paste0('v',1:3) newname ## [1] "v1" "v2" "v3" colnames(df1) = newname df1 ## v1 v2 v3 ## 1 1 apple TRUE ## 2 2 pear FALSE ## 3 3 banana FALSE ## 4 4 strawberry TRUE ## 5 5 lemon TRUE 6.6.1 数据框的索引 对于数据框来说,可以使用中括号和美元符($)进行索引;而使用中括号进行索引时,可以使用数值型向量,也可以使用字符型向量进行索引,但本质上使用的都是向量。当然,在下一章中我们会介绍更加方便的索引方式。 6.6.1.1 数字索引 数据框的索引与向量的索引非常相似,但不同的是,我们需要在行和列两个维度上进行索引,在中括号中,第一个数字对行进行索引,第二个数字对列进行索引,中间需要使用英文逗号进行分隔。以 penguin_data为例: # 选取第一行、第二列; penguin_data[1, 2] ## [1] 1922 # 如果想全部选取行或列,可以使用空索引 # 比如,选取第一列的所有行,不写行的索引就行 # 查看前五行 head(penguin_data[, 1]) ## [1] "Tsinghua" "Oxford" "Oxford" "Oxford" "Chile" "Bamberg" # 如果要使用非连续的数字进行索引,要带上c()以表明输入的是个向量 colnames(penguin_data[, c(1,3,5)]) ## [1] "Site" "sex" "romantic" 6.6.1.2 名称索引 在行或列的位置输入列的名称即可,比如查看出生日期和地址: head(penguin_data[, cc('age,Site')]) ## age Site ## 1 1922 Tsinghua ## 2 1940 Oxford ## 3 1945 Oxford ## 4 1948 Oxford ## 5 1948 Chile ## 6 1951 Bamberg 当然上面的例子使用的是列名,对于数据框的行来说也有对应的名称(使用rownames()查看、修改行名),数据框默认使用的是行数的字符串形式当做列名,只是大部分时候,我们不会使用行名进行索引。 6.6.1.3 美元符 美元符$是对列索引的一种方式,在 Rstudio 中,数据框后如果紧接$,就会自动弹出可视化窗口显示这个数据有哪些列名,也会提示列的类型和具体数据内容,非常方便;但局限在于,美元符一次只能提取一个列。 ## 使用美元符来提取出生日期 head(penguin_data$age) ## [1] 1922 1940 1945 1948 1948 1951 小练习 尝试把以下表格中的内容创建为数据框,并把第一列改为因子 6.7 矩阵与数组 矩阵和数据框类似,都是二维的数据;但不同点在于,数据框允许不同列的类型不一样,而矩阵中所有单元格的数据类型必须相同: #创建矩阵 m1 <- matrix(c(1:9),nrow=3) m1 ## [,1] [,2] [,3] ## [1,] 1 4 7 ## [2,] 2 5 8 ## [3,] 3 6 9 相同维度的矩阵可以继续组合,形成数组: #创建三维数组 a1 <- array(1:24,dim=c(3,4,2)) a1 ## , , 1 ## ## [,1] [,2] [,3] [,4] ## [1,] 1 4 7 10 ## [2,] 2 5 8 11 ## [3,] 3 6 9 12 ## ## , , 2 ## ## [,1] [,2] [,3] [,4] ## [1,] 13 16 19 22 ## [2,] 14 17 20 23 ## [3,] 15 18 21 24 6.8 列表 多个相同元素可以组合成向量,多个向量可以组合成矩阵或数据框,而不同元素、向量、矩阵或数据框仍然可以继续组合,进而形成了列表: # 使用list 来创建列表 l1 = list(1, c('a','b'), c(T,F)) l1 ## [[1]] ## [1] 1 ## ## [[2]] ## [1] "a" "b" ## ## [[3]] ## [1] TRUE FALSE # 列表里面也可以容纳列表 # 将l1与 penguin_data合并成一个列表 l2 = list(l1, penguin_data) 列表的索引与向量类似,但需要注意的是,使用中括号对列表进行索引,输出结果的类型仍然是列表;如果希望将数据还原成其原本的形式,就需要使用双中括号([[]]): # 索引第一个元素并查看类型 l1[1] ## [[1]] ## [1] 1 class(l1[1]) ## [1] "list" # 使用双中括号 class(l1[[1]]) ## [1] "numeric" 对于列表中嵌套列表的情况,就需要进行多次索引。比如通过索引来找到l2中的l1的第一个元素1(数值型),就需要两次索引,其中第一次索引返回l1列表,第二次索引从l1列表中找到第一个元素。 l2[[1]][[1]] ## [1] 1 class(l2[[1]][[1]]) ## [1] "numeric" # 如果使用一个中括号进行索引,返回数据类型仍然是列表 class(l2[[1]][1]) ## [1] "list" 6.9 函数 在导入数据的时候,我们使用了import()函数,在设置路径时,使用了here(),getwd(),setwd()等函数。在R中,函数是一种用于执行特定任务或计算的代码块。函数接受输入参数,执行特定的操作,并返回结果。如果我们不知道一个函数是什么,有什么用处。在R中,我们可以在Console中使用“?函数名”来打开帮助文档,以import()函数为例: 函数一般都包含许多参数来控制输出,比如import()函数中接受的参数为: 但很容易注意到,file 参数后面没有’=‘,其余所有参数后面都有’=’且设定有具体的值,比如 给encoding 参数赋值为NULL,这种方式是为了给参数设定一个默认值:在导入 penguin_data的例子中,我们仅仅输入了路径,别的参数如 encoding并没有被说明,这种操作之所以可行的原因就在于当我们没有给出某个参数的具体输入时,函数就会直接使用默认值,默认值的设置为函数的使用带来极大的便利。 但在导入 penguin_data的例子中,还有个问题是,我们输入路径时,也并没有声明一定是输入给 file 参数,为什么函数“知道”我们想输入给谁呢?这是因为,在函数中,如果有多个参数的话,函数会默认按照输入的参数的顺序进行匹配,我们只输入了一个参数,因而会与 file 参数进行匹配。 在我们的讨论中,似乎出现了两种参数:一种是函数内设定的参数名称,如 file、encoding 等,一种是我们实际输入的内容,如具体的路径。前者称为形式参数(file),后者称为实际参数(输入的具体路径)。在不输入形参的情况下,函数默认会按照输入实参的顺序与形参进行匹配;如果我们声明形参的实参时,就可以按照我们想要的顺序来输入实参,比如: ## 没有声明形参 penguin_data = bruceR::import( here::here('data', 'penguin', 'penguin_rawdata.csv'), ### file NULL ### encoding ) ## 如果声明形参,顺序可以调换 penguin_data = bruceR::import( encoding = NULL, file = here::here('data', 'penguin', 'penguin_rawdata.csv') ) ## 当然,如果按照这个顺序不输入形参的话会报错,大家可自行尝试 6.9.1 函数的调用 如果在已经加载包的情况下,可以直接使用某个包里的函数;如果没有调用某个包,还想使用其中的函数,就需要使用::来调用(比如bruceR::import()) 6.9.2 自定义函数 我们使用的函数有不同的来源,一种是来自 R 语言内置的 base 包的函数,一种是从 CRAN 中安装的第三方包中的函数,大部分时候上面的函数都可以满足我们的需求,我们要做的只是调用即可,但也会出现这些函数不能完全满足我们的需求情况,比如计算平均数和标准差通过内置函数mean()和sd()能实现,但我们希望在文章中输出为\\(Mean±SD\\)的形式,这时候我们就需要在已有函数的基础上稍作改动,即自定义函数。 6.9.3 函数的组成 函数定义通常由以下几个部分组成: - 函数名: 为函数指定一个唯一的名称,以便在调用时使用; - 参数: 定义函数接受的输入值。参数是可选的,可以有多个; - 函数体: 包含实际执行的代码块,用大括号 {} 括起来 - 返回值: 指定函数的输出结果,使用关键字return。 #定义一个函数:输入x和y,返回3倍x和5倍y的和 mysum <- function(x,y){ result = 3*x+5*y return(result) } #mysum:自定义的函数名 #x,y:形式参数 #result = 3*x+5*y:函数体 #return(result):返回值 #调用函数,x=1,y=2,省略形参 mysum(1,2) ## [1] 13 # 当然也可以声明形参 mysum(y=1,x=2) ## [1] 11 ### 尝试为函数设定默认值 mysum2 <- function(x = 6,y = 7){ result = 3*x+5*y return(result) } ### 省略参数 mysum2() ## [1] 53 ### 如果只输入一个参数会怎么样 mysum2(5) ## [1] 50 小练习:定义一个函数,输入值a,b,c,返回(a+b)/c;并计算abc分别为123时得到的值 #myabc <- function(***){ # result = *** # return(***) #} #用合理的代码替换以上“***”,删除每行前的“#”,即可运行 6.9.4 函数的简写 上面所介绍的函数是完整的写法,但也可以使用一些方法去简化函数的书写: return()的省略: 在一些非常简单的函数中,如果省略return(),函数则会返回最后一个计算出的表达式的值: mysum_simpli1 = function(x,y){ ## 由于会默认返回计算数值 ## 因此这里也没必有赋值给 result的操作了 3*x+5*y } 函数体的简写: function(x)可以简写为\\(x),以mysum函数为例: mysum_simpli2 = \\(x,y) {3*x+5*y} # 如果只有一行的话,为了书写简洁大括号甚至都可以省略 mysum_simpli3 = \\(x,y) 3*x+5*y 6.9.5 if 条件语句 当函数被调用时,如果输入的参数不符合预期,函数可能会抛出一个错误。这些错误信息对于程序员来说是非常重要的,因为它们指明了输入参数的问题所在。因此,当编写函数时,我们应该尽可能提供清晰的错误信息,以便用户能够理解并纠正错误。 比如,在学习数据类型时,我们提到字符型不能进行加减乘除等数学运算,比如在mysum函数中如果输入的内容为字符串,就会出现报错:non-numeric argument to binary operator。而在我们自定义的函数中,同样可以按照我们自己的想法来输出报错,但这需要对输出的结果进行判断:如果输出没有问题,就返回输出结果;如果出现错误,就需要给出为什么出现错误。这种判断其实是一种逻辑判断,即if-else条件语句: if-else 上面的语句含义为:如果(if)满足某个条件(condition,为逻辑运算),就执行 Expr1,否则(else),就执行Expr2。 举个例子,对于 mysum3 函数我们可以进行一下改进:在运算之前首先对输入的数据类型进行判断,如果输入为数值型,则进行运算并返回结果,否则就在屏幕上显示,x和 y 必须要为数字。 mysum3 <- function(x = 6,y = 7){ if(is.numeric(x) == T & is.numeric(y) == T){ result = 3*x+5*y return(result)} else{print("x and y must be number")} } #print:输出指定的内容 #is.numeric:判断是否为数值型。是则返回T,否则返回F # & : 表示“且” mysum3(5,6) ## [1] 45 mysum3('a','b') ## [1] "x and y must be number" "],["第六讲数据预处理.html", "Chapter 7 第六讲:数据预处理 7.1 Tidyverse 7.2 问卷数据 7.3 反应时数据", " Chapter 7 第六讲:数据预处理 大家晚上好,我们开始上课。在上节课,我们基本上介绍了如何将数据导入,以及导入后的一些基础知识。从纯粹的数据分析角度来看,主要是读取数据,然后查看数据的基本类型,这两节课的内容都是相对简单的,对吧?了解了这些基础知识之后,我们才能进行后续我们想要做的很多内容。 那么,今天我们将讲解一个非常重要的内容,就是数据的预处理。 这节课的内容相对较多,希望大家能够及时更新最新的课件,从GitHub上下载下来。我们将会进行一些代码演示。 首先,我们需要理解,数据在进入统计分析之前,必须经过一系列的处理,以使其成为较为干净的数据。在心理学领域,以前进行数据分析时,我们常常需要手动在Excel中进行数据清理,清理完毕后,再导入到SPSS进行后续的统计分析。 而当我们使用R语言时,从数据的最原始状态到清理的每一个步骤,我们都可以通过代码记录下来,这有助于提高分析的可重复性。这就是我们所说的数据预处理。 另一个关于数据预处理的重要知识点是,在数据分析过程中,可能超过60%的时间都花在了数据预处理上,而纯粹进行统计分析的时间相对较少。其是当数据量较大时,用于预处理的时间会更长。对于心理学领域来说,可能并不像听起来那么吓人,因为我们的原始数据通常相对容易清理。 7.1 Tidyverse 那么,在这一课中,我们将主要讲解三个方面的内容。首先,我们会介绍Tidyverse这个生态。当然,我们只会介绍一些最基本的知识,尤其是我们常用到的一些知识。在对Tidyverse进行介绍之后,我们将以两个问卷数据的分析作为实例进行演示。这在心理学以及可能的社会学研究中,也是一个经常遇到的情况。 首先,Tidyverse是一个生态系统,它基本上就是帮助我们进行数据处理的。在上节课,我们学习了如何导入数据,Tidyverse中也有一个包,专门用于读取数据。当你将数据读取到R中后,接下来就需要进行一系列操作,包括数据的转换、选择,然后将其整理成一个较为干净的数据框。此外,还包括对数据的整理和串行的操作等。Tidyverse中的各个包都是为了适应各种各样的预处理需求。 对于那些需要处理大量数据的情况,你可能需要重复执行相同的步骤多次。Tidyverse中的purrr包可以帮你进行批量处理。最后,你还可以通过ggplot2进行可视化。关于可视化,我们会在后续的课程中单独讲解。 今天我们要讲的核心内容就是数据的预处理。 当然,我们所说的Tidyverse是目前比较流行或者说最流行的整个生态系统。但这并不意味着我们直接使用base R就无法完成所有这些功能。理论上,你也可以通过base R完成所有这些功能。但我们之前一直强调的理念是,当我们作为心理学的研究生使用R时,最重要的是解决问题。现在已经有这么多人开发出了这么多好用的工具,我们就可以用它们来帮助我们解决问题。在解决问题的过程中,如果你对它们的原理更感兴趣,可以慢慢进行挖掘。 那么,我们可以说Tidyverse是一个非常成熟的一个包,也是一个系统。因此,它里面已经有很多类似的东西,我们称之为“文档”或者“手册”。它到底有哪些功能,这个在网络上应该也能找到。在这里,我们仅仅做一个展示。 如果大家对这个话题感兴趣,可以下载课件之后,直接访问这个网址:https://rstudio.github.io/cheatsheets/ 那么,Tidyverse的优势是什么呢? 优势:共享一个底层设计哲学、语法和数据结构,具有高度的一致性 Tidyverse被一些人认为是整个生态系统中非常独立的一个系统,也有人称它为“邪教”。当然,它是有一套自己的底层设计、语法和数据结构的。Tidyverse内部具有高度的一致性。目前的一个趋势是,很多以前不是按照Tidyverse的设计理念开发的包,也想将其转换为使用Tidyverse来重新编写或更新,这样可以让更多的人更方便地使用。它们就融入到了Tidyverse这个系统里面。 如果你去搜索一下与Tidy相关的包,你会发现它有一个很大的生态,不仅仅是前面提到的几个包。 “整洁数据(tidy)”:每行代表一个观察值,每列代表一个变量的值(再看) 函数的第一个参数总是一个数据框 Tidyverse的一个好处是,在数据清理中,它的输入通常是一个参数,即函数的第一个参数是一个数据框。也就是说,无论函数的排列顺序如何,第一个参数始终是一个数据框,作为输入对象。后面就是对这个数据框进行处理。 管道操作符:连接独立代码,省去中间变量,流水线 最常用的管道操作符为%>%,它将一个函数的输出作为下一个函数的输入 另一个好处是管道操作。通过管道操作符,可以将很多中间步骤串联起来,就像流水线一样。你前面操作完了之后,然后直接通过管道操作符送到下一个操作的环节,然后作为输入,经过下一个处理之后,再直接进入下一个处理。这样的话,你的整个流程不管有多么复杂,只要你能够通过管道把它写下来,最后就只有一个输入和一个输出。你的整个代码,包括你的结果,你的环境(Rstudio环境),都会非常干净。 我们这里举了一个例子: 比如你需要对一个数据进行筛选,然后再进行排序。一般来说,传统的思路是先有一个中间变量,对数据进行筛选后,将其保存为中间变量,然后再对这个保存的变量进行排序,排序完成后,再保存为另一个新的变量。这样的话,实际上中间就产生了多个中间变量。这样就会占用我们的内存。 假设需找到data中age大于30的所有行,并排序,代码如下: filtered_data <- filter(data,age>30) filtered_sorted_data <- arrange(filtered_data,age) 如果我们使用管道操作的话,它就是直接以这个data作为输入,经过filter之后,然后再经过arrange排序之后,它会返回到这个filtered_sorted_data。这样的话,我们就省掉了一些中间的步骤。如果你的中间步骤非常长的话,那你省掉的中间变量就非常多。这样的话,你的代码就不会占用过多的内存,也会很整洁。 使用管道操作符后,代码变为: filtered_sorted_data <- data %>% filter(age >30) %>% arrange(age) 以下是查看变量名的结果: 大家可能现在还没有这个感受,使用过后慢慢会感受到。接下来,我们会在例子中展示给大家看。 Tidyverse常见的管道符如表所示(依赖于magrittr包): 在Tidyverse中,管道操作符有多个,但我们通常使用最多的是第一个,即%>%。这个操作符的中间部分是>,最后是%。一般来说,它是向右操作的,将左边的数据传递到右边。然后你可以反过来,将右边的数据传递到左边,但这种用法比较少见。还有一个复合赋值操作符%<>%,我们也不常用。 一般来说,大家最常使用的是%>%这个管道操作符。另外,值得一提的是,在R 4.1版本之后,R语言原生提供了一个管道操作符|>。例如,在base包中,它自带了这样的工具,不需要再使用Tidyverse中的工具包来实现这个功能。 这就是关于Tidyverse的基本介绍。如果大家对这个感兴趣,可以在课后进一步了解。 7.2 问卷数据 7.2.1 研究问题&数据情况 研究问题: 社交复杂度(CSI)是否影响核心体温(CBT),特别是在离赤道比较远的 (低温)地区(DEQ)下 这节课的重点是教授如何使用Tidyverse进行数据预处理。我们之前提到,学习数据处理是为了解决研究问题。我们今天的研究问题采用了IJzerman等人在2018年的研究,我们要对这一心理学研究进行重复分析。本课程接下来会以重复IJzerman et al (2018)的分析进行问卷数据分析的示例。 下面是简要介绍: 该研究由IJzerman进行,是一个典型的心理学研究问题,涉及较大的数据量。2015年,IJzerman在Frontier上发表了一篇论文,提出了一个理论,他们认为在哺乳动物群体中,核心温度非常重要。因为恒温动物对温度有苛刻的生存条件,当温度过低或过高时,动物就会受到影响。人类也是如此。在野外环境中,动物如何调节自己的体温是一个问题。对于人类的祖先来说,可能也采用类似的方式来调节体温。研究发现,多个动物在一起时,保持相同核心温度所需的能量比一个动物保持相同体温所需的能量要低得多。因此,动物可以通过相互聚集形成群体来调节体温。人类在漫长的进化历史中可能也是这样。现在我们有了很多外部条件来帮助我们调节体温,如空调。在进化过程中,通过社交交往调节体温的机制是否会在人类身上留下一点点痕迹?这就是IJzerman的一个理论。他们认为,我们可以找到这种痕迹的,这种痕迹可以通过研究社交关系和身体核心温度之间的关系来找到。因此,他们的研究问题是:社交关系是否会影响核心温度,特别是在离赤道较远的低温地区?因为在温度较低的地方,可能更需要调节体温。在高温的地方,可能不需要体温调节。 研究假设: 对于在低温环境中的人来说,(在众多的变量中)社交网络复杂度能够影响个体的核心体温。这一效应受个体的恋爱状态(romantic)调节。 因此,他们的研究假设是,在较冷的地区,社交网络的某些变量是否与我们的核心温度密切相关,并且这种效应是否受到亲密关系状态的影响。 研究方法: 路径模型,探索性监督机器学习 该研究采用了路径模型来检验这些调节作用,并通过有监督的机器学习方法来探索不同变量之间是否存在关系。 这是他们主要的研究结果。如果大家感兴趣,可以进一步查阅相关资料。这里展示的是一个中介分析的图形。 另一个目的是判断CSI和其他变量与身体温度之间是否存在关联。 左边的图形实际上是用众多问卷数据来预测核心身体温度,即所谓的core body temperature。右边则是用身体温度来预测其他变量。我们主要关注的是左边的数据,如果大家能够看到的话,在这个地方有一个CSI,这表明社交关系网络的复杂度是一个重要的预测身体核心温度的变量。当然,还有一些其他非心理因素的变量,比如所在站点的最低温度,即与赤道的距离等,对核心温度的预测作用更强。 从心理变量的角度来看,研究发现身体的社会关系复杂度确实是一个重要变量。这与很多同学可能想要进行的研究具有相似之处,即探索不同变量之间的关系,并建立中介或路径模型,更广泛地说,就是结构方程模型。对于这类数据,我们肯定需要进行大量的问卷调查数据收集。 在这个数据库中,我们实际上有很多数据,包括身体温度,这个是通过测量口腔温度来进行的,还有一些生理条件信息,比如用药情况等,以及基本信息,如人口学信息,包括出生年份、性别、性取向等。有些变量可能需要向数据拥有者那样申请ID后才能使用,有些则是完全开放的。我们使用的是完全开放的数据,包括当地的地址和信息、社交网络信息,以及一些常见的心理学变量。 那么,当时为什么他做了这么多的问卷调查呢?因为他也不确定社交网络信息是否真的能够预测核心体温。因此,他尽可能地纳入了更多的变量,这种做法与我们很多探索性研究的高度一致。那么,如果我们这节课要处理这些数据,进行类似于他的分析,我们可能需要提取数据中相关的信息,并对一些问卷数据进行预处理。 例如,每个问卷都可能测量了某一维度的得分,我们需要计算这些维度的因子得分。这可能包括进行反向计分、计算因子得分,或者使用更简单的方法,如直接计算平均分或总分作为问卷得分。这意味着我们需要对问卷进行一些处理。接下来,我们将看一下如何使用Tidyverse来处理这些数据。 数据情况(Hu et al., 2019): 通过data/penguin文件夹下的penguin_full_codebook可以查看详细情况 首先,我们需要读取数据。和上节课一样,我们直接使用bruceR::import函数来读取数据,这是目前我们最推荐的读取数据的方法。 # 导入数据 df1 <- bruceR::import(here::here('data', 'penguin', 'penguin_rawdata.csv')) 下面是被读取的数据文件: 读取数据之后,我们可以使用View函数查看数据结构,包括站点、年龄等各种变量。我们还可以用colnames查看变量,因为可能需要选择一些我们认为比较重要的变量,比如有一些我们可能不需要的变量,就可以直接将其筛选掉。 #查看变量名(列名) colnames(df1) ## [1] "Site" "age" "sex" "monogamous" ## [5] "romantic" "health" "exercise" "eatdrink" ## [9] "gluctot" "artgluctot" "smoke" "cigs" ## [13] "avgtemp" "Temperature_t1" "Temperature_t2" "DEQ" ## [17] "AvgHumidity" "mintemp" "language" "langfamily" ## [21] "SNI1" "SNI2" "SNI3" "SNI4" ## [25] "SNI5" "SNI6" "SNI7" "SNI8" ## [29] "SNI9" "SNI10" "SNI11" "SNI12" ## [33] "SNI13" "SNI14" "SNI15" "SNI16" ## [37] "SNI17" "SNI18" "SNI19" "SNI20" ## [41] "SNI21" "SNI22" "SNI23" "SNI24" ## [45] "SNI25" "SNI26" "SNI27" "SNI28" ## [49] "SNI29" "SNI30" "SNI31" "SNI32" ## [53] "scontrol1" "scontrol2" "scontrol3" "scontrol4" ## [57] "scontrol5" "scontrol6" "scontrol7" "scontrol8" ## [61] "scontrol9" "scontrol10" "scontrol11" "scontrol12" ## [65] "scontrol13" "stress1" "stress2" "stress3" ## [69] "stress4" "stress5" "stress6" "stress7" ## [73] "stress8" "stress9" "stress10" "stress11" ## [77] "stress12" "stress13" "stress14" "phone1" ## [81] "phone2" "phone3" "phone4" "phone5" ## [85] "phone6" "phone7" "phone8" "phone9" ## [89] "onlineid1" "onlineid2" "onlineid3" "onlineid4" ## [93] "onlineid5" "onlineid6" "onlineid7" "onlineid8" ## [97] "onlineid9" "onlineid10" "onlineide11" "ECR1" ## [101] "ECR2" "ECR3" "ECR4" "ECR5" ## [105] "ECR6" "ECR7" "ECR8" "ECR9" ## [109] "ECR10" "ECR11" "ECR12" "ECR13" ## [113] "ECR14" "ECR15" "ECR16" "ECR17" ## [117] "ECR18" "ECR19" "ECR20" "ECR21" ## [121] "ECR22" "ECR23" "ECR24" "ECR25" ## [125] "ECR26" "ECR27" "ECR28" "ECR29" ## [129] "ECR30" "ECR31" "ECR32" "ECR33" ## [133] "ECR34" "ECR35" "ECR36" "HOME1" ## [137] "HOME2" "HOME3" "HOME4" "HOME5" ## [141] "HOME6" "HOME7" "HOME8" "HOME9" ## [145] "SNS1" "SNS2" "SNS3" "SNS4" ## [149] "SNS5" "SNS6" "SNS7" "ALEX1" ## [153] "ALEX2" "ALEX3" "ALEX4" "ALEX5" ## [157] "ALEX6" "ALEX7" "ALEX8" "ALEX9" ## [161] "ALEX10" "ALEX11" "ALEX12" "ALEX13" ## [165] "ALEX14" "ALEX15" "ALEX16" "KAMF1" ## [169] "KAMF2" "KAMF3" "KAMF4" "KAMF5" ## [173] "KAMF6" "KAMF7" "STRAQ_1" "STRAQ_2" ## [177] "STRAQ_3" "STRAQ_4" "STRAQ_5" "STRAQ_6" ## [181] "STRAQ_7" "STRAQ_8" "STRAQ_9" "STRAQ_10" ## [185] "STRAQ_11" "STRAQ_12" "STRAQ_13" "STRAQ_14" ## [189] "STRAQ_15" "STRAQ_16" "STRAQ_17" "STRAQ_18" ## [193] "STRAQ_19" "STRAQ_20" "STRAQ_21" "STRAQ_22" ## [197] "STRAQ_23" "STRAQ_24" "STRAQ_25" "STRAQ_26" ## [201] "STRAQ_27" "STRAQ_28" "STRAQ_29" "STRAQ_30" ## [205] "STRAQ_31" "STRAQ_32" "STRAQ_33" "STRAQ_34" ## [209] "STRAQ_35" "STRAQ_36" "STRAQ_37" "STRAQ_38" ## [213] "STRAQ_39" "STRAQ_40" "STRAQ_41" "STRAQ_42" ## [217] "STRAQ_43" "STRAQ_44" "STRAQ_45" "STRAQ_46" ## [221] "STRAQ_47" "STRAQ_48" "STRAQ_49" "STRAQ_50" ## [225] "STRAQ_51" "STRAQ_52" "STRAQ_53" "STRAQ_54" ## [229] "STRAQ_55" "STRAQ_56" "STRAQ_57" "socialdiversity" 另外,当我们需要进行问卷得分的计算时,我们可能需要选择与特定问卷相关的条目,然后对其进行预处理,以得到问卷得分。因此,我们必须要了解数据的当前结构,例如每个列的名称。我们得到的数据是比较干净的,每个名字基本上都可以反映出其大致信息,例如scontrol1、scontrol2等代表自我控制问卷的各个条目。这里的命名本身就是比较规范的。如果数据的列名不是这么清晰,可能还需要进行额外的处理步骤,比如重新命名问卷等。我们就不再一一赘述了。 那么,我们在这里的主要目标是什么,假设我们选择这几个变量,作为数据预处理的练习,我们把身体温度选出来,然后求一个平均的温度。 ●研究核心变量: CBT: 核心体温,测量两次,变量为Temperature_t1, Temperature_t2 CSI: 变量为social diversity Site: 数据源站点 DEQ: 距赤道的距离,变量为DEQ romantic: 是否处于恋爱关系,1=“yes”, 2 = “no” ALEX: 述情障碍,探索性监督机器学习需要的变量之一,5点量表,变量为 ALEX1-16,第4,12,14,16题反向计分 根据上述信息,我们需要计算一个关键变量,即“social diversity ”,它是文章中最核心的变量之一。同时,我们也将保留全球不同站点的信息(Site: 数据源站点 ),以便观察各地信息是否存在差异。另一个重要信息是距离赤道的距离,我们称之为“distance from equate”。此外,我们还需要记录参与者是否处于恋爱关系。 接下来,我们将从所有问卷中挑选出一个进行预处理,其他问卷的预处理可以依照相同规则进行。我们选出的问卷是关于“述情障碍”的,它旨在评估个体是否能够意识到并表达自己的情绪。这个量表在数据收集时已被纳入。 需要注意的是,量表中有些题目是反向计分的,在预处理时,我们必须将这些题目的得分反过来。这样,在计算总分时,我们可以直接通过相加的方式得到。 因此,我们的第一步是选择需要预处理的变量。第二步是检查这些数据类型是否符合我们的预期,并纠正任何偏差。然后,对于缺失值,我们可能需要决定是否删除或采取其他处理方式。在这里,我们可能会采取较为直接的方法,即直接删除缺失值。之后,我们将进行必要的运算,例如,选出与“述情障碍”量表相关的所有条目得分后,计算量表的总分。最后,如果我们对某个分组变量感兴趣,比如不同站点,我们可以通过分组的方式来快速计算一些统计量,例如均值、标准差等,以便快速掌握数据的一些基本信息。 好的,这就是我们现在给大家演示的几个数据预处理步骤。但大家可以看到,在不同的数据分析项目中,我们可能需要执行的步骤是不一样的,或者可能需要组合各种不同的步骤。总的来说,这里的几个步骤提供了一个基本框架。当大家实际进行数据处理时,可能需要根据自己的具体需求进行灵活的选择。 这里的好处在于,我们把步骤列出来,大家可以清晰地看到,在进行数据预处理时,我们首先需要明确我们的目标是什么,以及我们将采取哪些步骤来实现这些目标。这一点非常重要。如果我们不能清晰地思考这些步骤,那么我们就无法将它们转换成代码,也无法完成数据预处理。因此,在进行数据预处理时,最好是能够先明确你的目标是什么,你的第一步要做什么,第二步、第三步要做什么,你一定要把这些步骤思考得非常清楚。 当然,你可以边思考边尝试,尝试之后如果发现结果不符合预期,那么你就需要回头去修改你的步骤。但最终,你想要做好数据预处理,这些步骤肯定是需要清晰的。 好的,接下来我们将使用 Tidyverse 来对数据进行处理。首先,我们需要加载 Tidyverse 包。 # 不要忘记加载包 library(tidyverse) ## Warning:程辑包'tidyverse'是用R版本4.3.3来建造的 第一步我们已经提到过,就是选择变量。我们只选择与我们感兴趣的变量相关的部分,这里我们选择的是 Temperature_t1,Temperature_t2(两次测量的温度)、social diversity这个变量、site、DQ、romantic,以及ALEX1-16,这些代表述情障碍量表的条目。 在处理具有相同命名规则的数据或列名时,我们可以使用一个快速的方法,我们不需要把ALX1、ALX2、ALX3…一直到ALX16都写出来,我们只需要使用冒号来选择一个范围内的所有列,例如 “ALX1:ALX16”,这样 R 语言会自动为我们补全这个范围内的所有列名。 下面是在R中的代码: # 加载包后函数前不需要注明包,此处只是为了提示函数属于哪个包 # 选择我们需要的变量:Temperature_t1, Temperature_t2, SNI28-32, DEQ, romantic, ALEX1-16 df1 <- dplyr::select(df1, Temperature_t1,Temperature_t2, socialdiversity, Site, DEQ, romantic, ALEX1:ALEX16) select()函数会按照提供的参数顺序选择列 在我们使用的函数中,第一个参数通常是数据框(data frame),在这里我们已经将其读入并命名为 Beta frame。我们将使用的函数是 Tidyverse 中的一个包,叫做 dplyr,这是进行数据处理时最常用的包之一。我们用的是它里面的 select 函数,这个函数的使用非常直观,基本上可以理解为自然语言。 可以使用列名、范围(例如 starts_with()、ends_with()、contains()、matches() 等),或者使用 everything() 来选择所有列 在这个函数中,我们首先指定要从哪个数据框中选择,然后列出我们要选择的列名。在选择列时,我们可以使用列名,也可以使用其他方式来指定列名,比如列名的开头、结尾、包含的特定字符,或者是匹配模式。此外,我们还可以使用列的位置编号(1、2、3、4、5、6、7、8、9)来筛选,但这种方法不太常见,因为我们通常更关注列的名字,而不是它们的编号,尤其是当列的数量很多时,很难记住每个列的编号。 注意需要将函数结果赋值给一个新的变量/原始变量完成保存 需要注意的是,如果我们只写了dplyr::select 函数的后面部分,比如我们选中了DRR select,但没有将其结果赋值到一个新的变量中,那么这个选择操作虽然完成了,但没有被保存。在我们这里的例子中,我们选择完变量后将它们保存到了一个新的变量 df1 中,这样我们就把选择的结果存储到了 df1 这个变量名下。这时,数据框 df1 的内容就发生了变化,它不再包含之前那么多的列。 另外,我们可以直接使用 summary 函数来查看 df1的结果,这个函数可以帮助我们快速查看每个变量的基本情况,比如最小值、最大值等。 summary 函数是 R 语言基础包中的一个函数,我们通过它可以快速检查数据的一些基本统计特征。例如,我们在summary 函数的结果中发现 temperature 的最大值是1,000,我们立刻就能识别出这是一个异常值,因为人体的核心温度不可能达到这么高。此外,summary函数还会告诉我们每个变量有多少个缺失值(NA),这样我们可以快速了解数据的质量。 如果我们发现某些数据类型不正确,比如我们希望某个变量是数值型(numeric),但它实际上被读取为字符型(character),我们就需要进行数据类型的转换。上节课我们讲过,数据类型转换时我们使用 as.numeric 函数,将字符型数据转换为数值型。 在进行数据类型转换时,我们需要使用另一个在数据处理中常用的函数,即 mutate 函数。 # 转换数据类型 # 这里数据类型是正确的,只是示例 df1 <- dplyr::mutate(df1, Temperature_t1_new = as.numeric(Temperature_t1), Temperature_t2 = as.numeric(Temperature_t2)) mutate()函数常用于创建新的变量或修改现有变量 在 mutate 函数中,我们首先指定数据框,然后进行所需的操作。例如,我们可以将 temperature_t1 变量通过 as.numeric 函数转换为数值型,然后再将转换后的数据复制回原来的列中。这样,整个数据框的列名保持不变,但数据类型已经发生了变化。 存在多种变式,如mutate_at()通过列名、位置或者列的类型进行选择,mutate_if()对数据框中满足条件的列应用指定的函数 那么,mutate函数我们可以理解为是一个函数家族,它包含了一系列类似的函数。例如,mutate_at函数允许我们通过指定的列名进行有选择性的变换,而mutate_if函数则是通过条件来决定是否进行变换。在mutate函数的内部,我们可以使用各种函数来对数据进行操作。比如,我们在这里将temperature_t1变量通过as.numeric函数转换为数值型,这个as.numeric就是我们在这个函数内部,在变换的时候所使用的一个函数。 mutate()内使用函数时,同样需要注意缺失值的问题 在这个时候,我们需要注意的是,原始函数中的一些注意事项在我们使用mutate的时候仍然适用。例如,通常在求均值或者进行其他计算时,如果遇到缺失值,函数可能无法处理,或者处理结果会与预期不同。因此,在使用mutate时,如果内部使用的函数遇到缺失值,它仍然可能无法处理,可能会报错,或者得到的结果不符合预期。所以,所有在mutate内部使用的函数,我们都必须特别注意它们对缺失值的处理方式。 注意mutate()进行转换之后需要进行核查:是否符合预期 还有一个重要的注意事项是,在完成数据类型转换之后,需要进行检查,以确保数据已经转换成了你期望的样子。有时候,即使没有报错,转换的结果也可能并不是你所预期的。在这种情况下,你可以检查数据,以确定哪个地方出了问题。 注意需要将函数结果赋值给一个新的变量/原始变量完成保存 同样,如果我们只进行了转换操作,而没有将结果赋值到一个新的变量中,那么这个操作虽然完成了,但结果并没有被保存下来。您可以看到,在这里我们直接将转换后的结果赋值到了原来的变量名中,这样就替换了原来的变量。我们这里没有保存中间变量,但通常来说,我们可能会将结果保存到一个新的变量中,例如 df_new,以保留中间结果。在我们上面的例子中,还没有这样做。 稍等一下,我还是操作一下,不然就只是纯讲代码了。 这是我们第六课的notebook,我们可以从这里开始,从导入数据开始。 在这个notebook里面,右上角有一个绿色的小三角形,你点击一下,它就会运行整个代码框。 大家可以看到,这里的三个间号(…)加上一个r,这里表示代码框的起始点,下面再跟三个间号(…)表示代码框的终点。这样它只会运行这里的R代码,而且一定要加上r,表示这是R代码。 我们导入数据之后,这里有一个df1,它有1,517行、232列,这里叫做variables。我们可以查看一下它的情况。 我们前面说在studio里面有可能显示不全,在课件里面也可能显示不全。但在这里,它会显示得非常全,我们可以拖着看,它有两页。我们也可以去查看它的列名,在这里我们可以非常全地看到它的列名。(前面的…需要再确认,每个代码都需要加上) # 查看变量名(列名) colnames(df1) ## [1] "Temperature_t1" "Temperature_t2" "socialdiversity" "Site" ## [5] "DEQ" "romantic" "ALEX1" "ALEX2" ## [9] "ALEX3" "ALEX4" "ALEX5" "ALEX6" ## [13] "ALEX7" "ALEX8" "ALEX9" "ALEX10" ## [17] "ALEX11" "ALEX12" "ALEX13" "ALEX14" ## [21] "ALEX15" "ALEX16" "Temperature_t1_new" 然后加入Tidyverse的包。 # 不要忘记加载包 library(tidyverse) 好的,这里就是我们第一步。 然后我们运行dplyr::select选择需要变量。 大家可以看到,我们刚才在数据集中有232个变量。通过选择之后,因为我们没有保存中间变量,所以这里只保留了我们所选择的22个变量。这意味着其他200多个变量已经被直接丢弃了。当然,在R中这并不是一个大问题。只要我们没有从原始数据中删除这些变量,我们就可以重新读取数据,继续处理那些被排除在外的变量。在R中,数据的处理是灵活的,我们可以根据需要随时对数据集进行操作和变换。 然后我们可以在这一步使用summary函数来查看数据集的概要。 虽然在课件里面我们可能没有看到完整的概要信息,但在这里我们可以看到每个变量都会有一个对应的概要输出。 通过运行summary函数,我们可以快速了解每个变量的基本统计信息,如变量的数量、缺失值的数量、变量的最小值、最大值、中位数等。这对于初步的数据探索和异常值检查非常有帮助。 然后,关于mutate函数,我们可以看到,由于这里的数据原本就是正确的,例如,它能够给出均值,这表明它原本就是数值型的(numeric)。而对于站点(site)来说,大家可以看到它是字符型的(character)。在这里,我们还是可以进行一些示例操作,比如将某些变量转换为数值型。 我们在这里直接添加了一个new,这是作为一个示例,表示我们正在生成一个新的变量。 我们刚刚提到,mutate函数不仅可以修改原始变量,还可以生成新的变量。我们可以通过这种方式来修改数据。 那么,mutate函数不仅能够修改原始变量,也可以生成新的变量。 在这里,我们添加了一个new,表示我们正在创建一个新的变量。因此,即使我们没有保存中间变量,我们仍然可以生成新的变量。 对于处理缺失值的问题,例如,有些地方确实存在缺失值。由于有缺失值,我们需要将其删除。 # 按照Temperature, DEQ处理缺失值 df1 <- filter(df1, !is.na(Temperature_t1) & !is.na(Temperature_t2) & !is.na(DEQ)) filter()函数用于从数据框中筛选行(观测值),可以通过逻辑运算符组合多个条件 在这里,我们可以使用filter函数来进行行过滤。filter函数与select函数不同,select函数是选择列,而filter函数是选择行。在这个函数中,我们是通过逻辑判断来筛选行,例如,对于某个变量,我们只保留那些不是缺失值(NA)的行。 例如,对于temperature_t1,我们想要保留所有第一次测量温度时不是缺失值的行。我们使用!is.na()函数来进行逻辑判断,其中!表示逻辑上的”非”,即不是缺失值。通过这样的处理,我们可以筛选出符合特定条件的行,以便进行进一步的数据分析。 运算逻辑:遍历每一行,将给定的条件应用于该行,条件为真则保留,保留的行被组成一个新的数据框作为函数的返回值 我们得到了一个与我们的temperature_t1列函数长度相同的新向量。这个新向量中全是”true”和”false”。例如,如果它是第一个值,它是有数值的,那么这个时候它就不是NA,所以它就是”true”。然后以此类推,一直到1,517行,它都会进行一次判断,形成一个向量,这个向量里面全都是”true”和”false”。我们之前说过,实际上我们也可以通过”true”和”false”来进行选择。选择时,它会自动保留那些”true”的行,把那些”false”的行去掉。 这里的”filter”其实就是这个作用,但我们这里的筛选条件可能不止一个。我们需要不仅仅第一次的体温测量是有数值的,第二次的也需要有,而且我们还需要有地区的,跟赤道的距离的一个数据。实际上,我们是把三个逻辑的条件进行的一个用”and”(且)的关系连接起来。相当于是我们每一个逻辑判断都会得到一个新向量,得到一个适合去和”false”进行比较的一个向量。三个项通过”and”(且)的关系连起来之后,就是说三个都是等于”false”的时候,那么这一行它才是”false”。如果三个判断当中有一个是”false”的话,那么它最终的这个逻辑判断结果就是”false”。 所以,通过这种方式,我们就得到了一个非常符合我们条件的一个向量。它是一个”true”和”false”的向量。然后,我们通过filter这个函数,对我们的数据框df1进行一个筛选。 注意需要将函数结果赋值给一个新的变量/原始变量完成保存 同样,我们筛选完了之后,我们要把它保存起来。 大家可以看到,我们现在的df1有1,517行。如果我只运行后面这个,大家可以看到我选中了filter后面的这些代码,然后用Ctrl加Enter的话,那么它只是把这个结果打到了我们下面这个输出这个地方。 大家可以看到,它这里显示叫做description,然后df1是1,517行乘以23。也就是说,实际上如果我们只运行这个filter这个函数本身的话,它就给我们一个输出,但是它没有把它复制到任何一个变量。因为我们只运行后面一半,所以它没有把它复制到df1。所以df1这里还是1517行。大家注意到这个区别。 所以,我们一定要把它整个运行一下,就是从df1然后加上赋值符号,一直到最后。然后我们这样运行的话,就把它赋值到df1。 大家可以看到,这个时候刚才的是1,517,现在就变成了1,427,也就是它筛掉了一些有缺失值的数据。OK,那么我们就筛选出来了我们想要的数据,而且我们也选择出了我们想要的变量。那么接下来的话,我们就可以进行一些运算了。那么运算的过程中,其实我们就是要生成一个新的一个变量。 例如,如果我们想要计算temperature_t1和temperature_t2的均值,那么这个时候我们实际上是对每个个体的这两列进行均值计算,而不是对temperature_t1的所有个体求均值。这两个操作是不一样的:一个是每个个体有两列数据,我们以”行”为单位进行求均值;另一个是求temperature_t1这列的均值。一般来说,我们的数据框是以”行”为单位的,所以我们需要用某个变量来以”行”为单位进行求均值。 # 计算每个被试两次核心温度的均值,保存为Temperature df1 <- dplyr::mutate(df1, Temperature = rowMeans(select(df1, starts_with("Temperature")))) mean()函数用于计算向量或数组的平均值,colMeans()函数用于计算矩阵或数据框的每一列的平均值,rowMeans()函数用于计算矩阵或数据框的每一行的平均值 数据类型需为numeric starts_with()用于在数据框中选择列名以特定字符串开头的列 在这个函数中,我们又有一个函数嵌套了一个函数,即select函数。select函数是我们前面讲过的,用于选择哪些列作为输入来求均值。我们通过这个选择,选择出了以temperature开头的这些变量,这里只有两个变量,即temperature_t1和temperature_t2。然后,我们来求rowMeans,即以”行”为单位来求均值。求完了之后,得到了一个向量,我们把这个向量赋值到一个叫做Temperature的新变量里面。然后,这样的话他就得到了一个新的变量。我们可以演示一下。 这个就是两次temperature的一个均值Temperature,右边也对应地多了一个变量。 # 将4, 12, 14, 16题反向计分,计算ALEX,保存为ALEX df1 <- mutate(df1, ALEX4 = case_when( TRUE ~ 6 - ALEX4# 反向计分:6减去原始值 ), ALEX12 = case_when(TRUE ~ 6 - ALEX12), ALEX14 = case_when(TRUE ~ 6 - ALEX14), ALEX16 = case_when(TRUE ~ 6 - ALEX16) ) #也可以写成 case_when(ALEX4 == '1' ~ '5',ALEX4 == '2' ~ '4', ALEX4 == '3' ~ '3', ALEX4 == '4' ~ '2', ALEX4 == '5' ~ '1',TRUE ~ as.character(ALEX4)) case_when()函数是一个强大的条件判断函数,通常用于根据不同的条件生成新的变量或对现有变量进行转换 运算逻辑:逐行评估每个条件,并根据条件的结果来确定新值,若条件为真,则用‘~’后的值替换原始值 有多个条件时,按照条件的顺序逐个进行判断,一旦有条件满足,则返回对应的值并停止继续判断其他条件 使用 TRUE ~ 或者 TRUE ~ NA处理未匹配到任何条件的情况,这样可以确保即使所有条件都不满足时,函数也会返回一个默认值,避免产生错误 接下来,我们在下一步可能会使用的一个常见操作就是进行反向计分。反向计分有多种方式,我们在这里介绍的是一个比较通用的方法,称为case_when()。首先,我们可能都是在mutate函数里面进行操作的,因为我们要修改原来的变量。所以,我们会在mutate里面进行操作。然后,我们使用例如ALEX的12、14和16这几个条目进行反向计分。这个时候有几种方式都可以,比如我们这里使用case_when()。 在case_when()函数中,我们首先设定条件判断,对于每一页的数据,只要有这个数值存在,我们就通过减去原始分数的方式来反向计分。这个条件判断函数可以对很多条件进行判断,然后用波浪号(~)来表示如果符合这个条件,我们就执行波浪号后面的操作。大家可以看到,我们这个地方直接就是用TRUE ~ 6,TRUE表示我们对于这一点,我们都要做后面的操作,即用这个反向计分的方式。 我们可以看一下转换之后的结果。例如,原来的2分通过反向计分后变成了4分,原来的1分变成了5分。这是心理学中常用的反向计分方法。 对于case_when,实际上也可以采用其他条件,比如对年龄进行操作,如果年龄小于18岁,就将其修改为\"Child\";如果年龄在18岁到65岁之间,就修改为\"Adult\";如果年龄大于65岁,就改为\"Senior\"。这样也是可以的。 # age为num case_when( age < 18 ~ "Child", age >= 18 & age < 65 ~ "Adult", age >= 65 ~ "Senior" ) # age为chr case_when( age < "18" ~ "Child", age >= "18" & age < "65" ~ "Adult", age >= "65" ~ "Senior" ) 大家还记得我们在上节课讲解数据操作时所做的逻辑判断吗?实际上,在很多情况下我们都可以使用这种逻辑判断。因此,我们可以认为case_when()是一个通用的数据操作方式。另外一个通用的方法就是ifelse()。当然,我们在上节课讲到的那个条件判断if (condition) { do_this; } else { do_that; },在mutate函数里面也有类似的用法,就是将if和else连在一起写。 在mutate函数中,第一个参数是条件,第二个参数是条件为真时要执行的操作,第三个参数是条件为假时要执行的操作。如果我没有记错的话,它应该是ifelse(condition, do_this, do_that)。这里,第一个和第二个参数是条件,然后第三个参数是当条件为真时要执行的操作,第四个参数是当条件为假时要执行的操作。 大家可以看到,ifelse()也是一种常用的数据转换方式,用于对符合条件的数据进行转换。但是,如果我们需要判断多个条件,我们就要写很长的一串条件判断语句,就像是一串钥匙。在这种情况下,case_when()就是一个更好的选择,因为它允许我们更清晰地定义多个条件,并针对每个条件指定不同的操作。 另外,如果我们想要进行反向计分,实际上我们也可以通过这个bruceR来实现。 # 创建一个包含需要反向计分的变量的列表 vars_to_reverse <- c("ALEX4", "ALEX12", "ALEX14", "ALEX16") # 对列表中的变量进行反向计分 df1$ALEX <- bruceR::SUM(df1, varrange = "ALEX1:ALEX16", rev = vars_to_reverse, likert = 1:5) 因为之前提到过bruceR是专门针对心理学的一个软件包,在bruceR这个软件包中,它实际上有自己的更符合我们思维方式的反向计分方式。所以,如果有哪些需要进行反向计分,我们直接在求问卷总分的时候就可以有一个rev()的操作rev = vars_to_reverse,我们知道我们的变量名是从ALEX1到ALEX16,那么在求总分的时候,我们就可以直接把这些需要反向计分的分数写到这里,同时也可以把这个分数范围记录方式也算上:likert = 1:5,它就可以自己帮你完全操作。也就是说,你甚至不需要先把它反向计分,然后再求和,而是只要告诉broomR,在求总分的时候你需要哪些条目进行反向计分就可以了。这种方式比较符合我们心理学这个思维方式。 但实际上我们自己要算的就是第一步,就是通过case_when()或者其他的方式,或者以filter()等方式,去把这个条目的分数反过来,然后是1-5分,我们反向基本就是6减去这个原始的分数,然后我们再去求这个总分。那么我们求总分和跟之前实际上是一样的,就是用summarise()。然后再加上选择以ALEX开头的这些所有的行。 然后我们最后一个操作,比如我们想要进行这个分组的比较或者是分组的查看,比如我们想看在这个数据里面不同的站点,他们的平均分数是否有所区别,那么我们就可以,比如以站点作为分组的一个条件,首先把数据框分成不同的组,然后在不同的组里面,我们就求这个描述性的数据。 # 按Site计算Temperature的平均值 df1 <- dplyr::group_by(df1, Site) df2 <- dplyr::summarise(df1, mean_Temperature = mean(Temperature), n = n()) df1 <- dplyr::ungroup(df1) group_by()函数将数据框按照指定的分组变量进行分组,然后可以对每个分组进行单独的操作,如汇总、计算统计量等。 这里会涉及到几个函数,第一个就是分组,我们把这个数据框按照某个变量进行分组,那么也就是说这个站点里面它有几个独特的值,我们就把它分成几个组。然后这个site里面我们应该是有12个还是15个独特的值,比如有一个中国的站点,有一个英国的站点,有一个德国站点。那么我们就会按照这个站点的名字把它分成不同的组。 在完成分组操作后,建议使用ungroup()函数取消数据框的分组状态 summarise()函数用于对数据框进行汇总操作,常与group_by()连用 然后第二个就是summarise(),就是说我们去求这个数据框里面的做一些运算,然后把它运算完了之后,返回成一个列。然后我们这里就是把这个两次体温测量的均值按照站点做一个求平均。因为我们前面有一个分组,所以通过这个summarize()之后,我们把它赋值到df1。那么这个时候df1里面,它就会自动的有一个分组的标签。那么当你做第三个group_by()的时候,它会自动的分组来做你要做的这个运算,也就是说,比如我们这个size里面有十几个size,它就会对每一个size的数据都去求这个温度的平均值。然后有15个站点,我们最后就得到15个站点和每个站点的平均体温。 在summarise()函数中,可以使用各种统计函数来计算汇总统计量,例如n()、mean()、sum()、median()、min()、max()、sd()等 最后一个就是group_by(),就是我们用完了group_by()之后,即便我们求了这个summarize(),它这个分组标签还在里面,我们一般来说要把这个分组标签给去掉,就用ungroup()这个函数。如果不做这个函数的话,它后面就会一直要求保留这个分组变量在这个数据框里面,可能到后面你希望把这个分组变量不要了,但是如果你不把它去掉的话,它可能会一直保持在里面,会引起一些很奇怪的结果。所以这是分组进行一个数据描述,就是有两个主要的函数,一个是group_by(),第二个就是summarize(),我们通过summarize()这个函数对数据框里面进行汇总的操作。在这个汇总操作里面,有常用的这种统计的函数,比如有n(),因为心理学有的时候我们想要知道某个条件之下有多少个人,那么我们希望之后就是n(),然后就是mean(),sum(),还有median(),就是我们常用的一些统计量。 然后,运行一下代码没有问题。然后这个地方也是一个case_when()的演示。这个地方我们跳过了注册的部分,因为后面还有很多内容。那么这个地方是求Alex这个问卷得分的和,问卷得分叫做问卷得分。然后这个地方,我们可以用bruceR的方式来求。然后这个地方,就是我们刚才讲group_by()之后的一个特点。我们这里可以先看一下,在group_by()之前和group_by()之后,我们可以看一下。比如我们可以看这个地方,我们直接选中df1,然后它有一个description,然后我们把它分组之后重新赋予它。然后我们再查看一下,大家可以看到,这里的话,它跟之前就不太一样了。它这里有个groups,然后有个set 15,就表示它打成了一个分组的标签。如果你不用ungroup()的话,后面这个标签会一直在。 我们现在比如说用这个group_by()的值对吧,去求一个值。那么我们把这个summarize()之后的赋予到了一个新的一个变量,变量名叫做def2。我们可以看一下def2的话,它其实只有两个variables,第一个就是site,第二个就是main temperature。也就是说,它这个summarize就是我先根据你的分组这个标签,每一组球运算出你要得的东西,然后呢就把它返回。就形成了一个你分组的这个变量和你要求的这个结果这个描述性统计放在一起。然后你再增加一个,比方说你n等于n。我们要求一下它的这个多少个样本。那么这个时候大家可以看到,这个时候这个table就变成了除了site以外又增加了一个变量。它不仅有main temperature,还有一个n,就是我们关心的这个样本量。这个时候我们看到,每个site的样本量都是不一样的。那么这个对我们去描述数据的时候,或者是你要想把这个数据当中的一些描述性统计提取出来之后,是非常有帮助的。 那么我们也可以看一下df2,它会是这样的一个table。然后df2的话,它还是有这个group的这个标签的。那么如果我们把它ungroup()之后再来看的话,它这个group标签就应该就没有了。OK,前面我们讲的是分组。 7.2.2 操作步骤|完整的管道操作 我们之前跟大家说过,实际上我们可以通过Tidyverse的管道操作来进行数据处理。这样的话,没有任何的中间步骤,我们就可以直接从某个数据框的输入开始,最后得到我们整个一系列处理之后的结果。通过管道进行操作,我们就可以直接把它连起来。 # 用管道操作符合并以上代码 # 使用管道操作符时建议先单独查看变量的数据类型,转换完毕后在进行操作 # dplyr::glimpse(penguin_data) df2 <- df1 %>% dplyr::select(Temperature_t1, Temperature_t2, socialdiversity, Site, DEQ, romantic, ALEX1:ALEX16) %>% dplyr::filter(!is.na(Temperature_t1) & !is.na(Temperature_t2) & !is.na(DEQ)) %>% dplyr::mutate(Temperature = rowMeans(select(., starts_with("Temperature"))), ALEX4 = case_when(TRUE ~ 6 - ALEX4), ALEX12 = case_when(TRUE ~ 6 - ALEX12), ALEX14 = case_when(TRUE ~ 6 - ALEX14), ALEX16 = case_when(TRUE ~ 6 - ALEX16), ALEX = rowSums(select(., starts_with("ALEX")))) %>% dplyr::group_by(Site) %>% dplyr::summarise(mean_Temperature = mean(Temperature)) %>% dplyr::ungroup() # 查看数据 df2 大家可以看到,这里有一个值得注意的地方,就是我们原始数据直接通过这个管道输入到后面这个函数周围,作为它的一个输入。那么我们第一个argument就是这个dataframe,就可以省掉了。比如说我们之前做操作的时候,跟select()对吧,第一个参数就是df1,然后后面再连我们要选的这些内容。 在这个管道里面,我们可以直接把它省略掉,也可以我们用一个点逗号来表示,用一个点来表示是管道输过来的这个变量。一般来说,如果是这样一个管道操作的话,你把它做这个函数的第一个argument的话,就可以省略了。就是我们尽量把前面的完全连起来,最后就直接就变成了我们想要的这个结果了。 大家可以看到,在管道操作当中的话,看起来整个数据就是比较简洁的。从输入,到整个操作,完了之后就输出。 就是我们刚才看到的这个内容。我们这里这是原来的这个代码,因为没有加上n,所以说还是只有这个mean_Temperature。 那么我们可以看一下对问题系数的这个统计。 7.2.3 小结 数据的预处理主要依赖dplyr包,常见函数总结如下 filter() 选择符合某个条件的行(可能代表一个被试的数据) 我们常常用的函数有filter(),就是选择行,select()就是选择列 mutate() 创建新的变量或修改现有变量 对变量进行操作之后生成新的变量或者是修改现有的变量 case when() 重新编码变量 还有case_when()就是在这个mutate()组里面通过条件来重新编码 group_by() 依据某些变量产生的条件,给数据分组 如果使用 “group_by”, 一定要在summarise后使用 “ungroup” group_by()和summarise()还有ungroup这三个通常一起来使用,来进行分组做一些运算 summarise() 进行某些加减乘除的运算 ungroup() 取消刚刚进行的分组 select() 选择进行分析时需要用到的变量,同时也起到了为所有变量排序的功能 arrange() 某一列的值,按照某个顺序排列(其他列也会随之变动) 还有一个常用的函数,因为我们描述了一个比较干净的数据,所以我们没有用到它,就是rename(),就是重新命名。你刚开始拿到的数据,可能这个列的名字你一看,哇,非常不规范。比如是S1、S2一直到SN,你觉得这个对你来说没有任何意义,你想把它修改一下,那你就可以用rename()这个函数。这个不难,就是操作跟我们这里的都是类似的,大家可以搜索一下。 大家如果后面想要强化一下这一小节学的内容的话,你们可以去这个上面里面做一下。 练习 分步骤使用bruceR计算ALEX的值,保留ALEX在30-50间的被试,按照langfamily进行分组,计算Temperature均值 使用管道操作符合并上述代码 按照langfamily进行分组计算DEQ, CSI的均值 比如去算这个得分,然后保留特定得分的缺失值,然后按照langfamily的分组,然后计算campage均值,然后用管道进行操作。那么最后,你可以分组计算它的这个DEQ和CSI的均值。 大家如果想要对这一节内容进行更加深入的掌握,最好是进行一些练习,因为要提高编程能力,没有其他的捷径,就是多写、多敲、多犯错。 我们还有一个研究内容,那就是反应时间的数据,这也是心理学经常会碰到的问题。 7.3 反应时数据 7.3.1 研究问题 & 数据情况 课程将重复Hu et al.,2020作为反应时数据分析的示例 研究问题: 探究人们对自我相关刺激的优先加工是否仅在某些条件下发生 研究假设: 无论参与何种任务,与积极概念(好我)建立联结的自我形状会在反 应时间和准确性上表现更快更准确 我们这里简单地说,就是探讨个体是否会对自我相关的刺激进行优先加工。这是很多之前的研究都发现了的。那么我们可以探讨的是,这个优先加工是否只与特定的自我相关内容相关,而其他的虽然也跟自己相关,但可能不会被优先加工。 例如,在社会心理学中有一个概念叫做自我提升(self-enhancement),即大家都会觉得自己特别好、特别积极、特别幸运,前途一片光明。这是我们在一九八几年社会心理学中发现的一个积极的自我优势。那么在这个认知性项目中,是否也会存在这个优势呢?也就是说,跟我自己相关的信息中,有的是好的,有的是不好的,有的是我喜欢的,有的是我不喜欢的。那么跟我自己喜欢的这些信息,我才进行优先处理;跟我自己没有那么喜欢的,我可能不会那么优先处理。 我们做一个简单的实验,就是将几何图形和非常简单的出现概念进行连接。 例如,好的自己——好我,好的他人——好人、坏的自己——坏我,坏的他人——坏人等进行连接。然后,我们做一个简单的匹配任务,就是在上面呈现一个图形,下面呈现一个文字标签,然后去判断这个图形和标签是否符合你刚才学习的那种关系。我们的假设是,可能只有当这个图形和好的自己相联系的时候,我们的反应才会又快又准。这是我们的研究假设。 研究确实发现,绿色的条件代表自我的条件。大家一定要记住,good代表好的,bad代表坏的。那么,good的条件下,我们反应的时间是非常快的;而相对的,bad条件下,即坏我的时候,反应的时间就变慢了。 当然,我们主要关注的是这个数据。在其他实验中,我们也得到了一致的结论。我们主要就是关于这个matching touch的数据。那么我们来看一下这个数据,它是保存在这个data/match中。 数据情况: 数据保存于data/match文件夹下 N(被试) = 44, N(files) = 44 形状标签匹配任务数据命名为data_exp7_rep_match_*.out 下面这个文件夹,我们看一下,它这里有一堆以”.out”结尾的文件。对吧,它有一个基本的命名规则,就是data_experiment_7.然后是rap,然后是match,然后是下划线,后面就是编号。那么这里的前面部分实际上是对这个实验的一个描述。然后是Mac,表示matching task,就是我们关注的匹配任务。后面这个编号就是这个被试的编号。总共有44个被试,所以有44个这样的文件。 我们主要的变量就是这个标签,就是一个图形和一个标签,他们所代表的概念以及他们之间的关系。我们关注的因变量就是被试的反应是否正确。是不是有两个,它符合原来关系的时候,被试的判断是匹配;不符合原来他学习的关系的时候,他直接就反应不匹配。 我们还有两个变量,一个是-1,表示按了6键;一个是2,表示按了两个键或者按了别的键。然后这个是属于错误的反应。然后呢,对被试的反应时间,我们比如说要把这个小于200毫秒的,比如40毫秒的都去掉,因为人的这个反应不可能小于200毫秒。 接下来,我们将查看数据,并了解其结构。 主要变量: Shape/Label: 屏幕呈现的图形代表的概念 Match: 图形与呈现的标签是否匹配 ACC: 被试的判断是否正确,1 = “正确”, 0 = “错误”, -1, 2表示未按键或按了两个键的情况,属于无效作答 RT: 被试做出判断的反应时,[200,1500]的反应时纳入分析 这些数据看起来是这样的,对吧?如果我们打开这些文件,我们现在需要计算自我优势效应,即在匹配效应下,好的我和好的他人之间的差异。在这种情况下,好的我和好的他人都是好的,但是我的反应速度比他人快,这表明我们对自身的信息有一个优势效应。因此,我们需要批量读取数据,但由于有44个文件,我们不可能手动输入每个文件。我们必须使用代码来提高效率。 接下来,我们需要拆分变量,了解如何对字符进行拆分,以及如何将长数据转换为宽数据。我们将稍后讨论这些操作。 7.3.2 操作步骤 数据预处理目标:计算实验条件为Match-Moral时RT的SPE。 Step1: 批量读取并合并数据[for loop] Step2: 选择变量[select] Step3: 处理缺失值[drop_na, filter] Step4: 分实验条件计算变量[group_by, summarise] Step5: 拆分变量[extract, filter] Step6: 将长数据转为宽数据[pivot_wide] Step7: 计算实验条件为Match-Moral时RT的自我优势效应[mutate, select] 最后,我们将计算差异。在这种情况下,我们可以使用mutate函数来进行计算。 虽然这里还有一半的内容,但很多内容都是我们之前在数据顺序中讲过的。我们将重点讲解之前未讲过的内容。 Step1: 批量读取并合并数据[for loop] 在选用读取数据的函数时要注意函数默认的分隔符(参数sep),如read.csv默认为”,“, read.table默认为” ” .out文件是以空格或制表符分隔的文本文件 # 查看单个被试的数据 # 查看数据时要注意所需变量的数据类型,如果存在问题需要提前转换 p1 <- utils::read.table("data/match/data_exp7_rep_match_7302.out", header = TRUE) p2 <- utils::read.table("data/match/data_exp7_rep_match_7303.out", header = TRUE) 首先,我们将批量读取数据。我们刚刚提到有40个被试的数据。我们可以先读取一两个被试的数据,以查看其结构。在这里,我们将读取P1和P2。 我们使用的是read.table函数。为什么推荐使用这个函数呢?因为它实际上可以读取所有以.txt结尾的文本文件。它还可以设置分隔符,这是相当灵活的。如果我们开始尝试读取CSV文件,发现效果不理想,我们也可以使用read.csv。当然,你也可以使用readxl包,它能够自动识别文件类型。 当我们读取一个文件后,我们就可以看到它包含了日期、是否是练习或正式实验、被试编号、年龄等信息。 如果我们读取了两个文件后,我们可能想要将它们合并。通常,我们会通过行合并(rbind)来完成这个操作。当然,我们也可以使用dplyr包中的bind_rows函数。 # 将两个被试的数据合并 df3 <- base::rbind(p1, p2) df3 <- dplyr::bind_rows(p1, p2) rbind()函数,用于合并两个或多个数据框、矩阵、数组或列表,并将它们按行连接成一个新的对象,其中的 “r” 代表 “row”。 通常情况下,rbind会返回一个矩阵,但输入对象的特性(如列名、列属性等)也可能导致rbind()返回数据框。想要获得数据框最好使用bind_rows()函数。 合并数据框时,要确保被合并的数据框具有相同的列数和列名。如果列名不同,bind_rows()会尝试按照列名的顺序进行合并。如果无法自动匹配列名,则会产生错误。 还有一个问题,如果我们有40个被试的数据,我们不可能手动从P1到P40进行合并。这会非常累,而且bind_rows函数也会越写越长。我们肯定不希望以这种方式解决问题,尤其是当我们有更好的循环结构时。 for循环是一个很好的解决方案。它可以按照某个预设的序列重复执行某个操作。基本的语法是for (i in 1:10) { ... },其中i在1到10的序列中依次进行迭代。 虽然可以通过逐个导入数据并逐个合并得到最终的数据,但这样费时费力,且代码冗余。 编程中常用迭代结构执行重复操作,如for loop。 for loop的基本语法如下:for (variable in sequence) { # 在这里执行循环体操作 } # 单个操作循环,打印i + 1 for (i in 1:10) { print(i + 1) } ## [1] 2 ## [1] 3 ## [1] 4 ## [1] 5 ## [1] 6 ## [1] 7 ## [1] 8 ## [1] 9 ## [1] 10 ## [1] 11 每次迭代时,它会将i加1的结果打印出来。例如,如果i的值是1,它会打印出2;如果i的值是2,它会打印出3,依此类推。 使用for循环,我们可以轻松地读取并合并所有的被试数据,而不需要手动操作或编写冗长的代码。 所以,这就是为什么它被称为循环,因为它在一个特定的序列下进行循环。for循环就这么简单。我们用i <- 1:5,它就会打印出1到5。 # variable in sequence for (i in 1:5) {print(i)} ## [1] 1 ## [1] 2 ## [1] 3 ## [1] 4 ## [1] 5 还有其他方式,比如i <- seq(1, 5),它也会打印出1到5。 for (i in seq(1, 5)) {print(i) } ## [1] 1 ## [1] 2 ## [1] 3 ## [1] 4 ## [1] 5 例如,如果你使用Vector 12345,它也会打印出来。 my_vector <- c(1, 2, 3, 4, 5) for (i in my_vector) {print(i) } ## [1] 1 ## [1] 2 ## [1] 3 ## [1] 4 ## [1] 5 对于list也是如此: my_list <- list(a = 1, b = 2, c = 3) for (element in my_list) {print(element) } ## [1] 1 ## [1] 2 ## [1] 3 还有一种方式,比如我们可以对字符串进行循环,打印出字符串中的每一个字母。 关于string的循环: my_string <- "world" for (i in 1:nchar(my_string)) {print(substr(my_string, i, i)) } ## [1] "w" ## [1] "o" ## [1] "r" ## [1] "l" ## [1] "d" 这里想要展示的就是for循环是一个非常强大的工具,我们可以用在很多循环中,完成我们想要做的事情。 这意味着什么呢?就是for循环也可以与我们前面讲的条件语句结合。我们前面讲条件语句的时候,就说达到某个条件然后做什么。如果我把这个for循环与条件语句结合起来,这就意味着我可以根据不同条件做不同的事情。在这里,我只对i大于5的时候打印出i + 1。这意味着当i不大于5的时候,什么事情都不做。所以我们可以看到,它打印出来的就是6,因为i大于5的时候就是6、7、8、9、0,它只会打印出后面的这些数字。 我们也可以将for循环与前面学到的语句,甚至与函数进行各种组合,以达到我们的任务。 # 加简单条件 for (i in 1:10){ if (i > 5) { print(i + 1) } } ## [1] 7 ## [1] 8 ## [1] 9 ## [1] 10 ## [1] 11 当然,刚开始的时候,你可以去模仿或者做一些简单的事情。等你熟练之后,你会发现for loop非常有帮助,结合函数和条件语句。 那么,我们讲完for循环之后,我们刚刚提到的,其实整个文件夹中的文件是非常有规律的。我们能否将所有的文件名先读到一个列表中,变成一个字符串的向量?然后我们就依次去读这个向量的名字,对吧,然后把它放在一个for循环里面,就依次去读每一个文件名,然后再在这个里面跟它们进行合并?这样的话,即便我们有100个、1000个被试,我们也不怕,我们也不需要手动编写代码。一个for循环就解决了这个问题。 那么要如何利用for loop批量导入数据呢? # 把所有符合某种标题的文件全部读取到一个list中 # 使用 full.names 参数获取完整路径的文件列表 files <- list.files(here::here("data", "match"), pattern = "data_exp7_rep_match_.*\\\\.out$", full.names = TRUE) head(files, n = 10L) ## [1] "/Users/hcp4715/Library/CloudStorage/OneDrive-Personal/Teaching/Grad_R_course/R4PsyBook/bookdown_files/Books/Book/data/match/data_exp7_rep_match_7302.out" ## [2] "/Users/hcp4715/Library/CloudStorage/OneDrive-Personal/Teaching/Grad_R_course/R4PsyBook/bookdown_files/Books/Book/data/match/data_exp7_rep_match_7303.out" ## [3] "/Users/hcp4715/Library/CloudStorage/OneDrive-Personal/Teaching/Grad_R_course/R4PsyBook/bookdown_files/Books/Book/data/match/data_exp7_rep_match_7304.out" ## [4] "/Users/hcp4715/Library/CloudStorage/OneDrive-Personal/Teaching/Grad_R_course/R4PsyBook/bookdown_files/Books/Book/data/match/data_exp7_rep_match_7305.out" ## [5] "/Users/hcp4715/Library/CloudStorage/OneDrive-Personal/Teaching/Grad_R_course/R4PsyBook/bookdown_files/Books/Book/data/match/data_exp7_rep_match_7306.out" ## [6] "/Users/hcp4715/Library/CloudStorage/OneDrive-Personal/Teaching/Grad_R_course/R4PsyBook/bookdown_files/Books/Book/data/match/data_exp7_rep_match_7307.out" ## [7] "/Users/hcp4715/Library/CloudStorage/OneDrive-Personal/Teaching/Grad_R_course/R4PsyBook/bookdown_files/Books/Book/data/match/data_exp7_rep_match_7308.out" ## [8] "/Users/hcp4715/Library/CloudStorage/OneDrive-Personal/Teaching/Grad_R_course/R4PsyBook/bookdown_files/Books/Book/data/match/data_exp7_rep_match_7309.out" ## [9] "/Users/hcp4715/Library/CloudStorage/OneDrive-Personal/Teaching/Grad_R_course/R4PsyBook/bookdown_files/Books/Book/data/match/data_exp7_rep_match_7310.out" ## [10] "/Users/hcp4715/Library/CloudStorage/OneDrive-Personal/Teaching/Grad_R_course/R4PsyBook/bookdown_files/Books/Book/data/match/data_exp7_rep_match_7311.out" str(files) ## chr [1:44] "/Users/hcp4715/Library/CloudStorage/OneDrive-Personal/Teaching/Grad_R_course/R4PsyBook/bookdown_files/Books/Boo"| __truncated__ ... P.S.尽管函数叫list.files,但它得到的变量的属性是value,而不是list 所以,这里我们可能需要有一个函数,就是我们要把符合我们条件的这些文件名都列出来。在R语言中,有一个函数叫做list.files(),这个实际上在各个编程语言中都有这样的功能,因为找到一个文件夹里面所有特定符合特定命名规则的文件名基本上是所有编程语言中经常做的事情。在Python中也有,在MATLAB中也有,在SPSS中也有。 在这里,list.files()的第一个参数就是我们的路径,因为我们已经不在根目录了,我们的根目录是我们课件的目录。那个数据是在名为match的子文件夹里面的。所以,我们这个时候就用here::here()来解决路径的问题,避免不同操作系统之间的转换。 然后第二个参数就是它指定说你需要什么,如果你不加参数或者什么都不加的话,它就会把这个文件夹里面所有的文件都列出来,但这并不是我们想要的。我们想要的是以data_exp7_rep_match开头的文件,因为这些才是我们要的匹配任务下的数据。我们还有非匹配的数据和其他任务的数据。然后我们用通配符,就是一个#号,然后以out作为结尾,这里就是$,以这个作为结尾。 像是我们通过这种前面讲字符操作匹配的时候,这种叫做正则表达式的方式去找到符合这种条件的所有的文件。然后我们用这个full.names = TRUE,就是说把它整个完整路径都列出来。 这样做有什么好处呢?就是我们得到的就是文件以及它的整个路径,这样的话,我们就可以直接去把整个字符串放到我们的read.table里面去了,我们就不需要再做其他的,比如给它补路径什么的。 我们可以看前面的10个,大家如果运行这个代码的话,应该这个东西肯定是跟这里不一样的,它会根据大家自己的电脑放置我们这个课件的地址而改变。 然后我们这里是C:/users/,从R4psy/data/match/开始应该是一样的。 我们这里就这个花钥匙就得到了一串这个一个项链,里面的每一个元素都是我们对应我们想要的这些文件的名字以及他们的完整路径。我们也可以看一下,它是有44个这个元素,就表示它有44个文件名,那正好跟我们的44个被试是对上的。 然后,在我们读取数据之前,我们先定义一个小的函数,这个函数就是我们刚才在上面处理这个问题时用到的,就是用各种as什么什么的。 # 定义函数用于数据类型转换 convert_data_types = function(df) { df <- df %>% dplyr::mutate(Date = as.character(Date), Prac = as.character(Prac), Sub = as.numeric(Sub), Age = as.numeric(Age), Sex = as.character(Sex), Hand = as.character(Hand), Block = as.numeric(Block), Bin = as.numeric(Bin), Trial = as.numeric(Trial), Shape = as.character(Shape), Label = as.character(Label), Match = as.character(Match), CorrResp = as.character(CorrResp), Resp = as.character(Resp), ACC = as.numeric(ACC), RT = as.numeric(RT)) return(df) } 为什么我们要定义这个东西呢?就是要去保证每一个列,它的数据类型就是我们想要的,因为你在数据的这个load或者read的时候,如果你的数据类型不一样的话,它也是合并不了的,也可能会出错,或者会给你强行转换。但是,我们这里就先强制给它进行一个转换。例如,我们把应该是数值型的,像年龄、被试编号转成数值型,RT转成数值型,还有这个ACC转成数值型,然后其它的字符型转成字符型。转换了之后返回的就是第二个。 这里第一个是为了后面,让我们这个for循环比较简单一点。 # 创建一个空的数据框来存储读取的数据 df3 <- data.frame() # 循环读取每个文件,处理数据并添加到数据框中 for (i in seq_along(files)) { # 重复"读取到的.out个数"的次数 # 读取数据文件 df <- read.table(files[i], header = TRUE) # 使用 filter 函数过滤掉 Date 列值为 "Date" 的行 df <- dplyr::filter(df, Date != "Date") # 调用函数进行数据类型转换 df <- convert_data_types(df) # 使用 bind_rows() 函数将当前数据框与之前的数据框合并 df3 <- dplyr::bind_rows(df3, df) } # rbind合并后是matrics,需要转换 # df3 <- as.data.frame(do.call(rbind, df_list)) # 清除中间变量 rm(df, files, i) 那么我们怎么写这个for循环?首先我们要创建一个新一个空的一个数据框,用来抓我们最终读取的数据,并且把各个被试的数据合并到一起之后的一个总的数据。然后,我们就开始写这个for循环,从1到n,就是沿着这个file变量进行依次往后面迭代。然后大家可以看到df它是一个临时的变量,对吧,它读取这个table,然后读取的这个table的文件名就是我们这个files里面的这个第i个元素,也就是说,它会依次从第一个依次读取到第44个。那么这个i不断的变化,然后读取完了之后,我们可以就是过滤掉它这个数据,呃这个叫什么,第一个就是日期不等于日期的这个名字的,呃这个行,这个就是这里是一个特殊数据,而且跟我们这个数据是特别相关的。 因为我们这个数据文件进入的时候,有一些特殊的标识,我们需要进行一些过滤。但大家自己的数据可能不需要做这个操作,这是一个额外的操作,是为了我们处理这个数据而进行的特殊操作。然后,我们调用我们刚才定义的函数,对它进行一个数据转换。转换之后,我们可以看到,这里有一个很关键的操作,就是我们把原来的这个定义的df3,把它和我们刚才读取的这个df进行合并。 大家想想,当i = 2的时候,它会读取第一个被试的文件,读取到df里面,然后对df做了一系列的预处理之后,我们把它和df3进行一个合并。刚开始的时候,第一次操作的时候df3是空的,对吧?因为我们在前面是把它定义成一个空的数据框。所以当我们这个循环刚开始的时候,第一次的df读取第一个被试数据,然后第二次i = 3的时候,df3这个时候是空的,那么它是空的。然后它和第一个被试的数据进行结合之后,它实际上就是变成第一个被试数据了,对吧?然后这个时候循环到了它结束的时候,然后我们看第二次循环i = 2的时候,它就开始读取第二个被试的数据,然后这个时候df就是等于第二个被试数据,对吧?然后也同样进行了一番操作之后,然后到这个时候df3,它刚才已经替换成了第一个被试数据,对不对?所以他又和第二个被试数据进行结合,之后这个时候再复制到df3,也就是我们把df3进行了一次更新。这个时候的df3等于第一个被试和第二个被试进行结合的结果了。 然后我们再进行第三次操作,第三次操作的时候i = 3,df就直接被替换成第三个被试数据。然后再到这里,我们再把df3和df进行结合的时候,这个时候df就变成把前三个被试数据结合的一个数据,对吧?然后呢,依次不断叠带,最后把所有的四个被试的数据全部都往后不断加到df3这个数据框后面去。也就说,它最后这个循环完了之后,df3就是我们的一个完整的合并之后的一个数据了。 这个可能大家一下子无法完全理解,我看到大家好像有些迷茫。因为我的讲解还没有结束,我就接着讲,大家后面可以看一下视频,就是我刚才举了三个例子,一是什么,二是什么,三是什么。大家可以仔细再去纸上画一画,用铅笔什么的箭头画一画,看他的循环逻辑。然后你可以把这个i的序列减少一点,你自己看一看他每次的结果是什么。 刚才说的是一个forloop,就是我们自己不断地去迭代的逻辑。那么还有另外一个,就是我可以通过这个lapply函数,就是直接定型的,把所有的一次性的做完。 使用lapply也能完成批量导入与合并。lapply思维难度更高,但代码更简洁。 # 获取所有的.out文件名 files <- list.files(here::here("data", "match"), pattern = "data_exp7_rep_match_.*\\\\.out$", full.names = TRUE) # 读取每个.out文件,并进行数据清洗 df3 <- lapply(files, function(file) { df <- read.table(file, header = TRUE) df <- dplyr::filter(df, Date != "Date") # 过滤掉 Date 列值为 "Date" 的行 df <- mutate(df, convert_data_types(df) ) # 进行数据类型转换和数据清洗 return(df) }) # 合并所有数据框 df3 <- dplyr::bind_rows(df3) # 清除中间变量 rm(files) 当然这个就是比较高度压缩的一个代码,然后需要大家去理解自己的这个里面这个逻辑。他先就是把这个函数进行的一个高度的整合。我这里就不展开了吧。如果大家去想要理解的话,可能你先理解的for之后,然后再去再看这个lapply,这样的话会更好一点。 OK,假定大家都理解了这个forloop,那就我们就能够把这个df3就读取出来,然后得到了一个数据,就是把所有44个被试的数据都合并了。合并了之后我们可以把它保存下来,因为我不想再重复这个工作。或者因为每次搜索的话也会帮助你,数据量很大的时候,你也比较浪费时间。然后你这样的话,就可以直接把它就是存在某一个地方,比方说我们把它存成叫做match_row的一个CSV的文件,然后也是存在刚才一模一样的这个文件夹。 保存合并的数据文件 #for loop 或 lapply的都可以 write.csv(df3, file = here::here("data", "match","match_raw.csv"), row.names = FALSE) 这个地方有一个稍微值得注意点,就是这个 row.names等于FALSE,就是不要保存这个行的名字。你保存的话它会给你增加一行,增加一列就是这个行的名字,123456789…直到所有的这个行数。这个没有对我们来说一般来说没有意义,所以我们把这个一般会写成FALSE。 其实对于我们数据操作来说,就是一个一个被试的数据,当它是分开的时候,就假如说我们有这个就是有很多被试的分开文件的话。对于我们书写来说,第一步可能就是forloop是最最难的,后面都是我们前面讲过的。 处理缺失值[drop_na, filter] # 删除缺失值,选择符合标准的被试 df4 <- tidyr::drop_na(df3) # 删除含有缺失值的行 df4 <- dplyr::filter(df3, Hand == "R", # 选择右利手被试 ACC == 0 | ACC == 1 , # 排除无效应答(ACC = -1 OR 2) RT >= 0.2 & RT <= 1.5) # 选择RT属于[200,1500] 当然选择变量,然后处理这个缺失值,用filter,然后这里有一个tidyr里面的有一个很好用的一个函数叫做drop_na,用的时候要谨慎一点,因为它是一个比较粗暴的方法,就是把任何一行只要它有缺失值的话就直接给扔掉了。那么有的时候你可能知道,就比方说某一个数据里面,只要他任何一行里面有一个缺失值,那个数据你就完全你知道你不会用了。那你就可以用drop_na,要不然的话这个会丢掉太多的数据。这个好用,但是要谨慎的使用。 然后你可以进行一些其他的选择,比如你只选择这个右利手的被试,因为大家都是右利手的,然后你选择这个反应时间的范围,比如它小于0.2,那你就不要,因为你觉得这个数据是没有用的,反应过快。还有就是把那些比如说是无效的反应,你可以把它排除掉,它是-1的,或者是2的。这些都是说,我们通过多种方法来进行一个选择。然后分条件描述的话,跟之前是一样的。然后我们这里描述就是ACC。我们可以通过这个条件来进行一个描述。 这里有一个比较有意思的操作,就是拆分变量。 # 分实验条件计算 df4 <- dplyr::group_by(df4, Sub, Shape, Label, Match) df4 <- dplyr::summarise(df4, mean_ACC = mean(ACC), mean_RT = mean(RT)) ## `summarise()` has grouped output by 'Sub', 'Shape', 'Label'. You can override using the ## `.groups` argument. df4 <- dplyr::ungroup(df4) 拆分变量[extract, filter] # 将Shape变量拆分 df4 <- tidyr::extract(df4, Shape, into = c("Valence", "Identity"), regex = "(moral|immoral)(Self|Other)", remove = FALSE) df4 <- dplyr::filter(df4, Match == "match" & Valence == "moral") 比如我们原来它是这样的,就是它是有一个长格子Shape,就是它的图形是什么,moral-Self、``moral-Other``、immoral-Self, immoral-``Other。它实际上表示的是两个自变量,一个是它的这个valence,到底是积极的还是消极的,另外一个就是它的这个identity,到底是自我还是他人。所以我们可以通过这种字符拆分的方法,把它拆分成为把线拆分成为两个变量。 拆分的方式就是前面的要么是moral,要么是immoral,后面的要么是Self,要么是Other。比如说我们这里extract就是对shift对吧,对shift这个变量把它提取出两个列出来,一个叫做Valence,一个叫做Identity。提取的方式是按照这种方式进行提取。然后最后这个目的就是说,是不是要把原来的这个shape给去掉。我们这里选择没有去掉,对吧。 所以大家可以看到,我们做完这个操作之后,shape不在这里,但是同时又增加了Valence和Identity这两个变量。 我们可以进行数据选择,然后我们就想看一下match条件下,自我和他人之间的一个区别。 那么这个时候,我们就要做一个长短宽的一个操作。 将数据长转宽[pivot_wide] # 将长数据转为宽数据 df4 <- dplyr::select(df4, Sub, Identity, mean_RT) df4 <- tidyr::pivot_wider(df4, names_from = "Identity", values_from = "mean_RT") 什么叫长转宽?就是比方说在我们的SPSS当中,每一个行代表的是什么,一个被试对吧,每一列代表的是一个条件。但是如果我们看前面的这个操作的话,我们发现,比方说,前面那个条件,每个被试它有两列,而且还是我们筛选之后的数据。那么它有Valence和Identity对吧,这个是我们选择过了。但它大概单体里还有两个,就实际上在这个里面是一个column对吧,一列它专门来表示这个变量。然后它在不断的重复,对于每个被试来说,它都有一个other,一个self。那么它就不太像我们常说的那个数据,那个那种数据叫做宽数据,就是说每个被试代表一个行,那么我们用增加列的方式来去表示不同的变量。那么它的这个方式就是说,我们对于每一个变量的不同的水平,我们就把它放在一单独的一列,然后我们这个Identity它有两个水平,我们就重复这两个水平,对每个被试我们都重复两次。 比方说Valence,如果说我们看原来数据的话,它有两个数据,比方说Moral和Immoral。那么我们也不断的重复它,这样的话其实每个被试就会有有几行,就会有4行的对吧,就是有Moral-self、moral-other、Immoral-self、Immoral-other这样的一个情况。那么像这种每一个被试它有多个行的这种情况,我们叫做长的数据。宽数据就是每个被试只有一行,我们把它转成多个列,不同的列代表不同的数据。那这样的话,这个数据就相比原来数据变宽了对吧,这就是宽数据。我们SPSS传统上面就是用宽的数据的形式。那么我们一般来说,我们可以,我们要进行下一步的运算的时候,进行不同条件之间比较的时候,我们肯定要把它转成宽数据对吧,这样的话,就可以更好的进行一个比较。 然后我们这里就在数据里面选择Sub, Identity, mean_RT,这三个感兴趣的变量。然后我们的pivot_wider这个函数也非常符合我们的需要,它有两个函数,有一个姊妹的函数pivot_longer,就是长把它变得更宽,或者反过来,宽的变得更长。那么这里就是它的数据,然后names_from就是说你不是要把它变宽吗?变宽的意思就是你的列要增加对吧?那你的列要增加的话,你的名字从哪来?你的列的名字从哪来?你的列的名字从原来的这个data_frame_4的这个identity这里过来,然后你的values_from 的这个值从哪里来?就是从mean_RT这里来对吧。 所以大家看到pivot_wider,其实让我们这个长短宽的数据变得更圆扁的。就是你转换之后,你增加了列。那么你增加了这个列的名字从哪来,你增加了列的这个数值从哪来。当然,你可能现在记不住,但是你如果说你就是真的要用长短宽或者宽短长的时候,你最好的方法就是去看这个教程,他会告诉你用可转化的方式告诉你怎么转的。我们照着葫芦画瓢就行了。然后mutate就是转换了之后你检查一下对不对,因为如果要记它们到底是什么意思的话其实非常琐碎,我们也没有必要记。你就到时候有什么需求你就就转对吧。 比方说,你可能不止有identity一个变量,你可能也有moral这个变量的时候,你有两个列对吧,你要把它一下子转成一个,就是增加到四列的时候怎么转这个。这个教程网上有非常多的。 这个地方是稍微会有一点点细节的地方。 # 计算SPE df4 <- dplyr::mutate(df4, moral_SPE = Self - Other) df4 <- dplyr::select(df4, Sub, moral_SPE) 那么最后这个操作的话就比较简单对吧,就是我们转完了之后的话,转换之后呢,有subject对吧,有moral_SPE。那么它就是只有这三个列,我们把Self - Other的话,就得到我们感兴趣的自我优势效应。所以我们这里就是直接就要说用mutate,然后生成一个新的我们感兴趣的变量。 将上面所有的合并的话,那就是可以用长串代码对吧。当我们读取了数据之后,那这个大家可以后面去,基本上这里面每一个我们都讲过了,每一个操作我们都讲过。 大家可能后面如果对某一个不太懂的话,可能再回过头来再看一下这个视频,或者是问我们的助教。大家要积极主动地向助教提问,这样的话会能够学习的更快一点。 好,最后就是一个小结。 7.3.3 小结 separate() 把一个变量的单元格内的字符串拆成两份,变成两个变量 更适合用于按固定分隔符分割字符串,如将“2022-02-25”分成“2022”、“02”和“25”三列 extract() 类似于separate 更适合用于从字符串中提取特定的信息,如将“John Smith”分成“John”和“Smith”两列 unite() 把多个列(字符串)整合为一列 pivot_longer() 把宽数据转化为长数据 pivot_wider() 把长数据转化为宽数据 drop_na() 删除缺失值 就是我们在用到tidyr的时候,用了一些跟前面不一样的一些操作。比如提取字符串,还有这里有一个没有讲的,就是我们可以把字母进行一个拆分,separate这个也非常好用。然后还有unit,就是我们把几列,我们把它合并成一列,这个也是很好用的一个函数。还有就是pivot_wider,就是宽转长,pipit_wide长转宽,还有drop_na。这就是我们常用的需要操作的函数。 另外,基本上大家能够想到的这些预数据处理,只要大家能够用语言描述出来,基本上都能够找到这种相应的代码的操作。 那么最后是一个练习,大家可以下面练习一下。 练习 计算不同Shape情况下(immoral-self,moral-self,immoral-other,moral-other) 基于信号检测论match与mismatch之间的d’(match为信号,mismatch噪音) 以下是计算信号检测论d’的代码 dplyr::summarise( hit = length(ACC[Match == "match" & ACC == 1]), fa = length(ACC[Match == "mismatch" & ACC == 0]), miss = length(ACC[Match == "match" & ACC == 0]), cr = length(ACC[Match == "mismatch" & ACC == 1]), Dprime = qnorm( ifelse(hit / (hit + miss) < 1, hit / (hit + miss), 1 - 1 / (2 * (hit + miss)) ) ) - qnorm( ifelse(fa / (fa + cr) > 0, fa / (fa + cr), 1 / (2 * (fa + cr)) ) ) ) 我们在学习里面,突然有一个信号检测论,对吧,我们把一个作为信号,另外一个做一个噪音的话,那么我们就会有这个,比方说hit, fa,miss, cr。那么根据这四个类型的话,我们可以计算出这个信号检测量的这个敏感度Dprime。那么实际上我们可以用在tidyvers里面,对吧,一行代码就一个一拍plan就可以完成下来,你不需要单独定义函数什么的。那么大家可以考虑一下如何把每个条件下每个被试的Dprime计算出来。 那么大家,我们这里提供了一个类似的思路和最后解题的一个大致方案。 练习思路 Step1: 选择需要的变量 Step2: 基于Sub,Block,Bin和Shape分组 Step3: 使用计算公式 Step4: 删除击中、虚报、误报、正确拒绝 Step5: 按Sub和Shape分组 Step6: 长转宽,得到每个Shape情况下的信号检测论d值 最后你可能得到四种条件,每个条件下面都有一个自己的数据,然后每一行的被试,然后最后就是44个被试的Dprime数据。好,最后还有一句话,首先我们其实不是说告诉大家怎么用,而是给大家提供一个大家可用的的一些代码。第二个就是告诉大家我们预数理的一个思路是什么。然后大家真的在心态上面,如果真的你的原始数据没有那么好,没有那么整洁的话,要做好心理准备,就是说这个数据预处理实际上是你数据分析中占比比较多的一个部分。当你发现你这个数据不好处理的时候,大家要有耐心一点,然后多去问,每个人都是这么过来的。所以说呢,不要觉得好像只有你那么痛苦,人在知道大家都很痛苦的时候,你会发现好很多。 "],["第七讲描述性统计与数据可视化基础.html", "Chapter 8 第七讲:描述性统计与数据可视化基础 8.1 回顾 8.2 探索性数据分析 8.3 数据可视化 8.4 常用图形 8.5 Data Explorer 8.6 练习 8.7 参考阅读", " Chapter 8 第七讲:描述性统计与数据可视化基础 本节课将引入一个新的知识点:描述性统计和数据可视化。根据课程大纲,数据可视化将分为两课,今天我们将初步了解并实践数据可视化,而后续课程会进一步讲如何对可视化结果进行精细调整,以达到可出版的标准。 本次课程增加了一些新的工具包。如果是第一次运行这些代码,可能需要执行一些安装命令,以确保所有相关的工具包都能正确安装。安装命令代码: if (!requireNamespace('pacman', quietly = TRUE)) { install.packages('pacman')} pacman::p_load(here,skimr,quartets,GGally,showtext,bruceR,tidyverse,DataExplorer) 8.1 回顾 上节课我们介绍了数据预处理、如何批量读取数据、如何获取一个文件夹中特定类型文件的名字、如何定义函数进行转换数据类型以确保每一列数据的类型都是我们期望的、如何通过for循环来批量读取数据、介绍了for循环和if语句在R语言数据处理中的重要性并演示了如何使用for循环读取数据、合并数据,并将数据保存为CSV格式,这样可以避免每次都重新进行批量读取,节省时间。 8.1.1 批量导入数据 获取地址 # 所有数据路径 files <- list.files( ## <- & = here::here("data", "match"), pattern = "data_exp7_rep_match_.*.out$", full.names = TRUE) 数据类型转换 convert_data_types <- function(df) { df <- df %>% dplyr::mutate(Date = as.character(Date),Prac = as.character(Prac), Sub = as.numeric(Sub),Age = as.numeric(Age), Sex = as.character(Sex),Hand = as.character(Hand), Block = as.numeric(Block),Bin = as.numeric(Bin), Trial = as.numeric(Trial),Shape = as.character(Shape), Label = as.character(Label),Match = as.character(Match), CorrResp = as.character(CorrResp),Resp = as.character(Resp), ACC = as.numeric(ACC),RT = as.numeric(RT)) return(df) } for循环批量读取数据,将数据进行批量合并批量合并 df3 <- data.frame() for (i in seq_along(files)) { # 读取 df <- read.table(files[i], header = TRUE) %>% dplyr::filter(Date != "Date") %>% convert_data_types() # 合并 df3 <- dplyr::bind_rows(df3, df) } # 删除临时变量 rm(df, files, i) 通过export将数据保存为csv 保存数据 ## NOT RUN ## 上节课介绍了write.csv,也可使用bruceR::export bruceR::export( df3, file = here::here("data", "match","match_raw.csv")) ## 当然,export 不仅可以保存数据,也可以输出模型结果 当出现列名和变量名不同,可以通过rename将列名进行修改 修改列名-rename ## 修改第一列列名 Date 为小写 date df3 %>% dplyr::rename( ## new_name = old_name date = Date ) %>% colnames() ## [1] "date" "Prac" "Sub" "Age" "Sex" "Hand" "Block" "Bin" ## [9] "Trial" "Shape" "Label" "Match" "CorrResp" "Resp" "ACC" "RT" ## 将全部列名都变成小写 df3 %>% dplyr::rename_with( ## 将字符向量全部变成小写; ~ 声明这是一个函数,.代表前面的数据(df3)传到.所在的位置 ~tolower(.) #<< ## 即使用 tolower()对所有列名进行批量处理 ## ) %>% colnames() ## [1] "date" "prac" "sub" "age" "sex" "hand" "block" "bin" ## [9] "trial" "shape" "label" "match" "corrresp" "resp" "acc" "rt" 8.1.2 代码书写规范 为了使代码看起来更整洁,我们建议每个预处理步骤占用一行,并在管道符后换行,这样每一步的操作都清晰可见。 ## 以下代码看起来如何? iris %>% group_by(Species) %>% summarize_all(mean) %>% ungroup %>% gather(measure, value, -Species)%>% arrange(value) ## # A tibble: 12 × 3 ## Species measure value ## <fct> <chr> <dbl> ## 1 setosa Petal.Width 0.246 ## 2 versicolor Petal.Width 1.33 ## 3 setosa Petal.Length 1.46 ## 4 virginica Petal.Width 2.03 ## 5 versicolor Sepal.Width 2.77 ## 6 virginica Sepal.Width 2.97 ## 7 setosa Sepal.Width 3.43 ## 8 versicolor Petal.Length 4.26 ## 9 setosa Sepal.Length 5.01 ## 10 virginica Petal.Length 5.55 ## 11 versicolor Sepal.Length 5.94 ## 12 virginica Sepal.Length 6.59 ### 管道操作的代码看上去更加清晰,整洁 iris %>% dplyr::group_by(Species) %>% dplyr::summarize_if(is.numeric, mean) %>% dplyr::ungroup() %>% tidyr::gather(measure, value, -Species) %>% dplyr::arrange(value) ## # A tibble: 12 × 3 ## Species measure value ## <fct> <chr> <dbl> ## 1 setosa Petal.Width 0.246 ## 2 versicolor Petal.Width 1.33 ## 3 setosa Petal.Length 1.46 ## 4 virginica Petal.Width 2.03 ## 5 versicolor Sepal.Width 2.77 ## 6 virginica Sepal.Width 2.97 ## 7 setosa Sepal.Width 3.43 ## 8 versicolor Petal.Length 4.26 ## 9 setosa Sepal.Length 5.01 ## 10 virginica Petal.Length 5.55 ## 11 versicolor Sepal.Length 5.94 ## 12 virginica Sepal.Length 6.59 如果下载了notebook,能够获取到一个参考链接参考链接:tidyverse style guide,该链接可提供有助于记忆和提高数据分析思路的编码风格,供大家参考。 另外,值得一提的是,R Studio 有时会自动帮你整理代码。比如,我们可以采用 R Studio 中的一个自动格式调整功能,它会呈现给你一个更规整的代码样式,但这个快捷键可能在部分电脑上适用,而在其他电脑上可能有不同的快捷键组合,有时可能会发生冲突。但如果不冲突,你可以直接按 Ctrl + Shift + A 来实现格式化。 我们可以通过点击 R Studio 上面的“Code”来找到并使用这个功能。你可以看到,它自动地把代码调整得非常整齐,基本上是以逗号分隔,每一个逗号后面都分割成一行,前面的缩进也完全对齐。这样,你就可以清晰地看到这一行代码从哪里开始,到哪里结束,以及它完成了几个任务或输入了几个参数。 8.1.3 数据清洗 我们在上节课提到的数据清洗时遇到的一些常见代码,包括过滤、合并、转换、分组计算和字符串操作。进行数字符的处理,它是一个需要注意很多小细节的过程,大家在使用时需要注意。最后,函数式编程和类似的批量处理功能我们也没有展开讲,因为这些都是比较复杂的功能。随着大家经验的增加,探索这些功能会提高你的效率。 8.2 探索性数据分析 了解原始数据的特点,做到心中有数,属于一个更广泛的概念:探索性数据分析(Exploratory Data Analysis,EDA),在传统的心理学中,我们通常会清楚地知道要进行什么样的分析,但是在数据科学中,可能面临的是一个未知的数据集,我们不知道其中的规律和数据的结构。因此,探索性数据分析非常重要。 探索性分析通常通过可视化的方式总结数据特征,在大数据时代被广泛推崇。 进行EDA是为了更加了解自己的数据,从而做出基本的判断,但每一次探索背后都对应着特定的问题,进行EDA时,需要了解数据的基本信息,比如有哪些变量?变量的类型?变量的分布?变量之间的关系? # 读取数据 pg_raw <- bruceR::import(here::here( "data", "penguin","penguin_rawdata.csv")) mt_raw <- bruceR::import(here::here( "data", "match","match_raw.csv")) 8.2.1 常用函数介绍 summary summary(mt_raw) %>% knitr::kable() # 注:kable函数只为了输出 skim函数能够实现数据的快速预览,可以根据每一列的数据类型进行数据预览,会显示字符型数据的分布特点 Date Prac Sub Age Sex Hand Block Bin Trial Shape Label Match CorrResp Resp ACC RT Length:25920 Length:25920 Min. : 7302 Min. :18.0 Length:25920 Length:25920 Min. :1.00 Min. :1.00 Min. : 1.00 Length:25920 Length:25920 Length:25920 Length:25920 Length:25920 Min. :-1.000 Min. :0.106 Class :character Class :character 1st Qu.: 7313 1st Qu.:19.0 Class :character Class :character 1st Qu.:1.00 1st Qu.:1.00 1st Qu.: 6.75 Class :character Class :character Class :character Class :character Class :character 1st Qu.: 1.000 1st Qu.:0.610 Mode :character Mode :character Median : 7324 Median :20.0 Mode :character Mode :character Median :1.00 Median :2.00 Median :12.50 Mode :character Mode :character Mode :character Mode :character Mode :character Median : 1.000 Median :0.702 NA NA Mean : 8853 Mean :20.8 NA NA Mean :1.61 Mean :2.42 Mean :12.50 NA NA NA NA NA Mean : 0.796 Mean :0.715 NA NA 3rd Qu.: 7336 3rd Qu.:22.0 NA NA 3rd Qu.:2.00 3rd Qu.:3.00 3rd Qu.:18.25 NA NA NA NA NA 3rd Qu.: 1.000 3rd Qu.:0.805 NA NA Max. :73370 Max. :28.0 NA NA Max. :3.00 Max. :5.00 Max. :24.00 NA NA NA NA NA Max. : 2.000 Max. :1.183 skimr::skim()–1 skimr::skim(mt_raw) %>% capture.output() %>% .[1:12] ## [1] "── Data Summary ────────────────────────" " Values" ## [3] "Name mt_raw" "Number of rows 25920 " ## [5] "Number of columns 16 " "_______________________ " ## [7] "Column type frequency: " " character 9 " ## [9] " numeric 7 " "________________________ " ## [11] "Group variables None " "" skimr::skim()–2 skimr::skim(mt_raw) %>% capture.output() %>% .[13:24] ## [1] "── Variable type: character ──────────────────────────────────────────────────────────────────" ## [2] " skim_variable n_missing complete_rate min max empty n_unique whitespace" ## [3] "1 Date 0 1 20 20 0 24362 0" ## [4] "2 Prac 0 1 3 3 0 1 0" ## [5] "3 Sex 0 1 1 6 0 4 0" ## [6] "4 Hand 0 1 1 1 0 2 0" ## [7] "5 Shape 0 1 9 12 0 4 0" ## [8] "6 Label 0 1 9 12 0 4 0" ## [9] "7 Match 0 1 5 8 0 2 0" ## [10] "8 CorrResp 0 1 1 1 0 2 0" ## [11] "9 Resp 658 0.975 1 5 0 9 0" ## [12] "" skimr::skim()–3 skimr::skim(mt_raw) %>% capture.output() %>% .[25:41] ## [1] "── Variable type: numeric ────────────────────────────────────────────────────────────────────" ## [2] " skim_variable n_missing complete_rate mean sd p0 p25 p50 p75" ## [3] "1 Sub 0 1 8853. 9932. 7302 7313 7324 7336 " ## [4] "2 Age 0 1 20.8 2.48 18 19 20 22 " ## [5] "3 Block 0 1 1.61 0.803 1 1 1 2 " ## [6] "4 Bin 0 1 2.42 1.36 1 1 2 3 " ## [7] "5 Trial 0 1 12.5 6.92 1 6.75 12.5 18.2 " ## [8] "6 ACC 0 1 0.796 0.464 -1 1 1 1 " ## [9] "7 RT 0 1 0.715 0.151 0.106 0.610 0.702 0.805" ## [10] " p100 hist " ## [11] "1 73370 ▇▁▁▁▁" ## [12] "2 28 ▇▂▁▁▁" ## [13] "3 3 ▇▁▃▁▃" ## [14] "4 5 ▇▇▃▃▃" ## [15] "5 24 ▇▇▆▇▇" ## [16] "6 2 ▁▂▁▇▁" ## [17] "7 1.18 ▁▂▇▅▁" bruceR::Describe() bruceR::Describe(mt_raw) %>% capture.output() %>% .[2:17] ## 可以使用 file参数输出 Word ## [1] "────────────────────────────────────────────────────────────────────────────────────" ## [2] " N (NA) Mean SD | Median Min Max Skewness Kurtosis" ## [3] "────────────────────────────────────────────────────────────────────────────────────" ## [4] "Date* 25920 12218.64 6999.30 | 12220.50 1.00 24362.00 0.00 -1.19" ## [5] "Prac* 25920 1.00 0.00 | 1.00 1.00 1.00 NaN NaN" ## [6] "Sub 25920 8852.59 9931.83 | 7324.00 7302.00 73370.00 6.34 38.22" ## [7] "Age 25920 20.83 2.48 | 20.00 18.00 28.00 1.09 0.35" ## [8] "Sex* 25920 3.27 0.65 | 3.00 1.00 4.00 -0.84 1.58" ## [9] "Hand* 25920 1.98 0.15 | 2.00 1.00 2.00 -6.34 38.22" ## [10] "Block 25920 1.61 0.80 | 1.00 1.00 3.00 0.82 -0.97" ## [11] "Bin 25920 2.42 1.36 | 2.00 1.00 5.00 0.67 -0.82" ## [12] "Trial 25920 12.50 6.92 | 12.50 1.00 24.00 0.00 -1.20" ## [13] "Shape* 25920 2.50 1.12 | 2.50 1.00 4.00 0.00 -1.36" ## [14] "Label* 25920 2.49 1.11 | 2.00 1.00 4.00 0.02 -1.35" ## [15] "Match* 25920 1.50 0.50 | 1.50 1.00 2.00 0.00 -2.00" ## [16] "CorrResp* 25920 1.50 0.50 | 1.50 1.00 2.00 0.00 -2.00" Describe函数适用于我们已经知道要对哪些数据进行描述性统计的情况,可输出心理学所需的三线表,但如果不清楚需要对哪些数据进行描述性统计,那么describe函数不适用。 8.3 数据可视化 8.3.1 可视化的重要性 可视化有利于我们检查数据是否有意义,同时其也是一个诚实展现我们数据特征的方法。R绘图的方式有许多:Base graphics,grid,lattice,plotly……但在心理学研究中我们一般选择ggplot2。 7.2.2 为什么选择使用ggplot2? ggplot2是一种图形语法,它的核心是用图层来描述和构建图形。可以将数据映射到不同的图层中,然后将这些图层叠加起来形成最终的图形。所谓gg源于“grammar of graphics”,即图形语法。 化繁为简:ggplot2有大量的默认值,适合新手 精准定制:所有元素均可控,有利于文章的发表 易于重叠:ggplot2作出的图包含不同的图层,不同的图层包含不同的信息,叠加起来信息丰富且美观 日益丰富的生态系统:http://r-graph-gallery.com/ 8.3.2 可视化的逻辑 在我们的R语言环境中,我们通常处理的是数据框,这是最常见的数据结构。数据框可以简单理解为一张表格,例如有三列数据,每列数据有不同的行,每一行代表一个数据点。 当我们在R中使用ggplot2这个包来画图时,首先需要建立数据与可视化元素之间的映射关系。我们要思考的是如何把数据转换成可视化的元素。比如,我们有一列数字1、2、3,我们会自然地想到将这些数字放在坐标轴上,按照大小顺序排列。 ggplot2在画图时的第一步也是类似的,它将数字沿着坐标轴排列,然后将这些数字映射成几何图形,也就是将数字转换成视觉上的效果,并将其放置在一个特定的坐标系统中。通常我们看到的坐标系统就是笛卡尔坐标系,即我们常说的x轴和y轴。 在将数据映射到坐标轴上之后,它就变成了一个基本的图表。这就是ggplot2所做的最基本的事情:将数字映射到一个空间中,然后放置到坐标轴上,形成一个基本的图表。这里涉及到的一个重要概念是映射关系,比如我们设置x等于f,这里的f对应的是数据框中的一个列,y等于a,这里的a对应的是数据框中的另一个列。如果我们处理的是二维数据,我们就在二维空间中将每个数值进行映射,从而形成图表上的点。每个点在f和a上都有一个值,这两个值在二维坐标体系中确定了一个独特的位置。 除了映射位置之外,我们还可以通过数值来改变可视化元素的其他特性,比如颜色和大小。例如,我们可以用a来表示大小,用f来表示颜色。数字越小,颜色越浅,数字越大,颜色越深;同样,大小也是根据数字的大小来表示。这样,我们不仅可以在xy轴上从左到右、从下到上地映射数据,还可以根据它们的数值来赋予不同的颜色深度和大小,从而增加了图表的信息量和可读性。通过这种方式,我们可以通过映射到不同的空间位置和改变可视化元素的特性来创建丰富多样的图表。 一旦我们在不同的图层上完成了数据映射,我们可以将它们叠加在一起,创建一个复合图表。例如,我们可以在基本的笛卡尔坐标系上,首先叠加一个图层,将一列数据映射为条形图,然后再叠加另一个图层,将另一列数据映射为线图。通过这样的多层叠加,我们可以把想要表达的信息全部呈现在图表上。 在ggplot2中,我们首先进行数据的映射,这通过aes函数完成,它定义了数据如何映射到图表的视觉元素上。 例如,我们可以将时间点一的温度映射到x轴上,将时间点二的温度映射到y轴上。这样,尽管我们一开始看到的是一个空的图表,但实际上R已经读取了温度信息,并给了我们一个默认的坐标轴范围。然而,仅仅映射数据还不够,我们还需要指定数据的可视化形式,即选择合适的几何对象来表示数据。在ggplot2中,这通过添加各种以geom开头的图层来实现,比如geom_point用于绘制点图。我们还可以继续添加其他图层,比如通过geom_smooth添加回归线,来观察两个变量之间的关系。 ggplot2的一个特点是它提供了很多默认值,使得我们可以用很少的代码生成复杂的图表。例如,默认情况下,点图是以黑色圆点表示的,大小和颜色都是根据默认比例来设置的。但我们也完全可以自定义这些元素,比如用不同颜色表示不同性别的体温数据。此外,我们还可以对图表的各个元素进行精细控制,包括坐标轴和图例。我们可以调整坐标轴的标签、名称,以及图例的内容和样式。这些元素的每一个细节都是可以修改的,以适应我们的具体需求。 最后,我们可以在图层上应用美学映射(aes),这不仅仅是关于颜色和形状,还包括如何处理数据,比如将性别转换为因子,并据此生成不同的可视化元素。 总结一下,我们首先进行数据映射,然后选择合适的几何对象来表示数据,最后将不同的图层叠加起来,形成一个完整的图表。通过这种方式,我们可以创建出既丰富又易于理解的视觉表示。 数据映射 # 以penguin问卷中前后体温为例 p1 <- pg_raw %>% ggplot(aes(x = Temperature_t1, # 确定映射到xy轴的变量 y = Temperature_t2)) p1 ## 坐标轴名称已对应,虽然图片为空 添加图层-散点 p1 + geom_point() 添加图层-拟合曲线 p1 + geom_point() + geom_smooth(method = 'lm') ## `geom_smooth()` using formula = 'y ~ x' 改变映射 pg_raw %>% drop_na(Temperature_t1,Temperature_t2,sex) %>% ggplot(aes(x = Temperature_t1, y = Temperature_t2, color = factor(sex))) + geom_point() + geom_smooth() ## `geom_smooth()` using method = 'gam' and formula = 'y ~ s(x, bs = "cs")' 8.3.3 单个图片的组成 图表中实际上有许多元素是可以被操控的。这里举一个例子,比如坐标轴和图例。坐标轴,就像我们这里下面框出来的部分,它包括轴本身和框出的整个区域。在坐标轴中,我们可以进一步细分,比如轴的标签、名称,以及轴的尾部名称,这些都是可以调节的元素。此外,图表中的线条,比如颜色和粗细,也都是可以协调一致的。 另一个我们可以精细操控的元素是图例。图例的整个区域,包括名称、取值范围,以及每个取值范围对应的可视化元素,都是可以自定义的。例如,我们之前使用了性别变量,并将其转换为一个因子,图例中直接显示了因子的名称。这些名称当然是可以修改的,而且每个因子都有自己对应的可视化元素。这意味着图表中的每一个小元素都可以进行精细的修改,以满足我们的具体需求。 8.4 常用图形 8.4.1 直方图 对于连续变量,一般通过直方图对它进行可视化。以认知实验中被试的反应时为例,反应时的单位一般到秒,如何对它进行可视化呢?首先,我们需要进行映射,目的是为了对数据进行可视化。我们关注的是数据的频次分布,因此只需映射一个值,即反应时间,代码如下: pic_his <- mt_raw %>% # 确定映射到x轴的变量 ggplot(aes(x = RT)) + geom_histogram(bins = 40) + theme_classic() 在代码中,我们使用了前面提到的管道操作,将数据传入一个函数,并作为其输入。这个函数就是ggplot。ggplot的第一个参数是数据框,我们默认使用前面的数据框作为输入。aes进行数据映射,x是什么?是我们用数据框中的哪一个变量来做x的映射值。映射完成后,我们对映射的数据进行可视化,使用的是以goem开头的函数,它是直方图。如果我们不输入bins,它也会出图,但我们可以选择一个bins的数量,即直方图中的间隔数量。通过这样的处理,我们可以看到图像变得更加干净。而 theme_classic()会使X轴和Y轴在图中更加清晰,使图的整体风格相比默认模式发生变化。 ### 密度图 我们也可以使用密度图来描述反应时的分布情况。在直方图中,我们使用高度来表示数据的多少,而在密度图中,我们使用平滑曲线来表示数据的分布情况。在绘制图形时,我们可以使用“geom”命令来选择几何图形,如条形图或密度图。 pic_dens <- mt_raw %>% ggplot()+ # 绘制密度曲线 geom_density(aes(x = RT)) + theme_classic() 大家可以看到,这里的代码与之前有一点变化。我们同样将整个数据框,包括反应时数据,作为输入,并直接使用ggplot函数。在这里,我们没有添加任何额外的参数。接下来,我们通过叠加图层的方式来增加一个密度曲线。在这个密度曲线的函数中,我们补充了数据映射信息,这与之前的做法是一样的,即只映射x轴。我们将rt(反应时间)映射到x轴上,然后函数就会生成一个曲线图。我们使用了class口的一个功能来实现这一点。大家可以看到,这两个图的信息非常相似:曲线的高度与频次相对应,频次越高的地方,曲线的高度也越高。这里的y轴代表density,即密度。 8.4.2 直方图+密度图 我们也可以将直方图和密度图尝试叠加在一起 ## 尝试将两个图层叠加在一起 mt_raw %>% ggplot(aes(x = RT))+ geom_histogram(bins = 40) + geom_density() + theme_classic() 我们想要直接在这个频次图上叠加图层,首先将rt(反应时间)映射到x轴上,并使用histogram函数来绘制直方图。画完直方图后,我们考虑到ggplot2可以直接进行图层的叠加,所以我们可以尝试直接在直方图上叠加一层密度图。我们期望的是,如果图绘制正确,我们应该能看到一条沿着直方图边缘的曲线,因为这两者实际上表示的是同一种信息。 但是,我们实际上看不到这条曲线,这可能是因为y轴的信息差异,单位没有保持一致,直方图的单位可能是500或1000,而密度图的可能只有1或2,这导致密度图可能已经叠加在上面,但由于其规模较小,我们无法直观地看到。这是因为直方图和密度图在y轴上的单位没有保持一致。 为了解决这个问题,我们可以将直方图的y轴从计数转换为密度,这样无论是绘制直方图还是密度图,我们都是以密度作为绘图的数据映射,这样,我们就能在直方图上清晰地看到叠加的密度曲线,它们表示的是类似的信息,代码如下: pic_mix <- mt_raw %>% ggplot(aes(x = RT, ## 直方图的统计结果通过after_stat(density)传递给了密度图 y = after_stat(density))) + #<< geom_histogram() + geom_density() + theme_classic() # 设定绘图风格 在代码中,我们需要做的是将rt映射到x轴上,并显式地指定y轴的处理方式。默认情况下,当只绘制直方图时,y轴会自动设置为计数。但我们可以显式地将y轴设置为对rt进行density处理,这样y轴就会显示rt在不同区间的密度。通过这种方式,无论是使用histogram还是density,它们都将使用一致的y轴范围,从而使得两种信息能够在同一张图上进行呈现。 8.4.3 箱线图 箱型图也是我们常用的一种图形,除了将单个变量可视化,我们可以尝试将两个变量的关系可视化,在这种情况下,我们可以考虑使用箱线图(box plot)。大家可能还记得,我们的数据根据不同的条件(比如图形的好与坏,自己与他人,以及匹配与不匹配)分成了几种情况。如果我们想查看这四种条件下,无论匹配与否,反应时间是否有显著差异,我们可以通过箱线图进行快速可视化,代码如下: pic_box <- mt_raw %>% ggplot(aes( x = Label, y = RT)) + geom_boxplot(staplewidth = 1) + # 绘制箱线图并添加上下边缘线 theme_classic() 在映射数据时,我们将条件(label)映射到x轴,由于这是字符型数据,R语言会将其视为离散数据,并在x轴上表示为四个不同的点,每个点代表一个条件。R语言默认按照字母顺序对字符型数据进行排序。在y轴上,我们映射的是rt(反应时间),这是一个连续型数值。 完成数据映射后,我们可以选择多种方式来展示结果。对于初步查看不同条件之间是否有区别,我们通常使用均值或中位数来表示数据的整体情况,使用四分位距、全距或标准差来表示数据的离散情况。箱线图能够将这些元素综合展示,让我们能够同时看到数据的集中趋势和离散程度。 在这里,我们使用了ggplot2的箱线图功能,它会自动计算每个条件下反应时间的中位数和四分位距,并将离群值标记出来。箱线图的每个框代表一个条件,框中的横线表示中位数,上下边界表示四分位距。R语言默认使用1.5倍四分位距作为离群值的判定标准。 ggplot2的优势在于,它简化了绘制复杂图形的过程,通过集成的默认值和简单的命令,就能生成箱线图这样的数据可视化图形。 箱型图矩形中间线为中位数,上下两条线分别为上四分位数和下四分位数;1.5个四分位距(Q3-Q1)以外的值为离群值;goem_boxplot默认使用1.5IQR。 8.5 Data Explorer 8.5.1 Data Explorer Explorer也是一个很不错的数据探索工具,可以帮助我们快速探索数据。我们可以使用安装工具包来实现可视化,比如plot_string,它可以将DataFrame中的所有列名以可视化的形式表达出来,类似于思维导图中的树形图。 DataExplorer::plot_str(mt_raw) 另一个是plot_intro,它可以显示一些信息,比如有多少个离散数据列,有多少个连续数据列等等。我们可以看到,对于我们的匹配数据,离散列占56%,连续列占43%,所有列都是缺失值的占0%。每个数据至少都有一些值,完整的行占97.46%。缺失观测值的数量也可以通过可视化方式快速了解。 DataExplorer::plot_intro(mt_raw) 据探索包的一个独特特点,它可以帮助我们快速可视化数据。关于缺失值,我们可以使用plot_missing命令将具有缺失值的列可视化。大多数列都没有缺失值,只有一个响应列有2.5%的缺失值。 DataExplorer::plot_missing(mt_raw) 我们可以看到,几乎所有数字化变量的计数都可以用条形图表示。例如,性别可以用female,male,2和1表示。我们可以看到,大多数人是右撇子,而匹配和不匹配的比例是一致的。如果我们在匹配条件下看到匹配比不匹配多或不匹配更多,那么可能存在问题,因为我们的实验设计是一致的。同样,我们的实验条件应该是平衡的,因此看起来应该是一模一样的。 DataExplorer::plot_bar(mt_raw) 我们可以使用plot_bar将所有变量以bar图的形式呈现出来。我们还可以根据match条件将数据分成matched和mismatched两部分,并用bar图表示每个部分的比例。在大多数情况下,matched和mismatched是平衡的。我们还可以使用histogram来快速绘制所有变量的分布情况,特别是连续变量的分布情况。我们可以使用ggplot来检验数据是否符合正态分布。 我们还可以通过选择一个特定的列作为分组条件,来绘制不同组别的比例。当然,我们这里选择的比例基本上都是相同的,这是因为我们的条件设置在各部分都是一致的。例如,在练习部分,无论是左利手还是右利手完成,图形的呈现都是一样的。这说明了什么呢?这表明大多数情况下,匹配和未匹配的比例是1:1,这符合我们的实验设计。但是,在反应方面,比例可能不是1:1,这意味着被试的反应可能存在一些差异。 DataExplorer::plot_bar(mt_raw, by="Match") 对于直方图,它能够快速地为数字型数据生成可视化图形。我们之前已经关注了反应时间(rt),如果你有许多这样的数字型数据,多列的频数图可以一下子全部展示出来。这样,我们就可以看到每个列、每个数据列的整体情况。 DataExplorer::plot_histogram(mt_raw) 还可以画qq图来看数据的正态性 DataExplorer::plot_qq(pg_raw[,2:10]) 这个包是为了快速探索数据框中数据的情况而专门开发的。它提供了许多实用且快速的功能,例如相关系数(correlation)分析。如果你进行问卷分析,结构图会非常友好,因为它能让你看到不同条目之间的得分,以及它们是否显示出预期的高相关或低相关。 DataExplorer::plot_correlation(na.omit(pg_raw[, 2:30])) 在ggplot2体系中,还开发了一些符合其规范的附加包,比如“ggpairs”。这个包可以在数据组合后直接绘制它们之间的关系图,图中会展示相关系数和每个数字的分布。此外,还有散点图,它能在探索相关性时快速提供原始数据、统计值以及每个数据自身的分布情况。这些功能都帮助我们快速地进行数据探索。 8.5.2 使用ggpairs code ## 以 penguin project 数据中 ALEX, stress和 ECR 为例 pg_raw %>% mutate( # 计算均值 m_ALEX = bruceR::MEAN(., var = 'ALEX',items = 1:16,rev = c(4,12,14,16)), m_stress = bruceR::MEAN(., var = 'stress',items = 1:14,rev = c(4:7,9,10,13) ), m_ECR = bruceR::MEAN(., var = 'ECR',items = 1:36 ) ) %>% select(contains('m_')) %>% GGally::ggpairs() 本节课重点强调了数据探索的重要性,尤其是通过可视化方式进行探索。我们已经开始使用ggplot2,虽然我只是展示了ggplot2的一些功能,并没有深入讲解每个元素的操控细节。这是因为我们的数据分析课程有一个渐进的流程:从数据导入、清洗,到初步了解数据,接下来自然是进行统计分析。在接下来的几节课中,我们将学习如何使用R进行统计分析。 这就要求大家通过练习来提高自己操作数据和探索数据的能力,将前面学到的内容整合起来。例如,通过选择数据框中的数据来进行绘图,以及对数据类型进行转换,改变图形中元素的呈现顺序。我建议大家针对不同图形的击中率进行分组绘图,并使用箱线图来展示。 8.6 练习 8.7 参考阅读 R Graphics Cookbook ggplot2: Elegant Graphics for Data Analysis (3e) "],["第九讲回归模型二分层线性模型.html", "Chapter 9 第九讲:回归模型(二):分层线性模型 9.1 回顾 9.2 重复测量方差分析 9.3 分层线性模型/多层线性模型(HLM): 9.4 多层线性模型的应用 9.5 HLM的应用 9.6 思考", " Chapter 9 第九讲:回归模型(二):分层线性模型 # Packages if (!requireNamespace('pacman', quietly = TRUE)) { install.packages('pacman') } pacman::p_load( # 本节课需要用到的 packages here, tidyverse, # ANOVA & HLM bruceR, lmerTest, lme4, broom, afex, interactions, knitr) options(scipen=99999,digits = 5) 9.1 回顾 大家晚上好,我们开始上课,上节课我们介绍了使用r语言分析数据,以我们最常见的或者说我们心理学当中,最常见的两种方法ttest和ANOVA,但是我们给它加了一个标题,叫做回归模型或者线性模型一,然后我们在介绍完如何使用常规的R代码来实现这些功能之后,我们又给大家讲解了一下,为什么t-test和ANOVA实际上是线性模型的特例,那么这个是如果要运行我们Rmarkdown的话一定要提前准备一下这个代码。我们上节课是以这个表结尾的,也就是说我们常见的这个t-test,包括单样本、独立样本和配对样本的ttest以及单因素的方差分析和多因素的方差分析,基本上都可以用R里面的最基本的这个space,就是统计学的这个包里面的lm (linear model)来实现,而且我们也可以从线性模型的角度对它进行解读。比方说我们发现这个t-test,它可能就是一个自变量是二分变量的一个回归模型然后还有其他的我们在这里都有讲解。 R自带函数 线性模型 解释 单样本t t.test(y, mu = 0) lm(y ~ 1) 仅有截距的回归模型 独立样本t t.test( \\(y_1\\), \\(y_2\\)) lm(y ~ 1 + \\(G_2\\)) 自变量为二分变量的回归模型 配对样本t t.test( \\(y_1\\), \\(y_2\\), paired=T) lm( \\(y_1\\) - \\(y_2\\) ~ 1) 仅有截距的回归模型) 单因素ANOVA aov(y ~ G) lm(y ~ 1 + \\(G_1\\) + \\(G_2\\) + …) 一个离散自变量的回归模型 多因素ANOVA aov(y ~ G * S) lm(y ~ \\(G_1\\) + \\(G_2\\) + … + \\(S_1\\) + \\(S_2\\) + …) 多个离散自变量的回归模型 那么上节课给大家在讲解的时候,我们用的是penguin那个数据,在这里也可能会涉及到对一些离散变量的虚拟编码的问题,对离散变量的这个虚拟编码dummycoding,它其实有很多种方式,在这个space这个包里面,它专门有一个叫做controltreatment这样一个方式来对我们的离散变量怎么进入回归方程进行编码,有各种各样的方式,大家如果感兴趣呢,可以去参考一些相关的资料。 首先就是大家注意不同的软件或者不同软件包,它的默认的编码方式可能是不一样的,这里面可能会有一些区别,所以有可能比方说由于这个默认的编码方式不一样,最后会导致一个你看到的统计的结果是不一样的。在第八课我们在课后做了一点点小的修改,就是我们不仅仅可以采用,比方说FX那个包来去达到一模一样的一个回归,我们可以采用多种方式来实现跟方差分析一模一样的结果,其中一个方式,就是改变controltreatment改变编码方式。大家如果把我们那个最新的rmarkdown拉下去回顾的话,看一下我们上节课的课件。 [预处理] df.penguin <- bruceR::import(here::here('data', 'penguin', 'penguin_rawdata.csv')) %>% dplyr::mutate(subjID = row_number()) %>% dplyr::select(subjID,Temperature_t1, Temperature_t2, socialdiversity, Site, DEQ, romantic, ALEX1:ALEX16) %>% dplyr::filter(!is.na(Temperature_t1) & !is.na(Temperature_t2) & !is.na(DEQ)) %>% dplyr::mutate(romantic = factor(romantic, levels = c(1,2), labels = c("恋爱", "单身")), # 转化为因子 Temperature = rowMeans(select(., starts_with("Temperature")))) # 设定相应的标签 breaks <- c(0, 35, 50, 66.5) labels <- c('热带', '温带', '寒温带') # 创建新的变量 df.penguin$climate <- cut(df.penguin$DEQ, breaks = breaks, labels = labels) 在结果上面,这个treatment,我们的现在这种编码方式,实际上就是以其中的一个条件,比方说我们上节课讲到了不同的这个气温带,那么我们实际上就是以热带作为基线,另外的这种回归系数它分别其实表示的是另外两种条件跟它的一个差值,这里我们其实可以从这个这个统计数据上面能够看得出来。 [虚拟编码] # 比较不同气候条件下个体的体温是否存在差异: ## 虚拟编码 contrasts(df.penguin$climate) <- stats::contr.treatment(unique(df.penguin$climate)) ### contr.treatment本质上创建了一个矩阵 ### 由于3个分组,所以矩阵为2列 ## 建立回归模型 lm_temp <- stats::lm(Temperature ~ climate,data = df.penguin) [结果] ## 输出回归系数 lm_temp %>% tidy() %>% select(1:3) %>% mutate(across(where(is.numeric), ~round(., 3))) ## # A tibble: 3 × 3 ## term estimate std.error ## <chr> <dbl> <dbl> ## 1 (Intercept) 36.6 0.022 ## 2 climate寒温带 -0.178 0.028 ## 3 climate温带 -0.299 0.03 ## 可以看到回归的结果以热带为基准,系数则为均值之差 df.penguin %>% group_by(climate) %>% summarise(mean = mean(Temperature)) %>% as.data.frame() ## climate mean ## 1 热带 36.555 ## 2 温带 36.377 ## 3 寒温带 36.255 虚拟编码方式很多,可参考这里 9.2 重复测量方差分析 那这是我们上节课的一个结果,上节课我们回答了一个问题,对于这种有重复测量的这种情况怎么办?在我们心理学当中另外一个非常常见的一个现象,或者说使用的一个方法,就是用重复测量的方差分析。那比方说我们检验某一种干预,某种心理干预也好,或者是药物干预也好,它有没有效果,一般我们会设计一个干预前进行一次测量,干预之后再进行测量,不仅如此,可能还有实验组和控制组,有组间变量,有组内的变量,它就是前后测,前后测的时候就涉及到了一个重复测量的问题。所以在我们心理学当中,尤其在实验研究当中,我们非常常用的,就是重复测量的方差分析。那么我们上节课既然讲到了ttest和方差分析,组间的方差分析,它是一个线性模型的话,那么重复测量的方差分析,它是不是也是一个线性模型呢?我们这个时候以我们在课堂上经常采用的另外一个例子,就是我们那个实验matching的这个数据为例,这个数据,它是一个典型的认知心理学的实验,采用的是完全的被试内设计,我们有2*2的这个实验设计,也就是说我们有两种自变量,一个是刺激他的身份,另外一个是道德上的效价,是积极的moral还是消极的immoral,那么这两个自变量的话,它就组合成为4个条件,每一个被试在实验当中,都要经历所有的4种条件处理。我们通过之前学到的bruceR,然后用here这个包,在我们的这个课件的所在文件夹内部,可以通过这种方式把它读取进来,读取进来之后的话,我们可以看一下,我们先做了一个处理,在实验设计中,我们有两个自变量,但是在我们的这个数据里面我们当时是用一个变量shape来表示这两个自变量,就是两个自变量的各种组合,我们这里先把它进行了一个拆分,实际上是用的这个tidyr里面的一个函数,这是数据预处理的部分。大家有可能以后也会经常碰到这种字符的处理tidyr里面有一些函数是比较方便的。处理完了之后可以看到它这个数据,每一个被试,有他的年龄性别,还有他的左右利手,然后还有这个实验的信息,原来是一个变量,这个试次他这个形状代表的是什么,拆分之后的,它就拆分成了两个变量,一个叫做Valence,一个叫做Identity。 [预处理] mt_raw <- bruceR::import(here::here('data','match','match_raw.csv')) mt_raw <- mt_raw %>% tidyr::extract(Shape, into = c("Valence", "Identity"), regex = "(moral|immoral)(Self|Other)", remove = FALSE) [数据展示] 以match_raw.csv为例,一个2x2的被试内实验设计(Identity:Self vs.Other) x Valence:Moral vs.Immoral)),我们希望知道这两种条件之下被试的反应时是否存在显著差异然后我们就发现,这个数据实际上是有很多的,每个被试还有很多个试次,那么这种情况的话,大家通常做法是怎么做呢?如果通常我们用常规的这个重复测量方差分析,大家怎么做呢?比方说有40多个被试,然后有四种条件,我们最后会算出每个被试在每种条件下,比方说我们关心反应时间的话,就是反应时间的一个均值,那么最后我们得到比方说44个被试他们的这个反应时间的均值,然后呢我们就把它输入到SPSS里面,然后进行一个重复测量方差分析,然后把它对应好,这是我们常规的做法,它数理结构基本上就是这样的。每一个被试有自我有他人,自我和他人下面又有moral和immoral两个条件,other的也有moral和immoral。 这样的话,如果说我们在本科阶段我们讲方差分析的时候,我们就会告诉大家,这里面会进行方差的分解,我们把它分解为,不同条件之间的变异,或者不同的自变量引起的变异,以及这个被试的个体差异,我们主要关注的,比方说就是这个自变量它引起的变异在总体的变异当中的比值,然后根据这个比值去计算f值等等等等,计算MSE就是谁的,这是本科的或大概考研的时候,可能会涉及到内容,那么在SPSS里面,大家可能也看到过,类似的这个实现的方式,那么在r里面也很方便的进行实现. 那我们先按照常规的方式,先进行一个预处理,比方说我们一般看反应时间的时候,我们主要看的就是正确的反应试次的反应时间,我们把没有反应的和这个错误的都剔除掉,那么在这里我们还有一点额外处理,我们先还有一个条件,就是这个图片和这个文字是不是匹配的,我们这个时候只关心这个匹配的,因为我们上课做一个演示,简化一点,那么所以我们筛选出的是匹配的,这个试次的数据,因为我们关心反应时间,所以我们选出了全部为正确试次的一个反应,那么然后我们就通过这个group_by,在数据预处理里面我们提到的这个函数,以被试的ID、identity和valence三个作为分组变量,然后通过summarise去求他们的均值,这都是我们前面学到的内容,然后这样的话我们就得到了非常熟悉的这种数据。但这个时候如果大家想要,把它输到SPSS进行预处理的话,我们还要把它从长型的数据转成宽型的数据,但是在r里面我们其实没有必要转,我们可以直接使用bruceR里面的这个ANOVA,然后把被试的ID放进来,然后DV,dependent variable等于RT,很方便,这个within就表示是within-subject,应该是independent variable,我们这里有两个就是identity和valence那么到这里其实整个r里面就输完了,我们这里只不过是把它的结果,后面这个部分的话是为了显示结果,所以前面的这个部分,就是大家可能会比较关心的。如果你自己在r里面你就选择这段代码运行的话,你会在那个rmarkdown里面直接看到它全部的输出,那么我们看多少里面最重要的输出,或者我们最关心的输出可能就是这个方差分析表,那么这里面我们可以看到identity valence,它们分别的主效应和它们的交互作用,这时候我们可以看到很明显的,比方说这里有p值、f值这是大家都很喜欢的。 [ANOVA-bruceR] mt_mean <- mt_raw %>% dplyr::filter(!is.na(RT) & Match == "match" & ACC == 1) %>% dplyr::group_by(Sub,Identity,Valence) %>% dplyr::summarise(RT = mean(RT)) %>% dplyr::ungroup() ## `summarise()` has grouped output by 'Sub', 'Identity'. You can override using the `.groups` ## argument. ## 本例为长数据 ## RUN IN CONSOLE! ## 球形检验输出: bruceR::MANOVA(data = mt_mean, subID = 'Sub', # 被试编号 dv= 'RT', # dependent variable within = c('Identity', 'Valence')) %>% capture.output() %>% .[c(33:37)] ## ## * Data are aggregated to mean (across items/trials) ## if there are >=2 observations per subject and cell. ## You may use Linear Mixed Model to analyze the data, ## e.g., with subjects and items as level-2 clusters. ## [1] "Levene’s Test for Homogeneity of Variance:" ## [2] "No between-subjects factors. No need to do the Levene’s test." ## [3] "" ## [4] "Mauchly’s Test of Sphericity:" ## [5] "The repeated measures have only two levels. The assumption of sphericity is always met." 然后还有大家可能没有那么常用的、现在越来越推荐报告的,比方说这个eta-square,叫伊塔方,反映的是效应量的一个指标。那么bruceR里面有一个好处,就是它会输出多个效应量的指标,包括partialeta-squared,偏伊塔方,包括generalized eta-squared,这是一个更加推荐的效应量的指标还有cohen’s f²,这也是以前会有一些元分析的时候,大家会用到的一些指标。那么从这里顺便可以稍微多讲一句,就是当实验设计是ANOVA的时候,大家有的时候可能会想,我如何用它来规划样本量,把什么作为一个效应量,输入到g*power里面而这里要有一个非常值得注意的问题,就是大家如果是完全被试内实验设计,千万不要说我用g*power做了这个样本量的规划或者power analysis,因为g*power做不了这个事情,如果你审稿的稿件里写这么一句的话,审稿人如果有经验的话,就一下看到你可能就是表演了一下power analysis。 [ANOVA-bruceR(输出)] bruceR::MANOVA(data = mt_mean, subID = 'Sub', dv = 'RT', within = c('Identity', 'Valence')) %>% capture.output() %>% .[c(15:31)] ## ## * Data are aggregated to mean (across items/trials) ## if there are >=2 observations per subject and cell. ## You may use Linear Mixed Model to analyze the data, ## e.g., with subjects and items as level-2 clusters. ## [1] "ANOVA Table:" ## [2] "Dependent variable(s): RT" ## [3] "Between-subjects factor(s): –" ## [4] "Within-subjects factor(s): Identity, Valence" ## [5] "Covariate(s): –" ## [6] "─────────────────────────────────────────────────────────────────────────────────" ## [7] " MS MSE df1 df2 F p η²p [90% CI of η²p] η²G" ## [8] "─────────────────────────────────────────────────────────────────────────────────" ## [9] "Identity 0.008 0.003 1 43 2.554 .117 .056 [.000, .198] .009" ## [10] "Valence 0.128 0.002 1 43 64.374 <.001 *** .600 [.440, .706] .131" ## [11] "Identity * Valence 0.033 0.002 1 43 14.364 <.001 *** .250 [.085, .417] .037" ## [12] "─────────────────────────────────────────────────────────────────────────────────" ## [13] "MSE = mean square error (the residual variance of the linear model)" ## [14] "η²p = partial eta-squared = SS / (SS + SSE) = F * df1 / (F * df1 + df2)" ## [15] "ω²p = partial omega-squared = (F - 1) * df1 / (F * df1 + df2 + 1)" ## [16] "η²G = generalized eta-squared (see Olejnik & Algina, 2003)" ## [17] "Cohen’s f² = η²p / (1 - η²p)" 然后这是bruceR里面的输出,我们可以看到bruceR,实际上是对另外一个很方便的包,进行了一个封装,就是afex,我们其实之前也见过这个包,那么在afex里面我们可以得到同样的结果,这里我们就不再展示了,那么通常如果说我们做这个方差分析,到这里我们看到交互作用,接下来就进行简单效应分析,就像我们上节课结尾的时候,上节课我们用emmeans去查看不同条件下,比方说在不同的identity条件下面不同valence的效应,或者反过来不同的valence下面identity的效应,这里就不展开了,因为跟上节课是一模一样的,大家可以借用上节课的一个代码来做同样的事情。 [ANOVA-afex] ## bruceR::MANOVA 是对afex的封装 m_aov <- afex::aov_ez( data = mt_mean, id = 'Sub', dv = 'RT', within = c('Identity', 'Valence')) m_aov ## Anova Table (Type 3 tests) ## ## Response: RT ## Effect df MSE F ges p.value ## 1 Identity 1, 43 0.00 2.55 .009 .117 ## 2 Valence 1, 43 0.00 64.37 *** .131 <.001 ## 3 Identity:Valence 1, 43 0.00 14.36 *** .037 <.001 ## --- ## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '+' 0.1 ' ' 1 那么我们今天想要讲的是,从这个方差分析,它可能对于做实验的同学来说非常熟悉,在R里面也非常简单实现,那它到底有没有问题,或者说是不是有更好的做法,以及为什么我们可以认为它是一个线性模型的一个特例。 首先,我们看到的基本上就是,所有的效应都是叫做主水平的效应,如果大家去仔细回顾一下方差分解的逻辑,会发现在整个方差分解的过程中,我们关注的都是不同条件下主水平的一个均值,每个数据点和这个均值的偏移到底是什么样,从这个角度来讲的话,我们会发现在重复测量方差分析里面,我们基本上完全忽略了个体的差异,当然在我们大部分的认知实验当中,我们也都是不太关注个体差异的,我们可能更多的关注的是这个效应,也就是不同条件之间的一个差异,然后另外这个缺失值的处理,也是非常严格的,比方说我们有2*2四个条件,如果说我们有一个条件上,有一个缺失值的话,那整个被试的数据就没办法再使用了,你如果采用更加老一点的做法,不管你是用SPSS还是用EZ这个R包,当你的数据不平衡的时候,它直接就没有办法出结果,会报错,那么你只有把有缺失值的整个被试的数据完成删除掉,你的数据才能够运行。但是他会带来一个问题,即便这个被试可能缺失了一个条件,但他还有三个条件的数据,这三个条件数据其实也是有信息的,还有他会对这个数据类型有要求,比方说我们现在看到的这个自变量他是分类的条件,假如我们自变量是个连续的数据,我们这个时候就很难用这个重复测量的方差分析了,然后还有我们对每一个被试的整个数据,其实利用率是比较低的,我们可以回过头来看一下,我们这个数据,我们在看到这个原始数据的时候,每个被试在每个条件上都有很多个试次,每个试次下面都会有自己的反应时间,但是我们在做重复测量方差分析的时候,我们先把它求了一个均值,我们用的是一个每个被试,比方说它有60个trial,60个试次或者70个试次,最后我们就把它平均起来,当然你平均之后,每一个被试的每一个试次有一定的信息可以用起来,但是可能还有很多信息都被丢失掉了,这带来了数据的一个浪费,试次的一个浪费。 重复测量方差分析有没有局限性? 个体间差异同样无法估计; 处理缺失值只能将整行观测删除,会导致标准误增加、功效降低; 对因变量(连续)和自变量(分类)的类型有要求; 对每一个试次中数据利用率低,造成试次的浪费 重复测量方差分析有没有局限性? 因此,现在越来越多的期刊推荐使用多层线性回归(Hierarchical Linear Model) ,如Neuron。 那么在脑电的这个数据当中,其实有的时候你需要权衡到底每一个被试做多少次,以及做多少人,你才能够比较有比较强的统计检验力把你关注的这个效应看出来,那么每个被试做多少试次和被试的人数之间的权衡,其实这两年关注的人也很多,包括前段时间我们在OpenScience的公众号,邀请了新加坡国立大学的一个课题组他们介绍的就是在fMRI中扫描的时间被试的数量之间如何达到一个平衡,因为你增加扫fMRI扫描时间,你也能够提高这个数据的质量或者说信号的强度,所以这里会涉及到很多如何充分使用这个数据的问题,以及提高我们的统计功效statistical power,那么正是因为这个原因,其实这两年在一些期刊上面,我们也能够明显的看到,就是大家开始推荐不再完全依赖于t-test和ANOVA,而是采用mixed model,比方混合线型模型,我们这里举的是Neuron这两年发的一篇,叫做Premier,类似教程吧就是两年之前的,再加上Neuron,是如果大家做神经成像偶尔就会发现,Neuron实际上是我们整个神经科学领域,不仅仅包括认知神经科学,包括神经生物学领域,非常顶级的一个期刊也说这种主流的期刊也都开始推荐使用这种更加合理的方式,这就是我们今天,要跟大家简单介绍一下的,多层线性模型。 它实际上就是用来处理这种多层嵌套类型的数据,我们之前说过,我们的这个数据是有有嵌套的,整个实验的数据,它是分成在一个一个的被试,每一个被试下面又有不同的条件,每个条件下面又有不同的试次,所以它是有这种嵌套的结构的,那么如果我们以前的这个方法,它就是有一些信息的浪费,所以现在很多人在推荐使用,这个层级模型,或者叫做分层模型,或者叫多层模型,它的名字非常多啊,比方说Hierarchy Model,或者叫做Hierarchical Linear Model,或者叫做Multi-level Model,或者叫做Linear Mixed Model等等等等,就是名字很多,Random Effect Model啊,大家知道它本质上就是,我们要去处理这种多层线性模型,多层的这种数据结构,那么我们如何去考虑层级之间的一个相互影响,另外我们就比方说比考虑,它的效应在不同层级之间的一个变化,这个主要就是多层线性模型或者分层线性模型的一个核心,或者层级线性模型。 9.3 分层线性模型/多层线性模型(HLM): 用于处理”多层嵌套数据”,在一个以上层次上变化参数的线性模型。但多层线性模型的名字非常多,不同学科称呼不同,有许多“近义词”:] - 层级/分层模型(Hierarchical Model,HM) - 多水平模型(Multilevel Model,MLM) - 线性混合模型(Linear Mixed Model) - 混合效应模型(Mixed Effects Model) - 随机效应模型(Random Effects Model) - 随机系数模型(Random Coefficients Model)..... 但在注意与多元回归(multiple regression)进行区分,即逐步引入自变量到回归模型中,以检验每个自变量对因变量的影响是否独立于其他自变量。 那么可能在有一些领域,它可能根据多层线性模型的思路,它发展出了一些新的,特异于处理某一些特定类型的数据的方法,那么它还会给它一些叠加一些其他的名字,最重要的就是你要看到它本质是什么,它是不是比方说用以线性模型,或是广义线性模型,广义线性模型我们下节课会讲以线性模型作为最核心的一个模型,然后去考虑他的这个数据的层级结构,或层次结构,如果他有做这样做,基本上你就可以确定,他的原理上,可能就是跟我们这里讲的是差不多的,这里跟多元回归是完全不一样,多元回归是说有多个自变量,但他是没有考虑这个层级的结构的,那么在这个多层线型模型或者层级线型模型当中,有两个很重要的概念,我们先给大家简单的说一下,因为我们这里的层级模型,它都是以回归模型,就是以正态分布为核心的,那么在这种现行的层级模型当中,一般我们会考虑,从截距和斜率这两个效应上面去考虑我们这些效应,那么层级模型当中,它最关注两个效应就是固定效应和随机效应,一个叫fixed effect,一个叫random effect,这个所谓的固定和随机,这个名字本身非常不好理解,那我这里也不去跟大家把这个做非常细致的展开,大家如果感兴趣的话,可以去看知乎上的一个博客,包寒吴霜老师写的,那个博客对随机效应做了一个比较详尽的一个梳理,那么我们就通过一个例子来给大家展示一下,什么叫做固定效应和随机效应,那比方说我们看一个非常简单明了的一个层级数据,比方说工龄和薪水之间的一个关系,我们想要调查高校老师的工龄和工资之间有没有关系,那么从某一个学校里面,随机抽取出5个学院,然后获得他们的工资和工龄之间的关系,这是一个网上的数据,这里有数据来源,大家如果拿到rmarkdown,可以点击这个来源,然后这个数据结构大家可以看到,它基本上是这么一个嵌套的结构,首先你整个大学或者你整个学校,然后它有不同的departments,它有不同的学院或者系,每个系下面有不同的人,你调查的时候,实际上就是在不同的学院下面,去搜不同的人,然后再去从这个每个人身上,找到两个数据,一个是他工作多少年,另外一个是他的这个工资,然后你关心的是工资,和他的工作年限之间关系。 在回归模型中一般会在截距和斜率上分别讨论固定效应和随机效应。 例如,关于研究教师的工龄(Experience)与薪水(Salary)之间是否存在关系。在某校随机抽取了5个学院的教师信息,具体数据如下:] 数据来源见(https://github.com/mkfreeman/hierarchical-models/blob/master/generate-data.R) 问题:是否可用工龄预测某个学校员工的工资? 数据结构 ## `geom_smooth()` using formula = 'y ~ x' 那么如果我们不考虑他的这个嵌套结构的话,我们可能就把所有信息都放在一起,就做一个总体上的一个回归模型,然后做一个简单的线性回归,那么在这个情况之下我们可以看到,x就是我们的这个这个工作年龄,然后y就是他的工资,那么从这个角度讲的话,看起来似乎在工资和工作能力之间是有一个微弱相关的,但是我们这个时候假如说把不同的department,把他们不同的这个学院用不同的颜色标出来的话,我们可以看到这个线他到底有没有捕捉到任何一个学院的信息你看起来好像其实它跟表面上看它似乎捕捉到了某种关系,但是仔细观察,和每个学院的模式似乎都很不一样。在这种情况下,我们可以看到,如果我们不区分数据的层级结构,那么最终得到的回归线或者说预测关系可能就不太有用,尤其是当我们已经掌握了学院的信息时的情况下。因此,在这里,我们可以明显看到两个问题: 不同学院的底薪有可能存在差异(存在/不存在) 不同学院间间,工资与工龄的关系存在差异(存在/不存在) 首先,当我们的工龄为0时,大家的起点实际上是有所不同的,不同的颜色区别是非常明显的,回归线的起点似乎反映了一个综合的情况。其次,在不同的学院之间,工龄和工资的关系也存在差异。有的情况下,随着工龄的增加,工资似乎呈上升趋势,而有的情况下,可能看不到明显的趋势,比如粉红色的。我们要去做回归的时候,会不会有两个这种院之间的差异是完全没有考虑到的,一个是是他们的底薪或起薪不同,第二个是不同学院内工资和工龄之间的关系在学员之间是存在明显差异的。我们不能够用一个总体的趋势去捕捉到每个学院内部的工作年龄和工资之间的关系,那说明我们的模型可能就没有那么有用了。 另外,我们还需要考虑四种不同的模型。如果你认为所有不同学院的底薪或起薪都没有差异,对于回归线来说,它的起薪类似于我们这里的直线和y轴的交点,就是我们的截距,你认为这个截距在不同学院之间没有变化,认为这个截距是固定的,这就是所谓的固定截距。此外,如果你也认为工资和工龄的关系在不同学院之间以同样的速率变化,也就是说他们之间的关系在不同学院之间也是固定的,那么在我们的回归线上的表现就是固定斜率。那这样我们这个相对是一个比较简单的回归中这个线,它的斜率代表是什么?是x每增加一个单位,基本上对应的y要增加多少。也就是斜率越大的话表示工龄每增加一年,工资增加得越多。所以回归线的斜率表示x和y之间的关系,在这种情况下,当你认为在不同学院之间,工资和工龄的关系是完全一模一样的时候,是固定的时候,那么回归线的斜率也是被固定下来的,这种情况就称为固定截距、固定斜率的模型。 这意味有可能会出现四种情况 对应在图中,则会在截距与斜率之间出现差异: 1.不同学院的底薪相同,工资涨幅也相同;(固定截距,固定斜率) 2.不同学院间底薪不同,但工资涨幅相同;(随机截距,固定斜率) 3.不同学院间底薪相同,但工资涨幅不同;(固定截距,随机斜率) 4.不同学院间底薪和工资涨幅都不相同。(随机截距,随机斜率) 画图看看. [fixI-fixS] ## `geom_smooth()` using formula = 'y ~ x' 所以我们的最开始不管学院,把所有的数据放在一起,然后做一个简单的回归就完了,但是还有其他的可能性.比方说我们认为截距,也就是起薪,在不同学院之间是不一样的,但是假定每个学院工资的涨幅,也就是随着工龄的增加,你的工资增加的速率是相同的,也就是说在不同学院之间是保持一致的。那么就会出现这种情况,每一个学院他的起薪,它的起点是不一样的,但是看这个斜线,它的斜率都是一模一样的,这个就叫做固定的截距,随机的斜率。随机的意思是,斜率在不同的组之间,在群体当中不同的组之间,它是在进行变化的,vary,但它并不一定是随机的在变动。所以为什么随机效应这个词刚开始让人很容易误解,它是说某一个特定效应在组成这个总体的不同的组当中他是在变化的。我们现在看到的这个直线的起点在不同学院之间是在变化的,如果我们在0这个地方画一条垂线,那么这个斜线和这个0和y轴的交界点它就是在变化的,这就是我们说的变化的截距,intercept。但是工作年限每增加一个单位,我们的回归线基本上都是一模一样。这个时候我们可以看到,他其实也不一定能够捕捉到每一个学院的特点。比方说我们还是看这个粉红的线,这个粉红线,它看起来更应该是一个平的,而不是这个斜斜的一个增长的方式,但是因为我们在建这个模型的时候,我们没有让它进行变化,我们强制的让每一个学院的回归线的斜率都是一模一样的,把它固定住了,那么这个时候,我们看到的就是这么一个拟合的效果。 [ranI-fixS] 还有一说是我们认为底薪是相同的,但是工资的涨幅是不一样的,这个就叫做固定截距、随机斜率的情况。那么最后就是这两个都是不同的,它的起薪也不一样,斜率也不一样。我们刚刚看到这个其实比较少见,它的截距是固定的,但斜率是不一样的,这个其实很不符合现状。 [fixI-ranS] 最后一个就是说这个intercept和slope,截距和斜率都是在变化的,这个时候我们看到它的拟合的效果,就是比较好的。比方说我们看到这个粉红的线,它的起薪比这个蓝色的要高,但是它一直没有什么太大的变化,蓝色的起薪比较低但它可能一直在变化,所以通过这个情况我们就能够捕捉到一个比较好的特点。这五条线反映出来的就是我们的所说的veryeffect,或者叫做变化的或者叫做随机的效应。有一些研究者更加喜欢使用very effect就是是变化的这个效应而不是random effect。 [ranI-ranS] ## `geom_smooth()` using formula = 'y ~ x' 那我们现在再把这个四个图给大家看一下,这个时候都固定,所有的都是一模一样的;这个斜率是固定的,但截距是变化的,这5条线是平行的,但起点不一样。那么这个起点都是一样的,但斜率都不一样,这种情况很少,很少有人会去建这样的模型。这个情况就是两个都在变化,不管是截距还是斜率都在变化,这就是我们在混合线性模型里面经常会碰到的随机效应,也就是在我们现有回归的模型里面,当我们做这种层级模型的时候,我们有可能有两个效应在变化,一个是intercept,一个是slope,这个slope就对应着我们自变量对应变量的影响。 那么如果大家对刚才这两个概念基本上清楚的话,因为刚才说这个数据仅仅是用来展示两种随机效应,那么对于我们的这个数据来说,我们回到我们那个反应时的数据,我们这个数据其实也有一个嵌套关系,每一个变量都嵌套在每一个被试当中,每一个被试它自己可能就存在一个类似于我们刚才观察的这个回归线,每一个被试它的每一个条件下面还有很多个试次,每一个试次又是一个数据点。所以在这里的两种效益,固定效益代表的就比方说在总体上的不同条件下的一个差异,比方说在我们的match这个实验当中有两个自变量,这两个自变量identity和valence,它们对反应时间的整体的影响就是我们说的固定效应。每个被试身上的identity和valence的影响可能跟总体的是有偏差的,那么每个被试身上identity和valence的影响就是一个very effect,是在变化的一个随机的效应。那么它反映的这种不管是被试个体差异也好,还是说被试是对identity和valence是一种特异性的反应也好,它都在跟总体上面identity和valence的效应产生偏差,这个偏差有可能是有意义的。这个时候我们就能够把它通过这种层级模型捕捉到。 无论在我们的数据中,还是刚才的数据中,其实都出现了层级或嵌套关系;只是对于match数据,每个变量都嵌套在一个被试中,而每个被试都可视为一条回归线。 [两种效应] 固定效应代表了实验中稳定的、总体水平上的效应,即它们在不同个体、群体或条件之间的影响是一致的,如match数据中Identity和Valence的效应 随机效应则表示了数据中的随机变异或个体间的差异的程度,以及这种变异程度如何随着特定分组因素的变化而变化。 [match_data] [shcool] 我们可以看一下,对于我们Naisen的数据来说,每个被试都做一个数据它下面嵌套了自我、他人,然后自我和他人下面又嵌套了moral和immoral,然后在这下面又嵌套了有60个试次或者50个试次,这下面有50个试次,同样对其他被试来说也是如此。 那么他们这个数据类型的嵌套方式,其实我们也可以把它跟我们刚刚看这个数据做一个类比,我们这里可以再加一个整体上的一个效应,我们好像在平时实验当中我们很少去看,不同的这个实验处理,或者不同的自变量,在个体的身上是不是有差异。可以想象,可能每一个被试自身的反应速度就会有很大的差异,有的被试很快,不管做什么反应都很快,因为他的基线的反应是就是非常快的,那么另一些他的反应就是整个就比较慢,不管是说自我的还是他人的,moral还是immoral他都慢。这种反映的类似于截距上的一个差别,他的总体基线就会比别人更快一点或者更慢一点。 对于match数据,类比与学校员工薪水数据,我们也可以设想: 不同被试总体上会不会存在反应时上的差异:有些个体普遍反应速度更快,而有些反应速度普遍更慢(随机截距) 在自我条件下,两种Valence的差异是否完全相同?还是会有个体差异?(随机斜率) 画图尝试一下,计算被试平均反应时后进行排序,选取首尾的几名被试: 我们在这里展示的4个被试的数据,大家可以看到,像这个被试,他的反应时间整体上面就会比其他的要低,当然他每一个试次上面可能会有快有慢,但是整体上这个被试他会比这个被试快很多,这个就是我们说的在反应时间上面可能存在这样的一个随机的截距,一些被试平均的反应时间就会要更快一点或者更慢一点。另外我们刚才说到我们这个实验有两个自变量,一个是valence,一个是identity在这里举一个例子,比方说在自我条件下面两种不同的valence的差异是不是相同的。 再看重复测量方差分析的简单应的时候,我们也是这么做的,自我条件下面moral和immoral它之间的差异是多少,我们最后甚至可以把它量化出一个Cohen’sd,这个时候我们量化出的cohen’sd,在自我条件下这两种不同条件的差异就是什么我们通常所说的fixeffect,固定的效应。但即使我们可以通过重复测量方差分析把固定效应算出来,每一个被试之间的差异是完全不知道的,那么有没有差异呢?我们可以看到是有明显差异的。我们这里画的是在自我条件下moral和immoral之间有没有差异,可以看到对于被试7307来说,moral比immoral要慢一点,可以看到这个斜线从左往右是向下倾斜的,7311这个被试就更显眼了,moral是比immoral要慢的,但7313和7324两个被试呈现了一种相反的趋势,moral要比immoral更快。整体上可能有更多的被试表现出了右边两个被试的情况,moral要比immoral更快,所以整体上我们可能发现moral要比immoral反应时更短。我们可以很明显地看到被试的个人差异,在JEG2021年左右有一篇文章专门强调需要去考虑实验处理的异质性,这里我们就看到了实验处理的异质性,在不同被试身上实验处理的效应是不一样的,我们以往是完全不关心的,但其实是需要关心的,如果我们认为我们的测量工具或者认知任务可以在某种程度上对被试内在的不可测量的认知能力进行测量的话,我们应该关注这些个体差异。 刚刚我们通过对原始数据的可视化发现,原始数据有很多的数据点,可能出现随机截距和随机斜率两种情况,也就是说在每个被试身上反应时的效应是会发生变化的,如果我们发现确实存在这个问题,有什么更推荐的方式去实现它呢?实际上在过去的大概十多年间,围绕线性模型,或者说层级模型这一类的方法开发出了很多系列的包,lme4应该算是目前使用的最广泛的一个包,至少是最广泛之一,如果光看它的语句,其实是很简单的。首先就是这个这个包的名字,然后做这个沉积模型的这个函数就是lmer,后面就输入代码数据,然后就是这个format,实际上是跟线性模型是类似的.如果你按照顺序的话可以把这个formula就直接不用写,它也能够识别出来。 9.4 多层线性模型的应用 我们使用lme4包对多层线性模型建模,具体语句形式如下: fit <- lme4::lmer( data = , formula = DV ~ Fixed_Factor + (Random_intercept + Random_Slope | Random_Factor)) #<< 注: 但在建立模型之前,需要考虑好在我们的数据中,随机效应和固定效应分别是什么。一般都会添加随机截距,而随机斜率的加入应当考虑是否有充足理由; 另外,由于随机效应是从某些总体中抽样的离散单位,因而本质上是分类变量 这个公式跟之前的线形模型是类似的,这个波浪号前面是dependent variable,因变量。这个公式分两个部分,一部分就是括号括起来的,另外一个是括号之外的,跟以前的线型模型是一样的,也就是说以前的线性模型只关注了fixed effect,回顾一下刚刚说的四种可能的模型,固定截距和固定斜率实际上就是传统的简单回归,所以固定效应的这些变量就放在括号外面,随机的截距和斜率就放在这里,random effect就是我们说的分组变量,比方说要用什么来对数据进行分组,看每个组上有不同的效应。一般random intercept,随机的截距用跟我们前面写的回归实际上是一样的,然后随机的斜率加上自变量的名字就可以了。 这可能是很多同学第一次接触分层模型,也是第一次接触随机效应和固定效应,所以很难去思考到底应该怎么去加等等等等,这里先跟大家说一下,实际上这个模型的建立,把什么样的变量纳入到随机效应之中,实际上是需要仔细思考的,想一想到底有没有这种可能性,要有充足的理由,一般来说大家都会加随机的截距,随机的斜率的加入目前来说是有争论的,至少我看到的一些文章中是有争论的。另外,随机效应一般是从总体中的一些离散单位,本质上是一个分类的变量。关于刚刚说的争论,比方说应不应该把所有的自变量加到随机斜率里,现在不同的研究者是有不同的看法的,大家目前来说可以暂时不用管它,在对层级模型越来越熟悉之后可以去考虑这些比较细致的知识点。 刚才说的是在r里面用lme4这个包去建分层模型的一个基本语法结构,在这里我们可以把数据带入进来,数据就是我们刚才说的原始的数据,前面是RT,我们有两个自变量,一个是identity,一个是valence。这里大家可以看到有一个加号,后面加了随机的效应,这个竖线的左边就是你的公式是什么,你要让那些效应有变化,右边是变化的效应是在那些组织间以什么样的标准进行分组,然后进行变化的,这里表示每个被试的截距是不一样的,换成更易懂的话来说,每个被试基本的反应时间是有快有慢的。当我们建了这么一个模型后,它代表我们可能认为,identity和valence这两个效应在主水平上是要进行检验的,但我们并没有检验它在每个个体上的差异,也就是说我们认为identity和valence在每个被试身上的效应和在总体上都是类似的。当然我们也可以认为identity和valence在不同被试间是有变化的,和刚才的相比,我们可以看到公式的变化就是在括号里把identity和valence都加进来了,这个星号一般在r里表示乘号,它实际上表示两个变量间所有可能的组合,比方说identity*valence实际上表示identity的主效应加上valence的主效应,再加上它们之间的交互作用,交互作用是由冒号来表示的,目前这种用一个乘号来表示自变量之间所有可能的写法是很常见的,在r和python中至少我观测到的主流的方法都采用这种方式了。 [ranI] ## 随机截距 固定斜率 model <- lme4::lmer(data = mt_raw, RT ~ Identity * Valence + (1|Sub)) Identity*Valence: *表示两变量间所有可能的形式,等同与Identity + Valence + Identity:Valence (1|Sub): 1表示随机截距(0则表示固定截距); 管道(|)右侧Sub为随机因子 在这个随机的模型中,1表示随机截距,有时候我们也可以认为它没有随机的截距,把它固定下来,这个时候一定要写上0,0加上什么什么,我们在写固定效应的时候,没有写1加上identity乘以valence,是因为在r里面,默认的就有一个1加什么什么,但在随机的效应里必须写出来,也就是说如果我们认为它没有随机的截距,也得写出来。在前面的这个公式里面,如果我们认为他没有截距的话,我们也可以就写0加上什么什么,这个公式基本上都是通用的,当然随着模型越来越复杂,大家感兴趣的话可以查一下这个公式中很多其他符号的写法对于心理学院来说,我们最常用的可能就是这里解释的乘号,还有它们之间表现交互作用的冒号,还有括号里加竖线表示的随机效应。 假如我们用现在的这个数据建了两个模型,一个是随机截距、随机斜率,另一个是随机截距、固定斜率,这时候就存在一个问题,这两个模型可能都能跑出来,那么我们到底要看哪个结果?实际上当我们采用层级模型的时候,或者当我们把线性的回归模型当作一种模型来处理的时候,我们会发现它的结果远远比我们在心理统计学中学到的t检验、方差分析以及之后的一系列分析要更复杂,因为它的模型首先有很多个。大家可以看看到这里标注的公式,RT这个是identity*valence,假如我们不变固定效应的这部分,那么对于随机效应的部分我们还可以变换吗?我们这里可能给出了一个很全面的随机效应,大家想一想它还能继续变化吗?我们可不可以拿掉其中一个?比如拿掉valence,随机效应里就变成1+identity,可不可以?也是可以的,至少是可以运行的。或者把identity拿掉只留valence,只认为不同被试在valence这个自变量上面的效应有差异,在identity上认为它是一模一样的,这也是可行的。或者认为它们两个主效应都在,但没有交互作用,这也是可行的。所以我们发现可以建好几个模型,这时候你需要想清楚到底哪个模型是makesense的,是合理的,因为有些模型可以凭经验发现是不合理的,那你就不应该用这个数据去建这个模型。在你把这个模型建起来之后,它在r里跑也可以给你结果,但这个结果对你来说可能是没有意义的,你需要用你的专家经验只是去筛选那些模型是合理的,是值得把它跑出来,去test的。 当你觉得这几个模型好像都差不多,需要通过数据本身来告诉我哪个模型是更加合适的,在这种情况下,我们可能需要借助一些模型比较的方法,可以直接对这些模型进行比较,哪些模型能够更好地拟合数据,这个模型更能捕捉到数据内部的一些特点,这就涉及到模型比较的问题,模型比较的问题基本上在所有的统计学中都会涉及到,当我们使用层级模型的时候我们可能也会用模型比较的方法去选择模型。在认知计算建模里我们也会用模型比较的方法去选择模型,在SEM,结构方程模型里也有很多模型选择的标准,如果大家仔细去看mplus或者spss或者mos里的输出的话,也是需要对不同模型进行比较的。 [ranI ranS] ## 随机截距 随机斜率 model_full <- lme4::lmer(data = mt_raw, RT ~ Identity * Valence + (1 + Identity * Valence|Sub)) [模型比较] ## 模型比较 stats::anova(model, model_full) %>% capture.output() ## refitting model(s) with ML (instead of REML) ## [1] "Data: mt_raw" ## [2] "Models:" ## [3] "model: RT ~ Identity * Valence + (1 | Sub)" ## [4] "model_full: RT ~ Identity * Valence + (1 + Identity * Valence | Sub)" ## [5] " npar AIC BIC logLik deviance Chisq Df Pr(>Chisq) " ## [6] "model 6 -29173 -29124 14592 -29185 " ## [7] "model_full 15 -30024 -29902 15027 -30054 869 9 <0.0000000000000002 ***" ## [8] "---" ## [9] "Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1" 注:模型比较的指标、计算方法及其优劣请参考《认知建模中模型比较的方法》 在我们这个层级模型里,我们这里简单展示用spaceANOVA的函数比较两个模型,我们刚刚建了两个模型,一个是基本的模型,只有一个random intercept,另外一个不仅有random intercept,还有random slope,随机的斜率,当然如果你自己在r里面看结果,直接用输前面这部分就可以了,我们这里是把它给打出来了,我们可以看到它会给一个输出。大家可以看到这个地方是它一系列模型比较的指标,这里就有两个模型,一个是model,一个是model_full,这个npar表示它的parameters的数量,我们可以看到简单的模型里面只有6个,full里面有16个参数,这个是AIC——一个模型比较的指标,BIC——一个模型的指标,这个是Log likelyhood,这个是deviance,还有对模型比较之间用卡方做的一个检验,然后发现两个模型拟合之间是有差异的,,如果按照卡方的标准发现它是显著的,这个时候我们可以推断有随机截距和随机斜率的模型对数据的拟合更好,那么接下来我们就会选择它作为解读的对象。这里有一篇preprint的文章,是两个年轻的博士生跟我一起写的,我是主要是跟他们学习,大家感兴趣可以去看一下。 在这里我们基本可以说在重复测量方差分析的基础上又做了一个层级模型,并且假定我们做了几个潜在可能的模型,都对它进行了拟合,然后再通过ANOVA进行了选择,选择之后要对模型的输出进行解读,去看感兴趣的自变量的效应在主水平是不是存在,在个体层面是不是有很大的变异,或者这个变异到底有多大,我们应该如何去解释这个变异。所以我们要去对这个结果进行一个解读,那么我们怎么去查看这个结果呢?这里肯定要涉及到模型比较的事项,加入我们要去看一下固定效应是不是显著的,那么我们怎么查看显著呢?我们可以通过刚才ANOVA的模型比较的方式,去对两个模型进行比较,一个模型是没有固定效应的,一个模型是由固定效应的,这两个模型之间的差异就是一个有固定效应一个没有,如果有固定效应的模型比没有固定效应的模型显著地更好,那它可能说明了这些固定的效应就应该放到这些模型里,我们可以认为它是显著的。所以我们需要借没有固定效应的模型作基线,然后把我们刚刚已经跑完的有固定效应也有随机效应的模型,和这个只有随机效应没有固定效应的模型进行比较,比较完发现有固定效应的模型确实更好,这基本上就可以帮助我们判断,我们加入进来的固定效应,就是identity和valence,以及它们之间的交互作用,肯定是有作用的. 在建立模型后,我们希望知道固定效应大小是否显著,但由于多层线性模型中对于自由度的估计有多种方法,比较复杂,所以lme4::lmer()中没有提供显著性。 [anova] # 建立没有固定效应的“空模型” model_null <- lme4::lmer(data = mt_raw, RT ~ (1 + Identity*Valence|Sub)) ## 根据似然比进行模型比较 stats::anova(model_full, model_null) ## refitting model(s) with ML (instead of REML) ## Data: mt_raw ## Models: ## model_null: RT ~ (1 + Identity * Valence | Sub) ## model_full: RT ~ Identity * Valence + (1 + Identity * Valence | Sub) ## npar AIC BIC logLik deviance Chisq Df Pr(>Chisq) ## model_null 12 -29992 -29894 15008 -30016 ## model_full 15 -30024 -29902 15027 -30054 38.1 3 0.000000026 *** ## --- ## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1 我们也可以用FX,FX这个包我们经常介绍它,我们也可以用它的mix函数来进行检验。我们比方说就直接用他的这个mix把这个数据输入进来,然后大家可以看到这个地方的回归方程和我们刚刚在lme4里看到的是一模一样的,也就是说我们在FX里建了一个多层模型,然后通过它里面LRT这个算法来进行检验,它输出的结果和我们传统的方差分析很像,它会给出主效应和交互作用,如果想查看固定效应,也可以用FX的mix这个函数来查看固定效应。 在模型非常复杂时(多层嵌套),如果仅仅只想对固定效应进行检验,可以使用afex::mixed(),设置method 参数为 LRT(Likelihood-ratio tests) afex::mixed(data = mt_raw, RT ~ Identity * Valence + (1 + Identity*Valence|Sub), method = 'LRT') ## Contrasts set to contr.sum for the following variables: Identity, Valence ## REML argument to lmer() set to FALSE for method = 'PB' or 'LRT' ## Fitting 4 (g)lmer() models: ## [....] ## Mixed Model Anova Table (Type 3 tests, LRT-method) ## ## Model: RT ~ Identity * Valence + (1 + Identity * Valence | Sub) ## Data: mt_raw ## Df full model: 15 ## Effect df Chisq p.value ## 1 Identity 1 1.73 .188 ## 2 Valence 1 30.27 *** <.001 ## 3 Identity:Valence 1 12.03 *** <.001 ## --- ## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '+' 0.1 ' ' 1 第三个方法是我们我们可以用叫做lmertest这个包,这个包实际上是跟lme4兼容的一个包,主要就是对混合效应模型,或者说层级模型进行假设检验,这里就包含了lmer这个函数,lmer和我们前面用到的lme4基本上名字都是一样的,就是不同的包,它的一个主要特点就是会报告显著性,它在这里使用它自己的一些算法。我们在用lmer这个函数的时候,这里的语法的输入跟我们在lme4里基本是一模一样的,然后我们可以通过summary来去看它的结果。我们第一个看到的是它随机效应的相关矩阵,这里主要关注不同的随机效应间有没有相关性,但绝大部分的时候我们可能不会太关注这些信息,然后我们可能更多的看的是两个,第一个就是把这个固定效应,就是类似两个自变量有没有主效应和交互效应,另外一个我们有时候也会专门去看变化的随机效应,这里我们可能没有展示。 lmerTest是一个与lme4包兼容的包,主要用于对混合效应模型进行假设检验;其中也包含了lmerTest::lmer()函数,与lme4::lmer()不同的是,其结果报告了显著性(使用Satterthwaite分母自由) lmer_model <- lmerTest::lmer(data = mt_raw, RT ~ Identity * Valence + (1 + Identity * Valence|Sub)) # summary(lmer_model) # 如果使用lmerTest包进行建模,可以使用bruceR::HLM_summary()进行输出 ## RUN IN CONSOLE # HLM_summary(lmer_model) [随机效应] 注意: - 相关矩阵会体现自变量效应在个体上的差异,尤其是第一列(截距与斜率的相关),而具体的解释也应考虑对应固定效应系数本身的正负; 有可能会提供天花板与地板效应的相关信息,如任务过于简单,数据变化较小,有可能出现截距与斜率为负相关。 ## [1] "Random effects:" ## [2] " Groups Name Variance Std.Dev. Corr " ## [3] " Sub (Intercept) 0.00425 0.0652 " ## [4] " IdentitySelf 0.00147 0.0383 -0.24 " ## [5] " Valencemoral 0.00219 0.0468 -0.22 0.49 " ## [6] " IdentitySelf:Valencemoral 0.00510 0.0714 -0.02 -0.53 -0.80" ## [7] " Residual 0.01801 0.1342 " ## [8] "Number of obs: 25920, groups: Sub, 44" [固定效应] ## [1] "Fixed effects:" ## [2] " Estimate Std. Error t value" ## [3] "(Intercept) 0.72328 0.00997 72.51" ## [4] "IdentitySelf 0.01329 0.00624 2.13" ## [5] "Valencemoral -0.00913 0.00744 -1.23" ## [6] "IdentitySelf:Valencemoral -0.04150 0.01128 -3.68" 比如说在这个模式里面,我们应该是可以把每一个被试在identity上的效应提取出来,看它在被试间是怎样一个变化,以及比方说valence的效应,或者它们之间的交互作用,我们也可以对它进行一个可视化,这个可视化是个非常简陋的可视化,这里全部采用的是默认值,比方说顺序就是他人和自我,道德也是moral和immoral,跟我们之前呈现的顺序不一样,在讲完了这个统计模型之后,我们会专门讲怎么在gPlot里面,对这种最后要呈现的结果进行精细的打磨。那么实际上在现在有很多包,包括像interaction这样的包,它能够帮助我们迅速地把一些我们关注的效应进行可视化,让我们看到它们之间是什么样的情况,比方说这里我们可以看到是一个比较明显的交互作用,在other的条件之下的immoral和moral的差别,肯定是要比在自我条件下的差别是要小的,所以在这里我们可以非常明显的看到交互作用的存在。 [交互效应的可视化] ## 一种快捷的方法 interactions::cat_plot(model = model_full, pred = Identity, modx = Valence) 我们前面讲不同模型之间是等价的,我们也说从重复测量方差分析是一个特定的混合陷阱模型,那么它们到底能不能做到完全等价,或者说几乎是相同的呢?大家觉得能做到吗?我们可以先看一看。我们这里直接把之前的重复测量方差分析的结果打印出来。 我们可以仔细看一下它的这个值,对Valence来说,效应是f值是64.37,p值是很小的,另外一个f值是14.36;然后还有identity它的f值是2.55,df都是143,这个跟我们在心理统计学上学到的重复测量方差分析是很像的。 [anova] m_aov ## Anova Table (Type 3 tests) ## ## Response: RT ## Effect df MSE F ges p.value ## 1 Identity 1, 43 0.00 2.55 .009 .117 ## 2 Valence 1, 43 0.00 64.37 *** .131 <.001 ## 3 Identity:Valence 1, 43 0.00 14.36 *** .037 <.001 ## --- ## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '+' 0.1 ' ' 1 我们可以看一下lme4,我们刚才建了好多个模型,然后我们选出最合适的一个,那么我们看一下最合适这个是不是跟我们的重复测量方式分析是一样的呢?我们直接看的话,用anova这个函数,用fullmodel把它的方差分析表打出来,我们就会看到它的f值,有三个f值,identity是0.74,跟我们的2.55不一样,第二个是45.00,跟我们的64.37也不要太一样,第三个也不太一样,但总体趋势差不多,总体趋势就是identity很小,valence比较大,它们的交互作用也比较大。 [lme4] ## 使用lme4建立的模型 model_full %>% anova() ## Analysis of Variance Table ## npar Sum Sq Mean Sq F value ## Identity 1 0.013 0.013 0.74 ## Valence 1 0.811 0.811 45.00 ## Identity:Valence 1 0.244 0.244 13.54 我们再看一下lmertest的fullmodel,因为它是给出显著性的,所以我们可以看看它会不会得到类似结果,可以看到跟lme4得到类似的结果,但是模式也都是一样的,identity比较小,valence比较大,identity和valence的交互作用处于中间,下面两个都是显著的,所以我们发现重复测量的方差分析和常规的层级模型,其实好像不太一样。 [lmerTest] ## 使用lmerTest建立的模型 lmer_model %>% anova() ## Type III Analysis of Variance Table with Satterthwaite's method ## Sum Sq Mean Sq NumDF DenDF F value Pr(>F) ## Identity 0.031 0.031 1 43.0 1.73 0.19575 ## Valence 0.765 0.765 1 43.3 42.46 0.000000063 *** ## Identity:Valence 0.244 0.244 1 43.0 13.54 0.00065 *** ## --- ## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1 其实这个刚开始我也是比较困惑的,也找了很多解决方案,后来发现它需要以一种很特定的方式来建构层级模型,才能达到跟它很一致的结果。 因为我们今年一直采用matching_data的反应时的数据,我们用它来跟前面进行对比,这里大家可以看到我们用的是mt_mean,我们在数据上做到了一样,前面那里还有个问题,就是它们的数据是不太一样的,比如这里的mt_mean,它用的是一个原始的数据应不应该在这里使用?因为我们这个数据量很大,所以有一些没有清理的数据不影响整体的趋势但假如你自己做数据分析,数据预处理这里一定是要再考虑考虑的。当然这里我们也试过了,用一模一样的数据,也是用mt_mean来做层级模型,它得到的结果也也是不一样的,跟这里是同样的一个趋势。那么假如我们采用mt_mean这种方式去去建立这个模型,它最有可能得到跟重复测量方差分析一样的结果了,这个模型是比较特别的,首先它有identity和valence的主效应,然后它有被试间的random intercept,每一个被试在每一个identity下面会有一个独特的intercept,同样对每一个被试来说,在每一个valence上也有一个独特的random intercept。在这种情况之下,我们得到的结果是最接近的,是59.89,接近60,这个是15.36,跟64.37和14.36是最接近的,但还是没有达到一模一样,还有个问题大家可以看他的这个df,它的自由度,第一个自由度是一模一样的,所以它这里达到了一样的值,143和2.55,这个地方也是2.55,但后面两个自由度和F值不一样,如果大家有兴趣仔细去扒为什么出现这个问题,实际上是因为使用这两个不同函数的人是有不同的习惯的,或者说有不同的重视的点,对于做传统的重复测量方差分析的人来说,很重视方差的分解,然后如何去求f值,这方面会做的很精细,不同的包之间也是能达到精确的一致性的,对于绝大多数做lmer的人来说,他都是从线性模型的角度,他不会在把这个模型做完之后,再去做一个方差分析,然后看它跟重复测量方差分析是不是一模一样的结果。 # http://www.dwoll.de/rexrepos/posts/anovaMixed.html#two-way-repeated-measures-anova-rbf-pq-design model_aov <- mt_mean %>% # dplyr::filter(!is.na(RT) & Match == "match" & ACC == 1) %>% lmerTest::lmer( data = ., RT ~ Identity * Valence + (1|Sub) + (1|Identity:Sub) + (1|Valence:Sub) ) ## boundary (singular) fit: see help('isSingular') model_aov %>% anova() ## Type III Analysis of Variance Table with Satterthwaite's method ## Sum Sq Mean Sq NumDF DenDF F value Pr(>F) ## Identity 0.0055 0.0055 1 43 2.55 0.11735 ## Valence 0.1282 0.1282 1 86 59.89 0.000000000018 *** ## Identity:Valence 0.0329 0.0329 1 86 15.36 0.00018 *** ## --- ## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1 9.5 HLM的应用 在这种情况之下,我们就发现,它相对这个借助的计算的方法,是没有优化的,也就是说现在这个lmer这个包,或者lme4,它更多的是去建线性的模型,而不是说如何把线性模型的结果转化为传统意义上的方差分析。这个模型是本身比较复杂的,所以如果你要让这个模型的转换,得到一模一样的数值上的结果,其实需要做很多软件工程的工作了,你要写很多代码,然后把各种条件加上判断,然后就达到一致的结果。从我目前找到的资料看,很少用lme4这一类模型的开发者会在意这个问题,就是说一定要把它转化为跟传统的方差分析一模一样,所以这样的代码没有人去写。这个跟我们去年上课的时候的结果也不太一样,去年上课的时候我们写了这个代码之后,其实能够得到跟重复测量方差分析基本上一样的结果,只在小数点后面有差别,这里还有一个可能 就是说我们的这个模型本身,可能会有一些warnings,会有一些警告,如果大家跑自己在电脑跑的时候,会有一些warnings的,有个叫做similarity的一个问题,这里我就不展开了,这是属于模型最后拟合求解的时候的一个问题。也就是说因为我们这个数据存在这个问题,所以最后我们直接用这个方法的时候,它可能就没有办法得到跟重复测量方差分析在数字上一模一样的结果。原则上,这个公式应该是跟我们重复测量方差分析是一样的,但数字上面确实可能会出现不一样的结果, 在这里我稍微总结一下,借这个公式,也就是说我们在心理统计学上学到的常用统计方法,包括我们上节课讲到的ttest和方差分析,它是线性模型的一个特例,那么我们传统意义上用到的重复测量方差分析,它也是另外一个特例,它就是一个嵌套结构的,层级模型模型的一个特例,那么在你的数据和你求解的方法,各方面比较理想的状况下的话,应该是能够得到一模一样的结果的。 我们在这里看这个lme4的结果的时候,它是比较丰富的,大家也可以找一些更加最先进开发的包,去看能不能更好的帮助我们查看这个模型的结果。为什么呢?因为这个模型的结果建出来之后,我们这里看到这个model_full,这个文件本身是很大的,里面包含了很多的信息,肯定有很多信息是值得去提取出来的,我们这里讲到的只是最简单的,一个是fixeffect,如何把它里面固定的项提取出来,但我们在前面讲到了,随机的效应其实有的时候很关键,它也是可以被提取出来的,我们这里没有细讲,主要是时间不太够。另外一个需要说明的是,层级模型是一个很复杂的大的框架,我们仅仅是以重复测量方差分析为例,以我们常用的反应时间,这种非常特定的嵌套结构的数据来介绍了层级模型,但是其实看到的很多数据分析都是在这个框架之下的,它可以适合很多这种具有嵌套和层级结构的数据的,这里举了几个嵌套结构的例子。 元分析其实也是一个特例,元分析大家可能听说过,你看某一个领域的文献的时候,你肯定会想这里面有没有元分析告诉我,这个效应它的效应总体上是多大,实际上它本质上也是一个层级模型,那么它这个层级模型,比方说最高的就是我们总体的效应,然后就是不同的研究、不同的实验,甚至可以再给加1层,比方说不同的课题组或者不同的文章,不同文章里面有不同的实验,不同的实验项目有不同条件,所以它也是这么一层一层一层的,嵌套下的。当我们做元分析的时候,其实是建了一个特定的层级模型,我们用的是这种描述性的统计数据,而没有用原始数据,有原始数据的时候,其实我们可以把所有实验室的原始数据拿到一起,直接用一个大的层级模型,把它进行一个效应量的综合,这也是可以的。这个是比方说大家在看元分析时经常会看到一个图,这个森林图不同的效应从小到大进行排列,最后会出现一个什么样的结果,然后针对原本性的这个结果,检查他的异质性等等各方面的,还有做元回归,很多很多这样的一些内设统计方法。但它本质上讲,也就是我们今天讲的层级模型里面一个非常特殊的例子。 ## 可能遇到的问题 模型不收敛:混合线性模型的实现 自由度问题:多层线性模型(HLM)及其自由度问题 统计学中的固定效应 vs. 随机效应 「固定效应、主效应、简单效应」概念辨析 我们在使用层级模型的时候,可能会碰到很多问题,我们今天讲的是一个非常简单的一个入门,但是我们这个入门也基本上回应了前面我们将为什么要学习r语言的时候,说当我们学习r语言之后,就可以开始使用这些比较复杂的模型,它会引出我们更多值得学习的东西。同样如此,对于这个层级模型来说,当我们开始使用层级模型之后,我们就会碰到很多这种可能存在的问题,这些问题就会让我们进行进一步的去学习层级模型里面的一些技术细节,比方说模型收敛的问题,有可能你的模型很复杂,最后发现它不给你出任何结果,这个时候可能就是存在收敛的问题。然后还有自由度的问题,我们刚才其实已经碰到自由度的问题,当我们试图把层级模型和重负测量方差分析进行完全对应的时候,它的自由度的计算就会有一些技术细节的问题。还有我们讲了统计的固定效应和随机效应还有包括像是固定效应、主效应、简单效应,后面这两个都包寒吴霜老师在知乎上的帖子里,大家可以去看看,这也都是非常值得去关注的。大家如果做发展心理学,有一些比方说交叉滞后模型等等,交叉滞后也是我们碰到的层级模型的一个特例。 所以今天我们只是给大家开了个头,希望大家能够感受到大的统计框架的魅力。但是我们还有一个问题,就是大家可以想一想,我们今天其实是在刻意的,回避了一些数据,就像我们上节课的时候刻意回避了重复测量方差分析,引出了这节课内容,那么我们今天这个思考是要引出下一节课内容,就是当我们这个数据它不服从正态分布,你都没办法去假设他是服从正态分布的时候,怎么办?比方说我们碰到这个正确率的数据,在这个实验当中假如我们考虑每个试次,其实就是0和1,要么就是错了0,要么就对了1,大家可以想想我们以前是用什么样的统计方法来去对这个证据进行检验,它肯定是很确定的不服从正态分布的情况,这种情况之下,比方说有很多的试次,我们最后算出了它的平均的正确率,之后我们把它做t检验或者方差分析,也可以接受,那么有没有更好的办法,或者有没有更加适合的办法,这个可能就是我们下节课要讲的内容。 9.6 思考 当因变量不服从正态分布(如ACC)时如何处理? 回归模型2,今天我们就讲到这里,希望给大家开启了一个新的知识的一个窗口,谢谢大家。 "],["第十讲回归模型三广义线性模型.html", "Chapter 10 第十讲:回归模型(三):广义线性模型 10.1 前章回顾和本章数据预处理 10.2 广义线性模型 10.3 二项分布 10.4 不同方法比较 10.5 其他分布", " Chapter 10 第十讲:回归模型(三):广义线性模型 10.1 前章回顾和本章数据预处理 上节课我们讲解了如何把T检验和方差分析用回归模型的方式来进行解读。接着我们从认知心理学最常见的反应时数据出发,从层级模型的角度将数据分解为整体的水平(group level),以及每个被试个体试次的水平(trial level),层级模型可以提高统计检验力并且为研究者提供十分丰富的信息,因此也是学界越来越推荐的一种统计方法。 让我们先对本章数据进行预处理,这里用到的还是之前的认知实验的数据,我们先对正确率进行预处理。 这里需要注意一下我们对于正确率ACC的处理,实际上我们的认知实验数据当中包含了其他两种反应,这里我们直接删除了另外两种情况,只保留了正确和错误的反应,即0和1,当然也有一些实验会将1之外的所有反应归到0中做处理。另外我们筛选去除了反应在1500ms以上和200ms以下的反应时,因为这在经验上是不符合人类的反应速度的。 ## `summarise()` has grouped output by 'Sub', 'Valence'. You can override using the `.groups` ## argument. df.match.aov %>% dplyr::select(1:4) %>% head(5) %>% DT::datatable() 上面是我们通常所做的操作:将不同条件下的反应正确率做一个平均,然后进行方差分析。 (知识补充:easystats系统包是过去五六年快速发展起来的一个包系列,适用于统计分析,特别是心理学相关背景的统计分析,具体使用可以参考我们在B站上传的视频(链接如下)。https://www.bilibili.com/video/BV1rz421D7iJ/?spm_id_from=333.337.search-card.all.click) 让我们再来看一下正确率的原始数据。 head(df.match[c(3,11:17)],5) %>% DT::datatable() 可以发现其只存在0和1两种取值,这种分布显然不服从正态分布,因此我们不能简单地用前一章提到的一般线性模型进行处理。在传统的方差分析中,我们对正确率数据的处理是求出每个条件下的平均正确率再进行统计分析。这个平均正确率的取值作为一个连续数据,可以被放在坐标轴上形成一个分布,其与以0为原点,向两端无限延伸的标准正态分布也存在差异。因此我们需要对一般线性模型进行拓展,这也就是我们这一章所要讲的广义线性模型(Generalized Linear Model, GLM)。 10.2 广义线性模型 10.2.1 回归方程和普通线性模型 下面是线性模型的一个基本的形式:首先可以看到一个截距b0;假设我们有p个自变量,每一个自变量都会有一个它的斜率b;最后的残差\\(\\epsilon\\)是无法被这个回归解释的一个部分。 \\[Y = b_0 + b_{1}X_{1} + b_{2}X_{2} +... + b_{p}X_{p} + \\epsilon\\] -\\(Y\\): 因变量,Dependent variable - \\(X_i\\) : 自变量,Independent (explanatory) variable - \\(b_0\\) : 截距,Intercept - \\(b_i\\) : 斜率,Slope - \\(\\epsilon\\) : 残差,Residual (error) 当然我们也可以用更加一般化的方法来书写上面的线性回归方程,以下这个方程包含了依然一个截距,并且我们将所有自变量用求和符号相加,以及最后相应残差项。方程右边除去残差项的内容,就是该方程的预测项,左边的y则是对应的观测项。 当我们在x轴上选择一个具体取值的时候,y轴上对应的是一系列可能的y值,这些y值组成了每一个x值下对应的y的正态分布,这个正态分布的中心就是对应x的观测值。 另外还有一些其他的回归方程写法,贴在下方供读者参考。 -简单线性回归: \\[Y = b_0+b_1 X_1+ b_2 X_2+…+b_p X_p + \\epsilon\\] -线性代数表达:\\[y_i = b_0 + b_1 X_{i1} + b_2 X_{i2} + … + b_p X_{ip} + \\epsilon\\] -矩阵表达: \\[Y= X\\beta + \\epsilon\\] -代码表达(r):\\[Y \\sim X_1 + X_2 + ... + X_n\\] 我们也可以用一种更为简单的形式来写回归公式,因为我们观测到的y实际上是一个分布,所以我们用”~“来表示分布的含义。分布y中包含了预测项\\(\\mu\\)和误差项\\(\\epsilon\\)这两个参数。因此如果从数据分布的角度来看,线性回归实际上是在根据预测项推导出相应的分布,这也是为什么我们在进行线性回归的时候要假定数据呈正态分布,因为只有这样观测项才等于这个分布中数据的均值。 回归模型形式:观测项 = 预测项 + 误差项 假定观测项是正态分布,上述公式可以重新表达为: \\[y \\sim N(\\mu, \\epsilon)\\] 其中,\\(\\mu\\)为预测值,即 \\[μ = \\beta_0 + \\beta_1 x\\] 观测值服从以预测项为均值的正态分布,观测值与预测值之间的差值就是残差。 10.2.2 连接函数和广义线性模型 那么如果因变量不服从正态分布,如何构建回归模型? 当x无法直接去预测y的时候,我们就要通过一个连接函数,将x对应的线性组合值z,映射到q上,然后再将q作为一个预测值去预测y的分布。 具体而言,连接函数的作用就是将原本不能用于预测y的z转换为可以预测的值q。 这里我们就可以看到之前所讲的简单线性模型的非常特殊的点:简单线性模型可视为GLM的特殊形式,预测项的连接函数等于它本身(即不需要使用连接函数进行转换,写代码是有时候会写的”identity”就是不需要转换的意思),观测项为正态分布。 而在广义线性模型中:观测项不一定是正态分布(残差不一定是正态分布),连接函数不等于其自身,这也使得广义线性模型能够对非正态分布的因变量进行建模。 10.3 二项分布 10.3.1 伯努利实验 我们前面提到了正确率这种数据,其可能存在的取值只有0和1。事实上我们在抛硬币的游戏中也经常也涉及到类似的情况,也就是抛硬币只存在正面或者反面朝上两种情况。用统计学上的书面话语来讲,就叫”伯努利实验”。伯努利实验是一种在同样的条件下重复地、相互独立地进行的随机试验;该随机试验只有两种可能结果:发生或者不发生。假设该项试验独立重复地进行了n次,那么就称这一系列重复独立的随机试验为n重伯努利试验(n-fold bernoulli trials)。 而n次独立重复的伯努利试验的概率分布服从二项分布(Binomial Distribution),二项分布的公式如下。 \\[P(X=k )=𝐶_𝑛^𝑘 𝑝^𝑘 𝑞^{𝑛−𝑘}= 𝐶_𝑛^𝑘 𝑝^𝑘 (1−𝑝)^{𝑛−𝑘}\\] \\[𝐶_𝑛^𝑘= 𝑛!/𝑘!(𝑛−𝑘)! \\] 其中,p表示每次试验中事件A发生的概率;X表示n重伯努利试验中事件A发生的次数,X的可能取值为0,1,…,n;对每一个k(0 ≤ k ≤ n),事件{X = k} 指”n次试验中事件A恰好发生k次”;随机变量X服从以n, p为参数的二项分布,写作 \\(X \\sim B(n, p)\\),\\(p \\in [0,1]\\),\\(n \\in N\\) 还是以抛硬币来举例,假如我们抛十次硬币,十次全部正面朝上的概率就是二分之一的十次方,我们用到的两个参数就是正面朝上的概率以及抛的次数,如果我们只知道抛了几次,或者只知道正面朝上的概率,都不足以算出具体的概率。因此对于二项分布,我们要知道的两个参数分别为实验次数n,以及事件发生的概率p。 我们可以在R中来模拟一下抛硬币的实验。首先我们先写一个模拟抛硬币的函数。 我们让若干人每人抛若干次硬币,首先让5个人每人抛10次硬币。理论上来说,对于一枚公平的硬币,正面朝上次数为5的应该是最多的。 simulate_coin_toss(prob_head = 0.5,num_people = 5, num_tosses = 10) 但是事实上我们模拟出来发现小于5的反而更多一点,这好像在暗示我们这是一枚不公平的硬币。但是如果我们把人数n增多呢?以下模拟的是10人每人抛10次。 simulate_coin_toss(prob_head = 0.5,num_people = 10, num_tosses = 10) emmmm,似乎还是一枚不太公平的硬币。我们再将人数n增加到1000人。 simulate_coin_toss(prob_head = 0.5,num_people = 1000, num_tosses = 10) 这时候我们就可以看到数据呈现一个非常好的正态分布,此时正面朝上次数为5次的确实是最多的。 这里主要想让大家直观了解一下参数n对于事件发生次数分布的影响。让我们总结一下,已知一次试验中的每次尝试中事件A发生的概率\\(p\\),共进行\\(n\\)次独立重复的伯努利试验,假如事件A在一次试验中出现k次,事件A在n次试验中出现次数的平均数为: \\[(𝑘_1+𝑘_2+𝑘_3+...+𝑘_𝑛/𝑛)\\] 当n → ∞,\\(p\\) ≠ \\(q\\),\\(np\\) ≥ 5且\\(nq\\) ≥ 5,事件A在\\(n\\)次试验中出现次数的平均数为: \\[\\mu = np\\] 事件A出现次数所属分布的标准差: \\[ \\sigma = \\sqrt{𝑛𝑝𝑞}\\] 也就是说,当n趋于无穷的时候,事件发生次数的分布就会慢慢地逼近正态分布,它会存在均值np,我们也可以算出它相应的标准差。 假如我们想用大五人格分数来预测正确率,我们会先将分数标准化为\\(z\\),那么如何将\\(z\\)与正确率这一二分变量进行连接呢?我们需要先将\\(z\\)映射到(0,1)之间再作为预测项,例如使用如下转换函数: \\[\\frac{1}{1+exp(-z)}\\] 接着我们需要找到一个分布,能根据(0,1)之间的值转成二分变量,例如伯努利分布。 我们再次回到开头讲的线性模型的三部分,第一部分是将自变量通过线性组合得到z值,第二部分我们使用连接函数将连续的数值z映射到p的空间内,第三部分就是用p所属的一个分布去预测因变量。 这里会涉及到参数求解的问题,对于logit回归,我们可以使用极大似然估计对其进行求解,该求解过程比较复杂,一般由计算机自动完成,我们绝大部分都不需要了解。 10.3.2 GLM代码实操 虽然我们前面提到了非常复杂和抽象的数学运算,但是在R中我们可以用非常简单的几行代码来实现广义线性模型。在公式部分和前一章是一致的,1是截距,后面放上实验的两个自变量,因变量是正确率ACC。我们所做的改动主要是在前面将函数改成了glm,并且加上了family这个参数,让其等于binomial。 这里我们可以明显地看到当我们使用广义线性模型之后,相较于传统的方差分析,我们可以对单个被试进行数据分析,这也意味着我们依然可以像前一章那样建立层级模型。以下是对单个被试进行glm建模的代码。 df.match.7304 <- df.match %>% dplyr::filter(Sub == 7304) #选择被试7304 mod_7304_full <- stats::glm(data = df.match.7304, #数据 formula = ACC ~ 1 + Identity * Valence, #模型 family = binomial) #因变量为二项分布 summary(mod_7304_full) %>% #查看模型信息 capture.output() %>% .[c(6:11,15:19)] #课堂展示重要结果 ## [1] "Coefficients:" ## [2] " Estimate Std. Error z value Pr(>|z|) " ## [3] "(Intercept) 4.30 1.01 4.28 0.000019 ***" ## [4] "IdentityOther -2.69 1.05 -2.56 0.0106 * " ## [5] "Valenceimmoral -2.77 1.05 -2.64 0.0083 ** " ## [6] "IdentityOther:Valenceimmoral 2.10 1.13 1.86 0.0628 . " ## [7] "(Dispersion parameter for binomial family taken to be 1)" ## [8] "" ## [9] " Null deviance: 254.02 on 290 degrees of freedom" ## [10] "Residual deviance: 228.32 on 287 degrees of freedom" ## [11] "AIC: 236.3" 建立层级模型意味着我们需要对总体和单个被试的效应都进行比较,通常我们会建立多个模型,然后对不同的模型进行比较。首先我们建立一个只有被试随机效应而没有群体固定效应的模型。 #无固定效应 mod_null <- lme4::glmer(data = df.match, #数据 formula = ACC ~ (1 + Identity * Valence|Sub), #模型 family = binomial) #因变量二项分布 #performance::model_performance(mod_null) summary(mod_null) %>% capture.output()%>% .[c(7:8,14:24)] ## [1] " 9378.8 9460.2 -4678.4 9356.8 11999 " ## [2] "" ## [3] " Groups Name Variance Std.Dev. Corr " ## [4] " Sub (Intercept) 1.53 1.24 " ## [5] " IdentityOther 2.52 1.59 -0.86 " ## [6] " Valenceimmoral 2.40 1.55 -0.85 0.83 " ## [7] " IdentityOther:Valenceimmoral 3.33 1.83 0.69 -0.87 -0.82" ## [8] "Number of obs: 12010, groups: Sub, 41" ## [9] "" ## [10] "Fixed effects:" ## [11] " Estimate Std. Error z value Pr(>|z|) " ## [12] "(Intercept) 2.014 0.114 17.7 <0.0000000000000002 ***" ## [13] "---" 接着我们建立一个随机效应只包含截距的模型。 #随机截距,固定斜率 mod <- lme4::glmer(data = df.match, #数据 formula = ACC ~ 1 + Identity * Valence + (1|Sub), #模型 family = binomial) #因变量二项分布 #performance::model_performance(mod) summary(mod) %>% capture.output() %>% .[c(7:8,14:24,28:32)] ## [1] " 9639.0 9675.9 -4814.5 9629.0 12005 " ## [2] "" ## [3] " Groups Name Variance Std.Dev." ## [4] " Sub (Intercept) 0.237 0.487 " ## [5] "Number of obs: 12010, groups: Sub, 41" ## [6] "" ## [7] "Fixed effects:" ## [8] " Estimate Std. Error z value Pr(>|z|) " ## [9] "(Intercept) 2.4964 0.1015 24.60 < 0.0000000000000002 ***" ## [10] "IdentityOther -0.7160 0.0839 -8.53 < 0.0000000000000002 ***" ## [11] "Valenceimmoral -0.9474 0.0818 -11.58 < 0.0000000000000002 ***" ## [12] "IdentityOther:Valenceimmoral 0.8230 0.1086 7.58 0.000000000000034 ***" ## [13] "---" ## [14] " (Intr) IdnttO Vlncmm" ## [15] "IdenttyOthr -0.519 " ## [16] "Valencemmrl -0.533 0.641 " ## [17] "IdnttyOth:V 0.401 -0.773 -0.754" ## [18] NA 最后我们建立一个包含了所有固定效应和随机效应的全模型。 #随机截距,随机斜率 mod_full <- lme4::glmer(data = df.match, #数据 formula = ACC ~ 1 + Identity * Valence + (1 + Identity * Valence|Sub), #模型 family = binomial) #因变量二项分布 ##performance::model_performance(mod_full) summary(mod_full) %>% capture.output() %>% .[c(6:8,13:18,21:26,30:34)] ## [1] " AIC BIC logLik deviance df.resid " ## [2] " 9355.9 9459.4 -4664.0 9327.9 11996 " ## [3] "" ## [4] "Random effects:" ## [5] " Groups Name Variance Std.Dev. Corr " ## [6] " Sub (Intercept) 0.972 0.986 " ## [7] " IdentityOther 1.771 1.331 -0.79 " ## [8] " Valenceimmoral 1.028 1.014 -0.75 0.75 " ## [9] " IdentityOther:Valenceimmoral 2.306 1.518 0.54 -0.82 -0.74" ## [10] "Fixed effects:" ## [11] " Estimate Std. Error z value Pr(>|z|) " ## [12] "(Intercept) 2.772 0.178 15.53 < 0.0000000000000002 ***" ## [13] "IdentityOther -0.870 0.235 -3.71 0.00021 ***" ## [14] "Valenceimmoral -1.150 0.190 -6.06 0.0000000013 ***" ## [15] "IdentityOther:Valenceimmoral 0.988 0.272 3.63 0.00028 ***" ## [16] "Correlation of Fixed Effects:" ## [17] " (Intr) IdnttO Vlncmm" ## [18] "IdenttyOthr -0.801 " ## [19] "Valencemmrl -0.783 0.741 " ## [20] "IdnttyOth:V 0.573 -0.821 -0.747" 我们在运行全模型的时候可以明显感受到运行时间相较于之前变长了。当我们的模型内参数越多,模型越复杂的时候,计算机就需要花更多时间去拟合模型,也会有些时候因为找不到合适的参数而导致模型无法拟合。习惯了SPSS的读者可能会难以忍受,但实际上我们在后面处理一些大数据或者跑机器学习的时候,等待会是一件很常见的事情。这就提示我们合理分配时间,把要运行的代码提前运行起来,然后去做别的工作。 在这里,我们也可以根据结果来判断R语言对我们自变量的编码方式,可以看到结果中除了截距外的第一项为”Identityother”,由此我们可以判断R将”Identityself”编码为了基线,并据此来计算相应的回归系数和估计值,下面的其他结果也类似。 接下来我们对上述三个模型进行比较。 stats::anova(mod_null, mod, mod_full) #比较三个模型 ## Data: df.match ## Models: ## mod: ACC ~ 1 + Identity * Valence + (1 | Sub) ## mod_null: ACC ~ (1 + Identity * Valence | Sub) ## mod_full: ACC ~ 1 + Identity * Valence + (1 + Identity * Valence | Sub) ## npar AIC BIC logLik deviance Chisq Df Pr(>Chisq) ## mod 5 9639 9676 -4814 9629 ## mod_null 11 9379 9460 -4678 9357 272.1 6 < 0.0000000000000002 *** ## mod_full 14 9356 9459 -4664 9328 28.9 3 0.0000023 *** ## --- ## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1 可以发现全模型(mod_full)的效果是最好的,但当我们更换一种模型比较方法的时候,可能会得到不一样的结论,如下。 performance::compare_performance(mod_null, mod, mod_full, rank = TRUE, verbose = FALSE) ## Some of the nested models seem to be identical and probably only vary in their random ## effects. 让我们输出全模型的结果,尝试进行解读。 summary(mod_full) %>% capture.output() %>% .[c(21:27)] ## [1] "Fixed effects:" ## [2] " Estimate Std. Error z value Pr(>|z|) " ## [3] "(Intercept) 2.772 0.178 15.53 < 0.0000000000000002 ***" ## [4] "IdentityOther -0.870 0.235 -3.71 0.00021 ***" ## [5] "Valenceimmoral -1.150 0.190 -6.06 0.0000000013 ***" ## [6] "IdentityOther:Valenceimmoral 0.988 0.272 3.63 0.00028 ***" ## [7] "---" 在结果里面显示的估计值,并不直接等于p值,我们要根据前面转换函数的逆运算来讲其转换为p值,转换公式如下。 代入之后我们便可以求出不同实验条件下各自的p值。 MoralSelf: \\(P=\\frac{e^{2.73}}{1+e^{2.73}} = 0.939\\) ImmoralSelf: \\(P=\\frac{e^{2.73-1.10 }}{1+e^{2.73-1.10}} = 0.836\\) MoralOther: \\(P=\\frac{e^{2.73-0.76 }}{1+e^{2.73-0.76 }} = 0.878\\) ImmoralOther: \\(P=\\frac{e^{2.73-0.76-1.10+0.89}}{1+e^{2.73-0.76-1.10+0.89}} = 0.853\\) 我们可以使用cat_plot()函数将模型预测的结果快速地展示出来。 #交互作用 interactions::cat_plot(model = mod_full, pred = Identity, modx = Valence) 这里可以看到很明显的交互作用,也就是当我们把不同颜色的柱子连线就会发现二者的交叉。 10.4 不同方法比较 10.4.1 不同的建模方法 接下来我们对正确率不同的分析方法做一个比较。首先是传统的方差分析,方差分析实际上就是一个线性模型。以下是对正确率进行方差分析的代码。 res <- bruceR::MANOVA(data = df.match.aov, #数据 subID = 'Sub', # 被试编号 dv= 'mean_ACC', # 因变量 within = c('Identity', 'Valence')) #自变量(被试内) ## ## * Data are aggregated to mean (across items/trials) ## if there are >=2 observations per subject and cell. ## You may use Linear Mixed Model to analyze the data, ## e.g., with subjects and items as level-2 clusters. capture.output(res) %>% .[3:8] ## [1] "Response: mean_ACC" ## [2] " Effect df MSE F ges p.value" ## [3] "1 Identity 1, 40 0.01 3.08 + .017 .087" ## [4] "2 Valence 1, 40 0.01 16.26 *** .068 <.001" ## [5] "3 Identity:Valence 1, 40 0.01 8.52 ** .038 .006" ## [6] "---" 我们可以得到一个f值,并且因为研究的被试量比较大,我们可以发现这里呈现的主效应以及交互作用,和后面用glm或者层级模型做出来的结果有一样的趋势。我们可以用EMMAMNS()函数来查看模型的一些具体值。 res %>% bruceR::EMMEANS(effect = 'Valence', by = 'Identity') %>% capture.output() ## [1] "------ EMMEANS (effect = \\"Valence\\") ------" ## [2] "" ## [3] "Joint Tests of \\"Valence\\":" ## [4] "────────────────────────────────────────────────────────────────" ## [5] " Effect \\"Identity\\" df1 df2 F p η²p [90% CI of η²p]" ## [6] "────────────────────────────────────────────────────────────────" ## [7] " Valence Self 1 40 35.614 <.001 *** .471 [.282, .610]" ## [8] " Valence Other 1 40 0.412 .525 .010 [.000, .114]" ## [9] "────────────────────────────────────────────────────────────────" ## [10] "Note. Simple effects of repeated measures with 3 or more levels" ## [11] "are different from the results obtained with SPSS MANOVA syntax." ## [12] "" ## [13] "Estimated Marginal Means of \\"Valence\\":" ## [14] "───────────────────────────────────────────────────" ## [15] " \\"Valence\\" \\"Identity\\" Mean [95% CI of Mean] S.E." ## [16] "───────────────────────────────────────────────────" ## [17] " moral Self 0.916 [0.885, 0.947] (0.015)" ## [18] " immoral Self 0.814 [0.776, 0.852] (0.019)" ## [19] " moral Other 0.844 [0.809, 0.879] (0.017)" ## [20] " immoral Other 0.829 [0.794, 0.864] (0.017)" ## [21] "───────────────────────────────────────────────────" ## [22] "" ## [23] "Pairwise Comparisons of \\"Valence\\":" ## [24] "────────────────────────────────────────────────────────────────────────────────────────" ## [25] " Contrast \\"Identity\\" Estimate S.E. df t p Cohen’s d [95% CI of d]" ## [26] "────────────────────────────────────────────────────────────────────────────────────────" ## [27] " immoral - moral Self -0.102 (0.017) 40 -5.968 <.001 *** -0.736 [-0.985, -0.487]" ## [28] " immoral - moral Other -0.015 (0.024) 40 -0.642 .525 -0.111 [-0.459, 0.238]" ## [29] "────────────────────────────────────────────────────────────────────────────────────────" ## [30] "Pooled SD for computing Cohen’s d: 0.139" ## [31] "No need to adjust p values." ## [32] "" ## [33] "Disclaimer:" ## [34] "By default, pooled SD is Root Mean Square Error (RMSE)." ## [35] "There is much disagreement on how to compute Cohen’s d." ## [36] "You are completely responsible for setting `sd.pooled`." ## [37] "You might also use `effectsize::t_to_d()` to compute d." ## [38] "" 接下来是包括所有固定效应和随机效应的全模型的结果。 stats::anova(mod_full) ## Analysis of Variance Table ## npar Sum Sq Mean Sq F value ## Identity 1 0.2 0.2 0.2 ## Valence 1 26.8 26.8 26.8 ## Identity:Valence 1 13.6 13.6 13.6 下面我们在求出每个被试的正确率之后,将其当作层级模型来进行处理的结果,和上面其他模型得到的也比较类似。 mod_anova <- lme4::lmer(data = df.match, formula = ACC ~ 1 + Identity * Valence + (1 + Identity * Valence|Sub)) stats::anova(mod_anova) ## Analysis of Variance Table ## npar Sum Sq Mean Sq F value ## Identity 1 0.42 0.42 3.71 ## Valence 1 3.17 3.17 27.69 ## Identity:Valence 1 0.98 0.98 8.54 我们还可以用线性模型来做出类似于方差分析的结果,但由于二者算法并不完全相同,因此结果也存在一些细微的差异。 mod_mean <- lme4::lmer(data = df.match.aov, formula = mean_ACC ~ 1 + Identity * Valence + (1|Sub) + (1|Identity:Sub) + (1|Valence:Sub)) stats::anova(mod_mean) ## Analysis of Variance Table ## npar Sum Sq Mean Sq F value ## Identity 1 0.0272 0.0272 3.08 ## Valence 1 0.1410 0.1410 15.93 ## Identity:Valence 1 0.0769 0.0769 8.69 10.4.2 不同的模型比较方法 在建立了上述所有模型之后,我们想要知道的是,层级模型是否就是一个最好的模型呢?我们可以用默认的模型比较方法对上述模型进行比较。 performance::compare_performance(mod_full, mod_anova, rank = TRUE, verbose = FALSE) performance()结果会把比较好的模型排在上面,总体而言anova模型更好。不过我们可以看到一些参数,R2表示模型解释的变异,ICC反映的是个体变异的内容,这两个参数都是层级模型更优。我们再用anova()对模型做一次比较。 stats::anova(mod_full, mod_anova) ## refitting model(s) with ML (instead of REML) ## Data: df.match ## Models: ## mod_full: ACC ~ 1 + Identity * Valence + (1 + Identity * Valence | Sub) ## mod_anova: ACC ~ 1 + Identity * Valence + (1 + Identity * Valence | Sub) ## npar AIC BIC logLik deviance Chisq Df Pr(>Chisq) ## mod_full 14 9356 9459 -4664 9328 ## mod_anova 15 8396 8507 -4183 8366 962 1 <0.0000000000000002 *** ## --- ## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1 在结果的参数中,AIC的值一般是越小越好,我们会发现anova的模型反而是更好的。总体而言,两种方法似乎都提示我们传统的方差分析是更优的。 接着我们还用机器学习的方法对层级模型和方差分析模型进行了比较,70%的数据作为训练集,剩余的30%作为测试集,以此比较二者的模型预测效果。 # 设置种子以确保结果的可重复性 set.seed(456) # 随机选择70%的数据作为训练集,剩余的30%作为测试集 train_index <- caret::createDataPartition(df.match$Sub, p = 0.7, list = FALSE) train_data <- df.match[train_index, ] test_data <- df.match[-train_index, ] # 根据训练集生成模型 model_full <- lme4::glmer(data = train_data, formula = ACC ~ 1 + Identity * Valence + (1 + Identity * Valence|Sub), family = binomial) model_anova <- lme4::lmer(data = train_data, formula = ACC ~ 1 + Identity * Valence + (1 + Identity * Valence|Sub)) # 使用模型进行预测 pre_mod_full <- stats::predict(model_full, newdata = test_data, type = 'response') pre_mod_anova <- stats::predict(model_anova, newdata = test_data) 我们对二者的预测性能进行比较。 # 计算模型的性能指标 performance_mod_full <- c(RMSE = sqrt(mean((test_data$ACC - pre_mod_full)^2)), R2 = cor(test_data$ACC, pre_mod_full)^2) # 打印性能指标 print(performance_mod_full) ## RMSE R2 ## 0.342402 0.074984 # 计算模型的性能指标 performance_mod_anova <- c(RMSE = sqrt(mean((test_data$ACC - pre_mod_anova)^2)), R2 = cor(test_data$ACC, pre_mod_anova)^2) # 打印性能指标 print(performance_mod_anova) ## RMSE R2 ## 0.342263 0.075676 RSME表示的是模型预测值和实际值之间的差异,可以发现二者的区别并不是很大。我们接下来还使用了混淆矩阵和ROC曲线的方法对模型性能进行比较,具体的代码和结果可以参考如下内容。 # 将预测概率转换为分类结果 predicted_classes <- ifelse(pre_mod_full > 0.5, 1, 0) # 计算混淆矩阵 confusion_matrix <- caret::confusionMatrix(as.factor(predicted_classes), as.factor(test_data$ACC)) # 打印混淆矩阵和性能指标 print(confusion_matrix) ## Confusion Matrix and Statistics ## ## Reference ## Prediction 0 1 ## 0 37 27 ## 1 499 3037 ## ## Accuracy : 0.854 ## 95% CI : (0.842, 0.865) ## No Information Rate : 0.851 ## P-Value [Acc > NIR] : 0.33 ## ## Kappa : 0.095 ## ## Mcnemar's Test P-Value : <0.0000000000000002 ## ## Sensitivity : 0.0690 ## Specificity : 0.9912 ## Pos Pred Value : 0.5781 ## Neg Pred Value : 0.8589 ## Prevalence : 0.1489 ## Detection Rate : 0.0103 ## Detection Prevalence : 0.0178 ## Balanced Accuracy : 0.5301 ## ## 'Positive' Class : 0 ## # 计算ROC曲线和AUC roc_result <- pROC::roc(test_data$ACC, pre_mod_full) ## Setting levels: control = 0, case = 1 ## Setting direction: controls < cases print(roc_result) ## ## Call: ## roc.default(response = test_data$ACC, predictor = pre_mod_full) ## ## Data: pre_mod_full in 536 controls (test_data$ACC 0) < 3064 cases (test_data$ACC 1). ## Area under the curve: 0.699 # 绘制ROC曲线 plot(roc_result, main = "ROC Curve", col = "blue", lwd = 2) abline(a = 0, b = 1, lty = 2) # 添加对角线 ROC的结果会返回一个指标”area under the curve”(AUC,曲线下面积),这个值一般越大说明模型越好。可以发现anova的曲线下面积值低于glm,也就是说glm优于anova。 在进行了上述的模型比较之后,我们发现不同的方法得到的结果是不一致的。我们一般认为当模型越符合数据特征的时候,模型的表现应该会更好,这是我们的直觉。glm使用二项分布去捕捉正确率这一因变量的数据,这是更符合数据特征的,但是这种更符合数据特征的模型效果居然更差,这和我们的直觉冲突。 这是因为有一些模型比较的方法是只适合于线性模型的,对于广义线性模型进行比较的时候就会出现问题,所以我们用一些传统的模型比较指标会发现anova更优或者二者没有差异。但我们用glm去预测不同被试在不同条件下被试的反应的时候,我们通常会发现glm的效果比anova更好。 不管怎样,这也提示我们建立完模型之后,对于如何比较不同的模型,选择什么样的比较指标和方法也要去花费一定的心思,而不能拿来就用。 本章至此一直在介绍如何对正确率这种分类的变量进行层级模型的建模分析,那么我们为什么不使用传统的方差分析呢? 2008年的一篇文章(jager, 2008)提到,我们对正确率进行anova时,会产生难以解释的结果:假设在10个回答中,正确回答8次,错误回答2次,此时95%CI为[0.52,1.08] ( = 0.8 ± 0.275),我们发现方差不齐,不满足方差分析基本假设。 \\[\\mu = np\\] \\[𝜎 = √(𝑛𝑝𝑞 )\\] \\[𝜎_p^2 = \\frac{p(1-p)}{n}\\] Jaeger, T. F. (2008). Categorical data analysis: Away from ANOVAs (transformation or not) and towards logit mixed models. Journal of Memory and Language, 59(4), 434-446. doi:http://dx.doi.org/10.1016/j.jml.2007.11.007 《Journal of Memory and Language》看上去好像是关于记忆和语言的期刊,但实际上上面有很多方法学,包括混合线性模型方法的介绍,读者可以关注该期刊。 10.5 其他分布 参照本章之前的思路,只要y和x之间可以通过线性转换和连接函数建立关系,就可以去对各式各样的数据类型进行建模分析。例如泊松分布(Poisson distribution),这是一种在医学或者流行病学领域研究中常见的数据分布类型。泊松分布是指在固定时间间隔或空间区域内发生某种事件的次数的概率,它适用于事件以恒定平均速率独立发生的情况,例如电话呼叫、网站访问、机器故障等。 \\[P(X = k) = \\frac{e^{-\\lambda} \\lambda^k}{k!}\\] - λ:事件在给定时间或空间内的平均发生率(或平均数量)。 - k:可能的事件发生次数,可以是0, 1, 2, … 下面我们用代码来模拟一下泊松分布。 set.seed(123) # 设置随机种子以获得可重复的结果 random_samples <- rpois(1000, lambda = 5) hist(random_samples,col = 'white', border = 'black',) 同样我们可以通过连接函数对其进行广义线性模型的建模。 另外一些常见的分布还包括伽马分布(Gamma Distribution),这是统计学的一种连续概率函数,是概率统计中一种非常重要的分布。“指数分布”和”卡方分布”都是伽马分布的特例。 \\[f(x | \\alpha, \\beta) = \\frac{\\beta^\\alpha x^{\\alpha-1} e^{-\\beta x}}{\\Gamma(\\alpha)}\\] - α:形状参数(shape parameter),决定了分布的曲线形态,尤其是峰值的位置和曲线的尖峭程度。 - β:尺度参数(scale parameter),影响分布的宽度;当尺度参数增大时,分布会变得更宽且矮平;尺度参数减小时,分布会变得更窄且高耸。 下面是伽马分布的示意图。 总体而言,本章介绍的广义线性模型可以将我们能够处理的数据拓展到正态分布以外,给我们提供了一个更好的工具。另外,请读者朋友们可以思考一下,信号检测论是否可以用广义线性模型分析?大家可以思考后在网上搜到相关的资料,这里不再赘述。 "],["第十一讲回归模型四中介分析.html", "Chapter 11 第十一讲:回归模型(四):中介分析 11.1 准备工作 11.2 线性模型回顾 11.3 中介分析 11.4 因果推断 11.5 参考资料推荐 11.6 总结", " Chapter 11 第十一讲:回归模型(四):中介分析 本章继续探讨回归模型,除了R代码与操作外,本节课还涉及两个非常简单的知识点。 为什么我们把中介模型放到回归模型里边? 我们在做中介分析的时候, 乃至在进行心理学讨论的时候,到底在做什么样的推断? 我们希望通过中介模型达到什么样的科学研究效果。 11.1 准备工作 # Packages if (!requireNamespace('pacman', quietly = TRUE)) { install.packages('pacman') } pacman::p_load(tidyverse,easystats,magrittr, # 中介分析 lavaan, bruceR,tidySEM, # 数据集 quartets, # 绘图 patchwork,DiagrammeR,magick) options(scipen=99999,digits = 3) set.seed(1002) 11.2 线性模型回顾 11.2.1 线性模型及模型检验 我们首先来回顾一下线性回归模型: 回归方程用于分析一个因变量与多个自变量之间的关系。在回归中,将一个或多个自变量视为整体,对因变量进行预测,通过普通最小二乘法(OLS)或最大似然估计法(ML)进行拟合,解释不了的成分则被视为残差;而我们的目的在于,舍弃残差(随机部分),而获得可解释的成分。 除此之外,下图Anscombe’s Quartet中展示了经典的、检查原始数据的情境。尽管四种数值摘要非常相似,但每组数据的图形分布却截然不同。这四组数据分别展示了不同的数据特征和问题。 而我们研究中的线性模型,适合的是有内在线性关系的数据。当我们的数据间不存在线性关系的时候,就会导致一些问题。 [anscombe_quartet] 上面提到的这些问题,有的时候可以通过一些模型诊断的方法来将其诊断出来, 比方说下图performance::check_model的回归模型评估结果中,我们可以看到,当有一个极大的偏移值的时候,我们的估计会产生误差。 [performance] lm(y ~ x,data = anscombe_quartet %>% dplyr::filter(dataset == '(3) Outlier')) %>% performance::check_model(check = c('linearity','outliers')) #<< 11.2.2 多元线性模型的局限 当然,上述这些问题都是技术上的问题, 可以通过其他更稳健的方法来解决。 但我们探讨的关键是回归模型,它能告诉我们的科学问题到底是什么? 如果我们回到宏观的科学的层面,我们为什么要做研究?我们可以说科学的目的就是对科学问题进行“描述、解释、预测、控制”。 我们的模型可以分为:“描述模型”、“推断模型”、“预测模型”,而回归兼具这三种功能。 回归模型作为描述模型。 假如说我们有两个变量,存在两组数据,我们不关心它们的内在机制, 而是想把这两个变量之间的关系描述出来。 那么这个时候回归模型,就可以提供帮助。 比如可以使用LOESS(即geom_smooth()中method默认的参数)对数据进行描述; 当然也可以用相关,但是我们知道GLM肯定是更强大的,包括我们可以用层级模型,就是广义的线性模型,都是回归模型, 回归模型作为推断模型。 描述模型中,只是把关系刻画出来,不关心它是否显著,但是推断模型则不一样,我们希望去知道它有没有达到某一个特定的阈值,比方说在心理学中,我们就特别希望知道是不是达到显著的效果,p是不是小于0.05 对整个模型来说,自变量的这个线性组合形成的这个预测项x,能不能预测y? 整个模型有没有达到显著,它的这个r方有多大? 这是一种推断。 另外一种推断就是我们关注各个变量的(偏)回归系数,根据其显著性可以进行统计推断(如果是离散变量的时候即等价与ANOVA)。 回归模型作为预测模型。 回归模型进行预测时,则不关注各个变量之间的复杂关系,因而将自变量当做整体,关注其是否能够预测因变量(拟合指标) 11.2.2.1 局限 我们对回归功能的讨论中没有提及到因果。这个推断里面我们是说推断它显不显著, 那我们根据个回归模型能不能做因果推断? 至少单一靠数据本身是不行的,有可能我们在加了很多前置的条件(assumption)之后是可以的。 这里以医学研究中的随机对照实验(Randomized Controlled Trial,RCT)为例, 在医学领域中,像大的药厂,在进行药物试验,确定某一个新药是不是有效的时候,会做很多个阶段的实验,在最后一个阶段的实验就是在大规模的人群上面测试。 在新冠期间,我们听过了很多这种报道,比方说mRNA疫苗怎样检验有效性, 其实就是在各个国家的大规模的人群上进行随机控制组的双盲实验,比较疫苗与安慰剂两种条件下的效果。 那么,这种情况之下,我们能做出因果推断了吗?能说实验组条件下效果比控制组好,就是因为疫苗的原因吗? 可以,因为我们基本上可以理解为疫苗是导致结果产生差异的唯一原因。 我们思考一下此时该研究用了什么统计方法? 比方说因变量比较的是感染率,这种差异性检验在某种程度上是t检验的变式, 那么为什么这种情况下能够做因果推断呢? 实际上,使他们做出因果推断的并不是统计数据本身,而是因为他们所做的实验假设。 他们假设,在做大规模的、双盲的、随机控制组实验的时候,其他所有可能会影响结果的变量,都在随机的过程中相互抵消了。也就说两组条件差异来自不同的实验条件,以及每个被试具备的随机差异,当我们的样本量足够大的时候,随机的样本,随机的量足够的复杂,最后两组中我们可以“认为”,除控制条件外,其他条件是等同的,因此,最后能够观察到的差异就是个两组之间的干预带来的差异。那么在随机控制组双盲的实验当中,它就是人为的去,在一种很苛刻的条件下去满足这种统计的假设。 但即便如此, 那么医学界研究者也会不断的去监控这个证据,做后续的Meta Analysis,去看各个RCT研究中是否有差异。 举例是想证明,并不是回归模型不能做因果推断,而是我们在做因果推断的时候,要加一些很强的统计的假设,在这个基础上、在满足了假设的前提下,我们才能够、才可能做出因果推断。而且即便我们做了一个很严谨的实验, 我们在当时,某种程度上可以做出因果推断, 但是这也并不代表推断是静态的,做了一次就一定就是因果关系,后来也可能有一些实验去把这种推断推翻, 这里可以稍微提简单的提一嘴,我们心理学的研究者做预测的时候,我们并没有机器学习领域的人做预测做那么严谨。机器学习领域,他们用一些方法(例如交叉验证)来避免预测率过高, 我们心理学至少在传统做法上不太重视预测,提预测的时候,其实可能有的时候只是一个简单的回归。 那么,当我们研究横断数据的时候,回归模型的使用,依赖很多其他的设定, 比方说“变量之间是否独立”等, 以及其他做出因果推断的前提条件。 如果所有自变量都相互独立,使用多元回归是合理的; 但在现实中,变量之间存在相互作用更为普遍,而多元回归值仅关注到自变量对因变量的独立作用(偏回归系数),很难描述变量间复杂的关系。变量越多,这个问题越明显。 Ref: https://www.tmwr.org/software-modeling 11.3 中介分析 在心理学领域,中介分析的使用很常见,我们上文在讨论中介分析的时候,有一个比较强的直觉,想要去做出因果推断。例如我们想要去推断一个特定的机制,通过问卷测量了三个变量,假设是x、y和m,中介分析中我们认为x影响了y是一个因果关系,且x是通过m来影响的y 其中有一个很强的因果关系,那么这个关系它到底成不成立呢? 那我们接下来从统计层面,来探讨这种因果是否成立。 11.3.1 2.1 对于“机制”的表示——“图” 我们在做路径分析的时候,我们一般使用“图”来表征理论因果关系 图包括两部分:节点和边。节点表示具体变量,而箭头表示变量之间的关系; 对节点来说,在SEM中,观测变量用矩形表示,潜变量用椭圆表示。 边表示变量间关系,**单箭头直线表示直接因果关系,从原因指向结果;双曲线箭头则表示相关 我们在做假设模型时,一般都不会假设出现下图这种情况,可以看到, 从x1到x2,x2到x3,x3又回来到x1了,形成了一个闭环。 我们一般来说更希望的是不存在循环的关系,如下图,在统计和机械学习领域,称其为有向无环图(Directed Acyclic Graph, DAGs) 11.3.2 中介分析 中介分析: 对于中介过程的量化分析包括路径分析和SEM(同时包含测量模型和结构模型),后面的介绍基于路径分析。 关注变量间因果关系,自变量如何影响因变量(即机制),如X通过M作用于Y,M为中介变量。中介的存在意味着时间上发生的先后顺序: \\(X \\rightarrow M \\rightarrow Y\\) 。 这里会有一个问题,我们为什么会关心中介机制呢,或者是中介变量呢? 中介分析,它对我们现实生活到底有什么真实意义? 有一种可能的例子,比方说原生家庭是自变量X,心理健康因变量是Y, 在其中心理健康Y是我们想干预的因变量,而原生家庭X是无法或者很难改变的自变量, 此时我们想要进行干预,如果能够找到x到Y的一些心理、行为倾向、特质上的中介机制,通过干预中介变量M,也能够达到我们想要的效果,最终实现科学研究的现实意义。 所以这是为什么我们要去研究因果关系,尤其是中介的关系, 我们其实希望找到一些方法,最终能够帮助我们这个世界变得更好的一些。 我们在进行中介分析时,通常就会看以下这几个公式, 一个是总方程的公式, 比方说 \\(M = i_1+ aX\\),加上一个误差项 \\(e_1\\)。这里就是一个简单的线性回归模型。 总方程: \\[Y = i_1 + cX + e_1\\] 分解: \\[M = i_2 + aX + e_2\\] \\[Y = i_3 + c'X + bM + e_3\\] 当我们加入中介时,我们就假定有这么一个中介变量m,m是可以通过x来预测的:\\(M = i_2 + aX + e_2\\),且又能够再去预测y:\\(Y = i_3 + c'X + bM + e_3\\), 所以它最后形成这样的形式: 11.3.3 中介效应 \\[ Y = i_1 + cX + e_1\\] \\[ M = i_2 + aX + e_2\\] \\[Y = i_3 + c'X + bM + e_3\\] 如果将第二个方程代入第三个方程: \\[ Y = i_3 + c'X + b(i_2 + aX + e_2) + e_3\\] \\[= (b*i_2 + i_3) + c'X + abX + (b*e_2 + e_3)\\] \\[= i_4 + c'X + abX + e_5\\] 我们再从中介模型来分析,可以发现,将X对Y的效应分解成了中介效应ab和直接效应c’ 在中介模型路径图中, \\(X \\rightarrow Y\\)路径上的回归系数 \\(c'\\)为直接效应 中介效应:ab,或 \\(c - c'\\)。在M和Y均为连续变量的时候,有: \\(ab = c - c'\\) 中介效应分为两类:完全中介(即c’ = 0)和部分中介(c’ ≠ 0) 我们观测到的中介效应本质是回归系数的乘积,而回归系数意味着变量间存在因果关系么? 以Penguin_data中CBT与DEQ的关系举例,对于总效应c来说: tot = lm(CBT ~ DEQ,data = pg_raw %>% dplyr::filter(romantic == 1)) # 计算相关 r = pg_raw %>% dplyr::filter(romantic == 1) %>% correlation::correlation(select = cc("DEQ,CBT")) %>% .$r # 比较回归系数与相关 data.frame('相关系数' = (sd(pg_raw$CBT,na.rm = T)/sd(pg_raw$DEQ,na.rm = T))*r, '回归系数' = tot$coefficients[2]) %>% print() ## 相关系数 回归系数 ## DEQ -0.000663 -0.000673 可以发现,回归系数本质上只是(偏)相关( \\(\\beta = \\frac{S_y}{S_x}·r\\)),而中介效应ab也只是两个回归方程的回归系数的乘积,或者说是 \\(r_{XM}\\) 与 \\(r_{MY}\\)的乘积;如果我们认为相关、偏相关不等于因果,那么中介就等于因果吗?此时我们基本可以下结论,仅靠数据上的中介分析是无法推断因果的。 这一结论意味着我们要做因果推断的话,不能仅依靠数据,数值本身不会直接告诉你它是什么意思,数据的意义的解读还是要依靠专家,以及之前的研究设定。 比如前面举例的RCT,它的统计思路某种程度上就是一个最简单t- test,如果我们用回归来分析RCT,最终可能也是得到一个回归系数、统计上的一个数值。 但是是因为我们在RCT中做了严谨的、双盲的、随机控制组的实验,而且一般来说有非常大的被试量,在这种情况下,我们能够做出因果推断。 那么在中介分析中,是不是一定不能做因果推论呢? 当然不一定。但如果我们希望利用中介分析也能够做出因果推论的话, 需要思考是不是也要做类似RCT的工作? 这也是这节课想要讲到的一个非常重要的知识点, 如果我们的实验研究设计,内在的没有去设定一个严格的条件,来保证统计上观测到的这些关系是因果关系,那么我们的中介效应本质上就是回归系数的乘积、偏相关,我们观测到的也只是相关关系。 但如果想要去得到中介的因果效应,也并非不可能, 只是需要在研究设计上花心思去考虑。 当然我们又是一门r语言的课程,以下来讲一讲如何在R语言中实现中介分析。 11.3.4 中介效应的检验 中介效应的检验方法很多,如四步法、Sobel检验等,但最常用的是通过Bootstrap 来计算中介效应的置信区间(且两个随机变量的乘积很多情境中并非服从正态分布),如果其置信区间不包含0则认为该参数估计值显著: Bootstrap对原始样本进行有放回的重复抽样(允许重复抽取相同数据),抽样次数通常等于数据本身大小N相同,假设重复抽取1000次; 然后对每次抽取的样本计算中介效应ab,就得到了1000个ab的值,据此估计中介效应ab的分布情况,进而取2.5%和97.5%个百分位点计算95%置信区间。 除了bootstrap以外,基于贝叶斯的路径分析、SEM也开始慢慢流行 本质上也有一个原因:频率学的很多方法,在它的模型变复杂之后,其求解也变得很困难。 也就是说频率学方法中,如果数据很少,但模型很复杂,此时求到一个比较准确的这个参数值的话是非常困难的,有的时候甚至是不太可能的, 所以呢,大家就用这种其他的方法(例如Markov Chain Monte Carlo,MCMC)去近似这个参数值 。 11.3.5 问题提出 在第六章中,我们使用Penguins数据研究了社交复杂度(CSI)是否影响核心体温(CBT),特别是在离赤道比较远的(低温)地区(DEQ)。 这里,我们复现论文中第一个中介模型:社会复杂度(CSI)可以保护处于恋爱中的个体的体温(CBT)免受寒冷气候(DEQ)的影响。具体来说: DEQ为自变量,CBT为因变量,CSI为中介变量。 赤道距离(DEQ)应当正向预测社会复杂度(CSI),而社会复杂度应当正向预测体温(CBT),但赤道距离(DEQ)应当负向预测体温(CBT)(即遮掩效应,如下图) 这里我们不再去做探索分析,进行数据的预处理之后,直接计算各个变量的值,再去做中介分析。如果要从原始数据里面去计算社交网络的复杂度CSI,可以去仔细的去看一看这里面的代码,会涉及到比较多的反复的重新编码。 # 数据导入 pg_raw = bruceR::import(here::here('data','penguin','penguin_rawdata_full.csv')) 数据预处理这里,可能会有很多这种变量名的选择,还有一些重新编码, 大家可以看到这个地方用的code是我们2018年左右写的代码, 当时是用的apply,自己写了一个function,来进行重新编码, 但实际上,如果大家想要去锻炼一下数据预处理能力,可以尝试能不能用tidyverse改一改这个地方,可能会更简洁。这里不再仔细讲解。 # 计算CSI ### get the column names: snDivNames <- c("SNI3", "SNI5", "SNI7", "SNI9", "SNI11", "SNI13", "SNI15", "SNI17","SNI18","SNI19","SNI21") extrDivName <- c("SNI28","SNI29","SNI30","SNI31","SNI32") # colnames of the extra groups ### create a empty dataframe for social network diversity snDivData <- setNames(data.frame(matrix(ncol = length(snDivNames), nrow = nrow(pg_raw))), snDivNames) ### recode Q10 (spouse): 1-> 1; else ->0 snDivData$SNI1_r <- car::recode(pg_raw$SNI1,"1= 1; else = 0") ####re-code Q12 ~ Q30: NA -> 0; 0 -> 0; 1~10 -> 1 snDivData[,snDivNames] <- apply(pg_raw[,snDivNames],2,function(x) {x <- car::recode(x,"0 = 0; NA = 0; 1:10 = 1;"); x}) ### add suffix to the colnames colnames(snDivData[,snDivNames]) <- paste(snDivNames,"div", sep = "_") ### recode the social network at work by combining SNI17, SNI18 snDivData$SNIwork <- snDivData$SNI17 + snDivData$SNI18 snDivData$SNIwork_r <- car::recode(snDivData$SNIwork,"0 = 0;1:10 = 1") ### re-code extra groups, 0/NA --> 0; more than 0 --> 1 extrDivData <- pg_raw[,extrDivName] # Get extra data extrDivData$sum <- rowSums(extrDivData) # sum the other groups snDivData$extrDiv_r <- car::recode(extrDivData$sum,"0 = 0; NA = 0; else = 1") # recode ### Get the column names for social diversity snDivNames_r <- c("SNI1_r","SNI3","SNI5","SNI7","SNI9","SNI11","SNI13","SNI15","SNIwork_r", "SNI19","SNI21","extrDiv_r") ### Get the social diveristy score snDivData$SNdiversity <- rowSums(snDivData[,snDivNames_r]) pg_raw$socialdiversity <- snDivData$SNdiversity ## 更改列名 pg_raw %<>% dplyr::rename(CSI = socialdiversity) ### 计算CBT(mean) # 筛选大于34.99 的被试 pg_raw %<>% filter(Temperature_t1 > 34.99 & Temperature_t2 > 34.99) # 前测后测求均值 pg_raw %<>% dplyr::mutate(CBT = (Temperature_t1 + Temperature_t2)/2) 11.3.6 代码实现 数据准备好之后,我们需要选择合适的R包。 11.3.6.1 lavaan 介绍 lavaan包专门用于结构方程模型(SEM)的估计,如CFA、EFA、Multiple groups、Growth curves等。 现在lavaan包在R中做SEM是最常用的, 因为它的教程非常的全面,我们基本可以跟随教程,完整的把这些分析都做下来。 当然lavaan并不是唯一的,R中还有一些其他的包来做结构方程模型。 基本语法 \\(^*\\): formula type operator mnemonic latent variable definition =~ is measured by regression ~ is regressed on (residual) (co)variance ~~ is correlated with intercept ~ 1 intercept ‘defines’ new parameters := defines Ref: https://lavaan.ugent.be/tutorial/syntax1.html 这里呈现一些基本语法,很多都与前面讲过的回归模型相似。其中latent variable definition指我们去定义潜变量,但我们这里不过多涉及潜变量,SEM一般分为测量模型与结构模型,这里我们关心的主要是结构模型及中介分析中的路径分析,而不是测量模型中的潜变量。 11.3.6.2 lavaan语句 以Penguin_data中的假设为例,我们在lavaan中首先需要去定义模型,再将模型输入到 lavaan::sem中。 具体来说, 先用双引号,将模型写成一个字符串,让lavaan去自动识别med_model <- \"\"。 模型中将不同的效应分开定义, 比方说我们这里。 - 定义直接效应为CBT ~ c*DEQ,因变量Y为核心体温CBT,自变量X为距赤道距离DEQ。 - 定义中介路径为CSI ~ a*DEQ从自变量DEQ到CSI,CBT ~ b*CSI然后再从CSI到CBT。 - 定义中介效应为ab := a*b - 定义总效应为为total := c + (a*b) 可以发现,这里本质上就是把之前线性回归的公式,换成了r代码。 定义模型之后,我们再使用lavaan包进行拟合,这里Bootstrap次数因为时间关系设为100,实际研究中推荐1000次。 med_model <- " # 直接效应(Y = cX) CBT ~ c*DEQ # 语法同回归,但需要声明回归系数 # 中介路径(M) CSI ~ a*DEQ CBT ~ b*CSI # 定义间接效应c' #注: `:=`意思是根据已有的参数定义新的参数 ab := a*b # 总效应 total := c + (a*b)" # 注:这里数据仅以处于浪漫关系中的个体为例 fit <- lavaan::sem(med_model, data = pg_raw %>% dplyr::filter(romantic == 1), bootstrap = 100 # 建议1000 ) Tips:有一些同学可能不太习惯这种带有一定随机过程的结果, 比如说跑了两次的结果不太一样。实际上,不管是使用bootstrap还是贝叶斯,这种带一些随机过程的结果输出,总体上大致相似,只是每一次输出的结果可能不会精确的一致。 这个结果对于大部分习惯于论文结果格式的人来说,可能会觉得比较乱。不过我们仔细观察可以发现,输出结果与我们的论文中介模型图数值有着完整的对应关系,数值也会因随机过程有些微区别。 我们这里绘图使用的是tidySEM包,当然也有semPlot等包可以选择;tidySEM使用了tidyverse风格,并支持lavaan和Mplus等语法对SEM进行建模,可使用help(package = tidySEM)进行查看。 #中介图-tidySEM ## 与DiagrammeR::get_edges相冲突 detach("package:DiagrammeR", unload = TRUE) ## 细节修改可在Vignettes中查看tidySEM::Plotting_graphs lay = get_layout("", "CSI", "", "DEQ", "", "CBT", rows = 2) tidySEM::graph_sem(fit,digits = 3, layout = lay) Tips:我们使用tidySEM::的话,可能与其他包中的函数产生冲突,一般我们在函数前面加这个包的名字就可以解决,但当我们调用的函数内部又要调用冲突函数时,我们需要通过detach函数来避免冲突。 11.3.7 PROCESS in bruceR() 我们之前提到过,心理学同行中很多人会用SPSS中的PROCESS宏来进行中介分析。 前两年,BruceR的开发者包博士, 他也将类似功能加入了BruceR包里面。 bruceR包中介分析的用法非常符合我们心理学研究者的习惯,我们用bruceR::PROCESS 定义x、y、meds,再设定bootstrap的次数即可。 #[bruceR::PROCESS] ## RUN IN CONSOLE !!! pg_raw %>% dplyr::filter(romantic == 1) %>% bruceR::PROCESS( ## 注意这里默认nsim = 100,建议1000 x = 'DEQ', y = 'CBT',meds = 'CSI',nsim = 100) ## ## ****************** PART 1. Regression Model Summary ****************** ## ## PROCESS Model Code : 4 (Hayes, 2018; www.guilford.com/p/hayes3) ## PROCESS Model Type : Simple Mediation ## - Outcome (Y) : CBT ## - Predictor (X) : DEQ ## - Mediators (M) : CSI ## - Moderators (W) : - ## - Covariates (C) : - ## - HLM Clusters : - ## ## All numeric predictors have been grand-mean centered. ## (For details, please see the help page of PROCESS.) ## ## Formula of Mediator: ## - CSI ~ DEQ ## Formula of Outcome: ## - CBT ~ DEQ + CSI ## ## CAUTION: ## Fixed effect (coef.) of a predictor involved in an interaction ## denotes its "simple effect/slope" at the other predictor = 0. ## Only when all predictors in an interaction are mean-centered ## can the fixed effect denote the "main effect"! ## ## Model Summary ## ## ────────────────────────────────────────────────── ## (1) CBT (2) CSI (3) CBT ## ────────────────────────────────────────────────── ## (Intercept) 36.426 *** 7.159 *** 36.426 *** ## (0.015) (0.050) (0.015) ## DEQ -0.001 0.029 *** -0.002 ## (0.001) (0.004) (0.001) ## CSI 0.046 *** ## (0.011) ## ────────────────────────────────────────────────── ## R^2 0.001 0.082 0.024 ## Adj. R^2 -0.001 0.081 0.021 ## Num. obs. 763 763 763 ## ────────────────────────────────────────────────── ## Note. * p < .05, ** p < .01, *** p < .001. ## ## ************ PART 2. Mediation/Moderation Effect Estimate ************ ## ## Package Use : ‘mediation’ (v4.5.0) ## Effect Type : Simple Mediation (Model 4) ## Sample Size : 763 (37 missing observations deleted) ## Random Seed : set.seed() ## Simulations : 100 (Bootstrap) ## ## Warning: nsim=1000 (or larger) is suggested! ## ## Running 100 simulations... ## Indirect Path: "DEQ" (X) ==> "CSI" (M) ==> "CBT" (Y) ## ────────────────────────────────────────────────────────────── ## Effect S.E. z p [Boot 95% CI] ## ────────────────────────────────────────────────────────────── ## Indirect (ab) 0.001 (0.000) 3.826 <.001 *** [ 0.001, 0.002] ## Direct (c') -0.002 (0.001) -1.783 .075 . [-0.004, 0.000] ## Total (c) -0.001 (0.001) -0.640 .522 [-0.002, 0.001] ## ────────────────────────────────────────────────────────────── ## Percentile Bootstrap Confidence Interval ## (SE and CI are estimated based on 100 Bootstrap samples.) ## ## Note. The results based on bootstrapping or other random processes ## are unlikely identical to other statistical software (e.g., SPSS). ## To make results reproducible, you need to set a seed (any number). ## Please see the help page for details: help(PROCESS) ## Ignore this note if you have already set a seed. :) #bruceR::PROCESS-Regression ## RUN IN CONSOLE !!! pg_raw %>% dplyr::filter(romantic == 1) %>% bruceR::PROCESS( ## 注意这里默认nsim = 100,建议1000 x = 'DEQ', y = 'CBT',meds = 'CSI',nsim = 100) %>% capture.output() %>% .[27:43] ## ## Warning: nsim=1000 (or larger) is suggested! ## [1] "Model Summary" ## [2] "" ## [3] "──────────────────────────────────────────────────" ## [4] " (1) CBT (2) CSI (3) CBT " ## [5] "──────────────────────────────────────────────────" ## [6] "(Intercept) 36.426 *** 7.159 *** 36.426 ***" ## [7] " (0.015) (0.050) (0.015) " ## [8] "DEQ -0.001 0.029 *** -0.002 " ## [9] " (0.001) (0.004) (0.001) " ## [10] "CSI 0.046 ***" ## [11] " (0.011) " ## [12] "──────────────────────────────────────────────────" ## [13] "R^2 0.001 0.082 0.024 " ## [14] "Adj. R^2 -0.001 0.081 0.021 " ## [15] "Num. obs. 763 763 763 " ## [16] "──────────────────────────────────────────────────" ## [17] "Note. * p < .05, ** p < .01, *** p < .001." ## ## Warning: nsim=1000 (or larger) is suggested! ## [1] "Package Use : ‘mediation’ (v4.5.0)" ## [2] "Effect Type : Simple Mediation (Model 4)" ## [3] "Sample Size : 763 (37 missing observations deleted)" ## [4] "Random Seed : set.seed()" ## [5] "Simulations : 100 (Bootstrap)" ## [6] "" ## [7] "Running 100 simulations..." ## [8] "Indirect Path: \\"DEQ\\" (X) ==> \\"CSI\\" (M) ==> \\"CBT\\" (Y)" ## [9] "──────────────────────────────────────────────────────────────" ## [10] " Effect S.E. z p [Boot 95% CI]" ## [11] "──────────────────────────────────────────────────────────────" ## [12] "Indirect (ab) 0.001 (0.000) 4.095 <.001 *** [ 0.001, 0.002]" ## [13] "Direct (c') -0.002 (0.001) -1.753 .080 . [-0.004, 0.000]" ## [14] "Total (c) -0.001 (0.001) -0.653 .514 [-0.003, 0.001]" ## [15] "──────────────────────────────────────────────────────────────" 可以看到,bruceR::PROCESS的结果输出很符合我们在论文中看到的结果呈现方式, 对我们研究生或者研究者来说,非常友好,而且跑出来的结果还可以输出到Word文档中去。 R包给我们的便利是我们可以使用其快速得到想要的统计值,但回到我们这节课关于因果关系的讨论,我们需要时刻清楚我们在使用怎样的一个统计系数,我们该如何去验证变量间的因果关系。我们很容易直觉的认为中介模型是帮助我们去得到某种机制,帮助我们去获得某种因果关系。但实际上我们推断因果关系时不能仅数据与统计模型,也依赖于我们如何设计研究,收集数据。如果前提假设未能满足,那么统计本身并不能带来令人信服的结果。 11.3.8 反思 在刚才的分析中,我们希望证明:社会复杂度(CSI)可以保护处于恋爱中的个体的体温(CBT)免受寒冷气候(DEQ)的影响,因而通过中介分析来验证假设,但实际上我们得到的只是变量间的相关,而不能得到期望的因果关系。 那当我们做中介分析时,想要去获得某种程度上的因果推断或者提高其可信度,该怎样做呢? 11.4 因果推断 11.4.1 因果推断(Casual Inference) 因果推断是近些年非常火的话题,它涉及到很多的层面上的知识。 比如从哲学层面思考“ 在一个假想的世界当中,如果我们可以进行时间旅行,回溯到过去的话, 那么我们现在物理世界的因果,还是真正的因果吗?”当然这种思辨已经超越了我们所在的宏观个世界了,变得非常的抽象。 回到我们所处的这个世界里,确认变量间存在因果关系至少满足三个条件 \\(^*\\): 1.时间顺序:因在果之前发生; 2.共变:因果之间存在相关,原因的变化伴随结果的变化; 3.排除其他可能的解释 目前社科中常用的一个因果推断框架是反事实(conterfactual)推断,即观察到与事实情况相反的情况,例如: 一个人得了感冒, 而服用感冒药以后症状得到了缓解,对药效的归因为“如果当时不吃药,感冒就好不了”(即反事实) 但反事实理论框架要求需要针对特定的个体——相同个体,当时在感冒发生时不吃药,且最后“感冒好不了” 由于反事实的“不可观测性”,实际研究中使用随机对照的方式来解决(找到发生在相似个体身上的“反事实情况”)。 刘国芳,程亚华,辛自强.作为因果关系的中介效应及其检验[J].心理技术与应用,2018,6(11):665-676 这里简单提一下, 在《The Book of Why: The New Science of Cause and Effect》这本关于因果推断的科普书籍中,作者Judea Pearl等人提出了一种用于进行反事实推理的方法,叫做“do calculation” 。do算子的基本思想是模拟对系统中某个变量的人工干预,以观察这种干预对其他变量的影响。 11.4.2 因果推断与概率 这里以在我们日常生活中,看到这种因果推断为例,来思考因果推断与概率的关系。 假设100万儿童中已有99%接种了疫苗,1%没有接种。 - 接种疫苗:有1%的可能性出现不良反应,这种不良反应有1%的可能性导致儿童死亡,但不可能得天花。 - 未接种疫苗:有2%的概率得天花。最后,假设天花的致死率是20%。 要不要接种? 99万接种:则有990000*1% = 9900的人出现不良反应,9900*1% = 99人因不良反应死亡 1万未接种:有10000*2% = 200人得了天花,共200*20% = 40人因天花死亡 所以不接种疫苗更好? 如果基于一个反事实问题:疫苗接种率为0时会如何? 共100万*2% = 20000人得天花,20000*20% = 4000人会因天花死亡。 我们发现在这个因果推断当中,需要我们经过很精细的推理,去思考其内在的关系。有的时候仅靠直觉的话,可能会推导出差异很大的结论。 “’因果关系不能被简化为概率’这个认识来之不易……这个概念也存在于我们的直觉中,并且根深蒂固。例如,当我们说“鲁莽驾驶会导致交通事故”或“你会因为懒惰而挂科”时,我们很清楚地知道,前者只是增加了后者发生的可能性,而非必然会让后者发生。” Ref:《The Book of Why: The New Science of Cause and Effect》 11.4.3 基于实验的中介 中介模型的数值本身并不能告诉我们因果关系,而在心理学中,利用中介变量去发现因果关系又是很重要的,尤其是当自变量是一些不可干预的变量时,通过干预中介来达到科学研究的目的是很好的方式。这就会涉及到我们如何去验证带有真正的因果的中介,当然,就如同RCT研究中想要得出因果都需要很复杂的假设,要去验证真正有因果的中介,是一件难上加难的事情。 这里,我们用一个国内的学者葛枭语老师,发表在JESP上面一篇文章为例子,来探讨如何验证中介中的因果。 假设:教材难度(X)通过焦虑(M)来影响努力程度(Y),可以穷举出在哪些情况下我们不能验证中介中的因果: 教材难度(X)不能影响焦虑(M) 焦虑(M)不能影响努力程度(Y) 教材难度(X)可以影响焦虑(M),焦虑(M)也可以影响努力程度(Y),由 X 的变化引起的 M 的变化并不会导致 Y 的变化(即 M 对 Y 的影响与 X 对 Y 的影响无关)。 • 操纵X • 测量 M • 测量 Y 对X进行操纵(如使用不同难度的教材),可以验证X对M的因果关系,但M与Y之间的因果关系并没有得到验证 但如果我们理论假设错误,测量的是焦虑(A),但实际上实验操纵引发的中介应为恐惧(M,即实际路径应为X - M - Y,而我们测量路径为X - A - Y),那么刚才的实验设计可能无法证伪,因此需要对A进行操纵: • 操纵 X • 操纵 A • 测量 Y 对X(如使用不同难度的教材)和A(控制组 vs 提供相关辅导以减轻焦虑)进行操纵,如果对A的操纵不能影响Y,则可以证明中介路径不合理 所以这里意味着我们要去进行带有因果性质的中介分析时,我们要做很多科学的思考, 在实验设计上,尽量去控制混淆变量,这样的话才能够去真正的把因果关系搞清楚。 如果去看其他的一些领域的文章, 比如生物、农业或者精神疾病、公共卫生等领域, 其研究能够使用大量的数据,进行相对随机化的处理,就有可能通过中介模型,加上一些很强的统计的assumption,最终进行因果推断。 而对于我们心理学领域,很多问卷研究中,即便有几千人的数据,做了一个看起来很漂亮的模型,也很难发到真正的很好的期刊上面。因为其中的因果推断是受到质疑的,如果要把变量关系理清楚的话,需要投入大量的精力与时间,包括研究设计、统计训练等,不一定会比做认知实验的研究者花的时间少。 11.5 参考资料推荐 在此我们推荐一些相关的参考资料: 首先是葛枭语老师发表在JESP上实验验证中介模型的文章 (葛枭语, 2023), 我们刚刚提到的是其中最简单的一个形式,大家如果感兴趣的话,可以去深入研究一下。 另外,在AMPPS杂志上,最近也有一个探讨如何通过实验设计,去做因果性质的中介分析的文章 (Bullock et al , 2023, AMPPS)。 还有几个稍微科普性质一点的文章,作者名叫Julia M. Rohrer。 我们Open Science之前请过她来做过报告。当时的标题叫做“随意的因果推断”。她在AMPPS发了一个篇科普的文章,就通过模型去把变量之间的因果关系梳理清楚 (Rohrer, 2018), 他们最近又有一篇文章叫做“A lot of process” (Rohrer, 2022),很多用process做的这种中介调节分析, 其中的因果推断能不能成立,也是一个很值得反思的问题。 最后如果大家对统计方面的这个因果关系感兴趣,我是非常推荐去看《Statistical Rethinking: A Bayesian Course with Examples in R and Stan》(统计再思考),一本德国人写的英文书, 这本书把贝叶斯统计、r语言和因果放在一起讨论,形成了独特体系。 此外,作者Richard McElreath每年都会把他教授这本书的视频录制下来,上传到YouTube上。 11.6 总结 最后总结一下,这节课的两个主要目标: 第一个目标,就是介绍R语言中实现SEM数据分析的完整流程,包括使用 lavaan等R包的方法。 如果大家有需要去做中介、调节等研究,Lavaan包提供的教程,一般来说是足够的, 不过有一需要注意的点——如果使用不同的软件、不同的包去跑SEM,输出结果可能会不一样, 因为模型内置的这些参数设定可能有一些不同的。 这其中我没有过度去讲解代码,因为如果理解背后原理的话,通过软件或者R包教程,都能得到很清晰的解释。 更重要的第二个目标,是关于中介分析与因果推断 这里想介绍的一个观点就是:我们在用统计的中介模型的时,并没有进行严格的因果推断, 它某种程度上是一种相关,只有在极少数的情况下,其因果关系才是成立的。要进行严谨的、带有因果性质的中介分析,需要做很多的工作。当然这里也鼓励大家往这个方向去发展,如果我们立志要在纷繁复杂的社会科学背景下,在各个变量之间建立一个因果关系,并将其解释清楚,那还是值得去下功夫的。 Ref: lavaan(提供了完整的SEM代码教程): https://lavaan.ugent.be/tutorial/ 通过实验来验证中介效应(葛枭语, 2023) 内隐中介分析(Bullock et al , 2023, AMPPS) 相关不等于因果(Rohrer, 2018) A lot of processes (Rohrer, 2022) "],["第十二讲数据可视化进阶.html", "Chapter 12 第十二讲:数据可视化进阶 12.1 作图的必要性和作图数据处理 12.2 基础作图 12.3 进阶作图 12.4 高级图片处理–magick", " Chapter 12 第十二讲:数据可视化进阶 12.1 作图的必要性和作图数据处理 我们在前两章对心理学和社会科学中最常用的回归模型进行了讲解,在进行统计分析的过程中,最关键的并不是如何使用r代码,而是如何正确地使用r代码,这就意味着我们不可避免地要去学习其背后的统计知识。做完了所有的分析之后,我们希望将结果以一种比较美观的方式呈现给读者,这时就涉及到了进一步的数据可视化。 我们在前面章节也讲到过可视化,那时主要的目的是为了对数据进行初步的探索,让我们可以快速地辨别和发现数据之间的一些关系,帮助我们更好地了解数据。而本章所要讲的数据可视化,则是针对如何画出一个可发表的图像。 12.1.1 为什么要作图以及作图的原则 经常读论文的人可能会发现,我们读多了之后会形成一个习惯——先看标题,然后看摘要,接着就开始看文章的图标了。为什么大家会这么看呢?因为科研论文中很多时候我们就是用图表来展示我们最关键的信息。数据可视化有时候可以做到”一图胜千言”。但是要做出一张美观且直观的图片,是需要经过许多思考的。 我们在R中依然还是用ggplot来进行画图。那么如何画好一张图呢?首先我们要清楚自己想要通过图像达到什么样的目的,比如我们的实验流程图和最后呈现的统计分析图片,它们的目标肯定是不同的,所以不仅要知道作图的技术,还要知道用什么样的图片可以更好地传达自己的意图。 其次在明确了目标之后,作图也可以像翻译一样,做到信达雅三个标准。信,就是要传递足够的信息量,如果读者经常看顶刊就会发现,现在顶刊上图片的信息量越来越大,尤其在生物医药领域,一个图包含了非常多的子图,每一个子图又包含了很多元素。当然这并不一定是推荐的方式,信息量应该均衡至一眼可以看出表达信息的程度。达,就是图的逻辑性,图片的线条、图例等等要有层次感,让人可以清晰感受到元素之间的内在联系。最后的雅,就是作图要追求美观,如果对比一下普通期刊和Science、Nature这些顶刊上的图片,就会发现顶刊上的图片普遍更美观。国内也有一本图片很美观的期刊《Innovation》,期刊编辑部里甚至有专门的美工对图片色彩进行调节,这也充分体现出图片美观的重要性。 当然这里仅将这些原则罗列给诸位读者,要做出好看的图,还需要不断地打磨和在实践中不断提升自己的作图技巧。 12.1.2 作图数据准备 我们先来导入本章所要用到的包。 if (!requireNamespace('pacman', quietly = TRUE)) { install.packages('pacman') } pacman::p_load( # 本节课需要用到的 packages here, tidyverse, reshape, bruceR, ggplot2, patchwork, magick, ggdist) options(scipen=99999,digits = 5) 数据仍然是之前所用到的认知实验的数据,这里我们为了方便仅选取了match条件下的数据。我们之前发现Valence和Identity之间有显著的交互作用,作图开始时首先会想到的就是两条交叉的线,两条线的端点分别是各个条件下的均值,因此我们需要先得到各个条件下面的均值。 首先这是实验中每个试次的数据。 df.match.trial <- bruceR::import(here::here('data','match','match_raw.csv')) %>% tidyr::extract(Shape, into = c('Valence', 'Identity'), regex = '(moral|immoral)(Self|Other)', remove = FALSE) %>% #将Shape列分为两列 dplyr::mutate(Valence = factor(Valence, levels = c('moral','immoral'), labels = c('moral','immoral')), Identity = factor(Identity, levels = c('Self','Other'), labels = c('Self','Other'))) %>% dplyr::filter(ACC == 0 | ACC == 1, RT >= 0.2 & RT <= 1.5, Match == 'match', (!Sub %in% c(7302,7303,7338))) head(df.match.trial, 4) %>% DT::datatable() 接下来我们整合试次,得到每个被试在4中条件下的均值。 df.match.subj <- df.match.trial %>% dplyr::group_by(Sub, Identity, Valence) %>% dplyr::summarise(RT_mean = mean(RT), ACC_mean = mean(ACC)) %>% dplyr::ungroup() ## `summarise()` has grouped output by 'Sub', 'Identity'. You can override using the `.groups` ## argument. head(df.match.subj, 4) %>% DT::datatable() 因为我们在作图时想看到的是总体的交互作用,因此我们将所有被试的数据平均得到一个总体在4种条件下的RT和ACC均值。在下面的代码中,大家可以发现SD和SE,这是因为我们在作图时还需要在均值的基础上加上误差棒(error bar)。这里我们需要将误差棒和字母”T”进行区分,因为之前有个研究者拿字母”T”冒充误差棒,当然后来被人发现了,还在社交媒体上火了一把。这也提示我们作图时,拿最基础的柱图举例,不仅要画出集中趋势(平均值),还要画出离散趋势(误差项)。 df.match.sum <- df.match.subj %>% dplyr::group_by(Identity, Valence) %>% dplyr::summarise(grand_mean_RT = mean(RT_mean), SD_RT = sd(RT_mean), SE_RT = SD_RT/sqrt(n()-1), grand_mean_ACC = mean(ACC_mean), SD_ACC = sd(ACC_mean), SE_ACC = SD_ACC/sqrt(n()-1), n = n()) %>% dplyr::ungroup() ## `summarise()` has grouped output by 'Identity'. You can override using the `.groups` ## argument. head(df.match.sum, 4) %>% DT::datatable() 整体的均值加上误差,这样的图在传统意义上已经足够了。但是最近七八年,大家发现如果只呈现总体趋势,这对读者是有误导性的,因为如果我们呈现每个被试数据的话,有时候会发现整体的效应量实际上没有那么大。因此后来有研究者鼓励大家在作图时不仅要可视化整体数据,还要将每个被试的数据也呈现在图上。这样能够保证读者在读图时不会过高地估计实验的效应量。 12.2 基础作图 12.2.1 ggplot2基础回顾 让我们再来回顾一下ggplot的一些基础知识,gg的全称是grammar of graphics,即作图的语法。 ggplot的基本原理就是图层的叠加,这和Photoshop的逻辑是很类似的。 ggplot的图层叠加主要分为分为主要图层和可选图层。主要图层主要包含了数据(data),映射(aesthetics)和图形(geometries)。在代码中,便是如下图所示的,首先放入数据DATA,然后我们需要对数据建立空间上的映射MAPPINGS,最后再决定用什么样的几何图形对映射的数据进行可视化,也就是图中红色的部分,这些红色的图形又通过加号进行叠加。这就是基础的主要图层。 12.2.2 主要图层 我们先用总体的数据来绘制主要图层。在下面的代码中,我们放入了总体数据df.match.sum;接着将x轴定义为Identity,它有两个取值”self”和”other”,y轴定义为总体的反应时;填充”fill”指的是我们所用的图例,我们将不同的Identity的柱子用Valence进行填充;最后我们用geom_bar()这一函数来定义所要画的是柱状图,再加上误差棒geom_errorbar()。这样就做出了一个2×2的条形图。 # 以柱状图为例 p1 <- ggplot2::ggplot(data = df.match.sum, aes(x = Identity, y = grand_mean_RT, fill = Valence)) + ggplot2::geom_bar(stat = "Identity", position = "dodge") + ggplot2::geom_errorbar(data = df.match.sum, aes(ymin = grand_mean_RT - SE_RT, ymax = grand_mean_RT + SE_RT), width=.1, position = position_dodge(.9)) p1 值得注意的是,geom_errorbar()中我们映射的是y的最大值/最小值是整体RT加上/减去SE,所以误差棒表示的是均值上下各一个标准误,虽然大部分情况下研究者都用标准误来作为误差棒,但也有会用标准差SD来作为误差棒的。另外,我们可以用width定义误差棒的宽度,position_dodge()将误差棒进行小幅度错开,这也对应了前面柱状图里面position = “dodge”。 12.2.3 可选图层 上面所绘制的图其实不太符合心理学和APA的格式要求,这种R默认绘制出来的图有一些缺点:比如背景有阴影和框线,其次y轴不是以0为起点且轴的字体不是特别突出。另外我们可以看到ggplot的默认字体是很小的,这也是很多研究者吐槽的一点,如果不对字体进行调节是比较影响作图的直观性的。 这些问题需要我们加入额外的图层来加以解决,这就是可选图层。例如我们可以用scale_xxx()函数自定义坐标轴的一些信息,用theme_xxx()函数来切换图片的主题。 这里我们直接在主要图层p1的基础上累加可选图层。下面的代码中,expand=c(0, 0)让坐标原点从零点开始;breaks = seq(0, 0.75, 0.25)使得y轴以0开始,0.75结束,以0.25作为最小单位;limits = c(0, 0.75)则限定了y轴的范围,这些都通过scale_y_continuous()这一函数来进行调节,这是因为RT是连续变量,所以对应的是”continuous”。labs()用于修改主题和y坐标轴的标题。最后我们使用papaja包下的theme_apa()函数让图片自动符合APA主题。 # 以柱状图为例 p2 <- p1 + ggplot2::scale_y_continuous(expand=c(0, 0), breaks = seq(0, 0.75, 0.25), limits = c(0, 0.75)) + ggplot2::labs(title = "Mean RT for match trials", y = "RT") + papaja::theme_apa() p2 这里绘制的图整体是符合APA格式的,它以零点作为起点,没有背景的灰色和框线。同理,我们将主要图层和可选图层全部写在一起,可以得到ACC的图。 12.2.4 同时呈现多张图片 现在我们有了两张图,一张RT,一张ACC,我们希望让这两张图一起呈现,那么我们应该如何将二者拼在一起呢?这里我们将介绍两种方法。 第一种方法是分面(Facet),我们可以把画框当作一个面板,只要我们在数据框里面有分类的变量作为依据,就可以据此将不同的图片绘制在面板上。Facet也可以被认为是图层的一种,也是通过”+“加号叠加在原始图片上,可以分为一维(facet_wrap)和二维(facet_grid)两种,下图所示为二维,由于不同的子图之间有很多变量是相同的,这样陈列在一起方便我们进行肉眼上的比较和观察。 在我们的数据中,RT和ACC在x轴上的变量是一致的,但是y轴的因变量是不同的,因此我们需要一个额外的变量去对RT和ACC进行分类和标记,然后R才能根据这个不同的标记进行分面板的绘制和呈现。这听起来有点复杂,实际上就是意味着我们在呈现数据的时候要合并RT和ACC。在下面的代码中,我们将之前的grand_mean_RT和grand_mean_ACC全部重新命名为grand_mean,SD和SE也进行类似的操作,合并完后我们再新增一个叫做DV的变量对RT和ACC进行分类。 df1 <- df.match.sum[,-c(6, 7, 8)]%>% dplyr::rename(grand_mean = grand_mean_RT, SD = SD_RT, SE = SE_RT) %>% dplyr::mutate(DV = "RT") df.match.sum.long <- df.match.sum[,-c(3, 4, 5)] %>% dplyr::rename(grand_mean = grand_mean_ACC, SD = SD_ACC, SE = SE_ACC) %>% dplyr::mutate(DV = "ACC") %>% rbind(df1,.) rm(df1) head(df.match.sum.long, 3) %>% DT::datatable() 此时,我们的数据框就变成了一个长形的数据,并使用DV这一列对数据因变量进行分类。合并与分类结束之后,我们就可以使用Facet来进行呈现了。在下面的代码中, papaja::theme_apa()这一行之前其实和前面提到的代码是一致的,只是将y定义为合并之后的因变量grand_mean。这时我们需要新增的就是放入DV这一分类变量,scales = “free_y”表示在不同面板上y轴的单位不固定,根据数据情况来自动调整。最后根据实际情况增加一些title和轴上的label。 p4 <- df.match.sum.long %>% ggplot2::ggplot(., aes(x = Identity, y = grand_mean, fill = Valence)) + ggplot2::geom_bar(stat = "identity", position=position_dodge(), ) + ggplot2::geom_errorbar(aes(ymin = grand_mean-1.96*SE, ymax = grand_mean+1.96*SE), width = .1, position = position_dodge(.9)) + papaja::theme_apa() + ggplot2::facet_wrap(~DV, scales = "free_y") + ggplot2::labs(title = "Summary data for matching trials", x="Identity", y="mean") p4 可以看到合并后的图片如上图所示,共享一个y轴标签”mean”。可以看到因为没有使用scale_y_continuous()函数对y轴进行调整,因此这里y轴的起点并不是0,读者可以参照上面的代码来完善这幅合并之后的图。 第二种方法是使用patchwork,这种方法比较简单粗暴,不需要进行数据预处理,直接+在一起就可以,plot_layout(guides = “collect”)指将图例合并起来。 p2 + p3 + plot_layout(guides = "collect") 我们在图上可以观察出交互作用,红色柱子相连的话,其斜率是比绿色柱子相连的斜率更大的,也就是二者会相交。如果我们将y轴的范围缩短一些,这种交互作用的趋势将会在图上更显著地呈现出来。 以上所讲的这些总结起来包括了数据处理和图形美化这两部分,如下图所示(注:本图来自《R语言数据可视化之美:专业图表绘制指南》)。 12.3 进阶作图 12.3.1 整体和个体效应共存的图 刚才我们提到了研究者还希望呈现除了整体之外的被试个体的数据,这就意味着要去了解更多关于图层叠加的特点和技巧。比如我们想要得到下面这样效果的图,我们将之前的柱状图改成线的方式进行呈现,可以明显看到交互作用的存在;另外还将每一个被试的数据也放在了图上,以RT图中self的条件下为例,可以看到大部分被试跟整体趋势是一致的,也就是moral下面反应时比immoral更短,然而也有几名反过来的被试。这样就把群体水平和个体水平的信息很好地呈现在一幅图上了。那么如何做出这样的图呢? 首先我们要画出整体的均值,映射上和刚才所做的一致,图形的选择上,我们将柱状图geom_bar()替换为线图geom_line(),并用geom_point()将线的两端变成两个较大的点,并使用position_dodge(0.5)将点和线都错开一些并且互相对齐。同样的,我们加上error bar和修改y轴的坐标。值得注意的是,这里y轴的选取相比前面要更广,因为希望将所有被试的数据都纳入进来。以下是整体数据的线图代码。 s1 <- df.match.sum %>% ggplot2::ggplot(., aes(x = Identity, y = grand_mean_RT, group = Valence, color = Valence)) + ggplot2::geom_line(position = position_dodge(0.5)) + ggplot2::geom_point(size = 3, position = position_dodge(0.5)) + ggplot2::geom_errorbar(aes(ymin=grand_mean_RT-SE_RT, ymax=grand_mean_RT+SE_RT), width=.1, position = position_dodge(0.5)) + ggplot2::scale_y_continuous(limits = c(0.4, 0.9)) + #选取能纳入全部散点的范围 papaja::theme_apa() s1 接下来我们希望加入每个被试的数据点,依然使用geom_point()这个函数,可以注意到这里用到的是每个被试的数据df.match.subj,而不是前面的df.match.sum,代表每个被试在4种实验条件下的均值。下面是相应的代码。 s2 <- s1 + ggplot2::geom_point(data = df.match.subj, aes(x = Identity, y = RT_mean, group = Valence)) s2 可以看到确实把被试的数据点画了出来,但是似乎不是很美观,所有点都叠在一起。于是我们使用position = position_jitter(width = 0.1)这一语句将数据点抖动错开。 s3 <- s1 + ggplot2::geom_point(data = df.match.subj, aes(x = Identity, y = RT_mean, group = Valence), position = position_jitter(width = 0.1), alpha = 0.5) s3 但是我们发现数据点还是没有完全错开,moral和immoral两种条件下面的点还是混杂在一起。那么应该如何得到规律的抖动呢?可以看到上面的点是根据x轴的两个条件在进行抖动错开,实际上我们需要点根据moral和immoral进行抖动错开,因此我们需要设置新的基线。 我们加入新的位置变量conds。首先我们需要明白,虽然坐标轴上我们看到的x是self和other,但实际上它们真实的值是0和1,因此我们需要设置4种情况,self下的两种情况下,坐标分别是1±0.12;而other条件下则是2±0.12。 df.match.plot <- df.match.subj %>% dplyr::mutate(conds = case_when(Identity == "Self" & Valence == "moral" ~ "0.88", Identity == "Self" & Valence == "immoral" ~ "1.12", Identity == "Other" & Valence == "moral" ~ "1.88", Identity == "Other" & Valence == "immoral" ~ "2.12"), conds = as.numeric(conds)) 接下来讲conds作为x变量来画被试个体数据的点图。这时候点的基线就变成了conds下面的4种条件,而不是原来的self和other这两种基线。 s4 <- s1 + ggplot2::geom_point(data = df.match.plot, aes(x = conds, y = RT_mean, group = Valence), position = position_jitter(width = 0.08), alpha = 0.5) s4 这样以来,再把个体点图叠加到原来的整体线图上时,看上去就好像处于以整体的值为中心在进行变化。至于0.12这个偏差值如何设置呢?最好的办法就是多去试,然后看那个值最合适。 当我们画出被试散点之后,我们还希望看到被试个体的趋势,因此我们在s4的基础上再将被试的条件值连接起来。 s5 <- s4 + ggplot2::geom_line(data = df.match.plot, aes(x = conds, y = RT_mean, group = Sub), linetype = 1, size=0.8, color="#000000", alpha=0.1) s5 可以看到线和点的对应关系不是很好,线很齐而点很散,导致线的开头和点没有很好地对应在一起。那么如何使点和线正确连接呢?我们需要将点和线放在同一段代码下进行绘制,并且设置一致的dodge值,同时我们将个体值的透明度降低,让整体值的颜色更加凸显。 s6 <- s1 + ggplot2::geom_point(data = df.match.plot, aes(x = conds, y = RT_mean, group = as.factor(Sub)), position = position_dodge(0.08), color="#000000", alpha = 0.05) + ggplot2::geom_line(data = df.match.plot, aes(x = conds, y = RT_mean, group = as.factor(Sub)), position = position_dodge(0.08), linetype = 1, size=0.8, color="#000000", alpha=0.05) + ggplot2::labs(y = "RT") s6 这时就可以看到连接在一起的被试的点和线。这是ggplot画图的一个好处,当在同一个图层上叠加position和dodge时,不同几何图形之间的抖动错开是会进行自动对应的。同理可得ACC的图如下。 包含了个体数据的图信息更为全面,并且方便我们去判断实验操作的真实效果。可以看到虽然对于实验来说整体操纵是有效的,但对每一个被试来说心理学的操纵并不一定是有效的,数据在不同被试间的变异性是很强的。这和前面几章讲到的层级模型的思想是一致的,即既要捕捉整体趋势,也要捕捉个体趋势。 我们继续使用patchwork进行拼图,然后保存为pdf,保存的时候可以调整图片的宽高,因为pdf保存的图片是矢量图,因此用于投稿或者插入文章都是可行的。 s9 <- s6 + s8 + plot_layout(guides = "collect") s9 # 保存为pdf更加清晰 ggplot2::ggsave(filename = "./pic/chp12/p1.pdf", plot = s9, width = 8, height = 4) 12.3.2 可视化层级模型的random effect 我们之前提到过层级模型的随机效应,这里我们简单展示了如何将每个被试random effect中的截距进行可视化。 我们选取12名被试的数据进行一个简单的建模,随机效应只加入一个随机的截距。 #此处选择12个被试是为了在展示的时候更清晰 sublist <- unique(df.match.trial$Sub) target2 <- df.match.trial %>% dplyr::filter(Sub == sublist[1:12]) %>% dplyr::mutate(Label = factor(Label, levels = c("moralSelf", "moralOther", "immoralSelf", "immoralOther")), Sub = factor(Sub)) model <- lme4::lmer(data = target2, RT ~ Identity * Valence + (1 |Sub)) 接着我们使用ranef(model)$Sub将模型中被试的随机效应提取出来,并对”(Intercept)“进行重新命名。标准差可以从variance covariance matrix中提取出来,因为只有一个随机效应,因此用sqrt(diag(vcov(model))[1]就可以直接提取然后开平方变成标准误。接着为了美观,使用arrange()进行排序,factor()用于将被试这一数字变量因子化,否则排序将会没有效果。 # 提取随机效应 ranef_df <- as.data.frame(ranef(model)$Sub) %>% dplyr::mutate(Sub = row.names(.)) %>% dplyr::rename(Intercept = "(Intercept)") %>% dplyr::mutate(se = sqrt(diag(vcov(model))[1]), lower = Intercept - 1.96 *se, upper = Intercept + 1.96 *se) %>% dplyr::arrange(Intercept) %>% dplyr::mutate(Sub = factor(Sub, levels = .$Sub)) 接着我们使用ggplot绘制森林图。geom_vline()函数用于绘制x = 0上面的虚线。 # 绘制森林图 ranef_df %>% ggplot2::ggplot(., aes(x=Intercept, y=Sub)) + ggplot2::geom_point(size = 2) + ggplot2::geom_errorbarh(aes(xmax = upper, xmin = lower), height = .2, color = 'grey') + ggplot2::geom_vline(xintercept = 0, linetype = 2) + # ggplot2::facet_wrap(~ variable, nrow = 1) + # 按照对象分面 papaja::theme_apa() 森林图中点距离虚线的距离,代表的就是被试偏离整体intercept的多少,例如7313被试intercept和整体偏差了100ms的反应时,这在反应时上是一个相当大的差距;再比如7313和7314被试的intercept差距达到了200ms,这是一个更大的效应,足以说明被试之间的差异是很大的。 12.3.3 雨云图(rain cloud plot) 为了进一步展示数据的分布情况,可以使用雨云图来进行展现。当研究者提出呈现个体数据重要性的时候,雨云图得到了广泛的引用,这一小工具甚至可以说改变了整个科研界使用可视化的方式。 雨云图的代码讲解在此不做赘述,请读者自行运行下面这些include=FALSE的代码。 12.4 高级图片处理–magick ggplot生成的图像有时需要进一步手动修改(如修改图片格式、图片拼接等),也可能需要批量修改一些通过其他途径得到的图,R仍然可以处理:magick包可以应用于所有常见图片操作(甚至包括PDF),具体功能可以参考相关文档(https://search.r-project.org/CRAN/refmans/magick/html/magick.html),在这里我们仅以图片剪裁与拼接为例。 假设我们希望这两张图变为横向排版,那么首先需要对图片进行剪裁,然后进行横向拼接。 首先我们读取和查看这张本地图片的信息。 ## 读取图片;图片可以是本地,也可以是图片的网址链接 img = magick::image_read('pic/chp6/pr1.png') ## 查看图片相关信息 img %>% magick::image_info() ## # A tibble: 1 × 7 ## format width height colorspace matte filesize density ## <chr> <int> <int> <chr> <lgl> <int> <chr> ## 1 PNG 870 977 sRGB FALSE 92033 72x72 下面需要根据图片的width 和 height ,使用magick::image_crop()进行裁剪,geometry参数接受一个字符串,来对剪裁区域进行定位,比如\"850x480+10+10\"。这个字符串包含两个部分: 第一部分:包含图片剪裁的长和宽(单位可以是百分比,但下面会使用像素),即\"850x480\"(注意:其中连接符为小写字母x),大概指右图中红色线条; 第二部分:包含起始点位置,即\"+10+10\",意思是从左上角顶点向右10个像素,向下10个像素,大概对应右图中灰色点的位置,如果不写默认使用+0+0(即左上角顶点)。 我们使用下面代码对图片进行剪切。 img %>% magick::image_crop('850x480+10+10') 接下来我们将下半部分的图也剪切下来,然后进行拼接。 img1 = img %>% magick::image_crop('870x488') img2 = img %>% magick::image_crop('870x488+0+485') ## 使用image_append进行拼接,令stack = F进行横向拼接(T为竖向) img3 = image_append(c(img1,img2),stack = F) #<< img3 %>% print() ## # A tibble: 1 × 7 ## format width height colorspace matte filesize density ## <chr> <int> <int> <chr> <lgl> <int> <chr> ## 1 PNG 1740 488 sRGB FALSE 0 72x72 magick还有一些其他的功能,可以参考下面的代码。 #### NOT RUN #### # 保存图片到本地 image_write(image = img3,path = 'your path') # 修改尺寸(可以以像素为单位,这里以百分比为例) image_scale(img1,'40%') # 旋转 image_rotate(img1,90) # OCR(这里以英文为例,中文的识别率经测验确实不太行😢) image_read("http://jeroen.github.io/images/testocr.png") %>% image_ocr() %>% cat() 有读者可能会问,为什么要用magick而不用Photoshop来进行手工操作呢?第一,用代码可以实现精确的复制,不会每次都产生一点手工的误差;第二,加入实验过程中又收了一个被试,实验图片发生了一些微小的变化,如果用PS就要全部手工操作一遍,而用代码只要原样跑一遍即可。 这里也提供了一些ggplot2的参考网页,读者可以自行阅览。 ggplot2常用参数与函数汇总:https://zhuanlan.zhihu.com/p/637483028 ggplot2位置调整参数:https://zhuanlan.zhihu.com/p/409489632 ggplot2主题总结:https://zhuanlan.zhihu.com/p/463041897 ggplot2分面总结:https://zhuanlan.zhihu.com/p/225852640 patchwork常用功能:https://zhuanlan.zhihu.com/p/384456335 "],["第十三讲基于网络模型的心理学研究.html", "Chapter 13 第十三讲:基于网络模型的心理学研究 13.1 基于潜变量的心理学研究 13.2 潜变量模型在心理学的困境 13.3 基于网络的心理学视角 13.4 如果寻找可观测特征的因果关系 13.5 DAG(贝叶斯网络) 13.6 高斯图模型(GGM) 13.7 易辛模型Ising model 13.8 心理网络的估计 13.9 模型选择 13.10 心理网络的挑战 13.11 我们是否还需要潜变量模型", " Chapter 13 第十三讲:基于网络模型的心理学研究 杜新楷 本章主要围绕基于网络模型的心理学研究。 首先,会讨论基于潜变量模型的心理学研究。经过长时间发展,潜变量模型已经相对成熟,为什么我们还要使用一种新的模型——网络模型,他可以给我们带来什么新的视角和独特的思路?然后介绍基于网络的心理学理论,如何估计、选择网络模型,以及在R中如何将其实现。最后介绍在网络模型发展的情况下,潜变量模型的意义。 knitr::include_graphics('pic/chp13/page 3.png') 13.1 基于潜变量的心理学研究 首先,心理学有一个稳定的现象,什么东西之间都有相关的。比如,在智力的研究中,有一个非常稳定的现象positive manifold,是在探讨,测量智力的所有东西,哪怕测量的是不同的东西,比如测量数学能力、英语能力,他们之间也会有正相关,而且这个正相关非常普遍,他们也是在解释这些潜变量,低阶的潜变量之后还会发现一些没有被解释的变异,因而就提出了一个潜变量模型,就是g factor model。 在精神病理学中,也有一种非常常见的现象,commorbidity,是指并发症的意思,指精神科病人往往不只有一个病,有抑郁的人往往有焦虑,而有焦虑的人往往有抑郁,很少说一个人只有焦虑或抑郁,单一的诊断其实非常少见。过去基于潜变量的心理学使用共因来解释观测到的特征之间的相关,认为这些可观测的特征是潜变量的结果;可观测特征之间的相关是潜变量造成的,在控制潜变量之后,这些可观测特征之间的相关应该是消失的,这个是根据因果推断中的共因原则。 另外对可观测特征的干预是没有办法改变潜变量的值的。比如在感冒的时候,我们往往会打喷嚏还头疼,还咳嗽,这个时候我们不能说吃止痛药或者是吃止咳药,他们都无法治疗感冒,这些只能减轻症状,但不能让感冒好起来,最终还是需要通过诊断来确定感冒是病毒性感冒还是其他原因,然后针对具体的感冒类型选择治疗方案,如抗病毒或是消炎。这是我们不需要治疗打喷嚏或咳嗽,研究打喷嚏、咳嗽等可观测的特征是没有意义的。 knitr::include_graphics('pic/chp13/page 4.png') 13.2 潜变量模型在心理学的困境 潜变量模型在心理学研究中的困境是,心理学研究对象往往是概念,比如人格等,如果依据潜变量定义的话,他们是看不见摸不到的。 事实上,在可观测的潜变量特征之间存在有意义的因果关系,才导致他们之间存在相关。比如说睡不着会导致没精神,而没精神又会导致注意力不集中,而注意力不集中又导致自责,而自责之后又导致睡不着,然后又没精神……周而复始、恶性循环,最终可能会导致抑郁。上述即是网络模型的视角。 knitr::include_graphics('pic/chp13/page 5.png') 13.3 基于网络的心理学视角 在网络模型的视角下,可观测特征之间的关系是有意义的,对可观测特征的干预也是有效果的。比如在对病人的分析时发现,病人很多疾病或症状的源头是睡不着觉,就可以给病人开安眠药,在服了药睡眠质量提升一周后,可能发现他的其他症状也会有明显改善。类似的,有些人难以建立友谊或恋爱关系,这时鼓励他和别人交流,多参加社交场合,从而有机会认识更多的人。 所以,基于网络的心理学视角,会更着眼于可观测的问题,然后探索他们之间的因果,关注可观测特征之间的相互作用。网络分析中可观测特征和心理学概念之间的关系与潜变量是相反的:潜变量的因果方向是从潜变量到可观测的特征,但是在网络分析的视角中,则是可观测变量间相互作用,然后浮现、上升到具体的心理学现象。 knitr::include_graphics('pic/chp13/page 6.png') 13.4 如果寻找可观测特征的因果关系 那么我们如何寻找可观测特征之间的因果关系呢?这方面的研究则要归功于Judea Pearl,他做了非常多的有关因果推断的工作,发现我们可以从变量之间的偏相关中推断变量间因果关系,这个方法被称为d-separation rule。并且,他发现了三种原始因果图示,所有的偏相关来推断因果都可以归结为三种基本的图式(共因、链、对撞因子)。 knitr::include_graphics('pic/chp13/page 7.png') 13.4.1 d-separation 第一种图式是共因(Common cause),是指两个变量A和C具有一个共同原因B:如果在没有B的情况下,A和C之间存在相关,但一旦引入并控制B时,A和C的相关就会消失。B还有另外一个名字叫干扰变量(confounding factor),和心理学中很多的“假发现”有关系。我们可能发现两个变量之间存在“虚假相关(Spurious relationship)”,这是由于没有控制另外一个变量(B)导致的。例如,一个村子中小孩的数量和鸟的数量存在正相关,但这不是因为孩子会生鸟或鸟会生孩子,这是因为无论鸟的数量还是孩子的数量都与村子大小有关——村子更大会使得孩子和鸟都会更多。此外,B到A或C之间的箭头方向,反应了因果的方向(见图)。 第二种图式是链(Chain,也叫中介(Mediation))。我们以吸烟会导致癌症为例,吸烟本身不会导致癌症,而是烟中的焦油致癌,因此如将烟中的焦油进行过滤,那么吸烟的致癌率会大大降低。在上述过程中控制焦油的操作,也是我们在做中介分析中会做的事。在中介分析种,首先会分析A和C之间的相关,随后引入B,观察A和C之间相关关系的变化,会不会消失或变小。 第三种图式是对撞(Collider),是指A和C之间本来不相关,但是在引入B之后会产生相关,并且这种相关永远是负相关。举一个例子,A和C两个人去射击,如果已知A开枪或没开抢,这个信息对于知道C是否开枪是没有任何帮助的;但如果A和C是要处决B,且B已经死亡,即控制第三个变量(即B是否死亡),这个时候就可以通过C是否开枪来判断A是否开枪,因为这时如果不是C开的枪那么就一定是A开的枪,此时A和C之间就会产生负相关。 knitr::include_graphics('pic/chp13/page 8.png') 13.5 DAG(贝叶斯网络) 上述三种图示体现了有向无环图(Directed Acyclic Graph, DAG)中的基本建构。在网络中,圆圈内的字母表示某一可观测的特征,称之为节点(node),而不同圆圈(可观测特征)之间的连接线称之为连边(edge),连边上的箭头则表示因果关系的方向,DAG通过拼接因果关系的图示来寻找边,他们的起点永远是一个偏相关网络。 此外,Collider具有唯一解。如果A和C之间没有相关,但是引入B之后存在相关,就可以推断这一定是一个Collider,也一定知道方向一定是从A到B,从C到B。但如果是Common Cause或Chain的时候,这两个偏相关模式是一样的,没有办法进行区分。A和C之间本来有相关,引入B之后没有相关,B到A和A到B在统计上是没有办法区分的,这也会导致DAG有非常多等价模型的问题,但好处是在模型有唯一解的情况下,可以很直接的看到因果关系的方向。同时,我们也可以通过总结的方式来进行观察,在多少个循环里、或在多少个样本里的方向到底如何,但具体确认还是很困难。 DAG在R中可以使用bnlearn进行实现,教程见引用文献。 knitr::include_graphics('pic/chp13/page 11.png') 贝叶斯网络除了等价模型的问题,还有一个无环假设,因为如果在模型中引入双向因果,则模型无法识别,必须无环假设来对模型进行估计。但这是一个强假设,在心理学中正反馈与负反馈是非常常见的,这也对探索性研究非常不利。刚才提到,由于只能通过collider来确定因果方向,其他图示无法判断。并且如果模型中的节点越多,其等价模型也就越多,几乎没有办法进行判断应选择哪个模型 knitr::include_graphics('pic/chp13/page 9.png') knitr::include_graphics('pic/chp13/page 12.png') 13.6 高斯图模型(GGM) 因此,我们经常退一步,选择偏相关矩阵代表网络,这也是PC-algorithm的起点;在高斯图模型中,我们放弃了因果推断的方向,而使用理论或常识来自行判断来做探索性的研究。由于放弃了因果方向,因而模型有唯一解,同时没有无环假设,可以以此来指导哪两个变量之间存在正反馈或负反馈的关系。同时它也有一个很fancy的名字叫马尔科夫随机场,因为偏相关矩阵具有马尔科夫特性。 knitr::include_graphics('pic/chp13/page 13.png') 13.7 易辛模型Ising model 另外一个是易辛模型,适用于变量为二元变量。它实际上是一个物理模型,用来判断电磁场的能量,现在也被应用于心理学中来做模型推演和理论推导和数据分析。 knitr::include_graphics('pic/chp13/page 14.png') 易辛模型在数学上与IRT模型是等价的。即网络模型和潜变量模型在数学上是等价的,它的好处在于可以复现同样的数据,也少了潜变量的假设。 13.8 心理网络的估计 13.8.1 高斯图模型 对于高斯图模型,有两种估计方式,多元估计与医院估计。 在网络中,一个一个的圈圈叫做节点,他们之间的相关或关系叫做连边。如果一次性将网络中所有的点和边都估计出来,叫做多元估计(Multivariate estimation),通常使用最大似然法。 如果一个一个的估计,即使用多个多元回归,一次使用一个节点作为因变量,其他所有节点作为自变量,最后对结果取平均数,即一元估计(Univariate estiamtion)。 knitr::include_graphics('pic/chp13/page 17.png') 13.8.2 多元估计 13.8.2.1 最大似然估计 对于多元估计,一种方法是最大似然法。最大似然法简单来说,就是尝试各种参数的组合,看哪个参数组合能与数据相匹配的概率。似然可以用方程表示,最大似然即寻找方程的最大值(计算一阶导并等于0求解)。当数据遵循方差矩阵为∑的多元正态分布时,拟合函数如下图。 knitr::include_graphics('pic/chp13/page 18.png') 最大似然法的优势在于可以使用Full information maximum likelihood处理缺失值,而无需删除样本;还有一个是不需要先验分布,可以省去很多麻烦事情,也不会得到有偏估计。 knitr::include_graphics('pic/chp13/page 19.png') 13.8.2.2 贝叶斯估计 贝叶斯估计也是一种多元估计的方法(使用包BGGM)。基于马尔科夫链蒙特卡洛(MCMC)对参数后验分布取样,这就如同一个人走路,人往左走往右走的概率是由后验来决定的,他每走一个地方就在脚底垫一块砖,走的次数多的地方砖就比较高;最后走完了之后,砖就会摞成一个分布,这其实就是MCMC。这种方法也有很多优点:首先速度快;其次可以使用先验分布,如果有之前的也研究,可以基于之前研究的分布继续进行;最后,由于基于贝叶斯,可以进行非常多样的比较,贝叶斯的假设检验非常灵活,这一点是频率估计是无法做到的。BGMM的教程可参考图中链接。 knitr::include_graphics('pic/chp13/page 20.png') 13.8.3 一元估计 一元估计就是先估计Y1,然后把Y2、Y3、Y4作为自变量,以此类推,每个连接都有两个估计,最后根据AND/OR 的规则进行总结。 knitr::include_graphics('pic/chp13/page 24.png') 13.8.4 代码实现 13.8.4.1 bootnet包 # load libraries pacman::p_load(bootnet, qgraph, psychonetrics, tidyverse, BGGM) # load data data("bfi") # preprocessing ----------------------------------------------------------- # only use the first 25 items bfi <- bfi[, 1:25] bfi_na.rm <- na.omit(bfi) # estimate unconstrained networks ----------------------------------------- ##----------- ## bootnet ##----------- # estimate net_boot <- bootnet::estimateNetwork(data = bfi_na.rm, default = "pcor") ## defalut 为估计网络的方法,这里使用偏相关,别的方法可参考帮助文档 # store the ggm graph_boot <- net_boot$graph # Item descriptions Names <- scan("http://sachaepskamp.com/files/BFIitems.txt", what = "character", sep = "\\n") # Form item clusters Traits <- rep(c( 'Agreeableness', 'Conscientiousness', 'Extraversion', 'Neuroticism', 'Opennness' ),each=5) # plot the network qgraph(graph_boot, layout = "spring", theme = "colorblind", groups = Traits, nodeNames = Names, legend.cex = 0.4) 13.8.4.2 psychonetrics包 ##----------------- ## psychonetrics ##----------------- # FIML(full information likelihood maximum) # FIML handles missing data net_psy <- psychonetrics::ggm(bfi, estimator = "FIML") %>% runmodel ### 注意这里typeof(net_psy) 为S4对象,提取信息不用$,而是用@ ### 比如net_psy@submodel查看使用什么模型 # store ggm graph_psy <- psychonetrics::getmatrix(net_psy, "omega") # plot the network qgraph(graph_psy, layout = "spring", theme = "colorblind", groups = Traits, nodeNames = Names, legend.cex = 0.4) 13.8.4.3 BGGM ##-------- ## BGGM ##-------- # handles missing data with mice imputation by default net_bggm <- BGGM::explore(bfi) ## 也可使用estimate graph_bggm <- net_bggm$pcor_mat qgraph(graph_bggm, layout = "spring", theme = "colorblind", groups = Traits, nodeNames = Names, legend.cex = 0.4) # compare the three networks L <- averageLayout(graph_boot, graph_psy, graph_bggm) layout(t(1:3)) qgraph(graph_boot, title = "bootnet", layout = L) qgraph(graph_psy, title = "psychonetrics", layout = L) qgraph(graph_bggm, title = "bggm", layout = L) dev.off() # compare parameter estimations plot(c(graph_boot), c(graph_psy), xlab = "bootnet", ylab = "psychonetrics") abline(coef(lm(c(graph_boot) ~ c(graph_psy)))[1], coef(lm(c(graph_boot) ~ c(graph_psy)))[2]) plot(c(graph_bggm), c(graph_psy), xlab = "bggm", ylab = "psychonetrics") abline(coef(lm(c(graph_bggm) ~ c(graph_psy)))[1], coef(lm(c(graph_boot) ~ c(graph_psy)))[2]) 13.9 模型选择 以上模型都是饱和模型,都没有经过选择,即每一个观测都有一个对应的参数,不仅很难解释,而且会导致过多的假阳性,很多的相关都不显著,因而需要对模型进行选择。 knitr::include_graphics('pic/chp13/page 30.png') 在模型选择中有一个原则称为奥卡姆剃刀,即在拟合相似模型中选择最简单的,因为其推广性最好,也容易解释。在网络模型中基本上都要去做模型选择来简化模型,留下有解释意义的参数。 knitr::include_graphics('pic/chp13/page 31.png') 13.9.1 模型选择的方式 模型选择有很多方式:阈值法(thresholding)、剪枝法(pruning)、正则法(regularization)和模型搜索法(model search) 13.9.1.1 阈值法&剪枝法 阈值法与剪枝法比较像,但是有一些区别。 阈值法是将某些不符合标准的连接直接给藏起来,但也仅仅只是藏起来,并不会重新估计这个模型;而剪枝法会将不符合标准的连接设置为0,并对模型进行重新估计。 而对标准来说,可以是bootstrapped的P值,或是false discovery rates(FDR),或是贝叶斯因子;在一元估计中的AND/OR规则也可以用与模型选择。 knitr::include_graphics('pic/chp13/page 33.png') 13.9.2 代码实现 13.9.2.1 阈值法&剪枝法 ##---------------- ## Thresholding (阈值法) ##---------------- #------------------- bootnet -----------------------# # bootnet: sig(依据p值选择,具体threshold参数见帮助文档) net_boot_thresh_sig <- estimateNetwork(bfi, default = "pcor", threshold = "sig", alpha = 0.01) qgraph(net_boot_thresh_sig$graph, layout = L, title = "threshold alpha = .01") qgraph(net_boot$graph, layout = L) ## 或者可以先估计,再选择 # bootnet: boot ## nCores设置使用CPU核心数,nBoots设置重抽样数量,默认1000 net_boot_booted <- bootnet::bootnet(net_boot, nCores = parallel::detectCores(), nBoots = 100) net_boot_thresh_boot <- bootnet::bootThreshold(net_boot_booted, alpha = 0.01) qgraph(net_boot_thresh_boot$graph, layout = L, title = "threshold boot .01") #------------------- BGGM -----------------------# # BGGM: threshold with credible interval(使用select函数) net_bggm_thresh_ci <- BGGM::estimate(bfi) %>% select qgraph(net_bggm_thresh_ci$pcor_adj, layout = L, title = "bggm threshold ci") ## 注意select.estimate和select.explore不一样 ## select.estimate 根据95%CI进行选择,即删去包含0的边 ## select.explore根据贝叶斯因子小于3的标准进行排除 # BGGM: threshold with BF net_bggm_thresh_bf <- BGGM::explore(bfi) %>% select qgraph(net_bggm_thresh_bf$pcor_mat, layout = L, title = "bggm threshold bf") ##----------- ## pruning (剪枝法) ##----------- net_psy_prune <- psychonetrics::ggm(bfi, estimator = "FIML") %>% ## recursive 可以设置是否迭代进行 psychonetrics::prune(alpha = 0.01, recursive = TRUE) %>% runmodel qgraph(getmatrix(net_psy_prune, "omega"), layout = L, title = "psychonetrics pruned") 不同标准的模型选择结果会有一些不同,大家在解释的时候可以看一看哪一些发现比较稳定。阈值与剪枝的有点在于速度快,不需要估算很多的模型;可以有固定的假阳性率,是实际可控的(与正则法相比);其估计值是无偏的,即期望值等于真值;比较保守,假阳性率不高。 缺点在于:阈值法本身不是一种模型选择,只是把边给藏起来了;在小样本时不够精确。 knitr::include_graphics('pic/chp13/page 35.png') 13.9.2.2 正则法 正则法在似然函数中添加罚项\\(\\lambda\\),会把很大的偏相关给惩罚掉(即LASSO,Least Absolute Shrinkage and Selection Operator),让0多一点。而控制惩罚程度(罚项)的参数\\(\\lambda\\)也可以使用EBIC(Extended Bayesian Information Criterion)来选择,即\\(\\lambda\\)控制罚项,而\\(\\gamma\\)控制\\(\\lambda\\),\\(\\gamma\\)越大,对模型复杂度的惩罚越大,网络也就越稀疏(EBICglasso)。 在一元回归的时候,同样可以使用LASSO回归来惩罚偏相关;也可以使用交叉验证的方法,推荐阅读mgm的手册。 knitr::include_graphics('pic/chp13/page 36.png') 13.9.3 代码实现 13.9.3.1 正则化(EBICglasso) ##------------------ ## regularization ##------------------ net_boot_reg <- estimateNetwork(bfi_na.rm, default = "EBICglasso", tuning = 0.5) ## 注:对于不同的模型估计时tuning默认值不同,见帮助文档 ## 这里运行时提示Warning,提示选择了密集网络 ## EBICglasso会假设网络是稀疏网络,但也可能真实网络确实是密集的 qgraph(net_boot_reg$graph, layout = L, title = "bootnet EBICglasso") 正则法的优点是同样很快;另外一个独特的优点是非常适合小样本,阈值与剪枝两种方法并不适合小样本,在模拟数据中,N = 50时就能还原出很不错的网络;结果也很清晰,将不太显著的边直接压缩为0。 但是这种方法在大样本时表现差;同样正则法假设网络本身是稀疏的,如果使用正则法,它永远会将一些连边压缩为0,哪怕这些连边的真值不为0,如果我们的理论假设是这个网络应该是密集网络,这时就不应该使用正则法;最后,正则法没有固定的假阳性率,不同于前面两种方法,具体假阳性率是未知的。 knitr::include_graphics('pic/chp13/page 38.png') 13.9.3.2 模型搜索 模型搜索的方法应该是最好的方法,但也是最慢的。其逻辑是使用不带正则的最大似然估计不停调整并重新估计模型,直到选择最优解。比如简单的step-up方法,如下图所示,首先从空模型开始,然后加一个modification index(结构方程模型中的调整系数),每次把调整系数最大的连接给加上,然后再重新估算一次模型,并且比较模型拟合是否有提升;然后再在新的模型中找到新的调整系数,再计算、比较模型,直到模型拟合不再提升。 knitr::include_graphics('pic/chp13/page 39.png') knitr::include_graphics('pic/chp13/page 40.png') psychonetrics::modelsearch更加麻烦,具体见下图。 knitr::include_graphics('pic/chp13/page 42.png') knitr::include_graphics('pic/chp13/page 41.png') qgraph::ggmModSelect与modelsearch很相似,但是是从正则化网络出发的。 knitr::include_graphics('pic/chp13/page 43.png') knitr::include_graphics('pic/chp13/page 44.png') 13.9.4 代码实现 13.9.4.1 模型选择 ## 从零模型开始搜索(step-up) mod <- psychonetrics::ggm(bfi_na.rm, omega = "zero") %>% runmodel %>% stepup net <- getmatrix(mod,"omega") ## 从被剪枝后的模型开始搜索(modelsearch) mod <- psychonetrics::ggm(bfi_na.rm) %>% runmodel %>% prune(alpha = .01) %>% stepup ## ggModSelect net_ggmModSelect <- estimateNetwork(bfi_na.rm, default = 'ggmModSelect', corMethod = 'spearman') 13.9.5 总结 阈值和剪枝法就是将饱和模型里不合标准的连结给隐藏起来,或者是设置为0再重新估计。好处是速度快,拥有固定的假阳性率,同时是一个无偏估计。但缺点是它并不选择模型,在小样本中表现不好,尤其在小样本的时候可能发现不了真模型。 正则法就是在最大似然里惩罚模型复杂度,好处也是比较快,但它是适合小样本的,结果更容易解释。但缺点是它的估计永远是有偏的,这是正则估计的特性,因为他的表现不规律,它最后估计的期望不一定是真值。但在网络中,我们不解释连结的weight(表现为连边的粗细),仅说明是正连边还是负连边。 模型搜索,使用反复的方式来优化指标,一个个搜索反复添加、删除,直到找到最优的模型。好处是同时照顾power与假阳性的概率,是一个无偏估计。因为模型搜索检查的模型更多,所以它更可能converge到真模型。坏处是,速度很慢,在小样本中表现不好。 总而言之,小样本使用正则法,大样本使用模型搜索。Isvoranu和Epskamp(2023)的文章中有对样本大小的说明,以及其他更全面的解释。 knitr::include_graphics('pic/chp13/page 45.png') 13.10 心理网络的挑战 心理网络本身也面临一些挑战。 首先,网络理论不等于网络模型,心理学理论和模型之间的关系也有很多值得讨论的。DAG与GGM,二者谁才是最符合网络理论的网络模型?还是近期的新模型,一种有向有环的模型,它可能更好的符合网络理论的统计模型。 此外,Collider比较麻烦,它会在网络里面表现出一些虚假的负连结,因此如果网络里面如果出现一些负连接一定要小心,在解释的时候要看它符不符合逻辑。 还有在选择样本的时候,一定不能使用提前选择后的样本,比如只看有精神疾病的对象(e.g.只选取量表中得分超过10分的人),这会人为的引入collider effect。但最近有文章在估计时可以纠正这种Berkon’s bias,因此这么做也可以。 心理网络的中心度存在问题的,它在社会网路中中心度有着比较可靠的解释或数据基础,但在心理网络中,中心度并不好解释,如betweenness 和 closeness的可重复性并不太好,唯一一个比较好的可能就是degree centrality。 使用高斯图因果推断也存在问题,因为在使用时并不知道GGM对应的真正的因果产生机制(真模型)是什么——由于GGM牺牲了方向,因而真正的因果方向是不知道的。近期也有一些替代GGM的办法,但其可靠性值得进一步研究。 knitr::include_graphics('pic/chp13/page 48.png') 13.11 我们是否还需要潜变量模型 答案是确定的。很多人觉得使用网络模型就很讨厌潜变量模型,这其实是一个很不好的刻板印象。 潜变量模型首先对测量误差的估计是无可替代的,可以在研究时控制住测量误差,其他任何方法都不行,只有基于潜变量模型才可以。 同时,潜变量模型需要更小的样本,而网络模型需要的样本量就大多了。 对于IRT,对于量表中题项的评估也是无法被代替的;同样,基于IRT的计算机自适应测验的方式在网络模型中也没有被发展出来。 基于Network的心理测量其实也不是很全面,大家可以看看下面两个人的文章(见下图)——基于网络的心理测量应该怎么做,定义是什么样,其实都不太一样。基于网络模型的量表,跟基于潜变量模型的量表也不太一样。 什么时候用网络模型: 在数据探索时,试图想要发现一些稳定的现象时,网络分析是一个不需要前提理论的探索性方法。 当研究对象是可观测特征之间的动态关系时,如做临床中可以通过网络模型,可以直接看出治疗效果在哪一个症状上体现出来,这种动态的关系是否发生改变。这时网络分析所显示出的结果要比潜变量模型要多很多。 当有理论来支持可观测特征之间的因果关系时,这时候用网络模型更好 knitr::include_graphics('pic/chp13/page 49.png') "],["第十四讲心理学元分析入门.html", "Chapter 14 第十四讲:心理学元分析入门 14.1 什么是元分析 14.2 元分析的实施 14.3 回顾与总结", " Chapter 14 第十四讲:心理学元分析入门 大家好,我是刘铮。很高兴受到传鹏的邀请,有机会在这里与大家分享一些心理学元分析的知识。虽然我不是这方面的专家,但很乐意借此机会和大家交流,共同学习。 目前我正在香港中文大学深圳校区攻读应用心理学博士学位。今天我想和大家探讨的主题是“心理学元分析的入门及其应用、设计与实施”。 正如传鹏所说,进行元分析时,对我来说,理论知识固然重要,但实际操作更能加深理解。实际上,元分析的代码编写并不复杂,其背后的理论、框架和流程更为关键。因此,这节课对R语言代码的要求不会像之前那么高,相对结构方程模型等,代码会更加简单。 课程的前半部分,大家可能不需要使用R语言,主要是听我讲解。到了后半部分,我们会一起使用数据,在R语言环境中逐步操作,体验整个元分析的过程。希望通过这节课,大家能够对进行元分析充满信心,这是我的教学目标。OK,让我们开始吧。 14.1 什么是元分析 好的,让我们开始今天的课程。首先,我想对元分析做一个简单的介绍。尽管我知道心理学背景的学生可能对元分析有一定的了解,毕竟在许多期刊上我们都看到过元分析的研究发表。不过,我还是想再次强调一下元分析的定义。 元分析是一种统计方法,它通过定量手段综合多个研究的结果,将众多研究的效应量合并为一个单一的数字,并提供其他统计量来衡量整体效应。这种方法能够帮助我们更准确地理解某一研究领域的问题。 例如,在心理学中,我们经常探讨两个变量a和b之间的关系,比如a是否对b有影响。传统的研究方法可能涉及实验设计或问卷调查,并基于样本量得出结论。假设我们发现a对b有一个小的效应量,比如0.2。其他研究者可能在不同群体中进行类似的研究,比如在欧洲人中的效应量可能是0.5。如果总共有k个这样的研究,元分析就能综合这些研究的证据,提供一个更精确的答案,即a是否真的对b有影响。 此外,元分析还可以帮助我们探索不同研究中可能存在的调节效应。例如,如果不同的研究收集了不同的被试群体,我们可以通过元分析来观察被试群体的不同是否会对a和b之间的关系产生影响。这也是元分析能够做到的事情。 通过元分析,我们不仅能够得到更可靠的结论,还能够识别出影响效应大小的潜在因素,从而加深我们对研究领域的理解。现在,让我们进一步探讨元分析的具体应用和实施步骤。 当我们提到元分析时,经常会与综述和系统性综述一起讨论。这三个概念之间的关系其实很清晰。首先,综述通常是探索一个较宽泛问题的过程,比如在撰写毕业论文时,你可能会写一篇综述来概述你的研究范围。综述通常是定性的,不涉及对过去数据的统计分析,也不需要明确地告诉读者你系统地搜索了多少文献以及如何得出结论。 系统性综述则更加严格,它关注的问题范围更小,既可以是定性的也可以是定量的。进行系统性综述时,你必须有一个明确的搜索策略,比如研究a对b的关系时,你需要包含所有已发表的关于这一关系的论文。你可能基于一定的标准排除一些文章,然后对剩余的论文进行总结,比如通过表格来展示每篇论文的结论。系统性综述可能会定性地综合这些结论来讨论a对b的关系。 元分析则是系统性综述的进一步发展。在元分析中,你需要从通过搜索策略找到的论文中提取可用的研究数据,并进行统计分析,以量化的方式回答a对b关系的问题。元分析会计算统计学上的显著性,提供关于效应量大小的估计。 综述的范围最广,系统性综述是综述中的一个更聚焦的部分,而元分析则可以是系统性综述的一部分,也可以独立进行。我们经常看到元分析与系统性综述一起出现在论文中,但元分析也可以单独进行,不一定需要先做系统性综述。 元分析的两个基础功能我已经在定义时提到。首先,它能够综合一个领域的整体效应,回答诸如a对b是否有影响的问题。此外,元分析还可以检验效应的边界条件,即调节效应,比如探讨是否存在某些变量(如c、d、e、f)可能对a和b之间的影响具有调节作用。 但元分析的功能远不止这些。在心理学领域,元分析的应用已经变得非常多样化。例如,在我们研究的风险决策领域,我经常看到一些高级文章使用元分析来汇聚大量数据,以检验不同的理论预测。如果一个理论预测人在某种情况下是风险偏好的,而另一个理论预测人在相同情况下是风险规避的,通过元分析收集的数据,我们可以检验这些竞争理论的准确性,并可能发现某些理论的预测并不如其他理论那样准确。这样的发现可能会导致某些理论需要更新或修正。这些都是元分析在较高层次上能够实现的功能。 我们经常听到元分析被称为“毕业神器”或“灌水利器”,这些称呼其实有一定的道理。元分析的研究流程非常固定,基于我的经验,我总结出了一个十步的固定流程。尽管有一些高阶的元分析可能会进行更复杂的检验,比如对抗理论的检验,但基本的这十步流程在所有元分析中都是必须经历的。这些高阶的元分析可能会在完成这十步之后进行其他工作,但这十步是每个元分析的基础。 今天我们将从这十步的第一步开始,逐步讲解。我将这个流程分为三大块: 第一块是元分析的选题以及评估所选题目的可行性。 第二块涉及到如何收集进行元分析所需的数据。 第三块则是如何对收集到的数据进行统计分析,以及如何撰写结果报告。 通过掌握这三大块的步骤,你将能够熟练地进行元分析,并从中得出有意义的结论。现在,让我们开始详细讨论这十步流程。 14.2 元分析的实施 实施元分析时,选题是至关重要的第一步。首先,你需要明确你的研究问题,例如探讨a对b的关系。这个问题应该具有一定的争议性,在你的研究领域内,关于a对b的关系可能存在不同的观点和结论。这样的争议性话题更适合使用元分析的方法来探讨。同时,这个问题应该是大家普遍关注的,具有探讨的价值。 第二步是明确研究问题的具体性。你需要清晰地定义a和b的概念,避免多元的定义。如果这个阶段没有考虑清楚,后续的文献检索将会遇到困难。 此外,如果你打算进行的是一个较小的元分析,建议不要纳入过于多元的研究设计。例如,如果你的研究问题通常使用实验法或问卷法,并且有多种测量工具,你可能需要做出取舍。过于多元的研究方法可能会影响效应量的转换和比较,有时甚至会影响到审稿人对研究结果的评价。除非你的研究重点是探究问卷法和实验法在效应量上的差异,并将研究设计作为一个调节变量,否则建议在单个元分析中不要纳入太多元的研究设计。 确定了研究问题之后,下一步是评估这个研究问题的可行性。这里有几个关键点需要考虑: 1. 检查已有研究:首先,你需要查看是否已经有人发表了关于这个主题的元分析。如果有的话,并不意味着你不能进行你的研究,但你需要考虑是否能够为已有的研究增添新的内容。这可能包括更新文献、探索新的调节效应,或者纠正之前研究的方法学缺陷。 2. 文献量:如果你决定进行元分析,你需要考虑你将纳入的文献量。文献量对于元分析的重要性非常高,因为它直接影响到你研究的稳健性和发表的可能性。一般而言,心理学领域的元分析至少应该包括大约十篇文献,否则可能被认为不够稳健。然而,如果文献量过大,比如超过50篇,你可能需要更多的人力资源来进行编码工作,因为通常需要至少两个独立编码者来确保准确性。 3. 检索技巧:为了确定是否已经存在关于你的研究问题的元分析,你可以在Google Scholar上搜索a和b的近义词组合。通过阅读搜索结果的标题,你可以快速了解已有的研究设计类型,比如问卷法或实验法,以及它们是使用被试内设计还是被试间设计。 总之,评估研究问题的可行性是一个重要的步骤,它确保你的元分析既有学术价值,又能在实际操作中得以实施。 好的,今天我会以我和传鹏之前合作发表的一篇元分析文章为例,来讲解元分析的实施过程。这样做的目的是为了让讲解更加具体,让大家在实际操作时能够更好地理解和应用所学的知识。使用真实的研究案例作为背景,相比使用虚拟数据,可以让大家对元分析的概念和方法有更深刻的理解。接下来,我将详细介绍我们这篇文章的研究背景、方法、过程和结果,以及我们在实践中遇到的一些挑战和解决方案。这样,大家在进行自己的元分析时,可以参考我们的经验,少走一些弯路。 我们这篇文章的研究背景集中在自我信息加工领域,这是一个大家可能都比较熟悉的领域。在自我信息加工中,有一个广为人知的效应,称为自我参照效应或自我优势效应。这个效应表明,我们的大脑倾向于优先加工与自我相关的信息,这些信息也更难以被忽视和遗忘。比如,在嘈杂的环境中,如果有人叫你的名字,你往往能够过滤掉其他声音而捕捉到自己的名字,这就是自我参照效应的一个例子。 我们的研究目的是想探索自我参照效应在教育教学中可能的应用。具体来说,我们想要了解,如果在教学中融入自我参照编码,比如说在学习新词汇时,让学生记住“你是勇敢的”,这样的自我参照提示是否能够增强学习效果。我们想要探究的是,自我参照效应是否能够对学习效果产生积极影响,特别是在记忆和学习任务中。 我们的研究问题非常直接。首先,我们关注的是大量使用自我参照编码的文章,这些文章检验了这种效应在教育教学中是否有效。其次,如果自我参照编码在教育教学中可能有效或无效,我们想探讨它是否受到某些边界条件的影响。例如,教育教学的实施者或所使用的材料类型可能会产生边界效应。 接下来,我们研究了这些研究的大致设计。通过Google Scholar的搜索,我们发现这些研究通常采用实验组和对照组的设计。实验组的学生会学习包含自我参照编码的学习材料,例如“你是勇敢的”或“你是自信的、阳光的”。而对照组可能直接让学生记忆这些词汇,或使用其他不含自我参照编码的学习策略。 我们还研究了是否有类似的元分析发表。由于这个领域可能是认知科学和教育学的交叉领域,我们并没有找到非常相似主题的元分析。虽然教育学中已经有一些相关的研究,但它们被归入了另一个框架,并不是在认知和教育的框架下。因此,与我们进行的研究可能并不会产生冲突。我们发现相关文献的数量超过了十篇,因此我们认为这是一个可行的小项目。最终,我们确定了可以进行这样的研究。 既然我们已经确定了研究主题,对于元分析而言,第二步就是进行文献检索。这包括确定文章的关键词、检索关键词,以及需要检索的数据库。关键词的选取应围绕我们关注的自我参照编码在教育教学中应用的核心,包括与自我参照编码、学习效果和教育教学相关的一系列词汇。 关键词选定后,我们需要选择数据库。对于元分析,建议至少选择三个以上的数据库,并且最好是领域内公认的数据库。例如,在教育心理学领域,ERIC或PsycINFO是常用的数据库。如果研究领域有特定的数据库,如经管或管理方向,应当将这些数据库纳入检索范围。一个实用的方法是查阅领域内发表的权威元分析文章,以了解它们通常使用哪些数据库。 除了在数据库中检索,审稿人通常还会要求至少进行一种补充检索。补充检索有两种类型:一种是回溯检索(backward search),另一种是前瞻检索(forward search)。在回溯检索中,我们会查看相关元分析中纳入的研究,确保我们没有遗漏任何重要研究。前瞻检索则是查看在我们已发表的元分析之后,是否有新的研究发表,这些研究也应该被纳入考虑。 因此,我们的研究应该包含至少一种补充检索方法,以确保文献的全面性和时效性。 好的,接下来我们讨论如何在不同数据库中进行检索。虽然各个数据库可能有一些细微的差别,但它们的搜索结构基本上是相似的。以PubMed为例,我们可以这样进行检索: 1. 打开PubMed网站,进入搜索界面。 2. 由于我们是进行元分析,通常会使用高级搜索(advance search)选项。 3. 在高级搜索中,开始输入关键词。我们会按照类别逐个输入关键词。例如,首先输入与自我参照编码相关的关键词。 4. 在同一类别中,使用“OR”运算符将关键词连接起来,这样可以将搜索范围扩大到该类别下的所有相关内容。 5. 输入完一个类别的关键词后,点击“添加”(add),然后继续输入下一个类别的关键词,比如与学习相关的词汇。 6. 重复这个过程,直到所有相关的关键词类别都被输入并添加。 7. 最后,点击“检索”按钮,系统会显示出搜索结果。 如果像我前几天那样进行检索,可能会得到51个结果。要将这些结果导出,可以按照以下步骤操作: 1. 点击“发送至”(Send to)按钮。 2. 选择“引文管理器”(Citation Manager),因为我们会使用EndNote进行文献管理,这会使后续的文章筛选和整理更加方便。 3. 点击后,系统会将所选文献的引用信息打包下载为一个文件。 4. 在EndNote中打开这个文件,会看到所有刚刚打包的引用都会显示在这里。 这样,就完成了文献检索的整个过程。 在完成文献检索之后,下一步是进行文献筛选。为此,我们需要制定一套文献的纳入标准。这些标准相对标准化,包括对文献本身的一些要求。例如,我们可能仅关注英语文献,认为这已经足够。因此,我们会要求文章必须是英文撰写。关于主题,我们要求文献必须与教育教学相关,因为即便使用了教育教学的关键词进行数据库搜索,结果仍然可能比较宽泛。因此,我们需要仔细地逐一挑选。 我们希望查看在普通人群中的效果,所以选择以普通人为被试的研究。在研究设计方面,我们要求必须有独立比较组,即实验条件下的对照组。那些仅采用单组设计、缺乏对照组的研究可能在我们看来不够严谨,因此会被排除。 在测量方面,我们同样有要求。例如,我们关注的是教育效果或学习效果,因此需要报告与学习过程或表现相关的指标。此外,我们还需要完整的数据,如总被试数、均值和标准差,或者可能提供的t检验、f检验结果等,这些数据能帮助我们计算效应量。如果文章中没有这些统计数据,比如一些较早的心理学文章,那么对于元分析来说,这些数据就无法使用。 好的,那么元分析的前期流程,实际上可以用一个PRISMA流程图来展示。这几乎是每个元分析都要求做的事情。PRISMA流程图的网站链接我也已经提供,大家可以下载。网站上有一个模板,上面没有填写具体内容,你可以根据这个模板来填写你的数据库检索信息,以及最终纳入的文章。 在我们这个研究中,我们首先从数据库进行检索,然后从每个数据库得到一系列文章。在这个阶段,我们可能会进行去重处理,因为每个数据库可能会出现相似的文章。去重之后,我们根据之前的排除标准开始筛选这些文章。一开始可能会进行粗筛,比如手动找出重复的文章,非英语的文章,或者与主题完全不相关的文章。这样做一轮之后,可能会有些文章难以判断,这时你就需要查找这些文章的全文PDF,仔细阅读摘要,才能判断它们是否有用。 到了这个步骤,你通过阅读摘要来判断文章是否可以被纳入。这就是数据库检索的过程。同时,我们还会进行补充检索。将数据库检索和补充检索的结果合并起来,就得到了我们最终纳入的研究。PRISMA流程图能够清晰地展示从检索到筛选的整个过程,确保研究的透明度和复现性。 完成文献筛选后,接下来的步骤是对我们已经纳入的文献进行编码。编码过程中,我们需要设计一个编码本(code book),这是一个工具,用于指导编码者从文章中摘录哪些信息。 编码本通常会包含研究的基本信息,如作者、年份、期刊名、发表状态(已发表或如论文集等)、被试的特征(总被试数、性别比等),以及任何你关注的调节变量,这些可能与你的理论关注点相关。还可能包括人口学变量、实验设计、不同测量是否可能导致不同结果等信息,这些都是与每篇文章关注的理论密切相关的。 此外,编码本还会包含统计结果,这些是与效应量计算相关的统计结果。在编码过程中,为了保证严谨性,至少需要两个人进行背靠背的编码,一个人单独完成是不够的。这也是元分析中通常需要报告的内容。 两个人根据编码本进行背靠背编码后,需要讨论并对比结果。如果发现不一致的地方,需要检查可能的错误,并进行修正。编码结束后,需要计算一致性系数,以表明基于同一编码本,两人摘录的结果是可信的。常用的一致性系数有克隆巴克的阿尔法(Cronbach’s alpha)或者结果相似性(百分比相似)。在实际应用中,α系数使用得更多,如果报告相似性,审稿人可能会要求你计算α系数。 尽管这些步骤听起来可能比较简单,但对于一篇元分析来说,完成这些步骤实际上已经完成了大部分工作,除了撰写文章本身。到了这一步,你基本上已经整理出了一个包含纳入研究的信息表,这个信息表将包含在你的文章中。 信息表会包括文章的基本信息,如作者、年份、期刊等,以及研究的具体信息,比如被试量、性别比例、平均年龄等。由于我们关注的是教育教学的结果,结果变量可能会有很多种,比如测试准确率、记忆力等,这些都会被包括在内。 此外,信息表还会包括我们选定的调节变量,我们可能没有选择很多,但选择了几个我们认为对研究结果有重要影响的变量。例如,学生的群体、材料的正负性(中性材料、引发负面情绪的材料等)、学习材料的呈现方式(电脑或纸质)、实验环境(实验室或自由学习环境)等。这些调节变量是根据现有理论选择的,我们认为它们可能会对效应量产生影响。 这个信息表是元分析中非常关键的部分,因为它展示了研究的详细信息和数据,为后续的分析和讨论提供了基础。在撰写文章时,这个信息表将作为支持和验证你研究结果的重要依据。 好的,到了这一步,如果你已经完成了上述所有步骤,那么我们就可以进入数据分析环节了。现在,你可以打开之前下载的R代码和我准备的数据。如果你还没有下载,可以先跟着这个流程了解一下,然后回去再自己尝试运行。 进行元分析的数据处理实际上非常简单,我们将会使用几个R包来进行数据清洗,比如tidyverse。对于单个效应量的计算,有很多R包可以使用,甚至你可以自己编写函数来计算。我个人比较喜欢使用esc包。而对于效应量的合并,元分析中两个非常著名的R包是metafor和meta。 首先,我们需要编写代码来加载所需的R包。这段代码会检查你的电脑是否已经安装了这些包,如果没有安装,它将帮助你安装。然后,我们就可以加载这些包,准备进行数据分析了。 我们首先需要加载所需的R包。然后,我们进入到元分析中比较复杂的步骤,我个人认为这是最麻烦的一个步骤。之后的步骤实际上都很简单,通常只需要一行代码就可以完成。 # 加载所需R包 # Packages if (!requireNamespace('pacman', quietly = TRUE)) { install.packages('pacman') } pacman::p_load(tidyverse, esc, metafor, meta) 这个复杂的步骤是对单个研究进行效应量的转化。首先,我们回顾一下什么是效应量。效应量是衡量实验强度的一个指标,它不受样本容量大小的影响,因此在不同的研究之间具有可比性。这也是为什么在进行各种测试时,我们通常被要求报告效应量的原因,便于进行元分析的人能够汇总这些研究的效应。 效应量的种类有很多,我引用了一篇郑昊敏老师的文章,其中将效应量分为三类: 1.差异类的效应量:这类效应量常用于实验研究,衡量两个或多个组之间的差异。 2.相关类的效应量:这类效应量通常出现在问卷研究中,用于衡量两个或多个变量之间的共变程度。可能用到的有相关系数(correlation)、标准化相关系数、标准化回归系数等。 3.组重叠效应量:这类效应量在心理学研究中较为少见,它衡量的是两个分布的重叠程度。 在进行元分析时,我们需要根据研究类型和所报告的统计结果选择合适的效应量,并对其进行转化,以便于后续的分析和合并。 关于效应量的计算,我在这里可能不会详细讲解公式的具体内容,但我推荐了Cooper的这本书。在这本书中,他详细列出了各种情况下如何计算效应量,比如独立样本、配对样本、相关研究等。有了这样的参考,如果你遇到了一些难以处理的效应量转换,比如esc包中没有包含的某些效应量,你可以自己编写一个函数来计算,这通常只是基本的算术操作。 由于时间限制,我可能不会深入讲解这部分内容。如果大家对计算效应量感兴趣,可以在课后自行查阅相关资料进行学习。Cooper的书籍是一个很好的资源,可以提供详细的指导和公式。此外,R语言社区也有很多资源和学习材料,可以帮助你更好地理解和实施效应量的计算。 好的,我们直接来看如何在R中使用esc包来计算单个研究的效应量。首先,我们最喜欢的是那些直接报告了最原始数据的效应量。你可能会问,如果有的研究已经报告了效应量,我是否可以直接使用,而不需要计算均值(mean)和标准差(SD)?理论上是可以的,但实际上我们可能会遇到的问题是,研究者可能并不完全理解效应量的计算,导致他们报告的效应量可能并不准确。因此,建议如果研究提供了均值和标准差,最好是自己计算一遍,这样比较保险。而且,计算均值和标准差是最简单的计算方法之一。 我们可以举一个例子,假设我们研究中看到一篇文章,它报告了两个组的被试数量以及各自的均值和标准差。比如,这篇文章测量了学习效果(comprehension),并且进行了测试(test)和重测(retest)。我们计算效应量时,可以先关注测试的结果。根据文章,对照组(control group)的均值是87.1,标准差是7.6,而实验组的均值是87,标准差是7.3。从这些数据可以看出,效应量可能不会特别大。 在R中,我们可以使用esc包中的means_d函数来计算Cohen’s d效应量。这个函数的使用非常直接,你只需要提供对照组和实验组的均值、标准差以及两组的被试数量,函数就会计算出效应量。下面是一个简单的R代码示例: esc_mean_sd( grp1m = 87, grp1sd = 7.3, grp1n = 29, #第一组(干预组)的Mean,SD和人数 grp2m = 87.1, grp2sd = 7.6, grp2n = 30, #第二组(控制组)的Mean,SD和人数 es.type="g")#效应量的种类 ## ## Effect Size Calculation for Meta Analysis ## ## Conversion: mean and sd to effect size Hedges' g ## Effect Size: -0.0132 ## Standard Error: 0.2604 ## Variance: 0.0678 ## Lower CI: -0.5236 ## Upper CI: 0.4972 ## Weight: 14.7454 esc包中的means_d函数需要我们提供以下信息: 1. 第一组(通常是干预组)的均值(mean)、标准差(SD)和样本量(n)。我们通常用干预组的均值减去控制组的均值来计算效应量,如果结果是正的,则说明干预组的效果更好。 2. 第二组(通常是控制组)的均值(mean)、标准差(SD)和样本量(n)。 在esc包中,你需要选择计算的是Hedges’ g还是Cohen’s d。在元分析中,两者通常都可以使用,但如果样本量特别小,Hedges’ g因为有一个针对小样本的校正,所以可能是更好的选择。因此,通常会计算Hedges’ g。 代码运行后,我们会得到一个效应量的值。如果这个值接近于0,比如-0.01,那么这表明基本上没有效应。 对于后续的元分析,meta和metafor包对数据的要求略有不同。meta包需要方差(variance),而metafor包需要标准误(standard error),它们之间只是差一个平方。通常,我们会选择记录方差。 在实际研究中,除了均值和标准差,我们还可能会遇到标准误(se)。有时候,人们可能会将标准误误认为是标准差,如果你错误地将标准误当作标准差来计算效应量,结果可能会有很大的不同。因此,在处理数据时,需要特别注意区分标准差和标准误。 #例:假如Dutke et al. (2016)的研究报告的SD其实是SE esc_mean_se( #摘录时看清楚SD和SE非常重要! grp1m = 87, grp1se = 7.3, grp1n = 29, grp2m = 87.1, grp2se = 7.6, grp2n = 30, es.type="g") ## ## Effect Size Calculation for Meta Analysis ## ## Conversion: mean and se to effect size Hedges' g ## Effect Size: -0.0025 ## Standard Error: 0.2604 ## Variance: 0.0678 ## Lower CI: -0.5129 ## Upper CI: 0.5079 ## Weight: 14.7458 当然可以,以下是对提供的文字的整理和润色: 我们来进行一个假设的尝试。比如说,在我的研究中,目前报告的并不是标准差(SD),而是标准误(SE)。那么,我们应该如何计算呢?实际上,我们依然可以使用esc这个软件包。它对于均值(m)和标准误(SE)的处理非常明确。操作步骤是:首先输入第一组的人数和相应的均值及标准误,然后输入第二组的人数和相应的均值及标准误。接下来,我们就可以运行这个分析。 通过分析,我们可以看到效应量(g)的结果显示为负的0.002。这表明,对于标准差和标准误的计算,特别是当效应量较小时,它们之间的差异并不大。然而,如果效应量非常大,那么这两者之间的差异也会非常显著。如果在这个环节出现错误,那么后续的分析结果可能会出现异常,比如效应量可能会变成outliner。因此,当在进行元分析时,如果发现某些效应量异常大,那么就有必要回头检查,看看是否在数据摘录的过程中出现了问题。 在研究中,能够获取原始数据是最理想的情况,但实际上,很多研究报告并不会提供这些原始数据。相反,它们通常会报告统计分析的结果,如t检验的统计量和效应量等。在这篇文章中,研究结果显示,实验组在成绩上显著高于控制组,并报告了t检验的统计值和效应量。 虽然研究报告提供了这些信息,但我们通常对这些报告持谨慎态度,不会完全依赖于这些数据。为了验证报告中的效应量是否准确,我们可以使用esc软件包中的T检验函数来进行一些计算。这个函数会要求我们输入t值、总样本量以及效应量的类型。通过运行这个函数,我们可以得到一个效应量值。 esc::esc_t( t = 2.98, #t值 totaln = 40, #总被试数 es.type="g") #效应量的种类 ## ## Effect Size Calculation for Meta Analysis ## ## Conversion: t-value to effect size Hedges' g ## Effect Size: 0.9236 ## Standard Error: 0.3333 ## Variance: 0.1111 ## Lower CI: 0.2703 ## Upper CI: 1.5769 ## Weight: 9.0009 例如,我们运行了这个函数后,发现得到的效应量为0.92,这表明作者在报告中提供的效应量是非常准确的。然后就可以摘录effect size和variance,以备后续的元分析。 如果我们遇到的是相关研究,这类研究通常会报告相关系数或其他类型的回归系数。虽然我们的研究可能没有涉及问卷类数据,也不会进行相关效应量的转换,但如果我们遇到需要处理这类数据的情况,我们可以使用esc软件包来进行效应量的转换。 ## 研究报告非标准化回归系数b esc::esc_B( b=3.3, #非标准化回归系数 sdy=5, #因变量的SD grp1n = 100, #第一组的人数 grp2n = 150, #第二组的人数 es.type = "g") #效应量的种类 ## ## Effect Size Calculation for Meta Analysis ## ## Conversion: unstandardized regression coefficient to effect size Hedges' g ## Effect Size: 0.6941 ## Standard Error: 0.1328 ## Variance: 0.0176 ## Lower CI: 0.4338 ## Upper CI: 0.9544 ## Weight: 56.7018 例如,如果我们有一个研究报告了一个非标准化的回归系数,假设这个系数是3.3,我们还需要知道因变量的标准差(SD),以及两组的样本量。我们需要将这些信息输入到esc软件包中,并指定效应量的类型。通过运行相应的代码,我们可以计算出效应量。 ## 研究报告标准化回归系数beta esc_beta( beta=0.7, #标准化回归系数 sdy=3, #因变量的SD grp1n=100, #第一组的人数 grp2n=150, #第二组的人数 es.type = "g") #效应量的种类 ## ## Effect Size Calculation for Meta Analysis ## ## Conversion: standardized regression coefficient to effect size Hedges' g ## Effect Size: 1.9868 ## Standard Error: 0.1569 ## Variance: 0.0246 ## Lower CI: 1.6793 ## Upper CI: 2.2942 ## Weight: 40.6353 在问卷研究中,我们可能不常看到标准化回归系数,但标准化回归系数实际上已经是一种效应量的表达形式。尽管报告中可能较少包含这类系数,但如果研究确实报告了标准化回归系数,我们同样可以使用esc软件包中的贝塔函数来计算效应量。 例如,如果我们有一个研究报告中提供了标准化回归系数(beta值),我们可以使用esc软件包中的相应函数来计算效应量。由于beta值本身可能较大,计算出的效应量也可能相对较大。我们需要将这个效应量及其变异性记录下来,以便进行后续分析。 在研究过程中,我们可能会遇到各种不同类型的效应量。正如我之前提到的,Cooper的书中已经包含了许多种类的效应量以及它们各自的计算方法。因此,如果你们在课后遇到不同的效应量,可以尝试使用Cooper提供的公式来计算,同时esc软件包也提供了许多其他效应量的计算方法,大家可以自行探索和学习。 提到的是元分析的数据收集完成后,如何进行数据分析的步骤。以下是对提供的文字的整理和润色: 一旦我们完成了对所有研究的效应量的摘录和转换,我们就可以开始进行元分析的数据分析了。这个阶段标志着数据收集阶段的完成,是元分析中一个重要的里程碑。 在元分析的数据分析过程中,我们的第一步是对收集到的多个研究的效应量进行综合。这样做的目的是为了得到一个整体的效应大小,这是回答我们研究问题的第一步。在综合效应量时,我们通常会涉及到两种模型:固定效应模型和随机效应模型。 固定效应模型假设所有研究的效应量都来自于一个单一的同质化群体。然而,在元分析中,这种情况非常少见,因为即使是同一主题下的研究,也可能来自不同的群体,并且可能受到其他变量的影响。 因此,我们更常使用的是随机效应模型。这个模型考虑到了研究间的异质性,假设效应量来自于多个不同的群体。这种模型更符合实际情况,因为它考虑了研究间的变异性。 在解读元分析的综合效应量时,学界一直存在争议。传统上,我们依赖于Cohen在1988年提出的效应量解读标准,即0.2为小效应,0.5为中等效应,0.8为大效应。然而,这些标准在元分析中的应用已经受到了学界的质疑和争议。因此,如何正确解读元分析中的效应量,目前仍然是一个开放的问题,需要根据具体的研究领域和背景来综合考虑。 为了合并效应量,我们首先需要准备好我们的数据。我已经提供了一个效应量表格的示例,我们可以使用这个表格来演示数据预处理的过程。在R语言中,数据预处理通常包括选择需要的变量、清洗数据以及确保数据格式正确等步骤。 在元分析中,我们需要从表格中选取以下变量: 1. 独立的研究编号:这是为了标识每个独立的研究。 2. 作者信息:这有助于识别和追踪各个研究。 3. 每个研究的被试数:这是计算效应量时的重要信息。 4. 计算得到的效应量及其变异性:这是进行元分析的核心数据。 5. 调节变量:如果有的话,这些变量可能对效应量有影响,也需要纳入分析。 一旦我们选择了这些变量,我们就可以运行R代码来处理数据,并查看初步的结果。这个过程可能包括数据的导入、清洗和格式化,确保所有数据都是准确无误的。完成这些步骤后,我们就可以进行效应量的合并和元分析了。 library(tidyverse) #set working directory cur <- getwd() setwd(cur) #import effect_size <- read.csv("./data/effect_size.csv") %>% dplyr::select( id, #独立研究编号 Information, #作者信息 N, #被试数 ES, #刚刚计算的每个研究的效应量 VI, #刚刚计算的,效应量的variance "Cohort" = subgroup1, "Context" = subgroup2, "Materials" = subgroup3, "Valence" = subgroup4 ) %>% dplyr::filter(row_number() %in% c(1:20)) head(effect_size) ## id Information N ES VI Cohort Context Materials Valence ## 1 1 Axelsson et al. (2018) 23 0.4351 0.0489 1 1 1 1 ## 2 2 Cunningham et al. (2018), S1 30 1.3587 0.0827 1 1 1 ## 3 3 Cunningham et al. (2018), S2 84 0.8247 0.0491 1 1 1 ## 4 4 d'Ailly et al. (1997) 100 0.2108 0.0096 1 1 1 1 ## 5 5 Ditman et al. (2010), S1 36 0.4210 0.0564 3 1 1 1 ## 6 6 Dutke et al. (2016) 59 0.5690 0.0732 2 2 2 1 在元分析中,我们通常需要将原始数据转换为特定的格式,以便进行分析。例如,如果打算使用metafor软件包,需要将数据转换为escalc格式。这个转换过程可以通过使用metafor软件包中的escalc函数来实现。 在转换数据时,需要指定计算的是哪种类型的效应量。例如,如果计算的是两组之间的标准化平均差异(standardized mean difference),应该在escalc函数中指定这一点。同时,还需要提供数据集的名称,以及效应量(yi)和效应量变异性(VI)对应的变量名。转换完成后,如果想要绘制森林图并且希望图中的数据按照效应量大小排序,可以使用order函数对数据进行排序。这样,数据就会按照效应量从小到大的顺序排列,便于后续的分析和绘图。 #数据预处理 df <- metafor::escalc( #转换成escalc的格式 measure="SMD", data = effect_size, yi= ES, #指定每个研究的效应量是哪列 vi = VI, #指定每个研究的效应量的variance是哪列 slab = paste("Study ID:", id)) #注明研究的label #按效应量大小进行排序,方便后续画图展示 df <- df[order(df$yi), ] head(df) ## ## id Information N ES VI Cohort Context Materials Valence yi ## 12 12 Kühl and Münzer (2021), G2 60 -0.5278 0.0691 3 1 1 2 -0.5278 ## 10 10 Kühl and Zander (2017), S2 71 -0.4687 0.0583 3 1 1 2 -0.4687 ## 9 9 Kühl and Zander (2017), S1 77 -0.0640 0.0530 3 1 1 2 -0.0640 ## 18 18 Sinatra et al. (2016) 111 -0.0560 0.0541 3 1 1 1 -0.0560 ## 14 14 Li et al. (2021), S2 29 0.0477 0.0276 3 1 1 1 0.0477 ## 4 4 d'Ailly et al. (1997) 100 0.2108 0.0096 1 1 1 1 0.2108 ## vi ## 12 0.0691 ## 10 0.0583 ## 9 0.0530 ## 18 0.0541 ## 14 0.0276 ## 4 0.0096 在元分析中,计算综合效应量是一个关键步骤,这一步骤实际上可以通过一行代码来完成。我们可以使用metafor软件包中的rma函数来计算。在这个函数中,我们需要输入的是每个研究的效应量及其变异性(variance)。此外,我们还需要指定分析使用的方法(method)。通常情况下,我们可以使用函数的默认方法,除非有特定的理由需要更改。 res <- metafor::rma( yi,#每个研究的效应量 vi,#每个研究的效应量的variance method = "REML", #method="REML" is the default data = df) res ## ## Random-Effects Model (k = 20; tau^2 estimator: REML) ## ## tau^2 (estimated amount of total heterogeneity): 0.1917 (SE = 0.0820) ## tau (square root of estimated tau^2 value): 0.4379 ## I^2 (total heterogeneity / total variability): 80.32% ## H^2 (total variability / sampling variability): 5.08 ## ## Test for Heterogeneity: ## Q(df = 19) = 81.4191, p-val < .0001 ## ## Model Results: ## ## estimate se zval pval ci.lb ci.ub ## 0.4036 0.1128 3.5791 0.0003 0.1826 0.6247 *** ## ## --- ## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1 其中,yi是效应量所在列的名称,vi是效应量变异性所在列的名称,data是已经转换为escalc格式的数据集。 运行这行代码后,我们就完成了综合效应量的计算。结果将展示出综合效应量的大小,以及相关的统计信息,如置信区间等。这样,我们就可以得到元分析的主要结果,即研究效应量的综合估计。 在进行元分析后,我们得到的结果包含了多个统计量,但其中有一些是关键的,需要特别关注。以下是对提供的文字的整理和润色: 在元分析的结果中,我们首先需要关注的是综合效应量的估计(estimate)。这个估计值告诉我们所有研究合并后的平均效应量是多少。例如,如果综合效应量估计为0.4,这表明我们观察到的是一个小到中等大小的效应量。 除了综合效应量的估计值,我们还应该报告以下统计量: 1. 标准误(se):这反映了效应量估计的精确度。标准误越小,估计值越精确。 2. p值:这告诉我们综合效应量的显著性。如果p值小于常用的显著性水平(如0.05),则综合效应量在统计上是显著的。 3. 95%置信区间(CI):这提供了一个范围,我们可以在一定程度上相信真实效应量落在这个范围内。 在报告完这些统计量后,我们通常会解释这些结果的意义。例如,如果综合效应量是小到中等大小,我们可能会说,自我参照编码在教育教学中总体上是有效的,因为它已经接近中等效应量了。我们可能会这样报告:“综合效应量显示了一个小到中等的效果(small to moderate effect),这表明自我参照编码在教育教学中是有效的。” 这样的报告不仅提供了元分析的结果,还提供了对结果的解释,使读者能够理解这些结果对实践或理论的潜在意义。 在使用元分析时,确实可以采用不同的软件包来分析数据,并且可以得到类似的结果。例如,metafor和meta两个R包都可以用来进行元分析,但它们在处理数据和输出结果方面可能有所不同。 在使用meta包时,需要提供每个研究的效应量(yi)和效应量的变异性(VI)。但是,需要注意的是,meta包在计算时使用的是效应量标准误的平方,而不是效应量的变异性。因此,需要将VI的开方作为标准误输入到meta包中。此外,还需要提供研究标签(study labels)和数据集,以及指定是使用随机效应模型还是固定效应模型,并且可以选择是否输出置信区间。还需要指定效应量的度量类型,例如标准化均值差异(standardized mean difference)。 ## 用meta进行效应量合并 res1 <- meta::metagen( TE = yi,#每个研究的效应量 seTE = sqrt(vi), #每个研究的效应量的标准误 data = df, studlab = df$id, fixed = FALSE, random = TRUE, #选择随机效应模型 prediction = TRUE, #是否需要CI sm = "SMD") res1 ## Number of studies: k = 20 ## ## SMD 95%-CI z p-value ## Random effects model 0.4036 [ 0.1826; 0.6247] 3.58 0.0003 ## Prediction interval [-0.5463; 1.3536] ## ## Quantifying heterogeneity: ## tau^2 = 0.1917 [0.0848; 0.4818]; tau = 0.4379 [0.2912; 0.6941] ## I^2 = 76.7% [64.2%; 84.8%]; H = 2.07 [1.67; 2.56] ## ## Test of heterogeneity: ## Q d.f. p-value ## 81.42 19 < 0.0001 ## ## Details on meta-analytical method: ## - Inverse variance method ## - Restricted maximum-likelihood estimator for tau^2 ## - Q-Profile method for confidence interval of tau^2 and tau ## - Prediction interval based on t-distribution (df = 18) 其中,yi是效应量所在列的名称,VI是效应量变异性所在列的名称,your_data是包含数据的data.frame对象,study_labels是研究标签的向量,method用于指定模型类型(“random”或”fixed”),confint用于指定是否计算置信区间,measure用于指定效应量的度量类型。 运行这段代码后,meta包会提供综合效应量的估计,以及相关的统计信息,如置信区间等。这些结果应该与使用metafor包得到的结果非常相似,从而提供了两种不同方法之间的相互验证。 在元分析中,森林图(forest plot)是一种常用的图形展示方法,用于展示每个研究的结果,以及这些研究之间的异质性和整体效应量。森林图有助于读者直观地理解研究结果的一致性和差异性,以及综合效应量的大小。 森林图包含以下元素: 每个研究的效应量点估计值:这表示每个研究的效应量大小。 每个研究的95%置信区间:这表示每个效应量估计值的可信度范围。 研究标签:这有助于识别每个研究。 合并的效应量:这表示所有研究的效应量加权平均,通常位于森林图的底部,用菱形或方形表示。 ## 绘制森林图 #储存图像的代码 # tiff(file="forest_overall.tiff", # res=800,width = 9000,height = 4800)#save tiff metafor::forest( res, slab = paste(df$Information), header="Author(s) and Year" ) # dev.off() #储存图像的代码 森林图将展示每个研究的效应量估计值和置信区间,以及合并的效应量。通过这个图形,可以直观地看到每个研究的结果,以及它们如何组合在一起形成一个整体的效应量估计。 在进行元分析并绘制了森林图之后,接下来的一步是评估研究间的异质性。异质性指的是研究结果之间的差异性和多样性,反映了研究间的变异程度。评估异质性对于确定使用固定效应模型还是随机效应模型至关重要。 异质性的来源可能包括研究设计、方法学差异、样本群体异质性、测量工具的差异等。在元分析中,如果研究间存在异质性,那么使用随机效应模型可能是更合适的选择,因为它能够更好地反映不同研究间的变异。 在metafor包中,异质性的评估通常是通过Q检验和I²统计量来进行的。这些统计量在元分析的第一步输出中已经给出,因此不需要编写新的代码。 Q检验:Q检验用于评估研究间是否存在异质性。如果Q检验的p值小于0.10,通常认为研究间存在异质性。 I²统计量:I²统计量提供了异质性大小的度量。如果I²统计量高于75%,通常认为研究间具有非常高的一致性,这支持使用随机效应模型。 在结果中,Q检验的p值小于0.001,并且I²统计量约为80%。这些结果表明这个的研究数据具有显著的异质性,因此使用随机效应模型是合理的。这也支持了进行亚组分析或元回归分析,以进一步探索异质性的来源。 好的,完成了异质性分析之后,我们接下来还需要评估元分析得到的总体效应是否稳健。这意味着我们需要检验我们的分析是否受到单个研究的影响,因为如果研究的数量很小,那么单个研究的结果可能会对整体的效应量估计产生极大的偏差。如果某个研究占据了过大的权重,它可能会导致效应量被推向极端值。因此,我们需要确保总体效应量估计的稳健性。 为了评估稳健性,我们通常会进行敏感性分析。其中一种常用的方法是“去一法”(leave one out),也就是每次分析时都排除一个研究,然后重新估计效应量。通过这种方式,我们可以观察效应量在排除每个研究后的变化情况。如果某个研究被排除后,效应量发生了显著变化,那么这个研究可能对整体结果产生了不稳健的影响。 除了去一法,当研究数量较多时,我们还可以使用其他方法,比如Gosh plot。这个方法提出的是拟合所有研究子集的模型,而不是仅拟合k-1个模型。这意味着我们会拟合2k-1个模型,从而能够更全面地观察效应量如何受到不同研究组合的影响。通过这种方式,我们可以更准确地评估效应量估计的稳健性,尤其是在研究数量较多的情况下。 在我们这里,我们先介绍去一法,因为它可能是目前大多数研究中仍在使用的方法。对于去一法,metafor包提供了一个直接的函数来进行操作,即leave_one_out()。这个函数可以接收metafor包生成的结果列表作为输入,然后输出每个研究被排除后的效应量估计。 一旦我们得到了去一法的结果,通常会进行可视化,比如绘制一个新的森林图。对于3D图,我们会告诉函数使用去一法得到的效应量估计值和标准误,并指定标题,以及如何对结果进行排序。这样,我们可以通过代码运行来查看结果。 # 敏感性分析-去1法 l1o <- metafor::leave1out(res) #可视化 metafor::forest( l1o$estimate, sei = l1o$se, header = "Ommited Study", slab = paste(df$Information), xlab = "Leave One Out Estimate", refline = coef(res)) 在执行去一法敏感性分析并绘制新的森林图后,我们可以观察到效应量估计值的变化,从而评估每个研究对整体效应量估计的贡献。例如,如果去掉第一个研究后,效应量估计值显著增加,这可能表明该研究的结果与整体趋势不同,或者其效应量估计可能存在偏差。 通过敏感性分析,我们可以识别出那些对整体效应量估计有显著影响的“极端”研究。例如,如果某个研究报告了一个非常大的效应量,而通过去一法分析发现,去掉这个研究后,效应量的估计值并没有发生显著变化,这表明该研究对整体效应量估计的影响并不大,从而增强了研究的稳健性。 在敏感性分析的报告或讨论中,我们会指出效应量估计值的变化范围,并解释这些变化对研究稳健性的影响。例如,如果效应量估计值的变化范围从0.34到0.45,虽然存在一定的波动,但整体变化不大,这表明研究结果是稳健的,不会因为单个研究结果的极端值而产生重大偏差。 通过这种方式,敏感性分析帮助我们理解每个研究对整体结果的影响,并评估研究的稳健性。 好的,我们进入元分析的第九步,也就是亚组分析。这一步的目的是验证第二个研究问题,即是否存在某些边界条件或调节变量,这些变量可能会影响效应量的大小。 亚组分析的目的是考察不同亚组之间的差异,并确定效应量是否存在显著性。例如,如果我们想探究自我参照效应在学习疾病信息时的应用效果是否与材料附带的情感价值有关,我们可以将学习材料分为情感价值较高的材料和情感价值中性的材料,然后比较这两种材料下的学习效果是否存在差异。 进行亚组分析的过程相当简单,只需要使用metafor包中的update函数。这个函数需要接收一个已经生成的元分析结果对象,然后指定亚组的变量,并指定使用随机效应模型。 # 调节效应检验-亚组分析 ### 例子:我们想考察材料附带的情感价值(负性/非负性)是否会影响教学效果。 valence_subgroup <- stats::update( res1, subgroup = Valence, #亚组标记 random = TRUE, fixed = FALSE) valence_subgroup ## Number of studies: k = 20 ## ## SMD 95%-CI z p-value ## Random effects model 0.4036 [ 0.1826; 0.6247] 3.58 0.0003 ## Prediction interval [-0.5463; 1.3536] ## ## Quantifying heterogeneity: ## tau^2 = 0.1917 [0.0848; 0.4818]; tau = 0.4379 [0.2912; 0.6941] ## I^2 = 76.7% [64.2%; 84.8%]; H = 2.07 [1.67; 2.56] ## ## Test of heterogeneity: ## Q d.f. p-value ## 81.42 19 < 0.0001 ## ## Results for subgroups (random effects model): ## k SMD 95%-CI tau^2 tau Q I^2 ## Valence = 2 4 -0.1717 [-0.5702; 0.2268] 0.1048 0.3237 8.10 63.0% ## Valence = 1 16 0.5422 [ 0.3321; 0.7523] 0.1226 0.3501 52.31 71.3% ## ## Test for subgroup differences (random effects model): ## Q d.f. p-value ## Between groups 9.65 1 0.0019 ## ## Details on meta-analytical method: ## - Inverse variance method ## - Restricted maximum-likelihood estimator for tau^2 ## - Q-Profile method for confidence interval of tau^2 and tau ## - Prediction interval based on t-distribution (df = 18) 在执行亚组分析并查看结果后,我们可以直接从结果中看到不同亚组之间的效应量差异。例如,如果我们将材料分为情感价值较高的材料(例如复性材料)和情感价值中性的材料,我们可以观察到: - 对于情感价值较高的材料,效应量变为负数,这表明使用自我参照效应的策略在这种材料下可能不仅没有教学效果,反而可能产生负面影响。 - 对于情感价值中性的材料,效应量实际上比总体效应量还要大,这表明如果材料是中性的或者正性的,自我参照效应的教学策略效果非常好。 在报告亚组分析的结果时,我们通常会报告q检验的值和p值。如果p值小于常用的显著性水平(如0.05),则表明亚组效应是显著的,即情感价值是效应量的一个显著调节变量。 因此,亚组分析的结果可以帮助我们确定哪些因素可能会调节效应量的大小,从而更深入地理解研究结果。在报告亚组分析时,我们通常会报告q检验的值、p值,并解释这些结果对研究发现的贡献。 确实,除了类别型变量,我们也会遇到连续变量,例如年龄、性别比等。对于这些连续变量,我们可以使用元回归来探索它们与效应量之间的潜在关系。元回归是一种统计方法,用于分析一个或多个连续调节变量对元分析中效应量的影响。 在metafor包中,进行元回归分析的函数是meta_regression()。这个函数接收元分析的结果对象作为输入,然后指定调节变量。通过运行这个函数,我们可以得到元回归分析的结果。 # 调节效应检验-元回归 ### 例子:我们想考察被试量(N)是否影响效应量大小。 N_metareg <- meta::metareg( res1, N) N_metareg ## ## Mixed-Effects Model (k = 20; tau^2 estimator: REML) ## ## tau^2 (estimated amount of residual heterogeneity): 0.1749 (SE = 0.0788) ## tau (square root of estimated tau^2 value): 0.4182 ## I^2 (residual heterogeneity / unaccounted variability): 77.27% ## H^2 (unaccounted variability / sampling variability): 4.40 ## R^2 (amount of heterogeneity accounted for): 8.77% ## ## Test for Residual Heterogeneity: ## QE(df = 18) = 72.9187, p-val < .0001 ## ## Test of Moderators (coefficient 2): ## QM(df = 1) = 2.6276, p-val = 0.1050 ## ## Model Results: ## ## estimate se zval pval ci.lb ci.ub ## intrcpt 0.7970 0.2665 2.9908 0.0028 0.2747 1.3193 ** ## N -0.0071 0.0044 -1.6210 0.1050 -0.0157 0.0015 ## ## --- ## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1 在我们当前的研究中,由于没有采用元回归分析,我们无法直接探索被试量对效应量大小的影响。因此,我举了一个假设性的例子来说明如何进行这种探索。 假设我们想要考察被试量是否会影响效应量的大小,我们可以考虑将每个研究的被试量作为一个潜在的调节变量。为了探究这一点,我们可以将每个研究的被试量与效应量进行相关性分析,以查看它们之间是否存在显著的相关性。 在这种情况下,我们依然会报告Q值,这是用于评估研究间异质性的统计量。如果Q值表明研究间存在显著的异质性,这可能意味着被试量对效应量大小的影响不容忽视。然而,如果Q值并不显著,这可能表明被试量与效应量之间的关系并不显著。 综上所述,尽管在我们的研究中没有采用元回归分析,通过简单的相关性分析,我们可以初步了解被试量与效应量之间的关系。如果相关性分析的p值不显著,这可能意味着在当前的研究背景下,被试量对效应量大小的影响并不大。 在元分析中,发表偏倚是一个重要的问题,它指的是只有阳性结果的研究更容易被发表,而阴性结果则可能被忽视。这种偏倚会导致元分析结果的偏差,因为研究池中可能存在许多未发表的阴性结果。因此,如果一个研究者只选择发表阳性结果的研究,那么元分析得到的效应量平均估计将会偏高,从而影响结果的可靠性。 为了评估发表偏倚,研究人员可以使用漏斗图和发表偏倚检验方法,如Egger’s test、Begg’s test等。这些方法可以帮助我们了解是否存在发表偏倚,并评估其对元分析结果的影响。 在R语言中,可以使用metafor包中的函数来绘制漏斗图,例如funnel()函数。这个函数可以直接从metafor包生成的结果列表中获取数据,并生成漏斗图。通过这个图,我们可以直观地看到研究结果的分布情况,从而判断是否存在发表偏倚。 # 发表偏倚检验 ## 漏斗图 metafor::funnel( res, xlab = "Hedges' g") 生成漏斗图后,可以观察图形来判断是否存在发表偏倚。如果图形对称地分布在两侧,这通常表明没有发表偏倚。然而,如果图形出现不对称,特别是小样本研究集中在漏斗图的一侧,这可能表明存在发表偏倚。 漏斗图是一种直观的展示方法,但它只是一个辅助工具。如果需要正式报告是否存在发表偏倚,通常还需要进行发表偏倚检验,如Egger’s test或Begg’s test,并查看检验的p值。如果p值小于常用的显著性水平(如0.05),则可以认为存在发表偏倚。 在元分析中,为了确认是否存在发表偏倚,除了使用漏斗图进行可视化分析之外,还需要进行统计检验。这通常是通过Egger’s regression来实现,它可以提供一个统计量来评估漏斗图的对称性。如果Egger’s regression的p值不显著,这通常表明漏斗图是对称的,从而暗示没有发表偏倚。 ## Egger test metafor::regtest(res, model = "lm") ## ## Regression Test for Funnel Plot Asymmetry ## ## Model: weighted regression with multiplicative dispersion ## Predictor: standard error ## ## Test for Funnel Plot Asymmetry: t = 0.9106, df = 18, p = 0.3746 ## Limit Estimate (as sei -> 0): b = 0.1170 (CI: -0.4623, 0.6962) 在这个代码中,results是metafor包生成的结果列表。egger()函数会执行Egger’s regression分析,并返回一个结果对象。 生成Egger’s regression的结果后,可以查看其统计量,如ttest和pvalue,以判断是否存在发表偏倚。如果pvalue大于常用的显著性水平(如0.05),则可以认为没有发表偏倚。 完成这些步骤后,就可以完成基本的元分析流程。这个过程包括选题、数据收集(进行文献检索)、文献编码(转换效应量)、计算综合效应量、检验模型选择(一致性检验)、进行敏感性分析、探索调节效应和元回归分析,以及评估发表偏倚。通过这些步骤,可以对研究问题进行全面的元分析,并得出可靠的结论。 其实,一个元分析最基本的步骤也就基本上到这儿就结束了,当然还有就是数据分析阶段和你的结果报告,然后以及你需要去怎么frame你的文章的问题。这就是一个最基本的流程。 14.3 回顾与总结 确实,元分析并不是没有问题的,它仍然存在一些局限性和挑战。随着元分析技术的发展,学者们对元分析的理论和实践进行了深入的反思,并提出了一些新的方法来克服这些局限性。 传统元分析的一个基本假设是每个效应量之间是相互独立的。然而,在实际研究中,这种独立性往往是不成立的。例如,同一研究中的多个效应量可能来自同一个样本群体,因此它们之间可能存在较高的相关性。忽略这种相关性可能导致元分析结果的不准确。 为了解决这个问题,一些研究者提出了更高级别的元分析方法,例如多层元分析(multilevel meta-analysis)。在这种方法中,元分析被分解为更细的层级结构,每个层级代表研究的不同部分。例如,第一层可能是不同的研究,第二层可能是每个研究中的不同实验或测量,而第三层则是参与这些实验或测量的个体。 在这种更高级别的元分析中,研究者会考虑不同层级之间的相关性,并使用更复杂的统计模型来估计效应量。这种方法可以更准确地反映数据的真实结构,从而提高元分析结果的可靠性。 总结来说,元分析是一个强大的工具,但它的有效性和可靠性取决于研究者如何处理和分析数据。 确实,学界正在探索和改进元分析的方法,以更好地处理和分析嵌套数据结构。这种改进的方法与层次模型(hierarchical model)有相似之处,都涉及到将数据的方差分解为不同的层级。 在传统的元分析中,数据的方差通常被分解为抽样方差和研究间方差。然而,对于嵌套数据结构,如多个效应量来自同一研究,我们需要考虑研究内的方差。这种三水平元分析(three-level meta-analysis)的方法现在越来越流行,因为它能够更准确地反映数据的真实结构,从而提高元分析结果的可靠性。 三水平元分析的方法将元分析的数据结构分为三个层级: 1. 第一层:不同的研究。 2. 第二层:每个研究内的不同效应量或测量。 3. 第三层:参与这些效应量或测量的个体。 这种方法允许研究者考虑不同层级之间的相关性,并使用更复杂的统计模型来估计效应量。通过这种方式,三水平元分析能够更准确地处理嵌套数据,从而提高元分析结果的准确性和严谨性。 确实,元分析是一个强大的工具,但它并不是万能的,也存在一些局限性和潜在的问题。以下是元分析领域中一些核心的反思和考虑因素: 1. 质量控制:元分析依赖于纳入的研究的质量。如果纳入了质量较低的研究,那么元分析的结果可能会受到这些研究方法学缺陷或统计错误的影响。因此,研究者需要对纳入的研究进行质量筛查,以确保元分析结果的可靠性。 2. 操作性定义的一致性:在元分析中,研究之间必须有相似的操作性定义,以确保比较的有效性。如果不同研究对同一变量的操作性定义不同,那么将这些研究的结果放在一起进行元分析可能会导致不准确的结果。 3. 效应量的解读:元分析得到的效应量解读对后续研究的影响可能比我们想象的要深远。研究者需要意识到,元分析得到的效应量并不是绝对的,也不应该成为衡量后续研究成功的唯一标准。过度强调或依赖元分析的效应量可能会导致研究者进行p-hacking或数据操纵,以获得更符合期望的结果。 因此,元分析的使用者需要对这些潜在的问题保持警惕,并在进行元分析时考虑到这些因素。通过谨慎选择研究、确保操作性定义的一致性,并合理解读效应量,研究者可以最大限度地减少这些局限性对元分析结果的影响。 好的,由于时间有限,我只能简要介绍一下元分析的基本功能。实际上,元分析还有许多高级功能,由于篇幅所限,我无法在此详细讲解。不过,我推荐大家阅读一本在线的元分析书籍,它提供了包括多层元分析、网络元分析等高级分析方法的详细介绍和代码示例。这本书在我学习元分析时非常有帮助。 此外,我还推荐大家阅读一篇关于如何进行高质量元分析的传统文章。这篇文章详细讨论了如何规范化地报告元分析,以及元分析中需要纳入的要素,这对于避免审稿过程中的常见问题非常有帮助。 对于对元分析感兴趣的读者,我推荐几本相关杂志。中文方面,心理学报和心理科学进展经常发表元分析相关的文章。英文方面,我最喜欢的是Psychological Bulletin,因为它经常发表一些超越传统元分析框架、采用新颖方法进行的元分析研究。这些研究通常涉及多个领域的理论检验,使用各种高级统计方法,使得元分析的结果更加有趣和引人注目。对于教育心理学领域的元分析,Educational Psychology Review是首选,而Journal of Personality and Social Psychology (JPSP)则经常发表社会心理学领域的元分析研究。 我想强调一下元分析的重要性。尽管元分析已经得到广泛认可,但我们仍需注意不要过分夸大它的作用,也不能忽视它在学术领域中的贡献。元分析在顶级期刊中的发表量也证明了它的价值。正确使用元分析至关重要,既要避免高估其能力,也不能低估其在学科中的地位。 如果大家有任何问题,可以随时通过邮件联系我。我的邮箱地址会提供给大家。此外,你们也可以关注我的GitHub,上面有我们研究的完整代码。虽然今天的课程中代码较为简化,以便于大家学习和上手,但实际上我们的分析要复杂得多,包含了一些个性化的操作。感兴趣的话,可以进一步探索。 好的,如果大家没有问题,那么我们今天的课程就到这里结束。现在关于元分析的资源非常丰富,但要想真正掌握元分析,最好的方法还是亲自去重复别人的元分析或者自己进行一个小规模的元分析。我个人经验,进行元分析不是一次性的任务。你的文献检索、数据分析可能需要多次迭代,过程中可能需要一些调整和完善。因此,一开始就确保你的文件、编码簿等非常规范是非常重要的,这样在后续的重复和迭代过程中,你会有一个明确的依据。现在,元分析也不仅仅是发表一篇独立的文章,很多人在他们的毕业论文中,如果包含了多个研究,最终会进行一个小型的元分析,以综合这些研究的整体效应量。这种做法在国内也相当普遍,这种小型的元分析被称为mini meta-analysis。确实,很多人都在这样做。 好的, 那我们今天的课就到这里。各位同学,我们也下课。 "],["补充1如何进行基本的数据分析-相关与回归.html", "Chapter 15 补充1:如何进行基本的数据分析: 相关与回归 15.1 什么是相关 15.2 相关-代码实现 15.3 什么是回归", " Chapter 15 补充1:如何进行基本的数据分析: 相关与回归 15.1 什么是相关 相关分析是一种统计技术,用于测量两个变量之间线性关系的强度和方向。它涉及计算相关系数,相关系数的取值为[-1, 1],其中-1表示两个变量完全呈负相关,0表示无相关,1表示完全正相关。 Pearson(皮尔逊)相关 Pearson相关系数是最常用的方法之一,用于衡量两个变量之间的线性相关程度,取值范围为-1到1之间,其值越接近于1或-1表示两个变量之间的线性相关程度越强,而越接近于0则表示两个变量之间线性相关程度越弱或不存在线性相关性。皮尔逊相关系数适用场景是呈正态分布的连续变量,当数据集的数量超过500时,可以近似认为数据呈正态分布 Spearman(斯皮尔曼)相关 Spearman等级相关系数用于衡量两个变量之间的关联程度,但不要求变量呈现线性关系,而是通过对变量的等级进行比较来计算它们之间的相关性。 与皮尔逊相关系数相比,斯皮尔曼相关系数没有那些限制,比如要符合正态分布、样本容量要超过一定数量(比如30个). Kendall(肯德尔)相关 Kendall秩相关系数也用于衡量两个变量之间的关联程度,其计算方式与Spearman等级相关系数类似,但它是基于每个变量的秩来计算它们之间的相关性。 肯德尔相关系数,又称肯德尔秩相关系数,它也是一种秩相关系数,不过,它的目标对象是有序的类别变量,比如名次、年龄段等。 假想问题 在penguin数据中,参与者压力和自律水平的相关水平? 15.2 相关-代码实现 # 检查是否已安装 pacman if (!requireNamespace("pacman", quietly = TRUE)) { install.packages("pacman") } # 如果未安装,则安装包 # 使用p_load来载入需要的包 pacman::p_load("tidyverse", "bruceR", "performance") # 或者直接使用 easystats这个系列 pacman::p_load("tidyverse", "bruceR", "easystats") 检查工作路径 - 导入原始数据 # 检查工作路径 getwd() ## [1] "/Users/hcp4715/Library/CloudStorage/OneDrive-Personal/Teaching/Grad_R_course/R4PsyBook/bookdown_files/Books/Book" #读取数据 df.pg.raw <- read.csv('./data/penguin/penguin_rawdata.csv', header = T, sep=",", stringsAsFactors = FALSE) 首先需要将数据导入R中,并进行数据清洗和转换。可以使用Tidyverse包中的函数来选择和转换数据。在进行反向计分后,使用mutate函数来计算每个问卷的得分。 然后选择性别、压力和自我控制这三个变量,并使用Bruce R中的相关分析方法来计算它们之间的相关性。 需要注意的是,当有多个变量需要进行两两相关性分析时,需要进行P值的多重性校正。这里最后得到的是一个宽数据,需要在此基础上进行进一步的分析。 df.pg.corr <- df.pg.raw %>% dplyr::filter(sex > 0 & sex < 3) %>% # 筛选出男性和女性的数据 dplyr::select(sex, starts_with("scontrol"), starts_with("stress")) %>% # 筛选出需要的变量 dplyr::mutate(across(c(scontrol2, scontrol3,scontrol4, scontrol5,scontrol7, scontrol9, scontrol10,scontrol12,scontrol13, stress4, stress5, stress6,stress7, stress9, stress10,stress13), ~ case_when(. == '1' ~ '5', . == '2' ~ '4', . == '3' ~ '3', . == '4' ~ '2', . == '5' ~ '1', TRUE ~ as.character(.))) ) %>% # 反向计分修正 dplyr::mutate(across(starts_with("scontrol") | starts_with("stress"), ~ as.numeric(.)) ) %>% # 将数据类型转化为numeric dplyr::mutate(stress_mean = rowMeans(select(.,starts_with("stress")), na.rm = T), scontrol_mean = rowMeans(select(., starts_with("scontrol")), na.rm = T) ) %>% # 根据子项目求综合平均 dplyr::select(sex, stress_mean, scontrol_mean) 使用head查看一下前五行 bruceR::Corr() results.Corr <- capture.output({ bruceR::Corr(data = df.pg.corr[,c(2,3)], file = "./output/chp8/Corr.doc") }) writeLines(results.Corr, "./output/chp8/Corr.md") # .md最整齐 Bruce R默认采用pearson相关,也可以选择使用spearman或者kendall。如果有多个即两个以上的个变量,两两之间计算相关,往往还需要对P值进行多重校正。如果只有两个变量,就不需要了。 在保存文件时,要注意文件名和文件类型的后缀。 也可以用散点图来展示变量之间的相关性。 #绘制相关散点图 pairs(df.pg.corr[,c(2,3)]) RStudio有4个panel,最右下角的面板是用来画图的,有时运行画图命令之后图没有输出,可能就是由于这个面板的区域留得不够大,导致图无法呈现;这种情况下只需要把面板拖拽至合适的大小即可。 成功之后会自动输出一张图,图上是stress_mean和control_mean这两个变量的相关矩阵。 图的右侧是从-1到1的legend图例,颜色越深相关越高、颜色越浅相关越低;矩阵中的数字即为两个变量的相关系数,这里的值为0.05,可见这两个变量相关性很弱;如果相关显著,在相关系数的右上角会自动呈现星号,这个图中没有出现,表明二者相关并不显著。 结果也可以输出为word文档 但是需要精确地指出所需要的变量,也就是对应的columns。如果不指定,它会将所有变量两两组合计算相关;例如计算性别和一个连续变量之间的相关,但是这种情况下不能使用pearson。 如果一个二分变量(如性别)和一个连续变量进行相关分析,不应该使用pearson,应该用点二列相关,或者直接使用t检验。 在ezstates中有一个类似的包correlation,它的好处在于它会输出更多的信息,因此很适用于探索性的分析的阶段(这个时候对于变量之间的关系没有大概的了解,需要探索一下)。 它就会以一个可视的形式展现变量间的关系,便于快速发现哪两个变量之间相关变强,哪两个变量相对弱。 15.3 什么是回归 回归模型是通过对观测数据进行拟合来描述变量之间的关系。回归允许我们估计因变量如何随着自变量的变化而变化。 很多经典统计(T-test,方差分析,相关,回归分析)都有共同的技术,GLM。 广义线性模型(Generalized Linear Model,GLM), 或者线性混合模型(Generalized Linear Mixed Model),本质上就是Y=A+BX。通常会将Y作为预测项,有时候会在预测项上加上一个误差,这是可以扩展的,我们也可以假设他是一个非线性的关系,当它是线性的时候,我们实际上是在预测一个正态分布的均值,如果我们不是预测均值,我们可以通过一个转换,使用一个链接函数,转化后的参数仍然能用这种方法来组合预测,这个自变量或者因变量可以是非连续的变量。 ## 回归-代码实现 \\[y = \\beta_0 + \\beta_1 x_1 + \\beta_2 x_2 + ... + \\beta_p x_p + \\epsilon\\] 假想的问题 “在penguin数据中,我们希望找到参与者压力和自律水平关于性别的回归?” # 数据预处理 df.pg.lm <- df.pg.corr %>% # 将sex变量转化为factor类型 mutate(sex = as.factor(sex)) %>% # 自变量为scontrol和sex group_by(scontrol_mean,sex) %>% # 根据分组获得stress的平均值,。groups属性保留了之前的group_by summarise(stress_Mean = mean(stress_mean),.groups = 'keep') 希望找到压力,自律以及性别之间的关系。 在此之前,首先做了correlation。 在数据预处理阶段,把性别转换为factor类型,然后再把数据进行group by。 数据预处理之后,可以做一个探索分析,先用ggPlot画一个图,用不同的颜色分别表示男性和女性的数据。 绘制散点之后继续绘制趋势线时,使用了geom_smooth(), 这里自动使用了formula,y ~ x,表示用x来预测y。这里y即为strength, x即为self-control。两条线看起来是交叉的。图中呈现的粗略的结果支持在女性中存在相关, 在男性当中不存在,之后可以对它进行检验。 # 使用ggplot()画图 ggplot(df.pg.lm, aes(x = scontrol_mean, y = stress_Mean, color = sex)) + geom_point() + geom_smooth(method = "lm") + scale_color_discrete(name = "Gender", labels = c("Female", "Male")) + theme_minimal() ## `geom_smooth()` using formula = 'y ~ x' 直接使用lm linear model,它是base中的一个函数。这里是R中进行回归常用的公式写法, 也就是左边为因变量,右边为自变量, 运行后R就自动输出结果。 因此在R中因变量自变量要选择清楚。 这个例子中因变量是主观压力stress,三个自变量分别是sex性别、scontrol_mean、以及二者的交互作用(因为sex和scontrol_mean之间可能存在交互作用),那么代码就可以写成mod <- lm(stress_Mean ~ scontrol_mean + sex + scontrol_mean:sex, df.pg.lm)。 如果后续需要做线性回归及相关的分析,需要掌握这种写法。 公式会作为第一个参数被输入到函数中,也可以更完善的补全它的argument,也就是formula=之后的内容,在这个例子中其他的argument被省略了、为默认值。 运行代码之后,线性回归的结果被保存在变量mod中,然后可以用mod_summary将结果提取出来。 # 建立回归模型 mod <- lm(stress_Mean ~ scontrol_mean + sex + scontrol_mean:sex, df.pg.lm) # 使用bruceR::model_summary()输出结果 result.lm <- capture.output({ model_summary(mod, std = T, file = "./output/chp8/Lm.doc") }) writeLines(result.lm, "./output/chp8/Lm.md") 输出的结果中包括显著性检验,可以看到selfcontrol、sex以及它们之间的交互作用是否显著。 整个模型的一个决定系数指这个模型解释了多少变异,adjusted是调整之后的数据。 number of observations 为53。 推荐一个很好的包:performance,它可以进行常用的统计模型分析,包括Anova、T-test等。它会检查模型的各个方面,比如posterior predictive check,inlinearity,方差齐性,共线性,正态性检验等等。 它可以告诉你数据是否符合做这个模型的假定的assumption。 它还可以进行模型比较,比如我们有模型1是有交互作用的,模型2是没有交互作用的,它可以对模型1和模型2进行比较,告诉你哪个模型能够更合理地解释数据的变异,可以用model comparison这个函数来对三个模型进行比较,这个在easy state里面有。 在这一部分,有两个内容需要强调。 首先是需要掌握公式如何写,尤其是对于第一次在R中接触回归模型的同学,需要注意这个固定的写公式套路。 其次是如果需要做简单的回归分析,那么使用LM就可以;在心理统计学中,一个变量对另外一个变量的预测作用、多个变量对一个变量的预测作用、变量为二分变量这些情况,LM都可以适用。 可以自己试着编写一条进行回归模型比较的函数,并把相关指标建议附在控制台的输出结果中 P_anova<- function(model1, model2, file = NULL){ if(is.null(file)){ cat("\\033[1;32m Tips by P: AIC:Lower values indicate better fit for model comparison. BIC:Lower values indicate better fit for model comparison. liglik: The smaller the absolute value of logLik, the better the model fits. deviance: The smaller the deviance value, the better the model fit. Chisq: chi-square value(χ²) of likelihood ratio test. Pr(>Chisq): P-value of likelihood ratio test. \\033[0m\\n") cat("\\033[1;32m Model Comparison Table \\033[0m\\n") result<-anova(model1,model2) print_table(result) }else{ cat("\\033[1;32m Tips by P: AIC:Lower values indicate better fit for model comparison. BIC:Lower values indicate better fit for model comparison. liglik: The smaller the absolute value of logLik, the better the model fits. deviance: The smaller the deviance value, the better the model fit. Chisq: chi-square value(χ²) of likelihood ratio test. Pr(>Chisq): P-value of likelihood ratio test. \\033[0m\\n") cat("\\033[1;32m Model Comparison Table \\033[0m\\n") result<-anova(model1,model2) print_table(result) print_table(table, file = file) } } 可以自己试着写一条建立回归模型的函数,基lmerTest、bruceR和一些数据清理的包,这行代码可以输出两个模型(可以是:lm、glm(二项分布、泊松分布、高斯分布)、lmer)的方差分析表、回归系数、标准化系数、(随机效应)、随机效应的显著性检验、输出各自模型的三线表到word文档、两个模型之间的比较(AIC、BIC、LogLik以及似然比检验 /F 检验的结果)。Tips: compare.except指模型二与模型一相比剔除的解释变量,暂不支持对多层线性模型随机斜率的比较。 P_regress<- function(formula, data, family = NULL, compare.except = NULL, digits = 3, nsmall = digits, robust = FALSE, cluster = NULL, test.rand = FALSE, file1 = NULL, file2 = NULL, file3 = NULL){ if(is.null(compare.except)){ if (class(formula) == "formula" & grepl("\\\\|", deparse(formula))){ regress(formula = formula, data = data, family = family, digits = digits, nsmall = digits, robust = robust, cluster = cluster, test.rand = test.rand) model1 = lmer(formula = formula, data = data) if(!is.null(file1)){ print_table(model1,file = file1)} ranova = ranova(model1) cat("\\033[1;37m The significance test of random effects \\033[0m\\n") print_table(ranova) }else if(class(formula) == "formula" & grepl("family", deparse(formula))){ regress(formula = formula, data = data, family = family, digits = digits, nsmall = digits, robust = robust, cluster = cluster, test.rand = test.rand) if(family == "binomial"){ model1 = glm(formula = formula, data = data,family = binomial()) if(!is.null(file1)){ print_table(model1, file = file1)} }else if(family == "gaussian"){ model1 = glm(formula = formula, data = data,family = gaussian()) if(!is.null(file1)){ print_table(model1, file = file1)} }else{ model1 = glm(formula = formula, data = data,family = poisson()) if(!is.null(file1)){ print_table(model1, file = file1)} } }else{ regress(formula = formula, data = data, family = family, digits = digits, nsmall = digits, robust = robust, cluster = cluster, test.rand = test.rand) model1 = lm(formula = formula, data = data) if(!is.null(file1)){ print_table(model1, file = file1)} } }else{ if (class(formula) == "formula" & grepl("\\\\|", deparse(formula))){ regress(formula = formula, data = data, family = family, digits = digits, nsmall = digits, robust = robust, cluster = cluster, test.rand = test.rand) model1 = lmer(formula = formula, data = data) if(!is.null(file1)){ print_table(model1, file = file1)} ranova = ranova(model1) cat("\\033[1;37m The significance test of random effects \\033[0m\\n") print_table(ranova) # model1 ranova string<- formula_paste(formula) string1<-gsub(" ", " \\\\\\\\+ ", compare.except) new_string <- gsub(string1, "", string) formula = as.formula(new_string) model2 = lmer(formula = formula, data = data) regress(formula = formula, data = data, family = family, digits = digits, nsmall = digits, robust = robust, cluster = cluster, test.rand = test.rand) #model2 ranova = ranova(model2) cat("\\033[1;37m The significance test of random effects \\033[0m\\n") print_table(ranova) if(!is.null(file2)){ print_table(model2, file = file2)} #model2 ranova P_anova(model1, model2,file = file3)#模型比较 }else if(class(formula) == "formula" & grepl("family", deparse(formula))){ regress(formula = formula, data = data, family = family, digits = digits, nsmall = digits, robust = robust, cluster = cluster, test.rand = test.rand) #model1 if(family == "binomial"){ model1 = glm(formula = formula, data = data,family = binomial()) if(!is.null(file1)){ print_table(model1, file = file1)} string<- formula_paste(formula) string1<-gsub(" ", " \\\\\\\\+ ", compare.except) new_string <- gsub(string1, "", string) formula = as.formula(new_string) regress(formula = formula, data = data, family = family, digits = digits, nsmall = digits, robust = robust, cluster = cluster, test.rand = test.rand) #model2 if(!is.null(file2)){ print_table(model2, file = file2)} P_anova(model1, model2,file = file3) }else if(family == "gaussian"){ model1 = glm(formula = formula, data = data,family = gaussian()) if(!is.null(file1)){ print_table(model1, file = file1)} string<- formula_paste(formula) string1<-gsub(" ", " \\\\\\\\+ ", compare.except) new_string <- gsub(string1, "", string) formula = as.formula(new_string) regress(formula = formula, data = data, family = family, digits = digits, nsmall = digits, robust = robust, cluster = cluster, test.rand = test.rand) #model2 if(!is.null(file2)){ print_table(model2, file = file2)} P_anova(model1, model2,file = file3) }else{ model1 = glm(formula = formula, data = data,family = poisson()) if(!is.null(file1)){ print_table(model1, file = file1)} string<- formula_paste(formula) string1<-gsub(" ", " \\\\\\\\+ ", compare.except) new_string <- gsub(string1, "", string) formula = as.formula(new_string) regress(formula = formula, data = data, family = family, digits = digits, nsmall = digits, robust = robust, cluster = cluster, test.rand = test.rand) #model2 if(!is.null(file2)){ print_table(model2, file = file2)} if(!is.null(file3)){ P_anova(model1, model2,file = file3)} } }else{ regress(formula = formula, data = data, family = family, digits = digits, nsmall = digits, robust = robust, cluster = cluster, test.rand = test.rand) model1 = lm(formula = formula, data = data) if(!is.null(file1)){ print_table(model1, file = file1)} string<- formula_paste(formula) string1<-gsub(" ", " \\\\\\\\+ ", compare.except) new_string <- gsub(string1, "", string) formula = as.formula(new_string) regress(formula = formula, data = data, family = family, digits = digits, nsmall = digits, robust = robust, cluster = cluster, test.rand = test.rand) #model2 if(!is.null(file2)){ print_table(model2, file = file2)} P_anova(model1, model2,file = file3) } } } 可以自己试着编写一条回归分析中简单斜率检验的函数,并输出简单斜率检验图。适用于绝大多数调节模型。 P_simpleslopes<- function(y,x,mod,mod2 = NULL, data, main.title = "Simple Slope Test Graph", way = 2){ P_dif<- function(x){ return(length(unique(x))) } if(is.null(mod2)){ datamod = unlist(select(data, mod)) if(P_dif(datamod) == 2){ a = paste(x,mod, sep = " * ") formula = paste(y,a,sep = " ~ ") cat("\\033[37m PROCESS Model Code : 1 (Hayes, 2018; www.guilford.com/p/hayes3)\\033[0m\\n") cat("\\033[34m- Outcome (Y) :", paste(y), "\\033[0m\\n") cat("\\033[34m- Predictor (X) :", paste(x), "\\033[0m\\n") cat("\\033[34m- Moderator variable (M) :", paste(mod), "\\033[0m\\n") cat("\\033[34m- Formula :", paste(formula), "\\033[0m\\n") data = rename(data, "y" = y, "x" = x, "mod" = mod) model = lm(formula = y ~ x * mod, data = data) result<- sim_slopes(model, pred = x, modx = mod, jnplot = T) data1 = result$slopes names(data1)[1] = mod names(data1)[2] = "Effect" names(data1)[6] = "t" cat("\\033[1;37m Simple slope:\\033[0m\\n") print_table(data1) interact_plot(model, pred = x, modx = mod, interval = TRUE, x.label = x, y.label = y ,colors = "seagreen",main.title = main.title) }else{ a = paste(x,mod, sep = " * ") formula = paste(y,a,sep = " ~ ") cat("\\033[37m PROCESS Model Code : 1 (Hayes, 2018; www.guilford.com/p/hayes3)\\033[0m\\n") cat("\\033[34m- Outcome (Y) :", paste(y), "\\033[0m\\n") cat("\\033[34m- Predictor (X) :", paste(x), "\\033[0m\\n") cat("\\033[34m- Moderator variable (M) :", paste(mod), "\\033[0m\\n") cat("\\033[34m- Formula :", paste(formula), "\\033[0m\\n") data = rename(data, "y" = y, "x" = x, "mod" = mod) model = lm(formula = y ~ x * mod, data = data) result<- sim_slopes(model, pred = x, modx = mod, jnplot = T) data1 = result$slopes a = data1[,1] a = round(a, 3) a[1] = paste(a[1], "(- SD)", sep = " ") a[2] = paste(a[2], "(Mean)", sep = " ") a[3] = paste(a[3], "(+ SD)", sep = " ") data1[,1] = a names(data1)[1] = mod names(data1)[2] = "Effect" names(data1)[6] = "t" cat("\\033[1;37m Simple slope:\\033[0m\\n") print_table(data1) interact_plot(model, pred = x, modx = mod, interval = TRUE, x.label = x, y.label = y ,colors = "seagreen",main.title = main.title) } }else{ if(way == 3){ datamod = unlist(select(data, mod)) datamod2 = unlist(select(data, mod2)) if(P_dif(datamod) == 2){ if(P_dif(datamod2) == 2){ a = paste(x, mod, mod2,sep = " * ") c = paste(y , a, sep = " ~ ") cat("\\033[37m PROCESS Model Code : 3 (Hayes, 2018; www.guilford.com/p/hayes3)\\033[0m\\n") cat("\\033[34m- Outcome (Y) :", paste(y), "\\033[0m\\n") cat("\\033[34m- Predictor (X) :", paste(x), "\\033[0m\\n") cat("\\033[34m- Moderator variable 1 (M) :", paste(mod), "\\033[0m\\n") cat("\\033[34m- Moderator variable 2 (M) :", paste(mod2), "\\033[0m\\n") cat("\\033[34m- Formula :", paste(c), "\\033[0m\\n") data = rename(data, "y" = y, "mod" = mod, "x" = x, "mod2" = mod2) model = lm(formula = y ~ x * mod * mod2, data = data) result<- sim_slopes(model, pred = x, modx = mod, mod2 = mod2, jnplot = T) slopes = result$slopes data1 = data.table(slopes[[1]]) data2 = data.table(slopes[[2]]) names(data1)[1] = paste(mod,"(mod)",sep = " ") names(data1)[2] = "Effect" names(data1)[6] = "t" cat("\\033[1;37m While",paste(mod2), "(mod2) = 0.00 :\\033[0m\\n") print_table(data1) names(data2)[1] = paste(mod,"(mod)",sep = " ") names(data2)[2] = "Effect" names(data2)[6] = "t" cat("\\033[1;37m While",paste(mod2), "(mod2) = 1.00 :\\033[0m\\n") print_table(data2) interact_plot(model, pred = x, modx = mod, mod2 = mod2, interval = TRUE, x.label = x, y.label = y ,colors = "seagreen",main.title = main.title) }else{ a = paste(x, mod, mod2,sep = " * ") c = paste(y , a, sep = " ~ ") cat("\\033[37m PROCESS Model Code : 3 (Hayes, 2018; www.guilford.com/p/hayes3)\\033[0m\\n") cat("\\033[34m- Outcome (Y) :", paste(y), "\\033[0m\\n") cat("\\033[34m- Predictor (X) :", paste(x), "\\033[0m\\n") cat("\\033[34m- Moderator variable 1 (M) :", paste(mod), "\\033[0m\\n") cat("\\033[34m- Moderator variable 2 (M) :", paste(mod2), "\\033[0m\\n") cat("\\033[34m- Formula :", paste(c), "\\033[0m\\n") data = rename(data, "y" = y, "mod" = mod, "x" = x, "mod2" = mod2) model = lm(formula = y ~ x * mod * mod2, data = data) result<- sim_slopes(model, pred = x, modx = mod, mod2 = mod2, jnplot = T) slopes = result$slopes data1 = data.table(slopes[[1]]) data2 = data.table(slopes[[2]]) data3 = data.table(slopes[[3]]) names(data1)[1] = paste(mod,"(mod)",sep = " ") names(data1)[2] = "Effect" names(data1)[6] = "t" sd = sd(unlist(select(data, mod2))) mean = round(mean(unlist(select(data, mod2))),3) b = round(mean - sd,3) c = round(mean + sd,3) cat("\\033[1;37m While",paste(mod2), "(mod2) =",paste(b), "(- 1 SD):\\033[0m\\n") print_table(data1) names(data2)[1] = paste(mod,"(mod)",sep = " ") names(data2)[2] = "Effect" names(data2)[6] = "t" cat("\\033[1;37m While",paste(mod2), "(mod2) =",paste(mean), "(Mean):\\033[0m\\n") print_table(data2) names(data3)[1] = paste(mod,"(mod)",sep = " ") names(data3)[2] = "Effect" names(data3)[6] = "t" cat("\\033[1;37m While",paste(mod2), "(mod2) =",paste(c), "(+ 1 SD):\\033[0m\\n") print_table(data3) interact_plot(model, pred = x, modx = mod, mod2 = mod2, interval = TRUE, x.label = x, y.label = y ,colors = "seagreen",main.title = main.title) } }else{ if(P_dif(datamod2) == 2){ a = paste(x, mod, mod2,sep = " * ") c = paste(y , a, sep = " ~ ") cat("\\033[37m PROCESS Model Code : 3 (Hayes, 2018; www.guilford.com/p/hayes3)\\033[0m\\n") cat("\\033[34m- Outcome (Y) :", paste(y), "\\033[0m\\n") cat("\\033[34m- Predictor (X) :", paste(x), "\\033[0m\\n") cat("\\033[34m- Moderator variable 1 (M) :", paste(mod), "\\033[0m\\n") cat("\\033[34m- Moderator variable 2 (M) :", paste(mod2), "\\033[0m\\n") cat("\\033[34m- Formula :", paste(c), "\\033[0m\\n") data = rename(data, "y" = y, "mod" = mod, "x" = x, "mod2" = mod2) model = lm(formula = y ~ x * mod * mod2, data = data) result<- sim_slopes(model, pred = x, modx = mod, mod2 = mod2, jnplot = T) slopes = result$slopes data1 = data.table(slopes[[1]]) data2 = data.table(slopes[[2]]) a = unlist(data1[,1]) a = round(a, 3) a[1] = paste(a[1], "(- SD)", sep = " ") a[2] = paste(a[2], "(Mean)", sep = " ") a[3] = paste(a[3], "(+ SD)", sep = " ") data1[,1] = a names(data1)[1] = paste(mod,"(mod)",sep = " ") names(data1)[2] = "Effect" names(data1)[6] = "t" cat("\\033[1;37m While",paste(mod2), "(mod2) = 0.00 :\\033[0m\\n") print_table(data1) a = unlist(data2[,1]) a = round(a, 3) a[1] = paste(a[1], "(- SD)", sep = " ") a[2] = paste(a[2], "(Mean)", sep = " ") a[3] = paste(a[3], "(+ SD)", sep = " ") data2[,1] = a names(data2)[1] = paste(mod,"(mod)",sep = " ") names(data2)[2] = "Effect" names(data2)[6] = "t" cat("\\033[1;37m While",paste(mod2), "(mod2) = 1.00 :\\033[0m\\n") print_table(data2) interact_plot(model, pred = x, modx = mod, mod2 = mod2, interval = TRUE, x.label = x, y.label = y ,colors = "seagreen",main.title = main.title) }else{ a = paste(x, mod, mod2,sep = " * ") c = paste(y , a, sep = " ~ ") cat("\\033[37m PROCESS Model Code : 3 (Hayes, 2018; www.guilford.com/p/hayes3)\\033[0m\\n") cat("\\033[34m- Outcome (Y) :", paste(y), "\\033[0m\\n") cat("\\033[34m- Predictor (X) :", paste(x), "\\033[0m\\n") cat("\\033[34m- Moderator variable 1 (M) :", paste(mod), "\\033[0m\\n") cat("\\033[34m- Moderator variable 2 (M) :", paste(mod2), "\\033[0m\\n") cat("\\033[34m- Formula :", paste(c), "\\033[0m\\n") data = rename(data, "y" = y, "mod" = mod, "x" = x, "mod2" = mod2) model = lm(formula = y ~ x * mod * mod2, data = data) result<- sim_slopes(model, pred = x, modx = mod, mod2 = mod2, jnplot = T) slopes = result$slopes data1 = data.table(slopes[[1]]) data2 = data.table(slopes[[2]]) data3 = data.table(slopes[[3]]) a = unlist(data1[,1]) a = round(a, 3) a[1] = paste(a[1], "(- SD)", sep = " ") a[2] = paste(a[2], "(Mean)", sep = " ") a[3] = paste(a[3], "(+ SD)", sep = " ") data1[,1] = a names(data1)[1] = paste(mod,"(mod)",sep = " ") names(data1)[2] = "Effect" names(data1)[6] = "t" sd = sd(unlist(select(data, mod2))) mean = round(mean(unlist(select(data, mod2))),3) b = round(mean - sd,3) c = round(mean + sd,3) cat("\\033[1;37m While",paste(mod2), "(mod2) =",paste(b), "(- 1 SD):\\033[0m\\n") print_table(data1) a = unlist(data2[,1]) a = round(a, 3) a[1] = paste(a[1], "(- SD)", sep = " ") a[2] = paste(a[2], "(Mean)", sep = " ") a[3] = paste(a[3], "(+ SD)", sep = " ") data2[,1] = a names(data2)[1] = paste(mod,"(mod)",sep = " ") names(data2)[2] = "Effect" names(data2)[6] = "t" cat("\\033[1;37m While",paste(mod2), "(mod2) =",paste(mean), "(Mean):\\033[0m\\n") print_table(data2) a = unlist(data3[,1]) a = round(a, 3) a[1] = paste(a[1], "(- SD)", sep = " ") a[2] = paste(a[2], "(Mean)", sep = " ") a[3] = paste(a[3], "(+ SD)", sep = " ") data3[,1] = a names(data3)[1] = paste(mod,"(mod)",sep = " ") names(data3)[2] = "Effect" names(data3)[6] = "t" cat("\\033[1;37m While",paste(mod2), "(mod2) =",paste(c), "(+ 1 SD):\\033[0m\\n") print_table(data3) interact_plot(model, pred = x, modx = mod, mod2 = mod2, interval = TRUE, x.label = x, y.label = y ,colors = "seagreen",main.title = main.title) } } }else{ datamod = unlist(select(data, mod)) datamod2 = unlist(select(data, mod2)) if(P_dif(datamod) == 2){ if(P_dif(datamod2) == 2){ a = paste(x, mod, sep = " * ") b = paste(x, mod2, sep = " * ") ab = paste(a, b, sep = " + ") c = paste(y , ab, sep = " ~ ") cat("\\033[37m PROCESS Model Code : 2 (Hayes, 2018; www.guilford.com/p/hayes3)\\033[0m\\n") cat("\\033[34m- Outcome (Y) :", paste(y), "\\033[0m\\n") cat("\\033[34m- Predictor (X) :", paste(x), "\\033[0m\\n") cat("\\033[34m- Moderator variable 1 (M) :", paste(mod), "\\033[0m\\n") cat("\\033[34m- Moderator variable 2 (M) :", paste(mod2), "\\033[0m\\n") cat("\\033[34m- Formula :", paste(c), "\\033[0m\\n") data = rename(data, "y" = y, "mod" = mod, "x" = x, "mod2" = mod2) model = lm(formula = y ~ x * mod + x * mod2, data = data) result<- sim_slopes(model, pred = x, modx = mod, mod2 = mod2, jnplot = T) slopes = result$slopes data1 = data.table(slopes[[1]]) data2 = data.table(slopes[[2]]) names(data1)[1] = paste(mod,"(mod)",sep = " ") names(data1)[2] = "Effect" names(data1)[6] = "t" cat("\\033[1;37m While",paste(mod2), "(mod2) = 0.00 :\\033[0m\\n") print_table(data1) names(data2)[1] = paste(mod,"(mod)",sep = " ") names(data2)[2] = "Effect" names(data2)[6] = "t" cat("\\033[1;37m While",paste(mod2), "(mod2) = 1.00 :\\033[0m\\n") print_table(data2) interact_plot(model, pred = x, modx = mod, mod2 = mod2, interval = TRUE, x.label = x, y.label = y ,colors = "seagreen",main.title = main.title) }else{ a = paste(x, mod, sep = " * ") b = paste(x, mod2, sep = " * ") ab = paste(a, b, sep = " + ") c = paste(y , ab, sep = " ~ ") cat("\\033[37m PROCESS Model Code : 2 (Hayes, 2018; www.guilford.com/p/hayes3)\\033[0m\\n") cat("\\033[34m- Outcome (Y) :", paste(y), "\\033[0m\\n") cat("\\033[34m- Predictor (X) :", paste(x), "\\033[0m\\n") cat("\\033[34m- Moderator variable 1 (M) :", paste(mod), "\\033[0m\\n") cat("\\033[34m- Moderator variable 2 (M) :", paste(mod2), "\\033[0m\\n") cat("\\033[34m- Formula :", paste(c), "\\033[0m\\n") data = rename(data, "y" = y, "mod" = mod, "x" = x, "mod2" = mod2) model = lm(formula = y ~ x * mod + x * mod2, data = data) result<- sim_slopes(model, pred = x, modx = mod, mod2 = mod2, jnplot = T) slopes = result$slopes data1 = data.table(slopes[[1]]) data2 = data.table(slopes[[2]]) data3 = data.table(slopes[[3]]) names(data1)[1] = paste(mod,"(mod)",sep = " ") names(data1)[2] = "Effect" names(data1)[6] = "t" sd = sd(unlist(select(data, mod2))) mean = round(mean(unlist(select(data, mod2))),3) b = round(mean - sd,3) c = round(mean + sd,3) cat("\\033[1;37m While",paste(mod2), "(mod2) =",paste(b), "(- 1 SD):\\033[0m\\n") print_table(data1) names(data2)[1] = paste(mod,"(mod)",sep = " ") names(data2)[2] = "Effect" names(data2)[6] = "t" cat("\\033[1;37m While",paste(mod2), "(mod2) =",paste(mean), "(Mean):\\033[0m\\n") print_table(data2) names(data3)[1] = paste(mod,"(mod)",sep = " ") names(data3)[2] = "Effect" names(data3)[6] = "t" cat("\\033[1;37m While",paste(mod2), "(mod2) =",paste(c), "(+ 1 SD):\\033[0m\\n") print_table(data3) interact_plot(model, pred = x, modx = mod, mod2 = mod2, interval = TRUE, x.label = x, y.label = y ,colors = "seagreen",main.title = main.title) } }else{ if(P_dif(datamod2) == 2){ a = paste(x, mod, sep = " * ") b = paste(x, mod2, sep = " * ") ab = paste(a, b, sep = " + ") c = paste(y , ab, sep = " ~ ") cat("\\033[37m PROCESS Model Code : 2 (Hayes, 2018; www.guilford.com/p/hayes3)\\033[0m\\n") cat("\\033[34m- Outcome (Y) :", paste(y), "\\033[0m\\n") cat("\\033[34m- Predictor (X) :", paste(x), "\\033[0m\\n") cat("\\033[34m- Moderator variable 1 (M) :", paste(mod), "\\033[0m\\n") cat("\\033[34m- Moderator variable 2 (M) :", paste(mod2), "\\033[0m\\n") cat("\\033[34m- Formula :", paste(c), "\\033[0m\\n") data = rename(data, "y" = y, "mod" = mod, "x" = x, "mod2" = mod2) model = lm(formula = y ~ x * mod + x * mod2, data = data) result<- sim_slopes(model, pred = x, modx = mod, mod2 = mod2, jnplot = T) slopes = result$slopes data1 = data.table(slopes[[1]]) data2 = data.table(slopes[[2]]) a = unlist(data1[,1]) a = round(a, 3) a[1] = paste(a[1], "(- SD)", sep = " ") a[2] = paste(a[2], "(Mean)", sep = " ") a[3] = paste(a[3], "(+ SD)", sep = " ") data1[,1] = a names(data1)[1] = paste(mod,"(mod)",sep = " ") names(data1)[2] = "Effect" names(data1)[6] = "t" cat("\\033[1;37m While",paste(mod2), "(mod2) = 0.00 :\\033[0m\\n") print_table(data1) a = unlist(data2[,1]) a = round(a, 3) a[1] = paste(a[1], "(- SD)", sep = " ") a[2] = paste(a[2], "(Mean)", sep = " ") a[3] = paste(a[3], "(+ SD)", sep = " ") data2[,1] = a names(data2)[1] = paste(mod,"(mod)",sep = " ") names(data2)[2] = "Effect" names(data2)[6] = "t" cat("\\033[1;37m While",paste(mod2), "(mod2) = 1.00 :\\033[0m\\n") print_table(data2) interact_plot(model, pred = x, modx = mod, mod2 = mod2, interval = TRUE, x.label = x, y.label = y ,colors = "seagreen",main.title = main.title) }else{ a = paste(x, mod, sep = " * ") b = paste(x, mod2, sep = " * ") ab = paste(a, b, sep = " + ") c = paste(y , ab, sep = " ~ ") cat("\\033[37m PROCESS Model Code : 2 (Hayes, 2018; www.guilford.com/p/hayes3)\\033[0m\\n") cat("\\033[34m- Outcome (Y) :", paste(y), "\\033[0m\\n") cat("\\033[34m- Predictor (X) :", paste(x), "\\033[0m\\n") cat("\\033[34m- Moderator variable 1 (M) :", paste(mod), "\\033[0m\\n") cat("\\033[34m- Moderator variable 2 (M) :", paste(mod2), "\\033[0m\\n") cat("\\033[34m- Formula :", paste(c), "\\033[0m\\n") data = rename(data, "y" = y, "mod" = mod, "x" = x, "mod2" = mod2) model = lm(formula = y ~ x * mod + x * mod2, data = data) result<- sim_slopes(model, pred = x, modx = mod, mod2 = mod2, jnplot = T) slopes = result$slopes data1 = data.table(slopes[[1]]) data2 = data.table(slopes[[2]]) data3 = data.table(slopes[[3]]) a = unlist(data1[,1]) a = round(a, 3) a[1] = paste(a[1], "(- SD)", sep = " ") a[2] = paste(a[2], "(Mean)", sep = " ") a[3] = paste(a[3], "(+ SD)", sep = " ") data1[,1] = a names(data1)[1] = paste(mod,"(mod)",sep = " ") names(data1)[2] = "Effect" names(data1)[6] = "t" sd = sd(unlist(select(data, mod2))) mean = round(mean(unlist(select(data, mod2))),3) b = round(mean - sd,3) c = round(mean + sd,3) cat("\\033[1;37m While",paste(mod2), "(mod2) =",paste(b), "(- 1 SD):\\033[0m\\n") print_table(data1) a = unlist(data2[,1]) a = round(a, 3) a[1] = paste(a[1], "(- SD)", sep = " ") a[2] = paste(a[2], "(Mean)", sep = " ") a[3] = paste(a[3], "(+ SD)", sep = " ") data2[,1] = a names(data2)[1] = paste(mod,"(mod)",sep = " ") names(data2)[2] = "Effect" names(data2)[6] = "t" cat("\\033[1;37m While",paste(mod2), "(mod2) =",paste(mean), "(Mean):\\033[0m\\n") print_table(data2) a = unlist(data3[,1]) a = round(a, 3) a[1] = paste(a[1], "(- SD)", sep = " ") a[2] = paste(a[2], "(Mean)", sep = " ") a[3] = paste(a[3], "(+ SD)", sep = " ") data3[,1] = a names(data3)[1] = paste(mod,"(mod)",sep = " ") names(data3)[2] = "Effect" names(data3)[6] = "t" cat("\\033[1;37m While",paste(mod2), "(mod2) =",paste(c), "(+ 1 SD):\\033[0m\\n") print_table(data3) interact_plot(model, pred = x, modx = mod, mod2 = mod2, interval = TRUE, x.label = x, y.label = y ,colors = "seagreen",main.title = main.title) } } } } } "],["补充2从分析到手稿.html", "Chapter 16 补充2:从分析到手稿 16.1 通过Papaja撰写论文", " Chapter 16 补充2:从分析到手稿 16.1 通过Papaja撰写论文 经过之前的学习,同学们熟悉了Rmarkdown格式的书写和使用,并了解其保存形式为.rmd文件。Rmarkdown文件包含文字、代码以及对代码的编译,能够方便地记录丰富的内容,并且可以输出为html等多种格式。于是,就有人想到一站式的解决方案:直接将Rmarkdown输出为pdf文档或word文档,并通过在Rmarkdown文件中的代码对文字图片进行排版以符合出版要求。本节课即将讲到的Papaja包适用于心理学手稿的准备,符合APA第6版的版式标准。 16.1.1 Part1: Papaja包的安装 我们为本堂课准备了一个.rmd文件,如果papaja包安装成功后,打开该文件并点击knit按钮,在相同工作目录下将生成一个与.rmd相同文件名的pdf文件,该文件即为APA论文格式的例子。 注意的是,在安装papaja时可能会存在的几个问题: (1)安装速度过慢。 解决方法:开启*梯*子*下载。 (2)knit过程中提示需要更新包,点击确认后仍存在相同提示。 解决方法:手动更新该R包,例如找到该包卸载后重新下载等。 (3)有些包需要安装在用户名下的某个文件夹,出现中文目录。 解决方法:将Windows用户名改成英文。 安装完成后我们进入papaja语法的学习。 16.1.2 Part2: Papaja语法格式 (1)YAML头文件 在所有RMarkdown文件中的头部都会有一个YAML头模块,利用—分隔开,包含标题、作者、摘要等各种信息,同时该部分支持markdown语法进行编写。 在papaja的模板中,通讯作者默认数量为一个。affiliation在作者栏中编号,在下方统一存放。authornote为APA格式风格内容,用于展示该论文之前的故事(如参与哪些报告、是否为毕业论文等)。 关于参考文献,本例包含了两个.bib文件,chapter_12-r-references.bib与chapter_12-citation.bib,可以从文献管理软件中导出,papaja将在你引用参考文献时自动从这些文件中查找并使用。 linenumbers属性是一个较为常用的属性,能够为文献提供行标,方便审稿人指出需要修改的位置。 --- title : "Lecture 12: Preparing journal artical with *papaja*" shorttitle : "papaja" author: - name : "Hu Chuan-Peng" affiliation : "1" corresponding : yes # Define only one corresponding author address : "#122 Ninghai Rd, Gulou District, Nanjing" email : "hcp4715@hotmail.com" role: # Contributorship roles (e.g., CRediT, https://casrai.org/credit/) - "Conceptualization" - "Writing - Original Draft Preparation" - "Supervision" - name : "All Students" affiliation : "1,2" role: - "Writing - Original Draft Preparation" - "Writing - Review & Editing" affiliation: - id : "1" institution : "Nanjing Normal Unviersity" - id : "2" institution : "Collaborators' Affiliations" authornote: | Add complete departmental affiliations for each author here. Each new line herein must be indented, like this line. Author Note: This is for demonstration only. abstract: | Psychological science has encountered a serious replication crisis. To make self-correction of the field, researchers actively reform the current practices and increase the opennes, transparency, and reproducibility of studies in the field. Using R language for data analyses is recommended by many. With increasingly emphases on computational reproduciblity, *papaja* was developed to combine data analysis and manuscript preparation. The current chapter aims to demonstrate how to use *papaja*. We will introduce the package and key elements of the it. After the lecture, we expected students able to create an example APA manuscript using open data or examplary data we had provided at the beginning of the class. This demo and practice will further enhance the student's experience in computational reproducibility. By spreading the ideas of reproducbility and teaching papaja, this class will increase the computational reprodcubility. <!-- https://tinyurl.com/ybremelq --> keywords : "Reproducibility, R, Teaching, Demonstration" wordcount : "X" bibliography : - "chapter_12-r-references.bib" - "chapter_12-citation.bib" floatsintext : no linenumbers : yes draft : no mask : no figurelist : no tablelist : no footnotelist : no classoption : "man" output : papaja::apa6_pdf --- (2)包和文件的调用: 在定义YAML头文件之后,需要进行检查是否该文件已经调用了需要使用的包,以及是否包含了需要的文件(例如.bib格式文件)。 (3)参考文献的引用方式 如例子中(R-papaja?),这是Papaja的引用格式,包含中括号[]、(符号以及参考文献标识?)。参考文献标识为.bib文件中@后面的内容。这种标识转换后为APA标准引用,以括号的形式引用文献。 还有我们通常使用的另一种引用,即在句子中直接引用文章作者,此时只需要略去大括号即可,如@Hu_2020 16.1.3 Part3: 正文的撰写 了解并添加了YAML头文件和参考文献后,就开始撰写正文。 (1)数据的处理: 在markdown中创建chunks来存放数据处理代码 推荐命令,用于清理环境变量 # empty the global env rm(list = ls()) df.m.basic <- read.csv('./1012-lesson12/data.csv', header = T, sep=",", stringsAsFactors = FALSE) 数据处理结束后,在撰写文章时就可以通过**`**来引用数据,例如,** \\ r df.m.basic\\(Age_mean \\` **就会索引df.m.basic中的Age: 引用公式的方式也非常方便,在两个\\$中间键入符号即公式就可,如输入`\\)\\pm\\(`就会得到\\)$。 引用R包:通过如下语句` r cite_r(“chapter_12-r-references.bib”, pkgs = c(“afex”, “emmeans”, “ggplot2”, “dplyr”, “tidyr”, “patchwork”, “r-base”), withhold=FALSE) ` 即可引用,withhold=FALSE表示选中的包为白名单。上文中的七个包为最经常使用的包,可根据数据的分析代码进行修改引用。 (2)斜体书写:在绘图中将图题部分设置斜体,可用如下代码: title <- expression(paste("Senesitivity (", italic("d'"), ")")) 此时将输出Sensitivity(d)。 (3)拼图操作:将画好的多图进行拼接,可以使用patchwork包 library(patchwork) load("./1012-lesson12/p1_rt.rdata") load("./1012-lesson12/p1_dp.rdata") p1_dp + p1_rt + plot_annotation(tag_levels = 'A') + plot_layout(nrow = 1, byrow = TRUE, guides = 'collect') #plot_annotation属性:设置图片序号为A,B,C...或1,2,3... #plot_layout属性:图片怎么排布;guides——图例是否统一放置 图片引用 使用如下格式\\@ref(fig:plot1) 即可得到?? 数据分析: 注意:bruceR和papaja可能会因为输出导致冲突,在papaja中分析数据建议回归更原始的包,如在进行ANOVA分析时使用afex函数 16.1.4 总结 papaja的好处是,数据、分析、格式、参考文献等都储存在同一个工作项目中,能够更整齐更方便的撰写文章。 16.1.4.1 小作业 请大家fork我们的bookdown文件 https://github.com/hcp4715/R4PsyBook 做一个小的更改,并pull request。 "],["补充3效应量和元分析.html", "Chapter 17 补充3:效应量和元分析 17.1 效应量简介 17.2 算法实现 17.3 小练习 17.4 元分析简介 17.5 元分析实现", " Chapter 17 补充3:效应量和元分析 17.1 效应量简介 我们今天讲的内容,是在本科心理统计学没有讲的一个很重要的统计学概念——效应量。 今天的这个课程是给大家打一些基础,关于效应量和元分析的一些基础知识。并不是说上完这门课之后,大家就会有一个分析或者说能够熟练的去计算各种各样的效应量。因为大家后面会看到效应量的软件分析,其实很依赖整个研究问题和context。所以如何去解读效应不是一个技术的问题,这既需要懂得统计的方法,也需要你对研究领域很熟悉。提到效应量大家一定会想到Cohen’s d,实际上对效应量的定义是,研究者感兴趣的任何的效应量,只要研究者对它感兴趣,觉得它有意义,就可以认为它是研究中得效应量。所以效应量本质上就是效应的量(effects)。 心理学研究中的效应量通常是Cohen’s d,因为很多时候是比较不同组之间的差异,而不同组之间的差异进行比较的时候,我们希望摆脱它原始单位的影响,使之成为某种程度上没有单位的标准化统计量。而在其他的领域,很多时候关注点不一定在这种标准化的效应量。比方说,我们有的时候会看到,家庭的背景如何影响子女的教育成就。教育学的研究者不会关心标准化的效应量是多少。汇报Cohen’s d等于0.2或者0.4很难去解读它的实际意义。在这种情况下,变化家庭的收入,如每个月多1000元,子女的教育年限会如何增加,就可以把子女的教育用受教育的年份来进行一个量化,把家庭背景用诸如收入水平来进行量化。最后考察的是家庭收入对子女接受教育年限的影响,用年份或学位进行量化。这种研究直接以实际的这个生活中的这个效应作为效应量,而不是以quantity或者相关系数为效应量。所以首先大家一定要明确,效应量不是一个统计的指标,而是一个大家感兴趣的量。 在心理学的研究当中的话常常会碰到三大效应量: - d-family(difference family):如Cohen’s d, Hedges’ g - r-family(correlation family):如Pearson r, \\(R^2\\), \\(\\eta^2\\), \\(\\omega^2\\), & f - OR-family(categorical family):如odds ratio (OR), risk ratio(RR) 这些指标既说明两个变量之间有怎样的相关,也说明了某一个效应所解释的变异在总体的变异当中占多大的比例。不同的研究领域用的效应量也不一样。相对来说相关系数是最通用的,它会比d-family更加通用。如果大家看 Psychological Bulletin,心理学领域元分析发表很多的期刊,影响力非常高,它上面很多元分析都是以相关系数做为effect size。 2011年General of Experimental Psychology General杂志专门回顾了发表的文章中报告的效应量。其中发现了一个非常有趣的效应,报告最多的其实是Partial \\(\\eta\\) Squared,即\\(\\eta^{2}_{p}\\),很重要的原因是SPSS输出这个效应量。 Lakens, D. (2013). Calculating and reporting effect sizes to facilitate cumulative science: A practical primer for t-tests and ANOVAs. Frontiers in Psychology, 4, 863. 这篇文章报告了各种不同Cohen’s d的计算方式 推荐在研究中既报告效应量,也报告这个效应量的置信区间 - SPSS提供\\(\\eta^{2}_{p}\\) - JASP提供Cohen’s d,\\(\\eta^{2}_{p}\\),\\(\\eta^{2}_{G}\\) Lakens(Lakens,2013)提供了基于excel的计算程序,帮助心理学家方便的得到不同的效应量及其置信区间。 Gpower也可以计算效应量,但其输出的\\(\\eta^{2}_{p}\\)与SPSS是不一样的,需要转换。 效应量的估计最终回归到了统计中一个非常重要的概念:点估计。存在点估计也就不得不提到区间估计。所以说有效应量的点估计就会有95%的自行区间 这个参数估计啊或者点估计 在2012年的时候Cumming(Cumming, 2012) 在一本书讲到,我们应该不仅仅使用P值 还要使用effect size,confidence interval和meta-analysis这三个统计指标。它们都有一个共同特点——estimation based,即专门基于估计。 与传统的基于P值二分法相比,基于估计的指标不仅可以看到效应量是否显著,也能看到效应量的具体大小,是把心理学研究和现实的因素关联的很重要的点。例如某个效应很显著,但是其效应量非常微弱,它在社会层面便没有任何意义。有时一个非常小的效应,没有任何坏处且成本很低,当它被实施,会改变一部分人的命运,这个时候或许可以考虑把这个方法应用到现实世界。在解读效应量的时候,注意避免简单的使用所谓的small,medium和large等effect size对Cohen’s d评价,不管它的效应、样本量、研究背景等等。 17.2 算法实现 如何去计算单个研究中或单个样本中的效应量、置信区间以及如何将它们综合起来。下面先给出公式: 独立样本t-test: \\[Cohen's \\ d_s = \\frac{X_1 - X_2}{\\sqrt{SD_{pool}}} = \\frac{X_1 - X_2}{\\sqrt{\\frac{(n_1 -1)SD_1^2 + (n_2-1)SD_2^2)}{n_1+n2-2}}}\\] \\[Hedges's \\ g_s = Cohen's \\ d_s \\times (1 - \\frac{3}{4(n_1 + n_2) - 9}) \\] 配对样本t-test: \\[Cohen's \\ d_{rm} = \\frac{M_{diff}}{\\sqrt{SD_1^2 + SD_2^2 -2 \\times r \\times SD_1 \\times SD_2}} \\times \\sqrt{2(1-r)} \\] \\[Cohen's \\ d_{av} = \\sqrt{ \\frac {M_{diff}} {(SD_1 + SD_2)/2}} \\] 接下来尝试计算Cohen’s d,以match数据为例 选定感兴趣的效应量: Match条件下好我与好人的平均反应时间差异,即自我在好的情况下,它的优势效应 首先还是导入数据: rm(list = ls()) if (!requireNamespace("pacman", quietly = TRUE)) { install.packages("pacman") } # # 检查是否已安装 pacman, 如果未安装,则安装包 pacman::p_load("tidyverse", "easystats") # 使用p_load来载入需要的包 df.mt.raw <- read.csv('./data/match/match_raw.csv', # load data: header = T, sep=",", stringsAsFactors = FALSE) 应用11章的预处理数据: # from chapter 11, Chunk 3 df.mt.rt.subj <- df.mt.raw %>% dplyr::filter(ACC == 1 & RT > 0.2) %>% tidyr::extract(Shape, into = c("Valence", "Identity"), regex = "(moral|immoral)(Self|Other)", remove = FALSE) %>% dplyr::mutate(Valence = case_when(Valence == "moral" ~ "Good", Valence == "immoral" ~ "Bad"), RT_ms = RT * 1000) %>% dplyr::mutate(Valence = factor(Valence, levels = c("Good", "Bad")), Identity = factor(Identity, levels = c("Self", "Other"))) %>% dplyr::group_by(Sub, Match, Identity, Valence) %>% dplyr::summarise(RT_mean = mean(RT_ms)) %>% dplyr::ungroup() head(df.mt.rt.subj, 5) ## # A tibble: 5 × 5 ## Sub Match Identity Valence RT_mean ## <int> <chr> <fct> <fct> <dbl> ## 1 7302 match Self Good 694. ## 2 7302 match Self Bad 702. ## 3 7302 match Other Good 598. ## 4 7302 match Other Bad 666. ## 5 7302 mismatch Self Good 755. 对于我们感兴趣的效应量,计算需要知道五个变量:条件1的主水平RT、条件2主水平RT、条件1的主水平SD、条件2的主水平SD以及相关系数(可以用每个被试在两个条件之间的反应时间做一个相关)。 # from chapter 11, Chunk 3 df.mt.rt.subj.effect <- df.mt.rt.subj %>% dplyr::filter(Match == "match" & Valence == "Good") %>% dplyr::group_by(Identity) %>% dplyr::summarise(mean = mean(RT_mean), sd = sd(RT_mean)) df.mt.rt.subj.effect.wide <- df.mt.rt.subj %>% dplyr::filter(Match == "match" & Valence == "Good") %>% tidyr::pivot_wider(names_from = "Identity", values_from = "RT_mean") corr_est <- cor(df.mt.rt.subj.effect.wide$Self, df.mt.rt.subj.effect.wide$Other) 得到变量后,将Cohen’s drm公式转化为代码: Cohens_d_manu <- ((df.mt.rt.subj.effect$mean[1] - df.mt.rt.subj.effect$mean[2])/sqrt(df.mt.rt.subj.effect$sd[1]**2 + df.mt.rt.subj.effect$sd[2]**2 - 2*corr_est*df.mt.rt.subj.effect$sd[1]*df.mt.rt.subj.effect$sd[2]))*sqrt(2*(1-corr_est)) Cohens_d_manu ## [1] -0.56768 也可以使用其他方法计算,发现得到不同的效应量,原因是公式并不一致。 SelfOther_diff <- t.test(df.mt.rt.subj.effect.wide$Self, df.mt.rt.subj.effect.wide$Other, paired = TRUE) effectsize::effectsize(SelfOther_diff, paired = TRUE) ## For paired samples, 'repeated_measures_d()' provides more options. ## Cohen's d | 95% CI ## -------------------------- ## -0.48 | [-0.79, -0.16] 所以在调用R包时一定要注意查看其内部公式! 17.3 小练习 在match数据中,尝试计算自我条件下好与坏的Cohen’s d。分别使用三种方法,(1)手动;(2)effectsize工具包;(3)bruceR工具包-T test 17.4 元分析简介 想象你自己做了一个治疗研究,在平均主义这个框架之下,我们会通常认为真实的世界中存在对于该研究特定的效应量。假如这个研究被一万次重复,就会得到一万个效应,且这一万个效应会趋向于一个真实的情况,你的治疗方法是不是比没有治疗过程或选用其他的旧的治疗方法更有效率。 虽然说这个假定可能不合理,但是平均主义就是这样的假定。 假如说有很多人同时在不同地方做类似实验,把这些效应拿过来,对它们进行综合。本质上,某种程度这些实验关注着同一个效应,综合之后应该能够得到一个对真实的更好的估计,在统计过程当中是这样。所以就有了一个对效应量的综合方法——元分析。 元分析是一个统计方法,从这个角度来讲,是对已经存在的多个效应量,通过统计的方法把它进行综合。综合时需要效应量,相关系数R或者是Cohen’s d, 同时我们也需要效应量的估计误差作为权重。接下来都是技术的问题。 怎么去算这个权重呢? 在不同的方法里面或者不同的模型里面,会有自己的具体计算公式。很多时候,如果大家不想了解细节的话,可以不用去管它。如果想了解细节的话,可以去看权重到底是如何确定的。一般来说传统的元分析模型,是通过效应量变异大小的导数对它进行加权。例如对标准差的平方求导数,然后进行加权。当然还会有其他的方法,这里就不细致的展开。 元分析也是一种文章类型,例如之前提到的psychological bulletin,具有很高的影响因子。但也存在问题,有的时候元分析不被认为是实证的研究,因为其没有太多的创新性。 17.5 元分析实现 下面展示一下如何在自己已有的数据中,用R来做一个简单元分析: 把数据分成两部分, 一部分21名被试,一部分23名被试。 subjs <- unique(df.mt.rt.subj$Sub) set.seed(1234) subj_ls1 <- sample(subjs, 21) df.mt.rt.subj.ls1 <- df.mt.rt.subj %>% dplyr::filter(Sub %in% subj_ls1) df.mt.rt.subj.ls2 <- df.mt.rt.subj %>% dplyr::filter(!(Sub %in% subj_ls1)) 假定两组数据分别为实验1a与实验1b,并计算两个实验同样条件下的效应量。 与上文中计算效应量的方式一致,首先计算均值等所需变量: ## effect size of group 1 df.mt.rt.subj.effect.ls1 <- df.mt.rt.subj.ls1 %>% dplyr::filter(Match == "match" & Valence == "Good") %>% dplyr::group_by(Identity) %>% dplyr::summarise(mean = mean(RT_mean), sd = sd(RT_mean)) %>% dplyr::ungroup() %>% tidyr::pivot_wider(names_from = Identity, values_from = c(mean, sd)) colnames(df.mt.rt.subj.effect.ls1) <- c("Self_RT_M_mean","Other_RT_M_mean", "Self_RT_M_sd", "Other_RT_M_sd") df.mt.rt.subj.effect.ls1.wide <- df.mt.rt.subj.ls1 %>% dplyr::filter(Match == "match" & Valence == "Good") %>% tidyr::pivot_wider(names_from = "Identity", values_from = "RT_mean") corr_est.ls1 <- cor(df.mt.rt.subj.effect.ls1.wide$Self, df.mt.rt.subj.effect.ls1.wide$Other) df.mt.rt.subj.effect.ls1$Sample_size <- length(unique(df.mt.rt.subj.ls1$Sub)) df.mt.rt.subj.effect.ls1$ri <- corr_est.ls1 ## effect size of group 2 df.mt.rt.subj.effect.ls2 <- df.mt.rt.subj.ls2 %>% dplyr::filter(Match == "match" & Valence == "Good") %>% dplyr::group_by(Identity) %>% dplyr::summarise(mean = mean(RT_mean), sd = sd(RT_mean)) %>% dplyr::ungroup() %>% tidyr::pivot_wider(names_from = Identity, values_from = c(mean, sd)) colnames(df.mt.rt.subj.effect.ls2) <- c("Self_RT_M_mean","Other_RT_M_mean", "Self_RT_M_sd", "Other_RT_M_sd") df.mt.rt.subj.effect.ls2.wide <- df.mt.rt.subj.ls2 %>% dplyr::filter(Match == "match" & Valence == "Good") %>% tidyr::pivot_wider(names_from = "Identity", values_from = "RT_mean") corr_est.ls2 <- cor(df.mt.rt.subj.effect.ls2.wide$Self, df.mt.rt.subj.effect.ls2.wide$Other) df.mt.rt.subj.effect.ls2$Sample_size <- length(unique(df.mt.rt.subj.ls2$Sub)) df.mt.rt.subj.effect.ls2$ri <- corr_est.ls2 合并数据,计算效应量和效应量的误差,调用R包metafor: # and nrow with 1 df.mt.meta <- rbind(df.mt.rt.subj.effect.ls1, df.mt.rt.subj.effect.ls2) df.es <- metafor::escalc( measure = "SMCRH", #standardized mean change using raw score standardization with heteroscedastic population variances at the two measurement occasions (Bonett, 2008) m1i = Self_RT_M_mean, m2i = Other_RT_M_mean, sd1i = Self_RT_M_sd, sd2i = Other_RT_M_sd, ni = Sample_size, ri = ri, data = df.mt.meta ) %>% dplyr::mutate(unique_ID = c("study1a", "study1b")) 我们此时得到了两组数据的效应量以及其误差(Y1:-0.42712,Y2:-0.91145,V1:0.08473,V2:0.12716) 接着建立随机效应模型: # 随机效果模型 rma1 <- metafor::rma(yi, vi, data = df.es) 此处应该添加chapter-13meta结果与forest plot 这是最简单的可视化,在发表时还应考虑将图表绘制的更加直观与美观。 其中有许多细节,例如22的实验中,效应量数量为C82=28,在报告时需要选择合适的效应;选择的计算方法(效应量公式)也值得考虑,是手动输入还是函数中自带的公式;在一些报告中只会提供均值与标准差,而不提供相关系数,那么就只能依赖于作者报告的数据选择。 元分析还可以应用于自己的研究中: - 如果自己手中有许多实验,对这些实验进行效应量计算的工作,能使得实验更加准确(mini metaanalysis) - 在做预实验时发现效应不够显著,又做了正式实验,可以通过元分析的方式将样本量增加,也能通过元分析判断效应量是否稳定。 在使用Mini Meta-Analysis时不能只把显著的效应综合起来,只把显著效应综合起来的话,效应就被高估了* "],["references.html", "References", " References "]]
+[["第八讲回归模型一.html", "Chapter 9 第八讲:回归模型(一) 9.1 研究问题 9.2 t-test作为回归模型的特例 9.3 ANOVA & linear regression 9.4 线性回归 9.5 知识延申|单因素方差分析示例 9.6 知识延申|总结", " Chapter 9 第八讲:回归模型(一) 我们之前讨论的内容主要分为两方面。首先是学习R语言的一些基本知识,另一方面则是使用R代码来帮助我们解决描述性统计问题。如果大家还记得心理统计学中的内容,它分为描述统计和推断统计两部分。我们使用数据来进行统计推断,而R语言则可以帮助我们更灵活地实现各种统计方法。 纯粹的R代码学习 → 使用R语言来实现统计知识。当然,这种灵活性既有好处也有坏处。好处是我们有很多选择,坏处则是面对众多选择,我们可能会不知道如何选择。但是,如果我们能够度过刚开始不知道如何选择的阶段,后面就能更好地运用统计知识。另外,在接下来的课程中,我们将重点讨论回归模型,例如回归模型一、回归模型二和回归模型三。 我们之所以关注回归模型,是因为我们希望借此机会将大家在心理学、社会科学研究中常用的统计检验统一到回归框架下。这也是近年来一些研究者推荐的做法。实际上,心理学使用的统计方法与其他学科并没有本质区别,只是大家的偏好问题。在心理学领域中,最常用的方法便是各种回归模型的特例。 9.1 研究问题 我们从研究问题开始,先来回顾一下人类企鹅计划的关键变量。其中一个是恋爱状态,另一个是核心温度,还有一个是社交复杂程度,以及赤道距离。在刚开始进行数据分析时,我们可能没有特别明确的假设,但我们想要了解社会关系,尤其是亲密关系,是否会影响我们的体温,是否能帮助我们调节核心温度。 我们可以使用我们知道的统计方法进行一些探索性分析,比如检验恋爱状态和赤道距离之间是否存在交互作用。大家还记得中介模型吗?其中一个变量是社交复杂程度,另一个是核心温度,中间有一个变量是赤道距离。研究发现,在不同的情侣关系群体中,这三个变量之间的关系是不一样的。 在接下来的课程中,我们将回顾一些关键概念,并进行一些常用的统计检验,包括t检验和方差分析。我们还将介绍为什么t检验和方差分析实际上是线性回归的特例。关于第一个研究问题,我们主要关注恋爱状态是否会影响核心体温。我们会比较两组不同的人,看他们的核心体温是否有差异。 基于恋爱状态,我们可以将数据分为两组:一组是处于恋爱或亲密关系中的人,另一组是没有处于亲密关系的人。我们想要比较这两组之间是否存在差异,通常会采用独立样本t检验。这是我们在学习过程中接触过的统计方法,大家应该都不陌生。 (引自IJzerman et al., 2018) 9.2 t-test作为回归模型的特例 9.2.1 独立样本t检验(independent t-test) 当然,我们需要了解一些基础知识,比如独立样本t检验。第一,我们需要满足正态性假设,即两个样本都来自正态总体。我们也知道,如果样本量足够大,即使不严格服从正态分布,使用t检验也是没有问题的。第二,我们还需要满足方差同质性假设,即两个样本的方差应该是类似的。第三,两个样本应该是独立的。 在进行独立样本t检验时,我们的零假设是这两个独立样本在某个变量上的均值没有差异,即μ1等于μ2。而我们的备择假设则是它们的总体均值是有差异的,即μ1不等于μ2。接下来,我们需要计算t值,这是进行t检验时通常需要用到的一个公式。 \\[t = \\frac{\\bar{X}_1 - \\bar{X}_2}{\\sqrt{\\frac{s_1^2}{n_1} + \\frac{s_2^2}{n_2}}}\\] 在R语言中,如果我们要进行t检验,首先我们需要进行数据预处理。这包括清洗数据,处理缺失值,选择我们感兴趣的变量,以及生成新的变量等。 首先,我们可能需要为数据集生成一些新的变量,比如在原始数据中可能没有被试编号,我们可以随机生成。然后,我们选择自己在分析中关心的问题,使用dplyr函数选择我们感兴趣的变量。 在处理数据时,我们需要注意缺失值。在初步分析时,我们一般选择忽略缺失值,直接将其剔除,以便快速查看结果。此外,还需要将一些变量转换为因子,比如将是否处于亲密关系的变量转换为因子,并赋予两个水平:恋爱和单身。 接下来,我们关注的因变量是被试的体温,我们想知道在两组之间是否有差异。在原始数据中,有两次测量体温的数据,我们可以选择将两次测量的平均值作为新的变量,这可以通过R语言中的行函数实现。具体来说,我们可以创建一个新的变量,这个变量是以temperature字符串开头的列的平均值。 这些步骤都是数据预处理的一部分,是进行t检验之前必要的步骤。 df.penguin <- bruceR::import(here::here('data', 'penguin', 'penguin_rawdata.csv')) %>% dplyr::mutate(subjID = row_number()) %>% dplyr::select(subjID,Temperature_t1, Temperature_t2, socialdiversity, Site, DEQ, romantic, ALEX1:ALEX16) %>% # 选择变量 dplyr::filter(!is.na(Temperature_t1) & !is.na(Temperature_t2) & !is.na(DEQ)) %>% # 处理缺失值 dplyr::mutate(romantic = factor(romantic, levels = c(1,2), labels = c("恋爱", "单身")), # 转化为因子 Temperature = rowMeans(select(., starts_with("Temperature"))), # 计算两次核心温度的均值 ALEX4 = case_when(TRUE ~ 6 - ALEX4), ALEX12 = case_when(TRUE ~ 6 - ALEX12), ALEX14 = case_when(TRUE ~ 6 - ALEX14), ALEX16 = case_when(TRUE ~ 6 - ALEX16), ALEX = rowSums(select(., starts_with("ALEX")))) # 反向计分后计算总分 要在R语言中进行t检验,我们可以使用自带的stats包中的t.test函数。这是一个非常常用的t检验函数。在使用t.test函数时,我们需要输入一些参数。第一个参数是经过筛选的数据框,第二个参数是我们感兴趣的自变量(如temp),第三个参数是分组变量(如romantic)。此外,我们还可以假定两组的方差是相等的。运行t.test函数后,我们可以得到结果,包括t值、自由度(df)和p值。在这个例子中,t值为0.34664,自由度为1425,而p值较大,表示在恋爱组和非恋爱组之间的体温差异不显著。从结果来看,恋爱状态对体温的影响似乎并不大。 stats::t.test(data = df.penguin, # 数据框 Temperature ~ romantic, # 因变量~自变量 var.equal = TRUE) %>% capture.output() # 将输出变整齐 ## [1] "" ## [2] "\\tTwo Sample t-test" ## [3] "" ## [4] "data: Temperature by romantic" ## [5] "t = -0.34664, df = 1425, p-value = 0.7289" ## [6] "alternative hypothesis: true difference in means between group 恋爱 and group 单身 is not equal to 0" ## [7] "95 percent confidence interval:" ## [8] " -0.0555949 0.0388971" ## [9] "sample estimates:" ## [10] "mean in group 恋爱 mean in group 单身 " ## [11] " 36.38498 36.39333 " ## [12] "" 当然,除了t.test函数,还有其他方法可以进行类似的分析。不过,在这个简单的示例中,t.test已经足够满足我们的需求。 我们说t检验是一个特殊的线性回归模型,这是因为在R语言中,它们的编写方式非常相似。在回归模型中,我们将自变量放在前面,因变量放在后面。为了理解这个概念,我们需要简要回顾一下线性回归模型。 线性回归模型是一种统计方法,用于研究一个或多个变量能否预测或解释另一个变量。我们将用来预测的变量称为预测变量或自变量,而被预测的变量称为因变量或响应变量。线性回归的核心是通过拟合一条直线来表示两个变量之间的关系,使得直线与每个数据点之间的距离最小。这样,我们便可以利用这条直线来进行预测。 我们通常会用一个方程或等式来表示线性回归模型,如y = β0 + β1x1 + β2x2 + … + ε。其中y是我们关心的因变量,x1、x2等是自变量,β0、β1、β2等是回归系数,而ε是误差项。 \\[y = \\beta_0 + \\beta_1 x_1 + \\beta_2 x_2 + ... + \\beta_p x_p + \\epsilon\\] 在学习线性回归时,我们通常关注连续变量之间的关系。这意味着x和y都是连续变量。而在t检验中,我们关注的是分组变量,这似乎与线性回归有所不同。然而,在特定情况下,我们可以将t检验视为线性回归的一个特例。当我们分析的自变量是一个二分类变量时,线性回归模型实际上等同于独立样本t检验。所以,虽然它们在某些方面有所不同,但t检验确实可以看作是线性回归的一个特殊情况。 9.2.2 线性回归(linear regression) 在t检验中,我们的因变量,比如体温,是连续的,而自变量,比如是否处于亲密关系,是二分类的。这似乎与我们通常的线性回归模型有所不同,因为在线性回归中,我们通常关注的是连续变量之间的关系。然而,实际上,当我们的自变量是二分类变量时,我们也可以将其视为线性回归模型的一个特例。 想象一下,我们在图上有两堆数据点,一堆是处于亲密关系的人的体温,另一堆是非恋爱状态的人的体温。在这两堆数据点之间,我们可以拟合出一条回归线,这条线可以帮助我们进行预测。当x=1(处于亲密关系)时,我们预测的y值(体温)是多少?当x=2(非恋爱状态)时,我们预测的y值是多少? 在常规的线性回归中,x可以有很多值,y也可以有很多值。然而,在t检验中,x只有两个值,一个是1,一个是2(或者0和1)。我们要做的预测也是,当x=1时,y是多少?当x=2时,y是多少?这就是为什么我们说独立样本t检验是线性回归模型的一个特例,特别是当我们的自变量是二分变量时。 在R语言中,我们可以通过代码来验证这个观点。无论我们是使用t.test函数进行t检验,还是使用lm函数进行线性回归,我们得到的结果应该是一样的。这是因为在这种特殊情况下,这两种方法本质上是在做同样的事情。 实际上,在进行线性回归时,我们可以采用不同的编码方法来表示分类变量。比如,对于二分类变量,我们可以将其编码为0和1。当我们采用这种编码方式时,线性回归方程为y = β0 + β1x1。其中,x1只有两个可能的取值:0和1。当x1 = 0时,y = β0。当x1 = 1时,y = β0 + β1。这样,我们可以看到,β1实际上表示了两组之间的差异。当然,这只是一种方便我们理解的编码方式。在实际应用中,我们可能会遇到更复杂的编码方法,比如虚拟变量编码、效应编码等。这些编码方法在处理多分类变量时尤为有用。 总之,独立样本t检验实际上可以看作是线性回归模型的一个特例,特别是当我们的自变量是二分变量时。无论我们是使用t.test函数进行t检验,还是使用lm函数进行线性回归,我们得到的结果应该是一样的。这是因为在这种特殊情况下,这两种方法本质上是在做同样的事情。 首先,我们进行预处理数据。这个过程相对简单,大家应该都能理解。接下来,我们来看一下t检验。假设我们不对结果进行整理,而是直接查看原始输出,那么我们会看到以下内容:t值是多少,df值是多少,以及p值是多少。我们之前提到过,t检验实际上是一种特殊的回归。 # t检验 stats::t.test( data = df.penguin, Temperature ~ romantic, var.equal = TRUE) %>% capture.output() # 将输出变整齐 我们继续讨论回归模型。在R语言中,线性回归模型的常用函数是lm。我们可以通过比较t检验和线性回归模型得到的t值、df值和p值来判断它们是否相同。 在这个地方,大家可以看到我们使用的是stats包的lm代码,这个代码代表线性回归模型。我们在这里还是采用同样的数据,然后使用回归模型的公式。在这个公式中,temp是我们的因变量y,而romantics是我们的自变量x。这个x是一个因子,我们需要将其转换为因子。在编写这个公式时,我们只需在因变量前加上一个波浪号,然后加上自变量。接下来,lm函数会自动帮助我们处理因子变量在回归分析中的一些后续处理。最后,它会给我们一个结果。 # 线性回归 model.inde <- stats::lm( data = df.penguin, formula = Temperature ~ 1 + romantic ) summary(model.inde) 我们可以看到,这里的t值、p值与我们之前的结果基本相同(t值为0.347,p值为0.729)。这是因为我们主要关注的是单身对结果的影响。因此,我们可以得出这样的结论:两组之间的差异在t检验和回归模型中表现得相当一致。在回归模型中,可能还会有一个intercept,但我们通常不会关注它。斜率slope代表的含义是当x从一个单位变化到另一个单位时,我们的因变量y会发生多少变化。而对于我们这种只有二分变量的自变量的情况,就代表两组之间的差异。 因此,我们可以说,二分变量的t检验与回归模型之间存在密切关系。我们关注的是回归系数,它的统计检验值与t检验中的值完全相同。这就是我们希望展示的结果。所以,在心理学中常用的t检验实际上是线性回归的一种特殊情况。 那当然,这里涉及到一个知识点,我们没有详细展开讲,但大家以后感兴趣的话可以去了解。这个知识点就是编码方法。因为我们的数据有两个水平,可能有多种编码方法。例如,我们可以把一个水平编码为0,另一个水平编码为1。那么我们的公式可以表示为:y = β0 + β1 * x1。在这个情况下,x1只有两种取值:0和1。当x=0时,y等于β0;当x=1时,y等于β0 + β1。所以这个时候我们可以看出β1就是两组之间的区别。 当然,这只是一种方便我们理解的方法。另外一种常用的编码方法是将x1编码为-0.5和0.5。在这种情况下,对β的解读就不一样了。大家可以将这种编码带入公式后,自己去观察一下。我们在这里就不展开详细讲解了,因为我们主要想向大家介绍这个概念。大家可以在R语言中多探索一下这个知识点,以便更好地理解和应用。 9.2.3 单样本t检验(one sample t-test) 那么同理,一系列的t检验其实都可以认为是回归模型的特例。例如,我们来看单样本t检验。虽然我们平时很少使用单样本t检验,但在某些情况下,它还是有用的。比如,我们想要判断某个班级的成绩是否属于全校成绩的总体。在这种情况下,我们实际上是在比较这个班级的成绩均值和全校的总体均值。 在单样本t检验中,我们关心的是一个样本的均值是否等于某个特定值。例如,在“在penguin数据中,全体被试的核心体温(Temperature)是否等于36.6?”这个例子中,我们关心的是全体被试的核心温度是否等于正常人的温度。单样本t检验的计算公式可以不用深究,关键是理解其背后的逻辑。 \\[t = \\frac{\\bar{X} - \\mu}{s / \\sqrt{n}}\\] \\[y = \\beta_0 + \\beta_1 x_1 + \\beta_2 x_2 + ... + \\beta_p x_p + \\epsilon\\] 单样本t检验中,仅截距不为0。此时公式为: \\[y = \\beta_0\\] \\[H_0: \\beta_0 = 0\\] 如图所示,这里有一群点,我们想要比较的是这些点的均值(例如用菱形表示)与某条线(特定值)之间是否存在显著性差异,或者说这些点的均值与这条线是否来自同一个总体。在这种情况下,我们可以构建一个只包含截距项β0的回归模型,即y = β0。我们也可以使用回归模型进行统计检验。 当我们进行单样本t检验时,我们需要稍微调整公式。我们将因变量(temperature)减去我们想要比较的特定值(例如30),然后考虑减去特定值后的因变量均值是否属于一个以0为均值的正态分布。在这个回归模型中,我们加入一个截距项,用于进行t检验。 stats::t.test( x = df.penguin$Temperature, # 核心体温均值 mu = 36.6) 当然,在实际应用中,我们可以直接使用单样本t检验,将观测值x与某个特定值进行比较。这样,我们就可以判断样本数据的均值是否与特定值之间存在显著性差异,从而得出结论。总之,无论是单样本t检验还是回归模型,它们都可以帮助我们解决实际问题。在具体分析过程中,我们可以根据需要选择合适的方法。 model.single <- lm( data = df.penguin, formula = Temperature - 36.6 ~ 1 ) summary(model.single) 9.2.4 配对样本t检验(paired t-test) 那么,对于配对样本t检验,我们其实可以把它看作是单样本t检验。因为配对样本t检验中的数据是一一对应的,我们可以通过一个简单的操作,即将两列配对数据相减,得到一列新的数据。然后,我们可以将这列新数据进行单样本t检验。 \\[t = \\frac{\\bar{X} - \\mu}{s / \\sqrt{n}}\\] 这里的前提条件我们就不详细讲了。简单来说,对于配对样本t检验,我们可以先将两个值相减,得到他们的差值。然后,我们检验这个差值是否显著地不同于零。如果差值的分布明显不同于零,那么我们就可以认为两者之间存在显著的差异。 \\[y_1 - y_2 = \\beta_0 \\] \\[H_0: \\beta_0 = 0 \\] 我们之所以能这样做,是因为在配对样本t检验中,每个被试都有两个数据点,这两个数据点是一一对应的。但是,对于独立样本t检验,由于数据点之间没有形成配对关系,我们不能直接相减。 同样,对于配对样本t检验,我们也可以使用回归方法进行检验。方法类似于独立样本t检验,我们只需将两个值相减,然后检验这个差值的均值是否等于零。在R语言中,我们可以使用t.test函数来进行配对样本t检验。我们只需将两列数据分别作为x和y输入,然后设置参数paired为TRUE,表示这两列数据是配对的,就可以进行配对样本t检验了。 stats::t.test( x = df.penguin$Temperature_t1, y = df.penguin$Temperature_t2, paired = TRUE ) 在前面我们讲了关于亲密关系和非亲密关系的两组人在体温上是否有区别的问题。我们可以使用配对样本t检验来进行检验,而配对样本t检验实际上是一个特殊的回归模型。同样地,对于所有其他类型的t检验,它们也都可以看作是特定的回归模型。因此,t检验与线性模型的本质是相同的。 model.paired <- lm( Temperature_t1 - Temperature_t2 ~ 1, data = df.penguin ) summary(model.paired) 9.2.5 bruceR::TTEST 这里,我们可以介绍一下bruceR包中的t检验功能,它涵盖了我们前面提到的各种t检验,并且输出结果非常方便。如果大家下载了我们的课件,可以看到关于bruceR包中t检验各种参数的介绍。bruceR包基本上涵盖了所有类型的t检验,包括独立样本t检验、配对样本t检验和单样本t检验。它的输出结果通常是一个三线表,方便大家查看和理解。 stats::t.test( data = df.penguin, Temperature ~ romantic, var.equal = TRUE ) bruceR::TTEST( data = df.penguin, # 数据 y = "Temperature", # 因变量 x = "romantic" # 自变量 ) [单样本t检验] stats::t.test( x = df.penguin$Temperature, mu = 36.6 ) bruceR::TTEST( data = df.penguin, # 数据 y = "Temperature", # 确定变量 test.value = 36.6, # 固定值 test.sided = "=") # 假设的方向 [配对样本t检验] stats::t.test( x = df.penguin$Temperature_t1, #第1次 y = df.penguin$Temperature_t2, #第2次 paired = TRUE) bruceR::TTEST( data = df.penguin, # 数据 y = c("Temperature_t1", "Temperature_t2"), # 变量为两次核心体温 paired = T) # 配对数据,默认是FALSE 无论是使用bruceR包中的t检验功能,还是R语言自带的t检验功能,我们都可以发现它们得到的结果与线性回归模型的结果是一模一样的。这再次证实了t检验与线性回归模型之间的密切关系。在实际研究中,我们可以根据问题的具体情况选择合适的方法,无论是t检验还是线性回归模型。同时,理解它们之间的关系有助于我们更好地掌握统计分析的原理和技巧。 9.2.6 总结 我们来做一个简单的总结。对于t检验,我们可以使用R自带的t.test函数。虽然语法上略有不同,但实际上,线性模型和t检验在本质上是一致的。单样本t检验可以看作是一个仅有截距的线性模型;独立样本t检验是一个仅有截距和一个斜率的线性模型;而配对样本t检验则是一个基于差值的仅有截距的线性模型。 独立样本t检验也可以看作是一个自变量为二分变量的线性回归模型。要理解为什么仅有截距的回归模型与t检验相关,我们可能需要回顾正态分布、抽样分布等概念,以及为什么我们使用t值这个分布来进行检验。然而,我们在这里没有足够的时间来展开这个话题。如果大家对此感兴趣,可以自行深入学习,也很推荐大家关注包寒吴霜老师的专栏。 9.3 ANOVA & linear regression 接下来我们讨论方差分析,这也是心理学中常用的一种统计方法。在学习统计时,我们会花很多时间学习如何进行方差分解。 9.3.1 研究问题 以Penguin数据为例,我们刚才发现恋爱状态对体温没有太大影响。但我们可以进一步探讨它是否与距离赤道的距离有交互作用。因为我们知道,离赤道越远,气温就越寒冷。有时候,我们可能会发现某个自变量没有影响,但实际上它可能受到另一个自变量的调节。例如,在这个例子中,我们可以检验恋爱状态是否在较寒冷的地方影响体温,而在较暖和的地方则不需要调节。要检验这种交互作用,我们通常会想到使用双因素方差分析。 9.3.2 代码实操 & 知识回顾 双因素方差分析通常是我们在讨论方差分析时所指的完全随机方差分析。但在这里,我们需要注意我们使用的是双因素的重复测量方差分析,而不是完全随机的。也就是说,不同距离赤道的人以及处于不同恋爱状态的人并非随机分配。我们无法将某人随机分配到离赤道较近的恋爱状态这一组,因为这是一个横断数据。然而,我们仍然可以使用方差分析来进行数据分析。 在学习方差分析时,我们可能会了解到它的历史以及它所检验的假设。对我们来说,检验的假设是关键。也就是说,在各因素的各个水平下,因变量的均值是否完全相同。原假设认为各因素各水平下的因变量均值完全相同,而备择假设则认为各因素各水平下的因变量均值并不完全相同。通常情况下,只要有一个水平的因变量均值不同,方差分析就能探测出来。 当然,方差分析的使用也有一些前提假设,包括随机性、正态性、独立性和方差齐性等。这些内容我们在这里就不再详细展开了。 9.3.3 ANOVA代码实操|数据预处理 在R中实现方差分析的关键是选择合适的函数。当我们想进行多因素方差分析时,R中有一些常用的函数可以帮助我们实现。在进行方差分析之前,我们需要对数据进行一些预处理。 以距离赤道的距离为例,我们发现它是一个连续的数值变量。但为了进行方差分析,我们可以将其分为三个水平:热带、温带和寒温带。我们可以根据纬度的区别设定分隔点,例如,赤道到23.5度为热带,35度到40度为暖温带,40度到50度为中温带(统称为温带),50度以上为寒温带。我们可以使用R中的cut函数将连续性的数据划分为不同的段,并赋予不同的值。同时,我们可以为这个新变量命名为climate。 [vars] summary(df.penguin$DEQ) ## Min. 1st Qu. Median Mean 3rd Qu. Max. ## 1.293 34.433 39.912 39.842 51.317 60.391 # 设定分割点 # [0-23.5 热带, 23.5-35 亚热带], [35-40 暖温带, 40-50 中温带], [50-66.5 寒温带] breaks <- c(0, 35, 50, 66.5) # 设定相应的标签 labels <- c('热带', '温带', '寒温带') # 创建新的变量 df.penguin$climate <- cut(df.penguin$DEQ, breaks = breaks, labels = labels) summary(df.penguin$climate) ## 热带 温带 寒温带 ## 396 592 439 [tidy data] df <- df.penguin %>% select(subjID, climate, romantic, Temperature) 9.3.4 代码实操|正态性检验 在进行方差分析前,我们还需要检验数据的正态性。R中有一些方法可以帮助我们检验正态性,例如KS检验和QQ图。通过这些方法,我们可以观察数据的分布情况。虽然数据可能不是完全正态分布的,但接近正态分布的数据也是可以接受的。 # 正态性检验-Kolmogorov-Smirnov检验 # 若p >.05,不能拒绝数据符合正态分布的零假设 ks.test(df$Temperature, 'pnorm') ## Warning in ks.test.default(df$Temperature, "pnorm"): Kolmogorov - ## Smirnov检验里不应该有连结 ## ## Asymptotic one-sample Kolmogorov-Smirnov test ## ## data: df$Temperature ## D = 1, p-value < 0.00000000000000022 ## alternative hypothesis: two-sided # 进行数据转换,转换后仍非正态分布 df$Temperature_log <- log(df$Temperature) ks.test(df$Temperature_log, 'pnorm') ## Warning in ks.test.default(df$Temperature_log, "pnorm"): Kolmogorov - ## Smirnov检验里不应该有连结 ## ## Asymptotic one-sample Kolmogorov-Smirnov test ## ## data: df$Temperature_log ## D = 0.99981, p-value < 0.00000000000000022 ## alternative hypothesis: two-sided [qq图] # 正态性检验-qq图 qqnorm(df$Temperature) qqline(df$Temperature, col = "red") # 添加理论正态分布线 直方图 ggplot(df, aes(Temperature)) + geom_histogram(aes(y =..density..), color='black', fill='white', bins=30) + geom_density(alpha=.5, fill='red') ## Warning: The dot-dot notation (`..density..`) was deprecated in ggplot2 3.4.0. ## ℹ Please use `after_stat(density)` instead. ## This warning is displayed once every 8 hours. ## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was ## generated. 9.3.5 代码实操|双因素被试间方差分析 在R中实现方差分析,我们通常会用到aov函数,这是R自带的一个函数。实际上,这个函数来自于stats包,同样的,t.test函数和lm函数也是来源于这个包。在进行方差分析时,我们采用的语法与t检验非常相似,仍然是线性回归的写法。我们将因变量放在前面,自变量放在后面,中间用一个波浪号连接。然后,我们指定使用的数据。 在方差分析的语法中,我们使用乘号 * 来表示主效应及其交互作用。例如,我们可以写成climate * romantic,这表示考虑climate和romantic的主效应以及它们之间的交互作用。在R中,这样的写法非常常见,前面的波浪号表示因变量与自变量之间的关系,而乘号表示考虑主效应及交互作用。 实际上,这种线性方程有多种写法。例如,在t检验中,我们只关注romantic;而在方差分析中,我们加入了climate的主效应以及它们之间的交互作用。在R中最基础的包里,当我们进行方差分析时,实际上沿用了线性模型的语法。通过这种语法,我们可以得到方差分析的一些统计值,如F值、P值以及我们熟悉的平方和(SS,Sum of Squares)。 如果我们对方差分析的结果使用summary函数进行查看,我们会看到熟悉的方差分析表。在这个表中,我们可以看到两个主效应(romantic和climate)以及它们之间的交互作用。同时,表格中还包含了残差(Residuals)信息。这些信息有助于我们了解各个因素对因变量的影响程度和显著性。 aov1 <- stats::aov(Temperature ~ climate * romantic, data = df) summary(aov1) ## Df Sum Sq Mean Sq F value Pr(>F) ## climate 2 18.82 9.408 49.392 < 0.0000000000000002 *** ## romantic 1 0.24 0.244 1.280 0.25807 ## climate:romantic 2 1.91 0.955 5.014 0.00676 ** ## Residuals 1421 270.65 0.190 ## --- ## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1 在进行方差分析时,有时我们会发现,在R中进行的分析结果与在SPSS中进行的结果不完全一样。例如,climate的F值在R中为49.39,而在SPSS中为50.88。这种情况下,我们可能会感到困惑,不知道是否可以信任R的结果,甚至可能会认为R的分析方法不靠谱。 [SPSS] 实际上,这种差异的出现很可能是因为在R和SPSS中使用的方法或某些设置不同。这可能导致在相同的数据和看似相同的方法下,得到不同的分析结果。在这里,我们需要讨论一个问题:为什么方差分析这样一个看似简单的方法(我们在本科阶段就学过并且很熟悉),会在不同软件中产生不同的结果呢? 9.3.6 代码实操|平方和(SS)的计算 在进行方差分析时,可能会涉及到平方和计算方法的问题。在平衡设计下,即每个条件下的样本量相同时,三类平方和的结果是没有差别的。但在不平衡设计下,即每个条件下的样本量不同时,就需要进行一些调整。 这里的不平衡设计是指,比如我们有两个变量,climate有三个水平,romantic有两个水平,那么组合之后就有六个条件,但这六个条件下的被试数量可能是不同的。理论上,最好的情况是每个条件下的被试数量是一样的,这样的设计被称为平衡设计。在平衡设计中,三种类型的平方和的结果会很清晰,并且方差分析的结果独立于平方和的类型。但在实际情况中,这种设计可能较难实现,从而导致不平衡设计。 在不平衡设计下,计算方差时就会出现问题,因为我们需要将不同组的方差进行合并。尤其是当各组样本量差距较大时,三种类型的平方和计算结果可能会不同。在这种情况下,我们需要进行一些调整,需要根据具体研究设计和问题来选择使用哪一种类型的平方和。这就产生了三类平方和。SPSS默认使用的是第三类平方和,而在R中,aov函数默认使用的是第一类平方和。因此,当我们使用R进行方差分析时,可能会发现结果与SPSS有所不同。 这种情况下,我们需要更深入地理解平方和的计算方法,以便正确解读R的输出结果。这也是我们在使用R进行统计分析时,需要不断强化统计知识的一个重要原因。因为相比于SPSS这类点击式操作的软件,R提供了更多的选项,而正确理解和使用这些选项,就需要我们对统计学有更深入的理解。 我们是否能得到完全相同的结果呢?实际上,这是完全可能的。例如,我们可以使用afex这个包,这是在2017或2018年后出现的一个包。afex是为了满足实验心理学家进行方差分析的需求而设计的,其中包含了各种方差分析的功能。如果我们按照这样的写法,即将被试的identity写在这里,然后将类型设置为三,我们就能得到完全相同的结果。我们也可以在其他地方使用同样的方法,同样能得到一模一样的结果。 [car::Anova()] # 结果不一致,原因PPT显示不全,请回到rmd文档查看 aov1 <- car::Anova(stats::aov(Temperature ~ climate * romantic, data = df)) aov1 ## Anova Table (Type II tests) ## ## Response: Temperature ## Sum Sq Df F value Pr(>F) ## climate 19.034 2 49.9680 < 0.00000000000000022 *** ## romantic 0.244 1 1.2801 0.258072 ## climate:romantic 1.910 2 5.0136 0.006765 ** ## Residuals 270.654 1421 ## --- ## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1 我们是否能得到完全相同的结果呢?实际上,这是完全可能的。例如,我们可以使用afex这个包,这是在2017或2018年后出现的一个包。afex是为了满足实验心理学家进行方差分析的需求而设计的,其中包含了各种方差分析的功能。如果我们按照这样的写法,即将被试的identity写在这里,然后将类型设置为三,我们就能得到完全相同的结果。我们也可以在其他地方使用同样的方法,同样能得到一模一样的结果。 # 原因debug # 查看R的默认对比设置 options("contrasts") ## $contrasts ## unordered ordered ## "contr.treatment" "contr.poly" # 从输出结果可知,无序默认为contr.treatment(),有序默认为contr.poly() # factor()函数来创建无序因子,ordered()函数创建有序因子 is.factor(df$climate) ## [1] TRUE is.ordered(df$climate) ## [1] FALSE # climate是无序因子 # 创建一个3水平的因子的基准对比 c1 <- contr.treatment(3) # 创建一个新的对比,这个编码假设分类水平之间的差异被等分,每一个水平与总均值的差异等于1/3 my.coding <- matrix(rep(1/3, 6), ncol=2) # 将对比调整为每个水平与第一个水平的振幅减去1/3 # 可能的原因:除了关心每个水平对应的效果,同时也关心水平与水平之间的效果 my.simple <- c1-my.coding my.simple ## 2 3 ## 1 -0.3333333 -0.3333333 ## 2 0.6666667 -0.3333333 ## 3 -0.3333333 0.6666667 # 更改climate的对比 contrasts(df$climate) <- my.simple # 将数据集df的romantic列的对比设为等距对比,它假设分类水平之间的差异为等距离 contrasts(df$romantic) <- contr.sum(2)/2 # 方差分析 aov1 <- car::Anova(lm(Temperature ~ climate * romantic, data = df), type = 3) aov1 ## Anova Table (Type III tests) ## ## Response: Temperature ## Sum Sq Df F value Pr(>F) ## (Intercept) 1768309 1 9284069.6498 < 0.00000000000000022 *** ## climate 19 2 50.8832 < 0.00000000000000022 *** ## romantic 0 1 1.0006 0.317336 ## climate:romantic 2 2 5.0136 0.006765 ** ## Residuals 271 1421 ## --- ## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1 [afex::aov_ez()] afex::aov_ez(id = "subjID", dv = "Temperature", data = df, between = c("climate", "romantic"), type = 3) ## Contrasts set to contr.sum for the following variables: climate, romantic ## Anova Table (Type 3 tests) ## ## Response: Temperature ## Effect df MSE F ges p.value ## 1 climate 2, 1421 0.19 50.88 *** .067 <.001 ## 2 romantic 1, 1421 0.19 1.00 <.001 .317 ## 3 climate:romantic 2, 1421 0.19 5.01 ** .007 .007 ## --- ## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '+' 0.1 ' ' 1 # afex中的其他函数可以得到同样的结果 afex::aov_car(Temperature ~ climate * romantic + Error(subjID), data = df, type = 3) ## Contrasts set to contr.sum for the following variables: climate, romantic afex::aov_4(Temperature ~ climate * romantic + (1|subjID), data = df) ## Contrasts set to contr.sum for the following variables: climate, romantic 也就是说,如果我们使用最常见的R代码,很可能会得到与SPSS不同的结果。同样的现象也可能发生在我们习惯使用AMOS进行结构方程模型(SEM)的情况下。当你使用R的某些包进行结构方程模型时,即使是同样的数据和结构,可能会得到不同的结果。这可能是因为它们内部设定的不同。因此,我们需要去了解它们内部的设定是什么。 9.4 线性回归 我们刚刚讨论的是t检验,它是一种特定的回归模型。而对于ANOVA(方差分析),从它的表示方法来看,我们可以很明显地看出它实际上也是一个回归模型。因为它的方程表示形式与线性模型非常相似,包括一些自变量以及它们之间是否存在交互作用。在实际应用中,我们经常会遇到单因素方差分析和多因素方差分析。 在这种情况下,两因素方差分析也是一个特殊的回归模型。它的特殊之处在于我们的自变量是分类变量,也可以称为离散变量。因此,在进行方差分析时,我们需要注意处理这些分类变量或离散变量。 假设我们这里有一个2×2的设计,也就是有四个条件,这四个条件可以视为四组。每一组被试在某一因变量上都会有自己的取值。我们需要检验的是这四组被试的均值是否完全相同,即它们是否有偏离总体均值的情况。这就是我们进行方差分析的一个核心目标。在线性模型上,我们也在检验同样的问题。 比如说,我们可以通过某种方式对自变量进行编码,然后观察各组的均值与其他组之间的差异是否达到显著水平。举个例子,这里我们展示的是一种非常简单的编码方式,我们以某一条件下的值作为零点,比如第一组,然后其他所有的系数都是其他组的均值与这个零点的差异,也就是我们斜率的比较对象。 可能这么说有点抽象,让我们以一个具体的例子来说明。假设我们有一个变量叫”romantic”,我们可以把它编码为0和1。同样地,我们也可以把其他变量编码为0和1。如果我们简化这两种情况,就更容易理解了。 在这里,我们可以看到如果有一个组合为0和0,那么在我们的回归模型中,这个部分就变成了β0。在这种编码方式下,β0代表的就是某个条件下的均值。通过这种方式,我们可以看到β1可能代表的是某种条件下的值,比如当某个变量取值为1或0时的情况。同样,β2可能代表的是另一个变量在取值为1、0或者两个变量都为1时的情况。 所有这些回归系数都可以被理解为组间的差异值。我们的回归模型的目标就是去检验这些差异是否显著。在做回归模型时,我们检验显著性的方式可能有所不同。在这种最简化的情况下,我们可以看到,我们的2×2的两因素方差分析完全可以对应成为一个离散变量的回归模型。 如果我们采用ANOVA或者线性模型,并且我们的设定是一样的,那么得到的结果将会是完全一样的。可能我讲得有点快,大家可能需要回顾一下自己学的线性回归的知识。因为我不确定其他的统计老师在讲线性回归的时候是否已经将其与ANOVA或t检验进行了关联。但大家可以这样理解,它们本质上都是一样的。如果大家理解了,ANOVA其实就是线性回归的一个特定模式,那么你们就已经把握了其要点。 aov1 <- car::Anova( aov(Temperature ~ climate * romantic, data = df), type = 3 ) aov1 lm1 <- car::Anova( lm(Temperature ~ climate * romantic, data = df), type = 3 ) lm1 在实际研究中,即使我们知道ANOVA与线性回归有相似之处,但我们仍然需要报告传统的方差分析结果。在这种情况下,我们推荐使用bruceR包的mANOVA函数,它是一个非常强大的函数,基本上涵盖了我们在心理学中常用的所有ANOVA类型。使用mANOVA函数可以大大简化心理学研究者使用这个方法的门槛。 关于mANOVA函数的主要参数,我们不再详细讲解,大家可以查阅相关资料。该函数非常全面,而且有一个非常好的特点,就是它可以直接将结果保存为三线表格,方便我们将结果粘贴到Word文档中进行报告。 以我们刚才讲解的例子为例,我们要对climate和romantic进行方差分析,我们可以调用mANOVA函数,设置dependent variable为temperature,参数设为between表示主间变量。这里有两个主间变量。大家可以看到,得到的结果与SPSS是完全相同的,如50.88。 与SPSS不同之处在于mANOVA函数会输出Generalized Eta Squared(广义η²),这是方差分析的一个效应量指标,有助于我们更好地理解结果。关于效应量的指标,对于传统心理学来说,我们通常只关注p值。但实际上,最近的趋势是大家会更加关注效应量有多大,而不仅仅是p值是否显著。换句话说,我们不仅关心结果是否显著,还关心效应量是否足够大。 在方差分析中,我们经常会进行进一步的简单效应分析和事后多重比较。这里的操作比较灵活。例如,如果你使用的是纯粹的重复测量设计,那么在没有交互作用的情况下,我们可能会进行多重比较,也就是比较某一条件下不同水平之间是否存在差异。然而,如果存在交互作用,我们可能需要重新检查在某一自变量的水平下,另一个自变量是否具有效应。这种情况下,我们称之为简单效应。简单效应分析可以帮助我们深入了解在某一特定条件下,不同水平之间的差异,以便更好地解释和理解我们的研究结果。 res1 <- bruceR::MANOVA(data = df, dv = "Temperature", between = c("climate", "romantic")) ## [1] "Anova Table (Type III tests)" ## [2] "" ## [3] "Response: Temperature" ## [4] " Effect df MSE F ges p.value" ## [5] "1 climate 2, 1421 0.19 50.88 *** .067 <.001" ## [6] "2 romantic 1, 1421 0.19 1.00 <.001 .317" ## [7] "3 climate:romantic 2, 1421 0.19 5.01 ** .007 .007" ## [8] "---" ## [9] "Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '+' 0.1 ' ' 1" 在这个例子中,我们可以看到,我们对climate和romantic进行了方差分析,试图查看在不同的romantic条件下(即单身或非单身),climate是否有影响。这就是简单效应分析。 在R中,我们可以看到mANOVA函数的输出结果。首先,它会输出描述性统计结果,包括climate和romantic的各个水平的均值、标准差和样本量。然后,它会给出方差分析表,包括climate的主效应、romantic的主效应和它们的交互作用的效应。我们可以看到,climate的主效应是显著的,这与我们的预期一致。即在不同气候带的人们体温存在差异,其p值非常小,F值也比较大。此外,我们也发现climate和romantic的交互作用是显著的,这也与我们的预期相符。 原先,我们试图通过t检验来查看不同romantic状态下的人们体温是否有区别,但我们并没有发现显著差异。然后,我们怀疑这可能是因为人的体温受到climate的影响,这个效应可能掩盖了romantic的影响。因此,我们引入了climate这个因素,然后再次检验romantic和climate的交互作用。结果显示,这个交互作用是显著的,这也就验证了我们的猜想。 在mANOVA中,它会输出各种效应量指标,如η²和广义η²(Generalized Eta Squared)。通常情况下,在做方差分析时,我们会报告广义η²。实际上,关于方差分析的效应量指标是一个重要且不容易掌握的知识点。在mANOVA中,它还会输出其他效应量指标,如Cohen’s f和方差同质性检验结果。这些输出结果非常符合心理学研究者的使用习惯。 当我们发现交互作用时,我们通常需要进一步了解这个交互作用代表什么。在这种情况下,我们可以使用emmeans(estimated marginal means)函数来查看不同条件下的结果。这个函数可以帮助我们查看在不同条件下,climate和romantic之间的简单效应。通过emmeans函数,我们可以得到一个三线表,展示了在不同romantic条件下,climate的简单效应。在这个表格中,我们可以看到,在不同的romantic条件下,climate都具有显著效应。这些信息有助于我们更深入地了解交互作用背后的实际情况。 在这个分析过程中,我们首先在恋爱和单身的条件下分别看了climate的影响,并发现climate在两种情况下都有显著影响,虽然效应量可能会有所不同。然后,我们进一步进行了post-hoc比较,比较了恋爱和单身状态下,不同climate的体温差异,结果显示基本上所有的差异都是显著的。 然后,我们通过改变分组条件,看了在不同climate下,恋爱关系是否有影响。结果显示,在热带和温带,恋爱关系没有显著影响,但在寒带,恋爱关系则有显著影响。这个结果与我们的预期相符,表明在较冷的气候下,恋爱关系对体温有调节作用。 此外,我们还进行了F检验,实际上是在热带、温带和寒带三种条件下进行了单因素方差分析。由于只有两个水平(恋爱和单身),所以这个F检验的结果与后面的t检验结果基本一致。这是因为在只有两个水平的情况下,F检验和t检验的结果是一样的。如果你对这个有兴趣,你可以进一步查看t值和F值之间的关系。 sim_eff <- res1 %>% bruceR::EMMEANS("climate", by = "romantic") %>% bruceR::EMMEANS("romantic", by = "climate") —— EMMEANS (effect = “climate”) —— Joint Tests of “climate”: ───────────────────────────────────────────────────────────────── Effect “romantic” df1 df2 F p η²p [90% CI of η²p] ───────────────────────────────────────────────────────────────── climate 恋爱 2 1421 13.165 <.001 *** .018 [.008, .031] climate 单身 2 1421 41.817 <.001 *** .056 [.037, .075] ───────────────────────────────────────────────────────────────── Note. Simple effects of repeated measures with 3 or more levels are different from the results obtained with SPSS MANOVA syntax. ## Warning in rbind(deparse.level, ...): number of columns of result, 8, is not a ## multiple of vector length 6 of arg 2 Univariate Tests of “climate”: ─────────────────────────────────────────────────────────────── Sum of Squares df Mean Square F p ─────────────────────────────────────────────────────────────── 恋爱: “climate” 5.015 2 2.507 13.165 <.001 单身: “climate” 15.929 2 7.965 41.817 <.001 Residuals ─────────────────────────────────────────────────────────────── Note. Identical to the results obtained with SPSS GLM EMMEANS syntax. Estimated Marginal Means of “climate”: ───────────────────────────────────────────────────── “climate” “romantic” Mean [95% CI of Mean] S.E. ───────────────────────────────────────────────────── 热带 恋爱 36.510 [36.446, 36.575] (0.033) 温带 恋爱 36.393 [36.346, 36.440] (0.024) 寒温带 恋爱 36.296 [36.245, 36.347] (0.026) 热带 单身 36.590 [36.532, 36.648] (0.029) 温带 单身 36.356 [36.303, 36.409] (0.027) 寒温带 单身 36.182 [36.114, 36.250] (0.035) ───────────────────────────────────────────────────── Pairwise Comparisons of “climate”: ──────────────────────────────────────────────────────────────────────────────────────── Contrast “romantic” Estimate S.E. df t p Cohen’s d [95% CI of d] ──────────────────────────────────────────────────────────────────────────────────────── 温带 - 热带 恋爱 -0.117 (0.041) 1421 -2.879 .012 * -0.268 [-0.491, -0.045] 寒温带 - 热带 恋爱 -0.214 (0.042) 1421 -5.111 <.001 *** -0.490 [-0.720, -0.260] 寒温带 - 温带 恋爱 -0.097 (0.035) 1421 -2.741 .019 * -0.222 [-0.417, -0.028] 温带 - 热带 单身 -0.234 (0.040) 1421 -5.853 <.001 *** -0.536 [-0.756, -0.317] 寒温带 - 热带 单身 -0.409 (0.046) 1421 -8.969 <.001 *** -0.936 [-1.186, -0.686] 寒温带 - 温带 单身 -0.174 (0.044) 1421 -3.966 <.001 *** -0.400 [-0.641, -0.158] ──────────────────────────────────────────────────────────────────────────────────────── Pooled SD for computing Cohen’s d: 0.436 P-value adjustment: Bonferroni method for 3 tests. Disclaimer: By default, pooled SD is Root Mean Square Error (RMSE). There is much disagreement on how to compute Cohen’s d. You are completely responsible for setting sd.pooled. You might also use effectsize::t_to_d() to compute d. —— EMMEANS (effect = “romantic”) —— Joint Tests of “romantic”: ──────────────────────────────────────────────────────────────── Effect “climate” df1 df2 F p η²p [90% CI of η²p] ──────────────────────────────────────────────────────────────── romantic 热带 1 1421 3.287 .070 . .002 [.000, .008] romantic 温带 1 1421 1.055 .305 .001 [.000, .005] romantic 寒温带 1 1421 6.966 .008 ** .005 [.001, .013] ──────────────────────────────────────────────────────────────── Note. Simple effects of repeated measures with 3 or more levels are different from the results obtained with SPSS MANOVA syntax. ## Warning in rbind(deparse.level, ...): number of columns of result, 6, is not a ## multiple of vector length 5 of arg 2 Univariate Tests of “romantic”: ────────────────────────────────────────────────────────────────── Sum of Squares df Mean Square F p ────────────────────────────────────────────────────────────────── 热带: “romantic” 0.626 1 0.626 3.287 .070 . 温带: “romantic” 0.201 1 0.201 1.055 .305 寒温带: “romantic” 1.327 1 1.327 6.966 .008 ** Residuals 271 ────────────────────────────────────────────────────────────────── Note. Identical to the results obtained with SPSS GLM EMMEANS syntax. Estimated Marginal Means of “romantic”: ───────────────────────────────────────────────────── “romantic” “climate” Mean [95% CI of Mean] S.E. ───────────────────────────────────────────────────── 恋爱 热带 36.510 [36.446, 36.575] (0.033) 单身 热带 36.590 [36.532, 36.648] (0.029) 恋爱 温带 36.393 [36.346, 36.440] (0.024) 单身 温带 36.356 [36.303, 36.409] (0.027) 恋爱 寒温带 36.296 [36.245, 36.347] (0.026) 单身 寒温带 36.182 [36.114, 36.250] (0.035) ───────────────────────────────────────────────────── Pairwise Comparisons of “romantic”: ───────────────────────────────────────────────────────────────────────────────────── Contrast “climate” Estimate S.E. df t p Cohen’s d [95% CI of d] ───────────────────────────────────────────────────────────────────────────────────── 单身 - 恋爱 热带 0.080 (0.044) 1421 1.813 .070 . 0.183 [-0.015, 0.382] 单身 - 恋爱 温带 -0.037 (0.036) 1421 -1.027 .305 -0.085 [-0.247, 0.077] 单身 - 恋爱 寒温带 -0.115 (0.043) 1421 -2.639 .008 ** -0.262 [-0.458, -0.067] ───────────────────────────────────────────────────────────────────────────────────── Pooled SD for computing Cohen’s d: 0.436 No need to adjust p values. Disclaimer: By default, pooled SD is Root Mean Square Error (RMSE). There is much disagreement on how to compute Cohen’s d. You are completely responsible for setting sd.pooled. You might also use effectsize::t_to_d() to compute d. 9.5 知识延申|单因素方差分析示例 在本节课中,我们讲解了单因素方差分析和多因素方差分析,并重点讲解了多因素方差分析。我们提到了R中自带的函数以及其他一些适合心理学研究者使用的包,如PR和fx。这些包的输出结果与SPSS的输出结果非常相似,因此可以让老师和同学们更加放心地使用R进行分析。 # DEQ对Temperature的影响 res2 <- bruceR::MANOVA( data = df, dv = "Temperature", between = "climate") ## ## ====== ANOVA (Between-Subjects Design) ====== ## ## Descriptives: ## ───────────────────────────── ## "climate" Mean S.D. n ## ───────────────────────────── ## 热带 36.555 (0.411) 396 ## 温带 36.377 (0.401) 592 ## 寒温带 36.255 (0.504) 439 ## ───────────────────────────── ## Total sample size: N = 1427 ## ## ANOVA Table: ## Dependent variable(s): Temperature ## Between-subjects factor(s): climate ## Within-subjects factor(s): – ## Covariate(s): – ## ─────────────────────────────────────────────────────────────────────── ## MS MSE df1 df2 F p η²p [90% CI of η²p] η²G ## ─────────────────────────────────────────────────────────────────────── ## climate 9.408 0.192 2 1424 49.106 <.001 *** .065 [.045, .085] .065 ## ─────────────────────────────────────────────────────────────────────── ## MSE = mean square error (the residual variance of the linear model) ## η²p = partial eta-squared = SS / (SS + SSE) = F * df1 / (F * df1 + df2) ## ω²p = partial omega-squared = (F - 1) * df1 / (F * df1 + df2 + 1) ## η²G = generalized eta-squared (see Olejnik & Algina, 2003) ## Cohen’s f² = η²p / (1 - η²p) ## ## Levene’s Test for Homogeneity of Variance: ## ────────────────────────────────────────────── ## Levene’s F df1 df2 p ## ────────────────────────────────────────────── ## DV: Temperature 18.207 2 1424 <.001 *** ## ────────────────────────────────────────────── 9.6 知识延申|总结 此外,我们还讨论了线性模型与t检验之间的关系。可能有些同学在理解这部分内容时会感到困惑,这时候建议大家回顾一下本科阶段的教材,加深对回归模型和t检验之间关系的理解。学习代码和应用是很重要的,但要正确地使用它们,还需要掌握背后的统计知识。 总之,通过学习本节课的内容,我们可以更好地理解方差分析的原理和应用,并能够在R中使用相应的函数进行实际分析。同时,我们也要意识到,在学习和使用R进行统计分析时,理解和掌握背后的统计知识是非常重要的。 课堂总结 在今天的课程中,我们讨论了方差分析的原理和应用,并在R中使用了相关函数进行实际分析。我们还提到了一个国外博客,这个博客指出许多常见的统计检验都是线性模型的特例。这可以帮助大家更好地理解这些统计方法之间的联系。 最后,我们留了一个思考题:对于重复测量设计(within-subject design),如何用回归模型进行分析?此外,我们还给出了一些课堂练习,建议大家尝试自己编写代码来完成这些练习,以加深对课程内容的理解。 本节课在此结束。感谢大家的参与,如果有任何问题,请随时提问。 "]]
diff --git "a/bookdown_files/Books/Book/_book/\347\254\254\345\205\253\350\256\262\345\233\236\345\275\222\346\250\241\345\236\213\344\270\200.html" "b/bookdown_files/Books/Book/_book/\347\254\254\345\205\253\350\256\262\345\233\236\345\275\222\346\250\241\345\236\213\344\270\200.html"
new file mode 100644
index 00000000..0ddc9b50
--- /dev/null
+++ "b/bookdown_files/Books/Book/_book/\347\254\254\345\205\253\350\256\262\345\233\236\345\275\222\346\250\241\345\236\213\344\270\200.html"
@@ -0,0 +1,1210 @@
+
+
+
+
+
+
+ Chapter 9 第八讲:回归模型(一) | R语言在心理学研究中的应用: 从原始数据到可重复的论文手稿(V2)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
第八讲:回归模型(一)
+
我们之前讨论的内容主要分为两方面。首先是学习R语言的一些基本知识,另一方面则是使用R代码来帮助我们解决描述性统计问题。如果大家还记得心理统计学中的内容,它分为描述统计和推断统计两部分。我们使用数据来进行统计推断,而R语言则可以帮助我们更灵活地实现各种统计方法。
+
纯粹的R代码学习 → 使用R语言来实现统计知识。当然,这种灵活性既有好处也有坏处。好处是我们有很多选择,坏处则是面对众多选择,我们可能会不知道如何选择。但是,如果我们能够度过刚开始不知道如何选择的阶段,后面就能更好地运用统计知识。另外,在接下来的课程中,我们将重点讨论回归模型,例如回归模型一、回归模型二和回归模型三。
+
我们之所以关注回归模型,是因为我们希望借此机会将大家在心理学、社会科学研究中常用的统计检验统一到回归框架下。这也是近年来一些研究者推荐的做法。实际上,心理学使用的统计方法与其他学科并没有本质区别,只是大家的偏好问题。在心理学领域中,最常用的方法便是各种回归模型的特例。
+
+
研究问题
+
我们从研究问题开始,先来回顾一下人类企鹅计划的关键变量。其中一个是恋爱状态,另一个是核心温度,还有一个是社交复杂程度,以及赤道距离。在刚开始进行数据分析时,我们可能没有特别明确的假设,但我们想要了解社会关系,尤其是亲密关系,是否会影响我们的体温,是否能帮助我们调节核心温度。
+
我们可以使用我们知道的统计方法进行一些探索性分析,比如检验恋爱状态和赤道距离之间是否存在交互作用。大家还记得中介模型吗?其中一个变量是社交复杂程度,另一个是核心温度,中间有一个变量是赤道距离。研究发现,在不同的情侣关系群体中,这三个变量之间的关系是不一样的。
+
在接下来的课程中,我们将回顾一些关键概念,并进行一些常用的统计检验,包括t检验和方差分析。我们还将介绍为什么t检验和方差分析实际上是线性回归的特例。关于第一个研究问题,我们主要关注恋爱状态是否会影响核心体温。我们会比较两组不同的人,看他们的核心体温是否有差异。
+
基于恋爱状态,我们可以将数据分为两组:一组是处于恋爱或亲密关系中的人,另一组是没有处于亲密关系的人。我们想要比较这两组之间是否存在差异,通常会采用独立样本t检验。这是我们在学习过程中接触过的统计方法,大家应该都不陌生。
+
+(引自IJzerman et al., 2018 )
+
+
+
t -test作为回归模型的特例
+
+
独立样本t 检验(independent t -test)
+
当然,我们需要了解一些基础知识,比如独立样本t检验。第一,我们需要满足正态性假设,即两个样本都来自正态总体。我们也知道,如果样本量足够大,即使不严格服从正态分布,使用t检验也是没有问题的。第二,我们还需要满足方差同质性假设,即两个样本的方差应该是类似的。第三,两个样本应该是独立的。
+
在进行独立样本t检验时,我们的零假设是这两个独立样本在某个变量上的均值没有差异,即μ1等于μ2。而我们的备择假设则是它们的总体均值是有差异的,即μ1不等于μ2。接下来,我们需要计算t值,这是进行t检验时通常需要用到的一个公式。
+
\[t = \frac{\bar{X}_1 - \bar{X}_2}{\sqrt{\frac{s_1^2}{n_1} + \frac{s_2^2}{n_2}}}\]
+
在R语言中,如果我们要进行t检验,首先我们需要进行数据预处理。这包括清洗数据,处理缺失值,选择我们感兴趣的变量,以及生成新的变量等。
+
首先,我们可能需要为数据集生成一些新的变量,比如在原始数据中可能没有被试编号,我们可以随机生成。然后,我们选择自己在分析中关心的问题,使用dplyr函数选择我们感兴趣的变量。
+
在处理数据时,我们需要注意缺失值。在初步分析时,我们一般选择忽略缺失值,直接将其剔除,以便快速查看结果。此外,还需要将一些变量转换为因子,比如将是否处于亲密关系的变量转换为因子,并赋予两个水平:恋爱和单身。
+
接下来,我们关注的因变量是被试的体温,我们想知道在两组之间是否有差异。在原始数据中,有两次测量体温的数据,我们可以选择将两次测量的平均值作为新的变量,这可以通过R语言中的行函数实现。具体来说,我们可以创建一个新的变量,这个变量是以temperature字符串开头的列的平均值。
+
这些步骤都是数据预处理的一部分,是进行t检验之前必要的步骤。
+
df.penguin <- bruceR:: import (here:: here ('data' , 'penguin' , 'penguin_rawdata.csv' )) %>%
+ dplyr:: mutate (subjID = row_number ()) %>%
+ dplyr:: select (subjID,Temperature_t1, Temperature_t2, socialdiversity,
+ Site, DEQ, romantic, ALEX1: ALEX16) %>% # 选择变量
+ dplyr:: filter (! is.na (Temperature_t1) & ! is.na (Temperature_t2) & ! is.na (DEQ)) %>% # 处理缺失值
+ dplyr:: mutate (romantic = factor (romantic, levels = c (1 ,2 ), labels = c ("恋爱" , "单身" )), # 转化为因子
+ Temperature = rowMeans (select (., starts_with ("Temperature" ))), # 计算两次核心温度的均值
+ ALEX4 = case_when (TRUE ~ 6 - ALEX4),
+ ALEX12 = case_when (TRUE ~ 6 - ALEX12),
+ ALEX14 = case_when (TRUE ~ 6 - ALEX14),
+ ALEX16 = case_when (TRUE ~ 6 - ALEX16),
+ ALEX = rowSums (select (., starts_with ("ALEX" )))) # 反向计分后计算总分
+
+
+
要在R语言中进行t检验,我们可以使用自带的stats包中的t.test函数。这是一个非常常用的t检验函数。在使用t.test函数时,我们需要输入一些参数。第一个参数是经过筛选的数据框,第二个参数是我们感兴趣的自变量(如temp),第三个参数是分组变量(如romantic)。此外,我们还可以假定两组的方差是相等的。运行t.test函数后,我们可以得到结果,包括t值、自由度(df)和p值。在这个例子中,t值为0.34664,自由度为1425,而p值较大,表示在恋爱组和非恋爱组之间的体温差异不显著。从结果来看,恋爱状态对体温的影响似乎并不大。
+
stats:: t.test (data = df.penguin, # 数据框
+ Temperature ~ romantic, # 因变量~自变量
+ var.equal = TRUE ) %>%
+ capture.output () # 将输出变整齐
+
## [1] ""
+## [2] "\tTwo Sample t-test"
+## [3] ""
+## [4] "data: Temperature by romantic"
+## [5] "t = -0.34664, df = 1425, p-value = 0.7289"
+## [6] "alternative hypothesis: true difference in means between group 恋爱 and group 单身 is not equal to 0"
+## [7] "95 percent confidence interval:"
+## [8] " -0.0555949 0.0388971"
+## [9] "sample estimates:"
+## [10] "mean in group 恋爱 mean in group 单身 "
+## [11] " 36.38498 36.39333 "
+## [12] ""
+
当然,除了t.test函数,还有其他方法可以进行类似的分析。不过,在这个简单的示例中,t.test已经足够满足我们的需求。
+
我们说t检验是一个特殊的线性回归模型,这是因为在R语言中,它们的编写方式非常相似。在回归模型中,我们将自变量放在前面,因变量放在后面。为了理解这个概念,我们需要简要回顾一下线性回归模型。
+
线性回归模型是一种统计方法,用于研究一个或多个变量能否预测或解释另一个变量。我们将用来预测的变量称为预测变量或自变量,而被预测的变量称为因变量或响应变量。线性回归的核心是通过拟合一条直线来表示两个变量之间的关系,使得直线与每个数据点之间的距离最小。这样,我们便可以利用这条直线来进行预测。
+
我们通常会用一个方程或等式来表示线性回归模型,如y = β0 + β1x1 + β2x2 + … + ε。其中y是我们关心的因变量,x1、x2等是自变量,β0、β1、β2等是回归系数,而ε是误差项。
+
\[y = \beta_0 + \beta_1 x_1 + \beta_2 x_2 + ... + \beta_p x_p + \epsilon\]
+
在学习线性回归时,我们通常关注连续变量之间的关系。这意味着x和y都是连续变量。而在t检验中,我们关注的是分组变量,这似乎与线性回归有所不同。然而,在特定情况下,我们可以将t检验视为线性回归的一个特例。当我们分析的自变量是一个二分类变量时,线性回归模型实际上等同于独立样本t检验。所以,虽然它们在某些方面有所不同,但t检验确实可以看作是线性回归的一个特殊情况。
+
+
+
线性回归(linear regression)
+
在t检验中,我们的因变量,比如体温,是连续的,而自变量,比如是否处于亲密关系,是二分类的。这似乎与我们通常的线性回归模型有所不同,因为在线性回归中,我们通常关注的是连续变量之间的关系。然而,实际上,当我们的自变量是二分类变量时,我们也可以将其视为线性回归模型的一个特例。
+
想象一下,我们在图上有两堆数据点,一堆是处于亲密关系的人的体温,另一堆是非恋爱状态的人的体温。在这两堆数据点之间,我们可以拟合出一条回归线,这条线可以帮助我们进行预测。当x=1(处于亲密关系)时,我们预测的y值(体温)是多少?当x=2(非恋爱状态)时,我们预测的y值是多少?
+
在常规的线性回归中,x可以有很多值,y也可以有很多值。然而,在t检验中,x只有两个值,一个是1,一个是2(或者0和1)。我们要做的预测也是,当x=1时,y是多少?当x=2时,y是多少?这就是为什么我们说独立样本t检验是线性回归模型的一个特例,特别是当我们的自变量是二分变量时。
+
在R语言中,我们可以通过代码来验证这个观点。无论我们是使用t.test函数进行t检验,还是使用lm函数进行线性回归,我们得到的结果应该是一样的。这是因为在这种特殊情况下,这两种方法本质上是在做同样的事情。
+
+
实际上,在进行线性回归时,我们可以采用不同的编码方法来表示分类变量。比如,对于二分类变量,我们可以将其编码为0和1。当我们采用这种编码方式时,线性回归方程为y = β0 + β1x1。其中,x1只有两个可能的取值:0和1。当x1 = 0时,y = β0。当x1 = 1时,y = β0 + β1。这样,我们可以看到,β1实际上表示了两组之间的差异。当然,这只是一种方便我们理解的编码方式。在实际应用中,我们可能会遇到更复杂的编码方法,比如虚拟变量编码、效应编码等。这些编码方法在处理多分类变量时尤为有用。
+
总之,独立样本t检验实际上可以看作是线性回归模型的一个特例,特别是当我们的自变量是二分变量时。无论我们是使用t.test函数进行t检验,还是使用lm函数进行线性回归,我们得到的结果应该是一样的。这是因为在这种特殊情况下,这两种方法本质上是在做同样的事情。
+
首先,我们进行预处理数据。这个过程相对简单,大家应该都能理解。接下来,我们来看一下t检验。假设我们不对结果进行整理,而是直接查看原始输出,那么我们会看到以下内容:t值是多少,df值是多少,以及p值是多少。我们之前提到过,t检验实际上是一种特殊的回归。
+
# t检验
+ stats:: t.test (
+ data = df.penguin,
+ Temperature ~ romantic,
+ var.equal = TRUE ) %>%
+ capture.output () # 将输出变整齐
+
+
我们继续讨论回归模型。在R语言中,线性回归模型的常用函数是lm。我们可以通过比较t检验和线性回归模型得到的t值、df值和p值来判断它们是否相同。
+
在这个地方,大家可以看到我们使用的是stats包的lm代码,这个代码代表线性回归模型。我们在这里还是采用同样的数据,然后使用回归模型的公式。在这个公式中,temp是我们的因变量y,而romantics是我们的自变量x。这个x是一个因子,我们需要将其转换为因子。在编写这个公式时,我们只需在因变量前加上一个波浪号,然后加上自变量。接下来,lm函数会自动帮助我们处理因子变量在回归分析中的一些后续处理。最后,它会给我们一个结果。
+
# 线性回归
+ model.inde <- stats:: lm (
+ data = df.penguin,
+ formula = Temperature ~ 1 + romantic
+ )
+summary (model.inde)
+
+
我们可以看到,这里的t值、p值与我们之前的结果基本相同(t值为0.347,p值为0.729)。这是因为我们主要关注的是单身对结果的影响。因此,我们可以得出这样的结论:两组之间的差异在t检验和回归模型中表现得相当一致。在回归模型中,可能还会有一个intercept,但我们通常不会关注它。斜率slope代表的含义是当x从一个单位变化到另一个单位时,我们的因变量y会发生多少变化。而对于我们这种只有二分变量的自变量的情况,就代表两组之间的差异。
+
因此,我们可以说,二分变量的t检验与回归模型之间存在密切关系。我们关注的是回归系数,它的统计检验值与t检验中的值完全相同。这就是我们希望展示的结果。所以,在心理学中常用的t检验实际上是线性回归的一种特殊情况。
+
那当然,这里涉及到一个知识点,我们没有详细展开讲,但大家以后感兴趣的话可以去了解。这个知识点就是编码方法。因为我们的数据有两个水平,可能有多种编码方法。例如,我们可以把一个水平编码为0,另一个水平编码为1。那么我们的公式可以表示为:y = β0 + β1 * x1。在这个情况下,x1只有两种取值:0和1。当x=0时,y等于β0;当x=1时,y等于β0 + β1。所以这个时候我们可以看出β1就是两组之间的区别。
+
当然,这只是一种方便我们理解的方法。另外一种常用的编码方法是将x1编码为-0.5和0.5。在这种情况下,对β的解读就不一样了。大家可以将这种编码带入公式后,自己去观察一下。我们在这里就不展开详细讲解了,因为我们主要想向大家介绍这个概念。大家可以在R语言中多探索一下这个知识点,以便更好地理解和应用。
+
+
+
单样本t 检验(one sample t -test)
+
那么同理,一系列的t检验其实都可以认为是回归模型的特例。例如,我们来看单样本t检验。虽然我们平时很少使用单样本t检验,但在某些情况下,它还是有用的。比如,我们想要判断某个班级的成绩是否属于全校成绩的总体。在这种情况下,我们实际上是在比较这个班级的成绩均值和全校的总体均值。
+
在单样本t检验中,我们关心的是一个样本的均值是否等于某个特定值。例如,在“在penguin数据中,全体被试的核心体温(Temperature)是否等于36.6?”这个例子中,我们关心的是全体被试的核心温度是否等于正常人的温度。单样本t检验的计算公式可以不用深究,关键是理解其背后的逻辑。
+
\[t = \frac{\bar{X} - \mu}{s / \sqrt{n}}\]
+
\[y = \beta_0 + \beta_1 x_1 + \beta_2 x_2 + ... + \beta_p x_p + \epsilon\]
+
单样本t 检验中,仅截距不为0。此时公式为:
+
\[y = \beta_0\]
+\[H_0: \beta_0 = 0\]
+如图所示,这里有一群点,我们想要比较的是这些点的均值(例如用菱形表示)与某条线(特定值)之间是否存在显著性差异,或者说这些点的均值与这条线是否来自同一个总体。在这种情况下,我们可以构建一个只包含截距项β0的回归模型,即y = β0。我们也可以使用回归模型进行统计检验。
+
+
当我们进行单样本t检验时,我们需要稍微调整公式。我们将因变量(temperature)减去我们想要比较的特定值(例如30),然后考虑减去特定值后的因变量均值是否属于一个以0为均值的正态分布。在这个回归模型中,我们加入一个截距项,用于进行t检验。
+
stats:: t.test (
+ x = df.penguin$ Temperature, # 核心体温均值
+ mu = 36.6 )
+
+
当然,在实际应用中,我们可以直接使用单样本t检验,将观测值x与某个特定值进行比较。这样,我们就可以判断样本数据的均值是否与特定值之间存在显著性差异,从而得出结论。总之,无论是单样本t检验还是回归模型,它们都可以帮助我们解决实际问题。在具体分析过程中,我们可以根据需要选择合适的方法。
+
model.single <- lm (
+ data = df.penguin,
+ formula = Temperature - 36.6 ~ 1
+ )
+summary (model.single)
+
+
+
+
配对样本t 检验(paired t -test)
+
那么,对于配对样本t检验,我们其实可以把它看作是单样本t检验。因为配对样本t检验中的数据是一一对应的,我们可以通过一个简单的操作,即将两列配对数据相减,得到一列新的数据。然后,我们可以将这列新数据进行单样本t检验。
+
\[t = \frac{\bar{X} - \mu}{s / \sqrt{n}}\]
+这里的前提条件我们就不详细讲了。简单来说,对于配对样本t检验,我们可以先将两个值相减,得到他们的差值。然后,我们检验这个差值是否显著地不同于零。如果差值的分布明显不同于零,那么我们就可以认为两者之间存在显著的差异。
+\[y_1 - y_2 = \beta_0 \]
+\[H_0: \beta_0 = 0 \]
+我们之所以能这样做,是因为在配对样本t检验中,每个被试都有两个数据点,这两个数据点是一一对应的。但是,对于独立样本t检验,由于数据点之间没有形成配对关系,我们不能直接相减。
+
+
同样,对于配对样本t检验,我们也可以使用回归方法进行检验。方法类似于独立样本t检验,我们只需将两个值相减,然后检验这个差值的均值是否等于零。在R语言中,我们可以使用t.test函数来进行配对样本t检验。我们只需将两列数据分别作为x和y输入,然后设置参数paired为TRUE,表示这两列数据是配对的,就可以进行配对样本t检验了。
+
stats:: t.test (
+ x = df.penguin$ Temperature_t1,
+ y = df.penguin$ Temperature_t2,
+ paired = TRUE
+ )
+
+
在前面我们讲了关于亲密关系和非亲密关系的两组人在体温上是否有区别的问题。我们可以使用配对样本t检验来进行检验,而配对样本t检验实际上是一个特殊的回归模型。同样地,对于所有其他类型的t检验,它们也都可以看作是特定的回归模型。因此,t检验与线性模型的本质是相同的。
+
model.paired <- lm (
+ Temperature_t1 - Temperature_t2 ~ 1 ,
+ data = df.penguin
+ )
+summary (model.paired)
+
+
+
+
bruceR::TTEST
+
这里,我们可以介绍一下bruceR包中的t检验功能,它涵盖了我们前面提到的各种t检验,并且输出结果非常方便。如果大家下载了我们的课件,可以看到关于bruceR包中t检验各种参数的介绍。bruceR包基本上涵盖了所有类型的t检验,包括独立样本t检验、配对样本t检验和单样本t检验。它的输出结果通常是一个三线表,方便大家查看和理解。
+
+
stats:: t.test (
+ data = df.penguin,
+ Temperature ~ romantic,
+ var.equal = TRUE
+ )
+
+
bruceR:: TTEST (
+ data = df.penguin, # 数据
+ y = "Temperature" , # 因变量
+ x = "romantic" # 自变量
+ )
+
+
[单样本t检验]
+
stats:: t.test (
+ x = df.penguin$ Temperature,
+ mu = 36.6
+ )
+
+
bruceR:: TTEST (
+ data = df.penguin, # 数据
+ y = "Temperature" , # 确定变量
+ test.value = 36.6 , # 固定值
+ test.sided = "=" ) # 假设的方向
+
+
[配对样本t检验]
+
stats:: t.test (
+ x = df.penguin$ Temperature_t1, #第1次
+ y = df.penguin$ Temperature_t2, #第2次
+ paired = TRUE )
+
+
bruceR:: TTEST (
+ data = df.penguin, # 数据
+ y = c ("Temperature_t1" ,
+ "Temperature_t2" ), # 变量为两次核心体温
+ paired = T) # 配对数据,默认是FALSE
+
+
无论是使用bruceR包中的t检验功能,还是R语言自带的t检验功能,我们都可以发现它们得到的结果与线性回归模型的结果是一模一样的。这再次证实了t检验与线性回归模型之间的密切关系。在实际研究中,我们可以根据问题的具体情况选择合适的方法,无论是t检验还是线性回归模型。同时,理解它们之间的关系有助于我们更好地掌握统计分析的原理和技巧。
+
+
+
总结
+
我们来做一个简单的总结。对于t检验,我们可以使用R自带的t.test函数。虽然语法上略有不同,但实际上,线性模型和t检验在本质上是一致的。单样本t检验可以看作是一个仅有截距的线性模型;独立样本t检验是一个仅有截距和一个斜率的线性模型;而配对样本t检验则是一个基于差值的仅有截距的线性模型。
+
独立样本t检验也可以看作是一个自变量为二分变量的线性回归模型。要理解为什么仅有截距的回归模型与t检验相关,我们可能需要回顾正态分布、抽样分布等概念,以及为什么我们使用t值这个分布来进行检验。然而,我们在这里没有足够的时间来展开这个话题。如果大家对此感兴趣,可以自行深入学习,也很推荐大家关注包寒吴霜老师的专栏。
+
+
+
+
ANOVA & linear regression
+
接下来我们讨论方差分析,这也是心理学中常用的一种统计方法。在学习统计时,我们会花很多时间学习如何进行方差分解。
+
+
研究问题
+
以Penguin数据为例,我们刚才发现恋爱状态对体温没有太大影响。但我们可以进一步探讨它是否与距离赤道的距离有交互作用。因为我们知道,离赤道越远,气温就越寒冷。有时候,我们可能会发现某个自变量没有影响,但实际上它可能受到另一个自变量的调节。例如,在这个例子中,我们可以检验恋爱状态是否在较寒冷的地方影响体温,而在较暖和的地方则不需要调节。要检验这种交互作用,我们通常会想到使用双因素方差分析。
+
+
+
代码实操 & 知识回顾
+
双因素方差分析通常是我们在讨论方差分析时所指的完全随机方差分析。但在这里,我们需要注意我们使用的是双因素的重复测量方差分析,而不是完全随机的。也就是说,不同距离赤道的人以及处于不同恋爱状态的人并非随机分配。我们无法将某人随机分配到离赤道较近的恋爱状态这一组,因为这是一个横断数据。然而,我们仍然可以使用方差分析来进行数据分析。
+
在学习方差分析时,我们可能会了解到它的历史以及它所检验的假设。对我们来说,检验的假设是关键。也就是说,在各因素的各个水平下,因变量的均值是否完全相同。原假设认为各因素各水平下的因变量均值完全相同,而备择假设则认为各因素各水平下的因变量均值并不完全相同。通常情况下,只要有一个水平的因变量均值不同,方差分析就能探测出来。
+
当然,方差分析的使用也有一些前提假设,包括随机性、正态性、独立性和方差齐性等。这些内容我们在这里就不再详细展开了。
+
+
+
ANOVA代码实操|数据预处理
+
在R中实现方差分析的关键是选择合适的函数。当我们想进行多因素方差分析时,R中有一些常用的函数可以帮助我们实现。在进行方差分析之前,我们需要对数据进行一些预处理。
+
以距离赤道的距离为例,我们发现它是一个连续的数值变量。但为了进行方差分析,我们可以将其分为三个水平:热带、温带和寒温带。我们可以根据纬度的区别设定分隔点,例如,赤道到23.5度为热带,35度到40度为暖温带,40度到50度为中温带(统称为温带),50度以上为寒温带。我们可以使用R中的cut函数将连续性的数据划分为不同的段,并赋予不同的值。同时,我们可以为这个新变量命名为climate。
+[vars]
+
+
## Min. 1st Qu. Median Mean 3rd Qu. Max.
+## 1.293 34.433 39.912 39.842 51.317 60.391
+
# 设定分割点
+# [0-23.5 热带, 23.5-35 亚热带], [35-40 暖温带, 40-50 中温带], [50-66.5 寒温带]
+ breaks <- c (0 , 35 , 50 , 66.5 )
+
+# 设定相应的标签
+ labels <- c ('热带' , '温带' , '寒温带' )
+
+# 创建新的变量
+ df.penguin$ climate <- cut (df.penguin$ DEQ,
+ breaks = breaks,
+ labels = labels)
+summary (df.penguin$ climate)
+
## 热带 温带 寒温带
+## 396 592 439
+
[tidy data]
+
df <- df.penguin %>%
+ select (subjID, climate, romantic, Temperature)
+
+
+
+
+
代码实操|正态性检验
+
在进行方差分析前,我们还需要检验数据的正态性。R中有一些方法可以帮助我们检验正态性,例如KS检验和QQ图。通过这些方法,我们可以观察数据的分布情况。虽然数据可能不是完全正态分布的,但接近正态分布的数据也是可以接受的。
+
# 正态性检验-Kolmogorov-Smirnov检验
+# 若p >.05,不能拒绝数据符合正态分布的零假设
+ks.test (df$ Temperature, 'pnorm' )
+
## Warning in ks.test.default(df$Temperature, "pnorm"): Kolmogorov -
+## Smirnov检验里不应该有连结
+
##
+## Asymptotic one-sample Kolmogorov-Smirnov test
+##
+## data: df$Temperature
+## D = 1, p-value < 0.00000000000000022
+## alternative hypothesis: two-sided
+
# 进行数据转换,转换后仍非正态分布
+ df$ Temperature_log <- log (df$ Temperature)
+ks.test (df$ Temperature_log, 'pnorm' )
+
## Warning in ks.test.default(df$Temperature_log, "pnorm"): Kolmogorov -
+## Smirnov检验里不应该有连结
+
##
+## Asymptotic one-sample Kolmogorov-Smirnov test
+##
+## data: df$Temperature_log
+## D = 0.99981, p-value < 0.00000000000000022
+## alternative hypothesis: two-sided
+
[qq图]
+
# 正态性检验-qq图
+qqnorm (df$ Temperature)
+qqline (df$ Temperature, col = "red" ) # 添加理论正态分布线
+
+
直方图
+
ggplot (df, aes (Temperature)) +
+ geom_histogram (aes (y = ..density..), color= 'black' , fill= 'white' , bins= 30 ) +
+ geom_density (alpha= .5 , fill= 'red' )
+
## Warning: The dot-dot notation (`..density..`) was deprecated in ggplot2 3.4.0.
+## ℹ Please use `after_stat(density)` instead.
+## This warning is displayed once every 8 hours.
+## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
+## generated.
+
+
+
+
代码实操|双因素被试间方差分析
+
在R中实现方差分析,我们通常会用到aov函数,这是R自带的一个函数。实际上,这个函数来自于stats包,同样的,t.test函数和lm函数也是来源于这个包。在进行方差分析时,我们采用的语法与t检验非常相似,仍然是线性回归的写法。我们将因变量放在前面,自变量放在后面,中间用一个波浪号连接。然后,我们指定使用的数据。
+
在方差分析的语法中,我们使用乘号 * 来表示主效应及其交互作用。例如,我们可以写成climate * romantic,这表示考虑climate和romantic的主效应以及它们之间的交互作用。在R中,这样的写法非常常见,前面的波浪号表示因变量与自变量之间的关系,而乘号表示考虑主效应及交互作用。
+
实际上,这种线性方程有多种写法。例如,在t检验中,我们只关注romantic;而在方差分析中,我们加入了climate的主效应以及它们之间的交互作用。在R中最基础的包里,当我们进行方差分析时,实际上沿用了线性模型的语法。通过这种语法,我们可以得到方差分析的一些统计值,如F值、P值以及我们熟悉的平方和(SS,Sum of Squares)。
+
如果我们对方差分析的结果使用summary函数进行查看,我们会看到熟悉的方差分析表。在这个表中,我们可以看到两个主效应(romantic和climate)以及它们之间的交互作用。同时,表格中还包含了残差(Residuals)信息。这些信息有助于我们了解各个因素对因变量的影响程度和显著性。
+
aov1 <- stats:: aov (Temperature ~ climate * romantic, data = df)
+summary (aov1)
+
## Df Sum Sq Mean Sq F value Pr(>F)
+## climate 2 18.82 9.408 49.392 < 0.0000000000000002 ***
+## romantic 1 0.24 0.244 1.280 0.25807
+## climate:romantic 2 1.91 0.955 5.014 0.00676 **
+## Residuals 1421 270.65 0.190
+## ---
+## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
+
在进行方差分析时,有时我们会发现,在R中进行的分析结果与在SPSS中进行的结果不完全一样。例如,climate的F值在R中为49.39,而在SPSS中为50.88。这种情况下,我们可能会感到困惑,不知道是否可以信任R的结果,甚至可能会认为R的分析方法不靠谱。
+
[SPSS]
+
+实际上,这种差异的出现很可能是因为在R和SPSS中使用的方法或某些设置不同。这可能导致在相同的数据和看似相同的方法下,得到不同的分析结果。在这里,我们需要讨论一个问题:为什么方差分析这样一个看似简单的方法(我们在本科阶段就学过并且很熟悉),会在不同软件中产生不同的结果呢?
+
+
+
代码实操|平方和(SS)的计算
+
在进行方差分析时,可能会涉及到平方和计算方法的问题。在平衡设计下,即每个条件下的样本量相同时,三类平方和的结果是没有差别的。但在不平衡设计下,即每个条件下的样本量不同时,就需要进行一些调整。
+
这里的不平衡设计是指,比如我们有两个变量,climate有三个水平,romantic有两个水平,那么组合之后就有六个条件,但这六个条件下的被试数量可能是不同的。理论上,最好的情况是每个条件下的被试数量是一样的,这样的设计被称为平衡设计。在平衡设计中,三种类型的平方和的结果会很清晰,并且方差分析的结果独立于平方和的类型。但在实际情况中,这种设计可能较难实现,从而导致不平衡设计。
+
在不平衡设计下,计算方差时就会出现问题,因为我们需要将不同组的方差进行合并。尤其是当各组样本量差距较大时,三种类型的平方和计算结果可能会不同。在这种情况下,我们需要进行一些调整,需要根据具体研究设计和问题来选择使用哪一种类型的平方和。这就产生了三类平方和。SPSS默认使用的是第三类平方和,而在R中,aov函数默认使用的是第一类平方和。因此,当我们使用R进行方差分析时,可能会发现结果与SPSS有所不同。
+
这种情况下,我们需要更深入地理解平方和的计算方法,以便正确解读R的输出结果。这也是我们在使用R进行统计分析时,需要不断强化统计知识的一个重要原因。因为相比于SPSS这类点击式操作的软件,R提供了更多的选项,而正确理解和使用这些选项,就需要我们对统计学有更深入的理解。
+
我们是否能得到完全相同的结果呢?实际上,这是完全可能的。例如,我们可以使用afex这个包,这是在2017或2018年后出现的一个包。afex是为了满足实验心理学家进行方差分析的需求而设计的,其中包含了各种方差分析的功能。如果我们按照这样的写法,即将被试的identity写在这里,然后将类型设置为三,我们就能得到完全相同的结果。我们也可以在其他地方使用同样的方法,同样能得到一模一样的结果。
+
[car::Anova()]
+
# 结果不一致,原因PPT显示不全,请回到rmd文档查看
+ aov1 <- car:: Anova (stats:: aov (Temperature ~ climate * romantic, data = df))
+ aov1
+
## Anova Table (Type II tests)
+##
+## Response: Temperature
+## Sum Sq Df F value Pr(>F)
+## climate 19.034 2 49.9680 < 0.00000000000000022 ***
+## romantic 0.244 1 1.2801 0.258072
+## climate:romantic 1.910 2 5.0136 0.006765 **
+## Residuals 270.654 1421
+## ---
+## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
+
我们是否能得到完全相同的结果呢?实际上,这是完全可能的。例如,我们可以使用afex这个包,这是在2017或2018年后出现的一个包。afex是为了满足实验心理学家进行方差分析的需求而设计的,其中包含了各种方差分析的功能。如果我们按照这样的写法,即将被试的identity写在这里,然后将类型设置为三,我们就能得到完全相同的结果。我们也可以在其他地方使用同样的方法,同样能得到一模一样的结果。
+
# 原因debug
+# 查看R的默认对比设置
+options ("contrasts" )
+
## $contrasts
+## unordered ordered
+## "contr.treatment" "contr.poly"
+
# 从输出结果可知,无序默认为contr.treatment(),有序默认为contr.poly()
+# factor()函数来创建无序因子,ordered()函数创建有序因子
+
+is.factor (df$ climate)
+
## [1] TRUE
+
+
## [1] FALSE
+
# climate是无序因子
+
+# 创建一个3水平的因子的基准对比
+ c1 <- contr.treatment (3 )
+
+# 创建一个新的对比,这个编码假设分类水平之间的差异被等分,每一个水平与总均值的差异等于1/3
+ my.coding <- matrix (rep (1 / 3 , 6 ), ncol= 2 )
+# 将对比调整为每个水平与第一个水平的振幅减去1/3
+# 可能的原因:除了关心每个水平对应的效果,同时也关心水平与水平之间的效果
+ my.simple <- c1- my.coding
+ my.simple
+
## 2 3
+## 1 -0.3333333 -0.3333333
+## 2 0.6666667 -0.3333333
+## 3 -0.3333333 0.6666667
+
# 更改climate的对比
+contrasts (df$ climate) <- my.simple
+
+# 将数据集df的romantic列的对比设为等距对比,它假设分类水平之间的差异为等距离
+contrasts (df$ romantic) <- contr.sum (2 )/ 2
+
+# 方差分析
+ aov1 <- car:: Anova (lm (Temperature ~ climate * romantic, data = df),
+ type = 3 )
+ aov1
+
## Anova Table (Type III tests)
+##
+## Response: Temperature
+## Sum Sq Df F value Pr(>F)
+## (Intercept) 1768309 1 9284069.6498 < 0.00000000000000022 ***
+## climate 19 2 50.8832 < 0.00000000000000022 ***
+## romantic 0 1 1.0006 0.317336
+## climate:romantic 2 2 5.0136 0.006765 **
+## Residuals 271 1421
+## ---
+## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
+
[afex::aov_ez()]
+
afex:: aov_ez (id = "subjID" ,
+ dv = "Temperature" ,
+ data = df,
+ between = c ("climate" , "romantic" ),
+ type = 3 )
+
## Contrasts set to contr.sum for the following variables: climate, romantic
+
## Anova Table (Type 3 tests)
+##
+## Response: Temperature
+## Effect df MSE F ges p.value
+## 1 climate 2, 1421 0.19 50.88 *** .067 <.001
+## 2 romantic 1, 1421 0.19 1.00 <.001 .317
+## 3 climate:romantic 2, 1421 0.19 5.01 ** .007 .007
+## ---
+## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '+' 0.1 ' ' 1
+
# afex中的其他函数可以得到同样的结果
+ afex:: aov_car (Temperature ~ climate * romantic + Error (subjID), data = df, type = 3 )
+
## Contrasts set to contr.sum for the following variables: climate, romantic
+
afex:: aov_4 (Temperature ~ climate * romantic + (1 | subjID), data = df)
+
## Contrasts set to contr.sum for the following variables: climate, romantic
+
也就是说,如果我们使用最常见的R代码,很可能会得到与SPSS不同的结果。同样的现象也可能发生在我们习惯使用AMOS进行结构方程模型(SEM)的情况下。当你使用R的某些包进行结构方程模型时,即使是同样的数据和结构,可能会得到不同的结果。这可能是因为它们内部设定的不同。因此,我们需要去了解它们内部的设定是什么。
+
+
+
+
线性回归
+
我们刚刚讨论的是t检验,它是一种特定的回归模型。而对于ANOVA(方差分析),从它的表示方法来看,我们可以很明显地看出它实际上也是一个回归模型。因为它的方程表示形式与线性模型非常相似,包括一些自变量以及它们之间是否存在交互作用。在实际应用中,我们经常会遇到单因素方差分析和多因素方差分析。
+
在这种情况下,两因素方差分析也是一个特殊的回归模型。它的特殊之处在于我们的自变量是分类变量,也可以称为离散变量。因此,在进行方差分析时,我们需要注意处理这些分类变量或离散变量。
+
假设我们这里有一个2×2的设计,也就是有四个条件,这四个条件可以视为四组。每一组被试在某一因变量上都会有自己的取值。我们需要检验的是这四组被试的均值是否完全相同,即它们是否有偏离总体均值的情况。这就是我们进行方差分析的一个核心目标。在线性模型上,我们也在检验同样的问题。
+
比如说,我们可以通过某种方式对自变量进行编码,然后观察各组的均值与其他组之间的差异是否达到显著水平。举个例子,这里我们展示的是一种非常简单的编码方式,我们以某一条件下的值作为零点,比如第一组,然后其他所有的系数都是其他组的均值与这个零点的差异,也就是我们斜率的比较对象。
+
可能这么说有点抽象,让我们以一个具体的例子来说明。假设我们有一个变量叫”romantic”,我们可以把它编码为0和1。同样地,我们也可以把其他变量编码为0和1。如果我们简化这两种情况,就更容易理解了。
+
在这里,我们可以看到如果有一个组合为0和0,那么在我们的回归模型中,这个部分就变成了β0。在这种编码方式下,β0代表的就是某个条件下的均值。通过这种方式,我们可以看到β1可能代表的是某种条件下的值,比如当某个变量取值为1或0时的情况。同样,β2可能代表的是另一个变量在取值为1、0或者两个变量都为1时的情况。
+
所有这些回归系数都可以被理解为组间的差异值。我们的回归模型的目标就是去检验这些差异是否显著。在做回归模型时,我们检验显著性的方式可能有所不同。在这种最简化的情况下,我们可以看到,我们的2×2的两因素方差分析完全可以对应成为一个离散变量的回归模型。
+
+
如果我们采用ANOVA或者线性模型,并且我们的设定是一样的,那么得到的结果将会是完全一样的。可能我讲得有点快,大家可能需要回顾一下自己学的线性回归的知识。因为我不确定其他的统计老师在讲线性回归的时候是否已经将其与ANOVA或t检验进行了关联。但大家可以这样理解,它们本质上都是一样的。如果大家理解了,ANOVA其实就是线性回归的一个特定模式,那么你们就已经把握了其要点。
+
aov1 <- car:: Anova (
+ aov (Temperature ~ climate * romantic,
+ data = df),
+ type = 3
+ )
+ aov1
+
+
lm1 <- car:: Anova (
+ lm (Temperature ~ climate * romantic,
+ data = df),
+ type = 3
+ )
+ lm1
+
+
在实际研究中,即使我们知道ANOVA与线性回归有相似之处,但我们仍然需要报告传统的方差分析结果。在这种情况下,我们推荐使用bruceR包的mANOVA
函数,它是一个非常强大的函数,基本上涵盖了我们在心理学中常用的所有ANOVA类型。使用mANOVA
函数可以大大简化心理学研究者使用这个方法的门槛。
+
+
关于mANOVA
函数的主要参数,我们不再详细讲解,大家可以查阅相关资料。该函数非常全面,而且有一个非常好的特点,就是它可以直接将结果保存为三线表格,方便我们将结果粘贴到Word文档中进行报告。
+
+
以我们刚才讲解的例子为例,我们要对climate和romantic进行方差分析,我们可以调用mANOVA
函数,设置dependent variable为temperature,参数设为between表示主间变量。这里有两个主间变量。大家可以看到,得到的结果与SPSS是完全相同的,如50.88。
+
与SPSS不同之处在于mANOVA
函数会输出Generalized Eta Squared(广义η²),这是方差分析的一个效应量指标,有助于我们更好地理解结果。关于效应量的指标,对于传统心理学来说,我们通常只关注p值。但实际上,最近的趋势是大家会更加关注效应量有多大,而不仅仅是p值是否显著。换句话说,我们不仅关心结果是否显著,还关心效应量是否足够大。
+
在方差分析中,我们经常会进行进一步的简单效应分析和事后多重比较。这里的操作比较灵活。例如,如果你使用的是纯粹的重复测量设计,那么在没有交互作用的情况下,我们可能会进行多重比较,也就是比较某一条件下不同水平之间是否存在差异。然而,如果存在交互作用,我们可能需要重新检查在某一自变量的水平下,另一个自变量是否具有效应。这种情况下,我们称之为简单效应。简单效应分析可以帮助我们深入了解在某一特定条件下,不同水平之间的差异,以便更好地解释和理解我们的研究结果。
+
res1 <- bruceR:: MANOVA (data = df,
+ dv = "Temperature" ,
+ between = c ("climate" , "romantic" ))
+
## [1] "Anova Table (Type III tests)"
+## [2] ""
+## [3] "Response: Temperature"
+## [4] " Effect df MSE F ges p.value"
+## [5] "1 climate 2, 1421 0.19 50.88 *** .067 <.001"
+## [6] "2 romantic 1, 1421 0.19 1.00 <.001 .317"
+## [7] "3 climate:romantic 2, 1421 0.19 5.01 ** .007 .007"
+## [8] "---"
+## [9] "Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '+' 0.1 ' ' 1"
+
在这个例子中,我们可以看到,我们对climate和romantic进行了方差分析,试图查看在不同的romantic条件下(即单身或非单身),climate是否有影响。这就是简单效应分析。
+
在R中,我们可以看到mANOVA函数的输出结果。首先,它会输出描述性统计结果,包括climate和romantic的各个水平的均值、标准差和样本量。然后,它会给出方差分析表,包括climate的主效应、romantic的主效应和它们的交互作用的效应。我们可以看到,climate的主效应是显著的,这与我们的预期一致。即在不同气候带的人们体温存在差异,其p值非常小,F值也比较大。此外,我们也发现climate和romantic的交互作用是显著的,这也与我们的预期相符。
+
原先,我们试图通过t检验来查看不同romantic状态下的人们体温是否有区别,但我们并没有发现显著差异。然后,我们怀疑这可能是因为人的体温受到climate的影响,这个效应可能掩盖了romantic的影响。因此,我们引入了climate这个因素,然后再次检验romantic和climate的交互作用。结果显示,这个交互作用是显著的,这也就验证了我们的猜想。
+
在mANOVA中,它会输出各种效应量指标,如η²和广义η²(Generalized Eta Squared)。通常情况下,在做方差分析时,我们会报告广义η²。实际上,关于方差分析的效应量指标是一个重要且不容易掌握的知识点。在mANOVA中,它还会输出其他效应量指标,如Cohen’s f和方差同质性检验结果。这些输出结果非常符合心理学研究者的使用习惯。
+
+
当我们发现交互作用时,我们通常需要进一步了解这个交互作用代表什么。在这种情况下,我们可以使用emmeans
(estimated marginal means)函数来查看不同条件下的结果。这个函数可以帮助我们查看在不同条件下,climate和romantic之间的简单效应。通过emmeans
函数,我们可以得到一个三线表,展示了在不同romantic条件下,climate的简单效应。在这个表格中,我们可以看到,在不同的romantic条件下,climate都具有显著效应。这些信息有助于我们更深入地了解交互作用背后的实际情况。
+
在这个分析过程中,我们首先在恋爱和单身的条件下分别看了climate的影响,并发现climate在两种情况下都有显著影响,虽然效应量可能会有所不同。然后,我们进一步进行了post-hoc比较,比较了恋爱和单身状态下,不同climate的体温差异,结果显示基本上所有的差异都是显著的。
+
然后,我们通过改变分组条件,看了在不同climate下,恋爱关系是否有影响。结果显示,在热带和温带,恋爱关系没有显著影响,但在寒带,恋爱关系则有显著影响。这个结果与我们的预期相符,表明在较冷的气候下,恋爱关系对体温有调节作用。
+
此外,我们还进行了F检验,实际上是在热带、温带和寒带三种条件下进行了单因素方差分析。由于只有两个水平(恋爱和单身),所以这个F检验的结果与后面的t检验结果基本一致。这是因为在只有两个水平的情况下,F检验和t检验的结果是一样的。如果你对这个有兴趣,你可以进一步查看t值和F值之间的关系。
+
sim_eff <- res1 %>%
+ bruceR:: EMMEANS ("climate" , by = "romantic" ) %>%
+ bruceR:: EMMEANS ("romantic" , by = "climate" )
+
—— EMMEANS (effect = “climate”) ——
+
Joint Tests of “climate”:
+─────────────────────────────────────────────────────────────────
+Effect “romantic” df1 df2 F p η²p [90% CI of η²p]
+─────────────────────────────────────────────────────────────────
+climate 恋爱 2 1421 13.165 <.001 *** .018 [.008, .031]
+climate 单身 2 1421 41.817 <.001 *** .056 [.037, .075]
+─────────────────────────────────────────────────────────────────
+Note. Simple effects of repeated measures with 3 or more levels
+are different from the results obtained with SPSS MANOVA syntax.
+
## Warning in rbind(deparse.level, ...): number of columns of result, 8, is not a
+## multiple of vector length 6 of arg 2
+
Univariate Tests of “climate”:
+───────────────────────────────────────────────────────────────
+Sum of Squares df Mean Square F p
+───────────────────────────────────────────────────────────────
+恋爱: “climate” 5.015 2 2.507 13.165 <.001
+单身: “climate” 15.929 2 7.965 41.817 <.001
+Residuals
+───────────────────────────────────────────────────────────────
+Note. Identical to the results obtained with SPSS GLM EMMEANS syntax.
+
Estimated Marginal Means of “climate”:
+─────────────────────────────────────────────────────
+“climate” “romantic” Mean [95% CI of Mean] S.E.
+─────────────────────────────────────────────────────
+热带 恋爱 36.510 [36.446, 36.575] (0.033)
+温带 恋爱 36.393 [36.346, 36.440] (0.024)
+寒温带 恋爱 36.296 [36.245, 36.347] (0.026)
+热带 单身 36.590 [36.532, 36.648] (0.029)
+温带 单身 36.356 [36.303, 36.409] (0.027)
+寒温带 单身 36.182 [36.114, 36.250] (0.035)
+─────────────────────────────────────────────────────
+
Pairwise Comparisons of “climate”:
+────────────────────────────────────────────────────────────────────────────────────────
+Contrast “romantic” Estimate S.E. df t p Cohen’s d [95% CI of d]
+────────────────────────────────────────────────────────────────────────────────────────
+温带 - 热带 恋爱 -0.117 (0.041) 1421 -2.879 .012 * -0.268 [-0.491, -0.045]
+寒温带 - 热带 恋爱 -0.214 (0.042) 1421 -5.111 <.001 *** -0.490 [-0.720, -0.260]
+寒温带 - 温带 恋爱 -0.097 (0.035) 1421 -2.741 .019 * -0.222 [-0.417, -0.028]
+温带 - 热带 单身 -0.234 (0.040) 1421 -5.853 <.001 *** -0.536 [-0.756, -0.317]
+寒温带 - 热带 单身 -0.409 (0.046) 1421 -8.969 <.001 *** -0.936 [-1.186, -0.686]
+寒温带 - 温带 单身 -0.174 (0.044) 1421 -3.966 <.001 *** -0.400 [-0.641, -0.158]
+────────────────────────────────────────────────────────────────────────────────────────
+Pooled SD for computing Cohen’s d: 0.436
+P-value adjustment: Bonferroni method for 3 tests.
+
Disclaimer:
+By default, pooled SD is Root Mean Square Error (RMSE).
+There is much disagreement on how to compute Cohen’s d.
+You are completely responsible for setting sd.pooled
.
+You might also use effectsize::t_to_d()
to compute d.
+
—— EMMEANS (effect = “romantic”) ——
+
Joint Tests of “romantic”:
+────────────────────────────────────────────────────────────────
+Effect “climate” df1 df2 F p η²p [90% CI of η²p]
+────────────────────────────────────────────────────────────────
+romantic 热带 1 1421 3.287 .070 . .002 [.000, .008]
+romantic 温带 1 1421 1.055 .305 .001 [.000, .005]
+romantic 寒温带 1 1421 6.966 .008 ** .005 [.001, .013]
+────────────────────────────────────────────────────────────────
+Note. Simple effects of repeated measures with 3 or more levels
+are different from the results obtained with SPSS MANOVA syntax.
+
## Warning in rbind(deparse.level, ...): number of columns of result, 6, is not a
+## multiple of vector length 5 of arg 2
+
Univariate Tests of “romantic”:
+──────────────────────────────────────────────────────────────────
+Sum of Squares df Mean Square F p
+──────────────────────────────────────────────────────────────────
+热带: “romantic” 0.626 1 0.626 3.287 .070 .
+温带: “romantic” 0.201 1 0.201 1.055 .305
+寒温带: “romantic” 1.327 1 1.327 6.966 .008 **
+Residuals 271
+──────────────────────────────────────────────────────────────────
+Note. Identical to the results obtained with SPSS GLM EMMEANS syntax.
+
Estimated Marginal Means of “romantic”:
+─────────────────────────────────────────────────────
+“romantic” “climate” Mean [95% CI of Mean] S.E.
+─────────────────────────────────────────────────────
+恋爱 热带 36.510 [36.446, 36.575] (0.033)
+单身 热带 36.590 [36.532, 36.648] (0.029)
+恋爱 温带 36.393 [36.346, 36.440] (0.024)
+单身 温带 36.356 [36.303, 36.409] (0.027)
+恋爱 寒温带 36.296 [36.245, 36.347] (0.026)
+单身 寒温带 36.182 [36.114, 36.250] (0.035)
+─────────────────────────────────────────────────────
+
Pairwise Comparisons of “romantic”:
+─────────────────────────────────────────────────────────────────────────────────────
+Contrast “climate” Estimate S.E. df t p Cohen’s d [95% CI of d]
+─────────────────────────────────────────────────────────────────────────────────────
+单身 - 恋爱 热带 0.080 (0.044) 1421 1.813 .070 . 0.183 [-0.015, 0.382]
+单身 - 恋爱 温带 -0.037 (0.036) 1421 -1.027 .305 -0.085 [-0.247, 0.077]
+单身 - 恋爱 寒温带 -0.115 (0.043) 1421 -2.639 .008 ** -0.262 [-0.458, -0.067]
+─────────────────────────────────────────────────────────────────────────────────────
+Pooled SD for computing Cohen’s d: 0.436
+No need to adjust p values.
+
Disclaimer:
+By default, pooled SD is Root Mean Square Error (RMSE).
+There is much disagreement on how to compute Cohen’s d.
+You are completely responsible for setting sd.pooled
.
+You might also use effectsize::t_to_d()
to compute d.
+
+
+
知识延申|单因素方差分析示例
+
在本节课中,我们讲解了单因素方差分析和多因素方差分析,并重点讲解了多因素方差分析。我们提到了R中自带的函数以及其他一些适合心理学研究者使用的包,如PR和fx。这些包的输出结果与SPSS的输出结果非常相似,因此可以让老师和同学们更加放心地使用R进行分析。
+
# DEQ对Temperature的影响
+ res2 <- bruceR:: MANOVA (
+ data = df,
+ dv = "Temperature" ,
+ between = "climate" )
+
##
+## ====== ANOVA (Between-Subjects Design) ======
+##
+## Descriptives:
+## ─────────────────────────────
+## "climate" Mean S.D. n
+## ─────────────────────────────
+## 热带 36.555 (0.411) 396
+## 温带 36.377 (0.401) 592
+## 寒温带 36.255 (0.504) 439
+## ─────────────────────────────
+## Total sample size: N = 1427
+##
+## ANOVA Table:
+## Dependent variable(s): Temperature
+## Between-subjects factor(s): climate
+## Within-subjects factor(s): –
+## Covariate(s): –
+## ───────────────────────────────────────────────────────────────────────
+## MS MSE df1 df2 F p η²p [90% CI of η²p] η²G
+## ───────────────────────────────────────────────────────────────────────
+## climate 9.408 0.192 2 1424 49.106 <.001 *** .065 [.045, .085] .065
+## ───────────────────────────────────────────────────────────────────────
+## MSE = mean square error (the residual variance of the linear model)
+## η²p = partial eta-squared = SS / (SS + SSE) = F * df1 / (F * df1 + df2)
+## ω²p = partial omega-squared = (F - 1) * df1 / (F * df1 + df2 + 1)
+## η²G = generalized eta-squared (see Olejnik & Algina, 2003)
+## Cohen’s f² = η²p / (1 - η²p)
+##
+## Levene’s Test for Homogeneity of Variance:
+## ──────────────────────────────────────────────
+## Levene’s F df1 df2 p
+## ──────────────────────────────────────────────
+## DV: Temperature 18.207 2 1424 <.001 ***
+## ──────────────────────────────────────────────
+
+
+
知识延申|总结
+
此外,我们还讨论了线性模型与t检验之间的关系。可能有些同学在理解这部分内容时会感到困惑,这时候建议大家回顾一下本科阶段的教材,加深对回归模型和t检验之间关系的理解。学习代码和应用是很重要的,但要正确地使用它们,还需要掌握背后的统计知识。
+
总之,通过学习本节课的内容,我们可以更好地理解方差分析的原理和应用,并能够在R中使用相应的函数进行实际分析。同时,我们也要意识到,在学习和使用R进行统计分析时,理解和掌握背后的统计知识是非常重要的。
+
课堂总结
+
+
在今天的课程中,我们讨论了方差分析的原理和应用,并在R中使用了相关函数进行实际分析。我们还提到了一个国外博客,这个博客指出许多常见的统计检验都是线性模型的特例。这可以帮助大家更好地理解这些统计方法之间的联系。
+
最后,我们留了一个思考题:对于重复测量设计(within-subject design),如何用回归模型进行分析?此外,我们还给出了一些课堂练习,建议大家尝试自己编写代码来完成这些练习,以加深对课程内容的理解。
+
本节课在此结束。感谢大家的参与,如果有任何问题,请随时提问。
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/bookdown_files/Books/Book/_main_files/figure-html/unnamed-chunk-20-1.png b/bookdown_files/Books/Book/_main_files/figure-html/unnamed-chunk-20-1.png
new file mode 100644
index 00000000..232599a8
Binary files /dev/null and b/bookdown_files/Books/Book/_main_files/figure-html/unnamed-chunk-20-1.png differ
diff --git a/bookdown_files/Books/Book/_main_files/figure-html/unnamed-chunk-21-1.png b/bookdown_files/Books/Book/_main_files/figure-html/unnamed-chunk-21-1.png
new file mode 100644
index 00000000..6bd23b3b
Binary files /dev/null and b/bookdown_files/Books/Book/_main_files/figure-html/unnamed-chunk-21-1.png differ