Biopython 입문 — FASTA·FASTQ 파싱부터 서열 통계까지 (Python for Bio)

TL;DR — 서열(sequence) 데이터를 Python으로 다루는 표준 라이브러리가 Biopython입니다. Bio.SeqIO로 FASTA·FASTQ 파일을 읽고 쓰며, Seq 객체 하나로 역상보(reverse-complement)·전사(transcription)·번역(translation)·GC 함량(GC content)을 한 줄씩 계산합니다. 이 글은 설치부터 시작해 NCBI에서 받은 실제 유전자 서열로 길이·GC·ORF 통계를 내고, FASTQ 품질점수(Phred quality score)에 접근하는 데까지 갑니다.

흐름은 pip install biopythonSeqIO.read/SeqIO.parse로 파일 읽기 → record.seqSeq 객체 꺼내기 → len·gc_fraction·translate로 통계 → letter_annotations["phred_quality"]로 FASTQ 품질 접근입니다. 마지막에 결과를 pandas 데이터프레임으로 넘기는 한 줄까지 붙입니다.

이 글은 "Python for Bio" 시리즈의 출발점입니다. 개념으로 알던 FASTA·FASTQ를 실제 코드로 처음 만져 보는 다리 역할을 합니다.

🔗 관련 글  ·  개념 먼저 잡으려면 FASTQ 파일이란?(4줄 구조·Phred 점수)  ·  시퀀싱 원리는 NGS란? 1·2·3세대 비교  ·  번역(translation)의 생물학은 중심원리(Central Dogma)

이 글에서 만들 것

NGS 시퀀서가 뱉어내는 파일은 거의 다 FASTA 아니면 FASTQ입니다. 둘 다 그냥 텍스트 파일이라 메모장으로도 열리지만, 수만 개의 서열을 손으로 셀 수는 없습니다. 여기서 Biopython (Python용 생물정보학 라이브러리)이 등장합니다. 2000년부터 이어진 오픈소스 프로젝트로, 서열 입출력·정렬·번역·데이터베이스 접근 같은 일상 작업을 표준화된 객체 몇 개로 묶어 둔 도구입니다.

이 글에서는 다음을 직접 해 봅니다. 우선 Biopython을 설치하고, NCBI에서 실제 유전자 서열을 FASTA로 내려받습니다. 그 서열의 길이와 GC 함량을 재고, 역상보·전사·번역을 한 줄씩 계산합니다. 이어서 여섯 개의 번역틀(reading frame)을 훑어 ORF (open reading frame, 열린 해독틀) 후보를 세고, FASTQ 파일에서는 각 염기의 Phred 품질점수를 꺼내 평균을 냅니다. 마지막으로 이 결과들을 pandas 데이터프레임 한 줄로 옮겨, 그래프나 통계로 넘어갈 준비를 합니다.

# 핵심 흐름 한눈에 (Biopython)
from Bio import SeqIO
from Bio.SeqUtils import gc_fraction

record = SeqIO.read("gene.fasta", "fasta")   # ① FASTA 한 서열 읽기
seq = record.seq                              # ② Seq 객체 꺼내기
print(len(seq))                               # ③ 서열 길이
print(gc_fraction(seq) * 100)                 # ④ GC 함량 (%)
print(seq.reverse_complement())               # ⑤ 역상보 가닥
print(seq.translate(to_stop=True))            # ⑥ 단백질로 번역

1. 사전 준비 — 설치와 샘플 데이터

Biopython은 pip이나 conda 어느 쪽으로도 설치됩니다. 2025년 10월에 나온 1.86 버전 기준으로 Python 3.10~3.14를 지원합니다. 의존성은 수치 계산용 NumPy 하나뿐이라 설치가 가볍습니다.

# pip 사용 (가상환경 권장)
pip install biopython

# 또는 conda (conda-forge 채널)
conda install -c conda-forge biopython

설치가 끝났으면 버전을 확인합니다. 1.78 이전과 이후는 API가 꽤 달라졌으니, 옛날 블로그 코드를 참고할 때는 버전부터 맞춰 보는 게 좋습니다.

