Nighttime Lights Appendix
Code status: The code on this page was corrected on 17 September 2026 against the current Earth Engine Data Catalog and API documentation. It has not been executed against Earth Engine. Screenshots, printed values and described outputs come from earlier versions of this lesson and may differ from what the corrected code produces.
6.1 - Overview
Capturing and visualizing low-light emittance from around the earth has been utilized in various applications since the mid-1960’s. By consistently quantifying light emittance over long time periods, it is possible to use this as a proxy for economic development, especially in areas where there is not high-quality data and metrics to work with. Google Earth Engine has consolidated this data into an operational archive dating back to 1992, which provides unparalleled support for finding meaningful insights using this data set.
This tutorial is a supplement to the Open Nighttime Lights tutorial that the World Bank developed. The World Bank tutorial consists of six modules, including a background on the history of the data, working with the tools, extracting imagery, data analysis and image classification. It also contains a data archive section that explains how to access the raw data directly from Amazon Web Services and an applications section that attempts to estimate electricity usage using the nighttime data set. Each segment is well-written, and there is extensive documentation throughout.
The caveat here is that up until this point in the course, we have worked with the Google Earth Engine JavaScript code editor - Because the World Bank tutorial covers topics such as working with data frames, statistics and classification, it utilizes the Google Earth Engine Python API in a Jupyter Notebook. Python is a more natural fit and contains more capabilities for data analysis and Machine Learning than JavaScript, and while the GEE code editor is excellent for working with objects and methods, many of you might prefer working with Python. Based on your background and what you want to get out of this course, here is our general suggestion on how to proceed.
If you are comfortable working with Python, Jupyter Notebooks and setting up your own environment (pip, Conda, Brew), then follow along with the tutorial as it is. Module 2-2 in the World Bank tutorial explains how to get an environment up and running.
- If this is the case, spend some time reading about the functionality in the geemap package - it consolidates much of the mapping features in Earth Engine in an intuitive way, as well as functionality to integrate your results with Folium and custom basemaps.
If you want to learn to use Python but have never worked with virtual environments, then consider going through the tutorial in a Google Colab - it requires no setup of infrastructure, and you can get running immediately while learning Python. Once you are comfortable with this, you can always learn how to set up your own environment. Explanations on getting started can be located here. Note that there are several components of the tutorial, primarily in visualization using leaflet, that will not work.
If you want to stick with working with JavaScript, then the section below will provide you with some capabilities of doing the core functions in the code editor, mainly the segment in Module 3. After that, we suggest exporting the data for further analysis.
Again, this lab is more of a supplement for students that wish to keep using JavaScript and the GEE code editor. It is not designed to fully replace the World Bank tutorial, and while will get you started, there will be things that you will have to figure out on your own.
6.2 - Basic Operations
Module 1 is an essential introduction to the NightTime Lights dataset, while Module 2 introduces you to the data and the setting up your environment. In this section, we will cover the essential components of obtaining the data that you need in the correct context, some basic processing, building a composite and exporting the data in JavaScript, with enough code to show you how to get started and how to follow along with the tutorial.
We will follow along with module exactly as it is set up, so that you can refer to the Module and section numbers.
6.2.1 - Obtaining the Data
The code chunk below should be a good starting point on ingesting the data, looking at the range of data, and visualizing the average value across the image collection. Follow along with the same concepts in the tutorial, test using a specific image (instead of an image collection) and visualize your results. You can modify the opacity manually using the slider on the layers tab, and then build it into your visualization.
Note: ee.Geometry.Point takes coordinates as [longitude, latitude] in both JavaScript and Python, and so does Map.setCenter in the Code Editor. Only map widgets such as geemap’s center=[lat, lon] take latitude first.
// Read in Nighttime Lights
;
// Print size of the image collection
;
// Print out the dates of image collection
;
;
;
'Date range: ', start, end;
// Take the average of the "avg_vis" band across the image collection
;
;
;
;
;
//
center_lon, center_lat, zoomlevel;
nighttimeLights, nighttimeLightsVis, 'Nighttime Lights';
6.2.2 - Image Clipping
This section follows along with some of our earlier work in clipping our image to a certain area, whether that area is a geometry you build in code or your own shapefiles. The code below clips the imagery within a 200km buffer on the center of Los Angeles.
// Get December image - "avg_rad" band
// Set visibility parameters
;
;
;
// Build a 200km buffer around a point
// Clip image to boundary of buffer
;
;
// map set center
center_lon, center_lat, zoomlevel;
viirs2019_12_clipped, nighttimeLightsVis, 'Clipped to Buffer';

