検索
連載

[解決!Python]range関数と同様に、浮動小数点数値の等差数列を作成するには解決!Python

range関数は整数値を要素とする等差数列を作成できるが、浮動小数点数値を要素として同様な数列を得る方法を紹介する。

PC用表示 関連情報
Share
Tweet
LINE
Hatena
「解決!Python」のインデックス

連載目次

# range関数で得た値に係数をかける
for num in [num * 0.1 for num in range(0, 5)]:
    print(num)
# 出力結果:
#0.0
#0.1
#0.2
#0.30000000000000004
#0.4

# 浮動小数点の誤差をround関数で丸める
for num in [round(num * 0.1, 1) for num in range(0, 5)]:
    print(num)
# 出力結果:見た目は丸められているが、浮動小数点数値なので誤差は含まれている
#0.0
#0.1
#0.2
#0.3
#0.4

# 開始値、終了値、ステップ値に浮動小数点数値を受け取る関数の定義
def frange(start, end , step):
    if step == 0:
        raise ValueError('step must not be zero')

    start = float(start)
    end = float(end)
    step = float(step)

    # range関数と同様な振る舞いにする
    if abs(step) > abs(start - end):
        return [start]
    if step > 0 and end - start < 0:
        return []
    elif step < 0 and end - start > 0:
        return []

    exp = len(str(step).split('.')[1])  # 丸める際に使用する桁数
    result = [start]
    val = start
    if step > 0:
        while (val := round(val + step, exp)) < end:
            result.append(val)
    else:
        while (val := round(val + step, exp)) > end:
            result.append(val)
    return result

for num in frange(0.0, 0.5, 0.1):
    print(num)
# 出力結果:
#0.0
#0.1
#0.2
#0.3
#0.4

# range関数を使うバージョン
def frange(start, end, step):
    if step == 0:
        raise ValueError('step must not be zero')

    start = float(start)
    end = float(end)
    step = float(step)
    if abs(step) >= abs(start - end):
        return [start]

    exp = len(str(step).split('.')[1])  # ステップ値から整数化に使用する値を得る
    start = int(start * 10 ** exp)
    end = int(end * 10 ** exp)
    step = int(step * 10 ** exp)

    result = [round(val * 10 ** -exp, exp) for val in range(start, end, step)]
    return result

for num in frange(0.0, 0.5, 0.1):
    print(num)
# 出力結果:
#0.0
#0.1
#0.2
#0.3
#0.4

import numpy as np

for num in np.arange(0.0, 0.5, 0.1):
    print(num)
# 出力結果
#0.0
#0.1
#0.2
#0.30000000000000004
#0.4

for num in np.linspace(0.0, 0.5, 5):  # 0.0〜1.0の範囲を5要素で等分割する
    print(num)
# 出力結果:
#0.0
#0.125
#0.25
#0.375
#0.5

for num in np.linspace(0.0, 0.5, 6):  # 0.0〜0.5の範囲を6要素で等分割する
    print(num)
# 出力結果:
#0.0
#0.1
#0.2
#0.30000000000000004
#0.4
#0.5

for num in np.linspace(0.0, 0.5, 5, endpoint=False):  # 終了値を含まない
    print(num)
# 出力結果:
#0.0
#0.1
#0.2
#0.30000000000000004
#0.4


range関数と同様な処理を浮動小数点数値でも

 Pythonのrange関数は、指定した値を基に等差数列を表すrangeオブジェクトを生成するものだ。

 例えば、これは初項0、等差2、最大値10(を含まない範囲)の等差数列を表すrangeオブジェクトを作成して、その要素を表示するコードだ。

for num in range(0, 10, 2):
    print(num)


 range関数は便利だが、浮動小数点数値を要素とする等差数列が作成できないのが残念なところともいえる。とはいえ、forとrange関数を組み合わせ、得られた値に10-xをかけることで(xは正数)、求める処理はだいたい行える。

 例えば、以下は初項0.0、公差0.1、終了値0.5(を含まない)範囲の等差数列を表示するコードである。

