Building the dataset of research papers

(Adapted from: Building the "evolution" research papers dataset - Luís F. Simões. Converted to Python 3 and minor changes by Tobias Kuhn, 2015-10-22.)


The Entrez module, a part of the Biopython library, will be used to interface with PubMed.
You can download Biopython from here.

In this notebook we will be covering several of the steps taken in the Biopython Tutorial, specifically in Chapter 9 Accessing NCBI’s Entrez databases.


In [1]:
from Bio import Entrez

# NCBI requires you to set your email address to make use of NCBI's E-utilities
Entrez.email = "Your.Name.Here@example.org"

The datasets will be saved as serialized Python objects, compressed with bzip2. Saving/loading them will therefore require the pickle and bz2 modules.


In [2]:
import pickle, bz2, os

EInfo: Obtaining information about the Entrez databases


In [3]:
# accessing extended information about the PubMed database
pubmed = Entrez.read( Entrez.einfo(db="pubmed"), validate=False )[u'DbInfo']

# list of possible search fields for use with ESearch:
search_fields = { f['Name']:f['Description'] for f in pubmed["FieldList"] }

In search_fields, we find 'TIAB' ('Free text associated with Abstract/Title') as a possible search field to use in searches.


In [4]:
search_fields


Out[4]:
{'AFFL': "Author's institutional affiliation and address",
 'ALL': 'All terms from all searchable fields',
 'AUCL': 'Author Cluster ID',
 'AUID': 'Author Identifier',
 'AUTH': 'Author(s) of publication',
 'BOOK': 'ID of the book that contains the document',
 'CDAT': 'Date of completion',
 'CNTY': 'Country of publication',
 'COLN': 'Corporate Author of publication',
 'CRDT': 'Date publication first accessible through Entrez',
 'DSO': 'Additional text from the summary',
 'ECNO': 'EC number for enzyme or CAS registry number',
 'ED': "Section's Editor",
 'EDAT': 'Date publication first accessible through Entrez',
 'EID': 'Extended PMID',
 'EPDT': 'Date of Electronic publication',
 'FAUT': 'First Author of publication',
 'FILT': 'Limits the records',
 'FINV': 'Full name of investigator',
 'FULL': 'Full Author Name(s) of publication',
 'GRNT': 'NIH Grant Numbers',
 'INVR': 'Investigator',
 'ISBN': 'ISBN',
 'ISS': 'Issue number of publication',
 'JOUR': 'Journal abbreviation of publication',
 'LANG': 'Language of publication',
 'LAUT': 'Last Author of publication',
 'LID': 'ELocation ID',
 'MAJR': 'MeSH terms of major importance to publication',
 'MDAT': 'Date of last modification',
 'MESH': 'Medical Subject Headings assigned to publication',
 'MHDA': 'Date publication was indexed with MeSH terms',
 'OTRM': 'Other terms associated with publication',
 'PAGE': 'Page number(s) of publication',
 'PAPX': 'MeSH pharmacological action pre-explosions',
 'PDAT': 'Date of publication',
 'PID': 'Publisher ID',
 'PPDT': 'Date of print publication',
 'PS': 'Personal Name as Subject',
 'PTYP': 'Type of publication (e.g., review)',
 'PUBN': "Publisher's name",
 'SI': 'Cross-reference from publication to other databases',
 'SUBH': 'Additional specificity for MeSH term',
 'SUBS': 'CAS chemical name or MEDLINE Substance Name',
 'TIAB': 'Free text associated with Abstract/Title',
 'TITL': 'Words in title of publication',
 'TT': 'Words in transliterated title of publication',
 'UID': 'Unique number assigned to publication',
 'VOL': 'Volume number of publication',
 'WORD': 'Free text associated with publication'}

ESearch: Searching the Entrez databases

To have a look at the kind of data we get when searching the database, we'll perform a search for papers authored by Haasdijk:


In [5]:
example_authors = ['Haasdijk E']
example_search = Entrez.read( Entrez.esearch( db="pubmed", term=' AND '.join([a+'[AUTH]' for a in example_authors]) ) )
example_search


Out[5]:
{'RetMax': '20', 'QueryTranslation': 'Haasdijk E[Author]', 'TranslationSet': [], 'TranslationStack': [{'Field': 'Author', 'Explode': 'N', 'Count': '30', 'Term': 'Haasdijk E[Author]'}, 'GROUP'], 'RetStart': '0', 'IdList': ['24977986', '24901702', '24852945', '24708899', '24252306', '23580075', '23144668', '22174697', '22154920', '21870131', '21760539', '20662596', '20602234', '20386726', '18579581', '18305242', '17913916', '17804640', '17686042', '17183535'], 'Count': '30'}

Note how the result being produced is not in Python's native string format:


In [6]:
type( example_search['IdList'][0] )


Out[6]:
Bio.Entrez.Parser.StringElement

The part of the query's result we are most interested in is accessible through