You can do the same thing with either your own polygon vector files (import shapefile, kml), or use one of the vector files that GEE maintains - we can test use the TIGER state boundary file and clip the image to California.
// Get December image - "avg_rad" band
// Set visibility parameters
;
;
;
// Boundary of states
// Filter to California
;
// map set center
center_lon, center_lat, zoomlevel;
viirs2019_12_clipped, nighttimeLightsVis, 'California NightTime Lights';

The previous two examples showed the process of clipping individual images - to clip an entire image collection and extract a composite image, we can follow the same general approach, but use the map function to clip each image of the collection to our boundary. However, note that depending on the use case and the size of the image collection, this might take time to run and still leave you with a large amount of data. Before exporting all the data, perhaps reduce the image collection by extracting mean / median values, or use the reduce function.
// Define our clipping function
// Built specifically for the purposes of clipping to California
// Set visibility parameters
;
;
;
;
// Boundary of States
// Filter to California
// use `map` - which applied our function to each image in the image collection
// map set center
center_lon, center_lat, zoomlevel;
viirs_dmb_clipped, nighttimeLightsVis, 'California NightTime Lights';
6.2.3 - Conditional Operations
In this section, we will go over how to mask individual pixels based on conditional statements. This is one section that we will cover in JavaScript, but is probably easier to conduct in Python using ‘Pythonic’ methods and libraries such as NumPy. The charting is easier to work with in Python, but in the code chunk below, you can go through how to build a histogram to identify where a threshold value might be appropriate. Then, build a binary mask using GEE’s built in conditionals:
// get December image, we're using the "avg_rad" band
// center on Catalonia
// create a 200 km buffer around the center of Catalonia
;
6.2.4 - Build the Histogram
This histogram is quite tough to read, but there are values that range from 0 to over 1000 - note that the vast majority fall within the range of 0 and 4. This is used to get a basic understanding of our data.
//
;
// The result of the region reduction by `autoHistogram` is an array. Get the
// array and cast it as such for good measure.
;
histArray
// Subset the values that represent the bottom of the bins and project to
// a single dimension. Result is a 1-D array.
;
// Subset the values that represent the number of pixels per bin and project to
// a single dimension. Result is a 1-D array.
;
// Chart the two arrays using the `ui.Chart.array.values` function.
'ColumnChart';
histColumnFromArray;
6.2.5 - Mask Values
The histogram shows us that a large majority of the values fall near zero - if we build a mask using GEE’s built in conditionals to keep only pixels that have a value above 4, the output allows us to focus in on areas that have meaningful values. Additionally, this will improve compute time and analysis.
// Output is a binary mask (0-1)
// Initialize our map
;
lon, lat, 8;
viirs2019_12_mask, nighttimeVis, 'Nighttime';

Note that you can chain together conditionals to make a layered mask, and build a customized palette. The zones image has values from 0 to 3; masking it with itself hides the zeros, so the palette is stretched from 1 to 3.
// Initialize our map
lon, lat, 8;
zones, , 'zones';