for num in range(0, 5):
    num *= 0.1
    print(num)
# 出力結果:
#0.0
#0.1
#0.2
#0.30000000000000004
#0.4


 等差数列を含むリストが必要であれば、リスト内包表記を使って次のように書いてもよいだろう(出力は省略)。

flist = [num * 0.1 for num in range(0, 5)]:
for num in flist:
    print(num)


 ただし、上の出力結果を見ても分かる通り、浮動小数点数の演算には多くの場合、誤差が含まれることには注意が必要だ。以下のように、round関数の第2引数に小数点以下の有効桁数を指定することで丸めることも可能だが、誤差が含まれることには変わりはない。

for num in [round(num * 0.1, 1) for num in range(0, 5)]:
    print(num)
# 出力結果:見た目は丸められているが、浮動小数点数値なので誤差は含まれている
#0.0
#0.1
#0.2
#0.3
#0.4


 これは上のコードのprint関数を修正してみると分かる。

for num in [round(num * 0.1, 1) for num in range(0, 5)]:
    print(f'{num:.20f}')
# 出力結果:
#0.00000000000000000000
#0.10000000000000000555
#0.20000000000000001110
#0.29999999999999998890
#0.40000000000000002220


 ここまでに見てきたように、自分で初項、公差、終了値がどうなるかを考えてfor文を書いたり、リスト内包表記を記述したりするのは意外に面倒くさい。そこで、range関数と同様なパラメーターを持つfrange関数を定義すると、次のようなコードが考えられる(frange関数の「f」は「float」を意味する)。

def frange(start, end , step):
    if step == 0:
        raise ValueError('step must not be zero')

    start = float(start)
    end = float(end)
    step = float(step)

    # range関数と同様な振る舞いにする
    if abs(step) > abs(start - end):
        return [start]
    if step > 0 and end - start < 0:
        return []
    elif step < 0 and end - start > 0:
        return []

    exp = len(str(step).split('.')[1])  # 丸める際に使用する桁数
    result = [start]
    val = start
    if step > 0:
        while (val := round(val + step, exp)) < end:
            result.append(val)
    else:
        while (val := round(val + step, exp)) > end:
            result.append(val)
    return result


 コードの中程までは前準備やrange関数と同様な振る舞いとなるようにするためのものだ(特定条件での戻り値の設定)。最後の部分では、ステップ値(公差)を文字列化して小数点以下の有効桁数を計算した後、ステップ値が正値か負値かによって、処理を切り分けて、終了値に到達するまで等差数列の要素を計算している。このときには、上で見たround関数での浮動小数点数値の丸めも(気休めに)行っている。

 この関数の使用例を以下に示す。

for num in frange(0.0, 0.5, 0.1):
    print(num)
# 出力結果:
#0.0
#0.1
#0.2
#0.3
#0.4


 あるいは、与えられたステップ値を基に指数を求め、開始値と終了値、ステップ値である浮動小数点数値を整数化して(10xをかけて)、リスト内包表記とrange関数を組み合わせる方法もある。以下にそのコードを示す。

def frange(start, end, step):
    if step == 0:
        raise ValueError('step must not be zero')

    start = float(start)
    end = float(end)
    step = float(step)
    if abs(step) >= abs(start - end):
        return [start]

    exp = len(str(step).split('.')[1])  # ステップ値から整数化に使用する値を得る
    start = int(start * 10 ** exp)
    end = int(end * 10 ** exp)
    step = int(step * 10 ** exp)

    result = [round(val * 10 ** -exp, exp) for val in range(start, end, step)]
    return result


 何かのライブラリを使わずに自前で行うなら上記のようなコードが考えられるだろう(ただし、上の2つのコードが完璧な振る舞いをするかまでは精査していないので、まるごと使うときには注意してほしい)。

 一方、NumPyが提供するnumpy.arange関数およびnumpy.linspace関数を使っても同様な処理を行える。

 numpy.arange関数はおおよそ次のような構文となる。

