Subgraph filtering in Neo4j Graph Data Science library | by Tomaz Bratanic | Towards Data Science

It has been a while since I wrote a post about new features in the Neo4j Graph Data Science library (GDS). For those of you that never heard of the GDS library, it features more than 50 graph algorithms ranging from community detection to node embedding algorithms and more. In this blog post, I will present Subgraph filtering, one of the library’s newer features. To quickly recap how the GDS library works, let’s look at the following image:
Neo4j Graph Data Science library uses the Graph Loader component to project an in-memory graph. The in-memory project graph is separate from the stored graph in the Neo4j database. The GDS library then uses the in-memory graph projection, optimized for topology and property lookup operations, to execute graph algorithms. You can use either Native or Cypher projections to project an in-memory graph. In addition, subgraph filtering allows you to create a new projected in-memory graph based on an existing projected graph. For example, you could project a graph, identify the weakly connected components within that network, and then use subgraph filtering to create a new projected graph that consists only of the largest component in the network. This allows you a smoother graph data science workflow, where you don’t have to store intermediate results back to the database and then use Graph Loader to project a new in-memory graph.
In this blog post, we will be using the Harry Potter network dataset I have created in one of my previous blog posts. It consists of interactions between characters in the Harry Potter and the Philosopher’s Stone book.
The graph schema is relatively simple. It consists of characters and their interactions. We know the name of the characters and when they first appeared in the book (firstSeen). The INTERACTS relationship holds the information about how many times two characters have interacted (weight) and when they first interacted (firstSeen). If you want to follow along with exercises in this post, I would suggest you use Neo4j Sandbox and select a blank project.
Neo4j Sandbox provides a free cloud-instance of Neo4j database and comes with both APOC and GDS libraries pre-installed. However, if you want a local setup of Neo4j database, I have another blog post where I described how to get started with Neo4j Desktop.
To import the HP interaction network, you need to execute the following two Cypher queries.
Import characters:
LOAD CSV WITH HEADERS FROM "https://raw.githubusercontent.com/tomasonjo/blog-datasets/main/HP/character_first_seen.csv" as rowMERGE (c:Character{name:row.name})SET c.firstSeen = toInteger(row.value)
Import interactions:
LOAD CSV WITH HEADERS FROM "https://raw.githubusercontent.com/tomasonjo/blog-datasets/main/HP/HP_rels.csv" as rowMATCH (s:Character{name:row.source})MATCH (t:Character{name:row.target})MERGE (s)-[i:INTERACTS]->(t)SET i.weight = toInteger(row.weight),    i.firstSeen = toInteger(row.first_seen)
Once you have imported the dataset, you can inspect a sample subgraph with the following query:
MATCH p=(:Character)-[:INTERACTS]->(:Character)RETURN p LIMIT 25
You should see a similar visualization in Neo4j Browser:
As mention, the goal of this blog post is to demonstrate the power of subgraph filtering. We will not delve into specific algorithms and how they work. We will begin by projecting an in-memory graph with Native projections.
CALL gds.graph.create('interactions',  'Character',  {INTERACTS : {orientation:'UNDIRECTED'}},  {nodeProperties:['firstSeen'],    relationshipProperties: ['firstSeen', 'weight']})
We have projected an in-memory graph under the “interactions” name. The projected graph includes all Character nodes and their firstSeen properties. We have also defined that we want to project the INTERACTS relationships as undirected and include both firstSeen and weight properties.
Now that we have the projected named graph, we can go ahead and execute any of the graph algorithms on it. Here, I have chosen to run the Weakly Connected Components algorithm (WCC). The WCC algorithm is used to identify disconnected parts of your network that are also known as components.
CALL gds.wcc.stats('interactions')YIELD componentCount, componentDistribution
Result
You can use the stats mode of the algorithm when you are only interested in the high-level overview of the results and have no wish to store the results back to Neo4j or to the projected graph. We can observe that there are four components in our network, the largest having 110 members. Now, we will begin with subgraph filtering. The syntax for the subgraph filtering procedure is as follows:
CALL gds.beta.graph.subgraph(  graphName: String, -> name of the new projected graph  fromGraphName: String, -> name of the existing projected graph  nodeFilter: String, -> predicate used to filter nodes  relationshipFilter: String -> predicate used to filter relationships)
You can use the nodeFilter parameter to filter nodes based on node properties or labels. Similarly, you can use relationshipFilter parameter to filter relationships based on their properties and types. There is only a single node label and relationships type in our HP network, so we will only focus on filtering by properties.
We will begin by using the subgraph filtering to create a new projected in-memory graph that holds only relationships that have the weight property greater than 1.
CALL gds.beta.graph.create.subgraph(  'wgt1', // name of the new projected graph  'interactions', // name of the existing projected graph  '*', // node predicate filter  'r.weight > 1' // relationship predicate filter)
The wildcard operator * is used to define that we don’t want to apply any filtering. In this case, we have only filtered relationships, but kept all the nodes. The predicate syntax is similar to Cypher query. The relationship entity is always identified by r and the node entity is identified with variable n .
We can go ahead and run the WCC algorithm on the new in-memory graph that we created with the subgraph filtering. It is available under the wgt1name, for a lack of better name nomenclature.
CALL gds.wcc.mutate('wgt1', {mutateProperty:'wcc'})YIELD componentCount, componentDistribution
Result
The filtered projected graph has 43 components. This is reasonable as we ignored all the relationships that have the weight property equal or smaller to one, but left all the nodes. We have used the mutate mode of the algorithm to write the results back to the projected in-memory graph.
Let’s say we now want to run Eigenvector centrality only on the largest component of the graph. First, we need to identify the ID of the largest component. The results of the WCC algorithm are stored in the projected in-memory graph, so we need to use the gds.graph.streamNodeProperties procedure to access the WCC results and identify the largest component.
CALL gds.graph.streamNodeProperties('wgt1', ['wcc']) YIELD propertyValueRETURN propertyValue as component, count(*) as componentSizeORDER BY componentSize DESC LIMIT 5
Results
As we saw before, the largest component has 78 members and its id is 0. We can use the subgraph filtering feature to create a new projected in-memory graph that contains only the nodes in the largest component.
CALL gds.beta.graph.create.subgraph(  'largest_community',   'wgt1',  'n.wcc = 0',   '*')
Now, we can go ahead and accomplish our task by running the Eigenvector centrality on the largest component only.
CALL gds.eigenvector.stream('largest_community')YIELD nodeId, scoreRETURN gds.util.asNode(nodeId).name as character, scoreORDER BY score DESCLIMIT 5
Result
In the Harry Potter universe, there is a good chance that Harry Potter will always come out on top. We have already done two subgraph filtering steps, so you might get confused about what version of network is projected now. The GDS library offers a gds.graph.exportfeature that allows you to export a projected graph as a separate database instance within the same Neo4j server.
CALL gds.graph.export('largest_community', { dbName: 'lc1' });
Before you can access the new database instance in Neo4j Browser, you need to run the following command:
CREATE DATABASE lc1;
Now you can select the lc1database in the database dropdown menu and inspect its content.
I don’t know why, but the Sandbox instance doesn’t work well with the graph export, while the local Desktop version works just fine. At the moment, you can’t export string properties to a separate database. However, I’ve heard through the grapevine that string properties in a projected in-memory graph are to be supported soon.
In the last example, I will show how you can combine multiple node and relationship predicates using the AND or OR logical operators. First we will run the degree centrality on the interactions network and store the results to the projected graph using the mutate mode.
CALL gds.degree.mutate('interactions',  {mutateProperty:'degree'})
Now we can go ahead and filter the subgraph by using multiple node and relationship predicates:
CALL gds.beta.graph.create.subgraph(  'first_half', // new projected graph  'interactions', // existing projected graph  'n.firstSeen < 35583 AND n.degree > 2', // node predicates  'r.weight > 5' // relationship predicates)
In the node predicate, we have selected nodes with a degree value greater than two and the first seen property smaller than 35583. For the relationship predicate, I have chosen only relationships that have a weight greater than five. We can run any of the graph algorithms on the newly filtered in-memory subgraph:
CALL gds.eigenvector.stream('first_half')YIELD nodeId, scoreRETURN gds.util.asNode(nodeId).name as character, scoreORDER BY scoreDESC LIMIT 5
Results
Lastly, when you are done with your graph analysis, you can use the following Cypher query to drop all the projected in-memory graphs:
CALL gds.graph.list() YIELD graphNameCALL gds.graph.drop(graphName)YIELD databaseRETURN 'dropped ' + graphName
Subgraph filtering is a nice addition to the GDS library that allows smoother workflows. Instead of having to store the algoritm results back to Neo4j and use Native or Cypher projections to create a new in-memory graph, you can use subgraph filtering to filter an existing in-memory graph. Try it out on Neo4j Sandbox and let me know how it goes!
As always, the code is available on GitHub.