Case 2:GLP-1 类多肽药物¶

如果我们知道一个天然蛋白/多肽的功能,如何通过改变序列使它成为更好的药物?

In [ ]:
## 1. 获取数据

获取 GLP-1序列 uniprot GLP1_HUMAN,  
Semaglutide https://pubchem.ncbi.nlm.nih.gov/compound/56843331
同时获取Exendin4, Liraglutide, Tirzepatide 的序列

将序列整理成fasta格式。
In [1]:
from pathlib import Path
from io import StringIO
import requests
from Bio import SeqIO
from Bio.Seq import Seq
from Bio.SeqRecord import SeqRecord

output_dir = Path("sequence_data")
structure_dir = output_dir / "pubchem_sdf"
output_dir.mkdir(exist_ok=True)
structure_dir.mkdir(exist_ok=True)
fasta_path = output_dir / "GLP1_and_drug_peptides.fasta"


def fetch_uniprot_fasta(accession):
    url = f"https://rest.uniprot.org/uniprotkb/{accession}.fasta"
    response = requests.get(url, timeout=30)
    response.raise_for_status()
    return SeqIO.read(StringIO(response.text), "fasta")


def download_pubchem_sdf(cid):
    """Download the PubChem chemical structure because PubChem has no peptide FASTA endpoint."""
    url = f"https://pubchem.ncbi.nlm.nih.gov/rest/pug/compound/cid/{cid}/SDF"
    response = requests.get(url, timeout=60)
    response.raise_for_status()
    sdf_path = structure_dir / f"CID{cid}.sdf"
    sdf_path.write_bytes(response.content)
    return sdf_path


def fetch_pubchem_title(cid):
    url = f"https://pubchem.ncbi.nlm.nih.gov/rest/pug/compound/cid/{cid}/property/Title/JSON"
    response = requests.get(url, timeout=30)
    response.raise_for_status()
    properties = response.json()["PropertyTable"]["Properties"][0]
    return properties.get("Title", f"PubChem CID {cid}")


# PubChem stores these compounds as chemically modified molecules, not FASTA.
# The sequences below are peptide backbones; X represents Aib. Lipid and terminal
# modifications are omitted from FASTA, while the original PubChem SDF is downloaded.
peptide_data = {
    "Semaglutide|CID56843331": {
        "cid": 56843331,
        "sequence": "HXEGTFTSDVSSYLEGQAAKEFIAWLV RGRG".replace(" ", ""),
        "description": "Semaglutide backbone; N-terminal Aib=X; Lys26 lipid and terminal modifications omitted",
    },
    "Exendin-4|Exenatide|CID45588096": {
        "cid": 45588096,
        "sequence": "HGEGTFTSDLSKQMEEEAVRLFIEWLKNGGPSSGAPPPS",
        "description": "Exendin-4/Exenatide backbone; C-terminal amidation omitted",
    },
    "Liraglutide|CID16134956": {
        "cid": 16134956,
        "sequence": "HAEGTFTSDVSSYLEGQAAKEEFIAWLVRGRG",
        "description": "Liraglutide backbone; Lys26 lipid and terminal modifications omitted",
    },
    "Tirzepatide|CID156588324": {
        "cid": 156588324,
        "sequence": "YXEGTFTSDYSIXLDKIAQKAFVQWLIAGGPSSGAPPPS",
        "description": "Tirzepatide backbone; Aib=X, lipid and terminal modifications omitted",
    },
}

# GLP1_HUMAN is represented by the human GCG precursor entry P01275.
# P01116 is KRAS_HUMAN and must not be used here.
glp1_record = fetch_uniprot_fasta("P01275")
glp1_record.id = "GLP1_HUMAN|P01275"
glp1_record.description = "human proglucagon precursor containing GLP-1; UniProt GLUC_HUMAN P01275"

records = [glp1_record]
for record_id, item in peptide_data.items():
    cid = item["cid"]
    try:
        sdf_path = download_pubchem_sdf(cid)
        pubchem_title = fetch_pubchem_title(cid)
        source_note = f"PubChem SDF downloaded to {sdf_path}"
    except requests.RequestException as error:
        pubchem_title = f"PubChem CID {cid}"
        source_note = f"PubChem download unavailable: {error}"
    records.append(
        SeqRecord(
            Seq(item["sequence"]),
            id=record_id,
            description=f"{item['description']}; {pubchem_title}; {source_note}",
        )
    )

