이번 포스팅에서는 R data.frame에서 여러개의 칼럼 이름을 '변경 전 칼럼 이름 : 변경 후 칼럼 이름'의 매핑 테이블 (old_column_name : new_column_name mapping table) 을 이용해서 한꺼번에 변경하는 방법을 소개하겠습니다. data.frame에 칼럼 개수가 엄청 많고, 특정 칼럼에 대해서 선별적으로 칼럼 이름을 변경하고 싶을 때 전:후 칼럼 이름 매핑 테이블을 사용하는 이번 포스팅의 방법을 사용하면 편리합니다. 

 

renaming column names using mapping table in R data.frame

 

 

(1) 모든 칼럼을 순서대로 칼럼 이름을 변경하고 싶은 경우

 

참고로, R 에서 names(), rename() 등의 함수를 이용해서 칼럼 이름을 변경하는 방법은 https://rfriend.tistory.com/41 를 참고하세요. 

 

 

먼저, "X1" ~ "X10" 까지의 10개 칼럼을 가지는 예제 data.frame 을 만들어보겠습니다. 

 

## -- creating a sample data.frame with 10 columns
df <- data.frame(matrix(1:30, nrow=3))

print(df)
# X1 X2 X3 X4 X5 X6 X7 X8 X9 X10
# 1  1  4  7 10 13 16 19 22 25  28
# 2  2  5  8 11 14 17 20 23 26  29
# 3  3  6  9 12 15 18 21 24 27  30

 

 

다음으로, '변경 전 칼럼 이름 : 변경 후 칼럼 이름' 매핑 테이블을 만들어보겠습니다. 아래 예제에서는 변경 전 칼럼 이름 "X1"~"X10" 을 --> 변경 후 칼럼 이름 "var1"~"var10" 의 매핑 테이블 data.frame을 만들었습니다. (특정 칼럼만 선별적으로 변경하고 싶으면 해당 칼럼의 "변경 전 : 변경 후 매핑 테이블"을 만들면 됩니다.)

 

## -- creating a key(old column name):value(new column name) mapping table
old_col_nm <- names(df)
print(old_col_nm)
# [1] "X1"  "X2"  "X3"  "X4"  "X5"  "X6"  "X7"  "X8"  "X9"  "X10"

col_cnt <- ncol(df) # 10
new_col_nm <- paste0(c(rep("var", col_cnt)), 1:col_cnt)
print(new_col_nm)
# [1] "var1"  "var2"  "var3"  "var4"  "var5"  "var6"  "var7"  "var8"  "var9"  "var10"

df_col_dict <- data.frame("old_col_nm" = old_col_nm, "new_col_nm" = new_col_nm)
print(df_col_dict)
# old_col_nm new_col_nm
# 1          X1       var1
# 2          X2       var2
# 3          X3       var3
# 4          X4       var4
# 5          X5       var5
# 6          X6       var6
# 7          X7       var7
# 8          X8       var8
# 9          X9       var9
# 10        X10      var10

 

 

 

마지막으로, dplyr 패키지의 rename_at() 함수를 사용해서 "변경 전 칼럼 이름(old_col_nm)"을 "변경 후 칼럼 이름(new_col_nm)" 으로 변경해 보겠습니다. 

 

## -- changing data.frame's column names using key(old_col):value(new_col) mapping table
library(dplyr)
df_new <- df %>% 
  rename_at(vars(as.character(df_col_dict$old_col_nm)), 
            ~ as.character(df_col_dict$new_col_nm))

print(df_new)
# var1 var2 var3 var4 var5 var6 var7 var8 var9 var10
# 1    1    4    7   10   13   16   19   22   25    28
# 2    2    5    8   11   14   17   20   23   26    29
# 3    3    6    9   12   15   18   21   24   27    30

 

 

 

(2) 특정 칼럼만 선별적으로 이름을 바꾸고 싶은 경우

 

아래의 'col_dict' 테이블을 칼럼 이름을 변경하고자 하는 특정 칼럼의 old_col_nm : new_col_nm 으로 만들어서 적용하면 됩니다.

가령, 기존의 c1~c5'까지의 칼럼들 중에서 'c2', 'c4' 의 2개 칼럼만 선별적으로 변경하고 싶으면 아래처럼 'col_dict' 테이블을 만들어서 적용하면 돼요.

 

old_col_nm = c("c2", "c4")
new_col_nm = c("v2", "v4")

col_dict <- data.frame("old" = old_col_nm, "new" = new_col_nm)
print(col_dict)
# old new
# 2 c2 v2
# 4 c4 v4


library(dplyr)
c_df_new <- c_df %>%
rename_at(vars(as.character(col_dict$old)), ~ as.character(col_dict$new))

print(c_df_new)
# c1 v2 c3 v4 c5
# 1 1 4 7 10 13
# 2 2 5 8 11 14
# 3 3 6 9 12 15

 

이번 포스팅이 많은 도움이 되었기를 바랍니다. 

행복한 데이터 과학자 되세요!  :-)

 

반응형
Posted by Rfriend

댓글을 달아 주세요

지난번 포스팅에서는 두 개의 sf 클래스 객체의 지리 벡터 데이터 테이블을 R dplyr 패키지의 함수를 사용하여 Mutating Joins, Filtering Joins, Nesting Joins 하는 방법을 소개하였습니다(rfriend.tistory.com/625). 

 

