heterosplit 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. heterosplit-0.1.0/.github/workflows/ci.yml +90 -0
  2. heterosplit-0.1.0/.github/workflows/release.yml +63 -0
  3. heterosplit-0.1.0/.gitignore +40 -0
  4. heterosplit-0.1.0/.python-version +1 -0
  5. heterosplit-0.1.0/CHANGELOG.md +43 -0
  6. heterosplit-0.1.0/LICENSE +21 -0
  7. heterosplit-0.1.0/PKG-INFO +187 -0
  8. heterosplit-0.1.0/README.md +158 -0
  9. heterosplit-0.1.0/RELEASING.md +39 -0
  10. heterosplit-0.1.0/benchmarks/benchmark.py +159 -0
  11. heterosplit-0.1.0/docs/architecture.md +109 -0
  12. heterosplit-0.1.0/docs/benchmarks.md +104 -0
  13. heterosplit-0.1.0/examples/corrupted_leakage.py +52 -0
  14. heterosplit-0.1.0/examples/drugcomb.py +83 -0
  15. heterosplit-0.1.0/examples/quickstart.py +68 -0
  16. heterosplit-0.1.0/examples/recommendation.py +47 -0
  17. heterosplit-0.1.0/examples/train_link_prediction.py +257 -0
  18. heterosplit-0.1.0/pyproject.toml +110 -0
  19. heterosplit-0.1.0/src/heterosplit/__init__.py +58 -0
  20. heterosplit-0.1.0/src/heterosplit/adapters/__init__.py +12 -0
  21. heterosplit-0.1.0/src/heterosplit/adapters/pyg.py +170 -0
  22. heterosplit-0.1.0/src/heterosplit/adapters/tabular.py +48 -0
  23. heterosplit-0.1.0/src/heterosplit/api.py +21 -0
  24. heterosplit-0.1.0/src/heterosplit/audit/__init__.py +58 -0
  25. heterosplit-0.1.0/src/heterosplit/audit/_common.py +49 -0
  26. heterosplit-0.1.0/src/heterosplit/audit/contract.py +56 -0
  27. heterosplit-0.1.0/src/heterosplit/audit/feature_provenance.py +65 -0
  28. heterosplit-0.1.0/src/heterosplit/audit/message_passing.py +79 -0
  29. heterosplit-0.1.0/src/heterosplit/audit/negative_samples.py +74 -0
  30. heterosplit-0.1.0/src/heterosplit/audit/overlap.py +239 -0
  31. heterosplit-0.1.0/src/heterosplit/audit/report.py +107 -0
  32. heterosplit-0.1.0/src/heterosplit/canonical.py +78 -0
  33. heterosplit-0.1.0/src/heterosplit/cli.py +119 -0
  34. heterosplit-0.1.0/src/heterosplit/datasets/__init__.py +22 -0
  35. heterosplit-0.1.0/src/heterosplit/datasets/drugcomb.py +187 -0
  36. heterosplit-0.1.0/src/heterosplit/errors.py +40 -0
  37. heterosplit-0.1.0/src/heterosplit/manifest.py +215 -0
  38. heterosplit-0.1.0/src/heterosplit/message_passing.py +126 -0
  39. heterosplit-0.1.0/src/heterosplit/objective.py +71 -0
  40. heterosplit-0.1.0/src/heterosplit/py.typed +0 -0
  41. heterosplit-0.1.0/src/heterosplit/records.py +218 -0
  42. heterosplit-0.1.0/src/heterosplit/report/__init__.py +7 -0
  43. heterosplit-0.1.0/src/heterosplit/report/distributions.py +64 -0
  44. heterosplit-0.1.0/src/heterosplit/report/summary.py +110 -0
  45. heterosplit-0.1.0/src/heterosplit/result.py +146 -0
  46. heterosplit-0.1.0/src/heterosplit/schema.py +229 -0
  47. heterosplit-0.1.0/src/heterosplit/spec.py +268 -0
  48. heterosplit-0.1.0/src/heterosplit/splitters/__init__.py +48 -0
  49. heterosplit-0.1.0/src/heterosplit/splitters/assignment.py +175 -0
  50. heterosplit-0.1.0/src/heterosplit/splitters/base.py +184 -0
  51. heterosplit-0.1.0/src/heterosplit/splitters/context_disjoint.py +31 -0
  52. heterosplit-0.1.0/src/heterosplit/splitters/entity_disjoint.py +128 -0
  53. heterosplit-0.1.0/src/heterosplit/splitters/joint.py +121 -0
  54. heterosplit-0.1.0/src/heterosplit/splitters/pair.py +34 -0
  55. heterosplit-0.1.0/src/heterosplit/splitters/random.py +45 -0
  56. heterosplit-0.1.0/src/heterosplit/synthetic.py +127 -0
  57. heterosplit-0.1.0/tests/test_adapter_pyg.py +101 -0
  58. heterosplit-0.1.0/tests/test_adapter_tabular.py +54 -0
  59. heterosplit-0.1.0/tests/test_assignment.py +53 -0
  60. heterosplit-0.1.0/tests/test_audit.py +198 -0
  61. heterosplit-0.1.0/tests/test_audit_injection.py +63 -0
  62. heterosplit-0.1.0/tests/test_benchmark.py +31 -0
  63. heterosplit-0.1.0/tests/test_canonical.py +84 -0
  64. heterosplit-0.1.0/tests/test_cli.py +85 -0
  65. heterosplit-0.1.0/tests/test_drugcomb.py +110 -0
  66. heterosplit-0.1.0/tests/test_manifest.py +97 -0
  67. heterosplit-0.1.0/tests/test_message_passing.py +90 -0
  68. heterosplit-0.1.0/tests/test_objective.py +51 -0
  69. heterosplit-0.1.0/tests/test_properties.py +132 -0
  70. heterosplit-0.1.0/tests/test_records.py +111 -0
  71. heterosplit-0.1.0/tests/test_refine.py +67 -0
  72. heterosplit-0.1.0/tests/test_report.py +50 -0
  73. heterosplit-0.1.0/tests/test_review_fixes.py +140 -0
  74. heterosplit-0.1.0/tests/test_schema.py +111 -0
  75. heterosplit-0.1.0/tests/test_spec.py +140 -0
  76. heterosplit-0.1.0/tests/test_split_context.py +62 -0
  77. heterosplit-0.1.0/tests/test_split_entity.py +122 -0
  78. heterosplit-0.1.0/tests/test_split_joint.py +78 -0
  79. heterosplit-0.1.0/tests/test_split_pair.py +67 -0
  80. heterosplit-0.1.0/tests/test_split_random.py +90 -0
  81. heterosplit-0.1.0/tests/test_synthetic.py +88 -0
  82. heterosplit-0.1.0/tests/test_train_example.py +33 -0
  83. heterosplit-0.1.0/uv.lock +2585 -0
