q20·intermediate

Is the growing season shifting — are plants greening up earlier or staying green longer?

biospherevegetationclimate Datasets: 4 5–15 min (small AOI) using cloud-direct access

Phenology is the calendar of plant life: the date leaves come out in spring (greenup), the date they turn and fall (senescence/dormancy), and how long the green stays in between. Satellites watch whole landscapes turn green and brown from orbit every year. So you can ask a simple question. Are plants waking up earlier, or staying green longer, than they used to?

Is the growing season shifting — are plants greening up earlier or staying green longer?

Phenology is the calendar of plant life: the date leaves come out in spring (greenup), the date they turn and fall (senescence/dormancy), and how long the green stays in between. Satellites watch whole landscapes turn green and brown from orbit every year. So you can ask a simple question. Are plants waking up earlier, or staying green longer, than they used to?

Which data to use, and how

Use this dataset: MODIS MCD12Q2 (short name MCD12Q2), NASA’s ready-made phenology product. It has already found the greenup date for you. For every 500-metre patch of ground, once a year, it records the day the plants greened up, peaked, and went dormant. That’s why it’s the easiest start: the hard part is pre-computed. (Later you can add the raw greenness datasets below for more detail.)

Then do four steps (the runnable code further down does all of this; this is just the idea):

  1. Pick your region. A small latitude/longitude box around it, and the range of years you want to compare.
  2. Read the dates. For each year, pull two numbers out of MCD12Q2: the greenup day and the dormancy day. Growing-season length is just dormancy − greenup.
  3. Average your region. Take the average greenup day across your whole box for each year, so you have one number per year instead of millions of pixels.
  4. Fit a trend. Draw a straight line through “greenup day vs. year.” A downward slope means greenup is creeping earlier (days per decade). Do the same for dormancy and for season length.

That’s the whole method. Below, “How a scientist answers this” names the exact tools, and the Run it cell does it live on synthetic data so you can see each step work.

What you can find out

  • Whether greenup is starting earlier over your chosen years (the trend in MCD12Q2’s Greenup day-of-year, either for the whole region or pixel-by-pixel).
  • Whether plants stay green longer: later Senescence/Dormancy dates, and a longer growing season (Dormancy − Greenup).
  • Whether peak greenness is shifting: the date of peak green (MidGreenup/Peak) and how much green there is (EVI_Amplitude/EVI_Area, EVI being a satellite “greenness” index).
  • Whether the season is getting more productive, by adding the greenness datasets MOD15A2H (a leaf-area measure called LAI/FPAR) or MOD13Q1 (the NDVI/EVI greenness indices) and summing greenness across the season.
  • Whether the shift tracks warmer springs. If you bring in a temperature record of your own, you can line the two up and see if earlier greenup matches warmer years.

What it can’t tell you

  • Why the season shifted. The data shows that greenup moved, not what moved it: warming, more CO₂, or a farmer changing what they plant. Pinning down the cause needs extra climate or land-management data.
  • How much carbon the plants took in. Greenness tells you there are leaves, not how much carbon they captured. For actual carbon uptake you need a different product (a “flux” product like MOD17 GPP, or ground towers).
  • Which crop is which. Each pixel is 250–500 m wide and blends whole fields together, and MCD12Q2 reports one generic “land greening” date. It can’t tell corn from soybean.
  • Timing finer than the satellite’s rhythm. The greenness datasets only refresh every 8 or 16 days, so a greenup date is never more precise than that window. Don’t read a 3-day shift as real.
  • Whether two green-ups a year mean double-cropping. MCD12Q2 can report up to two green-up cycles per pixel, but it won’t tell you if the second one is a second planted crop or just noise.

Gotchas to watch for

  • The dates aren’t day-of-year, so convert them first. MCD12Q2 stores greenup as “days since 1 January 1970,” not “day 105 of this year.” You have to convert. Also throw out the fill value 32767, which marks “no valid date here” (e.g. bare ground). Leave it in and it wrecks your average.
  • There can be two cycles per pixel, so pick the right one. MCD12Q2 reports up to two green-up cycles a year (cycle 0 and cycle 1). For ordinary single-season areas use cycle 0. Only double-cropped or irrigated land fills in cycle 1.
  • Very dense plants max out the greenness meter. The leaf-area measure (LAI) flattens out once a canopy is thick (around LAI 5–6), so a lush cornfield can look “equally green” across its peak even when it isn’t. Matters only if you compare peak-summer productivity in dense crops.
  • The old satellite ages, so cross-check it. MODIS Terra’s sensor drifts late in the record, so a long-term trend can partly be the instrument aging, not the plants. The fix: confirm the trend against the newer VIIRS record (VNP15A2H) before believing a multi-decade signal.
  • 23 years is short for climate claims. A two-decade record is brief for a climate trend. Report the trend with its confidence interval (the plausible range), not as a settled fact.