numpy.arange(start, stop, step)


 startには開始値(初項)を、stopには最終値、stepにはステップ値(公差)を指定する。戻り値はNumPyの配列(ndarray)となる。range関数と同様にstopだけを指定してもよいし、stepを省略した場合のステップ値は1となる。

 以下に使用例を示す。

import numpy as np

for num in np.arange(0.0, 0.5, 0.1):
    print(num)
# 出力結果
#0.0
#0.1
#0.2
#0.30000000000000004
#0.4


 ただし、numpy.arange関数には出力される配列の長さが安定しないという問題がある。

for n in np.arange(5.0, 5.2, 0.1):
    print(f'{n:.20f}')


 例えば、このコードは5.0と5.1(に近い値)の2つだけがコンソールに出力されると考えられる。しかし、筆者の手元の環境(macOS版のCPython 3.10.4)では以下のように3つの値が表示される(これは浮動小数点数値の丸めによる影響だろう)。

>>> for n in np.arange(5.0, 5.2, 0.1):
...     print(f'{n:.20f}')
... 
5.00000000000000000000
5.09999999999999964473
5.19999999999999928946


 そのため、NumPyのドキュメントではステップ値が整数でないときにはnumpy.linspace関数を使うことが推奨されている。

 numpy.linspace関数はおおよそ次のようなパラメーターを持つ。

numpy.linspace(start, stop, num=50, endpoint=True)


 この関数も開始値(start)に指定した値と終了値(stop)に指定した値の範囲で等差数列を生成するものだが、numpy.arange関数との違いはステップ値ではなく、生成する配列の要素数をnumに指定する点だ。以下に例を示す。

for num in np.linspace(0.0, 0.5, 5):  # 0.0〜1.0の範囲を5要素で等分割する
    print(num)
# 出力結果:
#0.0
#0.125
#0.25
#0.375
#0.5


 ここで注意したいのは、最終値に指定した値が配列に含まれている点だ。numpy.arange(0.0, 0.5, 0.1)とした場合は0、0.1、0.2、0.3、0.4を要素とする配列が生成されるが、上の出力結果を見ると、これとは異なり、5つの要素の最後が最終値(stop)に指定した値となっている。そのため、これは開始値と最終値を5つの要素で等分割することを意味する。これは4つの区間に分割するということでもあり、各要素の間隔(公差)は0.5÷4=0.125となる。

 そのため、配列に最終値を含みつつ、numpy.arange(0.0, 0.5, 0.1)のように公差を0.1とする等差数列を得るには次のようにnumの値を1つ増やして、5つの区間に分割するように指示する必要がある。

for num in np.linspace(0.0, 0.5, 6):  # 0.0〜0.5の範囲を6要素で等分割する
    print(num)
# 出力結果:
#0.0
#0.1
#0.2
#0.30000000000000004
#0.4
#0.5


 あるいは、numpy.arange(0.0, 0.5, 0.1)が返す配列と同様に、最終値を配列に含めないようにするのであれば、パラメーターnumの値はそのままにendpoint=Falseを指定する。これは開始値と最終値の範囲をnum+1個の区間に分割して、最終値を要素に含まない配列を生成する。

for num in np.linspace(0.0, 0.5, 5, endpoint=False):  # 終了値を含まない
    print(num)
# 出力結果:
#0.0
#0.1
#0.2
#0.30000000000000004
#0.4


 numpy.arange関数はrange関数と同じ使い勝手だが、得られる要素数が安定しないなどの点に注意する必要があり、numpy.linspace関数は上で紹介したような点が最初は使いづらいかもしれない。これらのいずれを使うか、あるいは上記のような関数を自作するかはご自分で判断していただきたい。

「解決!Python」のインデックス

解決!Python

Copyright© Digital Advantage Corp. All Rights Reserved.

[an error occurred while processing this directive]
ページトップに戻る