494 lines
22 KiB
Python
494 lines
22 KiB
Python
#%% load packages
|
|
import os,sys
|
|
import subprocess
|
|
import argparse
|
|
import requests
|
|
import re
|
|
import time
|
|
from bs4 import BeautifulSoup
|
|
import pandas as pd
|
|
from pandas.api.types import is_string_dtype
|
|
from pandas.api.types import is_numeric_dtype
|
|
import numpy as np
|
|
#from csv import reader
|
|
from mcsm import *
|
|
#==============================
|
|
#%% global variables for defs
|
|
#==============================
|
|
#%%
|
|
|
|
def format_data(data_file):
|
|
"""
|
|
Read file containing SNPs for mcsm analysis and remove duplicates
|
|
|
|
@param data_file csv file containing nsSNPs for given drug and gene.
|
|
csv file format:
|
|
single column with no headers with nsSNP format as below:
|
|
A1B
|
|
B2C
|
|
@type data_file: string
|
|
|
|
@return unique SNPs
|
|
@type list
|
|
"""
|
|
data = pd.read_csv(data_file, header = None, index_col = False)
|
|
data = data.drop_duplicates()
|
|
mutation_list = data[0].tolist()
|
|
# print(data.head())
|
|
return mutation_list
|
|
|
|
# FIXME: documentation
|
|
def request_calculation(pdb_file, mutation, chain, ligand_id, wt_affinity, prediction_url, output_dir, gene_name, host):
|
|
"""
|
|
Makes a POST request for a ligand affinity prediction.
|
|
|
|
@param pdb_file: valid path to pdb structure
|
|
@type string
|
|
|
|
@param mutation: single mutation of the format: {WT}<POS>{Mut}
|
|
@type string
|
|
|
|
@param chain: single-letter(caps)
|
|
@type chr
|
|
|
|
@param lig_id: 3-letter code (should match pdb file)
|
|
@type string
|
|
|
|
@param wt affinity: in nM
|
|
@type number
|
|
|
|
@param prediction_url: mcsm url for prediction
|
|
@type string
|
|
|
|
@return response object
|
|
@type object
|
|
"""
|
|
with open(pdb_file, "rb") as pdb_file:
|
|
files = {"wild": pdb_file}
|
|
body = {
|
|
"mutation": mutation,
|
|
"chain": chain,
|
|
"lig_id": ligand_id,
|
|
"affin_wt": wt_affinity
|
|
}
|
|
|
|
response = requests.post(prediction_url, files = files, data = body)
|
|
#print(response.status_code)
|
|
#result_status = response.raise_for_status()
|
|
if response.history:
|
|
# if result_status is not None: # doesn't work!
|
|
print('PASS: valid mutation submitted. Fetching result url')
|
|
|
|
#return response
|
|
url_match = re.search('/mcsm_lig/results_prediction/.+(?=")', response.text)
|
|
url = host + url_match.group()
|
|
#===============
|
|
# writing file: result urls
|
|
#===============
|
|
out_url_file = output_dir + '/' + gene_name.lower() + '_result_urls.txt'
|
|
myfile = open(out_url_file, 'a')
|
|
myfile.write(url + '\n')
|
|
myfile.close()
|
|
|
|
else:
|
|
print('ERROR: invalid mutation! Wild-type residue doesn\'t match pdb file.'
|
|
, '\nSkipping to the next mutation in file...')
|
|
#===============
|
|
# writing file: invalid mutations
|
|
#===============
|
|
out_error_file = output_dir + '/' + gene_name.lower() + '_errors.txt'
|
|
failed_muts = open(out_error_file, 'a')
|
|
failed_muts.write(mutation + '\n')
|
|
failed_muts.close()
|
|
|
|
#=======================================================================
|
|
def scrape_results(result_url):
|
|
"""
|
|
Extract results data using the result url
|
|
|
|
@params result_url: txt file containing result url
|
|
one per line for each mutation
|
|
@type string
|
|
|
|
returns: mcsm prediction results (raw)
|
|
@type chr
|
|
"""
|
|
result_response = requests.get(result_url)
|
|
# if results_response is not None:
|
|
# page = results_page.text
|
|
if result_response.status_code == 200:
|
|
print('Fetching results')
|
|
# extract results using the html parser
|
|
soup = BeautifulSoup(result_response.text, features = 'html.parser')
|
|
# print(soup)
|
|
web_result_raw = soup.find(class_ = 'span4').get_text()
|
|
#metatags = soup.find_all('meta')
|
|
metatags = soup.find_all('meta', attrs={'http-equiv':'refresh'})
|
|
#print('meta tags:', metatags)
|
|
if metatags:
|
|
print('WARNING: Submission not ready for URL:', result_url)
|
|
# TODO: Add logging
|
|
#if debug:
|
|
# debug.warning('submission not ready for URL:', result_url)
|
|
else:
|
|
return web_result_raw
|
|
else:
|
|
sys.exit('FAIL: Could not fetch results'
|
|
, '\nCheck if url is valid')
|
|
|
|
|
|
def build_result_dict(web_result_raw):
|
|
"""
|
|
Build dict of mcsm output for a single mutation
|
|
Format web results which is preformatted to enable building result dict
|
|
# preformatted string object: Problematic!
|
|
# make format consistent
|
|
|
|
@params web_result_raw: directly from html parser extraction
|
|
@type string
|
|
|
|
@returns result dict
|
|
@type {}
|
|
"""
|
|
# remove blank lines from web_result_raw
|
|
mytext = os.linesep.join([s for s in web_result_raw.splitlines() if s])
|
|
|
|
# affinity change and DUET stability change cols are are split over
|
|
# multiple lines and Mutation information is empty!
|
|
mytext = mytext.replace('ange:\n', 'ange: ')
|
|
#print(mytext)
|
|
|
|
# initiliase result_dict
|
|
result_dict = {}
|
|
for line in mytext.split('\n'):
|
|
fields = line.split(':')
|
|
#print(fields)
|
|
if len(fields) > 1: # since Mutaton information is empty
|
|
dict_entry = dict([(x, y) for x, y in zip(fields[::2], fields[1::2])])
|
|
result_dict.update(dict_entry)
|
|
print(result_dict)
|
|
return result_dict
|
|
#%%
|
|
#=======================================================================
|
|
def format_mcsm_output(mcsm_outputcsv):
|
|
"""
|
|
@param mcsm_outputcsv: file containing mcsm results for all muts
|
|
which is the result of build_result_dict() being called for each
|
|
mutation and then converting to a pandas df and output as csv.
|
|
@type string
|
|
|
|
@return formatted mcsm output
|
|
@type pandas df
|
|
|
|
"""
|
|
#############
|
|
# Read file
|
|
#############
|
|
mcsm_data_raw = pd.read_csv(mcsm_outputcsv, sep = ',')
|
|
|
|
# strip white space from both ends in all columns
|
|
mcsm_data = mcsm_data_raw.apply(lambda x: x.str.strip() if x.dtype == 'object' else x)
|
|
|
|
dforig_shape = mcsm_data.shape
|
|
print('dimensions of input file:', dforig_shape)
|
|
|
|
#############
|
|
# rename cols
|
|
#############
|
|
# format colnames: all lowercase, remove spaces and use '_' to join
|
|
print('Assigning meaningful colnames i.e without spaces and hyphen and reflecting units'
|
|
, '\n=======================================================')
|
|
my_colnames_dict = {'Predicted Affinity Change': 'PredAffLog' # relevant info from this col will be extracted and the column discarded
|
|
, 'Mutation information': 'mutationinformation' # {wild_type}<position>{mutant_type}
|
|
, 'Wild-type': 'wild_type' # one letter amino acid code
|
|
, 'Position': 'position' # number
|
|
, 'Mutant-type': 'mutant_type' # one letter amino acid code
|
|
, 'Chain': 'chain' # single letter (caps)
|
|
, 'Ligand ID': 'ligand_id' # 3-letter code
|
|
, 'Distance to ligand': 'ligand_distance' # angstroms
|
|
, 'DUET stability change': 'duet_stability_change'} # in kcal/mol
|
|
|
|
mcsm_data.rename(columns = my_colnames_dict, inplace = True)
|
|
#%%=====================================================================
|
|
#################################
|
|
# populate mutationinformation
|
|
# col which is currently blank
|
|
#################################
|
|
# populate mutationinformation column:mcsm style muts {WT}<POS>{MUT}
|
|
print('Populating column : mutationinformation which is currently empty\n', mcsm_data['mutationinformation'])
|
|
mcsm_data['mutationinformation'] = mcsm_data['wild_type'] + mcsm_data['position'].astype(str) + mcsm_data['mutant_type']
|
|
print('checking after populating:\n', mcsm_data['mutationinformation']
|
|
, '\n=======================================================')
|
|
|
|
# Remove spaces b/w pasted columns
|
|
print('removing white space within column: \mutationinformation')
|
|
mcsm_data['mutationinformation'] = mcsm_data['mutationinformation'].str.replace(' ', '')
|
|
print('Correctly formatted column: mutationinformation\n', mcsm_data['mutationinformation']
|
|
, '\n=======================================================')
|
|
#%%=====================================================================
|
|
#############
|
|
# sanity check: drop dupliate muts
|
|
#############
|
|
# shouldn't exist as this should be eliminated at the time of running mcsm
|
|
print('Sanity check:'
|
|
, '\nChecking duplicate mutations')
|
|
if mcsm_data['mutationinformation'].duplicated().sum() == 0:
|
|
print('PASS: No duplicate mutations detected (as expected)'
|
|
, '\nDim of data:', mcsm_data.shape
|
|
, '\n===================================================')
|
|
else:
|
|
print('WARNING: Duplicate mutations detected'
|
|
, '\nDim of df with duplicates:', mcsm_data.shape
|
|
, 'Removing duplicate entries')
|
|
mcsm_data = mcsm_data.drop_duplicates(['mutationinformation'])
|
|
print('Dim of data after removing duplicate muts:', mcsm_data.shape
|
|
, '\n===========================================================')
|
|
#%%=====================================================================
|
|
#############
|
|
# Create col: duet_outcome
|
|
#############
|
|
# classification based on DUET stability values
|
|
print('Assigning col: duet_outcome based on DUET stability values')
|
|
print('Sanity check:')
|
|
# count positive values in the DUET column
|
|
c = mcsm_data[mcsm_data['duet_stability_change']>=0].count()
|
|
DUET_pos = c.get(key = 'duet_stability_change')
|
|
# Assign category based on sign (+ve : Stabilising, -ve: Destabilising, Mind the spelling (British spelling))
|
|
mcsm_data['duet_outcome'] = np.where(mcsm_data['duet_stability_change']>=0, 'Stabilising', 'Destabilising')
|
|
print('DUET Outcome:', mcsm_data['duet_outcome'].value_counts())
|
|
#if DUET_pos == mcsm_data['duet_outcome'].value_counts()['Stabilising']:
|
|
# print('PASS: DUET outcome assigned correctly')
|
|
#else:
|
|
# print('FAIL: DUET outcome assigned incorrectly'
|
|
# , '\nExpected no. of stabilising mutations:', DUET_pos
|
|
# , '\nGot no. of stabilising mutations', mcsm_data['duet_outcome'].value_counts()['Stabilising']
|
|
# , '\n======================================================')
|
|
#%%=====================================================================
|
|
#############
|
|
# Extract numeric
|
|
# part of ligand_distance col
|
|
#############
|
|
# Extract only the numeric part from col: ligand_distance
|
|
# number: '-?\d+\.?\d*'
|
|
mcsm_data['ligand_distance']
|
|
print('extracting numeric part of col: ligand_distance')
|
|
mcsm_data['ligand_distance'] = mcsm_data['ligand_distance'].str.extract('(\d+\.?\d*)')
|
|
print('Ligand Distance:',mcsm_data['ligand_distance'])
|
|
#%%=====================================================================
|
|
#############
|
|
# Create 2 columns:
|
|
# ligand_affinity_change and ligand_outcome
|
|
#############
|
|
# the numerical and categorical parts need to be extracted from column: PredAffLog
|
|
# regex used
|
|
# numerical part: '-?\d+\.?\d*'
|
|
# categorocal part: '\b(\w+ing)\b'
|
|
print('Extracting numerical and categorical parts from the col: PredAffLog')
|
|
print('to create two columns: ligand_affinity_change and ligand_outcome'
|
|
, '\n=======================================================')
|
|
|
|
# 1) Extracting the predicted affinity change (numerical part)
|
|
mcsm_data['ligand_affinity_change'] = mcsm_data['PredAffLog'].str.extract('(-?\d+\.?\d*)', expand = True)
|
|
print(mcsm_data['ligand_affinity_change'])
|
|
|
|
# 2) Extracting the categorical part (Destabillizing and Stabilizing) using word boundary ('ing')
|
|
#aff_regex = re.compile(r'\b(\w+ing)\b')
|
|
mcsm_data['ligand_outcome']= mcsm_data['PredAffLog'].str.extract(r'(\b\w+ing\b)', expand = True)
|
|
print(mcsm_data['ligand_outcome'])
|
|
print(mcsm_data['ligand_outcome'].value_counts())
|
|
|
|
#############
|
|
# changing spelling: British
|
|
#############
|
|
# ensuring spellings are consistent
|
|
american_spl = mcsm_data['ligand_outcome'].value_counts()
|
|
print('Changing to Bristish spellings for col: ligand_outcome')
|
|
mcsm_data['ligand_outcome'].replace({'Destabilizing': 'Destabilising', 'Stabilizing': 'Stabilising'}, inplace = True)
|
|
print(mcsm_data['ligand_outcome'].value_counts())
|
|
british_spl = mcsm_data['ligand_outcome'].value_counts()
|
|
# compare series values since index will differ from spelling change
|
|
check = american_spl.values == british_spl.values
|
|
if check.all():
|
|
print('PASS: spelling change successfull'
|
|
, '\nNo. of predicted affinity changes:\n', british_spl
|
|
, '\n===================================================')
|
|
else:
|
|
sys.exit('FAIL: spelling change unsucessfull'
|
|
, '\nExpected:\n', american_spl
|
|
, '\nGot:\n', british_spl
|
|
, '\n===================================================')
|
|
#%%=====================================================================
|
|
#############
|
|
# ensuring corrrect dtype for numeric columns
|
|
#############
|
|
# check dtype in cols
|
|
print('Checking dtypes in all columns:\n', mcsm_data.dtypes
|
|
, '\n=======================================================')
|
|
print('Converting the following cols to numeric:'
|
|
, '\nligand_distance'
|
|
, '\nduet_stability_change'
|
|
, '\nligand_affinity_change'
|
|
, '\n=======================================================')
|
|
|
|
# using apply method to change stabilty and affinity values to numeric
|
|
numeric_cols = ['duet_stability_change', 'ligand_affinity_change', 'ligand_distance']
|
|
mcsm_data[numeric_cols] = mcsm_data[numeric_cols].apply(pd.to_numeric)
|
|
# check dtype in cols
|
|
print('checking dtype after conversion')
|
|
cols_check = mcsm_data.select_dtypes(include='float64').columns.isin(numeric_cols)
|
|
if cols_check.all():
|
|
print('PASS: dtypes for selected cols:', numeric_cols
|
|
, '\nchanged to numeric'
|
|
, '\n===================================================')
|
|
else:
|
|
sys.exit('FAIL:dtype change to numeric for selected cols unsuccessful'
|
|
, '\n===================================================')
|
|
print(mcsm_data.dtypes)
|
|
#%%=====================================================================
|
|
#############
|
|
# scale duet values
|
|
#############
|
|
# Rescale values in DUET_change col b/w -1 and 1 so negative numbers
|
|
# stay neg and pos numbers stay positive
|
|
duet_min = mcsm_data['duet_stability_change'].min()
|
|
duet_max = mcsm_data['duet_stability_change'].max()
|
|
|
|
duet_scale = lambda x : x/abs(duet_min) if x < 0 else (x/duet_max if x >= 0 else 'failed')
|
|
|
|
mcsm_data['duet_scaled'] = mcsm_data['duet_stability_change'].apply(duet_scale)
|
|
print('Raw duet scores:\n', mcsm_data['duet_stability_change']
|
|
, '\n---------------------------------------------------------------'
|
|
, '\nScaled duet scores:\n', mcsm_data['duet_scaled'])
|
|
#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
|
# additional check added
|
|
c2 = mcsm_data[mcsm_data['duet_scaled']>=0].count()
|
|
DUET_pos2 = c2.get(key = 'duet_scaled')
|
|
|
|
if DUET_pos == DUET_pos2:
|
|
print('\nPASS: DUET values scaled correctly')
|
|
else:
|
|
print('\nFAIL: DUET values scaled numbers MISmatch'
|
|
, '\nExpected number:', DUET_pos
|
|
, '\nGot:', DUET_pos2
|
|
, '\n======================================================')
|
|
#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
|
#%%=====================================================================
|
|
#############
|
|
# scale affinity values
|
|
#############
|
|
# rescale values in affinity change col b/w -1 and 1 so negative numbers
|
|
# stay neg and pos numbers stay positive
|
|
aff_min = mcsm_data['ligand_affinity_change'].min()
|
|
aff_max = mcsm_data['ligand_affinity_change'].max()
|
|
|
|
aff_scale = lambda x : x/abs(aff_min) if x < 0 else (x/aff_max if x >= 0 else 'failed')
|
|
|
|
mcsm_data['affinity_scaled'] = mcsm_data['ligand_affinity_change'].apply(aff_scale)
|
|
print('Raw affinity scores:\n', mcsm_data['ligand_affinity_change']
|
|
, '\n---------------------------------------------------------------'
|
|
, '\nScaled affinity scores:\n', mcsm_data['affinity_scaled'])
|
|
#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
|
# additional check added
|
|
c_lig = mcsm_data[mcsm_data['ligand_affinity_change']>=0].count()
|
|
Lig_pos = c_lig.get(key = 'ligand_affinity_change')
|
|
|
|
c_lig2 = mcsm_data[mcsm_data['affinity_scaled']>=0].count()
|
|
Lig_pos2 = c_lig2.get(key = 'affinity_scaled')
|
|
|
|
if Lig_pos == Lig_pos2:
|
|
print('\nPASS: Ligand affintiy values scaled correctly')
|
|
else:
|
|
print('\nFAIL: Ligand affinity values scaled numbers MISmatch'
|
|
, '\nExpected number:', Lig_pos
|
|
, '\nGot:', Lig_pos2
|
|
, '\n======================================================')
|
|
#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
|
|
|
#%%=====================================================================
|
|
#############
|
|
# adding column: wild_pos
|
|
# useful for plots and db
|
|
#############
|
|
print('Creating column: wild_pos')
|
|
mcsm_data['wild_pos'] = mcsm_data['wild_type'] + mcsm_data['position'].astype(str)
|
|
print(mcsm_data['wild_pos'].head())
|
|
# Remove spaces b/w pasted columns
|
|
print('removing white space within created column: wild_pos')
|
|
mcsm_data['wild_pos'] = mcsm_data['wild_pos'].str.replace(' ', '')
|
|
print('Correctly formatted column: wild_pos\n', mcsm_data['wild_pos'].head()
|
|
, '\n=========================================================')
|
|
#%%=====================================================================
|
|
#############
|
|
# adding column: wild_chain_pos
|
|
# useful for plots and db and its explicit
|
|
#############
|
|
print('Creating column: wild_chain_pos')
|
|
mcsm_data['wild_chain_pos'] = mcsm_data['wild_type'] + mcsm_data['chain'] + mcsm_data['position'].astype(str)
|
|
print(mcsm_data['wild_chain_pos'].head())
|
|
# Remove spaces b/w pasted columns
|
|
print('removing white space within created column: wild_chain_pos')
|
|
mcsm_data['wild_chain_pos'] = mcsm_data['wild_chain_pos'].str.replace(' ', '')
|
|
print('Correctly formatted column: wild_chain_pos\n', mcsm_data['wild_chain_pos'].head()
|
|
, '\n=========================================================')
|
|
#%%=====================================================================
|
|
#############
|
|
# ensuring corrrect dtype in non-numeric cols
|
|
#############
|
|
#) char cols
|
|
char_cols = ['PredAffLog', 'mutationinformation', 'wild_type', 'mutant_type', 'chain', 'ligand_id', 'duet_outcome', 'ligand_outcome', 'wild_pos', 'wild_chain_pos']
|
|
|
|
#mcsm_data[char_cols] = mcsm_data[char_cols].astype(str)
|
|
cols_check_char = mcsm_data.select_dtypes(include = 'object').columns.isin(char_cols)
|
|
|
|
if cols_check_char.all():
|
|
print('PASS: dtypes for char cols:', char_cols, 'are indeed string'
|
|
, '\n===================================================')
|
|
else:
|
|
sys.exit('FAIL:dtype change to numeric for selected cols unsuccessful'
|
|
, '\n===================================================')
|
|
#mcsm_data['ligand_distance', 'ligand_affinity_change'].apply(is_numeric_dtype(mcsm_data['ligand_distance', 'ligand_affinity_change']))
|
|
print(mcsm_data.dtypes)
|
|
#%%=====================================================================
|
|
# Removing PredAff log column as it is not needed?
|
|
print('Removing col: PredAffLog since relevant info has been extracted from it')
|
|
mcsm_data_f = mcsm_data.drop(columns = ['PredAffLog'])
|
|
#%%=====================================================================
|
|
# sort df by position for convenience
|
|
print('Sorting df by position')
|
|
mcsm_data_fs = mcsm_data_f.sort_values(by = ['position'])
|
|
print('sorted df:\n', mcsm_data_fs.head())
|
|
|
|
# Ensuring column names are lowercase before output
|
|
mcsm_data_fs.columns = mcsm_data_fs.columns.str.lower()
|
|
#%%=====================================================================
|
|
#############
|
|
# sanity check before writing file
|
|
#############
|
|
expected_ncols_toadd = 6 # beware hardcoding!
|
|
dforig_len = dforig_shape[1]
|
|
expected_cols = dforig_len + expected_ncols_toadd
|
|
if len(mcsm_data_fs.columns) == expected_cols:
|
|
print('PASS: formatting successful'
|
|
, '\nformatted df has expected no. of cols:', expected_cols
|
|
, '\n---------------------------------------------------'
|
|
, '\ncolnames:', mcsm_data_fs.columns
|
|
, '\n---------------------------------------------------'
|
|
, '\ndtypes in cols:', mcsm_data_fs.dtypes
|
|
, '\n---------------------------------------------------'
|
|
, '\norig data shape:', dforig_shape
|
|
, '\nformatted df shape:', mcsm_data_fs.shape
|
|
, '\n===================================================')
|
|
else:
|
|
print('FAIL: something went wrong in formatting df'
|
|
, '\nLen of orig df:', dforig_len
|
|
, '\nExpected number of cols to add:', expected_ncols_toadd
|
|
, '\nExpected no. of cols:', expected_cols, '(', dforig_len, '+', expected_ncols_toadd, ')'
|
|
, '\nGot no. of cols:', len(mcsm_data_fs.columns)
|
|
, '\nCheck formatting:'
|
|
, '\ncheck hardcoded value:', expected_ncols_toadd
|
|
, '\nis', expected_ncols_toadd, 'the no. of expected cols to add?'
|
|
, '\n===================================================')
|
|
sys.exit()
|
|
|
|
return mcsm_data_fs
|
|
|