옥스포드 나노포어 12월 제품 업데이트

2007년 스티브 잡스 발표 기억하시나요? 화면에 전화기, 터치스크린 아이팟, 인터넷 장비 셋을 띄워놓고 빙글빙글 돌리면서 “Are you getting it?” 하다가 아이폰을 뿅! 하고 발표한 그 맥월드 키노트 말이죠. 이 키노트 이후로는 IT에 크게 관심 없는 사람도 애플이 신제품에 대해 발표를 할 때마다 괜히 설레며 새벽에 보기도 하고, 아침에 기사 검색도 해 보곤 합니다. 요새 바이오텍 업계에서 그런 회사가 하나 등장했습니다. 바로 DNA시퀀서를 만드는 옥스포드 나노포어 (ONT)입니다. 이 회사에서 제품 업데이트를 발표할 때마다 수천 명이 라이브스트림으로 보고, 트위터가 떠들썩해집니다. 아직 엔드유저 제품도 아닌 걸 고려하면 굉장한 반응입니다.

ONT는 보통 분기당 한 번씩 제품 업데이트를 발표하고 있습니다. 업데이트가 너무 빨라서 관심을 기울이고 있어도 따라가기 힘들 지경인데요. 신제품 소식을 듣고 주문하고 받아서 실험하고 잠깐 분석 좀 하다가, 다음 실험을 위해 또 주문하려고 가 보면 전에 주문했던 버전은 벌써 단종되고 없을 정도입니다. -.- 얼마 전 12월 2일까지 뉴욕에서 열린 NanoporeConf에서도 아주 재미있는 소식을 많이 발표했습니다. 간단히 요점만 줄여서 소식 전해드립니다.

PromethION

ONT의 가장 큰 시퀀서 제품인 PromethION이 드디어 첫 고객들한테 발송됐습니다. 역시 첫 고객들은 충성스러워서 다들 트위터에 자랑하고 돌려보기도 전에 벌써 대단한 데이터를 얻은 것처럼 흥분했더군요. ㅎㅎ 새로 공개된 스펙은 다음과 같습니다. 플로우 셀 수는 48개가 됐고요. 플로우 셀 마다 6000 채널 시퀀싱이 가능합니다. 현재 MinION 플로우셀은 512채널에 2048웰 이니까, 플로우 셀 기준으로 비교하면 대략 MinION의 최대 600배 쓰루풋을 낸다고 보면 되겠습니다. 그리고, 이 정도 채널 수가 되면 USB 3.0으로는 도저히 수용 불가능해서, 내장 컴퓨터가 들어가 있습니다. 여기에는 FPGA기반 베이스콜링 가속기가 포함된다고 하는군요. 그리고 가격은 연간 사용료 15000달러로 정했다고 합니다. 얼마 전에 ONT에서는 MinION은 기계 값은 완전 무료, PromethION은 기계 값은 무료로 하고 사용료만 받겠다고 발표했었습니다.

새 시퀀싱 케미스트리: 1D²

지난 3월에 CsgG 포어를 도입하면서 정확도가 드디어 제정신으로 쓸 수 있는 수준까지 올라왔는데요. 그 후에도 CsgG 뮤턴트를 계속 만들면서 더 정확하고 빠르게 개선하고 있다고 합니다. 지금 시험 중인 뮤턴트 하나는 기존 R9 포어보다 거의 5배 정도 더 잘 DNA가 들어간다고 하는군요. (지나가는 속도는 그대로이고 포어가 비어있는 시간이 줄어 듦)

기존의 1D, 2D 시퀀싱에 새로 1D² 시퀀싱이 추가됐습니다. 원래 헤어핀 어댑터를 이용해서 끝에서 한 바퀴 휙 돌아 2번 시퀀싱 하는 2D가 2번째 가닥의 시퀀싱 품질이 매우 안 좋았었는데요, 양쪽 모두 기존에 1D에서 쓰던 Y 어댑터를 쓰면서 모터 단백질과 테더링을 개선해서 첫 번째 가닥을 읽고 다 읽은 가닥이 떨어져 나간 뒤에 바로 반대쪽 상보 가닥이 들어가도록 했다고 합니다. 원래 2D에서 90-97%까지 정확도가 왔다 갔다했던 게, 1D²에서는 95-97% 정도로 아주 고르게 나오게 됐습니다. 따로 언급은 안 됐지만 라이브러리 프렙도 1D와 거의 같을 것이기 때문에 시간도 절약되고, 손실도 줄고, 그동안은 2D에 쓸 수 없었던 tagmentation 기반의 간단한 프렙 장치들도 쓸 수 있게 될 것 같네요.

베이스콜링 업그레이드: 긴 호모폴리머

올해 초까지 HMM을 쓰다가 RNN으로 바뀐 뒤 성능이 크게 올라갔었는데요. 구조상 HMM이나 RNN 기반 베이스콜러 모두 5nt 초과 호모폴리머는 베이스콜링이 불가능했습니다. 이 문제를 대폭 개선한 새로운 Transducer라는 베이스콜러가 처음 소개됐습니다. 일루미나 시퀀싱 베이스콜러 중에 베이스콜링이 어려운 라이브러리들을 잘 처리해줘서 유명했던 AYB를 만든 Tim Massingham이 ONT에 가더니만 결국 전공을 살렸네요. ㅋㅋ 10nt 정도 되는 homopolyer를 시험해 본 결과 30% 정도 길이가 짧아지는 경향은 있지만 거의 선형적으로 비례관계가 있도록 재고는 있다고 합니다. 조금 더 개선 되면 일반 배포한다고 하네요.

