sustain · Stage 4

engineering economicsで信頼性と容量へ投資する

直接費だけでなく機会費用、運用時間、信頼性損失、容量制約を同じ入力へ結び、unit economicsと感度分析から投資判断を再現する。

学習時間
320分
難易度
advanced
更新日
2026-07-30
到達証拠
成果物・説明・判断根拠・転用

到達目標

  1. 直接費、機会費用、運用費、信頼性損失から比較可能なtotal costを導ける

    • 機会費用、運用時間、信頼性、容量を含む投資比較
    • 価格精度と入力限界を明示した投資判断
  2. 需要、容量、headroom、constraint breachをunit economicsへ結べる

    • 機会費用、運用時間、信頼性、容量を含む投資比較
    • 安価、低unit cost、容量適合、事業価値の違いを説明する5分発表
  3. 一つの仮定だけを変える感度分析で選択が変わる境界を説明できる

    • 機会費用、運用時間、信頼性、容量を含む投資比較
    • demand growthだけを変えた選択の感度分析

能力の進行

  1. recognize

    請求額、engineering時間、運用時間、reliability loss、capacityを別のcost driverとして識別できる

    証拠: 機会費用、運用時間、信頼性、容量を含む投資比較

  2. explain

    最小direct costが最良の事業判断とは限らない理由をunit economicsで説明できる

    証拠: 安価、低unit cost、容量適合、事業価値の違いを説明する5分発表

  3. apply

    固定入力からtotal cost、unit cost、capacity headroomを再計算できる

    証拠: 機会費用、運用時間、信頼性、容量を含む投資比較

  4. diagnose

    価格精度の誇張、埋没費用、二重計上、需要予測の単一点依存を反証できる

    証拠: 価格精度と入力限界を明示した投資判断

  5. lead

    product、finance、reliability、capacityのownerと感度範囲を合意できる

    証拠: demand growthだけを変えた選択の感度分析

なぜ重要か

engineering投資はcloud invoiceだけでは比較できない。実装に使う時間は他の価値を遅らせるopportunity costになり、手作業はoperations hoursを消費し、障害はreliability costを生み、capacity不足は安価な案を実行不能にする。

FinOps Frameworkのunit economicsはcostをbusiness valueの単位へ結ぶ。AWS Well-ArchitectedとGoogle Cloud Well-Architectedのcost guidanceも、最安serviceの選択ではなく、workload要求、利用量、事業価値、継続的な見直しを扱う。

メンタルモデル

投資候補をcostの一点ではなく、制約を満たしたうえで価値を届けるoptionとして比べる。まず単位と期間を揃え、次にcapacity breachを除外し、残った候補をunit economicsとsensitivityで比較する。

costは精密な一値に見えても仮定の積み重ねである。best case、expected、worst caseを分けるsensitivityが、どの入力で選択が反転するかを示す。過去に支払ったsunk costは将来選択のbenefitへ足さない。

入力からcapacity制約とunit economicsを経て投資判断へ至るchain

注記

図を読む際の補足情報です。

  1. Direct: fixtureで与えた取得・実行費を置く。
  2. Opportunity: engineering hoursと一時間の代替価値を掛ける。
  3. Operations: operations hoursと運用単価を掛ける。
  4. Reliability: 障害確率、時間、時間当たり影響を掛ける。
  5. Capacity: 需要と供給からheadroomとbreachを導く。
  6. Decision: 制約違反を先に、unit costを次に比較する。

投資候補ごとにcost構成とcapacity制約を同じ軸で比較し、どの順で判断するか。

判断マトリクス
項目direct costlesson-defined取得・実行費。provider quoteではない。opportunity costengineering hours × 100/hour。operations costoperations hours × 80/hour。reliability lossfailure probability × incident hours × 10000/hour。total・capacity・unit cost四cost合計、required capacity、served units、headroom、total/served units。demand-growth sensitivitybase demand 800でgrowthだけを変え、constraint breachをunit costより先に評価する。
scale-upsynthetic fixture: capacity 1000、direct 12000、engineering 40h、operations 20h、failure 0.05×8h。1200040×100 = 400020×80 = 16000.05×8×10000 = 400012000 + 40×100 + 20×80 + 0.05×8×10000 = 21600; 21600/800 = 27.00/unitgrowth 0.25でrequired capacity 1000、headroom 0; 0.25超でbreach
automationsynthetic fixture: capacity 1600、direct 16000、engineering 120h、operations 5h、failure 0.01×2h。16000120×100 = 120005×80 = 4000.01×2×10000 = 20016000 + 120×100 + 5×80 + 0.01×2×10000 = 28600; 28600/800 = 35.75/unitgrowth 0.5でrequired capacity 1200、headroom 400、automationを選択

制約違反候補を先に除き、direct・opportunity・operations・reliabilityを合算したtotal costとunit costを比較し、一変数の感度境界を説明できる。

動く例で考える

scale-upとautomationの投資比較

前提
二候補のlesson-defined synthetic fixtureを同じ期間で比較する。金額はproduction forecastでもprovider quoteでもない。
入力
direct cost、engineering hours、operations hours、incident確率と影響、capacity、base demand 800 unitsを与える。
操作
各cost componentとtotal cost、served units当たりunit cost、capacity headroomを導く。transferではdemand growthだけを0から0.5へ変える。
観測
baselineではscale-upのunit costが低い。需要が1.5倍になるとscale-upはrequired capacityを満たさず、同じ候補とcost入力でもautomationが選ばれる。
結論
command successとして表を生成できても、system outcomeとして必要容量とreliability制約を満たすとは限らない。制約と感度を含む選択が必要である。
python3.13 - <<'PY'
import json

HARNESS = "engineering_economics_lab_v1"
BASE_DEMAND = 800
BASELINE_ASSUMPTIONS = {
    "demand_growth": 0.0,
    "engineering_hour_value": 100,
    "operations_hour_cost": 80,
    "loss_per_incident_hour": 10000,
}
TRANSFER_ASSUMPTIONS = {
    "engineering_hour_value": 100,
    "operations_hour_cost": 80,
    "loss_per_incident_hour": 10000,
    "demand_growth": 0.5,
}
INVESTMENT_CANDIDATES = [
    {
        "id": "scale-up",
        "direct_cost": 12000,
        "engineering_hours": 40,
        "operations_hours": 20,
        "failure_probability": 0.05,
        "expected_incident_hours": 8,
        "capacity": 1000,
    },
    {
        "id": "automation",
        "direct_cost": 16000,
        "engineering_hours": 120,
        "operations_hours": 5,
        "failure_probability": 0.01,
        "expected_incident_hours": 2,
        "capacity": 1600,
    },
]
PRICE_CONTEXT = {
    "kind": "synthetic-fixture",
    "claim_current_market_prices": False,
    "limitation": (
        "地域、通貨、税、契約、discount、観測日を含まない"
    ),
}
COMMAND_INPUT = {"exit_code": 0}
ASSUMPTION_FIELDS = {
    "demand_growth",
    "engineering_hour_value",
    "operations_hour_cost",
    "loss_per_incident_hour",
}
CANDIDATE_FIELDS = {
    "id",
    "direct_cost",
    "engineering_hours",
    "operations_hours",
    "failure_probability",
    "expected_incident_hours",
    "capacity",
}

def is_number(value):
    return type(value) in {int, float} and type(value) is not bool

