ThSQCA Tutorial (English)

Quick start (5 minutes)

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   5

Now 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           0

Read the table from top to bottom. Each row is one complete QCA analysis:

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.

What ThS-QCA does, and what it does not do

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() and QCA::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.

Preparing your data

ThS-QCA works on raw scores and binarizes them itself, so no calibration step is needed before a sweep.

Binary (0/1) variables

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:

sapply(dat[, c("X1", "X2", "X3")], function(x) all(x %in% c(0, 1)))

Conditions that are already fuzzy sets (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:

Sweeping membership scores

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:

The four sweeps

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.

OTS: sweep the outcome threshold (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           0

How 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.

CTS (single): sweep one condition threshold (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           1

How to read it.

sum(demo$QUA >= 7 & demo$SER >= 9)
#> [1] 16

CTS (multiple): sweep several condition thresholds (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           1

How to read it. With 27 rows, look for regions rather than single rows.

DTS: sweep outcome and condition thresholds together (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           0

How to read it. There are 24 cells (8 condition combinations by 3 outcome thresholds).

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).

Choosing a solution type

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   5

The truth table has remainders

Take 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.000

Six 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.

Three solutions from the same data

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:

Which solution type should I report?

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.

Multiple equivalent solutions

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_mode

The 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

Essential, selective and unique terms

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:

For the details of every model, write the full report:

generate_report(res_all, "ots_multiple_report.md", dat = demo2, format = "full")

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.

Understanding the output

Each sweep returns an object with three parts:

names(res_ots)
#> [1] "summary" "details" "params"

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.

  1. Consistency comes first. A consistency of at least 0.8 is a common minimum for calling a configuration sufficient, and it is the default cutoff you set with 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.
  2. Stable versus changing. Compare 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.
  3. “No solution” is a technical result. It means that, under the analysis settings you chose (consistency cutoff, frequency cutoff and the thresholds of that row), no configuration met the sufficiency criterion. It does not mean that no route to the outcome exists.
  4. Coverage is relative to a changing set. When the outcome threshold rises, the outcome set shrinks, so 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.

Negated outcomes

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:

res_neg$params$negate_outcome   # TRUE
res_neg$params$outcome          # "~LOY"

Reporting your results

A sweep yields many rows, so decide in advance which parts go into the main text and which into a supplement.

What to report

Report enough for a reader to reproduce every row.

c(R = R.version.string,
  QCA = as.character(packageVersion("QCA")),
  ThSQCA = as.character(packageVersion("ThSQCA")))
#>                                   R                                 QCA 
#> "R version 4.6.1 (2026-06-24 ucrt)"                            "3.25.5" 
#>                              ThSQCA 
#>                             "2.0.7"

Generating the report

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 output

The report also ends with a short snippet of QCA code that reproduces a cell directly, which is the subject of the next subsection.

Configuration charts

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*

Check a cell directly with QCA

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)
sol
#> 
#> M1: QUA*SER -> LOY

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().

Example wording

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:

Core and peripheral conditions (Fiss, 2011)

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:

  1. include = "?",
  2. dir.exp specified,
  3. 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.

Practical tips and frequently asked questions

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.

Conclusion

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.

References

Session info

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