Time Series Analysis of Satellite Imagery Using the Google Earth Engine Python API

Table of Contents

Time Series Analysis of Satellite Imagery Using the Google Earth Engine Python API

Introduction: Cloud-Native Remote Sensing for Multi-Year Analysis

Monitoring vegetation dynamics, agricultural productivity, deforestation, drought, and land-cover change often requires satellite observations collected over multiple years. Processing this data locally can quickly become computationally expensive, particularly when an analysis involves thousands of satellite scenes, cloud masking, raster calculations, and repeated spatial reductions.

Time Series Analysis Google Earth Engine Python API workflows address this challenge by moving much of the computation to Google’s cloud infrastructure. Instead of downloading every Landsat or Sentinel-2 scene to a local workstation, researchers can filter image collections, calculate vegetation indices, mask clouds, and extract statistics using Earth Engine’s server-side processing system.

The Python API makes these workflows programmable and reproducible. Libraries such as geemap, pandas, and matplotlib provide the visualization and analysis tools needed to turn cloud-processed satellite observations into interpretable time series.

Time Series Analysis Google Earth Engine Python API

IV.4.5 Multi-temporal analysis and change detection | Belgian Platform on Earth Observation

Time Series Analysis Google Earth Engine Python API

9

This tutorial develops a complete workflow using Sentinel-2 surface reflectance imagery. You will define a study area, filter and cloud-mask satellite scenes, calculate the Normalized Difference Vegetation Index (NDVI), extract pixel-level observations into a Pandas DataFrame, and plot a multi-year trend.

The tutorial also provides a Landsat 8 alternative and explains how to scale the workflow for larger studies.

By the end, you will understand not only how to run the analysis, but also how to distinguish computation performed on Earth Engine’s servers from data processing performed by Python on your computer.

1. Understanding the Earth Engine Processing Architecture

Google Earth Engine (GEE) combines a large geospatial data catalog with distributed processing infrastructure and client libraries. Its Python API allows you to construct processing workflows without downloading the underlying satellite imagery.

A typical cloud-native remote sensing workflow follows this architecture:

[ Satellite Data Catalog ] (Sentinel-2 SR Harmonized / Landsat Collection 2)
         │
         ▼
[ Cloud Processing Infrastructure ] (Distributed Google Infrastructure)
         │  (Filter dates & geometry → Mask clouds → Calculate NDVI → Reduce observations)
         ▼
[ Compact Analytical Output ] (Pixel observations, monthly summaries, or regional statistics)

 

1.1 Why cloud processing matters

Consider an agricultural monitoring project covering several years of Sentinel-2 observations. Downloading all source scenes would require substantial local storage and repeated preprocessing.

With Earth Engine, operations such as filtering an ee.ImageCollection, mapping a cloud-mask function, and calculating an index are represented as server-side computations. Only the results you explicitly request or export need to be transferred to your environment.

This is especially useful when working with large areas, long temporal windows, or repeated analytical experiments. Earth Engine still applies usage limits and computational constraints, so cloud execution does not make every workflow unlimited or instantaneous.

 

1.2 Understanding server-side and client-side objects

One of the most important concepts in Earth Engine programming is the distinction between server-side proxy objects and ordinary Python values.

Operation Where it executes Example
Define a geometry Python constructs a request object ee.Geometry.Rectangle(...)
Filter satellite imagery Earth Engine server collection.filterDate(...)
Map a cloud-mask function Earth Engine server collection.map(mask_clouds)
Calculate NDVI Earth Engine server image.normalizedDifference(...)
Reduce a pixel through time Earth Engine server image.reduceRegion(...)
Convert a small result to a DataFrame Client retrieves results ee.data.computeFeatures(...)
Plot a Pandas DataFrame Local Python environment plt.plot(...)

An object such as ee.Image does not generally contain a fully downloaded raster in Python memory. It describes an Earth Engine image that can participate in a server-side computation.

For example:

collection = (    ee.ImageCollection("COPERNICUS/S2_SR_HARMONIZED")    .filterDate("2019-01-01", "2025-01-01"))

This creates a filtered image-collection expression. It does not download every scene.

By contrast, calling .getInfo() requests a result and transfers it to the client. Use that method carefully: retrieving a small image count or metadata value is reasonable, while retrieving a large raster or massive feature collection can be inefficient or fail.

 

2. Setting Up the Google Earth Engine Python Environment

2.1 Install the required libraries

The workflow uses four principal libraries:

  • earthengine-api provides the Earth Engine Python client.

  • geemap supports interactive map visualization and geospatial workflows.

  • pandas organizes extracted satellite observations.

  • matplotlib plots time-series trends.

In a Jupyter notebook or Google Colab, install them with:

%pip install -U earthengine-api geemap pandas matplotlib

In a terminal, use:

python -m pip install -U earthengine-api geemap pandas matplotlib

Google Colab is a convenient starting point because Earth Engine and geemap are supported in its notebook workflow. A local environment works too, provided authentication, package compatibility, and notebook rendering are configured correctly.

 

2.2 Authenticate and initialize Earth Engine

Earth Engine’s Python client requires authentication and initialization before it can execute requests.

First, create or select a Google Cloud project that you can use with Earth Engine, and ensure the project is appropriately configured for Earth Engine access.

Then run:

import eeimport geemapimport pandas as pdimport matplotlib.pyplot as plt# Authenticate your Google account.ee.Authenticate()# Replace this with your configured Google Cloud project ID.PROJECT_ID = "your-google-cloud-project-id"# Initialize the Earth Engine client.ee.Initialize(project=PROJECT_ID)print(ee.String("Earth Engine initialized successfully").getInfo())

The authentication process may open a browser window or provide a notebook-specific authorization flow. Follow the prompts for your environment. The project identifier must be replaced with a real project ID that you have permission to use.

 

Run authentication interactively when establishing the environment. For scheduled or unattended workflows, use an appropriately configured service account or another supported non-interactive authentication method rather than depending on a browser prompt.

2.3 Verify your environment

Before building the time series, confirm that Earth Engine can access the selected satellite collection.

collection = ee.ImageCollection(    "COPERNICUS/S2_SR_HARMONIZED")print("Images available:", collection.limit(1).size().getInfo())

This requests only a small metadata result. It is a useful check that your authentication, initialization, and collection access are working.

The Sentinel-2 Surface Reflectance Harmonized collection contains atmospherically corrected imagery and is a suitable starting point for vegetation-index analysis. Its imagery includes the red and near-infrared bands used by NDVI.

3. Step 1: Define the Study Area and Date Range

The first analytical step is to define the geographic region and temporal window. These parameters control which satellite observations enter the workflow.

For this example, the study area is a rectangular region around Nairobi, Kenya. The same approach can be adapted to farms, watersheds, forest reserves, or administrative boundaries.

3.1 Create a geometry

Earth Engine geometries use geographic coordinates in longitude–latitude order.

# Study area: example region around Nairobi, Kenya.roi = ee.Geometry.Rectangle([    36.65, -1.40,   # Minimum longitude, latitude    37.00, -1.15    # Maximum longitude, latitude])# A sample field-monitoring location inside the region.point = ee.Geometry.Point([    36.82, -1.286])# Display the geometry on an interactive map.m = geemap.Map()m.centerObject(roi, 10)m.addLayer(roi, {}, "Study area")m.addLayer(point, {"color": "red"}, "Sample location")m

The rectangle is illustrative rather than an official administrative boundary. For a real research project, use a validated polygon representing the actual study area.

roi will restrict the image collection to scenes intersecting the region. The point will be used later to extract the NDVI of an individual pixel over time.

3.2 Set the date range

Define the temporal window using explicit start and end dates.

START_DATE = "2019-01-01"END_DATE = "2025-01-01"

Earth Engine’s filterDate() treats the start date as inclusive and the end date as exclusive. This configuration therefore covers observations from January 1, 2019, through December 31, 2024.

A multi-year window is important when investigating seasonal variation, annual crop cycles, vegetation recovery, or longer-term changes. The appropriate study duration depends on the phenomenon being measured.

3.3 Understand the temporal sampling problem

Satellite image collections do not necessarily provide one clear observation for every day or month. Clouds, acquisition schedules, image footprints, and missing data create irregular sampling.

For example, a point may have several clear observations in one month and none in another. Treating every satellite acquisition as an equally spaced observation would therefore be misleading.

This workflow first extracts the available per-scene observations, then calculates monthly medians in Pandas. Months without valid observations remain missing instead of being automatically interpreted as zero vegetation.

