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.
- heterosplit-0.1.0/.github/workflows/ci.yml +90 -0
- heterosplit-0.1.0/.github/workflows/release.yml +63 -0
- heterosplit-0.1.0/.gitignore +40 -0
- heterosplit-0.1.0/.python-version +1 -0
- heterosplit-0.1.0/CHANGELOG.md +43 -0
- heterosplit-0.1.0/LICENSE +21 -0
- heterosplit-0.1.0/PKG-INFO +187 -0
- heterosplit-0.1.0/README.md +158 -0
- heterosplit-0.1.0/RELEASING.md +39 -0
- heterosplit-0.1.0/benchmarks/benchmark.py +159 -0
- heterosplit-0.1.0/docs/architecture.md +109 -0
- heterosplit-0.1.0/docs/benchmarks.md +104 -0
- heterosplit-0.1.0/examples/corrupted_leakage.py +52 -0
- heterosplit-0.1.0/examples/drugcomb.py +83 -0
- heterosplit-0.1.0/examples/quickstart.py +68 -0
- heterosplit-0.1.0/examples/recommendation.py +47 -0
- heterosplit-0.1.0/examples/train_link_prediction.py +257 -0
- heterosplit-0.1.0/pyproject.toml +110 -0
- heterosplit-0.1.0/src/heterosplit/__init__.py +58 -0
- heterosplit-0.1.0/src/heterosplit/adapters/__init__.py +12 -0
- heterosplit-0.1.0/src/heterosplit/adapters/pyg.py +170 -0
- heterosplit-0.1.0/src/heterosplit/adapters/tabular.py +48 -0
- heterosplit-0.1.0/src/heterosplit/api.py +21 -0
- heterosplit-0.1.0/src/heterosplit/audit/__init__.py +58 -0
- heterosplit-0.1.0/src/heterosplit/audit/_common.py +49 -0
- heterosplit-0.1.0/src/heterosplit/audit/contract.py +56 -0
- heterosplit-0.1.0/src/heterosplit/audit/feature_provenance.py +65 -0
- heterosplit-0.1.0/src/heterosplit/audit/message_passing.py +79 -0
- heterosplit-0.1.0/src/heterosplit/audit/negative_samples.py +74 -0
- heterosplit-0.1.0/src/heterosplit/audit/overlap.py +239 -0
- heterosplit-0.1.0/src/heterosplit/audit/report.py +107 -0
- heterosplit-0.1.0/src/heterosplit/canonical.py +78 -0
- heterosplit-0.1.0/src/heterosplit/cli.py +119 -0
- heterosplit-0.1.0/src/heterosplit/datasets/__init__.py +22 -0
- heterosplit-0.1.0/src/heterosplit/datasets/drugcomb.py +187 -0
- heterosplit-0.1.0/src/heterosplit/errors.py +40 -0
- heterosplit-0.1.0/src/heterosplit/manifest.py +215 -0
- heterosplit-0.1.0/src/heterosplit/message_passing.py +126 -0
- heterosplit-0.1.0/src/heterosplit/objective.py +71 -0
- heterosplit-0.1.0/src/heterosplit/py.typed +0 -0
- heterosplit-0.1.0/src/heterosplit/records.py +218 -0
- heterosplit-0.1.0/src/heterosplit/report/__init__.py +7 -0
- heterosplit-0.1.0/src/heterosplit/report/distributions.py +64 -0
- heterosplit-0.1.0/src/heterosplit/report/summary.py +110 -0
- heterosplit-0.1.0/src/heterosplit/result.py +146 -0
- heterosplit-0.1.0/src/heterosplit/schema.py +229 -0
- heterosplit-0.1.0/src/heterosplit/spec.py +268 -0
- heterosplit-0.1.0/src/heterosplit/splitters/__init__.py +48 -0
- heterosplit-0.1.0/src/heterosplit/splitters/assignment.py +175 -0
- heterosplit-0.1.0/src/heterosplit/splitters/base.py +184 -0
- heterosplit-0.1.0/src/heterosplit/splitters/context_disjoint.py +31 -0
- heterosplit-0.1.0/src/heterosplit/splitters/entity_disjoint.py +128 -0
- heterosplit-0.1.0/src/heterosplit/splitters/joint.py +121 -0
- heterosplit-0.1.0/src/heterosplit/splitters/pair.py +34 -0
- heterosplit-0.1.0/src/heterosplit/splitters/random.py +45 -0
- heterosplit-0.1.0/src/heterosplit/synthetic.py +127 -0
- heterosplit-0.1.0/tests/test_adapter_pyg.py +101 -0
- heterosplit-0.1.0/tests/test_adapter_tabular.py +54 -0
- heterosplit-0.1.0/tests/test_assignment.py +53 -0
- heterosplit-0.1.0/tests/test_audit.py +198 -0
- heterosplit-0.1.0/tests/test_audit_injection.py +63 -0
- heterosplit-0.1.0/tests/test_benchmark.py +31 -0
- heterosplit-0.1.0/tests/test_canonical.py +84 -0
- heterosplit-0.1.0/tests/test_cli.py +85 -0
- heterosplit-0.1.0/tests/test_drugcomb.py +110 -0
- heterosplit-0.1.0/tests/test_manifest.py +97 -0
- heterosplit-0.1.0/tests/test_message_passing.py +90 -0
- heterosplit-0.1.0/tests/test_objective.py +51 -0
- heterosplit-0.1.0/tests/test_properties.py +132 -0
- heterosplit-0.1.0/tests/test_records.py +111 -0
- heterosplit-0.1.0/tests/test_refine.py +67 -0
- heterosplit-0.1.0/tests/test_report.py +50 -0
- heterosplit-0.1.0/tests/test_review_fixes.py +140 -0
- heterosplit-0.1.0/tests/test_schema.py +111 -0
- heterosplit-0.1.0/tests/test_spec.py +140 -0
- heterosplit-0.1.0/tests/test_split_context.py +62 -0
- heterosplit-0.1.0/tests/test_split_entity.py +122 -0
- heterosplit-0.1.0/tests/test_split_joint.py +78 -0
- heterosplit-0.1.0/tests/test_split_pair.py +67 -0
- heterosplit-0.1.0/tests/test_split_random.py +90 -0
- heterosplit-0.1.0/tests/test_synthetic.py +88 -0
- heterosplit-0.1.0/tests/test_train_example.py +33 -0
- 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
|
+
[](https://github.com/ZubairQazi/heterosplit/actions/workflows/ci.yml)
|
|
35
|
+

|
|
36
|
+

|
|
37
|
+
[](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
|
+
[](https://github.com/ZubairQazi/heterosplit/actions/workflows/ci.yml)
|
|
6
|
+

|
|
7
|
+

|
|
8
|
+
[](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.
|