pubmad.utils

View Source
  0import logging
  1from transformers import AutoTokenizer, AutoModelForSequenceClassification
  2import torch
  3from typing import List, Tuple
  4import requests
  5import networkx as nx
  6from itertools import product
  7from pubmad.types import Article, Entity
  8import matplotlib.pyplot as plt
  9import os
 10from nltk.tokenize import sent_tokenize
 11import pickle
 12from pathlib import Path
 13import time
 14from Bio import Entrez, Medline
 15from pyvis.network import Network
 16import re
 17import random
 18import numpy as np
 19import IPython
 20from math import ceil
 21
 22import nltk
 23nltk.download('punkt', quiet=True)
 24
 25# Number of sequences per batch
 26MAX_SEQ_PER_BATCH = 2
 27
 28print("Loading models...")
 29chemprot_model = AutoModelForSequenceClassification.from_pretrained(
 30    "pier297/autotrain-chemprot-re-838426740")
 31chemprot_tokenizer = AutoTokenizer.from_pretrained(
 32    "pier297/autotrain-chemprot-re-838426740")
 33
 34rel_tokenizer = AutoTokenizer.from_pretrained(
 35    "JacopoBandoni/BioBertRelationGenesDiseases")
 36
 37rel_model = AutoModelForSequenceClassification.from_pretrained(
 38    "JacopoBandoni/BioBertRelationGenesDiseases")
 39print('Finished loading models.')
 40
 41chemprot_model.eval()
 42rel_model.eval()
 43
 44device = 'cuda' if torch.cuda.is_available() else 'cpu'
 45rel_model = rel_model.to(device)
 46chemprot_model = chemprot_model.to(device)
 47
 48# logging.disable(logging.INFO) # disable INFO and DEBUG logging everywhere
 49# or
 50# disable WARNING, INFO and DEBUG logging everywhere
 51logging.disable(logging.WARNING)
 52
 53
 54def download_articles(title: str, start_year: int, end_year: int, max_results: int = 100, author: str = '', sort_by: str = 'relevance') -> List[Article]:
 55    """
 56    Download articles from PubMed using BioPython library.
 57    Args:
 58        query (str): The query to search for.
 59        start_year (int): The start year to search for.
 60        end_year (int): The end year to search for.
 61        max_results (int): The maximum number of results to return.
 62        author (str): The author to search for, leave empty to search for all authors.
 63        sort_by (str): The sort order to use. Can be 'relevance' to retrieve the most relevant results, or 'date' to sort by publication date. Defaults to 'relevance'.
 64    Returns:
 65        List[Article] A list of articles.
 66    """
 67    current_path = Path(os.getcwd()) / 'cache'
 68
 69    if not os.path.exists(current_path):
 70        os.mkdir(current_path)
 71
 72    file_name = '{}_{}_{}_{}.txt'.format(
 73        title, start_year, end_year, max_results)
 74    if os.path.exists(current_path / file_name):
 75        print("Loading from cache...")
 76        # TODO: clear_cache is not implemented yet
 77        with open(current_path / file_name, 'rb') as f:
 78            return pickle.load(f)
 79
 80    Entrez.email = 'pubmadbiosearch@gmail.com'
 81
 82    if author != "":
 83        query = '(' + title + ') AND (' + author + '[Author])'
 84    else:
 85        query = title
 86
 87    handle = Entrez.esearch(db='pubmed',
 88                            sort=sort_by,
 89                            retmax=max_results,
 90                            retmode='xml',
 91                            datetype='pdat',
 92                            mindate=str(start_year),
 93                            maxdate=str(end_year),
 94                            term=query)
 95    results = Entrez.read(handle)
 96    id_list = results['IdList']
 97    ids = ','.join(id_list)
 98    handle = Entrez.efetch(db='pubmed', rettype="medline",
 99                           id=ids, retmode='text')
100    results = Medline.parse(handle)
101    results = list(results)
102    articles = []
103    for article in results:
104        abstract = article.get("AB", "?")
105        title = article.get("TI", "?")
106        if abstract != "?" and title != "?":
107            if abstract.find("This article has been withdrawn at the request of the author(s) and/or editor") == -1 and \
108               abstract.find("An amendment to this paper has been published and can be accessed via a link at the top of the paper") == -1 and \
109               abstract.find("This corrects the article") == -1 and abstract.find("A Correction to this paaper has been published") == -1:
110                articles.append(Article(title=title, abstract=abstract,
111                                        pmid=article['PMID'], full_text='', publication_data=None))
112
113    print("Found {} articles".format(len(articles)))
114
115    # save articles to pickle file
116    with open(current_path / file_name, 'wb') as f:
117        pickle.dump(articles, f)
118    return articles
119
120
121def extract_entities_pmids(pmids: List[str]) -> List[List[Entity]]:
122    """Extract entities from one or more articles identified by the pmids provided
123
124    Args:
125        pmids (List[str]): list of pmids to extract entities from
126
127    Returns:
128        List[List[Entity]]: list of entities for each article
129    """
130
131    if len(pmids) == 0:
132        return []
133
134    # Query BERN2 server for entities.
135    extracted = False
136    bern_result = None
137
138    i = 0
139
140    while not extracted and i < 3:
141        try:
142            bern_result = query_pmid(pmids)
143            extracted = True
144        except Exception as e:
145            print("Error querying BERN2 server")
146            print("Sleep for 3 seconds and try again to not get banned by BERN2")
147            time.sleep(3)
148            i += 1
149            continue
150
151    entities = []
152
153    # Parse the result
154    for i, article in enumerate(bern_result):
155        entities_per_article = []
156        for entity in article["annotations"]:
157            if (entity['obj'] == 'disease' or entity['obj'] == 'gene' or entity['obj'] == 'drug'):
158                new_entity = Entity(mesh_id=entity['id'], mention=entity['mention'], type=entity['obj'],
159                                    prob=entity['prob'], span_begin=entity['span']['begin'], span_end=entity['span']['end'], pmid=pmids[i])
160                entities_per_article.append(new_entity)
161        entities.append(entities_per_article)
162
163    return entities
164
165
166def extract_entities(article: Article, source: str = 'abstract') -> List[Entity]:
167    """
168    Extract entities from an article using BERN2 (online).
169
170    Args:
171        article (Article): The article to extract entities from. 
172        source (str): The source to extract entities from. Can be 'abstract' or 'full_text'. Defaults to 'abstract'.
173
174    Returns:
175        List[Entity]: A list of entities.
176    """
177    # We call bern2 server for ner
178    text = ''
179    if source == 'abstract':
180        text = article.abstract
181    elif source == 'full_text':
182        text = article.full_text
183    else:
184        raise ValueError("Invalid source: {}".format(source))
185
186    # Query BERN2 server for entities.
187    extracted = False
188    bern_result = None
189
190    i = 0
191
192    while not extracted and i < 3:
193        try:
194            bern_result = query_plain(text)
195            extracted = True
196        except Exception as e:
197            print("Error querying BERN2 server")
198            print("Sleep for 3 seconds and try again to not get banned by BERN2")
199            time.sleep(3)
200            i += 1
201            continue
202
203    if i == 3:
204        return []
205
206    # Parse the result.
207    annotations = bern_result['annotations']
208    entities = []
209    for entity in annotations:
210        if (entity['obj'] == 'disease' or entity['obj'] == 'gene' or entity['obj'] == 'drug'):
211            pmid = article.pmid
212            if type(article.pmid) == list:
213                pmid = article.pmid[0]
214            new_entity = Entity(mesh_id=entity['id'], mention=entity['mention'], type=entity['obj'],
215                                prob=entity['prob'], span_begin=entity['span']['begin'], span_end=entity['span']['end'], pmid=pmid)
216            entities.append(new_entity)
217
218    return entities
219
220
221def display_graph(graph: nx.Graph, hide_isolated_nodes: bool = True, show: bool = True):
222    """
223    Display a graph.
224
225    Args:
226        graph (nx.Graph): The graph to display.
227        hide_isolated_nodes (bool): Whether to hide isolated nodes. Defaults to True.
228        show (bool): Whether to show the graph. Defaults to True. Otherwise it shows the window without stopping the program.
229    """
230    plt.figure()
231    if hide_isolated_nodes:
232        graph.remove_nodes_from(list(nx.isolates(graph)))
233
234    gene_nodes = [n for n, d in graph.nodes(data=True) if d['type'] == 'gene']
235    disease_nodes = [n for n, d in graph.nodes(
236        data=True) if d['type'] == 'disease']
237    other_nodes = [n for n, d in graph.nodes(
238        data=True) if d['type'] != 'gene' and d['type'] != 'disease']
239
240    pos = nx.spring_layout(graph, 100)
241
242    weights = nx.get_edge_attributes(graph, 'weight')
243
244    nx.draw_networkx_nodes(graph, pos, nodelist=gene_nodes,
245                           node_color='r', node_size=100, alpha=0.8, label='gene')
246    nx.draw_networkx_nodes(graph, pos, nodelist=disease_nodes,
247                           node_color='b', node_size=100, alpha=0.8, label='disease')
248    nx.draw_networkx_nodes(graph, pos, nodelist=other_nodes,
249                           node_color='g', node_size=100, alpha=0.8, label='drug')
250
251    nx.draw_networkx_labels(graph, pos, labels={
252                            n: d['mention'] for n, d in graph.nodes(data=True)}, font_size=10)
253
254    nx.draw_networkx_edges(graph, pos, width=1.0, alpha=0.5)
255    plt.axis('off')
256    plt.legend()
257    if show:
258        plt.show()
259    else:
260        plt.draw()
261
262
263def extract_naive_relations(entities: List[Entity]) -> List[Tuple[Entity, Entity, float]]:
264    '''
265    Extract relations from a list of entities.
266
267    Args:
268        entities (List[Entity]): A list of entities.
269
270    Returns:
271        List[Tuple[Entity, Entity, float]]: A list of relations.
272    '''
273    # Connect all the entities to each other.
274    relations = []
275    for entity1, entity2 in product(entities, entities):
276        if entity1.mesh_id != entity2.mesh_id:
277            # 1 meaning that the relation is present.
278            relations.append((entity1, entity2, 1))
279    return relations
280
281
282def extract_biobert_relations(article: Article, source: str = 'abstract', clear_cache: bool = False) -> Tuple[List[Entity], List[Tuple[Entity, Entity]], bool]:
283    """
284    Extract the entities from the article using BERN2 (online).
285    Then it calls BioBERT to extract relations between genes and diseases.
286
287    Args:
288        article (Article): The article to extract entities from.
289        source (str): The source to extract entities from. Can be 'abstract' or 'full_text'. Defaults to 'abstract'.
290        clear_cache (bool): Whether to clear the BioBERT cache. Defaults to False.
291
292    Returns:
293        Tuple[List[Entity], List[Tuple[Entity, Entity]], bool]: A tuple of entities, relations and whether the cache was used.
294
295    """
296    file_name = str(article.pmid)[:128] + '.txt'
297    file_name = file_name.replace('\n', '_')
298    path = Path(os.getcwd()) / 'cache'
299
300    if not os.path.exists(path / 'entities'):
301        # create directory
302        os.mkdir(path / 'entities')
303
304    if not os.path.exists(path / 'relations'):
305        # create directory
306        os.mkdir(path / 'relations')
307
308    if os.path.exists(path / 'entities' / file_name) and os.path.exists(path / 'relations' / file_name):
309        if clear_cache:
310            print("Clearing entities and relations cache for PMID {}".format(
311                article.pmid))
312            os.remove(path / 'entities' / file_name)
313            os.remove(path / 'relations' / file_name)
314        else:
315            with open(path / 'entities' / file_name, 'rb') as f:
316                entities = pickle.load(f)
317            with open(path / 'relations' / file_name, 'rb') as f:
318                relations = pickle.load(f)
319            return entities, relations, True
320
321    text = ''
322    if source == 'abstract':
323        text = article.abstract
324    elif source == 'full_text':
325        text = article.full_text
326    else:
327        raise ValueError("Invalid source: {}".format(source))
328
329    entities = extract_entities(article)
330
331    span_sentences = tokenize_into_sentences(article, source)
332
333    # divide entities in gene and disease entities
334    gene_entities = []
335    disease_entities = []
336    drug_entities = []
337    for entity in entities:
338        if entity.type == "disease":
339            disease_entities.append(entity)
340        elif entity.type == "gene":
341            gene_entities.append(entity)
342        elif entity.type == "drug":
343            drug_entities.append(entity)
344
345    relations = []
346    chemprot_batch = []
347    biobert_batch = []
348    for gene_idx, gene_entity in enumerate(gene_entities):
349        # extract gene-drug relations using fine-tuned bert on chemprot
350        for drug_idx, drug_entity in enumerate(drug_entities):
351            # find the sentence that contains the gene and disease
352            sentence_index_gene = find_entity(gene_entity, span_sentences)
353            sentence_index_drug = find_entity(drug_entity, span_sentences)
354
355            masked_text = ''
356            if gene_entity.span_begin < drug_entity.span_begin:
357                # wrap the gene_entity in << >>
358                # and the drug with [[ ]]
359                masked_text = text[span_sentences[sentence_index_gene][0]:gene_entity.span_begin] + "<< " + text[gene_entity.span_begin:gene_entity.span_end] + " >>" + \
360                    text[gene_entity.span_end:drug_entity.span_begin] + "[[ " + text[drug_entity.span_begin:drug_entity.span_end] + \
361                    " ]]" + \
362                    text[drug_entity.span_end:span_sentences[sentence_index_drug][1]]
363            else:
364                # wrap the drug_entity in << >>
365                # and the gene with [[ ]]
366                masked_text = text[span_sentences[sentence_index_drug][0]:drug_entity.span_begin] + "<< " + text[drug_entity.span_begin:drug_entity.span_end] + " >>" + \
367                    text[drug_entity.span_end:gene_entity.span_begin] + "[[ " + text[gene_entity.span_begin:gene_entity.span_end] + \
368                    " ]]" + \
369                    text[gene_entity.span_end:span_sentences[sentence_index_gene][1]]
370
371            chemprot_batch.append(
372                {'gene_idx': gene_idx, 'drug_idx': drug_idx, 'masked_text': masked_text})
373
374        # extract gene-disease relations using biobert
375        for disease_idx, disease_entity in enumerate(disease_entities):
376            # find the sentence that contains the gene and disease
377            sentence_index_gene = find_entity(gene_entity, span_sentences)
378            sentence_index_disease = find_entity(
379                disease_entity, span_sentences)
380
381            masked_text = ''
382            if gene_entity.span_begin < disease_entity.span_begin:
383                masked_text = text[span_sentences[sentence_index_gene][0]:gene_entity.span_begin] + "@GENE$" + text[gene_entity.span_end:
384                                                                                                                    disease_entity.span_begin] + "@DISEASE$" + text[disease_entity.span_end:span_sentences[sentence_index_disease][1]]
385            else:
386                masked_text = text[span_sentences[sentence_index_disease][0]:disease_entity.span_begin] + "@DISEASE$" + \
387                    text[disease_entity.span_end:gene_entity.span_begin] + "@GENE$" + \
388                    text[gene_entity.span_end:span_sentences[sentence_index_gene][1]]
389
390            biobert_batch.append(
391                {'gene_idx': gene_idx, 'disease_idx': disease_idx, 'masked_text': masked_text})
392
393    # Predict the relations using biobert
394    if len(biobert_batch) > 0:
395        for batch_idx in range(0, len(biobert_batch), MAX_SEQ_PER_BATCH):
396            batch = biobert_batch[batch_idx:batch_idx + MAX_SEQ_PER_BATCH]
397            masked_texts = [x['masked_text'] for x in batch]
398            tok_texts = rel_tokenizer(
399                masked_texts, max_length=512, padding=True, truncation=True, return_tensors='pt').to(device)
400            outputs = rel_model(**tok_texts)
401            class_logits = outputs["logits"].detach().cpu().numpy()
402            # len x 2
403            # apply softmax to get the probabilities
404            class_probs = np.exp(
405                class_logits) / np.sum(np.exp(class_logits), axis=1, keepdims=True)
406            for i in range(len(class_logits)):
407                if class_probs[i][0] > 0.5:
408                    j = i + batch_idx
409                    gene = gene_entities[biobert_batch[j]['gene_idx']]
410                    disease = disease_entities[biobert_batch[j]['disease_idx']]
411                    relations.append((gene, disease, float(class_probs[i][0])))
412
413            # free gpu memory
414            del tok_texts
415
416    # # Predict the relations using chemprot
417    if len(chemprot_batch) > 0:
418        for batch_idx in range(0, len(chemprot_batch), MAX_SEQ_PER_BATCH):
419            batch = chemprot_batch[batch_idx:batch_idx + MAX_SEQ_PER_BATCH]
420            masked_texts = [x['masked_text'] for x in batch]
421            tok_texts = chemprot_tokenizer(
422                masked_texts, max_length=512, padding=True, truncation=True, return_tensors='pt').to(device)
423            outputs = chemprot_model(**tok_texts)
424            class_logits = outputs["logits"].detach().cpu().numpy()
425            # len x 13
426            # apply softmax to get the probabilities
427            class_probs = np.exp(
428                class_logits) / np.sum(np.exp(class_logits), axis=1, keepdims=True)
429            for i in range(len(class_logits)):
430                # if 1 class is above 0.5, then we consider it as a relation
431                max_p = np.max(class_probs[i])
432                if max_p > 0.5:
433                    j = i + batch_idx
434                    gene = gene_entities[chemprot_batch[j]['gene_idx']]
435                    drug = drug_entities[chemprot_batch[j]['drug_idx']]
436                    relations.append((gene, drug, float(max_p)))
437
438            # free gpu memory
439            del tok_texts
440
441    # save entities and relations to file with pickle
442    with open(path / 'entities' / file_name, 'wb') as f:
443        pickle.dump(entities, f)
444    with open(path / 'relations' / file_name, 'wb') as f:
445        pickle.dump(relations, f)
446
447    return entities, relations, False
448
449
450def query_plain(text, url="http://bern2.korea.ac.kr/plain"):
451    """
452    Query the BERN2 server for plain text.
453    Args:
454        text (str): the text to be annotated
455        url (str): the url of the BERN2 server.
456    """
457    # Limit the text at 5000 characters
458    if len(text) > 5000:
459        text = text[:5000]
460
461    result = requests.post(url, json={'text': text})
462
463    if result.status_code != 200:
464        print("Error: {}".format(result.status_code))
465        raise Exception("Error: {}".format(result.status_code))
466
467    return result.json()
468
469
470def query_pmid(pmids, url="http://bern2.korea.ac.kr/pubmed"):
471    """query the BERN2 server for pmids"""
472    return requests.get(url + "/" + ",".join(pmids)).json()
473
474
475def tokenize_into_sentences(article: Article, source: str = 'abstract') -> List[List[int]]:
476    """tokenize the text into sentences returning the index of begin and end of each sentence
477
478    Args:
479        article (Article): article to tokenize.
480        source (str): The source of the text.
481
482    Returns:
483        List: A list of couples containing the begin and end of each sentence.
484    """
485    text = ''
486    if source == 'abstract':
487        text = article.abstract
488    elif source == 'full_text':
489        text = article.full_text
490    else:
491        raise ValueError("Invalid source: {}".format(source))
492
493    sentences = sent_tokenize(text)
494    span_sentences = []
495    # for each sentence, put the start and end index of each sentence
496    start = 0
497    for sentence in sentences:
498        span_sentences.append([start, start + len(sentence)])
499        start += len(sentence) + 1
500    return span_sentences
501
502
503def find_entity(entity: Entity, span_sentences: List[List[int]]) -> int:
504    """find in which sentence the entity is
505
506    Args:
507        entity (Entity): entity to find
508        span_sentences (List): A list of couples containing the index of the begin and end of each sentence.
509
510    Returns:
511        int: index of the sentence containing the entity
512    """
513    for i in range(len(span_sentences)):
514        if span_sentences[i][0] <= entity.span_begin and entity.span_end <= span_sentences[i][1]:
515            return i
516    return -1
517
518
519def add_title_node(G, n, d):
520    result_html = f"""
521  <h3>{d['mention']}</h3>
522  """
523    # articles:
524    #  <ul>
525    # """
526    #pmids = re.split("[,\\n]", d['pmid'])
527    # for pmid in pmids:
528    #  result_html += f"<li>{pmid}</li>"
529    #result_html += "</ul>"
530    return result_html
531
532
533def add_title_edge(G, edge):
534    # find intersection between pmids of the two nodes
535    node1 = dict(G.nodes(data=True))[edge[0]]
536    node2 = dict(G.nodes(data=True))[edge[1]]
537    n1_pmids = re.split(",", node1['pmid'])
538    n2_pmids = re.split(",", node2['pmid'])
539    intersection = list(set(n1_pmids) & set(n2_pmids))
540
541    # create html
542    result_html = f"""<p>common articles between <strong>{node1['mention']}</strong> and <strong>{node2['mention']}</strong></p>
543  
544  <ul>"""
545    for pmid in intersection:
546        result_html += f"<li>{pmid}</li>"
547    result_html += "</ul>"
548    return result_html
549
550
551def _html_graph_communities(G, communities, net, colors):
552    for i, community in enumerate(communities):
553        for node_id in community:
554            node = net.get_node(node_id)
555            node_type = dict(G.nodes(data=True))[node_id]['type']
556            node['color'] = colors[i]
557
558            if (node_type == 'gene'):
559                node['shape'] = 'dot'
560            elif (node_type == 'disease'):
561                node['shape'] = 'diamond'
562            elif (node_type == 'drug'):
563                node['shape'] = 'star'
564
565
566def _find_community(node_id, communities):
567    """
568        Find the index of the community containing the node.
569    """
570    for i, community in enumerate(communities):
571        if node_id in community:
572            return i
573
574
575def html_mark_subgraph(G, subG, name="nodes", hide_isolated_nodes=True):
576    """
577        Create a html file, named <name>, containing the graph G with the subgraph subG marked.
578
579        Args:
580            G (networkx.Graph): The graph to be displayed.
581            subG (networkx.Graph): The subgraph to be marked.
582            name (str): The name of the html file. Default is "nodes".
583            hide_isolated_nodes (bool): If True, the isolated nodes are not displayed. Default is True.
584    """
585
586    if hide_isolated_nodes:
587        G.remove_nodes_from(list(nx.isolates(G)))
588        subG.remove_nodes_from(list(nx.isolates(subG)))
589
590    net = Network('500px', '600px', notebook=True)
591
592    for n, d in G.nodes(data=True):
593        color = '#cccccc80'
594        if n not in subG.nodes():
595            net.add_node(n, d['mention'], color='#cccccc80',
596                title=add_title_node(G, n, d))
597
598    for n, d in subG.nodes(data=True):
599        color = '#cccccc80'
600        if d['type'] == 'gene':
601            color = '#0DA3E4'
602        elif d['type'] == 'disease':
603            color = '#bf4d2d'
604        elif d['type'] == 'drug':
605            color = '#5ef951'
606        net.add_node(n, d['mention'], color=color,
607            title=add_title_node(G, n, d))
608
609    # add edges not in subG
610    for edge in G.edges(data='True'):
611        weight = G.get_edge_data(edge[0], edge[1])['weight']
612        net.add_edge(edge[0], edge[1], value=weight,
613            color='#cccccc70', title=add_title_edge(G, edge))
614
615    # add edges in subG
616    for edge in subG.edges(data='True'):
617        weight = G.get_edge_data(edge[0], edge[1])['weight']
618        net.add_edge(edge[0], edge[1], value=weight,
619            color='#F0EB5A', title=add_title_edge(subG, edge))
620
621    net.show_buttons(filter_=['physics'])
622    net.show(name + ".html")
623    IPython.display.HTML(filename=Path(os.getcwd())/(name + ".html"))
624
625
626def html_graph(G, name="nodes", communities=None, hide_isolated_nodes=True):
627    """
628        Create an html file, named <name>, representing the graph G using vis.js.
629
630        Args:
631            G (NetworkX graph): The graph to be displayed.
632            name (str): The name of the file to be created. Default is "nodes".
633            communities (List): A list of communities to be displayed. If None, no communities are displayed. Default is None.
634            hide_isolated_nodes (bool): If True, hide isolated nodes. Default is True.
635
636        Returns:
637            A pyvis network object and writes the file to the current directory.
638    """
639
640    if hide_isolated_nodes:
641        G.remove_nodes_from(list(nx.isolates(G)))
642
643    net = Network('500px', '600px', notebook=True)
644
645    for n, d in G.nodes(data=True):
646        color = '#ccc'
647        if d['type'] == 'gene':
648            color = '#0DA3E4'
649        elif d['type'] == 'disease':
650            color = '#bf4d2d'
651        elif d['type'] == 'drug':
652            color = '#5ef951'
653        net.add_node(n, d['mention'], color=color,
654                     title=add_title_node(G, n, d))
655
656    # if communities is not None and len(communities) > 0
657    colors = []
658    if communities is not None and len(communities) > 0:
659        # generate random color for each community
660        colors = ["#"+''.join([random.choice('0123456789ABCDEF') for j in range(6)]) + "90"
661                  for i in range(len(communities))]
662        _html_graph_communities(G, communities, net, colors)
663
664    # add edges
665    for edge in G.edges(data='True'):
666        weight = G.get_edge_data(edge[0], edge[1])['weight']
667        if communities is not None and len(communities) > 0:
668            # find the community of the two nodes
669            community_node1_index = _find_community(edge[0], communities)
670            if edge[1] in communities[community_node1_index]:
671                net.add_edge(edge[0], edge[1], value=weight,
672                             color=colors[community_node1_index], title=add_title_edge(G, edge))
673            else:
674                net.add_edge(edge[0], edge[1], value=weight,
675                             color='#F0EB5A', title=add_title_edge(G, edge))
676        else:
677            net.add_edge(edge[0], edge[1], value=weight,
678                         color='#F0EB5A', title=add_title_edge(G, edge))
679
680    net.show_buttons(filter_=['physics'])
681    net.show(name + ".html")
682    IPython.display.HTML(filename=Path(os.getcwd())/(name + ".html"))
683
684    return net
685
686
687def filter_by_centrality(G, percentage_threshold: float = 0.1):
688    '''
689    Filter the graph by mantaining the 'percentage_threshold' of the nodes with the highest centrality.
690    Args:
691        G: networkx graph
692        percentage_threshold: float between 0 and 1. The percentage of most central nodes to keep.
693    '''
694    mesh_id_to_degree = dict()
695    for node_id in G.nodes():
696        mesh_id_to_degree[node_id] = G.degree(node_id)
697
698    sorted_mesh_ids = sorted(
699        mesh_id_to_degree, key=mesh_id_to_degree.get, reverse=True)
700
701    # take only the nodes with the highest centrality
702    nodes_to_keep = sorted_mesh_ids[:ceil(
703        len(sorted_mesh_ids)*percentage_threshold)]
704
705    return G.subgraph(nodes_to_keep).copy()
706
707
708def filter_by_name(G, name: str):
709    """
710    Filter the graph by mantaining the nodes with the given name.
711    Args:
712        G: networkx graph
713        name: str. The name of the nodes to keep.
714    Returns:
715        A list of nodes that include the given name.
716    """
717    nodes = []
718    for n, d in G.nodes(data=True):
719        if name in d['mention']:
720            d['mesh_id'] = n
721            nodes.append(d)
722
723    return nodes
724
725
726def filter_by_category(G, category: str, max_number_of_nodes: int = None, sort_by: str = 'degree'):
727    '''
728    Returns the nodes with the type category
729    Args:
730        G (Networkx Graph): graph to filter
731        category (str): category to filter by: 'gene', 'disease', 'drug'
732        max_number_of_nodes (int): maximum number of nodes to return. If None, all nodes are returned
733        sort_by (str): sort by 'degree' or 'name'
734    Returns:
735        A list of nodes with the given category
736    '''
737    if category not in ['gene', 'disease', 'drug']:
738        raise ValueError("category must be one of 'gene', 'disease', 'drug'")
739
740    nodes = []
741    for n, d in G.nodes(data=True):
742        if d['type'] == category:
743            d['mesh_id'] = n
744            nodes.append(d)
745
746    if sort_by == 'degree':
747        nodes = sorted(nodes, key=lambda node: G.degree(
748            node['mesh_id']), reverse=True)
749    elif sort_by == 'name':
750        nodes = sorted(nodes, key=lambda node: node['mention'])
751
752    if max_number_of_nodes is not None:
753        nodes = nodes[:max_number_of_nodes]
754
755    return nodes
756
757
758def expand_from_node(G, node, max_distance):
759    ''' 
760    Returns the subgraph that starting from the node id link all the node with distance k from id
761    Args:
762        G (Networkx Graph): graph to filter
763        node (str): node id
764        max_distance (int): maximum distance to expand
765    Returns:
766        A new graph containing the nodes with distance k from node
767    '''
768    if type(node) == List:
769        node = node[0]
770        print("node is a list, taking the first element")
771        print("Otherwise use 'expand_from_nodes'")
772    node_list = [node['mesh_id']]
773
774    for node, succ in nx.bfs_successors(G, node['mesh_id'], depth_limit=max_distance):
775        node_list += list(succ)
776
777    return G.subgraph(node_list).copy()
778
779
780def expand_from_nodes(G, nodes, max_distance: int):
781    '''
782    Returns the subgraph that starting from the node id link all the node with distance k from id 
783    Args:
784        G (Networkx Graph): graph to filter
785        nodes (list): list of nodes mesh_id
786        max_distance (int): maximum distance to expand
787    Returns:
788        A new graph containing the nodes with distance k from node
789    '''
790    if type(nodes[0]) == dict:
791        node_id_list = [node['mesh_id'] for node in nodes]
792
793        node_list = [node['mesh_id'] for node in nodes]
794    elif type(nodes[0]) == str:
795        node_id_list = nodes
796        node_list = nodes
797    else:
798        raise ValueError("nodes must be a list of mesh_id or a list of node objects")
799
800    for node_id in node_id_list:
801        for _, succ in nx.bfs_successors(G, node_id, depth_limit=max_distance):
802            node_list += list(succ)
803
804    return G.subgraph(node_list).copy()
805
806
807def get_common_articles(node1, node2):
808    '''
809    Returns the articles that both nodes mention
810    Args:
811        node1 (dict): node object
812        node2 (dict): node object
813    Returns:
814        A list of articles
815    '''
816    articles_node1 = node1['pmid']
817    articles_node2 = node2['pmid']
818    return list(set(articles_node1.split(',')).intersection(articles_node2.split(',')))
819
820
821def search_path(G, from_node, to_node):
822    '''
823    Returns the path between two nodes in the graph
824    Args:
825        G (Networkx Graph): graph to filter
826        from_node (str): node id
827        to_node (str): node id
828    Returns:
829        A list of nodes that form the path
830    '''
831    try:
832        return nx.shortest_path(G, source=from_node['mesh_id'], target=to_node['mesh_id'])
833    except nx.NetworkXNoPath:
834        return []
835
836
837def search_paths_to_category(G, from_node, to_category: str):
838    '''
839    Returns the paths from 'from_node' to nodes with the type 'to_category'
840    Args:
841        G (Networkx Graph): graph to filter
842        from_node (str): node id
843        to_category (str): to_category to filter by: 'gene', 'disease', 'drug'
844    Returns:
845        A list of paths
846    '''
847    if to_category not in ['gene', 'disease', 'drug']:
848        raise ValueError("to_category must be one of 'gene', 'disease', 'drug'")
849
850    targets_nodes = filter_by_category(G, to_category)
851    paths = []
852    for target_node in targets_nodes:
853        paths.append(search_path(G, from_node, target_node))
854
855    # remove empty paths
856    paths = [path for path in paths if len(path) > 0]
857
858    return paths
859
860
861def get_graph_from_path(G, path):
862    '''
863    Returns the subgraph that contains the nodes in the path
864    Args:
865        G (Networkx Graph): graph to filter
866        path (list): list of nodes mesh_id
867    Returns:
868        A new graph containing the nodes in the path
869    '''
870    return G.subgraph(path).copy()
871
872
873def get_graph_from_paths(G, paths):
874    '''
875    Returns the subgraph that contains the nodes in the paths
876    Args:
877        G (Networkx Graph): graph to filter
878        paths (list): list of paths
879    Returns:
880        A new graph containing the nodes in the paths
881    '''
882    nodes = [node for path in paths for node in path]
883    return G.subgraph(nodes).copy()
#   def download_articles( title: str, start_year: int, end_year: int, max_results: int = 100, author: str = '', sort_by: str = 'relevance' ) -> List[pubmad.types.Article]:
View Source
 55def download_articles(title: str, start_year: int, end_year: int, max_results: int = 100, author: str = '', sort_by: str = 'relevance') -> List[Article]:
 56    """
 57    Download articles from PubMed using BioPython library.
 58    Args:
 59        query (str): The query to search for.
 60        start_year (int): The start year to search for.
 61        end_year (int): The end year to search for.
 62        max_results (int): The maximum number of results to return.
 63        author (str): The author to search for, leave empty to search for all authors.
 64        sort_by (str): The sort order to use. Can be 'relevance' to retrieve the most relevant results, or 'date' to sort by publication date. Defaults to 'relevance'.
 65    Returns:
 66        List[Article] A list of articles.
 67    """
 68    current_path = Path(os.getcwd()) / 'cache'
 69
 70    if not os.path.exists(current_path):
 71        os.mkdir(current_path)
 72
 73    file_name = '{}_{}_{}_{}.txt'.format(
 74        title, start_year, end_year, max_results)
 75    if os.path.exists(current_path / file_name):
 76        print("Loading from cache...")
 77        # TODO: clear_cache is not implemented yet
 78        with open(current_path / file_name, 'rb') as f:
 79            return pickle.load(f)
 80
 81    Entrez.email = 'pubmadbiosearch@gmail.com'
 82
 83    if author != "":
 84        query = '(' + title + ') AND (' + author + '[Author])'
 85    else:
 86        query = title
 87
 88    handle = Entrez.esearch(db='pubmed',
 89                            sort=sort_by,
 90                            retmax=max_results,
 91                            retmode='xml',
 92                            datetype='pdat',
 93                            mindate=str(start_year),
 94                            maxdate=str(end_year),
 95                            term=query)
 96    results = Entrez.read(handle)
 97    id_list = results['IdList']
 98    ids = ','.join(id_list)
 99    handle = Entrez.efetch(db='pubmed', rettype="medline",
100                           id=ids, retmode='text')
101    results = Medline.parse(handle)
102    results = list(results)
103    articles = []
104    for article in results:
105        abstract = article.get("AB", "?")
106        title = article.get("TI", "?")
107        if abstract != "?" and title != "?":
108            if abstract.find("This article has been withdrawn at the request of the author(s) and/or editor") == -1 and \
109               abstract.find("An amendment to this paper has been published and can be accessed via a link at the top of the paper") == -1 and \
110               abstract.find("This corrects the article") == -1 and abstract.find("A Correction to this paaper has been published") == -1:
111                articles.append(Article(title=title, abstract=abstract,
112                                        pmid=article['PMID'], full_text='', publication_data=None))
113
114    print("Found {} articles".format(len(articles)))
115
116    # save articles to pickle file
117    with open(current_path / file_name, 'wb') as f:
118        pickle.dump(articles, f)
119    return articles

