Callysto.ca Banner

Open in Callysto

1. Introduction to Socrata

by Gustaaf J Wehnes, Calgary Board of Education

We will be using Socrata open data for our forays into the rapdily emerging field of data science. Socrata is used by many governments and organiations, including our own City of Calgary. Take some time to familiarize yourself with the technology through their home page

The SODA API

As programmers, we will want to know how to programatically interact with open data sources. will be using an API (Application Programming Interface) developed Thus, you will also want to bookmark the Socrata Open Data API (aka SODA). Pay particular attention to the Getting Started and the API Docs pages.

If you don’t know what an API is, here is a definition

The City of Calgary datasets

Also have a look at The City of Calgary’s Open Data Portal. There are all kinds of cool data sets here, along with tools for visualizing this data. Hopefully you can already start seeing how open data provides a massive opportunity for app developers and researchers.

In particular, have a look at three datasets we will be using in our journey: River Levels and Flows, School Locations, and School Enrolment.

https://dev.socrata.com/docs/functions/#,

https://data.calgary.ca/Environment/River-Levels-and-Flows/5fdg-ifgr

Queries in SODA

The Socrata interface allows the building of SQL-like queries: query Parameter. Theses queries are much more readable than the queries we were writing before using keywords in a URL

To allow the potential running of multiple queries, let’s take the time to create a function that does some of the dirty work for us. We use the requests library to obtain data from an internet request based on the domain, uuid, and query. The get method returns a byte array, which we then have to conver to a dataframe

function run_query

paramter domain: refers to the organization that supplies the data

parameter uuid: universal unique ID; refers to the specific dataset.

parameter query: SODA query

returns: panadas dataframe

Python tools

The last few pieces that needs introducing are the Python libraries that we will use. Again, take some time to have a look. The first is an open source data analysis and manipulation tool called pandas. Two other libraries, which you only need to be passingly familiar with, are requests and Python io.

Don’t get overwhelmed; there is a lot of functionality here, and just like me, you will just learn it as you go.

The beauty of using Jupyter is that these tools are pre-installed! So, no messing with downloading and installing. We simply import them :-)

import requests as rq
import pandas as pd
import io as io

Some terminology and a helper function

To simplify the execution of multiple queries, let’s take the time to create a function that does some of the dirty work for us. We use the requests library to obtain data from an internet request based on the domain, uuid of the dataset, and the query itself. A ‘session’ object is created with the requests library; the get method is used to return a byte array from an http source, and we then covert that array to a pandas dataframe object. Note that we also import the four libraries previously mentioned.

function run_query

paramter domain: refers to the organization that supplies the data

parameter uuid: universal unique ID; refers to the specific dataset.

parameter query: SODA query

returns: panadas dataframe

def run_query(domain, uuid, query):
    session = rq.Session()
    results = session.get(domain + uuid +".csv?$query=" + query)
    dataframe =  pd.read_csv(io.StringIO(results.content.decode('utf-8')))
    return dataframe

READY? Lets query some data!

You may already have come across the documentation for the SODA query. This will look quite familiar to the SQL statements that you have already learned about. Let’s run through a pile of examples, where you can see how you can use the tools to obtain data in CSV (or JSON) format.

simple query

Retrieve the entire dataset.

Note:

  • A maximum of 1000 records are returned. That is probably a good thing, as the dataset itself is huge (5.48 million records as of April 2020, and ever growing).

domain = "https://data.calgary.ca/resource/"

uuid_river_levels = "5fdg-ifgr"
uuid_school_enrollment = "9qye-mibh"
query = """
SELECT *
"""

