soilquality builds Soil Quality Indices — and, unusually, helps you find out whether the one you built is any good.
A soil quality index has no ground truth. Nothing tells you whether yours is correct. Most packages stop at producing a number; this one carries the routes the literature actually uses at every step of the pipeline, and the diagnostics that reveal when an index is not discriminating, when a model is circular, or when a comparison was never fair to begin with.
Selecting indicators
- PCA minimum data set selection, with the published within 10 % of the maximum loading rule available alongside the narrower default
- Correlation-network selection by centrality rather than variance — ecological hubs instead of high-variance variables
- Functional (EMDS) grouping: select one indicator per soil function, so a dominant function cannot crowd the others out
- PCA adequacy testing — KMO and Bartlett's sphericity, which most published work skips
Scoring
- Linear (higher / lower / optimum / threshold), plus the sigmoidal curve that dominates the SQI literature
- Reference-soil standardisation — score against an undisturbed soil instead of your own sample extremes, the documented escape from index incomparability
- Inherent-property adjustment — stop scoring a soil down for being what its parent material made it
Weighting and aggregation
- AHP with consistency-ratio validation, PCA loadings, or network centrality
- Area aggregation — a weight-free route that sidesteps the pipeline's most contested step entirely
Validation — the part most work skips
sqi_validate()leads with the distribution across decision categories, not a correlationsqi_stability()asks whether your conclusion survives a change of recipecheck_circularity()refuses to let you regress an index on the indicators that built it
Also
- Three example datasets, each for a different job
- Interactive Shiny application for non-programmers
- Vignettes, a full function reference, and a package website
An index is five decisions, and this package offers alternatives at each of them — plus the step most work skips.
select → group → score → weight → aggregate → VALIDATE
| Step | Options | Default | When to change it |
|---|---|---|---|
| Select | pca_select_mds() (variance), na_select_mds() (network centrality), expert list |
PCA | Network when ecological hubs matter more than variance, or when normality is doubtful |
| Group | groups = NULL, soil_function_groups |
none | Whenever more than one soil function matters — ungrouped selection can leave whole functions unrepresented |
| Score | linear, sigmoid, optimum, threshold, reference-soil | linear | Sigmoid to reproduce most published work; reference-soil for comparability across studies |
| Weight | AHP, PCA loading, network centrality, equal | equal | AHP when you have defensible expert judgement |
| Aggregate | method = "weighted", method = "area" |
weighted | Area to sidestep weighting entirely |
| Validate | sqi_validate(), sqi_stability() |
— | Always |
A soil quality index has no ground truth. Nothing tells you whether yours is correct. What you can establish is whether it discriminates, and whether your conclusion survives building it a different way.
sqi_validate() leads with the distribution across decision categories, not
with a correlation. Maaz et al. (2023) found two indices correlating at
r = 0.96 while one placed 94 % of plots in the middle band and the
other 61 %. An index that calls almost everything "medium" cannot inform a
decision, however well it correlates with anything else.
sqi_stability() runs the same samples through several recipes and reports
whether the ranking survives. A high rank correlation does not rescue a
changed extreme, if the extreme is what you act on.
Do not build an index from predicted soil properties. Chaudhry et al. (2024) found that computing an SQI from spectrally predicted properties gave R² = 0.23, while predicting the index directly from the same spectra gave R² = 0.90 — with individually acceptable property models. If you only have predictions, model the index itself.
See vignette("building-and-validating-an-sqi") for the pipeline end to end.
soilquality is not on CRAN. Install it from GitHub:
# Install pak if needed
if (!requireNamespace("pak", quietly = TRUE)) {
install.packages("pak")
}
# Install soilquality
pak::pak("ccarbajal16/soilquality")# Install devtools if needed
if (!requireNamespace("devtools", quietly = TRUE)) {
install.packages("devtools")
}
# Install soilquality
devtools::install_github("ccarbajal16/soilquality")Correlation-network selection (na_select_mds(), mds_consensus()) needs
igraph, which is suggested rather than required — this package is
MIT-licensed and igraph is GPL, so it is kept out of Imports deliberately.
Everything else works without it.
install.packages("igraph")library(soilquality)
# Load example data
data(soil_data)
# Compute SQI with automatic property detection
result <- compute_sqi_properties(
data = soil_data,
properties = c("Sand", "Silt", "Clay", "pH", "OM", "P", "K")
)
# View results summary
print(result)
# Visualize SQI distribution
plot(result, type = "distribution")
# Create comprehensive report
plot_sqi_report(result)# Use standard property set
result <- compute_sqi_properties(
data = soil_data,
properties = soil_property_sets$standard
)# Define custom scoring for specific properties
custom_rules <- list(
pH = optimum_range(optimal = 6.5, tolerance = 1),
OM = higher_better(),
BD = lower_better(),
P = threshold_scoring(
thresholds = c(0, 10, 20, 50),
scores = c(0, 0.5, 0.8, 1.0)
)
)
result <- compute_sqi_properties(
data = soil_data,
properties = c("pH", "OM", "BD", "P"),
scoring_rules = custom_rules
)# Create AHP matrix interactively
indicators <- c("pH", "OM", "P", "K")
ahp_matrix <- create_ahp_matrix(indicators, mode = "interactive")
# Use computed AHP weights in SQI calculation
result <- compute_sqi_properties(
data = soil_data,
properties = ahp_matrix$indicators,
pairwise_matrix = ahp_matrix$weights
)Producing an index is the easy half. This is the other one.
props <- c("pH", "OM", "N", "P", "K", "CEC", "BD")
index <- compute_sqi_properties(soil_data, properties = props,
id_column = "SampleID")
sqi_validate(index)On this dataset that call raises a warning, and it is not a bug — it is the diagnostic working. The default recipe puts 100 % of samples in the middle bands, with "very low" and "very high" both empty. An index that declines to call any sample clearly good or clearly bad cannot inform a decision.
Then ask whether the conclusion depends on how you built it:
sigmoid <- compute_sqi_properties(
soil_data, properties = props, id_column = "SampleID",
scoring_rules = standard_scoring_rules(props, scoring = "sigmoid")
)
sqi_stability(linear = index, sigmoid = sigmoid)Here the two recipes agree at ρ = 0.80 and keep the same best field — but the worst one changes. If the field you were about to intervene on depends on which scoring function you happened to pick, you do not have a result; you have an artefact of the recipe. A high rank correlation does not rescue that.
And before modelling what drives the index, check that the question is not circular:
check_circularity(index, ~ Sand + Clay) # outside the index: fine
try(check_circularity(index, ~ OM)) # a component: refused# Start Shiny app for GUI-based analysis
run_sqi_app()compute_sqi()- File-based workflowcompute_sqi_df()- In-memory workflow, and the engine the others delegate to. Carriesmethod,select,inherentandnetwork_argscompute_sqi_properties()- Property selection and constructor-based scoring
read_soil_csv()- Import soil data from CSV filesstandardize_numeric()- Z-score standardization of numeric columnsto_numeric()- Safe type conversion
pca_select_mds()- Selection by variance.withingives the published relative-loading rule,groupsselects within soil functions,selector = "norm"uses Yuan's norm valuena_select_mds()- Selection by centrality in a Spearman correlation network (needs the suggestedigraph)mds_consensus()- The intersection of both routes, as a robustness checkpca_adequacy()- KMO and Bartlett's test of sphericity
soil_function_groups- The five soil functions, for grouped selectionassign_function_groups()- Map property names onto those functions
score_higher_better(),score_lower_better(),score_optimum(),score_threshold()- The linear familyscore_sigmoid()- The non-linear curve that dominates the SQI literaturestandardize_to_reference()- Score against an undisturbed reference soilscore_indicators()- Apply scoring across a set of indicators
higher_better(),lower_better(),optimum_range(),threshold_scoring()sigmoid_scoring()- Sigmoidal rulereference_scoring()- Reference-soil rulestandard_scoring_rules()- Automatic assignment by property name, in"linear"or"sigmoid"flavoursoil_property_sets- Pre-defined property collections
ahp_weights()- Weights from a pairwise comparison matrix, with a consistency ratiocreate_ahp_matrix()- Interactive or programmatic matrix creationratio_to_saaty()- Convert importance ratios to the Saaty scalesqi_area()- Weight-free aggregation as the area of a radar diagram
sqi_validate()- Distribution across decision categories, sensitivity, fidelity to a total-data-set index, and an optional external criterionsqi_stability()- Does the ranking survive a change of recipe?check_circularity()- Refuses a model that regresses an index on its own components
adjust_inherent()- Remove the variation attributable to parent material and land-use historysensitivity_resistance()- Which indicators degrade faster than soil carbon, and which resist
plot.sqi_result()- S3 plot method with five typesplot_sqi_report()- Multi-panel reportplot_sqi_validation()- The distribution across decision categories
run_sqi_app()- Launch the Shiny application
Note: the Shiny app currently exposes the 1.0.0 workflow. The routes added since — sigmoid and reference scoring, area aggregation, network and grouped selection, and the whole validation layer — are available from the console only.
Three datasets, because they do three different jobs:
| Dataset | Rows | For |
|---|---|---|
soil_data |
50 | Scoring, weighting, aggregation. Properties drawn independently, so it carries almost no covariance |
soil_structured |
120 | Anything reading the structure between indicators. Texture sums to 100, SOC = OM/1.724, CEC generated by clay and organic colloids |
soil_inherent |
180 | Inherent-property adjustment and nested designs. Carries soil_type, land_use_history, management and PlotID, five samples per plot |
All three are simulated, and each generating script in data-raw/ states every relationship it used.
Comprehensive documentation is available through:
-
Vignettes:
- Building and validating a Soil Quality Index — the pipeline end to end, and the one to read first
- Introduction to soilquality - Basic workflow and concepts
- Advanced Usage - Custom scoring and property selection
- AHP Matrices Guide - Creating and interpreting pairwise comparisons
-
Function Reference: Access help for any function with
?function_name -
Package Website: https://ccarbajal16.github.io/soilquality
If you use this package in your research, please cite it as:
citation("soilquality")Or use:
Carbajal, Carlos (2026). soilquality: Soil Quality Index Calculation with PCA and AHP. R package version 2.3.0. https://github.com/ccarbajal16/soilquality
citation("soilquality") reads the installed DESCRIPTION, so it is the
authoritative version; the block above will drift as releases go by.
Contributions are welcome! Please feel free to submit a Pull Request or open an Issue for:
- Bug reports
- Feature requests
- Documentation improvements
- Code contributions
Please report issues at: https://github.com/ccarbajal16/soilquality/issues
For questions and discussions, use the GitHub Discussions page.
This package is licensed under the MIT License. See the LICENSE file for details.
This package implements methodologies from soil quality assessment literature, including PCA-based MDS selection and AHP weighting approaches widely used in soil science research.
