Session 4#

Mini challenge: the Hail Mary gene hunt#

You are Dr. Grace Ryland, and humanity’s last, most fragile plan is riding on you. You are the scientist aboard the Hail Mary — the ship Project Hail Mary sent out to figure out why the sun is dying, and why it might be about to take every other star in a 50-lightyear bubble down with it.

Somewhere along the way you picked up an unlikely lab partner: Rocky, an alien engineer with five arms, a knack for improvising equipment out of scrap, and absolutely no sense of how fragile human sample-labelling systems are. During a rough bit of turbulence, Rocky knocked over a rack of 10 patient samples. The labels are gone. The DNA, RNA and protein are all still there — you just don’t know whose is whose anymore.

This is, to put it mildly, a problem. Back on Earth, these 10 patients are relying on this exact data. And thankfully, you’re not starting from nothing: you have each sample’s full multi-omics profile, and you have the clinical notes you scribbled down before the accident — the “amaze, amaze, amaze, scientific notes” that are about to earn their keep.

Use the patient histories below, the molecular signatures locked in each sample, and every trick you’ve picked up so far in this workshop to re-identify all 10 samples. One piece of good news from earlier statistics work: you already know the mislabelled batch is exactly 5 Luminal A and 5 Luminal B subtypes — so whatever you land on, it should split 5/5.

Godspeed, Dr. Ryland. Earth is listening.

How this notebook is organised#

  1. Setup — environment check + imports

  2. Load data — the 10 mislabelled (“challenge”) samples, the labelled training cohort, and a pretrained MOFA multi-omics model

  3. Project the challenge samples into the MOFA factor space learned from the training cohort

  4. Visualise the factor space to see where the unknown samples fall relative to known subtypes

  5. Classify each unknown sample as Luminal A / Luminal B using a simple logistic regression on the factors

  6. Explain each prediction by finding the genes driving each unknown sample’s factor values, then linking those genes to diseases via a knowledge graph — to match against the clinical notes below

  7. Answer — fill in your final Pat_i -> TCGA-xxxx mapping

Patient clinical notes#

Prior to the accident, these are the disease/phenotype notes you recorded for each of the 10 patients whose samples are now mislabelled Pat_0Pat_9. You’ll use these — together with the molecular evidence gathered below — to work out which Pat_i is which TCGA-xxxx.

TCGA-EW-A6S9 — Presented with invasive breast carcinoma. Immunohistochemistry came back HER2-negative, but strongly positive for both estrogen and progesterone receptors, marking this out as a clearly hormone-receptor-driven tumor.

TCGA-A2-A0EP — Biopsy of a breast mass confirmed adenocarcinoma of ductal origin — glandular architecture arising from the mammary ducts, the most textbook presentation of the disease.

TCGA-E2-A1IG — A patient managing both asthma and Parkinson disease for several years, whose case took a sharp turn with the diagnosis of breast angiosarcoma, a rare and fast-growing vascular malignancy rather than a typical epithelial breast cancer.

TCGA-AQ-A1H3 — Decades of type 1 diabetes had already taken a toll by the time she came in: coronary artery disease and chronic kidney disease, the two classic downstream complications of long-standing diabetic vascular damage.

TCGA-AC-A2FE — Type 2 diabetes was already on the chart when breast angiosarcoma was diagnosed — a second, unrelated but noteworthy vascular malignancy layered onto an existing metabolic condition.

TCGA-A2-A1FX — Screening first picked up lobular neoplasia; further workup of a separate area of the breast then revealed medullary carcinoma, a less common but relatively well-differentiated histological subtype.

TCGA-A2-A4S3 — Living with multiple sclerosis, this patient went on to develop two independent primary cancers around the same time: an adenocarcinoma of the lung and a colorectal cancer, an unusually heavy cancer burden for one case.

TCGA-C8-A26W — Chronic kidney disease was a long-standing issue by the time gastric carcinoma appeared, and the picture was complicated further still by a later diagnosis of acute myeloid leukemia — three serious conditions converging in one patient.

TCGA-AR-A2LK — A palpable lump led to biopsy and a diagnosis of ductal adenocarcinoma of the breast, the same core histology as the most common form of breast adenocarcinoma.

TCGA-AO-A0JD — Type 2 diabetes was a pre-existing condition when this patient was diagnosed with breast angiosarcoma, a rare vascular tumor of the breast rather than the far more common ductal or lobular types.

1. Setup#

# Quick environment check (optional).
# Confirms the notebook is running on the expected machine / Python env / folder,
# since path or environment mismatches are a common source of confusing errors.
import socket
import os
import sys

print("Host:", socket.gethostname())
print("Python:", sys.executable)
print("Working dir:", os.getcwd())
Host: biomedicalai
Python: /opt/conda/envs/ECCB_2026/bin/python
Working dir: /home/bryan/ECCB2026_TEST/sessions/session-4-kg-query-mini-challenge
# ── Standard library ─────────────────────────────────────────────────────────
from pathlib import Path
import pickle

# ── Third-party ───────────────────────────────────────────────────────────────
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import networkx as nx
from matplotlib.colors import TwoSlopeNorm
from IPython.display import display

from sklearn.linear_model import LogisticRegression
# Extra metrics you may want once you've settled on a final answer and want to
# sanity-check a prediction against a partially-known label, or during model
# development on the labelled training cohort:
from sklearn.metrics import accuracy_score, balanced_accuracy_score, classification_report

# MOFA (Multi-Omics Factor Analysis): mofax is used to *read* a pretrained model.
# mofapy2 is only needed if you want to *train* a MOFA model from scratch, which
# isn't required for this challenge (a pretrained model is provided below).
import mofax as mfx

# ── Custom helpers for this workshop (see s4_helpers.py) ───────────────────────
from s4_helpers import (
    load_omics,
    evaluate_predictions,
    load_kg,
    print_graph_info,
    map_genes_to_kg,
    diseases_for_genes,
)

# ── Reproducibility ───────────────────────────────────────────────────────────
RANDOM_STATE = 42

2. Load data#

We need three things:

  1. The 10 mislabelled “challenge” samples (Pat_0Pat_9) — these are the ones we’re re-identifying.

  2. The labelled training cohort used to train the MOFA model, plus the train/test patient ID split that was used at training time.

  3. The pretrained MOFA model itself, so we can project the challenge samples into the same factor space.

# The 10 mislabelled samples for this challenge.
DATA_DIR = Path(".")

X_challenge_omics, y_challenge = load_omics(
    DATA_DIR,
    omic_filename="challenge_omics",
    omic_keys=["transcriptomics", "proteomics", "methylation"],
)

# The TCGA IDs Rocky mixed up (order not meaningful — this is what we're solving for).
missing_ids = [
    "TCGA-EW-A6S9", "TCGA-A2-A0EP", "TCGA-E2-A1IG",
    "TCGA-AQ-A1H3", "TCGA-AC-A2FE", "TCGA-A2-A1FX",
    "TCGA-A2-A4S3", "TCGA-C8-A26W", "TCGA-AR-A2LK", "TCGA-AO-A0JD",
]
pat_ids = [f"Pat_{i}" for i in range(10)]