이번 포스팅에서는 여기서 특수한 경우로 조금 더 깊이 들어가서, 두 테이블을 Join 하는 기준이 되는 Key 칼럼이 문자열로 되어 있고, 데이터 표준화가 미흡한 문제로 인해 정확하게 매칭이 안되어서 Join 이 안되는 경우에, R의 stringr 패키지를 사용해 정규 표현식의 문자열 매칭(a string matching using regular expression)으로 Key 값을 변환하여 두 테이블을 Join 하는 방법을 소개하겠습니다. 

 

 

 

먼저, 전세계 국가별 지리기하와 속성 정보를 모아놓은 sf 클래스 객체의 지리 벡터 데이터셋인 "world" 와, 2016년과 2017년 국가별 커피 생산량을 집계한 data frame 인 "coffee_data" 의 두 개 데이터셋을 spData 로 부터 가져오겠습니다. 

그리고 두 개 테이블 Join 을 위해 dplyr 패키지를 불러오고, 정규 표현식을 이용한 문자열 매칭을 위해 stringr 패키지를 불러오겠습니다. 

 

"world" 데이터셋은 177개의 행(국가)과 11개의 열(속성(attritubes)과 지리기하 칼럼(gemgraphy column)) 으로 이루어져 있습니다. "coffee_data"는 47개의 행과 3개의 열로 구성되어 있습니다. 

 

## =========================================================
## inner join using a string matching
## - reference: https://geocompr.robinlovelace.net/attr.html
## =========================================================

library(sf)
library(spData)
library(dplyr)
library(stringr) # for a string matching

## -- two geography vector dataset tables : world, coffee_data
## -- (a) world: World country pologons in spData
names(world)
# [1] "iso_a2"  "name_long" "continent" "region_un" "subregion" "type"  "area_km2"  "pop"  "lifeExp"   "gdpPercap"
# [11] "geom"

dim(world)
# [1] 177  11


## -- (b) coffee_data: World coffee productiond data in spData
## : estimated values for coffee production in units of 60-kg bags in each year
names(coffee_data)
# [1] "name_long"      "coffee_production_2016" "coffee_production_2017"

dim(coffee_data)
# [1] 47  3

 

 

(1) 두 테이블 inner join 하기: inner_join(x, y, by)

 

"world"와 "coffee_data"의 두개 데이터 테이블을 inner join 해보면 45개의 행(즉, 국가)과 13개의 열(= "world"로 부터 11개의 칼럼 + "coffee_data"로 부터 2개의 칼럼) 으로 이루어진 Join 결과를 반환합니다. 

위에서 "coffee_data" 데이터셋이 47개의 행으로 이루어졌다고 했는데요, inner join 한 결과는 행이 45개로서 2개가 서로 차이가 나는군요. 

 

## -- inner join
world_coffee_inner = inner_join(x = world, 
                                y = coffee_data, 
                                by = "name_long")

## or shortly
world_coffee_inner = inner_join(world, coffee_data)
# Joining, by = "name_long"


dim(world_coffee_inner)
# [1] 45 13


nrow(world_coffee_inner)
# [1] 45

 

 

(2) 두 문자열의 원소 차이 알아보고 문자열 매칭으로 찾아보기: setdiff(), str_subset()

 

Join 전과 후에 어느 국가에서 차이가 나는지 확인해 보기 위해 setdiff() 함수를 사용해서 Join의 Key로 사용이 되었던 'name_long' (긴 국가 이름)에 대해 "coffee_data" 와 "world" 데이터의 원소 간 차이를 구해보았습니다. 그랬더니 ["Congo, Dem. Rep. of", "Others"] 의 2개 'name_long' 에서 차이가 있네요. 

 

다음으로, "world" 의 'name_long' 칼럼의 원소 중에서  "Dem"으로 시작하고 "Congo"를 포함하고 있는 문자열을 stringr 패키지의 str_subset(string, pattern) 함수를 사용해 정규 표현식의 문자열 매칭으로 찾아보겠습니다. "world" 데이터셋의 'name_long' 칼럼에는 "Democratic Republic of the Congo" 라는 이름으로 데이터가 들어가 있네요. ("coffee_data"  데이터셋에는 "Confo, Dem. Rep. of" 라고 들어가 있다보니, 서로 같은 국가임에도 left_join() 을 했을 때 서로 정확하게 매칭이 안되어 Join 이 안되었습니다.)

 

참고로, str_subset() 은 x[str_detect(x, pattern)] 의 wrapper 입니다. 그리고 grep(pattern, x, value = TRUE) 와 동일한 역할을 수행합니다. 

 

## setdiff(): calculates the set difference of subsets of two data frames
setdiff(coffee_data$name_long, world$name_long)
# [1] "Congo, Dem. Rep. of" "Others"


## string matching (regex) function from the stringr package
str_subset(world$name_long, "Dem*.+Congo")
# [1] "Democratic Republic of the Congo"

 

 

 

(3) 문자열 매칭으로 Key 값 업데이트 하고, 다시 두 테이블 inner join 하기

 

이제 Join Key로 사용하는 'name_long' 칼럼에서 "Congo" 국가에 대한 표기가 "world" 와 "coffee_data" 의 두 개 데이터셋이 서로 조금 다르다는 이유로 Join 이 안된다는 문제를 해결해 보겠습니다. 

