Compare DMS data to natural sequences¶
In [1]:
# Imports
import os
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
import numpy as np
from Bio import SeqIO, AlignIO
# Plotting colors
# re-arranged for plot
tol_muted_adjusted = [
"#AA4499",
"#88CCEE",
"#EE7733",
"#44AA99",
"#1f78b4",
"#CC6677",
"#117733",
"#999933",
"#DDCC77",
"#CC3311",
"#882255",
"#000000",
"#DDDDDD",
]
# Seaborn style settings
sns.set(rc={
"figure.dpi":300,
"savefig.dpi":300,
"svg.fonttype":"none",
})
sns.set_style("ticks")
sns.set_palette(tol_muted_adjusted)
In [2]:
# this cell is tagged as `parameters` for papermill parameterization
dms_scores = None
alignment = None
metadata = None
out_dir = None
In [3]:
# Parameters
dms_scores = "results/summaries/cell_entry_and_pH_stability.csv"
alignment = "non-pipeline_analyses/Arenavirus_nextstrain/Results/NW_GPC/Alignments/protein_ungapped_no_outgroup.fasta"
metadata = "non-pipeline_analyses/Arenavirus_nextstrain/Results/NW_GPC/metadata.tsv"
out_dir = "results/comparison_to_natural_seqs/"
In [4]:
# # Uncomment for running interactive
# dms_scores = "../results/summaries/cell_entry_and_pH_stability.csv"
# alignment = "../non-pipeline_analyses/Arenavirus_nextstrain/Results/NW_GPC/Alignments/protein_ungapped_no_outgroup.fasta"
# metadata = "../non-pipeline_analyses/Arenavirus_nextstrain/Results/NW_GPC/metadata.tsv"
# out_dir = "../results/comparison_to_natural_seqs/"
In [5]:
def get_natural_sequence_counts(site: int, amino_acid: str, sequences: pd.Series) -> pd.Series:
"""
Vectorized count of amino acid occurrences at a site across sequences.
Uses str accessor for fast element-wise comparison instead of Python loop.
"""
return (sequences.str[site - 1] == amino_acid).sum()
def add_seq_counts(dms_df, seqs_df, query_str, col_name):
"""
Query sequences and add counts of each mutation
"""
# Extract subset of sequences sequences
seqs = seqs_df.query(query_str)["sequence"]
seq_counts = {
site: seqs.str[site - 1].value_counts()
for site in dms_df["site"].unique()
}
dms_df[col_name] = dms_df.apply(
lambda x: seq_counts[x["site"]].get(x["mutant"], 0), axis=1
)
return dms_df.copy()
# Load alignment
natural_seqs_df = pd.DataFrame(
[{"strain": str(r.id), "sequence": str(r.seq)} for r in AlignIO.read(alignment, "fasta")]
)
# Load metadata and merge with sequences
metadata_df = pd.read_csv(metadata, sep="\t")
natural_seqs_df = natural_seqs_df.merge(
metadata_df, how="left", on="strain", validate="one_to_one"
)
# Extract DMS parental sequence
ROMERO_STRAIN = "Romero_P3235_MW227339-1_2017-03-16"
SEQ_LEN = 485
romero_sequence = natural_seqs_df.loc[
natural_seqs_df["strain"] == ROMERO_STRAIN, "sequence"
].iat[0]
# Create a wildtype df to fill in missing Josiah/wildtype measurements as 0
romero_df = pd.DataFrame({
"site": range(1, SEQ_LEN + 1),
"wildtype": list(romero_sequence),
"mutant": list(romero_sequence),
"entry in C musculinus TfR1 cells": 0,
"entry in C callosus TfR1 cells": 0,
"pH stability": 0,
"Romero reference": True,
})
# Load DMS scores
scores = pd.read_csv(dms_scores)
# Combine DMS data and reference data
AA_level_df = (
pd.concat([scores, romero_df], ignore_index=True)
.sort_values("site")
.fillna(False)
.reset_index(drop=True)
)
# Drop gaps and wildtype counts
AA_level_df = (
AA_level_df
.query("wildtype != mutant and mutant != '-'")
.reset_index(drop=True)
)
queries_dict = {
"candid_related == 'yes' and virus == 'Junin virus'" : "candid_related_counts",
"candid_related == 'no' and virus == 'Junin virus'" : "Junin_counts",
"virus == 'Machupo virus'" : "Machupo_counts",
"virus == 'Guanarito virus'" : "Guanarito_counts",
"virus == 'Chapare virus'" : "Chapare_counts",
"virus == 'Sabia virus'" : "Sabia_counts",
}
for query, name in queries_dict.items():
AA_level_df = add_seq_counts(
AA_level_df,
natural_seqs_df,
query,
name,
)
# Drop unseen mutants now, before the bin expansion, to keep frame small.
AA_level_df = (
AA_level_df
.query("candid_related_counts > 0 or Junin_counts > 0 or Machupo_counts > 0 or Guanarito_counts > 0 or Chapare_counts > 0 or Sabia_counts > 0")
.reset_index(drop=True)
)
# Make output dir if doesn't exist
if not os.path.exists(out_dir):
os.mkdir(out_dir)
AA_level_df.to_csv(out_dir + "all_arenavirus_GPC_mutations.csv", index=False)
AA_level_df.head(25)
/loc/scratch/10204897/ipykernel_496/780397715.py:63: FutureWarning: Downcasting object dtype arrays on .fillna, .ffill, .bfill is deprecated and will change in a future version. Call result.infer_objects(copy=False) instead. To opt-in to the future behavior, set `pd.set_option('future.no_silent_downcasting', True)`
.fillna(False)
Out[5]:
| site | wildtype | mutant | entry in C callosus TfR1 cells | entry in C musculinus TfR1 cells | pH stability | sequential_site | region | Romero reference | candid_related_counts | Junin_counts | Machupo_counts | Guanarito_counts | Chapare_counts | Sabia_counts | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 4 | F | L | 0.121 | 0.1559 | -0.004533 | 4.0 | SSP | False | 0 | 1 | 56 | 16 | 14 | 3 |
| 1 | 5 | I | V | -0.7275 | -0.5309 | -0.01279 | 5.0 | SSP | False | 0 | 0 | 1 | 0 | 14 | 0 |
| 2 | 5 | I | L | -0.121 | -0.133 | -0.00307 | 5.0 | SSP | False | 0 | 0 | 0 | 0 | 0 | 1 |
| 3 | 5 | I | F | -0.9645 | -0.8474 | -0.04738 | 5.0 | SSP | False | 0 | 0 | 0 | 0 | 0 | 2 |
| 4 | 8 | M | F | -0.08289 | 0.03622 | 0.01414 | 8.0 | SSP | False | 0 | 0 | 56 | 16 | 14 | 3 |
| 5 | 9 | Q | E | -0.4582 | -0.2721 | -0.002436 | 9.0 | SSP | False | 0 | 0 | 0 | 0 | 0 | 3 |
| 6 | 10 | E | D | -0.4249 | -0.3477 | -0.01656 | 10.0 | SSP | False | 0 | 0 | 0 | 16 | 0 | 0 |
| 7 | 11 | I | V | -1.107 | -0.6762 | -0.05926 | 11.0 | SSP | False | 0 | 0 | 0 | 0 | 0 | 3 |
| 8 | 13 | T | I | -0.1068 | -0.1987 | -0.03291 | 13.0 | SSP | False | 0 | 0 | 0 | 16 | 0 | 0 |
| 9 | 13 | T | N | -0.2939 | -0.3443 | -0.1045 | 13.0 | SSP | False | 0 | 0 | 0 | 0 | 14 | 3 |
| 10 | 13 | T | V | -0.6867 | -0.6824 | -0.01471 | 13.0 | SSP | False | 0 | 0 | 56 | 0 | 0 | 0 |
| 11 | 14 | F | I | -0.2889 | -0.0889 | -0.1203 | 14.0 | SSP | False | 0 | 0 | 0 | 0 | 14 | 3 |
| 12 | 15 | L | I | 0.345 | -0.0917 | -0.01441 | 15.0 | SSP | False | 0 | 0 | 0 | 0 | 14 | 3 |
| 13 | 15 | L | F | -0.5096 | -0.3799 | -0.0331 | 15.0 | SSP | False | 0 | 0 | 0 | 16 | 0 | 0 |
| 14 | 16 | Q | E | 0.6121 | 0.3272 | -0.0076 | 16.0 | SSP | False | 0 | 0 | 0 | 16 | 0 | 0 |
| 15 | 16 | Q | H | -1.836 | -1.804 | 0.0296 | 16.0 | SSP | False | 0 | 0 | 0 | 0 | 0 | 3 |
| 16 | 19 | L | I | -0.1448 | -0.0845 | -0.02416 | 19.0 | SSP | False | 0 | 0 | 0 | 0 | 14 | 3 |
| 17 | 21 | I | V | -0.387 | -0.2729 | -0.01739 | 21.0 | SSP | False | 0 | 0 | 0 | 16 | 0 | 0 |
| 18 | 24 | V | I | 0.2928 | 0.462 | 0.02468 | 24.0 | SSP | False | 0 | 0 | 0 | 0 | 14 | 3 |
| 19 | 24 | V | A | -0.4142 | -0.1876 | 0.001948 | 24.0 | SSP | False | 0 | 0 | 0 | 16 | 0 | 0 |
| 20 | 25 | A | V | -0.8714 | -1.08 | 0.06278 | 25.0 | SSP | False | 0 | 0 | 0 | 16 | 0 | 0 |
| 21 | 27 | S | T | -0.2138 | -0.2838 | -0.07993 | 27.0 | SSP | False | 0 | 0 | 0 | 16 | 0 | 0 |
| 22 | 29 | I | L | -0.4522 | -0.2621 | 0.01315 | 29.0 | SSP | False | 0 | 0 | 0 | 16 | 0 | 0 |
| 23 | 31 | I | V | -0.07335 | -0.1088 | -0.006591 | 31.0 | SSP | False | 0 | 0 | 61 | 1 | 0 | 0 |
| 24 | 31 | I | M | -0.5194 | -0.3058 | 0.01158 | 31.0 | SSP | False | 0 | 0 | 0 | 1 | 0 | 0 |
In [6]:
def plot_distributions(fig, axes, metrics, seq_status, out_dir, fig_name, color, region=None, ylim=None):
# Plot histplot for each metric
for index_1, seq in enumerate(seq_status):
for index_2, metric in enumerate(metrics):
# Remove NaN/inf values for cleaner plotting
data_to_plot = None
if region == None:
data_to_plot = AA_level_df.query(f"{seq} > 0")[metric]
else:
data_to_plot = AA_level_df.query(f"({seq} > 0) and (site >= {region[0]}) and (site <= {region[1]})")[metric]
binrange = None
if index_2 == 1:
binrange = (-1,0.25)
else:
binrange = (-6,1)# (-3,1)
sns.histplot(
data=data_to_plot,
ax=axes[index_2][index_1],
bins=30,
binrange=binrange,
stat="count",
color=color,
edgecolor="black",
alpha=0.5,
#shrink=0.8,
)
if ylim != None:
axes[index_2][index_1].set_ylim(ylim[index_2][0], ylim[index_2][1])
axes[index_2][index_1].set_title(f"{seq}", fontsize=8)
axes[index_2][index_1].set_xlabel(f"{metric}", fontsize=8)
axes[index_2][index_1].set_ylabel("Number of mutations with effect", fontsize=8)
axes[index_2][index_1].grid(False)
fig.tight_layout()
# Make output dir if doesn't exist
if not os.path.exists(out_dir):
os.mkdir(out_dir)
# Save figure
plt.savefig(out_dir + fig_name)
plt.show()
Plot full length GPC data
In [7]:
# Define the metrics to facet by
metrics = [
"entry in C musculinus TfR1 cells",
# "entry in C callosus TfR1 cells",
"pH stability",
]
seq_status = [
"Junin_counts",
"candid_related_counts",
# "Machupo_counts",
# "Guanarito_counts",
# "Chapare_counts",
# "Sabia_counts",
# "other_NW_counts",
]
# Create figure
fig, axes = plt.subplots(2, 2, figsize=(6, 6))
plot_distributions(fig, axes, metrics, seq_status, out_dir, "JUNV_Candid_full_length.svg", color="#000000", region=None)
NW arenavirus full length
In [8]:
# Define the metrics to facet by
metrics = [
"entry in C musculinus TfR1 cells",
# "entry in C callosus TfR1 cells",
"pH stability",
]
seq_status = [
# "Junin_counts",
# "candid_related_counts",
"Machupo_counts",
"Guanarito_counts",
"Chapare_counts",
"Sabia_counts",
# "other_NW_counts",
]
# Create figure
fig, axes = plt.subplots(2, 4, figsize=(12, 6), sharey="row")
plot_distributions(fig, axes, metrics, seq_status, out_dir, "NW_GPCs_full_length.svg", color="#000000", region=None, ylim=[[0,65], [0,145]])
NW SSP, GP1, and GP2
In [9]:
# Create figure
fig, axes = plt.subplots(2, 4, figsize=(12, 6), sharey="row")
plot_distributions(fig, axes, metrics, seq_status, out_dir, "NW_GPCs_SSP.svg", color=tol_muted_adjusted[0], region=[1,58], ylim=[[0,13], [0,21]])
In [10]:
fig, axes = plt.subplots(2, 4, figsize=(12, 6), sharey="row")
plot_distributions(fig, axes, metrics, seq_status, out_dir, "NW_GPCs_GP1.svg", color=tol_muted_adjusted[1], region=[59,251], ylim=[[0,35], [0,85]])
In [11]:
fig, axes = plt.subplots(2, 4, figsize=(12, 6), sharey="row")
plot_distributions(fig, axes, metrics, seq_status, out_dir, "NW_GPCs_GP2.svg", color=tol_muted_adjusted[2], region=[252,485], ylim=[[0,21], [0,37]])
Create combined plot for each region
In [12]:
def plot_distributions_combined(fig, axes, metric, seq_status, out_dir, fig_name, color, region_list, ylim=None):
# Plot histplot for each metric
for index_1, seq in enumerate(seq_status):
for index_2, region in enumerate(region_list):
# Remove NaN/inf values for cleaner plotting
data_to_plot = None
if region == None:
data_to_plot = AA_level_df.query(f"{seq} > 0")[metric]
else:
data_to_plot = AA_level_df.query(f"({seq} > 0) and (site >= {region[0]}) and (site <= {region[1]})")[metric]
binrange = binrange = (-6,1)
sns.histplot(
data=data_to_plot,
ax=axes[index_2][index_1],
bins=30,
binrange=binrange,
stat="count",
color=color[index_2],
edgecolor="black",
alpha=0.5,
#shrink=0.8,
)
if ylim != None:
axes[index_2][index_1].set_ylim(ylim[index_2][0], ylim[index_2][1])
axes[index_2][index_1].set_title(f"{seq}", fontsize=8)
axes[index_2][index_1].set_xlabel(f"{metric}", fontsize=8)
axes[index_2][index_1].set_ylabel("Number of mutations with effect", fontsize=8)
axes[index_2][index_1].grid(False)
fig.tight_layout()
# Make output dir if doesn't exist
if not os.path.exists(out_dir):
os.mkdir(out_dir)
# Save figure
plt.savefig(out_dir + fig_name)
plt.show()
In [13]:
region_list = [
[1,58],
[59,251],
[252,485],
]
ylim_list = [
[0,13],
[0,36],
[0,21],
]
fig, axes = plt.subplots(3, 4, figsize=(12, 9), sharey="row", sharex="col")
plot_distributions_combined(fig, axes, "entry in C musculinus TfR1 cells", seq_status, out_dir, "NW_GPCs_combined.svg", tol_muted_adjusted[0:3], region_list, ylim_list)