dspin 1.5.0__py3-none-any.whl

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.
dspin/__init__.py ADDED
@@ -0,0 +1,21 @@
1
+
2
+ from . import annotate
3
+ from . import compute
4
+ from . import plot
5
+ from . import dspin
6
+ from importlib.metadata import version
7
+
8
+ # Import main classes for easy access
9
+ from .dspin import DSPIN, GeneDSPIN, ProgramDSPIN, AbstractDSPIN
10
+
11
+ __all__ = [
12
+ 'DSPIN',
13
+ 'GeneDSPIN',
14
+ 'ProgramDSPIN',
15
+ 'AbstractDSPIN',
16
+ 'annotate',
17
+ 'compute',
18
+ 'plot'
19
+ ]
20
+
21
+ __version__ = version("DSPIN")
dspin/annotate.py ADDED
@@ -0,0 +1,461 @@
1
+ # -*-coding:utf-8 -*-
2
+ '''
3
+ @Time : 2024/04/02 14:29
4
+ @Author : Jialong Jiang
5
+ '''
6
+
7
+ import csv
8
+ import pandas as pd
9
+ from typing import List
10
+ import numpy as np
11
+ import os
12
+ import logging
13
+
14
+
15
+ def automatic_gpt_annotation(data_folder, file_name, gpt_model="gpt-4-0125-preview", temp=0.0, max_gene=30):
16
+ """
17
+ Main function to automate GPT annotation for a set of genes.
18
+ """
19
+ # Validate input file extension
20
+ assert file_name.endswith('.csv'), 'Input file must be a CSV file'
21
+
22
+ try:
23
+ import openai
24
+ except ImportError:
25
+ raise ImportError('Please install the required packages: openai')
26
+
27
+ # Setup and read data
28
+ gene_df, all_gene = read_data(data_folder, file_name)
29
+ num_list = gene_df.shape[1]
30
+
31
+ output_dir = data_folder + file_name.replace('.csv', '_gpt/')
32
+ context = "You are an efficient and insightful assistant to a molecular biologist"
33
+
34
+ os.makedirs(output_dir, exist_ok=True)
35
+
36
+ for ii in range(num_list):
37
+
38
+ cur_gene_list = gene_df[str(ii)]
39
+ cur_gene_list = cur_gene_list[~cur_gene_list.isna()][: max_gene]
40
+ cur_prompt = make_user_prompt_with_score(cur_gene_list)
41
+
42
+ response, fingerprint = openai_chat(
43
+ context, cur_prompt, gpt_model, temp)
44
+
45
+ output_file = output_dir + f'program_{ii}.txt'
46
+
47
+ with open(output_file, 'w') as f:
48
+ f.write(response)
49
+
50
+
51
+ def automatic_david_annotation(data_folder, file_name, token_name, species='human'):
52
+ """
53
+ Main function to automate DAVID annotation for a set of genes.
54
+ """
55
+ # Validate input file extension
56
+ assert file_name.endswith('.csv'), 'Input file must be a CSV file'
57
+
58
+ try:
59
+ import mygene
60
+ from suds.client import Client
61
+ import xlsxwriter
62
+ except ImportError:
63
+ raise ImportError(
64
+ 'Please install the required packages: mygene, suds, xlsxwriter')
65
+
66
+ # Setup and read data
67
+ all_onmf_df, all_gene = read_data(data_folder, file_name)
68
+ num_list = all_onmf_df.shape[1]
69
+
70
+ # Gene ID conversion
71
+ gene_id_map = convert_gene_ids(all_gene, species)
72
+
73
+ # Setup DAVID Web Service client
74
+ client = setup_david_client(token_name)
75
+
76
+ # Process each gene list
77
+ process_gene_lists(data_folder, file_name,
78
+ all_onmf_df, gene_id_map, client)
79
+
80
+ gene_id_map_reverse = {v: k for k, v in gene_id_map.items()}
81
+
82
+ # Save results to Excel
83
+ save_to_excel(data_folder, file_name,
84
+ gene_id_map_reverse, all_onmf_df.columns.tolist(), full=False)
85
+ save_to_excel(data_folder, file_name,
86
+ gene_id_map_reverse, all_onmf_df.columns.tolist(), full=True)
87
+
88
+
89
+ def make_user_prompt_with_score(genes, feature_df=[], direct=False, customized_prompt=None):
90
+ """
91
+ This function is adapted from "Evaluation of large language models for discovery of gene set function"
92
+ Arxiv: arxiv.org/abs/2309.04019v2
93
+ GitHub: github.com/idekerlab/llm_evaluation_for_gene_set_interpretation
94
+
95
+ Create a "one shot" ChatGPT prompt based on the list of genes.
96
+ :return: A string containing the ChatGPT prompt text
97
+ """
98
+
99
+ general_analysis_instructions = """
100
+ Be concise, do not use unnecessary words.
101
+ Be factual, do not editorialize.
102
+ Be specific, avoid overly general statements such as 'the proteins are involved in various cellular processes'.
103
+ Avoid listing facts about individual proteins. Instead, try to group proteins with similar functions and discuss their interplay, synergistyc or antagonistic effects and functional integration within the system.
104
+ Also avoid choosing generic process names such as 'Cellular Signaling and Regulation'.
105
+ If you cannot identify a prominent biological process for the proteins in the system, I want you to communicate this in you analysis and name the process: “System of unrelated proteins”. Provide a score of 0.00 for a "System of unrelated proteins".
106
+ """
107
+
108
+ task_instructions = """
109
+ Write a critical analysis of the biological processes performed by this system of interacting proteins.
110
+ Base your analysis on prior knowledge available in your training data.
111
+ After completing your analysis, propose a brief and detailed name for the most prominent biological process performed by the system.
112
+ """
113
+
114
+ score_instructions = """
115
+ After completing your analysis, please also assign a confidence score to the process name you selected.
116
+ This score should follow the name in parentheses and range from 0.00 to 1.00. A score of 0.00 indicates the lowest confidence,
117
+ while 1.00 reflects the highest confidence. This score helps gauge how accurately the chosen name represents the functions and activities
118
+ within the system of interacting proteins. When determining your score, consider the proportion of genes in the protein system that participate
119
+ in the identified biological process. For instance, if you select "Ribosome biogenesis" as the process name but only a few genes in the system
120
+ contribute to this process, the score should be lower compared to a scenario where a majority of the genes are involved in "Ribosome biogenesis".
121
+ """
122
+
123
+ direct_instructions = """
124
+ Propose a name and provide analysis for the following gene set.
125
+ """
126
+
127
+ format_placeholder = """
128
+ Put your chosen name at the top of the analysis as 'Process: <name>’.
129
+ """
130
+
131
+ example_analysis = """
132
+ To help you in your work, I am providing an example system of interacting proteins and the corresponding example analysis output.
133
+
134
+ The example system of interacting proteins is:
135
+ PDX1, SLC2A2, NKX6-1, GLP1, GCG.
136
+
137
+ The example analysis output is:
138
+
139
+ Process: Pancreatic development and glucose homeostasis (0.96)
140
+
141
+ 1. PDX1 is a homeodomain transcription factor involved in the specification of the early pancreatic epithelium and its subsequent differentiation.
142
+ It activates the transcription of several genes including insulin, somatostatin, glucokinase and glucose transporter type 2.
143
+ It is essential for maintenance of the normal hormone-producing phenotype in the pancreatic beta-cell.
144
+ In pancreatic acinar cells, forms a complex with PBX1b and MEIS2b and mediates the activation of the ELA1 enhancer.
145
+
146
+ 2. NKX6-1 is also a transcription factor involved in the development of pancreatic beta-cells during the secondary transition.
147
+ Together with NKX2-2 and IRX3, controls the generation of motor neurons in the neural tube and belongs to the neural progenitor
148
+ factors induced by Sonic Hedgehog (SHH) signals.
149
+
150
+ 3.GCG and GLP1, respectively glucagon and glucagon-like peptide 1, are involved in glucose metabolism and homeostasis.
151
+ GCG raises blood glucose levels by promoting gluconeogenesis and is the counter regulatory hormone of Insulin.
152
+ GLP1 is a potent stimulator of Glucose-Induced Insulin Secretion (GSIS). Plays roles in gastric motility and suppresses blood glucagon levels.
153
+ Promotes growth of the intestinal epithelium and pancreatic islet mass both by islet neogenesis and islet cell proliferation.
154
+
155
+ 4. SLC2A2, also known as GLUT2, is a facilitative hexose transporter. In hepatocytes, it mediates bi-directional transport of glucose accross the plasma membranes,
156
+ while in the pancreatic beta-cell, it is the main transporter responsible for glucose uptake and part of the cell's glucose-sensing mechanism.
157
+ It is involved in glucose transport in the small intestine and kidney too.
158
+
159
+ To summarize, the genes in this set are involved in the specification, differentiation, growth and functionality of the pancreas,
160
+ with a particular emphasis on the pancreatic beta-cell. Particularly, the architecture of the pancreatic islet ensures proper glucose sensing
161
+ and homeostasis via a number of different hormones and receptors that can elicit both synergistic and antagonistic effects in the pancreas itself and other peripheral tissues.
162
+ """
163
+
164
+ if direct == True:
165
+ prompt_text = direct_instructions
166
+ prompt_text += format_placeholder
167
+ elif customized_prompt:
168
+ prompt_text = customized_prompt
169
+ prompt_text += format_placeholder
170
+ else:
171
+ prompt_text = task_instructions
172
+ prompt_text += score_instructions
173
+ prompt_text += format_placeholder
174
+ prompt_text += general_analysis_instructions
175
+ prompt_text += example_analysis
176
+
177
+ prompt_text += "\n\nHere are the interacting proteins:\n"
178
+ prompt_text += f'\nProteins: '
179
+ prompt_text += ", ".join(genes) + ".\n\n"
180
+
181
+ if feature_df:
182
+ prompt_text += "\n\nHere are the gene features:\n"
183
+ prompt_text = add_gene_feature_summary(prompt_text, feature_df)
184
+
185
+ return prompt_text
186
+
187
+
188
+ def openai_chat(context, prompt, model, temperature, seed: int = None):
189
+
190
+ try:
191
+ import openai
192
+ except ImportError:
193
+ raise ImportError('Please install the required packages: openai')
194
+
195
+ try:
196
+ openai.api_key = os.environ['OPENAI_API_KEY']
197
+ except:
198
+ raise Exception(
199
+ "API key not found. Please set the OPENAI_API_KEY environment variable.")
200
+
201
+ backoff_time = 10 # Start backoff time at 10 second
202
+ max_tokens = 2000
203
+
204
+ response = openai.chat.completions.create(
205
+ model=model,
206
+ messages=[{"role": "system", "content": context},
207
+ {"role": "user", "content": prompt}],
208
+ max_tokens=max_tokens,
209
+ n=1,
210
+ stop=None,
211
+ seed=seed,
212
+ temperature=temperature,
213
+ )
214
+
215
+ # print(response)
216
+ # tokens_used = response["usage"]["total_tokens"]
217
+ tokens_used = response.usage.total_tokens
218
+ response_content = response.choices[0].message.content
219
+ system_fingerprint = response.system_fingerprint
220
+
221
+ return response_content, system_fingerprint
222
+
223
+
224
+ def read_data(data_folder, file_name):
225
+ """
226
+ Read gene data from a CSV file and return a unique list of gene names.
227
+
228
+ Parameters:
229
+ data_folder (str): The folder where the data file is located.
230
+ file_name (str): The name of the CSV file containing gene data.
231
+
232
+ Returns:
233
+ list: A unique list of gene names.
234
+ """
235
+ program_file_name = os.path.join(data_folder, file_name)
236
+ gene_df = pd.read_csv(program_file_name)
237
+ gene_list = gene_df.values.flatten().astype('str')
238
+ unique_genes = list(np.unique(gene_list))
239
+ if 'nan' in unique_genes:
240
+ unique_genes.remove('nan')
241
+ return gene_df, unique_genes
242
+
243
+
244
+ def convert_gene_ids(all_gene, species):
245
+ """
246
+ Convert gene symbols to ENSEMBL IDs using the MyGeneInfo service.
247
+
248
+ Parameters:
249
+ all_gene (list): A list of gene symbols.
250
+ species (str): The species for gene ID conversion (default is 'human').
251
+
252
+ Returns:
253
+ dict: A dictionary mapping gene symbols to ENSEMBL IDs.
254
+ """
255
+
256
+ try:
257
+ import mygene
258
+ except ImportError:
259
+ raise ImportError('Please install the required packages: mygene')
260
+
261
+ mg = mygene.MyGeneInfo()
262
+ result = mg.querymany(all_gene, scopes="symbol",
263
+ fields="ensembl.gene", species=species)
264
+ gene_id_map = {}
265
+ for gene_info in result:
266
+ if 'ensembl' in gene_info:
267
+ ensembl_info = gene_info['ensembl']
268
+ ensembl_id = ensembl_info['gene'] if isinstance(
269
+ ensembl_info, dict) else ensembl_info[0]['gene']
270
+ gene_id_map[gene_info['query']] = ensembl_id
271
+ return gene_id_map
272
+
273
+
274
+ def setup_david_client(token_name):
275
+ """
276
+ Setup and authenticate with the DAVID Web Service.
277
+
278
+ Parameters:
279
+ token_name (str): The authentication token for DAVID Web Service.
280
+
281
+ Returns:
282
+ Client: An authenticated suds client for DAVID Web Service.
283
+ """
284
+
285
+ from suds.client import Client
286
+
287
+ url = 'https://davidbioinformatics.nih.gov/webservice/services/DAVIDWebService?wsdl'
288
+ client = Client(url)
289
+ client.wsdl.services[0].setlocation(
290
+ 'https://davidbioinformatics.nih.gov/webservice/services/DAVIDWebService.DAVIDWebServiceHttpSoap11Endpoint/')
291
+ client.service.authenticate(token_name)
292
+ return client
293
+
294
+
295
+ def parse_david_output(term_clustering_report, save_path):
296
+ """
297
+ Parse the output from DAVID and save to a file.
298
+
299
+ Parameters:
300
+ term_clustering_report: The term clustering report from DAVID Web Service.
301
+ save_path (str): The file path to save the parsed output.
302
+ """
303
+ total_clusters = len(term_clustering_report)
304
+ print('Total clusters:', total_clusters)
305
+ with open(save_path, 'w') as f_out:
306
+ for i, cluster_record in enumerate(term_clustering_report, start=1):
307
+ enrichment_score = cluster_record.score
308
+ f_out.write(
309
+ f'Annotation Cluster {i}\tEnrichmentScore:{enrichment_score}\n')
310
+ headers = ['Category', 'Term', 'Count', '%', 'Pvalue', 'Genes', 'List Total',
311
+ 'Pop Hits', 'Pop Total', 'Fold Enrichment', 'Bonferroni', 'Benjamini', 'FDR']
312
+ f_out.write('\t'.join(headers) + '\n')
313
+ for chart_record in cluster_record.simpleChartRecords:
314
+ row = [
315
+ chart_record.categoryName,
316
+ chart_record.termName,
317
+ str(chart_record.listHits),
318
+ str(chart_record.percent),
319
+ str(chart_record.ease),
320
+ chart_record.geneIds,
321
+ str(chart_record.listTotals),
322
+ str(chart_record.popHits),
323
+ str(chart_record.popTotals),
324
+ str(chart_record.foldEnrichment),
325
+ str(chart_record.bonferroni),
326
+ str(chart_record.benjamini),
327
+ str(chart_record.afdr)
328
+ ]
329
+ f_out.write('\t'.join(row) + '\n')
330
+
331
+
332
+ def process_gene_lists(data_folder, file_name, all_onmf_df, gene_id_map, client):
333
+ """
334
+ Process each gene list, communicate with DAVID, and parse the results.
335
+
336
+ Parameters:
337
+ data_folder (str): The folder where the data files are located.
338
+ file_name (str): The name of the original CSV file containing gene data.
339
+ all_onmf_df (pd.DataFrame): A DataFrame containing gene lists.
340
+ gene_id_map (dict): A dictionary mapping gene symbols to ENSEMBL IDs.
341
+ client (Client): An authenticated suds client for DAVID Web Service.
342
+ """
343
+
344
+ def map_gene_ids(gene_id_map, gene_list):
345
+ return gene_list.map(gene_id_map).fillna(gene_list)
346
+
347
+ num_lists = all_onmf_df.shape[1]
348
+ save_folder = os.path.join(
349
+ data_folder, file_name.replace('.csv', '_david_results/'))
350
+ os.makedirs(save_folder, exist_ok=True)
351
+
352
+ overlap = 3
353
+ initialSeed = 3
354
+ finalSeed = 3
355
+ linkage = 0.5
356
+ kappa = 50
357
+
358
+ for i in range(num_lists):
359
+ cur_gene_list = all_onmf_df.iloc[:, i].dropna()
360
+ cur_gene_list = map_gene_ids(gene_id_map, cur_gene_list)
361
+ save_path = os.path.join(save_folder, f'list_{all_onmf_df.columns[i]}.csv')
362
+
363
+ input_ids = ','.join(cur_gene_list)
364
+ id_type = 'ENSEMBL_GENE_ID'
365
+ list_name = f'List_{i}'
366
+ list_type = 0
367
+
368
+ client.service.addList(input_ids, id_type, list_name, list_type)
369
+ term_clustering_report = client.service.getTermClusterReport(
370
+ overlap, initialSeed, finalSeed, linkage, kappa)
371
+
372
+ print(f'Gene list {all_onmf_df.columns[i]} ', end='\t')
373
+ # print(term_clustering_report)
374
+ if term_clustering_report is None:
375
+ raise Exception(
376
+ 'No results returned from DAVID, possibly wrong token name.')
377
+
378
+ parse_david_output(term_clustering_report, save_path)
379
+
380
+
381
+ def save_to_excel(data_folder, file_name, gene_id_map_reverse, list_name, full=True):
382
+ """
383
+ Save the parsed DAVID output to an Excel file, with an option to include full or filtered results.
384
+
385
+ Parameters:
386
+ data_folder (str): The folder where the data files are located.
387
+ file_name (str): The name of the original CSV file containing gene data.
388
+ gene_id_map_reverse (dict): A dictionary mapping ENSEMBL IDs to gene symbols.
389
+ list_name (list): A list of gene list names.
390
+ full (bool): Whether to save the full results. If False, only results with Pvalue <= 0.05 are saved.
391
+ """
392
+
393
+ def map_gene_id_reverse(gene_name):
394
+ try:
395
+ return gene_id_map_reverse[gene_name]
396
+ except:
397
+ return gene_name
398
+
399
+ def map_gene_id_reverse_list(gene_list):
400
+ try:
401
+ return ','.join([map_gene_id_reverse(gene) for gene in gene_list.split(', ')])
402
+ except:
403
+ return gene_list
404
+
405
+ def format_pvalue(pvalue):
406
+ try:
407
+ if isinstance(pvalue, str):
408
+ return '%.1e' % float(pvalue)
409
+ else:
410
+ return pvalue
411
+ except:
412
+ return pvalue
413
+
414
+ def format_percent(value):
415
+ try:
416
+ if isinstance(value, str):
417
+ return int(float(value))
418
+ else:
419
+ return value
420
+ except:
421
+ return value
422
+
423
+ def filt_fun(pvalue):
424
+ try:
425
+ return float(pvalue) > 0.05
426
+ except:
427
+ return False
428
+
429
+ save_folder = os.path.join(
430
+ data_folder, file_name.replace('.csv', '_david_results/'))
431
+ xlsx_name = file_name.replace(
432
+ '.csv', '_david_full.xlsx') if full else file_name.replace('.csv', '_david.xlsx')
433
+
434
+ with pd.ExcelWriter(data_folder + xlsx_name) as writer:
435
+ for lname in list_name:
436
+ cur_pd = pd.read_csv(save_folder + f'list_{lname}.csv', sep='\t', names=[
437
+ 'Category', 'Term', 'Count', '%', 'Pvalue', 'Genes', 'List Total', 'Pop Hits', 'Pop Total', 'Fold Enrichment', 'Bonferroni', 'Benjamini', 'FDR'])
438
+ filt1 = cur_pd['Category'] == 'UP_SEQ_FEATURE'
439
+ filt2 = cur_pd['Pvalue'].apply(filt_fun)
440
+ if not full:
441
+ cur_pd = cur_pd[~(filt1 | filt2)]
442
+
443
+ cur_pd['Term'] = cur_pd['Term'].apply(lambda x: x.split('~')[- 1])
444
+ cur_pd['Genes'] = cur_pd['Genes'].apply(map_gene_id_reverse_list)
445
+ cur_pd['Pvalue'] = cur_pd['Pvalue'].apply(format_pvalue)
446
+ cur_pd['%'] = cur_pd['%'].apply(format_percent)
447
+
448
+ ind_insert = np.where(
449
+ cur_pd['Category'].str.startswith('Annotation Cluster'))[0]
450
+ if len(ind_insert) > 1:
451
+ for kk, ind in enumerate(ind_insert[1:]):
452
+ cur_pd = pd.DataFrame(np.insert(
453
+ cur_pd.values, ind + kk, values=[" "] * len(cur_pd.columns), axis=0), columns=cur_pd.columns)
454
+
455
+ sheet_name = lname
456
+ cur_pd.to_excel(writer, sheet_name=sheet_name, index=False)
457
+ writer.sheets[sheet_name].set_column(0, 0, 25)
458
+ writer.sheets[sheet_name].set_column(1, 1, 50)
459
+ writer.sheets[sheet_name].set_column(2, 2, 5)
460
+ writer.sheets[sheet_name].set_column(3, 3, 5)
461
+ writer.sheets[sheet_name].set_column(5, 5, 100)