def validate_assumption_transfer(baseline, transferred):
    for assumptions in (baseline, transferred):
        if (
            type(assumptions) is not dict
            or set(assumptions) != ASSUMPTION_FIELDS
            or not all(
                is_number(value) and value >= 0
                for value in assumptions.values()
            )
        ):
            raise AssertionError(
                "economics-transfer-invariant: invalid assumption schema"
            )
    changed = [
        field
        for field in sorted(set(baseline) | set(transferred))
        if (
            field not in baseline
            or field not in transferred
            or baseline[field] != transferred[field]
        )
    ]
    if changed != ["demand_growth"]:
        raise AssertionError(
            "economics-transfer-invariant: transfer changed wrong input"
        )
    return changed

def validate_candidate_snapshot(candidates):
    valid = (
        type(candidates) is list
        and bool(candidates)
        and all(
            type(candidate) is dict
            and set(candidate) == CANDIDATE_FIELDS
            and type(candidate["id"]) is str
            and bool(candidate["id"])
            and all(
                is_number(candidate[field])
                and candidate[field] >= 0
                for field in CANDIDATE_FIELDS - {"id"}
            )
            and candidate["capacity"] > 0
            and candidate["failure_probability"] <= 1
            for candidate in candidates
        )
        and len({candidate["id"] for candidate in candidates})
        == len(candidates)
    )
    if not valid:
        raise AssertionError(
            "economics-candidate-invariant: invalid candidate schema"
        )

def fixed_candidate_snapshot():
    # JSON値から別々のfull snapshotを作り、ID一致だけでnested costや
    # capacityのdriftを見逃さない。
    return json.loads(
        json.dumps(INVESTMENT_CANDIDATES, ensure_ascii=False)
    )

def validate_economics_context():
    if (
        not is_number(BASE_DEMAND)
        or BASE_DEMAND <= 0
        or type(PRICE_CONTEXT) is not dict
        or set(PRICE_CONTEXT)
        != {"kind", "claim_current_market_prices", "limitation"}
        or type(PRICE_CONTEXT["kind"]) is not str
        or not PRICE_CONTEXT["kind"]
        or type(PRICE_CONTEXT["claim_current_market_prices"]) is not bool
        or type(PRICE_CONTEXT["limitation"]) is not str
        or not PRICE_CONTEXT["limitation"]
        or type(COMMAND_INPUT) is not dict
        or set(COMMAND_INPUT) != {"exit_code"}
        or type(COMMAND_INPUT["exit_code"]) is not int
    ):
        raise AssertionError(
            "economics-candidate-invariant: invalid context schema"
        )

def compare_investment(candidate, assumptions):
    required_capacity = round(
        BASE_DEMAND * (1 + assumptions["demand_growth"])
    )
    direct_cost = candidate["direct_cost"]
    opportunity_cost = (
        candidate["engineering_hours"]
        * assumptions["engineering_hour_value"]
    )
    operations_cost = (
        candidate["operations_hours"]
        * assumptions["operations_hour_cost"]
    )
    reliability_cost = (
        candidate["failure_probability"]
        * candidate["expected_incident_hours"]
        * assumptions["loss_per_incident_hour"]
    )
    total_cost = direct_cost + opportunity_cost + operations_cost + reliability_cost
    served_units = min(candidate["capacity"], required_capacity)
    capacity_headroom = candidate["capacity"] - required_capacity
    constraint_breaches = int(capacity_headroom < 0)
    return {
        "id": candidate["id"],
        "direct_cost": direct_cost,
        "opportunity_cost": opportunity_cost,
        "operations_cost": operations_cost,
        "reliability_cost": reliability_cost,
        "total_cost": total_cost,
        "served_units": served_units,
        "unit_cost": total_cost / served_units,
        "operations_hours": candidate["operations_hours"],
        "capacity": candidate["capacity"],
        "required_capacity": required_capacity,
        "capacity_headroom": capacity_headroom,
        "constraint_breaches": constraint_breaches,
    }

def compare_all(candidates, assumptions):
    return [
        compare_investment(candidate, assumptions)
        for candidate in candidates
    ]

