【注意】人間よりもコンピュータの方が小数点に弱い！？

## はじめに

コンピュータと言われたら、皆さんはどんなイメージを持ちますか？

「完璧！」「ミスすることなんてない！」「正確無比！」

しかし実際には、コンピュータも計算結果として正しい値が得られないことがあります。

その一例が、小数点のある数（浮動小数点数）の計算です。

今回は、その代表的な例を見ながら、なぜこのようなことが起こるのかを解説していきます。

## 実例

### Pythonの場合

まずはPythonでの例を見てみましょう。

```python
# culc.py
a = 0
for i in range(100):
    a += 0.1
print(a)
```

<details><summary>結果を見る</summary>

```
9.99999999999998
```

</details>

100回 `0.1` を足したので、結果は **10** になると思いきや、**9.99999999999998** と表示されます。

### JavaScriptの場合

次にJavaScriptで同様のコードを実行してみます。

```javascript
let a = 0;
for (let i = 0; i < 100; i++) {
    a += 0.1;
}
console.log(a);
```

<details><summary>結果を見る</summary>

```
9.99999999999998
```

</details>

やはり期待した **10** ではなく、**9.99999999999998** という結果になります。

## なぜ正確な値が得られないのか？

小数点を扱うときに、なぜこのような誤差が生じるのでしょうか？

その原因は、コンピュータの **浮動小数点数の表現方法** にあります。

### 浮動小数点数とは

コンピュータは、有限のビット数(2進数)で数値を表現します。

