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

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

장혜식
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], c=orig_density, edgecolor='none', s=5)
axes[0].set_xlabel('pct_gc')
axes[0].set_ylabel('mapped reads')

axes[1].scatter(gc_logenr[0], gc_logenr[1], c=adj_density, edgecolor='none', s=5)
axes[1].set_xlabel('pct_gc')
axes[1].set_ylabel('adjusted mapped reads')
axes[1].set_ylim(-1, 1)

plt.tight_layout()

왼쪽은 GC비율로 보정하기 전, 오른쪽은 보정 후. GC에 맞춰서 파도치던 분포가 올곧게 딱 잡혔다. 후후훗.

이제 원래 작업하던 테이블에 보정된 값을 넣어둔다.

In [148]:
_, expected = stats.loess_fit(gc_count_inliers[0], gc_count_inliers[1],
                              covs_with_gc['pct_gc'], alpha=.4)
covs_w_logenr = covs_with_gc.copy()
covs_w_logenr['log2enrichment'] = np.log2(covs_w_logenr['count'] / expected)
covs_w_logenr = covs_w_logenr[covs_w_logenr['count'] > 0].dropna()

GC보정된 리드 밀도 살펴보기

혹시나 하는 마음에 Y염색체를 다시 살며시 들여다 본다.

In [174]:
chrY_enrichment = covs_w_logenr[covs_w_logenr['chrom'] == 'chrY']['log2enrichment']
xv = np.linspace(-8, 4, 100)
yv = gaussian_kde(chrY_enrichment, bw_method=0.2)(xv)
plt.plot(xv, yv)
plt.xlabel('log2 enrichment')
plt.ylabel('density')
plt.title('chromosome Y')
xv[np.argmax(yv)]
Out[174]:
-3.1515151515151514

대략 -3.15 정도에서 피크가 나온다. $2^{-3.15}=0.1127$ 로.. 여전히 아들이 확실해보인다.

앞에서 그렸던 빈 단위 커버리지를 이번에 GC보정한 log2 enrichment로 그리면 다른 것도 좀 더 깔끔히 나올까?

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

chromnameY = -1.2
wiggle_y = .05

for i, (chrom, rows) in enumerate(covs_w_logenr.groupby('chrom_sortkey')):
    ax.scatter(rows.index, rows['log2enrichment'],
               edgecolor='none', s=4, alpha=0.4,
               c=chromcolors[chrom])

    center_y = np.median(rows['log2enrichment'].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, 31000)
ax.set_ylim(-4, 1)
Out[180]:
(-4, 1)

오! 아까 GC보정 전엔 22번 염색체도 꽤 튀었는데, 이제 괜찮다. 19번 염색체도 훨씬 덜 튀어보인다. X 염색체는 GC보정 전에는 특별히 다른 상염색체와 달라보이지 않았는데, 이제 떨어진 게 확실히 보인다. 아기 DNA에 Y 때문에 X가 한 카피 적게 들어있으므로 대략 5-10% 정도 (log2로 0.07 정도) 빠지는 게 정상이다. 아들이 확실하다.;

그나저나 19번 염색체가 튀는 건 어떤 이유 때문인지 유전자가 없는 intergenic region만 골라서 해 본다던지 좀 더 자세히 들여다 봐야 할 것 같다.

엄마와 아빠 유전형으로 아기 DNA 더 세밀하게 구분하기

여기까지는 그냥 각 염색체별 리드 개수만 열심히 세고, 서열에서 나오는 상세한 정보는 따로 활용하지는 않았다. 시퀀싱의 멋은 그냥 세는 게 아니라 서열 내용을 자세히 분석하는 것 아닌가!
엄마 아빠의 유전형을 알면 읽어낸 서열로 더 자세한 분석이 가능한데, 아기 DNA는 엄마 DNA 반, 아빠 DNA 반을 물려 받으니까, 엄마와 아빠가 다른 유전형을 갖고 있는 부분을 봐서, 아빠와 유전형이 맞는 것을 고르면 아기 DNA의 반에 대해 거의 정확한 비율을 추산할 수 있게 된다.

상용 NIPT에서는 비용 문제 때문에 엄마와 아빠의 유전형 정보를 쓰지는 않지만, 우리 부부는 유전형 정보를 미리 갖고 있었기 때문에, 우리의 유전형 정보로 아기 DNA를 좀 더 세분화 해보기로 했다. 23andMe 데이터를 이런 곳에 처음으로 유용하게 써 볼 줄은 몰랐네..;

미리 준비를 좀 해야 한다. 23andMe는 GRCh37 기준 좌표계를 쓰는데, 우리는 여기서 GRCh38을 쓰고 있어서 좌표 변환이 필요하다. 굳이 번거롭게 변환하지 말고 UCSC Genome Browser가 정리해 놓은 dbSNP 데이터에서 GRCh38 좌표를 바로 얻어온다. dbSNP는 메모리에 띄워놓고 있기에 부담스러울 정도로 방대하니, 23andMe에 있는 것만 추린다.

In [182]:
snptbl = pd.read_table('snp146.bed.gz', compression='gzip',
                       names=['chrom', 'start', 'end', 'name', 'score', 'strand'])
snpcalls_m = pd.read_table('genotype-mother.txt.gz', compression='gzip',
                       comment='#', names=['name', 'chrom', 'position', 'genotype'],
                       low_memory=False)
snpcalls_f = pd.read_table('genotype-father.txt.gz', compression='gzip',
                       comment='#', names=['name', 'chrom', 'position', 'genotype'],
                       low_memory=False)
snpcalls = pd.merge(
    snpcalls_m[['name', 'genotype']], snpcalls_f[['name', 'genotype']],
    how='inner', left_on='name', right_on='name', suffixes=['_mo', '_fa'])
snpmerged = pd.merge(snptbl, snpcalls, how='inner', left_on='name', right_on='name')
snpmerged.to_csv('snpcalls.bed.gz', compression='gzip', header=False, index=False, sep='\t')
del snptbl, snpcalls_m, snpcalls_f, snpcalls

23andMe에서 제공하는 SNP으로 제한한 bed가 준비됐다. 지놈에 정렬된 서열들에서 이 SNP loci에 정렬된 녀석들을 골라온다.

In [184]:
!samtools mpileup -l snpcalls.txt.gz bamtori.sorted.bam | cut -f1,2,4,5,6 | gzip -c - > snpmatched.txt.gz
[mpileup] 1 samples in 1 input files
<mpileup> Set max per-file depth to 8000
In [186]:
serum_snps = pd.read_table('snpmatched.txt.gz', compression='gzip',
                          names=['chrom', 'position', 'reads', 'basecalls', 'basequalities'])
serum_snps['position'] -= 1

parental_snps =  pd.read_table('snpcalls.bed.gz', compression='gzip',
                          names=['chrom', 'start', 'end', 'name', 'score', 'strand',
                                 'mother', 'father'])

snps = pd.merge(parental_snps, serum_snps,
                how='inner', right_on=['chrom', 'position'],
                left_on=['chrom', 'start']).dropna()
snps.head()
Out[186]:
chrom start end name score strand mother father position reads basecalls basequalities
0 chr1 841165 841166 rs12124819 0 + AA AA 841165 1 A @
1 chr1 864626 864627 rs6681049 0 + CT CT 864626 1 T <
2 chr1 989517 989518 rs6665000 0 + AA AA 989517 1 a :
3 chr1 1095184 1095185 rs6687776 0 + CC CC 1095184 1 C-1N 7
4 chr1 1127257 1127258 rs9442373 0 + AC AC 1127257 1 A <

워낙 기본적인 리드 커버리지가 낮기 때문에 SNP별로 뭘 해 보기는 어렵고, 그냥 일치되는 게 있으면 모두 독립적인 SNP인 것처럼 따로 데이터를 취합한다. 엄마와 아빠가 유전형이 같은 곳에서는 아무 정보도 없으니까 엄마와 아빠가 유전형이 다른 곳만 일단 쓴다. 다른 곳은 이렇게 세 가지 유형이 있을 수 있다.

| 번호 | 엄마    | 아빠    |   예     |
|-----|--------|---------|---------|
|  1  | homo   | homo    | GG / AA |
|  2  | homo   | hetero  | GG / AG |
|  3  | hetero | homo    | AG / GG |

2번과 3번의 경우엔 확률 계산이 복잡해지니까 깨끗하게 떨어지는 1번을 우선 쓰자. 양쪽이 모두 homozygote이고 다른 타입이므로 직접적으로 비율을 계산할 수 있다. 아기는 무조건 heterozygote가 된다.