In [7]:
example_ids = [ int(id) for id in example_search['IdList'] ]
print(example_ids)


[24977986, 24901702, 24852945, 24708899, 24252306, 23580075, 23144668, 22174697, 22154920, 21870131, 21760539, 20662596, 20602234, 20386726, 18579581, 18305242, 17913916, 17804640, 17686042, 17183535]

PubMed IDs dataset

We will now assemble a dataset comprised of research articles containing the keyword "evolution", in either their titles or abstracts.


In [8]:
search_term = 'air'

In [9]:
Ids_file = 'data/' + search_term + '__Ids.pkl.bz2'

In [10]:
if os.path.exists( Ids_file ):
    Ids = pickle.load( bz2.BZ2File( Ids_file, 'rb' ) )
else:
    # determine the number of hits for the search term
    search = Entrez.read( Entrez.esearch( db="pubmed", term=search_term+'[TIAB]', retmax=0 ) )
    total = int( search['Count'] )
    
    # `Ids` will be incrementally assembled, by performing multiple queries,
    # each returning at most `retrieve_per_query` entries.
    Ids_str = []
    retrieve_per_query = 10000
    
    for start in range( 0, total, retrieve_per_query ):
        print('Fetching IDs of results [%d,%d]' % ( start, start+retrieve_per_query ) )
        s = Entrez.read( Entrez.esearch( db="pubmed", term=search_term+'[TIAB]', retstart=start, retmax=retrieve_per_query ) )
        Ids_str.extend( s[ u'IdList' ] )
    
    # convert Ids to integers (and ensure that the conversion is reversible)
    Ids = [ int(id) for id in Ids_str ]
    
    for (id_str, id_int) in zip(Ids_str, Ids):
        if str(id_int) != id_str:
            raise Exception('Conversion of PubMed ID %s from string to integer it not reversible.' % id_str )
    
    # Save list of Ids
    pickle.dump( Ids, bz2.BZ2File( Ids_file, 'wb' ) )
    
total = len( Ids )
print('%d documents contain the search term "%s".' % ( total, search_term ) )


190555 documents contain the search term "air".

Taking a look at what we just retrieved, here are the last 5 elements of the Ids list:


In [11]:
Ids[:5]


Out[11]:
[26489032, 26489005, 26488732, 26488527, 26488458]

ESummary: Retrieving summaries from primary IDs

To have a look at the kind of metadata we get from a call to Entrez.esummary(), we now fetch the summary of one of Haasdijk's papers (using one of the PubMed IDs we obtained in the previous section:


In [12]:
example_paper = Entrez.read( Entrez.esummary(db="pubmed", id='23144668') )[0]

def print_dict( p ):
    for k,v in p.items():
        print(k)
        print('\t', v)

print_dict(example_paper)


References
	 []
Source
	 Evol Intell
Volume
	 5
LastAuthor
	 Haasdijk E
Id
	 23144668
HasAbstract
	 1
PubDate
	 2012 Dec
AuthorList
	 ['Eiben AE', 'Kernbach S', 'Haasdijk E']
DOI
	 10.1007/s12065-012-0071-x
History
	 {'pubmed': ['2012/11/13 06:00'], 'revised': '2012/02/17 00:00', 'received': '2011/11/28 00:00', 'epublish': '2012/04/20 00:00', 'entrez': '2012/11/13 06:00', 'medline': ['2012/11/13 06:00'], 'accepted': '2012/03/22 00:00'}
FullJournalName
	 Evolutionary intelligence
PubStatus
	 ppublish+epublish
RecordStatus
	 PubMed
PubTypeList
	 ['Journal Article']
Title
	 Embodied artificial evolution: Artificial evolutionary systems in the 21st Century.
ESSN
	 1864-5917
ArticleIds
	 {'pubmed': ['23144668'], 'pmcid': 'pmc-id: PMC3490067;', 'eid': '23144668', 'pii': '71', 'doi': '10.1007/s12065-012-0071-x', 'rid': '23144668', 'medline': [], 'pmc': 'PMC3490067'}
ISSN
	 1864-5909
SO
	 2012 Dec;5(4):261-272
EPubDate
	 2012 Apr 20
PmcRefCount
	 2
LangList
	 ['English']
NlmUniqueID
	 101475575
Item
	 []
ELocationID
	 
Pages
	 261-272
Issue
	 4

For now, we'll keep just some basic information for each paper: title, list of authors, publication year, and DOI.

In case you are not familiar with the DOI system, know that the paper above can be accessed through the link http://dx.doi.org/10.1007/s12065-012-0071-x (which is http://dx.doi.org/ followed by the paper's DOI).


In [13]:
( example_paper['Title'], example_paper['AuthorList'], int(example_paper['PubDate'][:4]), example_paper['DOI'] )


Out[13]:
('Embodied artificial evolution: Artificial evolutionary systems in the 21st Century.',
 ['Eiben AE', 'Kernbach S', 'Haasdijk E'],
 2012,
 '10.1007/s12065-012-0071-x')

