API reference¶
Estimator¶
graphdml.GraphDML
¶
Bases: BaseEstimator
Direct and peer effects on a network via graph double machine learning.
Estimates theta (average direct effect) and alpha (average peer effect) in the
partially linear network model::
T = m(X, A) + e_T
Y = theta * T + alpha * (E @ T) + g(X, A) + e_Y
where E is a known exposure map and m, g are unknown functions of a node's
own and its neighbors' covariates, learned with any scikit-learn model.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
model_y
|
scikit-learn regressor
|
Learns |
None
|
model_t
|
scikit-learn regressor or classifier
|
Learns |
None
|
model_g
|
scikit-learn regressor
|
Learns |
None
|
featurizer
|
object with ``transform(data)``, "own" or "neighborhood"
|
Builds the tabular features the nuisance models see. Defaults to
:class: |
None
|
exposure
|
(str, ExposureMap, list or None)
|
How neighbors' treatments reach a node: "sum", "mean", "weighted", ... A list fits
one peer coefficient per exposure. |
"sum"
|
score
|
('iv-type', 'partialling-out')
|
"iv-type" stays consistent if either the treatment model or the outcome-confounding
model |
"iv-type"
|
focal_set
|
('min_degree', 'random')
|
How to choose the focal set of nodes with independent scores. An array gives the
focal nodes explicitly (validated). |
"min_degree"
|
nuisance_training
|
('buffered', 'focal')
|
Which nodes train the nuisance models for each fold. "focal" trains on the other folds' focal nodes only. "buffered" trains on every node whose data is independent of the held-out fold's scores, which is typically several times more data. |
"buffered"
|
n_folds
|
int
|
|
5
|
n_rep
|
int
|
Repetitions of cross-fitting (with new focal sets and folds). Estimates are aggregated by the median, with the variance adjusted for split-to-split spread. |
1
|
aggregation
|
('dml2', 'dml1')
|
"dml2" solves the pooled moment condition; "dml1" averages per-fold estimates. |
"dml2"
|
clip_propensity
|
float
|
Clip classifier propensities to |
0.01
|
mode
|
(None, 'original')
|
|
None
|
estimation_nodes
|
array of node positions or boolean mask
|
Restrict the estimate to a sub-population: only these nodes can be focal. All nodes still supply neighbors' treatments and nuisance training data. |
None
|
random_state
|
(int, RandomState or None)
|
|
None
|
Attributes:
| Name | Type | Description |
|---|---|---|
coef_, se_, cov_ |
estimates, standard errors and covariance, ordered as
|
|
ate_ |
the average total effect, treating every node vs none (direct + peer).
|
|
ade_, ape_ |
the direct and (first) peer effect.
|
|
total_weights_ |
weights turning ``coef_`` into the total effect: 1 for the direct
|
effect and, per exposure, its average value when every node is treated. |
focal_nodes_, fold_assignment_ |
focal node positions and their folds (first rep).
|
|
n_focal_ |
effective sample size.
|
|
residuals_ |
per-focal-node predictions and residuals (first rep).
|
|
diagnostics_ |
dict of diagnostics; warnings_ : list of warning messages.
|
|
ate_
property
¶
Average total effect: treating every node vs no node (direct + peer).
ade_
property
¶
Average direct effect.
ape_
property
¶
Average peer effect for the first exposure map (None if no exposure).
fit(data)
¶
Estimate direct and peer effects from data.
summary_frame(alpha=0.05, total=True)
¶
Estimates, standard errors, z statistics, p-values and confidence intervals.
Rows: direct (ADE), one peer:<exposure> row per exposure (APE), and, when
there is a peer effect and total=True, total: the average total effect of
treating every node vs no node (the ATE).
cluster_summary_frame(groups, alpha=0.05, total=True)
¶
Like :meth:summary_frame, with standard errors clustered by groups.
groups gives a cluster label for every node (e.g. village). Use it when nodes
may share common shocks beyond their dependency sets. Requires n_rep=1.
summary(alpha=0.05)
¶
Human-readable results, diagnostics, warnings and identifying assumptions.
Data¶
graphdml.GraphData
dataclass
¶
Node covariates, treatment and outcome, plus the network that connects the nodes.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
X
|
array of shape (n_nodes, n_features)
|
Pre-treatment covariates. Must be numeric and finite. |
required |
T
|
array of shape (n_nodes,)
|
Treatment, binary (0/1) or continuous. |
required |
Y
|
array of shape (n_nodes,)
|
Outcome. |
required |
adjacency
|
sparse matrix of shape (n_nodes, n_nodes)
|
|
required |
feature_names
|
sequence of str
|
|
None
|
node_ids
|
array of shape (n_nodes,)
|
External identifiers such as Neo4j element ids. Defaults to |
None
|
directed
|
bool
|
|
False
|
Notes
Most users build this with :meth:from_edges, :meth:from_pandas or
:meth:from_networkx. Self-loops are dropped (a node does not "influence itself"
through the network; its own treatment enters the model directly).
n_edges
property
¶
Number of edges (unordered pairs for undirected graphs).
pattern
cached
property
¶
Binary adjacency: pattern[i, j] == 1 iff j is an in-neighbor of i.
degree
cached
property
¶
In-degree of every node (number of nodes that can influence it).
from_edges(X, T, Y, edges, *, weights=None, directed=False, feature_names=None, node_ids=None)
classmethod
¶
Build from an (n_edges, 2) array of (source, target) node positions.
source influences target. For undirected graphs the order is irrelevant.
Duplicate edges keep their first occurrence; self-loops are dropped.
from_pandas(nodes, edges, *, features, treatment, outcome, node_id=None, source='source', target='target', weight=None, directed=False)
classmethod
¶
Build from a node table and an edge table.
node_id names the id column of nodes (defaults to the index). edges
refers to nodes by those ids; source influences target.
from_networkx(graph, *, features, treatment, outcome, weight=None)
classmethod
¶
Build from a networkx graph whose nodes carry the given attributes.
For a DiGraph, an edge u -> v means u influences v.
with_values(*, T=None, Y=None, X=None, feature_names=None)
¶
Return a copy with the treatment, outcome and/or covariates replaced.
If X changes its number of columns and no feature_names are given, the
names are reset to X0, X1, ....
to_pandas()
¶
Return (nodes, edges) DataFrames. Edges have source influencing target.
Exposure maps¶
graphdml.SumExposure
¶
Bases: ExposureMap
Z_i = sum of in-neighbors' treatments (Z = A T).
graphdml.MeanExposure
¶
Bases: ExposureMap
Z_i = average of in-neighbors' treatments (0 for nodes without neighbors).
graphdml.WeightedExposure
¶
Bases: ExposureMap
Z_i = sum_j w_ij T_j using adjacency weights, optionally row-normalised.
graphdml.TwoHopExposure
¶
Bases: ExposureMap
Treatments of nodes at distance exactly two (summed or averaged).
Used by :func:graphdml.two_hop_test to check the 1-hop interference assumption.
graphdml.MatrixExposure
¶
Bases: ExposureMap
A user-supplied exposure operator E (sparse or dense (n, n)).
Features¶
graphdml.NeighborhoodFeatures
¶
Bases: BaseEstimator
Own covariates plus aggregated neighbor covariates over one or more hops.
Hop 1 applies each aggregation in aggs to in-neighbors' covariates. Each further
hop averages the previous hop's features over in-neighbors (mean propagation, as in
SGC/SIGN-style graph features), so aggs=("mean",), hops=2 yields
[X,  X, ² X] with  the row-normalised adjacency.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
aggs
|
tuple of {"mean", "sum", "max", "min"}
|
|
("mean", "max", "min")
|
hops
|
int
|
|
2
|
include_degree
|
bool
|
Append each node's in-degree as a feature. |
True
|
graphdml.OwnFeatures
¶
Bases: BaseEstimator
Only the node's own covariates (ignores the network; i.i.d. DML baseline).
graphdml.PrecomputedFeatures
¶
Bases: BaseEstimator
A fixed (n_nodes, n_features) matrix, e.g. graph embeddings computed elsewhere.
The features must be functions of covariates and graph structure only, never of treatments or outcomes.
Diagnostics and checks¶
graphdml.placebo_test(estimator, data, n_permutations=5, alpha=0.05, random_state=0)
¶
Refit with randomly permuted treatments; every effect should be ~0.
This checks the estimation pipeline (e.g. leakage, a broken featurizer), not the causal assumptions. Uses a Bonferroni correction across permutations and effects.
graphdml.negative_control_test(estimator, data, outcome, alpha=0.05)
¶
Refit with an outcome the treatment cannot have affected; every effect should be ~0.
Good negative controls share the confounders of the real outcome but are causally unaffected by treatment, e.g. the same outcome measured before treatment.
graphdml.two_hop_test(estimator, data, alpha=0.05)
¶
Test whether two-hop neighbors' treatments matter once one-hop exposure is modelled.
The refit uses a focal set that is independent at the two-hop scale (pairwise distance
= 5), so it has less power than the main analysis.
graphdml.selftest
¶
graphdml.selftest(): check on your own machine that the estimator finds real signals
and does not invent false ones.
It repeatedly simulates two worlds with the same strong network confounding, one with planted effects and one with none (the null twin), fits graphdml, and compares the estimates with the known truth. Methods that ignore the network are shown alongside, to show what the confounding would do to them.
SelfTestResult
dataclass
¶
Outcome of :func:selftest. passed is True when every check below holds.
selftest(n_reps=30, n=6000, *, direct_effect=1.0, peer_effect=0.6, estimator=None, alpha=0.05, n_jobs=-1, random_state=0)
¶
Run the signal check and report whether graphdml passed.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n_reps
|
int
|
Repetitions per world; each fits one model on a fresh |
30
|
n
|
int
|
Nodes per simulated graph. |
6000
|
direct_effect
|
float
|
The effects planted in the signal world (the null world has none). |
1.0
|
peer_effect
|
float
|
The effects planted in the signal world (the null world has none). |
1.0
|
estimator
|
GraphDML
|
An unfitted estimator to test (default: |
None
|
alpha
|
float
|
Significance level used for detection and false alarms. |
0.05
|
n_jobs
|
int
|
Parallel workers (joblib). |
-1
|
random_state
|
int
|
Seeds the repetitions. |
0
|
Returns:
| Type | Description |
|---|---|
SelfTestResult
|
|
Notes
The criteria are set from statistics, not tuned to results. The bands use 3 Monte
Carlo standard errors, so they loosen when n_reps is small.
- coverage: the share of 95% intervals containing the truth is at least
0.95 - 3 * SEin the planted world, for every effect. - power: the planted direct and peer effects are detected in at least 80% of runs, with the correct sign.
- false alarms: in the null world, the share of runs that claim an effect is at
most
alpha + 3 * SE, for every effect.
Datasets¶
graphdml.datasets.GraphDataset
dataclass
¶
A ready-to-fit :class:~graphdml.GraphData plus its ground truth and story.
Attributes:
| Name | Type | Description |
|---|---|---|
name |
str
|
|
data |
GraphData
|
|
truth |
dict
|
|
exposure |
str
|
The exposure map the data were generated with. |
treatment_name, outcome_name |
str
|
What |
DESCR |
str
|
The story card: what the variables are, what confounds, what naive methods miss. |
node_effects |
dict of arrays
|
Per-node effects ( |
extras |
dict
|
Anything else: oracle nuisances, negative-control outcomes, positions, latents. |
focal_truth(nodes)
¶
Average true effects over a subset of nodes (e.g. model.focal_nodes_).
Equals :attr:truth when effects are the same for every node.
graphdml.datasets.make_flu_town(n_residents=5000, *, avg_neighbors=6.0, confounding=1.0, direct_effect=-2.0, peer_effect=-0.5, random_state=0)
¶
Flu shots in a town: direct protection, herd protection and network confounding.
Treatment flu_shot (binary), outcome sick_days this season, exposure "sum"
(number of vaccinated neighbors). Health-conscious residents cluster together, and
neighbors' health-consciousness drives both vaccination and sickness. Naive regression
overstates how much the shot protects you. See DESCR for the full story.
graphdml.datasets.make_referral_app(n_users=8000, *, links_per_user=3, confounding=1.0, direct_effect=12.0, peer_effect=40.0, random_state=0)
¶
Promo coupons in a social app: targeting on friends' engagement, mean exposure.
Treatment coupon (binary), outcome monthly_spend in dollars, exposure
"mean" (share of friends who got a coupon): the peer effect of 40 means +$4 for
every additional 10% of friends with a coupon. The friendship graph has influencer hubs
(preferential attachment), and the targeting model favours users with engaged friends.
graphdml.datasets.make_classroom_tutoring(n_classes=200, class_size=25, *, friends_per_student=6.0, confounding=1.0, direct_effect=1.5, peer_effect=0.4, heterogeneous=False, random_state=0)
¶
Tutoring hours in classrooms: continuous treatment, communities, focal-set estimand.
Treatment tutoring_hours (continuous), outcome test_score, exposure "mean"
(friends' average tutoring hours). Friendships form mostly within classes, and students
with higher prior GPA have more friends. With heterogeneous=True tutoring helps
lower-GPA students more, so the focal set (which favours low-degree students) has a
larger average effect than the whole school: compare truth with
focal_truth(model.focal_nodes_).
graphdml.datasets.make_homophily_trap(n_people=5000, *, avg_friends=6.0, homophily=3.0, direct_effect=1.0, random_state=0)
¶
An online course and salary growth, where the true peer effect is zero.
Treatment took_course (binary), outcome salary_growth (%), exposure "mean".
An unobserved trait ("ambition") drives enrolment, salary growth and who befriends
whom. GraphDML reports a spurious peer effect; extras["negative_control"] (last
year's salary growth, which the course cannot affect) exposes the problem via
:func:graphdml.negative_control_test.
graphdml.datasets.make_signal_check(n=6000, *, direct_effect=1.0, peer_effect=0.6, confounding=1.0, avg_degree=4.0, noise_sd=2.0, random_state=0)
¶
Planted direct and peer effects, hidden behind strong network confounding.
Set direct_effect=0, peer_effect=0 for the null twin: the same data-generating
process with no true effect. Methods that ignore the network still "find" effects in
the null twin, because the confounding is strong; a correct method should not.
T is binary. Both treatment and outcome depend nonlinearly on a node's own
covariates and on its neighbors' (mean and maximum), with strength confounding.
The exposure is "mean": the share of treated neighbors.
graphdml.datasets.make_lastfm_promo(*, confounding=1.0, direct_effect=2.0, peer_effect=3.0, noise_sd=3.0, random_state=0, data_home=None)
¶
A concert promotion on the real LastFM Asia social network (semi-synthetic).
The network (7,624 users, 27,806 mutual follows) and covariates (activity, taste) are
real; the promotion promo and outcome listening_hours are simulated, so the true
effects are known. Exposure "mean": the share of friends who received the promo.
The targeting rule and the outcome both depend nonlinearly on friends' taste.
graphdml.datasets.load_insurance_experiment(data_dir)
¶
Load the insurance experiment as a :class:GraphDataset (no ground truth).
- Nodes: the 4,902 surveyed households. An edge
j -> imeansinamedjas a friend (jcan influencei). T= assigned to an intensive session (randomized);Y= bought insurance.- Covariates: household characteristics (median-imputed, with missing indicators), the randomized design variables (second round, information treatments), the number of friends named, and administrative-village dummies.
extras["exposure"]: the study's peer exposure, the share of named friends who attended a first-round intensive session (the denominator counts every named friend, surveyed or not, as in the study). It is linear inT, so it is given as a :class:~graphdml.MatrixExposure.extrasalso hassecond_round,info_none,natural_village(for clustered SEs), the study's own exposure variable for checking, and the published estimates.
Neo4j¶
graphdml.io.neo4j.Neo4jGraphSource
¶
Bases: _Client
Read a graph from Neo4j into :class:~graphdml.GraphData.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
driver
|
Driver
|
An existing driver; otherwise one is opened from |
None
|
database
|
str
|
|
None
|
load(node_label, rel_type, *, features, treatment, outcome, id_property=None, influence='undirected')
¶
Load all node_label nodes and the rel_type relationships among them.
influence says how a relationship (a)-[:REL]->(b) transmits effects:
"undirected" (both ways, e.g. FRIENDS_WITH), "source_to_target" (a
influences b) or "target_to_source" (b influences a, e.g. a
FOLLOWS b). Node ids are id_property values if given (recommended; they
should be unique), otherwise Neo4j element ids.
load_query(node_query, edge_query, *, features, treatment, outcome, influence='undirected', parameters=None)
¶
Load from your own Cypher (e.g. one village, or a computed property).
node_query must return a column id plus the named property columns;
edge_query must return source and target ids. Edges whose endpoints
are not returned by the node query are dropped.
graphdml.io.neo4j.Neo4jResultWriter
¶
Bases: _Client
Write a fitted :class:~graphdml.GraphDML back to Neo4j.
Each call creates one (:GDMLRun {name, ...estimates}) node and an
(:GDMLRun)-[:ESTIMATED_ON {fold, t_hat, y_hat, res_t, ..., res_y}]->(n)
relationship to every focal node, so runs can be compared, visualised (focal nodes are
exactly the ones with an ESTIMATED_ON edge) and deleted without touching your data.
write(model, *, run_name, node_label, id_property=None, description='', dry_run=False, batch_size=1000)
¶
Write the run. id_property must match what the data were loaded with.
Returns a summary dict. With dry_run=True nothing is written and the summary
includes the first rows that would be.
list_runs(name=None)
¶
All runs (or one by name), newest first.
delete_run(run_name)
¶
Delete a run node and its relationships. Returns the number of runs deleted.
graphdml.io.neo4j.GDSEmbeddings
¶
Bases: BaseEstimator
Featurizer: a base featurizer's output plus Neo4j GDS FastRP node embeddings.
FastRP embeddings summarise each node's position in the graph (structure only, never
treatment or outcome), so they can stand in for unobserved structural confounders such
as centrality or community. Rows are aligned to data.node_ids, which must be the
id_property values used when loading.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
source
|
Neo4jGraphSource
|
|
required |
node_label
|
the same graph that was loaded
|
|
required |
rel_type
|
the same graph that was loaded
|
|
required |
id_property
|
the same graph that was loaded
|
|
required |
dimension
|
embedding size
|
|
16
|
base
|
featurizer whose output is kept alongside the embeddings
|
(default :class: |
'default'
|
random_seed
|
FastRP seed (embeddings are deterministic given the seed)
|
|
42
|