# The true subtype of each challenge sample is unknown by construction — that's
# half of what we're inferring (the other half is the exact patient identity).
y_challenge["subtype"] = ["Unknown" for _ in range(10)]
y_challenge = y_challenge[["subtype"]]

# Relabel the challenge cohort with anonymised Pat_0..Pat_9 IDs.
for view in X_challenge_omics:
    X_challenge_omics[view].index = pat_ids
y_challenge.index = pat_ids

X_challenge_omics
Omic view dimensions:
  transcriptomics:   10 patients x  29995 features
  proteomics     :   10 patients x    464 features
  methylation    :   10 patients x 200000 features

Subtype counts:
subtype
Unknown    10
Name: count, dtype: int64
{'transcriptomics':        ENSG00000000003.15  ENSG00000000005.6  ENSG00000000419.13  \
 Pat_0           10.206609           3.890222           10.963447   
 Pat_1           11.227947           7.885052           10.747011   
 Pat_2           10.858597           3.622629           10.543180   
 Pat_3           11.418415           6.649170           10.675950   
 Pat_4           11.639118           7.683469           10.614152   
 Pat_5           10.006381           3.991210           10.967056   
 Pat_6           11.320906           4.597212           11.209474   
 Pat_7           11.521549           4.382121           11.129100   
 Pat_8           11.692278           4.079725           11.140742   
 Pat_9           12.186842           4.716691           11.855058   
 
        ENSG00000000457.14  ENSG00000000460.17  ENSG00000000938.13  \
 Pat_0            9.960213            8.783081            7.883629   
 Pat_1           11.352407            8.963408           11.828274   
 Pat_2           10.480666            8.805772            8.354700   
 Pat_3           11.772228            9.739870            9.070792   
 Pat_4           10.360072            8.960729            9.935186   
 Pat_5            9.476851            8.826529            8.802416   
 Pat_6           10.049028            9.896462            9.213614   
 Pat_7           10.304102            9.837964            8.720828   
 Pat_8           10.533073           10.023556            8.988475   
 Pat_9           10.738790           10.903126            8.964495   
 
        ENSG00000000971.16  ENSG00000001036.14  ENSG00000001084.13  \
 Pat_0           11.083770           10.744436           11.751679   
 Pat_1           12.929817           11.243190           11.040297   
 Pat_2           11.695405           10.179517           10.889103   
 Pat_3           12.100870           10.894993            9.977176   
 Pat_4           11.250770           11.529095           10.553126   
 Pat_5           11.684284           11.000672           11.628035   
 Pat_6           10.423992           12.156724           11.485138   
 Pat_7           11.510676           12.033402           10.843561   
 Pat_8           11.263677           10.980872           11.134081   
 Pat_9            9.823592           13.432973           11.613547   
 
        ENSG00000001167.14  ...  ENSG00000288611.1  ENSG00000288612.1  \
 Pat_0           11.388343  ...           4.450666           5.667865   
 Pat_1           10.466207  ...           2.843467           6.377397   
 Pat_2           11.436503  ...           4.163000           5.356899   
 Pat_3           11.895307  ...           3.298848           5.391283   
 Pat_4           11.372468  ...           2.843467           5.553009   
 Pat_5           11.608577  ...           3.287055           6.508915   
 Pat_6           11.997321  ...           5.576287           6.716968   
 Pat_7           11.658691  ...           3.471029           6.382145   
 Pat_8           11.812687  ...           3.518504           5.361268   
 Pat_9           12.380097  ...           4.099344           4.571069   
 
        ENSG00000288638.1  ENSG00000288648.1  ENSG00000288657.1  \
 Pat_0           2.843467           2.843467           3.375382   
 Pat_1           2.843467           3.345657           2.843467   
 Pat_2           2.843467           2.843467           3.397723   
 Pat_3           2.843467           3.298848           2.843467   
 Pat_4           2.843467           3.329320           2.843467   
 Pat_5           2.843467           2.843467           2.843467   
 Pat_6           2.843467           2.843467           2.843467   
 Pat_7           2.843467           2.843467           3.471029   
 Pat_8           2.843467           2.843467           2.843467   
 Pat_9           2.843467           3.419187           2.843467   
 
        ENSG00000288658.1  ENSG00000288663.1  ENSG00000288670.1  \
 Pat_0           3.591567           4.375290           8.897982   
 Pat_1           5.122605           5.929506           7.922134   
 Pat_2           3.622629           4.512358           9.500332   
 Pat_3           3.625866           5.010104           8.810702   
 Pat_4           4.768499           4.901473           8.606419   
 Pat_5           3.468372           6.272242           8.215869   
 Pat_6           4.937380           6.139969           8.373386   
 Pat_7           3.724245           5.185608           8.314728   
 Pat_8           3.666569           5.240143           9.601401   
 Pat_9           3.828054           4.099344           7.871598   
 
        ENSG00000288674.1  ENSG00000288675.1  
 Pat_0           4.207402           5.903832  
 Pat_1           4.776614           5.675877  
 Pat_2           3.792194           5.234692  
 Pat_3           4.234930           4.518756  
 Pat_4           4.509409           5.603773  
 Pat_5           4.201095           5.387085  
 Pat_6           3.759852           4.860542  
 Pat_7           3.824557           5.467033  
 Pat_8           4.489482           4.489482  
 Pat_9           3.419187           5.124520  
 
 [10 rows x 29995 columns],
 'proteomics':        1433BETA  1433EPSILON  1433ZETA     4EBP1  4EBP1_pS65  4EBP1_pT37T46  \
 Pat_0  0.209990    -0.075390  0.371980 -0.224730   -0.485060      -0.692580   
 Pat_1  0.312400     0.030429  0.253340 -0.611520   -0.201840       0.671530   
 Pat_2  0.127790    -0.070532 -0.027055 -0.582010   -0.450870      -0.473950   
 Pat_3  0.672860     0.454690  0.690000 -0.435830   -0.677050      -0.216300   
 Pat_4  0.151560     0.191260  0.116100  0.007940   -0.475930       0.386940   
 Pat_5 -0.145800     0.105943  0.923275  0.579005   -0.479165      -0.398805   
 Pat_6  0.471200     0.135960  0.229710  0.421070   -0.820150      -0.588590   
 Pat_7  0.266130     0.400270 -0.676650 -0.516900   -0.331410       0.146250   
 Pat_8 -0.064925    -0.012983 -0.019147  0.122100   -0.503420       0.400560   
 Pat_9  0.179010    -0.000011 -0.305770  0.387160   -0.403100      -0.892310   
 
        4EBP1_pT70     53BP1  ACC_pS79      ACC1  ...       XPF     XRCC1  \
 Pat_0   -0.237400 -0.067915  0.081168  0.007453  ...  0.260623 -0.099892   
 Pat_1   -0.490780 -0.814960  0.529520  0.870960  ...  0.298706 -0.115650   
 Pat_2   -0.318360  0.360740  1.002400  0.899580  ...  0.620847  0.350880   
 Pat_3   -0.325280 -1.454200  2.059000  0.801960  ...  0.635439 -0.797760   
 Pat_4   -0.402460 -0.066013  0.806990  0.031855  ...  0.325157 -0.015553   
 Pat_5   -0.006398 -0.010776  0.126345  0.721685  ... -0.006203 -0.149885   
 Pat_6    0.402940 -0.669310 -0.067353 -0.189610  ...  0.559783  0.345610   
 Pat_7    0.178840 -0.865140 -0.086904 -0.452800  ...  0.196953  0.150710   
 Pat_8   -0.245610 -0.295440  0.246430  0.367050  ... -0.084701  0.126790   
 Pat_9   -0.103780 -0.602700 -0.336820  0.341820  ... -0.226228 -0.024386   
 
             YAP  YAP_pS127       YB1  YB1_pS102    YTHDF2    YTHDF3    ZAP-70  \
 Pat_0 -0.287960  -0.456050 -0.359370  -0.057382 -0.435499 -0.672054  0.516419   
 Pat_1  0.049778   0.223490  0.037169   0.000000 -0.597347 -1.673852 -0.829929   
 Pat_2 -0.272050  -0.422110  0.269720  -0.268640  0.450075 -0.159570 -0.370457   
 Pat_3  1.320500   1.670100 -0.531290   0.322170 -1.465223 -0.971948 -0.591835   
 Pat_4 -0.154470  -0.104590  0.326420  -0.141670 -0.160695  0.429300  1.105883   
 Pat_5 -0.485700  -1.128265  0.203920  -0.249505  0.249155  0.477140 -0.294727   
 Pat_6  0.249630  -0.449320 -0.368930   0.314880 -0.911570 -0.257785  1.340228   
 Pat_7  0.373290   0.214090 -0.328940  -0.029406 -1.517599 -1.559424  0.885328   
 Pat_8 -0.375900  -0.628490 -0.159960  -0.422880  0.027947  0.742192 -0.973856   
 Pat_9 -0.339030  -0.793460 -0.252160   0.010017  0.671120  0.392285 -0.176342   
 
            ZEB1  
 Pat_0  0.364914  
 Pat_1  0.282237  
 Pat_2  0.434968  
 Pat_3  0.912490  
 Pat_4  0.431498  
 Pat_5 -0.221702  
 Pat_6  0.511333  
 Pat_7  0.671174  
 Pat_8  0.074150  
 Pat_9 -0.122987  
 
 [10 rows x 464 columns],
 'methylation':        cg11738485  cg01893212  cg23179456  cg12466610  cg22473620  cg15690342  \
 Pat_0    0.036465    0.459944    0.765009    0.523401    0.666596    0.171290   
 Pat_1    0.642683    0.366179    0.147451    0.082516    0.234550    0.329291   
 Pat_2    0.587153    0.808454    0.802197    0.598610    0.793824    0.825494   
 Pat_3    0.559811    0.694687    0.661540    0.230455    0.499805    0.755860   
 Pat_4    0.984174    0.585158    0.581683    0.290122    0.598232    0.629066   
 Pat_5    0.015636    0.750984    0.780966    0.469563    0.737902    0.572635   
 Pat_6    0.029568    0.030172    0.660712    0.548529    0.721839    0.184347   
 Pat_7    0.727794    0.039441    0.030866    0.971210    0.345076    0.237567   
 Pat_8    0.981246    0.829965    0.660667    0.055368    0.041200    0.849246   
 Pat_9    0.680905    0.046090    0.837534    0.614237    0.798068    0.886255   
 
        cg02467990  cg21885317  cg20399616  cg22831607  ...  cg03082813  \
 Pat_0    0.309456    0.087059    0.146893    0.304569  ...    0.099009   
 Pat_1    0.343183    0.929976    0.146072    0.168229  ...    0.111423   
 Pat_2    0.767110    0.951831    0.768201    0.042451  ...    0.047717   
 Pat_3    0.643704    0.192525    0.652058    0.669029  ...    0.072981   
 Pat_4    0.509020    0.905908    0.616243    0.629752  ...    0.086513   
 Pat_5    0.714676    0.330090    0.758003    0.295251  ...    0.084584   
 Pat_6    0.021985    0.937415    0.039530    0.585501  ...    0.088012   
 Pat_7    0.040871    0.170520    0.099211    0.062540  ...    0.048544   
 Pat_8    0.812736    0.695391    0.802597    0.868684  ...    0.097715   
 Pat_9    0.041928    0.931980    0.742047    0.672269  ...    0.047196   
 
        cg13744910  cg09550558  cg09395540  cg13222853  cg06575013  cg23123972  \
 Pat_0    0.964663    0.122532    0.922226    0.869302    0.093457    0.851539   
 Pat_1    0.972984    0.069577    0.948352    0.883429    0.126899    0.910045   
 Pat_2    0.965976    0.064062    0.821157    0.910605    0.078588    0.926255   
 Pat_3    0.973397    0.070123    0.937828    0.911747    0.095471    0.893391   
 Pat_4    0.971771    0.066935    0.938623    0.896895    0.114243    0.882909   
 Pat_5    0.964760    0.064851    0.873053    0.930288    0.093407    0.912283   
 Pat_6    0.958149    0.087123    0.917065    0.863395    0.097908    0.848616   
 Pat_7    0.975608    0.079897    0.934193    0.871475    0.149388    0.933717   
 Pat_8    0.976676    0.079897    0.829877    0.882533    0.105870    0.885309   
 Pat_9    0.976905    0.071415    0.904568    0.928623    0.080946    0.671952   
 
        cg10507099  cg03538345  cg14015726  
 Pat_0    0.019959    0.166893    0.941172  
 Pat_1    0.017771    0.127076    0.944468  
 Pat_2    0.017049    0.111346    0.953182  
 Pat_3    0.018662    0.104881    0.918535  
 Pat_4    0.016234    0.087803    0.947652  
 Pat_5    0.020630    0.069685    0.933060  
 Pat_6    0.021112    0.094935    0.812604  
 Pat_7    0.022462    0.163373    0.943986  
 Pat_8    0.017831    0.147168    0.934284  
 Pat_9    0.016314    0.088723    0.943192  
 
 [10 rows x 200000 columns]}
