I developed [Mule Deer Expert Elicitation App](https://script.google.com/macros/s/AKfycbykbBJA0vlY11n57RxHDjJ7xiBMPjOI5p_SYg7Egn7Tsj_oxi9Fap2uG5PQzyGROHzmqA/exec) while working with Colorado Parks & Wildlife and the USGS to develop a [[Habitat Quantification Tool]] (HQT) for mule deer in western Colorado.
We needed to develop a new [[state and transition model]] to forecast and quantify the benefits of habitat restoration projects. The transition probabilities of state and transition models are often parameterized through expert elicitation in the absence of data. A major challenge with eliciting such expertise is that the transition probabilities are not always the intuitive way in which experts understand changes on a landscape. To help experts provide better estimates, I developed a [[data-driven web app]] using [[Plotly]] and [[base/Google Apps Script/Google Apps Script|Google Apps Script]] to instantly translate inputted transition probabilities and forage values into visualizations of expected forage value (a proxy for habitat quality) over time. The app logged the results to a Google Sheet.
Under the hood, the app uses [[Markov Chain Monte Carlo]] to simulate stochastic transitions over time. It was a fun challenge to write performant algorithms in JavaScript for this project.
Following a brief training, experts were able to successfully use the app independently to provide parameter estimates and a consensus was found amongst a majority of users, allowing us to populate a state and transition model for use in the HQT.
## Markov chain Monte Carlo implementation
The code to run the simulations is duplicated below. Full code can be found [here](https://script.google.com/home/projects/1qFQaPko1VtWEkaZGmrf3t4pc5DBwnvOzZU1nik08N-KkmnZ01_NuuX8p/edit).
```JavaScript
// TRANSITIONS and FORAGEVALUES provided as user input as dict
// var TRANSITIONS = {
// "A": [0.0, 0.0, .05, .05, .05],
// "B": [0.0, .05, 0.0, 0.0, 0.0],
// "C": [.05, 0.0, 0.0, .05, 0.0],
// "D": [.05, 0.0, .05, 0.0, .05],
// "E": [.05, .05, 0.0, .05, 0.0],
// };
// var FORAGEVALUES = {
// "A": [50, 50, 50],
// "B": [50, 50, 50],
// "C": [50, 50, 50],
// "D": [50, 50, 50],
// "E": [50, 50, 50]
// }
var STATES = ["A", "B", "C", "D", "E"];
var STATENAMES = {
"A": "Shrubland State",
"B": "Mesic Shrubland State",
"C": "Grassland State",
"D": "Perennial Grass Loss State",
"E": "Invaded State",
}
var FORAGEELS = [
['summerForageState1', 'summerForageState2', 'summerForageState3', 'summerForageState4','summerForageState5'],
['transitionForageState1', 'transitionForageState2', 'transitionForageState3', 'transitionForageState4', 'transitionForageState5'],
['winterForageState1', 'winterForageState2', 'winterForageState3', 'winterForageState4','winterForageState5']
];
var TRANSELS = [
[false, false, 'transition1to3', 'transition1to4', 'transition1to5'],
[false, false, false, false, 'transition2to5'],
['transition3to1',false, false, 'transition3to4', false],
['transition4to1', false, 'transition4to3', false, 'transition4to5'],
['transition5to1', 'transition5to2', false, 'transition5to4', false]
];
function randomChoice(arr) {
return arr[Math.floor(arr.length * Math.random())];
}
function rollDice(){
// get random number between 0 and 1
return Math.random();
}
function testTransition(transitionProbability){
// return true if dice roll is less than equal to transition probability
var diceRoll = rollDice();
if (diceRoll <= transitionProbability){
return true
}
}
function advanceState(state, transitions){
// return next state in the simulation (example of starting at state B)
var statesArray = Object.keys(transitions); // ["A", "B", "C", "D", "E"]
var transitionArray = transitions[state]; // [0, 1, 0, 0, 0]
// test each transition
var potentialTransitions = [];
transitionArray.forEach((transitionProbability, index) => {
if(testTransition(transitionProbability)){
var nextState = statesArray[index];
potentialTransitions.push(nextState);
}
});
// select from potential transitions
if (potentialTransitions.length === 0){
return state;
} else {
nextState = randomChoice(potentialTransitions);
return nextState
};
}
function simulateStates(duration, initialState, transitions){
var year = 1;
var stateArray = [initialState];
// run simulation
while (year < duration){
currentState = stateArray.slice(-1)[0];
nextState = advanceState(currentState, transitions);
stateArray.push(nextState);
year += 1;
}
return stateArray;
}
function scoreState(state, seasonIndex, forageValues){
return forageValues[state][seasonIndex];
}
function scoreStateArray(stateArray, seasonIndex, forageValues){
var valueArray = [];
stateArray.forEach(state => {
valueArray.push(scoreState(state, seasonIndex, forageValues));
});
return valueArray
}
function integrateValueArray(valueArray){
return valueArray.reduce((a, b) => a + b) / valueArray.length;
}
function runSimulation(duration, initialState, seasonIndex, forageValues, transitions){
var stateArray = simulateStates(duration, initialState, transitions);
var valueArray = scoreStateArray(stateArray, seasonIndex, forageValues);
var totalValue = integrateValueArray(valueArray);
return {
"stateArray": stateArray,
"valueArray": valueArray,
"totalValue": totalValue
};
}
function monteCarlo(repetitions, duration, initialState, seasonIndex, forageValues, transitions){
var stateArrays = [];
var valueArrays = [];
for(let i = 0; i < repetitions; i++){
resultsObject = runSimulation(duration, initialState, seasonIndex, forageValues, transitions);
stateArrays.push(resultsObject['stateArray']);
valueArrays.push(resultsObject['valueArray']);
};
// reduce valueArray
result = valueArrays[0].map((col, i) => valueArrays.map(row => row[i]).reduce((acc, c) => acc + c, 0) / valueArrays.length);
return result
}
function getElementValue(el){
if(el){
return +document.getElementById(el).value // + returns as number
} else {
return 0
}
}
function getForage(){
// convert array of elements to array of values
var forageValues = FORAGEELS.map(row => {
return row.map(getElementValue);
});
// transpose array of values (from seasons to states)
var transposedArray = forageValues[0].map((_, colIndex) => forageValues.map(row => row[colIndex]));
// create object
var forageResults = {};
transposedArray.forEach((valueArray, index) => {
state = STATES[index];
forageResults[state] = valueArray;
});
return forageResults
}
function getTransitions(){
// get element values (integers)
var transitionValues = TRANSELS.map(row => {
return row.map(getElementValue)
});
// update transition probability for same state
transitionValues.forEach((valueArray, index) => {
valueArray[index] = 100 - valueArray.reduce((acc, c) => acc + c, 0);
});
var transitionResults = {};
transitionValues.forEach((valueArray, index) => {
state = STATES[index];
valueArray = valueArray.map(value => value / 100);
transitionResults[state] = valueArray;
});
return transitionResults
}
```