Territory planning in spreadsheets is an exercise in frustration. You stare at rows of account names, zip codes, and rep assignments, but you cannot actually see whether coverage is balanced or where the gaps are. An interactive map built with Python and Folium makes territory imbalances immediately obvious.
What You Need¶
Install the required libraries:
pip install folium pandas geopy simple-salesforce
Folium generates interactive Leaflet.js maps from Python. No JavaScript knowledge is required.
Pulling Account Data¶
Start by querying your CRM for accounts with location and revenue data:
import os
import pandas as pd
from simple_salesforce import Salesforce
sf = Salesforce(
username=os.environ["SF_USERNAME"],
password=os.environ["SF_PASSWORD"],
security_token=os.environ["SF_TOKEN"],
)
query = """
SELECT Name, BillingCity, BillingState, BillingLatitude,
BillingLongitude, AnnualRevenue, Owner.Name
FROM Account
WHERE BillingLatitude != null AND AnnualRevenue > 0
"""
results = sf.query_all(query)
df = pd.DataFrame(results["records"]).drop(columns=["attributes"])
df["OwnerName"] = df["Owner"].apply(lambda x: x["Name"])
If your accounts lack coordinates, geocode them from city and state:
from geopy.geocoders import Nominatim
from geopy.extra.rate_limiter import RateLimiter
geolocator = Nominatim(user_agent="territory_mapper")
geocode = RateLimiter(geolocator.geocode, min_delay_seconds=1)
def get_coords(row):
if pd.notna(row["BillingLatitude"]):
return row["BillingLatitude"], row["BillingLongitude"]
location = geocode(f"{row['BillingCity']}, {row['BillingState']}")
if location:
return location.latitude, location.longitude
return None, None
Building the Map¶
Create a color-coded map where each rep’s accounts appear in a distinct color:
import folium
def build_territory_map(df):
center_lat = df["BillingLatitude"].mean()
center_lon = df["BillingLongitude"].mean()
m = folium.Map(location=[center_lat, center_lon], zoom_start=5)
# Assign colors to reps
reps = df["OwnerName"].unique()
colors = ["red", "blue", "green", "purple", "orange",
"darkred", "cadetblue", "darkgreen", "darkpurple", "pink"]
rep_colors = {rep: colors[i % len(colors)] for i, rep in enumerate(reps)}
for _, row in df.iterrows():
revenue = row["AnnualRevenue"]
radius = max(5, min(20, revenue / 500000)) # Scale marker by revenue
folium.CircleMarker(
location=[row["BillingLatitude"], row["BillingLongitude"]],
radius=radius,
popup=folium.Popup(
f"<b>{row['Name']}</b><br>"
f"Rep: {row['OwnerName']}<br>"
f"Revenue: ${revenue:,.0f}",
max_width=250,
),
color=rep_colors[row["OwnerName"]],
fill=True,
fill_opacity=0.7,
).add_to(m)
return m
Adding a Legend¶
A legend makes it clear which color maps to which rep:
def add_legend(m, rep_colors):
legend_html = """
<div style="position: fixed; bottom: 30px; left: 30px; z-index: 1000;
background: white; padding: 12px; border-radius: 5px;
border: 2px solid grey; font-size: 13px;">
<b>Territory Legend</b><br>
"""
for rep, color in rep_colors.items():
legend_html += (
f'<i style="background:{color}; width:12px; height:12px; '
f'display:inline-block; border-radius:50%; margin-right:6px;">'
f'</i>{rep}<br>'
)
legend_html += "</div>"
m.get_root().html.add_child(folium.Element(legend_html))
return m
Generating a Territory Summary¶
Before exporting the map, print a quick coverage summary:
def print_territory_summary(df):
summary = df.groupby("OwnerName").agg(
accounts=("Name", "count"),
total_revenue=("AnnualRevenue", "sum"),
).sort_values("total_revenue", ascending=False)
print("\nTerritory Summary:")
print(f"{'Rep':<25} {'Accounts':>10} {'Revenue':>15}")
print("-" * 52)
for rep, row in summary.iterrows():
print(f"{rep:<25} {row['accounts']:>10} ${row['total_revenue']:>14,.0f}")
Saving and Sharing¶
Export the map as a standalone HTML file anyone can open in a browser:
def main():
m = build_territory_map(df)
m.save("territory_map.html")
print("Map saved to territory_map.html")
print_territory_summary(df)
if __name__ == "__main__":
main()
The exported file is a single HTML document with all map tiles and interactivity built in. Share it over Slack, embed it in Confluence, or host it on an internal server.
Key Takeaways¶
- Folium generates interactive territory maps from Python without any JavaScript or frontend development
- Scaling marker size by revenue makes it immediately clear where the high-value accounts are concentrated
- Color-coding by rep reveals territory imbalances that spreadsheets hide, such as one rep covering a massive geographic area while another has a dense cluster
- Exporting to a standalone HTML file makes sharing effortless since no special software is needed to view the map