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?
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. |
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
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:
A term with substantial raw coverage but near-zero unique coverage is redundant in practice — drop it and the same cases are still explained:
Two identical terms each have unique coverage of exactly zero, which is the degenerate case the property makes obvious.
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 ¶
Case-level diagnostics for a sufficiency solution.
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 a researcher actually has: which cases support this path, which contradict it, and which outcomes does it fail to explain.
Case typology
For a term X and outcome Y, with the crossover at 0.5
(Schneider and Rohlfing 2013):
X > 0.5,Y > 0.5,X <= Y: typical, supporting the claim.X > 0.5,Y > 0.5,X > Y: deviant for consistency in degree, the right corner at the wrong magnitude.X > 0.5,Y <= 0.5: deviant for consistency in kind. The term holds and the outcome does not; this is the case-level contradiction.X <= 0.5,Y > 0.5: deviant for coverage, an outcome this term does not explain.X <= 0.5,Y <= 0.5: individually irrelevant, outside both sets.
Unique coverage
Raw coverage counts outcome membership a term accounts for. Unique coverage counts only what no other term accounts for::
covU_i = [ sum(min(Xi, Y)) - sum(min(Xi, max_over_others(Xj), Y)) ] / sum(Y)
A term with substantial raw coverage but near-zero unique coverage is redundant in practice: drop it and the solution still explains the same cases.
References
Schneider, C. Q. and Rohlfing, I. (2013). Combining QCA and process tracing in set-theoretic multi-method research. Sociological Methods & Research 42(4), 559-597.
CaseRole ¶
Bases: Enum
Where a case sits relative to one sufficiency claim.
contradicts_sufficiency
property
¶
Return whether this role counts against the sufficiency claim.
CaseDiagnostic
dataclass
¶
CaseDiagnostic(
case: str,
term_membership: float,
outcome_membership: float,
role: CaseRole,
uniquely_covered: bool,
)
One case, judged against one term.
TermDiagnostics
dataclass
¶
TermDiagnostics(
expression: str,
fit: SufficiencyFit,
unique_coverage: float,
frequency: int,
cases: tuple[CaseDiagnostic, ...],
)
One solution term, its fit, and every case's relation to it.
deviant_consistency
property
¶
Return cases contradicting the claim, in kind or in degree.
contradictory
property
¶
Return cases where the term holds but the outcome does not.
deviant_coverage
property
¶
Return outcome cases this term does not reach.
uniquely_covered
property
¶
Return cases this term covers that no other term does.
redundant
property
¶
Return whether the term adds no coverage another term does not already give.
SolutionDiagnostics
dataclass
¶
SolutionDiagnostics(
outcome: str,
terms: tuple[TermDiagnostics, ...],
fit: SufficiencyFit,
)
Diagnostics for a whole disjunctive solution.
redundant_terms
property
¶
redundant_terms: tuple[TermDiagnostics, ...]
Return terms contributing no unique coverage.
to_frame ¶
Return one row per term, with fit and case counts.
Returns:
| Type | Description |
|---|---|
DataFrame
|
Columns |
Source code in src/setqca/analysis/sufficiency.py
cases_frame ¶
Return one row per case per term, for case-oriented work.
Returns:
| Type | Description |
|---|---|
DataFrame
|
Columns |
Source code in src/setqca/analysis/sufficiency.py
classify_case ¶
classify_case(
term_membership: float, outcome_membership: float
) -> CaseRole
Classify one case against one sufficiency claim.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
term_membership
|
float
|
Membership of the case in the term. |
required |
outcome_membership
|
float
|
Membership of the case in the outcome. |
required |
Returns:
| Type | Description |
|---|---|
CaseRole
|
The case's role, per the typology in the module docstring. |
Source code in src/setqca/analysis/sufficiency.py
sufficiency_diagnostics ¶
sufficiency_diagnostics(
data: DataFrame,
*,
outcome: str,
terms: Sequence[str | SetExpression],
case_id: str | None = None,
) -> SolutionDiagnostics
Diagnose a disjunctive sufficiency solution case by case.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
DataFrame
|
Calibrated memberships in |
required |
outcome
|
str
|
Name of the outcome column. |
required |
terms
|
sequence of str or SetExpression
|
The solution's terms. Strings are parsed, so
|
required |
case_id
|
str
|
Column holding case labels. Defaults to the frame index, so no particular schema is assumed. |
None
|
Returns:
| Type | Description |
|---|---|
SolutionDiagnostics
|
Per-term fit including unique coverage, and every case's role. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If no terms are given, or the data are not calibrated. |
KeyError
|
If a named column is absent. |
Examples:
>>> diagnostics = sufficiency_diagnostics(
... data, outcome="SURV", terms=["DEV*URB*LIT*IND*STB"]
... )
>>> diagnostics.terms[0].typical
('BE', 'CZ', 'NL', 'UK')
Source code in src/setqca/analysis/sufficiency.py
279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 | |