4. Step 2: Filter Sentinel-2 Imagery and Mask Clouds

Cloud contamination is one of the principal sources of error in optical satellite time-series analysis. Clouds, cirrus, cloud shadows, and snow can distort vegetation indices and create false temporal changes.

We will use the Sentinel-2 Level-2A Surface Reflectance Harmonized collection:

COPERNICUS/S2_SR_HARMONIZED

For NDVI, the relevant bands are:

Band Description Native resolution
B4 Red reflectance 10 m
B8 Near-infrared reflectance 10 m
SCL Scene Classification Layer 20 m

The Scene Classification Layer identifies categories such as vegetation, bare ground, cloud shadows, clouds, cirrus, and snow. We will use it to remove problematic pixels before computing NDVI.

 

4.1 Filter the image collection

s2_raw = (    ee.ImageCollection("COPERNICUS/S2_SR_HARMONIZED")    .filterBounds(roi)    .filterDate(START_DATE, END_DATE)    .filter(        ee.Filter.lt("CLOUDY_PIXEL_PERCENTAGE", 80)    ))print("Candidate scenes:", s2_raw.size().getInfo())

These filters have different purposes:

  • filterBounds(roi) retains scenes that intersect the study area.

  • filterDate(...) restricts imagery to the required period.

  • CLOUDY_PIXEL_PERCENTAGE eliminates scenes with high overall cloud cover.

The cloud-percentage filter is only a preliminary screening step. It describes cloudiness at the scene level, not necessarily the exact cloud conditions at the sample point. A scene can be cloudy overall but clear over the study area, or relatively clear overall but cloudy at the point of interest.

The 80% threshold is deliberately permissive; adjust it to your study conditions.

4.2 Write a cloud-masking function

For this tutorial, we will exclude the following SCL classes:

  • Class 1: saturated or defective pixels.

  • Class 3: cloud shadows.

  • Class 7: low-probability clouds or unclassified pixels.

  • Classes 8 and 9: medium- and high-probability clouds.

  • Class 10: cirrus.

  • Class 11: snow or ice.

def mask_s2_clouds(image):    """    Mask clouds, cloud shadows, cirrus, snow/ice,    and defective pixels using the Sentinel-2 SCL band.    """    scl = image.select("SCL")    clear = (        scl.neq(1)        .And(scl.neq(3))        .And(scl.neq(7))        .And(scl.neq(8))        .And(scl.neq(9))        .And(scl.neq(10))        .And(scl.neq(11))    )    # Retain only the bands needed for NDVI and scale    # Sentinel-2 surface reflectance to reflectance units.    reflectance = (        image.select(["B4", "B8"])        .multiply(0.0001)        .updateMask(clear)    )    return reflectance.copyProperties(        image,        ["system:time_start", "system:index"]    )

Why apply the scale factor? Sentinel-2 surface reflectance values are stored as scaled integers. Multiplying by 0.0001 converts the selected bands to reflectance units.

For NDVI, this common multiplicative factor cancels mathematically in the ratio. Applying it here nevertheless makes the reflectance values easier to interpret and supports subsequent spectral calculations.

The SCL mask is a practical baseline, not a perfect cloud detector. Very thin clouds, cloud-edge effects, and some shadows may survive. For demanding applications, consider combining SCL masking with the Sentinel-2 cloud-probability collection and a spatial buffer around detected clouds.

 

4.3 Calculate NDVI for every image

The Normalized Difference Vegetation Index is defined as:

\[ NDVI=\frac{NIR-Red}{NIR+Red} \]

For Sentinel-2:

\[ NDVI=\frac{B8-B4}{B8+B4} \]

Healthy green vegetation commonly produces positive NDVI values because vegetation reflects strongly in the near-infrared region while absorbing red radiation. Bare ground, water, clouds, and other surfaces can produce different responses.

Now define a function that processes each image.

def add_ndvi(image):    """    Calculate NDVI from cloud-masked Sentinel-2    surface reflectance and preserve acquisition metadata.    """    ndvi = image.normalizedDifference(        ["B8", "B4"]    ).rename("NDVI")    return ndvi.copyProperties(        image,        ["system:time_start", "system:index"]    )s2_masked = s2_raw.map(mask_s2_clouds)ndvi_collection = s2_masked.map(add_ndvi)print(    "NDVI scenes:",    ndvi_collection.size().getInfo())

