레이블이 R-project인 게시물을 표시합니다. 모든 게시물 표시
레이블이 R-project인 게시물을 표시합니다. 모든 게시물 표시

2018년 1월 31일 수요일

[하루에 한 R] PhyloSift 결과 종합하기(merge 함수로 tab-delimited CSV 파일의 일괄 입력 및 병합 등)

Species boundary를 살짝 넘어가는 박테리아 유전체 50개 정도를 가지고 whole genome phylogenetic tree를 그려보고자 하였다. Harvest suite로 분석을 하면 MUMi distance cutoff에 미치지 못하는(default = 0.1) 몇 개가 빠져나가고, Roary로는 아예 core gene이 잡히질 않았다(뭘 잘못했나?). 상위 genus에서 폭넓게 분포하는 어떤 마커들의 분포(LS-BSR로 자료 생성)를 시각화하는 것이 목표라서 tree는 아주 정확할 필요가 없다. 다만 모든 샘플을 아우를 정도로만 윤곽을 잡아주면 된다.

16S rRNA gene sequence로는 해상도가 좋은 트리가 나오기 어려울 것이 자명하다. 그래서 택한 도구는 PhyloSift였다. 이 도구는 원래 shotgun metagenomic read로부터 phylogenetic distribution을 알아보기 위하여 개발된 것이지만, 단일 유전체 어셈블리로부터 universal marker(목록; 원래 40개였으나 실제로 쓰이는 것은 37개)를 찾아낼 수 있으므로 미생물 유전체 조립의 완성도를 평가하는데 쓰이기도 한다. 더욱 좋은 것은 alignment까지 된 상태의 서열이 생성되므로 이를 잘 이용하면 tree를 쉽게 그릴 수 있다. PhyloSift가 metagenome sample로부터 만들어내는 tree와는 다른 것이니 주의가 필요하다.

Bacillus_subtilis_168.fa라는 유전체 파일을 이용하여 PhyloSift를 실행했다고 가정하자(search 및 align mode). 그러면 PS_temp/Bacillus_subtilis_168.fa/ 아래에 alignDir 및 blastDir가 생기고, 그 하위에 각 마커에 대한 파일이 쌓인다. marker_summary.txt는 다음과 같이 생겼다.


각 유전체 서열마다 별도로 작성된 이 파일을 병합하여 정리하면 모든 마커가 전부 확인되었는지의 여부를 쉽게 알 수 있을 것이다. 오늘의 고행은 여기서부터 시작되었다. Perl을 사용하면 어떻게 해서든 결과를 얻을 수 있었겠지만, 왠지 R을 쓰고 싶었다. R을 이용하여 코드를 짜 놓으면 재사용성이 높을 것 같았기 때문이었다.

marker_summary.text file의 정리


각 유전체 서열에 대해서 별도의 디렉토리에 나뉜 marker_summary.txt 파일을 한 곳으로 모으되 균주명이 파일의 제목이 되게 바꾸었다. Shell script를 써서 다음과 같이 구현하였다(짧은 코드인데도 공부할 것이 많아서 2019년 3월 8일 설명과 함께 업데이트). 이 스크립트에서는 완벽하지 않다. 털어내야 할 FASTA 파일의 확장자(여기에서는 fasta)를 실제 상황에 맞게 바꾸어야 하고, 출력파일을 저장할 OUTDIR 환경변수를 미리 선언하고 해당 디렉토리도 만들어 두어야 한다. 그리고 for block의 처음에 나오는 <<<도 이해하기가 조금 어렵다. f 변수에는 aaa/bbb/ccc...라는 형태의 값이 저장되는데, 이를 슬래쉬로 구분한 다음 두번째 것인 bbb를 택하겠다는 의미이다. 만약 <<<쓰지 않으면 cut은 f 변수가 가리키는 파일의 내용을 대상으로 작업을 한다. 이 명령행은 FILE=$(echo $f | cut -d'/' -f2)로 바꾸 실행해도 완전히 동일한 결과를 낸다. 16s_reps_bac과 DNGNGWU* 마커에 대한 결과를 같이 출력하고 싶다면 egrep "16s_reps_bac|DNGNGWU" 명령을 써라.
FILES=`find $1 -name marker_summary.txt -type f | grep blastDir`
for f in $FILES
do
        FILE=$(cut -d'/' -f2 <<<$f)
        FILE=${FILE%.fasta}
#        cp $f ${OUTDIR}/${FILE}
        grep DNGNGWU $f > ${OUTDIR}/${FILE}
done
find로 찾아낸 파일명(경로 포함)은 다음과 같다. 빨강색 문자열만을 뽑아내는 것이 포인트이다. cut 명령을 사용하여 슬래쉬(/)를 기준으로 분리한 두번째 문자열을 취한 다음 뒷부분의 .fasta를 제거해야 한다.

PS_temp/Paenibacillus_sp._UNCCL52_GCF_000686825.1.fasta/blastDir/marker_summary.txt

오늘의 시도에서는 파일 내부를 grep으로 뒤져서 DNGNGWU...로 시작하는 표준 마커에 대한 결과만을 모았다. match가 없는 마커의 경우 0이 표시되는 것이 아니라 아예 summary file에 존재하지 않는다. 이것이 나중에 R에서 병합 작업을 하는 것을 약간 어렵게 하였다. 만약 모든 마커에 대한 결과가 다 있었다면, 마커의 순서가 동일한지만 확인한 뒤에 cbind()를 하는 것으로 쉽게 끝났을 것이다.

[R] for loop를 이용한 텍스트 파일의 일괄 입력 및 outer join


이러한 용도의 R 패키지가 이미 개발된 것이 있을지도 모르지만 되도록 R의 기본 기능을 충실하에 이용하여 구현하고자 하였다. 먼저 정리된 파일이 모여있는 디렉토리로 이동하여 R을 실행하였다. 다른 곳에서 R을 실행한 뒤 setwd() 함수를 실행하여 파일 위치를 지정하여도 좋다. 파일의 일괄 입력에서는 Import multiple files to R의 예제를 거의 그대로 따랐다.
list.filenames = list.files()
# 특정 확장자를 지닌 파일만 읽어들이려면 list.files(pattern="*.txt")
# create an empty list
list.data=list()
length(list.filenames)
# create a loop to read in your data
for (i in 1:length(list.filenames))
{
# 이중 대괄호 안에 인덱스를 넣어야 데이터프레임을 부르는 것이 된다.
list.data[[i]] = read.table(list.filenames[i],sep=",",header=F)
colnames(list.data[[i]]) = c("markers", list.filenames[i])
}
# 다음의 두 명령은 결과가 약간 다르다.
# 궁금하면 class() 함수를 써 보라.
list.data[1]
list.data[[1]]
# add the names of your data to the list
names(list.data) = list.filenames
# name으로 사용된 파일명에서 확장자를 제거하려면 방법을 따로 알아보아라.
다음으로는 데이터 프레임을 join해야 한다. 기준이 되는 컬럼은 markers이다. outer join을 하였으므로 값이 없는 경우에는 NA가 채워지고, 이는 나중에 0으로 채우면 된다. R에서의 join 혹은 merge에 대한 개념은 여기를 참고하였다. 국문 페이지로는 "[R Friend] R 데이터프레임 결합: rbind(), cbind(), merge()"가 매우 유용하다. merge 함수는 두 개의 데이터프레임을 대상으로 한다. 만약 인덱스로 접근 가능한 데이터프레임이 있는데 총 50개가 있다면 이를 어떻게 합치겠는가? 1번과 2번을 merge하여 임시 데이터프레임을 거쳐서 2번에 저장한다(한꺼번에 저장하는 것이 더 간단할지도 모른다). 그 다음 인덱스를 1 증기시키고 2번과 3번을 merge하여 3번에 저장한다. 이를 인덱스=(전체 데이터프레임 수 - 1)이 될 때까지 for loop를 돌린 다음 종료하면 된다.

# full outer join
for (i in 1:(length(list.filenames)-1))
{
df = merge(x=list.data[[i]],y=list.data[[i+1]],by="markers",all=TRUE)
list.data[[i+1]] = df
}
df = list.data[[length(list.filenames)]]
df[is.na(df)] = 0
rownames(df) = df[,1]
df = df[,-1]
colnames(df) = list.filenames
# 최종 확인
dim(df)
View(df)

View(df)의 결과 화면(일부).
첫번째 컬럼(markers)를 row name으로 전환하여 새로운 데이터프레임 df.2에 저장하자.
> df.2 = data.frame(df[,-1], row.names=df[,1])
> dim(df.2)  # 컬럼 수가 하나 줄어들었다.

결과 점검하기


