Skip to content

API reference

Everything documented here is importable directly from the setqca namespace.

Estimators

setqca.models

High-level csQCA and fsQCA estimators.

Direction module-attribute

Direction = Literal['+', '-', '0']

Directional expectation: "+" present, "-" absent, "0" no expectation.

FSQCA dataclass

FSQCA(
    consistency: float = 0.8,
    pri: float = 0.0,
    frequency: int = 1,
    exclusion_consistency: float | None = None,
    max_solutions: int = 256,
    directional_expectations: dict[str, Direction] = dict(),
)

Fuzzy-set Qualitative Comparative Analysis estimator.

Parameters:

Name Type Description Default
consistency float

Inclusion cutoff on sufficiency consistency for truth-table rows.

0.8
pri float

Minimum PRI for a row to be coded sufficient.

0.0
frequency int

Minimum number of cases for a row to count as observed.

1
exclusion_consistency float

Consistency below which a row is coded "0". Rows between the two cutoffs are coded contradictory. Defaults to consistency.

None
max_solutions int

Upper bound on the number of tied minimal covers returned.

256
directional_expectations dict of str to Direction

Theoretical expectations enabling the intermediate solution. Empty by default, which skips intermediate minimisation. Accepts the enum, the QCA symbols "+"/"-"/"0", or 1/0.

dict()
Notes

All three solution families use an exact classical Quine-McCluskey engine. Intermediate solutions follow Ragin and Sonnett (2005): the parsimonious solution's simplifying assumptions are split into easy and difficult counterfactuals, and only the easy ones are admitted.

fit

fit(
    data: DataFrame,
    *,
    outcome: str,
    conditions: list[str] | tuple[str, ...],
    case_id: str | None = None,
) -> QCAResult

Fit fsQCA to already calibrated condition and outcome memberships.

Parameters:

Name Type Description Default
data DataFrame

Calibrated memberships in [0, 1].

required
outcome str

Name of the outcome column.

required
conditions list of str or tuple of str

Names of the condition columns.

required
case_id str

Column holding case labels. Defaults to the frame index.

None

Returns:

Type Description
QCAResult

Truth table plus conservative, parsimonious and — when directional expectations are supplied — intermediate solutions.

Raises:

Type Description
ValueError

If no truth-table row is sufficient under the chosen thresholds.

Source code in src/setqca/models.py
def fit(
    self,
    data: pd.DataFrame,
    *,
    outcome: str,
    conditions: list[str] | tuple[str, ...],
    case_id: str | None = None,
) -> QCAResult:
    """Fit fsQCA to already calibrated condition and outcome memberships.

    Parameters
    ----------
    data : pandas.DataFrame
        Calibrated memberships in ``[0, 1]``.
    outcome : str
        Name of the outcome column.
    conditions : list of str or tuple of str
        Names of the condition columns.
    case_id : str, optional
        Column holding case labels. Defaults to the frame index.

    Returns
    -------
    QCAResult
        Truth table plus conservative, parsimonious and — when directional
        expectations are supplied — intermediate solutions.

    Raises
    ------
    ValueError
        If no truth-table row is sufficient under the chosen thresholds.
    """
    self._validate_thresholds()
    truth_table = build_truth_table(
        data,
        outcome=outcome,
        conditions=conditions,
        inclusion_cutoff=self.consistency,
        exclusion_cutoff=self.exclusion_consistency,
        pri_cutoff=self.pri,
        frequency_cutoff=self.frequency,
        case_id=case_id,
    )
    return self._fit_from_truth_table(data, truth_table)

CSQCA dataclass

CSQCA(
    consistency: float = 1.0,
    pri: float = 0.0,
    frequency: int = 1,
    exclusion_consistency: float | None = None,
    max_solutions: int = 256,
    directional_expectations: dict[str, Direction] = dict(),
)

Bases: FSQCA

Crisp-set QCA with strict 0/1 input validation.

Identical to :class:FSQCA except that every condition and the outcome must already be calibrated to binary membership, and the default inclusion cutoff is perfect consistency.

fit

fit(
    data: DataFrame,
    *,
    outcome: str,
    conditions: list[str] | tuple[str, ...],
    case_id: str | None = None,
) -> QCAResult

Fit csQCA after verifying that every input column is crisp.

Raises:

Type Description
ValueError

If any condition or the outcome contains values other than 0 or 1.