Summaries dataset

We are now ready to assemble a dataset containing the summaries of all the paper Ids we previously fetched.

To reduce the memory footprint, and to ensure the saved datasets won't depend on Biopython being installed to be properly loaded, values returned by Entrez.read() will be converted to their corresponding native Python types. We start by defining a function for helping with the conversion of strings:


In [14]:
Summaries_file = 'data/' + search_term + '__Summaries.pkl.bz2'

In [15]:
if os.path.exists( Summaries_file ):
    Summaries = pickle.load( bz2.BZ2File( Summaries_file, 'rb' ) )
else:
    # `Summaries` will be incrementally assembled, by performing multiple queries,
    # each returning at most `retrieve_per_query` entries.
    Summaries = []
    retrieve_per_query = 500
    
    print('Fetching Summaries of results: ')
    for start in range( 0, len(Ids), retrieve_per_query ):
        if (start % 10000 == 0):
            print('')
            print(start, end='')
        else:
            print('.', end='')
        
        # build comma separated string with the ids at indexes [start, start+retrieve_per_query)
        query_ids = ','.join( [ str(id) for id in Ids[ start : start+retrieve_per_query ] ] )
        
        s = Entrez.read( Entrez.esummary( db="pubmed", id=query_ids ) )
        
        # out of the retrieved data, we will keep only a tuple (title, authors, year, DOI), associated with the paper's id.
        # (all values converted to native Python formats)
        f = [
            ( int( p['Id'] ), (
                str( p['Title'] ),
                [ str(a) for a in p['AuthorList'] ],
                int( p['PubDate'][:4] ),                # keeps just the publication year
                str( p.get('DOI', '') )            # papers for which no DOI is available get an empty string in their place
                ) )
            for p in s
            ]
        Summaries.extend( f )
    
    # Save Summaries, as a dictionary indexed by Ids
    Summaries = dict( Summaries )
    
    pickle.dump( Summaries, bz2.BZ2File( Summaries_file, 'wb' ) )

Let us take a look at the first 3 retrieved summaries:


In [16]:
{ id : Summaries[id] for id in Ids[:3] }


Out[16]:
{26488732: ('Use of Whole-Genome Sequencing to Link Burkholderia pseudomallei from Air Sampling to Mediastinal Melioidosis, Australia.',
  ['Currie BJ',
   'Price EP',
   'Mayo M',
   'Kaestli M',
   'Theobald V',
   'Harrington I',
   'Harrington G',
   'Sarovich DS'],
  2015,
  '10.3201/eid2111.141802'),
 26489005: ('A simple chemical solution deposition of Co3O4 thin film electrocatalyst for oxygen evolution reaction.',
  ['Jeon HS', 'Jee MS', 'Kim H', 'Ahn SJ', 'Hwang YJ', 'Min BK'],
  2015,
  '10.1021/acsami.5b06189'),
 26489032: ('The Occurrence of Beer Spoilage Lactic Acid Bacteria in Craft Beer Production.',
  ['Garofalo C',
   'Osimani A',
   'Milanović V',
   'Taccari M',
   'Aquilanti L',
   'Clementi F'],
  2015,
  '10.1111/1750-3841.13112')}

EFetch: Downloading full records from Entrez

Entrez.efetch() is the function that will allow us to obtain paper abstracts. Let us start by taking a look at the kind of data it returns when we query PubMed's database.


In [17]:
q = Entrez.read( Entrez.efetch(db="pubmed", id='23144668', retmode="xml") )

q is a list, with each member corresponding to a queried id. Because here we only queried for one id, its results are then in q[0].


In [18]:
type(q), len(q)


Out[18]:
(Bio.Entrez.Parser.ListElement, 1)

At q[0] we find a dictionary containing two keys, the contents of which we print below.


In [19]:
type(q[0]), q[0].keys()


Out[19]:
(Bio.Entrez.Parser.DictionaryElement,
 dict_keys(['MedlineCitation', 'PubmedData']))

In [20]:
print_dict( q[0][ 'PubmedData' ] )


ArticleIdList
	 [StringElement('10.1007/s12065-012-0071-x', attributes={'IdType': 'doi'}), StringElement('71', attributes={'IdType': 'pii'}), StringElement('23144668', attributes={'IdType': 'pubmed'}), StringElement('PMC3490067', attributes={'IdType': 'pmc'})]