river_levels = run_query(domain, uuid_river_levels, query)
river_levels
station_number station_name timestamp level flow id
0 05BJ008 Glenmore Reservoir at Calgary 2020-07-03T07:15:00.000Z 1074.356 NaN 05BJ008-2020-07-03 07:15:00
1 05BH005 Bow River near Cochrane 2020-07-03T07:15:00.000Z 17.026 244.822 05BH005-2020-07-03 07:15:00
2 05BH005 Bow River near Cochrane 2020-07-03T07:10:00.000Z 17.025 244.472 05BH005-2020-07-03 07:10:00
3 05BJ004 Elbow River at Bragg Creek 2020-07-03T07:05:00.000Z 1.821 26.320 05BJ004-2020-07-03 07:05:00
4 05BJ001 Elbow River below Glenmore Dam 2020-07-03T07:05:00.000Z 1.300 32.213 05BJ001-2020-07-03 07:05:00
... ... ... ... ... ... ...
995 05BH015 Jumpingpound Creek at Township Road 252 2020-07-02T19:25:00.000Z 2.936 8.152 05BH015-2020-07-02 19:25:00
996 05BH014 Nose Creek above Airdrie 2020-07-02T19:25:00.000Z 9.193 3.146 05BH014-2020-07-02 19:25:00
997 05BH004 Bow River at Calgary 2020-07-02T19:25:00.000Z 1.865 281.155 05BH004-2020-07-02 19:25:00
998 05BJ001 Elbow River below Glenmore Dam 2020-07-02T19:25:00.000Z 1.330 34.213 05BJ001-2020-07-02 19:25:00
999 05BJ004 Elbow River at Bragg Creek 2020-07-02T19:25:00.000Z 1.845 27.569 05BJ004-2020-07-02 19:25:00

1000 rows × 6 columns

limiting the amount of data

Retrieve only the first 100 record of the dataset.

Note:

  • You can set any limit that you like, but be aware of how much data you are going to get back. Obviously, the larger the dataset (rows * columns) the longer the download time (and the data used).

query = """
SELECT
    *
LIMIT
    100
"""

river_levels = run_query(domain, uuid_river_levels, query)
river_levels
station_number station_name timestamp level flow id
0 05BJ008 Glenmore Reservoir at Calgary 2020-07-03T07:15:00.000Z 1074.356 NaN 05BJ008-2020-07-03 07:15:00
1 05BH005 Bow River near Cochrane 2020-07-03T07:15:00.000Z 17.026 244.822 05BH005-2020-07-03 07:15:00
2 05BH005 Bow River near Cochrane 2020-07-03T07:10:00.000Z 17.025 244.472 05BH005-2020-07-03 07:10:00
3 05BH004 Bow River at Calgary 2020-07-03T07:05:00.000Z 1.763 248.663 05BH004-2020-07-03 07:05:00
4 05BH005 Bow River near Cochrane 2020-07-03T07:05:00.000Z 17.025 244.472 05BH005-2020-07-03 07:05:00
... ... ... ... ... ... ...
95 05BH005 Bow River near Cochrane 2020-07-03T05:50:00.000Z 17.023 243.777 05BH005-2020-07-03 05:50:00
96 05BJ001 Elbow River below Glenmore Dam 2020-07-03T05:50:00.000Z 1.361 36.300 05BJ001-2020-07-03 05:50:00
97 05BH004 Bow River at Calgary 2020-07-03T05:50:00.000Z 1.767 249.901 05BH004-2020-07-03 05:50:00
98 05BH005 Bow River near Cochrane 2020-07-03T05:45:00.000Z 17.023 243.777 05BH005-2020-07-03 05:45:00
99 05BH015 Jumpingpound Creek at Township Road 252 2020-07-03T05:45:00.000Z 2.907 7.294 05BH015-2020-07-03 05:45:00

100 rows × 6 columns

paging through data

Retrieve records 101 to 200. You will again receive 100 record.

Note:

  • This is known as paging. You could, if you want, retrieve the data in small chunks. We will not be doing this in this course, but it is good to know that it can be done.

query = """
SELECT
    *
LIMIT
    100
OFFSET
    100
"""

