“leaflet 패키지로 감사받은 유치원 지도 제작하기”

WooJun Kang


지난해 10월 MBC는 전국 17개 시도교육청의 2014년 이후 유치원 감사보고서홈페이지에 공개했습니다.

figure1) MBC 홈페이지

figure1) MBC 홈페이지


공개한지 하루만에 내려받은 건수만 120만 건에 이르며 많은 대중에 관심을 받았습니다. 서울시 교육감이 한유총에 설립허가를 취소하며 일명 ’사립유치원 사태’는 일단락되가는 것처럼 보입니다. 하지만 공개된 보고서는 비리 유치원의 전체 명단이 아니라 잘못을 인정한 유치원의 경우만 일단 공개됐습니다. 빙산에 일각인 것입니다.

현재까지 MBC에 공개된 비리유치원 명단을 바탕으로 서울에 있는 비리유치원에 대한 정보를 한눈에 보기쉽게 지도형태로 제작해보고자 합니다.


아래는 MBC 홈페이지에 올라온 유치원 감사보고서 공개 관련 공지사항입니다.
유치원 감사보고서 공개 관련 공지사항

이번 감사결과는 전국 모든 유치원에 대한 전수조사 결과가 아닙니다. 17개 시도교육청이 2014년 이후 자체 기준에 따라 일부 유치원을 선별해 실시한 감사 결과입니다.

감사에 적발된 유치원 명단이 모두 공개된 것도 아닙니다. 가령 경기도의 경우 행정처분 이상의 조치가 필요한 중대 비리가 있거나 감사를 아예 거부한 유치원 18곳은 수사 의뢰를 통해 현재 재판이 진행 중입니다. 이런 유치원들은 최종 판결이 나오기까지는 실명 공개를 할 수 없습니다.

각 교육청마다 적발사항에 대한 공개 수준이 다른 점을 이해해주시기 바랍니다. 공개된 자료는 유치원 설립자와 원장 이름 등 개인정보와 처분에 불복해 소송을 제기한 경우만 삭제했을 뿐 교육청에서 제출한 자료에 어떤 수정도 가하지 않았습니다.

교육청의 행정처분은 시정, 주의, 경고, 경징계(견책. 감봉1월~3월), 중징계(정직1월 ~3월. 해임. 파면)순으로 처분이 무겁습니다.

감사에서 적발돼 이번에 명단이 공개된 모든 유치원이 심각한 비위행위를 저질렀다고 볼 수는 없습니다. 고의성이 없는 단순 착오나 실수로 규정에 어긋난 행위를 했다 감사에 적발된 유치원들도 있을 수 있다는 점을 알려드립니다.




목차
1. Data Cleansing
2. Convert row base data set to column base data set
3. 다음 지도 API 이용해 유치원 주소 가져오기
4. leaflet 패키지를 활용해 지도 제작하기



패키지 불러오기

library(tidyverse)
library(readxl)
library(httr)
library(jsonlite)
library(rvest)
library(urltools)
library(leaflet)
library(htmltools)



1. Data Cleansing

MBC 사이트에서 PDF형태로 제공하고 있기에 ‘서울 유치원 감사보고서’를 엑셀형태로 바꿔 불러옵니다.

figure2) 감사보고서 원본

seoul <- readxl::read_xlsx(path = './data/Seoul-report.xlsx')

glimpse(seoul)
## Observations: 116
## Variables: 6
## $ 유치원명                              <chr> "서울청계숲 유치원", "서울청계숲 유치원", "서울청…
## $ 감사기관                              <chr> "서울시교육청", "서울시교육청", "서울시교육청", …
## $ 적발내용                              <chr> "우유  급식예산  낭비", "학교시설물  안전  및 …
## $ 행정처분내용                          <chr> "시정", "시정", "시정(회수  54,200원)", "…
## $ `처분  수용  여부 (수용  또는  불복)` <chr> "수용", "수용", "수용", "수용", "수용", "수용", "수…
## $ `처분사항 이행여부`                   <chr> "이행완료", "이행완료", "이행완료", "이행완료", "이…
cat('적발된 유치원 수 :', seoul$유치원명 %>% unique() %>% length())
## 적발된 유치원 수 : 24

변수 중 처분사항 이행여부와, 행정처분내용 만을 지도에 나타낼 것입니다. 이를 위해 두 변수를 몇 개의 집단을 갖는 팩터 형태의 변수로 변환해 줍니다.


a) 처분사항 이행여부

98% 이상이 처분사항에 대해서 이행완료했습니다.

seoul$`처분사항 이행여부` %>% table() %>% prop.table() %>% print()
## .
##                                                                                                            이행완료 
##                                                                                                          0.98275862 
## 이행중\r\n-  10,235,768원  이행  완료\r\n-  2,850,000원은  집행에  대한 법률  해석의  이견으로  이행 여부  검토  중 
##                                                                                                          0.00862069 
##                                처분  사항  이행중\r\n-  회수금액  분할  납부  중\r\n(현재  4,048,595원  이행  완료) 
##                                                                                                          0.00862069