grepl(pattern, x) 함수로 "coffee_data" 데이터셋의 'name_long' 칼럼에서 "Congo" 가 들어있는 행을 찾아서, 그 행의 값의 str_subset() 의 정규표현식 문자열 매칭으로 찾은 (str_subset(world$name_long, "Dem*.+Congo") 이름인 "Demogratic Republic of the Congo" 라는 이름으로 대체를 해보겠습니다. 이렇게 하면 "world"와 "coffee_data"에 있는 "Congo" 국가의 긴 이름이 동일하게 "Demogratic Republic of Congo"로 되어 Join 이 제대로 될 것입니다. 

 

## updating 'name_long' values using a string matching
coffee_data$name_long[grepl("Congo", coffee_data$name_long)] = 
  str_subset(world$name_long, "Dem*.+Congo")

## inner join again using an updated key
world_coffee_match = inner_join(world, coffee_data)
#> Joining, by = "name_long"

nrow(world_coffee_match)
#> [1] 46

 

 

 참고로, R에서 문자열 패턴 매칭을 할 때 grepl(pattern, x) 은 패턴 매칭되는 여부에 대해 TRUE, FALSE 로 블러언 값을 반환하는 반면에, grep(pattern, x) 은 패턴 매칭이 되는(TRUE) 위치 인덱스(Position Index)를 반환합니다. 

 

## -- grepl: pattern matching and returns boolean
grepl("Congo", coffee_data$name_long)
# [1] FALSE FALSE FALSE FALSE FALSE FALSE  TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
# [21] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
# [41] FALSE FALSE FALSE FALSE FALSE FALSE FALSE


## -- grep: pattern matching and returns position
grep("Congo", coffee_data$name_long)
# [1] 7

 

 

 

(4) Join 할 때 테이블 쓰는 순서의 중요성

 

dplyr 패키지로 두 테이블을 Join 할 때 왼쪽(x, LHS, Left Hand Side)에 써주는 테이블의 데이터 구조로 Join 한 결과를 반환합니다. 즉, Join 할 테이블을 써주는 순서가 중요합니다. 

가령, 아래의 예에서는 "world" 가 'sf' 클래스의 지리 벡터 객체이고, 'coffee_data'는 tydiverse의 tibble, data.frame 객체입니다. left_join(world, coffee_data) 로 'world' 의 'sf' 지리 벡터 객체를 Join 할 때 왼쪽(LHS, x)에 먼저 써주면 Join 한 결과도 'sf' 클래스의 지리 벡터 객체가 됩니다.(R이 지리공간 벡터 데이터임을 알고 'sf' 클래스를 적용한 지리공간 데이터 처리 및 분석이 가능함). 

반면에, left_join(coffee_data, world) 로 'coffee_data'의 'data.frame'을 Join 할 때 왼쪽(LHS, x)에 먼저 써주면 Join 한 결과도 'data.frame' 객체가 반환됩니다. (지리공간 'sf' 클래스가 더이상 아님) 

 

## starting with a non-spatial dataset and 
## adding variables from a simple features object.
## the result is not another simple feature object, 
## but a data frame in the form of a tidyverse tibble: 
## the output of a join tends to match its first argument.

## -- (a) 'sf' object first, then returns 'sf' object.
world_coffee = left_join(world, coffee_data)
#> Joining, by = "name_long"

class(world_coffee)
# [1] "sf"         "tbl_df"     "tbl"        "data.frame"


## -- (b) 'data.frame' object first, then returns 'data.frame' object.
coffee_world = left_join(coffee_data, world)
#> Joining, by = "name_long"

class(coffee_world)
#> [1] "tbl_df"     "tbl"        "data.frame"

 

 

(5) data.frame을 'sf' 클래스 객체로 변환하기

 

'sf' 패키지의 st_as_df() 함수를 사용하면 data.frame 을 'sf' 클래스 객체로 변환할 수 있습니다. 

## -- converting data.frame to 'sf' class object

st_as_sf(coffee_world)

# imple feature collection with 47 features and 12 fields (with 2 geometries empty)
# geometry type:  MULTIPOLYGON
# dimension:      XY
# bbox:           xmin: -117.1278 ymin: -33.76838 xmax: 156.02 ymax: 35.49401
# geographic CRS: WGS 84
# # A tibble: 47 x 13
# name_long coffee_producti~ coffee_producti~ iso_a2 continent region_un subregion type  area_km2     pop lifeExp gdpPercap
# <chr>                <int>            <int> <chr>  <chr>     <chr>     <chr>     <chr>    <dbl>   <dbl>   <dbl>     <dbl>
#   1 "Angola"                NA               NA AO     Africa    Africa    Middle A~ Sove~ 1245464.  2.69e7    60.9     6257.
# 2 "Bolivia"                3                4 BO     South Am~ Americas  South Am~ Sove~ 1085270.  1.06e7    68.4     6325.
# 3 "Brazil"              3277             2786 BR     South Am~ Americas  South Am~ Sove~ 8508557.  2.04e8    75.0    15374.
# 4 "Burundi"               37               38 BI     Africa    Africa    Eastern ~ Sove~   26239.  9.89e6    56.7      803.
# 5 "Cameroo~                8                6 CM     Africa    Africa    Middle A~ Sove~  460325.  2.22e7    57.1     3196.
# 6 "Central~               NA               NA CF     Africa    Africa    Middle A~ Sove~  621860.  4.52e6    50.6      597.
# 7 "Congo, ~                4               12 NA     NA        NA        NA        NA         NA  NA         NA         NA 
# 8 "Colombi~             1330             1169 CO     South Am~ Americas  South Am~ Sove~ 1151883.  4.78e7    74.0    12716.
# 9 "Costa R~               28               32 CR     North Am~ Americas  Central ~ Sove~   53832.  4.76e6    79.4    14372.
# 10 "C\u00f4~              114              130 CI     Africa    Africa    Western ~ Sove~  329826.  2.25e7    52.5     3055.
# # ... with 37 more rows, and 1 more variable: geom <MULTIPOLYGON [arc_degree]>

 

다음번 포스팅에서는  '지리공간 벡터 데이터에서 새로운 속성을 만들고 지리공간 정보를 제거하는 방법'에 대해서 알아보겠습니다. 

 

[Reference]

- Geocomputation with R, 'Attribute data operations': geocompr.robinlovelace.net/attr.html

 

 

이번 포스팅이 많은 도움이 되었기를 바랍니다. 

행복한 데이터 과학자 되세요!  :-)

 

