Threshold-Sweep QCA (TS-QCA) is a framework for systematically
exploring how different threshold settings affect the results of
crisp-set Qualitative Comparative Analysis (QCA).
In crisp-set QCA, the researcher must choose thresholds to binarize:
Small changes in these thresholds may lead to substantial differences in truth tables and minimized solutions.
TS-QCA provides:
The TSQCA package implements four sweep methods:
| Method | What varies | What stays fixed | Purpose |
|---|---|---|---|
| CTS–QCA | One X threshold | Y + other Xs | Evaluate influence of a single condition |
| MCTS–QCA | Multiple X thresholds | Y | Explore combinations of X thresholds |
| OTS–QCA | Y threshold | All Xs | Assess robustness to Y calibration |
| DTS–QCA | X and Y thresholds | None | Full 2D sensitivity analysis |
Scope: This package focuses on sufficiency analysis—identifying condition combinations that are sufficient for an outcome. Necessity analysis (whether a condition is required for an outcome) involves different logical structures and evaluation metrics, and is planned for future versions.
TS-QCA assumes:
Example dataset structure:
library(TSQCA)
data("sample_data")
dat <- sample_data
str(dat)
#> 'data.frame': 80 obs. of 4 variables:
#> $ Y : int 8 4 5 7 2 2 7 5 3 8 ...
#> $ X1: int 7 2 6 8 8 9 8 8 1 5 ...
#> $ X2: int 7 5 8 4 0 5 8 4 4 2 ...
#> $ X3: int 1 6 6 5 3 4 5 3 6 8 ...Define outcome and conditions:
In real-world social science research, datasets often contain both binary variables (e.g., gender, yes/no responses) and continuous variables (e.g., sales, satisfaction scores). When using TSQCA with such mixed data, special attention is required.
sweep_listThe internal qca_bin() function uses the rule
x >= thr for binarization:
x = 0: 0 >= 1 → FALSE →
0 (preserved)x = 1: 1 >= 1 → TRUE →
1 (preserved)This ensures that binary variables remain unchanged during the binarization process.
Suppose your dataset has:
When using ctSweepM():
# CORRECT: Specify threshold explicitly for each variable
sweep_list <- list(
X1 = 1, # Binary variable: use threshold 1
X2 = 6:8, # Continuous: sweep thresholds
X3 = 6:8 # Continuous: sweep thresholds
)
res_mixed <- ctSweepM(
dat = dat,
outcome = "Y",
conditions = c("X1", "X2", "X3"),
sweep_list = sweep_list,
thrY = 7,
dir.exp = c(1, 1, 1)
)This explores 1 × 3 × 3 = 9 threshold combinations, treating X1 as a fixed binary condition while sweeping X2 and X3.
# WRONG: Using sweep range for binary variables
sweep_list <- list(
X1 = 6:8, # All values become 0 (since 0 < 6 and 1 < 6)
X2 = 6:8,
X3 = 6:8
)If you accidentally specify X1 = 6:8, both 0 and 1 will
fail the >= 6 condition, making all X1 values become 0.
This destroys the information in your binary variable.
Always examine your data structure before setting up threshold sweeps:
# Check variable ranges
summary(dat[, c("X1", "X2", "X3")])
# Identify binary variables (only 0 and 1)
sapply(dat[, c("X1", "X2", "X3")], function(x) {
unique_vals <- sort(unique(x))
if (length(unique_vals) == 2 && all(unique_vals == c(0, 1))) {
"Binary (use threshold = 1)"
} else {
paste("Continuous (range:", min(x), "-", max(x), ")")
}
})ctSweepS)CTS–QCA varies the threshold for one X condition, keeping the others fixed.
sweep_var <- "X3" # Condition (X) whose threshold is swept
sweep_range <- 6:9 # Candidate threshold values to evaluate
thrY <- 7 # Outcome (Y) threshold (fixed)
thrX_default <- 7 # Threshold for other X conditions (fixed)
res_cts <- ctSweepS(
dat = dat,
outcome = "Y",
conditions = c("X1", "X2", "X3"),
sweep_var = sweep_var,
sweep_range = sweep_range,
thrY = thrY,
thrX_default = thrX_default,
dir.exp = c(1, 1, 1),
return_details = TRUE
)
summary(res_cts)
#> CTS-QCA Summary
#> ===============
#>
#> Analysis Parameters:
#> Outcome: Y
#> Conditions: X1, X2, X3
#> Consistency cutoff: 0.8
#> Frequency cutoff: 1
#>
#> Results by Threshold:
#>
#> threshold expression inclS covS n_solutions
#> 6 X1*X2 1.000 0.303 1
#> 7 X3 + X1*X2 0.906 0.879 1
#> 8 X3 + X1*X2 1.000 0.818 1
#> 9 X3 + X1*X2 1.000 0.515 1ctSweepM)MCTS–QCA evaluates all combinations of thresholds for multiple X conditions.
# Create a sweep list specifying thresholds for each condition
sweep_list <- list(
X1 = 6:7,
X2 = 6:7,
X3 = 6:7
)
res_mcts <- ctSweepM(
dat = dat,
outcome = "Y",
conditions = c("X1", "X2", "X3"),
sweep_list = sweep_list,
thrY = 7,
dir.exp = c(1, 1, 1),
return_details = TRUE
)
summary(res_mcts)
#> MCTS-QCA Summary
#> ================
#>
#> Analysis Parameters:
#> Outcome: Y
#> Conditions: X1, X2, X3
#> Consistency cutoff: 0.8
#> Frequency cutoff: 1
#>
#> Results by Threshold:
#>
#> threshold combo_id expression inclS covS n_solutions
#> X1=6, X2=6, X3=6 1 X1*X2 0.833 0.455 1
#> X1=7, X2=6, X3=6 2 X1*X2 1.000 0.394 1
#> X1=6, X2=7, X3=6 3 X1*X2*~X3 0.818 0.273 1
#> X1=7, X2=7, X3=6 4 X1*X2 1.000 0.303 1
#> X1=6, X2=6, X3=7 5 X3 0.864 0.576 1
#> X1=7, X2=6, X3=7 6 ~X1*X3 + X1*X2 0.931 0.818 1
#> X1=6, X2=7, X3=7 7 X3 0.864 0.576 1
#> X1=7, X2=7, X3=7 8 X3 + X1*X2 0.906 0.879 1otSweep)OTS–QCA varies only the threshold of Y, keeping X thresholds fixed.
res_ots <- otSweep(
dat = dat,
outcome = "Y",
conditions = c("X1", "X2", "X3"),
sweep_range = 6:8,
thrX = c(X1 = 7, X2 = 7, X3 = 7),
dir.exp = c(1, 1, 1),
return_details = TRUE
)
summary(res_ots)
#> OTS-QCA Summary
#> ===============
#>
#> Analysis Parameters:
#> Outcome: Y
#> Conditions: X1, X2, X3
#> Consistency cutoff: 0.8
#> Frequency cutoff: 1
#>
#> Results by Threshold:
#>
#> thrY expression inclS covS n_solutions
#> 6 X3 + X1*X2 0.906 0.853 1
#> 7 X3 + X1*X2 0.906 0.879 1
#> 8 No solution NA NA 0dtSweep)DTS–QCA varies both X thresholds and Y thresholds, creating a full 2D grid.
sweep_list_dts_X <- list(
X1 = 6:7,
X2 = 6:7,
X3 = 6:7
)
sweep_range_dts_Y <- 6:7
res_dts <- dtSweep(
dat = dat,
outcome = "Y",
conditions = c("X1", "X2", "X3"),
sweep_list_X = sweep_list_dts_X,
sweep_range_Y = sweep_range_dts_Y,
dir.exp = c(1, 1, 1),
return_details = TRUE
)
summary(res_dts)
#> DTS-QCA Summary
#> ===============
#>
#> Analysis Parameters:
#> Outcome: Y
#> Conditions: X1, X2, X3
#> Consistency cutoff: 0.8
#> Frequency cutoff: 1
#>
#> Results by Threshold:
#>
#> thrY combo_id thrX expression inclS covS n_solutions
#> 6 1 X1=6, X2=6, X3=6 X1*X2 0.833 0.441 1
#> 7 1 X1=6, X2=6, X3=6 X1*X2 0.833 0.455 1
#> 6 2 X1=7, X2=6, X3=6 X1*X2 1.000 0.382 1
#> 7 2 X1=7, X2=6, X3=6 X1*X2 1.000 0.394 1
#> 6 3 X1=6, X2=7, X3=6 X1*X2*~X3 0.818 0.265 1
#> 7 3 X1=6, X2=7, X3=6 X1*X2*~X3 0.818 0.273 1
#> 6 4 X1=7, X2=7, X3=6 X1*X2 1.000 0.294 1
#> 7 4 X1=7, X2=7, X3=6 X1*X2 1.000 0.303 1
#> 6 5 X1=6, X2=6, X3=7 X3 0.864 0.559 1
#> 7 5 X1=6, X2=6, X3=7 X3 0.864 0.576 1
#> 6 6 X1=7, X2=6, X3=7 ~X1*X3 + X1*X2 0.931 0.794 1
#> 7 6 X1=7, X2=6, X3=7 ~X1*X3 + X1*X2 0.931 0.818 1
#> 6 7 X1=6, X2=7, X3=7 X3 0.864 0.559 1
#> 7 7 X1=6, X2=7, X3=7 X3 0.864 0.576 1
#> 6 8 X1=7, X2=7, X3=7 X3 + X1*X2 0.906 0.853 1
#> 7 8 X1=7, X2=7, X3=7 X3 + X1*X2 0.906 0.879 1Each sweep result contains:
inclS),covS).General guidance:
When QCA minimization produces multiple equivalent intermediate solutions, researchers face a methodological challenge: which solution should be reported? Traditional approaches often report only the first solution (M1), but this may miss important causal heterogeneity.
TSQCA v0.2.0 addresses this by:
Understanding the distinction between essential and selective prime implicants is essential for robust QCA interpretation:
| Type | Definition | Interpretation |
|---|---|---|
| Essential prime implicants | Present in ALL solutions | Robust findings; essential causal factors |
| Selective prime implicants | Present in SOME but not all solutions | Context-dependent; may vary across cases |
| Unique terms | Present in only ONE specific solution | Solution-specific; least robust |
extract_modeThe extract_mode parameter controls how solutions are
extracted:
# Returns all solutions concatenated
# Useful for seeing all equivalent solutions
result_all <- otSweep(
dat = dat,
outcome = "Y",
conditions = c("X1", "X2", "X3"),
sweep_range = 6:8,
thrX = c(X1 = 7, X2 = 7, X3 = 7),
extract_mode = "all"
)
# Output includes n_solutions column
head(result_all$summary)
# expression column shows: "M1: A*B + C; M2: A*B + D; M3: ..."# Returns essential prime implicants (terms common to all solutions)
# Best for identifying robust findings
result_essential <- otSweep(
dat = dat,
outcome = "Y",
conditions = c("X1", "X2", "X3"),
sweep_range = 6:8,
thrX = c(X1 = 7, X2 = 7, X3 = 7),
extract_mode = "essential"
)
# Output includes:
# - expression: essential prime implicants
# - selective_terms: terms in some but not all solutions
# - unique_terms: solution-specific terms
# - n_solutions: number of equivalent solutionsConsider a scenario where QCA produces three equivalent solutions:
A*B + C → YA*B + D → YA*B + E → YThe analysis reveals:
| Component | Terms | Interpretation |
|---|---|---|
| Essential (EPI) | A*B |
Present in all three solutions; robust finding |
| Selective (SPI) | C, D, E |
Each appears in one solution; context-dependent |
| Unique (M1) | C |
Only in solution 1 |
| Unique (M2) | D |
Only in solution 2 |
| Unique (M3) | E |
Only in solution 3 |
Recommendation: Report the essential prime
implicants (A*B → Y) as your main finding, and discuss the
selective prime implicants as alternative pathways in your discussion
section.
The generate_report() function creates comprehensive
markdown reports from your analysis results. This automates the
documentation process and ensures reproducibility.
Contains comprehensive analysis details:
return_details = TRUE (default in
v0.2.0)# Complete workflow
result <- otSweep(
dat = mydata,
outcome = "Y",
conditions = c("X1", "X2", "X3"),
sweep_range = 6:8,
thrX = c(X1 = 7, X2 = 7, X3 = 7)
)
# For main text
generate_report(result, "manuscript_results.md", dat = mydata, format = "simple")
# For supplementary materials
generate_report(result, "supplementary_full.md", dat = mydata, format = "full")All analysis parameters are stored in result$params for
reproducibility:
# View stored parameters
result$params
# Includes:
# - outcome, conditions: variable names
# - thrX, thrY: threshold values
# - incl.cut, n.cut, pri.cut: QCA parameters
# - dir.exp, include: minimization settingsWhen using sweep functions, the number of QCA analyses grows quickly. A systematic approach prevents wasted computation:
Understanding computational complexity helps plan your analysis:
| Function | Complexity | Example | Analyses |
|---|---|---|---|
otSweep() |
O(n) | 5 Y thresholds | 5 |
ctSweepS() |
O(n) | 5 X thresholds | 5 |
ctSweepM() |
O(m^k) | 3 thresholds × 3 conditions | 27 |
dtSweep() |
O(n × m^k) | 3 Y × 3^3 X | 81 |
For dtSweep() and ctSweepM(), reduce
conditions first:
If the same solution appears across multiple thresholds, your findings are robust:
thrY | expression | inclS | covS
-----|---------------|-------|------
6 | A*B + C → Y | 0.85 | 0.72
7 | A*B + C → Y | 0.88 | 0.68
8 | A*B + C → Y | 0.91 | 0.65
Interpretation: The solution A*B + C is
robust across threshold variations.
If solutions vary significantly, investigate the threshold sensitivity:
thrY | expression | inclS | covS
-----|---------------|-------|------
6 | A*B + C → Y | 0.82 | 0.75
7 | A*B → Y | 0.89 | 0.62
8 | B*D → Y | 0.93 | 0.45
Interpretation: Results are threshold-sensitive. Consider reporting the most theoretically justified threshold with appropriate caveats.
In QCA, researchers typically analyze conditions sufficient for the presence of an outcome (Y = 1). However, understanding conditions for the absence of an outcome (~Y) can provide equally valuable insights:
~ NotationTSQCA v0.3.0 supports the QCA package’s tilde (~)
notation for negated outcomes:
# Analyze conditions for Y >= threshold (standard)
result_Y <- otSweep(
dat = dat,
outcome = "Y",
conditions = c("X1", "X2", "X3"),
sweep_range = 6:8,
thrX = c(X1 = 7, X2 = 7, X3 = 7)
)
# Analyze conditions for Y < threshold (negated)
result_negY <- otSweep(
dat = dat,
outcome = "~Y",
conditions = c("X1", "X2", "X3"),
sweep_range = 6:8,
thrX = c(X1 = 7, X2 = 7, X3 = 7)
)When using ~Y, the solution shows conditions sufficient
for the absence of the outcome:
| Analysis | Solution Example | Interpretation |
|---|---|---|
outcome = "Y" |
X1*X2 + X3 |
High X1 AND X2, OR high X3 → High Y |
outcome = "~Y" |
~X1*~X3 + ~X2*~X3 |
Low X1 AND X3, OR low X2 AND X3 → Low Y |
Note: Negated conditions (~X1) in the
solution mean the absence of that condition (below
threshold).
# ctSweepS with negated outcome
result <- ctSweepS(
dat = dat,
outcome = "~Y",
conditions = c("X1", "X2", "X3"),
sweep_var = "X3",
sweep_range = 6:8,
thrY = 7,
thrX_default = 7
)
# ctSweepM with negated outcome
result <- ctSweepM(
dat = dat,
outcome = "~Y",
conditions = c("X1", "X2"),
sweep_list = list(X1 = 6:7, X2 = 6:7),
thrY = 7
)
# dtSweep with negated outcome
result <- dtSweep(
dat = dat,
outcome = "~Y",
conditions = c("X1", "X2"),
sweep_list_X = list(X1 = 6:7, X2 = 7),
sweep_range_Y = 6:8
)TSQCA can generate Fiss-style configuration charts (Table 5 format) commonly used in QCA publications.
Configuration charts are now automatically included in reports
generated by generate_report():
# Generate report with configuration charts (default)
generate_report(result, "my_report.md", dat = dat, format = "full")
# Disable charts if needed
generate_report(result, "my_report.md", dat = dat, include_chart = FALSE)
# Use LaTeX symbols for academic papers
generate_report(result, "my_report.md", dat = dat, chart_symbol_set = "latex")You can also generate configuration charts directly:
# From path strings
paths <- c("A*B*~C", "A*D", "B*E")
chart <- config_chart_from_paths(paths)
cat(chart)
#> | Condition | M1 | M2 | M3 |
#> |:--:|:--:|:--:|:--:|
#> | A | ● | ● | |
#> | B | ● | | ● |
#> | C | ⊗ | | |
#> | D | | ● | |
#> | E | | | ● |
#>
#> *● = presence, ⊗ = absence, blank = don't care*Three symbol sets are available:
When you have multiple equivalent solutions:
solutions <- list(
c("A*B", "C*D"),
c("A*B", "C*E")
)
chart <- config_chart_multi_solutions(solutions)
cat(chart)
#> **Note:** 2 equivalent solutions exist. Tables are shown separately below.
#>
#> ### Solution M1
#>
#> | Condition | M1 | M2 |
#> |:--:|:--:|:--:|
#> | A | ● | |
#> | B | ● | |
#> | C | | ● |
#> | D | | ● |
#>
#> ---
#>
#> ### Solution M2
#>
#> | Condition | M1 | M2 |
#> |:--:|:--:|:--:|
#> | A | ● | |
#> | B | ● | |
#> | C | | ● |
#> | E | | ● |
#>
#> *● = presence, ⊗ = absence, blank = don't care*TSQCA provides a structured and reproducible way to evaluate
how threshold choices influence QCA results.
Using CTS, MCTS, OTS, and DTS sweeps, researchers can:
New in v0.5.0:
include_chart,
chart_symbol_setconfig_chart_from_paths(),
config_chart_multi_solutions()New in v0.3.0:
outcome,
conditions)~Y notation)New in v0.2.0:
For more information on TS-QCA methodology, see:
sessionInfo()
#> R version 4.4.2 (2024-10-31 ucrt)
#> Platform: x86_64-w64-mingw32/x64
#> Running under: Windows 11 x64 (build 26200)
#>
#> Matrix products: default
#>
#>
#> locale:
#> [1] LC_COLLATE=C LC_CTYPE=Japanese_Japan.utf8
#> [3] LC_MONETARY=Japanese_Japan.utf8 LC_NUMERIC=C
#> [5] LC_TIME=Japanese_Japan.utf8
#>
#> time zone: Asia/Tokyo
#> tzcode source: internal
#>
#> attached base packages:
#> [1] stats graphics grDevices utils datasets methods base
#>
#> other attached packages:
#> [1] QCA_3.23 admisc_0.39 TSQCA_1.0.0
#>
#> loaded via a namespace (and not attached):
#> [1] cli_3.6.5 knitr_1.50 rlang_1.1.6 xfun_0.55
#> [5] otel_0.2.0 promises_1.5.0 shiny_1.12.1 jsonlite_2.0.0
#> [9] xtable_1.8-4 htmltools_0.5.9 httpuv_1.6.16 sass_0.4.10
#> [13] lpSolve_5.6.23 rmarkdown_2.30 evaluate_1.0.4 jquerylib_0.1.4
#> [17] fastmap_1.2.0 yaml_2.3.10 lifecycle_1.0.4 compiler_4.4.2
#> [21] Rcpp_1.1.0 rstudioapi_0.17.1 later_1.4.4 digest_0.6.39
#> [25] R6_2.6.1 magrittr_2.0.4 bslib_0.9.0 declared_0.25
#> [29] tools_4.4.2 mime_0.13 venn_1.12 cachem_1.1.0