이행완료하지 않은 유치원을 출력합니다.

seoul[which(seoul$`처분사항 이행여부` != '이행완료'), ]
## # A tibble: 2 x 6
##   유치원명 감사기관 적발내용 행정처분내용 `처분  수용  여부 (수용… `처분사항 이행여부`…
##   <chr>    <chr>    <chr>    <chr>        <chr>            <chr>           
## 1 오즈의마법사유… 강남서초교육지… 설립자의  직… 시정(회수  13,4… 수용             "처분  사항  이행중\r\…
## 2 반포자이유치원… 강남서초교육지… 예산  목적 … 기관경고,  시정(회… 수용             "이행중\r\n-  10,2…


이행완료하지 않은 두 건중 한 건은 분할 납부중이고, 한 건은 법률 해석 이견으로 이행 여부를 검토 중이라고 합니다. 두 건 모두 ’이행중’이라고 바꿔줍니다.

seoul$`처분사항 이행여부` <- ifelse(seoul$`처분사항 이행여부` == '이행완료', 
                                    yes = '이행완료', 
                                    no = '이행중')
seoul$`처분사항 이행여부` %>% table() %>% prop.table() %>% print()
## .
##   이행완료     이행중 
## 0.98275862 0.01724138


b) 행정처분내용

빈도수 기준 상위 5개의 행정처분내용을 출력합니다.

seoul$행정처분내용 %>% table() %>% 
  sort(decreasing = TRUE) %>% 
  head(n = 5)
## .
##       시정 주의(원장)   기관주의 경고(원장)   기관경고 
##         20          9          7          6          6


행정처분은 시정, 주의, 경고, 경징계, 중징계 순으로 처분이 무겁습니다. stringr패키지를 이용하여 위의 5개 레벨을 기준으로 factor로 바꿔줍니다.

seoul$행정처분내용 %>% head(n = 5)
## [1] "시정"                                 
## [2] "시정"                                 
## [3] "시정(회수  54,200원)"                 
## [4] "시정"                                 
## [5] "시정(환불  93,615,443원),  경고(원장)"


우선 정규표현식을 이용해 괄호 안 처분 세부 내용과 ’기관’이란 단어를 제거합니다.

temp <- str_remove_all(string = seoul$행정처분내용,
                               pattern = "\\(.*?\\)") %>% 
  str_remove_all(pattern = '기관') %>% 
  str_split(pattern = ',  ')


여러 개의 행정처분을 한번에 처분받았을 경우 가장 무거운 처분으로 대치합니다.

head(temp, n = 5)
## [[1]]
## [1] "시정"
## 
## [[2]]
## [1] "시정"
## 
## [[3]]
## [1] "시정"
## 
## [[4]]
## [1] "시정"
## 
## [[5]]
## [1] "시정" "경고"


list로 반환됐기에 for문을 돌리기 위한 사용자 함수를 만듭니다.

penalty <- function(x){
  ifelse('중징계' %in% x, '중징계',
         ifelse('경징계' %in% x, '경징계',
                ifelse('경고' %in% x, '경고',
                       ifelse('주의' %in% x, '주의', '시정'))))
}


반환된 리스트를 for문을 활용해 사용자 함수를 적용시켜줍니다.

temp1 <- c()
for(i in 1:nrow(seoul)){
  
  # 사용자 함수를 적용한 뒤 빈 벡터에 추가하기.
  temp1 <- append(temp1, penalty(x = temp[[i]]))

  }

seoul$행정처분내용 <- temp1
table(seoul$행정처분내용)
## 
## 경고 시정 주의 
##   41   36   39
rm(temp)
rm(temp1)
rm(penalty)



2. convert row base data set to column base data set

기존 데이터 셋은 로우 베이스(한 유치원에 대한 정보가 여러 행에 표현) 이므로 지도로 나타내기 위해 컬럼 베이스 데이터 셋(한 유치원에 대한 정보를 한 행에 여러 열로 표현)으로 변환해 줍니다. 이를위해 행정처분내용과, 처분사항 이행여부 변수를 원 핫 인코딩한 후에 이를 빈도수로 채워줍니다.

table(seoul$행정처분내용)
## 
## 경고 시정 주의 
##   41   36   39
table(seoul$`처분사항 이행여부`)
## 
## 이행완료   이행중 
##      114        2
DB <- seoul %>% 
  
  # 행정처분내용 더미변수화
  cbind(seoul$행정처분내용 %>% 
          as.factor() %>% 
          dummies::dummy() %>% 
          as.data.frame() %>% 
          `colnames<-`(c('경고', '시정', '주의')) %>% 
          
          # 처분사항 이행여부 더미변수화
          cbind(seoul$`처분사항 이행여부` %>% 
                  as.factor() %>% 
                  dummies::dummy() %>% 
                  as.data.frame() %>% 
                  `colnames<-`(c('이행완료', '이행중')))) %>% 
  
  # 모두 다 처분사항에 대해 수용 했기에 수용 여부 변수는 제거하기.
  select(-c('행정처분내용', 
            '처분사항 이행여부', 
            '처분  수용  여부 (수용  또는  불복)', 
            '적발내용', 
            '감사기관')) %>% 
  
  # 유치원별 처분내용과 이행여부를 빈도수로 나타내기.
  group_by(유치원명) %>% 
     summarise(적발수 = n(),
               경고 = sum(경고),
               시정 = sum(시정),
               주의 = sum(주의),
               이행완료 = sum(이행완료),
               이행중 = sum(이행중))



