Tidy Tuesday 웹사이트에서 상기 GDPR 원문을 데이터프레임으로 가져오고 GDPR 원문에 담긴 감정을 분석해보자.
library(tidyverse)
library(tidytext)
gdpr_text <- readr::read_tsv('https://raw.githubusercontent.com/rfordatascience/tidytuesday/master/data/2020/2020-04-21/gdpr_text.tsv')
gdpr_text %>%
DT::datatable()
감정분석을 위해서 wordcloud
팩키지 comparison.cloud()
를 사용해서 긍적어와 부정어를 시각화해본다.
gdpr_text %>%
unnest_tokens(word, gdpr_text) %>%
inner_join(get_sentiments("bing")) %>%
count(word, sentiment, sort = TRUE) %>%
reshape2::acast(word ~ sentiment, value.var = "n", fill = 0) %>%
wordcloud::comparison.cloud(colors = c("#F8766D", "#7CAE00"),
max.words = 100, match.colors = TRUE)
KISA GDPR 대응지원 센터 웹사이트에 가면 자료실: GDPR 조문을 국문과 영문으로 번역한 PDF 파일이 존재한다. 또한, General Data Protection Regulation (GDPR) 웹사이트에서 조문(Article)별로 검색이 편리하게 정리되어 있다.
GDPR 벌금 데이터셋은 TidyTuesday, “rfordatascience/tidytuesday” GitHub 저장소에 2020-04-21 기준 저장되어 있다. 추가로 최신 데이터가 필요한 경우 Roel’s R-tefacts (April 8, 2020), “Scraping Gdpr Fines - Into the DOM with a flavour of regex” 블로그를 참조하여 데이터를 크롤링한다.
GDPR Enforcement Tracker 웹사이트에도 GDPR 위반 관련 데이터가 잘 정리되어 있다.
library(rvest)
link<- "https://www.privacyaffairs.com/gdpr-fines/"
page <- read_html(link)
temp <- page %>% html_nodes("script") %>% .[9] %>%
rvest::html_text()
ends <- str_locate_all(temp, "\\]")
starts <- str_locate_all(temp, "\\[")
table1 <- temp %>% stringi::stri_sub(from = starts[[1]][1,2], to = ends[[1]][1,1]) %>%
str_remove_all("\n") %>%
str_remove_all("\r") %>%
jsonlite::fromJSON()
gdpr_dat <- temp %>% stringi::stri_sub(from = starts[[1]][2,2], to = ends[[1]][2,1]) %>%
str_remove_all("\n") %>%
str_remove_all("\r") %>%
jsonlite::fromJSON() %>%
as_tibble() %>%
janitor::clean_names()
gdpr_dat %>%
write_csv("data/gdpr_fines.csv")
gdpr_dat %>%
arrange(desc(price)) %>%
select(-id, -picture, -summary) %>%
mutate(source = paste0("<a href='", source, "'>", source, "</a>")) %>%
DT::datatable(escape = FALSE)
크롤링된 데이터에 일부 문제가 있어 이를 바로잡는다.
날짜가 1970-01-01 등으로 결측값이 있거나 Article 관련해서 체계적으로 데이터가 정비되어 있지 않아 이를 바로 후속 분석이 가능한 형태로 가공한다.
library(lubridate)
gdpr_df <- gdpr_dat %>%
mutate(date = as_date(date, format = "%m/%d/%Y", tz = "UTC")) %>%
mutate(wrong_date = date == as_date("1970-01-01")) %>%
mutate(date = if_else(
grepl("25.maj_2018", source) &
wrong_date,
as_date("2018-05-25"),
date
)) %>%
mutate(date = if_else(
grepl("2019/03/29", source) &
wrong_date,
as_date("2019-03-29"),
date
)) %>%
mutate(date = if_else(
grepl(
"https://theword.iuslaboris.com/hrlaw/insights/spain-video-surveillance-and-data-protection-in-the-workplace",
source
) & wrong_date,
as_date("2019-09-20"),
date
)) %>%
mutate(date = if_else(
grepl(
"https://www.etrend.sk/ekonomika/gdpr-zacina-hryzt-telekomunikacny-operator-dostal-pokutu-40-tisic-eur.html",
source
) & wrong_date,
as_date("2019-09-27"),
date
)) %>%
filter(date > as_date("1970-01-01"))
Article
관련 사항을 체계적으로 정비한다. articles
칼럼을 추출하여 이를 정규표현식으로 정제한다.
gdpr_tbl <- gdpr_df %>%
transmute(id, country = name, price, authority, date, controller, type,
articles = str_extract_all(article_violated, "Art. ?[:digit:]+")) %>%
mutate(ttl_articles = map_int(articles, length)) %>%
unnest(articles) %>%
mutate(articles = str_trim(articles) %>% str_remove(., " ") %>% str_extract(., "Art\\. ?[:digit:]+")) %>%
nest(articles = c(articles))
gdpr_tbl
# A tibble: 180 x 9
id country price authority date controller type ttl_articles
<int> <chr> <int> <chr> <date> <chr> <chr> <int>
1 1 Poland 2000 "Polish … 2019-10-18 Polish Ma… Non-… 1
2 2 Romania 2500 "Romania… 2019-10-17 UTTIS IND… Info… 4
3 3 Spain 60000 "Spanish… 2019-10-16 Xfera Mov… Non-… 2
4 4 Spain 8000 "Spanish… 2019-10-16 Iberdrola… Fail… 1
5 5 Romania 150000 "Romania… 2019-10-09 Raiffeise… Fail… 1
6 6 Romania 20000 "Romania… 2019-10-09 Vreau Cre… Fail… 2
7 7 Greece 200000 "Helleni… 2019-10-07 Telecommu… Fail… 2
8 9 Spain 30000 "Spanish… 2019-10-01 Vueling A… Non-… 2
9 10 Romania 9000 "Romania… 2019-09-26 Inteligo … Non-… 2
10 11 Germany 195407 "Data Pr… 2019-09-19 Delivery … Non-… 3
# … with 170 more rows, and 1 more variable: articles <list>
GDPR로 인해 부과된 벌금에 대한 사항이 중요 정보별로 준비되어 있어 관심 사항별로 살펴보자.
국가별로 벌금부과 건수와 총금액을 합산하여 표를 만들어 살펴본다.
gdpr_tbl %>%
group_by(country) %>%
summarise(건수 = n(),
금액 = sum(price)) %>%
arrange(-금액) %>%
DT::datatable() %>%
DT::formatRound(c("건수", "금액"), digits=0)
국가별로 벌금부과 건수와 총금액을 합산된 정보를 이용하여 두 변수간의 관계를 산점도를 그려 시각화한다. ggimg
팩키지에 svg
지원이 없다보니 이를 우회하는 방법으로 .svg
파일을 다운로드 받아 .png
파일로 모두 변환시키고 나서 이를 가져와서 작업한다.
library(ggimg)
flag_df <- gdpr_dat %>%
select(id, picture) %>%
mutate(flag = str_extract(picture, "[\\w,\\s-]+\\.svg$") %>%
paste0("data/", .))
## 웹국기를 로컬 컴퓨터로 다운로드
# map2(flag_df$picture, flag_df$flag, download.file)
## .svg 파일을 .png로 변환
# svg_files <- list.files(path="data", pattern = "svg")
#
# svg_path_files <- paste0("data/", svg_files)
# png_path_files <- str_replace(svg_path_files, "svg", "png")
#
# for(i in seq_along(svg_path_files)) {
# tmp <- rsvg::rsvg(svg_path_files[i])
# png::writePNG(tmp, png_path_files[i])
# }
## 국기 시각화
library(ggrepel)
flag_df %>%
mutate(flag = str_replace(flag, "svg", "png")) %>%
inner_join(gdpr_tbl) %>%
group_by(picture, flag, country) %>%
summarise(건수 = n(),
금액 = sum(price)) %>%
ggplot(aes(x=건수, y=금액, label = country)) +
geom_point() +
theme_minimal() +
scale_y_log10(labels = scales::dollar_format(prefix ="€")) +
geom_point_img(aes(
x = 건수,
y = 금액,
img = flag), size = 0.5) +
geom_text_repel()
국가별로 벌금부과 건수와 총금액을 합산된 정보를 이용했다면 이번에는 조항별로 살펴보자.
gdpr_tbl %>%
unnest(articles) %>%
group_by(articles) %>%
summarise(건수 = n(),
금액 = sum(price)) %>%
arrange(-금액) %>%
DT::datatable() %>%
DT::formatRound(c("건수", "금액"), digits=0)
마찬가지 방식으로 벌금부과 건수와 총금액을 산점도를 그려 관계를 살펴보자.
GDPR 위반시 가장 많이 언급된 단어 빈도수 분석은 다음과 같다. GDPR 위반시 부과된 벌금과 함께 summary
필드에 관련 내용이 간략히 요약되어 있다.
library(tidytext)
library(extrafont)
loadfonts()
gdpr_df %>%
select(authority, summary) %>%
unnest_tokens(word, summary) %>%
anti_join(stop_words) %>%
count(word, sort = TRUE) %>%
head(30) %>%
mutate(word = fct_reorder(word,n)) %>%
ggplot() +
geom_col(aes(x = n, y = word), fill = "red") +
gghighlight::gghighlight(word == 'consent') +
labs(title = "GDPR 위반시 자주 언급된 단어",
caption = "데이터 출처: Privacy Affairs",
x="단어 출현빈도수",
y="") +
theme_minimal(base_family = "NanumGothic")
GDPR 위반 관련하여 서로 많이 연관된 조항을 파악하는 것도 필요하다. UpSetR을 활용하여 파악하는 것이 가능해졌다. 사용법은 각 사례별로 해당되는 조항(article)은 1, 그렇지 않는 조항은 0으로 표식하는 것이고 이를 upset()
함수에 넣어주면 된다.
GDPR 위반시 언급되는 위반 조항(article)에 대해서 네트워크 분석을 진행하여 조항간 관계를 파악할 수 있다.
library(tidygraph)
library(ggraph)
# Edgelist 생성
gdpr_edge_list <- gdpr_tbl %>%
unnest(articles) %>%
group_by(id) %>%
filter(n() >= 2) %>%
do(data.frame(t(combn(.$articles, 2)), stringsAsFactors = FALSE)) %>%
ungroup() %>%
select(- id) %>%
rename(from = X1, to = X2) %>%
group_by(from, to) %>%
summarise(weight = n()) %>%
ungroup()
# tidygraph 그래프객체 생성
gdpr_graph <- as_tbl_graph(gdpr_edge_list) %>%
to_undirected() %>%
activate(nodes) %>%
mutate(centrality = centrality_authority()) %>%
mutate(group = as.factor(group_edge_betweenness())) %>%
group_by(.add = group) %>%
mutate(name_first = last(name, order_by = centrality)) %>%
ungroup()
# 그래프 시각화
gdpr_graph %>%
ggraph(layout = 'linear', circular = TRUE) +
geom_edge_arc(aes(alpha = weight, width = weight), show.legend = FALSE) +
geom_node_label(aes(label = name,
colour = group)) +
theme_graph(base_family = "NanumGothic") +
guides(colour = FALSE, size = FALSE) +
labs(title = str_wrap("GDPR 위반시 함께 언급되는 조항 관계도",
width = 40))
국가별 적용된 조항(article)에 따라 GDPR 위반 벌금을 한장의 그래프로 표현한다.
[GitHub Gist: @ofchurches, ofchurches/gist:da902a393ce8e529b33ca0a137cd5ff3](https://gist.github.com/ofchurches/da902a393ce8e529b33ca0a137cd5ff3)↩︎
Roel’s R-tefacts (April 8, 2020), “Scraping Gdpr Fines - Into the DOM with a flavour of regex”↩︎
Philgee (Apr-30-2020), “Lets tidy the tuesday: A look at the GDPR-Violations”↩︎
stackoverflow, " Generating an edge list from ID and grouping vectors "↩︎
Jesse Sadler(October 25, 2017), “Introduction to Network Analysis with R: Creating static and interactive network graphs”↩︎