river_levels = run_query(domain, uuid_river_levels, query)
river_levels
station_number station_name timestamp level flow id
0 05BJ010 Elbow River at Sarcee Bridge 2020-07-03T05:45:00.000Z 4.438 33.929 05BJ010-2020-07-03 05:45:00
1 05BH005 Bow River near Cochrane 2020-07-03T05:45:00.000Z 17.023 243.777 05BH005-2020-07-03 05:45:00
2 05BJ004 Elbow River at Bragg Creek 2020-07-03T05:45:00.000Z 1.823 26.423 05BJ004-2020-07-03 05:45:00
3 05BH015 Jumpingpound Creek at Township Road 252 2020-07-03T05:45:00.000Z 2.907 7.294 05BH015-2020-07-03 05:45:00
4 05BJ008 Glenmore Reservoir at Calgary 2020-07-03T05:45:00.000Z 1074.358 NaN 05BJ008-2020-07-03 05:45:00
... ... ... ... ... ... ...
95 05BH004 Bow River at Calgary 2020-07-03T04:35:00.000Z 1.761 248.046 05BH004-2020-07-03 04:35:00
96 05BH014 Nose Creek above Airdrie 2020-07-03T04:35:00.000Z 9.180 2.987 05BH014-2020-07-03 04:35:00
97 05BJ004 Elbow River at Bragg Creek 2020-07-03T04:35:00.000Z 1.823 26.423 05BJ004-2020-07-03 04:35:00
98 05BJ001 Elbow River below Glenmore Dam 2020-07-03T04:35:00.000Z 1.361 36.300 05BJ001-2020-07-03 04:35:00
99 05BH015 Jumpingpound Creek at Township Road 252 2020-07-03T04:35:00.000Z 2.911 7.410 05BH015-2020-07-03 04:35:00

100 rows × 6 columns

select a limited number of fields

This can dramatically reduce the amount of data returned (as the dataframe is like a table, so rows x columns).

query = """
SELECT
    station_number,
    timestamp,
    level
LIMIT
    100    
"""

river_levels = run_query(domain, uuid_river_levels, query)
river_levels
station_number timestamp level
0 05BH005 2019-11-10T10:40:00.000Z 16.306
1 05BH005 2019-11-10T10:45:00.000Z 16.305
2 05BH005 2019-11-10T10:50:00.000Z 16.309
3 05BH005 2019-11-10T10:55:00.000Z 16.307
4 05BH005 2019-11-10T11:00:00.000Z 16.308
... ... ... ...
95 05BH005 2010-04-21T10:30:00.000Z 16.006
96 05BH005 2010-04-21T10:45:00.000Z 16.031
97 05BH005 2010-04-21T11:00:00.000Z 16.030
98 05BH005 2010-04-21T11:15:00.000Z 16.056
99 05BH005 2010-04-21T11:30:00.000Z 16.094

100 rows × 3 columns

providing an alias for a field

You can rename a field with a custom name. This will be useful later when we calculate our own fields; for now, you can see the syntax is simple. Observe that the dataframe has the desired field names.

query = """
SELECT
    station_number as station,
    timestamp as time,
    level as river_level
LIMIT
    100    
"""

river_levels = run_query(domain, uuid_river_levels, query)
river_levels
station time river_level
0 05BH005 2019-11-10T10:40:00.000Z 16.306
1 05BH005 2019-11-10T10:45:00.000Z 16.305
2 05BH005 2019-11-10T10:50:00.000Z 16.309
3 05BH005 2019-11-10T10:55:00.000Z 16.307
4 05BH005 2019-11-10T11:00:00.000Z 16.308
... ... ... ...
95 05BH005 2010-04-21T10:30:00.000Z 16.006
96 05BH005 2010-04-21T10:45:00.000Z 16.031
97 05BH005 2010-04-21T11:00:00.000Z 16.030
98 05BH005 2010-04-21T11:15:00.000Z 16.056
99 05BH005 2010-04-21T11:30:00.000Z 16.094

100 rows × 3 columns

data transform functions

If you look at the dataset ‘meta’ data on the City of Calgary’s website, you will notice that all of the fields have a type. This is no different than in most programming languages: you have text, integers, and even date types.