import Bio
print(Bio.__version__)
1.86

샘플 데이터는 NCBI (미국 국립생물정보센터)에서 실제 서열을 받아 씁니다. Biopython의 Bio.Entrez 모듈이 NCBI에 직접 질의를 보냅니다. 여기서는 대장균(Escherichia coli)의 lacZ 유전자(β-갈락토시다아제를 만드는 유전자, GenBank 등록번호 J01636.1)를 FASTA로 내려받습니다. NCBI 정책상 Entrez.email에 본인 이메일을 적어야 하며, 이는 과도한 요청을 막기 위한 식별용입니다.

from Bio import Entrez, SeqIO

Entrez.email = "your_email@example.com"   # 본인 이메일 (NCBI 요구사항)

# efetch: nucleotide DB에서 J01636.1 을 FASTA 형식으로 받기
with Entrez.efetch(db="nucleotide", id="J01636.1",
                   rettype="fasta", retmode="text") as handle:
    record = SeqIO.read(handle, "fasta")   # 서열 1개 → read()

# 받은 서열을 로컬 파일로 저장 (다음 단계부터 재사용)
SeqIO.write(record, "lacz.fasta", "fasta")
print(record.id, "·", len(record.seq), "bp")
J01636.1 · 7477 bp

인터넷 연결이 막혀 있거나 NCBI 접속이 어려우면, 아래처럼 짧은 더미 서열을 직접 만들어 똑같이 따라올 수 있습니다. 형식만 FASTA면 되므로 분석 코드는 그대로 동작합니다.

from Bio.Seq import Seq
from Bio.SeqRecord import SeqRecord

dummy = SeqRecord(
    Seq("ATGACCATGATTACGGATTCACTGGCCGTCGTTTTACAACGTCGTGACTGGGAAAACCCTGGCGTTACCCAACTTAATCGCCTTGCAGCACATCCCCCTTTCGCCAGCTGGCGTAATAGCGAAGAGGCCCGCACCGATCGCCCTTCCCAACAGTTGCGCAGCCTGAATGGCGAATGGTAA"),
    id="demo_gene", description="dummy CDS for tutorial")
SeqIO.write(dummy, "lacz.fasta", "fasta")   # 위와 같은 파일명으로 저장
💡 FASTA와 FASTQ의 차이 — FASTA는 >식별자 한 줄 뒤에 서열만 옵니다. FASTQ는 여기에 품질점수가 더해져 read 하나가 네 줄(식별자·서열·구분자·품질)이 됩니다. 참조 유전체나 유전자 서열은 보통 FASTA, 시퀀서에서 갓 나온 raw read는 FASTQ라고 기억하면 편합니다. 개념이 가물가물하면 FASTQ 파일이란? 글을 먼저 보고 오세요.
그림 1. Biopython 데이터 흐름 — FASTA·FASTQ 파일을 SeqIO로 읽어 Seq 객체로 만들고, 서열 통계를 내어 pandas로 넘기기까지.

2. SeqRecord 구조 — 읽어 온 서열의 속살

SeqIO.read(서열 1개)나 SeqIO.parse(여러 개)가 돌려주는 건 단순한 문자열이 아니라 SeqRecord 객체입니다. 서열 본체뿐 아니라 식별자·설명·주석까지 한 묶음으로 들고 있습니다. 어떤 정보가 들었는지 먼저 들여다봅니다.

from Bio import SeqIO

record = SeqIO.read("lacz.fasta", "fasta")   # 저장해 둔 파일 다시 읽기

print("id        :", record.id)          # 서열 식별자 (>뒤 첫 단어)
print("desc      :", record.description)  # 설명 (>뒤 전체)
print("길이      :", len(record.seq), "bp")
print("앞 30 bp  :", record.seq[:30])     # Seq 객체도 슬라이싱 가능
print("타입      :", type(record.seq))
id        : J01636.1
desc      : J01636.1 E.coli lactose operon with lacI, lacZ, lacY and lacA genes
길이      : 7477 bp
앞 30 bp  : GGGGCTGGCTTAACTATGCGGCATCAGAGC
타입      : <class 'Bio.Seq.Seq'>

핵심은 record.seq로 꺼낸 Seq 객체입니다. 겉보기엔 문자열 같지만, 생물학적 의미가 있는 메서드(역상보·전사·번역)를 품고 있는 특별한 자료형입니다. 슬라이싱(record.seq[:30])이나 len은 문자열처럼 그대로 쓰면서, 추가로 서열 연산까지 되는 셈입니다. SeqRecord는 이 Seq를 알맹이로 두고 식별자·설명을 껍질처럼 두른 구조라고 보면 됩니다.

3. Seq 객체 — 역상보·전사·번역

이제 Seq 객체의 진짜 쓸모를 봅니다. DNA 한 가닥만 알아도 상보 가닥, mRNA, 단백질 서열을 모두 끌어낼 수 있습니다. 중심원리(Central Dogma)의 DNA → RNA → 단백질 흐름이 그대로 메서드로 들어와 있습니다.

from Bio.Seq import Seq

# 짧은 코딩 서열로 시연 (ATG로 시작하는 가상의 CDS)
cds = Seq("ATGGCCATTGTAATGGGCCGCTGAAAGGGTGCCCGATAG")

print("원본       :", cds)
print("상보 가닥   :", cds.complement())          # A↔T, G↔C 만 교체
print("역상보      :", cds.reverse_complement())  # 상보 + 뒤집기 (실제 반대편 가닥)
print("전사 mRNA   :", cds.transcribe())          # T → U (DNA → RNA)
print("번역 단백질 :", cds.translate())           # 코돈 3개 → 아미노산 1개
원본       : ATGGCCATTGTAATGGGCCGCTGAAAGGGTGCCCGATAG
상보 가닥   : TACCGGTAACATTACCCGGCGACTTTCCCACGGGCTATC
역상보      : CTATCGGGCACCCTTTCAGCGGCCCATTACAATGGCCAT
전사 mRNA   : AUGGCCAUUGUAAUGGGCCGCUGAAAGGGUGCCCGAUAG
번역 단백질 : MAIVMGR*KGAR*

번역 결과 끝의 *는 종결코돈(stop codon)을 뜻합니다. 위 서열에는 중간(TGA)과 끝(TAG)에 종결코돈이 두 개 보이는데, 첫 종결코돈까지만 번역하고 멈추려면 to_stop=True를 줍니다. 또 세균(bacteria)이나 미토콘드리아는 표준 코돈표와 조금 다르므로, table 인자로 NCBI 유전암호 번호를 지정합니다(세균=11).

# 첫 종결코돈에서 멈추기
print(cds.translate(to_stop=True))            # MAIVMGR

# 세균 유전암호(표 11)로 번역
print(cds.translate(table=11, to_stop=True))  # MAIVMGR

# 길이가 3의 배수가 아니면 번역 시 경고가 뜸 → 미리 점검
read = Seq("ATGGCCATTGTAATGG")   # 16 bp (3의 배수 아님)
print(len(read) % 3)                          # 1 → 끝 1 bp가 남음
print(read[:len(read) - len(read) % 3].translate())  # 3의 배수로 잘라 번역
MAIVMGR
MAIVMGR
1
MAIVM
📌 Seq는 더 이상 알파벳을 안 받는다 — 예전(1.78 이전) Biopython에서는 Seq("ATGC", IUPAC.unambiguous_dna)처럼 알파벳을 함께 넘겼습니다. 지금은 Bio.Alphabet이 통째로 사라져 Seq("ATGC")만 쓰면 됩니다. 대신 번역처럼 분자 종류가 중요한 자리에서는 table 같은 인자로 직접 알려 줍니다. 옛 코드에서 from Bio.Alphabet import ...가 보이면 그 줄을 지우는 게 1순위입니다.
그림 2. Seq 객체의 네 가지 변환 — 상보·역상보·전사·번역이 같은 DNA 가닥에서 만들어 내는 결과.

