Tracking Change Over Time
The /v1/timeseries endpoint returns a single variable's value across multiple vintages for one geography. Use it when the question is about change — growth, decline, trend — rather than a snapshot.
When to Use Timeseries
Use /v1/timeseries when you need to answer questions like:
- Is population growing or shrinking in this county?
- How has median household income changed over the last 16 vintages?
- Which metro areas are seeing the fastest age shifts?
For a single vintage across many places, use /v1/compare instead. Timeseries is specifically optimized for the time dimension.
For ACS 5-Year data, each vintage aggregates five years of survey responses. The 2023 vintage covers 2019–2023; the 2024 vintage covers 2020–2024. Four of those five years overlap between consecutive vintages. This means year-over-year changes are smoothed — a single sharp event (a plant closure, a disaster) won't show up as a spike. Interpret trends directionally, not as precise annual measurements. ACS 1-Year data provides single-year snapshots without this overlap, but only for geographies with 65,000+ population.
Basic Example: Texas Population Across Three Vintages
curl -G "https://api.prairiecloud.io/v1/timeseries" \
-H "X-API-Key: pck_live_your_key_here" \
--data-urlencode "variable=pop_total" \
--data-urlencode "geo=state:48" \
--data-urlencode "start_vintage=2009" \
--data-urlencode "end_vintage=2024" \
--data-urlencode "dataset=acs5"
import requests
resp = requests.get(
"https://api.prairiecloud.io/v1/timeseries",
headers={"X-API-Key": "pck_live_your_key_here"},
params={
"variable": "pop_total",
"geo": "state:48",
"start_vintage": 2009,
"end_vintage": 2024,
"dataset": "acs5",
},
)
data = resp.json()
Parameters:
| Parameter | Required | Description |
|---|---|---|
variable | Yes | Single variable api_name |
geo | Yes | Single geography specifier |
start_vintage | No | Start of vintage range (inclusive). Defaults to earliest available. |
end_vintage | No | End of vintage range (inclusive). Defaults to latest available. |
dataset | No | Dataset key (default: acs5) |
Response:
{
"data": {
"variable": { "api_name": "pop_total", "label": "Total Population" },
"geo": { "geo_key": "state:48", "geo_name": "Texas" },
"series": [
{ "vintage_year": 2022, "value_numeric": 29527941, "moe_numeric": null, "value_is_null": false, "null_reason": null },
{ "vintage_year": 2023, "value_numeric": 30029572, "moe_numeric": null, "value_is_null": false, "null_reason": null },
{ "vintage_year": 2024, "value_numeric": 30503301, "moe_numeric": null, "value_is_null": false, "null_reason": null }
]
},
"meta": { "source": { "dataset": "acs5", "vintages_available": [2022, 2023, 2024] }, ... },
"links": { "self": "..." }
}
Each entry in series is one vintage, ordered chronologically. The moe_numeric field carries the margin of error when the Census Bureau provides one — null at the state level, populated for smaller geographies.
Comparing Multiple Geographies Over Time
/v1/timeseries accepts one geography per request. To compare trends across multiple geographies, make parallel requests:
from concurrent.futures import ThreadPoolExecutor
import requests
API_KEY = "pck_live_your_key_here"
HEADERS = {"X-API-Key": API_KEY}
GEOS = ["state:48", "state:06", "state:36"] # TX, CA, NY
def fetch_timeseries(geo):
r = requests.get(
"https://api.prairiecloud.io/v1/timeseries",
headers=HEADERS,
params={
"variable": "pop_total",
"geo": geo,
"start_vintage": 2022,
"end_vintage": 2024,
"dataset": "acs5",
},
)
data = r.json()["data"]
return data["geo"]["geo_name"], data["series"]
with ThreadPoolExecutor() as pool:
results = list(pool.map(fetch_timeseries, GEOS))
for name, series in results:
print(name)
for point in series:
print(f" {point['vintage_year']}: {point['value_numeric']:,}")
Output:
Texas
2022: 29,527,941
2023: 30,029,572
2024: 30,503,301
California
2022: 39,029,342
2023: 38,965,193
2024: 38,940,231
New York
2022: 19,677,151
2023: 19,571,216
2024: 19,453,561
If you need the same variable across many geographies for one vintage, use /v1/compare instead — it handles multiple geos in a single request.
Real-World Use Case: Is Median Household Income Rising in My County?
Question: Has median household income in Travis County, TX (Austin) increased from 2022 to 2024?
import requests
resp = requests.get(
"https://api.prairiecloud.io/v1/timeseries",
headers={"X-API-Key": "pck_live_your_key_here"},
params={
"variable": "income_median_household",
"geo": "county:48453", # Travis County, TX
"start_vintage": 2009,
"end_vintage": 2024,
"dataset": "acs5",
},
)
data = resp.json()["data"]
print(f"Geography: {data['geo']['geo_name']}")
for point in data["series"]:
moe_str = f" ± ${point['moe_numeric']:,}" if point["moe_numeric"] else ""
print(f" {point['vintage_year']}: ${point['value_numeric']:,}{moe_str}")
# Calculate nominal change
values = [p["value_numeric"] for p in data["series"] if p["value_numeric"] is not None]
if len(values) >= 2:
change = values[-1] - values[0]
pct = (change / values[0]) * 100
print(f"\nChange (2022→2024): ${change:+,} ({pct:+.1f}%)")
Output:
Geography: Travis County, Texas
2022: $84,207 ± $1,203
2023: $86,441 ± $1,118
2024: $89,330 ± $1,074
Change (2022→2024): +$5,123 (+6.1%)
When evaluating whether a change is real, compare the magnitude of the change against the margins of error. A $5,123 increase with ±$1,203 MOE is meaningful. A $800 increase with ±$1,200 MOE is not statistically distinguishable from zero.
Understanding the Overlap Caveat
Because consecutive ACS 5-Year vintages share four of five survey years, they are not independent measurements. With 16 vintages (2009–2024), this overlap is important to understand:
| Vintage | Yr -4 | Yr -3 | Yr -2 | Yr -1 | Yr 0 |
|---|---|---|---|---|---|
| 2022 | 2018 | 2019 | 2020 | 2021 | 2022 |
| 2023 | 2019 | 2020 | 2021 | 2022 | |
| 2024 | 2020 | 2021 | 2022 |
For fully independent estimates, compare vintages 5 years apart (e.g., 2009, 2014, 2019, 2024). PrairieCloud's 16-vintage range makes this easy — you get three fully independent data points spanning 15 years.
A value that changed sharply in a single year will be diluted across five years in each estimate. This is by design — it's what makes ACS 5-Year reliable for small geographies. But it means:
- Don't treat vintage-to-vintage differences as annual changes.
- Do use the trend direction as a signal — if values are consistently moving in one direction across three vintages, the underlying reality is almost certainly moving that direction.
- For sharp year-specific events, check ACS 1-Year data (not available via PrairieCloud) or supplement with other sources.
/v1/timeseries accepts a single variable and a single geo per call. To track multiple variables or geographies over time, make parallel requests and merge the results.
Practical Pattern: Multi-Variable Timeseries
import requests
from concurrent.futures import ThreadPoolExecutor
API_KEY = "pck_live_your_key_here"
HEADERS = {"X-API-Key": API_KEY}
BASE = "https://api.prairiecloud.io"
VARIABLES = ["income_median_household", "pop_total", "tenure_owner_occupied"]
GEO = "county:48453" # Travis County, TX
START = 2022
END = 2024
def fetch_series(variable):
r = requests.get(
f"{BASE}/v1/timeseries",
headers=HEADERS,
params={"variable": variable, "geo": GEO, "start_vintage": START, "end_vintage": END, "dataset": "acs5"},
)
return variable, r.json()["data"]["series"]
with ThreadPoolExecutor() as pool:
results = dict(pool.map(lambda v: fetch_series(v), VARIABLES))
# results["income_median_household"] → [{vintage: 2022, value: ...}, ...]
Next Steps
- Comparing Geographies — side-by-side comparison for a single vintage across many places
- Variable Catalog — browse and search available
api_nameidentifiers - API Reference: /v1/timeseries — complete parameter docs including filtering and pagination