반응형
Posted by Rfriend

댓글을 달아 주세요

지난번 포스팅에서는 R 지리공간 벡터 데이터의 속성 정보에 대해서 Base R, dplyr, data.table 패키지를 사용하여 그룹별로 집계하는 방법(rfriend.tistory.com/624)을 소개하였습니다. 

 

이번 포스팅에서는 dplyr 패키지를 사용하여 두 개의 지리공간 벡터 데이터 테이블을 Join 하는 여러가지 방법을 소개하겠습니다. [1]  Database SQL에 이미 익숙한 분이라면 이번 포스팅은 매우 쉽습니다. 왜냐하면 dplyr 의 두 테이블 간 Join 이 SQL의 Join 을 차용해서 만들어졌기 때문입니다. 

R의 sf 클래스 객체인 지리공간 벡터 데이터를 dplyr 의 함수를 사용해서 두 테이블을 join 하면 속성(attributes)과 함께 지리공간 geometry 칼럼과 정보도 join 된 후의 테이블에 자동으로 그대로 따라가게 됩니다.  

 

(1) Mutating Joins : 두 테이블을 합쳐서 새로운 테이블을 생성하기

    - (1-1) inner join

    - (1-2) left join

    - (1-3) right join

    - (1-4) full join

 

(2) Filtering Joins : 두 테이블의 매칭되는 부분을 기준으로 한쪽 테이블을 걸러내기

   - (2-1) semi join

   - (2-2) anti join

 

(3) Nesting joins : 한 테이블의 모든 칼럼을 리스트로 중첩되게 묶어서 다른 테이블에 합치기

   - (3-1) nest join

 

 

R dplyr 패키지가 두 테이블 Join 을 하는데 제공하는 함수는 inner_join(), left_join(), right_join(), full_join(), semi_join(), anti_join(), nest_join() 의 총 7개가 있으며, 이는 크게 (a) Mutating Joins, (b) Filtering Joins, (3) Nesting Joins의 3개의 범주로 분류할 수 있습니다. 

 

[ R dplyr 패키지로 두 개의 테이블 Join 하기 (Joining two tables together using R dplyr) ]

joining two tables using R dplyr

 

(1) Mutating Joins

Mutation Joins 는 두 개의 테이블을 Key를 기준으로 Join 하여 두 개 테이블로 부터 가져온 (전체 또는 일부) 행과 모든 열로 Join 하여 새로운 테이블을 만들 때 사용합니다. 위의 그림에서 보는 바와 같이 왼쪽(Left Hand Side, LHS)의 테이블과 오른쪽(Right Hand Side, RHD)의 테이블로 부터 모두 행과 열을 가져와서 Join 된 테이블을 반환하며, 이때 왼쪽(LHS)와 오른쪽(RHS) 중에서 어느쪽 테이블이 기준이 되느냐에 따라 사용하는 함수가 달라집니다. 

 

(1-1) inner join

 

먼저, 예제로 사용할 sf 클래스 객체로서, spData 패키지에서 세계 국가별 속성정보와 지리기하 정보를 가지고 있는 'world' 데이터셋, 그리고 2016년과 2017년도 국가별 커피 생산량을 집계한 coffee_data 데이터셋을 가져오겠습니다. "world" 데이터셋은 177개의 관측치, 11개의 칼럼을 가지고 있고, "coffee_data" 데이터셋은 47개의 관측치, 3개의 칼럼을 가지고 있습니다.  그리고 두 데이터셋은 공통적으로 'name_long' 이라는 국가이름 칼럼을 가지고 있으며, 이는 두 테이블을 Join 할 때 기준 Key 로 사용이 됩니다. 

테이블 Join 을 위해 dplyr 패키지를 불러오겠습니다. 

 

## ==================================
## GeoSpatial Data Analysis using R
## : Vector attribute joining
## : reference: https://geocompr.robinlovelace.net/attr.html
## ==================================

library(sf)
library(spData) # for sf data
library(dplyr)

## -- (a) world: World country pologons in spData
names(world)
# [1] "iso_a2"  "name_long" "continent" "region_un" "subregion" "type"  "area_km2"  "pop"  "lifeExp"   "gdpPercap"
# [11] "geom"

dim(world)
# [1] 177  11


## -- (b) coffee_data: World coffee productiond data in spData
## : estimated values for coffee production in units of 60-kg bags in each year
names(coffee_data)
# [1] "name_long"      "coffee_production_2016" "coffee_production_2017"

dim(coffee_data)
# [1] 47  3

 

 

dplyr 패키지의 테이블 Join 에 사용하는 함수들의 기본 구문은 아래와 같이 왼쪽(x, LHS), 오른쪽(y, RHS) 테이블, 두 테이블을 매칭하는 기준 칼럼(by), 데이터 source가 다를 경우 복사(copy) 여부, 접미사(suffix) 등의 매개변수로 구성되어 서로 비슷합니다. 

 

## dplyr join syntax
library(dplyr)