In [193]:
import re

called_homozygote = lambda gt: gt[0] == gt[1] if len(gt) == 2 and gt[0] != '-' else False
def classify_homozygotic_parental_snps(basereads, maternal, paternal,
                                       pat=re.compile('([+-][0-9]+)?([ACGTNacgtn])')):
    r = [0, 0, 0] # maternal matched, paternal matched, matched to neither of them
    for indelmark, base in pat.findall(basereads):
        if indelmark or base in 'nN':
            pass # skip it to simplify the analysis.

        ubase = base.upper()
        if ubase == maternal:
            r[0] += 1
        elif ubase == paternal:
            r[1] += 1
        else:
            r[2] += 1
    return r

snps_mofa_diffs = snps[snps['mother'] != snps['father']].copy()
snps_homo_homo = snps_mofa_diffs[
    snps_mofa_diffs['mother'].apply(called_homozygote) &
    snps_mofa_diffs['father'].apply(called_homozygote)]

snpcounts_homo_homo = pd.DataFrame(
    snps_homo_homo.apply(lambda row:
                         classify_homozygotic_parental_snps(row['basecalls'], row['mother'][0],
                                                            row['father'][0]), axis=1).tolist(),
    index=snps_homo_homo.index,
    columns='mother father mismatched'.split())
In [196]:
snpcounts_homo_homo.sum(axis=0)
Out[196]:
mother        10698
father          743
mismatched      113
dtype: int64

엄마와 아빠가 서로 다른 타입으로 homozygote인 SNP중에 엄마와 같은 것이 10698개, 아빠와 같은 것이 743개다. 아빠에 맞는 것이 아기DNA의 대략 반이므로, 아빠에 맞는 것의 2배를 아기 DNA 비율로 추산할 수 있다.

In [198]:
743/(743+10698)*100*2
Out[198]:
12.98837514203304

아기 DNA의 양은 좀 더 정확한 추산치는 이제 대략 13% 정도다. 오오.

그리고, 여기서 하나 더 알 수 있는 것은, 엄마와 아빠의 대립 SNP 중에 엄마 SNP에 압도적으로 많이 매치되는 것을 보아, 다른 사람의 피나 다른 사람의 샘플이 섞여서 엉뚱하게 나온 게 아니란 것이다.

그렇다면, 엄마에도 매치되지 않고 아빠에도 매치되지 않는 113개는 무엇일까? 거의 대부분 SNP은 A/G나 C/G 같이 두 염기가 대립하지 세 가지가 나오는 것은 드물다. 저것들은 아마도 시퀀싱에러가 아닐까 싶은데.. 확실히 해 보기 위해서 오독했을 가능성이 높은지 확인해 보자. 오독률을 알 수 있는 “리드 퀄리티”는 phred score로 보통 나타낸다. 10점이면 90% 정도 확률로 정확함, 20점이면 99% 확률로 정확함, 30점이면 99.9% 확률로 정확함을 나타낸다. 모두 기계에서 주는 추정값이다.

In [201]:
def iter_snpcall_reads(tbl):
    parse_basecalls = re.compile('([+-][0-9]+)?([ACGTNacgtn])')
    for _, row in tbl.iterrows():
        maternaltype = row['mother'][0]
        paternaltype = row['father'][0]
        basecalls = parse_basecalls.findall(row['basecalls'])
        for (indel, baseread), phredqual in zip(basecalls, row['basequalities']):
            if not indel and baseread not in 'nN':
                yield row['chrom'], row['position'], baseread.upper(), ord(phredqual) - 33, maternaltype, paternaltype

homosnpreads = pd.DataFrame.from_records(iter_snpcall_reads(snps_homo_homo),
                                         columns=['chrom', 'position', 'baseread', 'quality', 'maternal', 'paternal'])
assert (homosnpreads['baseread'] == homosnpreads['maternal']).sum() == 10665
In [208]:
maternal_reads = \
    homosnpreads[(homosnpreads['baseread'] == homosnpreads['maternal'])]
paternal_reads = \
    homosnpreads[(homosnpreads['baseread'] == homosnpreads['paternal'])]
mismatched_reads = \
    homosnpreads[(homosnpreads['baseread'] != homosnpreads['maternal']) &
                 (homosnpreads['baseread'] != homosnpreads['paternal'])]

mat_x, mat_y = plotutils.prepare_cumulative(maternal_reads['quality'])
pat_x, pat_y = plotutils.prepare_cumulative(paternal_reads['quality'])
mismatched_x, mismatched_y = plotutils.prepare_cumulative(mismatched_reads['quality'])

plt.plot(mat_x, mat_y, label='Maternal-matching loci')
plt.plot(pat_x, pat_y, label='Paternal-matching loci')
plt.plot(mismatched_x, mismatched_y, label='Mismatched loci', ls='--', c='magenta')
plt.legend(loc='best', fontsize=12)
Out[208]:
<matplotlib.legend.Legend at 0x7f7031098a20>

오.. 예상했던대로 엄마 것도 아니고 아빠 것도 아닌 SNP들은 리드 퀄리티가 현저히 떨어진다. 중간값이 대략 20쯤 된다.

자 그렇다면 염색체별로 차이가 있을 수 있으니 아빠:엄마 비율을 자세히 살펴보자.

In [211]:
r = []
for chrom, grp in homosnpreads.groupby('chrom'):
    if len(chrom) > 5:
        continue
    maternal_reads =  grp[(grp['baseread'] == grp['maternal'])]
    paternal_reads = grp[(grp['baseread'] == grp['paternal'])]
    mismatched_reads = grp[(grp['baseread'] != grp['maternal']) &
                           (grp['baseread'] != grp['paternal'])]
    r.append((chrom, len(maternal_reads), len(paternal_reads), len(mismatched_reads)))
    
homosnpmatches = pd.DataFrame(r, columns=['chrom', 'maternal', 'paternal', 'mismatched'])
homosnpmatches['p2m_ratio'] = homosnpmatches['paternal'] / (homosnpmatches['maternal'] + homosnpmatches['paternal'])
homosnpmatches.sort_values(by='p2m_ratio')
Out[211]:
chrom maternal paternal mismatched p2m_ratio
22 chrX 2 0 0 0.000000
14 chr22 186 7 0 0.036269
6 chr15 343 16 6 0.044568
19 chr7 602 29 2 0.045959
16 chr4 606 31 2 0.048666
4 chr13 390 23 2 0.055690
10 chr19 164 10 0 0.057471
15 chr3 758 48 3 0.059553
11 chr2 931 60 2 0.060545
20 chr8 620 40 2 0.060606
3 chr12 538 35 3 0.061082
5 chr14 290 19 1 0.061489
0 chr1 851 57 3 0.062775
1 chr10 615 44 11 0.066768
18 chr6 652 49 4 0.069900
21 chr9 537 43 1 0.074138
12 chr20 244 20 2 0.075758
2 chr11 571 47 4 0.076052
9 chr18 404 34 0 0.077626
8 chr17 229 20 2 0.080321
17 chr5 664 59 3 0.081604
7 chr16 278 25 2 0.082508
13 chr21 157 20 1 0.112994

X 염색체는 아빠가 1쪽 밖에 없어서 그런지 여기서 비교할 수 있는 것 자체가 적게 나왔다. (총 2개) 대부분 전체 유전체 평균 수치인 6.5% 근처로 나오는데, 22번 염색체가 유난히 적고, 21번 염색체가 유난히 많다. 21번 염색체는 수가 20개 밖에 되지 않지만, 아빠 SNP이 다른 염색체의 거의 2배가 나왔다. 이항분포로 보면,

In [222]:
%load_ext rpy2.ipython
%R pbinom(20, 157+20, 0.065, lower.tail=FALSE)
The rpy2.ipython extension is already loaded. To reload it, use:
  %reload_ext rpy2.ipython
Out[222]:
array([ 0.00574275])

흔히 나올 만한 숫자는 아니다. 21번 염색체가 더 많이 들어간 걸까? 좀 더 숫자를 늘리기 위해서 엄마는 homozygote, 아빠는 heterozygote인 경우도 해 보자.

In [226]:
snps_homo_hetero = snps_mofa_diffs[
    snps_mofa_diffs['mother'].apply(called_homozygote) &
    ~snps_mofa_diffs['father'].apply(called_homozygote)]

