1 Introduction

This report describes exploratory analysis of changes in air quality in the City of Southampton, UK in Spring 2020.

lastHA <- max(fixedDT[source == "hantsAir"]$dateTimeUTC)
diffHA <- lubridate::now() - lastHA
lastAURN <- max(fixedDT[source == "AURN"]$dateTimeUTC)
diffAURN <- lubridate::now() - lastAURN

Data for Southampton downloaded from :

Southampton City Council collects various forms of air quality data at the sites shown in Table 2.1. The data is available in raw form from http://www.hantsair.org.uk/hampshire/asp/Bulletin.asp?la=Southampton&bulletin=daily&site=SH5.

Some of these sites feed data to AURN. The data that goes via AURN is ratified to check for outliers and instrument/measurement error. AURN data less than six months old has not undergone this process. AURN data is (c) Crown 2020 copyright Defra and available for re-use via https://uk-air.defra.gov.uk, licenced under the Open Government Licence (OGL).

2 Data

In this report we use data from the following sources:

Table 2.1 shows the available sites and sources. Note that some of the non-AURN sites appear to have stopped updating recently. For a detailed analysis of recent missing data see Section ??.

t <- fixedDT[!is.na(value), .(nObs = .N, firstData = min(dateTimeUTC), latestData = max(dateTimeUTC), nMeasures = uniqueN(pollutant)), 
    keyby = .(site, source)]

kableExtra::kable(t, caption = "Sites, data source and number of valid observations. note that measures includes wind speed and direction in the AURN sourced data", 
    digits = 2) %>% kable_styling()
Table 2.1: Sites, data source and number of valid observations. note that measures includes wind speed and direction in the AURN sourced data
site source nObs firstData latestData nMeasures
Southampton - A33 Roadside (near docks, AURN site) hantsAir 85918 2017-01-01 00:00:00 2020-06-08 10:00:00 3
Southampton - Background (near city centre, AURN site) hantsAir 162148 2017-01-25 11:00:00 2020-06-08 10:00:00 6
Southampton - Onslow Road (near RSH) hantsAir 82232 2017-01-01 00:00:00 2020-04-15 07:00:00 3
Southampton - Victoria Road (Woolston) hantsAir 60078 2017-01-01 00:00:00 2020-04-01 06:00:00 3
Southampton A33 (via AURN) AURN 220010 2017-01-01 00:00:00 2020-06-07 23:00:00 8
Southampton Centre (via AURN) AURN 343216 2017-01-01 00:00:00 2020-06-07 23:00:00 13

Table 2.2 shows the poillutants recorded at each site.

t <- with(fixedDT[!is.na(value)], table(pollutant, site))

kableExtra::kable(t, caption = "Sites, pollutant and number of valid observations", digits = 2) %>% kable_styling()
Table 2.2: Sites, pollutant and number of valid observations
Southampton - A33 Roadside (near docks, AURN site) Southampton - Background (near city centre, AURN site) Southampton - Onslow Road (near RSH) Southampton - Victoria Road (Woolston) Southampton A33 (via AURN) Southampton Centre (via AURN)
no 29055 28702 27412 20026 29641 28657
no2 29020 28702 27408 20026 29640 28657
nox 0 22877 27412 20026 29640 28658
nv10 0 0 0 0 23208 21105
nv2.5 0 0 0 0 0 22627
o3 0 0 0 0 0 28645
pm10 27843 26124 0 0 25681 26180
pm2.5 0 27632 0 0 0 27702
so2 0 0 0 0 0 28261
sp2 0 28111 0 0 0 0
v10 0 0 0 0 23208 21105
v2.5 0 0 0 0 0 22627
wd 0 0 0 0 29496 29496
ws 0 0 0 0 29496 29496

To avoid confusion and 'double counting', in the remainder of the analysis we replace the Southampton AURN site data with the data for the same site sourced via AURN as shown in Table 2.3. This has the disadvantage that the data is slightly less up to date (see Table 2.1). As will be explained below in the comparative analysis we will use only the AURN data to avoid missing data issues.

fixedDT <- fixedDT[!(site %like% "AURN site")]

t <- fixedDT[!is.na(value), .(nObs = .N, nPollutants = uniqueN(pollutant), lastDate = max(dateTimeUTC)), keyby = .(site, 
    source)]

kableExtra::kable(t, caption = "Sites, data source and number of valid observations", digits = 2) %>% kable_styling()
Table 2.3: Sites, data source and number of valid observations
site source nObs nPollutants lastDate
Southampton - Onslow Road (near RSH) hantsAir 82232 3 2020-04-15 07:00:00
Southampton - Victoria Road (Woolston) hantsAir 60078 3 2020-04-01 06:00:00
Southampton A33 (via AURN) AURN 220010 8 2020-06-07 23:00:00
Southampton Centre (via AURN) AURN 343216 13 2020-06-07 23:00:00