Source code in src/setqca/models.py
def fit(
    self,
    data: pd.DataFrame,
    *,
    outcome: str,
    conditions: list[str] | tuple[str, ...],
    case_id: str | None = None,
) -> QCAResult:
    """Fit csQCA after verifying that every input column is crisp.

    Raises
    ------
    ValueError
        If any condition or the outcome contains values other than 0 or 1.
    """
    for column in (*conditions, outcome):
        values = data[column].to_numpy(dtype=np.float64)
        if not np.isin(values, (0.0, 1.0)).all():
            raise ValueError(f"CSQCA requires binary 0/1 calibration; {column!r} is not crisp.")
    # `dataclass(slots=True)` rebuilds the class object, which leaves the
    # implicit `__class__` cell of zero-argument `super()` pointing at the
    # discarded original. The explicit unbound call is the supported form.
    return FSQCA.fit(self, data, outcome=outcome, conditions=conditions, case_id=case_id)

Results

setqca.results

Structured QCA result objects.

SolutionKind module-attribute

SolutionKind = str

Name of a solution family: "conservative", "parsimonious" or "intermediate".

FittedSolution dataclass

FittedSolution(
    boolean: BooleanSolution,
    fit: SufficiencyFit,
    term_fits: tuple[SufficiencyFit, ...],
)

A Boolean solution together with its fuzzy empirical fit.

expression

expression(conditions: tuple[str, ...]) -> str

Render the solution in standard QCA notation, e.g. A*~B + C.

Source code in src/setqca/results.py
def expression(self, conditions: tuple[str, ...]) -> str:
    """Render the solution in standard QCA notation, e.g. ``A*~B + C``."""
    return self.boolean.as_expression(conditions)

QCAResult dataclass

QCAResult(
    method: str,
    outcome: str,
    conditions: tuple[str, ...],
    truth_table: TruthTable,
    conservative: tuple[FittedSolution, ...],
    parsimonious: tuple[FittedSolution, ...],
    intermediate: tuple[FittedSolution, ...] | None,
    intermediate_experimental: bool,
    counterfactuals: CounterfactualAnalysis | None = None,
)

Complete fitted QCA result for one outcome.

solutions

solutions(kind: SolutionKind) -> tuple[FittedSolution, ...]

Return the fitted solutions of one family.

Parameters:

Name Type Description Default
kind str

One of "conservative", "parsimonious" or "intermediate".

required

Returns:

Type Description
tuple of FittedSolution

Empty when the requested family was not computed.

Raises:

Type Description
ValueError

If kind is not a recognised solution family.

Source code in src/setqca/results.py
def solutions(self, kind: SolutionKind) -> tuple[FittedSolution, ...]:
    """Return the fitted solutions of one family.

    Parameters
    ----------
    kind : str
        One of ``"conservative"``, ``"parsimonious"`` or ``"intermediate"``.

    Returns
    -------
    tuple of FittedSolution
        Empty when the requested family was not computed.

    Raises
    ------
    ValueError
        If ``kind`` is not a recognised solution family.
    """
    if kind not in _SOLUTION_KINDS:
        raise ValueError(f"Unknown solution kind {kind!r}; expected one of {_SOLUTION_KINDS}.")
    values: tuple[FittedSolution, ...] | None = getattr(self, kind)
    return () if values is None else values

summary_frame

summary_frame(
    solution: SolutionKind = "conservative",
) -> DataFrame

Return one row per minimal solution of the requested family.

Parameters:

Name Type Description Default
solution str

Solution family to summarise.

"conservative"

Returns:

Type Description
DataFrame

Columns solution, consistency, coverage, PRI, n_implicants and n_literals. Empty when the family was not computed.

Source code in src/setqca/results.py
def summary_frame(self, solution: SolutionKind = "conservative") -> pd.DataFrame:
    """Return one row per minimal solution of the requested family.

    Parameters
    ----------
    solution : str, default "conservative"
        Solution family to summarise.

    Returns
    -------
    pandas.DataFrame
        Columns ``solution``, ``consistency``, ``coverage``, ``PRI``,
        ``n_implicants`` and ``n_literals``. Empty when the family was not
        computed.
    """
    values = self.solutions(solution)
    return pd.DataFrame(
        {
            "solution": [item.expression(self.conditions) for item in values],
            "consistency": [item.fit.consistency for item in values],
            "coverage": [item.fit.coverage for item in values],
            "PRI": [item.fit.pri for item in values],
            "n_implicants": [len(item.boolean.implicants) for item in values],
            "n_literals": [item.boolean.literal_count for item in values],
        }
    )

fit_boolean_solution

fit_boolean_solution(
    solution: BooleanSolution,
    *,
    data: DataFrame,
    outcome: str,
    conditions: tuple[str, ...],
) -> FittedSolution

Evaluate a Boolean solution as a fuzzy set over the original cases.

Each prime implicant becomes a conjunction under the minimum t-norm and the solution as a whole becomes their disjunction under the maximum s-norm.

Parameters:

Name Type Description Default
solution BooleanSolution

Minimal cover produced by the Boolean minimiser.

required
data DataFrame

Calibrated case-level data.

required
outcome str

Name of the outcome column.

required
conditions tuple of str

Condition names in minterm order.

required

Returns:

Type Description
FittedSolution

The solution with overall and term-level parameters of fit.

Source code in src/setqca/results.py
def fit_boolean_solution(
    solution: BooleanSolution,
    *,
    data: pd.DataFrame,
    outcome: str,
    conditions: tuple[str, ...],
) -> FittedSolution:
    """Evaluate a Boolean solution as a fuzzy set over the original cases.

    Each prime implicant becomes a conjunction under the minimum t-norm and the
    solution as a whole becomes their disjunction under the maximum s-norm.

    Parameters
    ----------
    solution : BooleanSolution
        Minimal cover produced by the Boolean minimiser.
    data : pandas.DataFrame
        Calibrated case-level data.
    outcome : str
        Name of the outcome column.
    conditions : tuple of str
        Condition names in minterm order.

    Returns
    -------
    FittedSolution
        The solution with overall and term-level parameters of fit.
    """
    y = data[outcome].to_numpy(dtype=np.float64)
    n_cases = len(data)
    term_memberships: list[FloatArray] = []
    term_fits: list[SufficiencyFit] = []
    for implicant in solution.implicants:
        components: list[FloatArray] = [
            _oriented_membership(data, condition, bit)
            for condition, bit in zip(conditions, implicant.pattern, strict=True)
            if bit is not None
        ]
        # An implicant with no fixed literal is the tautology covering every case.
        membership = (
            np.ones(n_cases, dtype=np.float64) if not components else np.minimum.reduce(components)
        )
        term_memberships.append(membership)
        term_fits.append(sufficiency(membership, y))
    overall = (
        np.zeros(n_cases, dtype=np.float64)
        if not term_memberships
        else np.maximum.reduce(term_memberships)
    )
    return FittedSolution(solution, sufficiency(overall, y), tuple(term_fits))

Parameters of fit

setqca.metrics

Set-theoretic parameters of fit for QCA.

SufficiencyFit dataclass

SufficiencyFit(
    consistency: float, coverage: float, pri: float
)

Parameters of fit for a sufficiency relation X <= Y.

NecessityFit dataclass

NecessityFit(
    consistency: float, coverage: float, ron: float
)

Parameters of fit for a necessity relation Y <= X.

sufficiency

sufficiency(
    cause: ArrayLike, outcome: ArrayLike
) -> SufficiencyFit

Calculate fuzzy-set sufficiency consistency, coverage and PRI.

PRI follows the implementation used by the R QCA package: inconsistency that simultaneously supports the outcome and its negation is removed from numerator and denominator.

Source code in src/setqca/metrics.py
def sufficiency(cause: npt.ArrayLike, outcome: npt.ArrayLike) -> SufficiencyFit:
    """Calculate fuzzy-set sufficiency consistency, coverage and PRI.

    PRI follows the implementation used by the R ``QCA`` package: inconsistency
    that simultaneously supports the outcome and its negation is removed from
    numerator and denominator.
    """
    x = validate_membership(cause, name="cause")
    y = validate_membership(outcome, name="outcome")
    if x.shape != y.shape:
        raise ValueError("cause and outcome must have equal length.")

    xy = np.minimum(x, y)
    sum_xy = float(xy.sum())
    sum_x = float(x.sum())
    sum_y = float(y.sum())
    contradictory = float(np.minimum(xy, 1.0 - y).sum())

    return SufficiencyFit(
        consistency=_safe_ratio(sum_xy, sum_x),
        coverage=_safe_ratio(sum_xy, sum_y),
        pri=_safe_ratio(sum_xy - contradictory, sum_x - contradictory),
    )

