2010年1月7日木曜日

lossless proration algorithm

按分した数値を丸めて再度合計したときの、元の値とのずれをどう処理するかについて考えてみた。

■ 問題
例えば、10円をなるべく 11:12:27 (計50) に近い比率で按分したいとする。比率を掛けて丸めると以下のようになる
10 * (11 / 50) = 2.2 ≒ 2円
10 * (12 / 50) = 2.4 ≒ 2円
10 * (27 / 50) = 5.4 ≒ 5円
小数部分を合計すると 10円だが、丸めた整数部分を計算すると 9円で 1円消えてしまう。この 1円をどうするか。

◆ ① 誤差を許容する
仕様によっては、これもあると思う。検索してみると、そういう但し書きのある公官庁発表のデータもいくつか見つかる。

◆ ② 誤差を最大値に割り当てる
上例の場合、5 円に誤差 1円を加算して 2円、2円、6円とする。ロジック的には、どこに誤差を散らすか決定するために、余計に一手間二手間かかるはず。
◆ ③ 累積値で丸める
以下のように累積値で四捨五入して、、、
10 * (11 / 50) =  2.2 ≒ 2 →  2 - 0 = 2円
10 * (23 / 50) =  4.6 ≒ 5 →  5 - 2 = 3円
10 * (50 / 50) = 10.0 ≒10 → 10 - 5 = 5円
、、2円、3円、5円となる。

■ 比較
3つの方法で得られた値は、以下のように比較できる
元の比率111227計=50
101025計=45
101030計=50
101525計=50
((元-①)/元)^20.0083 0.0278 0.0055 0.0415
((元-②)/元)^20.0083 0.0278 0.0123 0.0484
((元-③)/元)^20.0083 0.0625 0.0055 0.0763
近いのは誤差を許容した ①のやり方だが、合計値が一致しないので除外する。

累積値で丸める ③は比率を構成する数値の並び方で値が変わってくるはずで、この例だけではなんとも言えないが、計算量は少なくてすみそう。

最大値に誤差を割り当てた ②が、元の比率に近い。直感的には、この ②のパターンが、もとの比率を一番よく反映していそう。細かいことを考えると、この手法を採った場合、同じ比率の場合に、どれに端数を割り当てるかという問題が残る。例えば 3円を2人で分けたり、1円を2人で分けたりする場合など。

■ 実装
使用箇所を Java で書いた場合を想像するとこんな感じか
Proration proration = new Proration(new int[] {11, 12, 27});
int[] actual = proration.prorate(10);
int[] expected = {2, 2, 6};
assertArrayEquals(expected, actual);
まあ「関数型言語だったらアレなのになあ」なんてボヤきながら書くような、ベタな Java コーディングになりそうだ。

単に業務ロジックの実装として書くとしたら、普通に必要なコードだけ書けば良いだろうけど、フレームワークとかライブラリにしようとしたら、以下のような事が考慮するポイントになるかもしれない。
  • 複数の型で generic に使えるようにする。
  • 端数割り当てロジックを実行時に指定できるようにする。
  • ②で、端数の割り当て先選択ロジックを実行時に指定できるようにする。

■ 所感
検索したら、すぐに定番的なやり方が見つかるかと思ってたら、そうでもなかった。

そもそも案分を英語でなんて言うのかわからなかったりしたが、prorate と言う単語を見つけてググると、とりあえずこんなのを見つけた。「lossless」な proration アルゴリズムについての質問とその一個の解答が載ってた。

0 件のコメント:

コメントを投稿