Website Personalization using IP Geolocation

In this HOWTO, we will walk through the process of converting an IP address to a geolocation. Many websites and applications can use geolocation in unique and interesting ways:

  • Apartment and housing websites can show nearby homes and apartments
  • Online retailers can showcase heavier clothing options such as sweaters and jackets in colder locations.
  • Political campaigns can have unique calls-to-action depending on the state.

Our Scenario

We will be building a widget to personalize the Infochimps website experience. At Infochimps, we want visitors on our Geo APIs page to have a unique experience based on their location. Our website widget will display the visitor's current location on a map and show the results of both the Digital Element IP Geolocation and IP Demographic API calls.

APIs used

Download the code

website-personalization.zip

App Overview

  • Retrieve the website visitor's geolocation -- Take the IP address and convert it to a geolocation using the Digital Element Geolocation API.
  • Retrieve census demographic information -- Takes the IP address and returns US Census information for that location using the American Community Survey API.
  • Display geolocation and census information -- Display data from our queries in tables and construct sentences describing the data results.
  • Display the visitor's geolocation on a map -- Takes the latitude and longitude from our geolocation query and generates a map with a marker at that location using the Google Maps API.

1) Retrieve the website visitor's geolocation

Our example Rails application contains one controller, two views, and a number of helper methods. The personalization_controller starts by requesting the remote IP address from the browser. With the IP address in hand, we can construct a query for the Digital Element geo data and place the result of the query in the @digital_element_result hash.

/app/controllers/personalization_controller.rb -- Lines 1-14

  require 'open-uri'

class PersonalizationController < ApplicationController

  # Your Infochimps API Key
  API_KEY = 'JimEngland-Hw3n_vzMB283_I1dXT7f9zgHU69'

  def widget
    @ip_address = params['ip'] || request.remote_ip

    #Digital Element Geo API query
    query = { :ip => @ip_address }
    url = 'http://api.infochimps.com/web/analytics/ip_mapping/digital_element/geo'
    @digital_element_result = query_api(url, query)

The private query_api method combines the request URL, the IP Address, and our Infochimps API Key, then sends the request using the open-uri wrapper and returns the parsed JSON response.

/app/controllers/personalization_controller.rb -- Lines 28-34

  private
  def query_api(url, query)
    query[:apikey] = API_KEY
    query_url = [url, query.to_query].join('?')
    JSON.parse open(query_url).read
    rescue
  end

2) Retrieve census demographic information

Next, we will send a similar request to the American Community Survey API. We have included an additional parameter: g.radius, which is the maximum distance from the call's geolocated point to return results. Because there can be multiple results, we will focus on the result which is closest to our geolocated point: @census_result['results'][0].

/app/controllers/personalization_controller.rb Lines 16-25

  #US Census Topline API geo query  
url = 'http://api.infochimps.com/social/demographics/us_census/topline/search'
query = { 'g.radius'=> 1000, 'g.ip_address'=> @ip_address }
@census_result = query_api(url, query)
@census_result = @census_result['results'][0] if @census_result && @census_result['results']

if @census_result.blank? || @digital_element_result.blank?
  # redirect to working ip for empty result
  redirect_to '/personalization/widget?ip=67.78.118.7', :status => 301
end

We have also included error handling to our application. If either of our queries returns no results, then we default the IP address to 67.78.118.7, an Austin, TX IP address that is known to have results.

3) Display geolocation and census information

We will place the results of the above queries in the widget.html.erb view. We have constructed two ways to view the information: 1) a data table and 2) sentences describing the data results. Our views use the http://blueprintcss.org/ for simple styling and positioning of elements.

/app/views/personalization/widget.html.erb -- Lines 9-53

  <nav>
  <ul class="bubble-tabs">
    <li id="geo-tab" class="selected">
      <a href="#">Geolocation</a>
    </li>
    <li id="census-tab">
      <a href="#">Census Data</a>
    </li>
  </ul>
</nav>

<div class="span-16">
  <table id="geo-table">
    <thead>
      <tr>
        <th>Field</th>
        <th>Data</th>
      </tr>
    </thead>
    <tbody>
      <% @digital_element_result.each do |key, value| %>
        <tr>
          <td><%= key %></td>
          <td><%= value %></td>
        </tr>      
      <% end %>
    </tbody>
  </table>
  <table id="census-table">
    <thead>
      <tr>
        <th>Field</th>
        <th>Data</th>
      </tr>
    </thead>
    <tbody>
      <% @census_result.each do |key, value| %>
        <tr>
          <td><%= key %></td>
          <td><%= value %></td>
        </tr>      
      <% end %>
    </tbody>
  </table>
</div>

We have built two HTML tables: #geo-table and #census-table that display the results of each respective query. By default, we show the geo table and hide the census table. Using jQuery, we have placed simple .click() functions on the #geo-tab and #census-tab navigation to show and hide the tables.

