commit 5abeb4fd486b4ed8661a215219a2d7374059b052 Author: [Quy Anh] «Elliot» Nguyen Date: Wed Jun 24 16:52:08 2026 +0200 --- diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4eb9f5a --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +backend/app/__pycache__ +backend/datasets/* +backend/deploy.sh +backend/env +backend/.env.production +backend/project/__pycache__ + +frontend/.env diff --git a/Architecture Diagram.drawio b/Architecture Diagram.drawio new file mode 100644 index 0000000..eceb039 --- /dev/null +++ b/Architecture Diagram.drawio @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..00d2e13 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,24 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to \ No newline at end of file diff --git a/Mini-Project Canvas template.docx b/Mini-Project Canvas template.docx new file mode 100644 index 0000000..6f5cd5b Binary files /dev/null and b/Mini-Project Canvas template.docx differ diff --git a/README.md b/README.md new file mode 100644 index 0000000..7985339 --- /dev/null +++ b/README.md @@ -0,0 +1,25 @@ +# HSL Bike Helper [![Unlicense](https://img.shields.io/badge/License-Unlicense-2ea44f)](https://github.com/ElliotAtHelsinki/data-science-project/blob/main/LICENSE.md) ![v - 1.0.0](https://img.shields.io/badge/v-1.0.0-blue) ![PRs Welcome](https://img.shields.io/badge/PRs-welcome-green.svg) + +HSL Bike Helper is an app that predicts the number of bikes at stations in [`Helsinki`](https://hel.fi/) and [`Espoo`](https://espoo.fi/) at any hour using the [`SARIMA`](https://en.wikipedia.org/wiki/Autoregressive_integrated_moving_average) model. + +### Repository +https://gitea.elliot-at-zuri.ch/root/DATA11001-Introduction-to-Data-Science + +### Installation +The application can be directly accessed via a web browser at the following addresses, without requiring any installation: +\- **Frontend**: https://hsl-frontend.elliot-at-zuri.ch +\- **Backend**: https://hsl-backend.elliot-at-zuri.ch/app/predict + +Optionally, however, the application can be installed as a mobile or desktop app, since it is a PWA. + +### Development +To run the `Django` backend locally, navigate to the `backend` folder and: +\- Source the `env`: `source env/bin/activate` +\- Create an empty `.env` file: `touch .env` +\- Install the dependencies: `pip install -r requirements.txt` +\- Run the first two code blocks of the `main.ipynb` file +\- Start the server: `python manage.py runserver` + +To run the `Next.js` frontend locally, navigate to the `frontend` folder and: +\- Install the dependencies: `npm install` +\- Start the application: `npm run dev` diff --git a/Technical Report.pdf b/Technical Report.pdf new file mode 100644 index 0000000..cfa99c7 Binary files /dev/null and b/Technical Report.pdf differ diff --git a/backend/.dockerignore b/backend/.dockerignore new file mode 100644 index 0000000..8be1bf6 --- /dev/null +++ b/backend/.dockerignore @@ -0,0 +1,6 @@ +env +datasets/20* +datasets/full_bike_data.csv +main.ipynb +deploy.sh +.env diff --git a/backend/.env b/backend/.env new file mode 100644 index 0000000..e69de29 diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..88417f5 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,14 @@ +FROM python:3.13.0-bookworm + +WORKDIR /usr/src/app + +COPY . . +COPY .env.production .env + +RUN pip install -r requirements.txt + +EXPOSE 9000 + +ENV DJANGO_ENV=production + +CMD ["python", "manage.py", "runserver", "0.0.0.0:9000", "--noreload"] diff --git a/backend/README.md b/backend/README.md new file mode 100644 index 0000000..3627456 --- /dev/null +++ b/backend/README.md @@ -0,0 +1 @@ +## This is the Django backend for our Data Science project. diff --git a/backend/app/__init__.py b/backend/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/admin.py b/backend/app/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/backend/app/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/backend/app/apps.py b/backend/app/apps.py new file mode 100644 index 0000000..ed327d2 --- /dev/null +++ b/backend/app/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class AppConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'app' diff --git a/backend/app/migrations/__init__.py b/backend/app/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/models.py b/backend/app/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/backend/app/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/backend/app/tests.py b/backend/app/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/backend/app/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/backend/app/urls.py b/backend/app/urls.py new file mode 100644 index 0000000..453fe15 --- /dev/null +++ b/backend/app/urls.py @@ -0,0 +1,8 @@ +from django.urls import path + +from . import views + +urlpatterns = [ + path('', views.index, name='index'), + path('predict', views.predict, name='predict'), +] diff --git a/backend/app/views.py b/backend/app/views.py new file mode 100644 index 0000000..759872d --- /dev/null +++ b/backend/app/views.py @@ -0,0 +1,80 @@ +from django.http import HttpResponse, JsonResponse +from datetime import datetime +import pandas as pd +import statsmodels.api as sm + +stations = ['Kamppi (M)', 'Rautatientori - itä'] +station_dict = {} + +for station in stations: + departure_data = pd.read_csv('datasets/' + station + '_hourly_aggregate.csv') + return_data = pd.read_csv('datasets/' + station + '_return_hourly_aggregate.csv') + departure_data['Departure'] = pd.to_datetime(departure_data['Departure'], format='mixed') + return_data['Return'] = pd.to_datetime(return_data['Return'], format='mixed') + + departure_data.set_index(departure_data['Departure'], inplace=True) + return_data.set_index(return_data['Return'], inplace=True) + + departure_data['trip'] = pd.to_numeric(departure_data['trip'], errors='coerce') + return_data['trip'] = pd.to_numeric(return_data['trip'], errors='coerce') + + departure_data = departure_data.dropna(axis=1) + return_data = return_data.dropna(axis=1) + + departure_mod = sm.tsa.statespace.SARIMAX(departure_data['trip'], order=(1, 1, 1), seasonal_order=(0, 1, 0, 24), freq='h').fit(disp=False, low_memory=True) + return_mod = sm.tsa.statespace.SARIMAX(return_data['trip'], order=(1, 1, 1), seasonal_order=(0, 1, 0, 24), freq='h').fit(disp=False, low_memory=True) + + station_dict[station] = {} + station_dict[station]['departure_mod'] = departure_mod + station_dict[station]['return_mod'] = return_mod + + +def index(request): + return HttpResponse('You\'re at the app index.') + + +def predict(request): + if request.method == 'GET': + current = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + timestamp = request.GET.get('timestamp', current) + station = request.GET.get('station', 'Kamppi (M)') + ts = timestamp + + defaultBikeCount = 28 + + departingCount = -1 + if departure_data.index.max() >= pd.to_datetime(timestamp): + ls = departure_data[departure_data['Departure'] == ts]['trip'].tolist() + if len(ls) != 0: + departingCount = ls[0] + else: + pass + else: + departure_mod = station_dict[station]['departure_mod'] + departForecast = departure_mod.forecast(timestamp) + departingCount = round(departForecast[-1]) + + returningCount = -1 + if return_data.index.max() >= pd.to_datetime(timestamp): + ls = return_data[return_data['Return'] == ts]['trip'].tolist() + if len(ls) != 0: + returningCount = ls[0] + else: + pass + else: + return_mod = station_dict[station]['return_mod'] + returnForecast = return_mod.forecast(timestamp) + returningCount = round(returnForecast[-1]) + + bikeAtStationCount = defaultBikeCount - departingCount + returningCount + + result = { + 'timestamp': timestamp, + 'station': station, + 'departingCount': departingCount, + 'returningCount': returningCount, + 'bikeAtStationCount': bikeAtStationCount, + 'increasing': returningCount > departingCount + } + + return JsonResponse(result) \ No newline at end of file diff --git a/backend/cross_validation_parameters.ipynb b/backend/cross_validation_parameters.ipynb new file mode 100644 index 0000000..3097045 --- /dev/null +++ b/backend/cross_validation_parameters.ipynb @@ -0,0 +1,204 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 2, + "id": "cd21c67a-e679-43e3-85a4-471a35522d42", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"\\nparam_mse = list()\\nfor param in parameter_space:\\n model = sm.tsa.statespace.SARIMAX(train_data['trip'], order=param, seasonal_order=(0, 1, 0, 24), freq='h').fit()\\n forecast = model.forecast(datetime(year=2023,month=10,day=31,hour=17))\\n forecast = forecast[datetime(year=2023,month=4,day=1):]\\n #test_data.index = pd.DatetimeIndex(forecast.index)\\n \\n final = pd.concat([forecast,test_data], axis=1)\\n errors = final['trip'] - final['predicted_mean']\\n mse = np.mean(errors**2)\\n\\n param_mse.append((mse, param))\\n\"" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import pandas as pd\n", + "import numpy as np\n", + "from sklearn.model_selection import TimeSeriesSplit\n", + "from datetime import datetime\n", + "import statsmodels.api as sm\n", + "\n", + "# Use Kamppi as an example, could be generalized to all stations\n", + "df = pd.read_csv(\"datasets/Jämeräntaival_hourly_aggregate.csv\")\n", + "df['Departure'] = pd.to_datetime(df['Departure'], format='mixed')\n", + "df.set_index('Departure', inplace=True)\n", + "\n", + "train_end = datetime(year=2022, month=10, day=31)\n", + "train_start = datetime(year=2018,month=4,day=1)\n", + "train_data = df[:train_end]\n", + "test_end = datetime(year=2023, month=10, day = 31)\n", + "test_data = df[datetime(year=2023,month=4,day=1):]\n", + "\n", + "parameter_space = list()\n", + "\n", + "for i in range(0,3):\n", + " for j in range(0,3):\n", + " for k in range(0,3):\n", + " parameter_space.append((i,j,k))\n", + "\n", + "#parameter_space = [x for x in parameter_space if x[1] == 0]\n", + "parameter_space = [x for x in parameter_space if x != (0,0,0)]\n", + "'''\n", + "param_mse = list()\n", + "for param in parameter_space:\n", + " model = sm.tsa.statespace.SARIMAX(train_data['trip'], order=param, seasonal_order=(0, 1, 0, 24), freq='h').fit()\n", + " forecast = model.forecast(datetime(year=2023,month=10,day=31,hour=17))\n", + " forecast = forecast[datetime(year=2023,month=4,day=1):]\n", + " #test_data.index = pd.DatetimeIndex(forecast.index)\n", + " \n", + " final = pd.concat([forecast,test_data], axis=1)\n", + " errors = final['trip'] - final['predicted_mean']\n", + " mse = np.mean(errors**2)\n", + "\n", + " param_mse.append((mse, param))\n", + "'''\n", + "\n", + "\n", + "\n", + " \n" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "id": "db9591dd-7bfd-46d3-aa50-8a757776aeb0", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "9.043665073451029 9.056463656418249\n", + "9.029642126494705 9.043665073451029\n", + "9.029317575211266 9.029642126494705\n", + "(np.float64(9.029317575211266), (1, 0, 2))\n" + ] + } + ], + "source": [ + "min_mse = param_mse[0][0]\n", + "\n", + "index = 0\n", + "for i in range(0,len(param_mse)):\n", + " if param_mse[i][0] < min_mse:\n", + " print(param_mse[i][0], min_mse)\n", + " min_mse = param_mse[i][0]\n", + " index = i\n", + "\n", + "\n", + "print(param_mse[index])\n" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "5b01e8b2-19f2-4809-bee7-0e0589489841", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/aleksi/venv/lib/python3.12/site-packages/statsmodels/tsa/base/tsa_model.py:473: ValueWarning: No frequency information was provided, so inferred frequency h will be used.\n", + " self._init_dates(dates, freq)\n", + " This problem is unconstrained.\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "RUNNING THE L-BFGS-B CODE\n", + "\n", + " * * *\n", + "\n", + "Machine precision = 2.220D-16\n", + " N = 3 M = 10\n", + "\n", + "At X0 0 variables are exactly at the bounds\n", + "\n", + "At iterate 0 f= 2.51523D+00 |proj g|= 8.46816D-02\n", + "\n", + "At iterate 5 f= 2.46808D+00 |proj g|= 9.28777D-03\n", + "\n", + "At iterate 10 f= 2.45763D+00 |proj g|= 8.55309D-03\n", + "\n", + "At iterate 15 f= 2.45509D+00 |proj g|= 5.19865D-03\n", + "\n", + "At iterate 20 f= 2.45471D+00 |proj g|= 1.64676D-03\n", + "\n", + "At iterate 25 f= 2.45467D+00 |proj g|= 4.36219D-04\n", + "\n", + "At iterate 30 f= 2.45467D+00 |proj g|= 2.78346D-06\n", + "\n", + " * * *\n", + "\n", + "Tit = total number of iterations\n", + "Tnf = total number of function evaluations\n", + "Tnint = total number of segments explored during Cauchy searches\n", + "Skip = number of BFGS updates skipped\n", + "Nact = number of active bounds at final generalized Cauchy point\n", + "Projg = norm of the final projected gradient\n", + "F = final function value\n", + "\n", + " * * *\n", + "\n", + " N Tit Tnf Tnint Skip Nact Projg F\n", + " 3 30 36 1 0 0 2.783D-06 2.455D+00\n", + " F = 2.4546707052577292 \n", + "\n", + "CONVERGENCE: NORM_OF_PROJECTED_GRADIENT_<=_PGTOL \n", + "10.388099588499935\n" + ] + } + ], + "source": [ + "model = sm.tsa.statespace.SARIMAX(train_data['trip'], order=(1,1,1), seasonal_order=(0,1,0,24), freq='h').fit()\n", + "forecast = model.forecast(datetime(year=2023,month=10,day=31,hour=17))\n", + "forecast = forecast[datetime(year=2023,month=4,day=1):]\n", + "#test_data.index = pd.DatetimeIndex(forecast.index)\n", + "\n", + "final = pd.concat([forecast,test_data], axis=1)\n", + "errors = final['trip'] - final['predicted_mean']\n", + "mse = np.mean(errors**2)\n", + "\n", + "print(mse)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "726acc6f-ea29-4bb6-8252-a6556ae10ac4", + "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.12.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/backend/db.sqlite3 b/backend/db.sqlite3 new file mode 100644 index 0000000..6a0cc48 Binary files /dev/null and b/backend/db.sqlite3 differ diff --git a/backend/error_metric_calculations_results.txt b/backend/error_metric_calculations_results.txt new file mode 100644 index 0000000..b18cedf --- /dev/null +++ b/backend/error_metric_calculations_results.txt @@ -0,0 +1 @@ +[(np.float64(3.3274909833208963), 'no exogenous data'), (np.float64(3.158263780804383), 'just weather'), (np.float64(3.3220296790264277), 'just weekdays and hours'), (np.float64(3.1363872336562717), 'all exogenous')] diff --git a/backend/forecast_error_metric_calculations.ipynb b/backend/forecast_error_metric_calculations.ipynb new file mode 100644 index 0000000..0ac1b0d --- /dev/null +++ b/backend/forecast_error_metric_calculations.ipynb @@ -0,0 +1,301 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 56, + "id": "cd21c67a-e679-43e3-85a4-471a35522d42", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "asd\n", + "asd2\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/otto/.pyenv/versions/3.11.2/lib/python3.11/site-packages/statsmodels/tsa/statespace/sarimax.py:966: UserWarning: Non-stationary starting autoregressive parameters found. Using zeros as starting parameters.\n", + " warn('Non-stationary starting autoregressive parameters'\n", + "/Users/otto/.pyenv/versions/3.11.2/lib/python3.11/site-packages/statsmodels/tsa/statespace/sarimax.py:978: UserWarning: Non-invertible starting MA parameters found. Using zeros as starting parameters.\n", + " warn('Non-invertible starting MA parameters found.'\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "RUNNING THE L-BFGS-B CODE\n", + "\n", + " * * *\n", + "\n", + "Machine precision = 2.220D-16\n", + " N = 8 M = 10\n", + "\n", + "At X0 0 variables are exactly at the bounds\n", + "\n", + "At iterate 0 f= 2.52687D+00 |proj g|= 3.92148D-01\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + " This problem is unconstrained.\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "At iterate 5 f= 2.24021D+00 |proj g|= 1.15085D-01\n", + "\n", + "At iterate 10 f= 2.19412D+00 |proj g|= 5.75993D-03\n", + "\n", + "At iterate 15 f= 2.19337D+00 |proj g|= 8.22902D-04\n", + "\n", + "At iterate 20 f= 2.19327D+00 |proj g|= 4.98625D-03\n", + "\n", + "At iterate 25 f= 2.19325D+00 |proj g|= 3.48694D-05\n", + "\n", + " * * *\n", + "\n", + "Tit = total number of iterations\n", + "Tnf = total number of function evaluations\n", + "Tnint = total number of segments explored during Cauchy searches\n", + "Skip = number of BFGS updates skipped\n", + "Nact = number of active bounds at final generalized Cauchy point\n", + "Projg = norm of the final projected gradient\n", + "F = final function value\n", + "\n", + " * * *\n", + "\n", + " N Tit Tnf Tnint Skip Nact Projg F\n", + " 8 25 26 1 0 0 3.487D-05 2.193D+00\n", + " F = 2.1932501470633761 \n", + "\n", + "CONVERGENCE: REL_REDUCTION_OF_F_<=_FACTR*EPSMCH \n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/otto/.pyenv/versions/3.11.2/lib/python3.11/site-packages/statsmodels/tsa/statespace/sarimax.py:966: UserWarning: Non-stationary starting autoregressive parameters found. Using zeros as starting parameters.\n", + " warn('Non-stationary starting autoregressive parameters'\n", + "/Users/otto/.pyenv/versions/3.11.2/lib/python3.11/site-packages/statsmodels/tsa/statespace/sarimax.py:978: UserWarning: Non-invertible starting MA parameters found. Using zeros as starting parameters.\n", + " warn('Non-invertible starting MA parameters found.'\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "RUNNING THE L-BFGS-B CODE\n", + "\n", + " * * *\n", + "\n", + "Machine precision = 2.220D-16\n", + " N = 10 M = 10\n", + "\n", + "At X0 0 variables are exactly at the bounds\n", + "\n", + "At iterate 0 f= 2.52689D+00 |proj g|= 3.92567D-01\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + " This problem is unconstrained.\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "At iterate 5 f= 2.23987D+00 |proj g|= 1.15352D-01\n", + "\n", + "At iterate 10 f= 2.19343D+00 |proj g|= 5.82758D-03\n", + "\n", + "At iterate 15 f= 2.19242D+00 |proj g|= 1.41481D-03\n", + "\n", + "At iterate 20 f= 2.19228D+00 |proj g|= 1.02376D-03\n", + "\n", + "At iterate 25 f= 2.19225D+00 |proj g|= 8.93884D-04\n", + "\n", + "At iterate 30 f= 2.19224D+00 |proj g|= 6.02699D-05\n", + "\n", + "At iterate 35 f= 2.19224D+00 |proj g|= 4.09553D-04\n", + "\n", + "At iterate 40 f= 2.19223D+00 |proj g|= 4.81752D-04\n", + "\n", + "At iterate 45 f= 2.19220D+00 |proj g|= 9.39974D-04\n", + "\n", + "At iterate 50 f= 2.19205D+00 |proj g|= 5.16353D-03\n", + "\n", + " * * *\n", + "\n", + "Tit = total number of iterations\n", + "Tnf = total number of function evaluations\n", + "Tnint = total number of segments explored during Cauchy searches\n", + "Skip = number of BFGS updates skipped\n", + "Nact = number of active bounds at final generalized Cauchy point\n", + "Projg = norm of the final projected gradient\n", + "F = final function value\n", + "\n", + " * * *\n", + "\n", + " N Tit Tnf Tnint Skip Nact Projg F\n", + " 10 50 54 1 0 0 5.164D-03 2.192D+00\n", + " F = 2.1920465431369465 \n", + "\n", + "STOP: TOTAL NO. of ITERATIONS REACHED LIMIT \n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/otto/.pyenv/versions/3.11.2/lib/python3.11/site-packages/statsmodels/base/model.py:607: ConvergenceWarning: Maximum Likelihood optimization failed to converge. Check mle_retvals\n", + " warnings.warn(\"Maximum Likelihood optimization failed to \"\n" + ] + } + ], + "source": [ + "import pandas as pd\n", + "import numpy as np\n", + "from sklearn.model_selection import TimeSeriesSplit\n", + "from datetime import datetime\n", + "import statsmodels.api as sm\n", + "\n", + "stations = ['Jämeräntaival']\n", + "data = pd.read_csv('datasets/' + stations[0] + '_hourly_aggregate.csv')\n", + "data['Departure'] = pd.to_datetime(data['Departure'], format='mixed')\n", + "\n", + "results = []\n", + "\n", + "weather_df = pd.read_csv('datasets/weather_hourly_helsinki.csv')\n", + "weather_df = weather_df.loc[1:, :]\n", + "weather_df.columns = weather_df.iloc[0]\n", + "weather_df = weather_df.loc[2:, :]\n", + "weather_df['time'] = pd.to_datetime(weather_df['time'], format='mixed')\n", + "\n", + "data = pd.merge(weather_df, data, how='inner', left_on='time', right_on='Departure')\n", + "data = data.drop(['time'], axis=1)\n", + "\n", + "data['temperature_2m (°C)'] = pd.to_numeric(data['temperature_2m (°C)'], errors='coerce')\n", + "data['rain (mm)'] = pd.to_numeric(data['rain (mm)'], errors='coerce')\n", + "data['trip'] = pd.to_numeric(data['trip'], errors='coerce')\n", + "\n", + "\n", + "# generation of weekday & hour series\n", + "datedata = pd.DataFrame()\n", + "datedata['date'] = data['Departure']\n", + "datedata['weekday'] = data['Departure'].dt.weekday\n", + "days = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat']\n", + "for day in days:\n", + " datedata[day] = 0\n", + " datedata.loc[datedata['date'].dt.weekday == 0, day] = 1\n", + "for i in range(0, 23):\n", + " asd = 'hour' + str(i)\n", + " days.append(asd)\n", + " datedata[asd] = 0\n", + " datedata.loc[datedata['date'].dt.hour == i, asd] = 1\n", + "\n", + "data = pd.merge(datedata, data, how='inner', left_on='date', right_on='Departure')\n", + "data.set_index(data['Departure'], inplace=True)\n", + "\n", + "\n", + "\n", + "train_end = datetime(year=2022, month=10, day=31)\n", + "train_start = datetime(year=2018,month=4,day=1)\n", + "train_data = data[:train_end]\n", + "test_end = datetime(year=2023, month=10, day = 31)\n", + "test_data = data[datetime(year=2023,month=4,day=1):]\n", + "\n", + "\n", + "def MASE(y_true, y_pred, y_train):\n", + " forecast = y_pred.reset_index(drop=True)\n", + " outsample = y_true[:].iloc[:len(y_pred)]\n", + " insample = y_train.reset_index(drop=True).to_numpy()\n", + " frequency=1\n", + " return np.mean(np.abs(forecast - outsample)) / np.mean(np.abs(insample[:-frequency] - insample[frequency:]))\n", + "\n", + "\n", + "# reset indexes as they do funky things\n", + "test_data.reset_index(drop=True, inplace=True)\n", + "train_data.reset_index(drop=True, inplace=True)\n", + "\n", + "print('')\n", + "\n", + "# no exogenous variables at all\n", + "model = sm.tsa.statespace.SARIMAX(train_data['trip'], order=(3,1,2), seasonal_order=(1, 1, 1, 24)).fit()\n", + "forecast = model.forecast(steps=24*30*7)\n", + "forecast = forecast[:]\n", + "results.append((MASE(test_data['trip'], forecast, train_data['trip']), 'no exog'))\n", + "\n", + "#just the weather as exogenous data\n", + "exogenous = ['rain (mm)', 'temperature_2m (°C)']\n", + "model = sm.tsa.statespace.SARIMAX(train_data['trip'], order=(3,1,2), seasonal_order=(1, 1, 1, 24), exog=train_data[exogenous]).fit()\n", + "forecast = model.forecast(steps=24*30*7, exog=test_data[exogenous].iloc[:(24*30*7)])\n", + "forecast = forecast[:]\n", + "results.append((MASE(test_data['trip'], forecast, train_data['trip']), 'just weather'))\n", + "\n", + "#just weekday + time of the day as exogenous\n", + "model = sm.tsa.statespace.SARIMAX(train_data['trip'], order=(3,1,2), seasonal_order=(1, 1, 1, 24), exog=train_data[days]).fit()\n", + "forecast = model.forecast(steps=24*30*7, exog=test_data[days].iloc[:(24*30*7)])\n", + "forecast = forecast[:]\n", + "results.append((MASE(test_data['trip'], forecast, train_data['trip']), 'just timely stuff'))\n", + "\n", + "\n", + "#all exogenous variables\n", + "for asd in days:\n", + " exogenous.append(asd)\n", + "model = sm.tsa.statespace.SARIMAX(train_data['trip'], order=(3,1,2), seasonal_order=(1, 1, 1, 24), exog=train_data[exogenous]).fit()\n", + "forecast = model.forecast(steps=24*30*7, exog=test_data[exogenous].iloc[:(24*30*7)])\n", + "forecast = forecast[:]\n", + "results.append((MASE(test_data['trip'], forecast, train_data['trip']), 'all exogs'))\n", + "\n", + "\n", + "print(results)\n", + " \n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5b01e8b2-19f2-4809-bee7-0e0589489841", + "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.11.2" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/backend/initial_model.ipynb b/backend/initial_model.ipynb new file mode 100644 index 0000000..047f443 --- /dev/null +++ b/backend/initial_model.ipynb @@ -0,0 +1,108 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 65, + "id": "edadd89f-0fc1-4e07-a249-2a0d4052258c", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " SARIMAX Results \n", + "==============================================================================\n", + "Dep. Variable: Trip count No. Observations: 7999\n", + "Model: ARIMA(2, 1, 2) Log Likelihood -14325.727\n", + "Date: Thu, 26 Sep 2024 AIC 28661.454\n", + "Time: 17:35:05 BIC 28696.389\n", + "Sample: 04-01-2023 HQIC 28673.412\n", + " - 10-31-2023 \n", + "Covariance Type: opg \n", + "==============================================================================\n", + " coef std err z P>|z| [0.025 0.975]\n", + "------------------------------------------------------------------------------\n", + "ar.L1 0.4432 0.034 12.890 0.000 0.376 0.511\n", + "ar.L2 0.2343 0.009 26.703 0.000 0.217 0.251\n", + "ma.L1 -1.5914 0.035 -45.151 0.000 -1.660 -1.522\n", + "ma.L2 0.5926 0.035 16.913 0.000 0.524 0.661\n", + "sigma2 2.1040 0.015 136.783 0.000 2.074 2.134\n", + "===================================================================================\n", + "Ljung-Box (L1) (Q): 0.14 Jarque-Bera (JB): 29710.16\n", + "Prob(Q): 0.71 Prob(JB): 0.00\n", + "Heteroskedasticity (H): 0.77 Skew: 2.39\n", + "Prob(H) (two-sided): 0.00 Kurtosis: 11.15\n", + "===================================================================================\n", + "\n", + "Warnings:\n", + "[1] Covariance matrix calculated using the outer product of gradients (complex-step).\n", + "2024-02-28 07:00 0.438484\n", + "Freq: h, dtype: float64\n" + ] + } + ], + "source": [ + "import pandas as pd\n", + "import statsmodels.api as sm\n", + "from statsmodels.tsa.arima.model import ARIMA\n", + "\n", + "import matplotlib.pyplot as plt\n", + "df = pd.read_csv('datasets/aggregated_2023_Designmuseo.csv')\n", + "df['Departure'] = pd.to_datetime(df['Departure'], format='mixed')\n", + "df['Trip count'] = pd.to_numeric(df['Trip count'])\n", + "\n", + "start_date = '2023-01-04 00:00'\n", + "end_date = '2023-31-10 23:00'\n", + "\n", + "start = pd.to_datetime(start_date, format = '%Y-%d-%m %H:%M')\n", + "end_date = pd.to_datetime(end_date, format = '%Y-%d-%m %H:%M')\n", + "dates = pd.DataFrame({'Departure':pd.date_range(start, end_date, freq='h')})\n", + "\n", + "\n", + "df3 = pd.concat([df, dates])\n", + "values = {'Trip count' : 0, 'Departure station id' : 7.0, 'Departure station name' : 'Designmuseo'}\n", + "df3.fillna(value=values, inplace = True)\n", + "df3 = df3.sort_values(by='Departure')\n", + "\n", + "df3.set_index('Departure', inplace=True)\n", + "df3.index = pd.DatetimeIndex(df3.index).to_period('h')\n", + "\n", + "model = ARIMA(df3['Trip count'], order=(2,1,2))\n", + "model_fit = model.fit()\n", + "# summary of fit model\n", + "print(model_fit.summary())\n", + "'''\n", + "print(df3.sort_values(by='Departure'))\n", + "plt.acorr(df3['Trip count'], maxlags = 1000)\n", + "plt.grid(True)\n", + "'''\n", + "'''\n", + "sm.graphics.tsa.plot_pacf(df3['Trip count'], lags =24, method='ywm')\n", + "plt.show()\n", + "'''\n", + "print(model_fit.forecast(1))\n" + ] + } + ], + "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.12.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/backend/main.ipynb b/backend/main.ipynb new file mode 100644 index 0000000..784eecd --- /dev/null +++ b/backend/main.ipynb @@ -0,0 +1,1305 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Importing Libraries**" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import pandas as pd\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "import seaborn as sns\n", + "import statsmodels.api as sm\n", + "import datetime as datetime" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Preprocessing**" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Aggregating over Sammonpuistikko\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Albertinkatu\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Arabiankatu\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Piispanportti\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Marian sairaala\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Perämiehenkatu\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Töölönlahdenkatu\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Koskelantie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Päijänteentie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Lintulahdenkatu\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Kansallismuseo\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Laulurastaantie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Kaivopuisto\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Jämeräntaival\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Niittykumpu (M)\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Toppelundinportti\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Näkinsilta\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Kutsuntatie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Muurarinkuja\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Urheilupuisto (M)\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Matinkyläntie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Gyldenintie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Hauenkalliontie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Kössi Koskisen aukio\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Fleminginkatu\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Hakaniemi (M)\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Kauppakorkeakoulu\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Nordenskiöldinaukio\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Munkkiniemen aukio\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Mamsellimyllynkatu\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Hietalahdentori\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Karhupuisto\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Matinkartanontie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Rautatientori / länsi\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Lauttasaarensilta\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Rautatientori / itä\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Itälahdenkatu\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Marjaniementie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Komeetankatu\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Piispankallio\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Ympyrätalo\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Lystimäki\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Toinen linja\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Lastenlehto\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Vuosaaren puistopolku\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Kaivonkatsojanpuisto\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Niittymaa\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Vallipolku\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Etuniementie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Pitäjänmäen asema\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Sompasaari\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Erottajan aukio\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Kuikkarinne\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Lystimäensilta\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Olympiastadion\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Apollonkatu\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Töölöntulli\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Jätkäsaarenlaituri\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Reiherintie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Kamppi (M)\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Töölönkatu\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Välimerenkatu\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Kuusitie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Kalasatama (M)\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Sumukuja\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Intiankatu\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Kiasma\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Baana\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Haukilahdenkatu\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Varustuksentie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Pajupillintie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Tilkanvierto\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Lauttasaaren ostoskeskus\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Pasilan asema\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Vanha kirkkopuisto\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Siilitie 13\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Bermudankuja\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Betonimies\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Haapaniemenkatu\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Vilhonvuorenkatu\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Heikkiläntie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Siilitie 9\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Jalavatie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Mäntyviita\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Muusantori\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Eteläinen Hesperiankatu\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Korjaamo\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Puotinkylän kartano\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Vallilan varikko\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Muotoilijankatu\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Aurinkotuulenkatu\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Linnanmäki\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Pajamäki\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Messeniuksenkatu\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Eläinmuseo\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Mannerheimintie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Pohjois-Haagan asema\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Ida Aalbergin tie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Kustaankatu\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Tollinpolku\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Venttiilikuja\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Syystie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Töölönlahden puisto\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Porolahden koulu\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Relanderinaukio\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Käpyläntie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Brahen kenttä\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Pernajantie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Oravannahkatori\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Tontunmäentie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Viiskulma\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Brahen puistikko\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Orpaanporras\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Otsolahti\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Peukaloisentie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Kalkkihiekantie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Varsapuistikko\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Narinkka\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Töölöntori\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Itäkeskus (M)\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Merihaka\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Ulappasilta\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Painiitty\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Hankasuontie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Munkkivuoren ostoskeskus\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Isoisänsilta\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Laivalahden puistotie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Haakoninlahdenkatu\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Halkaisijantie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Vanha Kauppahalli\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Liisanpuistikko\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Kaisaniemenpuisto\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Myllypuro (M)\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Tapionaukio\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Väärämäentie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Paavalinpuisto\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Laajalahden aukio\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Vanha Viertotie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Porthania\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Verkatehtaanpuisto\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Annankatu\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Länsisatamankuja\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Otaranta\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Louhentori\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Kiskontie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Esterinportti\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Herttoniemi (M)\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Mikkolantie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Urhea-kampus\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Huovitie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Suurpellonaukio\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Jäähalli\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Viikin tiedepuisto\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Arabian kauppakeskus\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Rastila (M)\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Kivikonlaita\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Tietäjä\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Leikosaarentie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Hauenkallio\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Pohjolanaukio\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Mastokatu\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Ooppera\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Pohjankulma\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Länsituuli\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Itämerentori\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Merisotilaantori\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Laajasalon ostoskeskus\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Eränkävijäntori\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Kustaankartano\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Ritarikatu\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Viljelijäntie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Olarinluoma\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Kylävoudintie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Maistraatintori\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Laivasillankatu\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Gebhardinaukio\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Rummunlyöjänkatu\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Länsisatamankatu\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Velodrominrinne\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Aalto-yliopisto (M), Korkeakouluaukio\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Kalevalantie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Saniaiskuja\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Katariina Saksilaisen katu\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Siilitie (M)\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Juhana Herttuan tie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Agronominkatu\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Kauppakeskus Columbus\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Laajalahden keskus\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Tenholantie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Maunula\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Huopalahdentie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Voikukantie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Vesakkotie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Kriikunakuja\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Rajasaarentie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Designmuseo\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Punakiventie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Mestarinkatu\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Kesäkatu\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Koskelan varikko\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Pohjolankatu\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Westendinasema\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Halmetie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Friisilänaukio\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Aurinkolahdenaukio\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Rautatieläisenkatu\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Kontula (M)\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Hernepellontie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Hakalehto\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Käpylän asema\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Kaapelitehdas\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Suomenlahdentie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Läkkitori\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Mellunmäki (M)\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Mäkelänkatu\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Haagan tori\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Tyynenmerenkatu\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Unioninkatu\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Paloheinäntie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Lukutori\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Heikkilänaukio\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Sepänkatu\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Metsänneidonpolku\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Malminkartanon asema\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Postipuun koulu\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Tilkantori\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Piispansilta\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Keilaniemi (M)\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Ulvilantie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Haapasaarentie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Korppaanmäentie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Meri-Rastilan tori\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Verkkosaari\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Lokitie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Diakoniapuisto\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Kapteeninpuistikko\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Kauppakeskus Kaari\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Komentajankatu\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Tapiolan urheilupuisto\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Karhulantie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Pihlajamäki\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Nelikkotie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Teurastamo\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Toppelundintie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Kalevankatu\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Otto Brandtin tie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Puistokaari\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Vallikatu\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Postipuisto\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Ruskeasuon varikko\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Marjaniemi\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Agnetankuja\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Huopalahden asema\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Hernesaarenranta\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Ramsinniementie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Kivikonkaari\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Lepolantie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Säterinniitty\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Vähäntuvantie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Petter Wetterin tie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Säterinrinne\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Kanavaranta\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Cygnaeuksenkatu\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Puotila (M)\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Siltavoudintie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Tekniikantie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Tunnelitie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Ala-Malmin tori\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Hanasaari\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Hertanmäenkatu\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Thalianaukio\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Paciuksenkaari\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Opastinsilta\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Leppävaarankäytävä\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Kalkkipellonmäki\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Ratapihantie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Radiokatu\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Arielinkatu\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Sörnäinen (M)\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Ruomelantie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Lehtisaarentie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Luoteisväylä\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Golfpolku\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Sofianlehdonkatu\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Tuohipolku\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Kivikon liikuntapuisto\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Ratsutori\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Kulttuuriaukio\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Puotinharju\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Torpparinmäentie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Margareetankuja\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Asentajanpuisto\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Vuosaaren liikuntapuisto\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Herttoniemenranta\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Ilmalan asema\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Leppävaaranaukio\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Sateentie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Eteläesplanadi\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Humalniementie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Kannelmäen liikuntapuisto\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Senaatintori\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Auringonkatu\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Koukkusaarentie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Jännetie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Käskynhaltijantie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Sateenkaarentie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Valimotie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Aalto-yliopisto (M), Tietotie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Kulosaari (M)\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Tulisuontie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Melkonkuja\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Elimäenkatu\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Yhdyskunnankuja\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Teollisuuskatu\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Von Daehnin katu\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Ruutikatu\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Kaustisentie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Jokipellontie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Mellstenintie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Rapakiventie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Seurasaari\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Pukinmäen liikuntapuisto\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Tiistiläntie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Pukinmäen asema\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Sinkilätie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Malmin asema\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Huhtakuja\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Tupasaarentie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Saunalahdentie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Koivusaari (M)\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Haukilahdenaukio\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Takomotie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Teerisuontie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Ajomiehentie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Keilaranta\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Töyrynummi\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Aulangontie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Linnakepolku\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Kasarmitori\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Salmisaarenranta\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Majurinkulma\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Mustikkamaa\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Puistolan asema\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Vihdintie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Viikin normaalikoulu\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Valimon asema\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Meilahden sairaala\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Itäportti\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Sähkömies\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Leppäsuonaukio\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Herttoniemen kirkko\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Kontulankaari\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Matinlahdenranta\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Trumpettikuja\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Honkasuo\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Paloheinän kirjasto\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Abraham Wetterin tie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Oulunkylän asema\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Puotilan ostoskeskus\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Mosaiikkitori\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Kannelmäen asema\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Eerikinkatu\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Pirkkolan liikuntapuisto\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Länsiterminaali\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Sallatunturintie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Torpanranta\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Tammisalon aukio\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Stenbäckinkatu\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Leppävaaran uimahalli\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Hietaniemenkatu\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Revontulentie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Ahertajantie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Westendintie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Näyttelijäntie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Marttila\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Karviaistie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Nuottaniementie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Puotilantie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Elfvik\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Kuunkatu\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Karhusuontie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Roihupelto\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over A.I. Virtasen aukio\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Sepetlahdentie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Prinsessantie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Teljäntie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Itäkeskus Metrovarikko\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Humikkalankuja\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Savela\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Paloheinän maja\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Tuukkalantie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Tiistinkallio\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Vanha Tapanilantie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Haukilahdensolmu\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Uimastadion\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Kirkkoherrantie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Kuninkaantammi\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Hakuninmaa\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Hämeenlinnanväylä\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Alakiventie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Malmin sairaala\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Etupellonpuisto\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Kuusisaari\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Säteri\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Koivu-Mankkaa\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Helluntairaitti\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Mäkitorpantie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Töyrynummentie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Hagalundinpuisto\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Haukilahdenranta\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Ehrenströmintie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Purjetie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Gunillantie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Linnuntie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Maarinranta\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Isosaarentie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Mäkkylän asema\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Kalannintie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Orionintie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Piikintie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Innopoli\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Kurkimäentie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Maununneva\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Mankkaanaukio\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Kotinummentie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Korkeasaari\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Lettopolku\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Puistolantori\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Upseerinkatu\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Derby Business Park\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Siltamäki\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Jollas\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Workshop Helsinki\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Mankkaanlaaksontie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Gransinmäki\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Keilalahti\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Puistolan VPK\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Lallukankuja\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Tapanilan asema\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Jakomäentie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Maatullinkuja\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Outotec\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Tilketori\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Vartioharjuntie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Alppikylä\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Avaruuskatu\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Nokkala\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Gallen-Kallelan tie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Malminkartanonhuippu\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Leppävaaran urheilupuisto\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Kurkimäki\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Rukatunturintie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Kurkijoentie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Tuulimäki\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Porvarintie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Jakomäki\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Paciuksenkatu\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Heikinlaakso\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Koetilankuja\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Pop-Up Kansalaistori\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Sörnäisten metroasema\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Museokatu\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Kampin metroasema\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Hakaniemen metroasema\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Eiran Sairaala\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Kauppatori\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Erottaja\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over nan\n", + "Writing aggregated dataframe to .csv...\n", + "Failed to write nan due to 'float' object has no attribute 'replace'\n", + "Aggregating over Hollolantie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Kirjurinkuja\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Lumivaarantie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Kalasataman metroasema\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Hylkeenpyytäjänkatu\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Messitytönkatu\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Kaironkatu\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Cygnauksenkatu\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Marian Sairaala\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Porkkalankatu\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Veturitori\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Mäntytie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Kalastajantie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Lintumetsä\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Relay Box test station\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Kauppakartanonkuja\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Armas Launiksen katu\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Workshop Konala\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Tiurintie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over O'Bike Station\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Ruomelantie***\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Leiritori\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Adjutantinkatu\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Lahnalahdentie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Professorintie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Niemenmäenkuja\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Olarinkatu\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Ulvilanpuisto\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Friisinkalliontie\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Workshop Smoove\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over 999 Pop-Up Stadin Ammattiopisto\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over 9 Station Test\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over 09 Erottaja\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Herttoniemen ranta\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Puotilan metroasema\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Kulosaaren metroasema\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Myllypuron metroasema\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Herttoniemen metroasema\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Itäkeskuksen metroasema\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Siilitien metroasema\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Pop-Up Jätkäsaari\n", + "Writing aggregated dataframe to .csv...\n", + "Aggregating over Kirjurinkuja\n", + "Writing aggregated dataframe to .csv...\n" + ] + } + ], + "source": [ + "from os import listdir\n", + "\n", + "weather_df = pd.read_csv('datasets/weather_hourly_helsinki.csv', header=2)\n", + "bike_df = pd.DataFrame(columns=['Departure', 'Return', 'Departure station id', 'Departure station name', 'Return station id', 'Return station name', 'Covered distance (m)', 'Duration (sec.)'])\n", + "\n", + "print('Loading datasets...')\n", + "for dataset in listdir('datasets'):\n", + " if dataset == 'weather_hourly_helsinki.csv' or dataset == 'full_bike_data.csv' or dataset.endswith('.gz'):\n", + " continue\n", + " temp_df = pd.read_csv('datasets/' + dataset, low_memory=False)\n", + " bike_df = pd.concat([bike_df, temp_df])\n", + "\n", + "bike_df['Departure'] = pd.to_datetime(bike_df['Departure'], format='mixed')\n", + "bike_df['Return'] = pd.to_datetime(bike_df['Return'], format='mixed')\n", + "bike_df.to_csv('datasets/full_bike_data.csv')\n", + "\n", + "for station in bike_df['Departure station name'].unique():\n", + " print(f'Aggregating over {station}')\n", + " columns = ['Departure', 'Departure station name', 'Departure station id']\n", + " temp_station = bike_df.loc[bike_df['Departure station name'] == station, columns]\n", + " temp_station['trip'] = 1\n", + "\n", + " temp_station = temp_station.resample('h', on='Departure').trip.sum()\n", + "\n", + " print('Writing aggregated dataframe to .csv...')\n", + " try:\n", + " name = station.replace('/', '-')\n", + " temp_station.to_csv('datasets/' + name + '_hourly_aggregate.csv', mode='x')\n", + " except Exception as e:\n", + " print(f'Failed to write {station} due to {e}')\n", + "\n", + "for station in bike_df['Return station name'].unique():\n", + " print(f'Aggregating over {station}')\n", + " columns = ['Return', 'Return station name', 'Return station id']\n", + " temp_station = bike_df.loc[bike_df['Return station name'] == station, columns]\n", + " temp_station['trip'] = 1\n", + "\n", + " temp_station = temp_station.resample('h', on='Return').trip.sum()\n", + "\n", + " print('Writing aggregated dataframe to .csv...')\n", + " try:\n", + " name = station.replace('/', '-')\n", + " temp_station.to_csv('datasets/' + name + '_return_hourly_aggregate.csv', mode='x')\n", + " except Exception as e:\n", + " print(f'Failed to write {station} due to {e}')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Plotting the Distribution of Bike Trips by Year**" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/tmp/ipykernel_354687/4263803594.py:1: DtypeWarning: Columns (3,5) have mixed types. Specify dtype option on import or set low_memory=False.\n", + " df = pd.read_csv('datasets/full_bike_data.csv', low_memory=True)\n", + "/tmp/ipykernel_354687/4263803594.py:8: FutureWarning: \n", + "\n", + "Passing `palette` without assigning `hue` is deprecated and will be removed in v0.14.0. Assign the `x` variable to `hue` and set `legend=False` for the same effect.\n", + "\n", + " sns.barplot(x=bike_trips_per_year['Year'], y=bike_trips_per_year['Number of Trips'], palette='viridis')\n" + ] + }, + { + "data": { + "image/png": "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EwAAAACY8IngNH/+fDVs2FABAQG655579OWXX96y/apVq9SsWTMFBASodevW+uSTTzxUKQAAAICyyOvBaeXKlYqPj9eUKVO0e/dutWnTRr1799bp06eLbb9jxw4NHjxYw4YNU2ZmpmJiYhQTE6O9e/d6uHIAAAAAZYXXg9Ps2bM1YsQIDR06VC1atNCbb76pSpUqacmSJcW2nzdvnh544AE9++yzat68uaZPn6527drpjTfe8HDlAAAAAMoKf2+e/MqVK9q1a5cSEhKK1vn5+alHjx5KT08vdp/09HTFx8fbrevdu7fWrl1bbPuCggIVFBQUfc7Ly5Mk5efnm9Z3tbDAtA3sOfK9OuPa1Z9deryywOVjcIUxcJarx6CwgDFwlivHoPBn/i64Ha4cg2uXGIPb4coxKLh4xWXHKktcOQaXLl5z2bHKErMxuL7dMAzzgxle9N133xmSjB07dtitf/bZZ40OHToUu0/58uWN5cuX262bP3++UatWrWLbT5kyxZDEwsLCwsLCwsLCwsJS7HLixAnT7OLVK06ekJCQYHeFymaz6cyZM6pRo4YsFosXK7t9+fn5CgsL04kTJxQUFOTtcsokxsC7+P69jzHwPsbA+xgD72MMvK+kj4FhGDp//rzq1Klj2tarwemOO+5QuXLldOrUKbv1p06dUmhoaLH7hIaGOtXearXKarXaratatertF+1DgoKCSuR/oKUJY+BdfP/exxh4H2PgfYyB9zEG3leSxyA4ONihdl6dHKJChQqKiopSampq0TqbzabU1FRFR0cXu090dLRde0natGnTTdsDAAAAwG/l9Vv14uPjNWTIELVv314dOnTQ3LlzdfHiRQ0dOlSSFBsbq7p16yoxMVGSNGbMGHXp0kWzZs1S3759lZycrIyMDC1atMib3QAAAABQink9OA0aNEg//PCDJk+erNzcXLVt21YbNmxQSEiIJCk7O1t+fv+5MNapUyctX75ckyZN0oQJE3TXXXdp7dq1atWqlbe64HFWq1VTpky54RZEeA5j4F18/97HGHgfY+B9jIH3MQbeV5bGwGIYjsy9BwAAAABll9dfgAsAAAAAvo7gBAAAAAAmCE4AAAAAYILgBAAAAAAmCE5ekJiYqLvvvluBgYGqVauWYmJidPDgQbs2P//8s+Li4lSjRg1VqVJFf/jDH2548e/o0aMVFRUl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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "df = pd.read_csv('datasets/full_bike_data.csv', low_memory=True)\n", + "\n", + "df['Departure'] = pd.to_datetime(df['Departure'])\n", + "df['Year'] = df['Departure'].dt.year\n", + "bike_trips_per_year = df.groupby('Year').size().reset_index(name='Number of Trips')\n", + "\n", + "plt.figure(figsize=(10, 6))\n", + "sns.barplot(x=bike_trips_per_year['Year'], y=bike_trips_per_year['Number of Trips'], palette='viridis')\n", + "\n", + "plt.xlabel('Year')\n", + "plt.ylabel('Number of Trips (in millions)')\n", + "\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Model for Predicting Departure Count**" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "0.08274263164029039\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/tmp/ipykernel_3569458/992788154.py:23: FutureWarning: Series.__getitem__ treating keys as positions is deprecated. In a future version, integer keys will always be treated as labels (consistent with DataFrame behavior). To access a value by position, use `ser.iloc[pos]`\n", + " print(res.forecast('2024-06-15 14:00:00')[-1])\n" + ] + } + ], + "source": [ + "station = 'Kamppi (M)'\n", + "\n", + "data = pd.read_csv('datasets/' + station + '_hourly_aggregate.csv')\n", + "data['Departure'] = pd.to_datetime(data['Departure'], format='mixed')\n", + "\n", + "weather_df = pd.read_csv('datasets/weather_hourly_helsinki.csv', header=2)\n", + "weather_df['time'] = pd.to_datetime(weather_df['time'], format='mixed')\n", + "\n", + "data = pd.merge(weather_df, data, how='inner', left_on='time', right_on='Departure')\n", + "data = data.drop(['time'], axis=1)\n", + "data.set_index(data['temperature_2m (°C)'], inplace=True)\n", + "data.set_index(data['rain (mm)'], inplace=True)\n", + "data.set_index(data['Departure'], inplace=True)\n", + "\n", + "data['temperature_2m (°C)'] = pd.to_numeric(data['temperature_2m (°C)'], errors='coerce')\n", + "data['rain (mm)'] = pd.to_numeric(data['rain (mm)'], errors='coerce')\n", + "data['trip'] = pd.to_numeric(data['trip'], errors='coerce')\n", + "\n", + "data = data.dropna(axis=1)\n", + "\n", + "mod = sm.tsa.statespace.SARIMAX(data['trip'], order=(1, 1, 1), seasonal_order=(0, 1, 0, 24), freq='h')\n", + "res = mod.fit(disp=False)\n", + "print(res.forecast('2024-06-15 14:00:00')[-1])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Model for Predicting Return Count**" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/elliot/Projects/data-science-project/backend/env/lib/python3.12/site-packages/statsmodels/tsa/base/tsa_model.py:473: ValueWarning: No frequency information was provided, so inferred frequency h will be used.\n", + " self._init_dates(dates, freq)\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2023-11-01 00:00:00 0.332839\n", + "2023-11-01 01:00:00 0.108983\n", + "2023-11-01 02:00:00 1.033872\n", + "2023-11-01 03:00:00 0.008670\n", + "2023-11-01 04:00:00 0.000214\n", + " ... \n", + "2024-06-15 10:00:00 0.075210\n", + "2024-06-15 11:00:00 1.075206\n", + "2024-06-15 12:00:00 2.075204\n", + "2024-06-15 13:00:00 0.075204\n", + "2024-06-15 14:00:00 0.075204\n", + "Freq: h, Name: predicted_mean, Length: 5463, dtype: float64\n" + ] + } + ], + "source": [ + "station = 'Kamppi (M)'\n", + "\n", + "data = pd.read_csv('datasets/' + station + '_return_hourly_aggregate.csv')\n", + "data['Return'] = pd.to_datetime(data['Return'], format='mixed')\n", + "\n", + "weather_df = pd.read_csv('datasets/weather_hourly_helsinki.csv', header=2)\n", + "weather_df['time'] = pd.to_datetime(weather_df['time'], format='mixed')\n", + "\n", + "data = pd.merge(weather_df, data, how='inner', left_on='time', right_on='Return')\n", + "data = data.drop(['time'], axis=1)\n", + "data.set_index(data['temperature_2m (°C)'], inplace=True)\n", + "data.set_index(data['rain (mm)'], inplace=True)\n", + "data.set_index(data['Return'], inplace=True)\n", + "\n", + "data['temperature_2m (°C)'] = pd.to_numeric(data['temperature_2m (°C)'], errors='coerce')\n", + "data['rain (mm)'] = pd.to_numeric(data['rain (mm)'], errors='coerce')\n", + "data['trip'] = pd.to_numeric(data['trip'], errors='coerce')\n", + "\n", + "data = data.dropna(axis=1)\n", + "\n", + "mod = sm.tsa.statespace.SARIMAX(data['trip'], order=(1, 1, 1), seasonal_order=(0, 1, 0, 24), freq='h')\n", + "res = mod.fit(disp=False)\n", + "print(res.forecast('2024-06-15 14:00:00'))" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "env", + "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.12.6" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/backend/manage.py b/backend/manage.py new file mode 100755 index 0000000..2c49f3a --- /dev/null +++ b/backend/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'project.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/backend/project/__init__.py b/backend/project/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/project/asgi.py b/backend/project/asgi.py new file mode 100644 index 0000000..baef92b --- /dev/null +++ b/backend/project/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for project project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/5.1/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'project.settings') + +application = get_asgi_application() diff --git a/backend/project/settings.py b/backend/project/settings.py new file mode 100644 index 0000000..601e23e --- /dev/null +++ b/backend/project/settings.py @@ -0,0 +1,122 @@ +""" +Django settings for project project. + +Generated by 'django-admin startproject' using Django 5.1.1. + +For more information on this file, see +https://docs.djangoproject.com/en/5.1/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/5.1/ref/settings/ +""" + +from pathlib import Path +import os +from dotenv import load_dotenv + +load_dotenv() +DJANGO_ENV = os.getenv('DJANGO_ENV', 'development') +__prod__ = DJANGO_ENV == 'production' + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY=os.getenv('SECRET_KEY') if __prod__ else 'django-insecure-tu6x-0wmq^rlhrlcd=k$p2ytx7jr7y0jp+*xhq8$(2g5(+ensv' + +# SECURITY WARNING: don't run with debug turned on in production! +# DEBUG = False if __prod__ else True +DEBUG = True + +# ALLOWED_HOSTS = [os.getenv('BACKEND_ORIGIN')] if __prod__ else ['.localhost', '127.0.0.1', '[::1]', '127.0.0.1:3000', 'localhost:3000'] +ALLOWED_HOSTS = ['*'] + +# Application definition + +INSTALLED_APPS = ['django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'corsheaders'] + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', + 'corsheaders.middleware.CorsMiddleware', + 'django.middleware.common.CommonMiddleware', +] + +ROOT_URLCONF = 'project.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'project.wsgi.application' + +# Database +# https://docs.djangoproject.com/en/5.1/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': BASE_DIR / 'db.sqlite3', + } +} + +# Password validation +# https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + +# Internationalization +# https://docs.djangoproject.com/en/5.1/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_TZ = True + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/5.1/howto/static-files/ + +STATIC_URL = 'static/' + +# Default primary key field type +# https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + +CORS_ALLOW_ALL_ORIGINS = True diff --git a/backend/project/urls.py b/backend/project/urls.py new file mode 100644 index 0000000..df25060 --- /dev/null +++ b/backend/project/urls.py @@ -0,0 +1,23 @@ +""" +URL configuration for project project. + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/5.1/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.contrib import admin +from django.urls import include, path + +urlpatterns = [ + path('admin/', admin.site.urls), + path('app/', include('app.urls')) +] diff --git a/backend/project/wsgi.py b/backend/project/wsgi.py new file mode 100644 index 0000000..aae984b --- /dev/null +++ b/backend/project/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for project project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/5.1/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'project.settings') + +application = get_wsgi_application() diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..6c2a928 --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,8 @@ +pandas +numpy +matplotlib +seaborn +statsmodels +datetime +django-cors-headers +python-dotenv diff --git a/frontend/.env.example b/frontend/.env.example new file mode 100644 index 0000000..5845b63 --- /dev/null +++ b/frontend/.env.example @@ -0,0 +1 @@ +NEXT_PUBLIC_BACKEND_ORIGIN= \ No newline at end of file diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..fd3dbb5 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,36 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js +.yarn/install-state.gz + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..36791f0 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1 @@ +## This is the Next.js frontend for our Data Science project. diff --git a/frontend/app/favicon.ico b/frontend/app/favicon.ico new file mode 100644 index 0000000..9320a8b Binary files /dev/null and b/frontend/app/favicon.ico differ diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx new file mode 100644 index 0000000..dda95a9 --- /dev/null +++ b/frontend/app/layout.tsx @@ -0,0 +1,23 @@ +import type { Metadata } from 'next' +import { Providers } from './providers' +import { Flex } from '@chakra-ui/react' + +export const metadata: Metadata = { + title: 'HSL Bike Helper', + description: 'An app that makes prediction based on Open Data from the Helsinki Bike System.', +} + +const RootLayout = ({ children, }: Readonly<{ children: React.ReactNode }>) => { + return ( + + + + + {children} + + + + + ) +} +export default RootLayout diff --git a/frontend/app/manifest.ts b/frontend/app/manifest.ts new file mode 100644 index 0000000..80c13c7 --- /dev/null +++ b/frontend/app/manifest.ts @@ -0,0 +1,26 @@ +import type { MetadataRoute } from 'next' + +const manifest = (): MetadataRoute.Manifest => { + return { + name: 'HSL Bike Helper', + short_name: 'HSL', + description: 'An app that makes prediction based on Open Data from the Helsinki Bike System.', + start_url: '/', + display: 'standalone', + background_color: '#ffffff', + theme_color: '#000000', + icons: [ + { + src: '/icon-192x192.png', + sizes: '192x192', + type: 'image/png', + }, + { + src: '/icon-512x512.png', + sizes: '512x512', + type: 'image/png', + }, + ], + } +} +export default manifest diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx new file mode 100644 index 0000000..be686dd --- /dev/null +++ b/frontend/app/page.tsx @@ -0,0 +1,78 @@ +'use client' + +import { InputField, Wrapper } from '@/components' +import { Box, Button, Flex, FormLabel, Select, Text } from '@chakra-ui/react' +import { Field, Form, Formik } from 'formik' +import { useState } from 'react' + +interface Response { + timestamp: string + station: string + departingCount: number + returningCount: number + bikeAtStationCount: number + increasing: boolean +} + +const Home: React.FC = () => { + const [predictionData, setPredictionData] = useState() + + return ( + + + { + const response: Response = await (await fetch(`${process.env.NEXT_PUBLIC_BACKEND_ORIGIN}/app/predict?timestamp=${timestamp}&station=${station}`)).json() + setPredictionData(response) + }} + > + {({ isSubmitting, values }) => ( +
+ + + {/* @ts-ignore */} + {({ field }) => ( + <> + Station + + + )} + + + Timestamp + + + + { + predictionData && + + Timestamp: {predictionData.timestamp} + Station: {predictionData.station} + Predicted Number of Bikes Arriving at Station: {predictionData.returningCount} + Predicted Number of Bikes Departing from Station: {predictionData.departingCount} + Predicted Number of Bikes at Station: {predictionData.bikeAtStationCount} + Predicted Bike Count Status: {predictionData.increasing ? 'Decreasing' : 'Increasing'} + + In need of more bikes:   + { + predictionData.bikeAtStationCount > 10 ? 'No' : + predictionData.bikeAtStationCount > 5 ? 'Yes, but not urgently.' : + 'Yes, urgently.' + } + + + } + + +
+ )} +
+
+
+ ) +} +export default Home diff --git a/frontend/app/providers.tsx b/frontend/app/providers.tsx new file mode 100644 index 0000000..e000593 --- /dev/null +++ b/frontend/app/providers.tsx @@ -0,0 +1,9 @@ +'use client' + +import { ChakraProvider } from '@chakra-ui/react' + +export const Providers = ({ children }: { children: React.ReactNode }) => { + return ( + {children} + ) +} diff --git a/frontend/components/InputField.tsx b/frontend/components/InputField.tsx new file mode 100644 index 0000000..d12b466 --- /dev/null +++ b/frontend/components/InputField.tsx @@ -0,0 +1,18 @@ +'use client' +import { FormControl, FormErrorMessage, FormLabel, Input, InputProps } from '@chakra-ui/react' +import { useField } from 'formik' + +type Props = React.InputHTMLAttributes & { name: string, label: string, isTextArea?: boolean } & InputProps + +export const InputField: React.FC = ({size: _, isTextArea=false, ...props}) => { + const [field, { error }] = useField(props) + const { label, placeholder } = props + + return ( + + {label} + + {error ? {error} : null} + + ) +} \ No newline at end of file diff --git a/frontend/components/TextareaField.tsx b/frontend/components/TextareaField.tsx new file mode 100644 index 0000000..ee8f022 --- /dev/null +++ b/frontend/components/TextareaField.tsx @@ -0,0 +1,36 @@ +'use client' +import { FormControl, FormErrorMessage, FormLabel, Textarea, TextareaProps } from '@chakra-ui/react' +import autosize from 'autosize' +import { useField } from 'formik' +import { useRef, useEffect } from 'react' + +type Props = React.TextareaHTMLAttributes & { name: string, label: string } & TextareaProps + +export const TextareaField: React.FC = (props): JSX.Element => { + const [field, { error }] = useField(props) + const { label, placeholder } = props + + // https://github.com/chakra-ui/chakra-ui/issues/670 + const ref: any = useRef() + useEffect(() => { + const current = ref.current + autosize(current) + return () => { + autosize.destroy(current) + } + }, []) + + return ( + + {label} +