Showing posts with label mongodb. Show all posts
Showing posts with label mongodb. Show all posts

Wednesday, 19 April 2017

Half Marathon Comparison Using Azure, Google Maps, Python, MongoDB and Javascript

This time last year I blogged about a half-marathon I had run where I paced it badly and slowed down massively at the end.  I did the same race this year and ran a faster time but more importantly paced it more consistently and so enjoyed the experience more.

The run was over the same course and the weather was similar so this provides a good opportunity to compare and contrast both years.  At a superficial level, as part of the results that are provided you get to see your split after every 5K.  Hence it was possible to compare the splits of last year with this year:

So simply put:
  • After 5K, in 2017 I was 38 seconds behind where I was in 2016.
  • After 10K I was 44 seconds behind and after 15K I was a massive 80 seconds behind.
  • However after 20K in 2017 I had turned this around was 14 seconds ahead of 2016.
  • Then I ran the final 1.1K 27 seconds faster in 2017 than in 2016 to finish 41 seconds up.
Note that none of this was down to significantly better fitness, I just paced the run more sensibly in 2017.  (Put differently I was a lot more stupid in 2016!).

As a Geek I wanted to go further in this analysis so I thought it would be fun to visually compare 2016 versus 2017 on a map.  i.e. See my 2016 self zoom past my 2017 self then see my 2017 self catch up and pass 2016.  Having tinkered with AWS and Bluemix it was time to drive a different cloud computing offering so I decided to take up Microsoft's kind offer of £150 of credit.  

Here's the result.  The "6" marker is 2017, the "7" marker is 2017.



So you can see:
  • Me starting further up the road in 2017.
  • The 2016 me catching up and passing the 2017 me around the University.
  • 2016 me staying ahead for a long period of time.
  • 2017 me catching up and quickly passing 2016 me on the final straight stretch to the finish.

So a fascinating profile!

Here's a diagram of what I put together.  Full description and code then follows.



The above diagram shows the following key steps:
  • Garmin Sports watch syncs with Garmin Connect (standard activity)
  • GPX files downloaded from Garmin Connect and uploaded to Azure Virtual Machine (covered in Step 1 below).
  • Python script to parse GPX files and load them in a MongoDB instance (Step 2)
  • Apache webserver and Python cgi-bin to extract data from the MongoDB instance and offer a simple API (step 3)
  • HTML, CSS and Javascript to access API and present animated map markers using the Google Maps Javascript API (step 4)
Step 1 - Getting a Azure Linux Virtual Machine
Microsoft Azure is very easy and intuitive to use.  I already had a Microsoft account for Outlook.com so just used this to go through the Azure free trial sign up process.  This gave me £150 worth of free credit on the Azure platform.

After quickly reviewing tutorials I requested a Linux Virtual Machine using the steps New - Compute - Ubuntu Server 16.04TS and then providing some basic configuration details.  Within roughly a minute the server had been setup and I could get details as to how to SSH onto the VM using PuTTY.  The size of the platform was Standard DS1 v2 (1 core, 3.5 GB memory) which was suitable for my needs.

A tile on the Azure dashboard gave me access to all manner of information and configuration options for the VM.  Example below:



Take a step back now - for an olde skool Technology guy such as myself I am still super impressed by cloud computing capabilities.  No massive forms to fill out, no tetchy administrators to haggle with, no IP networking to organise - just BOOM! and you've got a machine to play with.

The final part of this step was to use an FTP client (WinSCP) to upload the Garmin GPX files to the VM.

Step 2 - MongoDB, GPX File Parsing and Database Loading
The plan was to use the GPX files recorded by my Garmin sports watch in 2016 and 2017 to allow map markers to be animated.  So what's a GPX file?  Here's a definition:

GPX, or GPS Exchange Format, is an XML schema designed as a common GPS data format for software applications. It can be used to describe waypoints, tracks, and routes. The format is open and can be used without the need to pay license fees.

Here's the top section of one of my half-marathon GPX files:

