Metadata-Version: 2.5
Name: pyqca
Version: 0.3.0
Summary: Python-native toolkit for csQCA, mvQCA, fsQCA, gsQCA, and threshold-sweep analysis
Project-URL: Homepage, https://github.com/t-yamsaki/PyQCA
Project-URL: Documentation, https://pyqca.readthedocs.io/en/latest/
Project-URL: Repository, https://github.com/t-yamsaki/PyQCA
Project-URL: Issues, https://github.com/t-yamsaki/PyQCA/issues
Author: Taishi Yamasaki
License-Expression: MIT
License-File: LICENSE
License-File: THIRD_PARTY_NOTICES.md
Keywords: Boolean minimization,QCA,fuzzy set,multi-value QCA,qualitative comparative analysis,threshold sweep
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Science/Research
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Scientific/Engineering :: Information Analysis
Requires-Python: >=3.11
Requires-Dist: numpy>=1.26
Requires-Dist: pandas>=2.1
Provides-Extra: dev
Requires-Dist: build>=1.2; extra == 'dev'
Requires-Dist: flake8>=7.0; extra == 'dev'
Requires-Dist: furo==2025.12.19; extra == 'dev'
Requires-Dist: isort>=5.13; extra == 'dev'
Requires-Dist: matplotlib>=3.8; extra == 'dev'
Requires-Dist: mypy>=1.9; extra == 'dev'
Requires-Dist: pandas-stubs>=2.1; extra == 'dev'
Requires-Dist: pytest-cov>=4.1; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: ruff>=0.4; extra == 'dev'
Requires-Dist: sphinx-copybutton==0.5.2; extra == 'dev'
Requires-Dist: sphinx==9.1.0; extra == 'dev'
Requires-Dist: twine>=5.0; extra == 'dev'
Provides-Extra: docs
Requires-Dist: furo==2025.12.19; extra == 'docs'
Requires-Dist: sphinx-copybutton==0.5.2; extra == 'docs'
Requires-Dist: sphinx==9.1.0; extra == 'docs'
Provides-Extra: mlqca
Requires-Dist: scikit-learn>=1.4; extra == 'mlqca'
Requires-Dist: xgboost>=2.0; extra == 'mlqca'
Provides-Extra: mlqca-explain
Requires-Dist: scikit-learn>=1.4; extra == 'mlqca-explain'
Requires-Dist: shap>=0.46; extra == 'mlqca-explain'
Requires-Dist: xgboost>=2.0; extra == 'mlqca-explain'
Provides-Extra: test
Requires-Dist: pytest-cov>=4.1; extra == 'test'
Requires-Dist: pytest>=8.0; extra == 'test'
Provides-Extra: viz
Requires-Dist: matplotlib>=3.8; extra == 'viz'
Description-Content-Type: text/markdown

# PyQCA

**PyQCA** is a Python-native toolkit for **Qualitative Comparative Analysis (QCA)**.

