Calibration¶
Calibration converts raw measures into set memberships. It is the step where substantive knowledge enters the analysis, and it determines everything downstream. A truth table built on poor anchors is precisely wrong.
Direct calibration¶
Three anchors define the transformation:
| Anchor | Membership | Meaning |
|---|---|---|
full_out |
≈ 1 - idm |
Fully outside the set |
crossover |
0.5 | Maximum ambiguity |
full_in |
≈ idm |
Fully inside the set |
from setqca import calibrate_direct
membership = calibrate_direct(
raw_innovation,
full_out=10,
crossover=50,
full_in=90,
)
With the default idm=0.95, the anchors map to approximately 0.05, 0.5 and
0.95 for an increasing set.
Increasing and decreasing sets¶
The anchor order selects the direction. full_out < crossover < full_in
defines an increasing set; reversing the order defines a decreasing one.
# "low corruption": higher raw scores mean lower membership
low_corruption = calibrate_direct(corruption, full_out=80, crossover=50, full_in=20)
Anchors that are not strictly ordered around the crossover raise ValueError
rather than producing an arbitrary curve.
Logistic versus piecewise¶
logistic = calibrate_direct(x, full_out=0, crossover=50, full_in=100)
piecewise = calibrate_direct(x, full_out=0, crossover=50, full_in=100, logistic=False)
| Logistic (default) | Piecewise | |
|---|---|---|
| Endpoints | Approached asymptotically, never exactly reached | Exactly 0 and 1 at and beyond the anchors |
| Shape control | idm |
below and above exponents |
| Use when | You want the standard smooth transformation | You need exact full membership for cases at or beyond an anchor |
The logistic transformation is evaluated in a numerically stable form, so values far outside the anchors saturate cleanly to 0 or 1 rather than overflowing.
Shaping the piecewise curve¶
below and above are positive exponents applied on either side of the
crossover. Raising above accelerates the approach to full membership:
calibrate_direct([75], full_out=0, crossover=50, full_in=100, logistic=False) # 0.750
calibrate_direct([75], full_out=0, crossover=50, full_in=100, logistic=False, above=2.0) # 0.875
The anchors themselves always map to 0, 0.5 and 1 regardless of the exponents.
Crisp calibration¶
calibrate_crisp cuts a raw variable into ordered integer categories using
threshold semantics equivalent to R's findInterval.
from setqca import calibrate_crisp
binary = calibrate_crisp(gdp_per_capita, [20_000])
categories = calibrate_crisp(gdp_per_capita, [10_000, 20_000, 30_000])
One threshold yields a binary crisp set. Multiple thresholds yield categories
0..k; this signature is the forward-compatible API for multi-value QCA, which
is a roadmap item rather than a current feature.
Thresholds are sorted automatically, and duplicates raise ValueError.
The 0.5 problem¶
A membership of exactly 0.5 has no defined truth-table corner: it is neither
more in than out nor more out than in. setqca refuses to guess.
build_truth_table(data, outcome="Y", conditions=["A"])
# ValueError: At least one condition is exactly 0.5 ...
Resolve these cases substantively — by revisiting the anchors, or by making a documented decision about the case — or opt in explicitly:
With the override, scores of exactly 0.5 are assigned to the present corner,
because corner assignment uses x >= 0.5.
Specifications¶
A calibration is a decision worth recording. CalibrationSpec makes it a value
you can store, compare, ship in a replication package, and replay:
from setqca import calibrate, direct_spec
spec = direct_spec(
"innovation",
full_out=20,
crossover=50,
full_in=80,
note="OECD reporting threshold; see section 3.2",
)
result = calibrate(raw["innovation"], spec)
result.values # the calibrated memberships
result.spec # what produced them
result.diagnostics # and what is worrying about them
The note carries the reason through serialisation, because the reason is
part of the specification:
A specification is validated when it is written, not when it eventually meets data — badly ordered anchors raise immediately.
Indirect calibration¶
When theory dictates a shape the three-anchor transformation cannot express — a plateau, a step, an asymmetric ramp — give the mapping explicitly:
from setqca import indirect_spec
spec = indirect_spec(
"capacity",
mapping=((0, 0.0), (30, 0.5), (70, 0.5), (100, 1.0)),
note="no meaningful variation between 30 and 70",
)
Points are interpolated linearly and held flat beyond the ends. The mapping must be non-decreasing, since a calibration that reverses direction is a different concept, not a calibration.
Diagnostics¶
A calibration can be arithmetically valid and analytically useless.
from setqca import diagnose_calibration, diagnose_frame
print(diagnose_calibration(calibrated))
diagnose_frame(data) # one row per condition
Five failures are reported:
| Warning | Why it matters |
|---|---|
| Cases exactly at 0.5 | The truth-table corner is undefined. This is the only one that makes the vector unusable. |
| Pile-up near the crossover | Small anchor changes will move cases between corners, so the result is fragile. |
| Compression to the extremes | The calibration is effectively crisp; the fuzzy detail has been squeezed out. |
| Low variance | The condition barely varies and carries little information. |
| Never present / never absent | Every case falls on one side, so the condition cannot discriminate. |
None is fatal by itself — they are reported so you can decide, not enforced.
Quantile helpers, and why they are not a calibration¶
Suggested from quantiles (0.05, 0.5, 0.95): full_out=12, crossover=48, full_in=91
Quantiles describe the sample, not the concept. Anchors must be justified
substantively; these are a starting point for that argument, not a substitute
for it.
Data-driven anchors are not calibration
A set defined by its own distribution cannot support a claim about set membership. If the crossover is the sample median, then "more in than out" means "above average for these cases" — which changes when you add a case, and says nothing about the concept.
The helper exists to show you where your cases actually lie so you can argue for anchors. It returns the caveat attached to the result, and nothing in this package will apply quantile anchors for you.
Reusing a calibration¶
DirectCalibration is a frozen dataclass, so a calibration is a value you can
store, pass around, and apply to new cases — which is what you need when
extending an analysis to additional cases without silently re-anchoring.
from setqca import DirectCalibration
spec = DirectCalibration(full_out=10, crossover=50, full_in=90)
train = spec.transform(raw_train)
holdout = spec.transform(raw_holdout)
setqca.calibration ¶
Calibration: turning raw measures into set memberships.
Calibration is where substantive knowledge enters a QCA, and where a result is most easily manufactured. The primitives are here, along with reproducible specifications, diagnostics for the failures that spoil a truth table, and quantile helpers that are explicitly not a calibration.
Examples:
>>> from setqca.calibration import calibrate, direct_spec
>>> spec = direct_spec("innovation", full_out=20, crossover=50, full_in=80)
>>> result = calibrate([10, 50, 90], spec)
>>> result.diagnostics.warnings
AnchorSuggestion
dataclass
¶
AnchorSuggestion(
quantiles: tuple[float, float, float],
values: tuple[float, float, float],
caveat: str = "Quantiles describe the sample, not the concept. Anchors must be justified substantively; these are a starting point for that argument, not a substitute for it.",
)
Quantiles of a raw variable, offered as a starting point only.
Attributes:
| Name | Type | Description |
|---|---|---|
quantiles |
tuple[float, float, float]
|
The probabilities used. |
values |
tuple[float, float, float]
|
The corresponding raw values, in ascending order. |
caveat |
str
|
A standing reminder that these are not a calibration. |
anchors
property
¶
Return the suggested (full_out, crossover, full_in).
CalibrationDiagnostics
dataclass
¶
CalibrationDiagnostics(
n: int,
at_crossover: int,
near_crossover: int,
extreme: int,
minimum: float,
maximum: float,
mean: float,
standard_deviation: float,
above_crossover: int,
warnings: tuple[str, ...],
)
What a calibrated vector looks like, and what is worrying about it.
Attributes:
| Name | Type | Description |
|---|---|---|
n |
int
|
Number of cases. |
at_crossover |
int
|
Cases with membership exactly 0.5. Their truth-table corner is undefined, so these block analysis rather than merely warning. |
near_crossover |
int
|
Cases within |
extreme |
int
|
Cases within |
minimum, maximum, mean, standard_deviation |
Summary statistics of the calibrated values. |
|
above_crossover |
int
|
Cases that will be assigned the "present" corner. |
warnings |
tuple[str, ...]
|
Human-readable descriptions of every issue found. |
usable
property
¶
Return whether the vector can be used for a truth table at all.
Only an exact crossover membership makes a vector unusable; everything else is a judgement call left to the researcher.
pile_up_share
property
¶
Return the proportion of cases bunched around the crossover.
compression_share
property
¶
Return the proportion of cases pushed to the extremes.
CalibrationResult
dataclass
¶
CalibrationResult(
spec: object,
values: FloatArray,
diagnostics: CalibrationDiagnostics,
)
Calibrated values together with the specification that produced them.
to_frame ¶
Return the calibrated values as a one-column frame.
DirectCalibration
dataclass
¶
DirectCalibration(
full_out: float,
crossover: float,
full_in: float,
idm: float = 0.95,
logistic: bool = True,
below: float = 1.0,
above: float = 1.0,
)
Three-anchor direct fuzzy-set calibration specification.
The logistic implementation follows the standard three-anchor direct
calibration parameterisation: the crossover maps to 0.5 and the inclusion
and exclusion anchors map to idm and 1-idm respectively.
transform ¶
Calibrate raw values into fuzzy membership scores.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
values
|
array_like
|
Raw numeric values on the original measurement scale. |
required |
Returns:
| Type | Description |
|---|---|
FloatArray
|
Membership scores in |
Source code in src/setqca/calibration/_direct.py
CalibrationMethod ¶
Bases: Enum
How raw values become set memberships.
CalibrationSpec
dataclass
¶
CalibrationSpec(
condition: str,
method: CalibrationMethod = DIRECT,
anchors: tuple[float, float, float] | None = None,
idm: float = 0.95,
logistic: bool = True,
below: float = 1.0,
above: float = 1.0,
thresholds: tuple[float, ...] | None = None,
mapping: tuple[tuple[float, float], ...] | None = None,
note: str = "",
)
A complete, replayable description of one condition's calibration.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
condition
|
str
|
Name of the condition this calibrates. |
required |
method
|
CalibrationMethod
|
Which transformation to apply. |
DIRECT
|
anchors
|
tuple of float
|
|
None
|
idm
|
float
|
Membership at the inclusion anchor, for the direct logistic form. |
0.95
|
logistic
|
bool
|
Use the logistic rather than piecewise direct transformation. |
True
|
below
|
float
|
Piecewise shaping exponents. |
1.0
|
above
|
float
|
Piecewise shaping exponents. |
1.0
|
thresholds
|
tuple of float
|
Cut points for the crisp method. |
None
|
mapping
|
tuple of (float, float)
|
|
None
|
note
|
str
|
Free text recording why these choices were made. Carried through serialisation, because the reason is part of the specification. |
''
|
apply ¶
Calibrate raw values according to this specification.
Source code in src/setqca/calibration/_spec.py
with_anchors ¶
with_anchors(
anchors: tuple[float, float, float],
) -> CalibrationSpec
Return a copy with different anchors, for sensitivity work.
to_dict ¶
Return a plain dictionary suitable for JSON or YAML.
Source code in src/setqca/calibration/_spec.py
from_dict
classmethod
¶
from_dict(payload: dict[str, Any]) -> CalibrationSpec
Rebuild a specification from :meth:to_dict output.
Raises:
| Type | Description |
|---|---|
KeyError
|
If the payload lacks a condition or method. |
ValueError
|
If the method is unknown, or the specification is invalid. |
Source code in src/setqca/calibration/_spec.py
to_json ¶
Serialise to a JSON string.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
indent
|
int
|
Passed to :func: |
None
|
sort_keys
|
bool
|
Sort keys, which makes stored specifications diff cleanly. |
False
|
Source code in src/setqca/calibration/_spec.py
from_json
classmethod
¶
from_json(text: str) -> CalibrationSpec
diagnose_calibration ¶
diagnose_calibration(
values: ArrayLike, *, name: str = "values"
) -> CalibrationDiagnostics
Inspect a calibrated vector for the failures that spoil a truth table.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
values
|
array_like
|
Calibrated memberships in |
required |
name
|
str
|
Name used in the warning messages. |
"values"
|
Returns:
| Type | Description |
|---|---|
CalibrationDiagnostics
|
Summary statistics and a list of warnings. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the values are not calibrated memberships. |
Source code in src/setqca/calibration/_diagnostics.py
diagnose_frame ¶
Diagnose every calibrated column and return a tidy summary.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
DataFrame
|
Calibrated memberships. |
required |
columns
|
list of str
|
Columns to check. Defaults to every column. |
None
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
One row per column, with the summary statistics and a joined warning string. |
Source code in src/setqca/calibration/_diagnostics.py
suggest_anchors ¶
suggest_anchors(
values: ArrayLike,
*,
quantiles: tuple[float, float, float] = (
0.05,
0.5,
0.95,
),
) -> AnchorSuggestion
Report quantiles of a raw variable to inform an anchor decision.
This is a diagnostic, not a calibration. Data-driven anchors describe the sample rather than the concept, and a set defined by its own distribution cannot support a claim about set membership. Use the output to see where your cases actually lie, then argue for anchors on substantive grounds.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
values
|
array_like
|
Raw, uncalibrated measures. |
required |
quantiles
|
tuple of float
|
Probabilities for the exclusion, crossover and inclusion anchors. |
(0.05, 0.50, 0.95)
|
Returns:
| Type | Description |
|---|---|
AnchorSuggestion
|
The quantile values, with the caveat attached. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the quantiles are not strictly increasing and inside |
Source code in src/setqca/calibration/_diagnostics.py
calibrate_crisp ¶
Calibrate a raw numeric variable into ordered crisp categories.
With one threshold this produces a binary crisp set. Multiple thresholds
produce integer categories 0..k and form a future mvQCA-compatible API.
Source code in src/setqca/calibration/_direct.py
calibrate_direct ¶
calibrate_direct(
values: ArrayLike,
*,
full_out: float,
crossover: float,
full_in: float,
idm: float = 0.95,
logistic: bool = True,
below: float = 1.0,
above: float = 1.0,
) -> FloatArray
Calibrate raw values with three-anchor direct fuzzy calibration.
Convenience wrapper around :class:DirectCalibration for one-shot use.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
values
|
array_like
|
Raw numeric values to calibrate. |
required |
full_out
|
float
|
The three qualitative anchors. |
required |
crossover
|
float
|
The three qualitative anchors. |
required |
full_in
|
float
|
The three qualitative anchors. |
required |
idm
|
float
|
Membership assigned to the inclusion anchor. Must lie in |
0.95
|
logistic
|
bool
|
Use the logistic transformation. When |
True
|
below
|
float
|
Positive exponents shaping the piecewise transformation on either side
of the crossover. Ignored when |
1.0
|
above
|
float
|
Positive exponents shaping the piecewise transformation on either side
of the crossover. Ignored when |
1.0
|
Returns:
| Type | Description |
|---|---|
FloatArray
|
Fuzzy membership scores in |
Examples:
>>> calibrate_direct([10, 20, 30], full_out=10, crossover=20, full_in=30).round(2)
array([0.05, 0.5 , 0.95])
Source code in src/setqca/calibration/_direct.py
crisp_spec ¶
crisp_spec(
condition: str,
*,
thresholds: tuple[float, ...],
note: str = "",
) -> CalibrationSpec
Build a crisp threshold specification.
Source code in src/setqca/calibration/_spec.py
direct_spec ¶
direct_spec(
condition: str,
*,
full_out: float,
crossover: float,
full_in: float,
idm: float = 0.95,
logistic: bool = True,
below: float = 1.0,
above: float = 1.0,
note: str = "",
) -> CalibrationSpec
Build a three-anchor direct calibration specification.
Source code in src/setqca/calibration/_spec.py
indirect_spec ¶
indirect_spec(
condition: str,
*,
mapping: tuple[tuple[float, float], ...],
note: str = "",
) -> CalibrationSpec
Build an explicit monotone mapping specification.
Use this when theory dictates a shape the direct transformation cannot express — a plateau, a step, an asymmetric ramp. Points are interpolated linearly and held flat beyond the ends.
Source code in src/setqca/calibration/_spec.py
calibrate ¶
calibrate(
values: ArrayLike, spec: CalibrationSpec
) -> CalibrationResult
Apply a specification and diagnose the result in one step.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
values
|
array_like
|
Raw values, or calibrated ones for the identity method. |
required |
spec
|
CalibrationSpec
|
The calibration to apply. |
required |
Returns:
| Type | Description |
|---|---|
CalibrationResult
|
The calibrated values, the specification that produced them, and diagnostics. Keeping the three together is what makes a calibration reproducible rather than a number that appeared once. |