Skip to content

Instantly share code, notes, and snippets.

@ConorAspell
Last active December 30, 2017 20:31
Show Gist options
  • Save ConorAspell/ac2f6cda7bae02fd7a7fd271476f867b to your computer and use it in GitHub Desktop.
Save ConorAspell/ac2f6cda7bae02fd7a7fd271476f867b to your computer and use it in GitHub Desktop.
Directed Network of Rugby Results
license: mit

A force directed graph uses a physics simulation to lay out a network. It has a huge amount of uses in many different fields, with this Block I decided to explore it's potential in analysing a sporting teams season.

I selected Rugby Union as the sport to analyse as it has an assymmetric season, i.e. everyone will not play everyone.

I created a CSV of all the results of the 2017 international rugby season between tier 1 nations and created a directed graph using NetworkX for Python from the data and produced the JSON for this visualisation. Using NetworkX I also calculated each node's centrality which affects the node's size in the above visualisation

The centrality measure was in-degree centrality, this measures how well connected the incoming edges are connected to the rest of the network. A high value would indicate that the teams who beat you, were beaten by a lot of others, as well as that you were beaten by a lot. England and New Zealand both had values of 0.11 recurring while Ireland had 0.22 recurring and Scotland had 0.33, indicating that these teams had good seasons. Note, in the visualisation the values are reversed as it is more intuitive that larger nodes had better seasons. The degree centrality does not account for weight either, meaning the margin of victory does not affect the centrality or node size.

One of the more interesting countries to observe in this visualisation is South Africa. It is almost universally acknowledged that they had a terrible season in 2017, however, the in-degree centrality measure has them performing at the same level as Australia and France and better than Wales, Argentina and Italy. This is due to their big victories against France, Argentina and Italy, as well as a victory against Wales and 2 draws against Australia, offsetting losses to Ireland, England and New Zealand.

Wales is also interesting to observe as they had a very mixed season while playing every team except Argentina. They had 3 wins to 5 losses putting them below the top teams but all the games were close.

