Skip to content

Commit 4326963

Browse files
author
Nathaniel Henry
committed
Rewrite the docs and tutorials for the tabular output default.
1 parent acb01c7 commit 4326963

11 files changed

Lines changed: 282 additions & 46 deletions

README.md

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,31 +13,32 @@ pip install closecity
1313
pip install "closecity[tiger]" # to auto-download census-block boundaries
1414
```
1515

16-
This pulls in `httpx` and `geopandas`, so feature methods return GeoDataFrames out
17-
of the box.
16+
This pulls in `httpx`, `pandas`, and `geopandas`, so results come back as data
17+
frames out of the box: a GeoDataFrame where geometry applies, a plain DataFrame
18+
otherwise.
1819

1920
## A first call
2021

21-
You make requests through a client. Feature results come back as GeoDataFrames, so
22-
you can map them right away.
22+
You make requests through a client. Routes with geometry come back as
23+
GeoDataFrames, so you can map them right away.
2324

2425
```python
2526
from closecity import Client
2627

27-
# The key (ck_live_ or ck_test_) comes from https://account.close.city
28+
# The key (ck_live_) comes from https://account.close.city
2829
close = Client("ck_live_your_key") # use your own key here
2930

3031
# Grocery stores within a 1.5 km walk of a point, as points:
3132
groceries = close.pois_search(lat = 41.823, lon = -71.412, radius_m = 1500, type = 30)
3233
groceries.plot()
3334
```
3435

35-
Catalog and lookup routes are free and need no key:
36+
Catalog and lookup routes are free, need no key, and come back as data frames:
3637

3738
```python
3839
close = Client()
39-
close.modes().data["modes"] # walk, bike, transit
40-
close.places("Providence").data["places"][0] # a city name to its GEOID and centre
40+
close.modes() # walk, bike, transit
41+
close.places("Providence") # a city name to its GEOID and centre
4142
```
4243

4344
## Words you will see
@@ -53,14 +54,20 @@ A few terms come up throughout the API:
5354
polygon.
5455
- **Catchment.** The reverse of an isochrone: every block that can reach a place.
5556

56-
## Spatial output, on or off
57+
## Choosing an output
5758

58-
Feature methods return GeoDataFrames by default. Block routes join census-block
59-
boundaries for you, using `pygris` (the `tiger` extra) to download them once. To work
60-
with the raw data instead, build the client with `spatial=False`:
59+
Set `output` on the client, or per call:
60+
61+
- `output = "spatial"` (the default) returns a GeoDataFrame where geometry applies
62+
and a DataFrame otherwise. Block routes join census-block boundaries with
63+
`pygris` (the `tiger` extra), downloaded once and cached.
64+
- `output = "tabular"` returns a plain DataFrame for every route and never
65+
downloads boundaries. Reach for it when you only want the numbers.
66+
- `output = "raw"` returns the underlying `Reply` / `Paginator`, with the parsed
67+
body on `.data` and the token counts alongside.
6168

6269
```python
63-
close = Client("ck_live_your_key", spatial = False) # use your own key here
70+
close = Client("ck_live_your_key", output = "raw") # use your own key here
6471
for poi in close.pois_search(lat = 41.823, lon = -71.412, radius_m = 1500):
6572
print(poi["name"])
6673
```

docs/api.rst

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,10 @@ Replies and pagination
1919
.. autoclass:: closecity.Paginator
2020
:members:
2121

22-
Spatial conversion
23-
------------------
22+
Output conversion
23+
-----------------
24+
25+
.. autofunction:: closecity.to_pandas
2426

2527
.. autofunction:: closecity.to_geopandas
2628

docs/getting_started.md

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -42,45 +42,65 @@ close = Client("ck_live_your_key") # use your own key here
4242
```
4343

4444
The catalog and lookup routes are free, so `Client()` with no key works for those.
45+
They come back as data frames:
4546

4647
```{code-cell} python
47-
close.modes().data["modes"]
48+
close.modes()
4849
```
4950

5051
## Look things up instead of guessing
5152

5253
Read the numeric id for a category from the catalog, and turn a city name into a
53-
GEOID and a centre point.
54+
GEOID and a centre point. Both are plain data frames, so you filter and index them
55+
the usual way.
5456

5557
```{code-cell} python
56-
types = close.destination_types().data["destination_types"]
57-
ids = {t["label"]: t["dest_type_id"] for t in types}
58-
grocery = ids["grocery_stores"]
58+
types = close.destination_types()
59+
grocery = types.loc[types["label"] == "grocery_stores", "dest_type_id"].iloc[0]
5960
60-
providence = close.places("Providence").data["places"][0]
61+
matches = close.places("Providence")
62+
providence = matches.iloc[0]
6163
providence["geoid"]
6264
```
6365

6466
## Make a call and map it
6567

66-
Feature methods return a GeoDataFrame, so you can map the result straight away.
68+
Routes with geometry return a GeoDataFrame, so you can map the result straight
69+
away.
6770

