EV Charging Stations Analysis

energy
EV
R
visualization
API
Published

June 20, 2023

Modified

May 8, 2024

Introduction

Recently I’ve been interested in analyzing trends in electric vehicle (EV) charging stations, using data from the Alternative Fuels Data Center’s Alternative Fuel Stations database. In this first post I’ll go over retrieving the data via an API, getting it into a tidy format, and some initial analysis and visualization.

Data

I’ll retrieve the EV station data using the AFDC API. The documentation for the AFDC fuel-stations API can be found at: https://developer.nrel.gov/docs/transportation/alt-fuel-stations-v1/all/#station-count-record-fields

  • You can obtain a free API key at: [https://developer.nrel.gov/signup/]. I’ve saved my API key in my local .Renviron file so I can load it without exposing the key in my code.

    Tip

    I find the easiest way to store an API key in your .Renviron file is with the usethis (Wickham, Bryan, et al. 2023) package. Simply call the usethis::edit_r_environ() function to open the .Renviron file, add your key in the form API_KEY=“xxxxx”, and save. After you restart R, you will be able to load the key in your code using Sys.getenv("API_KEY").

  • I will request data for all EV stations in Colorado.

  • I’ll retrieve the data from the API using the httr (Wickham 2023) package.

Code
# API key is stored in my .Renviron file
api_key <- Sys.getenv("AFDC_KEY")

# base url for AFDC alternative fuel stations API
target <- "https://developer.nrel.gov/api/alt-fuel-stations/v1"

# Return data for all electric stations in Colorado
api_path <- ".json?&fuel_type=ELEC&state=CO&limit=all"

complete_api_path <- paste0(target,api_path,'&api_key=',api_key)

response <- httr::GET(url = complete_api_path)

if (response$status_code != 200) {
 print(paste('Warning, API call returned error code',response$status_code))
}

print(paste("Data last downloaded on", response$date, "with response code of",response$status_code))
[1] "Data last downloaded on 2023-12-21 18:16:03 with response code of 200"
  • The result returned from the API is a response object, and the data is in JSON format. The response (which I’m not printing here because would show my API key) contains a status code; a code of 200 means the API request was successful. Some of the general error codes the API might return are described here.

  • I’ll use the jsonlite (Ooms 2014) package to convert the JSON to R.

Code
ev_dat <- jsonlite::fromJSON(httr::content(response,"text"))

class(ev_dat)
[1] "list"
Code
names(ev_dat)
[1] "station_locator_url" "total_results"       "station_counts"     
[4] "fuel_stations"      
  • The converted response is actually a list containing the data as well as some metadata about the request.

  • The total_results field gives the total number of fuel station records that match your requested query (regardless of any limit applied).

Code
ev_dat$total_results
[1] 2318
  • The *station_counts* field gives a breakdown by fuel type (here I requested only electric so the counts for all other fuel types are zero).

    • total includes the number of individual chargers/plugs, which is why it is greater than the station count.

    • In this case, there are 2318 stations, and a total of 5737 chargers/plugs.

Code
ev_dat$station_counts$fuels$ELEC
$total
[1] 5737

$stations
$stations$total
[1] 2318

Finally, the data we want to analyze is in the fuel_stations data frame.

Code
ev <- ev_dat$fuel_stations

Filter out non-EV data columns

The returned data contains many non-electric fields that we don’t need (they will all be NA since we requested electric fuel type only), so I’ll remove the non-relevant fields from the data frame to clean things up a bit, using the starts_with function from the dplyr (Wickham, François, et al. 2023) package.

I’ll also change the date column type and add a variable for year opened, since I want to look at how many stations were opened over time.

Code
# filter out non-EV related fields
ev <- ev %>% select(-dplyr::starts_with("lng")) %>% 
  select(-starts_with("cng")) %>%
  select(-starts_with("lpg")) %>%
  select(-starts_with("hy")) %>% 
  select(-starts_with("ng")) %>% 
  select(-starts_with("e85")) %>% 
  select(-starts_with("bd")) %>% 
  select(-starts_with("rd")) %>% 
  filter(status_code == 'E') |>
  select(-ends_with("_fr")) # drop french language columns


# change date field to date type and add a year opened variable
ev$open_date <- lubridate::ymd(ev$open_date)
ev$open_year <- lubridate::year(ev$open_date)

Analysis

Station Openings Over Time

How many stations opened each year?

First I’d like to look at how many EV stations opened over time, so I’ll make a new data frame summarizing the number of stations opened by year.

Code
ev_opened <- ev %>% 
  count(open_year,name = "nopened")  %>% 
  filter(!is.na(open_year)) 

ev_opened |>
  DT::datatable(rownames = FALSE, options = list(pageLength = 5))
Table 1: Number of charging stations opened per year.
Code
ev_opened %>% ggplot(aes(open_year, nopened)) + 
  geom_col() +
  xlab("Year Opened") +
  ylab("# Stations Opened") +
  ggtitle('EV Stations Opened in Colorado Each Year',subtitle = paste("Data downloaded on", response$date)) +
  theme_grey(base_size = 15) +
  geom_text(aes(label = nopened), vjust = 0)
Figure 1: Number of EV Charging Stations Opened In Colorado each year

Cumulative sum of stations opened over time

We can also look at the cumulative sum of stations opened over time

Code
ev_opened %>% ggplot(aes(open_year,cumsum(nopened))) +
  geom_line(linewidth = 1.5) +
  xlab("Year") +
  ylab("# Stations") +
  ggtitle("Cumulative sum of EV stations opened in CO") +
  theme_grey(base_size = 15)
Figure 2: Cumulative sum of EV stations opened in CO

Station openings by level/charger type

Next I want to dig a little deeper and break down the station openings by charger type and/or level. I’d expect to see more Level 2 chargers in earlier years, and an increase in DC fast charging stations in more recent years. I’ll make a new data frame with the number of chargers opened by year, grouped by charging level (Level 1, Level 2, or DC fast).

  • Note here I’m working with the number of chargers of each level, not the number of stations.
Code
ev_opened_level <- ev %>% 
  select(id,open_date,
         open_year,
         ev_dc_fast_num,
         ev_level2_evse_num,ev_level1_evse_num) %>%
  group_by(open_year) %>%
  summarize(n_DC = sum(ev_dc_fast_num,na.rm = TRUE), 
            n_L2 = sum(ev_level2_evse_num,na.rm = TRUE),
            n_L1 = sum(ev_level1_evse_num,na.rm = TRUE) ) %>% 
  filter(!is.na(open_year))

ev_opened_level |>
  DT::datatable(rownames = FALSE, options = list(pageLength = 5))
Table 2: Number of EV chargers opened per year, by charging level

To make plotting easier, I’ll pivot the dataframe from wide to long format so I can group by charging level:

Code
ev_opened_level_long <- ev_opened_level %>% 
  tidyr::pivot_longer(cols = c('n_DC','n_L2','n_L1'),
                      names_to = "Level",
                      names_prefix = "n_",
                      values_to = "n_opened")

ev_opened_level_long |>
  DT::datatable(rownames = FALSE, options = list(pageLength = 5))
Table 3: Number of EV chargers opened per year by charging level, in long format for plotting.

Now I can go ahead and plot the number of chargers opened over time, by level.

Code
g <- ev_opened_level_long %>% 
  ggplot(aes(open_year, n_opened, group = Level)) +
  geom_line(aes(col = Level), linewidth = 1.5) +
  geom_point(aes(col = Level), size = 4) +
  xlab("Year Opened") +
  ylab("# Charges Opened") +
  ggtitle("Number of Chargers Opened Per Year By Level")
  
plotly::ggplotly(g)
Figure 3: Number of Chargers Opened Per Year By Level

Session Info

Code
R version 4.3.1 (2023-06-16)
Platform: x86_64-apple-darwin20 (64-bit)
Running under: macOS Sonoma 14.2.1

Matrix products: default
BLAS:   /Library/Frameworks/R.framework/Versions/4.3-x86_64/Resources/lib/libRblas.0.dylib 
LAPACK: /Library/Frameworks/R.framework/Versions/4.3-x86_64/Resources/lib/libRlapack.dylib;  LAPACK version 3.11.0

locale:
[1] en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8

time zone: America/Denver
tzcode source: internal

attached base packages:
[1] stats     graphics  grDevices datasets  utils     methods   base     

other attached packages:
[1] dplyr_1.1.3    ggplot2_3.4.4  jsonlite_1.8.7 httr_1.4.7    

loaded via a namespace (and not attached):
 [1] gtable_0.3.4      compiler_4.3.1    renv_1.0.3        tidyselect_1.2.0 
 [5] tidyr_1.3.0       jquerylib_0.1.4   scales_1.2.1      yaml_2.3.7       
 [9] fastmap_1.1.1     R6_2.5.1          labeling_0.4.3    generics_0.1.3   
[13] curl_5.1.0        knitr_1.44        htmlwidgets_1.6.2 tibble_3.2.1     
[17] munsell_0.5.0     lubridate_1.9.3   bslib_0.5.1       pillar_1.9.0     
[21] rlang_1.1.1       utf8_1.2.4        DT_0.30           cachem_1.0.8     
[25] xfun_0.40         sass_0.4.7        lazyeval_0.2.2    viridisLite_0.4.2
[29] plotly_4.10.3     timechange_0.2.0  cli_3.6.1         withr_2.5.1      
[33] magrittr_2.0.3    crosstalk_1.2.0   digest_0.6.33     grid_4.3.1       
[37] rstudioapi_0.15.0 lifecycle_1.0.3   vctrs_0.6.4       data.table_1.14.8
[41] evaluate_0.22     glue_1.6.2        farver_2.1.1      fansi_1.0.5      
[45] colorspace_2.1-0  purrr_1.0.2       rmarkdown_2.25    ellipsis_0.3.2   
[49] tools_4.3.1       pkgconfig_2.0.3   htmltools_0.5.6.1

References

Ooms, Jeroen. 2014. “The Jsonlite Package: A Practical and Consistent Mapping Between JSON Data and r Objects.” https://arxiv.org/abs/1403.2805.
Wickham, Hadley. 2023. “Httr: Tools for Working with URLs and HTTP.” https://CRAN.R-project.org/package=httr.
Wickham, Hadley, Jennifer Bryan, Malcolm Barrett, and Andy Teucher. 2023. “Usethis: Automate Package and Project Setup.”
Wickham, Hadley, Romain François, Lionel Henry, Kirill Müller, and Davis Vaughan. 2023. “Dplyr: A Grammar of Data Manipulation.” https://CRAN.R-project.org/package=dplyr.