Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 99 additions & 0 deletions docs/guide/sufficiency-diagnostics.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# Sufficiency diagnostics

Parameters of fit summarise a solution in a few numbers. They do not say which
cases produced those numbers — and that is usually the question you actually
have. Which cases support this path? Which contradict it? Which outcomes does it
fail to explain?

```python
from setqca import sufficiency_diagnostics

diagnostics = sufficiency_diagnostics(
data,
outcome="SURV",
terms=["DEV*URB*LIT*IND*STB", "DEV*~URB*LIT*~IND*STB"],
)
print(diagnostics)
print(diagnostics.to_frame())
print(diagnostics.cases_frame())
```

Terms are given as expression strings and parsed, so a solution can be pasted
straight in. Case labels come from the frame index by default, or from a column
you name — no particular schema is assumed.

## The case typology

For a term `X` and outcome `Y`, with the crossover at 0.5:

| Membership | Role | What it means |
| --- | --- | --- |
| `X > 0.5`, `Y > 0.5`, `X ≤ Y` | **typical** | Supports the claim. These are the cases to study for the mechanism. |
| `X > 0.5`, `Y > 0.5`, `X > Y` | **deviant consistency (degree)** | Right corner, wrong magnitude — more in the term than in the outcome. |
| `X > 0.5`, `Y ≤ 0.5` | **deviant consistency (kind)** | The term holds and the outcome does not. This is the case-level contradiction. |
| `X ≤ 0.5`, `Y > 0.5` | **deviant coverage** | An outcome this term does not explain. |
| `X ≤ 0.5`, `Y ≤ 0.5` | **individually irrelevant** | Outside both sets. |

```python
term = diagnostics.terms[0]
term.typical # ('BE', 'CZ', 'NL')
term.contradictory # cases where the term holds but the outcome does not
term.deviant_coverage # outcomes this term misses
term.deviant_consistency # both kinds of consistency deviance
term.uniquely_covered # cases no other term reaches
```

!!! note "Only consistency deviance counts against the claim"
A deviant-coverage case is not evidence against sufficiency. It says the
outcome occurred through some other path, which is exactly what a
disjunctive solution expects. `CaseRole.contradicts_sufficiency` encodes
the distinction.

## Unique coverage

Raw coverage counts the outcome membership a term accounts for. **Unique**
coverage counts only what no other term accounts for:

```text
covU_i = [ Σ min(Xᵢ, Y) − Σ min(Xᵢ, max_{j≠i} Xⱼ, Y) ] / Σ Y
```

A term with substantial raw coverage but near-zero unique coverage is redundant
in practice — drop it and the same cases are still explained:

```python
diagnostics.redundant_terms
```

Two identical terms each have unique coverage of exactly zero, which is the
degenerate case the property makes obvious.

!!! info "A small divergence from R"
R reports `covU` as `NA` for a single-term solution, since there is no other
term to be unique against. `setqca` reports the raw coverage instead: with
nothing to share with, everything the term covers is uniquely covered by it.
Verified against R for every multi-term solution on the Lipset data.

## Reading R's `cases` column

R's per-term `cases` column lists cases whose membership in the term exceeds the
crossover. The typology splits that same set further, so R's list corresponds to
**typical plus deviant-in-degree**, not to typical alone.

On the Lipset conservative solution R lists `BE, CZ, NL, UK` for the first term.
`setqca` agrees on all four being in the term, and additionally reports that UK
is deviant in degree — its membership in the term exceeds its membership in the
outcome. That distinction is the point of the typology, and it is not visible
from the `cases` column alone.

## Choosing cases to study

The typology exists to support case selection in multi-method work:

- **Typical** cases are where the proposed mechanism should be visible.
- **Deviant consistency** cases are where it should be visible and is not — the
most informative cases for revising the theory.
- **Deviant coverage** cases point at paths the solution is missing.
- **Uniquely covered** cases are the ones that justify keeping a term at all.