def iter_snpcall_reads_father_hetero(tbl):
    parse_basecalls = re.compile('([+-][0-9]+)?([ACGTNacgtn])')
    for _, row in tbl.iterrows():
        maternaltype = row['mother'][0]
        paternaltype = ''.join(set(row['father']) - set(maternaltype))
        if maternaltype == '-' or paternaltype == '-':
            continue
        basecalls = parse_basecalls.findall(row['basecalls'])
        for (indel, baseread), phredqual in zip(basecalls, row['basequalities']):
            if not indel and baseread not in 'nN':
                yield row['chrom'], row['position'], baseread.upper(), ord(phredqual) - 33, maternaltype, paternaltype

snpreads_fhetero = pd.DataFrame.from_records(iter_snpcall_reads_father_hetero(snps_homo_hetero),
                                         columns=['chrom', 'position', 'baseread', 'quality', 'maternal', 'paternal'])
In [227]:
r = []
for chrom, grp in snpreads_fhetero.groupby('chrom'):
    if len(chrom) > 5:
        continue
    maternal_reads =  grp[(grp['baseread'] == grp['maternal'])]
    paternal_reads = grp[(grp['baseread'] == grp['paternal'])]
    mismatched_reads = grp[(grp['baseread'] != grp['maternal']) &
                           (grp['baseread'] != grp['paternal'])]
    r.append((chrom, len(maternal_reads), len(paternal_reads), len(mismatched_reads)))
    
snpmatches_fhetero = pd.DataFrame(r, columns=['chrom', 'maternal', 'paternal', 'mismatched'])
snpmatches_fhetero['p2m_ratio'] = snpmatches_fhetero['paternal'] / (snpmatches_fhetero['maternal'] + snpmatches_fhetero['paternal'])
snpmatches_fhetero.sort_values(by='p2m_ratio')
Out[227]:
chrom maternal paternal mismatched p2m_ratio
22 chrX 4258 3 52 0.000704
9 chr18 1026 26 3 0.024715
17 chr5 2082 60 11 0.028011
21 chr9 1626 48 6 0.028674
4 chr13 1341 43 4 0.031069
0 chr1 2912 95 18 0.031593
1 chr10 2033 68 24 0.032366
15 chr3 2456 85 19 0.033451
5 chr14 1172 41 5 0.033800
11 chr2 2745 97 13 0.034131
18 chr6 2105 76 15 0.034846
12 chr20 871 32 4 0.035437
19 chr7 1912 71 10 0.035804
20 chr8 1853 75 11 0.038900
3 chr12 1660 68 7 0.039352
16 chr4 2143 88 9 0.039444
10 chr19 509 21 5 0.039623
7 chr16 966 40 9 0.039761
14 chr22 408 17 5 0.040000
2 chr11 1764 75 14 0.040783
8 chr17 877 40 8 0.043621
6 chr15 1137 52 12 0.043734
13 chr21 427 22 2 0.048998

오. 여기서 본 SNP 중에 X염색체에 있는 4000개가 넘는 SNP가 거의 몽땅 엄마에 매치된다. 역시 아들이다. ㅎㅎㅎ; (딸이면 아빠에게서도 X염색체를 물려받아서, 이 비율이 다른 상염색체와 거의 같게 나와야 한다.)

자 여기서도 21번이 다른 것들보다 유난히 높다. 아빠가 hetero이고 엄마가 homo이기 떄문에, 아기 DNA의 4분의 1은 아빠와 엄마가 다른 SNP loci에서 엄마와 맞지 않고 아빠와 맞게 된다. 따라서, 이 비율에 2배 하면 아빠한테서 물려받은 아기 DNA 비율을 추산할 수 있다.

21번 염색체의 수치인 4.9%는 아빠한테서 9.8% 정도를 물려받았다는 것이 된다. 앞에서 계산한 아기 DNA의 비율이 13% 정도 되기 때문에, 21번 염색체의 경우 거의 75%를 아빠한테서 물려받은 게 된다. 리드 수를 이용한 파이프라인에서는 trisomy 21 가능성은 거의 없다고 했다. 앞에서 homo-homo SNP들의 경우엔 다른 염색체들의 2배 가까이 아빠에 매치되었으니까, 종합하면 21번 염색체 두 카피를 모두 아빠한테서 받고 엄마한테서는 받지 않은 paternal uniparental disomy (patUPD)의 가능성도 무시할 수 없다.

하지만 논문을 열심히 뒤져보니, 21번 염색체의 patUPD가 워낙 드문 현상인데다, 21번 염색체는 다행히도 아직 UPD에 의한 표현형(즉 눈에 띄는 질병 또는 특징)이 보고된 것이 없다. 완전 정상이라고 보고된 사례만 몇 있다. 그래서 21번 염색체 patUPD도 가능성이 없는 것은 아니지만, 기저율 덕에 실질적인 가능성은 낮을 것 같고. 실제로 patUPD라고 하더라도 특별히 문제있을 것 같지 않다.

SNP 데이터로 뭘 좀 더 해보면 좋을텐데, 아내와 내가 다른 유전형이면서, 재미있는 정보가 알려진 것이 있고, 리드가 그래도 2개는 되는 걸 찾아보니 하나도 없었다. 커버리지가 이수성을 보는 것에 최적화되어 있다보니 어쩔 수 없는 것 같다. 몇 년 지나서 시퀀싱이 아주 싸지게 되면 전가족 시퀀싱을 한 번 해 보면 재미있겠다~

결론

여러가지 재미있게 분석을 해서 얻은 결론은

  • 아들임이 확실하다는 독립적인 증거를 셋이나 찾음. (Y염색체 비율, X염색체 비율 감소, X염색체 SNP)
  • 다른 사람 샘플과 뒤바뀌었을 가능성은 거의 없음.
  • 21번 염색체 patUPD가 있을 수도 있지만 가능성은 매우 낮고, 있더라도 아기에 큰 영향은 없을 듯하다.
  • 커버리지 0.3X는 생각보다 더 리드 구경하기가 힘들다. 대부분 SNP은 한 번도 읽히지 않고, 읽힌 것도 대부분 딱 1번 읽혀서, SNP단위로 뭘 해 보기는 어렵다.
  • 이수성만 검사하는 경우라면 커버리지를 훨씬 더 내려서 가격을 내릴 수 있을 것 같다.

말로만 듣던 NIPT 데이터를 대충이나마 직접 한 번 다뤄보니 감촉이 약간 느껴지는 것 같기도 하고 어떤 것인지 이해가 좀 됐다. 아직 눈으로 보지도 못한 둘째를 데이터로 미리 만나보니 그것도 기분이 참 좋았다. ^^; 셋째가 나오기 전에 이번 데이터로 뭔가 새로운 방법이나 개선점을 하나 만들 수 있으면 좋을텐데~

한국에서 23andMe 하는 과정

처음으로 대중적인 관심을 집중적으로 받은 오락유전체학 (entertainment genomics) 상품인 23andMe가 서비스를 시작한지 3년이 다 돼가고 있습니다. 언론과 학계의 관심 뿐만 아니라 사업적으로도 상당히 잘 되고 있다는 것이 제법 긍정적이죠. 이게 처음에 워낙 비싸서 정말 얼리어답터나 유전병에 관심이 많은 사람이 아니면 하기가 어려웠지만, 이제는 200달러에 비교적 쉽게 침을 한 번 뱉어볼 수 있는 정도가 됐습니다. 그래서 아 그래 나도 맨날 엄한 암세포나 대장균 유전자말고 내 유전자하고 좀 놀아보자 하고 마음먹고 해 보았지요~

이미 홍창범님김형용님께서 해 보셨기 때문에 조언을 얻어서 진행해 보았습니다. 미국에 막 부탁할 만한 지인이 없는 경우도 쉽게 할 수 있도록 최대한 지인활용을 배제하는 것을 원칙으로.. (?;;)

신용카드 결제하기

우선 23andMe 사이트에 가서 주문합니다. 그런데, 주문할 때 아주 큰 문제는 한국을 결제 주소로 쓸 수가 없고, 신용카드와 결제주소가 영 안 맞으면 결제가 거부되기 때문에, 이 부분에 한해서는 지인을 활용하여 외국 신용카드를 써야합니다. 미국 카드도 괜찮고 일본 카드도 되는데, 저는 일본에 계신 거친마루님께 제가 침을 좀 뱉어보고자 하니 결제 한 번만 해 주십시오 하여 대신 결제를 해 주셨습니다. ^_^ (감사!) 단, $99에 1년 서비스를 추가 결제해야되게 돼 있어서 여러 번 신세를 지지 않으려면 선물세트($207)로 사는 것이 더 편하겠습니다.