<?xml version="1.0" encoding="UTF-8"?>
<gpx creator="Garmin Connect" version="1.1"
  xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/11.xsd"
  xmlns="http://www.topografix.com/GPX/1/1"
  xmlns:ns3="http://www.garmin.com/xmlschemas/TrackPointExtension/v1"
  xmlns:ns2="http://www.garmin.com/xmlschemas/GpxExtensions/v3" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <metadata>
    <link href="connect.garmin.com">
      <text>Garmin Connect</text>
    </link>
    <time>2016-04-03T09:19:19.000Z</time>
  </metadata>
  <trk>
    <name>Whitley Ward Running</name>
    <type>running</type>
    <trkseg>
      <trkpt lat="51.42623794265091419219970703125" lon="-0.992680527269840240478515625">
        <ele>45.40000152587890625</ele>
        <time>2016-04-03T09:19:19.000Z</time>
        <extensions>
          <ns3:TrackPointExtension>
            <ns3:hr>82</ns3:hr>
          </ns3:TrackPointExtension>
        </extensions>
      </trkpt>
      <trkpt lat="51.42622503452003002166748046875" lon="-0.9927202574908733367919921875">
        <ele>45.40000152587890625</ele>
        <time>2016-04-03T09:19:20.000Z</time>
        <extensions>
          <ns3:TrackPointExtension>
            <ns3:hr>82</ns3:hr>
          </ns3:TrackPointExtension>
        </extensions>
      </trkpt>

So some metadata and then a <trk> section with a <trkseg> subsection which is a container for a bunch of <trkpt> elements.  Within each of these you can see:

  • The position logged (latitude and longitude)
  • Elevation
  • Date and time
  • Heart rate 

I wanted to store all the data in a database and I chose to use MongoDB on Azure as I enjoyed using it for a Raspberry Pi project last year (and so also had cracked using Python to write to and read from the database).

Getting the database was super easy.  Within the Azure I did New - Databases - "Database as a Service for MongoDB", entered a few details and minute or two later had a MongoDB instance.

Remembering that the structure of a document database is different to a relational database as follows:

Relational Database TermDocument Database Term
DatabaseDatabase
TableCollection
RowDocument

...I set up a database called "geekmongo" and created a collection within it called "Test".  Hence the task at hand was to parse the GPX files, create JSON documents and write them to the Test collection in the geekmongo database.

#Import statements
import xml.etree.ElementTree as ET
from datetime import datetime
from pymongo import MongoClient

#File to process
FileOne = '/home/map/rhm/activity_1109977624_2016.gpx'

#Database related - Got all these from "Connection String" area on Azure for the database instance
#Created the collection test myself manually on Azure
dBAddress = 'Your Connection String'

#Start parsing that XML
tree = ET.parse(FileOne)
root = tree.getroot()

#Set up for the mongo instance, the database then the collection
#Connect to the database
client = MongoClient(dBAddress)
db = client.geekmongo
collection = db.Test

#Get the first timestamp as we'll reference all subsequent ones to this in order to be able to calculate elapsed timestamp
FirstTimeStamp = root[1][2][0][1].text
#Turn it into a time object we can use
TStart = datetime.strptime(FirstTimeStamp[:-5],"%Y-%m-%dT%H:%M:%S")

#Loop through the XML file picking out lat and lng and writing them to a file, (unless they're the last ones on the list)
#Example is {"elapsed": 0, "lat":51.4566827,"lng":-0.9690389},
LoopVar = 0
for myTrkpt in root[1][2]:
  #Calculate the elapsed time
  TimeNow = myTrkpt[1].text
  TNow = datetime.strptime(TimeNow[:-5],"%Y-%m-%dT%H:%M:%S")
  TimeElapsed = abs(TNow - TStart).seconds

  #Build a Python dictionary that we'll write to the MongoDB
  MongoDoc = {}
  MongoDoc["elapsed"] = TimeElapsed
  MongoDoc["lat"] =  myTrkpt.attrib.get('lat')
  MongoDoc["lng"] =  myTrkpt.attrib.get('lon')
  MongoDoc["elevation"] =  myTrkpt[0].text
  MongoDoc["timestamp"] =  TimeNow[:-5]
  MongoDoc["heart"] = myTrkpt[2][0][0].text
  MongoDoc["cadence"] = 0

  #Write the document to the footie collection
  collection.insert_one(MongoDoc)

print("Done")


So here I use the Python XML and pymongo modules to parse the GPX files and write to the database respectively.

With pymongo you create an object and then connect to the database using a "Connection String".  You get this from the Azure management console for the MongoDB instance under the "Connection String" settings area.  This string contains the database address and the credentials required to access it.  You then can create a collection object which you write documents to using the insert_one() method.

Using the XML module you create an object called "root" and can use indices to access the different parts of the GPX structure.  So for example root[0] will be the first part of the GPX file.

The code then loops through the <trkpt> elements of the GPX file, picks out all the relevant data and then creates a Python dictionary which will be written to the database.  I also calculate an "elapsed" field which is the difference in seconds between the first <trkpt> elements and the element in question.  I foresee this being useful later...