::: setqca.analysis.sufficiency
11 changes: 6 additions & 5 deletions docs/mathematical_validation.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ run over all cases.
| Necessity consistency | `Σ min(X,Y) / Σ Y` | `metrics.necessity` | `TestNecessity` | `1e-9` vs R | ✅ Verified |
| Necessity coverage | `Σ min(X,Y) / Σ X` | `metrics.necessity` | `TestNecessity` | `1e-9` vs R | ✅ Verified |
| Relevance of necessity | `Σ (1−X) / Σ (1 − min(X,Y))` | `metrics.necessity` | `TestNecessity` | `1e-9` vs R | ✅ Verified |
| Unique coverage | `cov(Tᵢ) − cov(⋃ⱼ≠ᵢ Tⱼ)` | — | — | — | ❌ Not implemented |
| Unique coverage | `[Σ min(Xᵢ,Y) − Σ min(Xᵢ, max_{j≠i} Xⱼ, Y)] / Σ Y` | `analysis.sufficiency` | `test_sufficiency_diagnostics`, parity | `1e-9` vs R | ✅ Verified |
| Case typology | crossover comparison of `X` and `Y` | `analysis.sufficiency` | `test_sufficiency_diagnostics` | exact | ✅ Tested |
| Trivial necessity | `RoN` below threshold with high consistency | `analysis.necessity` | `test_necessity`, parity | `1e-9` vs R | ✅ Verified |
| SUIN disjunction | `consistency(A+B) ≥ max over parts` | `analysis.necessity` | `test_necessity` | `1e-12` | ✅ Tested |
| Direct calibration, logistic | see below | `calibration.DirectCalibration` | `TestCalibration`, parity | `1e-9` vs R | ✅ Verified |
Expand Down Expand Up @@ -111,10 +112,10 @@ poison downstream aggregation.

## Findings

1. **Unique coverage is absent.** Raw coverage is implemented and verified; the
per-term unique coverage reported by other QCA software is not. Solutions
currently expose overall and per-term fit through `FittedSolution.term_fits`,
which is raw coverage per term. This is a gap, not a divergence.
1. **Unique coverage is implemented and verified**, closing the gap this audit
first recorded. It matches R's `covU` for every multi-term solution on the
Lipset data. R leaves `covU` undefined for a one-term solution; setqca
reports the raw coverage there, since there is no other term to share with.
2. **Intermediate solutions now follow the standard algorithm.** Simplifying
assumptions are derived from the parsimonious solution and split into easy
and difficult counterfactuals, matching R `QCA` on the Lipset data.
Expand Down
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ nav:
- Calibration: guide/calibration.md
- Expressions: guide/expressions.md
- Necessity: guide/necessity.md
- Sufficiency diagnostics: guide/sufficiency-diagnostics.md
- Truth tables: guide/truth-tables.md
- Minimisation: guide/minimisation.md
- Methodology: METHODOLOGY.md
Expand Down
16 changes: 15 additions & 1 deletion src/setqca/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,16 @@
from importlib.metadata import PackageNotFoundError
from importlib.metadata import version as _version

from .analysis import NecessityAnalysis, NecessityCandidate, necessity_analysis
from .analysis import (
CaseDiagnostic,
CaseRole,
NecessityAnalysis,
NecessityCandidate,
SolutionDiagnostics,
TermDiagnostics,
necessity_analysis,
sufficiency_diagnostics,
)
from .calibration import DirectCalibration, calibrate_crisp, calibrate_direct
from .counterfactuals import (
CounterfactualAnalysis,
Expand Down Expand Up @@ -50,6 +59,8 @@
"CSQCA",
"FSQCA",
"BooleanSolution",
"CaseDiagnostic",
"CaseRole",
"Condition",
"Configuration",
"CounterfactualAnalysis",
Expand All @@ -71,7 +82,9 @@
"PrimeImplicantChart",
"QCAResult",
"SetExpression",
"SolutionDiagnostics",
"SufficiencyFit",
"TermDiagnostics",
"TruthCode",
"TruthTable",
"TruthTableRow",
Expand All @@ -90,4 +103,5 @@
"parse_expression",
"simplify_expression",
"sufficiency",
"sufficiency_diagnostics",
]
14 changes: 14 additions & 0 deletions src/setqca/analysis/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,23 @@
NecessityCandidate,
necessity_analysis,
)
from .sufficiency import (
CaseDiagnostic,
CaseRole,
SolutionDiagnostics,
TermDiagnostics,
classify_case,
sufficiency_diagnostics,
)

__all__ = [
"CaseDiagnostic",
"CaseRole",
"NecessityAnalysis",
"NecessityCandidate",
"SolutionDiagnostics",
"TermDiagnostics",
"classify_case",
"necessity_analysis",
"sufficiency_diagnostics",
]
Loading
Loading