Computing Greenness Exposure for Birth Cohorts
This guide, part of Remote Sensing Indices for Health Studies, works through a specific and unusually error-prone assignment: residential greenness during pregnancy, where the exposure window is short, dated per subject, and overlaps a seasonal cycle that is itself correlated with the outcome.
Problem Context & Constraints
The straightforward approach takes one annual greenness composite, samples it in a buffer around the maternal address, and calls it exposure. It fails on a birth cohort for a reason that is structural rather than technical: a birth in March and a birth in September have gestational windows covering different halves of the vegetation cycle, and birth month is associated with a long list of outcomes through pathways that have nothing to do with vegetation. Aligning the exposure to the gestational window therefore introduces seasonality into the exposure variable, and any association it produces is confounded by season unless season is handled explicitly.
The opposite error is equally common. Using a fixed summer composite for everyone removes the seasonal confound and also removes the within-subject timing that the trimester hypothesis depends on. If the study claims a second-trimester effect, the exposure must be measured in the second trimester; if it cannot be, the claim has to be about residential greenness generally rather than about timing.
Three constraints follow. The gestational window must be dated per subject, from an estimated conception date, not from birth year. Season must be a covariate, at minimum as birth month, and preferably as a term that allows a smooth seasonal effect. And leaf-off periods must be handled deliberately — a January window in a temperate deciduous region measures bare canopy, which is a real measurement of a different quantity, not a low-greenness neighbourhood.
Prerequisites
python3.11numpy1.26.4,pandas2.2.2,geopandas0.14.4,rasterio1.3.10- A dated stack of cloud-masked greenness composites at sub-annual resolution — monthly or 16-day
- Birth records with birth date, gestational age at birth, and a geocoded maternal address with match type
- A land-cover or canopy layer if the leaf-off correction is to be applied
Step-by-Step Solution
Compute the gestational window per subject, average the dated composites that fall inside it within the chosen buffer, and return both the window-aligned exposure and a season-neutral annual exposure so the analysis can compare them.
# Gestational-window greenness exposure from a dated composite stack.
# Pinned: numpy==1.26.4, pandas==2.2.2, geopandas==0.14.4, rasterio==1.3.10
import logging
import numpy as np
import pandas as pd
import geopandas as gpd
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
log = logging.getLogger("exposure.greenness")
DEFAULT_BUFFER_M = 300
def gestational_windows(births: pd.DataFrame) -> pd.DataFrame:
"""Conception-to-birth window and trimester boundaries per subject."""
ga_days = births["gestational_age_weeks"].astype(float) * 7.0
end = pd.to_datetime(births["birth_date"])
start = end - pd.to_timedelta(ga_days, unit="D")
out = pd.DataFrame({"study_id": births["study_id"].to_numpy(),
"win_start": start.to_numpy(), "win_end": end.to_numpy()})
out["t1_end"] = out["win_start"] + (out["win_end"] - out["win_start"]) / 3.0
out["t2_end"] = out["win_start"] + 2.0 * (out["win_end"] - out["win_start"]) / 3.0
out["birth_month"] = end.dt.month.to_numpy()
return out
def buffer_means_by_date(addresses: gpd.GeoDataFrame, composites: pd.DataFrame,
sample_fn, buffer_m: int = DEFAULT_BUFFER_M) -> pd.DataFrame:
"""Mean index inside the buffer for every composite date, per address.
composites: date, raster_path (one row per dated composite)
sample_fn: callable(raster_path, GeoDataFrame_of_buffers) -> array of means
"""
buf = addresses.copy()
buf["geometry"] = buf.geometry.buffer(buffer_m)
frames = []
for rec in composites.sort_values("date").itertuples():
vals = sample_fn(rec.raster_path, buf)
frames.append(pd.DataFrame({"study_id": buf["study_id"].to_numpy(),
"date": rec.date, "index": vals}))
out = pd.concat(frames, ignore_index=True)
log.info("sampled %d composite dates for %d addresses at %d m",
composites.shape[0], len(buf), buffer_m)
return out
def window_exposure(series: pd.DataFrame, windows: pd.DataFrame) -> pd.DataFrame:
"""Mean index inside the gestational window, per trimester, plus an annual mean."""
m = series.merge(windows, on="study_id", how="inner")
m["date"] = pd.to_datetime(m["date"])
inside = m[(m["date"] >= m["win_start"]) & (m["date"] <= m["win_end"])]
def agg(d):
t1 = d[d["date"] <= d["t1_end"].iloc[0]]["index"]
t2 = d[(d["date"] > d["t1_end"].iloc[0]) & (d["date"] <= d["t2_end"].iloc[0])]["index"]
t3 = d[d["date"] > d["t2_end"].iloc[0]]["index"]
return pd.Series({
"green_gestation": float(d["index"].mean()),
"green_t1": float(t1.mean()) if len(t1) else np.nan,
"green_t2": float(t2.mean()) if len(t2) else np.nan,
"green_t3": float(t3.mean()) if len(t3) else np.nan,
"n_composites": int(d["index"].notna().sum()),
})
win = inside.groupby("study_id").apply(agg, include_groups=False).reset_index()
# Season-neutral comparator: the same buffer averaged over ALL dates, which
# carries no birth-timing information and isolates the neighbourhood effect.
annual = (series.groupby("study_id")["index"].mean()
.rename("green_annual").reset_index())
out = win.merge(annual, on="study_id", how="outer").merge(
windows[["study_id", "birth_month"]], on="study_id", how="left")
thin = out["n_composites"] < 6
out.loc[thin, ["green_gestation", "green_t1", "green_t2", "green_t3"]] = np.nan
log.info("window exposure: n=%d thin_windows=%d median_composites=%.0f",
len(out), int(thin.sum()), out["n_composites"].median())
return out
Returning both the window-aligned and the season-neutral exposure is the design point. The annual mean over all dates at the same address contains no birth-timing information at all, so it estimates the neighbourhood greenness effect cleanly. The gestational mean adds timing and, inseparably, season. Running the analysis with both and comparing tells you how much of any association depends on the timing alignment, which is the question a reviewer will ask.
The trimester split uses the actual gestational age rather than a fixed 40 weeks, because preterm birth is frequently the outcome and assuming term length would make the exposure window a function of the outcome — a straightforward way to manufacture an association.
Validation & Edge Cases
Preterm births and truncated windows. A birth at 28 weeks has a shorter window with fewer composites in it. The composite-count threshold nulls the extreme cases, but the remainder still have systematically fewer observations, and that correlates with the outcome. Report the composite count as a covariate.
Maternal address is the address at delivery. Residential moves during pregnancy are common. Where a prenatal address is recorded, use it; where it is not, the exposure describes the delivery address and should be described as such.
Leaf-off windows in deciduous regions. A winter gestational window returns a genuinely low index, which is a correct measurement of bare canopy and a poor measure of the neighbourhood’s vegetation. Where the hypothesis is about vegetation presence rather than active vegetation, a canopy-cover product is a better exposure and carries no seasonality.
Composite cadence coarser than a trimester. A 16-day composite gives roughly six observations per trimester, which is adequate; a monthly composite gives three, and a quarterly one gives a single value that is not a mean of anything. Check the cadence against the shortest window the study will estimate before committing to a product, because no amount of care downstream recovers timing resolution the stack never had.
Address buffers that overlap water. A coastal or lakeside address buffer includes open water, which returns a strongly negative index and drags the mean down in a way unrelated to vegetation. Mask permanent water from the buffer and report the remaining land area, so two addresses with the same land greenness are not separated by how much sea their circle happened to include.
Evergreen and arid regions. In an evergreen forest the seasonal amplitude is small and the confounding is mild; in an arid city the amplitude is driven by irrigation rather than climate. The severity of the seasonal problem is regional, and it should be measured — plot the index cycle for the study area before choosing the design.
Buffer radius and the address itself. A 300 m buffer around a rural address includes farmland whose index dwarfs any residential vegetation. Consider excluding agricultural land cover where the hypothesis is about residential greenery.
INFO exposure.greenness sampled 46 composite dates for 22418 addresses at 300 m
INFO exposure.greenness window exposure: n=22418 thin_windows=412 median_composites=19
Measuring the seasonal amplitude of the study area before choosing a design takes minutes and tells you how severe the confounding will be:
Compliance Notes
Record the composite stack identifier and dates, the buffer radius and its justification, the gestational window derivation including the use of observed rather than assumed gestational age, the composite-count threshold, and both the window-aligned and season-neutral exposures with their distributions by birth month.
Birth records are protected health data and the maternal address is the identifying element, so the buffer sampling runs inside the protected environment and the outputs are keyed by study identifier. One study-design item belongs in the record as well: whether birth month or a smooth seasonal term entered the model. An analysis that assigns a season-dependent exposure without a seasonal adjustment has a known confounder left uncontrolled, and a reader needs to be able to check that rather than assume it.
Related Topics
- Remote Sensing Indices for Health Studies — the parent guide on index choice and what each one measures.
- Cloud Masking and Compositing for Exposure Rasters — producing the dated stack this assignment consumes.
- Choosing a Buffer Radius for Proximity Exposure — justifying the radius rather than inheriting it.