Skip to main content

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.

ACS 5-Year data overlaps across vintages

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:

ParameterRequiredDescription
variableYesSingle variable api_name
geoYesSingle geography specifier
start_vintageNoStart of vintage range (inclusive). Defaults to earliest available.
end_vintageNoEnd of vintage range (inclusive). Defaults to latest available.
datasetNoDataset 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
Use /v1/compare for single-vintage multi-geo

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%)
Interpreting MOE on trends

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:

VintageYr -4Yr -3Yr -2Yr -1Yr 0
202220182019202020212022
20232019202020212022
2024202020212022
Use wider vintage gaps for cleaner trend analysis

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.
One variable, one geography per request

/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