@@ -0,0 +1,90 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ concurrency:
9
+ group: ci-${{ github.ref }}
10
+ cancel-in-progress: true
11
+
12
+ jobs:
13
+ lint:
14
+ name: lint & type-check
15
+ runs-on: ubuntu-latest
16
+ steps:
17
+ - uses: actions/checkout@v5
18
+
19
+ - name: Install uv
20
+ uses: astral-sh/setup-uv@v6
21
+ with:
22
+ enable-cache: true
23
+
24
+ - name: Install Python 3.12
25
+ run: uv python install 3.12
26
+
27
+ - name: Sync (core + dev toolchain)
28
+ run: uv sync --python 3.12
29
+
30
+ - name: Lint (ruff)
31
+ run: |
32
+ uv run ruff check .
33
+ uv run ruff format --check .
34
+
35
+ - name: Type check (mypy)
36
+ run: uv run mypy
37
+
38
+ test:
39
+ name: test (py${{ matrix.python-version }})
40
+ runs-on: ubuntu-latest
41
+ strategy:
42
+ fail-fast: false
43
+ matrix:
44
+ python-version: ["3.10", "3.11", "3.12", "3.13"]
45
+ steps:
46
+ - uses: actions/checkout@v5
47
+
48
+ - name: Install uv
49
+ uses: astral-sh/setup-uv@v6
50
+ with:
51
+ enable-cache: true
52
+
53
+ - name: Install Python ${{ matrix.python-version }}
54
+ run: uv python install ${{ matrix.python-version }}
55
+
56
+ - name: Sync (core + dev toolchain)
57
+ run: uv sync --python ${{ matrix.python-version }}
58
+
59
+ - name: Test
60
+ run: uv run pytest
61
+
62
+ coverage:
63
+ name: coverage (with pyg extra)
64
+ runs-on: ubuntu-latest
65
+ steps:
66
+ - uses: actions/checkout@v5
67
+
68
+ - name: Install uv
69
+ uses: astral-sh/setup-uv@v6
70
+ with:
71
+ enable-cache: true
72
+
73
+ - name: Install Python 3.12
74
+ run: uv python install 3.12
75
+
76
+ # Install the [pyg] extra so the PyG adapter + training example run and are
77
+ # measured — the coverage gate then reflects the whole library, not just the core.
78
+ - name: Sync (core + dev + pyg)
79
+ run: uv sync --python 3.12 --extra pyg
80
+
81
+ - name: Test with coverage (fail under 90%)
82
+ run: >
83
+ uv run --extra pyg pytest
84
+ --cov --cov-report=term-missing --cov-report=xml --cov-fail-under=90
85
+
86
+ - name: Upload coverage
87
+ uses: actions/upload-artifact@v5
88
+ with:
89
+ name: coverage-xml
90
+ path: coverage.xml
@@ -0,0 +1,63 @@
1
+ name: Release
2
+
3
+ # Publishes to PyPI when a version tag is pushed (e.g. `git tag v0.1.0 && git push --tags`).
4
+ #
5
+ # One-time setup on PyPI (trusted publishing, no API token needed): create the project's
6
+ # "trusted publisher" with owner=ZubairQazi, repo=heterosplit, workflow=release.yml,
7
+ # environment=pypi. See https://docs.pypi.org/trusted-publishers/. Test first against
8
+ # TestPyPI via the manual "Run workflow" button (target: testpypi).
9
+
10
+ on:
11
+ push:
12
+ tags: ["v*"]
13
+ workflow_dispatch:
14
+ inputs:
15
+ target:
16
+ description: "Publish target"
17
+ default: testpypi
18
+ type: choice
19
+ options: [testpypi, pypi]
20
+
21
+ permissions:
22
+ contents: read
23
+
24
+ jobs:
25
+ build:
26
+ runs-on: ubuntu-latest
27
+ steps:
28
+ - uses: actions/checkout@v5
29
+ - uses: astral-sh/setup-uv@v6
30
+ - name: Build sdist + wheel
31
+ run: uv build
32
+ - name: Check that the tag matches the package version
33
+ if: startsWith(github.ref, 'refs/tags/v')
34
+ run: |
35
+ version="v$(uv run python -c 'import heterosplit; print(heterosplit.__version__)')"
36
+ if [ "$version" != "${GITHUB_REF_NAME}" ]; then
37
+ echo "tag ${GITHUB_REF_NAME} != package version ${version}" >&2
38
+ exit 1
39
+ fi
40
+ - uses: actions/upload-artifact@v5
41
+ with:
42
+ name: dist
43
+ path: dist/
44
+
45
+ publish:
46
+ needs: build
47
+ runs-on: ubuntu-latest
48
+ environment: ${{ github.event.inputs.target || 'pypi' }}
49
+ permissions:
50
+ id-token: write # OIDC trusted publishing
51
+ steps:
52
+ - uses: actions/download-artifact@v5
53
+ with:
54
+ name: dist
55
+ path: dist/
56
+ - name: Publish to TestPyPI
57
+ if: github.event.inputs.target == 'testpypi'
58
+ uses: pypa/gh-action-pypi-publish@release/v1
59
+ with:
60
+ repository-url: https://test.pypi.org/legacy/
61
+ - name: Publish to PyPI
62
+ if: github.event.inputs.target != 'testpypi'
63
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,40 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # Distribution / packaging
7
+ build/
8
+ dist/
9
+ *.egg-info/
10
+ .eggs/
11
+
12
+ # Virtual environments
13
+ .venv/
14
+ venv/
15
+ env/
16
+
17
+ # uv
18
+ # (uv.lock IS committed; the cache dir is not)
19
+ .uv/
20
+
21
+ # Test / coverage / typing caches
22
+ .pytest_cache/
23
+ .mypy_cache/
24
+ .ruff_cache/
25
+ .hypothesis/
26
+ .coverage
27
+ .coverage.*
28
+ coverage.xml
29
+ htmlcov/
30
+
31
+ # Benchmark / example artifacts
32
+ *.manifest.json
33
+ benchmarks/results/
34
+ examples/output/
35
+
36
+ # OS / editor cruft
37
+ .DS_Store
38
+ *.swp
39
+ .idea/
40
+ .vscode/
@@ -0,0 +1 @@
1
+ 3.12
@@ -0,0 +1,43 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. The format is based on
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to
5
+ [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [0.1.0] - 2026-09-12
10
+
11
+ First public release: a feature-complete v1 of the correctness core, auditor, and
12
+ integrations.
13
+
14
+ ### Added
15
+
16
+ - **Split regimes (8):** random/transductive, pair, source, destination, either, both,
17
+ context, and joint cold-start, over a prediction-record model with per-entity-type
18
+ shared codebooks.
19
+ - **Leakage audit suite:** regime-aware contract checks — entity/pair/context overlap
20
+ across all splits (incl. val-vs-test), reversed-pair awareness, message-passing
21
+ reconstruction leakage, duplicate observations, and optional negative-sample and
22
+ feature-provenance auditors; `raise_for_leakage()` fails on any violation.
23
+ - **Deterministic manifests:** collision-resistant input fingerprint, normalized spec,
24
+ per-split counts, and index hashes; `digest()` reproducibility key with measurements
25
+ kept separate. Reloadable without re-running.
26
+ - **Constrained assignment:** seeded longest-processing-time greedy plus a
27
+ size-preserving local-search refinement (capped for scale).
28
+ - **Message passing:** leakage-safe training-graph reconstruction (held-out edges and,
29
+ for self-relations, their reverses removed).
30
+ - **Adapters:** PyTorch Geometric `HeteroData` (optional `[pyg]` extra) and a
31
+ dependency-free tabular adapter.
32
+ - **Datasets:** real DrugComb drug--drug--cell-line loader (streaming CSV + Zenodo
33
+ downloader).
34
+ - **Reporting & CLI:** JSON/Markdown distribution + audit report; `heterosplit demo` /
35
+ `heterosplit split`.
36
+ - **Examples & benchmarks:** synthetic generator, corrupted-leakage demo, benchmark
37
+ harness with a group-shuffle baseline, and an end-to-end GraphSAGE link-prediction
38
+ example showing the regime changes measured performance.
39
+ - **Docs:** architecture and benchmark methodology; property-based and adversarial-review
40
+ test coverage (~94%).
41
+
42
+ [Unreleased]: https://github.com/ZubairQazi/heterosplit/compare/v0.1.0...HEAD
43
+ [0.1.0]: https://github.com/ZubairQazi/heterosplit/releases/tag/v0.1.0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Zubair Qazi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,187 @@
1
+ Metadata-Version: 2.5
2
+ Name: heterosplit
3
+ Version: 0.1.0
4
+ Summary: Leakage-safe entity-disjoint splits and audits for heterogeneous link-prediction datasets.
5
+ Project-URL: Homepage, https://github.com/ZubairQazi/heterosplit
6
+ Project-URL: Repository, https://github.com/ZubairQazi/heterosplit
7
+ Project-URL: Issues, https://github.com/ZubairQazi/heterosplit/issues
8
+ Author: Zubair Qazi
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: cold-start,data-splitting,gnn,graph,leakage,link-prediction,pytorch-geometric
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
22
+ Classifier: Typing :: Typed
23
+ Requires-Python: >=3.10
24
+ Requires-Dist: numpy>=1.24
25
+ Provides-Extra: pyg
26
+ Requires-Dist: torch-geometric>=2.5; extra == 'pyg'
27
+ Requires-Dist: torch>=2.1; extra == 'pyg'
28
+ Description-Content-Type: text/markdown
29
+
30
+ # HeteroSplit
31
+
32
+ **Leakage-safe entity-disjoint splits and audits for heterogeneous link-prediction datasets.**
33
+
34
+ [![CI](https://github.com/ZubairQazi/heterosplit/actions/workflows/ci.yml/badge.svg)](https://github.com/ZubairQazi/heterosplit/actions/workflows/ci.yml)
35
+ ![coverage](https://img.shields.io/badge/coverage-94%25-brightgreen)
36
+ ![python](https://img.shields.io/badge/python-3.10--3.13-blue)
37
+ [![license: MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE)
38
+
39
+ HeteroSplit constructs, validates, and *reports* cold-start / inductive splits for
40
+ heterogeneous link-prediction tasks (drug–drug–cell-line synergy, drug–target
41
+ interaction, recommendation, knowledge graphs). It is designed to **complement**
42
+ PyTorch Geometric — it focuses narrowly on split *semantics* and leakage *auditing*,
43
+ and does not replace PyG's loaders, samplers, or training stack.
44
+
45
+ > **Status:** early release (`0.1.x`). APIs may change. This is positioned as a
46
+ > reusable implementation of entity-disjoint split policies and leakage audits for
47
+ > PyG `HeteroData` link-prediction tasks — not (yet) a research novelty claim.
48
+
49
+ ## Why
50
+
51
+ Graph libraries make *random* node/edge splitting easy, but cold-start experiments
52
+ often rely on one-off scripts with ambiguous semantics. A nominally "cold" test set
53
+ can still leak information through:
54
+
55
+ - the same entity appearing in training under another edge or relation;
56
+ - reverse edges left in the message-passing graph;
57
+ - an unordered pair reversed across splits;
58
+ - context entities (e.g. cell lines) crossing a supposedly disjoint boundary;
59
+ - features / precomputed embeddings fit using held-out entities;
60
+ - negative sampling that pulls entities or pairs from the wrong regime.
61
+
62
+ HeteroSplit makes the split *contract* explicit, enforces it, and produces evidence
63
+ (a serializable **manifest** + **audit report**) that the contract holds.
64
+
65
+ **Why it matters (real DrugComb):** a small GraphSAGE link predictor scores **AUC 0.84**
66
+ under a random split but **0.27** on genuinely unseen drugs (either-cold-start) — random
67
+ splitting massively overestimates cold-start performance. See
68
+ [benchmarks](docs/benchmarks.md#real-data-drugcomb).
69
+
70
+ ## Install
71
+
72
+ The correctness core is pure Python + NumPy:
73
+
74
+ ```bash
75
+ pip install heterosplit
76
+ ```
77
+
78
+ PyTorch Geometric integration is an optional extra:
79
+
80
+ ```bash
81
+ pip install "heterosplit[pyg]"
82
+ ```
83
+
84
+ ### From source (development)
85
+
86
+ This project uses [`uv`](https://docs.astral.sh/uv/):
87
+
88
+ ```bash
89
+ git clone https://github.com/ZubairQazi/heterosplit
90
+ cd heterosplit
91
+ uv sync # creates .venv and installs the dev toolchain
92
+ uv run pytest # run the test suite
93
+ uv run --extra pyg pytest # include the PyG adapter tests
94
+ ```
95
+
96
+ ## Quickstart
97
+
98
+ ```python
99
+ from heterosplit import make_synthetic_dataset, split_records
100
+
101
+ # A DrugComb-shaped dataset: (drug, drug, cell-line) synergy observations.
102
+ # Swap in your own PredictionRecords / HeteroData for real data.
103
+ data = make_synthetic_dataset(
104
+ n_records=5000,
105
+ n_source_entities=120,
106
+ n_context_entities=30,
107
+ n_labels=2,
108
+ source_type="drug",
109
+ context_type="cell_line",
110
+ relation="synergy",
111
+ seed=42,
112
+ )
113
+
114
+ # Joint cold-start: a test triple must involve an unseen drug AND an unseen cell line.
115
+ spec = data.spec(
116
+ "joint_cold_start",
117
+ holdout={"drug": "either", "cell_line": "all"},
118
+ ratios=(0.8, 0.1, 0.1),
119
+ stratify_by="label",
120
+ seed=42,
121
+ )
122
+
123
+ result = split_records(data.records, spec)
124
+ result.audit.raise_for_leakage() # fails loudly on any leakage
125
+ result.manifest.save("split-manifest.json") # deterministic, reloadable
126
+
127
+ train_edges = result.message_passing_edge_index() # leakage-safe training graph
128
+ ```
129
+
130
+ Runnable versions live in [`examples/`](examples/) (`quickstart.py`,
131
+ `recommendation.py`, `drugcomb.py`, `corrupted_leakage.py`), or try the CLI:
132
+
133
+ ```bash
134
+ uv run heterosplit demo --regime joint_cold_start
135
+ uv run heterosplit split --input data.csv --spec spec.json --out-dir out/
136
+ ```
137
+
138
+ ### Real data: DrugComb
139
+
140
+ The [DrugComb](https://drugcomb.org) drug–drug–cell-line synergy corpus is wired up
141
+ directly (CC-BY-4.0):
142
+
143
+ ```python
144
+ from heterosplit.datasets.drugcomb import download_drugcomb_summary, load_drugcomb_csv
145
+
146
+ download_drugcomb_summary("drugcomb.csv") # ~1.4 GB, once
147
+ records = load_drugcomb_csv("drugcomb.csv", max_rows=200_000) # streams; filters mono-therapy rows
148
+ ```
149
+
150
+ `records_from_drugcomb(frame, ...)` maps an in-memory DrugComb frame (dict or DataFrame)
151
+ if you already have one.
152
+
153
+ ## Documentation
154
+
155
+ - [Architecture / design](docs/architecture.md)
156
+ - [Benchmarks & methodology](docs/benchmarks.md)
157
+
158
+ ## Split taxonomy (v1 target)
159
+
160
+ | Regime | Test-set contract |
161
+ |---|---|
162
+ | Random / transductive | Test edges are unseen; entities may have appeared in training. |
163
+ | Pair cold-start | The `(s, d)` pair is unseen, but each entity may appear separately. |
164
+ | Source cold-start | Test source entities never appear as training sources. |
165
+ | Destination cold-start | Test destination entities never appear as training destinations. |
166
+ | Either-entity cold-start | At least one endpoint of each test edge is unseen. |
167
+ | Both-entity cold-start | Both endpoints of every test edge are unseen. |
168
+ | Context cold-start | Test context entities never occur in training. |
169
+ | Joint cold-start | A configured combination of endpoint and context disjointness. |
170
+
171
+ ## Leakage audit & manifests
172
+
173
+ Every split ships with an **audit** and a reproducible **manifest**:
174
+
175
+ - `result.audit` turns the regime's contract into machine-checkable findings — entity /
176
+ pair / context overlap (across *all* splits, including val-vs-test), reversed unordered
177
+ pairs, message-passing leakage, duplicate observations, and optional negative-sample /
178
+ feature-provenance checks. Each finding has a count, severity, and concrete offending
179
+ ids; `raise_for_leakage()` fails on any violation.
180
+ - `result.manifest` records library/schema versions, a collision-resistant input
181
+ fingerprint, the normalized spec, per-split counts, and hashes of the split indices.
182
+ `manifest.digest()` is a stable reproducibility key; runtime/memory measurements are
183
+ kept separate so a fixed input + spec + seed always produce the same digest.
184
+
185
+ ## License
186
+
187
+ [MIT](LICENSE)
@@ -0,0 +1,158 @@
1
+ # HeteroSplit
2
+
3
+ **Leakage-safe entity-disjoint splits and audits for heterogeneous link-prediction datasets.**
4
+
5
+ [![CI](https://github.com/ZubairQazi/heterosplit/actions/workflows/ci.yml/badge.svg)](https://github.com/ZubairQazi/heterosplit/actions/workflows/ci.yml)
6
+ ![coverage](https://img.shields.io/badge/coverage-94%25-brightgreen)
7
+ ![python](https://img.shields.io/badge/python-3.10--3.13-blue)
8
+ [![license: MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE)
9
+
10
+ HeteroSplit constructs, validates, and *reports* cold-start / inductive splits for
11
+ heterogeneous link-prediction tasks (drug–drug–cell-line synergy, drug–target
12
+ interaction, recommendation, knowledge graphs). It is designed to **complement**
13
+ PyTorch Geometric — it focuses narrowly on split *semantics* and leakage *auditing*,
14
+ and does not replace PyG's loaders, samplers, or training stack.
15
+
16
+ > **Status:** early release (`0.1.x`). APIs may change. This is positioned as a
17
+ > reusable implementation of entity-disjoint split policies and leakage audits for
18
+ > PyG `HeteroData` link-prediction tasks — not (yet) a research novelty claim.
19
+
20
+ ## Why
21
+
22
+ Graph libraries make *random* node/edge splitting easy, but cold-start experiments
23
+ often rely on one-off scripts with ambiguous semantics. A nominally "cold" test set
24
+ can still leak information through:
25
+
26
+ - the same entity appearing in training under another edge or relation;
27
+ - reverse edges left in the message-passing graph;
28
+ - an unordered pair reversed across splits;
29
+ - context entities (e.g. cell lines) crossing a supposedly disjoint boundary;
30
+ - features / precomputed embeddings fit using held-out entities;
31
+ - negative sampling that pulls entities or pairs from the wrong regime.
32
+
33
+ HeteroSplit makes the split *contract* explicit, enforces it, and produces evidence
34
+ (a serializable **manifest** + **audit report**) that the contract holds.
35
+
36
+ **Why it matters (real DrugComb):** a small GraphSAGE link predictor scores **AUC 0.84**
37
+ under a random split but **0.27** on genuinely unseen drugs (either-cold-start) — random
38
+ splitting massively overestimates cold-start performance. See
39
+ [benchmarks](docs/benchmarks.md#real-data-drugcomb).
40
+
41
+ ## Install
42
+
43
+ The correctness core is pure Python + NumPy:
44
+
45
+ ```bash
46
+ pip install heterosplit
47
+ ```
48
+
49
+ PyTorch Geometric integration is an optional extra:
50
+
51
+ ```bash
52
+ pip install "heterosplit[pyg]"
53
+ ```
54
+
55
+ ### From source (development)
56
+
57
+ This project uses [`uv`](https://docs.astral.sh/uv/):
58
+
59
+ ```bash
60
+ git clone https://github.com/ZubairQazi/heterosplit
61
+ cd heterosplit
62
+ uv sync # creates .venv and installs the dev toolchain
63
+ uv run pytest # run the test suite
64
+ uv run --extra pyg pytest # include the PyG adapter tests
65
+ ```
66
+
67
+ ## Quickstart
68
+
69
+ ```python
70
+ from heterosplit import make_synthetic_dataset, split_records
71
+
72
+ # A DrugComb-shaped dataset: (drug, drug, cell-line) synergy observations.
73
+ # Swap in your own PredictionRecords / HeteroData for real data.
74
+ data = make_synthetic_dataset(
75
+ n_records=5000,
76
+ n_source_entities=120,
77
+ n_context_entities=30,
78
+ n_labels=2,
79
+ source_type="drug",
80
+ context_type="cell_line",
81
+ relation="synergy",
82
+ seed=42,
83
+ )
84
+
85
+ # Joint cold-start: a test triple must involve an unseen drug AND an unseen cell line.
86
+ spec = data.spec(
87
+ "joint_cold_start",
88
+ holdout={"drug": "either", "cell_line": "all"},
89
+ ratios=(0.8, 0.1, 0.1),
90
+ stratify_by="label",
91
+ seed=42,
92
+ )
93
+
94
+ result = split_records(data.records, spec)
95
+ result.audit.raise_for_leakage() # fails loudly on any leakage
96
+ result.manifest.save("split-manifest.json") # deterministic, reloadable
97
+
98
+ train_edges = result.message_passing_edge_index() # leakage-safe training graph
99
+ ```
100
+
101
+ Runnable versions live in [`examples/`](examples/) (`quickstart.py`,
102
+ `recommendation.py`, `drugcomb.py`, `corrupted_leakage.py`), or try the CLI:
103
+
104
+ ```bash
105
+ uv run heterosplit demo --regime joint_cold_start
106
+ uv run heterosplit split --input data.csv --spec spec.json --out-dir out/
107
+ ```
108
+
109
+ ### Real data: DrugComb
110
+
111
+ The [DrugComb](https://drugcomb.org) drug–drug–cell-line synergy corpus is wired up
112
+ directly (CC-BY-4.0):
113
+
114
+ ```python
115
+ from heterosplit.datasets.drugcomb import download_drugcomb_summary, load_drugcomb_csv
116
+
117
+ download_drugcomb_summary("drugcomb.csv") # ~1.4 GB, once
118
+ records = load_drugcomb_csv("drugcomb.csv", max_rows=200_000) # streams; filters mono-therapy rows
119
+ ```
120
+
121
+ `records_from_drugcomb(frame, ...)` maps an in-memory DrugComb frame (dict or DataFrame)
122
+ if you already have one.
123
+
124
+ ## Documentation
125
+
126
+ - [Architecture / design](docs/architecture.md)
127
+ - [Benchmarks & methodology](docs/benchmarks.md)
128
+
129
+ ## Split taxonomy (v1 target)
130
+
131
+ | Regime | Test-set contract |
132
+ |---|---|
133
+ | Random / transductive | Test edges are unseen; entities may have appeared in training. |
134
+ | Pair cold-start | The `(s, d)` pair is unseen, but each entity may appear separately. |
135
+ | Source cold-start | Test source entities never appear as training sources. |
136
+ | Destination cold-start | Test destination entities never appear as training destinations. |
137
+ | Either-entity cold-start | At least one endpoint of each test edge is unseen. |
138
+ | Both-entity cold-start | Both endpoints of every test edge are unseen. |
139
+ | Context cold-start | Test context entities never occur in training. |
140
+ | Joint cold-start | A configured combination of endpoint and context disjointness. |
141
+
142
+ ## Leakage audit & manifests
143
+
144
+ Every split ships with an **audit** and a reproducible **manifest**:
145
+
146
+ - `result.audit` turns the regime's contract into machine-checkable findings — entity /
147
+ pair / context overlap (across *all* splits, including val-vs-test), reversed unordered
148
+ pairs, message-passing leakage, duplicate observations, and optional negative-sample /
149
+ feature-provenance checks. Each finding has a count, severity, and concrete offending
150
+ ids; `raise_for_leakage()` fails on any violation.
151
+ - `result.manifest` records library/schema versions, a collision-resistant input
152
+ fingerprint, the normalized spec, per-split counts, and hashes of the split indices.
153
+ `manifest.digest()` is a stable reproducibility key; runtime/memory measurements are
154
+ kept separate so a fixed input + spec + seed always produce the same digest.
155
+
156
+ ## License
157
+
158
+ [MIT](LICENSE)
@@ -0,0 +1,39 @@
1
+ # Releasing
2
+
3
+ HeteroSplit publishes to PyPI via GitHub Actions using **trusted publishing** (OIDC — no
4
+ API tokens stored in the repo).
5
+
6
+ ## One-time PyPI setup
7
+
8
+ 1. Create the project on PyPI (and, for a dry run, TestPyPI).
9
+ 2. Add a **trusted publisher** for it (PyPI → project → Publishing):
10
+ - Owner: `ZubairQazi`
11
+ - Repository: `heterosplit`
12
+ - Workflow: `release.yml`
13
+ - Environment: `pypi` (and `testpypi` for the dry run)
14
+ 3. In the GitHub repo, create matching environments named `pypi` and `testpypi`
15
+ (Settings → Environments).
16
+
17
+ See <https://docs.pypi.org/trusted-publishers/>.
18
+
19
+ ## Cutting a release
20
+
21
+ 1. Bump `__version__` in `src/heterosplit/__init__.py` (single source of truth; hatchling
22
+ reads it).
23
+ 2. Move the `Unreleased` notes in `CHANGELOG.md` under a new version heading with today's
24
+ date.
25
+ 3. Commit, then tag and push:
26
+
27
+ ```bash
28
+ git commit -am "release: v0.1.1"
29
+ git tag v0.1.1
30
+ git push && git push --tags
31
+ ```
32
+
33
+ The `release.yml` workflow builds the sdist + wheel, verifies the tag matches
34
+ `__version__`, and publishes to PyPI.
35
+
36
+ ## Dry run (TestPyPI)
37
+
38
+ Use the workflow's **Run workflow** button (Actions → Release) with target `testpypi`
39
+ before a real release.