WP35 Phase 3: formal framework (5 theorems, 3 conjectures), D/K/P indices with IRT alternative, measurement diagnostics, a-priori power
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
16 changed files +1,139 −0
added
data/processed/modern_indices.parquet
+0 −0
Binary file not shown.
added
data/processed/seshat_crisis_indices.parquet
+0 −0
Binary file not shown.
added
data/processed/seshat_indices.parquet
+0 −0
Binary file not shown.
added
paper/figures/fig_irt_invariance.pdf
+0 −0
Binary file not shown.
added
paper/figures/fig_measurement_DK.pdf
+0 −0
Binary file not shown.
modified
research/bib_overrides.csv
+2 −0
@@ -47,3 +47,5 @@ marzen2017|10.1098/rsif.2017.0166|Marzen & DeDeo 2017 J R Soc Interface — adde | ||
| 47 | 47 | flack2012|10.1098/rstb.2011.0214|Flack 2012 Phil Trans B — added after novelty search |
| 48 | 48 | hoyer2024|10.31235/osf.io/rk4gd|Hoyer et al. 2024 SocArXiv preprint 'All crises are unhappy in their own way' — no journal version found 2026-11-01 |
| 49 | 49 | wolpert2017|10.1017/9781316584200.010|Wolpert 2017 'The many faces of state space compression' (same volume) |
| 50 | +lindley1952|10.1017/s0305004100027638|Lindley 1952 Math. Proc. Cambridge Phil. Soc. 48 — verified by Crossref (used in framework.md Theorem 5) | |
| 51 | +loynes1962|10.1017/s0305004100036781|Loynes 1962 Math. Proc. Cambridge Phil. Soc. 58 — verified by Crossref | |
added
research/framework.md
+264 −0
@@ -0,0 +1,264 @@ | ||
| 1 | +# WP35 — Formal framework: coordination demand, coordination capacity, and institutions as codes | |
| 2 | + | |
| 3 | +Status labels used throughout: **DEFINITION** (a convention), **THEOREM / PROPOSITION** (a statement proved below | |
| 4 | +or a standard result with the proof reproduced), **CONJECTURE** (a statement we believe is worth testing, with no | |
| 5 | +proof; what would falsify it is stated). Nothing in this file is an empirical finding. The results concern an | |
| 6 | +idealized communication system; §8 states what is *not* claimed and which pre-registered test (if any) can speak to | |
| 7 | +each statement. Citations use the keys of `research/reference_candidates.csv`. | |
| 8 | + | |
| 9 | +--- | |
| 10 | + | |
| 11 | +## 1. Primitives and definitions | |
| 12 | + | |
| 13 | +**DEFINITION 1.1 (agents, tasks, plans).** A society has $N$ agents. In each period, coordination tasks arrive at rate | |
| 14 | +$\lambda$ per agent. A task requires that $k \ge 2$ participants settle on a common element of a finite *plan set* | |
| 15 | +$S$, $|S| = s$, the required plan being drawn independently across tasks from a distribution $p$ on $S$ with Shannon | |
| 16 | +entropy $H(p) = -\sum_{x \in S} p(x)\log_2 p(x) \le \log_2 s$ (shannon1948; cover2006). Communication is counted in | |
| 17 | +*messages* (one transmission between two agents) and in *bits* (the length of what is transmitted). Verification and | |
| 18 | +record-keeping use a second channel whose cost is counted separately as *administrative overhead* (DEFINITION 2.1). | |
| 19 | + | |
| 20 | +**DEFINITION 1.2 (coordination demand).** The coordination demand of a period is the task volume that must be | |
| 21 | +settled: $D = N \lambda k$ participant-slots per period. In the worst case in which every pair of agents must be | |
| 22 | +reconciled, $D$ is bounded by the number of pairs, $N(N-1)/2$, i.e. $D = \Theta(N^2)$; under bounded $\lambda$ and | |
| 23 | +$k$, $D = \Theta(N)$. Either way $D$ is increasing in $N$. | |
| 24 | + | |
| 25 | +**DEFINITION 1.3 (coordination capacity).** Fix a tolerated error rate $\varepsilon \in (0,1)$. The coordination | |
| 26 | +capacity $K$ of an institutional system is the maximal number of tasks it can settle per period such that the | |
| 27 | +fraction of tasks settled on a wrong or inconsistent plan is at most $\varepsilon$. $K$ depends on the available | |
| 28 | +communication budget (bits per period), the code in use (§2), the routing structure (§5) and the overhead (§3). | |
| 29 | + | |
| 30 | +**DEFINITION 1.4 (utilization).** $\rho = D/K$. When $D$ and $K$ are measured in the same units (tasks per period), | |
| 31 | +$\log D - \log K = \log \rho$ exactly. | |
| 32 | + | |
| 33 | +**Limitation (not a theorem).** The pre-registered empirical index $P = z(\log D) - z(K)$ uses standardized proxies | |
| 34 | +(population, territory, settlement hierarchy for $D$; shares of administrative, legal/monetary, communication and | |
| 35 | +record-keeping indicators for $K$). It is a monotone proxy for $\log\rho$ only under CONJECTURE C1 (§7); it is not a | |
| 36 | +measurement of $\rho$, and the Seshat/V-Dem indicators are not Shannon quantities. | |
| 37 | + | |
| 38 | +--- | |
| 39 | + | |
| 40 | +## 2. Theorem 1 — coding gain of a shared code | |
| 41 | + | |
| 42 | +**DEFINITION 2.1 (code).** A *shared code* is an injective map $c: S \to \{0,1\}^*$ known to all participants, | |
| 43 | +with lengths $\ell(x) = |c(x)|$. It is *prefix-free* if no codeword is a prefix of another. Institutions in the sense of | |
| 44 | +this paper (standardized names, units, legal forms, accounting categories, calendars) are shared codes in exactly this | |
| 45 | +sense (arrow1974; cremer2007). | |
| 46 | + | |
| 47 | +**Assumption 2.2.** Settling a task means that the initiating participant identifies the required plan $x \in S$ to the | |
| 48 | +other $k-1$ participants without ambiguity; messages to different participants are counted separately in messages | |
| 49 | +but identically in bits per recipient, so we state the result per recipient. | |
| 50 | + | |
| 51 | +**THEOREM 1 (coding gain).** | |
| 52 | +(i) Without a shared code, identifying an arbitrary element of $S$ without error requires at least | |
| 53 | +$\lceil \log_2 s \rceil$ bits per recipient in the worst case. | |
| 54 | +(ii) With a shared prefix-free code that is optimal for $p$, the expected length per recipient | |
| 55 | +$L_p^\* = \min_{c\ \mathrm{prefix\text{-}free}} \sum_x p(x)\ell(x)$ satisfies | |
| 56 | +$$H(p) \le L_p^\* < H(p) + 1 .$$ | |
| 57 | +(iii) Hence the expected saving per recipient, $G(p) = \lceil\log_2 s\rceil - L_p^\*$, obeys | |
| 58 | +$G(p) > \lceil\log_2 s\rceil - H(p) - 1 \ge 0$ whenever $\lceil\log_2 s\rceil \ge H(p)+1$, and $G(p)$ is strictly | |
| 59 | +positive as soon as $p$ is sufficiently concentrated ($H(p) < \lceil\log_2 s\rceil - 1$). | |
| 60 | + | |
| 61 | +*Proof.* (i) Without a shared code there is no prior agreement on which binary string denotes which plan, so the | |
| 62 | +only error-free strategy is a fixed-length enumeration of $S$ agreed by the trivial convention of listing; any scheme | |
| 63 | +that distinguishes $s$ alternatives with binary strings of a fixed length $m$ needs $2^m \ge s$, i.e. | |
| 64 | +$m \ge \lceil\log_2 s\rceil$. (The point is not that $\log_2 s$ is a lower bound for *all* conceivable codes — it is the | |
| 65 | +cost of the *codeless* baseline, in which the message must be self-describing.) | |
| 66 | + | |
| 67 | +(ii) *Lower bound.* For any prefix-free code the Kraft inequality holds: $\sum_x 2^{-\ell(x)} \le 1$ (cover2006, | |
| 68 | +Thm 5.2.1). Put $q(x) = 2^{-\ell(x)}/Z$ with $Z = \sum_x 2^{-\ell(x)} \le 1$. Then | |
| 69 | +$$\sum_x p(x)\ell(x) - H(p) = \sum_x p(x)\log_2\frac{p(x)}{2^{-\ell(x)}} = \sum_x p(x)\log_2\frac{p(x)}{q(x)} - \log_2 Z | |
| 70 | += D_{KL}(p\|q) - \log_2 Z \ge 0,$$ | |
| 71 | +because $D_{KL} \ge 0$ (Gibbs' inequality) and $-\log_2 Z \ge 0$. *Upper bound.* Choose | |
| 72 | +$\ell(x) = \lceil -\log_2 p(x)\rceil$. Then $\sum_x 2^{-\ell(x)} \le \sum_x p(x) = 1$, so by the converse Kraft | |
| 73 | +theorem (cover2006, Thm 5.2.1) a prefix-free code with these lengths exists, and | |
| 74 | +$\sum_x p(x)\ell(x) < \sum_x p(x)(-\log_2 p(x) + 1) = H(p) + 1$. The optimal code does at least as well. | |
| 75 | + | |
| 76 | +(iii) Subtract (ii) from (i). $\square$ | |
| 77 | + | |
| 78 | +**Remark 2.3.** Theorem 1 is the source-coding theorem of shannon1948 applied to the plan set. Its content for the | |
| 79 | +paper is only this: *a shared code converts a worst-case identification cost into an average-case cost that is lower | |
| 80 | +exactly to the extent that tasks are predictable.* The "compression" in the title of the hypothesis refers to this | |
| 81 | +step and to nothing else. | |
| 82 | + | |
| 83 | +--- | |
| 84 | + | |
| 85 | +## 3. Theorem 2 — break-even scale of an institution | |
| 86 | + | |
| 87 | +**DEFINITION 3.1 (institutional cost structure).** Adopting a code costs a fixed set-up $F \ge 0$ (designing and | |
| 88 | +promulgating the standard), a learning cost $\ell \ge 0$ per agent (each of the $N$ agents must learn the code), and an | |
| 89 | +administrative overhead $a \ge 0$ bits per task (verification, record-keeping, enforcement). It saves $G > 0$ bits per | |
| 90 | +task (Theorem 1, per recipient; multiply by $k-1$ if desired — the argument is unchanged). | |
| 91 | + | |
| 92 | +**THEOREM 2 (break-even).** Over a horizon with $T$ tasks, the institution lowers total communication cost if and only | |
| 93 | +if | |
| 94 | +$$T\,(G - a) > F + N\ell .$$ | |
| 95 | +With $T = N\lambda\tau$ over $\tau$ periods, this is equivalent to | |
| 96 | +$$\lambda\tau > \frac{F/N + \ell}{G - a}\quad\text{when } G > a,$$ | |
| 97 | +and the inequality can never hold when $a \ge G$. | |
| 98 | + | |
| 99 | +*Proof.* Total cost without the institution is $C_0 = T\lceil\log_2 s\rceil$. With it, $C_1 = F + N\ell + T(L_p^\* + a)$. | |
| 100 | +$C_1 < C_0 \iff F + N\ell < T(\lceil\log_2 s\rceil - L_p^\* - a) = T(G - a)$. If $a \ge G$ the right-hand side is | |
| 101 | +$\le 0 < F + N\ell$ unless $F = \ell = 0$ and $a = G$, in which case the costs are equal, so strict improvement is | |
| 102 | +impossible. If $G > a$, substitute $T = N\lambda\tau$ and divide both sides by $N(G-a) > 0$. $\square$ | |
| 103 | + | |
| 104 | +**Corollary 3.2 (scale advantage).** The threshold $(F/N + \ell)/(G-a)$ is decreasing in $N$: holding $\lambda,\tau$ | |
| 105 | +fixed, a larger society crosses the break-even point sooner, because the set-up cost is spread over more agents. It is | |
| 106 | +*not* decreasing in $N$ through $\ell$: per-agent learning costs do not benefit from scale. This is the formal version | |
| 107 | +of the observation that institutions "pay off at scale" (johnson1982; garicano2000; bolton1994), and it also makes | |
| 108 | +precise when they do not: when overhead $a$ is close to the coding gain $G$, or when horizons $\tau$ are short. | |
| 109 | + | |
| 110 | +--- | |
| 111 | + | |
| 112 | +## 4. Theorem 3 — mismatched code penalty (institutional rigidity) | |
| 113 | + | |
| 114 | +**THEOREM 3 (cost of a stale code).** Let the code be optimal for $p$ in the sense of using lengths | |
| 115 | +$\ell_p(x) = \lceil -\log_2 p(x) \rceil$, and let the true task distribution be $q$ with $q \ll p$ (every plan with | |
| 116 | +positive $q$-probability has positive $p$-probability). Then the expected length under $q$ satisfies | |
| 117 | +$$H(q) + D_{KL}(q\|p) \le \mathbb{E}_q[\ell_p(X)] < H(q) + D_{KL}(q\|p) + 1 .$$ | |
| 118 | +The excess cost of using the $p$-code when the world is $q$, relative to the $q$-optimal code, lies in | |
| 119 | +$(D_{KL}(q\|p) - 1,\ D_{KL}(q\|p) + 1)$ bits per task. | |
| 120 | + | |
| 121 | +*Proof.* Since $-\log_2 p(x) \le \ell_p(x) < -\log_2 p(x) + 1$, | |
| 122 | +$$\mathbb{E}_q[\ell_p(X)] \ge \sum_x q(x)\left(-\log_2 p(x)\right) = \sum_x q(x)\log_2\frac{q(x)}{p(x)} + \sum_x q(x)\left(-\log_2 q(x)\right) = D_{KL}(q\|p) + H(q),$$ | |
| 123 | +and the same computation with the $+1$ gives the strict upper bound. The $q$-optimal code has expected length in | |
| 124 | +$[H(q), H(q)+1)$ by Theorem 1(ii), which yields the stated interval for the excess. (This is the "wrong code" result of | |
| 125 | +cover2006, Thm 5.4.3.) $\square$ | |
| 126 | + | |
| 127 | +**Corollary 4.1 (fragility).** A shock that moves the task distribution from $p$ to $q$ raises the per-task | |
| 128 | +communication cost of an existing institution by about $D_{KL}(q\|p)$ bits. Combined with Theorem 2, the institution | |
| 129 | +remains worthwhile after the shock only if $G(p) - D_{KL}(q\|p) > a$ (up to the $\pm 1$ rounding). An institution can | |
| 130 | +therefore *lower* efficiency after a shock, and the more specialized the code (the smaller $H(p)$ relative to | |
| 131 | +$\log_2 s$), the larger the divergence a given shock can produce. This is the precise sense in which standardization | |
| 132 | +trades efficiency for rigidity; it echoes tainter1988's declining marginal returns but gives the mechanism a unit (bits | |
| 133 | +per task) and a magnitude (a divergence). | |
| 134 | + | |
| 135 | +--- | |
| 136 | + | |
| 137 | +## 5. Theorem 4 — hierarchical routing bound and root load | |
| 138 | + | |
| 139 | +**DEFINITION 5.1 (hierarchy).** A hierarchy is a rooted tree whose $N$ leaves are the agents and whose internal | |
| 140 | +nodes are administrative units; every internal node has at most $b \ge 2$ children; the depth (leaf-to-root distance) | |
| 141 | +is $d = \lceil \log_b N \rceil$ for a complete $b$-ary tree. A task between two leaves is *settled by relay* when each | |
| 142 | +participant sends a message up to their lowest common ancestor (LCA) and the LCA sends the settlement down. | |
| 143 | + | |
| 144 | +**THEOREM 4.** | |
| 145 | +(i) *Message bound.* Any pairwise task can be settled with at most $2d$ messages (up-path plus down-path through the | |
| 146 | +LCA), versus up to $N-1$ messages for a flat broadcast in which the initiator must reach every other agent. For $M$ | |
| 147 | +pairwise tasks the total is at most $2M\lceil\log_b N\rceil$. | |
| 148 | +(ii) *Root load.* In a complete $b$-ary tree with $N = b^d$ leaves, if the two participants of a task are drawn | |
| 149 | +uniformly at random among leaves (independently, with replacement), the probability that their LCA is the root is | |
| 150 | +$1 - 1/b$. Hence the expected number of tasks that pass through the root is $M(1 - 1/b)$. | |
| 151 | +(iii) *Federated / modular coordination.* If a fraction $\theta \in [0,1]$ of tasks is *local* — both participants | |
| 152 | +belong to the same first-level module (child subtree of the root) by construction of the task process — and the | |
| 153 | +remaining $1-\theta$ are drawn uniformly as in (ii), the expected root load is $M(1-\theta)(1 - 1/b)$. | |
| 154 | + | |
| 155 | +*Proof.* (i) In a tree there is a unique simple path between two leaves; it passes through their LCA and has length at | |
| 156 | +most $2d$ since each leaf is at depth $\le d$ and the LCA is at depth $\ge 0$. Sending one message per edge along | |
| 157 | +the two leaf-to-LCA paths settles the task in $\le 2d$ messages. A flat broadcast from one initiator to the other | |
| 158 | +$N-1$ agents uses $N-1$ messages. Summing over $M$ tasks gives (i). | |
| 159 | +(ii) The root has $b$ children, each the root of a subtree with $b^{d-1}$ leaves. The first participant lies in some | |
| 160 | +child subtree $j$; the second participant, drawn uniformly and independently, lies in the same subtree $j$ with | |
| 161 | +probability $b^{d-1}/b^d = 1/b$. The LCA is the root exactly when the two participants lie in different child subtrees, | |
| 162 | +which has probability $1 - 1/b$. Expectation of a sum of $M$ indicators gives $M(1-1/b)$. | |
| 163 | +(iii) Local tasks never reach the root (their LCA lies inside a module); the non-local fraction behaves as in (ii). | |
| 164 | +By linearity of expectation the load is $M(1-\theta)(1-1/b)$. $\square$ | |
| 165 | + | |
| 166 | +**Remark 5.2.** Theorem 4 formalizes the trade-off between centralized administration and federated coordination | |
| 167 | +(radner1993; bolton1994): a hierarchy cuts the *message* cost of coordination from linear to logarithmic in $N$, but | |
| 168 | +concentrates a constant fraction $1 - 1/b$ of all non-local tasks on the apex, whose processing rate then bounds $K$ | |
| 169 | +(Definition 1.3). Modularity reduces apex load by the locality fraction $\theta$ at the price of *not* reconciling | |
| 170 | +across modules. These are the five regimes compared in the pre-registered simulations (no institution, shared norms, | |
| 171 | +standardized records, centralized administration, federated), and the simulations measure messages and bytes | |
| 172 | +directly rather than assuming the bounds. | |
| 173 | + | |
| 174 | +--- | |
| 175 | + | |
| 176 | +## 6. Theorem 5 — backlog dynamics under mismatch | |
| 177 | + | |
| 178 | +**DEFINITION 6.1 (backlog).** Let $D_t$ be the number of tasks arriving in period $t$ and $K_t$ the number the system | |
| 179 | +can settle in period $t$. The backlog of unsettled tasks follows the Lindley recursion | |
| 180 | +$$B_{t+1} = \max\{0,\ B_t + D_t - K_t\},\qquad B_0 \ge 0 .$$ | |
| 181 | + | |
| 182 | +**THEOREM 5.** | |
| 183 | +(i) *Deterministic case.* If $D_t = D$ and $K_t = K$ for all $t$, then: if $D \le K$, $B_t \le \max\{B_0 - t(K-D), 0\}$, | |
| 184 | +so the backlog reaches $0$ after at most $\lceil B_0/(K-D) \rceil$ periods when $D < K$ and stays constant at $B_0$ | |
| 185 | +when $D = K$; if $D > K$, $B_t = B_0 + t(D-K) \to \infty$ linearly. | |
| 186 | +(ii) *Stochastic case.* If $(D_t - K_t)_{t\ge0}$ is i.i.d. with finite mean, then when $\mathbb{E}[D_t] < \mathbb{E}[K_t]$ | |
| 187 | +the backlog has a proper stationary distribution to which $B_t$ converges from any $B_0$, and when | |
| 188 | +$\mathbb{E}[D_t] > \mathbb{E}[K_t]$, $B_t \to \infty$ almost surely. (Standard: the recursion is the single-server | |
| 189 | +waiting-time recursion of Lindley (1952), and the stability dichotomy is Loynes' theorem (1962); the proof for (ii) is | |
| 190 | +not reproduced here and the result is cited.) | |
| 191 | +(iii) *Delay under a Markovian approximation.* If arrivals are Poisson with rate $\lambda_D$ and settlements are | |
| 192 | +exponential with rate $\mu$ (an M/M/1 queue), with $\rho = \lambda_D/\mu < 1$, the expected sojourn time is | |
| 193 | +$W = 1/(\mu - \lambda_D) = (1/\mu)/(1-\rho)$, which is increasing and convex in $\rho$ on $[0,1)$ and diverges as | |
| 194 | +$\rho \uparrow 1$. | |
| 195 | + | |
| 196 | +*Proof of (i).* If $D \le K$, then $B_{t+1} = \max\{0, B_t - (K-D)\}$; by induction | |
| 197 | +$B_t \le \max\{0, B_0 - t(K-D)\}$, with equality as long as the argument is nonnegative, which gives the hitting time | |
| 198 | +when $D < K$ and constancy when $D = K$. If $D > K$, then $B_t + D - K > 0$ for all $t$ (since $B_t \ge 0$), so the | |
| 199 | +maximum is never binding and $B_{t+1} = B_t + (D-K)$, hence $B_t = B_0 + t(D-K)$. | |
| 200 | +*Proof of (iii).* The M/M/1 stationary number in system is geometric with parameter $\rho$, so the mean is | |
| 201 | +$\rho/(1-\rho)$ and Little's law gives $W = \rho/((1-\rho)\lambda_D) = 1/(\mu(1-\rho)) = 1/(\mu - \lambda_D)$ (any | |
| 202 | +standard queueing reference). The function $f(\rho) = (1-\rho)^{-1}$ has $f' = (1-\rho)^{-2} > 0$ and | |
| 203 | +$f'' = 2(1-\rho)^{-3} > 0$ on $[0,1)$, and $f(\rho) \to \infty$ as $\rho \uparrow 1$. $\square$ | |
| 204 | + | |
| 205 | +**Scope of Theorem 5.** This is a *mechanism*, not evidence. Polities are not single-server queues: capacity is | |
| 206 | +endogenous (institutions are built in response to backlog), arrivals are correlated across periods and agents, and | |
| 207 | +"unsettled tasks" are not observed in the historical record. The theorem is used in the paper only to motivate two | |
| 208 | +qualitative predictions — that costs rise smoothly and convexly in utilization, and that sustained $\rho > 1$ is | |
| 209 | +qualitatively different from $\rho < 1$ — and both are stated as conjectures below. | |
| 210 | + | |
| 211 | +--- | |
| 212 | + | |
| 213 | +## 7. Conjectures (unproved; each with its falsifier) | |
| 214 | + | |
| 215 | +**CONJECTURE C1 (proxy monotonicity).** The pre-registered Seshat and V-Dem proxies are monotone in the model's | |
| 216 | +primitives: the scale variables (population, territory, largest settlement, settlement hierarchy) increase with | |
| 217 | +$D$ of Definition 1.2, and the four indicator blocks (administration, standardized rules and money, communication | |
| 218 | +infrastructure, record-keeping) increase with $K$ of Definition 1.3, so that $P = z(\log D) - z(K)$ is monotone in | |
| 219 | +$\log\rho$ up to measurement error. *Reason:* population and territory raise the number and the dispersion of | |
| 220 | +participants; written records, legal codes, money and measurement systems are shared codes (Theorem 1); roads, | |
| 221 | +couriers and postal systems raise bits per period; administrative levels implement the routing of Theorem 4. | |
| 222 | +*Falsifier:* the proxies for $D$ and $K$ are so collinear (as Seshat's first principal component suggests; shin2020) | |
| 223 | +that $P$ retains almost no variance, or alternative defensible $K$ weightings reverse the sign of $P$ for a | |
| 224 | +substantial share of polities — both are reported in `research/measurement.md`. | |
| 225 | + | |
| 226 | +**CONJECTURE C2 (sustained overload raises breakdown probability).** Periods with $\rho > 1$ sustained longer than | |
| 227 | +the time needed to build capacity raise the probability of institutional breakdown. *Mechanism family (not proved):* | |
| 228 | +unsettled tasks accumulate (Theorem 5), so participants cannot form the common knowledge required for coordinated | |
| 229 | +action (chwe2000) and local defection spreads by contagion through the interaction network (morris2000); institutions | |
| 230 | +whose codes are stale after a shock lose their cost advantage (Theorem 3) and are abandoned. *Falsifier:* the | |
| 231 | +pre-registered H2 tests find $\beta_P$ not positive, or $P$ does not beat the population-only and capacity-only | |
| 232 | +baselines out of sample. | |
| 233 | + | |
| 234 | +**CONJECTURE C3 (smooth, not step).** The relation between $P$ and instability is convex and smooth away from | |
| 235 | +$\rho \approx 1$ and steepens near it, rather than a discrete threshold. *Reason:* Theorem 5(iii) gives a smooth, | |
| 236 | +convex delay in $\rho$; discontinuities would require an additional tipping mechanism (e.g. a critical mass in a | |
| 237 | +contagion model) that the framework does not supply. *Falsifier:* the pre-registered H4 comparison finds that a hinge | |
| 238 | +or step model beats the smooth model out of sample in $\ge 80\%$ of folds, which would favour a tipping mechanism; | |
| 239 | +conversely, if neither nonlinear form beats the linear model, C3's "steepening" clause is unsupported. | |
| 240 | + | |
| 241 | +--- | |
| 242 | + | |
| 243 | +## 8. What is not claimed, and which test speaks to each statement | |
| 244 | + | |
| 245 | +- **No Shannon measurement of history.** Theorems 1 and 3 are about codes for a plan set with a known distribution. | |
| 246 | + Seshat indicators (presence of writing, coins, courts, roads) are not message lengths, and no entropy or divergence | |
| 247 | + is computed from historical data anywhere in this project. Shannon quantities appear only in the simulations, where | |
| 248 | + the plan distribution $p$ and the messages are defined by the experimenter and can be measured. | |
| 249 | +- **No causal claim.** The theorems describe an idealized communication system; the empirical tests are associational | |
| 250 | + and pre-registered as such (`research/preregistration.md`, §1 and §5); the identification discussion is in | |
| 251 | + `research/identification.md`. | |
| 252 | +- **Endogeneity of capacity.** The framework takes $K$ as given within a period; historically, $K$ is built in response | |
| 253 | + to $D$ (johnson1982; shin2020). This is why H1 is pre-registered as an association without a direction. | |
| 254 | + | |
| 255 | +| Statement | What can speak to it | Pre-registered test | | |
| 256 | +|---|---|---| | |
| 257 | +| Theorem 1 (coding gain) | Simulated messages and bits with vs. without a shared code | H3 (regimes b, c vs a) | | |
| 258 | +| Theorem 2 (break-even) | Simulated total cost across $N$, $\lambda$, horizon, overhead $a$ | H3 factorial cells; search for cells where institutions lose | | |
| 259 | +| Theorem 3 (stale code) | Simulated shocks to the task distribution | H3 adverse regimes (shock treatment) | | |
| 260 | +| Theorem 4 (routing, root load) | Simulated messages and apex load under centralized vs federated regimes | H3 (regimes d, e) | | |
| 261 | +| Theorem 5 (backlog) | Not testable with the historical data (backlog unobserved); illustrated in simulation only | none (stated as mechanism) | | |
| 262 | +| Conjecture C1 | Measurement diagnostics: collinearity of $D$ and $K$ proxies, sensitivity of $P$ to weights | `research/measurement.md` | | |
| 263 | +| Conjecture C2 | Lagged $P$ and subsequent instability vs baselines, out of sample | H2 (Seshat Y1–Y3; V-Dem Y4–Y6), H5 | | |
| 264 | +| Conjecture C3 | Linear vs hinge vs spline in $P$, out of sample | H4 | | |
added
research/measurement.md
+80 −0
@@ -0,0 +1,80 @@ | ||
| 1 | +# WP35 — Measurement report (Phase 3) | |
| 2 | + | |
| 3 | +All numbers come from `results/empirical/measurement_diagnostics.json`, `measurement_diagnostics_modern.json`, | |
| 4 | +`measurement_irt_item_parameters.csv`, `power_h2a_*.csv`, produced by `python -m wp35.measurement.build_indices` | |
| 5 | +(2026-11-01). **No outcome variable was read by that module.** Definitions follow `research/preregistration.md` §3. | |
| 6 | + | |
| 7 | +## 1. Seshat (polity level) | |
| 8 | + | |
| 9 | +### 1.1 Coordination demand D | |
| 10 | +Four logged scale variables (polity population, territory, largest-settlement population; settlement hierarchy levels | |
| 11 | +unlogged). First principal component of their pairwise-complete correlation matrix explains **82.1%** of variance with | |
| 12 | +near-equal loadings (log pop 0.26, log territory 0.24, log capital 0.26, settlement hierarchy 0.24). Score = loading- | |
| 13 | +weighted mean of available standardized variables, requiring ≥ 2 of 4; **442 polities** have `D_pc1`; 299 have | |
| 14 | +population alone (`log_pop`). Range bounds (`value_from`/`value_to`) propagate to `D_pc1_lo/hi`: the median half-width | |
| 15 | +is 0.017 SD, the 90th percentile 0.093 SD, so range uncertainty in the scale variables is small relative to | |
| 16 | +between-polity variation (the larger uncertainty is *which* polities have any estimate at all). | |
| 17 | + | |
| 18 | +### 1.2 Coordination capacity K | |
| 19 | +Four blocks of presence indicators, each the share coded present among coded items (≥ 50% coded required): | |
| 20 | +administration (5 items incl. rank-scaled administrative levels), standardized rules and money (10), communication | |
| 21 | +infrastructure (7), record-keeping (9). `K_mean` = mean of blocks with ≥ 3 blocks: **344 polities**. Inter-block | |
| 22 | +correlations 0.75–0.79; first PC of the blocks explains 82.4% (loadings 0.25 each), and `K_pc1` correlates 0.999 with | |
| 23 | +`K_mean`. Cronbach α of the four blocks by macro-region ranges 0.83 (Europe) to 0.97 (South Asia); mean off-diagonal | |
| 24 | +block correlation by era 0.67 (1001–1500) to 0.81 (pre-1000 BCE) — the four capacity blocks hang together | |
| 25 | +in every region and era. | |
| 26 | +Latent alternative: 2PL IRT (MAP, log-normal prior on discrimination, EM converged in 135 iterations) on the 30 binary | |
| 27 | +items (disputed 0.5 → missing; administrative levels excluded) gives `K_irt` for **579 polities** (≥ 3 items). It | |
| 28 | +correlates **0.96** with `K_mean`. Item difficulties order sensibly (tokens, non-written records, roads, couriers easy; | |
| 29 | +paper currency, general postal service, examination system, merit promotion hard; `article` is nearly universal and | |
| 30 | +uninformative). Five record-keeping items (written record, script, lists/tables, calendar, practical literature) hit the | |
| 31 | +discrimination ceiling (8): they are coded almost jointly in Seshat, i.e. locally dependent; the latent score is | |
| 32 | +therefore dominated by the "writing cluster", which is why we keep the transparent block mean as primary. Posterior SD of | |
| 33 | +`K_irt`: median 0.34, 90th percentile 0.61 (on a unit-variance scale) — individual-polity capacity scores carry | |
| 34 | +substantial uncertainty; this is carried into the econometrics through the IRT variant and the bound runs. | |
| 35 | +Cross-region invariance: refitting the IRT separately on the Old World (n = 532) and on the Americas + Southeast | |
| 36 | +Asia/Oceania (n = 94) gives item difficulties correlated **0.88** across the two fits (mean absolute difference 1.16 | |
| 37 | +logits, inflated by boundary estimates on the small New-World sample). Comparability is acceptable for the ordering | |
| 38 | +of items but the New-World sample is too small for a strict invariance test; leave-region-out results (H5) are the | |
| 39 | +operational safeguard. | |
| 40 | +Leave-one-block-out: dropping any block leaves `K` correlated ≥ 0.98 with the full index and `P` correlated ≥ 0.95 | |
| 41 | +with `P_main`. | |
| 42 | + | |
| 43 | +### 1.3 Coordination pressure P and the collinearity problem | |
| 44 | +`P_main = z(D_pc1) − z(K_mean)` is defined for **317 polities** and, in pre-crisis form, for **127 of the 166 CrisisDB | |
| 45 | +records**. As anticipated from Turchin et al. (2018) and Shin et al. (2020), scale and capacity are strongly | |
| 46 | +collinear: corr(`D_pc1`, `K_mean`) = **0.84** (0.80 with `K_irt`). Consequently `P_main` retains only **16.6%** of the | |
| 47 | +variance of z(D)+z(K); it correlates 0.39 with D and −0.18 with K, and −0.04 with the Turchin-style single complexity | |
| 48 | +axis (`complexity_pc1`, first PC of 57 social-complexity indicators, 49.7% of variance). The mismatch index is thus | |
| 49 | +nearly orthogonal to overall complexity, which is exactly what makes the H2 test informative *if* P carries signal, and | |
| 50 | +what makes it fragile: P is a residual, so measurement error in either D or K leaks into it. The pre-registered | |
| 51 | +baselines (D-only, K-only, complexity-PC1-only) address this directly. | |
| 52 | + | |
| 53 | +### 1.4 A-priori power (pre-registration §5.8; random outcomes, actual P distribution, n = 127) | |
| 54 | +| Outcome | β (per SD of P, logit) | power | | |
| 55 | +|---|---|---| | |
| 56 | +| Y1b binary (base rate 0.77) | 0.4 / 0.5 / 0.6 / 0.8 | 0.47 / 0.63 / 0.79 / 0.95 | | |
| 57 | +| Y1 fractional (14 items, base 0.29) | 0.10 / 0.15 / 0.20 | 0.49 / 0.81 / 0.96 | | |
| 58 | +**Decision (pre-registered rule):** the binary severe-outcome test Y1b is *underpowered a priori* (MDE ≈ 0.6 > 0.5); | |
| 59 | +the fractional severity Y1 is adequately powered for effects of ≈ 0.15 log-odds per SD. Y1 remains the primary | |
| 60 | +Seshat H2 outcome; Y1b is reported as secondary. | |
| 61 | + | |
| 62 | +## 2. Modern panel | |
| 63 | +`K_modern` = mean of six standardized V-Dem items (≥ 4 coded): rigorous/impartial administration, meritocratic | |
| 64 | +appointment, transparent laws, fiscal capacity source, territorial control, EMB capacity; PC1 explains 60.5% with | |
| 65 | +loadings 0.13–0.19. `D_modern` = log Maddison population (1820+); `P_modern` defined for **15,697 country-years**. | |
| 66 | +corr(D, K) = 0.06 (no collinearity problem in the modern panel). **Caveat fixed before analysis:** corr(`K_modern`, | |
| 67 | +`v2x_polyarchy`) = **0.75** — the V-Dem capacity items are strongly associated with electoral democracy even though no | |
| 68 | +democracy index enters K; all modern specifications therefore control for polyarchy as pre-registered, and the K-only | |
| 69 | +baseline is reported. | |
| 70 | + | |
| 71 | +## 3. Ethnographic Atlas | |
| 72 | +`log_community_size` from EA031 category midpoints (611 societies), `log_population` from continuous EA202 (953), | |
| 73 | +`jurisdictional_hierarchy_beyond` = EA033 − 1 (0–4 levels; 1,155). No outcome; used for H1 only, with Glottolog | |
| 74 | +family (173 families) and region controls. | |
| 75 | + | |
| 76 | +## 4. What the measurement model cannot do | |
| 77 | +- Seshat K items are presence codes: they measure *existence*, not reach or quality, of institutions. | |
| 78 | +- Most facts are undated, so "pre-crisis" K is mostly whole-polity K (see pre-registration §3.6). | |
| 79 | +- No direct indicator of division of labour or dispersion in Polaris; D is scale only. | |
| 80 | +- IRT local dependence among writing items; invariance only coarsely checked. | |
added
results/empirical/measurement_diagnostics.json
+321 −0
@@ -0,0 +1,321 @@ | ||
| 1 | +{ | |
| 2 | + "D_pc1_loadings": { | |
| 3 | + "log_pop": 0.2646, | |
| 4 | + "log_terr": 0.2366, | |
| 5 | + "log_cap": 0.2617, | |
| 6 | + "settl_hier": 0.2371 | |
| 7 | + }, | |
| 8 | + "D_pc1_var_share": 0.8209, | |
| 9 | + "K_pc1_loadings": { | |
| 10 | + "K_admin": 0.2481, | |
| 11 | + "K_rules_money": 0.2505, | |
| 12 | + "K_comm_infra": 0.2526, | |
| 13 | + "K_records": 0.2488 | |
| 14 | + }, | |
| 15 | + "K_pc1_var_share": 0.8239, | |
| 16 | + "complexity_pc1_var_share": 0.497, | |
| 17 | + "complexity_pc1_n_vars": 57, | |
| 18 | + "irt_loglik": -3345.5533228338218, | |
| 19 | + "irt_iterations": 135, | |
| 20 | + "n_D_pc1": 442, | |
| 21 | + "n_K_mean": 344, | |
| 22 | + "n_K_irt": 579, | |
| 23 | + "n_P_main": 317, | |
| 24 | + "n_P_main_crisis": 127, | |
| 25 | + "corr_zD_zK": { | |
| 26 | + "D_pc1~K_mean": 0.8384, | |
| 27 | + "D_pc1~K_irt": 0.7998, | |
| 28 | + "log_pop~K_mean": 0.7966, | |
| 29 | + "K_mean~K_irt": 0.9625, | |
| 30 | + "K_mean~K_pc1": 0.9994, | |
| 31 | + "P_main~complexity_pc1": -0.0434, | |
| 32 | + "P_main~D_pc1": 0.3855, | |
| 33 | + "P_main~K_mean": -0.1797 | |
| 34 | + }, | |
| 35 | + "P_variance_share_of_D_plus_K": 0.1655, | |
| 36 | + "block_correlations": { | |
| 37 | + "K_admin": { | |
| 38 | + "K_admin": 1.0, | |
| 39 | + "K_rules_money": 0.755, | |
| 40 | + "K_comm_infra": 0.766, | |
| 41 | + "K_records": 0.751 | |
| 42 | + }, | |
| 43 | + "K_rules_money": { | |
| 44 | + "K_admin": 0.755, | |
| 45 | + "K_rules_money": 1.0, | |
| 46 | + "K_comm_infra": 0.789, | |
| 47 | + "K_records": 0.757 | |
| 48 | + }, | |
| 49 | + "K_comm_infra": { | |
| 50 | + "K_admin": 0.766, | |
| 51 | + "K_rules_money": 0.789, | |
| 52 | + "K_comm_infra": 1.0, | |
| 53 | + "K_records": 0.773 | |
| 54 | + }, | |
| 55 | + "K_records": { | |
| 56 | + "K_admin": 0.751, | |
| 57 | + "K_rules_money": 0.757, | |
| 58 | + "K_comm_infra": 0.773, | |
| 59 | + "K_records": 1.0 | |
| 60 | + } | |
| 61 | + }, | |
| 62 | + "block_alpha_by_macro_region": { | |
| 63 | + "Central_Asia": { | |
| 64 | + "n": 17, | |
| 65 | + "alpha": 0.933 | |
| 66 | + }, | |
| 67 | + "East_Asia": { | |
| 68 | + "n": 35, | |
| 69 | + "alpha": 0.964 | |
| 70 | + }, | |
| 71 | + "Europe": { | |
| 72 | + "n": 57, | |
| 73 | + "alpha": 0.831 | |
| 74 | + }, | |
| 75 | + "North_Africa": { | |
| 76 | + "n": 27, | |
| 77 | + "alpha": 0.886 | |
| 78 | + }, | |
| 79 | + "North_America": { | |
| 80 | + "n": 19, | |
| 81 | + "alpha": 0.936 | |
| 82 | + }, | |
| 83 | + "South_Asia": { | |
| 84 | + "n": 11, | |
| 85 | + "alpha": 0.968 | |
| 86 | + }, | |
| 87 | + "Southeast_Asia_Oceania": { | |
| 88 | + "n": 19, | |
| 89 | + "alpha": 0.935 | |
| 90 | + }, | |
| 91 | + "Southwest_Asia": { | |
| 92 | + "n": 39, | |
| 93 | + "alpha": 0.88 | |
| 94 | + } | |
| 95 | + }, | |
| 96 | + "block_corr_mean_by_era": { | |
| 97 | + "1-1000": { | |
| 98 | + "n": 46, | |
| 99 | + "mean_offdiag_corr": 0.726 | |
| 100 | + }, | |
| 101 | + "1000BCE-0": { | |
| 102 | + "n": 35, | |
| 103 | + "mean_offdiag_corr": 0.756 | |
| 104 | + }, | |
| 105 | + "1001-1500": { | |
| 106 | + "n": 51, | |
| 107 | + "mean_offdiag_corr": 0.666 | |
| 108 | + }, | |
| 109 | + "1501-1800": { | |
| 110 | + "n": 38, | |
| 111 | + "mean_offdiag_corr": 0.785 | |
| 112 | + }, | |
| 113 | + "post-1800": { | |
| 114 | + "n": 20, | |
| 115 | + "mean_offdiag_corr": 0.765 | |
| 116 | + }, | |
| 117 | + "pre-1000BCE": { | |
| 118 | + "n": 43, | |
| 119 | + "mean_offdiag_corr": 0.805 | |
| 120 | + } | |
| 121 | + }, | |
| 122 | + "measurement_error": { | |
| 123 | + "D_pc1_halfwidth_median": 0.016672931644356626, | |
| 124 | + "D_pc1_halfwidth_q90": 0.09309851215013848, | |
| 125 | + "K_irt_psd_median": 0.34213753777518097, | |
| 126 | + "K_irt_psd_q90": 0.6062046010250374, | |
| 127 | + "sd_K_irt": 1.008347206077521, | |
| 128 | + "P_main_range_median": 0.036226778179016206 | |
| 129 | + }, | |
| 130 | + "irt_invariance": { | |
| 131 | + "items_compared": 30, | |
| 132 | + "corr_difficulty_old_vs_new": 0.88, | |
| 133 | + "mean_abs_diff_difficulty": 1.162, | |
| 134 | + "items": [ | |
| 135 | + "full_time_bureaucrat", | |
| 136 | + "specialized_government_building", | |
| 137 | + "merit_promotion", | |
| 138 | + "examination_system", | |
| 139 | + "formal_legal_code", | |
| 140 | + "judge", | |
| 141 | + "court", | |
| 142 | + "professional_lawyer", | |
| 143 | + "indigenous_coin", | |
| 144 | + "foreign_coin", | |
| 145 | + "paper_currency", | |
| 146 | + "token", | |
| 147 | + "precious_metal", | |
| 148 | + "article", | |
| 149 | + "courier", | |
| 150 | + "postal_station", | |
| 151 | + "general_postal_service", | |
| 152 | + "road", | |
| 153 | + "bridge", | |
| 154 | + "canal", | |
| 155 | + "port", | |
| 156 | + "written_record", | |
| 157 | + "script", | |
| 158 | + "phonetic_alphabetic_writing", | |
| 159 | + "non_phonetic_writing", | |
| 160 | + "lists_tables_and_classification", | |
| 161 | + "calendar", | |
| 162 | + "practical_literature", | |
| 163 | + "nonwritten_record", | |
| 164 | + "mnemonic_device" | |
| 165 | + ], | |
| 166 | + "fits": { | |
| 167 | + "old_world": { | |
| 168 | + "n": 525, | |
| 169 | + "b": [ | |
| 170 | + -0.567, | |
| 171 | + -0.598, | |
| 172 | + 0.554, | |
| 173 | + 1.04, | |
| 174 | + -0.659, | |
| 175 | + -0.089, | |
| 176 | + -0.159, | |
| 177 | + 0.605, | |
| 178 | + -0.041, | |
| 179 | + -0.287, | |
| 180 | + 2.037, | |
| 181 | + -1.102, | |
| 182 | + -1.05, | |
| 183 | + -3.99, | |
| 184 | + -0.984, | |
| 185 | + 0.164, | |
| 186 | + 1.134, | |
| 187 | + -1.121, | |
| 188 | + -0.949, | |
| 189 | + -0.328, | |
| 190 | + -0.901, | |
| 191 | + -0.736, | |
| 192 | + -0.845, | |
| 193 | + -0.409, | |
| 194 | + 1.716, | |
| 195 | + -0.673, | |
| 196 | + -0.669, | |
| 197 | + -0.498, | |
| 198 | + -3.397, | |
| 199 | + -1.374 | |
| 200 | + ], | |
| 201 | + "a": [ | |
| 202 | + 3.44, | |
| 203 | + 3.543, | |
| 204 | + 1.905, | |
| 205 | + 3.328, | |
| 206 | + 2.428, | |
| 207 | + 2.984, | |
| 208 | + 2.47, | |
| 209 | + 2.412, | |
| 210 | + 3.564, | |
| 211 | + 1.758, | |
| 212 | + 1.82, | |
| 213 | + 0.646, | |
| 214 | + 2.036, | |
| 215 | + 0.626, | |
| 216 | + 2.456, | |
| 217 | + 2.305, | |
| 218 | + 1.695, | |
| 219 | + 3.24, | |
| 220 | + 2.977, | |
| 221 | + 2.077, | |
| 222 | + 1.802, | |
| 223 | + 8.0, | |
| 224 | + 8.0, | |
| 225 | + 1.644, | |
| 226 | + 0.822, | |
| 227 | + 8.0, | |
| 228 | + 8.0, | |
| 229 | + 6.448, | |
| 230 | + 1.202, | |
| 231 | + 1.277 | |
| 232 | + ] | |
| 233 | + }, | |
| 234 | + "new_world_oceania": { | |
| 235 | + "n": 94, | |
| 236 | + "b": [ | |
| 237 | + 0.128, | |
| 238 | + 0.085, | |
| 239 | + 2.663, | |
| 240 | + 3.537, | |
| 241 | + 0.354, | |
| 242 | + 0.801, | |
| 243 | + 0.728, | |
| 244 | + 1.073, | |
| 245 | + 1.881, | |
| 246 | + 1.071, | |
| 247 | + 2.593, | |
| 248 | + 1.207, | |
| 249 | + 0.757, | |
| 250 | + -3.221, | |
| 251 | + 0.518, | |
| 252 | + 1.58, | |
| 253 | + 1.786, | |
| 254 | + 0.072, | |
| 255 | + 0.07, | |
| 256 | + 0.565, | |
| 257 | + 0.578, | |
| 258 | + 0.159, | |
| 259 | + 0.004, | |
| 260 | + 0.736, | |
| 261 | + 1.473, | |
| 262 | + 0.642, | |
| 263 | + -0.049, | |
| 264 | + 0.922, | |
| 265 | + -1.575, | |
| 266 | + -0.93 | |
| 267 | + ], | |
| 268 | + "a": [ | |
| 269 | + 3.407, | |
| 270 | + 1.466, | |
| 271 | + 0.849, | |
| 272 | + 1.541, | |
| 273 | + 3.221, | |
| 274 | + 2.923, | |
| 275 | + 3.622, | |
| 276 | + 3.145, | |
| 277 | + 1.934, | |
| 278 | + 1.517, | |
| 279 | + 1.11, | |
| 280 | + 0.467, | |
| 281 | + 1.44, | |
| 282 | + 0.454, | |
| 283 | + 1.585, | |
| 284 | + 1.687, | |
| 285 | + 1.946, | |
| 286 | + 1.387, | |
| 287 | + 1.46, | |
| 288 | + 1.677, | |
| 289 | + 2.691, | |
| 290 | + 2.461, | |
| 291 | + 2.748, | |
| 292 | + 3.894, | |
| 293 | + 0.629, | |
| 294 | + 2.858, | |
| 295 | + 3.09, | |
| 296 | + 3.732, | |
| 297 | + 0.826, | |
| 298 | + 0.696 | |
| 299 | + ] | |
| 300 | + } | |
| 301 | + } | |
| 302 | + }, | |
| 303 | + "leave_one_block_out": { | |
| 304 | + "admin": { | |
| 305 | + "corr_K_with_full": 0.9858, | |
| 306 | + "corr_P_with_main": 0.9575 | |
| 307 | + }, | |
| 308 | + "rules_money": { | |
| 309 | + "corr_K_with_full": 0.9877, | |
| 310 | + "corr_P_with_main": 0.9615 | |
| 311 | + }, | |
| 312 | + "comm_infra": { | |
| 313 | + "corr_K_with_full": 0.9874, | |
| 314 | + "corr_P_with_main": 0.9657 | |
| 315 | + }, | |
| 316 | + "records": { | |
| 317 | + "corr_K_with_full": 0.9816, | |
| 318 | + "corr_P_with_main": 0.9474 | |
| 319 | + } | |
| 320 | + } | |
| 321 | +} | |
| \ No newline at end of file | ||
added
results/empirical/measurement_diagnostics_modern.json
+72 −0
@@ -0,0 +1,72 @@ | ||
| 1 | +{ | |
| 2 | + "K_modern_pc1_loadings": { | |
| 3 | + "v2clrspct": 0.1859, | |
| 4 | + "v2stcritrecadm": 0.1678, | |
| 5 | + "v2cltrnslw": 0.1906, | |
| 6 | + "v2stfisccap": 0.15, | |
| 7 | + "v2svstterr": 0.1337, | |
| 8 | + "v2elembcap": 0.1721 | |
| 9 | + }, | |
| 10 | + "K_modern_pc1_var_share": 0.6052, | |
| 11 | + "n_P_modern": 15697, | |
| 12 | + "corr_D_K_modern": 0.0624, | |
| 13 | + "corr_K_modern_polyarchy": 0.7541, | |
| 14 | + "item_corr": { | |
| 15 | + "v2clrspct": { | |
| 16 | + "v2clrspct": 1.0, | |
| 17 | + "v2stcritrecadm": 0.644, | |
| 18 | + "v2cltrnslw": 0.813, | |
| 19 | + "v2stfisccap": 0.473, | |
| 20 | + "v2svstterr": 0.422, | |
| 21 | + "v2elembcap": 0.574 | |
| 22 | + }, | |
| 23 | + "v2stcritrecadm": { | |
| 24 | + "v2clrspct": 0.644, | |
| 25 | + "v2stcritrecadm": 1.0, | |
| 26 | + "v2cltrnslw": 0.621, | |
| 27 | + "v2stfisccap": 0.438, | |
| 28 | + "v2svstterr": 0.359, | |
| 29 | + "v2elembcap": 0.521 | |
| 30 | + }, | |
| 31 | + "v2cltrnslw": { | |
| 32 | + "v2clrspct": 0.813, | |
| 33 | + "v2stcritrecadm": 0.621, | |
| 34 | + "v2cltrnslw": 1.0, | |
| 35 | + "v2stfisccap": 0.513, | |
| 36 | + "v2svstterr": 0.424, | |
| 37 | + "v2elembcap": 0.654 | |
| 38 | + }, | |
| 39 | + "v2stfisccap": { | |
| 40 | + "v2clrspct": 0.473, | |
| 41 | + "v2stcritrecadm": 0.438, | |
| 42 | + "v2cltrnslw": 0.513, | |
| 43 | + "v2stfisccap": 1.0, | |
| 44 | + "v2svstterr": 0.374, | |
| 45 | + "v2elembcap": 0.497 | |
| 46 | + }, | |
| 47 | + "v2svstterr": { | |
| 48 | + "v2clrspct": 0.422, | |
| 49 | + "v2stcritrecadm": 0.359, | |
| 50 | + "v2cltrnslw": 0.424, | |
| 51 | + "v2stfisccap": 0.374, | |
| 52 | + "v2svstterr": 1.0, | |
| 53 | + "v2elembcap": 0.443 | |
| 54 | + }, | |
| 55 | + "v2elembcap": { | |
| 56 | + "v2clrspct": 0.574, | |
| 57 | + "v2stcritrecadm": 0.521, | |
| 58 | + "v2cltrnslw": 0.654, | |
| 59 | + "v2stfisccap": 0.497, | |
| 60 | + "v2svstterr": 0.443, | |
| 61 | + "v2elembcap": 1.0 | |
| 62 | + } | |
| 63 | + }, | |
| 64 | + "corr_K_by_region": { | |
| 65 | + "1": -0.085, | |
| 66 | + "2": 0.259, | |
| 67 | + "3": 0.081, | |
| 68 | + "4": 0.041, | |
| 69 | + "5": -0.045, | |
| 70 | + "6": -0.012 | |
| 71 | + } | |
| 72 | +} | |
| \ No newline at end of file | ||
added
results/empirical/measurement_irt_item_parameters.csv
+31 −0
@@ -0,0 +1,31 @@ | ||
| 1 | +item,a,b,block | |
| 2 | +full_time_bureaucrat,3.6527424079806603,-0.41859686853078504,admin | |
| 3 | +specialized_government_building,3.066730654084431,-0.5085373165354822,admin | |
| 4 | +merit_promotion,1.7802655820815738,0.7021586341368805,admin | |
| 5 | +examination_system,3.3869433671617886,1.1375006596571404,admin | |
| 6 | +formal_legal_code,2.9525895205555868,-0.4141059941613492,rules_money | |
| 7 | +judge,3.552224718836929,0.060069448676890166,rules_money | |
| 8 | +court,3.0760342165002372,0.008468451933063378,rules_money | |
| 9 | +professional_lawyer,2.72592841935458,0.6535317471947827,rules_money | |
| 10 | +indigenous_coin,3.7072105660727472,0.15112395390732436,rules_money | |
| 11 | +foreign_coin,1.9763009083036096,-0.08042415203888174,rules_money | |
| 12 | +paper_currency,1.4853538625942566,2.2492864209104257,rules_money | |
| 13 | +token,0.579698340872784,-0.8294519474758425,rules_money | |
| 14 | +precious_metal,2.1558974591242315,-0.6735737580810314,rules_money | |
| 15 | +article,0.5286046257791284,-4.364025532653697,rules_money | |
| 16 | +courier,2.670690519140207,-0.6724574117368375,comm_infra | |
| 17 | +postal_station,2.560978399199805,0.3210643907483727,comm_infra | |
| 18 | +general_postal_service,1.9077225274480532,1.1474143346185122,comm_infra | |
| 19 | +road,2.8878850939209206,-0.8863284670864775,comm_infra | |
| 20 | +bridge,2.7265013330964507,-0.7485140494607205,comm_infra | |
| 21 | +canal,2.165331667573359,-0.1798629790826995,comm_infra | |
| 22 | +port,2.2796348701360767,-0.5152583130598091,comm_infra | |
| 23 | +written_record,8.0,-0.5885046260962389,records | |
| 24 | +script,7.974647676175637,-0.6926954931493575,records | |
| 25 | +phonetic_alphabetic_writing,2.1001761814441022,-0.1634518168751169,records | |
| 26 | +non_phonetic_writing,0.6965894707957191,1.9288626298500053,records | |
| 27 | +lists_tables_and_classification,7.643776344260694,-0.4587993518664177,records | |
| 28 | +calendar,8.0,-0.5777488445589021,records | |
| 29 | +practical_literature,8.0,-0.2566162216282787,records | |
| 30 | +nonwritten_record,1.2026821075448688,-2.671075035463131,records | |
| 31 | +mnemonic_device,1.038487055773755,-1.398153218842954,records | |
added
results/empirical/power_h2a_binary.csv
+8 −0
@@ -0,0 +1,8 @@ | ||
| 1 | +beta,power,n | |
| 2 | +0.2,0.1875,127 | |
| 3 | +0.3,0.31,127 | |
| 4 | +0.4,0.47,127 | |
| 5 | +0.5,0.625,127 | |
| 6 | +0.6,0.785,127 | |
| 7 | +0.8,0.945,127 | |
| 8 | +1.0,0.99,127 | |
added
results/empirical/power_h2a_fractional.csv
+7 −0
@@ -0,0 +1,7 @@ | ||
| 1 | +beta,power,n | |
| 2 | +0.1,0.4925,127 | |
| 3 | +0.15,0.805,127 | |
| 4 | +0.2,0.9625,127 | |
| 5 | +0.3,1.0,127 | |
| 6 | +0.4,1.0,127 | |
| 7 | +0.5,1.0,127 | |
modified
results/logs/execution_log.md
+3 −0
@@ -17,3 +17,6 @@ | ||
| 17 | 17 | | 2026-11-01 03:55 | 2 | Glottolog 5.3 languages.csv registered + fetched (language families for Galton controls) | registry 9 sources | |
| 18 | 18 | | 2026-11-01 04:10 | 2 | `python -m wp35.harmonization.build_panels` (3 runs: EA033 code parsing fixed via `ord`; EA202 continuous; Y6 redefined as last regime year with irregular end type after inspecting France 1789–1960) | `data/processed/*.parquet`, `results/diagnostics/*`, `paper/figures/fig_seshat_temporal_coverage.pdf`, `fig_seshat_missingness.pdf`, `fig_dplace_focal_years.pdf`, `fig_modern_coverage.pdf`, `coverage_summary.json` | |
| 19 | 19 | | 2026-11-01 04:15 | 2 | Data dictionary + parser tests (`pytest`: 6 passed) | `data/metadata/data_dictionary.md`, `tests/test_harmonization.py` | |
| 20 | +| 2026-11-01 04:40 | 3 | Formal framework drafted (fork agent) and reviewed: Theorems 1–5 with proofs, Conjectures C1–C3, mapping to tests; Lindley 1952 / Loynes 1962 verified by Crossref and added | `research/framework.md` | | |
| 21 | +| 2026-11-01 04:50 | 3 | `python -m wp35.measurement.build_indices` (2 runs; IRT regularized with MAP priors after boundary estimates) | `data/processed/seshat_indices.parquet`, `seshat_crisis_indices.parquet`, `modern_indices.parquet`; `results/empirical/measurement_*`, `power_h2a_*.csv`; figures `fig_measurement_DK.pdf`, `fig_irt_invariance.pdf` | | |
| 22 | +| 2026-11-01 05:00 | 3 | Measurement report: corr(D,K)=0.84, P keeps 16.6% of variance; MDE Y1b≈0.6 (underpowered), Y1≈0.15 | `research/measurement.md` | | |
added
src/wp35/measurement/build_indices.py
+288 −0
@@ -0,0 +1,288 @@ | ||
| 1 | +"""Phase 3 measurement: build D, K, P (and rival covariates) exactly as pre-registered, with alternatives, | |
| 2 | +measurement error, cross-region comparability diagnostics and an a-priori power computation. | |
| 3 | +Usage: python -m wp35.measurement.build_indices | |
| 4 | +No outcome (Y1, Y2, Y3, Y4-6) is read or used anywhere in this module. | |
| 5 | +""" | |
| 6 | +from __future__ import annotations | |
| 7 | +import json | |
| 8 | +from pathlib import Path | |
| 9 | +import numpy as np | |
| 10 | +import pandas as pd | |
| 11 | +import matplotlib | |
| 12 | +matplotlib.use("Agg") | |
| 13 | +import matplotlib.pyplot as plt | |
| 14 | +import statsmodels.api as sm | |
| 15 | +from wp35.measurement.irt import fit_2pl | |
| 16 | +from wp35.harmonization.build_panels import K_BLOCKS, STANDARDS, D_VARS, PROD_INFRA, MILITARY | |
| 17 | + | |
| 18 | +ROOT = Path(__file__).resolve().parents[3] | |
| 19 | +PROC = ROOT / "data" / "processed"; RES = ROOT / "results" / "empirical"; FIG = ROOT / "paper" / "figures" | |
| 20 | +RES.mkdir(parents=True, exist_ok=True); FIG.mkdir(parents=True, exist_ok=True) | |
| 21 | +SEED = 20261101 | |
| 22 | +K_ITEMS_MODERN = ["v2clrspct", "v2stcritrecadm", "v2cltrnslw", "v2stfisccap", "v2svstterr", "v2elembcap"] | |
| 23 | +BIN_K = [v for b in K_BLOCKS.values() for v in b if v != "administrative_level"] | |
| 24 | + | |
| 25 | + | |
| 26 | +def z(x: pd.Series, ref: pd.Series | None = None) -> pd.Series: | |
| 27 | + ref = x if ref is None else ref | |
| 28 | + return (x - ref.mean()) / ref.std(ddof=0) | |
| 29 | + | |
| 30 | + | |
| 31 | +def pc1_pairwise(df: pd.DataFrame, min_coded: int = 2): | |
| 32 | + """First principal component of the pairwise-complete correlation matrix; scores = loading-weighted mean of the | |
| 33 | + available standardized variables (requires >= min_coded coded). Loadings signed so that the sum is positive.""" | |
| 34 | + Zs = (df - df.mean()) / df.std(ddof=0) | |
| 35 | + C = Zs.corr(min_periods=10).fillna(0).values | |
| 36 | + w, V = np.linalg.eigh(C); v = V[:, -1]; v = v if v.sum() > 0 else -v | |
| 37 | + load = pd.Series(v / np.abs(v).sum(), index=df.columns) | |
| 38 | + avail = Zs.notna() | |
| 39 | + num = (Zs.fillna(0) * load).sum(axis=1); den = (avail * load.abs()).sum(axis=1) | |
| 40 | + score = num / den.where(den > 0) | |
| 41 | + score[avail.sum(axis=1) < min_coded] = np.nan | |
| 42 | + return score, load, float(w[-1] / w.sum()) | |
| 43 | + | |
| 44 | + | |
| 45 | +def block_score(wide: pd.DataFrame, items: list[str], min_share: float = 0.5) -> pd.Series: | |
| 46 | + sub = wide.reindex(columns=items).copy() | |
| 47 | + if "administrative_level" in sub.columns: | |
| 48 | + r = sub["administrative_level"].rank(method="average") | |
| 49 | + sub["administrative_level"] = (r - 1) / (r.max() - 1) | |
| 50 | + coded = sub.notna().mean(axis=1) | |
| 51 | + return sub.mean(axis=1).where(coded >= min_share) | |
| 52 | + | |
| 53 | + | |
| 54 | +def build_seshat(): | |
| 55 | + wide = pd.read_parquet(PROC / "seshat_polity_wide.parquet") | |
| 56 | + agg = pd.read_parquet(PROC / "seshat_polity_agg.parquet") | |
| 57 | + meta = pd.read_parquet(PROC / "seshat_polity_meta.parquet").set_index("polity_id") | |
| 58 | + logw = agg.pivot(index="polity_id", columns="variable", values="logv_mean") | |
| 59 | + lo = agg.pivot(index="polity_id", columns="variable", values="lo_min"); hi = agg.pivot(index="polity_id", columns="variable", values="hi_max") | |
| 60 | + out = pd.DataFrame(index=wide.index) | |
| 61 | + # ---- D ---- | |
| 62 | + Dmat = pd.DataFrame({"log_pop": logw["polity_population"], "log_terr": logw["polity_territory"], | |
| 63 | + "log_cap": logw["population_of_the_largest_settlement"], "settl_hier": wide["settlement_hierarchy"]}) | |
| 64 | + out["D_pc1"], D_load, D_var = pc1_pairwise(Dmat) | |
| 65 | + out["log_pop"] = Dmat.log_pop; out["log_terr"] = Dmat.log_terr; out["log_cap"] = Dmat.log_cap; out["settl_hier"] = Dmat.settl_hier | |
| 66 | + # bounds for measurement-error sensitivity (geometric variables only) | |
| 67 | + with np.errstate(divide="ignore"): | |
| 68 | + out["log_pop_lo"] = np.log(lo["polity_population"].where(lo["polity_population"] > 0)); out["log_pop_hi"] = np.log(hi["polity_population"].where(hi["polity_population"] > 0)) | |
| 69 | + out["log_terr_lo"] = np.log(lo["polity_territory"].where(lo["polity_territory"] > 0)); out["log_terr_hi"] = np.log(hi["polity_territory"].where(hi["polity_territory"] > 0)) | |
| 70 | + # ---- K blocks ---- | |
| 71 | + for b, items in K_BLOCKS.items(): | |
| 72 | + out[f"K_{b}"] = block_score(wide, items) | |
| 73 | + blocks = out[[f"K_{b}" for b in K_BLOCKS]] | |
| 74 | + out["K_mean"] = blocks.mean(axis=1).where(blocks.notna().sum(axis=1) >= 3) | |
| 75 | + out["K_n_blocks"] = blocks.notna().sum(axis=1) | |
| 76 | + out["K_pc1"], K_load, K_var = pc1_pairwise(blocks, min_coded=3) | |
| 77 | + # IRT on binary items (disputed 0.5 -> missing) | |
| 78 | + Xb = wide.reindex(columns=BIN_K).copy(); Xb = Xb.where(Xb != 0.5) | |
| 79 | + irt = fit_2pl(Xb.values) | |
| 80 | + out["K_irt"] = irt["eap"]; out["K_irt_psd"] = irt["psd"]; out["K_irt_nitems"] = irt["n_items"] | |
| 81 | + item_params = pd.DataFrame({"item": BIN_K, "a": irt["a"], "b": irt["b"], "block": [next(k for k, v in K_BLOCKS.items() if i in v) for i in BIN_K]}) | |
| 82 | + item_params.to_csv(RES / "measurement_irt_item_parameters.csv", index=False) | |
| 83 | + # standards sub-index (sparse; latent only) | |
| 84 | + out["K_standards"] = block_score(wide, STANDARDS) | |
| 85 | + # ---- P ---- | |
| 86 | + for dname in ["D_pc1", "log_pop"]: | |
| 87 | + for kname in ["K_mean", "K_irt", "K_pc1"]: | |
| 88 | + out[f"P_{dname}_{kname}"] = z(out[dname]) - z(out[kname]) | |
| 89 | + out["P_main"] = out["P_D_pc1_K_mean"] | |
| 90 | + # standardization windows (sensitivity) | |
| 91 | + df = out.join(meta[["macro_region", "era"]]) | |
| 92 | + out["P_within_era"] = df.groupby("era").D_pc1.transform(z) - df.groupby("era").K_mean.transform(z) | |
| 93 | + out["P_within_region"] = df.groupby("macro_region").D_pc1.transform(z) - df.groupby("macro_region").K_mean.transform(z) | |
| 94 | + out["P_rank"] = out.D_pc1.rank(pct=True) - out.K_mean.rank(pct=True) | |
| 95 | + # bounds | |
| 96 | + Dlo = pd.DataFrame({"log_pop": out.log_pop_lo, "log_terr": out.log_terr_lo, "log_cap": Dmat.log_cap, "settl_hier": Dmat.settl_hier}) | |
| 97 | + Dhi = pd.DataFrame({"log_pop": out.log_pop_hi, "log_terr": out.log_terr_hi, "log_cap": Dmat.log_cap, "settl_hier": Dmat.settl_hier}) | |
| 98 | + out["D_pc1_lo"] = ((Dlo - Dmat.mean()) / Dmat.std(ddof=0)).fillna(0).mul(D_load).sum(axis=1) / (Dlo.notna() * D_load.abs()).sum(axis=1).replace(0, np.nan) | |
| 99 | + out["D_pc1_hi"] = ((Dhi - Dmat.mean()) / Dmat.std(ddof=0)).fillna(0).mul(D_load).sum(axis=1) / (Dhi.notna() * D_load.abs()).sum(axis=1).replace(0, np.nan) | |
| 100 | + out.loc[out.D_pc1.isna(), ["D_pc1_lo", "D_pc1_hi"]] = np.nan | |
| 101 | + out["P_main_lo"] = z(out.D_pc1_lo, out.D_pc1) - z(out.K_mean); out["P_main_hi"] = z(out.D_pc1_hi, out.D_pc1) - z(out.K_mean) | |
| 102 | + # drop inferred codes sensitivity: recompute K_mean using only non-inferred facts | |
| 103 | + facts = pd.read_parquet(PROC / "seshat_facts.parquet") | |
| 104 | + ni = facts[(~facts.inferred) & facts.value.notna()].groupby(["polity_id", "variable"]).value.mean().unstack() | |
| 105 | + out["K_mean_noinf"] = pd.concat([block_score(ni, it) for it in K_BLOCKS.values()], axis=1).pipe(lambda b: b.mean(axis=1).where(b.notna().sum(axis=1) >= 3)).reindex(out.index) | |
| 106 | + out["P_main_noinf"] = z(out.D_pc1) - z(out.K_mean_noinf) | |
| 107 | + # ---- rival covariates (excluded from K by design) ---- | |
| 108 | + wf = facts[facts.sheet == "Warfare"].groupby(["polity_id", "variable"]).value.mean().unstack() | |
| 109 | + out["X_warfare"] = wf.mean(axis=1).where(wf.notna().mean(axis=1) >= 0.5).reindex(out.index) | |
| 110 | + out["X_prod_infra"] = block_score(wide, PROD_INFRA) | |
| 111 | + mil = wide.reindex(columns=MILITARY).copy(); r = mil["military_level"].rank(); mil["military_level"] = (r - 1) / (r.max() - 1) | |
| 112 | + out["X_military"] = mil.mean(axis=1).where(mil.notna().mean(axis=1) >= 0.5) | |
| 113 | + out["X_moralizing"] = wide.get("moralizing_supernatural_punishment_and_reward") | |
| 114 | + rt = facts[facts.sheet == "Religion"].groupby(["polity_id", "variable"]).value.mean().unstack() | |
| 115 | + out["X_moralizing"] = rt.get("moralizing_supernatural_punishment_and_reward").reindex(out.index) if "moralizing_supernatural_punishment_and_reward" in rt else np.nan | |
| 116 | + # Turchin-style single complexity axis over ALL social-complexity indicators (baseline B3) | |
| 117 | + sc_vars = [v for v in facts[facts.sheet == "Social complexity"].variable.unique() if v in wide.columns] | |
| 118 | + scmat = wide[sc_vars].copy() | |
| 119 | + for v in ["polity_population", "polity_territory", "population_of_the_largest_settlement"]: | |
| 120 | + scmat[v] = logw[v] | |
| 121 | + scmat = scmat.loc[:, scmat.notna().sum() >= 100] | |
| 122 | + out["complexity_pc1"], C_load, C_var = pc1_pairwise(scmat, min_coded=5) | |
| 123 | + out = out.join(meta[["seshat_region", "homeNGA", "macro_region", "era", "start_year", "end_year", "midpoint", "duration", "censored", "centralization", "utm_zone"]]) | |
| 124 | + out.index.name = "polity_id" | |
| 125 | + out.reset_index().to_parquet(PROC / "seshat_indices.parquet", index=False) | |
| 126 | + # ---- pre-crisis versions (same loadings/standardization as the full sample) ---- | |
| 127 | + pre = pd.read_parquet(PROC / "seshat_precrisis_agg.parquet") | |
| 128 | + pw = pre.pivot(index="crisis_id", columns="variable", values="value_mean"); plog = pre.pivot(index="crisis_id", columns="variable", values="logv_mean") | |
| 129 | + cr = pd.DataFrame(index=pw.index) | |
| 130 | + Dp = pd.DataFrame({"log_pop": plog.get("polity_population"), "log_terr": plog.get("polity_territory"), "log_cap": plog.get("population_of_the_largest_settlement"), "settl_hier": pw.get("settlement_hierarchy")}, index=pw.index) | |
| 131 | + Zp = (Dp - Dmat.mean()) / Dmat.std(ddof=0) | |
| 132 | + cr["D_pc1"] = (Zp.fillna(0) * D_load).sum(axis=1) / (Zp.notna() * D_load.abs()).sum(axis=1).replace(0, np.nan) | |
| 133 | + cr.loc[Zp.notna().sum(axis=1) < 2, "D_pc1"] = np.nan | |
| 134 | + cr["log_pop"] = Dp.log_pop | |
| 135 | + for b, items in K_BLOCKS.items(): | |
| 136 | + cr[f"K_{b}"] = block_score(pw, items) | |
| 137 | + bl = cr[[f"K_{b}" for b in K_BLOCKS]]; cr["K_mean"] = bl.mean(axis=1).where(bl.notna().sum(axis=1) >= 3) | |
| 138 | + Xp = pw.reindex(columns=BIN_K).where(lambda d: d != 0.5) | |
| 139 | + # EAP with the full-sample item parameters (no refit): posterior under fixed a, b | |
| 140 | + nodes, weights = np.polynomial.hermite_e.hermegauss(41); weights /= weights.sum() | |
| 141 | + from scipy.special import expit | |
| 142 | + Pq = expit(irt["a"][None, :] * (nodes[:, None] - irt["b"][None, :])) | |
| 143 | + Xv = Xp.values; obs = ~np.isnan(Xv); Xz_ = np.where(obs, Xv, 0) | |
| 144 | + L = Xz_ @ np.log(Pq + 1e-300).T + ((1 - Xz_) * obs) @ np.log(1 - Pq + 1e-300).T + np.log(weights)[None, :] | |
| 145 | + post = np.exp(L - L.max(axis=1, keepdims=True)); post /= post.sum(axis=1, keepdims=True) | |
| 146 | + cr["K_irt"] = np.where(obs.sum(axis=1) >= 3, post @ nodes, np.nan) | |
| 147 | + cr["K_pc1"] = (((bl - blocks.mean()) / blocks.std(ddof=0)).fillna(0) * K_load).sum(axis=1) / (bl.notna() * K_load.abs()).sum(axis=1).replace(0, np.nan) | |
| 148 | + cr.loc[bl.notna().sum(axis=1) < 3, "K_pc1"] = np.nan | |
| 149 | + for dname in ["D_pc1", "log_pop"]: | |
| 150 | + for kname in ["K_mean", "K_irt", "K_pc1"]: | |
| 151 | + cr[f"P_{dname}_{kname}"] = z(cr[dname], out[dname]) - z(cr[kname], out[kname]) | |
| 152 | + cr["P_main"] = cr["P_D_pc1_K_mean"] | |
| 153 | + wfp = pre[pre.variable.isin(wf.columns)].pivot(index="crisis_id", columns="variable", values="value_mean") | |
| 154 | + cr["X_warfare"] = wfp.mean(axis=1).where(wfp.notna().mean(axis=1) >= 0.5).reindex(cr.index) | |
| 155 | + cr["X_prod_infra"] = block_score(pw, PROD_INFRA) | |
| 156 | + milp = pw.reindex(columns=MILITARY).copy(); milp["military_level"] = milp["military_level"].rank().pipe(lambda r: (r - 1) / (r.max() - 1)); cr["X_military"] = milp.mean(axis=1).where(milp.notna().mean(axis=1) >= 0.5) | |
| 157 | + scp = pw.reindex(columns=scmat.columns).copy() | |
| 158 | + for v in ["polity_population", "polity_territory", "population_of_the_largest_settlement"]: | |
| 159 | + scp[v] = plog.get(v) | |
| 160 | + Zc = (scp - scmat.mean()) / scmat.std(ddof=0) | |
| 161 | + cr["complexity_pc1"] = (Zc.fillna(0) * C_load).sum(axis=1) / (Zc.notna() * C_load.abs()).sum(axis=1).replace(0, np.nan) | |
| 162 | + cr.loc[Zc.notna().sum(axis=1) < 5, "complexity_pc1"] = np.nan | |
| 163 | + crisis = pd.read_parquet(PROC / "seshat_crisis.parquet").set_index("crisis_id") | |
| 164 | + cr["polity_id"] = crisis.polity_id; cr["crisis_start"] = crisis.crisis_start | |
| 165 | + cr = cr.join(meta[["macro_region", "era", "seshat_region", "homeNGA", "start_year"]], on="polity_id") | |
| 166 | + cr["years_since_start"] = cr.crisis_start - cr.start_year | |
| 167 | + cr.reset_index().to_parquet(PROC / "seshat_crisis_indices.parquet", index=False) | |
| 168 | + # ---- diagnostics: collinearity, comparability, measurement error ---- | |
| 169 | + diag = {"D_pc1_loadings": D_load.round(4).to_dict(), "D_pc1_var_share": round(D_var, 4), | |
| 170 | + "K_pc1_loadings": K_load.round(4).to_dict(), "K_pc1_var_share": round(K_var, 4), | |
| 171 | + "complexity_pc1_var_share": round(C_var, 4), "complexity_pc1_n_vars": int(scmat.shape[1]), | |
| 172 | + "irt_loglik": irt["loglik"], "irt_iterations": irt["iterations"], | |
| 173 | + "n_D_pc1": int(out.D_pc1.notna().sum()), "n_K_mean": int(out.K_mean.notna().sum()), "n_K_irt": int(out.K_irt.notna().sum()), | |
| 174 | + "n_P_main": int(out.P_main.notna().sum()), "n_P_main_crisis": int(cr.P_main.notna().sum()), | |
| 175 | + "corr_zD_zK": {"D_pc1~K_mean": round(out.D_pc1.corr(out.K_mean), 4), "D_pc1~K_irt": round(out.D_pc1.corr(out.K_irt), 4), | |
| 176 | + "log_pop~K_mean": round(out.log_pop.corr(out.K_mean), 4), "K_mean~K_irt": round(out.K_mean.corr(out.K_irt), 4), | |
| 177 | + "K_mean~K_pc1": round(out.K_mean.corr(out.K_pc1), 4), "P_main~complexity_pc1": round(out.P_main.corr(out.complexity_pc1), 4), | |
| 178 | + "P_main~D_pc1": round(out.P_main.corr(out.D_pc1), 4), "P_main~K_mean": round(out.P_main.corr(out.K_mean), 4)}, | |
| 179 | + "P_variance_share_of_D_plus_K": round(float(out.P_main.var() / (z(out.D_pc1).var() + z(out.K_mean).var())), 4), | |
| 180 | + "block_correlations": blocks.corr().round(3).to_dict(), | |
| 181 | + "block_alpha_by_macro_region": {}, "block_corr_mean_by_era": {}, | |
| 182 | + "measurement_error": {"D_pc1_halfwidth_median": float(((out.D_pc1_hi - out.D_pc1_lo) / 2).median()), | |
| 183 | + "D_pc1_halfwidth_q90": float(((out.D_pc1_hi - out.D_pc1_lo) / 2).quantile(0.9)), | |
| 184 | + "K_irt_psd_median": float(out.K_irt_psd.median()), "K_irt_psd_q90": float(out.K_irt_psd.quantile(0.9)), | |
| 185 | + "sd_K_irt": float(out.K_irt.std()), "P_main_range_median": float((out.P_main_hi - out.P_main_lo).abs().median())}} | |
| 186 | + for r, g in df.groupby("macro_region"): | |
| 187 | + b = g[[f"K_{k}" for k in K_BLOCKS]].dropna() | |
| 188 | + if len(b) >= 8: | |
| 189 | + k = b.shape[1]; alpha = k / (k - 1) * (1 - b.var(ddof=1).sum() / b.sum(axis=1).var(ddof=1)) | |
| 190 | + diag["block_alpha_by_macro_region"][r] = {"n": int(len(b)), "alpha": round(float(alpha), 3)} | |
| 191 | + for e, g in df.groupby("era"): | |
| 192 | + b = g[[f"K_{k}" for k in K_BLOCKS]] | |
| 193 | + c = b.corr().values; diag["block_corr_mean_by_era"][e] = {"n": int(b.dropna().shape[0]), "mean_offdiag_corr": round(float(c[np.triu_indices(4, 1)].mean()), 3)} | |
| 194 | + # IRT invariance: refit on Old World vs Americas/Oceania and compare difficulties | |
| 195 | + new_world = df.macro_region.isin(["North_America", "South_America", "Southeast_Asia_Oceania"]) | |
| 196 | + inv = {} | |
| 197 | + for name, mask in [("old_world", ~new_world), ("new_world_oceania", new_world)]: | |
| 198 | + f = fit_2pl(Xb[mask.values].values); inv[name] = {"n": int(mask.sum()), "b": f["b"].round(3).tolist(), "a": f["a"].round(3).tolist()} | |
| 199 | + bo, bn = np.array(inv["old_world"]["b"]), np.array(inv["new_world_oceania"]["b"]) | |
| 200 | + ok = np.isfinite(bo) & np.isfinite(bn) & (np.abs(bo) < 5.9) & (np.abs(bn) < 5.9) | |
| 201 | + diag["irt_invariance"] = {"items_compared": int(ok.sum()), "corr_difficulty_old_vs_new": round(float(np.corrcoef(bo[ok], bn[ok])[0, 1]), 3), | |
| 202 | + "mean_abs_diff_difficulty": round(float(np.abs(bo[ok] - bn[ok]).mean()), 3), "items": BIN_K, "fits": inv} | |
| 203 | + # leave-one-block-out sensitivity of K and P | |
| 204 | + loo = {} | |
| 205 | + for b in K_BLOCKS: | |
| 206 | + others = [f"K_{k}" for k in K_BLOCKS if k != b]; kk = out[others].mean(axis=1).where(out[others].notna().sum(axis=1) >= 2) | |
| 207 | + loo[b] = {"corr_K_with_full": round(float(kk.corr(out.K_mean)), 4), "corr_P_with_main": round(float((z(out.D_pc1) - z(kk)).corr(out.P_main)), 4)} | |
| 208 | + diag["leave_one_block_out"] = loo | |
| 209 | + (RES / "measurement_diagnostics.json").write_text(json.dumps(diag, indent=2)) | |
| 210 | + # ---- figures ---- | |
| 211 | + fig, axes = plt.subplots(1, 3, figsize=(12, 3.8)) | |
| 212 | + axes[0].scatter(out.K_mean, out.D_pc1, s=8, alpha=0.5, c="#444"); axes[0].set_xlabel("K (mean of 4 blocks)"); axes[0].set_ylabel("D (PC1 of log scale variables)"); axes[0].set_title(f"r = {out.D_pc1.corr(out.K_mean):.2f}, n = {int((out.D_pc1.notna() & out.K_mean.notna()).sum())}", fontsize=9) | |
| 213 | + axes[1].scatter(out.K_mean, out.K_irt, s=8, alpha=0.5, c="#444"); axes[1].set_xlabel("K mean"); axes[1].set_ylabel("K IRT (2PL EAP)"); axes[1].set_title(f"r = {out.K_mean.corr(out.K_irt):.2f}", fontsize=9) | |
| 214 | + axes[2].errorbar(item_params.b, range(len(item_params)), xerr=None, fmt="o", ms=3, color="#444"); axes[2].set_yticks(range(len(item_params))); axes[2].set_yticklabels(item_params.item, fontsize=5); axes[2].set_xlabel("IRT difficulty b"); axes[2].set_title("Item difficulties", fontsize=9) | |
| 215 | + fig.tight_layout(); fig.savefig(FIG / "fig_measurement_DK.pdf"); plt.close(fig) | |
| 216 | + fig, ax = plt.subplots(figsize=(4.5, 4.2)); ax.scatter(bo[ok], bn[ok], s=12, c="#444"); lim = [min(bo[ok].min(), bn[ok].min()) - .3, max(bo[ok].max(), bn[ok].max()) + .3]; ax.plot(lim, lim, "k--", lw=.8) | |
| 217 | + ax.set_xlabel("difficulty, Old World fit"); ax.set_ylabel("difficulty, Americas/Oceania fit"); ax.set_title(f"IRT item invariance (r = {diag['irt_invariance']['corr_difficulty_old_vs_new']:.2f})", fontsize=9) | |
| 218 | + fig.tight_layout(); fig.savefig(FIG / "fig_irt_invariance.pdf"); plt.close(fig) | |
| 219 | + return out, cr, diag | |
| 220 | + | |
| 221 | + | |
| 222 | +def build_modern(): | |
| 223 | + p = pd.read_parquet(PROC / "modern_panel.parquet") | |
| 224 | + items = p[K_ITEMS_MODERN] | |
| 225 | + Zi = (items - items.mean()) / items.std(ddof=0) | |
| 226 | + p["K_modern"] = Zi.mean(axis=1).where(Zi.notna().sum(axis=1) >= 4) | |
| 227 | + p["K_modern_pc1"], Km_load, Km_var = pc1_pairwise(items, min_coded=4) | |
| 228 | + p["D_modern"] = p.log_pop | |
| 229 | + p["P_modern"] = z(p.D_modern) - z(p.K_modern) | |
| 230 | + p["P_modern_pc1"] = z(p.D_modern) - z(p.K_modern_pc1) | |
| 231 | + p["P_modern_within_year"] = p.groupby("year").D_modern.transform(z) - p.groupby("year").K_modern.transform(z) | |
| 232 | + p["P_modern_area"] = z(((z(p.log_pop) + z(p.log_area)) / 2)) - z(p.K_modern) | |
| 233 | + p.to_parquet(PROC / "modern_indices.parquet", index=False) | |
| 234 | + diag = {"K_modern_pc1_loadings": Km_load.round(4).to_dict(), "K_modern_pc1_var_share": round(Km_var, 4), | |
| 235 | + "n_P_modern": int(p.P_modern.notna().sum()), "corr_D_K_modern": round(float(p.D_modern.corr(p.K_modern)), 4), | |
| 236 | + "corr_K_modern_polyarchy": round(float(p.K_modern.corr(p.v2x_polyarchy)), 4), | |
| 237 | + "item_corr": items.corr().round(3).to_dict(), | |
| 238 | + "corr_K_by_region": {int(r): round(float(g.D_modern.corr(g.K_modern)), 3) for r, g in p.groupby("e_regionpol_6C") if g.P_modern.notna().sum() > 50}} | |
| 239 | + (RES / "measurement_diagnostics_modern.json").write_text(json.dumps(diag, indent=2)) | |
| 240 | + return p, diag | |
| 241 | + | |
| 242 | + | |
| 243 | +def power_h2a(cr: pd.DataFrame, n_rep: int = 400): | |
| 244 | + """A-priori MDE for H2-A: random outcomes assigned to the actual P distribution of crisis records (no real Y used).""" | |
| 245 | + rng = np.random.default_rng(SEED) | |
| 246 | + d = cr.dropna(subset=["P_main"]).copy() | |
| 247 | + P = z(d.P_main).values; reg = pd.factorize(d.seshat_region)[0]; n = len(P) | |
| 248 | + base = 0.77 # Y1b positive share read from coverage_summary (122/158); used only to set the intercept | |
| 249 | + res = [] | |
| 250 | + for beta in [0.2, 0.3, 0.4, 0.5, 0.6, 0.8, 1.0]: | |
| 251 | + rej = 0 | |
| 252 | + for _ in range(n_rep): | |
| 253 | + a0 = np.log(base / (1 - base)) | |
| 254 | + y = rng.random(n) < 1 / (1 + np.exp(-(a0 + beta * P))) | |
| 255 | + try: | |
| 256 | + m = sm.Logit(y.astype(float), sm.add_constant(P)).fit(disp=0, cov_type="cluster", cov_kwds={"groups": reg}) | |
| 257 | + rej += m.pvalues[1] < 0.05 | |
| 258 | + except Exception: | |
| 259 | + pass | |
| 260 | + res.append({"beta": beta, "power": rej / n_rep, "n": n}) | |
| 261 | + res = pd.DataFrame(res); res.to_csv(RES / "power_h2a_binary.csv", index=False) | |
| 262 | + # fractional outcome: Binomial(14, logistic(a0 + beta P)) with base severity 0.29 | |
| 263 | + res2 = [] | |
| 264 | + for beta in [0.1, 0.15, 0.2, 0.3, 0.4, 0.5]: | |
| 265 | + rej = 0 | |
| 266 | + for _ in range(n_rep): | |
| 267 | + a0 = np.log(0.29 / 0.71); pr = 1 / (1 + np.exp(-(a0 + beta * P))); y = rng.binomial(14, pr) / 14 | |
| 268 | + try: | |
| 269 | + m = sm.GLM(y, sm.add_constant(P), family=sm.families.Binomial()).fit(cov_type="cluster", cov_kwds={"groups": reg}) | |
| 270 | + rej += m.pvalues[1] < 0.05 | |
| 271 | + except Exception: | |
| 272 | + pass | |
| 273 | + res2.append({"beta": beta, "power": rej / n_rep, "n": n}) | |
| 274 | + res2 = pd.DataFrame(res2); res2.to_csv(RES / "power_h2a_fractional.csv", index=False) | |
| 275 | + return res, res2 | |
| 276 | + | |
| 277 | + | |
| 278 | +def main(): | |
| 279 | + out, cr, diag = build_seshat() | |
| 280 | + p, dm = build_modern() | |
| 281 | + pw1, pw2 = power_h2a(cr) | |
| 282 | + print(json.dumps({k: v for k, v in diag.items() if k in ("D_pc1_loadings", "D_pc1_var_share", "K_pc1_loadings", "n_D_pc1", "n_K_mean", "n_K_irt", "n_P_main", "n_P_main_crisis", "corr_zD_zK", "P_variance_share_of_D_plus_K", "measurement_error", "irt_invariance", "leave_one_block_out", "block_alpha_by_macro_region")}, indent=1, default=str)[:6000]) | |
| 283 | + print(json.dumps({k: v for k, v in dm.items() if k != "item_corr"}, indent=1)) | |
| 284 | + print("power binary:\n", pw1.to_string(index=False)); print("power fractional:\n", pw2.to_string(index=False)) | |
| 285 | + | |
| 286 | + | |
| 287 | +if __name__ == "__main__": | |
| 288 | + main() | |
added
src/wp35/measurement/irt.py
+63 −0
@@ -0,0 +1,63 @@ | ||
| 1 | +"""Two-parameter logistic IRT (marginal maximum likelihood, EM with Gauss–Hermite quadrature). | |
| 2 | + | |
| 3 | +Items are binary (0/1); missing entries contribute nothing to the likelihood (missing-at-random by design, no | |
| 4 | +imputation). Returns item discriminations a_j, difficulties b_j, EAP scores and posterior SDs for each unit. | |
| 5 | +Deterministic (no randomness). Written for transparency rather than speed (N ≈ 600 × J ≈ 30 is instant). | |
| 6 | +""" | |
| 7 | +from __future__ import annotations | |
| 8 | +import numpy as np | |
| 9 | +from scipy.special import expit | |
| 10 | + | |
| 11 | + | |
| 12 | +def fit_2pl(X: np.ndarray, n_quad: int = 41, max_iter: int = 1000, tol: float = 1e-6, min_items: int = 3, | |
| 13 | + prior_sd_log_a: float = 0.5, prior_sd_b: float = 2.0): | |
| 14 | + """MAP variant: log-normal prior on a (mean 0, sd prior_sd_log_a) and normal prior on b (sd prior_sd_b) to keep | |
| 15 | + items that are nearly constant in the sample from running to the boundary (Heywood-type cases).""" | |
| 16 | + X = np.asarray(X, dtype=float) | |
| 17 | + N, J = X.shape | |
| 18 | + obs = ~np.isnan(X) | |
| 19 | + Xz = np.where(obs, X, 0.0) | |
| 20 | + nodes, weights = np.polynomial.hermite_e.hermegauss(n_quad) # standard normal prior | |
| 21 | + weights = weights / weights.sum() | |
| 22 | + a = np.ones(J); b = np.zeros(J) | |
| 23 | + ll_old = -np.inf | |
| 24 | + for it in range(max_iter): | |
| 25 | + # E-step: posterior over nodes for each unit | |
| 26 | + P = expit(a[None, :] * (nodes[:, None] - b[None, :])) # Q x J | |
| 27 | + logP, log1P = np.log(P + 1e-300), np.log(1 - P + 1e-300) | |
| 28 | + # log-lik of unit i at node q = sum_j obs_ij [x_ij logP_qj + (1-x_ij) log(1-P_qj)] | |
| 29 | + L = Xz @ logP.T + ((1 - Xz) * obs) @ log1P.T # N x Q | |
| 30 | + L += np.log(weights)[None, :] | |
| 31 | + m = L.max(axis=1, keepdims=True) | |
| 32 | + post = np.exp(L - m); norm = post.sum(axis=1, keepdims=True); post /= norm | |
| 33 | + ll = float((m.squeeze() + np.log(norm.squeeze())).sum()) | |
| 34 | + # M-step: Newton updates per item on expected counts | |
| 35 | + r = post.T @ Xz # Q x J expected correct | |
| 36 | + n = post.T @ obs.astype(float) # Q x J expected tries | |
| 37 | + for j in range(J): | |
| 38 | + for _ in range(10): | |
| 39 | + p = expit(a[j] * (nodes - b[j])) | |
| 40 | + g_a = np.sum((r[:, j] - n[:, j] * p) * (nodes - b[j])) - (np.log(a[j]) / prior_sd_log_a ** 2) / a[j] | |
| 41 | + g_b = np.sum((r[:, j] - n[:, j] * p) * (-a[j])) - b[j] / prior_sd_b ** 2 | |
| 42 | + w = n[:, j] * p * (1 - p) | |
| 43 | + H_aa = -np.sum(w * (nodes - b[j]) ** 2) - (1 - np.log(a[j])) / (prior_sd_log_a ** 2 * a[j] ** 2) | |
| 44 | + H_bb = -np.sum(w * a[j] ** 2) - 1 / prior_sd_b ** 2 | |
| 45 | + H_ab = np.sum(w * a[j] * (nodes - b[j])) | |
| 46 | + H = np.array([[H_aa, H_ab], [H_ab, H_bb]]) - 1e-6 * np.eye(2) | |
| 47 | + step = np.linalg.solve(H, np.array([g_a, g_b])) | |
| 48 | + a[j] -= step[0]; b[j] -= step[1] | |
| 49 | + a[j] = float(np.clip(a[j], 0.05, 8.0)); b[j] = float(np.clip(b[j], -6, 6)) | |
| 50 | + if np.abs(step).max() < 1e-6: | |
| 51 | + break | |
| 52 | + if abs(ll - ll_old) < tol: | |
| 53 | + break | |
| 54 | + ll_old = ll | |
| 55 | + # final EAP | |
| 56 | + P = expit(a[None, :] * (nodes[:, None] - b[None, :])) | |
| 57 | + L = Xz @ np.log(P + 1e-300).T + ((1 - Xz) * obs) @ np.log(1 - P + 1e-300).T + np.log(weights)[None, :] | |
| 58 | + m = L.max(axis=1, keepdims=True); post = np.exp(L - m); post /= post.sum(axis=1, keepdims=True) | |
| 59 | + eap = post @ nodes | |
| 60 | + psd = np.sqrt(np.maximum(post @ nodes ** 2 - eap ** 2, 0)) | |
| 61 | + n_items = obs.sum(axis=1) | |
| 62 | + eap = np.where(n_items >= min_items, eap, np.nan); psd = np.where(n_items >= min_items, psd, np.nan) | |
| 63 | + return {"a": a, "b": b, "eap": eap, "psd": psd, "loglik": ll, "iterations": it + 1, "n_items": n_items} | |
| 64 | ||