@@ -10,16 +10,18 @@ kernelspec:
1010
1111# Competitor walksheds
1212
13- A coffee shop wants to know which competitors draw from the same neighbourhood it does.
14- Its ** walkshed** is every residential block that can walk to it. This tutorial finds
15- the competitors — the cafes those same blocks can also walk to — and maps them. The
16- example city is Providence, Rhode Island.
13+ A coffee shop wants to understand the competition inside its own catchment. Its
14+ ** walkshed** is every residential block that can walk to it in 10 minutes. This
15+ tutorial asks, block by block, how many other cafes those residents can also reach on
16+ foot, and which one is closest. The example city is Providence, Rhode Island.
1717
18- * Running this tutorial uses about 2,000 tokens.*
18+ * Running this tutorial uses about 450 tokens.*
1919
2020``` {code-cell} python
2121:tags: [remove-cell]
2222import os
23+ import numpy as np
24+ import plotly.colors as pc
2325from closecity import Client, close_map
2426import plotly.io as pio
2527
@@ -49,91 +51,82 @@ city_boundary = close.place_boundary(geoid = city["geoid"])
4951
5052## Find the shops and our walkshed
5153
52- ` place_pois ` returns every cafe within the city's boundary — no radius to guess. Pick
53- one as the subject, then pull its walkshed: every block that can walk to it in 10
54- minutes .
54+ ` place_pois ` returns every cafe within the city's boundary. Pick one as the subject,
55+ then pull its walkshed: every block that can walk to it in 10 minutes. Draw the
56+ walkshed with the cafes on top, our shop in orange .
5557
5658``` {code-cell} python
5759cafes = close.place_pois(geoid = city["geoid"], type = cafe)
5860ours = cafes.iloc[0]
5961print(ours["name"])
60- cafes["is_ours"] = cafes["dest_id"] == ours["dest_id"]
6162
62- our_shed = close.poi_catchment(
63- dest_id = int(ours["dest_id"]),
64- mode = "walk",
65- max_minutes = 10
63+ our_shed = close.poi_catchment(dest_id = int(ours["dest_id"]), mode = "walk",
64+ max_minutes = 10)
65+ close_map(
66+ cafes,
67+ color = ["#f36e21" if d == ours["dest_id"] else "#202a5b" for d in cafes["dest_id"]],
68+ label = "name",
69+ background = our_shed.dissolve(),
70+ background_color = "#74b9ff",
71+ boundary = city_boundary,
6672)
67- walkshed = our_shed.dissolve()
6873```
6974
70- Draw the walkshed, with the cafes on top and our shop in orange.
75+ ## What each block can reach
76+
77+ Now split the walkshed by block. A single ` block_pois ` call takes the whole list of
78+ walkshed blocks and returns, for every block, each cafe its residents can walk to
79+ within 10 minutes — the real routed answer, not a straight-line guess, and one
80+ request rather than one per block. Passing a list of GEOIDs tags every row with its
81+ origin ` geoid ` , so grouping by it reads two things per block: how many cafes are in
82+ reach, and which one is closest by walk time.
7183
7284``` {code-cell} python
73- close_map(
74- cafes,
75- color = ["#f36e21" if o else "#202a5b" for o in cafes["is_ours"]],
76- label = "name",
77- background = walkshed,
78- background_color = "#74b9ff",
79- boundary = city_boundary
85+ reach = close.block_pois(
86+ list(our_shed["geoid"]),
87+ mode = "walk", type = cafe, max_minutes = 10, output = "tabular",
88+ )
89+
90+ per_block = reach.groupby("geoid")
91+ our_shed["n_cafes"] = (
92+ our_shed["geoid"].map(per_block.size()).fillna(0).astype(int)
8093)
94+ winners = reach.loc[per_block["travel_time"].idxmin()].set_index("geoid")
95+ our_shed["closest_cafe"] = our_shed["geoid"].map(winners["dest_id"])
8196```
8297
83- ## Who else those blocks can reach
98+ ## How many cafes each block can reach
8499
85- A competitor is a cafe that the residents of * our* walkshed can also walk to. So take
86- the blocks in our walkshed, and for each of the nearest cafes, pull its walkshed: if
87- the two share blocks, those residents can reach it too. ` our_shed["geoid"] ` is just a
88- column of block ids, so the overlap is a plain set intersection. (We check the nearest
89- cafes to keep the token cost down; the recipe scales to all of them.)
100+ Shade every block in the walkshed by the number of cafes within a 10-minute walk;
101+ blue marks the blocks with the most choice.
90102
91103``` {code-cell} python
92- our_geoids = set(our_shed["geoid"])
93- others = cafes[~cafes["is_ours"]].copy()
94- others["dist"] = (others["lon"] - ours["lon"]) ** 2 + (others["lat"] - ours["lat"]) ** 2
95- nearest = others.nsmallest(min(15, len(others)), "dist")
96-
97- shared = {}
98- for _, cafe_row in nearest.iterrows():
99- their_shed = close.poi_catchment(
100- dest_id = int(cafe_row["dest_id"]),
101- mode = "walk",
102- max_minutes = 10
103- )
104- shared[cafe_row["dest_id"]] = len(our_geoids & set(their_shed["geoid"]))
105-
106- for dest_id, n in sorted(shared.items(), key = lambda kv: -kv[1]):
107- if n == 0:
108- continue
109- name = nearest.loc[nearest["dest_id"] == dest_id, "name"].iloc[0]
110- print(f"{name:28} {n:3} shared blocks ({100 * n / len(our_geoids):.0f}% of ours)")
111-
112- competitors = {d for d, n in shared.items() if n > 0}
113- cafes["is_competitor"] = cafes["dest_id"].isin(competitors)
104+ close_map(our_shed, fill = "n_cafes", reverse = True, boundary = city_boundary)
114105```
115106
116- ## Map the contested ground
107+ ## Which cafe is closest
117108
118- Draw every cafe over our walkshed: our shop in orange, the competitors those blocks can
119- also reach in red, and the rest in navy . The red cafes are the ones fighting for the
120- same walk-in traffic .
109+ Give each cafe that wins at least one block a colour, then paint every block with the
110+ colour of its closest cafe . The cafe points share those colours. The result is the
111+ contested ground — where our shop's catchment gives way to a competitor's .
121112
122113``` {code-cell} python
123- def cafe_color(row):
124- if row["is_ours"]:
125- return "#f36e21"
126- return "#e03131" if row["is_competitor"] else "#202a5b"
114+ closest = [int(c) for c in our_shed["closest_cafe"].dropna().unique()]
115+ colors = pc.qualitative.Bold
116+ palette = {cid: colors[i % len(colors)] for i, cid in enumerate(closest)}
117+
118+ block_color = [palette[int(c)] if not np.isnan(c) else "#dddddd"
119+ for c in our_shed["closest_cafe"]]
120+ winning = cafes[cafes["dest_id"].isin(closest)]
127121
128122close_map(
129- cafes,
130- color = [cafe_color(r) for _, r in cafes.iterrows()],
131- label = "name",
132- background = walkshed,
133- background_color = "#74b9ff",
134- boundary = city_boundary
123+ our_shed,
124+ color = block_color,
125+ points = winning,
126+ points_color = [palette[int(d)] for d in winning["dest_id"]],
127+ boundary = city_boundary,
135128)
136129```
137130
138- The same recipe scales up: raise the cafe count you check , or compare whole cities by
139- pulling each one's cafes with ` place_pois ` .
131+ The same recipe scales up: raise ` max_minutes ` , or compare whole cities by pulling
132+ each one's cafes with ` place_pois ` .
0 commit comments