새로운 저가형 플로우셀: Flongle

MinION이 기계는 공짜인데, 플로우셀이 너무 비싸죠. 그래서 전자장치를 모두 계속 쓸 수 있는 어댑터로 빼 버리고, 막과 단백질, 플라이스틱만 남겨놓은 소형 플로우셀을 새로 만들었다고 합니다. 휴대전화용 시퀀서인 SmidgION 플로우셀과 똑같은 거라고 하네요. 가격은 클로닝할 때 플라스미드 시퀀싱 하는 가격과 비교할 수 있을 정도로 맞춘다고 하니, 쓰루풋이 반 정도로 낮긴 하지만 기존의 MinION 플로우셀을 완전히 대체할 수도 있을 것 같습니다.

피를 넣으면 라이브러리가 돼서 나오는 필터 팁: Zumbador

지난 5월 런던콜링에서 엄청 떠들썩하게 발표됐던 Zumbador가 좀 더 구체적인 모습을 드러냈습니다. 라이브러리 프렙 전 과정을 실온에서 할 수 있다고 하고요. Tagmentation기반 1D 프로토콜로 만든다고 합니다. P1000 팁보다도 작은 크기로, 위에 피를 떨어뜨리면, 아래로 라이브러리가 나옵니다. ㅎㅎㅎ 그리고, 피 외에 다른 곳에서도 DNA나 RNA를 뽑을 수 있도록 bead beater라는 초소형 장치를 또 새로 공개했습니다. 식물 잎이나 씨앗, 섬유 같은 곳에서 아무 것도 없이 물만 더 있으면 DNA를 뽑아서 바로 Zumbador에 넣어서 라이브러리를 만들 수 있다고 하네요. 낯선 곳에 갔는데 뭔가 의심스러운 것이 있으면 바로 시퀀싱 해보고 안심(?)할 수 있다고 하는군요. ㅎㅎ 음식점에 가서 원산지를 믿을 수 없거나 청결을 믿을 수 없는 음식 먹기 전에 시퀀싱 해 보는 사람도 나올 것 같습니다. -o-;

선택적 시퀀싱

지난번에 공개했던 Cas9 기반 enrichment가 좀 더 구체적으로 공개됐습니다. 효소 활성이 없는 Cas9에 붙여서 이 Cas9을 가지고 면역침전이나 비슷한 방법을 이용해서 정제하는 것이 기본 아이디어인데요. 이번에 공개한 결과를 보면, Cas9가 붙은 채로 플로우셀에 들어가도 모터 단백질이 그냥 밀고 지나가서 전혀 시퀀싱에 문제 없다고 합니다. 그래서 tagmentation과 동시에 Cas9을 처리할 수 있고 그 후 정제도 단순하다 보니 기존의 다른 키트들보다 훨씬 간단하게 돼 버렸네요. 1시간 이내에 DNA부터 시작해서 enriched 라이브러리가 나오게 돼 버렸습니다.

리드를 조금 읽다가 rRNA같이 안 읽어도 되는 시퀀스다 싶으면 바로 반대로 전기를 걸어서 뱉어내 버리는 Read Until도 약간 소개했는데요. 자세한 것은 이미 전에 다 공개됐던 것이고, API를 통해서 사용자 프로그램이 직접 MinKNOW에 연결해서 돌게 된다고 하네요. 그런데 그 이후로 모터 단백질이 450bps로 속도가 올라가는 바람에.. 이제 한 10kbp정도는 돼야 좀 뱉어도 뱉는 것 같지 짧은 건 프로그램에서 뱉어내라고 하면 웬만하면 “이미 다 지나가고 없는디? =.=”하고 포어가 당황하게 생겼어요.

최초의 ~~~~ 지놈: Cliveome

ONT 제품 업데이트 발표 때마다 카리스마를 발산하고 있는 ONT의 간판스타 Clive Brown이 자기 지놈을 시퀀싱 해서 공개했습니다. 인류 최초로 “자기” 피에서 DNA를 뽑아서 스스로 라이브러리를 만들어서, 시퀀싱해서, 어셈블리해서 공개하는 개인 유전체라고 하네요. James Watson이나 Craig Venter도 피만 줬지 자기 손으로 시퀀싱하고 라이브러리 만들지는 않았죠. ㅎㅎ

MinION 36개 플로우셀로 150Gb를 뽑아냈고, 25kb이상 long read가 30Gb 나왔다고 합니다. haplotype assembly를 만들려면 이 정도는 돼야 하겠지만.. 지금 시퀀스 정확도로 잘 될지는 의문이네요. 그래도 다른 시퀀서에서는 할 수 없었던 시그널 수준 취합이 가능하니까 앞으로는 꽤 괜찮아질지도 모르겠네요. 트잉여답게 피 뽑는 순간 부터 자기 피 뽑는다고 긴장된다고 트위터에 엄살 생중계를.. ㅋㅋ; 앞으로도 자기 VDJ지역 대상으로 time-course immunoprofiling을 꾸준히 해서 계속 공개한다고 하고요. Zumbador, Cas9 enrichment 등등 새로 제품 개발되는 것마다 나오면 다 시험삼아 다 해 보고 Cliveome을 꾸준히 업데이트하겠다고 하네요. 데이터는 어제 GitHub에 모두 공개되었습니다.

