06_estimation_calculator.py

Download
python 310 lines 11.0 KB
  1"""
  2Software Estimation Calculator
  3
  4Demonstrates three widely-used software estimation techniques:
  5
  61. COCOMO II Basic Model
  7   Effort (person-months) = a * (KLOC)^b
  8   Where a and b are mode-dependent constants:
  9     - Organic:      a=2.4, b=1.05  (small, familiar projects)
 10     - Semi-detached: a=3.0, b=1.12  (medium complexity)
 11     - Embedded:     a=3.6, b=1.20  (highly constrained)
 12
 132. Three-Point Estimation (PERT)
 14   Expected = (Optimistic + 4 * Most_Likely + Pessimistic) / 6
 15   Std_Dev  = (Pessimistic - Optimistic) / 6
 16
 173. Story Point Velocity Calculator
 18   Estimates remaining effort based on historical team velocity.
 19
 204. Function Point Counting (simplified)
 21   Counts inputs, outputs, queries, files, and interfaces
 22   to produce an unadjusted function point total.
 23
 24Run:
 25    python 06_estimation_calculator.py
 26"""
 27
 28import math
 29from dataclasses import dataclass
 30
 31
 32# ---------------------------------------------------------------------------
 33# 1. COCOMO II Basic Model
 34# ---------------------------------------------------------------------------
 35
 36COCOMO_MODES = {
 37    "organic":       (2.4, 1.05),
 38    "semi-detached": (3.0, 1.12),
 39    "embedded":      (3.6, 1.20),
 40}
 41
 42
 43def cocomo_basic(kloc: float, mode: str = "semi-detached") -> dict[str, float]:
 44    """
 45    Estimate effort and duration using COCOMO II Basic model.
 46
 47    Args:
 48        kloc: Thousands of lines of code (estimated)
 49        mode: 'organic', 'semi-detached', or 'embedded'
 50
 51    Returns:
 52        Dictionary with effort (person-months), duration (months),
 53        team size, and productivity.
 54    """
 55    if mode not in COCOMO_MODES:
 56        raise ValueError(f"Mode must be one of {list(COCOMO_MODES.keys())}")
 57    if kloc <= 0:
 58        raise ValueError("KLOC must be positive")
 59
 60    a, b = COCOMO_MODES[mode]
 61    effort_pm = a * (kloc ** b)          # person-months
 62    duration_m = 2.5 * (effort_pm ** 0.38)  # calendar months
 63    team_size = effort_pm / duration_m
 64    productivity = (kloc * 1000) / effort_pm  # LOC per person-month
 65
 66    return {
 67        "effort_person_months": round(effort_pm, 2),
 68        "duration_months": round(duration_m, 2),
 69        "team_size": round(team_size, 1),
 70        "productivity_loc_pm": round(productivity),
 71    }
 72
 73
 74# ---------------------------------------------------------------------------
 75# 2. Three-Point Estimation (PERT)
 76# ---------------------------------------------------------------------------
 77
 78@dataclass
 79class ThreePointTask:
 80    name: str
 81    optimistic: float    # Best case (days)
 82    most_likely: float   # Most probable (days)
 83    pessimistic: float   # Worst case (days)
 84
 85    @property
 86    def expected(self) -> float:
 87        return (self.optimistic + 4 * self.most_likely + self.pessimistic) / 6
 88
 89    @property
 90    def std_dev(self) -> float:
 91        return (self.pessimistic - self.optimistic) / 6
 92
 93    @property
 94    def variance(self) -> float:
 95        return self.std_dev ** 2
 96
 97
 98def pert_project(tasks: list[ThreePointTask], confidence: float = 0.90) -> dict:
 99    """
100    Aggregate PERT estimates across tasks for a project-level estimate.
101
102    Uses the Central Limit Theorem: the sum of independent normal distributions
103    is also normal, with mean = sum of means, variance = sum of variances.
104
105    Args:
106        tasks: List of ThreePointTask instances.
107        confidence: Desired confidence level (0.80, 0.90, or 0.95).
108
109    Returns:
110        Dictionary with expected total, std dev, and confidence interval.
111    """
112    z_scores = {0.80: 1.28, 0.90: 1.645, 0.95: 1.96}
113    z = z_scores.get(confidence, 1.645)
114
115    total_expected = sum(t.expected for t in tasks)
116    total_variance = sum(t.variance for t in tasks)
117    total_std_dev = math.sqrt(total_variance)
118
119    return {
120        "expected_days": round(total_expected, 1),
121        "std_dev_days": round(total_std_dev, 1),
122        "confidence_level": f"{int(confidence * 100)}%",
123        "lower_bound": round(total_expected - z * total_std_dev, 1),
124        "upper_bound": round(total_expected + z * total_std_dev, 1),
125        "task_count": len(tasks),
126    }
127
128
129# ---------------------------------------------------------------------------
130# 3. Story Point Velocity Calculator
131# ---------------------------------------------------------------------------
132
133def velocity_estimate(
134    remaining_points: int,
135    sprint_velocity: list[int],
136    sprint_length_weeks: int = 2,
137) -> dict:
138    """
139    Estimate time to complete remaining story points using historical velocity.
140
141    Args:
142        remaining_points: Total story points left in the backlog.
143        sprint_velocity: Points completed per sprint in recent history.
144        sprint_length_weeks: Length of one sprint in weeks.
145
146    Returns:
147        Dictionary with average velocity, sprints needed, and calendar weeks.
148    """
149    if not sprint_velocity:
150        raise ValueError("Sprint velocity history must not be empty")
151
152    avg_velocity = sum(sprint_velocity) / len(sprint_velocity)
153    min_velocity = min(sprint_velocity)
154    max_velocity = max(sprint_velocity)
155
156    sprints_avg = math.ceil(remaining_points / avg_velocity)
157    sprints_optimistic = math.ceil(remaining_points / max_velocity)
158    sprints_pessimistic = math.ceil(remaining_points / min_velocity)
159
160    return {
161        "average_velocity": round(avg_velocity, 1),
162        "sprints_needed_avg": sprints_avg,
163        "sprints_needed_optimistic": sprints_optimistic,
164        "sprints_needed_pessimistic": sprints_pessimistic,
165        "weeks_avg": sprints_avg * sprint_length_weeks,
166        "weeks_optimistic": sprints_optimistic * sprint_length_weeks,
167        "weeks_pessimistic": sprints_pessimistic * sprint_length_weeks,
168    }
169
170
171# ---------------------------------------------------------------------------
172# 4. Function Point Counting (simplified unadjusted)
173# ---------------------------------------------------------------------------
174
175# Complexity weights per IFPUG standard (simplified to average weights)
176FP_WEIGHTS = {
177    "external_inputs":   4,   # screens, forms
178    "external_outputs":  5,   # reports, screens with derived data
179    "external_queries":  4,   # online queries with no derived output
180    "internal_files":    10,  # logical internal data groups
181    "external_interfaces": 7, # data shared with other systems
182}
183
184
185def count_function_points(components: dict[str, int]) -> dict:
186    """
187    Compute Unadjusted Function Points (UFP) from component counts.
188
189    Args:
190        components: Dict mapping component type to count.
191                    Keys: external_inputs, external_outputs, external_queries,
192                          internal_files, external_interfaces
193
194    Returns:
195        Dictionary with individual FP contributions and total UFP.
196    """
197    breakdown = {}
198    total = 0
199    for key, weight in FP_WEIGHTS.items():
200        count = components.get(key, 0)
201        contribution = count * weight
202        breakdown[key] = {"count": count, "weight": weight, "fp": contribution}
203        total += contribution
204
205    # Rough LOC conversion (language-dependent; Python ~50 LOC/FP)
206    estimated_kloc_python = (total * 50) / 1000
207
208    return {
209        "breakdown": breakdown,
210        "unadjusted_fp": total,
211        "estimated_kloc_python": round(estimated_kloc_python, 2),
212    }
213
214
215# ---------------------------------------------------------------------------
216# Demo
217# ---------------------------------------------------------------------------
218
219def print_section(title: str) -> None:
220    print(f"\n{'=' * 60}")
221    print(f"  {title}")
222    print("=" * 60)
223
224
225def demo_cocomo() -> None:
226    print_section("1. COCOMO II Basic Estimation")
227    project_kloc = 15.0
228    print(f"\n  Project size: {project_kloc} KLOC\n")
229    for mode in COCOMO_MODES:
230        result = cocomo_basic(project_kloc, mode)
231        print(f"  Mode: {mode.capitalize()}")
232        print(f"    Effort:       {result['effort_person_months']} person-months")
233        print(f"    Duration:     {result['duration_months']} months")
234        print(f"    Team size:    {result['team_size']} people")
235        print(f"    Productivity: {result['productivity_loc_pm']} LOC/person-month")
236        print()
237
238
239def demo_pert() -> None:
240    print_section("2. Three-Point Estimation (PERT)")
241    tasks = [
242        ThreePointTask("Requirements analysis",   3,  5, 10),
243        ThreePointTask("System design",           5,  8, 15),
244        ThreePointTask("Backend development",    20, 30, 50),
245        ThreePointTask("Frontend development",   15, 22, 40),
246        ThreePointTask("Integration & testing",   8, 12, 20),
247        ThreePointTask("Deployment & handover",   2,  4,  8),
248    ]
249
250    print(f"\n  {'Task':<30} {'Opt':>5} {'ML':>5} {'Pes':>5} {'Exp':>6} {'SD':>5}")
251    print("  " + "-" * 58)
252    for t in tasks:
253        print(f"  {t.name:<30} {t.optimistic:>5.0f} {t.most_likely:>5.0f} "
254              f"{t.pessimistic:>5.0f} {t.expected:>6.1f} {t.std_dev:>5.1f}")
255
256    result = pert_project(tasks, confidence=0.90)
257    print(f"\n  Project Total ({result['confidence_level']} confidence):")
258    print(f"    Expected duration: {result['expected_days']} days")
259    print(f"    Std deviation:     {result['std_dev_days']} days")
260    print(f"    90% range:         {result['lower_bound']}{result['upper_bound']} days")
261
262
263def demo_velocity() -> None:
264    print_section("3. Story Point Velocity Calculator")
265    history = [38, 42, 35, 40, 44, 39]
266    remaining = 320
267    result = velocity_estimate(remaining, history, sprint_length_weeks=2)
268
269    print(f"\n  Remaining backlog:  {remaining} story points")
270    print(f"  Sprint history:     {history}")
271    print(f"  Average velocity:   {result['average_velocity']} pts/sprint\n")
272    print(f"  Scenario        Sprints  Weeks")
273    print(f"  {'─'*35}")
274    print(f"  Average         {result['sprints_needed_avg']:>7}  {result['weeks_avg']:>5}")
275    print(f"  Optimistic      {result['sprints_needed_optimistic']:>7}  {result['weeks_optimistic']:>5}")
276    print(f"  Pessimistic     {result['sprints_needed_pessimistic']:>7}  {result['weeks_pessimistic']:>5}")
277
278
279def demo_function_points() -> None:
280    print_section("4. Function Point Counting")
281    components = {
282        "external_inputs":    12,  # data entry forms
283        "external_outputs":    8,  # reports
284        "external_queries":    6,  # search/query screens
285        "internal_files":      5,  # main data entities
286        "external_interfaces": 3,  # third-party API integrations
287    }
288    result = count_function_points(components)
289
290    print(f"\n  {'Component':<25} {'Count':>6} {'Weight':>7} {'FP':>6}")
291    print("  " + "-" * 46)
292    for key, data in result["breakdown"].items():
293        label = key.replace("_", " ").title()
294        print(f"  {label:<25} {data['count']:>6}   x {data['weight']:>3}  = {data['fp']:>4}")
295    print("  " + "-" * 46)
296    print(f"  {'Unadjusted Function Points':<25} {'':>6} {'':>7} {result['unadjusted_fp']:>6}")
297    print(f"\n  Estimated size (Python, ~50 LOC/FP): {result['estimated_kloc_python']} KLOC")
298
299
300if __name__ == "__main__":
301    print("\nSoftware Estimation Calculator")
302    print("Comparing COCOMO II, PERT, Velocity, and Function Points\n")
303    demo_cocomo()
304    demo_pert()
305    demo_velocity()
306    demo_function_points()
307    print("\n" + "=" * 60)
308    print("  Done. Use these estimates as starting points, not commitments.")
309    print("=" * 60 + "\n")