/app/views/personalization/widget.html.erb -- Lines 63-77

  <% content_for(:additional_javascripts) do %>
  <script type="text/javascript">
    $(function(){
      $('#geo-tab a').click(function(e){
        e.preventDefault();
        $('#').show(); $('#census-table').hide(); $('#geo-tab').addClass('selected');$('#census-tab').removeClass('selected');
      });
      $('#census-tab a').click(function(e){
        e.preventDefault();
        $('#geo-table').hide(); $('#census-table').show(); $('#geo-tab').removeClass('selected');$('#census-tab').addClass('selected');
      });
      $('#census-table').hide();
    })
  </script>
<% end %>

We have also included a number of sentences describing the data. The sentences are called in the widget view and generated using Rails helpers:

/app/views/personalization/widget.html.erb -- Lines 3-7

  <center>
  <h2><%= location_sentence(@digital_element_result) %></h2>
  <h4><%= income_level_sentence(@census_result['median_household_income']) %></h4>
  <h4><%= race_demographics_sentence(@census_result) %></h4>
</center>

The location_sentence helper combines the city and region into a sentence.

/app/helpers/personalization_helper.rb -- Lines 2-4

  def location_sentence(result)
   'Based on your IP address, we have located you in ' + result['city'].capitalize + ', ' + result['region'].upcase + '.'
end

The income_level_sentence helper takes the median_household_income variable from @census_result and customizes the sentence based on the income level.

/app/helpers/personalization_helper.rb Lines 6-22

  def income_level_sentence(income)
  #Create income level statement.
  sentence = "Households in this area earn "
  sentence += case income
  when 0...25000
    'a very low level of income'
  when 25000...50000
    'a low level of income'
  when 50000...75000
    'a medium level of income'
  when 75000...100000
    'a moderately high level of income'
  else
    'an affluent level of income'
  end
  sentence + " with a median household income of " + number_to_currency(income, :precision => 0) + '.'
end

The race_demographics_sentence helper takes the race percentage variables and constructs a sentence stating the gender split and the top three races that live in that area.

/app/helpers/personalization_helper.rb Lines 24-29

  def race_demographics_sentence(result)
   race_results = {"black"=>"percent_black", "mixed Race"=>"percent_mixed_race", "pacific"=>"percent_pacific", "native"=>"percent_native", "hispanic"=>"percent_race_hispanic", "asian"=>"percent_asian", "white"=>"percent_white"}
   race_results.each{ |title, result_key| race_results[title] = result[result_key].to_f }
   top_race_results = race_results.to_a.sort{|x,y| y[1] <=> x[1] }.map{|key_pair| key_pair[0]}
   "This area's population is #{gender_sentence_fragment(result['percent_female'])} and  is predominantly #{top_race_results.first} with strong portions of #{top_race_results.second} and #{top_race_results.third}."
end

In order to get the gender breakdown, race_demographics_sentence calls the gender_sentence_fragment helper with the percent_female variable. We have multiple adjectives depending on the level of gender skewness.

/app/helpers/personalization_helper.rb Lines 31-44

  def gender_sentence_fragment(percent_female)
  percent_male = 100 - percent_female
  modifier = case [percent_male, percent_female].max
  when 70..100 then 'extremely'
  when 65...70 then 'strongly'
  when 60...65 then 'moderately'
  when 55...60 then 'slightly'
  end
  if (45..55).include? percent_female
    "mostly balanced across gender"
  else
    "skewed #{modifier} #{percent_female > 50 ? 'female' : 'male'}"
  end
end

4) Display the visitor's geolocation on a map

Our final display option for the page is a small map. We initialize a new Google Map, center the map according to @digital_element_result['latitude'] and @digital_element_result['latitude'], and place a marker at that location as well.

/app/views/layouts/application.html.erb -- Lines 28-34

  <script type="text/javascript" src="http://maps.googleapis.com/maps/api/js?sensor=true"></script>
<script type="text/javascript">
  function initialize() {
    var latlng = new google.maps.LatLng( <%= @digital_element_result['latitude'] %>, <%= @digital_element_result['longitude'] %> );
    var myOptions = {
      zoom: 5,
      center: latlng,
      mapTypeId: google.maps.MapTypeId.ROADMAP
    };
    var map = new google.maps.Map(document.getElementById("map_canvas"), myOptions);
    var marker = new google.maps.Marker({ position: latlng, map: map });
  }
  window.onload = initialize
    </script>
<%= yield :additional_javascripts %>

The map is then displayed on line 56 of widget.html.erb:

/app/views/personalization/widget.html.erb -- Line 56

  <div id="map_canvas" style="width:300px;height:260px"></div>