Guide to Network Analysis (Part 6 - Solve Location Allocation)

Introduction

Now we have learned about Network Datasets and Network Analysis Layer (NA Layer) in Part 1, how to find routes from one point to another, and among multiple points in Part 2, how to generate service area in Part 3, how to find closest facility in Part 4, and how to create an Origin Destination Cost Matrix in Part 5, let's move onto the sixth topic - how to solve location allocation. Please refer to the road map below if you want to revisit the previous topics or jump to the next topic -

  • Network Dataset and Network Analysis services (Part 1)
  • Find Routes (Part 2)
  • Generate Service Area (Part 3)
  • Find Closest Facility (Part 4)
  • Generate Origin Destination Cost Matrix (Part 5)
  • Solve Location Allocation (You are here!)
  • Vehicle Routing Problem Service (Part 7)

Why is location so important?

Location is often considered the most important factor leading to the success of a private- or public-sector organization. Private-sector organizations can profit from a good location, whether a small coffee shop with a local client or a multinational network of factories with distribution centers and a worldwide chain of retail outlets.

Location can help keep fixed and overhead costs low and accessibility high. Public-sector facilities, such as schools, hospitals, libraries, fire stations, and emergency response services (ERS) centers, can provide high-quality service to the community at a low cost when a good location is chosen [1].

What is Location-Allocation Analysis?

The location-allocation tool can be configured to solve specific problem types. Examples include the following:

  • A retail store wants to see which potential store locations would need to be developed to capture 10 percent of the retail market in the area.

  • A fire department wants to determine where it should locate fire stations to reach 90 percent of the community within a four-minute response time.

  • A police department wants to preposition personnel given past criminal activity at night.

  • After a storm, a disaster response agency wants to find the best locations to set up triage facilities, with limited patient capacities, to tend to the affected population.

The goal of location-allocation is to locate facilities in a way that supplies the demand points most efficiently, given facilities that provide goods and services and a set of demand points that consume them. "As the name suggests, location-allocation is a two-fold problem that simultaneously locates facilities and allocates demand points to the facilities" [2]. A good Location-Allocation Analysis would meet the following requirements [3]:

  • to minimize the overall distance between demand points and facilities,
  • maximize the number of demand points covered within a certain distance of facilities,
  • maximize an apportioned amount of demand that decays with increasing distance from a facility,
  • or maximize the amount of demand captured in an environment of friendly and competing facilities.

The API of Solve Location Allocation is useful when you need to choose which facilities from a set of facilities to operate based on their potential interaction with demand points. Along with using the API, we will need to perform the following steps:

  • Extract the required input FeatureSets for the tool from FeatureLayerCollection items in the GIS.
  • Define a function to extract, symbolize, and display the output results from the API.

Fig 1. Using Location Allocation Analysis tools to find the optimized future site(s) for fire stations and the potential service areas (source: [3]).

The API Method

The ArcGIS API for Python provides two ways to solve the location allocation problems, which as shown in the table below:

Operationnetwork.analysisfeatures.use_proximity
Routefind_routesplan_routes
ServiceAreagenerate_service_areascreate_drive_time_areas
ClosestFacilityfind_closest_facilitiesfind_nearest
OD Cost Matrixgenerate_origin_destination_cost_matrixconnect_origins_to_destinations
Location Allocationsolve_location_allocationchoose_best_facilities

These two methods are defined in different modules of the arcgis package, and will make distinct REST calls in the back end. A key separation from network.analysis to features.use_proximity is that the former provides full capabilities of solvers and runs faster, and the latter is workflow-driven and provides service-to-service I/O approach.

Defined in the network.analysis module, solve_location_allocation supports full capabilities of operations; while choose_best_facilities provides a workflow approach that user can input a feature service and get returned a feature service. We will walk through the data preparation, implementation, and visualization of output here.

Problem types

Initially, it may appear that all location-allocation analyses solve the same problem, but the best location is not the same for all types of facilities. For instance, the best location for an ERS center is different than the best location for a manufacturing plant. Location-Allocation analysis solver offers seven different problem types to answer specific kinds of questions [1], as stated below:

  • Minimize Weighted Impedance (P-Median)
  • Maximize Coverage
  • Maximize Coverage and Minimize Facilities
  • Maximize Attendance
  • Maximize Market Share
  • Target Market Share
  • Maximize Capacitated Coverage

Problem statement

Part 6 aims at showing how one can choose the optimal store locations based on the existing stores in the same brand, and in competitor brands, and also demand points which can be represented as population clusters in this case. Let's assume the user story is like this:

Jim is a GIS analyst in need to provide his customer - a chained Pizza Shop - new store locations that would generate the most business in the neighborhood. The main objective is to locate stores close to population centers, which provide demand for the stores, based on the premise that people tend to shop more at nearby stores than at those that are farther away.

The location-allocation analysis will be performed differently for four different problem types: maximize attendance, maximize market share, target market share and Minimize Impedance. The differences among these problem types will become apparent as you follow the workflow stated below [4].

Data Preparation

When you are ready, let's make your hands dirty with some real implementations! Getting started, let's first connect to a GIS instance.

from arcgis.gis import GIS
import arcgis.network as network
from arcgis.features import FeatureLayer, Feature, FeatureSet, FeatureCollection, find_locations
import pandas as pd
import time
import datetime as dt

If you have already set up a profile to connect to your ArcGIS Online organization, execute the cell below to load the profile and create the GIS class object. If not, use a traditional username/password log-in e.g. my_gis = GIS('https://www.arcgis.com', 'username', 'password', verify_cert=False, set_active=True)

my_gis = GIS(profile="your_online_profile")

Then we need to access and/or explore the input dataset (in this case, the facilities and demand points feature class).

Define Facilities and Demand Points Feature Class

At least one facility and one demand point need to be set as input to successfully execute the tool. You can load up to 1,000 facilities and 10,000 demand points.

A previously published Feature Service which contains pizza store locations and the census tract information for the San Francisco Downtown is to be used here for facilities and demand points feature classes.

try:
    sf_item = my_gis.content.get("6e757aa31942469fb91dd84ad53b8485")
    display(sf_item)
except RuntimeError as re:
    print("You dont have access to the item.")
Pick_Pizza_Shops_San_Francisco
Pick Pizza Shop locationFeature Layer Collection by api_data_owner
Last Modified: October 09, 2019
0 comments, 29 views

First, we need to get the candidate store locations - these are the potential places to open a new store. The solution from the location-allocation analysis process will include a subset of these stores.