浮動小数点数（floating point number）は、実数を近似的に表現するための方式で、[**IEEE 754規格**](https://ja.wikipedia.org/wiki/IEEE_754)に基づいています。

浮動小数点数は、指数部と仮数部で数値を表現します。

しかし、有限のビット数で無限に続く小数を正確に表すことはできません。

例えば、10進数では **0.1** は正確に表せますが、2進数では無限に続く循環小数になります。

### 0.1 の二進数表現

**0.1** を二進数で表すと、

```
0.0001100110011001100110011001100110011001100110011001101...
```

と無限に続く小数になります。

コンピュータは有限の桁数でこれを表現するため、途中で **切り捨て** または **丸め** を行います。

その結果、微小な誤差が生じます。

### 誤差の蓄積

ループで **0.1** を100回足すと、この小さな誤差が積み重なり、最終的に期待値とズレが生じます。

例えば、Pythonの例では、各加算での誤差が累積し、最終的に **9.99999999999998** という結果になっています。

いわゆる**丸め誤差**です。

:::note info
ここでコンピュータで起こる誤差について覚えておきましょう。
1. ***丸め誤差***
    症状：小数点の計算で、2進数で正確に表現できないことによる誤差
    例：`0.1 + 0.2 = 0.30000000000000004`
    対処：Decimal('0.1') + Decimal('0.2') = 0.3　のように `Decimal`を利用

2. ***桁落ち***
    症状：近い値の大きな数の引き算で精度損失
    例：`√10**15+1 - √10**15 `
    対処：√x+1 - √x = 1 / (√x+1 + √x)に変更するなど、有効数字の桁数に注意した計算方法
    [参考](https://pianofisica.hatenablog.com/entry/2020/09/04/163000)

3. ***情報落ち***
    症状：スケールが大きく異なる値の計算での精度損失
    例：`1000000 + [0.0001]*1000 = 1000000.9999994654`
    対処：n=[0.0001]*1000 → n+100000のように、小さい値同士を先に計算

4. ***打ち切り誤差***
    症状：無限の計算を有限で打ち切る際の誤差
    例：`π ≒ 3.1415`
    対処：|次の項| < 1e-5 など、許容誤差を設定して打ち切り

5. ***オーバーフロー***
    症状：最大値超過
    例：32ビット整数の最大値(2147483647)に1を加える → -2147483648
    対処：if x > MAX_INT - y: エラー処理 など、計算前に範囲チェック

6. ***アンダーフロー***
    症状：最小値以下
    例：1.0e-308 × 1.0e-308 = 0
    対処：(1.0e-308 × 1.0e8) × (1.0e-308 × 1.0e8) × 1.0e-16 のように、スケーリングして計算

桁落ちや情報落ちは大きな意味では丸め誤差の一種である。
:::


## どのように対処すべきか

浮動小数点数の誤差問題は、数値計算でよく遭遇する問題です。

これを回避・軽減するための方法をいくつか紹介します。

### 1. 小数モジュールを使用する（Pythonの場合）

Pythonでは、`decimal` モジュールを使用することで、任意精度の十進数計算が可能になります。

```python
from decimal import Decimal, getcontext

getcontext().prec = 28  # 任意の精度を設定
a = Decimal('0')
for i in range(100):
    a += Decimal('0.1')
print(a)
```

**結果**

```
10.0
```

</details>

**Decimal** クラスを使用することで、誤差のない正確な計算が可能です。

### 2. 整数で計算する

小数部分を整数に変換して計算する方法です。

例えば、**0.1** を **1** として扱い、計算後に適切にスケーリングします。

```python
a = 0
for i in range(100):
    a += 1  # 0.1を1として計算
print(a / 10)  # 最後に10で割って元の単位に戻す
```

**結果**
```
10.0
```

</details>

整数計算は誤差が生じないため、信頼性の高い結果が得られます。

### 3. 誤差を考慮した比較を行う

浮動小数点数を比較する際に、直接の等価比較（`==`）ではなく、許容誤差を設けて比較します。

```python
a = 0
for i in range(100):
    a += 0.1

if abs(a - 10) < 1e-9:
    print("aは10に非常に近い値です")
```

<details><summary>結果を見る</summary>

```
aは10に非常に近い値です
```

</details>

この方法では、微小な誤差を許容し、実質的に等しいと判断します。

## 他の言語での対処法

### JavaScriptの場合

JavaScriptでも同様の問題が起こりますが、`toFixed` や `toPrecision` メソッドを使用して表示桁数を指定することで見かけ上の誤差を調整できます。

```javascript
let a = 0;
for (let i = 0; i < 100; i++) {
    a += 0.1;
}
console.log(a.toFixed(10));  // 10.0000000000
```

また、`BigDecimal` ライブラリを使用して高精度な計算を行うこともできます。

### IEEE 754 に基づく他の言語

C言語やJavaなど、多くのプログラミング言語で浮動小数点数はIEEE 754規格に基づいています。

したがって、同様の問題が発生します。

必要に応じて、高精度の数値型やライブラリを使用することが推奨されます。

## まとめ

コンピュータは、有限のビット数で数値を表現するため、小数計算で誤差が生じることがあります。

これを理解し、適切な対応策を講じることで、正確な計算結果を得ることができます。

プログラミングにおいては、浮動小数点数の特性を理解し、必要に応じて **適切な数値型** や **計算方法** を選択することが重要です。

---

**参考文献**

- [Python公式ドキュメント - 浮動小数点の誤差について](https://docs.python.org/ja/3/tutorial/floatingpoint.html)
- [Pythonでの0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 == 1.0 の結果は？](https://qiita.com/JavaLangRuntimeException/items/73d904b73106d1be5aae)
- [JavaScriptにおける丸め誤差と対応](https://zenn.dev/ymmt1089/articles/20220603_big_number)
- [プログラムはなぜ動くのか](https://www.amazon.co.jp/%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%A0%E3%81%AF%E3%81%AA%E3%81%9C%E5%8B%95%E3%81%8F%E3%81%AE%E3%81%8B-%E2%80%95-%E7%9F%A5%E3%81%A3%E3%81%A6%E3%81%8A%E3%81%8D%E3%81%9F%E3%81%84%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0%E3%81%AE%E5%9F%BA%E7%A4%8E%E7%9F%A5%E8%AD%98-%E7%9F%A2%E6%B2%A2-%E4%B9%85%E9%9B%84/dp/4822281019?crid=2S6ZSUPYEHE0U&dib=eyJ2IjoiMSJ9.2kmBDP2CqkqsziGYqhBEgiP5ZCmemm2bNASgYWOStGdbT1HdA5Hd6pg_1S2BRv84s_7uJZTFhf9tRgadfSeeB7knBRP9FWESGt3UgWi-5t31jasM-pLikoyiTnJ4PwjvrMQSRf2KB6wx9pNlAMSvFr_7WVQeuwrj0_hYlO3aJ8ENlEQKEZ93fKYNY9JB4RpE6IBN7-PxDCaS6_M8KNqv8ainvlYtbPiVWgcbL4WsCiC4xqURqik2mtGZSloMrkpaKc7V3Fp6ePNotYV2f0CJEJZyhjP0mOIdomXQpVRuePQ.mEj1UuZoeqlqA_giDuw97WTXR0hnwR8Ve9nQZ27rzs8&dib_tag=se&keywords=%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%A0%E3%81%AF%E3%81%AA%E3%81%9C%E5%8B%95%E3%81%8F%E3%81%AE%E3%81%8B&qid=1733400494&sprefix=%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%A0%E3%81%AF%2Caps%2C176&sr=8-3) →　ここではVisualBasicを例と出していた
- [コンピュータで扱う数値の誤差について｜ 基本情報技術者試験のポイント解説](https://www.foresight.jp/fe/column/about-error/#:~:text=%E6%89%93%E5%88%87%E3%82%8A%E8%AA%A4%E5%B7%AE%E3%81%A8%E3%81%AF%E3%80%81%E4%B8%BB,%E8%A8%88%E7%AE%97%E3%81%8C%E6%8C%99%E3%81%92%E3%82%89%E3%82%8C%E3%81%BE%E3%81%99%E3%80%82)
→ 基本情報や応用情報技術者試験はIT知識を網羅的に知る機会なのでおすすめです