Note: for the sake of readability, this question was heavily shortened. Parts based on a humane mistake were removed, and the focus was put again on the biased mixed models.
Please correct me if I have understood or made something wrong. The question is, why the mixed models of the lme4
and nlme
packages of R
do seem to bias towards small deviating clusters, opposing the objective of partial pooling. Below is the longer explanation.
In various sources of the mixed-model theory, it is said that the purpose of the partial pooling is to avoid giving too much weight for small unreliable clusters when the models are fitted. For example, in the page 254 of "Data analysis using regression and multilevel/hierarchial models" book (Gelman and Hill, 2006) it is clearly stated that:
"Averages from counties [the clusters in the example] with smaller sample sizes carry less information, and the weighting pulls the multilevel estimates closer to the overall state [all data] average..." "Averages from counties with larger sample sizes carry more information, and the corresponding multilevel estimates are close to the county averages..."
On the page 253, there is the formula 12.1 that defines the strength of the pull of clusters in partial pooling. The individual cluster sizes are in directly proportional relationship with the weight given for the clusters' own inherent averages. Thus, the smaller the cluster size is the stronger the pull towards the completely-pooled overall average estimate should be. The book also refers to the use of lme4::lmer()
, and at least I didn't spot any mentioned expectations regarding the objective. The same principles with similar formula are also lectured in the video below:
https://youtu.be/cs-t0Xitr8k?feature=shared
Even though the partial pooling of the clusters would seem to be correct, the fitted mixed models can sometimes be oddly biased towards the smaller clusters. I have run linear completely-pooling and partially-pooling mixed-model fits many times with simulated data, and it seems that the lme4::lmer()
models can give too much weight for the small clusters, which is totally opposite of the objective. To exclude possible installation issues, I have observed this behavior repeatedly with different installations and versions of R
on two different computers. In this discussion, @GeorgeSavva kindly helped me to find the partial-pooled cluster means using the predict(mmodel)
(though, if I have time, I would still like to calculate the same manually, to gain deeper understanding by seeing the elemental steps). Using the code below, we can see that the clusters move only slightly, and the smallest clusters are affected the most as expected regarding the theory. However, still in these picked-up scenarios the mixed models are oddly strongly biased toward the small clusters. I need to further digest the responses in this discussion, do they provide justified explanation for these anomalies that seem to counteract the purpose. If the clusters move so little, why the mixed models seem to bias disproportionately much.
library(lme4)
library(ggplot2)
set.seed(824523)
#Create different-sized clusters
clustersizes <- c(2,10,10,10,2,10,10,10)
clusters <- c(1:length(clustersizes))
#Generate 1-level measurements
nmeas <- sum(clustersizes)
data <- rnorm(nmeas, 0, 1)
#Generate predictor
predictor <- c(rep("control", sum(clustersizes[1:(max(clusters)/2)])),
rep("intervention", sum(clustersizes[((max(clusters)/2) + 1):max(clusters)])))
#Generate and add random effects
randomeffects <- rnorm(max(clusters), 0, 3)
data <- data + rep(randomeffects, times = clustersizes)
#Combine the data into dataframe
df <- data.frame(dependent = data,
predictor = predictor,
cluster = rep(clusters, times = clustersizes)
)
#Add fixed effect
df[(predictor == "intervention"),]$dependent <- df[(predictor == "intervention"),]$dependent + 5
#Inspect data if necessary
boxplot(dependent ~ cluster, df)
#Make linear complete-pool second-level-ignorant model
linear <- lm(dependent ~ predictor, df)
cpoolinterc <- linear[["coefficients"]][["(Intercept)"]]
cpoolslope <- linear[["coefficients"]][["predictorintervention"]]
#Make partial-pooling mixed model
mmodel <- lmer(dependent ~ predictor + (1|cluster), data = df)
ppoolinterc <- fixef(mmodel)[["(Intercept)"]]
ppoolslope <- fixef(mmodel)[["predictorintervention"]]
#Visualize the usual and mixed-model lines with cluster means before and after the partial pooling
df$ppclustermean <- predict(mmodel)
plotmodelmeans <- ggplot() +
geom_abline(intercept = cpoolinterc - cpoolslope, slope = cpoolslope, size = 2, linetype = "solid") +
geom_abline(intercept = ppoolinterc - ppoolslope, slope = ppoolslope, size = 2, linetype = "dashed") +
#Slopes are removed from the intercepts due to 0 -> 1 x remapping of the dotplot caused by the factor()
coord_cartesian(ylim = c(-15, 15)) +
theme_void() +
stat_summary(data = df, aes(x = factor(predictor), y = dependent, group = factor(cluster)), fun = mean, geom = "crossbar", width = 0.7) +
stat_summary(data = df, aes(x = factor(predictor), y = ppclustermean, group = factor(cluster)), fun = mean, geom = "crossbar", width = 0.7, colour = "red")
plotmodelmeans
The solid line is the lm()
model, the dashed line represents the lmer()
mixed model. On the left and right sides there are the control and intervention groups, respectively. Individual measurements are not shown, only the pre-partial-pooling (black) and post-partial-pooling (red) cluster means as horizontal lines. The two smallest clusters happened to be also the lowest ones, towards which the mixed model is biasing to.
And here is the figure with the following modifications:
set.seed(1324523)
#Create different-sized clusters
clustersizes <- c(2,10,10,10,2,10,10,100)
This time, the smallest intervention cluster was the highest one. Notice that despite the lower huge cluster, the mixed model is turning absurdly strong towards the little cluster.
Hopefully I haven't done something stupid. Here is my attempt to draw all the data points after the partial pooling, I got the random effects using the predict()
function. Sorry about the big chunks of code, I want to favor complete pieces to avoid accidents if someone copies and tries these.
library(lme4)
library(ggplot2)
set.seed(1324523)
#Create different-sized clusters
clustersizes <- c(2,10,10,10,2,10,10,10)
clusters <- c(1:length(clustersizes))
#Generate 1-level measurements
nmeas <- sum(clustersizes)
data <- rnorm(nmeas, 0, 1)
#Generate predictor
predictor <- c(rep("control", sum(clustersizes[1:(max(clusters)/2)])),
rep("intervention", sum(clustersizes[((max(clusters)/2) + 1):max(clusters)])))
#Generate and add random effects
randomeffects <- rnorm(max(clusters), 0, 1)
data <- data + rep(randomeffects, times = clustersizes)
#Combine the data into dataframe
df <- data.frame(dependent = data,
predictor = predictor,
cluster = rep(clusters, times = clustersizes)
)
#Add fixed effect
df[(predictor == "intervention"),]$dependent <- df[(predictor == "intervention"),]$dependent + 1
#Inspect data if necessary
boxplot(dependent ~ cluster, df)
#Make linear complete-pool second-level-ignorant model
linear <- lm(dependent ~ predictor, df)
cpoolinterc <- linear[["coefficients"]][["(Intercept)"]]
cpoolslope <- linear[["coefficients"]][["predictorintervention"]]
#Make partial-pooling mixed model
mmodel <- lmer(dependent ~ predictor + (1|cluster), data = df)
ppoolinterc <- fixef(mmodel)[["(Intercept)"]]
ppoolslope <- fixef(mmodel)[["predictorintervention"]]
#Reposition the clusters according to the partial pooling and fit lm() again
df$ppclustermean <- predict(mmodel)
df$ppdata <- df$dependent
for (i in 1:length(clusters)) {
clustermean <- mean(df[(df$cluster == i),]$dependent)
ppclustermean <- df[(df$cluster == i),]$ppclustermean[1] #All the values are the same, pick up the first one
df[(df$cluster == i),]$ppdata <- df[(df$cluster == i),]$ppdata - clustermean + ppclustermean
}
linear <- lm(ppdata ~ predictor, df)
ppdatainterc <- linear[["coefficients"]][["(Intercept)"]]
ppdataslope <- linear[["coefficients"]][["predictorintervention"]]
#Visualize the usual and mixed-model lines with cluster means before and after the partial pooling
plotmodelmeans <- ggplot() +
geom_abline(intercept = cpoolinterc - cpoolslope, slope = cpoolslope, size = 2, linetype = "solid") +
geom_abline(intercept = ppoolinterc - ppoolslope, slope = ppoolslope, size = 2, linetype = "dashed") +
geom_abline(intercept = ppdatainterc - ppdataslope, slope = ppdataslope, size = 2, linetype = "dashed", colour = "red") +
#Slopes are removed from the intercepts due to 0 -> 1 x remapping of the dotplot caused by the factor()
coord_cartesian(ylim = c(-4, 4)) +
theme_void() +
geom_dotplot(data = df, aes(x = factor(predictor), y = dependent, fill = factor(cluster)), binaxis = "y", stackdir = "center", binwidth = 0.3, alpha = 0.5) +
geom_dotplot(data = df, aes(x = factor(predictor), y = ppdata, fill = factor(cluster)), binaxis = "y", stackdir = "center", binwidth = 0.3, alpha = 1.0) +
scale_fill_brewer(palette="Set1") +
stat_summary(data = df, aes(x = factor(predictor), y = dependent, group = factor(cluster)), fun = mean, geom = "crossbar", width = 0.7) +
stat_summary(data = df, aes(x = factor(predictor), y = ppclustermean, group = factor(cluster)), fun = mean, geom = "crossbar", width = 0.7, colour = "red")
plotmodelmeans
The above code produces the image below. For this data I decreased the ICC to get stronger partial pooling. Again, the solid black line is the lm()
model fitted on the initial simulated data (transparent dots), the dashed black line is the lmer()
mixed model. The new dashed red line is another lm()
model fitted on the (hopefully properly) partially-pooled data (opaque dots). The black horizontal lines are the cluster means before the partial pooling, the red ones are after. Notice that despite the prominent movements of the clusters, the both lm()
models align almost perfectly. And once again, the mixed model is going after the smaller clusters 1 and 5, which happened to be negatively biased.
I have made similar observations also with the nlme
package.
predict.merMod()
withre.form=NULL
$\endgroup$fixef(mmodel)
, have the tendency of turning more towards the small clusters than the second-level ignorant model? Even if I have made something wrong regarding other stuff, this contradiction with the very motives of the mixed modeling baffles me. $\endgroup$predict()
function, the explanation in the documentation is slim. At least the two first values given bypredict(mmodel, re.form=NULL)
do not correspond to my pre and post partially-pooled averages of the cluster 1. $\endgroup$