with fasta_path.open("w", encoding="utf-8") as handle:
    SeqIO.write(records, handle, "fasta")

print(f"FASTA 文件已写出: {fasta_path}")
print(f"PubChem SDF 文件目录: {structure_dir}")
for record in records:
    print(f"{record.id}: {len(record.seq)} aa\n{record.seq}")
FASTA 文件已写出: sequence_data/GLP1_and_drug_peptides.fasta
PubChem SDF 文件目录: sequence_data/pubchem_sdf
GLP1_HUMAN|P01275: 180 aa
MKSIYFVAGLFVMLVQGSWQRSLQDTEEKSRSFSASQADPLSDPDQMNEDKRHSQGTFTSDYSKYLDSRRAQDFVQWLMNTKRNRNNIAKRHDEFERHAEGTFTSDVSSYLEGQAAKEFIAWLVKGRGRRDFPEEVAIVEELGRRHADGSFSDEMNTILDNLAARDFINWLIQTKITDRK
Semaglutide|CID56843331: 31 aa
HXEGTFTSDVSSYLEGQAAKEFIAWLVRGRG
Exendin-4|Exenatide|CID45588096: 39 aa
HGEGTFTSDLSKQMEEEAVRLFIEWLKNGGPSSGAPPPS
Liraglutide|CID16134956: 32 aa
HAEGTFTSDVSSYLEGQAAKEEFIAWLVRGRG
Tirzepatide|CID156588324: 39 aa
YXEGTFTSDYSIXLDKIAQKAFVQWLIAGGPSSGAPPPS

2. 多重序列比对,WebLogo可视化¶

In [4]:
from pathlib import Path
import shutil
import subprocess
from IPython.display import Image, display
from Bio import SeqIO
from Bio.Seq import Seq
from Bio.SeqRecord import SeqRecord

analysis_dir = Path("sequence_data")
input_fasta = analysis_dir / "GLP1_and_drug_peptides.fasta"
alignment_fasta = analysis_dir / "GLP1_drug_mafft_alignment.fasta"
logo_png = analysis_dir / "GLP1_drug_WebLogo.png"

if shutil.which("mafft") is None:
    raise RuntimeError("未找到 MAFFT。请在 biosoft 环境中安装:conda install -n biosoft -c bioconda mafft")

source_records = list(SeqIO.parse(input_fasta, "fasta"))

# P01275 is a precursor. Extract mature human GLP-1(7-37), not the 180-aa precursor.
glp1_precursor = next(record for record in source_records if record.id.startswith("GLP1_HUMAN"))
glp1_7_37_sequence = "HAEGTFTSDVSSYLEGQAAKEFIAWLVKGR"
if glp1_7_37_sequence not in str(glp1_precursor.seq):
    raise ValueError("没有在 UniProt P01275 前体中找到 GLP-1(7-37) 序列")

glp1_record = SeqRecord(
    Seq(glp1_7_37_sequence),
    id="GLP1_7-37_HUMAN",
    description="mature human GLP-1(7-37), extracted from UniProt P01275",
)

comparison_ids = [
    "Semaglutide",
    "Exendin-4",
    "Liraglutide",
    "Tirzepatide",
]
comparison_records = [glp1_record]
for name in comparison_ids:
    matching_records = [record for record in source_records if record.id.startswith(name)]
    if not matching_records:
        raise ValueError(f"输入 FASTA 中没有找到 {name}")
    record = matching_records[0]
    comparison_records.append(
        SeqRecord(Seq(str(record.seq)), id=name, description=record.description)
    )

comparison_fasta = analysis_dir / "GLP1_drug_sequences_for_alignment.fasta"
SeqIO.write(comparison_records, comparison_fasta, "fasta")

with alignment_fasta.open("w", encoding="utf-8") as output_handle:
    subprocess.run(
        ["mafft", "--auto", "--quiet", str(comparison_fasta)],
        check=True,
        stdout=output_handle,
        text=True,
    )

if shutil.which("weblogo") is None:
    raise RuntimeError("未找到 WebLogo。请在 biosoft 环境中安装:conda install -n biosoft -c bioconda weblogo")