이 외에도 direct RNA sequencing과 또 다른 여러 응용 분야에 대해 재미있는 발표가 많이 올라왔습니다. 관심 있는 분은 https://vimeo.com/user5318092 여기에서 살펴보세요~

앞으로 태어날 우리 아기, 데이터로 미리 만나볼까~

여기에서 노트북 형식으로도 볼 수 있습니다. 주피터 노트북에서 직접 써 보고 싶은 분은 여기에서 받으실 수 있습니다.

장혜식
http://highthroughput.org/

[알림 1]
이 글에서 소개하는 정보와 소스코드, 방법론 등은 모두 연구 또는 학술 목적으로만 사용 가능합니다. 의료용으로 사용하실 때는 의료기관을 통해 법률에서 정하는 절차로 사용하셔야 합니다.

[알림 2] 이 노트북은 유전학은 잘 모르지만 데이터 분석에 관심이 많은 분들과, 데이터 분석은 잘 모르지만 유전학은 잘 아는 분들도 큰 사전지식 없이 읽으실 수 있게 쓰려고 노력했습니다. 그래서 정확성을 희생하고 간단한 말로 대체한 부분이 많으니 제대로 된 정의에 대해서는 논문들을 참고하시길 부탁드립니다. 사실 저도 인간유전체학을 하진 않기 때문에 정확히는 잘 모릅니다. 하핫;;

[알림 3] 이 노트북에서 사용된 시퀀싱 데이터는 테라젠바이오연구소에서 만들어주셨습니다.

NIPT (non-invasive prenatal testing)는 임신 초기에 아기의 유전형을 검사하는 가장 최근에 나온 방법이다. 이름이 비침습적이라고 진짜 아무 곳도 안 찌르고 들어가는 건 아니고, 엄마 피를 뽑아서 분석한다. 그래도, 아기는 안 건드리기 때문에 양수 검사융모막 검사처럼 문제가 생겼을 때 아기에게 해가 가지는 않는다.

엄마 피로 도대체 아기의 유전자를 어떻게 검사하나? 놀랍게도(!) 임신 10-14주 정도 되는 엄마 피에 떠다니는 DNA의 대략 5-20% 정도는 아기 DNA라고 한다. 정확하게 말하면 엄마 피에 있는 DNA 중에 백혈구 같은 세포 안에 들어있는 DNA 말고, 둥둥 밖에 떠다니는 DNA 중의 5-20% 이다. 이 아기 DNA를 잘 분석하면 아기가 어떤 유전형을 가졌는지 아기를 건드리지 않고도 확인할 수 있다는 것이 NIPT의 기본 개념이다.

이론 상으로는 이런 방법으로 아기의 정말 자세한 유전형을 알아낼 수도 있지만, 엄마 DNA가 무려 90% 가까이 차지하고, 나머지 10% 가량만 아기 DNA라서 현실적으론 어렵다. 아기 DNA에 대한 커버리지만 충족시키는데도 기본적인 전체 유전체 시퀀싱(WGS)보다 10배 이상 드는데, 게다가 엄마DNA와 섞여있다보니 어느게 누구 것인지 통계적으로 확실히 구분할 수 있는 수준까지 가자면 일반적인 WGS의 수 백배가 필요하다. 다른 것 다 빼고 시퀀싱 시약 비용만 억대가 넘어간다.

그래서 지금 상용화되어 있는 NIPT에서는 다른 것은 모두 포기하고 염색체 개수가 이상한 이수성(aneuploidy)만 검사한다. 이수성으론 가장 유명한 것이 21번 염색체가 2개가 아니라 3개가 들어가는 3염색체성(trisomy 21)인데, 이 문제가 생기면 거의 대부분 다운 증후군이 나타난다. Trisomy 21은 생각보다 자주 발생하는 현상이라 완전 남 얘기가 아니다. 40대 초반 산모에서는 1%까지 빈도가 올라간다.

NIPT로 aneuploidy는 어떻게 검사할까? 현재 업계에서 쓰는 방법에선 0.1-0.5X 정도의 커버리지로 전체 유전체를 임의로 찔끔찔끔 읽어낸다. 즉, 엄마 피에 돌아다니는 DNA 조각 중에 아무거나 골라서 그게 뭔지 A/C/G/T가 연속되는 문자열로 읽어내는데(sequencing), 이걸 이미 알고 있는 사람 DNA 전체 문자열(서열)에 딱 맞는 부분을 찾아서 배열했을 때 대충 10%(0.1X)에서 50%(0.5X)정도 덮일 정도로 살짝 읽어내는 것이다. 많이 읽을 수록 커버리지가 올라가고 데이터도 더 좋아지지만, 돈이 많이 든다.