History
	 [DictElement({'Year': '2011', 'Month': '11', 'Day': '28'}, attributes={'PubStatus': 'received'}), DictElement({'Year': '2012', 'Month': '2', 'Day': '17'}, attributes={'PubStatus': 'revised'}), DictElement({'Year': '2012', 'Month': '3', 'Day': '22'}, attributes={'PubStatus': 'accepted'}), DictElement({'Year': '2012', 'Month': '4', 'Day': '20'}, attributes={'PubStatus': 'epublish'}), DictElement({'Year': '2012', 'Month': '11', 'Day': '13', 'Hour': '6', 'Minute': '0'}, attributes={'PubStatus': 'entrez'}), DictElement({'Year': '2012', 'Month': '11', 'Day': '13', 'Hour': '6', 'Minute': '0'}, attributes={'PubStatus': 'pubmed'}), DictElement({'Year': '2012', 'Month': '11', 'Day': '13', 'Hour': '6', 'Minute': '0'}, attributes={'PubStatus': 'medline'})]
PublicationStatus
	 ppublish

The key 'MedlineCitation' maps into another dictionary. In that dictionary, most of the information is contained under the key 'Article'. To minimize the clutter, below we show the contents of 'MedlineCitation' excluding its 'Article' member, and below that we then show the contents of 'Article'.


In [21]:
print_dict( { k:v for k,v in q[0][ 'MedlineCitation' ].items() if k!='Article' } )


OtherAbstract
	 []
DateCreated
	 {'Year': '2012', 'Month': '11', 'Day': '12'}
MedlineJournalInfo
	 {'ISSNLinking': '1864-5909', 'MedlineTA': 'Evol Intell', 'NlmUniqueID': '101475575'}
GeneralNote
	 []
SpaceFlightMission
	 []
PMID
	 23144668
CitationSubset
	 []
OtherID
	 []
KeywordList
	 []

In [22]:
print_dict( q[0][ 'MedlineCitation' ][ 'Article' ] )


Journal
	 {'ISSN': StringElement('1864-5909', attributes={'IssnType': 'Print'}), 'Title': 'Evolutionary intelligence', 'JournalIssue': DictElement({'Issue': '4', 'PubDate': {'Year': '2012', 'Month': 'Dec'}, 'Volume': '5'}, attributes={'CitedMedium': 'Print'}), 'ISOAbbreviation': 'Evol Intell'}
Language
	 ['ENG']
AuthorList
	 ListElement([DictElement({'LastName': 'Eiben', 'ForeName': 'A E', 'AffiliationInfo': [{'Affiliation': 'VU University Amsterdam, Amsterdam, The Netherlands.', 'Identifier': []}], 'Initials': 'AE', 'Identifier': []}, attributes={'ValidYN': 'Y'}), DictElement({'LastName': 'Kernbach', 'ForeName': 'S', 'AffiliationInfo': [], 'Initials': 'S', 'Identifier': []}, attributes={'ValidYN': 'Y'}), DictElement({'LastName': 'Haasdijk', 'ForeName': 'Evert', 'AffiliationInfo': [], 'Initials': 'E', 'Identifier': []}, attributes={'ValidYN': 'Y'})], attributes={'CompleteYN': 'Y', 'Type': 'authors'})
PublicationTypeList
	 [StringElement('JOURNAL ARTICLE', attributes={'UI': ''})]
Pagination
	 {'MedlinePgn': '261-272'}
ArticleDate
	 [DictElement({'Year': '2012', 'Month': '4', 'Day': '20'}, attributes={'DateType': 'Electronic'})]
ELocationID
	 []
ArticleTitle
	 Embodied artificial evolution: Artificial evolutionary systems in the 21st Century.
Abstract
	 {'AbstractText': ['Evolution is one of the major omnipresent powers in the universe that has been studied for about two centuries. Recent scientific and technical developments make it possible to make the transition from passively understanding to actively using evolutionary processes. Today this is possible in Evolutionary Computing, where human experimenters can design and manipulate all components of evolutionary processes in digital spaces. We argue that in the near future it will be possible to implement artificial evolutionary processes outside such imaginary spaces and make them physically embodied. In other words, we envision the "Evolution of Things", rather than just the evolution of digital objects, leading to a new field of Embodied Artificial Evolution (EAE). The main objective of this paper is to present a unifying vision in order to aid the development of this high potential research area. To this end, we introduce the notion of EAE, discuss a few examples and applications, and elaborate on the expected benefits as well as the grand challenges this developing field will have to address.']}

A paper's abstract can therefore be accessed with:


In [23]:
{ int(q[0]['MedlineCitation']['PMID']) : str(q[0]['MedlineCitation']['Article']['Abstract']['AbstractText'][0]) }


Out[23]:
{23144668: 'Evolution is one of the major omnipresent powers in the universe that has been studied for about two centuries. Recent scientific and technical developments make it possible to make the transition from passively understanding to actively using evolutionary processes. Today this is possible in Evolutionary Computing, where human experimenters can design and manipulate all components of evolutionary processes in digital spaces. We argue that in the near future it will be possible to implement artificial evolutionary processes outside such imaginary spaces and make them physically embodied. In other words, we envision the "Evolution of Things", rather than just the evolution of digital objects, leading to a new field of Embodied Artificial Evolution (EAE). The main objective of this paper is to present a unifying vision in order to aid the development of this high potential research area. To this end, we introduce the notion of EAE, discuss a few examples and applications, and elaborate on the expected benefits as well as the grand challenges this developing field will have to address.'}

