brunch

You can make anything
by writing

C.S.Lewis

by 지은 x NULL Aug 20. 2019

[생활 속 데이터분석] 주차장에 자리 있나요?(2/3)

실시간을 넘어서 미래의 상태를 알고 싶을 때

※ 이전 글(1/3)에서 이어집니다.




찰나의 데이터로는 현재 상태를 확인하는 것 외에 원하던 결과를 얻지 못했습니다. 데이터 없이 미래를 '예측'할 수 있는 묘수가 있는 건 아니니 계속해서 데이터를 모았습니다. (글을 쓰는 현재까지 보름 정도 PC를 켜두고 코드를 동작시키고 있습니다.)

4시간이 아니라 4일 하고도 23시간 44분




모은 데이터를 불러옵니다.


저장 형태는 32->64->96->...->3200처럼 늘어났고 모은 데이터 전체를 붙여서(bind) 10회당 한 번꼴로 엑셀파일로 저장되게 했는데, 중간중간 멈추고 끊기는 경우가 있어서 파일이 파편화됐습니다.

여러 조각으로 나눠지거나 여러 사람이 작업한 파일을 하나로 합치는 방법은

간단하게 생각하면 데이터 프레임을 하나 만들고, 파일을 하나씩 불러들여서 행(row)을 이어붙이는 방식으로 하면 됩니다.


# 데이터 로드
useable_dfs <- data.frame()
for (filepath in dir(path = "파일을모아둔경로", pattern = "[.]xlsx$")) {
  scrapped <- read_excel(filepath)
  useable_dfs <- bind_rows(useable_dfs, scrapped)
}


이런 파편화-통합 작업 시 같은 데이터가 중복으로 들어갈 가능성이 있어 '완전히 동일한' 데이터는 하나씩만 남기고 지워줍니다.


# 단순중복 체크

useable_dfs[duplicated(useable_dfs),] %>% nrow()


부연 설명을 추가하면

duplicated(useable_dfs) : 중복 데이터를 리턴

useable_dfs[행번호, 열번호] : 즉, 중복 데이터 행의 모든 열을 가져옴

%>% nrow() : 행(row) 개수 확인용

따라서 위 한줄 코드의 결과는 중복된(완전 일치하는) 데이터 개수를 표시합니다. 다음 코드 진행과는 무관하다는 뜻이기도 하죠. 실제 제거는 아래와 같이 합니다.


# 수집과정에서 생긴 단순중복(동일한 데이터) 제거

useable_dfs <- useable_dfs[!duplicated(useable_dfs),]


str(useable_dfs)




데이터를 '쓸 수 있게' 가공합니다.


그간 웹페이지 이름을 따서 useable_데이터프레임s라는 변수명으로 데이터를 합쳤는데, 앞으로는 parking이라는 이름으로 새로운 데이터프레임을 만들어서 사용해보겠습니다. Timezone 문제가 발생하지 않도록 주의합니다.


# 작업용 Raw 데이터 사본 생성

parking <- useable_dfs

#parking$datetime <- format(parking$datetime, tz="Asia/Seoul", usetz=TRUE)


여러 파일에 나눠 받은 뒤 합쳤을 때 00시 00분 내 하나의 데이터만 남기고 나머지는 지워줍니다.

다른 라이브러리 추가 없이 substr(부분 문자열)로 앞부분만 살렸습니다. 그걸 다시 POSIXct로 변환하면 '12시 34분 00초'처럼 만들어지고, 동시간대엔 주차장별로 하나의 데이터만 남겨주기 위해 '주차장+시간'을 고유값으로 하여 duplicated를 찾아봅니다. !duplicated()로 중복이 아닌 데이터만 골라서 임시변수 dup는 선택하지 않는(select(-컬럼명)) 걸로 합니다.

사실 이 방법이면 앞서 작업한 완전 일치 데이터도 당연히 걸러줄 수 있습니다.

※ 처음부터 최적의 솔루션을 알려드리려는 강좌가 아니라 현상이 주어졌을 때 문제를 발견한 후 사고의 흐름대로 방법을 찾아보는 연습입니다.


# 분단위 데이터 한정(동일 분(minute) 내의 다른 데이터는 제거)

parking$datetime[1] # 원래 데이터

substr(parking$datetime[1], 1, 16) # 초 이하 버림


# 임시 컬럼 dup 생성

parking$dup <- paste(parking$`주차장명`, as.POSIXct(substr(parking$datetime, 1, 16)))


x <- parking[duplicated(parking$dup),]

# 중복 제거

parking <- parking[!duplicated(parking$dup),] %>% select(-dup)