Notice that the workflow has two mapped functions. The first prepares and masks the reflectance bands; the second derives NDVI from those bands.

Because both functions are applied through ImageCollection.map(), the processing is constructed on the Earth Engine server rather than downloading each image into a Python loop.

The result is an ee.ImageCollection in which each image contains a single NDVI band and retains the satellite acquisition timestamp.

4.4 Display an NDVI image for quality control

Before extracting a time series, inspect a processed image to ensure the study area, band selection, and masking behave as expected.

first_ndvi = ee.Image(    ndvi_collection.sort("system:time_start").first())m = geemap.Map()m.centerObject(roi, 10)m.addLayer(    first_ndvi,    {        "min": -0.2,        "max": 0.9,        "palette": [            "8c510a",            "f6e8c3",            "01665e"        ]    },    "First available NDVI")m.addLayer(roi, {}, "Study boundary")m

This color stretch is intended for visualization, not for classifying land cover automatically. A green pixel is not necessarily a particular crop type, and NDVI alone cannot distinguish all types of vegetation or reliably establish the cause of a change.

Check several different dates, particularly wet and dry seasons, before interpreting temporal patterns.

5. Step 3: Prepare the Multi-Temporal NDVI Series

Once the image collection has been cleaned and NDVI calculated, the next task is to turn the individual acquisitions into an ordered temporal record.

There are two useful approaches:

  1. Per-acquisition time series: Retain each valid satellite observation. This preserves the original temporal detail but produces unevenly spaced measurements.

  2. Monthly composites: Combine all valid observations within each month, usually using a median reducer. This produces a more compact series and reduces some short-term noise.

For this tutorial, we will retain individual observations in Earth Engine and aggregate them to monthly medians after extraction. That gives us access to the available observations while making the final chart easier to interpret.

5.1 Understand image collection reducers

Earth Engine reducers summarize information across images or pixels, but the dimensions being reduced matter.

Operation What is summarized? Result
collection.median() Different images at each pixel One median image
collection.mean() Different images at each pixel One mean image
image.reduceRegion() Pixels within a geographic region A dictionary of statistics
image.reduceRegions() Pixels inside each feature in a collection A feature collection of statistics
collection.map(...) Each image independently A transformed collection

A median composite across all years would collapse the temporal dimension. It is useful for producing a representative image, but it cannot by itself show year-to-year trends.

For time-series analysis, retain the acquisition date of each observation and summarize the values over time only after you have extracted the relevant measurements. Earth Engine’s image-collection reduction methods are documented in its official reduction guide.

 

5.2 Preserve the acquisition timestamp

The timestamp is the link between an NDVI value and the date on which it was measured.

The functions above preserve system:time_start, so we can retrieve the observation date from each image using:

image.date().format("YYYY-MM-dd")

This is a server-side operation. It creates an Earth Engine date string rather than immediately converting the image metadata into a local Python date.

Preserving this property is important for joining observations to meteorological data, comparing vegetation with rainfall, and calculating seasonal or annual statistics.

6. Step 4: Extract Pixel-Level Time-Series Values into Pandas

A satellite image collection contains spatial data, not a ready-made time-series table. To plot NDVI at a particular location, we need to extract the value of the NDVI band for that pixel in every available image.

This example samples the point defined earlier. It retrieves the acquisition date and NDVI value for each observation.

6.1 Build a server-side feature collection

First, create a function that samples one image at the point location.

def image_to_feature(image):    """    Extract one NDVI value and its timestamp from an image.    This function is evaluated by Earth Engine.    """    image = ee.Image(image)    sample = image.select("NDVI").reduceRegion(        reducer=ee.Reducer.first(),        geometry=point,        scale=10,        maxPixels=1_000_000    )    return ee.Feature(        None,        {            "date": image.date().format("YYYY-MM-dd"),            "time_start": image.get("system:time_start"),            "ndvi": sample.get("NDVI")        }    )

The key parameters are:

  • ee.Reducer.first() retrieves the value of the sampled pixel at the requested location.

  • geometry=point identifies the sampling position.

  • scale=10 requests a nominal 10-meter analysis scale, matching the native resolution of Sentinel-2’s red and near-infrared bands.

  • maxPixels provides a safety limit for the reduction. It does not improve resolution or force the analysis to use more pixels.