A paper for which no abstract is available will simply not contain the 'Abstract' key in its 'Article' dictionary:


In [24]:
print_dict( Entrez.read( Entrez.efetch(db="pubmed", id='17782550', retmode="xml") )[0]['MedlineCitation']['Article'] )


Journal
	 {'ISSN': StringElement('0036-8075', attributes={'IssnType': 'Print'}), 'Title': 'Science (New York, N.Y.)', 'JournalIssue': DictElement({'Issue': '3', 'PubDate': {'Year': '1880', 'Month': 'Jul', 'Day': '17'}, 'Volume': '1'}, attributes={'CitedMedium': 'Print'}), 'ISOAbbreviation': 'Science'}
Language
	 ['eng']
PublicationTypeList
	 [StringElement('Journal Article', attributes={'UI': 'D016428'})]
Pagination
	 {'MedlinePgn': '35'}
ArticleDate
	 []
ELocationID
	 []
ArticleTitle
	 EVOLUTION OF LOCOMOTIVES IN AMERICA.

Some of the ids in our dataset refer to books from the NCBI Bookshelf, a collection of freely available, downloadable, on-line versions of selected biomedical books. For such ids, Entrez.efetch() returns a slightly different structure, where the keys [u'BookDocument', u'PubmedBookData'] take the place of the [u'MedlineCitation', u'PubmedData'] keys we saw above.

Here is an example of the data we obtain for the id corresponding to the book The Social Biology of Microbial Communities:


In [25]:
r = Entrez.read( Entrez.efetch(db="pubmed", id='24027805', retmode="xml") )

In [26]:
print_dict( r[0][ 'PubmedBookData' ] )


ArticleIdList
	 [StringElement('24027805', attributes={'IdType': 'pubmed'})]
History
	 [DictElement({'Year': '2013', 'Month': '9', 'Day': '13', 'Hour': '6', 'Minute': '0'}, attributes={'PubStatus': 'pubmed'}), DictElement({'Year': '2013', 'Month': '9', 'Day': '13', 'Hour': '6', 'Minute': '0'}, attributes={'PubStatus': 'medline'}), DictElement({'Year': '2013', 'Month': '9', 'Day': '13', 'Hour': '6', 'Minute': '0'}, attributes={'PubStatus': 'entrez'})]
PublicationStatus
	 ppublish

In [27]:
print_dict( r[0][ 'BookDocument' ] )


Sections
	 [{'SectionTitle': StringElement('THE NATIONAL ACADEMIES', attributes={'part': 'fm.s1', 'book': 'nap13500'}), 'Section': []}, {'SectionTitle': StringElement('PLANNING COMMITTEE FOR A WORKSHOP ON THE MICROBIOME IN HEALTH AND DISEASE', attributes={'part': 'fm.s2', 'book': 'nap13500'}), 'Section': []}, {'SectionTitle': StringElement('FORUM ON MICROBIAL THREATS', attributes={'part': 'fm.s3', 'book': 'nap13500'}), 'Section': []}, {'SectionTitle': StringElement('BOARD ON GLOBAL HEALTH', attributes={'part': 'fm.s5', 'book': 'nap13500'}), 'Section': []}, {'SectionTitle': StringElement('Reviewers', attributes={'part': 'fm.s7', 'book': 'nap13500'}), 'Section': []}, {'SectionTitle': StringElement('Acknowledgments', attributes={'part': 'fm.ack', 'book': 'nap13500'}), 'Section': []}, {'SectionTitle': StringElement('Workshop Overview', attributes={'part': 'workshop', 'book': 'nap13500'}), 'Section': []}, {'SectionTitle': StringElement('Appendixes', attributes={'part': 'nap13500.appgroup1', 'book': 'nap13500'}), 'Section': []}]
Language
	 ['eng']
PMID
	 24027805
ItemList
	 []
Abstract
	 {'CopyrightInformation': 'Copyright © 2012, National Academy of Sciences.', 'AbstractText': ['On March 6 and 7, 2012, the Institute of Medicine’s (IOM’s) Forum on Microbial Threats hosted a public workshop to explore the emerging science of the “social biology” of microbial communities. Workshop presentations and discussions embraced a wide spectrum of topics, experimental systems, and theoretical perspectives representative of the current, multifaceted exploration of the microbial frontier. Participants discussed ecological, evolutionary, and genetic factors contributing to the assembly, function, and stability of microbial communities; how microbial communities adapt and respond to environmental stimuli; theoretical and experimental approaches to advance this nascent field; and potential applications of knowledge gained from the study of microbial communities for the improvement of human, animal, plant, and ecosystem health and toward a deeper understanding of microbial diversity and evolution.']}
LocationLabel
	 []
ArticleIdList
	 [StringElement('NBK114831', attributes={'IdType': 'bookaccession'})]
