Spatial field variation is common in breeding trials. Mandala can fit simple row-column mixed models and then provide spatial diagnostic plots from the fitted model. The examples use a small augmented single-location trial.
library(mandala)
aug <- read.csv(
system.file("extdata", "augmented_single_n200.csv", package = "mandala"),
stringsAsFactors = FALSE,
check.names = FALSE
)
for (v in c("geno", "row", "col")) {
aug[[v]] <- factor(aug[[v]])
}
aug$yld <- as.numeric(aug$yld)This dataset has many unreplicated genotypes and a smaller set of replicated check genotypes. Row and column describe the field position of each plot.
data.frame(
plots = nrow(aug),
genotypes = length(unique(aug$geno)),
replicated_genotypes = sum(table(aug$geno) > 1),
unreplicated_genotypes = sum(table(aug$geno) == 1),
rows = length(unique(aug$row)),
columns = length(unique(aug$col))
)
#> plots genotypes replicated_genotypes unreplicated_genotypes rows columns
#> 1 200 160 20 140 14 15The first model estimates genotype effects without using field-position information. This gives a simple baseline for judging spatial adjustment.
fit_base <- mandala(
yld ~ 1,
~ geno,
data = aug,
verbose = FALSE
)
summary(fit_base)
#> Model statement:
#> mandala(fixed = yld ~ 1, random = ~geno, data = aug, verbose = FALSE)
#>
#> Variance Components:
#> component estimate std.error z.ratio bound %ch
#> geno 0.6686315 0.14727894 4.539899 P NA
#> R.sigma2 0.4571173 0.09963492 4.587923 P NA
#>
#> Fixed Effects (BLUEs) [first 5]:
#> effect estimate std.error z.ratio
#> (Intercept) 7.149171 0.0819997 87.18534
#>
#> Converged: TRUE | Iterations: 5
#>
#> Model Notes:
#> - Selected prediction SEs are available through mandala_predict().
#> - Selected fixed-effect tests are available with mandala_fixed_tests(type = 'selected').
#>
#> Random Effects (BLUPs) [first 5]:
#> random level estimate std.error z.ratio
#> geno G1 -0.0259458 0.3585428 -0.07236459
#> geno G10 -0.6446108 0.3585428 -1.79786301
#> geno G100 0.4141553 0.5233297 0.79138499
#> geno G101 -0.1874388 0.5233297 -0.35816584
#> geno G102 0.6048431 0.5233297 1.15575913
#>
#> logLik: -286.519 AIC: 577.037 BIC: 583.624 logLik_Trunc: -103.650The second model adds row and column effects. This is a simple row-column spatial adjustment: it does not assume a full AR1 residual model, but it is a useful first step for field trends that align with rows or columns.
fit_spatial <- mandala(
yld ~ 1,
~ geno + row + col,
data = aug,
verbose = FALSE
)
summary(fit_spatial)
#> Model statement:
#> mandala(fixed = yld ~ 1, random = ~geno + row + col, data = aug,
#> verbose = FALSE)
#>
#> Variance Components:
#> component estimate std.error z.ratio bound %ch
#> geno 0.42983930 0.06126367 7.016218 P NA
#> row 0.29599940 0.12308129 2.404910 P NA
#> col 0.30309231 0.12172672 2.489941 P NA
#> R.sigma2 0.06052303 0.01945609 3.110749 P NA
#>
#> Fixed Effects (BLUEs) [first 5]:
#> effect estimate std.error z.ratio
#> (Intercept) 7.186965 0.2108871 34.07968
#>
#> Converged: TRUE | Iterations: 11
#>
#> Model Notes:
#> - Selected prediction SEs are available through mandala_predict().
#> - Selected fixed-effect tests are available with mandala_fixed_tests(type = 'selected').
#>
#> Random Effects (BLUPs) [first 5]:
#> random level estimate std.error z.ratio
#> geno G1 -0.34350610 0.1730044 -1.9855338
#> geno G10 -0.69072325 0.1787749 -3.8636481
#> geno G100 0.42780962 0.2863221 1.4941552
#> geno G101 0.06766663 0.2869923 0.2357785
#> geno G102 0.37495894 0.2763080 1.3570326
#>
#> logLik: -222.107 AIC: 452.214 BIC: 465.387 logLik_Trunc: -39.238Mandala can also fit residual AR1 structures through
R_formula. A one-margin AR1 model is written by crossing an
identity margin with an AR1 margin. A separable two-dimensional residual
model is written as ar1(row):ar1(col).
fit_ar1_col <- mandala(
yld ~ 1,
~ geno,
data = aug,
R_formula = ~ id(row):ar1(col),
verbose = FALSE
)
fit_ar1_row <- mandala(
yld ~ 1,
~ geno,
data = aug,
R_formula = ~ ar1(row):id(col),
verbose = FALSE
)
fit_ar1_2d <- mandala(
yld ~ 1,
~ geno,
data = aug,
R_formula = ~ ar1(row):ar1(col),
verbose = FALSE
)
rbind(
ar1_col = fit_ar1_col$varcomp,
ar1_row = fit_ar1_row$varcomp,
ar1_2d = fit_ar1_2d$varcomp
)
#> component estimate std.error z.ratio bound %ch
#> ar1_col.1 geno 0.3678125 0.07474901 4.920634 P NA
#> ar1_col.2 R.sigma2 0.7903385 0.07467178 10.584166 P NA
#> ar1_col.3 R.rho.col 0.8550000 0.34497086 2.478470 P NA
#> ar1_row.1 geno 0.4952079 0.08177405 6.055807 P NA
#> ar1_row.2 R.sigma2 0.5176285 0.05140675 10.069272 P NA
#> ar1_row.3 R.rho.row 0.8575177 0.25926688 3.307471 P NA
#> ar1_2d.1 geno 0.4007287 0.06639072 6.035915 P NA
#> ar1_2d.2 R.sigma2 0.3149987 0.05321163 5.919735 P NA
#> ar1_2d.3 R.rho.row 0.4457830 0.06824734 6.531873 P NA
#> ar1_2d.4 R.rho.col 0.7079273 0.13492079 5.246984 P NAA two-dimensional spline can be fitted as a random spatial smooth.
The compact call pspline2D(row, col) creates internal
numeric coordinates and adds the spatial plane used by Mandala’s spline
model.
fit_spline <- mandala(
yld ~ 1,
~ geno + pspline2D(row, col),
data = aug,
verbose = FALSE
)
fit_spline$varcomp
#> component estimate std.error z.ratio bound %ch
#> 1 geno 0.445503646 0.06530119 6.82228944 P NA
#> 2 f(row.num) 0.357005638 0.41565919 0.85889027 P NA
#> 3 f(col.num) 0.224737496 0.27146797 0.82786007 P NA
#> 4 f(row.num):col.num 0.006806629 0.17435814 0.03903820 P NA
#> 5 row.num:f(col.num) 0.006806629 0.07019476 0.09696776 P NA
#> 6 f(row.num):f(col.num) 0.010268015 0.02003222 0.51257493 P NA
#> 7 row 0.006806629 0.01855800 0.36677592 P NA
#> 8 col 0.064312123 0.04266959 1.50721224 P NA
#> 9 R.sigma2 0.056571369 0.01992362 2.83941213 P NAFor models fitted to the same response and data, logLik,
AIC, and BIC provide a quick model comparison.
Lower AIC or BIC is preferred, while a higher
logLik indicates better likelihood fit.
fit_list <- list(
baseline = fit_base,
row_col = fit_spatial,
ar1_col = fit_ar1_col,
ar1_row = fit_ar1_row,
ar1_row_col = fit_ar1_2d,
pspline2D = fit_spline
)
model_compare <- data.frame(
model = c("baseline", "row_col", "ar1_col", "ar1_row", "ar1_row_col", "pspline2D"),
logLik = c(
fit_base$logLik,
fit_spatial$logLik,
fit_ar1_col$logLik,
fit_ar1_row$logLik,
fit_ar1_2d$logLik,
fit_spline$logLik
),
AIC = c(
fit_base$AIC,
fit_spatial$AIC,
fit_ar1_col$AIC,
fit_ar1_row$AIC,
fit_ar1_2d$AIC,
fit_spline$AIC
),
BIC = c(
fit_base$BIC,
fit_spatial$BIC,
fit_ar1_col$BIC,
fit_ar1_row$BIC,
fit_ar1_2d$BIC,
fit_spline$BIC
)
)
model_compare
#> model logLik AIC BIC
#> 1 baseline -286.5187 577.0375 583.6241
#> 2 row_col -222.1069 452.2138 465.3870
#> 3 ar1_col -240.7168 487.4337 497.3136
#> 4 ar1_row -244.3509 494.7018 504.5818
#> 5 ar1_row_col -230.0119 468.0237 481.1970
#> 6 pspline2D -212.7031 443.4061 472.9092
best_model <- model_compare$model[which.min(model_compare$AIC)]
best_fit <- fit_list[[best_model]]
best_model
#> [1] "pspline2D"In this augmented example, the row-column and spline models should improve the fit over the baseline model because the dataset contains a clear field trend. The best model is still a statistical choice: compare fit, parsimony, residual diagnostics, and whether the spatial terms make sense for the field layout.
This example has only an intercept fixed effect, so the fixed-effect test is mostly a workflow demonstration. In trials with treatments, environments, or covariates, the same function tests those fixed terms.
mandala_fixed_tests(best_fit, type = "selected", denDF = "residual")
#>
#> Fixed-effect term tests
#> -----------------------
#> Method: selected
#> Denominator df: residual
#>
#> Df denDF F.inc Wald Pr status
#> (Intercept) 1 196 820.8275192 820.8275192 5.396274e-72 ok
#> row.num 1 196 30.8677667 30.8677667 8.928691e-08 ok
#> col.num 1 196 12.7333661 12.7333661 4.516097e-04 ok
#> row.num:col.num 1 196 0.2266391 0.2266391 6.345574e-01 ok
#>
#> Note:
#> - Residual df were used for all fixed-effect terms.mandala_spatial_diagnostics() shows observed values,
fitted values, residuals, and a smoothed spatial surface. This is often
the fastest way to see whether spatial pattern remains after fitting the
model.
The empirical variogram summarizes how residual similarity changes with field distance. Strong structure in the variogram can indicate remaining spatial pattern.
vg_2d <- mandala_variogram(
best_fit,
data = aug,
response = "yld",
row = "row",
col = "col",
plot_type = "2d"
)The same empirical variogram can also be displayed as a 3-D surface.
vg_3d <- mandala_variogram(
best_fit,
data = aug,
response = "yld",
row = "row",
col = "col",
plot_type = "3d"
)Finally, predict adjusted genotype values from the spatial model.
If SpATS is installed, a comparable genotype-random spatial spline can be fitted and compared with Mandala’s spline model through fitted values.
if (requireNamespace("SpATS", quietly = TRUE)) {
aug$row_num <- as.numeric(as.character(aug$row))
aug$col_num <- as.numeric(as.character(aug$col))
fit_spats <- SpATS::SpATS(
response = "yld",
genotype = "geno",
genotype.as.random = TRUE,
spatial = ~ SpATS::SAP(row_num, col_num, nseg = c(8, 8), degree = 3, pord = 2),
data = aug,
control = SpATS::controlSpATS(tolerance = 1e-03, monitoring = 0)
)
fitted_spats <- as.numeric(fit_spats$fitted)
fitted_mandala <- as.numeric(fit_spline$fitted)
comp_df <- data.frame(
yld = aug$yld,
fitted_spats = fitted_spats,
fitted_mandala = fitted_mandala
)
r_fit <- cor(comp_df$fitted_spats, comp_df$fitted_mandala)
plot(
comp_df$fitted_spats, comp_df$fitted_mandala,
xlab = "SpATS fitted values",
ylab = "Mandala fitted values",
main = "SpATS vs Mandala fitted values"
)
abline(0, 1, col = "red", lty = 2)
legend(
"topleft",
legend = sprintf("r = %.4f", r_fit),
bty = "n"
)
data.frame(fitted_correlation = r_fit)
} else {
data.frame(note = "Install SpATS to run this comparison.")
}#> fitted_correlation
#> 1 0.9981549
Spatial adjustment is a central component of modern field-trial analysis because fertility gradients, management zones, and local field trends can inflate residual variation or bias genotype comparisons. Row-column random effects, separable AR1 residuals, empirical variograms, and two-dimensional spline smooths are common tools for diagnosing and modeling these trends.