for lyr in sf_item.layers:
    print(lyr.properties.name)
ExistingStore
CandidateStores
CompetitorStores
TractCentroids
Restricted Turns
Highways
candidate_fl = sf_item.layers[1]
try:
    candidate_facilities = candidate_fl.query(where="1=1", as_df=False)
    display(candidate_facilities)
except RuntimeError as re:
    print("Query failed.")
<FeatureSet> 16 features

Stores need to be located to best service the existing populations. A point layer of census tract centroids is a good fit to represent demand points.

demand_points_fl = sf_item.layers[3]
try:
    demand_points = demand_points_fl.query(where="1=1", as_df=False)
    display(demand_points)
except RuntimeError as re:
    print("Query failed.")
<FeatureSet> 208 features

Also, to note that, there are already existing stores in the study area. Considering the existing stores as required facilities layer will help eliminate competitions between the new store and the existing stores of the same brand, hence to fulfill the store-expansion requirements.

existing_fl = sf_item.layers[0]
try:
    required_facilities = existing_fl.query(where="1=1", as_df=False)
    display(required_facilities)
except RuntimeError as re:
    print("Query failed.")
<FeatureSet> 1 features

The location-allocation tool can also locate new stores to maximize the market share in light of competing stores. Here, the market share is computed using a Huff, or Gravity Model. One last input data needed for the market share scenario is the competitor layer, which in this case, means existing pizza stores from other brands.

competitor_fl = sf_item.layers[2]
try:
    competitor_facilities = competitor_fl.query(where="1=1", as_df=False)
    display(competitor_facilities)
except RuntimeError as re:
    print("Query failed.")
<FeatureSet> 3 features

Set up the properties of the analysis

Having the facilities and demand points defined as above is already sufficient for the basic functions of solving location allocation analysis. However, if you want to involve complicated models, for example, to take existing stores into consideration, or to beat competition from other pizza stores, here is something you need to:

  1. Specify whether the facility is a candidate, required, or a competitor facility. The field value is specified as one of the following integers (use the numeric code, not the name in parentheses):
  • 0 (Candidate)—A facility that may be part of the solution.
  • 1 (Required)—A facility that must be part of the solution.
  • 2 (Competitor)—A rival facility that potentially removes demand from your facilities. Competitor facilities are specific to the Maximize Market Share and Target Market Share problem types; they are ignored in other problem types.
  1. Specify Weight for the demands points feature class.
  • The relative weighting of the demand point. A value of 2.0 means the demand point is twice as important as one with a weight of 1.0. If demand points represent households, for example, weight could indicate the number of people in each household. In this case, the population of each census tract is applied as weight.
  1. Choose the correct problem type per analysis - problem types are often referred to as models. For example, Maximize attendance is a good problem type for choosing retail store locations, since it assumes that all stores are equally attractive and people are more likely to shop at nearby stores.

Further explanations to the specifications of the input arguments of the API method can be found in the help doc of REST API.

Facility Type

In order to specify facility type per feature in the facilities feature set, we will need to:

  • First, assign the candidate, required, and competitor facilities a new field called FacilityType.
  • Next, in preparation to append all three facilities feature sets into one, we need to change the object id fields for entries with the duplicate object ids, e.g. the one and only feature inside the required_facilities, {'OBJECTID': 1, 'NAME': 'Existing Store', 'FacilityType': 1} to be changed into new OBJECTID (which is 17 in this case).
  • Lastly, merge these three feature sets into a new feature set.
object_id_count = 0

for f in candidate_facilities:
    object_id_count+=1
    f.attributes.update({"FacilityType":0})
    print(f.attributes)
{'OBJECTID': 1, 'NAME': 'Store_1', 'FacilityType': 0}
{'OBJECTID': 2, 'NAME': 'Store_2', 'FacilityType': 0}
{'OBJECTID': 3, 'NAME': 'Store_3', 'FacilityType': 0}
{'OBJECTID': 4, 'NAME': 'Store_4', 'FacilityType': 0}
{'OBJECTID': 5, 'NAME': 'Store_5', 'FacilityType': 0}
{'OBJECTID': 6, 'NAME': 'Store_6', 'FacilityType': 0}
{'OBJECTID': 7, 'NAME': 'Store_7', 'FacilityType': 0}
{'OBJECTID': 8, 'NAME': 'Store_11', 'FacilityType': 0}
{'OBJECTID': 9, 'NAME': 'Store_12', 'FacilityType': 0}
{'OBJECTID': 10, 'NAME': 'Store_13', 'FacilityType': 0}
{'OBJECTID': 11, 'NAME': 'Store_14', 'FacilityType': 0}
{'OBJECTID': 12, 'NAME': 'Store_15', 'FacilityType': 0}
{'OBJECTID': 13, 'NAME': 'Store_16', 'FacilityType': 0}
{'OBJECTID': 14, 'NAME': 'Store_17', 'FacilityType': 0}
{'OBJECTID': 15, 'NAME': 'Store_18', 'FacilityType': 0}
{'OBJECTID': 16, 'NAME': 'Store_19', 'FacilityType': 0}
for f in required_facilities:
    object_id_count+=1
    f.attributes.update({"FacilityType":1, "OBJECTID":object_id_count})
    print(f.attributes)
{'OBJECTID': 17, 'NAME': 'Existing Store', 'FacilityType': 1}
for f in competitor_facilities:
    object_id_count+=1
    f.attributes.update({"FacilityType":2, "OBJECTID":object_id_count})
    print(f.attributes)
{'OBJECTID': 18, 'NAME': 'Competitor_1', 'FacilityType': 2}
{'OBJECTID': 19, 'NAME': 'Competitor_2', 'FacilityType': 2}
{'OBJECTID': 20, 'NAME': 'Competitor_3', 'FacilityType': 2}
facilities_flist = []

for ea in candidate_facilities:
    facilities_flist.append(ea)

for ea in required_facilities:
    facilities_flist.append(ea)
    
for ea in competitor_facilities:
    facilities_flist.append(ea)