Book
	 {'Publisher': {'PublisherName': 'National Academies Press (US)', 'PublisherLocation': 'Washington (DC)'}, 'AuthorList': [ListElement([DictElement({'CollectiveName': 'Institute of Medicine (US) Forum on Microbial Threats', 'AffiliationInfo': [], 'Identifier': []}, attributes={'ValidYN': 'Y'})], attributes={'CompleteYN': 'Y', 'Type': 'authors'})], 'BookTitle': StringElement('The Social Biology of Microbial Communities: Workshop Summary', attributes={'book': 'nap13500'}), 'Isbn': ['9780309264327', '0309264324'], 'CollectionTitle': StringElement('The National Academies Collection: Reports funded by National Institutes of Health', attributes={'book': 'napcollect'}), 'ELocationID': [], 'PubDate': {'Year': '2012'}}
AuthorList
	 []
PublicationType
	 []
KeywordList
	 []

In a book from the NCBI Bookshelf, its abstract can then be accessed as such:


In [28]:
{ int(r[0]['BookDocument']['PMID']) : str(r[0]['BookDocument']['Abstract']['AbstractText'][0]) }


Out[28]:
{24027805: 'On March 6 and 7, 2012, the Institute of Medicine’s (IOM’s) Forum on Microbial Threats hosted a public workshop to explore the emerging science of the “social biology” of microbial communities. Workshop presentations and discussions embraced a wide spectrum of topics, experimental systems, and theoretical perspectives representative of the current, multifaceted exploration of the microbial frontier. Participants discussed ecological, evolutionary, and genetic factors contributing to the assembly, function, and stability of microbial communities; how microbial communities adapt and respond to environmental stimuli; theoretical and experimental approaches to advance this nascent field; and potential applications of knowledge gained from the study of microbial communities for the improvement of human, animal, plant, and ecosystem health and toward a deeper understanding of microbial diversity and evolution.'}

Abstracts dataset

We can now assemble a dataset mapping paper ids to their abstracts.


In [29]:
Abstracts_file = 'data/' + search_term + '__Abstracts.pkl.bz2'

In [30]:
import http.client
from collections import deque

if os.path.exists( Abstracts_file ):
    Abstracts = pickle.load( bz2.BZ2File( Abstracts_file, 'rb' ) )
else:
    # `Abstracts` will be incrementally assembled, by performing multiple queries,
    # each returning at most `retrieve_per_query` entries.
    Abstracts = deque()
    retrieve_per_query = 500
    
    print('Fetching Abstracts of results: ')
    for start in range( 0, len(Ids), retrieve_per_query ):
        if (start % 10000 == 0):
            print('')
            print(start, end='')
        else:
            print('.', end='')
        
        # build comma separated string with the ids at indexes [start, start+retrieve_per_query)
        query_ids = ','.join( [ str(id) for id in Ids[ start : start+retrieve_per_query ] ] )
        
        # issue requests to the server, until we get the full amount of data we expect
        while True:
            try:
                s = Entrez.read( Entrez.efetch(db="pubmed", id=query_ids, retmode="xml" ) )
            except http.client.IncompleteRead:
                print('r', end='')
                continue
            break
        
        i = 0
        for p in s:
            abstr = ''
            if 'MedlineCitation' in p:
                pmid = p['MedlineCitation']['PMID']
                if 'Abstract' in p['MedlineCitation']['Article']:
                    abstr = p['MedlineCitation']['Article']['Abstract']['AbstractText'][0]
            elif 'BookDocument' in p:
                pmid = p['BookDocument']['PMID']
                if 'Abstract' in p['BookDocument']:
                    abstr = p['BookDocument']['Abstract']['AbstractText'][0]
            else:
                raise Exception('Unrecognized record type, for id %d (keys: %s)' % (Ids[start+i], str(p.keys())) )
            
            Abstracts.append( (int(pmid), str(abstr)) )
            i += 1
    
    # Save Abstracts, as a dictionary indexed by Ids
    Abstracts = dict( Abstracts )
    
    pickle.dump( Abstracts, bz2.BZ2File( Abstracts_file, 'wb' ) )

Taking a look at one paper's abstract:


In [31]:
Abstracts[26488732]


Out[31]:
'The frequency with which melioidosis results from inhalation rather than percutaneous inoculation or ingestion is unknown. We recovered Burkholderia pseudomallei from air samples at the residence of a patient with presumptive inhalational melioidosis and used whole-genome sequencing to link the environmental bacteria to B. pseudomallei recovered from the patient.'

To understand how to obtain paper citations with Entrez, we will first assemble a small set of PubMed IDs, and then query for their citations. To that end, we search here for papers published in the PLOS Computational Biology journal (as before, having also the word "air" in either the title or abstract):


In [32]:
CA_search_term = search_term+'[TIAB] AND PLoS computational biology[JOUR]'
CA_ids = Entrez.read( Entrez.esearch( db="pubmed", term=CA_search_term ) )['IdList']
CA_ids


