Suppose we have some function, \(g(x)\), defined over the range \(a \leq x \leq b\) and we want to evaluate the integral: \[ \theta = \int\limits_{a}^{b} g(x) \mathrm{d}x\] Define \(Y\) as follows with \(X \sim U(a,b)\): \[ Y = \left(b-a\right)g(X) \] By definition,
\[ E_{X}\lbrack g(X) \rbrack = \int\limits_{a}^{b} g(x)f_{X}(x)\mathrm{d}x\]
The probability density function for a \(X \sim U(a,b)\) random variable is: \[f_{X}(x) = \begin{cases} \frac{1}{b-a} & a \leq x \leq b\\ 0 & \text{otherwise} \end{cases}\] Therefore, \[ E_{X}\lbrack g(x) \rbrack = \int\limits_{a}^{b} g(x)f_{X}(x)\mathrm{d}x = \int\limits_{a}^{b} g(x)\frac{1}{b-a}\mathrm{d}x \]
Substituting, yields, \[\begin{aligned} E\lbrack Y \rbrack & = E\lbrack \left(b-a\right)g(X) \rbrack = \left(b-a\right)E\lbrack g(X) \rbrack\\ & = \left(b-a\right)\int\limits_{a}^{b} g(x)\frac{1}{b-a}\mathrm{d}x = \int\limits_{a}^{b} g(x)\mathrm{d}x = \theta\end{aligned}\] Therefore, by estimating the expected value of \(Y\), we can estimate the desired integral.
A good estimator for \(E\lbrack Y \rbrack\) is the sample average of observations of \(Y\). Let \(Y_{1}, Y_{2},...Y_{n}\) be a random sample of observations of \(Y\). Let \(X_{i}\) be the \(i^{th}\) observation of \(X\). Substituting each \(X_{i}\) yields the \(i^{th}\) observation of \(Y\),
\[Y_{i} = \left(b-a\right)g(X_{i})\]
Then, the sample average of is:
\[\begin{aligned} \bar{Y}(n) & = \frac{1}{n}\sum\limits_{i=1}^{n} Y_{i} = \left(b-a\right)\frac{1}{n}\sum\limits_{i=1}^{n}\left(b-a\right)g(X_{i})\\ & = \left(b-a\right)\frac{1}{n}\sum\limits_{i=1}^{n}g(X_{i}) = \hat{\theta} \\\end{aligned}\]
where \(X_{i} \sim U(a,b)\).
Suppose that we want to estimate the area under \(f(x) = x^{\frac{1}{2}}\) over the range from \(1\) to \(4\). That is, we want to evaluate the integral:
\[\theta = \int\limits_{1}^{4} x^{\frac{1}{2}}\mathrm{d}x = \dfrac{14}{3}=4.6\bar{6}\] According to the previously presented theory, we need to generate \(X_i \sim U(1,4)\) and then compute \(\bar{Y}\), where \(Y_i = (4-1)\sqrt{X{_i}}= 3\sqrt{X{_i}}\).
fun main() {
val a = 1.0
val b = 4.0
val ucdf = UniformRV(a, b, streamNum = 3)
val stat = Statistic("Area Estimator")
val n = 100 // sample size
for (i in 1..n) {
val x = ucdf.value
val gx = sqrt(x)
val y = (b - a) * gx
stat.collect(y)
}
System.out.printf("True Area = %10.3f %n", 14.0 / 3.0)
System.out.printf("Area estimate = %10.3f %n", stat.average)
println("Confidence Interval")
println(stat.confidenceInterval)
}True Area = 4.667
Area estimate = 4.781
Confidence Interval
[4.608646560421988, 4.952515649272401]
Notice that the estimate is not equal to the true area. This difference is due to sampling error. We can determine the sample size need to bound the error.
The confidence interval for a point estimator can serve as the basis for determining how many observations to have in the sample. The quantity:
\[ h = t_{1-(\alpha/2), n-1} \dfrac{s}{\sqrt{n}} \]
is called the half-width of the confidence interval. You can place a bound, \(\epsilon\), on the half-width by picking a sample size that satisfies:
\[ h = t_{1-(\alpha/2), n-1} \dfrac{s}{\sqrt{n}} \leq \epsilon \]
We call \(\epsilon\) the margin of error for the bound and \(t_{p, \nu}\) is the \(100p\) percentage point of the Student t-distribution with \(\nu\) degrees of freedom.
The required sample size can be approximated using the normal distribution. Solving for \(n\) yields:
\[n \geq \left(\dfrac{t_{1-(\alpha/2), n-1} \; s}{\epsilon}\right)^2\]
As \(n\) gets large, \(t_{1-(\alpha/2), n-1}\) converges to the \(100(1-(\alpha/2))\) percentage point of the standard normal distribution \(z_{1-(\alpha/2)}\). This yields the following approximation:
\[ n \geq \left(\dfrac{z_{1-(\alpha/2)} s}{\epsilon}\right)^2 \]
Statistic Class to Compute the Sample SizeThe normal approximation is implemented in the function, estimateSampleSize of the Statisitic class’s companion object.
/** Estimate the sample size based on a normal approximation
* @param desiredHW the desired half-width (must be bigger than 0)
* @param stdDev the standard deviation (must be bigger than or equal to 0)
* @param level the confidence level (must be between 0 and 1)
* @return the estimated sample size
*/
fun estimateSampleSize(desiredHW: Double, stdDev: Double, level: Double): Long {
require(desiredHW > 0.0) { "The desired half-width must be > 0" }
require(stdDev >= 0.0) { "The desired std. dev. must be >= 0" }
require(!(level <= 0.0 || level >= 1.0)) { "Confidence Level must be (0,1)" }
val a = 1.0 - level
val a2 = a / 2.0
val z = Normal.stdNormalInvCDF(1.0 - a2)
val m = z * stdDev / desiredHW * (z * stdDev / desiredHW)
return (m + .5).roundToLong()
}Suppose we are required to estimate the output from a simulation so that we are 99% confidence that we are within \(\pm 0.1\) of the true population mean. After taking a pilot sample of size \(n=10\), we have estimated \(s=6\). What is the required sample size?
For a 99% confidence interval, we have \(\alpha = 0.01\) and \(\alpha/2 = 0.005\). Thus, \(z_{1-0.005} = z_{0.995} = 2.5758293064439264\). Because the margin of error, \(\epsilon\) is \(0.1\), we have that,
\[ n \geq \left(\dfrac{z_{1-(\alpha/2)}\; s}{\epsilon}\right)^2 = \left(\dfrac{2.5758293064439264 \times 6}{0.1}\right)^2 = 23885.63 \approx 23886 \]
The following KSL code will easily compute the sample size.
fun main() {
val desiredHW = 0.1
val s0 = 6.0
val level = 0.99
val n = Statistic.estimateSampleSize(
desiredHW = desiredHW,
stdDev = s0,
level = level
)
println("Sample Size Determination")
println("desiredHW = $desiredHW")
println("stdDev = $s0")
println("Level = $level")
println("recommended sample size = $n")
}Sample Size Determination
desiredHW = 0.1
stdDev = 6.0
Level = 0.99
recommended sample size = 23886
If the quantity of interest is a proportion, then a different method can be used. In particular, a \(100\times(1 - \alpha)\%\) large sample two sided confidence interval for a proportion, \(p\), has the following form:
\[ \hat{p} \pm z_{1-(\alpha/2)} \sqrt{\dfrac{\hat{p}(1 - \hat{p})}{n}} \]
where \(\hat{p}\) is the estimate for \(p\). From this, you can determine the sample size via the following equation:
\[ n = \left(\dfrac{z_{1-(\alpha/2)}}{\epsilon}\right)^{2} \hat{p}(1 - \hat{p}) \]
Suppose that we want to simulate a normally distributed random variable, \(X\), with \(E[X] = 10\) and \(Var[X] = 16\). From the simulation, we want to estimate the true population mean with 99 percent confidence for a half-width \(\pm 0.50\) margin of error. In addition to estimating the population mean, we want to estimate the probability that the random variable exceeds 8. That is, we want to estimate, \(p= P(X>8)\). We want to be 95% confident that our estimate of \(P(X>8)\) has a margin of error of \(\pm 0.05\). What are the sample size requirements needed to meet the desired margins of error?
Let \(X\) represent the unknown random variable of interest. Then, we are interested in estimating \(E[X]=\mu\) and \(p = P[X > 8]\). To estimate a probability it is useful to define an indicator variable. An indicator variable has the value 1 if the condition associated with the probability is true and has the value 0 if it is false. To estimate \(p\), define the following indicator variable:
\[ Y_i = \begin{cases} 1 & X_i > 8\\ 0 & X_i \leq 8 \\ \end{cases} \] This definition of the indicator variable \(Y_i\) allows \(\bar{Y}\) to be used to estimate \(p\).
\[ \hat{p} = \bar{Y} = \frac{1}{n}\sum_{i=1}^{n}Y_i = \frac{\#\lbrace X_i > 8\rbrace}{n} \]
fun main() {
val rv = NormalRV(10.0, 16.0, streamNum = 3)
val estimateX = Statistic("Estimated X")
val estOfProb = Statistic("Pr(X>8)")
val r = StatisticReporter(mutableListOf(estOfProb, estimateX))
val n0 = 20 // sample size
for (i in 1..n0) {
val x = rv.value
estimateX.collect(x)
estOfProb.collect(x > 8)
}
println(r.halfWidthSummaryReport())
}| Performance Measures | Average | 95% Half-Width |
|---|---|---|
| Pr(X>8) | 0.80 | 0.1921 |
| Estimated X | 12.2484 | 2.2248 |
To determine the sample size so that we get a half-width that is less than \(\epsilon = 0.1\) with 99% confidence, we can use the previous equations. Since the table reports a half-width for a 95% confidence interval, we need to solve for \(s\) in terms of \(h\). Rearranging the half-width equation yields,
\[ s = \dfrac{h\sqrt{n}}{t_{1-(\alpha/2), n-1}} \]
Using the results and \(\alpha = 0.05\) yields,
\[ s_0 = \dfrac{h\sqrt{n_0}}{t_{1-(\alpha/2), n_0-1}} = \dfrac{2.2248\sqrt{20}}{t_{0.975, 19}}= \dfrac{2.2248\sqrt{20}}{2.093024}=4.7536998 \]
For a 99% confidence interval, we have \(\alpha = 0.01\) and \(\alpha/2 = 0.005\). Thus, \(z_{0.995} = 2.576\). Because the margin of error, \(\epsilon\) is \(0.5\), we have that,
\[ n \geq \left(\dfrac{z_{1-(\alpha/2)}\; s}{\epsilon}\right)^2 = \left(\dfrac{2.5758293064439264 \times 4.7536998}{0.5}\right)^2 = 599.73 \approx 600 \]
To determine the sample size for estimating \(p=P(X>8)\) with 95% confidence to \(\pm 0.1\), we can use Equation @ref(eq:pSampleSize)
\[ n = \left(\dfrac{z_{1-(\alpha/2)}}{\epsilon}\right)^{2} \hat{p}(1 - \hat{p})=\left(\dfrac{z_{0.975}}{0.05}\right)^{2} (0.80)(1 - 0.80)=\left(\dfrac{1.96}{0.05}\right)^{2} (0.80)(0.20) = 245.86 \approx 246 \]
By using the maximum of \(\max{(246, 600)=600}\), we can re-run the simulation for this number of replications. Doing so, yields,
| Performance Measures | Average | 95% Half-Width |
|---|---|---|
| Pr(X>8) | 0.7117 | 0.0363 |
| Estimated X | 10.2712 | 0.3284 |
The half-width criteria are met.
fun main() {
val rv = NormalRV(10.0, 16.0, streamNum = 3)
val estimateX = Statistic("Estimated X")
val estOfProb = Statistic("Pr(X>8)")
val r = StatisticReporter(mutableListOf(estOfProb, estimateX))
val n0 = 20 // sample size
for (i in 1..n0) {
val x = rv.value
estimateX.collect(x)
estOfProb.collect(x > 8)
}
println(r.halfWidthSummaryReport())
val desiredHW = 0.5
val s0 = estimateX.standardDeviation
val level = 0.99
val n = Statistic.estimateSampleSize(desiredHW = desiredHW,stdDev = s0,level = level)
println("Sample Size Determination")
println("desiredHW = $desiredHW")
println("stdDev = $s0")
println("Level = $level")
println("recommended sample size = $n")
val m = Statistic.estimateProportionSampleSize(desiredHW = 0.05,pEst = estOfProb.average,level = 0.95)
println("Recommended sample size for proportion = $m")
}The results match the values computed from the equations.
Sample Size Determination
desiredHW = 0.5
stdDev = 4.753707020199372
Level = 0.99
recommended sample size = 600
Recommended sample size for proportion = 246
Consider the game of “craps” as played in Las Vegas. The basic rules of the game are as follows: one player, the “shooter”, rolls a pair of dice. If the outcome of that roll is a 2, 3, or 12, the shooter immediately loses; if it is a 7 or an 11, the shooter wins. In all other cases, the number the shooter rolls on the first toss becomes the “point”, which the shooter must try to duplicate on subsequent rolls. If the shooter manages to roll the point before rolling a 7, the shooter wins; otherwise the shooter loses. It may take several rolls to determine whether the shooter wins or loses. After the first roll, only a 7 or the point have any significance until the win or loss is decided. Using the KSL random and statistic packages give answers and corresponding estimates to the following questions. Be sure to report your estimates in the form of confidence intervals.
fun main() {
val d1 = DUniformRV(1, 6, streamNum = 3)
val d2 = DUniformRV(1, 6, streamNum = 4)
val probOfWinning = Statistic("Prob of winning")
val numTosses = Statistic("Number of Toss Statistics")
val numGames = 5000
for (k in 1..numGames) {
var winner = false
val point = d1.value.toInt() + d2.value.toInt()
var numberoftoss = 1
if (point == 7 || point == 11) {
// automatic winner
winner = true
} else if (point == 2 || point == 3 || point == 12) {
// automatic loser
winner = false
} else {// now must roll to get point...} else { // now must roll to get point
var continueRolling = true
while (continueRolling) {
// increment number of tosses
numberoftoss++
// make next roll
val nextRoll = d1.value.toInt() + d2.value.toInt()
if (nextRoll == point) {// hit the point, stop rolling
winner = true
continueRolling = false
} else if (nextRoll == 7) {// crapped out, stop rolling
winner = false
continueRolling = false
}
}
}
probOfWinning.collect(winner)
numTosses.collect(numberoftoss.toDouble())
}As we can see from the results of the statistics reporter, the probability of winning is just a little less than 50 percent.
Half-Width Statistical Summary Report - Confidence Level (95.000)%
Name Count Average Half-Width
--------------------------------------------------------------------------------
Prob of winning 5000 0.4960 0.0139
Number of Toss Statistics 5000 3.4342 0.0856
--------------------------------------------------------------------------------
Sly’s convenience store sells BBQ wings for 25 cents a piece. They cost 15 cents a piece to make. The BBQ wings that are not sold on a given day are purchased by a local food pantry for 2 cents each. Assuming that Sly decides to make 30 wings a day, what is the expected revenue for the wings provided that the demand distribution is as show in in this table:
| \(d_{i}\) | 5 | 10 | 40 | 45 | 50 | 55 | 60 |
|---|---|---|---|---|---|---|---|
| \(f(d_{i})\) | 0.1 | 0.2 | 0.3 | 0.2 | 0.1 | 0.05 | 0.05 |
| \(F(d_{i})\) | 0.1 | 0.3 | 0.6 | 0.8 | 0.9 | 0.95 | 1.0 |
fun main() {
val q = 30.0 // order qty
val s = 0.25 //sales price
val c = 0.15 // unit cost
val u = 0.02 //salvage value
val values = doubleArrayOf(5.0, 10.0, 40.0, 45.0, 50.0, 55.0, 60.0)
val cdf = doubleArrayOf(0.1, 0.3, 0.6, 0.8, 0.9, 0.95, 1.0)
val dCDF = DEmpiricalRV(values, cdf, streamNum = 3)
val stat = Statistic("Profit")
val n = 100.0 // sample size
var i = 1
while (i <= n) {
val d = dCDF.value
val amtSold = minOf(d, q)
val amtLeft = maxOf(0.0, q - d)
val g = s * amtSold + u * amtLeft - c * q
stat.collect(g)
i++
}
}Running the model for 100 days results in the following output.
Count = 100.000000
Average = 1.677500
Std. Dev. = 2.201324
Half-width = 0.436790
95.0% CI = [1.2407095787617952, 2.1142904212382048]
A manufacturing process has a simple inspection process. The process manufactures items and places them in boxes of with four items per box. The manufacturing process has a defect rate of 15%. That is, the probability of an individual item being defective is 0.15. Assume that the chance of an item being defective is independent of any other item being defective. An inspector tests each box by randomly sampling one item from the box. If the selected item is good, the box is passed; if the selected item is defective, the box is rejected. We are interested in estimating the expected number of boxes that the inspector will look at before the first box is rejected.
The probability that an item is defective can be modeled with a Bernoulli random variable with probability of success equal 0.15. In the following code, we model the inspection process with a function that counts the number of boxes that are inspected until the first rejected box.
fun main() {
val itemRV = BernoulliRV(probOfSuccess = 0.15, streamNum = 3)
val itemsPerBox = 4
val stat = Statistic("Num Until Rejection")
val sampleSize = 100
for (i in 1..sampleSize) {
val countBoxes = numUntilFirstRejection(itemRV, itemsPerBox)
stat.collect(countBoxes)
}
print(String.format("%s \t %f %n", "Count = ", stat.count))
print(String.format("%s \t %f %n", "Average = ", stat.average))
print(String.format("%s \t %f %n", "Std. Dev. = ", stat.standardDeviation))
print(String.format("%s \t %f %n", "Half-width = ", stat.halfWidth))
println((stat.confidenceLevel * 100).toString() + "% CI = " + stat.confidenceInterval)
}/**
* This function counts the number of boxes inspected until the first
* box is found that has a randomly selected item that is defective.
*/
fun numUntilFirstRejection(itemRV: BernoulliRV, itemsPerBox: Int): Double {
require(itemsPerBox >= 1) { "There must be at least 1 item per box" }
var count = 0.0
do {
count++
// randomly generate the box
val box = itemRV.sample(itemsPerBox)
// randomly sample from the box
val inspection = KSLRandom.randomlySelect(box)
// stops if bad = 1.0 is found, continues if good = 0.0 is found
} while (inspection != 1.0)
return count
}Consider a project consisting of nine jobs (A, B, \(\cdots\), I) with the precedence relationships and activity times. The table provides the minimum, mode, and maximum for each activity. We will assume that the activity times can be modeled using a triangular distribution with the appropriate parameters.
| Job | Predecessors | Min | Mode | Max |
|---|---|---|---|---|
| A | - | 2 | 5 | 8 |
| B | A | 6 | 9 | 12 |
| C | A | 6 | 7 | 8 |
| D | B,C | 1 | 4 | 7 |
| E | A | 7 | 8 | 9 |
| F | D,E | 5 | 14 | 17 |
| G | C | 3 | 12 | 21 |
| H | F,G | 3 | 6 | 9 |
| I | H | 5 | 8 | 11 |
Project Network Diagram
val activityRVs = mapOf<String, TriangularRV>(
"A" to TriangularRV(2.0, 5.0, 8.0, streamNum = 2),
"B" to TriangularRV(6.0, 9.0, 12.0, streamNum = 3),
"C" to TriangularRV(6.0, 7.0, 8.0, streamNum = 4),
"D" to TriangularRV(1.0, 4.0, 7.0, streamNum = 5),
"E" to TriangularRV(7.0, 8.0, 9.0, streamNum = 6),
"F" to TriangularRV(5.0, 14.0, 17.0, streamNum = 7),
"G" to TriangularRV(3.0, 12.0, 21.0, streamNum = 8),
"H" to TriangularRV(3.0, 6.0, 9.0, streamNum = 9),
"I" to TriangularRV(5.0, 8.0, 11.0, streamNum = 10),
) val timeToCompleteStat = Statistic("Time to Completion")
val probLT50Days = Statistic("P(T<=50)")
val probWith42and48Days = Statistic("P(42<=T<=48)")
val interval = Interval(42, 48)
val sampleSize = 1000
for (i in 1..sampleSize) {
val a = generateActivityTimes(activityRVs)
val t = timeToCompletion(a, paths)
timeToCompleteStat.collect(t)
probLT50Days.collect(t <= 50)
probWith42and48Days.collect(interval.contains(t))
}
val sr = StatisticReporter(mutableListOf(timeToCompleteStat, probLT50Days, probWith42and48Days))
println(sr.halfWidthSummaryReport())Statistical Summary Report
| Name | Count | Average | Half-Width |
|---|---|---|---|
| Time to Completion | 1000.0 | 47.80137141578008 | 0.24824203870040715 |
| P(T<=50) | 1000.0 | 0.7010000000000005 | 0.02842407807532723 |
| P(42<=T<=48) | 1000.0 | 0.4729999999999999 | 0.03099757091483164 |
Because running Monte-Carlo experiments is very common, the KSL has provided some classes to support simple experimentation on Monte-Carlo problems within the ksl.utilities.mcintegration package.
MCReplicationIfcTo use the MCExperiment class the user can either subclass MCExperiment or provide a function that represents a replication of an individual observation. This can be done by using the MCReplicationIfc functional interface.
class NewsVendor(var demand: RVariableIfc) : MCReplicationIfc {
var orderQty = 30.0 // order qty
var salesPrice = 0.25 //sales price
var unitCost = 0.15 // unit cost
var salvageValue = 0.02 //salvage value
override fun replication(j: Int): Double {
val d = demand.value
val amtSold = minOf(d, orderQty)
val amtLeft = maxOf(0.0, orderQty - d)
return salesPrice * amtSold + salvageValue * amtLeft - unitCost * orderQty
}
}This code creates an instance of the news vendor problem and supplies the same demand distribution used in the previous section. The instance of NewsVendor, which implements the MCReplicationIfc interface is supplied to the MCExperiment as a constructor parameter. Then the desired half-width bound is set and the simulation executed.
fun main() {
val values = doubleArrayOf(5.0, 10.0, 40.0, 45.0, 50.0, 55.0, 60.0)
val cdf = doubleArrayOf(0.1, 0.3, 0.6, 0.8, 0.9, 0.95, 1.0)
val dCDF = DEmpiricalRV(values, cdf, streamNum = 3)
val nv = NewsVendor(dCDF)
val exp = MCExperiment(nv)
exp.desiredHWErrorBound = 0.01
exp.runSimulation()
println(exp)
}Monte Carlo Simulation Results
initial Sample Size = 30
max Sample Size = 10000
reset Stream OptionOn = false
Estimated sample size needed to meet criteria = 2045.0
desired half-width error bound = 0.01
actual half-width = 0.00999980532316225
error gap (hw - bound) = -1.946768377510122E-7
-----------------------------------------------------------
The half-width criteria was met!
-----------------------------------------------------------
**** Sampling results ****
Number of macro replications executed = 2047.0
Number of micro replications per macro replication = 100
Total number of observations = 204700.0
ID 2
Name Statistic_1
Number 2047.0
Average 1.5049719101123593
Standard Deviation 0.23069885711869198
Standard Error 0.005099017725643279
Half-width 0.00999980532316225
Confidence Level 0.95
Confidence Interval [1.494972104789197, 1.5149717154355216]
MCExperiment Key Concepts\[ (\bar{Y}_{\cdot 1},\bar{Y}_{\cdot 2}, \dots, \bar{Y}_{\cdot k} ) \]
MCExperiment Key ConceptsUsing the KSL’s random variate generation methods and the functionality for collecting and reporting statistics Monte Carlo experiments can be easily setup and executed.
The quantities to be estimated from Monte Carlo experiments are random variables. We use the Monte Carlo experiment to generate a random sample from the experiment.
Because we are performing an experiment, we should plan the sampling by understanding the sampling error and if possible, pre-plan the required sample size.
Besides the random variate generation and statistical classes within the KSL, we can use the KSL’s MCExperiment class to execute Monte Carlo experiments.