# The labelled training cohort (TCGA-BRCA) that the MOFA model below was trained on.
TRAIN_DATA_DIR = Path("/data/")

X_omics, y = load_omics(
    TRAIN_DATA_DIR,
    omic_filename="omics",
    omic_keys=["transcriptomics", "proteomics", "methylation"],
)

with open(TRAIN_DATA_DIR / "patient_splits.pkl", "rb") as f:
    splits = pickle.load(f)

train_ids = splits["train_ids"]
test_ids = splits["test_ids"]

# Split the labels using the same patient IDs used for the omics data.
y_train = y.loc[train_ids]
y_test = y.loc[test_ids]
Omic view dimensions:
  transcriptomics:  500 patients x  29995 features
  proteomics     :  500 patients x    464 features
  methylation    :  500 patients x 200000 features

Subtype counts:
paper_BRCA_Subtype_PAM50
LumA      237
LumB      100
Basal      97
Her2       41
Normal     25
Name: count, dtype: int64
# Load the pretrained MOFA model.
mofa_model_mfx = mfx.mofa_model("mofa_pretrained.hdf5")

print(mofa_model_mfx)          # Model overview
print(mofa_model_mfx.shape)    # (n_samples, n_factors)

# Extract key components.
factors = mofa_model_mfx.get_factors(df=True)               # Factor/latent space values
weights = mofa_model_mfx.get_weights(df=True)         # Feature weights/loadings
var_exp = mofa_model_mfx.get_variance_explained()     # R^2 per factor per view

