ThSQCA runs a crisp-set QCA analysis many times, once for each threshold setting you specify, and collects the solutions in one table. This section gets you from zero to a first result. Later sections explain each step.
The examples use small simulated data sets, so that every number in this tutorial can be reproduced exactly. The first one mimics a marketing survey: three evaluation scores (0 to 10) and a loyalty score. The data-generating code is shown so that you can see how the structure was built.
make_demo_data <- function(n = 400, seed = 2026) {
set.seed(seed)
clip <- function(x) pmin(10, pmax(0, round(x)))
QUA <- clip(rnorm(n, 6, 2)) # quality evaluation
SER <- clip(rnorm(n, 6, 2)) # service evaluation
ENV <- clip(rnorm(n, 6, 2)) # store environment evaluation
# Loyalty is high when quality and service are both high, or (a little
# less strongly) when quality and environment are both high.
core <- pmax(pmin(QUA, SER), pmin(QUA, ENV) - 1)
LOY <- clip(core + rnorm(n, 0, 0.7))
data.frame(LOY, QUA, SER, ENV)
}
demo <- make_demo_data()
head(demo)
#> LOY QUA SER ENV
#> 1 7 7 9 3
#> 2 5 4 7 5
#> 3 4 6 4 4
#> 4 6 6 6 4
#> 5 5 5 4 7
#> 6 0 1 2 5Now run a sweep of the outcome threshold. We treat a
case as a member of the outcome set when LOY >= t, and
we let t take the values 5, 6, 7 and 8. The condition
thresholds are held fixed at 7.
library(ThSQCA)
res <- otSweep(
dat = demo,
outcome = "LOY",
conditions = c("QUA", "SER", "ENV"),
sweep_range = 5:8, # outcome thresholds to try
thrX = c(QUA = 7, SER = 7, ENV = 7) # fixed condition thresholds
)summary(res)
#> OTS Summary
#> ===========
#>
#> Analysis Parameters:
#> Outcome: LOY
#> Conditions: QUA, SER, ENV
#> Consistency cutoff: 0.8
#> Frequency cutoff: 1
#>
#> Results by Threshold:
#>
#> thrY expression inclS covS n_solutions
#> 5 QUA*SER + QUA*ENV 0.981 0.403 1
#> 6 QUA*SER + QUA*ENV 0.926 0.565 1
#> 7 QUA*SER 0.894 0.656 1
#> 8 No solution NA NA 0Read the table from top to bottom. Each row is one complete QCA analysis:
LOY >= 5 and LOY >= 6, two
configurations are sufficient for the outcome: high quality together
with high service (QUA*SER), and high quality together with
a high store environment score (QUA*ENV).LOY >= 7, only QUA*SER remains.LOY >= 8, no configuration reaches the
consistency cutoff (0.8), so the analysis reports “No solution”.That is the whole idea: the sufficiency structure you report can depend on the target level of the outcome. A single-threshold analysis would show only one of these rows. Reporting all four rows shows where the structure is stable and where it changes.
The remaining sections show how to prepare your data, how to sweep the other thresholds, how to choose a solution type, how to read the output, and how to report the results.
In crisp-set QCA, the outcome and each condition must be turned into 0/1 membership using a threshold. Threshold-Sweep QCA (ThS-QCA) treats these thresholds as an explicit analytical dimension instead of a fixed preprocessing input. It records the sufficiency solution obtained at each threshold setting.
ThSQCA implements four sweeps:
| Sweep | What varies | What stays fixed | Function |
|---|---|---|---|
| CTS (single) | One condition threshold | Outcome threshold and the other conditions | ctSweepS() |
| CTS (multiple) | Several condition thresholds (a grid) | Outcome threshold | ctSweepM() |
| OTS | Outcome threshold | All condition thresholds | otSweep() |
| DTS | Outcome threshold and condition thresholds | Nothing | dtSweep() |
What ThSQCA does not do. It does not reimplement QCA. For every threshold setting, it binarizes the data and then calls
QCA::truthTable()andQCA::minimize(). ThSQCA only loops over the settings, collects the results, and reports them. Section “Reporting your results” shows how to check any single cell directly against the QCA package.
Scope. The sweeps address sufficiency: which combinations of conditions are sufficient for the outcome at each threshold setting.
Tip: The Sweep Builder web tool generates ready-to-run ThSQCA code from your variable names and thresholds.
ThS-QCA works on raw scores and binarizes them itself, so no calibration step is needed before a sweep.
x >= threshold).
Thresholds can be any real number.If a condition is already binary (for example an indicator for a customer segment), do not sweep it. Give it the threshold 1, which leaves 0 as 0 and 1 as 1. Any larger threshold would turn every value into 0 and destroy the variable.
# X1 is binary (0/1); X2 and X3 are 0-10 scores.
res_mixed <- ctSweepM(
dat = dat,
outcome = "Y",
conditions = c("X1", "X2", "X3"),
sweep_list = list(X1 = 1, # binary: fixed threshold 1, not swept
X2 = 6:8, # scores: swept
X3 = 6:8),
thrY = 7
)This explores 1 x 3 x 3 = 9 threshold combinations. A quick way to spot binary variables before you set up a sweep:
pre_calibrated)Sometimes a condition has a theoretically grounded fuzzy calibration
that you do not want to sweep. List it in pre_calibrated.
Such a variable is passed to QCA::truthTable() as it is,
without binarization, and needs no entry in thrX. The other
conditions are still binarized at their thresholds.
d <- demo
d$ENV_fz <- pmin(1, pmax(0, (d$ENV - 2.5) / 6)) # example calibration (0 to 1)
res_mix <- otSweep(
dat = d,
outcome = "LOY",
conditions = c("QUA", "SER", "ENV_fz"),
sweep_range = 5:8,
thrX = c(QUA = 7, SER = 7), # no entry for ENV_fz
pre_calibrated = "ENV_fz"
)Points to remember:
pre_calibrated is never swept. To
sweep a variable that is already on a 0 to 1 membership scale, leave it
out of pre_calibrated; see the next subsection.Membership scores can be swept like any other numeric variable. Leave
them out of pre_calibrated and give thresholds on the 0 to
1 scale. A case is then a member when its membership score is at least
the threshold, so the sweep asks how the sufficiency structure changes
as the membership criterion becomes stricter (for example 0.4, 0.6,
0.8). This applies to the conditions and to the outcome alike.
fz <- function(x) pmin(1, pmax(0, (x - 2.5) / 6)) # example calibration (0 to 1)
d_fz <- data.frame(LOY_fz = fz(demo$LOY), QUA_fz = fz(demo$QUA),
SER_fz = fz(demo$SER), ENV_fz = fz(demo$ENV))
res_fz <- otSweep(
dat = d_fz,
outcome = "LOY_fz",
conditions = c("QUA_fz", "SER_fz", "ENV_fz"),
sweep_range = c(0.4, 0.6, 0.8), # outcome membership criteria
thrX = c(QUA_fz = 0.6, SER_fz = 0.6, ENV_fz = 0.6)
)Which approach to use is a design decision, and both are defensible:
pre_calibrated, the fuzzy scores stay as they are
and QCA works with them directly. The calibration is fixed and is not
part of the sensitivity analysis.>=). At a threshold of 0.5 this includes cases at the
crossover point, where membership is most ambiguous. Check how many
cases sit exactly at the thresholds you use.Each subsection follows the same pattern: when to use the sweep, a minimal call, and how to read the result. All calls use the demo data from the quick start. The compute chunks hide the console output so that only the summary table is shown.
otSweep)Use it when the outcome is measured on a graded
scale and you want to know how the solution changes as the target level
of the outcome rises. The quick start already showed this sweep. The
call is repeated here with the two optional arguments you will use most
often, incl.cut and n.cut.
res_ots <- otSweep(
dat = demo,
outcome = "LOY",
conditions = c("QUA", "SER", "ENV"),
sweep_range = 5:8,
thrX = c(QUA = 7, SER = 7, ENV = 7),
incl.cut = 0.8, # consistency cutoff for the truth table
n.cut = 1 # minimum number of cases per configuration
)summary(res_ots)
#> OTS Summary
#> ===========
#>
#> Analysis Parameters:
#> Outcome: LOY
#> Conditions: QUA, SER, ENV
#> Consistency cutoff: 0.8
#> Frequency cutoff: 1
#>
#> Results by Threshold:
#>
#> thrY expression inclS covS n_solutions
#> 5 QUA*SER + QUA*ENV 0.981 0.403 1
#> 6 QUA*SER + QUA*ENV 0.926 0.565 1
#> 7 QUA*SER 0.894 0.656 1
#> 8 No solution NA NA 0How to read it. The solution changes between
LOY >= 6 and LOY >= 7 (two
configurations become one), and it disappears at
LOY >= 8. In the vocabulary of the method, the sweep has
a threshold transition between 6 and 7, and a further one
between 7 and 8.
By default otSweep() returns the complex solution
(include = ""). In this data set all eight combinations of
the three conditions are observed, so the complex, parsimonious and
intermediate solutions coincide. The choice of solution type matters
when some combinations are unobserved; the section “Choosing a solution
type” below uses a second data set for that.
ctSweepS)Use it when you want to know how sensitive the solution is to a single calibration decision, such as “what counts as a high service evaluation”.
res_cts <- ctSweepS(
dat = demo,
outcome = "LOY",
conditions = c("QUA", "SER", "ENV"),
sweep_var = "SER", # the condition whose threshold is swept
sweep_range = 5:9, # candidate thresholds for SER
thrY = 7, # fixed outcome threshold (LOY >= 7)
thrX_default = 7 # fixed threshold for the other conditions
)summary(res_cts)
#> CTS (single) Summary
#> ====================
#>
#> Analysis Parameters:
#> Outcome: LOY
#> Conditions: QUA, SER, ENV
#> Consistency cutoff: 0.8
#> Frequency cutoff: 1
#>
#> Results by Threshold:
#>
#> threshold expression inclS covS n_solutions
#> 5 No solution NA NA 0
#> 6 No solution NA NA 0
#> 7 QUA*SER 0.894 0.656 1
#> 8 QUA*SER 0.909 0.333 1
#> 9 QUA*SER*~ENV 0.917 0.122 1How to read it.
SER >= 5 or
SER >= 6), no configuration reaches the consistency
cutoff.SER >= 7, the solution is QUA*SER,
and its consistency stays close to 0.9.covS) falls as the criterion tightens (0.656,
0.333, 0.122), because fewer cases satisfy the condition.SER >= 9 the solution adds ~ENV.
Only a small number of cases meet QUA >= 7 and
SER >= 9, as the next chunk shows, so this last row
rests on very few cases and should be read with caution.ctSweepM)Use it when you want to explore the joint space of several calibration decisions at once. The function evaluates every combination of the candidate thresholds you supply, so the number of cells grows quickly (here 3 x 3 x 3 = 27).
res_mcts <- ctSweepM(
dat = demo,
outcome = "LOY",
conditions = c("QUA", "SER", "ENV"),
sweep_list = list(QUA = 6:8, SER = 6:8, ENV = 6:8), # candidates per condition
thrY = 7 # fixed outcome threshold
)summary(res_mcts)
#> CTS (multiple) Summary
#> ======================
#>
#> Analysis Parameters:
#> Outcome: LOY
#> Conditions: QUA, SER, ENV
#> Consistency cutoff: 0.8
#> Frequency cutoff: 1
#>
#> Results by Threshold:
#>
#> threshold combo_id expression inclS covS n_solutions
#> QUA=6, SER=6, ENV=6 1 No solution NA NA 0
#> QUA=7, SER=6, ENV=6 2 No solution NA NA 0
#> QUA=8, SER=6, ENV=6 3 No solution NA NA 0
#> QUA=6, SER=7, ENV=6 4 No solution NA NA 0
#> QUA=7, SER=7, ENV=6 5 QUA*SER 0.894 0.656 1
#> QUA=8, SER=7, ENV=6 6 QUA*SER 0.919 0.378 1
#> QUA=6, SER=8, ENV=6 7 No solution NA NA 0
#> QUA=7, SER=8, ENV=6 8 QUA*SER 0.909 0.333 1
#> QUA=8, SER=8, ENV=6 9 QUA*SER 0.905 0.211 1
#> QUA=6, SER=6, ENV=7 10 No solution NA NA 0
#> QUA=7, SER=6, ENV=7 11 No solution NA NA 0
#> QUA=8, SER=6, ENV=7 12 QUA*SER*~ENV 0.800 0.311 1
#> QUA=6, SER=7, ENV=7 13 No solution NA NA 0
#> QUA=7, SER=7, ENV=7 14 QUA*SER 0.894 0.656 1
#> QUA=8, SER=7, ENV=7 15 QUA*SER*~ENV 1.000 0.300 1
#> QUA=6, SER=8, ENV=7 16 No solution NA NA 0
#> QUA=7, SER=8, ENV=7 17 QUA*SER 0.909 0.333 1
#> QUA=8, SER=8, ENV=7 18 QUA*SER*~ENV 1.000 0.189 1
#> QUA=6, SER=6, ENV=8 19 No solution NA NA 0
#> QUA=7, SER=6, ENV=8 20 No solution NA NA 0
#> QUA=8, SER=6, ENV=8 21 QUA*ENV 0.889 0.178 1
#> QUA=6, SER=7, ENV=8 22 No solution NA NA 0
#> QUA=7, SER=7, ENV=8 23 QUA*SER 0.894 0.656 1
#> QUA=8, SER=7, ENV=8 24 QUA*SER + QUA*ENV 0.920 0.511 1
#> QUA=6, SER=8, ENV=8 25 No solution NA NA 0
#> QUA=7, SER=8, ENV=8 26 QUA*SER 0.909 0.333 1
#> QUA=8, SER=8, ENV=8 27 QUA*SER*~ENV + QUA*~SER*ENV 0.939 0.344 1How to read it. With 27 rows, look for regions rather than single rows.
QUA = 6, every cell reports “No solution”.QUA = 7 and SER is 7 or 8, the
solution is QUA*SER, whatever the threshold for
ENV. This is a stable region of the threshold space.~ENV or QUA*ENV
appear only in cells with QUA = 8, the strictest quality
criterion in this grid.dtSweep)Use it when you want the fullest picture: the target level of the outcome and the criteria for the conditions vary at the same time. The result is a two-dimensional map, with one row per combination.
res_dts <- dtSweep(
dat = demo,
outcome = "LOY",
conditions = c("QUA", "SER", "ENV"),
sweep_list_X = list(QUA = 6:7, SER = 6:7, ENV = 6:7), # condition candidates
sweep_range_Y = 6:8 # outcome candidates
)summary(res_dts)
#> DTS Summary
#> ===========
#>
#> Analysis Parameters:
#> Outcome: LOY
#> Conditions: QUA, SER, ENV
#> Consistency cutoff: 0.8
#> Frequency cutoff: 1
#>
#> Results by Threshold:
#>
#> thrY combo_id thrX expression inclS covS n_solutions
#> 6 1 QUA=6, SER=6, ENV=6 QUA*SER 0.863 0.712 1
#> 7 1 QUA=6, SER=6, ENV=6 No solution NA NA 0
#> 8 1 QUA=6, SER=6, ENV=6 No solution NA NA 0
#> 6 2 QUA=7, SER=6, ENV=6 QUA*SER 0.922 0.531 1
#> 7 2 QUA=7, SER=6, ENV=6 No solution NA NA 0
#> 8 2 QUA=7, SER=6, ENV=6 No solution NA NA 0
#> 6 3 QUA=6, SER=7, ENV=6 QUA*SER 0.893 0.520 1
#> 7 3 QUA=6, SER=7, ENV=6 No solution NA NA 0
#> 8 3 QUA=6, SER=7, ENV=6 No solution NA NA 0
#> 6 4 QUA=7, SER=7, ENV=6 QUA*SER 0.985 0.367 1
#> 7 4 QUA=7, SER=7, ENV=6 QUA*SER 0.894 0.656 1
#> 8 4 QUA=7, SER=7, ENV=6 No solution NA NA 0
#> 6 5 QUA=6, SER=6, ENV=7 QUA*SER 0.863 0.712 1
#> 7 5 QUA=6, SER=6, ENV=7 No solution NA NA 0
#> 8 5 QUA=6, SER=6, ENV=7 No solution NA NA 0
#> 6 6 QUA=7, SER=6, ENV=7 QUA*SER + QUA*ENV 0.908 0.667 1
#> 7 6 QUA=7, SER=6, ENV=7 No solution NA NA 0
#> 8 6 QUA=7, SER=6, ENV=7 No solution NA NA 0
#> 6 7 QUA=6, SER=7, ENV=7 QUA*SER 0.893 0.520 1
#> 7 7 QUA=6, SER=7, ENV=7 No solution NA NA 0
#> 8 7 QUA=6, SER=7, ENV=7 No solution NA NA 0
#> 6 8 QUA=7, SER=7, ENV=7 QUA*SER + QUA*ENV 0.926 0.565 1
#> 7 8 QUA=7, SER=7, ENV=7 QUA*SER 0.894 0.656 1
#> 8 8 QUA=7, SER=7, ENV=7 No solution NA NA 0How to read it. There are 24 cells (8 condition combinations by 3 outcome thresholds).
LOY >= 6, every one of the 8 condition
combinations gives a solution.LOY >= 7, a solution exists only when
QUA = 7 and SER = 7.LOY >= 8, there is no solution anywhere in this
grid.In this data set, the higher the target level of the outcome, the
stricter the criteria for the conditions had to be before a
configuration was consistent enough. To look for solutions at
LOY >= 8, you could extend the condition thresholds
upward (for example QUA and SER in 8:9).
QCA can minimize the truth table in three ways, which differ in how
they treat logical remainders: combinations of
conditions that do not occur in the data. All sweep functions accept the
same two arguments as QCA::minimize().
| Solution type | include |
dir.exp |
Logical remainders |
|---|---|---|---|
| Complex (default) | "" |
NULL |
Not used |
| Parsimonious | "?" |
NULL |
Any remainder may be used if it shortens the formula |
| Intermediate | "?" |
c(1, 1, ...) |
Only remainders consistent with your directional expectations |
dir.exp states, for each condition, whether its presence
(1) or absence (0) is expected to contribute
to the outcome.
The first data set has no remainders, so the three types give the
same answer. To see the difference we need data in which some
combinations are missing. The second data set is again simulated. It
describes 60 business-to-business software accounts: RNW is
renewal intention, and TRU (trust in the vendor),
PRC (price fairness) and SUP (support quality)
are the conditions, all on 0 to 10 scales. The three conditions share a
common factor, so that accounts tend to be high on all of them or low on
all of them, and some combinations do not occur.
make_demo_data2 <- function(n = 60, seed = 89, rho = 0.85) {
set.seed(seed)
clip <- function(x) pmin(10, pmax(0, round(x)))
L <- rnorm(n) # common factor
mk <- function() clip(6 + 2 * (rho * L + sqrt(1 - rho^2) * rnorm(n)))
TRU <- mk(); PRC <- mk(); SUP <- mk()
# Renewal is high when trust is high and either price or support is high.
RNW <- clip(pmin(TRU, pmax(PRC, SUP)) + rnorm(n, 0, 0.7))
data.frame(RNW, TRU, PRC, SUP)
}
demo2 <- make_demo_data2()
head(demo2)
#> RNW TRU PRC SUP
#> 1 3 3 4 4
#> 2 6 8 7 7
#> 3 9 9 9 7
#> 4 5 5 4 6
#> 5 10 10 8 9
#> 6 1 2 4 5Take the target RNW >= 5 and the criterion 7 or more
for every condition. The truth table, built directly with the QCA
package, shows which of the eight combinations occur.
library(QCA)
bin2 <- data.frame(
RNW = as.integer(demo2$RNW >= 5),
TRU = as.integer(demo2$TRU >= 7),
PRC = as.integer(demo2$PRC >= 7),
SUP = as.integer(demo2$SUP >= 7)
)
tt2 <- truthTable(bin2, outcome = "RNW", conditions = c("TRU", "PRC", "SUP"),
incl.cut = 0.8, n.cut = 1, show.cases = FALSE)
tt2
#>
#> OUT: output value
#> n: number of cases in configuration
#> incl: sufficiency inclusion score
#> PRI: proportional reduction in inconsistency
#>
#> TRU PRC SUP OUT n incl PRI
#> 1 0 0 0 0 30 0.433 0.433
#> 2 0 0 1 1 7 0.857 0.857
#> 4 0 1 1 1 3 1.000 1.000
#> 6 1 0 1 1 3 1.000 1.000
#> 7 1 1 0 1 4 1.000 1.000
#> 8 1 1 1 1 13 1.000 1.000Six rows are listed. Rows 3 (TRU PRC SUP = 0 1 0) and 5
(1 0 0) are missing because no account has that combination: they are
the logical remainders. Five of the observed rows meet the consistency
cutoff (OUT = 1), and only the combination low on all three
conditions (row 1) does not.
Run the same OTS sweep three times, changing only
include and dir.exp.
thr2 <- c(TRU = 7, PRC = 7, SUP = 7)
cond2 <- c("TRU", "PRC", "SUP")
run2 <- function(...) {
otSweep(dat = demo2, outcome = "RNW", conditions = cond2, sweep_range = 5:8,
thrX = thr2, incl.cut = 0.8, n.cut = 1, ...)
}
res_cx <- run2() # complex
res_ps <- run2(include = "?") # parsimonious
res_im <- run2(include = "?", dir.exp = c(1, 1, 1)) # intermediate
data.frame(
thrY = 5:8,
complex = res_cx$summary$expression,
parsimonious = res_ps$summary$expression,
intermediate = res_im$summary$expression
)
#> thrY complex parsimonious intermediate
#> 1 5 SUP + TRU*PRC TRU + SUP SUP + TRU*PRC
#> 2 6 TRU*PRC + ~PRC*SUP TRU + ~PRC*SUP TRU*PRC + ~PRC*SUP
#> 3 7 TRU*SUP TRU*SUP TRU*SUP
#> 4 8 No solution No solution No solution(The chunk hides one warning, which the next section explains.)
How to read the comparison:
RNW >= 5 the complex solution is
SUP + TRU*PRC, while the parsimonious solution is
TRU + SUP. The term TRU*PRC was shortened to
TRU. That step is justified only by the assumption that the
unobserved combination TRU present, PRC and
SUP absent (row 5) would also be sufficient for renewal.
The parsimonious formula is shorter because it makes this assumption,
not because the data say more.dir.exp = c(1, 1, 1) a remainder is used only if it extends
an observed sufficient configuration by adding conditions whose presence
is expected to help. Neither remainder qualifies: each contains a single
condition, and no observed sufficient configuration is contained in it.
The intermediate solution therefore falls back on the complex one. In
other data sets the intermediate solution lies between the other
two.inclS 0.967 and covS 0.690 at
RNW >= 5), because the two remainders contain no cases.
Only the formulas differ, and with them the assumptions about cases that
were not observed. You can confirm this by printing
summary() for each of the three results.RNW >= 7 upward the three
types agree (TRU*SUP, then no solution at 8).| Solution | When it fits | Strength | Caution |
|---|---|---|---|
| Complex | Exploration; you want no assumptions about unobserved cases | Every claim rests on observed cases | Formulas can be long |
| Parsimonious | Checking which conditions survive maximal simplification | Shortest formulas | Relies on remainders that may be implausible |
| Intermediate | Theory-driven reporting | Uses only plausible remainders | Needs a justified dir.exp |
Ragin (2008) recommends the intermediate solution for reporting,
together with the parsimonious solution to show which conditions are
core. Whichever you choose, state the solution type,
incl.cut, n.cut and dir.exp in
your methods section. Because the sweep repeats the analysis at every
threshold, the chosen type applies to all rows.
Look again at the parsimonious result at RNW >= 5:
the table showed TRU + SUP, but the
n_solutions column of the sweep summary reads 2. The
shortening of TRU*PRC can go two ways: to TRU
(assuming row 5 is sufficient) or to PRC (assuming the
other remainder, PRC alone, is sufficient). Both choices
fit the observed data equally well, so QCA returns two equivalent
models. Sweeps signal this with a warning and with the
n_solutions column.
extract_modeThe argument extract_mode controls what the sweep table
shows.
extract_mode |
What the expression column contains |
|---|---|
"first" (default) |
Model M1 only. Check n_solutions to see whether others
exist. |
"all" |
All models, for example M1: ...; M2: ... |
"essential" |
Terms common to all models, plus extra columns for the rest |
res_all <- run2(include = "?", extract_mode = "all")
#> Warning: Multiple equivalent solutions exist for thrY = 5 (n_solutions > 1). Only the first
#> solution (M1) and its fit metrics are shown. Use generate_report() for full analysis.
summary(res_all)
#> OTS Summary
#> ===========
#>
#> Analysis Parameters:
#> Outcome: RNW
#> Conditions: TRU, PRC, SUP
#> Consistency cutoff: 0.8
#> Frequency cutoff: 1
#>
#> Results by Threshold:
#>
#> thrY expression inclS covS n_solutions
#> 5 M1: TRU + SUP; M2: PRC + SUP 0.967 0.690 2
#> 6 M1: TRU + ~PRC*SUP 0.963 0.788 1
#> 7 M1: TRU*SUP 0.875 0.667 1
#> 8 No solution NA NA 0
#>
#> Note: 1 threshold setting(s) had multiple solutions.
#> Use generate_report() for full details.At RNW >= 5 the two models are TRU + SUP
and PRC + SUP. The warning printed above says that fit
measures are those of M1. Here M2 has the same values
(inclS 0.967, covS 0.690), but in general you
should check each model.
res_ess <- run2(include = "?", extract_mode = "essential")
res_ess$summary[, c("thrY", "expression", "selective_terms", "unique_terms", "n_solutions")]
#> thrY expression selective_terms unique_terms n_solutions
#> 1 5 SUP TRU + PRC M1:TRU; M2:PRC 2
#> 2 6 TRU + ~PRC*SUP <NA> <NA> 1
#> 3 7 TRU*SUP <NA> <NA> 1
#> 4 8 No solution <NA> <NA> 0| Type | Definition | In this example (RNW >= 5) |
|---|---|---|
| Essential prime implicants | Present in every model | SUP |
| Selective prime implicants | Present in some but not all models | TRU, PRC |
| Unique terms | Present in only one model | M1: TRU; M2: PRC |
A careful report says that SUP appears in every
equivalent model, and that the second term is TRU or
PRC, depending on which unobserved combination is assumed
sufficient. Two cautions:
"essential" row the inclS and
covS values are those of the full model M1
(SUP + TRU), not of SUP alone. Alone,
SUP has consistency 0.962 and coverage 0.595.For the details of every model, write the full report:
In the full report, the section for RNW >= 5 lists
the number of solutions, both models, the essential and selective terms,
the unique terms and the raw QCA output. Its per-term table and
configuration chart follow M1, and a note says so.
Each sweep returns an object with three parts:
summary: the table you saw above (one row per threshold
setting).details: the full QCA results for every setting (truth
table, solution, fit measures). generate_report() reads
this part.params: the settings that were used, for
reproducibility.The columns of the summary table are:
| Column | Meaning |
|---|---|
thrY, threshold, thrX |
The threshold setting for that row |
expression |
The minimized solution (* is AND, + is OR,
~ is negation) |
inclS |
Solution consistency: how consistently the cases covered by the solution are also members of the outcome set |
covS |
Solution coverage: the share of the outcome set that the solution accounts for |
n_solutions |
Number of equivalent minimal solutions found |
Four points help avoid common misreadings.
incl.cut. Coverage says
how much of the outcome set is accounted for. It does not rank
solutions; use it to describe how much of the outcome the solution
explains.expression across rows. Rows with the same expression form
a stable region. A change of expression between adjacent rows marks a
threshold transition. A solution that changes with small threshold
shifts is less robust.covS
at LOY >= 5 and covS at
LOY >= 7 are proportions of different sets. A larger
covS in a later row does not mean that the solution has
become “better”. Likewise, covS is not the share of
customers or cases that a solution “captures” in the population.Changing a threshold changes which cases belong to the sets, so the sufficiency structure and its fit change with it. These tables describe that dependence. They do not show that manipulating a condition would change the outcome.
QCA usually analyses what is sufficient for the presence of the outcome. The absence of the outcome can be analysed too, for example the combinations sufficient for low loyalty or for non-renewal. Prefix the outcome with a tilde:
# Presence: cases with LOY >= threshold
res_pos <- otSweep(dat = demo, outcome = "LOY", conditions = c("QUA", "SER", "ENV"),
sweep_range = 4:6, thrX = c(QUA = 7, SER = 7, ENV = 7))
# Absence: cases with LOY < threshold
res_neg <- otSweep(dat = demo, outcome = "~LOY", conditions = c("QUA", "SER", "ENV"),
sweep_range = 4:6, thrX = c(QUA = 7, SER = 7, ENV = 7))In the solution of a ~ analysis, ~QUA means
that quality is below its threshold. The formulas for
LOY and ~LOY are not mirror images of each
other, so both need to be analysed and interpreted on their own.
All four sweep functions accept a ~ outcome. The stored
settings record it:
A sweep yields many rows, so decide in advance which parts go into the main text and which into a supplement.
Report enough for a reader to reproduce every row.
incl.cut,
n.cut, pri.cut, the solution type
(include) and any directional expectations
(dir.exp).generate_report() writes a Markdown file. The
"simple" format suits a main text or an appendix. The
"full" format includes truth tables and fit measures for
every setting, essential and selective terms, and configuration charts,
and suits supplementary material.
generate_report(res_ots, "ots_report_simple.md", dat = demo, format = "simple")
generate_report(res_ots, "ots_report_full.md", dat = demo, format = "full")Solution formulas in the report use your own outcome name (for
example -> LOY). Optional arguments control what is
included:
generate_report(res_ots, "r.md", dat = demo, include_chart = FALSE) # no charts
generate_report(res_ots, "r.md", dat = demo, chart_symbol_set = "latex") # LaTeX symbols
generate_report(res_ots, "r.md", dat = demo, include_raw_output = FALSE) # omit QCA outputThe report also ends with a short snippet of QCA code that reproduces a cell directly, which is the subject of the next subsection.
Reports contain Fiss-style configuration charts (conditions in rows, solution terms in columns). The chart functions can also be used on their own, starting from path strings:
paths <- c("A*B*~C", "A*D", "B*E")
cat(config_chart_from_paths(paths))
#> | Condition | T1 | T2 | T3 |
#> |:--:|:--:|:--:|:--:|
#> | A | ● | ● | |
#> | B | ● | | ● |
#> | C | ⊗ | | |
#> | D | | ● | |
#> | E | | | ● |
#>
#> *● = presence, ⊗ = absence, blank = don't care*Symbol sets are "unicode" (default),
"ascii" for maximum compatibility, and "latex"
for PDF output ($\bullet$ for presence,
$\otimes$ for absence).
cat(config_chart_from_paths(paths, symbol_set = "ascii"))
#> | Condition | T1 | T2 | T3 |
#> |:--:|:--:|:--:|:--:|
#> | A | O | O | |
#> | B | O | | O |
#> | C | X | | |
#> | D | | O | |
#> | E | | | O |
#>
#> *O = presence, X = absence, blank = don't care*When a threshold has several equivalent solutions, a chart with one block per solution is available:
solutions <- list(c("A*B", "C*D"), c("A*B", "C*E"))
cat(config_chart_multi_solutions(solutions))
#> **Note:** 2 equivalent solutions exist. Tables are shown separately below.
#>
#> ### Solution M1
#>
#> | Condition | T1 | T2 |
#> |:--:|:--:|:--:|
#> | A | ● | |
#> | B | ● | |
#> | C | | ● |
#> | D | | ● |
#>
#> ---
#>
#> ### Solution M2
#>
#> | Condition | T1 | T2 |
#> |:--:|:--:|:--:|
#> | A | ● | |
#> | B | ● | |
#> | C | | ● |
#> | E | | ● |
#>
#> *● = presence, ⊗ = absence, blank = don't care*Because ThSQCA calls the QCA package for every cell, any row can be
reproduced with QCA alone. The example reproduces the row
LOY >= 7 of the OTS sweep, in which all thresholds are
7.
bin <- function(x, t) as.integer(x >= t)
d7 <- data.frame(
LOY = bin(demo$LOY, 7), QUA = bin(demo$QUA, 7),
SER = bin(demo$SER, 7), ENV = bin(demo$ENV, 7)
)
tt <- truthTable(d7, outcome = "LOY", conditions = c("QUA", "SER", "ENV"),
incl.cut = 0.8, n.cut = 1, show.cases = FALSE)
sol <- minimize(tt)The solution matches the LOY >= 7 row of the sweep
(QUA*SER). Before you publish, do this for at least the
rows you discuss in the text, and compare the formulas, the
inclS and covS values and the number of models
(M1, M2, …). For solution types other than complex, pass the same
include and dir.exp to
minimize().
The following paragraph illustrates a careful description of the OTS result. It is written for the simulated data and should be adapted to your study.
We examined how the sufficient configurations for high loyalty change with the outcome threshold (LOY >= 5 to 8), holding the condition thresholds at 7 (consistency cutoff 0.8, frequency cutoff 1, complex solution). At LOY >= 5 and LOY >= 6, two configurations met the criterion: quality together with service, and quality together with store environment. At LOY >= 7, only the first remained. At LOY >= 8, no configuration met the consistency cutoff under these settings. These patterns describe how the sufficiency structure depends on the definition of the outcome; they are not evidence that improving any condition would raise loyalty.
Some habits make the wording safer:
Fiss (2011) refined QCA configuration tables by distinguishing two kinds of condition within a solution term:
| Type | Definition | Symbol |
|---|---|---|
| Core condition | Appears in both the parsimonious and the intermediate solutions | large filled symbol |
| Peripheral condition | Appears in the intermediate solution only | small symbol |
A condition that survives even the most aggressive simplification (the parsimonious solution) is more central to the configuration than one that appears only once theoretical expectations are applied. The four-symbol set is:
● = core condition present ⊗ = core condition absent
⊙ = peripheral condition present ⊘ = peripheral condition absent
(blank) = the condition does not matter
For LaTeX output the symbols are $\bullet$,
$\otimes$, $\odot$ and
$\oslash$.
compute_fiss_core() needs an intermediate-solution sweep
with the logical remainders enabled and the details stored:
include = "?",dir.exp specified,return_details = TRUE (the default).res_i <- otSweep(
dat = demo,
outcome = "LOY",
conditions = c("QUA", "SER", "ENV"),
sweep_range = 6:8,
thrX = c(QUA = 7, SER = 7, ENV = 7),
include = "?",
dir.exp = c(1, 1, 1)
)
# For every threshold, re-run QCA::minimize() without dir.exp to obtain the
# parsimonious solution, and compare it with the stored intermediate one.
res_fiss <- compute_fiss_core(res_i, conditions = c("QUA", "SER", "ENV"))
print_fiss_summary(res_fiss, thr_key = "7") # one threshold
cat(generate_fiss_chart(res_fiss, symbol_set = "unicode"))
cat(generate_fiss_chart(res_fiss, symbol_set = "latex"))
generate_report(res_fiss, "fiss_report.md", dat = demo, format = "full",
include_fiss_core = TRUE)print_fiss_summary() lists, term by term, which
conditions are core and which are peripheral. When the parsimonious and
intermediate solutions are identical, every condition is core and none
is peripheral.
Start small, then expand. Test a call with one threshold, then a short range, and only then the full grid. The number of QCA analyses grows quickly:
| Function | Number of analyses | Example |
|---|---|---|
otSweep() |
one per outcome threshold | 5 thresholds: 5 |
ctSweepS() |
one per threshold of the swept condition | 5 thresholds: 5 |
ctSweepM() |
product of the candidate counts | 3 x 3 x 3: 27 |
dtSweep() |
outcome thresholds x condition grid | 3 x (3 x 3 x 3): 81 |
A grid with five conditions and five candidates each has 3,125 cells; reduce the number of swept conditions first.
Why do I see a solution at one threshold but “No solution” at the next? Consistency depends on how the cases are classified. Near a threshold where several cases change membership at once, a configuration can drop below the consistency cutoff. Check the truth table of that row in the full report.
A solution appears only at the edge of my range.
Look at the number of cases behind it (see the SER >= 9
example above). Solutions built on very few cases are fragile.
The solution changes a lot across thresholds. What do I report? Report the whole sweep, and say where the structure is stable and where it changes. If you also give one headline result, choose its thresholds on substantive grounds before looking at the sweep, and state that the results are threshold-sensitive.
The sweep prints a warning about multiple solutions.
Equivalent models exist at one or more thresholds; see “Multiple
equivalent solutions”. Use extract_mode = "all" or
"essential" and generate_report().
Where to find help. Questions and bug reports are welcome at https://github.com/im-research-yt/ThSQCA/issues.
ThSQCA makes the threshold choices of a crisp-set QCA explicit. With the CTS, OTS and DTS sweeps you can see where a sufficiency structure is stable, where it changes, and where it disappears, and you can report this in a reproducible way. Because every cell is an ordinary QCA analysis, each result can be checked against the QCA package.
sessionInfo()
#> R version 4.6.1 (2026-06-24 ucrt)
#> Platform: x86_64-w64-mingw32/x64
#> Running under: Windows 11 x64 (build 26200)
#>
#> Matrix products: default
#> LAPACK version 3.12.1
#>
#> locale:
#> [1] LC_COLLATE=C LC_CTYPE=Japanese_Japan.utf8 LC_MONETARY=Japanese_Japan.utf8
#> [4] LC_NUMERIC=C 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.25.5 admisc_0.41 ThSQCA_2.0.7
#>
#> loaded via a namespace (and not attached):
#> [1] digest_0.6.39 R6_2.6.1 fastmap_1.2.0 xfun_0.60 cachem_1.1.0
#> [6] knitr_1.52 htmltools_0.5.9 rmarkdown_2.32 lifecycle_1.0.5 cli_3.6.6
#> [11] venn_1.13 declared_0.27 sass_0.4.10 jquerylib_0.1.4 compiler_4.6.1
#> [16] rstudioapi_0.19.0 tools_4.6.1 evaluate_1.0.5 bslib_0.12.0 yaml_2.3.12
#> [21] otel_0.2.0 jsonlite_2.0.0 rlang_1.3.0