1.0365の2乗を正しく計算するには?

解決


Aoi  2012-03-27 10:22:50  No: 147522  IP: [192.*.*.*]

vs2005のvb.netにて小数のべき乗時正しい値が得られません
具体的には
1.0365の2乗を計算したいのですが…
1.07433225となるところが1.0743322499999999となってしまいます
変数はdecimalで定義しているので誤差は出ないと思っていたのですが…
どなたか解決策をご教示下さい

編集 削除
ZEN  2012-03-27 10:56:20  No: 147523  IP: [192.*.*.*]

例えばDoubleで計算したものをDecimalにしていませんか?
とりあえず、どのように計算したら1.0743322499999999となるか、コードを見せてもらえますか?

以下を試してみましたが、bの値はちゃんと1.07433225Dになっていました。

Dim a As Decimal = 1.0365
Dim b As Decimal = a ^ 2
MsgBox(b)

編集 削除
Aoi  2012-03-27 11:13:58  No: 147524  IP: [192.*.*.*]

イミディエイトウインドウで
?1.0365D ^ 2.0D
で現象がでます
ソースは…ものすごく複雑でして…
式を切り出してイミディエイトウインドウで確認している際に発見しました
8位で四捨五入するので、結果が変わってしまうんです

OSとかも書きますと
windowsXP
VS2005は8.0.50727.762(SP.050727-7600)
VBは77971-009-0000007-41803
です

編集 削除
ZEN  2012-03-27 11:40:38  No: 147525  IP: [192.*.*.*]

こちらは、VB2008ですが、確かにイミディエイトウインドウで

?1.0365D ^ 2.0D

で同じ現象になります。
ただし、これは結果をDoubleで評価しているのでしょう。

例えば、以下方法ではメッセージボックスの表示は1.07433225ですが、cの値をウォッチすると1.0743322499999999になります。

Dim a As Decimal = 1.0365D
Dim b As Decimal = 2D
Dim c As Double = a ^ b
MsgBox(c)

ただし、以下の場合は、メッセージボックスの表示も1.07433225で、cの値をウォッチしても1.07433225Dとなります。

Dim a As Decimal = 1.0365D
Dim b As Decimal = 2D
Dim c As Decimal = a ^ b
MsgBox(c)

つまり、どこかでDoubleに変換した計算が含まれているのではないでしょうか。
ソースが複雑とのことなので、可能性がありそうですが、コードを見ないと何とも言えません。

あと、四捨五入を行っているとのことですが、どのような方法で行っているのでしょうか?
もっとも、1.0365の2乗の精度に小数点以下8桁を求めていますが、1.0365の数値の精度が小数点以下8桁でなければ無意味であることは理解できていますか?

編集 削除
Aoi  2012-03-27 11:45:23  No: 147526  IP: [192.*.*.*]

計算式のコードです