단 하나라도 마커가 발견되지 않은 유전체는 어느 것일까? 또 어떤 마커가 발견되지 않았을까? 마커의 총 수가 37개라고 해서 컬럼의 합을 가지고 판단의 기준을 삼을 수는 없다. 왜냐하면 주어진 마커가 2회 발견되는 경우도 있을 수 있기 때문이다. apply()함수를 사용하면 이를 매우 쉽게 찾아낼 수 있다. 

각 row에 대해서 어느 하나라도 0이 존재하는지를 알아보자. 빨강색으로 표시한 마커가 TRUE를 나타내고 있다. 바로 저 row name만을 반환할 수 없을까?


> apply(df.2 == 0, 1, any) # row에 대하여
> apply(df.2 == 0, 2, any) # column에 대하여
위 명령어를 입력한 다음 눈으로 찾을 수는 없는 노릇이다. 정답은 다음과 같다.
> rownames(df.2)[apply(df.2 == 0, 1, any)]
[1] "DNGNGWU00040"
> colnames(df.2)[apply(df.2 == 0, 2, any)]
[1] "Paenibacillus_polymyxa_TD94_GCF_000520775.1"
오늘 하루가 다 지나갔다! 데이터프레임에서 특정 조건을 만족하는 서브셋을 추출하는 것도 중요하지만, 그 조건에 만족하는 column 혹은 row의 이름을 알아내는 것도 중요하다. 

2017년 12월 19일 화요일

[하루에 한 R] 그래픽스 생기초 - plot area와 margin 등의 이해

R GUI에서 그림을 그리면 기본적으로 정사각형의 창이 뜨면서 그림이 그려진다. 백문이 불여일견이니 한번 해 보자.
> plot(1:10)

창의 가장자리에 마우스를 대고 적당히 끌어서 크기와 종횡비(aspect ratio)를 조절한 다음 화면을 캡쳐하든지(해상도가 낮은 그림이라면 이 정도로도 무방), 혹은 마우스 오른쪽 버튼을 눌러서 나오는 팝업 메뉴에서 복사(메타파일 또는 비트맵)를 하거나 파일로 저장(메타파일 또는 eps)을 하면 된다. 메타파일로 복사를 하면 파워포인트에서 붙여넣기를 할 수 있다.

그런데 마우스로 드래그하여 조정한 상태를 유지하면서 다른 종류의 그래픽 파일(png, pdf...)로 출력하려면 어떻게 해야 하는가? 조정한 상태의 전체 크기와 마진이 얼마인지를 알아야 이것이 가능하다. 그러면 먼저 마진(margin)의 개념을 이해하도록 하자. 마진이란 위에서 보인 그림에서 네모로 둘러친 부분(내부는 plot region)을 둘러싼 바깥 네 부분의 여백을 뜻한다. 마진은 X-/Y-축 설명과 타이틀 등이 위치하는 곳이다. outer margin도 있지만 기본 크기는 0으로 정의된다.

기본적으로 생성되는 그래픽 창의 크기는 7 x 7인치이다. 이것을 어떻게 알아내는가? dev.size() 또는 dev.size("cm")라고 입력한 뒤 출력되는 값을 보면 된다. 앞의 것은 인치, 뒤의 것은 센티미터 단위이다. dev.new()를 입력하여 그래픽스 창을 하나 띄운 다음 마우스로 드래그하여 크기를 바꾼 뒤 dev.size()를 실행해 보라. 조정된 크기가 R prompt에 나타날 것이다.

만약 랜드스케이프 형태(레터 용지의 크기)의 그림을 얻고 싶다면 다음과 같이 하라. 단위를 mm 또는 cm로 바꾸는 방법도 어디엔가는 있을 것이다. width와 height를 지정하는 숫자에 따옴표를 둘러싸는 실수를 하면 안된다.
> dev.new(width=11.0,height=8.5) # letter landscape
> dev.new(width=11.692,height=8.267) #A4 landscape
Plot을 파일로 저장하려면 그래픽스 디바이스를 연 뒤 다음과 같이 하면 된다.
> png("file.png")
> plot(...)
> dev.off()
마진의 크기는 mar(단위는 라인) 또는 mai(단위는 인치) 벡터로 확인할 수 있으며, plot region의 크기는 pin(단위는 인치) 벡터로 같은 일을 할 수 있다. 마진 벡터의 각 원소는 plot의 아래쪽으로부터 시계방향으로 돌아가면서 크기를 나타낸다. 즉 아래, 왼쪽, 위, 오른쪽의 순서인 것이다. 7 x 7 인치의 기본 그래픽스 디바이스를 띄운 다음 마진과 plot region의 크기를 확인해 보자.
> graphics.off()
> par()$mar
[1] 5.1 4.1 4.1 2.1
> par()$mai
[1] 1.02 0.82 0.82 0.42
> par()$pin
[1] 5.759999 5.149582
그래픽스 화면을 드래그하면 이들 수치는 어떻게 될까? 일단 창 안에 그림이 있어야 하니 간단하게 plot(1:10)이라고 입력을 하여 플롯을 얻은 다음 마우스로 창의 크기를 이리저리 바꾸어 본다. 그 다음에 par()$mai와 par()$pin을 각각 입력해 보라. 어떤가? 마진은 그대로이지만 plot region의 크기를 나타내는 pin의 값은 변했다. 실제 마우스로 그림의 크기를 변화시키면 마진의 크기, 글꼴, 라벨 등은 그대로인데 plot region 내부만 변형이 됨을 알 수 있다. 즉 마진의 설정이 우선하는 것으로 생각하면 된다. 그림을 너무 작게 줄여서 위 아래 마진의 합이 그림의 높이보다 큰 상태가 되면(좌우 마진도 마찬가지) ""figure margins too large"라는 메시지가 그래픽 디바이스 창에 나타난다. 그림을 줄여도 마진 영역은 그대로 지켜야 하니 plot region이 확보되지 않음을 의미하는 것이다.

마구 조정을 한 다음의 그림 전체 크기는 얼마일까? par()$fin으로 확인 가능하다. 그래픽스 디바이스를 끈 다음 다음의 명령을 실행해 보자. 기본 그래픽스 창의 크기를 표시할 것이다.
> par()$fin
[1] 6.999999 6.989582
그렇다. 대략 7 x 7인치에 해당하는 것이다. 이상의 작업을 통해서 내가 그리려는 적절한 그림의 크기와 마진을 알아냈다고 가정하자. 그러면 이를 par() 함수에 설정해 넣으면 된다. 마진의 기본 크기는 (5,4,4,2) + 0.1이다(단위는 인치가 아님). 다음의 예제에서는 크기와 마진을 임의로 설정한 플롯을 그려서 pdf 파일에 출력하는 방법을 보여주고 있다.
pdf("test.pdf",width=11,height=8.5)
par(mai=c(4,4,2,2))
plot(1:10,xlab="x label",ylab="y label",main="Main title",sub="sub title")
dev.off()
빨간 숫자는 마진의 크기를 나타낸다.

ggplot2를 쓰면 화려한 그림을 통해서 복잡한 데이터에 숨은 의미를 찾는 길이 더욱 쉬워진다. 그렇다 하더라도 plot의 크기와 마진에 대한 기본적인 조작 방법을 파악하는 것도 중요하다. 

gplots에서 heatmap.2() 레이아웃 변경


Moving color key in R heatmap.2를 참조하면 color key의 위치와 크기를 변경할 수 있다. Row 및 Col label을 잘 보이게 하려면 cexRow/cexCol 인수를 줄이는 것도 좋지만 heatmap.2(x, margins=c(5,20))과 같이 margins 인수를 써도 된다. 앞의 것은 바닥 마진, 뒤의 것은 오른쪽 마진의 크기이다. heatmap.2()로 만들어진 그림은 마우스를 대고 드래그를 한다고 해서 마진 부분이 늘어나서 가려졌던 라벨이 보이거나 하지는 않는 것 같다. key의 위치를 변경하거나 크기를 바꾸는 등 상세한 편집 작업을 하고 싶다면 Moving color key in R heatmap.2를 참조하라.

참고 사이트


2017년 12월 14일 목요일

[하루에 한 R] 매트릭스에서 특정 값을 갖는 row 추출하기

[작성 후 추가한 글] 제목이 잘못되었다. 엄밀히 말해서 매트릭스가 아니라 데이터프레임이다.