6.2.6 - Cell Statistics and Band Math
It is worthwhile to read through this section thoroughly on the World Bank tutorial, as the techniques you learn here will be very useful in later sections. We will go over scaling an image to center each pixel at zero. We are working in the region of East Timor - the general process is to read in the December 2017 Nighttime Lights average, clip it to the East Timor Feature Collection, and then calculate the mean and standard deviation using the reduceRegion function. Now that we have those values, we can standardize the scaling. Compare the before and after images - in the first, it is very difficult to get any meaningful values, because the range of values is so narrow. Once scaled, we can more easily differentiate between urban areas and rural areas. You will also note that by doing this, the noise increases as well, as you can tell from the reduced ‘sharpness’ of the image. This can be an issue in many cases, and will be addressed in other components of the module.
// get December image, we're using the "avg_rad" band
// get the geometry for Timor-Leste from GEE's tagged datasets
// clip our VIIRS image to Timor-Leste
// Set visibility parameters
;
12625, -85, 9;
ntl_tls, nighttimeLightsVis, '"VIIRS-DNB Dec 2017"';

// Reduce image to find the mean and standard deviation
// Convert these to Numbers using the ee.Number constructor
// Print Output to ensure values look correct
'Mean Avg Radiance', mu
'StdDev', std
// Subtract mean and divide by standard deviation
// Set visibility parameters
;
12625, -85, 9;
ntl_tls_std, nighttimeLightsVis, 'Scaled Image';

6.2.7 - Expressions
In this module, we will work with the .expression() methods built-into images. This allows us to work with customized functions and complete more advanced band math than pre-built functionality. This is a very short module, but the key point here is that being able to manipulate and find unique relationships in imagery. Once you understand how to build an expression, opportunities are limitless. In the images below, we invert the pixel values by multiplying each pixel by -1 and adding 63 (max value).
// get 1996 composite, apply mask, and add as layer
;
lon, lat, 7;
dmsp1996, nighttimeLightsVis, '1996 Composite'

// Use Expression to invert the pixels
dmsp1996_inv, nighttimeLightsVis, '1996 Composite Inverse'

6.2.8 - Expression (Continued)
In the previous example we built an expression using some of the GEE built-in operations, such as .multiply() and .add(). Chaining methods works well for short calculations, but long formulas can become hard to read. Another methodology is to build our expression with a string and then provide the input as a key-value pair. The expression uses the same operators as the chained methods, but a formula written as a string can be easier to read and reuse. See the code chunk below for the methodology. Follow along with the World Bank tutorial using this methodology, and try to build some of your own functions to see the result. Using ‘Inspector’ would be helpful to test whether your function acted as expected.
// We plug this formula in, identify our variable "X" and set it to our 1996 DMSP-OLS "stable_lights" band
dmsp1996_inv2, nighttimeLightsVis, '1996 Composite Inverse'
6.2.9 - Make a Composite
Building a temporal composite is an important part of analysis and modeling. Earlier pages of this track introduced these concepts, and this tutorial extends some of the functionality.
// 2015 image collection - "avg_rad" band
// Confirm that there are 12 images in this collection
'Images:',
// initialize map on Sao Paulo
// VIIRS "avg_rad" is radiance (nanoWatts/sr/cm^2), so use the VIIRS stretch rather than the DMSP 0-63 range
;
// Initialize the map
lon, lat, 7;
viirs2015med, nighttimeLightsVis, '2015 Monthly Median'

Instead of looping over years, the next code chunk maps a function over a list of years and builds one annual median composite for each year from 2015 to 2019.
// Define start and end years
'Number of years: ',
// Map function to each year in our list
'Annual composites:', yearComps
Importing and Exporting Data
Using the GEE code editor is relatively straightforward for importing spatial files, such as Shapefiles. Follow the documentation and you should be able to import the data that you need.
While the documentation on exporting data is also relatively straightforward, it is important to understand exactly what you are exporting.
Conclusion
As noted earlier, this lab is more of a JavaScript supplement to the excellent World Bank tutorial. There are many data and remote sensing libraries in Python that can help you take your work to the next stage.