From b25beff62abc8465aa2458e3678c3104d15a26a8 Mon Sep 17 00:00:00 2001 From: Eliton Date: Mon, 9 Feb 2026 23:01:55 +0100 Subject: [PATCH 1/8] feat: count_images_per_interval on reduceInterval --- geetools/ee_image_collection.py | 11 +++++++++++ tests/test_ImageCollection.py | 19 +++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/geetools/ee_image_collection.py b/geetools/ee_image_collection.py index c30abcc8..9aa9243c 100644 --- a/geetools/ee_image_collection.py +++ b/geetools/ee_image_collection.py @@ -1024,6 +1024,7 @@ def reduceInterval( unit: str = "month", duration: int = 1, keep_original_names: bool = True, + count_images_per_interval: bool = False, ) -> ee.ImageCollection: """Reduce the images included in the same duration interval using the provided reducer. @@ -1037,6 +1038,7 @@ def reduceInterval( unit: The unit of time to split the collection. Available units: ``year``, ``month``, ``week``, ``day``, ``hour``, ``minute`` or ``second``. duration: The duration of each split. keep_original_names: Whether to keep the original band names or not. This is a workaround to preserve older behaviour, it should disappear in the future. + count_images_per_interval: Whether to add the property `n_images_per_interval` with the number of images used for the reduction in each interval. Returns: A new :py:class:`ee.ImageCollection` with the reduced images. @@ -1061,6 +1063,15 @@ def reduceInterval( # Every subcollection is sorted in case one use the "first" reducer imageCollectionList = self.groupInterval(unit, duration) + # add the number of images per interval as a property if requested + if count_images_per_interval: + def add_count(ic): + ic = ee.ImageCollection(ic) + count = ic.size() + return ic.set("n_images_per_interval", count) + + imageCollectionList = imageCollectionList.map(add_count) + # create a reducer from user parameters red = getattr(ee.Reducer, reducer)() if isinstance(reducer, str) else reducer diff --git a/tests/test_ImageCollection.py b/tests/test_ImageCollection.py index 6bdc48b5..2e6f1a28 100644 --- a/tests/test_ImageCollection.py +++ b/tests/test_ImageCollection.py @@ -373,6 +373,25 @@ def test_reduce_interval_image_with_system_id(self, s2_sr): firstImg = ic.first() assert "system:id" in firstImg.propertyNames().getInfo() + def test_reduce_interval_with_count_images_per_interval(self, jaxa_rainfall): + # get 3 month worth of data and group it with default parameters + ic = jaxa_rainfall.filterDate("2020-01-01", "2020-01-02") + reduced = ic.geetools.reduceInterval("mean", duration=1, unit="day", count_images_per_interval=True) + firstImg = reduced.first() + assert "n_images_per_interval" in firstImg.propertyNames().getInfo() + + def test_reduce_interval_with_count_images_per_interval_count(self, jaxa_rainfall): + ic = jaxa_rainfall.filterDate("2020-01-01", "2020-01-02") + reduced = ic.geetools.reduceInterval("mean", duration=1, unit="day", count_images_per_interval=True) + firstImg = reduced.first() + assert firstImg.get('n_images_per_interval').getInfo() == 24 + + def test_reduce_interval_with_count_images_per_interval_empty_days(self, s2_sr): + ic = s2_sr.filterDate("2021-01-01", "2021-01-07") + reduced = ic.geetools.reduceInterval("mean", duration=1, unit="day", count_images_per_interval=True) + count_images_per_interval = reduced.aggregate_array("n_images_per_interval").getInfo() + assert count_images_per_interval == [0, 1, 0] + class TestClosestDate: """Test the ``closestDate`` method.""" From fa35cad0a8dc6bb7d5189a221bd2d1aecf2638d7 Mon Sep 17 00:00:00 2001 From: Eliton Date: Wed, 11 Feb 2026 14:54:04 +0000 Subject: [PATCH 2/8] chore: reuse sizeName within groupInterval --- geetools/ee_image_collection.py | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/geetools/ee_image_collection.py b/geetools/ee_image_collection.py index 9aa9243c..f1d2edf1 100644 --- a/geetools/ee_image_collection.py +++ b/geetools/ee_image_collection.py @@ -957,7 +957,7 @@ def aggregateArray(self, properties: list[str] | ee.List | None = None) -> ee.Di values = keys.map(lambda p: self._obj.aggregate_array(p)) return ee.Dictionary.fromLists(keys, values) - def groupInterval(self, unit: str = "month", duration: int = 1) -> ee.List: + def groupInterval(self, unit: str = "month", duration: int = 1, count_images_per_interval: bool = False) -> ee.List: """Transform the :py:class:`ee.ImageCollection` into a list of smaller collection of the specified duration. For example using unit as "month" and duration as 1, the :py:class:`ee.ImageCollection` will be transformed @@ -968,6 +968,7 @@ def groupInterval(self, unit: str = "month", duration: int = 1) -> ee.List: Args: unit: The unit of time to split the collection. Available units: ``year``, ``month``, ``week``, ``day``, ``hour``, ``minute`` or ``second``. duration: The duration of each split. + count_images_per_interval: Whether to add the property `n_images_per_interval` with the number of images used for the reduction in each interval. Returns: A list of :py:class:`ee.ImageCollection` grouped by interval @@ -988,7 +989,7 @@ def groupInterval(self, unit: str = "month", duration: int = 1) -> ee.List: split = collection.geetools.groupInterval("month", 1) print(split.getInfo()) """ - sizeName = "__geetools_generated_size__" # set generated properties name + sizeName = "n_images_per_interval" # set generated properties name # create an ic variable to avoid calling self._obj multiple times # and extract the property names to copy @@ -1011,10 +1012,11 @@ def add_size(ic): def delete_size_property(ic): ic = ee.ImageCollection(ic) return ee.ImageCollection(ic.copyProperties(ic, properties=toCopy)) + + imageCollectionList = imageCollectionList.map(add_size).filter(ee.Filter.gt(sizeName, 0)) - imageCollectionList = ( - imageCollectionList.map(add_size).filter(ee.Filter.gt(sizeName, 0)).map(delete_size_property) - ) + if not count_images_per_interval: + imageCollectionList = imageCollectionList.map(delete_size_property) return ee.List(imageCollectionList) @@ -1061,16 +1063,7 @@ def reduceInterval( """ # create a list of image collections to be reduced # Every subcollection is sorted in case one use the "first" reducer - imageCollectionList = self.groupInterval(unit, duration) - - # add the number of images per interval as a property if requested - if count_images_per_interval: - def add_count(ic): - ic = ee.ImageCollection(ic) - count = ic.size() - return ic.set("n_images_per_interval", count) - - imageCollectionList = imageCollectionList.map(add_count) + imageCollectionList = self.groupInterval(unit, duration, count_images_per_interval) # create a reducer from user parameters red = getattr(ee.Reducer, reducer)() if isinstance(reducer, str) else reducer From 22adcddab0652dd84eabae8b9443c421177ada9b Mon Sep 17 00:00:00 2001 From: Eliton Date: Wed, 18 Feb 2026 09:32:56 +0000 Subject: [PATCH 3/8] chore: keep the original prop --- geetools/ee_image_collection.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/geetools/ee_image_collection.py b/geetools/ee_image_collection.py index f1d2edf1..6ec83d5b 100644 --- a/geetools/ee_image_collection.py +++ b/geetools/ee_image_collection.py @@ -968,7 +968,7 @@ def groupInterval(self, unit: str = "month", duration: int = 1, count_images_per Args: unit: The unit of time to split the collection. Available units: ``year``, ``month``, ``week``, ``day``, ``hour``, ``minute`` or ``second``. duration: The duration of each split. - count_images_per_interval: Whether to add the property `n_images_per_interval` with the number of images used for the reduction in each interval. + count_images_per_interval: Whether to keep the property `__geetools_generated_size__` with the number of images used for the reduction in each interval. Returns: A list of :py:class:`ee.ImageCollection` grouped by interval @@ -989,13 +989,17 @@ def groupInterval(self, unit: str = "month", duration: int = 1, count_images_per split = collection.geetools.groupInterval("month", 1) print(split.getInfo()) """ - sizeName = "n_images_per_interval" # set generated properties name + sizeName = "__geetools_generated_size__" # set generated properties name # create an ic variable to avoid calling self._obj multiple times # and extract the property names to copy ic = self._obj toCopy = ic.first().propertyNames() + # Removing sizeName prop acoording to count_images_per_interval flag + if not count_images_per_interval: + toCopy = toCopy.filter(ee.Filter.neq("item", sizeName)) + # transform the interval into a duration in milliseconds # I can use the DateRangeAccessor as it's imported earlier in the __init__.py file # I don't know if it should be properly imported here, let's see with user feedback @@ -1013,10 +1017,9 @@ def delete_size_property(ic): ic = ee.ImageCollection(ic) return ee.ImageCollection(ic.copyProperties(ic, properties=toCopy)) - imageCollectionList = imageCollectionList.map(add_size).filter(ee.Filter.gt(sizeName, 0)) - - if not count_images_per_interval: - imageCollectionList = imageCollectionList.map(delete_size_property) + imageCollectionList = ( + imageCollectionList.map(add_size).filter(ee.Filter.gt(sizeName, 0)).map(delete_size_property) + ) return ee.List(imageCollectionList) @@ -1040,7 +1043,7 @@ def reduceInterval( unit: The unit of time to split the collection. Available units: ``year``, ``month``, ``week``, ``day``, ``hour``, ``minute`` or ``second``. duration: The duration of each split. keep_original_names: Whether to keep the original band names or not. This is a workaround to preserve older behaviour, it should disappear in the future. - count_images_per_interval: Whether to add the property `n_images_per_interval` with the number of images used for the reduction in each interval. + count_images_per_interval: Whether to keep the property `__geetools_generated_size__` with the number of images used for the reduction in each interval. Returns: A new :py:class:`ee.ImageCollection` with the reduced images. From 8386f15681ab11cede8d31a7f7f8789356e235d9 Mon Sep 17 00:00:00 2001 From: Eliton Date: Mon, 16 Mar 2026 08:48:46 +0000 Subject: [PATCH 4/8] lint: black --- geetools/ee_image_collection.py | 6 ++++-- tests/test_ImageCollection.py | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/geetools/ee_image_collection.py b/geetools/ee_image_collection.py index 6ec83d5b..88633bab 100644 --- a/geetools/ee_image_collection.py +++ b/geetools/ee_image_collection.py @@ -957,7 +957,9 @@ def aggregateArray(self, properties: list[str] | ee.List | None = None) -> ee.Di values = keys.map(lambda p: self._obj.aggregate_array(p)) return ee.Dictionary.fromLists(keys, values) - def groupInterval(self, unit: str = "month", duration: int = 1, count_images_per_interval: bool = False) -> ee.List: + def groupInterval( + self, unit: str = "month", duration: int = 1, count_images_per_interval: bool = False + ) -> ee.List: """Transform the :py:class:`ee.ImageCollection` into a list of smaller collection of the specified duration. For example using unit as "month" and duration as 1, the :py:class:`ee.ImageCollection` will be transformed @@ -1016,7 +1018,7 @@ def add_size(ic): def delete_size_property(ic): ic = ee.ImageCollection(ic) return ee.ImageCollection(ic.copyProperties(ic, properties=toCopy)) - + imageCollectionList = ( imageCollectionList.map(add_size).filter(ee.Filter.gt(sizeName, 0)).map(delete_size_property) ) diff --git a/tests/test_ImageCollection.py b/tests/test_ImageCollection.py index 2e6f1a28..a4c988d7 100644 --- a/tests/test_ImageCollection.py +++ b/tests/test_ImageCollection.py @@ -384,7 +384,7 @@ def test_reduce_interval_with_count_images_per_interval_count(self, jaxa_rainfal ic = jaxa_rainfall.filterDate("2020-01-01", "2020-01-02") reduced = ic.geetools.reduceInterval("mean", duration=1, unit="day", count_images_per_interval=True) firstImg = reduced.first() - assert firstImg.get('n_images_per_interval').getInfo() == 24 + assert firstImg.get("n_images_per_interval").getInfo() == 24 def test_reduce_interval_with_count_images_per_interval_empty_days(self, s2_sr): ic = s2_sr.filterDate("2021-01-01", "2021-01-07") From 2441919e39887753269471181ed5f3141fae1d9b Mon Sep 17 00:00:00 2001 From: Eliton Date: Mon, 16 Mar 2026 08:49:28 +0000 Subject: [PATCH 5/8] fix: typo --- geetools/ee_image_collection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/geetools/ee_image_collection.py b/geetools/ee_image_collection.py index 88633bab..151183f8 100644 --- a/geetools/ee_image_collection.py +++ b/geetools/ee_image_collection.py @@ -998,7 +998,7 @@ def groupInterval( ic = self._obj toCopy = ic.first().propertyNames() - # Removing sizeName prop acoording to count_images_per_interval flag + # Removing sizeName prop according to count_images_per_interval flag if not count_images_per_interval: toCopy = toCopy.filter(ee.Filter.neq("item", sizeName)) From 84fe05e19335fb1cc26849f6df314c308989081b Mon Sep 17 00:00:00 2001 From: Eliton Date: Mon, 16 Mar 2026 17:46:49 +0000 Subject: [PATCH 6/8] tests: review tests for reduce interval --- tests/test_ImageCollection.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/tests/test_ImageCollection.py b/tests/test_ImageCollection.py index a4c988d7..d09b80db 100644 --- a/tests/test_ImageCollection.py +++ b/tests/test_ImageCollection.py @@ -10,6 +10,7 @@ from ee.ee_exception import EEException from jsonschema import validate from matplotlib import pyplot as plt +import geetools as geetools def reduce( @@ -373,24 +374,31 @@ def test_reduce_interval_image_with_system_id(self, s2_sr): firstImg = ic.first() assert "system:id" in firstImg.propertyNames().getInfo() + def test_reduce_interval_without_count_images_per_interval(self, jaxa_rainfall): + # get 3 month worth of data and group it with default parameters + ic = jaxa_rainfall.filterDate("2020-01-01", "2020-01-02") + reduced = ic.geetools.reduceInterval("mean", duration=1, unit="day", count_images_per_interval=False) + firstImg = reduced.first() + assert "__geetools_generated_size__" not in firstImg.propertyNames().getInfo() + def test_reduce_interval_with_count_images_per_interval(self, jaxa_rainfall): # get 3 month worth of data and group it with default parameters ic = jaxa_rainfall.filterDate("2020-01-01", "2020-01-02") reduced = ic.geetools.reduceInterval("mean", duration=1, unit="day", count_images_per_interval=True) firstImg = reduced.first() - assert "n_images_per_interval" in firstImg.propertyNames().getInfo() + assert "__geetools_generated_size__" in firstImg.propertyNames().getInfo() def test_reduce_interval_with_count_images_per_interval_count(self, jaxa_rainfall): ic = jaxa_rainfall.filterDate("2020-01-01", "2020-01-02") reduced = ic.geetools.reduceInterval("mean", duration=1, unit="day", count_images_per_interval=True) firstImg = reduced.first() - assert firstImg.get("n_images_per_interval").getInfo() == 24 + assert firstImg.get("__geetools_generated_size__").getInfo() == 24 def test_reduce_interval_with_count_images_per_interval_empty_days(self, s2_sr): ic = s2_sr.filterDate("2021-01-01", "2021-01-07") reduced = ic.geetools.reduceInterval("mean", duration=1, unit="day", count_images_per_interval=True) - count_images_per_interval = reduced.aggregate_array("n_images_per_interval").getInfo() - assert count_images_per_interval == [0, 1, 0] + count_images_per_interval = reduced.aggregate_array("__geetools_generated_size__").getInfo() + assert count_images_per_interval == [17, 8, 11] class TestClosestDate: From 555be4b7d5a86105cd99dbc42a7d5edcdad6e6ef Mon Sep 17 00:00:00 2001 From: Eliton Date: Mon, 16 Mar 2026 17:47:10 +0000 Subject: [PATCH 7/8] chore: move groupInterval logic to reduceInterval --- geetools/ee_image_collection.py | 28 +++++++++++----------------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/geetools/ee_image_collection.py b/geetools/ee_image_collection.py index 151183f8..4389177b 100644 --- a/geetools/ee_image_collection.py +++ b/geetools/ee_image_collection.py @@ -958,7 +958,7 @@ def aggregateArray(self, properties: list[str] | ee.List | None = None) -> ee.Di return ee.Dictionary.fromLists(keys, values) def groupInterval( - self, unit: str = "month", duration: int = 1, count_images_per_interval: bool = False + self, unit: str = "month", duration: int = 1 ) -> ee.List: """Transform the :py:class:`ee.ImageCollection` into a list of smaller collection of the specified duration. @@ -970,7 +970,6 @@ def groupInterval( Args: unit: The unit of time to split the collection. Available units: ``year``, ``month``, ``week``, ``day``, ``hour``, ``minute`` or ``second``. duration: The duration of each split. - count_images_per_interval: Whether to keep the property `__geetools_generated_size__` with the number of images used for the reduction in each interval. Returns: A list of :py:class:`ee.ImageCollection` grouped by interval @@ -992,15 +991,7 @@ def groupInterval( print(split.getInfo()) """ sizeName = "__geetools_generated_size__" # set generated properties name - - # create an ic variable to avoid calling self._obj multiple times - # and extract the property names to copy ic = self._obj - toCopy = ic.first().propertyNames() - - # Removing sizeName prop according to count_images_per_interval flag - if not count_images_per_interval: - toCopy = toCopy.filter(ee.Filter.neq("item", sizeName)) # transform the interval into a duration in milliseconds # I can use the DateRangeAccessor as it's imported earlier in the __init__.py file @@ -1015,12 +1006,8 @@ def add_size(ic): ic = ee.ImageCollection(ic) return ic.set({sizeName: ic.size()}) - def delete_size_property(ic): - ic = ee.ImageCollection(ic) - return ee.ImageCollection(ic.copyProperties(ic, properties=toCopy)) - imageCollectionList = ( - imageCollectionList.map(add_size).filter(ee.Filter.gt(sizeName, 0)).map(delete_size_property) + imageCollectionList.map(add_size).filter(ee.Filter.gt(sizeName, 0)) ) return ee.List(imageCollectionList) @@ -1066,9 +1053,12 @@ def reduceInterval( reduced = collection.geetools.reduceInterval("mean", "month", 1) print(reduced.getInfo()) """ + # Default property name that keeps the number of images used for the reduction in each interval + sizeName = "__geetools_generated_size__" + # create a list of image collections to be reduced # Every subcollection is sorted in case one use the "first" reducer - imageCollectionList = self.groupInterval(unit, duration, count_images_per_interval) + imageCollectionList = self.groupInterval(unit, duration) # create a reducer from user parameters red = getattr(ee.Reducer, reducer)() if isinstance(reducer, str) else reducer @@ -1098,13 +1088,17 @@ def reduce(ic): ic = ee.ImageCollection(ic) start = ic.aggregate_min("system:time_start") end = ic.aggregate_max("system:time_end") + count = ic.get(sizeName) firstImg = ic.first() propertyNames = firstImg.propertyNames() image = ic.reduce(red).rename(bandNames).copyProperties(firstImg, propertyNames) - return image.set("system:time_start", start, "system:time_end", end) + return image.set("system:time_start", start, "system:time_end", end, sizeName, count) reducedImagesList = imageCollectionList.map(reduce) + if not count_images_per_interval: + reducedImagesList = reducedImagesList.map(lambda i: ee.Image(i).copyProperties(i, exclude=[sizeName])) + # set back the original properties propertyNames = self._obj.propertyNames() ic = ee.ImageCollection(reducedImagesList).copyProperties(self._obj, propertyNames) From ff44f6e0678123efac02e7976048d58e24d05fa1 Mon Sep 17 00:00:00 2001 From: Eliton Date: Mon, 16 Mar 2026 17:47:40 +0000 Subject: [PATCH 8/8] lint: lint --- geetools/ee_image_collection.py | 14 ++++++-------- tests/test_ImageCollection.py | 1 + 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/geetools/ee_image_collection.py b/geetools/ee_image_collection.py index 4389177b..51f605be 100644 --- a/geetools/ee_image_collection.py +++ b/geetools/ee_image_collection.py @@ -957,9 +957,7 @@ def aggregateArray(self, properties: list[str] | ee.List | None = None) -> ee.Di values = keys.map(lambda p: self._obj.aggregate_array(p)) return ee.Dictionary.fromLists(keys, values) - def groupInterval( - self, unit: str = "month", duration: int = 1 - ) -> ee.List: + def groupInterval(self, unit: str = "month", duration: int = 1) -> ee.List: """Transform the :py:class:`ee.ImageCollection` into a list of smaller collection of the specified duration. For example using unit as "month" and duration as 1, the :py:class:`ee.ImageCollection` will be transformed @@ -1006,9 +1004,7 @@ def add_size(ic): ic = ee.ImageCollection(ic) return ic.set({sizeName: ic.size()}) - imageCollectionList = ( - imageCollectionList.map(add_size).filter(ee.Filter.gt(sizeName, 0)) - ) + imageCollectionList = imageCollectionList.map(add_size).filter(ee.Filter.gt(sizeName, 0)) return ee.List(imageCollectionList) @@ -1055,7 +1051,7 @@ def reduceInterval( """ # Default property name that keeps the number of images used for the reduction in each interval sizeName = "__geetools_generated_size__" - + # create a list of image collections to be reduced # Every subcollection is sorted in case one use the "first" reducer imageCollectionList = self.groupInterval(unit, duration) @@ -1097,7 +1093,9 @@ def reduce(ic): reducedImagesList = imageCollectionList.map(reduce) if not count_images_per_interval: - reducedImagesList = reducedImagesList.map(lambda i: ee.Image(i).copyProperties(i, exclude=[sizeName])) + reducedImagesList = reducedImagesList.map( + lambda i: ee.Image(i).copyProperties(i, exclude=[sizeName]) + ) # set back the original properties propertyNames = self._obj.propertyNames() diff --git a/tests/test_ImageCollection.py b/tests/test_ImageCollection.py index d09b80db..2f5fa731 100644 --- a/tests/test_ImageCollection.py +++ b/tests/test_ImageCollection.py @@ -10,6 +10,7 @@ from ee.ee_exception import EEException from jsonschema import validate from matplotlib import pyplot as plt + import geetools as geetools