We use this data to compare:

  • pre and during-lockdown air quality measures
  • air quality measures during lockdown 2020 with average measures for the same time periods in the preceding 3 years (2017-2019)

It should be noted that air pollution levels in any given period of time are highly dependent on the prevailing meteorological conditions. As a result it can be very difficult to disentangle the affects of a reduction in source strength from the affects of local surface conditions. This is abundantly clear in the analysis which follows given that the Easter weekend was forecast to have very high import of pollution from Europe and that the wind direction and speed was highly variable across the lockdown period (see Figure ??).

Further, air quality is not wholly driven by sources that lockdown might suppress and indeed that suppression may lead to rebound affects. For example we might expect more emissions due to increased domestic heating during cooler lockdown periods. As a result the analysis presented below must be considered a preliminary ‘before meteorological adjustment’ and ‘before controlling for other sources’ analysis of the affect of lockdown on air quality in Southampton.

For much more detailed analysis see a longer and very messy data report.

3 WHO air quality thresholds

A number of the following plots show the relevant WHO air quality thresholds and limits. These are taken from:

4 Nitrogen Dioxide (no2)

yLab <- "Nitrogen Dioxide (ug/m3)"
no2dt <- fixedDT[pollutant == "no2"]

Figure 4.1 shows the NO2 trend over time. Is lockdown below trend?

no2dt[, `:=`(date, as.Date(dateTimeUTC))]  # set date to date for this one

oaNO2 <- openair::TheilSen(no2dt[date < as.Date("2020-06-01")], "value", ylab = "NO2", deseason = TRUE, xlab = "Year", 
    date.format = "%Y", date.breaks = 4)
## [1] "Taking bootstrap samples. Please wait."
Theil-Sen de-seasoned trend (NO2)

Figure 4.1: Theil-Sen de-seasoned trend (NO2)

p <- oaNO2$plot

getModelTrendTable <- function(oa, fname) {
    # oa is an openAir object created by theilSen calculates the % below trend using the theil sen slope line
    # parameters oa <- oaGWh
    oaData <- as.data.table(oa$data$main.data)
    rDT <- oaData[, .(date, conc, a, b, slope)]
    # https://github.com/davidcarslaw/openair/blob/master/R/TheilSen.R#L192 and
    # https://github.com/davidcarslaw/openair/blob/master/R/TheilSen.R#L625
    rDT[, `:=`(x, time_length(date - as.Date("1970-01-01"), unit = "days"))]  # n days since x = 0
    rDT[, `:=`(expectedVal, a + (b * x))]  # b = slope / 365
    
    # checks
    p <- ggplot2::ggplot(rDT, aes(x = date)) + geom_line(aes(y = conc)) + labs(y = "Value", caption = fname)
    p <- p + geom_line(aes(y = expectedVal), linetype = "dashed")
    ggplot2::ggsave(here::here("docs", "plots", paste0("SSC_trendModelTestPlot_", fname, ".png")))
    rDT[, `:=`(diff, conc - expectedVal)]
    rDT[, `:=`(pcDiff, (diff/expectedVal) * 100)]
    
    t <- rDT[, .(date, conc, a, b, slope, expectedVal, diff, pcDiff)]
    return(t)
}

t <- getModelTrendTable(oaNO2, fname = "NO2")

ft <- dcast(t[date >= as.Date("2020-01-01") & date < as.Date("2020-06-01")], date ~ ., value.var = c("diff", "pcDiff"))
ft[, `:=`(date, format.Date(date, format = "%b %Y"))]
kableExtra::kable(ft, caption = "Units and % above/below expected", digits = 2) %>% kable_styling()
Table 4.1: Units and % above/below expected
date diff pcDiff
Jan 2020 -3.93 -12.91
Feb 2020 -4.13 -13.70
Mar 2020 -6.11 -20.49
Apr 2020 -3.17 -10.75
May 2020 -4.65 -15.94

5 Oxides of Nitrogen (nox)

yLab <- "Oxides of Nitrogen (ug/m3)"
noxdt <- fixedDT[pollutant == "nox"]

Figure 5.1 shows the NOx trend over time. Is lockdown below trend?

noxdt[, `:=`(date, as.Date(dateTimeUTC))]  # set date to date for this one

oaNOx <- openair::TheilSen(noxdt[date < as.Date("2020-06-01")], "value", ylab = "NOx", deseason = TRUE, xlab = "Year", 
    date.format = "%Y", date.breaks = 4)
## [1] "Taking bootstrap samples. Please wait."
Theil-Sen de-seasoned trend (NOx)

Figure 5.1: Theil-Sen de-seasoned trend (NOx)

p <- oaNOx$plot

t <- getModelTrendTable(oaNOx, fname = "NOx")

ft <- dcast(t[date >= as.Date("2020-01-01") & date < as.Date("2020-06-01")], date ~ ., value.var = c("diff", "pcDiff"))
ft[, `:=`(date, format.Date(date, format = "%b %Y"))]
kableExtra::kable(ft, caption = "Units and % above/below expected", digits = 2) %>% kable_styling()
Table 5.1: Units and % above/below expected
date diff pcDiff
Jan 2020 -5.11 -9.30
Feb 2020 -6.81 -12.55
Mar 2020 -9.41 -17.54
Apr 2020 -0.35 -0.66
May 2020 -5.28 -10.08

6 Sulphour Dioxide

yLab <- "Sulphour Dioxide (ug/m3)"
so2dt <- fixedDT[pollutant == "so2"]

Figure 6.1 shows the SO2 trend over time. Is lockdown below trend?

so2dt[, `:=`(date, as.Date(dateTimeUTC))]  # set date to date for this one

oaSO2 <- openair::TheilSen(so2dt[date < as.Date("2020-06-01")], "value", ylab = "SO2", deseason = TRUE, xlab = "Year", 
    date.format = "%Y", date.breaks = 4)
## [1] "Taking bootstrap samples. Please wait."
Theil-Sen de-seasoned trend (SO2)

Figure 6.1: Theil-Sen de-seasoned trend (SO2)

t <- getModelTrendTable(oaSO2, fname = "SO2")

ft <- dcast(t[date >= as.Date("2020-01-01") & date < as.Date("2020-06-01")], date ~ ., value.var = c("diff", "pcDiff"))
ft[, `:=`(date, format.Date(date, format = "%b %Y"))]
kableExtra::kable(ft, caption = "Units and % above/below expected", digits = 2) %>% kable_styling()
Table 6.1: Units and % above/below expected
date diff pcDiff
Jan 2020 -0.74 -41.70
Feb 2020 -0.46 -25.66
Mar 2020 -0.21 -11.60
Apr 2020 1.90 105.20
May 2020 1.21 66.56

7 Ozone

yLab <- "Ozone (ug/m3)"
o3dt <- fixedDT[pollutant == "o3"]

Figure 7.1 shows the O3 trend over time. Is lockdown below trend?

o3dt[, `:=`(date, as.Date(dateTimeUTC))]  # set date to date for this one

oaO3 <- openair::TheilSen(o3dt[date < as.Date("2020-06-01")], "value", ylab = "O3", deseason = TRUE, xlab = "Year", 
    date.format = "%Y", date.breaks = 4)
## [1] "Taking bootstrap samples. Please wait."
Theil-Sen de-seasoned trend (O3)

Figure 7.1: Theil-Sen de-seasoned trend (O3)

t <- getModelTrendTable(oaO3, fname = "O3")

ft <- dcast(t[date >= as.Date("2020-01-01") & date < as.Date("2020-06-01")], date ~ ., value.var = c("diff", "pcDiff"))
ft[, `:=`(date, format.Date(date, format = "%b %Y"))]
kableExtra::kable(ft, caption = "Units and % above/below expected", digits = 2) %>% kable_styling()
Table 7.1: Units and % above/below expected
date diff pcDiff
Jan 2020 4.30 9.31
Feb 2020 8.84 19.07
Mar 2020 4.77 10.25
Apr 2020 4.31 9.22
May 2020 6.80 14.48

8 PM 10

yLab <- "PM 10 (ug/m3)"
pm10dt <- fixedDT[pollutant == "pm10"]

Figure 8.1 shows the PM10 trend over time. Is lockdown below trend?

pm10dt[, `:=`(date, as.Date(dateTimeUTC))]  # set date to date for this one

oaPM10 <- openair::TheilSen(pm10dt[date < as.Date("2020-06-01")], "value", ylab = "PM10", deseason = TRUE, xlab = "Year", 
    date.format = "%Y", date.breaks = 4)
## [1] "Taking bootstrap samples. Please wait."
Theil-Sen de-seasoned trend (PM10)

Figure 8.1: Theil-Sen de-seasoned trend (PM10)

t <- getModelTrendTable(oaPM10, fname = "SPM10")

ft <- dcast(t[date >= as.Date("2020-01-01") & date < as.Date("2020-06-01")], date ~ ., value.var = c("diff", "pcDiff"))
ft[, `:=`(date, format.Date(date, format = "%b %Y"))]
kableExtra::kable(ft, caption = "Units and % above/below expected", digits = 2) %>% kable_styling()
Table 8.1: Units and % above/below expected
date diff pcDiff
Jan 2020 -0.10 -0.62
Feb 2020 -1.58 -10.10
Mar 2020 1.55 9.91
Apr 2020 -0.06 -0.38
May 2020 1.57 10.19

9 PM 2.5

yLab <- "PM 2.5 (ug/m3)"
pm25dt <- fixedDT[pollutant == "pm2.5"]

Figure 9.1 shows the PM10 trend over time. Is lockdown below trend?

pm25dt[, `:=`(date, as.Date(dateTimeUTC))]  # set date to date for this one

oaPM25 <- openair::TheilSen(pm25dt[date < as.Date("2020-06-01")], "value", ylab = "PM2.5", deseason = TRUE, xlab = "Year", 
    date.format = "%Y", date.breaks = 4)
## [1] "Taking bootstrap samples. Please wait."
Theil-Sen de-seasoned trend (PM2.5)

Figure 9.1: Theil-Sen de-seasoned trend (PM2.5)

t <- getModelTrendTable(oaPM25, fname = "PM2.5")

ft <- dcast(t[date >= as.Date("2020-01-01") & date < as.Date("2020-06-01")], date ~ ., value.var = c("diff", "pcDiff"))
ft[, `:=`(date, format.Date(date, format = "%b %Y"))]
kableExtra::kable(ft, caption = "Units and % above/below expected", digits = 2) %>% kable_styling()
Table 9.1: Units and % above/below expected
date diff pcDiff
Jan 2020 0.70 7.70
Feb 2020 -0.42 -4.72
Mar 2020 0.38 4.27
Apr 2020 1.59 18.11
May 2020 1.73 19.89

10 About

10.2 Comments and feedback

If you wish to comment please open an issue:

10.3 Citation

If you wish to refer to any of the material from this report please cite as:

  • Anderson, B., (2020) Air Quality in Southampton (UK): Exploring the effect of UK covid 19 lockdown on air quality: Summary for BBC South , Sustainable Energy Research Group, University of Southampton: Southampton, UK.

Report circulation:

  • Public

This work is (c) 2020 the University of Southampton and is part of a collection of air quality data analyses.

11 Runtime

Report generated using knitr in RStudio with R version 3.6.0 (2019-04-26) running on x86_64-redhat-linux-gnu (#1 SMP Thu May 7 19:30:37 EDT 2020).

t <- proc.time() - myParams$startTime

elapsed <- t[[3]]

Analysis completed in 13.099 seconds ( 0.22 minutes).

R packages used in this report:

  • data.table - (Dowle et al. 2015)
  • ggplot2 - (Wickham 2009)
  • here - (Müller 2017)
  • kableExtra - (Zhu 2019)
  • lubridate - (Grolemund and Wickham 2011)
  • openAir - (Carslaw and Ropkins 2012)
  • skimr - (Arino de la Rubia et al. 2017)
  • viridis - (Garnier 2018)

References

Arino de la Rubia, Eduardo, Hao Zhu, Shannon Ellis, Elin Waring, and Michael Quinn. 2017. Skimr: Skimr. https://github.com/ropenscilabs/skimr.

Carslaw, David C., and Karl Ropkins. 2012. “Openair — an R Package for Air Quality Data Analysis.” Environmental Modelling & Software 27–28 (0): 52–61. doi:10.1016/j.envsoft.2011.09.008.

Dowle, M, A Srinivasan, T Short, S Lianoglou with contributions from R Saporta, and E Antonyan. 2015. Data.table: Extension of Data.frame. https://CRAN.R-project.org/package=data.table.

Garnier, Simon. 2018. Viridis: Default Color Maps from ’Matplotlib’. https://CRAN.R-project.org/package=viridis.

Grolemund, Garrett, and Hadley Wickham. 2011. “Dates and Times Made Easy with lubridate.” Journal of Statistical Software 40 (3): 1–25. http://www.jstatsoft.org/v40/i03/.

Müller, Kirill. 2017. Here: A Simpler Way to Find Your Files. https://CRAN.R-project.org/package=here.

Wickham, Hadley. 2009. Ggplot2: Elegant Graphics for Data Analysis. Springer-Verlag New York. http://ggplot2.org.

Zhu, Hao. 2019. KableExtra: Construct Complex Table with ’Kable’ and Pipe Syntax. https://CRAN.R-project.org/package=kableExtra.