subprocess.run(
    [
        "weblogo",
        "-f", str(alignment_fasta),
        "-o", str(logo_png),
        "-F", "png",
        "--title", "GLP-1 peptide sequence conservation",
        "--fineprint", "",
    ],
    check=True,
)

print(f"比对输入: {comparison_fasta}")
print(f"MAFFT 比对结果: {alignment_fasta}")
print(f"WebLogo 图片: {logo_png}")
display(Image(filename=str(logo_png)))

for record in SeqIO.parse(alignment_fasta, "fasta"):
    print(f"{record.id}: {record.seq}")
比对输入: sequence_data/GLP1_drug_sequences_for_alignment.fasta
MAFFT 比对结果: sequence_data/GLP1_drug_mafft_alignment.fasta
WebLogo 图片: sequence_data/GLP1_drug_WebLogo.png
No description has been provided for this image
GLP1_7-37_HUMAN: HAEGTFTSDVSSYLEGQAAK-EFIAWLVKGR---------
Semaglutide: HXEGTFTSDVSSYLEGQAAK-EFIAWLVRGRG--------
Exendin-4: HGEGTFTSDLSKQMEEEAVR-LFIEWLKNGGPSSGAPPPS
Liraglutide: HAEGTFTSDVSSYLEGQAAKEEFIAWLVRGRG--------
Tirzepatide: YXEGTFTSDYSIXLDKIAQK-AFVQWLIAGGPSSGAPPPS

3. 基于 PubChem SDF 和 RDKit 的多肽药物分子表面性质与理化性质分析¶

本部分基于 PubChem 下载的完整 SDF 结构,而不是仅使用 FASTA 主链序列,分析各多肽药物的表面性质和基本理化性质。sdf_* 列为 PubChem 写入 SDF 的原始属性,rdkit_* 列为 RDKit 根据分子结构重新计算的结果。

主要分析指标包括:

  • 电荷:sdf_total_charge 为 SDF 中记录的总形式电荷;rdkit_formal_charge_sum 为各原子形式电荷之和;rdkit_gasteiger_charge_min/max 和 rdkit_gasteiger_charge_abs_sum 描述基于结构计算的原子部分电荷分布。形式电荷 0 不代表生理 pH 下的实际净电荷。
  • 疏水性:sdf_xlogp3 和 rdkit_logP_XlogP3 是基于结构的 LogP 描述符,用于比较分子整体疏水性。对于脂肪酸修饰多肽,该指标还会受到脂质链影响。
  • 极性:sdf_tPSA_A2 和 rdkit_tPSA_A2 是拓扑极性表面积;数值越大,通常表示极性原子暴露和氢键相互作用能力越强,但不等同于实验溶解度。
  • 氢键能力:hydrogen_bond_donors 和 hydrogen_bond_acceptors 分别表示氢键供体和受体数量,可用于比较亲水性及潜在受体相互作用。
  • 分子大小:molecular_formula_from_sdf、分子量、精确质量、heavy_atom_count 和元素组成反映完整修饰分子的大小与组成。
  • 柔性和三维结构特征:rotatable_bonds 反映可旋转键数量,fraction_CSP3 反映饱和碳比例,aromatic_rings 统计芳香环数量,molar_refractivity 可作为体积和极化率相关描述符。
  • 结构复杂度:molecular_complexity 为 PubChem 的结构复杂度指标,可辅助比较不同药物分子的化学复杂程度。

这些指标适合进行结构描述和相对比较,不能单独替代 pH 依赖的净电荷、实验溶解度、聚集实验或体内药代动力学数据。

In [6]:
from pathlib import Path
import pandas as pd
from IPython.display import display
from rdkit import Chem
from rdkit.Chem import Crippen, Descriptors, Lipinski, rdMolDescriptors

analysis_dir = Path("sequence_data")
structure_dir = analysis_dir / "pubchem_sdf"
properties_csv = analysis_dir / "GLP1_drug_SDF_surface_properties.csv"

sdf_files = {
    "Semaglutide": structure_dir / "CID56843331.sdf",
    "Exendin-4": structure_dir / "CID45588096.sdf",
    "Liraglutide": structure_dir / "CID16134956.sdf",
    "Tirzepatide": structure_dir / "CID156588324.sdf",
}


