8.1. 文字データ#

データ解析では数値だけでなく、文字データも重要な対象となります。特にバイオインフォマティクスの分野では、DNA や RNA の配列といった生物学的情報が A、C、G、T などの文字列として表現されるため、これらを正しく扱うスキルが不可欠です。本節では、こうした文字データの基本的な取り扱い方について解説します。

8.1.1. 文字データ#

文字データは、そのままでは変数名と区別がつかないため、クオーテーションマーク(')やダブルクオーテーションマーク(")で囲んで扱います。例えば、変数 str_data に文字の A を代入する場合は、次のように書きます。

str_data = 'A'

この変数の中身を print 関数で出力すると、次のように表示されます。

print(str_data)
A

Jupyter Notebook では、次のように変数名を実行しても、その中身を確認することができます。

str_data
'A'

また、複数の文字からなるデータも同様に変数に代入できます。このような複数の文字で構成されるデータは、一般的に文字列データと呼ばれます。

str_data = 'hello, world'
str_data
'hello, world'

文字列を変数に代入すると、その変数は複数の文字を持つデータとして扱われます。文字列は、リストのように各文字が順番に並んでおり、それぞれの文字にインデックス(番号)が割り振られています。このため、インデックスを使って特定の位置の文字を取り出したり、スライス機能を使って文字列の一部分を切り出したりすることができます。インデックスはリストと同様に 0 から始まるため、最初の文字を取得したい場合は [0] のように指定します。

str_data = 'Smile. Tomorrow will be worse.'
str_data[0]
'S'
str_data[7:15]
'Tomorrow'
str_data[21:-1]
'be worse'

また、リストのように for 文で一文字ずつ取り出すことも可能です。

str_data = 'hello, world'

for x in str_data:
    print(x)
h
e
l
l
o
,
 
w
o
r
l
d

ここで、if 文と for 文を組み合わせて、与えられた塩基配列の中に文字「A」が何回出現するかを数える例を示します。文字列は for 文で一文字ずつ取り出せるため、取り出した文字が「A」かどうかを if 文で順に判定してカウントします。

dna = 'ACTTAACAGT'
nA = 0

for s in dna:
    if s == 'A':
        nA = nA + 1


nA
4

練習問題 TS-1

与えられた塩基配列中の A、C、G、T の出現回数をそれぞれ求めるプログラムを作成せよ。

s = 'CAGTCATGCGATGTATTATGCATCAC'

文字列の中に特定の文字列が含まれているかどうかを検索する例を見てみましょう。例えば、与えられた塩基配列の中から「ATG」を検索し、もし見つかればその位置を、見つからなければ -1 を出力するプログラムを考えます。

文字列検索を行う際は、まず最初の文字、つまりインデックスが 0 のところから始めます。「ATG」を検索するために、連続する 3 文字を順に取り出して判定します。このとき、スライス機能を利用します。例えば、[0:3][1:4] のように指定します。

p = -1
s = 'CAGTCATGCGATGTATTATGCATCAC'

for i in range(len(s)):
    x = s[i:(i+3)]
    if x == 'ATG':
        p = i

p
17

このように文字列検索を簡単に実装することができました。しかし、詳しく見ると、このプログラムでは、入力された塩基配列に複数の「ATG」が含まれる場合、一番最後に見つかった位置だけを出力する仕様になっています。正しく動作させるには、もう一工夫が必要です。

練習問題 TS-2

与えられた塩基配列の中に「ATG」が含まれているかどうかを調べ、複数個ある場合はすべての位置をリストとして出力するプログラムを作成しなさい。

s = 'CAGTCATGCGATGTATTATGCATCAC'

練習問題 TS-3

与えられた塩基配列に対して、逆相補鎖を求めるプログラムを作成しなさい。なお、逆相補鎖とは、まず配列中の各塩基を相補的な塩基に置き換えたうえで、文字列全体を逆順にしたものを指します。具体的には、A は T に、T は A に、C は G に、G は C に置換します。たとえば、「AACGT」という配列が与えられた場合、まず「TTGCA」に置き換え、さらにこれを逆順にして「ACGTT」とすることで、逆相補鎖を得ることができます。

s = 'CAGTCATGCGATGTATTATGCATCAC'

複数行に渡るような長めの文字列データの場合は、三つのシングルクォートまたはダブルクォート(''' または """)で囲むことで見やすくなります。このとき、''' で囲まれた内側の文字列は、改行も含めて一つの文字列データとみなされ、変数に代入されます。

s = '''Smile.
Tomorrow will be worse.'''

Jupyter Notebook で変数 s をそのまま実行すると、次のように文字列の中に含まれる改行は \n という記号で表示されます。

s
'Smile.\nTomorrow will be worse.'

これは、Python が改行を \n という 1 文字の特殊な記号(エスケープシーケンス)として扱っているためです。

ええ、わかっています。誰がどう見ても \n は 2 文字です。でも、コンピューターの世界では、これが 1 文字。\ は文字ではなく、呪文であり、後ろの文字に呪いをかけて意味を変えてしまいます。世界がちょっとだけ歪む瞬間です。「なぜそうなるのか?」と気になったあなた、その探究心は素晴らしいですが、精神的な安定のためには深入りしないことをおすすめします。調べ始めたその瞬間から、あなたは人生そのものを疑い始めてしまいます。

一方で、print 関数やファイル出力時に使用する write 関数などでこの文字列を出力すると、\n は実際の改行として適切に処理され、画面上では改行された形で表示されます。

print(s)
Smile.
Tomorrow will be worse.

8.1.2. 文字列処理#

二つの文字列を連結するには、+ 演算子を利用します。いくつかの例を見てみましょう。

s1 = 'Smile.'
s2 = 'Tomorrow will be worse.'
s3 = ' '

s = s1 + s2
s
'Smile.Tomorrow will be worse.'
s = s1 + s3 + s2
s
'Smile. Tomorrow will be worse.'
s = s1 + ' ' + s2
s
'Smile. Tomorrow will be worse.'

文字列の分割には、split メソッドを利用します。分割した結果はリストになります。次は、カンマで文字列を分割する例です。

s = '21,31,41,51,61'
x = s.split(',')
x
['21', '31', '41', '51', '61']
x[0]
'21'

なお、分割された各要素も文字列のままです。そのため、これらを + で結合すると数値の足し算ではなく、単に文字列が連結されるだけになります。

x[0] + x[1]
'2131'

ある文字列の中に、特定の部分文字列が含まれているかどうかを検索するには、find メソッドを利用します。

s = 'hello, world'
p1 = s.find('o')
p1
4

find メソッドは、文字列の先頭から検索を行い、最初に見つかった位置を返します。2 回目以降の出現を調べるには、検索開始位置を指定して、改めて検索を行う必要があります。

p2 = s.find('o', p1 + 1)
p2
8

なお、文字列検索にはここで紹介した find メソッドよりも、正規表現を用いる方法が一般的に広く使われます。正規表現を利用すると、曖昧な検索、特定文字列の繰り返し回数の指定、文字数や数値の桁数の制約を加えた検索など、さまざまなパターンを厳密に指定して検索することが可能になります。そのため、簡単な検索であれば find メソッドでも十分ですが、本格的な文字列データの解析を行う場合は、正規表現の使用をお勧めします。

続いて、文字列の置換を行うメソッドを紹介します。これは、文字列の中に含まれる特定の文字列を、別の文字列に置き換える方法です。ここでは replace メソッドを使用します。以下の例では、文字列中の , ; に置換しています。

s = '21, 31, 41,51,61'
x = s.replace(', ', ';')
x
'21;31;41,51,61'

このように、replace メソッドでは、指定したパターンと完全に一致する部分だけが置換の対象になります。たとえば、上の例では , は置換されますが、, のように空白のない部分は置換されません。より柔軟かつ高度な置換を行いたい場合には、正規表現の利用が便利です。正規表現を使えば、たとえば「カンマに続いて空白が 0 個以上ある箇所」といった曖昧なパターンにもマッチさせて置換を行うことができます。

最後に、文字列の両端から特定の文字を取り除く方法を紹介します。文字列の両端から特定の文字を除去するには strip メソッドを使用します。また、左端のみを除去したい場合は lstrip、右端のみは rstrip を使用します。除去する文字を指定しない場合、空白文字(スペース、タブ、改行など)が対象となります。これは、たとえばファイルの各行の末尾にある改行文字などを除去する際に便利です。

次の例では、文字列の両端から空白文字を取り除いています。空白文字が複数連続していても、すべて除去されます。

s = ' hello, world \n'
x = s.strip()
x
'hello, world'

また、メソッドに除去したい特定の文字を指定することもできます。

s = 'hello, world'
x = s.strip('d')
x
'hello, worl'

複数の文字を指定した場合、それぞれの文字がいずれかに一致すれば除去対象となり、両端から順に削除されます。

s = 'hello, world'
x = s.strip('hd')
x
'ello, worl'

8.1.3. 文字と数値の交互変換#

Python の世界では、文字列と数値は異なる型として扱われ、それぞれに応じた動作が定義されています。文字列同士を + で足し算すると、それらは連結されます。一方、数値同士であれば、数学的な和が計算されます。しかし、文字列と数値を直接 + で足すことはできません。この場合、型が異なるためエラーが発生します。

a = '12'
b = 21

a + b
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[27], line 4
      1 a = '12'
      2 b = 21
----> 4 a + b

TypeError: can only concatenate str (not "int") to str
b + a
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[28], line 1
----> 1 b + a

TypeError: unsupported operand type(s) for +: 'int' and 'str'

文字列を 整数や小数に変換することができます。整数への変換には int 関数を、小数への変換には float 関数を使用します。ただし、すべての文字列が数値に変換できるわけではありません。文字列の内容が数値として解釈できない場合、これらの関数を使うとエラーが発生します。

a1 = '12'
a2 = '12.345'
a3 = '1.23e6'

int(a1)
12
int(a2)
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[30], line 1
----> 1 int(a2)

ValueError: invalid literal for int() with base 10: '12.345'
float(a1)
12.0
float(a2)
12.345
float(a3)
1230000.0

数値を文字列に変換するには、str 関数を使用します。基本的に、すべての数値は文字列に変換することが可能です。ただし、桁数の多い小数を文字列に変換する場合、桁数が自動的に丸められたり、省略されたりする桁落ちloss of significance)が発生することがあります。

b1 = 12
b2 = 12.345
b3 = 12.3456789012345678901
b4 = 1.23e6

str(b1)
'12'
str(b2)
'12.345'
str(b3)
'12.345678901234567'
str(b4)
'1230000.0'

8.1.4. f-strings#

Python の f-strings は、文字列の中に変数や計算結果を簡単に埋め込むための方法です。たとえば、計算結果を含むメッセージを作成する際に非常に便利で、コードをシンプルで読みやすく保つことができます。

f-strings を使うには、文字列の前に f を付けて、文字列内の {} の中に変数や式を記述するだけです。プログラムを実行すると、その部分に変数の値や式の結果が展開され、文字列に埋め込まれます。ここでは、f-strings を使った場合と使わなかった場合の違いを見てみましょう。

tree = 'kunugi'
hight = 5.42
weight = 2.08

x1 = f'DATA: {tree} {hight}cm {weight}g'
x1
'DATA: kunugi 5.42cm 2.08g'
x2 = 'DATA: {tree} {hight}cm {weight}g'
x2
'DATA: {tree} {hight}cm {weight}g'

画面への出力だけが目的であれば、変数に保存せずに、print 関数で直接出力するとコードが簡潔です。

print(f'DATA: {tree} {hight}cm {weight}g')
DATA: kunugi 5.42cm 2.08g

f-strings の {} の中には変数名だけでなく、Python の式も書くことができます。たとえば、計算式を直接 {} の中に書くと、その計算結果が自動的に文字列内に埋め込まれます。次のような例が挙げられます。

a = 1
b = 2

print(f'a + b = {a + b}')
a + b = 3
print(f'{a} + {b} = {a + b}')
1 + 2 = 3

f-strings の {} の中で、変数名の後に = を書くと、変数名とその値をセットで文字列に出力できます。この機能は、プログラムの動作確認やデバッグ時に、変数名と中身を同時に確認したい場合にとても便利です。

a = 1
b = 2
print(f'{a=}, {b=}, {a+b=}')
a=1, b=2, a+b=3

また、{} の中に コロン(:)を使うことで、整形オプションを指定することができます。例えば、数値の先頭を 0 で埋めて、全体を 10 桁に整える場合は、次のようにします。

x = 10
print(f'{x:010}')
0000000010

桁の大きな数値を見やすくするために、カンマ区切りで整形するには :, を指定します。

x = 1234567890.123
print(f'{x:,}')
1,234,567,890.123

また、小数点以下の桁数を固定したい場合は f を利用します。次は、小数点以下を 3 桁に固定して文字列に埋め込む例です。

x = 12.3456
print(f'{x:.3f}')
12.346

有効数字の桁数を指定する場合は g を利用します。例えば、有効数字を 3 桁にしたい場合は次のようにします。

x = 12345.3456
print(f'{x:.3g}')
1.23e+04
x = 12.3456
print(f'{x:.3g}')
12.3

数値を指数表記で表示するには、e を使います。例えば、小数点以下 3 桁まで表示する場合は次のように書きます。

x = 12.3456
print(f'{x:.3e}')
1.235e+01
print(f'{x:.3E}')
1.235E+01

パーセント表示をする場合、数値をあらかじめ 100 倍する必要はありません。フォーマット指定に % を使うと、自動で 100 倍されてパーセント記号が付加されます。

x = 0.123456
print(f'percent: {x:.2%}')
percent: 12.35%

文字列の 位置寄せ(左寄せ・中央寄せ・右寄せ)もフォーマット指定で行えます。ゼロ埋めと同じく、フォーマット部分に幅を指定し、<(左寄せ)、^(中央寄せ)、>(右寄せ)を使います。空白で埋めるのが基本ですが、必要に応じて他の文字で埋めることも可能です。

s = 'abc'
print(f'{s: >9}')
      abc
print(f'{s:.^9}')
...abc...
print(f'{s: <9}')
abc      

このように、f-strings は変数の確認から数値の整形まで、幅広い用途に対応できる便利な機能です。フォーマット指定は場面に応じて使い分けることで、出力の見やすさや精度を簡単にコントロールできます。