It was interesting to look at the Azure console as I ran the scripts to parse the GPX files and write documents to the database.  Here's what was shown:

Here you can see a peaks "insert" requests as the data was being inserted.  The two peaks represent the two separate files being parsed and loaded.

Step 3 - A Web Server and an API
I wanted to create an API such that Javascript running within a browser could make a AJAX request to extract the data.  At some point I'll explore an Azure Web App for this sort of thing but for now I decided to use an Apache web server running on the Azure Linux VM and use a cgi-bin Python script to provide the functionality of the API.  I simply ran the sudo apt-get install apache2 command to install Apache and used this guide to get cgi-bin working for Python scripts.

To get the web server to work I had to do some configuration within the Azure console.  Specifically I had to configure a rule to enable HTTP traffic (port 80) on the platform.  To do this I selected the VM from the console, selected "Network Interfaces" then selected the Network Security Group.  I then configured the "HTTPRule" shown below:


To create the API I wrote the following Python script:



#!/usr/bin/env python

#Import statements
from pymongo import MongoClient
import cgi
import re
import cgitb

#Enable error logging
cgitb.enable()

#Database related - Got all these from "Connection String" area on Azure for the database instance
dBAddress = 'Your Connection String' 
CollectionID = 'Test'

#Get the query string parameters provided.  'name' field is the mongo name, 'value' field is the value
arguments = cgi.FieldStorage()
MongoName = arguments['name'].value
MongoValue = arguments['value'].value

#Form the document to use for the database access.  We will do a Regex because we may be searching on a partial date
MongoRegex = {}
MongoRegex['$regex'] = MongoValue
MongoDoc = {}
MongoDoc[MongoName] = MongoRegex

#Connect to the database, get a database object and get a collection
client = MongoClient(dBAddress)
db = client.geekmongo
collection = db.Test

print ('content-type: application/json\n\n')

#Get the total number of documents returned and set up a counter variable
TotalDocCount = collection.count(MongoDoc)
DocCounter = 0

#Start the output string
OutString = '{"markers":['

#Do a database find based upon the parameters provided.  Use this to form the output.  Need elapsed (integer), lat (long 4dp), lng (long 4dp)
#elevation (1dp), timestamp (string) and heart rate (integer)
for rhmDoc in collection.find(MongoDoc):
  OutString = OutString + '{"elapsed":' + str(rhmDoc["elapsed"]) + ','
  OutString = OutString + '"lat":' + str(round(float(rhmDoc["lat"]),4)) + ',' 
  OutString = OutString + '"lng":' + str(round(float(rhmDoc["lng"]),4)) + ','
  OutString = OutString + '"elevation":' + str(round(float(rhmDoc["elevation"]),1)) + ','
  OutString = OutString + '"timestamp":' + chr(34) + rhmDoc["timestamp"] + '",'
  OutString = OutString + '"heart":' + str(rhmDoc["heart"]) + '}'

  #See how many documents we've dealt with and whether we need to add a , to the end of the document
  DocCounter += 1
  #If we're on the last document then we add the ] to close the JSON array
  if (DocCounter == TotalDocCount):
    OutString = OutString + '],'
  else:
    OutString = OutString + ','

#Add the total items part
OutString = OutString + '"TotalItems":' + str(TotalDocCount) + '}'

#Stream the output to the client
print OutString

Here I use the CGI module to read query string parameters provided by the client.  So for example the URL:

http://<server URL>/GetGPXData.py?name=heart&value=82


...will result in a database query being made for all documents that contain the heart rate value of 82.

The script then takes the response of the database query and forms a string JSON document with all the values to pass back to the client.


The "regex" component of the MongoDB query document means you can do a "contains" search on the database.  i.e. "contains 2016" to return all the values for 2016.

Step 4 - Web Page, Javascript and Google Maps API
So the final part of the project was to write a web page that could use Javascript to a)download data from the API I just created and b)plot it on a map using the Google Maps API.
  • The HTML, CSS and Javascript is below.  Highlights:
  • Uses Google maps Javascript API to bring up a map, place and move markers.  I started with this tutorial.
  • Uses the API previously described to acquire the position data to plot (function startRace).
  • Uses a Javascript interval to "fire" and cause an assessment of position and the map to be updated
  • Calculates the straight line distance between the markers.
<!DOCTYPE html>
<html>
<head>
<style>
#map {
height: 500px;
width: 100%;
}
</style>