커버리지가 워낙 낮다보니 뭐 다른 건 분석할 수 있는 게 거의 없고, 사람 DNA 전체에서 일정 간격으로 자른 구간에서 DNA가 몇 번 읽혔는지 세서 그 숫자를 잘 비교하는 수 밖에 없다. 아기 DNA가 10%, 엄마 DNA가 90% 인 샘플이라면, 아기 DNA가 정상적인 염색체에 비해 trisomy가 있는 염색체는 5% 정도 더 읽히게 된다. 염색체가 1벌만 들어가는 monosomy가 발생했다면 5% 적게 읽힌다.

밤톨이의 등장

원래 넷을 낳겠다는 아내의 원대한 계획에 발 맞춰 얼마 전 둘째가 생겼다. 장모님이 밤이 후두둑 떨어지는 걸 받는 꿈을 꿨다고 해서 태명이 어쩌다가 밤톨이가 됐다. 원래 병원을 별로 좋아하지 않는 아내지만, 집 앞에 있는 병원의 시스템이 특별히 ESC를 누르지 않으면 저절로 OK가 계속 눌리는 프로그램처럼 딱히 얘기하지 않아도 자꾸 검사를 하게 돼 있어서 초기 검사를 꽤 했다. 12주엔 목둘레 투명대 검사(nuchal scan)를 했는데 한참을 재더니 재는 각도에 따라서 3.0mm이 넘기도 하고 아무리 좁게 잡아도 2.7mm은 항상 넘는다고 했다. 이 두께로 나오면 여러가지 염색체 이상이 발생했거나, 앞으로 신경관/심장 발달에 문제가 있을 가능성이 높다고 양수검사나 융모막검사를 하자고 했다.

그런데 이 검사들은 침습적 검사라서 실패하면 유산 가능성이 있고, 그 가능성이 1%를 넘기도 한다. 너무 위험한 검사라서 안 그래도 하루 종일 걱정을 하는 아내가 더욱 걱정을 하기 시작했다. 아내는 며칠동안 하루 종일 목둘레 검사 결과가 안 좋았지만 정상적으로 출산한 산모들과 양수검사를 하다가 사고가 난 엄마들의 인터넷 글들을 검색해서 읽었다. 정말 끝도 없이 나왔다. -O-; 여러모로 알아보다 기저율이 그렇게 높지 않은 나이이니까 문제가 없을 가능성이 훨씬 높다고 생각하고, 위험을 무릅쓸 필요는 없겠다 싶어서 그냥 추가 검사는 하지 않고 이전 검사 결과는 그냥 무시하기로 했다.

그러던 중에, 테라젠에서 NIPT검사에 관련된 논문을 냈다는 소식을 들었다. 예전에 같이 연구했던 친구들이 요즘 회사에서 NIPT사업화 관련해서 연구를 많이 하고 있다는 얘기도 듣고, 가장 활발하게 시퀀싱 사업을 하고 있는 Illumina에서도 요즘 NIPT 프로토콜과 제품을 많이 개발하고 있던게 생각났다. 그래서 NIPT 방법에 대해서 공부도 좀 해 보고, 인간 유전체 분석은 한 번도 안 해 봤기에, 그것도 어떤 방법으로 하는지 궁금해서, 테라젠의 김태형 이사님께 연락해서 아내의 NIPT 검사 데이터를 만들었다. 아직 태어나지도 않은 아이를 DNA 데이터로 먼저 만나다니! 정말 공부도 쏙쏙 되고, 뭔가 획기적인 결과를 하나 만들 수 있을 것만 같았다. ㅋㅋ

기술적으론, 아내의 피에서 뽑은 세포 밖 DNA를 Ion Proton으로 0.3X 정도 읽어서 각 염색체에 매핑되는 리드 개수를 몇 가지 보정한 후에 서로 다른 염색체들끼리의 리드 개수 비율을 구한다. 그 비율은 염색체별로 시퀀싱 효율 차이가 있기 때문에, 똑같은 DNA 준비 과정과 똑같은 시퀀싱 플랫폼을 거친 다른 임신부들의 검사 결과를 백그라운드 분포로 잡고 비교하는 방식으로 이수성의 가능성을 계산한다.

리드 매핑하고 bin에 들어가는 리드 세기

먼저 시퀀싱센터에서 받은 데이터를 매핑해서 지놈 전체를 100kbp 단위 빈으로 나눠서 리드를 센다. 빈 크기는 50k부터 300kbp까지 다양하게 많이 쓰이지만, 이번 데이터 경우에는 100kbp가 노이즈는 적으면서도 충분한 빈 개수를 확보할 수 있는 적당한 수준이었다. 사실 빈 크기가 결과에 큰 영향을 주지는 않는다.

시퀀싱 기계에서 읽은 DNA 서열을 이미 알려져 있는 사람 DNA 서열 어느 부분에 해당하는지 찾아보는 매핑 과정과 각 빈에 들어가는 리드 수를 세는 과정은 snakemake로 파이프라인을 간단하게 만드는 게 더 쉬우니까 이걸로 일단 돌린다. 매핑은 웬만한 프로그램과 비교해선 진짜 말 그대로 100배 이상 느린 것으로 유명한 GSNAP으로 한다. 보통은 BWAbowtie2 같은 것을 많이 사용하는데, 리드 개수도 얼마 안 되고 CPU와 메모리가 아주 펑펑 써도 충분할 정도로 남아 돌아서.. ㅎㅎ