def read_sdf_molecule(sdf_path):
    supplier = Chem.SDMolSupplier(str(sdf_path), removeHs=False, sanitize=True)
    molecule = next((mol for mol in supplier if mol is not None), None)
    if molecule is None:
        raise ValueError(f"无法从 SDF 读取分子: {sdf_path}")
    return molecule


def sdf_property(molecule, name, default=""):
    return molecule.GetProp(name) if molecule.HasProp(name) else default


def calculate_surface_properties(name, sdf_path):
    molecule = read_sdf_molecule(sdf_path)
    Chem.rdPartialCharges.ComputeGasteigerCharges(molecule)
    partial_charges = []
    for atom in molecule.GetAtoms():
        charge = atom.GetProp("_GasteigerCharge")
        if charge not in ("nan", "-nan"):
            partial_charges.append(float(charge))

    formal_charges = [atom.GetFormalCharge() for atom in molecule.GetAtoms()]
    element_counts = {}
    for atom in molecule.GetAtoms():
        element_counts[atom.GetSymbol()] = element_counts.get(atom.GetSymbol(), 0) + 1

    # SDF/PubChem fields are metadata written in the downloaded file.
    # RDKit fields below are recalculated from the molecular graph.
    row = {
        "drug": name,
        "sdf_file": str(sdf_path),
        "molecular_formula_from_sdf": sdf_property(molecule, "PUBCHEM_MOLECULAR_FORMULA"),
        "sdf_total_charge": sdf_property(molecule, "PUBCHEM_TOTAL_CHARGE"),
        "sdf_molecular_weight_Da": sdf_property(molecule, "PUBCHEM_MOLECULAR_WEIGHT"),
        "sdf_exact_mass_Da": sdf_property(molecule, "PUBCHEM_EXACT_MASS"),
        "sdf_xlogp3": sdf_property(molecule, "PUBCHEM_XLOGP3_AA"),
        "sdf_tPSA_A2": sdf_property(molecule, "PUBCHEM_CACTVS_TPSA"),
        "rdkit_formal_charge_sum": sum(formal_charges),
        "rdkit_nonzero_formal_charge_atoms": sum(charge != 0 for charge in formal_charges),
        "rdkit_gasteiger_charge_min": min(partial_charges),
        "rdkit_gasteiger_charge_max": max(partial_charges),
        "rdkit_gasteiger_charge_abs_sum": sum(abs(charge) for charge in partial_charges),
        "rdkit_molecular_weight_Da": Descriptors.MolWt(molecule),
        "rdkit_exact_molecular_weight_Da": Descriptors.ExactMolWt(molecule),
        "rdkit_logP_XlogP3": Crippen.MolLogP(molecule),
        "rdkit_tPSA_A2": rdMolDescriptors.CalcTPSA(molecule),
        "hydrogen_bond_donors": Lipinski.NumHDonors(molecule),
        "hydrogen_bond_acceptors": Lipinski.NumHAcceptors(molecule),
        "heavy_atom_count": molecule.GetNumHeavyAtoms(),
        "rotatable_bonds": Lipinski.NumRotatableBonds(molecule),
        "aromatic_rings": rdMolDescriptors.CalcNumAromaticRings(molecule),
        "fraction_CSP3": rdMolDescriptors.CalcFractionCSP3(molecule),
        "molar_refractivity": Crippen.MolMR(molecule),
        "molecular_complexity": sdf_property(molecule, "PUBCHEM_CACTVS_COMPLEXITY"),
        "element_counts": "; ".join(f"{element}:{count}" for element, count in sorted(element_counts.items())),
    }
    return row


property_rows = []
for drug_name, sdf_path in sdf_files.items():
    if not sdf_path.exists():
        raise FileNotFoundError(f"找不到 SDF 文件,请先运行数据获取单元: {sdf_path}")
    property_rows.append(calculate_surface_properties(drug_name, sdf_path))

surface_properties = pd.DataFrame(property_rows).set_index("drug")
surface_properties.to_csv(properties_csv, encoding="utf-8-sig")

print(f"SDF 表面性质结果已保存: {properties_csv}")
print("说明:sdf_* 列来自 PubChem SDF 属性;rdkit_* 列由 RDKit 根据 SDF 分子图重新计算。")
display(surface_properties.round(3))
SDF 表面性质结果已保存: sequence_data/GLP1_drug_SDF_surface_properties.csv
说明:sdf_* 列来自 PubChem SDF 属性;rdkit_* 列由 RDKit 根据 SDF 分子图重新计算。
sdf_file molecular_formula_from_sdf sdf_total_charge sdf_molecular_weight_Da sdf_exact_mass_Da sdf_xlogp3 sdf_tPSA_A2 rdkit_formal_charge_sum rdkit_nonzero_formal_charge_atoms rdkit_gasteiger_charge_min ... rdkit_tPSA_A2 hydrogen_bond_donors hydrogen_bond_acceptors heavy_atom_count rotatable_bonds aromatic_rings fraction_CSP3 molar_refractivity molecular_complexity element_counts
drug
Semaglutide sequence_data/pubchem_sdf/CID56843331.sdf C187H291N45O59 0 4114 4112.1187318 -5.8 1650 0 0 -0.508 ... 1646.18 57 63 291 156 6 0.610 1040.275 9590 C:187; H:291; N:45; O:59
Exendin-4 sequence_data/pubchem_sdf/CID45588096.sdf C184H282N50O60S 0 4187 4185.0306624 -21 1780 0 0 -0.481 ... 1749.75 58 66 295 144 5 0.614 1041.334 10300 C:184; H:282; N:50; O:60; S:1
Liraglutide sequence_data/pubchem_sdf/CID16134956.sdf C172H265N43O51 0 3751 3749.9498161 -3.4 1510 0 0 -0.508 ... 1513.76 54 55 266 137 6 0.593 955.167 8760 C:172; H:265; N:43; O:51
Tirzepatide sequence_data/pubchem_sdf/CID156588324.sdf C225H348N48O68 0 4813 4812.5315671 -6.8 1790 0 0 -0.508 ... 1789.63 58 70 341 174 6 0.636 1223.004 11700 C:225; H:348; N:48; O:68

4 rows × 25 columns

4. Protein Language Model 序列嵌入与 UMAP 可视化¶

使用 ESM-2 蛋白质语言模型将每条肽序列转换为固定长度 embedding,再用 UMAP 降维到二维。这里使用 esm2_t6_8M_UR50D 小型模型,适合短肽的初步比较。由于当前只有 5 条序列,UMAP 图用于探索相似性,不应作为稳定的统计聚类结论。

将蛋白质序列输入大语言模型(如 ESM-2、ProtT5 等),模型会将每个氨基酸或整条蛋白质转化为一个高维连续向量(Embedding)。序列一致性是看字面上的“逐字对齐”,而是embedding看背后的“语义与功能内涵”。

In [3]:
from pathlib import Path
import os
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import torch
from IPython.display import display
from transformers import AutoModel, AutoTokenizer
from umap import UMAP
from Bio import SeqIO

analysis_dir = Path("sequence_data")
input_fasta = analysis_dir / "GLP1_drug_sequences_for_alignment.fasta"
embedding_npy = analysis_dir / "GLP1_drug_ESM2_embeddings.npy"
embedding_csv = analysis_dir / "GLP1_drug_ESM2_embeddings.csv"
umap_csv = analysis_dir / "GLP1_drug_ESM2_UMAP_coordinates.csv"
umap_png = analysis_dir / "GLP1_drug_ESM2_UMAP.png"
model_name = "facebook/esm2_t6_8M_UR50D"

# Hugging Face model downloads use this local proxy.
proxy_url = "http://127.0.0.1:7897"
os.environ["HTTP_PROXY"] = proxy_url
os.environ["HTTPS_PROXY"] = proxy_url
os.environ["ALL_PROXY"] = proxy_url
os.environ["NO_PROXY"] = "127.0.0.1,localhost"

records = list(SeqIO.parse(input_fasta, "fasta"))
sequence_names = [record.id for record in records]
sequences = [str(record.seq).replace("-", "") for record in records]

print(f"加载模型: {model_name}")
print(f"模型下载代理: {proxy_url}")
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModel.from_pretrained(model_name)
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = model.to(device).eval()

# Mean-pool residue representations while excluding ESM special tokens.
encoded = tokenizer(
    sequences,
    return_tensors="pt",
    padding=True,
    truncation=True,
    add_special_tokens=True,
)
encoded = {key: value.to(device) for key, value in encoded.items()}
with torch.no_grad():
    hidden_states = model(**encoded).last_hidden_state

residue_mask = encoded["attention_mask"][:, 1:-1].unsqueeze(-1).float()
residue_states = hidden_states[:, 1:-1, :]
embeddings = (residue_states * residue_mask).sum(dim=1) / residue_mask.sum(dim=1).clamp_min(1.0)
embeddings = embeddings.cpu().numpy()

np.save(embedding_npy, embeddings)
pd.DataFrame(embeddings, index=sequence_names).to_csv(embedding_csv, encoding="utf-8-sig")

n_samples = len(embeddings)
if n_samples < 3:
    raise ValueError("UMAP 至少需要 3 条序列")

umap_model = UMAP(
    n_neighbors=min(5, n_samples - 1),
    min_dist=0.2,
    metric="cosine",
    init="random",
    random_state=42,
)
umap_coordinates = umap_model.fit_transform(embeddings)
umap_table = pd.DataFrame(
    umap_coordinates,
    columns=["UMAP1", "UMAP2"],
    index=sequence_names,
)
umap_table.index.name = "sequence"
umap_table.to_csv(umap_csv, encoding="utf-8-sig")

fig, ax = plt.subplots(figsize=(8, 6))
colors = plt.cm.tab10(np.linspace(0, 1, n_samples))
ax.scatter(umap_table["UMAP1"], umap_table["UMAP2"], c=colors, s=120, edgecolor="black")
for sequence_name, row in umap_table.iterrows():
    ax.annotate(sequence_name, (row["UMAP1"], row["UMAP2"]), xytext=(6, 6), textcoords="offset points")
ax.set_title("ESM-2 sequence embeddings projected by UMAP")
ax.set_xlabel("UMAP 1")
ax.set_ylabel("UMAP 2")
ax.grid(alpha=0.25)
fig.tight_layout()
fig.savefig(umap_png, dpi=300, bbox_inches="tight")
plt.show()

print(f"Embedding 数组: {embedding_npy}, shape={embeddings.shape}")
print(f"Embedding 表格: {embedding_csv}")
print(f"UMAP 坐标: {umap_csv}")
print(f"UMAP 图片: {umap_png}")
display(umap_table.round(4))
加载模型: facebook/esm2_t6_8M_UR50D
模型下载代理: http://127.0.0.1:7897
Warning: You are sending unauthenticated requests to the HF Hub. Please set a HF_TOKEN to enable higher rate limits and faster downloads.
Loading weights: 100%|██████████| 102/102 [00:00<00:00, 8089.00it/s]
[transformers] EsmModel LOAD REPORT from: facebook/esm2_t6_8M_UR50D
Key                       | Status     | 
--------------------------+------------+-
lm_head.layer_norm.bias   | UNEXPECTED | 
lm_head.bias              | UNEXPECTED | 
lm_head.dense.bias        | UNEXPECTED | 
lm_head.dense.weight      | UNEXPECTED | 
lm_head.layer_norm.weight | UNEXPECTED | 
pooler.dense.weight       | MISSING    | 
pooler.dense.bias         | MISSING    | 

