見積金額・工数と単価のテクニック

このブログは技術系ブログだったことを思い出して、たまには釣り以外のことを書こうと思い立ちました。
SEだったらたまにはあるかもしれないお話。
見積金額と工数を顧客に提示した後で、職種ごとの工数を求められちゃうケース。(そんなにない?)
ちゃんと決めていればよかったのに、勢いよく出しちゃったよみたいなこと、あるんじゃないでしょうか。

見積金額xx円、工数xx人月で、PM、SE、PGとかの工数はそれぞれいくつなのみたいな。
後から『それっぽい』数字を入れるのは意外と大変。

そんな問題はソルバーで解きましょう。
フリーで使えるGLPK(GNU Linear Programming Kit)があれば十分です。

例として、10人月で700万円、PM、SE、PGの人月単価はそれぞれ100万円、80万円、60万円としましょう。
この問題のモデルファイルを作る必要があるので、次みたいな感じで作りましょう。
var x1 integer; # PM
var x2 integer; # SE
var x3 integer; # PG
minimize ERROR: 700000000 - 1000000 * x1 - 800000 * x2 - 600000 * x3;
s.t. KOUSU_CONSTRAINT: x1 + x2 + x3 = 1000;
s.t. MONEY_CONSTRAINT: 700000000 - 1000000 * x1 - 800000 * x2 - 600000 * x3 >= 0;
end;
最初の3行で工数用の変数を定義します。それぞれ整数値が入るようにします。
4行目は目的関数なのですが、とりあえず誤差最小を目指しましょう。
700万円の部分はx1~x3が100倍なので、100倍にしています。
2次式をとって最小化するのがよいのですが、GLPKは2次計画問題は解けないので、目的関数と同じ式で0以上で縛ります。

KOUSU_CONSTRAINTで職種ごとの工数の合計が10人月になるように縛っています。
100倍の数値になっているのは、職種ごとの工数を0.01刻みで求めたいので、100倍にしています。

さて、実行してみましょう。
# glpsol -m <モデルファイル名> -o <出力ファイル名>
出力されたファイルを見ると、次のようになっていると思います。
Problem:    kousu
Rows:       3
Columns:    3 (3 integer, 0 binary)
Non-zeros:  9
Status:     INTEGER OPTIMAL
Objective:  ERROR = 1000 (MINimum)
No.   Row name        Activity     Lower bound   Upper bound
------ ------------    ------------- ------------- -------------
1 ERROR                       0
2 KOUSU_CONSTRAINT
1000          1000             =
3 MONEY_CONSTRAINT
-7e+08        -7e+08             =
No. Column name       Activity     Lower bound   Upper bound
------ ------------    ------------- ------------- -------------
1 x1           *            250
2 x2           *              0
3 x3           *            750
Integer feasibility conditions:
KKT.PE: max.abs.err = 0.00e+00 on row 0
max.rel.err = 0.00e+00 on row 0
High quality
KKT.PB: max.abs.err = 0.00e+00 on row 0
max.rel.err = 0.00e+00 on row 0
High quality
End of output
x1のActivityが250となっています。これはx1(PM)が2.5人月であることを示しています。
SEは0人月でPGが7.5人月です。流石にPMとPGだけというのもあるので、
もうちょっといい感じにするために、先ほどのモデルファイルに次の行を追加して再実行しましょう。
s.t. X1_CONSTRAINT_LOWER: x1 >= 50;
s.t. X2_CONSTRAINT_LOWER: x2 >= 150;
s.t. X3_CONSTRAINT_LOWER: x3 >= 100;
s.t. X1_CONSTRAINT_UPPER: x1 <= 200;
s.t. X2_CONSTRAINT_UPPER: x2 <= 500;
s.t. X3_CONSTRAINT_UPPER: x3 <= 800;
出力された結果をみると、PMが1.75人月、SEが1.5人月、PGが6.75人月となっています。
Problem:    kousu
Rows:       9
Columns:    3 (3 integer, 0 binary)
Non-zeros:  15
Status:     INTEGER OPTIMAL
Objective:  ERROR = 1000 (MINimum)
No.   Row name        Activity     Lower bound   Upper bound
------ ------------    ------------- ------------- -------------
1 ERROR                       0
2 KOUSU_CONSTRAINT
1000          1000             =
3 MONEY_CONSTRAINT
-7e+08        -7e+08             =
4 X1_CONSTRAINT_LOWER
175            50
5 X2_CONSTRAINT_LOWER
150           150
6 X3_CONSTRAINT_LOWER
675           100
7 X1_CONSTRAINT_UPPER
175                         200
8 X2_CONSTRAINT_UPPER
150                         500
9 X3_CONSTRAINT_UPPER
675                         800
No. Column name       Activity     Lower bound   Upper bound
------ ------------    ------------- ------------- -------------
1 x1           *            175
2 x2           *            150
3 x3           *            675
Integer feasibility conditions:
KKT.PE: max.abs.err = 0.00e+00 on row 0
max.rel.err = 0.00e+00 on row 0
High quality
KKT.PB: max.abs.err = 0.00e+00 on row 0
max.rel.err = 0.00e+00 on row 0
High quality
End of output
先ほどよりはましになりました。x1~x3の上限下限を適当に設定すれば、それっぽい数字が簡単にでます。
めでたしめでたし。

素晴らしいツールのクズみたいな使い方なので、見積時にはちゃんと考えておきましょう。