← back to map

How War Factor Analysis Works

The Question

Personality psychology has OCEAN — five latent dimensions (Openness, Conscientiousness, Extraversion, Agreeableness, Neuroticism) that emerged from factor-analyzing thousands of personality questionnaires. They weren't designed; they were discovered.

Can we do the same for wars? Take every interstate war in modern history, encode each as a feature vector, and let PCA find the latent dimensions of armed conflict?

The answer is: nobody has done it. Rummel (1963–76) factor-analyzed conflict behaviors across nations, not individual wars. The Correlates of War project built the dataset but never ran dimensionality reduction on it. This tool does.

The Data

All 95 interstate wars from the Correlates of War v4.0 dataset, covering 1823 (Franco-Spanish War) through 2003 (Invasion of Iraq). Each war includes per-state participation records: side, battle deaths, outcome, initiator status.

The data lives on an ATProto PDS as com.minomobi.wars.war records — publicly readable, permanently addressable, owned by the publisher. The dashboard fetches directly from the PDS. No API key, no backend, no database. The protocol is the database.

COW CSV → sync script → ATProto PDS → browser fetch → PCA → scatter plot ↓ com.minomobi.wars.war (one record per war)

Feature Engineering

Each war is encoded as a 7-dimensional vector. The features were chosen to capture distinct aspects of conflict character:

FeatureWhat it capturesTransform
log(deaths)Scale of violencelog₁₀(total battle deaths + 1)
log(duration)Temporal extentlog₁₀(days + 1)
participantsHow many states foughtRaw count
coalition_sizeLargest alliance blocMax states on one side
death_asymmetryBalance of casualtiesmin(side deaths) / max(side deaths), 0–1
decisiveClear winner/loser?Binary: 1 if outcomes include both winner and loser
start_yearHistorical era(year − 1800) / 200, normalized

Deaths and duration use log transforms because they span 4+ orders of magnitude (the Franco-Spanish War killed 1,000; World War II killed 16.6 million).

PCA: Principal Component Analysis

PCA finds the axes of maximum variance in the data. It's an eigendecomposition of the covariance matrix of the standardized feature matrix.

Raw features (95 wars × 7 features) ↓ Standardize (zero mean, unit variance) ↓ Covariance matrix (7 × 7) ↓ Jacobi eigendecomposition ↓ Eigenvectors = principal components (the "dimensions") Eigenvalues = variance explained by each ↓ Project wars onto components → scatter coordinates

What the components mean

Each PC is a weighted combination of the original 7 features. The weights (loadings) tell you what the dimension means. The dashboard shows loading bars for each axis so you can read the dimensions directly.

Typical findings from this dataset:

PCTypical interpretationKey loadings
PC1Scale & LethalityHigh on deaths, duration, participants
PC2Coalition ComplexityHigh on participants and coalition size, low on others
PC3Symmetry of LossHigh on death asymmetry
PC4DecisivenessHigh on decisive outcome
PC5ModernityHigh on start year

These aren't fixed labels — they emerge from the data. The dashboard auto-suggests names based on the highest-loading features.

Implementation

Everything runs in the browser. No server-side computation.

Jacobi eigendecomposition

The covariance matrix is symmetric, so we use iterative Jacobi rotations to find eigenvalues and eigenvectors. Each iteration zeroes the largest off-diagonal element via a Givens rotation. Convergence is fast for a 7×7 matrix — typically under 50 iterations.

repeat until max|off-diagonal| < 1e-10: find largest off-diagonal element a[p][q] compute rotation angle θ = ½ atan2(2a[p][q], a[p][p] − a[q][q]) apply Givens rotation to rows/cols p, q accumulate rotation in eigenvector matrix

Rendering

Canvas 2D with device-pixel-ratio scaling. Points sized by war magnitude (deaths, participants, or duration). Color-coded by region, era, decisiveness, or death scale. Hit testing for hover tooltips and click-to-select.

ATProto Storage

War records use the com.minomobi.wars.war lexicon. Each record stores the computed features plus the full participant list. The sync script (scripts/sync-wars-to-atproto.py) reads the COW CSV, computes features, and writes to the PDS using com.atproto.repo.applyWrites in batches of 10.

Record structure: { warNum: 4139, warName: "World War II", startYear: 1939, endYear: 1945, durationDays: 2076, totalDeaths: 16634907, numParticipants: 29, maxCoalitionSize: 21, deathAsymmetry: 0.56, decisive: true, region: "Europe & Africa", participants: [ { stateName: "Germany", side: 2, batDeath: 3500000, outcome: 2 }, { stateName: "USSR", side: 1, batDeath: 7500000, outcome: 1 }, ... ], source: "correlatesofwar.org/v4.0" }

Once synced, anyone can fetch the full dataset via the public ATProto API — no authentication required. The wars are just records in a repo, like any other ATProto content.

What You See in the Plot

World Wars I and II dominate PC1 (scale). They're the extreme outliers on every magnitude measure. The Korean War and Iran-Iraq War are the next tier.

The Gulf War and Kosovo appear in an interesting region — high coalition complexity (13+ coalition members) but relatively few deaths, producing a unique signature: large multilateral operations with asymmetric lethality.

19th-century European wars cluster tightly: bilateral, brief, decisive, moderate casualties. The Central American wars cluster too: small-scale, decisive, two-party.

Wars of decolonization and Cold War proxy conflicts spread across the middle — moderate scale, variable duration, less decisive outcomes.

Limitations

Interstate only. The COW dataset separates interstate, intra-state, extra-state, and non-state wars. This tool currently uses only the 95 interstate wars. Civil wars, colonial wars, and non-state conflicts are in the CSV data but not yet included.

Battle deaths only. Civilian casualties are not in the COW war dataset. Total war mortality would shift the picture significantly — WW2 killed ~70M total vs. the 16.6M battle deaths recorded here.

COW ends at 2003. The dataset doesn't include the Syrian Civil War, Yemen, Russia-Ukraine, or other ongoing conflicts. Future versions could incorporate UCDP data for post-2003 coverage.

N=95. Factor analysis works best with large samples. 95 observations on 7 variables is workable but not ideal. The results are suggestive, not definitive. Think of this as exploration, not proof.

Feature selection is a choice. Different features would produce different dimensions. Adding geography, terrain, technology era, regime type, or economic indicators would change the factor structure. The 7 chosen features are what the COW data provides directly.

Prior Art

Rummel (1963–76) — Factor-analyzed conflict behavior across nations. Found three domestic conflict dimensions (Turmoil, Revolution, Subversion) and several foreign conflict dimensions. Applied to national behavior patterns, not individual wars.

Rohner, Vanden Eynde & Verhille (2025) — "Variants of Violence" classifies countries along income, democracy, and security capacity. Algorithm-assigned, not hand-coded. Closest modern analogue, but classifies contexts, not wars.

Correlates of War project (1963–present) — The foundational dataset. 9-type war taxonomy (hand-assigned). No dimensionality reduction applied to the war-level data.

UCDP/PRIO Armed Conflict Dataset — Covers 1946–present with finer granularity. Natural complement for extending this analysis.

Data Source

Sarkees, Meredith Reid and Frank Wayman (2010). Resort to War: 1816–2007. Washington DC: CQ Press.

Correlates of War Project. Inter-State War Data, v4.0. correlatesofwar.org/data-sets/COW-war/