Visualizing and Analyzing Proteins in Python | by Aren Carpenter | Towards Data Science

Human biology is incredibly complex. Even with our ever-growing understanding, our answers only uncover more and more questions. The completion of the Human Genome Project gave many scientists confidence that we could solve pressing issues in biology through genomics. However, as our understanding of biology has grown, we’ve recognized that other factors influence how an organism’s genome is utilized. Thus, new fields of study were born to address these interconnected and flexible domains, including transcriptomics (study of mRNA) and proteomics (study of proteins).
As I covered in my previous blog, the Biopython package is quite powerful and can visualize and analyze DNA and RNA sequences simply. And it has protein analysis capabilities, too! So let’s dive in.
The Protein Data Bank is a one-stop shop for exploring and downloading protein sequences. PDB developed its own file format for this purpose —the aptly named, .pdb. But as larger, more complex proteins were analyzed another format was developed — CIF and mmCIF. CIF (Crystallographic Information File) was developed to archive small molecule crystallographic experiments studying the arrangement of atoms in crystalline solids. CIF was expanded to larger molecules (macromolecules, hence mm) with mmCIF and has replaced the PDB format. [1]
While mmCIF is now the standard, there is still legacy support for PDB files.
Let’s look at Phytohemagglutinin-L, a lectin found in certain legumes, like kidney beans.
Import the necessary packages:
from Bio.PDB import *import nglview as nvimport ipywidgets
Now we’ll create an instance of Biopython’s PDBParser, and use the nglview library to create our interactive visualization. We can pan, zoom, and rotate the molecule and even hover for specific atom information.
pdb_parser = PDBParser()structure = pdb_parser.get_structure("PHA-L", "Data/1FAT.pdb")view = nv.show_biopython(structure)
Our process for CIF files will be very similar, just utilize the MMCIF Parser instead! Here we’re visualizing a larger protein 6EBK, or the voltage-activated Kv1.2–2.1 paddle chimera channel in lipid nanodiscs (a mouthful...)
cif_parser = MMCIFParser()structure = cif_parser.get_structure("6EBK", "fa/6ebk.cif")view = nv.show_biopython(structure)
The most immediate way to access information about the protein is through the Header, a dictionary of metadata, available in both PDB and CIF file formats.
mmcif_dict = MMCIFDict.MMCIFDict("fa/1fat.cif")len(mmcif_dict) # 689
This generates a large dictionary of information about the protein, including the citation that sequenced the protein, structure information, atom by atom location and angles, and chemical compositions. As you can see, there are 689 items in this dictionary.
One of the most important pieces of information you’ll want to analyze is the residue — think amino acid — sequence of the protein or polypeptide. Because proteins can be comprised of several polypeptides, we use a framework to know which level of organization we are examining. From the overall structure, down to individual atoms.
The Structure object in our file follows the SMCRA architecture in a parent-child relationship:
A Structure consists of models
A Model consists of chains
A Chain consists of residues (amino acids)
A Residue consists of Atoms
There are many ways to parse the structure metadata to return the protein’s residue sequence. Three ways are below:
# .get_residues() method in a loopfor model in structure:    for residue in model.get_residues():        print(residue)# .get_residues() method as generator objectresidues = structure.get_residues() # returns a generator object[item for item in residues]# .unfold_entities - keyword for each level of the SMCRA structureSelection.unfold_entities(structure, "R") # R is for residues
Getting the residue sequence above returns the sequence for the entire protein structure, but proteins are often comprised of several smaller polypeptides that we may want to analyze separately. Biopython enables this through polypeptide builders that can generate these individual polypeptides.
polypeptide_builder = CaPPBuilder()counter = 1for polypeptide in polypeptide_builder.build_peptides(structure):    seq = polypeptide.get_sequence()    print(f"Sequence: {counter}, Length: {len(seq)}")    print(seq)    counter += 1# Sequence: 1, Length: 36# SNDIYFNFQRFNETNLILQRDASVSSSGQLRLTNLN# Sequence: 2, Length: 196# NGEPRVGSLGRAFYSAPIQIWDNTTGTVASFATSFT...ASKLS# Sequence: 3, Length: 233# SNDIYFNFQRFNETNLILQRDASVSSSGQLRLTNLN...ASKLS# Sequence: 4, Length: 36# SNDIYFNFQRFNETNLILQRDASVSSSGQLRLTNLN# Sequence: 5, Length: 196# NGEPRVGSLGRAFYSAPIQIWDNTTGTVASFATSFT...ASKLS# Sequence: 6, Length: 35# SNDIYFNFQRFNETNLILQRDASVSSSGQLRLTNL# Sequence: 7, Length: 196# NGEPRVGSLGRAFYSAPIQIWDNTTGTVASFATSFT...ASKLS
So now we have the residue sequences for these 7 chains, but we also have access to many methods for analyzing these sequences.
from Bio.SeqUtils.ProtParam import ProteinAnalysis
The only caveat is that calling .get_sequences() above returns a Biopython Seq() object — check out my previous blog to dive deeper into Seq() objects and their functionality — and ProteinAnalysis expects a string.
analyzed_seq = ProteinAnalysis(str(seq))
We are now ready to run the following methods to build understanding of our sequence!
We can calculate the molecular weight of the polypeptide.
analyzed_seq.molecular_weight()# 4176.51669
Protein GRAVY returns the GRAVY (grand average of hydropathy) value for the protein sequences you enter. The GRAVY value is calculated by adding the hydropathy value for each residue and dividing by the length of the sequence (Kyte and Doolittle; 1982). [2]
A higher value is more hydrophobic. A lower value is more hydrophilic. We’ll discuss later how to generate residue by residue hydrophobicity later.
analyzed_seq.gravy()# -0.5611
We can easily count the number of each type of amino acid.
analyzed_seq.count_amino_acids()# {'A': 1, 'C': 0, 'D': 2, 'E': 1, 'F': 3, 'G': 1, 'H': 0, 'I': 2, 'K': 0, 'L': 5, 'M': 0, 'N': 6, 'P': 0, 'Q': 3, 'R': 3, 'S': 5, 'T': 2, 'V': 1, 'W': 0, 'Y': 1}
And the percentage of each amino acid in the sequence!
analyzed_seq.get_amino_acids_percent()# {'A': 0.027777777777777776, 'C': 0.0, 'D': 0.05555555555555555, 'E': 0.027777777777777776, 'F': 0.08333333333333333, 'G': 0.027777777777777776, 'H': 0.0, 'I': 0.05555555555555555, 'K': 0.0, 'L': 0.1388888888888889, 'M': 0.0, 'N': 0.16666666666666666, 'P': 0.0, 'Q': 0.08333333333333333, 'R': 0.08333333333333333, 'S': 0.1388888888888889, 'T': 0.05555555555555555, 'V': 0.027777777777777776, 'W': 0.0, 'Y': 0.027777777777777776}
A very useful method — .secondary_structure_fraction() — returns the fraction of amino acids that tend to be found in the three classical secondary structures. These are beta sheets, alpha helixes, and turns (where the residues change direction).
analyzed_seq.secondary_structure_fraction() # helix, turn, sheet# (0.3333333333333333, 0.3333333333333333, 0.19444444444444445)
Protein scales are a way of measuring certain attributes of residues over the length of the peptide sequence using a sliding window. Scales are comprised of values for each amino acid based on different physical and chemical properties, such as hydrophobicity, secondary structure tendencies, and surface accessibility. As opposed to some chain-level measures like overall molecule behavior, scales allow a more granular understanding of how smaller sections of the sequence will behave.
from Bio.SeqUtils.ProtParam import ProtParamData
Some common scales include:
kd → Kyte & Doolittle Index of Hydrophobicity [Original Article]
Flex → Normalized average flexibility parameters (B-values) [Original Article]
hw → Hopp & Wood Index of Hydrophilicity [Original Article]
em → Emini Surface fractional probability (Surface Accessibility) [Original Book]
Documentation on some common scales can be found here.
Let’s look at the Index of Hydrophobicity (kd) as an example. Here is the scale, where each residue has an associated value representing its level of hydrophobicity.
kd = {"A": 1.8, "R": -4.5, "N": -3.5, "D": -3.5, "C": 2.5,       "Q": -3.5, "E": -3.5, "G": -0.4, "H": -3.2, "I": 4.5,       "L": 3.8, "K": -3.9, "M": 1.9, "F": 2.8, "P": -1.6,       "S": -0.8, "T": -0.7, "W": -0.9, "Y": -1.3, "V": 4.2}
Positive values are hydrophobic. Isoleucine (I) and Valine (V) are the most hydrophobic, and Arginine (R) and Lysine (K) are the most hydrophilic. Hydrophobic residues are generally located internally in the polypeptide, while hydrophilic residues are external, so this scale also gives a sense of how this polypeptide may be folded.
Protein scale analysis requires setting a window size over which an average value is calculated. You can also specify using the “edge” keyword how important the neighboring residues are, basically weighting their importance to the average for the window.
analysed_seq.protein_scale(window=7, param_dict=ProtParamData.kd)# [-0.7571428571428572, -0.2428571428571429, -0.24285714285714288, -0.38571428571428573, -0.6285714285714287, -0.942857142857143, -1.842857142857143, -1.442857142857143, -2.3428571428571425, -1.3000000000000003, -0.01428571428571433, 0.1285714285714285, 0.1285714285714285, -0.014285714285714235, -0.4142857142857143, 0.3428571428571428, -0.31428571428571417, -0.35714285714285715, -1.014285714285714, -0.6285714285714284, -0.10000000000000002, 0.3428571428571429, -0.4142857142857142, 0.24285714285714285, -1.0, -0.34285714285714286, -0.32857142857142857, -0.7142857142857143, -0.1142857142857144, -0.11428571428571435]
Let’s bring all of our methods together and create a script that can iterate through each chain in our structure and run some routine analysis. We will create an empty container and then populate it with a dictionary of key information for each sequence. Once in this nested structure, we can slice as we would any container in Python to select individual entries.
# Create empty list for chainsall_seqs = []counter = 1# For each polypeptide in the structure, run protein analysis methods and store in dictfor pp in ppb.build_peptides(structure):    seq_info = {} # create an empty dict    seq = pp.get_sequence() # get the sequence like above    analyzed_seq = ProteinAnalysis(str(seq)) # needs to be a str     # Specify dict keys and values        seq_info['Sequence Number'] = counter # set sequence id    seq_info['Sequence'] = seq # store BioPython Seq() object    seq_info['Sequence Length'] = len(seq) # length of seq    seq_info['Molecular Weight'] = analyzed_seq.molecular_weight()    seq_info['GRAVY'] = analyzed_seq.gravy() # hydrophobicity     seq_info['AA Count'] = analyzed_seq.count_amino_acids()     seq_info['AA Percent'] = analyzed_seq.get_amino_acids_percent()    # tuple of (helix, turn, sheet)    seq_info['Secondary Structure'] = \        analyzed_seq.secondary_structure_fraction()        # Update all_seqs list and increase counter    all_seqs.append(seq_info)    counter += 1
Selecting the first sequence returns the dictionary with our analyses and values!
all_seqs[0] # select the first sequence# {'Sequence Number': 1, 'Sequence': Seq('SNDIYFNFQRFNETNLILQRDASVSSSGQLRLTNLN'), 'Sequence Length': 36, 'Molecular Weight': 4176.52, 'GRAVY': -0.5611, 'Amino Acid Count': {'A': 1,  'C': 0,  'D': 2,  'E': 1,  'F': 3,  'G': 1,  'H': 0,  'I': 2,  'K': 0,  'L': 5,  'M': 0,  'N': 6,  'P': 0,  'Q': 3,  'R': 3,  'S': 5,  'T': 2,  'V': 1,  'W': 0,  'Y': 1}, 'Amino Acid Percent': {'A': 0.027777777777777776,  'C': 0.0,  'D': 0.05555555555555555,  'E': 0.027777777777777776,  'F': 0.08333333333333333,  'G': 0.027777777777777776,  'H': 0.0,  'I': 0.05555555555555555,  'K': 0.0,  'L': 0.1388888888888889,  'M': 0.0,  'N': 0.16666666666666666,  'P': 0.0,  'Q': 0.08333333333333333,  'R': 0.08333333333333333,  'S': 0.1388888888888889,  'T': 0.05555555555555555,  'V': 0.027777777777777776,  'W': 0.0,  'Y': 0.027777777777777776}, 'Secondary Structure': (0.3333333333333333,  0.3333333333333333,  0.19444444444444445)}
We can select specific values easily, too.
all_seqs[0]['Sequence']# Seq('SNDIYFNFQRFNETNLILQRDASVSSSGQLRLTNLN')all_seqs[0]['Molecular Weight']# 4176.52
Not only does Biopython make working with DNA sequences easy, it also can be leveraged for proteomics to visualize and analyze proteins. It provides powerful and flexible methods for routine protein analysis that can be used to develop custom pipelines based on your specific needs. I know that as I continue to dive deeper into what Biopython has to offer, I will continue to be impressed, so you can expect more articles covering its capabilities in the future.
As always, all of the code and dependencies described in this article can be found in this repo, which I will continue to update as I explore Biopython. I hope this guide shows you how simple it can be to start your own bioinformatics projects with Biopython, and I look forward to seeing what you can create!
I’m always looking to connect and explore other projects! You can follow me on GitHub or LinkedIn, and check out my other stories on Medium. I also have a Twitter!
[1] R. K. Green, Beginner’s Guide to PDB Structures and the PDBx/mmCIF Format.
[2] J. Kyte and R.F. Doolittle, A simple method for displaying the hydropathic character of a protein (1983), J. Mol. Biol. 157 (1): 105–32.