4. 서열 통계 — 길이·GC 함량·ORF

여러 서열을 한꺼번에 다룰 때는 SeqIO.parse로 반복문을 돕니다. 받아 온 lacZ 유전자(7,477 bp)로 기본 통계를 내 봅니다. GC 함량은 Bio.SeqUtils.gc_fraction이 0~1 사이 실수로 돌려주므로 100을 곱해 퍼센트로 바꿉니다.

from Bio import SeqIO
from Bio.SeqUtils import gc_fraction

record = SeqIO.read("lacz.fasta", "fasta")
seq = record.seq

length = len(seq)                      # 서열 길이 (bp)
gc = gc_fraction(seq) * 100            # GC 함량 (%) — 0~1 → 0~100
a = seq.count("A"); t = seq.count("T")
g = seq.count("G"); c = seq.count("C")

print(f"길이      : {length:,} bp")
print(f"GC 함량   : {gc:.1f} %")
print(f"A/T/G/C   : {a} / {t} / {g} / {c}")
길이      : 7,477 bp
GC 함량   : 53.7 %
A/T/G/C   : 1698 / 1771 / 2055 / 1953
⚠️ GC() 함수는 사라졌습니다 — 인터넷의 옛 예제는 from Bio.SeqUtils import GC를 쓰지만, 이 함수는 최신 버전에서 제거됐습니다(ImportError). 지금은 gc_fraction을 쓰되, 옛 GC()가 퍼센트(0~100)를 돌려준 것과 달리 gc_fraction은 비율(0~1)을 돌려준다는 점이 다릅니다. 그래서 위 코드처럼 * 100을 붙입니다.

다음은 ORF (open reading frame) 후보를 세어 봅니다. ORF는 개시코돈(ATG)에서 시작해 종결코돈(TAA·TAG·TGA)으로 끝나는 구간으로, 단백질을 코딩할 가능성이 있는 영역입니다. DNA는 양쪽 가닥에 각각 세 가지 번역틀이 있어 모두 여섯 개의 틀(6-frame)을 봐야 합니다. 여기서는 각 틀을 번역해 종결코돈으로 끊고, 길이가 일정 이상인 단백질 조각만 ORF 후보로 셉니다.

from Bio import SeqIO

record = SeqIO.read("lacz.fasta", "fasta")
seq = record.seq
min_aa = 100   # 아미노산 100개(=300 bp) 이상만 의미 있는 ORF 후보로

orf_count = 0
# 정방향(+)과 역방향(-) 두 가닥
for strand, nuc in [(+1, seq), (-1, seq.reverse_complement())]:
    # 한 가닥당 번역틀 3개 (시작 위치 0, 1, 2)
    for frame in range(3):
        # 길이를 3의 배수로 자른 뒤 번역
        trimmed = nuc[frame:]
        trimmed = trimmed[:len(trimmed) - len(trimmed) % 3]
        # 종결코돈(*)으로 쪼개 각 조각을 후보로
        for protein in trimmed.translate().split("*"):
            if len(protein) >= min_aa:
                orf_count += 1

print(f"6개 번역틀에서 {min_aa} aa 이상 ORF 후보: {orf_count}개")
6개 번역틀에서 100 aa 이상 ORF 후보: 7개

lacZ 영역에는 실제로 여러 유전자(lacI·lacZ·lacY·lacA)가 들어 있어, 100 아미노산 이상 ORF 후보가 일곱 개 잡힙니다. 이 방식은 개시코돈을 엄밀히 따지지 않은 단순 카운트라 진짜 유전자 수와는 다를 수 있습니다. 정밀한 유전자 예측에는 Prodigal 같은 전용 도구를 쓰지만, "이 서열에 코딩 영역이 얼마나 있을 법한가"를 빠르게 가늠하는 데는 이 정도로 충분합니다.

5. FASTQ 품질점수 — Phred score 접근

