Geographic Center of Zipcodes

At the last meeting I took down everyone’s zipcodes, in order to  determine the geographic center of the members present. The goal was to determine (roughly!) where a better group project meeting site would need to be located if we move away from bRainSilo.

There were only 14 people present, though many regular members were there. I added Fred to the list, even though he was not present.  I could not easily find an online tool that would determine the center of a set of zipcodes, so I wrote my own using a combination of a geonames.org api and a google maps api and Python.  I basically find the latitude and longitude of each address or centroid of each zipcode; average them all, ignoring spherical distortion since this is a pretty small patch of the Earth; then convert that back into an address.

(Turns out the google maps api does not give the official census bureau zipcode centroid, so I use geonames’ api when all I have is a zipcode.  If I had a full address, I used the google maps api.)

Anyway, the center is surprisingly central east-west but further south than I expected. It’s within 2 miles of PCC Sylvania and also 2 miles north of the I-5 and Hwy217 interchange.

Also interesting is that even though google map’s idea of the center of each zipcode is different from the official center in most cases, the errors all seem to average out, so the address I get just using google’s api is within 0.1 miles of the more accurate one.

#
# center_of_zipcodes.py
#
# find address corresponding to center of a set of zipcodes using google maps api
# written by Pete Skeggs, 4/5/2011, for PARTS
# updated 4/13/2011 to use geonames.org API to get true census bureau centroids when just given a zipcode; also, use an address when known and look that up with the google maps api
#
import httplib
import json
import urllib

# define functions

def ztoll(z):
   conn = httplib.HTTPConnection("api.geonames.org")
   # note: you need to replace 'demo' below with a valid username; I decided not
   # to share mine
   url = "/postalCodeLookupJSON?postalcode=%s&country=US&username=demo" % z
   conn.request("GET", url)
   r = conn.getresponse()
   o = json.load(r)
   c = {'lat' : 0, 'lng' : 0}
   # format returned:
   #{"postalcodes":[{"adminName2":"Multnomah","adminCode2":"051","postalcode":"97210","adminCode1":"OR","countryCode":"US",
   # "lng":-122.703348,"placeName":"Portland","lat":45.530318,"adminName1":"Oregon"}]}
   c['lat'] = o[u'postalcodes'][0][u'lat']
   c['lng'] = o[u'postalcodes'][0][u'lng']
   c['addr'] = z
   return c

def addrtoll(addr):
   conn = httplib.HTTPConnection("maps.googleapis.com")
   url = "/maps/api/geocode/json?address=%s&sensor=false" % urllib.quote_plus(addr)
   conn.request("GET", url)
   r = conn.getresponse()
   o = json.load(r)
   c = {'lat' : 0, 'lng' : 0}
   # format returned:
   # { "status": "OK", "results": [{"types":["postal_code"],"formatted_address":"Portland, OR 97210, USA",
   # "address_components":[{"long_name":"97210","short_name":"97210","types":["postal_code"]},
   # {"long_name":"Portland","short_name":"Portland","types":["locality","political"]},
   # {"long_name":"Oregon","short_name":"OR","types":["administrative_area_level_1","political"]},
   # {"long_name":"UnitedStates","short_name":"US","types":["country","political"]}],
   #"geometry":{"location":{"lat":45.5552753,"lng":-122.7384041},"location_type":"APPROXIMATE",
   #"viewport":{"southwest":{"lat":45.5158719,"lng":-122.7897450},"northeast":{"lat":45.5858230,"lng":-122.6920030}},
   #"bounds":{"southwest":{"lat":45.5158719,"lng":-122.7897450},"northeast":{"lat":45.5858230,"lng":-122.6920030}}}}]}
   c['lat'] = o[u'results'][0][u'geometry'][u'location'][u'lat']
   c['lng'] = o[u'results'][0][u'geometry'][u'location'][u'lng']
   c['addr'] = o[u'results'][0][u'formatted_address']
   return c

def lltoz(lat, lng):
   conn = httplib.HTTPConnection("maps.googleapis.com")
   url = "/maps/api/geocode/json?latlng=%f,%f&sensor=false" % (lat, lng)
   conn.request("GET", url)
   r = conn.getresponse()
   o = json.load(r)
   addr = o[u'results'][0][u'formatted_address']
   return addr

# define variables

# list of zipcodes for PARTS members at April 2011 meeting
addresses = [
"97007",
"97210",
"97223",
"97034",
"97202",
"97232",
"97224",
"97071",
"97219",
"97086",
"97224",
"97211",
"97006",
"97224",
"97225",
"97229"]
lat = 0
lng = 0
count = 0

# main code

for a in addresses:
   if len(a) == 5:
      c = ztoll(a)
   else:
      c = addrtoll(a)
   print "%d. %s: %f, %f" % (count+1, c['addr'], c['lat'], c['lng'])
   lat = lat + c['lat']
   lng = lng + c['lng']
   count = count + 1

lat = lat / count
lng = lng / count
print "average of %d zipcodes: lat %f, lng %f" % (count, lat, lng)
print "address at center: %s" % lltoz(lat, lng)

This yields the following output (using a valid geonames.org username — you need to register for one of your own):

~$ python center_of_zipcodes.py
1. 97007: 45.450489, -122.865171
2. 97210: 45.530318, -122.703348
3. 97223: 45.447390, -122.795294
4. 97034: 45.409263, -122.684721
5. 97202: 45.484007, -122.636534
6. 97232: 45.528712, -122.636310
7. 97224: 45.409448, -122.801400
8. 97071: 45.144617, -122.858342
9. 97219: 45.457956, -122.707380
10. 97086: 45.444600, -122.537200
11. 97224: 45.409448, -122.801400
12. 97211: 45.565259, -122.644815
13. 97006: 45.520130, -122.860376
14. 97224: 45.409448, -122.801400
15. 97225: 45.498473, -122.778659
16. 97229: 45.548317, -122.827561
average of 16 zipcodes: lat 45.453617, lng -122.746244
address at center: 6655 SW Kingsview Ct, Tigard, OR 97223, USA
~$

Author: plskeggs

President of PARTS...

1 thought on “Geographic Center of Zipcodes”

  1. Very interesting GIS analysis. I would have thought a stronger pull from the Washington county area. But, PCC Sylvania is not a bad location – maybe they would allow us to use one of their shop classrooms, or even make a class out of it to draw people in wanting to earn credits and work on robots – though that is probably too much administrative overhead.

    Btw.
    My home zipcode is 97229, and I am temporarily in 77002.

Leave a Reply

Your email address will not be published. Required fields are marked *