{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "## Single-layer Territory Management. Optimizing territories considering only clients\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 use two different techniques to solve territory management problems when only the location of clients needs to be considered, i.e., we will have a single layer of data consisting of client 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. \n", "\n", "Their clients are mainly offices and clinics of medical doctors.\n", "\n", "They are interested in creating 5 balanced territories.\n", "\n", "We will use the following two 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", "\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": 2, "metadata": {}, "outputs": [], "source": [ "import geopandas as gpd\n", "import matplotlib.pyplot as plt\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 shapely import wkt\n", "from shapely.geometry import mapping, Polygon\n", "from sklearn.cluster import KMeans\n", "from spopt.region.maxp import MaxPHeuristic\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": 3, "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": 39, "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": 143, "metadata": {}, "outputs": [], "source": [ "def plot_clinic_balance(clusters, 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=clusters, \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()\n", " \n", " return axs" ] }, { "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": 151, "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": 151, "metadata": {}, "output_type": "execute_result" } ], "source": [ "wof_grographies = Geography.get('wof_geojson_4e78587c')\n", "wof_grographies.to_dict()" ] }, { "cell_type": "code", "execution_count": 36, "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.06460 32.9...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.06460 32.9... 1555446728 \n", "\n", " lastmodified_timestamp \n", "0 2019-04-16 20:32:08+00:00 " ] }, "execution_count": 36, "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", "\n", "tx_boundary['geom'] = list(map(wkt.loads, tx_boundary['geom']))\n", "tx_boundary = gpd.GeoDataFrame(tx_boundary, geometry='geom', crs='epsg:4326')\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": 21, "metadata": {}, "outputs": [], "source": [ "poi_dataset = Dataset.get('pb_consumer_po_62cddc04')" ] }, { "cell_type": "code", "execution_count": 32, "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
01484649291#-95.391433#29.7307642020-08-01CHARLES GARCIANaN1484649291CHARLES GARCIA, MD PANaNUSANaNHOUSTONNaNTEXASTX77006-61224704 MONTROSE BLVD, HOUSTON, TX, 77006-61224704 MONTROSE BLVDHOUSTON, TX, 77006-6122-95.39143329.730764S8HPNTSCZAHIGH1.0(713) 333-0151NaNNaNWWW.CHARLESGARCIAMD.COMNaNOFFICES AND CLINICS MEDICAL DOCTORS,NSK8011.0NaN80110513OPTHALMOLOGIST621111.010942513DIVISION I. - SERVICESHEALTH SERVICESOFFICES AND CLINICS OF DOCTORS OF MEDICINEOFFICES AND CLINICS OF MEDICAL DOCTORS75.075.02019.0515248.0515248.020.0G13.00.00.0NaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNNaNNaNNaNNaNNaNNaNNaN0.00.0NaNNaNPOINT (-95.39143 29.73076)
11121837904#-93.987719#29.9561972020-08-01SOUTHEAST TEXAS IMAGING LLPNaN1121837904NaNNaNUSANaNNEDERLANDNaNTEXASTX77627-62901323 S 27TH ST STE 700, NEDERLAND, TX, 77627-62901323 S 27TH ST STE 700NEDERLAND, TX, 77627-6290-93.98771929.956197S8HPNTSCZAHIGH1.0(409) 729-5400NaNNaNNaNNaNOFFICES AND CLINICS MEDICAL DOCTORS,NSK8011.0NaN80110519RADIOLOGIST621111.010942519DIVISION I. - SERVICESHEALTH SERVICESOFFICES AND CLINICS OF DOCTORS OF MEDICINEOFFICES AND CLINICS OF MEDICAL DOCTORS11.011.01995.01386157.01386157.020.0G12.01.00.0SOUTHEAST TEXAS IMAGING LLP1323 S 27TH ST STE 700, NEDERLAND, TEXAS, 7762...1323 S 27TH ST STE 700NEDERLANDTEXASUSA776276290.0SOUTHEAST TEXAS IMAGING LLP1323 S 27TH ST STE 700, NEDERLAND, TEXAS, 7762...1323 S 27TH ST STE 700NEDERLANDTEXAS776276290.0YSOUTHEAST TEXAS IMAGING LLP1323 S 27TH ST STE 700, NEDERLAND, TEXAS, 7762...1323 S 27TH ST STE 700NEDERLANDTEXASUSA7.76276e+082.01.0NaNNaNPOINT (-93.98772 29.95620)
21129360394#-97.401965#27.7761692020-08-01CARDIOLOGY ASSOCIATES OF CORPUS CHRISTINaN1129360394NaNNaNUSANaNCORPUS CHRISTINaNTEXASTX78404-31601521 S STAPLES ST STE 700, CORPUS CHRISTI, TX,...1521 S STAPLES ST STE 700CORPUS CHRISTI, TX, 78404-3160-97.40196527.776169S8HPNTSCZAHIGH1.0(361) 888-8271NaNNaNWWW.HEARTOFCC.COMNaNOFFICES AND CLINICS MEDICAL DOCTORS,NSK8011.0NaN80110101CARDIOLOGIST AND CARDIO-VASCULAR SPECIALIST621111.010942101DIVISION I. - SERVICESHEALTH SERVICESOFFICES AND CLINICS OF DOCTORS OF MEDICINEOFFICES AND CLINICS OF MEDICAL DOCTORS100.0100.01976.011077130.011077130.020.0G3.01.00.0CARDIOLOGY ASSOCIATES OF CORPUS CHRISTI1521 S STAPLES ST STE 700, CORPUS CHRISTI, TEX...1521 S STAPLES ST STE 700CORPUS CHRISTITEXASUSA784043160.0CARDIOLOGY ASSOCIATES OF CORPUS CHRISTI1521 S STAPLES ST STE 700, CORPUS CHRISTI, TEX...1521 S STAPLES ST STE 700CORPUS CHRISTITEXAS784043160.0YCARDIOLOGY ASSOCIATES OF CORPUS CHRISTI1521 S STAPLES ST STE 700, CORPUS CHRISTI, TEX...1521 S STAPLES ST STE 700CORPUS CHRISTITEXASUSA7.84043e+083.01.0NaNNaNPOINT (-97.40197 27.77617)
31119324698#-98.203039#26.2545862020-08-01VINCENT HONRUBIA, M.D., P.A.NaN1119324698NaNNaNUSANaNEDINBURGNaNTEXASTX78539-14062821 MICHAELANGELO DR STE 201, EDINBURG, TX, 7...2821 MICHAELANGELO DR STE 201EDINBURG, TX, 78539-1406-98.20303926.254586S8HPNTSCZAHIGH1.0(956) 421-4500NaNNaNWWW.SOUTHTEXASSINUSINSTITUTE.COMNaNOFFICES 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 DOCTORS13.013.02008.01464692.01464692.020.0G0.00.00.0NaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNNaNNaNNaNNaNNaNNaNNaN0.00.0NaNNaNPOINT (-98.20304 26.25459)
41130039254#-95.282651#32.2788742020-08-01TINSLEY ASSOCIATES, L.L.P.NaN1130039254EXCEPTIONAL HOME CARENaNUSANaNTYLERNaNTEXASTX75703-39781510 E GRANDE BLVD, TYLER, TX, 75703-39781510 E GRANDE BLVDTYLER, TX, 75703-3978-95.28265132.278874S8HPNTSCZAHIGH1.0(903) 533-0290NaNNaNNaNNaNOFFICES AND CLINICS MEDICAL DOCTORS,NSK8011.08082.080110516PEDIATRICIAN621111.010942516DIVISION I. - SERVICESHEALTH SERVICESOFFICES AND CLINICS OF DOCTORS OF MEDICINEOFFICES AND CLINICS OF MEDICAL DOCTORS23.0200.02002.011307457.011307457.020.0G12.01.00.0TINSLEY ASSOCIATES, L.L.P.1510 E GRANDE BLVD, TYLER, TEXAS, 757033978, USA1510 E GRANDE BLVDTYLERTEXASUSA757033978.0TINSLEY ASSOCIATES, L.L.P.1510 E GRANDE BLVD, TYLER, TEXAS, 7570339781510 E GRANDE BLVDTYLERTEXAS757033978.0YTINSLEY ASSOCIATES, L.L.P.1510 E GRANDE BLVD, TYLER, TEXAS, 757033978, USA1510 E GRANDE BLVDTYLERTEXASUSA7.57034e+082.01.0NaNNaNPOINT (-95.28265 32.27887)
\n", "
" ], "text/plain": [ " geoid do_date \\\n", "0 1484649291#-95.391433#29.730764 2020-08-01 \n", "1 1121837904#-93.987719#29.956197 2020-08-01 \n", "2 1129360394#-97.401965#27.776169 2020-08-01 \n", "3 1119324698#-98.203039#26.254586 2020-08-01 \n", "4 1130039254#-95.282651#32.278874 2020-08-01 \n", "\n", " NAME BRANDNAME PB_ID \\\n", "0 CHARLES GARCIA NaN 1484649291 \n", "1 SOUTHEAST TEXAS IMAGING LLP NaN 1121837904 \n", "2 CARDIOLOGY ASSOCIATES OF CORPUS CHRISTI NaN 1129360394 \n", "3 VINCENT HONRUBIA, M.D., P.A. NaN 1119324698 \n", "4 TINSLEY ASSOCIATES, L.L.P. NaN 1130039254 \n", "\n", " TRADE_NAME FRANCHISE_NAME ISO3 AREANAME4 AREANAME3 \\\n", "0 CHARLES GARCIA, MD PA NaN USA NaN HOUSTON \n", "1 NaN NaN USA NaN NEDERLAND \n", "2 NaN NaN USA NaN CORPUS CHRISTI \n", "3 NaN NaN USA NaN EDINBURG \n", "4 EXCEPTIONAL HOME CARE NaN USA NaN TYLER \n", "\n", " AREANAME2 AREANAME1 STABB POSTCODE \\\n", "0 NaN TEXAS TX 77006-6122 \n", "1 NaN TEXAS TX 77627-6290 \n", "2 NaN TEXAS TX 78404-3160 \n", "3 NaN TEXAS TX 78539-1406 \n", "4 NaN TEXAS TX 75703-3978 \n", "\n", " FORMATTEDADDRESS \\\n", "0 4704 MONTROSE BLVD, HOUSTON, TX, 77006-6122 \n", "1 1323 S 27TH ST STE 700, NEDERLAND, TX, 77627-6290 \n", "2 1521 S STAPLES ST STE 700, CORPUS CHRISTI, TX,... \n", "3 2821 MICHAELANGELO DR STE 201, EDINBURG, TX, 7... \n", "4 1510 E GRANDE BLVD, TYLER, TX, 75703-3978 \n", "\n", " MAINADDRESSLINE ADDRESSLASTLINE LONGITUDE \\\n", "0 4704 MONTROSE BLVD HOUSTON, TX, 77006-6122 -95.391433 \n", "1 1323 S 27TH ST STE 700 NEDERLAND, TX, 77627-6290 -93.987719 \n", "2 1521 S STAPLES ST STE 700 CORPUS CHRISTI, TX, 78404-3160 -97.401965 \n", "3 2821 MICHAELANGELO DR STE 201 EDINBURG, TX, 78539-1406 -98.203039 \n", "4 1510 E GRANDE BLVD TYLER, TX, 75703-3978 -95.282651 \n", "\n", " LATITUDE GEORESULT CONFIDENCE_CODE COUNTRY_ACCESS_CODE TEL_NUM \\\n", "0 29.730764 S8HPNTSCZA HIGH 1.0 (713) 333-0151 \n", "1 29.956197 S8HPNTSCZA HIGH 1.0 (409) 729-5400 \n", "2 27.776169 S8HPNTSCZA HIGH 1.0 (361) 888-8271 \n", "3 26.254586 S8HPNTSCZA HIGH 1.0 (956) 421-4500 \n", "4 32.278874 S8HPNTSCZA HIGH 1.0 (903) 533-0290 \n", "\n", " FAXNUM EMAIL HTTP OPEN_24H \\\n", "0 NaN NaN WWW.CHARLESGARCIAMD.COM NaN \n", "1 NaN NaN NaN NaN \n", "2 NaN NaN WWW.HEARTOFCC.COM NaN \n", "3 NaN NaN WWW.SOUTHTEXASSINUSINSTITUTE.COM 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 80110513 \n", "1 OFFICES AND CLINICS MEDICAL DOCTORS,NSK 8011.0 NaN 80110519 \n", "2 OFFICES AND CLINICS MEDICAL DOCTORS,NSK 8011.0 NaN 80110101 \n", "3 OFFICES AND CLINICS MEDICAL DOCTORS,NSK 8011.0 NaN 80119901 \n", "4 OFFICES AND CLINICS MEDICAL DOCTORS,NSK 8011.0 8082.0 80110516 \n", "\n", " SIC8_DESCRIPTION ALT_INDUSTRY_CODE \\\n", "0 OPTHALMOLOGIST 621111.0 \n", "1 RADIOLOGIST 621111.0 \n", "2 CARDIOLOGIST AND CARDIO-VASCULAR SPECIALIST 621111.0 \n", "3 GENERAL AND FAMILY PRACTICE, PHYSICIAN/SURGEON 621111.0 \n", "4 PEDIATRICIAN 621111.0 \n", "\n", " MICODE TRADE_DIVISION GROUP \\\n", "0 10942513 DIVISION I. - SERVICES HEALTH SERVICES \n", "1 10942519 DIVISION I. - SERVICES HEALTH SERVICES \n", "2 10942101 DIVISION I. - SERVICES HEALTH SERVICES \n", "3 10230302 DIVISION I. - SERVICES HEALTH SERVICES \n", "4 10942516 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 75.0 75.0 \n", "1 OFFICES AND CLINICS OF MEDICAL DOCTORS 11.0 11.0 \n", "2 OFFICES AND CLINICS OF MEDICAL DOCTORS 100.0 100.0 \n", "3 OFFICES AND CLINICS OF MEDICAL DOCTORS 13.0 13.0 \n", "4 OFFICES AND CLINICS OF MEDICAL DOCTORS 23.0 200.0 \n", "\n", " YEAR_START SALES_VOLUME_LOCAL SALES_VOLUME_US_DOLLARS CURRENCY_CODE \\\n", "0 2019.0 515248.0 515248.0 20.0 \n", "1 1995.0 1386157.0 1386157.0 20.0 \n", "2 1976.0 11077130.0 11077130.0 20.0 \n", "3 2008.0 1464692.0 1464692.0 20.0 \n", "4 2002.0 11307457.0 11307457.0 20.0 \n", "\n", " AGENT_CODE LEGAL_STATUS_CODE STATUS_CODE SUBSIDIARY_INDICATOR \\\n", "0 G 13.0 0.0 0.0 \n", "1 G 12.0 1.0 0.0 \n", "2 G 3.0 1.0 0.0 \n", "3 G 0.0 0.0 0.0 \n", "4 G 12.0 1.0 0.0 \n", "\n", " PARENT_BUSINESS_NAME \\\n", "0 NaN \n", "1 SOUTHEAST TEXAS IMAGING LLP \n", "2 CARDIOLOGY ASSOCIATES OF CORPUS CHRISTI \n", "3 NaN \n", "4 TINSLEY ASSOCIATES, L.L.P. \n", "\n", " PARENT_ADDRESS \\\n", "0 NaN \n", "1 1323 S 27TH ST STE 700, NEDERLAND, TEXAS, 7762... \n", "2 1521 S STAPLES ST STE 700, CORPUS CHRISTI, TEX... \n", "3 NaN \n", "4 1510 E GRANDE BLVD, TYLER, TEXAS, 757033978, USA \n", "\n", " PARENT_STREET_ADDRESS PARENT_AREANAME3 PARENT_AREANAME1 PARENT_COUNTRY \\\n", "0 NaN NaN NaN NaN \n", "1 1323 S 27TH ST STE 700 NEDERLAND TEXAS USA \n", "2 1521 S STAPLES ST STE 700 CORPUS CHRISTI TEXAS USA \n", "3 NaN NaN NaN NaN \n", "4 1510 E GRANDE BLVD TYLER TEXAS USA \n", "\n", " PARENT_POSTCODE DOMESTIC_ULTIMATE_BUSINESS_NAME \\\n", "0 NaN NaN \n", "1 776276290.0 SOUTHEAST TEXAS IMAGING LLP \n", "2 784043160.0 CARDIOLOGY ASSOCIATES OF CORPUS CHRISTI \n", "3 NaN NaN \n", "4 757033978.0 TINSLEY ASSOCIATES, L.L.P. \n", "\n", " DOMESTIC_ULTIMATE_ADDRESS \\\n", "0 NaN \n", "1 1323 S 27TH ST STE 700, NEDERLAND, TEXAS, 7762... \n", "2 1521 S STAPLES ST STE 700, CORPUS CHRISTI, TEX... \n", "3 NaN \n", "4 1510 E GRANDE BLVD, TYLER, TEXAS, 757033978 \n", "\n", " DOMESTIC_ULTIMATE_STREET_ADDRESS DOMESTIC_ULTIMATE_AREANAME3 \\\n", "0 NaN NaN \n", "1 1323 S 27TH ST STE 700 NEDERLAND \n", "2 1521 S STAPLES ST STE 700 CORPUS CHRISTI \n", "3 NaN NaN \n", "4 1510 E GRANDE BLVD TYLER \n", "\n", " DOMESTIC_ULTIMATE_AREANAME1 DOMESTIC_ULTIMATE_POSTCODE \\\n", "0 NaN NaN \n", "1 TEXAS 776276290.0 \n", "2 TEXAS 784043160.0 \n", "3 NaN NaN \n", "4 TEXAS 757033978.0 \n", "\n", " GLOBAL_ULTIMATE_INDICATOR GLOBAL_ULTIMATE_BUSINESS_NAME \\\n", "0 N NaN \n", "1 Y SOUTHEAST TEXAS IMAGING LLP \n", "2 Y CARDIOLOGY ASSOCIATES OF CORPUS CHRISTI \n", "3 N NaN \n", "4 Y TINSLEY ASSOCIATES, L.L.P. \n", "\n", " GLOBAL_ULTIMATE_ADDRESS \\\n", "0 NaN \n", "1 1323 S 27TH ST STE 700, NEDERLAND, TEXAS, 7762... \n", "2 1521 S STAPLES ST STE 700, CORPUS CHRISTI, TEX... \n", "3 NaN \n", "4 1510 E GRANDE BLVD, TYLER, TEXAS, 757033978, USA \n", "\n", " GLOBAL_ULTIMATE_STREET_ADDRESS GLOBAL_ULTIMATE_AREANAME3 \\\n", "0 NaN NaN \n", "1 1323 S 27TH ST STE 700 NEDERLAND \n", "2 1521 S STAPLES ST STE 700 CORPUS CHRISTI \n", "3 NaN NaN \n", "4 1510 E GRANDE BLVD TYLER \n", "\n", " GLOBAL_ULTIMATE_AREANAME1 GLOBAL_ULTIMATE_COUNTRY GLOBAL_ULTIMATE_POSTCODE \\\n", "0 NaN NaN NaN \n", "1 TEXAS USA 7.76276e+08 \n", "2 TEXAS USA 7.84043e+08 \n", "3 NaN NaN NaN \n", "4 TEXAS USA 7.57034e+08 \n", "\n", " FAMILY_MEMBERS HIERARCHY_CODE TICKER_SYMBOL EXCHANGE_NAME \\\n", "0 0.0 0.0 NaN NaN \n", "1 2.0 1.0 NaN NaN \n", "2 3.0 1.0 NaN NaN \n", "3 0.0 0.0 NaN NaN \n", "4 2.0 1.0 NaN NaN \n", "\n", " geom \n", "0 POINT (-95.39143 29.73076) \n", "1 POINT (-93.98772 29.95620) \n", "2 POINT (-97.40197 27.77617) \n", "3 POINT (-98.20304 26.25459) \n", "4 POINT (-95.28265 32.27887) " ] }, "execution_count": 32, "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['geom'] = list(map(wkt.loads, pois['geom']))\n", "pois = gpd.GeoDataFrame(pois, geometry='geom', crs='epsg:4326')\n", "pois.head()" ] }, { "cell_type": "code", "execution_count": 33, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "(59554, 74)" ] }, "execution_count": 33, "metadata": {}, "output_type": "execute_result" } ], "source": [ "pois.shape" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### 1.3 Visualize data" ] }, { "cell_type": "code", "execution_count": 44, "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", " StackTrace\n", "
    \n", "
    \n", "
    \n", "\n", "\n", "\n", "\n", "\n", "\">\n", "\n", "" ], "text/plain": [ "" ] }, "execution_count": 44, "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, \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", " 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 4. 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 4 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": 45, "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
    084489edffffffffPOLYGON ((-97.03425 29.34525, -96.79420 29.479...
    18426d31ffffffffPOLYGON ((-102.58398 34.37842, -102.33428 34.5...
    28448f29ffffffffPOLYGON ((-104.42876 30.05621, -104.18949 30.2...
    384446ddffffffffPOLYGON ((-95.82009 29.75644, -95.57955 29.887...
    484489c5ffffffffPOLYGON ((-98.02512 29.30940, -97.78501 29.445...
    \n", "
    " ], "text/plain": [ " hex_id geometry\n", "0 84489edffffffff POLYGON ((-97.03425 29.34525, -96.79420 29.479...\n", "1 8426d31ffffffff POLYGON ((-102.58398 34.37842, -102.33428 34.5...\n", "2 8448f29ffffffff POLYGON ((-104.42876 30.05621, -104.18949 30.2...\n", "3 84446ddffffffff POLYGON ((-95.82009 29.75644, -95.57955 29.887...\n", "4 84489c5ffffffff POLYGON ((-98.02512 29.30940, -97.78501 29.445..." ] }, "execution_count": 45, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Buffer\n", "buffer = 2.5e4 # 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], 4)\n", "grid.head()" ] }, { "cell_type": "code", "execution_count": 46, "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": 46, "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": 48, "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
      084489edffffffffPOLYGON ((-97.03425 29.34525, -96.79420 29.479...154.250000
      18426d31ffffffffPOLYGON ((-102.58398 34.37842, -102.33428 34.5...12.000000
      28448f29ffffffffPOLYGON ((-104.42876 30.05621, -104.18949 30.2...00.000000
      384446ddffffffffPOLYGON ((-95.82009 29.75644, -95.57955 29.887...11084.801538
      484489c5ffffffffPOLYGON ((-98.02512 29.30940, -97.78501 29.445...4964.854093
      \n", "
      " ], "text/plain": [ " hex_id geometry \\\n", "0 84489edffffffff POLYGON ((-97.03425 29.34525, -96.79420 29.479... \n", "1 8426d31ffffffff POLYGON ((-102.58398 34.37842, -102.33428 34.5... \n", "2 8448f29ffffffff POLYGON ((-104.42876 30.05621, -104.18949 30.2... \n", "3 84446ddffffffff POLYGON ((-95.82009 29.75644, -95.57955 29.887... \n", "4 84489c5ffffffff POLYGON ((-98.02512 29.30940, -97.78501 29.445... \n", "\n", " poi_count employee_avg \n", "0 15 4.250000 \n", "1 1 2.000000 \n", "2 0 0.000000 \n", "3 1108 4.801538 \n", "4 496 4.854093 " ] }, "execution_count": 48, "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": 49, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "count 402.000000\n", "mean 148.144279\n", "std 709.392374\n", "min 0.000000\n", "25% 0.000000\n", "50% 4.000000\n", "75% 40.500000\n", "95% 496.950000\n", "max 8268.000000\n", "Name: poi_count, dtype: float64" ] }, "execution_count": 49, "metadata": {}, "output_type": "execute_result" } ], "source": [ "areas['poi_count'].describe(percentiles=[0.25, 0.5, 0.75, 0.95])" ] }, { "cell_type": "code", "execution_count": 51, "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", " StackTrace\n", "
        \n", "
        \n", "
        \n", "\n", "\n", "\n", "\n", "\n", "\">\n", "\n", "" ], "text/plain": [ "" ] }, "execution_count": 51, "metadata": {}, "output_type": "execute_result" } ], "source": [ "breaks=[1, 4, 40, 500]\n", "\n", "Map(Layer(areas, \n", " style=color_bins_style('poi_count', breaks=breaks),\n", " legends=color_bins_legend('Number of Clients')))" ] }, { "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", "We will explore two different techniques:\n", "- [KMeans clustering](https://en.wikipedia.org/wiki/K-means_clustering).\n", "- [Max-p spatial clustering algorithm](https://region.readthedocs.io/en/latest/users/max-p-regions/). " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### 3.1 Approach 1. KMeans clustering\n", "\n", "This is a very well known and broadly used technique. However, this technique doesn't allow you to incorporate balancing criteria and usually generates low quality results for territory management." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "##### 3.1.1. Calculate clusters" ] }, { "cell_type": "code", "execution_count": 52, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Done!\n" ] } ], "source": [ "no_territories = 5\n", "kmeans = KMeans(no_territories, random_state=111)\n", "\n", "kmeans.fit_predict(list(map(lambda point:[point.x, point.y], areas.to_crs('epsg:26914').centroid)), \n", " sample_weight=areas['poi_count'])\n", "print('Done!')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "##### 3.1.2. Label grid cells\n", "\n", "We assign each cell to the cluster it belongs to.\n", "\n", "In order to have comparable results with other techniques, we identify 5 representative cells, each of which being far enough from each other to make sure they are in different clusters. This way, we can compare how each cluster changes." ] }, { "cell_type": "code", "execution_count": 53, "metadata": {}, "outputs": [], "source": [ "areas['kmeans_cluster'] = kmeans.labels_\n", "areas['kmeans_cluster'] += 1" ] }, { "cell_type": "code", "execution_count": 107, "metadata": {}, "outputs": [], "source": [ "# This dictionary contains the representative cells with the cluster they represent.\n", "trans_dict={'8448c69ffffffff':1,\n", " '8426d47ffffffff':2,\n", " '8426cdbffffffff':3,\n", " '84446edffffffff':4,\n", " '844880dffffffff':5}" ] }, { "cell_type": "code", "execution_count": 55, "metadata": {}, "outputs": [], "source": [ "areas['kmeans_cluster_aux'] = -1\n", "for hex_id in trans_dict:\n", " areas.loc[areas['kmeans_cluster'] == areas.loc[areas['hex_id'] == hex_id, 'kmeans_cluster'].iloc[0], 'kmeans_cluster_aux'] = trans_dict[hex_id]\n", "areas['kmeans_cluster'] = areas['kmeans_cluster_aux']\n", "areas.drop(columns='kmeans_cluster_aux', inplace=True)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We create a string variable with a more descriptive name for our visualization" ] }, { "cell_type": "code", "execution_count": 56, "metadata": {}, "outputs": [], "source": [ "areas['kmeans_cluster_cat'] = list(map(lambda v:f'Cluster_{v}', areas['kmeans_cluster']))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "##### 3.1.3. Visualize and analyze results" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "KMeans calculates nice compact clusters (see map). However, we get very unbalanced clusters with Cluster_3 having 10 times more clients than Cluster_1, as can be seen in the chart below." ] }, { "cell_type": "code", "execution_count": 58, "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", " StackTrace\n", "
          \n", "
          \n", "
          \n", "\n", "\n", "\n", "\n", "\n", "\">\n", "\n", "" ], "text/plain": [ "" ] }, "execution_count": 58, "metadata": {}, "output_type": "execute_result" } ], "source": [ "Map(Layer(areas, \n", " style=color_category_style('kmeans_cluster_cat', cat=sorted(areas['kmeans_cluster_cat'].unique())),\n", " legends=color_category_legend('KMeans Clustering')))" ] }, { "cell_type": "code", "execution_count": 59, "metadata": {}, "outputs": [ { "data": { "image/png": "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IyPl6+urqKioC+5r4sSJSkxM1B133KH//Oc/l5UbAAAAwLXlggUrJydHFRUVio2N1bFjxzRx4kQVFBSoQ4cOeuaZZ+Tre/4hIiMjtXv3blkslipHsYqLizVp0iQFBAScdTur1er4uUGDBsrJydGGDRu0fv16DRo0SCkpKeratatjHbvdLsMwtHz5ctWqVUuSVFJSIn9/fx09elR+fn4XzFpUVKRvv/1We/bs0ZtvvqnS0lKdOHFCw4YN0+LFiy/0VgEAAAC4xp33FMHMzEylpKSovLxckjR9+nQVFRVp8uTJ2rNnj1555ZUL7qB+/fqKi4vThAkTZLFYJEkWi0WpqamqW7euatas6Vg3KChIW7ZskXSm2P1m2bJlSk5OVnR0tMaPH6/o6Gjt2LFDkuTj46PKykoFBgYqKipKS5YskSQdP35c/fv3V25urtNvRkhIiL788ktlZWUpKytLY8aM0d133025AgAAAOCU8xast99+WwsXLtQDDzygkydP6uOPP9a4ceN03333afLkycrKynJqJ1OmTFF4eLj69eun+Ph49enTR+Hh4UpLS6uyXkpKiqZNm6ZevXopPz9fwcHBkqSePXvKZrMpNjZWCQkJslgsevjhhyVJMTExGjBggH766SfNmTNHmzdvVlxcnPr06aPu3burR48el/K+AAAAAMBF8zIMwzjXky1atNB3330nSdqwYYOGDRumb7/9Vn5+fpKk5s2ba9OmTVcm6VWuoKBA9h+7uTsGAADAVevOB392d4SrXkFBgSIiItwdA0441+/qvBcl+fj4yGq1ys/PT998842aNWvmKFclJSWOa508icVi0cCBA8/6XO3atbVs2bIrnAgAAABAdXHegnX33XfrjTfeUPfu3bVq1SolJiY6nktPT1fLli1dHtBsgYGBTp/aCAAAAAAX47wF66mnntLQoUM1f/583XPPPerXr58kqVOnTiovL+doDwAAAAD8znkLVuPGjZWTk6OjR48qKCjIsXzs2LFq06aN6tat6/KAAAAAAOApLvg9WF5eXlXKlSTFxsa6LBAAAAAAeKrz3qYdAAAAAOA8ChYAAAAAmISCBQAAAAAmoWABAAAAgEkoWAAAAABgEgoWAAAAAJiEggUAAAAAJrng92DBOYbdrjsf/NndMQAAAK5adtspefvUdHcMwKU4gmWS01aruyMAVRQUFLg7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Dh069KflZxvv0KFDVf4FF3DGBx98oG3btjkeG4YhX1/fS/5sPNc8B87mwIED6tevn3x8fLR06VJdd91155xDzMnqh4J1GaKjo7V582b9/PPPkqTly5erY8eO7g2Fasvb21vPPvus9u3bJ0latmyZmjRpoo4dO2rVqlUqLy+X1WrVypUr1alTJ/n6+uq+++7TihUrJJ0paLt27VKrVq2YuzBVx44d9d5776myslLHjx/Xhx9+qE6dOikkJEQNGzbU6tWrJUlffPGFvL29dccdd6hDhw769NNPdeTIERmGoRUrVqhTp06O8X6bt0VFRfriiy/Uvn17t70+eKYdO3ZowYIFstlsOnXqlN555x3FxsZe8mfjueY58EfHjh3TQw89pM6dO2vevHmqWbOmJJn+95o5efXyMriq/bKsW7dOc+fOVUVFhRo2bKjZs2erbt267o6FaiorK0uLFy+WzWZTSEiInn32WYWFhSk9PV3Z2dmqqKhQx44d9dRTT8nLy0uHDx9WSkqKCgsL5eXlpaefftpx/jdzF5fjmWee0e23364hQ4aosrJSs2fP1ldffaWKigo9+OCDGjJkiKQzt2mfNGmSjh49Kj8/P02fPl133nmnJOm9997TkiVLVFFRoWbNmmn69Ony9/dXWVmZUlNTlZ+fL5vNpscee0zx8fHufLnwEL+flydPntS0adO0efNmVVZWqmvXrnryyScv+bPxfPMc+L1XX31VCxYs0B133FFl+ZtvvqkVK1aY9veaOXn1omABAAAAgEk4RRAAAAAATELBAgAAAACTULAAAAAAwCQULAAAAAAwCQULAAAAAExCwQIAwAP89h14AICrGwULAID/Gjp0qOPLPq8m+fn56t+/v7tjAACc4OvuAAAAXC1ee+01d0c4qxMnTqiiosLdMQAATuAIFgCg2iksLFTz5s318ssvq2XLloqOjlZGRoYkae/evRoxYoRatmypjh07avHixTIMQ5KUmJiot99++4Ljnz59WmlpaWrdurVatWql5ORknT59WpKUl5enhIQE3XXXXYqPj9e6desc2zVp0kQ//fST4/GYMWP00ksvOfY9b948xcfHq3nz5nrooYdUWFioI0eOaNiwYTp27JiaN2+uo0ePmvY+AQDMR8ECAFRL5eXl2r59u9atW6f09HQtXLhQubm5GjRokG677Tbl5eVp0aJFWrFihZYvX35RY7/00kv64YcflJWVpdzcXO3fv18vv/yyduzYoccee0yPPvqovvnmG40dO1ZPPPGEtm/f7tS4H374oRYuXKjPP/9chmFo0aJFuvHGG7V48WLVrVtXmzZt0g033HApbwcA4AqhYAEAqq2JEycqICBATZs2Vc+ePTVt2jSdOHFCY8eOlZ+fn2677TYNHTpU77///kWN++GHH+rRRx9V/fr1FRgYqOeee04PPPCAPvzwQ7Vp00adO3eWr6+v2rVrpw4dOig7O9upcXv06KEGDRqoTp06iomJ0c8//3wJrxoA4E5cgwUAqJb8/f1Vv359x+OQkBAdOXJEjRs3lq/v//35CwsLU1FR0UWNffjwYYWEhFQZW5JKSkoUFhZWZd2LGT8oKMjxs6+vr+PURQCA5+AIFgCgWjp9+rRKS0sdjw8cOKDmzZuruLhYlZWVjuWFhYW66aabLmrs+vXrq7i42PF4y5YteuuttxQaGqr9+/dXWff343t7e1e5WQXXUwFA9UPBAgBUW3PnzpXVatWPP/6orKwsjR49WjfddJNeeOEFWa1W7dq1S6+//rri4uIuaty4uDgtWrRIhw8f1okTJzR37lwdPnxYsbGx2rBhgz7++GPZbDatW7dOn376qWJjYyVJjRs3Vm5urgzDUF5enn744Qen9ufn5yer1Sqr1XrR7wEA4MriFEEAQLVVu3Zt3XfffapZs6YmTpyoe+65R+np6UpLS1Pbtm1Vs2ZNDRgwQElJSRc17mOPPaaTJ0+qZ8+eqqysVNeuXTVq1Cj5+fnp5Zdf1pw5c/T000/r5ptv1ty5cxUZGSlJmjRpkmbOnKk333xTrVq1Uvfu3Z3aX5MmTRQeHq5WrVrpgw8+UKNGjS76vQAAXBleBid4AwCqmcLCQnXs2FHff/+9ateu7e44AIBrCKcIAgAAAIBJOEUQAIA/aNu2rcrLy8/6XFxcnKZNm3aFEwEAPAWnCAIAAACASThFEAAAAABMQsECAAAAAJNQsAAAAADAJBQsAAAAADAJBQsAAAAATPL/AX9lv5z7l3+RAAAAAElFTkSuQmCC\n", "text/plain": [ "
          " ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "plot_clinic_balance(sorted(areas['kmeans_cluster_cat'].unique()), areas, 'kmeans_cluster_cat', poi_count='sum')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### 3.2 Approach 2. Max-p\n", "\n", "Let's now try to balance the number of clients per cluster while maintaining connected clusters as compact as possible.\n", "\n", "We will use [Pysal's implementation of the Max-p algorithm](https://github.com/pysal/spopt). Max-p is a spatial clustering algorithm that calculates spatially connected clusters, with similar properties, while balancing one criterion, or mixed or criteria." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "##### 3.2.1. Weights. Adjacency matrix\n", "\n", "The first thing we need to do is calculate the adjacency matrix which will tell the algorithm which cells are contiguous.\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": 60, "metadata": {}, "outputs": [], "source": [ "wgt = Rook.from_dataframe(areas, geom_col='geometry')" ] }, { "cell_type": "code", "execution_count": 61, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[(2, 6), (3, 27), (4, 34), (5, 33), (6, 302)]" ] }, "execution_count": 61, "metadata": {}, "output_type": "execute_result" } ], "source": [ "wgt.histogram" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "##### 3.2.2 Balancing criteria\n", "\n", "We would like to balance clusters based on total number of clients. 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 20%, which means that we allow clusters to be as much as 20% below the perfect balance.\n", "\n", "*Note* we are only using the number of clients 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. The good news is Max-p allows you to do that." ] }, { "cell_type": "code", "execution_count": 132, "metadata": {}, "outputs": [], "source": [ "# Trick to help the algorithm find compact areas\n", "areas['poi_count'] += 10" ] }, { "cell_type": "code", "execution_count": 133, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Minimum number of clients per cluster 10171\n" ] } ], "source": [ "balance_tolerance = 0.2 # 5%\n", "perfect_balance = areas['poi_count'].sum()/5\n", "threshold = int(np.floor(perfect_balance * (1-balance_tolerance)))\n", "print('Minimum number of clients per cluster', threshold)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "##### 3.2.3. Similarity criteria\n", "\n", "Max-p also allows you to set similarity criteria. These are variables that you want to have a similar behavior **within clusters**. For example, you might be interested in having clusters with similar demographic or socioeconomic characteristics.\n", "\n", "In this case, we don't have any specific criteria, so we will use the gris cell centroid coordinates as similarity criteria in order to get clusters as compact as possible. You can try removing these or only adding one of the coordinates to clearly see whats the effect of these similarity criteria." ] }, { "cell_type": "code", "execution_count": 134, "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", "
          hex_idgeometrypoi_countemployee_avgkmeans_clusterkmeans_cluster_catlatlonlat_normlon_normmaxp_clustermaxp_cluster_cat
          084489edffffffffPOLYGON ((-97.03425 29.34525, -96.79420 29.479...254.2500005Cluster_529.597247-97.0488820.3520240.7260355Cluster_5
          18426d31ffffffffPOLYGON ((-102.58398 34.37842, -102.33428 34.5...112.0000002Cluster_234.626792-102.6120150.8162730.3081922Cluster_2
          28448f29ffffffffPOLYGON ((-104.42876 30.05621, -104.18949 30.2...100.0000001Cluster_130.309779-104.4597960.4177930.1694061Cluster_1
          384446ddffffffffPOLYGON ((-95.82009 29.75644, -95.57955 29.887...11184.8015384Cluster_430.007381-95.8321040.3898810.8174264Cluster_4
          484489c5ffffffffPOLYGON ((-98.02512 29.30940, -97.78501 29.445...5064.8540935Cluster_529.561949-98.0419290.3487650.6514485Cluster_5
          \n", "
          " ], "text/plain": [ " hex_id geometry \\\n", "0 84489edffffffff POLYGON ((-97.03425 29.34525, -96.79420 29.479... \n", "1 8426d31ffffffff POLYGON ((-102.58398 34.37842, -102.33428 34.5... \n", "2 8448f29ffffffff POLYGON ((-104.42876 30.05621, -104.18949 30.2... \n", "3 84446ddffffffff POLYGON ((-95.82009 29.75644, -95.57955 29.887... \n", "4 84489c5ffffffff POLYGON ((-98.02512 29.30940, -97.78501 29.445... \n", "\n", " poi_count employee_avg kmeans_cluster kmeans_cluster_cat lat \\\n", "0 25 4.250000 5 Cluster_5 29.597247 \n", "1 11 2.000000 2 Cluster_2 34.626792 \n", "2 10 0.000000 1 Cluster_1 30.309779 \n", "3 1118 4.801538 4 Cluster_4 30.007381 \n", "4 506 4.854093 5 Cluster_5 29.561949 \n", "\n", " lon lat_norm lon_norm maxp_cluster maxp_cluster_cat \n", "0 -97.048882 0.352024 0.726035 5 Cluster_5 \n", "1 -102.612015 0.816273 0.308192 2 Cluster_2 \n", "2 -104.459796 0.417793 0.169406 1 Cluster_1 \n", "3 -95.832104 0.389881 0.817426 4 Cluster_4 \n", "4 -98.041929 0.348765 0.651448 5 Cluster_5 " ] }, "execution_count": 134, "metadata": {}, "output_type": "execute_result" } ], "source": [ "areas['lat'] = np.array(list(map(lambda point:[point.y, point.x], areas.centroid)))[:,0]\n", "areas['lon'] = np.array(list(map(lambda point:[point.y, point.x], areas.centroid)))[:,1]\n", "areas['lat_norm'] = (areas['lat'] - areas['lat'].min())/(areas['lat'].max() - areas['lat'].min())\n", "areas['lon_norm'] = (areas['lon'] - areas['lon'].min())/(areas['lon'].max() - areas['lon'].min())\n", "areas.head()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "##### 3.2.4. Calculate clusters" ] }, { "cell_type": "code", "execution_count": 135, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "max_p: 5\n", "number of good partitions: 2\n", "0\n", "totalWithinRegionDistance after SA: \n", "6900.531405712055\n", "totalWithinRegionDistance after SA: \n", "7225.077640946365\n", "totalWithinRegionDistance after SA: \n", "7395.269464681606\n", "totalWithinRegionDistance after SA: \n", "6905.536769392361\n", "totalWithinRegionDistance after SA: \n", "7370.850274070599\n", "1\n", "totalWithinRegionDistance after SA: \n", "5642.22448183758\n", "totalWithinRegionDistance after SA: \n", "5613.498459949133\n", "totalWithinRegionDistance after SA: \n", "5618.573555920197\n", "totalWithinRegionDistance after SA: \n", "5749.532705832657\n", "totalWithinRegionDistance after SA: \n", "5677.7927687007\n", "best objective value:\n", "5613.498459949133\n" ] } ], "source": [ "maxp_heur = MaxPHeuristic(areas, wgt, ['lat_norm', 'lon_norm'], 'poi_count', threshold, \n", " 5, max_iterations_construction=2, max_iterations_sa=5, verbose=True)\n", "maxp_heur.solve()" ] }, { "cell_type": "code", "execution_count": 136, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "5" ] }, "execution_count": 136, "metadata": {}, "output_type": "execute_result" } ], "source": [ "np.max(maxp_heur.labels_)" ] }, { "cell_type": "code", "execution_count": 137, "metadata": {}, "outputs": [], "source": [ "# Undo change\n", "areas['poi_count'] -= 10" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "##### 3.2.5. Label grid cells\n", "\n", "We assign each cell to the cluster it belongs to and rename clusters based on the representative cells we mentioned in the KMeans section." ] }, { "cell_type": "code", "execution_count": 138, "metadata": {}, "outputs": [], "source": [ "areas['maxp_cluster'] = maxp_heur.labels_" ] }, { "cell_type": "code", "execution_count": 139, "metadata": {}, "outputs": [], "source": [ "areas['maxp_cluster_aux'] = -1\n", "for hex_id in trans_dict:\n", " areas.loc[areas['maxp_cluster'] == areas.loc[areas['hex_id'] == hex_id, 'maxp_cluster'].iloc[0], 'maxp_cluster_aux'] = trans_dict[hex_id]\n", "areas['maxp_cluster'] = areas['maxp_cluster_aux']\n", "areas.drop(columns='maxp_cluster_aux', inplace=True)" ] }, { "cell_type": "code", "execution_count": 140, "metadata": {}, "outputs": [], "source": [ "areas['maxp_cluster_cat'] = list(map(lambda v:f'Cluster_{v}', areas['maxp_cluster']))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "##### 3.2.6. Visualize and analyze results\n", "\n", "We can see how clusters now are less compact than they were with KMeans, but now clusters are balanced, with all of them satisfying the minimum requirement of 4256 clients per cluster." ] }, { "cell_type": "code", "execution_count": 141, "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", " "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", " StackTrace\n", "
            \n", "
            \n", "
            \n", "\n", "\n", "\n", "\n", "\">\n", "\n", "" ], "text/plain": [ "" ] }, "execution_count": 141, "metadata": {}, "output_type": "execute_result" } ], "source": [ "Layout([Map(Layer(areas, \n", " style=color_category_style('kmeans_cluster_cat', cat=sorted(areas['kmeans_cluster_cat'].unique())),\n", " legends=color_category_legend('KMeans Clustering'),\n", " popup_hover=[popup_element('hex_id'), popup_element('maxp_cluster_cat')])),\n", " Map(Layer(areas, \n", " style=color_category_style('maxp_cluster_cat', cat=sorted(areas['maxp_cluster_cat'].unique())),\n", " legends=color_category_legend('Max-p Clustering'),\n", " popup_hover=[popup_element('hex_id'), popup_element('maxp_cluster_cat')]))\n", " ], map_height=400)" ] }, { "cell_type": "code", "execution_count": 142, "metadata": {}, "outputs": [ { "data": { "image/png": "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99aJEycUHh5uX69169YqKSnRqVOnZLPZ5OfnZ38uICBAxcXFOn/+PO/DFcyaNUvDhw9Xx44d7cuKior061//2v6YGdfdkSNH5O/vr+nTp2vfvn265ZZblJSUxIxN1Lx5c6Wnpys2NlYtW7aUzWbTH//4R40fP16BgYH29Vq3bq39+/erqKhIrVq1krv7//3/w0szLioqsv/eufSakpKSm3o8PxazZs2SdPE/RC8pKioydab/vb1Lr2ksLjfj7/99O378uDIzMzVnzpw6/2640nvWWFxuxiUlJXr22We1atUqrV+/3r6cGdfN5Wb8zTffqLS0VKNHj9aJEyfUtWtXJSUlMePv4QzWDbDZbJdd/v0/Qri6yspKPfnkk/r222+VkZFx2f9b7+7ufsVZe3h48D5cwVtvvSVPT089/PDDtZYzY/PU1NRo+/btGj58uDZs2KBHH31U48aNs38W/fuYcd3s379fy5Yt06ZNm/Thhx9qwoQJmjx58mXnda0ZX+nfPi6qy++Gq82UeV/ZV199pbi4OD366KPq1atXnX83MOPaqqurNXXqVE2fPt3+CZlLmLF5ampqlJeXp6VLl+rtt99WeXm5lixZwoy/p36nd7HAwED9+9//tj8uKSnRrbfeKh8fHxemqj++++47xcbGysPDQ2vWrNEtt9xy2Zm2bt1at912mySpvLy81nMBAQG8D1fwzjvvaM+ePYqOjta4ceN0/vx5RUdHKyAggBmbpFWrVvr5z3+uTp06SZL69u0rq9Uqd3d3ZmySDz/8UHfffbfatWsnSYqLi9OBAwcUFBR02RkHBQXp5MmTtf5gX3ouMDBQJ06c+MFyXHSl3791nel/b+/EiRPMW9K7776rxMREPf3005owYYIk1fl3w5Xes8bqq6++UmFhoebPn6/o6GitW7dOmzZt0owZM5ixiVq1aqWIiAj5+vrKy8tLgwcP1hdffMGMv4eCdQPCw8O1e/duffPNN5KkdevWqU+fPq4NVU+cPn1ajz76qPr166clS5aoadOmki5ew7Jx40ZVVlaqqqpKGzZsUN++feXp6akHHnjAfrp/3759OnTokLp37877cAVZWVn661//quzsbK1YsUJNmzZVdna2IiIimLFJ7r//fh0/ftx+58Bdu3bJzc1NCQkJzNgk//u//6tdu3bp5MmTki5e19OmTRv16dNHb7/9tmpqanTmzBm9++676tu3r1q3bq127dpp06ZNkqQPPvhA7u7u+uUvf6nevXvr/fffV2lpqQzD0Pr169W3b19XHt6Pitkz7dOnj/3fenFxsT744AP16tXLZcf3Y7B582ZlZGRo1apVioqKsi+v6++GK71njVXnzp21fft2ZWdnKzs7W7GxsYqMjNSzzz7LjE3Uv39/bd68WefPn7dfQ/WrX/2KGX8P12DdgNtuu03z5s3TlClTVF1drXbt2mnBggWujlUv/PGPf1RRUZFyc3OVm5trX/7mm2+qX79+GjZsmKqrq9WnTx8NGTJEkpSamqqUlBQNGjRIbm5uWrhwoVq0aCFJvA/XoXfv3vr666+ZsQn8/f21bNkypaen69y5c/Ly8tJLL72krl27MmOT/PrXv9bo0aMVHx+vJk2a6NZbb9Urr7yin/3sZ/r2228VHR2t6upqDR8+XPfcc48k6fnnn9fMmTP16quvysvLS0uXLpW7u7v+53/+R5MmTVJCQoKqq6vVqVMnjR071sVH+OMxYsQIU2c6efJkpaWl6cEHH5TValVSUpL9TGRj9fzzz8swDKWkpNiX3X333UpNTa3T74arvWf4IWZsjpEjR6q8vFwxMTGyWq2688477TfTYsYXuRmN/RZVAAAAAGASPiIIAAAAACahYAEAAACASShYAAAAAGASChYAAAAAmISCBQAAAAAmoWABAFAPHDt2zNURAAAOoGABAPAfY8aMsX9J5o9Jfn6+RowY4eoYAAAH8EXDAAD8x+uvv+7qCJd19uxZVVdXuzoGAMABnMECADQ4hYWF6ty5s5YtW6Zu3bopPDxcmZmZkqSjR49q/Pjx6tatm/r06aOVK1fKMAxJUnx8vP7whz9cc/sXLlxQRkaG7r33XnXv3l3Jycm6cOGCJCkvL08xMTG6++67FR0dre3bt9tf17FjR3399df2x1OmTNFLL71k3/eSJUsUHR2tzp0769FHH1VhYaFKS0s1duxYnT59Wp07d9apU6dMmxMAwHwULABAg1RZWan9+/dr+/btWr58uV5++WVt3bpVo0aN0h133KG8vDytWLFC69ev17p1665r2y+99JK++OILZWdna+vWrTp+/LiWLVumAwcO6De/+Y0mTJigTz75RFOnTtWTTz6p/fv3O7Tdd999Vy+//LL++c9/yjAMrVixQrfddptWrlypli1b6vPPP9dPfvKTuowDAHCTULAAAA3WjBkz5OPjo7vuuktDhgzR7NmzdfbsWU2dOlVeXl664447NGbMGL3zzjvXtd13331XEyZMUEBAgHx9fbVw4UI9/PDDevfdd9WjRw/169dPnp6e6tmzp3r37q2cnByHtjt48GC1bdtWLVq0UEREhL755ps6HDUAwJW4BgsA0CB5e3srICDA/rh169YqLS1Vhw4d5On5f3/+goKCVFxcfF3bPnnypFq3bl1r25JUVlamoKCgWutez/b9/PzsP3t6eto/uggAqD84gwUAaJAuXLig8vJy++PvvvtOnTt3VklJiWpqauzLCwsLdfvtt1/XtgMCAlRSUmJ/vGfPHv3+979XYGCgjh8/Xmvd72/f3d291s0quJ4KABoeChYAoMFavHixqqqq9OWXXyo7O1uTJ0/W7bffrueff15VVVU6dOiQVq1apaioqOvablRUlFasWKGTJ0/q7NmzWrx4sU6ePKnIyEjt3LlTf//732W1WrV9+3a9//77ioyMlCR16NBBW7dulWEYysvL0xdffOHQ/ry8vFRVVaWqqqrrngEA4ObiI4IAgAarefPmeuCBB9S0aVPNmDFD99xzj5YvX66MjAzdd999atq0qUaOHKmEhITr2u5vfvMbnTt3TkOGDFFNTY0GDBigSZMmycvLS8uWLdOiRYv0u9/9Tj/96U+1ePFihYaGSpJmzpypefPm6c0331T37t01aNAgh/bXsWNHBQcHq3v37vrLX/6i9u3bX/csAAA3h5vBB7wBAA1MYWGh+vTpo88++0zNmzd3dRwAQCPCRwQBAAAAwCR8RBAAgP9y3333qbKy8rLPRUVFafbs2Tc5EQCgvuAjggAAAABgEj4iCAAAAAAmoWABAAAAgEkoWAAAAABgEgoWAAAAAJiEggUAAAAAJvn/AZdvjM8+0VoLAAAAAElFTkSuQmCC\n", "text/plain": [ "
            " ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "plot_clinic_balance(sorted(areas['maxp_cluster_cat'].unique()), areas, 'maxp_cluster_cat', poi_count='sum')" ] } ], "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.6.7" } }, "nbformat": 4, "nbformat_minor": 4 }