이전 글에서 했던 형변환 작업도 적절하게 해주고, 분 단위 데이터를 시간(hour)으로 묶어줍니다. 시간별 평균값(mean)을 구해서 Chart 변수에 추가합니다.

시간도 substr으로 쪼갤 수 있겠지만 lubridate 패키지를 이용했습니다.


# 자료형 변환

parking$datetime <- as.POSIXct(parking$datetime) # POSIX로 변환

parking$`주차면수` <- as.numeric(as.character(parking$`주차면수`))

parking$`잔여주차가능대수` <- as.numeric(as.character(parking$`잔여주차가능대수`))

#parking$번호 <- as.factor(parking$번호)


# 시간 추가

parking$hour <- lubridate::hour(parking$datetime)

Chart <- parking %>% group_by(hour) %>% tally(mean(`잔여주차가능대수`))


group_by() + tally()로 만든 데이터는 24시간짜리 아래와 같은 형태가 됩니다.

> Chart

# A tibble: 24 x 2

    hour     n

   <int> <dbl>

 1     0  307.

 2     1  311.

 3     2  314.

 4     3  316.

 5     4  318.

 6     5  320.

 7     6  314.

 8     7  294.

 9     8  259.

10     9  232.

# ... with 14 more rows




차트를 그려줍니다.


Chart 데이터를 시간별 n값으로 플로팅해봤습니다.

ggsave는 R Studio에서 개별 확인하지 않고 직접 파일 형태로 저장하는 부분입니다.


ggplot(Chart, aes(x = hour, y = n)) +

  geom_area()


ggsave("1_area.png", width = 6, height = 4, units = "in", dpi = 200)


기본 qplot도 아니고 ggplot 패키지까지 썼는데 이게 무슨 그림인가 싶으실 텐데요. 그래도 데이터를 읽어보면 가용주차면 즉 빈자리 기준이니까 자정무렵 300면에서 낮시간에 200면 정도로 줄었다가 저녁에 다시 빠져나가는 형태네요.

gvisChart에서는 분명 area가 적절해보였는데 검정색이 너무 두드러지니까 다시 line으로 바꿔봅니다.


ggplot(Chart, aes(x = hour, y = n)) +

  geom_line()

# 사용자정의 함수 이용(반복 사용되는 인수를 간편하게 수정하기 위함)

f.ggsave("2_line.png")


f.으로 시작하는 건 제가 임의로 만든 함수입니다. 반복되는 작업의 경우 코드량을 줄이기 위한 목적입니다.(성능은 논외로 하고) 함수 선언 방식은 다른 언어와 유사합니다. 몇 가지 옵션을 번갈아가면서 쓸 예정이라 target 파라미터를 추가했습니다.

f.ggsave <- function(filename, target="web") {
  if (target == "web") {
    ggsave(filename = filename, width = 6, height = 4, units = "in", dpi = 200)
  } else if (target == "facet") {
    ggsave(filename, width = 12, height = 8, units = "in", dpi = 100)
  } else {
    ggsave(filename, width = 6, height = 4, units = "in", dpi = 300)
  }
}


사실 선이든 영역이든 중요한 게 아닙니다. tally(mean())을 거치면서 32개 주차장 모두를 평균 냈으니 1,000면짜리도 있고 10면인 곳도 있는데 숫자가 아무 의미가 없어졌습니다. 대강의 경향성 정도만 확인했습니다.

이제 비로소 주차장별로 나눠서 확인을 해봅니다.


# 주차장별로 구분

#Chart <- parking %>% group_by(hour) %>% tally(mean(`잔여주차가능대수`))

Chart <- parking %>% group_by(`주차장명`, hour) %>% tally(mean(`잔여주차가능대수`)) # 위 코드와 달라진 점은?

> Chart

# A tibble: 768 x 3

# Groups:   주차장명 [32]

   주차장명    hour     n

   <chr>      <int> <dbl>

 1 가양라이품     0  34.6

 2 가양라이품     1  34.7

 3 가양라이품     2  34.7

 4 가양라이품     3  34.6

 5 가양라이품     4  34.7

 6 가양라이품     5  34.2

 7 가양라이품     6  33.1

 8 가양라이품     7  31.3

 9 가양라이품     8  26.1

10 가양라이품     9  19.5

# ... with 758 more rows



플롯을 그려보면 이제 뭔가 원하던 데이터가 나온 느낌입니다. 대부분 중앙에서 약간 우측으로 치우친 완만한 U자형이네요. 낮게 깔린 직선처럼 보이는 건 데이터 누락이 있었던 걸까요?


