Sunday, 28 July 2013

Jenk's Natural Breaks for Classification

This script, with sample data, can be run to demonstrate how a set of values can be subdivided into categories, based upon the natural breaks (user specifies the number) within the data set.

'''GoodVarFit.py
Goodness of variance fit via Jenks Natural Breaks

Source:    http://danieljlewis.org/files/2010/06/Jenks.pdf
Original:  https://stat.ethz.ch/pipermail/r-sig-geo/2006-March/000811.html
Modifications: Dan Patterson July 2013
'''

def getJenksBreaks( dataList, numClass ):
  dataList.sort()
  mat1 = []
  for i in range(0,len(dataList)+1):
    temp = []
    for j in range(0,numClass+1):
      temp.append(0)
    mat1.append(temp)
  mat2 = []
  for i in range(0,len(dataList)+1):
    temp = []
    for j in range(0,numClass+1):
      temp.append(0)
    mat2.append(temp)
  for i in range(1,numClass+1):
    mat1[1][i] = 1
    mat2[1][i] = 0
    for j in range(2,len(dataList)+1):
      mat2[j][i] = float('inf')
  v = 0.0
  for l in range(2,len(dataList)+1):
    s1 = 0.0
    s2 = 0.0
    w = 0.0
    for m in range(1,l+1):
      i3 = l - m + 1
      val = float(dataList[i3-1])
      s2 += val * val
      s1 += val
      w += 1
      v = s2 - (s1 * s1) / w
      i4 = i3 - 1
      if i4 != 0:
        for j in range(2,numClass+1):
          if mat2[l][j] >= (v + mat2[i4][j - 1]):
            mat1[l][j] = i3
            mat2[l][j] = v + mat2[i4][j - 1]
    mat1[l][1] = 1
    mat2[l][1] = v
  k = len(dataList)
  kclass = []
  for i in range(0,numClass+1):
    kclass.append(0)
  kclass[numClass] = float(dataList[len(dataList) - 1])
  countNum = numClass
  while countNum >= 2:
    #print "rank = " + str(mat1[k][countNum])
    id = int((mat1[k][countNum]) - 2)
    #print "val = " + str(dataList[id])
    kclass[countNum - 1] = dataList[id]
    k = int((mat1[k][countNum] - 1))
    countNum -= 1
  return kclass


def getGVF( dataList, numClass ):
  '''
  The Goodness of Variance Fit (GVF) is found by taking the difference between the squared deviations
  from the array mean (SDAM) and the squared deviations from the class means (SDCM), and dividing by the SDAM
  Returns:
    the class start, end, average and values from which they were determined
  '''
  breaks = getJenksBreaks(dataList, numClass)
  dataList.sort()
  listMean = sum(dataList)/float(len(dataList))
  #print listMean
  SDAM = 0.0
  for i in range(0,len(dataList)):
    sqDev = (dataList[i] - listMean)**2
    SDAM += sqDev
  SDCM = 0.0
  breaksData = []
  for i in range(0,numClass):
    if breaks[i] == 0:
      classStart = 0
    else:
      classStart = dataList.index(breaks[i])
      classStart += 1
    classEnd = dataList.index(breaks[i+1])
    classList = dataList[classStart:classEnd+1]
    classMean = sum(classList)/float(len(classList))
    breaksData.append( [classStart, classEnd, classMean, classList] )
    preSDCM = 0.0
    for j in range(0,len(classList)):
      sqDev2 = (classList[j] - classMean)**2
      preSDCM += sqDev2
    SDCM += preSDCM
    goodVarFit = (SDAM - SDCM)/SDAM
  return [breaksData, goodVarFit]

if __name__ == "__main__":
  dataList = [0,1,2,3, 10,11,12,13, 20,21,22,23, 30,31,32,33, 40,41,42,43,
              50,51,52,53,60,61,62,63,70,71,72,73,80,81,82,83,90,91,92,93,
              100,101,102,103,110,111,112,113]
  plotPoints = []
  for numClass in range(2,12):
    breaksData, goodVarFit = getGVF( dataList, numClass )
    breaks = len(breaksData)
    for i in range(0,breaks):
      print "\nBreak IDs (start, end): ", breaksData[i][0], breaksData[i][1]
      print "Class mean:   ", breaksData[i][2]
      print "Class values: ", breaksData[i][3]
    print "Goodness of Var Fit: ", goodVarFit
    plotPoints.append([numClass, goodVarFit])
  print "Final plot points: ", plotPoints

No comments:

Post a Comment