diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 237fc87..46aab0d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,6 +18,19 @@ jobs: cache-dependency-glob: pyproject.toml - run: uvx ruff check . + docs: + # Catches broken links and API-docs errors before they reach the published site. + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: astral-sh/setup-uv@v10.2.0 + with: + python-version: "3.12" + activate-environment: true + cache-dependency-glob: pyproject.toml + - run: uv pip install -e ".[docs]" + - run: mkdocs build --strict + test: runs-on: ubuntu-latest strategy: diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..22f7bd8 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,50 @@ +name: Docs + +# Builds the documentation site and publishes it to GitHub Pages whenever main changes. + +on: + push: + branches: [main] + paths: + - "docs/**" + - "mkdocs.yml" + - "src/**" + - "CHANGELOG.md" + - "pyproject.toml" + - ".github/workflows/docs.yml" + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: astral-sh/setup-uv@v10.2.0 + with: + python-version: "3.12" + activate-environment: true + cache-dependency-glob: pyproject.toml + - run: uv pip install -e ".[docs]" + - run: mkdocs build --strict + - uses: actions/upload-pages-artifact@v5 + with: + path: site + + deploy: + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - id: deployment + uses: actions/deploy-pages@v5 diff --git a/.gitignore b/.gitignore index 3882666..a09584d 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ dist/ .ipynb_checkpoints/ benchmarks/results/*.log private/ +site/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 1133c7b..3d8cfed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,11 @@ All notable changes to graphdml are listed here. Versions follow ## [Unreleased] +## [0.2.0] - 2026-10-09 + ### Added +- Documentation site (https://samrdreyfuss.github.io/graphdml/): guides, methodology, + validation, example datasets and an API reference generated from the docstrings. - `graphdml.selftest()` and `make_signal_check()`: a built-in check, on synthetic data with planted effects and a null twin that has none, that the estimator finds real signals and does not invent false ones. Reports bias, coverage, power and false-alarm rates against @@ -53,5 +57,6 @@ First release. - Neo4j integration: load graphs, write results back, GDS FastRP features, demo. - Validation suite and benchmarks. -[Unreleased]: https://github.com/Samrdreyfuss/graphdml/compare/v0.1.0...HEAD +[Unreleased]: https://github.com/Samrdreyfuss/graphdml/compare/v0.2.0...HEAD +[0.2.0]: https://github.com/Samrdreyfuss/graphdml/compare/v0.1.0...v0.2.0 [0.1.0]: https://github.com/Samrdreyfuss/graphdml/releases/tag/v0.1.0 diff --git a/CITATION.cff b/CITATION.cff index 3583a1a..ba74368 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -6,7 +6,7 @@ authors: - family-names: Dreyfuss given-names: Sam license: MIT -version: 0.1.0 +version: 0.2.0 repository-code: "https://github.com/Samrdreyfuss/graphdml" references: - type: conference-paper diff --git a/README.md b/README.md index 45e82fa..cdbf8e2 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,8 @@ **Causal effects on networks, with honest confidence intervals.** +**[Documentation](https://samrdreyfuss.github.io/graphdml/)** · [PyPI](https://pypi.org/project/graphdml/) · [Changelog](https://github.com/Samrdreyfuss/graphdml/blob/main/CHANGELOG.md) + When a treatment reaches one person, it often reaches their friends too: a vaccine protects the neighbors, a coupon gets shared, a training spreads by word of mouth. graphdml estimates: diff --git a/benchmarks/report.py b/benchmarks/report.py index 0a37370..3529a85 100644 --- a/benchmarks/report.py +++ b/benchmarks/report.py @@ -112,7 +112,7 @@ def lastfm_section() -> str: "# Validation results\n\n" f"Generated by `benchmarks/report.py` on {dt.date.today()} for graphdml " f"{graphdml.__version__}. The protocol and pass criteria are in " - "[the validation protocol](../docs/validation.md); raw per-seed results are in " + "[the validation protocol](validation.md); raw per-seed results are in " "`benchmarks/results/*.csv`.\n" ) DOC.write_text("\n\n".join([header, coverage_section(), design_section(), diff --git a/docs/api.md b/docs/api.md new file mode 100644 index 0000000..ff4a269 --- /dev/null +++ b/docs/api.md @@ -0,0 +1,65 @@ +# API reference + +## Estimator + +::: graphdml.GraphDML + +## Data + +::: graphdml.GraphData + +## Exposure maps + +::: graphdml.SumExposure + +::: graphdml.MeanExposure + +::: graphdml.WeightedExposure + +::: graphdml.TwoHopExposure + +::: graphdml.MatrixExposure + +## Features + +::: graphdml.NeighborhoodFeatures + +::: graphdml.OwnFeatures + +::: graphdml.PrecomputedFeatures + +## Diagnostics and checks + +::: graphdml.placebo_test + +::: graphdml.negative_control_test + +::: graphdml.two_hop_test + +::: graphdml.selftest + +## Datasets + +::: graphdml.datasets.GraphDataset + +::: graphdml.datasets.make_flu_town + +::: graphdml.datasets.make_referral_app + +::: graphdml.datasets.make_classroom_tutoring + +::: graphdml.datasets.make_homophily_trap + +::: graphdml.datasets.make_signal_check + +::: graphdml.datasets.make_lastfm_promo + +::: graphdml.datasets.load_insurance_experiment + +## Neo4j + +::: graphdml.io.neo4j.Neo4jGraphSource + +::: graphdml.io.neo4j.Neo4jResultWriter + +::: graphdml.io.neo4j.GDSEmbeddings diff --git a/docs/changelog.md b/docs/changelog.md new file mode 100644 index 0000000..786b75d --- /dev/null +++ b/docs/changelog.md @@ -0,0 +1 @@ +--8<-- "CHANGELOG.md" diff --git a/docs/datasets.md b/docs/datasets.md new file mode 100644 index 0000000..92e5e29 --- /dev/null +++ b/docs/datasets.md @@ -0,0 +1,36 @@ +# Example datasets +Every dataset is a `GraphDataset` with the data, the true effects (where known) and a story +card: `print(ds.DESCR)`. Five generate their data offline; `make_lastfm_promo` downloads a +public network on first use; `load_insurance_experiment` needs a free download. + +--- + +--8<-- "src/graphdml/datasets/descr/flu_town.md" + +--- + +--8<-- "src/graphdml/datasets/descr/referral_app.md" + +--- + +--8<-- "src/graphdml/datasets/descr/classroom_tutoring.md" + +--- + +--8<-- "src/graphdml/datasets/descr/homophily_trap.md" + +--- + +--8<-- "src/graphdml/datasets/descr/signal_check.md" + +--- + +--8<-- "src/graphdml/datasets/descr/lastfm_promo.md" + +--- + +--8<-- "src/graphdml/datasets/descr/insurance_experiment.md" + +--- + +--8<-- "src/graphdml/datasets/descr/toy_graph.md" diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..e898621 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,64 @@ +# graphdml + +**Causal effects on networks, with honest confidence intervals.** + +When a treatment reaches one person, it often reaches their friends too: a vaccine protects +the neighbors, a coupon gets shared, a training spreads by word of mouth. graphdml estimates + +* the **ATE**: the average effect of treating everyone vs no one, including the ripple +* the **direct effect** (ADE): what the treatment does for the person who receives it +* the **peer effect** (APE): what it does for the people connected to them + +using double machine learning with any scikit-learn model, adjusting for confounding that +runs through the network, with confidence intervals that account for neighbors being +dependent. Graphs can come from pandas, networkx or Neo4j. + +## Install + +```bash +pip install graphdml # core +pip install "graphdml[neo4j]" # with Neo4j support +``` + +Requires Python 3.10 or newer. + +## Quickstart + +```python +from graphdml import GraphDML +from graphdml.datasets import make_flu_town + +ds = make_flu_town() # 5,000 residents; true effects: -2.0 and -0.5 sick days +model = GraphDML(exposure="sum", random_state=0).fit(ds.data) +print(model.summary()) +``` + +## Where to go next + +| | | +|---|---| +| [Example datasets](datasets.md) | Eight datasets with known truth, and the story behind each | +| [Methodology](methodology.md) | Exactly what `fit` computes, and why | +| [Design choices](design.md) | Each default, with the evidence behind it | +| [Validation](validation.md) | The protocol, and the [results](validation-results.md) | +| [Neo4j integration](neo4j.md) | Load graphs, write results back, GDS embeddings | +| [API reference](api.md) | Every public class and function | + +## Does it find real signals? + +```python +import graphdml +print(graphdml.selftest()) +``` + +Simulates a world with planted effects and a null twin with none, and checks that graphdml +finds the first and stays quiet on the second, while a method that ignores the network +does not. See the [README](https://github.com/Samrdreyfuss/graphdml#does-it-find-real-signals-run-the-self-test) +for sample output. + +## Citing + +The estimator builds on Khatami, Parikh, Chen, Roy and Salimi, +[*Graph Machine Learning based Doubly Robust Estimator for Network Causal Effects*](https://proceedings.mlr.press/v258/khatami25a.html) +(AISTATS 2025). Please cite their paper along with graphdml; see +[CITATION.cff](https://github.com/Samrdreyfuss/graphdml/blob/main/CITATION.cff). diff --git a/docs/javascripts/mathjax.js b/docs/javascripts/mathjax.js new file mode 100644 index 0000000..e9656e9 --- /dev/null +++ b/docs/javascripts/mathjax.js @@ -0,0 +1,9 @@ +window.MathJax = { + tex: { + inlineMath: [["\\(", "\\)"]], + displayMath: [["\\[", "\\]"]], + processEscapes: true, + processEnvironments: true, + }, + options: { ignoreHtmlClass: ".*|", processHtmlClass: "arithmatex" }, +}; diff --git a/docs/validation-results.md b/docs/validation-results.md index 80ed112..4d07866 100644 --- a/docs/validation-results.md +++ b/docs/validation-results.md @@ -1,6 +1,6 @@ # Validation results -Generated by `benchmarks/report.py` on 2026-10-06 for graphdml 0.1.0. The protocol and pass criteria are in [the validation protocol](../docs/validation.md); raw per-seed results are in `benchmarks/results/*.csv`. +Generated by `benchmarks/report.py` on 2026-10-09 for graphdml 0.1.0. The protocol and pass criteria are in [the validation protocol](validation.md); raw per-seed results are in `benchmarks/results/*.csv`. ## Coverage diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..fc78036 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,63 @@ +site_name: graphdml +site_description: Causal effects on networks with double machine learning +site_url: https://samrdreyfuss.github.io/graphdml/ +repo_url: https://github.com/Samrdreyfuss/graphdml +repo_name: Samrdreyfuss/graphdml +edit_uri: edit/main/docs/ + +theme: + name: material + features: + - navigation.sections + - navigation.top + - content.code.copy + - search.suggest + palette: + - media: "(prefers-color-scheme: light)" + scheme: default + toggle: {icon: material/weather-night, name: Switch to dark mode} + - media: "(prefers-color-scheme: dark)" + scheme: slate + toggle: {icon: material/weather-sunny, name: Switch to light mode} + +nav: + - Home: index.md + - Guides: + - Example datasets: datasets.md + - Neo4j integration: neo4j.md + - Method: + - Methodology: methodology.md + - Design choices: design.md + - Validation: + - Protocol: validation.md + - Results: validation-results.md + - API reference: api.md + - Changelog: changelog.md + +markdown_extensions: + - admonition + - tables + - toc: + permalink: true + - pymdownx.arithmatex: + generic: true + - pymdownx.highlight + - pymdownx.superfences + - pymdownx.snippets: + check_paths: true + +extra_javascript: + - javascripts/mathjax.js + - https://unpkg.com/mathjax@3/es5/tex-mml-chtml.js + +plugins: + - search + - mkdocstrings: + handlers: + python: + options: + docstring_style: numpy + show_source: false + show_root_heading: true + heading_level: 3 + members_order: source diff --git a/pyproject.toml b/pyproject.toml index 7404f5a..a0bc7e1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "graphdml" -version = "0.1.0" +version = "0.2.0" description = "Causal effects on networks with double machine learning: ATE, direct and peer effects with confidence intervals that account for network dependence." readme = "README.md" license = "MIT" @@ -43,6 +43,7 @@ dependencies = [ [project.optional-dependencies] neo4j = ["neo4j>=5.14"] +docs = ["mkdocs-material>=9.5", "mkdocstrings[python]>=0.25"] validation = ["statsmodels>=0.14", "doubleml>=0.9", "joblib>=1.3", "tabulate>=0.9"] dev = [ "graphdml[neo4j]", @@ -54,7 +55,7 @@ dev = [ [project.urls] Homepage = "https://github.com/Samrdreyfuss/graphdml" -Documentation = "https://github.com/Samrdreyfuss/graphdml/tree/main/docs" +Documentation = "https://samrdreyfuss.github.io/graphdml/" Source = "https://github.com/Samrdreyfuss/graphdml" Issues = "https://github.com/Samrdreyfuss/graphdml/issues" Changelog = "https://github.com/Samrdreyfuss/graphdml/blob/main/CHANGELOG.md" diff --git a/src/graphdml/__init__.py b/src/graphdml/__init__.py index 6cc061e..bdaf948 100644 --- a/src/graphdml/__init__.py +++ b/src/graphdml/__init__.py @@ -20,7 +20,7 @@ from graphdml.features import NeighborhoodFeatures, OwnFeatures, PrecomputedFeatures from graphdml.selftest import selftest -__version__ = "0.1.0" +__version__ = "0.2.0" __all__ = [ "ExposureMap", diff --git a/src/graphdml/selftest.py b/src/graphdml/selftest.py index bfa70e7..0bb19a3 100644 --- a/src/graphdml/selftest.py +++ b/src/graphdml/selftest.py @@ -87,17 +87,36 @@ def selftest( Parameters ---------- - n_reps : repetitions per world; each fits one model on a fresh ``n``-node graph. - estimator : an unfitted :class:`GraphDML` to test (default: ``GraphDML(exposure="mean")`` - with the library's default learners). Pass your own to test your settings. - - Checks (set from statistics, not tuned to results; the bands use 3 Monte Carlo - standard errors, so they loosen when ``n_reps`` is small): + n_reps : int, default=30 + Repetitions per world; each fits one model on a fresh ``n``-node graph. + n : int, default=6000 + Nodes per simulated graph. + direct_effect, peer_effect : float + The effects planted in the signal world (the null world has none). + estimator : GraphDML, optional + An unfitted estimator to test (default: ``GraphDML(exposure="mean")`` with the + library's default learners). Pass your own to test your settings. + alpha : float, default=0.05 + Significance level used for detection and false alarms. + n_jobs : int, default=-1 + Parallel workers (joblib). + random_state : int, default=0 + Seeds the repetitions. + + Returns + ------- + SelfTestResult + ``passed``, a results ``table`` and the individual ``checks``. + + 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 * SE`` in the planted world, for every effect; + ``0.95 - 3 * SE`` in 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; + 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. """