Download articles from PubMed using BioPython library. Args: query (str): The query to search for. start_year (int): The start year to search for. end_year (int): The end year to search for. max_results (int): The maximum number of results to return. author (str): The author to search for, leave empty to search for all authors. sort_by (str): The sort order to use. Can be 'relevance' to retrieve the most relevant results, or 'date' to sort by publication date. Defaults to 'relevance'. Returns: List[Article] A list of articles.

#   def extract_entities_pmids(pmids: List[str]) -> List[List[pubmad.types.Entity]]:
View Source
122def extract_entities_pmids(pmids: List[str]) -> List[List[Entity]]:
123    """Extract entities from one or more articles identified by the pmids provided
124
125    Args:
126        pmids (List[str]): list of pmids to extract entities from
127
128    Returns:
129        List[List[Entity]]: list of entities for each article
130    """
131
132    if len(pmids) == 0:
133        return []
134
135    # Query BERN2 server for entities.
136    extracted = False
137    bern_result = None
138
139    i = 0
140
141    while not extracted and i < 3:
142        try:
143            bern_result = query_pmid(pmids)
144            extracted = True
145        except Exception as e:
146            print("Error querying BERN2 server")
147            print("Sleep for 3 seconds and try again to not get banned by BERN2")
148            time.sleep(3)
149            i += 1
150            continue
151
152    entities = []
153
154    # Parse the result
155    for i, article in enumerate(bern_result):
156        entities_per_article = []
157        for entity in article["annotations"]:
158            if (entity['obj'] == 'disease' or entity['obj'] == 'gene' or entity['obj'] == 'drug'):
159                new_entity = Entity(mesh_id=entity['id'], mention=entity['mention'], type=entity['obj'],
160                                    prob=entity['prob'], span_begin=entity['span']['begin'], span_end=entity['span']['end'], pmid=pmids[i])
161                entities_per_article.append(new_entity)
162        entities.append(entities_per_article)
163
164    return entities

