Skip to content

Commit 878d7ca

Browse files
committed
docs: add multi-value guide
1 parent d000357 commit 878d7ca

5 files changed

Lines changed: 109 additions & 10 deletions

File tree

README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,6 @@ contract.
197197
## Non-goals for 0.1
198198

199199
- claiming complete parity with R `QCA`;
200-
- mvQCA;
201200
- tQCA;
202201
- CCubes/eQMC performance parity.
203202

docs/ROADMAP.md

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,8 @@
2727
- optional R-compatible calibration snapping, so extreme memberships can be
2828
reported exactly as R does when replicating an existing analysis
2929

30-
## 0.3 — multi-value and performance
30+
## 0.3 — performance
3131

32-
- mvQCA
33-
- categorical-set expressions
3432
- faster bitset/cube minimiser
3533
- prime-implicant consistency filters
3634
- row dominance

docs/guide/multivalue.md

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
# Multi-value QCA
2+
3+
A multi-value condition takes one of several unordered categories — regime type,
4+
welfare regime, sector — rather than being present or absent. Forcing such a
5+
condition into a binary set either loses information or invents a dichotomy the
6+
concept does not have.
7+
8+
```python
9+
from setqca.multivalue import MVQCA
10+
11+
result = MVQCA(consistency=0.8).fit(
12+
data, outcome="Y", conditions=["regime", "wealth"]
13+
)
14+
print(result)
15+
print(result.truth_table.to_frame())
16+
print(result.summary_frame("parsimonious"))
17+
```
18+
19+
Conditions hold integer category codes from `0`; the outcome is a membership in
20+
`[0, 1]`. The workflow deliberately mirrors `FSQCA` and `CSQCA` — moving between
21+
them is a change of estimator, not a change of method.
22+
23+
## Notation
24+
25+
`regime{0,2}*wealth{1}` reads "regime is 0 or 2, and wealth is 1". A condition
26+
allowing *every* level constrains nothing and is omitted from the expression, so
27+
the binary case reduces to familiar QCA notation.
28+
29+
## The property space
30+
31+
```python
32+
result.domain # regime{0,1,2}, wealth{0,1}
33+
result.domain.size # 6 logically possible configurations
34+
```
35+
36+
Configurations are indexed in mixed radix, which generalises the binary minterm
37+
and reduces to it exactly when every condition has two levels.
38+
39+
!!! warning "Declare levels that have no cases"
40+
Levels are inferred from the data, which understates a category that is
41+
theoretically possible but happens to be unobserved. That matters: an
42+
unobserved level is a *remainder*, and remainders change the parsimonious
43+
solution.
44+
45+
```python
46+
MVQCA(levels={"regime": 4, "wealth": 2}).fit(...)
47+
```
48+
49+
Declaring fewer levels than the data contain is an error.
50+
51+
## Why not Boolean dummies
52+
53+
The obvious shortcut is to encode `A{0,1,2}` as three binary indicators and
54+
reuse the binary minimiser. **That transformation does not preserve the
55+
semantics.**
56+
57+
The binary space contains points such as `A_0 = A_1 = 1` — a case that is
58+
simultaneously in two mutually exclusive categories, which corresponds to no
59+
configuration at all. The minimiser is free to build implicants across those
60+
points, producing terms that look valid and describe nothing. Recovering a
61+
multi-value expression afterwards requires exactly the mutual-exclusivity
62+
constraints the encoding threw away.
63+
64+
So the cube algebra is implemented directly. A cube allows a **set** of levels
65+
per condition, and merging generalises the binary rule:
66+
67+
> two cubes that agree on every condition but one merge into a single cube whose
68+
> set at that condition is the union of the two.
69+
70+
Because the two cubes agree everywhere else, the merged cube covers exactly
71+
their union and nothing more — the same property the binary rule relies on. A
72+
test asserts precisely that, and another asserts every cube covers only real
73+
configurations.
74+
75+
The exact cover is then solved by the **same verified solver the binary engine
76+
uses**, so both inherit one exactness guarantee rather than two implementations.
77+
Minimisation is checked against exhaustive enumeration for four different level
78+
combinations, and against the binary minimiser for every three-condition
79+
problem.
80+
81+
## Agreement with R
82+
83+
R `QCA` supports multi-value and writes literals as `regime[2]`. The truth table
84+
and the parsimonious solution match exactly on the benchmarks in
85+
`validation/fixtures/r_qca.json`.
86+
87+
The conservative solution can differ in *representation*:
88+
89+
```text
90+
R: regime[2] + regime[1]*wealth[1]
91+
setqca: regime{2} + regime{1,2}*wealth{1}
92+
```
93+
94+
Both cover the same configurations and both cost two terms and three literals,
95+
so both are minimal. The difference is that R writes single-value literals only,
96+
while `setqca` also forms subset literals — and here R's `regime[1]*wealth[1]`
97+
is a **proper subset** of `regime{1,2}*wealth{1}`, so R's term is not a prime
98+
implicant. The parity tests therefore compare cost and coverage rather than
99+
text, which is the comparison that carries meaning.
100+
101+
::: setqca.multivalue

docs/guide/truth-tables.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -87,11 +87,11 @@ recorded. A row's outcome code alone conflates situations that call for
8787
different responses:
8888

8989
```python
90-
table.positive_rows() # coded "1"
91-
table.negative_rows() # coded "0"
90+
table.positive_rows() # coded "1"
91+
table.negative_rows() # coded "0"
9292
table.contradictions() # coded "C"
93-
table.remainders() # coded "R"
94-
table.excluded_rows() # kept out by a *threshold*, not by the evidence
93+
table.remainders() # coded "R"
94+
table.excluded_rows() # kept out by a *threshold*, not by the evidence
9595
print(table.summary())
9696
```
9797

@@ -124,8 +124,8 @@ stored and re-minimised without recalibrating or rebuilding:
124124
text = table.to_json()
125125
restored = TruthTable.from_json(text)
126126

127-
restored.minimize() # conservative
128-
restored.minimize(include_remainders=True) # parsimonious
127+
restored.minimize() # conservative
128+
restored.minimize(include_remainders=True) # parsimonious
129129
```
130130

131131
Both agree with the estimator exactly — there are tests asserting so. Only the

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ nav:
4747
- Sufficiency diagnostics: guide/sufficiency-diagnostics.md
4848
- Truth tables: guide/truth-tables.md
4949
- Minimisation: guide/minimisation.md
50+
- Multi-value QCA: guide/multivalue.md
5051
- Robustness: guide/robustness.md
5152
- Methodology: METHODOLOGY.md
5253
- Architecture: ARCHITECTURE.md

0 commit comments

Comments
 (0)