## -- (a) Mutating Joins
inner_join(x, y, by = NULL, copy = FALSE, suffix = c(".x", ".y"), ...)
left_join(x, y, by = NULL, copy = FALSE, suffix = c(".x", ".y"), ...)
right_join(x, y, by = NULL, copy = FALSE, suffix = c(".x", ".y"), ...)
full_join(x, y, by = NULL, copy = FALSE, suffix = c(".x", ".y"), ...)

## -- (b) Filtering Joins
semi_join(x, y, by = NULL, copy = FALSE, ...)
anti_join(x, y, by = NULL, copy = FALSE, ...)

## -- (c) Nesting Joins
nest_join(x, y, by = NULL, copy = FALSE, keep = FALSE, name = NULL, ...)

 

 

inner join 은 두 테이블에서 Key 칼럼을 기준으로 서로 매칭이 되는 행에 대해서만, 두 테이블의 모든 칼럼을 반환합니다. 그럼, "world"와 "coffee_data" 두 데이터셋 테이블을 공통의 칼럼인 "name_long" 을 기준으로 inner join 해보겠습니다.  두 테이블에 공통으로 "name_long"이 존재하는 관측치가 45개가 있네요. 

만약 두 테이블 x, y 에 다수의 매칭되는 값이 있을 경우에는, 모든 가능한 조합의 값을 반환하므로, 주의가 필요합니다. 

dplyr 의 Join 함수들은 두 테이블 Join 의 기준이 되는 Key 칼럼 이름을 by 매개변수에 안써주면 두 테이블에 공통으로 존재하는 칼럼을 Key 로 삼아서 Join 을 수행하고, 콘솔 창에 'Joining, by = "name_long"' 과 같이 Key 를 출력해줍니다. 

 

## -- (1) Mutating Joins
## -- (1-1) inner join
world_coffee_inner = inner_join(x = world,       # LHS
                                y = coffee_data, # RHS
                                by = "name_long" # joining key
                                )

## or shortly
world_coffee_inner = inner_join(world, coffee_data)
# Joining, by = "name_long"


dim(world_coffee_inner)
# [1] 45 13

nrow(world_coffee_inner)
# [1] 45

 

 

(1-2) left join

 

left join 은 왼쪽의 테이블(LHS, x)을 모두 반환하고 (기준이 됨), 오른쪽 테이블(RHS, y)은 왼쪽 테이블과 Key 값이 매칭되는 관측치에 대해서만 모든 칼럼을 왼쪽 테이블에 Join 하여 반환합니다. 만약 오른쪽 테이블(RHS, y)에 매칭되는 값이 없는 경우 x 테이블의 y에 해당하는 행은 NA 로 채워집니다. 

아래 예에서는 왼쪽에 있는 "world" 테이블을 기준으로 오른쪽의 "coffee_data"를 공통으로 존재하는 'name_long' 칼럼을 Key로 해서 left join 을 한 것입니다. 12번째와 13번째 칼럼에 오른쪽 테이블인 "coffee_data" 에서 Join 해서 가져온 "coffee_production_2016", "coffee_production_2017"의 칼럼이 왼쪽 "world" 테이블에 Join 이 되었습니다. 

plot() 함수로 다면(multi-polygons) 기하도형으로 구성된 세계 국가별 지도에 2017년도 커피 생산량을 시각화해보았습니다. 지리기학 벡터 데이터를 Join 했을 때 누릴 수 있는 geometry 칼럼을 사용할 수 있는 혜택이 되겠습니다. 

 

## -- (1-2) left join
world_coffee_left = left_join(world, coffee_data)
# Joining, by = "name_long"

class(world_coffee_left)
# [1] "sf"         "tbl_df"     "tbl"        "data.frame"

names(world_coffee_left)
# [1] "iso_a2"                 "name_long"              "continent"              "region_un"             
# [5] "subregion"              "type"                   "area_km2"               "pop"                   
# [9] "lifeExp"                "gdpPercap"              "geom"                   "coffee_production_2016"
# [13] "coffee_production_2017"

plot(world_coffee_left["coffee_production_2017"])

 

두 테이블을 Join 할 때 기준이 되는 Key 칼럼의 이름이 서로 다른 경우 by 매개변수에 서로 다른 변수 이름을 구체적으로 명시해주면 됩니다. 아래 예에서는 오른쪽 "coffee_data" 테이블의 'name_long' 칼럼 이름을 'nm'으로 바꿔준 후에, by = c(name_long = "nm") 처럼 Join하려는 두 테이블의 서로 다른 이름의 Key 변수들을 명시해주었습니다. 

 

## -- Using the 'by' argument to specify the joining variables
coffee_renamed = rename(coffee_data, nm = name_long)
world_coffee2 = left_join(world, coffee_renamed, 
                          by = c(name_long = "nm")) # specify the joining variables

names(world_coffee2)
# [1] "iso_a2"                 "name_long"              "continent"              "region_un"             
# [5] "subregion"              "type"                   "area_km2"               "pop"                   
# [9] "lifeExp"                "gdpPercap"              "geom"                   "coffee_production_2016"
# [13] "coffee_production_2017"

 

 

(1-3) right join

 

right join 은 오른쪽 테이블(RHS, y) 을 전부 반환하고, 왼쪽 테이블 (LHS, x) 은 오른쪽(y) 테이블과 매칭이 되는 값에 대해서만 모든 칼럼을 Join 해서 반환합니다. Key 칼럼을 기준으로 왼쪽 테이블에 없는 값은 NA 처리가 되어 오른쪽 테이블에 Join 됩니다. (위의 그림 도식을 참고하세요). 