def select_investment(comparisons):
    return min(
        comparisons,
        key=lambda option: (
            option["constraint_breaches"],
            option["unit_cost"],
        ),
    )["id"]

def main():
    validate_economics_context()
    changed_fields = validate_assumption_transfer(
        BASELINE_ASSUMPTIONS,
        TRANSFER_ASSUMPTIONS,
    )
    validate_candidate_snapshot(INVESTMENT_CANDIDATES)
    baseline_assumptions = json.loads(
        json.dumps(BASELINE_ASSUMPTIONS, ensure_ascii=False)
    )
    transferred_assumptions = json.loads(
        json.dumps(TRANSFER_ASSUMPTIONS, ensure_ascii=False)
    )
    baseline_candidates = fixed_candidate_snapshot()
    transferred_candidates = fixed_candidate_snapshot()
    validate_candidate_snapshot(baseline_candidates)
    validate_candidate_snapshot(transferred_candidates)
    baseline = compare_all(
        baseline_candidates,
        baseline_assumptions,
    )
    if baseline_candidates != transferred_candidates:
        raise AssertionError(
            "economics-candidate-invariant: candidate snapshots drifted"
        )
    transferred = compare_all(
        transferred_candidates,
        transferred_assumptions,
    )
    for option in baseline + transferred:
        component_sum = (
            option["direct_cost"]
            + option["opportunity_cost"]
            + option["operations_cost"]
            + option["reliability_cost"]
        )
        assert (
            option["total_cost"] == component_sum
        ), "economics-causal-invariant"
        assert (
            option["unit_cost"]
            == option["total_cost"] / option["served_units"]
        ), "economics-unit-invariant"
        assert (
            option["capacity_headroom"]
            == option["capacity"] - option["required_capacity"]
        ), "economics-capacity-invariant"

    baseline_selected = select_investment(baseline)
    transferred_selected = select_investment(transferred)
    same_candidates = baseline_candidates == transferred_candidates
    sensitivity_recomputed = (
        baseline[0]["required_capacity"]
        != transferred[0]["required_capacity"]
    )
    system_outcome = all(
        option["constraint_breaches"] == 0
        for option in baseline
        if option["id"] == baseline_selected
    )
    assert baseline_selected == "scale-up", "economics-baseline-invariant"
    assert transferred_selected == "automation", "economics-transfer-invariant"

    report = {
        "harness": HARNESS,
        "investment_comparison": baseline,
        "decision": {
            "selected_option": baseline_selected,
            "selection_order": [
                "constraint_breaches",
                "unit_cost",
            ],
        },
        "sensitivity_comparison": transferred,
        "demand_growth_transfer": {
            "changed_assumption": "demand-growth",
            "changed_fields": changed_fields,
            "baseline_assumptions": baseline_assumptions,
            "transferred_assumptions": transferred_assumptions,
            "same_investment_candidates": same_candidates,
            "baseline_candidate_snapshot": baseline_candidates,
            "transferred_candidate_snapshot": transferred_candidates,
            "baseline_selected_option": baseline_selected,
            "transferred_selected_option": transferred_selected,
            "baseline_demand_growth": baseline_assumptions[
                "demand_growth"
            ],
            "transferred_demand_growth": transferred_assumptions[
                "demand_growth"
            ],
            "sensitivity_recomputed": sensitivity_recomputed,
        },
        "price_claim": {
            "current_price_claimed": PRICE_CONTEXT[
                "claim_current_market_prices"
            ],
            "fixture_only": PRICE_CONTEXT["kind"]
            == "synthetic-fixture",
            "accuracy_limitation": PRICE_CONTEXT["limitation"],
        },
        "fixture_metadata": {
            "kind": "synthetic",
            "provenance": "lesson-defined engineering economics fixture",
            "limitations": PRICE_CONTEXT["limitation"],
            "synthetic_or_observed_explicit": True,
        },
        "runtime_bound": {
            "records": (
                len(INVESTMENT_CANDIDATES)
                * len([baseline_assumptions, transferred_assumptions])
            ),
            "subprocesses": 0,
            "maximum_iterations": len(baseline_candidates),
        },
        "external_network_used": False,
        "command_success_distinction": {
            "command_completed": COMMAND_INPUT["exit_code"] == 0,
            "system_outcome_checked": system_outcome,
            "command_success_equals_system_outcome": (
                COMMAND_INPUT["exit_code"] == 0
                and not system_outcome
            ),
            "outcome_evidence": {
                "selected": baseline_selected,
                "constraint_breaches": [
                    option["constraint_breaches"]
                    for option in baseline
                    if option["id"] == baseline_selected
                ],
            },
        },
        "mastery_evidence": {
            "lab_steps": [
                {
                    "step": index,
                    "evidence": evidence,
                }
                for index, evidence in enumerate(
                    [
                        PRICE_CONTEXT,
                        baseline_candidates,
                        [
                            option["capacity_headroom"]
                            for option in baseline
                        ],
                        baseline_selected,
                        transferred_selected,
                    ],
                    start=1,
                )
            ],
            "assessments": [
                {
                    "assessment": 1,
                    "evidence": [
                        option["total_cost"]
                        for option in baseline
                    ],
                },
                {
                    "assessment": 2,
                    "evidence": PRICE_CONTEXT["limitation"],
                },
            ],
            "rubric_dimensions": [
                "technical-correctness",
                "judgment",
                "evidence",
                "communication",
            ],
            "transfer": {
                "task": (
                    "demand growthだけを変え、同じ投資候補の"
                    "unit economicsと選択を感度分析で再評価する"
                ),
                "changed_assumption": "demand-growth",
                "evidence": {
                    "baseline": baseline_selected,
                    "transferred": transferred_selected,
                },
            },
        },
    }
    print(json.dumps(report, ensure_ascii=False, sort_keys=True))

