Capstone Project - The Battle of the Neighborhoods (Week 2)

Applied Data Science Capstone by IBM/Coursera

Table of contents

Introduction: Business Problem

In this project we will try to find an optimal location for a breakfast point. Specifically, this report will be targeted to stakeholders interested in opening a breakfast spot near richmond circle in Bangalore.

Since there are lots of breakfast spots and eating joints near Richmond Circle, we will try to detect locations that are not already crowded with breakfast spots. We are also particularly interested in areas with no breakfast spots in the vicinity. We would also prefer locations as close to richmond cirle as possible, assuming that first two conditions are met.

We will use our data science powers to generate a few most promissing neighborhoods based on this criteria. Advantages of each area will then be clearly expressed so that best possible final location can be chosen by stakeholders.

Data

Based on definition of our problem, factors that will influence our decission are:

  • number of existing breakfast spots in the neighborhood
  • number of and distance to breakfast spots in the neighborhood, if any
  • distance of neighborhood from richmond circle

We decided to use regularly spaced circular grids of locations, centered around Richmond Circle center, to define our neighborhoods.

Following data sources will be needed to extract/generate the required information:

  • centers of candidate areas will be generated algorithmically and approximate addresses of centers of those areas will be obtained using Google Maps API reverse geocoding
  • number of breakfast spots and their type and location in every neighborhood will be obtained using Foursquare API
  • coordinate of center will be obtained using Google Maps API geocoding of well known Richmond Town in Bangalore.

Neighborhood Candidates

Let’s create latitude & longitude coordinates for centroids of our candidate neighborhoods. We will create a grid of cells covering our area of interest which is aprox. 12x12 killometers centered around Richmond Circle, Bangalore.

Let’s first find the latitude & longitude of Richmond Circle, using specific, well known address and Google Maps geocoding API.

# The code was removed by Watson Studio for sharing.
# place to create data sets for neighborhood
# Richmond Circle Under Fly Over, Bengaluru, Karnataka, 

google_api_key='AIzaSefcBBchangedmqxX1a41u4forsecuritylPlVreasomnsGs'
import requests

def get_coordinates(api_key, address, verbose=False):
    try:
        url = 'https://maps.googleapis.com/maps/api/geocode/json?key={}&address={}'.format(api_key, address)
        response = requests.get(url).json()
        if verbose:
            print('Google Maps API JSON result =>', response)
        results = response['results']
        geographical_data = results[0]['geometry']['location'] # get geographical coordinates
        lat = geographical_data['lat']
        lon = geographical_data['lng']
        return [lat, lon]
    except:
        return [None, None]
    
address = 'Richmond Circle Under Fly Over, Bengaluru, Karnataka'
richmond_circle = get_coordinates(google_api_key, address)
print('Coordinate of {}: {}'.format(address, richmond_circle))
Coordinate of Richmond Circle Under Fly Over, Bengaluru, Karnataka: [12.9644176, 77.5968593]

Now let’s create a grid of area candidates, equaly spaced, centered around city center and within ~3km from Richmond Circle. Our neighborhoods will be defined as circular areas with a radius of 300 meters, so our neighborhood centers will be 600 meters apart. Reason of chosing 3 km vicinity was because of the limit on calls that can be made to FourSquare API for number of generated neighborhoods.

To accurately calculate distances we need to create our grid of locations in Cartesian 2D coordinate system which allows us to calculate distances in meters (not in latitude/longitude degrees). Then we’ll project those coordinates back to latitude/longitude degrees to be shown on Folium map. So let’s create functions to convert between WGS84 spherical coordinate system (latitude/longitude degrees) and UTM Cartesian coordinate system (X/Y coordinates in meters).

#!pip install shapely
import shapely.geometry

#!pip install pyproj
import pyproj

import math

def lonlat_to_xy(lon, lat):
    proj_latlon = pyproj.Proj(proj='latlong',datum='WGS84')
    proj_xy = pyproj.Proj(proj="utm", zone=33, datum='WGS84')
    xy = pyproj.transform(proj_latlon, proj_xy, lon, lat)
    return xy[0], xy[1]

def xy_to_lonlat(x, y):
    proj_latlon = pyproj.Proj(proj='latlong',datum='WGS84')
    proj_xy = pyproj.Proj(proj="utm", zone=33, datum='WGS84')
    lonlat = pyproj.transform(proj_xy, proj_latlon, x, y)
    return lonlat[0], lonlat[1]

def calc_xy_distance(x1, y1, x2, y2):
    dx = x2 - x1
    dy = y2 - y1
    return math.sqrt(dx*dx + dy*dy)

print('Coordinate transformation check')
print('-------------------------------')
print('Richmond Circle longitude={}, latitude={}'.format(richmond_circle[1], richmond_circle[0]))
x, y = lonlat_to_xy(richmond_circle[1], richmond_circle[0])
print('Richmond Circle UTM X={}, Y={}'.format(x, y))
lo, la = xy_to_lonlat(x, y)
print('Richmond Circle longitude={}, latitude={}'.format(lo, la))
Coordinate transformation check
-------------------------------
Richmond Circle longitude=77.5968593, latitude=12.9644176
Richmond Circle UTM X=8889780.034270031, Y=2965060.0774481003
Richmond Circle longitude=77.59685929994781, latitude=12.964417600002193

Let’s create a hexagonal grid of cells: we offset every other row, and adjust vertical row spacing so that every cell center is equally distant from all it’s neighbors.

richmond_circle_x, richmond_circle_y = lonlat_to_xy(richmond_circle[1], richmond_circle[0]) # City center in Cartesian coordinates

k = math.sqrt(3) / 2 # Vertical offset for hexagonal grid cells
x_min = richmond_circle_x - 3000
x_step = 600
y_min = richmond_circle_y - 3000 - (int(21/k)*k*600 - 12000)/2
y_step = 600 * k 

latitudes = []
longitudes = []
distances_from_center = []
xs = []
ys = []
for i in range(0, int(21/k)):
    y = y_min + i * y_step
    x_offset = 300 if i%2==0 else 0
    for j in range(0, 21):
        x = x_min + j * x_step + x_offset
        distance_from_center = calc_xy_distance(richmond_circle_x, richmond_circle_y, x, y)
        if (distance_from_center <= 6001):
            lon, lat = xy_to_lonlat(x, y)
            latitudes.append(lat)
            longitudes.append(lon)
            distances_from_center.append(distance_from_center)
            xs.append(x)
            ys.append(y)