Unfortunately, fields are sometimes not of the desired type. This may be due to the dataset not being well designed, or for reasons of saving space. Fortunately, SODA provides a series of Data Transform functions which allow you to transform (or ‘cast’) the field to a different type.

Note:

  • Subclauses in the WHERE clause are separated by AND instead of commas, to reflect that these are boolean expressions

  • OR could be used as well

  • at this time, I am not certain why I cannot convert level to a number using the to_number function. Stay tuned…

query = """
SELECT
    station_name,
    timestamp,
    date_extract_y(to_floating_timestamp(timestamp,'UTC')) as year,
    date_extract_m(to_floating_timestamp(timestamp,'UTC')) as month,
    date_extract_d(to_floating_timestamp(timestamp,'UTC')) as day,
    date_extract_hh(to_floating_timestamp(timestamp,'UTC')) as hour,
    date_extract_mm(to_floating_timestamp(timestamp,'UTC')) as minute,
    date_extract_ss(to_floating_timestamp(timestamp,'UTC')) as second,
    level
LIMIT
    10
"""

river_levels = run_query(domain, uuid_river_levels, query)
river_levels
station_name timestamp year month day hour minute second level
0 Bow River near Cochrane 2019-11-10T10:40:00.000Z 2019 11 10 10 40 0 16.306
1 Bow River near Cochrane 2019-11-10T10:45:00.000Z 2019 11 10 10 45 0 16.305
2 Bow River near Cochrane 2019-11-10T10:50:00.000Z 2019 11 10 10 50 0 16.309
3 Bow River near Cochrane 2019-11-10T10:55:00.000Z 2019 11 10 10 55 0 16.307
4 Bow River near Cochrane 2019-11-10T11:00:00.000Z 2019 11 10 11 0 0 16.308
5 Bow River near Cochrane 2019-11-10T11:05:00.000Z 2019 11 10 11 5 0 16.306
6 Bow River near Cochrane 2019-11-10T11:10:00.000Z 2019 11 10 11 10 0 16.306
7 Bow River near Cochrane 2019-11-10T11:15:00.000Z 2019 11 10 11 15 0 16.307
8 Bow River near Cochrane 2019-11-10T11:20:00.000Z 2019 11 10 11 20 0 16.306
9 Bow River near Cochrane 2019-11-10T11:25:00.000Z 2019 11 10 11 25 0 16.307

WHERE clause

Just like in SQL, we can filter the records returned using the WHERE clause. There are a lot of powerful operators available, as per the documentation. Don’t hesitate to experiment!

Note:

  • Subclauses in the WHERE clause are separated by AND instead of commas, to reflect that these are boolean expressions

  • OR could be used as well

query = """
SELECT
    station_name,
    timestamp,
    date_extract_y(to_floating_timestamp(timestamp,'UTC')) as year,
    date_extract_m(to_floating_timestamp(timestamp,'UTC')) as month,
    date_extract_d(to_floating_timestamp(timestamp,'UTC')) as day,
    date_extract_hh(to_floating_timestamp(timestamp,'UTC')) as hour,
    date_extract_mm(to_floating_timestamp(timestamp,'UTC')) as minute,
    date_extract_ss(to_floating_timestamp(timestamp,'UTC')) as second,
    level
WHERE
    station_name = 'Bow River at Calgary' AND
    year = 2004 AND
    (month between 4 AND 5 OR month between 9 AND 10) AND
    day = 1 AND
    hour = 0 AND
    minute = 0
LIMIT
    10
"""

river_levels = run_query(domain, uuid_river_levels, query)
river_levels
station_name timestamp year month day hour minute second level
0 Bow River at Calgary 2004-04-01T00:00:00.000Z 2004 4 1 0 0 0 0.998
1 Bow River at Calgary 2004-05-01T00:00:00.000Z 2004 5 1 0 0 0 0.990
2 Bow River at Calgary 2004-09-01T00:00:00.000Z 2004 9 1 0 0 0 1.416
3 Bow River at Calgary 2004-10-01T00:00:00.000Z 2004 10 1 0 0 0 1.249

ORDER BY clause

