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",
- " time | \n",
- " lat | \n",
- " lng | \n",
- " height | \n",
- "
\n",
- " \n",
- " \n",
- " \n",
- " | 18 | \n",
- " 2015-08-20 03:48:54.662000+00:00 | \n",
- " 32.519187 | \n",
- " 35.014365 | \n",
- " 122.599998 | \n",
- "
\n",
- " \n",
- " | 23 | \n",
- " 2015-08-20 03:49:07.742000+00:00 | \n",
- " 32.518916 | \n",
- " 35.014340 | \n",
- " 123.099998 | \n",
- "
\n",
- " \n",
- " | 237 | \n",
- " 2015-08-20 03:59:22.869000+00:00 | \n",
- " 32.510184 | \n",
- " 35.011083 | \n",
- " 100.599998 | \n",
- "
\n",
- " \n",
- " | 316 | \n",
- " 2015-08-20 04:03:01.834000+00:00 | \n",
- " 32.508154 | \n",
- " 35.014292 | \n",
- " 79.800003 | \n",
- "
\n",
- " \n",
- " | 373 | \n",
- " 2015-08-20 04:05:18.810000+00:00 | \n",
- " 32.508727 | \n",
- " 35.017679 | \n",
- " 84.300003 | \n",
- "
\n",
- " \n",
- " | 454 | \n",
- " 2015-08-20 04:08:36.758000+00:00 | \n",
- " 32.509996 | \n",
- " 35.021516 | \n",
- " 91.099998 | \n",
- "
\n",
- " \n",
- " | 461 | \n",
- " 2015-08-20 04:08:59.841000+00:00 | \n",
- " 32.510334 | \n",
- " 35.021376 | \n",
- " 97.900002 | \n",
- "
\n",
- " \n",
- " | 623 | \n",
- " 2015-08-20 04:16:15.900000+00:00 | \n",
- " 32.512992 | \n",
- " 35.016803 | \n",
- " 101.500000 | \n",
- "
\n",
- " \n",
- " | 629 | \n",
- " 2015-08-20 04:16:31.960000+00:00 | \n",
- " 32.513221 | \n",
- " 35.017020 | \n",
- " 106.599998 | \n",
- "
\n",
- " \n",
- " | 650 | \n",
- " 2015-08-20 04:17:20.855000+00:00 | \n",
- " 32.513897 | \n",
- " 35.017175 | \n",
- " 113.900002 | \n",
- "
\n",
- " \n",
- "
\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)),
+})