우선 데이터가 있는 곳으로 뿅

In [2]:
%cd ~/tmp/niptraw
/atp/hyeshik/tmp/niptraw

아래는 Snakefile 얼개 그림과 내용. Snakefile 맨 윗쪽에서 쓰고 있는 powersnake 모듈은 여기에서 받을 수 있다.

In [31]:
!snakemake --rulegraph | dot -Tpng > rule.png

from IPython.display import Image
Image(filename='rule.png')
Out[31]:
In [32]:
!cat Snakefile
from powersnake import *

SAMPLES = ['bamtori']
BINWIDTH = 100000
GENOME_FASTA_URL = 'http://hgdownload.soe.ucsc.edu/goldenPath/hg38/bigZips/hg38.fa.gz'
GENOME_CHROMSIZES_URL = 'http://hgdownload.soe.ucsc.edu/goldenPath/hg38/bigZips/hg38.chrom.sizes'
SNP_TABLE_URL = 'http://hgdownload.soe.ucsc.edu/goldenPath/hg38/database/snp146.txt.gz'

rule all:
    input:
        expand('{sample}.sorted.bam{sufx}', sample=SAMPLES, sufx=['.bai', '']),
        expand('{sample}.bincov.txt', sample=SAMPLES),
        'snp146.bed.gz'

rule download_genome_reference_sequence:
    output: 'reference/hg38.fa.gz'
    shell: 'wget -O {output} {GENOME_FASTA_URL}'

rule download_genome_size_table:
    output: 'reference/hg38.chrom.sizes'
    shell: 'wget -O {output} {GENOME_CHROMSIZES_URL}'

rule build_gsnap_index:
    input: 'reference/hg38.fa.gz'
    output: 'reference/hg38/hg38.genomecomp'
    shell: 'gmap_build --gunzip --circular=chrM,chrMT  \
                -D reference -d hg38 {input}'

rule align_to_genome:
    input: ref='reference/hg38/hg38.genomecomp', fastq='{sample}.fastq.gz'
    output: temp('{sample}.bam')
    threads: 32
    shell: 'gsnap -D reference -d hg38 -m 0.05 -t {threads} -A sam -B4 \
                --gunzip {input.fastq} | \
            samtools view -b -q 3 -o {output} -'

rule sort_alignments:
    input: '{sample}.bam'
    output: '{sample}.sorted.bam'
    threads: 32
    shell: 'samtools sort -@ {threads} -o {output} {input}'

rule index_alignments:
    input: '{sample}.bam'
    output: '{sample}.bam.bai'
    shell: 'samtools index {input}'

rule make_depth_table:
    input: '{sample}.sorted.bam'
    output: '{sample}.bincov.txt'
    shell: 'bedtools makewindows -g reference/hg38.chrom.sizes -w {BINWIDTH} | \
            bedtools coverage -g reference/hg38.chrom.sizes -b {input} -a - \
                -nonamecheck > {output}'

rule download_snp_table:
    output: temp('snp146.txt.gz')
    shell: 'wget -O {output} {SNP_TABLE_URL}'

rule convert_snp_to_bed:
    input: 'snp146.txt.gz'
    output: 'snp146.bed.gz'
    shell: 'zcat {input} | cut -f2-7 | gzip -c - > {output}'

# vim: syntax=snakemake

레퍼런스를 준비해서 매핑하고 빈에 들어간 리드를 센다.

In [38]:
!snakemake -j -- 2>&1 > niptrun.log

리드 개수 데이터 훑어보고 전처리하기

작업 준비가 모두 끝났으니, 이상한 것이 없는지 한 번 살펴보고 쓰기 좋게 손질을 좀 해 둔다.

우선 테이블을 불러들인다. bedtools coverage에서 나오는 포맷이다.

In [33]:
coverage = pd.read_table('bamtori.bincov.txt',
                         names=['chrom', 'start', 'end', 'count', 'nzpos', 'length', 'nzratio'])

염색체 번호 순서대로 정렬할 수 있게 키를 만들어서 정렬한다. 크기가 균일하지 않은 맨 마지막 빈들은 제거한다. Y 염색체도 온전한 빈이 550개가 넘게 나오기 때문에 굳이 궁상맞게 따로 처리할 필요는 없다. 물리적으로 실재하는 염색체 말고 가상의 염색체들(alternative chromosome이나 unplaced scaffolds 등)은 제대로 하자면 복잡하니까 미리 제거한다.

In [35]:
chrom_human_order = lambda x: 'chr{:03d}'.format(int(x[3:])) if x[3:].isdigit() else x

binsize = coverage['length'].value_counts().index[0]

covs = coverage[(coverage['length'] == binsize) & (coverage['chrom'].apply(len) <= 6)].copy()
covs['chrom_sortkey'] = covs['chrom'].apply(chrom_human_order)
covs = covs.sort_values(by=['chrom_sortkey', 'start']).reset_index(drop=True)
covs.head()
Out[35]:
chrom start end count nzpos length nzratio chrom_sortkey
0 chr1 0 100000 175 18882 100000 0.18882 chr001
1 chr1 100000 200000 87 9354 100000 0.09354 chr001
2 chr1 200000 300000 59 6184 100000 0.06184 chr001
3 chr1 300000 400000 34 4553 100000 0.04553 chr001
4 chr1 400000 500000 41 5205 100000 0.05205 chr001

새 컬럼 chrom_sortkeychrom으로 문자열 정렬하면 보통 생각하는 숫자를 기준으로 한 염색체 순서를 맞추기가 어려워서 따로 0을 덧붙여서 만들었다.

컬럼 count에 각 빈에 들어가는 리드 개수가 들어 있다. nzposnzratio는 이번엔 특별히 쓸 일은 없다.

이런 저런 개인적으로 자주 쓰는 라이브러리들을 쓰기 위해 패스를 추가한다. tailseeker 패키지는 GitHub 저장소에서 받을 수 있다.

In [37]:
import sys
sys.path.append('/atp/hyeshik/p/tailseeker')
import readline
from tailseeker import plotutils, stats
from matplotlib import style
style.use('ggplot-hs') # ggplot-hs 없으면 ggplot으로 바꾸세요

matplotlib 스타일 ggplot-hs는 내장 스타일인 ggplot에서 레이블 글꼴 크기, 색깔만 조금 바꾼 것이다. readlineconda와 시스템 기본 libreadline이 버전이 다르다보니 rpy2 모듈을 부를 때 R과 Python이 서로 다른 libreadline을 부르려고 해서 어쩔 수 없이 미리 readline을 불러야만 rpy2를 쓸 수 있게 돼서 우선 이렇게 쓰고 있다.; 다음 업그레이드할 때는 고쳐야겠다.

일단 read count가 대충 어떤 분포로 나오는지 살펴보자. 요즘 생물학에선 바이올린 플랏이 대유행이다. 유행을 좇아 본다.

In [41]:
plt.figure(figsize=(9, 3))
vp = plt.violinplot([np.log2(bins['count'] + 1) for chrom, bins in covs.groupby('chrom')],
                    showextrema=False, showmedians=True, widths=0.8)
plt.setp(vp['bodies'], facecolor='k', alpha=.65)
plt.setp(vp['cmedians'], lw=2)

# chromosome 번호로 X 틱 레이블을 단다.
chromosomes = covs['chrom'].unique()
plt.xticks(np.arange(1, len(chromosomes) + 1),
           [c[3:] for c in chromosomes])

plt.xlabel('chromosome')
plt.ylabel('log2 read count/bin')
Out[41]:
<matplotlib.text.Text at 0x7f717c7155c0>

특별한 처리 없이도 대충 7.3 근처에서 모인다. 그런데! Y염색체에서 대충 중간값이 4.2 정도 되는 한 무리가 나온다. 이 수치를 다른 상염색체와 비교하면 대략 Y 염색체의 비율이 $2^{4.2-7.3}$ 쯤 된다고 할 수 있다. 즉, 11.7% 쯤 되는데, Y염색체는 엄마 DNA에는 없으므로 전적으로 아기 DNA라고 할 수 있다. 상염색체 기준으로 하면 23.4%에 육박하는 수치이므로 보통 아기 DNA가 10-20% 나오는 걸 감안하면 Y염색체가 있다는 걸 확실히 알 수 있다. IGV로 봐도 repeat 지역 여부에 관계없이 고루 퍼져있는 것을 볼 수 있었다. Y는 반복서열이 워낙 많아서 매핑이 어렵고 centromere의 비중이 상대적으로 높기 때문에 아래에 0 근처에 깔리는 빈이 많은 것은 예상할 수 있는 결과다. 아…. 귀염둥이 딸과 노는 꿈은 물거품이.. Y_Y 그래도 아들 둘이면 같이 잘 놀테니까! ^_^

분포만 그리면 재미가 없으니까 outlier가 대충 어떻게 분포하는지 멋지게 점으로 찍어보자. 염색체들을 이어 붙여서 찍을 것이므로 다른 염색체들끼리 구분할 수 있게 무지개색을 준비한다. Y염색체는 칙칙하니까 회색으로 칠한다.

In [44]:
chroms = sorted(covs['chrom_sortkey'].unique())
chromcolors = {chrom: color for chrom, color
               in zip(chroms, plotutils.colormap_lch(len(chroms), end=360))}
chromcolors['chrY'] = '#3d3d3d'

자 이제 찍는다. 각 염색체별로 median은 보기 좋게 바이올린 플랏과 마찬가지로 줄로 긋는다. 염색체 이름은 색깔 맞춰서 중간쯤에 그리는데, 뒤로 가면 글자끼리 겹치니까 위, 아래로 번갈아 표시한다.

In [48]:
fig, ax = plt.subplots(1, 1, figsize=(10, 4))

chromnameY = 8.5
wiggle_y = .2

for i, (chrom, rows) in enumerate(covs.groupby('chrom_sortkey')):
    ax.scatter(rows.index, np.log2(rows['count']),
               edgecolor='none', s=4, alpha=0.2,
               c=chromcolors[chrom])

    center_y = np.median(np.log2(rows['count']).dropna())
    ax.plot([rows.index[0], rows.index[-1]],
            [center_y, center_y],
            c='black')
    
    center_x = np.mean(rows.index)
    ax.annotate(chrom[3:].lstrip('0'),
                (center_x, chromnameY + (wiggle_y * (i % 2) * 2 - wiggle_y)),
                color=chromcolors[chrom], ha='center')

