---
title: "ThSQCA Tutorial (English)"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{ThSQCA Tutorial (English)}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r setup, include=FALSE}
knitr::opts_chunk$set(
  collapse = TRUE,
  comment = "#>"
)
options(width = 100)
```

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

```{r data}
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)
```

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.

```{r quick-ots, results="hide"}
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
)
```

```{r quick-summary}
summary(res)
```

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

- At `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`).
- At `LOY >= 7`, only `QUA*SER` remains.
- At `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.

# 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](https://im-research-yt.github.io/ThSQCA/tools/sweep-builder.html)
> 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.

- Any numeric data can be used: continuous, interval, or ordinal scales (for
  example 0 to 10 ratings or 1 to 7 Likert items), and membership scores
  between 0 and 1.
- A case belongs to a set when its score is **greater than or equal to** the
  threshold (`x >= threshold`). Thresholds can be any real number.
- Handle missing values before you run 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.

```{r binary-example, eval=FALSE}
# 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:

```{r binary-check, eval=FALSE}
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.

```{r pre-cal, eval=FALSE}
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:

- Values must lie in [0, 1]; otherwise the function stops with an error.
- A variable listed in `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.
- The QCA package advises against memberships of exactly 0.5. ThSQCA warns once
  per sweep, naming the variable, if a pre-calibrated variable contains them.
  Choose a calibration whose crossover point does not coincide with observed
  values.

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

```{r sweep-membership, eval=FALSE}
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:

- With `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.
- With a sweep, each threshold turns the membership scores into a crisp set, so
  the membership criterion itself becomes the object of the sensitivity
  analysis. Thresholds on the raw scale are read in the units of the measure
  ("a service score of at least 7"); thresholds on the membership scale are read
  as degrees of membership ("membership of at least 0.6").
- A case whose score equals the threshold counts as a member (the rule is
  `>=`). 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.

# 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`.

```{r ots, results="hide"}
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
)
```

```{r ots-summary}
summary(res_ots)
```

**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".

```{r cts-s, results="hide"}
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
)
```

```{r cts-s-summary}
summary(res_cts)
```

**How to read it.**

- With a lenient criterion for service (`SER >= 5` or `SER >= 6`), no
  configuration reaches the consistency cutoff.
- From `SER >= 7`, the solution is `QUA*SER`, and its consistency stays close
  to 0.9.
- Coverage (`covS`) falls as the criterion tightens (0.656, 0.333, 0.122),
  because fewer cases satisfy the condition.
- At `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.

```{r cts-s-sparse}
sum(demo$QUA >= 7 & demo$SER >= 9)
```

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

```{r cts-m, results="hide"}
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
)
```

```{r cts-m-summary}
summary(res_mcts)
```

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

- When `QUA = 6`, every cell reports "No solution".
- Whenever `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.
- Solutions containing `~ENV` or `QUA*ENV` appear only in cells with
  `QUA = 8`, the strictest quality criterion in this grid.

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

```{r dts, results="hide"}
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
)
```

```{r dts-summary}
summary(res_dts)
```

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

- At `LOY >= 6`, every one of the 8 condition combinations gives a solution.
- At `LOY >= 7`, a solution exists only when `QUA = 7` and `SER = 7`.
- At `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).

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

```{r data2}
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)
```

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

```{r tt2}
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
```

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

```{r types, warning=FALSE}
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
)
```

(The chunk hides one warning, which the next section explains.)

How to read the comparison:

- **Complex versus parsimonious.** At `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.
- **Intermediate equals complex here.** With `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.
- **Same fit, different claims.** The fit measures are identical across the
  three types at every threshold (for example `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.
- **From `RNW >= 7` upward** the three types agree (`TRU*SUP`, then no
  solution at 8).

## 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 |

```{r multi-all}
res_all <- run2(include = "?", extract_mode = "all")
summary(res_all)
```

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.

```{r multi-essential, warning=FALSE}
res_ess <- run2(include = "?", extract_mode = "essential")
res_ess$summary[, c("thrY", "expression", "selective_terms", "unique_terms", "n_solutions")]
```

## 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:

- In the `"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.
- Do not choose between M1 and M2 by their order in the output. When the models
  fit equally well, the data cannot separate them; theory or case knowledge has
  to.

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

```{r multi-report, eval=FALSE}
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:

```{r str-result}
names(res_ots)
```

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

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:

```{r negated, eval=FALSE}
# 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:

```{r negated-params, eval=FALSE}
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.

- The **threshold space**: the candidate values for each swept threshold, and
  the fixed values for the others.
- The **QCA settings**: `incl.cut`, `n.cut`, `pri.cut`, the solution type
  (`include`) and any directional expectations (`dir.exp`).
- The **software versions**: R, QCA and ThSQCA.
- The **summary table** of the sweep (main text), and the full report
  (supplement).

```{r versions}
c(R = R.version.string,
  QCA = as.character(packageVersion("QCA")),
  ThSQCA = as.character(packageVersion("ThSQCA")))
```

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

```{r report, eval=FALSE}
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:

```{r report-options, eval=FALSE}
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:

```{r chart}
paths <- c("A*B*~C", "A*D", "B*E")
cat(config_chart_from_paths(paths))
```

