@@ -11,7 +11,7 @@ kernelspec:
1111# The amenity basket
1212
1313A city planner wants every resident to be able to walk to a basket of six everyday
14- amenities: a grocery store , a library, a park, a frequent-transit stop, a restaurant,
14+ amenities: a supermarket , a library, a park, a frequent-transit stop, a restaurant,
1515and a cafe. This tutorial measures how many residents already have that, and shows
1616where the gaps are. The idea follows
1717[ this analysis] ( https://nathenry.com/writing/2023-02-07-seattle-walkability.html ) ,
@@ -22,7 +22,7 @@ here applied to Richmond, Virginia.
2222``` {code-cell} python
2323:tags: [remove-cell]
2424import os
25- from closecity import Client
25+ from closecity import Client, close_map
2626close = Client(os.environ.get("CLOSECITY_KEY"))
2727```
2828
@@ -32,7 +32,7 @@ Read the six category ids from the free catalog, and turn the city name into a c
3232point.
3333
3434``` python
35- from closecity import Client
35+ from closecity import Client, close_map
3636
3737close = Client(" ck_live_your_key" ) # use your own key here
3838```
@@ -41,7 +41,7 @@ close = Client("ck_live_your_key") # use your own key here
4141types = close.destination_types()
4242ids = dict(zip(types["label"], types["dest_type_id"]))
4343basket = {
44- "grocery ": ids["grocery_stores"],
44+ "supermarket ": ids["grocery_stores"],
4545 "library": ids["libraries"],
4646 "park": ids["parks"],
4747 "transit": ids["frequent_transit"],
@@ -54,15 +54,17 @@ city = close.places("Richmond").iloc[0]
5454
5555## Pull the blocks, with population
5656
57- One call gets the walk time from every block near downtown to each of the six
58- categories, along with each block's population.
57+ One call gets the walk time from every block to each of the six categories, plus
58+ each block's population. To keep this tutorial cheap we take the central blocks
59+ within a radius; ` place_blocks(city["geoid"]) ` pulls ** every** block in the city
60+ the same way (at a higher token cost).
5961
6062``` {code-cell} python
6163blocks = close.blocks_query(
6264 center = {"lon": city["lon"], "lat": city["lat"]}, radius_m = 2500,
6365 mode = "walk", type = list(basket.values()), include_population = True)
6466
65- one_per_block = blocks.drop_duplicates("geoid")
67+ one_per_block = blocks.drop_duplicates("geoid").copy()
6668total_pop = one_per_block["population"].sum()
6769```
6870
@@ -78,14 +80,13 @@ for name, type_id in basket.items():
7880 print(f"{name:11} {100 * pop / total_pop:3.0f}%")
7981```
8082
81- Map one amenity to see the pattern .
83+ Map one amenity — every block shown, the covered ones highlighted .
8284
8385``` {code-cell} python
8486near_transit = set(blocks.loc[(blocks.dest_type_id == basket["transit"]) &
8587 (blocks.travel_time <= 15), "geoid"])
86- one_per_block = one_per_block.assign(
87- has_transit = one_per_block.geoid.isin(near_transit))
88- one_per_block.plot(column = "has_transit", cmap = "Greens")
88+ one_per_block["has_transit"] = one_per_block.geoid.isin(near_transit)
89+ close_map(one_per_block, highlight = "has_transit", color = "#058040")
8990```
9091
9192## The 15-minute-city score
@@ -97,9 +98,8 @@ already pulled, so it costs nothing more.
9798``` {code-cell} python
9899covered = blocks[blocks.travel_time <= 15]
99100score = covered.groupby("geoid")["dest_type_id"].nunique()
100- one_per_block = one_per_block.assign(
101- score = one_per_block.geoid.map(score).fillna(0).astype(int))
102- one_per_block.plot(column = "score", cmap = "viridis", legend = True)
101+ one_per_block["score"] = one_per_block.geoid.map(score).fillna(0).astype(int)
102+ close_map(one_per_block, fill = "score")
103103```
104104
105105## Who can reach all six
@@ -117,9 +117,8 @@ basket_pop = one_per_block.loc[one_per_block.geoid.isin(covered_all),
117117 "population"].sum()
118118print(f"All six amenities: {100 * basket_pop / total_pop:.0f}% of residents")
119119
120- one_per_block = one_per_block.assign(
121- full_basket = one_per_block.geoid.isin(covered_all))
122- one_per_block.plot(column = "full_basket", cmap = "Oranges")
120+ one_per_block["full_basket"] = one_per_block.geoid.isin(covered_all)
121+ close_map(one_per_block, highlight = "full_basket", color = "#f36e21")
123122```
124123
125124## Which amenity to add first
@@ -137,10 +136,28 @@ for name, type_id in basket.items():
137136 print(f"{name:11} {pop:6.0f} residents would gain access")
138137```
139138
140- Map the uncovered blocks to see where new amenities would do the most good.
139+ ## Site a new supermarket
140+
141+ The counts above say * which* amenity to add; the next question is * where* . Take a
142+ candidate site near the city centre and ask how many residents would newly gain a
143+ supermarket within a 15-minute walk if one opened there. A ` direction="to" `
144+ isochrone gives exactly the blocks that could reach the site on foot in 15 minutes.
145+
146+ ``` {code-cell} python
147+ reachable = close.isochrone(lon = city["lon"], lat = city["lat"], mode = "walk",
148+ direction = "to", minutes = 15, format = "blocks")
149+
150+ near_supermarket = set(blocks.loc[(blocks.dest_type_id == basket["supermarket"]) &
151+ (blocks.travel_time <= 15), "geoid"])
152+ newly_served = set(reachable.geoid) - near_supermarket
153+ gain_pop = one_per_block.loc[one_per_block.geoid.isin(newly_served),
154+ "population"].sum()
155+ print(f"A supermarket here would newly serve {gain_pop:.0f} residents")
156+ ```
157+
158+ Map the whole city and highlight the blocks that would newly gain access.
141159
142160``` {code-cell} python
143- one_per_block = one_per_block.assign(
144- uncovered = one_per_block.geoid.isin(uncovered))
145- one_per_block.plot(column = "uncovered", cmap = "Blues")
161+ one_per_block["newly_served"] = one_per_block.geoid.isin(newly_served)
162+ close_map(one_per_block, highlight = "newly_served", color = "#e8590c")
146163```
0 commit comments