Because TIMSS and other ILSAs sample schools first (the primary sampling unit or PSU) and then classrooms/students within schools using multistage sampling, standard variance formulas that assume independent observations would understate the true sampling error.
Replicate weights are a set of variables used to correctly compute the standard errors of a point estimate (e.g., regression coefficient, mean). Jackknife Repeated Replication (JRR) is a method for generating replicate weights. Conceptually, a statistic of interest is computed as many times as there are replicate weights and the variability of that statistic is used in the computation of the standard error. JRR involves (specifically for a variant referred to as JK2 used in TIMSS):
Creating the weights:
Using the weights:
TIMSS typically uses 75–150+ replicate weights (depending on the
number of primary sampling units), and the information regarding the
replicate weights are provided directly in the international database—
you just apply the JRR weights using software (e.g.,the IEA IDB
Analyzer, R) that support this replication method. NOTE: the replication
weights are not in the dataset but the JKZONE and
JKREP variables are.
Using regular standard errors on TIMSS data without JRR will more likely be statistically significant since it ignores the clustering of students within schools (i.e., standard errors are underestimated).
In statistics, Maurice Quenouille introduced jackknifing (in 1949) and a jackknife (coined by Tukey referring to a multipurpose tool) is referred to as a resampling technique for estimating the variance of a statistic (sometimes referred to as the “delete-1 jackknife”). The standard jackknife works using the following general steps:
\[\frac{(n-1)}{n} \Sigma({\hat{\theta}_{-i}}-\bar{\theta}_.)^2\]
data(mtcars)
n <- nrow(mtcars) #32 observations
full_mean <- mean(mtcars$mpg) #theta
mns <- numeric() #container to store estimated theta
for (i in 1:n){
mns[i] <- mean(mtcars$mpg[-i]) #leave one out
}
jack_var <- ((n - 1)/n) * sum((mns - mean(mns))^2) #(n - 1) / n is a jackknife correction factor
sqrt(jack_var)
[1] 1.065424
sd(mtcars$mpg) / sqrt(n)
[1] 1.065424
Jackknifing is different from bootstrapping in the sense that it is deterministic– it will always give the same result compared to a bootstrap which is based on random draws. There is also a difference is the original overall mean is used vs. the mean of the means.
This data file is similar to the LSAs we use (i.e., nested, uses disproportional sampling of different strata, oversampling:)
library(here)
library(dplyr)
library(survey)
library(sjmisc)
# df1 <- read.table(here("05_data/dat1_150.dat"))
# names(df1) <- c('id', 'schwgt', 'y', 'w1', 'x1',
# 'minor', 'one', 'nwt', 'totwgt', 'stuwgt',
# 'strata', 'mos')
df1 <- rio::import('https://github.com/flh3/pubdata/raw/refs/heads/main/ILSA/n150.sav')
n_distinct(df1$id) #school id
[1] 150
n_distinct(df1$strata)
[1] 3
This is just a synthetic dataset of 150 schools, with 25 students per school (25 * 150):
nrow(df1)
[1] 3750
head(df1)
id schwgt y w1 x1 minor one nwt totwgt stuwgt strata mos
1 8 13.673 1.595 -0.176 -2.011 0 1 0.561 39.653 2.9 2 65
2 8 13.673 0.788 -0.176 -1.525 0 1 0.561 39.653 2.9 2 65
3 8 13.673 0.371 -0.176 -1.735 0 1 0.561 39.653 2.9 2 65
4 8 13.673 2.219 -0.176 -0.627 0 1 0.561 39.653 2.9 2 65
5 8 13.673 1.066 -0.176 1.735 0 1 0.561 39.653 2.9 2 65
6 8 13.673 0.653 -0.176 -0.340 0 1 0.561 39.653 2.9 2 65
The following syntax shows how the zones and
rep are created.
First, sort by strata and measure of size (enrollment). Second, a pair of schools is placed in a zone (i.e., 2 schools per zone).
# 1. Build a SCHOOL-level frame, sorted within stratum for pairing
school_frame <- df1 %>%
distinct(id, strata, mos) %>%
arrange(strata, mos) %>%
group_by(strata) %>%
mutate(
within_stratum_rank = row_number(),
n_in_stratum = n(),
pair_id = ceiling(within_stratum_rank / 2)
) %>%
ungroup() %>%
mutate(zone = paste(strata, pair_id, sep = "_"))
In cases where are there are no pairs (e.g., there is an extra school
in strata 1 since there are 22 pairs + 1 extra since there are 45
schools), a pseudo-pair is created by splitting the last school randomly
into two and giving it a pseudo zone (the one school is split in two).
See zones 1_23 and 3_38.
Showing the last 6 observations in the 3 strata:
school_frame %>% group_by(strata) %>% slice_tail(n = 6)
# A tibble: 18 × 7
# Groups: strata [3]
id strata mos within_stratum_rank n_in_stratum pair_id zone
<dbl> <dbl> <dbl> <int> <int> <dbl> <chr>
1 1922 1 137 40 45 20 1_20
2 1286 1 141 41 45 21 1_21
3 1534 1 146 42 45 21 1_21
4 1406 1 158 43 45 22 1_22
5 2256 1 168 44 45 22 1_22
6 1741 1 187 45 45 23 1_23
7 213 2 148 25 30 13 2_13
8 167 2 159 26 30 13 2_13
9 272 2 169 27 30 14 2_14
10 123 2 186 28 30 14 2_14
11 20 2 214 29 30 15 2_15
12 178 2 275 30 30 15 2_15
13 1995 3 172 70 75 35 3_35
14 1303 3 179 71 75 36 3_36
15 2191 3 192 72 75 36 3_36
16 2032 3 201 73 75 37 3_37
17 2092 3 215 74 75 37 3_37
18 1517 3 263 75 75 38 3_38
Have to identify the unpaired groups– those groups/schools must be split into two “quasi-schools” to complete its zone:
odd_leftover_schools <- school_frame %>%
group_by(strata) %>%
filter(n_in_stratum %% 2 == 1, within_stratum_rank == n_in_stratum) %>%
pull(id)
After, split each leftover school’s students randomly in half; each half becomes a quasi-school for JK2 purposes:
df1$quasi_school <- df1$id
for (sch in odd_leftover_schools) {
ids <- which(df1$id == sch)
half <- sample(ids, size = floor(length(ids) / 2)) #randomly choose
df1$quasi_school[half] <- paste0(sch, "_a")
df1$quasi_school[setdiff(ids, half)] <- paste0(sch, "_b")
}
# Rebuild school_frame at the quasi-school level so split schools count as two separate pairing units
school_frame <- df1 %>%
distinct(quasi_school, id, strata, mos) %>%
arrange(strata, mos, quasi_school) %>%
group_by(strata) %>%
mutate(pair_id = ceiling(row_number() / 2)) %>%
ungroup() %>%
mutate(zone = paste(strata, pair_id, sep = "_")) %>%
group_by(zone) %>%
mutate(rep = row_number() - 1) %>% # 0 or 1 -- which half of the pair
ungroup()
# Attach zone + member back onto the student-level data
df1 <- df1 %>%
left_join(
school_frame %>% select(quasi_school, zone, rep),
by = "quasi_school"
)
n_zones <- length(unique(df1$zone))
cat("Number of jackknife zones:", n_zones)
Number of jackknife zones: 76
There are 76 zones. Within each zone, the schools are split (1 or 0; the jackknife replication code)
library(fhutils)
ctab(df1, zone, rep) %>% head()
zone/rep 0 1 Total
1_1 24 (49.0%) 25 (51.0%) 49 (100.0%)
1_10 25 (50.0%) 25 (50.0%) 50 (100.0%)
1_11 25 (50.0%) 25 (50.0%) 50 (100.0%)
1_12 25 (50.0%) 25 (50.0%) 50 (100.0%)
1_13 25 (50.0%) 25 (50.0%) 50 (100.0%)
1_14 25 (50.0%) 25 (50.0%) 50 (100.0%)
This is what is seen in TIMSS with the variables: JKZONE (e.g., 75 zones) and JKREP (i.e., 0 vs 1).
Sample TIMSS dataset:
library(BIFIEsurvey)
data(data.timss3)
head(data.timss3)
IDSTUD TOTWGT JKZONE JKREP female books lang migrant scsci likesc
1 400010201 17.46818 1 1 1 3 1 0 NA 2
2 400010203 17.46818 1 1 0 3 1 0 2 4
3 400010204 17.46818 1 1 1 5 1 0 2 2
4 400010205 17.46818 1 1 1 3 1 0 1 1
5 400010206 17.46818 1 1 1 3 1 0 2 1
6 400010207 17.46818 1 1 1 2 1 0 3 1
ASMMAT1 ASSSCI1 ASMMAT2 ASSSCI2 ASMMAT3 ASSSCI3 ASMMAT4 ASSSCI4 ASMMAT5
1 542.768 600.128 557.154 578.214 505.855 570.009 523.688 560.259 577.832
2 521.717 512.277 532.734 519.171 556.618 554.299 510.659 505.586 545.532
3 455.962 496.747 462.111 544.805 445.180 528.147 472.595 549.990 456.757
4 512.242 583.776 510.212 613.559 530.564 569.237 497.112 597.199 527.811
5 505.640 532.747 563.068 567.919 529.584 563.538 488.248 482.694 583.264
6 454.723 483.655 405.011 459.855 408.625 451.032 434.679 464.432 343.622
ASSSCI5
1 607.334
2 565.149
3 545.888
4 623.008
5 578.385
6 317.674
n_distinct(data.timss3$JKZONE)
[1] 75
n_distinct(data.timss3$JKREP)
[1] 2
We can use the survey package. There are two ways
There are 75 columns (or as many zones) of the original weights. The only difference, is that in each zone:
I have written a function (with help from AI!) that will make the replicate weights. You need to specify the zone, the rep/member, and the overall weight variable.
library(survey)
build_jk2_rw <- function(zone, rep, weight) {
zn <- unique(zone)
n_zones <- length(zn)
n <- length(weight)
m <- matrix(weight, nrow = n, ncol = n_zones)
colnames(m) <- paste0("rw", seq_len(n_zones))
for (h in seq_len(n_zones)) {
in_zone <- zone == zn[h]
m[in_zone & rep == 0, h] <- weight[in_zone & rep == 0] * 2
m[in_zone & rep == 1, h] <- 0
}
m
}
rw <- build_jk2_rw(df1$zone, df1$rep, df1$totwgt)
dim(rw)
[1] 3750 76
Once we have the matrix of replicate weights (rw), we
can use this in the survey package:
des <- svrepdesign(
data = df1,
repweights = rw, #specified here
weights = ~totwgt, #note the ~,
mse = TRUE,
type = 'JK2'
)
summary(des)
Call: svrepdesign.default(data = df1, repweights = rw, weights = ~totwgt,
mse = TRUE, type = "JK2")
JK2 jackknife with 76 replicates and MSE variances.
Variables:
[1] "id" "schwgt" "y" "w1" "x1"
[6] "minor" "one" "nwt" "totwgt" "stuwgt"
[11] "strata" "mos" "quasi_school" "zone" "rep"
After specifying the design, can use the svy functions
and instead of the dataframe, pass along the design of the survey.
svymean(~y + w1 + x1 + minor, des)
mean SE
y 0.424941 0.0672
w1 -0.022955 0.0852
x1 0.013857 0.0184
minor 0.190580 0.0087
This shows both the mean and the standard error.
NOTE: the standard error is computed as:
\[SE = \sqrt{\Sigma{(\hat{\theta}_j -
\bar{\theta})^2}}\] where \(\hat{\theta_j}\) is the statistic of
interest computed using \(j\) weights
from the \(j\) column. \(\bar{\theta}\) is the overall weighted mean
(using \(totwgt\)) (done when
mse = TRUE as is recommended in the TIMSS manual). If
mse = FALSE then the mean of the \(\hat{\theta_j}\) is used (results can be
slightly different).
To compute this manually (for w1):
js <- ncol(rw) #how many replicate weights
wbar <- weighted.mean(df1$w, df1$totwgt) #overall weight
mn <- numeric() #container for j means
for (i in 1:js){
mn[i] <- weighted.mean(df1$w, rw[,i])
}
hist(mn) #76 means
sqrt(sum((mn - wbar)^2)) #standard error formula
[1] 0.08518286
The standard error is the similar to the one that was done using the function.
survey package (acceptable but could be slightly
different). As the dataset specifies the ZONE and the REP variables, we
can use these:des2 <- svydesign(
data = df1,
weights = ~totwgt,
strata = ~zone, #JKZONE in TIMSS
id =~ rep, #JKREP in TIMSS
nest = TRUE #this is done as rep is 1 or 0; not unique
)
Once that is done, need to specify that as a jackknife replication:
des2a <- as.svrepdesign(
des2,
type = "JKn"
)
Note: this is specified as “JKn” (not JK2). Then can use the
svy functions as usual:
svymean(~y + w1 + x1 + minor, des2a)
mean SE
y 0.424941 0.0673
w1 -0.022955 0.0852
x1 0.013857 0.0185
minor 0.190580 0.0087
The results are similar (the same?) as in the previous example.
NOTE: that the naive standard error (i.e., \(\frac{SD}{\sqrt{n}}\)is too small
(especially for w1):
psych::describe(select(df1, y, w1, x1, minor), skew = F, ranges = F)
vars n mean sd se
y 1 3750 0.40 1.52 0.02
w1 2 3750 -0.04 0.94 0.02
x1 3 3750 0.01 1.01 0.02
minor 4 3750 0.27 0.44 0.01
Note that this is using unweighted data so the means will be different.
des3 <- svydesign(
data = df1,
id = ~id, #school id
weights = ~totwgt
)
There are slight differences– this is still better than the naive standard errors.
svymean(~y + w1 + x1 + minor, des3)
mean SE
y 0.424941 0.0658
w1 -0.022955 0.0795
x1 0.013857 0.0196
minor 0.190580 0.0102
There are many svy functions in the survey
package. If we want fit a linear regression, can use
svyglm:
m1 <- svyglm(y ~ w1 + x1 + minor, des)
m2 <- svyglm(y ~ w1 + x1 + minor, des2a)
m3 <- svyglm(y ~ w1 + x1 + minor, des3)
m4 <- estimatr::lm_robust(y ~ w1 + x1 + minor, data = df1, weight = totwgt) #this is naive OLS; incorrect
library(modelsummary)
modelsummary(list("RW" = m1, "RW2" = m2, "CRSE" = m3, "OLS" = m4), stars = TRUE)
| RW | RW2 | CRSE | OLS | |
|---|---|---|---|---|
| + p < 0.1, * p < 0.05, ** p < 0.01, *** p < 0.001 | ||||
| (Intercept) | 0.524*** | 0.524*** | 0.524*** | 0.524*** |
| (0.054) | (0.054) | (0.056) | (0.028) | |
| w1 | 0.533*** | 0.533*** | 0.533*** | 0.533*** |
| (0.061) | (0.062) | (0.063) | (0.026) | |
| x1 | 0.489*** | 0.489*** | 0.489*** | 0.489*** |
| (0.022) | (0.022) | (0.022) | (0.024) | |
| minor | -0.490*** | -0.490*** | -0.490*** | -0.490*** |
| (0.057) | (0.057) | (0.053) | (0.051) | |
| Num.Obs. | 3750 | 3750 | 3750 | 3750 |
| R2 | 0.234 | 0.234 | 0.234 | 0.234 |
| R2 Adj. | -38.887 | -38.341 | -18.670 | 0.233 |
| AIC | 12671.9 | 12672.4 | 12672.4 | 13206.4 |
| BIC | 110409.6 | 108895.8 | 71155.6 | 13237.5 |
| Log.Lik. | -55184.202 | -54427.325 | -35557.225 | |
| F | 232.274 | 228.540 | 215.093 | |
| RMSE | 1.31 | 1.31 | 1.31 | 1.31 |
Here is a list of svy functions:
| Category | Functions |
|---|---|
| Descriptive Statistics | svymean(), svytotal(),
svyvar(), svyquantile(), svyby(),
svyratio(), svycontrast() |
| Tabulations & Tests | svytable(), svychisq(),
svyttest(), svyranktest(),
svyhist(), svyboxplot() |
| Generalized Linear Models | svyglm(), svyolr(),
svycoxph(), svymle(), svykm(),
svysmooth() |
Practice this using the TIMSS dataset.
The PISA replication weights are constructed differently from the TIMSS weights. This is referred to as using Balanced Repeated Replications (BRR).
data("data.pisaNLD") #from BIFIEsurvey
df2 <- data.pisaNLD
dim(df2)
[1] 3992 405
#has a lot of variables; has 80 replicate weights
The replicate weights in PISA are given and can just be used. The
weights are computed differently from what TIMSS does (as shown). The
example below specifies the design using these weights. The names of all
the replicate weights have to be entered (using the function below using
dplyr but can also use the alternative which is using
regular expressions).
pisa_des <- svrepdesign(
data = df2,
weights = ~W_FSTUWT, #don't forget the ~
repweights = dplyr::select(df2, starts_with("W_FSTR")), #"W_FSTURWT[0-9]+",
type = "Fay", # PISA's method: Fay's modified BRR
rho = 0.5, # Fay's perturbation factor k
mse = TRUE # PISA default, use original mean
)
Of note: there is 1) type (dictated by PISA) and 2)
rho. There is an option combined.weights and
the default is fine (TRUE)– just means the the replicate
weights are already sampling weights (not just a perturbation
factor).
PISA doesn’t use the JK2 jackknife— it uses Fay’s Balanced Repeated Replication (BRR), where instead of dropping one PSU per replicate (i.e., the original weight x 0 as done in TIMSS), each replicate perturbs roughly half the sample up by a factor of 2−k and half down by a factor of k, with k = .5 (that is fixed or 1.5 vs 0.5).
The halfs are determined by a Hadamard matrix (no need to get into
that). In this case, no group ever gets a zero weight. This is also what
rho is– it is the perturbation factor k (and given in PISA
documentation). We don’t need to construct the weights- they are already
given (see the dataset; there are so many of them).
To get the mean of ESCS:
svymean(~ESCS, pisa_des, na.rm = T) #has missing values
mean SE
ESCS 0.097788 0.0234
To compute this manually (this helps my understanding of this; you won’t necessarily do this):
rp2 <- dplyr::select(df2, starts_with("W_FSTR")) #matrix of replicate weights
mns <- numeric()
for (x in 1:80){
mns[x] <- weighted.mean(df2$ESCS, rp2[,x], na.rm = T)
}
omean <- weighted.mean(df2$ESCS, df2$W_FSTUWT, na.rm = TRUE) #the original mean
sqrt((1/20) * sum((mns - omean)^2)) #standard error
[1] 0.02335901
The adjustment factor (1/20) is based on R (i.e., the number of replicates or 80 for PISA) and the correction factor k (rho or the Fay factor) and is \(\frac{1}{R(1-k)^2}\) or \(\frac{1}{80(1-.5)^2} = \frac{1}{20}\).
We can use the different svy functions since the design
is already given.
We can use the jtools package for this
(survey can do this but without p-values):
library(jtools)
svycor(~PV1MATH + PV2MATH + ESCS, design = pisa_des,
na.rm = TRUE, sig.stats = TRUE)
PV1MATH PV2MATH ESCS
PV1MATH 1 0.93* 0.43*
PV2MATH 0.93* 1 0.43*
ESCS 0.43* 0.43* 1
NOTE: the na.rm = TRUE is set since there are missing
values. If not, you will get an error. Using:
svyvar(~PV1MATH + PV2MATH + ESCS, design = pisa_des,
na.rm = T) |>
as.matrix() |>
cov2cor()
PVs are a form of multiply imputed values for each student. See notes. The estimate (b) is just the average of the estimate (that’s easy). The standard errors are combined using this formula based on Rubin’s rules (\(m\) is the number of plausible values; \(T_B\) is the variance of the estimate):
\[T_B = \overline{U}_b + \left(1 + \frac{1}{m}\right) Bb\]
\(\overline{U}_b\) is simply: \(\overline{U}_b = \frac{\Sigma SE_b^2}{m}\). It is the average of the squared standard errors (i.e., the variance)
Bb is \(B_b = \frac{\Sigma(b - \bar{b})^2}{m - 1}\) or just the variance of the estimate
If we were to do this manually with 5 PVs:
tmp <- svymean(~PV1MATH + PV2MATH + PV3MATH + PV4MATH + PV5MATH, pisa_des)
coef(tmp) #these are the means
PV1MATH PV2MATH PV3MATH PV4MATH PV5MATH
538.0611 537.7628 537.8091 537.2219 538.2615
SE(tmp) #these are the standard errors
[1] 3.190609 3.085825 3.074595 3.111499 3.038390
We can just take the average of the estimates to get the estimate. To get \(Bb\), we just take the variance.
est <- mean(coef(tmp)) #the mean of all the imputations
est #our statistics of interest
[1] 537.8233
Bb <- var(coef(tmp)) #the variance of the estimates
Ubar <- mean(SE(tmp)^2) #this is the SE-- don't forget to square it!
Putting this together:
T_b <- Ubar + (1 + 1/5) * Bb
The standard error is the square root of this value or \(\sqrt{T_b}\):
sqrt(T_b) #this is the correct pooled SE
[1] 3.130174
We won’t be doing this manualy. This is easy to specify once you have the design. The syntax just looks clunky!
library(mitools) #for combining the values properly
# Define the mapping of the analysis variable name to the dataset columns
pv_map <- list(math ~ PV1MATH + PV2MATH + PV3MATH + PV4MATH + PV5MATH)
# Run svy function across all plausible values
# VERY IMPORTANT::: only use one variable --> DO NOT DO ~math + ESCS
pv_models <- withPV(
mapping = pv_map,
data = pisa_des, #indicate the design
action = function(x) svymean(~math, design = x) #x is the design
)
# Pool the estimates and standard errors using Rubin's rules
pooled_res <- MIcombine(pv_models)
# Inspect pooled results
summary(pooled_res)
Multiple imputation results:
withPV.svyrep.design(mapping = pv_map, data = pisa_des, action = function(x) svymean(~math,
design = x))
MIcombine.default(pv_models)
results se (lower upper) missInfo
PV1MATH 537.8233 3.130174 531.6876 543.959 2 %
Can do this for a regression model (the syntax just looks strange but what’s important is to change the regression model):
# pv_reg <- withPV(
# mapping = pv_map,
# data = pisa_des, #indicate the design
# rewrite = F,
# action = quote(svyglm(math ~ ESCS + factor(LANG) +
# factor(ST03Q01), design = .DESIGN))
# )
# I like this way better
pv_reg <- withPV(
mapping = pv_map,
data = pisa_des, #indicate the design
action = \(x) svyglm(math ~ ESCS + factor(LANG) +
factor(ST03Q01), design = x)
)
res2 <- MIcombine(pv_reg)
summary(res2)
Multiple imputation results:
withPV.svyrep.design(mapping = pv_map, data = pisa_des, action = function(x) svyglm(math ~
ESCS + factor(LANG) + factor(ST03Q01), design = x))
MIcombine.default(pv_reg)
results se (lower upper) missInfo
(Intercept) 540.737455 3.134911 534.591710 546.88320 3 %
ESCS 42.589146 2.291321 38.094239 47.08405 6 %
factor(LANG)1 -47.885371 8.717825 -65.004928 -30.76581 8 %
factor(ST03Q01)2 4.815302 3.412780 -1.874784 11.50539 2 %
Manually running a regression and then combining (note this is just using cluster robust variance estimation, not using the replicate weights).
Need some data management. Create a list of imputed datasets, one dataset per plausible value.
library(tidyr)
sm <- select(df2, PV1MATH:PV5MATH, ESCS, LANG, ST03Q01, SCHOOLID, W_FSTUWT)
tall <- pivot_longer(sm, PV1MATH:PV5MATH, values_to = "MATH")
tall$pv <- readr::parse_number(tall$name)
datlist <- split(tall, tall$pv)
length(datlist)
[1] 5
datlist is now a list of the five datasets, one for each
plausible value.
library(estimatr)
allmods <- lapply(datlist, \(x)
lm_robust(MATH ~ ESCS + factor(LANG) + factor(ST03Q01), weights = W_FSTUWT, cluster = SCHOOLID, data = x, se_type = 'stata'))
pooled <- MIcombine(allmods)
summary(pooled)
Multiple imputation results:
MIcombine.default(allmods)
results se (lower upper) missInfo
(Intercept) 540.737455 4.779036 531.370312 550.10460 1 %
ESCS 42.589146 2.748725 37.199428 47.97886 4 %
factor(LANG)1 -47.885371 8.428535 -64.441448 -31.32929 9 %
factor(ST03Q01)2 4.815302 3.510376 -2.065975 11.69658 2 %
Results are similar. Can also use:
library(mitml)
testEstimates(allmods)
Call:
testEstimates(model = allmods)
Final parameter estimates and inferences obtained from 5 imputed data sets.
Estimate Std.Error t.value df P(>|t|) RIV FMI
(Intercept) 540.737 4.779 113.148 28050.506 0.000 0.012 0.012
ESCS 42.589 2.749 15.494 2815.845 0.000 0.039 0.038
factor(LANG)1 -47.885 8.429 -5.681 549.699 0.000 0.093 0.089
factor(ST03Q01)2 4.815 3.510 1.372 7816.348 0.170 0.023 0.023
Unadjusted hypothesis test as appropriate in larger samples.
— END (2026.09.03)