23andMe 키트

키트 배송 방법

침을 담아가는 키트를 받을 때 주소로 한국을 쓸 수 없어서 지인을 활용하거나 요즘 아주 많은 배송대행 업체를 써야 합니다. 배송대행도 요새 크게 비싼 편이 아니라서 배송대행을 하기로 하고, 가격도 비교적 싸고 잔문제가 별로 없었던 세븐존이라는 업체에 보냈습니다. 오레곤으로 보내면 미국 내 세금이 안 붙는 대신 좀 느리고, 캘리포니아로 보내면 순식간에 도착하는 대신 세금이 붙습니다. 키트가 워낙 가벼워서 배송비와 대행료를 합쳐서 13000원이면 충분했습니다.

국내 통관

통관용 물품신고를 배송대행업체에 신청할 때 작성해야하는데요, 여기서 얼마로 쓰느냐에 따라서 세금이 달라집니다. 보통 통관할 때 구입한 사이트와 똑같이 생긴게 가격도 같으면 빨리 된다길래 $99로 신고했더니만 관세가 꽤 붙어버렸네요. 2개 통관에 45590원 냈습니다. 그런데 나중에 확인해보니까 키트를 다시 받을 때 23andMe가 받는 가격이 $25라고 하니까, $25로 신고하면 관세도 안 붙고 좋을 것 같은데.. 다음엔 그 쪽으로 해 봐야겠네요;

침 뱉기

튜브가 도착하면 침을 모아서 잘 뱉으면 됩니다. 사용방법은 친절하게 써 있으니 그냥 그대로 하면 되고, 뚜껑에 모여있는 보존액이 충분히 침하고 섞어주고 열심히 흔들어서 DNA분해효소를 무력화하는 것이 중요합니다. 튜브는 DNA Genotek의 ORAGene-DNA OG-500의 OEM 제품이 옵니다. 자세한 정보는 매뉴얼에 안 써 있는데, 특허출원내용을 보면 뚜껑에 있는 보존액의 주성분은 Mg2+를 잡아서 DNase 활성을 막아주는 킬레이팅 에이전트 (EDTA일 확률이 높지만 CDTA, DTPA, DOTA, TETA 등도 언급되어 있음)와, 단백질 구조를 풀어서 DNase를 너덜너덜(;)하게 해 주는 환원제 (2-mercaptoethanol, DTT, dithionite, ascorbic acid 등 여러가지 중 하나), 항산화제 (항산화 비타민 등), pH 유지를 위한 버퍼 에이전트 (Tris, HEPES 등 중 하나)를 포함하고 있다고 합니다. 예상가능한 조합이지만 구체적으로 뭐라고 안 써놔서 실제로 받은 키트에 뭐가 들어있을지는 모르겠네요. DTT나 2-mercaptoethanol 특유의 냄새는 전혀 안 나는 걸 봐서는 보통 쓰는 조합은 아니거나 농도가 비교적 낮은 모양입니다.

침은 모두 2ml 뱉도록 되어있는데 최종적으로는 보존액 2ml과 합쳐서 4ml이 됩니다. 신기하게도 여기서 DNA를 뽑으면 무려 110µg 정도 얻을 수 있다는군요. (침에 DNA가 이렇게 많았다니!!)

미국으로 다시 보내기

23andMe에서 직접 샘플을 처리하는 것이 아니고 LA에 있는 National Genetics Institute라는 의료용 유전자진단 전문 회사가 처리합니다. 그래서 침도 여기로 보내도록 주소가 되어 있습니다. 23andMe에서 미국 내 반송용으로는 선불택배를 지불해 놓았지만, 한국에서는 보낼 수 없으니까 마찬가지로 지인 활용을 할 수도 있고, 직접 보내는 것도 사실 가격 차이는 없습니다. 특히 NGI가 LA공항 바로 옆에 있으니까 훨씬 빨리 가겠죠~ (그냥 상상;;) 우체국 EMS에서 2개를 반송하는데 22000원으로 생각보다 싸게 보낼 수 있었습니다. 통관물품신고는 저는 홍창범님이 하신 대로 Toy, $10으로 신고하고 조마조마 하고 있었는데, 나중에 보니 공항에서 받자마자 바로 통관되는 게 크게 까다로운 것 같지는 않네요. 정식으로 하자면 보존액처리된 침 샘플은 미국 항공안전규정에서 생물학적 규제 면제 품목에 해당돼서 법적으로 큰 문제가 없지만, 많은 배송업체들이 침 샘플을 다룬 경험이 거의 없어서 엉뚱하게 돌려보내는 경우도 있다고 하네요. 눈치껏 잘 하면 됩니다. ^^; 저는 EMS를 한국에서 발송하고 4일 만에 NGI에 도착했습니다.

결과

결과는 받고 나서 6주 후에 나온다고 하는데.. 전 아직 안 받아서 나중에 받으면~~ ^^;

후배를 찾습니다~

몇 군데에 이미 전에 올린 적이 있어서 이미 보신 분들도 있겠지만, 좀 더 많은 홍보를 (;ㅁ;) 위해서 잠잠한 블로그에도 올려봅니다. ^.^;;

제가 공부하고 있는 실험실에 새로 석사과정, 통합과정 또는 박사과정 대학원생으로 참여할 대학원생을 모집합니다.

저희 실험실은 동물 RNA의 유전자발현 조절 분야를 주로 연구합니다. 특히 microRNA의 생합성 경로와 조절 경로의 주요 단백질들의 기작을 밝힌 것으로 많이 알려져 있습니다. (지도교수님 최근 기사와 인터뷰, 조금 오래된 인터뷰 참조)

이번에 찾고 있는 대학원생(1명)은 실험실에서 하는 분야 중 유전체학과 생물정보학 쪽을 주로 하도록 뽑을 예정이고요. 따라서, 학부나 석사 전공으로 컴퓨터과학, 통계학, 생물정보학 등을 전공하신 분을 기대하고 있습니다. 원하는 경우에는 실험도 배워서 스스로 데이터 만들어서 분석하는 요즘 유행하는 스타일로 공부할 수도 있습니다.

특히 유전자 발현 조절 분야는 대규모 전사체/유전체 실험으로 계속 커지는 추세라서 대용량 데이터를 정량적으로 다뤄서 생물 연구를 할 수 있는 기회가 계속 늘고 있습니다.
실험실에서 최근에 많이 하는 high-throughput 실험들을 많이 도입해서 하고 있고, 원하는
실험을 필요하다면 웬만큼은 부담이 되더라도 할 수 있을 정도의 여유는 되기 때문에
연구할 데이터와 기회가 풍부하고요. 실험실의 주요 연구분야가 아직 미지의 영역이 매우 많은
분야라서 머리를 쥐어짜서 연구분야를 찾아 헤멜 필요도 없어서 연구 측면에서는 국내에서
대학원 생활하기에는 아주 좋은 여건이 될 것입니다~

입학전공은 생명과학부, 생물정보학 협동과정, 유전공학 협동과정, 종양생물학 협동과정 중 하나를 선택할 수 있습니다. 2011년 후기 이후 입학 가능하며 입시 이전에 협의가 되어야하고, 연구원으로 미리 일할 수도 있으므로, 되도록이면 빨리 연락을 주시는 편이 좋습니다. (보통 1년 이전에도 많이 연락하는 편입니다.)

실험실에 관한 정보는 홈페이지를 참조하시고, 문의사항이나 지원에 관련된 것은 모두 저한테 자유롭게 보내주세요. 메일 주소는 제 자기소개 페이지 맨 끝에 있습니다. ^.^; 혹시 주변에 관심 있을 것 같은 학생이 있으면 알려주세요~ (나쁜 아저씨 아니에요~;;)

근황

거의 한 해 동안 글을 안 썼습니다. 바쁜 일도 많았지만 안 쓰다 보니 안 들어오고, 안 들어오니 안 쓰고 순환의 연속으로… 크크.
그래도 여지껏 RSS 구독을 남겨두신 분들께 혹시 궁금하시면 요즘 어떻게 살고 있는지
알려드리려고 근황을 남겨둡니다.

셀카질
올림푸스 E-P1 산 기념으로 시험 셀카;