만약 왼쪽과 오른쪽 테이블에 다수의 매칭되는 값들이 있을 경우 매칭되는 값들의 모든 조합으로 Join 됩니다. 아래 예에서 Join 의 기준이 되는 Key 를 명기해주는 매개변수 by = 'name_long' 는 두 테이블에 공통으로 존재하므로 생략 가능합니다. 

 

## -- (1-3) right join: return all rows from y, and all columns from x.
world_coffee_right = right_join(x = world, 
                                y = coffee_data, 
                                by = 'name_long')


dim(world) # -- left
# [1] 177  11

dim(coffee_data) # -- right
# [1] 47  3

dim(world_coffee_right) # -- right join
# [1] 47 13

 

 

(1-4) full join

 

full Join 은 왼쪽 (LHS, x)과 오른쪽(RHS, y)의 모든 행과 열을 반환합니다. 

 

## -- (1-4) full join: return all rows and all columns from both x and y.
world_coffee_full = full_join(x = world, 
                              y = coffee_data, 
                              by = 'name_long')

dim(world_coffee_full)
# [1] 179  13


names(world_coffee_full)
# [1] "iso_a2"        "name_long"   "continent"    "region_un"             
# [5] "subregion"     "type"        "area_km2"     "pop"                   
# [9] "lifeExp"       "gdpPercap"   "geom"         "coffee_production_2016"
# [13] "coffee_production_2017"

 

 

어느 한쪽 테이블에서 버려지는 값이 없으며, 만약 왼쪽이나 오른쪽 테이블에 없는 값이면 "NA" 처리됩니다. 아래의 왼쪽 "world" 테이블과 오른쪽의 "coffee_data" 테이블 간에 서로 매칭되지 않는 부분은 "NA"가 들어가 있음을 알 수 있습니다. 

 

## Where there are not matching values, returns 'NA' for the one missing.
head(world_coffee_full[, c(2:3, 9:13)], 10)

# Simple feature collection with 10 features and 6 fields
# geometry type:  MULTIPOLYGON
# dimension:      XY
# bbox:           xmin: -180 ymin: -55.25 xmax: 180 ymax: 83.23324
# geographic CRS: WGS 84
# # A tibble: 10 x 7
# name_long   continent   lifeExp gdpPercap                                        geom coffee_productio~ coffee_productio~
#   <chr>       <chr>         <dbl>     <dbl>                 <MULTIPOLYGON [arc_degree]>             <int>             <int>
#  1 Fiji        Oceania        70.0     8222. (((180 -16.06713, 180 -16.55522, 179.3641 ~                NA                NA
#  2 Tanzania    Africa         64.2     2402. (((33.90371 -0.95, 34.07262 -1.05982, 37.6~                81                66
#  3 Western Sa~ Africa         NA         NA  (((-8.66559 27.65643, -8.665124 27.58948, ~                NA                NA
#  4 Canada      North Amer~    82.0    43079. (((-122.84 49, -122.9742 49.00254, -124.91~                NA                NA
#  5 United Sta~ North Amer~    78.8    51922. (((-122.84 49, -120 49, -117.0312 49, -116~                NA                NA
#  6 Kazakhstan  Asia           71.6    23587. (((87.35997 49.21498, 86.59878 48.54918, 8~                NA                NA
#  7 Uzbekistan  Asia           71.0     5371. (((55.96819 41.30864, 55.92892 44.99586, 5~                NA                NA
#  8 Papua New ~ Oceania        65.2     3709. (((141.0002 -2.600151, 142.7352 -3.289153,~               114                74
#  9 Indonesia   Asia           68.9    10003. (((141.0002 -2.600151, 141.0171 -5.859022,~               742               360
# 10 Argentina   South Amer~    76.3    18798. (((-68.63401 -52.63637, -68.25 -53.1, -67.~                NA                N

 

 

 

(2) Filtering Joins

 

Filtering Joins 은 두 테이블의 매칭되는 값을 기준으로 한쪽 테이블의 값을 걸러내는데 사용합니다. 

 

(2-1) semi join

 

semi join 은 왼쪽(LHS, x)과 오른쪽(RHS, y) 테이블의 서로 매칭되는 값에 대해 왼쪽(LHS, x)의 모든 칼럼을 반환합니다. 이때 매칭 여부를 평가하는데 사용되었던 오른쪽 테이블(RHS, y)의 값은 하나도 가져오지 않으며, 단지 왼쪽 테이블(x)을 걸러내느데(filtering)만 사용하였다는 점이 위의 (1-2) Left Join 과 다른 점입니다. (위의 도식을 참고하세요)

 

## -- (2) Filtering joins
## -- (2-1) semi join
## : return all rows from x where there are matching values in y, 
## : keeping just columns form x.

world_coffee_semi = semi_join(world, coffee_data)
# Joining, by = "name_long"

dim(world_coffee_semi)
# [1] 45 11

names(world_coffee_semi)
# [1] "iso_a2"    "name_long" "continent" "region_un" "subregion" "type"  "area_km2"  "pop"      
# [9] "lifeExp"   "gdpPercap" "geom"

 

 

(2-2) anti join

 

anti join 은 왼쪽 테이블(LHS, x)과 오른쪽 테이블(RHS, y)의 매칭되는 부분을 왼쪽 테이블(LHS, x)에서 걸러낸 x의 모든 칼럼을 반환합니다. 이때 매칭 여부를 평가하는데 사용되었던 오른쪽(RHS, y) 테이블의 값은 하나도 가져오지 않으며, 단지 왼쪽 테이블(x)을 걸러내는데(filtering)만 사용합니다.