Extract entities from one or more articles identified by the pmids provided

Args: pmids (List[str]): list of pmids to extract entities from

Returns: List[List[Entity]]: list of entities for each article

#   def extract_entities( article: pubmad.types.Article, source: str = 'abstract' ) -> List[pubmad.types.Entity]:
View Source
167def extract_entities(article: Article, source: str = 'abstract') -> List[Entity]:
168    """
169    Extract entities from an article using BERN2 (online).
170
171    Args:
172        article (Article): The article to extract entities from. 
173        source (str): The source to extract entities from. Can be 'abstract' or 'full_text'. Defaults to 'abstract'.
174
175    Returns:
176        List[Entity]: A list of entities.
177    """
178    # We call bern2 server for ner
179    text = ''
180    if source == 'abstract':
181        text = article.abstract
182    elif source == 'full_text':
183        text = article.full_text
184    else:
185        raise ValueError("Invalid source: {}".format(source))
186
187    # Query BERN2 server for entities.
188    extracted = False
189    bern_result = None
190
191    i = 0
192
193    while not extracted and i < 3:
194        try:
195            bern_result = query_plain(text)
196            extracted = True
197        except Exception as e:
198            print("Error querying BERN2 server")
199            print("Sleep for 3 seconds and try again to not get banned by BERN2")
200            time.sleep(3)
201            i += 1
202            continue
203
204    if i == 3:
205        return []
206
207    # Parse the result.
208    annotations = bern_result['annotations']
209    entities = []
210    for entity in annotations:
211        if (entity['obj'] == 'disease' or entity['obj'] == 'gene' or entity['obj'] == 'drug'):
212            pmid = article.pmid
213            if type(article.pmid) == list:
214                pmid = article.pmid[0]
215            new_entity = Entity(mesh_id=entity['id'], mention=entity['mention'], type=entity['obj'],
216                                prob=entity['prob'], span_begin=entity['span']['begin'], span_end=entity['span']['end'], pmid=pmid)
217            entities.append(new_entity)
218
219    return entities

