Skip to content

Commit 021b197

Browse files
add logging + toy data + fix tuto
1 parent 0d1fad6 commit 021b197

12 files changed

Lines changed: 2185 additions & 1465 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Other
22
.DS_Store
3+
sandbox/
34

45
# Byte-compiled / optimized / DLL files
56
__pycache__/

customics/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
import importlib.metadata
2+
import logging
23

4+
from ._logging import configure_logger
35
from . import datasets, metrics
6+
from .utils import get_common_samples, get_sub_omics_df, toy_dataset
47
from .model import CustOMICS
58

69
__version__ = importlib.metadata.version("customics")
10+
11+
log = logging.getLogger("customics")
12+
configure_logger(log)

customics/_logging.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import logging
2+
3+
log = logging.getLogger(__name__)
4+
5+
6+
class ColorFormatter(logging.Formatter):
7+
grey = "\x1b[38;20m"
8+
blue = "\x1b[36;20m"
9+
yellow = "\x1b[33;20m"
10+
red = "\x1b[31;20m"
11+
bold_red = "\x1b[31;1m"
12+
reset = "\x1b[0m"
13+
14+
prefix = "[%(levelname)s] (%(name)s)"
15+
suffix = "%(message)s"
16+
17+
FORMATS = {
18+
logging.DEBUG: f"{grey}{prefix}{reset} {suffix}",
19+
logging.INFO: f"{blue}{prefix}{reset} {suffix}",
20+
logging.WARNING: f"{yellow}{prefix}{reset} {suffix}",
21+
logging.ERROR: f"{red}{prefix}{reset} {suffix}",
22+
logging.CRITICAL: f"{bold_red}{prefix}{reset} {suffix}",
23+
}
24+
25+
def format(self, record):
26+
log_fmt = self.FORMATS.get(record.levelno)
27+
formatter = logging.Formatter(log_fmt)
28+
return formatter.format(record)
29+
30+
31+
def configure_logger(log: logging.Logger):
32+
log.setLevel(logging.INFO)
33+
34+
consoleHandler = logging.StreamHandler()
35+
consoleHandler.setFormatter(ColorFormatter())
36+
37+
log.addHandler(consoleHandler)
38+
log.propagate = False

customics/explain/shap.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
"""SHAP-based explainability utilities for customics models."""
22

3+
from __future__ import annotations
4+
35
from typing import TYPE_CHECKING
46

57
import pandas as pd

customics/metrics/classification.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ def multi_classification_evaluation(
9090
)
9191
plt.xlabel("Predicted Labels")
9292
plt.ylabel("True Labels")
93-
plt.savefig(filename + ".png")
93+
plt.savefig(str(filename) + ".png")
9494
plt.clf()
9595
return scores
9696

customics/model.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
import logging
4+
from pathlib import Path
45