Notes:
- UNEXPECTED:	can be ignored when loading from different task/architecture; not ok if you expect identical arch.
- MISSING:	those params were newly initialized because missing from the checkpoint. Consider training on your downstream task.
/home/wangzg/miniconda3/envs/biosoft/lib/python3.10/site-packages/umap/umap_.py:1952: UserWarning: n_jobs value 1 overridden to 1 by setting random_state. Use no seed for parallelism.
  warn(
No description has been provided for this image
Embedding 数组: sequence_data/GLP1_drug_ESM2_embeddings.npy, shape=(5, 320)
Embedding 表格: sequence_data/GLP1_drug_ESM2_embeddings.csv
UMAP 坐标: sequence_data/GLP1_drug_ESM2_UMAP_coordinates.csv
UMAP 图片: sequence_data/GLP1_drug_ESM2_UMAP.png
UMAP1 UMAP2
sequence
GLP1_7-37_HUMAN 35.555801 12.2658
Semaglutide 36.603699 12.2495
Exendin-4 34.750198 11.6009
Liraglutide 35.598202 11.1073
Tirzepatide 36.754398 11.2388
In [4]:
from pathlib import Path
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from IPython.display import display
from sklearn.decomposition import PCA
from sklearn.metrics.pairwise import cosine_similarity

analysis_dir = Path("sequence_data")
cosine_csv = analysis_dir / "GLP1_drug_ESM2_cosine_similarity.csv"
pca_csv = analysis_dir / "GLP1_drug_ESM2_PCA_coordinates.csv"
pca_png = analysis_dir / "GLP1_drug_ESM2_PCA.png"

# embeddings and sequence_names are generated in the previous ESM-2 cell.
if "embeddings" not in globals() or "sequence_names" not in globals():
    embedding_npy = analysis_dir / "GLP1_drug_ESM2_embeddings.npy"
    embedding_csv = analysis_dir / "GLP1_drug_ESM2_embeddings.csv"
    embeddings = np.load(embedding_npy)
    sequence_names = pd.read_csv(embedding_csv, index_col=0).index.tolist()

cosine_matrix = cosine_similarity(embeddings)
cosine_table = pd.DataFrame(
    cosine_matrix,
    index=sequence_names,
    columns=sequence_names,
)
cosine_table.to_csv(cosine_csv, encoding="utf-8-sig")

pca_model = PCA(n_components=2, random_state=42)
pca_coordinates = pca_model.fit_transform(embeddings)
pca_table = pd.DataFrame(
    pca_coordinates,
    columns=["PC1", "PC2"],
    index=sequence_names,
)
pca_table.index.name = "sequence"
pca_table.to_csv(pca_csv, encoding="utf-8-sig")

fig, ax = plt.subplots(figsize=(8, 6))
colors = plt.cm.tab10(np.linspace(0, 1, len(sequence_names)))
ax.scatter(pca_table["PC1"], pca_table["PC2"], c=colors, s=120, edgecolor="black")
for sequence_name, row in pca_table.iterrows():
    ax.annotate(sequence_name, (row["PC1"], row["PC2"]), xytext=(6, 6), textcoords="offset points")
ax.set_title(
    "ESM-2 embeddings projected by PCA "
    f"(PC1 {pca_model.explained_variance_ratio_[0]:.1%}, "
    f"PC2 {pca_model.explained_variance_ratio_[1]:.1%})"
)
ax.set_xlabel("PC1")
ax.set_ylabel("PC2")
ax.grid(alpha=0.25)
fig.tight_layout()
fig.savefig(pca_png, dpi=300, bbox_inches="tight")
plt.show()

print(f"余弦相似度矩阵: {cosine_csv}")
print(f"PCA 坐标: {pca_csv}")
print(f"PCA 图片: {pca_png}")
print("余弦相似度矩阵:")
display(cosine_table.round(4))
print("PCA 坐标:")
display(pca_table.round(4))
No description has been provided for this image
余弦相似度矩阵: sequence_data/GLP1_drug_ESM2_cosine_similarity.csv
PCA 坐标: sequence_data/GLP1_drug_ESM2_PCA_coordinates.csv
PCA 图片: sequence_data/GLP1_drug_ESM2_PCA.png
余弦相似度矩阵:
GLP1_7-37_HUMAN Semaglutide Exendin-4 Liraglutide Tirzepatide
GLP1_7-37_HUMAN 1.0000 0.9949 0.9762 0.9934 0.9640
Semaglutide 0.9949 1.0000 0.9743 0.9940 0.9721
Exendin-4 0.9762 0.9743 1.0000 0.9765 0.9565
Liraglutide 0.9934 0.9940 0.9765 1.0000 0.9604
Tirzepatide 0.9640 0.9721 0.9565 0.9604 1.0000
PCA 坐标:
PC1 PC2
sequence
GLP1_7-37_HUMAN 0.3209 0.2854
Semaglutide 0.1140 0.3535
Exendin-4 0.2971 -0.8928
Liraglutide 0.4135 0.2721
Tirzepatide -1.1454 -0.0182