{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Monte Carlo simulation for project risk assessment"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"This notebook is an element of the free [risk-engineering.org courseware](https://risk-engineering.org/). It can be distributed under the terms of the [Creative Commons Attribution-ShareAlike licence](https://creativecommons.org/licenses/by-sa/4.0/).\n",
"\n",
"Author: Eric Marsden \n",
"\n",
"---\n",
"\n",
"This notebook contains an introduction to use of Python and the NumPy library for Monte Carlo simulation applied to a simple project risk problem. The [associated lecture slides](https://risk-engineering.org/monte-carlo-methods/) provide an introduction to the use of stochastic simulation methods."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Problem statement"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"A construction project involves three tasks:\n",
"\n",
"- Task 1 is likely to take three days (70% probability), but it might also be completed in\n",
" two days (10% probability) or four days (20% probability)\n",
"\n",
"- Task 2 has a 60% probability of taking six days to finish, a 20% probability each of being completed in five days or eight days\n",
"\n",
"- Task 3 has an 80% probability of being completed in four days, 5% probability of being completed in three days and a 15% \n",
" probability of being completed in five days.\n",
" \n",
"Your task is to provide information to the project manager concerning the expected completion time of the project and possible delays."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Basic approach using best and worst case"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"A basic approach to project risk would calculate best case and worst case project completion time. Our example is very simple so it’s easy to make this estimate by hand. We illustrate how to use interval arithmetic to resolve this kind of problem in more complex scenarios. We use the [mpmath Python library](http://mpmath.org/) for floating point arithmetic with arbitrary precision, which provides support for interval arithmetic. You may need to install this library, for instance with\n",
"\n",
"> pip install mpmath\n",
"\n",
"We assume that each task is dependent on the task before it, meaning that the three tasks must be executed in sequence. The total duration of the project is simply the sum of the individual task durations."
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"mpi('10.0', '17.0')"
]
},
"execution_count": 1,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"from mpmath import iv\n",
"\n",
"task1 = iv.mpf([2, 4])\n",
"task2 = iv.mpf([5, 8])\n",
"task3 = iv.mpf([3, 5])\n",
"task1 + task2 + task3"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"So the best case completion time is 10 days, and the worst case is 17 days. That’s a very big range of uncertainty in our project risk assessment!"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Monte Carlo stochastic simulation"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Using a Monte Carlo stochastic simulation method, we will estimate the probability distribution of completion time, providing much more information for decision-making on project risk than only best and worst cases."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"We start by defining a function that simulates the completion time of task 1. The functions use pseudorandom numbers generated from a uniform distribution. "
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [],
"source": [
"import numpy\n",
"\n",
"def task1_days() -> int:\n",
" u = numpy.random.uniform()\n",
" if u < 0.7: return 3\n",
" if u < 0.8: return 2\n",
" return 4"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Let’s check that the function returns a plausible value if called once:"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"4"
]
},
"execution_count": 3,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"task1_days()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Then check that the distribution of a large number of calls looks plausible:"
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {},
"outputs": [
{
"data": {
"image/svg+xml": [
"\n",
"\n",
"\n",
"\n"
],
"text/plain": [
""
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"import matplotlib.pyplot as plt\n",
"%matplotlib inline\n",
"%config InlineBackend.figure_formats=['svg']\n",
"\n",
"N = 1000\n",
"sim = numpy.zeros(N, dtype=int)\n",
"for i in range(N):\n",
" sim[i] = task1_days()\n",
"plt.stem(numpy.bincount(sim), '-.')\n",
"plt.xlabel(u\"Days\")\n",
"plt.margins(0.1)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"We check that this result is plausible: the most likely duration is three days, and a duration of four days roughly two times more likely than a duration of 2 days. OK, this fits our task description."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"We then define similar functions for the duration of tasks 2 and 3."
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {},
"outputs": [],
"source": [
"def task2_days():\n",
" u = numpy.random.uniform()\n",
" if u < 0.6: return 6\n",
" if u < 0.8: return 5\n",
" return 8\n",
"\n",
"def task3_days():\n",
" u = numpy.random.uniform()\n",
" if u < 0.8: return 4\n",
" if u < 0.85: return 3\n",
" return 5"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"We assume that each task is dependent on the task before it, meaning that the three tasks must be executed in sequence. The total duration of the project is simply the sum of the individual task durations. \n",
"\n",
"Note that in real applications, some tasks will most likely be able to be executed in parallel. Furthermore, the number of tasks will be much greater, and can reach several hundred. The method shown here will still work, though a graphical user interface to describe tasks will be very helpful to users. "
]
},
{
"cell_type": "code",
"execution_count": 6,
"metadata": {},
"outputs": [],
"source": [
"def project_duration():\n",
" return task1_days() + task2_days() + task3_days()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"We can now run a large number of simulations of project execution, and from these estimate the worst case, best case and median durations. We can also provide the expected probability distribution of the duration."
]
},
{
"cell_type": "code",
"execution_count": 7,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Worst case: 17 days\n",
"Best case: 10 days\n",
"Median: 13.0 days\n"
]
},
{
"data": {
"image/svg+xml": [
"\n",
"\n",
"\n",
"\n"
],
"text/plain": [
""
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"N = 10000\n",
"sim = numpy.zeros(N, dtype=int)\n",
"for i in range(N):\n",
" sim[i] = project_duration()\n",
"plt.stem(numpy.bincount(sim), '-.')\n",
"plt.title(u\"Total project duration\")\n",
"plt.xlabel(u\"Days\");\n",
"print(u\"Worst case: {} days\".format(sim.max()))\n",
"print(u\"Best case: {} days\".format(sim.min()))\n",
"print(u\"Median: {} days\".format(numpy.median(sim)))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"We can also provide other quantile measures of estimated project duration. For example, to determine the duration that we are 95% confident will not be exceeded, we calculate the 95th percentile."
]
},
{
"cell_type": "code",
"execution_count": 8,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"16.0"
]
},
"execution_count": 8,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"numpy.percentile(sim, 95)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Parallel tasks"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Suppose that tasks 2 and 3 run in parallel, instead of sequentially (one after the other). Their contribution to total project duration is the maximum of the two durations, rather than the sum as previously. We could adjust the code that calculates total project duration as follows: "
]
},
{
"cell_type": "code",
"execution_count": 9,
"metadata": {},
"outputs": [],
"source": [
"def project_duration() -> int:\n",
" return task1_days() + max(task2_days(), task3_days())"
]
},
{
"cell_type": "markdown",
"metadata": {
"collapsed": true
},
"source": [
"## Historical note"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The first major use of stochastic simulation techniques for project risk management was the [PERT method](https://en.wikipedia.org/wiki/Program_evaluation_and_review_technique), developed by the US Navy Special Projects Office in the 1950s to help manage the development of Polaris nuclear submarines. The method incorporated uncertainty in project schedule estimates based on inputs provided by subproject leaders. Rather than ask engineers about the variance of their duration estimates (not a very natural feature of human estimates…), the PERT specialists would ask engineers to provide optimistic, most likely and pessimistic estimates of duration, and would fit a beta probability distribution to these parameters. "
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"anaconda-cloud": {},
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.6.4"
}
},
"nbformat": 4,
"nbformat_minor": 1
}