Because the geometry is a point, this is a pixel-level sample, not an average over a 10-meter-radius circle.

Next, convert the image collection into a list and construct a feature for each image.

# Build the list on the Earth Engine server.image_list = ndvi_collection.toList(    ndvi_collection.size())# Convert each image into a feature containing# its timestamp and sampled NDVI value.features = ee.FeatureCollection(    image_list.map(image_to_feature))

Here, toList() and .map() construct server-side operations. No image raster is downloaded to Python in this step.

The list-based approach is convenient for a point time series over a moderate study period. For much larger collections, prefer a batch-export workflow rather than building and retrieving a very large list in a single interactive request.

6.2 Transfer the compact results to a Pandas DataFrame

Earth Engine’s ee.data.computeFeatures() can retrieve the properties of a feature collection as a Pandas DataFrame.

df = ee.data.computeFeatures({    "expression": features,    "fileFormat": "PANDAS_DATAFRAME"})print(df.head())print(df.columns)

The returned table should contain the properties date, time_start, and ndvi. It may also contain other system-generated properties depending on the client library and response.

Unlike downloading the original imagery, this operation transfers only the compact table of extracted observations.

Earth Engine’s Python quickstart demonstrates this server-to-Pandas pattern for feature collections.

 

6.3 Clean and sort the observations

The table may contain null values when the pixel is masked because of clouds, shadows, or missing data. These should not be treated as NDVI values of zero.

# Convert the extracted values to the expected types.df["date"] = pd.to_datetime(    df["date"],    errors="coerce")df["ndvi"] = pd.to_numeric(    df["ndvi"],    errors="coerce")# Remove invalid dates and masked observations.df = df.dropna(    subset=["date", "ndvi"]).copy()# Sort observations chronologically.df = df.sort_values("date").reset_index(drop=True)print(df.head(10))print("Valid observations:", len(df))

This step creates a chronological series of valid pixel observations.

Depending on the location and time of year, multiple scenes may contribute observations within a short period, while some periods may have no clear observations at all. This is expected for optical satellite monitoring in cloudy regions.

6.4 Aggregate the observations to monthly medians

To make the series easier to interpret, compute one median NDVI value per calendar month.

monthly_ndvi = (    df.set_index("date")["ndvi"]    .resample("MS")    .median()    .rename("NDVI"))print(monthly_ndvi.head(12))

"MS" means month start. Pandas creates a monthly index across the time span and calculates the median from all available observations in each month.

Months without valid observations remain NaN. They should generally remain missing in a time-series plot rather than being replaced with zero or automatically interpolated.

A monthly median reduces the influence of unusually high or low individual observations, but it does not guarantee that all atmospheric contamination is removed. Nor does it make each month equally representative of the true vegetation condition: a month with one observation has less temporal sampling than a month with several clear observations.

6.5 What if you need the average NDVI for a whole region?

The point-sampling method measures the pixel at a single location. To study average vegetation conditions across a farm, watershed, or administrative unit, the reducer must summarize pixels over that region instead.

In the image_to_feature() function, replace the sampling block with:

stats = image.select("NDVI").reduceRegion(    reducer=ee.Reducer.mean(),    geometry=roi,    scale=10,    maxPixels=1_000_000_000,    tileScale=4)

Then change the feature property from sample.get("NDVI") to stats.get("NDVI").

This produces one region-wide mean per acquisition rather than one pixel value. The choice of reducer, scale, and geometry affects the scientific meaning of the result. Earth Engine recommends specifying an appropriate scale and, when necessary, a coordinate reference system for regional reductions. tileScale can help with some memory-intensive reductions, but it does not remove all computation limits.

 

For agricultural studies, also consider masking non-crop pixels or using field-specific polygons. A regional mean that combines forests, urban surfaces, bare soil, and crops may obscure the process being investigated.

7. Step 5: Plot NDVI Trends with Matplotlib

Once observations are available in Pandas, conventional Python analysis tools can be used for exploratory data analysis, reporting, and statistical modeling.

7.1 Plot the monthly NDVI series

fig, ax = plt.subplots(figsize=(13, 5))ax.plot(    monthly_ndvi.index,    monthly_ndvi.values,    marker="o",    markersize=3,    linewidth=1.2,    label="Monthly median NDVI")ax.set_title("Sentinel-2 NDVI Time Series")ax.set_xlabel("Date")ax.set_ylabel("NDVI")ax.set_ylim(-1, 1)ax.grid(True, alpha=0.3)ax.legend()fig.tight_layout()plt.show()