plotutils.apply_dropped_spine(ax)

ax.set_ylabel('Reads per 100k bin (log2)')
ax.set_xlim(-100, 15800*2)
ax.set_ylim(2, 10)
Out[48]:
(2, 10)

centromere 지역에서 집중적으로 빠지거나 튀는 부분이 눈에 띈다. 뭐 크게 문제는 아니다. 특별히 상염색체들끼리는 돋보이는 차이는 없고, 19번과 22번이 다른 것들보다 좀 떨어진다. 19번은 유전자 밀도가 워낙 높아서 GC ratio도 독보적으로 높고, 그 덕에 시퀀싱이 잘 안 되는 것으로 유명한다. 자세히 보지는 않았지만 아마도 그 원인이겠지? ; 22번도 유전자 밀도가 좀 높은 편이다.

사실 이수성(aneuploidy)이 있으면 log2 스케일로 0.15 정도 위아래로 왔다갔다 할 것이다. trisomy가 있다면 22번이 아래로 내려온 정도로 위로 튀면 된다. trisomy가 가장 잘 발생하는 21번 (다운 증후군), 18번 (에드워즈 증후군), 13번 (파타우 증후군) 염색체들은 다행히도 대충 대세를 따른다. 사실 이렇게 한 데이터만 놓고서 분석해서는 정확히 알기는 힘들고, 같은 플랫폼에서 검사한 다른 사람들의 데이터가 많이 모여야 정상치의 범위를 알 수 있는데.. 일단은 데이터가 1개 밖에 없으니 이걸로 대충 보고 만족한다.

GC 비율로 보정하기

사실 웬만한 것은 상용화될 수준까지 가면 간단하지 않은 문제가 돼 버린다. NIPT를 이용한 이수성 검사에서도 다른 사람들의 데이터와 비교하는 방식으로 통계적 유의성을 구하지만, 데이터를 좀 더 균일하게 만드는 여러가지 편향 보정법이 알려져있다.

가장 대표적인 것은 GC 비율 보정인데, 간단하다보니 대부분 업체에서 이 보정은 거의 반드시 채택하고 있다. DNA를 구성하는 AT/GC 중에 염색체에서 GC가 많이 들어있는 지역은 DNA 2가닥끼리 서로 붙어있으려는 힘이 훨씬 강하다. 유전체 전체를 시퀀싱할 때는 DNA를 그냥 쓰면 너무 길어서 다루기가 힘들어서, 초음파를 쏘는 등의 방법으로 DNA를 물리적으로 갈기갈기 찢은 다음에 읽는다. (여기서 쓰는 초음파는 산부인과에서 쓰는 초음파보다 훨씬 세다. 물 속에 담그고 쓰는데도 어쩌다 들으면 귀가 멍멍하다.) 그런데 GC가 많은 지역은 서로 붙어있으려는 힘이 강하다보니 그냥 흔드는 방법으로는 훨씬 덜 찢어져서, 시퀀싱까지 살려서 가기 어렵다. 그래서 GC가 많은 지역은 수가 적게 나올 수 밖에 없다.

그럼 우리도 한 번 해 보자! 먼저 사람 유전체에서 앞에서 계속 쓰던 100kbp 구간별로 GC 비율을 구한다. 이 비율은 굳이 귀찮게 프로그램 만들지 않아도 bedtools느님이 기본제공해 주신다. 하지만 랜덤액세스를 하기 때문에 gzip 압축된 상태로는 못하니까 일단 풀고 한다.

In [73]:
!zcat reference/hg38.fa.gz > reference/hg38.fa
In [57]:
genome_nuc_comp = !bedtools makewindows -g reference/hg38.chrom.sizes -w 100000 | bedtools nuc -fi reference/hg38.fa -bed -
In [58]:
import io
nuccomp = pd.read_table(io.StringIO('\n'.join(genome_nuc_comp))).iloc[:, :5]
nuccomp.columns = ['chrom', 'start', 'end', 'pct_at', 'pct_gc']
nuccomp.head()
Out[58]:
chrom start end pct_at pct_gc
0 chr1 0 100000 0.51793 0.38207
1 chr1 100000 200000 0.54185 0.45815
2 chr1 200000 300000 0.28508 0.19460
3 chr1 300000 400000 0.27479 0.24553
4 chr1 400000 500000 0.59808 0.40192

몇 개 빈에서 AT와 GC를 합쳐도 1이 안 되는 게 있다. 베이스를 모르는 N때문에 그런데, N은 AT인지 GC인지 모르니까 그냥 얘네들은 빼고 다시 계산한다.

In [59]:
nuccomp['pct_gc'] = nuccomp['pct_gc'] / (nuccomp['pct_gc'] + nuccomp['pct_at'])
del nuccomp['pct_at']

이제 앞에서 구해뒀던 빈별 커버리지 데이터와 GC비율을 합치고, 예상했던대로 GC비율과 리드 카운트가 관계가 있는지 보자.

In [66]:
covs_with_gc = pd.merge(covs, nuccomp, left_on=['chrom', 'start', 'end'],
                        right_on=['chrom', 'start', 'end']).dropna()
