2019년 2월 3일 일요일

[하루에 한 R] Key/value 형태의 자료를 이용한 치환 - named vector 이야기

Perl에서는 hash라는 자료형을 이용하여 key/value 형태의 자료를 다룬다.


$color_of{apple} = 'red';
# initialize a hash
my %color_of = (
    "apple"  => "red",
    "orange" => "orange",
    "grape"  => "purple",
);

R에서는 이와 유사한 자료 구조로서 list가 있지만 hash보다는 훨씬 복잡한 구조의 데이터를 담을 수 있다. 예를 들어서 list_data = list("red","green",c(1,2,4), TRUE,13.43)의 사례에서 볼 수 있듯이 문자열, 숫자, 벡터, 논리값 등 어떤 타입의 원소든지 담을 수 있다. 물론 Perl의 hash에서도 참조(reference)를 이용하면 어떤 자료형이든 scalar로 전환되므로 hash의 값으로 저장할 수 있다. Perl의 hash에서는 각 원소에 접근하려면 key를 사용해야 되지만, list는 훨씬 다양한 방법으로 인덱싱하여 접근하거나 혹은 슬라이싱이 가능하다.

이번 글에서는 R에서 key/value 목록을 사용하여 간단하게 데이터를 변환하는 방법을 알아보고자 한다. 알파벳 소문자 a, b, c, d, e를 NATO 음성 문자(NATO phonetic alphabet) alpha, bravo, charlie, deltam echo로 변환하는 아래의 사례에서는 list를 쓰지 않고도 named vector를 사용하고 있다. Perl의 사고 체계와는 약간 다르다는 것을 이해해야 한다. Perl에서는 $hash{key} = value의 형태로 자료를 정의하므로 특정 key에 해당하는 value를 확인하는 과정이 복잡하지 않다. 그러나 R의 named vector에서는 names(var)[var=="key"] 구문을 사용해야 key에 대응하는 value가 반환된다. 언뜻 난해해 보인다.

> abcde = c("a","b","c","d","e")
> nato.abcde = c("Alpha","Bravo","Charlie","Delta","Echo")
> names(abcde) = nato.abcde
> abcde
  Alpha   Bravo Charlie   Delta    Echo 
    "a"     "b"     "c"     "d"     "e" 
> names(abcde)[abcde=="a"]
[1] "Alpha"
> data = c("e","e","a","b","c","d")
> for(i in data){print(names(abcde)[abcde==i])}
[1] "Echo"
[1] "Echo"
[1] "Alpha"
[1] "Bravo"
[1] "Charlie"
[1] "Delta"

내용상으로 value에 해당하는 것이 names() 함수의 값으로 반환되는 것이 좀 어색하다. 왠지 names()로 지정 또는 반환되는 값은 key에 해당하는 것이 더 자연스러울 것이라는 생각이 든다. 다시 말해서 "a"를 입력했을 때 "Alpha"를 얻고자 한다면, 다음 중 어느 것이 더 자연스럽겠는가? 두 경우에서 myVar 벡터의 구조는 서로 정반대이다.

  1. result = myVar["a"] 
  2. result = names(myVar)[myVar=="a"]

두말할 나위도 없이 (1)번이 더 직관적이다. 하지만 이 글의 윗부분에서 든 사례에서는 (2)의 방식을 택하고 있다. 어떤 방식이 더 낫다고는 할 수 없다. 다만 어떤 벡터 원소의 이름은 그 원소의 속성 중 하나라고 이해하면 (2)의 방법도 수긍이 간다. 위에 보인 R 코드에서 abcde를 입력했을 때 화면으로 출력되는 자료의 모습을 한번 음미해 보자. Alpha, Bravo, Charlie...는 마치 데이터프레임의 column label과 흡사하다. 이러니 혼동을 불러 일으키는 것도 당연하다.

좀 더 현명한 방법은 아래와 같이 list를 사용하는 것이다. setNames() 함수를 사용하면 리스트를 생성하면서 동시에 이름을 부여할 수 있다(괄호 안에서 as.list() 함수를 쓰지 않으면 단순한 named vector가 된다). 여기에서는 names()가 가리키는 방향이 위의 named vector의 사례와는 반대임을 유의해야 한다. 즉, key에 해당하는 것을 가리킨다.

> myList = setNames(as.list(nato.abcde),abcde)
# myList = list(a="Alpha",b="Bravo",c="Charlie",d="Delta",e="Echo")
# add a new key/value element > myList$f = "Foxtrot" > myList[["h"]] = "golf" > names(myList)[names(myList)=="h"] = "g" > for (i in ls(myList)) {print(myList[[i]])} [1] "Alpha" [1] "Bravo" [1] "Charlie" [1] "Delta" [1] "Echo" [1] "Foxtrot" [1] "golf" > for (i in data) {print(myList[[i]])} [1] "Echo" [1] "Echo" [1] "Alpha" [1] "Bravo" [1] "Charlie" [1] "Delta"

'펄(Perl)'과 '알(R)'의 짤막한 비교언어학이었다.

[업데이트 2019-02-18] 실용적인 사례


위에서 보인 예문에서는 for 반복문 안에서 단지 print를 이용하여 값을 화면에 인쇄한 것에 지나지 않는다. 실제로는 data = c("e","e","a","b","c","d")라는 벡터가 주어졌을 때 각 원소를 이에 대응하는 "Echo"    "Echo"    "Alpha"   "Bravo"   "Charlie" "Delta"로 바꾸어 벡터로 반환해 주어야 비로소 쓸모가 있다. Named vector를 사용하여 간단하게 처리하는 코드를 소개해 본다. Named vector 'abcde'는 위에서 정의한 것을 그대로 사용한다. 변환 작업을 할 벡터 data가 주어졌을 때 여기에 원소와 동일한 이름을 붙이는 것이 핵심이다.

> abcde = c("a","b","c","d","e")
> nato.abcde = c("Alpha","Bravo","Charlie","Delta","Echo")
> names(abcde) = nato.abcde
> data = c("e","e","a","b","c","d")
> names(data) = data
> for (i in data) {
+ temp = names(abcde)[abcde==i]
+ names(data)[names(data)==i] = temp
+ }
> names(data)
[1] "Echo"    "Echo"    "Alpha"   "Bravo"   "Charlie" "Delta"  

위의 방법에서는 data vector의 이름을 변경하였다. 이름과 값이 동일하다면, 값을 바꾸어도 된다. 맨 마지막 행에서는 unname() 함수를 사용하여 data 벡터의 이름을 제거해야 원하는 결과가 나온다.

> data = c("e","e","a","b","c","d")
> names(data) = data
> for (i in data) {
+ temp = names(abcde)[abcde==i]
+ data[names(data)==i] = temp
+ }
> unname(data)
[1] "Echo"    "Echo"    "Alpha"   "Bravo"   "Charlie" "Delta"  

named vector에서는 이름이 같아도 문제가 되지 않는다. 아마 data frame에서는 곤란할 것이다.

댓글 없음: