Adhika Setya Pramudita

Adhika Setya Pramudita

Collection of thoughts

09 Feb 2025

Alfamart vs Indomart dominance in Jakarta

Alfamart and Indomart are two of the most popular convenience stores in Indonesia.

For thought experiment, lets try to explore the data that we can get from OpenStreetMap about it. Do note though that OSM data is not always complete and up to date, so the results might not be 100% accurate. If you notice specific place are missing, do try to add them on OSM!

Pull list of minimarket

To do this, we are using osmnx library, which simplify the query process to the OpenStreetMap database, and we got a tidy GeoDataFrame ready to process further.

 1import osmnx as ox
 2
 3# Define the area - using Jakarta's bounding box
 4bbox = (106.6894, -6.3697, 106.9755, -6.1297)  # (west, south, east, north)
 5
 6alfamart_tags = {'brand': 'Alfamart'}
 7alfamart = ox.features.features_from_bbox(bbox, tags=alfamart_tags)
 8
 9indomaret_tags = {'brand': 'Indomaret'}
10indomaret = ox.features.features_from_bbox(bbox, tags=indomaret_tags)
11
12# Print the number of Alfamart and Indomaret locations
13print(f"Number of Alfamart: {len(alfamart)}")
14print(f"Number of Indomaret: {len(indomaret)}")
Number of Alfamart: 305
Number of Indomaret: 413
Next, we convert the OSM data into GeoDataFrames. This will make it easier for us to plot the data on a map.
 1import pandas as pd
 2from shapely.geometry import Point
 3import geopandas as gpd
 4
 5alfamart_points = gpd.GeoDataFrame(
 6    geometry=[Point(geom.centroid.xy) for geom in alfamart.geometry],
 7    crs=alfamart.crs
 8)
 9alfamart_points['type'] = 'Alfamart'
10
11indomaret_points = gpd.GeoDataFrame(
12    geometry=[Point(geom.centroid.xy) for geom in indomaret.geometry],
13    crs=indomaret.crs
14)
15indomaret_points['type'] = 'Indomaret'
16
17# Combine the data
18all_points = pd.concat([alfamart_points, indomaret_points])

Plotting the data

Now, we can plot the data on a map. We will use contextily to add the basemap, and matplotlib to plot the data.

Show code (71 lines)
 1# Create a map using contextily and matplotlib
 2import contextily as ctx
 3import matplotlib.pyplot as plt
 4
 5all_points_gdf = gpd.GeoDataFrame(all_points)
 6# Create figure and axis
 7fig, ax = plt.subplots(figsize=(15, 15))
 8
 9# Ensure data is in Web Mercator projection (EPSG:3857) for contextily
10all_points_gdf = all_points_gdf.to_crs(epsg=3857)
11
12# Calculate grid size for zoom level 11 (in meters since we're in Web Mercator)
13grid_size = 1_000
14
15# Group points into grid cells
16all_points_gdf['grid_x'] = (all_points_gdf.geometry.x // grid_size) * grid_size
17all_points_gdf['grid_y'] = (all_points_gdf.geometry.y // grid_size) * grid_size
18
19# Count stores by type in each grid cell
20grid_counts = all_points_gdf.groupby(['grid_x', 'grid_y', 'type']).size().unstack(fill_value=0)
21grid_counts['dominant'] = grid_counts.idxmax(axis=1)
22grid_counts['difference'] = abs(grid_counts['Alfamart'] - grid_counts['Indomaret'])
23
24# Create rectangles for each grid cell
25for idx, row in grid_counts.iterrows():
26    grid_x, grid_y = idx
27        
28    # Color intensity based on dominance
29    opacity = min(row['difference'] / 5, 0.8)  # Cap at 0.8 opacity
30    
31    # Red for Alfamart dominance, Blue for Indomaret
32    color = 'red' if row['dominant'] == 'Alfamart' else 'blue'
33    
34    # Create rectangle
35    rect = plt.Rectangle(
36        (grid_x, grid_y), 
37        grid_size, 
38        grid_size,
39        facecolor=color,
40        alpha=opacity,
41        edgecolor='none'
42    )
43    ax.add_patch(rect)
44
45# Set map bounds to Jakarta area (converted to Web Mercator coordinates)
46# Set bounds based on the data points with some padding
47x_min = all_points_gdf.geometry.x.min()
48x_max = all_points_gdf.geometry.x.max()
49y_min = all_points_gdf.geometry.y.min() 
50y_max = all_points_gdf.geometry.y.max()
51x_padding = (x_max - x_min) * 0.1
52y_padding = (y_max - y_min) * 0.1
53
54ax.set_xlim([x_min - x_padding, x_max + x_padding])
55ax.set_ylim([y_min - y_padding, y_max + y_padding])
56
57ctx.add_basemap(
58    ax, 
59    source=ctx.providers.CartoDB.Positron,
60    crs=all_points_gdf.crs
61)
62
63legend_elements = [
64    plt.Rectangle((0,0), 1, 1, facecolor='red', alpha=0.6, label='Alfamart Dominant'),
65    plt.Rectangle((0,0), 1, 1, facecolor='blue', alpha=0.6, label='Indomaret Dominant')
66]
67ax.legend(handles=legend_elements, loc='upper right')
68ax.set_axis_off()
69
70plt.title('Minimarket Chain Dominance in Jakarta\nGrid size: ~1km', pad=20)
71plt.show()
Cell output

🗒️ Download (alfamart-vs-indomart.ipynb)