timeseries module¶
Module for timeseries analysis on Earth Engine data.
HarmonicRegression
¶
Perform harmonic regression on an Earth Engine ImageCollection.
Attributes:
Name | Type | Description |
---|---|---|
image_collection |
ee.ImageCollection |
Input time series of selected band. |
ref_date |
ee.Date |
Reference date to calculate time. |
band |
str |
Name of dependent variable band. |
order |
int |
Number of harmonics. |
omega |
float |
Base frequency multiplier. |
independents |
List[str] |
Names of independent variable bands. |
composite |
ee.Image |
Median composite of the selected band. |
Source code in geeagri/timeseries.py
class HarmonicRegression:
"""
Perform harmonic regression on an Earth Engine ImageCollection.
Attributes:
image_collection (ee.ImageCollection): Input time series of selected band.
ref_date (ee.Date): Reference date to calculate time.
band (str): Name of dependent variable band.
order (int): Number of harmonics.
omega (float): Base frequency multiplier.
independents (List[str]): Names of independent variable bands.
composite (ee.Image): Median composite of the selected band.
"""
def __init__(self, image_collection, ref_date, band_name, order=1, omega=1):
"""
Initialize the HarmonicRegression object.
Args:
image_collection (ee.ImageCollection): Input image collection.
ref_date (str or ee.Date): Reference date to compute relative time.
band_name (str): Name of dependent variable band.
order (int): Number of harmonics (default 1).
omega (float): Base frequency multiplier (default 1).
"""
self.image_collection = image_collection.select(band_name)
self.ref_date = ee.Date(ref_date) if isinstance(ref_date, str) else ref_date
self.band = band_name
self.order = order
self.omega = omega
# Names of independent variables: constant, cos_1, ..., sin_1, ...
self.independents = (
["constant"]
+ [f"cos_{i}" for i in range(1, order + 1)]
+ [f"sin_{i}" for i in range(1, order + 1)]
)
# Precompute median composite of the selected band
self.composite = self.image_collection.median()
def _add_time_unit(self, image):
"""
Add time difference in years from ref_date as band 't'.
Args:
image (ee.Image): Input image.
Returns:
ee.Image: Image with additional 't' band.
"""
dyear = ee.Number(image.date().difference(self.ref_date, "year"))
return image.addBands(ee.Image.constant(dyear).rename("t").float())
def _add_harmonics(self, image):
"""
Add harmonic basis functions: constant, cos_i, sin_i bands.
Args:
image (ee.Image): Input image.
Returns:
ee.Image: Image with added harmonic bands.
"""
image = self._add_time_unit(image)
t = image.select("t")
harmonic_bands = [ee.Image.constant(1).rename("constant")]
for i in range(1, self.order + 1):
freq = ee.Number(i).multiply(self.omega).multiply(2 * math.pi)
harmonic_bands.append(t.multiply(freq).cos().rename(f"cos_{i}"))
harmonic_bands.append(t.multiply(freq).sin().rename(f"sin_{i}"))
return image.addBands(ee.Image(harmonic_bands))
def get_harmonic_coeffs(self):
"""
Fit harmonic regression and return coefficients image.
Returns:
ee.Image: Coefficients image with bands like <band>_constant, <band>_cos_1, etc.
"""
harmonic_coll = self.image_collection.map(self._add_harmonics)
regression = harmonic_coll.select(self.independents + [self.band]).reduce(
ee.Reducer.linearRegression(len(self.independents), 1)
)
coeffs = (
regression.select("coefficients")
.arrayProject([0])
.arrayFlatten([self.independents])
.multiply(10000)
.toInt32()
)
new_names = [f"{self.band}_{name}" for name in self.independents]
return coeffs.rename(new_names)
def get_phase_amplitude(
self, harmonic_coeffs, cos_band, sin_band, stretch_factor=1, return_rgb=True
):
"""
Compute phase & amplitude and optionally create RGB visualization.
Args:
harmonic_coeffs (ee.Image): Coefficients image from get_harmonic_coeffs().
cos_band (str): Name of cosine coefficient band.
sin_band (str): Name of sine coefficient band.
stretch_factor (float): Stretch amplitude to enhance contrast.
return_rgb (bool): If True, return RGB image; else return HSV image.
Returns:
ee.Image: RGB visualization (uint8) or HSV image.
"""
phase = harmonic_coeffs.select(cos_band).atan2(harmonic_coeffs.select(sin_band))
amplitude = harmonic_coeffs.select(cos_band).hypot(
harmonic_coeffs.select(sin_band)
)
hsv = (
phase.unitScale(-math.pi, math.pi)
.addBands(amplitude.multiply(stretch_factor))
.addBands(self.composite)
)
if return_rgb:
return hsv.hsvToRgb().unitScale(0, 1).multiply(255).toByte()
else:
return hsv
def _fit_harmonics(self, harmonic_coeffs, image):
"""
Compute fitted values from harmonic coefficients and harmonic bands.
Args:
harmonic_coeffs (ee.Image): Coefficients image divided by 10000.
image (ee.Image): Image with harmonic bands.
Returns:
ee.Image: Image with fitted values.
"""
return (
image.select(self.independents)
.multiply(harmonic_coeffs)
.reduce("sum")
.rename("fitted")
.copyProperties(image, ["system:time_start"])
)
def get_fitted_harmonics(self, harmonic_coeffs):
"""
Compute fitted harmonic time series over the collection.
Args:
harmonic_coeffs (ee.Image): Coefficients image from get_harmonic_coeffs().
Returns:
ee.ImageCollection: Collection with fitted harmonic value as 'fitted' band.
"""
harmonic_coeffs_scaled = harmonic_coeffs.divide(10000)
harmonic_coll = self.image_collection.map(self._add_harmonics)
return harmonic_coll.map(
lambda img: self._fit_harmonics(harmonic_coeffs_scaled, img)
)
__init__(self, image_collection, ref_date, band_name, order=1, omega=1)
special
¶
Initialize the HarmonicRegression object.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
image_collection |
ee.ImageCollection |
Input image collection. |
required |
ref_date |
str or ee.Date |
Reference date to compute relative time. |
required |
band_name |
str |
Name of dependent variable band. |
required |
order |
int |
Number of harmonics (default 1). |
1 |
omega |
float |
Base frequency multiplier (default 1). |
1 |
Source code in geeagri/timeseries.py
def __init__(self, image_collection, ref_date, band_name, order=1, omega=1):
"""
Initialize the HarmonicRegression object.
Args:
image_collection (ee.ImageCollection): Input image collection.
ref_date (str or ee.Date): Reference date to compute relative time.
band_name (str): Name of dependent variable band.
order (int): Number of harmonics (default 1).
omega (float): Base frequency multiplier (default 1).
"""
self.image_collection = image_collection.select(band_name)
self.ref_date = ee.Date(ref_date) if isinstance(ref_date, str) else ref_date
self.band = band_name
self.order = order
self.omega = omega
# Names of independent variables: constant, cos_1, ..., sin_1, ...
self.independents = (
["constant"]
+ [f"cos_{i}" for i in range(1, order + 1)]
+ [f"sin_{i}" for i in range(1, order + 1)]
)
# Precompute median composite of the selected band
self.composite = self.image_collection.median()
get_fitted_harmonics(self, harmonic_coeffs)
¶
Compute fitted harmonic time series over the collection.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
harmonic_coeffs |
ee.Image |
Coefficients image from get_harmonic_coeffs(). |
required |
Returns:
Type | Description |
---|---|
ee.ImageCollection |
Collection with fitted harmonic value as 'fitted' band. |
Source code in geeagri/timeseries.py
def get_fitted_harmonics(self, harmonic_coeffs):
"""
Compute fitted harmonic time series over the collection.
Args:
harmonic_coeffs (ee.Image): Coefficients image from get_harmonic_coeffs().
Returns:
ee.ImageCollection: Collection with fitted harmonic value as 'fitted' band.
"""
harmonic_coeffs_scaled = harmonic_coeffs.divide(10000)
harmonic_coll = self.image_collection.map(self._add_harmonics)
return harmonic_coll.map(
lambda img: self._fit_harmonics(harmonic_coeffs_scaled, img)
)
get_harmonic_coeffs(self)
¶
Fit harmonic regression and return coefficients image.
Returns:
Type | Description |
---|---|
ee.Image |
Coefficients image with bands like |
Source code in geeagri/timeseries.py
def get_harmonic_coeffs(self):
"""
Fit harmonic regression and return coefficients image.
Returns:
ee.Image: Coefficients image with bands like <band>_constant, <band>_cos_1, etc.
"""
harmonic_coll = self.image_collection.map(self._add_harmonics)
regression = harmonic_coll.select(self.independents + [self.band]).reduce(
ee.Reducer.linearRegression(len(self.independents), 1)
)
coeffs = (
regression.select("coefficients")
.arrayProject([0])
.arrayFlatten([self.independents])
.multiply(10000)
.toInt32()
)
new_names = [f"{self.band}_{name}" for name in self.independents]
return coeffs.rename(new_names)
get_phase_amplitude(self, harmonic_coeffs, cos_band, sin_band, stretch_factor=1, return_rgb=True)
¶
Compute phase & amplitude and optionally create RGB visualization.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
harmonic_coeffs |
ee.Image |
Coefficients image from get_harmonic_coeffs(). |
required |
cos_band |
str |
Name of cosine coefficient band. |
required |
sin_band |
str |
Name of sine coefficient band. |
required |
stretch_factor |
float |
Stretch amplitude to enhance contrast. |
1 |
return_rgb |
bool |
If True, return RGB image; else return HSV image. |
True |
Returns:
Type | Description |
---|---|
ee.Image |
RGB visualization (uint8) or HSV image. |
Source code in geeagri/timeseries.py
def get_phase_amplitude(
self, harmonic_coeffs, cos_band, sin_band, stretch_factor=1, return_rgb=True
):
"""
Compute phase & amplitude and optionally create RGB visualization.
Args:
harmonic_coeffs (ee.Image): Coefficients image from get_harmonic_coeffs().
cos_band (str): Name of cosine coefficient band.
sin_band (str): Name of sine coefficient band.
stretch_factor (float): Stretch amplitude to enhance contrast.
return_rgb (bool): If True, return RGB image; else return HSV image.
Returns:
ee.Image: RGB visualization (uint8) or HSV image.
"""
phase = harmonic_coeffs.select(cos_band).atan2(harmonic_coeffs.select(sin_band))
amplitude = harmonic_coeffs.select(cos_band).hypot(
harmonic_coeffs.select(sin_band)
)
hsv = (
phase.unitScale(-math.pi, math.pi)
.addBands(amplitude.multiply(stretch_factor))
.addBands(self.composite)
)
if return_rgb:
return hsv.hsvToRgb().unitScale(0, 1).multiply(255).toByte()
else:
return hsv
extract_timeseries_to_point(lat, lon, image_collection, start_date=None, end_date=None, band_names=None, scale=None, crs=None, crsTransform=None, out_csv=None)
¶
Extracts pixel time series from an ee.ImageCollection at a point.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
lat |
float |
Latitude of the point. |
required |
lon |
float |
Longitude of the point. |
required |
image_collection |
ee.ImageCollection |
Image collection to sample. |
required |
start_date |
str |
Start date (e.g., '2020-01-01'). |
None |
end_date |
str |
End date (e.g., '2020-12-31'). |
None |
band_names |
list |
List of bands to extract. |
None |
scale |
float |
Sampling scale in meters. |
None |
crs |
str |
Projection CRS. Defaults to image CRS. |
None |
crsTransform |
list |
CRS transform matrix (3x2 row-major). Overrides scale. |
None |
out_csv |
str |
File path to save CSV. If None, returns a DataFrame. |
None |
Returns:
Type | Description |
---|---|
pd.DataFrame or None |
Time series data if not exporting to CSV. |
Source code in geeagri/timeseries.py
def extract_timeseries_to_point(
lat,
lon,
image_collection,
start_date=None,
end_date=None,
band_names=None,
scale=None,
crs=None,
crsTransform=None,
out_csv=None,
):
"""
Extracts pixel time series from an ee.ImageCollection at a point.
Args:
lat (float): Latitude of the point.
lon (float): Longitude of the point.
image_collection (ee.ImageCollection): Image collection to sample.
start_date (str, optional): Start date (e.g., '2020-01-01').
end_date (str, optional): End date (e.g., '2020-12-31').
band_names (list, optional): List of bands to extract.
scale (float, optional): Sampling scale in meters.
crs (str, optional): Projection CRS. Defaults to image CRS.
crsTransform (list, optional): CRS transform matrix (3x2 row-major). Overrides scale.
out_csv (str, optional): File path to save CSV. If None, returns a DataFrame.
Returns:
pd.DataFrame or None: Time series data if not exporting to CSV.
"""
import pandas as pd
from datetime import datetime
if not isinstance(image_collection, ee.ImageCollection):
raise ValueError("image_collection must be an instance of ee.ImageCollection.")
property_names = image_collection.first().propertyNames().getInfo()
if "system:time_start" not in property_names:
raise ValueError("The image collection lacks the 'system:time_start' property.")
point = ee.Geometry.Point([lon, lat])
try:
if start_date and end_date:
image_collection = image_collection.filterDate(start_date, end_date)
if band_names:
image_collection = image_collection.select(band_names)
image_collection = image_collection.filterBounds(point)
except Exception as e:
raise RuntimeError(f"Error filtering image collection: {e}")
try:
result = image_collection.getRegion(
geometry=point, scale=scale, crs=crs, crsTransform=crsTransform
).getInfo()
result_df = pd.DataFrame(result[1:], columns=result[0])
if result_df.empty:
raise ValueError(
"Extraction returned an empty DataFrame. Check your point, date range, or selected bands."
)
result_df["time"] = result_df["time"].apply(
lambda t: datetime.utcfromtimestamp(t / 1000)
)
if out_csv:
result_df.to_csv(out_csv, index=False)
else:
return result_df
except Exception as e:
raise RuntimeError(f"Error extracting data: {e}.")