[![CI](https://github.com/t-yamsaki/PyQCA/actions/workflows/ci.yml/badge.svg)](https://github.com/t-yamsaki/PyQCA/actions/workflows/ci.yml)
[![Docs build](https://github.com/t-yamsaki/PyQCA/actions/workflows/docs.yml/badge.svg)](https://github.com/t-yamsaki/PyQCA/actions/workflows/docs.yml)
[![Documentation](https://readthedocs.org/projects/pyqca/badge/?version=latest)](https://pyqca.readthedocs.io/en/latest/)

[日本語 README](https://github.com/t-yamsaki/PyQCA/blob/main/README.ja.md)

It provides a unified framework for running **crisp-set QCA (csQCA)**,
**multi-value QCA (mvQCA)**, **fuzzy-set QCA (fsQCA)**,
**generalized-set QCA (gsQCA)**, and **machine-learning-enhanced QCA
(mlQCA)** in Python. PyQCA also supports pluggable minimization backends,
threshold and calibration sensitivity analysis, reporting, and reproducibility
tools.

> Status: experimental / under active development

---

## Documentation

The Sphinx documentation covers installation, core QCA, generalized-set QCA,
calibration, minimization, sensitivity analysis, reporting, mlQCA, and the
generated API reference. The hosted documentation is available at
https://pyqca.readthedocs.io/en/latest/.

Build it locally with:

```bash
python -m pip install -e ".[docs]"
python -m sphinx -W --keep-going -b html docs docs/_build/html
```

For local previews, open `docs/_build/html/index.html` after a local build.

---

## Why PyQCA?

QCA is widely used in social science, management research, political science, evaluation research, and configurational causal analysis. Mature QCA tooling exists in R, especially for crisp-set, multi-value, and fuzzy-set QCA workflows.

PyQCA aims to provide a Python-native QCA foundation for researchers and practitioners who want to:

* run csQCA, mvQCA, fsQCA, and gsQCA directly in Python;
* analyze crisp, multi-valued, and fuzzy-set conditions in a generalized-set interface;
* switch between different minimization algorithms;
* inspect consistency, coverage, truth tables, and solution formulas;
* conduct threshold-sensitivity analysis systematically;
* use XGBoost evidence to propose conditions, calibration thresholds, and QCA
  model combinations;
* evaluate mlQCA condition and cutoff stability with cross-validation and
  bootstrap resampling;
* integrate QCA workflows with Python data science tools such as pandas, NumPy, scikit-learn, Jupyter, and visualization libraries.

PyQCA is not intended to be only a thin wrapper around existing R packages. The goal is to provide a Python-native architecture for standard QCA workflows and future methodological extensions.

---

## Core Features

### 1. csQCA

Run crisp-set QCA on binary set-membership scores.

```python
from qca import CSQCA

model = CSQCA(
    data=df,
    outcome="Y",
    conditions=["A", "B", "C"],
)

result = model.fit(
    consistency_cutoff=0.8,
    coverage_cutoff=0.1,
)

print(result.truth_table)
print(result.solutions)
```

---

### 2. mvQCA

Multi-value category values must not be the exact string `*`, which is reserved for the internal don't-care wildcard. Observed data, declared domains, `value_columns` keys, and category unions containing this value raise `ValueError`. Recode it to a different category value before analysis; strings such as `A*` remain valid.

Run multi-value QCA on categorical and ordinal conditions.

```python
from qca import MVQCA

model = MVQCA(
    data=df,
    outcome="Y",
    conditions=["A", "B", "C"],
)

result = model.fit(
    consistency_cutoff=0.8,
    coverage_cutoff=0.1,
)

print(result.solutions)
print(result.consistency)
print(result.coverage)
```

---

### 3. fsQCA

Piecewise membership bounds must be finite and satisfy `0 <= lower < 0.5 < upper <= 1`. Bounds exactly equal to 0.5 are rejected to avoid a flat segment at the crossover; endpoints 0 and 1 are allowed. Scalar, Series, and quantile-based piecewise calibration apply the same validation and raise `ValueError` for invalid bounds. Logistic calibration retains its separate, stricter endpoint requirements.

Piecewise calibration uses the actual positive anchor intervals, without imposing an absolute minimum width. Direct scalar/Series calls require finite, strictly increasing anchors and raise `ValueError` for degenerate anchors; quantile calibration retains its existing warning-and-skip policy for invalid estimated anchors. Unit conversion preserves memberships within floating-point precision while the anchors remain distinct and finite. Values that round to identical floating-point anchors cannot be distinguished; no artificial interval is substituted.

Grouped `calibrate_piecewise` calibration uses each group's actual rows, even when index labels are duplicated within or across groups. Returned rows retain the input order and index; anchor counts, `min_nonnull`, and calibrated/skipped counts reflect rows rather than unique labels.

Run fuzzy-set QCA with calibrated set-membership scores.

```python
from qca import FSQCA, calibrate_crisp, calibrate_piecewise

calibrated = calibrate_piecewise(df, "raw_score", out_col="A").df
calibrated = calibrate_crisp(calibrated, "raw_flag", threshold=1, out_col="B").df

model = FSQCA(
    data=calibrated,
    outcome="Y",
    conditions=["A", "B"],
    condition_types={"A": "fuzzy", "B": "crisp"},
)

result = model.fit(
    consistency_cutoff=0.8,
    coverage_cutoff=0.1,
)

print(result.truth_table)
print(result.solutions)
```

---

### 4. GSQCA

Truth-table grouping uses temporary column names that do not collide with input columns. Condition names beginning with `__crisp_` or `__gsqca_corner_` are supported without changing case assignments or minimization results.

Explicit `value_columns` aliases are resolved from the original input before any canonical columns are written. Swapped aliases, shared source columns, and sources used by other explicitly mapped conditions are allowed and independent of mapping/condition order. A target that collides with the outcome, case ID, a condition name, or another value-membership target (including an implicitly resolved value) raises `ValueError`. Explicit aliases still take precedence over competing conventional columns for the same value. All source columns must exist in the original input; the caller's DataFrame is not modified.

`GSQCA` is the high-level interface for generalized-set QCA workflows
containing crisp, multi-value, and fuzzy-set conditions.

It treats csQCA, mvQCA, and fsQCA as special cases. Use `GSQCA` for workflows
that combine condition kinds.
The implemented semantics are documented by PyQCA and are not presented as a
drop-in clone of any single external gsQCA package.

```python
from qca import GSQCA

model = GSQCA(
    data=df,
    outcome="Y",
    conditions=["A", "B", "C", "D"],
    condition_types={
        "A": "crisp",
        "B": "multi",
        "C": "fuzzy",
        "D": "fuzzy",
    },
)

result = model.fit(
    consistency_cutoff=0.8,
    coverage_cutoff=0.1,
    minimizer="standard",
)

print(result.summary())
```

The same workflow can be built from the unified condition schema:

```python
schema = [
    {"name": "A", "type": "crisp", "domain": [0, 1], "calibrated": True},
    {"name": "B", "type": "multi-value", "domain": ["low", "high"]},
    {"name": "C", "type": "fuzzy", "domain": [0, 1], "calibrated": True},
]

model = GSQCA.from_schema(data=df, outcome="Y", schema=schema)

print(model.workflow)          # GSQCA-crisp-fuzzy-multi
print(model.condition_schema)  # normalized schema DataFrame
```

For multivalent fuzzy set variables, declare value-specific calibrated
membership columns:

```python
schema = [
    {
        "name": "sector",
        "type": "multi-value",
        "calibrated": True,
        "value_columns": {
            "public": "sector_public",
            "private": "sector_private",
        },
    },
]
```

Fitted results report `qca_type="GSQCA"`.

---

### 5. Pluggable Minimization Backends

Directional expectations must map existing condition names to values in their domains (0/1 for crisp or fuzzy conditions; a declared or observed category for multi-value conditions). Unknown names and out-of-domain values raise `ValueError` before minimization, including when no positive rows exist. A partial mapping leaves omitted conditions unconstrained. `None` retains the warning and uses the complex solution as the intermediate solution; `{}` explicitly applies no directional restrictions to safe logical remainders. To leave a condition unconstrained, omit its key rather than assigning `None`.

PyQCA separates the QCA engine from the minimization algorithm.

This makes it possible to compare standard Boolean minimization with alternative optimization-based approaches.

```python
result = model.fit(
    minimizer="standard",
)
```

```python
result = model.fit(
    minimizer="set_cover",
)
```

Available minimizers:

| Minimizer          | Description                                          |
| ------------------ | ---------------------------------------------------- |
| `standard`         | Prime implicants with exact cover selection                |
| `qmc`              | Same exact algorithm as standard |
| `set_cover`        | Exact cover selection, as in the standard backend |
| `greedy_set_cover` | Greedy approximation for large candidate rule spaces |
| `exact_set_cover`  | Exact set-covering optimization backend              |

Exact cover search can take exponential time. Use `greedy_set_cover` explicitly when approximate cover selection is acceptable; shared prime generation can still be expensive.

Backends can also be benchmarked against the same model and thresholds:

```python
from qca import benchmark_minimizers

bench = benchmark_minimizers(
    model,
    minimizers=["standard", "qmc", "greedy_set_cover", "exact_set_cover"],
    outcome_threshold=0.75,
    consistency_cutoff=0.75,
)

print(bench.summary())
```

---

### 6. ThresholdSweep

Threshold binarization (`qca_bin`) returns floating-point memberships: finite values at or above the threshold become 1, and lower values become 0. NaN and positive/negative infinity remain missing (NaN), including after outcome negation and in `pre_calibrated` conditions. Thresholds must be finite. Preparation preserves these missing values; every threshold-sweep path rejects a run containing them, reporting affected columns and row counts in the summary's `error` field. Resolve or explicitly exclude those rows before analysis; the sweep does not silently impute or drop them.

PyQCA includes a threshold-sweep analysis layer inspired by the ThS-QCA framework.

Instead of treating thresholds as fixed preprocessing choices, `ThresholdSweep` treats them as explicit analytical variables.

```python
import pandas as pd

from qca import ThresholdSweep

raw_df = pd.DataFrame(
    {
        "case": [f"c{i}" for i in range(1, 9)],
        "X1": [9, 8, 8, 7, 6, 5, 4, 3],
        "X2": [8, 8, 7, 6, 7, 5, 4, 2],
        "X3": [9, 7, 8, 6, 5, 6, 3, 2],
        "Y": [10, 9, 9, 8, 7, 6, 4, 2],
    }
)

sweep = ThresholdSweep(
    raw_df,
    outcome="Y",
    conditions=["X1", "X2", "X3"],
    case_id="case",
)

result = sweep.outcome(
    thresholds=[6, 7, 8, 9],
    condition_thresholds={"X1": 7, "X2": 7, "X3": 7},
    incl_cut=0.8,
    n_cut=1,
    coverage_cutoff=0.1,
)

print(result.summary_df)
```

`ThresholdSweep` follows the ThSQCA threshold algorithm:

* raw variables are binarized with `x >= threshold`;
* the outcome is always threshold-binarized for each run;
* pre-calibrated condition variables can be passed through with `pre_calibrated`;
* `outcome`, `condition`, `multi_condition`, and `dual` mirror ThSQCA's `otSweep`, `ctSweepS`, `ctSweepM`, and `dtSweep` modes.

Available sweep modes:

| API                       | Purpose                                        |
| ------------------------- | ---------------------------------------------- |
| `sweep.outcome()`         | Sweep outcome thresholds                       |
| `sweep.condition()`       | Sweep a single condition threshold             |
| `sweep.multi_condition()` | Sweep a grid of multiple condition thresholds  |
| `sweep.dual()`            | Sweep outcome and condition thresholds jointly |
| `sweep.fuzzy_anchors()`   | Sweep fsQCA calibration anchors                |

---

### 7. Machine-Learning-Enhanced QCA

The optional `qca.mlqca` workflow connects predictive modeling to PyQCA's
native QCA engines. It uses XGBoost to rank candidate conditions and extract
split thresholds, then applies PyQCA calibration, csQCA or gsQCA analysis,
radical or theory-constrained model search, Pareto evaluation, and
cross-validation or bootstrap stability analysis.

```python
from qca import (
    MLQCAConfig,
    fit_xgboost_predictor,
    search_csqca_models,
    validate_mlqca_input,
)

# raw_df contains numeric candidate conditions and a binary outcome Y.
validated = validate_mlqca_input(raw_df, outcome="Y")
config = MLQCAConfig(mode="radical", top_k=10, model_size=4)
predictor = fit_xgboost_predictor(validated, config)
result = search_csqca_models(validated, predictor, config)
```

XGBoost and scikit-learn are optional dependencies. See
[Machine-Learning-Enhanced QCA Workflow](#machine-learning-enhanced-qca-workflow) for the
complete workflow, gsQCA integration, stability analysis, and published
example reproduction.

---

## Example: Outcome Threshold Sweep

```python
import pandas as pd
from qca.sweep import ThresholdSweep

sweep_df = pd.DataFrame(
    {
        "case": [f"c{i}" for i in range(1, 13)],
        "A": [9, 8, 8, 7, 7, 6, 6, 5, 5, 4, 3, 2],
        "B": [8, 8, 7, 7, 6, 6, 5, 5, 4, 4, 3, 2],
        "C": [9, 7, 8, 6, 7, 5, 6, 4, 5, 3, 4, 2],
        "Y": [10, 9, 9, 8, 8, 7, 7, 6, 5, 4, 3, 2],
    }
)

sweep = ThresholdSweep(
    sweep_df,
    outcome="Y",
    conditions=["A", "B", "C"],
    case_id="case",
)

res = sweep.outcome(
    thresholds=[5, 6, 7, 8, 9],
    condition_thresholds={"A": 7, "B": 7, "C": 7},
    minimizer="set_cover",
    incl_cut=0.85,
    n_cut=1,
    coverage_cutoff=0.1,
)

res.summary_df
```

Example output:

| thrY | expression          | consistency | coverage | n_solutions | solution_type | valid |
| ---: | ------------------- | ----------: | -------: | ----------: | ------------- | :---- |
|  5.0 | (A * C) + (A * B)  |        1.00 |     0.56 |           1 | complex       | True  |
|  6.0 | (A * C) + (A * B)  |        1.00 |     0.63 |           1 | complex       | True  |
|  7.0 | (A * C) + (A * B)  |        1.00 |     0.71 |           1 | complex       | True  |
|  8.0 | (A * C) + (A * B)  |        1.00 |     1.00 |           1 | complex       | True  |
|  9.0 | (A * B * C)        |        1.00 |     1.00 |           1 | complex       | True  |

This helps researchers inspect how sufficient configurations emerge, disappear, branch, or become more complex as thresholds change.

---

## Example: fsQCA Calibration Anchor Sweep

For `AnchorSensitivity`, dictionary `dir_exp` keys may use either the original condition name or its calibrated name (default `<condition>_fuzzy`, or custom `out_col`). Original names are mapped before minimization; unchanged conditions keep their names. Matching duplicate aliases are accepted, but conflicting values produce an error for the run. Unknown names and invalid values remain errors. Scalar and sequence specifications retain their existing meaning and condition order. mlQCA adapters continue to accept calibrated condition names, including for low-direction calibration.

For fuzzy-set QCA, threshold sensitivity often appears through calibration anchors: full non-membership, crossover, and full membership.

PyQCA supports anchor sweep analysis through `AnchorSensitivity` and the convenience `ThresholdSweep.fuzzy_anchors()` adapter.

```python
import pandas as pd
from qca import ThresholdSweep

anchor_df = pd.DataFrame(
    {
        "case": [f"c{i}" for i in range(1, 11)],
        "Budget": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
        "Capacity": [0, 0, 0, 1, 1, 0, 1, 1, 1, 1],
        "Y": [0.05, 0.10, 0.20, 0.40, 0.55, 0.50, 0.70, 0.80, 0.90, 0.95],
    }
)

anchor_sweep = ThresholdSweep(
    anchor_df,
    outcome="Y",
    conditions=["Budget", "Capacity"],
    case_id="case",
)

res = anchor_sweep.fuzzy_anchors(
    condition="Budget",
    anchor_grid={
        "full_out": [1, 2, 3],
        "crossover": [5, 6],
        "full_in": [8, 9, 10],
    },
    incl_cut=0.8,
    outcome_threshold=0.75,
)

print(res.stability)
print(res.to_markdown())
res.plot_heatmap()
```

The dedicated API can also sweep one anchor family at a time:

```python
from qca import AnchorSensitivity

anchors = AnchorSensitivity(
    anchor_df,
    outcome="Y",
    conditions=["Budget", "Capacity"],
    case_id="case",
)

res = anchors.crossover(
    "Budget",
    values=[5, 6, 7],
    full_out=1,
    full_in=10,
    incl_cut=0.8,
)
```

This extends threshold-sweep analysis beyond crisp-set dichotomization and into fuzzy-set calibration sensitivity.

---

## Example: Reporting and Reproducibility

```python
from pathlib import Path

from qca import (
    ExperimentLogger,
    collect_reproducibility_metadata,
    generate_markdown_report,
    jupyter_summary,
    to_latex_table,
)
from qca.viz import plot_configuration_chart

result = model.fit(consistency_cutoff=0.8)

reports = Path("reports")
reports.mkdir(parents=True, exist_ok=True)

generate_markdown_report(result, path="reports/qca_result.md")
to_latex_table(result, table="solutions", path="reports/solutions.tex")

metadata = collect_reproducibility_metadata(result, extra={"dataset": "study-1"})
ExperimentLogger("reports/experiments.jsonl").log(result, name="baseline")

summary = jupyter_summary(result)
plot_configuration_chart(result)
```

These helpers keep publication-oriented exports, notebook inspection, and run metadata close to the analysis object.

---

## Machine-Learning-Enhanced QCA Workflow

The optional `qca.mlqca` package implements a Python-native workflow inspired
by the published mlQCA protocol. XGBoost is used to rank candidate conditions
and extract split thresholds; calibration, model search, QCA evaluation,
reporting, and stability analysis are performed by PyQCA.

Install the optional dependencies:

```bash
pip install -e ".[mlqca]"
```

The `mlqca-explain` extra additionally installs SHAP for external exploratory
work. PyQCA's built-in importance table uses XGBoost prediction contributions
and does not require the standalone SHAP package.

### XGBoost, Calibration, and QCA Search

```python
import numpy as np
import pandas as pd

from qca import (
    MLQCAConfig,
    fit_xgboost_predictor,
    search_csqca_models,
    validate_mlqca_input,
)

rng = np.random.default_rng(201)
raw_df = pd.DataFrame(
    {
        "policy": rng.normal(size=80),
        "capacity": rng.normal(size=80),
        "support": rng.normal(size=80),
    }
)
raw_df["Y"] = (
    raw_df["policy"] + 0.5 * raw_df["capacity"] > 0
).astype(int)

validated = validate_mlqca_input(raw_df, outcome="Y")
config = MLQCAConfig(
    mode="radical",
    top_k=3,
    model_size=2,
    random_state=201,
    model_params={
        "n_estimators": 100,
        "max_depth": 3,
        "learning_rate": 0.2,
    },
)

predictor = fit_xgboost_predictor(validated, config)
result = search_csqca_models(validated, predictor, config)

print(predictor.feature_importance)
print(predictor.cutoff_candidates)
print(result.pareto_models)
print(result.best_qca_result)
```

`mode="radical"` evaluates combinations from the top-ranked conditions.
`mode="conservative"` requires theory-driven constraints through required or
excluded conditions, or explicit `conservative_models`.

Generalized-set workflows combining crisp, fuzzy, and multi-value conditions
can be connected to `GSQCA` with `fit_gsqca_from_predictor()`. Fuzzy anchor
proposals can be evaluated with `run_anchor_sensitivity()`.

### Cross-Validation and Bootstrap Stability

PyQCA provides stratified cross-validation and stratified bootstrap analysis.
Cross-validation evaluates each model on its held-out fold. Bootstrap uses
out-of-bag cases when both outcome classes are available.

```python
from pathlib import Path

from qca import bootstrap_mlqca, cross_validate_mlqca

cv_result = cross_validate_mlqca(
    validated,
    config,
    n_splits=5,
    top_k=3,
)
bootstrap_result = bootstrap_mlqca(
    validated,
    config,
    n_bootstrap=100,
    top_k=3,
)

print(cv_result.run_summary)
print(cv_result.feature_stability)
print(bootstrap_result.cutoff_stability)

Path("reports").mkdir(parents=True, exist_ok=True)
cv_result.to_markdown("reports/mlqca_cv.md")
```

The stability tables report top-condition selection rates, feature-use rates,
rank variation, contribution variation, and rounded cutoff-selection rates.
Use a fixed `random_state` for reproducible resampling.

### Published Example Reproduction

The test suite includes a mechanically converted copy of the public
`voteData` fixture from the mlQCA repository. With seed `201` and a
representative parameter setting from the published search space, the
reproduction test:

* fits all 427 published cases;
* reproduces training accuracy and ROC AUC of `1.0`;
* recovers at least five of the six conditions highlighted by the tutorial
  within PyQCA's top ten conditions;
* confirms the expected 210 four-condition combinations from ten candidates.

This is a protocol-level reproduction test, not a claim that Python XGBoost
must produce byte-identical trees or rankings to the R/caret workflow.
Features near the top-ten boundary can vary across XGBoost and Python runtime
versions. Fixture provenance is documented in `tests/data/README.md` and
`THIRD_PARTY_NOTICES.md`.

---

## Architecture

```text
qca
├── __init__.py
├── _constants.py
├── _types.py
├── _version.py
│
├── calibration
│   ├── __init__.py
│   ├── _validators.py
│   ├── crisp.py
│   ├── logistic.py
│   └── piecewise.py
│
├── core
│   ├── __init__.py
│   ├── conditions.py
│   ├── literals.py
│   └── results.py
│
├── engines
│   ├── __init__.py
│   ├── _helpers.py
│   ├── base.py
│   ├── csqca.py
│   ├── fsqca.py
│   ├── mvqca.py
│   └── gsqca.py
│
├── minimizers
│   ├── __init__.py
│   ├── algorithms.py
│   ├── backends.py
│   ├── benchmark.py
│   ├── engine.py
│   ├── implicant.py
│   ├── qmc.py
│   ├── remainder.py
│   ├── set_cover.py
│   └── standard.py
│
├── mlqca
│   ├── __init__.py
│   ├── _optional.py
│   ├── backend.py
│   ├── calibration.py
│   ├── config.py
│   ├── csqca.py
│   ├── cutoffs.py
│   ├── fuzzy.py
│   ├── importance.py
│   ├── gsqca.py
│   ├── reporting.py
│   ├── reproducibility.py
│   ├── results.py
│   ├── schema.py
│   ├── search.py
│   ├── stability.py
│   ├── validation.py
│   ├── viz.py
│   └── xgboost.py
│
├── reporting
│   ├── __init__.py
│   ├── _formatters.py
│   ├── export.py
│   ├── jupyter.py
│   ├── report.py
│   └── reproducibility.py
│
├── results
│   ├── __init__.py
│   └── solution.py
│
├── sweep
│   ├── __init__.py
│   ├── anchors.py
│   └── threshold.py
│
└── viz
    ├── __init__.py
    ├── _helpers.py
    ├── bar_plots.py
    ├── configuration_chart.py
    ├── coverage.py
    ├── dashboard.py
    ├── distribution.py
    ├── truth_table.py
    └── xy_plot.py
```

---

Current engine architecture:

* `QCAEngineBase` holds the shared validation, truth-table, minimization, and
  result-building core.
* `CSQCA`, `FSQCA`, `MVQCA`, and `GSQCA` are first-class engines in their own
  modules: `qca.engines.csqca`, `qca.engines.fsqca`, `qca.engines.mvqca`, and
  `qca.engines.gsqca`.
* `GSQCA` is the first-class generalized-set engine for workflows that combine
  crisp, fuzzy, and multi-value condition kinds.
* `qca.core` owns the shared condition schema, literal objects, and result
  dataclasses; engine classes are exposed only from `qca.engines` and the
  package-level API.
* Private modules prefixed with `_` contain shared implementation details and
  are not part of the stable public API.

## Design Principles

### 1. Python-native

PyQCA is designed for Python workflows from the ground up.

```python
import pandas as pd
from qca import FSQCA
```

The library should work naturally with pandas DataFrames, Jupyter notebooks, Python visualization tools, and scientific computing libraries.

---

### 2. Methodologically transparent

PyQCA should expose intermediate objects such as:

* calibrated data;
* truth tables;
* candidate configurations;
* minimized solutions;
* consistency and coverage scores;
* case-level coverage;
* threshold-sweep results;
* mlQCA feature-importance and split-threshold tables;
* calibration and anchor proposals;
* candidate-model rankings and Pareto frontiers;
* cross-validation and bootstrap stability tables.

QCA and mlQCA should not behave like black boxes. Predictive evidence,
calibration choices, condition combinations, failed model evaluations, and
resampling stability remain inspectable instead of being hidden behind a
single selected solution.

---

### 3. GSQCA by design

Many real datasets contain crisp, categorical, ordinal, and fuzzy-set variables.

PyQCA treats generalized-set QCA as a first-class use case through `GSQCA`.

---

### 4. Pluggable algorithms

Minimization is treated as a replaceable backend.

This enables comparisons between standard Boolean minimization, Quine-McCluskey-style minimization, set-covering-based minimization, and future optimization-based approaches.

---

### 5. Thresholds as analytical variables

Threshold choices can strongly influence QCA results.

PyQCA’s sweep layer is designed to help researchers examine how results change across reasonable threshold spaces instead of relying on a single fixed calibration choice.

---

### 6. Machine learning as evidence, not authority

PyQCA treats machine learning as an empirical aid for condition screening,
calibration proposals, and model-space exploration. Predictive importance does
not replace theoretical knowledge, causal interpretation, or QCA assumptions.

The `radical` mlQCA mode explores combinations from the ranked condition set.
The `conservative` mode requires theory-driven constraints. Pareto evaluation,
published-example reproduction, and resampling stability are exposed so that
researchers can assess how strongly a proposed configuration depends on one
model fit or one calibration choice.

---

## Installation

This branch prepares **0.3.0**. See the [release notes](https://pyqca.readthedocs.io/en/latest/release_notes.html) for changes that can affect existing analyses.

For development installation:

```bash
git clone https://github.com/t-yamsaki/PyQCA.git
cd PyQCA
pip install -e .
```

Install optional feature groups as needed:

```bash
pip install -e ".[viz]"
pip install -e ".[mlqca]"
pip install -e ".[mlqca-explain]"
```

After version 0.3.0 is published to PyPI:

```bash
python -m pip install "pyqca==0.3.0"
```

---

## Basic Usage

```python
import pandas as pd
from qca import GSQCA

df = pd.DataFrame(
    {
        "case": [f"c{i}" for i in range(1, 9)],
        "A": [1, 1, 1, 1, 0, 0, 0, 0],
        "B": ["high", "high", "low", "low", "high", "high", "low", "low"],
        "C": [0.9, 0.8, 0.7, 0.6, 0.4, 0.3, 0.2, 0.1],
        "Y": [0.95, 0.85, 0.80, 0.70, 0.40, 0.30, 0.20, 0.10],
    }
)

model = GSQCA(
    data=df,
    case_id="case",
    outcome="Y",
    conditions=["A", "B", "C"],
    condition_types={
        "A": "crisp",
        "B": "multi",
        "C": "fuzzy",
    },
)

result = model.fit(
    consistency_cutoff=0.8,
    coverage_cutoff=0.1,
    minimizer="standard",
)

print(result.summary())
```

---

## Result Objects

PyQCA result objects are designed to be inspectable and exportable.

```python
result.truth_table
result.solutions
result.formula
result.formulas
result.consistency
result.coverage
result.case_coverage
result.condition_schema
result.to_dataframe()
result.to_formula()
result.to_formulas()
result.export_formula("solution.txt")
result.to_markdown()
```

Threshold-sweep results provide additional fields:

```python
sweep_result.summary_df
sweep_result.settings
sweep_result.solutions
sweep_result.truth_tables
sweep_result.stability
sweep_result.to_markdown()
sweep_result.plot_heatmap()
sweep_result.plot_trajectory()
```

---

## Relationship to Existing QCA Tools

PyQCA is inspired by the broader QCA software ecosystem, especially mature R-based workflows.

Its intended position is:

| Tool / Approach         | Main Role                                                                                                                              |
| ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| R `QCA` package         | Established QCA workflow in R                                                                                                          |
| scpQCA-style algorithms | Set-covering-based alternative minimization                                                                                            |
| ThS-QCA framework       | Threshold-sweep sensitivity analysis                                                                                                   |
| R `mlQCA` package       | Published machine-learning-enhanced QCA protocol using predictive condition selection and calibration evidence                        |
| PyQCA                   | Python-native QCA platform integrating core QCA and GSQCA, minimization, sensitivity analysis, and an independent `qca.mlqca` workflow |

PyQCA aims to complement existing tools, not replace them. Its `qca.mlqca`
package is methodologically informed by Huang's mlQCA work but independently
implemented with PyQCA data models, engines, result objects, reporting, and
stability analysis. See `THIRD_PARTY_NOTICES.md` and `PROVENANCE_AUDIT.md`.

---

## Research Use Cases

PyQCA is designed for research workflows such as:

* configurational analysis in social science;
* management and organization studies;
* policy evaluation;
* marketing and advertising strategy analysis;
* small-N and medium-N causal analysis;
* robustness and sensitivity analysis of QCA results;
* comparison of minimization algorithms;
* QCA-based interpretable rule discovery;
* integration with machine learning pipelines.

---

## Example Research Workflow

```python
from pathlib import Path

import pandas as pd

from qca import GSQCA, ThresholdSweep

# 1. Analyze calibrated generalized-set conditions.
gsqca_df = pd.DataFrame(
    {
        "case": [f"c{i}" for i in range(1, 9)],
        "Strategy": ["focus", "focus", "broad", "broad"] * 2,
        "Budget": [0.9, 0.8, 0.7, 0.6, 0.4, 0.3, 0.2, 0.1],
        "CreativeQuality": [0.8, 0.9, 0.6, 0.7, 0.3, 0.4, 0.1, 0.2],
        "MarketFit": [1, 1, 1, 0, 1, 0, 0, 0],
        "HighPerformance": [0.95, 0.90, 0.80, 0.65, 0.45, 0.35, 0.20, 0.10],
    }
)

model = GSQCA(
    data=gsqca_df,
    case_id="case",
    outcome="HighPerformance",
    conditions=["Strategy", "Budget", "CreativeQuality", "MarketFit"],
    condition_types={
        "Strategy": "multi",
        "Budget": "fuzzy",
        "CreativeQuality": "fuzzy",
        "MarketFit": "crisp",
    },
)

# 2. Run standard QCA
result = model.fit(
    consistency_cutoff=0.8,
    coverage_cutoff=0.1,
    minimizer="standard",
)

# 3. Run set-covering minimization
scp_result = model.fit(
    consistency_cutoff=0.8,
    coverage_cutoff=0.1,
    minimizer="set_cover",
)

# 4. Run threshold sweep
raw_df = pd.DataFrame(
    {
        "case": [f"c{i}" for i in range(1, 13)],
        "Strategy": [9, 8, 8, 7, 7, 6, 6, 5, 5, 4, 3, 2],
        "Budget": [8, 8, 7, 7, 6, 6, 5, 5, 4, 4, 3, 2],
        "CreativeQuality": [9, 7, 8, 6, 7, 5, 6, 4, 5, 3, 4, 2],
        "MarketFit": [8, 9, 7, 8, 6, 7, 5, 6, 4, 5, 3, 2],
        "HighPerformance": [10, 9, 9, 8, 8, 7, 7, 6, 5, 4, 3, 2],
    }
)

sweep = ThresholdSweep(
    raw_df,
    outcome="HighPerformance",
    conditions=["Strategy", "Budget", "CreativeQuality", "MarketFit"],
    case_id="case",
)

sweep_result = sweep.outcome(
    thresholds=[6, 7, 8, 9],
    condition_thresholds={
        "Strategy": 7,
        "Budget": 7,
        "CreativeQuality": 7,
        "MarketFit": 7,
    },
    incl_cut=0.8,
    n_cut=1,
    minimizer="set_cover",
)

# 5. Export report
Path("reports").mkdir(parents=True, exist_ok=True)
sweep_result.to_markdown("reports/threshold_sweep.md")
```

---

## Citation

If you use PyQCA in academic work, please cite the software using
[`CITATION.cff`](https://github.com/t-yamsaki/PyQCA/blob/main/CITATION.cff).

Suggested citation:

```bibtex
@software{pyqca,
  title = {PyQCA: A Python-native toolkit for core, generalized-set, threshold-sweep, and machine-learning-enhanced QCA},
  author = {Yamasaki, Taishi},
  year = {2026},
  url = {https://github.com/t-yamsaki/PyQCA}
}
```

PyQCA’s threshold-sweep layer is inspired by the ThS-QCA framework:

```bibtex
@article{toyoda2026thsqca,
  title = {ThS-QCA: Threshold-Sweep Qualitative Comparative Analysis in R},
  author = {Toyoda, Yuki},
  year = {2026},
  journal = {arXiv preprint arXiv:2601.11229}
}
```

When using `qca.mlqca`, please also cite the methodological article:

```bibtex
@article{huang2025mlqca,
  title = {Towards machine-learning enhanced QCA: optimizing coverage and empirical significance},
  author = {Huang, Qin},
  year = {2025},
  journal = {Quality \& Quantity},
  volume = {59},
  pages = {4259--4281},
  doi = {10.1007/s11135-025-02146-2}
}
```

The PyQCA citation identifies the software implementation. The Huang citation
identifies the mlQCA methodology that informed the optional workflow.

---

## License

This project is released under the MIT License.

See [`LICENSE`](https://github.com/t-yamsaki/PyQCA/blob/main/LICENSE) for the project license,
[`THIRD_PARTY_NOTICES.md`](https://github.com/t-yamsaki/PyQCA/blob/main/THIRD_PARTY_NOTICES.md) for attribution, and
[`PROVENANCE_AUDIT.md`](https://github.com/t-yamsaki/PyQCA/blob/main/PROVENANCE_AUDIT.md) for the source-origin review.

---

## Contributing

Contributions are welcome.

Contributions are accepted under the project's MIT License. See
[`CONTRIBUTING.md`](https://github.com/t-yamsaki/PyQCA/blob/main/CONTRIBUTING.md) for development, attribution, and
provenance requirements.

Potential contribution areas:

* QCA algorithm implementation;
* minimization backends;
* benchmarking against R QCA outputs;
* fsQCA calibration utilities;
* threshold-sweep visualization;
* mlQCA backends, calibration strategies, and stability diagnostics;
* reproduction studies comparing PyQCA with published mlQCA examples;
* documentation and tutorials;
* examples from applied research.

Contributions to `qca.mlqca` must document methodological sources, dataset
provenance, dependency licenses, and whether code was independently written or
adapted. Do not translate or copy GPL-licensed R source into the MIT-licensed
codebase. Reproduction fixtures must have redistribution-compatible licenses.

Please open an issue before submitting a large pull request.

---

## Project Status

PyQCA is currently experimental.

The current development priority is methodological correctness,
reproducibility, and a clear public API. Breaking changes may occur before
v1.0.

Recommended use for now:

* exploratory research;
* prototype analysis;
* benchmarking;
* educational examples;
* methodological experimentation.

For publications and high-stakes applied research, validate important results
against established QCA software, methodological expectations, and the source
data.

Future work is tracked through
[GitHub Issues](https://github.com/t-yamsaki/PyQCA/issues). Published versions
and release notes will be available through
[GitHub Releases](https://github.com/t-yamsaki/PyQCA/releases).

## Numerical semantics and compatibility

See the [0.3.0 release notes](https://pyqca.readthedocs.io/en/latest/release_notes.html) for migration details.

PRI uses the standard triple intersection of configuration, outcome, and negated outcome. This corrects row selection with `pri_cut`; see [PRI semantics](https://pyqca.readthedocs.io/en/latest/user_guide/core_qca.html).

The default `standard` and `qmc` backends now use exact cover selection: fewest terms, then lowest total logical complexity. One optimal cover is returned; equally optimal alternatives are not enumerated. Exact search can take exponential time and never silently falls back to an approximation. Use `greedy_set_cover` explicitly for approximate selection. See [minimization semantics](https://pyqca.readthedocs.io/en/latest/user_guide/minimization.html).

Sufficiency metrics now retain positive fuzzy memberships even when every score is at most 0.5. Empirical case-count filters remain separate; see [low-membership sufficiency](https://pyqca.readthedocs.io/en/latest/user_guide/core_qca.html).

Logistic calibration now honors asymmetric three-anchor specifications using separate log-odds slopes around the crossover. Custom targets must straddle 0.5; asymmetric curves expose segment coefficients instead of a single slope/intercept. See [calibration semantics](https://pyqca.readthedocs.io/en/latest/user_guide/calibration.html).

Cases with a set-condition membership exactly 0.5 are now excluded from truth-table row assignment and frequency counts, with a warning. All original cases still contribute to consistency and PRI. See [crossover semantics](https://pyqca.readthedocs.io/en/latest/user_guide/core_qca.html).

Multi-value engines resolve memberships using the same condition schema as truth-table assignment. A categorical condition uses its category column even when auxiliary columns named `A=0`, `A[0]`, or `A__0` exist. GSQCA uses value memberships when `value_columns` is declared, `calibrated=True` is specified with value columns, or only value columns are present. The standalone `MultiValueLiteral.membership(data)` API has no engine schema and retains its value-column-first behavior.

Solution formulas and coverage-table labels preserve multi-value categories explicitly, including `A=0`, `A=1`, and `A=2`. Crisp/fuzzy set conditions retain `A` and `~A` notation. Low-level `Implicant.label` and `build_coverage_table` accept optional `is_multivalue` flags; callers without schema metadata retain legacy integer 0/1 set notation.

Necessary-condition triviality now flags exact universal necessary conditions (all condition memberships equal 1), rather than high coverage. Low coverage alone does not establish triviality. Manually constructed necessary results without the optional `is_universal` evidence return `is_trivial=None`. See [triviality semantics and compatibility](https://pyqca.readthedocs.io/en/latest/user_guide/core_qca.html).

Truth-table `include` now reflects `frequency_cutoff` together with consistency and PRI, both in `fit` results and standalone `build_truth_table`. Low-frequency rows remain visible and are still treated as logical remainders during minimization. See [frequency and inclusion](https://pyqca.readthedocs.io/en/latest/user_guide/core_qca.html).

Necessity consistency and coverage now retain positive membership sums even below `1e-12`; only truly zero denominators return zero. Case counts are unchanged. See [low-membership necessity](https://pyqca.readthedocs.io/en/latest/user_guide/core_qca.html).

Intermediate solutions use easy counterfactuals supported by observed positive configurations and bounded by the selected parsimonious solution. They preserve complex subset intermediate subset parsimonious logic. An empty direction mapping uses the selected parsimonious region; `None` retains the complex-solution fallback. See [intermediate solutions](https://pyqca.readthedocs.io/en/latest/user_guide/minimization.html).

mlQCA resolves XGBoost feature names against model metadata. SHAP direction inference uses a scale-stable correlation; unidentified directions require an explicit override instead of using the sign of mean SHAP contributions. Crisp/fuzzy calibration applies normalized direction aliases to both memberships and returned proposals, without modifying the input table. Anchor sensitivity accepts original or calibrated names for the same calibrated set, including low-direction features. See [mlQCA semantics](https://pyqca.readthedocs.io/en/latest/user_guide/mlqca.html).
