Example Notebook: BioCypher and Pandas
Tip: You can run the tutorial interactively in Google Colab.
Introduction
The main purpose of BioCypher is to facilitate the pre-processing of biomedical data, and thus save development time in the maintenance of curated knowledge graphs, while allowing simple and efficient creation of task-specific lightweight knowledge graphs in a user-friendly and biology-centric fashion.
We are going to use a toy example to familiarise the user with the basic functionality of BioCypher. One central task of BioCypher is the harmonisation of dissimilar datasets describing the same entities. Thus, in this example, the input data - which in the real-world use case could come from any type of interface - are represented by simulated data containing some examples of differently formatted biomedical entities such as proteins and their interactions.
There are two other versions of this tutorial, which only differ in the output format. The first uses a CSV output format to write files suitable for Neo4j admin import, and the second creates an in-memory collection of Pandas dataframes. You can find the former in the tutorial directory of the BioCypher repository. This tutorial simply takes the latter, in-memory approach to a Jupyter notebook.
While BioCypher was designed as a graph-focused framework, due to commonalities in bioinformatics workflows, BioCypher also supports Pandas DataFrames. This allows integration with methods that use tabular data, such as machine learning and statistical analysis, for instance in the scVerse framework.
Setup
To run this tutorial interactively, you will first need to install perform some setup steps specific to running on Google Colab. You can collapse this section and run the setup steps with one click, as they are not required for the explanation of BioCyper’s functionality. You can of course also run the steps one by one, if you want to see what is happening. The real tutorial starts with section 1, “Adding data” (do not follow this link on colab, as you will be taken back to the website; please scroll down instead).
[ ]:
!pip install biocypher
Tutorial files
In the biocypher
root directory, you will find a tutorial
directory with the files for this tutorial. The data_generator.py
file contains the simulated data generation code, and the other files, specifically the .yaml
files, are named according to the tutorial step they are used in.
Let’s download these:
[3]:
import yaml
import requests
import subprocess
schema_path = "https://raw.githubusercontent.com/biocypher/biocypher/main/tutorial/"
[ ]:
!wget -O data_generator.py "https://github.com/biocypher/biocypher/raw/main/tutorial/data_generator.py"
[ ]:
owner = "biocypher"
repo = "biocypher"
path = "tutorial" # The path within the repository (optional, leave empty for the root directory)
github_url = "https://api.github.com/repos/{owner}/{repo}/contents/{path}"
api_url = github_url.format(owner=owner, repo=repo, path=path)
response = requests.get(api_url)
# Get list of yaml files from the repo
files = response.json()
yamls = []
for file in files:
if file["type"] == "file":
if file["name"].endswith(".yaml"):
yamls.append(file["name"])
# wget all yaml files
for yaml in yamls:
url_path = schema_path + yaml
subprocess.run(["wget", url_path])
Let’s also define functions with which we can visualize those
[ ]:
# helper function to print yaml files
import yaml
def print_yaml(file_path):
with open(file_path, 'r') as file:
yaml_data = yaml.safe_load(file)
print("--------------")
print(yaml.dump(yaml_data, sort_keys=False, indent=4))
print("--------------")
Configuration
BioCypher is configured using a YAML file; it comes with a default (which you can see in the Configuration section). You can use it, for instance, to select an output format, the output directory, separators, logging level, and other options. For this tutorial, we will use a dedicated configuration file for each of the steps. The configuration files are located in the tutorial
directory, and are called using the
biocypher_config_path
argument at instantiation of the BioCypher interface. For more information, see also the Quickstart Configuration section.
Section 1: Adding data
Input data stream (“adapter”)
The basic operation of adding data to the knowledge graph requires two components: an input stream of data (which we call adapter) and a configuration for the resulting desired output (the schema configuration). The former will be simulated by calling the Protein
class of our data generator 10 times.
[ ]:
# create a list of proteins to be imported
from data_generator import Protein
n_proteins = 3
proteins = [Protein() for _ in range(n_proteins)]
Each protein in our simulated data has a UniProt ID, a label (“uniprot_protein”), and a dictionary of properties describing it. This is - purely by coincidence - very close to the input BioCypher expects (for nodes): - a unique identifier - an input label (to allow mapping to the ontology, see the second step below) - a dictionary of further properties (which can be empty)
These should be presented to BioCypher in the form of a tuple. To achieve this representation, we can use a generator function that iterates through our simulated input data and, for each entity, forms the corresponding tuple. The use of a generator allows for efficient streaming of larger datasets where required.
[ ]:
def node_generator(proteins):
for protein in proteins:
yield (
protein.get_id(),
protein.get_label(),
protein.get_properties(),
)
entities = node_generator(proteins)
The concept of an adapter can become arbitrarily complex and involve programmatic access to databases, API requests, asynchronous queries, context managers, and other complicating factors. However, it always boils down to providing the BioCypher driver with a collection of tuples, one for each entity in the input data. For more info, see the section on Adapters.
As descibed above, nodes possess:
a mandatory ID,
a mandatory label, and
a property dictionary,
while edges possess:
an (optional) ID,
two mandatory IDs for source and target,
a mandatory label, and
a property dictionary.
How these entities are mapped to the ontological hierarchy underlying a BioCypher graph is determined by their mandatory labels, which connect the input data stream to the schema configuration. This we will see in the following section.
Schema configuration
How each BioCypher graph is structured is determined by the schema configuration YAML file that is given to the BioCypher interface. This also serves to ground the entities of the graph in the biomedical realm by using an ontological hierarchy. In this tutorial, we refer to the Biolink model as the general backbone of our ontological hierarchy. The basic premise of the schema configuration YAML file is that each component of the desired knowledge graph output should be configured here; if (and only if) an entity is represented in the schema configuration and is present in the input data stream, will it be part of our knowledge graph.
In our case, since we only import proteins, we only require few lines of configuration:
[ ]:
print_yaml('01_schema_config.yaml')
--------------
protein:
represented_as: node
preferred_id: uniprot
input_label: uniprot_protein
--------------
The first line (protein
) identifies our entity and connects to the ontological backbone; here we define the first class to be represented in the graph. In the configuration YAML, we represent entities — similar to the internal representation of Biolink — in lower sentence case (e.g., “small molecule”). Conversely, for class names, in file names, and property graph labels, we use PascalCase instead (e.g., “SmallMolecule”) to avoid issues with handling spaces. The transformation is done by
BioCypher internally. BioCypher does not strictly enforce the entities allowed in this class definition; in fact, we provide several methods of extending the existing ontological backbone ad hoc by providing custom inheritance or hybridising ontologies. However, every entity should at some point be connected to the underlying ontology, otherwise the multiple hierarchical labels will not be populated. Following this first line
are three indented values of the protein class.
The second line (represented_as
) tells BioCypher in which way each entity should be represented in the graph; the only options are node
and edge
. Representation as an edge is only possible when source and target IDs are provided in the input data stream. Conversely, relationships can be represented as both node
or edge
, depending on the desired output. When a relationship should be represented as a node, i.e., “reified”, BioCypher takes care to create a set of two edges and a
node in place of the relationship. This is useful when we want to connect the relationship to other entities in the graph, for example literature references.
The third line (preferred_id
) informs the uniqueness of represented entities by selecting an ontological namespace around which the definition of uniqueness should revolve. In our example, if a protein has its own uniprot ID, it is understood to be a unique entity. When there are multiple protein isoforms carrying the same uniprot ID, they are understood to be aggregated to result in only one unique entity in the graph. Decisions around uniqueness of graph constituents sometimes require some
consideration in task-specific applications. Selection of a namespace also has effects in identifier mapping; in our case, for protein nodes that do not carry a uniprot ID, identifier mapping will attempt to find a uniprot ID given the other identifiers of that node. To account for the broadest possible range of identifier systems while also dealing with parsing of namespace prefixes and validation, we refer to the Bioregistry project namespaces, which should be
preferred values for this field.
Finally, the fourth line (input_label
) connects the input data stream to the configuration; here we indicate which label to expect in the input tuple for each class in the graph. In our case, we expect “uniprot_protein” as the label for each protein in the input data stream; all other input entities that do not carry this label are ignored as long as they are not in the schema configuration.
Creating the graph (using the BioCypher interface)
All that remains to be done now is to instantiate the BioCypher interface (as the main means of communicating with BioCypher) and call the function to create the graph.
[ ]:
from biocypher import BioCypher
bc = BioCypher(
biocypher_config_path='01_biocypher_config.yaml',
schema_config_path='01_schema_config.yaml',
)
# Add the entities that we generated above to the graph
bc.add(entities)
INFO -- Loading ontologies...
INFO -- Instantiating OntologyAdapter class for https://github.com/biolink/biolink-model/raw/v3.2.1/biolink-model.owl.ttl.
[ ]:
# Print the graph as a dictionary of pandas DataFrame(s) per node label
bc.to_df()["protein"]
{'protein': protein sequence \
0 F7V4U2 RMFDDRFPVELRICTGSLVIINLGEFAEQHDKQDGSKPSHQPMFAT...
1 K2Y8U3 HWPPSGVSCGVFPECWYRWRDEQWACFGPHIKYNKDNTWSWAQWMH...
2 L1V6V9 QAEPKYKLAQENCRVQIKLPKIVGTCRPHWMTKTYHVLHTCVLWKS...
description taxon id preferred_id
0 i f c m m q e o o s 9606 F7V4U2 uniprot
1 e y p g j t j y r x 9606 K2Y8U3 uniprot
2 a i b t l j e g n j 9606 L1V6V9 uniprot }
Section 2: Merging data
Plain merge
Using the workflow described above with minor changes, we can merge data from different input streams. If we do not want to introduce additional ontological subcategories, we can simply add the new input stream to the existing one and add the new label to the schema configuration (the new label being entrez_protein
). In this case, we would add the following to the schema configuration:
[ ]:
from data_generator import Protein, EntrezProtein
[ ]:
print_yaml('02_schema_config.yaml')
--------------
protein:
represented_as: node
preferred_id: uniprot
input_label:
- uniprot_protein
- entrez_protein
--------------
[ ]:
# Create a list of proteins to be imported
proteins = [
p for sublist in zip(
[Protein() for _ in range(n_proteins)],
[EntrezProtein() for _ in range(n_proteins)],
) for p in sublist
]
# Create a new BioCypher instance
bc = BioCypher(
biocypher_config_path='02_biocypher_config.yaml',
schema_config_path='02_schema_config.yaml',
)
# Run the import
bc.add(node_generator(proteins))
INFO -- Loading ontologies...
INFO -- Instantiating OntologyAdapter class for https://github.com/biolink/biolink-model/raw/v3.2.1/biolink-model.owl.ttl.
[14]:
bc.to_df()["protein"]
[14]:
{'protein': protein sequence \
0 K2W3K5 TVKISILFNPLPNQDMNTTTCQAESNYKAIYLYPWCSMDDVWNVEA...
1 186009 FHYHGGMGPFMTYQNFLHWEQMQPMKLFNEPMQFHDWYGTHVNWPG...
2 S6E6D1 CSVQIQIGMSQDSPDSSEGNMDCPPRNIGGYEIVCNVQGKRCYSTD...
3 926766 HKEAELLVKGQIQTPKCLRHNHFYAKLTIVIELNYMVDRYGKDMAR...
4 Z1F6R2 FMVWKDCLCIRMRHMAVPVPQYHCEYFEVILERWEVPCFSVLNRCK...
5 362641 PISDEQEMGSEFCGHCNTGVYQVEMHFFECEDLNPKVQPKWIFTVT...
description taxon id preferred_id
0 e e v h x f t f j l 9606 K2W3K5 uniprot
1 b c q m l d a u u g 9606 186009 uniprot
2 i z t s l x v g j l 9606 S6E6D1 uniprot
3 t n a j d l j a t a 9606 926766 uniprot
4 h d m k q n r e h r 9606 Z1F6R2 uniprot
5 l m x k h m v g p y 9606 362641 uniprot }
This again creates a single DataFrame, now for both protein types, but now including both input streams (you should note both uniprot & entrez style IDs in the id column). However, we are generating our entrez
proteins as having entrez IDs, which could result in problems in querying. Additionally, a strict import mode including regex pattern matching of identifiers will fail at this point due to the difference in pattern of UniProt vs. Entrez IDs. This issue could be resolved by mapping the
Entrez IDs to UniProt IDs, but we will instead use the opportunity to demonstrate how to merge data from different sources into the same ontological class using ad hoc subclasses.
Ad hoc subclassing
In the previous section, we saw how to merge data from different sources into the same ontological class. However, we did not resolve the issue of the entrez
proteins living in a different namespace than the uniprot
proteins, which could result in problems in querying. In proteins, it would probably be more appropriate to solve this problem using identifier mapping, but in other categories, e.g., pathways, this may not be possible because of a lack of one-to-one mapping between different
data sources. Thus, if we so desire, we can merge datasets into the same ontological class by creating ad hoc subclasses implicitly through BioCypher, by providing multiple preferred identifiers. In our case, we update our schema configuration as follows:
[15]:
print_yaml('03_schema_config.yaml')
--------------
protein:
represented_as: node
preferred_id:
- uniprot
- entrez
input_label:
- uniprot_protein
- entrez_protein
--------------
This will “implicitly” create two subclasses of the protein
class, which will inherit the entire hierarchy of the protein
class. The two subclasses will be named using a combination of their preferred namespace and the name of the parent class, separated by a dot, i.e., uniprot.protein
and entrez.protein
. In this manner, they can be identified as proteins regardless of their sources by any queries for the generic protein
class, while still carrying information about their
namespace and avoiding identifier conflicts.
The only change affected upon the code from the previous section is the referral to the updated schema configuration file.
In the output, we now generate two separate files for the protein
class, one for each subclass (with names in PascalCase).
Let’s create a DataFrame with the same nodes as above, but with a different schema configuration:
[16]:
bc = BioCypher(
biocypher_config_path='03_biocypher_config.yaml',
schema_config_path='03_schema_config.yaml',
)
bc.add(node_generator(proteins))
for name, df in bc.to_df().items():
print(name)
display(df)
INFO -- Loading ontologies...
INFO -- Instantiating OntologyAdapter class for https://github.com/biolink/biolink-model/raw/v3.2.1/biolink-model.owl.ttl.
[16]:
{'uniprot.protein': uniprot.protein sequence \
0 K2W3K5 TVKISILFNPLPNQDMNTTTCQAESNYKAIYLYPWCSMDDVWNVEA...
1 S6E6D1 CSVQIQIGMSQDSPDSSEGNMDCPPRNIGGYEIVCNVQGKRCYSTD...
2 Z1F6R2 FMVWKDCLCIRMRHMAVPVPQYHCEYFEVILERWEVPCFSVLNRCK...
description taxon id preferred_id
0 e e v h x f t f j l 9606 K2W3K5 uniprot
1 i z t s l x v g j l 9606 S6E6D1 uniprot
2 h d m k q n r e h r 9606 Z1F6R2 uniprot ,
'entrez.protein': entrez.protein sequence \
0 186009 FHYHGGMGPFMTYQNFLHWEQMQPMKLFNEPMQFHDWYGTHVNWPG...
1 926766 HKEAELLVKGQIQTPKCLRHNHFYAKLTIVIELNYMVDRYGKDMAR...
2 362641 PISDEQEMGSEFCGHCNTGVYQVEMHFFECEDLNPKVQPKWIFTVT...
description taxon id preferred_id
0 b c q m l d a u u g 9606 186009 entrez
1 t n a j d l j a t a 9606 926766 entrez
2 l m x k h m v g p y 9606 362641 entrez }
Now we see two separate DataFrames, one for each subclass of the protein
class.
Section 3: Handling properties
While ID and label are mandatory components of our knowledge graph, properties are optional and can include different types of information on the entities. In source data, properties are represented in arbitrary ways, and designations rarely overlap even for the most trivial of cases (spelling differences, formatting, etc). Additionally, some data sources contain a large wealth of information about entities, most of which may not be needed for the given task. Thus, it is often desirable to filter out properties that are not needed to save time, disk space, and memory.
Maintaining consistent properties per entity type is particularly important when using the admin import feature of Neo4j, which requires consistency between the header and data files. Properties that are introduced into only some of the rows will lead to column misalignment and import failure. In “online mode”, this is not an issue.
We will take a look at how to handle property selection in BioCypher in a way that is flexible and easy to maintain.
Designated properties
The simplest and most straightforward way to ensure that properties are consistent for each entity type is to designate them explicitly in the schema configuration. This is done by adding a properties
key to the entity type configuration. The value of this key is another dictionary, where in the standard case the keys are the names of the properties that the entity type should possess, and the values give the type of the property. Possible values are:
str
(orstring
),int
(orinteger
,long
),float
(ordouble
,dbl
),bool
(orboolean
),arrays of any of these types (indicated by square brackets, e.g.
string[]
).
In the case of properties that are not present in (some of) the source data, BioCypher will add them to the output with a default value of None
. Additional properties in the input that are not represented in these designated property names will be ignored. Let’s imagine that some, but not all, of our protein nodes have a mass
value. If we want to include the mass value on all proteins, we can add the following to our schema configuration:
[18]:
print_yaml('04_schema_config.yaml')
--------------
protein:
represented_as: node
preferred_id:
- uniprot
- entrez
input_label:
- uniprot_protein
- entrez_protein
properties:
sequence: str
description: str
taxon: str
mass: int
--------------
This will add the mass
property to all proteins (in addition to the three we had before); if not encountered, the column will be empty. Implicit subclasses will automatically inherit the property configuration; in this case, both uniprot.protein
and entrez.protein
will have the mass
property, even though the entrez
proteins do not have a mass
value in the input data.
If we wanted to ignore the mass value for all properties, we could simply remove the mass
key from the properties
dictionary.
[19]:
from data_generator import EntrezProtein, RandomPropertyProtein
[20]:
# Create a list of proteins to be imported (now with properties)
proteins = [
p for sublist in zip(
[RandomPropertyProtein() for _ in range(n_proteins)],
[EntrezProtein() for _ in range(n_proteins)],
) for p in sublist
]
# New instance, populated, and to DataFrame
bc = BioCypher(
biocypher_config_path='04_biocypher_config.yaml',
schema_config_path='04_schema_config.yaml',
)
bc.add(node_generator(proteins))
for name, df in bc.to_df().items():
print(name)
display(df)
INFO -- Loading ontologies...
INFO -- Instantiating OntologyAdapter class for https://github.com/biolink/biolink-model/raw/v3.2.1/biolink-model.owl.ttl.
[20]:
{'uniprot.protein': uniprot.protein sequence \
0 S1Z9L5 RHLRGDVMQEDHHTSSERMVYNVLPQDYKVVSCEYWNTQVTALWVI...
1 W9J5F1 IPFSQSAWAQQRIGPKGTKAHGVTQPAPMDIKNLCNLTDLTLILDF...
2 T1J3U0 WFGCCHKQYVSHVIDRQDPQSPSDNPSLVSQLQFFMWGIQIQNGEI...
description taxon mass id preferred_id
0 u x e o k m a i o s 3899 None S1Z9L5 uniprot
1 i x k c r b p d d p 8873 None W9J5F1 uniprot
2 m a w r r u x c w o 1966 9364 T1J3U0 uniprot ,
'entrez.protein': entrez.protein sequence \
0 405878 RMTDGFEWQLDFHAFIWCNQAAWQLPLEVHISQGNGGWRMGLYGNM...
1 154167 CGMNYDNGYFSVAYQSYDLWYHQQLKTRGVKPAEKDSDKDLGIDVI...
2 234189 GQWQECIQGFTPQQMCVDCCAETKLANKSYYHSWMTWRLSGLCFNM...
description taxon mass id preferred_id
0 y c s v s n e c h o 9606 None 405878 entrez
1 i k n c e n r n c d 9606 None 154167 entrez
2 o v w y g h y e v y 9606 None 234189 entrez }
Inheriting properties
Sometimes, explicit designation of properties requires a lot of maintenance work, particularly for classes with many properties. In these cases, it may be more convenient to inherit properties from a parent class. This is done by adding a properties
key to a suitable parent class configuration, and then defining inheritance via the is_a
key in the child class configuration and setting the inherit_properties
key to true
.
Let’s say we have an additional protein isoform
class, which can reasonably inherit from protein
and should carry the same properties as the parent. We can add the following to our schema configuration:
[21]:
from data_generator import RandomPropertyProteinIsoform
[22]:
print_yaml('05_schema_config.yaml')
--------------
protein:
represented_as: node
preferred_id:
- uniprot
- entrez
input_label:
- uniprot_protein
- entrez_protein
properties:
sequence: str
description: str
taxon: str
mass: int
protein isoform:
is_a: protein
inherit_properties: true
represented_as: node
preferred_id: uniprot
input_label: uniprot_isoform
--------------
This allows maintenance of property lists for many classes at once. If the child class has properties already, they will be kept (if they are not present in the parent class) or replaced by the parent class properties (if they are present).
Again, apart from adding the protein isoforms to the input stream, the code for this example is identical to the previous one except for the reference to the updated schema configuration.
We now create three separate DataFrames, all of which are children of the protein
class; two implicit children (uniprot.protein
and entrez.protein
) and one explicit child (protein isoform
).
[23]:
# create a list of proteins to be imported
proteins = [
p for sublist in zip(
[RandomPropertyProtein() for _ in range(n_proteins)],
[RandomPropertyProteinIsoform() for _ in range(n_proteins)],
[EntrezProtein() for _ in range(n_proteins)],
) for p in sublist
]
# Create BioCypher driver
bc = BioCypher(
biocypher_config_path='05_biocypher_config.yaml',
schema_config_path='05_schema_config.yaml',
)
# Run the import
bc.add(node_generator(proteins))
for name, df in bc.to_df().items():
print(name)
display(df)
INFO -- Loading ontologies...
INFO -- Instantiating OntologyAdapter class for https://github.com/biolink/biolink-model/raw/v3.2.1/biolink-model.owl.ttl.
uniprot.protein
uniprot.protein sequence \
0 A9L6G4 SWIVVGQPDSHNKRLVNYHWMRCEHPLRCWRPIYVVRVSFQSQCEQ...
1 E4N2H2 PGVMILDNMQHKCSKELSTRQIITNHWICNSAPISWSSGMDRSCLD...
2 V4F1T1 DQCHNLCPGSSFQCPENAFGNDWIDHMPQETGLMQYDDPQSGMWFT...
description taxon mass id preferred_id
0 m o k j a f w v w r 4220 None A9L6G4 uniprot
1 n v i r s f m f d w 6339 6481 E4N2H2 uniprot
2 w e v v a b o b b u 9176 6510 V4F1T1 uniprot
protein isoform
protein isoform sequence \
0 F0N9A4 QDVVLVEGCGDEGWIHMPEKRPGQAYKWCERFRPIPDFTNSIKIAY...
1 B1W6O2 SQKHFRRWWTNDCFGQELMSIYYNVKFWDNLIEMTGGPASRVCLGQ...
2 G6V5R9 ASAITPFSYEKPHTVTLDATEVFPKMQDAQAIEREIHFSKSTLVYG...
description taxon mass id preferred_id
0 r f e a v a a g w r 8061 None F0N9A4 uniprot
1 a c a v v k v k c w 6786 None B1W6O2 uniprot
2 c k g d a l f r t v 6868 1323 G6V5R9 uniprot
entrez.protein
entrez.protein sequence \
0 52329 DYRSMAPTFILMKIYPACDAITKRRWSVATVKDGEFIWWSAVKIFP...
1 581107 LLVFNMGQLAVAGYGNTMVSAMMCFCCDVKARMGMSWLPKITTMQW...
2 270569 MVCSHHELAVAFQTMCPIQGDAATAKANAHRTTDKQNWMVVKWFRT...
description taxon mass id preferred_id
0 q k r b h g t q x x 9606 None 52329 entrez
1 h f g z j r b g m w 9606 None 581107 entrez
2 s b p v f u t y g v 9606 None 270569 entrez
Section 4: Handling relationships
Naturally, we do not only want nodes in our knowledge graph, but also edges. In BioCypher, the configuration of relationships is very similar to that of nodes, with some key differences. First the similarities: the top-level class configuration of edges is the same; class names refer to ontological classes or are an extension thereof. Similarly, the is_a
key is used to define inheritance, and the inherit_properties
key is used to inherit properties from a parent class. Relationships also
possess a preferred_id
key, an input_label
key, and a properties
key, which work in the same way as for nodes.
Relationships also have a represented_as
key, which in this case can be either node
or edge
. The node
option is used to “reify” the relationship in order to be able to connect it to other nodes in the graph. In addition to the configuration of nodes, relationships also have fields for the source
and target
node types, which refer to the ontological classes of the respective nodes, and are currently optional.
To add protein-protein interactions to our graph, we can modify the schema configuration above to the following:
[24]:
print_yaml('06_schema_config_pandas.yaml')
--------------
protein:
represented_as: node
preferred_id:
- uniprot
- entrez
input_label:
- uniprot_protein
- entrez_protein
properties:
sequence: str
description: str
taxon: str
mass: int
protein isoform:
is_a: protein
inherit_properties: true
represented_as: node
preferred_id: uniprot
input_label: uniprot_isoform
protein protein interaction:
is_a: pairwise molecular interaction
represented_as: edge
preferred_id: intact
input_label: interacts_with
properties:
method: str
source: str
--------------
Now that we have added protein protein interaction
as an edge, we have to simulate some interactions:
[25]:
from data_generator import InteractionGenerator
# Simulate edges for proteins we defined above
ppi = InteractionGenerator(
interactors=[p.get_id() for p in proteins],
interaction_probability=0.05,
).generate_interactions()
[26]:
# naturally interactions/edges contain information about the interacting source and target nodes
# let's look at the first one in the list
interaction = ppi[0]
f"{interaction.get_source_id()} {interaction.label} {interaction.get_target_id()}"
[26]:
'A9L6G4 interacts_with V4F1T1'
[27]:
# similarly to nodes, it also has a dictionary of properties
interaction.get_properties()
[27]:
{'source': 'signor', 'method': 'u z c x m d c u g s'}
As with nodes, we add first createa a new BioCypher instance, and then populate it with nodes as well as edges:
[28]:
bc = BioCypher(
biocypher_config_path='06_biocypher_config.yaml',
schema_config_path='06_schema_config_pandas.yaml',
)
[29]:
# Extract id, source, target, label, and property dictionary
def edge_generator(ppi):
for interaction in ppi:
yield (
interaction.get_id(),
interaction.get_source_id(),
interaction.get_target_id(),
interaction.get_label(),
interaction.get_properties(),
)
bc.add(node_generator(proteins))
bc.add(edge_generator(ppi))
INFO -- Loading ontologies...
INFO -- Instantiating OntologyAdapter class for https://github.com/biolink/biolink-model/raw/v3.2.1/biolink-model.owl.ttl.
Let’s look at the interaction DataFrame:
[30]:
bc.to_df()["protein protein interaction"]
[30]:
protein protein interaction | _from | _to | source | method | |
---|---|---|---|---|---|
0 | intact703256 | A9L6G4 | V4F1T1 | signor | u z c x m d c u g s |
1 | None | E4N2H2 | F0N9A4 | intact | None |
Finally, it is worth noting that BioCypher relies on ontologies, which are machine readable representations of domains of knowledge that we use to ground the contents of our knowledge graphs. While details about ontologies are out of scope for this tutorial, and are described in detail in the BioCypher documentation, we can still have a glimpse at the ontology that we used implicitly in this tutorial:
[31]:
bc.show_ontology_structure()
Showing ontology structure based on https://github.com/biolink/biolink-model/raw/v3.2.1/biolink-model.owl.ttl
entity
├── association
│ └── gene to gene association
│ └── pairwise gene to gene interaction
│ └── pairwise molecular interaction
│ └── protein protein interaction
└── named thing
└── biological entity
└── polypeptide
└── protein
├── entrez.protein
├── protein isoform
└── uniprot.protein
[31]:
<treelib.tree.Tree at 0x7f7327b3a880>