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 -> bmeans "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:
- Select influencers -- top
n_influencersnodes byin_degree(configurable) with deterministic tie-breaking. - Build the interaction table -- one row per edge into an influencer
(
['influencer', 'user']), self-loops excluded. - Score users -- correspondence analysis maps each user to a score in
[-1, 1]on a bipolar ideology axis (theIdeologyScorerplug point; the built-inLatentIdeologyScoreris deterministic). - 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 singlenx.DiGraphand get aLayerResult, or adict[layer_id, DiGraph]and get adict[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 (seenetpol/config.py).influencer_strategyis typedLiteral["degree", "in_degree"]. -
LatentIdeologyScorer(min_sources=2, max_sources=None)-- built-in scorer. -
IdeologyScorer--Protocolto plug in your own scoring. -
LayerResult-- what you get back per network/layer:field type meaning layer_idHashable | Nonelayer id ( Nonefor single networks)n_nodes,n_edgesintsize of the analyzed graph influencerslist[Hashable]selected influencer node ids scoresDataFrame | Noneideology scores, indexed by node id, columns score_1..score_n, values in[-1, 1]dip_statistic,p_valuefloat | NoneHartigan's dip test output adjusted_p_valuefloat | NoneBH-adjusted p-value (multilayer + FDR only) is_polarizedbool | Nonep-value (adjusted if available) below significance_levelskip_reasonstr | Nonewhy the layer was skipped, if it was was_analyzedboolproperty: Trueiff scoring and dip test ran
Type aliases (documented in netpol/types.py) make the data shapes explicit:
LayerId = HashableLayers = dict[LayerId, nx.DiGraph]Results = dict[LayerId, LayerResult]InteractionTable = DataFramewith columns['influencer', 'user']ScoreTable = DataFrameindexed by node id with columnsscore_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_reasonon every failure path (no silentexcept), directed-graph validation,min_edgesguardrail.
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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file netpol-0.2.0.tar.gz.
File metadata
- Download URL: netpol-0.2.0.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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e29d8de85ffd66f12a03fd40ba7d938d24981235a14d73a1e432cb0761264b7a
|
|
| MD5 |
fac155c385074fee454a3fbf5b723a90
|
|
| BLAKE2b-256 |
210e41c95141c5e0f08ffe0fc868a054edc21969a533d1149d51218f73b08489
|
Provenance
The following attestation bundles were made for netpol-0.2.0.tar.gz:
Publisher:
publish.yml on alessiogandelli/netpol
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
netpol-0.2.0.tar.gz -
Subject digest:
e29d8de85ffd66f12a03fd40ba7d938d24981235a14d73a1e432cb0761264b7a - Sigstore transparency entry: 2639939057
- Sigstore integration time:
-
Permalink:
alessiogandelli/netpol@628f7a60a372d284e1a33a4b0c21d7c81ef39162 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/alessiogandelli
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@628f7a60a372d284e1a33a4b0c21d7c81ef39162 -
Trigger Event:
push
-
Statement type:
File details
Details for the file netpol-0.2.0-py3-none-any.whl.
File metadata
- Download URL: netpol-0.2.0-py3-none-any.whl
- Upload date:
- Size: 18.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3575e8e777890b06017df9236036bf11631cafc1c7ef28208f1f6be2da2d7f16
|
|
| MD5 |
637980c6caa1981cb6dd7fe8b7a4b297
|
|
| BLAKE2b-256 |
1ee76260cc1436db4b5e79b22d9917c4ae88e44ab77d5307ec74cb249d2b86e4
|
Provenance
The following attestation bundles were made for netpol-0.2.0-py3-none-any.whl:
Publisher:
publish.yml on alessiogandelli/netpol
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
netpol-0.2.0-py3-none-any.whl -
Subject digest:
3575e8e777890b06017df9236036bf11631cafc1c7ef28208f1f6be2da2d7f16 - Sigstore transparency entry: 2639939184
- Sigstore integration time:
-
Permalink:
alessiogandelli/netpol@628f7a60a372d284e1a33a4b0c21d7c81ef39162 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/alessiogandelli
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@628f7a60a372d284e1a33a4b0c21d7c81ef39162 -
Trigger Event:
push
-
Statement type: