diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..a317618 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,12 @@ +# Items that don't need to be in a Docker image. +# Anything not used by the build system should go here. +Dockerfile.backend +Dockerfile.frontend +.dockerignore +.gitignore +README.md + +# Artifacts that will be built during image creation. +# This should contain all files created during `npm run build`. +build +node_modules \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8c7ac77 --- /dev/null +++ b/.gitignore @@ -0,0 +1,343 @@ +# Created by https://www.toptal.com/developers/gitignore/api/visualstudiocode,python,node,svelte +# Edit at https://www.toptal.com/developers/gitignore?templates=visualstudiocode,python,node,svelte + +### Node ### +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +### Node Patch ### +# Serverless Webpack directories +.webpack/ + +# Optional stylelint cache + +# SvelteKit build / generate output +.svelte-kit + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +### Svelte ### +# gitignore template for the SvelteKit, frontend web component framework +# website: https://kit.svelte.dev/ + +.svelte-kit/ +package + +### VisualStudioCode ### +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history +.ionide + +# Support for Project snippet scope +.vscode/*.code-snippets + +# Ignore code-workspaces +*.code-workspace + +# End of https://www.toptal.com/developers/gitignore/api/visualstudiocode,python,node,svelte + +model/data/* +model/models/* +.python-version +!frontend/src/lib +ssl +osrm +backend/models/stations/*/*.png diff --git a/Dockerfile.backend b/Dockerfile.backend new file mode 100644 index 0000000..c9667e6 --- /dev/null +++ b/Dockerfile.backend @@ -0,0 +1,32 @@ +FROM python:3.9-slim AS development + +ENV POETRY_VERSION=1.1.14 + +RUN apt-get update && apt-get install -y build-essential python-dev + +WORKDIR /usr/src/app +COPY ./backend/pyproject.toml ./ + +RUN pip install --upgrade pip +RUN pip install poetry==${POETRY_VERSION} +RUN poetry install +RUN poetry export -f requirements.txt --without-hashes --output requirements.txt +RUN pip install -r requirements.txt +# RUN pip install -e . + +FROM tiangolo/uvicorn-gunicorn-fastapi:python3.9-slim + +ENV POETRY_VERSION=1.1.14 + +COPY ./backend /usr/src/app +COPY ./ssl /usr/src/app + +RUN apt-get update && apt-get install -y build-essential python-dev + +WORKDIR /usr/src/app + +RUN pip install --upgrade pip +RUN pip install poetry==${POETRY_VERSION} +RUN poetry export -f requirements.txt --without-hashes --output requirements.txt +RUN pip install --no-cache -r requirements.txt +RUN pip install -e . \ No newline at end of file diff --git a/Dockerfile.frontend b/Dockerfile.frontend new file mode 100644 index 0000000..b50d61a --- /dev/null +++ b/Dockerfile.frontend @@ -0,0 +1,37 @@ +FROM node:18-alpine3.15 as development + +WORKDIR /usr/src/app + +COPY ./frontend/package.json ./ + +RUN npm install -g vite +RUN npm install -g typescript +RUN yarn install + +ARG API_URL +ENV VITE_API_URL=http://localhost:8080/api +ENV VITE_OSRM_URL=http://localhost:5000 +EXPOSE 5173 + +# build +FROM node:18-alpine3.15 as build + +WORKDIR /usr/src/app + +COPY ./frontend ./ +COPY ./ssl/ ./etc/ssl + +RUN yarn install + +ARG API_URL +ENV VITE_API_URL=${API_URL} +ENV VITE_OSRM_URL=${OSRM_URL} +RUN npx browserslist@latest --update-db +RUN yarn run build + +# production environment +FROM nginx:1.23.1-alpine +COPY --from=build /usr/src/app/dist /usr/share/nginx/html +COPY --from=build /usr/src/app/etc/ssl ./etc/ssl +EXPOSE 80 443 +CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file diff --git a/Dockerfile.model b/Dockerfile.model new file mode 100644 index 0000000..56792cb --- /dev/null +++ b/Dockerfile.model @@ -0,0 +1,15 @@ +FROM python:3.9-slim + +ENV POETRY_VERSION=1.1.14 + +COPY ./model /code + +RUN apt-get update && apt-get install -y build-essential python-dev + +WORKDIR /code + +RUN pip install --upgrade pip +RUN pip install poetry==${POETRY_VERSION} +RUN poetry export -f requirements.txt --without-hashes --output requirements.txt +RUN pip install --no-cache -r requirements.txt +RUN pip install -e . \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..d33ec2d --- /dev/null +++ b/Makefile @@ -0,0 +1,24 @@ +requirements: + aptitude remove docker docker-engine docker.io containerd runc + apt-get update + apt-get install ca-certificates curl gnupg lsb-release + mkdir -p /etc/apt/keyrings + curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg + echo \ + "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian \ + $(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null + apt-get update + apt-get install docker-ce docker-ce-cli containerd.io docker-compose-plugin + + +osrm/washington-latest.osm.pbf: + mkdir -p osrm + wget http://download.geofabrik.de/north-america/us/washington-latest.osm.pbf + mv washington-latest.osm.pbf osrm/washington-latest.osm.pbf + +osrm/washington-latest.osrm.cell_metrics: osrm/washington-latest.osm.pbf + docker run -t -v "${PWD}/osrm:/data" osrm/osrm-backend osrm-extract -p /opt/car.lua /data/washington-latest.osm.pbf + docker run -t -v "${PWD}/osrm:/data" osrm/osrm-backend osrm-partition /data/washington-latest.osrm + docker run -t -v "${PWD}/osrm:/data" osrm/osrm-backend osrm-customize /data/washington-latest.osrm + +osrm: osrm/washington-latest.osrm.cell_metrics \ No newline at end of file diff --git a/backend/config.toml b/backend/config.toml new file mode 100644 index 0000000..a8bf22d --- /dev/null +++ b/backend/config.toml @@ -0,0 +1,46 @@ +[weather.api] +url = "https://api.open-meteo.com/v1/forecast" + +[weather.api.params] +latitude = 47.50 +longitude = -122.25 +current_weather = "true" + +[encoding.weekday_of_data] +0 = "MONDAY" +1 = "TUESDAY" +2 = "WEDNESDAY" +3 = "THURSDAY" +4 = "FRIDAY" +5 = "SATURDAY" +6 = "SUNDAY" + +[encoding.weather] +0 = "CLEAR" +1 = "CLEAR" +2 = "CLEAR" +3 = "OVERCAST" +45 = "OVERCAST" +48 = "OVERCAST" +51 = "RAIN" +53 = "RAIN" +55 = "RAIN" +56 = "RAIN" +57 = "RAIN" +61 = "RAIN" +63 = "RAIN" +65 = "RAIN" +66 = "RAIN" +67 = "RAIN" +71 = "SNOW" +73 = "SNOW" +75 = "SNOW" +77 = "SNOW" +80 = "RAIN" +81 = "RAIN" +82 = "RAIN" +85 = "SNOW" +86 = "SNOW" +95 = "RAIN" +96 = "RAIN" +99 = "RAIN" \ No newline at end of file diff --git a/backend/data/exposure.f b/backend/data/exposure.f new file mode 100644 index 0000000..47b6514 Binary files /dev/null and b/backend/data/exposure.f differ diff --git a/backend/data/frequency_by_hour.json b/backend/data/frequency_by_hour.json new file mode 100644 index 0000000..41dab42 --- /dev/null +++ b/backend/data/frequency_by_hour.json @@ -0,0 +1 @@ +{"0":0.0050900686,"1":0.0158174333,"2":0.0171484276,"3":0.0124435246,"4":0.006142888,"5":0.0064318456,"6":0.0026728109,"7":0.0045997093,"8":0.0043807541,"9":0.0058399343,"10":0.0042754031,"11":0.0050713551,"12":0.0068906961,"13":0.0069319682,"14":0.0065065502,"15":0.0069968891,"16":0.0066838066,"17":0.0080487877,"18":0.0061393715,"19":0.0060999034,"20":0.0081993719,"21":0.0083201329,"22":0.0100623108,"23":0.0095419165} \ No newline at end of file diff --git a/backend/data/frequency_by_weather.json b/backend/data/frequency_by_weather.json new file mode 100644 index 0000000..412eb49 --- /dev/null +++ b/backend/data/frequency_by_weather.json @@ -0,0 +1 @@ +{"CLEAR":0.0070853888,"OVERCAST":0.0041143967,"RAIN":0.0186074299,"SNOW":0.0235211105} \ No newline at end of file diff --git a/backend/main.py b/backend/main.py new file mode 100644 index 0000000..f0f5487 --- /dev/null +++ b/backend/main.py @@ -0,0 +1,101 @@ +import json +from fastapi import Body, FastAPI +from fastapi.middleware.cors import CORSMiddleware + +from src import ( + DATA_DIR, + get_frequency, + get_per_x_vehicles, + get_return_period, + get_severity, + get_time, + get_weather, + get_weekday, + load_exposure, + load_models, +) + +STATION_MODELS, STATION_CLASSIFIER, SEVERITY_MODEL = load_models() +EXPOSURE = load_exposure() + + +app = FastAPI() + +origins = ["https://riskyrouter.com/", "https://www.riskyrouter.com/", "http://localhost:5173", "http://0.0.0.0:5173"] + +app.add_middleware( + CORSMiddleware, + allow_origins=origins, + allow_methods=["GET", "POST"], + allow_headers=["*"], + allow_credentials=True, +) + + +@app.post("/api/frequency") +def frequency_route(payload: dict = Body(...)): + freq, median_daily_volume = get_frequency( + payload, STATION_MODELS, STATION_CLASSIFIER, EXPOSURE + ) + per_x_vehicles = get_per_x_vehicles(freq) + return_period = get_return_period(per_x_vehicles, median_daily_volume) + return_period_plus_1 = return_period + 1 + return { + "per_x_vehicles": per_x_vehicles, + "return_period": return_period, + "return_period_plus_1": return_period_plus_1, + } + + +@app.post("/api/severity") +def severity_route( + body_type: int, + vehicle_year: str, + alcohol: bool, + relative_speed: float, +): + prob = get_severity( + body_type=body_type, + vehicle_year=vehicle_year, + alcohol=alcohol, + relative_speed=relative_speed, + severity_model=SEVERITY_MODEL, + ) + return { + "injury_fatal_prob": prob, + } + + +@app.get("/api/weekday") +def weekday_route(): + return get_weekday() + + +@app.get("/api/time") +def time_route(): + return get_time() + + +@app.get("/api/weather") +def weather_route(): + return get_weather() + + +@app.get("/api/frequency_by_hour") +def hour_route(): + with open(DATA_DIR / "frequency_by_hour.json", "r") as f: + data = json.load(f) + + x_values = list(data.keys()) + y_values = list(data.values()) + return {"x": x_values, "y": y_values} + + +@app.get("/api/frequency_by_weather") +def weather_route(): + with open(DATA_DIR / "frequency_by_weather.json", "r") as f: + data = json.load(f) + + x_values = list(data.keys()) + y_values = list(data.values()) + return {"x": x_values, "y": y_values} \ No newline at end of file diff --git a/backend/models/severity/sev_model.joblib b/backend/models/severity/sev_model.joblib new file mode 100644 index 0000000..2b246e0 Binary files /dev/null and b/backend/models/severity/sev_model.joblib differ diff --git a/backend/models/station_id_classifier.joblib b/backend/models/station_id_classifier.joblib new file mode 100644 index 0000000..5f29f79 Binary files /dev/null and b/backend/models/station_id_classifier.joblib differ diff --git a/backend/models/stations/0/freq_model.joblib b/backend/models/stations/0/freq_model.joblib new file mode 100644 index 0000000..170bb92 Binary files /dev/null and b/backend/models/stations/0/freq_model.joblib differ diff --git a/backend/models/stations/1/freq_model.joblib b/backend/models/stations/1/freq_model.joblib new file mode 100644 index 0000000..e1ef46d Binary files /dev/null and b/backend/models/stations/1/freq_model.joblib differ diff --git a/backend/models/stations/10/freq_model.joblib b/backend/models/stations/10/freq_model.joblib new file mode 100644 index 0000000..12f151c Binary files /dev/null and b/backend/models/stations/10/freq_model.joblib differ diff --git a/backend/models/stations/11/freq_model.joblib b/backend/models/stations/11/freq_model.joblib new file mode 100644 index 0000000..418ce0f Binary files /dev/null and b/backend/models/stations/11/freq_model.joblib differ diff --git a/backend/models/stations/12/freq_model.joblib b/backend/models/stations/12/freq_model.joblib new file mode 100644 index 0000000..650fd2d Binary files /dev/null and b/backend/models/stations/12/freq_model.joblib differ diff --git a/backend/models/stations/13/freq_model.joblib b/backend/models/stations/13/freq_model.joblib new file mode 100644 index 0000000..56a539c Binary files /dev/null and b/backend/models/stations/13/freq_model.joblib differ diff --git a/backend/models/stations/14/freq_model.joblib b/backend/models/stations/14/freq_model.joblib new file mode 100644 index 0000000..846443b Binary files /dev/null and b/backend/models/stations/14/freq_model.joblib differ diff --git a/backend/models/stations/15/freq_model.joblib b/backend/models/stations/15/freq_model.joblib new file mode 100644 index 0000000..809713f Binary files /dev/null and b/backend/models/stations/15/freq_model.joblib differ diff --git a/backend/models/stations/16/freq_model.joblib b/backend/models/stations/16/freq_model.joblib new file mode 100644 index 0000000..76cf9b6 Binary files /dev/null and b/backend/models/stations/16/freq_model.joblib differ diff --git a/backend/models/stations/17/freq_model.joblib b/backend/models/stations/17/freq_model.joblib new file mode 100644 index 0000000..663d29a Binary files /dev/null and b/backend/models/stations/17/freq_model.joblib differ diff --git a/backend/models/stations/18/freq_model.joblib b/backend/models/stations/18/freq_model.joblib new file mode 100644 index 0000000..8c2fe9e Binary files /dev/null and b/backend/models/stations/18/freq_model.joblib differ diff --git a/backend/models/stations/19/freq_model.joblib b/backend/models/stations/19/freq_model.joblib new file mode 100644 index 0000000..8f5a43e Binary files /dev/null and b/backend/models/stations/19/freq_model.joblib differ diff --git a/backend/models/stations/2/freq_model.joblib b/backend/models/stations/2/freq_model.joblib new file mode 100644 index 0000000..8acd835 Binary files /dev/null and b/backend/models/stations/2/freq_model.joblib differ diff --git a/backend/models/stations/20/freq_model.joblib b/backend/models/stations/20/freq_model.joblib new file mode 100644 index 0000000..494c54e Binary files /dev/null and b/backend/models/stations/20/freq_model.joblib differ diff --git a/backend/models/stations/21/freq_model.joblib b/backend/models/stations/21/freq_model.joblib new file mode 100644 index 0000000..424c56b Binary files /dev/null and b/backend/models/stations/21/freq_model.joblib differ diff --git a/backend/models/stations/23/freq_model.joblib b/backend/models/stations/23/freq_model.joblib new file mode 100644 index 0000000..c6241fb Binary files /dev/null and b/backend/models/stations/23/freq_model.joblib differ diff --git a/backend/models/stations/24/freq_model.joblib b/backend/models/stations/24/freq_model.joblib new file mode 100644 index 0000000..12e8d14 Binary files /dev/null and b/backend/models/stations/24/freq_model.joblib differ diff --git a/backend/models/stations/25/freq_model.joblib b/backend/models/stations/25/freq_model.joblib new file mode 100644 index 0000000..bd1f99a Binary files /dev/null and b/backend/models/stations/25/freq_model.joblib differ diff --git a/backend/models/stations/26/freq_model.joblib b/backend/models/stations/26/freq_model.joblib new file mode 100644 index 0000000..6a75513 Binary files /dev/null and b/backend/models/stations/26/freq_model.joblib differ diff --git a/backend/models/stations/27/freq_model.joblib b/backend/models/stations/27/freq_model.joblib new file mode 100644 index 0000000..fcd6a99 Binary files /dev/null and b/backend/models/stations/27/freq_model.joblib differ diff --git a/backend/models/stations/28/freq_model.joblib b/backend/models/stations/28/freq_model.joblib new file mode 100644 index 0000000..78cef28 Binary files /dev/null and b/backend/models/stations/28/freq_model.joblib differ diff --git a/backend/models/stations/29/freq_model.joblib b/backend/models/stations/29/freq_model.joblib new file mode 100644 index 0000000..7c92c02 Binary files /dev/null and b/backend/models/stations/29/freq_model.joblib differ diff --git a/backend/models/stations/3/freq_model.joblib b/backend/models/stations/3/freq_model.joblib new file mode 100644 index 0000000..0de727d Binary files /dev/null and b/backend/models/stations/3/freq_model.joblib differ diff --git a/backend/models/stations/30/freq_model.joblib b/backend/models/stations/30/freq_model.joblib new file mode 100644 index 0000000..6ca98e4 Binary files /dev/null and b/backend/models/stations/30/freq_model.joblib differ diff --git a/backend/models/stations/31/freq_model.joblib b/backend/models/stations/31/freq_model.joblib new file mode 100644 index 0000000..be546d9 Binary files /dev/null and b/backend/models/stations/31/freq_model.joblib differ diff --git a/backend/models/stations/32/freq_model.joblib b/backend/models/stations/32/freq_model.joblib new file mode 100644 index 0000000..5e01cf1 Binary files /dev/null and b/backend/models/stations/32/freq_model.joblib differ diff --git a/backend/models/stations/33/freq_model.joblib b/backend/models/stations/33/freq_model.joblib new file mode 100644 index 0000000..1988c98 Binary files /dev/null and b/backend/models/stations/33/freq_model.joblib differ diff --git a/backend/models/stations/34/freq_model.joblib b/backend/models/stations/34/freq_model.joblib new file mode 100644 index 0000000..bd02776 Binary files /dev/null and b/backend/models/stations/34/freq_model.joblib differ diff --git a/backend/models/stations/35/freq_model.joblib b/backend/models/stations/35/freq_model.joblib new file mode 100644 index 0000000..46b33d1 Binary files /dev/null and b/backend/models/stations/35/freq_model.joblib differ diff --git a/backend/models/stations/36/freq_model.joblib b/backend/models/stations/36/freq_model.joblib new file mode 100644 index 0000000..935f25e Binary files /dev/null and b/backend/models/stations/36/freq_model.joblib differ diff --git a/backend/models/stations/37/freq_model.joblib b/backend/models/stations/37/freq_model.joblib new file mode 100644 index 0000000..757c08b Binary files /dev/null and b/backend/models/stations/37/freq_model.joblib differ diff --git a/backend/models/stations/4/freq_model.joblib b/backend/models/stations/4/freq_model.joblib new file mode 100644 index 0000000..dde70fc Binary files /dev/null and b/backend/models/stations/4/freq_model.joblib differ diff --git a/backend/models/stations/5/freq_model.joblib b/backend/models/stations/5/freq_model.joblib new file mode 100644 index 0000000..7c22c4e Binary files /dev/null and b/backend/models/stations/5/freq_model.joblib differ diff --git a/backend/models/stations/6/freq_model.joblib b/backend/models/stations/6/freq_model.joblib new file mode 100644 index 0000000..2a2aab5 Binary files /dev/null and b/backend/models/stations/6/freq_model.joblib differ diff --git a/backend/models/stations/7/freq_model.joblib b/backend/models/stations/7/freq_model.joblib new file mode 100644 index 0000000..62dc429 Binary files /dev/null and b/backend/models/stations/7/freq_model.joblib differ diff --git a/backend/models/stations/8/freq_model.joblib b/backend/models/stations/8/freq_model.joblib new file mode 100644 index 0000000..768ed7c Binary files /dev/null and b/backend/models/stations/8/freq_model.joblib differ diff --git a/backend/models/stations/9/freq_model.joblib b/backend/models/stations/9/freq_model.joblib new file mode 100644 index 0000000..93a1df5 Binary files /dev/null and b/backend/models/stations/9/freq_model.joblib differ diff --git a/backend/pyproject.toml b/backend/pyproject.toml new file mode 100644 index 0000000..6ffc577 --- /dev/null +++ b/backend/pyproject.toml @@ -0,0 +1,35 @@ +[tool.poetry] +name = "src" +version = "0.1.0" +description = "" +authors = ["Caesar Balona "] + +[tool.poetry.dependencies] +python = ">=3.9,<3.12" +pandas = "^1.4.4" +fastapi = "^0.83.0" +uvicorn = "^0.18.3" +scikit-learn = "^1.1.2" +pyarrow = "^9.0.0" +requests = "^2.28.1" +tomli = "^2.0.1" +imblearn = "^0.0" + +[tool.poetry.dev-dependencies] +autoflake = "*" +black = "*" +flake8 = "*" +flake8-bugbear = "*" +flake8-builtins = "*" +flake8-comprehensions = "*" +flake8-debugger = "*" +flake8-eradicate = "*" +isort = "*" +mypy = "*" +pep8-naming = "*" +pytest = "*" +tryceratops = "*" + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" diff --git a/backend/src/__init__.py b/backend/src/__init__.py new file mode 100644 index 0000000..1e3bbed --- /dev/null +++ b/backend/src/__init__.py @@ -0,0 +1,192 @@ +from datetime import datetime +from pathlib import Path +from statistics import harmonic_mean + +import joblib +import pandas as pd +import pytz +import requests +import tomli + +ROOT_DIR = Path(__file__).resolve().parents[1] + +DATA_DIR = ROOT_DIR / "data" # noqa: E221 + +MODELS_DIR = ROOT_DIR / "models" # noqa: E221 + +TIMEZONE = pytz.timezone("America/Los_Angeles") + +# Configuration +with open("config.toml", "rb") as f: + CONFIG = tomli.load(f) + + +def load_models(): + station_models = {} + for folder in (MODELS_DIR / "stations").iterdir(): + with open(f"{folder}/freq_model.joblib", "rb") as f: + model = joblib.load(f) + + station_models.update({folder.name: model}) + + with open(MODELS_DIR / "severity" / "sev_model.joblib", "rb") as f: + severity_model = joblib.load(f) + + severity_model.set_params(balancedrandomforestclassifier__n_jobs=1) + + with open(MODELS_DIR / "station_id_classifier.joblib", "rb") as f: + station_classifier = joblib.load(f) + + return station_models, station_classifier, severity_model + + +def load_exposure(): + exposure = pd.read_feather(DATA_DIR / "exposure.f") + exposure = ( + exposure.groupby( + [ + "StationId", + "StreetType", + "HourofData", + "WeekdayofData", + ] + ) + .agg( + SpeedLimitMPH=("SpeedLimitMPH", "median"), + TotalVolume=("TotalVolume", "mean"), + ) + .reset_index() + ) + + exposure["ActualTotalVolume"] = exposure.TotalVolume + exposure.TotalVolume = 1000 + + return exposure + + +def query_seattle_weather_data(): + api = CONFIG["weather"]["api"] + + query = { + "latitude": api["params"]["latitude"], + "longitude": api["params"]["longitude"], + "current_weather": "true", + # "timezone": api["params"]["timezone"], + } + return requests.get(api["url"], params=query) + + +def get_weather(): + code_map = CONFIG["encoding"]["weather"] + response = query_seattle_weather_data() + code = dict(response.json())["current_weather"]["weathercode"] + return code_map[str(int(code))] + + +def get_frequency(payload, station_models, station_classifier, exposure): + weekday_map = CONFIG["encoding"]["weekday_of_data"] + total_trip_time = payload["summary"]["totalTime"] + + time_of_request = datetime.now(tz=TIMEZONE) + weekday = weekday_map[str(time_of_request.weekday())] + hour = time_of_request.hour + + df = pd.DataFrame(payload["coordinates"]) + df.columns = ["Latitude", "Longitude"] + + df["StationId"] = station_classifier.predict(df) + + route_exposure = df.merge(exposure, on="StationId") + df = route_exposure.loc[ + (route_exposure.WeekdayofData == weekday) & (route_exposure.HourofData == hour) + ].copy() + + df["Weather"] = get_weather() + for station_id in df.StationId.unique(): + pred_df = df[df["StationId"] == station_id].copy() + freq = station_models[str(station_id)].predict(pred_df) + df.loc[df["StationId"] == station_id, "Freq"] = (freq / 60) * ( + total_trip_time / 60 + ) + + # Median daily exposure + median_hourly = ( + route_exposure[route_exposure.StreetType == "STREET"] + .groupby( + ["StationId", "StreetType", "HourofData", "WeekdayofData"], observed=True + )["ActualTotalVolume"] + .median() + .to_numpy() + ) + + median_daily_volume = sum(median_hourly) // 7 + + return harmonic_mean(df.Freq), median_daily_volume + + +def get_per_x_vehicles(frequency, exposure=1000): + return exposure // frequency + + +def integer_thousand_format(number): + return f"{number:,.0f}" + + +def percent_format(number): + if number < 0.01: + return f"< {0.01:.0%}" + return f"{number:.0%}" + + +def get_return_period(per_x_vehicles, daily_volume): + return_period = per_x_vehicles // daily_volume + return return_period + + +def get_severity( + body_type: int, + vehicle_year: str, + alcohol: bool, + relative_speed: float, + severity_model, +): + payload = { + "BodyType": body_type, + "VehicleYear": vehicle_year, + "Alcohol": int(alcohol), + "RelativeSpeed": relative_speed, + "Distraction": 0, + } + columns = [ + "HourofData", + "Weather", + "BodyType", + "VehicleYear", + "Alcohol", + "Distraction", + "RelativeSpeed", + ] + + time_of_request = datetime.now(tz=TIMEZONE) + hour = time_of_request.hour + + df = pd.DataFrame(payload, index=[0]) + df["Weather"] = get_weather() + df["HourofData"] = hour + df = df[columns] + + _, prob = severity_model.predict_proba(df)[0] + + return prob + + +def get_weekday(): + weekday_map = CONFIG["encoding"]["weekday_of_data"] + + time_of_request = datetime.now(tz=TIMEZONE) + return weekday_map[str(time_of_request.weekday())] + + +def get_time(): + time_of_request = datetime.now(tz=TIMEZONE) + return f"{time_of_request.time():%Hh%M}" diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..c0496d9 --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,47 @@ +version: "3.9" + +services: + osrm: + container_name: osrm + image: osrm/osrm-backend + restart: unless-stopped + volumes: + - type: bind + source: ./osrm + target: /opt/data + ports: + - "5000:5000" + command: osrm-routed --algorithm mld data/washington-latest.osrm + backend: + container_name: backend + restart: on-failure + build: + context: . + dockerfile: Dockerfile.backend + target: development + volumes: + - ./backend:/usr/src/app + ports: + - "8080:8080" + command: bash -c "pip install -e . && uvicorn main:app --host 0.0.0.0 --port 8080 --reload" + frontend: + container_name: frontend + restart: on-failure + build: + context: . + dockerfile: Dockerfile.frontend + target: development + args: + - API_URL=${API_URL} + - OSRM_URL=${OSRM_URL} + environment: + - CHOKIDAR_USEPOLLING=true + volumes: + - ./frontend:/usr/src/app + - /usr/src/app/node_modules + ports: + - 5173:5173 + command: yarn run dev + depends_on: + - backend + - osrm diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..3ffa1cd --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,49 @@ +version: "3.9" + +services: + osrm: + container_name: osrm + image: osrm/osrm-backend + restart: unless-stopped + volumes: + - type: bind + source: ./osrm + target: /opt/data + ports: + - '5000:5000' + command: + osrm-routed --algorithm mld data/washington-latest.osrm + backend: + container_name: backend + restart: unless-stopped + build: + context: . + dockerfile: Dockerfile.backend + volumes: + - backend_static_volume:/usr/src/app/static + ports: + - '8080:8080' + command: uvicorn main:app --host 0.0.0.0 --port 8080 #--ssl-keyfile=./cert.key --ssl-certfile=./cert.pem --ssl-cert-reqs=1 --ssl-ca-certs=./cloudflare.crt --reload + frontend: + container_name: frontend + restart: unless-stopped + build: + context: . + dockerfile: Dockerfile.frontend + args: + - API_URL=${API_URL} + - OSRM_URL=${OSRM_URL} + ports: + - 80:80 + - 443:443 + volumes: + - ./frontend/nginx:/etc/nginx/conf.d + - ./ssl:/etc/ssl + env_file: + - .env + depends_on: + - backend + - osrm + +volumes: + backend_static_volume: diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..7fcef62 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,28 @@ + + + + + + RiskyRouter + + + +
+ + + + + + + diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..a09e44b --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,30 @@ +{ + "name": "frontend", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview", + "check": "svelte-check --tsconfig ./tsconfig.json" + }, + "devDependencies": { + "@sveltejs/vite-plugin-svelte": "^1.0.2", + "@tsconfig/svelte": "^3.0.0", + "autoprefixer": "^10.4.12", + "postcss": "^8.4.16", + "svelte": "^3.49.0", + "svelte-check": "^2.8.1", + "svelte-preprocess": "^4.10.7", + "tailwindcss": "^3.1.8", + "tslib": "^2.4.0", + "typescript": "^4.6.4", + "vite": "^3.1.0" + }, + "dependencies": { + "leaflet": "^1.9.1", + "leaflet-control-geocoder": "^2.4.0", + "leaflet-routing-machine": "^3.2.12" + } +} diff --git a/frontend/postcss.config.cjs b/frontend/postcss.config.cjs new file mode 100644 index 0000000..33ad091 --- /dev/null +++ b/frontend/postcss.config.cjs @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/frontend/src/App.svelte b/frontend/src/App.svelte new file mode 100644 index 0000000..55aaa9a --- /dev/null +++ b/frontend/src/App.svelte @@ -0,0 +1,508 @@ + + + + +
+
+
+

RiskyRouter

+
+
+ About +
+
+
+
+
+
+
+
+
+
+
+
+ Based on the weather, week day, and time of day... +
+ +
+ +
+
+ At least + 1 + vehicle involved in an accident on this route for every + ... + vehicles +
+
+ +
+
+ An accident occurs every + ... + to + ... + days on routes similar to this based on its median daily traffic volume +
+ +
+ If you happen to be in an accident... +
+ + +
+ +
+
+ It may result in serious injury or fatality with at least + {formatted_injury_fatal_prob} + probability. +
+
+
+
Vehicle Year
+ +
Car
+ +
+ Relative Speed +
+ +
Drinking?
+ +
+
+
+

+ The values given above are based on limited data and approximate + models and should not be used to justify risky behaviour. In some + rare cases, probabilities will be inconsistent. It is irrefutable + that increased speed kills + and that + drinking and driving kills and is illegal. +

+
+
+
+
+
+ {#if user.relative_speed > speeding_base} +
+
+ +
+
+ {#if injury_fatal_prob_speeding_delta > 0.01} + Excessive speeding increases the risk of serious injury or + fatality by {formatted_injury_fatal_prob_speeding_delta}! Take it easy and stick to the recommended speed limits. + {:else} + Excessive speeding increases the risk of an accident! Take it + easy and stick to the recommended speed limits. + {/if} +
+
+ {:else if user.relative_speed > limit_base} +
+
+ +
+
+ {#if injury_fatal_prob_speeding_delta > 0.01} + Speeding increases the risk of serious injury or fatality by {formatted_injury_fatal_prob_speeding_delta}! Take it easy and stick to the recommended speed limits. + {:else} + Speeding increases the risk of an accident! Take it easy and + stick to the recommended speed limits. + {/if} +
+
+ {:else} +
+
+ +
+
+ {#if injury_fatal_prob_speeding_delta < -0.01} + Good job on sticking to the speed limit! Excessive speeding + increases the risk of serious injury or fatality by {formatted_injury_fatal_prob_speeding_delta}. + {:else} + Good job on sticking to the speed limit! Excessive speeding + increases the risk of an accident. + {/if} +
+
+ {/if} +
+
+
+ {#if user.alcohol} +
+
+ +
+
+ {#if injury_fatal_prob_alcohol_delta > 0.01} + Never drink and drive. Using a designated driver will reduce + serious injury or fatality risk in the event of an accident by {formatted_injury_fatal_prob_alcohol_delta}. + {:else} + Never drink and drive. Using a designated driver will reduce + the risk of an accident. + {/if} +
+
+ {:else} +
+
+ +
+
+ {#if injury_fatal_prob_alcohol_delta < -0.01} + Good job on staying sober or having a designated driver! + Driving under the influence increases serious injury or + fatality risk by {formatted_injury_fatal_prob_alcohol_delta}. + {:else} + Good job on staying sober or having a designated driver! Using + a designated driver will reduce the risk of an accident. + {/if} +
+
+ {/if} +
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+ + diff --git a/frontend/src/app.css b/frontend/src/app.css new file mode 100644 index 0000000..2e43686 --- /dev/null +++ b/frontend/src/app.css @@ -0,0 +1,58 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +.leaflet-routing-container { + padding: 0.5rem !important; +} + +.leaflet-routing-geocoder > input { + padding: 0.5rem !important; +} + +.leaflet-routing-add-waypoint { + padding: 0.25rem 0.5rem 0.25rem 0.5rem; +} + +.tooltip { + position: relative; /* making the .tooltip span a container for the tooltip text */ + z-index: 10000; +} + +.tooltip:before { + content: attr(data-text); /* here's the magic */ + position: absolute; + + /* vertically center */ + top: 0%; + transform: translateY(-100%); + + /* move to right */ + left: 100%; + margin-left: 15px; /* and add a small left margin */ + + /* basic styles */ + width: 500px; + padding: 10px; + border-radius: 10px; + background: #1d3461; + color: #fff; + text-align: center; + + display: none; /* hide by default */ +} + +.tooltip.left:before { + /* reset defaults */ + left: initial; + margin: initial; + + /* set new values */ + right: 0%; + margin-right: 15px; +} + +.tooltip:hover:before { + display: block; + z-index: 10000; +} diff --git a/frontend/src/lib/About.svelte b/frontend/src/lib/About.svelte new file mode 100644 index 0000000..f49301f --- /dev/null +++ b/frontend/src/lib/About.svelte @@ -0,0 +1,357 @@ + + +
+
+
+
+

+ About RiskyRouter +

+

+ is a route mapping software that provides an estimate of + the accident risk of a chosen route and the risk of serious injury or fatality + should an accident occur under certain conditions within the city of Seattle. + was created by for the + . +

+
+

+ All the work is available open-source under the MPL-2.0 license as per the + competition guidelines. My hope for the project is that it is a + demonstration of what actuaries with access to the necessary skills can do + not only for insurers but also for the public. It is not necessary for one + individual actuary to have all the skills required, as individual skills + can be found in several individuals and their efforts combined. An actuary + with only actuarial skills will at least know what is possible and what + skills to seek out. Further, I hope it serves as a resource for actuaries, + data scientists, software developers, and web developers to use when + building similar applications. +

+ +
+ +

+ How Works +

+

+ is made up of 4 components: +

+
    +
  1. + Frequency and severity models based on traffic and accident data from + the city of Seattle, as well as additional accident data from the USA. +
  2. +
  3. A route mapping engine.
  4. +
  5. A backend API to facilitate queries to the underlying models.
  6. +
  7. A frontend web interface.
  8. +
+ +
+ +

+ 1. Frequency and Severity Models +

+

+ Several models have been built to determine the frequency of accidents + based on their geographic locations, and one model has been built to + calculate the probability of serious injury or death in the event of an + accident given several conditions. +

+
+

Frequency Modelling

+ +

+ Data were collected from which provides open access data for the city of Seattle including GIS data. + The data collected are: +

+
+ + + + + + + + + + + + + + + + + + + + + +
DataDetail
Traffic volume in 5 minute intervals from select locations around + Seattle.
Streets data includes Arterial Classification, Street Names, Block + Number, Direction, One-way, Surface Width, Surface Type, Pavement + Condition, Speed Limit, Percent Slope. The street data was primarily + used to determine if a given coordinate is a street or intersection.
Detail on traffic collisions in the city of Seattle including + geographic coordinate.
+ +
+ +

+ In addition, historic weather data was collected from OpenMeteo and + included in the volume data to determine weather at the time of traffic + volume recording. +

+ +

+ The data are downloaded programmatically from the Seattle Open Data + website. Thereafter they are transformed into the format needed for the + modelling. +

+ +

+ Traffic volume data is collected at certain predetermined locations around + Seattle, and hence volume is not known for all possible streets and + routes. K-Nearest Neighbours was used to classify street point coordinates + to their nearest traffic volume recording station. This way, every segment + on every street (including intersections) will have an associated traffic + volume at a given time, on a given day, under certain weather conditions. + This traffic volume is used as the exposure metric in the frequency model. +

+ +

+ Collision data maps directly to street coordinate data, and hence + collision data can easily be joined with volume data by coordinate, date + and time. This creates the final dataset used to build a frequency model. +

+ +

+ For the full underlying code of how the frequency data is built, refer to + model/src/data/seattle. +

+ +

+ Fitting a single frequency model to all the data is not ideal, as this + will result in heterogenous variance as it is not expected that the + frequency distribution is the same at all stations. Instead, a separate + frequency model is fit for each station where volume is recorded. This is + beneficial, as it is expected areas in the central business district will + exhibit different frequency behaviour to suburban areas. Hence, the model + first uses a decision tree to select the appropriate model to use given + proximity to a traffic volume station, and then predicts frequency using + that model. This means that a chosen route may use several different + frequency prediction models. +

+ +

+ For each model, a gradient boosting machine (GBM) is used to predict + frequency. Several models were assessed, from basic GLMs, to random + forest, and even nearest-neighbour-based approaches. However, the GBM + performed the best overall, and handled the imbalance inherent in the data + most appropriately. (Note there is significant imbalance as data is + hourly, and in most cases, 97-98% of the data has no recorded collisions). +

+ +

+ Details on the model can be found in model/src/models/frequency.py +

+ +

Severity Modelling

+

+ Separate data is used for severity modelling, as the collision data + provided by Seattle has limited features on which to predict. Collision + data was collected from the National Highway Traffic Safety Administration + Crash Report Sampling System. The data are transformed for modelling. This + detail can be found in model/src/data/crss. The objective is to predict + whether an accident results in serious injury or death. Again, several + models were assessed, with the best performing being a balanced random + forest, due to the imbalance in the data, where most accidents result in + minor injury or no injury. Details on the model can be found in + model/src/models/severity.py +

+
+

+ The Route Mapping Engine +

+

+ The route mapping engine used is OSRM and is limited to the state of + Washington. OSRM is a C++ implementation of a high-performance routing + engine to find the shortest paths in a road network. The elements of the + route are passed to the frequency model to determine frequency of + accidents along a route and are adjusted to the time travelled (as the + exposure data is per hour). +

+
+

Backend API

+

+ The backend API is the link between the models and the webpage and routing + module. The backend API feeds the routing and webpage data to the + appropriate model, as well as fetches the current time and weather + conditions in Seattle. +

+

+ The backend is built using FastAPI, an open-source python package for + fast, scalable APIs. +

+

+ The existence of an API also allows programmatic access to the models. For + example, an insurer can bulk query the API using policy information to + determine the riskiness of routes travelled by policyholders. Or, an + insurer can query the API using their existing rating software at quote + state to determine a premium, without needing to access the frontend web + interface. +

+
+

+ Frontend Web Interface +

+

+ The frontend web interface is what the end-user interacts with and + surfaces all the previous components in one easy to use location. +

+

+ The top bar directly below the title gives the current weather, weekday, + and time in Seattle. The middle left block houses the routing interface + where routes can be found similar to google maps. The middle right + provides the frequency and severity of a given route, and allows the user + to change additional items such as age, car, and speed. The bottom + provides some additional information to advise on safer options. +

+

+ The frontend is built using Javascript and hosted on a server at + riskyrouter.com. The deployment is performed using Docker. For the actual + website, I have served it using nginx and provided secured certificates. + Anyone can clone the github repository and host the website on their own + server in exactly the same manner, needing only to provide their own + domain name, server, and server configuration. Docker removes all the + admin of deployment except for these final parts which are unique to every + deployment. Individual items can be deployed separately. For example, an + insurer can deploy only the API and routing module within their company to + query the API locally. +

+
+ +

Use Cases

+

has many uses cases for different users.

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
UserUse Case
General public in SeattleUnderstanding the riskiness of their commute or any travel they may + undertake within Seattle and opt to alter travel arrangements to + reduce risk.
General publicUnderstand the risk of serious injury or death in the event of an + accident based on certain conditions such as weather, time of day, + speed, age, vehicle year, and whether they have been drinking.
GovernmentsEducate the public on the dangers of drunk driving or speeding, and + importance of having safer and more recent vehicles. Focused + initiatives to reduce accident frequency on hotspot routes. +
Personal auto insurersDetermine the accident frequency of existing or potential + policyholders based on their frequent commutes and use this to offer + discounts, determine premiums, or alter behaviour to reduce risk. + This may be a more palatable option than full in-vehicle telematics. + For example, two individuals may have exactly the same + characteristics, but one may have a much lower risk travel profile + than the other, and should then be attracted as a risk with a lower + premium.
Health and life insurersUnderstand the serious injury and fatality risk of policyholders + based on their frequent commutes.
+ +
+ +

Limitations

+

+ Being based on statistical models, obviously has several limitations: +

+
    +
  • + Class imbalance was a major problem in the modelling, and all results + should be interpreted with this in mind. +
  • +
  • + As models are only estimates of reality, they should not be taken as + absolute truth, but only indications of relationships found within the + data. +
  • +
  • + I do not recommend RiskyRouter be used for for anything more than + interest sake or demonstration. To use it for actual production purposes + would require significant additional development as well as peer review + and sign off by appropriately qualified individuals (such as a statutory + actuary). +
  • +
+
+
+
diff --git a/frontend/src/lib/HourFrequency.svelte b/frontend/src/lib/HourFrequency.svelte new file mode 100644 index 0000000..236d9a8 --- /dev/null +++ b/frontend/src/lib/HourFrequency.svelte @@ -0,0 +1,42 @@ + + +
+
Frequency of Accidents by Hour of Day
+ +
diff --git a/frontend/src/lib/Leaflet.svelte b/frontend/src/lib/Leaflet.svelte new file mode 100644 index 0000000..3f280bc --- /dev/null +++ b/frontend/src/lib/Leaflet.svelte @@ -0,0 +1,135 @@ + + + + + + + + + + + + + diff --git a/frontend/src/lib/Link.svelte b/frontend/src/lib/Link.svelte new file mode 100644 index 0000000..6a56f75 --- /dev/null +++ b/frontend/src/lib/Link.svelte @@ -0,0 +1,6 @@ + + +{text} diff --git a/frontend/src/lib/RiskyRouterName.svelte b/frontend/src/lib/RiskyRouterName.svelte new file mode 100644 index 0000000..09ed28b --- /dev/null +++ b/frontend/src/lib/RiskyRouterName.svelte @@ -0,0 +1 @@ +RiskyRouter \ No newline at end of file diff --git a/frontend/src/lib/Time.svelte b/frontend/src/lib/Time.svelte new file mode 100644 index 0000000..2847609 --- /dev/null +++ b/frontend/src/lib/Time.svelte @@ -0,0 +1,26 @@ + + +
+  {time} +
diff --git a/frontend/src/lib/Weather.svelte b/frontend/src/lib/Weather.svelte new file mode 100644 index 0000000..a05c71a --- /dev/null +++ b/frontend/src/lib/Weather.svelte @@ -0,0 +1,35 @@ + + +
+ {#if weather === "Clear"} + + {:else if weather === "Overcast"} + + {:else if weather === "Rain"} + + {:else} + + {/if} +  {weather} +
diff --git a/frontend/src/lib/WeatherFrequency.svelte b/frontend/src/lib/WeatherFrequency.svelte new file mode 100644 index 0000000..e433e3d --- /dev/null +++ b/frontend/src/lib/WeatherFrequency.svelte @@ -0,0 +1,42 @@ + + +
+
Frequency of Accidents by Weather
+ +
diff --git a/frontend/src/lib/Weekday.svelte b/frontend/src/lib/Weekday.svelte new file mode 100644 index 0000000..30951ee --- /dev/null +++ b/frontend/src/lib/Weekday.svelte @@ -0,0 +1,26 @@ + + +
+  {weekday} +
diff --git a/frontend/src/main.ts b/frontend/src/main.ts new file mode 100644 index 0000000..5c1f795 --- /dev/null +++ b/frontend/src/main.ts @@ -0,0 +1,8 @@ +import './app.css' +import App from './App.svelte' + +const app = new App({ + target: document.getElementById('app') +}) + +export default app diff --git a/frontend/src/vite-env.d.ts b/frontend/src/vite-env.d.ts new file mode 100644 index 0000000..4078e74 --- /dev/null +++ b/frontend/src/vite-env.d.ts @@ -0,0 +1,2 @@ +/// +/// diff --git a/frontend/svelte.config.js b/frontend/svelte.config.js new file mode 100644 index 0000000..6c578ae --- /dev/null +++ b/frontend/svelte.config.js @@ -0,0 +1,11 @@ +import preprocess from "svelte-preprocess"; + +export default { + // Consult https://github.com/sveltejs/svelte-preprocess + // for more information about preprocessors + preprocess: [ + preprocess({ + postcss: true, + }), + ], +} diff --git a/frontend/tailwind.config.cjs b/frontend/tailwind.config.cjs new file mode 100644 index 0000000..6019973 --- /dev/null +++ b/frontend/tailwind.config.cjs @@ -0,0 +1,17 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: ["index.html"], + theme: { + extend: { + colors: { + 'space-cadet': '#1D3461', + 'yale-blue': '#1F487E', + 'cg-blue': '#247BA0', + 'granite-gray': '#605F5E', + 'red-salsa': '#FB3640' + }, + }, + }, + plugins: [], + content: ["./index.html",'./src/**/*.{svelte,js,ts}'] +} diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..d383031 --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "@tsconfig/svelte/tsconfig.json", + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "module": "ESNext", + "resolveJsonModule": true, + "baseUrl": ".", + /** + * Typecheck JS in `.svelte` and `.js` files by default. + * Disable checkJs if you'd like to use dynamic types in JS. + * Note that setting allowJs false does not prevent the use + * of JS in `.svelte` files. + */ + "allowJs": true, + "checkJs": true, + "isolatedModules": true + }, + "include": ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.js", "src/**/*.svelte"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/frontend/tsconfig.node.json b/frontend/tsconfig.node.json new file mode 100644 index 0000000..65dbdb9 --- /dev/null +++ b/frontend/tsconfig.node.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "composite": true, + "module": "ESNext", + "moduleResolution": "Node" + }, + "include": ["vite.config.ts"] +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts new file mode 100644 index 0000000..1c09fd3 --- /dev/null +++ b/frontend/vite.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from "vite"; +import { svelte } from "@sveltejs/vite-plugin-svelte"; +// import basicSsl from "@vitejs/plugin-basic-ssl"; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [svelte()], + server: { + // https: true, + host: true, + }, + watch: { + usePolling: true, + }, +}); diff --git a/model/config.toml b/model/config.toml new file mode 100644 index 0000000..2b00a7e --- /dev/null +++ b/model/config.toml @@ -0,0 +1,50 @@ +start_date = 2021-09-01 # Inclusive +end_date = 2022-08-31 # Inclusive + +[data.hour_bins] +EARLY_MORNING = [0, 6] +MORNING_RUSH = [6, 9] +MORNING_QUIET = [9, 12] +MIDDAY_BREAK = [12, 15] +EVENING_RUSH = [15, 19] +NIGHT = [19, 24] + +[data.vehicle_year_bins] +PRE_2000 = [1900, 2000] +2000_TO_2010 = [2000, 2010] +2010_TO_CURRENT = [2010, 2030] + +[data.age_bins] +16_TO_21 = [16, 21] +21_TO_25 = [21, 25] +25_TO_30 = [25, 30] +30_TO_45 = [30, 45] +45_TO_65 = [45, 65] +65_PLUS = [65, 120] + +[data.speeding_bins] +BELOW_SPEED_LIMIT = [0, 0.9] +WITHIN_SPEED_LIMIT = [0.9, 1.1] +SPEEDING = [1.1, 1.2] +EXCESSIVE_SPEEDING = [1.2, 10] + +[weather.api] +url = "https://archive-api.open-meteo.com/v1/era5" + +[weather.api.params] +latitude = 47.50 +longitude = -122.25 +start_date = 2021-09-01 +end_date = 2022-08-31 +hourly = ["rain", "snowfall", "cloudcover"] + +[weather.classification] +# Classify weather based on recorded metrics +# Order of precedence if multiple classes: +# CLEAR < OVERCAST < FOG < RAIN < SNOW +rainfall = 1.5 # Considered moderate raining if mm >= this number +snowfall = 1.5 # Considered snowfall if mm >= this number +cloudcover = 0.95 # Considered overcast if % >= this number + +[gbm] +min_volume = 60 diff --git a/model/poetry.lock b/model/poetry.lock new file mode 100644 index 0000000..b8203fb --- /dev/null +++ b/model/poetry.lock @@ -0,0 +1,1336 @@ +[[package]] +name = "attrs" +version = "22.1.0" +description = "Classes Without Boilerplate" +category = "dev" +optional = false +python-versions = ">=3.5" + +[package.extras] +dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"] +docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] +tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] +tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "cloudpickle"] + +[[package]] +name = "autoflake" +version = "1.6.1" +description = "Removes unused imports and unused variables" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +pyflakes = ">=1.1.0" +tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""} + +[[package]] +name = "beautifulsoup4" +version = "4.11.1" +description = "Screen-scraping library" +category = "main" +optional = false +python-versions = ">=3.6.0" + +[package.dependencies] +soupsieve = ">1.2" + +[package.extras] +html5lib = ["html5lib"] +lxml = ["lxml"] + +[[package]] +name = "black" +version = "22.8.0" +description = "The uncompromising code formatter." +category = "dev" +optional = false +python-versions = ">=3.6.2" + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +pathspec = ">=0.9.0" +platformdirs = ">=2" +tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""} +typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.7.4)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + +[[package]] +name = "certifi" +version = "2022.9.24" +description = "Python package for providing Mozilla's CA Bundle." +category = "main" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "charset-normalizer" +version = "2.1.1" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +category = "main" +optional = false +python-versions = ">=3.6.0" + +[package.extras] +unicode_backport = ["unicodedata2"] + +[[package]] +name = "click" +version = "8.1.3" +description = "Composable command line interface toolkit" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.5" +description = "Cross-platform colored terminal text." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "commonmark" +version = "0.9.1" +description = "Python parser for the CommonMark Markdown spec" +category = "dev" +optional = false +python-versions = "*" + +[package.extras] +test = ["hypothesis (==3.55.3)", "flake8 (==3.7.8)"] + +[[package]] +name = "contourpy" +version = "1.0.5" +description = "Python library for calculating contours of 2D quadrilateral grids" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +numpy = ">=1.16" + +[package.extras] +test-no-codebase = ["pillow", "matplotlib", "pytest"] +test-minimal = ["pytest"] +test = ["isort", "flake8", "pillow", "matplotlib", "pytest"] +docs = ["sphinx-rtd-theme", "sphinx", "docutils (<0.18)"] +bokeh = ["selenium", "bokeh"] + +[[package]] +name = "cycler" +version = "0.11.0" +description = "Composable style cycles" +category = "main" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "eradicate" +version = "2.1.0" +description = "Removes commented-out code." +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "flake8" +version = "5.0.4" +description = "the modular source code checker: pep8 pyflakes and co" +category = "dev" +optional = false +python-versions = ">=3.6.1" + +[package.dependencies] +mccabe = ">=0.7.0,<0.8.0" +pycodestyle = ">=2.9.0,<2.10.0" +pyflakes = ">=2.5.0,<2.6.0" + +[[package]] +name = "flake8-bugbear" +version = "22.9.23" +description = "A plugin for flake8 finding likely bugs and design problems in your program. Contains warnings that don't belong in pyflakes and pycodestyle." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +attrs = ">=19.2.0" +flake8 = ">=3.0.0" + +[package.extras] +dev = ["coverage", "hypothesis", "hypothesmith (>=0.2)", "pre-commit"] + +[[package]] +name = "flake8-builtins" +version = "1.5.3" +description = "Check for python builtins being used as variables or parameters." +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +flake8 = "*" + +[package.extras] +test = ["coverage", "coveralls", "mock", "pytest", "pytest-cov"] + +[[package]] +name = "flake8-comprehensions" +version = "3.10.0" +description = "A flake8 plugin to help you write better list/set/dict comprehensions." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +flake8 = ">=3.0,<3.2.0 || >3.2.0" + +[[package]] +name = "flake8-debugger" +version = "4.1.2" +description = "ipdb/pdb statement checker plugin for flake8" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +flake8 = ">=3.0" +pycodestyle = "*" + +[[package]] +name = "flake8-eradicate" +version = "1.4.0" +description = "Flake8 plugin to find commented out code" +category = "dev" +optional = false +python-versions = ">=3.7,<4.0" + +[package.dependencies] +attrs = "*" +eradicate = ">=2.0,<3.0" +flake8 = ">=3.5,<6" + +[[package]] +name = "fonttools" +version = "4.37.3" +description = "Tools to manipulate font files" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.extras] +all = ["fs (>=2.2.0,<3)", "lxml (>=4.0,<5)", "zopfli (>=0.1.4)", "lz4 (>=1.7.4.2)", "matplotlib", "sympy", "skia-pathops (>=0.5.0)", "uharfbuzz (>=0.23.0)", "brotlicffi (>=0.8.0)", "scipy", "brotli (>=1.0.1)", "munkres", "unicodedata2 (>=14.0.0)", "xattr"] +graphite = ["lz4 (>=1.7.4.2)"] +interpolatable = ["scipy", "munkres"] +lxml = ["lxml (>=4.0,<5)"] +pathops = ["skia-pathops (>=0.5.0)"] +plot = ["matplotlib"] +repacker = ["uharfbuzz (>=0.23.0)"] +symfont = ["sympy"] +type1 = ["xattr"] +ufo = ["fs (>=2.2.0,<3)"] +unicode = ["unicodedata2 (>=14.0.0)"] +woff = ["zopfli (>=0.1.4)", "brotlicffi (>=0.8.0)", "brotli (>=1.0.1)"] + +[[package]] +name = "idna" +version = "3.4" +description = "Internationalized Domain Names in Applications (IDNA)" +category = "main" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "iniconfig" +version = "1.1.1" +description = "iniconfig: brain-dead simple config-ini parsing" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "isort" +version = "5.10.1" +description = "A Python utility / library to sort Python imports." +category = "dev" +optional = false +python-versions = ">=3.6.1,<4.0" + +[package.extras] +pipfile_deprecated_finder = ["pipreqs", "requirementslib"] +requirements_deprecated_finder = ["pipreqs", "pip-api"] +colors = ["colorama (>=0.4.3,<0.5.0)"] +plugins = ["setuptools"] + +[[package]] +name = "joblib" +version = "1.2.0" +description = "Lightweight pipelining with Python functions" +category = "main" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "kiwisolver" +version = "1.4.4" +description = "A fast implementation of the Cassowary constraint solver" +category = "main" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "loguru" +version = "0.6.0" +description = "Python logging made (stupidly) simple" +category = "main" +optional = false +python-versions = ">=3.5" + +[package.dependencies] +colorama = {version = ">=0.3.4", markers = "sys_platform == \"win32\""} +win32-setctime = {version = ">=1.0.0", markers = "sys_platform == \"win32\""} + +[package.extras] +dev = ["sphinx-rtd-theme (>=0.4.3)", "sphinx-autobuild (>=0.7.1)", "Sphinx (>=4.1.1)", "isort (>=5.1.1)", "black (>=19.10b0)", "pytest-cov (>=2.7.1)", "pytest (>=4.6.2)", "tox (>=3.9.0)", "flake8 (>=3.7.7)", "docutils (==0.16)", "colorama (>=0.3.4)"] + +[[package]] +name = "matplotlib" +version = "3.6.0" +description = "Python plotting package" +category = "main" +optional = false +python-versions = ">=3.8" + +[package.dependencies] +contourpy = ">=1.0.1" +cycler = ">=0.10" +fonttools = ">=4.22.0" +kiwisolver = ">=1.0.1" +numpy = ">=1.19" +packaging = ">=20.0" +pillow = ">=6.2.0" +pyparsing = ">=2.2.1" +python-dateutil = ">=2.7" +setuptools_scm = ">=7" + +[[package]] +name = "mccabe" +version = "0.7.0" +description = "McCabe checker, plugin for flake8" +category = "dev" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "mypy" +version = "0.971" +description = "Optional static typing for Python" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +mypy-extensions = ">=0.4.3" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = ">=3.10" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +python2 = ["typed-ast (>=1.4.0,<2)"] +reports = ["lxml"] + +[[package]] +name = "mypy-extensions" +version = "0.4.3" +description = "Experimental type system extensions for programs checked with the mypy typechecker." +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "numpy" +version = "1.23.3" +description = "NumPy is the fundamental package for array computing with Python." +category = "main" +optional = false +python-versions = ">=3.8" + +[[package]] +name = "packaging" +version = "21.3" +description = "Core utilities for Python packages" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" + +[[package]] +name = "pandas" +version = "1.5.0" +description = "Powerful data structures for data analysis, time series, and statistics" +category = "main" +optional = false +python-versions = ">=3.8" + +[package.dependencies] +numpy = [ + {version = ">=1.21.0", markers = "python_version >= \"3.10\""}, + {version = ">=1.20.3", markers = "python_version < \"3.10\""}, +] +python-dateutil = ">=2.8.1" +pytz = ">=2020.1" + +[package.extras] +test = ["pytest-xdist (>=1.31)", "pytest (>=6.0)", "hypothesis (>=5.5.3)"] + +[[package]] +name = "pathspec" +version = "0.10.1" +description = "Utility library for gitignore style pattern matching of file paths." +category = "dev" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "pep8-naming" +version = "0.13.2" +description = "Check PEP-8 naming conventions, plugin for flake8" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +flake8 = ">=3.9.1" + +[[package]] +name = "pillow" +version = "9.2.0" +description = "Python Imaging Library (Fork)" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.extras] +docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-issues (>=3.0.1)", "sphinx-removed-in", "sphinxext-opengraph"] +tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] + +[[package]] +name = "platformdirs" +version = "2.5.2" +description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.extras] +docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)", "sphinx (>=4)"] +test = ["appdirs (==1.4.4)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)", "pytest (>=6)"] + +[[package]] +name = "pluggy" +version = "1.0.0" +description = "plugin and hook calling mechanisms for python" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.extras] +testing = ["pytest-benchmark", "pytest"] +dev = ["tox", "pre-commit"] + +[[package]] +name = "py" +version = "1.11.0" +description = "library with cross-python path, ini-parsing, io, code, log facilities" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "pyarrow" +version = "9.0.0" +description = "Python library for Apache Arrow" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +numpy = ">=1.16.6" + +[[package]] +name = "pycodestyle" +version = "2.9.1" +description = "Python style guide checker" +category = "dev" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "pyflakes" +version = "2.5.0" +description = "passive checker of Python programs" +category = "dev" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "pygments" +version = "2.13.0" +description = "Pygments is a syntax highlighting package written in Python." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.extras] +plugins = ["importlib-metadata"] + +[[package]] +name = "pyparsing" +version = "3.0.9" +description = "pyparsing module - Classes and methods to define and execute parsing grammars" +category = "main" +optional = false +python-versions = ">=3.6.8" + +[package.extras] +diagrams = ["railroad-diagrams", "jinja2"] + +[[package]] +name = "pytest" +version = "7.1.3" +description = "pytest: simple powerful testing with Python" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +attrs = ">=19.2.0" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" +py = ">=1.8.2" +tomli = ">=1.0.0" + +[package.extras] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] + +[[package]] +name = "python-dateutil" +version = "2.8.2" +description = "Extensions to the standard Python datetime module" +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "pytz" +version = "2022.2.1" +description = "World timezone definitions, modern and historical" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "requests" +version = "2.28.1" +description = "Python HTTP for Humans." +category = "main" +optional = false +python-versions = ">=3.7, <4" + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<3" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<1.27" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use_chardet_on_py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "rich" +version = "12.5.1" +description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +category = "dev" +optional = false +python-versions = ">=3.6.3,<4.0.0" + +[package.dependencies] +commonmark = ">=0.9.0,<0.10.0" +pygments = ">=2.6.0,<3.0.0" + +[package.extras] +jupyter = ["ipywidgets (>=7.5.1,<8.0.0)"] + +[[package]] +name = "scikit-learn" +version = "1.1.2" +description = "A set of python modules for machine learning and data mining" +category = "main" +optional = false +python-versions = ">=3.8" + +[package.dependencies] +joblib = ">=1.0.0" +numpy = ">=1.17.3" +scipy = ">=1.3.2" +threadpoolctl = ">=2.0.0" + +[package.extras] +tests = ["numpydoc (>=1.2.0)", "pyamg (>=4.0.0)", "mypy (>=0.961)", "black (>=22.3.0)", "flake8 (>=3.8.2)", "pytest-cov (>=2.9.0)", "pytest (>=5.0.1)", "pandas (>=1.0.5)", "scikit-image (>=0.16.2)", "matplotlib (>=3.1.2)"] +examples = ["seaborn (>=0.9.0)", "pandas (>=1.0.5)", "scikit-image (>=0.16.2)", "matplotlib (>=3.1.2)"] +docs = ["sphinxext-opengraph (>=0.4.2)", "sphinx-prompt (>=1.3.0)", "Pillow (>=7.1.2)", "numpydoc (>=1.2.0)", "sphinx-gallery (>=0.7.0)", "sphinx (>=4.0.1)", "memory-profiler (>=0.57.0)", "seaborn (>=0.9.0)", "pandas (>=1.0.5)", "scikit-image (>=0.16.2)", "matplotlib (>=3.1.2)"] +benchmark = ["memory-profiler (>=0.57.0)", "pandas (>=1.0.5)", "matplotlib (>=3.1.2)"] + +[[package]] +name = "scipy" +version = "1.9.1" +description = "SciPy: Scientific Library for Python" +category = "main" +optional = false +python-versions = ">=3.8,<3.12" + +[package.dependencies] +numpy = ">=1.18.5,<1.25.0" + +[[package]] +name = "setuptools-scm" +version = "7.0.5" +description = "the blessed package to manage your versions by scm tags" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +packaging = ">=20.0" +tomli = ">=1.0.0" +typing-extensions = "*" + +[package.extras] +test = ["pytest (>=6.2)", "virtualenv (>20)"] +toml = ["setuptools (>=42)"] + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "soupsieve" +version = "2.3.2.post1" +description = "A modern CSS selector implementation for Beautiful Soup." +category = "main" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "threadpoolctl" +version = "3.1.0" +description = "threadpoolctl" +category = "main" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "toml" +version = "0.10.2" +description = "Python Library for Tom's Obvious, Minimal Language" +category = "dev" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +category = "main" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "tqdm" +version = "4.64.1" +description = "Fast, Extensible Progress Meter" +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[package.extras] +dev = ["py-make (>=0.1.0)", "twine", "wheel"] +notebook = ["ipywidgets (>=6)"] +slack = ["slack-sdk"] +telegram = ["requests"] + +[[package]] +name = "tryceratops" +version = "1.1.0" +description = "A linter to manage your exception like a PRO!" +category = "dev" +optional = false +python-versions = ">=3.8,<4.0" + +[package.dependencies] +click = ">=7" +rich = ">=10.14.0" +toml = ">=0.10.2" + +[[package]] +name = "typing-extensions" +version = "4.3.0" +description = "Backported and Experimental Type Hints for Python 3.7+" +category = "main" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "urllib3" +version = "1.26.12" +description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, <4" + +[package.extras] +brotli = ["brotlicffi (>=0.8.0)", "brotli (>=1.0.9)", "brotlipy (>=0.6.0)"] +secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "urllib3-secure-extra", "ipaddress"] +socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] + +[[package]] +name = "win32-setctime" +version = "1.1.0" +description = "A small Python utility to set file creation time on Windows" +category = "main" +optional = false +python-versions = ">=3.5" + +[package.extras] +dev = ["black (>=19.3b0)", "pytest (>=4.6.2)"] + +[metadata] +lock-version = "1.1" +python-versions = ">=3.9,<3.12" +content-hash = "ecedcf5526a5090a024b0bff91f39d87129ea6faff146cfba30a36f7d1c546b3" + +[metadata.files] +attrs = [ + {file = "attrs-22.1.0-py2.py3-none-any.whl", hash = "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"}, + {file = "attrs-22.1.0.tar.gz", hash = "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6"}, +] +autoflake = [ + {file = "autoflake-1.6.1-py2.py3-none-any.whl", hash = "sha256:dfef4c851fb07e6111f9115d3e7c8c52d8564cbf71c12ade2d8b8a2a7b8bd176"}, + {file = "autoflake-1.6.1.tar.gz", hash = "sha256:72bce741144ef6db26005d47dba242c1fd6a91ea53f7f4c5a90ad4b051e394c2"}, +] +beautifulsoup4 = [ + {file = "beautifulsoup4-4.11.1-py3-none-any.whl", hash = "sha256:58d5c3d29f5a36ffeb94f02f0d786cd53014cf9b3b3951d42e0080d8a9498d30"}, + {file = "beautifulsoup4-4.11.1.tar.gz", hash = "sha256:ad9aa55b65ef2808eb405f46cf74df7fcb7044d5cbc26487f96eb2ef2e436693"}, +] +black = [ + {file = "black-22.8.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ce957f1d6b78a8a231b18e0dd2d94a33d2ba738cd88a7fe64f53f659eea49fdd"}, + {file = "black-22.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5107ea36b2b61917956d018bd25129baf9ad1125e39324a9b18248d362156a27"}, + {file = "black-22.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e8166b7bfe5dcb56d325385bd1d1e0f635f24aae14b3ae437102dedc0c186747"}, + {file = "black-22.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd82842bb272297503cbec1a2600b6bfb338dae017186f8f215c8958f8acf869"}, + {file = "black-22.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:d839150f61d09e7217f52917259831fe2b689f5c8e5e32611736351b89bb2a90"}, + {file = "black-22.8.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:a05da0430bd5ced89176db098567973be52ce175a55677436a271102d7eaa3fe"}, + {file = "black-22.8.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a098a69a02596e1f2a58a2a1c8d5a05d5a74461af552b371e82f9fa4ada8342"}, + {file = "black-22.8.0-cp36-cp36m-win_amd64.whl", hash = "sha256:5594efbdc35426e35a7defa1ea1a1cb97c7dbd34c0e49af7fb593a36bd45edab"}, + {file = "black-22.8.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a983526af1bea1e4cf6768e649990f28ee4f4137266921c2c3cee8116ae42ec3"}, + {file = "black-22.8.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b2c25f8dea5e8444bdc6788a2f543e1fb01494e144480bc17f806178378005e"}, + {file = "black-22.8.0-cp37-cp37m-win_amd64.whl", hash = "sha256:78dd85caaab7c3153054756b9fe8c611efa63d9e7aecfa33e533060cb14b6d16"}, + {file = "black-22.8.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:cea1b2542d4e2c02c332e83150e41e3ca80dc0fb8de20df3c5e98e242156222c"}, + {file = "black-22.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5b879eb439094751185d1cfdca43023bc6786bd3c60372462b6f051efa6281a5"}, + {file = "black-22.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0a12e4e1353819af41df998b02c6742643cfef58282915f781d0e4dd7a200411"}, + {file = "black-22.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3a73f66b6d5ba7288cd5d6dad9b4c9b43f4e8a4b789a94bf5abfb878c663eb3"}, + {file = "black-22.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:e981e20ec152dfb3e77418fb616077937378b322d7b26aa1ff87717fb18b4875"}, + {file = "black-22.8.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8ce13ffed7e66dda0da3e0b2eb1bdfc83f5812f66e09aca2b0978593ed636b6c"}, + {file = "black-22.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:32a4b17f644fc288c6ee2bafdf5e3b045f4eff84693ac069d87b1a347d861497"}, + {file = "black-22.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0ad827325a3a634bae88ae7747db1a395d5ee02cf05d9aa7a9bd77dfb10e940c"}, + {file = "black-22.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53198e28a1fb865e9fe97f88220da2e44df6da82b18833b588b1883b16bb5d41"}, + {file = "black-22.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:bc4d4123830a2d190e9cc42a2e43570f82ace35c3aeb26a512a2102bce5af7ec"}, + {file = "black-22.8.0-py3-none-any.whl", hash = "sha256:d2c21d439b2baf7aa80d6dd4e3659259be64c6f49dfd0f32091063db0e006db4"}, + {file = "black-22.8.0.tar.gz", hash = "sha256:792f7eb540ba9a17e8656538701d3eb1afcb134e3b45b71f20b25c77a8db7e6e"}, +] +certifi = [ + {file = "certifi-2022.9.24-py3-none-any.whl", hash = "sha256:90c1a32f1d68f940488354e36370f6cca89f0f106db09518524c88d6ed83f382"}, + {file = "certifi-2022.9.24.tar.gz", hash = "sha256:0d9c601124e5a6ba9712dbc60d9c53c21e34f5f641fe83002317394311bdce14"}, +] +charset-normalizer = [ + {file = "charset-normalizer-2.1.1.tar.gz", hash = "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845"}, + {file = "charset_normalizer-2.1.1-py3-none-any.whl", hash = "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"}, +] +click = [ + {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, + {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, +] +colorama = [ + {file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"}, + {file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"}, +] +commonmark = [ + {file = "commonmark-0.9.1-py2.py3-none-any.whl", hash = "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9"}, + {file = "commonmark-0.9.1.tar.gz", hash = "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60"}, +] +contourpy = [ + {file = "contourpy-1.0.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:87121b9428ac568fb84fae4af5e7852fc34f02eadc4e3e91f6c8989327692186"}, + {file = "contourpy-1.0.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1fb782982c42cee667b892a0b0c52a9f6c7ecf1da5c5f4345845f04eaa862f93"}, + {file = "contourpy-1.0.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:689d7d2a840619915d0abd1ecc6e399fee202f8ad315acda2807f4ca420d0802"}, + {file = "contourpy-1.0.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d88814befbd1433152c5f6dd536905149ba028d795a22555b149ae0a36024d9e"}, + {file = "contourpy-1.0.5-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:df65f4b2b4e74977f0336bef12a88051ab24e6a16873cd9249f34d67cb3e345d"}, + {file = "contourpy-1.0.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf6b4c0c723664f65c2a47c8cb6ebbf660b0b2e2d936adf2e8503d4e93359465"}, + {file = "contourpy-1.0.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:bcc98d397c3dea45d5b262029564b29cb8e945f2607a38bee6163694c0a8b4ef"}, + {file = "contourpy-1.0.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:2bf5c846c257578b03d498b20f54f53551616a507d8e5463511c58bb58e9a9cf"}, + {file = "contourpy-1.0.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cdacddb18d55ffec42d1907079cdc04ec4fa8a990cdf5b9d9fe67d281fc0d12e"}, + {file = "contourpy-1.0.5-cp310-cp310-win32.whl", hash = "sha256:434942fa2f9019b9ae525fb752dc523800c49a1a28fbd6d9240b0fa959573dcc"}, + {file = "contourpy-1.0.5-cp310-cp310-win_amd64.whl", hash = "sha256:3b3082ade8849130203d461b98c2a061b382c46074b43b4edd5cefd81af92b8a"}, + {file = "contourpy-1.0.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:057114f698ffb9e54657e8fda6802e2f5c8fad609845cf6afaf31590ef6a33c0"}, + {file = "contourpy-1.0.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:218722a29c5c26677d37c44f5f8a372daf6f07870aad793a97d47eb6ad6b3290"}, + {file = "contourpy-1.0.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6c02e22cf09996194bcb3a4784099975cf527d5c29caf759abadf29ebdb2fe27"}, + {file = "contourpy-1.0.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c0d5ee865b5fd16bf62d72122aadcc90aab296c30c1adb0a32b4b66bd843163e"}, + {file = "contourpy-1.0.5-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d45822b0a2a452327ab4f95efe368d234d5294bbf89a99968be27c7938a21108"}, + {file = "contourpy-1.0.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dca5be83a6dfaf933a46e3bc2b9f2685e5ec61b22f6a38ad740aac9c16e9a0ff"}, + {file = "contourpy-1.0.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:3c3f2f6b898a40207843ae01970e57e33d22a26b22f23c6a5e07b4716751085f"}, + {file = "contourpy-1.0.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c2b4eab7c12f9cb460509bc34a3b086f9802f0dba27c89a63df4123819ad64af"}, + {file = "contourpy-1.0.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:09ed9b63f4df8a7591b7a4a26c1ad066dcaafda1f846250fdcb534074a411692"}, + {file = "contourpy-1.0.5-cp311-cp311-win32.whl", hash = "sha256:f670686d99c867d0f24b28ce8c6f02429c6eef5e2674aab287850d0ee2d20437"}, + {file = "contourpy-1.0.5-cp311-cp311-win_amd64.whl", hash = "sha256:c51568e94f7f232296de30002f2a50f77a7bd346673da3e4f2aaf9d2b833f2e5"}, + {file = "contourpy-1.0.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7c9e99aac7b430f6a9f15eebf058c742097cea3369f23a2bfc5e64d374b67e3a"}, + {file = "contourpy-1.0.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3210d93ad2af742b6a96cf39792f7181822edbb8fe11c3ef29d1583fe637a8d8"}, + {file = "contourpy-1.0.5-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:128bd7acf569f8443ad5b2227f30ac909e4f5399ed221727eeacf0c6476187e6"}, + {file = "contourpy-1.0.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:813c2944e940ef8dccea71305bacc942d4b193a021140874b3e58933ec44f5b6"}, + {file = "contourpy-1.0.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:a74afd8d560eaafe0d9e3e1db8c06081282a05ca4de00ee416195085a79d7d3d"}, + {file = "contourpy-1.0.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2d0ad9a85f208473b1f3613c45756c7aa6fcc288266a8c7b873f896aaf741b6b"}, + {file = "contourpy-1.0.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:60f37acd4e4227c5a29f737d9a85ca3145c529a8dd4bf70af7f0637c61b49222"}, + {file = "contourpy-1.0.5-cp37-cp37m-win32.whl", hash = "sha256:b50e481a4317a8efcfffcfddcd4c9b36eacba440440e70cbe0256aeb6fd6abae"}, + {file = "contourpy-1.0.5-cp37-cp37m-win_amd64.whl", hash = "sha256:0395ae71164bfeb2dedd136e03c71a2718a5aa9873a46f518f4133be0d63e1d2"}, + {file = "contourpy-1.0.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:3ca40d7844b391d90b864c6a6d1bb6b88b09035fb4d866d64d43c4d26fb0ab64"}, + {file = "contourpy-1.0.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3109fa601d2a448cec4643abd3a31f972bf05b7c2f2e83df9d3429878f8c10ae"}, + {file = "contourpy-1.0.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:06c4d1dde5ee4f909a8a95ba1eb04040c6c26946b4f3b5beaf10d45f14e940ee"}, + {file = "contourpy-1.0.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f54dcc9bb9390fd0636301ead134d46d5229fe86da0db4d974c0fda349f560e"}, + {file = "contourpy-1.0.5-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:46b8e24813e2fb5a3e598c1f8b9ae403e1438cb846a80cc2b33cddf19dddd7f2"}, + {file = "contourpy-1.0.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:061e1f066c419ffe25b615a1df031b4832ea1d7f2676937e69e8e00e24512005"}, + {file = "contourpy-1.0.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:19ea64fa0cf389d2ebc10974616acfa1fdecbd73d1fd9c72215b782f3c40f561"}, + {file = "contourpy-1.0.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:dfe924e5a63861c82332a12adeeab955dc8c8009ddbbd80cc2fcca049ff89a49"}, + {file = "contourpy-1.0.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bed3a2a823a041e8d249b1a7ec132933e1505299329b5cfe1b2b5ec689ec7675"}, + {file = "contourpy-1.0.5-cp38-cp38-win32.whl", hash = "sha256:0389349875424aa8c5e61f757e894687916bc4e9616cc6afcbd8051aa2428952"}, + {file = "contourpy-1.0.5-cp38-cp38-win_amd64.whl", hash = "sha256:2b5e334330d82866923015b455260173cb3b9e3b4e297052d758abd262031289"}, + {file = "contourpy-1.0.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:def9a01b73c9e27d70ea03b381fb3e7aadfac1f398dbd63751313c3a46747ef5"}, + {file = "contourpy-1.0.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:59c827e536bb5e3ef58e06da0faba61fd89a14f30b68bcfeca41f43ca83a1942"}, + {file = "contourpy-1.0.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f05d311c937da03b0cd26ac3e14cb991f6ff8fc94f98b3df9713537817539795"}, + {file = "contourpy-1.0.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:970a4be7ec84ccda7c27cb4ae74930bbbd477bc8d849ed55ea798084dd5fca8c"}, + {file = "contourpy-1.0.5-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f7672148f8fca48e4efc16aba24a7455b40c22d4f8abe42475dec6a12b0bb9a"}, + {file = "contourpy-1.0.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eba62b7c21a33e72dd8adab2b92dd5610d8527f0b2ac28a8e0770e71b21a13f9"}, + {file = "contourpy-1.0.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:dd084459ecdb224e617e4ab3f1d5ebe4d1c48facb41f24952b76aa6ba9712bb0"}, + {file = "contourpy-1.0.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c5158616ab39d34b76c50f40c81552ee180598f7825dc7a66fd187d29958820f"}, + {file = "contourpy-1.0.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:f856652f9b533c6cd2b9ad6836a7fc0e43917d7ff15be46c5baf1350f8cdc5d9"}, + {file = "contourpy-1.0.5-cp39-cp39-win32.whl", hash = "sha256:f1cc623fd6855b25da52b3275e0c9e51711b86a9dccc75f8c9ab4432fd8e42c7"}, + {file = "contourpy-1.0.5-cp39-cp39-win_amd64.whl", hash = "sha256:e67dcaa34dcd908fcccbf49194211d847c731b6ebaac661c1c889f1bf6af1e44"}, + {file = "contourpy-1.0.5-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:bfd634cb9685161b2a51f73a7fc4736fd0d67a56632d52319317afaa27f08243"}, + {file = "contourpy-1.0.5-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79908b9d02b1d6c1c71ff3b7ad127f3f82e14a8e091ab44b3c7e34b649fea733"}, + {file = "contourpy-1.0.5-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b4963cf08f4320d98ae72ec7694291b8ab85cb7da3b0cd824bc32701bc992edf"}, + {file = "contourpy-1.0.5-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3cfc067ddde78b76dcbc9684d82688b7d3c5158fa2254a085f9bcb9586c1e2d8"}, + {file = "contourpy-1.0.5-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:9939796abcadb2810a63dfb26ff8ca4595fe7dd70a3ceae7f607a2639b714307"}, + {file = "contourpy-1.0.5-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d8150579bf30cdf896906baf256aa200cd50dbe6e565c17d6fd3d678e21ff5de"}, + {file = "contourpy-1.0.5-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed9c91bf4ce614efed5388c3f989a7cfe08728ab871d995a486ea74ff88993db"}, + {file = "contourpy-1.0.5-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b46a04588ceb7cf132568e0e564a854627ef87a1ed3bf536234540a79ced44b0"}, + {file = "contourpy-1.0.5-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b85553699862c09937a7a5ea14ee6229087971a7d51ae97d5f4b407f571a2c17"}, + {file = "contourpy-1.0.5-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:99a8071e351b50827ad976b92ed91845fb614ac67a3c41109b24f3d8bd3afada"}, + {file = "contourpy-1.0.5-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:fb0458d74726937ead9e2effc91144aea5a58ecee9754242f8539a782bed685a"}, + {file = "contourpy-1.0.5-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f89f0608a5aa8142ed0e53957916623791a88c7f5e5f07ae530c328beeb888f"}, + {file = "contourpy-1.0.5-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ce763369e646e59e4ca2c09735cd1bdd3048d909ad5f2bc116e83166a9352f3c"}, + {file = "contourpy-1.0.5-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c16fa267740d67883899e054cccb4279e002f3f4872873b752c1ba15045ff49"}, + {file = "contourpy-1.0.5-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a30e95274f5c0e007ccc759ec258aa5708c534ec058f153ee25ac700a2f1438b"}, + {file = "contourpy-1.0.5.tar.gz", hash = "sha256:896631cd40222aef3697e4e51177d14c3709fda49d30983269d584f034acc8a4"}, +] +cycler = [ + {file = "cycler-0.11.0-py3-none-any.whl", hash = "sha256:3a27e95f763a428a739d2add979fa7494c912a32c17c4c38c4d5f082cad165a3"}, + {file = "cycler-0.11.0.tar.gz", hash = "sha256:9c87405839a19696e837b3b818fed3f5f69f16f1eec1a1ad77e043dcea9c772f"}, +] +eradicate = [ + {file = "eradicate-2.1.0-py3-none-any.whl", hash = "sha256:8bfaca181db9227dc88bdbce4d051a9627604c2243e7d85324f6d6ce0fd08bb2"}, + {file = "eradicate-2.1.0.tar.gz", hash = "sha256:aac7384ab25b1bf21c4c012de9b4bf8398945a14c98c911545b2ea50ab558014"}, +] +flake8 = [ + {file = "flake8-5.0.4-py2.py3-none-any.whl", hash = "sha256:7a1cf6b73744f5806ab95e526f6f0d8c01c66d7bbe349562d22dfca20610b248"}, + {file = "flake8-5.0.4.tar.gz", hash = "sha256:6fbe320aad8d6b95cec8b8e47bc933004678dc63095be98528b7bdd2a9f510db"}, +] +flake8-bugbear = [ + {file = "flake8-bugbear-22.9.23.tar.gz", hash = "sha256:17b9623325e6e0dcdcc80ed9e4aa811287fcc81d7e03313b8736ea5733759937"}, + {file = "flake8_bugbear-22.9.23-py3-none-any.whl", hash = "sha256:cd2779b2b7ada212d7a322814a1e5651f1868ab0d3f24cc9da66169ab8fda474"}, +] +flake8-builtins = [ + {file = "flake8-builtins-1.5.3.tar.gz", hash = "sha256:09998853b2405e98e61d2ff3027c47033adbdc17f9fe44ca58443d876eb00f3b"}, + {file = "flake8_builtins-1.5.3-py2.py3-none-any.whl", hash = "sha256:7706babee43879320376861897e5d1468e396a40b8918ed7bccf70e5f90b8687"}, +] +flake8-comprehensions = [ + {file = "flake8-comprehensions-3.10.0.tar.gz", hash = "sha256:181158f7e7aa26a63a0a38e6017cef28c6adee71278ce56ce11f6ec9c4905058"}, + {file = "flake8_comprehensions-3.10.0-py3-none-any.whl", hash = "sha256:dad454fd3d525039121e98fa1dd90c46bc138708196a4ebbc949ad3c859adedb"}, +] +flake8-debugger = [ + {file = "flake8-debugger-4.1.2.tar.gz", hash = "sha256:52b002560941e36d9bf806fca2523dc7fb8560a295d5f1a6e15ac2ded7a73840"}, + {file = "flake8_debugger-4.1.2-py3-none-any.whl", hash = "sha256:0a5e55aeddcc81da631ad9c8c366e7318998f83ff00985a49e6b3ecf61e571bf"}, +] +flake8-eradicate = [ + {file = "flake8-eradicate-1.4.0.tar.gz", hash = "sha256:3088cfd6717d1c9c6c3ac45ef2e5f5b6c7267f7504d5a74b781500e95cb9c7e1"}, + {file = "flake8_eradicate-1.4.0-py3-none-any.whl", hash = "sha256:e3bbd0871be358e908053c1ab728903c114f062ba596b4d40c852fd18f473d56"}, +] +fonttools = [ + {file = "fonttools-4.37.3-py3-none-any.whl", hash = "sha256:a5bc5f5d48faa4085310b8ebd4c5d33bf27c6636c5f10a7de792510af2745a81"}, + {file = "fonttools-4.37.3.zip", hash = "sha256:f32ef6ec966cf0e7d2aa88601fed2e3a8f2851c26b5db2c80ccc8f82bee4eedc"}, +] +idna = [ + {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, + {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, +] +iniconfig = [ + {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, + {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, +] +isort = [ + {file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"}, + {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"}, +] +joblib = [ + {file = "joblib-1.2.0-py3-none-any.whl", hash = "sha256:091138ed78f800342968c523bdde947e7a305b8594b910a0fea2ab83c3c6d385"}, + {file = "joblib-1.2.0.tar.gz", hash = "sha256:e1cee4a79e4af22881164f218d4311f60074197fb707e082e803b61f6d137018"}, +] +kiwisolver = [ + {file = "kiwisolver-1.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2f5e60fabb7343a836360c4f0919b8cd0d6dbf08ad2ca6b9cf90bf0c76a3c4f6"}, + {file = "kiwisolver-1.4.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:10ee06759482c78bdb864f4109886dff7b8a56529bc1609d4f1112b93fe6423c"}, + {file = "kiwisolver-1.4.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c79ebe8f3676a4c6630fd3f777f3cfecf9289666c84e775a67d1d358578dc2e3"}, + {file = "kiwisolver-1.4.4-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:abbe9fa13da955feb8202e215c4018f4bb57469b1b78c7a4c5c7b93001699938"}, + {file = "kiwisolver-1.4.4-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7577c1987baa3adc4b3c62c33bd1118c3ef5c8ddef36f0f2c950ae0b199e100d"}, + {file = "kiwisolver-1.4.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8ad8285b01b0d4695102546b342b493b3ccc6781fc28c8c6a1bb63e95d22f09"}, + {file = "kiwisolver-1.4.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8ed58b8acf29798b036d347791141767ccf65eee7f26bde03a71c944449e53de"}, + {file = "kiwisolver-1.4.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a68b62a02953b9841730db7797422f983935aeefceb1679f0fc85cbfbd311c32"}, + {file = "kiwisolver-1.4.4-cp310-cp310-win32.whl", hash = "sha256:e92a513161077b53447160b9bd8f522edfbed4bd9759e4c18ab05d7ef7e49408"}, + {file = "kiwisolver-1.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:3fe20f63c9ecee44560d0e7f116b3a747a5d7203376abeea292ab3152334d004"}, + {file = "kiwisolver-1.4.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:62ac9cc684da4cf1778d07a89bf5f81b35834cb96ca523d3a7fb32509380cbf6"}, + {file = "kiwisolver-1.4.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41dae968a94b1ef1897cb322b39360a0812661dba7c682aa45098eb8e193dbdf"}, + {file = "kiwisolver-1.4.4-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:02f79693ec433cb4b5f51694e8477ae83b3205768a6fb48ffba60549080e295b"}, + {file = "kiwisolver-1.4.4-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d0611a0a2a518464c05ddd5a3a1a0e856ccc10e67079bb17f265ad19ab3c7597"}, + {file = "kiwisolver-1.4.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:db5283d90da4174865d520e7366801a93777201e91e79bacbac6e6927cbceede"}, + {file = "kiwisolver-1.4.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:1041feb4cda8708ce73bb4dcb9ce1ccf49d553bf87c3954bdfa46f0c3f77252c"}, + {file = "kiwisolver-1.4.4-cp37-cp37m-win32.whl", hash = "sha256:a553dadda40fef6bfa1456dc4be49b113aa92c2a9a9e8711e955618cd69622e3"}, + {file = "kiwisolver-1.4.4-cp37-cp37m-win_amd64.whl", hash = "sha256:03baab2d6b4a54ddbb43bba1a3a2d1627e82d205c5cf8f4c924dc49284b87166"}, + {file = "kiwisolver-1.4.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:841293b17ad704d70c578f1f0013c890e219952169ce8a24ebc063eecf775454"}, + {file = "kiwisolver-1.4.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f4f270de01dd3e129a72efad823da90cc4d6aafb64c410c9033aba70db9f1ff0"}, + {file = "kiwisolver-1.4.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f9f39e2f049db33a908319cf46624a569b36983c7c78318e9726a4cb8923b26c"}, + {file = "kiwisolver-1.4.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c97528e64cb9ebeff9701e7938653a9951922f2a38bd847787d4a8e498cc83ae"}, + {file = "kiwisolver-1.4.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d1573129aa0fd901076e2bfb4275a35f5b7aa60fbfb984499d661ec950320b0"}, + {file = "kiwisolver-1.4.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ad881edc7ccb9d65b0224f4e4d05a1e85cf62d73aab798943df6d48ab0cd79a1"}, + {file = "kiwisolver-1.4.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b428ef021242344340460fa4c9185d0b1f66fbdbfecc6c63eff4b7c29fad429d"}, + {file = "kiwisolver-1.4.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:2e407cb4bd5a13984a6c2c0fe1845e4e41e96f183e5e5cd4d77a857d9693494c"}, + {file = "kiwisolver-1.4.4-cp38-cp38-win32.whl", hash = "sha256:75facbe9606748f43428fc91a43edb46c7ff68889b91fa31f53b58894503a191"}, + {file = "kiwisolver-1.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:5bce61af018b0cb2055e0e72e7d65290d822d3feee430b7b8203d8a855e78766"}, + {file = "kiwisolver-1.4.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8c808594c88a025d4e322d5bb549282c93c8e1ba71b790f539567932722d7bd8"}, + {file = "kiwisolver-1.4.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f0a71d85ecdd570ded8ac3d1c0f480842f49a40beb423bb8014539a9f32a5897"}, + {file = "kiwisolver-1.4.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b533558eae785e33e8c148a8d9921692a9fe5aa516efbdff8606e7d87b9d5824"}, + {file = "kiwisolver-1.4.4-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:efda5fc8cc1c61e4f639b8067d118e742b812c930f708e6667a5ce0d13499e29"}, + {file = "kiwisolver-1.4.4-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7c43e1e1206cd421cd92e6b3280d4385d41d7166b3ed577ac20444b6995a445f"}, + {file = "kiwisolver-1.4.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc8d3bd6c72b2dd9decf16ce70e20abcb3274ba01b4e1c96031e0c4067d1e7cd"}, + {file = "kiwisolver-1.4.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4ea39b0ccc4f5d803e3337dd46bcce60b702be4d86fd0b3d7531ef10fd99a1ac"}, + {file = "kiwisolver-1.4.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:968f44fdbf6dd757d12920d63b566eeb4d5b395fd2d00d29d7ef00a00582aac9"}, + {file = "kiwisolver-1.4.4-cp39-cp39-win32.whl", hash = "sha256:da7e547706e69e45d95e116e6939488d62174e033b763ab1496b4c29b76fabea"}, + {file = "kiwisolver-1.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:ba59c92039ec0a66103b1d5fe588fa546373587a7d68f5c96f743c3396afc04b"}, + {file = "kiwisolver-1.4.4-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:91672bacaa030f92fc2f43b620d7b337fd9a5af28b0d6ed3f77afc43c4a64b5a"}, + {file = "kiwisolver-1.4.4-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:787518a6789009c159453da4d6b683f468ef7a65bbde796bcea803ccf191058d"}, + {file = "kiwisolver-1.4.4-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da152d8cdcab0e56e4f45eb08b9aea6455845ec83172092f09b0e077ece2cf7a"}, + {file = "kiwisolver-1.4.4-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:ecb1fa0db7bf4cff9dac752abb19505a233c7f16684c5826d1f11ebd9472b871"}, + {file = "kiwisolver-1.4.4.tar.gz", hash = "sha256:d41997519fcba4a1e46eb4a2fe31bc12f0ff957b2b81bac28db24744f333e955"}, +] +loguru = [ + {file = "loguru-0.6.0-py3-none-any.whl", hash = "sha256:4e2414d534a2ab57573365b3e6d0234dfb1d84b68b7f3b948e6fb743860a77c3"}, + {file = "loguru-0.6.0.tar.gz", hash = "sha256:066bd06758d0a513e9836fd9c6b5a75bfb3fd36841f4b996bc60b547a309d41c"}, +] +matplotlib = [ + {file = "matplotlib-3.6.0-cp310-cp310-macosx_10_12_universal2.whl", hash = "sha256:6b98e098549d3aea2bfb93f38f0b2ecadcb423fa1504bbff902c01efdd833fd8"}, + {file = "matplotlib-3.6.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:798559837156b8e2e2df97cffca748c5c1432af6ec5004c2932e475d813f1743"}, + {file = "matplotlib-3.6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e572c67958f7d55eae77f5f64dc7bd31968cc9f24c233926833efe63c60545f2"}, + {file = "matplotlib-3.6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ec2edf7f74829eae287aa53d64d83ad5d43ee51d29fb1d88e689d8b36028312"}, + {file = "matplotlib-3.6.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:51092d13499be72e47c15c3a1ae0209edaca6be42b65ffbbefbe0c85f6153c6f"}, + {file = "matplotlib-3.6.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9295ca10a140c21e40d2ee43ef423213dc20767f6cea6b87c36973564bc51095"}, + {file = "matplotlib-3.6.0-cp310-cp310-win32.whl", hash = "sha256:1a4835c177821f3729be27ae9be7b8ae209fe75e83db7d9b2bfd319a998f0a42"}, + {file = "matplotlib-3.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:2b60d4abcb6a405ca7d909c80791b00637d22c62aa3bb0ffff7e589f763867f5"}, + {file = "matplotlib-3.6.0-cp311-cp311-macosx_10_12_universal2.whl", hash = "sha256:66a0db13f77aa7806dba29273874cf862450c61c2e5158245d17ee85d983fe8e"}, + {file = "matplotlib-3.6.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:1739935d293d0348d7bf662e8cd0edb9c2aa8f20ccd646db755ce0f3456d24e4"}, + {file = "matplotlib-3.6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1559213b803959a2b8309122585b5226d1c2fb66c933b1a2094cf1e99cb4fb90"}, + {file = "matplotlib-3.6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5bd3b3ff191f81509d9a1afd62e1e3cda7a7889c35b5b6359a1241fe1511015"}, + {file = "matplotlib-3.6.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f1954d71cdf15c19e7f3bf2235a4fe1600ba42f34d472c9495bcf54d75a43e4e"}, + {file = "matplotlib-3.6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d840712f4b4c7d2a119f993d7e43ca9bcaa73aeaa24c322fa2bdf4f689a3ee09"}, + {file = "matplotlib-3.6.0-cp311-cp311-win32.whl", hash = "sha256:89e1978c3fbe4e3d4c6ad7db7e6f982607cb2546f982ccbe42708392437b1972"}, + {file = "matplotlib-3.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:9711ef291e184b5a73c9d3af3f2d5cfe25d571c8dd95aa498415f74ac7e221a8"}, + {file = "matplotlib-3.6.0-cp38-cp38-macosx_10_12_universal2.whl", hash = "sha256:fbbceb0a0dfe9213f6314510665a32ef25fe29b50657567cd00115fbfcb3b20d"}, + {file = "matplotlib-3.6.0-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:62319d57dab5ad3e3494dd97a214e22079d3f72a0c8a2fd001829c2c6abbf8d1"}, + {file = "matplotlib-3.6.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:140316427a7c384e3dd37efb3a73cd67e14b0b237a6d277def91227f43cdcec2"}, + {file = "matplotlib-3.6.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ccea337fb9a44866c5300c594b13d4d87e827ebc3c353bff15d298bac976b654"}, + {file = "matplotlib-3.6.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:16a899b958dd76606b571bc7eaa38f09160c27dfb262e493584644cfd4a77f0f"}, + {file = "matplotlib-3.6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cd73a16a759865831be5a8fb6546f2a908c8d7d7f55c75f94ee7c2ca13cc95de"}, + {file = "matplotlib-3.6.0-cp38-cp38-win32.whl", hash = "sha256:2ed779a896b70c8012fe301fb91ee37e713e1dda1eb8f37de04cdbf506706983"}, + {file = "matplotlib-3.6.0-cp38-cp38-win_amd64.whl", hash = "sha256:eca6f59cd0729edaeaa7032d582dffce518a420d4961ef3e8c93dce86be352c3"}, + {file = "matplotlib-3.6.0-cp39-cp39-macosx_10_12_universal2.whl", hash = "sha256:408bbf968c15e9e38df9f25a588e372e28a43240cf5884c9bc6039a5021b7d5b"}, + {file = "matplotlib-3.6.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:7127e2b94571318531caf098dc9e8f60f5aba1704600f0b2483bf151d535674a"}, + {file = "matplotlib-3.6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f0d5b9b14ccc7f539143ac9eb1c6b57d26d69ca52d30c3d719a7bc4123579e44"}, + {file = "matplotlib-3.6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:baa19508d8445f5648cd1ffe4fc6d4f7daf8b876f804e9a453df6c3708f6200b"}, + {file = "matplotlib-3.6.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0ae1b9b555212c1e242666af80e7ed796705869581e2d749971db4e682ccc1f3"}, + {file = "matplotlib-3.6.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0958fc3fdc59c1b716ee1a5d14e73d03d541d873241a37c5c3a86f7ef6017923"}, + {file = "matplotlib-3.6.0-cp39-cp39-win32.whl", hash = "sha256:efe9e8037b989b14bb1887089ae763385431cc06fe488406413079cfd2a3a089"}, + {file = "matplotlib-3.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b0320f882214f6ffde5992081520b57b55450510bdaa020e96aacff9b7ae10e6"}, + {file = "matplotlib-3.6.0-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:11c1987b803cc2b26725659cfe817478f0a9597878e5c4bf374cfe4e12cbbd79"}, + {file = "matplotlib-3.6.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:802feae98addb9f21707649a7f229c90a59fad34511881f20b906a5e8e6ea475"}, + {file = "matplotlib-3.6.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efd2e12f8964f8fb4ba1984df71d85d02ef0531e687e59f78ec8fc07271a3857"}, + {file = "matplotlib-3.6.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:4eba6972b796d97c8fcc5266b6dc42ef27c2dce4421b846cded0f3af851b81c9"}, + {file = "matplotlib-3.6.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:df26a09d955b3ab9b6bc18658b9403ed839096c97d7abe8806194e228a485a3c"}, + {file = "matplotlib-3.6.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e01382c06ac3710155a0ca923047c5abe03c676d08f03e146c6a240d0a910713"}, + {file = "matplotlib-3.6.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4699bb671dbc4afdb544eb893e4deb8a34e294b7734733f65b4fd2787ba5fbc6"}, + {file = "matplotlib-3.6.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:657fb7712185f82211170ac4debae0800ed4f5992b8f7ebba2a9eabaf133a857"}, + {file = "matplotlib-3.6.0.tar.gz", hash = "sha256:c5108ebe67da60a9204497d8d403316228deb52b550388190c53a57394d41531"}, +] +mccabe = [ + {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, + {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, +] +mypy = [ + {file = "mypy-0.971-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f2899a3cbd394da157194f913a931edfd4be5f274a88041c9dc2d9cdcb1c315c"}, + {file = "mypy-0.971-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:98e02d56ebe93981c41211c05adb630d1d26c14195d04d95e49cd97dbc046dc5"}, + {file = "mypy-0.971-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:19830b7dba7d5356d3e26e2427a2ec91c994cd92d983142cbd025ebe81d69cf3"}, + {file = "mypy-0.971-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:02ef476f6dcb86e6f502ae39a16b93285fef97e7f1ff22932b657d1ef1f28655"}, + {file = "mypy-0.971-cp310-cp310-win_amd64.whl", hash = "sha256:25c5750ba5609a0c7550b73a33deb314ecfb559c350bb050b655505e8aed4103"}, + {file = "mypy-0.971-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d3348e7eb2eea2472db611486846742d5d52d1290576de99d59edeb7cd4a42ca"}, + {file = "mypy-0.971-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3fa7a477b9900be9b7dd4bab30a12759e5abe9586574ceb944bc29cddf8f0417"}, + {file = "mypy-0.971-cp36-cp36m-win_amd64.whl", hash = "sha256:2ad53cf9c3adc43cf3bea0a7d01a2f2e86db9fe7596dfecb4496a5dda63cbb09"}, + {file = "mypy-0.971-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:855048b6feb6dfe09d3353466004490b1872887150c5bb5caad7838b57328cc8"}, + {file = "mypy-0.971-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:23488a14a83bca6e54402c2e6435467a4138785df93ec85aeff64c6170077fb0"}, + {file = "mypy-0.971-cp37-cp37m-win_amd64.whl", hash = "sha256:4b21e5b1a70dfb972490035128f305c39bc4bc253f34e96a4adf9127cf943eb2"}, + {file = "mypy-0.971-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:9796a2ba7b4b538649caa5cecd398d873f4022ed2333ffde58eaf604c4d2cb27"}, + {file = "mypy-0.971-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5a361d92635ad4ada1b1b2d3630fc2f53f2127d51cf2def9db83cba32e47c856"}, + {file = "mypy-0.971-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b793b899f7cf563b1e7044a5c97361196b938e92f0a4343a5d27966a53d2ec71"}, + {file = "mypy-0.971-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d1ea5d12c8e2d266b5fb8c7a5d2e9c0219fedfeb493b7ed60cd350322384ac27"}, + {file = "mypy-0.971-cp38-cp38-win_amd64.whl", hash = "sha256:23c7ff43fff4b0df93a186581885c8512bc50fc4d4910e0f838e35d6bb6b5e58"}, + {file = "mypy-0.971-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:1f7656b69974a6933e987ee8ffb951d836272d6c0f81d727f1d0e2696074d9e6"}, + {file = "mypy-0.971-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d2022bfadb7a5c2ef410d6a7c9763188afdb7f3533f22a0a32be10d571ee4bbe"}, + {file = "mypy-0.971-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ef943c72a786b0f8d90fd76e9b39ce81fb7171172daf84bf43eaf937e9f220a9"}, + {file = "mypy-0.971-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d744f72eb39f69312bc6c2abf8ff6656973120e2eb3f3ec4f758ed47e414a4bf"}, + {file = "mypy-0.971-cp39-cp39-win_amd64.whl", hash = "sha256:77a514ea15d3007d33a9e2157b0ba9c267496acf12a7f2b9b9f8446337aac5b0"}, + {file = "mypy-0.971-py3-none-any.whl", hash = "sha256:0d054ef16b071149917085f51f89555a576e2618d5d9dd70bd6eea6410af3ac9"}, + {file = "mypy-0.971.tar.gz", hash = "sha256:40b0f21484238269ae6a57200c807d80debc6459d444c0489a102d7c6a75fa56"}, +] +mypy-extensions = [ + {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, + {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, +] +numpy = [ + {file = "numpy-1.23.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c9f707b5bb73bf277d812ded9896f9512a43edff72712f31667d0a8c2f8e71ee"}, + {file = "numpy-1.23.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ffcf105ecdd9396e05a8e58e81faaaf34d3f9875f137c7372450baa5d77c9a54"}, + {file = "numpy-1.23.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ea3f98a0ffce3f8f57675eb9119f3f4edb81888b6874bc1953f91e0b1d4f440"}, + {file = "numpy-1.23.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:004f0efcb2fe1c0bd6ae1fcfc69cc8b6bf2407e0f18be308612007a0762b4089"}, + {file = "numpy-1.23.3-cp310-cp310-win32.whl", hash = "sha256:98dcbc02e39b1658dc4b4508442a560fe3ca5ca0d989f0df062534e5ca3a5c1a"}, + {file = "numpy-1.23.3-cp310-cp310-win_amd64.whl", hash = "sha256:39a664e3d26ea854211867d20ebcc8023257c1800ae89773cbba9f9e97bae036"}, + {file = "numpy-1.23.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1f27b5322ac4067e67c8f9378b41c746d8feac8bdd0e0ffede5324667b8a075c"}, + {file = "numpy-1.23.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2ad3ec9a748a8943e6eb4358201f7e1c12ede35f510b1a2221b70af4bb64295c"}, + {file = "numpy-1.23.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bdc9febce3e68b697d931941b263c59e0c74e8f18861f4064c1f712562903411"}, + {file = "numpy-1.23.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:301c00cf5e60e08e04d842fc47df641d4a181e651c7135c50dc2762ffe293dbd"}, + {file = "numpy-1.23.3-cp311-cp311-win32.whl", hash = "sha256:7cd1328e5bdf0dee621912f5833648e2daca72e3839ec1d6695e91089625f0b4"}, + {file = "numpy-1.23.3-cp311-cp311-win_amd64.whl", hash = "sha256:8355fc10fd33a5a70981a5b8a0de51d10af3688d7a9e4a34fcc8fa0d7467bb7f"}, + {file = "numpy-1.23.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bc6e8da415f359b578b00bcfb1d08411c96e9a97f9e6c7adada554a0812a6cc6"}, + {file = "numpy-1.23.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:22d43376ee0acd547f3149b9ec12eec2f0ca4a6ab2f61753c5b29bb3e795ac4d"}, + {file = "numpy-1.23.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a64403f634e5ffdcd85e0b12c08f04b3080d3e840aef118721021f9b48fc1460"}, + {file = "numpy-1.23.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efd9d3abe5774404becdb0748178b48a218f1d8c44e0375475732211ea47c67e"}, + {file = "numpy-1.23.3-cp38-cp38-win32.whl", hash = "sha256:f8c02ec3c4c4fcb718fdf89a6c6f709b14949408e8cf2a2be5bfa9c49548fd85"}, + {file = "numpy-1.23.3-cp38-cp38-win_amd64.whl", hash = "sha256:e868b0389c5ccfc092031a861d4e158ea164d8b7fdbb10e3b5689b4fc6498df6"}, + {file = "numpy-1.23.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:09f6b7bdffe57fc61d869a22f506049825d707b288039d30f26a0d0d8ea05164"}, + {file = "numpy-1.23.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8c79d7cf86d049d0c5089231a5bcd31edb03555bd93d81a16870aa98c6cfb79d"}, + {file = "numpy-1.23.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5d5420053bbb3dd64c30e58f9363d7a9c27444c3648e61460c1237f9ec3fa14"}, + {file = "numpy-1.23.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5422d6a1ea9b15577a9432e26608c73a78faf0b9039437b075cf322c92e98e7"}, + {file = "numpy-1.23.3-cp39-cp39-win32.whl", hash = "sha256:c1ba66c48b19cc9c2975c0d354f24058888cdc674bebadceb3cdc9ec403fb5d1"}, + {file = "numpy-1.23.3-cp39-cp39-win_amd64.whl", hash = "sha256:78a63d2df1d947bd9d1b11d35564c2f9e4b57898aae4626638056ec1a231c40c"}, + {file = "numpy-1.23.3-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:17c0e467ade9bda685d5ac7f5fa729d8d3e76b23195471adae2d6a6941bd2c18"}, + {file = "numpy-1.23.3-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:91b8d6768a75247026e951dce3b2aac79dc7e78622fc148329135ba189813584"}, + {file = "numpy-1.23.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:94c15ca4e52671a59219146ff584488907b1f9b3fc232622b47e2cf832e94fb8"}, + {file = "numpy-1.23.3.tar.gz", hash = "sha256:51bf49c0cd1d52be0a240aa66f3458afc4b95d8993d2d04f0d91fa60c10af6cd"}, +] +packaging = [ + {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, + {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, +] +pandas = [ + {file = "pandas-1.5.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0d8d7433d19bfa33f11c92ad9997f15a902bda4f5ad3a4814a21d2e910894484"}, + {file = "pandas-1.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5cc47f2ebaa20ef96ae72ee082f9e101b3dfbf74f0e62c7a12c0b075a683f03c"}, + {file = "pandas-1.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e8e5edf97d8793f51d258c07c629bd49d271d536ce15d66ac00ceda5c150eb3"}, + {file = "pandas-1.5.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c76f1d104844c5360c21d2ef0e1a8b2ccf8b8ebb40788475e255b9462e32b2be"}, + {file = "pandas-1.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:1642fc6138b4e45d57a12c1b464a01a6d868c0148996af23f72dde8d12486bbc"}, + {file = "pandas-1.5.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:171cef540bfcec52257077816a4dbbac152acdb8236ba11d3196ae02bf0959d8"}, + {file = "pandas-1.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a68a9b9754efff364b0c5ee5b0f18e15ca640c01afe605d12ba8b239ca304d6b"}, + {file = "pandas-1.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:86d87279ebc5bc20848b4ceb619073490037323f80f515e0ec891c80abad958a"}, + {file = "pandas-1.5.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e252a9e49b233ff96e2815c67c29702ac3a062098d80a170c506dff3470fd060"}, + {file = "pandas-1.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:de34636e2dc04e8ac2136a8d3c2051fd56ebe9fd6cd185581259330649e73ca9"}, + {file = "pandas-1.5.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:1d34b1f43d9e3f4aea056ba251f6e9b143055ebe101ed04c847b41bb0bb4a989"}, + {file = "pandas-1.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1b82ccc7b093e0a93f8dffd97a542646a3e026817140e2c01266aaef5fdde11b"}, + {file = "pandas-1.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4e30a31039574d96f3d683df34ccb50bb435426ad65793e42a613786901f6761"}, + {file = "pandas-1.5.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc987f7717e53d372f586323fff441263204128a1ead053c1b98d7288f836ac9"}, + {file = "pandas-1.5.0-cp38-cp38-win32.whl", hash = "sha256:e178ce2d7e3b934cf8d01dc2d48d04d67cb0abfaffdcc8aa6271fd5a436f39c8"}, + {file = "pandas-1.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:33a9d9e21ab2d91e2ab6e83598419ea6a664efd4c639606b299aae8097c1c94f"}, + {file = "pandas-1.5.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:73844e247a7b7dac2daa9df7339ecf1fcf1dfb8cbfd11e3ffe9819ae6c31c515"}, + {file = "pandas-1.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e9c5049333c5bebf993033f4bf807d163e30e8fada06e1da7fa9db86e2392009"}, + {file = "pandas-1.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:85a516a7f6723ca1528f03f7851fa8d0360d1d6121cf15128b290cf79b8a7f6a"}, + {file = "pandas-1.5.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c7f38d91f21937fe2bec9449570d7bf36ad7136227ef43b321194ec249e2149d"}, + {file = "pandas-1.5.0-cp39-cp39-win32.whl", hash = "sha256:2504c032f221ef9e4a289f5e46a42b76f5e087ecb67d62e342ccbba95a32a488"}, + {file = "pandas-1.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:8a4fc04838615bf0a8d3a03ed68197f358054f0df61f390bcc64fbe39e3d71ec"}, + {file = "pandas-1.5.0.tar.gz", hash = "sha256:3ee61b881d2f64dd90c356eb4a4a4de75376586cd3c9341c6c0fcaae18d52977"}, +] +pathspec = [ + {file = "pathspec-0.10.1-py3-none-any.whl", hash = "sha256:46846318467efc4556ccfd27816e004270a9eeeeb4d062ce5e6fc7a87c573f93"}, + {file = "pathspec-0.10.1.tar.gz", hash = "sha256:7ace6161b621d31e7902eb6b5ae148d12cfd23f4a249b9ffb6b9fee12084323d"}, +] +pep8-naming = [ + {file = "pep8-naming-0.13.2.tar.gz", hash = "sha256:93eef62f525fd12a6f8c98f4dcc17fa70baae2f37fa1f73bec00e3e44392fa48"}, + {file = "pep8_naming-0.13.2-py3-none-any.whl", hash = "sha256:59e29e55c478db69cffbe14ab24b5bd2cd615c0413edf790d47d3fb7ba9a4e23"}, +] +pillow = [ + {file = "Pillow-9.2.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:a9c9bc489f8ab30906d7a85afac4b4944a572a7432e00698a7239f44a44e6efb"}, + {file = "Pillow-9.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:510cef4a3f401c246cfd8227b300828715dd055463cdca6176c2e4036df8bd4f"}, + {file = "Pillow-9.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7888310f6214f19ab2b6df90f3f06afa3df7ef7355fc025e78a3044737fab1f5"}, + {file = "Pillow-9.2.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:831e648102c82f152e14c1a0938689dbb22480c548c8d4b8b248b3e50967b88c"}, + {file = "Pillow-9.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1cc1d2451e8a3b4bfdb9caf745b58e6c7a77d2e469159b0d527a4554d73694d1"}, + {file = "Pillow-9.2.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:136659638f61a251e8ed3b331fc6ccd124590eeff539de57c5f80ef3a9594e58"}, + {file = "Pillow-9.2.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:6e8c66f70fb539301e064f6478d7453e820d8a2c631da948a23384865cd95544"}, + {file = "Pillow-9.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:37ff6b522a26d0538b753f0b4e8e164fdada12db6c6f00f62145d732d8a3152e"}, + {file = "Pillow-9.2.0-cp310-cp310-win32.whl", hash = "sha256:c79698d4cd9318d9481d89a77e2d3fcaeff5486be641e60a4b49f3d2ecca4e28"}, + {file = "Pillow-9.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:254164c57bab4b459f14c64e93df11eff5ded575192c294a0c49270f22c5d93d"}, + {file = "Pillow-9.2.0-cp311-cp311-macosx_10_10_universal2.whl", hash = "sha256:408673ed75594933714482501fe97e055a42996087eeca7e5d06e33218d05aa8"}, + {file = "Pillow-9.2.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:727dd1389bc5cb9827cbd1f9d40d2c2a1a0c9b32dd2261db522d22a604a6eec9"}, + {file = "Pillow-9.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50dff9cc21826d2977ef2d2a205504034e3a4563ca6f5db739b0d1026658e004"}, + {file = "Pillow-9.2.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cb6259196a589123d755380b65127ddc60f4c64b21fc3bb46ce3a6ea663659b0"}, + {file = "Pillow-9.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b0554af24df2bf96618dac71ddada02420f946be943b181108cac55a7a2dcd4"}, + {file = "Pillow-9.2.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:15928f824870535c85dbf949c09d6ae7d3d6ac2d6efec80f3227f73eefba741c"}, + {file = "Pillow-9.2.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:bdd0de2d64688ecae88dd8935012c4a72681e5df632af903a1dca8c5e7aa871a"}, + {file = "Pillow-9.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d5b87da55a08acb586bad5c3aa3b86505f559b84f39035b233d5bf844b0834b1"}, + {file = "Pillow-9.2.0-cp311-cp311-win32.whl", hash = "sha256:b6d5e92df2b77665e07ddb2e4dbd6d644b78e4c0d2e9272a852627cdba0d75cf"}, + {file = "Pillow-9.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:6bf088c1ce160f50ea40764f825ec9b72ed9da25346216b91361eef8ad1b8f8c"}, + {file = "Pillow-9.2.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:2c58b24e3a63efd22554c676d81b0e57f80e0a7d3a5874a7e14ce90ec40d3069"}, + {file = "Pillow-9.2.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eef7592281f7c174d3d6cbfbb7ee5984a671fcd77e3fc78e973d492e9bf0eb3f"}, + {file = "Pillow-9.2.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dcd7b9c7139dc8258d164b55696ecd16c04607f1cc33ba7af86613881ffe4ac8"}, + {file = "Pillow-9.2.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a138441e95562b3c078746a22f8fca8ff1c22c014f856278bdbdd89ca36cff1b"}, + {file = "Pillow-9.2.0-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:93689632949aff41199090eff5474f3990b6823404e45d66a5d44304e9cdc467"}, + {file = "Pillow-9.2.0-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:f3fac744f9b540148fa7715a435d2283b71f68bfb6d4aae24482a890aed18b59"}, + {file = "Pillow-9.2.0-cp37-cp37m-win32.whl", hash = "sha256:fa768eff5f9f958270b081bb33581b4b569faabf8774726b283edb06617101dc"}, + {file = "Pillow-9.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:69bd1a15d7ba3694631e00df8de65a8cb031911ca11f44929c97fe05eb9b6c1d"}, + {file = "Pillow-9.2.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:030e3460861488e249731c3e7ab59b07c7853838ff3b8e16aac9561bb345da14"}, + {file = "Pillow-9.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:74a04183e6e64930b667d321524e3c5361094bb4af9083db5c301db64cd341f3"}, + {file = "Pillow-9.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d33a11f601213dcd5718109c09a52c2a1c893e7461f0be2d6febc2879ec2402"}, + {file = "Pillow-9.2.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fd6f5e3c0e4697fa7eb45b6e93996299f3feee73a3175fa451f49a74d092b9f"}, + {file = "Pillow-9.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a647c0d4478b995c5e54615a2e5360ccedd2f85e70ab57fbe817ca613d5e63b8"}, + {file = "Pillow-9.2.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:4134d3f1ba5f15027ff5c04296f13328fecd46921424084516bdb1b2548e66ff"}, + {file = "Pillow-9.2.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:bc431b065722a5ad1dfb4df354fb9333b7a582a5ee39a90e6ffff688d72f27a1"}, + {file = "Pillow-9.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:1536ad017a9f789430fb6b8be8bf99d2f214c76502becc196c6f2d9a75b01b76"}, + {file = "Pillow-9.2.0-cp38-cp38-win32.whl", hash = "sha256:2ad0d4df0f5ef2247e27fc790d5c9b5a0af8ade9ba340db4a73bb1a4a3e5fb4f"}, + {file = "Pillow-9.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:ec52c351b35ca269cb1f8069d610fc45c5bd38c3e91f9ab4cbbf0aebc136d9c8"}, + {file = "Pillow-9.2.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:0ed2c4ef2451de908c90436d6e8092e13a43992f1860275b4d8082667fbb2ffc"}, + {file = "Pillow-9.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4ad2f835e0ad81d1689f1b7e3fbac7b01bb8777d5a985c8962bedee0cc6d43da"}, + {file = "Pillow-9.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea98f633d45f7e815db648fd7ff0f19e328302ac36427343e4432c84432e7ff4"}, + {file = "Pillow-9.2.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7761afe0126d046974a01e030ae7529ed0ca6a196de3ec6937c11df0df1bc91c"}, + {file = "Pillow-9.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a54614049a18a2d6fe156e68e188da02a046a4a93cf24f373bffd977e943421"}, + {file = "Pillow-9.2.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:5aed7dde98403cd91d86a1115c78d8145c83078e864c1de1064f52e6feb61b20"}, + {file = "Pillow-9.2.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:13b725463f32df1bfeacbf3dd197fb358ae8ebcd8c5548faa75126ea425ccb60"}, + {file = "Pillow-9.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:808add66ea764ed97d44dda1ac4f2cfec4c1867d9efb16a33d158be79f32b8a4"}, + {file = "Pillow-9.2.0-cp39-cp39-win32.whl", hash = "sha256:337a74fd2f291c607d220c793a8135273c4c2ab001b03e601c36766005f36885"}, + {file = "Pillow-9.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:fac2d65901fb0fdf20363fbd345c01958a742f2dc62a8dd4495af66e3ff502a4"}, + {file = "Pillow-9.2.0-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:ad2277b185ebce47a63f4dc6302e30f05762b688f8dc3de55dbae4651872cdf3"}, + {file = "Pillow-9.2.0-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c7b502bc34f6e32ba022b4a209638f9e097d7a9098104ae420eb8186217ebbb"}, + {file = "Pillow-9.2.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d1f14f5f691f55e1b47f824ca4fdcb4b19b4323fe43cc7bb105988cad7496be"}, + {file = "Pillow-9.2.0-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:dfe4c1fedfde4e2fbc009d5ad420647f7730d719786388b7de0999bf32c0d9fd"}, + {file = "Pillow-9.2.0-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:f07f1f00e22b231dd3d9b9208692042e29792d6bd4f6639415d2f23158a80013"}, + {file = "Pillow-9.2.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1802f34298f5ba11d55e5bb09c31997dc0c6aed919658dfdf0198a2fe75d5490"}, + {file = "Pillow-9.2.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17d4cafe22f050b46d983b71c707162d63d796a1235cdf8b9d7a112e97b15bac"}, + {file = "Pillow-9.2.0-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:96b5e6874431df16aee0c1ba237574cb6dff1dcb173798faa6a9d8b399a05d0e"}, + {file = "Pillow-9.2.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:0030fdbd926fb85844b8b92e2f9449ba89607231d3dd597a21ae72dc7fe26927"}, + {file = "Pillow-9.2.0.tar.gz", hash = "sha256:75e636fd3e0fb872693f23ccb8a5ff2cd578801251f3a4f6854c6a5d437d3c04"}, +] +platformdirs = [ + {file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"}, + {file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"}, +] +pluggy = [ + {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, + {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, +] +py = [ + {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, + {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, +] +pyarrow = [ + {file = "pyarrow-9.0.0-cp310-cp310-macosx_10_13_universal2.whl", hash = "sha256:767cafb14278165ad539a2918c14c1b73cf20689747c21375c38e3fe62884902"}, + {file = "pyarrow-9.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0238998dc692efcb4e41ae74738d7c1234723271ccf520bd8312dca07d49ef8d"}, + {file = "pyarrow-9.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:55328348b9139c2b47450d512d716c2248fd58e2f04e2fc23a65e18726666d42"}, + {file = "pyarrow-9.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fc856628acd8d281652c15b6268ec7f27ebcb015abbe99d9baad17f02adc51f1"}, + {file = "pyarrow-9.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29eb3e086e2b26202f3a4678316b93cfb15d0e2ba20f3ec12db8fd9cc07cde63"}, + {file = "pyarrow-9.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e753f8fcf07d8e3a0efa0c8bd51fef5c90281ffd4c5637c08ce42cd0ac297de"}, + {file = "pyarrow-9.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:3eef8a981f45d89de403e81fb83b8119c20824caddf1404274e41a5d66c73806"}, + {file = "pyarrow-9.0.0-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:7fa56cbd415cef912677270b8e41baad70cde04c6d8a8336eeb2aba85aa93706"}, + {file = "pyarrow-9.0.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:f8c46bde1030d704e2796182286d1c56846552c50a39ad5bf5a20c0d8159fc35"}, + {file = "pyarrow-9.0.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ad430cee28ebc4d6661fc7315747c7a18ae2a74e67498dcb039e1c762a2fb67"}, + {file = "pyarrow-9.0.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81a60bb291a964f63b2717fb1b28f6615ffab7e8585322bfb8a6738e6b321282"}, + {file = "pyarrow-9.0.0-cp37-cp37m-win_amd64.whl", hash = "sha256:9cef618159567d5f62040f2b79b1c7b38e3885f4ffad0ec97cd2d86f88b67cef"}, + {file = "pyarrow-9.0.0-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:5526a3bfb404ff6d31d62ea582cf2466c7378a474a99ee04d1a9b05de5264541"}, + {file = "pyarrow-9.0.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:da3e0f319509a5881867effd7024099fb06950a0768dad0d6873668bb88cfaba"}, + {file = "pyarrow-9.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2c715eca2092273dcccf6f08437371e04d112f9354245ba2fbe6c801879450b7"}, + {file = "pyarrow-9.0.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f11a645a41ee531c3a5edda45dea07c42267f52571f818d388971d33fc7e2d4a"}, + {file = "pyarrow-9.0.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5b390bdcfb8c5b900ef543f911cdfec63e88524fafbcc15f83767202a4a2491"}, + {file = "pyarrow-9.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:d9eb04db626fa24fdfb83c00f76679ca0d98728cdbaa0481b6402bf793a290c0"}, + {file = "pyarrow-9.0.0-cp39-cp39-macosx_10_13_universal2.whl", hash = "sha256:4eebdab05afa23d5d5274b24c1cbeb1ba017d67c280f7d39fd8a8f18cbad2ec9"}, + {file = "pyarrow-9.0.0-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:02b820ecd1da02012092c180447de449fc688d0c3f9ff8526ca301cdd60dacd0"}, + {file = "pyarrow-9.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:92f3977e901db1ef5cba30d6cc1d7942b8d94b910c60f89013e8f7bb86a86eef"}, + {file = "pyarrow-9.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f241bd488c2705df930eedfe304ada71191dcf67d6b98ceda0cc934fd2a8388e"}, + {file = "pyarrow-9.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c5a073a930c632058461547e0bc572da1e724b17b6b9eb31a97da13f50cb6e0"}, + {file = "pyarrow-9.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f59bcd5217a3ae1e17870792f82b2ff92df9f3862996e2c78e156c13e56ff62e"}, + {file = "pyarrow-9.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:fe2ce795fa1d95e4e940fe5661c3c58aee7181c730f65ac5dd8794a77228de59"}, + {file = "pyarrow-9.0.0.tar.gz", hash = "sha256:7fb02bebc13ab55573d1ae9bb5002a6d20ba767bf8569b52fce5301d42495ab7"}, +] +pycodestyle = [ + {file = "pycodestyle-2.9.1-py2.py3-none-any.whl", hash = "sha256:d1735fc58b418fd7c5f658d28d943854f8a849b01a5d0a1e6f3f3fdd0166804b"}, + {file = "pycodestyle-2.9.1.tar.gz", hash = "sha256:2c9607871d58c76354b697b42f5d57e1ada7d261c261efac224b664affdc5785"}, +] +pyflakes = [ + {file = "pyflakes-2.5.0-py2.py3-none-any.whl", hash = "sha256:4579f67d887f804e67edb544428f264b7b24f435b263c4614f384135cea553d2"}, + {file = "pyflakes-2.5.0.tar.gz", hash = "sha256:491feb020dca48ccc562a8c0cbe8df07ee13078df59813b83959cbdada312ea3"}, +] +pygments = [ + {file = "Pygments-2.13.0-py3-none-any.whl", hash = "sha256:f643f331ab57ba3c9d89212ee4a2dabc6e94f117cf4eefde99a0574720d14c42"}, + {file = "Pygments-2.13.0.tar.gz", hash = "sha256:56a8508ae95f98e2b9bdf93a6be5ae3f7d8af858b43e02c5a2ff083726be40c1"}, +] +pyparsing = [ + {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"}, + {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"}, +] +pytest = [ + {file = "pytest-7.1.3-py3-none-any.whl", hash = "sha256:1377bda3466d70b55e3f5cecfa55bb7cfcf219c7964629b967c37cf0bda818b7"}, + {file = "pytest-7.1.3.tar.gz", hash = "sha256:4f365fec2dff9c1162f834d9f18af1ba13062db0c708bf7b946f8a5c76180c39"}, +] +python-dateutil = [ + {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, + {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, +] +pytz = [ + {file = "pytz-2022.2.1-py2.py3-none-any.whl", hash = "sha256:220f481bdafa09c3955dfbdddb7b57780e9a94f5127e35456a48589b9e0c0197"}, + {file = "pytz-2022.2.1.tar.gz", hash = "sha256:cea221417204f2d1a2aa03ddae3e867921971d0d76f14d87abb4414415bbdcf5"}, +] +requests = [ + {file = "requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"}, + {file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"}, +] +rich = [ + {file = "rich-12.5.1-py3-none-any.whl", hash = "sha256:2eb4e6894cde1e017976d2975ac210ef515d7548bc595ba20e195fb9628acdeb"}, + {file = "rich-12.5.1.tar.gz", hash = "sha256:63a5c5ce3673d3d5fbbf23cd87e11ab84b6b451436f1b7f19ec54b6bc36ed7ca"}, +] +scikit-learn = [ + {file = "scikit-learn-1.1.2.tar.gz", hash = "sha256:7c22d1305b16f08d57751a4ea36071e2215efb4c09cb79183faa4e8e82a3dbf8"}, + {file = "scikit_learn-1.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6c840f662b5d3377c4ccb8be1fc21bb52cb5d8b8790f8d6bf021739f84e543cf"}, + {file = "scikit_learn-1.1.2-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:2b8db962360c93554cab7bb3c096c4a24695da394dd4b3c3f13409f409b425bc"}, + {file = "scikit_learn-1.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e7d1fc817867a350133f937aaebcafbc06192517cbdf0cf7e5774ad4d1adb9f"}, + {file = "scikit_learn-1.1.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ec3ea40d467966821843210c02117d82b097b54276fdcfb50f4dfb5c60dbe39"}, + {file = "scikit_learn-1.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:bbef6ea1c012ff9f3e6f6e9ca006b8772d8383e177b898091e68fbd9b3f840f9"}, + {file = "scikit_learn-1.1.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a90ca42fe8242fd6ff56cda2fecc5fca586a88a24ab602d275d2d0dcc0b928fb"}, + {file = "scikit_learn-1.1.2-cp38-cp38-macosx_12_0_arm64.whl", hash = "sha256:a682ec0f82b6f30fb07486daed1c8001b6683cc66b51877644dfc532bece6a18"}, + {file = "scikit_learn-1.1.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c33e16e9a165af6012f5be530ccfbb672e2bc5f9b840238a05eb7f6694304e3f"}, + {file = "scikit_learn-1.1.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f94c0146bad51daef919c402a3da8c1c6162619653e1c00c92baa168fda292f2"}, + {file = "scikit_learn-1.1.2-cp38-cp38-win32.whl", hash = "sha256:2f46c6e3ff1054a5ec701646dcfd61d43b8ecac4d416014daed8843cf4c33d4d"}, + {file = "scikit_learn-1.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:b1e706deca9b2ad87ae27dafd5ac4e8eff01b6db492ed5c12cef4735ec5f21ea"}, + {file = "scikit_learn-1.1.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:567417dbbe6a6278399c3e6daf1654414a5a1a4d818d28f251fa7fc28730a1bf"}, + {file = "scikit_learn-1.1.2-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:d6f232779023c3b060b80b5c82e5823723bc424dcac1d1a148aa2492c54d245d"}, + {file = "scikit_learn-1.1.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:589d46f28460469f444b898223b13d99db9463e1038dc581ba698111f612264b"}, + {file = "scikit_learn-1.1.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76800652fb6d6bf527bce36ecc2cc25738b28fe1a17bd294a218fff8e8bd6d50"}, + {file = "scikit_learn-1.1.2-cp39-cp39-win32.whl", hash = "sha256:1c8fecb7c9984d9ec2ea48898229f98aad681a0873e0935f2b7f724fbce4a047"}, + {file = "scikit_learn-1.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:407e9a1cb9e6ba458a539986a9bd25546a757088095b3aab91d465b79a760d37"}, +] +scipy = [ + {file = "scipy-1.9.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c61b4a91a702e8e04aeb0bfc40460e1f17a640977c04dda8757efb0199c75332"}, + {file = "scipy-1.9.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:d79da472015d0120ba9b357b28a99146cd6c17b9609403164b1a8ed149b4dfc8"}, + {file = "scipy-1.9.1-cp310-cp310-macosx_12_0_universal2.macosx_10_9_x86_64.whl", hash = "sha256:825951b88f56765aeb6e5e38ac9d7d47407cfaaeb008d40aa1b45a2d7ea2731e"}, + {file = "scipy-1.9.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f950a04b33e17b38ff561d5a0951caf3f5b47caa841edd772ffb7959f20a6af0"}, + {file = "scipy-1.9.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cc81ac25659fec73599ccc52c989670e5ccd8974cf34bacd7b54a8d809aff1a"}, + {file = "scipy-1.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:8d3faa40ac16c6357aaf7ea50394ea6f1e8e99d75e927a51102b1943b311b4d9"}, + {file = "scipy-1.9.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7a412c476a91b080e456229e413792bbb5d6202865dae963d1e6e28c2bb58691"}, + {file = "scipy-1.9.1-cp38-cp38-macosx_12_0_arm64.whl", hash = "sha256:eb954f5aca4d26f468bbebcdc5448348eb287f7bea536c6306f62ea062f63d9a"}, + {file = "scipy-1.9.1-cp38-cp38-macosx_12_0_universal2.macosx_10_9_x86_64.whl", hash = "sha256:3c6f5d1d4b9a5e4fe5e14f26ffc9444fc59473bbf8d45dc4a9a15283b7063a72"}, + {file = "scipy-1.9.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:bc4e2c77d4cd015d739e75e74ebbafed59ba8497a7ed0fd400231ed7683497c4"}, + {file = "scipy-1.9.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0419485dbcd0ed78c0d5bf234c5dd63e86065b39b4d669e45810d42199d49521"}, + {file = "scipy-1.9.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:34441dfbee5b002f9e15285014fd56e5e3372493c3e64ae297bae2c4b9659f5a"}, + {file = "scipy-1.9.1-cp38-cp38-win32.whl", hash = "sha256:b97b479f39c7e4aaf807efd0424dec74bbb379108f7d22cf09323086afcd312c"}, + {file = "scipy-1.9.1-cp38-cp38-win_amd64.whl", hash = "sha256:e8fe305d9d67a81255e06203454729405706907dccbdfcc330b7b3482a6c371d"}, + {file = "scipy-1.9.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:39ab9240cd215a9349c85ab908dda6d732f7d3b4b192fa05780812495536acc4"}, + {file = "scipy-1.9.1-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:71487c503e036740635f18324f62a11f283a632ace9d35933b2b0a04fd898c98"}, + {file = "scipy-1.9.1-cp39-cp39-macosx_12_0_universal2.macosx_10_9_x86_64.whl", hash = "sha256:3bc1ab68b9a096f368ba06c3a5e1d1d50957a86665fc929c4332d21355e7e8f4"}, + {file = "scipy-1.9.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f7c39f7dbb57cce00c108d06d731f3b0e2a4d3a95c66d96bce697684876ce4d4"}, + {file = "scipy-1.9.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47d1a95bd9d37302afcfe1b84c8011377c4f81e33649c5a5785db9ab827a6ade"}, + {file = "scipy-1.9.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96d7cf7b25c9f23c59a766385f6370dab0659741699ecc7a451f9b94604938ce"}, + {file = "scipy-1.9.1-cp39-cp39-win32.whl", hash = "sha256:09412eb7fb60b8f00b328037fd814d25d261066ebc43a1e339cdce4f7502877e"}, + {file = "scipy-1.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:90c805f30c46cf60f1e76e947574f02954d25e3bb1e97aa8a07bc53aa31cf7d1"}, + {file = "scipy-1.9.1.tar.gz", hash = "sha256:26d28c468900e6d5fdb37d2812ab46db0ccd22c63baa095057871faa3a498bc9"}, +] +setuptools-scm = [ + {file = "setuptools_scm-7.0.5-py3-none-any.whl", hash = "sha256:7930f720905e03ccd1e1d821db521bff7ec2ac9cf0ceb6552dd73d24a45d3b02"}, + {file = "setuptools_scm-7.0.5.tar.gz", hash = "sha256:031e13af771d6f892b941adb6ea04545bbf91ebc5ce68c78aaf3fff6e1fb4844"}, +] +six = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] +soupsieve = [ + {file = "soupsieve-2.3.2.post1-py3-none-any.whl", hash = "sha256:3b2503d3c7084a42b1ebd08116e5f81aadfaea95863628c80a3b774a11b7c759"}, + {file = "soupsieve-2.3.2.post1.tar.gz", hash = "sha256:fc53893b3da2c33de295667a0e19f078c14bf86544af307354de5fcf12a3f30d"}, +] +threadpoolctl = [ + {file = "threadpoolctl-3.1.0-py3-none-any.whl", hash = "sha256:8b99adda265feb6773280df41eece7b2e6561b772d21ffd52e372f999024907b"}, + {file = "threadpoolctl-3.1.0.tar.gz", hash = "sha256:a335baacfaa4400ae1f0d8e3a58d6674d2f8828e3716bb2802c44955ad391380"}, +] +toml = [ + {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, + {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, +] +tomli = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] +tqdm = [ + {file = "tqdm-4.64.1-py2.py3-none-any.whl", hash = "sha256:6fee160d6ffcd1b1c68c65f14c829c22832bc401726335ce92c52d395944a6a1"}, + {file = "tqdm-4.64.1.tar.gz", hash = "sha256:5f4f682a004951c1b450bc753c710e9280c5746ce6ffedee253ddbcbf54cf1e4"}, +] +tryceratops = [ + {file = "tryceratops-1.1.0-py3-none-any.whl", hash = "sha256:f4770960782a2ae87cad7a7fd1206476468d0f41383c9fd7a49c6d537534015e"}, + {file = "tryceratops-1.1.0.tar.gz", hash = "sha256:58e56b33eeb7a9cfd643957069bb1872ebe26c1eeac7abb1877c9535b7e473db"}, +] +typing-extensions = [ + {file = "typing_extensions-4.3.0-py3-none-any.whl", hash = "sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02"}, + {file = "typing_extensions-4.3.0.tar.gz", hash = "sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6"}, +] +urllib3 = [ + {file = "urllib3-1.26.12-py2.py3-none-any.whl", hash = "sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997"}, + {file = "urllib3-1.26.12.tar.gz", hash = "sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e"}, +] +win32-setctime = [ + {file = "win32_setctime-1.1.0-py3-none-any.whl", hash = "sha256:231db239e959c2fe7eb1d7dc129f11172354f98361c4fa2d6d2d7e278baa8aad"}, + {file = "win32_setctime-1.1.0.tar.gz", hash = "sha256:15cf5750465118d6929ae4de4eb46e8edae9a5634350c01ba582df868e932cb2"}, +] diff --git a/model/pyproject.toml b/model/pyproject.toml new file mode 100644 index 0000000..41d7b15 --- /dev/null +++ b/model/pyproject.toml @@ -0,0 +1,45 @@ +[tool.poetry] +name = "src" +version = "0.1.0" +description = "" +authors = ["Caesar Balona "] + +[tool.poetry.dependencies] +python = ">=3.9,<3.12" +beautifulsoup4 = ">=4.11" +requests = ">=2.28" +loguru = ">=0.6" +tqdm = ">=4.64" +pandas = ">=1.4" +matplotlib = ">=3.5" +tomli = ">=2.0" +pyarrow = ">=9.0" +scikit-learn = ">=1.1" +click = "^8.1.3" + +[tool.poetry.dev-dependencies] +autoflake = "*" +black = "*" +flake8 = "*" +flake8-bugbear = "*" +flake8-builtins = "*" +flake8-comprehensions = "*" +flake8-debugger = "*" +flake8-eradicate = "*" +isort = "*" +mypy = "*" +pep8-naming = "*" +pytest = "*" +tryceratops = "*" + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" + +[tool.isort] +profile = "black" +src_paths = ["src", "tests"] + +[tool.black] +target-version = ["py39", "py310"] +include = '\.pyi?$' diff --git a/model/references/data_schema.toml b/model/references/data_schema.toml new file mode 100644 index 0000000..1456187 --- /dev/null +++ b/model/references/data_schema.toml @@ -0,0 +1,160 @@ +[seattle.streets.columns] +OBJECTID = "ObjectId" +ARTCLASS = "ArterialCode" +SPEEDLIMIT = "SpeedLimitMPH" +SEGLENGTH = "Length" + +[seattle.streets.arterial_code] +NOT_DESIGNATED = 0 +PRINCIPAL_ARTERIAL = 1 +MINOR_ARTERIAL = 2 +COLLECTOR_ARTERIAL = 3 +STATE_HIGHWAY = 4 +INTERSTATE_FREEWAY = 5 + +[seattle.intersections.columns] +INTR_ID = "IntersectionId" +SHAPE_LNG = "Longitude" +SHAPE_LAT = "Latitude" + +[seattle.volume.weekday_of_data] +MONDAY = 0 +TUESDAY = 1 +WEDNESDAY = 2 +THURSDAY = 3 +FRIDAY = 4 +SATURDAY = 5 +SUNDAY = 6 + +[seattle.weather.columns] +time = "Time" +rain = "Rainfall" +snowfall = "Snowfall" +cloudcover = "Cloudcover" +cloudcover_low = "CloudcoverLow" +relativehumidity_2m = "RelativeHumidity" + +[seattle.weather.weekday_of_data] +MONDAY = 0 +TUESDAY = 1 +WEDNESDAY = 2 +THURSDAY = 3 +FRIDAY = 4 +SATURDAY = 5 +SUNDAY = 6 + +[seattle.collisions.columns] +OBJECTID = "ObjectId" +INCKEY = "IncidentId" +COLDETKEY = "CollisionId" +FATALITIES = "NumFatalities" +INCDTTM = "Time" +INJURIES = "NumInjuries" +INTKEY = "IntersectionId" +LIGHTCOND = "LightCondition" +PEDCOUNT = "NumPedestrians" +PEDCYLCOUNT = "NumCyclists" +PERSONCOUNT = "NumPersons" +SERIOUSINJURIES = "NumSeriousInjuries" +SEVERITYCODE = "SeverityCode" +STATUS = "MatchingStatus" +VEHCOUNT = "NumVehicles" +WEATHER = "Weather" +LONGITUDE = "Longitude" +LATITUDE = "Latitude" + +[seattle.collisions.weekday_of_data] +MONDAY = 0 +TUESDAY = 1 +WEDNESDAY = 2 +THURSDAY = 3 +FRIDAY = 4 +SATURDAY = 5 +SUNDAY = 6 + +[seattle.collisions.severity_code] +UNKNONW = "0" +PROPERTY_DAMAGE = "1" +INJURY = "2" +SERIOUS_INJURY = "2b" +FATALITY = 3 + +[crss.accident.columns] +CASENUM = "CaseId" +HOUR_IM = "HourofData" +WEATHR_IM = "Weather" + +[crss.vehicle.columns] +CASENUM = "CaseId" +VEH_NO = "VehicleId" +MDLYR_IM = "VehicleYear" +BDYTYP_IM = "BodyType" +TRAV_SP = "Speed" +VSPD_LIM = "SpeedLimit" +MXVSEV_IM = "MaximumSeverity" +V_ALCH_IM = "Alcohol" + +[crss.person.columns] +CASENUM = "CaseId" +AGE_IM = "AgeofDriver" +PER_TYP = "PersonType" + +[crss.distract.columns] +CASENUM = "CaseId" +MDRDSTRD = "Distraction" +DRDISTRACT = "Distraction" + +[crss.accident.weather] +CLEAR = 1 +RAIN = 2 +SLEET_HAIL = 3 +SNOW = 4 +FOG = 5 +SEVERE_CROSSWINDS = 6 +BLOWING_SAND_SOIL_DIRT = 7 +OTHER = 8 +CLOUDY = 10 +BLOWING_SNOW = 11 +FREEZING_RAIN = 12 +NOT_REPORTED = 98 +UNKNOWN = 99 + +[crss.vehicle.maximum_severity] +NO_INJURY = 0 +POSSIBLE_INJURY = 1 +MINOR_INJURY = 2 +SERIOUS_INJURY = 3 +FATAL = 4 +INJURED_UNKNOWN_SEVERITY = 5 +DIED_PRIOR = 6 +NO_OCCUPANTS = 8 +UNKNOWN = 9 + +[crss.vehicle.alcohol] +ALCOHOL_INVOLVED = 1 +NO_ALCOHOL_INVOLVED = 2 + +[crss.distract.distraction] +NOT_DISTRACTED = 0 +LOOKED_BUT_DID_NOT_SEE = 1 +BY_OTHER_OCCUPANTS = 3 +BY_A_MOVING_OBJECT_IN_VEHICLE = 4 +WHILE_TALKING_OR_LISTENING_PHONE = 5 +WHILE_MANIPULATING_PHONE = 6 +WHILE_ADJUSTING_AUDIO_CLIMATE = 7 +WHILE_ADJUSTING_OTHER = 9 +WHILE_USING_REACHING_DEVICE = 10 +OUTSIDE_PERSON_OBJECT = 12 +EATING_DRINKING = 13 +SMOKING = 14 +OTHER_MOBILE_PHONE = 15 +NO_DRIVER = 16 +INATTENTION = 17 +CARELESS = 18 +INATTENTIVE = 19 +DISTRACTED_UNKNOWN = 92 +INATTENTIVE_UNKNOWN = 93 +NOT_REPORTED = 96 +DAY_DREAMING = 97 +OTHER = 98 +UNKNOWN = 99 \ No newline at end of file diff --git a/model/src/__init__.py b/model/src/__init__.py new file mode 100644 index 0000000..6d00abb --- /dev/null +++ b/model/src/__init__.py @@ -0,0 +1,42 @@ +# fmt: off +from pathlib import Path + +import tomli + +# Paths +ROOT_DIR = Path(__file__).resolve().parents[1] + +RAW_DATA_DIR = ROOT_DIR / "data" / "raw" # noqa: E221 +PROC_DATA_DIR = ROOT_DIR / "data" / "processed" # noqa: E221 + +MODELS_DIR = ROOT_DIR / "models" # noqa: E221 + + +# Configuration +with open(ROOT_DIR / "config.toml", "rb") as f: + CONFIG = tomli.load(f) + +with open(ROOT_DIR / "references" / "data_schema.toml", "rb") as f: + DATA_SCHEMA = tomli.load(f) + + +# Constants +EPSILON = 1e-7 + +SECONDS_PER_MINUTE = 60 # noqa: E221 +SECONDS_PER_HOUR = 60 * SECONDS_PER_MINUTE # noqa: E221 +SECONDS_PER_DAY = 24 * SECONDS_PER_HOUR # noqa: E221 +SECONDS_PER_WEEK = 7 * SECONDS_PER_DAY # noqa: E221 + +# 1 interval is 5 minutes or 300 seconds +INTERVAL = 300 # noqa: E221 +INTERVALS_PER_MINUTE = SECONDS_PER_MINUTE / INTERVAL # noqa: E221 +INTERVALS_PER_HOUR = SECONDS_PER_HOUR / INTERVAL # noqa: E221 +INTERVALS_PER_DAY = SECONDS_PER_DAY / INTERVAL # noqa: E221 +INTERVALS_PER_WEEK = SECONDS_PER_WEEK / INTERVAL # noqa: E221 + +INTERVALS_PER_TIME_OF_DAY = { + label: (interval[1] - interval[0]) * INTERVALS_PER_HOUR + for label, interval in CONFIG["data"]["hour_bins"].items() +} +# fmt: on diff --git a/model/src/charts/make_charts.py b/model/src/charts/make_charts.py new file mode 100644 index 0000000..3dc9230 --- /dev/null +++ b/model/src/charts/make_charts.py @@ -0,0 +1,57 @@ +import json +from pathlib import Path +import click +import pandas as pd +from src import PROC_DATA_DIR +from src.models.frequency import load_modelling_data + + +def frequency_by_hour(): + df = load_modelling_data() + df = df.groupby(["HourofData"])[["NumVehicles", "TotalVolume"]].sum() + df["Frequency"] = df.NumVehicles / df.TotalVolume + df.Frequency.to_json(PROC_DATA_DIR / "frequency_by_hour.json") + + +def frequency_by_weather(): + df = load_modelling_data() + df = df.groupby(["Weather"])[["NumVehicles", "TotalVolume"]].sum() + df["Frequency"] = df.NumVehicles / df.TotalVolume + df.Frequency.to_json(PROC_DATA_DIR / "frequency_by_weather.json") + + +def frequency_heatmap(): + street = pd.read_feather(PROC_DATA_DIR / "streets.f") + street = street[["PointId", "Longitude", "Latitude"]].drop_duplicates().copy() + + risk = pd.read_feather(PROC_DATA_DIR / "risk.f") + risk = risk[["StationId", "NumVehicles", "Longitude", "Latitude"]].copy() + df = street.merge(risk, on=["Longitude", "Latitude"]).drop_duplicates() + + # Exposure data + exposure = pd.read_feather(PROC_DATA_DIR / "exposure.f") + exposure = exposure.groupby(["StationId"])[["TotalVolume"]].sum() + exposure = exposure[exposure.TotalVolume > 0] + exposure["TotalVolume"] = exposure["TotalVolume"] / 1000 # Per 1000 vehicles + + df = df.merge(exposure, on=["StationId"]) + df["Frequency"] = df.NumVehicles / df.TotalVolume + + with open(PROC_DATA_DIR / "frequency_heatmap.json", "w") as f: + json.dump(df[["Latitude", "Longitude", "Frequency"]].to_numpy().tolist(), f) + + +@click.command() +def main(): + """ """ + + frequency_by_hour() + frequency_by_weather() + frequency_heatmap() + + +if __name__ == "__main__": + # not used in this stub but often useful for finding various files + project_dir = Path(__file__).resolve().parents[2] + + main() diff --git a/model/src/data/__init__.py b/model/src/data/__init__.py new file mode 100644 index 0000000..d47ef1d --- /dev/null +++ b/model/src/data/__init__.py @@ -0,0 +1,96 @@ +import os +import re +from pathlib import Path +from types import MappingProxyType +from typing import List, Mapping + +import pandas as pd +import requests +from bs4 import BeautifulSoup +from loguru import logger +from tqdm import tqdm + +from src import CONFIG + + +def download_file( + url: str, filename: Path, metadata: dict = MappingProxyType({}) +) -> None: + + meta = [f"{k}: {v}" for k, v in metadata.items()] + + r = requests.get(url, stream=True) + if r.ok: + logger.info(f"Downloading: {url} - {' - '.join(meta)}") + + with open(filename, "wb") as f: + for chunk in tqdm(r.iter_content(chunk_size=1024 * 8)): + if chunk: + f.write(chunk) + f.flush() + os.fsync(f.fileno()) + else: + logger.warning(f"Download failed: status code {r.status_code}\n{r.text}") + + +def traverse_nhtsa_and_download_files( + start_folder: str, + save_folder: Path, + years: List[int] = None, + overwrite: bool = False, +) -> None: + url = f"https://www.nhtsa.gov/file-downloads?p={start_folder}" + years = years or [] + + source = requests.get(url).text + soup = BeautifulSoup(source, "html.parser") + + save_path = save_folder / start_folder + save_path.mkdir(parents=True, exist_ok=True) + + table = soup.find("table", id="nhtsa_s3") + for row in table.find_all("tr"): + cells = row.find_all("td") + if len(cells) < 2: + continue + if cells[0].find("img").get("src").split("/")[-1] == "up.png": + continue + if re.match(r"^\d{4}\/$", cells[1].find("a").text): + if ( + len(years) > 0 + and int(re.findall(r"\d+", cells[1].find("a").text)[0]) not in years + ): + continue + if cells[0].find("img").get("src").split("/")[-1] == "dir.png": + traverse_nhtsa_and_download_files( + start_folder + cells[1].text, + save_folder=save_folder, + years=years, + overwrite=overwrite, + ) + if cells[0].find("img").get("src").split("/")[-1] == "file.png": + file_url = cells[1].find("a").get("href") + file_name = cells[1].find("a").text + + if os.path.exists(save_path / file_name) and not overwrite: + logger.warning(f"File {file_name} already exists.") + continue + + download_file(file_url, save_path / file_name, {"Filesize": cells[2].text}) + + +def encode_categories(encoding: Mapping, reverse: bool = False) -> Mapping: + if reverse: + return {v: k for k, v in encoding.items()} + return encoding + + +def bin_data(col: pd.Series, bin_config_key: str) -> pd.Series: + bins = CONFIG["data"][bin_config_key] + labels = list(bins.keys()) + intervals = [tuple(interval) for interval in bins.values()] + + binned = pd.cut(col, bins=pd.IntervalIndex.from_tuples(intervals, closed="left")) + binned = binned.cat.rename_categories(labels) + + return binned diff --git a/model/src/data/crss/download.py b/model/src/data/crss/download.py new file mode 100644 index 0000000..d08d7b3 --- /dev/null +++ b/model/src/data/crss/download.py @@ -0,0 +1,16 @@ +from pathlib import Path +from typing import List + +from src.data import traverse_nhtsa_and_download_files + + +def download_crss_data(save_folder: Path, years: List[int], overwrite: bool = False): + """ + Download FARS data from the NHTSA website. + """ + traverse_nhtsa_and_download_files( + start_folder="nhtsa/downloads/CRSS/", + save_folder=save_folder, + years=years, + overwrite=overwrite, + ) diff --git a/model/src/data/crss/transform.py b/model/src/data/crss/transform.py new file mode 100644 index 0000000..5c92e2d --- /dev/null +++ b/model/src/data/crss/transform.py @@ -0,0 +1,435 @@ +from typing import List +from zipfile import ZipFile + +import pandas as pd +from loguru import logger + +from src import DATA_SCHEMA, PROC_DATA_DIR, RAW_DATA_DIR +from src.data import encode_categories + + +def reduce_weather(col: pd.Series) -> pd.Series: + out_col = col.copy() + + out_col = out_col.cat.add_categories(["OVERCAST"]) + + out_col[out_col == "BLOWING_SAND_SOIL_DIRT"] = "OTHER" + out_col[out_col == "BLOWING_SNOW"] = "SNOW" + out_col[out_col == "CLOUDY"] = "OVERCAST" + out_col[out_col == "FREEZING_RAIN"] = "RAIN" + out_col[out_col == "NOT_REPORTED"] = "OTHER" + out_col[out_col == "SEVERE_CROSSWINDS"] = "OTHER" + out_col[out_col == "SLEET_HAIL"] = "RAIN" + out_col[out_col == "UNKNOWN"] = "OTHER" + + removals = [ + "BLOWING_SAND_SOIL_DIRT", + "BLOWING_SNOW", + "CLOUDY", + "FREEZING_RAIN", + "NOT_REPORTED", + "SEVERE_CROSSWINDS", + "SLEET_HAIL", + "UNKNOWN", + ] + + for cat in removals: + try: + out_col = out_col.cat.remove_categories(removals=[cat]) + except ValueError: + continue + + return out_col + + +def reduce_maximum_severity(col: pd.Series) -> pd.Series: + out_col = col.copy() + + out_col = out_col.cat.add_categories(["OTHER", "NONE_TO_MINOR_INJURY"]) + + out_col[out_col == "NO_INJURY"] = "NONE_TO_MINOR_INJURY" + out_col[out_col == "POSSIBLE_INJURY"] = "NONE_TO_MINOR_INJURY" + out_col[out_col == "MINOR_INJURY"] = "NONE_TO_MINOR_INJURY" + out_col[out_col == "INJURED_UNKNOWN_SEVERITY"] = "OTHER" + out_col[out_col == "DIED_PRIOR"] = "OTHER" + out_col[out_col == "NO_OCCUPANTS"] = "OTHER" + out_col[out_col == "UNKNOWN"] = "OTHER" + + removals = [ + "NO_INJURY", + "POSSIBLE_INJURY", + "MINOR_INJURY", + "INJURED_UNKNOWN_SEVERITY", + "DIED_PRIOR", + "NO_OCCUPANTS", + "UNKNOWN", + ] + + for cat in removals: + try: + out_col = out_col.cat.remove_categories(removals=[cat]) + except ValueError: + continue + + return out_col + + +def reduce_body_type(col: pd.Series) -> pd.Series: + out_col = col.copy() + + out_col[col <= 39] = "FOUR_WHEEL_VEHICLE" + out_col[col >= 80] = "LESS_THAN_FOUR_WHEEL_VEHICLE" + + return out_col.astype("category") + + +def reduce_distraction(col: pd.Series) -> pd.Series: + out_col = col.copy() + + out_col = out_col.cat.add_categories(["MOBILE_PHONE"]) + + out_col[out_col == "LOOKED_BUT_DID_NOT_SEE"] = "OTHER" + out_col[out_col == "BY_OTHER_OCCUPANTS"] = "OTHER" + out_col[out_col == "BY_A_MOVING_OBJECT_IN_VEHICLE"] = "OTHER" + out_col[out_col == "WHILE_TALKING_OR_LISTENING_PHONE"] = "MOBILE_PHONE" + out_col[out_col == "WHILE_MANIPULATING_PHONE"] = "MOBILE_PHONE" + out_col[out_col == "WHILE_ADJUSTING_AUDIO_CLIMATE"] = "OTHER" + out_col[out_col == "WHILE_ADJUSTING_OTHER"] = "OTHER" + out_col[out_col == "WHILE_USING_REACHING_DEVICE"] = "OTHER" + out_col[out_col == "OUTSIDE_PERSON_OBJECT"] = "OTHER" + out_col[out_col == "EATING_DRINKING"] = "OTHER" + out_col[out_col == "SMOKING"] = "OTHER" + out_col[out_col == "OTHER_MOBILE_PHONE"] = "MOBILE_PHONE" + out_col[out_col == "NO_DRIVER"] = "OTHER" + out_col[out_col == "INATTENTION"] = "OTHER" + out_col[out_col == "CARELESS"] = "OTHER" + out_col[out_col == "INATTENTIVE"] = "OTHER" + out_col[out_col == "OTHER_UNKNOWN"] = "OTHER" + out_col[out_col == "INATTENTIVE_UNKNOWN"] = "OTHER" + out_col[out_col == "NOT_REPORTED"] = "OTHER" + out_col[out_col == "DAY_DREAMING"] = "OTHER" + out_col[out_col == "OTHER"] = "OTHER" + out_col[out_col == "UNKNOWN"] = "OTHER" + + removals = [ + "LOOKED_BUT_DID_NOT_SEE", + "BY_OTHER_OCCUPANTS", + "BY_A_MOVING_OBJECT_IN_VEHICLE", + "WHILE_TALKING_OR_LISTENING_PHONE", + "WHILE_MANIPULATING_PHONE", + "WHILE_ADJUSTING_AUDIO_CLIMATE", + "WHILE_ADJUSTING_OTHER", + "WHILE_USING_REACHING_DEVICE", + "OUTSIDE_PERSON_OBJECT", + "EATING_DRINKING", + "SMOKING", + "OTHER_MOBILE_PHONE", + "NO_DRIVER", + "INATTENTION", + "CARELESS", + "INATTENTIVE", + "DISTRACTED_UNKNOWN", + "INATTENTIVE_UNKNOWN", + "NOT_REPORTED", + "DAY_DREAMING", + "UNKNOWN", + ] + + for cat in removals: + try: + out_col = out_col.cat.remove_categories(removals=[cat]) + except ValueError: + continue + + return out_col + + +def transform_accident_data(years: List[int]) -> pd.DataFrame: + logger.info( + f"Transforming CRSS accident data for years: [{', '.join([str(y) for y in years])}]" + ) + + schema = DATA_SCHEMA["crss"]["accident"] + + accident_dfs = [] + + for year in years: + data_path = ( + RAW_DATA_DIR + / "nhtsa" + / "downloads" + / "CRSS" + / str(year) + / f"CRSS{year}SAS.zip" + ) + + with ZipFile(data_path) as zf: + with zf.open("accident.sas7bdat") as f: + accident_dfs.append(pd.read_sas(f, format="sas7bdat")) + + logger.info(f"Merging {len(accident_dfs)} CRSS accident dataframes and saving.") + df = pd.concat(accident_dfs) + + # Drop unnecessary columns + df = df.drop(columns=list(set(df.columns) - set(schema["columns"].keys()))) + df = df.rename(columns=schema["columns"]) + + # Convert to appropriate data types + df.CaseId = pd.to_numeric(df.CaseId, downcast="integer") + df.HourofData = pd.to_numeric(df.HourofData, downcast="integer").astype("category") + df.Weather = pd.to_numeric(df.Weather, downcast="integer") + + # Category mappings + df.Weather = df.Weather.map( + encode_categories(encoding=schema["weather"], reverse=True) + ).astype("category") + + # Reduce categories + df.Weather = reduce_weather(df.Weather) + + # Rename columns, filter, and save + df = df[df.Weather != "OTHER"] + df.Weather = df.Weather.cat.remove_categories(["OTHER"]) + df = df.reset_index(drop=True) + + df.to_feather(PROC_DATA_DIR / "crss_accident.f") + logger.success(f"Saved {PROC_DATA_DIR / 'crss_accident.f'}") + + return df + + +def transform_vehicle_data(years: List[int]) -> pd.DataFrame: + logger.info( + f"Transforming CRSS vehicle data for years: [{', '.join([str(y) for y in years])}]" + ) + + schema = DATA_SCHEMA["crss"]["vehicle"] + + vehicle_dfs = [] + + for year in years: + data_path = ( + RAW_DATA_DIR + / "nhtsa" + / "downloads" + / "CRSS" + / str(year) + / f"CRSS{year}SAS.zip" + ) + + with ZipFile(data_path) as zf: + with zf.open("vehicle.sas7bdat") as f: + vehicle_dfs.append(pd.read_sas(f, format="sas7bdat")) + + logger.info(f"Merging {len(vehicle_dfs)} CRSS vehicle dataframes and saving.") + df = pd.concat(vehicle_dfs) + + # Drop unnecessary columns + df = df.drop(columns=list(set(df.columns) - set(schema["columns"].keys()))) + df = df.rename(columns=schema["columns"]) + + # Convert to appropriate data types + df.CaseId = pd.to_numeric(df.CaseId, downcast="integer") + df.VehicleId = pd.to_numeric(df.VehicleId, downcast="integer") + df.VehicleYear = pd.to_numeric(df.VehicleYear, downcast="integer") + df.BodyType = pd.to_numeric(df.BodyType, downcast="integer") + df.Speed = pd.to_numeric(df.Speed, downcast="integer") + df.SpeedLimit = pd.to_numeric(df.SpeedLimit, downcast="integer") + df.MaximumSeverity = pd.to_numeric(df.MaximumSeverity, downcast="integer") + df.Alcohol = pd.to_numeric(df.Alcohol, downcast="integer") + + # Filter + df = df[ + df.BodyType.isin( + [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 17, + 10, + 11, + 13, + 14, + 15, + 16, + 19, + 20, + 21, + 22, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 39, + 80, + 81, + 82, + 83, + 84, + 85, + 86, + 87, + 88, + 89, + 90, + ] + ) + ] + + # Category mappings + df.MaximumSeverity = df.MaximumSeverity.map( + encode_categories(encoding=schema["maximum_severity"], reverse=True) + ).astype("category") + df.Alcohol = df.Alcohol.map( + encode_categories(encoding=schema["alcohol"], reverse=True) + ).astype("category") + + # Reduce categories + df.MaximumSeverity = reduce_maximum_severity(df.MaximumSeverity) + df.BodyType = reduce_body_type(df.BodyType) + + # Rename columns, filter, and save + df = df[df.MaximumSeverity != "OTHER"] + df.MaximumSeverity = df.MaximumSeverity.cat.remove_categories(["OTHER"]) + df = df.reset_index(drop=True) + + df.to_feather(PROC_DATA_DIR / "crss_vehicle.f") + logger.success(f"Saved {PROC_DATA_DIR / 'crss_vehicle.f'}") + + return df + + +def transform_person_data(years: List[int]) -> pd.DataFrame: + logger.info( + f"Transforming CRSS person data for years: [{', '.join([str(y) for y in years])}]" + ) + + schema = DATA_SCHEMA["crss"]["person"] + + person_dfs = [] + + for year in years: + data_path = ( + RAW_DATA_DIR + / "nhtsa" + / "downloads" + / "CRSS" + / str(year) + / f"CRSS{year}SAS.zip" + ) + + with ZipFile(data_path) as zf: + with zf.open("person.sas7bdat") as f: + person_dfs.append(pd.read_sas(f, format="sas7bdat")) + + logger.info(f"Merging {len(person_dfs)} CRSS person dataframes and saving.") + df = pd.concat(person_dfs) + + # Drop unnecessary columns and data + df = df.drop(columns=list(set(df.columns) - set(schema["columns"].keys()))) + df = df.rename(columns=schema["columns"]) + df = df[df.PersonType == 1] + df = df.drop(columns=["PersonType"]) + + # Convert to appropriate data types + df.CaseId = pd.to_numeric(df.CaseId, downcast="integer") + df.AgeofDriver = pd.to_numeric(df.AgeofDriver, downcast="integer") + + # Rename columns, filter, and save + df = df.reset_index(drop=True) + + df.to_feather(PROC_DATA_DIR / "crss_person.f") + logger.success(f"Saved {PROC_DATA_DIR / 'crss_person.f'}") + + return df + + +def transform_distract_data(years: List[int]) -> pd.DataFrame: + logger.info( + f"Transforming CRSS distract data for years: [{', '.join([str(y) for y in years])}]" + ) + + schema = DATA_SCHEMA["crss"]["distract"] + + distract_dfs = [] + + for year in years: + data_path = ( + RAW_DATA_DIR + / "nhtsa" + / "downloads" + / "CRSS" + / str(year) + / f"CRSS{year}SAS.zip" + ) + + with ZipFile(data_path) as zf: + with zf.open("distract.sas7bdat") as f: + distract_dfs.append(pd.read_sas(f, format="sas7bdat")) + + logger.info(f"Merging {len(distract_dfs)} CRSS distract dataframes and saving.") + df = pd.concat(distract_dfs) + + # Drop unnecessary columns and data + df = df.drop(columns=list(set(df.columns) - set(schema["columns"].keys()))) + df = df.rename(columns=schema["columns"]) + + # Convert to appropriate data types + df.CaseId = pd.to_numeric(df.CaseId, downcast="integer") + df.Distraction = pd.to_numeric(df.Distraction, downcast="integer") + + # Category mappings + df.Distraction = df.Distraction.map( + encode_categories(encoding=schema["distraction"], reverse=True) + ).astype("category") + + # Reduce categories + df.Distraction = reduce_distraction(df.Distraction) + + # Rename columns, filter, and save + df = df[(df.Distraction != "OTHER")] + df.Distraction = df.Distraction.cat.remove_categories(["OTHER"]) + df = df.reset_index(drop=True) + + df.to_feather(PROC_DATA_DIR / "crss_distract.f") + logger.success(f"Saved {PROC_DATA_DIR / 'crss_distract.f'}") + + return df + + +def merge_crss_data( + accident_df: pd.DataFrame, + vehicle_df: pd.DataFrame, + person_df: pd.DataFrame, + distract_df: pd.DataFrame, +) -> pd.DataFrame: + logger.info("Merging CRSS data.") + df = accident_df.merge(vehicle_df, on=["CaseId"]) + df = df.merge(person_df, on=["CaseId"]) + df = df.merge(distract_df, on=["CaseId"]) + + # Filter + df = df[ + (df.Speed > 0) + & (df.Speed < 200) + & (df.SpeedLimit > 0) + & (df.SpeedLimit < 99) + & (df.AgeofDriver < 120) + & (df.AgeofDriver >= 15) + & (df.VehicleYear > 1900) + & (df.VehicleYear <= 2030) + ] + df["RelativeSpeed"] = df.Speed / df.SpeedLimit + + df.dropna().reset_index(drop=True).to_feather(PROC_DATA_DIR / "accident.f") + logger.info("Saving accident data.") + logger.success(f"Saved {PROC_DATA_DIR / 'accident.f'}") + return df diff --git a/model/src/data/make_data.py b/model/src/data/make_data.py new file mode 100644 index 0000000..9c5e156 --- /dev/null +++ b/model/src/data/make_data.py @@ -0,0 +1,160 @@ +from pathlib import Path + +import click +import pandas as pd +from loguru import logger + +from src import PROC_DATA_DIR, RAW_DATA_DIR +from src.data.crss.download import download_crss_data +from src.data.crss.transform import ( + merge_crss_data, + transform_accident_data, + transform_distract_data, + transform_person_data, + transform_vehicle_data, +) +from src.data.seattle.collisions.download import download_seattle_collision_data +from src.data.seattle.collisions.transform import ( + save_seattle_risk_data, + transform_seattle_collision_data, +) +from src.data.seattle.streets.download import ( + download_seattle_intersection_data, + download_seattle_street_data, + download_seattle_weather_data, +) +from src.data.seattle.streets.transform import ( + save_exposure_data, + transform_seattle_intersection_data, + transform_seattle_streets_data, + transform_seattle_volume_data, + transform_seattle_weather_data, +) + + +@click.command() +@click.option( + "--start_year", + "-sy", + type=click.INT, + default=2019, + help="Start year to prepare data for", +) +@click.option( + "--end_year", + "-ey", + type=click.INT, + default=2019, + help="End year to prepare data for", +) +@click.option( + "--frequency", + "-f", + is_flag=True, + help="Frequency only", +) +@click.option( + "--severity", + "-s", + is_flag=True, + help="Severity only", +) +@click.option( + "--overwrite", + "-o", + is_flag=True, + help="Overwrite existing data", +) +@click.option( + "--download", + "-d", + is_flag=True, + help="Download data", +) +def main( + start_year: int, + end_year: int, + frequency: bool = False, + severity: bool = False, + overwrite: bool = False, + download: bool = False, +): + """ + Runs data processing scripts to turn raw data from (data/raw) into + cleaned data ready to be analyzed (saved in data/processed). + """ + if frequency and severity: + pass + elif frequency: + logger.info("Frequency only selected.") + severity = False + elif severity: + logger.info("Severity only selected.") + frequency = False + else: + frequency = True + severity = True + + years = list(range(start_year, end_year + 1)) + + if frequency: + # Exposure data + if download: + download_seattle_street_data(RAW_DATA_DIR, overwrite=overwrite) + download_seattle_intersection_data(RAW_DATA_DIR, overwrite=overwrite) + download_seattle_weather_data(RAW_DATA_DIR, overwrite=overwrite) + + if not Path(PROC_DATA_DIR / "exposure.f").exists() or overwrite: + street_df = transform_seattle_streets_data() + intersec_df = transform_seattle_intersection_data() + weather_df = transform_seattle_weather_data() + vol_df = transform_seattle_volume_data(weather_df) + + exposure = save_exposure_data( + street=street_df, intersection=intersec_df, volume=vol_df + ) + else: + logger.warning( + "Exposure data already exists. Use -o to overwrite existing data." + ) + + if severity: + # Collision data + if download: + download_seattle_collision_data(RAW_DATA_DIR, overwrite=overwrite) + + if not Path(PROC_DATA_DIR / "risk.f").exists() or overwrite: + collision_df = transform_seattle_collision_data() + risk_df = save_seattle_risk_data(collision=collision_df) + else: + logger.warning( + "Risk data already exists. Use -o to overwrite existing data." + ) + + # Accident data + if download: + download_crss_data( + save_folder=RAW_DATA_DIR, years=years, overwrite=overwrite + ) + + if not Path(PROC_DATA_DIR / "accident.f").exists() or overwrite: + accident_df = transform_accident_data(years) + vehicle_df = transform_vehicle_data(years) + person_df = transform_person_data(years) + distract_df = transform_distract_data(years) + + accident_df = merge_crss_data( + accident_df, vehicle_df, person_df, distract_df + ) + else: + logger.warning( + "CRSS accident data already exists. Use -o to overwrite existing data." + ) + accident_df = pd.read_feather(PROC_DATA_DIR / "accident.f") + + +if __name__ == "__main__": + # not used in this stub but often useful for finding various files + project_dir = Path(__file__).resolve().parents[2] + + main() diff --git a/model/src/data/seattle/collisions/download.py b/model/src/data/seattle/collisions/download.py new file mode 100644 index 0000000..82ce904 --- /dev/null +++ b/model/src/data/seattle/collisions/download.py @@ -0,0 +1,23 @@ +import os +from pathlib import Path + +from loguru import logger + +from src.data import download_file + + +def download_seattle_collision_data(save_folder: Path, overwrite: bool = False): + url = ( + "https://data-seattlecitygis.opendata.arcgis.com/datasets" + "/SeattleCityGIS::collisions-all-years.geojson" + ) + + save_folder = save_folder / "seattle" / "collisions" + save_folder.mkdir(parents=True, exist_ok=True) + file_name = "collisions.geojson" + + if os.path.exists(save_folder / file_name) and not overwrite: + logger.warning(f"File {file_name} already exists.") + return + + download_file(url, save_folder / file_name) diff --git a/model/src/data/seattle/collisions/transform.py b/model/src/data/seattle/collisions/transform.py new file mode 100644 index 0000000..3f5ec71 --- /dev/null +++ b/model/src/data/seattle/collisions/transform.py @@ -0,0 +1,206 @@ +import json + +import joblib +import pandas as pd +import pytz +from loguru import logger + +from src import CONFIG, DATA_SCHEMA, MODELS_DIR, PROC_DATA_DIR, RAW_DATA_DIR +from src.data import encode_categories # bin_hour_of_data, + + +def reduce_weather(col: pd.Series) -> pd.Series: + out_col = col.copy() + + out_col[out_col == "Raining"] = "RAIN" + out_col[out_col == "Fog/Smog/Smoke"] = "OTHER" + out_col[out_col == "Overcast"] = "OVERCAST" + out_col[out_col == "Sleet/Hail/Freezing Rain"] = "RAIN" + out_col[out_col == "Unknown"] = "CLEAR" + out_col[out_col == "Snowing"] = "SNOW" + out_col[out_col == "Severe Crosswind"] = "OTHER" + out_col[out_col == "Blowing Sand/Dirt"] = "OTHER" + out_col[out_col == "Partly Cloudy"] = "CLEAR" + out_col[out_col == "Blowing Snow"] = "SNOW" + + out_col = out_col.str.upper().copy() + + out_col = out_col.astype("category") + + return out_col + + +def reduce_light_condition(col: pd.Series) -> pd.Series: + out_col = col.copy() + + out_col[out_col == "Dark - Street Lights On"] = "DARK" + out_col[out_col == "Dawn"] = "DARK" + out_col[out_col == "Dark - No Street Lights"] = "DARK" + out_col[out_col == "Dark - Street Lights Off"] = "DARK" + out_col[out_col == "Dusk"] = "DARK" + out_col[out_col == "Dark - Unknown Lighting"] = "DARK" + out_col[out_col == "Other"] = "DAYLIGHT" + out_col[out_col == "Unknown"] = "DAYLIGHT" + + out_col = out_col.str.upper().copy() + + out_col = out_col.astype("category") + + return out_col + + +def transform_seattle_collision_data() -> pd.DataFrame: + logger.info("Transforming collision data") + + schema = DATA_SCHEMA["seattle"]["collisions"] + + with open(RAW_DATA_DIR / "seattle" / "collisions" / "collisions.geojson", "r") as f: + collision_json = dict(json.load(f)) + + collisions = pd.DataFrame([row["properties"] for row in collision_json["features"]]) + collisions = collisions[collisions.STATUS == "Matched"] + + # Get coords + coords = pd.DataFrame( + [ + row["geometry"]["coordinates"] + for row in collision_json["features"] + if row["geometry"] + ], + columns=["LONGITUDE", "LATITUDE"], + ) + ids = pd.DataFrame( + [ + row["properties"]["OBJECTID"] + for row in collision_json["features"] + if row["geometry"] + ], + columns=["OBJECTID"], + ) + coords = coords.join(ids) + + # Merge in coords + df = collisions.merge(coords, on=["OBJECTID"]) + + # Rename + df = df.rename(columns=schema["columns"]) + + # Drop unnecessary columns + df = df[list(schema["columns"].values())] + + # Convert to appropriate data types + df.ObjectId = pd.to_numeric(df.ObjectId, downcast="integer") + df.IncidentId = pd.to_numeric(df.IncidentId, downcast="integer") + df.CollisionId = pd.to_numeric(df.CollisionId, downcast="integer") + df.NumFatalities = pd.to_numeric(df.NumFatalities, downcast="integer") + df.NumInjuries = pd.to_numeric(df.NumInjuries, downcast="integer") + df.IntersectionId = pd.to_numeric(df.IntersectionId, downcast="integer") + df.NumPedestrians = pd.to_numeric(df.NumPedestrians, downcast="integer") + df.NumCyclists = pd.to_numeric(df.NumCyclists, downcast="integer") + df.NumPersons = pd.to_numeric(df.NumPersons, downcast="integer") + df.NumSeriousInjuries = pd.to_numeric(df.NumSeriousInjuries, downcast="integer") + df.NumVehicles = pd.to_numeric(df.NumVehicles, downcast="integer") + df.Longitude = pd.to_numeric(df.Longitude, downcast="float") + df.Latitude = pd.to_numeric(df.Latitude, downcast="float") + + df.Time = pd.to_datetime(df.Time, format="%m/%d/%Y %I:%M:%S %p", errors="coerce") + pacific_time = pytz.timezone("America/Los_Angeles") + df.Time = df.Time.dt.tz_localize( + pacific_time, ambiguous=True, nonexistent="shift_forward" + ) + + # Remove NA + for col in df.columns: + if col in ["IntersectionId"]: + continue + if df[col].isna().any(): + logger.warning( + f"{col} has {df[col].isna().sum()} NA values that will be dropped." + ) + df = df.dropna(subset=[col]) + + # Category mappings + df["WeekdayofData"] = df.Time.dt.weekday.map( + encode_categories(encoding=schema["weekday_of_data"], reverse=True) + ).astype("category") + df.SeverityCode = df.SeverityCode.map( + encode_categories(encoding=schema["severity_code"], reverse=True) + ) + + # Add intersection col + df["StreetType"] = "STREET" + df.loc[~df.IntersectionId.isna(), "StreetType"] = "INTERSECTION" + df.StreetType = df.StreetType.astype("category") + + # Reduce categories + df.LightCondition = reduce_light_condition(df.LightCondition) + df.Weather = reduce_weather(df.Weather) + df = df.loc[df.Weather != "OTHER"] + df.Weather = df.Weather.cat.remove_categories(removals=["OTHER"]) + + # Remove extra columns + df = df.drop(columns=["MatchingStatus"]) + + # Rename columns, filter, aggregate, and save + df = df[ + (df.Time.dt.date >= CONFIG["start_date"]) + & (df.Time.dt.date <= CONFIG["end_date"]) + ] + df = df.reset_index(drop=True) + + df.to_feather(PROC_DATA_DIR / "collisions.f") + logger.success(f"Saved {PROC_DATA_DIR / 'collisions.f'}") + + return df + + +def save_seattle_risk_data(collision: pd.DataFrame) -> pd.DataFrame: + logger.info("Adding StationId to risk data") + + # Add StationId + with open(MODELS_DIR / "station_id_classifier.joblib", "rb") as f: + station_classifier = joblib.load(f) + + collision["StationId"] = station_classifier.predict( + collision[["Latitude", "Longitude"]] + ) + collision.StationId = pd.to_numeric(collision.StationId, downcast="integer") + + # Remove columns + risk = collision.drop( + columns=[ + "ObjectId", + "IncidentId", + "CollisionId", + "IntersectionId", + "Longitude", + "Latitude", + ] + ).copy() + + # Aggregate + risk.Time = risk.Time.dt.round("H", ambiguous=True, nonexistent="shift_forward") + risk = ( + risk.groupby( + [ + "Time", + "LightCondition", + "Weather", + "WeekdayofData", + "StreetType", + "StationId", + ], + observed=True, + ) + .sum() + .reset_index() + .dropna() + ) + risk["HourofData"] = risk.Time.dt.hour + risk.HourofData = pd.to_numeric(risk.HourofData, downcast="integer") + + # Save + risk = risk.reset_index(drop=True).copy() + + risk.to_feather(PROC_DATA_DIR / "risk.f") + logger.success(f"Saved {PROC_DATA_DIR / 'risk.f'}") diff --git a/model/src/data/seattle/streets/download.py b/model/src/data/seattle/streets/download.py new file mode 100644 index 0000000..e9038c7 --- /dev/null +++ b/model/src/data/seattle/streets/download.py @@ -0,0 +1,96 @@ +import json +import os +from pathlib import Path + +import requests +from loguru import logger + +from src import CONFIG +from src.data import download_file + + +def download_seattle_street_data(save_folder: Path, overwrite: bool = False): + url = ( + "https://opendata.arcgis.com/api/v3/datasets" + "/73f5184d9062458c81ff86e5f5bcdbb8_9" + "/downloads/data" + "?format=geojson" + "&spatialRefId=4326" + "&where=1=1" + ) + + save_folder = save_folder / "seattle" / "streets" + save_folder.mkdir(parents=True, exist_ok=True) + file_name = "streets.geojson" + + if os.path.exists(save_folder / file_name) and not overwrite: + logger.warning(f"File {file_name} already exists.") + return + + download_file(url, save_folder / file_name) + + +def download_seattle_intersection_data(save_folder: Path, overwrite: bool = False): + url = ( + "https://data-seattlecitygis.opendata.arcgis.com" + "/datasets/SeattleCityGIS::intersections-3.geojson" + ) + + save_folder = save_folder / "seattle" / "intersections" + save_folder.mkdir(parents=True, exist_ok=True) + file_name = "intersections.geojson" + + if os.path.exists(save_folder / file_name) and not overwrite: + logger.warning(f"File {file_name} already exists.") + return + + download_file(url, save_folder / file_name) + + +def download_seattle_volume_data(save_folder: Path, overwrite: bool = False): + url = "https://data.seattle.gov/api/views/fe9f-nc4f/rows.csv?accessType=DOWNLOAD" + + save_folder = save_folder / "seattle" / "volume" + save_folder.mkdir(parents=True, exist_ok=True) + file_name = "volume.csv" + + if os.path.exists(save_folder / file_name) and not overwrite: + logger.warning(f"File {file_name} already exists.") + else: + download_file(url, save_folder / file_name) + + url = ( + "https://data.seattle.gov/api/views/fe9f-nc4f/files" + "/77367d8d-278e-4a2a-a388-8449be33639f" + "?download=true" + "&filename=Location%20Lat%20Long.xlsx" + ) + file_name = "latlong.xlsx" + + if os.path.exists(save_folder / file_name) and not overwrite: + logger.warning(f"File {file_name} already exists.") + else: + download_file(url, save_folder / file_name) + + +def download_seattle_weather_data(save_folder: Path, overwrite: bool = False): + api = CONFIG["weather"]["api"] + + save_folder = save_folder / "seattle" / "weather" + save_folder.mkdir(parents=True, exist_ok=True) + file_name = "weather.json" + + if os.path.exists(save_folder / file_name) and not overwrite: + logger.warning(f"File {file_name} already exists.") + else: + query = { + "latitude": api["params"]["latitude"], + "longitude": api["params"]["longitude"], + "start_date": f"{api['params']['start_date']:%Y-%m-%d}", + "end_date": f"{api['params']['end_date']:%Y-%m-%d}", + "hourly": api["params"]["hourly"], + } + response = requests.get(api["url"], params=query) + + with open(save_folder / file_name, "w") as f: + json.dump(response.json(), f) diff --git a/model/src/data/seattle/streets/transform.py b/model/src/data/seattle/streets/transform.py new file mode 100644 index 0000000..cc7f313 --- /dev/null +++ b/model/src/data/seattle/streets/transform.py @@ -0,0 +1,297 @@ +import json + +import joblib +import pandas as pd +import pytz +from loguru import logger +from sklearn.neighbors import KNeighborsClassifier + +from src import ( + CONFIG, + DATA_SCHEMA, + INTERVALS_PER_HOUR, + MODELS_DIR, + PROC_DATA_DIR, + RAW_DATA_DIR, +) +from src.data import encode_categories + + +def transform_seattle_streets_data() -> pd.DataFrame: + logger.info("Transforming Seattle streets data") + schema = DATA_SCHEMA["seattle"]["streets"] + + with open(RAW_DATA_DIR / "seattle" / "streets" / "streets.geojson", "r") as f: + street_json = dict(json.load(f)) + + streets = pd.DataFrame([row["properties"] for row in street_json["features"]]) + + coords = pd.DataFrame( + [row["geometry"]["coordinates"][0] for row in street_json["features"]] + ) + coords.columns = [f"coord_{col}" for col in coords] + coords = streets[["OBJECTID"]].join(coords) + coords = coords.melt(id_vars="OBJECTID", value_vars=coords.columns).dropna( + subset="value" + ) + coords["PointId"] = coords.variable.str.split("_").str[-1] + coords["Latitude"], coords["Longitude"] = coords.value.str[1], coords.value.str[0] + coords = coords.drop(columns=["variable", "value"]) + + df = streets.merge(coords, on="OBJECTID") + + # Rename + df = df.rename(columns=schema["columns"]) + + # Drop unnecessary columns + df = df[list(schema["columns"].values()) + ["PointId", "Longitude", "Latitude"]] + + # Convert to appropriate data types + df.ObjectId = pd.to_numeric(df.ObjectId, downcast="integer") + df.PointId = pd.to_numeric(df.PointId, downcast="integer") + df.ArterialCode = pd.to_numeric(df.ArterialCode, downcast="integer") + df.SpeedLimitMPH = pd.to_numeric(df.SpeedLimitMPH, downcast="float") + df.Length = pd.to_numeric(df.Length, downcast="float") + df.Longitude = pd.to_numeric(df.Longitude, downcast="float") + df.Latitude = pd.to_numeric(df.Latitude, downcast="float") + + # Remove NA + for col in df.columns: + if df[col].isna().any(): + logger.warning( + f"{col} has {df[col].isna().sum()} NA values that will be dropped." + ) + df = df.dropna(subset=[col]) + + # Category mappings + df.ArterialCode = df.ArterialCode.map( + encode_categories(encoding=schema["arterial_code"], reverse=True) + ).astype("category") + + # Rename columns, filter, and save + df = df.reset_index(drop=True) + + df.to_feather(PROC_DATA_DIR / "streets.f") + logger.success(f"Saved {PROC_DATA_DIR / 'streets.f'}") + + return df + + +def transform_seattle_intersection_data() -> pd.DataFrame: + logger.info("Transforming Seattle intersection data") + schema = DATA_SCHEMA["seattle"]["intersections"] + + with open( + RAW_DATA_DIR / "seattle" / "intersections" / "intersections.geojson", "r" + ) as f: + street_json = dict(json.load(f)) + + df = pd.DataFrame([row["properties"] for row in street_json["features"]]) + + # Rename + df = df.rename(columns=schema["columns"]) + + # Drop unnecessary columns + df = df[list(schema["columns"].values())] + + # Convert to appropriate data types + df.IntersectionId = pd.to_numeric(df.IntersectionId, downcast="integer") + df.Longitude = pd.to_numeric(df.Longitude, downcast="float") + df.Latitude = pd.to_numeric(df.Latitude, downcast="float") + + # Remove NA + for col in df.columns: + if df[col].isna().any(): + logger.warning( + f"{col} has {df[col].isna().sum()} NA values that will be dropped." + ) + df = df.dropna(subset=[col]) + + # Rename columns, filter, and save + df = df.reset_index(drop=True) + + df.to_feather(PROC_DATA_DIR / "intersections.f") + logger.success(f"Saved {PROC_DATA_DIR / 'intersections.f'}") + + return df + + +def transform_seattle_volume_data(weather: pd.DataFrame) -> pd.DataFrame: + logger.info("Transforming Seattle volume data") + schema = DATA_SCHEMA["seattle"]["volume"] + + vol = pd.read_csv( + RAW_DATA_DIR / "seattle" / "volume" / "volume.csv", + usecols=[0, 1, 2, 4], + ) + vol.Time = pd.to_datetime( + vol.Time.str.replace(" P[D,S]T", "", regex=True), format="%m/%d/%Y %H:%M:%S" + ) + pacific_time = pytz.timezone("America/Los_Angeles") + vol.Time = vol.Time.dt.tz_localize( + pacific_time, ambiguous=True, nonexistent="shift_forward" + ) + + # Round to hours so can join with weather + vol.Time = vol.Time.dt.round("H", ambiguous=True, nonexistent="shift_forward") + vol = vol.groupby(["APEG", "Time"]).sum().reset_index() + + # Merge in weather data + df = vol.merge(weather, on=["Time"], how="inner") + + df = df.groupby(["APEG", "Time"], observed=True).sum().reset_index().dropna() + df["Intervals"] = INTERVALS_PER_HOUR # Need intervals for accurate exposure + + # Add weather + weather_map = CONFIG["weather"]["classification"] + + df["Weather"] = "CLEAR" + df.loc[df.Cloudcover >= weather_map["cloudcover"], "Weather"] = "OVERCAST" + df.loc[df.Rainfall >= weather_map["rainfall"], "Weather"] = "RAIN" + df.loc[df.Snowfall >= weather_map["snowfall"], "Weather"] = "SNOW" + df.Weather = df.Weather.astype("category") + + # Finalise + df["WeekdayofData"] = pd.to_datetime(df.Time).dt.weekday + df.WeekdayofData = df.WeekdayofData.map( + encode_categories(encoding=schema["weekday_of_data"], reverse=True) + ).astype("category") + df = df.drop( + columns=[ + "Rainfall", + "Snowfall", + "Cloudcover", + ] + ) + + # Aggregate + df = ( + df.groupby(["APEG", "Time", "WeekdayofData", "Weather"], observed=True) + .sum() + .reset_index() + ) + df["HourofData"] = df.Time.dt.hour + + # Coordinates + latlng = pd.read_excel( + RAW_DATA_DIR / "seattle" / "volume" / "latlong.xlsx", + usecols=["APEG", "Latitude", "Longitude"], + ) + + # Rename + df = df.rename(columns={"APEG": "StationId", "Vol": "TotalVolume"}) + latlng = latlng.rename(columns={"APEG": "StationId"}) + + # Merge + df = df.merge(latlng, on=["StationId"]) + + # Map StationId + station_id_map = {station: i for i, station in enumerate(df.StationId.unique())} + df.StationId = df.StationId.map(encode_categories(encoding=station_id_map)).astype( + "int8" + ) + + # Convert to appropriate data types + df.HourofData = pd.to_numeric(df.HourofData, downcast="integer") + df.TotalVolume = pd.to_numeric(df.TotalVolume, downcast="float") + df.Intervals = pd.to_numeric(df.Intervals, downcast="integer") + df.Longitude = pd.to_numeric(df.Longitude, downcast="float") + df.Latitude = pd.to_numeric(df.Latitude, downcast="float") + + # Remove NA + for col in df.columns: + if df[col].isna().any(): + logger.warning( + f"{col} has {df[col].isna().sum()} NA values that will be dropped." + ) + df = df.dropna(subset=[col]) + + # Rename columns, filter, and save + df = df.reset_index(drop=True) + + df.to_feather(PROC_DATA_DIR / "volume.f") + logger.success(f"Saved {PROC_DATA_DIR / 'volume.f'}") + + return df + + +def transform_seattle_weather_data(): + logger.info("Transforming Seattle weather data") + schema = DATA_SCHEMA["seattle"]["weather"] + + with open(RAW_DATA_DIR / "seattle" / "weather" / "weather.json", "r") as f: + weather_json = json.load(f) + + df = pd.DataFrame(dict(weather_json)["hourly"]) + + pacific_time = pytz.timezone("America/Los_Angeles") + df.time = ( + pd.to_datetime(df.time).dt.tz_localize(pytz.utc).dt.tz_convert(pacific_time) + ) + + # Rename + df = df.rename(columns=schema["columns"]).reset_index(drop=True) + + # Dtypes + df.Rainfall = pd.to_numeric(df.Rainfall, downcast="float") + df.Snowfall = pd.to_numeric( + df.Snowfall * 10, downcast="float" + ) # Raw data is in cm, conver to mm + df.Cloudcover = pd.to_numeric(df.Cloudcover / 100, downcast="float") + df.CloudcoverLow = pd.to_numeric(df.CloudcoverLow / 100, downcast="float") + df.RelativeHumidity = pd.to_numeric(df.RelativeHumidity / 100, downcast="float") + + # Save + df.to_feather(PROC_DATA_DIR / "weather.f") + logger.success(f"Saved {PROC_DATA_DIR / 'weather.f'}") + + return df + + +def save_exposure_data( + street: pd.DataFrame, + intersection: pd.DataFrame, + volume: pd.DataFrame, +) -> pd.DataFrame: + logger.info("Merging street and intersection data") + df = street.merge(intersection, on=["Longitude", "Latitude"], how="left") + + df["StreetType"] = "STREET" + df.loc[~df.IntersectionId.isna(), "StreetType"] = "INTERSECTION" + df.StreetType = df.StreetType.astype("category") + + num_lost = len(set(intersection.IntersectionId) - set(df.IntersectionId)) + logger.warning(f"{num_lost} intersections were not mapped. Dropped.") + + # dtypes + df.IntersectionId = pd.to_numeric(df.IntersectionId, downcast="float") + + # Map volume to streets + knn = KNeighborsClassifier(n_neighbors=1) + knn.fit(volume[["Latitude", "Longitude"]], volume.StationId) + with open(MODELS_DIR / "station_id_classifier.joblib", "wb") as f: + joblib.dump(knn, f) + + df["StationId"] = knn.predict(df[["Latitude", "Longitude"]]) + + # Aggregate and merge with volume + df = ( + df[["SpeedLimitMPH", "StreetType", "StationId"]] + .groupby(["StationId", "StreetType"], observed=True) + .median() + .reset_index() + ) + + exposure = df.merge( + volume.drop(columns=["Latitude", "Longitude"]), + on=["StationId"], + how="inner", + ) + + # Save + exposure = exposure.reset_index(drop=True) + + exposure.to_feather(PROC_DATA_DIR / "exposure.f") + logger.success(f"Saved {PROC_DATA_DIR / 'exposure.f'}") + + return exposure diff --git a/model/src/models/frequency.py b/model/src/models/frequency.py new file mode 100644 index 0000000..1386656 --- /dev/null +++ b/model/src/models/frequency.py @@ -0,0 +1,226 @@ +from pathlib import Path + +import joblib +import matplotlib.pyplot as plt +import numpy as np +import pandas as pd +from loguru import logger +from sklearn.compose import ColumnTransformer +from sklearn.ensemble import HistGradientBoostingRegressor +from sklearn.metrics import ( + mean_absolute_error, + mean_poisson_deviance, + mean_squared_error, +) +from sklearn.model_selection import GridSearchCV, train_test_split +from sklearn.pipeline import Pipeline +from sklearn.preprocessing import OrdinalEncoder + +from src import CONFIG, PROC_DATA_DIR + +MIN_VOLUME = CONFIG["gbm"]["min_volume"] + + +def load_modelling_data(): + # Risk data + risk = pd.read_feather(PROC_DATA_DIR / "risk.f") + risk = risk.drop( + columns=[ + "LightCondition", + "NumFatalities", + "NumInjuries", + "NumPedestrians", + "NumCyclists", + "NumPersons", + "NumSeriousInjuries", + ] + ) + + # Exposure data + exposure = pd.read_feather(PROC_DATA_DIR / "exposure.f") + + # Combine + df = exposure.merge( + risk, + on=[ + "StationId", + "Time", + "StreetType", + "WeekdayofData", + "HourofData", + "Weather", + ], + how="left", + ).fillna( + { + "NumFatalities": 0, + "NumInjuries": 0, + "NumPersons": 0, + "NumSeriousInjuries": 0, + "NumVehicles": 0, + } + ) + + df = df[df.TotalVolume > MIN_VOLUME] # Very small volumes mess with model fit + + df["TotalVolume"] = df["TotalVolume"] / 1000 # Per 1000 vehicles + df["Frequency"] = df["NumVehicles"] / df["TotalVolume"] + + return df + + +def prep_station_data(df: pd.DataFrame, station_id: int, filepath: Path): + station_df = df.loc[df.StationId == station_id].copy() + + # Save plot of frequency and volume + fig, (ax0, ax1, ax2) = plt.subplots(ncols=3, figsize=(16, 4)) + ax0.set_title("Number of vehicles in collision") + _ = station_df["NumVehicles"].hist(bins=30, log=True, ax=ax0) + ax1.set_title("Thousand vehicles per hour") + _ = station_df["TotalVolume"].hist(bins=30, log=True, ax=ax1) + ax2.set_title("Frequency\n(# of vehicle collisions per 1000 vehicles per hour)") + _ = station_df["Frequency"].hist(bins=30, log=True, ax=ax2) + plt.savefig(filepath / "freq_vol.png") + plt.close() + + return station_df + + +def split_for_training(df: pd.DataFrame): + df_train, df_test = train_test_split(df, test_size=0.2) + + avg_freq = np.average(df_train["Frequency"], weights=df_train["TotalVolume"]) + logger.info(f"Average Frequency = {avg_freq:.3%}") + + zero_vehicles = ( + df_train.loc[df_train["NumVehicles"] == 0, "TotalVolume"].sum() + / df_train["TotalVolume"].sum() + ) + logger.info( + f"Fraction of exposure with zero vehicles in accident = {zero_vehicles:.2%}" + ) + + return df_train, df_test + + +def score_estimator(estimator, df_test): + """Score an estimator on the test set.""" + y_pred = estimator.predict(df_test) + + mse = mean_squared_error( + df_test["Frequency"], y_pred, sample_weight=df_test["TotalVolume"] + ) + mae = mean_absolute_error( + df_test["Frequency"], y_pred, sample_weight=df_test["TotalVolume"] + ) + logger.info(f"MSE: {mse:.4f}") + logger.info(f"MAE: {mae:.4f}") + + # Ignore non-positive predictions, as they are invalid for + # the Poisson deviance. + mask = y_pred > 0 + if (~mask).any(): + n_masked, n_samples = (~mask).sum(), mask.shape[0] + logger.warning( + "Estimator yields invalid, non-positive predictions " + f" for {n_masked} samples out of {n_samples}. These predictions " + "are ignored when computing the Poisson deviance." + ) + + mpd = mean_poisson_deviance( + df_test["Frequency"][mask], + y_pred[mask], + sample_weight=df_test["TotalVolume"][mask], + ) + logger.info(f"mean Poisson deviance: {mpd:.4f}") + + +def get_preprocessor(): + # Preprocessor + tree_preprocessor = ColumnTransformer( + [ + ( + "categorical", + OrdinalEncoder(), + ["StreetType", "WeekdayofData", "Weather"], + ), + ("numeric", "passthrough", ["SpeedLimitMPH", "HourofData"]), + ], + remainder="drop", + ) + return tree_preprocessor + + +def gridsearch_gbm(df_train, df_test): + tree_preprocessor = get_preprocessor() + + # Model + poisson_gbrt = Pipeline( + [ + ("preprocessor", tree_preprocessor), + ( + "regressor", + HistGradientBoostingRegressor( + loss="poisson", + ), + ), + ] + ) + + # Gridsearch + parameters = { + "regressor__learning_rate": [0.1, 0.3, 0.5, 0.7, 1], + "regressor__max_iter": [100, 200, 300, 500], + "regressor__min_samples_leaf": [100, 200, 300, 500], + } + + search = GridSearchCV( + poisson_gbrt, + parameters, + scoring="neg_mean_poisson_deviance", + cv=10, + n_jobs=8, + verbose=1, + ) + search.fit( + df_train, + df_train["Frequency"], + regressor__sample_weight=df_train["TotalVolume"], + ) + + model = search.best_estimator_ + best_params = search.best_params_ + + # Evaluate + score_estimator(model, df_test) + + return best_params + + +def fit_final_gbm(df, params, filepath): + tree_preprocessor = get_preprocessor() + + # Model + poisson_gbrt = Pipeline( + [ + ("preprocessor", tree_preprocessor), + ( + "regressor", + HistGradientBoostingRegressor( + loss="poisson", + learning_rate=params["regressor__learning_rate"], + max_iter=params["regressor__max_iter"], + min_samples_leaf=params["regressor__min_samples_leaf"], + ), + ), + ] + ) + + poisson_gbrt.fit( + df, + df["Frequency"], + regressor__sample_weight=df["TotalVolume"], + ) + + with open(filepath / "freq_model.joblib", "wb") as f: + joblib.dump(poisson_gbrt, f) diff --git a/model/src/models/make_models.py b/model/src/models/make_models.py new file mode 100644 index 0000000..fdebee5 --- /dev/null +++ b/model/src/models/make_models.py @@ -0,0 +1,84 @@ +import gc +from pathlib import Path + +import click +from loguru import logger + +from src import MODELS_DIR +from src.models import frequency, severity + + +def make_frequency_models(overwrite): + df = frequency.load_modelling_data() + stations = list(df.StationId.unique()) + sorted(stations) + + for station_id in stations: + filepath = MODELS_DIR / "stations" / f"{station_id}" + filepath.mkdir(exist_ok=True, parents=True) + + if (filepath / "freq_model.joblib").exists() and not overwrite: + logger.warning(f"Model for station {station_id} already exists, skipping.") + continue + + (filepath / "model_fit.log").unlink(missing_ok=True) # Empty logs + logger.add(filepath / "model_fit.log") + logger.info(f"Training model for station {station_id}") + + station_df = frequency.prep_station_data(df, station_id, filepath) + df_train, df_test = frequency.split_for_training(station_df) + params = frequency.gridsearch_gbm(df_train, df_test) + frequency.fit_final_gbm(station_df, params, filepath) + + logger.remove() + gc.collect() + + +def make_severity_models(overwrite): + df = severity.load_modelling_data() + + filepath = MODELS_DIR / "severity" + filepath.mkdir(exist_ok=True, parents=True) + + if (filepath / "model.joblib").exists() and not overwrite: + logger.warning("Severity model already exists, skipping.") + return + + logger.add(filepath / "model_fit.log") + logger.info("Training severity model") + severity.fit_final_model(df, filepath) + + +@click.command() +# @click.option( +# "--frequency", +# "-f", +# is_flag=True, +# help="Frequency only", +# ) +# @click.option( +# "--severity", +# "-s", +# is_flag=True, +# help="Severity only", +# ) +@click.option( + "--overwrite", + "-o", + is_flag=True, + help="Overwrite existing models", +) +def main( + overwrite: bool = False, +): + """ """ + + make_frequency_models(overwrite) + make_severity_models(overwrite) + + +if __name__ == "__main__": + # not used in this stub but often useful for finding various files + project_dir = Path(__file__).resolve().parents[2] + + main() diff --git a/model/src/models/severity.py b/model/src/models/severity.py new file mode 100644 index 0000000..9f1ee80 --- /dev/null +++ b/model/src/models/severity.py @@ -0,0 +1,65 @@ +import pandas as pd +import numpy as np +import joblib +from sklearn.preprocessing import OneHotEncoder, OrdinalEncoder +from sklearn.pipeline import make_pipeline +from sklearn.compose import make_column_transformer +from imblearn.ensemble import BalancedRandomForestClassifier + +from src import CONFIG, PROC_DATA_DIR + + +def load_modelling_data(): + X = pd.read_feather(PROC_DATA_DIR / "accident.f") + + X = X[ + [ + "HourofData", + "Weather", + "BodyType", + "VehicleYear", + "Alcohol", + "RelativeSpeed", + "MaximumSeverity", + ] + ] + + X.BodyType = np.where(X.BodyType == "FOUR_WHEEL_VEHICLE", 1, 0) + X.Alcohol = np.where(X.Alcohol == "NO_ALCOHOL_INVOLVED", 0, 1) + + return X + + +def get_preprocessor(): + vehicle_year_labels = list(CONFIG["data"]["vehicle_year_bins"].keys()) + # Preprocessor + weather_encoder = OneHotEncoder(sparse=False) + vehicle_year_encoder = OrdinalEncoder(categories=[vehicle_year_labels]) + + preprocessor = make_column_transformer( + (weather_encoder, ["Weather"]), + (vehicle_year_encoder, ["VehicleYear"]), + remainder="passthrough", + n_jobs=1, + ) + return preprocessor + + +def fit_final_model(df, filepath): + preprocessor = get_preprocessor() + + X = df.drop(columns=["MaximumSeverity"]).copy() + y = df["MaximumSeverity"].astype(str).copy() + y[y != "NONE_TO_MINOR_INJURY"] = 1 + y[y == "NONE_TO_MINOR_INJURY"] = 0 + y = pd.to_numeric(y, downcast="integer") + + # Model + rf_clf = make_pipeline( + preprocessor, + BalancedRandomForestClassifier(random_state=42, n_jobs=-1), + ) + rf_clf.fit(X, y) + + with open(filepath / "sev_model.joblib", "wb") as f: + joblib.dump(rf_clf, f, compress=("xz", 1))