| Title: | Air Quality Data from Indian Monitoring Networks and Satellites |
|---|---|
| Description: | Downloads and examines air quality data from three sources. The Central Pollution Control Board (CPCB) of India operates more than 600 monitoring stations. Open Air Quality (OAQ) <https://oaq.notf.in> collects data from low-cost sensor networks in India. The Atmospheric Composition Analysis Group at Washington University in St. Louis gives satellite estimates of fine particulate matter (PM2.5) for all areas of the Earth <https://www.satpm.org>. The package calculates the CPCB National Air Quality Index and the mean PM2.5 in an area. |
| Authors: | Saket Choudhary [aut, cre, cph], Mahesh Kumar Prajapat [aut], Darakhshan Nehal [ctb] |
| Maintainer: | Saket Choudhary <[email protected]> |
| License: | MIT + file LICENSE |
| Version: | 0.3.0 |
| Built: | 2026-11-03 18:42:03 UTC |
| Source: | https://github.com/cran/vayuayan |
Comprehensive analysis of station data
analyze_station_data(data)analyze_station_data(data)
data |
List of cities with nested stations |
List with analysis results
analyze_station_data(cpcb_get_all_india())analyze_station_data(cpcb_get_all_india())
CPCB's "possible health impacts" text for the category of each AQI value.
aqi_health_impact(aqi_value)aqi_health_impact(aqi_value)
aqi_value |
Numeric vector of AQI values. |
Character vector, "No Data" for missing values.
aqi_health_impact(c(250, NA))aqi_health_impact(c(250, NA))
The category of the sub-index of each concentration, for example of a PM2.5 mean from satellite data. The breakpoints are for 24-hour averages (8-hour for CO and O3), so for an annual or monthly mean the category is an indication only.
aqi_pollutant_category(pollutant, concentration)aqi_pollutant_category(pollutant, concentration)
pollutant |
A pollutant label (see |
concentration |
Numeric vector of 24-hour averages (8-hour for CO and O3), in mg/m3 for CO and ug/m3 for the others. |
Character vector of AQI categories.
aqi_pollutant_category("PM2.5", c(20, 75))aqi_pollutant_category("PM2.5", c(20, 75))
Match a pollutant label such as "PM2.5 (ug/m3)", "pm25" or
"Ozone" to its National AQI name.
aqi_pollutant_name(label)aqi_pollutant_name(label)
label |
Character vector of labels. |
Character vector of NAQI names ("PM10", "PM2.5",
"NO2", "O3", "CO", "SO2", "NH3",
"Pb"), NA for labels that are not NAQI pollutants.
aqi_pollutant_name(c("PM2.5 (ug/m3)", "Ozone", "Benzene"))aqi_pollutant_name(c("PM2.5 (ug/m3)", "Ozone", "Benzene"))
The CPCB National AQI sub-index of one pollutant, by linear interpolation within its breakpoint band.
aqi_sub_index(pollutant, concentration)aqi_sub_index(pollutant, concentration)
pollutant |
A pollutant label (see |
concentration |
Numeric vector of 24-hour averages (8-hour for CO and O3), in mg/m3 for CO and ug/m3 for the others. |
Numeric vector of sub-indices; above the last breakpoint the
slope of the Severe band continues, so a sub-index can be more than 500.
NA stays NA.
aqi_sub_index("PM2.5", c(25, 45, 300))aqi_sub_index("PM2.5", c(25, 45, 300))
Convert station name to clean underscore-separated format
clean_station_name(station_name)clean_station_name(station_name)
station_name |
Original station name string |
Cleaned station name with underscores
clean_station_name("Bandra Kurla Complex, Mumbai - IITM")clean_station_name("Bandra Kurla Complex, Mumbai - IITM")
Spread across collocated sensors on each day, as in the EPA protocol.
collocated_precision(daily, value = "pm25", min_sensors = 3, date_col = "date")collocated_precision(daily, value = "pm25", min_sensors = 3, date_col = "date")
daily |
Daily means of sensors at one place ( |
value |
Column of the daily means. |
min_sensors |
Fewest sensors with a mean on a day. |
date_col |
Day column. |
Data frame with date_col, sensors, mean,
sd (sample standard deviation across the sensors) and
cv_pct, per day with enough sensors.
daily <- data.frame( sensor_id = rep(c("a", "b", "c"), 2), date = rep(as.Date("2026-01-01") + 0:1, each = 3), pm25 = c(10, 12, 14, 20, 20, 26) ) collocated_precision(daily)daily <- data.frame( sensor_id = rep(c("a", "b", "c"), 2), date = rep(as.Date("2026-01-01") + 0:1, each = 3), pm25 = c(10, 12, 14, 20, 20, 26) ) collocated_precision(daily)
Each daily record against statistics of the same day's readings: the mean, last, maximum and median reading and the mean of the last 2 and last 4 readings.
compare_daily_records(readings, records, value = "pm25", tolerance = 0.005)compare_daily_records(readings, records, value = "pm25", tolerance = 0.005)
readings |
15-minute readings ( |
records |
Daily records ( |
value |
Parameter to compare. |
tolerance |
Values within it count as equal (0.005: the daily records have 2 decimals). |
Data frame with sensor_id, date, record,
n, the statistics, record_is_<statistic> for each, and
class: "mean", "last", "both" (equal to the
mean and to the last reading) or "neither". One row per
sensor-day with a record and readings; missing readings are left out.
readings <- data.frame( sensor_id = "s1", timestamp = as.POSIXct("2026-03-10", tz = "UTC") + 900 * 0:3, pm25 = c(10, 10, 10, 300) ) records <- data.frame(sensor_id = "s1", date = as.Date("2026-03-10"), pm25 = 300) compare_daily_records(readings, records)readings <- data.frame( sensor_id = "s1", timestamp = as.POSIXct("2026-03-10", tz = "UTC") + 900 * 0:3, pm25 = c(10, 10, 10, 300) ) records <- data.frame(sensor_id = "s1", date = as.Date("2026-03-10"), pm25 = 300) compare_daily_records(readings, records)
The overall AQI is the highest sub-index. CPCB requires at least three pollutants, one of them PM2.5 or PM10.
compute_aqi(concentrations)compute_aqi(concentrations)
concentrations |
Named numeric vector or list: pollutant label to averaged concentration. Labels that are not NAQI pollutants and missing values are ignored. |
List with aqi (integer, NA when the data are
insufficient), category, dominant_pollutant and the named
sub_indices.
compute_aqi(c("PM2.5" = 45, PM10 = 80, NO2 = 30))$aqicompute_aqi(c("PM2.5" = 45, PM10 = 80, NO2 = 30))$aqi
The US AQI is the highest sub-index, rounded half up. The EPA has no minimum number of pollutants. For O3 and SO2 with both averaging times, the higher sub-index counts. If the 1-hour SO2 is 305 ppb or more and the 24-hour SO2 is below 305 ppb, the SO2 sub-index is 200.
compute_us_aqi(concentrations)compute_us_aqi(concentrations)
concentrations |
Named numeric vector or list: pollutant label to
concentration (see |
List with aqi (integer, NA when no pollutant has a
sub-index), category, dominant_pollutant and the named
sub_indices.
compute_us_aqi(c("PM2.5" = 35.4, O3 = 0.060))$aqicompute_us_aqi(c("PM2.5" = 35.4, O3 = 0.060))$aqi
Add lat/lon coordinates to a station data frame. Coordinates come from the bundled CPCB station directory, then from the CPCB live feed for stations newer than the directory, then from Nominatim geocoding of the station name as a last resort.
cpcb_add_coords(stations)cpcb_add_coords(stations)
stations |
Data frame from |
Same data frame with lat and lon columns added
stations <- cpcb_get_station_list("Mumbai") stations_with_coords <- cpcb_add_coords(stations) head(stations_with_coords)stations <- cpcb_get_station_list("Mumbai") stations_with_coords <- cpcb_add_coords(stations) head(stations_with_coords)
Download (or reuse) the CPCB daily city file for each city and year and
return it in long format. CPCB files of past years are cached in
vayuayan_cache_dir(). With cache_dir, each sheet is also
saved as <city>_aqi_<year>.csv there and read back on later calls.
cpcb_city_daily(city, year, cache_dir = NULL)cpcb_city_daily(city, year, cache_dir = NULL)
city |
Character vector of city names. |
year |
Integer year. |
cache_dir |
Optional directory for CSV copies of the sheets. |
Data frame with columns city, day, month and
value.
daily <- cpcb_city_daily(c("Delhi", "Mumbai"), 2022) aggregate(value ~ city + month, daily, mean)daily <- cpcb_city_daily(c("Delhi", "Mumbai"), 2022) aggregate(value ~ city + month, daily, mean)
Download daily PM2.5 AQI data for a city from the CPCB data repository for a given year. Data is saved as a CSV file with one row per day and one column per month.
cpcb_download_city_data(city, year, save_location)cpcb_download_city_data(city, year, save_location)
city |
Character. City name (e.g. |
year |
Integer. Year for data (e.g. |
save_location |
Character. Full file path to save the downloaded CSV. |
Data frame preview (first 6 rows) of the downloaded data.
The full data is saved to save_location.
cpcb_download_city_data( city = "Delhi", year = 2022, save_location = tempfile(fileext = ".csv") )cpcb_download_city_data( city = "Delhi", year = 2022, save_location = tempfile(fileext = ".csv") )
Download CPCB's hourly AQI workbooks for a city or a station and combine the months of a year.
cpcb_download_hourly_city_data(city, year, save_location = "", month = NULL) cpcb_download_hourly_station_data( station_id, year, save_location = "", month = NULL )cpcb_download_hourly_city_data(city, year, save_location = "", month = NULL) cpcb_download_hourly_station_data( station_id, year, save_location = "", month = NULL )
city |
Character. City name. |
year |
Integer year. |
save_location |
Character. CSV path to save the result to; |
month |
Optional month name (e.g. |
station_id |
Character. Station ID (e.g. |
Data frame of hourly AQI with Year and Month columns.
delhi_jan <- cpcb_download_hourly_city_data("Delhi", 2024, month = "January")delhi_jan <- cpcb_download_hourly_city_data("Delhi", 2024, month = "January")
Download a station's raw CPCB file for a year, for example 15-minute observations of every pollutant and weather variable.
cpcb_download_raw_station_data( station_id, year, save_location = "", frequency = "15_minute", tidy = FALSE )cpcb_download_raw_station_data( station_id, year, save_location = "", frequency = "15_minute", tidy = FALSE )
station_id |
Character. Station ID (e.g. |
year |
Integer year. |
save_location |
Character. CSV path to save the result to; |
frequency |
Character. Raw interval: |
tidy |
Logical. If |
Data frame of observations.
raw <- cpcb_download_raw_station_data("site_301", 2024, tidy = TRUE)raw <- cpcb_download_raw_station_data("site_301", 2024, tidy = TRUE)
Download daily PM2.5 AQI data for a single CPCB monitoring station for a given year. Data is saved as a CSV file with one row per day and one column per month.
cpcb_download_station_data(station_id, year, save_location)cpcb_download_station_data(station_id, year, save_location)
station_id |
Character. Station ID (e.g. |
year |
Integer. Year for data (e.g. |
save_location |
Character. Full file path to save the downloaded CSV. |
Data frame preview (first 6 rows) of the downloaded data.
The full data is saved to save_location.
cpcb_download_station_data( station_id = "Bandra Kurla Complex, Mumbai - MPCB", year = 2022, save_location = tempfile(fileext = ".csv") )cpcb_download_station_data( station_id = "Bandra Kurla Complex, Mumbai - MPCB", year = 2022, save_location = tempfile(fileext = ".csv") )
Get all air quality monitoring stations in India with their latest readings, from the public CPCB station feed.
cpcb_get_all_india()cpcb_get_all_india()
Data frame of cities (cityName, cityID,
stateID) with a stationsInCity list-column of station data
frames (id, name, latitude, longitude,
live, avg, lastUpdate, pollutants).
stations <- cpcb_get_all_india() df <- stations_to_dataframe(stations) head(df)stations <- cpcb_get_all_india() df <- stations_to_dataframe(stations) head(df)
Get the cities of a state from the bundled CPCB station directory. Matching is case-insensitive and also accepts CPCB state codes.
cpcb_get_city_list(state)cpcb_get_city_list(state)
state |
Character. State name (e.g. |
Character vector of city names
cities <- cpcb_get_city_list("Maharashtra") head(cities)cities <- cpcb_get_city_list("Maharashtra") head(cities)
List every state, city and station in the CPCB station directory bundled with the package. Needs no internet connection.
cpcb_get_complete_list()cpcb_get_complete_list()
List with cities (named by state; data frames of
label/value) and stations (named by city; data
frames of station label and ID value).
all_data <- cpcb_get_complete_list() names(all_data)all_data <- cpcb_get_complete_list() names(all_data)
Get file path for downloading data from CPCB. Used internally by
cpcb_download_city_data() and cpcb_download_station_data().
cpcb_get_file_path( station_id = "", station_name = "", state = "", city = "", year = "", frequency = "daily", data_type = "cityLevel" )cpcb_get_file_path( station_id = "", station_name = "", state = "", city = "", year = "", frequency = "daily", data_type = "cityLevel" )
station_id |
Character. Station ID (e.g. |
station_name |
Character. Station name. |
state |
Character. State name. |
city |
Character. City name. |
year |
Integer or character. Year for data (e.g. |
frequency |
Character. Data frequency: |
data_type |
Character. Type of data: |
List of file entries as CPCB sends them, each with filepath, year and
month (NA when CPCB gives none).
paths <- cpcb_get_file_path( city = "Delhi", frequency = "daily", data_type = "cityLevel" ) str(paths[[1]])paths <- cpcb_get_file_path( city = "Delhi", frequency = "daily", data_type = "cityLevel" ) str(paths[[1]])
Get live AQI data of a station. Without station_id, the station
nearest to coords or to the IP-based location is used.
cpcb_get_live_aqi_data( station_id = NULL, coords = NULL, date = NULL, hour = NULL )cpcb_get_live_aqi_data( station_id = NULL, coords = NULL, date = NULL, hour = NULL )
station_id |
Optional character. Station ID or name. If NULL, nearest station
is found automatically using |
coords |
Optional numeric vector |
date, hour
|
Ignored; the feed holds only the latest reading. |
List as returned by cpcb_get_live_aqi_data_for_station().
# Get live AQI at the current location aqi <- cpcb_get_live_aqi_data() # Get live AQI for a specific station aqi <- cpcb_get_live_aqi_data(station_id = "site_103")# Get live AQI at the current location aqi <- cpcb_get_live_aqi_data() # Get live AQI for a specific station aqi <- cpcb_get_live_aqi_data(station_id = "site_103")
Get the latest AQI reading for a station from the public CPCB station feed.
The feed holds only the latest hour, so date_time is ignored.
cpcb_get_live_aqi_data_for_station(station_id, date_time = NULL)cpcb_get_live_aqi_data_for_station(station_id, date_time = NULL)
station_id |
Character. Station ID (e.g. |
date_time |
Ignored; the feed holds only the latest reading. |
List with station_id, station_name,
lastUpdate, airQualityIndex and a metrics data
frame of pollutant readings (name, avg, min,
max, avgDesc).
aqi <- cpcb_get_live_aqi_data_for_station(station_id = "site_103")aqi <- cpcb_get_live_aqi_data_for_station(station_id = "site_103")
Find the CPCB monitoring station nearest to given coordinates, or to the IP-based location when none are given.
cpcb_get_nearest_station(coords = NULL)cpcb_get_nearest_station(coords = NULL)
coords |
Optional numeric vector c(lat, lon). If NULL, uses IP location. |
List with station_id, station_name, and distance_km
# Auto-detect location nearest <- cpcb_get_nearest_station() cat( "Nearest station:", nearest$station_name, "(", round(nearest$distance_km, 1), "km)" ) # Provide coordinates manually (Mumbai) nearest <- cpcb_get_nearest_station(coords = c(19.076, 72.877))# Auto-detect location nearest <- cpcb_get_nearest_station() cat( "Nearest station:", nearest$station_name, "(", round(nearest$distance_km, 1), "km)" ) # Provide coordinates manually (Mumbai) nearest <- cpcb_get_nearest_station(coords = c(19.076, 72.877))
The k CPCB stations in the live feed nearest to a location,
optionally within a radius.
cpcb_get_nearest_stations(coords = NULL, k = 5, max_distance_km = Inf)cpcb_get_nearest_stations(coords = NULL, k = 5, max_distance_km = Inf)
coords |
Optional numeric vector c(lat, lon). If NULL, uses IP location. |
k |
Integer. Number of stations to return. |
max_distance_km |
Numeric. Only stations within this distance. |
Data frame sorted by distance with station_id,
station_name, city, state, latitude,
longitude, live, avg and distance_km. It
has no rows when no station is in range.
# 5 nearest stations to central Delhi cpcb_get_nearest_stations(c(28.6139, 77.2090), k = 5) # nearest station within 10 km, if any cpcb_get_nearest_stations(c(28.6139, 77.2090), k = 1, max_distance_km = 10)# 5 nearest stations to central Delhi cpcb_get_nearest_stations(c(28.6139, 77.2090), k = 5) # nearest station within 10 km, if any cpcb_get_nearest_stations(c(28.6139, 77.2090), k = 1, max_distance_km = 10)
Get the states in the CPCB station directory bundled with the package. Needs no internet connection.
cpcb_get_state_list()cpcb_get_state_list()
Character vector of state names
states <- cpcb_get_state_list() head(states)states <- cpcb_get_state_list() head(states)
Get the monitoring stations of a city from the bundled CPCB station directory.
cpcb_get_station_list(city)cpcb_get_station_list(city)
city |
Character. City name (e.g. |
Data frame with columns label (station name) and
value (station ID)
stations <- cpcb_get_station_list("Delhi") head(stations)stations <- cpcb_get_station_list("Delhi") head(stations)
Get approximate system location using IP-based geolocation. Returns city-level coordinates based on the machine's public IP address.
cpcb_get_system_location()cpcb_get_system_location()
Numeric vector c(lat, lon)
loc <- cpcb_get_system_location() cat("Latitude:", loc["lat"], "Longitude:", loc["lon"])loc <- cpcb_get_system_location() cat("Latitude:", loc["lat"], "Longitude:", loc["lon"])
The metrics of live station data as a table with numeric columns.
cpcb_live_metrics(aqi_data)cpcb_live_metrics(aqi_data)
aqi_data |
List from |
Data frame with columns pollutant, avg, min,
max (numeric, NA where CPCB has none) and period,
one row per pollutant.
cpcb_live_metrics(list(metrics = data.frame( name = "PM2.5", avg = "80", min = 60, max = "NA", avgDesc = "Over the last 24 hours" )))cpcb_live_metrics(list(metrics = data.frame( name = "PM2.5", avg = "80", min = 60, max = "NA", avgDesc = "Over the last 24 hours" )))
Turn the last_hours series of live station data (one series of
date/val points per pollutant) into one row per time and
pollutant. The CPCB feed has no recent hours, so the result is empty for
feed data.
cpcb_live_to_long(aqi_data, station_id = "")cpcb_live_to_long(aqi_data, station_id = "")
aqi_data |
List from |
station_id |
Optional station ID for every row. |
Data frame with columns station_id, timestamp
(POSIXct, UTC), pollutant and value, sorted by pollutant
and time.
cpcb_live_to_long(list(station_id = "site_301", last_hours = list( list(name = "PM2.5", data = list(list(date = "2024-01-01 10:00:00", val = 80))) )))cpcb_live_to_long(list(station_id = "site_301", last_hours = list( list(name = "PM2.5", data = list(list(date = "2024-01-01 10:00:00", val = 80))) )))
cpcb_download_raw_station_data(tidy = TRUE) for several stations,
as one long table. Stations without a raw file for the year are skipped
with a warning.
cpcb_raw_readings(station_id, year, pollutants = NULL)cpcb_raw_readings(station_id, year, pollutants = NULL)
station_id |
Character vector of station IDs or names. |
year |
Integer year. |
pollutants |
Optional pollutant names to keep, such as
|
Data frame as cpcb_raw_to_long().
raw <- cpcb_raw_readings(c("site_1554", "site_162"), 2025, "PM2.5")raw <- cpcb_raw_readings(c("site_1554", "site_162"), 2025, "PM2.5")
Turn a raw CPCB station file, with a Timestamp column and one
"<name> (<unit>)" column per pollutant, into one row per time and
pollutant.
cpcb_raw_to_long(raw, station = NULL, start_date = NULL, end_date = NULL)cpcb_raw_to_long(raw, station = NULL, start_date = NULL, end_date = NULL)
raw |
Data frame as returned by |
station |
Optional named list with |
start_date, end_date
|
Optional first and last dates to keep (inclusive). |
Data frame with columns station_id, station_name,
city, state, timestamp (POSIXct, UTC),
pollutant, unit and value, sorted by time and
pollutant. Rows without a time or a numeric value are dropped.
raw <- data.frame( Timestamp = c("2024-01-01 00:00:00", "2024-01-01 00:15:00"), "PM2.5 (ug/m3)" = c(158, 160), "CO (mg/m3)" = c(1.6, NA), check.names = FALSE ) cpcb_raw_to_long(raw, station = list(station_id = "site_301"))raw <- data.frame( Timestamp = c("2024-01-01 00:00:00", "2024-01-01 00:15:00"), "PM2.5 (ug/m3)" = c(158, 160), "CO (mg/m3)" = c(1.6, NA), check.names = FALSE ) cpcb_raw_to_long(raw, station = list(station_id = "site_301"))
Download (or reuse) the CPCB daily file for each station and year and
return them in one long table. CPCB files of past years are cached in
vayuayan_cache_dir(). With cache_dir, each sheet is also
saved as <station_id>_<year>.csv there. Stations that are not found
or have no data for the year are skipped with a warning.
cpcb_station_daily(station_id, year, cache_dir = NULL)cpcb_station_daily(station_id, year, cache_dir = NULL)
station_id |
Character vector of station IDs or names, for example
the |
year |
Integer year. |
cache_dir |
Optional directory for CSV copies of the sheets. |
Data frame with columns station_id, station_name,
day, month and value.
daily <- cpcb_station_daily(c("Anand Vihar, Delhi", "ITO, Delhi"), 2024)daily <- cpcb_station_daily(c("Anand Vihar, Delhi", "ITO, Delhi"), 2024)
Download a station's raw 15-minute data for a year and compute its daily
NAQI with daily_aqi().
cpcb_station_daily_aqi(station_id, year, day_ends_at = 24)cpcb_station_daily_aqi(station_id, year, day_ends_at = 24)
station_id |
Character. Station ID (e.g. |
year |
Integer year. |
day_ends_at |
Hour at which each day's 24 hours end; 16 matches CPCB's 4 PM bulletin. |
Data frame as returned by daily_aqi(), limited to dates in
year.
anand_vihar <- cpcb_station_daily_aqi("Anand Vihar, Delhi", 2024, day_ends_at = 16)anand_vihar <- cpcb_station_daily_aqi("Anand Vihar, Delhi", 2024, day_ends_at = 16)
CPCB daily files have one row per day of the month and one column per month. This turns that sheet into one row per day.
cpcb_tidy_daily(x, year = NULL)cpcb_tidy_daily(x, year = NULL)
x |
Data frame from a CPCB daily file, or the path to a CSV written by
|
year |
Optional year of the sheet. When given, a |
Data frame with integer columns day and month,
numeric value, and date when year is given. Days
without a reading are dropped.
sheet <- data.frame(Day = 1:2, January = c(180, NA), February = c(150, 140)) cpcb_tidy_daily(sheet) cpcb_tidy_daily(sheet, year = 2024)sheet <- data.frame(Day = 1:2, January = c(180, NA), February = c(150, 140)) cpcb_tidy_daily(sheet) cpcb_tidy_daily(sheet, year = 2024)
Daily NAQI of each station from long data, such as 15-minute raw data from
cpcb_raw_to_long(). Values are first averaged by hour. PM10, PM2.5,
NO2, SO2, NH3 and Pb use the daily mean of the hourly values; CO and O3
use the highest 8-hour running mean (of at least 6 hourly values). A
sub-index needs data in at least min_hours hours of the day.
CPCB's daily bulletin reports the AQI at 4 PM from the 24 hours before it;
day_ends_at = 16 computes the same window. CPCB validates its data
before publishing, so results can differ from the published AQI.
daily_aqi(long, min_hours = .aqi_min_hours, day_ends_at = 24)daily_aqi(long, min_hours = .aqi_min_hours, day_ends_at = 24)
long |
Data frame with columns |
min_hours |
Minimum hours with data for a sub-index (CPCB: 16). |
day_ends_at |
Hour (1–24) at which each day's 24 hours end. 24 means calendar days. A row is dated by the day its window ends. |
One row per station and date: station_id, date,
aqi, category, dominant_pollutant,
n_pollutants and one <pollutant>_sub_index column per
pollutant with data. aqi, category and
dominant_pollutant are NA when the data are insufficient.
hours <- seq(as.POSIXct("2024-01-01", tz = "UTC"), by = "hour", length.out = 24) long <- data.frame( timestamp = rep(hours, 3), pollutant = rep(c("PM2.5", "PM10", "NO2"), each = 24), value = rep(c(45, 80, 30), each = 24) ) daily_aqi(long)hours <- seq(as.POSIXct("2024-01-01", tz = "UTC"), by = "hour", length.out = 24) long <- data.frame( timestamp = rep(hours, 3), pollutant = rep(c("PM2.5", "PM10", "NO2"), each = 24), value = rep(c(45, 80, 30), each = 24) ) daily_aqi(long)
Per sensor and year, how many days the daily record equals the mean, the
last reading or neither, counting only days whose mean and last reading
differ by more than min_gap (so that a record cannot match both by
chance), with the first day equal to only the last reading and the last
day equal to only the mean before it.
daily_record_summary(audit, min_gap = 1)daily_record_summary(audit, min_gap = 1)
audit |
Output of |
min_gap |
Smallest difference between the mean and the last reading. |
Data frame with (provider,) sensor_id, year,
days, clear_days, mean, last,
neither, last_mean_day and first_last_day.
audit <- data.frame( sensor_id = "s", date = as.Date("2026-02-27") + 0:3, class = c("mean", "mean", "last", "last"), mean = 30, last = 90 ) daily_record_summary(audit)audit <- data.frame( sensor_id = "s", date = as.Date("2026-02-27") + 0:3, class = c("mean", "mean", "last", "last"), mean = 30, last = 90 ) daily_record_summary(audit)
Convert vector of AQI values to categories
get_aqi_categories(aqi_values)get_aqi_categories(aqi_values)
aqi_values |
Numeric vector of AQI values |
Character vector of AQI categories
get_aqi_categories(c(42, 180, NA, 450))get_aqi_categories(c(42, 180, NA, 450))
Convert AQI numeric value to category
get_aqi_category(aqi_value)get_aqi_category(aqi_value)
aqi_value |
Numeric AQI value |
AQI category string
get_aqi_category(150)get_aqi_category(150)
Calculate great circle distance between two points
haversine_distance(lat1, lon1, lat2, lon2)haversine_distance(lat1, lon1, lat2, lon2)
lat1 |
Latitude of first point |
lon1 |
Longitude of first point |
lat2 |
Latitude of second point |
lon2 |
Longitude of second point |
Distance in kilometers
# Delhi to Mumbai haversine_distance(28.61, 77.21, 19.08, 72.88)# Delhi to Mumbai haversine_distance(28.61, 77.21, 19.08, 72.88)
Union of BharatViz LGD district polygons, selected by district or state name. The district layer is downloaded once per session.
india_boundary(district = NULL, state = NULL)india_boundary(district = NULL, state = NULL)
district |
Character vector of district names (column
|
state |
Character vector of state names (column |
An sfc polygon in EPSG:4326.
https://geo.bharatviz.org/geojsons/districts/India_LGD_districts.geojson
mumbai <- india_boundary(district = c("Mumbai", "Mumbai Suburban")) delhi <- india_boundary(state = "Delhi")mumbai <- india_boundary(district = c("Mumbai", "Mumbai Suburban")) delhi <- india_boundary(state = "Delhi")
For every row in from (e.g. low-cost sensors), find the k
closest rows in to (e.g. reference stations) by haversine distance.
Useful for pairing sensor networks for calibration or cross-validation.
nearest_stations(from, to, k = 1, from_id = "id", to_id = "id")nearest_stations(from, to, k = 1, from_id = "id", to_id = "id")
from |
Data frame of source points. Must contain |
to |
Data frame of candidate target points. Must contain
|
k |
Integer number of nearest targets to return per source point. |
from_id, to_id
|
Column names identifying each point, carried into the
output as |
A data frame with one row per (source, nearest target) pair:
from_id, to_id, rank (1 = closest), and
dist_km.
sensors <- data.frame(id = c("a", "b"), lat = c(28.61, 19.08), lon = c(77.21, 72.88)) stations <- data.frame( id = c("delhi", "mumbai", "pune"), lat = c(28.63, 19.07, 18.52), lon = c(77.22, 72.87, 73.86) ) nearest_stations(sensors, stations, k = 2)sensors <- data.frame(id = c("a", "b"), lat = c(28.61, 19.08), lon = c(77.21, 72.88)) stations <- data.frame( id = c("delhi", "mumbai", "pune"), lat = c(28.63, 19.07, 18.52), lon = c(77.22, 72.87, 73.86) ) nearest_stations(sensors, stations, k = 2)
oaq_check_daily_records() for every day of some years of several
sensors. Today (in India) is left out by default, as its values are still
being written.
oaq_audit_daily_records( session, sensors, years, value = "pm25", tolerance = 0.005, until = NULL )oaq_audit_daily_records( session, sensors, years, value = "pm25", tolerance = 0.005, until = NULL )
session |
List returned by |
sensors |
Data frame with |
years |
Integer years. |
value |
Parameter to compare. |
tolerance |
Values within it count as equal. |
until |
First day left out (default: today in India, IST). |
Data frame as compare_daily_records() with a
provider column.
audit <- oaq_audit_daily_records( oaq_session(), data.frame(provider = "cpcb", sensor_id = "103"), 2026 ) daily_record_summary(audit)audit <- oaq_audit_daily_records( oaq_session(), data.frame(provider = "cpcb", sensor_id = "103"), 2026 ) daily_record_summary(audit)
Fetches the daily records and the 15-minute readings of the days and calls
compare_daily_records().
oaq_check_daily_records( session, provider, sensor_id, dates, value = "pm25", tolerance = 0.005 )oaq_check_daily_records( session, provider, sensor_id, dates, value = "pm25", tolerance = 0.005 )
session |
List returned by |
provider |
Character. Provider id (e.g. |
sensor_id |
Character. Sensor id, or several. |
dates |
Days, as |
value |
Parameter to check. |
tolerance |
Values within it count as equal (0.005: the daily records have 2 decimals). |
Data frame as compare_daily_records(). Days without a
record or readings are left out.
oaq_check_daily_records(oaq_session(), "cpcb", "164", c("2026-05-29", "2026-05-30"))oaq_check_daily_records(oaq_session(), "cpcb", "164", c("2026-05-29", "2026-05-30"))
Count the sensors that are not OAQ's copy of CPCB, by city and provider, next to the CPCB stations of the city.
oaq_city_tally(sensors)oaq_city_tally(sensors)
sensors |
Data frame from |
Data frame with city, provider, sensors,
cpcb_stations (CPCB stations of the city in the station
directory, 0 where CPCB has none) and median_km_to_cpcb (median
distance of the sensors to their nearest CPCB station), most sensors
first.
sensors <- data.frame( id = c("s1", "s2", "s3"), name = c("A", "B", "C"), city = c("Bengaluru", "Bengaluru", "Delhi"), lat = c(12.99, 12.95, 28.61), lon = c(77.58, 77.6, 77.21), provider = c("airnet", "airnet", "cpcb") ) oaq_city_tally(sensors)sensors <- data.frame( id = c("s1", "s2", "s3"), name = c("A", "B", "C"), city = c("Bengaluru", "Bengaluru", "Delhi"), lat = c(12.99, 12.95, 28.61), lon = c(77.58, 77.6, 77.21), provider = c("airnet", "airnet", "cpcb") ) oaq_city_tally(sensors)
OAQ's own daily aggregates (oaq_get_monthly(),
oaq_get_annual()) are often the last reading of the day rather than
its mean, so this computes the means from the readings of
oaq_get_daily() instead.
oaq_daily_means(session, provider, sensor_id, dates, min_readings = 64)oaq_daily_means(session, provider, sensor_id, dates, min_readings = 64)
session |
List returned by |
provider |
Character. Provider id (e.g. |
sensor_id |
Character. Sensor id, or several. |
dates |
Days, as |
min_readings |
Fewest readings of a parameter for a daily mean; the default, 64, is 16 hours, as for the National AQI. |
Data frame with sensor_id, date (Date) and,
for each parameter, its mean and its number of readings
(n_<parameter>). Means with fewer than min_readings
readings are NA. Days that OAQ has no file for are left out.
oaq_daily_means(oaq_session(), "airnet", "12032", c("2026-09-14", "2026-09-15"))oaq_daily_means(oaq_session(), "airnet", "12032", c("2026-09-14", "2026-09-15"))
The daily values of oaq_get_annual() for several years. These are
OAQ's values, which since about March 2026 are often the last reading of
the day rather than its mean; see oaq_check_daily_records() and
oaq_daily_means().
oaq_daily_records(session, provider, sensor_id, years)oaq_daily_records(session, provider, sensor_id, years)
session |
List returned by |
provider |
Character. Provider id (e.g. |
sensor_id |
Character. Sensor id, or several. |
years |
Integer years. |
Data frame with sensor_id, date (Date) and
one numeric column per parameter. Years that OAQ has no file for are
left out.
oaq_daily_records(oaq_session(), "cpcb", "164", 2026)oaq_daily_records(oaq_session(), "cpcb", "164", 2026)
Pulls the station list for provider, optionally restricts it to one
or more cities, then fetches monthly aggregates for every station across the
requested months. Stations that return no data for a month are skipped.
oaq_fetch_monthly_stations( session, provider, year, months = sprintf("%02d", 1:12), city = NULL )oaq_fetch_monthly_stations( session, provider, year, months = sprintf("%02d", 1:12), city = NULL )
session |
List returned by |
provider |
Character vector of provider ids (e.g. |
year |
Integer year (e.g. |
months |
Character vector of zero-padded months (e.g.
|
city |
Optional character vector of city names to keep (matched against
the station list's |
Since about March 2026, OAQ's daily values are often the last 15-minute
reading of the day, not its mean; use oaq_daily_means() or readings_mean() on
oaq_readings() for daily means.
A data frame of daily records with provider,
station_id, date (class Date), the pollutant
columns returned by the API as numbers, and lat/lon/
name from the station list.
session <- oaq_session() blr <- oaq_fetch_monthly_stations( session, c("cpcb", "airnet"), year = 2026, months = c("03", "04"), city = "Bengaluru" )session <- oaq_session() blr <- oaq_fetch_monthly_stations( session, c("cpcb", "airnet"), year = 2026, months = c("03", "04"), city = "Bengaluru" )
Retrieve annual aggregated air quality data for a specific sensor and year.
Since about March 2026, OAQ's daily values are often the last 15-minute
reading of the day, not its mean; use oaq_daily_means() or readings_mean() on
oaq_readings() for daily means.
oaq_get_annual(session, provider, sensor_id, year)oaq_get_annual(session, provider, sensor_id, year)
session |
List returned by |
provider |
Character. Provider name (e.g. |
sensor_id |
Character. Sensor station ID. |
year |
Integer or character. Year (e.g. |
Parsed annual aggregate record.
session <- oaq_session() annual <- oaq_get_annual(session, "airnet", "12036", 2026)session <- oaq_session() annual <- oaq_get_annual(session, "airnet", "12036", 2026)
Get the latest air quality map readings for all sensors in a city.
oaq_get_city_latest(session, provider, city)oaq_get_city_latest(session, provider, city)
session |
List returned by |
provider |
Character. Provider name (e.g. |
city |
Character. City name as in the provider hierarchy (any case). |
Parsed list of sensor readings for the city.
session <- oaq_session() latest <- oaq_get_city_latest(session, "airnet", "Bengaluru")session <- oaq_session() latest <- oaq_get_city_latest(session, "airnet", "Bengaluru")
Retrieve daily aggregated air quality data for a sensor and day.
oaq_get_daily(session, provider, sensor_id, year, month, day)oaq_get_daily(session, provider, sensor_id, year, month, day)
session |
List returned by |
provider |
Character. Provider name (e.g. |
sensor_id |
Character. Sensor station ID. |
year |
Integer or character. Year (e.g. |
month |
Integer or character. Month (1–12). |
day |
Integer or character. Day of month (1–31). |
Parsed daily data record.
session <- oaq_session() daily <- oaq_get_daily(session, "airnet", "12036", 2026, 9, 14)session <- oaq_session() daily <- oaq_get_daily(session, "airnet", "12036", 2026, 9, 14)
Get the Country > City > Sensor geographical tree for a data provider.
oaq_get_hierarchy(session, provider)oaq_get_hierarchy(session, provider)
session |
List returned by |
provider |
Character. Provider name (e.g. |
Parsed hierarchy list.
session <- oaq_session() hierarchy <- oaq_get_hierarchy(session, "AURASSURE")session <- oaq_session() hierarchy <- oaq_get_hierarchy(session, "AURASSURE")
Retrieve monthly aggregated air quality data for a specific sensor and month.
Since about March 2026, OAQ's daily values are often the last 15-minute
reading of the day, not its mean; use oaq_daily_means() or readings_mean() on
oaq_readings() for daily means.
oaq_get_monthly(session, provider, sensor_id, year, month)oaq_get_monthly(session, provider, sensor_id, year, month)
session |
List returned by |
provider |
Character. Provider name (e.g. |
sensor_id |
Character. Sensor station ID. |
year |
Integer or character. Year (e.g. |
month |
Integer or character. Month (1–12). |
Parsed monthly aggregate record.
session <- oaq_session() monthly <- oaq_get_monthly(session, "airnet", "12036", 2026, "09")session <- oaq_session() monthly <- oaq_get_monthly(session, "airnet", "12036", 2026, "09")
Retrieve the last 24 hours of readings for a specific sensor station.
oaq_get_station_24h(session, provider, sensor_id)oaq_get_station_24h(session, provider, sensor_id)
session |
List returned by |
provider |
Character. Provider name (e.g. |
sensor_id |
Character. Sensor station ID. |
Parsed list of hourly readings.
session <- oaq_session() data <- oaq_get_station_24h(session, "airnet", "12036")session <- oaq_session() data <- oaq_get_station_24h(session, "airnet", "12036")
Get the latest sensor readings from all monitoring stations under a provider.
oaq_get_stations_latest(session, provider)oaq_get_stations_latest(session, provider)
session |
List returned by |
provider |
Character. Provider name (e.g. |
Parsed list of station-level latest readings.
session <- oaq_session() stations <- oaq_get_stations_latest(session, "AURASSURE")session <- oaq_session() stations <- oaq_get_stations_latest(session, "AURASSURE")
Retrieve the list of all available data providers on the OAQ platform, including their IDs, display names, and data attribution.
oaq_list_providers(session)oaq_list_providers(session)
session |
List returned by |
List with a providers element containing id, name, and
attribution for each provider.
session <- oaq_session() providers <- oaq_list_providers(session) providers$providers$idsession <- oaq_session() providers <- oaq_list_providers(session) providers$providers$id
The k nearest CPCB stations of each sensor, from the bundled CPCB
station directory, so the result does not depend on which stations are
online.
oaq_nearest_cpcb(sensors, k = 1)oaq_nearest_cpcb(sensors, k = 1)
sensors |
Data frame from |
k |
Number of CPCB stations per sensor. |
Data frame with provider, sensor_id,
sensor_name, city, rank, cpcb_station_id,
cpcb_station_name, cpcb_city and distance_km,
nearest first for each sensor.
sensors <- data.frame( id = "s1", name = "Kumara Park", city = "Bengaluru", lat = 12.99, lon = 77.58, provider = "airnet" ) oaq_nearest_cpcb(sensors, k = 2)sensors <- data.frame( id = "s1", name = "Kumara Park", city = "Bengaluru", lat = 12.99, lon = 77.58, provider = "airnet" ) oaq_nearest_cpcb(sensors, k = 2)
The readings of oaq_get_daily() for several days, as one table.
oaq_readings(session, provider, sensor_id, dates)oaq_readings(session, provider, sensor_id, dates)
session |
List returned by |
provider |
Character. Provider id (e.g. |
sensor_id |
Character. Sensor id, or several. |
dates |
Days, as |
Data frame with sensor_id, timestamp (POSIXct of the
local wall time, labeled UTC as CPCB data are) and one numeric column
per parameter. Days that OAQ has no file for, and readings without a
valid time, are left out.
oaq_readings(oaq_session(), "airnet", "12036", c("2026-09-14", "2026-09-15"))oaq_readings(oaq_session(), "airnet", "12036", c("2026-09-14", "2026-09-15"))
Every sensor of the providers, all providers by default. type is
"Reference Station" for the CPCB stations that OAQ mirrors and
"Sensor" for low-cost sensors.
oaq_sensors(session, provider = NULL)oaq_sensors(session, provider = NULL)
session |
List returned by |
provider |
Optional character vector of provider ids; |
Data frame as oaq_stations_table().
sensors <- oaq_sensors(oaq_session()) table(sensors$provider, sensors$type)sensors <- oaq_sensors(oaq_session()) table(sensors$provider, sensors$type)
Performs the OAQ 2-step handshake: calls the Data Broker with your API key and returns the signed-URL parameters needed for all subsequent data requests. Sessions are valid for 24 hours.
oaq_session(api_key = Sys.getenv("OAQ_API_KEY"))oaq_session(api_key = Sys.getenv("OAQ_API_KEY"))
api_key |
Character. Your OAQ API key. Defaults to
|
Named list with base_url and sig_string (the full
query-string to append to every data URL).
session <- oaq_session()session <- oaq_session()
Combine oaq_get_stations_latest() across providers into one table
with numeric columns, dropping stations without coordinates.
oaq_stations_table(session, provider)oaq_stations_table(session, provider)
session |
List returned by |
provider |
Character vector of provider ids. |
Data frame of stations with a provider column and numeric
lat, lon and pollutant columns.
session <- oaq_session() stations <- oaq_stations_table(session, c("cpcb", "airnet"))session <- oaq_session() stations <- oaq_stations_table(session, c("cpcb", "airnet"))
On days with at least min_sensors sensors, each sensor's daily mean
over the median of the other sensors.
peer_ratio( daily, value = "pm25", min_sensors = 3, tolerance = 0.3, id_col = "sensor_id", date_col = "date" )peer_ratio( daily, value = "pm25", min_sensors = 3, tolerance = 0.3, id_col = "sensor_id", date_col = "date" )
daily |
Daily means of sensors at one place ( |
value |
Column of the daily means. |
min_sensors |
Fewest sensors with a mean on a day. |
tolerance |
Ratios within |
id_col |
Sensor column. |
date_col |
Day column. |
Data frame with id_col, days, median_ratio
and within_pct, sorted by median_ratio.
daily <- data.frame( sensor_id = rep(c("a", "b", "c"), 2), date = rep(as.Date("2026-01-01") + 0:1, each = 3), pm25 = c(10, 12, 24, 20, 20, 40) ) peer_ratio(daily)daily <- data.frame( sensor_id = rep(c("a", "b", "c"), 2), date = rep(as.Date("2026-01-01") + 0:1, each = 3), pm25 = c(10, 12, 24, 20, 20, 40) ) peer_ratio(daily)
Plot one day's readings with their mean and OAQ's daily record
plot_daily_record_day(readings, check, value = "pm25", title = NULL)plot_daily_record_day(readings, check, value = "pm25", title = NULL)
readings |
Readings of one sensor on one day ( |
check |
The day's row of |
value |
Parameter plotted. |
title |
Plot title (default: sensor and date). |
A ggplot object.
readings <- data.frame( sensor_id = "s1", timestamp = as.POSIXct("2026-03-10", tz = "UTC") + 900 * 0:95, pm25 = c(rep(40, 95), 120) ) records <- data.frame(sensor_id = "s1", date = as.Date("2026-03-10"), pm25 = 120) if (requireNamespace("ggplot2", quietly = TRUE)) { plot_daily_record_day(readings, compare_daily_records(readings, records)) }readings <- data.frame( sensor_id = "s1", timestamp = as.POSIXct("2026-03-10", tz = "UTC") + 900 * 0:95, pm25 = c(rep(40, 95), 120) ) records <- data.frame(sensor_id = "s1", date = as.Date("2026-03-10"), pm25 = 120) if (requireNamespace("ggplot2", quietly = TRUE)) { plot_daily_record_day(readings, compare_daily_records(readings, records)) }
Record minus mean per day and sensor, before and from the change date, with the median absolute difference of each under the sensor's label.
plot_daily_record_difference(audit, change, labels = NULL)plot_daily_record_difference(audit, change, labels = NULL)
audit |
Output of |
change |
Date that splits the days. |
labels |
Optional named character vector of row labels by sensor key
( |
A ggplot object.
audit <- data.frame( provider = "cpcb", sensor_id = "103", date = as.Date("2026-02-25") + 0:9, class = rep(c("mean", "last"), each = 5), mean = 50, last = 80, record = rep(c(50, 80), each = 5) ) if (requireNamespace("ggplot2", quietly = TRUE)) { plot_daily_record_difference(audit, "2026-03-01") }audit <- data.frame( provider = "cpcb", sensor_id = "103", date = as.Date("2026-02-25") + 0:9, class = rep(c("mean", "last"), each = 5), mean = 50, last = 80, record = rep(c(50, 80), each = 5) ) if (requireNamespace("ggplot2", quietly = TRUE)) { plot_daily_record_difference(audit, "2026-03-01") }
Plot OAQ's daily record against the daily mean and the last reading
plot_daily_record_scatter(audit)plot_daily_record_scatter(audit)
audit |
Output of |
A ggplot object with two panels.
audit <- data.frame( sensor_id = "103", date = as.Date("2026-02-25") + 0:9, class = rep(c("mean", "last"), each = 5), mean = 50, last = 80, record = rep(c(50, 80), each = 5) ) if (requireNamespace("ggplot2", quietly = TRUE)) plot_daily_record_scatter(audit)audit <- data.frame( sensor_id = "103", date = as.Date("2026-02-25") + 0:9, class = rep(c("mean", "last"), each = 5), mean = 50, last = 80, record = rep(c(50, 80), each = 5) ) if (requireNamespace("ggplot2", quietly = TRUE)) plot_daily_record_scatter(audit)
Plot a sensor's OAQ daily record and daily mean over time
plot_daily_record_series( audit, sensor_id, change = NULL, title = NULL, value = "pm25" )plot_daily_record_series( audit, sensor_id, change = NULL, title = NULL, value = "pm25" )
audit |
Output of |
sensor_id |
Sensor to plot. |
change |
Optional date to mark with a vertical line. |
title |
Plot title. |
value |
Parameter name for the axis label. |
A ggplot object.
audit <- data.frame( sensor_id = "103", date = as.Date("2026-02-25") + 0:9, class = "last", mean = 50 + 0:9, last = 80, record = 80 - 0:9 ) if (requireNamespace("ggplot2", quietly = TRUE)) plot_daily_record_series(audit, "103")audit <- data.frame( sensor_id = "103", date = as.Date("2026-02-25") + 0:9, class = "last", mean = 50 + 0:9, last = 80, record = 80 - 0:9 ) if (requireNamespace("ggplot2", quietly = TRUE)) plot_daily_record_series(audit, "103")
Plot which statistic each day's OAQ record equals
plot_daily_record_timeline(audit, change = NULL, labels = NULL)plot_daily_record_timeline(audit, change = NULL, labels = NULL)
audit |
Output of |
change |
Optional date to mark with a vertical line. |
labels |
Optional named character vector of row labels by sensor key
( |
A ggplot object.
audit <- data.frame( provider = "cpcb", sensor_id = "103", date = as.Date("2026-02-25") + 0:9, class = rep(c("mean", "last"), each = 5), mean = 50, last = 80, record = 50 ) if (requireNamespace("ggplot2", quietly = TRUE)) { plot_daily_record_timeline(audit, change = "2026-03-01") }audit <- data.frame( provider = "cpcb", sensor_id = "103", date = as.Date("2026-02-25") + 0:9, class = rep(c("mean", "last"), each = 5), mean = 50, last = 80, record = 50 ) if (requireNamespace("ggplot2", quietly = TRUE)) { plot_daily_record_timeline(audit, change = "2026-03-01") }
The left panel plots each sensor-day against the reference, with the 1:1
line. The right panel plots the median of the sensors and of the reference
over time. With change, the days from that date are orange in the
left panel and a line marks the date in the right panel.
plot_sensor_vs_reference( paired, value = "pm25", reference = "reference", change = NULL, sensor_label = "Sensors", reference_label = "Reference", unit = "PM2.5 (µg/m³)" )plot_sensor_vs_reference( paired, value = "pm25", reference = "reference", change = NULL, sensor_label = "Sensors", reference_label = "Reference", unit = "PM2.5 (µg/m³)" )
paired |
Data frame with one row per sensor and day: columns
|
value |
Column of the sensor values. |
reference |
Column of the reference values. |
change |
Optional date to split the days at, such as
|
sensor_label |
Name of the sensors in the legends. |
reference_label |
Name of the reference in the axis label and legend. |
unit |
Quantity and unit, for the axis labels. |
A patchwork of two ggplot2 panels.
paired <- data.frame( date = rep(seq(as.Date("2026-01-01"), by = "day", length.out = 90), 2), pm25 = c(runif(90, 20, 60), runif(90, 20, 60)), reference = runif(180, 20, 60) ) plot_sensor_vs_reference(paired, change = "2026-03-01")paired <- data.frame( date = rep(seq(as.Date("2026-01-01"), by = "day", length.out = 90), 2), pm25 = c(runif(90, 20, 60), runif(90, 20, 60)), reference = runif(180, 20, 60) ) plot_sensor_vs_reference(paired, change = "2026-03-01")
Delete all cached NetCDF files from the cache directory.
pm25_clear_cache(cache_dir = NULL)pm25_clear_cache(cache_dir = NULL)
cache_dir |
Character. Directory containing cached files. |
Invisibly returns NULL.
pm25_clear_cache()pm25_clear_cache()
Download a WUSTL ACAG SatPM2.5 NetCDF file from the AWS Open Data registry and cache it locally. A cached file larger than 1 MB is reused without downloading it again.
Use region = "AS" for work within Asia, including India; its files
are much smaller than the global ones.
pm25_download_netcdf( year, month = NULL, cache_dir = NULL, force_download = FALSE, region = "GL", version = "V6GL03" )pm25_download_netcdf( year, month = NULL, cache_dir = NULL, force_download = FALSE, region = "GL", version = "V6GL03" )
year |
Integer. Year for data, within the range of |
month |
Optional integer (1–12). If NULL, downloads the annual file. |
cache_dir |
Character. Directory to cache NetCDF files. |
force_download |
Logical. Download again even if the file is cached. |
region |
Character. Spatial subset: |
version |
Character. Dataset version:
|
Character. Path to the downloaded (or cached) NetCDF file.
https://www.satpm.org/v6-gl-03
# Monthly Asia subset nc_path <- pm25_download_netcdf(year = 2023, month = 11, region = "AS") # The older GWR product nc_path <- pm25_download_netcdf(2022, 11, region = "AS", version = "V5GL04")# Monthly Asia subset nc_path <- pm25_download_netcdf(year = 2023, month = 11, region = "AS") # The older GWR product nc_path <- pm25_download_netcdf(2022, 11, region = "AS", version = "V5GL04")
Satellite PM2.5 of the grid cell that contains each point.
pm25_get_point( latitude, longitude, year = NULL, month = NULL, version = "V6GL03", region = "GL", cache_dir = NULL, nc_file = NULL )pm25_get_point( latitude, longitude, year = NULL, month = NULL, version = "V6GL03", region = "GL", cache_dir = NULL, nc_file = NULL )
latitude, longitude
|
Numeric vectors of coordinates in decimal degrees. |
year |
Integer year. |
month |
Optional integer (1–12). If NULL, uses the annual file. |
version, region, cache_dir
|
Passed to |
nc_file |
Optional path to a local NetCDF file to use instead of a download. |
Numeric vector of PM2.5 in ug/m3, NA where the grid has no
value.
pm25_get_point(28.6139, 77.2090, year = 2023, month = 11, region = "AS")pm25_get_point(28.6139, 77.2090, year = 2023, month = 11, region = "AS")
Compute mean, SD, min, max, and pixel count of satellite PM2.5 for a region defined by a GeoJSON file. Optionally group by a column in the GeoJSON (e.g. district name) to return per-group statistics.
pm25_get_stats( geojson_file, year = NULL, month = NULL, cache_dir = NULL, group_by = NULL, nc_file = NULL, region = "GL", version = "V6GL03" )pm25_get_stats( geojson_file, year = NULL, month = NULL, cache_dir = NULL, group_by = NULL, nc_file = NULL, region = "GL", version = "V6GL03" )
geojson_file |
Character. Path or URL to a GeoJSON file defining the
region (remote |
year |
Integer. Year of the NetCDF data (e.g. |
month |
Optional integer (1–12). If NULL, uses annual data. Ignored
when |
cache_dir |
Character. Directory to cache NetCDF files. |
group_by |
Optional character. Column name(s) in the GeoJSON to group polygons by (comma-separated for multiple columns). |
nc_file |
Optional character. Path to a local NetCDF file to use
directly. When supplied, no download is attempted and |
region |
Character. Spatial subset to download, passed to
|
version |
Character. Dataset version, passed to
|
List (or data frame if group_by is specified) with
PM2.5 statistics: mean, std, min, max,
count.
# Single region (remote BharatViz boundary) stats <- pm25_get_stats( "https://geo.bharatviz.org/geojsons/cities/delhi.geojson", year = 2023, month = 11 ) cat("Mean PM2.5:", round(stats$mean, 1), "ug/m3") # Grouped by district stats_by_district <- pm25_get_stats( "https://geo.bharatviz.org/geojsons/districts/India_LGD_districts.geojson", year = 2023, month = 11, group_by = "district_name" ) # Use a local (offline) NetCDF file directly stats <- pm25_get_stats( "https://geo.bharatviz.org/geojsons/cities/delhi.geojson", nc_file = "V5GL04.HybridPM25.Global.202211-202211.nc" )# Single region (remote BharatViz boundary) stats <- pm25_get_stats( "https://geo.bharatviz.org/geojsons/cities/delhi.geojson", year = 2023, month = 11 ) cat("Mean PM2.5:", round(stats$mean, 1), "ug/m3") # Grouped by district stats_by_district <- pm25_get_stats( "https://geo.bharatviz.org/geojsons/districts/India_LGD_districts.geojson", year = 2023, month = 11, group_by = "district_name" ) # Use a local (offline) NetCDF file directly stats <- pm25_get_stats( "https://geo.bharatviz.org/geojsons/cities/delhi.geojson", nc_file = "V5GL04.HybridPM25.Global.202211-202211.nc" )
Mean and standard deviation of satellite PM2.5 within each feature of a GeoJSON file, over the cells whose centers fall inside it.
pm25_get_stats_by_polygon( geojson_file, year = NULL, month = NULL, id_field = NULL, version = "V6GL03", region = "GL", cache_dir = NULL, nc_file = NULL )pm25_get_stats_by_polygon( geojson_file, year = NULL, month = NULL, id_field = NULL, version = "V6GL03", region = "GL", cache_dir = NULL, nc_file = NULL )
geojson_file |
Path or URL of a GeoJSON file, or an |
year |
Integer year. |
month |
Optional integer (1–12). If NULL, uses the annual file. |
id_field |
Optional column that names each feature. Defaults to
|
version, region, cache_dir
|
Passed to |
nc_file |
Optional path to a local NetCDF file to use instead of a download. |
Data frame with the identifier column, mean and std.
pm25_get_stats_by_polygon( "https://geo.bharatviz.org/geojsons/districts/India_LGD_districts.geojson", year = 2023, month = 11, id_field = "district_name", region = "AS" )pm25_get_stats_by_polygon( "https://geo.bharatviz.org/geojsons/districts/India_LGD_districts.geojson", year = 2023, month = 11, id_field = "district_name", region = "AS" )
List all cached NetCDF files in the cache directory with file sizes.
pm25_list_cached_files(cache_dir = NULL)pm25_list_cached_files(cache_dir = NULL)
cache_dir |
Character. Directory containing cached files. |
Character vector of cached file paths.
files <- pm25_list_cached_files()files <- pm25_list_cached_files()
Reads the pixels covering shape's bounding box and returns them as a
terra raster in EPSG:4326, ready for masking or zonal extraction.
pm25_load_netcdf_bbox( nc_path, shape, var = "GWRPM25", lon_name = "lon", lat_name = "lat", mask = FALSE )pm25_load_netcdf_bbox( nc_path, shape, var = "GWRPM25", lon_name = "lon", lat_name = "lat", mask = FALSE )
nc_path |
Path to a gridded PM2.5 NetCDF file. |
shape |
An |
var |
NetCDF variable name. Defaults to |
lon_name, lat_name
|
Longitude/latitude dimension names. |
mask |
Logical. If |
A terra SpatRaster cropped to the bounding box.
shape <- sf::st_read("https://geo.bharatviz.org/geojsons/cities/delhi.geojson") r <- pm25_load_netcdf_bbox(pm25_v5gl04_path(2022, 11, "."), shape)shape <- sf::st_read("https://geo.bharatviz.org/geojsons/cities/delhi.geojson") r <- pm25_load_netcdf_bbox(pm25_v5gl04_path(2022, 11, "."), shape)
Plot a PM2.5 raster with an optional boundary outline. The color scale spans the 2nd to 98th percentile of the raster so that outliers do not wash out the map.
pm25_map(r, boundary = NULL, title = NULL)pm25_map(r, boundary = NULL, title = NULL)
r |
A |
boundary |
Optional |
title |
Optional plot title. |
A ggplot object. The subtitle gives the raster mean.
shape <- india_boundary(state = "Delhi") r <- pm25_load_netcdf_bbox(pm25_v5gl04_path(2022, 11, "."), shape, mask = TRUE) pm25_map(r, shape, title = "Delhi")shape <- india_boundary(state = "Delhi") r <- pm25_load_netcdf_bbox(pm25_v5gl04_path(2022, 11, "."), shape, mask = TRUE) pm25_map(r, shape, title = "Delhi")
Mean satellite PM2.5 over the bounding box of shape for each month
of a year, downloading the monthly files when they are not in
cache_dir. A directory that already holds the files under their
WUSTL names, such as a local copy of V5GL04, is read without downloading.
pm25_monthly( shape, year, months = 1:12, version = "V6GL03", region = "GL", cache_dir = NULL )pm25_monthly( shape, year, months = 1:12, version = "V6GL03", region = "GL", cache_dir = NULL )
shape |
An |
year |
Integer year. |
months |
Integer vector of months. |
version, region, cache_dir
|
Passed to |
Data frame with columns month and pm25.
delhi <- india_boundary(state = "Delhi") pm25_monthly(delhi, 2022, version = "V5GL04", region = "AS")delhi <- india_boundary(state = "Delhi") pm25_monthly(delhi, 2022, version = "V5GL04", region = "AS")
Reads only the pixels covering shape's bounding box and returns the
mean value.
pm25_read_netcdf_bbox( nc_path, shape, var = "GWRPM25", lon_name = "lon", lat_name = "lat" )pm25_read_netcdf_bbox( nc_path, shape, var = "GWRPM25", lon_name = "lon", lat_name = "lat" )
nc_path |
Path to a gridded PM2.5 NetCDF file. |
shape |
An |
var |
NetCDF variable name. Defaults to |
lon_name, lat_name
|
Longitude/latitude dimension names. |
Numeric mean value over the bounding box.
shape <- sf::st_read("https://geo.bharatviz.org/geojsons/cities/delhi.geojson") pm25_read_netcdf_bbox(pm25_v5gl04_path(2022, 11, "."), shape)shape <- sf::st_read("https://geo.bharatviz.org/geojsons/cities/delhi.geojson") pm25_read_netcdf_bbox(pm25_v5gl04_path(2022, 11, "."), shape)
Build the file path for a WUSTL V5GL04 monthly NetCDF
pm25_v5gl04_path(year, month, base_dir)pm25_v5gl04_path(year, month, base_dir)
year |
Integer year (1998–2022). |
month |
Integer month (1–12). |
base_dir |
Directory holding the monthly NetCDF files. |
Character path to the monthly NetCDF file.
pm25_v5gl04_path(2022, 11, base_dir = "~/data/V5GL04/Monthly")pm25_v5gl04_path(2022, 11, base_dir = "~/data/V5GL04/Monthly")
Low-cost optical sensors can read high in humid air. Hours with a
reference value below min_reference are left out, as the ratio is
unstable there.
ratio_by_humidity( hourly, sensor = "sensor", reference = "reference", rh = "rh", breaks = c(0, 40, 50, 60, 70, 80, 90, 100), min_reference = 5 )ratio_by_humidity( hourly, sensor = "sensor", reference = "reference", rh = "rh", breaks = c(0, 40, 50, 60, 70, 80, 90, 100), min_reference = 5 )
hourly |
Data frame with hourly sensor, reference and humidity values. |
sensor, reference, rh
|
Their columns. |
breaks |
Humidity band limits (bands are |
min_reference |
Smallest reference value used. |
Data frame with band ("40-50" for 40 < RH <= 50),
hours, median_ratio, q25 and q75.
hourly <- data.frame(sensor = c(20, 30, 45), reference = c(20, 20, 30), rh = c(35, 65, 95)) ratio_by_humidity(hourly)hourly <- data.frame(sensor = c(20, 30, 45), reference = c(20, 20, 30), rh = c(35, 65, 95)) ratio_by_humidity(hourly)
Daily or hourly means of sensor readings
readings_mean( readings, value = "pm25", by = "day", min_readings = NULL, id_col = "sensor_id", time_col = "timestamp" )readings_mean( readings, value = "pm25", by = "day", min_readings = NULL, id_col = "sensor_id", time_col = "timestamp" )
readings |
Data frame with one row per reading, such as
|
value |
Column to average. |
by |
|
min_readings |
Fewest readings for a mean. |
id_col, time_col
|
Sensor and time columns. |
Data frame with id_col, date (Date) or
hour (POSIXct), value and n, for the periods with
enough readings.
r <- data.frame( sensor_id = "s1", timestamp = as.POSIXct("2026-01-01", tz = "UTC") + 900 * 0:95, pm25 = 1:96 ) readings_mean(r)r <- data.frame( sensor_id = "s1", timestamp = as.POSIXct("2026-01-01", tz = "UTC") + 900 * 0:95, pm25 = 1:96 ) readings_mean(r)
Per sensor: least-squares slope and intercept of the sensor on the reference, R squared, RMSE and NRMSE, and which EPA targets are met.
sensor_accuracy( paired, sensor = "pm25", reference = "reference", min_days = 30, id_col = "sensor_id" )sensor_accuracy( paired, sensor = "pm25", reference = "reference", min_days = 30, id_col = "sensor_id" )
paired |
Data frame with one row per sensor and day, with the sensor and the reference daily means. |
sensor, reference
|
Columns of the sensor and reference values. |
min_days |
Fewest paired days for a sensor to be reported. |
id_col |
Sensor column. |
Data frame with id_col, days, slope,
intercept, r2, rmse, nrmse_pct,
slope_ok, intercept_ok, r2_ok, error_ok
(RMSE or NRMSE) and targets_met (0 to 4).
paired <- data.frame(sensor_id = "s1", reference = 1:40, pm25 = 2 + 1.1 * (1:40)) sensor_accuracy(paired)paired <- data.frame(sensor_id = "s1", reference = 1:40, pm25 = 2 + 1.1 * (1:40)) sensor_accuracy(paired)
Share of the expected 15-minute readings, and of days with a valid 24-hour average (75 \
sensor_completeness( readings, start, end, value = "pm25", ids = NULL, id_col = "sensor_id", time_col = "timestamp" )sensor_completeness( readings, start, end, value = "pm25", ids = NULL, id_col = "sensor_id", time_col = "timestamp" )
readings |
Data frame with one row per reading, such as
|
start, end
|
First and last day of the period (inclusive). |
value |
Column to average. |
ids |
Sensors to report, also those without readings. |
id_col, time_col
|
Sensor and time columns. |
Data frame with id_col, readings_pct,
valid_days and valid_days_pct, sorted by id_col.
r <- data.frame( sensor_id = "s1", timestamp = as.POSIXct("2026-01-01", tz = "UTC") + 900 * 0:95, pm25 = 10 ) sensor_completeness(r, "2026-01-01", "2026-01-02")r <- data.frame( sensor_id = "s1", timestamp = as.POSIXct("2026-01-01", tz = "UTC") + 900 * 0:95, pm25 = 10 ) sensor_completeness(r, "2026-01-01", "2026-01-02")
Screening flags per sensor: negative, zero and extreme readings, stuck runs and spikes. These are screening rules, not EPA targets: a flag marks readings to examine, not proof of a fault.
sensor_plausibility( readings, value = "pm25", max_value = 1000, stuck_run = 8, spike_factor = 10, spike_min = 100, spike_window = 9, id_col = "sensor_id", time_col = "timestamp" )sensor_plausibility( readings, value = "pm25", max_value = 1000, stuck_run = 8, spike_factor = 10, spike_min = 100, spike_window = 9, id_col = "sensor_id", time_col = "timestamp" )
readings |
Data frame with one row per reading, such as
|
value |
Column to check, such as |
max_value |
Readings above it are flagged. |
stuck_run |
Runs of at least this many equal consecutive readings are flagged as stuck (8 readings are 2 hours). |
spike_factor, spike_min
|
A reading above |
spike_window |
Readings in the centered window (odd); the median needs at least half of them. |
id_col, time_col
|
Sensor and time columns. |
Data frame with id_col, readings,
negative_pct, zero_pct, above_max_pct,
stuck_pct and spikes_pct.
r <- data.frame( sensor_id = "s1", timestamp = as.POSIXct("2026-01-01", tz = "UTC") + 900 * 0:19, pm25 = c(rep(5, 10), 900, 6:14) ) sensor_plausibility(r)r <- data.frame( sensor_id = "s1", timestamp = as.POSIXct("2026-01-01", tz = "UTC") + 900 * 0:19, pm25 = c(rep(5, 10), 900, 6:14) ) sensor_plausibility(r)
One row per city with station counts and the AQI of its live stations.
stations_to_city_summary(data)stations_to_city_summary(data)
data |
Cities with nested stations, from |
Data frame with city_name, city_id, state_id,
total_stations, live_stations, offline_stations,
live_percentage, avg_aqi, min_aqi, max_aqi
and stations_with_data.
summary <- stations_to_city_summary(cpcb_get_all_india()) head(summary[order(-summary$avg_aqi), ])summary <- stations_to_city_summary(cpcb_get_all_india()) head(summary[order(-summary$avg_aqi), ])
Convert nested station data to a flat data frame with geographic information, AQI values, and categories. This is the primary way to get station coordinates (latitude/longitude) from live CPCB data.
stations_to_dataframe(data)stations_to_dataframe(data)
data |
List of cities with nested stations (from cpcb_get_all_india()) |
Data frame with columns: station_id, station_name, city_name, city_id, state_id, longitude, latitude, live, avg_aqi, status, aqi_category
df <- stations_to_dataframe(cpcb_get_all_india()) table(df$aqi_category)df <- stations_to_dataframe(cpcb_get_all_india()) table(df$aqi_category)
A ggplot2 theme with a light surface, recessive grid and axes, a
left-aligned bold title and the legend below the panel. The
plot_* functions use it. Same look as the Python package's
set_theme().
theme_vayuayan(base_size = 10.5)theme_vayuayan(base_size = 10.5)
base_size |
Base font size in points. |
A ggplot2 theme, to add to a plot with +.
library(ggplot2) ggplot(mtcars, aes(wt, mpg)) + geom_point() + theme_vayuayan()library(ggplot2) ggplot(mtcars, aes(wt, mpg)) + geom_point() + theme_vayuayan()
US EPA AQI category of AQI values
us_aqi_category(aqi_value)us_aqi_category(aqi_value)
aqi_value |
Numeric vector of AQI values. |
Character vector of categories ("Good" to
"Hazardous", which includes all values above 300),
"No Data" for missing values.
us_aqi_category(c(42, 120, NA))us_aqi_category(c(42, 120, NA))
The EPA color of each AQI value as a hex code: the standard colors (EPA Table 2), or the ColorVision Assist colors (Table 3), which have more contrast for people with color vision deficiency.
us_aqi_color(aqi_value, color_vision_assist = FALSE)us_aqi_color(aqi_value, color_vision_assist = FALSE)
aqi_value |
Numeric vector of AQI values. |
color_vision_assist |
Logical. Use the ColorVision Assist colors. |
Character vector of hex colors, NA for missing values.
us_aqi_color(c(42, 120, NA)) us_aqi_color(120, color_vision_assist = TRUE)us_aqi_color(c(42, 120, NA)) us_aqi_color(120, color_vision_assist = TRUE)
Match a pollutant label such as "PM2.5 (ug/m3)", "Ozone",
"O3_1h" or "SO2 24h" to its US EPA AQI name.
us_aqi_pollutant_name(label)us_aqi_pollutant_name(label)
label |
Character vector of labels. |
Character vector of US AQI names ("PM2.5", "PM10",
"O3", "O3_1h", "CO", "SO2",
"SO2_24h", "NO2"), NA for other labels.
us_aqi_pollutant_name(c("PM2.5 (ug/m3)", "Ozone", "NH3"))us_aqi_pollutant_name(c("PM2.5 (ug/m3)", "Ozone", "NH3"))
The US EPA AQI sub-index of one pollutant (May 2024 tables). Each concentration is truncated to the precision of the table, then the sub-index is a linear function of the concentration within its band.
us_aqi_sub_index(pollutant, concentration)us_aqi_sub_index(pollutant, concentration)
pollutant |
A pollutant label (see |
concentration |
Numeric vector of averages: 24-hour for PM2.5, PM10
and |
Numeric vector of sub-indices. Above the highest breakpoint the
sub-index continues on the slope of the 301-500 band, so it can be more
than 500. NA for missing values, for 8-hour O3 above 0.200 ppm,
1-hour O3 below 0.125 ppm, 1-hour SO2 above 304 ppb and 24-hour SO2
below 305 ppb, where the EPA uses the other averaging time.
us_aqi_sub_index("PM2.5", c(9, 35.4, 400))us_aqi_sub_index("PM2.5", c(9, 35.4, 400))
Downloads are cached under one root, laid out like the Python package's
cache: CPCB repository files under cpcb/ by their repository path,
and PM2.5 NetCDF files under pm25/ by their WUSTL file names.
The root is the vayuayan.cache_dir option, else the
VAYUAYAN_CACHE_DIR environment variable, else
tools::R_user_dir("vayuayan", "cache"). Setting
VAYUAYAN_CACHE_DIR makes R and Python share one cache. Files already
in the Python package's default cache (~/.cache/vayuayan, or its
pm25_data folder in the working directory) are read without
downloading them again.
vayuayan_cache_dir()vayuayan_cache_dir()
Character path of the cache root. It is not created.
vayuayan_cache_dir()vayuayan_cache_dir()
Display package information and usage
vayuayan_welcome()vayuayan_welcome()
NULL, invisibly. Called for its printed side effect.
vayuayan_welcome()vayuayan_welcome()