6871
```{code-cell} python
6972
groceries = close.pois_search(lat = providence["lat"], lon = providence["lon"],
7073
radius_m = 1500, type = grocery)
7174
groceries.plot(color = "#202a5b")
7275
```
7376

74-
## Turn spatial output off
77+
## Choose an output
7578

76-
Set the client's `spatial` flag to `False` to work with the raw data.
79+
Every route returns tabular data by default. The `output` setting controls the
80+
shape:
81+
82+
- `"spatial"` (the default) returns a GeoDataFrame where geometry applies, and a
83+
plain DataFrame otherwise.
84+
- `"tabular"` returns a plain DataFrame for every route and never downloads block
85+
boundaries. Reach for it when you only want the numbers.
86+
- `"raw"` returns the underlying reply, with the parsed body on `.data` and the
87+
token counts alongside.
88+
89+
Set it on the client, or pass `output=` to a single call.
7790

7891
```{code-cell} python
79-
close.spatial = False
92+
close.output = "raw"
8093
summary = close.block_summary("440070008001068", mode = "walk")
8194
summary.results
8295
```
8396

97+
```{code-cell} python
98+
:tags: [remove-cell]
99+
close.output = "spatial"
100+
```
101+
102+
In the frame modes, the token counts and other reply metadata ride on `df.attrs`.
103+
84104
## Errors
85105

86106
Problem responses become typed exceptions.

docs/index.rst

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,21 +9,24 @@ public transit. This is the data behind `close.city <https://close.city>`_.
99
1010
from closecity import Client
1111
12-
# The key (ck_live_ or ck_test_) comes from https://account.close.city
12+
# The key (ck_live_) comes from https://account.close.city
1313
close = Client("ck_live_your_key") # use your own key here
1414
15-
# Feature methods return a GeoDataFrame, ready to map:
15+
# Routes with geometry return a GeoDataFrame, ready to map:
1616
groceries = close.pois_search(lat = 41.823, lon = -71.412, radius_m = 1500)
1717
groceries.plot()
1818
19-
Install with ``pip install closecity``. The catalog and lookup routes are free; the
20-
data routes need a key from `account.close.city <https://account.close.city>`_.
19+
Results are tabular by default: a GeoDataFrame where geometry applies, a plain
20+
DataFrame otherwise. Install with ``pip install closecity``. The catalog and
21+
lookup routes are free; the data routes need a key from
22+
`account.close.city <https://account.close.city>`_.
2123

2224
.. toctree::
2325
:maxdepth: 2
2426
:caption: Contents
2527

2628
installation
2729
getting_started
30+
token_use
2831
tutorials/index
2932
api

docs/installation.rst

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,17 @@ the box.
1212
Optional extras
1313
---------------
1414

15-
Block methods (``blocks_query``, ``place_blocks``, ``poi_catchment``) join
16-
census-block boundaries. To let them download the boundaries for you, add the
17-
``tiger`` extra:
15+
In the default spatial output mode, block methods (``blocks_query``,
16+
``place_blocks``, ``poi_catchment``) join census-block boundaries. To let them
17+
download the boundaries for you, add the ``tiger`` extra:
1818

1919
.. code-block:: bash
2020
2121
pip install "closecity[tiger]"
2222
23+
You do not need it for ``output="tabular"``, which returns the same rows without
24+
geometry.
25+
2326
Plotting a GeoDataFrame uses matplotlib:
2427

2528
.. code-block:: bash
@@ -29,8 +32,8 @@ Plotting a GeoDataFrame uses matplotlib:
2932
API keys
3033
--------
3134

32-
The catalog and lookup routes are free and need no key. Every data route needs a key
33-
(``ck_live_`` or ``ck_test_``), created at
35+
The catalog and lookup routes are free and need no key. Every data route needs a
36+
key (``ck_live_``), created at
3437
`account.close.city <https://account.close.city>`_.
3538

3639
.. code-block:: python