</head>
<body>
<h3>Reading Half Marathon Analysis</h3>
<div id="map"></div>
<input type='button' id='btnLoad' value='Load One' onclick='loadLocsOne();'>
<input type='button' id='btnLoad' value='Load Two' onclick='loadLocsTwo();'>
<p id="Distance"></p>
<input type='button' id='btnLoad' value='Race' onclick='startRace();'>
<input type='button' id='btnLoad' value='Stop' onclick='stopRace();'>
<input type='button' id='btnLoad' value='Heart Chart' onclick='heartChart();'>
<script type="text/javascript">
//This is V10 that adds getting the data from an 'API'
//Some nasty global variables. Discovered needed to use setInterval to control a marker and these were needed for that.
var map //Enables us to reference the map in all parts of the code
var markerOne //A marker entity
var markerTwo //A marker entity
var timeElapsed //A variable to hold how many seconds have elapsed
var maxElapsed //Defines the maximum elapsed time we'll have across the two JSON structures
var locsOne = {}; //A position array
var locsTwo = {}; //A position array
var intervalVar //Use for the setInterval thingy
var raceStarted //Boolean that defines whether the race has started
//Initialise the map and put a marker on it
function initMap() {
var readingOne = {lat: 51.4366827, lng: -0.9680389};
var readingTwo = {lat: 51.4466827, lng: -0.9780389};
map = new google.maps.Map(document.getElementById('map'), {
zoom: 13,
center: readingOne
});
markerOne = new google.maps.Marker({
position: readingOne,
map: map,
label: "6"
});
markerTwo = new google.maps.Marker({
position: readingTwo,
map: map,
label: "7"
//color: 0xFFFFFF
});
//This is a load or reload so state that the race is not started
raceStarted = false;
}
//Just move a marker
function positionMarker(inMarker, inLat, inLng)
{
//Set the position of the marker
var newPos = {lat: inLat, lng: inLng};
//Set the position of the marker
inMarker.setPosition(newPos);
}
//Initialises matters when user presses "Race"
function startRace()
{
//What we do first depends on whether the race is started!
if (raceStarted == false)
{
//Initialise the position number
timeElapsed = 0;
//We need to find out the max elapsed time across the two structures. In this way we'll increment the elapsed time every time the interval handler
//fires. If we find a position we update the marker. If not we leave the marker where it is. When we've exhausted all possible elapsed times then
//we know to stop the handler
var maxElapsedOne = locsOne.markers[locsOne.TotalItems - 1].elapsed;
var maxElapsedTwo = locsTwo.markers[locsTwo.TotalItems - 1].elapsed;
//Set up the max elapsed value
if (maxElapsedOne > maxElapsedTwo)
{
maxElapsed = maxElapsedOne;
}
else
{
maxElapsed = maxElapsedTwo;
}
raceStarted = true;
}
//Set up to move the marker
intervalVar = setInterval(function(){ assessMarkerMove()}, 10);
}
//Stops the race
function stopRace()
{
clearInterval(intervalVar);
}
//Handles assessing whether to move the markers and if required doing so
//locs.markers[posNumber].lat,locs.markers[posNumber].lng
function assessMarkerMove()
{
//Variables
var i
//See if we can find a marker associated with the current elapsed time
for (i in locsOne.markers)
{
if (locsOne.markers[i].elapsed == timeElapsed)
{
positionMarker(markerOne, locsOne.markers[i].lat, locsOne.markers[i].lng);
}
}
for (i in locsTwo.markers)
{
if (locsTwo.markers[i].elapsed == timeElapsed)
{
positionMarker(markerTwo, locsTwo.markers[i].lat, locsTwo.markers[i].lng)
}
}
//Calculate the straightline distance between the markers. Only do this every 10 iterations else it look messy
if (Number.isInteger(timeElapsed / 10) == true){
distanceBetween = Math.round(google.maps.geometry.spherical.computeDistanceBetween(markerOne.position, markerTwo.position));
document.getElementById("Distance").innerHTML = distanceBetween + ' metres between!';}
//Increment the counter of how many times this has been called
timeElapsed++;
//See whether we've reached the end of the array
if (timeElapsed > maxElapsed)
{
clearInterval(intervalVar);
raceStarted = false;
}
}
//Called when the load data button is pressed
function loadData()
{
loadLocsOne();
loadLocsTwo();
document.getElementById("Distance").innerHTML = 'Data Loaded!!';
}
//Load the first array
function loadLocsOne() {
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
//alert(this.responseText);
locsOne = JSON.parse(this.responseText);
document.getElementById("Distance").innerHTML = 'locsOne Loaded';
}
};
xhttp.open("GET", "http://a.b.c.d/cgi-bin/GetGPXData.py?name=timestamp&value=2016", true);

}
//Load the second array
function loadLocsTwo() {
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
locsTwo = JSON.parse(this.responseText);
document.getElementById("Distance").innerHTML = 'locsTwo Loaded';}
};
xhttp.open("GET", "http://a.b.c.d/cgi-bin/GetGPXData.py?name=timestamp&value=2017", true);
xhttp.send();
}
</script>
<script async defer
src="https://maps.googleapis.com/maps/api/js?key=<Your Key Here>&callback=initMap&libraries=geometry">
</script>
</body>
</html>