print(len(latitudes), 'candidate neighborhood centers generated.')
244 candidate neighborhood centers generated.

Let’s visualize the data we have so far: city center location and candidate neighborhood centers:

#!pip install folium

import folium
map_richmond = folium.Map(location=richmond_circle, zoom_start=13)
folium.Marker(richmond_circle, popup='Richmond Circle').add_to(map_richmond)
for lat, lon in zip(latitudes, longitudes):
    #folium.CircleMarker([lat, lon], radius=2, color='blue', fill=True, fill_color='blue', fill_opacity=1).add_to(map_richmond) 
    folium.Circle([lat, lon], radius=300, color='blue', fill=False).add_to(map_richmond)
    #folium.Marker([lat, lon]).add_to(map_richmond)
map_richmond

OK, we now have the coordinates of centers of neighborhoods/areas to be evaluated, equally spaced (distance from every point to it’s neighbors is exactly the same) and within ~3km from Richmond Circle.

Let’s now use Google Maps API to get approximate addresses of those locations.


def get_address(api_key, latitude, longitude, verbose=False):
    try:
        url = 'https://maps.googleapis.com/maps/api/geocode/json?key={}&latlng={},{}'.format(api_key, latitude, longitude)
        response = requests.get(url).json()
        if verbose:
            print('Google Maps API JSON result =>', response)
        results = response['results']
        address = results[0]['formatted_address']
        return address
    except:
        return None

addr = get_address(google_api_key, richmond_circle[0], richmond_circle[1])
print('Reverse geocoding check')
print('-----------------------')
print('Address of [{}, {}] is: {}'.format(richmond_circle[0], richmond_circle[1], addr))
Reverse geocoding check
-----------------------
Address of [12.9644176, 77.5968593] is: Richmond Circle (Under Fly Over), Langford Gardens, Bengaluru, Karnataka 560025, India
print('Obtaining location addresses: ', end='')
addresses = []
for lat, lon in zip(latitudes, longitudes):
    address = get_address(google_api_key, lat, lon)
    if address is None:
        address = 'NO ADDRESS'
    address = address.replace(', India', '') # We don't need country part of address
    addresses.append(address)
    print(' .', end='')
print(' done.')
Obtaining location addresses:  . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . done.
addresses[150:170]
['72/1, Shanthala Nagar, Sampangi Rama Nagar, Bengaluru, Karnataka 560001',
 '65, St Marks Rd, Shanthala Nagar, Ashok Nagar, Bengaluru, Karnataka 560001',
 'P Block, Field Marshal Cariappa Rd, Shanthala Nagar, Ashok Nagar, Bengaluru, Karnataka 560025',
 '74, Sumero Towers, Brigade Rd, Ashok Nagar, Richmond Town, Opp St Patricks, Bengaluru, Karnataka 560025',
 '27, Anthony Nicholas St, Ashok Nagar, Bengaluru, Karnataka 560025',
 '204 Raheja Plaza, 17, Commissariat Rd, Bengaluru, 560025',
 '11, M.G. Rd, Corporation Quarters, Victoria Layout, Bengaluru, Karnataka 560001',
 '95, 1st Cross Rd, Victoria Layout, Bengaluru, Karnataka 560047',
 'Lower Agaram Road, Victoria Layout, Agram, Bengaluru, Karnataka 560007',
 'Unnamed Road, Nagashettyhalli, Agram, Bengaluru, Karnataka 560007',
 '106, 1st Floor, Vikasa Soudha, Dr Ambedkar Veedhi, Bengaluru, Karnataka 560001',
 'Vidhana Soudha, Ambedkar Veedhi, Sampangi Rama Nagar, Bengaluru, Karnataka 560001',
 'High Court of Karnataka, Ambedkar Veedhi, Sampangi Rama Nagar, Bengaluru, Karnataka 560001',
 'Unnamed Road, Ambedkar Veedhi, Sampangi Rama Nagar, Bengaluru, Karnataka 560001',
 'D18, 4th Floor, Golden Orchid Apartments, Kasturba Road, Bangalore., Bengaluru, Karnataka 560001',
 '39, St Marks Rd, Shanthala Nagar, Ashok Nagar, Bengaluru, Karnataka 560001',
 '204, Raheja Chambers-12, Museum Rd, Bengaluru, Karnataka 560001',
 'G-6, Mota Royal Arcade, Brigade Road, Bengaluru, Karnataka 560001',
 '74/4, Ashok Nagar, 74/4, 3rd Cross Rd, Ashok Nagar, Bengaluru, Karnataka 560025',
 '8, Magrath Rd, Ashok Nagar, Bengaluru, Karnataka 560025']

Looking good. Let’s now place all this into a Pandas dataframe.

import pandas as pd

df_locations = pd.DataFrame({'Address': addresses,
                             'Latitude': latitudes,
                             'Longitude': longitudes,
                             'X': xs,
                             'Y': ys,
                             'Distance from center': distances_from_center})

df_locations.head(10)

…and let’s now save/persist this data into local file.

df_locations.to_pickle('./locations.pkl')    

Foursquare

Now that we have our location candidates, let’s use Foursquare API to get info on breakfast spots in each neighborhood.

We’re interested in venues in ‘nightlife’ category, ‘shop and services’ category and ‘breakfast spot’ category. So we will include in out list only venues that have these in category name. Finding out the number os spots for all these mentioned categories will help in instilling more confidence in stakeholders regarding the choice of business they want to start with.

Foursquare credentials are defined in hidden cell bellow.

# The code was removed by Watson Studio for sharing.
foursquare_client_id, foursquare_client_secret='154JNGBAO4somethingS15NUS5HchangedCTHORGJMWKOF','3B0JXThisCLFKNZGGME4MalsoZCA0changed2LST3KCKKC'
# Category IDs corresponding to Italian breakfast spots were taken from Foursquare web site (https://developer.foursquare.com/docs/resources/categories):

food_category = '4d4b7105d754a06374d81259' # 'Root' category for all food-related venues

def is_restaurant(categories, specific_filter=None):
    restaurant_words = ['restaurant', 'diner', 'taverna', 'steakhouse']
    restaurant = False
    specific = False
    for c in categories:
        category_name = c[0].lower()
        category_id = c[1]
        for r in restaurant_words:
            if r in category_name:
                restaurant = True
        if 'fast food' in category_name:
            restaurant = False
        if not(specific_filter is None) and (category_id in specific_filter):
            specific = True
            restaurant = True
    return restaurant, specific

def get_categories(categories):
    return [(cat['name'], cat['id']) for cat in categories]