위의 (2-1)의 semi join 은 x와 y의 매칭되는 부분의 x값만을 반환하였다면, 이번 (2-2)의 anti join 은 반대로 x와 j의 매칭이 안되는 부분의 x값만을 반환하는게 다릅니다. (y 값은 안가져오는 것은 semi join 과 anti join 이 동일함.)

 

## -- (6) anti join
## : return all rows from x where there are not matching values in y, 
## : keeping just columns from x.
world_coffee_anti = anti_join(world, coffee_data)
# Joining, by = "name_long"

dim(world_coffee_anti)
# [1] 132  11

names(world_coffee_anti)
# [1] "iso_a2"    "name_long" "continent" "region_un" "subregion" "type"      "area_km2"  "pop"      
# [9] "lifeExp"   "gdpPercap" "geom"

 

 

 

(3) Nesting Joins

(3-1) nest join

 

nest join 은 왼쪽 테이블(LHS, x)의 모든 행과 열을 반환하며, 이때 오른쪽(RHS, y)의 매칭되는 부분의 모든 칼럼의 값들을 list 형태로 중첩되게 묶어서 왼쪽 x 테이블에 join 해줍니다. 즉, 오른쪽 y 테이블의 매칭되는 값들의 칼럼이 여러개 이더라도 왼쪽 x 테이블에 join 이 될 때는 1개의 칼럼에 list 형태로 오른쪽 y 테이블의 여러개 칼럼의 값들이 묶여서 join 됩니다. 

 

## -- (3) Nesting joins
## -- (3-1) nest join
## : eturn all rows and all columns from x. Adds a list column of tibbles. 
## : Each tibble contains all the rows from y that match that row of x. 
world_coffee_nest = nest_join(world, coffee_data)
# Joining, by = "name_long"

dim(world_coffee_nest)
# [1] 177  12

names(world_coffee_nest)
# [1] "iso_a2"      "name_long"   "continent"   "region_un"   "subregion"   "type"        "area_km2"   
# [8] "pop"         "lifeExp"     "gdpPercap"   "geom"        "coffee_data"


head(world_coffee_nest[, 10:12], 3)
# Simple feature collection with 3 features and 2 fields
# geometry type:  MULTIPOLYGON
# dimension:      XY
# bbox:           xmin: -180 ymin: -18.28799 xmax: 180 ymax: 27.65643
# geographic CRS: WGS 84
# # A tibble: 3 x 3
# gdpPercap                                                                                geom coffee_data    
# <dbl>                                                         <MULTIPOLYGON [arc_degree]> <list>         
# 1     8222. (((180 -16.06713, 180 -16.55522, 179.3641 -16.80135, 178.7251 -17.01204, 178.5968 ~ <tibble [0 x 2~
# 2     2402. (((33.90371 -0.95, 34.07262 -1.05982, 37.69869 -3.09699, 37.7669 -3.67712, 39.2022~ <tibble [1 x 2~
# 3       NA  (((-8.66559 27.65643, -8.665124 27.58948, -8.6844 27.39574, -8.687294 25.88106, -1~ <tibble [0 x 2~

 

 

말로만 설명하면 잘 이해가 안될 듯 하여 아래에 nest_join(world, coffee_data) 된 테이블의 아웃풋을 화면 캡쳐하였습니다. nest join 된 후의 테이블에서 오른쪽의 "coffee_data" 라는 1개의 칼럼에 보면 list(coffee_proeuction_2016 = 81, coffee_proeuction_2017 = xx) 라고 해서 "coffee_data" 에 들어있는 2개의 칼럼이 1개의 리스트 형태의 칼럼에 중첩이 되어서 들어가 있음을 알 수 있습니다. 

 

 

다음번 포스팅에서는 Join 했을 때 Join 의 기준이 되는 Key 값이 일부 표준화가 안되어서 제대로 Join 이 안될 경우에 정규 표현식(Regular expression)을 사용해서 Join 하는 방법(rfriend.tistory.com/626)을 소개하겠습니다. 

 

[Reference]

[1] [dplyr] join two tables together: dplyr.tidyverse.org/reference/join.html

[2] [Geocomputation with R] Attritube data operations: geocompr.robinlovelace.net/attr.html

 

이번 포스팅이 많은 도움이 되었기를 바랍니다. 

행복한 데이터 과학자 되세요! :-)

 

반응형
Posted by Rfriend

댓글을 달아 주세요

이번 포스팅에서는 R dplyr 패키지의 case_when() 함수를 이용해서 연속형 변수를 여러개의 범주로 구분하여 범주형 변수를 만들어보겠습니다. dplyr 패키지의 case_when() 함수를 사용하면 여러개의 if, else if 조건절을 사용하지 않고도 벡터화해서 쉽고 빠르게 처리를 할 수 있습니다. R dplyr 의 case_when() 함수는 SQL의 case when 절과 유사하다고 보면 되겠습니다. 




간단한 예제로 1~10 까지의 양의 정수를 "2 이하", "3~5", "6~8", "9 이상" 의 4개 범주로 구분을 해보겠습니다. 

(dplyr::case_when()에서 dplyr:: 는 생략해도 되며, dplyr 패키지의 함수를 이용하다는 의미입니다)


case_when(

조건 ~ 할당값, 

조건 ~ 할당값, 

TRUE ~ 할당값)

의 형식으로 작성합니다. 


아래의 예에서는 조건절이 총 4개 사용되었는데요, if, else if, else if, else 등의 조건절문 없이 case_when() 함수의 괄호안에 바로 조건을 나열했고, 마지막에는 앞의 조건절에 모두 해당 안되는 나머지(else)에 대해서 TRUE ~ "9~" 로 지정을 해주었습니다. 