{
"nodes":[
{
"name": "Australia",
"group":0,
"colour":9,
"centrality":0.5555555555555556
},{
"name": "South_Africa",
"group":1,
"colour":19,
"centrality":0.5555555555555556
},{
"name": "Wales",
"group":2,
"colour":7,
"centrality":0.4444444444444444
},{
"name": "France",
"group":3,
"colour":9,
"centrality":0.5555555555555556
},{
"name": "New_Zealand",
"group":4,
"colour":14,
"centrality":0.8888888888888888
},{
"name": "England",
"group":5,
"colour":8,
"centrality":0.8888888888888888
},{
"name": "Scotland",
"group":6,
"colour":6,
"centrality":0.6666666666666667
},{
"name": "Ireland",
"group":1,
"colour":6,
"centrality":0.7777777777777778
},{
"name": "Italy",
"group":8,
"colour":9,
"centrality":0.11111111111111116
},{
"name": "Argentina",
"group":9,
"colour":5,
"centrality":0.4444444444444444
}],"links": [
{"source": 0, "target": 1, "value": 0, "type": "arrow"},
{"source": 0, "target": 2, "value": 8, "type": "arrow"},
{"source": 0, "target": 8, "value": 13, "type": "arrow"},
{"source": 0, "target": 4, "value": 5, "type": "arrow"},
{"source": 0, "target": 9, "value": 17, "type": "arrow"},
{"source": 1, "target": 0, "value": 0, "type": "arrow"},
{"source": 1, "target": 8, "value": 29, "type": "arrow"},
{"source": 1, "target": 3, "value": 69, "type": "arrow"},
{"source": 1, "target": 9, "value": 40, "type": "arrow"},
{"source": 2, "target": 1, "value": 2, "type": "arrow"},
{"source": 2, "target": 7, "value": 13, "type": "arrow"},
{"source": 2, "target": 8, "value": 26, "type": "arrow"},
{"source": 3, "target": 8, "value": 22, "type": "arrow"},
{"source": 3, "target": 2, "value": 2, "type": "arrow"},
{"source": 3, "target": 6, "value": 6, "type": "arrow"},
{"source": 4, "target": 0, "value": 26, "type": "arrow"},
{"source": 4, "target": 2, "value": 15, "type": "arrow"},
{"source": 4, "target": 3, "value": 20, "type": "arrow"},
{"source": 4, "target": 1, "value": 1, "type": "arrow"},
{"source": 4, "target": 6, "value": 5, "type": "arrow"},
{"source": 4, "target": 9, "value": 43, "type": "arrow"},
{"source": 5, "target": 0, "value": 24, "type": "arrow"},
{"source": 5, "target": 2, "value": 5, "type": "arrow"},
{"source": 5, "target": 3, "value": 3, "type": "arrow"},
{"source": 5, "target": 6, "value": 40, "type": "arrow"},
{"source": 5, "target": 8, "value": 21, "type": "arrow"},
{"source": 5, "target": 9, "value": 27, "type": "arrow"},
{"source": 6, "target": 0, "value": 34, "type": "arrow"},
{"source": 6, "target": 2, "value": 16, "type": "arrow"},
{"source": 6, "target": 7, "value": 5, "type": "arrow"},
{"source": 6, "target": 8, "value": 50, "type": "arrow"},
{"source": 7, "target": 5, "value": 4, "type": "arrow"},
{"source": 7, "target": 1, "value": 35, "type": "arrow"},
{"source": 7, "target": 8, "value": 53, "type": "arrow"},
{"source": 7, "target": 3, "value": 10, "type": "arrow"},
{"source": 7, "target": 9, "value": 9, "type": "arrow"},
{"source": 9, "target": 8, "value": 16, "type": "arrow"}
]
}
<!DOCTYPE html>
<html>
<head>
<title>D3 Matrix Example</title>
<script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>
</head><style>
path.link {
fill: none;
stroke: #666;
}
path.link.zero{
opacity: 0.1;
stroke-width: 0.5px;
}
path.link.twofive {
opacity: 0.25;
stroke-width: 1.5px;
}
path.link.fivezero {
opacity: 0.50;
stroke-width: 3.0px;
}
path.link.sevenfive {
opacity: 0.75;
stroke-width: 4.5px;
}
path.link.onezerozero {
opacity: 1.0;
stroke-width: 6.0px;
}
circle {
fill: #ccc;
stroke: #fff;
stroke-width: 1.5px;
}
text {
fill: #000;
pointer-events: none;
}
#content {
padding: 7px;
}
</style>
<body>
</body>
<script>
// get the data
var nodecolor = d3.scale.category20();
d3.json("data.json", function(error, graph) {
var nodes = {};
size = 25;
// Compute the distinct nodes from the links.
var links = graph.links;
var width = 900,
height = 600;
var force = d3.layout.force()
.nodes(graph.nodes)
.links(links)
.size([width, height])
.linkDistance(function(d) {
return d.value*10;
})
.charge(-5000)
.on("tick", tick)
.start();
// Set the range
var v = d3.scale.linear().range([0, 100]);
// Scale the range of the data
v.domain([0, d3.max(links, function(d) { return d.value; })]);
// asign a type per value to encode opacity
links.forEach(function(link) {
if (v(link.value) == 0) {
link.type = "zero";
}
else if (v(link.value) <= 25) {
link.type = "twofive";
} else if (v(link.value) <= 50 && v(link.value) > 25) {
link.type = "fivezero";
} else if (v(link.value) <= 75 && v(link.value) > 50) {
link.type = "sevenfive";
} else if (v(link.value) <= 100 && v(link.value) > 75) {
link.type = "onezerozero";
}
});
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
// build the arrow.
svg.append("svg:defs").selectAll("marker")
.data(["end"]) // Different link/path types can be defined here
.enter().append("svg:marker") // This section adds in the arrows
.attr("id", String)
.attr("viewBox", "0 -5 10 10")
.attr("refX", 15)
.attr("refY", -1.5)
.attr("markerWidth", 6)
.attr("markerHeight", 6)
.attr("orient", "auto")
.append("svg:path")
.attr("d", "M0,-5L10,0L0,5");
// add the links and the arrows
var path = svg.append("svg:g").selectAll("path")
.data(force.links())
.enter().append("svg:path")
.attr("class", function(d) { return "link " + d.type; })
.attr("marker-end", "url(#end)");
// define the nodes
var node = svg.selectAll(".node")
.data(force.nodes())
.enter().append("g")
.attr("class", "node")
.call(force.drag);
// add the nodes
node.append("circle")
.attr("r", function (d) {
console.log(d);
return d.centrality * size;
})
.style("fill", function(d) {
return nodecolor(d.group);
});
// add the text
node.append("text")
.attr("x", 12)
.attr("dy", ".35em")
.text(function(d) { return d.name; });
// add the curvy lines
function tick() {
path.attr("d", function(d) {
var dx = d.target.x - d.source.x,
dy = d.target.y - d.source.y,
dr = Math.sqrt(dx * dx + dy * dy);
return "M" +
d.source.x + "," +
d.source.y + "A" +
dr + "," + dr + " 0 0,1 " +
d.target.x + "," +
d.target.y;
});
node
.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")"; });
}
});
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment