Metadata-Version: 2.4
Name: netpol
Version: 0.2.1
Summary: Measure polarization in (multilayer) social networks via latent-ideology scoring and Hartigan's dip test.
License: MIT
License-File: LICENSE
Keywords: polarization,social-networks,networkx,multilayer,ideology,dip-test,correspondence-analysis
Author: alessiogandelli
Author-email: alessiogandelli99@gmail.com
Requires-Python: >=3.10
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Science/Research
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Scientific/Engineering :: Information Analysis
Requires-Dist: diptest (>=0.8)
Requires-Dist: networkx (>=3.0)
Requires-Dist: numpy (>=1.24)
Requires-Dist: pandas (>=2.0)
Project-URL: Homepage, https://github.com/alessiogandelli/netpol
Project-URL: Repository, https://github.com/alessiogandelli/netpol
Description-Content-Type: text/markdown

# netpol

Measure polarization in (multilayer) social networks.

Given a network -- or a `dict` of per-layer networks -- `netpol` selects the
top influencers, scores every user on a **bipolar latent-ideology axis** via
correspondence analysis, and tests whether the resulting score distribution is
multimodal (polarized) using **Hartigan's dip test**, with optional
**Benjamini-Hochberg FDR correction** across layers.

The method follows Falkenberg et al. (2021) and Flamino et al. (2021).

## Install

```bash
pip install netpol
```

For local development:

```bash
git clone https://github.com/alessiogandelli/netpol && cd netpol
poetry install
poetry run pytest
```

## Edge convention

Fixed and non-negotiable:

> `a -> b` means **"a retweets/endorses b"**.

Pass `networkx.DiGraph`s only (undirected graphs and `MultiDiGraph`s raise
`TypeError`).  A multilayer network is just `dict[layer_id, DiGraph]`.

## Quickstart

```python
import networkx as nx
from netpol import PolarizationConfig, analyze, LatentIdeologyScorer

def polarized_network():           # two camps, each retweeting one influencer
    g = nx.DiGraph()
    for i in range(100):
        g.add_edge(f"c1_{i}", "inf_1")
        g.add_edge(f"c2_{i}", "inf_2")
    return g

config = PolarizationConfig(n_influencers=2, min_edges=1)
result = analyze(polarized_network(), config, LatentIdeologyScorer(min_sources=1))
print(result.is_polarized)         # True
```

For a multilayer network, pass a `dict[layer_id, DiGraph]` instead -- the
same `analyze` call (or `analyze_layers` explicitly) returns a
`dict[layer_id, LayerResult]` with FDR correction across layers:

```python
results = analyze({"l1": layer1(), "l2": layer2()}, config)
print(results["l1"].is_polarized)
```

See `examples/quickstart.py` for a runnable version.

## How it works

Per layer:

1. **Select influencers** -- top `n_influencers` nodes by `in_degree`
   (configurable) with deterministic tie-breaking.
2. **Build the interaction table** -- one row per edge *into* an influencer
   (`['influencer', 'user']`), self-loops excluded.
3. **Score users** -- correspondence analysis maps each user to a score in
   `[-1, 1]` on a bipolar ideology axis (the `IdeologyScorer` plug point; the
   built-in `LatentIdeologyScorer` is deterministic).
4. **Test for polarization** -- Hartigan's dip test on the score distribution.

Across layers, `analyze` / `analyze_layers` applies Benjamini-Hochberg FDR
correction to the per-layer p-values and re-evaluates `is_polarized` against
the adjusted values.

## API

Everything public is importable from the package root, so `netpol.` autocompletes
the full surface in your IDE.

Entry points:

- `analyze(target, config, scorer=None)` -- top-level entry point. Pass a single
  `nx.DiGraph` and get a `LayerResult`, or a `dict[layer_id, DiGraph]` and get a
  `dict[layer_id, LayerResult]` (with FDR correction).
- `analyze_network(graph, config, scorer=None)` -> `LayerResult` -- the
  single-network primitive.
- `analyze_layers(layers, config, scorer=None)` -> `dict[layer_id, LayerResult]` --
  the multilayer orchestration.

Configuration and results:

- `PolarizationConfig` -- frozen config dataclass (see `netpol/config.py`).
  `influencer_strategy` is typed `Literal["degree", "in_degree"]`.
- `LatentIdeologyScorer(min_sources=2, max_sources=None)` -- built-in scorer.
- `IdeologyScorer` -- `Protocol` to plug in your own scoring.
- `LayerResult` -- what you get back per network/layer:

  | field | type | meaning |
  |---|---|---|
  | `layer_id` | `Hashable \| None` | layer id (`None` for single networks) |
  | `n_nodes`, `n_edges` | `int` | size of the analyzed graph |
  | `influencers` | `list[Hashable]` | selected influencer node ids |
  | `scores` | `DataFrame \| None` | ideology scores, indexed by node id, columns `score_1..score_n`, values in `[-1, 1]` |
  | `dip_statistic`, `p_value` | `float \| None` | Hartigan's dip test output |
  | `adjusted_p_value` | `float \| None` | BH-adjusted p-value (multilayer + FDR only) |
  | `is_polarized` | `bool \| None` | p-value (adjusted if available) below `significance_level` |
  | `skip_reason` | `str \| None` | why the layer was skipped, if it was |
  | `was_analyzed` | `bool` | property: `True` iff scoring and dip test ran |

Type aliases (documented in `netpol/types.py`) make the data shapes explicit:

- `LayerId = Hashable`
- `Layers = dict[LayerId, nx.DiGraph]`
- `Results = dict[LayerId, LayerResult]`
- `InteractionTable = DataFrame` with columns `['influencer', 'user']`
- `ScoreTable = DataFrame` indexed by node id with columns `score_1..score_n`

## What this does / doesn't do (yet)

Does:

- Faithful, deterministic implementation of the latent-ideology + dip-test
  pipeline (single-layer and multilayer).
- FDR correction, explicit `skip_reason` on every failure path (no silent
  `except`), directed-graph validation, `min_edges` guardrail.

Does **not** do yet (see [`docs/DEBATES.md`](docs/DEBATES.md) for the open
questions and `[REVISIT]` items):

- Effect-size / separation measure paired with the dip test.
- Score normalization across layers for comparison.
- Multivariate modality testing for `ideology_dimensions > 1`.
- Influencer-selection scope beyond per-layer (global/hybrid), adaptive pool
  sizing, or authority/HITS ranking.

## References

- M. Falkenberg et al., "Growing polarisation around climate change on social
  media", arXiv:2112.12137 (2021).
- J. Flamino et al., "Shifting polarization and Twitter news influencers
  between two US presidential elections", arXiv:2111.02505 (2021).

## License

MIT. See `LICENSE`.