The real-data code

The Run it cell above runs this method on synthetic data with no login. Below is the whole recipe against the real archive: search MCD12Q2, open the HDF-EOS phenology layers, convert the dates, region-average each year, fit the trend in days-per-decade, and plot it. It needs a free Earthdata Login.

import earthaccess, numpy as np, xarray as xr
import datetime as dt
import matplotlib.pyplot as plt

earthaccess.login(strategy="netrc")

aoi = (-96.0, 39.0, -87.0, 44.0)        # your region as (W, S, E, N) — here, the US Corn Belt
years = range(2001, 2022)               # one MCD12Q2 granule set per year

# 1. One greenup/dormancy/length number per year. MCD12Q2 stores Greenup & Dormancy
#    as "days since 1970-01-01"; convert to day-of-year, drop the 32767 fill, use
#    cycle 0 (band 0) for single-season land, and area-average over the box.
def yearly_phenology(y):
    grans = earthaccess.search_data(
        short_name="MCD12Q2", bounding_box=aoi,
        temporal=(f"{y}-01-01", f"{y}-12-31"))
    gd, dd = [], []
    for fh in earthaccess.open(grans):
        ds = xr.open_dataset(fh, engine="rasterio", group="MODIS_Grid_2D")
        green = ds["Greenup"].isel(band=0).values.astype("float64")
        dorm = ds["Dormancy"].isel(band=0).values.astype("float64")
        green[green == 32767] = np.nan       # 32767 = "no valid date" (bare ground)
        dorm[dorm == 32767] = np.nan
        # days-since-1970 -> day-of-year within year y
        jan1 = (dt.date(y, 1, 1) - dt.date(1970, 1, 1)).days
        gd.append(np.nanmean(green - jan1))
        dd.append(np.nanmean(dorm - jan1))
    return np.nanmean(gd), np.nanmean(dd)

greenup, dormancy = zip(*[yearly_phenology(y) for y in years])
greenup, dormancy = np.array(greenup), np.array(dormancy)
length = dormancy - greenup             # season length = dormancy - greenup

# 2. Fit a straight line of region-mean day vs year; slope*10 = days per decade.
yrs = np.array(list(years))
def trend(series):
    ok = ~np.isnan(series)
    slope = np.polyfit(yrs[ok], series[ok], 1)[0]
    return slope * 10.0                  # days per decade

g_dec, d_dec, l_dec = trend(greenup), trend(dormancy), trend(length)

# 3. One-line verdict.
verb = "earlier" if g_dec < 0 else "later"
print(f"Greenup is moving {verb}: {g_dec:+.1f} days/decade | "
      f"dormancy {d_dec:+.1f} d/dec | season length {l_dec:+.1f} d/dec")

# 4. Plot all three series with their fitted lines.
fig, ax = plt.subplots()
for series, lab in [(greenup, "greenup"), (dormancy, "dormancy"), (length, "length")]:
    ok = ~np.isnan(series)
    ax.plot(yrs[ok], series[ok], "o-", label=lab)
    ax.plot(yrs[ok], np.poly1d(np.polyfit(yrs[ok], series[ok], 1))(yrs[ok]), "--", lw=1)
ax.set_xlabel("year"); ax.set_ylabel("day of year"); ax.legend()
plt.tight_layout(); plt.show()

# Before you trust it: re-run against the VIIRS record (VNP15A2H) — MODIS Terra's sensor
# drifts late in the record, so confirm the trend survives the instrument handoff (see gotchas).

Where the data comes from

This is the simple case: everything lives in one place under one free login. All four products (MCD12Q2, MOD15A2H, MOD13Q1, and the VIIRS continuation VNP15A2H) come from NASA’s LP DAAC (the Land Processes archive) via Earthdata Login. The only real work is stitching time together (MODIS hands off to VIIRS around 2012) and opening the slightly fiddly HDF-EOS file format. No juggling multiple logins. If you later add a climate record from another archive, see recipes/r01-three-daac-composition.mdx for the multi-archive pattern.

