可视化 Expatistan 网站上的各国生活成本指数数据

可视化 Expatistan 网站上的各国生活成本指数数据

本周的小项目作业是“爬取 Expatistan 网站上的各国生活成本数据并绘制一幅世界地图进行展示”。

  1. 数据源:Expatistan

  2. 世界地图的底图数据:world.geo.json 文件。

  3. 爬取数据的 R 包,可以用 rvest

Tips: 可能需要到的函数:read_html,html_nodes,html_table;

  1. 绘制地图的 R 包,ggplot + sf (本周有教程),用 tmap 也行。

  2. 拓展作业:可以再绘制一些其他的图来展示各国生活成本的排名。


参考结果

爬取数据

这种表格数据用 rvest 包爬取非常容易:

1
2
3
4
5
6
7
library(tidyverse)
library(hrbrthemes)
library(rvest)
# 把网址保存成一个名为 url 的变量:
url <- "https://www.expatistan.com/cost-of-living/country/ranking"
# 使用 read_html() 函数读取解析网页文件,保存为名为 html 的变量:
html <- read_html(url)

解析得到的 html 是个 xml_document,这是一种结构性的数据,我们可以使用 html_nodes() 函数从中找寻某个节点,通常找寻的办法有两个:CSS 和 XPath,都可以用,首先我们用 xpath:

1
2
3
4
5
html %>% 
html_nodes(xpath = '//*[@id="content"]/div/div[1]/div[1]/table')

## {xml_nodeset (1)}
## [1] <table class="country-ranking centered">\n<thead><tr>\n<th>Ranking</th>\n ...

table 标签对应的就是我们想要爬取数据的这个表格。那么这个 xpath 从哪来的呢?

或者我们可以用 CSS 选择:

1
2
3
4
5
html %>% 
html_nodes(css = '#content > div > div.block.first.comparison > div.prices > table')

## {xml_nodeset (1)}
## [1] <table class="country-ranking centered">\n<thead><tr>\n<th>Ranking</th>\n ...

CSS 选择器是这么来的

两种方式的效果是一样的,至于选择哪种就看你的偏好了。

得到了 table 所在的节点之后呢,我们可以使用 html_table() 函数解析表格,解析之后再转化为 tibble 数据框并赋值给 df 变量:

1
2
3
4
5
html %>% 
html_nodes(xpath = '//*[@id="content"]/div/div[1]/div[1]/table') %>%
html_table() %>%
.[[1]] %>%
as_tibble() -> df

完整的表格是这样的:

1
2
df %>% 
DT::datatable()

我们再把这个数据整理一下,例如 Ranking 变量可以转换成数值型变量, Price Index * 的名字也改改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
library(stringr)
df <- df %>%
`colnames<-`(c("ranking", "country", "price_index")) %>%
mutate(ranking = str_remove_all(ranking, "[st nd rd th]")) %>%
type_convert()

df

## # A tibble: 90 x 3
## ranking country price_index
## <dbl> <chr> <int>
## 1 1 Bermuda 334
## 2 2 Cayman Islands 326
## 3 3 Switzerland 269
## 4 4 Hong Kong 262
## 5 5 Bahamas 246
## 6 6 Ireland 224
## 7 7 Iceland 218
## 8 8 Singapore 215
## 9 9 Denmark 213
## 10 10 Norway 206
## # … with 80 more rows

先画个简单的柱状图吧!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
library(forcats)
df %>%
slice(1:10) %>%
mutate(
country = fct_reorder(country, price_index)
) %>%
ggplot() +
geom_col(aes(x = country,
y = price_index,
fill = country)) +
theme(legend.position = "none") +
scale_fill_brewer(palette = "Paired") +
coord_flip() +
labs(y = "Price Index",
x = "",
title = "Cost of Living Ranking: Top 10",
subtitle = "Czech Republic = 100",
caption = "Data Source: Expatistan\nhttps://www.expatistan.com/cost-of-living/country/ranking") +
theme(plot.margin = grid::unit(c(1, 0.5, 0.5, 0.2), "cm"))

这是个世界地图的数据,最好的可视化当然是画世界地图了!

我们使用 ggplot2 + sf 绘制世界地图,由于地图数据和 df 里面的美国名称不一致,所以我把地图数据里面的 United States of America 换成了 United States。这里使用了 left_join 进行数据框的匹配,匹配规则是“把 World 数据集中的 name 变量和 df 中的 country 变量一一对应”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
library(ggplot2)
library(sf)
World <- read_sf('world.geo.json') %>%
mutate(name = if_else(name == "United States of America", "United States", name))
World

## Simple feature collection with 177 features and 2 fields
## geometry type: MULTIPOLYGON
## dimension: XY
## bbox: xmin: -180 ymin: -55.61183 xmax: 180 ymax: 83.64513
## epsg (SRID): 4326
## proj4string: +proj=longlat +datum=WGS84 +no_defs
## # A tibble: 177 x 3
## id name geometry
## * <chr> <chr> <MULTIPOLYGON [°]>
## 1 AFG Afghanistan (((61.21082 35.65007, 62.23065 35.27066, 62.98466…
## 2 AGO Angola (((16.32653 -5.87747, 16.57318 -6.622645, 16.8601…
## 3 ALB Albania (((20.59025 41.8554, 20.46317 41.51509, 20.60518 …
## 4 ARE United Arab Emirates (((51.57952 24.2455, 51.75744 24.29407, 51.79439 …
## 5 ARG Argentina (((-65.5 -55.2, -66.45 -55.25, -66.95992 -54.8968…
## 6 ARM Armenia (((43.58275 41.09214, 44.97248 41.24813, 45.1795 …
## 7 ATF French Southern and… (((68.935 -48.625, 69.58 -48.94, 70.525 -49.065, …
## 8 AUS Australia (((145.398 -40.79255, 146.3641 -41.1377, 146.9086…
## 9 AUT Austria (((16.97967 48.1235, 16.90375 47.71487, 16.34058 …
## 10 AZE Azerbaijan (((45.00199 39.74, 45.29814 39.47175, 45.73998 39…
## # … with 167 more rows
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
wdf <- World %>% 
left_join(df, by = c("name" = "country")) %>%
rename(`Price Index` = `price_index`)
ggplot(wdf) +
geom_sf(aes(fill = `Price Index`),
color = "white", size = 0.05) +
theme_modern_rc(base_family = enfont,
plot_title_family = enfont,
subtitle_family = enfont,
caption_family = enfont) +
scale_fill_viridis_c() +
theme(plot.margin = grid::unit(c(1, 0.2, 0.3, 0.2), "cm")) +
labs(y = "",
x = "",
title = "Cost of Living Ranking by Country",
subtitle = "Czech Republic = 100",
caption = "Data Source: Expatistan\nhttps://www.expatistan.com/cost-of-living/country/ranking")

离散变量

价格指数是个连续变量,但是我们可以把它切割成分组的离散变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 使用分位数进行切割,例如我们想分成 6 组
nclass = 6

# 计算分位数
quantiles <- wdf %>%
pull(`Price Index`) %>%
quantile(probs = seq(0, 1,
length.out = nclass + 1),
na.rm = TRUE) %>%
as.vector()

labels <- imap_chr(quantiles, function(., idx){
return(paste0(quantiles[idx], " – ",
quantiles[idx + 1]))
})
# 删除最后一个标签,要不然我们就会看到像 "62 - NA" 这样的标签:
labels <- labels[1:length(labels) - 1]
labels

## [1] "66 – 86" "86 – 95" "95 – 116" "116 – 151" "151 – 189" "189 – 334"
1
2
3
4
5
6
7
8
9
10
11
12
13
wdf <- 
wdf %>%
mutate(
`Price Index` = cut(`Price Index`,
breaks = quantiles,
labels = labels,
include.lowest = TRUE)
)

unique(wdf$`Price Index`)

## [1] <NA> 86 – 95 189 – 334 151 – 189 66 – 86 95 – 116 116 – 151
## Levels: 66 – 86 86 – 95 95 – 116 116 – 151 151 – 189 189 – 334

绘制地图:

1
2
3
4
5
6
7
8
9
10
11
12
13
ggplot(wdf) + 
geom_sf(aes(fill = `Price Index`),
color = "white", size = 0.05) +
theme_modern_rc(base_family = enfont,
plot_title_family = enfont,
subtitle_family = enfont,
caption_family = enfont) +
scale_fill_manual(values = c("#2A363B", "#019875", "#99B898", "#FECEA8", "#FF847C", "#E84A5F", "#C0392B", "#96281B")) +
theme(plot.margin = grid::unit(c(1, 0.2, 0.3, 0.2), "cm")) +
labs(y = "", x = "",
title = "Cost of Living Ranking by Country",
subtitle = "Czech Republic = 100",
caption = "Data Source: Expatistan\nhttps://www.expatistan.com/cost-of-living/country/ranking")

爬取城市生活成本指数的排名

这个表在这里:https://www.expatistan.com/cost-of-living/index

爬取方法类似:

1
2
3
4
5
6
7
8
"https://www.expatistan.com/cost-of-living/index" %>% 
read_html() %>%
html_nodes(xpath = '//*[@id="ranking"]/div[1]/table') %>%
html_table() %>%
.[[1]] %>%
`colnames<-`(c("Ranking", "City", "Price Index")) %>%
as_tibble() %>%
DT::datatable()

编写 Shiny 文档

Shiny 文档和 R Markdown 文档不同的地方在于,它是实时运行的,我做了个 Shiny 文档:https://czxa.top/shiny/cost/ ,实时运行意味着每次你打开它的时候里面的代码就会自动运行一遍,所以这个文档上的表格和图表和原始网站上的始终是一致的。该 Shiny 应用的源码可以从这里下载:https://t.zsxq.com/Q3rzNjY

知识星球附件链接:https://t.zsxq.com/MRNjQJ6

#

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×