Python♪小数の繰り返し処理の落とし穴。range()、arange()、linspace()

for文で多用されるPythonの組み込み関数range()では小数が扱えません。しかし、私はこの仕様は気に入っています。なぜなら、小数の使用は動作が不安定になる可能性があるからです。実はNumPyのarange()やlinspace()では小数が使えますが、arange()の使用には注意が必要です。

今回の記事では、小数の計算には誤差があることが与える影響についてfor文を例に説明したいと思います。コンピューターらしい独特な考え方なのではないでしょうか。

0.ゆうちゃんとPythonシリーズ

この記事は「ゆうちゃんとPythonシリーズ」の記事です。一連の記事は、以下のリンク集を参照してください。

中学生のゆうちゃんとPythonシリーズ

なお、それぞれの記事は、シリーズの中でそれまでに習った文法を使ってサンプルコードを考えています。実際には、もっと、効率のよい書き方があるかもしれませんが、ご了承ください。

1.range()は小数が使えない

いきなりですが、コード01はrange()の使用例です。range(start, stop, step)の引数start, stop, stepはそれぞれ整数でなければならず、生成される値も整数です。

#コード01
for i in range(2, 10, 2):
    print(i)
#出力01
2
4
6
8

したがって、コード02のように引数に小数を与えるとエラーになります。

#コード02
for x in range(0.2, 1, 0.2):
    print(x)
#出力02
TypeError: 'float' object cannot be interpreted as an integer

C言語やJavaなどではstepに小数を指定することができるのに、Pythonのrange()ではできません。実際、stepを小数にしたいケースも多く、そのような時にPythonではどのように処理すればよいのでしょうか。

2.コンピューターは小数の計算が苦手

小数の繰返し処理の説明をするまえに、コード03を見てください。0.1 * 3、 0.1 * 6の計算結果は、それぞれ、0.3、 0.6になるはずですが、おかしな数字になっています。

このように、コンピューターは小数の計算が苦手であり、小数の計算では通常は無視できる程度の誤差を含んでしまう可能性があります。

そして、この誤差が、for文で小数を使う場合に不安定な結果を与える原因になります。

#コード03
print(0.1 * 1)
print(0.1 * 2)
print(0.1 * 3)
print(0.1 * 4)
print(0.1 * 5)
print(0.1 * 6)
#出力03
0.1
0.2
0.30000000000000004
0.4
0.5
0.6000000000000001

3.NumPyのarange()を使う方法

Pythonでも小数の繰返し処理を行う方法がいくつかありますので、ご紹介したいと思います。

まずは、NumPyのarange()を使用する方法です。arange()では小数を扱うことができ、書式はarange(start, stop, step)です。そして、startから始まり、増分ステップがstepである数列を生成します。 数列の範囲はstart以上stop未満の範囲です。 なお、stop「以下」ではなく、stop「未満」であることに注意してください。

コード04はサンプルコードです。2行目ではNumPyという数値計算ライブラリを読み込み、npという名前に読み替えています。 NumPy は標準のライブラリではないので、インストールする必要がありますが、 非常に強力かつ有名なライブラリです。anacondaでは最初から使える状態になっています。

「な~んだ、arange()を使えば簡単・・・」と思ったあなた!実は3つの落とし穴があるのです。

#コード04
import numpy as np
for x in np.arange(0.0, 0.2, 0.1):
    print(x)
#出力04
0.0
0.1

(1) 1番目の落とし穴(誤差が出る)

まず、最初の落とし穴について説明します。コード05の出力05では、0.3、0.6、0.7が変な数字になっています。 このように、小数の計算は厳密ではなく、コンピューターは近似値を求める道具であると割り切って使う必要があります。ただ、この誤差を理解して許容できるかどうか判断しなければなりません。

なお、この小さな誤差すらも許せない場合には、float型()ではなく、decimal型を使う方法もありますが、decimal型はfloat型の計算に比べて遅く、どうしても許容できない場合に部分的に使用されることが多いようです。この記事では説明を省略します。

#コード05
import numpy as np
for x in np.arange(0.0, 1.0, 0.1):
    print(x)
#出力05
0.0
0.1
0.2
0.30000000000000004
0.4
0.5
0.6000000000000001
0.7000000000000001
0.8
0.9

(2) 2番目の落とし穴(最後の数字が指定範囲をわずかに超える)

2番目の落とし穴について説明します。コード06では、0.0以上0.6以下の値を0.1ステップで出力しました。出力06で注目すべきは、最後の数字が 0.6000000000000001となっていることです。

多少の誤差は許容できても、「必ず0.0以上0.6以下の範囲でなければならない」場合には、0.6を超えてしまいうので必要な条件を満たすことができません。

#コード06
import numpy as np
for x in np.arange(0.0, 0.7, 0.1):
    print(x)
#出力06
0.0
0.1
0.2
0.30000000000000004
0.4
0.5
0.6000000000000001

少なくとも最初と最後だけは、ピッタリの数字を指定したい場合には、NumPyのlinspace()を使用する必要があります。linspace()については、この記事でも説明します。

(3) 3番目の落とし穴(生成される要素の個数が不安定)

3番目の落とし穴についてはコード07を見てください。 コード07 の3行目、5行目は以下のような結果を期待すると思います。

np.arange(0.5, 0.9, 0.1) → 0.5, 0.6, 0.7, 0.8
np.arange(0.5, 0.8, 0.1) → 0.5, 0.6, 0.7

しかし、出力07をみると、どちらも同じ結果になってしまいました。3つの落とし穴のうち、最も気になる問題です。

#コード07
import numpy as np
for x in np.arange(0.5,0.9,0.1):
    print('Case1:',x)
for x in np.arange(0.5,0.8,0.1):
    print('Case2:',x)
#出力07
Case1: 0.5
Case1: 0.6
Case1: 0.7
Case1: 0.7999999999999999
Case2: 0.5
Case2: 0.6
Case2: 0.7
Case2: 0.7999999999999999

これは、0.7999999999999999が0.8よりも小さいため、np.arange(0.5, 0.8, 0.1)の結果に、0.7999999999999999が含まれてしまうからです。

つまり、arange()で小数を使う場合、一つ一つの出力結果に誤差が生じる可能性があるだけではなく、出力個数についても結果が不安定になってしまうのです。

また、np.arange(0.5, 0.8, 0.1)で、最後の数字が0.7のつもりでいたなら、0.7999999999999999は、1割以上も大きな値になります。誤差というには大きすぎる値です。

numpy.arange()で小数の繰返し処理を行う場合には、このような3つの落とし穴があることを知った上で使用する必要があります。

この3番目の落とし穴は許容できないが、1つ1つの数字の小さな誤差を許容できる場合には、NumPyを使わなくても、組み込み関数のrange()で解決できます。この方法についても、この記事で説明します。

4.組み込み関数のrange()を使う方法

range()は整数しか返すことができませんが、コード08のような計算をすれば、小数の繰返し処理が可能です。しかも、 numpy.arange()の場合と違い、 range()が扱うの値は整数なので、出力が想定外の個数になることはありません。

ただし、最後の数字が、0.6000000000000001のように、想定した範囲を少しでも超えることが許容できない場合には使用することができません。

#コード08
for i in range(0, 7):
    x = i * 0.1
    print('Case1:', x)
#出力08
0.0
0.1
0.2
0.30000000000000004
0.4
0.5
0.6000000000000001

5.NumPyのlinspace() を使えば、最初と最後がピッタリ合う

最後にNumPyのlinspace()を使った方法を紹介します。linspace(start, stop, 要素数)では、増分ステップではなく要素数を指定します。 要素数を多くすれば増分ステップが小さくなります。 また、範囲はstop「未満」ではなくstop「以下」です。つまり、例えばnp.linspace(0.0, 0.3, 4)では、範囲は 0.0以上かつ0.3以下 であり、ステップ幅は(0.3 – 0.0) / (4 – 1) = 0.1となります。

要素数でstepを指定するため、ステップ幅がどんな値になるのか直感的に分かりにくいのが玉にきずですが、一番、安心して使うことができます。