def format_address(location):
    address = ', '.join(location['formattedAddress'])
    address = address.replace(', India', '')
    return address

def get_venues_near_location(lat, lon, category, client_id, client_secret, radius=500, limit=100):
    version = '20180724'
    url = 'https://api.foursquare.com/v2/venues/explore?client_id={}&client_secret={}&v={}&ll={},{}&categoryId={}&radius={}&limit={}'.format(
        client_id, client_secret, version, lat, lon, category, radius, limit)
    try:
        results = requests.get(url).json()['response']['groups'][0]['items']
        venues = [(item['venue']['id'],
                   item['venue']['name'],
                   get_categories(item['venue']['categories']),
                   (item['venue']['location']['lat'], item['venue']['location']['lng']),
                   format_address(item['venue']['location']),
                   item['venue']['location']['distance']) for item in results]        
    except:
        venues = []
    return venues
# Try getting venues as any one category to test the get_venues_near_any_location function
venues = get_venues_near_location(richmond_circle[0], richmond_circle[1], '4bf58dd8d48988d143941735', foursquare_client_id, foursquare_client_secret, radius=350, limit=100)
len(venues)
21
# Let's now go over our neighborhood locations and get nearby breakfast spots; we'll also maintain a dictionary of all found breakfast spots.

import pickle


def get_venues(lats, lons, category_id):
    allvenues={}
    location_venues=[]
    
    print('Obtaining venues around candidate locations:', end='')
    for lat, lon in zip(lats, lons):
        # Using radius=350 to meke sure we have overlaps/full coverage so we don't miss any restaurant (we're using dictionaries to remove any duplicates resulting from area overlaps)
        venues = get_venues_near_location(lat, lon, category_id, foursquare_client_id, foursquare_client_secret, radius=350, limit=100)
        area_venues = []
        for venue in venues:
            venue_id = venue[0]
            venue_name = venue[1]
            venue_categories = venue[2]
            venue_latlon = venue[3]
            venue_address = venue[4]
            venue_distance = venue[5]
            #is_res, is_italian = is_restaurant(venue_categories, specific_filter=italian_restaurant_categories)
            
            #if is_res:
            x, y = lonlat_to_xy(venue_latlon[1], venue_latlon[0])
            venue_detail = (venue_id, venue_name, venue_latlon[0], venue_latlon[1], venue_address, venue_distance, x, y)
            if venue_distance<=300:
                area_venues.append(venue_detail)
                
            allvenues[venue_id] = venue_detail
                
        location_venues.append(area_venues)
        
        print(' .', end='')
    print(' done.')
    
    return allvenues, location_venues

# Try to load from local file system in case we did this before

night_life_category='4d4b7105d754a06376d81259'
night_life_venues={}
location_night_lives=[]
night_life_file_name='night_life.pkl'

shop_n_service_category='4d4b7105d754a06378d81259'
shop_n_service_venues={}
location_shop_n_service=[]
shop_n_service_file_name='shop_n_service.pkl'

breakfast_spot_category='4bf58dd8d48988d143941735'
breakfast_spots={}
location_breakfast_stops=[]
breakfast_spot_file_name='breakfast_spot.pkl'


venue_categories=[night_life_file_name, shop_n_service_file_name, breakfast_spot_file_name]

for venue_category in venue_categories:

    loaded = False
    
    if 'night_life' in venue_category:
        
        try:

            with open(night_life_file_name, 'rb') as f:
                night_life_venues = pickle.load(f)
            
            with open('location_'+night_life_file_name, 'rb') as f:
                location_night_lives = pickle.load(f)
                
            print('Nightlife spots data loaded.')
            loaded = True
        except:
            pass

        # If load failed use the Foursquare API to get the data
        if not loaded:
            night_life_venues, location_night_lives = get_venues(latitudes, longitudes, night_life_category)

            # Let's persists this in local file system
            with open(night_life_file_name, 'wb') as f:
                pickle.dump(night_life_venues, f)
            
            with open('location_'+night_life_file_name, 'wb') as f:
                pickle.dump(location_night_lives, f)
                
            print('Nightlife spots data obtained.')
                
    elif 'shop_n_service' in venue_category:
        
        try:

            with open(shop_n_service_file_name, 'rb') as f:
                shop_n_service_venues = pickle.load(f)
            
            with open('location_'+shop_n_service_file_name, 'rb') as f:
                location_shop_n_service = pickle.load(f)
                
            print('Shop and Service spots data loaded.')
            loaded = True
        except:
            pass

        # If load failed use the Foursquare API to get the data
        if not loaded:
            shop_n_service_venues, location_shop_n_service = get_venues(latitudes, longitudes, shop_n_service_category)

            # Let's persists this in local file system
            with open(shop_n_service_file_name, 'wb') as f:
                pickle.dump(shop_n_service_venues, f)
            
            with open('location_'+shop_n_service_file_name, 'wb') as f:
                pickle.dump(shop_n_service_venues, f)
            
            print('Shop and Service spots data obtained.')
                
    elif 'breakfast_spot' in venue_category:
        
        try:

            with open(breakfast_spot_file_name, 'rb') as f:
                breakfast_spots = pickle.load(f)
            
            with open('location_'+breakfast_spot_file_name, 'rb') as f:
                location_breakfast_stops = pickle.load(f)
                
            print('Breakfast spots data loaded.')
            loaded = True
        except:
            pass

        # If load failed use the Foursquare API to get the data
        if not loaded:
            breakfast_spots, location_breakfast_stops = get_venues(latitudes, longitudes, breakfast_spot_category)

            # Let's persists this in local file system
            with open(breakfast_spot_file_name, 'wb') as f:
                pickle.dump(breakfast_spots, f)
            
            with open('location_'+night_life_file_name, 'wb') as f:
                pickle.dump(location_breakfast_stops, f)
            
            print('Breakfast spots data obtained.')
Obtaining venues around candidate locations: . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. done.  Nightlife spots data obtained.  Obtaining venues around candidate
locations: . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . done.  Shop and Service spots
data obtained.  Obtaining venues around candidate locations: . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . done.  Breakfast spots data obtained.
import numpy as np

print('Total number of Night Life spots:', len(night_life_venues))
print('Average number of night life spots in neighborhood:', np.array([len(r) for r in location_night_lives]).mean())

print('Total number of Shop and Service venues:', len(shop_n_service_venues))
print('Average number of  Shop and Service venues in neighborhood:', np.array([len(r) for r in location_shop_n_service]).mean())

