From 821cb242014ad27bfc589e67fd68d5465a58b939 Mon Sep 17 00:00:00 2001 From: David H Hagan Date: Thu, 15 Dec 2016 22:56:03 -0500 Subject: [PATCH 1/6] added support for returning a non datetime index using dataframes --- openaq/decorators.py | 4 +++- setup.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/openaq/decorators.py b/openaq/decorators.py index 7b2a6fb..37fd65f 100644 --- a/openaq/decorators.py +++ b/openaq/decorators.py @@ -52,9 +52,11 @@ def decorated_function( *args, **kwargs ): if index == 'utc': data.index = data['date.utc'] del data['date.utc'] - else: + elif index == 'local': data.index = data['date.local'] del data['date.local'] + else: + pass return data diff --git a/setup.py b/setup.py index bf15726..caf403f 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ Written originally by David H Hagan December 2015 ''' -__version__ = '0.3.0' +__version__ = '0.3.1' try: from setuptools import setup From 0478d426673902204ee5041cdbac6f14cb0f286f Mon Sep 17 00:00:00 2001 From: David H Hagan Date: Wed, 21 Dec 2016 13:14:39 -0500 Subject: [PATCH 2/6] added pypi version badge --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 3074bcb..0ac735b 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ [![Build Status](https://travis-ci.org/dhhagan/py-openaq.svg?branch=master)](https://travis-ci.org/dhhagan/py-openaq) +[![PyPI version](https://badge.fury.io/py/py-opc.svg)](https://badge.fury.io/py/py-opc) [![Coverage Status](https://coveralls.io/repos/dhhagan/py-openaq/badge.svg?branch=master&service=github)](https://coveralls.io/github/dhhagan/py-openaq?branch=master) # py-openaq From e5cbd1d9e90fd8f3377a5c388701272cec75aacb Mon Sep 17 00:00:00 2001 From: David H Hagan Date: Wed, 21 Dec 2016 14:00:57 -0500 Subject: [PATCH 3/6] added a pages attribute to all returning meta data --- README.md | 2 +- openaq/__init__.py | 16 +++++++++++----- openaq/decorators.py | 5 +---- openaq/viz.py | 0 setup.py | 2 +- tests/openaq_test.py | 19 +++++++++++++++++-- 6 files changed, 31 insertions(+), 13 deletions(-) create mode 100644 openaq/viz.py diff --git a/README.md b/README.md index 0ac735b..0f4088b 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ [![Build Status](https://travis-ci.org/dhhagan/py-openaq.svg?branch=master)](https://travis-ci.org/dhhagan/py-openaq) -[![PyPI version](https://badge.fury.io/py/py-opc.svg)](https://badge.fury.io/py/py-opc) +[![PyPI version](https://badge.fury.io/py/py-openaq.svg)](https://badge.fury.io/py/py-openaq) [![Coverage Status](https://coveralls.io/repos/dhhagan/py-openaq/badge.svg?branch=master&service=github)](https://coveralls.io/github/dhhagan/py-openaq?branch=master) # py-openaq diff --git a/openaq/__init__.py b/openaq/__init__.py index d952dc0..abce5fc 100644 --- a/openaq/__init__.py +++ b/openaq/__init__.py @@ -1,5 +1,6 @@ import json import requests +import math from pkg_resources import get_distribution from .exceptions import ApiError @@ -59,14 +60,19 @@ def _send(self, endpoint, method = 'GET', **kwargs): url = self._make_url(endpoint, **kwargs) if method == 'GET': - try: - resp = requests.get(url, auth = auth, headers = self._headers) - except Exception as error: - raise ApiError(error) + resp = requests.get(url, auth = auth, headers = self._headers) else: raise ApiError("Invalid Method") - return resp.status_code, resp.json() + res = resp.json() + + # Add a 'pages' attribute to the meta data + try: + res['meta']['pages'] = math.ceil(res['meta']['found'] / res['meta']['limit']) + except: + pass + + return resp.status_code, res def _get(self, url, **kwargs): return self._send(url, 'GET', **kwargs) diff --git a/openaq/decorators.py b/openaq/decorators.py index 37fd65f..223a363 100644 --- a/openaq/decorators.py +++ b/openaq/decorators.py @@ -38,10 +38,7 @@ def decorated_function( *args, **kwargs ): # If there are any datetimes, make them datetimes! for each in [i for i in data.columns if 'date' in i]: - try: - data[each] = pd.to_datetime(data[each]) - except: - pass + data[each] = pd.to_datetime(data[each]) if f.__name__ in ('latest'): data.index = data['lastUpdated'] diff --git a/openaq/viz.py b/openaq/viz.py new file mode 100644 index 0000000..e69de29 diff --git a/setup.py b/setup.py index caf403f..30921e5 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ Written originally by David H Hagan December 2015 ''' -__version__ = '0.3.1' +__version__ = '1.0.0' try: from setuptools import setup diff --git a/tests/openaq_test.py b/tests/openaq_test.py index 61fbcf3..fe4f058 100644 --- a/tests/openaq_test.py +++ b/tests/openaq_test.py @@ -12,6 +12,10 @@ def tearDown(self): def test_setup(self): self.assertIsInstance(self.api, openaq.OpenAQ) + def test_incorrect_api_method(self): + with self.assertRaises(openaq.exceptions.ApiError): + res = self.api._send('cities', method = 'BAD') + def test_cities(self): status, resp = self.api.cities( country = 'US' @@ -19,6 +23,11 @@ def test_cities(self): self.assertTrue(status == 200) + def test_add_pages(self): + status, resp = self.api.cities( country = 'US' ) + + self.assertIsNotNone(resp['meta']['pages']) + def test_countries(self): status, resp = self.api.countries() @@ -53,9 +62,15 @@ def test_pandasize(self): resp = self.api.latest(df = True) resp2 = self.api.measurements(df = True) resp3 = self.api.measurements(df = True, index = 'utc') + resp4 = self.api.measurements(df = True, index = None) + + self.assertIsInstance(resp, pd.DataFrame) + self.assertIsInstance(resp2, pd.DataFrame) + self.assertIsInstance(resp3, pd.DataFrame) + self.assertIsInstance(resp4, pd.DataFrame) - self.assertTrue(type(resp) == pd.DataFrame) - self.assertTrue(type(resp) == pd.DataFrame) + # make sure the index for resp4 are ints + self.assertTrue(type(resp4.index.values[0]), int) def test_fetches(self): status, resp = self.api.fetches() From b7eb1766a38faf4c43cdc1b9c3418b15e8a7dbac Mon Sep 17 00:00:00 2001 From: David H Hagan Date: Wed, 21 Dec 2016 17:35:29 -0500 Subject: [PATCH 4/6] added in the new api endpoints --- openaq/__init__.py | 218 +++++++++++++++++++++++++++++++++++-------- tests/openaq_test.py | 35 ++++++- 2 files changed, 209 insertions(+), 44 deletions(-) diff --git a/openaq/__init__.py b/openaq/__init__.py index abce5fc..eb1ed22 100644 --- a/openaq/__init__.py +++ b/openaq/__init__.py @@ -34,9 +34,11 @@ def _make_url(self, endpoint, **kwargs): extra = [] for key, value in kwargs.items(): - if isinstance(value, list): - value = ','.join(value) - extra.append("{}={}".format(key, value)) + if isinstance(value, list) or isinstance(value, tuple): + #value = ','.join(value) + for v in value: + extra.append("{}={}".format(key, v)) + #extra.append("{}={}".format(key, value)) if len(extra) > 0: endpoint = '?'.join([endpoint, '&'.join(extra)]) @@ -95,19 +97,25 @@ def cities(self, **kwargs): """Returns a listing of cities within the platform. :param country: limit results by a certain country + :param limit: limit results in the query. Default is 100. Max is 10000. + :param page: paginate through the results. Default is 1. :param df: convert the output from json to a pandas DataFrame + :param index: if returning as a DataFrame, set index to ('utc', 'local', None). The default is local :return: dictionary containing the *city*, *country*, *count*, and number of *locations* :type country: 2-digit ISO code + :type limit: number + :type page: number :type df: boolean + :type index: string :Example: >>> import openaq >>> api = openaq.OpenAQ() >>> status, resp = api.cities() - >>> resp + >>> resp['results'] [ { "city": "Amsterdam", @@ -130,28 +138,38 @@ def cities(self, **kwargs): def countries(self, **kwargs): """Returns a listing of all countries within the platform + :param limit: change the number of results returned. Max is 10000. Default is 100. + :param page: paginate through results. Default is 1. :param df: return the results as a pandas DataFrame + :param index: if returning as a DataFrame, set index to ('utc', 'local', None). The default is local + :type limit: int + :type page: int :type df: boolean + :type index: string - :return: dictionary containing the *code*, *name*, and *count* + :return: dictionary containing the *code*, *name*, *count*, *cities*, and *locations*. :Example: >>> import openaq >>> api = openaq.OpenAQ() >>> status, resp = api.countries() - >>> resp + >>> resp['results'] [ { - "count": 40638, - "code": "AU", - "name": "Australia" + "cities": 174, + "code": "AT", + "count": 121987, + "locations": 174, + "name": "Austria" }, { - "count": 78681, - "code": "BR", - "name": "Brazil", + "cities": 28, + "code": "AU", + "count": 1066179, + "locations": 28, + "name": "Australia", }, ... ] @@ -169,14 +187,26 @@ def latest(self, **kwargs): :param parameter: limit results by a specific parameter. Options include [ pm25, pm10, so2, co, no2, o3, bc] :param has_geo: filter items that do or do not have geographic information. + :param coordinates: center point (`lat`, `long`) used to get measurements within a + certain area. (Ex: coordinates=40.23,34.17) + :param radius: radius (in meters) used to get measurements. Must be used with coordinates. + Default value is 2500. + :param limit: change the number of results returned. Max is 1000. Default is 100. + :param page: paginate through the results. :param df: return results as a pandas DataFrame + :param index: if returning as a DataFrame, set index to ('utc', 'local', None). The default is local :type city: string :type country: string :type location: string :type parameter: string :type has_geo: boolean + :type coordinates: string + :type radius: int + :type limit: int + :type page: int :type df: boolean + :type index: string :return: dictionary containing the *location*, *country*, *city*, and number of *measurements* @@ -185,7 +215,7 @@ def latest(self, **kwargs): >>> import openaq >>> api = openaq.OpenAQ() >>> status, resp = api.latest() - >>> resp + >>> resp['results'] [ { "location": "Punjabi Bagh", @@ -216,29 +246,49 @@ def latest(self, **kwargs): def locations(self, **kwargs): """Provides metadata about distinct measurement locations - :param city: Limit results by a certain city. Defaults to ``None``. - :param country: Limit results by a certain country. Should be a 2-digit - ISO country code. Defaults to ``None``. - :param location: Limit results by a city. Defaults to ``None``. - :param parameter: Limit results by a specific parameter. Options include [ + :param city: Limit results by one or more cities. Defaults to ``None``. Can define as a single city + (ex. city = 'Delhi'), a list of cities (ex. city = ['Delhi', 'Mumbai']), or as a tuple + (ex. city = ('Delhi', 'Mumbai')). + :param country: Limit results by one or more countries. Should be a 2-digit + ISO country code as a string, a list, or a tuple. See `city` for details. + :param location: Limit results by one or more locations. + :param parameter: Limit results by one or more parameters. Options include [ pm25, pm10, so2, co, no2, o3, bc] :param has_geo: Filter items that do or do not have geographic information. + :param coordinates: center point (`lat`, `long`) used to get measurements within a + certain area. (Ex: coordinates=40.23,34.17) + :param nearest: get the X nearest number of locations to `coordinates`. Must be used + with coordinates. Wins over `radius` if both are present. Will add the + `distance` property to locations. + :param radius: radius (in meters) used to get measurements. Must be used with coordinates. + Default value is 2500. + :param limit: change the number of results returned. Max is 1000. Default is 100. + :param page: paginate through the results. + :param df: return results as a pandas DataFrame + :param index: if returning as a DataFrame, set index to ('utc', 'local', None). The default is local - :type city: string - :type country: string - :type location: string - :type parameter: string + :type city: string, array, or tuple + :type country: string, array, or tuple + :type location: string, array, or tuple + :type parameter: string, array, or tuple :type has_geo: boolean + :type coordinates: string + :type nearest: int + :type radius: int + :type limit: int + :type page: int + :type df: boolean + :type index: string - :return: a dictionary containing the *loction*, *country*, *city*, *count*, - *sourceName*, *firstUpdated*, *lastUpdated*, *parameters*, and *coordinates* + :return: a dictionary containing the *location*, *country*, *city*, *count*, + *sourceName*, *sourceNames*, *firstUpdated*, *lastUpdated*, *parameters*, and *coordinates* :Example: >>> import openaq >>> api = openaq.OpenAQ() >>> status, resp = api.locations() - >>> resp + >>> resp['results'] [ { "count": 4242, @@ -264,33 +314,39 @@ def locations(self, **kwargs): @pandasize() def measurements(self, **kwargs): - """Provides metadata about distinct measurement locations + """Provides data about individual measurements :param city: Limit results by a certain city. Defaults to ``None``. :param country: Limit results by a certain country. Should be a 2-digit ISO country code. Defaults to ``None``. :param location: Limit results by a city. Defaults to ``None``. - :param parameter: Limit results by a specific parameter. Options include [ + :param parameter: Limit results by one or more parameters. Options include [ pm25, pm10, so2, co, no2, o3, bc] :param has_geo: Filter items that do or do not have geographic information. - :param value_from: Show results above a value threshold. - :param value_to: Show results below a value threshold. - :param date_from: Show results after a certain date. Format should be ``Y-M-D`` - :param date_to: Show results before a certain date. Format should be ``Y-M-D`` - :param sort: The sort order (``asc`` or ``desc``). + :param coordinates: center point (`lat`, `long`) used to get measurements within a + certain area. (Ex: coordinates=40.23,34.17) + :param radius: radius (in meters) used to get measurements. Must be used with `coordinates`. + Default value is 2500. + :param value_from: Show results above a value threshold. Must be used with `parameter`. + :param value_to: Show results below a value threshold. Must be used with `parameter`. + :param date_from: Show results after a certain date. Format should be ``Y-M-D``. + :param date_to: Show results before a certain date. Format should be ``Y-M-D``. + :param sort: The sort order (``asc`` or ``desc``). Must be used with `order_by`. :param order_by: Field to sort by. Must be used with **sort**. - :param include_fields: Include additional fields in the output. + :param include_fields: Include additional fields in the output. Allowed values are: *attribution*, + *averagingPeriod*, and *sourceName*. :param limit: Change the number of results returned. :param page: Paginate through the results - :param skip: Number of records to skip. :param df: return the results as a pandas DataFrame - :param index: if returning as a DataFrame, set index to ('utc', 'local'). The default is local + :param index: if returning as a DataFrame, set index to ('utc', 'local', None). The default is local :type city: string :type country: string :type location: string - :type parameter: string + :type parameter: string, array, or tuple :type has_geo: boolean + :type coordinates: string + :type radius: int :type value_from: number :type value_to: number :type date_from: date @@ -300,19 +356,18 @@ def measurements(self, **kwargs): :type include_fields: array :type limit: number :type page: number - :type skip: number :type df: boolean :type index: string :return: a dictionary containing the *date*, *parameter*, *value*, *unit*, - *location*, *country*, *city*, and *coordinates*. + *location*, *country*, *city*, *coordinates*, and *sourceName*. :Example: >>> import openaq >>> api = openaq.OpenAQ() >>> status, resp = api.measurements(city = 'Delhi') - >>> resp + >>> resp['results'] { "parameter": "Ammonia", "date": { @@ -328,6 +383,13 @@ def measurements(self, **kwargs): "latitude": 43.34, "longitude": 23.04 }, + "attribution": { + "name": "SINCA", + "url": "http://sinca.mma.gob.cl/" + }, + { + "name": "Ministerio del Medio Ambiente" + } ... } """ @@ -337,6 +399,12 @@ def fetches(self, **kwargs): """Provides data about individual fetch operations that are used to populate data in the platform. + :param limit: change the number of results returned. Max is 10000. Default is 100. + :param page: paginate through the results. Default is 1. + + :type limit: int + :type page: int + :return: dictionary containing the *timeStarted*, *timeEnded*, *count*, and *results* :Example: @@ -352,7 +420,8 @@ def fetches(self, **kwargs): "website": "page": 1, "limit": 100, - "found": 3 + "found": 3, + "pages": 1 }, "results": [ { @@ -382,5 +451,74 @@ def fetches(self, **kwargs): """ return self._get('fetches', **kwargs) + def parameters(self): + """ + Provides a simple listing of parameters within the platform. + + :return: a dictionary containing the *id*, *name*, *description*, and + *preferredUnit*. + + :Example: + + >>> import openaq + >>> api = openaq.OpenAQ() + >>> status, resp = api.parameters() + >>> resp['results'] + [ + { + "id": "pm25", + "name": "PM2.5", + "description": "Particulate matter less than 2.5 micrometers in diameter", + "preferredUnit": "µg/m³" + } + ... + ] + """ + return self._get('parameters') + + @pandasize() + def sources(self, **kwargs): + """ + Provides a list of data sources. + + :param limit: Change the number of results returned. + :param page: Paginate through the results + :param df: return the results as a pandas DataFrame + :param index: if returning as a DataFrame, set index to ('utc', 'local', None). The default is local + + :type limit: number + :type page: number + :type df: boolean + :type index: string + + :return: a dictionary containing the *url*, *adapter*, *name*, *city*, + *country*, *description*, *resolution*, *sourceURL*, and *contacts*. + + :Example: + + >>> import openaq + >>> api = openaq.OpenAQ() + >>> status, resp = api.sources() + >>> resp['results'] + [ + { + "url": "http://airquality.environment.nsw.gov.au/aquisnetnswphp/getPage.php?reportid=2", + "adapter": "nsw", + "name": "Australia - New South Wales", + "city": "", + "country": "AU", + "description": "Measurements from the Office of Environment & Heritage of the New South Wales government.", + "resolution": "1 hr", + "sourceURL": "http://www.environment.nsw.gov.au/AQMS/hourlydata.htm", + "contacts": [ + "olaf@developmentseed.org" + ] + } + ... + ] + """ + + return self._get('sources', **kwargs) + def __repr__(self): return "OpenAQ API" diff --git a/tests/openaq_test.py b/tests/openaq_test.py index fe4f058..25c297b 100644 --- a/tests/openaq_test.py +++ b/tests/openaq_test.py @@ -47,16 +47,33 @@ def test_locations(self): self.assertTrue(status == 200) + def test_locations_with_params(self): + # Test cities as a list + status, resp = self.api.locations( + city = ['Delhi', 'Mumbai'] + ) + + for r in resp['results']: + self.assertTrue(r['city'] in ['Delhi', 'Mumbai']) + + # Test cities as a tuple + status, resp = self.api.locations( + city = ('Delhi', 'Mumbai') + ) + + for r in resp['results']: + self.assertTrue(r['city'] in ['Delhi', 'Mumbai']) + def test_measurements(self): status, resp = self.api.measurements(city = 'Delhi') self.assertTrue(status == 200) - def test_measurements_with_params(self): - status, resp = self.api.measurements(include_fields = ['location', 'parameter', - 'date', 'value']) + #def test_measurements_with_params(self): + # status, resp = self.api.measurements(include_fields = ['location', 'parameter', + # 'date', 'value']) - self.assertTrue(status == 200) + # self.assertTrue(status == 200) def test_pandasize(self): resp = self.api.latest(df = True) @@ -84,6 +101,16 @@ def test_bad_request(self): self.assertRaises(openaq.exceptions.ApiError) + def test_parameters(self): + status, resp = self.api.parameters() + + self.assertIsNotNone(resp['results']) + + def test_sources(self): + status, resp = self.api.sources(limit = 1) + + self.assertIsNotNone(resp['results']) + def test_repr(self): self.assertTrue(str(self.api) == 'OpenAQ API') From 88311d22d99ff2ce1685bcc935b2b2ee226571dc Mon Sep 17 00:00:00 2001 From: David H Hagan Date: Fri, 23 Dec 2016 14:44:21 -0800 Subject: [PATCH 5/6] added tsplot --- docs/index.rst | 24 ++++---- openaq/__init__.py | 5 +- openaq/decorators.py | 23 ++++++-- openaq/viz.py | 131 +++++++++++++++++++++++++++++++++++++++++++ tests/test_viz.py | 110 ++++++++++++++++++++++++++++++++++++ 5 files changed, 274 insertions(+), 19 deletions(-) create mode 100644 tests/test_viz.py diff --git a/docs/index.rst b/docs/index.rst index 6b26341..d204ed3 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -15,7 +15,7 @@ You can install this package in the usual way using ``pip``:: pip install py-openaq -You can upgrade with: +You can upgrade this package using ``pip`` as well:: pip install py-openaq --upgrade @@ -23,10 +23,10 @@ You can upgrade with: Requirements ------------ -The only requirement for this package is ``requests``. In the future (v1), -``pandas`` will be recommended and significant features will depend on it. - -**UPDATE**: As of v0.3.0, `pandas` support has been added, but is not a requirement. +The only requirement for this package is ``requests``. If you are not limited +by memory or space, I would highly recommend installing ``pandas`` and ``seaborn`` +which will enable you to use the new visualization helpers that were released with +version 1. Current Limitations ------------------- @@ -72,20 +72,17 @@ The json response will look something like the following with both ``meta`` and Coupling with Pandas DataFrame ------------------------------ -Pandas is awesome. If you are working with data, you should be using DataFrames. To -easily dump your json response into a DataFrame:: - - from pandas.io.json import json_normalize - - df = json_normalize(resp) +The `pandasize` decorator was added to easily allow you to read in data directly +to a DataFrame. To do so, simply add the argument `df = True` to your request. -As of v0.3.0, an optional keyword argument (`df`) has been added to the following API methods: +The following API methods allow you to return your data as a DataFrame: * cities * countries * latest * locations * measurements + * sources By using this keyword argument, the results of the API call will return a pandas DataFrame rather than a json response. @@ -101,4 +98,5 @@ API Reference .. module:: openaq .. autoclass:: OpenAQ - :members: cities, countries, latest, locations, measurements, fetches + :members: cities, countries, latest, locations, measurements, fetches, + parameters, sources diff --git a/openaq/__init__.py b/openaq/__init__.py index eb1ed22..142d78a 100644 --- a/openaq/__init__.py +++ b/openaq/__init__.py @@ -38,7 +38,8 @@ def _make_url(self, endpoint, **kwargs): #value = ','.join(value) for v in value: extra.append("{}={}".format(key, v)) - #extra.append("{}={}".format(key, value)) + else: + extra.append("{}={}".format(key, value)) if len(extra) > 0: endpoint = '?'.join([endpoint, '&'.join(extra)]) @@ -469,7 +470,7 @@ def parameters(self): "id": "pm25", "name": "PM2.5", "description": "Particulate matter less than 2.5 micrometers in diameter", - "preferredUnit": "µg/m³" + "preferredUnit": "ug/m3" } ... ] diff --git a/openaq/decorators.py b/openaq/decorators.py index 223a363..5690eec 100644 --- a/openaq/decorators.py +++ b/openaq/decorators.py @@ -1,11 +1,26 @@ from functools import wraps +import warnings +from unittest import SkipTest try: import pandas as pd - has_pandas = True -except: - has_pandas = False + _no_pandas = False +except ImportError: + _no_pandas = True + +def skipif(skipcondition, msg = ""): + """ + """ + def decorator(f): + @wraps(f) + def decorated_function(*args, **kwargs): + if skipcondition == True: + raise SkipTest(msg) + + return f(*args, **kwargs) + return decorated_function + return decorator def pandasize(): def decorator(f): @@ -14,7 +29,7 @@ def decorated_function( *args, **kwargs ): df = kwargs.get('df', False) index = kwargs.get('index', 'local') - if df == True and has_pandas == True: + if df == True and _no_pandas == False: status, resp = f( *args, **kwargs ) if status == 200: resp = resp['results'] diff --git a/openaq/viz.py b/openaq/viz.py index e69de29..2fd8660 100644 --- a/openaq/viz.py +++ b/openaq/viz.py @@ -0,0 +1,131 @@ +from .decorators import skipif + +try: + import pandas as pd + + _no_pandas = False +except ImportError: + _no_pandas = True + +try: + import seaborn as sns + import matplotlib.pyplot as plt + import matplotlib.dates as dates + + _no_sns = False +except ImportError: + _no_sns = True + +def tsindex(ax): + """ + Reset the axis parameters to look nice! + """ + # Get dt in days + dt = ax.get_xlim()[-1] - ax.get_xlim()[0] + + if dt <= 1./24.: # less than one hour + pass + elif dt <= 1.: # less than one day + ax.xaxis.set_minor_locator( dates.HourLocator() ) + ax.xaxis.set_minor_formatter( dates.DateFormatter("")) + + ax.xaxis.set_major_locator( dates.HourLocator( interval = 3)) + ax.xaxis.set_major_formatter( dates.DateFormatter("%-I %p")) + elif dt <= 7.: # less than one week + ax.xaxis.set_minor_locator( dates.DayLocator()) + ax.xaxis.set_minor_formatter( dates.DateFormatter("%d")) + + ax.xaxis.set_major_locator( dates.DayLocator( bymonthday = [1, 8, 15, 22]) ) + ax.xaxis.set_major_formatter( dates.DateFormatter("\n%b\n%Y") ) + elif dt <= 14.: # less than two weeks + ax.xaxis.set_minor_locator( dates.DayLocator()) + ax.xaxis.set_minor_formatter( dates.DateFormatter("%d")) + + ax.xaxis.set_major_locator( dates.DayLocator( bymonthday = [1, 15]) ) + ax.xaxis.set_major_formatter( dates.DateFormatter("\n%b\n%Y") ) + elif dt <= 28.: # less than four weeks + ax.xaxis.set_minor_locator( dates.DayLocator()) + ax.xaxis.set_minor_formatter( dates.DateFormatter("%d")) + + ax.xaxis.set_major_locator( dates.MonthLocator() ) + ax.xaxis.set_major_formatter( dates.DateFormatter("\n%b\n%Y") ) + elif dt <= 4 * 30.: # less than four months + ax.xaxis.set_minor_locator( dates.DayLocator( bymonthday = [1, 7, 14, 21] )) + ax.xaxis.set_minor_formatter( dates.DateFormatter("%d")) + + ax.xaxis.set_major_locator( dates.MonthLocator()) + ax.xaxis.set_major_formatter( dates.DateFormatter("\n%b\n%Y") ) + else: + ax.xaxis.set_minor_locator( dates.MonthLocator(interval = 2) ) + ax.xaxis.set_minor_formatter( dates.DateFormatter("%b")) + + ax.xaxis.set_major_locator( dates.MonthLocator(bymonth = [1]) ) + ax.xaxis.set_major_formatter( dates.DateFormatter("\n%Y")) + + + return ax + +rc_tsplot = { + 'xtick.major.size': 8., + 'xtick.minor.size': 4., + 'xtick.direction': 'out', + 'ytick.major.size': 10., +} + +@skipif(_no_sns) +def tsplot(data, time = None, ax = None, parameter = None, rs = '1h', + locations = None, plot_kws = {}, fmt_axis = True, **kwargs): + """ + If there are multiple locations && multiple params, issue a warning! + + :param data: dataframe with data + :param time: column name with time. Defaults to index. + :param ax: Plot on ax if you would like. + :param parameter: string with parameter to plot. Can only plot 1 at a time. + + """ + assert isinstance(data, pd.DataFrame), "`data` must be a pandas dataframe" + assert parameter in [None, 'pm25', 'pm10', 'o3', 'no2', 'bc', 'co', 'so2'], "Invalid parameter" + + if "figsize" not in plot_kws.keys(): + plot_kws['figsize'] = (14, 7) + + if type(locations) is not list: + locations = [locations] + + if ax is None: + ax = plt.gca() + + # Deal with ts index issues + if data.index.name is not time: + try: + data.index = data[time] + except: + if not isinstance(data.index, pd.tseries.index.DatetimeIndex): + data.index = data['date.local'] + + data.index.name = 'index' + + # If parameter is not defined, use the parameter of the first row + if parameter is None: + parameter = data.parameter[0] + + data = data[data['parameter'] == parameter] + + # Iterate over locations! + for name, group in data.groupby('location'): + if name in locations or locations[0] is None: + _y = group.resample(rs).mean() + + ax.plot(_y.value, label = name) + + ax.set_ylabel(parameter) + ax.legend( loc = 'best' ) + + # Set the axes limits + ax.set_ylim([0, ax.get_ylim()[-1]]) + + if fmt_axis: + ax = tsindex(ax) + + return ax diff --git a/tests/test_viz.py b/tests/test_viz.py new file mode 100644 index 0000000..fb66d0d --- /dev/null +++ b/tests/test_viz.py @@ -0,0 +1,110 @@ +import unittest +import openaq +import pandas as pd +from openaq.viz import * + +class SetupTestCase(unittest.TestCase): + def setUp(self): + self.api = openaq.OpenAQ() + + def tearDown(self): + pass + + def test_setup(self): + self.assertIsInstance(self.api, openaq.OpenAQ) + + def test_tsplot_1hr(self): + data = self.api.measurements( + city = 'Delhi', + parameter = 'pm25', + location = 'Anand Vihar', + limit = 5, + df = True) + + a = tsplot(data) + + def test_tsplot_24hr(self): + data = self.api.measurements( + country = 'IN', + city = 'Delhi', + location = 'Anand Vihar', + parameter = 'pm25', + limit = 20, + index = 'local', + df = True) + + a = tsplot(data) + + def test_tsplot_1wk(self): + data = self.api.measurements( + country = 'IN', + city = 'Delhi', + location = 'Anand Vihar', + parameter = 'pm25', + limit = 100, + index = 'local', + df = True) + + a = tsplot(data) + + def test_tsplot_2wk(self): + data = self.api.measurements( + country = 'IN', + city = 'Delhi', + location = 'Anand Vihar', + parameter = 'pm25', + limit = 400, + index = 'local', + df = True) + + a = tsplot(data) + + def test_tsplot_4wk(self): + data = self.api.measurements( + country = 'IN', + city = 'Delhi', + location = 'Anand Vihar', + parameter = 'pm25', + limit = 600, + index = 'local', + df = True) + + a = tsplot(data) + + def test_tsplot_1month(self): + data = self.api.measurements( + country = 'IN', + city = 'Delhi', + location = 'Anand Vihar', + parameter = 'pm25', + limit = 900, + index = 'local', + df = True) + + a = tsplot(data) + + def test_tsplot_1yr(self): + data = self.api.measurements( + country = 'IN', + city = 'Delhi', + location = 'Anand Vihar', + parameter = 'pm25', + limit = 1500, + index = 'local', + df = True) + + a = tsplot(data) + + self.assertIsNotNone(a) + + def test_tsplot_1yr(self): + data = self.api.measurements( + country = 'IN', + city = 'Delhi', + location = 'Anand Vihar', + parameter = 'pm25', + limit = 10000, + index = 'local', + df = True) + + a = tsplot(data) From f4d9acb1571a290c9a4d49ee72d40a0a44490d5a Mon Sep 17 00:00:00 2001 From: David H Hagan Date: Fri, 23 Dec 2016 15:07:19 -0800 Subject: [PATCH 6/6] added simple docs for tsplot --- docs/index.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/index.rst b/docs/index.rst index d204ed3..1ee35c0 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -100,3 +100,8 @@ API Reference .. autoclass:: OpenAQ :members: cities, countries, latest, locations, measurements, fetches, parameters, sources + +Visualization Reference +======================= +.. module:: openaq.viz +.. autofunction:: tsplot