docs/token_use.md

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
---
2+
jupytext:
3+
text_representation:
4+
extension: .md
5+
format_name: myst
6+
kernelspec:
7+
display_name: Python 3
8+
name: python3
9+
---
10+
11+
# Efficient token use
12+
13+
```{code-cell} python
14+
:tags: [remove-cell]
15+
import os
16+
from closecity import Client
17+
close = Client(os.environ.get("CLOSECITY_KEY"))
18+
```
19+
20+
The API is metered in tokens: one per returned row, minimum one per request, and
21+
10 per isochrone contour. Tokens are rows, so spending well is mostly about not
22+
asking for rows you will throw away.
23+
24+
## Free things
25+
26+
The whole catalog is free and keyless: `modes()`, `destination_types()`,
27+
`vintage()`, `places()`, `isochrone_meta()`, `last_updated()`, and `health()`. Do
28+
your lookups before you spend a token.
29+
30+
Revalidation is free too. Keep a reply's `etag` and pass it back as
31+
`if_none_match`; an unchanged response returns a free `304`. Read `output="raw"`
32+
when you want the etag:
33+
34+
```python
35+
close.output = "raw"
36+
first = close.block_summary("440070008001068", mode = "walk")
37+
again = close.block_summary("440070008001068", mode = "walk",
38+
if_none_match = first.etag)
39+
again.not_modified # True, and nothing was charged
40+
```
41+
42+
## The big levers
43+
44+
The rows a query returns multiply out as modes times types times blocks:
45+
46+
- **Pass `mode`.** Omitting it returns all three modes, so three times the rows.
47+
- **Request leaf types, not parents.** A parent type expands to its leaves, so
48+
`type = grocery` (one leaf) is a quarter of the rows of the `parks` parent.
49+
- **Shrink the area.** Cost grows with the square of `radius_m`, so halving the
50+
radius is roughly a quarter of the tokens.
51+
- **Use `max_minutes`.** It drops rows you would filter out anyway.
52+
53+
## Isochrones are the cheap reach primitive
54+
55+
An isochrone is 10 tokens per contour regardless of area, and `format="blocks"`
56+
returns every reachable block with its minutes. A whole walkshed for 10 tokens,
57+
where a catchment or block query charges per block:
58+
59+
```python
60+
shed = close.isochrone(block = "440070008001068", minutes = 30, mode = "walk",
61+
format = "blocks")
62+
```
63+
64+
## Watch what you spend
65+
66+
Every metered reply carries the token counts. In the frame output modes they ride
67+
on `df.attrs`; in `output="raw"` they are on the reply.
68+
69+
```{code-cell} python
70+
groceries = close.pois_search(lat = 41.823, lon = -71.412, radius_m = 1200)
71+
groceries.attrs["tokens_charged"], groceries.attrs["tokens_remaining"]
72+
```
73+
74+
When you only want the numbers, `output="tabular"` skips the block-boundary
75+
download entirely. The block boundaries the spatial mode joins are cached locally,
76+
so re-running a script costs time, not tokens; pass a `block_geometry` frame you
77+
already have to skip even that.

docs/tutorials/amenity_basket.md

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ where the gaps are. The idea follows
1717
[this analysis](https://nathenry.com/writing/2023-02-07-seattle-walkability.html),
1818
here applied to Richmond, Virginia.
1919

20+
*Running this tutorial uses about 3,500 tokens.*
21+
2022
```{code-cell} python
2123
:tags: [remove-cell]
2224
import os
@@ -36,8 +38,8 @@ close = Client("ck_live_your_key") # use your own key here
3638
```
3739

3840
```{code-cell} python
39-
types = close.destination_types().data["destination_types"]
40-
ids = {t["label"]: t["dest_type_id"] for t in types}
41+
types = close.destination_types()
42+
ids = dict(zip(types["label"], types["dest_type_id"]))
4143
basket = {
4244
"grocery": ids["grocery_stores"],
4345
"library": ids["libraries"],
@@ -47,7 +49,7 @@ basket = {
4749
"cafe": ids["cafes"],
4850
}
4951
50-
city = close.places("Richmond").data["places"][0]
52+
city = close.places("Richmond").iloc[0]
5153
```
5254

5355
## Pull the blocks, with population
@@ -86,6 +88,20 @@ one_per_block = one_per_block.assign(
8688
one_per_block.plot(column = "has_transit", cmap = "Greens")
8789
```
8890

91+
## The 15-minute-city score
92+
93+
Count, for each block, how many of the six amenities are within a 15-minute walk.
94+
That score, from 0 to 6, is the map planners reach for. It reuses the data you
95+
already pulled, so it costs nothing more.
96+
97+
```{code-cell} python
98+
covered = blocks[blocks.travel_time <= 15]
99+
score = 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)
103+
```
104+
89105
## Who can reach all six
90106

91107
A block is fully covered only if all six amenities are within 15 minutes.

docs/tutorials/competitor_walksheds.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ does. Its **walkshed** is every residential block that can walk to it. This tuto
1515
finds nearby competitors and measures how much of that walkshed they share. The
1616
example city is Providence, Rhode Island.
1717

18+
*Running this tutorial uses about 900 tokens.*
19+
1820
```{code-cell} python
1921
:tags: [remove-cell]
2022
import os
@@ -33,11 +35,11 @@ close = Client("ck_live_your_key") # use your own key here
3335
```
3436

3537
```{code-cell} python
36-
types = close.destination_types().data["destination_types"]
37-
ids = {t["label"]: t["dest_type_id"] for t in types}
38+
types = close.destination_types()
39+
ids = dict(zip(types["label"], types["dest_type_id"]))
3840
cafe = ids["cafes"]
3941
40-
city = close.places("Providence").data["places"][0]
42+
city = close.places("Providence").iloc[0]
4143
```
4244

4345
## Find the shops

0 commit comments

Comments
 (0)