Just like in SQL, we can order the records returned using the ORDER BY clause. You can specify multiple fields in order to create a secondary order.

query = """
SELECT
    station_name,
    timestamp,
    date_extract_y(to_floating_timestamp(timestamp,'UTC')) as year,
    date_extract_m(to_floating_timestamp(timestamp,'UTC')) as month,
    date_extract_d(to_floating_timestamp(timestamp,'UTC')) as day,
    date_extract_hh(to_floating_timestamp(timestamp,'UTC')) as hour,
    date_extract_mm(to_floating_timestamp(timestamp,'UTC')) as minute,
    date_extract_ss(to_floating_timestamp(timestamp,'UTC')) as second,
    level
WHERE
    station_name = 'Bow River at Calgary' AND
    year = 2004 AND
    (month between 4 AND 5 OR month between 9 AND 10) AND
    (day = 1 OR day = 15) AND
    hour = 0 AND
    minute = 0
ORDER BY
    month DESC,
    day DESC
LIMIT
    10
"""

river_levels = run_query(domain, uuid_river_levels, query)
river_levels
station_name timestamp year month day hour minute second level
0 Bow River at Calgary 2004-10-15T00:00:00.000Z 2004 10 15 0 0 0 1.185
1 Bow River at Calgary 2004-10-01T00:00:00.000Z 2004 10 1 0 0 0 1.249
2 Bow River at Calgary 2004-09-15T00:00:00.000Z 2004 9 15 0 0 0 1.396
3 Bow River at Calgary 2004-09-01T00:00:00.000Z 2004 9 1 0 0 0 1.416
4 Bow River at Calgary 2004-05-15T00:00:00.000Z 2004 5 15 0 0 0 1.095
5 Bow River at Calgary 2004-05-01T00:00:00.000Z 2004 5 1 0 0 0 0.990
6 Bow River at Calgary 2004-04-15T00:00:00.000Z 2004 4 15 0 0 0 0.997
7 Bow River at Calgary 2004-04-01T00:00:00.000Z 2004 4 1 0 0 0 0.998

GROUP BY clause

This is where things get interesting, as you would know if you’ve gone through the w3schools’scourse on SQL. As this group by section explains, grouping is most often used along with aggregate functions (COUNT, MAX, MIN, SUM, AVG).

For these next examples, we will use the school enrollment dataset. The first example below will check how many high schools existed for each school authority over three years.

You can think of grouping as collapsing the data that would be returned if you ran the query without the GROUP BY clause. All of the records that share the same values for all of the fields are aggregated (collapsed) into a single row. The fields that you are not grouping by thus need to have an aggregate function of some sort in the WHERE clause: hence COUNT(level), which will do exactly that.

Note:

  • Observe how the aggregated field is automatically named

query = """
SELECT
    school_authority_name,
    school_year,
    COUNT(school_name)
WHERE
    (school_year = "2018_2019" OR school_year = "2017_2018" OR school_year = "2016_2017" OR school_year = "2015_2016") AND
    grade_12 > 0
GROUP BY
    school_authority_name,
    school_year
ORDER BY
    school_authority_name,
    school_year
LIMIT
    60
"""
    