The horizontal axis represents time, while the vertical axis represents the NDVI values of the sampled pixel after monthly aggregation.

The plot provides a first view of seasonal vegetation cycles, possible changes in land cover, and unusually low or high observations. At a regional scale, the same approach can be used to monitor vegetation conditions across multiple years.

A temporal line chart is only the initial analytical step. The apparent trend should be checked against observation counts, cloud-mask quality, land-cover context, and seasonal differences before assigning an ecological or agricultural explanation.

7.2 Add a rolling trend line

A rolling average can help reveal broader changes beneath short-term variability.

monthly_smoothed = monthly_ndvi.rolling(    window=3,    min_periods=2).mean()fig, ax = plt.subplots(figsize=(13, 5))ax.plot(    monthly_ndvi.index,    monthly_ndvi.values,    marker="o",    markersize=3,    linewidth=1,    label="Monthly median")ax.plot(    monthly_smoothed.index,    monthly_smoothed.values,    linewidth=2,    label="Three-month rolling mean")ax.set_title("NDVI Seasonal and Trend Components")ax.set_xlabel("Date")ax.set_ylabel("NDVI")ax.set_ylim(-1, 1)ax.grid(True, alpha=0.3)ax.legend()fig.tight_layout()plt.show()

This rolling mean is an exploratory smoothing method, not a formal trend model. It can blur abrupt changes, and its value depends on the chosen window length. For rigorous analysis, consider a seasonal decomposition, regression with seasonal terms, or a method specifically designed to detect changes in satellite time series.

7.3 Export the observations for reproducibility

Keep a copy of the extracted data so that later analysis can be reproduced without repeating the extraction request.

df.to_csv(    "sentinel2_ndvi_pixel_timeseries.csv",    index=False)monthly_ndvi.to_csv(    "sentinel2_ndvi_monthly_median.csv",    header=True)

These files contain the extracted time-series measurements, not the source satellite imagery.

For publication-quality analysis, retain the study-area geometry, date range, collection ID, mask logic, scale, reducer, and script version alongside the data. These are essential elements of the processing record.

8. Alternative Workflow: Landsat 8 Time-Series Analysis

Sentinel-2 provides 10-meter red and near-infrared observations, while Landsat 8 Collection 2 Level-2 surface reflectance provides 30-meter observations in the corresponding bands. Landsat is particularly valuable for longer-term change analysis when used with additional Landsat missions and an appropriate cross-sensor methodology.

For Landsat 8, the relevant surface-reflectance bands are:

Band Description Resolution
SR_B4 Red surface reflectance 30 m
SR_B5 Near-infrared surface reflectance 30 m
QA_PIXEL Pixel-level quality flags 30 m
QA_RADSAT Radiometric saturation flags 30 m

The collection identifier is LANDSAT/LC08/C02/T1_L2. Its surface-reflectance bands use a multiplicative scale factor of 0.0000275 and an additive offset of -0.2. These calibration values must be applied before calculating NDVI if you want the index to reflect the scaled surface reflectance correctly.

 

8.1 Filter and mask Landsat 8 imagery

The QA_PIXEL bit flags identify fill, dilated clouds, cirrus, clouds, cloud shadows, and snow. The following function masks pixels with any of those bits set and excludes scenes where any saturation flag is present in QA_RADSAT.

landsat_raw = (    ee.ImageCollection("LANDSAT/LC08/C02/T1_L2")    .filterBounds(roi)    .filterDate(START_DATE, END_DATE)    .filter(        ee.Filter.lt("CLOUD_COVER", 80)    ))def prepare_landsat8(image):    """    Mask poor-quality pixels, scale surface reflectance,    and calculate NDVI from Landsat 8.    """    qa = image.select("QA_PIXEL")    # Bits 0-5: fill, dilated cloud, cirrus,    # cloud, cloud shadow, and snow.    clear = qa.bitwiseAnd(63).eq(0)    # Conservative saturation mask.    not_saturated = image.select("QA_RADSAT").eq(0)    red = (        image.select("SR_B4")        .multiply(0.0000275)        .add(-0.2)    )    nir = (        image.select("SR_B5")        .multiply(0.0000275)        .add(-0.2)    )