FASTQ는 read마다 염기별 품질점수를 함께 담습니다. 이 점수가 Phred 점수(Q)인데, 염기 판정이 틀렸을 확률 PQ = −10 × log₁₀ P 의 관계로 정의됩니다. Q = 20이면 오류율 1%, Q = 30이면 0.1%입니다. 파일에는 점수가 ASCII 문자로 인코딩돼 있지만, Biopython이 자동으로 풀어 letter_annotations["phred_quality"]에 숫자 리스트로 넣어 줍니다.

먼저 시연용 FASTQ를 직접 한 건 만들어 보겠습니다(실전에서는 시퀀서나 SRA에서 받은 파일을 그대로 엽니다).

# 시연용 FASTQ 한 건 작성 — 식별자/서열/+/품질 4줄
fastq_text = """@read1
GATTTGGGGTTCAAAGCAGTATCGATCAAATAGTAAATCC
+
!''*((((***+))%%%++)(%%%%).1***-+*''))**55
"""
with open("demo.fastq", "w") as f:
    f.write(fastq_text)

이제 SeqIO.parse로 읽으면, 각 read의 서열과 품질점수가 함께 딸려 옵니다.

from Bio import SeqIO

for record in SeqIO.parse("demo.fastq", "fastq"):
    quals = record.letter_annotations["phred_quality"]   # 정수 리스트
    avg = sum(quals) / len(quals)
    print("read id   :", record.id)
    print("서열      :", record.seq)
    print("품질 앞 8 :", quals[:8])
    print(f"평균 품질 : Q{avg:.1f}")
read id   : read1
서열      : GATTTGGGGTTCAAAGCAGTATCGATCAAATAGTAAATCC
품질 앞 8 : [0, 6, 6, 9, 7, 7, 7, 7]
평균 품질 : Q21.6