작년 2월 말에 대전에서 졸업하고, 서울로 이사했습니다. 요즘은 낙성대역 근처에 삽니다. 보기보다 꽤 살 만한 동네입니다. 언덕이 많아 눈 쌓이면 매우 곤란한 점만 빼면. 처음 몇 달 간은 아침에 바삐 출근하는 사람들을 보기가 좀 갑갑했죠. 대전에선 하늘이 넓은 곳에서 여유롭게 돌아다녔는데 아무래도 서울은 좀 달라요.

직접(?) 구운 RNA 쿠키~
직접(?) 구운 RNA 초코쿠키

작년 9월부터는 일하고 있던 연구실에서 박사과정을 시작했습니다. 2009년 2학기 시작이니까, 보통 4~5년 정도 한다고 생각하면 2013년이나 2014년 정도까지는 계속 학생입니다. (히히) 새로 옮긴 연구실은 microRNA라는 생분자를 연구하는 곳입니다. 실험실 분위기가 아주 좋아서 매일 출근하는 게 즐겁습니다. 지도교수님은 잘 모르는 분야의 얘기라도 호기심을 가지고 항상 관심있게 들어주시고, 학문적으로도 놀랍도록 식견이 있으시지만 인품도 모두가 내가 연구책임자가 되어도 저렇게 할 수 있을까 싶을 정도로 좋으셔서, 일할 수 있는 가장 좋은 환경에서 행복하게 일하고 있습니다. ^_^

실험실 설정샷
일하는 척 설정샷. ㅋㅋ;

연구실에서 생물정보를 전공한 사람은 혼자라서, 여러 프로젝트에서 나오는 대용량 자료 처리는 대부분 맡아서 하고 있습니다. 그 덕에, 많은 사람들과 많은 연구주제에 동시에 참여할 수 있어서, 다양한 분야를 배울 기회가 됐습니다. 혼자하는 주력 연구로는 새로운 작은 RNA 발견을 위한 유전체학적 분석을 하고 있습니다. 돈도 많이 들고 유독물질과 방사능도 많이 접하게 돼서 원래 하던 일처럼 편하지는 않지만, 이제 실험도 어느 정도는 적응이 돼서 재미있게 하고 있습니다. 이히

내 자리내 실험대
공부하는 자리와 실험대. 사진 찍을 때는 HHKB였지만 지금은 Filco 쓰고 있어요. 파이펫은 길슨.

그리고 토요일마다 실험실원 여러 명에게 프로그래밍을 가르쳐드리고 있습니다. 요즘 생물 데이터가 워낙 대용량화되는 추세라서 뭘 하려면 정보의 흐름을 다루는 능력이 필요해서 다들 흥미를 가지고 참여하고 있습니다. 파이썬으로 하고 있는데, 전에 C++을 잠시 배운 적이 있었던 사람들이 “프로그래밍이 이렇게 재미있는 것인 줄 몰랐어요!”라고 합니다. 역시 파이썬! ㅋㅋ;

연구실 밖 풍경
연구실 자리에서 보이는 바깥 풍경. 쭈욱 오르막이라 모두 가까이 보여서 좋음 +_+

주 5일제 하던 곳에서 주 6일제인 곳으로 옮기다보니, 사실 대전에서 서울에 왔어도 오히려 사람을 만날 수 있는 시간이 더 줄었습니다. 그래서 거의 모임이 있어도 못 가고, 점점 사회에서 떨어져서 산에 사는 사람이 되는 느낌이.. >_<.

저도 작년 예약판매할 때 아이폰을 샀습니다. 원래 아이팟 터치를 늘 친구처럼 데리고 다녀서 소지품 수를 줄이는 효과도 좋지만, 거의 생활을 완전히 바꿔놓은 놀라운 기계네요. 화장실에서 책을 안 읽게 되었다는 슬픈 단점도 있습니다만.. 크… 몇년간 IT 관련해서는 뭘 배운 것이 없었는데, 아이폰 프로그래밍도 조금씩 해 보고 있습니다. 꼭 뭐 만들어 봐야지! 히히

스탠포드 로댕 미술관
스탠포드 로댕 미술관 앞에서 실험실 동생과 동상 따라하기~

올해 초엔 처음으로 미국에 갔습니다. 아 말로만 듣다가 직접 가 보니 음식도 맞고 사람들도 괜찮고 좋네요. 다음에 또 가려면 열심히 연구해서 학회에 뭔가를 꼭 내야겠어요. (동기유발 효과가 불끈불끈) 콜로라도에 있는 키스톤 리조트에서 관련분야 학회에 참가한 뒤 샌 프란시스코에 갔습니다. 샌 프란시스코는 항상 날씨가 좋고 좀처럼 비가 오지 않는다는데, 제가 갔던 기간 중엔 폭풍우가 몰아치더군요.; 스탠포드 로댕 미술관. 그동안 미술관에 여러 번 갔지만 감동을 느끼기는 처음이었습니다. 작가가 그리면서 느꼈던 감정을 그대로 느끼는 듯한 감동! 이히히 다른 미술 전시회도 다시 가야 겠어요.

이제 봄 입니다. 돌아올게요. 종종 또 보아요!

사진 프리젠테이션 배경음악으로 좋은 노래

여러 명이 모이면, 사진 찍는 사람이 한 명은 있기 마련이고, 기억에 오래 남습니다.
행사가 끝날 무렵 사진을 모아서 동영상 비슷한 걸 만들어서 같이 보면, 회상도 되고
뿌듯하기도 해서 뭉클해지는데요. 사진 프리젠테이션이 사실을 왜곡하는 게 아닌가
싶을 정도로 강력해서 웬만한 기억은 모두 순식간에 아름다웠던 기억으로 만들어버리는
막강한 힘이 있습니다.

마침 어제도 어학당 마지막 학기를 기념해서 학기 중에 다른 분들이 찍었던 사진들을
모아서 FotoMagico로 음악을
깔아서 보여드렸는데, 다들 반응이 "아니 이랬었나!" 하면서 센티멘탈해진다고 놀랐습니다. ?

팬 & 줌 효과 (켄 번 효과)도 중요하지만
역시 배경음악도 큰 역할을 하는데, 이런 회고용 사진 프리젠테이션 배경음악을 몇 번
골라보니까 좋은 회고 사진 배경음악의 기준이 몇몇 있는 것 같습니다~

  • 전반적으로 비슷한 느낌으로 진행돼야 하고, 너무 극적인 전개가 있으면 안 좋다.
  • 거북하거나 복잡한 느낌을 남기지 않으려면 코드와 가사가 긍정적인 편이 좋다.
  • 전주가 짧아서, 가사가 시작되기 전의 썰렁함이 적어야 좋다.
  • 간주가 길어서 가사 부분과 이질적인 시간이 생기면 다른 종류의 사진을 중간에 넣어야 해서 귀찮으니, 그냥 간주가 짧은게 좋다.
  • 보컬이 너무 기교있거나 강한 느낌을 주면 사진보다 보컬에 신경이 쓰이기 때문에, 기교 없이 간단하게 부르는 노래가 좋다.
  • 체육활동이나 승부와 관련이 있는 행사라면 좀 발랄한 비트도 괜찮다.
  • 간단한 멜로디 패턴이 많이 반복되는 형식이면, 중간에 잘라 붙여서 길이 조절하기가 쉬워서 좋고, 모르는 노래라도 프리젠테이션 도중에 익숙해져서 친숙한 느낌을 줄 수 있다.

