IAmKarthik's picture
Deploy ESM-2, MoLFormer, and affinity ONNX application
245bd92 verified
Raw
History Blame Contribute Delete
6.04 kB
from __future__ import annotations
import argparse
import hashlib
import json
from dataclasses import asdict, dataclass
from pathlib import Path
import numpy as np
import pandas as pd
COLUMNS = ("protein_sequence", "compound_smiles", "label")
AMINO_ACIDS = set("ACDEFGHIKLMNPQRSTUVWY")
@dataclass(frozen=True)
class DatasetProfile:
rows: int
unique_proteins: int
unique_compounds: int
unique_pairs: int
protein_length_min: int
protein_length_max: int
protein_length_mean: float
smiles_length_min: int
smiles_length_max: int
smiles_length_mean: float
label_min: float
label_max: float
label_mean: float
label_std: float
duplicate_pairs: int
def load_dataset(path: str | Path, nrows: int | None = None) -> pd.DataFrame:
frame = pd.read_csv(path, nrows=nrows)
missing_columns = set(COLUMNS) - set(frame.columns)
if missing_columns:
raise ValueError(f"Missing required columns: {sorted(missing_columns)}")
frame = frame.loc[:, COLUMNS].copy()
if frame.isna().any().any():
counts = frame.isna().sum()
raise ValueError(f"Missing values found: {counts[counts > 0].to_dict()}")
frame["protein_sequence"] = frame["protein_sequence"].astype(str).str.strip().str.upper()
frame["compound_smiles"] = frame["compound_smiles"].astype(str).str.strip()
frame["label"] = pd.to_numeric(frame["label"], errors="raise").astype(np.float32)
invalid = frame["protein_sequence"].map(lambda sequence: bool(set(sequence) - AMINO_ACIDS))
if invalid.any():
raise ValueError(f"{int(invalid.sum())} protein sequences contain unsupported residues")
return frame
def profile_dataset(frame: pd.DataFrame) -> DatasetProfile:
protein_lengths = frame["protein_sequence"].str.len()
smiles_lengths = frame["compound_smiles"].str.len()
unique_pairs = frame.drop_duplicates(["protein_sequence", "compound_smiles"]).shape[0]
return DatasetProfile(
rows=len(frame),
unique_proteins=frame["protein_sequence"].nunique(),
unique_compounds=frame["compound_smiles"].nunique(),
unique_pairs=unique_pairs,
protein_length_min=int(protein_lengths.min()),
protein_length_max=int(protein_lengths.max()),
protein_length_mean=float(protein_lengths.mean()),
smiles_length_min=int(smiles_lengths.min()),
smiles_length_max=int(smiles_lengths.max()),
smiles_length_mean=float(smiles_lengths.mean()),
label_min=float(frame["label"].min()),
label_max=float(frame["label"].max()),
label_mean=float(frame["label"].mean()),
label_std=float(frame["label"].std()),
duplicate_pairs=len(frame) - unique_pairs,
)
def _stable_fraction(value: str, seed: int) -> float:
digest = hashlib.sha256(f"{seed}:{value}".encode("utf-8")).digest()
return int.from_bytes(digest[:8], "big") / 2**64
def assign_splits(
frame: pd.DataFrame,
strategy: str = "cold_protein",
train_fraction: float = 0.8,
validation_fraction: float = 0.1,
seed: int = 42,
) -> pd.Series:
if train_fraction <= 0 or validation_fraction <= 0:
raise ValueError("Train and validation fractions must be positive")
if train_fraction + validation_fraction >= 1:
raise ValueError("Train plus validation fraction must be below 1")
if strategy == "cold_protein":
keys = frame["protein_sequence"]
elif strategy == "cold_compound":
keys = frame["compound_smiles"]
elif strategy == "pair":
keys = frame["protein_sequence"] + "|" + frame["compound_smiles"]
elif strategy == "random":
keys = pd.Series(np.arange(len(frame)).astype(str), index=frame.index)
else:
raise ValueError("strategy must be one of: cold_protein, cold_compound, pair, random")
fractions = keys.map(lambda value: _stable_fraction(str(value), seed))
splits = np.where(
fractions < train_fraction,
"train",
np.where(fractions < train_fraction + validation_fraction, "validation", "test"),
)
return pd.Series(splits, index=frame.index, name="split")
def make_sample(
source: str | Path,
destination: str | Path,
rows: int = 512,
seed: int = 42,
) -> pd.DataFrame:
frame = load_dataset(source)
if rows >= len(frame):
sample = frame
else:
bins = pd.qcut(frame["label"], q=min(10, rows), duplicates="drop")
sample = (
frame.assign(_bin=bins)
.groupby("_bin", observed=True, group_keys=False)
.apply(
lambda group: group.sample(
n=max(1, round(rows * len(group) / len(frame))),
random_state=seed,
),
include_groups=False,
)
.head(rows)
.loc[:, COLUMNS]
)
if len(sample) < rows:
remainder = frame.drop(index=sample.index).sample(
rows - len(sample),
random_state=seed,
)
sample = pd.concat([sample, remainder], ignore_index=True)
destination = Path(destination)
destination.parent.mkdir(parents=True, exist_ok=True)
sample.to_csv(destination, index=False)
return sample
def main() -> None:
parser = argparse.ArgumentParser(
description="Validate, profile, or sample the affinity dataset"
)
parser.add_argument("--data", default="data/train.csv")
parser.add_argument("--output", default="")
parser.add_argument("--sample-rows", type=int, default=0)
parser.add_argument("--seed", type=int, default=42)
args = parser.parse_args()
if args.sample_rows:
if not args.output:
parser.error("--output is required with --sample-rows")
frame = make_sample(args.data, args.output, args.sample_rows, args.seed)
else:
frame = load_dataset(args.data)
profile = asdict(profile_dataset(frame))
print(json.dumps(profile, indent=2))
if __name__ == "__main__":
main()