# NOTE: the exact factor-name format (e.g. "Factor1" vs "Factor 1") depends on
# your mofax version. Check this before relying on either format later in the
# notebook:
print("Factor column names look like:", list(factors.columns[:3]))
MOFA+ model: mofa pretrained
Samples (cells): 375
Features: 4464
Groups: TCGA-BRCA_train (375)
Views: methylation (2000), proteomics (464), transcriptomics (2000)
Factors: 10
Expectations: W, Z
(375, 4464)
Factor column names look like: ['Factor1', 'Factor2', 'Factor3']

3. Project the challenge samples into the MOFA factor space#

MOFA was fit on the training cohort only. To place the 10 unknown challenge samples on the same latent axes, we need to project them using the feature weights the model already learned — without ever touching the (unknown) labels.

# Restrict each omics view to the features MOFA actually used (its most-variable,
# training-selected feature set), so train and test matrices line up column-for-column.
X_train_raw = {name: X.loc[train_ids] for name, X in X_omics.items()}
X_test_raw = {name: X for name, X in X_challenge_omics.items()}

features_in_mofa = set(weights.index)
X_train_omics = {}
X_test_omics = {}

for name, X_train_view in X_train_raw.items():
    selected_features = list(features_in_mofa & set(X_train_view.columns))
    X_train_omics[name] = X_train_view.loc[:, selected_features]
    X_test_omics[name] = X_test_raw[name].loc[:, selected_features]

print("Feature counts after filtering to MOFA's feature set:")
for name in X_omics:
    print(
        f"  {name:15s}: {X_omics[name].shape[1]:6d} original -> "
        f"{X_train_omics[name].shape[1]:6d} used by MOFA"
    )
Feature counts after filtering to MOFA's feature set:
  transcriptomics:  29995 original ->   2000 used by MOFA
  proteomics     :    464 original ->    464 used by MOFA
  methylation    : 200000 original ->   2000 used by MOFA
def project_test_patients_to_mofa_factors(model, X_train_by_view, X_test_by_view, train_factors, view_names):
    """Project held-out patients into the fixed MOFA factor space.

    The projection uses MOFA weights learned from training patients. To keep
    train and test factor values comparable, preprocessing and calibration are
    fit on training patients only and then applied unchanged to test patients
    (test labels are never used anywhere in this function).
    """
    factor_columns = train_factors.columns.astype(str).tolist()
    projected_test_by_view = []

    for view_name in view_names:
        # W is the learned feature x factor weight matrix for this omics view.
        view_weights = model.get_weights(views=view_name, df=True)
        view_weights.columns = view_weights.columns.astype(str)
        view_weights = view_weights.reindex(columns=factor_columns)

        # Use only features present in the trained weights AND both train/test matrices.
        common_features = view_weights.index.intersection(X_train_by_view[view_name].columns)
        common_features = common_features.intersection(X_test_by_view[view_name].columns)
        view_weights = view_weights.loc[common_features]

        X_train_view = X_train_by_view[view_name].loc[:, common_features].astype(float)
        X_test_view = X_test_by_view[view_name].loc[:, common_features].astype(float)
        X_train_view.index = X_train_view.index.astype(str)
        X_test_view.index = X_test_view.index.astype(str)

        # Learn centering/scaling on training patients only, then apply it to test patients.
        train_mean = X_train_view.mean(axis=0)
        train_std = X_train_view.std(axis=0, ddof=0).replace(0, 1)
        X_train_scaled = (X_train_view - train_mean) / train_std
        X_test_scaled = (X_test_view - train_mean) / train_std

        # Project X into the fixed MOFA weight space via the pseudo-inverse of W.
        raw_train_projection = X_train_scaled.to_numpy() @ np.linalg.pinv(view_weights.to_numpy()).T
        raw_test_projection = X_test_scaled.to_numpy() @ np.linalg.pinv(view_weights.to_numpy()).T

        # Calibrate the raw projection to the factor scale returned by the trained
        # MOFA model. Fitted on training patients only; test labels are never used.
        train_design = np.column_stack([raw_train_projection, np.ones(raw_train_projection.shape[0])])
        test_design = np.column_stack([raw_test_projection, np.ones(raw_test_projection.shape[0])])
        train_target = train_factors.loc[X_train_view.index, factor_columns].to_numpy()
        calibration = np.linalg.lstsq(train_design, train_target, rcond=None)[0]
        projected_values = test_design @ calibration

        projected = pd.DataFrame(projected_values, index=X_test_view.index, columns=factor_columns)
        projected_test_by_view.append(projected)

    # Each view gives one projected factor table; average them since all views
    # are available for every challenge sample here.
    return sum(projected_test_by_view) / len(projected_test_by_view)
# Extract training-patient factor values straight from the fitted MOFA model.
train_factors_mfx = mofa_model_mfx.get_factors(df=True)
train_factors_mfx.index = train_factors_mfx.index.astype(str)

# Project the 10 held-out challenge patients into that same factor space.
test_factors_mfx = project_test_patients_to_mofa_factors(
    mofa_model_mfx,
    X_train_omics,
    X_test_omics,
    train_factors_mfx,
    list(X_train_omics.keys()),
)
# Attach the known training subtypes to the model so mofax's built-in plots can
# colour training points by subtype.
mofa_sample_metadata = pd.DataFrame(index=train_factors_mfx.index)
mofa_sample_metadata["subtype"] = y_train.reindex(train_factors_mfx.index).values

mofa_model_mfx.samples_metadata = mofa_sample_metadata

4. Visualise the factor space#

