Introduction

I have been working with graphs both in Python and in Julia quite a lot recently. While I enjoy using LightGraphs.jl as it has a very lighweight and clean API its limitation is that it is missing some functionalities that are available in other packages like igraph.

In this post I want to comment on the basic steps for integrating both packages.

The examples are run under Linux, Julia 1.6, Cairo v1.0.5, Compose v0.9.2, GraphPlot v0.4.4, LightGraphs v1.3.5, and PyCall v1.92.2.

Before you start

You need to add igraph to your Python installation that is used by Julia using the following command:

using PyCall
run(`$(PyCall.python) -m pip install python-igraph`)

We will also use the partition_igraph package for community detection. It is installed using:

run(`$(PyCall.python) -m pip install partition_igraph`)

This package provides the ECG (ensemble clustering for graphs) algorithm that is a very nice approach to community detection. If you do not know it you might want to check out this and this papers.

The point of this post is that ECG is available only as a package for Python, so if you wanted to use it from Julia you need to use a bridge to igraph.

Getting the graph

We will want to use the standard Zachary’s karate club graph as an example. Let us start with loading the graph both in LightGraphs.jl and in igraph. First we load the required packages:

julia> using Cairo

julia> using Compose

julia> using GraphPlot

julia> using LightGraphs

julia> using PyCall

julia> using Random

julia> ig = pyimport("igraph");

julia> pyimport("partition_igraph");

Now load the graph both in LightGraphs.jl and in igraph:

julia> z_ig = ig.Graph.Famous("Zachary")
PyObject <igraph.Graph object at 0x7f5a18d49050>

julia> z_lg = smallgraph(:karate)
{34, 78} undirected simple Int64 graph

Let us check what is the mapping between these two graphs. The numbering of vertices in LightGraphs.jl is 1-based, whilie in igraph is 0-based. Let us check if in this case the mapping from z_lg to z_ig is just x -> x-1.

julia> nv(z_lg) == z_ig.vcount()
true

julia> ne(z_lg) == z_ig.ecount()
true

julia> z_lg_es = sort!(Tuple.(edges(z_lg)));

julia> z_ig_es = sort!([(e.source + 1, e.target + 1) for e in z_ig.es()]);

julia> z_lg_es == z_ig_es
true

Indeed in this case we see that the graphs are identical, as they have the same number of vertices, and the same edges. Note that I needed to sort! the edges collected to tuples as the iterators that produce these edges had a different ordering in both packages.

Note that it is also easy to write graph converters between LightGraphs.jl and igraph. Here is an example:

julia> function ig2lg(ig_g)
           lg_g = SimpleGraph(ig_g.vcount())
           for e in ig_g.es()
               add_edge!(lg_g, e.source + 1, e.target + 1)
           end
           return lg_g
       end
ig2lg (generic function with 1 method)

julia> ig2lg(z_ig)
{34, 78} undirected simple Int64 graph

julia> ig2lg(z_ig) == z_lg
true

The crucial thing to remember here is that LightGraphs.jl supports only simple graphs, so this property of the graph would have to be checked in igraph before doing the conversion. (similar operations can also be performed for directed simple graphs.)

Doing community detection

Now we move to the main point of our task. We want to run ECG algorithm on z_ig and then plot the z_lg graph using gplot function coloring its vertices using the communities found using ECG.

As ECG is randomized we run it 1,000 times and pick the split that maximizes modularity:

julia> ps = [z_ig.community_ecg().membership for i in 1:1000]; # generate partitions

julia> unique!(ps); # reduce as we have many duplicates

julia> mps = z_ig.modularity.(ps); # calculate modularity for each partition

julia> bestp = ps[argmax(mps)] .+ 1; # get the best partition

Note how easy it is to mix Python functionality with Julia in the above code. In the last line I add 1 to the resulting vector to make in 1-based.

Finally we save the plot of the graph to a PNG file:

julia> cls = ["red","green","blue", "pink", "black"]; # we allow up to 5 communities

julia> Random.seed!(12);

julia> z_plot = gplot(z_lg,
                      NODESIZE=0.03, nodefillc=cls[bestp],
                      EDGELINEWIDTH=0.2, edgestrokec="gray");

julia> draw(PNG("z_plot.png"), z_plot);

Note that the mapping between z_ig and z_lg is just x -> x + 1 we do not have to reorder the bestp vector.

This is the plot you should get:

Karate graph

As you can see the ECG algorithm seems to have identified the communities in the graph reasonably well.

Conclusions

As you can see integration between LightGraphs.jl and igraph is very easy. The major things you have to keep in mind when using it are:

  • igraph uses 0-based intexing, while LightGraphs.jl is 1-based;
  • always make sure what transformation of vertex indices you should use to map vertex numbers between igraph and LightGraphs.jl (a natural one is just x -> x + 1 if they are stored in the same order);
  • edges might be stored in both packages in a different order;
  • make sure to check if you are working with a graph or a digraph and use a proper graph type in LightGraphs.jl;
  • remember that LightGraphs.jl only supports simple graphs.

I hope this post will help you to get started with igraph integration if you might need to use it!