print('Total number of breakfast spots:', len(breakfast_spots))
print('Average number of breakfast spots in neighborhood:', np.array([len(r) for r in location_breakfast_stops]).mean())

Total number of Night Life spots: 126
Average number of night life spots in neighborhood: 1.5327868852459017
Total number of Shop and Service venues: 575
Average number of  Shop and Service venues in neighborhood: 6.19672131147541
Total number of breakfast spots: 237
Average number of breakfast spots in neighborhood: 3.040983606557377
print('-----------------------')
print('List of all night life spots')
print('-----------------------')
for r in list(night_life_venues.values())[:10]:
    print(r)
print('...')
print('Total:', len(night_life_venues))

print('-----------------------')
print('List of all shop n service  spots')
print('-----------------------')
for r in list(shop_n_service_venues.values())[:10]:
    print(r)
print('...')
print('Total:', len(shop_n_service_venues))

print('-----------------------')
print('List of all breakfast spots')
print('-----------------------')
for r in list(breakfast_spots.values())[:10]:
    print(r)
print('...')
print('Total:', len(breakfast_spots))


-----------------------
List of all night life spots
-----------------------
('4c6d573b46353704a7c108bc', 'Upper Deck', 12.956464343758645, 77.58047295589992, 'Pai Viceroy, JC Road, Bangalore, Karnātaka', 185, 8887228.903905662, 2962006.407890346)
('50ade477e4b05090c0d41a59', 'Costarica bar', 12.954587112670714, 77.5796814750655, 'India', 257, 8887239.22279178, 2961555.8225103826)
('586bccb49e35d309bfc5d559', 'Steaming Mugs', 12.953452, 77.580383, 'Bangalore, Karnātaka', 240, 8887480.459819702, 2961386.891416425)
('516fb513d86c1212c3ab08b8', 'Beetle Juice Bar', 12.95649237204395, 77.58352875709534, 'No. 19, H-siddiah Road, Bengaluru - 560 027. (Opposite Lions eye hospital), Bangalore 560002, Karnātaka', 302, 8887834.914104737, 2962280.2489024573)
('5780158c498e010ca9aede58', 'Wilson Garden Club', 12.947919, 77.59464, 'Wilson Garden,, Bangalore 560027, Karnātaka', 278, 8890815.39193308, 2961514.372607275)
('4e82d2e7f5b9796ec72c3930', 'MAY FLOWER The Business Hotel', 12.948275956165299, 77.59791346727442, 'Wilson Garden (11 th cross), Bangalore 560027, Karnātaka', 245, 8891435.64543502, 2961874.2861979306)
('5116689fe4b0665cdfbe07d0', 'Sabharwal Inn', 12.948541042700468, 77.59795263658775, 'Wilson Garden, Bangalore, Karnātaka', 221, 8891419.717583893, 2961931.5761284996)
('4ed60944f9f4bb3148003a37', 'Mico Road Bar and Restaurant - Adugodi', 12.943908635722837, 77.60631680488586, 'Adugodi, Nanjappa Layout, Opposite Munichinnapa School Ground (New Mico Road), Bangalore 560030, Karnātaka', 316, 8893501.269093152, 2961724.968951109)
('4f1c3feae4b0e6badc6e4069', 'Hd-3', 12.94397711067887, 77.60781727491764, 'India', 254, 8893794.19099121, 2961870.686594795)
('50463854e4b05fe83d3c8367', 'Royal Stag', 12.943296432495117, 77.60840606689453, '#3, 1st Main (Temple Street , Adugodi), Bangalore 560029, Karnātaka', 280, 8893972.508485982, 2961784.089911972)
...
Total: 126
-----------------------
List of all shop n service  spots
-----------------------
('56f27903498e3e22fd0d7837', 'car n style', 12.956987, 77.580601, 'Jc Road, Bangalore, Karnātaka', 139, 8887207.642323775, 2962123.758324489)
('4fa163f7e4b0cca3845773f9', 'Arjun Jewellers', 12.953929340923576, 77.57925054218948, 'Avenue Road (Opp.to Raja Market,3rd Floor), Bangalore, Karnātaka', 320, 8887212.243044712, 2961384.464422502)
('5be156ecc824ae002c355b5f', 'Apollo Pharmacy', 12.9536558, 77.5789871, 'No G/15, Susheela Road, Near Karnataka Bank (Mavalli), Bangalore 560004, Karnātaka', 265, 8887184.247512784, 2961305.8145005596)
('54096179498e5eb221b226d2', 'Melange India', 12.955462484810122, 77.57671594619751, '# 51,Raja Hariharan Tower, 1st Floor, Near Basappa Circle,V.V.Puram Bengaluru, Karnataka (vishveshwarapuram), Bangalore 560004, Karnātaka', 308, 8886570.405874606, 2961473.4014179735)
('4e5630991495eb38e20433a6', 'Digicomp Complete Solution Limited', 12.953743934631348, 77.57772064208984, '#53-58, Sajjan Rao Circle, Sri Chakravarthi Complex (3rd Floor, V V Puram), Bangalore 560004, Karnātaka', 316, 8886924.166694779, 2961212.616168484)
('50dd656be4b07dd33c316797', 'Moto Cross', 12.958256855222272, 77.58154510093637, 'India', 281, 8887282.021518294, 2962464.420589429)
('531479e411d2c9a72389b62e', 'Motolab', 12.954173503399286, 77.58415860259547, 'Krumbigal Road, Bangalore 560019, Karnātaka', 315, 8888167.831312992, 2961864.6730010994)
('58bd4e55b3cdc84ffe6c001f', 'Automobi Book Bike Car Service Online', 12.951954562835663, 77.5821353495121, 'E-83, Rangappa Street (Chikkamavalli), Bangalore 560004, Karnātaka', 333, 8887963.392568622, 2961236.5674549565)
('5ad1edbdb1ec135abffc81b0', 'Zuby Auto Electricals', 12.955053, 77.583579, '7th Cross, Lal Bagh Fort Road, Bangalore 560004, KARNATALA', 174, 8887973.71001274, 2961992.3966398495)
('4fd32dfbe4b00127b9b14ba3', 'Megha enterprises', 12.954903157328246, 77.58556326481954, 'Opposite Urvashi Theater (Lalbagh Main Road), Bangalore, Karnātaka', 293, 8888382.292345088, 2962136.108395417)
...
Total: 575
-----------------------
List of all breakfast spots
-----------------------
('4d85719d7e8ef04d03be21be', 'NMH Tiffin House', 12.954300016820962, 77.5788060159709, 'Near Minerva Circle, VV Puram (Rama Iyengar Rd), Bangalore, Karnātaka', 202, 8887090.566945955, 2961420.7296396075)
('4d29ccc66e27a14317362824', 'Hotel Nandhini', 12.955307313850327, 77.5794984145012, 'No. 114/2, Near Minerva Circle (L.F. Road), Bangalore, Karnātaka', 278, 8887138.346942313, 2961685.989060371)
('4f9106e7e4b047933d3bd06c', 'Kamat Minerva', 12.955600874430939, 77.58048282394307, 'Jc Road', 259, 8887308.115111755, 2961831.960018265)
('4fe2dff4e4b0c7d06f33f8d2', 'Springs Hotel and Spa', 12.956703136089297, 77.58353012347467, '#19, (Siddiah Road), Bangalore 560002, Karnātaka', 279, 8887816.326982196, 2962323.163113327)
('516ffab3067d95e05584788d', 'MG Fine dining bar', 12.956445321450987, 77.5835394859314, 'No. 19, H-Siddiah Road, Bengaluru - 560 027., Bangalore, Karnātaka', 307, 8887841.26073476, 2962271.6371081555)
('4b5294ccf964a520ff8227e3', 'Mavalli Tiffin Room (MTR)', 12.955122296207755, 77.58555184054242, '#14 (Lalbagh Rd), Bangalore 560027, Karnātaka', 345, 8888360.407518657, 2962179.604043238)
('4e64d4521495676d56be7450', 'Cafe Coffee Day', 12.949943253445102, 77.592996055573, 'India', 284, 8890306.695670519, 2961781.229926126)
('4f5074c5e4b04150a5eb3963', 'Poornima Restaurant', 12.952285076798729, 77.59415314990886, 'India', 307, 8890327.578844659, 2962358.468835596)
('4c2b41f89a559c748c2b0de2', 'Costa Coffee', 12.948889199332454, 77.59759451664563, 'JNC Road (Koramangla 5th Block), Bangalore, Karnātaka', 227, 8891317.195563907, 2961970.8551002247)
('56fc7fd3498e735c8645031f', 'Tree Top Restaurant', 12.947969217029428, 77.59466236688444, 'India', 164, 8890815.353511754, 2961526.536844382)
...
Total: 237

