A story about one of the retail chains (J.C. Penny) releasing their list of stores closing in 2017 crossed paths with my Feedly reading list today and jogged my memory that there were a number of chains closing many of their doors this year, and I wanted to see the impact that might have on various states.
I’m also doing this to add one more example of:
- scraping (with content caching)
- data cleaning
- per-capita normalization
- comparing salient information to other indicators
to the growing list of great examples out there by the extended R community. Plus, I feel compelled to try to keep up with @ma_salmon’s blogging pace.
Let’s jump right in…
library(httr)
library(rvest)
library(knitr)
library(kableExtra)
library(ggalt)
library(statebins)
library(hrbrthemes)
library(tidyverse)
options(knitr.table.format = "html")
update_geom_font_defaults(font_rc_light, size = 2.75)
“Closing” lists of four major retailers — K-Mart, Sears, Macy’s and J.C. Penny — abound (HTML formatting a list seems to be the “easy way out” story-wise for many blogs and newspapers). We can dig a bit deeper than just a plain set of lists, but first we need the data.
The Boston Globe has a nice, predictable, mostly-uniform pattern to their list-closing “stories”, so we’ll use that data. Site content can change quickly, so it makes sense to try to cache content whenever possible as we scrape it. To that end, we’ll use httr::GET
vs xml2::read_html
since GET
preserves all of the original request and response information and read_html
returns an external pointer that has no current support for serialization without extra work.
closings <- list(
kmart = "https://www.bostonglobe.com/metro/2017/01/05/the-full-list-kmart-stores-closing-around/4kJ0YVofUWHy5QJXuPBAuM/story.html",
sears = "https://www.bostonglobe.com/metro/2017/01/05/the-full-list-sears-stores-closing-around/yHaP6nV2C4gYw7KLhuWuFN/story.html",
macys = "https://www.bostonglobe.com/metro/2017/01/05/the-full-list-macy-stores-closing-around/6TY8a3vy7yneKV1nYcwY7K/story.html",
jcp = "https://www.bostonglobe.com/business/2017/03/17/the-full-list-penney-stores-closing-around/vhoHjI3k75k2pSuQt2mZpO/story.html"
)
saved_pgs <- "saved_store_urls.rds"
if (file.exists(saved_pgs)) {
pgs <- read_rds(saved_pgs)
} else {
pgs <- map(closings, GET)
write_rds(pgs, saved_pgs)
}
This is what we get from that scraping effort:
map(pgs, content) %>%
map(html_table) %>%
walk(~glimpse(.[[1]]))
## Observations: 108
## Variables: 3
## $ X1 <chr> "300 Highway 78 E", "2003 US Hwy 280 Bypass", "3600 Wilson ...
## $ X2 <chr> "Jasper", "Phenix City", "Bakersfield", "Coalinga", "Kingsb...
## $ X3 <chr> "AL", "AL", "CA", "CA", "CA", "CA", "CO", "CO", "CT", "FL",...
## Observations: 42
## Variables: 4
## $ X1 <chr> "301 Cox Creek Pkwy", "1901 S Caraway Road", "90 Elm St; En...
## $ X2 <chr> "Florence", "Jonesboro", "Enfield", "Lake Wales", "Albany",...
## $ X3 <chr> "AL", "AR", "CT", "FL", "GA", "GA", "IN", "KS", "KY", "LA",...
## $ X4 <chr> "Y", "N", "N", "Y", "Y", "N", "N", "N", "Y", "Y", "Y", "Y",...
## Observations: 68
## Variables: 6
## $ X1 <chr> "Mission Valley Apparel", "Paseo Nuevo", "*Laurel Plaza", "...
## $ X2 <chr> "San Diego", "Santa Barbara", "North Hollywood", "Simi Vall...
## $ X3 <chr> "CA", "CA", "CA", "CA", "FL", "FL", "FL", "FL", "FL", "GA",...
## $ X4 <int> 385000, 141000, 475000, 190000, 101000, 195000, 143000, 140...
## $ X5 <int> 1961, 1990, 1995, 2006, 1995, 2000, 1977, 1974, 2000, 1981,...
## $ X6 <int> 140, 77, 105, 105, 68, 83, 86, 73, 72, 69, 9, 57, 54, 87, 5...
## Observations: 138
## Variables: 3
## $ Mall/Shopping Center <chr> "Auburn Mall", "Tannehill Promenade", "Ga...
## $ City <chr> "Auburn", "Bessemer", "Gadsden", "Jasper"...
## $ State <chr> "AL", "AL", "AL", "AL", "AR", "AR", "AZ",...
We now need to normalize the content of the lists.
map(pgs, content) %>%
map(html_table) %>%
map(~.[[1]]) %>%
map_df(select, abb=3, .id = "store") -> closings
We’re ultimately just looking for city/state for this simple exercise, but one could do more precise geolocation (perhaps with rgeocodio
) and combine that with local population data, job loss estimates, current unemployment levels, etc. to make a real story out of the closings vs just do the easy thing and publish a list of stores.
count(closings, abb) %>%
left_join(data_frame(name = state.name, abb = state.abb)) %>%
left_join(usmap::statepop, by = c("abb"="abbr")) %>%
mutate(per_capita = (n/pop_2015) * 1000000) %>%
select(name, n, per_capita) -> closings_by_state
(NOTE: you can get the code for the entire Rmd via RPubs or GitHub)
Compared to unemployment/underutilization
I’d have used the epidata
package to get the current state unemployment data but it’s not quite current enough, so we can either use a package to get data from the Bureau of Labor Statistics or just scrape it. A quick coin-flip says we’ll scrape the data.
We’ll use the U-6 rate since that is an expanded definition of “underutilization” including “total unemployed, plus all marginally attached workers, plus total employed part time for economic reasons, as a percent of the civilian labor force plus all marginally attached workers” and is likely to more representative for the populations working at retail chains. I could be wrong. I only play an economist on ?. If you are an economist, please drop a note telling me where I errd in my thinking ?
pg <- read_html("https://www.bls.gov/lau/stalt16q4.htm")
html_nodes(pg, "table#alternmeas16\\:IV") %>%
html_table(header = TRUE, fill = TRUE) %>%
.[[1]] %>%
docxtractr::assign_colnames(1) %>%
rename(name=State) %>%
as_data_frame() %>%
slice(2:52) %>%
type_convert() %>%
left_join(closings_by_state, by="name") %>%
filter(!is.na(n)) -> with_unemp
ggplot(with_unemp, aes(per_capita, `U-6`)) +
geom_label(aes(label=name), fill="#8c96c6", color="white", size=3.5, family=font_rc) +
scale_x_continuous(limits=c(-0.125, 6.75)) +
labs(x="Closings per-capita (1MM)",
y="BLS Labor Underutilization (U-6 rate)",
title="Per-capita store closings compared to current BLS U-6 Rate") +
theme_ipsum_rc(grid="XY")
If I were a reporter (again, I only play one on ?), I think I’d be digging a bit deeper on the impact of these (and the half-dozen or so other) retailers closing locations in New Mexico, Nevada, West Virginia, Wyoming, (mebbe Maine, though I’m super-b ased :-), North Dakota & South Dakota. I also hope @Marketplace does a few more stories on the changing retail landscape in the U.S. over the coming months to see if there are any overt consequences to the loss of jobs and anchor stores.
If you end up tinkering with the data, drop a note in the comments if something you discover piques your interest. For those interested in potentially marrying the data up with some additional cartography, there should be enough precision the store lists to get distinct enough lat/lon paris after geocoding (I did a quick test with rgeocodio
) to make some interesting map views, especially if you can find more store closing lists.
2 Comments
Now correlate this with the recession in oil exploration and coal in various states
Better yet the publicly available Baker Hughes rig counts by state
7 Trackbacks/Pingbacks
[…] A story about one of the retail chains (J.C. Penny) releasing their list of stores closing in 2017 crossed paths with my Feedly reading list today and jogged my memory that there were a number of chains closing many of their doors this year, and I wanted to see the impact that might have on… Continue reading → […]
[…] article was first published on R – rud.is, and kindly contributed to […]
[…] article was first published on R – rud.is, and kindly contributed to […]
[…] Exploring 2017 Retail Store Closings with R […]
[…] been doing intermittent prep work for a follow-up to an earlier post on store closings and came across this CNN Money “article” on it. Said “article” is a […]
[…] talked about the retailpocalypse before and this morning I was greeted with the news about Dressbarn closing all 650 stores as I fired up a […]
[…] talked about the retailpocalypse before and this morning I was greeted with the news about Dressbarn closing all 650 stores as I fired up a […]