Extract entities from an article using BERN2 (online).

Args: article (Article): The article to extract entities from. source (str): The source to extract entities from. Can be 'abstract' or 'full_text'. Defaults to 'abstract'.

Returns: List[Entity]: A list of entities.

#   def display_graph( graph: networkx.classes.graph.Graph, hide_isolated_nodes: bool = True, show: bool = True ):
View Source
222def display_graph(graph: nx.Graph, hide_isolated_nodes: bool = True, show: bool = True):
223    """
224    Display a graph.
225
226    Args:
227        graph (nx.Graph): The graph to display.
228        hide_isolated_nodes (bool): Whether to hide isolated nodes. Defaults to True.
229        show (bool): Whether to show the graph. Defaults to True. Otherwise it shows the window without stopping the program.
230    """
231    plt.figure()
232    if hide_isolated_nodes:
233        graph.remove_nodes_from(list(nx.isolates(graph)))
234
235    gene_nodes = [n for n, d in graph.nodes(data=True) if d['type'] == 'gene']
236    disease_nodes = [n for n, d in graph.nodes(
237        data=True) if d['type'] == 'disease']
238    other_nodes = [n for n, d in graph.nodes(
239        data=True) if d['type'] != 'gene' and d['type'] != 'disease']
240
241    pos = nx.spring_layout(graph, 100)
242
243    weights = nx.get_edge_attributes(graph, 'weight')
244
245    nx.draw_networkx_nodes(graph, pos, nodelist=gene_nodes,
246                           node_color='r', node_size=100, alpha=0.8, label='gene')
247    nx.draw_networkx_nodes(graph, pos, nodelist=disease_nodes,
248                           node_color='b', node_size=100, alpha=0.8, label='disease')
249    nx.draw_networkx_nodes(graph, pos, nodelist=other_nodes,
250                           node_color='g', node_size=100, alpha=0.8, label='drug')
251
252    nx.draw_networkx_labels(graph, pos, labels={
253                            n: d['mention'] for n, d in graph.nodes(data=True)}, font_size=10)
254
255    nx.draw_networkx_edges(graph, pos, width=1.0, alpha=0.5)
256    plt.axis('off')
257    plt.legend()
258    if show:
259        plt.show()
260    else:
261        plt.draw()

Display a graph.

Args: graph (nx.Graph): The graph to display. hide_isolated_nodes (bool): Whether to hide isolated nodes. Defaults to True. show (bool): Whether to show the graph. Defaults to True. Otherwise it shows the window without stopping the program.

#   def extract_naive_relations( entities: List[pubmad.types.Entity] ) -> List[Tuple[pubmad.types.Entity, pubmad.types.Entity, float]]:
View Source
264def extract_naive_relations(entities: List[Entity]) -> List[Tuple[Entity, Entity, float]]:
265    '''
266    Extract relations from a list of entities.
267
268    Args:
269        entities (List[Entity]): A list of entities.
270
271    Returns:
272        List[Tuple[Entity, Entity, float]]: A list of relations.
273    '''
274    # Connect all the entities to each other.
275    relations = []
276    for entity1, entity2 in product(entities, entities):
277        if entity1.mesh_id != entity2.mesh_id:
278            # 1 meaning that the relation is present.
279            relations.append((entity1, entity2, 1))
280    return relations

Extract relations from a list of entities.

Args: entities (List[Entity]): A list of entities.

Returns: List[Tuple[Entity, Entity, float]]: A list of relations.