품질점수가 ASCII 문자(!''*(...)에서 숫자 리스트로 자동 변환된 게 핵심입니다. !Q = 0, 'Q = 6에 해당합니다(Sanger/Illumina는 ASCII 값에서 33을 빼는 Phred+33 방식). 이 숫자만 있으면 read별 평균 품질, 위치별 품질 곡선, 품질 필터링 같은 QC 작업을 전부 직접 짤 수 있습니다. 대용량 파일이라면 보통 FastQC 같은 도구가 이 계산을 대신해 주지만, 원리는 정확히 이것입니다.

💡 gzip으로 압축된 FASTQ — 실전 FASTQ는 거의 .fastq.gz로 압축돼 옵니다. Biopython은 압축을 직접 풀지 못하므로, 파이썬 표준 라이브러리 gzip으로 텍스트 모드 핸들을 열어 SeqIO.parse에 넘깁니다. import gzipwith gzip.open("reads.fastq.gz", "rt") as handle: 처럼 "rt"(read text) 모드로 열면 압축을 푼 채로 한 줄씩 읽힙니다. 압축을 미리 풀어 디스크를 낭비할 필요가 없습니다.
그림 3. Biopython 입문 워크플로 — 설치부터 서열 통계·FASTQ 품질·pandas 연결까지 한눈에.

6. 자주 발생하는 에러 & 해결 (FAQ)

  • ImportError: cannot import name 'GC'GC()는 옛 함수라 최신 버전에서 빠졌습니다. from Bio.SeqUtils import gc_fraction으로 바꾸고, 퍼센트가 필요하면 결과에 * 100을 곱하세요(gc_fraction은 0~1 비율 반환).
  • ModuleNotFoundError: No module named 'Bio.Alphabet' — 1.78 버전부터 Bio.Alphabet이 제거됐습니다. 옛 코드의 from Bio.Alphabet import ... 줄과 Seq(..., IUPAC.xxx)의 알파벳 인자를 모두 지우세요. 지금은 Seq("ATGC")만으로 충분합니다.
  • ValueError: Sequence length is not a multiple of threetranslate()에 길이가 3의 배수가 아닌 서열을 주면 경고가 납니다. 코딩 구간만 잘라 seq[start:end]로 3의 배수를 맞추거나, ORF 예제처럼 len % 3만큼 끝을 잘라내세요.
  • SeqIO.read()가 에러를 냄read는 파일에 서열이 정확히 한 개일 때만 씁니다. 여러 서열이 든 멀티-FASTA에서는 parse로 반복문을 돌려야 합니다. 반대로 parse 결과를 len()으로 세려 하면 제너레이터라 안 되니, list(SeqIO.parse(...))로 감싸세요.
  • 한글·특수문자로 파일 읽기 실패(UnicodeDecodeError) — FASTA·FASTQ는 ASCII가 원칙이지만, 설명 줄에 깨진 문자가 섞이면 읽기가 멈춥니다. open(path, encoding="utf-8", errors="replace")로 핸들을 열어 SeqIO.parse에 넘기면 우회됩니다. 윈도우에서 만든 파일이 \r\n 줄바꿈을 써도 Biopython은 알아서 처리합니다.
  • NCBI efetch가 비거나 멈춤Entrez.email을 안 적으면 차단될 수 있습니다. 또 짧은 시간에 너무 많이 요청하면 NCBI가 막으므로, 대량 다운로드는 Entrez.api_key를 발급받아 쓰거나 초당 3회 이하로 제한하세요.

7. 확장 — pandas로 넘기기

서열별 통계를 냈다면 다음은 표로 정리해 비교·시각화하는 단계입니다. SeqIO.parse로 여러 서열을 돌면서 딕셔너리 리스트를 만들면, pandas가 그걸 데이터프레임으로 바로 받습니다.

import pandas as pd
from Bio import SeqIO
from Bio.SeqUtils import gc_fraction

# 멀티-FASTA의 모든 서열을 한 줄씩 표로
df = pd.DataFrame([
    {"id": r.id, "length": len(r.seq), "gc_pct": round(gc_fraction(r.seq) * 100, 1)}
    for r in SeqIO.parse("lacz.fasta", "fasta")
])
print(df)
          id  length  gc_pct
0  J01636.1    7477    53.7

이렇게 데이터프레임으로 옮기고 나면 df.describe()로 요약 통계를 보거나, df.plot.scatter("length", "gc_pct")로 길이-GC 산점도를 그리는 식으로 넘어갈 수 있습니다. 서열 수백 개의 길이 분포나 GC 편향을 한눈에 보는 작업이 여기서부터 시작됩니다. Biopython이 서열을 숫자로 바꿔 주면, 그다음은 일반적인 데이터 분석 도구의 영역입니다.

References

  1. Cock, P. J. A., Antao, T., Chang, J. T. et al. (2009). Biopython: freely available Python tools for computational molecular biology and bioinformatics. Bioinformatics, 25(11), 1422–1423. (Biopython 원논문)
  2. Biopython Tutorial and Cookbook (공식 문서)Bio.SeqIO·Seq·SeqRecord 전체 가이드.
  3. Bio.SeqUtils API — gc_fraction — GC 함량 계산 함수 레퍼런스.
  4. Bio.SeqIO.QualityIO — FASTQ 품질점수 처리 — Phred+33 인코딩 구현.
  5. Biopython NEWS (버전별 변경점) — 1.86 릴리스 노트·Bio.Alphabet 제거 이력.
  6. Ewing, B., & Green, P. (1998). Base-calling of automated sequencer traces using phred. II. Error probabilities. Genome Research, 8(3), 186–194. (Phred 품질점수 정의)

Pipette & Pipeline · A bio portfolio journal

이 글을 쓴 사람 Yumingming

생명융합공학과 박사과정.
Microbiome · Cosmetics · RNA Therapeutics · Bioinformatics를 공부하며,
실험(Wet Lab)과 데이터(Dry Lab)를 잇는 글을 논문(article) 기반으로 씁니다.

About · 더 알아보기 →

⚕️ 이 글은 학습·정보 제공 목적이며, 의학적 진단·치료·조언을 대체하지 않습니다. 건강·질병·치료에 관한 결정은 반드시 의사 등 전문가와 상의하세요. 자세한 내용은 면책조항을 참고해 주세요.