出力09のように、途中の値は多少誤差がありますが、最初と最後の数字は指定した数字とピッタリ合います。また、生成するデータの個数も必ず指定した通りになります。

#コード09
import numpy as np
for x in np.linspace(0.0, 0.3, 4):
    print(x)
#出力09
0.0
0.09999999999999999
0.19999999999999998
0.3

わたし「for文に小数を使うときは、特に最後の数字がどうなるのか気をつけてね。」
ゆうちゃん「でも、結局、numpy.arange()を使わなければいいんでしょ。」
わたし(おっ、問題のポイントはわかってるじゃない!)
わたし「その方が楽かもね。でも、他の言語を使うときには気をつけてね。」
ゆうちゃん「Pythonが好きだから、他は関係ないもんね~。」
わたし「あははは、そりゃそうね。」(楽し~。きっと大物になるよ。)

ゆうちゃんは、修行中の身ですので、しばらくrange()で頑張って欲しいと思っていますが、小数の場合はやっぱりnumpy.linspace()が楽だと思います。

6.まとめ

小数の繰り返し処理が行いたい場合、以下のような方法があります。そして、どうしても誤差が許容できない場合には、計算速度は遅いですがdecimal型という最終兵器を使いましょう。

(1) numpy.arange()

誤差が生じる可能性がある。生成される要素の数が不明確。

(2) range()の値(整数)から必要な小数を計算する。

誤差が生じる可能性がある。生成される要素の数は明快。

(3) numpy.linspace()

最初と最後の数字だけは誤差が生じない。生成される要素の数は明快。
※小数の場合は最も楽だと思います。

例題

for文を使って、0.5, 0.6, 0.7, 0.8, 0.9, 1.0を出力してください。ただし、0.001よりも小さな誤差は無視できるものとします。

(例題1) range()の値(整数)から計算する方法

for文でrange()を使い、range()から生成される整数を使って、 0.5, 0.6, 0.7, 0.8, 0.9, 1.0を出力してください。

解答の表示・非表示の切り替え

※ブラウザによっては最初から表示されてしまいます。(Google Chrome推奨)

#コード10
for i in range(5, 11):
    print(i * 0.1)
#出力10
0.5
0.6000000000000001
0.7000000000000001
0.8
0.9
1.0

(例題2) numpy.linspace() を使う方法

for文でNumPyのlinspace()を使い、 0.5, 0.6, 0.7, 0.8, 0.9, 1.0を出力してください。

解答の表示・非表示の切り替え

※ブラウザによっては最初から表示されてしまいます。(Google Chrome推奨)

1.0 – 0.5 = 0.5なので、要素数をうっかり5としないようにしましょう。なお、0.5と1.0以外は小さな誤差がでる可能性があります。

#コード11
import numpy as np
for i in np.linspace(0.5, 1.0, 6):
    print(i)
#出力11
0.5
0.6
0.7
0.8
0.9
1.0

(例題3) numpy.arange()を使う方法

for文でNumPyのarange()を使い、 0.5, 0.6, 0.7, 0.8, 0.9, 1.0を出力してください。

解答の表示・非表示の切り替え

※ブラウザによっては最初から表示されてしまいます。(Google Chrome推奨)

以下、コード12は間違った例です。これでは、1.0999999999999999が余分です。

#コード12
import numpy as np
for i in np.arange(0.5, 1.1, 0.1):
    print(i)
#出力12
0.5
0.6
0.7
0.7999999999999999
0.8999999999999999
0.9999999999999999
1.0999999999999999

arange()が悪い訳ではありません。分かって使えば、例えばコード13のように、0.5~1.1までではなく、0.5~1.05までを指定することで、同じようなことができます。

#コード13
import numpy as np
for i in np.arange(0.5, 1.05, 0.1):
    print(i)
#出力13
0.5
0.6
0.7
0.7999999999999999
0.8999999999999999
0.9999999999999999

私が実際に購入した教材のご紹介

以下、私が実際に購入したPythonの教材をまとめてみました。 Pythonを学習する上で、少しでもお役に立つことができればうれしいです。

Python♪私が購入したPythonの書籍のレビュー

UdemyのPythonの動画講座を書籍を買う感覚で購入してみた