Out[32]:
['24853675', '23737742', '23209398', '22383866', '20865052', '19300479']

In [33]:
CA_summ = {
    p['Id'] : ( p['Title'], p['AuthorList'], p['PubDate'][:4], p['FullJournalName'], p.get('DOI', '') )
    for p in Entrez.read( Entrez.esummary(db="pubmed", id=','.join( CA_ids )) )
    }
CA_summ


Out[33]:
{'19300479': ('Computational model of the insect pheromone transduction cascade.',
  ['Gu Y', 'Lucas P', 'Rospars JP'],
  '2009',
  'PLoS computational biology',
  '10.1371/journal.pcbi.1000321'),
 '20865052': ('Reverse engineering of oxygen transport in the lung: adaptation to changing demands and resources through space-filling networks.',
  ['Hou C', 'Gheorghiu S', 'Huxley VH', 'Pfeifer P'],
  '2010',
  'PLoS computational biology',
  '10.1371/journal.pcbi.1000902'),
 '22383866': ('A cell-based computational modeling approach for developing site-directed molecular probes.',
  ['Yu JY', 'Zheng N', 'Mane G', 'Min KA', 'Hinestroza JP', 'Zhu H', 'Stringer KA', 'Rosania GR'],
  '2012',
  'PLoS computational biology',
  '10.1371/journal.pcbi.1002378'),
 '23209398': ('Experimental studies and dynamics modeling analysis of the swimming and diving of whirligig beetles (Coleoptera: Gyrinidae).',
  ['Xu Z', 'Lenaghan SC', 'Reese BE', 'Jia X', 'Zhang M'],
  '2012',
  'PLoS computational biology',
  '10.1371/journal.pcbi.1002792'),
 '23737742': ('A mechanical design principle for tissue structure and function in the airway tree.',
  ['LaPrad AS', 'Lutchen KR', 'Suki B'],
  '2013',
  'PLoS computational biology',
  '10.1371/journal.pcbi.1003083'),
 '24853675': ('Evolution of pathogen specialisation in a host metapopulation: joint effects of host and pathogen dispersal.',
  ['Papaïx J', 'Burdon JJ', 'Lannou C', 'Thrall PH'],
  '2014',
  'PLoS computational biology',
  '10.1371/journal.pcbi.1003633')}

Because we restricted our search to papers in an open-access journal, you can then follow their DOIs to freely access their PDFs at the journal's website:
10.1371/journal.pcbi.0040023, 10.1371/journal.pcbi.1000948, 10.1371/journal.pcbi.1002236.

We will now issue calls to Entrez.elink() using these PubMed IDs, to retrieve the IDs of papers that cite them. The database from which the IDs will be retrieved is PubMed Central, a free digital database of full-text scientific literature in the biomedical and life sciences.

You can, for instance, find archived here, with the PubMed Central ID 2951343, the paper "Critical dynamics in the evolution of stochastic strategies for the iterated prisoner's dilemma", which as we saw above, has the PubMed ID 20949101.

A complete list of the kinds of links you can retrieve with Entrez.elink() can be found here.


In [34]:
CA_citing = {
    id : Entrez.read( Entrez.elink(
            cmd = "neighbor",               # ELink command mode: "neighbor", returns
                                            #     a set of UIDs in `db` linked to the input UIDs in `dbfrom`.
            dbfrom = "pubmed",              # Database containing the input UIDs: PubMed
            db = "pmc",                     # Database from which to retrieve UIDs: PubMed Central
            LinkName = "pubmed_pmc_refs",   # Name of the Entrez link to retrieve: "pubmed_pmc_refs", gets
                                            #     "Full-text articles in the PubMed Central Database that cite the current articles"
            from_uid = id                   # input UIDs
            ) )
    for id in CA_ids
    }

CA_citing['24853675']


Out[34]:
[{'LinkSetDbHistory': [], 'IdList': ['24853675'], 'ERROR': [], 'DbFrom': 'pubmed', 'LinkSetDb': [{'Link': [{'Id': '4408149'}, {'Id': '4105914'}], 'DbTo': 'pmc', 'LinkName': 'pubmed_pmc_refs'}]}]

We have in CA_citing[paper_id][0]['LinkSetDb'][0]['Link'] the list of papers citing paper_id. To get it as just a list of ids, we can do


In [35]:
cits = [ l['Id'] for l in CA_citing['24853675'][0]['LinkSetDb'][0]['Link'] ]
cits


Out[35]:
['4408149', '4105914']

However, one more step is needed, as what we have now are PubMed Central IDs, and not PubMed IDs. Their conversion can be achieved through an additional call to Entrez.elink():


In [36]:
cits_pm = Entrez.read( Entrez.elink( dbfrom="pmc", db="pubmed", LinkName="pmc_pubmed", from_uid=",".join(cits)) )
cits_pm


