3 Building a UK local news directory
In this chapter, I do two things:
put together all existing data sources for local news titles in the UK and build a huge (and, I admit, messy) UK local and regional news directory. The directory attempts to be capture “official” titles as well as established hyperlocals. To get there, I follow similar sources (e.g., JICREG, ABC, LMW) to previous studies, but differently from all of them I: a) automate the data extraction where possible, to keep the data as current as possible; b) merge all possible trusted sources, instead of relying on a single one or a few (I use regulators, media owners lists, associations lists, circulation auditors, and more).
create my own directory which ultimately relies on the best source out of all them (hint: it’s Media Coalition Reform latest available dataset from 2021), and simply updates with the latest newspaper closures and launches as reported by the Press Gazette. The goal of this directory is to become the basis and starting point for my PhD research, guiding me in selecting a suitable sample of local papers to study in my project on the geographical relevance of local news content.
Truth be said, I was hoping to be able to perform a data verification and cleaning in the automation merging (my messy dataset) that would lead me to an original and clean directory, however I encountered big challenges in merging and cleaning the data (for example differences in how newspapers or their owners are called across different sources, or not entirely trusting one source). So that lead me to ultimately build my own directory from the one cleanest and most updated source I found. Nonetheless, I think my automation efforts can still be useful to keep track of changes on listings across different sources, and let anyone be able to see these changes overtime (and if anyone wants to clean my dataset, please be my guest!).
3.1 General challenges
Putting together a directory is not really straightforward in the UK. For one, due to the lack of a dataset existing solely with the purpose of mapping the number of local titles in the country, without any further specification or limitations such as that they should be hyperlocals, or should be recorded by JICREG.
As Ramsay and Moore explained in 2016 (p.18):
Establishing the exact number of distinct local newspapers available in the UK proved to be a difficult and time-consuming process, due to deficiencies in existing publicly available databases. These databases miss out a number of newspapers, and in many cases contain only grouped or aggregate information for a number of titles covering different local areas. For this analysis, a new and comprehensive list of individual local newspaper titles, including distinct sub-editions, was generated, using a variety of information sources.
And (p.19):
Gaps in existing datasets are in part due to the large number of titles, and relatively regular ‘churn’ of the figures due to closures, mergers and the creation of new titles. However, some of the sources of data – including the brand lists provided publicly or on request by some publishing groups – are (at the time of writing) out-of-date, and some combine multiple individual and distinct titles into single grouped cases. In order to compile as comprehensive a list of local titles as possible, this report used numerous different data sources and, where necessary, newspapers and news groups were contacted directly. Given the discrepancies between the sources, the number of newspapers involved, and the regularity with which newspapers change hands, merge or close, it must be borne in mind that a small number of titles may have escaped inclusion.
3.2 The big dataset
Below, I parse data from around 17 differnt sources, which, for easiness, I have grouped in: regulators, membership association sites, publishers, and other.
3.2.1 Regulators
After the Leveson Inquiry the Press Recognition Panel (PRP) was set up under the Royal Charter on self-regulation of the press to judge whether press regulators meet the criteria recommended by the Inquiry. By 2016 two new press regulatory bodies were set up in the UK: the Independent Press Standards Organisation (IPSO), which regulates most national newspapers and many other media outlets, and IMPRESS, which regulates a much smaller number of outlets but is the only press regulator recognised by the PRP.
IPSO
The Independent Press Standards Organisation (IPSO) is the independent regulator for the newspaper and magazine industry in the UK. It was created in 2014 following the windup of the Press Complaints Commission (PCC), which had existed since 1990. IPSO monitors and regulates newspapers and magazines, overseeing their actions, upholding high standards of journalism and helping to maintain freedom of expression for the press. Several broadsheet newspapers (e.g., The Guardian and The Financial Times) have declined to join IPSO, establishing their own independent complaints systems. IPSO regulates more than 1,500 print titles and more than 1,100 online titles, including most of the UK’s national newspapers. IPSO is funded by its member publishers. IPSO has not sought recognition under the Royal Charter.
# generate urls
<- c("0,1,2,3,4,5,6,7,8,9", letters)
suffices <- paste0("https://www.ipso.co.uk/complain/who-ipso-regulates/?letters=", suffices)
urls
# define the scraping function
<- function(urls) {
scraper read_html(urls) %>%
html_elements(".pagination-listings") %>%
html_elements("li") %>%
html_text() %>%
as.data.frame()
}
# iterate over the urls
<- map_dfr(urls, scraper)
ipso <- ipso %>%
ipso mutate(
Publisher = str_extract(., "\\s\\([^)]+\\)$"),
Publisher = str_remove(Publisher, "\\("),
Publisher = str_remove(Publisher, "\\)"),
Publication = str_remove(., "\\s\\([^)]+\\)$"),
Publisher = str_remove(Publisher, "\\s$")) %>%
select(-.) %>%
mutate_all(str_trim) %>%
mutate(retrieval_date = Sys.Date())
<- dim(ipso)[1]
ipso_len
# processing publishers: how many are there, and who are the biggest ones?
<- ipso %>%
ipso_publishers count(Publisher) %>%
arrange(desc(n)) %>%
mutate(perc = round(n/nrow(ipso)*100,2)) %>%
na.omit()
<- nrow(ipso_publishers) ipso_publishers_tot
Ipso regulates 2103 titles. These belong to 99 different publishers. Here below are shown the 10 predominant ones.
kable(ipso_publishers[1:10, ])
Publisher | n | perc |
---|---|---|
Newsquest Media Group | 461 | 21.92 |
National World Publishing Ltd | 348 | 16.55 |
Reach PLC | 251 | 11.94 |
Future PLC | 165 | 7.85 |
Tindle Newspapers Limited | 93 | 4.42 |
Immediate Media Company Limited | 86 | 4.09 |
H. Bauer Publishing | 83 | 3.95 |
DC Thomson Group | 76 | 3.61 |
Iliffe Media Group Ltd | 54 | 2.57 |
Nub News Ltd | 49 | 2.33 |
# append to ipso file
<- read_csv("./files/ipso.csv")
ipso_main <- ipso_main %>% bind_rows(ipso) %>% group_by(Publication, Publisher) %>% arrange(retrieval_date) %>% slice(1L)
ipso write_csv(ipso, "./files/ipso.csv")
Impress
IMPRESS stands for the Independent Monitor for the Press. The company’s aim revolves around helping to build understanding and trust between journalists and the public. Like IPSO, they operate as press regulators, maintaining a Standards Code and assessing any breaches of this code by their members.
<- "https://www.impress.press/regulated-publications/"# url
url
# define the scraping function
<- function(url) {
scraper_impress read_html(url) %>%
html_elements(".span-md-8") %>%
html_elements("a") %>%
html_text()
}
# iterate over the urls
<- as.data.frame(scraper_impress(url)) %>%
impress_members rename("Publication" = "scraper_impress(url)") %>%
mutate(Type = case_when(
str_detect(Publication, "online") ~ "Online",
str_detect(Publication, "print") ~ "Print",
TRUE ~ "Unknown"
))
# now let's get the social media handles and urls
<- read_html(url) %>%
social_media html_elements(".span-md-8") %>%
html_elements("a") %>%
html_attr("href")
<- function(page) {
impress_sm_scraper <- read_html(url(page))
doc list(
<- doc %>% html_element("h1") %>% html_text() %>% str_remove(".*\\| "),
Publication <-
twitter_url %>% html_elements(xpath = "//p/a[contains(@href, 'twitter.com/')]/@href") %>% html_text(),
doc <-
twitter_handle %>% html_elements(xpath = "//p/a[contains(@href, 'twitter.com/')]/text()") %>% html_text(),
doc <-
facebook_handle %>% html_elements(xpath = "//a[contains(@href, 'facebook.com/')]/@href") %>% html_text()
doc
)
}
<- lapply(social_media, impress_sm_scraper)
table
<- map(table, as.data.table)
dt_list <- rbindlist(dt_list, fill = TRUE, idcol = T) %>% group_by(.id) %>% slice_tail(n = 1)
dt
<- impress_members %>% rowid_to_column() %>% full_join(dt, by = c("rowid" = ".id")) %>%
impress select(-V1) %>%
rename("Twitter handle" = V3,
"Twitter url" = V2,
"Facebook url" = V4) %>%
mutate(Publication = gsub("\\s+\\(print.*|\\s+\\(onl.*", "", Publication)
%>%
) group_by(Publication) %>%
summarise(
across(everything(), # apply to all columns
~paste0(unique(na.omit(.x)), collapse = "; "))) %>% # function is defined which combines unique non-NA values
mutate(retrieval_date = Sys.Date())
# append to impress file
#write_csv(impress, "./files/impress.csv") #if running the code for the first time
<- read_csv("./files/impress.csv")
impress_main <- impress_main %>% bind_rows(impress) %>% group_by(Publication) %>% arrange(retrieval_date) %>% slice(1L)
impress write_csv(impress, "./files/impress.csv")
3.2.2 Membership Associations and Circulation Compilers
HoldtheFrontPage
HoldtheFrontPage (HTFP) is a news website focusing on UK regional media, whose audience is journalists and journalism students. They publish articles that highlight battles, efforts, and achievements of local and regional newspapers. HoldtheFrontPage.co.uk Limited is independently-owned and is a member of the Independent Community News Network and the Independent Press Standards Organisation.
HoldtheFrontPage maintains a directory of UK regional and local newspapers, newspaper websites, newspaper publishing companies and the people who work for them. The directory is organised in separate pages, grouping titles by typology: Websites, Daily Newspapers, Weekly Newspapers, and Hyperlocals. There are also Media Companies and Freelancers, which are left out of this analysis as not relevant to my directory.
# generate urls
<- "https://www.holdthefrontpage.co.uk/directory/"
root_url <- c("newspaperwebsites/",
suffices "dailynewspapers/",
"weeklynewspapers/")
<- paste0(root_url,
urls
suffices)
# define scraping function
<- function(page) {
pager <- read_html(url(page))
doc data.frame(
Publication = doc %>% html_elements(".pf-content") %>%
html_elements("li") %>%
html_text(),
Website = doc %>% html_elements(".pf-content") %>%
html_elements("li") %>% html_element("a") %>%
html_attr("href"),
Type = doc %>% html_elements(".entry-title") %>%
html_text()
)
}
<- do.call(rbind, lapply(urls, pager))
htfp 1:76, 3] <- "Online daily"
htfp[77:866, 3] <- "Online weekly"
htfp[
# I am doing hyperlocals separately from the rest as the web structure is different
<-
hyperlocals "https://www.holdthefrontpage.co.uk/directory/hyperlocal-publications/"
<-
hl data.frame(
<-
Publication read_html(hyperlocals) %>% html_elements("td:nth-child(1)") %>%
html_text(),
<-
Website read_html(hyperlocals) %>% html_elements("td:nth-child(2)") %>%
html_element("a") %>% html_attr("href"),
<- "Hyperlocal publications"
Type
)
names(hl) <- c("Publication", "Website", "Type")
<- hl[-c(1:2), ]
hl
# put it all together
<- rbind(htfp, hl) %>%
htfp_database mutate(Notes = as.character(str_extract(Publication, "\\(.*|\\,.*")),
Notes = str_remove_all(Notes, "\\)|\\(|\\, "),
Publication = str_remove_all(Publication, " \\(.*|\\,.*"),
Publication = str_remove(Publication, "\\s$")) %>%
mutate(Type = case_when(Type == "Regional Weekly Newspapers in the UK" ~ "Weekly",
== "Regional Daily Newspapers in the UK" ~ "Daily",
Type == "Online daily" ~ " Online Daily",
Type == "Online weekly" ~ "Online Weekly",
Type TRUE ~ as.character(Type))) %>%
group_by(Publication) %>%
summarise(across(everything(), ~paste0(unique(na.omit(.x)), collapse = "; "))) %>% # combine unique non-NA values
mutate(retrieval_date = Sys.Date())
<- nrow(htfp_database)
HTFP_len <- nrow(htfp) + nrow(hl)
total_titles <- htfp_database %>%
print filter(Type %in% c("Weekly", "Daily")) %>%
nrow()
<- htfp_database %>%
online_only filter(Type %in% c("Online Daily", "Online Weekly")) %>%
nrow()
<- nrow(hl)
hyperlocals <- HTFP_len - print - hyperlocals
online <- online - online_only
online_and_print <- HTFP_len - hyperlocals
non_hyperlocals <- htfp_database %>%
dailies filter(str_detect(Type, 'Daily')) %>%
nrow()
<- htfp_database %>%
weeklies filter(str_detect(Type, 'Weekly')) %>%
nrow()
# append to htfp_database file
#write_csv(htfp_database, "./files/htfp.csv") #if running the code for the first time
<- read_csv("./files/htfp.csv")
htfp_main <- htfp_main %>% bind_rows(htfp) %>% group_by(Publication) %>% arrange(retrieval_date) %>% slice(1L)
htfp write_csv(htfp, "./files/htfp.csv")
On HTFP’s site, there are currently 1347 unique titles listed. Oftentimes, titles have both a print and an online edition. When counting them separately, we find 1749 titles. The table below summarises what the HTFP directory includes, updated to 2023-02-02.
kable(data.frame(c("Online", "Online only", "Online and print", "Print only", "Dailies", "Weeklies", "Unique titles"), c(hyperlocals, "unknown", "unknown", 0, "unknown", "unknown", hyperlocals), c(online, online_only, online_and_print, print, dailies, weeklies, non_hyperlocals)), col.names = c("Variable","Hyperlocals", "Legacy Media"))
Variable | Hyperlocals | Legacy Media |
---|---|---|
Online | 116 | 861 |
Online only | unknown | 436 |
Online and print | unknown | 425 |
Print only | 0 | 370 |
Dailies | unknown | 111 |
Weeklies | unknown | 1127 |
Unique titles | 116 | 1231 |
JICREG
JICREG (Joint industry Currency for Regional Media Research) is the trading currency for local media in Great Britain. It has representation on the board and technical committee from advertisers, agencies and publishers, guaranteeing its objectivity as a trusted and transparent audience measurement system.
Launched in 1990, JICREG provides free and paid subscribers access to its monthly updated data. The platform, JICREG Online, is capable of generating over 100,000 reports. JICREG is used both by media companies and communication brands to help understand audience circulation.
As of 05/10/2022 there are 520 local daily and weekly newsbrands reported within the JICREG database, which include 71 daily titles and 449 weekly titles covering Great Britain (Northern Ireland is excluded). To be included on JICREG the print title must:
have an audited circulation figure, ABC is the preferred auditor although other auditors are accepted if they comply with JICREG audit guidelines;
be published at least weekly and also be a member of the News Media Association and listed within the Local Media Works database.
Automating data extraction from JICREG is no easy task. The page is behind login and most useful features behind a paid subscription which I do not have. Within the free account, there is no option to obtain the full dataset at once and scraping seems tricky because of the embedded Javascript on the page (e.g. URL is not customisable, so I would have to use Selenium and type the name of each location I want to search: so 380 LADs). For this reason, I have done the only available alternative, which is to head over to Historical Records within the JICREG website and download as pdf the latest release of circulation figures (October 2021) for all newspapers in their database. I have converted the pdf to csv.
<- read.csv("./files/JICREG_2020.csv") %>% mutate(Scope = "Local") jicreg
Local Media Works
The Local Media Works Database is a database of newsbrands maintained by Local Media Works, part of the News Media Association (NMA) – a representative group set up by the national, regional and local newspaper industry to promote their interests. This is closely linked – but not identical – to the Joint Industry Committee for Regional Media Research (JICREG) database. The NMA promotes the interests of news media publishers to Government, regulatory authorities, industry bodies and other organisations whose work affects the industry.
The database can be searched by geographic location or newspaper name, although the geographic unit is not specified. One cannot extract the full dataset. Otherwise one can look at a page featuring the list of newsbrands which supposedly should make their database, but in this case circulation information is not featured.
Regarding the database, their website states:
The LMW Database provides the most up-to-date and detailed information available on UK local news brands across the UK including audited circulation breakdowns, web traffic data and other data on local newspapers and their companion websites. Data for each newspaper, website and other platform includes address and contact information, type, frequency, circulation and readership information, both total and by area, advertising rates, format, and publishing day.
The page mentions JICREG (and I suppose the circulation figures they report originate indeed from JICREG). Regarding JICREG, LMW states:
The JICREG database provides detailed newspaper readership and website audience data for local newsbrands in the UK. The gateway to this data is JICREG Online which enables advertisers, marketers and campaigners to target tightly defined audiences. In 2021, JICREG Life is Local was launched by Local Media Works for the JICREG database update.
# It says on the JICREG website that is should match Local Media Works directory. Let's check.
<- "https://newsmediauk.org/local-media-works/a-z-newsbrands/"
url
<- read_html(url)
lmw
<- data.frame(
lmw Publication = lmw %>% html_elements(".az-newsbrand") %>%
html_elements("a") %>%
html_text(),
Website = lmw %>%
html_elements(".az-newsbrand") %>%
html_elements("a") %>%
html_attr("href"),
Scope = "Local",
retrieval_date = Sys.Date()
)
# titles in lmw not in jicreg
<- anti_join(lmw, jicreg, by= "Publication")
l_not_j
# append to lmw file
#write_csv(lmw, "./files/lmw.csv") #if running the code for the first time
<- read_csv("./files/lmw.csv")
lmw_main <- lmw_main %>% bind_rows(lmw) %>% group_by(Publication) %>% arrange(retrieval_date) %>% slice(1L)
lmw write_csv(lmw, "./files/lmw.csv")
ABC
ABC circulation data is unfortunately hard to automatically retrieve. Thus I extracted the data in December 2022 manually. The good thing is that the data is updated yearly or twice a year at regular dates, reducing the need for automation.
<- read_csv("./files/abc.csv") abc
3.2.3 Publishers
Inspired by Moore and Ramsay, I decided the picture would finally be complete if I added an extraction of the titles from each major media company here. Following the list found in Ramsay and Moore, and cross-checking it with Hold the Front Page (more updated) I enlisted these companies to scrape:
1. Reach PLC
2. Newsquest (includes the former Archant)
3. Illiffe - websites and papers
4. Tindle
5. DC Thomson
6. MNA
From the perspective of the main UK regional publishers, one is missing from this parsing effort: National World (before april 2022 called JPIMedia and before 2018 Johnston Press). On the site (and elsewhere online) I cannot find a list of their brands.
<- read_html("https://en.wikipedia.org/wiki/List_of_Reach_plc_titles") %>%
reach html_elements(xpath = "//div[@id='mw-content-text']/div[1]/ul[2]/li") %>%
html_text() %>%
as.data.frame() %>%
rename(Publication = 1) %>%
mutate(retrieval_date = Sys.Date())
# append to file
#write_csv(reach, "./files/reach.csv") #if running the code for the first time
<- read_csv("./files/reach.csv")
reach_main <- reach_main %>% bind_rows(reach) %>% group_by(Publication) %>% arrange(retrieval_date) %>% slice(1L)
reach write_csv(reach, "./files/reach.csv")
<- read_html("https://www.newsquest.co.uk/news-brands") %>%
newsquest html_elements(xpath = '//div[@class="modal-scroll grid"]//p[@class="divider"][1]') %>% html_text() %>% trimws() %>% str_split_fixed("\\s+\\s+", 5) %>% as.data.frame() %>% mutate(retrieval_date = Sys.Date())
$V5 <- str_remove(newsquest$V5, "London\\s+\\s+")
newsquestcolnames(newsquest) <- c("Publication", "Address1", "Address2", "County", "Postcode", "retrieval_date")
# save
#write_csv(newsquest, "./files/newsquest.csv") #if running the code for the first time
<- read_csv("./files/newsquest.csv")
newsquest_main <- newsquest_main %>% bind_rows(newsquest) %>% group_by(Publication) %>% arrange(retrieval_date) %>% slice(1L)
newsquest write_csv(newsquest, "./files/newsquest.csv")
<- c("https://www.iliffemedia.co.uk/portfolio/online/", "https://www.iliffemedia.co.uk/portfolio/publications/")
url
<- function(x) {
scraper read_html(x) %>%
html_elements(xpath = "//option[@value]") %>% html_text() %>% trimws()
}
<- lapply(url, scraper)
illiffe <- map(illiffe, as.data.frame)
illiffe <- rbindlist(illiffe) %>% filter(!row_number() %in% 1)
illiffe colnames(illiffe) <- "Publication"
$retrieval_date <- Sys.Date()
illiffe
# save
write_csv(illiffe, "./files/illiffe.csv") #if running the code for the first time
<- read_csv("./files/illiffe.csv")
illiffe_main <- illiffe_main %>% bind_rows(illiffe) %>% group_by(Publication) %>% arrange(retrieval_date) %>% slice(1L)
illiffe write_csv(illiffe, "./files/illiffe.csv")
<- read_html("https://tindlenews.co.uk/portfolio") %>%
tindle html_elements("#newsitems") %>%
html_elements("a") %>%
html_attr("href")
<- function (x) {
scraper <- read_html(x)
doc data.frame(
<- doc %>% html_elements(".wrap .row") %>% html_element("h1") %>% html_text() %>% na.omit(),
Publication <- doc %>% html_elements(".m-0") %>% html_element("p") %>% html_text() %>% extract2(1)
Address
)
}
<- rbindlist(lapply(tindle, scraper))
tindle colnames(tindle) <- c("Publication", "Address")
$retrieval_date <- Sys.Date()
tindle
# save
#write_csv(tindle, "./files/tindle.csv") #if running the code for the first time
<- read_csv("./files/tindle.csv")
tindle_main <- tindle_main %>% bind_rows(tindle) %>% group_by(Publication) %>% arrange(retrieval_date) %>% slice(1L)
tindle write_csv(tindle, "./files/tindle.csv")
#DC Thomson brands list feels quite irrelevant. Most are magazines or trade publications. The news publications include Press and Journal, Evening Express, and The Sunday Post, which arguably are too large scale to be considered local.
<- read_html("https://www.dcthomson.co.uk/brands/") %>%
dc html_elements(".overflow .block") %>%
html_attr("href")
<- function (x) {
scraper <- read_html(x)
doc list(
<- doc %>% html_elements("h1") %>% html_text(),
Publication <- doc %>% html_elements(".twitter-social-link") %>% html_attr("href"),
Twitter <- doc %>% html_elements(".address") %>% html_text()
Address
)
}
<- rbindlist(lapply(dc, scraper)) %>% na.omit()
dc1 colnames(dc1) <- c("Publication", "Twitter", "Address")
$retrieval_date <- Sys.Date()
dc1
# save
#write_csv(dc1, "./files/dc_thomson.csv") #if running the code for the first time
<- read_csv("./files/dc_thomson.csv")
dc_thomson_main <- dc_thomson_main %>% bind_rows(dc1) %>% group_by(Publication) %>% arrange(retrieval_date) %>% slice(1L)
dc_thomson write_csv(dc_thomson, "./files/dc_thomson.csv")
<- read_html("https://www.mnamedia.co.uk/news-titles/") %>% html_elements(".full_size h3") %>% html_text() %>% as.data.frame() %>% rename("Publication" = 1) %>% filter(!str_detect(Publication, "WEEKLY|DAILY|MONTHLY"))
mna2
<-
mna read_html("https://www.mnamedia.co.uk/news-titles/") %>% html_elements(xpath = "//body/div/div/div/main/div/div/div/div/div/div/div/div/div/div/div/div/div/div/div[1]/div[1]/div[1]/div[1]/div[1]/div[1]/div[1]/div[1]/div[1]/div[1]/div[1]/div[1]/div[1]/div[1]/div[1]/div") %>% html_text() %>% trimws() %>% as.data.frame() %>% filter(!row_number() %in% c(1, 2)) %>% rename("Description" = 1) %>% mutate(Frequency = substr(Description, 1, 1)) %>% mutate(
Description = str_remove(Description, "WEEKLY|DAILY|MONTHLY"),
Description = str_replace_all(Description,
pattern = "([[:upper:]])",
replacement = " \\1"),
Description = str_replace_all(Description, " ", " ")
%>% fuzzy_left_join(mna2,
) by = c("Description" = "Publication"),
match_fun = str_detect) %>%
mutate(retrieval_date = Sys.Date())
# save
#write_csv(mna, "./files/mna.csv") #if running the code for the first time
<- read_csv("./files/mna.csv")
mna_main <- mna_main %>% bind_rows(mna) %>% group_by(Publication) %>% arrange(retrieval_date) %>% slice(1L)
mna write_csv(mna, "./files/mna.csv")
3.2.4 Hyperlocals, Ramsay & Moore, Media Coalition Reform
Community journalism hyperlocals The Independent Community News Network (ICNN) is a network of hyperlocal that exists to support independent titles and publishers and to promote quality journalism, in their words, to help address the democratic deficit in news poor communities and help create more jobs at the local level.
From their website: “Of these 100+ members, nearly half have either complimentary print newspapers, or publish a weekly, fortnightly, or monthly newspaper exclusively. Membership is most concentrated in London, Bristol, Cardiff, and Manchester. The 15+ titles that cover the city of Bristol and the surrounding area are exclusively print publications. However, many of these belong to the same publisher. The south coast appears to be fertile ground for independent community news, as does the east midlands. Anglia, including Cambridge, Bedfordshire, and Hertfordshire are vastly under-represented in terms of represented independent news coverage. This is compounded by the lack of legacy media in these areas too. The same can be said for Yorkshire and the North East. This is not to say that there is no coverage in these areas at all. This map indexes titles represented by ICNN only. Our most recent members, as of September 2020, are North Edinburgh Community News (affectionately called The Ems), Altrincham Today, and the Mansfield, Ashfield and Warsop News Journal.”
<-
patterns_cj c("(?<=.com).*", "(?<=.co.uk).*", "(?<=.org).*") # cleaning urls for matching
<- c("PointZ\\s\\(", "\\s\\d\\)")
remove
# dataset I extracted from interactive map (contains old members that were not shown in map but included in the javascrip map file)
<- read_csv("./files/ICNN.csv") %>%
cj mutate(
Coordinates = str_remove_all(Coordinates, paste(remove, collapse = "|")),
Website = str_remove(Website, pattern = paste(patterns_cj, collapse = "|"))
%>% na.omit()
)
# dataset I extrscted from member page (does not contain geographic info, but the list seems more updated, thus I will merge this with the above)
# sometimes matches are good at the Publication level, sometimes at the Website level. To maximise both, I am making two joins and then coalescing on the two resulting dfs
<-
cj2 read_delim("./files/icnn_members_feb23.csv",
col_names = FALSE,
delim = ";") %>%
na.omit() %>% left_join(cj,
by = c("X1" = "Publication")) # matching on Publication
<-
patterns_cj2 c("(?<=.com).*",
"(?<=.co.uk).*",
"(?<=.org).*",
"(?:.(?!\\/))+$") # cleaning urls for matching
<-
cj3 read_delim("./files/icnn_members_feb23.csv",
col_names = FALSE,
delim = ";") %>%
na.omit() %>% mutate(X2 = str_remove(X2, pattern = paste(patterns_cj2, collapse = "|"))) %>% left_join(cj,
by = c("X2" = "Website")) # matching on Website
# coalescing the 2: the resulting cleaned dataframe. There are a few observations missing coordinates, as they were on the members page but not the other
<- inner_join(cj2, cj3, by = "X1") %>%
cj4 mutate(
Coordinates = coalesce(Coordinates.x, Coordinates.y),
Website = coalesce(X2.y, Website),
Publication = coalesce(X1, Publication)
%>%
) select(Publication, Website, Coordinates) %>%
distinct(Publication, .keep_all = TRUE)
# there is another (seemed more powerful) way I could have done this, using sqldbf, but I was not familiar with the sql syntax so I went all dplyr instead
Ramsay and Moore
I reached out to Dr Gordon Ramsay (author of many of the consulting work, as well as Monopolising the News), in relation to accessing the data behind local news mapping. He supplied me with his dataset of the time. Although much of this info is outdated, I can extract some useful info for those outlets still in existence (e.g., geographical location).
<- read_csv("./files/Ramsay.csv") ramsay
Media Coalition Reform 2021
Brilliant dataset published online and created for the Who Owns the UK Media 2021 report. However, the methodology followed to put it together is unknown.
<- read_csv("./files/MRC_2021.csv") %>%
mrc2021 filter(!`2021 owner` %in% c("CLOSED", "CLOSED/INCORPORATED")) %>% # filter out closures
rename("Publication" = Title)
3.2.5 Merging
# keep only each unique dataset
rm(list=ls()[! ls() %in% c("ipso","htfp_database", "impress", "jicreg", "lmw", "abc", "reach", "newsquest", "illiffe", "tindle", "dc1", "mna", "media_info", "cj4", "mrc2021", "ramsay")])
<- function(x) {
merger <- x %>% select(Publication)
x
}
<- Filter(function(x) is(x, "data.frame"), mget(ls()))
dfs <- lapply(dfs, merger)
dfs <- bind_rows(dfs, .id = "id")
merged saveRDS(merged, "merged.rds") # saving the 7899 row object
First operation is cleaning duplicates that exist across datasets. My total list at present includes 7899 titles, which surely include many duplicates. In fact, considered I am merging 15 files, if a title appeared in all 15 datasets I would be counting it 15 times.
# keep only each unique dataset
rm(list=ls()[! ls() %in% c("ipso","htfp_database", "impress", "jicreg", "lmw", "abc", "reach", "newsquest", "illiffe", "tindle", "dc1", "mna", "media_info", "cj4", "mrc2021", "ramsay")])
<- Filter(function(x) is(x, "data.frame"), mget(ls()))
dfs
# make directory!
<- bind_rows(dfs, .id = "id") %>%
directory group_by(Publication) %>%
summarise(across(everything(), ~paste0(unique(na.omit(.x)), collapse = "; ")))
saveRDS(directory, "directory.RDS")
<- readRDS("directory.RDS") %>% mutate_all(~na_if(., ""))
directory <- nrow(directory) count
My directory has, upon a full merge of my sources, 4419 titles. Surely most of them are undetected duplicates and many irrelevant publications (e.g. national titles), especially from directories such as ipso.
3.3 The directory
To be able to work with a smaller and cleaner dataset, I decided to not invest any further time in trying to clean my above huge dataset, and instead look at what I can do with the data at hand to have something that is better than anything already out there (e.g., more updated, more comprehensive).
Out of all my data sources, two impressed me: the Media Reform Coalition dataset and Ramsay’s old directory. The former for its “recentness” and the latter for its methodological robustness. Following this logic, I can rely on these two datasets solely, while the remainder datasets can be used for simply extracting further variables, or implementing gaps.
A quality statement of the consistency given by the two datasets (which surely is partly influenced by the fact that MRC’s mission had started with Ramsay - indicating consistency in spelling of titles and approach), can be seen in the figure below. Around 900 titles appear in both Ramsay’s and MRC’s datasets, which considered they have respectively 1106 and 1021 titles, makes these two datasets an ideal starting point for identifying titles in existence without too much data wrangling, and geographically mapping titles (since both present geographic data, at the postcode/city and LAD level respectively).
<- readRDS("merged.rds")
merged
<- merged %>%
pairs group_by(Publication) %>%
reframe(pair = c(outer(sort(id),
sort(id), paste, sep = '-'))) %>%
%>%
ungroup count(pair, sort = TRUE, name = 'number_of_events')
<- pairs %>%
p separate(pair, "-", into = c("A", "B")) %>%
filter(A != B) %>%
unite("pair", A:B, sep = "-") %>%
distinct(number_of_events, .keep_all = TRUE) %>%
ggplot(aes(x = number_of_events, y = 0)) +
geom_jitter(
height = 0.25,
size = 4.5,
col = "lightslateblue",
fill = "lightslateblue",
alpha = 0.5
+
) scale_y_continuous(limits = c(-0.2, 0.2), breaks = c(0, 0)) +
xlab("co-occurrences") +
ylab("") +
theme_minimal() +
theme(axis.text.y = element_blank(),
axis.line.y = element_blank(),
panel.grid.major = element_blank(),
panel.grid.minor = element_blank()) +
annotate(
geom = "text",
x = 800,
y = 0.05,
label = "Ramsay-MRC21",
hjust = "left"
)
<- pairs %>%
p1 separate(pair, "-", into = c("A", "B")) %>%
filter(A != B) %>%
unite("pair", A:B, sep = "-") %>%
distinct(number_of_events, .keep_all = TRUE) %>%
ggplot(aes(x=reorder(pair,-number_of_events), y = number_of_events)) +
geom_bar(stat = "identity", fill = "lightslateblue")+
coord_flip() +
xlab("pair") +
ylab("titles co-occurrences")+
theme_minimal()+
theme(panel.grid.minor.y = element_blank(),
panel.grid.major = element_blank(),
axis.text.y = element_text(size = 6.8))
<- fs::path(knitr::fig_path(), "cooccurrences.jpeg")
jpgfile ggsave(jpgfile, p1, width = 40, height = 30, units = "cm", scaling = 3)
::include_graphics(jpgfile) knitr
There are around 150 titles that do not appear in MRC but appear in Ramsay. I checked a few of those, and they were papers which shut down since Ramsay’s research. I can thus make the assumption that this applies to all of these titles, and reduce my main source to 1. Thus, if I used solely the MRC dataset as a starting point, what improvements could I make?
- check whether all 2021 closures and openings recorded by PressGazette have been included
- remove 2022 closures and insert 2022 launches as recorded by PressGazette
- Archant and JPIMedia have since changed names (update this)
- add hyperlocals by ICNN
By filtering out MRC and ICNN from the merged dataset created above, I will be able to retain additional information about MRC and ICNN titles where these titles were present in any of the other datasets.
3.3.1 Number of titles
I can improve the MRC dataset in terms of adding hyperlocals by ICNN and by updating it with closures and launches from PressGazette. Furthermore, I can expand information on social media handles for these outlets through extracting this information from a website called media.info.
<- c("mrc","cj4")
pattern <- directory %>%
directory_final filter(grepl(paste0(pattern, collapse = "|"), id)) %>%
mutate(Scope = if_else(Scope == "", "Local", Scope))
Using PressGazette’s yearly article on print and digital newspaper launches and closures in the UK, I made sure the new and old titles would be respectively included or excluded in my directory. In total, there were 57 changes in 2021, including 10 closures and 47 launches, whereas in 2022 (up until July) there have been 10 closures and 8 launches.
<- read_csv("./files/PG_launches_closures_2020_2022.csv")
pg <- anti_join(directory_final, pg[pg$Event == "Closure",], by = "Publication") # removal of closures
directory_final <- full_join(directory_final, pg[pg$Event == "Launch",], by = c("Publication", "Publisher", "Frequency")) %>% select(-Event) %>% rename("Launch" = Date)
directory_final
<- nrow(directory_final) directory_size
Removing closures from my MRC + hyperlocals dataset and adding launches leads me to identifying 1179 local news titles in the UK - and I think, for now, that is the best I can do.
So what other conclusions can I draw from this directory building?
Different sources, different sizes. Extracting directly from the publishers leads me to smaller estimates compared to relying on my directory (with the exception of DC Thomson, whose site features several national magazines and trade papers). Why this is the case is not sure. It could be that titles appearing both online and in print are presented once on brands sites, and twice in my directory. Or it could be a lack of updating or showcasing on the brands sites.
I investigated Tindle, which is relatively small as a publisher and has a big disparity. What I noticed was that Tindle’s sites presents 24 titles, whereas my directory has 74. Of these 74 titles, many are contained and recognised as owned by Tindle by many other data sources beyond MRC, such as HTFP, ipso, Local Media Works, abc. However, there was a correlation between Tindle’s titles being branded on Tindle’s page, and that title featuring on the ABC website. This makes me think that what is missing from Tindle’s site are relatively small titles.
<- directory_final %>%
directory_publishers mutate(`2021 owner` = case_match(`2021 owner`,
"Archant"~"Newsquest PLC",
"JPI Media"~"National World",
"Reach Plc"~"Reach PLC",
.default = `2021 owner`),
`2021 owner` = if_else(is.na(`2021 owner`),Publisher,`2021 owner`)) %>%
count(`2021 owner`)
<- readRDS("merged.rds")
merged <- merged %>%
merged_publishers group_by(id) %>%
%>% filter(id %in% c("dc1","illiffe","mna","newsquest","reach","tindle")) %>%
count mutate(id = recode(id, dc1 = "DC Thomson & Company Limited", illiffe = "Iliffe Media", mna = "Midland News Association",
newsquest = "Newsquest PLC", reach = "Reach PLC", tindle = "Tindle Newspapers Ltd"))
<- full_join(merged_publishers, directory_publishers, by = c("id" = "2021 owner")) %>%
publishers rename("Brands member lists" = n.x) %>%
rename("MRC" = n.y) %>%
na.omit() %>%
pivot_longer(c(`Brands member lists`, MRC), names_to = "Source") %>%
ggplot(aes(x = reorder(id,-value), y = value, color = Source)) +
geom_point(alpha = 0.6, size = 4)+
xlab("")+
ylab("number of titles")+
coord_flip()+
theme_minimal()
<- fs::path(knitr::fig_path(), "publishers.jpeg")
jpgfile ggsave(jpgfile, publishers, width = 45, height = 12, units = "cm", scaling = 3)
::include_graphics(jpgfile) knitr
3.3.2 Ownership
Relying on data by Media Coalition Reform means having data that is built to understand ownership concentration. My directory takes the latest MRC dataset (2021) and applies relevant adjustments that reflect market acquisitions, closures and launches, rebranding efforts.
<- directory_final %>%
directory_final mutate(publisher_recoded = if_else(is.na(`2021 owner`),Publisher,`2021 owner`),
publisher_recoded = case_match(`2021 owner`,
"Archant"~"Newsquest PLC",
"JPI Media"~"National World",
"Reach Plc"~"Reach PLC",
"Reach"~"Reach PLC",
"Archant (now Newsquest)"~"Newsquest PLC",
.default = `2021 owner`)) %>%
select(-Publisher) %>%
relocate(Publication, publisher_recoded, id)
<- directory_final %>%
ownership group_by(publisher_recoded) %>%
count(sort = TRUE) %>%
filter(n > 5 & !is.na(publisher_recoded)) %>%
ggplot(aes(x = reorder(publisher_recoded, n), y = n)) +
geom_bar(stat = "identity", fill = "lightslateblue")+
coord_flip()+
theme_minimal()+
ylab("Number of titles") +
xlab("") +
scale_y_continuous(breaks = seq(0,450,25))+
theme(panel.grid.minor = element_blank(),
panel.grid.major.y = element_blank(),
axis.text.y = element_text(size = 10))+
labs(caption = "Filtered by publishers with a minimum of 5 titles.\nNA here indicates independent publishers from the hyperlocal dataset")
<- fs::path(knitr::fig_path(), "ownership.jpeg")
jpgfile ggsave(jpgfile, ownership, width = 60, height = 20, units = "cm", scaling = 3)
::include_graphics(jpgfile) knitr
<- directory_final %>%
table_ownership group_by(publisher_recoded) %>%
count(sort = TRUE) %>%
ungroup() %>%
mutate(perc = round(n/sum(n, na.rm = TRUE)*100,2)) %>%
filter(n > 10)
kable(table_ownership, col.names = c("Owner", "Count", "%"))
Owner | Count | % |
---|---|---|
Newsquest PLC | 305 | 25.87 |
Reach PLC | 211 | 17.90 |
National World | 178 | 15.10 |
NA | 169 | 14.33 |
Tindle Newspapers Ltd | 74 | 6.28 |
Iliffe Media | 71 | 6.02 |
Midland News Association | 17 | 1.44 |
Bullivant Media Ltd | 15 | 1.27 |
Independent | 14 | 1.19 |
Alpha Newspaper Group | 13 | 1.10 |
3.3.3 Frequency
The vast majority of titles appear to be weeklies. There are only 80 known dailies, against 869 weeklies. Knowing there are several hundres LADs in the UK, this resonates with previous findings that showed that many LADs lack a daily title.
<- directory_final %>%
frequency select(Publication, publisher_recoded, Frequency) %>%
mutate(Frequency = recode(na_if(Frequency, ""), .missing = "unknown")) %>%
mutate(Frequency = str_to_lower(Frequency)) %>%
count(Frequency, sort = TRUE) %>%
ggplot(aes(x = reorder(Frequency,n), y = n)) +
geom_bar(stat="identity", fill = "lightslateblue")+
xlab("Frequency")+
ylab("Number of titles")+
scale_y_continuous(breaks = seq(0,900, 50))+
coord_flip()+
theme_minimal()+
theme(panel.grid.minor = element_blank(),
panel.grid.major.y = element_blank(),
axis.text.y = element_text(size = 10))
<- fs::path(knitr::fig_path(), "frequency.jpeg")
jpgfile ggsave(jpgfile, frequency, width = 60, height = 14, units = "cm", scaling = 3)
::include_graphics(jpgfile) knitr
3.3.4 Hyperlocals
How many are hyperlocals or independent publishers?
<- nrow(filter(directory_final, str_detect(id, "cj4") | publisher_recoded == "Independent")) h
The ICNN dataset includes solely hyperlocals. Instead, PressGazette includes independent publishers, and in one instance a publisher is included as independent in PG and features also in the ICNN site (probably because lines are blurry in terms of definitions, and while all hyperlocals are probably independent, not all independent publishers are hyperlocals. Between the two of them, I find 116 titles.
3.3.5 Twitter
While I am at it, I also extract Twitter information from a website called Media.info, which gathers data on media companies in the UK, including newspapers of all sizes and scopes, as well as magazine and TV. In this section I gather data from their newspaper directory to extract Twitter handles.
# TWITTER
<- c(".*\\d\\.\\s+", "\\s+-.*", "\\s+\\(.*")
remove <- read_html("https://media.info/uk/newspapers/data/twitter/titles") %>%
mi_twitter html_elements(".data") %>% html_table() %>% as.data.frame() %>%
mutate(Twitter = str_extract(X1, "@.*")) %>% mutate(Publication = str_remove_all(X1, paste(remove, collapse = "|"))) %>%
select(-c(X1,X2)) %>% na.omit()
<- directory_final %>% fuzzy_left_join(mi_twitter, by = "Publication", match_fun = str_detect) %>%
directory_final mutate(Twitter = Twitter.y) %>%
select(-c(Twitter.x, Twitter.y))
saveRDS(directory_final, "directory_with_hyperlocals.RDS")