diff --git a/.github/.keep b/.github/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml new file mode 100644 index 0000000..0d7b732 --- /dev/null +++ b/.github/workflows/python-app.yml @@ -0,0 +1,33 @@ +# This workflow will install Python dependencies, run tests and lint with a single version of Python +# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions + +name: Python application + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Set up Python 3.9 + uses: actions/setup-python@v2 + with: + python-version: "3.9" + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install -r requirements.txt + - name: Lint with flake8 + run: | + # stop the build if there are Python syntax errors or undefined names + python -m flake8 track tests + - name: Test with pytest + run: | + python -m pytest -v tests diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fc95a5e --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +.hypothesis +.pytest_cache +__pycache__ +venv + +# news/changed (on disk, untracked for new files) +# git add -> (staging area) +# git commit -> (history) +# git push -> (history on server) \ No newline at end of file diff --git a/prod-requirements.txt b/prod-requirements.txt new file mode 100644 index 0000000..e398298 --- /dev/null +++ b/prod-requirements.txt @@ -0,0 +1,5 @@ +# Production requirements + +matplotlib==3.5.1 +numpy==1.22.0 +pandas==1.3.5 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..7d50c2c --- /dev/null +++ b/requirements.txt @@ -0,0 +1,17 @@ +# Development requirements + +-r prod-requirements.txt + +flake8==4.0.1 +flake8-bugbear==22.1.11 +hypothesis==6.36.0 +pandera==0.8.1 +pytest==6.2.5 +PyYAML==6.0 + +# To check for python, run in console +# python -m pytest --version + +# From terminal +# python3.9 -m venv venv +# ./venv/bin/python -m pip install -r requirements.txt diff --git a/tests/dist_cases.yml b/tests/dist_cases.yml new file mode 100644 index 0000000..c3d7039 --- /dev/null +++ b/tests/dist_cases.yml @@ -0,0 +1,12 @@ +- lat1: 0 + lng1: 0 + lat2: 1 + lng2: 1 + distance: 144.1700384962146 +# checking same location +- lat1: 0 + lng1: 0 + lat2: 0 + lng2: 0 + distance: 0 + name: "same location" diff --git a/tests/test_track.py b/tests/test_track.py new file mode 100644 index 0000000..83955ee --- /dev/null +++ b/tests/test_track.py @@ -0,0 +1,95 @@ +import math +from pathlib import Path + +import yaml +from hypothesis import given +from hypothesis.strategies import floats + +import track +import pytest +from track.schema import track_schema + + +def test_smoke(): + pass + + +def test_distance(): + dist = track.distance(0, 0, 1, 1) + assert 144.1700384962146 == dist + + +distance_cases = [ + # coord1, coord2, distance + [(0, 0), (1, 1), 144.1700384962146], + # [(1, 1), (1, 1), 0], + pytest.param((1, 1), (1, 1), 0, id='same location'), + # ... +] + +# Exercise: Instead of reading tests from distance_cases, read then from +# dist_cases.yml +# You'll need to install PyYAML to read YAML files +# import yaml +# with open('tests/dist_cases.yml') as fp: +# data = yaml.safe_load(fp) # data is a list of dicts + +tests_dir = Path(__file__).absolute().parent + + +def load_dist_cases(): + with open(tests_dir / 'dist_cases.yml') as fp: + data = yaml.safe_load(fp) + + cases = [] + for case in data: + coord1 = (case['lat1'], case['lng1']) + coord2 = (case['lat2'], case['lng2']) + cases.append([coord1, coord2, case['distance']]) + return cases + + +@pytest.mark.parametrize('coord1, coord2, expected', load_dist_cases()) +def test_distance_many(coord1, coord2, expected): + lat1, lng1 = coord1 + lat2, lng2 = coord2 + dist = track.distance(lat1, lng1, lat2, lng2) + assert expected == dist + + +def test_distance_type_error(): + with pytest.raises(TypeError): + track.distance('1', 1, 0, 0) + + # try: + # track.distance('1', 1, 0, 0) + # assert False, 'did not raise' + # except TypeError: + # pass + + +# from hypothesis import given +# from hypothesis.strategies import floats + +@given(floats(), floats(), floats(), floats()) +def test_distance_fuzz(lat1, lng1, lat2, lng2): + # print(lat1, lng1, lat2, lng2) + # run: python -m pytest -s + dist = track.distance(lat1, lng1, lat2, lng2) + if math.isnan(dist): + return + assert dist >= 0 + + +# from schema import track_schema +def test_load_track(): + csv_file = tests_dir / 'track.csv' + df = track.load_track(csv_file) + track_schema.validate(df) + + +""" +Running tests: +- python -m flake8 lib.py tests +- python -m pytest -v +""" diff --git a/track.csv b/tests/track.csv similarity index 100% rename from track.csv rename to tests/track.csv diff --git a/track.ipynb b/track.ipynb deleted file mode 100644 index b66605d..0000000 --- a/track.ipynb +++ /dev/null @@ -1,334 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "id": "9a86be15-068f-4e79-8c09-042e8d7ea8b0", - "metadata": {}, - "outputs": [], - "source": [ - "import pandas as pd" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "b1d2c42f-70fb-4258-9221-4bca2a768d6e", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "time datetime64[ns, UTC]\n", - "lat float64\n", - "lng float64\n", - "height float64\n", - "dtype: object" - ] - }, - "execution_count": 2, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "df = pd.read_csv('track.csv', parse_dates=['time'])\n", - "df.dtypes" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "bd693dea-cfc0-495b-afbd-e868cb874053", - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
timelatlngheight
182015-08-20 03:48:54.662000+00:0032.51918735.014365122.599998
232015-08-20 03:49:07.742000+00:0032.51891635.014340123.099998
2372015-08-20 03:59:22.869000+00:0032.51018435.011083100.599998
3162015-08-20 04:03:01.834000+00:0032.50815435.01429279.800003
3732015-08-20 04:05:18.810000+00:0032.50872735.01767984.300003
4542015-08-20 04:08:36.758000+00:0032.50999635.02151691.099998
4612015-08-20 04:08:59.841000+00:0032.51033435.02137697.900002
6232015-08-20 04:16:15.900000+00:0032.51299235.016803101.500000
6292015-08-20 04:16:31.960000+00:0032.51322135.017020106.599998
6502015-08-20 04:17:20.855000+00:0032.51389735.017175113.900002
\n", - "
" - ], - "text/plain": [ - " time lat lng height\n", - "18 2015-08-20 03:48:54.662000+00:00 32.519187 35.014365 122.599998\n", - "23 2015-08-20 03:49:07.742000+00:00 32.518916 35.014340 123.099998\n", - "237 2015-08-20 03:59:22.869000+00:00 32.510184 35.011083 100.599998\n", - "316 2015-08-20 04:03:01.834000+00:00 32.508154 35.014292 79.800003\n", - "373 2015-08-20 04:05:18.810000+00:00 32.508727 35.017679 84.300003\n", - "454 2015-08-20 04:08:36.758000+00:00 32.509996 35.021516 91.099998\n", - "461 2015-08-20 04:08:59.841000+00:00 32.510334 35.021376 97.900002\n", - "623 2015-08-20 04:16:15.900000+00:00 32.512992 35.016803 101.500000\n", - "629 2015-08-20 04:16:31.960000+00:00 32.513221 35.017020 106.599998\n", - "650 2015-08-20 04:17:20.855000+00:00 32.513897 35.017175 113.900002" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "df.sample(10).sort_index()" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "8f72b35c-ca7c-41ea-b0a9-f21397a76405", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "0 NaT\n", - "1 0 days 00:00:17.499000\n", - "2 0 days 00:00:00.926000\n", - "3 0 days 00:00:01.159000\n", - "4 0 days 00:00:01.009000\n", - "Name: time, dtype: timedelta64[ns]" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "time = df['time'].diff()\n", - "time.head()" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "4b2b73fa-7088-4dfd-800e-5bc613a6428a", - "metadata": {}, - "outputs": [], - "source": [ - "import numpy as np\n", - "\n", - "lat_km = 92\n", - "lng_km = 111\n", - "\n", - "def distance(lat1, lng1, lat2, lng2):\n", - " delta_lat = (lat1 - lat2) * lat_km\n", - " delta_lng = (lng1 - lng2) * lng_km\n", - " return np.hypot(delta_lat, delta_lng)" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "7a834256-f239-49aa-87c8-f29088937a94", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "0 NaN\n", - "1 0.007684\n", - "2 0.009230\n", - "3 0.006492\n", - "4 0.006225\n", - "dtype: float64" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "dist_km = distance(\n", - " df['lat'], df['lng'],\n", - " df['lat'].shift(), df['lng'].shift(),\n", - ")\n", - "dist_km.head()" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "d6e5526e-2dc5-4beb-b06f-9fdad8ce79b7", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "4.693669332948701" - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "dist_km.sum()" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "8298d052-624d-444b-b862-6721c2de35a1", - "metadata": {}, - "outputs": [], - "source": [ - "time_hours = time / pd.Timedelta(hours=1)\n", - "speed_kmh = dist_km / time_hours" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "6f1a9779-2a12-4960-a9bf-d8981cc771aa", - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "import matplotlib.pyplot as plt\n", - "plt.style.use('seaborn-whitegrid')\n", - "\n", - "date = df['time'][0].strftime('%a, %b %d %Y')\n", - "ax = speed_kmh.plot.box(title=f'{date} Run')\n", - "ax.set_xticks([]) # Remove \"None\"\n", - "ax.set_ylabel(r'Running speed $\\frac{km}{h}$');" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "edc6c62f-ccd9-4b62-9ead-d8c19fc78c82", - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.1" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/track/__init__.py b/track/__init__.py new file mode 100644 index 0000000..c5c1e46 --- /dev/null +++ b/track/__init__.py @@ -0,0 +1,3 @@ +"""track is a library for working with jogging log information""" + +from .lib import load_track, running_speed, plot_speed, distance # noqa \ No newline at end of file diff --git a/track/__main__.py b/track/__main__.py new file mode 100644 index 0000000..b719ba1 --- /dev/null +++ b/track/__main__.py @@ -0,0 +1,26 @@ +from track import running_speed, load_track, plot_speed +import logging + +# __name__ -> dunder name +if __name__ == '__main__': + from argparse import ArgumentParser, FileType + + logging.basicConfig( + level='INFO', + format='%(asctime)s - %(levelname)s - %(message)s', + datefmt='%Y-%m-%d:%H:%M:%S', + ) + + parser = ArgumentParser(description='Generate box plot of running speed') + parser.add_argument('csv_file', help='CSV file', type=FileType('r')) + parser.add_argument('out_file', help='Output file', type=FileType('w')) + args = parser.parse_args() + + df = load_track(args.csv_file.name) + speed = running_speed(df) + ax = plot_speed('2022-01-23', speed) + ax.figure.savefig(args.out_file.name) + + +# Example: +# python -m track tests/track.csv /tmp/track.png diff --git a/track/lib.py b/track/lib.py new file mode 100644 index 0000000..651f7d1 --- /dev/null +++ b/track/lib.py @@ -0,0 +1,50 @@ +import numpy as np +import pandas as pd +from pandera import check_input, check_output +import logging + +# relative import +from .schema import track_schema + +lat_km = 92 +lng_km = 111 + + +@check_output(track_schema) +def load_track(file_name): + logging.info('loading %s', file_name) + # The below will create a new string even if INFO level not enabled + # logging.info(f'loading {file_name}') + df = pd.read_csv(file_name, parse_dates=['time']) + logging.info('%s loaded %d rows', file_name, len(df)) + return df + + +# type hints +def distance(lat1: float, lng1: float, lat2: float, lng2: float) -> float: + """Return Euclidean distance (in kilometers) between two coordinates) + + >>> distance(0, 0, 1, 1) + 144.1700384962146 + """ + delta_lat = (lat1 - lat2) * lat_km + delta_lng = (lng1 - lng2) * lng_km + return np.hypot(delta_lat, delta_lng) + + +@check_input(track_schema) +def running_speed(df): + dist_km = distance( + df['lat'], df['lng'], + df['lat'].shift(), df['lng'].shift(), + ) + + time_hours = df['time'].diff() / pd.Timedelta(hours=1) + return dist_km / time_hours + + +def plot_speed(date, speed_kmh): + ax = speed_kmh.plot.box(title=f'{date} Run') + ax.set_xticks([]) # Remove "None" + ax.set_ylabel(r'Running speed $\frac{km}{h}$') + return ax diff --git a/track/schema.py b/track/schema.py new file mode 100644 index 0000000..f358819 --- /dev/null +++ b/track/schema.py @@ -0,0 +1,11 @@ +"""Define common schema for data""" + +import pandera as pa +import pandas as pd + +track_schema = pa.DataFrameSchema({ + 'time': pa.Column(pd.DatetimeTZDtype('ns', 'UTC')), + 'lat': pa.Column(float, checks=pa.Check.in_range(-90, 90)), + 'lng': pa.Column(float, checks=pa.Check.in_range(-180, 180)), + 'height': pa.Column(float, checks=pa.Check.in_range(-422, 8849)), +})