Friday, 11 March 2016

First Football (Soccer) Stats Analysis Using Raspberry Pi, Python, MongoDB and R

In my last post I described the setup I'd created on my Raspberry Pi to do football (soccer for some of you) statistics analysis.  Here's an "architecture" diagram:

So in simple terms I:

  • Use Python to gather data from internet sources, parse it and load it into..
  • MongoDB where I store data in document format before...
  • Analysing the data using R and...
  • Presenting the results for you lucky people in Blogger!

So it was time to gather some data, load it and analyse it!

A quick look around the internet showed me a variety of sources, the first of which was:

http://www.football-data.co.uk/

This is a betting focussed website but has a set of free CSV files showing results from multiple European leagues going back to the early '90s, (older data is more sparse than more recent data).  As I say, they offer their data for free but I urge you to say thanks to them as an ad funded website in the only way you can if you see what I mean.

Grabbing a file and looking at it in LibreOffice Calc shows there to be all sorts of interesting data available.  Here's a screenshot showing data for the Belgian Pro league.


So much nice data to play with!!

Step 1- Getting All the Data
Looking at an example URL for one of the CSV files on football-data.co.uk shows it to be:
http://www.football-data.co.uk/mmz4281/1516/E0.csv

So here we see:
  • A base URL - http://www.football-data.co.uk/mmz4281/
  • Four digits indicating the year.  Here 1516 means the season 2015-16
  • The league in question.  Here E0 means English Premier League

Hence it's pretty easy to write a Python script to iterate through the years (9394 to 1516) and leagues to grab all the CSV files.  The script is below.  The comments should explain all but in simple terms it iterates through a list of years (YearList) and for each year a list of leagues (LeagueList), forms a URL for a wget command and then renames the file so we can have all the leagues for all the years in the same folder.

#Downloading a bulk load of Football CSV files from http://www.football-data.co.uk/mmz4281/
#Example URL is http://www.football-data.co.uk/mmz4281/1516/E0.csv - This is the URL for EPL Season 1516
import os
import time

#Constants
DirForFiles = "/home/pi/Documents/FootballProject/"

#Next part is a set of digits that represent the year.  Do these as a list
YearList = ['9394','9495','9596','9697','9798','9899','9900','0001','0102','0203','0304','0405','0506','0607','0708','0809','0910','1011','1112','1213','1314','
1415','1516']

#Then the values that are used to represent leagues.  Another List
LeagueList = ['E0','E1','E2','E3','EC','SC0','SC1','SC2','SC3','D1','D2','I1','I2','SP1','SP2','F1','F2','N1','B1','P1','T1','G1']


#Iterate through the years
for TheYear in YearList:
  #Now iterate through the leagues, forming the command to get
  for TheLeague in LeagueList:
    GetCommand = "sudo wget " + BaseURL + TheYear + "/" + TheLeague + ".csv"
    
    #Also form the name the file will take when downloaded
    FileWhenDownloaded = DirForFiles + TheLeague + ".csv"

    #And the file name to rename to
    RenameFileTo = DirForFiles + TheLeague + "_" + TheYear + ".csv"
    
    #Run the wget command#
    os.system(GetCommand)
    time.sleep(0.5)

    #Rename the file
    RenameCommand = "sudo mv " + FileWhenDownloaded + " " + RenameFileTo
    os.system(RenameCommand)
    time.sleep(0.5)
   

    #print (GetCommand + "|" + FileWhenDownloaded + "|" + RenameFileTo)

A quick ls command shows all the lovely files ready for analysing! 


Step 2 - Loading into MongoDB 
In my last post I covered the basics of loading data into MongoDB using Python. Now it was time to load data downloaded from the football-data site.  I decided to just load one league for one season to have an initial play with the data.

It seems that there is different fields for different leagues for different years.  Field names are common, it's just that they're absent or present from file to file.  The full list of field names is here.

I decided to model the data as one simple JSON document per match with each record as a key value pair.  So for example a simplified document for the first match of the Belgian league file shown above would be:

{
  "Div":"B1",
  "Date":"31/07/09",
  "HomeTeam":"Standard",
  "AwayTeam":"St Truiden"
}

There may well be more elegant ways of modelling the data.  As I explore and learn more I'll work this out!

To prepare MongoDB I created a database called "Footie" and a collection called "Results" using these commands in the Mongo utility:

> use Footie
> db.createCollection("Results")

I wrote the script below to load data for one season and one league (English League 2, 2015/16).

In the script I:

  • Create an object to access MongoDB
  • Open a CSV file to read it
  • Read the first line, remove trailing non-printing characters, split it and use this to form a list (HeaderList) that will make up the key element of the JSON document.
  • Then for each subsequent line, read it, split it into another list (LineList)
  • I then iterate through each list, forming key value pairs and writing them to a Python dictionary, (which is required to write a JSON document to MongoDB).
  • I then write the document to MongoDB!
#Create JSON from football results and write to MongoDB
from pymongo import MongoClient
import sys

#Constants
DirPath = "/home/pi/Documents/FootballProject/"

#Connect to the Footie database
client = MongoClient()
db = client.Footie
#Get a collection
collection = db.Results

#Open the file to process
MyFile = open(DirPath + "E3_1516.csv",'r')

#Read the first lines which is the header line, remove the \r\n at the end and turn it into a list
LineOne = MyFile.readline()
LineOne = LineOne[:-2]
HeaderList = LineOne.split(',')

#Now loop through the file reading lines, creating JSONs and writing them
FileLine = MyFile.readline()
while len(FileLine) > 0:
  print(FileLine)
  #Get rid of last two characters and put in a list
  FileLine = FileLine[:-2]
  LineList = FileLine.split(',')
  
  #Form a JSON from these lists; needs to be a Python Dictionary
  JSONDict = dict()

  #Loop through both lists and add to the JSON
  for i in range(len(HeaderList)):
    #Interestingly field names in MongoDB can't contain a "." so turn it into a European style ","
    HeaderList[i] = HeaderList[i].replace(".",",")
    JSONDict[HeaderList[i]] = LineList[i]
  
  #Write the document to the collection
  print JSONDict
  collection.insert_one(JSONDict)

  #Setup for next loop
  FileLine = MyFile.readline()

print ("Finished writing JSONs")

#Close the file
MyFile.close()

The net result from the Mongo tool being (abridged):

> db.Results.find()
{ "_id" : ObjectId("56ccc42d74fece04d3d5a323"), "BbAHh" : "0.25", "HY" : "1", "BbAH" : "25", "BbMx<2,5" : "1.7", "HTHG" : "0", "HR" : "0", "HS" : "12", "VCA" : "2.5", "BbMx>2,5" : "2.28", "BbMxD" : "3.4", "AwayTeam" : "Luton", "BbAvD" : "3.19", "PSD" : "3.34", "BbAvA" : "2.38", "HC" : "3", "HF" : "12", "Bb1X2" : "44", "BbAvH" : "2.96", "WHD" : "3.2", "Referee" : "G Eltringham", "WHH" : "2.9", "WHA" : "2.5", "IWA" : "2.2", "AST" : "4", "BbMxH" : "3.25", "HTAG" : "0", "BbMxAHA" : "2.14", "IWH" : "2.8", "LBA" : "2.4", "BWA" : "2.15", "BWD" : "3.2", "LBD" : "3.25", "HST" : "4", "PSA" : "2.46", "Date" : "08/08/15", "LBH" : "3.1", "BbAvAHA" : "2.06", "BbAvAHH" : "1.77", "IWD" : "3.1", "AC" : "4", "FTR" : "D", "VCD" : "3.4", "AF" : "15", "VCH" : "3", "FTHG" : "1", "BWH" : "3.1", "AS" : "10", "AR" : "0", "BbAv<2,5" : "1.65", "AY" : "0", "BbAv>2,5" : "2.16", "Div" : "E3", "PSH" : "3.08", "B365H" : "3.2", "HomeTeam" : "Accrington", "B365D" : "3.4", "B365A" : "2.4", "BbMxAHH" : "1.82", "HTR" : "D", "BbOU" : "37", "FTAG" : "1", "BbMxA" : "2.5" }
{ "_id" : ObjectId("56ccc4c674fece05830a706c"), "BbAHh" : "0.25", "HY" : "1", "BbAH" : "25", "BbMx<2,5" : "1.7", "HTHG" : "0", "HR" : "0", "HS" : "12", "VCA" : "2.5", "BbMx>2,5" : "2.28", "BbMxD" : "3.4", "AwayTeam" : "Luton", "BbAvD" : "3.19", "PSD" : "3.34", "BbAvA" : "2.38", "HC" : "3", "HF" : "12", "Bb1X2" : "44", "BbAvH" : "2.96", "WHD" : "3.2", "Referee" : "G Eltringham", "WHH" : "2.9", "WHA" : "2.5", "IWA" : "2.2", "AST" : "4", "BbMxH" : "3.25", "HTAG" : "0", "BbMxAHA" : "2.14", "IWH" : "2.8", "LBA" : "2.4", "BWA" : "2.15", "BWD" : "3.2", "LBD" : "3.25", "HST" : "4", "PSA" : "2.46", "Date" : "08/08/15", "LBH" : "3.1", "BbAvAHA" : "2.06", "BbAvAHH" : "1.77", "IWD" : "3.1", "AC" : "4", "FTR" : "D", "VCD" : "3.4", "AF" : "15", "VCH" : "3", "FTHG" : "1", "BWH" : "3.1", "AS" : "10", "AR" : "0", "BbAv<2,5" : "1.65", "AY" : "0", "BbAv>2,5" : "2.16", "Div" : "E3", "PSH" : "3.08", "B365H" : "3.2", "HomeTeam" : "Accrington", "B365D" : "3.4", "B365A" : "2.4", "BbMxAHH" : "1.82", "HTR" : "D", "BbOU" : "37", "FTAG" : "1", "BbMxA" : "2.5" }

Step 3 - Some Analysis in R
Using R I did some initial analysis of the data.  I thought it would be interesting to ask the all-important question "who is the naughtiest team in League 2?".  The data can help as the following fields are present:

HF = Home Team Fouls Committed
AF = Away Team Fouls Committed 
HY = Home Team Yellow Cards
AY = Away Team Yellow Cards
HR = Home Team Red Cards
AR = Away Team Red Cards



Before using I, I practised the query I wanted to run in the MongoDB shell.  The query is:


> use Footie 
switched to db Footie 
> db.Results.find({},{HomeTeam: 1, AwayTeam:1,HF:1,AF:1,HY:1,AY:1,HR:1,AR:1,_id: 0})

Which breaks down as:

> db.Results.find({}, means run a query on the Results collection and provide no filter parameters, i.e. give everything. 

...and...

{HomeTeam: 1, AwayTeam:1,HF:1,AF:1,HY:1,AY:1,HR:1,AR:1,_id: 0}) 
 means turn all the fields with a "1" on in the output and turn the _id (which is on by default) off.

...and this yields (abridged):

> db.Results.find({},{HomeTeam: 1, AwayTeam:1,HF:1,AF:1,HY:1,AY:1,HR:1,AR:1,_id: 0})
{ "HY" : "1", "HR" : "0", "AwayTeam" : "Luton", "HF" : "12", "AF" : "15", "AR" : "0", "AY" : "0", "HomeTeam" : "Accrington" }
{ "HY" : "1", "HR" : "0", "AwayTeam" : "Luton", "HF" : "12", "AF" : "15", "AR" : "0", "AY" : "0", "HomeTeam" : "Accrington" }
{ "HY" : "1", "HR" : "0", "AwayTeam" : "Plymouth", "HF" : "9", "AF" : "7", "AR" : "0", "AY" : "0", "HomeTeam" : "AFC Wimbledon" }
{ "HY" : "1", "HR" : "0", "AwayTeam" : "Northampton", "HF" : "11", "AF" : "8", "AR" : "0", "AY" : "1", "HomeTeam" : "Bristol Rvs" }
{ "HY" : "1", "HR" : "0", "AwayTeam" : "Newport County", "HF" : "9", "AF" : "12", "AR" : "0", "AY" : "2", "HomeTeam" : "Cambridge" }

To get the same data into a R data frame I did the following to set things up:








> library(RMongo)
Loading required package: rJava
> mg1 <- mongoDbConnect('Footie')
> print(dbShowCollections(mg1))
[1] "Results"        "system.indexes"

...then this to run the query.  

> query <- dbGetQueryForKeys(mg1, 'Results', "{}","{HomeTeam: 1, AwayTeam:1,HF:1,AF:1,HY:1,AY:1,HR:1,AR:1,_id:0}")
> data1 <- query

(Note the use of the dbGetQueryForKeys method which splits the Mongo shell query shown above into two parts).

Which gives this output (abridged):

> data1
          HomeTeam       AwayTeam HF AF HY AY HR AR X_id
1       Accrington          Luton 12 15  1  0  0  0   NA
2    AFC Wimbledon       Plymouth  9  7  1  0  0  0   NA
3      Bristol Rvs    Northampton 11  8  1  1  0  0   NA
4        Cambridge Newport County  9 12  1  2  0  0   NA
5           Exeter         Yeovil  5 10  1  0  0  0   NA

...am not sure why I get the X_id put I'm sure I can deal with it!

I now need to get this side-by-side data (so home and away team on the same row) into a data frame where there's one row per match per team.

To do this I created one data frame for home teams, one for away teams then merged them.

First for the home team.  Get the home team data (columns 1, 3, 5 and 7) and then rename them to make them consistent when we combine home and away data frames:

> data2 <- data1[,c(1,3,5,7)
> colnames(data2) <- c("Team","Fouls","Yellows","Reds") 
> head(data2)
           Team Fouls Yellows Reds
1    Accrington    12       1    0
2 AFC Wimbledon     9       1    0
3   Bristol Rvs    11       1    0
4     Cambridge     9       1    0
5        Exeter     5       1    0
6    Hartlepool    12       4    0

Get away team data and rename:

> data3 <- data1[,c(2,4,6,8)]
> colnames(data3) <- c("Team","Fouls","Yellows","Reds")
            Team Fouls Yellows Reds
1          Luton    15       0    0
2       Plymouth     7       0    0
3    Northampton     8       1    0
4 Newport County    12       2    0
5         Yeovil    10       0    0
6      Morecambe    14       2    0

Merge the two together using the bind function:

data4 < rbind(data2, data3)

> head(data4)
           Team Fouls Yellows Reds
1    Accrington    12       1    0
2 AFC Wimbledon     9       1    0
3   Bristol Rvs    11       1    0
4     Cambridge     9       1    0
5        Exeter     5       1    0
6    Hartlepool    12       4    0

So as a quick check, in the first data frame we had this as the bottom result:

> data1[371,]
    HomeTeam   AwayTeam HF AF HY AY HR AR X_id
371   Yeovil Portsmouth  7 10  1  0  0  1   NA

Now you can see this is split over two rows of our data frame:
> data4[c(371,742),]
           Team Fouls Yellows Reds
371      Yeovil     7       1    0
3711 Portsmouth    10       0    1

Now aggregate to get the count per team across the season so far.  Here you first list the values you're aggregating, then what to group them by, the the function (sum in this case):

> data_agg <-aggregate(list(Fouls=data4$Fouls,Yellows=data4$Yellows,Reds=data4$Reds), list(Team=data4$Team), sum)

Which gives us (abridged):

> data_agg
             Team Fouls Yellows Reds
1      Accrington   302      57    3
2   AFC Wimbledon   377      40    3
3          Barnet   331      46    3
4     Bristol Rvs   299      39    2

Not much use so time to order the data using the "order" function.  There are many ways to order, I selected to order by fouls first, then yellow cards, then red cars.

> agg_sort <- data_agg[order(-data_agg$Fouls,-data_agg$Yellows,-data_agg$Reds),]
> agg_sort
             Team Fouls Yellows Reds
22        Wycombe   420      48    2
13      Mansfield   417      62    5
2   AFC Wimbledon   377      40    3
8     Dag and Red   373      42    2
21      Stevenage   351      56    3
12          Luton   351      53    3
7    Crawley Town   348      52    4
16    Northampton   347      44    5
11  Leyton Orient   338      45    3
19       Plymouth   332      53    0
3          Barnet   331      46    3
17   Notts County   329      56    3
5       Cambridge   329      38    3
23         Yeovil   317      34    3
24           York   316      50    3
18         Oxford   310      46    2
15 Newport County   306      37    2
1      Accrington   302      57    3
9          Exeter   301      38    0
4     Bristol Rvs   299      39    2
14      Morecambe   294      55    2
6        Carlisle   284      39    3
20     Portsmouth   264      28    5
10     Hartlepool   252      45    2

So there, it's official*, Wycombe Wanderers are the naughtiest team in English League 2.



(*Apologies for fans of Wycombe.  It;s just stats geekery and no reflection on your fine football team!).