喫茶XRAYトップページ


[8071] 四捨五入返信 削除
2014/10/8 (水) 16:50:38 Mr.XRAY
四捨五入と丸めに関するサンプルです.
例えば以下のコードを実行します.
コードの下の図を見てください.
動作確認は,Windows 7 U64(SP1) + Delphi XE5 Pro で,32 ビットのEXEです.


//-----------------------------------------------------------------------------
//  (1)と(2)の計算式は,数学的には等しく,179.935となる
//  SimpleRoundToの使用にはusesにMathが必要
//
//  Math.GetRoundModeで取得したモードはrmNearest
//  Extendedの扱いは
//  Math.SetPrecisionMode(pmDouble);
//  でDouble型として扱うようにできる
//-----------------------------------------------------------------------------
procedure TForm1.Button1Click(Sender: TObject);
var
  a,b,c,d,e,f:Extended;
  strText : String;
begin
  a:=389.7;
  b:=94.4;
  c:=30;
  d:=0.35;
  e:=(a+b+c)*d;    //(1)
  f:=a*d+b*d+c*d;  //(2)

  StrText :=  FloatToStr(e)+#13+
              FloatToStr(f)+#13+ sLineBreak +
              FloatToStr(SimpleRoundTo(e,-2))+#13+
              FloatToStr(SimpleRoundTo(f,-2));
  MessageBox(Handle, PChar(strText), '結果', MB_OK);
end;

//-----------------------------------------------------------------------------
//  変数の型をDoubleとした場合
//-----------------------------------------------------------------------------
procedure TForm1.Button2Click(Sender: TObject);
var
  a,b,c,d,e,f:Double;
  strText : String;
begin
  a:=389.7;
  b:=94.4;
  c:=30;
  d:=0.35;
  e:=(a+b+c)*d;    //(1)
  f:=a*d+b*d+c*d;  //(2)

  StrText :=  FloatToStr(e)+#13+
              FloatToStr(f)+#13+ sLineBreak +
              FloatToStr(SimpleRoundTo(e,-2))+#13+
              FloatToStr(SimpleRoundTo(f,-2));
  MessageBox(Handle, PChar(strText), '結果', MB_OK);
end;


数学的に正確な値は,どちらも 179.94 です.
  e:=(a+b+c)*d;    //(1)
  f:=a*d+b*d+c*d;  //(2)

しかし,結果の図のように Extended 型では e と f が違っています.
Double 型では同じ値になっていますが,数学的に正しい値ではありません.


[8072] Re:四捨五入返信 削除
2014/10/8 (水) 16:51:20 Mr.XRAY
今度は,64 ビットのEXE を作成して実行してみます.
やはり,e と f の表示が違います.


[8073] Re2:四捨五入返信 削除
2014/10/8 (水) 16:52:07 Mr.XRAY
>今度は,64 ビットのEXE を作成して実行してみます.
>やはり,e と f の表示が違います.


当サイトに次のような記事があります.
これとも関係してきます.

[890_計算誤差 ( 数値計算の誤差 ) と多倍長演算]
http://mrxray.on.coocan.jp/Delphi/plSamples/890_CalcError.htm

サンプルの

  e:=(a+b+c)*d;    //(1)
  f:=a*d+b*d+c*d;  //(2)

e と f の値は,数学的には同じでも,コンピュータ内部では異なる場合があります.
FloatToStr 関数は,数値を文字列にする際,15 桁で丸めます.
一方,SimpleRoundTo 関数は,元の値を使用して四捨五入します.

このことを利用すると,数値を一度 FloatToStr 関数で文字列にして,
再度実数にすれば,16 桁以上の精度があれば,計算結果の違いを回避すること
ができることになります.
今回のような単純な計算であれば利用可能でしょう.
そのテスト用コードです.

//-----------------------------------------------------------------------------
//  (1)と(2)の計算式は,数学的には等しく,179.935となる
//-----------------------------------------------------------------------------
procedure TForm1.Button1Click(Sender: TObject);
var
  a,b,c,d,e,f:Extended;
  strText : String;
begin
  a:=389.7;
  b:=94.4;
  c:=30;
  d:=0.35;
  e:=(a+b+c)*d;    //(1)
  f:=a*d+b*d+c*d;  //(2)

  //StrToFloatは15桁で丸めを行うので15桁に丸めた文字列を取得する
  //その文字列を再度実数にする
  e := StrToFloat(FloatToStr(e));
  f := StrToFloat(FloatToStr(f));

  StrText :=  FloatToStr(e)+#13+
              FloatToStr(f)+#13+ sLineBreak +
              FloatToStr(SimpleRoundTo(e,-2))+#13+
              FloatToStr(SimpleRoundTo(f,-2));
  MessageBox(Handle, PChar(strText), '結果', MB_OK);
end;


下図が実行結果です.
指定小数点以上の精度が必要ないか,計算の結果の精度が分かっている場合,
次のように,計算の結果をその小数点の桁数に四捨五入してしまう方法も
考えられます.

  e := SimpleRoundTo(e, -10);
  f := SimpleRoundTo(f, -10);

この他,FloatToStrF 関数, FloatToText 関数 や FloatToTextFmt 関数で,
精度を指定する方法もあります.

   FloatToStrF(f, ffFixed, 10, 2))

は,10 桁の精度で,固定小数点形式の小数点 2 桁までの値の文字列を取得します.

[System.SysUtils.FloatToStrF - RAD Studio API Documentation]
http://docwiki.embarcadero.com/Libraries/XE6/ja/System.SysUtils.FloatToStrF




[8074] Re3:四捨五入返信 削除
2014/10/8 (水) 17:06:18 Mr.XRAY
> 下図が実行結果です.

重要なことは,サンプルコードの値を四捨五入した結果が違っていても,
これは文字列に変換した時の現象,問題です.
計算結果としての e, f の値を,次の計算に使用することには何ら問題はない
ことになります.


[8078] 計算結果をバイト値で確認返信 削除
2014/10/9 (木) 12:59:10 Mr.XRAY
>一方,SimpleRoundTo 関数は,元の値を使用して四捨五入します.
>そのために違いが生じることになります.


SimpleRoundTo 関数で使用する元の値を確認してみます.
元の値とは計算式による値のことです.
Delphi XE2 で実装された浮動小数点用レコードで計算結果の値をバイト値
で確認してみました.
図のように,e の値が 1 だけ小さい値になっています.
つまり,FloatToStr(SimpleRoundTo) の結果は,計算結果を正確に反映していると言えます.

[Delphi のデータ型 - RAD Studio]
http://docwiki.embarcadero.com/RADStudio/XE6/ja/Delphi_%E3%81%AE%E3%83%87%E3%83%BC%E3%82%BF%E5%9E%8B#.E6.B5.AE.E5.8B.95.E5.B0.8F.E6.95.B0.E7.82.B9.E3.83.87.E3.83.BC.E3.82.BF.E5.9E.8B

で,Extended 型のレコード型は TExtended80Rec なので,これを使用しています.
動作確認は,Windows 7 U6(SP1) + Delphi XE5 Pro (VCL-32) です.


//-----------------------------------------------------------------------------
//  (1)と(2)の計算式は,数学的には等しく,179.935となる
//  SimpleRoundToの使用にはusesにMathが必要
//  Delphi XE2 で実装された浮動小数点用レコードで計算結果の値を確認
//-----------------------------------------------------------------------------
procedure TForm1.Button1Click(Sender: TObject);
var
  a,b,c,d,e,f:Extended;
   FF: TExtended80Rec;
   I: Integer;
   StrText : string;
 begin
  a:=389.7;
  b:=94.4;
  c:=30;
  d:=0.35;
  e:=(a+b+c)*d;    //(1)
  f:=a*d+b*d+c*d;  //(2)

  FF := TExtended80Rec(e);
  StrText := '';
  for I := 9 downto 0 do begin
    StrText := StrText + IntToHex(FF.Bytes[I], 2) + ' ';
  end;

  FF := TExtended80Rec(f);
  StrText := StrText + sLineBreak + '';
  for I := 9 downto 0 do begin
    StrText := StrText + IntToHex(FF.Bytes[I], 2) + ' ';
  end;

  StrText := StrText + sLineBreak + sLineBreak +
             FloatToStr(e)+#13+
             FloatToStr(f)+#13+ sLineBreak +
             FloatToStr(SimpleRoundTo(e,-2))+#13+
             FloatToStr(SimpleRoundTo(f,-2));

  MessageBox(Handle, PChar(StrText), '結果', MB_OK);
end;



[8079] Re:計算結果をバイト値で確認返信 削除
2014/10/9 (木) 14:27:31 Mr.XRAY
> つまり,FloatToStr(SimpleRoundTo の結果は,計算結果を正確に反映していると言えます.

   FloatToStr(e)
   FloatToStr(f)
 
は 179.935 になります.
既に述べたように,FloatToStr が有効桁数 15 で丸めをするためと
考えられます.
つまり,今回のサンプルでは,計算結果の違いが 16 桁目以上にあることになります.
実際に [8078] でそのことを確認しました.

[System.SysUtils.FloatToStr - RAD Studio API Documentation]
http://docwiki.embarcadero.com/Libraries/XE6/ja/System.SysUtils.FloatToStr

一方,SimpleRoundTo は,引数の値としても元の値を用いようとします.
その結果ということになります.
当然ですが,以下のように,SimpleRoundTo の丸めの位置を小数点 3 桁目にすると
下図のような結果となります.
 
  FloatToStr(SimpleRoundTo(e,-3)+#13+
  FloatToStr(SimpleRoundTo(f,-3));


[8080] Re2:計算結果をバイト値で確認返信 削除
2014/10/9 (木) 14:37:20 Mr.XRAY
>したがって,以下のように,SimpleRoundTo の丸めの位置を小数点 3 桁目にすると
>下図のような結果となります.


以下のようなコードでも確認できます.
小数点以下,何桁目で丸めをするかということと,
何桁目までを表示するかということとは違うことにも注意します.


procedure TForm1.Button1Click(Sender: TObject);
var
  a,b,c,d,e,f:Extended;
  StrText : string;
 begin
  e:= 179.9349;
  f:= 179.9350;

  StrText := FloatToStr(e) + sLineBreak +
             FloatToStr(f) + sLineBreak + sLineBreak +
             FloatToStr(SimpleRoundTo(e,-2)) + sLineBreak +
             FloatToStr(SimpleRoundTo(f,-2)) + sLineBreak + sLineBreak +
             FloatToStr(SimpleRoundTo(e,-3)) + sLineBreak +
             FloatToStr(SimpleRoundTo(f,-3));

  MessageBox(Handle, PChar(StrText), '結果', MB_OK);
end;

179.9349 を小数点 2 桁目で丸めると,179.93 となるのは,
小数点 3 桁目の値が 4 (5 よりも小さい) だからです.
最初に示したサンプルコードでの e の計算結果は,実際には以下のようになっています.
  179.93499999999....
少なくても仮数部 (有効桁数) の 15 桁目までは 9 ということになります.


[8081] というわけで...返信 削除
2014/10/9 (木) 14:53:27 Mr.XRAY
最初に示したサンプルの結果は,結局,

[890_計算誤差 ( 数値計算の誤差 ) と多倍長演算]
http://mrxray.on.coocan.jp/Delphi/plSamples/890_CalcError.htm

の記事の内容が原因ということが分かりました.
たまにある質問なのですが,現在のコンピュータが有限桁数の演算しか
できないというこを理解していないと,数学的に正しい結果でないことが,
疑問となることがあります.

実際問題として,最後の桁の数値が 1 ぐらい違っていても,
数値計算では誤差範囲内と言えます.

ネタ元はメールでの問い合わせですが,例によって,いろいろやってみました.
質問された方が納得するかどうかは不明です (^^;


[8083] Re:というわけで...返信 削除
2014/10/9 (木) 22:33:50 Mr.XRAY
Delphi Forum に,内容をまとめて投稿しておきました.
[数値計算の誤差と四捨五入]
http://ht-deko.minim.ne.jp/delphiforum/?vasthtmlaction=viewtopic&t=1203.0#postid-2404

このような数値計算の誤差による違いを表現できる例というのは,
中々見つかるものではありません.
個人的には貴重なサンプルではないかと思っています.


[8082] Re:計算結果をバイト値で確認返信 削除
2014/10/9 (木) 15:47:49 Mr.XRAY
>Delphi XE2 で実装された浮動小数点用レコードで計算結果の値をバイト値
>で確認してみました.


Double 型であれば,FormatFloat を使用すれば実際の値を確認できます.
数学的な値は,197.935 です.
しかし,これがコンピュータによる計算の結果なのです.
当然ですが,どちらも,小数点 2 桁で四捨五入する(丸めると)
179.93 となります.


procedure TForm1.Button1Click(Sender: TObject);
var
  a,b,c,d,e,f:Double;
  strText : String;
begin
  a:=389.7;
  b:=94.4;
  c:=30;
  d:=0.35;
  e:=(a+b+c)*d;    //(1)
  f:=a*d+b*d+c*d;  //(2)

  StrText :=  FormatFloat('000.0000000000000000', e) + sLineBreak +
              FormatFloat('000.0000000000000000', f);
  MessageBox(Handle, PChar(strText), '結果', MB_OK);
end;



[8077] 銀行型丸め返信 削除
2014/10/9 (木) 01:15:21 Mr.XRAY
ところで,SimpleRoundTo は,いわゆる「算術的型丸め」ですが,
「丸め」の方式にはいくつかの種類があります.

以下の記事が参考になるかも知れません.

[端数処理 - Wikipedia]
http://ja.wikipedia.org/wiki/%E7%AB%AF%E6%95%B0%E5%87%A6%E7%90%86

[Round() 関数は四捨五入関数ではない: ITコンサルタント 市井賢児のメモ]
http://ichy.seesaa.net/article/2842259.html

[3-33 銀行型丸め(Banker's Rounding)]
http://www.geocities.jp/oraclesqlpuzzle/3-33.html


[8075] 改行文字と #13 と sLineBreak返信 削除
2014/10/8 (水) 18:31:05 Mr.XRAY
コード中で使用している sLineBreak については,以下を参考にしてください.

[05-03_改行文字の扱いと sLineBreak 定数]
http://mrxray.on.coocan.jp/Delphi/plSamples/200_MemoReadSpeed.htm#05-03


[8076] Extended 型返信 削除
2014/10/9 (木) 00:46:17 Mr.XRAY

[8084] ごめんなさい.間違えました返信 削除
2014/10/9 (木) 23:13:56 Mr.XRAY
この「四捨五入」のスレッドの記事に大きな間違いがありました.
これから修正します.
読んで納得してしまった方がいたらゴメンナサイ.
読んでみて,「あれっ?」と思った方もゴメンナサイ.


[8085] Re:ごめんなさい.間違えました返信 削除
2014/10/10 (金) 07:14:31 Mr.XRAY
修正というか,かなり文章を変更してしまいました.
コードも一部変更しています.
結果の図に変更はありません.

今回の例は,Extended 型というのがミソでした.
FormatFloat 関数では,桁数を大きくしても,実際の内部の値が取得できませんでした.
Double 型であれば,計算結果を見ること出来て,説明が簡単だったのですが...


いろいろテストしましたが,数学的には正しくなくても,数値計算の結果としては
当然の結果と言えます.
今回のテストは,非常に単純な計算でしたが,実際の実務での計算はもっと計算式も
多く,複雑であると思われます.その時,結果として取得した値が,数学的に正しい
かを把握して検証するのは,一般的には困難です.
今回のように,数学的な計算結果との照合というのは無理があるかも知れません.
そういうことを研究している研究者もいますが,我々の目的ではありません.

コンピュータを使用した計算,特に実数の使用した計算では,常に誤差が伴うことを
認識する必要があると思われます.
基本的に,数値計算では,数学的な計算と一致する結果を計算するというのは,
期待できない場合があります.今回の計算のサンプルでも,基本的な解決策は
ありません.

[890_計算誤差 ( 数値計算の誤差 ) と多倍長演算]
http://mrxray.on.coocan.jp/Delphi/plSamples/890_CalcError.htm

の記事にあるように,確実に誤差のない計算を希望するのであれば,BCD 演算
ということになります.また,Currency 型 (固定小数点形式) を使用すれば,
小数点 4 桁までは正確な演算が可能です.


[8086] マスター お疲れ様です返信 削除
2014/10/10 (金) 11:03:51 taki
▼ Mr.XRAYさん
 
> 確実に誤差のない計算を希望するのであれば,BCD 演算
> ということになります.また,Currency 型 (固定小数点形式) を使用すれば,
> 小数点 4 桁までは正確な演算が可能です.


マスター お疲れ様です。

え〜っと、通常の実数の演算は2進数で行われるので誤差が出るのですね。

例えば、a := 10; としても、実際には 9.9999999…がセットされているかも…ですね。
32Bitであろうと64BitであろうとDouble型であろうとExtended型であろうと
小数点以下の.999…の桁数が増えるだけということでしょうか。

でも、FloatToStrは「9.999…」と文字列にされると困るので、こそっと有効桁数で
丸め処理をして、「10」と表示しているのですね。なるほど。

それと、学校では四捨五入は4は切り捨てて、5は切り上げるとならったのですが、
銀行で使われている四捨五入は実は五捨五入で結果が常に偶数?になるように
なっているのですね。

例えば小数点1位で丸める場合、2.35→2.4、2.45→2.4、2.55→2.6、2.65→2.6
こうすると、増える方に丸め処理と減る方に丸める処理が同じ確率で行われ
全体としての誤差が減るのですね。
この事を知っていないと2進数の演算誤差と混ざって???となるのですね。

間違いがあったら突っ込んでください。m(_ _)m

昔使っていたIBMのマシンの演算はBCDだったようです。ファイルの数字が0〜9で
表現されてました。

そういえば、Z-80の命令に10進補正命令というのがあったのを思い出しました。


[8087] Re:マスター お疲れ様です返信 削除
2014/10/10 (金) 11:21:36 taki
> え〜っと、通常の実数の演算は2進数で行われるので誤差が出るのですね。
>
> 例えば、a := 10; としても、実際には 9.9999999…がセットされているかも…ですね。


やっちまった。

10は2進数1010で表現できてしまう。
正確には浮動小数点に変換する時に誤差がでるのか?

http://www.picfun.com/mathlib02.html

10は例としては不適当でした。スミマセン


[8088] いろいろテストしても意味がない?返信 削除
2014/10/10 (金) 14:06:11 Mr.XRAY
▼ takiさん

> でも、FloatToStrは「9.999…」と文字列にされると困るので、こそっと有効桁数で
> 丸め処理をして、「10」と表示しているのですね。なるほど。


おそらくそういうことではないかと思います.
ところが,質問された方のコードでは SimpleRoundTo で 2 桁にしてしまって
いますから,その時点で 15 桁で丸めるという「親切?」な仕様が意味なくなって
いるわけです.

素直に,FloatToStrF 関数, FloatToText 関数 や FloatToDecimal 関数等を利用
すればいいのですが,いい題材だと思って,思いつくまま書いてしまいました.
結果,まとまりのない書き込みになってしまいました(汗)

「敵?」のスキルや知識を知ることは難しいです.ハイ.
今回のような現象が起きると,過去の例では,いろいろ数値を変えたりして
テストする人が多いのですが,それは数値計算における演算誤差発生の様子を
調べているだけです.

私のように,素直に「コンピュータでの計算だからそんなもんだ」と
割り切ってしまうといいのですが...


[8089] Re:いろいろテストしても意味がない?返信 削除
2014/10/10 (金) 16:00:39 Mr.XRAY
> > でも、FloatToStrは「9.999…」と文字列にされると困るので、こそっと有効桁数で
> > 丸め処理をして、「10」と表示しているのですね。なるほど。
>
> おそらくそういうことではないかと思います.


これが曲者だったわけです.
計算結果を FloatToStr で文字列にしたら,数学的な計算結果と一致した.
しかし,本当は,15 桁目で丸めた結果なんですが.
そこで,何故同じ FloatToStr 関数を使用しているのに,小数点の以下の桁数を
指定したら誤差がでてしまうのか?
といったことではないでしょうか.多分.


[8090] Re2:いろいろテストしても意味がない?返信 削除
2014/10/10 (金) 17:30:48 taki
▼ Mr.XRAYさん

> 小数点の以下の桁数を
> 指定したら誤差がでてしまうのか?
> といったことではないでしょうか.多分.


10進数の179.935が2進数で近似されているわけだから、
その値は179.9349999999995〜179.9350000000004の間にある?
この条件なら15桁目で丸め処理をすると179.935になる。

関数で小数点以下の桁数を指定して丸め処理をしても、
179.94は179.9399999999995〜179.9400000000004の間にある?

う〜ん、私にはわかりません。(+_+)


[8094] Re3:いろいろテストしても意味がない?返信 削除
2014/10/11 (土) 19:14:18 Mr.XRAY
>その値は179.9349999999995〜179.9350000000004の間にある?

あ〜,こういうことを書かれると手抜きした私の立場が. (^^;
立場を取り戻すために,こんなことやってみました.ヘヘヘッ
ほとんど意味ありませんが.

小数点 16 桁目を調べるために 16 乗しています.
16 桁目も 9 になっています.あとちょっとだったようです
思ったのですが,15 桁で丸めるというのは,内部処理でも精度 16 桁が限界
だからなのかも知れません.


//-----------------------------------------------------------------------------
//  (1)と(2)の計算式は,数学的には等しく,179.935となる
//-----------------------------------------------------------------------------
procedure TForm1.Button1Click(Sender: TObject);
var
  A, B, C, D, E, F : Extended;
  IntE : Int64;
  IntF : Int64;
begin
  Memo1.Lines.Clear;
  Memo1.Font.Name := 'MS ゴシック';
  Memo1.Font.Size := 10;

  A := 389.7;
  B := 94.4;
  c := 30;
  D := 0.35;
  E := (A + B + C) * D;        //(1)
  F := A * D + B * D + C * D;  //(2)

  //Int64は16乗が限界?
  IntE := Trunc(E * Math.Power(10, 16));
  IntF := Trunc(F * Math.Power(10, 16));

  Memo1.Lines.Add(IntToStr(intE));
  Memo1.Lines.Add(IntToStr(intF));
end;


INCM/CMT
Cyclamen v3.82