I have been trying to generate an ee.Image object where each pixel value contains the number of the month where average NDVI is at a minimum. So the output image should have pixel values from 1-12, where 1 indicates January has the lowest mean NDVI values, 2 indicates February, etc.

I first calculated the maximum values for each month across the time series. Then I took the mean across all Januarys, all Februarys etc. to produce the mean monthly NDVI cycle as follows:

// Find MODIS NDVI images for relevant date range
var start_date = ee.Date("2010-01-01");
var end_date = ee.Date("2020-01-01");

var modis_filtered = ee.ImageCollection("MODIS/006/MOD13Q1")
                         .filterDate(start_date, end_date)

// Calculate monthly max NDVI values
var monthly_images = function(IC) {
  // Get number of months in interval
  var n_months = ee.List.sequence(0, end_date.difference(start_date, "month").round().subtract(1));
  // Iterate monthly max over all months in the interval
  var images = n_months.map(function(n) {
    // Get images for given month
    var start = start_date.advance(n, "month");
    var end = start.advance(1, "month");
    var filtered = IC.filterDate(start, end);

    var max = filtered.mean();

    return max.set("month", start.get("month"), "year", start.get("year"));
    return ee.ImageCollection.fromImages(images);

var ndvi_monthly_max = monthly_images(modis_filtered);

// Calculate average monthly cycle
var month_list = ee.List.sequence(1, 12);

var cycle = ee.ImageCollection.fromImages(month_list.map(function(x) {
  return ndvi_monthly_max.filter(ee.Filter.eq("month", x))
                      .set("month", x);

This gives an ImageCollection with one Image for each month. It's then simple to calculate the minimum value for each pixel, but I can't figure out a clean way to calculate which month that minimum occurs in. The best I have come up with is:

  1. Calculate the minimum across all months,
  2. Test whether the pixel values in each month are equal to that minimum,
  3. Replace all matching pixels in each month with the month index, and mask the rest,
  4. Mosaic the resulting 12 images together.
var min_ndvi_month = cycle.map(function(image) {
  // Image containing month number
  var month_image = ee.Image.constant(image.get("month"))
  // Replace pixel with month number if it is equal to min; otherwise mask
  var is_min = image.eq(min_ndvi);
  var is_min_mask = is_min.updateMask(is_min);
  var is_min_month = is_min_mask.where(1, month_image);
  return is_min_month;

// Mosaic output to one image
var output = min_ndvi_month.mosaic();

Map.addLayer(output, {min: 1, max: 12}, "month of min NDVI");

This seems to work, but seems ugly, convoluted and error prone! Is there a better way?

1 Answer 1


Add a month band to each image (an integer from 1 to 12) and use the numInputs option to ee.Reducer.min() to get the month band that goes along with the min value. And since you're taking the mean of the means, so you don't need to map over months twice; you can use calendarRange instead of filterDate to just get all the images for month N from all years in the first function.

var month_list = ee.List.sequence(1, 12);
var monthly_images = ee.ImageCollection.fromImages(month_list.map(function(n) {
    var filtered = modis_filtered.filter(ee.Filter.calendarRange(n, n, 'month'))
    var mean = filtered.mean();
    var monthBand = ee.Image.constant(n).int().rename('month')
    return mean.addBands(monthBand)

// Find min of the first band and bring along the matching second band.
var minMonth = monthly_images.reduce(ee.Reducer.min(2)).select(1)
  • Thanks! I needed to edit the code to run without error - the month number can be added as ee.Image.constant(n) rather than ee.Image.constant(start.get("month")) in this case, and monthly_images needs to be converted to an ImageCollection for the reducer to work. But this approach does what I asked so I have marked it as the answer.
    – G-Lomax
    Nov 12, 2021 at 11:34

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.