해들리 위컴(Hardley Wickham)은 뉴질랜드의 통계학자 및 데이터 사이언티스트이자 저명한 R 개발자이다. RStudio의 Chief Scientist이기도 한 그는 ggplot2(), dplyr 등 데이터 처리와 시각화에 탁월한 R 패키지를 다수 개발하였고 활발한 저술과 강연활동도 펼치고 있다. 그가 개발한 패키지들은 GitHub 사이트에서 볼 수 있다. Quora에는 이런 질문도 있었다. '어떻게 해들리 위컴은 패키지 개발을 통해 그렇게 R에 많이 기여할 수 있었나요?'

이 질문에 대해서 위컴 자신이 답을 달았다. 다른 사람이 쓴 답이 무슨 소용이 있으랴.

  • 나는 많이 씁니다. 침대에서 일어나자마자 매일 아침 60-90분 정도를 씁니다(코드? 아티클?)
  • 나는 많이 읽습니다. 300개 정도의 블로그를 팔로우하고, 트위터와 스택 오버플로우에서 R이라는 태그가 달린 글을 계속 봅니다. 물론 모든 글을 깊이있게 읽지는 못하고 훑어보는 수준이지만 나에게 큰 도움이 됩니다.
  • Chunking(덩이 짓기?). '컨텍스트 스위칭'은 비용이 많이 듭니다(여기에서 context switching이란 소프트웨어 개발에 관한 용어가 아니라 어떤 일을 하다가 다른 일로 전환하는 것을 의미하는 것으로 보인다). 동시에 여러 패키지를 작업하면 아무것도 되지 않습니다. 새로운 기능에 대한 문제점과 아이디어를 꾸준히 축적하고 있다가 임계 질량에 다다르면 이 패키지에 며칠을 보내는 식으로 일을 합니다.
한마디로 말해서 참 멋진 사람이다. dplyr 패키지에 대한 국문 소개는 양우성님의 웹사이트를 참조하면 좋다. 내가 여기까지 흘러들어오게 된 것은 인하대 유동현 교수님 덕분이다.

오늘은 데이터프레임에서 특정 조건에 맞는 행을 뽑아오는 방법에 대해 공부해 보려고 한다. 숫자를 가지고 조건에 맞는 행을 뽑는 것이 아니라, 캐릭터 값을 가지고 일을 하려는 것이다. 몇 천 유전자에 대한 blast 결과를 테이블로 출력하여 R로 읽어들여 데이터프레임으로 만든 다음 내가 관심을 갖고 있는 몇십, 혹은 몇백개의 유전자에 해당하는 hit row를 쏙 빼내려는 것이다. Genes of interest는 별도의 벡터에 저장된 상태라고 가정하자. Perl에서는 hash를 사용하여 if (exists $seen{$item}) {...}으로 늘 하던 일이다. 여담이지만 내가 Perl애소 이 기법을 처음 접한 것은 phred/phrap/consed 패키지의 phredPhrap 스크립트를 뜯어보면서였다.

dplyr 패키지를 쓰면 오늘 논하는 것을 포함하여 더욱 정교한 작업을 할 수 있을 것이다. 그러나 이 글을 작성하는 목적은 R base package의 기본 기능을 최대한 활용해 보자는 것이다. 핵심이 되는 것은 which() 함수와 %in% 연산자이다. 이 특수 연산자에 대한 도움말을 띄워 보자. 함수에 대한 도움말을 찾을 때만 help() 또는 ? 명령어를 쓰라는 법은 없다.
> ?"%in%"   # 또는 help("%in%")
a %in% b라고 입력하면 a 벡터의 모든 원소에 대해서 이 값이 b 벡터에도 존재하는지를 TRUE or FALSE로 출력하는 것이다. 실제 사례를 보자.
> a = c("A","B","C","D","E")
> b = c("C","D","E","F","G")
> a %in% b
[1] FALSE FALSE  TRUE  TRUE  TRUE
which() 함수를 이용하면 a 벡터의 원소 중 b에도 있는 것, 그리고 a에만 있는 것을 다음과 같이 출력할 수 있다.
> a[which(a %in% b)]
[1] "C" "D" "E"
> a[-which(a %in% b)]
[1] "A" "B"
벡터에 대하여 방법을 알아보았으니 어제 글에서 활용한 샘플, 즉 blast 결과 파일을 파싱한 데이터프레임에 대해서 이 일을 해 보자. 'd'라고 명명한 데이터프레임의 구조는 다음과 같다. head() 함수를 이용하여 앞부분만 발췌하였다.


추출할 유전자는 벡터 e(for extract)에 넣어두었다. 몇 개 되지 않으면 손으로 입력해도 되지만, 그 수가 많다면 파일로부터 읽어들이는 것이 현명할 것이다.
> e
[1] "BfmR" "CsuB" "PgaB" "EntA" "PlcD"
d$V1 컬럼의 값 중 e 벡터에 존재하는 것을 찾아서 그 row를 추출하면 된다. 코드는 다음의 한 줄이다. 각괄호 안의 쉼표를 빼먹어서는 안된다.
> d[which(d$V1 %in% e), ]
실제 실행 화면을 보자.


너무나 간단해서 내가 다 미안할 지경이다. 다음에는 데이터프레임의 row 혹은 column을 그룹 단위로 조작하는 방법에 대해서 공부해 보련다.

2017년 12월 13일 수요일

[하루에 한 R] BLAST tabular output에서 best hit 뽑아내기

BLAST+(최신 버전)가 완전히 정착된 것이 언제인데 나는 아직도 'legacy' BLAST(v2.2.26)를 즐겨 사용한다. blastall 커맨드 라인 사용법에 너무나 길들여져 있기 때문이다. BLAST+의 옵션 설명서를 인쇄하여 책상 앞에다 붙여놓아야 겨우 익숙해질까?

예전, 하지만 그렇게 오래 전도 아니던 시절, 미생물 유전체의 functional annotation을 하려면 Swiss-Prot이나 NCBI NR 데이터베이스를 로컬 컴퓨터에 받은 다음 수천 개의 query를 blastp로 날리는 일을 수시로 했었다. 요즘은 RAST server에 올려 버리거나, 로컬 머신을 쓴다 하여도 Prokka를 쓰면 단 몇 분에 해결이 되이 큰 용량의 공개 단백질 서열 DB에 대해서 BLAST를 직접 돌릴 일이 많지 않다. 또 그럴 일이 있으면 유료 서비스에 가입한 Blast2GO에서 검색을 수행하면 된다.

그래도 간혹 연구 목적을 위해 특별한 목적으로 만든 서열 데이터베이스에 대해서 BLAST를 실행할 일이 있다. 그 결과물을 파싱하는 프로그램을 직접 구현하는 것이 예전에는 매우 보편적인 생명정보학 실습 과제이기도 했다. 누가 그랬더라? 이제 제발 그런 거 하지 말라고...

사람의 눈으로만 보기 편한 BLAST 결과물을 컴퓨터에게 읽히려고 하니 프로그래밍으로 처리하기에 얼마나 불편하겠는가? 사실 내가 tabular output format(blastall -m 8 또는 -m 9; BLAST+에서는 -outfmt 6)에 관심을 가진 것도 얼마 되지 않았다. 이를 엑셀로 불러들이면 완벽하지는 않더라도 대량의 결과를 처리하기에 좋으니까 말이다. 엑셀의 귀재라면 각 query에 대한 hit 중에서 best를 골라내는 방법을 함수로 구현할 수 있을지도 모른다.

R을 사용하면 BLAST의 tabular format output으로부터 각 query에 대한 best hit을 쉽게 뽑을 수 있을 것이라는 생각을 오랫동안 하고 있었다. 사실 R 환경 자체에서 BLAST를 실행할 수 있는 rBLAST라는 패키지가 있으니 이 환경 안에서 best hit을 찾을 수 있을 것이고 또 더 검색을 해 보면 다른 도구가 있을 것이다. 범위를 좀 더 넓힌다면 reciprocal best hit을 찾아주는 유틸리티도 있을 것이다. 하지만 나는 좀 더 '하등한' 수준의 해결 방법을 알고 싶었다.

[SEQanswers] How to output only best-hit result using standalone blast <= 심지어 sort 명령어를 사용한 방법도 있다. one-liner를 즐기는 사람에게는 좋은 소일거리가 될 것이다.

