Choosing and comparing nighttime-light sources

This vignette’s output is pre-rendered from live ISRO Bhuvan and NASA Earthdata data (see vignettes/source-comparison.Rmd.orig), since NASA’s LAADS DAAC can be too slow for a CI build. To refresh it, re-knit the .orig file locally with EARTHDATA_TOKEN set and copy the result over source-comparison.Rmd.

lightson exposes related but non-interchangeable products. Choose a source based on the period, update frequency, measurement units, and intended analysis. Do not append their raw values into a single time series.

At a glance

Source/product Coverage Cadence Approx. resolution Values Authentication
ISRO Bhuvan NTL 2012–2024 Annual 500 m RGB-derived BT.709 luminance None
NASA VNP46A4 2012–2025 Annual 500 m Radiance, nW/cm^2/sr Earthdata token
NASA VNP46A3 2012–present Monthly 500 m Radiance, nW/cm^2/sr Earthdata token
NASA VNP46A2 2012–near-present Daily 500 m Radiance, nW/cm^2/sr Earthdata token

Availability can lag acquisition. In particular, completed annual composites are normally published well into the following year. Check the provider when reproducibility depends on an exact latest date.

Bhuvan: simple India annual data

Bhuvan is convenient when an unauthenticated India-only workflow matters. Its state statistics endpoint returns physical radiance statistics, but its WMS serves rendered RGB imagery. bhuvan_raster() therefore converts WMS colours to dimensionless luminance; it cannot recover the original radiance.

The examples below use a handful of states over 2021-2022 and the bundled LGD state boundaries, keeping every fetch small.

library(lightson)

states <- get_india_admin("state")
sel <- c("Bihar", "West Bengal")
states_sel <- states[states$state_name %in% sel, ]

state_stats <- bhuvan_stats(years = 2021:2022)
bhuvan_maps <- bhuvan_raster(states_sel, years = 2021:2022)
#> Using cached: bhuvan_ec05c5bc4c11_2021.tif
#> Using cached: bhuvan_ec05c5bc4c11_2022.tif

Bhuvan uses VNP46A4 Collection 1 for 2012-2023 and Collection 2 from 2024. The processing change creates a discontinuity, so a 2023-2024 difference must not be interpreted directly as growth. For a consistent calibrated 2012-2025 annual panel, prefer NASA Collection 2 for every year.

NASA Black Marble: annual, monthly, and daily

All three NASA products use the same Earthdata bearer token; the example below uses the annual product. Requests are scoped to states_sel so only the (single) VIIRS tile covering those two states downloads:

token <- earthdata_token()

annual <- ntl_download(
  states_sel,
  years = 2021:2022, token = token,
  product = "annual"
)

Daily downloads (product = "daily") default to quality = "high", retaining mandatory QA flag zero. This rejects cloud contamination and other poor retrievals but can leave missing pixels. Use quality = "all" only when you intend to inspect and apply the quality bands yourself. Daily data are near-current rather than a true real-time feed.

Use annual data for long-run development panels, monthly data for seasonal or short-run economic change, and daily data for narrowly dated events such as power outages. Aggregating QA-filtered daily observations into a monthly statistic is not automatically equivalent to the official VNP46A3 composite.

Comparison

Use the same polygons and aggregation statistic for both sources. Here the comparison uses the two states already downloaded above, over 2021-2022, so it does not cross Bhuvan’s 2024 collection break.

viirs_panel <- extract_panel(annual, states_sel, id_col = "state_name")
viirs_panel$source <- "NASA VNP46A4 C2"
viirs_panel$units <- "nW/cm^2/sr"

bhuvan_panel <- extract_panel(bhuvan_maps, states_sel, id_col = "state_name")
bhuvan_panel$source <- "ISRO Bhuvan WMS"
bhuvan_panel$units <- "BT.709 luminance"

Raw levels cannot be compared because the units differ. A useful diagnostic is to express each state and source relative to its own 2021 value. An index of 120 then means that the source reports a 20% increase since 2021; it does not mean that the two sensors observed the same absolute brightness.

comparison <- rbind(viirs_panel, bhuvan_panel)
comparison <- ntl_index(
  comparison,
  baseline = 2021,
  group_cols = c("source", "region_id"),
  index_col = "index_2021"
)

library(ggplot2)
ggplot(comparison, aes(year, index_2021, colour = source)) +
  geom_hline(yintercept = 100, colour = "grey60", linetype = 2) +
  geom_line(linewidth = 0.8) +
  geom_point(size = 2) +
  facet_wrap(~region_id) +
  labs(
    x = NULL, y = "Index (2021 = 100)",
    title = "Within-source nighttime-light change",
    subtitle = "Index trajectories are comparable; raw levels are not",
    colour = NULL
  ) +
  theme_ntl() +
  theme(legend.position = "bottom")
plot of chunk indexed-comparison
plot of chunk indexed-comparison

Check whether conclusions depend on the source instead of relying only on the figure. The following produces one correlation per state across the two overlap years and ranks states with the weakest agreement:

agreement <- ntl_source_agreement(viirs_panel, bhuvan_panel)
head(agreement, 10)
#>     region_id correlation observations
#> 2 West Bengal          -1            2
#> 1       Bihar           1            2

With only two overlap observations this correlation is a diagnostic, not a validation statistic.

For an outage study using daily data, use pre-event and post-event windows, compare affected states with unaffected controls, and record the exact QA rule. Do not fill cloudy daily observations with zero.

For most India economic panels, the preferred core series is VNP46A4 Collection 2 from 2012 onward. Bhuvan remains useful for unauthenticated India exploration and for its pre-aggregated state statistics.