# Built-in mofax scatter grid, training patients only, coloured by subtype.
mfx.plot_factors_scatter(
    mofa_model_mfx,
    x="Factor1",
    y="Factor2",
    group_label="subtype",
    color="subtype",
    zero_line_x=True,
    zero_line_y=True,
    ncols=3,
    size=40,
    alpha=1,
)
plt.show()
../_images/26a96126f543ec226c71d26674c664318377f3281361c0033eac51618eb03e42.png
# Combine training (known subtype) and challenge (unknown subtype) factors into
# one long table we can reuse for every plot below.
plot_df = pd.concat([train_factors_mfx, test_factors_mfx])

subtypes_col = pd.concat([
    y_train,
    pd.Series(["Unknown"] * len(test_factors_mfx), index=test_factors_mfx.index),
])
plot_df["subtype"] = subtypes_col

plot_df
Factor1 Factor2 Factor3 Factor4 Factor5 Factor6 Factor7 Factor8 Factor9 Factor10 subtype
TCGA-S3-AA0Z -2.677049 -4.491632 1.438483 -0.422795 0.741272 1.235917 1.497584 -0.306961 0.952876 -0.117106 Basal
TCGA-E9-A1N6 0.180977 -0.561914 -3.574715 -2.498562 -3.337365 -2.594017 -1.536899 1.478458 -0.399653 1.112885 LumB
TCGA-C8-A26Y -1.037958 -2.685832 0.221065 -2.129348 -1.283929 -3.698094 -2.550864 3.238068 0.323258 0.714914 Her2
TCGA-A7-A0D9 0.757382 -0.338205 -1.839327 6.520308 -4.192802 1.799493 -0.594609 1.617057 -4.788059 2.172852 LumA
TCGA-A2-A0CT 4.982207 -0.566879 0.930730 -0.789196 -1.455145 2.251426 3.763444 1.592921 0.738021 0.874641 LumA
... ... ... ... ... ... ... ... ... ... ... ...
Pat_5 1.732713 1.484934 0.798178 -0.922396 -0.938942 2.177108 -1.136659 0.900399 -0.561686 -0.393969 Unknown
Pat_6 0.525045 -0.649719 0.079504 -1.348195 1.465892 0.453576 -1.885745 0.881061 -0.181360 0.944625 Unknown
Pat_7 -0.511048 1.179021 -1.281183 -0.298532 0.825806 -0.048020 -1.702661 -0.368831 0.132252 0.810454 Unknown
Pat_8 2.410831 1.197439 1.041670 -1.269500 -0.668765 0.721102 0.172933 -1.027378 -0.076351 0.376981 Unknown
Pat_9 0.916728 0.003875 0.923834 -0.510366 0.090924 -0.598543 -1.956078 0.735629 0.040657 -0.187265 Unknown

385 rows × 11 columns

fig, ax = plt.subplots(figsize=(8, 6))

# Split known vs unknown subtypes (handles both NaN and the literal 'Unknown' label).
known = plot_df[plot_df["subtype"].notna() & ~plot_df["subtype"].isin(["Unknown"])]
unknown = plot_df[plot_df["subtype"].isna() | plot_df["subtype"].isin(["Unknown"])]

# Known subtypes as coloured circles.
sns.scatterplot(data=known, x="Factor1", y="Factor2", hue="subtype", s=40, alpha=1, ax=ax)

# Unknown (challenge) samples as bigger grey crosses, drawn on top.
ax.scatter(
    unknown["Factor1"], unknown["Factor2"],
    marker="X", s=120, color="grey", label="Unknown", zorder=5, linewidths=1.2,
)

ax.axhline(0, color="grey", linewidth=0.8, linestyle="--")
ax.axvline(0, color="grey", linewidth=0.8, linestyle="--")
ax.set_title("Factor 1 vs Factor 2 by Subtype")
ax.legend(title="Subtype", bbox_to_anchor=(1.05, 1), loc="upper left")
plt.tight_layout()
plt.show()
../_images/56466d8207b8cf9081704ac57b2b63dd76a9d98308334b3d91865a3e1f0a794b.png

4.1 Compare subtypes across a wider set of factors#

Two factors aren’t always enough to separate every subtype — below we look at a few more factors as boxplots (by known subtype) and then as individual scatter points for the unknown samples, so we can eyeball which subtype each challenge sample is likely to belong to before formally classifying it in Section 5.

subtype_order = ["Basal", "LumB", "Her2", "LumA", "Normal"]
factors_of_interest = ["Factor2", "Factor1", "Factor6", "Factor4"]

fig, axes = plt.subplots(1, len(factors_of_interest), figsize=(15, 4), squeeze=False)

for ax, factor in zip(axes.ravel(), factors_of_interest):
    groups = [
        plot_df.loc[plot_df["subtype"] == subtype, factor].dropna()
        for subtype in subtype_order
    ]
    ax.boxplot(groups, tick_labels=subtype_order, showfliers=False)
    ax.set_title(factor)
    ax.set_ylabel("Factor value")
    ax.tick_params(axis="x", rotation=45)

plt.tight_layout()
plt.show()
../_images/5e67896505a7395ecbbaf870219ab5a102868140a823434c0e695a5315dd2036.png
# Y-axis limits per factor, just for a bit of visual polish.
ylims = {
    "Factor2": (-8, 4),
    "Factor1": (-4, 4),
    "Factor6": (-4, 4),
    "Factor4": (-4, 6),
}

unknown_df = plot_df[plot_df["subtype"] == "Unknown"]

fig, axes = plt.subplots(1, len(factors_of_interest), figsize=(16, 5), squeeze=False)

for ax, factor in zip(axes.ravel(), factors_of_interest):
    vals = unknown_df[factor].dropna()

    ax.scatter(range(len(vals)), vals.values, s=80, color="steelblue", zorder=5)
    ax.set_xticks(range(len(vals)))
    ax.set_xticklabels(vals.index, rotation=90, fontsize=8)
    ax.axhline(0, color="grey", linewidth=0.8, linestyle="--")
    ax.set_ylim(ylims[factor])
    ax.set_title(factor)
    ax.set_ylabel("Factor value")
    ax.set_xlabel("Sample ID")

plt.tight_layout()
plt.show()
../_images/3bad83e33d8c38695f4d1f3e3fd298796840f0847d3ed7270a0bb673c2a34576.png
# Built-in mofax heatmap of the mean factor value per subtype (training patients only —
# challenge patients have subtype == NaN/'Unknown' so they don't contribute here).
mfx.plot_factors_matrix(
    mofa_model_mfx,
    group_label="subtype",
    cmap="coolwarm",
    center=0,
)
plt.title("Mean MOFA factor value by subtype")
plt.show()
../_images/78ab3fb1d13e049ac03020af6b2e2f99046574bfc922cfc345aa75c6706a8a01.png
# Recreate that same subtype-mean heatmap side-by-side with the unknown samples,
# on a shared colour scale, so it's easy to visually match each unknown sample's
# row-pattern to the subtype it most resembles.
heatmap_factors = [f"Factor{i}" for i in [1, 2, 4, 6]]