56
import numpy as np
67
import pandas as pd
@@ -885,7 +886,7 @@ def stratify(
885886
# Serialisation
886887
# ---------------------------------------------------------------------- #
887888

888-
def save(self, path: str) -> None:
889+
def save(self, path: str | Path) -> None:
889890
"""Save the model architecture config and trained weights to *path*.
890891
891892
The checkpoint contains the five parameter dicts needed to reconstruct
@@ -909,11 +910,15 @@ def save(self, path: str) -> None:
909910
"history": self.history,
910911
"_is_fitted": self._is_fitted,
911912
}
913+
914+
checkpoint_path = Path(path)
915+
checkpoint_path.parent.mkdir(parents=True, exist_ok=True)
916+
912917
torch.save(checkpoint, path)
913-
logger.info("Model saved to %s", path)
918+
logger.info(f"Model saved to {path}")
914919

915920
@classmethod
916-
def load(cls, path: str, device: torch.device | None = None) -> CustOMICS:
921+
def load(cls, path: str | Path, device: torch.device | None = None) -> CustOMICS:
917922
"""Load a model previously saved with [save](api/train/#customics.CustOMICS.save).
918923
919924
Parameters
@@ -945,5 +950,5 @@ def load(cls, path: str, device: torch.device | None = None) -> CustOMICS:
945950
model.history = checkpoint.get("history", [])
946951
model._is_fitted = checkpoint.get("_is_fitted", True)
947952
model.to(device)
948-
logger.info("Model loaded from %s", path)
953+
logger.info(f"Model loaded from {path}")
949954
return model

customics/utils.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import os
44

5+
import numpy as np
56
import pandas as pd
67
import seaborn as sns
78
from sklearn.model_selection import KFold, train_test_split
@@ -10,6 +11,36 @@
1011
sns.set_palette("muted")
1112
sns.set_context("notebook", font_scale=1.5, rc={"lines.linewidth": 2.5})
1213

14+
# ---------------------------------------------------------------------------
15+
# Toy dataset loader
16+
# ---------------------------------------------------------------------------
17+
18+
19+
def toy_dataset() -> tuple[dict[str, pd.DataFrame], pd.DataFrame]:
20+
"""Load toy multi-omics dataset from GitHub.
21+
22+
Returns
23+
-------
24+
tuple of (dict, pd.DataFrame)
25+
- dict: Multi-omics dictionary (source name → DataFrame).
26+
- DataFrame: Clinical metadata with sample IDs as index.
27+
"""
28+
PREFIX = "https://raw.githubusercontent.com/prism-oncology/customics/refs/heads/main/data"
29+
30+
omics_df = {
31+
"protein": pd.read_csv(f"{PREFIX}/toy_data/protein.txt", sep="\t", index_col=0).T,
32+
"gene_exp": pd.read_csv(f"{PREFIX}/toy_data/gene_exp.txt", sep="\t", index_col=0).T,
33+
"methyl": pd.read_csv(f"{PREFIX}/toy_data/methyl.txt", sep="\t", index_col=0).T,
34+
}
35+
36+
clinical_df = pd.read_csv(f"{PREFIX}/toy_data/labels.txt", sep="\t", index_col=1, header=0)
37+
38+
rng = np.random.default_rng(42)
39+
clinical_df["OS"] = rng.integers(0, 2, size=len(clinical_df)) # 0 = censored, 1 = event
40+
clinical_df["OS.time"] = rng.integers(200, 3000, size=len(clinical_df)) # days
41+
42+
return omics_df, clinical_df
43+
1344

1445
# ---------------------------------------------------------------------------
1546
# Sample alignment

customics/visualization.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
"""Visualisation helpers for trained customics models."""
22

3+
from __future__ import annotations
4+
35
from typing import TYPE_CHECKING
46

57
import matplotlib.pyplot as plt
68
import numpy as np
9+
import pandas as pd
710
import seaborn as sns
811
from sklearn.manifold import TSNE
912

@@ -167,7 +170,7 @@ def save_plot_score(filename: str, z: np.ndarray, y: np.ndarray, title: str, sho
167170
show : bool
168171
If True, display the plot interactively after saving.
169172
"""
170-
tsne = TSNE(n_components=2, verbose=0, perplexity=40, n_iter=300)
173+
tsne = TSNE(n_components=2, verbose=0, perplexity=40)
171174
embedding = tsne.fit_transform(z)
172175
df = pd.DataFrame({"targets": y, "x-axis": embedding[:, 0], "y-axis": embedding[:, 1]})
173176
sns.scatterplot(
@@ -179,7 +182,7 @@ def save_plot_score(filename: str, z: np.ndarray, y: np.ndarray, title: str, sho
179182
)
180183
plt.title(title)
181184
plt.legend(bbox_to_anchor=(1.5, 1.1), loc=2, borderaxespad=0.0)
182-
plt.savefig(filename + ".png", bbox_inches="tight")
185+
plt.savefig(str(filename) + ".png", bbox_inches="tight")
183186
if show:
184187
plt.show()
185188
plt.clf()

docs/getting_started.md

Lines changed: 0 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -39,96 +39,6 @@ Choose one of the following, depending on your needs:
3939
uv sync --dev
4040
```
4141

42-
## Usage
43-
`CustOmics` provides a simple, scikit-learn–style `API` through the customics package. Below is a complete example showing how to train, evaluate, and interpret a multi-omics model:
44-
45-
#### 1. Prepare your data
46-
`omics_train`: *dict mapping source name → pd.DataFrame (samples × features)*<br>
47-
`clinical_df`: *pd.DataFrame with columns for labels, event indicator, and survival time.*
48-
49-
50-
``` python
51-
import torch
52-
import pandas as pd
53-
from customics import CustOMICS
54-
55-
omics_train = {
56-
"rna": pd.read_csv("rna_train.csv", index_col=0),
57-
"cnv": pd.read_csv("cnv_train.csv", index_col=0),
58-
"methyl": pd.read_csv("methyl_train.csv", index_col=0),
59-
}
60-
clinical_df = pd.read_csv("clinical.csv", index_col=0)
61-
```
62-
63-
#### 2. Configure the model
64-
65-
``` python
66-
source_params = {
67-
"rna": {"input_dim": 5000, "hidden_dim": [1024, 512], "latent_dim": 128, "norm": True, "dropout": 0.2},
68-
"cnv": {"input_dim": 2000, "hidden_dim": [512, 256], "latent_dim": 128, "norm": True, "dropout": 0.2},
69-
"methyl":{"input_dim": 8000, "hidden_dim": [1024, 512], "latent_dim": 128, "norm": True, "dropout": 0.2},
70-
}
71-
central_params = {"hidden_dim": [512, 256], "latent_dim": 128, "norm": True, "dropout": 0.2, "beta": 1}
72-
classif_params = {"n_class": 5, "lambda": 5.0, "hidden_layers": [128, 64], "dropout": 0.2}
73-
surv_params = {"lambda": 1.0, "dims": [64, 32], "activation": "SELU",
74-
"l2_reg": 1e-2, "norm": True, "dropout": 0.2}
75-
train_params = {"switch": 10, "lr": 1e-3}
76-
77-
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
78-
```
79-
80-
#### 3. Train
81-
82-
``` python
83-
model = CustOMICS(
84-
source_params=source_params,
85-
central_params=central_params,
86-
classif_params=classif_params,
87-
surv_params=surv_params,
88-
train_params=train_params,
89-
device=device,
90-
)
91-
model.fit(
92-
omics_train=omics_train,
93-
clinical_df=clinical_df,
94-
label="PAM50", # classification target column
95-
event="OS", # survival event column (0/1)
96-
surv_time="OS.time", # survival time column
97-
omics_val=omics_val, # optional validation set
98-
batch_size=32,
99-
n_epochs=30,
100-
verbose=True,
101-
)
102-
```
103-
104-
#### 4. Evaluate
105-
Classification metrics (Accuracy, F1, AUC, …)
106-
107-
``` python
108-
metrics = model.evaluate(
109-
omics_test, clinical_df,
110-
label="PAM50", event="OS", surv_time="OS.time",
111-
task="classification",
112-
)
113-
# Survival concordance index
114-
ci = model.evaluate(
115-
omics_test, clinical_df,
116-
label="PAM50", event="OS", surv_time="OS.time",
117-
task="survival",
118-
)
119-
```
120-
121-
#### 4. Visualise & explain
122-
123-
``` python
124-
model.plot_loss()
125-
model.plot_representation(omics_train, clinical_df, label="PAM50",
126-
filename="latent_space", title="t-SNE of latent space")
127-
model.stratify(omics_train, clinical_df, event="OS", surv_time="OS.time")
128-
model.explain(sample_ids, omics_train, clinical_df,
129-
source="rna", subtype="Her2", label="PAM50")
130-
```
131-
13242
## Next steps
13343

13444
See our main tutorial [here](tutorials/usage) for more details, or our [API](api/train/).

0 commit comments

Comments
 (0)