ggplot(Chart, aes(x = hour, y = n)) +

  geom_line() +

  facet_wrap(vars(`주차장명`))


ggsave("3_주차장구분.png", width = 12, height = 8, units = "in", dpi = 100)


# 가로세로 적당한 크기로 펼쳐줍니다.

ggplot(Chart, aes(x = hour, y = n)) +

  geom_line() +

  facet_wrap(`주차장명` ~ ., ncol = 4)


f.ggsave(filename = "4_주차장facet(4x8).png", target = "facet")



그렇지는 않았습니다. facet 과정에서 축을 공유하다보니 직선 혹은 잘못된 데이터처럼 보였는데, 아래 그림을 보면 문제가 없음을 알 수 있습니다. 다만 '보기 좋은' 것과는 거리가 멀어 계속해서 고쳐봤습니다.


## 스케일이 달라 거의 직선으로 보이는 경우(서울역관광버스주차장)

## 가용 비율로 변경

### -> 더 간단한 해결방법. 단, 플롯마다 y축 텍스트가 추가되어 복잡


ggplot(Chart, aes(x = hour, y = n)) +

  geom_line() +

  facet_wrap(`주차장명` ~ ., ncol = 4, scales = "free")


f.ggsave("4-1_주차장facet(4x8).png", "facet")




# 주차면수 불러오기

x <- parking %>% distinct(`주차장명`, `주차면수`)


# 주차면수 join

Chart <- left_join(Chart, x[1:32,])

## Joining, by = "주차장명"


# 여유 비율을 ratio로 추가

Chart %>% mutate(ratio = n/`주차면수`*100) %>% 

  ggplot(aes(x = hour, y = ratio)) +

  geom_line() +

  facet_wrap(`주차장명` ~ ., ncol=4)


f.ggsave("5_주차가용비율.png", "facet")


Chart %>% mutate(ratio = n/`주차면수`*100) %>% 

  ggplot(aes(x = hour, y = ratio)) +

  geom_line() +

  scale_x_continuous(breaks = seq(0, 23, 3)) +

  facet_wrap(`주차장명` ~ ., ncol=4) +

  theme_minimal()



f.ggsave("5-1_테마변경.png", "facet")


Y축이 각기 다른 구체적인 숫자보다 주차 가능비율(%)을 플롯으로 보니 32개 차트가 한눈에 들어옵니다. 그런데 개별 차트가 너무 작아서 정작 구체적인 정보를 알기 어렵다는 문제가 있네요. 몇 가지를 추가해봤습니다.

Sys.time()으로 가져온 현재 시각의 hour()를 필터로 써서 768÷24=32개만 보면 어떨까요.


# 현재시간대와 동일 시간대의 데이터만 확인

Chart %>% filter(hour == lubridate::hour(Sys.time())) %>%

  kable()

주차장명hourn주차면수

가양라이품134.66405238

개화산역1261.559477322

개화역1396.792157483

구로디지털단지역148.53594892

구파발역1342.816994399

(하략)


# 주차장 가용율(ratio) 별도 컬럼으로 추가(이어서 계속 사용하므로)

# Chart <- Chart %>% mutate(ratio = n/주차면수*100) #일반적인 방법

Chart$ratio <- Chart$n/Chart$`주차면수`*100

Chart$label <- ifelse(Chart$hour == lubridate::hour(Sys.time()), paste0(round(Chart$ratio, digits = 2),"%"), NA) 



geom_ 옵션이 꽤 복잡한데요, 하나씩 설명드리기 보다는 각 함수명, 옵션명을 키워드 삼아 구글링하는 것을 추천합니다. 요컨대 현재 시간대 위치에 빨간색 세로선(vline)을 긋고, 앞서 확인해본 데이터를 가져와서 라벨링하는 코드입니다.


Plot <- ggplot(Chart, aes(x = hour, y = ratio)) +

  geom_line() +

  geom_text(aes(label = label,

                x = lubridate::hour(Sys.time()), y = ratio)) +

  geom_vline(xintercept = lubridate::hour(Sys.time()), color = "red", linetype = 2) + # as.numeric() transformation

  scale_x_continuous(breaks = seq(0, 23, 3)) +

  facet_wrap(`주차장명` ~ ., ncol=4) +

  theme_minimal()


ggsave("5-2_수직선라벨추가.png", Plot, width = 12, height = 8, units = "in", dpi = 100)


반복되어 변수로 치환할 수 있는 부분, 불필요한 코드 등을 정리했습니다. 현재 시간대, 다른 시간대도 문제 없나 살펴봅니다.


current_hour <- lubridate::hour(Sys.time()) # 현재시간대 이용