The explicit NDVI ratio is useful here because scaled surface-reflectance values can occasionally be negative. Earth Engine’s normalizedDifference() masks a pixel if either input band is negative, which may unnecessarily remove some valid observations from a surface-reflectance analysis. The explicit expression avoids that particular behavior while masking pixels whose red-plus-NIR denominator is close to zero.

The saturation filter is intentionally conservative. It rejects a pixel if any of the Landsat bands flagged by QA_RADSAT are saturated, rather than checking only the two bands required for NDVI.

8.2 Reuse the point-extraction workflow

The output has the same NDVI band and retains system:time_start, so the existing extraction function can be reused:

landsat_list = landsat_ndvi.toList(    landsat_ndvi.size())landsat_features = ee.FeatureCollection(    landsat_list.map(image_to_feature))landsat_df = ee.data.computeFeatures({    "expression": landsat_features,    "fileFormat": "PANDAS_DATAFRAME"})landsat_df["date"] = pd.to_datetime(    landsat_df["date"],    errors="coerce")landsat_df["ndvi"] = pd.to_numeric(    landsat_df["ndvi"],    errors="coerce")landsat_df = (    landsat_df    .dropna(subset=["date", "ndvi"])    .sort_values("date"))landsat_monthly = (    landsat_df.set_index("date")["ndvi"]    .resample("MS")    .median())

The extraction function refers to the earlier point geometry, so it works for either satellite collection without changes.

8.3 Sentinel-2 versus Landsat for temporal analysis

Consideration Sentinel-2 Landsat 8
Spatial detail for NDVI 10 m 30 m
Surface-reflectance bands B4, B8 SR_B4, SR_B5
Temporal observations Generally more frequent Nominal 16-day revisit for Landsat 8 alone
Cloud masking SCL, optionally cloud probability QA_PIXEL, optionally additional quality checks
Long-term context Sentinel-2 observations begin in 2015 Landsat 8 observations begin in 2013; earlier Landsat missions extend the record
Best use Field-scale and detailed vegetation monitoring Long-term land-change analysis and Landsat-based monitoring

The nominal revisit interval does not guarantee a valid clear observation at the study location. Clouds and image availability determine the useful temporal sampling.

Important: Do not simply combine Sentinel-2 and Landsat NDVI values into one series and assume they are perfectly interchangeable. Their spectral response functions, spatial resolutions, acquisition dates, and processing characteristics differ. Cross-sensor time series may require harmonization, comparable spatial aggregation, and consistent quality masking.

9. Best Practices for Scalable Cloud-Native Remote Sensing

A working NDVI script is a starting point. Reliable multi-year analysis also requires attention to data quality, computational cost, and reproducibility.

9.1 Keep large operations on the Earth Engine server

Avoid downloading image collections or transferring intermediate raster data into Python unless you have a specific reason to do so.

Prefer:

# Server-side filtering and processing.ndvi_collection = (    ee.ImageCollection("COPERNICUS/S2_SR_HARMONIZED")    .filterBounds(roi)    .filterDate(START_DATE, END_DATE)    .map(mask_s2_clouds)    .map(add_ndvi))

Avoid repeatedly calling .getInfo() inside a loop over images. Each call requests a result from the server, which adds latency and may generate an excessive number of requests.

For exploration, retrieve small metadata values and compact tables. For larger analytical outputs, use Earth Engine export tasks.

 

9.2 Export large time series rather than retrieving everything interactively

When processing a large collection, many locations, or thousands of polygons, consider exporting the output as a table to Google Drive or Cloud Storage.

For example, the feature collection created earlier can be exported to Google Drive:

task = ee.batch.Export.table.toDrive(    collection=features,    description="Sentinel2_NDVI_TimeSeries",    fileFormat="CSV",    folder="EarthEngineExports")task.start()print("Export task submitted:", task.id)

This submits a server-side export task. Open the Earth Engine task interface or inspect the task status through the API to monitor completion. The task is not complete merely because start() returned successfully.

For very large studies, prefer a batch workflow over a synchronous Pandas transfer. For regional statistics, a useful architecture is to generate one feature per region and date, export the table, and perform downstream statistical analysis in Python.

9.3 Use appropriate temporal aggregation

The right aggregation interval depends on the scientific question.