Symbol sets are `"unicode"` (default), `"ascii"` for maximum compatibility, and
`"latex"` for PDF output (`$\bullet$` for presence, `$\otimes$` for absence).

```{r chart-ascii}
cat(config_chart_from_paths(paths, symbol_set = "ascii"))
```

When a threshold has several equivalent solutions, a chart with one block per
solution is available:

```{r chart-multi}
solutions <- list(c("A*B", "C*D"), c("A*B", "C*E"))
cat(config_chart_multi_solutions(solutions))
```

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

```{r verify, results="hide"}
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)
```

```{r verify-print}
sol
```

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:

- Describe **sufficiency structures** at each threshold, not effects.
- Report "no solution" as a result **under stated settings**.
- Do not generalize from the path counts of a few rows (the number of
  configurations need not fall steadily as a threshold rises).
- State coverage as a proportion of the **outcome set** at that threshold.
- When equivalent solutions exist, report all of them or the essential terms,
  not only M1.

# Core and peripheral conditions (Fiss, 2011)

Fiss (2011) refined QCA configuration tables by distinguishing two kinds of
condition within a configuration (a term of the intermediate solution):

| Type | Definition | Symbol |
|------|------------|--------|
| **Core condition** | Also part of the parsimonious solution | large symbol |
| **Peripheral condition** | Eliminated in the parsimonious solution, so it appears in the intermediate solution only | small symbol |

A condition that survives even the most aggressive simplification (the
parsimonious solution) rests on stronger evidence 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
```

**How ThSQCA applies the definition.** In Fiss's own solution tables the
classification is made configuration by configuration: solutions are grouped
by their core conditions, and the same condition can be core in one
configuration and peripheral in another. `compute_fiss_core()` follows this.
For each term of the intermediate solution it looks for the parsimonious
terms contained in it (every condition of the parsimonious term has the same
status in the intermediate term). The conditions of those parsimonious terms
are core; the other conditions of the term are peripheral. With the
parsimonious solution `~A*E + A*B`, for example, the intermediate term
`~A*~B*C*E` contains `~A*E`, so `~A` and `E` are core, while `~B` and `C`
are peripheral even though `B` appears in the other parsimonious term.

Two situations are not covered by Fiss (2011). ThSQCA treats both
conservatively and says so in a warning:

- **Tied parsimonious solutions.** `QCA::minimize()` records which
  parsimonious solution each intermediate solution was derived from. If the
  reported intermediate solution comes from one of several tied parsimonious
  solutions, only that one is used. If it comes from several of them, a
  condition is core only when it is core relative to each. In the second
  data set at `RNW >= 5`, the intermediate solution `SUP + TRU*PRC` is
  derived both from `TRU + SUP` and from `PRC + SUP`; `TRU` would be core
  relative to the first and `PRC` relative to the second, so both are
  reported as peripheral and only `SUP` is core.
- **No contained parsimonious term.** An intermediate term can be covered
  by the parsimonious solution as a whole without containing any single
  parsimonious term. All its conditions are then reported as peripheral.

If you want to report core and peripheral conditions relative to one
particular parsimonious solution, state that choice in the text.

For LaTeX output the symbols are `$\bullet$`, `$\otimes$`, `$\odot$` and
`$\oslash$`.

`compute_fiss_core()` works on results of `otSweep()` and `ctSweepS()`
(results of `ctSweepM()` and `dtSweep()` are not supported yet). It 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).

```{r fiss, eval=FALSE}
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, compare each term of the stored intermediate solution
# with the parsimonious term(s) contained in it.
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. For the second data set at
`RNW >= 6`, the parsimonious solution is `TRU + ~PRC*SUP` and the intermediate
solution `TRU*PRC + ~PRC*SUP`: in `TRU*PRC`, `TRU` is core and `PRC`
peripheral; in `~PRC*SUP`, both conditions are core.

# 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

- Fiss, P. C. (2011). Building better causal theories: A fuzzy set approach to
  typologies in organization research. *Academy of Management Journal*, 54(2),
  393-420. DOI: [10.5465/amj.2011.60263120](https://doi.org/10.5465/amj.2011.60263120)
- Ragin, C. C. (2008). *Redesigning Social Inquiry: Fuzzy Sets and Beyond*.
  University of Chicago Press. DOI: [10.7208/chicago/9780226702797.001.0001](https://doi.org/10.7208/chicago/9780226702797.001.0001)
- Dusa, A. (2019). *QCA with R: A Comprehensive Resource*. Springer. DOI:
  [10.1007/978-3-319-75668-4](https://doi.org/10.1007/978-3-319-75668-4)
- Oana, I.-E., & Schneider, C. Q. (2024). A robustness test protocol for applied
  QCA: Theory and R software application. *Sociological Methods & Research*,
  53(1), 57-88. DOI: [10.1177/00491241211036158](https://doi.org/10.1177/00491241211036158)
- Toyoda, Y. (2026). ThSQCA: Reproducible threshold-sweep workflows for QCA in
  R. *SocArXiv*. DOI: [10.31235/osf.io/yb8xs_v1](https://doi.org/10.31235/osf.io/yb8xs_v1)

# Session info

```{r session-info}
sessionInfo()
```