Let’s now see all the collected breakfast spots in our area of interest on map.

So now we have all the breakfast spots in area within few kilometers from Richmond Circle, and we know their number! We also know which breakfast points exactly are in vicinity of every neighborhood candidate center.

This concludes the data gathering phase - we’re now ready to use this data for analysis to produce the report on optimal locations for a breakfast spot/point near Richmond Circle.

Methodology

In this project we will direct our efforts on detecting areas of Bengalure that have low breakfast spot density. We will limit our analysis to area ~3km around richmod circle.

In first step we have collected the required data: location and type (category) of every restaurant within 3km from Richmond Circle (Under the Fly Over). We have also identified breakfast spots (according to Foursquare categorization).

Second step in our analysis will be calculation and exploration of ‘breakfast spot density’ across different areas of Bengaluru - we will use heatmaps to identify a few promising areas close to center with low number of breakfast spot in general (and less breakfast spot in vicinity) and focus our attention on those areas.

In third and final step we will focus on most promising areas and within those create clusters of locations that meet some basic requirements established in discussion with stakeholders: we will take into consideration locations with no more than two breakfast spot in radius of 250 meters, and we want locations without breakfast spot in radius of 400 meters. We will present map of all such locations but also create clusters (using k-means clustering) of those locations to identify general zones / neighborhoods / addresses which should be a starting point for final ‘street level’ exploration and search for optimal venue location by stakeholders.

Analysis

Let’s perform some basic explanatory data analysis and derive some additional info from our raw data. First let’s count the number of breakfast spot, night life spots and shop and services spots in every area candidate:

location_night_lives_count = [len(res) for res in location_night_lives]
location_shop_n_service_count = [len(res) for res in location_shop_n_service]
location_breakfast_stops_count = [len(res) for res in location_breakfast_stops]

df_locations['Night_Life_Spots_In_Area'] = location_night_lives_count
df_locations['Shop_N_Service_Spots_In_Area'] = location_shop_n_service_count
df_locations['Breakfast_Spots_In_Area'] = location_breakfast_stops_count

print('Average number of Night Life spots in every area with radius=300m:', np.array(location_night_lives_count).mean())
print('Average number of Shop and Service spots in every area with radius=300m:', np.array(location_shop_n_service_count).mean())
print('Average number of Breakfast spots in every area with radius=300m:', np.array(location_breakfast_stops_count).mean())

df_locations.head(10)
Average number of Night Life spots in every area with radius=300m: 1.5327868852459017
Average number of Shop and Service spots in every area with radius=300m: 6.19672131147541
Average number of Breakfast spots in every area with radius=300m: 3.040983606557377

OK, now let’s calculate the distance to nearest breakfast spot from every area candidate center (not only those within 300m - we want distance to closest one, regardless of how distant it is).

distances_to_breakfast_spots= []

for area_x, area_y in zip(xs, ys):
    min_distance = 10000
    for res in breakfast_spots.values():
        res_x = res[6]
        res_y = res[7]
        d = calc_xy_distance(area_x, area_y, res_x, res_y)
        if d<min_distance:
            min_distance = d
    distances_to_breakfast_spots.append(min_distance)

df_locations['Distance_to_Breakfast_spots'] = distances_to_breakfast_spots
df_locations.head(10)
print('Average distance to closest Breakfast spot from each area center:', df_locations['Distance_to_Breakfast_spots'].mean())
Average distance to closest Breakfast spot from each area center: 569.7323627628746

OK, so on average Breakfast spot can be found within ~600m from every area center candidate. That’s fairly close, so we need to filter our areas carefully!

Let’s crete a map showing heatmap / density of breakfast spots and try to extract some meaningfull info from that. Also, let’s show borders of Richmond boroughs on our map and a few circles indicating distance of 1km, 2km and 3km from Richmond circle.

richmond_boroughs_url = 'https://raw.githubusercontent.com/stupidnetizen/Coursera_Capstone/master/richmond_circle_area.geojson'
import json
# Download from the above URL and load the geojson file
with open('richmond_circle_area.geojson', 'r') as f:
    richmond_boroughs = json.load(f)

def boroughs_style(feature):
    return { 'color': 'blue', 'fill': False }
night_life_spots_latlons = [[res[2], res[3]] for res in night_life_venues.values()]

