{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "## Two-layer Territory Management. Optimizing territories considering clients and sales reps\n", "\n", "In territory management, a territory is a customer group or geographic area over which either an individual salesperson or a sales team has responsibility. These territories are usually defined based on geography, sales potential, number of clients or a combination of these factors.\n", "\n", "The main complexity in territory management is to create areas that are balanced with regards to more than one factor that usually behave very differently. There is no one-size-fits-all solution, and if the balance is off, sales management is likely to leave someone within their organization unhappy or leave money on the table. This is why it is very important to identify and understand all the components and requirements of your use case to apply the most appropriate technique.\n", "\n", "We can differentiate between two main use cases: when the location of sales reps is important (usually because they have to travel to visit their clients) and when it is not (travel rarely occurs). The first case is clearly more complex than the latter.\n", "\n", "In this notebook we will work on a Territory Management problem in which clusters need to be balanced in terms of number of clients and distance to their sales rep. This way, we will have two layers of data: one consisting of the client locations and the other one with the sales rep locations. We will prove the value Spatial Data Science techniques by showing their additional value compared to traditional techniques." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Use case description\n", "\n", "A pharma lab is interested in balancing their sales territories in the state of Texas based on the number of current and potential clients per territory. They would also like these clusters to be close to sales reps locations since they usually have to travel to visit them.\n", "\n", "Their clients are mainly offices and clinics of medical doctors.\n", "\n", "They are interested in creating 5 balanced territories taking into account number of clients and distance traveled.\n", "\n", "We will use the following three datasets from [CARTO's Data Observatory](https://carto.com/spatial-data-catalog/):\n", "- Points of Interest (POIs). In particular, office and clinic of medical doctors POIs. We will use [Pitney Bowes POI-Consumer dataset](https://carto.com/spatial-data-catalog/browser/dataset/pb_consumer_po_62cddc04/).\n", "- Texas boundary geometry. We'll use [Who's on First GeoJSON - Global dataset](https://carto.com/spatial-data-catalog/browser/geography/wof_geojson_4e78587c/).\n", "- Sales rep locations. This dataset is provided by the client.\n", "\n", "*Note* the POI dataset is premium and a subscription is needed to access this data." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 0. Setup\n", "\n", "We'll start by importing all packages we'll use." ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "import geopandas as gpd\n", "import matplotlib.pyplot as plt\n", "import networkx as nx\n", "import numpy as np\n", "import pandas as pd\n", "import seaborn as sns\n", "\n", "from cartoframes.auth import set_default_credentials\n", "from cartoframes.data.observatory import *\n", "from cartoframes.viz import *\n", "from h3 import h3\n", "from libpysal.weights import Rook\n", "from ortools.graph import pywrapgraph\n", "from scipy.spatial.distance import cdist\n", "from shapely import wkt\n", "from shapely.geometry import mapping, Polygon\n", "from sklearn.cluster import KMeans\n", "from splot.libpysal import plot_spatial_weights\n", "\n", "pd.set_option('display.max_columns', None)\n", "plt.rc('axes', titlesize='large')\n", "plt.rc('xtick', labelsize='large')\n", "plt.rc('ytick', labelsize='large')\n", "sns.set_style('whitegrid')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In order to be able to use the Data Observatory via CARTOframes, you need to set your CARTO account credentials first.\n", "\n", "Please, visit the [Authentication guide](https://carto.com/developers/cartoframes/guides/Authentication/) for further detail." ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "set_default_credentials('creds.json')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### 0.1. Functions" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The following function creates an [H3](https://eng.uber.com/h3/) polyfill of the polygon and at the resolution indicated." ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [], "source": [ "def create_h3_grid(polygon, resolution=8):\n", " hex_id_list = list(h3.polyfill(geojson = mapping(polygon), res = resolution, geo_json_conformant=True))\n", " hexagon_list = list(map(lambda x : Polygon(h3.h3_to_geo_boundary(h=x, geo_json=True)), hex_id_list))\n", " grid = pd.DataFrame(data={'hex_id':hex_id_list, 'geometry':hexagon_list})\n", " grid = gpd.GeoDataFrame(grid, crs='epsg:4326')\n", " return grid" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The function below is used throughout the analysis to check is clusters are balanced based on different metrics.\n", "\n", "The function arguments are:\n", "- `cluster_names` so that we can provide descriptive names to clusters\n", "- `areas_df` is the GeoDataFrame\n", "- `groupby` is the column with the cluster to which each cell belongs to\n", "- `**kaggregations` for the different metrics we'd like to calculate" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [], "source": [ "def plot_cluster_comparison(cluster_names, areas_df, groupby, **kaggregations):\n", " areas_df_g = areas_df.groupby(groupby).agg(kaggregations).reset_index()\n", "\n", " n_plots = len(kaggregations)\n", " fig, axs = plt.subplots(1, n_plots, figsize=(9 + 3*n_plots,4))\n", " if n_plots == 1:\n", " axs = [axs]\n", " \n", " for i in range(n_plots):\n", " sns.barplot(y=groupby, x=list(kaggregations.keys())[i], data=areas_df_g, order=cluster_names, \n", " palette=['#7F3C8D','#11A579','#3969AC','#F2B701','#E73F74'], ax=axs[i])\n", " axs[i].set_xlabel(list(kaggregations.keys())[i], fontsize=13)\n", " axs[i].set_ylabel('Sales rep locations', fontsize=13)\n", " \n", " fig.tight_layout()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 1. Download and visualize data\n", "\n", "Next, we will download the data described in the usecase using [CARTOframes](https://carto.com/developers/cartoframes/).\n", "\n", "*Note* in this notebook some prior knowledge on how to explore and download data from the [Data Observatory](https://carto.com/spatial-data-catalog/) is assumed. If this is your first time exploring and downloading data from the [Data Observatory](https://carto.com/spatial-data-catalog/), take a look at [CARTOframes Guides](https://carto.com/developers/cartoframes/guides/Introduction/) and the [Data Observatory examples](https://carto.com/developers/cartoframes/guides/Data-Observatory/) and **discover how easy it is to get started!**" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### 1.1 Texas boundary geometry\n", "\n", "We are interested in the geometry of the state of Texas. We'll download it from [Who's on First GeoJSON - Global dataset](https://carto.com/spatial-data-catalog/browser/geography/wof_geojson_4e78587c/)." ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "{'slug': 'wof_geojson_4e78587c',\n", " 'name': 'GeoJSON - Global',\n", " 'description': \"The main table in Who's On First. Holds all the relevant information for a place in the 'body' JSON field.\",\n", " 'country_id': 'glo',\n", " 'provider_id': 'whos_on_first',\n", " 'geom_type': 'MULTIPLE',\n", " 'update_frequency': None,\n", " 'is_public_data': True,\n", " 'lang': 'eng',\n", " 'version': '20190520',\n", " 'provider_name': \"Who's On First\",\n", " 'id': 'carto-do-public-data.whos_on_first.geography_glo_geojson_20190520'}" ] }, "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ "wof_grographies = Geography.get('wof_geojson_4e78587c')\n", "wof_grographies.to_dict()" ] }, { "cell_type": "code", "execution_count": 6, "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", "
geoididbodynamecountryparent_idis_currentplacetypegeometry_typebboxgeomlastmodifiedlastmodified_timestamp
08568875385688753{\"id\": 85688753, \"type\": \"Feature\", \"propertie...TexasUS856337931regionPolygonPOLYGON((-93.508039 25.837164, -93.508039 36.5...POLYGON ((-103.06466 32.95910, -103.06442 32.0...15554467282019-04-16 20:32:08+00:00
\n", "
" ], "text/plain": [ " geoid id body \\\n", "0 85688753 85688753 {\"id\": 85688753, \"type\": \"Feature\", \"propertie... \n", "\n", " name country parent_id is_current placetype geometry_type \\\n", "0 Texas US 85633793 1 region Polygon \n", "\n", " bbox \\\n", "0 POLYGON((-93.508039 25.837164, -93.508039 36.5... \n", "\n", " geom lastmodified \\\n", "0 POLYGON ((-103.06466 32.95910, -103.06442 32.0... 1555446728 \n", "\n", " lastmodified_timestamp \n", "0 2019-04-16 20:32:08+00:00 " ] }, "execution_count": 6, "metadata": {}, "output_type": "execute_result" } ], "source": [ "state_name = 'Texas'\n", "country_code = 'US'\n", "placetype = 'region'\n", "\n", "sql_query = f\"\"\"SELECT * \n", " FROM $geography$ \n", " WHERE name = '{state_name}' AND \n", " country = '{country_code}' AND \n", " placetype='{placetype}'\"\"\"\n", "\n", "tx_boundary = wof_grographies.to_dataframe(sql_query=sql_query)\n", "tx_boundary.crs='epsg:4326'\n", "tx_boundary['geom'] = tx_boundary.simplify(0.01)\n", "tx_boundary" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### 1.2. Client locations\n", "\n", "We'll download all POIs in Texas classified as \"OFFICES AND CLINICS OF MEDICAL DOCTORS\" from [Pitney Bowes POI-Consumer dataset](https://carto.com/spatial-data-catalog/browser/dataset/pb_consumer_po_62cddc04/).\n", "\n", "*Note* this is a premium dataset and a subscription is required." ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [], "source": [ "poi_dataset = Dataset.get('pb_consumer_po_62cddc04')" ] }, { "cell_type": "code", "execution_count": 8, "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", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
geoiddo_datenamebrandnamepb_idtrade_namefranchise_nameiso3areaname4areaname3areaname2areaname1stabbpostcodeformattedaddressmainaddresslineaddresslastlinelongitudelatitudegeoresultconfidence_codecountry_access_codetel_numfaxnumemailhttpopen_24hbusiness_linesic1sic2sic8sic8_descriptionalt_industry_codemicodetrade_divisiongroupclasssub_classemployee_hereemployee_countyear_startsales_volume_localsales_volume_us_dollarscurrency_codeagent_codelegal_status_codestatus_codesubsidiary_indicatorparent_business_nameparent_addressparent_street_addressparent_areaname3parent_areaname1parent_countryparent_postcodedomestic_ultimate_business_namedomestic_ultimate_addressdomestic_ultimate_street_addressdomestic_ultimate_areaname3domestic_ultimate_areaname1domestic_ultimate_postcodeglobal_ultimate_indicatorglobal_ultimate_business_nameglobal_ultimate_addressglobal_ultimate_street_addressglobal_ultimate_areaname3global_ultimate_areaname1global_ultimate_countryglobal_ultimate_postcodefamily_membershierarchy_codeticker_symbolexchange_namegeom
01128750296#-96.398536#32.4699342020-08-01SIMMONS & ASSOC SOUTH CENTRAL LLCNaN1128750296SIMMONS & ASSOCIATESNaNUSANaNSCURRYNaNTEXASTX75158-33049084 FM 2451, SCURRY, TX, 75158-33049084 FM 2451SCURRY, TX, 75158-3304-96.39853632.469934S8HPNTSCZAHIGH1.0(972) 452-8013NaNNaNWWW.SIMMONSINC.COMNaNOFFICES AND CLINICS MEDICAL DOCTORS,NSK8011.0NaN80110000OFFICES AND CLINICS OF MEDICAL DOCTORS621111.010238011DIVISION I. - SERVICESHEALTH SERVICESOFFICES AND CLINICS OF DOCTORS OF MEDICINEOFFICES AND CLINICS OF MEDICAL DOCTORS1.01.01997.0251657.0251657.020.0G3.00.00.0NaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNNaNNaNNaNNaNNaNNaNNaN0.00.0NaNNaNPOINT (-96.39854 32.46993)
11217171653#-96.858002#32.7154812020-08-01SOUTHWEST DALLAS ORTHOPEDIC ASSOCIATESNaN1217171653NaNNaNUSANaNDALLASNaNTEXASTX75224-30592909 S HAMPTON RD STE D121, DALLAS, TX, 75224-...2909 S HAMPTON RD STE D121DALLAS, TX, 75224-3059-96.85800232.715481S8HPNTSCZAHIGH1.0(214) 333-3741NaNNaNWWW.DALLASORTHO.COMNaNOFFICES AND CLINICS MEDICAL DOCTORS,NSK8011.0NaN80110514ORTHOPEDIC PHYSICIAN621111.010942514DIVISION I. - SERVICESHEALTH SERVICESOFFICES AND CLINICS OF DOCTORS OF MEDICINEOFFICES AND CLINICS OF MEDICAL DOCTORS6.06.01991.0620608.0620608.020.0G13.00.00.0NaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNNaNNaNNaNNaNNaNNaNNaN0.00.0NaNNaNPOINT (-96.85800 32.71548)
21123005494#-97.104542#32.9268252020-08-01S ROBERT HARLA DOPANaN1123005494GRAPEVINE DERMATOLOGYNaNUSANaNGRAPEVINENaNTEXASTX76051-86322321 IRA E WOODS AVE STE 180, GRAPEVINE, TX, 7...2321 IRA E WOODS AVE STE 180GRAPEVINE, TX, 76051-8632-97.10454232.926825S8HPNTSCZAHIGH1.0(817) 329-2263NaNNaNWWW.DERMDFW.COMNaNOFFICES AND CLINICS MEDICAL DOCTORS,NSK8011.0NaN80110503DERMATOLOGIST621111.010942503DIVISION I. - SERVICESHEALTH SERVICESOFFICES AND CLINICS OF DOCTORS OF MEDICINEOFFICES AND CLINICS OF MEDICAL DOCTORS13.013.01990.01345253.01345253.020.0G13.00.00.0NaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNNaNNaNNaNNaNNaNNaNNaN0.00.0NaNNaNPOINT (-97.10454 32.92683)
31221252299#-101.902112#33.5734532020-08-01CONSULTANTS IN INFECTIOUS DISEASES LLPNaN1221252299NaNNaNUSANaNLUBBOCKNaNTEXASTX79410-18044102 24TH ST STE 403, LUBBOCK, TX, 79410-18044102 24TH ST STE 403LUBBOCK, TX, 79410-1804-101.90211233.573453S8HPNTSCZAHIGH1.0(806) 725-7150NaNNaNWWW.COVMEDGROUP.ORGNaNOFFICES AND CLINICS MEDICAL DOCTORS,NSK8011.0NaN80110510INFECTIOUS DISEASE SPECIALIST, PHYSICIAN/SURGEON621111.010942510DIVISION I. - SERVICESHEALTH SERVICESOFFICES AND CLINICS OF DOCTORS OF MEDICINEOFFICES AND CLINICS OF MEDICAL DOCTORS22.022.01995.01058845.01058845.020.0G12.00.00.0NaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNNaNNaNNaNNaNNaNNaNNaN0.00.0NaNNaNPOINT (-101.90211 33.57345)
41217934291#-95.419502#29.1702832020-08-01SUZAN CARPENTERNaN1217934291CARPENTER, SU ZAN MDNaNUSANaNANGLETONNaNTEXASTX77515-58361113 E CEDAR ST, ANGLETON, TX, 77515-58361113 E CEDAR STANGLETON, TX, 77515-5836-95.41950229.170283S8HPNTSCZAHIGH1.0(979) 849-5703NaNNaNNaNNaNOFFICES AND CLINICS MEDICAL DOCTORS,NSK8011.0NaN80119901GENERAL AND FAMILY PRACTICE, PHYSICIAN/SURGEON621111.010230302DIVISION I. - SERVICESHEALTH SERVICESOFFICES AND CLINICS OF DOCTORS OF MEDICINEOFFICES AND CLINICS OF MEDICAL DOCTORS3.03.01990.0257133.0257133.020.0G13.00.00.0NaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNNaNNaNNaNNaNNaNNaNNaN0.00.0NaNNaNPOINT (-95.41950 29.17028)
\n", "
" ], "text/plain": [ " geoid do_date \\\n", "0 1128750296#-96.398536#32.469934 2020-08-01 \n", "1 1217171653#-96.858002#32.715481 2020-08-01 \n", "2 1123005494#-97.104542#32.926825 2020-08-01 \n", "3 1221252299#-101.902112#33.573453 2020-08-01 \n", "4 1217934291#-95.419502#29.170283 2020-08-01 \n", "\n", " name brandname pb_id \\\n", "0 SIMMONS & ASSOC SOUTH CENTRAL LLC NaN 1128750296 \n", "1 SOUTHWEST DALLAS ORTHOPEDIC ASSOCIATES NaN 1217171653 \n", "2 S ROBERT HARLA DOPA NaN 1123005494 \n", "3 CONSULTANTS IN INFECTIOUS DISEASES LLP NaN 1221252299 \n", "4 SUZAN CARPENTER NaN 1217934291 \n", "\n", " trade_name franchise_name iso3 areaname4 areaname3 areaname2 \\\n", "0 SIMMONS & ASSOCIATES NaN USA NaN SCURRY NaN \n", "1 NaN NaN USA NaN DALLAS NaN \n", "2 GRAPEVINE DERMATOLOGY NaN USA NaN GRAPEVINE NaN \n", "3 NaN NaN USA NaN LUBBOCK NaN \n", "4 CARPENTER, SU ZAN MD NaN USA NaN ANGLETON NaN \n", "\n", " areaname1 stabb postcode \\\n", "0 TEXAS TX 75158-3304 \n", "1 TEXAS TX 75224-3059 \n", "2 TEXAS TX 76051-8632 \n", "3 TEXAS TX 79410-1804 \n", "4 TEXAS TX 77515-5836 \n", "\n", " formattedaddress \\\n", "0 9084 FM 2451, SCURRY, TX, 75158-3304 \n", "1 2909 S HAMPTON RD STE D121, DALLAS, TX, 75224-... \n", "2 2321 IRA E WOODS AVE STE 180, GRAPEVINE, TX, 7... \n", "3 4102 24TH ST STE 403, LUBBOCK, TX, 79410-1804 \n", "4 1113 E CEDAR ST, ANGLETON, TX, 77515-5836 \n", "\n", " mainaddressline addresslastline longitude \\\n", "0 9084 FM 2451 SCURRY, TX, 75158-3304 -96.398536 \n", "1 2909 S HAMPTON RD STE D121 DALLAS, TX, 75224-3059 -96.858002 \n", "2 2321 IRA E WOODS AVE STE 180 GRAPEVINE, TX, 76051-8632 -97.104542 \n", "3 4102 24TH ST STE 403 LUBBOCK, TX, 79410-1804 -101.902112 \n", "4 1113 E CEDAR ST ANGLETON, TX, 77515-5836 -95.419502 \n", "\n", " latitude georesult confidence_code country_access_code tel_num \\\n", "0 32.469934 S8HPNTSCZA HIGH 1.0 (972) 452-8013 \n", "1 32.715481 S8HPNTSCZA HIGH 1.0 (214) 333-3741 \n", "2 32.926825 S8HPNTSCZA HIGH 1.0 (817) 329-2263 \n", "3 33.573453 S8HPNTSCZA HIGH 1.0 (806) 725-7150 \n", "4 29.170283 S8HPNTSCZA HIGH 1.0 (979) 849-5703 \n", "\n", " faxnum email http open_24h \\\n", "0 NaN NaN WWW.SIMMONSINC.COM NaN \n", "1 NaN NaN WWW.DALLASORTHO.COM NaN \n", "2 NaN NaN WWW.DERMDFW.COM NaN \n", "3 NaN NaN WWW.COVMEDGROUP.ORG NaN \n", "4 NaN NaN NaN NaN \n", "\n", " business_line sic1 sic2 sic8 \\\n", "0 OFFICES AND CLINICS MEDICAL DOCTORS,NSK 8011.0 NaN 80110000 \n", "1 OFFICES AND CLINICS MEDICAL DOCTORS,NSK 8011.0 NaN 80110514 \n", "2 OFFICES AND CLINICS MEDICAL DOCTORS,NSK 8011.0 NaN 80110503 \n", "3 OFFICES AND CLINICS MEDICAL DOCTORS,NSK 8011.0 NaN 80110510 \n", "4 OFFICES AND CLINICS MEDICAL DOCTORS,NSK 8011.0 NaN 80119901 \n", "\n", " sic8_description alt_industry_code \\\n", "0 OFFICES AND CLINICS OF MEDICAL DOCTORS 621111.0 \n", "1 ORTHOPEDIC PHYSICIAN 621111.0 \n", "2 DERMATOLOGIST 621111.0 \n", "3 INFECTIOUS DISEASE SPECIALIST, PHYSICIAN/SURGEON 621111.0 \n", "4 GENERAL AND FAMILY PRACTICE, PHYSICIAN/SURGEON 621111.0 \n", "\n", " micode trade_division group \\\n", "0 10238011 DIVISION I. - SERVICES HEALTH SERVICES \n", "1 10942514 DIVISION I. - SERVICES HEALTH SERVICES \n", "2 10942503 DIVISION I. - SERVICES HEALTH SERVICES \n", "3 10942510 DIVISION I. - SERVICES HEALTH SERVICES \n", "4 10230302 DIVISION I. - SERVICES HEALTH SERVICES \n", "\n", " class \\\n", "0 OFFICES AND CLINICS OF DOCTORS OF MEDICINE \n", "1 OFFICES AND CLINICS OF DOCTORS OF MEDICINE \n", "2 OFFICES AND CLINICS OF DOCTORS OF MEDICINE \n", "3 OFFICES AND CLINICS OF DOCTORS OF MEDICINE \n", "4 OFFICES AND CLINICS OF DOCTORS OF MEDICINE \n", "\n", " sub_class employee_here employee_count \\\n", "0 OFFICES AND CLINICS OF MEDICAL DOCTORS 1.0 1.0 \n", "1 OFFICES AND CLINICS OF MEDICAL DOCTORS 6.0 6.0 \n", "2 OFFICES AND CLINICS OF MEDICAL DOCTORS 13.0 13.0 \n", "3 OFFICES AND CLINICS OF MEDICAL DOCTORS 22.0 22.0 \n", "4 OFFICES AND CLINICS OF MEDICAL DOCTORS 3.0 3.0 \n", "\n", " year_start sales_volume_local sales_volume_us_dollars currency_code \\\n", "0 1997.0 251657.0 251657.0 20.0 \n", "1 1991.0 620608.0 620608.0 20.0 \n", "2 1990.0 1345253.0 1345253.0 20.0 \n", "3 1995.0 1058845.0 1058845.0 20.0 \n", "4 1990.0 257133.0 257133.0 20.0 \n", "\n", " agent_code legal_status_code status_code subsidiary_indicator \\\n", "0 G 3.0 0.0 0.0 \n", "1 G 13.0 0.0 0.0 \n", "2 G 13.0 0.0 0.0 \n", "3 G 12.0 0.0 0.0 \n", "4 G 13.0 0.0 0.0 \n", "\n", " parent_business_name parent_address parent_street_address parent_areaname3 \\\n", "0 NaN NaN NaN NaN \n", "1 NaN NaN NaN NaN \n", "2 NaN NaN NaN NaN \n", "3 NaN NaN NaN NaN \n", "4 NaN NaN NaN NaN \n", "\n", " parent_areaname1 parent_country parent_postcode \\\n", "0 NaN NaN NaN \n", "1 NaN NaN NaN \n", "2 NaN NaN NaN \n", "3 NaN NaN NaN \n", "4 NaN NaN NaN \n", "\n", " domestic_ultimate_business_name domestic_ultimate_address \\\n", "0 NaN NaN \n", "1 NaN NaN \n", "2 NaN NaN \n", "3 NaN NaN \n", "4 NaN NaN \n", "\n", " domestic_ultimate_street_address domestic_ultimate_areaname3 \\\n", "0 NaN NaN \n", "1 NaN NaN \n", "2 NaN NaN \n", "3 NaN NaN \n", "4 NaN NaN \n", "\n", " domestic_ultimate_areaname1 domestic_ultimate_postcode \\\n", "0 NaN NaN \n", "1 NaN NaN \n", "2 NaN NaN \n", "3 NaN NaN \n", "4 NaN NaN \n", "\n", " global_ultimate_indicator global_ultimate_business_name \\\n", "0 N NaN \n", "1 N NaN \n", "2 N NaN \n", "3 N NaN \n", "4 N NaN \n", "\n", " global_ultimate_address global_ultimate_street_address \\\n", "0 NaN NaN \n", "1 NaN NaN \n", "2 NaN NaN \n", "3 NaN NaN \n", "4 NaN NaN \n", "\n", " global_ultimate_areaname3 global_ultimate_areaname1 global_ultimate_country \\\n", "0 NaN NaN NaN \n", "1 NaN NaN NaN \n", "2 NaN NaN NaN \n", "3 NaN NaN NaN \n", "4 NaN NaN NaN \n", "\n", " global_ultimate_postcode family_members hierarchy_code ticker_symbol \\\n", "0 NaN 0.0 0.0 NaN \n", "1 NaN 0.0 0.0 NaN \n", "2 NaN 0.0 0.0 NaN \n", "3 NaN 0.0 0.0 NaN \n", "4 NaN 0.0 0.0 NaN \n", "\n", " exchange_name geom \n", "0 NaN POINT (-96.39854 32.46993) \n", "1 NaN POINT (-96.85800 32.71548) \n", "2 NaN POINT (-97.10454 32.92683) \n", "3 NaN POINT (-101.90211 33.57345) \n", "4 NaN POINT (-95.41950 29.17028) " ] }, "execution_count": 8, "metadata": {}, "output_type": "execute_result" } ], "source": [ "sql_query = \"\"\"\n", " SELECT * except(do_label) FROM $dataset$ \n", " WHERE SUB_CLASS = 'OFFICES AND CLINICS OF MEDICAL DOCTORS' \n", " AND STABB = 'TX'\n", " AND CAST(do_date AS date) >= (SELECT MAX(CAST(do_date AS date)) from $dataset$)\n", "\"\"\"\n", "pois = poi_dataset.to_dataframe(sql_query=sql_query)\n", "pois.columns = list(map(str.lower, pois.columns))\n", "pois.crs='epsg:4326'\n", "pois.head()" ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "(59554, 74)" ] }, "execution_count": 9, "metadata": {}, "output_type": "execute_result" } ], "source": [ "pois.shape" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### 1.3. Sales reps" ] }, { "cell_type": "code", "execution_count": 11, "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", "
idcitystate_namelatlongeometry
00DallasTexas32.725728-96.844694POINT (-96.84469 32.72573)
11AustinTexas30.255450-97.729611POINT (-97.72961 30.25545)
22San AntonioTexas29.424510-98.524053POINT (-98.52405 29.42451)
33HoustonTexas29.778360-95.332154POINT (-95.33215 29.77836)
44El PasoTexas31.795306-106.456893POINT (-106.45689 31.79531)
\n", "
" ], "text/plain": [ " id city state_name lat lon \\\n", "0 0 Dallas Texas 32.725728 -96.844694 \n", "1 1 Austin Texas 30.255450 -97.729611 \n", "2 2 San Antonio Texas 29.424510 -98.524053 \n", "3 3 Houston Texas 29.778360 -95.332154 \n", "4 4 El Paso Texas 31.795306 -106.456893 \n", "\n", " geometry \n", "0 POINT (-96.84469 32.72573) \n", "1 POINT (-97.72961 30.25545) \n", "2 POINT (-98.52405 29.42451) \n", "3 POINT (-95.33215 29.77836) \n", "4 POINT (-106.45689 31.79531) " ] }, "execution_count": 11, "metadata": {}, "output_type": "execute_result" } ], "source": [ "sales_reps = pd.read_csv('data/sales_reps.csv')\n", "sales_reps['geometry'] = gpd.points_from_xy(sales_reps['lon'], sales_reps['lat'])\n", "sales_reps = gpd.GeoDataFrame(sales_reps, crs='epsg:4326')\n", "sales_reps" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### 1.4. Visualize data" ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [ { "data": { "text/html": [ "\n", "\n", "\n", " None\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", "\n", " \n", "\n", " \n", "\n", " \n", " \n", " \n", " \n", " \n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "\n", "\n", "\n", " Static map image\n", " \n", " \n", "
\n", "
\n", "
\n", " \n", " \n", "
\n", "
\n", " \n", "\n", "
\n", " \n", " \n", " \n", " \n", " \n", "
\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
\n", " \n", " \n", " \n", " \n", " \n", "
\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
\n", " \n", " \n", " \n", " \n", " \n", "
\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
\n", " \n", " \n", "
\n", "
\n", "
\n", "
\n", " \n", "
\n", "
\n", "
\n", "\n", " \n", "\n", "
\n", "
\n", " :\n", "
\n", " \n", " \n", "
\n", "
\n", "\n", "
\n", " StackTrace\n", "
    \n", "
    \n", "
    \n", "\n", "\n", "\n", "\n", "\n", "\">\n", "\n", "" ], "text/plain": [ "" ] }, "execution_count": 12, "metadata": {}, "output_type": "execute_result" } ], "source": [ "Map([Layer(tx_boundary,\n", " style=basic_style(opacity=0, stroke_color='#11A579', stroke_width=5),\n", " legends=basic_legend('Texas Boundary')),\n", " Layer(pois.sample(5000), \n", " style=basic_style(color='#F2B701', size=2, opacity=0.9, stroke_width=0),\n", " popup_hover=[popup_element('name', 'Client'),\n", " popup_element('employee_here', '# Employees')],\n", " legends=basic_legend('Client Locations')),\n", " Layer(sales_reps,\n", " style=basic_style(color='red', size=5, stroke_width=0),\n", " popup_hover=popup_element('city', 'City'),\n", " legends=basic_legend('Sales Rep Locations'))], \n", " basemap=basemaps.darkmatter)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 2. Discretize space. H3 grid\n", "\n", "A fundamental step in territory management is to discretize space. Territory management algorithms are computationally complex and hence it is crucial to leverage the spatial component to reduce complexity. We can do this by working at an aggregated level instead of considering each client location independently.\n", "\n", "We first need to identify the smallest spatial aggregation that makes sense for your business, our **geographic support**. This can be census block groups, zip codes or counties, or you can be interested in using a standard grid, in which case it would ideally be a hierarchical spatial index such as [Quadkey grid](https://docs.microsoft.com/en-us/azure/azure-maps/zoom-levels-and-tile-grid?tabs=csharp) or [H3 grid](https://eng.uber.com/h3/).\n", "\n", "In this notebook we will use an H3 grid of resolution 5. We can easily discretize space by performing a polyfill of the Texas boundary polygon. \n", "\n", "*Note* a buffer has been applied because H3 will fill the polygon with all hexagons of resolution 5 whose centroid lies within the polygon to be filled and we want to make sure the whole territory is covered." ] }, { "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", " \n", " \n", " \n", " \n", " \n", " \n", "
    hex_idgeometry
    08526d9d7fffffffPOLYGON ((-99.33557 32.87875, -99.26773 32.953...
    18526d9a3fffffffPOLYGON ((-98.71570 32.47235, -98.64788 32.546...
    28526dc1bfffffffPOLYGON ((-100.32151 34.39136, -100.25314 34.4...
    385446c77fffffffPOLYGON ((-94.98183 29.87978, -94.91454 29.952...
    485444db7fffffffPOLYGON ((-95.10421 32.87010, -95.03517 32.941...
    \n", "
    " ], "text/plain": [ " hex_id geometry\n", "0 8526d9d7fffffff POLYGON ((-99.33557 32.87875, -99.26773 32.953...\n", "1 8526d9a3fffffff POLYGON ((-98.71570 32.47235, -98.64788 32.546...\n", "2 8526dc1bfffffff POLYGON ((-100.32151 34.39136, -100.25314 34.4...\n", "3 85446c77fffffff POLYGON ((-94.98183 29.87978, -94.91454 29.952...\n", "4 85444db7fffffff POLYGON ((-95.10421 32.87010, -95.03517 32.941..." ] }, "execution_count": 13, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Buffer\n", "buffer = 1e4 # in meters\n", "tx_boundary['geometry_buffer'] = tx_boundary.to_crs('epsg:26914').buffer(buffer).to_crs('epsg:4326')\n", "\n", "grid = create_h3_grid(tx_boundary['geometry_buffer'].iloc[0], 5)\n", "grid.head()" ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [ { "data": { "text/html": [ "\n", "\n", "\n", " None\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", "\n", " \n", "\n", " \n", "\n", " \n", " \n", " \n", " \n", " \n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", "\n", "\n", "\n", " Static map image\n", " \n", " \n", "
    \n", "
    \n", "
    \n", " \n", " \n", "
    \n", "
    \n", " \n", "\n", "
    \n", " \n", " \n", " \n", " \n", " \n", "
    \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
    \n", " \n", " \n", " \n", " \n", " \n", "
    \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
    \n", " \n", " \n", "
    \n", "
    \n", "
    \n", "
    \n", " \n", "
    \n", "
    \n", "
    \n", "\n", " \n", "\n", "
    \n", "
    \n", " :\n", "
    \n", " \n", " \n", "
    \n", "
    \n", "\n", "
    \n", " StackTrace\n", "
      \n", "
      \n", "
      \n", "\n", "\n", "\n", "\n", "\n", "\">\n", "\n", "" ], "text/plain": [ "" ] }, "execution_count": 14, "metadata": {}, "output_type": "execute_result" } ], "source": [ "Map([Layer(grid,\n", " style=basic_style(opacity=0.75),\n", " legends=basic_legend('H3 grid')),\n", " Layer(tx_boundary,\n", " style=basic_style(opacity=0, stroke_color='#E73F74', stroke_width=5),\n", " legends=basic_legend('Texas Boundary'))])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### 2.1. Aggregate client info at the grid level.\n", "\n", "We will aggregate clients by:\n", "- Counting the number of clients per cell\n", "- Calculating the median rating of clients within eack cell" ] }, { "cell_type": "code", "execution_count": 15, "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", "
      hex_idgeometrypoi_countemployee_avg
      08526d9d7fffffffPOLYGON ((-99.33557 32.87875, -99.26773 32.953...00.000000
      18526d9a3fffffffPOLYGON ((-98.71570 32.47235, -98.64788 32.546...00.000000
      28526dc1bfffffffPOLYGON ((-100.32151 34.39136, -100.25314 34.4...00.000000
      385446c77fffffffPOLYGON ((-94.98183 29.87978, -94.91454 29.952...133.833333
      485444db7fffffffPOLYGON ((-95.10421 32.87010, -95.03517 32.941...13.000000
      \n", "
      " ], "text/plain": [ " hex_id geometry \\\n", "0 8526d9d7fffffff POLYGON ((-99.33557 32.87875, -99.26773 32.953... \n", "1 8526d9a3fffffff POLYGON ((-98.71570 32.47235, -98.64788 32.546... \n", "2 8526dc1bfffffff POLYGON ((-100.32151 34.39136, -100.25314 34.4... \n", "3 85446c77fffffff POLYGON ((-94.98183 29.87978, -94.91454 29.952... \n", "4 85444db7fffffff POLYGON ((-95.10421 32.87010, -95.03517 32.941... \n", "\n", " poi_count employee_avg \n", "0 0 0.000000 \n", "1 0 0.000000 \n", "2 0 0.000000 \n", "3 13 3.833333 \n", "4 1 3.000000 " ] }, "execution_count": 15, "metadata": {}, "output_type": "execute_result" } ], "source": [ "pois_g = gpd.sjoin(pois, grid, how='right').groupby('hex_id').agg({'geoid':'count', 'employee_here':'mean'}).\\\n", " reset_index().rename(columns={'geoid':'poi_count', 'employee_here':'employee_avg'})\n", "pois_g[['poi_count', 'employee_avg']] = pois_g[['poi_count', 'employee_avg']].fillna(0)\n", "areas = grid.merge(pois_g, on='hex_id')\n", "areas = gpd.GeoDataFrame(areas, crs='epsg:4326')\n", "areas.head()" ] }, { "cell_type": "code", "execution_count": 16, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "count 2584.000000\n", "mean 23.047214\n", "std 151.594386\n", "min 0.000000\n", "50% 0.000000\n", "75% 1.000000\n", "90% 14.000000\n", "97.5% 226.000000\n", "99% 571.870000\n", "max 3471.000000\n", "Name: poi_count, dtype: float64" ] }, "execution_count": 16, "metadata": {}, "output_type": "execute_result" } ], "source": [ "areas['poi_count'].describe(percentiles=[0.75, 0.9, 0.975, 0.99])" ] }, { "cell_type": "code", "execution_count": 17, "metadata": {}, "outputs": [ { "data": { "text/html": [ "\n", "\n", "\n", " None\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", "\n", " \n", "\n", " \n", "\n", " \n", " \n", " \n", " \n", " \n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", "\n", "\n", "\n", " Static map image\n", " \n", " \n", "
      \n", "
      \n", "
      \n", " \n", " \n", "
      \n", "
      \n", " \n", "\n", "
      \n", " \n", " \n", " \n", " \n", " \n", "
      \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
      \n", " \n", " \n", " \n", " \n", " \n", "
      \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
      \n", " \n", " \n", "
      \n", "
      \n", "
      \n", "
      \n", " \n", "
      \n", "
      \n", "
      \n", "\n", " \n", "\n", "
      \n", "
      \n", " :\n", "
      \n", " \n", " \n", "
      \n", "
      \n", "\n", "
      \n", " StackTrace\n", "
        \n", "
        \n", "
        \n", "\n", "\n", "\n", "\n", "\n", "\">\n", "\n", "" ], "text/plain": [ "" ] }, "execution_count": 17, "metadata": {}, "output_type": "execute_result" } ], "source": [ "breaks = [15, 200, 550]\n", "\n", "Map([Layer(areas[areas['poi_count'] > 0], \n", " style=color_bins_style('poi_count', breaks=breaks),\n", " legends=color_bins_legend('Number of Clients')),\n", " Layer(sales_reps, \n", " style=basic_style('#F2B701'), \n", " popup_hover=popup_element('city'),\n", " legends=basic_legend('Sales Reps'))])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 3. Territory Optimization\n", "\n", "Once we have our data aggregated, it's time to start working on building balanced territories.\n", "\n", "It is important to note that clients mainly concentrate around sales rep locations, and there are large areas with a very low density of clients. In addition, the sales rep in El Paso is very far from Eastern Texas, where most clients concentrate. This means it's going to be very challenging to balance distance and number of clients, especially for the sales rep in El Paso.\n", "\n", "We will explore two different techniques:\n", "- Closest distance.\n", "- [Minimum cost flow approach](https://en.wikipedia.org/wiki/Minimum-cost_flow_problem#:~:text=The%20minimum%2Dcost%20flow%20problem,flow%20through%20a%20flow%20network.). " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### 3.1. Data preparation\n", "\n", "For both techniques we need to calculate sales rep - client distance matrix.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Distance metric**\n", "\n", "*Note* different distance metrics can be used for calculating distance. Results change significantly wdepending on this metric.\n", " - `metric`: Distance metric. [Here](https://docs.scipy.org/doc/scipy/reference/generated/scipy.spatial.distance.cdist.html) you can find all available metrics." ] }, { "cell_type": "code", "execution_count": 18, "metadata": {}, "outputs": [], "source": [ "metric = 'cityblock' # 'euclidean'" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Representative point**\n", "\n", "In order to calculate distances, we need a reference location for each cell. Since we are using regular polygons, we can use the centroid. However, selecting the centroid is discouraged with non-convex geometries as they may have their centroid out of the polygon. If that's the case, the Shapely function `representative_point()` returns a point within the polygon." ] }, { "cell_type": "code", "execution_count": 19, "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", "
        hex_idgeometrypoi_countemployee_avgrep_point
        08526d9d7fffffffPOLYGON ((-99.33557 32.87875, -99.26773 32.953...00.000000POINT (-99.37959 32.96652)
        18526d9a3fffffffPOLYGON ((-98.71570 32.47235, -98.64788 32.546...00.000000POINT (-98.75897 32.56035)
        28526dc1bfffffffPOLYGON ((-100.32151 34.39136, -100.25314 34.4...00.000000POINT (-100.36707 34.47834)
        385446c77fffffffPOLYGON ((-94.98183 29.87978, -94.91454 29.952...133.833333POINT (-95.02049 29.96866)
        485444db7fffffffPOLYGON ((-95.10421 32.87010, -95.03517 32.941...13.000000POINT (-95.14402 32.95831)
        \n", "
        " ], "text/plain": [ " hex_id geometry \\\n", "0 8526d9d7fffffff POLYGON ((-99.33557 32.87875, -99.26773 32.953... \n", "1 8526d9a3fffffff POLYGON ((-98.71570 32.47235, -98.64788 32.546... \n", "2 8526dc1bfffffff POLYGON ((-100.32151 34.39136, -100.25314 34.4... \n", "3 85446c77fffffff POLYGON ((-94.98183 29.87978, -94.91454 29.952... \n", "4 85444db7fffffff POLYGON ((-95.10421 32.87010, -95.03517 32.941... \n", "\n", " poi_count employee_avg rep_point \n", "0 0 0.000000 POINT (-99.37959 32.96652) \n", "1 0 0.000000 POINT (-98.75897 32.56035) \n", "2 0 0.000000 POINT (-100.36707 34.47834) \n", "3 13 3.833333 POINT (-95.02049 29.96866) \n", "4 1 3.000000 POINT (-95.14402 32.95831) " ] }, "execution_count": 19, "metadata": {}, "output_type": "execute_result" } ], "source": [ "areas['rep_point'] = areas['geometry'].centroid\n", "areas.head()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Distance array**" ] }, { "cell_type": "code", "execution_count": 20, "metadata": {}, "outputs": [], "source": [ "dist_array = cdist(gpd.GeoSeries(areas['rep_point'], crs='epsg:4326').to_crs('epsg:26914').apply(lambda point:[point.x, point.y]).tolist(),\n", " sales_reps['geometry'].to_crs('epsg:26914').apply(lambda point:[point.x, point.y]).tolist(),\n", " metric=metric)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### 3.2. Approach 1. Closest sales rep\n", "\n", "This is a very simple approach in which every grid cell is assigned to its closer sales rep." ] }, { "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", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
        hex_idgeometrypoi_countemployee_avgrep_pointsales_rep_1sales_rep_1_catsales_rep_dist_1
        08526d9d7fffffffPOLYGON ((-99.33557 32.87875, -99.26773 32.953...00.000000POINT (-99.37959 32.96652)0Dallas262156.516612
        18526d9a3fffffffPOLYGON ((-98.71570 32.47235, -98.64788 32.546...00.000000POINT (-98.75897 32.56035)0Dallas199715.113991
        28526dc1bfffffffPOLYGON ((-100.32151 34.39136, -100.25314 34.4...00.000000POINT (-100.36707 34.47834)0Dallas520625.827092
        385446c77fffffffPOLYGON ((-94.98183 29.87978, -94.91454 29.952...133.833333POINT (-95.02049 29.96866)3Houston51541.627399
        485444db7fffffffPOLYGON ((-95.10421 32.87010, -95.03517 32.941...13.000000POINT (-95.14402 32.95831)0Dallas188844.476467
        \n", "
        " ], "text/plain": [ " hex_id geometry \\\n", "0 8526d9d7fffffff POLYGON ((-99.33557 32.87875, -99.26773 32.953... \n", "1 8526d9a3fffffff POLYGON ((-98.71570 32.47235, -98.64788 32.546... \n", "2 8526dc1bfffffff POLYGON ((-100.32151 34.39136, -100.25314 34.4... \n", "3 85446c77fffffff POLYGON ((-94.98183 29.87978, -94.91454 29.952... \n", "4 85444db7fffffff POLYGON ((-95.10421 32.87010, -95.03517 32.941... \n", "\n", " poi_count employee_avg rep_point sales_rep_1 \\\n", "0 0 0.000000 POINT (-99.37959 32.96652) 0 \n", "1 0 0.000000 POINT (-98.75897 32.56035) 0 \n", "2 0 0.000000 POINT (-100.36707 34.47834) 0 \n", "3 13 3.833333 POINT (-95.02049 29.96866) 3 \n", "4 1 3.000000 POINT (-95.14402 32.95831) 0 \n", "\n", " sales_rep_1_cat sales_rep_dist_1 \n", "0 Dallas 262156.516612 \n", "1 Dallas 199715.113991 \n", "2 Dallas 520625.827092 \n", "3 Houston 51541.627399 \n", "4 Dallas 188844.476467 " ] }, "execution_count": 21, "metadata": {}, "output_type": "execute_result" } ], "source": [ "areas['sales_rep_1'] = list(map(np.argmin, dist_array))\n", "areas['sales_rep_1_cat'] = sales_reps.loc[areas['sales_rep_1'], 'city'].tolist()\n", "areas['sales_rep_dist_1'] = list(map(np.min, dist_array))\n", "areas.head()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "##### 3.2.1. Visualize and analyze results\n", "\n", "As we presumed, we can see clusters are unbalanced both in terms of number of clients and maximum distance because of the very uneven spatial distribution of clients.\n", "\n", "Main issues to tacke:\n", "- Dallas gets almost 10 times as many clients as El Paso.\n", "- El Paso and Dallas sales reps max distance more than doubles that from Houston sales rep." ] }, { "cell_type": "code", "execution_count": 22, "metadata": {}, "outputs": [ { "data": { "text/html": [ "\n", "\n", "\n", " None\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", "\n", " \n", "\n", " \n", "\n", " \n", " \n", " \n", " \n", " \n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", "\n", "\n", "\n", " Static map image\n", " \n", " \n", "
        \n", "
        \n", "
        \n", " \n", " \n", "
        \n", "
        \n", " \n", "\n", "
        \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
        \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
        \n", " \n", " \n", "
        \n", "
        \n", "
        \n", "
        \n", " \n", "
        \n", "
        \n", "
        \n", "\n", " \n", "\n", "
        \n", "
        \n", " :\n", "
        \n", " \n", " \n", "
        \n", "
        \n", "\n", "
        \n", " StackTrace\n", "
          \n", "
          \n", "
          \n", "\n", "\n", "\n", "\n", "\n", "\">\n", "\n", "" ], "text/plain": [ "" ] }, "execution_count": 22, "metadata": {}, "output_type": "execute_result" } ], "source": [ "Map([Layer(areas[areas['poi_count'] > 0],\n", " style=color_category_style('sales_rep_1_cat', cat=sales_reps['city'].tolist()), \n", " legends=color_category_legend('Sales Rep'), encode_data=False),\n", " Layer(sales_reps, \n", " style=basic_style(color='black'))])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The following two charts show the number of clients and maximum distance per cluster, respectively." ] }, { "cell_type": "code", "execution_count": 23, "metadata": {}, "outputs": [ { "data": { "image/png": "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\n", "text/plain": [ "
          " ] }, "metadata": { "needs_background": "light" }, "output_type": "display_data" } ], "source": [ "plot_cluster_comparison(sales_reps['city'].tolist(), areas, 'sales_rep_1_cat', poi_count='sum')" ] }, { "cell_type": "code", "execution_count": 24, "metadata": {}, "outputs": [ { "data": { "image/png": "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\n", "text/plain": [ "
          " ] }, "metadata": { "needs_background": "light" }, "output_type": "display_data" } ], "source": [ "plot_cluster_comparison(sales_reps['city'].tolist(), areas[areas['poi_count']>0], 'sales_rep_1_cat', sales_rep_dist_1='max')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### 3.3. Approach 2. Minimum Cost Flow\n", "\n", "[Minimum cost flow](https://en.wikipedia.org/wiki/Minimum-cost_flow_problem#:~:text=The%20minimum%2Dcost%20flow%20problem,flow%20through%20a%20flow%20network.) is a well known type of problems in Optimization that finds the cheapest possible way of sending a certain amount of flow through a flow network. Initially it is not the approach to follow for spatial clustering problems. However, since we can represent our problem as a graph and interpret sales reps as source nodes and grid cells as sink nodes, we can use algorithms designed for Minimum Cost Flow problems for solving Two-Layer Territory Management use cases.\n", "\n", "We will use [ortools pywrapgraph wrapper](https://developers.google.com/optimization/flow/mincostflow) as solver." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "##### 3.3.1. Data preparation\n", "\n", "We need to perform some extra data processing for in order tu use this algorithm." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Balancing criteria**\n", "\n", "We would like to balance clusters based on total number of clients and maximum distance. Normally we are not looking for a perfect balance, especially when dealing with more than one multiple criteria, and a balance tolerance is introduced. In our case, we will consider a tolerance of 10% in number of clients, which means that we allow clusters to be as much as 10% below the perfect balance.\n", "\n", "*Note* if you get very unbalanced clusters in terms of maximum distance, you should set a higher tolerance.\n", "\n", "*Note* we are only using the number of clients and the maximum distance to balance, but this dataset also has the number of employees per client and you might even have other data you might be interested in using. In order to introduce other criteria in this algorithm, you need to create a multidimensional index to combine all criteria. Distance may or may not be included as it is always treated separately." ] }, { "cell_type": "code", "execution_count": 25, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "The maximum number of clients a sales rep can have assigned is 13102\n" ] } ], "source": [ "balance_tolerance = 0.1 # 10%\n", "max_pois= int(np.round((1+balance_tolerance) * areas['poi_count'].sum()/sales_reps.shape[0]))\n", "print('The maximum number of clients a sales rep can have assigned is', max_pois)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Pairwise distance**\n", "\n", "First, we need pairwise distances between cells. We will use the euclidean distance in this case." ] }, { "cell_type": "code", "execution_count": 26, "metadata": {}, "outputs": [], "source": [ "catesian_dist = cdist(gpd.GeoSeries(areas['rep_point'], crs='epsg:4326').to_crs('epsg:26914').apply(lambda point:[point.x, point.y]).tolist(),\n", " gpd.GeoSeries(areas['rep_point'], crs='epsg:4326').to_crs('epsg:26914').apply(lambda point:[point.x, point.y]).tolist())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Consider all cells**\n", "\n", "Even though we only care about cells with clients, we will consider all of them to reduce complexity. This way, we only need to consider contiguous cells as adjacent, otherwise we would need to define some k-nearest weights to make sure we can get from one cell to any other.\n", "\n", "In order for the algorithm to not disregard cells without clients, we artifically add 1 to all cells." ] }, { "cell_type": "code", "execution_count": 27, "metadata": {}, "outputs": [], "source": [ "areas['poi_count'] = areas['poi_count']+1" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Graph component data**\n", "\n", "Next, we need to generate all information required by the algorithm, namely:\n", "- Adjacency matrix\n", "- Edges, unit cost, capacity, and supply/demand" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "*Adjacency matrix*\n", "\n", "We will use [Rook weights](https://pysal.org/libpysal/generated/libpysal.weights.Rook.html) which considers two polygons to be contiguous if they share one edge." ] }, { "cell_type": "code", "execution_count": 28, "metadata": {}, "outputs": [], "source": [ "wgt = Rook.from_dataframe(areas, geom_col='geometry')" ] }, { "cell_type": "code", "execution_count": 29, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[(2, 5), (3, 65), (4, 123), (5, 69), (6, 2322)]" ] }, "execution_count": 29, "metadata": {}, "output_type": "execute_result" } ], "source": [ "wgt.histogram" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "*Edges, unit cost, capacity, and supply/demand*\n", "\n", "Here we create all graph components required by the algorithm." ] }, { "cell_type": "code", "execution_count": 31, "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", "
          start_nodeend_nodecapacityunit_cost
          14974258425891310220
          149752584255131020
          14976258525891310220
          1497725851850131020
          14978258625891310220
          1497925861165131020
          14980258725891310220
          1498125872235131020
          14982258825891310220
          149832588547131020
          \n", "
          " ], "text/plain": [ " start_node end_node capacity unit_cost\n", "14974 2584 2589 13102 20\n", "14975 2584 255 13102 0\n", "14976 2585 2589 13102 20\n", "14977 2585 1850 13102 0\n", "14978 2586 2589 13102 20\n", "14979 2586 1165 13102 0\n", "14980 2587 2589 13102 20\n", "14981 2587 2235 13102 0\n", "14982 2588 2589 13102 20\n", "14983 2588 547 13102 0" ] }, "execution_count": 31, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Starting idx for sales reps and default sink\n", "sales_reps_first_idx = areas.shape[0]\n", "default_sink_idx = sales_reps_first_idx + sales_reps.shape[0]\n", "\n", "# Default sink demand\n", "default_sink_demand = int(max_pois*sales_reps.shape[0]-areas['poi_count'].sum())\n", "\n", "# Edges, unit cost, and capacity for grid cell connections\n", "graph_info_nd_array = [[i, k, max_pois, int(np.round(catesian_dist[i][k]/1e3))] for i in range(len(wgt.cardinalities)) for k in wgt[i]]\n", "\n", "# Default sink unit cost\n", "default_sink_ucost = np.transpose(graph_info_nd_array)[-1].max()+1\n", "\n", "# Edges, unit cost, and capacity for sales rep - cell connections and default sink\n", "for idx, row in gpd.sjoin(sales_reps, areas).iterrows():\n", " # default sink\n", " graph_info_nd_array.append([sales_reps_first_idx+idx, default_sink_idx, max_pois, default_sink_ucost])\n", " # same cell connection\n", " cell_id = row['index_right']\n", " graph_info_nd_array.append([sales_reps_first_idx+idx, cell_id, max_pois, 0])\n", " \n", "# DataFrame with edges, unit cost, and capacity\n", "graph_info = pd.DataFrame(graph_info_nd_array, columns=['start_node', 'end_node', 'capacity', 'unit_cost'])\n", "graph_info.tail(10)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "##### 3.3.2. Calculate clusters" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We first build the graph with all its information." ] }, { "cell_type": "code", "execution_count": 32, "metadata": {}, "outputs": [], "source": [ "# Instantiate a SimpleMinCostFlow solver.\n", "min_cost_flow = pywrapgraph.SimpleMinCostFlow()\n", "\n", "# Add edges, unit costs, and capacities\n", "for idx, row in graph_info.iterrows():\n", " min_cost_flow.AddArcWithCapacityAndUnitCost(row['start_node'].item(), row['end_node'].item(), row['capacity'].item(), row['unit_cost'].item())\n", " \n", "# Add supply/demand\n", "for idx, row in areas.iterrows():\n", " min_cost_flow.SetNodeSupply(idx, -row['poi_count'])\n", " \n", "for idx, row in sales_reps.iterrows():\n", " min_cost_flow.SetNodeSupply(sales_reps_first_idx+idx, max_pois)\n", " \n", "min_cost_flow.SetNodeSupply(default_sink_idx, -default_sink_demand)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We're ready to solve now." ] }, { "cell_type": "code", "execution_count": 33, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Minimum cost: 11903441\n" ] } ], "source": [ "if min_cost_flow.Solve() == min_cost_flow.OPTIMAL:\n", " print('Minimum cost:', min_cost_flow.OptimalCost())\n", " '''print('')\n", " print(' Arc Flow / Capacity Cost')\n", " for i in range(min_cost_flow.NumArcs()):\n", " cost = min_cost_flow.Flow(i) * min_cost_flow.UnitCost(i)\n", " print('%1s -> %1s %3s / %3s %3s' % (\n", " min_cost_flow.Tail(i),\n", " min_cost_flow.Head(i),\n", " min_cost_flow.Flow(i),\n", " min_cost_flow.Capacity(i),\n", " cost))'''\n", "else:\n", " print('There was an issue with the min cost flow input.')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "##### 3.3.3. Label grid cells\n", "\n", "As a solution of the Minimum Cost Flow problem, we have a graph with 5 components, one per cluster. In order to infer the cluster to which each cell belongs, we need to recursively explore the graph.\n", "\n", "Let's first build the solution graph." ] }, { "cell_type": "code", "execution_count": 34, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n" ] } ], "source": [ "SG = nx.DiGraph()\n", "\n", "nodes = [(i, {'color':'blue'}) for i in range(areas.shape[0])]\n", "nodes += [(i+sales_reps_first_idx, {'color':'red'}) for i in range(sales_reps.shape[0])]\n", "SG.add_nodes_from(nodes)\n", "\n", "edges = []\n", "total = 0\n", "for i in range(min_cost_flow.NumArcs()):\n", " if min_cost_flow.Flow(i) > 0.1 and min_cost_flow.Head(i) != default_sink_idx: \n", " edges.append((min_cost_flow.Tail(i), min_cost_flow.Head(i)))\n", " total +=1\n", " \n", "SG.add_edges_from(edges)\n", "\n", "'''pos = areas['rep_point'].apply(lambda point:(point.x, point.y)).tolist() +\\\n", " sales_reps.apply(lambda row: (row['lon'], row['lat']), axis=1).tolist()\n", "pos = dict(zip(list(range(len(nodes))), pos))\n", "\n", "plt.figure(figsize=(18, 18))\n", "\n", "node_color = ['blue']*areas.shape[0] + ['red']*sales_reps.shape[0]\n", "nx.draw(SG, node_size=5, pos=pos,width=0.1, edge_color='grey', node_color=node_color)\n", "#nx.draw(SG, node_size=5,width=0.1, edge_color='grey', node_color=node_color)'''\n", "print('')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let's now calculate each cell's cluster." ] }, { "cell_type": "code", "execution_count": 35, "metadata": {}, "outputs": [], "source": [ "def tag_successors(successors, cluster, sol_dict):\n", " for s in successors:\n", " \n", " if sol_dict.get(s) is None:\n", " sol_dict[s] = cluster\n", " elif sol_dict[s] != cluster:\n", " # Border cells can be assign to more than one cluster.\n", " #print(f'Watch out! Node {s} belongs to clusters {sol_dict.get(s)} and {cluster}')\n", " pass\n", " else:\n", " # Already tagged path\n", " continue\n", " \n", " if len(list(SG.successors(s))) == 0:\n", " pass\n", " else:\n", " tag_successors(SG.successors(s), cluster, sol_dict)\n", "\n", "sol_dict = dict()\n", "for cluster in range(sales_reps.shape[0]):\n", " successors = list(SG.successors(sales_reps_first_idx+cluster))\n", " tag_successors(successors, sales_reps_first_idx+cluster, sol_dict) " ] }, { "cell_type": "code", "execution_count": 36, "metadata": {}, "outputs": [], "source": [ "assert(len(sol_dict)==areas.shape[0])" ] }, { "cell_type": "code", "execution_count": 37, "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", "
          hex_idgeometrypoi_countemployee_avgrep_pointsales_rep_1sales_rep_1_catsales_rep_dist_1sales_rep_2sales_rep_2_cat
          08526d9d7fffffffPOLYGON ((-99.33557 32.87875, -99.26773 32.953...10.000000POINT (-99.37959 32.96652)0Dallas262156.5166124El Paso
          18526d9a3fffffffPOLYGON ((-98.71570 32.47235, -98.64788 32.546...10.000000POINT (-98.75897 32.56035)0Dallas199715.1139914El Paso
          28526dc1bfffffffPOLYGON ((-100.32151 34.39136, -100.25314 34.4...10.000000POINT (-100.36707 34.47834)0Dallas520625.8270924El Paso
          385446c77fffffffPOLYGON ((-94.98183 29.87978, -94.91454 29.952...143.833333POINT (-95.02049 29.96866)3Houston51541.6273993Houston
          485444db7fffffffPOLYGON ((-95.10421 32.87010, -95.03517 32.941...23.000000POINT (-95.14402 32.95831)0Dallas188844.4764670Dallas
          \n", "
          " ], "text/plain": [ " hex_id geometry \\\n", "0 8526d9d7fffffff POLYGON ((-99.33557 32.87875, -99.26773 32.953... \n", "1 8526d9a3fffffff POLYGON ((-98.71570 32.47235, -98.64788 32.546... \n", "2 8526dc1bfffffff POLYGON ((-100.32151 34.39136, -100.25314 34.4... \n", "3 85446c77fffffff POLYGON ((-94.98183 29.87978, -94.91454 29.952... \n", "4 85444db7fffffff POLYGON ((-95.10421 32.87010, -95.03517 32.941... \n", "\n", " poi_count employee_avg rep_point sales_rep_1 \\\n", "0 1 0.000000 POINT (-99.37959 32.96652) 0 \n", "1 1 0.000000 POINT (-98.75897 32.56035) 0 \n", "2 1 0.000000 POINT (-100.36707 34.47834) 0 \n", "3 14 3.833333 POINT (-95.02049 29.96866) 3 \n", "4 2 3.000000 POINT (-95.14402 32.95831) 0 \n", "\n", " sales_rep_1_cat sales_rep_dist_1 sales_rep_2 sales_rep_2_cat \n", "0 Dallas 262156.516612 4 El Paso \n", "1 Dallas 199715.113991 4 El Paso \n", "2 Dallas 520625.827092 4 El Paso \n", "3 Houston 51541.627399 3 Houston \n", "4 Dallas 188844.476467 0 Dallas " ] }, "execution_count": 37, "metadata": {}, "output_type": "execute_result" } ], "source": [ "areas['sales_rep_2'] = list({k: v for k, v in sorted(sol_dict.items(), key=lambda item:item[0])}.values())\n", "areas['sales_rep_2'] -= sales_reps_first_idx\n", "areas['sales_rep_2_cat'] = sales_reps.loc[areas['sales_rep_2'], 'city'].tolist()\n", "areas.head()" ] }, { "cell_type": "code", "execution_count": 38, "metadata": {}, "outputs": [], "source": [ "areas['sales_rep_dist_2'] = areas.apply(lambda row : dist_array[row.name, row.sales_rep_2], axis=1)" ] }, { "cell_type": "code", "execution_count": 39, "metadata": {}, "outputs": [], "source": [ "areas['poi_count'] = areas['poi_count']-1" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "##### 3.3.4. Visualize and analyze results\n", "\n", "We can see how the number of clients per cluster is more balanced now with the exception of El Paso. This issue was already identified at the beginning of the analysis. In order to double the number of clients for El Paso, we went from a maximum distance of 840 km to more than 1150 km, doubling the max distance of the cluster with the second highest max distnace.\n", "\n", "Next step would be to play with the balance tolerance to give a higher priority to distance or to the number of clients." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The following layout map compares the clusters generated by both techniques." ] }, { "cell_type": "code", "execution_count": 44, "metadata": {}, "outputs": [ { "data": { "text/html": [ "\n", "\n", "\n", " CARTOframes\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", "\n", " \n", "\n", " \n", " \n", " \n", " \n", "\n", " \n", " \n", " \n", " \n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", "\n", "\n", "\n", "
          \n", " \n", "
          \n", " \n", " \n", " \n", " \n", " \n", " \n", "
          \n", " \n", "\n", "
          \n", " "Static\n", " \n", "
          \n", "
          \n", " \n", "
          \n", " \n", " \n", "
          \n", "
          \n", " \n", "\n", "
          \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
          \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
          \n", " \n", " \n", "
          \n", "
          \n", "
          \n", "
          \n", " \n", "\n", "
          \n", "
          \n", "
          \n", " \n", " \n", " \n", " \n", " \n", " \n", "
          \n", " \n", "\n", "
          \n", " "Static\n", " \n", "
          \n", "
          \n", " \n", "
          \n", " \n", " \n", "
          \n", "
          \n", " \n", "\n", "
          \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
          \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
          \n", " \n", " \n", "
          \n", "
          \n", "
          \n", "
          \n", " \n", "\n", "
          \n", "
          \n", "
          \n", " \n", "
          \n", " \n", "
          \n", "\n", " \n", "\n", "
          \n", "
          \n", " :\n", "
          \n", " \n", " \n", "
          \n", "
          \n", "\n", "
          \n", " StackTrace\n", "
            \n", "
            \n", "
            \n", "\n", "\n", "\n", "\n", "\">\n", "\n", "" ], "text/plain": [ "" ] }, "execution_count": 44, "metadata": {}, "output_type": "execute_result" } ], "source": [ "Layout([Map([Layer(areas[areas['poi_count'] > 0],\n", " style=color_category_style('sales_rep_1_cat', cat=sales_reps['city'].tolist()), \n", " legends=color_category_legend('Closest Distance', 'Sales Reps'), encode_data=False),\n", " Layer(sales_reps, \n", " style=basic_style(color='black'))]),\n", " Map([Layer(areas[areas['poi_count'] > 1],\n", " style=color_category_style('sales_rep_2_cat', cat=sales_reps['city'].tolist()), \n", " legends=color_category_legend('Min Cost Flow', 'Sales Reps'), encode_data=False),\n", " Layer(sales_reps, \n", " style=basic_style(color='black'))])\n", " ], map_height=400)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The following two charts show the number of clients and maximum distance per cluster, respectively." ] }, { "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": [ "plot_cluster_comparison(sales_reps['city'].tolist(), areas, 'sales_rep_2_cat', poi_count='sum')" ] }, { "cell_type": "code", "execution_count": 43, "metadata": {}, "outputs": [ { "data": { "image/png": "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\n", "text/plain": [ "
            " ] }, "metadata": { "needs_background": "light" }, "output_type": "display_data" } ], "source": [ "plot_cluster_comparison(sales_reps['city'].tolist(), areas, 'sales_rep_2_cat', sales_rep_dist_2='max')" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [] } ], "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.3" } }, "nbformat": 4, "nbformat_minor": 4 }