Math.Round((1+a)^(b/12-(c*12+d)/12,7,MidpointRounding.AwayFromZero)
使っている変数は全部decimalで
変数の内容は
a:0.0365D
b:830
c:67
d:2
です

編集 削除
Aoi  2012-03-27 12:30:16  No: 147527  IP: [192.*.*.*]

式のカッコが足りないですね
Math.Round((1+a)^(b/12-(c*12+d)/12),7,MidpointRounding.AwayFromZero)
会社のpcから書き込めるといいのですが、最近厳しいので…
ソースを見ながら転記したのでカッコをつけ忘れてしまいました

bがループのカウンタになってまして、今回偶然乗ずる値が2になりました
通常はもっと小数点以下の桁数が多い数値になります
一気に計算してしまっていますが、これがdoubleになってしまっている可能性はありますか?

それと…
ZENさんと同じソースで実験したところ1.07433225になりました

編集 削除
ZEN  2012-03-27 12:42:26  No: 147528  IP: [192.*.*.*]

式の括弧閉じが足りないようなのでこちらでは検証できませんが、Math.Roundの第一引数が、勝手にDoubleにキャストされている等考えられます。
試しに、Math.Roundの第一引数をCDec等でDecimalにキャストしてみてはどうですか?

編集 削除
ZEN  2012-03-27 12:51:57  No: 147529  IP: [192.*.*.*]

> Math.Round((1+a)^(b/12-(c*12+d)/12),7,MidpointRounding.AwayFromZero)

指数の部分の結果が2にならないのですが、式は正しいですか?
あと機になるのは、b/12等の計算を行っていますが、これは割り切れないので誤差の原因となりますよ。
正しい式が分からないので何とも言えませんが、何度も12で割る必要があるのであれば、誤差を最小にするために通分すべきです。

編集 削除
ZEN  2012-03-27 13:13:50  No: 147530  IP: [192.*.*.*]

勘違いしていました。指数の部分は2でした。ごめんなさい。
ただ、先ほど指摘した12で割る部分、少なくとも

b/12-(c*12+d)/12

は、以下のようにすべきです。

(b-(c*12+d))/12

編集 削除
ZEN  2012-03-27 13:41:14  No: 147531  IP: [192.*.*.*]

以下のようにすると、小数点以下第8位は切り捨てになりますが、

Math.Round((1 + a) ^ ((b - (c * 12 + d)) / 12), 7, MidpointRounding.AwayFromZero)

以下のようにすると小数点以下第8位は切り上げられますので、

Math.Round(CDec((1 + a) ^ ((b - (c * 12 + d)) / 12)), 7, MidpointRounding.AwayFromZero)

やはりMath.Roundの第一引数がDoubleとして評価されているようですね。
Decimalで計算したい場合は、計算結果をその都度Decimalにキャストしないと、Doubleのオーバーロードが使われてしまうようですね。

編集 削除
Aoi  2012-03-27 13:46:59  No: 147532  IP: [192.*.*.*]

指数部分は(830/12-(67*12+2)/12)…(830/12-806/12)=2.0になるはずです
おっしゃる通りで12で割る部分は1つに出来るんですが、式自体仕様になっているので変えられないんです

解決しました
ご指摘通りにMath.Roundの第一引数をCdecしてあげたら1.07433225Dになりました
勝手にdoubleにキャストされてしまうのはどういう場合なんでしょうか?
今回はべき乗している箇所でしたが、加減乗除すべての演算でdoubleにキャストされて誤差が出ることを考慮すべきなんでしょうか?

編集 削除
ZEN  2012-03-27 14:05:56  No: 147533  IP: [192.*.*.*]

解決して何よりです。

> 今回はべき乗している箇所でしたが、加減乗除すべての演算でdoubleにキャストされて誤差が出ることを考慮すべきなんでしょうか?

例えば、iがIntegerだとした場合、例えばi / 3という式だけを考えた場合、この式はDoubleで評価されるということを何かで見た覚えがあります。
そういう意味では、Roundなど複数の型のオーバーロードがあるメソッドを使うときは、今回のようにDecimalに明示的にキャストするか、いっそのこと計算結果を一度Decimal型に入れることが必要だと思います。

編集 削除
Aoi  2012-03-27 14:57:52  No: 147534  IP: [192.*.*.*]

加減算は大丈夫みたいですね
掛け算と割り算はdecimalにキャストするようにしたいと思います

色々と調べて頂きありがとうございました

編集 削除
Mira  2012-04-05 16:59:34  No: 147535  IP: [192.*.*.*]

こんにちは解決してるのに割り込みます^^;

私の推測ではウォッチ式はObject型で値を格納してると思われます
下記の式を実行すれば格納する型によって演算が変わっていることが
よくわかります

以上のことから
VBの場合は格納する変数の型さえDecimalにしていれば気にしなくていい^^;
と考えていいのではないでしょうか

            Dim A1 As Double = 1.0365
            Dim B1 As Double = 2
            Dim A2 As Decimal = 1.0365
            Dim B2 As Decimal = 2

            Dim A1B1O As Object = A1 ^ B1
            Dim A2B2O As Object = A2 ^ B2
            Dim A1B1D As Decimal = A1 ^ B1
            Dim A2B2D As Decimal = A2 ^ B2

編集 削除
魔界の仮面弁士  2012-04-05 20:05:02  No: 147536  IP: [192.*.*.*]

》ZENさん
> ただし、これは結果をDoubleで評価しているのでしょう。
VB.NET で「a = b ^ c」という演算処理を行った場合、それは
  Dim w As Double = Math.Pow( Convert.ToDouble(b), Convert.ToDouble(c) )
  Dim a As Decimal = New Decimal(w)
に相当する処理に置き換えられます。
ヘルプにも、「^ 演算子」の演算結果は常に Double 値であると明記されていますね。


》ZENさん
> 例えば、iがIntegerだとした場合、例えばi / 3という式だけを考えた場合、
> この式はDoubleで評価されるということを何かで見た覚えがあります。
i / 3 の演算結果は、基本的には Double 型で返される仕様です。ただし、
i が Single 型の場合は、結果も Single 型になりますし、
i が Decimal 型の場合は、結果も Decimal 型です。
詳細はヘルプの「/ 演算子」の項をご覧ください。


》Aoi さん
> 掛け算と割り算はdecimalにキャストするようにしたいと思います
最初の回答で ZEN さんが既に書かれていますが、この場合、
「演算結果を Decimal に変換する」のではなく、演算処理を
「Decimal で演算されるような計算式」で行う必要があることに
注意してください。これは、演算結果が既に誤差を含んでいた場合、
後から Decimal に変換しても、誤差が消えるわけでは無いからです。


》Aoi さん
> 加減算は大丈夫みたいですね
加減算の場合でも、「桁落ち誤差」や「情報落ち誤差」が発生する可能性はあります。

'--------------------
Dim a As Decimal, b As Decimal
a = 0.0000000987654321D
b = 1234567890000000D

Dim x As Decimal, y As Decimal, z As Decimal

x = 0D
y = 0D
z = 0D
For i As Integer = 1 To 1000
    x += a
    y += b
    z += (a + b)
Next
For i As Integer = 1 To 1000
    x += b
    y += a
Next

MsgBox(x.ToString() & vbCrLf & y.ToString() & vbCrLf & z.ToString())
'--------------------

上記は、a と b をそれぞれ 100 回ずつ足している処理です。
単純に考えれば、最終結果として予想される値は、
  v = 1234567890000000000.0000987654321
なのですが、最終結果は Decimal の有効桁数を越えています。

実際に上記の処理を試してみると、結果が
  x = 1234567890000000000.0000987654
  y = 1234567890000000000.0000988000
  z = 1234567890000000000.0000987804
となっており、加算順序を変えるだけで最終結果が変化していることが分かります。

このような場合には、(x のように)値の小さい物から加算することで
誤差を抑えることができます。


》Aoi さん
> 1.0365の2乗を計算したいのですが…
> 1.07433225となるところが1.0743322499999999となってしまいます
そもそも Double 型は、内部的には二進小数として管理される数値型です。

1÷3 の結果を、10進数では有限桁数で表現できない(3進数なら表現できる)のと同様、
1÷10の結果は、2進数では有限桁数で表現できない(10進数なら表現できる)。

そして 1.0365 という10進小数の場合、その2進表現は、

1.00001001010110000001000001100010010011011101001011110001101010011111101111100111011011001000101101000011
      1001010110000001000001100010010011011101001011110001101010011111101111100111011011001000101101000011
      1001010110000001000001100010010011011101001011110001101010011111101111100111011011001000101101000011
      1001010110000001000001100010010011011101001011110001101010011111101111100111011011001000101101000011…

のような循環小数となります。変数が保持できる桁数は有限ですから、
途中の桁で打ち切られて、「近似値」として扱われることになります。


ちなみに、1.0365 が Decimal として扱われた場合には、誤差なく格納できます。
概念的には
「整数 10635 の右から 4 桁目に小数点を打った値」のように管理されます。

そのため、Double/Single の場合のような誤差は生じません。
もちろん、1÷3 を格納するような場合は近似値になってしまいますけれどね。

編集 削除
魔界の仮面弁士  2012-04-05 23:49:49  No: 147537  IP: [192.*.*.*]

> 上記は、a と b をそれぞれ 100 回ずつ足している処理です。
失礼しました。100 回ではなく 1000 回ですね、訂正しておきます。



》Aoiさん
> 1.0365の2乗を計算したいのですが…

「2乗」などのように、自然数が入る分には、
1.0365 * 1.0365 などで乗り切れますが、
小数の場合はそうもいきませんしね…。


先の回答にも書いたように、残念ながら「^ 演算子」の結果は
Double 固定ですし、Math クラスや Decimal クラスにも、
Decimal 精度でのべき乗演算メソッドは有していません。

数式を組み替える事でべき乗計算せずに済ませられないか検討してみてください。
その上で、どうしても精度が必要な場合は、自分で算出することになるでしょう。
http://okwave.jp/qa/q144547.html
http://keisan.casio.jp/has10/SpecExec.cgi



》Miraさん
> VBの場合は格納する変数の型さえDecimalにしていれば気にしなくていい^^;
> と考えていいのではないでしょうか

それは違うと思いますよ。最初の回答で、ZEN さんも、
>> 例えばDoubleで計算したものをDecimalにしていませんか?
と書いていますよね。

たとえ Object 型で受け取ろうとも、演算結果が Double である以上、
その Object が、Double 値を示しているという事実は変わりませんし、
Decimal で受けても、Double 値として演算されている点は一緒です。

そして Double は、0.50 や 0.25 といった値ならば誤差なく格納できるものの、
0.1 や 1.07433225 は近似値としての扱いになってしまう数値型です。


確かに、二進小数な Double に比べ、Decimal は十進小数なこともあり、
我々が普段扱う値に近いのは Decimal の方と言えます。そのため、誤差を含んだ値を
Decimal 変換すると、求める値に近い値となることも少なからずありますが、
それをもって「気にしなくていい」というのは、ちょっと乱暴すぎるかと。


Miraさんのコードでは、確かに A1B1D や A2B2D が 1.07433225 になりますが、
それは丸め処理によって、たまたま期待値と同じ結果が得られたということです。

今回の処理では、演算処理の時点で Double として誤差を含んでしまっている以上、
最後に Decimal に変換するだけでは、必ずしも誤差を排除できるとは限りません。


次の処理を見てください。

Dim r As Double = 1.0743322499999999R   'Dim r As Double = A1 ^ B1
Dim x As New Decimal(r)    'Dim x As Decimal = r
Dim y As Decimal = Decimal.Parse(r.ToString("R", Nothing))
Dim z As Decimal = Decimal.Parse(r.ToString("G", Nothing))

この場合の実行結果は、
  x = 1.07433225
  y = 1.0743322499999999
  z = 1.07433225
となります。

Miraさんのコードの A1B1D や A2B2D の値というのは、
上記でいうところの x の値に相当しています。

もしも r の値が、「1.07433225 の格納誤差」なのだとしたら、
確かに x の値は正しい値であるかのように見えますね。

しかし r の値が、本当に「1.0743322499999999」なのだとしたら、
誤差なく変換できているのは、y の方ということになってしまうわけです。

「^ 演算子」が Double 型の精度である以上、本当に求めていた値が
1.0743322500000000 なのか 1.0743322499999999 なのかは、演算結果からでは
保証できないわけですから、後から Decimal 化すれば済むという話にはなりません。

大変かもしれませんが、計算後の事後対応ではなく、入力値や演算段階での対処が望ましいかと。


# この話は、x と y いずれの Decimal 型変換処理が正しいのかを
# 論じているわけではない事に注意してください。
# 計算処理を行ってから Decimal に格納するのでは意味が無く、そもそもの
# 計算結果に十分な精度をもたせることを保証するべきである、という話です。

編集 削除