11 R 3.2.1 프로그래밍 - 팩터와 테이블의 이해와 활용 R프로그래밍에서의 팩터와 테이블의 개념에 대해 함께 알아 봅시다. 우선, R에서 팩터는 벡터에 추가 정보가 더해진 것으로 보면 됩니다. 추가 정보는 레벨이라고 하여 벡터의 값 가운데 겹치지 않는 값의 기록으로 이루어져 있습니다. 팩터에 사용되는 일반적인 함수로는 tapply(), split() 그리고 by()가 있습니다. R에서 테이블은 어떤 값이나 리스트를 원소로 갖는 이차 배열입니다. table()의 첫 번째 인수는 팩터나 팩터 리스트리고 데이터 인수는 데이터 프레임입니다. 대부분의 수학적 용도가 아닌 행렬, 배열 연산이 데이터 프레임에서 사용될 수 있듯이, 테이블에서도 동일하게 적용할 수 있습니다. aggregate()와 cut()과 같은 함수로 팩터와 테이블을 쉽게 다룰 수 있습니다. 팩터와 테이블의 기초 팩터와 테이블의 기초 팩터는 간단하게 벡터에 추가 정보가 더해진 것으로 보면 됩니다. 하지만 내부적으로 좀 다릅니다. 추가 정보는 벡터의 값 가운데 겹치지 않는 값의 기록으로 이뤄져 이고, 이를 ‘레벨’이라고 합니다. 예제를 살펴볼까요? xf의 고유 값(5,12,13)이 레벨입니다. 좀 더 자세히 살펴보면 내부는 다음과 같습니다. xf의 중심 값은 (5,12,13,12)가 아니라 (1,2,3,2)입니다. 이는 데이터가 첫 번째는 레벨 1, 다음은 레벨 2, 레벨 3, 마지막이 레벨 2의 값으로 이뤄졌다는 뜻입니다. 그러므로 데이터는 레벨값으로 기록되고 있는 것입니다. 레벨 역시 따로 기록되고 있는데요. 이때는 당연히 숫자가 아닌 문자 ‘5’로 기록됩니다. 그래도 펙터의 길이는 레벨의 수가 아닌 데이터의 길이로 정의됩니다. 새 레벨을 추가할 수도 있습니다. 원래 xff는 88을 포함하지 않았지만, 이후에 사용될지도 모르므로 미리 정의해 놓았습니다. 그리고 이후 새 값을 추가했습니다. 비슷한 맥락으로 ‘불법’레벨을 집어 넣을 수 있습니다. 만약 시도할 경우 어떻게 될까요? 팩터에는 apply()군의 또 다른 일원인 tapply()를 사용합니다. 이 함수와 더불어 팩터에 많이 사용되는 두 함수 split()과 by()를 살펴봅시다. 먼저 tapply() 함수부터 살펴봅시다. 이해를 쉽게 하기 위해 투표자 나이 벡터 x와 이 투표자의 지지 정당 같은 비수치적 성향을 보여주는 팩터 f가 있고, 각 정당별로 지지자의 나이 평균을 구하고 싶다고 가정해봅시다. 보통 x에 벡터, f에 팩터나 팩터의 리스트, g에 함수를 넣어 tapply(x, f, g)를 호출합니다. 이 간단한 예제에서 사용할 함수 g()는 R의 내장 함수 mean()입니다. 만약 두 정당과 성별 같은 또 다른 팩터로 묶고 싶다면, 성별과 정당 팩터로 구성된 f가 필요합니다. f의 각 팩터는 x와 길이가 같아야 합니다. 이 투표자 예제라면 당연히 나이의 수와 정당의 수가 같을 것입니다. F의 요소가 벡터로 돼 있다면 as.factor()를 적용해 팩터로 변환해야 합니다. tapply()는 임시로 x를 팩터의 각 레벨에 대응되는 그룹별로 나눈 후 이에 따른 x의 부분 벡터에 g()를 적용합니다. 간단한 예제를 살펴봅시다. tapply() 함수는 벡터(“R”,”D”,”D”,”R”,”U”,”D”)를 “D”,”R”,”U” 레벨의 팩터로 취급합니다. “D”는 2,3,6번째, “R”은 1,4번째, “U”는 5번째에 기록돼 있습니다. 편의를 위해 x,y,z를 인덱스 벡터로 순서대로 (2,3,6),(1,4),(5)로 표현하기로 합시다. 그러면 tapply()는 mean(u[x]), mean(u[y]), mean(u[z])를 계산하고 이 평균을 3개의 원소를 가진 벡터로 반환할 것입니다. 벡터의 각 원소의 이름은 tapply()가 쓰인 팩터의 레벨을 반영해 ‘D’, ’R’, ’U’가 됩니다. 두 개 이상의 팩터일 때는 어떻게 될까요? 각 팩터는 각 그룹을 만들고, 각 그룹 간에 AND 연산이 일어납니다. 예를 들어 성별, 나이, 수입에 대한 변수를 포함하는 경제학 데이터 세트가 있고 수입의 평균이 나이와 성별에 의해 어떻게 나뉘는지 보고 싶다고 해봅시다. 이때 tapply(x, f, g)에서 x는 수입, f는 성별과 해당 사람이25살을 기준으로 아래인지 혹은 위인지가 표기된 두 개의 팩터가 될 것입니다. 이때 만약 g를 mean()으로 한다면 tapply()는 각 네 그룹에 대해서 평균 수입을 반환할 것입니다. 이 설정을 갖고 만든 단순한 예제입니다. 여기서는 성별과 25살 이상인지 여부에 대한 구분자인 두 팩터를 명시했습니다. 각 팩터는 두 개의 레벨을 가지므로, tapply()는 수입 데이터를 성별과 나이의 조합인 4개의 그룹으로 나누고 각 그룹에 대해 mean()을 적용했습니다. 1 tapply()가 벡터를 개별 그룹으로 나누고 각 그룹에 대해 명시된 함수를 적용하는 데 비해, split()은 첫 번째 단계인 그룹을 만드는 데에서 그칩니다. 세부 트릭이 없는 간단한 형태는 split(x,f)입니다. 이 때 x와 f는 tapply(x,f,g)에서의 역할과 유사합니다. x는 데이터 프레임이나 벡터이고 f는 팩터 혹은 팩터의 리스트로서 x를 그룹별로 나눠서 리스트로 반환해 주는 기능을 합니다. 이때 split()의 x에는 데이터 프레임이 쓰일 수 있지만 tapply()에서는 안됨을 기억합시다. 예제를 통해 확인해 볼까요? split()의 결과는 리스트이므로, 각 구성요소는 $로 표기된다는 것을 상기합시다. 그러므로 예를 들어‘M, 1’이라고 된 마지막 벡터는 첫 번째 팩터의 ‘M’과 두 번째 팩터의 ‘1’의 조합 결과를 의미합니다. 다른 예로 전복에 대한 예제를 보겠습니다. 수컷, 암컷, 새끼 전복에 해당하는 벡터의 인덱스를 찾고자 하는데요. 이 예제에서는 간단하게 7개의 관측 값 벡터 (“M”, ”F”, ”F”, ”I”, ”M”, ”M”, ”F”)로 구성된 데이터가 할당된 g를 사용합니다. 이를 가지고 순식간에 split()을 실행할 수 있습니다. 이 결과 암컷의 경우 2,3,7번째, 새끼 전복은 4번째, 수컷은 1,5,6번째 기록에 들어 있다는 것을 알려줍니다. 단계별로 살펴볼까요? 팩터로 처리되는 벡터 g는 ‘M’, ‘F’, ‘I’의 세 레벨로 이뤄져 있습니다. 첫 번째 레벨에 해당하는 인덱스는 1,5,6으로, 이는 g[1], g[5], g[6]은 모두 ‘M’ 값을 갖고 있다는 말입니다. 그러므로 R은 결과값의 M 요소에 1:7 중 1,5,6번째 원소를 할당하는데, 이는 (1,5,6) 벡터로 나타납니다. 텍스트 일치 여부 코드를 간단하게 하는 데에도 비슷한 방법을 사용할 수 있습니다. 텍스트 파일을 넣고 어떤 단어가 텍스트에 있는지를 판단한 다음, 각 단어 별 텍스트 내 위치를 정리한 리스트를 결과물로 내놓는 것입니다. 다음과 같이 split()을 사용해서 이 코드를 보다 짧게 만들 수 있습니다. scan()을 호출하면 tf 파일을 읽어와 나온 단어의 리스트 txt를 반환합니다. 그러므로 txt[[1]]에는 파일의 첫 번째 단어가 들어 있고, txt[[2]]에는 두 번째 단어가 들어 있는 형태가 됩니다. 또한 length(txt)는 읽어 들인 전체 단어의 수를 출력합니다. 오류가 없다면 아마도 값은 220일 것입니다. 한편 split()의 두 번째 인수로 들어간 txt는 팩터로 취급됩니다. 팩터의 레벨은 파일 내의 다양한 단어들이 됩니다. 만약 예를 들어 파일에 world란 단어가 6번 나오고 climate란 단어가 10번 나왔다면, world와 climate는 txt의 두 레벨이 되죠. split()을 호출하면 이 단어나 다른 단어들이 txt에 나타나는지를 판단할 수 있습니다. 이번엔 by()함수에 대해 알아봅시다. 전복 데이터 예제에서 수컷, 암컷, 새끼 전복의 성별을 구분해 길이 대비 지름으로회귀분석을 해봅시다. 처음 보기에 이는 tapply()를 이용해 각각에 맞게 무언가를 할 수 있을 것 같지만, 함수의 첫 번째 인수에 데이터 프레임이나 행렬이 아닌 벡터가 들어가야 한다는 벽에 부딪힙니다. 사용되는 함수는 range() 같이 다변량 함수일 수 있지만, 입력 값은 벡터여야 한다는 벽에 부딪히는 거죠. 회귀분석을 위한 입력값은 예측될 값과 예측 변수로 이뤄진 최소 두 개의 열을 가진 행렬이나 데이터 프레임이어야 합니다. 전복 데이터의 경우에는 행렬은 지름과 길이 열로 이뤄져 있습니다. by()가 여기서 사용됩니다. 이는 tapply()와 비슷한 역할을 하지만, 벡터 대신 객체를 사용합니다. 실제로 내부에서는 tapply()를 호출합니다. 이를 회귀분석 한 것을 클릭하여 확인해보세요. by()를 호출할 때 첫 번째 인수에는 데이터, 두 번째에는 그룹을 만들 팩터, 세 번째에는 각 그룹에 적용할 함수를 넣습니다. 이는 tapply()를 호출할 때와 매우 유사합니다. 단 tapply()에서는 팩터의 레벨에 따라서 벡터의 인덱스를 갖고 그룹을 만드는데, by()는 데이터 프레임 aba의 행 번호를 사용해 그룹을 만듭니다. 이 때 3개의 부분 데이터 프레임이 만들어지는데, 각각 성별 레벨 M, F, I에 대한 것입니다. 앞서 정의한 무명 함수는 행렬 인수 m의 세 번째 열에 대해 두 번째 열을 사용해 회귀분석을 수행합니다. 이 함수는 앞서 만든 세 개의 부분 데이터 프레임에 한 번씩 총 세 번 호출돼, 세 개의 회귀분석 식을 만들게 됩니다. 팩터와 테이블의 응용 팩터와 테이블의 응용 R에서의 테이블 사용하기에 대해 예제를 통해 알아봅시다. 여기서 tapply()는 임시로 u를 부분 벡터로 쪼개, 각 부분 벡터에 ength()를 적용합니다. 이때 u에 뭐가 들어있든 상관없다는 것을 기억합시다. 우리가 보고자 하는 것은 단순히 팩터에 대한 것입니다. 부분 벡터의 길이는 각 두 팩터의 조합인 3*2 = 6가지 경우가 각각 몇 번 발생했는지를 센 것입니다. 예를 들어 5가‘a’의 자리에 두 번 나왔고 ‘bc’의 자리에는 한 번도 나오지 않았으므로 결과의 첫 번째 줄에는 2와 NA가 들어간것입니다. 통계에서는 이를 ‘분할표’라고 합니다. 이 예제에서는 한 가지 문제가 있는데요. 바로 NA 값입니다. 이는 첫 번째에‘5’이고 두 번째 레벨이 ‘bc’인 경우가 하나도 없다는 뜻이므로 실제로는 0이 돼야 합니다. table() 함수는 분할표를 보다 정확하게 만들어 줍니다. table()에서 첫 번째 인수는 팩터나 팩터의 리스트입니다. 여기서 두 팩터는 (5,12,13,12,13,5,13)과 (“a”,”bc”,”a”,”a”,”bc”,”a”,”a”)이죠. 이 경우 팩터로 해석되는 객체는 이를 하나로 세 버립니다. 일반적으로 table()의 데이터 인수는 데이터 프레임으로 해석됩니다. 후보자 X가 재선을 치르는 것에 대한 투표 데이터로 이뤄진 ct.dat 파일이 있다고 가정해봅시다. ct.dat 파일은 이와 같습니다. 일반적인 통계 관점으로 보면 이 파일의 각 행에는 보고자 하는 하나의 값들이 들어 있어야 하는데요. 이 예제에서는 5명에게 X에 투표할 예정인가, 지난 번에 X에 투표했는가의 질문을 했고, 이로부터 데이터 파일에 다섯 개의 행을 얻게 된 것입니다. 자, 그럼 이제 파일을 읽어 들입니다. 그리고 table()을 사용해 이 데이터에 대한 분할표를 만듭니다. 예를 들어 테이블의 왼쪽 위의 2는 두 질문 모두 ‘No’라고 대답했다는 뜻임을 알 수 있습니다. 가운데 줄 오른쪽의 1은 한 사람이 첫 번째 질문에는‘Not Sure’, 두 번째 질문에는 ‘Yes’라고 대답했다는 것을 가리킵니다. 단일 팩터에 대해 계산할 때는 이처럼 1차원의 빈도 수 테이블을 얻을 수 있습니다. 다음은 투표자의 성별, 인종, 정치 관점에 대한 3차원 테이블 예제입니다. R은 2차원 테이블 여러 개로 3차원 테이블을 출력합니다. 이 경우 보수 성향에 대한 성별과 인종 테이블을 생성하고 진보 성향에 대해서도 동일하게 생성합니다. 예를 들어 두 번째의 2차원 테이블을 보면 진보 성향인 두 백인 남자가 있음을 알 수 있습니다. 대부분의 수학적 용도가 아닌 행렬, 배열 연산이 데이터 프레임에서 사용될 수 있듯이, 테이블에서도 동일하게 적용할 수 있습니다. 예를 들어 행렬의 방식으로 테이블 셀에 접근할 수 있습니다. 투표 예제에 이를 적용해 볼까요? 첫 번째 명령어 호출 시 cttab이‘테이블’ 클래스라는 것을 확인했음에도 불구하고 두 번째 명령어에서 마치 행렬인 양 이 테이블의 ‘[1,1]의 원소’를 출력했습니다. 이 개념 그대로 세 번째 명령어에서는 이 ‘행렬’의 첫 번째 행을 출력했습니다. 테이블에서는 행렬과 상수를 곱할 수 있습니다. 예를 들어 셀 별 수치를 비율로 변환하는 법은 다음과 같습니다. 통계에서 변수의 중간합 값은 이 변수에 해당하는 값들을 모두 더해 계산할 수 있습니다. 투표 예제에서 Vote.for.X.의 중간합 값은 2+0=2, 0+1=1, 1+1=2입니다. 물론 이는 행렬에 apply()를 적용해 바로 계산할 수 있습니다. 여기서 No 같은 라벨은 table()에서 만들어 낸 행렬의 행 이름에서 나온 것임을 염두해 둡시다. R에서는 중간합 값을 계산해 주는 addmargins()라는 함수를 제공합니다. 예제를 보면 이렇게 두 가지 차원에 대한 중간합 값을 한 번에 편하게 확인합니다. 각 차원 및 레벨의 이름은 dimnames()로 찾을 수 있습니다. 투표 예제를 활용해 봅시다. 이번 선거에서 X에게 투표할 것이라고 응답한 사람들을 위한 회의에서 이 데이터를 보여줄 것이라고 가정해 봅시다. 그래서 Not Sure 항목을 제외하고 다음과 같은 부분 테이블을 보여주려고 합니다. subtable() 함수는 부분 테이블 추출 기능을 하는데요. 이 함수에는 tbl과 subnames의 두 인수가 필요합니다. tbl은 클래스를 갖고자 사용하고자 하는 테이블을 말하며, subnames는 부분 테이블 추출 시에 사용하고자 하는 리스트를 말합니다. tbl의 각 차원을 이름으로 하는 리스트의 각 구성 요소에는 필요한 레벨의 이름이 들어갑니다. 코드를 보기 전에 예제를 한번 다시 훑어 봅시다. cttab의 인수에는 각 차원의 이름이 Voted.for.X와 Voted.for.X.Last.Time인 2차원 테이블이 들어 갈 것입니다. 첫 번째 차원의 레벨 이름은 No, Not Sure, Yes이고 두 번째는 No와 Yes입니다. 이 중 Not Sure는 제거하고 싶으므로, 형식 인수 subnames에 해당하는 실인수는 이렇습니다. 그럼 이제 이 함수가 어떤 일을 수행했는지 자세히 살펴봅시다. 코드를 작성하기 전에 테이블 클래스 객체의 구조가 어떻게 이뤄졌는지 간단히 탐색해 보았습니다. table() 함수의 코드를 살펴보니 테이블 클래스 객체는 각 셀의 횟수를 원소로 갖는 배열로 이뤄졌다는 중요한 사실을 발견했습니다. 그러므로 원하는 부분 배열을 추출해 각 부분 배열의 차원에 대해 이름을 붙이고, 그 결과를 테이블 클래스로 만들면 된다는 전략이 나옵니다. 코드를 만들기 위해 첫 번째로 해야 할 일은 각 사용자가 원하는 부분 테이블에 해당하는 부분 배열을 구성하고, 이를 대부분의 코드에 사용하는 것입니다. 이를 위해 3번째 줄에서 일단 전체 셀의 횟수 배열을 추출해 이를 tblarray에 저장합니다. 그럼 이것으로 원하는 부분 배열을 어떻게 찾을까요? 원하는 부분 배열을 얻기 위해 tblarray 배열의 부분 집합을 만드는 식을 이렇게 만듭니다. 여기서는 투표 예제를 적용해 이렇게 쓸 수 있습니다. 개념상으로는 매우 간단하지만 실제로 tblarray는 서로 다른 차원으로 구성될 수 있으므로 간단히 사용하기는 어렵습니다. R의 배열은 실제로 ‘[‘() 함수에 의해 쓰여집니다. 이 함수는 다양한 수의 인수를 가지는데요. 2차원 배열일 경우에는 두 개, 3차원 배열에는 세 개가 쓰이는 식입니다. 이 문제는 R의 do.call()을 사용해 해결할 수 있습니다. 여기서 f는 함수이고 argslist는 f()에 사용되는 인수의 리스트입니다. 즉 이 코드는 이런 식으로 쓸 수 있습니다. 이로 인해 인수의 수가 다양하게 사용되는 함수를 보다 쉽게 호출할 수 있습니다. 이 예제를 적용하기 위해서는 tblarray의 차원 이름과 각 차원에서 필요한 레벨들로 구성된 리스트 형태가 필요합니다. 이는 그렇게 구성된 리스트입니다. 7번째부터 11번째 줄에서는 이런 리스트를 만드는 과정을 일반화하는데요. 이것이 부분 배열입니다. 그 후 이름을 부여하고 테이블 클래스로 변환해 줍니다. 전자의 경우는 다음 인수들을 사용해 R의 array()를 적용합니다. 이는 실제로 작성하기에는 개념적으로 좀 복잡한 함수지만 테이블 클래스의 내부 구조를 완벽하게 한번 깨우친다면 훨씬 쉽게 할 수 있을 것입니다. 행이나 차원이 매우 큰 테이블을 보는 것은 어려운 일입니다. 이 경우 가장 많이 나타나는 셀들에 초점을 맞추는 것이 하나의 방법인데요. tabdom() 함수는 테이블에서 가장 자주 나타나는 셀을 알려줍니다. 간단히 호출해 볼까요? 이 경우 tbl 테이블에서 k개의 높은 빈도의 셀을 알려줍니다. 예제를 보면, 이 코드는 5와 12가 d에서 각 4번씩으로 가장 많이 나타나고, 다음은 4가 2번 나타난다고 알려줍니다. 투표 예제의 cttab 테이블에 적용해 보면, No-No가 두 번으로 가장 자주 나왔고, 두 번째로 Yes-No가 한번 나왔습니다. 그럼 어떻게 이런 결과가 나왔을까요? cttab 테이블을 다시 사용해 봅시다. cttab을 만들 때 나온 데이터 프레임 ct는 원래 데이터 프레임이 ‘아니다’라는 것을 기억하시기 바랍니다. 이는 테이블 그 자체가 다르게 표현된 것일 뿐입니다. 여기에는 각 팩터의 조합이 한 행에 들어가 있고, 각 조합의 인자의 수를 보여주는 Freq 열이 추가됐습니다. 이 열을 사용해 우리가 하고자 하는 것을 보다 빠르게 할 수 있습니다. 주석은 코드를 보다 명확하게 볼 수 있게 작성해야 합니다. order()를 사용한 정렬 방식은 데이터 프레임을 정렬하는 기본적인 방식으로, 정렬이 필요한 상황이 꽤 자주 발생하므로 기억해 둘 필요가 있습니다. 여기서 사용한 테이블을 데이터 프레임으로 변경하는 방식은 셀에 0이 들어가는 경우를 피하고 싶다면 팩터에서 레벨을 제거할 때 조심해야 합니다. R은 테이블과 팩터를 쉽게 다룰 수 있는 수많은 함수를 갖고 있습니다. 우리는 이 중 aggregate()와 cut()의 두 함수에 대해 다룰 것입니다. aggregate() 함수는 그룹의 각 변수 별로 tapply()를 한 번씩 호출합니다. 예를 들어 전복 데이터에서는 성별로 나뉜 각 변수 별 중간값을 구할 수 있습니다. 첫 번째 인수인 aba[,-1]은 첫 번째 열인 Gender를 제외한 전체 데이터 프레임이고,두 번째 인수는 Gender 팩터로 리스트 형이며, 마지막으로 세 번째 인수는 R이 팩터별로 부분그룹으로 나눠진 각 데이터 프레임에 대해서 각 열의 중간값을 계산하라고 알려주는 역할을 합니다. 이 예제에서는 3개의 부분그룹이있으므로 aggregate()의 결과값으로 3개의 행이 나오게 됩니다. 테이블을 위해 팩터를 생성하는 일반적인 방법은 cut() 함수를 사용하는 것입니다. 데이터 벡터 x와 b에 의해 정의된 데이터 집합들을 넣어줍니다. 그러면 각 집합이 X의 원소 중 어디에 속하게 되는지를 알려줍니다. 여기서 사용할 함수 호출 방식은 이렇습니다. 다음 예제를 봅시다. 여기서 z[1]인 0.88114802는 (0,0,0.1)이었던 9번 집합에 떨어지고, z[2]는 0.28532689로 3번 집합에 떨어지는 식입니다. 이 함수는 예제에서 보다시피 [#8] 결과값으로 벡터를 출력합니다. 하지만 이를 팩터로바꿀 수 있고, 테이블을 만드는 데에도 이를 사용할 수 있습니다. 예를 들어 사용자 정의 히스토그램 함수를 작성하는데 이 기능을 사용한다고 상상해 봅시다. R의 findInterval()도 이를 위해 유용할 것입니다.1. 팩터와 레벨
2. 팩터에 사용되는 함수
1. 테이블 사용하기
2. 기타 관련 함수
'R프로그래밍' 카테고리의 다른 글
R 3.2.1 프로그래밍 - 데이터 프레임의 결합과 적용 (0) | 2016.05.13 |
---|---|
R 3.2.1 프로그래밍 - 데이터 프레임의 생성과 연산 (0) | 2016.05.13 |
R 3.2.1 프로그래밍 - 리스트의 적용과 재귀 리스트 (0) | 2016.05.13 |
R 3.2.1 프로그래밍 - 리스트의 생성과 연산 (0) | 2016.05.13 |
R 3.2.1 프로그래밍 - 행렬과 배열의 활용 (0) | 2016.05.10 |