Most empirical models of biogeography, species distributions, photosynthesis, nutrient uptake, yield, trophic dynamics, crop management and/or small area dietary requirements assume no interactions between the essential mineral nutrient elements and typically describe the associated processes as a function of only the potentially most limiting parts (i.e., Liebig’s law). However, the law of the minimum does not hold consistently at plant community or ecosystem scales (see e.g., Danger et al., 2008). In agricultural systems, overall mineral nutrient compositions are likely to affect both crop yields e.g., see the recent article by Aliyu et al., (2021), as well as the overall nutritional quality of the plant products that are subsequently consumed by animals, including humans.
The objective of this notebook is to provide starter code in R for exploratory data analyses (EDA) and reporting of the mineral nutrient compositions of the main cereal grains (maize, pearl-millet, rice and sorghum) that are grown in Malawi. The markdown notebook presented here is maintained on Github, and you can fork and alter it from there for your reference and as you see fit.
Compositional Data Analysis (CoDA) refers to the analyses of compositional data (CoDa). CoDa are defined as vectors with strictly positive components whose sum is constant (e.g, 1 or 100%). The terms also cover all other parts of a whole (so-called subcompositions), which carry only relative information (van den Boogaart and Tolosana-Delgado, 2013). Because a change in any proportion of a whole changes at least one other proportion, CoDa are interdependent. Anything that is part of a whole is compositional. So arguably, all ionomic data should be treated as CoDa because as a whole the concentrations must sum to the respective unit totals of the constituent parts i.e., in the case of plants and animals as compositions of water, organic and inorganic materials. In turn, the organic, and in this case specifically the inorganic nutrient subcompositions considered here, should also not be analyzed and interpreted without normalizing them to one another for downstream analyses and diagnostics (see e.g., Parent et al., 2013).
The original approach to CoDA developed by J. Aitchison (1982) uses logarithmic ratios (LRs) of the compositional parts as the fundamental starting point for CoDa description and modeling. There is also a recent CoDa review paper by M. Greenacre (2021) openly available, which provides excellent guidance, as well as R scripts, about the practical applications and interpretation of CoDa. There is also an example, open source article available that treats genomic survey data in an analogous fashion and uses some of the graphical modeling techniques that we will be illustrating here (see Friedman and Alm, (2012), and the references therein).
The data we will be using were generated by the GeoNutrition project in Malawi. They consist of 1,809 cereal grain samples that were collected for maize (1,606), millet (31), rice (54), and sorghum (117) in proportion of their national geographical occurrence. The nationally representative cropland sampling frame and the laboratory methods that were used in the data collections are described in Gashu et al., (2021).
The data include ICP-MS concentration measurements of the respective cereal mineral nutrient contents (in mg kg-1), including: Na, Mg, P, S, K, Ca, Cr, Mn, Fe, Co, Cu, Zn, Se, Mo, and I. They also include the georeference of where the samples were collected (lon/lat, EPSG:3857), the cereal crop at that location, the source of the grain samples (crop, field stack or store), if or not, fertilizer and/or lime were used in growing the crop, and basic soil properties (pH, soil organic matter and cation exchange capacity). Note that any crop nutrient measurements below their respective limits of detection (>LOD) have been set to 2/3 of the respective minimum observed values. Other imputation methods are also possible in this context see Greenacre (2021).
To actually run this notebook, you will need to load the packages indicated in the chunk directly below to download and assemble the data. The chunk (not shown but in the markdown document) also generates an overview map of where the samples were collected in Malawi … you can click and zoom into the individual sampling locations.
# Package names
packages <- c("osfr", "dplyr", "leaflet", "solitude", "compositions", "doParallel", "caret", "caretEnsemble", "MASS", "randomForest", "xgboost", "nnet", "klaR", "stargazer", "archetypes", "arm")
# Install packages
installed_packages <- packages %in% rownames(installed.packages())
if (any(installed_packages == FALSE)) {
install.packages(packages[!installed_packages])
}
# Load packages
invisible(lapply(packages, library, character.only = TRUE))
There will be additional packages that will be need to be installed to run the graphical models presented in a later section of this notebook, which require more customized procedures for installation depending on the R version that you are running. We’ll deal with those in the relevant sections below. This next chunk downloads the Malawi data.
# Create a data folder in your current working directory
dir.create("MW_coda", showWarnings=F)
setwd("./MW_coda")
dir.create("Results")
# Crop data download from OSF at: https://osf.io/btxyc/
osf_retrieve_file("btxyc") %>% osf_download(conflicts = "overwrite")
codat <- read.table("MW_grain_CoDa.csv", header=T, sep=",")
# Soil data download from OSF at: https://osf.io/5tgu3/
osf_retrieve_file("5tgu3") %>% osf_download(conflicts = "overwrite")
soils <- read.table("soil_props.csv", header=T, sep=",")
# Merge crop and soil data
codat <- merge(codat, soils, by = "CropSampleID")
# Download figures at: at: https://osf.io/pnw7g/
osf_retrieve_file("pnw7g") %>% osf_download(conflicts = "overwrite")
This section of the notebook sets up a CoDa data frame that includes 2 amalgamations: mineral macro-nutrients (i.e., Na, Mg, P, S, K, and Ca … in atomic number order), and micro-nutrients (i.e., Cr, Mn, Fe, Co, Cu, Zn, Se, Mo, and I). Note that the distinction between macro and micro here is only intended to reflect the recommended daily dietary values (DDV) that should be consumed by people, and not their overall nutritional importance and/or benefits.
This next chunk closes the CoDa, and calculates centered logratios (CLR). These can then be analyzed, summarized and interpreted with conventional univariate, multivariate, and data modeling methods (Greenacre, 2021). Note that you could also use other logratios such as the ALR, or the ILR and/or different amalgamations of subcompositions, but we shall leave those possibilities for you to explore. We also write out the resulting data frame to your ./MW_coda/Results
directory should you wish to process it in software other than R.
# Calculate the CLRs
varc <- c("Na", "Mg", "P", "S", "K", "Ca", "Cr", "Mn", "Fe", "Co", "Cu", "Zn", "Se", "Mo", "I")
combo <- codat[varc]
combo <- combo / rowSums(combo) ## close the composition
combo <- as.data.frame(clr(combo)) ## centered log ratio (clr) transform
# Define the (macro / micro) amalgamation and add it to the overall nutrient composition
varm <- c("Na", "Mg", "P", "S", "K", "Ca")
macro <- rowSums(combo[varm])
combo <- cbind(combo, macro)
# Attach site data info
locv <- c("CropSampleID", "SoilSampleID", "lat", "lon", "crop", "source", "fert", "pH", "soc", "eCEC")
locs <- codat[locv]
site <- cbind(locs, combo)
# Write file
write.csv(site, "./MW_coda/Results/MW_site_coda.csv", row.names = F)
You can plot the raw CLR differences between crop species with something like in this next ugly chunk …
par(mfrow = c(5,3), pty="s", mar=c(2,4,1,1))
boxplot(site$Na ~ site$crop, notch = T, ylab = "Na", xlab = "", cex.axis = 1, cex.lab = 1.3)
boxplot(site$Mg ~ site$crop, notch = T, ylab = "Mg", xlab = "", cex.axis = 1, cex.lab = 1.3)
boxplot(site$P ~ site$crop, notch = T, ylab = "P", xlab = "", cex.axis = 1, cex.lab = 1.3)
boxplot(site$S ~ site$crop, notch = T, ylab = "S", xlab = "", cex.axis = 1, cex.lab = 1.3)
boxplot(site$K ~ site$crop, notch = T, ylab = "K", xlab = "", cex.axis = 1, cex.lab = 1.3)
boxplot(site$Ca ~ site$crop, notch = T, ylab = "Ca", xlab = "", cex.axis = 1, cex.lab = 1.3)
boxplot(site$Cr ~ site$crop, notch = T, ylab = "Cr", xlab = "", cex.axis = 1, cex.lab = 1.3)
boxplot(site$Mn ~ site$crop, notch = T, ylab = "Mn", xlab = "", cex.axis = 1, cex.lab = 1.3)
boxplot(site$Fe ~ site$crop, notch = T, ylab = "Fe", xlab = "", cex.axis = 1, cex.lab = 1.3)
boxplot(site$Co ~ site$crop, notch = T, ylab = "Co", xlab = "", cex.axis = 1, cex.lab = 1.3)
boxplot(site$Cu ~ site$crop, notch = T, ylab = "Cu", xlab = "", cex.axis = 1, cex.lab = 1.3)
boxplot(site$Zn ~ site$crop, notch = T, ylab = "Zn", xlab = "", cex.axis = 1, cex.lab = 1.3)
boxplot(site$Se ~ site$crop, notch = T, ylab = "Se", xlab = "", cex.axis = 1, cex.lab = 1.3)
boxplot(site$Mo ~ site$crop, notch = T, ylab = "Mo", xlab = "", cex.axis = 1, cex.lab = 1.3)
boxplot(site$I ~ site$crop, notch = T, ylab = "I", xlab = "", cex.axis = 1, cex.lab = 1.3)
Note that there are quite a few outliers apparent in the boxplot figure above. This short section demonstrates an unspervised anomaly and outlier detection method for CoDa (and other multivariate data, see Liu, Ting and Zhou (2008). We will be using the isolationForest
function from the solitude
package here, with default tuning parameters. We also plan to expand on anomaly and outlier detection more in subsequent notebooks, so this next chunk should be considered as just an initial draft.
# Select CoDa variables
coda <- site[ ,11:25]
# Fit isolationForest
iso <- isolationForest$new()
iso$fit(coda)
score <- coda %>% iso$predict()
# Tag outliers based on quantile threshold
thresh <- as.numeric(quantile(score$anomaly_score, probs = 0.95)) ## you can change the quantile level here
coda$outlier <- as.factor(ifelse(score$anomaly_score >= thresh, "y", "n"))
site <- cbind(site, coda$outlier)
names(site)[names(site)=="coda$outlier"] <- "outlier"
At the specified 95% quantile threshold of the modeled anomaly_score
this identifies 91 potential CoDa sample outliers that we will use in some of the subsequent EDAs in this notebook e.g., see the provenance sample tagging section further below.
Archetypal analysis (Cutler and Breiman, 1994) is an unsupervised learning method similar to e.g., K-means clustering. It describes each observation in a dataset as a mixture of archetypes. Rather than describing average or typical observations and measurements, archetypal analysis seeks to identify extreme points on the convex hull of a data matrix, which can provide a simple and quite useful way of clustering and looking at multivariate data (Eugster and Leisch, 2009). The identified archetypes themselves are mixtures of the individual CoDA variables under consideration. Archetypal mixture coefficients and projections are also CoDa, which is why we include them in this notebook. Note that other clustering and ordination algorithms could also be used in this context (Greenacre, 2021).
This next chunk fits the Malawi CLR data using the archetypes
package, which is a robust implementation of the original Cutler and Breiman (1994) algorithm that uses k-fold validation.
# Fit nutrient archetypes with 10-fold cross validation
set.seed(85321)
comp.arc <- stepArchetypes(data=site[,11:25], k = 1:10, nrep = 10, verbose = FALSE)
Similar to other clustering and ordination algorithms, we can use the identified archetypes for dimensional reduction and unsupervised classification.
# Select no. of archetypes, the scree plot above suggests 3-7 ... we use 4 here
comp.arc4 <- bestModel(comp.arc[[4]])
# Classify CLRs by "dominant archetypes" (DA)
darc <- as.data.frame(predict(comp.arc4, site[,11:25]))
darc$DA <- apply(darc, 1, which.max)
colnames(darc) <- c("A1","A2","A3","A4","DA")
site <- cbind(site, darc)
# Overwrite the site file
write.csv(site, "./MW_coda/Results/MW_site_coda.csv", row.names = F)
As before, you can plot the raw CLR differences between dominant archetypes as: (code not shown, but included in the markdown).
This is an aside, but it gives you an idea of what the “most” dominant archetypal sites look like from a GeoSurvey perspective in Malawi. This should remind us of the fact that there are probably quite explicit spatial links to assessing nutrient compositions that should be explored. However, those links are certainly not plainly visible in satellite images, apart from the co-occurrence of croplands, buildings/settlements and their potential anthropic effects apparent in these examples.
However, we do expect that some form dimensional CoDa reduction technique will help with some of the subsequent spectral and spatial modeling goals for assessing or monitoring national grain nutrient compositions, and we will follow-up those propositions with additional notebooks.
The main reason for including this section in notebook is that where and from which sources the grain samples were obtained i.e., their “provenance” may be important. Provenance could make a difference as to how those respective data should be interpreted for diagnostics and monitoring. In this survey dataset the grain samples were collected either directly from the standing crop, from harvested crop stacks located in the respective fields, or from household grain stores, on both fertilized and unfertilized fields. Both household grain store and fertilized fields are problematic in this context in that we don’t actually know what happened after samples were removed from the field, mixed and stored from potentially different farm fields. We also cannot assess what the actual effect of the types and amounts of fertilizer and/or lime that were used were because explicit georeference and sample age links to the field data are missing. The combination of these factors could influence reported survey results e.g., via background contamination and noise of the obtained grain survey samples, and any other overall sample survey artifacts or treatment differences.
The next chunks provides an example of sample provenance tagging and tracking. We differentiate reference samples that were obtained directly from the linked grain and soil observations (crop or stack) on unfertilized fields, which are not CoDa outliers from all other survey samples (i.e., those stored and/or from fertilized samples and/or outliers). Note that you can easily change and/or expand the rule(s) for this. However you define “reference samples”, these should be your highest-value targets in future surveys because they define a consistent field baseline for monitoring any diagnostic changes. Also note that these landmark targets are hard to collect in the field because they occur only, and have to be sampled, during a quite narrow time window during any given cropping season. This next chunk tags the reference samples and produces a sketch map of where those samples were located in the currently available survey.
# Tag reference samples with <dplyr>
site <- site %>% mutate(ref = case_when(source == 'crop' | source == 'stack' & outlier == 'n'
& fert == 'n' ~ 'y', TRUE ~ 'n'))
# Reference sample/site file write out
write.csv(site, "./MW_coda/Results/MW_ref_coda.csv", row.names = F)
# Reference sample location map
rloc <- site[ which(site$ref == 'y'), ]
r <- leaflet() %>%
setView(lng = mean(rloc$lon), lat = mean(rloc$lat), zoom = 7) %>%
addProviderTiles(providers$OpenStreetMap.Mapnik) %>%
addCircleMarkers(rloc$lon, rloc$lat, clusterOptions = markerClusterOptions())
r ## plot widget
You could track and monitor your reference samples with the following short, exploratory machine learning workflow. We initially split the data into calibration and validation sets and select the relevant features for distinguishing between ref = y
or ref = n
samples. This could also be readily updated with new data as (i.e., from new locations and/or from repeat samples), become available over time.
# Set calibration/validation set randomization seed
seed <- 123581
set.seed(seed)
# Split data into calibration and validation sets
gsIndex <- createDataPartition(site$ref, p = 4/5, list = F, times = 1)
gs_cal <- site[ gsIndex,]
gs_val <- site[-gsIndex,]
# Specify sample calibration labels
labs <- c("ref")
lcal <- as.vector(t(gs_cal[labs]))
# Specify calibration features
fcal <- gs_cal[ ,c(3:4,11:25)] ## selects lat/lon & CoDa features
The following caretEnsemble
chunk then generates the reference sample / site predictions on the validation set with default-tuning of the relevant model hyperparameters. Note that if you would like more control over the individual model fits, you can also use their respective tuneList
arguments.
# Start doParallel to parallelize model fitting
seed <- 123581
set.seed(seed)
mc <- makeCluster(detectCores())
registerDoParallel(mc)
# Specify model training controls
tc <- trainControl(method = "cv", number = 10, classProbs = T,
summaryFunction = twoClassSummary, allowParallel = TRUE,
savePredictions = "final")
# Fit 5 calibration models using all of the CoDa features
clist <- caretList(fcal, lcal,
trControl = tc,
tuneList = NULL,
methodList = c("glmStepAIC", "rf", "xgbTree", "nnet", "nb"),
metric = "ROC")
gs_val$gl <- predict(clist$glmStepAIC, gs_val, type = 'prob')$y
gs_val$rf <- predict(clist$rf, gs_val, type = 'prob')$y
gs_val$xt <- predict(clist$xgbTree, gs_val, type = 'prob')$y
gs_val$nn <- predict(clist$nnet, gs_val, type = 'prob')$y
gs_val$nb <- predict(clist$nb, gs_val, type = 'prob')$y
stopCluster(mc)
fname <- paste("./MW_coda/Results/", labs, "_clist.rds", sep = "")
saveRDS(clist, fname)
The next chunk then stacks the 5 previous models, the so-called base learners, on the validation set. We will use glmStepAIC
function for model selection and weighting, but you may want to try other top learners in this context as well.
# Set validation labels and features
lval <- as.vector(t(gs_val[labs])) ## subset validation labels
fval <- gs_val[ ,34:38] ## subset validation features
# Start doParallel to parallelize model fitting
mc <- makeCluster(detectCores())
registerDoParallel(mc)
# Control setup
set.seed(1385321)
tc <- trainControl(method = "cv", number = 10, classProbs = T,
summaryFunction = twoClassSummary, allowParallel = TRUE,
savePredictions = "final")
# Model training
st <- train(fval, lval,
method = "glmStepAIC",
family = "binomial",
metric = "ROC",
trControl = tc)
# Model outputs & predictions
gs_val$st <- predict(st, gs_val, type = 'prob')$y ## stacked predictions on the validation set
stopCluster(mc)
fname <- paste("./MW_coda/Results/", labs, "_st.rds", sep = "")
saveRDS(st, fname)
The next chunks generate the stacked predictions (st
) on the full site
dataset and compares those to the currently available, field-based provenance tags.
# Generate predictions on overall dataset
site$gl <- predict(clist$glmStepAIC, site, type = 'prob')$y
site$rf <- predict(clist$rf, site, type = 'prob')$y
site$xt <- predict(clist$xgbTree, site, type = 'prob')$y
site$nn <- predict(clist$nnet, site, type = 'prob')$y
site$nb <- predict(clist$nb, site, type = 'prob')$y
site$st <- predict(st, site)
# Confusion matrix
confusionMatrix(site$st, as.factor(site$ref))
## Confusion Matrix and Statistics
##
## Reference
## Prediction n y
## n 790 51
## y 66 902
##
## Accuracy : 0.9353
## 95% CI : (0.923, 0.9462)
## No Information Rate : 0.5268
## P-Value [Acc > NIR] : <2e-16
##
## Kappa : 0.8702
##
## Mcnemar's Test P-Value : 0.1956
##
## Sensitivity : 0.9229
## Specificity : 0.9465
## Pos Pred Value : 0.9394
## Neg Pred Value : 0.9318
## Prevalence : 0.4732
## Detection Rate : 0.4367
## Detection Prevalence : 0.4649
## Balanced Accuracy : 0.9347
##
## 'Positive' Class : n
##
The fact that we can readily differentiate ref = y
from ref = n
samples with high sensitivity and specificity in this survey provides a good indication that we should probably be dealing with at least two CoDa measurement sub-populations based on their provenance tags (i.e., field vs store from either fertilized or unfertilized fields). Given the sample geographical locations and their associated CoDa, the samples are clearly compositionally and/or geographically separable. Note that these differences were not considered in the Gashu et al., (2021) article, and may have distorted the Malawi results presented therein without regard to any of the underlying survey artifacts.
This section provides initial univariate data model summaries of parts of the CLR transformed data by crop species. We focus on the contrast of the macro / micro nutrient subcompositions between crops. This can be readily extended to other compositional amalgamations. These next chunks fit the differences between the individual macro nutrient CLRs of millet, rice and sorghum as compared to maize, post-stratified for sample provenance (ref
) and dominant archetype (DA
). We will be doing this using mixed-effects (random intercept) regression with the arm
package.
# Fit nutrients by crop species with mixed-effects regression
m0 <- lmer(macro~crop + (1|ref) + (1|DA), data = site)
m1 <- lmer(Na~crop + (1|ref) + (1|DA), data = site)
m2 <- lmer(Mg~crop + (1|ref) + (1|DA), data = site)
m3 <- lmer(P~crop + (1|ref) + (1|DA), data = site)
m4 <- lmer(S~crop + (1|ref) + (1|DA), data = site)
m5 <- lmer(K~crop + (1|ref) + (1|DA), data = site)
m6 <- lmer(Ca~crop + (1|ref) + (1|DA), data = site)
# Generate regression summary table
stargazer::stargazer(m0, m1, m2, m3, m4, m5, m6, type = "html",
ci = TRUE, ci.level = 0.95, intercept.bottom = FALSE, digits = 1,
omit.table.layout = "sn", model.numbers = FALSE,
column.sep.width = "24pt",
covariate.labels = c("Intercept","millet","rice","sorghum"),
title = "Macro-nutrient regression summary using maize as the reference crop.")
Dependent variable: | |||||||
macro | Na | Mg | P | S | K | Ca | |
Intercept | 24.5*** | -1.8*** | 5.4*** | 6.2*** | 5.7*** | 6.6*** | 2.4*** |
(23.2, 25.7) | (-2.0, -1.6) | (5.1, 5.6) | (6.0, 6.5) | (5.5, 6.0) | (6.3, 6.8) | (2.2, 2.6) | |
millet | -1.4*** | -0.4*** | -0.3*** | -0.4*** | -0.3*** | -0.3*** | 0.3*** |
(-1.8, -1.0) | (-0.6, -0.2) | (-0.4, -0.2) | (-0.5, -0.3) | (-0.4, -0.2) | (-0.4, -0.2) | (0.2, 0.4) | |
rice | -1.6*** | -0.3*** | -0.2*** | -0.3*** | -0.2*** | -0.6*** | 0.01 |
(-1.9, -1.2) | (-0.5, -0.2) | (-0.2, -0.1) | (-0.3, -0.2) | (-0.3, -0.1) | (-0.7, -0.6) | (-0.1, 0.1) | |
sorghum | -0.9*** | -0.4*** | 0.02 | -0.2*** | -0.3*** | -0.3*** | 0.4*** |
(-1.1, -0.6) | (-0.5, -0.3) | (-0.03, 0.1) | (-0.3, -0.2) | (-0.4, -0.2) | (-0.4, -0.3) | (0.3, 0.4) | |
Based on this, there appear to be clear differences between the macro compositional parts of the 4 cereal crops covered by this survey. Maize appears to have a higher macro-nutrient proportion than millet, rice or sorghum. Conversely, because of the binary macro / micro contrast that is imposed here, the proportion of total micro-minerals would be expected to be proportionally higher in millet, rice and sorghum as compared to maize. The corresponding micro-nutrient summary is shown in the table below.
Dependent variable: | |||||||||
Cr | Mn | Fe | Co | Cu | Zn | Se | Mo | I | |
Intercept | -3.5*** | 0.04 | 1.1*** | -5.6*** | -1.0*** | 1.4*** | -5.8*** | -3.3*** | -7.8*** |
(-4.2, -2.8) | (-0.2, 0.3) | (0.5, 1.8) | (-6.0, -5.1) | (-1.2, -0.8) | (1.1, 1.6) | (-6.8, -4.9) | (-4.4, -2.2) | (-8.0, -7.6) | |
millet | -1.8*** | 0.2*** | 0.7*** | 1.0*** | 0.04 | -0.01 | 0.5** | 0.6*** | 0.2 |
(-2.2, -1.3) | (0.1, 0.3) | (0.3, 1.1) | (0.7, 1.2) | (-0.1, 0.2) | (-0.1, 0.1) | (0.1, 1.0) | (0.2, 1.1) | (-0.2, 0.6) | |
rice | 0.1 | 0.9*** | -0.3** | 0.7*** | -0.1** | -0.2*** | -0.4** | 0.9*** | 0.04 |
(-0.3, 0.4) | (0.8, 0.9) | (-0.7, -0.005) | (0.5, 0.8) | (-0.2, -0.01) | (-0.2, -0.1) | (-0.8, -0.1) | (0.6, 1.3) | (-0.3, 0.3) | |
sorghum | -0.5*** | 0.5*** | 0.4*** | -0.3*** | -0.02 | -0.4*** | -0.4*** | 0.7*** | 0.8*** |
(-0.8, -0.2) | (0.5, 0.6) | (0.2, 0.7) | (-0.4, -0.2) | (-0.1, 0.05) | (-0.5, -0.4) | (-0.6, -0.1) | (0.5, 1.0) | (0.6, 1.1) | |
So far, we have largely been dealing with univariate CoDa models, as is common practice. The aim of this next section is to illustrate an empirical multivariate basis that describes the relevant conditional relationships between the different CoDa parts with graphical interaction models. The types of models that we will be using are well described and illustrated in Højsgaard, Edwards and Lauritzen (2012) including the code and references therein. There is also an open R tutorial by Højsgaard (2012) available, which you might want to consult.
To run this section of the notebook you will initially need to install the relevant packages. This is a bit of an art form depending on the version of R that you are currently running. Both the gRim
and statnet
packages that we shall be using for these analyses and their visualization have dependencies to other packages e.g., bioconductor
, which can be a bit tricky depending on their versioning cycle on CRAN. The next chunks are what worked for us using our current R.Version()
, but this is likely to change and you may need to adjust accordingly depending on your setup.
# Install <bioconductor> first ... see: http://www.bioconductor.org/install/
# do this only once
# if (!requireNamespace("BiocManager", quietly = TRUE))
# install.packages("BiocManager")
# BiocManager::install(version = "3.13")
# note that this will query you in the console as to whether to update other associated packages ... (a/s/n). Reply with n if all of the listed packages are up to date.
# Then install and load gRbase, gRim and statnet
packages <- c("gRbase", "gRim", "statnet")
# Install packages
installed_packages <- packages %in% rownames(installed.packages())
if (any(installed_packages == FALSE)) {
install.packages(packages[!installed_packages])
}
# Load packages
invisible(lapply(packages, library, character.only = TRUE))
Similarly, the statnet
package, which we will use for graphical model visualization currently may have some specific installation requirements that you can track on their Github repository.
The first graphical model that we fit provides a baseline for describing nutrient interactions by crop species for provenance reference (ref = y
) sites. We start with a model that initially contains no interactions and then adds only the most significant ones sequentially (i.e., with Foward selection).
# Select reference sites
refs <- site[ which(site$ref=='y'), ]
# CoDa interactions by crop type
coda <- site[ ,c(5,11:25)]
# Forward selection
mm1 <- mmod(~.^1, data = coda)
mm2 <- forward(mm1)
mmf2 <- ugList(terms(mm2), result="matrix")
net <- as.network(x = mmf2, directed = FALSE, loops = FALSE, matrix.type = "adjacency")
This next chunk produces the associated interaction graph. Note that this represents the maximal interaction graph for the reference data.
par(mar=c(1,1,1,1))
plot.network(net, vertex.col = "light grey", vertex.cex = 6, displaylabels = T, label.pos = 5, mode = "circle")
You can also obtain the Maximal cliques of this graph. In Graph theory and in statistics, a clique is a complete subset of a graph, and a maximal clique is a clique which cannot be enlarged. In this particular case, “maximal cliques” can be interpreted as specific grain nutrient subcompositions that may warrant further exploration for any subsequent diagnostic modeling.
getCliques(mmf2)
## [[1]]
## [1] "K" "crop" "Cu" "Mg" "Zn" "P" "Ca" "Mn"
##
## [[2]]
## [1] "K" "crop" "Cu" "Mg" "Zn" "P" "S"
##
## [[3]]
## [1] "K" "crop" "Cu" "Mg" "Zn" "Cr" "S"
##
## [[4]]
## [1] "K" "crop" "Cu" "Mg" "Zn" "Cr" "I"
##
## [[5]]
## [1] "K" "crop" "Cu" "Mg" "Se" "Mn" "P"
##
## [[6]]
## [1] "K" "crop" "Mo" "Na" "Fe" "Cr"
##
## [[7]]
## [1] "K" "crop" "Mo" "Na" "Fe" "Co"
##
## [[8]]
## [1] "K" "crop" "Fe" "S" "Cr"
We can fit a similar model by separating the interactions by macro and micro-nutrient sub-compositions, by crop species. The mm3
base model specification sets that up. We can then use Backward selection to retain only the most significant interactions in the corresponding mm4
fit, which also produces the associated graph.
# Marco and micro-nutrient interactions by crop type
mm3 <- mmod(~crop:Na:Mg:P:S:K:Ca+crop:Cr:Mn:Fe:Co:Cu:Zn:Se:Mo:I, data = coda)
mm4 <- backward(mm3)
mmf4 <- ugList(terms(mm4), result="matrix")
net <- as.network(x = mmf4, directed = FALSE, loops = FALSE, matrix.type = "adjacency")
Under the given model assumptions, the corresponding maximal cliques (sub-compositions) are identified as:
## [[1]]
## [1] "crop" "Cr" "Fe" "Co" "Cu" "Se" "Mo" "I" "Mn"
##
## [[2]]
## [1] "crop" "Cr" "Fe" "Co" "Cu" "Se" "Mo" "I" "Zn"
##
## [[3]]
## [1] "crop" "Na" "P" "K" "S" "Mg"
##
## [[4]]
## [1] "crop" "Na" "P" "K" "Ca"
Note that this halves the number of sub-compositions that might need to be considered relative to the baseline (m2
) model, and therefore could be considered as a minimal CoDa interaction model relative to the main cereal crops that are reported in Malawi.
The main takeaways from this notebook are the following:
Converting nutrient concentrations to multivariate logratios is a simple step, but unlike other data transformations it preserves the integrity of the relevant compositional parts that are needed to normalize the data for most subsequent univariate, multivariate and/or inferential analyses and the associated statistics. Also, CoDA estimates can always be back-transformed into their original units given total concentration sums.
We think that it may be more informative to analyze mineral nutrient concentration data as CoDa. The associated LR conversions (ALR, CLR, ILR) do not make EDAs any more computationally difficult or time consuming, but they should improve any subsequent data modeling, geostatistical and/or machine learning based predictions in instances where subcompositional closure principles (sensu Aitchison geometry) apply.
Automated, multivariate anomaly and outlier detection is advancing with the application of Isolation principles to machine learning. We shall explore this topic further in additional notebooks.
Clustering the CoDa nutrient data in fewer compositional dimensions e.g., via archetypal analysis, provides a simple framework and generalization for post-stratifying nutrient survey results that should improve the precision and accuracy of national cereal grain mineral nutrient estimates in Malawi. They also represent an alternate means for closing and normalizing CoDa. Other clustering, standard Ordination techniques and genotype × environment interaction (GGE) approaches could and should also be explored in this context (see Greenacre, 2021).
Reference sample tagging and tracking can help with locating where and under which circumstances the CoDa were obtained, and identifies high-value targets for future surveys (sensu, their provenance). This also broaches the subject of generating deployment-ready, spectral and/or spatial nutrient predictions using machine learning and/or geostatistics. These additional topics will be covered in more detail in subsequent notebooks. The main lesson is to pay close(r) attention to the collection and interpretation of provenance information in future surveys!
Graphical interaction models can help to clarify the conditional relationships between compositional parts conditioned on observations and measurements. They can also be used to select, compositionally close, model and compare different sub-compositions, including those guided by domain experts. This is our preferred EDA technique for developing e.g., reproducible soil and/or spatial diagnostics of potential nutrient deficiencies … and data permitting.
Any questions, comments or corrections about this notebook are most welcome via AFSIS.