#   def extract_biobert_relations( article: pubmad.types.Article, source: str = 'abstract', clear_cache: bool = False ) -> Tuple[List[pubmad.types.Entity], List[Tuple[pubmad.types.Entity, pubmad.types.Entity]], bool]:
View Source
283def extract_biobert_relations(article: Article, source: str = 'abstract', clear_cache: bool = False) -> Tuple[List[Entity], List[Tuple[Entity, Entity]], bool]:
284    """
285    Extract the entities from the article using BERN2 (online).
286    Then it calls BioBERT to extract relations between genes and diseases.
287
288    Args:
289        article (Article): The article to extract entities from.
290        source (str): The source to extract entities from. Can be 'abstract' or 'full_text'. Defaults to 'abstract'.
291        clear_cache (bool): Whether to clear the BioBERT cache. Defaults to False.
292
293    Returns:
294        Tuple[List[Entity], List[Tuple[Entity, Entity]], bool]: A tuple of entities, relations and whether the cache was used.
295
296    """
297    file_name = str(article.pmid)[:128] + '.txt'
298    file_name = file_name.replace('\n', '_')
299    path = Path(os.getcwd()) / 'cache'
300
301    if not os.path.exists(path / 'entities'):
302        # create directory
303        os.mkdir(path / 'entities')
304
305    if not os.path.exists(path / 'relations'):
306        # create directory
307        os.mkdir(path / 'relations')
308
309    if os.path.exists(path / 'entities' / file_name) and os.path.exists(path / 'relations' / file_name):
310        if clear_cache:
311            print("Clearing entities and relations cache for PMID {}".format(
312                article.pmid))
313            os.remove(path / 'entities' / file_name)
314            os.remove(path / 'relations' / file_name)
315        else:
316            with open(path / 'entities' / file_name, 'rb') as f:
317                entities = pickle.load(f)
318            with open(path / 'relations' / file_name, 'rb') as f:
319                relations = pickle.load(f)
320            return entities, relations, True
321
322    text = ''
323    if source == 'abstract':
324        text = article.abstract
325    elif source == 'full_text':
326        text = article.full_text
327    else:
328        raise ValueError("Invalid source: {}".format(source))
329
330    entities = extract_entities(article)
331
332    span_sentences = tokenize_into_sentences(article, source)
333
334    # divide entities in gene and disease entities
335    gene_entities = []
336    disease_entities = []
337    drug_entities = []
338    for entity in entities:
339        if entity.type == "disease":
340            disease_entities.append(entity)
341        elif entity.type == "gene":
342            gene_entities.append(entity)
343        elif entity.type == "drug":
344            drug_entities.append(entity)
345
346    relations = []
347    chemprot_batch = []
348    biobert_batch = []
349    for gene_idx, gene_entity in enumerate(gene_entities):
350        # extract gene-drug relations using fine-tuned bert on chemprot
351        for drug_idx, drug_entity in enumerate(drug_entities):
352            # find the sentence that contains the gene and disease
353            sentence_index_gene = find_entity(gene_entity, span_sentences)
354            sentence_index_drug = find_entity(drug_entity, span_sentences)
355
356            masked_text = ''
357            if gene_entity.span_begin < drug_entity.span_begin:
358                # wrap the gene_entity in << >>
359                # and the drug with [[ ]]
360                masked_text = text[span_sentences[sentence_index_gene][0]:gene_entity.span_begin] + "<< " + text[gene_entity.span_begin:gene_entity.span_end] + " >>" + \
361                    text[gene_entity.span_end:drug_entity.span_begin] + "[[ " + text[drug_entity.span_begin:drug_entity.span_end] + \
362                    " ]]" + \
363                    text[drug_entity.span_end:span_sentences[sentence_index_drug][1]]
364            else:
365                # wrap the drug_entity in << >>
366                # and the gene with [[ ]]
367                masked_text = text[span_sentences[sentence_index_drug][0]:drug_entity.span_begin] + "<< " + text[drug_entity.span_begin:drug_entity.span_end] + " >>" + \
368                    text[drug_entity.span_end:gene_entity.span_begin] + "[[ " + text[gene_entity.span_begin:gene_entity.span_end] + \
369                    " ]]" + \
370                    text[gene_entity.span_end:span_sentences[sentence_index_gene][1]]
371
372            chemprot_batch.append(
373                {'gene_idx': gene_idx, 'drug_idx': drug_idx, 'masked_text': masked_text})
374
375        # extract gene-disease relations using biobert
376        for disease_idx, disease_entity in enumerate(disease_entities):
377            # find the sentence that contains the gene and disease
378            sentence_index_gene = find_entity(gene_entity, span_sentences)
379            sentence_index_disease = find_entity(
380                disease_entity, span_sentences)
381
382            masked_text = ''
383            if gene_entity.span_begin < disease_entity.span_begin:
384                masked_text = text[span_sentences[sentence_index_gene][0]:gene_entity.span_begin] + "@GENE$" + text[gene_entity.span_end:
385                                                                                                                    disease_entity.span_begin] + "@DISEASE$" + text[disease_entity.span_end:span_sentences[sentence_index_disease][1]]
386            else:
387                masked_text = text[span_sentences[sentence_index_disease][0]:disease_entity.span_begin] + "@DISEASE$" + \
388                    text[disease_entity.span_end:gene_entity.span_begin] + "@GENE$" + \
389                    text[gene_entity.span_end:span_sentences[sentence_index_gene][1]]
390
391            biobert_batch.append(
392                {'gene_idx': gene_idx, 'disease_idx': disease_idx, 'masked_text': masked_text})
393
394    # Predict the relations using biobert
395    if len(biobert_batch) > 0:
396        for batch_idx in range(0, len(biobert_batch), MAX_SEQ_PER_BATCH):
397            batch = biobert_batch[batch_idx:batch_idx + MAX_SEQ_PER_BATCH]
398            masked_texts = [x['masked_text'] for x in batch]
399            tok_texts = rel_tokenizer(
400                masked_texts, max_length=512, padding=True, truncation=True, return_tensors='pt').to(device)
401            outputs = rel_model(**tok_texts)
402            class_logits = outputs["logits"].detach().cpu().numpy()
403            # len x 2
404            # apply softmax to get the probabilities
405            class_probs = np.exp(
406                class_logits) / np.sum(np.exp(class_logits), axis=1, keepdims=True)
407            for i in range(len(class_logits)):
408                if class_probs[i][0] > 0.5:
409                    j = i + batch_idx
410                    gene = gene_entities[biobert_batch[j]['gene_idx']]
411                    disease = disease_entities[biobert_batch[j]['disease_idx']]
412                    relations.append((gene, disease, float(class_probs[i][0])))
413
414            # free gpu memory
415            del tok_texts
416
417    # # Predict the relations using chemprot
418    if len(chemprot_batch) > 0:
419        for batch_idx in range(0, len(chemprot_batch), MAX_SEQ_PER_BATCH):
420            batch = chemprot_batch[batch_idx:batch_idx + MAX_SEQ_PER_BATCH]
421            masked_texts = [x['masked_text'] for x in batch]
422            tok_texts = chemprot_tokenizer(
423                masked_texts, max_length=512, padding=True, truncation=True, return_tensors='pt').to(device)
424            outputs = chemprot_model(**tok_texts)
425            class_logits = outputs["logits"].detach().cpu().numpy()
426            # len x 13
427            # apply softmax to get the probabilities
428            class_probs = np.exp(
429                class_logits) / np.sum(np.exp(class_logits), axis=1, keepdims=True)
430            for i in range(len(class_logits)):
431                # if 1 class is above 0.5, then we consider it as a relation
432                max_p = np.max(class_probs[i])
433                if max_p > 0.5:
434                    j = i + batch_idx
435                    gene = gene_entities[chemprot_batch[j]['gene_idx']]
436                    drug = drug_entities[chemprot_batch[j]['drug_idx']]
437                    relations.append((gene, drug, float(max_p)))
438
439            # free gpu memory
440            del tok_texts
441
442    # save entities and relations to file with pickle
443    with open(path / 'entities' / file_name, 'wb') as f:
444        pickle.dump(entities, f)
445    with open(path / 'relations' / file_name, 'wb') as f:
446        pickle.dump(relations, f)
447
448    return entities, relations, False

Extract the entities from the article using BERN2 (online). Then it calls BioBERT to extract relations between genes and diseases.

Args: article (Article): The article to extract entities from. source (str): The source to extract entities from. Can be 'abstract' or 'full_text'. Defaults to 'abstract'. clear_cache (bool): Whether to clear the BioBERT cache. Defaults to False.

Returns: Tuple[List[Entity], List[Tuple[Entity, Entity]], bool]: A tuple of entities, relations and whether the cache was used.

#   def query_plain(text, url='http://bern2.korea.ac.kr/plain'):
View Source
451def query_plain(text, url="http://bern2.korea.ac.kr/plain"):
452    """
453    Query the BERN2 server for plain text.
454    Args:
455        text (str): the text to be annotated
456        url (str): the url of the BERN2 server.
457    """
458    # Limit the text at 5000 characters
459    if len(text) > 5000:
460        text = text[:5000]
461
462    result = requests.post(url, json={'text': text})
463
464    if result.status_code != 200:
465        print("Error: {}".format(result.status_code))
466        raise Exception("Error: {}".format(result.status_code))
467
468    return result.json()

Query the BERN2 server for plain text. Args: text (str): the text to be annotated url (str): the url of the BERN2 server.

