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")