Sources + further reading

How a scientist answers this
Parameters
Land-surface phenology dates as day-of-year from MODIS MCD12Q2 (500 m, annual): Greenup, MidGreenup, Peak, Senescence, Dormancy, plus EVI_Amplitude/EVI_Area per cycle; season length = Dormancy − Greenup. Productivity proxies come from MOD15A2H LAI/FPAR (8-day) and MOD13Q1 NDVI/EVI (16-day, 250 m) integrated over the season, with VIIRS VNP15A2H extending the record post-MODIS. Use only good-quality cycles (QA/overall-quality flags) and report trend slope per year in days/year.
Method
For each pixel or AOI mean, build a per-year time series of Greenup/Dormancy day-of-year and fit a robust Theil–Sen slope with a Mann–Kendall significance test; growing-season length and its trend follow from Dormancy − Greenup, and seasonal productivity from the LAI/NDVI integral. Earlier greenup is a negative day-of-year slope; longer seasons are a positive length slope.
Validation
Cross-check MCD12Q2 dates against an independent greenness series (MOD13Q1/MOD15A2H seasonal curves) and against an external spring-temperature record; report cycle counts and QA-pass fraction, and note the ~8–16-day cadence sets the floor on date precision and that mixed 250–500 m pixels blur crop-specific timing.
In plain EnglishTrack the calendar day each year when plants green up and when they brown off, then test whether those dates are drifting earlier or later and whether the green season is getting longer — by more than the normal year-to-year jitter.

Make it yours → Set your area of interest, the span of years, and which phenology metric (Greenup, Dormancy, or season length) the notebook trends, and swap in VIIRS VNP15A2H to extend past the MODIS record.

🧪 Virtual Mock Lab stand-in data · runs in your browser · no login

A safe place to practise the method on MCD12Q2. The code is the real earthaccess workflow, but we feed it stand-in data shaped like the real product — so you can run it, change the numbers, and break it without an account. When you want the real data, use the recipe under “The real-data code” above, or download the notebook below.

editable · runs in your browser

Datasets used

Computed · real data Worked examples, computed end-to-end on real satellite data — not yet scientist-verified · expand ▾
Real trends, computed on Google Earth Engine (area-weighted Theil–Sen + Mann–Kendall).

We ran the analysis on real data for Indian regions and report exactly what came back — including the honest nulls and refusals. Each card shows the slope, significance, and (for rainfall) a gauge cross-check.

Has growing-season greenness (NDVI) over Punjab changed since 2003?
greenness (NDVI) Punjab 2003–2022
+0.003 NDVI (trend) significant trend
95% CI [0.002, 0.004] Mann–Kendall p = 0.0001
computed · ERA5/GEE · not yet scientist-verified
Has growing-season greenness (NDVI) over the Indo-Gangetic Plain changed since 2003?
greenness (NDVI) Indo-Gangetic Plain 2003–2022
+0.003 NDVI (trend) significant trend
95% CI [0.001, 0.003] Mann–Kendall p = 0
computed · ERA5/GEE · not yet scientist-verified
Has growing-season greenness (NDVI) over Marathwada changed since 2003?
greenness (NDVI) Marathwada 2003–2022
+0.003 NDVI (trend) significant trend
95% CI [0.001, 0.005] Mann–Kendall p = 0.0032
computed · ERA5/GEE · not yet scientist-verified
Has growing-season greenness (NDVI) over Gujarat changed since 2003?
greenness (NDVI) Gujarat 2003–2022
+0.003 NDVI (trend) significant trend
95% CI [0.002, 0.004] Mann–Kendall p = 0.0001
computed · ERA5/GEE · not yet scientist-verified
Has growing-season greenness (NDVI) over Madhya Pradesh changed since 2003?
greenness (NDVI) Madhya Pradesh 2003–2022
+0.003 NDVI (trend) significant trend
95% CI [0.002, 0.005] Mann–Kendall p = 0.0001
computed · ERA5/GEE · not yet scientist-verified
Has growing-season greenness (NDVI) over Tamil Nadu changed since 2003?
greenness (NDVI) Tamil Nadu 2003–2022
+0.002 NDVI (trend) significant trend
95% CI [0, 0.004] Mann–Kendall p = 0.0252
computed · ERA5/GEE · not yet scientist-verified

See all verified answers →