[GIS] Max NDVI per month & year using Google Earth Engine to CSV – CSV gives -999 values only

exportgoogle-earth-enginegoogle-earth-engine-javascript-apindvitime series

This question refers to Calculating NDVI per region, month & year with Google Earth Engine

I have modified the code posted by @Kel Markert at https://code.earthengine.google.com/349615d7802d59f677181bef0badad9f to attempt to get a maximum monthly NDVI value from a small polygon over a number of years from Landsat 8 in Google Earth Engine and export to CSV.

The output .csv table only gives -999 values for all months when using the max reducer reducer: ee.Reducer.max(). When using the median reducer reducer: ee.Reducer.median(), the output provides meaningful values.

Is Max the incorrect reducer to use?

Code Link to Google Earth Engine Script

Table asset link – region polygon

    var region = table,
L8 = ee.ImageCollection("LANDSAT/LC08/C01/T1_TOA");

var cloudlessNDVI = L8.map(function(image) {
  // Get a cloud score in [0, 100].
  var cloud = ee.Algorithms.Landsat.simpleCloudScore(image).select('cloud');

  // Create a mask of cloudy pixels from an arbitrary threshold.
  var mask = cloud.lte(20);

  // Compute NDVI.
  var ndvi = image.normalizedDifference(['B5', 'B4']).rename('NDVI');

  // Return the masked image with an NDVI band.
  return image.addBands(ndvi).updateMask(mask);
});

var startDate = ee.Date('2013-05-01'); // set analysis start time
var endDate = ee.Date('2019-12-31'); // set analysis end time

// calculate the number of months to process
var nMonths = ee.Number(endDate.difference(startDate,'month')).round();

// get a list of time strings to pass into a dictionary later on
var monList = ee.List.sequence(0, nMonths).map(function (n) {
  return startDate.advance(n, 'month').format('YYYMMdd');
})
print(monList)

var result = region.map(function(feature){
  // map over each month
  var timeSeries = ee.List.sequence(0,nMonths).map(function (n){
    // calculate the offset from startDate
    var ini = startDate.advance(n,'month');
    // advance just one month
    var end = ini.advance(1,'month');
    // filter and reduce
    var data = cloudlessNDVI.filterDate(ini,end).max().reduceRegion({
      reducer: ee.Reducer.max(),
      geometry: feature.geometry(),
      scale: 30
    });
    // get the value and check that it has data
    var val = ee.Number(data.get('NDVI'));
    val = ee.Number(ee.Algorithms.If(val,val,-999));
    // return max
    return val;
  });
  // create new dictionary with date strings and values
  var timeDict = ee.Dictionary.fromLists(monList,timeSeries);
  // return feature with a timeseries property and results
  return feature.set(timeDict);
});

// print to see if it is doing what we expect...
print(result);

// Export the data to a table for further analysis
Export.table.toDrive({
  collection:result,
  description:"MCM1_NDVI",
  fileFormat:"CSV",
  //selectors:["HRpcode","timeseries"]
})

Best Answer

You can obtain the answer you want with the small geometry you use using a different approach. Note that a Landsat pixel and the scale you are currently using is 30m. Your geometry is just about 10x10m.

Add the moment you are using an unweighted reducer (ee.Reducer.max), which will only take pixels into account if the centroid is within the geometry (see here). Neither the geometry nor the scale you are using apparently include the centroid of the pixels, thus you return no valid values (which you set to -999 afterwards).

You have two options:

1) Use a scale which will include the centroid and use the unweighted reducer. Set the scale for example at 10m or calculate approximately the scale of a square geometry using:

var scale = table.geometry().area(1).sqrt();

2) Use a weighted reducer which will include pixels which approximately include at least 0.5% of the pixel. An example would be using a mean reducer. In such a small region you probably aggregate one pixel, so that is fine for your max outcome.

var data = cloudlessNDVI.filterDate(ini,end).max().reduceRegion({
  reducer: ee.Reducer.mean(),
  geometry: feature.geometry(),
  scale: 30
});

Here an example returning both values (and you will see they are similar, confirming there is just one pixel in the region).