Skip to main content

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

pip install netpol

For local development:

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.DiGraphs only (undirected graphs and MultiDiGraphs raise TypeError). A multilayer network is just dict[layer_id, DiGraph].

Quickstart

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:

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 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.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

netpol-0.2.1.tar.gz (14.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

netpol-0.2.1-py3-none-any.whl (18.2 kB view details)

Uploaded Python 3

File details

Details for the file netpol-0.2.1.tar.gz.

File metadata

  • Download URL: netpol-0.2.1.tar.gz
  • Upload date:
  • Size: 14.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for netpol-0.2.1.tar.gz
Algorithm Hash digest
SHA256 0871b3f974f2270729b0c2e74814e00c723133dc0672d2edc944dee6b344d272
MD5 6aaf1e91737a0e64e505dff960f9484b
BLAKE2b-256 dc2680e39b12effb4ea1241f4f15c7ebad4e2eb92cd155bdaf6549860053ef00

See more details on using hashes here.

Provenance

The following attestation bundles were made for netpol-0.2.1.tar.gz:

Publisher: publish.yml on alessiogandelli/netpol

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file netpol-0.2.1-py3-none-any.whl.

File metadata

  • Download URL: netpol-0.2.1-py3-none-any.whl
  • Upload date:
  • Size: 18.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for netpol-0.2.1-py3-none-any.whl
Algorithm Hash digest
SHA256 675f0bb85e520db13503389ff9f0eb0978fb0e52dadbbc61b9be52c4c58760ef
MD5 7b5f349bcd7d757015878848177ddaa3
BLAKE2b-256 b550f27886242dfe890f0f6fbc13870d1787b413422b927d4fed473801396c41

See more details on using hashes here.

Provenance

The following attestation bundles were made for netpol-0.2.1-py3-none-any.whl:

Publisher: publish.yml on alessiogandelli/netpol

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

0.3.0

2 files

This release

0.2.1 This release

2 files

0.2.0

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page