unknown_mask = plot_df["subtype"].isna() | plot_df["subtype"].isin(["Unknown", "NA", "unknown"])

unknown_mat = plot_df.loc[unknown_mask, heatmap_factors].dropna(how="all")
heat_unknown = unknown_mat.T  # rows = factors, cols = sample IDs

means_by_subtype = (
    plot_df.loc[~unknown_mask, ["subtype"] + heatmap_factors]
    .dropna(subset=["subtype"])
    .groupby("subtype")[heatmap_factors]
    .mean()
    .reindex(subtype_order)
)

# Colour scale is defined ONLY from the subtype-mean matrix, so the unknown
# heatmap is shown on the exact same scale as the reference panel.
vmax = np.nanmax(np.abs(means_by_subtype.values))
norm = TwoSlopeNorm(vmin=-vmax, vcenter=0.0, vmax=vmax)

fig = plt.figure(figsize=(max(10, 0.35 * heat_unknown.shape[1] + 6), 8))
gs = fig.add_gridspec(1, 3, width_ratios=[2.2, 0.18, max(4, 0.35 * heat_unknown.shape[1])], wspace=0.25)

ax_ref = fig.add_subplot(gs[0, 0])
cax = fig.add_subplot(gs[0, 1])
ax_unk = fig.add_subplot(gs[0, 2])

sns.heatmap(means_by_subtype.T, ax=ax_ref, cmap="coolwarm", norm=norm, cbar=False,
            linewidths=0.2, linecolor="white")
ax_ref.set_title("Mean MOFA factor value by subtype")
ax_ref.set_xlabel("Factors")
ax_ref.set_ylabel("Subtype")
ax_ref.tick_params(axis="x", rotation=90)

sns.heatmap(heat_unknown, ax=ax_unk, cmap="coolwarm", norm=norm, cbar=True,
            cbar_ax=cax, linewidths=0.0)
ax_unk.set_title("Unknown participants (same scale as subtype means)")
ax_unk.set_xlabel("Sample ID")
ax_unk.set_ylabel("Factors")
ax_unk.tick_params(axis="x", rotation=90)

cax.set_ylabel("Factor value")
plt.tight_layout()
plt.show()
/tmp/ipykernel_1508621/1113544594.py:46: UserWarning: This figure includes Axes that are not compatible with tight_layout, so results might be incorrect.
  plt.tight_layout()
../_images/229387e7662dab1bd5f54398fde3572c946ef71ac24cb0a81ea6f3572053971f.png

5. Classify each unknown sample: Luminal A or Luminal B#

We already know (from earlier statistics) that all 10 unknown samples are either Luminal A or Luminal B. A simple logistic regression on the MOFA factors — trained on the labelled cohort — gives us a first, quantitative pass at this split, which we can combine with the visual read from Section 4.

# 1) Get y_train as a 1D Series of subtype labels.
subtype_labels = y_train
if isinstance(subtype_labels, pd.DataFrame):
    if "paper_BRCA_Subtype_PAM50" in subtype_labels.columns:
        subtype_labels = subtype_labels["paper_BRCA_Subtype_PAM50"]
    elif "subtype" in subtype_labels.columns:
        subtype_labels = subtype_labels["subtype"]
    else:
        raise ValueError("Could not find a subtype column in y_train.")

# 2) Align train X and y by sample IDs.
common_ids = train_factors_mfx.index.intersection(subtype_labels.index)
X_train_clf = train_factors_mfx.loc[common_ids]
y_train_clf = subtype_labels.loc[common_ids]

# 3) Fit a simple multi-class logistic regression on all factors.
clf = LogisticRegression(solver="lbfgs", max_iter=5000, random_state=RANDOM_STATE)
clf.fit(X_train_clf, y_train_clf)

# 4) Predict on the challenge (unknown) samples.
test_pred = clf.predict(test_factors_mfx)
predicted_subtype = pd.Series(test_pred, index=test_factors_mfx.index, name="predicted_subtype")

predicted_subtype
Pat_0    LumA
Pat_1    LumA
Pat_2    LumA
Pat_3    LumA
Pat_4    LumA
Pat_5    LumA
Pat_6    LumB
Pat_7    LumA
Pat_8    LumA
Pat_9    LumB
Name: predicted_subtype, dtype: str
predicted_proba = pd.DataFrame(
    clf.predict_proba(test_factors_mfx),
    index=test_factors_mfx.index,
    columns=clf.classes_,
)

fig, ax = plt.subplots(figsize=(8, max(6, 0.25 * predicted_proba.shape[0])))

sns.heatmap(
    predicted_proba,
    cmap="viridis",
    vmin=0, vmax=1,
    linewidths=0.2,
    linecolor="white",
    cbar_kws={"label": "Predicted probability"},
    ax=ax,
)
ax.set_title("Predicted subtype probabilities (challenge samples)")
ax.set_xlabel("Subtype")
ax.set_ylabel("Sample ID")
ax.tick_params(axis="x", rotation=45)
ax.tick_params(axis="y", rotation=0)

plt.tight_layout()
plt.show()

predicted_proba
../_images/72fdb9c1053be47aa063275fdfebc01637136fdb5c8d5ebb9b00b30a01a4abae.png
Basal Her2 LumA LumB Normal
Pat_0 3.847440e-05 0.013562 0.671983 0.310265 0.004151
Pat_1 2.466155e-04 0.000998 0.926213 0.051993 0.020549
Pat_2 2.847282e-07 0.000012 0.719408 0.280566 0.000013
Pat_3 2.101006e-05 0.000099 0.947450 0.049191 0.003239
Pat_4 1.386258e-05 0.000172 0.923490 0.074452 0.001872
Pat_5 4.128784e-07 0.000006 0.683015 0.316967 0.000012
Pat_6 2.432563e-03 0.068885 0.081754 0.841215 0.005713
Pat_7 3.578692e-04 0.004467 0.770673 0.206939 0.017563
Pat_8 4.269855e-07 0.000212 0.761841 0.237774 0.000172
Pat_9 1.166027e-03 0.016971 0.281572 0.682959 0.017332

6. Explain each unknown sample: driving genes -> diseases#

Knowing Luminal A vs B narrows things down but doesn’t tell us which patient a sample belongs to. For that we look at which genes most strongly drive each unknown sample’s position in factor space, then look up which diseases those genes are associated with in a knowledge graph — so we can match against the clinical notes at the top of the notebook.

We run this for all 10 unknown samples (not just one), since we need to resolve every Pat_i.