try:
    main()
except AssertionError as error:
    raise SystemExit(str(error)) from None
PY
需要成長で変わる投資候補のfeasibility比較

注記

図を読む際の補足情報です。

  1. 金額とcapacityはlesson-defined synthetic fixtureであり、provider quoteや予測精度を主張しない。
  2. 候補はまずconstraintを満たすかで絞り、その後にunit costと負の帰結を比較する。
  3. 需要成長以外の仮定は固定し、一変数の感度として読む。

同じ合成fixtureで需要成長だけを変えたとき、costより先にcapacity constraintが判断をどう変えるか。

選択肢の比較
項目baseline需要800成長前のrequired capacityとunit costを比較する。需要25%増・1000scale-upのheadroomが0になる境界。需要50%増・1200capacity constraint違反を先に判定する。判断feasibility、cost、再評価条件をまとめる。
scale-upcapacity 1000、total cost 21600の合成候補。headroom 200; 27.00/unit; feasibleheadroom 0; constraint境界200不足; constraint違反baselineは低cost。25%超で再評価
automationcapacity 1600、total cost 28600の合成候補。headroom 800; 35.75/unit; feasibleheadroom 600; feasibleheadroom 400; feasible50%増では唯一feasible。高い初期costも併記

baselineでは低costのscale-upを選べても、需要50%増ではcapacity constraint違反によりautomationだけがfeasibleになる境界を説明できる。

トレードオフと失敗モード

制約と感度から投資行動を選ぶdecision table
観測 判断 次の行動 残す証拠
capacity breachあり その需要条件では選ばない 需要抑制、分割、別候補を比較 required capacityとheadroom
制約内でunit costが低い 小さく採用 実績を観測して入力を校正 served unit、cost、reliability
仮定で選択が反転 段階投資 反転入力を先に測定 sensitivity rangeとowner
価格時点が不明 予算断定を停止 official quoteと契約を更新 地域、通貨、期間、観測日
  • 誤診: direct costが最小なので最も経済的である。反証: opportunity cost、operations hours、reliability loss、capacity breachを同じ期間と単位で加える。
  • 誤診: 小数点まで計算したのでprice accuracyが高い。反証: 金額の出自、観測日、地域、契約、uncertaintyを示し、入力範囲でsensitivityを再実行する。