facilities = FeatureSet(facilities_flist)
display(facilities)
<FeatureSet> 20 features
for f in facilities:
    print(f.attributes)
{'OBJECTID': 1, 'NAME': 'Store_1', 'FacilityType': 0}
{'OBJECTID': 2, 'NAME': 'Store_2', 'FacilityType': 0}
{'OBJECTID': 3, 'NAME': 'Store_3', 'FacilityType': 0}
{'OBJECTID': 4, 'NAME': 'Store_4', 'FacilityType': 0}
{'OBJECTID': 5, 'NAME': 'Store_5', 'FacilityType': 0}
{'OBJECTID': 6, 'NAME': 'Store_6', 'FacilityType': 0}
{'OBJECTID': 7, 'NAME': 'Store_7', 'FacilityType': 0}
{'OBJECTID': 8, 'NAME': 'Store_11', 'FacilityType': 0}
{'OBJECTID': 9, 'NAME': 'Store_12', 'FacilityType': 0}
{'OBJECTID': 10, 'NAME': 'Store_13', 'FacilityType': 0}
{'OBJECTID': 11, 'NAME': 'Store_14', 'FacilityType': 0}
{'OBJECTID': 12, 'NAME': 'Store_15', 'FacilityType': 0}
{'OBJECTID': 13, 'NAME': 'Store_16', 'FacilityType': 0}
{'OBJECTID': 14, 'NAME': 'Store_17', 'FacilityType': 0}
{'OBJECTID': 15, 'NAME': 'Store_18', 'FacilityType': 0}
{'OBJECTID': 16, 'NAME': 'Store_19', 'FacilityType': 0}
{'OBJECTID': 17, 'NAME': 'Existing Store', 'FacilityType': 1}
{'OBJECTID': 18, 'NAME': 'Competitor_1', 'FacilityType': 2}
{'OBJECTID': 19, 'NAME': 'Competitor_2', 'FacilityType': 2}
{'OBJECTID': 20, 'NAME': 'Competitor_3', 'FacilityType': 2}

Weight

Next, let's specify a new field Weight for the demands points feature class. Here, we will use the population per census tract as the weight factor.

for f in demand_points:
    tmp = f.get_value("POP2000")
    f.attributes.update({"Weight":tmp})

As mentioned previously, the best location is not the same for all problem types. For instance, the best location for an ERS center is different than the best location for a manufacturing plant.

Next, We will walk through four examples that each one uses the same input feature classes, but yet create different output in fulfilling various needs.

Example 1: Maximize Attendance

What is 'Maximize Attendance'?

When the problem_type is set to Maximize Attendance, Facilities are chosen such that as much demand weight as possible is allocated to facilities while assuming the demand weight decreases in relation to the distance between the facility and the demand point [5].

Specialty stores that have little or no competition benefit from this problem type, but it may also be beneficial to general retailers and restaurants that don't have the data on competitors necessary to perform market share problem types. Some businesses that might benefit from this problem type include coffee shops, fitness centers, dental and medical offices, and electronics stores. Public transit bus stops are often chosen with the help of Maximize Attendance. Maximize Attendance assumes that the farther people have to travel to reach your facility, the less likely they are to use it. This is reflected in how the amount of demand allocated to facilities diminishes with distance [5].

The following list describes how the Maximize Attendance problem handles demand:

  • A demand point that cannot reach any facilities due to a cutoff distance or time is not allocated.
  • When a demand point can reach a facility, its demand weight is only partially allocated to the facility. The amount allocated decreases as a function of the maximum cutoff distance (or time) and the travel distance (or time) between the facility and the demand point.
  • The weight of a demand point that can reach more than one facility is proportionately allocated to the nearest facility only.

Solving Problem

First solver we will showcase here is the arcgis.network.analysis.solve_location_allocation tool, and the important parameters for this tool include:

  • Cutoff- Specify the travel time or travel distance value at which to stop searching for destinations from the origin. Any destination beyond the cutoff value will not be considered. The value needs to be in the units specified by the Time Units parameter if the impedance attribute in your travel mode is time based or in the units specified by the Distance Units parameter if the impedance attribute in your travel mode is distance based. If a value is not specified, the tool will not enforce any travel time or travel distance limit when searching for destinations.

  • measurement_transformation_model: Measurement Transformation Model (str). Optional parameter. This sets the equation for transforming the network cost between facilities and demand points. This property, coupled with the Impedance Parameter, specifies how severely the network impedance between facilities and demand points influences the solver's choice of facilities. If needed, pick from ['Linear', 'Power', 'Exponential']

    • ArcGIS will use a linear decay in calculating people's propensity to visit a store. That is, with a five-minute impedance cutoff and a linear impedance transformation, the probability of visiting a store decays at 1/5, or 20 percent; therefore, a store one minute away from a demand point has an 80 percent probability of a visit compared to a store four minutes away, which only has a 20 percent probability [4].
  • You can set save_output_network_analysis_layer to True if you want to output the resulting NA layer as Layer file, though this process can take up more computation time [4].

%%time

result1 = network.analysis.solve_location_allocation(   problem_type='Maximize Attendance',
                                                        travel_direction='Demand to Facility',
                                                        number_of_facilities_to_find='3',
                                                        measurement_transformation_model="Linear",
                                                        measurement_transformation_factor=2,
                                                        demand_points=demand_points,
                                                        facilities=candidate_facilities,
                                                        measurement_units='Minutes',
                                                        default_measurement_cutoff=5
                                                    )
print('Analysis succeeded? {}'.format(result1.solve_succeeded))
Analysis succeeded? True
Wall time: 10.2 s
result1
ToolOutput(solve_succeeded=True, output_allocation_lines=<FeatureSet> 46 features, output_facilities=<FeatureSet> 16 features, output_demand_points=<FeatureSet> 208 features, output_network_analysis_layer=None, output_result_file=None, output_network_analysis_layer_package=None)

Tabularizing the response from solve_location_allocation

Now, let's explore the tabularized output from solve_location_allocation:

# Display the analysis results in a pandas dataframe.
result1.output_facilities.sdf[['Name', 'FacilityType', 
                               'Weight','DemandCount', 'DemandWeight', 'TotalWeighted_Minutes', 'Total_Minutes']]
NameFacilityTypeWeightDemandCountDemandWeightTotalWeighted_MinutesTotal_Minutes
0Store_10100.0000000.0000000.000000
1Store_20100.0000000.0000000.000000
2Store_331144.46880111.46809747.655993
3Store_40100.0000000.0000000.000000
4Store_50100.0000000.0000000.000000
5Store_60100.0000000.0000000.000000
6Store_70100.0000000.0000000.000000
7Store_110100.0000000.0000000.000000
8Store_120100.0000000.0000000.000000
9Store_1331124.85362811.18459035.731858
10Store_140100.0000000.0000000.000000
11Store_1531206.51660917.34620067.416956
12Store_160100.0000000.0000000.000000
13Store_170100.0000000.0000000.000000
14Store_180100.0000000.0000000.000000
15Store_190100.0000000.0000000.000000

Looking at the FacilityType column of the output table above, we can see that Store_12, Store_13, and Store_15 are marked as numeric code 3 (Chosen) - which means these are the optimized selection after running the specified model.

DemandCount - This field contains a count of demand points allocated to the facility. A nonzero value means the facility was chosen as part of the solution.

DemandWeight - This field contains a sum of the effective weight from all demand points allocated to the facility. The value is a sum of all the Weight values from the demand points allocated to the facility. In the case of the Maximize Attendance and Maximize Market Share problem types, the value is an apportioned sum of the Weight field values, since these problem types allow demand to decay with distance or be split among many facilities [1].

Visualizing the response from from solve_location_allocation

To improve the re-usablity of codes, we will wrap up the lines for visualizing returned results into an individual function called visualize_locate_allocate_result:

# Import the ArcGIS API for Python
from arcgis import *
from IPython.display import display

# Define a function to display the output analysis results in a map
def visualize_locate_allocate_results(map_widget, solve_locate_allocate_result, zoom_level):
    # The map widget
    m = map_widget
    # The locate-allocate analysis result
    result = solve_locate_allocate_result
    
    # 1. Parse the locate-allocate analysis results
    # Extract the output data from the analysis results
    # Store the output points and lines in pandas dataframes
    demand_df = result.output_demand_points.sdf
    lines_df = result.output_allocation_lines.sdf

    # Extract the allocated demand points (pop) data.
    demand_allocated_df = demand_df[demand_df['DemandOID'].isin(lines_df['DemandOID'])]
    demand_allocated_fset = features.FeatureSet.from_dataframe(demand_allocated_df)
    display(demand_allocated_df.head())

    # Extract the un-allocated demand points (pop) data.
    demand_not_allocated_df = demand_df[~demand_df['DemandOID'].isin(lines_df['DemandOID'])]
    demand_not_allocated_df['AllocatedWeight'] = demand_not_allocated_df['AllocatedWeight'].replace(np.nan, 0)
    demand_not_allocated_df['FacilityOID'] = demand_not_allocated_df['FacilityOID'].replace(np.nan, 0)
    if len(demand_not_allocated_df)>0:
        display(demand_not_allocated_df.head())
        demand_not_allocated_fset = features.FeatureSet.from_dataframe(demand_not_allocated_df)

    # Extract the chosen facilities (candidate sites) data.
    facilities_df = result.output_facilities.sdf[['Name', 'FacilityType', 
                                                 'Weight','DemandCount', 'DemandWeight', 'SHAPE']]
    facilities_chosen_df = facilities_df[facilities_df['FacilityType'] == 3]
    facilities_chosen_fset = features.FeatureSet.from_dataframe(facilities_chosen_df)

    # 2. Define the map symbology
    # Allocation lines
    allocation_line_symbol_1 = {'type': 'esriSLS', 'style': 'esriSLSSolid',
                                'color': [255,255,255,153], 'width': 0.7}

    allocation_line_symbol_2 = {'type': 'esriSLS', 'style': 'esriSLSSolid',
                                'color': [0,255,197,39], 'width': 3}

    allocation_line_symbol_3 = {'type': 'esriSLS', 'style': 'esriSLSSolid',
                                'color': [0,197,255,39], 'width': 5}
    
    allocation_line_symbol_4 = {'type': 'esriSLS', 'style': 'esriSLSSolid',
                                'color': [0,92,230,39], 'width': 7}
    
    # Patient points within 90 minutes drive time to a proposed location.
    allocated_demand_symbol = {'type' : 'esriPMS', 'url' : 'https://maps.esri.com/legends/Firefly/cool/1.png',
                               'contentType' : 'image/png', 'width' : 26, 'height' : 26,
                               'angle' : 0, 'xoffset' : 0, 'yoffset' : 0}

    # Patient points outside of a 90 minutes drive time to a proposed location.
    unallocated_demand_symbol = {'type' : 'esriPMS', 'url' : 'https://maps.esri.com/legends/Firefly/warm/1.png',
                                 'contentType' : 'image/png', 'width' : 19.5, 'height' : 19.5,
                                 'angle' : 0, 'xoffset' : 0, 'yoffset' : 0}

    # Selected facilities
    selected_facilities_symbol = {"angle":0,"xoffset":0,"yoffset":0,"type":"esriPMS",
                                  "url":"http://static.arcgis.com/images/Symbols/PeoplePlaces/Pizza.png",
                                  "contentType":"image/png","width":13,"height":13}
    
    # 3. Display the analysis results in the map
    
    # Zoom out to display all of the allocated census points.
    m.zoom = zoom_level
    
    # Display the locations of pop within the specified drive time to the selected site(s).
    m.draw(shape=demand_allocated_fset, symbol=allocated_demand_symbol)

    # Display the locations of pop outside the specified drive time to the selected site(s).
    if len(demand_not_allocated_df)>0:
        m.draw(shape = demand_not_allocated_fset, symbol = unallocated_demand_symbol)

    # Display the chosen site.
    m.draw(shape=facilities_chosen_fset, symbol=selected_facilities_symbol)
    m.draw(shape=result.output_allocation_lines, symbol=allocation_line_symbol_2)
    m.draw(shape=result.output_allocation_lines, symbol=allocation_line_symbol_1)
# Display the analysis results in a map.

# Create a map of SF, California.
map1 = my_gis.map('San Francisco, CA')
map1.basemap = 'dark-gray'
map1
# Call custom function defined earlier in this notebook to 
# display the analysis results in the map.
visualize_locate_allocate_results(map1, result1, zoom_level=8)
item_properties = {
    "title": "Location Allocation example of Pizza Stores in San Francisco",
    "tags" : "Location Allocation",
    "snippet": "Location Allocation example of Pizza Stores in San Francisco",
    "description": "a web map of Location Allocation example of Pizza Stores in San Francisco"
}

item = map1.save(item_properties)
print(item)
<Item title:"Location Allocation example of Pizza Stores in San Francisco" type:Web Map owner:arcgis_python>

Next, let's look at other examples with different goals in solving the problem.

Example 2: Maximize Market Share

What is 'Maximize Market Share'?

Maximize Market Share—A specific number of facilities are chosen such that the allocated demand is maximized in the presence of competitors. The goal is to capture as much of the total market share as possible with a given number of facilities, which you specify. The total market share is the sum of all demand weight for valid demand points. The market share problem types require the most data because, along with knowing your own facilities' weight, you also need to know that of your competitors' facilities. The same types of facilities that use the Maximize Attendance problem type can also use market share problem types given that they have comprehensive information that includes competitor data. Large discount stores typically use Maximize Market Share to locate a finite set of new stores. The market share problem types use a Huff model, which is also known as a gravity model or spatial interaction [5].

The following list describes how the Maximize Market Share problem handles demand:

  • A demand point that cannot reach any facilities due to a cutoff distance or time is not allocated.
  • A demand point that can only reach one facility has all its demand weight allocated to that facility.
  • A demand point that can reach two or more facilities has all its demand weight allocated to them; furthermore, the weight is split among the facilities proportionally to the facilities' attractiveness (facility weight) and inversely proportional to the distance between the facility and demand point. Given equal facility weights, this means more demand weight is assigned to near facilities than far facilities.

The total market share, which can be used to calculate the captured market share, is the sum of the weight of all valid demand points.

Solving Problem

Number_of_Facilities_to_Find - Specify the number of facilities the solver should choose. The default value is 1.

  • The facilities with a FacilityType field value of 1 (Required) are always chosen first. Any excess facilities to choose are picked from candidate facilities.

  • Any facilities that have a FacilityType value of 3 (Chosen) before solving are treated as candidate facilities at solve time.

  • If the number of facilities to find is less than the number of required facilities, an error occurs.

Number of Facilities to Find is disabled for the Minimize Facilities and Target Market Share problem types since the solver determines the minimum number of facilities needed to meet the objectives.

So we are giving the Number_of_Facilities_to_Find a value of 4 here, since the analysis will first count the existing store and we need to retain 3 more spots for the candidate locations.

result1b = network.analysis.solve_location_allocation(  problem_type='Maximize Market Share', 
                                                        facilities=facilities, 
                                                        demand_points=demand_points,
                                                        number_of_facilities_to_find=4,
                                                        measurement_transformation_model="Power",
                                                        measurement_transformation_factor=2,
                                                        travel_direction='Demand to Facility',
                                                        impedance="TravelTime",
                                                        measurement_units='Minutes', 
                                                        default_measurement_cutoff=5
                                                    )

print('Solve succeeded? {}'.format(result1b.solve_succeeded))
WARNING 030194: Data values longer than 500 characters for field [Facilities:Name] are truncated.
Solve succeeded? True
# Display the analysis results in a pandas dataframe.
result1b.output_facilities.sdf[['Name', 'FacilityType', 
                                'Weight','DemandCount', 'DemandWeight', 'TotalWeighted_Minutes', 'Total_Minutes']]
NameFacilityTypeWeightDemandCountDemandWeightTotalWeighted_MinutesTotal_Minutes
0Store_10100.0000000.0000000.000000
1Store_20100.0000000.0000000.000000
2Store_3311412.14890939.86266047.655993
3Store_40100.0000000.0000000.000000
4Store_50100.0000000.0000000.000000
5Store_60100.0000000.0000000.000000
6Store_70100.0000000.0000000.000000
7Store_110100.0000000.0000000.000000
8Store_120100.0000000.0000000.000000
9Store_130100.0000000.0000000.000000
10Store_14311616.00000056.32600556.326005
11Store_15312111.37535935.47943472.347427
12Store_160100.0000000.0000000.000000
13Store_170100.0000000.0000000.000000
14Store_180100.0000000.0000000.000000
15Store_190100.0000000.0000000.000000
16Existing Store1132.62298910.25304311.537555
17Competitor_1211917.20942654.32540262.183695
18Competitor_2211612.06310937.76081352.859307
19Competitor_3211710.58020832.84978456.760605

Looking at the FacilityType column of the output table above, we can see that Store_3, Store_14, and Store_15 are marked as numeric code 3 (Chosen) - which means these are the optimized selection after running the specified model.

Total_Minutes - This field contains a sum of network costs between the facility and each of the demand points allocated to the facility. The [Impedance] portion of the field name is replaced with the network attribute name, for example, Total_Minutes, where Minutes is the name of the network attribute.

TotalWeighted_Minutes - This field stores the cumulative weighted cost for a facility. The weighted cost for a demand point is its weight multiplied by the least-cost path between the facility and the demand point. The weighted cost for a facility is the sum of all the weighted costs of demand points allocated to the facility. For example, if a demand point with a weight of two is allocated to a facility 10 minutes of drive away, the TotalWeighted_Minutes value is 20 (2 x 10). If another demand point with a weight of three is allocated to the same facility and is 5 minutes of drive away, the TotalWeighted_Minutes value increases to 35 (3 x 5 + 20).

# Display the analysis results in a map.

# Create a map of SF, California.
map1b = my_gis.map('San Francisco, CA')
map1b.basemap = 'dark-gray'
map1b
# Call custom function defined earlier in this notebook to 
# display the analysis results in the map.
visualize_locate_allocate_results(map1b, result1b, zoom_level=8)

Note that the sites with pizza symbols are the newly chosen store locations. The existing stores and the competitor stores are also shown as the centers of rays.

item_properties['title'] += " (2)"

item = map1b.save(item_properties)
print(item)
<Item title:"Location Allocation example of Pizza Stores in San Francisco (2)" type:Web Map owner:arcgis_python>

Example 3: Target Market Share

What is 'Target Market Share'?

Target Market Share — Target Market Share chooses the minimum number of facilities necessary to capture a specific percentage of the total market share in the presence of competitors. The total market share is the sum of all demand weight for valid demand points. You set the percent of the market share you want to reach and let the solver choose the fewest number of facilities necessary to meet that threshold [5].

The market share problem types require the most data because, along with knowing your own facilities' weight, you also need to know that of your competitors' facilities. The same types of facilities that use the Maximize Attendance problem type can also use market share problem types given that they have comprehensive information that includes competitor data.

Large discount stores typically use the Target Market Share problem type when they want to know how much expansion would be required to reach a certain level of the market share or see what strategy would be needed just to maintain their current market share given the introduction of new competing facilities. The results often represent what stores would like to do if budgets weren't a concern. In other cases where budget is a concern, stores revert to the Maximize Market Share problem and simply capture as much of the market share as possible with a limited number of facilities.

The following list describes how the target market share problem handles demand:

  • The total market share, which is used in calculating the captured market share, is the sum of the weight of all valid demand points.
  • A demand point that cannot reach any facilities due to a cutoff distance or time is not allocated.
  • A demand point that can only reach one facility has all its demand weight allocated to that facility.
  • A demand point that can reach two or more facilities has all its demand weight allocated to them; furthermore, the weight is split among the facilities proportionally to the facilities' attractiveness (facility weight) and inversely proportional to the distance between the facility and demand point. Given equal facility weights, this means more demand weight is assigned to near facilities than far facilities.

Solving Problem

target_market_share - This parameter is specific to the Target Market Share problem type. It is the percentage of the total demand weight that you want the chosen and required facilities to capture. The solver chooses the minimum number of facilities needed to capture the target market share specified here. The default value is 10 percent.

Note that, because there are competitor stores in the area that already took up some of the market share, setting the market share as 100 is not realistic. In doing so, a warning message is rendered - "Market share attained: 48.7194894610517 percent." Which means the best situation (when all candidate stores are established) the attained market share could only be 48.7195%.

result2b = network.analysis.solve_location_allocation(  problem_type='Target Market Share', 
                                                        target_market_share=100,
                                                        facilities=facilities, 
                                                        demand_points=demand_points,
                                                        travel_direction='Demand to Facility',
                                                        measurement_units='Minutes', 
                                                        default_measurement_cutoff=5
                                                    )

print('Solve succeeded? {}'.format(result2b.solve_succeeded))
WARNING 030194: Data values longer than 500 characters for field [Facilities:Name] are truncated.
Unable to obtain the requested market share of 100 percent.
Market share attained: 55.152410301244 percent.
Solve succeeded? True
# Display the analysis results in a table.
# Display the analysis results in a pandas dataframe.
result2b.output_facilities.sdf[['Name', 'FacilityType', 
                                'Weight','DemandCount', 'DemandWeight', 'SHAPE']]
NameFacilityTypeWeightDemandCountDemandWeightSHAPE
0Store_13186.307213{"x": -122.51001818199995, "y": 37.77236363700...
1Store_23196.279457{"x": -122.48887272699994, "y": 37.75376363600...
2Store_3311411.736065{"x": -122.46492727299994, "y": 37.77472727300...
3Store_431106.537595{"x": -122.47394545399999, "y": 37.74316363600...
4Store_53185.351509{"x": -122.44929090899996, "y": 37.73154545500...
5Store_63133.000000{"x": -122.49174545399995, "y": 37.64930909100...
6Store_73177.000000{"x": -122.48318181799999, "y": 37.70110909100...
7Store_113166.000000{"x": -122.43378181799994, "y": 37.65536363600...
8Store_12311211.400418{"x": -122.43898181799995, "y": 37.71923636400...
9Store_1331128.942410{"x": -122.44021818099998, "y": 37.74538181800...
10Store_14311611.538479{"x": -122.421636364, "y": 37.74296363700006, ...
11Store_15312110.947335{"x": -122.43098181799996, "y": 37.78296363700...
12Store_1631116.060129{"x": -122.42687272699999, "y": 37.76929090900...
13Store_173183.945429{"x": -122.43234545499996, "y": 37.80521818200...
14Store_1831124.402119{"x": -122.41281818199997, "y": 37.80574545500...
15Store_193193.557941{"x": -122.39890909099995, "y": 37.79707272800...
16Existing Store1131.710915{"x": -122.39431795899998, "y": 37.79674801900...
17Competitor_121199.648551{"x": -122.40921699299997, "y": 37.79576199200...
18Competitor_221168.428084{"x": -122.436438039, "y": 37.76927424200005, ...
19Competitor_321178.206352{"x": -122.44080450799999, "y": 37.79101207000...

As shown in the table above, in order to reach a 48.7195% market share, all the 16 candidate stores are chosen as new sites, which in reality would not happen.

So setting up a practical goal here, we are to set target_market_share to be 33.34(%), targeting to reach at least 1/3 of the market share.

result2 = network.analysis.solve_location_allocation(   problem_type='Target Market Share', 
                                                        target_market_share=33.34,
                                                        facilities=facilities, 
                                                        demand_points=demand_points,
                                                        travel_direction='Demand to Facility',
                                                        measurement_units='Minutes', 
                                                        default_measurement_cutoff=5
                                                    )

print('Solve succeeded? {}'.format(result2.solve_succeeded))
Solve succeeded? True
result2
ToolOutput(solve_succeeded=True, output_allocation_lines=<FeatureSet> 135 features, output_facilities=<FeatureSet> 20 features, output_demand_points=<FeatureSet> 208 features, output_network_analysis_layer=None, output_result_file=None, output_network_analysis_layer_package=None)
# Display the analysis results in a table.
# Display the analysis results in a pandas dataframe.
result2.output_facilities.sdf[['Name', 'FacilityType', 
                               'Weight','DemandCount', 'DemandWeight', 'TotalWeighted_Minutes', 'Total_Minutes']]
NameFacilityTypeWeightDemandCountDemandWeightTotalWeighted_MinutesTotal_Minutes
0Store_10100.0000000.0000000.000000
1Store_20100.0000000.0000000.000000
2Store_3311412.19136640.14372247.655993
3Store_4311010.00000035.58290635.582906
4Store_50100.0000000.0000000.000000
5Store_60100.0000000.0000000.000000
6Store_73177.00000021.49620221.496202
7Store_110100.0000000.0000000.000000
8Store_12311211.58730336.98790538.224502
9Store_130100.0000000.0000000.000000
10Store_14311615.41269753.82167456.326005
11Store_15312111.57280737.27434072.347427
12Store_160100.0000000.0000000.000000
13Store_170100.0000000.0000000.000000
14Store_180100.0000000.0000000.000000
15Store_190100.0000000.0000000.000000
16Existing Store1132.56245410.04679411.537555
17Competitor_1211917.27028454.59544362.183695
18Competitor_2211611.85744737.40636652.859307
19Competitor_3211710.54564133.67225956.760605

From the output table above, we can see that in order to reach a 1/3 market share, at least 8 stores, namely Store_2, Store_3, Store_7, Store_11, Store_12, Store_13, Store_14, and Store_15 are chosen as new sites.

# Display the analysis results in a map.

# Create a map of Visalia, California.
map2 = my_gis.map('San Francisco, CA')
map2.basemap = 'dark-gray'
map2
# Call custom function defined earlier in this notebook to 
# display the analysis results in the map.
visualize_locate_allocate_results(map2, result2, zoom_level=8)

The 8 chosen sites are symbolized with pizza symbols. The existing stores and the competitor stores are also shown as the centers of rays.

item_properties['title'] = "Location Allocation example of Pizza Stores in San Francisco (3)"

item = map2.save(item_properties)
print(item)
<Item title:"Location Allocation example of Pizza Stores in San Francisco (3)" type:Web Map owner:arcgis_python>

Example 4: Minimize Impedance in Async Manner

What is 'Minimize Impedance'?

Minimize Impedance — This is also known as the P-Median problem type. Facilities are located such that the sum of all weighted travel time or distance between demand points and solution facilities is minimized. (Weighted travel is the amount of demand allocated to a facility multiplied by the travel distance or time to the facility.) [5]

This problem type is traditionally used to locate warehouses, because it can reduce the overall transportation costs of delivering goods to outlets. Since Minimize Impedance reduces the overall distance the public needs to travel to reach the chosen facilities, the minimize impedance problem without an impedance cutoff is ordinarily regarded as more equitable than other problem types for locating some public-sector facilities such as libraries, regional airports, museums, department of motor vehicles offices, and health clinics.

The following list describes how the minimize impedance problem type handles demand:

  • A demand point that cannot reach any facilities due to setting a cutoff distance or time is not allocated.
  • A demand point that can only reach one facility has all its demand weight allocated to that facility.
  • A demand point that can reach two or more facilities has all its demand weight allocated to the nearest facility only.

What is Async?

Asynchronous and synchronous modes define how the application interacts with the service and gets the results. [3]

Synchronous execution mode: the application must wait for the request to finish and get the results. This execution mode is well-suited for requests that complete quickly (under 10 seconds).

Asynchronouse execution mode: the client must periodically check whether the service has finished execution and, once completed, retrieve the results. While the service is executing, the application is available to do other things. This execution mode is well-suited for requests that take a long time to complete because it allows users to continue to interact with the application while the results are generated.

Solving Problem

Here, in order to achieve the goal that the sum of all weighted travel distance between demand points and solution facilities is minimized, we need to set the impedance and measurement_units to Kilometers (Length measurements along roads are stored in kilometers and can be used for performing analysis based on shortest distance).

Also, future is set to True here to enable the tool to be run an asynchronous manner. When using synchronous network analysis services, you get back a Python dictionary with all the information you may need such as routes, stops, barriers, and directions. In contrast, when using asynchronous network analysis services, the result object returned is of ToolOutput type. This object has several properties associated with it.

if_async = True
result3 = network.analysis.solve_location_allocation(   problem_type='Minimize Impedance', 
                                                        facilities=candidate_facilities, 
                                                        demand_points=demand_points,
                                                        number_of_facilities_to_find=3,
                                                        travel_direction='Demand to Facility',
                                                        impedance="Kilometers",
                                                        measurement_units='Kilometers', 
                                                        default_measurement_cutoff=100,
                                                        future=if_async
                                                    )

When if_async is set to True, you are free to skip the following six cells for now, and go on to execute cells in the next section - The cell above will be running in back stage, and when you are done with the next section, you can come back at the cell below to check if the async process is finished.

If the printout results is Async job not done yet!, which means the backstage solver isn't finished at this point, you can again skip the five cells right after, and go on the execute cells in the section after the previously executed ones.

This workflow aims to perform parallel tasks at the wait time of the async process.

if if_async:
    if result3.done():
        result3 = result3.result()
        print("Async job done!")
    else:
        print("Async job not done yet!")
Async job not done yet!
print('Solve succeeded? {}'.format(result3.result().solve_succeeded))
Solve succeeded? True
# Display the analysis results in a table.
# Display the analysis results in a pandas dataframe.
result3.result().output_facilities.sdf[['Name', 'FacilityType', 'Weight','DemandCount', 
                                        'DemandWeight', 'TotalWeighted_Kilometers', 'Total_Kilometers']]
NameFacilityTypeWeightDemandCountDemandWeightTotalWeighted_KilometersTotal_Kilometers
0Store_101000.0000000.000000
1Store_201000.0000000.000000
2Store_301000.0000000.000000
3Store_401000.0000000.000000
4Store_501000.0000000.000000
5Store_601000.0000000.000000
6Store_7315050262.744533262.744533
7Store_1101000.0000000.000000
8Store_1201000.0000000.000000
9Store_1301000.0000000.000000
10Store_14317070227.969438227.969438
11Store_15318888280.409621280.409621
12Store_1601000.0000000.000000
13Store_1701000.0000000.000000
14Store_1801000.0000000.000000
15Store_1901000.0000000.000000

Looking at the FacilityType column of the output table above, we can see that Store_7, Store_14, and Store_15 are marked as numeric code 3 (Chosen) - which means these are the optimized selection after running the specified model.

What's worth mentioning here, is that now the Impedance is changed from time-based to distance-based, and hence the output fields no longer contain 'TotalWeighted_Minutes' and 'Total_Minutes', but instead 'TotalWeighted_Kilometers' and 'Total_Kilometers'.

# Display the analysis results in a map.

# Create a map of Visalia, California.
map3 = my_gis.map('San Francisco, CA')
map3.basemap = 'dark-gray'
map3
# Call custom function defined earlier in this notebook to 
# display the analysis results in the map.
visualize_locate_allocate_results(map3, result3.result(), zoom_level=8)
ObjectIDNameWeightAllocatedWeightGroupNameImpedanceTransformationImpedanceParameterSourceIDSourceOIDPosAlong...HOUSEHOLDSHSE_UNITSBUS_COUNTDemandOIDFacilityOIDCutoffBearingBearingTolNavLatencySHAPE
01060816029.0011NoneNoneNone170751960.000000...1679171511217NoneNoneNoneNone{"x": -122.48865310099995, "y": 37.65080723100...
12060816028.0011NoneNoneNone170650580.921931...148415065927NoneNoneNoneNone{"x": -122.48354988899996, "y": 37.65999776700...
23060816017.0011NoneNoneNone170771940.436193...129413135537NoneNoneNoneNone{"x": -122.45648442999999, "y": 37.66327196200...
34060816019.0011NoneNoneNone170811110.108167...3273333011847NoneNoneNoneNone{"x": -122.43424735199994, "y": 37.66238451900...
45060816025.0011NoneNoneNone170662110.569551...145914674457NoneNoneNoneNone{"x": -122.45118705799996, "y": 37.64021943800...

5 rows × 30 columns

item_properties['title'] = "Location Allocation example of Pizza Stores in San Francisco (4)"

item = map3.save(item_properties)
print(item)
<Item title:"Location Allocation example of Pizza Stores in San Francisco (4)" type:Web Map owner:arcgis_python>

A second solver in arcgis.features.find_locations module

Now let's look at a second solver in arcgis.features module that can also solve a location allocation problem, with a Feature service (input) to (Feature Service) approach. Parameters for this solver include:

  • goal: Required string. Specify the goal that must be satisfied when allocating demand locations to facilities. Choice list:[‘Allocate’, ‘MinimizeImpedance’, ‘MaximizeCoverage’, ‘MaximizeCapacitatedCoverage’, ‘PercentCoverage’]. Default value is ‘Allocate’.
  • travel_mode: Specify the mode of transportation for the analysis. Choice list: [‘Driving Distance’, ‘Driving Time’, ‘Rural Driving Distance’, ‘Rural Driving Time’, ‘Trucking Distance’, ‘Trucking Time’, ‘Walking Distance’, ‘Walking Time’]
%%time

result3b = find_locations.choose_best_facilities(goal="MinimizeImpedance",
                                                 demand_locations_layer=demand_points_fl,
                                                 travel_mode='Driving Distance',
                                                 travel_direction="DemandToFacility",
                                                 required_facilities_layer=existing_fl,
                                                 candidate_facilities_layer=candidate_fl,
                                                 candidate_count=3,
                                                 output_name="choose best facilities (5)")
Wall time: 2min 49s
print(result3b)
<Item title:"choose best facilities (5)" type:Feature Layer Collection owner:arcgis_python>
la_df.columns
Index(['OBJECTID', 'Demand', 'AllocatedDemand', 'ImpedanceTransformation',
       'ImpedanceParameter', 'SourceID', 'SourceOID', 'PosAlong', 'SideOfEdge',
       'SnapX', 'SnapY', 'SnapZ', 'DistanceToNetworkInMeters', 'DemandOID',
       'FacilityOID', 'Cutoff', 'Bearing', 'BearingTol', 'NavLatency',
       'Status', 'ID', 'NAME', 'STATE_NAME', 'AREA', 'POP2000', 'HOUSEHOLDS',
       'HSE_UNITS', 'BUS_COUNT', 'SHAPE'],
      dtype='object')
location_allocation_sublayer = FeatureLayer.fromitem(result3b, layer_id=1)
la_df = location_allocation_sublayer.query(where='1=1', as_df=True)

# filter only the required columns
la_df2 = la_df[['NAME','DistanceToNetworkInMeters','DemandOID', 'FacilityOID',
                'Status','SourceID','SourceOID','AllocatedDemand']]
la_df2
NAMEDistanceToNetworkInMetersDemandOIDFacilityOIDStatusSourceIDSourceOIDAllocatedDemand
0060816029.0017.12754918Ok170751961
1060816028.0021.49664828Not located on closest170650581
2060816017.0035.35319338Ok170771941
3060816019.004.48209848Ok170811111
4060816025.0016.18054458Ok170662111
...........................
203060750111.0024.6570352041Ok174056741
204060750122.0051.5488582051Ok174047451
205060750176.0130.5142902061Ok174110061
206060750178.0022.2934972071Ok174040221
207060750124.0021.4413202081Ok174045901

208 rows × 8 columns

# Display the analysis results in a map.

# Create a map of Visalia, California.
map3b = my_gis.map('San Francisco, CA')
map3b.basemap = 'dark-gray'
map3b.layout.height = '650px'
map3b.legend = True
map3b
Fig 2. Legend for the three layers in the returned Feature Service.

The layer (id=0) contains required, chosen, and unassigned facilities, which as shown in the legend are displayed as blue diamonds, green rectangles, and red rectangles. We can see three chosen pizza store sites here (from the candidates).

The layer (id=1) contains the demand locations, and those to be covered or capacitated by the required facilities are assigned blue points (as their symbologies), those being covered by chosen facilities are assigned as green points, while those not supported by any facilities will be in red points. Here, all census tracts are tendered, and hence so red points are seen.

The last layer (id=2) contains the allocation lines connecting facilities to demand points. Again, lines connecting required facilities with demand points are represented in blue lines, those connecting chosen facilities with demand points are in green lines.

for i in range(3):
    map3b.add_layer(result3b.layers[i])

In the last section, we have adopted a different method - arcgis.features.use_proximity.choose_best_facilities - in allocating locations. In doing so, we also explored the scenario with output_name specified (which forms a Feature Service to Feature Service user experience).

Conclusions

The ArcGIS Network Analysis module provides a versatile set of tools that assist strategic decision making to reduce the costs of travel. You have learned how the location-allocation analysis can be performed for four different problem types: maximize attendance, maximize market share, target market share and Minimize Impedance. However, examples shown in this part of guide only scratch the surface of the situations where you could use these tools.

By performing your analysis using Python and the Jupyter Notebook, you also document your methodology in a form that can be easily shared to colleagues and stake holders, enabling them to reproduce your results or repeat the analyses in an iterative fashion with different parameters or different input datasets. You can also export the output tables from the solve_location_allocation tool by to tables, maps, or other deliverables [2].

References

[1] "Location-allocation analysis layer", https://pro.arcgis.com/en/pro-app/help/analysis/networks/location-allocation-analysis-layer.htm, accessed on 10/09/2019

[2] "Identifying suitable sites for new ALS clinics using location allocation analysis", https://developers.arcgis.com/python/sample-notebooks/identifying-suitable-sites-for-als-clinics-using-location-allocation-analysis/, accessed 10/09/2019

[3] "Asynchronous network analysis services", https://developers.arcgis.com/python/guide/performing-network-analysis-tasks-asynchronously/, accessed on 10/09/2019

[4] "Exercise 9: Choosing optimal store locations using location-allocation", https://desktop.arcgis.com/en/arcmap/latest/extensions/network-analyst/exercise-9-choosing-optimal-store-sites-using-location-allocation.htm, accessed on 10/10/2019

[5] "SolveLocationAllocation", https://logistics.arcgis.com/arcgis/rest/directories/arcgisoutput/World/LocationAllocation_GPServer/World_LocationAllocation/SolveLocationAllocation.htm, accessed on 10/10/2019

Your browser is no longer supported. Please upgrade your browser for the best experience. See our browser deprecation post for more details.