Skip to content

Graph Convolutional Matrix Factorization - #442

Draft
PascalIversen wants to merge 16 commits into
developmentfrom
feat/gcmf-geometric-mf
Draft

Graph Convolutional Matrix Factorization #442
PascalIversen wants to merge 16 commits into
developmentfrom
feat/gcmf-geometric-mf

Conversation

@PascalIversen

@PascalIversen PascalIversen commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator

Graph convolutional matrix factorization for drug-response prediction. Predicts the cell-line x drug response matrix as R = U V^T (as in normal matrix fact.) But the latent factors are learned end2end by graph convolutions over feature-similarity or prior knowledge graphs. One can use mutiple graphs for drugs and cell lines in the relational gcmf version.
The P-versions also produce aleatoric uncertainty estimates.
The main idea is to combine learning in primal and dual space (one can use a large-dimensional feature space and transform it to a similarity, and exploit it without blowing up the model complexity). Also, the graph convolution smooths the embeddings with those of the neighboring cells, which I hope prevents overfitting/helps generalization.

The variants are

  • GCMF: base single-graph model.
  • RGCMF: relational/multi-graph variant (currently with multi-omics cell graphs; pathway and
    bioassay drug relations) fused by a relational graph convolution. (works best on LCO per my experiments)
  • PGCMF / PRGCMF: probabilistic variants with a heteroscedastic Gaussian-NLL
    head emitting calibrated per-prediction uncertainty.

Per my experiments this is chocolate worthy but will rerun on the leaderboard scripts.

@PascalIversen
PascalIversen marked this pull request as draft June 23, 2026 14:09
Graph-convolutional matrix factorization for drug-response prediction. Predicts the
cell-line x drug response matrix as R = U V^T (as in ordinary matrix factorization),
but the latent factors are learned end-to-end by graph convolutions over
feature-similarity or prior-knowledge graphs (k-NN gene-expression graph for cell
lines, Morgan-fingerprint Tanimoto graph for drugs). Each convolution smooths a node's
embedding over its graph neighbours; the smoothed factors are read out by the dot
product, plus per-cell/per-drug biases and an optional MLP head.

Four selectable models, all registered in MULTI_DRUG_MODEL_FACTORY:
* GCMF   - base single-graph model.
* RGCMF  - relational/multi-graph variant (multi-omics cell graphs; pathway and
           bioassay drug relations) fused by a relational graph convolution.
           Works best on leave-cell-out in our experiments.
* PGCMF / PRGCMF - probabilistic variants with a heteroscedastic Gaussian-NLL head
           emitting calibrated per-prediction aleatoric uncertainty.

Includes hyperparameters.yaml for all four, bundled gzipped drug-relation resources
for RGCMF/PRGCMF, smoke tests (train/predict/save-load round-trip), docs (model page
+ toctree + usage table), and a check-added-large-files exclude for the bundled
resources.

Lint/type clean: flake8 0, mypy 0, black/isort clean, pre-commit green.
@PascalIversen
PascalIversen force-pushed the feat/gcmf-geometric-mf branch from 60e0898 to 9ec282e Compare June 23, 2026 14:11
…tion fallback

- Un-bundle the drug-relation tables: remove drevalpy/models/GCMF/resources/ and
  revert the check-added-large-files whitelist. Resources now load only from
  <data_path>/meta/gcmf_drug_relations/ (downloaded with the meta bundle).
- Rename gcmf_nd_drug_similarity -> gcmf_drug_relations and gcmf_nd_cache -> gcmf_cache;
  remove provenance/port framing from docstrings and comments.
- RGCMF now falls back to the base GCMF graph when a tower resolves to zero relations,
  instead of crashing; update tests accordingly.
@PascalIversen
PascalIversen force-pushed the feat/gcmf-geometric-mf branch from 0b50db5 to ac84dbc Compare July 16, 2026 09:04
PascalIversen and others added 8 commits July 16, 2026 11:22
…etric-mf

# Conflicts:
#	drevalpy/models/__init__.py
#	tests/test_drp_model.py
GCMF._build_*_adj is declared to return Tensor | list[Tensor]; the RGCMF
fallback list needs a plain Tensor item, so cast the (always single-tensor)
base result.
RGCMF no longer silently substitutes the base graph when a relation is absent:
a configured drug-relation resource that is missing now raises FileNotFoundError,
and an empty relation set raises ValueError. Tests provide a synthetic drug_pathways
relation so RGCMF/PRGCMF train relationally, plus a test for the missing-resource error.
Drop the synthetic drug_pathways fixture and the drug_relation_views override, so
RGCMF/PRGCMF train on their real defaults (drug_pathways, drug_bioassay) loaded from
<data_path>/meta/gcmf_drug_relations/. The suite now genuinely depends on the shipped
resources instead of masking them with a toy stand-in.

Add test_default_drug_relations_ship_with_meta_bundle, which asserts every default
relation resolves in the downloaded bundle, so a relation dropped from the bundle
fails with a clear message rather than as a downstream training error.