3. 다음 지도 API 이용해 유치원 주소 가져오기

서울 이내 유치원만 가져와야 되므로, 서울 시청 기준으로 반경 20km이내 유치원만 검색되게 설정해줍니다.

# 서울시청좌표 
# 37.5663 / 126.9779

address <- data.frame()
for(i in 1:nrow(DB)){
  
  name <- DB$유치원명[i]
  # cat(i, ': [', name, '주소 가져오는 중 ]\n')
  
  # http 요청  
  res <- GET(url = 'https://dapi.kakao.com/v2/local/search/keyword.json',
             query = list(query = DB$유치원명[i],
                          x = 126.9779,
                          y = 37.5663,
                          radius = 20000),
             add_headers(Authorization = Sys.getenv('KAKAO_MAP_API_KEY')))
  
  # json 형태의 데이터 추출
  json <- res %>% content(as = 'text') %>% fromJSON()
  
  # 주소 추출
  addr <- json$documents$road_address_name
  # 위도 추출
  latitude <- json$documents$y
  # 경도 추출
  longitude <- json$documents$x
  # 하나의 데이터 프레임으로 만들기
  addr_info <- cbind(addr[1], latitude[1], longitude[1]) %>% 
    `colnames<-`(c('address', 'latitude', 'longitute'))
  
  # 데이터 프레임 형태로 저장
  address <- rbind(address, addr_info, stringsAsFactors = FALSE)
  
  # ip차단을 대비해 시간간격 두기
  Sys.sleep(time = 0.5)
  
}

## 메인 데이터셋과 합쳐주기
kinder_DB <- cbind(DB, address)


최종 데이터 셋을 출력해봅니다.

str(kinder_DB)
## 'data.frame':    24 obs. of  10 variables:
##  $ 유치원명 : chr  "계상유치원" "계성유치원" "고덕유치원" "굿프랜드유치원" ...
##  $ 적발수   : int  4 2 3 7 10 2 8 4 6 3 ...
##  $ 경고     : int  2 0 1 1 2 2 2 1 4 1 ...
##  $ 시정     : int  2 0 0 1 6 0 6 0 1 0 ...
##  $ 주의     : int  0 2 2 5 2 0 0 3 1 2 ...
##  $ 이행완료 : int  4 2 3 7 10 2 8 4 5 3 ...
##  $ 이행중   : int  0 0 0 0 0 0 0 0 1 0 ...
##  $ address  : chr  "서울 양천구 남부순환로86길 22" "서울 용산구 효창원로15길 15" "서울 강동구 구천면로 645" "서울 영등포구 대방천로 180" ...
##  $ latitude : chr  "37.5124899409493" "37.5358241601413" "37.55062605178119" "37.498584589787" ...
##  $ longitute: chr  "126.83982489666" "126.952796450861" "127.16868459022714" "126.910430196472" ...



4. leaflet 패키지를 활용해 지도 제작하기

지도위 마크를 클릭하면 나오는 팝업창 설정해줍니다. 팝업창 작성형식은 html과 유사합니다.

popup <- paste(
               # 유치원명 굵게, 밑줄치기
               "<b><u>", kinder_DB$유치원명, '</u></b>', '<br/>',
               
               # 주소는 기울임체, 사이즈 작게하고 괄호안에 넣기
               '<i><small>(', kinder_DB$address, ')</small></i>', '<br/>',
               
               # 유치원에 대한 감사 처분에 대한 정보
               '<sub>',
               '적발수  :', kinder_DB$적발수, '<br/>', 
               '경고    :', kinder_DB$경고, '<br/>',
               '시정    :', kinder_DB$시정, '<br/>',
               '주의    :', kinder_DB$주의, '<br/>',
               '이행완료:', kinder_DB$이행완료, '<br/>',
               '이행중  :', kinder_DB$이행중,
               '</sub>')


leaflet 패키지를 이용해 지도를 출력합니다.

my_map <- leaflet() %>%
  addTiles() %>%
  addMarkers(lat = as.numeric(kinder_DB$latitude),   # 위도와 경도 숫자형 변수로 변환
             lng = as.numeric(kinder_DB$longitute),
             popup = popup,                          # 클릭 시 나오는 팝업창
             label = htmlEscape(kinder_DB$유치원명)) # 커서를 마크위에 올리면 나오는 정보
my_map