def top_features_weighted_by_expression(
    model,
    X: pd.DataFrame,
    sample_id: str,
    factor: str,
    view: str,
    top_n: int = 20,
    standardize: bool = True,
):
    """Rank features for one sample/factor by |weight| * |expression|.

    A feature's contribution to a sample's factor value is roughly proportional
    to its MOFA weight times how far that sample's (standardized) expression is
    from the cohort mean. Sorting by this product surfaces the genes that most
    plausibly explain *why* this particular sample sits where it does on this factor.
    """
    # W: features x factors
    W = model.get_weights(views=view, df=True)
    w = W[factor]

    x = X.loc[sample_id]
    if standardize:
        # z-score each feature across samples so weight and expression are comparable.
        x = (x - X.mean(axis=0)) / X.std(axis=0).replace(0, pd.NA)

    impact = (w.abs() * x.abs()).dropna().sort_values(ascending=False)

    out = pd.DataFrame({
        "weight_abs": w.abs().reindex(impact.index),
        "x_abs": x.abs().reindex(impact.index),
        "impact": impact,
    })
    return out.head(top_n)
# Which factors and view to explain each sample with. Factor1/2/4/6 were the
# ones that visually separated subtypes best in Section 4 — feel free to widen
# this set (or try 'proteomics'/'methylation') if your matches aren't conclusive.
factors_to_explain = ["Factor1", "Factor2", "Factor4", "Factor6"]
view = "transcriptomics"

# top_genes_by_sample: {sample_id -> list of top driving gene names}
top_genes_by_sample = {}

for sample_id in unknown_df.index:
    genes_for_this_sample = []
    for factor in factors_to_explain:
        top = top_features_weighted_by_expression(
            mofa_model_mfx,
            X_test_omics[view],
            sample_id=sample_id,
            factor=factor,
            view=view,
            top_n=25,
        )
        genes_for_this_sample += list(top.index)
    top_genes_by_sample[sample_id] = sorted(set(genes_for_this_sample))

for sample_id, genes in top_genes_by_sample.items():
    print(f"{sample_id}: {len(genes)} candidate driving genes")
Pat_0: 84 candidate driving genes
Pat_1: 89 candidate driving genes
Pat_2: 85 candidate driving genes
Pat_3: 91 candidate driving genes
Pat_4: 82 candidate driving genes
Pat_5: 85 candidate driving genes
Pat_6: 83 candidate driving genes
Pat_7: 78 candidate driving genes
Pat_8: 85 candidate driving genes
Pat_9: 83 candidate driving genes
# Load the gene-disease knowledge graph once.
G = load_kg()
print_graph_info(G)
Number of nodes: 881
Number of edges: 1768
Graph type: undirected

Nodes by type:
     760  gene
      90  disease
      31  icd10
Edges by type:
   1,656  associated_with
      81  is_a
      31  maps_to

Self-loops: 0
Graph density: 0.004561
Connected components: 1
Largest component: 881 nodes (100.0% of the graph)
Average clustering coefficient: 0.0137
# For each unknown sample, map its driving genes onto the KG and pull the
# diseases most associated with them — these are the clues to match against
# the clinical notes at the top of the notebook.
disease_hits_by_sample = {}

for sample_id, genes in top_genes_by_sample.items():
    mapped = map_genes_to_kg(G, genes)
    n_in_kg = mapped["in_kg"].sum()

    diseases = diseases_for_genes(G, genes, top_n=30)
    disease_hits_by_sample[sample_id] = diseases

    print(f"\n=== {sample_id}: {n_in_kg} / {len(mapped)} genes found in KG ===")
    display(diseases.head(10))