Verified: 8 passed with the relations present; with them removed the bundle guard and
both relational models fail, while GCMF/PGCMF stay green.
The relational tests pinned cell_relation_views to a single gene_expression graph and
forced gene_list=None, so the multi-omics cell graphs and the gene lists they reduce with
were never exercised. RGCMF/PRGCMF now run their real defaults on both sides: all four
cell relations (gene_expression, methylation, mutations, copy_number_variation_gistic)
and their real gene_expression_intersection gene list. Only the base GCMF/PGCMF still
relax gene_list, because they default to landmark_genes, which the toy bundle does not
ship. Cost is negligible: 0.41s vs 0.01s to build the similarities on toy data.

Extend the bundle guard (now test_default_relations_ship_with_meta_bundle) to also assert
the per-omics gene lists behind the cell relations are present, not just the drug relations.

Verified with a cold cache: 8 passed with the bundle intact; removing a cell gene list
fails the guard plus both relational models. Note the guard is load-bearing - with a warm
<dataset>/gcmf_cache/ the training tests still pass on a missing gene list, since the
cache key covers the gene-list name but not its contents.
The drug relations were mapped onto a dataset's drugs by normalizing drug names
(lowercase, strip spaces/hyphens/underscores) and matching those. That is fragile, and it
ignored pubchem_id, the identifier the datasets actually key drugs on.

Relations are now joined on pubchem_id directly. The tables carry a pubchem_id column and
a table without one raises a clear error instead of falling back to name matching, so no
fuzzy matching remains anywhere in the model. This also drops the drug_names.csv read from
load_drug_features and removes _norm_name entirely.

Two further fixes while here:
- _build_target_jaccard addressed the table positionally (iloc[:, 0] / iloc[:, 1]); it now
  resolves the feature column by name, so a schema change cannot silently shift the join.
- _map_named_similarity is now _map_indexed_similarity and expects a matrix indexed by
  pubchem_id on both axes, matching the "targets" path.

Coverage is unchanged against name matching: CTRPv2 265/545 drug_pathways and 382/545
drug_bioassay, identical to before, now via an exact join. Note a drug name can map to
different pubchem_ids in different datasets (4 such names across the shipped datasets), so
the tables list one row per identifier and every dataset joins against the id it uses.
The kendall kernel rescaled tau as (tau - sim.min()) / (1 - sim.min()), where sim.min() is
the least similar pair in the current cohort. A pair's similarity therefore depended on
which other cell lines were present, so the same two cell lines scored differently in
different CV splits. On CTRPv2 one pair with tau = 0.0990 scored 0.4089 in a 70-cell cohort
and 0.3753 in a 45-cell one. The comment on that line already described the fixed
[-1, 1] -> [0, 1] map, which is what the code now does: (tau + 1) / 2. That pair now scores
0.5495 in both, and the shared block of the two cohorts is identical.

k-NN topology is unaffected, since the old transform was monotonic and neighbour ranking was
already correct. Edge weights do change (mean 0.011 after the GCN normalization) because all
four models run with use_edge_weights, so RGCMF/PRGCMF numbers shift slightly.

Add _SIM_KERNEL_VERSION to the cell-similarity cache key. Without it the cached matrices
under <dataset>/gcmf_cache/ still hold the old scaling and would be reused, so the fix would
silently not apply on any machine with a warm cache. Also correct the cache docstring, which
claimed the key covers more than it does: it does not cover the contents of the omics table
or the gene list, only their names.
…ture column

_build_target_jaccard raises when the table carries nothing besides the pubchem_id join key
and drug_name, but the docstring had no :raises: entry, which darglint flags as DAR401.
@PascalIversen
PascalIversen force-pushed the feat/gcmf-geometric-mf branch from 8e42e69 to 5633025 Compare August 5, 2026 15:04
PascalIversen and others added 6 commits August 5, 2026 17:55
…m targets

The train/predict test drew its targets from np.random.default_rng(0).normal(), so despite
the docstring saying it trained on TOYv1 only the features were real. It asserted that
predictions were finite and survived a save/load round-trip, which a model that learned
nothing would also satisfy. It now uses the sample_dataset fixture, the 1776 measured TOYv1
responses, the same source every other model test already uses.

Assert the predictions correlate with the responses (> 0.2). Measured across the family after
the two training epochs the test runs: GCMF 0.697, PGCMF 0.706, RGCMF 0.716, PRGCMF 0.709, so
the threshold has ample headroom for cross-platform numerics. An untrained model scores
-0.041 and fails it, which is the case the previous version could not detect.

Also drop the invented rows from the missing-pubchem_id test: the join key is validated before
any row is read, so the header alone is what that test exercises.
@PascalIversen
PascalIversen marked this pull request as ready for review August 7, 2026 08:45
@PascalIversen
PascalIversen marked this pull request as draft August 7, 2026 09:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant