Original set:
Create pseudo-copy (CNTRL-drag in TOC) of it and make spatial join one to many with clone. In this case I used distance 500m.
Output table:
Remove records from this table where PAR_ID = PAR_ID_1 - easy.
Iterate through table and remove records where (PAR_ID,PAR_ID_1
)=(PAR_ID_1, PAR_ID) of any record above it. Not so easy, use acrpy.
Calculate catchment centroids (UniqID=PAR_ID). They are nodes or network. Connect them by lines using spatial join table. This is separate topic surely covered somewhere on this forum.
The script below assumes that nodes table looks like that:
where MUID came from parcels, P2013 is field to summarise. In this case = 1 for counting only. [rcvnode] - script output to store group ID equal NODEREC of the first node in the group/cluster defined.
Links table structure with important fields highlighted
Times stores link/edge weight, i.e. cost of travel from node to node. Equal 1 in this case so that cost of travel to all the neighbours is the same. [fi] and [ti] are sequential number of connected nodes. To populate this table search this forum on how to assign from and to nodes to link.
Script customised for my own workbench mxd. Has to be modified, hardcoded with your naming of the fields and sources:
import arcpy, traceback, os, sys,time
import itertools as itt
scriptsPath=os.path.dirname(os.path.realpath(__file__))
os.chdir(scriptsPath)
import COMMON
sys.path.append(r'C:\Users\felix_pertziger\AppData\Roaming\Python\Python27\site-packages')
import networkx as nx
RATIO = int(arcpy.GetParameterAsText(0))
try:
def showPyMessage():
arcpy.AddMessage(str(time.ctime()) + " - " + message)
mxd = arcpy.mapping.MapDocument("CURRENT")
theT=COMMON.getTable(mxd)
FIND NODES LAYER
theNodesLayer = COMMON.getInfoFromTable(theT,1)
theNodesLayer = COMMON.isLayerExist(mxd,theNodesLayer)
GET LINKS LAYER
theLinksLayer = COMMON.getInfoFromTable(theT,9)
theLinksLayer = COMMON.isLayerExist(mxd,theLinksLayer)
arcpy.SelectLayerByAttribute_management(theLinksLayer, "CLEAR_SELECTION")
linksFromI=COMMON.getInfoFromTable(theT,14)
linksToI=COMMON.getInfoFromTable(theT,13)
G=nx.Graph()
arcpy.AddMessage("Adding links to graph")
with arcpy.da.SearchCursor(theLinksLayer, (linksFromI,linksToI,"Times")) as cursor:
for row in cursor:
(f,t,c)=row
G.add_edge(f,t,weight=c)
del row, cursor
pops=[]
pops=arcpy.da.TableToNumPyArray(theNodesLayer,("P2013"))
length0=nx.all_pairs_shortest_path_length(G)
nNodes=len(pops)
aBmNodes=[]
aBig=xrange(nNodes)
host=[-1]*nNodes
while True:
RATIO+=-1
if RATIO==0:
break
aBig = filter(lambda x: x not in aBmNodes, aBig)
p=itt.combinations(aBig, 2)
pMin=1000000
small=[]
for a in p:
S0,S1=0,0
for i in aBig:
p=pops[i][0]
p0=length0[a[0]][i]
p1=length0[a[1]][i]
if p0<p1:
S0+=p
else:
S1+=p
if S0!=0 and S1!=0:
sMin=min(S0,S1)
sMax=max(S0,S1)
df=abs(float(sMax)/sMin-RATIO)
if df<pMin:
pMin=df
aBest=a[:]
arcpy.AddMessage('%s %i %i' %(aBest,sMax,sMin))
if df<0.005:
break
lSmall,lBig,S0,S1=[],[],0,0
arcpy.AddMessage ('Ratio %i' %RATIO)
for i in aBig:
p0=length0[aBest[0]][i]
p1=length0[aBest[1]][i]
if p0<p1:
lSmall.append(i)
S0+=p0
else:
lBig.append(i)
S1+=p1
if S0<S1:
aBmNodes=lSmall[:]
for i in aBmNodes:
host[i]=aBest[0]
for i in lBig:
host[i]=aBest[1]
else:
aBmNodes=lBig[:]
for i in aBmNodes:
host[i]=aBest[1]
for i in lSmall:
host[i]=aBest[0]
with arcpy.da.UpdateCursor(theNodesLayer, "rcvnode") as cursor:
i=0
for row in cursor:
row[0]=host[i]
cursor.updateRow(row)
i+=1
del row, cursor
except:
message = "\n*** PYTHON ERRORS *** "; showPyMessage()
message = "Python Traceback Info: " + traceback.format_tb(sys.exc_info()[2])[0]; showPyMessage()
message = "Python Error Info: " + str(sys.exc_type)+ ": " + str(sys.exc_value) + "\n"; showPyMessage()
Output example for 6 groups:
You'll need site package NETWORKX
http://networkx.github.io/documentation/development/install.html
Script takes required number of clusters as parameter (6 in above example). It is using nodes and links tables to make a graph with equal weight/distance of travel edges (Times=1). It considers combination of all nodes by 2 and calculates total of [P2013] in two groups of neighbours. When required ratio achieved, e.g. (6-1)/1 at first iteration, continues with reduced ratio target, i.e. 4, etc. till 1. Starting points are of huge importance so make sure your 'end' nodes are sitting on the top of your nodes table (sorting?) See first 3 groups in the example output. It helps to avoid 'branch cutting' at every next iteration.
Script customisation to work from mxd:
- you don't need import COMMON. It is my own thing, that reads my own environment table, where theNodesLayer, theLinksLayer, linksFromI, linksToI specified. Replace relevant lines with your own naming of nodes and links layers.
- Note that field P2013 can store anything, e.g. number of tenants or parcel area. If so you might cluster polygons to hold approximately equal number of person etc.