대충 몇 번 해보지는 않았지만 이런 게 중요했던 것 같습니다. 그래서 제가 써 봤던 배경음악을 소개해 드릴게요~

  • 상상 – 송은이: 단순하게 계속 반복돼서 쉽게 친숙해지고 자르기도 정말 쉽고, 목소리도 친근한데다, 아무 행사 사진에 깔아도 아무데나 척척 잘 어울려서 감동도 주고 긍정적인 느낌도 주는 곡~ 원곡은 3분 29초인데, 잘라붙이면 2분 10초, 1분 30초 로도 편집이 가능하고 중간을 반복해서 5분 정도로 늘릴 수도 있습니다. (BioXP 7기 동영상 (글 중간에 있음)에서 사용)
  • 비밀의 화원 – 이상은: 창준형이 소개해 준 매우 좋은 회고용 배경음악. 역시 친숙해지기 쉬운 멜로디, 단순한 전개, 희망적 메시지 등 중요 요건을 모두 갖추고 있습니다. 중간에 분위기가 다른 패턴이 몇 번 나오는데, 여기서 다른 사건 뭉치의 사진으로 전환하면 좋습니다.
  • 많이 안아 주고 싶어요 – 비누도둑: 비밀의 화원은 좀 부담스러운 분위기도 있긴 한데, 이 곡은 시종일관 사랑스러운 분위기라 긍정적인 분위기 사진만 있을 때 일관적으로 좋은 분위기를 만들 수 있습니다. +_+ 역시 반복이 매우 많아서 길이 조절이 쉽고, 비트가 앞의 두 곡에 비해서 좀 빠른 편이라 화면전환에 대충 동기화하기가 쉽습니다.
  • Love – 요조: 마찬가지로 처음부터 끝까지 사랑스러운 분위기이고, 쉽게 친숙해지는 분위기에 아주 따뜻한 느낌을 줍니다. 전주도 기타가 썰렁하지 않고 바로 사진이 나와도 좋은 분위기로 시작해 줍니다. 화기애애한 모임들 배경음악으로 아주 좋습니다.
  • Games People Play – Inner Circle: 지루하다 못해 상투적일 정도의 배경음악이기는 하지만, 그래도 전주도 전혀 없는데다 여행이나 야외 활동 사진으로 아무데나 깔아도 정말 잘 어울립니다. 누구나 다 아는 멜로디에, 영어라 가사가 안 들리는 것도 장점이고요. ?
  • Signal Song – 라이너스의 담요: 담요 노래는 거의 대부분이 강아지를 안고 부르는 분위기이기는 하지만, 이 곡은 특히 아기나 애완동물 사진 프리젠테이션 배경음악으로 잘 어울립니다. +_+ 다만 0:35 근처까지 전주 부분이 효과음만 나오기에 그 앞을 잘라야 프리젠테이션이 안 썰렁합니다.
  • Ready, Get Set, Go! (radio edit) – 페퍼톤스: 사진 부분보다는, 엔딩 크레딧 롤 올릴 때 매우 좋습니다. 엔딩 크레딧 롤(?)은 참가한 사람들 이름이나 간단한 통계, 있었던 사건들 목록 같은 것을 보여주면서 사진과는 또 다른 매우 효과적인 회고용 감동 도구로 쓸 수 있습니다. Radio Edit는 원래 3분 46초인데, 역시 1분 10초, 1분 40초, 2분 20초 정도로 편집할 수 있습니다.

여러분들이 알고 계시는 좋은 사진 프리젠테이션 배경음악은 어떤 게 있나요? 저도 알려주세요~ +_+

동아시아 민족의 유전적 관계

작년에 구글이 투자한 것으로 주목 받았던 23andMe가 올해 타임즈가 2008년 최고의 발명품으로까지 선정할 정도로 대박을 치면서 단일염기다형성(SNP)이라는 말이 더 이상 유전학 전문 용어가 아니라 "돌연변이"처럼 일반 상식에 들어가게 될 무렵… 유럽인들의 유전자 조사를 해 봤더니 지리적 관계와 신기할 정도로 일치하더라!하는 논문이 블로그계에서 한동안 인기를 끌었습니다. 이 논문에서 재미있었던 것은, 단지 유전자 변이 관의 관계만 가지고도 거의 지도를 재현할 수 있을 정도로 지리적 관계가 나왔다는 것도 있었고요. 핀란드가 유전적으로 유럽에서 뚝 떨어져 있다는 사실도 역사를 그대로 재현하듯 나왔다는 것입니다.

세계적으로 유럽 뿐만 아니라 인간 유전적 다형성 연구(HGDP), 세계 주요 인구의 유전적 다형성(HapMap) 등의 대형 프로젝트가 꾸준히 SNP 데이터를 수집해서 가공하고 연구하고 있습니다. 한국인은 앞의 두 프로젝트에서 여러가지 이유로 빠졌는데, 한국에서도 국립보건원생명공학연구원/대학 컨소시움에서 독립적으로 한국인의 단일염기다형성 데이터베이스를 구축해서 올해부터 뭔가를 발표하기 시작했습니다. 한편으로는, 며칠 전에 최초로 한국인 유전체 서열이 공개되어서 떠들썩 했는데요. 이제 한국에서도 외국 논문에서만 봤던 쌔끈한 그래프들을 직접 만들어 볼 수 있는 날이 점점 다가오고 있습니다!

그러던 참에, 어제 온라인 오픈액세스 저널인 PLoS ONE에 동아시아인의 유전적 구조에 관한 논문이 발표됐는데요. 앞에서 소개했던 유럽에서의 조사를 동아시아에서 재현한 것입니다. 한국, 일본, 중국 뿐만 아니라 태국, 필리핀, 베트남, 캄보디아 등등 많은 국가를 상대로 했는데, 실제로 이 연구에서 직접 만든 SNP 데이터는 한국인과 미국에 사는 아시아인 밖에 없고, 나머지는 다 앞에서 언급했던 HGDP와 HapMap에서 가져왔네요. 한국인은 아직 KHapMap이 발표되기 전에 시작했는지 직접 21명 피를 한국에서 뽑아갔다고 하는군요~ (요새 환율로 60만원 정도 하는 걸 공짜로… 아.. 부럽다.. ㅡㅠㅡ)

동아시아의 유전적 관계 doi:10.1371/journal.pone.0003862.g001

왼쪽은 그냥 동아시아 지도가 가물가물하는 사람을 위해 그려놓은 (;;) 것이고, 오른쪽은 유전정보의 관계만을 사용해서 PCA로 두 가지 기준 수치로 2차원으로 표현한 것입니다. 잘 비교해 보면 기가 막히게도 지리적 관계와 유전적 관계가 맞아떨어집니다! 이 조사에서 나타난 결과로는 한국인은 중국 한족과 일본인의 중간 쯤 되는데, 일본과 훨씬 더 가깝게 나타났다고 합니다. 그리고 시베리아 북쪽의 사하공화국에 사는 야쿠트족은 원래 역사에서 중앙 아시아에서 온 민족답게, 대부분 나라에서 동중국이 기원이라고 추측되는 가운데 야쿠트족만 따로 떨어져 나타났습니다.

이런 연구에서 실용적으로(?) 쓰려고 만드는 몇 가지 도출 정보로는 "유전변이 몇 개를 봐야 어느 나라 출신인지 알 수 있나?" 같은 게 있는데요. 사실 진짜 실용적이라기보다는, 23andMe같이 개인 유전체학으로 사업하는 데서 고객들의 흥미를 끌기 위한 서비스로 이것보다 재미있는 게 없죠. 그래서 이 논문에서도 그런 연구를 했는데, 한국인과 일본인을 구분하려면 5000개 정도 SNP를 보면 비교적 정확하게 구분할 수 있었다고 하고요. 논문에서는 미국에 사는 중국인들이 조상알아보기마커 1500개를 활용하면 싸게 자기 유전적 조상을 알아볼 수 있지 않겠냐 하긴 하는데.. 사실 유럽인들하고 달리 아시아 출신들은 자기 조상이 어디서 왔는지는 워낙 잘 알아서 새삼 신기할 것도 없지 않을까요? ;;

그나저나 어서 중국 김가장에 사는 사람들과 경주 김씨 종가 남자들 침을 받아다가 23andMe에 보내서 진짜 경주 김씨가 흉노족 후예인지 알아봐서 미스터리를 풀어주세요!

프로그래밍 과목 조교하기