breakfast_spots_latlons = [[res[2], res[3]] for res in breakfast_spots.values()]
import folium
from folium import plugins
from folium.plugins import HeatMap

map_richmond = folium.Map(location=richmond_circle, zoom_start=13)
folium.TileLayer('cartodbpositron').add_to(map_richmond) #cartodbpositron cartodbdark_matter
HeatMap(night_life_spots_latlons).add_to(map_richmond)
folium.Marker(richmond_circle).add_to(map_richmond)
folium.Circle(richmond_circle, radius=1000, fill=False, color='white').add_to(map_richmond)
folium.Circle(richmond_circle, radius=2000, fill=False, color='white').add_to(map_richmond)
folium.Circle(richmond_circle, radius=3000, fill=False, color='white').add_to(map_richmond)
folium.GeoJson(richmond_boroughs, style_function=boroughs_style, name='geojson').add_to(map_richmond)
map_richmond

Looks like a few pockets of low night life spots density closest to city center can be found south, south-east and west from Richmond Circle.

Let’s create another heatmap map showing heatmap/density of breakfast spot only.

map_richmond = folium.Map(location=richmond_circle, zoom_start=13)
folium.TileLayer('cartodbpositron').add_to(map_richmond) #cartodbpositron cartodbdark_matter
HeatMap(breakfast_spots_latlons).add_to(map_richmond)
folium.Marker(richmond_circle).add_to(map_richmond)
folium.Circle(richmond_circle, radius=1000, fill=False, color='white').add_to(map_richmond)
folium.Circle(richmond_circle, radius=2000, fill=False, color='white').add_to(map_richmond)
folium.Circle(richmond_circle, radius=3000, fill=False, color='white').add_to(map_richmond)
folium.GeoJson(richmond_boroughs, style_function=boroughs_style, name='geojson').add_to(map_richmond)
map_richmond

This map is so ‘hot’ (breakfast spot are quite densly distributed near RIchmond Circle) it also indicates higher density of existing breakfast spot directly north and west from Richmond Circle, with closest pockets of low breakfast spot density positioned east, south-east and south from richmond circle.

Based on this we will now focus our analysis on areas south-west, south, south-east and east from Richmond Circle center - we will move the center of our area of interest and reduce it’s size to have a radius of 2.5km. This places our location candidates mostly in boroughs with large low restaurant density south and south west from richmond circle, however this borough is less interesting to stakeholders as it’s mostly residental and less popular with tourists).

roi_x_min = richmond_circle_x - 2000
roi_y_max = richmond_circle_y + 1000
roi_width = 5000
roi_height = 5000
roi_center_x = roi_x_min + 2500
roi_center_y = roi_y_max - 2500
roi_center_lon, roi_center_lat = xy_to_lonlat(roi_center_x, roi_center_y)
roi_center = [roi_center_lat, roi_center_lon]

map_richmond = folium.Map(location=roi_center, zoom_start=14)
HeatMap(breakfast_spots_latlons).add_to(map_richmond)
folium.Marker(richmond_circle).add_to(map_richmond)
folium.Circle(roi_center, radius=2500, color='white', fill=True, fill_opacity=0.4).add_to(map_richmond)
folium.GeoJson(richmond_boroughs, style_function=boroughs_style, name='geojson').add_to(map_richmond)
map_richmond

Not bad - this nicely covers all the pockets of low breakfast spot density in nearby bouroughs.

Let’s also create new, more dense grid of location candidates restricted to our new region of interest (let’s make our location candidates 100m appart).

k = math.sqrt(3) / 2 # Vertical offset for hexagonal grid cells
x_step = 100
y_step = 100 * k 
roi_y_min = roi_center_y - 2500

roi_latitudes = []
roi_longitudes = []
roi_xs = []
roi_ys = []
for i in range(0, int(51/k)):
    y = roi_y_min + i * y_step
    x_offset = 50 if i%2==0 else 0
    for j in range(0, 51):
        x = roi_x_min + j * x_step + x_offset
        d = calc_xy_distance(roi_center_x, roi_center_y, x, y)
        if (d <= 2501):
            lon, lat = xy_to_lonlat(x, y)
            roi_latitudes.append(lat)
            roi_longitudes.append(lon)
            roi_xs.append(x)
            roi_ys.append(y)

print(len(roi_latitudes), 'candidate neighborhood centers generated.')
2261 candidate neighborhood centers generated.

OK. Now let’s calculate two most important things for each location candidate: number of breakfast spot in vicinity (we’ll use radius of 250 meters) and distance to closest breakfast spot.

def count_breakfast_spots_nearby(x, y, breakfast_spots, radius=250):    
    count = 0
    for res in breakfast_spots.values():
        res_x = res[6]; res_y = res[7]
        d = calc_xy_distance(x, y, res_x, res_y)
        if d<=radius:
            count += 1
    return count

def find_nearest_breakfast_spot(x, y, breakfast_spots):
    d_min = 100000
    for res in breakfast_spots.values():
        res_x = res[6]; res_y = res[7]
        d = calc_xy_distance(x, y, res_x, res_y)
        if d<=d_min:
            d_min = d
    return d_min

roi_breakfast_spots_counts = []
roi_breakfast_distances = []

print('Generating data on location candidates... ', end='')
for x, y in zip(roi_xs, roi_ys):
    count = count_breakfast_spots_nearby(x, y, breakfast_spots, radius=250)
    roi_breakfast_spots_counts.append(count)
    distance = find_nearest_breakfast_spot(x, y, breakfast_spots)
    roi_breakfast_distances.append(distance)
print('done.')

Generating data on location candidates... done.

```python
# Let's put this into dataframe
df_roi_locations = pd.DataFrame({'Latitude':roi_latitudes,
                                 'Longitude':roi_longitudes,
                                 'X':roi_xs,
                                 'Y':roi_ys,
                                 'Breakfast spots nearby':roi_breakfast_spots_counts,
                                 'Distance to breakfast spot':roi_breakfast_distances})

df_roi_locations.head(10)

OK. Let us now filter those locations: we’re interested only in locations with no more than two breakfast spot in radius of 250 meters, and no breakfast spot in radius of 400 meters.

good_res_count = np.array((df_roi_locations['Breakfast spots nearby']<=2))
print('Locations with no more than two Breakfast spots nearby:', good_res_count.sum())

good_ita_distance = np.array(df_roi_locations['Distance to breakfast spot']>=400)
print('Locations with no breakfast_spot  within 400m:', good_ita_distance.sum())

good_locations = np.logical_and(good_res_count, good_ita_distance)
print('Locations with both conditions met:', good_locations.sum())

df_good_locations = df_roi_locations[good_locations]

Locations with no more than two Breakfast spots nearby: 2058
Locations with no breakfast_spot  within 400m: 934
Locations with both conditions met: 934 ```

