phosx 0.1.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
phosx-0.1.3/PKG-INFO ADDED
@@ -0,0 +1,62 @@
1
+ Metadata-Version: 2.1
2
+ Name: phosx
3
+ Version: 0.1.3
4
+ Summary:
5
+ Author: Alessandro Lussana
6
+ Author-email: alussana@ebi.ac.uk
7
+ Requires-Python: >=3.10,<4.0
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.10
10
+ Classifier: Programming Language :: Python :: 3.11
11
+ Requires-Dist: h5py (>=3.9.0,<4.0.0)
12
+ Requires-Dist: pandas (>=2.0.3,<3.0.0)
13
+ Requires-Dist: seaborn (>=0.12.2,<0.13.0)
14
+ Description-Content-Type: text/markdown
15
+
16
+ # PhosX
17
+
18
+ Kinase activity inference from phosphosproteomics data based on substrate sequence specificity.
19
+
20
+ ## Installation
21
+
22
+ ### Build from source (requires [Poetry](https://python-poetry.org))
23
+
24
+ ```
25
+ poetry build
26
+ pip install dist/*.whl
27
+ ```
28
+
29
+ ### Install from [PyPI](https://pypi.org)
30
+
31
+ ```
32
+ pip install phosx
33
+ ```
34
+
35
+ ## Usage
36
+
37
+ ```
38
+ phosx [-h] [-v] [-p PSSM] [-n N_PERMUTATIONS] [-c N_PROC] [--plot-figures] [-o OUTPUT_DIR] seqrnk
39
+ ```
40
+ ```
41
+ positional arguments:
42
+ seqrnk Path to the seqrnk file.
43
+
44
+ options:
45
+ -h, --help show this help message and exit
46
+ -v, --version Print package version and exit
47
+ -p PSSM, --pssm PSSM Path to the h5 file storing custom PSSMs; defaults to built-in PSSMs
48
+ -n N_PERMUTATIONS, --n-permutations N_PERMUTATIONS
49
+ Number of random permutations; defaults to 10000
50
+ -c N_PROC, --n-proc N_PROC
51
+ Number of cores used for multithreading; defaults to 1
52
+ --plot-figures Save figures in svg format; see also --output_dir
53
+ -o OUTPUT_DIR, --output-dir OUTPUT_DIR
54
+ Output files directory; only relevant if used with --plot_figures; defaults to 'phosx_output/'
55
+ ```
56
+
57
+ ## TODO
58
+
59
+ - [ ] **Filter temporary warnings** - when plotting figures from `phosx.pssm_enrichments.ks_statistic` and `phosx.pssm_enrichments.compute_ks_pvalues`, many warnings will be thrown (`UserWarning: The figure layout has changed to tight`). This is a Matplotlib known issue: [https://github.com/matplotlib/matplotlib/issues/26290](https://github.com/matplotlib/matplotlib/issues/26290)
60
+ - [x] **Create dirs** - if not existent, for output files
61
+ - [ ] **Add docs**
62
+ - [ ] **FDR** - Report also the FDR q-value in the output table
phosx-0.1.3/README.md ADDED
@@ -0,0 +1,47 @@
1
+ # PhosX
2
+
3
+ Kinase activity inference from phosphosproteomics data based on substrate sequence specificity.
4
+
5
+ ## Installation
6
+
7
+ ### Build from source (requires [Poetry](https://python-poetry.org))
8
+
9
+ ```
10
+ poetry build
11
+ pip install dist/*.whl
12
+ ```
13
+
14
+ ### Install from [PyPI](https://pypi.org)
15
+
16
+ ```
17
+ pip install phosx
18
+ ```
19
+
20
+ ## Usage
21
+
22
+ ```
23
+ phosx [-h] [-v] [-p PSSM] [-n N_PERMUTATIONS] [-c N_PROC] [--plot-figures] [-o OUTPUT_DIR] seqrnk
24
+ ```
25
+ ```
26
+ positional arguments:
27
+ seqrnk Path to the seqrnk file.
28
+
29
+ options:
30
+ -h, --help show this help message and exit
31
+ -v, --version Print package version and exit
32
+ -p PSSM, --pssm PSSM Path to the h5 file storing custom PSSMs; defaults to built-in PSSMs
33
+ -n N_PERMUTATIONS, --n-permutations N_PERMUTATIONS
34
+ Number of random permutations; defaults to 10000
35
+ -c N_PROC, --n-proc N_PROC
36
+ Number of cores used for multithreading; defaults to 1
37
+ --plot-figures Save figures in svg format; see also --output_dir
38
+ -o OUTPUT_DIR, --output-dir OUTPUT_DIR
39
+ Output files directory; only relevant if used with --plot_figures; defaults to 'phosx_output/'
40
+ ```
41
+
42
+ ## TODO
43
+
44
+ - [ ] **Filter temporary warnings** - when plotting figures from `phosx.pssm_enrichments.ks_statistic` and `phosx.pssm_enrichments.compute_ks_pvalues`, many warnings will be thrown (`UserWarning: The figure layout has changed to tight`). This is a Matplotlib known issue: [https://github.com/matplotlib/matplotlib/issues/26290](https://github.com/matplotlib/matplotlib/issues/26290)
45
+ - [x] **Create dirs** - if not existent, for output files
46
+ - [ ] **Add docs**
47
+ - [ ] **FDR** - Report also the FDR q-value in the output table
@@ -0,0 +1 @@
1
+ __version__ = '0.1.3'
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env python3
2
+
3
+ import argparse
4
+ from os import path
5
+ from phosx.pssm_enrichments import kinase_activities
6
+
7
+ def parse_phosx_args():
8
+ parser = argparse.ArgumentParser(
9
+ prog='phosx',
10
+ description='Kinase activity inference from phosphosproteomics data based on substrate sequence specificity',
11
+ epilog=''
12
+ )
13
+ parser.add_argument(
14
+ '-v', '--version',
15
+ action="version",
16
+ version="v0.1.0",
17
+ help='Print package version and exit'
18
+ )
19
+ parser.add_argument(
20
+ 'seqrnk',
21
+ type=str,
22
+ help='Path to the seqrnk file.'
23
+ )
24
+ parser.add_argument(
25
+ '-p', '--pssm',
26
+ type=str,
27
+ default=str(path.join(path.dirname(__file__), 'data/pssm.h5')),
28
+ help='Path to the h5 file storing custom PSSMs; defaults to built-in PSSMs'
29
+ )
30
+ parser.add_argument(
31
+ '-n', '--n-permutations',
32
+ type=int,
33
+ default=10000,
34
+ help='Number of random permutations; defaults to 10000'
35
+ )
36
+ parser.add_argument(
37
+ '-c', '--n-proc',
38
+ type=int,
39
+ default=1,
40
+ help='Number of cores used for multithreading; defaults to 1'
41
+ )
42
+ parser.add_argument(
43
+ '--plot-figures',
44
+ action='store_true',
45
+ help='Save figures in svg format; see also --output_dir'
46
+ )
47
+ parser.add_argument(
48
+ '-o', '--output-dir',
49
+ type=str,
50
+ default='phosx_output/',
51
+ help='Output files directory; only relevant if used with --plot_figures; defaults to \'phosx_output/\''
52
+ )
53
+ args = parser.parse_args()
54
+ return args
55
+
56
+ def main():
57
+ args = parse_phosx_args()
58
+ kinase_activities(
59
+ args.seqrnk,
60
+ args.pssm,
61
+ args.n_permutations,
62
+ args.n_proc,
63
+ args.plot_figures,
64
+ args.output_dir
65
+ )
66
+
67
+ if __name__ == '__main__':
68
+ main()
Binary file
@@ -0,0 +1,455 @@
1
+ #!/usr/bin/env python3
2
+
3
+ import os
4
+ import pandas as pd
5
+ import numpy as np
6
+ import h5py
7
+ from random import shuffle
8
+ from multiprocessing import Pool
9
+
10
+ AA_LIST = ['G','P','A','V','L','I','M','C','F','Y','W','H','K','R','Q','N','E','D','S','T']
11
+ POSITIONS_LIST = list(range(-5,5))
12
+
13
+ def scale_neg1_pos1(
14
+
15
+ series:pd.Series
16
+
17
+ ):
18
+
19
+ series = 2 * series / (series.max() - series.min())
20
+ series = series - series.min() - 1
21
+
22
+ return series
23
+
24
+ def scale_01(
25
+
26
+ series:pd.Series
27
+
28
+ ):
29
+
30
+ series = series / (series.max() - series.min())
31
+ series = series - series.min()
32
+
33
+ return series
34
+
35
+ def score_sequence(
36
+
37
+ seq_str:str,
38
+ pssm_df:pd.DataFrame
39
+
40
+ ):
41
+
42
+ if len(seq_str) != len(pssm_df):
43
+
44
+ raise Exception('Sequence length cannot be different from pssm length')
45
+
46
+ """ multiply probabilities instead of adding scores
47
+ p = 1
48
+
49
+ for i in range(len(seq_str)):
50
+
51
+ pos = list(pssm_df.index)[i]
52
+ p = p * pssm_df.loc[pos, seq_str[i]]
53
+ """
54
+ p = 0
55
+
56
+ if pssm_df.loc[0, seq_str[5]] != 0:
57
+
58
+ for i in range(len(seq_str)):
59
+
60
+ if seq_str[i] != '_':
61
+
62
+ pos = list(pssm_df.index)[i]
63
+ p = p + pssm_df.loc[pos, seq_str[i]]
64
+
65
+ return p
66
+
67
+ def pssm_scoring(
68
+
69
+ seq: str,
70
+ pssm_df_dict: dict
71
+
72
+ ):
73
+
74
+ record = {}
75
+
76
+ for kinase in pssm_df_dict.keys():
77
+
78
+ p = score_sequence(seq, pssm_df_dict[kinase])
79
+ record[kinase] = p
80
+
81
+ out_series = pd.Series(record)
82
+
83
+ return out_series
84
+
85
+ # take the largest deviation between test and null curves
86
+ def ks_statistic(
87
+
88
+ test_var_series:pd.Series,
89
+ null_var_df:pd.DataFrame,
90
+ plot_bool:bool=False,
91
+ out_plot_dir_str:str=''
92
+
93
+ ):
94
+
95
+ kinase = test_var_series.name
96
+
97
+ deltas = []
98
+ running_sum_test = 0
99
+ running_sum_null = 0
100
+
101
+ for i in range(len(test_var_series)):
102
+
103
+ running_sum_test = running_sum_test + test_var_series[i]
104
+ running_sum_null = running_sum_null + null_var_df[kinase][i]
105
+ deltas.append(running_sum_test - running_sum_null)
106
+
107
+ max_delta = max(deltas)
108
+ min_delta = min(deltas)
109
+
110
+ if plot_bool:
111
+
112
+ data = pd.Series(deltas)
113
+
114
+ import matplotlib.pyplot as plt
115
+ import seaborn as sns
116
+
117
+ if not os.path.exists(out_plot_dir_str):
118
+ os.makedirs(out_plot_dir_str)
119
+
120
+ plt.clf()
121
+ fig, ax = plt.subplots(figsize=(4, 3))
122
+
123
+ ax = sns.lineplot(
124
+ data=data,
125
+ linewidth=2.5
126
+ )
127
+
128
+ ax.set(
129
+ title=f'{kinase}',
130
+ xlabel='Phosphosite rank',
131
+ ylabel='Running Sum Statistic'
132
+ )
133
+
134
+ sns.despine()
135
+ plt.tight_layout()
136
+
137
+ plt.savefig(os.path.join(out_plot_dir_str, f'{kinase}.svg'))
138
+ plt.close()
139
+
140
+ if abs(max_delta) > abs(min_delta):
141
+
142
+ return max_delta
143
+
144
+ else:
145
+
146
+ return min_delta
147
+ """ # take the deviation at the end of the test and null curves
148
+ def ks_statistic(
149
+
150
+ test_var_series:pd.Series,
151
+ null_var_df:pd.DataFrame,
152
+ plot_bool:bool=False,
153
+ out_plot_dir_str:str=''
154
+
155
+ ):
156
+
157
+ kinase = test_var_series.name
158
+
159
+ running_sum_test = 0
160
+ running_sum_null = 0
161
+
162
+ running_sum_test_list = [0]
163
+ running_sum_null_list = [0]
164
+
165
+ for i in range(len(test_var_series)):
166
+
167
+ running_sum_test = running_sum_test + test_var_series[i]
168
+ running_sum_null = running_sum_null + null_var_df[kinase][i]
169
+
170
+ running_sum_test_list.append(running_sum_test)
171
+ running_sum_null_list.append(running_sum_null)
172
+
173
+ if plot_bool:
174
+
175
+ import matplotlib.pyplot as plt
176
+ import seaborn as sns
177
+
178
+ if not os.path.exists(out_plot_dir_str):
179
+ os.makedirs(out_plot_dir_str)
180
+
181
+ data = pd.DataFrame({
182
+ f'{kinase}': running_sum_test_list,
183
+ 'Null': running_sum_null_list
184
+ })
185
+
186
+ plt.clf()
187
+ fig, ax = plt.subplots(figsize=(4, 3))
188
+
189
+ ax = sns.lineplot(
190
+ data=pd.DataFrame(data),
191
+ linewidth=2.5
192
+ )
193
+
194
+ ax.set(
195
+ title=f'{kinase}',
196
+ xlabel='Phosphosite rank',
197
+ ylabel='Statistic'
198
+ )
199
+
200
+ sns.despine()
201
+ plt.tight_layout()
202
+
203
+ plt.savefig(os.path.join(out_plot_dir_str, f'{kinase}.svg'))
204
+ plt.close()
205
+
206
+ delta = running_sum_test - running_sum_null
207
+
208
+ return delta
209
+ """
210
+
211
+ def compute_null_ks(
212
+
213
+ pssm_scoring_df:pd.DataFrame,
214
+ seqrnk_null_scores_df:pd.DataFrame,
215
+ seqrnk_series:pd.Series,
216
+
217
+ ):
218
+
219
+ idx_list = list(range(len(seqrnk_series)))
220
+
221
+ # permute phosphosites
222
+ shuffle(idx_list)
223
+ shuffled_pssm_scoring_df = pssm_scoring_df.reindex(idx_list)
224
+ shuffled_pssm_scoring_df.index = list(range(len(seqrnk_series)))
225
+
226
+ # multiply the kinase pssm scores by the phosphosites seqrnk
227
+ shuffled_seqrnk_pssm_scores_df = shuffled_pssm_scoring_df.apply(
228
+ lambda x: x * seqrnk_series,
229
+ axis=0
230
+ )
231
+
232
+ # compute ks between shuffled pssm scores and null scores
233
+ ks_series = shuffled_seqrnk_pssm_scores_df.apply(
234
+ ks_statistic,
235
+ args=[seqrnk_null_scores_df, False],
236
+ axis=0
237
+ )
238
+
239
+ ks_df = pd.DataFrame([ks_series])
240
+
241
+ return ks_df
242
+
243
+ def compute_ks_empirical_distrib(
244
+
245
+ pssm_scoring_df:pd.DataFrame,
246
+ null_scoring_df:pd.DataFrame,
247
+ seqrnk_series:pd.Series,
248
+ n:int=1000,
249
+ n_proc:int=1
250
+
251
+ ):
252
+ # multiply the null pssm scores by the phosphosites seqrnk
253
+ seqrnk_null_scores_df = null_scoring_df.apply(
254
+ lambda x: x * seqrnk_series,
255
+ axis=0
256
+ )
257
+
258
+ arg1 = [pssm_scoring_df for i in range(n)]
259
+ arg2 = [seqrnk_null_scores_df for i in range(n)]
260
+ arg3 = [seqrnk_series for i in range(n)]
261
+
262
+ with Pool(processes=n_proc) as pool:
263
+ dfs_list = pool.starmap(compute_null_ks, zip(arg1, arg2, arg3))
264
+
265
+ out_df = pd.concat(dfs_list, axis=0)
266
+ out_df.index = list(range(len(out_df)))
267
+
268
+ return out_df
269
+
270
+ def compute_ks_pvalues(
271
+
272
+ ks_empirical_distrib_df: pd.DataFrame,
273
+ ks_series: pd.Series,
274
+ plot_bool:bool=False,
275
+ out_plot_dir_str:str=''
276
+
277
+ ):
278
+
279
+ if plot_bool:
280
+
281
+ import matplotlib.pyplot as plt
282
+ import seaborn as sns
283
+
284
+ if not os.path.exists(out_plot_dir_str):
285
+ os.makedirs(out_plot_dir_str)
286
+
287
+ ks_pvalue_series = pd.Series()
288
+ ks_pvalue_series.name = 'p value'
289
+
290
+ for kinase_str in ks_empirical_distrib_df.columns:
291
+
292
+ empirical_list = ks_empirical_distrib_df[kinase_str]
293
+
294
+ value_float = ks_series[kinase_str]
295
+
296
+ num_int = sum([x>value_float for x in empirical_list])
297
+ den_int = len(empirical_list)
298
+ pvalue = num_int / den_int
299
+
300
+ if pvalue > 0.5:
301
+ num_int = den_int - num_int
302
+ pvalue = num_int / den_int
303
+ ks_pvalue_series[kinase_str] = pvalue
304
+
305
+ if plot_bool:
306
+
307
+ plt.clf()
308
+ fig, ax = plt.subplots(figsize=(5, 3))
309
+
310
+ ax = sns.displot(
311
+ data=empirical_list,
312
+ height=3,
313
+ aspect=5/3
314
+ )
315
+
316
+ ax.set(
317
+ title=f'Empirical ks distribution for {kinase_str} ({len(empirical_list)} perm.)',
318
+ xlabel='KS',
319
+ ylabel='Count'
320
+ )
321
+
322
+ plt.axvline(x=value_float)
323
+
324
+ sns.despine()
325
+ plt.tight_layout()
326
+
327
+ plt.savefig(os.path.join(out_plot_dir_str, f'{kinase_str}_ks_dist.svg'))
328
+ plt.close()
329
+
330
+ return(ks_pvalue_series)
331
+
332
+ def read_pssms(
333
+
334
+ pssms_h5_file:str
335
+
336
+ ):
337
+
338
+ pssms_h5 = h5py.File(pssms_h5_file, 'r')
339
+ pssm_df_dict = {}
340
+
341
+ for kinase in pssms_h5.keys():
342
+
343
+ pssm_df_dict[kinase] = pd.DataFrame(pssms_h5[kinase])
344
+ pssm_df_dict[kinase].columns = AA_LIST
345
+ pssm_df_dict[kinase].index = POSITIONS_LIST
346
+
347
+ return pssm_df_dict
348
+
349
+ def read_seqrnk(
350
+
351
+ seqrnk_file:str
352
+
353
+ ):
354
+
355
+ seqrnk = pd.read_csv(seqrnk_file, sep='\t', header=None)
356
+ seqrnk.columns = ['Sequence', 'Score']
357
+
358
+ return seqrnk
359
+
360
+ def kinase_activities(
361
+
362
+ seqrnk_file:str,
363
+ pssms_h5_file:str,
364
+ n_perm:int=10000,
365
+ n_proc:int=1,
366
+ plot_figures:bool=False,
367
+ out_plot_dir:str='',
368
+
369
+ ):
370
+
371
+ pssm_df_dict = read_pssms(pssms_h5_file)
372
+ seqrnk = read_seqrnk(seqrnk_file)
373
+
374
+ # tmp: filter out Y phosphosites
375
+ seqrnk = seqrnk.loc[
376
+ [seqrnk['Sequence'][i][5] == 'S' or seqrnk['Sequence'][i][5] == 'T' for i in range(len(seqrnk))]
377
+ ]
378
+ seqrnk.index = range(len(seqrnk))
379
+
380
+ # score phosphosite sequences with each PSSM
381
+ seq_series = seqrnk['Sequence']
382
+ arg1 = [seq_series[i] for i in range(len(seq_series))]
383
+ arg2 = [pssm_df_dict for i in range(len(seq_series))]
384
+ with Pool(processes=n_proc) as pool:
385
+ dfs_list = pool.starmap(pssm_scoring, zip(arg1, arg2))
386
+ pssm_scoring_df = pd.concat(dfs_list, axis=1).T
387
+ pssm_scoring_df.index = list(range(len(seq_series)))
388
+
389
+ # scale the PSSM scores between 0 and 1 for each phosphosite
390
+ pssm_scoring_scaled01_df = pssm_scoring_df.apply(
391
+ scale_01,
392
+ axis=1
393
+ )
394
+
395
+ # obtain mean (null) pssm scores for each psssm
396
+ # TODO can be made just a Series (following code to be optimised)
397
+ pssm_null_scoring_df = pssm_scoring_scaled01_df.apply(
398
+ lambda x: pd.Series([x.mean()] * len(x), index=x.index),
399
+ axis=0
400
+ )
401
+
402
+ # compute empirical distribution of ks statistics
403
+ ks_empirical_distrib_df = compute_ks_empirical_distrib(
404
+ pssm_scoring_df=pssm_scoring_scaled01_df,
405
+ null_scoring_df=pssm_null_scoring_df,
406
+ seqrnk_series=seqrnk['Score'],
407
+ n=n_perm,
408
+ n_proc=n_proc
409
+ )
410
+
411
+ # multiply the pssm scores by the phosphosites seqrnk
412
+ seqrnk_pssm_scoring_row_scaled_df = pssm_scoring_scaled01_df.apply(
413
+ lambda x: x * seqrnk['Score'],
414
+ axis=0
415
+ )
416
+
417
+ # multiply the null scores by the phosphosites seqrnk
418
+ seqrnk_pssm_null_scoring_df = pssm_null_scoring_df.apply(
419
+ lambda x: x * seqrnk['Score'],
420
+ axis=0
421
+ )
422
+
423
+ # compute ks statistics
424
+ ks_series = seqrnk_pssm_scoring_row_scaled_df.apply(
425
+ ks_statistic,
426
+ args=[seqrnk_pssm_null_scoring_df, plot_figures, out_plot_dir],
427
+ axis=0
428
+ )
429
+ ks_series.name = 'KS'
430
+
431
+ # compute ks pvalues
432
+ ks_pvalue_series = compute_ks_pvalues(
433
+ ks_empirical_distrib_df=ks_empirical_distrib_df,
434
+ ks_series=ks_series,
435
+ plot_bool=plot_figures,
436
+ out_plot_dir_str=out_plot_dir
437
+ )
438
+
439
+ # compute FPR
440
+ results_df = pd.concat([ks_series, ks_pvalue_series], axis=1)
441
+ results_df['FPR p value'] = results_df['p value'] * len(results_df)
442
+ results_df['FPR p value'].loc[results_df['FPR p value'] > 1] = 1
443
+
444
+ # compute activity score as -log10(p value), signed with the sign of ks,
445
+ # capped based on smallest computable p value
446
+ activity_score_series = results_df['p value'].copy()
447
+ min_non0_fpr_float = min(activity_score_series.loc[activity_score_series != 0])
448
+ max_abs_activity_score_float = -np.log10(min_non0_fpr_float)
449
+ activity_score_series.loc[activity_score_series == 0] = np.nextafter(np.float32(0), np.float32(1))
450
+ activity_score_series = -np.log10(activity_score_series)
451
+ activity_score_series.loc[activity_score_series > max_abs_activity_score_float] = max_abs_activity_score_float
452
+ results_df['Activity Score'] = activity_score_series * results_df['KS'].apply(lambda x: 1 if x > 0 else -1)
453
+
454
+ # export results
455
+ print(results_df.to_csv(sep='\t', header=True, index=True))
@@ -0,0 +1,20 @@
1
+ [tool.poetry]
2
+ name = "phosx"
3
+ version = "0.1.3"
4
+ description = ""
5
+ authors = ["Alessandro Lussana <alussana@ebi.ac.uk>"]
6
+ readme = "README.md"
7
+ include = ["phosx/data/*"]
8
+
9
+ [tool.poetry.dependencies]
10
+ python = "^3.10"
11
+ h5py = "^3.9.0"
12
+ pandas = "^2.0.3"
13
+ seaborn = "^0.12.2"
14
+
15
+ [tool.poetry.scripts]
16
+ phosx = 'phosx.cli:main'
17
+
18
+ [build-system]
19
+ requires = ["poetry-core"]
20
+ build-backend = "poetry.core.masonry.api"