#   def query_pmid(pmids, url='http://bern2.korea.ac.kr/pubmed'):
View Source
471def query_pmid(pmids, url="http://bern2.korea.ac.kr/pubmed"):
472    """query the BERN2 server for pmids"""
473    return requests.get(url + "/" + ",".join(pmids)).json()

query the BERN2 server for pmids

#   def tokenize_into_sentences( article: pubmad.types.Article, source: str = 'abstract' ) -> List[List[int]]:
View Source
476def tokenize_into_sentences(article: Article, source: str = 'abstract') -> List[List[int]]:
477    """tokenize the text into sentences returning the index of begin and end of each sentence
478
479    Args:
480        article (Article): article to tokenize.
481        source (str): The source of the text.
482
483    Returns:
484        List: A list of couples containing the begin and end of each sentence.
485    """
486    text = ''
487    if source == 'abstract':
488        text = article.abstract
489    elif source == 'full_text':
490        text = article.full_text
491    else:
492        raise ValueError("Invalid source: {}".format(source))
493
494    sentences = sent_tokenize(text)
495    span_sentences = []
496    # for each sentence, put the start and end index of each sentence
497    start = 0
498    for sentence in sentences:
499        span_sentences.append([start, start + len(sentence)])
500        start += len(sentence) + 1
501    return span_sentences

tokenize the text into sentences returning the index of begin and end of each sentence

Args: article (Article): article to tokenize. source (str): The source of the text.

Returns: List: A list of couples containing the begin and end of each sentence.

#   def find_entity(entity: pubmad.types.Entity, span_sentences: List[List[int]]) -> int:
View Source
504def find_entity(entity: Entity, span_sentences: List[List[int]]) -> int:
505    """find in which sentence the entity is
506
507    Args:
508        entity (Entity): entity to find
509        span_sentences (List): A list of couples containing the index of the begin and end of each sentence.
510
511    Returns:
512        int: index of the sentence containing the entity
513    """
514    for i in range(len(span_sentences)):
515        if span_sentences[i][0] <= entity.span_begin and entity.span_end <= span_sentences[i][1]:
516            return i
517    return -1

find in which sentence the entity is

Args: entity (Entity): entity to find span_sentences (List): A list of couples containing the index of the begin and end of each sentence.

Returns: int: index of the sentence containing the entity

#   def add_title_node(G, n, d):
View Source
520def add_title_node(G, n, d):
521    result_html = f"""
522  <h3>{d['mention']}</h3>
523  """
524    # articles:
525    #  <ul>
526    # """
527    #pmids = re.split("[,\\n]", d['pmid'])
528    # for pmid in pmids:
529    #  result_html += f"<li>{pmid}</li>"
530    #result_html += "</ul>"
531    return result_html
#   def add_title_edge(G, edge):
View Source
534def add_title_edge(G, edge):
535    # find intersection between pmids of the two nodes
536    node1 = dict(G.nodes(data=True))[edge[0]]
537    node2 = dict(G.nodes(data=True))[edge[1]]
538    n1_pmids = re.split(",", node1['pmid'])
539    n2_pmids = re.split(",", node2['pmid'])
540    intersection = list(set(n1_pmids) & set(n2_pmids))
541
542    # create html
543    result_html = f"""<p>common articles between <strong>{node1['mention']}</strong> and <strong>{node2['mention']}</strong></p>
544  
545  <ul>"""
546    for pmid in intersection:
547        result_html += f"<li>{pmid}</li>"
548    result_html += "</ul>"
549    return result_html
#   def html_mark_subgraph(G, subG, name='nodes', hide_isolated_nodes=True):
View Source
576def html_mark_subgraph(G, subG, name="nodes", hide_isolated_nodes=True):
577    """
578        Create a html file, named <name>, containing the graph G with the subgraph subG marked.
579
580        Args:
581            G (networkx.Graph): The graph to be displayed.
582            subG (networkx.Graph): The subgraph to be marked.
583            name (str): The name of the html file. Default is "nodes".
584            hide_isolated_nodes (bool): If True, the isolated nodes are not displayed. Default is True.
585    """
586
587    if hide_isolated_nodes:
588        G.remove_nodes_from(list(nx.isolates(G)))
589        subG.remove_nodes_from(list(nx.isolates(subG)))
590
591    net = Network('500px', '600px', notebook=True)
592
593    for n, d in G.nodes(data=True):
594        color = '#cccccc80'
595        if n not in subG.nodes():
596            net.add_node(n, d['mention'], color='#cccccc80',
597                title=add_title_node(G, n, d))
598
599    for n, d in subG.nodes(data=True):
600        color = '#cccccc80'
601        if d['type'] == 'gene':
602            color = '#0DA3E4'
603        elif d['type'] == 'disease':
604            color = '#bf4d2d'
605        elif d['type'] == 'drug':
606            color = '#5ef951'
607        net.add_node(n, d['mention'], color=color,
608            title=add_title_node(G, n, d))
609
610    # add edges not in subG
611    for edge in G.edges(data='True'):
612        weight = G.get_edge_data(edge[0], edge[1])['weight']
613        net.add_edge(edge[0], edge[1], value=weight,
614            color='#cccccc70', title=add_title_edge(G, edge))
615
616    # add edges in subG
617    for edge in subG.edges(data='True'):
618        weight = G.get_edge_data(edge[0], edge[1])['weight']
619        net.add_edge(edge[0], edge[1], value=weight,
620            color='#F0EB5A', title=add_title_edge(subG, edge))
621
622    net.show_buttons(filter_=['physics'])
623    net.show(name + ".html")
624    IPython.display.HTML(filename=Path(os.getcwd())/(name + ".html"))

Create a html file, named , containing the graph G with the subgraph subG marked.

Args: G (networkx.Graph): The graph to be displayed. subG (networkx.Graph): The subgraph to be marked. name (str): The name of the html file. Default is "nodes". hide_isolated_nodes (bool): If True, the isolated nodes are not displayed. Default is True.

#   def html_graph(G, name='nodes', communities=None, hide_isolated_nodes=True):
View Source
627def html_graph(G, name="nodes", communities=None, hide_isolated_nodes=True):
628    """
629        Create an html file, named <name>, representing the graph G using vis.js.
630
631        Args:
632            G (NetworkX graph): The graph to be displayed.
633            name (str): The name of the file to be created. Default is "nodes".
634            communities (List): A list of communities to be displayed. If None, no communities are displayed. Default is None.
635            hide_isolated_nodes (bool): If True, hide isolated nodes. Default is True.
636
637        Returns:
638            A pyvis network object and writes the file to the current directory.
639    """
640
641    if hide_isolated_nodes:
642        G.remove_nodes_from(list(nx.isolates(G)))
643
644    net = Network('500px', '600px', notebook=True)
645
646    for n, d in G.nodes(data=True):
647        color = '#ccc'
648        if d['type'] == 'gene':
649            color = '#0DA3E4'
650        elif d['type'] == 'disease':
651            color = '#bf4d2d'
652        elif d['type'] == 'drug':
653            color = '#5ef951'
654        net.add_node(n, d['mention'], color=color,
655                     title=add_title_node(G, n, d))
656
657    # if communities is not None and len(communities) > 0
658    colors = []
659    if communities is not None and len(communities) > 0:
660        # generate random color for each community
661        colors = ["#"+''.join([random.choice('0123456789ABCDEF') for j in range(6)]) + "90"
662                  for i in range(len(communities))]
663        _html_graph_communities(G, communities, net, colors)
664
665    # add edges
666    for edge in G.edges(data='True'):
667        weight = G.get_edge_data(edge[0], edge[1])['weight']
668        if communities is not None and len(communities) > 0:
669            # find the community of the two nodes
670            community_node1_index = _find_community(edge[0], communities)
671            if edge[1] in communities[community_node1_index]:
672                net.add_edge(edge[0], edge[1], value=weight,
673                             color=colors[community_node1_index], title=add_title_edge(G, edge))
674            else:
675                net.add_edge(edge[0], edge[1], value=weight,
676                             color='#F0EB5A', title=add_title_edge(G, edge))
677        else:
678            net.add_edge(edge[0], edge[1], value=weight,
679                         color='#F0EB5A', title=add_title_edge(G, edge))
680
681    net.show_buttons(filter_=['physics'])
682    net.show(name + ".html")
683    IPython.display.HTML(filename=Path(os.getcwd())/(name + ".html"))
684
685    return net

Create an html file, named , representing the graph G using vis.js.

Args: G (NetworkX graph): The graph to be displayed. name (str): The name of the file to be created. Default is "nodes". communities (List): A list of communities to be displayed. If None, no communities are displayed. Default is None. hide_isolated_nodes (bool): If True, hide isolated nodes. Default is True.

Returns: A pyvis network object and writes the file to the current directory.