=== Pat_0: 8 / 84 genes found in KG ===
disease_id name n_genes mean_score genes
0 MONDO_0006512 estrogen-receptor positive breast cancer 4 0.342725 ARHGEF38, CCDC170, ESR1, NEK10
1 MONDO_0000618 Her2-receptor negative breast cancer 4 0.237250 CCDC170, DNER, ESR1, NEK10
2 MONDO_0006513 estrogen-receptor negative breast cancer 3 0.305100 CCDC170, ESR1, NEK10
3 MONDO_0007254 breast cancer 2 0.681400 ERBB2, ESR1
4 MONDO_0004950 gastric carcinoma 2 0.432450 ERBB2, ESR1
5 MONDO_0004989 breast carcinoma 2 0.704800 ERBB2, ESR1
6 MONDO_0006244 HER2 positive breast carcinoma 2 0.541300 ERBB2, ESR1
7 MONDO_0021115 luminal B breast carcinoma 2 0.373450 CCDC170, ESR1
8 MONDO_0004988 breast adenocarcinoma 2 0.581300 ERBB2, ESR1
9 MONDO_0005298 osteoporosis 2 0.567900 CCDC170, ESR1
=== Pat_1: 8 / 89 genes found in KG ===
disease_id name n_genes mean_score genes
0 MONDO_0021115 luminal B breast carcinoma 3 0.331667 ESR1, FOXA1, GATA3
1 MONDO_0004988 breast adenocarcinoma 3 0.548900 ESR1, FOXA1, GATA3
2 MONDO_0006116 breast carcinoma by gene expression profile 3 0.370000 ESR1, FOXA1, GATA3
3 MONDO_0005590 breast ductal adenocarcinoma 3 0.379933 ESR1, FOXA1, GATA3
4 MONDO_0005298 osteoporosis 2 0.577850 ESR1, PGR
5 MONDO_0000552 breast lobular carcinoma 2 0.379500 ESR1, FOXA1
6 EFO_0009782 progesterone-receptor positive breast cancer 2 0.178700 ESR1, THSD4
7 MONDO_0005063 medullary breast carcinoma 2 0.139350 ESR1, MUC16
8 MONDO_0004379 female breast carcinoma 2 0.324100 ESR1, GATA3
9 MONDO_0000618 Her2-receptor negative breast cancer 2 0.313500 ESR1, THSD4
=== Pat_2: 4 / 85 genes found in KG ===
disease_id name n_genes mean_score genes
0 MONDO_0003024 breast angiosarcoma 2 0.08565 ADIPOQ, IL33
1 MONDO_0004979 asthma 1 0.71850 IL33
2 MONDO_0008315 prostate cancer 1 0.79930 AR
3 MONDO_0005298 osteoporosis 1 0.48990 AR
4 MONDO_0005590 breast ductal adenocarcinoma 1 0.37290 AR
5 MONDO_0006244 HER2 positive breast carcinoma 1 0.37000 AR
6 MONDO_0006043 metaplastic breast carcinoma 1 0.27840 AR
7 MONDO_0004379 female breast carcinoma 1 0.27810 AR
8 EFO_0009782 progesterone-receptor positive breast cancer 1 0.07400 AR
9 MONDO_0005180 Parkinson disease 1 0.61020 MAPT
=== Pat_3: 11 / 91 genes found in KG ===
disease_id name n_genes mean_score genes
0 MONDO_0006512 estrogen-receptor positive breast cancer 2 0.3263 ARHGEF38, NEK10
1 MONDO_0006513 estrogen-receptor negative breast cancer 2 0.2624 LGR6, NEK10
2 MONDO_0021115 luminal B breast carcinoma 2 0.2775 ERBB4, GATA3
3 MONDO_0004988 breast adenocarcinoma 1 0.5676 GATA3
4 MONDO_0005298 osteoporosis 1 0.4447 PGR
5 MONDO_0005147 type 1 diabetes mellitus 1 0.5100 SLC5A1
6 MONDO_0005300 chronic kidney disease 1 0.4716 SLC5A1
7 MONDO_0004379 female breast carcinoma 1 0.3700 GATA3
8 MONDO_0005590 breast ductal adenocarcinoma 1 0.3731 GATA3
9 MONDO_0006256 invasive breast carcinoma 1 0.3772 GATA3
=== Pat_4: 4 / 82 genes found in KG ===
disease_id name n_genes mean_score genes
0 MONDO_0005148 type 2 diabetes mellitus 1 0.8566 ABCC8
1 MONDO_0006513 estrogen-receptor negative breast cancer 1 0.2838 OSR1
2 MONDO_0000552 breast lobular carcinoma 1 0.3700 AFF3
3 MONDO_0006116 breast carcinoma by gene expression profile 1 0.2775 AFF3
4 MONDO_0003024 breast angiosarcoma 1 0.0681 ADIPOQ
=== Pat_5: 4 / 85 genes found in KG ===
disease_id name n_genes mean_score genes
0 MONDO_0002486 lobular neoplasia 1 0.0812 CD36
1 MONDO_0003024 breast angiosarcoma 1 0.0681 ADIPOQ
2 MONDO_0005063 medullary breast carcinoma 1 0.0925 MUC16
3 MONDO_0006117 breast diffuse large B-cell lymphoma 1 0.0925 MUC16
4 MONDO_0006512 estrogen-receptor positive breast cancer 1 0.3104 ARHGEF38
=== Pat_6: 4 / 83 genes found in KG ===
disease_id name n_genes mean_score genes
0 MONDO_0005061 lung adenocarcinoma 2 0.63250 EGFR, ERBB4
1 MONDO_0006804 inflammatory breast carcinoma 2 0.12815 EGFR, ERBB4
2 MONDO_0005301 multiple sclerosis 1 0.59030 KCNG1
3 MONDO_0007254 breast cancer 1 0.67900 EGFR
4 MONDO_0008170 ovarian cancer 1 0.53470 FOLR1
5 MONDO_0018177 glioblastoma 1 0.65510 EGFR
6 MONDO_0005575 colorectal cancer 1 0.61490 EGFR
7 MONDO_0006043 metaplastic breast carcinoma 1 0.27970 EGFR
8 MONDO_0000616 progesterone-receptor negative breast cancer 1 0.07400 EGFR
9 MONDO_0004950 gastric carcinoma 1 0.37980 ERBB4
=== Pat_7: 6 / 78 genes found in KG ===
disease_id name n_genes mean_score genes
0 MONDO_0007254 breast cancer 1 0.6138 TOP2A
1 MONDO_0008170 ovarian cancer 1 0.5534 TOP2A
2 MONDO_0004988 breast adenocarcinoma 1 0.3787 TOP2A
3 MONDO_0006804 inflammatory breast carcinoma 1 0.2988 TOP2A
4 MONDO_0006256 invasive breast carcinoma 1 0.2895 TOP2A
5 MONDO_0005023 ductal breast carcinoma in situ 1 0.2668 TOP2A
6 MONDO_0006116 breast carcinoma by gene expression profile 1 0.2775 NTRK3
7 MONDO_0005051 invasive lobular breast carcinoma 1 0.0833 NTRK3
8 MONDO_0005300 chronic kidney disease 1 0.5778 AGTR1
9 MONDO_0005090 schizophrenia 1 0.5300 GPM6A
=== Pat_8: 5 / 85 genes found in KG ===
disease_id name n_genes mean_score genes
0 MONDO_0005298 osteoporosis 2 0.57785 ESR1, PGR
1 MONDO_0021115 luminal B breast carcinoma 2 0.35875 ESR1, FOXA1
2 MONDO_0004988 breast adenocarcinoma 2 0.53955 ESR1, FOXA1
3 MONDO_0000552 breast lobular carcinoma 2 0.37950 ESR1, FOXA1
4 MONDO_0006513 estrogen-receptor negative breast cancer 2 0.34155 ESR1, OSR1
5 MONDO_0005590 breast ductal adenocarcinoma 2 0.38335 ESR1, FOXA1
6 MONDO_0006116 breast carcinoma by gene expression profile 2 0.37000 ESR1, FOXA1
7 MONDO_0004989 breast carcinoma 1 0.71190 ESR1
8 MONDO_0006244 HER2 positive breast carcinoma 1 0.46140 ESR1
9 MONDO_0005628 male breast carcinoma 1 0.42640 ESR1
=== Pat_9: 3 / 83 genes found in KG ===
disease_id name n_genes mean_score genes
0 MONDO_0003024 breast angiosarcoma 2 0.09795 MMP7, PIGR
1 MONDO_0005148 type 2 diabetes mellitus 1 0.85660 ABCC8

7. Your answer: match each sample to a patient#

Using:

  • the predicted Luminal A / Luminal B subtype for each sample (Section 5), and

  • the top associated diseases for each sample (Section 6),

match each of Pat_0Pat_9 to one of the 10 TCGA-xxxx IDs from the clinical notes at the top of this notebook. Every ID should be used exactly once, and your final answer should contain 5 Luminal A and 5 Luminal B matches.

# TODO: fill this in with your final answer, e.g. {"Pat_0": "TCGA-EW-A6S9", ...}
sample_to_patient = {pat_id: None for pat_id in pat_ids}

sample_to_patient
{'Pat_0': None,
 'Pat_1': None,
 'Pat_2': None,
 'Pat_3': None,
 'Pat_4': None,
 'Pat_5': None,
 'Pat_6': None,
 'Pat_7': None,
 'Pat_8': None,
 'Pat_9': None}
# Sanity checks before you submit.
assert all(v is not None for v in sample_to_patient.values()), "Every Pat_i needs a match."
assert set(sample_to_patient.values()) == set(missing_ids), "Every TCGA ID should be used exactly once."

n_luma = (predicted_subtype.reindex(sample_to_patient.keys()) == "LumA").sum()
n_lumb = (predicted_subtype.reindex(sample_to_patient.keys()) == "LumB").sum()
print(f"Predicted split among your matches -> LumA: {n_luma}, LumB: {n_lumb} (expect 5 / 5)")

# Uncomment once you're happy with your answer:
# evaluate_predictions(sample_to_patient)
---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
Cell In[24], line 2
      1 # Sanity checks before you submit.
----> 2 assert all(v is not None for v in sample_to_patient.values()), "Every Pat_i needs a match."
      3 assert set(sample_to_patient.values()) == set(missing_ids), "Every TCGA ID should be used exactly once."
      4 
      5 n_luma = (predicted_subtype.reindex(sample_to_patient.keys()) == "LumA").sum()

AssertionError: Every Pat_i needs a match.