{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# PoC of Recommender System for safe and efficient Food Deliveries during Infectious Disease induced Lockdowns" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Course \"IBM Data Science\", Capstone Project, April 2020 by [Markus Mächler](https://www.linkedin.com/in/markus-maechler/)\n", "\n", "_written and tested with Jupyter Notebook 6.0.3 (with Python 3.7.6 on Win10x64 and MacOS 10.15.4)_" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Folium maps are not rendered on github. For the best experience, [please view this notebook with this link](https://nbviewer.jupyter.org/github/Funisher-code/Coursera_Capstone/blob/master/notebook/POC_food_delivery_recommender_system.ipynb).**\n", "\n", "----" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Introduction" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Major problems during a lockdown concerning shopping / food deliveries\n", "1. keep people at risk **safe but not hungry**\n", "2. keep shops and restaurants **up and running**\n", "3. use helpers as **efficiently and safely** as possible" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The purpose of this project is creating a small POC (proof of concept) to help tackling our three problems by creating a simple but efficient recommender system that could be used to place actual orders." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**For more background information, [read the full report here](https://github.com/Funisher-code/Coursera_Capstone/blob/master/report/COVID-19_Safe_And_Efficient_Food_Deliveries.md).**" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Data + Methodology" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Location Data\n", "\n", "For every order, **location data of all the three parties** is absolutely necessary. For this POC I'm using the following data sources.\n", "\n", "- customers: **hypothetical address of a customer in Zurich**\n", "- helpers: **hypothetical addresses in Zurich**\n", "- shops: **Foursquare location data** acquired via API in the vicinity of people at risk\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### install dependencies if not already installed" ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "# map rendering library\n", "\n", "#!conda install -c conda-forge folium=0.5.0 --yes # uncomment if not installed already" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "# used to locate the coordinates of addresses, cities, countries, and landmarks across the globe\n", "# using third-party geocoders and other data sources\n", "\n", "#!conda install -c conda-forge geopy --yes # uncomment if not installed already" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [], "source": [ "# install python library to generate fake data\n", "\n", "#!conda install -c conda-forge faker --yes # uncomment if not installed already" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### import necessary libraries" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Libraries imported.\n" ] } ], "source": [ "from geopy.geocoders import Nominatim\n", "import geopy.distance\n", "import folium \n", "from collections import namedtuple\n", "import pandas as pd # for working with dataframes\n", "import numpy as np # for workig with matrizes\n", "from scipy import stats # for getting quick descriptive statistics\n", "import matplotlib.pyplot as plt # for plotting\n", "from matplotlib.colors import LinearSegmentedColormap # to define custom colored maps in our plots\n", "import requests # to handle requests\n", "import time # used to sleep for a defined amount of time\n", "from faker import Faker # to generate fake person data\n", "\n", "print('Libraries imported.')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### customer location data" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let's define an address of a customer to work with. This would simply be something a customer would have to specify upon signing up. I just picked one at random." ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [], "source": [ "custAddr = 'Gsteigstrasse 9, 8049 Zurich'" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now we define an instance of geocoder with user agent _food_explorer_.\n", "\n", "And we use it to convert our customer address to a location in terms of latitude and longitude." ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "The geograpical coordinate of Gsteigstrasse 9, 8049 Zurich are 47.4028839, 8.499580847726023.\n" ] } ], "source": [ "geolocator = Nominatim(user_agent=\"food_explorer\")\n", "location = geolocator.geocode(custAddr)\n", "custLatitude = location.latitude\n", "custLongitude = location.longitude\n", "print('The geograpical coordinate of {} are {}, {}.'.format(custAddr, custLatitude, custLongitude))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let's quickly check if the coordinates reflect the addess of the customer." ] }, { "cell_type": "code", "execution_count": 7, "metadata": { "scrolled": false }, "outputs": [ { "data": { "text/html": [ "
" ], "text/plain": [ "" ] }, "execution_count": 7, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# create map of Zurich using the latitude and longitude value generated by geocoder\n", "map_folium = folium.Map(location=[custLatitude, custLongitude], zoom_start=15)\n", "\n", "# add marker of our address to the map\n", "label = custAddr\n", "label = folium.Popup(label, parse_html=True)\n", "folium.CircleMarker(\n", " [custLatitude, custLongitude],\n", " radius=5,\n", " popup=label,\n", " color='red',\n", " parse_html=False).add_to(map_folium) \n", " \n", "map_folium" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Again, if you can't see the map above, that's totally normal github behaviour. [Please view this notebook with this link](https://nbviewer.jupyter.org/github/Funisher-code/Coursera_Capstone/blob/master/notebook/POC_food_delivery_recommender_system.ipynb).\n", "\n", "Back to Geolocator. That certainly looks like Zurich but let's zoom all in to make sure it's accurate." ] }, { "cell_type": "code", "execution_count": 8, "metadata": { "scrolled": true }, "outputs": [ { "data": { "text/html": [ "
" ], "text/plain": [ "" ] }, "execution_count": 8, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# create map of Zurich using the latitude and longitude value generated by geocoder\n", "map_folium = folium.Map(location=[custLatitude, custLongitude], zoom_start=18)\n", "\n", "# add marker of our address to the map\n", "label = custAddr\n", "label = folium.Popup(label, parse_html=True)\n", "folium.CircleMarker(\n", " [custLatitude, custLongitude],\n", " radius=10,\n", " popup=label,\n", " color='red',\n", " parse_html=False).add_to(map_folium) \n", " \n", "map_folium" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Nice, that's something we can work with.\n", "\n", "So customers signing up for the delivery service can simply specify their address as location data." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "### helper location data " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Just like with our customer we will use a Nominatom geolocator to convert our helper addresses to geolocations.\n", "\n", "Unlike with the freshly signed up customer, we will import a list I created with __fictional helper data__ in the vicinity of our customer.\n", "\n", "_TLDR alert, if you don't care how I got this data, leave the next session out [by clicking here to import the resulting csv](#tldr1)._\n", ".\n", "\n", ".\n", "\n", ".\n", "\n", ".\n", "\n", ".\n", "\n", ".\n", "\n", ".\n", "\n", ".\n", "\n", ".\n", "\n", ".\n", "\n", ".\n", "\n", "Or you can take the long way\n", "\n", ".\n", "\n", ".\n", "\n", ".\n", "\n", ".\n", "\n", ".\n", "\n", ".\n", "\n", "**Thanks for staying with me. Here's what I did:**" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "1. query https://tel.search.ch/ for the postal code 8049, since that's where our customer lives.\n", "2. copy all search results from the website. that looks something like this:\n", "```\n", "name01, surname01\t\n", "name01, surname01\n", "street01 number01, 8049 Zürich\n", "044 123 45 67\n", "Details\n", "name02, surname02\t\n", "name02, surname02\n", "street02 number02, 8049 Zürich\n", "044 765 43 21 *\n", "Details\n", "```\n", "3. use some powerful editor: get rid of leading and trailing garbage, then convert to a usable csv format. I was on Win10 then and used notepad++ for the following:\n", " - search for \"\\n\" and replace with \";\" since \",\" is already widely used in the addresses\n", " - search for \"\\r\" and \"\\t\" and replace with nothing\n", " - search for \";Details;\" and replace with \"\\n\"\n", "\n", "\n", "4. what we now have resembles something like this:\n", "```\n", "name01, surname01;name01, surname01;street01 number01, 8049 Zürich;044 123 45 67\n", "name02, surname02;name02, surname02;street02 number02, 8049 Zürich;044 765 43 21 *\n", "...\n", "```" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "First we import the resulting file to a pandas dataframe. **I tried the following code:**" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "```python\n", "helperData=pd.read_csv('helperAddr_raw.csv', sep=';', header=None)\n", "```" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This leads to the following error:" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "```python\n", "ParserError: Error tokenizing data. C error: Expected 4 fields in line 51, saw 5\n", "```" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "That didn't work. What happened? After inspection of line 51 I realized that there are cases, where people also specify professions. This results in an additional column.\n", "\n", "**This is just another example why carefully curing your raw data is both important and time consuming.**\n", "\n", "For our purpose I'll just skip rows that don't match the number of columns." ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "b'Skipping line 4: expected 4 fields, saw 5\\nSkipping line 5: expected 4 fields, saw 5\\nSkipping line 8: expected 4 fields, saw 5\\nSkipping line 10: expected 4 fields, saw 5\\nSkipping line 11: expected 4 fields, saw 5\\nSkipping line 12: expected 4 fields, saw 5\\nSkipping line 14: expected 4 fields, saw 5\\nSkipping line 16: expected 4 fields, saw 5\\nSkipping line 17: expected 4 fields, saw 5\\nSkipping line 21: expected 4 fields, saw 5\\nSkipping line 23: expected 4 fields, saw 5\\nSkipping line 27: expected 4 fields, saw 5\\nSkipping line 28: expected 4 fields, saw 5\\nSkipping line 33: expected 4 fields, saw 5\\nSkipping line 39: expected 4 fields, saw 5\\nSkipping line 41: expected 4 fields, saw 5\\nSkipping line 42: expected 4 fields, saw 5\\nSkipping line 43: expected 4 fields, saw 5\\nSkipping line 44: expected 4 fields, saw 5\\nSkipping line 46: expected 4 fields, saw 5\\nSkipping line 48: expected 4 fields, saw 5\\nSkipping line 50: expected 4 fields, saw 5\\nSkipping line 53: expected 4 fields, saw 5\\nSkipping line 54: expected 4 fields, saw 5\\nSkipping line 61: expected 4 fields, saw 5\\nSkipping line 64: expected 4 fields, saw 5\\nSkipping line 71: expected 4 fields, saw 5\\nSkipping line 72: expected 4 fields, saw 5\\nSkipping line 73: expected 4 fields, saw 5\\nSkipping line 77: expected 4 fields, saw 5\\nSkipping line 78: expected 4 fields, saw 5\\nSkipping line 79: expected 4 fields, saw 5\\nSkipping line 80: expected 4 fields, saw 5\\nSkipping line 82: expected 4 fields, saw 5\\nSkipping line 84: expected 4 fields, saw 5\\nSkipping line 85: expected 4 fields, saw 5\\nSkipping line 87: expected 4 fields, saw 5\\nSkipping line 90: expected 4 fields, saw 5\\nSkipping line 92: expected 4 fields, saw 5\\nSkipping line 93: expected 4 fields, saw 5\\nSkipping line 94: expected 4 fields, saw 5\\nSkipping line 97: expected 4 fields, saw 5\\nSkipping line 100: expected 4 fields, saw 5\\nSkipping line 102: expected 4 fields, saw 5\\nSkipping line 104: expected 4 fields, saw 5\\nSkipping line 105: expected 4 fields, saw 5\\nSkipping line 106: expected 4 fields, saw 5\\nSkipping line 107: expected 4 fields, saw 5\\nSkipping line 108: expected 4 fields, saw 5\\nSkipping line 109: expected 4 fields, saw 5\\nSkipping line 110: expected 4 fields, saw 5\\nSkipping line 111: expected 4 fields, saw 5\\nSkipping line 113: expected 4 fields, saw 5\\nSkipping line 115: expected 4 fields, saw 5\\nSkipping line 117: expected 4 fields, saw 5\\nSkipping line 122: expected 4 fields, saw 5\\nSkipping line 134: expected 4 fields, saw 5\\nSkipping line 142: expected 4 fields, saw 5\\nSkipping line 151: expected 4 fields, saw 5\\nSkipping line 161: expected 4 fields, saw 5\\nSkipping line 163: expected 4 fields, saw 5\\nSkipping line 164: expected 4 fields, saw 5\\nSkipping line 166: expected 4 fields, saw 5\\nSkipping line 172: expected 4 fields, saw 5\\nSkipping line 175: expected 4 fields, saw 5\\nSkipping line 188: expected 4 fields, saw 5\\nSkipping line 189: expected 4 fields, saw 5\\nSkipping line 192: expected 4 fields, saw 5\\nSkipping line 199: expected 4 fields, saw 5\\nSkipping line 200: expected 4 fields, saw 5\\n'\n" ] } ], "source": [ "helperData=pd.read_csv('helperAddr_raw.csv', sep=';', header=None, error_bad_lines=False)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "That's quite some skipped lines. Let's see how many addresses we got:" ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "(130, 4)" ] }, "execution_count": 10, "metadata": {}, "output_type": "execute_result" } ], "source": [ "helperData.shape" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "So after skipping quite some rows this resulted in 130 unique \"fictional helpers\".\n", "\n", "By the way, the **asterik after the phone number** signifies, that the holder of the number doesn't want to get bothered by ad calls.\n", "\n", "**Allthough this is all puplicly available information, let's get rid of all names and phone numbers.**\n", "\n", "Then we name the remaining column address and have a look at the first 5 rows." ] }, { "cell_type": "code", "execution_count": 11, "metadata": { "scrolled": true }, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
address
0Michelstrasse 6, 8049 Zürich
1Riedhofstrasse 277, 8049 Zürich
2Brunnwiesenstrasse. 78, 8049 Zürich
3Rütihofstrasse 32, 8049 Zürich
4Segantinistrasse 38, 8049 Zürich
\n", "
" ], "text/plain": [ " address\n", "0 Michelstrasse 6, 8049 Zürich\n", "1 Riedhofstrasse 277, 8049 Zürich\n", "2 Brunnwiesenstrasse. 78, 8049 Zürich\n", "3 Rütihofstrasse 32, 8049 Zürich\n", "4 Segantinistrasse 38, 8049 Zürich" ] }, "execution_count": 11, "metadata": {}, "output_type": "execute_result" } ], "source": [ "helperData.drop(columns=[0,1,3], inplace=True)\n", "helperData.rename(columns={2:'address'}, inplace=True)\n", "helperData.head()" ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [], "source": [ "helperData.to_csv('helperData_addresses.csv',sep=';') # save dataframe to a csv" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "#### TLDR off\n", "\n", "If you don't want to perform the manual steps up above you may join again from here.\n", "\n", "Or if you accidentally missed the fun part or just made up your mind, you can [click here to go back](#tldr1back).\n", "\n", "If you're not interested in enriching the helper dataframe with geolocation and fake email data, [you can skip that and jump to the next part, where we're visualizing the helper and customer locations on a map.](#tldr1evenfurther).\n", "\n", "\n", "\n", "**Again, thanks for your interest and sticking with me.**\n", "\n", "Let's import the csv containing our addresses." ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
address
0Michelstrasse 6, 8049 Zürich
1Riedhofstrasse 277, 8049 Zürich
2Brunnwiesenstrasse. 78, 8049 Zürich
3Rütihofstrasse 32, 8049 Zürich
4Segantinistrasse 38, 8049 Zürich
\n", "
" ], "text/plain": [ " address\n", "0 Michelstrasse 6, 8049 Zürich\n", "1 Riedhofstrasse 277, 8049 Zürich\n", "2 Brunnwiesenstrasse. 78, 8049 Zürich\n", "3 Rütihofstrasse 32, 8049 Zürich\n", "4 Segantinistrasse 38, 8049 Zürich" ] }, "execution_count": 13, "metadata": {}, "output_type": "execute_result" } ], "source": [ "helperData=pd.read_csv('helperData_addresses.csv',sep=';') # import csv that resulted from manual data gathering\n", "helperData.drop(columns=['Unnamed: 0'], inplace=True) # drop the unnamed column with the row numbers\n", "helperData.head()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In the next step, we us geolocator again to convert our helper addresses to locations.\n", "\n", "I had to find out that too many requests in a short time leads to timeouts and the for loop to crash all the time.\n", "But the **following combination of a try block and sleep function workes just fine**: " ] }, { "cell_type": "code", "execution_count": 14, "metadata": { "scrolled": true }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "starting geolocator conversion:\n", "row 1 converted: Michelstrasse 6, 8049 Zürich to 47.4045882 , 8.4956993\n", "row 2 converted: Riedhofstrasse 277, 8049 Zürich to 47.4068335 , 8.486364788336479\n", "row 3 converted: Brunnwiesenstrasse. 78, 8049 Zürich to 47.401818899999995 , 8.502904109747357\n", "row 4 converted: Rütihofstrasse 32, 8049 Zürich to 47.4139362 , 8.477824429475469\n", "row 5 converted: Segantinistrasse 38, 8049 Zürich to 47.405278499999994 , 8.500653449999996\n", "row 6 converted: Riedhofstrasse 63, 8049 Zürich to 47.4047016 , 8.492083950865386\n", "row 7 converted: Riedhofstrasse 299, 8049 Zürich to 47.4077043 , 8.4850702\n", "row 8 converted: Imbisbühlstrasse 17, 8049 Zürich to 47.403117 , 8.4936818\n", "row 9 converted: Limmattalstrasse 266, 8049 Zürich to 47.4023642 , 8.4938806\n", "row 10 converted: Im Wingert 12, 8049 Zürich to 47.4003858 , 8.509406051787845\n", "row 11 converted: Kettberg 5, 8049 Zürich to 47.398512049999994 , 8.511144443967313\n", "row 12 converted: Riedhofstrasse 29, 8049 Zürich to 47.40424195 , 8.493492214249404\n", "row 13 converted: Im Wingert 36, 8049 Zürich to 47.401517850000005 , 8.50783103406484\n", "row 14 converted: Eichholzweg 6, 8049 Zürich to 47.4035913 , 8.507237738482058\n", "row 15 converted: Nötzlistrasse 15, 8049 Zürich to 47.4048336 , 8.502146001864272\n", "row 16 converted: Giacomettistrasse 19, 8049 Zürich to 47.402628500000006 , 8.505656048990708\n", "row 17 converted: Limmattalstrasse 353, 8049 Zürich to 47.4023642 , 8.4938806\n", "row 18 converted: Regensdorferstrasse 29, 8049 Zürich to 47.40405615 , 8.49537580285676\n", "row 19 converted: Winzerhalde 46, 8049 Zürich to 47.400393699999995 , 8.490019776228419\n", "row 20 converted: Kappenbühlweg 11, 8049 Zürich to 47.40397745 , 8.498320080768725\n", "row 21 converted: Segantinistrasse 125, 8049 Zürich to 47.406653649999996 , 8.493464163600624\n", "row 22 converted: Kettberg 7, 8049 Zürich to 47.398474050000004 , 8.511562547187086\n", "row 23 converted: Limmattalstrasse 388, 8049 Zürich to 47.4023642 , 8.4938806\n", "row 24 converted: Segantinistrasse 141, 8049 Zürich to 47.40724525 , 8.49231699229448\n", "row 25 converted: Riedhofstrasse 260, 8049 Zürich to 47.4073525 , 8.487545992277301\n", "row 26 converted: Winzerhalde 46, 8049 Zürich to 47.400393699999995 , 8.490019776228419\n", "row 27 converted: Riedhofstrasse 378, 8049 Zürich to 47.40997865 , 8.481313518053021\n", "row 28 converted: Giacomettistrasse 16, 8049 Zürich to 47.40261315 , 8.504931210121436\n", "row 29 converted: Limmattalstrasse 155, 8049 Zürich to 47.4009218 , 8.501674200879306\n", "row 30 converted: Limmattalstrasse 289, 8049 Zürich to 47.4023642 , 8.4938806\n", "row 31 converted: Winzerstrasse 85, 8049 Zürich to 47.4001692 , 8.4942713\n", "row 32 converted: Ottenbergstrasse 52, 8049 Zürich to 47.3982443 , 8.5082464\n", "row 33 converted: Rebstockweg 10, 8049 Zürich to 47.40084375 , 8.499320839294402\n", "row 34 converted: Limmattalstrasse 29, 8049 Zürich to 47.39646655 , 8.510388795637967\n", "row 35 converted: Ackersteinstrasse 143, 8049 Zürich to 47.398583200000004 , 8.504129803030196\n", "row 36 converted: Obere Bläsistrasse 3, 8049 Zürich to 47.40407485 , 8.504589908836735\n", "row 37 converted: Grossmannstrasse 30, 8049 Zürich to 47.3965458 , 8.506668409076683\n", "row 38 converted: Geeringstrasse 37, 8049 Zürich to 47.414803649999996 , 8.4800326180024\n", "row 39 converted: Naglerwiesenstrasse 52, 8049 Zürich to 47.4129728 , 8.481258365051458\n", "row 40 converted: Limmattalstrasse 5, 8049 Zürich to 47.3959048 , 8.51437249405286\n", "row 41 converted: Imbisbühlhalde 11, 8049 Zürich to 47.407041050000004 , 8.483379649989264\n", "row 42 converted: Segantinistrasse 114, 8049 Zürich to 47.406776750000006 , 8.494158042122827\n", "row 43 converted: Ottenbergstrasse 23, 8049 Zürich to 47.39691345 , 8.511821033987438\n", "row 44 converted: Ferdinand-Hodler-Strasse 10, 8049 Zürich to 47.40406205 , 8.5014806\n", "row 45 converted: Am Börtli 6, 8049 Zürich to 47.399609 , 8.508015663133941\n", "row 46 converted: Heizenholz 45, 8049 Zürich to 47.411592299999995 , 8.485578726437662\n", "row 47 converted: Heizenholz 45, 8049 Zürich to 47.411592299999995 , 8.485578726437662\n", "row 48 converted: Limmattalstrasse 163, 8049 Zürich to 47.40139945 , 8.50031672341791\n", "row 49 converted: Riedhofweg 35, 8049 Zürich to 47.4094958 , 8.484568246313057\n", "row 50 converted: Appenzellerstrasse 11, 8049 Zürich to 47.39936925 , 8.50878727210031\n", "row 51 converted: Segantinistrasse 216, 8049 Zürich to 47.4055211 , 8.4990666\n", "row 52 converted: Limmattalstrasse 55, 8049 Zürich to 47.397697300000004 , 8.507149700000003\n", "row 53 converted: Grossmannstrasse 30, 8049 Zürich to 47.3965458 , 8.506668409076683\n", "row 54 converted: Segantinisteig 2, 8049 Zürich to 47.40659025 , 8.492463546329287\n", "row 55 converted: Kürbergstrasse 34, 8049 Zürich to 47.39851575 , 8.50836297095779\n", "row 56 converted: Riedhofstrasse 366, 8049 Zürich to 47.4099269 , 8.482156201366113\n", "row 57 converted: Schärrergasse 3, 8049 Zürich to 47.4024113 , 8.497628509978188\n", "row 58 converted: Rütihofstrasse 2, 8049 Zürich to 47.41447625 , 8.479546130169688\n", "row 59 converted: Konrad-Ilg-Strasse 22, 8049 Zürich to 47.406844050000004 , 8.478812911455975\n", "row 60 converted: Im oberen Boden 11, 8049 Zürich to 47.4158279 , 8.4810428\n", "row 61 converted: Hohenklingenstrasse 28, 8049 Zürich to 47.40206635 , 8.492932251058683\n", "row 62 converted: Ackersteinstrasse 161, 8049 Zürich to 47.3996702 , 8.502347348296613\n", "row 63 converted: Im Maas 8, 8049 Zürich to 47.40202845 , 8.503932799627133\n", "row 64 converted: Reinhold-Frei-Strasse 27, 8049 Zürich to 47.40922205 , 8.484806457645165\n", "row 65 converted: Konrad-Ilg-Strasse 22, 8049 Zürich to 47.406844050000004 , 8.478812911455975\n", "row 66 converted: Riedhofstrasse 366, 8049 Zürich to 47.4099269 , 8.482156201366113\n", "row 67 converted: Rebbergstrasse 49, 8049 Zürich to 47.397550949999996 , 8.512940485974205\n", "row 68 converted: Ferdinand-Hodler-Strasse 15, 8049 Zürich to 47.404049 , 8.5007635\n", "row 69 converted: Kappenbühlweg 9, 8049 Zürich to 47.4035729 , 8.4986268\n", "row 70 converted: Riedhofstrasse 366, 8049 Zürich to 47.4099269 , 8.482156201366113\n", "row 71 converted: Riedhofstrasse 366, 8049 Zürich to 47.4099269 , 8.482156201366113\n", "row 72 converted: Hardeggstrasse 11, 8049 Zürich to 47.39591585 , 8.503814796285795\n", "row 73 converted: Winzerstrasse 9, 8049 Zürich to 47.4001692 , 8.4942713\n", "row 74 converted: Rütihofstrasse 32, 8049 Zürich to 47.4139362 , 8.477824429475469\n", "row 75 converted: Winzerstrasse 17, 8049 Zürich to 47.4001692 , 8.4942713\n", "row 76 converted: Segantinistrasse 116, 8049 Zürich to 47.40698605 , 8.494096435663089\n", "row 77 converted: Limmattalstrasse 387, 8049 Zürich to 47.4023642 , 8.4938806\n", "row 78 converted: Limmattalstrasse 44, 8049 Zürich to 47.3967363 , 8.510600824286353\n", "row 79 converted: Grossmannstrasse 25, 8049 Zürich to 47.3957288 , 8.507356001739868\n", "row 80 converted: Ackersteinstrasse 72, 8049 Zürich to 47.39660415 , 8.508825040147928\n", "row 81 converted: Limmattalstrasse 385, 8049 Zürich to 47.4023642 , 8.4938806\n", "row 82 converted: Limmattalstrasse 47, 8049 Zürich to 47.3972976 , 8.507906203219179\n", "row 83 converted: Ferdinand-Hodler-Strasse 3, 8049 Zürich to 47.4033394 , 8.502052907946574\n", "row 84 converted: Rütihofstrasse 27, 8049 Zürich to 47.4134899 , 8.479216234892291\n", "row 85 converted: Limmattalstrasse 65, 8049 Zürich to 47.39809405 , 8.506421018270299\n", "row 86 converted: Bergellerstrasse 41, 8049 Zürich to 47.4065698 , 8.4918502\n", "row 87 converted: Lachenacker 23, 8049 Zürich to 47.406785150000005 , 8.489951210345343\n", "row 88 converted: Michelstrasse 42, 8049 Zürich to 47.406724 , 8.495634063234608\n", "row 89 converted: Segantinistrasse 67, 8049 Zürich to 47.4055331 , 8.498280308181073\n", "row 90 converted: Kappenbühlweg 11, 8049 Zürich to 47.40397745 , 8.498320080768725\n", "row 91 converted: Heizenholz 32, 8049 Zürich to 47.411048050000005 , 8.487030915985708\n", "row 92 converted: Michelstrasse 52, 8049 Zürich to 47.40735665 , 8.49514231967135\n", "row 93 converted: Segantinistrasse 141, 8049 Zürich to 47.40724525 , 8.49231699229448\n", "row 94 converted: Wildenstrasse 15, 8049 Zürich to 47.4074537 , 8.488328928012049\n", "row 95 converted: Ackersteinstrasse 12, 8049 Zürich to 47.39557175 , 8.514209950400817\n", "row 96 converted: Bläsistrasse 49, 8049 Zürich to 47.40327485 , 8.50388585112654\n", "row 97 converted: Imbisbühlstrasse 159, 8049 Zürich to 47.4035273 , 8.4925379\n", "row 98 converted: Rütihofstrasse 3, 8049 Zürich to 47.41406945 , 8.480366719245135\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "row 99 converted: Reinhold-Frei-Strasse 67, 8049 Zürich to 47.4105669 , 8.483620068895908\n", "row 100 converted: Riedhofstrasse 41, 8049 Zürich to 47.4039369 , 8.4929601\n", "row 101 converted: Rütihofstrasse 31, 8049 Zürich to 47.4135121 , 8.478689754897568\n", "row 102 converted: Regensdorferstrasse 157, 8049 Zürich to 47.4034924 , 8.4973495\n", "row 103 converted: Im oberen Boden 72, 8049 Zürich to 47.416623 , 8.481363723023257\n", "row 104 converted: Segantinistrasse 63, 8049 Zürich to 47.4054201 , 8.498685540823669\n", "row 105 converted: Rebbergstrasse 65, 8049 Zürich to 47.3977891 , 8.5115899\n", "row 106 converted: Appenzellerstrasse 55, 8049 Zürich to 47.4006704 , 8.507789952394452\n", "row 107 converted: Kappenbühlweg 11, 8049 Zürich to 47.40397745 , 8.498320080768725\n", "row 108 converted: Heizenholz 45, 8049 Zürich to 47.411592299999995 , 8.485578726437662\n", "row 109 converted: Frankentalerstrasse 35, 8049 Zürich to 47.40705455 , 8.479552765429101\n", "row 110 converted: Bauherrenstrasse 48, 8049 Zürich to 47.401067999999995 , 8.498660900058468\n", "row 111 converted: Am Wasser 60, 8049 Zürich to 47.3960814 , 8.5061461\n", "row 112 converted: Frankentalerstrasse 35, 8049 Zürich to 47.40705455 , 8.479552765429101\n", "row 113 converted: Am Wasser 135, 8049 Zürich to 47.3991721 , 8.49784505\n", "row 114 converted: Engadinerweg 3, 8049 Zürich to 47.40721385 , 8.494296400462744\n", "row 115 converted: Riedhofstrasse 291, 8049 Zürich to 47.4075104 , 8.4859317\n", "row 116 converted: Ackersteinstrasse 150, 8049 Zürich to 47.3995796 , 8.503456578133632\n", "row 117 converted: Im Stelzenacker 17, 8049 Zürich to 47.4164693 , 8.482322248287408\n", "row 118 converted: Imbisbühlstrasse 91, 8049 Zürich to 47.4035273 , 8.4925379\n", "row 119 converted: Am Wasser 104C, 8049 Zürich to 47.3972397 , 8.5029985\n", "row 120 converted: Rütihofstrasse 45, 8049 Zürich to 47.41304865 , 8.4780279\n", "row 121 converted: Rütihofstrasse 14, 8049 Zürich to 47.41509235 , 8.47869765\n", "row 122 converted: Segantinistrasse 93, 8049 Zürich to 47.4062361 , 8.495016249999999\n", "row 123 converted: Bäulistrasse 26A, 8049 Zürich to 47.3986874 , 8.502970441951632\n", "row 124 converted: Am Börtli 10, 8049 Zürich to 47.4000024 , 8.508161923884499\n", "row 125 converted: Giblenstrasse 48, 8049 Zürich to 47.4122174 , 8.481714491018776\n", "row 126 converted: Riedhofweg 4, 8049 Zürich to 47.408466 , 8.484371898020221\n", "row 127 converted: Kürbergstrasse 50, 8049 Zürich to 47.398748499999996 , 8.510023075722788\n", "row 128 converted: Riedhofstrasse 70, 8049 Zürich to 47.40531435 , 8.492104419535483\n", "row 129 converted: Winzerhalde 85, 8049 Zürich to 47.4024914 , 8.485379154222267\n", "row 130 converted: Gsteigstrasse 31, 8049 Zürich to 47.4030586 , 8.5019213\n" ] } ], "source": [ "helpLat=[] # list to store latitutes\n", "helpLong= [] # list to store longitudes\n", "location=None # variable to check if geolocator returned a results\n", "count=0 # counter for converted addresses \n", "\n", "print('starting geolocator conversion:')\n", "for tmpAddr in helperData['address']:\n", " while(location is None): # try to use geolocator until a result is returned\n", " try: \n", " location = geolocator.geocode(tmpAddr) # try converting address to gps position\n", " count+=1\n", " helpLat.append(location.latitude) # append latitude to list\n", " helpLong.append(location.longitude) # append longitude to list\n", " print('row',count,'converted:',tmpAddr,'to',location.latitude,',',location.longitude)\n", " except: # in case geocoder times out, wait half a second \n", " #print('.',end='') # uncomment to see how many attempts failed\n", " time.sleep(0.5) #wait half a second\n", " location=None" ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Number of elements in the list helpLat: 130\n", "Number of elements in the list helpLong: 130\n" ] } ], "source": [ "print('Number of elements in the list helpLat: ',len(helpLat))\n", "print('Number of elements in the list helpLong:',len(helpLong))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Seems to have worked.\n", "\n", "Now let's **add the resulting geoposition rows to our helper dataframe**, name the columns and have a look at the resulting dataframe." ] }, { "cell_type": "code", "execution_count": 16, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
addresslatitudelongitude
helperId
0Michelstrasse 6, 8049 Zürich47.4045888.495699
1Riedhofstrasse 277, 8049 Zürich47.4068338.486365
2Brunnwiesenstrasse. 78, 8049 Zürich47.4018198.502904
3Rütihofstrasse 32, 8049 Zürich47.4139368.477824
4Segantinistrasse 38, 8049 Zürich47.4052788.500653
\n", "
" ], "text/plain": [ " address latitude longitude\n", "helperId \n", "0 Michelstrasse 6, 8049 Zürich 47.404588 8.495699\n", "1 Riedhofstrasse 277, 8049 Zürich 47.406833 8.486365\n", "2 Brunnwiesenstrasse. 78, 8049 Zürich 47.401819 8.502904\n", "3 Rütihofstrasse 32, 8049 Zürich 47.413936 8.477824\n", "4 Segantinistrasse 38, 8049 Zürich 47.405278 8.500653" ] }, "execution_count": 16, "metadata": {}, "output_type": "execute_result" } ], "source": [ "helperData['latitude']=helpLat\n", "helperData['longitude']=helpLong\n", "helperData.index.name='helperId'\n", "helperData.head()" ] }, { "cell_type": "code", "execution_count": 17, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
addresslatitudelongitude
helperId
125Riedhofweg 4, 8049 Zürich47.4084668.484372
126Kürbergstrasse 50, 8049 Zürich47.3987488.510023
127Riedhofstrasse 70, 8049 Zürich47.4053148.492104
128Winzerhalde 85, 8049 Zürich47.4024918.485379
129Gsteigstrasse 31, 8049 Zürich47.4030598.501921
\n", "
" ], "text/plain": [ " address latitude longitude\n", "helperId \n", "125 Riedhofweg 4, 8049 Zürich 47.408466 8.484372\n", "126 Kürbergstrasse 50, 8049 Zürich 47.398748 8.510023\n", "127 Riedhofstrasse 70, 8049 Zürich 47.405314 8.492104\n", "128 Winzerhalde 85, 8049 Zürich 47.402491 8.485379\n", "129 Gsteigstrasse 31, 8049 Zürich 47.403059 8.501921" ] }, "execution_count": 17, "metadata": {}, "output_type": "execute_result" } ], "source": [ "helperData.tail()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now we **create and add some fake email addresses** for our helpers:" ] }, { "cell_type": "code", "execution_count": 18, "metadata": {}, "outputs": [], "source": [ "# create a list of fake names and email addresses\n", "fake = Faker(['de_DE']) \n", "helperEmail=[]\n", "for i in range(len(helperData)):\n", " helperEmail.append(fake.email())" ] }, { "cell_type": "code", "execution_count": 19, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "['olena23@web.de',\n", " 'abdul37@lorch.de',\n", " 'ron06@etzler.org',\n", " 'stanislaw11@gorlitz.com',\n", " 'dorleboerner@aol.de']" ] }, "execution_count": 19, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# check result\n", "helperEmail[:5]" ] }, { "cell_type": "code", "execution_count": 20, "metadata": {}, "outputs": [], "source": [ "# add fake email addresses to our dataframe\n", "helperData['email']=helperEmail" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Have a final look at our dataframe:" ] }, { "cell_type": "code", "execution_count": 21, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
addresslatitudelongitudeemail
helperId
0Michelstrasse 6, 8049 Zürich47.4045888.495699olena23@web.de
1Riedhofstrasse 277, 8049 Zürich47.4068338.486365abdul37@lorch.de
2Brunnwiesenstrasse. 78, 8049 Zürich47.4018198.502904ron06@etzler.org
3Rütihofstrasse 32, 8049 Zürich47.4139368.477824stanislaw11@gorlitz.com
4Segantinistrasse 38, 8049 Zürich47.4052788.500653dorleboerner@aol.de
\n", "
" ], "text/plain": [ " address latitude longitude \\\n", "helperId \n", "0 Michelstrasse 6, 8049 Zürich 47.404588 8.495699 \n", "1 Riedhofstrasse 277, 8049 Zürich 47.406833 8.486365 \n", "2 Brunnwiesenstrasse. 78, 8049 Zürich 47.401819 8.502904 \n", "3 Rütihofstrasse 32, 8049 Zürich 47.413936 8.477824 \n", "4 Segantinistrasse 38, 8049 Zürich 47.405278 8.500653 \n", "\n", " email \n", "helperId \n", "0 olena23@web.de \n", "1 abdul37@lorch.de \n", "2 ron06@etzler.org \n", "3 stanislaw11@gorlitz.com \n", "4 dorleboerner@aol.de " ] }, "execution_count": 21, "metadata": {}, "output_type": "execute_result" } ], "source": [ "helperData.head()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Looks good to me.\n", "\n", "How about saving our work to a csv in case anything goes wrong:" ] }, { "cell_type": "code", "execution_count": 22, "metadata": {}, "outputs": [], "source": [ "helperData.to_csv('helperData.csv')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**With that, our manual data gathering part for our ficticious helpers and customer is done.**\n", "\n", "----" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "### customers and helper geolocations visualized on a map\n", "Now let's visualize the helpers and our customer on the map.\n", "\n", "The customer is represented with a red circle and the possible helpers with blue circles." ] }, { "cell_type": "code", "execution_count": 23, "metadata": {}, "outputs": [], "source": [ "# helperData = pd.read_csv('helperData.csv') # uncomment in case you want to proceed from here" ] }, { "cell_type": "code", "execution_count": 24, "metadata": { "scrolled": true }, "outputs": [ { "data": { "text/html": [ "
" ], "text/plain": [ "" ] }, "execution_count": 24, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# create map of Zurich using the latitude and longitude values generated by geocoder\n", "map_folium = folium.Map(location=[custLatitude, custLongitude], zoom_start=15)\n", "\n", "# add a red cirle for the position of our customer to the map\n", "label = custAddr\n", "label = folium.Popup(label, parse_html=True)\n", "folium.CircleMarker(\n", " [custLatitude, custLongitude],\n", " radius=10,\n", " popup=label,\n", " color='red',\n", " parse_html=False).add_to(map_folium) \n", "\n", "# add smaller blue circles for helpers\n", "for email, lat, lng in zip(helperData['email'], helperData['latitude'], helperData['longitude']): \n", " label = email\n", " label = folium.Popup(label, parse_html=True)\n", " folium.CircleMarker(\n", " [lat, lng],\n", " radius=3,\n", " popup=label,\n", " color='blue',\n", " fill=True,\n", " fill_color='#1234cc',\n", " fill_opacity=0.7,\n", " parse_html=False).add_to(map_folium) \n", "\n", "map_folium" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Very nice. Let's hope that we have that many helpers in the real world." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### shops - get Foursquare location data" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Right now we have a customer and possible helpers. But we still need positions of shops or restaurants where the helpers can fetch the goods and bring them to the customer. " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "For that will use realtime data we get by querying the Foursquare API. So we're finally done with fake data." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "First of all, we're gonna read the necessary Foursquare credentials and version **from the local File \"cred_foursquare.json\"**" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "this json could resemble the following structure:\n", "```json\n", "{\n", " \"name\": \"Password JSON\",\n", " \"version\": \"1.0.0\",\n", " \"description\": \"\",\n", " \"command\": \"\",\n", " \"log\": \"\",\n", " \"location\": \"\",\n", " \"timeout\": \"0\",\n", " \"commandargs\": \"\",\n", " \"keys\":\n", " [\n", " {\n", " \"scriptkey\": \"VERSION\",\n", " \"scriptvalue\": \"20180605\",\n", " \"scriptdefaultvalue\": \"\"\n", " },\n", " {\n", " \"scriptkey\": \"ID\",\n", " \"scriptvalue\": \"***************************\"\n", " \"scriptdefaultvalue\": \"\"\n", " },\n", " {\n", " \"scriptkey\": \"SECRET\",\n", " \"scriptvalue\": \"***************************\",\n", " \"scriptdefaultvalue\": \"\",\n", " \"type\": \"password\"\n", " }\n", " ]\n", "}\n", "```" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The ID and SECRET can be obtained for free on https://developer.foursquare.com/ by registering for a free developer account.\n", "\n", "After saving the credentials in the file \"cred_foursquare.json\" we can load the values into variables." ] }, { "cell_type": "code", "execution_count": 25, "metadata": {}, "outputs": [], "source": [ "cred=pd.read_json(r'cred_foursquare.json') # read the json file" ] }, { "cell_type": "code", "execution_count": 26, "metadata": {}, "outputs": [], "source": [ "VERSION=cred['keys'][0]['scriptvalue'] # Foursquare API version\n", "CLIENT_ID=cred['keys'][1]['scriptvalue'] # Foursquare ID\n", "CLIENT_SECRET=cred['keys'][2]['scriptvalue'] # Foursquare Secret\n", "LIMIT=100 # max number of answers because number of queries on the free account are limited" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now let's think about __what type of venues__ we want to include as possible shops / restaurants \n", "\n", "Everybody can have a look at all of Foursquare's venue categories and the corresponding IDs that can be queried with the API:\n", "https://developer.foursquare.com/docs/build-with-foursquare/categories/\n", "\n", "__I'll go with the following in this case:__\n", "\n", "- Food\n", "4d4b7105d754a06374d81259\n", "\n", "- Food & Drink Shop\n", "4bf58dd8d48988d1f9941735\n", "\n", "- Fruit & Vegetable Store\n", "52f2ab2ebcbc57f1066b8b1c\n", "\n", "- Market\n", "50be8ee891d4fa8dcc7199a7\n", "\n", "- Pharmacy\n", "4bf58dd8d48988d10f951735\n", "\n", "- Shopping Mall\n", "4bf58dd8d48988d1fd941735\n", "\n", "\n", "Of course there would also be a possibility to query for much more specific kinds of restaurants, e.g. a\n", "Vegetarian / Vegan Restaurant\n", "4bf58dd8d48988d1d3941735\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "To get going, we first __define a function that queries the Foursquare API and returns nearby venues__." ] }, { "cell_type": "code", "execution_count": 27, "metadata": {}, "outputs": [], "source": [ "def getNearbyVenues(names, latitudes, longitudes, radius, categories): # function adapted from course script\n", " \n", " venues_list=[]\n", " for name, lat, lng in zip(names, latitudes, longitudes):\n", " print('Searching venues for',name)\n", " \n", " # create the API request URL\n", " url = 'https://api.foursquare.com/v2/venues/explore?&client_id={}&client_secret={}&v={}&ll={},{}&radius={}&limit={}&categoryId={}'.format(\n", " CLIENT_ID, \n", " CLIENT_SECRET, \n", " VERSION, \n", " lat, \n", " lng, \n", " radius, \n", " LIMIT,\n", " categories)\n", " \n", " # make the GET request\n", " results = requests.get(url).json()[\"response\"]['groups'][0]['items']\n", " \n", " # return only relevant information for each nearby venue\n", " venues_list.append([(\n", " name, \n", " lat, \n", " lng, \n", " v['venue']['name'], \n", " v['venue']['location']['lat'], \n", " v['venue']['location']['lng'], \n", " v['venue']['categories'][0]['name']) for v in results])\n", "\n", " nearby_venues = pd.DataFrame([item for venue_list in venues_list for item in venue_list])\n", " nearby_venues.columns = ['Customer Email', \n", " 'Neighborhood Latitude', \n", " 'Neighborhood Longitude', \n", " 'Venue', \n", " 'Venue Latitude', \n", " 'Venue Longitude', \n", " 'Venue Category']\n", " \n", " return(nearby_venues)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now that we have the function in place, let's use it to query for the venues that are close to our customer.\n", "\n", "For the radius, let's just assume a reasonable number: A normal healthy young human walks at about 4 km/h by foot. So in 15 minutes the helper would travel 1 kilometer (=1000 meters). That's a reasonable number in a City like Zurich, where one has plenty of shopping possibilites. In rural areas one would probably consider cars as the transport way of choice and would also need to apply a larger radius.\n", "\n", "For here, 1000 meters should be fine." ] }, { "cell_type": "code", "execution_count": 28, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Searching venues for hungrycustomer1942@bluewin.ch\n" ] }, { "data": { "text/plain": [ "(12, 7)" ] }, "execution_count": 28, "metadata": {}, "output_type": "execute_result" } ], "source": [ "venues = getNearbyVenues(names=['hungrycustomer1942@bluewin.ch'],\n", " latitudes=[custLatitude],\n", " longitudes=[custLongitude],\n", " radius=1000,\n", " categories='4d4b7105d754a06374d81259,4bf58dd8d48988d1f9941735,52f2ab2ebcbc57f1066b8b1c,50be8ee891d4fa8dcc7199a7,4bf58dd8d48988d10f951735,4bf58dd8d48988d1fd941735'\n", " )\n", "venues.shape" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "So in our customers case we were able to find 12 venues nearby (in the radius of 1000 meters)." ] }, { "cell_type": "code", "execution_count": 29, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
Customer EmailNeighborhood LatitudeNeighborhood LongitudeVenueVenue LatitudeVenue LongitudeVenue Category
0hungrycustomer1942@bluewin.ch47.4028848.499581Kebab-Haus Höngg47.4021728.495997Fast Food Restaurant
1hungrycustomer1942@bluewin.ch47.4028848.499581Desperado47.4020838.496903Mexican Restaurant
2hungrycustomer1942@bluewin.ch47.4028848.499581Argentina Steakhouse47.4039358.496524Steakhouse
3hungrycustomer1942@bluewin.ch47.4028848.499581Osteria da Biagio47.4022648.495558Italian Restaurant
4hungrycustomer1942@bluewin.ch47.4028848.499581Pizzeria Rapido47.4016728.499121Pizza Place
5hungrycustomer1942@bluewin.ch47.4028848.499581Marcello's Bistro47.4030018.498206Café
6hungrycustomer1942@bluewin.ch47.4028848.499581Restaurant Turbinenhaus47.3953748.505774Italian Restaurant
7hungrycustomer1942@bluewin.ch47.4028848.499581Del Sole Pizzakurier Höngg47.4002818.503689Pizza Place
8hungrycustomer1942@bluewin.ch47.4028848.499581Maharani47.4027798.492496Indian Restaurant
9hungrycustomer1942@bluewin.ch47.4028848.499581Restaurant Werdinsel47.3993538.489361Snack Place
10hungrycustomer1942@bluewin.ch47.4028848.499581ETH Chemiecafeteria47.4080148.507681Café
11hungrycustomer1942@bluewin.ch47.4028848.499581SV Bistro Hönggerberg47.4083128.507627Café
\n", "
" ], "text/plain": [ " Customer Email Neighborhood Latitude \\\n", "0 hungrycustomer1942@bluewin.ch 47.402884 \n", "1 hungrycustomer1942@bluewin.ch 47.402884 \n", "2 hungrycustomer1942@bluewin.ch 47.402884 \n", "3 hungrycustomer1942@bluewin.ch 47.402884 \n", "4 hungrycustomer1942@bluewin.ch 47.402884 \n", "5 hungrycustomer1942@bluewin.ch 47.402884 \n", "6 hungrycustomer1942@bluewin.ch 47.402884 \n", "7 hungrycustomer1942@bluewin.ch 47.402884 \n", "8 hungrycustomer1942@bluewin.ch 47.402884 \n", "9 hungrycustomer1942@bluewin.ch 47.402884 \n", "10 hungrycustomer1942@bluewin.ch 47.402884 \n", "11 hungrycustomer1942@bluewin.ch 47.402884 \n", "\n", " Neighborhood Longitude Venue Venue Latitude \\\n", "0 8.499581 Kebab-Haus Höngg 47.402172 \n", "1 8.499581 Desperado 47.402083 \n", "2 8.499581 Argentina Steakhouse 47.403935 \n", "3 8.499581 Osteria da Biagio 47.402264 \n", "4 8.499581 Pizzeria Rapido 47.401672 \n", "5 8.499581 Marcello's Bistro 47.403001 \n", "6 8.499581 Restaurant Turbinenhaus 47.395374 \n", "7 8.499581 Del Sole Pizzakurier Höngg 47.400281 \n", "8 8.499581 Maharani 47.402779 \n", "9 8.499581 Restaurant Werdinsel 47.399353 \n", "10 8.499581 ETH Chemiecafeteria 47.408014 \n", "11 8.499581 SV Bistro Hönggerberg 47.408312 \n", "\n", " Venue Longitude Venue Category \n", "0 8.495997 Fast Food Restaurant \n", "1 8.496903 Mexican Restaurant \n", "2 8.496524 Steakhouse \n", "3 8.495558 Italian Restaurant \n", "4 8.499121 Pizza Place \n", "5 8.498206 Café \n", "6 8.505774 Italian Restaurant \n", "7 8.503689 Pizza Place \n", "8 8.492496 Indian Restaurant \n", "9 8.489361 Snack Place \n", "10 8.507681 Café \n", "11 8.507627 Café " ] }, "execution_count": 29, "metadata": {}, "output_type": "execute_result" } ], "source": [ "venues" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now how about we create a **function** we can reuse to **display a map of all three parties (customer, helpers, shops) together**:" ] }, { "cell_type": "code", "execution_count": 30, "metadata": { "scrolled": true }, "outputs": [], "source": [ "def createRawMap(custLatitude, custLongitude, custAddr, helperData, venues, zoomStart=15):\n", "\n", " # create map of Zurich using the latitude and longitude values generated by geocoder\n", " map_folium = folium.Map(location=[custLatitude, custLongitude], zoom_start=zoomStart)\n", "\n", " # add a red cirle for the position of our customer to the map\n", " label = custAddr\n", " label = folium.Popup(label, parse_html=True)\n", " folium.CircleMarker(\n", " [custLatitude, custLongitude],\n", " radius=10,\n", " popup=label,\n", " color='red',\n", " parse_html=False).add_to(map_folium) \n", "\n", "\n", " # add smaller blue circles for helpers\n", " for email, lat, lng in zip(helperData['email'], helperData['latitude'], helperData['longitude']): \n", " label = email\n", " label = folium.Popup(label, parse_html=True)\n", " folium.CircleMarker(\n", " [lat, lng],\n", " radius=3,\n", " popup=label,\n", " color='blue',\n", " fill=True,\n", " fill_color='#1234cc',\n", " fill_opacity=0.7,\n", " parse_html=False).add_to(map_folium) \n", "\n", " # add smaller green circles for helpers\n", " for venue, lat, lng in zip(venues['Venue'], venues['Venue Latitude'], venues['Venue Longitude']): \n", " label = venue\n", " label = folium.Popup(label, parse_html=True)\n", " folium.CircleMarker(\n", " [lat, lng],\n", " radius=3,\n", " popup=label,\n", " color='green',\n", " fill=True,\n", " fill_color='#90ee90',\n", " fill_opacity=0.7,\n", " parse_html=False).add_to(map_folium)\n", " \n", " return map_folium" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "and see if it works:" ] }, { "cell_type": "code", "execution_count": 31, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "display(createRawMap(custLatitude, custLongitude, custAddr, helperData, venues))" ] }, { "cell_type": "markdown", "metadata": { "slideshow": { "slide_type": "slide" } }, "source": [ "Nice.\n", "\n", "Customer = red circle\n", "\n", "Helpers = blue dots\n", "\n", "Shops = green dots\n", "\n", "As a human it would be quite obvious now to tell the customer to order at Marcello's Bistro or Pizzerie Rapodio.\n", "There are also quite a few ideal helpers because they live both near to the bistro and the customer.\n", "\n", "However, our customer may not like the food Marcello's Bistro produces etc., choices may not be that obvious or there simply could be too many reqeusts for a person to process by phone.\n", "\n", "__We would like to automate such decisions and ideally introduce various parameters in the process.__\n", "\n", "So now here our recommender system comes into place." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### recommender system - what measurements do we use to get to our recommendations?" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "what we want to minimize:\n", "- total distance helper has to travel\n", "\n", "assumptions:\n", "- distances measured in air-line distance (may in some cases where street topology is special not lead to the ultimate best decisions, e.g. elevation ignored or dead end streets)\n", "\n", "variables to measure distances the helper has to travel:\n", "1. __HtoS__ = distance from the helper (H) to the shop (S)\n", "2. __StoC__ = distance from shop (S) to customer (C)\n", "3. __CtoH__ = distance from customer (C) to shop (S)\n", "\n", "for the travel time of our helper to be minimized we basically need to **minimze the sum of our variables 1 to 3** (HtoS+StoC+CtoH = __totDis__)" ] }, { "cell_type": "code", "execution_count": 32, "metadata": {}, "outputs": [], "source": [ "# initialize variables\n", "numH = helperData.shape[0] # number of helpers\n", "numS = venues.shape[0] # number of shops\n", "numC = 1 # test with only 1 customer\n", "HtoS = np.zeros([numS, numH]) # 2-dim matrix, helpers in columns, shops in rows\n", "StoC = np.zeros([numS,1]) # 1-dim matrix, shops in rows\n", "CtoH = np.zeros([1, numH]) # 1-dim matrix, helpers in columns\n", "totDist = np.zeros([numS, numH]) #2-dim matrix, helpers in columns, shops in rows" ] }, { "cell_type": "code", "execution_count": 33, "metadata": {}, "outputs": [], "source": [ "# calculate distances from all helpers to all shops (in meters rounded to whole numbers)\n", "for hId, helper in helperData.iterrows():\n", " for sId, shop in venues.iterrows():\n", " tmpCord1 = (helper['latitude'], helper['longitude'])\n", " tmpCord2 = (shop['Venue Latitude'], shop['Venue Longitude'])\n", " HtoS[sId][hId]=round(geopy.distance.distance(tmpCord1, tmpCord2).m,0)" ] }, { "cell_type": "code", "execution_count": 34, "metadata": {}, "outputs": [], "source": [ "# calculate distances from all shops to our customer (in meters rounded to whole numbers)\n", "tmpCord1 = (custLatitude, custLongitude)\n", "for sId, shop in venues.iterrows():\n", " tmpCord2 = (shop['Venue Latitude'], shop['Venue Longitude'])\n", " StoC[sId][0]=round(geopy.distance.distance(tmpCord1, tmpCord2).m,0)" ] }, { "cell_type": "code", "execution_count": 35, "metadata": {}, "outputs": [], "source": [ "# calculate distances from our customer to all helpers (in meters rounded to whole numbers)\n", "tmpCord2 = (custLatitude, custLongitude)\n", "for hId, helper in helperData.iterrows():\n", " tmpCord1 = (helper['latitude'], helper['longitude'])\n", " CtoH[0][hId]=round(geopy.distance.distance(tmpCord1, tmpCord2).m,0) " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### descriptive statistics" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now we do some quick and dirty **heatmaps and descriptive statistics to get a feel for distribution of the distances** we have calculated here." ] }, { "cell_type": "code", "execution_count": 36, "metadata": {}, "outputs": [], "source": [ "# define custom color map ranging from green to white to red \n", "greenToRed=LinearSegmentedColormap.from_list('gr',[\"g\", \"w\",\"w\",\"w\",\"w\",\"w\", \"r\"], N=128)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The color range from green to red is **separated by 5 segments of white to get a better feel for the extreme values**." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### HtoS – distances between helpers and shops" ] }, { "cell_type": "code", "execution_count": 37, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "DescribeResult(nobs=1560, minmax=(66.0, 2997.0), mean=1020.1467948717949, variance=347633.2350121708, skewness=0.6149120572489256, kurtosis=-0.05753935414874878)\n" ] } ], "source": [ " print(stats.describe(HtoS, axis=None))" ] }, { "cell_type": "code", "execution_count": 38, "metadata": { "scrolled": false }, "outputs": [ { "data": { "image/png": "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\n", "text/plain": [ "
" ] }, "metadata": { "needs_background": "light" }, "output_type": "display_data" } ], "source": [ "# set font size of plot labels to 24 pt\n", "font = {'size' : 24}\n", "plt.rc('font', **font)\n", "\n", "# Heatmap of distances from all helpers to all shops (green = closer, red = further away)\n", "plt.figure(figsize = (30,30))\n", "plt.imshow(HtoS, cmap=greenToRed)\n", "plt.title(\"distances - HtoS\")\n", "plt.xlabel('helpers')\n", "plt.ylabel('shops')\n", "plt.show())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The closest shop to a helper is only 66 meters away, mean distance is about 1 km and variance is obviously very high.\n", "\n", "The shop with indice 6 seems to be the one that is furthest away from some of the helpers. If you have a look a the venues datafram, you see, that this is \"Restaurant Turbinenhaus\". If you look on the map, you can see it's located at the bottom right of the map and therefore naturally far away from helpers on the top left." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### StoC – distances between shops and customers" ] }, { "cell_type": "code", "execution_count": 39, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "DescribeResult(nobs=12, minmax=(105.0, 957.0), mean=482.5833333333333, variance=99210.08333333334, skewness=0.3728582113979914, kurtosis=-1.475914078451692)\n" ] } ], "source": [ "print(stats.describe(StoC,axis=None))" ] }, { "cell_type": "code", "execution_count": 40, "metadata": { "scrolled": true }, "outputs": [ { "data": { "image/png": "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\n", "text/plain": [ "
" ] }, "metadata": { "needs_background": "light" }, "output_type": "display_data" } ], "source": [ "# Heatmap of distances from all shops to our customer (green = closer, red = further away)\n", "plt.imshow(StoC.transpose(), cmap=greenToRed) # transposed to use less space\n", "#ax1 = plt.axes(frameon=False)\n", "plt.title(\"distances - StoC\")\n", "plt.xlabel('shops')\n", "plt.tick_params(\n", " axis='y', # changes apply to the x-axis\n", " which='both', # both major and minor ticks are affected\n", " left=False, # ticks along the bottom edge are off\n", " top=False, # ticks along the top edge are off\n", " labelleft=False) # labels along the bottom edge are off\n", "plt.show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We get a good picture about possible shop recommendations here. The shop with indice 6 also seems to be far away from our customer. It probably won't make it into our recommendation list, then.\n", "\n", "**Shops 5,4 and 1 could be potentially interesting when the shop has to be close.**" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### CtoH – distances between our customer and our helpers" ] }, { "cell_type": "code", "execution_count": 41, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "DescribeResult(nobs=130, minmax=(105.0, 2080.0), mean=876.6769230769231, variance=306164.14287418005, skewness=0.6655659563462873, kurtosis=-0.6208715228192303)\n" ] } ], "source": [ "print(stats.describe(CtoH,axis=None))" ] }, { "cell_type": "code", "execution_count": 42, "metadata": {}, "outputs": [ { "data": { "image/png": "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\n", "text/plain": [ "
" ] }, "metadata": { "needs_background": "light" }, "output_type": "display_data" } ], "source": [ "# Heatmap of distances from our customer to all helpers (green = closer, red = further away)\n", "plt.figure(figsize = (30,30))\n", "plt.imshow(CtoH, cmap=greenToRed)\n", "plt.title(\"distances - CtoH\")\n", "plt.xlabel('helpers')\n", "plt.tick_params(\n", " axis='y', # changes apply to the x-axis\n", " which='both', # both major and minor ticks are affected\n", " left=False, # ticks along the bottom edge are off\n", " top=False, # ticks along the top edge are off\n", " labelleft=False) # labels along the bottom edge are offplt.show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "There are quite a few helpers located close to our customer, which is promising." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### totDist – total distance" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Having our 3 matrices in place it is actually really simple now to **calculate our total distance:**" ] }, { "cell_type": "code", "execution_count": 43, "metadata": {}, "outputs": [], "source": [ "# calculate total distance\n", "totDist=HtoS+StoC+CtoH" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Again, we do simple descriptive statistics and a quick heatmap:" ] }, { "cell_type": "code", "execution_count": 44, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "DescribeResult(nobs=1560, minmax=(281.0, 6034.0), mean=2379.4070512820513, variance=1379222.639206181, skewness=0.5854207297337536, kurtosis=-0.2367640214900928)\n" ] } ], "source": [ "print(stats.describe(totDist,axis=None))" ] }, { "cell_type": "code", "execution_count": 45, "metadata": {}, "outputs": [ { "data": { "image/png": "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\n", "text/plain": [ "
" ] }, "metadata": { "needs_background": "light" }, "output_type": "display_data" } ], "source": [ "# Heatmap of total distances for all shop and helper combinations (green = closer, red = further away)\n", "plt.figure(figsize = (30,30))\n", "plt.imshow(HtoS, cmap=greenToRed)\n", "plt.title(\"distances - totDist (total distance)\")\n", "plt.xlabel('helpers')\n", "plt.ylabel('shops')\n", "plt.show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Again, we can clearly see that \"Restaurant Turbinenhaus\" with index 6 will not be a good recommendation. " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### weighting" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now we have 4 different distance measures with totDist clearly being the most important one.\n", "\n", "**BUT: Let's consider the case, where the customer orders refrigerated or especially heavy goods.**\n", "\n", "As a customer you don't want to receive your refrigerated goods thawed.\n", "\n", "As a helper you don't want to lift heavy goods for a long time and over a long distance.\n", "\n", "**So total distance won't be the most important measure anymore.** For both cases it would then make sense to **minimize the distance between the shop and the customer**. Even if this means to have a larger total distance than any other helper shop combinations. \n", "\n", "Additionally, refrigerate goods thaw more quickly, the hotter it is. So the local weather needs to be addressed as well." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### calculation of the weights" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Warning: Tuning of the following parameters is based on (somewhat educated) guesses, not sound science." ] }, { "cell_type": "code", "execution_count": 46, "metadata": {}, "outputs": [], "source": [ "# function that returns a weight matrix depending on the weather, items purchased and equipment of the helper\n", "def returnWeights (temperature, isRefrigerated=False, isHeavy=False):\n", " \n", " # array structure \n", " # for weights[HtoS, StoC, StoC_T, CtoH, totDist] -> \"_T\" means that the helper has a trolley\n", " w_standard = np.array([0, 0, 0, 0, 10]) # total distance gets optimized, rest omitted\n", " \n", " if temperature>30: # StoC gets weighted more the hotter it is (if we have refrigerated items),\n", " # for the helper having a trolley (with a thermos bag) helps to keep the goods cold\n", " w_refrigerated = np.array([0, 20, 10, 0, 0])\n", " elif temperature>25:\n", " w_refrigerated = np.array([0, 10, 5, 0, 0])\n", " elif temperature>15:\n", " w_refrigerated = np.array([0, 4, 2, 0, 0])\n", " else:\n", " w_refrigerated = np.array([0, 2, 1, 0, 0])\n", "\n", " # StoC gets weighted more for heavy items (3) and even more so if the helper has no trolley (8)\n", " w_heavy = np.array([0, 20, 10, 0, 0])\n", " \n", " # calculate total weight by adding the subweights\n", " w_total = w_standard + isRefrigerated*w_refrigerated + isHeavy*w_heavy\n", "\n", " # calculate and return normalized weight matrix where all weights sum up to 1\n", " w_total = w_total / w_total.sum()\n", " return w_total" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We somehow need to get hold of *daily temperature* data that can be used to call our returnWeights function.\n", "\n", "For the current purpose we can use the free **openweathermap.org API**.\n", "\n", "We simply repeat the same process as for the Foursquare API:" ] }, { "cell_type": "code", "execution_count": 47, "metadata": {}, "outputs": [], "source": [ "# read Openweathermap.org API credentials like in the example of the foursquare API further above\n", "cred_weather=pd.read_json(r'cred_openweathermap.json')\n", "\n", "OWM_VERSION=cred_weather['keys'][0]['scriptvalue'] # Openweathermap API version\n", "OWM_APIKEY=cred_weather['keys'][1]['scriptvalue'] # Openweathermap API key" ] }, { "cell_type": "code", "execution_count": 48, "metadata": {}, "outputs": [], "source": [ "def getWeatherData (lat, lon, APIKEY, VERSION):\n", " # create the API request URL\n", " url = 'https://api.openweathermap.org/data/{}/weather?lat={}&lon={}&appid={}&units=metric'.format(\n", " VERSION, \n", " lat, \n", " lon, \n", " APIKEY)\n", "\n", " # make the GET request\n", " results = requests.get(url).json()\n", " \n", " return results" ] }, { "cell_type": "code", "execution_count": 49, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "{'coord': {'lon': 8.5, 'lat': 47.4},\n", " 'weather': [{'id': 500,\n", " 'main': 'Rain',\n", " 'description': 'light rain',\n", " 'icon': '10d'}],\n", " 'base': 'stations',\n", " 'main': {'temp': 19.93,\n", " 'feels_like': 16.91,\n", " 'temp_min': 18.33,\n", " 'temp_max': 21,\n", " 'pressure': 1008,\n", " 'humidity': 32},\n", " 'visibility': 10000,\n", " 'wind': {'speed': 2.1, 'deg': 40},\n", " 'rain': {'3h': 0.33},\n", " 'clouds': {'all': 95},\n", " 'dt': 1587910061,\n", " 'sys': {'type': 1,\n", " 'id': 6941,\n", " 'country': 'CH',\n", " 'sunrise': 1587874624,\n", " 'sunset': 1587925808},\n", " 'timezone': 7200,\n", " 'id': 2658017,\n", " 'name': 'Werdhölzli',\n", " 'cod': 200}" ] }, "execution_count": 49, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# call function to get current weather of our customer location\n", "weather = getWeatherData(custLatitude, custLongitude, OWM_APIKEY, OWM_VERSION)\n", "weather" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This works fine, the selected weather station 'Werdhölzli' makes sense and data seems to be reasonable for today.\n", "\n", "Let's extract the current temperature at our customer location." ] }, { "cell_type": "code", "execution_count": 50, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "19.93 °C\n" ] } ], "source": [ "# extract max temperature\n", "temp = weather['main']['temp']\n", "print(temp,'°C')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let's create a few test cases, where we play around with the parameters and have a look at the changes it produces on the weight matrix." ] }, { "cell_type": "code", "execution_count": 51, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Now we have a look at what weights are applied when we alter our variables\n", "\n", "First we're getting the current temperature:\n", "temperature at customer location: 19.93 °C\n", "\n", "[HtoS StoC StoC_T CtoH totDis]\n", "[0.000 0.000 0.000 0.000 1.000] -> standard case\n", "[0.000 0.500 0.250 0.000 0.250] -> heavy items\n", "[0.000 0.250 0.125 0.000 0.625] -> refrigerated items with today's weather\n", "[0.000 0.571 0.286 0.000 0.143] -> heavy and refrigerated item on a hot summer day\n" ] } ], "source": [ "print('Now we have a look at what weights are applied when we alter our variables\\n')\n", "\n", "print('First we\\'re getting the current temperature:')\n", "temp=getWeatherData(custLatitude, custLongitude, OWM_APIKEY, OWM_VERSION)['main']['temp']\n", "print('temperature at customer location:',temp,'°C\\n')\n", "\n", "np.set_printoptions(formatter={'float': lambda x: \"{0:0.3f}\".format(x)}) # print floats to only 3 digits precision\n", "\n", "print('[HtoS StoC StoC_T CtoH totDis]')\n", "standard_weights = returnWeights (temp, isRefrigerated=False, isHeavy=False)\n", "print(standard_weights,'-> standard case')\n", "\n", "heavy_weights = returnWeights (temp, isRefrigerated=False, isHeavy=True)\n", "print(heavy_weights,'-> heavy items')\n", "\n", "frigo_weights = returnWeights (temp, isRefrigerated=True, isHeavy=False)\n", "print(frigo_weights,'-> refrigerated items with today\\'s weather')\n", "\n", "hot_heavy_frigo_weights = returnWeights (temperature=35, isRefrigerated=True, isHeavy=True)\n", "print(hot_heavy_frigo_weights,'-> heavy and refrigerated item on a hot summer day')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "So in this setting **total distance gets less weight and the distance from the shop to the customer gets more weight under certain conditions**. Just like we wanted to." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "It's also obvious that HtoS and CtoH have no relevance at all in our current setting. So we might as well leave it out completely. But because future relevant use cases might bring importance to those, we'll keep it for now." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The parameter isRefrigerated depends on the food ordered. The trolly on the other hand is something that is determined by the helper.\n", "\n", "So let's create a **one hot encoded matrix for trolleys** with 1 indicating that the helper has a trolley:" ] }, { "cell_type": "code", "execution_count": 52, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "DescribeResult(nobs=130, minmax=(0, 1), mean=0.23846153846153847, variance=0.18300536672629697, skewness=1.2274688184341327, kurtosis=-0.4933202997719137)\n" ] } ], "source": [ "# generate matrix with length corresponding to our number of helpers and about 20% having a trolley\n", "hTrolley=np.random.choice([0, 1], size=helperData.shape[0], p=[.8, .2])\n", "#hTrolley=hTrolley.reshape((-1, 1))\n", "print(stats.describe(hTrolley,axis=None))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now we're only missing a reusable **function** that applies all this data and returns a **score matrix**:" ] }, { "cell_type": "code", "execution_count": 53, "metadata": { "scrolled": true }, "outputs": [], "source": [ "def calcScores(HtoS, StoC, CtoH, totDist, weights, hTrolley):\n", " w_HtoS=weights[0]\n", " w_StoC=weights[1]\n", " w_StoC_T=weights[2]\n", " w_CtoH=weights[3]\n", " w_totDist=weights[4]\n", " \n", " return w_HtoS*HtoS + w_StoC*StoC*hTrolley + w_StoC_T*StoC*abs(hTrolley-1) + w_CtoH*CtoH + w_totDist*totDist" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This function norms the score matrix by dividing it by the maximum value. This way we get a range from 0 to 1.\n", "\n", "0 would be the case, where the helper and the customer live at the same address as the shop.\n", "\n", "**So the closer the score to 0, the better:**" ] }, { "cell_type": "code", "execution_count": 54, "metadata": {}, "outputs": [], "source": [ "def normPerfectToZero(array):\n", " return array / array.max()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now we should be all set up." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "There could be numerous other parameters applied, e.g. shop rating and so an. But for the purpose of this PoC we'll leave it at that and see what our recommender system returns for our customer." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Results" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### standard case - not refrigerated, not heavy" ] }, { "cell_type": "code", "execution_count": 55, "metadata": {}, "outputs": [], "source": [ "standard_score = normPerfectToZero(calcScores(HtoS, StoC, CtoH, totDist, standard_weights, hTrolley))" ] }, { "cell_type": "code", "execution_count": 56, "metadata": {}, "outputs": [], "source": [ "# set font size of plot labels to 24 pt\n", "font = {'size' : 24}\n", "plt.rc('font', **font)" ] }, { "cell_type": "code", "execution_count": 57, "metadata": { "scrolled": true }, "outputs": [ { "data": { "image/png": "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\n", "text/plain": [ "
" ] }, "metadata": { "needs_background": "light" }, "output_type": "display_data" } ], "source": [ "# Heatmap of total distances for all shop and helper combinations\n", "# (green = lower score = better, red = high score = bad choice)\n", "plt.figure(figsize = (30,30))\n", "plt.imshow(standard_score, cmap=greenToRed)\n", "plt.title(\"scores – standard case\")\n", "plt.xlabel('helpers')\n", "plt.ylabel('shops')\n", "plt.show()" ] }, { "cell_type": "code", "execution_count": 58, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "DescribeResult(nobs=1560, minmax=(0.04656943984090156, 1.0), mean=0.39433328658966715, variance=0.03788120326673271, skewness=0.5854207297337527, kurtosis=-0.23676402149009323)" ] }, "execution_count": 58, "metadata": {}, "output_type": "execute_result" } ], "source": [ "stats.describe(standard_score,axis=None)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### heavy items" ] }, { "cell_type": "code", "execution_count": 59, "metadata": {}, "outputs": [], "source": [ "heavy_score = normPerfectToZero(calcScores(HtoS, StoC, CtoH, totDist, heavy_weights, hTrolley))" ] }, { "cell_type": "code", "execution_count": 60, "metadata": {}, "outputs": [ { "data": { "image/png": "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\n", "text/plain": [ "
" ] }, "metadata": { "needs_background": "light" }, "output_type": "display_data" } ], "source": [ "# Heatmap of total distances for all shop and helper combinations\n", "# (green = lower score = better, red = high score = bad choice)\n", "plt.figure(figsize = (30,30))\n", "plt.imshow(heavy_score, cmap=greenToRed)\n", "plt.title(\"scores – heavy items\")\n", "plt.xlabel('helpers')\n", "plt.ylabel('shops')\n", "plt.show()" ] }, { "cell_type": "code", "execution_count": 61, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "DescribeResult(nobs=1560, minmax=(0.04902832465388035, 1.0), mean=0.3781364090839513, variance=0.03131248140918763, skewness=0.5316299109179674, kurtosis=-0.04014952503549729)" ] }, "execution_count": 61, "metadata": {}, "output_type": "execute_result" } ], "source": [ "stats.describe(heavy_score,axis=None)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### refrigerated items" ] }, { "cell_type": "code", "execution_count": 62, "metadata": {}, "outputs": [], "source": [ "frigo_score = normPerfectToZero(calcScores(HtoS, StoC, CtoH, totDist, frigo_weights, hTrolley))" ] }, { "cell_type": "code", "execution_count": 63, "metadata": {}, "outputs": [ { "data": { "image/png": "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\n", "text/plain": [ "
" ] }, "metadata": { "needs_background": "light" }, "output_type": "display_data" } ], "source": [ "# Heatmap of total distances for all shop and helper combinations\n", "# (green = lower score = better, red = high score = bad choice)\n", "plt.figure(figsize = (30,30))\n", "plt.imshow(frigo_score, cmap=greenToRed)\n", "plt.title(\"scores – refrigerated items\")\n", "plt.xlabel('helpers')\n", "plt.ylabel('shops')\n", "plt.show()" ] }, { "cell_type": "code", "execution_count": 64, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "DescribeResult(nobs=1560, minmax=(0.047620549370841085, 1.0), mean=0.39404257951515825, variance=0.03629694972459201, skewness=0.5651590284958143, kurtosis=-0.17996869422459616)" ] }, "execution_count": 64, "metadata": {}, "output_type": "execute_result" } ], "source": [ "stats.describe(frigo_score,axis=None)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### refrigerated and heavy items on a hot summers day" ] }, { "cell_type": "code", "execution_count": 65, "metadata": {}, "outputs": [], "source": [ "hot_heavy_frigo_score = normPerfectToZero(calcScores(HtoS, StoC, CtoH, totDist, hot_heavy_frigo_weights, hTrolley))" ] }, { "cell_type": "code", "execution_count": 66, "metadata": {}, "outputs": [ { "data": { "image/png": "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\n", "text/plain": [ "
" ] }, "metadata": { "needs_background": "light" }, "output_type": "display_data" } ], "source": [ "# Heatmap of total distances for all shop and helper combinations\n", "# (green = lower score = better, red = high score = bad choice)\n", "plt.figure(figsize = (30,30))\n", "plt.imshow(hot_heavy_frigo_score, cmap=greenToRed)\n", "plt.title(\"scores – refrigerated and heavy items on a hot summers day\")\n", "plt.xlabel('helpers')\n", "plt.ylabel('shops')\n", "plt.show()" ] }, { "cell_type": "code", "execution_count": 67, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "DescribeResult(nobs=1560, minmax=(0.050168590988045374, 1.0), mean=0.36525276858627226, variance=0.030269464745680003, skewness=0.5951194092669662, kurtosis=0.06004874094664281)" ] }, "execution_count": 67, "metadata": {}, "output_type": "execute_result" } ], "source": [ "stats.describe(hot_heavy_frigo_score,axis=None)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### short analysis" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "When we have a quick look at the heatmaps of our score matrix, we can see that the score matrix seems to represent what it should and makes sense.\n", "\n", "There's shop rows that are either mostly white (shops with medium distance to the customer), white and green (closer shops) or white and red (shops with a larger distance). This is nice.\n", "\n", "**Shops 0 to 5 seem to have the highest scores. It's not as clear anymore that shops 5,4 and 1 are closest to the customer since not only StoC has an effect, but also the total distance.**\n", "\n", "When we look at the rows in the score matrices, we can clearly see that **helpers are either a fit or not**. There's not a single helper that has both red and green scores with different shops. Then there's helper rows that show no color at all, which are probably the ones having a medium distance to the customer.\n", "\n", "Also it's quite striking, that the \"standard case\" and \"refrigerated items case\" look quite alike. This is not surprising, since on the day I calculated this, the weather was not particularly hot. So there's only a minor differnece in weighting taking place.\n", "\n", "Also the \"heavy items case\" and the \"refrigerated and heavy items on a hot summers day case\" share more similarities, with the **extreme combination showing the most differences to the standard case**. This makes sense because there the weighting has the most differences to the standard case.\n", "\n", "**This appears to be a good base for making our recommendations.**" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### recommendation lists" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now we can list the **top 20 recommendations** (smallest scores and the corresponding helper shop combination):" ] }, { "cell_type": "code", "execution_count": 68, "metadata": {}, "outputs": [], "source": [ "# function that takes a 2dim numpy array and a number k\n", "# it returns a list with k rows where each row consists of [row index, column index, value]\n", "def findKMinFromNp(originalArray, k):\n", " array=np.copy(originalArray) # copy the numpy array in order not to change the original array\n", " dimArray=len(array[0]) # get number of columns\n", " arrMax=np.amax(array)+1 # get maximum value of array and add 1\n", " \n", " res=[] # initialize results list\n", " for i in range(k): # repeat this step k times\n", " minIndex=array.argmin() #find minimum value in array\n", " row=int(minIndex/dimArray) # determine row index of min value\n", " col=minIndex%dimArray # determine column index of min value\n", " value=array[row][col] # save value\n", " res.append([row, col, value]) # append results in results list in the form [value, row index, column index]\n", " array[row][col]=arrMax # overwrite the min value with a value that is larger than the current maximum\n", " # ...to prevent finding the same minimum in the next iteration \n", " return res" ] }, { "cell_type": "code", "execution_count": 69, "metadata": {}, "outputs": [], "source": [ "def printRecommendation(scores, venues):\n", " for rank, score in enumerate(scores, start=1):\n", " print(rank,'/ shop:',venues.iloc[score[0]]['Venue'],'/ helper:',helperData.iloc[score[1]]['email'])" ] }, { "cell_type": "code", "execution_count": 70, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "1 / shop: Marcello's Bistro / helper: zrogge@karz.com\n", "2 / shop: Marcello's Bistro / helper: alexeiholsten@web.de\n", "3 / shop: Marcello's Bistro / helper: bbachmann@web.de\n", "4 / shop: Marcello's Bistro / helper: gerlachgerhardt@gmail.com\n", "5 / shop: Marcello's Bistro / helper: angelicabloch@web.de\n", "6 / shop: Marcello's Bistro / helper: iris38@beyer.de\n", "7 / shop: Pizzeria Rapido / helper: vzirme@tlustek.de\n", "8 / shop: Pizzeria Rapido / helper: selmaaustermuehle@googlemail.com\n", "9 / shop: Pizzeria Rapido / helper: alexeiholsten@web.de\n", "10 / shop: Desperado / helper: alexeiholsten@web.de\n" ] } ], "source": [ "standard_recommendation=findKMinFromNp(standard_score,10)\n", "printRecommendation(standard_recommendation, venues)" ] }, { "cell_type": "code", "execution_count": 71, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "1 / shop: Marcello's Bistro / helper: zrogge@karz.com\n", "2 / shop: Marcello's Bistro / helper: bbachmann@web.de\n", "3 / shop: Marcello's Bistro / helper: gerlachgerhardt@gmail.com\n", "4 / shop: Marcello's Bistro / helper: angelicabloch@web.de\n", "5 / shop: Marcello's Bistro / helper: iris38@beyer.de\n", "6 / shop: Marcello's Bistro / helper: alexeiholsten@web.de\n", "7 / shop: Pizzeria Rapido / helper: selmaaustermuehle@googlemail.com\n", "8 / shop: Marcello's Bistro / helper: wally73@heydrich.com\n", "9 / shop: Pizzeria Rapido / helper: zrogge@karz.com\n", "10 / shop: Pizzeria Rapido / helper: kreinberta@paertzelt.net\n" ] } ], "source": [ "heavy_recommendation=findKMinFromNp(heavy_score,10)\n", "printRecommendation(heavy_recommendation, venues)" ] }, { "cell_type": "code", "execution_count": 72, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "1 / shop: Marcello's Bistro / helper: zrogge@karz.com\n", "2 / shop: Marcello's Bistro / helper: alexeiholsten@web.de\n", "3 / shop: Marcello's Bistro / helper: bbachmann@web.de\n", "4 / shop: Marcello's Bistro / helper: gerlachgerhardt@gmail.com\n", "5 / shop: Marcello's Bistro / helper: angelicabloch@web.de\n", "6 / shop: Marcello's Bistro / helper: iris38@beyer.de\n", "7 / shop: Pizzeria Rapido / helper: selmaaustermuehle@googlemail.com\n", "8 / shop: Pizzeria Rapido / helper: vzirme@tlustek.de\n", "9 / shop: Pizzeria Rapido / helper: zrogge@karz.com\n", "10 / shop: Pizzeria Rapido / helper: kreinberta@paertzelt.net\n" ] } ], "source": [ "frigo_recommendation=findKMinFromNp(frigo_score,10)\n", "printRecommendation(frigo_recommendation, venues)" ] }, { "cell_type": "code", "execution_count": 73, "metadata": { "scrolled": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "1 / shop: Marcello's Bistro / helper: zrogge@karz.com\n", "2 / shop: Marcello's Bistro / helper: bbachmann@web.de\n", "3 / shop: Marcello's Bistro / helper: gerlachgerhardt@gmail.com\n", "4 / shop: Marcello's Bistro / helper: angelicabloch@web.de\n", "5 / shop: Marcello's Bistro / helper: iris38@beyer.de\n", "6 / shop: Marcello's Bistro / helper: wally73@heydrich.com\n", "7 / shop: Pizzeria Rapido / helper: selmaaustermuehle@googlemail.com\n", "8 / shop: Pizzeria Rapido / helper: zrogge@karz.com\n", "9 / shop: Pizzeria Rapido / helper: kreinberta@paertzelt.net\n", "10 / shop: Marcello's Bistro / helper: selmaaustermuehle@googlemail.com\n" ] } ], "source": [ "hot_heavy_frigo_recommendation=findKMinFromNp(hot_heavy_frigo_score,10)\n", "printRecommendation(hot_heavy_frigo_recommendation, venues)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now isn't that nice. Already in the top 10 recommendations we can see major differences depending on the circumstances.\n", "\n", "Since Marcello's Bistro is definitely the closest to our customer, it makes sense that the top 6 recommendations are for this shop. But there's quite some differences in helper choice. Obviously because of the helpers having trolleys or not. \n", "\n", "Take for example alexeiholsten@web.de as number 2 recommendation in the standard case. This helper is also number 2 in the \"refrigerated items case\" under not so hot circumstances. In the \"heavy items case\" he's number 6 and in the extreme combo he's not even in the top 10 anymore.\n", "\n", "So this works quite well." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "So now we have our list. It further is very easy to get to the details of the shop and the helper, once the decision is made. e.g. here's the details for combo from the top of the list:" ] }, { "cell_type": "code", "execution_count": 74, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "Customer Email hungrycustomer1942@bluewin.ch\n", "Neighborhood Latitude 47.4029\n", "Neighborhood Longitude 8.49958\n", "Venue Marcello's Bistro\n", "Venue Latitude 47.403\n", "Venue Longitude 8.49821\n", "Venue Category Café\n", "Name: 5, dtype: object" ] }, "execution_count": 74, "metadata": {}, "output_type": "execute_result" } ], "source": [ "venues.iloc[standard_recommendation[0][0]]" ] }, { "cell_type": "code", "execution_count": 75, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "address Kappenbühlweg 9, 8049 Zürich\n", "latitude 47.4036\n", "longitude 8.49863\n", "email zrogge@karz.com\n", "Name: 68, dtype: object" ] }, "execution_count": 75, "metadata": {}, "output_type": "execute_result" } ], "source": [ "helperData.iloc[standard_recommendation[0][1]]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### create a map of the distances for our top 3 recommendations" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now wouldn't it be nice to see the top 3 recommendations visually on our folium map?\n", "\n", "Credit for the following two nice functions, that draw lines with arrows on folium maps, goes to [Bob Haffner](https://medium.com/@bobhaffner/folium-lines-with-arrows-25a0fe88e4e)." ] }, { "cell_type": "code", "execution_count": 76, "metadata": {}, "outputs": [], "source": [ "def get_arrows(locations, color='blue', size=6, n_arrows=3):\n", " \n", " '''\n", " Get a list of correctly placed and rotated \n", " arrows/markers to be plotted\n", " \n", " Parameters\n", " locations : list of lists of lat lons that represent the \n", " start and end of the line. \n", " eg [[41.1132, -96.1993],[41.3810, -95.8021]]\n", " arrow_color : default is 'blue'\n", " size : default is 6\n", " n_arrows : number of arrows to create. default is 3\n", " Return\n", " list of arrows/markers\n", " '''\n", " \n", " Point = namedtuple('Point', field_names=['lat', 'lon'])\n", " \n", " # creating point from our Point named tuple\n", " p1 = Point(locations[0][0], locations[0][1])\n", " p2 = Point(locations[1][0], locations[1][1])\n", " \n", " # getting the rotation needed for our marker. \n", " # Subtracting 90 to account for the marker's orientation\n", " # of due East(get_bearing returns North)\n", " rotation = get_bearing(p1, p2) - 90\n", " \n", " # get an evenly space list of lats and lons for our arrows\n", " # note that I'm discarding the first and last for aesthetics\n", " # as I'm using markers to denote the start and end\n", " arrow_lats = np.linspace(p1.lat, p2.lat, n_arrows + 2)[1:n_arrows+1]\n", " arrow_lons = np.linspace(p1.lon, p2.lon, n_arrows + 2)[1:n_arrows+1]\n", " \n", " arrows = []\n", " \n", " #creating each \"arrow\" and appending them to our arrows list\n", " for points in zip(arrow_lats, arrow_lons):\n", " arrows.append(folium.RegularPolygonMarker(location=points, \n", " fill_color=color, number_of_sides=3, \n", " radius=size, rotation=rotation))\n", " return arrows" ] }, { "cell_type": "code", "execution_count": 77, "metadata": {}, "outputs": [], "source": [ "def get_bearing(p1, p2):\n", " \n", " '''\n", " Returns compass bearing from p1 to p2\n", " \n", " Parameters\n", " p1 : namedtuple with lat lon\n", " p2 : namedtuple with lat lon\n", " \n", " Return\n", " compass bearing of type float\n", " \n", " Notes\n", " Based on https://gist.github.com/jeromer/2005586\n", " '''\n", " \n", " long_diff = np.radians(p2.lon - p1.lon)\n", " \n", " lat1 = np.radians(p1.lat)\n", " lat2 = np.radians(p2.lat)\n", " \n", " x = np.sin(long_diff) * np.cos(lat2)\n", " y = (np.cos(lat1) * np.sin(lat2) \n", " - (np.sin(lat1) * np.cos(lat2) \n", " * np.cos(long_diff)))\n", " bearing = np.degrees(np.arctan2(x, y))\n", " \n", " # adjusting for compass bearing\n", " if bearing < 0:\n", " return bearing + 360\n", " return bearing" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Now we can draw the final map:**" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "standard case:" ] }, { "cell_type": "code", "execution_count": 78, "metadata": {}, "outputs": [], "source": [ "topHelper=helperData.iloc[standard_recommendation[0][1]]\n", "topVenue=venues.iloc[standard_recommendation[0][0]]\n", "secondHelper=helperData.iloc[standard_recommendation[1][1]]\n", "secondVenue=venues.iloc[standard_recommendation[1][0]]\n", "thirdHelper=helperData.iloc[standard_recommendation[2][1]]\n", "thirdVenue=venues.iloc[standard_recommendation[2][0]]\n", "\n", "p=[]\n", "\n", "p.append([topHelper['latitude'], topHelper['longitude'], topVenue['Venue Latitude'], topVenue['Venue Longitude'], 'green'])\n", "p.append([topVenue['Venue Latitude'], topVenue['Venue Longitude'], custLatitude, custLongitude, 'green'])\n", "p.append([custLatitude, custLongitude, topHelper['latitude'], topHelper['longitude'], 'green'])\n", "\n", "p.append([secondHelper['latitude'], secondHelper['longitude'], secondVenue['Venue Latitude'], secondVenue['Venue Longitude'], 'orange'])\n", "p.append([secondVenue['Venue Latitude'], secondVenue['Venue Longitude'], custLatitude, custLongitude, 'orange'])\n", "p.append([custLatitude, custLongitude, secondHelper['latitude'], secondHelper['longitude'], 'orange'])\n", "\n", "p.append([thirdHelper['latitude'], thirdHelper['longitude'], thirdVenue['Venue Latitude'], thirdVenue['Venue Longitude'], 'red'])\n", "p.append([thirdVenue['Venue Latitude'], thirdVenue['Venue Longitude'], custLatitude, custLongitude, 'red'])\n", "p.append([custLatitude, custLongitude, thirdHelper['latitude'], thirdHelper['longitude'], 'red'])" ] }, { "cell_type": "code", "execution_count": 79, "metadata": { "scrolled": false }, "outputs": [ { "data": { "text/html": [ "
" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "# draw base map with position markers\n", "folium_map=createRawMap(custLatitude, custLongitude, custAddr, helperData, venues, zoomStart=17)\n", "\n", "# add lines and arrows\n", "for pos in p:\n", " p1=[pos[0], pos[1]]\n", " p2=[pos[2], pos[3]]\n", " colorLine=pos[4]\n", " folium.PolyLine(locations=[p1, p2], color=colorLine).add_to(folium_map)\n", "\n", " arrows = get_arrows(locations=[p1, p2], color=colorLine, n_arrows=1)\n", " for arrow in arrows:\n", " arrow.add_to(folium_map)\n", " \n", "display(folium_map)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now this is really a funny coincidence. 2 of the \"helpers\" are living at \"Tertianum Residenz - Im Brühl\". This is a place where you can spend your retirement days.\n", "\n", "Maybe not the best location to recruit helpers, when the helpers belong to the risk group themselves.\n", "\n", "In real life, of course one would have to make reasonably sure, that the helpers are up for the task.\n", "\n", "Since this is only based on fake data extracted from OSINT sources, it's not relevant yet. But either way, this example once again shows that data cleaning is the most important process. Otherwise you'll have a lot of work for your model that ends producing in accordance to the garbage in garbage out principle.\n", "\n", "Here maybe it's the janitor and his 18 year old son, both living at the place and young and healthy?" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "### That was fun. Let's try to apply the code to a different customer.\n", "\n", "Once you have arrived here, you can easily:\n", "1. plug in a different customer address (custAddr2) down below\n", "2. mark this cell\n", "3. jump to the [end of the notebook](#end) and hit shift and mark the last cell, then...\n", "3. hit run" ] }, { "cell_type": "code", "execution_count": 80, "metadata": {}, "outputs": [], "source": [ "custAddr_2 = 'Singlistrasse 11, 8049 Zurich' # <-- input address of choice here, then run rest of the notebook" ] }, { "cell_type": "code", "execution_count": 81, "metadata": { "scrolled": true }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "The geograpical coordinate of Singlistrasse 11, 8049 Zurich are 47.403521, 8.4947084.\n" ] } ], "source": [ "# convert address to location\n", "location_2 = geolocator.geocode(custAddr_2)\n", "custLatitude_2 = location_2.latitude\n", "custLongitude_2 = location_2.longitude\n", "print('The geograpical coordinate of {} are {}, {}.'.format(custAddr_2, custLatitude_2, custLongitude_2))" ] }, { "cell_type": "code", "execution_count": 82, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Searching venues for hungrycustomer1942@bluewin.ch\n" ] }, { "data": { "text/plain": [ "(9, 7)" ] }, "execution_count": 82, "metadata": {}, "output_type": "execute_result" } ], "source": [ "venues_2 = getNearbyVenues(names=['hungrycustomer1942@bluewin.ch'],\n", " latitudes=[custLatitude_2],\n", " longitudes=[custLongitude_2],\n", " radius=1000,\n", " categories='4d4b7105d754a06374d81259,4bf58dd8d48988d1f9941735,52f2ab2ebcbc57f1066b8b1c,50be8ee891d4fa8dcc7199a7,4bf58dd8d48988d10f951735,4bf58dd8d48988d1fd941735')\n", "venues_2.shape" ] }, { "cell_type": "code", "execution_count": 83, "metadata": {}, "outputs": [], "source": [ "# initialize variables\n", "numH_2 = helperData.shape[0] # number of helpers\n", "numS_2 = venues_2.shape[0] # number of shops\n", "numC_2 = 1 # test with only 1 customer\n", "HtoS_2 = np.zeros([numS_2, numH_2]) # 2-dim matrix, helpers in columns, shops in rows\n", "StoC_2 = np.zeros([numS_2,1]) # 1-dim matrix, shops in rows\n", "CtoH_2 = np.zeros([1, numH_2]) # 1-dim matrix, helpers in columns\n", "totDist_2 = np.zeros([numS_2, numH_2]) #2-dim matrix, helpers in columns, shops in rows" ] }, { "cell_type": "code", "execution_count": 84, "metadata": {}, "outputs": [], "source": [ "# calculate distances from all helpers to all shops (in meters rounded to whole numbers)\n", "for hId, helper in helperData.iterrows():\n", " for sId, shop in venues_2.iterrows():\n", " tmpCord1 = (helper['latitude'], helper['longitude'])\n", " tmpCord2 = (shop['Venue Latitude'], shop['Venue Longitude'])\n", " HtoS_2[sId][hId]=round(geopy.distance.distance(tmpCord1, tmpCord2).m,0)" ] }, { "cell_type": "code", "execution_count": 85, "metadata": {}, "outputs": [], "source": [ "# calculate distances from all shops to our customer (in meters rounded to whole numbers)\n", "tmpCord1 = (custLatitude_2, custLongitude_2)\n", "for sId, shop in venues_2.iterrows():\n", " tmpCord2 = (shop['Venue Latitude'], shop['Venue Longitude'])\n", " StoC_2[sId][0]=round(geopy.distance.distance(tmpCord1, tmpCord2).m,0)" ] }, { "cell_type": "code", "execution_count": 86, "metadata": {}, "outputs": [], "source": [ "# calculate distances from our customer to all helpers (in meters rounded to whole numbers)\n", "tmpCord2 = (custLatitude_2, custLongitude_2)\n", "for hId, helper in helperData.iterrows():\n", " tmpCord1 = (helper['latitude'], helper['longitude'])\n", " CtoH_2[0][hId]=round(geopy.distance.distance(tmpCord1, tmpCord2).m,0) " ] }, { "cell_type": "code", "execution_count": 87, "metadata": {}, "outputs": [], "source": [ "# calculate total distance\n", "totDist_2=HtoS_2+StoC_2+CtoH_2" ] }, { "cell_type": "code", "execution_count": 88, "metadata": { "scrolled": true }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "1 / shop: Argentina Steakhouse / helper: anneli05@aol.de\n", "2 / shop: Maharani / helper: kuschkarl-august@holzapfel.de\n", "3 / shop: Argentina Steakhouse / helper: olena23@web.de\n", "4 / shop: Osteria da Biagio / helper: kuschkarl-august@holzapfel.de\n", "5 / shop: Argentina Steakhouse / helper: iris38@beyer.de\n", "6 / shop: Osteria da Biagio / helper: metin34@aol.de\n", "7 / shop: Osteria da Biagio / helper: ehoerle@gmail.com\n", "8 / shop: Osteria da Biagio / helper: josef95@boerner.com\n", "9 / shop: Osteria da Biagio / helper: ditschlerinmehdi@googlemail.com\n", "10 / shop: Osteria da Biagio / helper: evelyneloeffler@paertzelt.de\n" ] } ], "source": [ "standard_score2 = normPerfectToZero(calcScores(HtoS_2, StoC_2, CtoH_2, totDist_2, standard_weights, hTrolley))\n", "standard_recommendation2=findKMinFromNp(standard_score2,10)\n", "printRecommendation(standard_recommendation2, venues_2)" ] }, { "cell_type": "code", "execution_count": 89, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "1 / shop: Argentina Steakhouse / helper: anneli05@aol.de\n", "2 / shop: Argentina Steakhouse / helper: olena23@web.de\n", "3 / shop: Argentina Steakhouse / helper: iris38@beyer.de\n", "4 / shop: Osteria da Biagio / helper: kuschkarl-august@holzapfel.de\n", "5 / shop: Osteria da Biagio / helper: metin34@aol.de\n", "6 / shop: Osteria da Biagio / helper: ehoerle@gmail.com\n", "7 / shop: Osteria da Biagio / helper: josef95@boerner.com\n", "8 / shop: Osteria da Biagio / helper: ditschlerinmehdi@googlemail.com\n", "9 / shop: Osteria da Biagio / helper: evelyneloeffler@paertzelt.de\n", "10 / shop: Osteria da Biagio / helper: walburgajacob@gmx.de\n" ] } ], "source": [ "hot_heavy_frigo_score2 = normPerfectToZero(calcScores(HtoS_2, StoC_2, CtoH_2, totDist_2, hot_heavy_frigo_weights, hTrolley))\n", "hot_heavy_frigo_recommendation2=findKMinFromNp(hot_heavy_frigo_score2,10)\n", "printRecommendation(hot_heavy_frigo_recommendation2, venues_2)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "To display the distances on the map, we need to extract the positions of our top 3 helper shop combinations:" ] }, { "cell_type": "code", "execution_count": 90, "metadata": {}, "outputs": [], "source": [ "topHelper=helperData.iloc[standard_recommendation2[0][1]]\n", "topVenue=venues_2.iloc[standard_recommendation2[0][0]]\n", "secondHelper=helperData.iloc[standard_recommendation2[1][1]]\n", "secondVenue=venues_2.iloc[standard_recommendation2[1][0]]\n", "thirdHelper=helperData.iloc[standard_recommendation2[2][1]]\n", "thirdVenue=venues_2.iloc[standard_recommendation2[2][0]]\n", "custLat=custLatitude_2\n", "custLong=custLongitude_2\n", "\n", "p=[]\n", "\n", "p.append([topHelper['latitude'], topHelper['longitude'], topVenue['Venue Latitude'], topVenue['Venue Longitude'], 'green'])\n", "p.append([topVenue['Venue Latitude'], topVenue['Venue Longitude'], custLat, custLong, 'green'])\n", "p.append([custLat, custLong, topHelper['latitude'], topHelper['longitude'], 'green'])\n", "\n", "p.append([secondHelper['latitude'], secondHelper['longitude'], secondVenue['Venue Latitude'], secondVenue['Venue Longitude'], 'orange'])\n", "p.append([secondVenue['Venue Latitude'], secondVenue['Venue Longitude'], custLat, custLong, 'orange'])\n", "p.append([custLat, custLong, secondHelper['latitude'], secondHelper['longitude'], 'orange'])\n", "\n", "p.append([thirdHelper['latitude'], thirdHelper['longitude'], thirdVenue['Venue Latitude'], thirdVenue['Venue Longitude'], 'red'])\n", "p.append([thirdVenue['Venue Latitude'], thirdVenue['Venue Longitude'], custLat, custLong, 'red'])\n", "p.append([custLat, custLong, thirdHelper['latitude'], thirdHelper['longitude'], 'red'])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Now we can draw the final map:**" ] }, { "cell_type": "code", "execution_count": 91, "metadata": { "scrolled": true }, "outputs": [ { "data": { "text/html": [ "
" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "# draw base map with position markers\n", "folium_map=createRawMap(custLatitude_2, custLongitude_2, custAddr_2, helperData, venues_2, zoomStart=16)\n", "\n", "# add lines and arrows\n", "for pos in p:\n", " p1=[pos[0], pos[1]]\n", " p2=[pos[2], pos[3]]\n", " colorLine=pos[4]\n", " folium.PolyLine(locations=[p1, p2], color=colorLine).add_to(folium_map)\n", "\n", " arrows = get_arrows(locations=[p1, p2], color=colorLine, n_arrows=1)\n", " for arrow in arrows:\n", " arrow.add_to(folium_map)\n", " \n", "display(folium_map)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Very nice!\n", "\n", "If you want to try with a different customer adress, [simply jump here and have some fun](#tldr2).\n", "" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### **For discussion and conclusion you can [have a look at the full report here](https://github.com/Funisher-code/Coursera_Capstone/blob/master/report/COVID-19_Safe_And_Efficient_Food_Deliveries.md).**" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "----\n", "\n", "### And now?\n", "\n", "The goal was to build a simple PoC of a recommender system.\n", "\n", "Goal achieved.\n", "\n", "But of course there is lots of room for improvement!" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**I hope you enjoyed following the code as much as I did piecing it together.**\n", "\n", "If you have suggestions or remarks, you can reach me [here](https://www.linkedin.com/in/markus-maechler/).\n", "\n", "----" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.7.6" } }, "nbformat": 4, "nbformat_minor": 4 }