Let us see how this looks on a map.

good_latitudes = df_good_locations['Latitude'].values
good_longitudes = df_good_locations['Longitude'].values

good_locations = [[lat, lon] for lat, lon in zip(good_latitudes, good_longitudes)]

map_richmond = folium.Map(location=roi_center, zoom_start=14)
folium.TileLayer('cartodbpositron').add_to(map_richmond)
HeatMap(breakfast_spots_latlons).add_to(map_richmond)
folium.Circle(roi_center, radius=2500, color='white', fill=True, fill_opacity=0.6).add_to(map_richmond)
folium.Marker(richmond_circle).add_to(map_richmond)
for lat, lon in zip(good_latitudes, good_longitudes):
    folium.CircleMarker([lat, lon], radius=2, color='blue', fill=True, fill_color='blue', fill_opacity=1).add_to(map_richmond) 
folium.GeoJson(richmond_boroughs, style_function=boroughs_style, name='geojson').add_to(map_richmond)
map_richmond

Looking good. We now have a bunch of locations fairly close to Richmond Circle (mostly in south and south east of Richmond borough), and we know that each of those locations has no more than two breakfast spot in radius of 250m, and no breakfast spot closer than 400m. Any of those locations is a potential candidate for a new breakfast spot, at least based on nearby competition.

Let’s now show those good locations in a form of heatmap:

map_richmond = folium.Map(location=roi_center, zoom_start=14)
HeatMap(good_locations, radius=25).add_to(map_richmond)
folium.Marker(richmond_circle).add_to(map_richmond)
for lat, lon in zip(good_latitudes, good_longitudes):
    folium.CircleMarker([lat, lon], radius=2, color='blue', fill=True, fill_color='blue', fill_opacity=1).add_to(map_richmond)
folium.GeoJson(richmond_boroughs, style_function=boroughs_style, name='geojson').add_to(map_richmond)
map_richmond

Looking good. What we have now is a clear indication of zones with low number of breakfast spot in vicinity, and no Italian breakfast spot at all nearby.

Let us now cluster those locations to create centers of zones containing good locations. Those zones, their centers and addresses will be the final result of our analysis.

from sklearn.cluster import KMeans

number_of_clusters = 15

good_xys = df_good_locations[['X', 'Y']].values
kmeans = KMeans(n_clusters=number_of_clusters, random_state=0).fit(good_xys)

cluster_centers = [xy_to_lonlat(cc[0], cc[1]) for cc in kmeans.cluster_centers_]

map_richmond = folium.Map(location=roi_center, zoom_start=14)
folium.TileLayer('cartodbpositron').add_to(map_richmond)
HeatMap(breakfast_spots_latlons).add_to(map_richmond)
folium.Circle(roi_center, radius=2500, color='white', fill=True, fill_opacity=0.4).add_to(map_richmond)
folium.Marker(richmond_circle).add_to(map_richmond)
for lon, lat in cluster_centers:
    folium.Circle([lat, lon], radius=500, color='green', fill=True, fill_opacity=0.25).add_to(map_richmond) 
for lat, lon in zip(good_latitudes, good_longitudes):
    folium.CircleMarker([lat, lon], radius=2, color='blue', fill=True, fill_color='blue', fill_opacity=1).add_to(map_richmond)
folium.GeoJson(richmond_boroughs, style_function=boroughs_style, name='geojson').add_to(map_richmond)
map_richmond

Not bad - our clusters represent groupings of most of the candidate locations and cluster centers are placed nicely in the middle of the zones ‘rich’ with location candidates.

Addresses of those cluster centers will be a good starting point for exploring the neighborhoods to find the best possible location based on neighborhood specifics.

Let’s see those zones on a city map without heatmap, using shaded areas to indicate our clusters:

map_richmond = folium.Map(location=roi_center, zoom_start=14)
folium.Marker(richmond_circle).add_to(map_richmond)
for lat, lon in zip(good_latitudes, good_longitudes):
    folium.Circle([lat, lon], radius=250, color='#00000000', fill=True, fill_color='#0066ff', fill_opacity=0.07).add_to(map_richmond)
for lat, lon in zip(good_latitudes, good_longitudes):
    folium.CircleMarker([lat, lon], radius=2, color='blue', fill=True, fill_color='blue', fill_opacity=1).add_to(map_richmond)
for lon, lat in cluster_centers:
    folium.Circle([lat, lon], radius=500, color='green', fill=False).add_to(map_richmond) 
folium.GeoJson(richmond_boroughs, style_function=boroughs_style, name='geojson').add_to(map_richmond)
map_richmond

Let’s zoom in on candidate areas in Richmond Town:

map_richmond = folium.Map(location=richmond_circle, zoom_start=15)
folium.Marker(richmond_circle).add_to(map_richmond)
for lon, lat in cluster_centers:
    folium.Circle([lat, lon], radius=500, color='green', fill=False).add_to(map_richmond) 
for lat, lon in zip(good_latitudes, good_longitudes):
    folium.Circle([lat, lon], radius=250, color='#0000ff00', fill=True, fill_color='#0066ff', fill_opacity=0.07).add_to(map_richmond)
for lat, lon in zip(good_latitudes, good_longitudes):
    folium.CircleMarker([lat, lon], radius=2, color='blue', fill=True, fill_color='blue', fill_opacity=1).add_to(map_richmond)
folium.GeoJson(richmond_boroughs, style_function=boroughs_style, name='geojson').add_to(map_richmond)
map_richmond

…and candidate areas near Richmond Circle:

map_richmond = folium.Map(location=richmond_circle, zoom_start=15)
folium.Marker(richmond_circle).add_to(map_richmond)
for lon, lat in cluster_centers:
    folium.Circle([lat, lon], radius=500, color='green', fill=False).add_to(map_richmond) 
for lat, lon in zip(good_latitudes, good_longitudes):
    folium.Circle([lat, lon], radius=250, color='#0000ff00', fill=True, fill_color='#0066ff', fill_opacity=0.07).add_to(map_richmond)
for lat, lon in zip(good_latitudes, good_longitudes):
    folium.CircleMarker([lat, lon], radius=2, color='blue', fill=True, fill_color='blue', fill_opacity=1).add_to(map_richmond)