知識チェック

  1. engineering hoursをopportunity costへ変換するときの二重計上をどう防ぐか。
  2. unit costが低くてもcapacity不足の候補を選べない理由を説明せよ。
  3. reliabilityを金額へ換算できない場合、どの制約と定量証拠を残すか。
  4. command successとsystem outcomeを分ける投資後の観測を三つ挙げよ。

出典と次の学習

FinOps FrameworkのUnit EconomicsとArchitecting and Workload Placement、AWS Well-Architected Cost Optimization Pillar、Google Cloud Well-Architected cost optimization pillarを、事業価値、workload制約、継続改善の一次guidanceとして使った。

次はproviderのcurrent pricing、組織のengineering cost、incident実績、capacity観測を同じschemaへ入れ、fixtureとの差とprice accuracyの限界を明記した投資reviewを行う。

実践ラボ

二つのcapacity投資をunit economicsで比較する

提出成果物: 機会費用、運用時間、信頼性、容量を含む投資比較

  1. 価格ではなくlesson-defined synthetic inputsであることと限界を記録する
  2. 各候補のdirect cost、engineering hours、operations hours、reliability inputsを固定する
  3. 需要からrequired capacity、headroom、constraint breachを導く
  4. total costとserved unitあたりcostを計算してbaselineを選ぶ
  5. demand growthだけを変え同じ候補を感度分析して選択を再評価する

説明して理解を確かめる

5分で、FinOps Frameworkのunit economics、AWSとGoogle Cloudのcost guidanceを価格表としてではなく事業価値とworkload制約を結ぶ判断枠として使う理由を説明する。

アセスメント

  1. 問い: direct costが最小の候補に運用時間とincident riskが集中する。比較式をどう直すか。

    期待する証拠: opportunity cost、operations cost、reliability cost、二重計上防止、入力owner

  2. 問い: provider価格を確認せずにこのfixtureからproduction予算を断定できるか。

    期待する証拠: synthetic fixture、currencyと期間、地域と契約、価格更新日、sensitivity range

別問題へ転用する

demand growthだけを変え、同じ投資候補のunit economicsと選択を感度分析で再評価する

復習スケジュール

  1. 1日後

    opportunity costをdirect costと分ける理由は何か

  2. 7日後

    capacity constraintをunit costより先に評価する条件は何か

  3. 30日後

    価格精度を誇張せず投資判断を更新するには何を記録するか

  4. 90日後

    opportunity costをdirect costと分ける理由は何か

評価ルーブリック

4段階の評価基準
観点未達発展途上熟達卓越
technical-correctnessdirect costだけを比較する複数costはあるが単位、期間、capacity constraintが一致しない全cost driver、served units、headroom、constraint breachを同じ入力から導く二重計上、uncertainty、option value、需要分布でも式の境界を維持する
judgment最安値を自動的に選ぶ信頼性を挙げるが事業影響へ換算しないcost、reliability、capacity、可逆性、時間軸から選ぶ投資を小さなoptionへ分け、観測に応じて中止または拡大できる
evidence金額だけを示し入力出自がない計算表はあるが価格時点とsynthetic限界がない入力、式、単位、結果、制約、感度、限界を再実行可能に残す実績との差を継続校正し、仮定変更が選択へ伝播する証拠を持つ
communication安いまたは高いとだけ報告するtotal costはあるが事業単位とtrade-offが読めないどの仮定、cost driver、制約が選択を決めたか説明するengineering、finance、product、operationsが同じ感度表で異議を検証できる

出典

以下の外部資料は利用者が選択したときだけ開きます。