8.2. ファイル処理#
CSV などのファイルからデータを読み込むには、Pandas などのライブラリを用いると便利です。しかし、生物データを記録するファイル形式は CSV に限らず、例えば塩基配列データを記録した FASTA や GenBank、遺伝子注釈情報を記録した GFF などが存在します。このような多様なフォーマットに対応するためには、自分でファイルを開いて中身を確認し、必要な情報を抽出する力が求められます。
8.2.1. 読み取り#
Python では、open
関数を使ってファイルを開きます。処理が終わったタイミングで、明示的にファイルを閉じる必要があります。次は、どんぐりのデータセット(acorns.clean.csv)の最初の行を読み取り、ファイルを閉じる例です。なお、readline
メソッドはファイルの内容を 1 行だけ読み込む関数です。
# !wget https://py.biopapyrus.jp/data/acorns.clean.csv
fh = open('acorns.clean.csv')
print(fh.readline())
fh.close()
tree,weight,height,diameter
ファイルの内容をすべて出力したり、あるいは変数に保存したい場合は、for 構文を使います。open
関数で作られたオブジェクト fh
は、見かけ上、ファイルの各行が要素となっているリストのように扱えます。そのため、fh
を for 文で回せば、ファイルの中身を 1 行ずつ取り出すことができます。
fh = open('acorns.clean.csv')
for x in fh:
print(x)
fh.close()
tree,weight,height,diameter
kunugi,5.55,2.27,1.89
kunugi,4.62,1.98,1.84
kunugi,5.05,2.08,1.90
kunugi,5.44,2.18,1.91
kunugi,5.60,2.20,1.93
kunugi,4.83,2.19,1.85
kunugi,5.55,2.16,2.00
kunugi,5.13,2.20,1.87
kunugi,6.03,2.26,2.01
kunugi,7.41,2.35,2.12
konara,1.60,2.11,1.10
konara,1.49,2.04,1.09
konara,1.74,2.30,1.17
konara,1.57,2.11,1.11
konara,1.88,2.33,1.11
konara,1.63,2.12,1.14
konara,1.45,2.06,1.09
konara,1.07,1.91,0.98
konara,1.53,2.17,1.17
konara,1.61,2.00,1.12
matebashii,2.96,2.75,1.35
matebashii,3.18,2.80,1.44
matebashii,3.08,2.73,1.42
matebashii,2.89,2.78,1.38
matebashii,3.68,2.83,1.51
matebashii,2.93,2.62,1.38
matebashii,2.30,2.50,1.26
matebashii,3.18,2.68,1.46
matebashii,3.13,2.76,1.40
matebashii,2.20,2.54,1.21
shirakashi,1.76,1.95,1.23
shirakashi,1.96,2.06,1.23
shirakashi,1.77,1.93,1.24
shirakashi,1.88,2.07,1.25
shirakashi,1.70,1.97,1.20
shirakashi,1.78,2.09,1.20
shirakashi,1.82,1.97,1.24
shirakashi,1.70,1.95,1.20
shirakashi,1.56,1.98,1.13
shirakashi,1.90,2.06,1.25
shirakashi,1.37,1.89,1.09
shirakashi,2.01,1.94,1.39
shirakashi,2.12,1.97,1.32
shirakashi,1.89,2.03,1.26
shirakashi,1.87,2.06,1.22
arakashi,1.68,1.89,1.23
arakashi,1.42,1.87,1.12
arakashi,1.54,1.82,1.15
arakashi,1.30,1.77,1.19
arakashi,1.53,1.81,1.18
arakashi,1.47,1.85,1.13
arakashi,1.64,1.89,1.26
arakashi,1.44,1.81,1.14
arakashi,1.36,1.87,1.09
arakashi,1.56,1.90,1.17
arakashi,1.49,1.82,1.17
arakashi,1.51,1.84,1.16
arakashi,1.41,1.88,1.12
arakashi,1.48,1.83,1.17
このようにファイルを開いて、各行のデータを順番に取り出すことができるようになりました。次に、各行に記録されているどんぐりのデータセットのうち、重さだけを取り出してリストに格納する例を見ていきましょう。重さのデータは、ファイルの各行をカンマ区切りで分割したとき、2 番目の位置にある値です。そこで、split
メソッドを利用して文字列を分割し、その 2 番目の値をリスト w
に追加する(w.append
)処理を書いていきます。
w = []
fh = open('acorns.clean.csv')
for x in fh:
d = x.split(',')
w.append(d[1])
fh.close()
w
['weight',
'5.55',
'4.62',
'5.05',
'5.44',
'5.60',
'4.83',
'5.55',
'5.13',
'6.03',
'7.41',
'1.60',
'1.49',
'1.74',
'1.57',
'1.88',
'1.63',
'1.45',
'1.07',
'1.53',
'1.61',
'2.96',
'3.18',
'3.08',
'2.89',
'3.68',
'2.93',
'2.30',
'3.18',
'3.13',
'2.20',
'1.76',
'1.96',
'1.77',
'1.88',
'1.70',
'1.78',
'1.82',
'1.70',
'1.56',
'1.90',
'1.37',
'2.01',
'2.12',
'1.89',
'1.87',
'1.68',
'1.42',
'1.54',
'1.30',
'1.53',
'1.47',
'1.64',
'1.44',
'1.36',
'1.56',
'1.49',
'1.51',
'1.41',
'1.48']
ただし、この処理ではファイル 1 行目のヘッダーもデータとしてリストに追加されてしまいます。そのため、ファイルを読み込むときに、1 行目だけスキップする処理(continue
)を加える必要があります。
w = []
fh = open('acorns.clean.csv')
i = 0
for x in fh:
i = i + 1
if i == 1:
continue
d = x.split(',')
w.append(d[1])
fh.close()
w
['5.55',
'4.62',
'5.05',
'5.44',
'5.60',
'4.83',
'5.55',
'5.13',
'6.03',
'7.41',
'1.60',
'1.49',
'1.74',
'1.57',
'1.88',
'1.63',
'1.45',
'1.07',
'1.53',
'1.61',
'2.96',
'3.18',
'3.08',
'2.89',
'3.68',
'2.93',
'2.30',
'3.18',
'3.13',
'2.20',
'1.76',
'1.96',
'1.77',
'1.88',
'1.70',
'1.78',
'1.82',
'1.70',
'1.56',
'1.90',
'1.37',
'2.01',
'2.12',
'1.89',
'1.87',
'1.68',
'1.42',
'1.54',
'1.30',
'1.53',
'1.47',
'1.64',
'1.44',
'1.36',
'1.56',
'1.49',
'1.51',
'1.41',
'1.48']
また、こうして取り出した重さのデータは文字列のままになっています。そこで、リストに追加する際に float
関数を使って小数に変換する処理を加えます。
w = []
fh = open('acorns.clean.csv')
i = 0
for x in fh:
i = i + 1
if i == 1:
continue
d = x.split(',')
w.append(float(d[1]))
fh.close()
w
[5.55,
4.62,
5.05,
5.44,
5.6,
4.83,
5.55,
5.13,
6.03,
7.41,
1.6,
1.49,
1.74,
1.57,
1.88,
1.63,
1.45,
1.07,
1.53,
1.61,
2.96,
3.18,
3.08,
2.89,
3.68,
2.93,
2.3,
3.18,
3.13,
2.2,
1.76,
1.96,
1.77,
1.88,
1.7,
1.78,
1.82,
1.7,
1.56,
1.9,
1.37,
2.01,
2.12,
1.89,
1.87,
1.68,
1.42,
1.54,
1.3,
1.53,
1.47,
1.64,
1.44,
1.36,
1.56,
1.49,
1.51,
1.41,
1.48]
このように、テキストデータから必要な情報を取り出す基本的な方法は、まずファイルを開き、for文を使って各行を順に処理し、最後にファイルを閉じるという流れです。この手順を踏むことで、どのようなフォーマットのファイルであっても、効率的に必要な情報を抽出することが可能です。
なお、ファイルの閉じ忘れを防ぐため、Python では with
文を使ってファイルを読み込むことが推奨されています。この構文を使えば、処理の終了とともにファイルが自動的に閉じられます。ファイルの開き方が with open() as fh
のように変わる点と、fh.close()
が不要になる点を除けば、それ以外の処理は通常と同じです。
w = []
with open('acorns.clean.csv') as fh:
i = 0
for x in fh:
i = i + 1
if i == 1:
continue
d = x.split(',')
w.append(float(d[1]))
w
[5.55,
4.62,
5.05,
5.44,
5.6,
4.83,
5.55,
5.13,
6.03,
7.41,
1.6,
1.49,
1.74,
1.57,
1.88,
1.63,
1.45,
1.07,
1.53,
1.61,
2.96,
3.18,
3.08,
2.89,
3.68,
2.93,
2.3,
3.18,
3.13,
2.2,
1.76,
1.96,
1.77,
1.88,
1.7,
1.78,
1.82,
1.7,
1.56,
1.9,
1.37,
2.01,
2.12,
1.89,
1.87,
1.68,
1.42,
1.54,
1.3,
1.53,
1.47,
1.64,
1.44,
1.36,
1.56,
1.49,
1.51,
1.41,
1.48]
練習問題 TF-1
どんぐりデータセット(acorns.clean.csv
)を読み込み、クヌギのどんぐりに対応する重さ・高さ・直径のデータを、それぞれ変数 k_weight
、k_height
、k_diameter
に格納するプログラムを作成せよ。
練習問題 TF-2
どんぐりデータセット(acorns.clean.csv
)を読み込み、樹種・重さ・高さ・直径のデータを抽出し、Pandas のデータフレームに変換するプログラムを作成せよ。ただし、Pandas ライブラリの pd.read_csv
関数などは使用しないこと。
練習問題 TF-3
哺乳類睡眠データセット(msleep.txt
)を読み込み、すべてのデータを Pandas のデータフレームに変換するプログラムを作成せよ。ただし、Pandas ライブラリの pd.read_csv
関数などは使用しないこと。このファイルにはコメント行が含まれており、これらはデータとして読み込まないように注意すること。また、欠損値は NA
として記録されている。
FASTA ファイルについて見ていきましょう。FASTA ファイルは、塩基配列を記録するための形式です。最初の行は >
記号から始まり、この行には遺伝子名やそのほかの情報が記述されます。続く行には、その遺伝子の塩基配列が 1 行または複数行にわたって記述されます。基本的に、ファイルの末尾、または次に >
が現れるまでの塩基配列は、ひとつの遺伝子に対応する情報です。以下は、FT 遺伝子の塩基配列が 3 行にわたって記述されている例です。
>FT
ACAAAAACAAGTAAAACAGAAACAATCAACACAGAGAAACCACCTGTTTGTTCAAGATCAAAGATGTCTA
TAAATATAAGAGACCCTCTTATAGTAAGCAGAGTTGTTGGAGACGTTCTTGATCCGTTTAATAGATCAAT
CACTCTAAAGGTTACTTATGGCCAAAGAGAGGTGACTAATGGCTTGGATCTAAGGCCTTCTCAGGTTCAA
では、実際のデータファイル(ft.fa)を使って内容を読み込んでみましょう。
# !wget https://py.biopapyrus.jp/data/ft.fa
with open('ft.fa') as fh:
for line in fh:
print(line)
>FT
ACAAAAACAAGTAAAACAGAAACAATCAACACAGAGAAACCACCTGTTTGTTCAAGATCAAAGATGTCTA
TAAATATAAGAGACCCTCTTATAGTAAGCAGAGTTGTTGGAGACGTTCTTGATCCGTTTAATAGATCAAT
CACTCTAAAGGTTACTTATGGCCAAAGAGAGGTGACTAATGGCTTGGATCTAAGGCCTTCTCAGGTTCAA
AACAAGCCAAGAGTTGAGATTGGTGGAGAAGACCTCAGGAACTTCTATACTTTGGTTCTTTCACTTGAAC
TCCCTTTTGTCTCTTTTCTTCTTTAGTTTCTTCAGTGCTTCTTACACCTTCTTTTTTAAAATAGAAATTA
TTTTCCTTTTTTGGGGTATACTGAAAATATTTCTTGGGCATGCAGAGACCTTGGTTAAAAATGCCCCACG
CTTTCCTTTTCTCTGTTTTTTTATGATTTATTTGGTTTTACTTTATGATACCCAAATCAAAAACTAATTT
ATATTTCATTTCTTTTTTCATGAAGATGGACCCATAAAATATTTCTTCCTATCCTAAATTAAATAGAGAT
AAATTTATGATCTATCCCAAATTTTTTCCACCAACTTCTTGCATAAGTGATTATTATTTAGGATAAGAAT
CTTGGGATTTTTCTTTGTTCCTCCTACCTAATAATTTAATTTGCATTTGTTTTTTTTTGTTTTTTTTTAA
GTATAATTTTTTACTATACTTTGAAAATGGCATTTTTGAAATATCCTTTTTGCTAATTAAAATATCTGCA
AAAAGATATAGAAAATTGATGTTCATTAAAACAAATATATATATTGATGAATCTCTGTTGTGGAATATTT
GAAAACTGTTTGAATAAGGATATTCTGATATTCAAGCCAGCCTTTAAGATACTCTCTGCTATATATAGAC
ATGTAGCTACTACCTTTTTTCTATTCATAGTTTTCTTCTTCTTTCTTGTGTTATCTCATTTTCCAAACTT
CAAAAAAGAAAAAGAAAAAAAGACCTTTTGCTTTCTTGATTTCTTTGAAAATGATAATCTTATCTTCTTA
TAATTCTTCGTCTTATTTGTTTAATGAAGGTTATGGTGGATCCAGATGTTCCAAGTCCTAGCAACCCTCA
CCTCCGAGAATATCTCCATTGGTTTGTGCACTAACTCAACTCTTTAATTAATTCACTTTTAAGTTATAGC
ATAGCTCAAACATGTTGCTCGAATTATATATATAGACACTCAACTACACATGTAAAACTGGACATGTATT
TTGGATACTTTGAAATTGAGTAGATCACTTATAACTTAAGACTCAAACATTTTACATTTAATAGAAGGAG
ACATATAAAATCAACTTCGAGAGTGCGATGCATCAATTTGTCTCCCAAAAAAGCCCACACCCAAGCTAAA
TTGACATATTTTGTTCAAAAACTTTACTATGTGTAATGTAAATATGTATTATGTATGTCTGTGTATTTTA
CTGATCGCACATAGATTCTATAGAATGTATAGGACGTGACTTGAACTAAGATTTGTTTTTTTCACTTTAA
AGTGGTCTTTGTACGGGAATATCATTGAGCTTAAAATAGCCTGATGACATCATCATTGTTCCTTGAAGTG
AGCAGGACTAGGTTTCCTGTTATATCACTTTTTATTTTTATTTATATATTCATGCACTTGAATGTTATTG
CATGTTTTGCTAGTCACTTGCACTAGTAATCTAGGAATTAGTTACGTTGCTATTATTTTTTTGTAAGAAA
ATAAAGTTTAAGTAGCATTTCAATTCTGTCAAAAAAAGAAGTAGCATTTCAATTATGTTGTGGTGCCATA
GCTTAAACATGTGTATGGCTATCTCTCATTACAGGTTGGTGACTGATATCCCTGCTACAACTGGAACAAC
CTTTGGTGAGTTTTATTCTATATATTAGATCGCTAGGTGTTAGAAATATAGAAAGGTATATGAAATAGCC
TAATTAATTAGTTACTAGCTAGAAAATTCACATGTTTTGATGAACTTTTTATTTTTCAGGCAATGAGATT
GTGTGTTACGAAAATCCAAGTCCCACTGCAGGAATTCATCGTGTCGTGTTTATATTGTTTCGACAGCTTG
GCAGGCAAACAGTGTATGCACCAGGGTGGCGCCAGAACTTCAACACTCGCGAGTTTGCTGAGATCTACAA
TCTCGGCCTTCCCGTGGCCGCAGTTTTCTACAATTGTCAGAGGGAGAGTGGCTGCGGAGGAAGAAGACTT
TAGATGGCTTCTTCCTTTATAACCAATTGATATTGCATACTCTGATGAGATTTATGCATCTATAGTATTT
TAATTTAATAACCATTTTATGATACGAGTAACGAACGGTGATGATGCCTATAGTAGTTCAATATATAAGT
GTGTAATAAAAATGAGAGGGGGAGGAAAATGAGAGTGTTTTACTTATATAGTGTGTGATGCGATAATTAT
ATTAATCTACATGAAATGAAGTGTTATATTTAT
ファイルが正しく読み込めることを確認できたら、次に FT 遺伝子の塩基配列のうち、A、C、G、T の出現回数を調べてみましょう。open
関数で作成される fh
オブジェクトには、ファイルの各行が含まれています。このうち、>
から始まる行を除けば、それ以外の行はすべて塩基配列です。各行は文字列であり、1 文字ずつが塩基(A、C、G、T)に対応します。そのため、各行をループで回して 1 文字ずつ取り出し、出現回数をカウントすることができます。
nA = 0
nC = 0
nG = 0
nT = 0
with open('ft.fa') as fh:
for line in fh:
for base in line:
if base == 'A':
nA = nA + 1
elif base == 'C':
nC = nC + 1
elif base == 'G':
nG = nG + 1
elif base == 'T':
nT = nT + 1
print(f'{nA=} {nC=} {nG=} {nT=}')
nA=762 nC=391 nG=407 nT=924
次に、同じデータを利用して、塩基配列の長さを求めてみましょう。上のプログラムでは、base
には各塩基が代入されています。塩基が現れるたびに 1 を加算すれば、塩基配列の長さ(つまり遺伝子の長さ)を求めることができます。
ft_length = 0
with open('ft.fa') as fh:
for line in fh:
for base in line:
ft_length = ft_length + 1
ft_length
2523
しかし、このプログラムには一つバグがあります。for base in line
でループを回したとき、各行(line
)の文字を一つずつ取り出して base
に代入していますが、このとき、行末の改行コード(\n
)も 1 文字として含まれてしまいます。
塩基の出現回数をカウントする際には、base
が 'A'
、'C'
、'G'
、'T'
と一致するかどうかを判定していたため、この改行コードは無視され、問題が表面化しませんでした。しかし、塩基配列の長さを単純に文字数として数えると、各行ごとに改行コード 1 文字分が余分にカウントされてしまいます。
この問題を回避するために、rstrip()
メソッドを使って、各行の末尾にある改行コードなどの余分な文字を削除してから処理を行います。
ft_length = 0
with open('ft.fa') as fh:
for line in fh:
line = line.rstrip()
for base in line:
ft_length = ft_length + 1
ft_length
2486
練習問題 TF-4
FT 遺伝子の塩基配列データ(ft.fa)を読み込み、塩基 A、C、G、T の出現確率(出現回数を塩基全長で割った値)を求め、グラフでわかりやすく示せ。
練習問題 TF-5
開花関連遺伝子の塩基配列データ(flower_genes.fa)を読み込み、各遺伝子について、塩基 A、C、G、T の出現確率(出現回数を塩基全長で割った値)を求め、グラフでわかりやすく示せ。このファイルには複数の遺伝子が含まれていることに注意すること。
練習問題はスムーズに解けた。でも実務に踏み込んだ瞬間、「ヘッダー行が長すぎて読み込めない FASTA」、「GTF と言いつつ、なぜか一部が GFF」、「列数が微妙に違う行が混在し、毎回違う結果が出る解析」、「タブとスペースが混在していることに気づかず突き進む」など、背筋が凍る現象が次々に襲いかかってくる。それが現実。この業界、闇が深い。
8.2.2. 書き込み#
既存のデータをファイルに書き込む場合も、open
関数を使ってファイルを書き込みモードで開き、ファイルオブジェクト outfh
を作成します。この outfh
の write
メソッドを用いてデータを書き込みます。たとえば、hello.txt というファイルを作成し、その中に “hello, world” という文字列を書き込む例を見てみましょう。
with open('hello.txt', mode = 'w') as outfh:
outfh.write('hello, world')
このコードを実行すると、Jupyter Notebook を実行しているディレクトリと同じ場所に hello.txt ファイルが作成されます。実際にそのファイルを開いて中身を確認してみましょう。
ファイルに書き込む際、データは基本的に文字列である必要があります。数値などが含まれているとエラーになります。
with open('hello.txt', mode = 'w') as outfh:
outfh.write('hello, world')
outfh.write(2025)
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[13], line 3
1 with open('hello.txt', mode = 'w') as outfh:
2 outfh.write('hello, world')
----> 3 outfh.write(2025)
TypeError: write() argument must be str, not int
そのため、分析結果などをファイルに書き込む場合には、str
関数などを使って数値を文字列に変換する必要があります。さらに、for 文を使う際には、zip
関数を利用することで、同じ長さのリストを複数同時にループ処理できます。
trees = ['kunugi', 'kunugi', 'arakshi', 'shirakashi']
weights = [5.55, 5.23, 1.42, 1.64]
with open('donguri.txt', mode = 'w') as outfh:
outfh.write('tree,weight')
for tree, weight in zip(trees, weights):
outfh.write(f'{tree},{weight}')
しかし、このようにして書き込まれたファイルは、すべてのデータが 1 行に連続して書き込まれてしまいます。これは、write
関数が「1 回呼び出すごとに 1 行を書く」関数ではなく、渡された文字列をそのままファイルに書き込むだけの関数だからです。つまり、書き込むデータの中に改行コード(\n
)が含まれていなければ、すべてのデータが同じ行に並んでしまいます。逆に、データの中に改行コードを含めれば、含まれた分だけ改行されることになります。
trees = ['kunugi', 'kunugi', 'arakshi', 'shirakashi']
weights = [5.55, 5.23, 1.42, 1.64]
with open('donguri.txt', mode = 'w') as outfh:
outfh.write('tree,weight\n')
for tree, weight in zip(trees, weights):
outfh.write(f'{tree},{weight}\n')