Open In Colab

7. Making simple VIIRS-DNB annual composites (5 min)

Temporal composites, such as annual composites, are created for satellite data for a variety of reasons. Sometimes it can be desirable to visualize or conduct trend analysis on a smoother, less dense time series, especially if you’re comparing multiple data sources and need a common temporal unit of analysis, like a year.

Often the aim is to reduce the noise that occurs with shorter time periods under the intuition that noise (e.g. via stochastic processes) will be minimalized, or “canceled out”, when data are aggregated over longer time periods, whereas the true signal will be preserved or even strengthened relative to the noise levels.

We’ve been working with DMSP-OLS data to this point. For this exercise, we’ll work with the VIIRS-DNB data so you can get familiar with this source. Refer to Introduction to nighttime light data (20 min) for a refresher on the VIIRS-DNB dataset.

We will create a simple annual composite of VIIRS-DNB by aggregating monthly composites in Google Earth Engine (GEE) using the Reduce() function on an ImageCollection.

Our tasks in this exercise:

  1. Brief background on the VIIRS-DNB stray-light monthly composite data.

  2. Filter a selection of VIIRS-DNB data for the year 2015.

  3. Create an annual composite of 2015 using the Reduce function.

  4. Create a time series of annual composites from VIIRS-DNB monthly composites for 2015-2019.

7.1. Background on VIIRS-DNB monthly composites

For this exercise, we’re going to work with the VIIRS-Day/Night Band (DNB) image collection that has been corrected for stray-light and filtered for data quality, which includes cloud coverage. The daily images are aggregated into monthly composites. In Google Earth Engine, this Image Collection is: NOAA/VIIRS/DNB/MONTHLY_V1/VCMSLCFG

You can read more about the data and the methodology for cleaning and creating composites here

Read more about the process to correct for stray light in this paper [MWL13].

Note: these monthly composites are not filtered to screen out light from auroras, fires, boats or other temporal lights; however, annual composites are available that have this correction through 2015 here

Cleaning and creating composites using daily Sensor Data Records made available through the partnership with the World Bank, University of Michigan, and NOAA will also be the subject of a more advanced Python library as part of this Open Night Lights platform (forthcoming).

7.2. Get ImageCollection for 2015 VIIRS-DNB data

# reminder that if you are installing libraries in a Google Colab instance you will be prompted to restart your kernal

    import geemap, ee
except ModuleNotFoundError:
    if 'google.colab' in str(get_ipython()):
        print("package not found, installing w/ pip in Google Colab...")
        !pip install geemap
        print("package not found, installing w/ conda...")
        !conda install mamba -c conda-forge -y
        !mamba install geemap -c conda-forge -y
    import geemap, ee
except Exception as e:

# get the 2015 image collection, we're using the "avg_rad" band
viirs2015 = ee.ImageCollection("NOAA/VIIRS/DNB/MONTHLY_V1/VCMSLCFG").filterDate("2015-01-01","2015-12-31").select('avg_rad')

# as a gut-check, there should be 12 images in this collection!
print(f"there are {viirs2015.size().getInfo()} images in this collection")
there are 12 images in this collection

7.3. Create an annual composite using the median of these images

Recall in DMSP-OLS annual composites in Google Earth Engine (5 min) we were introduced to the Reducer function. In GEE, ImageCollections also have a few handy reducer functions built in, including median and mean among others. As the names suggest, these functions reduce a given ImageCollection to a single Image by calculating the median or the mean.

7.3.1. We’ll reduce our ImageCollection of 12 images from 2015 to a single Image by calculating the median. Then we’ll initiate a geemap Map object and visualize our layer as we’ve done before.

We haven’t “been” to South America in our tutorials yet, so we’ll center our image on Sao Paulo, Brazil.

We’ve already selected our band “avg_rad” when filtering by date, so we can just use our existing variable, viirs2015.

As we did with our DMSP-OLS images, let’s also apply the mask to ignore cells in our raster that contain no data.

viirs2015med = viirs2015.median()

# iniatialize map on Sao Paulo
lat = -23.54
lon = -46.63

# initialize our map
map1 = geemap.Map(center=[lat,lon], zoom=8)


map1.addLayer(viirs2015med.mask(viirs2015med), {}, "VIIRS-DNB 2015 (monthly med)")

7.4. Create a time series of annual composites from 2015 to 2019

If you want to create a time series of annual composites, you can filter and reduce the collection as we did for 2015, but for each year you want data.

Thinking programmatically, of course, don’t do this manually. We’ll use the Earth Engine list and map functions to iterate through our ImageCollection and create annual composites for each year.

7.4.1. First, we’ll use Earth Engine’s list method to create a list object of years:

# define our start and end years
start = 2015
end = 2019

years = ee.List.sequence(start, end)

print(f"our list has {years.size().getInfo()} years in it")
our list has 5 years in it

7.4.2. Then we’ll create a function to filter Images to a given year and reduce them, producing an annual composite that we’ll map to each year in our list and create a new image collection


def viirs_annual_median_reduce(year):
    return ee.ImageCollection(colID).filter(

# map function to each year in our list
yearComps = ee.ImageCollection.fromImages(

7.4.3. Now we can filter on our image collection on any year to get that composite. We could add multiple years to our map object for comparison.

Let’s create a new map Object and later will add a slide window to it.

In a later tutorial we’ll learn how to make time series plots and compare images using histograms. For now, we’ll just add each annual composite as a layer to our map using a simple Python loop. (and we’ll mask each layer).

map2 = geemap.Map(center=[lat,lon], zoom=8)


# add each layer
for year in range(start,end+1):
    img = yearComps.filterMetadata("year","equals",year).first() #there's only one image, but we extract from collection
    map2.addLayer(img.mask(img), {}, f"VIIRS-DNB {year}", opacity=.75)
Warning, this is based on `ipyleaflet` a Python library that does not play well with Google Colab, so the split panel display will not work in the Google Colab environment but should on your local machine.
# create a split panel map
left_layer = geemap.ee_tile_layer(yearComps.filterMetadata("year","equals",2015), {},
                                  'VIIRS-DNB 2015', opacity=0.75)
right_layer = geemap.ee_tile_layer(yearComps.filterMetadata("year","equals",2019), {},
                                   'VIIRS-DNB 2019', opacity=0.75)

map3 = geemap.Map(center=[lat,lon], zoom=8)
map3.split_map(left_layer=left_layer, right_layer=right_layer)

VIIRS-DNB has noticeably higher resolution than the DMSP-OLS, so differences in light spatial distribution are visible when investigating dynamics over even a few years.

Visualizing these changes is interesting, but later in the tutorial we’ll look at carrying out operations on our images that facilitate analytical study as well.

7.5. References:


Stephen Mills, Stephanie Weiss, and Calvin Liang. Viirs day/night band (dnb) stray light characterization and correction. In Earth Observing Systems XVIII, volume 8866, 88661P. International Society for Optics and Photonics, 2013.