#   def filter_by_centrality(G, percentage_threshold: float = 0.1):
View Source
688def filter_by_centrality(G, percentage_threshold: float = 0.1):
689    '''
690    Filter the graph by mantaining the 'percentage_threshold' of the nodes with the highest centrality.
691    Args:
692        G: networkx graph
693        percentage_threshold: float between 0 and 1. The percentage of most central nodes to keep.
694    '''
695    mesh_id_to_degree = dict()
696    for node_id in G.nodes():
697        mesh_id_to_degree[node_id] = G.degree(node_id)
698
699    sorted_mesh_ids = sorted(
700        mesh_id_to_degree, key=mesh_id_to_degree.get, reverse=True)
701
702    # take only the nodes with the highest centrality
703    nodes_to_keep = sorted_mesh_ids[:ceil(
704        len(sorted_mesh_ids)*percentage_threshold)]
705
706    return G.subgraph(nodes_to_keep).copy()

Filter the graph by mantaining the 'percentage_threshold' of the nodes with the highest centrality. Args: G: networkx graph percentage_threshold: float between 0 and 1. The percentage of most central nodes to keep.

#   def filter_by_name(G, name: str):
View Source
709def filter_by_name(G, name: str):
710    """
711    Filter the graph by mantaining the nodes with the given name.
712    Args:
713        G: networkx graph
714        name: str. The name of the nodes to keep.
715    Returns:
716        A list of nodes that include the given name.
717    """
718    nodes = []
719    for n, d in G.nodes(data=True):
720        if name in d['mention']:
721            d['mesh_id'] = n
722            nodes.append(d)
723
724    return nodes

Filter the graph by mantaining the nodes with the given name. Args: G: networkx graph name: str. The name of the nodes to keep. Returns: A list of nodes that include the given name.

#   def filter_by_category( G, category: str, max_number_of_nodes: int = None, sort_by: str = 'degree' ):
View Source
727def filter_by_category(G, category: str, max_number_of_nodes: int = None, sort_by: str = 'degree'):
728    '''
729    Returns the nodes with the type category
730    Args:
731        G (Networkx Graph): graph to filter
732        category (str): category to filter by: 'gene', 'disease', 'drug'
733        max_number_of_nodes (int): maximum number of nodes to return. If None, all nodes are returned
734        sort_by (str): sort by 'degree' or 'name'
735    Returns:
736        A list of nodes with the given category
737    '''
738    if category not in ['gene', 'disease', 'drug']:
739        raise ValueError("category must be one of 'gene', 'disease', 'drug'")
740
741    nodes = []
742    for n, d in G.nodes(data=True):
743        if d['type'] == category:
744            d['mesh_id'] = n
745            nodes.append(d)
746
747    if sort_by == 'degree':
748        nodes = sorted(nodes, key=lambda node: G.degree(
749            node['mesh_id']), reverse=True)
750    elif sort_by == 'name':
751        nodes = sorted(nodes, key=lambda node: node['mention'])
752
753    if max_number_of_nodes is not None:
754        nodes = nodes[:max_number_of_nodes]
755
756    return nodes

Returns the nodes with the type category Args: G (Networkx Graph): graph to filter category (str): category to filter by: 'gene', 'disease', 'drug' max_number_of_nodes (int): maximum number of nodes to return. If None, all nodes are returned sort_by (str): sort by 'degree' or 'name' Returns: A list of nodes with the given category

#   def expand_from_node(G, node, max_distance):
View Source
759def expand_from_node(G, node, max_distance):
760    ''' 
761    Returns the subgraph that starting from the node id link all the node with distance k from id
762    Args:
763        G (Networkx Graph): graph to filter
764        node (str): node id
765        max_distance (int): maximum distance to expand
766    Returns:
767        A new graph containing the nodes with distance k from node
768    '''
769    if type(node) == List:
770        node = node[0]
771        print("node is a list, taking the first element")
772        print("Otherwise use 'expand_from_nodes'")
773    node_list = [node['mesh_id']]
774
775    for node, succ in nx.bfs_successors(G, node['mesh_id'], depth_limit=max_distance):
776        node_list += list(succ)
777
778    return G.subgraph(node_list).copy()

Returns the subgraph that starting from the node id link all the node with distance k from id Args: G (Networkx Graph): graph to filter node (str): node id max_distance (int): maximum distance to expand Returns: A new graph containing the nodes with distance k from node

#   def expand_from_nodes(G, nodes, max_distance: int):
View Source
781def expand_from_nodes(G, nodes, max_distance: int):
782    '''
783    Returns the subgraph that starting from the node id link all the node with distance k from id 
784    Args:
785        G (Networkx Graph): graph to filter
786        nodes (list): list of nodes mesh_id
787        max_distance (int): maximum distance to expand
788    Returns:
789        A new graph containing the nodes with distance k from node
790    '''
791    if type(nodes[0]) == dict:
792        node_id_list = [node['mesh_id'] for node in nodes]
793
794        node_list = [node['mesh_id'] for node in nodes]
795    elif type(nodes[0]) == str:
796        node_id_list = nodes
797        node_list = nodes
798    else:
799        raise ValueError("nodes must be a list of mesh_id or a list of node objects")
800
801    for node_id in node_id_list:
802        for _, succ in nx.bfs_successors(G, node_id, depth_limit=max_distance):
803            node_list += list(succ)
804
805    return G.subgraph(node_list).copy()

Returns the subgraph that starting from the node id link all the node with distance k from id Args: G (Networkx Graph): graph to filter nodes (list): list of nodes mesh_id max_distance (int): maximum distance to expand Returns: A new graph containing the nodes with distance k from node

#   def get_common_articles(node1, node2):
View Source
808def get_common_articles(node1, node2):
809    '''
810    Returns the articles that both nodes mention
811    Args:
812        node1 (dict): node object
813        node2 (dict): node object
814    Returns:
815        A list of articles
816    '''
817    articles_node1 = node1['pmid']
818    articles_node2 = node2['pmid']
819    return list(set(articles_node1.split(',')).intersection(articles_node2.split(',')))

Returns the articles that both nodes mention Args: node1 (dict): node object node2 (dict): node object Returns: A list of articles

#   def search_path(G, from_node, to_node):
View Source
822def search_path(G, from_node, to_node):
823    '''
824    Returns the path between two nodes in the graph
825    Args:
826        G (Networkx Graph): graph to filter
827        from_node (str): node id
828        to_node (str): node id
829    Returns:
830        A list of nodes that form the path
831    '''
832    try:
833        return nx.shortest_path(G, source=from_node['mesh_id'], target=to_node['mesh_id'])
834    except nx.NetworkXNoPath:
835        return []

Returns the path between two nodes in the graph Args: G (Networkx Graph): graph to filter from_node (str): node id to_node (str): node id Returns: A list of nodes that form the path

#   def search_paths_to_category(G, from_node, to_category: str):
View Source
838def search_paths_to_category(G, from_node, to_category: str):
839    '''
840    Returns the paths from 'from_node' to nodes with the type 'to_category'
841    Args:
842        G (Networkx Graph): graph to filter
843        from_node (str): node id
844        to_category (str): to_category to filter by: 'gene', 'disease', 'drug'
845    Returns:
846        A list of paths
847    '''
848    if to_category not in ['gene', 'disease', 'drug']:
849        raise ValueError("to_category must be one of 'gene', 'disease', 'drug'")
850
851    targets_nodes = filter_by_category(G, to_category)
852    paths = []
853    for target_node in targets_nodes:
854        paths.append(search_path(G, from_node, target_node))
855
856    # remove empty paths
857    paths = [path for path in paths if len(path) > 0]
858
859    return paths

Returns the paths from 'from_node' to nodes with the type 'to_category' Args: G (Networkx Graph): graph to filter from_node (str): node id to_category (str): to_category to filter by: 'gene', 'disease', 'drug' Returns: A list of paths

#   def get_graph_from_path(G, path):
View Source
862def get_graph_from_path(G, path):
863    '''
864    Returns the subgraph that contains the nodes in the path
865    Args:
866        G (Networkx Graph): graph to filter
867        path (list): list of nodes mesh_id
868    Returns:
869        A new graph containing the nodes in the path
870    '''
871    return G.subgraph(path).copy()

Returns the subgraph that contains the nodes in the path Args: G (Networkx Graph): graph to filter path (list): list of nodes mesh_id Returns: A new graph containing the nodes in the path

#   def get_graph_from_paths(G, paths):
View Source
874def get_graph_from_paths(G, paths):
875    '''
876    Returns the subgraph that contains the nodes in the paths
877    Args:
878        G (Networkx Graph): graph to filter
879        paths (list): list of paths
880    Returns:
881        A new graph containing the nodes in the paths
882    '''
883    nodes = [node for path in paths for node in path]
884    return G.subgraph(nodes).copy()

Returns the subgraph that contains the nodes in the paths Args: G (Networkx Graph): graph to filter paths (list): list of paths Returns: A new graph containing the nodes in the paths