그러면 본론으로 들어가서, blastall -m 9(tabular with comment lines; #으로 시작) 명령으로 작성한 결과물을 R에서 읽어들여서 best hit을 찾는 방법을 공부해 보자. 예전에 마이크로어레이 자료 분석을 공부하면서 control spot 때문에 한 유전자에 대한 형광 측정값이 여럿 존재하는 경우 이것의 median 값을 취하도록 R 코드를 짰던 것 같은데 도저히 기억이 나질 않는다. 아마 apply() 혹은 by() 계열의 함수를 쓰지 않았었나 생각한다.

그런데 구글링을 계속 해 보니 base R만을 이용한 아주 원초적인 방법을 제시한 글이 있어서 소개하고자 한다. 이 안에는 외부 패키지와 함수를 이용한 복잡한 방법도 포함되어 있다. 공부를 위해서 나머지 방법도 따라서 해 보아야 한다.

[Stack Overflow] Extract the maximum value within each group in a dataframe

blastall -m 9 옵션을 사용하여 출력 파일(tblastn.m9,.txt)을 만든 뒤 각 query에 대한 best hit을 뽑아보자.
> d = read.table("tblastn.m9.txt",sep="\t",comment.char="#",header=F)
> d
             V1   V2     V3  V4 V5 V6  V7  V8   V9  V10      V11    V12
1          BfmR bfmR 100.00 238  0  0   1 238    1  714 8.0e-156  423.0
2          BfmR bfmS  30.56  36 25  0  21  56 1437 1330  2.3e-01   22.3
3          BfmS bfmS  99.82 549  1  0   1 549    1 1647  0.0e+00 1083.0
4          BfmS bfmS  27.78  54 39  1 133 186  658  533  7.1e+00   18.9
5          BfmS  nfu  38.46  13  8  0 117 129  442  480  8.1e+00   18.5
6          CsuA csuA 100.00 192  0  0   1 192    1  576 2.0e-129  353.0
7          CsuA csuE  43.75  16  9  0 177 192  970 1017  4.5e-01   20.8
8          CsuB csuB 100.00 172  0  0   1 172    1  516 4.0e-114  312.0
...중간 생략
> do.call(rbind, lapply(split(d,d$V1), function(x) {return(x[which.max(x$V12),])}))
                       V1   V2     V3  V4 V5 V6 V7  V8 V9  V10    V11  V12
BfmR                 BfmR bfmR 100.00 238  0  0  1 238  1  714 8e-156  423
BfmS                 BfmS bfmS  99.82 549  1  0  1 549  1 1647  0e+00 1083
CsuA                 CsuA csuA 100.00 192  0  0  1 192  1  576 2e-129  353
CsuB                 CsuB csuB 100.00 172  0  0  1 172  1  516 4e-114  312
CsuC                 CsuC csuC 100.00 277  0  0  1 277  1  831  0e+00  568
CsuD                 CsuD csuD 100.00 832  0  0  1 832  1 2496  0e+00 1650
CsuE                 CsuE csuE 100.00 339  0  0  1 339  1 1017  0e+00  666
EntA                 EntA entA 100.00 256  0  0  1 256  1  768  0e+00  502
EpsA                 EpsA epsA  99.73 366  1  0  1 366  1 1098  0e+00  727
NfuA                 NfuA  nfu 100.00 212  0  0  1 212  1  636 2e-162  438
OmpA_partial OmpA_partial ompA 100.00 315  0  0  1 315  1  945  0e+00  592
pgaA                 pgaA pgaA 100.00 747  0  0  1 747  1 2241  0e+00 1544
PgaB                 PgaB pgaB  99.80 510  1  0  1 510  1 1530  0e+00  980
PgaC                 PgaC pgaC 100.00 392  0  0  1 392  1 1176  0e+00  805
PgaD                 PgaD pgaD 100.00 154  0  0  1 154  1  462 6e-101  278
PlcD                 PlcD plcD 100.00 487  0  0  1 487  1 1461  0e+00 1018
Ptk                   Ptk  ptk 100.00 727  0  0  1 727  1 2181  0e+00 1372
V1(query)의 값이 동일한 hit group에 대해서 V12(bit score)가 최대인 것을 출력하는 것이다. 어떻게 이보다 더 간단명료할 수 있겠는가?

[하루에 한 R] order() 함수의 이해

어떤 벡터가 있을 때 sort() 함수를 적용하면 그 원소들을 다음과 같이 오름차순으로 정리하여 출력한다. 내림차순으로 정동하고 싶으면 decreasing=T를 설정하면 된다.
> x = c(1,5,3,2,4)
> sort(x)
[1] 1 2 3 4 5
order() 함수는 벡터의 원소를 이 인덱스 번호순으로 놓으면 값이 정렬된다는 것을 의미한다. Character 타입의 벡터를 사용하여 설명하는 것이 이해하기 쉬울 것이다. 이를 이해한다면 아래에서 보인 마지막 명령이 무엇을 뜻하는지도 납득할 수 있다.
> y = c("a","c","e","b","c")
> sort(y)
[1] "a" "b" "c" "c" "e"
> order(y)
[1] 1 4 2 5 3
> y[order(y)]
[1] "a" "b" "c" "c" "e"
order(y)를 사용하여 알 수 있는 것은 벡터 y의 원소를 값에 따라서 순서대로 나열하려면 첫번째, 네번째, 두번째, 다섯번째, 그리고 세번째 원소의 순서대로 놓으면 된다는 것이다.

order() 함수를 이용하면 행이나 열을 name에 따라서 정렬하는 것도 가능하다. 연습용 4x3 매트릭스를 만들고 colnames()로 각 열에 이름을 붙인 뒤 이것에 따라서 컬럼을 정렬하는 연습을 해 본다. 컬럼의 위치만 바뀔 뿐 각 컬럼 내의 값은 원래의 행 번호를 그대로 지킨다.
> z =  matrix(c(4:1,12:9,5:8),4,3)
> z
     [,1] [,2] [,3]
[1,]    4   12    5
[2,]    3   11    6
[3,]    2   10    7
[4,]    1    9    8
> colnames(z) = c("A","C","B")
> z
     A  C B
[1,] 4 12 5
[2,] 3 11 6
[3,] 2 10 7
[4,] 1  9 8
> z[, order(colnames(z))]
     A B  C
[1,] 4 5 12
[2,] 3 6 11
[3,] 2 7 10
[4,] 1 8  9
apply()와 join() 계열의 함수를 잘 써야 하는데... 아직도 멀었다!

2015년 8월 25일 화요일

[하루에 한 R] heatmap.2 그림을 조정해 보자

오늘은 하루에 '한 R'이 아니고 '두 R'이 되었다. gplots 패키지를 이용하여 그린 heatmap.2 플롯이 맘에 들지 않으니 손을 좀 대 보자. 어떤 문제가 있는가?
이것은 다음의 스크립트로 그려진 것이다.
> heatmap.2(as.matrix(data.final), col=greenred(10), trace="none")
우선 너무 작아서 있으나 없으나 소용이 없는 probe ID를 나타나지 않게 하자.
> heatmap.2(as.matrix(data.final), col=greenred(10), trace="none", labRow=NA)
Row의 label을 없애버린 것이다. 다음으로는 cexCol 파라미터를 줄여서 컬럼 라벨이 잘리지 않게 만들어 보자.
> heatmap.2(as.matrix(data.final), col=greenred(10), trace="none", labRow=NA, cexCol=0.7)
이상으로도 그렇게 나쁘지는 않다. 그러나 글자 크기를 줄임으로 인하여 가독성이 나빠졌다. 글씨 크기를 그대로 유지하려면 마진을 키우면 된다. 대신 정사각형으로 예쁘던 heatmap이 직사각형이 될 것이다. heatmap.2() 함수 안에서 사용하는 margins는 column/row의 라벨을 나타내는 공간에 관한 것이다. R의 plot area와 주변부의 여유 공간을 뜻하는 margin과는 다르다(참고 R graphics 생기초 링크)
> heatmap.2(as.matrix(data.final), col=greenred(10), trace="none", labRow=NA, margins=c(7,5))
컬럼 라벨(샘플명)의 크기는 그대로 유지되면서 마진이 늘어나는 바람에 글씨가 잘리지 않고 전부 보인다. heatmap이 직사각형으로 찌그러진 것이 보기 싫으면 margins=c(7.7)로 해 보라. heatmap 오른편의 마진이 늘어나면서 heatmap의 가로 사이즈가 줄고 이에 따라서 자연스럽게 정사각형으로 되돌아온다. 이것은 그림을 따로 첨부하지는 않겠다.

마지막으로 Color Key and Histogram이라는 글이 바로 아래의 color key와 살짝 겹치는 문제를 해결해 보자. keysize(기본 수치는 1.5)를 약간 줄여도 제목 텍스트와의 간격은 벌어지지 않는다. 결국 문제는 무엇인가 하면 Color Key and Histogram이라는 텍스트가 두 줄이 된 것이다. 이를 바로잡자고 해서 Key의 폭을 늘이면 heatmap의 폭이 줄어들 것이다. 만약 histogram을 표시할 필요가 없다면 heatmap.2(x,...density.info="none")이라고 하면 된다. 그러면 Color Key라고만 표시가 되어 한결 간결해진다.

좀 더 심화된 코스를 원하는가? 키, 덴드로그램, 힛맵의 레이아웃을 아예 바꾸는 방법도 있다. 이를 알아보려면 이 링크를 상세히 읽어보고 layout() 함수와 lmat 매트릭스 및 lwid, lhei 벡터를 이해해야 한다. 예전에는 종이에 그림을 그려가면서 열심히 공부했었는데 다 잊어버렸다! 오늘은 여기까지만 하자.

기타 heatmap으로 그려지는 플롯의 상세한 설정이 나와있는 설명은 다음 사이트를 참조하기 바란다.


[하루에 한 R] Expression data로 heatmap 그리기

오래전의 microarray 실험 데이터를 가지고 뒤늦은 논문을 쓰고 있다. 대단한 발견이라고 할만한 것은 없고, 너무 오래되어서 더 이상 시의성이 없는 데이터로 추락(?)하기 전에 일단락하여 발표하기 위함이다. 사실 과학에서는 과거의 연구성과가 상당한 시간이 흐른 뒤에 발굴되어 재평가되면서 과학계의 역사를 새로 쓰는 일이 다반사이다. 따라서 변신에 변신을 꾀하면서 지나치게 유행을 좇아 연구 분야 변신을 하는 것은 그렇게 바람직하지는 않다고 본다. '시대적 요청'이라는 것이 있기는 하지만 말이다.

heatmap 또는 heat map이란 데이터를 2차원 형태로 늘어놓고 각각의 값을 색으로 표현한 데이터 시각화 기법의 하나이다. 데이터의 배열을 행 또는 열에 따라 적절히 조절(clustering)함으로써 눈에 확 뜨이는 두드러지는 패턴을 찾아낼 수 있다.

21C 미생물프론티어 사업단 시절을 풍미했던 바로 그 대장균 microarray를 이용한 데이터를 이용해 보고자 한다. 한 개의 slide에 같은 프로브 셋이 두 개의 블록으로 나뉘어 찍혀 있으므로, 칩 하나로부터 두 개의 샘플 데이터가 얻어진다. 엄밀히 따지면 이렇게 나온 데이터는 technical replicate에 해당한다. 그러나 편의상 마치 biological replicate인 것처럼 다루었다. 이를 보고 '말도 안돼!'라고 외치실 분이 있을지도 모른다^^ 이번 포스팅에서는 R을 이용하여 QC, normalization, DEG 추출 등과 같은 expression analysis를 하는 방법을 다루고자 함이 아니다. 단지 hierarchical clustering을 동반한 heatmap을 그리는 작업을 설명하기 위해 고전적인 microarray data를 이용하는 것일 뿐이다.


처음에는 CLC Genomics Workbench의 Transcriptomics Analysis(과거에는 "Expression Analysis")로 heatmap을 그려 보았는데 도무지 마음에 드는 그림이 나오지 않았었다. CLC에서는 GEO 데이터를 어떻게 임포트하는가? GSE####_series_matrix.txt.gz 형식의 series matrix 파일을 다운로드하였다면 압축을 풀고 Import -> Standard Import 메뉴에서 파일 유형을 Expression array/data: GEO SOFT format series file (.txt/.text)로 설정하여 해당 파일을 읽어들이면 된다.

그러면 R을 사용해서 좀 더 예쁜 heatmap을 그려보자. R에 기본으로 내장된 heatmap() 함수는 그다지 예쁘지 않은데다가 사용자 입맛대로 조정을 할 여지가 별로 많지 않다. 따라서 gplots 패키지가 제공하는 heatmap.2() 함수가 인기를 끄는 모양이다. HeatPlus 라는 패키지도 있으니 참고하면 되겠다. 몇 가지 유용한 링크부터 먼저 소개하고 진행하겠다.

입력물로는 GEO에서 다운로드한 series matrix 파일을 이용하겠다. !로 시작하는 코멘트 라인은 전부 제거하여 헤더와 데이터 부분만을 남겼다. 이 파일은 이미 normalization과 log2 transformation이 된 상태이다. 파일명은 data.txt로 수정하여 접근하기 쉬운 폴더(C:\R_work)에 복사해 두었다. 첫번째 시도는 우선 되는대로 막 그리는 것.
> install.packages("gplots")
> library("gplots")
> setwd("C:R_work")
> data = read.table("data.txt", header=TRUE, sep="\t", row.names="ID_REF")
> jpeg("plot1.jpg")
> heatmap.2(as.matrix(data), col=greenred(10), trace="none")
> dev.off()

jpeg으로 출력한 그림을 보자. 어떤 문제점이 존재하는가? microarray data의 heatmap은 각 샘플이 얼마나 유사한지를 보여주기도 하므로 일종의 QC 개념으로 쓸 수도 있다. 동일 조건에서 나온 각 6개의 샘플들이 한데 묶이므로 일단 큰 문제는 없다. 단 하나의 슬라이드에서 나온 두 블록이 항상 가장 가까이 묶이지는 않았음을 기억은 해 두자. 샘플의 이름이 마진에 비해 너무 길어서 잘렸고, 4721개나 되는 probe의 수는 하나의 heatmap으로 표현하기에는 너무 많다. probe ID와 dendrogram이 제대로 보이질 않으니 말이다. 그리고 대부분의 데이터가 0 근처에 있어서 너무 밋밋한 그림이 되었다.

Plot의 마진을 설정하는 방법은 완전히 다른 주제라서 여기에서는 다루지 않겠다. 이제 그림을 좀 더 돋보이게 하는 방법을 알아보겠다. 

모든 컬럼에서 특정 조건(값의 범위)을 충족시키는 row만 남기기(미완?)

Two-color microarray experiment에서 얻어진 데이터이므로 log2(Fold_change)가 0 근처에 있는 것은 그림을 복잡하게만 만들뿐 heatmap에 남겨둘 필요가 없다. 특정 column의 값을 기준으로 하여 전체 row를 삭제하는 방법은 인터넷 검색에서 흔히 찾을 수 있다. 예를 들어 첫번째 컬럼의 값이 -0.5보다 크고 0.5보다 작은 것을 데이터에서 제외해 보자. 이를 "dropping"이라 한다.
> data.2 = data[-which(data[,1] > -0.5 & data[,1] < 0.5), ]
그렇다면 18개의 모든 컬럼에 대해서 -0.5 < 데이터값 < 0.5를 만족시키는 row를 일괄적으로 삭제하려면 어떻게 하면 좋을까? 어느 한 컬럼이라도 -0.5 .. 0.5의 범주에 들면 그 row를 삭제한다는 것이 아니고(이렇게 되면 너무 많은 row가 제거될 것이다), 모든 컬럼이 다 그럴 때에만 제거하지는 것이다. 데이터가 카운트 값이면 row 단위로 합하여 그것을 기준으로 하면 되겠지만 이번의 사례에서는 '범위'가 판단의 기준이라서 쉽지가 않다.

아주 단순하게는(그리고 미련하게는) which() 함수 내부의 조건에 해당하는 부분을 모든 컬럼으로 확장하면 된다. (data[,1] > -0.5 & data[,1] < 0.5) & (data[,2] > -0.5 & data[,2] < 0.5) & (data[,3] > -0.5 & data[,3] < 0.5) ... (data[,18] > -0.5 & data[,18] < 0.5) 오타가 안나도록 주의깊게 타이프를 치면 된다. 그런데 이 방법은 너무 우습지 않은가? 만약 dropping을 마친 row가 아직도 너무 많아서 cutoff 값을 바꾸고 싶다면? 구글링을 열심히 해 보았지만 이 경우 딱 맞는 솔루션은 보이질 않는다. R에서 별로 바람직하게 여겨지지 않는 for 문을 사용하여 어찌어찌 만들어 보았는데 최종적으로 선별된 프로브의 수가 너무 많아서 역시 구별이 가능한 수준의 그림이 만들어지질 않는다.

row를 값의 범위에 의해 선별하여 버리는 방법을 알아내는 것이 문제가 아니라, 남은 row 자체가 너무 많다는 것이 문제다. 결국은 논문 작업을 위해서 뽑아둔 DEG 목록을 활용하기로 하였다. 몇 그룹의 조건으로부터 총 813개의 DEG를 확보하여 all_DEG.txt 파일에 저장하였다. 한 줄에 하나씩의 probe ID가 들어있고 중복을 없애는 작업이 필요하다. scan() 함수를 사용하여 매트릭스 형태가 아닌 데이터 파일을 입력하는 것은 조금 복잡하다. 이 링크를 참조하여 추후에 조금 더 공부하도록 하자.
> deg = scan(file="all_DEGs.txt", what=list(probe=character()))
> deg.nr = unique(deg$probe)
여기까지 왔으면 heatmap 작성 대상 유전자(probe ID)의 목록이 deg.nr이라는 벡터에 들어있는 상태이다.

목록에 존재하는 row name에 해당하는 레코드만을 데이터프레임에서 추출하기

추출할 probe의 목록을 deg.nr이라는 벡터에 담았으니, 이를 참조하여 data 데이터프레임의 일부를 꺼내면 된다. 참조할 곳은 특정 컬럼이 아니라 row name에 해당한다.
> data.final = data[which(row.names(data) %in% deg.nr), ]
> heatmap.2(as.matrix(data.final), col=greenred(10), trace="none")
probe가 457개로 줄어들어서 한층 보기가 수월해졌다.


다음의 숙제는 plot의 마진을 설정하는 일이다. 아마도 par() 함수를 쓰게 되지 않을까 싶다.





2015년 4월 24일 금요일

[하루에 한 R] 행렬 형태의 데이터 구조

R의 초보자 딱지도 아직 떼지 못했는데 주제넘게 이런 포스팅을 하여 인터넷 공간을 어지럽히는 것은 아닌지... [하루에 한 R]을 쓰게 된 동기는 다른 독자들이 이 글을 보고 조금이나마 도움을 얻기 위함이 아니라, 개인적인 공부를 하기 위함이다. 그러니 실수나 오류가 있을 수 있다.

오늘의 포스팅에서는 IT에서 이야기하는 복잡한 <데이터 구조>의 기술적인 면을 이야기하고자 함이 아니다. 엑셀이든, csv 파일이든, 일반적으로 행렬형태의 데이터를 우리가 어떻게 바라보아야 하는지 개념을 잡는데 도움을 주고자 간단한 그림과 함께 글을 써 나가도록 한다.

여러개의 샘플에 대해서 어떤 측정이나 조사를 하려고 한다. 측정이나 조사에는 여러개의 수치화 가능한 항목이 있을 것이다. 갯수가 얼마 안되면 대충 수첩에 적어도 좋겠지만, 대부분의 경우는 엑셀과 같은 프로그램에서 입력을 하는 것이 수월할 것이다. 예를 들어서 전체 학생의 중간고사 성적표 일람을 생각해 보자. 하나의 행(row)은 개별적인 학생에 해당하고, 각 열(column)에는 국어, 영어, 수학 등의 시험 점수가 적힌다. 이를 90도 돌려서 개별 학생의 성적을 세로(열)로 배열하고 싶은 사람도 있을 것이다. 논리적으로는 문제가 없지만 영 어색하다. 각 학생은 하나의 샘플이라 간주할 수 있고, 수가 많은 샘플은 행 단위로 추가되는 것이 바람직하다. 따라서 아래 그림이 가장 보편적인 2차원 행렬 형태의 데이터 집합 구조라고 보면 된다.

첫 행과 첫 열부터 기록치를 써내려가는 사람은 없을 것이다. 첫 행과 첫 열에는 나머지 셀에 어떤 항목이 들어가는지 제목부터 달아 놓는 것이 자연스럽다. 특히 중요한 것은 각 열의 이름이다. 이를 R에서는 보통 variable이라고 이야기한다.

생명과학 분야에서 흔히 다루는 expression data의 구조는 이와 조금 다르다. 개개의 observation(chip 또는 RNA-seq 실험)이 열로 배치되고, 행은 유전자 ID로 삼는 것이 관례이다. 따라서 hierarchical clustering 등을 하려면 t() 함수를 사용하여 트랜스포즈를 해야 된다.

R에서 행과 열의 이름을 지정할 때에는 dimnames()라는 함수를 쓴다. dimname(data)라고 하면 data 오브젝트에 이미 설정된 행과 열의 이름을 반환한다. 조금 뒤에 살펴볼 row.names() 함수도 마찬가지로 작동한다. 먼저 dimnames() 함수의 사용법을 알아보자.

> countries = c("Austria", "France", "Germany")
> variables = c("GDP", "Population", "Inflation")
> dimnames(country.data) = list(countries, variable)

만약 세번째 컬럼을 쏙 빼서 row name으로 삼고 싶다면?

> row.names(country.data) = country.data[, 3]
> country.data = country.data[, -3]  # 세번째 컬럼 제거

R이 훌륭한 점은 이렇게 행렬 형태로 구성된 데이터를 매우 자유롭게 다룰 수 있다는 점이다. 물론 어떤 사람은 엑셀이 더 편리하다고 말할 것이다. 마우스를 클릭해서 눈에 즉시 보이는 결과를 얻는다는 점에서는 엑셀이 낫다. 그러나 어떤 데이터를 임포트한 뒤 특정 컬럼이나 행을 없애고 계산을 하고 새로운 행과 열을 만들고... 이런 일을 하다보면 최종 결과물만 남을뿐 과정을 기억하기가 어렵다. 이런 점에서 스크립트 형식으로 작업하는 R은 훨씬 유연하고 정확하며 실수가 적다.

R은 다음과 같은 일을 하는데 정말 편리하다. 단, 반드시 유념할 것이 있다. for..와 같은 반복문을 쓰지 않는 것이 R의 기본 정신이다. 나는 최소한 그렇게 생각한다.

  • 어떤 조건을 만족하는 행이나 열 제거하기
  • 어떤 조건을 만족하는 행이나 열만 뽑아내기
  • 행이나 열을 기준으로 계산하거나(예: 평균, 최대값, 최소값...) 조작하기(예: 정렬)
  • 테이블 여러개를 합치기
인터넷을 참조해 보면 이러한 상황에 사용할 수 있는 정말로 아름다운 사례가 많다. 시간이 허락하는대로 포스팅해 보고자 한다. 아, [하루에 한 Perl]도 해야 되는데...

2015년 4월 23일 목요일

[하루에 한 R] 특정 조건에 맞는 row만 출력하기

어제의 포스팅에서 다루었던 파일(data3.txt)을 이용하여 R의 또 다른 활용 방법을 살펴보자. 특정 조건을 만족하는 row만을 출력해 보는 것이다.
$ cat data3.txt
Nation,Pop.density, Area, Population
UK,255.6,243610,62262000
France,111,547030,63601002
Germany,233,357021,81799600
Korea,513,100210,51098531
N.Korea,200,120538,25051706
Japan,337,377873,127387000

 첫 줄의 요소 수는 나머지 라인의 요소 수와 같다. 이를 감안하여 R에서 데이터 파일을 읽어들이자. 이렇게 읽어들인 data 오브젝트는 list이다. mode(list)라고 실행하면 금방 알 수 있다.
> data = read.table("data3.txt",sep=",",header=TRUE,row.name=1)
> data
        Pop.density   Area Population
UK            255.6 243610   62262000
France        111.0 547030   63601002
Germany       233.0 357021   81799600
Korea         513.0 100210   51098531
N.Korea       200.0 120538   25051706
Japan         337.0 377873  127387000
오늘의 과제는 인구밀도(Pop.density)가 300보다 작은 널널한 나라를 찾아보자는 것이다.

방법 1. 슬라이싱을 절묘하게 이용하기

마치 암호같지만 잘 살펴보면 그 작동 원리를 이해할 수 있다. 두 가지 방법을 나열해 보겠다.
> data[, "Pop.density"] < 300
[1]  TRUE  TRUE  TRUE FALSE  TRUE FALSE
> data[data[, "Pop.density"] < 300, ]
        Pop.density   Area Population
UK            255.6 243610   62262000
France        111.0 547030   63601002
Germany       233.0 357021   81799600
N.Korea       200.0 120538   25051706

> data$Pop.density < 300
[1]  TRUE  TRUE  TRUE FALSE  TRUE FALSE
> data[data$Pop.density < 300, ]
        Pop.density   Area Population
UK            255.6 243610   62262000
France        111.0 547030   63601002
Germany       233.0 357021   81799600
N.Korea       200.0 120538   25051706
여기에서 R 코드의 가장 아름다운 면이 드러난다. 바로 [, -1]과 같이 데이터를 '쳐 내는' 것이다. 그러면 위 코드에 대한 설명을 해 보겠다. 첫번째 사례에서는 data[, something] < 300이라는 조작을 했다. [ ] 안의 두번째 위치이니 something에 해당하는 컬럼에 대해서 조건을 만족하는지를 모든 row에 대해서 시험하는 것이다. 따라서 반환되는 값은 TRUE FALSE... 와 같다. 이를 전체 데이터에 대해서 다시 첫번째 인자로 넣었으니 이 조건을 만족하는 row를 내놓으라는 뜻이 된다.

조건을 하나 더 붙여보자. 인구밀도는 300보다 작은데다가 면적은 350000을 넘는 덩치가 크고도 널널한 나라를 찾아보자. & 혹은 |를 쓰면 된다. Perl에서 && 및 || 연산자를 떠올리면 된다. 정확히 같은 것과 같지 않은 것도 뽑을 수 있다. 그건 각자 알아보라.

> data$Pop.density < 300 & data$Area > 350000
[1] FALSE  TRUE  TRUE FALSE FALSE FALSE
> data[data$Pop.density < 300 & data$Area > 350000, ]
        Pop.density   Area Population
France          111 547030   63601002
Germany         233 357021   81799600

방법 2. 데이터프레임으로 전환한 뒤 subset() 함수 적용하기
> data.frame = as.data.frame(data)
> subset(data.frame, Pop.density < 300)
        Pop.density   Area Population
UK            255.6 243610   62262000
France        111.0 547030   63601002
Germany       233.0 357021   81799600
N.Korea       200.0 120538   25051706
조건을 여러개 다는 것도 위에서 살펴본 것과 마찬가지 방법으로 가능하다. 
> subset(data.frame, Pop.density < 300 & Area > 350000)
        Pop.density   Area Population
France          111 547030   63601002
Germany         233 357021   81799600
팁 하나: 출력물을 파일로 보내고 싶다.... sink() 함수 활용하기

출력물이 한 화면을 넘어가면 이를 일일이 긁어서 페이스트를 할 수는 없다. 다음과 같이 해 보라. result라는 파일에 화면 출력물이 고스란히 저장된다. 다시 출력을 화면으로 되돌리는 것은 인수 없이 sink()를 실행한 이후이다. 공백은 탭 형태로 저장된다. 
> sink("result")
> subset(data.frame, Pop.density < 300 & Area > 350000)
> sink()
정말 멋지지 않은가? 다음에는 heatmap을 그리는 방법에 대해서 포스팅하고자 한다.

[업데이트] 2019년 2월 8일에 추가한 글

슬라이싱(slicing)의 정확한 의미는 무엇인가? 칼로 얇게 썰어내듯이 데이터 프레임의 어떤 부분(주로 row)를 잘라서 취하는 것을 일반적으로 의미하는가, 혹은 dplyr 패키지의 slice() 함수를 사용하는 것만을 의미하는가? Subsetting에 대해서도 똑같이 이야기할 수 있다. slice() 함수의 기능은 'select rows by position'이라고 하였다. 그렇다면 row를 논리적 조건에 의해 골라내는 것은 슬라이싱이라고 불러서는 안될 것이다. subset() 함수는 R의 기본 패키지에 포함되어 있다.


[하루에 한 R] 매트릭스 형태의 텍스트 파일 입력과 헤더의 처리

최근 세균 유래의 RNA-seq 데이터 분석 처리를 의뢰받으면서 1년 넘게 손을 놓고 있었던 R 관련 자료를 뒤적이게 되었다. 세균 RNA-seq 데이터 처리에 특화된 Rockhopper라는 도구가 있어서 비교적 편하게 expression 수치와 fold change 값은 뽑아내었다. 그러나 의뢰자에게 제공할 QC 등과 관련한 시각화 자료를 제공하기에는 R만큼 좋은 것이 없다. 다시 기초 자료를 뒤적이면서 R에서 행렬(matrix) 형태의 자료를 입력하여 다루는 기본적인 사항을 블로그에 정리해 보기로 하였다.

우선 다음과 같이 간단한 자료 파일(data1.txt)이 있다고 가정하자. 설명은 전혀 없고 숫자와 이들을 구분하는 콤마로만 구성된 텍스트 파일이다.
$ cat data1.txt
255.6,243610,62262000
111,547030,63601002
233,357021,81799600
513,100210,51098531
200,120538,25051706
337,377873,127387000
R에서는 아무런 옵션 없이 다음과 같이 단순하게 읽어들여 보자.
> data = read.table("data1.txt",sep=",")
> data 
     V1     V2        V3
1 255.6 243610  62262000
2 111.0 547030  63601002
3 233.0 357021  81799600
4 513.0 100210  51098531
5 200.0 120538  25051706
6 337.0 377873 127387000
데이터의 row가 매우 길다면 head() 함수를 써서 데이터의 앞부분 일부를 볼 수 있다. 더욱 바람직하게는 View() 함수를 쓰면 별도의 창이 뜨면서 데이터가 표시된다. view()가 아니고 View()임에 유의하라. 아무튼 데이터를 표시해 보면 원본 파일에는 없었던 V1, V2, V3가 맨 위에 나온다. R에서는 이를 variable이라고 한다. 반면 맨 왼쪽에는 1, 2, 3...라는 일련번호가 붙어서 각 레코드(row)를 구분해주고 있다. View(data)를 실행해 보자.


이제 데이터 파일을 살짝 바꾸어서 맨 윗줄이 헤더역할을 하게 만들어 보자. 즉 첫줄은 각 컬럼이 수록한 값을 설명하는 표지가 되는 것이다. 다시 말하자면 각 컬럼은 이름을 갖게 되는 것이다.
$ cat data2.txt
Pop.density,Area,Population
UK,255.6,243610,62262000
France,111,547030,63601002
Germany,233,357021,81799600
Korea,513,100210,51098531
N.Korea,200,120538,25051706
Japan,337,377873,127387000
여기서 유의할 점은, 첫 줄(variable name 수록)의 요소는 3개이지만 두번째 줄부터는 4개의 요소가 담겨있다는 점이다. 즉 헤더 줄을 제외한 나머지 줄의 첫번째 요소는 row name이라는 암묵적인 선언인 것이다. 이를 R에서 읽어보자. 다음과 같이 variable(=column name)과 row name이 자동적으로 인식이 되었다. 이제 데이터 파일이 수록한 수치가 무엇인지를 알게 되었을 것이다. 우리나라를 포함한 5개 국가의 인구밀도, 면적, 그리고 총 인구수를 나태낸 것이다.
> data = read.table("data2.txt",sep=",")
> data
        Pop.density   Area Population
UK            255.6 243610   62262000
France        111.0 547030   63601002
Germany       233.0 357021   81799600
Korea         513.0 100210   51098531
N.Korea       200.0 120538   25051706
Japan         337.0 377873  127387000

위에서 빨강색 상자로 둘러친 부분(row.names)은 data[,1]를 입력하여 반환할 수 있는 일반적인 컬럼이 아니다.

만약 헤더라인의 첫번째 요소에 'Nation'이 들어가는 것이 좀 더 완벽하다고 생각한다면?

$ cat data3.txt
Nation,Pop.density,Area,Polulation
UK,255.6,243610,62262000
France,111,547030,63601002
Germany,233,357021,81799600
Korea,513,100210,51098531
N.Korea,200,120538,25051706
Japan,337,377873,127387000

이 파일을 R에서 읽어보자.

> data = read.table("data3.txt",sep=",")
> data
       V1          V2     V3          V4
1  Nation Pop.density   Area  Population
2      UK       255.6 243610    62262000
3  France         111 547030    63601002
4 Germany         233 357021    81799600
5   Korea         513 100210    51098531
6 N.Korea         200 120538    25051706
7   Japan         337 377873   127387000

그렇다. 데이터 파일의 모든 줄이 같은 수의 요소를 갖고 있다면, R은 모든 줄을 다 데이터 자체로 생각하는 것이다! 첫 줄의 요소 수가 나머지 줄보다 하나 적다면, 자동적으로 첫 줄을 헤더라인, 즉 variable 명을 담고 있는 것으로 인식한다. 그리고 더욱 중요한 것은, 두번째 줄부터 나타나는 첫번째 요소는 row name으로 인식한다는 것.

따라서 data3.txt파일처럼 모든 라인이 같은 수의 요소로 이루어졌다 하더라도 첫 줄이 헤더임을 명시하려면 다음과 같이 하면 된다.

> data = read.table("data3.txt",sep=",",header=TRUE)
> data
   Nation Pop.density   Area Population
1      UK       255.6 243610   62262000
2  France       111.0 547030   63601002
3 Germany       233.0 357021   81799600
4   Korea       513.0 100210   51098531
5 N.Korea       200.0 120538   25051706
6   Japan       337.0 377873  127387000

그러나 아직 완벽하지 않다. 나라 이름이 아직 row name으로 인식되지 않고 있기 때문이다. 첫번째 컬럼이 row name에 해당한다는 것을 알려주려면 다음과 같이 해야 한다.

> data = read.table("data3.txt",sep=",",header=TRUE, row.names=1)
> data
        Pop.density   Area Population
UK            255.6 243610   62262000
France        111.0 547030   63601002
Germany       233.0 357021   81799600
Korea         513.0 100210   51098531
N.Korea       200.0 120538   25051706
Japan         337.0 377873  127387000

row.name=2라고 쓰면 데이터 파일의 두번째 줄을  row name으로 인식한다. 당연한 이야기지만 row name으로 선언된 컬럼 내의 값은 각자 유일해야 한다. 그렇지 않으면 에러가 발생한다.

헤더라인의 첫번째 요소를 'Nation'이라고 명시적으로 나타내는 것이 옳은가? 즉 Nation도 하나의 variable로 봐야 할 것인가? 이건 철학적인 문제라서 무엇이 정답이라고 할 수는 없다. 단, 첫 줄의 요소 수가 나머지 줄과 동일한가 혹은 그렇지 않은가(하나 적은가)에 따라서 R의 read.table() 기본 작동이 달라진다는 것, header=TRUE라고 선언하는 것은 오직 variable의 인식에만 관여할 뿐 row name과는 상관이 없다는 것을 기억하는 것이 중요하다.

row.names라는 파라미터를 모른다면 다음과 같이 추가적인 단계를 거치면 된다. 첫번째 컬럼을 row.names(data)에 할당하고, 그 컬럼을 없애면 된다.

> data = read.table("data3.txt",sep=",",header=TRUE)
> row.names(data) = data[,1]
> data = data[,-1]
> data
        Pop.density   Area Population
UK            255.6 243610   62262000
France        111.0 547030   63601002
Germany       233.0 357021   81799600
Korea         513.0 100210   51098531
N.Korea       200.0 120538   25051706
Japan         337.0 377873  127387000

이상의 사례에서 보았듯이 새로운 데이터(여러개의 variable로 구성)는 하나의 줄(row)로 추가되는 것이 자연스럽다. 그러나 생명과학에서 흔히 다루는 발현수치, 즉 microarray나 RNA-seq 자료는 어떠한가? 여기에서는 기본적으로 한번의 실험에서 얻어진 모든 유전자의 발현 수치가 하나의 컬럼으로 구성된다. 대신 row는 각 유전자에 해당한다. 따라서 매트릭스 형태의 발현 수치 데이터를 읽어들인 후 계층적 클러스터링을 하려면 t() 함수를 써서 트랜스포즈를 해야 하는 것이다. 즉 행과 열을 바뀌치기 하는 것이 필요하다.

매우 기본적인 사항이지만 평소에 눈여겨보지 않았던 것을 이렇게 정리하고 나니 머리속이 좀 맑아지는 기분이다...

2017년 12월 15일에 추가한 글: R 매뉴얼을 통해서 header 관련한 동작을 비로소 이해하였다. read.table() 함수에서는 기본적으로 header=F이지만 첫 줄의 데이터 필드가 나머지 줄보다 하나 적을 때 자동으로 T가 된다. 반면에 read.csv(), read.delim() 등 다른 계열의 함수에서는 기본적으로 header=T이다.

2018년 2월 25일에 추가한 글: 간혹 매트릭스 형태의 데이터 파일 중에서는 첫번째 줄의 요소 수가 나머지 줄과 동일하면서도 첫번째 셀(1,1)을 비워 둔 것을 보게 된다. 분리자가 탭("\n")인 경우의 사례를 보자. 첫번째 줄의 최초 분리자 바로 앞에는 아무것도 없다.
\tPop.density\nArea\nPopulation
UK\n255.6\n243610\n62262000
France\n111\n547030\n63601002
Germany\n233\n357021\n81799600
Korea\n513\n100210\n51098531
N.Korea\n200\n120538\n25051706
Japan\n337\n377873\n127387000
이러한 파일은 엑셀에서 즉시 읽어들이기에는 좋다. 하지만 R 입장에서는 빨강색 "\t"이 없다면 read.table() 함수로 그냥 읽으면 될 것을 header=T,row.names=1 파라미터를 추가로 공급해야 함을 잊지 말자.


2013년 9월 13일 금요일

R의 매력

아주 초보적인 수준의 microarray data, 그리고 RNA-seq data를 다루기 위해 R을 공부하고 있다. 인터넷에서 초보용 가이드를 처음 내려받아 인쇄하여 보기 시작한 것이 2011년이었고, 작년부터는 하루 이틀 정도의 단기 강좌를 시간이 나는대로 열심히 들었다.

실제로 내 데이터를 가지고 몰두 한 총 시간은 그렇게 많지 않다. 단지 요즘 몇 주 동안 집중해서 들여다보고 있노라니 어려움이 점차 해소되는 느낌이 들기 시작하였고, R 특유의 매력에 빠져들게 되었다. 특히 merge와 apply 계열의 함수의 막강한 위력을 체험하고 있다.

쓸 줄 아는 언어는 인간의 언어 이외에는 Perl이 유일한데, 올해로 거의 13년째 Perl을 쓰고 있지만 수준은 여전히 그 바닥을 넘지 못하고 있다. Perl과 R은 물론 많은 면에서 다르고 서로 보충적인 성격이 강하다.

[, -1]

이 얼마나 아름다운 데이터 조작법인가! for loop를 쓰지 않기 위해 노력하는 것, 그것이 바로 "R style"이다. 좀 더 복잡한 논리적인 계산이나 데이터 조작, 그리고 텍스트 처리에서는 Perl을 따라갈 언어가 없다고 해도 과언이 아닐 것이다. 그러나 일단 행렬 형태로 데이터를 전환시켜 놓으면 R이 힘을 발휘한다. 게다가 더욱 멋진 것은 publication-ready 수준의 다양한 그림을 그려 준다는 것. 내장되어 있는 통계 분석 기능은 또 어떠한가?

배움에는 나이가 없다. 열심히 공부하세...

2012년 10월 31일 수요일

[R-project] rep() 함수 활용

KOBIC이 주최하는 제12차 차세대 생명정보학 교육에 참석 중이다. 김상철 박사가 강의하는 R 강좌를 올해 들어 두번째 듣고 있다. 자기의 데이터를 가지고 실제 코딩을 해 봐야 실력이 늘 텐데, 게으름에 차일피일 미루다 보니 좀처럼 잘 되지 않는다.

아직도 R의 기본적인 문법을 가지고 헤매는 상태이다. Perl과 달리 R은 기본적으로 벡터 형태의 데이터를 다루고 있어서 Perl처럼 반복문을 많이 쓰지 않아도 된다. 그러려면 rep() 함수를 잘 이해해야 하는데, 이것이 생초보자에게 첫번째 좌절을 안겨주는 관문이 된다. 이건 지극히 개인적인 경험이므로 다른 사람은 어떤지 모르겠다.

이 포스팅은 내 기억을 정리하기 위한 것이지 일반적인 R의 강좌를 쓰고자 함이 아니니 혹시 그런 이유로 이 블로그에 들르신 분은 과감히 [뒤로가기] 버튼을 눌러 주시길...

 아주 간단한 rep()의 예제를 보자.


> rep("A", 4)
[1] "A" "A" "A" "A"

앞의 인수로 주어진 데이터를 뒤의 인수에서 지정한 회수만큼 단순히 반복하는 것이다. 만약 반복하고자 하는 인수가 벡터라면 어떻게 될까?

> x=("A", "B", "C")
> rep(x, 3)
[1] "A" "B" "C" "A" "B" "C" "A" "B" "C"


전체 엘리먼트에 대해서 두번째 인수만큼 반복하는 것이다.

그럼 이번에는 좀 더 복잡한 사례를 들어 보겠다. 1 1 1 1 2 2 2 2 3 3 3 3을 출력하고 싶다면?


> rep(c(1, 2, 3), c(4, 4, 4))
[1] 1 1 1 2 2 2 3 3 3

또는 다음과 같이 두번째 인수를 간략하게 표현할 수도 있다.


> rep(1:3, rep(4,3))
 [1] 1 1 1 1 2 2 2 2 3 3 3 3



이상은 두번째 인수도 벡터로 주어지는 경우이다. 두 인수는 각각 벡터이고, length가 같아야 한다. 첫번째 인수 벡터의 각 원소에 대하여 반복하는 회수를 두번째 벡터에서 결정하는 것이다. 조금 더 복잡한 사례를 들어 보자. 1 2  2 3 3 3 4 4 4 5 5 5 5 5은 다음과 같이 표현 가능하다.

> rep(1:5, 1:5)
 [1] 1 2 2 3 3 3 4 4 4 4 5 5 5 5 5

이제야 조금 알겠다...