school_enrollment = run_query(domain, uuid_school_enrollment, query)
school_enrollment
school_authority_name school_year COUNT_school_name
0 Access International College (Calgary) Incorpo... 2017_2018 1
1 Access International College (Calgary) Incorpo... 2018_2019 1
2 Aurora Learning Foundation (Calgary) 2015_2016 1
3 Aurora Learning Foundation (Calgary) 2016_2017 1
4 Aurora Learning Foundation (Calgary) 2018_2019 1
5 Calgary Academy Society 2017_2018 2
6 Calgary Academy Society 2018_2019 2
7 Calgary Chinese Alliance School Society 2015_2016 1
8 Calgary Chinese Alliance School Society 2018_2019 1
9 Calgary French & International School Society 2015_2016 1
10 Calgary French & International School Society 2016_2017 1
11 Calgary French & International School Society 2017_2018 1
12 Calgary French & International School Society 2018_2019 1
13 Calgary German Language School Society 2015_2016 1
14 Calgary Quest Children's Society 2015_2016 1
15 Calgary Quest Children's Society 2016_2017 1
16 Calgary Quest Children's Society 2017_2018 1
17 Calgary Quest Children's Society 2018_2019 1
18 Calgary Roman Catholic Separate School Distric... 2015_2016 14
19 Calgary Roman Catholic Separate School Distric... 2016_2017 13
20 Calgary Roman Catholic Separate School Distric... 2017_2018 14
21 Calgary Roman Catholic Separate School Distric... 2018_2019 15
22 Calgary School District No. 19 2015_2016 36
23 Calgary School District No. 19 2016_2017 37
24 Calgary School District No. 19 2017_2018 36
25 Calgary School District No. 19 2018_2019 36
26 Centro Linguistico E Culturale Italiano Calgary 2015_2016 1
27 The Calgary Chinese Public School Society 2015_2016 1
28 The Calgary Chinese Public School Society 2016_2017 1
29 The Calgary Society for Effective Education of... 2015_2016 2
30 The Calgary Society for Effective Education of... 2016_2017 2
31 The Canadian Reformed School Society of Calgary 2015_2016 1
32 The Canadian Reformed School Society of Calgary 2016_2017 1
33 The Canadian Reformed School Society of Calgary 2017_2018 1
34 The Canadian Reformed School Society of Calgary 2018_2019 1

You can also try the MAX, MIN, AVG, and SUM functions. In this case, let’s only run the query on schools that have Grade 12 students.

Note:

  • We will provide slightly more descriptive field aliasses.

  • You can re-use a field to get multiple aggregations.

query = """
SELECT
    school_authority_name,
    COUNT(school_name) as total_schools,
    MAX(grade_12) as max_grade_12,
    AVG(grade_12) as avg_grade_12,
    MIN(grade_12) as min_grade_12,
    SUM(grade_12) as total_grade12
WHERE
    school_year = "2018_2019" AND
    grade_12 > 0
GROUP BY
    school_authority_name
LIMIT
    50
"""
    
school_enrollment = run_query(domain, uuid_school_enrollment, query)
school_enrollment
school_authority_name total_schools max_grade_12 avg_grade_12 min_grade_12 total_grade12
0 Aurora Learning Foundation (Calgary) 1 2 2.000000 2 2
1 Calgary Quest Children's Society 1 27 27.000000 27 27
2 Calgary French & International School Society 1 5 5.000000 5 5
3 Calgary School District No. 19 36 981 332.027778 1 11953
4 Calgary Chinese Alliance School Society 1 2 2.000000 2 2
5 Calgary Academy Society 2 51 31.000000 11 62
6 The Canadian Reformed School Society of Calgary 1 14 14.000000 14 14
7 Calgary Roman Catholic Separate School Distric... 15 1458 359.400000 2 5391
8 Access International College (Calgary) Incorpo... 1 9 9.000000 9 9

AN IMPORTANT DISTINCTION

The data above begs the question of what the largest Grade 12 class in the city is! However, you do not have to use the MAX function to find this. Rather, you could simply use the SORT BY and the LIMIT function. This gets around the difficulty of having to either group or aggregate on all fields. If you grouped the above query by school_name, you would receive a maximum for each school. If you were to use an aggregate function instead, you would not get the right school name!

This also allows me to quickly discuss the NaN value, which can be though of like a null. If you do not include the WHERE clause in the query below, you will get a record with NaN,

query = """
SELECT
    COUNT(school_name) as total_schools,
    MAX(grade_12) as max_grade_12
WHERE
    school_year = "2018_2019" AND
    grade_12 > 0
LIMIT
    1
"""
    
school_enrollment = run_query(domain, uuid_school_enrollment, query)
school_enrollment
total_schools max_grade_12
0 59 1458

Conclusion

This notebook introduced Socrata and the SODA API. Up next is an Introduction to Plotly.

Callysto.ca License