folium.GeoJson(richmond_boroughs, style_function=boroughs_style, name='geojson').add_to(map_richmond)
map_richmond

Finaly, let’s reverse geocode those candidate area centers to get the addresses which can be presented to stakeholders.

candidate_area_addresses = []
print('==============================================================')
print('Addresses of centers of areas recommended for further analysis')
print('==============================================================\n')
for lon, lat in cluster_centers:
    addr = get_address(google_api_key, lat, lon).replace(', India', '')
    candidate_area_addresses.append(addr)    
    x, y = lonlat_to_xy(lon, lat)
    d = calc_xy_distance(x, y, richmond_circle_x, richmond_circle_y)
    print('{}{} => {:.1f}km from Richmond Circle'.format(addr, ' '*(50-len(addr)), d/1000))
    
==============================================================
Addresses of centers of areas recommended for further analysis
==============================================================

10/2, Alfred St, Richmond Town, Bengaluru, Karnataka 560025 => 1.8km from Richmond Circle
Hosur Main Road, Sudhama Nagar, Bengaluru, Karnataka 560004 => 3.2km from Richmond Circle
147, 2nd Main Rd, Jalakanteshwara Nagar, Shanti Nagar, Bengaluru, Karnataka 560027 => 2.6km from Richmond Circle
Laxmi Road, Shantinagar                            => 1.2km from Richmond Circle
77, SUPREM KUTIR, 5th Cross Rd, Bheemanna Garden, Ayappa Garden, Shanti Nagar, Bengaluru, Karnataka 560027 => 1.7km from Richmond Circle
9, 1st Cross Rd, K.S. Garden, Sudhama Nagar, Bengaluru, Karnataka 560027 => 2.5km from Richmond Circle
27/1, Hosur Rd, Langford Town, Shanti Nagar, Bengaluru, Karnataka 560025 => 2.9km from Richmond Circle
13, 5th Cross Rd, Sudhama Nagar, Bengaluru, Karnataka 560027 => 2.7km from Richmond Circle
16, Muniyappa Rd, Sampangi Rama Nagara, Sampangi Rama Nagar, Bengaluru, Karnataka 560027 => 0.9km from Richmond Circle
KSRTC Bus Depot 2, 4th Cross Lakshmi Rd, Shanti Nagar, Bengaluru, Karnataka 560027 => 2.1km from Richmond Circle
81A, BTS Main Rd, Chinnayanpalya, Wilson Garden, Bengaluru, Karnataka 560027 => 3.6km from Richmond Circle
Mallige Hospital, Siddapura Rd, Mavalli, Bengaluru, Karnataka 560004 => 3.8km from Richmond Circle
Al-ameen towers, Hosur Main Road, Sudhama Nagar, Bengaluru, Karnataka 560027 => 3.5km from Richmond Circle
2A, Cemetry Rd, Bhetal Layout, Langford Town, Shanti Nagar, Bengaluru, Karnataka 560025 => 2.3km from Richmond Circle
132, J P Nagar, Jalakanteshwara Nagar, Shanti Nagar, Bengaluru, Karnataka 560027 => 3.2km from Richmond Circle

This concludes our analysis. We have created 15 addresses representing centers of zones containing locations with low number of breakfast spot and no breakfast spot nearby, all zones being fairly close to city center (all less than 4km from Richmond Circle, and about half of those less than 2km from Richmond circle). Although zones are shown on map with a radius of ~500 meters (green circles), their shape is actually very irregular and their centers/addresses should be considered only as a starting point for exploring area neighborhoods in search for potential restaurant locations. Most of the zones are located around nearby boroughs, which we have identified as interesting due to being popular with tourists, fairly close to city center and well connected by public transport.

map_richmond = folium.Map(location=roi_center, zoom_start=14)
folium.Circle(richmond_circle, radius=50, color='red', fill=True, fill_color='red', fill_opacity=1).add_to(map_richmond)
for lonlat, addr in zip(cluster_centers, candidate_area_addresses):
    folium.Marker([lonlat[1], lonlat[0]], popup=addr).add_to(map_richmond) 
for lat, lon in zip(good_latitudes, good_longitudes):
    folium.Circle([lat, lon], radius=250, color='#0000ff00', fill=True, fill_color='#0066ff', fill_opacity=0.05).add_to(map_richmond)
map_richmond

Results and Discussion

Our analysis shows that although there is a great number of breakfast spot in Bengaluru , there are pockets of low breakfast spot density fairly close to richmond circle center. Highest concentration of breakfast spot was detected north and east from Richmond Circle, so we focused our attention to areas south, south-west and west, corresponding to boroughs near by.

After directing our attention to this more narrow area of interest (covering approx. 5x5km south-east from Richmond Circle) we first created a dense grid of location candidates (spaced 100m appart); those locations were then filtered so that those with more than two breakfast spots in radius of 250m and those with breakfast spot closer than 400m were removed.

Those location candidates were then clustered to create zones of interest which contain greatest number of location candidates. Addresses of centers of those zones were also generated using reverse geocoding to be used as markers/starting points for more detailed local analysis based on other factors.

Result of all this is 15 zones containing largest number of potential new breakfast spot locations based on number of and distance to existing venues. This, of course, does not imply that those zones are actually optimal locations for a new breakfast spot! Purpose of this analysis was to only provide info on areas close to Richmond circle center but not crowded with existing breakfast spot - it is entirely possible that there is a very good reason for small number of breakfast spot in any of those areas, reasons which would make them unsuitable for a new restaurant regardless of lack of competition in the area. Recommended zones should therefore be considered only as a starting point for more detailed analysis which could eventually result in location which has not only no nearby competition but also other factors taken into account and all other relevant conditions met.

Conclusion

Purpose of this project was to identify Bengaluru areas close to center with low number of breakfast spot in order to aid stakeholders in narrowing down the search for optimal location for a new breakfast spot. By calculating restaurant density distribution from Foursquare data we have first identified general boroughs that justify further analysis , and then generated extensive collection of locations which satisfy some basic requirements regarding existing nearby breakfast spot. Clustering of those locations was then performed in order to create major zones of interest (containing greatest number of potential locations) and addresses of those zone centers were created to be used as starting points for final exploration by stakeholders.

Final decission on optimal breakfast spot location will be made by stakeholders based on specific characteristics of neighborhoods and locations in every recommended zone, taking into consideration additional factors like attractiveness of each location (proximity to park or water), levels of noise / proximity to major roads, real estate availability, prices, social and economic dynamics of every neighborhood etc.