저희 학교는 등록금이 상당 부분 세금에서 지원되는 대신, 모든 학기에 뭐든 조교를 하도록 돼 있습니다. 학과 사무실 조교 같은 자잘한 일 도와주는 조교부터 시작해서, 슈퍼컴 관리 조교, BK21 서류 관리 조교도 있지만 대부분은 수업을 돕는 학과목 조교를 합니다. 저도 지금까지 쭉 운도 없이 계속 학과목 조교를 해왔습니다. 지난 학기까지는 쭉 과학 기초과목을 해서 별로 특별한 것은 없었는데, 이번 학기에는 전산, 전자과에서 누구나 듣는 기초과목에다가 바이오를 짬뽕한 "바이오데이터구조"라는 과목을 맡았는데요. 과에서 2학년 필수과목이다보니 보통 저희 과 과목은 수강생이 많아도 10명 정도인데, 이 과목은 처음엔 수강생이 60명이 넘었습니다. (물론 이 안에는 학점을 쉽게 따려고 오는 전산과, 전자과 고학년들도 있긴 하죠. 🙂

처음 맡는 프로그래밍 관련 과목이라, 제가 학부 때 느꼈던 "조교가 이런 걸 하면 무척 좋지 않을까!"를 한 번 실행에 옮겨 보기로 했습니다. 사실 중간고사증후군보다 졸업논문증후군이 훨씬 심하죠 –;;

제가 맡은 부분은 학기 프로젝트 관리/채점 부분이라서, 이런 것들을 한 번 생각해 봤는데요.

  • 괜히 코드에 a = 1; 같은 것까지 주석 달아야 점수 잘 나온다는 생각은 안 갖게
  • 코드의 실행 결과가 제대로 안 나오거나 만들다 말았어도, 코드의 세세 부분을 보고 겪었을 만한 세부 경험을 기준으로 채점해서 프로그래밍을 잘 못해도 포기하지 않도록
  • 코딩 결과 자체보다, 코드를 돌려본 결과를 성능/속도/알고리즘 등 여러 측면에서 시험해 보는 과정과 원인 분석 과정, 개선 방안, 도메인 문맥에서의 의미 등을 살펴보게
  • 결과 보고서와 코드가 결국 조교 혼자 보라고 쓰는 것이라고 생각하면 영 지루하니, 어떻게든 피드백을 많이 줘서 누군가 읽긴 읽었구나 하는 느낌을 확실히 받도록
  • 프로젝트 진행 중에 학생들의 질문에는 스펙 설명같은 것 자체에 곁들여서, 진행과 관련된 현실적인 조언이나 관련 학문에서의 정보를 전달하게
  • 질문에 대한 답변이나 채점 결과는 가급적이면 빠를 수록 공부에 효과가 좋으므로 가급적 빠르게

그래서 프로젝트를 시작할 무렵에는 우선 가이드라인을 제시했습니다. 원래 내용은 꽤 길지만 요약하면

  • 주석 너무 많이 달지 마라, 주석이 적어도 잘 이해되는 코드가 좋은 거다.
  • 사소한 문제 때문에 진행하기가 힘든 상황이면, 그런 것들은 보고서와 코드에 표시하고 우선 상황을 대충 억지로 넘긴 다음에 시간이 날 때 다시 봐라.
  • 보고서에는 이런이런~~ 것들에 대한 토론이 있으면 좋다. (예시 10가지)

이렇게 시작하고 나중에 오는 질문에는 가급적이면 질문 자체에 대한 직접적인 답보다는, 왜 그렇게 되는지, 실제 프로젝트의 상황에서 어떤 경우가 있어서 그런 결정을 해야하는지 같은 것들을 가급적이면 같이 썼는데, 사실 처음 배우는 프로그래밍 과목에서 하기로는 좀 어려운 프로젝트다보니 제대로 전달이 잘 안 된 것 같아서 좀 아쉽기는 했습니다.

드디어 제출이 다가왔을 때는, 직접 내면 좀 번거로우니까 전자메일로 받기로 했는데요. 아무래도 전자메일에 큰 첨부파일을 보내다보면 사고도 많이 생기고 해서, 별도의 2가지 경로로 보낼 수 있게 메일 주소를 따로 2개를 마련해서 둘 다 보내도록 했습니다. 그리고, 그래도 혹시 또 메일은 알 수 없으니, 과목 홈페이지 게시판에 MD5 체크섬을 올리면 MD5 체크섬이 맞으면 나중에 제출해도 게시판에 올린 시간으로 인정하기로 했습니다. 그런데 정말로 한 학생이 5일 뒤에 메일이 안 갔냐고 자기 성적이 안 올라왔다고 그러는데, 메일이 유실됐는지 전혀 로그에서도 찾을 수 없는 일이 발생했습니다. 마침 게시판에 MD5 체크섬이 올라와 있어서 구원해 줄 수 있었죠.

결국 약간 늦은 학생도 있었지만 대부분 제출이 끝나고 채점을 했는데요. 역시 채점은 하다보면, 점수로만 표현하기는 좀 아쉬운 뭔가 그런 것이 있습니다. 그래서, 아예 성적표 사이트를 하나 만들어서, 각각의 개인의 제출물에 대한 피드백과 학생들의 부분별 상대적 위치를 알 수 있는 도표를 볼 수 있도록 했습니다. (실제 인물이 아니라 이 글에서 인용하려고 가상의 학번을 만들었습니다.)

피드백은 직접 일일이 쓰기는 좀 많아서, 세부항목별로 따로 Z-score를 계산해서 낮은 순서로 몇 개, 높은 순서로 몇 개를 추려서 "좀 더 열심히", "참 잘했어요" 아래에 코멘트를 자동으로 쓰게 했습니다. 뭐 그런대로 괜찮게 나오더군요. 🙂 하나 재미있는 것은, 웹서버 로그를 보니까, 자기 성적만 보고 가는 학생은 30% 정도 밖에 안 되고, 나머지는 친구 학번을 다 넣어보고 가더군요.;; (친구 관계 네트워크라도 그릴 수 있을 정도!)

이제 프로젝트가 끝나긴 했는데, 제가 맡은 부분이 기말고사가 또 남아있어서.. ㅡㅡ; 또 하려니 막막하네요 -ㅇ-; 그래도, 학생들이 그냥 조교라서 하는 아부도 많이 섞여있겠지만 제출하는 메일이나 게시판 댓글에서 도움이 많이 됐다, 좋은 경험을 할 수 있었다, 많은 것을 배울 수 있었다. 라고 해 줘서 무척 힘이 났습니다. 이제 졸업 준비를 해야하는데.. -ㅇ-;;

요즘 생각난 경험 나누기 행사 세 가지

서울에 있을 때는 이런 저런 행사를 많이 했는데 요즘 대전에 있다보니
영 근질근질해서, 가끔 이런 행사 하면 정말 재미있겠다 상상하며 졸곤(;;) 합니다.
어디 적어두는 습관이 없다보니 생각을 아무리 해 봐야 늘 남는 게 없는데요. 흐흐;;
그래서 최근에 생각났던 걸 함께 생각해 보기도 하고 스스로 안 까먹으려고
적어 놓아 봅니다.

개발자 구보씨의 3일

제가 가장 좋아하는 TV 프로그램은 단연 KBS1 다큐멘터리 3일 입니다.
이 프로그램에선 어떤 장소나 사건을 주제로 3일 동안 같은 곳을 지키며 오가는 사람을 취재합니다.
강남역, 구로역 같은 사람 많이 다니는 지하철 역이 되기도 하고, 강남고속터미널이나 통도사, 동해의 어촌, 추석 특송 기간 동안의 택배 직원들 등등
생활을 밀접하게 다루다보니, 역에서 지나가는 사람들을 보며 저 사람들이
어디서 어디로 가고 어떤 일을 하고 어떤 생각을 하고 가족들과는 어떻게 지내고
어떤 게 행복한지 등이 늘 궁금해 했던 것을 조금이나마 엿볼 수 있게 해 줍니다.
특히 같은 자리에 3일을 쭉 있다보니, 면접보러 서울에 왔다가 다시 며칠 있다가 내려가는 사람, 자전거 여행하러 갔다가 2박 3일 여행하고 돌아오는 사람들의 전과 후를 모두 볼 수 있다보니 정말 재미있습니다. 한 편을 보면 마치 100명하고 술 마시면서 인생 사는 얘기를 하고 온 것 같은 기분이죠.

그래서 개발자도 이런 것들을 할 수 있지 않을까 생각해 봤는데요. 개발자라고 묶으면 왠지 뻔히 하루 종일 컴퓨터 보고 키보드만 칠 것 같지만, 알고보면 회의도 하고, 아이디어 만들기도 하고, 제안서도 쓰고, 싸우기도 하고, 몰래 만화도 보고, 여자친구와 메신저도 하고… 하는 일이나 회사 환경, 개인적인 환경에 따라 적지 않은 차이가 있습니다. 그냥 보면 다 똑같은 개발자의 실제 일하는 환경을 엿보면 고년차 개발자들끼리, 또는 갓 IT업계에 들어온 신입, 대학생, 고등학생 등등.. 추상적인 "이 쪽 전망이 어떻더라…" 보다 도움이 될 것 같아서요.

72시간 VJ들이 쫓아다니는 건 현실적으로 어려우니, 대충 타협해서 72시간 중에 종종 자기 모습이나 하는 일, 주변 환경을 사진으로 찍어서, 그 중 24장을 꼽아서 자기 생활에 대해 페차쿠차 형식으로 발표하는 것입니다! +_+ 자기 자리 자랑도 있을 것이고.. 몰래 회의 장면 같은 데서 이상한 동료 욕도 할 수 있고.. 어려웠던 문제 해결하는 과정을 무용담처럼 얘기할 수도 있고… 단편적인 생활 스케줄을 쫙 훑기보다는, 살아있는 진짜 3일처럼 당시의 생생한 연결된 이야기를 들으면 더욱 좋겠죠!

서울에 사는 세계 개발자 페차쿠차

한국 IT게에서 비전통적 컨퍼런스를 상당히 일찍 시도했던 "KLDP CodeFest"에서는 초기에 계속 꾸준히 서울 인근에 사는 외국인 개발자들이 몇 명씩 참여했습니다. 지난 번 파이썬 페차쿠차는 진행 언어가 한국어였지만 한국어를 잘 하는 프랑스인 개발자가 한 분 참여해서 자리를 빛내주었습니다. 그 때 생각이 떠올랐는데요. 서울에 사는 외국인 개발자들과 또한 그들과 교류하고 싶은 한국인 개발자들이 소통하는 계기가 있으면 좋겠네요.

그래서 역시 지난 번 파이썬 페차쿠차와 마찬가지로 자기가 하는 일이나 한국에서 일하는 개발자로의 경험, 어려움, 팁 같은 것을 페차쿠차로 발표하는 자리가 있으면 촉진할 수 있는 좋은 기회가 되지 않을까 합니다. 아무래도 한국어에 서투른 개발자들도 많이 참가할 수 있도록 공식 언어도 영어로 지정해서 행사장에 누가 있어도 서로 말을 거는 데 주저하지 않을 수 있도록 하면 더욱 좋을 것 같습니다. (한국어를 너무 사랑하는 분들은 이 부분에서 거부감이 있을 수도 있겠지만, 현실적으로 취지를 살려서 한국어를 못하는 개발자를 배제하지 않으려면 이 방법이 최선인 듯 하네요.)

장난감 문제 축제

예전에 언젠가 한 번 제 블로그에 올린 적 있는 생각이기도 한데요.
앞의 "구보씨"와 마찬가지로 다양한 분야에서 일하는 개발자들이 모여서
경험을 나누고 이해를 넓히는 방법으로 장난감 문제를 쓰는 방법을
생각해 봤습니다. 우선 자기 개발 분야에서 아주 간단해서 잘 모르는 사람도
10분 안에 풀 수 있는 장난감 문제를 1개 준비해 옵니다. 예를 들어 게임
프로그래머라면 2D 좌표계에서 충돌 검사를 하는 문제를 가져온다거나,
자판기에 들어가는 펌웨어를 만드는 프로그래머라면 자판기에서 돈 넣으면
잔돈 계산하는 문제, 이미지 처리를 주로 하는 프로그래머라면 간단한 껍데기를 채워넣어서 간단한 알고리즘으로 그림파일 외곽선을 보여주는 문제를 가져오는 등 최대한 자기 분야 특성은 살리지만 장난감 문제인 것을 가져
오면 되겠죠.

그래서 이 문제를 이제 잘 모르는 사람들끼리 무작위로 2명 씩 짝을 만들어서
공략합니다! 이제 그 뒤 부터는 예전에 코드레이스같은 곳에서 했던 형식이나 재미있게 할 수 있는 형식을 여러 가지 만들 수 있을 것 같네요. 채점은 아마도 각 문제를 출제한 사람이 뽑게? ^^;

그냥 최근 떠올랐던 생각 세 가지를 적어 봤는데요. 좀 다듬어서 해 볼 만한 것도 있을 것 같네요. 언제 기회가 되면 한 번 추진을!

Grand Mint Festival 2008 다녀왔습니다!

10월 18일-19일 올림픽공원에서 한 거대 박하 축제 2008에 다녀 왔습니다. +_+_+_+_+

사실 GMF나 주최측인 mint paper도 전혀 모르고 있다가 Mocca가 한국에서 공연한다는 얘기를 듣고 GMF에 관심을 가지게 되었는데요. 그 뒤로 GMF에 출연하는 밴드들 노래를 듣다가 홀라당 빠져버려서 한 동안 전의를 불사르며 지내다 드디어 다녀왔습니다!!!! 아고 다리야!! 크크크;

공연 참가

전체 공연 팀은 50팀이 넘었지만, 병렬로 진행되고 이틀만 가다 보니, 몇몇 곳만 가게 됐는데, 저는 위 사진에 있는 11개 팀을 열심히 봤습니다. (윗 줄은 토요일, 아랫 줄은 일요일) 대부분 예습하면서 처음 들은 팀들이었지만, 거의 1달을 쥬크온 플레이리스트에 올려놓고 반복학습하고, 민트라디오를 듣다보니 마치 다들 고등학교 때 부터 좋아했던 것 같이 느껴지네요. ^^;;

전반적으로 일상에서는 팬을 찾기도 쉽지 않은 밴드들이, 축제장 안에서는 마치 아이돌처럼 사람들이 좋아하니 무척 흐뭇(;;)했고요. 대전에서 오랫동안 무료한 생활을 하다가 큰 축제를 가니 사람보는 재미에 푹 빠져서~ 거의 대전에서 한 200년 봐야 볼 수 있는 다양한 사람을 다 본 것 같네요. (GMF의 여성관객 비율은 국내 음악축제 중 거의 최고수준!)

일요일 라이너스의 담요 공연 준비 중

▲ "라이너스의 담요" 공연 준비 중 (공연 중은 촬영이 금지;;)

특히 일요일 "라이너스의 담요", "Mocca" 공연은 장소도 호수를 배경으로 해서 대형 분수도 종종 뿜어주고 해서, 푹 빠져서 헤벌레 해서 정신을 놓고 보았습니다. 세상에나 세상에나!

다른 기대공연이었던 "페퍼톤스", ""은 짧게들 끝나 아쉬워서 다음에 꼭 다른 데서 또 만나겠어요! 뎁♡♡

이한철 M.net Take 1

▲ GMF M.net Take1 이한철 촬영 중 (동네 아저씨같은 인상에 주목!) ©허지연

제가 원래 후기같은 것 쓰는데 많이 서투르니, 이만 줄이고 기억나는 말 소개.. (정확히 받아적은 것은 아님)

  • 페퍼톤스 이장원: (악을 쓰며) 안녕하세요! 락!발라드 밴드 페퍼톤스입니다!!
  • 박새별: (유희열이 라디오에서 안테나뮤직에서 가수는 박새별 밖에 없다고 한 것을 언급하며) 제가 삑사리내면 역시 안테나뮤직 소속 맞구나 하고 박수 꼭 쳐 주세요~
  • : 여러분 이거 닌텐도 위 게임기에 있는 걸로 만든거 랍니다. (악기에 위모트를 붙여서 공연 중에 뒷 화면에 그림을 그림)
  • : 스티브 잡스 아저씨 고맙습니다. (VJ에게 비디오아트 기술 발전에 중요한 사람이라는 얘기를 들었다며)
  • 요조: 여러분 제가 음란가수인가요?
  • 요조: (모든 곡이 끝나고 나서, 객석을 향한 마이크의 음량이 화면에 바 그래프로 연결돼 있고 그 위에 "앵콜지수"라고 표시 됨) 이렇게 뜨겁게 앵콜해 주셔서 고맙습니다~
  • 라즈베리필드 소이: 이 곡은 149번 버스에서 만들어서 제목이 149예요.
  • 라이너스의 담요 연진: 나를 있는 그대로 사랑해 줄 사람 완전 대모집합니다. (남자 몇 명이 저요 하며 손 들자) 나를 있는 그대로 사랑해 줄 순 없을 껄!?
  • 라이너스의 담요 연진: 있다가 제가 전자양 세션으로 가는데, 언니네 이발관한테 캐발릴까봐 다들 완전 걱정 중이예요. (전자양과 언니네 이발관이 같은 시간)
  • 마이 앤트 메리: (마지막 곡을 앞두고) 여러분 있다가 앵콜 하실 건가요? (네~) 네 그러면 마지막 곡이랑 이어서 가겠습니다.
  • 전자양: (잠시 곡이 끝나고 조용한 사이, 옆 공연장 소리가 크게 들리자) 중간 중간에 썰렁할 때 BG깔아 주시고 역시 좋네요! 선배님 감사합니다! (약간 씁쓸하지만 긍정적인 어조로)
  • 전자양: (다시 다음 조용한 사이, 언니네 이발관 노래가 들리자) 언니네 이발관 새 앨범 좋더라구요. 사실 저도 공연 보고 싶었는데, 공연을 해야하네.. (밝은 어조로)