covs_with_gc.plot(kind='scatter', x='pct_gc', y='count',
                  edgecolor='none', s=5, c='black')
Out[66]:
<matplotlib.axes._subplots.AxesSubplot at 0x7f7178ebc710>

위에 엄청나게 튄 outlier들때문에 뭘 볼 수가 없다. 이게 보이더라도 너무 겹쳐서 밀도가 잘 안 보일 것 같으니, 점의 밀도를 KDE로 색깔로 같이 찍어본다.

In [68]:
from scipy.stats import gaussian_kde
gc_count_mtx = covs_with_gc[['pct_gc', 'count']].as_matrix().T
density = gaussian_kde(gc_count_mtx, bw_method=0.1)(gc_count_mtx)
In [70]:
fig, axes = plt.subplots(1, 2, figsize=(10, 4))

covs_with_gc.plot(kind='scatter', x='pct_gc', y='count', edgecolor='none', s=5,
                  c=density, vmin=0, cmap='jet', ax=axes[0])
covs_with_gc.plot(kind='scatter', x='pct_gc', y='count', edgecolor='none', s=5,
                  c=density, vmin=0, cmap='jet', ax=axes[1])
axes[1].set_ylim(0, 500)
axes[1].set_xlim(0.3, 0.65)

plt.tight_layout()

오! GC 비율이 36-37%정도 되는 빈들이 가장 잘 나오고, 거기서 더 올라가면 떨어진다. 대략 GC 60%까지 가면 리드가 40% 정도 줄어드는 것 같다. AT가 적을 때도 약간 떨어지는 듯한 모양인데, 아마도 조각이 너무 잘아져서 크기로 정제하는 부분에서 없어지지 않을까 싶다. 이제 이걸로 LOESS regression을 하는 것이 생물학에서 보통 쓰는 레퍼토리인데.. GC 비율이 35%에서 42% 쯤 되는 지역의 아랫쪽에 아웃라이어가 꽤 있어서 약간 울퉁불퉁하게 나오기가 쉬울 것 같다. 아웃라이어들이 어떤 놈들인지 찾아봐서 제거하면 좋겠지만, 주업이 아니므로 대충 그냥 마구 잘라서 바로 LOESS에 집어넣는다. 아마도 centromere, heterochromatin이거나 reference와 다른 부분 등이 아닐까 싶다.

In [118]:
from sklearn import svm

gc_count_mtx_training = gc_count_mtx[:, gc_count_mtx[1] > 90]
model = svm.OneClassSVM(nu=0.06, kernel="rbf", gamma=0.5).fit(gc_count_mtx_training.T)
isinlier = model.predict(gc_count_mtx.T) == 1

gc_count_inliers = gc_count_mtx[:, isinlier]
fit_x, fit_y = stats.loess_fit(gc_count_inliers[0], gc_count_inliers[1],
                               np.linspace(0, 1, 200), alpha=.4)

fig, axes = plt.subplots(1, 2, figsize=(10, 4))

covs_with_gc.plot(kind='scatter', x='pct_gc', y='count', edgecolor='none', s=5,
                  c=isinlier, vmin=-.1, vmax=1.1, cmap='RdBu_r', ax=axes[0])
covs_with_gc.plot(kind='scatter', x='pct_gc', y='count', edgecolor='none', s=5,
                  c=isinlier, vmin=-.1, vmax=1.1, cmap='RdBu_r', ax=axes[1])
axes[1].set_ylim(0, 500)
axes[1].set_xlim(0.3, 0.65)
axes[1].plot(fit_x, fit_y, c='magenta', lw=3)
axes[0].plot(fit_x, fit_y, c='magenta', lw=3)

plt.tight_layout()

빨간색이 one class SVM으로 잡은 인라이어. 아래에 깔리는 아웃라이어들은 모델 파라미터에 넣어서 없애려니 금방 안 돼서 그냥 입력값을 미리 잘라서 넣어버렸다 -ㅇ-; 가운데 심지지역의 모서리 부분은 어차피 밀도가 중간에 비해서 많이 낮은 편이라 큰 영향은 안 주리라 보고 그냥 이대로 간다. 하하하하. =3 마젠타색 선이 LOESS 중심선인데, 대충 모양에 맞게 나왔다.

이제 원래 데이터를 LOESS 중심선에 맞춰서 끌어올린 다음에 원래 데이터와 보정한 데이터를 비교해 본다. log가 다루기 편하니, 이제 중심선에 비교해서 얼마나 enrich되었는지 log로 계산해서 쓴다.

In [125]:
gc_inliers_center = np.vstack(stats.loess_fit(gc_count_inliers[0], gc_count_inliers[1],
                                              gc_count_inliers[0], alpha=.4))

gc_logenr = gc_count_inliers.copy()
gc_logenr[1] = np.log2(gc_count_inliers[1] / gc_inliers_center[1])

orig_density = gaussian_kde(gc_count_inliers, bw_method=0.1)(gc_count_inliers)
adj_density = gaussian_kde(gc_logenr, bw_method=0.1)(gc_logenr)
In [129]:
fig, axes = plt.subplots(1, 2, figsize=(10, 4))

axes[0].scatter(gc_count_inliers[0], gc_count_inliers[1