library(dplyr)


x <- 1:10

x


[1]  1  2  3  4  5  6  7  8  9 10



dplyr::case_when(

  x <= 2 ~ "~2",

  x <= 5 ~ "3~5",

  x <= 8 ~ "6~8",

  TRUE ~ "9~"

)


 [1] "~2"  "~2"  "3~5" "3~5" "3~5" "6~8" "6~8" "6~8" "9~"  "9~" 





이때 조건절의 순서가 중요합니다. 복수의 조건절을 나열하면 앞에서 부터 순서대로(in order) 조건에 해당하는 관측치에 대해 값을 할당하게 됩니다. 따라서 만약 TRUE ~ "9~"를 case_when(() 조건절의 제일 앞에 사용하게 되면 1~10까지의 모든 값에 대해 "9~" 를 할당하게 됩니다. 따라서 조건절의 처리 순서를 반드시 고려해서 조건절을 작성해줘야 합니다. 



# order matters!!!

case_when(

  TRUE ~ "9~",

  x <= 2 ~ "~2",

  x <= 5 ~ "3~5",

  x <= 8 ~ "6~8",

)


[1] "9~" "9~" "9~" "9~" "9~" "9~" "9~" "9~" "9~" "9~"

 




case_when() 조건절의 오른쪽(right hand side)의 데이터 유형이 모두 동일해야 합니다. 만약 데이터 유형이 다를 경우 error를 발생합니다. 가령, 아래 예에서는 오른쪽에 character를 반환하게끔 되어있는데 logical 인 NA 가 포함되는 경우 Error가 발생합니다. 이때는 'NA_character_' 를 사용해서 NA가 character로 반환되게끔 해주면 됩니다. 


  • 오른쪽에 문자형(character) 반환하는 경우 NA 값으로는 NA_character_ 사용

 잘못된 사용 예 (오른쪽 데이터 유형 다름)

 올바른 사용 예 (오른쪽 데이터 유형 같음)


# error as NA is logical not character

case_when(

  x <= 2 ~ "~2",

  x <= 5 ~ "3~5",

  x <= 8 ~ "6~8",

  TRUE ~ NA

)


Error: must be a character vector, not a logical vector

Call `rlang::last_error()` to see a backtrace


# use NA_character_

case_when(

  x <= 2 ~ "~2",

  x <= 5 ~ "3~5",

  x <= 8 ~ "6~8",

  TRUE ~ NA_character_

)


[1] "~2"  "~2"  "3~5" "3~5" "3~5" "6~8" "6~8" "6~8" NA    NA 



  • 오른쪽에 숫자형(numeric)을 반환하는 경우 NA 값으로는 NA_real_ 사용

  잘못된 사용 예 (오른쪽 데이터 유형 다름)

 올바른 사용 예 (오른쪽 데이터 유형 같음)


# error as NA is logical not numeric

case_when(

  x <= 2 ~ 2,

  x <= 5 ~ 5,

  x <= 8 ~ 8,

  TRUE ~ NA

)


Error: must be a double vector, not a logical vector

Call `rlang::last_error()` to see a backtrace


# use NA_real_

case_when(

  x <= 2 ~ 2,

  x <= 5 ~ 5,

  x <= 8 ~ 8,

  TRUE ~ NA_real_

)


[1]  2  2  5  5  5  8  8  8 NA NA




dplyr의 case_when() 함수는 mutate() 함수와 함께 사용하면 매우 강력하고 편리하게 여러개의 조건절을 사용해서 새로운 변수를 만들 수 있습니다. 아래는 mtcars 데이터셋의 cyl (실린더 개수)  와 hp (자동차 마력) 의 두 개 변수를 사용해  첫번째 "or" 조건절로 "big" 유형으로 찾고, 두번째 "and" 조건절로 "medium" 유형을 찾으며, 마지막으로 나머지에 대해서는 "small" 유형을 명명해본 예입니다. 



mtcars$name <- row.names(mtcars)


mtcars %>% 

  select(name, mpg, cyl, hp) %>% 

  mutate(

    type = case_when(

      cyl >= 8 | hp >= 180 ~ "big",          # or

      cyl >= 4 & hp >= 120 ~ "medium", # and

      TRUE ~ "small"

    )

  )


                 name  mpg cyl  hp   type

1            Mazda RX4 21.0   6 110  small

2        Mazda RX4 Wag 21.0   6 110  small

3           Datsun 710 22.8   4  93  small

4       Hornet 4 Drive 21.4   6 110  small

5    Hornet Sportabout 18.7   8 175    big

6              Valiant 18.1   6 105  small

7           Duster 360 14.3   8 245    big

8            Merc 240D 24.4   4  62  small

9             Merc 230 22.8   4  95  small

10            Merc 280 19.2   6 123 medium

---- 이하 생략 ----

 




위에서 R dplyr의 case_when() 함수로 진행했던 내용을 PostgreSQL, Greenplum DB에서 하려면 SQL CASE WHEN 문을 아래처럼 사용하면 됩니다. 참고하세요. 



-- PostgreSQL CASE WEHN


SELECT 

   name, 

   mpg, 

   cyl, 

   hp, 

   CASE 

      WHEN (cyl >= 8) OR (hp >= 180) THEN "big"

      WHEN (cyl >= 4) AND (hp >= 120) THEN "median"

      ELSE "small"

   END AS type

FROM mtcars

 



많은 도움이 되었기를 바랍니다. 

이번 포스팅이 도움이 되었다면 아래의 '공감~'를 꾹 눌러주세요. 



반응형
Posted by Rfriend

댓글을 달아 주세요