Research objective Useful approach
Crop growth monitoring Weekly or monthly observations, depending on clear-scene availability
Seasonal vegetation patterns Monthly or seasonal medians
Drought monitoring NDVI anomalies relative to an appropriate historical baseline, ideally supported by rainfall or other indicators
Long-term vegetation trends Consistent annual or seasonal summaries with explicit handling of missing data
Abrupt land-cover change Dense observations and change-detection methods rather than smoothing alone

Monthly composites can reduce noise and improve interpretability, but they may hide short-lived events. Annual means can obscure seasonal differences. Select a temporal resolution before analysis and document why it suits the research question.

9.4 Improve the cloud mask when accuracy matters

SCL-based masking is convenient, but demanding applications may need a stronger cloud detection strategy. The Sentinel-2 cloud-probability collection, COPERNICUS/S2_CLOUD_PROBABILITY, provides pixel-level cloud probabilities from 0 to 100. It can be joined to Sentinel-2 surface reflectance imagery using matching acquisition identifiers and combined with spatial buffering around cloud detections.

A robust workflow should check whether the mask accidentally removes clear vegetation, whether cloud shadows remain, and whether the valid-pixel count changes systematically between seasons.

9.5 Match spatial resolution and masking across comparisons

For a comparison between farms or administrative units, use consistent reduction parameters and comparable study periods.

When mixing datasets, remember that specifying scale=10 does not make a 30-meter Landsat image into a native 10-meter observation. The analysis grid, original pixel size, projection, and resampling behavior all affect the result.

If the objective is a fair comparison between Sentinel-2 and Landsat, consider aggregating Sentinel-2 data to an appropriate common spatial support and accounting for differences between the sensors.

9.6 Check missing data before interpreting trends

A rising NDVI series may indicate improved vegetation, but it could also reflect seasonal timing, a change in observation frequency, residual cloud contamination, or a change in land cover.

Before drawing conclusions, inspect:

  • The number of valid observations in each month or year.

  • The seasonal pattern in NDVI.

  • The stability of the study area’s land cover.

  • Whether the cloud mask and spatial sampling are consistent.

  • Whether the apparent trend persists under reasonable alternative aggregation choices.

For formal trend detection, fit an appropriate statistical model and report uncertainty rather than treating a smoothed chart as proof of change.

10. Common Errors and Troubleshooting

Problem Likely cause Recommended fix
ee.Initialize() fails Missing credentials or incorrect/unconfigured Cloud Project Authenticate again and verify project configuration and Earth Engine access
NDVI collection is empty Incorrect dates, geometry, filters, or unavailable imagery Test filterBounds(), filterDate(), and scene counts separately
Many NDVI values are null Cloud or SCL masking, missing pixels, or location outside scene coverage Inspect source imagery and masks; verify the sample coordinate
.getInfo() times out or returns too much data Attempting to retrieve a large server-side object Reduce the output, inspect only small metadata values, or use a batch export
Monthly chart has gaps No valid clear observations during those months Keep gaps explicit, improve quality screening where justified, or report temporal coverage
Results differ between Sentinel-2 and Landsat Resolution, reflectance scaling, spectral response, sampling, or masking differences Harmonize the analysis design before comparing sensors
Regional reduction exceeds limits Too many pixels, complex geometry, or resource-intensive computation Check the analysis scale, simplify geometry where appropriate, use tileScale, or export in manageable batches

Conclusion

Time-series analysis using the Google Earth Engine Python API makes multi-year satellite monitoring practical without requiring researchers to download and store every source image locally. By combining Earth Engine’s server-side computation with Python’s data-analysis tools, you can create reproducible workflows for vegetation monitoring, crop assessment, environmental change, and land-surface analysis.

The central workflow is straightforward:

  1. Authenticate and initialize Earth Engine.

  2. Define a study area and time range.

  3. Filter Sentinel-2 or Landsat imagery and mask contaminated pixels.

  4. Calculate NDVI while preserving acquisition timestamps.

  5. Extract observations into a Pandas DataFrame.

  6. Aggregate and plot the time series.

  7. Validate data quality and export larger results for downstream analysis.

The most important engineering principle is to process the imagery in the cloud and bring only the analytical results into Python. Combine that architecture with consistent preprocessing, transparent missing-data handling, and appropriate statistical methods, and the result becomes far more useful than a simple satellite-image chart.

Official documentation and reference material