Out[36]:
[{'LinkSetDbHistory': [], 'IdList': ['4105914', '4408149'], 'ERROR': [], 'DbFrom': 'pmc', 'LinkSetDb': [{'Link': [{'Id': '25926883'}, {'Id': '25067946'}], 'DbTo': 'pubmed', 'LinkName': 'pmc_pubmed'}]}]

In [37]:
ids_map = { pmc_id : link['Id'] for (pmc_id,link) in zip(cits_pm[0]['IdList'], cits_pm[0]['LinkSetDb'][0]['Link']) }
ids_map


Out[37]:
{'4105914': '25926883', '4408149': '25067946'}

And to check these papers:


In [38]:
{   p['Id'] : ( p['Title'], p['AuthorList'], p['PubDate'][:4], p['FullJournalName'], p.get('DOI', '') )
    for p in Entrez.read( Entrez.esummary(db="pubmed", id=','.join( ids_map.values() )) )
    }


Out[38]:
{'25067946': ('Guiding deployment of resistance in cereals using evolutionary principles.',
  ['Burdon JJ', 'Barrett LG', 'Rebetzke G', 'Thrall PH'],
  '2014',
  'Evolutionary applications',
  '10.1111/eva.12175'),
 '25926883': ('Crop pathogen emergence and evolution in agro-ecological landscapes.',
  ['Papaïx J', 'Burdon JJ', 'Zhan J', 'Thrall PH'],
  '2015',
  'Evolutionary applications',
  '10.1111/eva.12251')}

Citations dataset

We have now seen all the steps required to assemble a dataset of citations to each of the papers in our dataset.


In [39]:
Citations_file = 'data/' + search_term + '__Citations.pkl.bz2'
Citations = []

At least one server query will be issued per paper in Ids. Because NCBI allows for at most 3 queries per second (see here), this dataset will take a long time to assemble. Should you need to interrupt it for some reason, or the connection fail at some point, it is safe to just rerun the cell below until all data is collected.


In [40]:
import http.client

if Citations == [] and os.path.exists( Citations_file ):
    Citations = pickle.load( bz2.BZ2File( Citations_file, 'rb' ) )

if len(Citations) < len(Ids):
    
    i = len(Citations)
    checkpoint = len(Ids) / 10 + 1      # save to hard drive at every 10% of Ids fetched
    
    for pm_id in Ids[i:]:               # either starts from index 0, or resumes from where we previously left off
        
        while True:
            try:
                # query for papers archived in PubMed Central that cite the paper with PubMed ID `pm_id`
                c = Entrez.read( Entrez.elink( dbfrom = "pubmed", db="pmc", LinkName = "pubmed_pmc_refs", id=str(pm_id) ) )
                
                c = c[0]['LinkSetDb']
                if len(c) == 0:
                    # no citations found for the current paper
                    c = []
                else:
                    c = [ l['Id'] for l in c[0]['Link'] ]
                    
                    # convert citations from PubMed Central IDs to PubMed IDs
                    p = []
                    retrieve_per_query = 500
                    for start in range( 0, len(c), retrieve_per_query ):
                        query_ids = ','.join( c[start : start+retrieve_per_query] )
                        r = Entrez.read( Entrez.elink( dbfrom="pmc", db="pubmed", LinkName="pmc_pubmed", from_uid=query_ids ) )
                        # select the IDs. If no matching PubMed ID was found, [] is returned instead
                        p.extend( [] if r[0]['LinkSetDb']==[] else [ int(link['Id']) for link in r[0]['LinkSetDb'][0]['Link'] ] )
                    c = p
            
            except http.client.BadStatusLine:
                # Presumably, the server closed the connection before sending a valid response. Retry until we have the data.
                print('r')
                continue
            break
        
        Citations.append( (pm_id, c) )
        if (i % 10000 == 0):
            print('')
            print(i, end='')
        if (i % 100 == 0):
            print('.', end='')
        i += 1
        
        if i % checkpoint == 0:
            print('\tsaving at checkpoint', i)
            pickle.dump( Citations, bz2.BZ2File( Citations_file, 'wb' ) )
    
    print('\n done.')
    
    # Save Citations, as a dictionary indexed by Ids
    Citations = dict( Citations )
    
    pickle.dump( Citations, bz2.BZ2File( Citations_file, 'wb' ) )

To see that we have indeed obtained the data we expected, you can match the ids below, with the ids listed at the end of last section.


In [41]:
Citations[24853675]


Out[41]:
[25926883, 25067946]

Where do we go from here?

Running the code above generates multiple local files, containing the datasets we'll be working with. Loading them into memory is a matter of just issuing a call like
data = pickle.load( bz2.BZ2File( data_file, 'rb' ) ).

The Entrez module will therefore no longer be needed, unless you wish to extend your data processing with additional information retrieved from PubMed.

Should you be interested in looking at alternative ways to handle the data, have a look at the sqlite3 module included in Python's standard library, or Pandas, the Python Data Analysis Library.