Chart$label <- ifelse(Chart$hour == current_hour, paste0(round(Chart$ratio, digits = 2),"%"), NA) 


Plot <- ggplot(Chart, aes(x = hour, y = ratio)) +

  geom_line() +

  geom_text(aes(label = label, x = current_hour, y = ratio,

            vjust = 0, hjust = 0), size = 3) +

  geom_vline(xintercept = current_hour, color = "red", linetype = 2) + # as.numeric() transformation

  scale_x_continuous(breaks = seq(0, 23, 3)) +

  facet_wrap(`주차장명` ~ ., ncol=4) +

  theme_minimal()


ggsave("6_튜닝(현재시각기준).png", Plot, width = 12, height = 8, units = "in", dpi = 100)


current_hour <- 12 # 다른 시간대 확인을 위해 하드코딩

Chart$label <- ifelse(Chart$hour == current_hour, paste0(round(Chart$ratio, digits = 2),"%"), NA) 


Plot <- ggplot(Chart, aes(x = hour, y = ratio)) +

  geom_line() +

  geom_text(aes(label = label, x = current_hour, y = ratio,

                vjust = 0, hjust = 0), size = 3) +

  geom_vline(xintercept = current_hour, color = "red", linetype = 2) + 

  scale_x_continuous(breaks = seq(0, 23, 3)) +

  facet_wrap(`주차장명` ~ ., ncol=4) +

  theme_minimal()


ggsave("6-1_시간변경.png", Plot, width = 12, height = 8, units = "in", dpi = 100)


밤중에는 텅 빈 주차장이 정오무렵이면 반 이상 들어차고, 많은 곳은 가용 대수가 10% 밑으로 떨어지네요. 동대문만 U자 형태가 아니라 밤시간이 더 붐비는 활처럼 보이는 게 신기합니다.

이제 가고자 하는 주차장, 원하는(도착에 즈음한) 시간대가 있다면 current_hour를 직접 바꿔가면서 확인해볼 수 있습니다.


도착하는 그 때 제 자리 있나요?



# for 루프로 전체 시간대 확인

for (current_hour in seq(0,23)) {

  Chart$label <- ifelse(Chart$hour == current_hour, paste0(round(Chart$ratio, digits = 2),"%"), NA) 

  

  Plot <- ggplot(Chart, aes(x = hour, y = ratio)) +

    geom_line() +

    geom_text(aes(label = label, x = current_hour, y = ratio,

                  vjust = 0, hjust = 0), size = 3) +

    geom_vline(xintercept = current_hour, color = "red", linetype = 2) +

    scale_x_continuous(breaks = seq(0, 23, 3)) +

    facet_wrap(`주차장명` ~ ., ncol=4) +

    theme_minimal()

  

  ggsave(sprintf("7_%02d시.png", current_hour), Plot, width = 12, height = 8, units = "in", dpi = 100)

}


이쯤이면 데이터를 적당히 정리했다고 보고, 시각화를 통해 원하던 정보도 얻을 수 있었습니다. 하지만 원 데이터를 봤을 때는 주말과 평일간 주차장 이용행태 차이가 확연했는데, 평균을 쓰다보니 어느쪽에도 맞지 않는 모호한 값이 대푯값이 된 듯합니다. 이를 바로잡기 위해 요일 개념을 추가해서 봐야겠는데...


다음 글(작성 중)로 이어집니다.






※ 전체 코드 확인 및 복사가 필요한 분들을 위해 R Markdown 링크도 공유합니다.

-.NULL




[예고] 다음 글을 시작하기에 앞서

보름 동안 그러모은 데이터로 만들어본 결과물부터 한 번 보여드립니다. 계획에는 없었지만 실제 활용 가능한 애플리케이션(응용프로그램) 형태로 만들어봤습니다.


인터랙티브 플롯


플롯을 그리거나 웹 대시보드를 만드는 건 구글링에 조금만 익숙해지면 어렵지 않습니다.

그보다는, 

사이트에 흔히 공개된 32줄짜리 표를 한땀한땀 모아서
이런 걸 해볼 수도 있겠구나

정도로 봐주시면 좋겠네요.


실제 데모 링크도 함께 남깁니다. (http://llun.shinyapps.io/findparking)

접속이 몰리거나 장시간 이어진다면 연결이 안 될 수도 있습니다.



매거진의 이전글 [생활 속 데이터분석] (주차장에) 제 자리 있나요?

작품 선택

키워드 선택 0 / 3 0

댓글여부

afliean
브런치는 최신 브라우저에 최적화 되어있습니다. IE chrome safari