Robustness¶
A QCA solution is conditional on decisions the data do not make for you: where the calibration anchors sit, how consistent a row must be to count as sufficient, how many cases a row needs. Reporting one solution from one set of those choices hides how much of the result was the choice rather than the evidence.
from setqca import RobustnessGrid, robustness_analysis
analysis = robustness_analysis(
data,
outcome="SURV",
conditions=["DEV", "URB", "LIT"],
grid=RobustnessGrid(
consistency=[0.75, 0.80, 0.85, 0.90],
pri=[0.50, 0.60, 0.70],
frequency=[1, 2],
),
)
print(analysis)
print(analysis.to_frame())
What comes back¶
Robustness of the conservative solution
Specifications: 24 (21 produced a solution, 3 did not)
Baseline: cons=0.85, pri=0.6, n=1
Stable terms (1):
DEV*LIT*STB — 21/21 specifications
Threshold-sensitive terms (2):
DEV*URB — 6/21 specifications
URB*STB — 4/21 specifications
Baseline terms that do not survive: URB*STB
Stability is not validity: a mis-specified model can be perfectly stable.
Four buckets, each answering a different question:
| Accessor | Question |
|---|---|
stable_terms() |
Which paths survive nearly every cutoff? |
fragile_terms() |
Which appear only under some? |
disappearing_terms() |
Which baseline paths do not survive? |
emerging_terms() |
Which stable paths does the baseline miss? |
The threshold defaults to 0.8 and is adjustable on each call.
Failures are recorded, not dropped
A specification that produces no solution gets a row with a failure
message and NaN fit, rather than vanishing. "The model collapses above
0.9" is a finding about your data, and silently omitting those rows would
make the surviving ones look more robust than they are.
Sweeping calibration anchors¶
Calibration is where substantive judgement enters, so it is also where a result
is most easily manufactured. calibration_robustness recalibrates from the raw
measures for each anchor combination:
from setqca.analysis.robustness import calibration_robustness
analysis = calibration_robustness(
raw_data,
outcome="SURV",
conditions=["DEV", "URB"],
grid=RobustnessGrid(
consistency=[0.80],
anchors={"DEV": [(10, 50, 90), (20, 50, 80), (10, 40, 90)]},
),
outcome_anchors=(10, 50, 90),
base_anchors={"URB": (10, 50, 90)},
)
The input is raw, so every condition needs anchors from one source or the
other — swept in the grid, or fixed through base_anchors. Passing calibrated
data to this function, or a grid with anchors to robustness_analysis, is an
error rather than a silent mis-read.
Comparing solutions¶
Textual identity is the strictest comparison and often the least informative. Four scales are available:
from setqca.analysis.robustness import solution_similarity
similarity = solution_similarity(left_terms, right_terms, data)
similarity.identical # exact set equality
similarity.term_overlap # Jaccard over terms
similarity.configurational # Jaccard over the literals used
similarity.membership # fuzzy Jaccard over case membership
The last is the one that catches agreement the text hides: two solutions can be
written differently and still select the same cases. A and A+A*B are
textually distinct and have membership similarity 1.0, because the second term
adds nothing.
Robustness is not validity¶
A path that appears under every threshold is stable, not true.
Stability says the finding does not depend on one arbitrary cutoff. It says nothing about whether the conditions are causally relevant, whether the calibration was substantively sensible, whether the cases were well chosen, or whether an omitted condition is doing the work. A thoroughly mis-specified model can be perfectly stable — sweeping thresholds cannot detect a problem that lives in the model rather than the cutoffs.
Nothing in this module reports a verdict. The measures are descriptive; the interpretation is yours.
setqca.analysis.robustness ¶
Sensitivity of a QCA result to the choices that produced it.
A QCA solution is conditional on decisions the data do not make for you: where the calibration anchors sit, how consistent a row must be to count as sufficient, how many cases a row needs. Reporting one solution from one set of those choices hides how much of the result was the choice rather than the evidence.
This module runs the analysis across a grid of those choices and reports which paths survive.
What robustness is not
A path that appears under every threshold is stable, not true. Stability says the finding does not depend on one arbitrary cutoff. It says nothing about whether the conditions are causally relevant, whether the calibration was substantively sensible, or whether the case selection was sound. A thoroughly mis-specified model can be perfectly stable.
Nothing here reports a verdict. The measures are descriptive, and the interpretation is the researcher's.
Specification
dataclass
¶
Specification(
consistency: float,
pri: float,
frequency: int,
anchors: tuple[
tuple[str, tuple[float, float, float]], ...
] = (),
)
One combination of analytical choices.
RobustnessGrid
dataclass
¶
RobustnessGrid(
consistency: Sequence[float] = (0.75, 0.8, 0.85),
pri: Sequence[float] = (0.0,),
frequency: Sequence[int] = (1,),
anchors: Mapping[
str, Sequence[tuple[float, float, float]]
] = dict(),
)
The analytical choices to sweep.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
consistency
|
sequence of float
|
Inclusion and PRI cutoffs to try. |
(0.75, 0.8, 0.85)
|
pri
|
sequence of float
|
Inclusion and PRI cutoffs to try. |
(0.75, 0.8, 0.85)
|
frequency
|
sequence of int
|
Frequency cutoffs to try. |
(1,)
|
anchors
|
mapping of str to sequence of (float, float, float)
|
Alternative calibration anchors per condition, as
|
dict()
|
specifications ¶
specifications() -> Iterator[Specification]
Yield every combination in the grid, in a deterministic order.
Source code in src/setqca/analysis/robustness.py
RobustnessRun
dataclass
¶
RobustnessRun(
specification: Specification,
terms: frozenset[str],
consistency: float,
coverage: float,
implicants: int,
literals: int,
solutions: int,
failure: str | None = None,
)
The outcome of one specification.
A specification that produces no solution is recorded rather than dropped: "the model collapses above 0.85" is itself a finding.
SolutionSimilarity
dataclass
¶
SolutionSimilarity(
identical: bool,
term_overlap: float,
configurational: float,
membership: float,
)
Several ways two solutions can resemble each other.
Attributes:
| Name | Type | Description |
|---|---|---|
identical |
bool
|
The two term sets are equal. |
term_overlap |
float
|
Jaccard index over term sets: exact string agreement. |
configurational |
float
|
Jaccard index over the literals used, so solutions that differ in how terms are cut but use the same conditions still score highly. |
membership |
float
|
Fuzzy Jaccard over case membership in the solution,
|
RobustnessAnalysis
dataclass
¶
RobustnessAnalysis(
grid: RobustnessGrid,
runs: tuple[RobustnessRun, ...],
baseline: Specification,
family: str,
data: DataFrame,
)
The result of sweeping a grid of analytical choices.
failed
property
¶
failed: tuple[RobustnessRun, ...]
Return specifications under which the model produced nothing.
baseline_terms
property
¶
Return the terms of the baseline specification, if it succeeded.
term_stability ¶
term_stability() -> tuple[TermStability, ...]
Return every term seen, with how often it survived.
Source code in src/setqca/analysis/robustness.py
stable_terms ¶
Return terms appearing in at least threshold of successful runs.
fragile_terms ¶
Return terms that appear, but in fewer than threshold of runs.
Source code in src/setqca/analysis/robustness.py
disappearing_terms ¶
Return baseline terms that do not survive the sweep.
Source code in src/setqca/analysis/robustness.py
emerging_terms ¶
Return stable terms the baseline did not report.
Source code in src/setqca/analysis/robustness.py
similarity_to_baseline ¶
similarity_to_baseline() -> tuple[
tuple[Specification, SolutionSimilarity], ...
]
Compare every successful run against the baseline solution.
Source code in src/setqca/analysis/robustness.py
to_frame ¶
Return one row per specification.
Returns:
| Type | Description |
|---|---|
DataFrame
|
Columns |
Source code in src/setqca/analysis/robustness.py
solution_similarity ¶
solution_similarity(
left: frozenset[str],
right: frozenset[str],
data: DataFrame,
) -> SolutionSimilarity
Compare two solutions on four scales, from strictest to loosest.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
left
|
frozenset of str
|
Solution terms, in standard QCA notation. |
required |
right
|
frozenset of str
|
Solution terms, in standard QCA notation. |
required |
data
|
DataFrame
|
Calibrated data, used for the membership comparison. |
required |
Returns:
| Type | Description |
|---|---|
SolutionSimilarity
|
Exact identity, term overlap, configurational overlap and membership agreement. |
Source code in src/setqca/analysis/robustness.py
robustness_analysis ¶
robustness_analysis(
data: DataFrame,
*,
outcome: str,
conditions: Sequence[str],
grid: RobustnessGrid | None = None,
family: str = "conservative",
directional_expectations: Mapping[str, Direction]
| None = None,
case_id: str | None = None,
) -> RobustnessAnalysis
Sweep truth-table thresholds and report which paths survive.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
DataFrame
|
Calibrated memberships in |
required |
outcome
|
str
|
Name of the outcome column. |
required |
conditions
|
sequence of str
|
Condition columns. |
required |
grid
|
RobustnessGrid
|
Choices to sweep. Defaults to three consistency cutoffs. |
None
|
family
|
str
|
Which solution family to track. |
"conservative"
|
directional_expectations
|
mapping
|
Required when |
None
|
case_id
|
str
|
Column holding case labels. |
None
|
Returns:
| Type | Description |
|---|---|
RobustnessAnalysis
|
Every specification's result, with term stability across the sweep. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the grid specifies calibration anchors, which need raw data — use
:func: |
Source code in src/setqca/analysis/robustness.py
calibration_robustness ¶
calibration_robustness(
raw: DataFrame,
*,
outcome: str,
conditions: Sequence[str],
grid: RobustnessGrid,
outcome_anchors: tuple[float, float, float],
base_anchors: Mapping[str, tuple[float, float, float]]
| None = None,
family: str = "conservative",
directional_expectations: Mapping[str, Direction]
| None = None,
case_id: str | None = None,
) -> RobustnessAnalysis
Sweep calibration anchors as well as thresholds, starting from raw data.
Calibration is where substantive judgement enters, so it is also where a result is most easily manufactured. This recalibrates from the raw measures for every anchor combination in the grid.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
raw
|
DataFrame
|
Uncalibrated measures. |
required |
outcome
|
str
|
Name of the outcome column. |
required |
conditions
|
sequence of str
|
Condition columns. |
required |
grid
|
RobustnessGrid
|
Must specify |
required |
outcome_anchors
|
tuple of float
|
Anchors used to calibrate the outcome, held fixed across the sweep. |
required |
base_anchors
|
mapping of str to tuple of float
|
Anchors for conditions the grid does not sweep. Every condition needs anchors from one source or the other, since the input is raw. |
None
|
family
|
str
|
Which solution family to track. |
"conservative"
|
directional_expectations
|
mapping
|
Required when |
None
|
case_id
|
str
|
Column holding case labels. |
None
|
Returns:
| Type | Description |
|---|---|
RobustnessAnalysis
|
As :func: |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the grid specifies no anchors, or a condition has no anchors at all. |
KeyError
|
If anchors name a condition outside the model. |
Source code in src/setqca/analysis/robustness.py
501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 | |