necessity

necessity(
    cause: ArrayLike, outcome: ArrayLike
) -> NecessityFit

Calculate fuzzy-set necessity consistency, coverage and RoN.

Source code in src/setqca/metrics.py
def necessity(cause: npt.ArrayLike, outcome: npt.ArrayLike) -> NecessityFit:
    """Calculate fuzzy-set necessity consistency, coverage and RoN."""
    x = validate_membership(cause, name="cause")
    y = validate_membership(outcome, name="outcome")
    if x.shape != y.shape:
        raise ValueError("cause and outcome must have equal length.")

    xy = np.minimum(x, y)
    sum_xy = float(xy.sum())
    sum_x = float(x.sum())
    sum_y = float(y.sum())
    ron_num = float((1.0 - x).sum())
    ron_den = float((1.0 - np.minimum(x, y)).sum())

    return NecessityFit(
        consistency=_safe_ratio(sum_xy, sum_y),
        coverage=_safe_ratio(sum_xy, sum_x),
        ron=_safe_ratio(ron_num, ron_den),
    )

Set expressions

setqca.sets

Typed set expressions for calibrated QCA conditions.

SetExpression

Bases: ABC

Abstract fuzzy-set expression over calibrated conditions.

Expressions compose with the standard Python operators & (intersection, minimum t-norm), | (union, maximum s-norm) and ~ (negation).

evaluate abstractmethod

evaluate(data: DataFrame) -> FloatArray

Evaluate membership of the expression for every case.

Parameters:

Name Type Description Default
data DataFrame

Frame of calibrated condition memberships.

required

Returns:

Type Description
FloatArray

Membership of each case in the expression.

Source code in src/setqca/sets.py
@abstractmethod
def evaluate(self, data: pd.DataFrame) -> FloatArray:
    """Evaluate membership of the expression for every case.

    Parameters
    ----------
    data : pandas.DataFrame
        Frame of calibrated condition memberships.

    Returns
    -------
    FloatArray
        Membership of each case in the expression.
    """

Condition dataclass

Condition(name: str)

Bases: SetExpression

Named calibrated condition drawn from a column of the data.

evaluate

evaluate(data: DataFrame) -> FloatArray

Return the calibrated membership column for this condition.

Source code in src/setqca/sets.py
def evaluate(self, data: pd.DataFrame) -> FloatArray:
    """Return the calibrated membership column for this condition."""
    if self.name not in data.columns:
        raise KeyError(f"Missing condition column: {self.name}")
    return validate_membership(data[self.name].to_numpy(), name=self.name)

Negation dataclass

Negation(operand: SetExpression)

Bases: SetExpression

Fuzzy-set negation using 1 - membership.

evaluate

evaluate(data: DataFrame) -> FloatArray

Return one minus the membership of the negated operand.

Source code in src/setqca/sets.py
def evaluate(self, data: pd.DataFrame) -> FloatArray:
    """Return one minus the membership of the negated operand."""
    return 1.0 - self.operand.evaluate(data)

Intersection dataclass

Intersection(operands: tuple[SetExpression, ...])

Bases: SetExpression

Fuzzy conjunction using the minimum t-norm.

evaluate

evaluate(data: DataFrame) -> FloatArray

Return the elementwise minimum across all operands.

Source code in src/setqca/sets.py
def evaluate(self, data: pd.DataFrame) -> FloatArray:
    """Return the elementwise minimum across all operands."""
    if not self.operands:
        raise ValueError("Intersection requires at least one operand.")
    arrays = (operand.evaluate(data) for operand in self.operands)
    return reduce(np.minimum, arrays)

Union dataclass

Union(operands: tuple[SetExpression, ...])

Bases: SetExpression

Fuzzy disjunction using the maximum s-norm.

evaluate

evaluate(data: DataFrame) -> FloatArray

Return the elementwise maximum across all operands.

Source code in src/setqca/sets.py
def evaluate(self, data: pd.DataFrame) -> FloatArray:
    """Return the elementwise maximum across all operands."""
    if not self.operands:
        raise ValueError("Union requires at least one operand.")
    arrays = (operand.evaluate(data) for operand in self.operands)
    return reduce(np.maximum, arrays)