Coding Stories: Random Experiences
By: Greg Benedis-Grab
Tags: JavaScript, Computing Education, Mathematics, P5js, Monte Carlo

Speaking of random while preparing for the school year I got an email from a friend who was starting a new job. She wrote, “I have a problem that you need to hear about!” She then explained that her school struggles with assigning students to electives based on their preferences. She wondered whether a computer could help. I immediately thought… Yes, of course.

I am always on the lookout for a good CS problem and was hopeful that this would qualify. I have engaged in assigning student groups by hand at various points in my teaching career. I would even call it a right of passage for educators. Below is an approximation of the steps I followed (as I remember them)

Step 1: Imagine 9 Students vying for three electives

Each student is represented by a rectangle constructed of four vertically aligned boxes. The top box contains the students id number ranging in this case from 0 to 8. The following three boxes from top to bottom are the elective preference rankings for that student identified by color codes, red, green and blue.

Step 2: Put all students in their first choice group.

Step 3: Identify the group with the smallest enrollment and look for a student in an over-enrolled group who would want to move in. Who wants to move in? Well the higher the rank choice the better. And if two student tie for rank choice then give it to the one from the bigger initial group.

Step 4: repeat the process until you have even groups

These steps not only helped me figure out the solution but also helped to clarify the problem. The first step in tackling a coding problem is to carefully define it. This includes laying out the steps as well as clarifying what information is important, how it can be represented, and the best way to store it. By starting with a simple example and walking through it carefully in my mind I was now ready to start building my animation.

I decided p5 was going to be a good platform to visualize this process. To help organize my code I defined a Student class.

class Student {

constructor(num) {

this.num = num;

this.rankings = shuffle([...groupList]);

}

} // note... groupList = [0,1,2]

Preferences are stored in the ranking attribute of the Student object. The .id attribute is the student ID number. The next step is to represent this object visually on the screen. I added a .pos attribute to the constructor to define the default position of the rectangle.

this.pos = createVector(60+num*17,10);

Then I wrote a show() function for the object so it could be displayed in the draw() loop.

show() {

textAlign(CENTER);

textSize(10);

fill(255);

stroke(0);

strokeWeight(1);

rect(this.pos.x,this.pos.y,14,20);

fill(0);

text(this.num,this.pos.x+7,this.pos.y+15);

for (let i=0;i<3;i++){

fill(colors[this.rankings[i]]);

rect(this.pos.x,this.pos.y+i*20+20,14,20);

}

}

With this framework I could begin my animation of the group sorting routine described above. In my setup() function I instantiated 9 Student objects in the global students array

for (let i=0;i<num_students;i++) {

students.push(new Student(i));

}

I set a list of colors for the each group.

colors = [color(255,0,0),color(0,255,0),color(0,0,255)];

I drew boxes for each of the groups

for(let i=0;i<3;i++) { // create boxes for each group

stroke(colors[i]);

rect(40,i*100+100,300,90);

}

Using the .forEach() method on arrays I came up with a tidy way to display all the students

students.forEach((s)=>s.show());

I wrote a .glideTo() method in the Student class to handle the animation of the students moving across the screen

glideTo(pos) {

this.start = new Date().getTime();

this.move = pos.sub(this.pos);

}

I also revised my show() function to display the student in an intermediate location during the glide.

The last step was to articulate the algorithm that moves students to a new group. I needed to put the clearly defined steps above into proper JavaScript syntax. I could have written it more clearly with additional lines of code but I started experimenting with the .reduce method along with some es6 arrow syntax.

// find the smallest group

let smallest = groups.reduce((iMin, group, i, arr) => group.length < arr[iMin].length ? i : iMin, 0); // if the groups are even we are done

if (groups[smallest] >= 3) return; // identify the student we want to move

let toMove = students.reduce((iBest,s,i,arr)=>{

curRank = s[i].rankings.indexOf(smallest);

if curRank<s[iBest].rankings.indexOf(smallest) ? return i : iBest

}) // Initiate the move

students[toMove].assignGroup(smallest);

Putting it all together I had an animation of the simple version of of the sorting problem. I increased the number of students to 45.

It appears that the computer is going to be of help here. But I started to wonder whether this algorithm was going to produce the best possible solution. I remembered sometimes struggling to find good placements for some students when I used to do it by hand. Then I recalled a time I used a Monte Carlo simulation to apply random moves to solve a complex problem. I wondered if I could do the same thing here. Based on my memory of Monte Carlo and some googling I came up with the following approach. Disclaimer: I am by no means an expert in Monte Carlo so I am open to feedback.

Step 1: Write a fitness function to describe the overall happiness of the students

//fitness_of_one_student = numGroups - 1 - rankChoice

//rankChoice is the ordinal group preference. e.g. 1st choice == 0 def fitness(data=this.students) {

let choices = data.map((s)=>s.choice());

return 1440 - choices.reduce((a,b)=>a+b);

}

Suppose we have 9 groups and 180 students. If all the students get their first choice then they all contribute 8 points to the fitness function and the total fitness is 1440. I could also express fitness or happiness as a percentage by dividing the total by 1440. If all the students were put in their last choice preference then the total fitness would be 0 or 0%

Step 2: Put the students randomly in groups.

Step 3: Choose a random pair of students who are in different groups and determine the change in fitness that would occur. I might write some code to calculate this as follows

let p1 = {group:0};

let p2 = {group:0};

while(p1.group != p2.group) {

p1 = this.students[int(random(this.students.length))];

p2 = this.students[int(random(this.students.length))];

}

let new_fit = p1.choice(p2.group)+p2.choice(p1.group);

let cur_fit = p1.choice()+p2.choice();

let change = newg-curg

Using the value of change in fitness I could select which swaps to allow. A greedy approach would be to only make swaps where the fitness improved. However, the danger is that you could get stuck in a sub-optimal state. The suggestion I saw online was to use the following approach.

let r = random();

let comparison = exp(k*change); // exp is a power of e

if (r<comparison) makeSwap();

This function has the advantage that if change is >= 0 then the swap will always take place. If the swap is < 0 then it will sometimes take place favoring swaps with lower losses to fitness. This routine takes a long time to converge so I experimented with the following comparison.

if (change>0 || r<A*exp(k*change)) makeSwap()

In this case I still make all the advantageous swaps but I only make a fraction of swaps (based on the value of A) that are 0 change and then the probability of swapping drops off exponentially from there. This converged much more quickly.

I wanted to think up a new way to visualize this work. I thought of pizzas. I created 9 pizzas or groups. Each student is a slice of pizza and the slice color represents the happiness of that student. 0 shown with green means they got their first choice and they are very happy. 8 indicates last choice with red and shows a total lack of happiness. Then I added gliding of slices to liven things up and included a percent happiness on the screen.

From this animation you can gain insight into the Monte Carlo process. So I suppose the point of this work is that you can look up how to implement a Monte Carlo simulation and get it working. That is challenging and worth doing. But for me the real learning came when I started playing around with the simulation. By adjusting the A and k I could see how it impacted the overall results. I could run the simulation multiple times with the same preferences. I could really see what the simulation was doing. More importantly I could make decisions about what to make interactive based on what I wanted to learn and what I was thinking about. Simulations are beautiful and inspiring and they are ways of thinking more deeply about the models that we create of the world around us. I find that coding is the best way to get deeper into the thinking of simulations and they often lead to more insights into the problem they are trying to model.

I was excited about the work that I had done so I went to go show it to a one of the math teachers at my school. He asked me if I had heard of the stable marriage problem.

I visited Wikipedia and watched some videos. The stable marriage problem is intriguing because it involves rank preferences on the part of two distinct groups. In this case it is men and women. The solution to the problem is a straight forward algorithm not requiring anything as fancy as a Monte Carlo simulation. First I thought… well that problem is different because both parties have rank preferences. In my problem students choose electives but electives do not choose students. Then I started to worry that maybe my problem was not so different after all. I asked a few people what they thought and could not get an answer. I decided to play around with the code to see how the two approaches compared.

Oh no. The algorithm that I gave in the first animation turned out to produce the best possible solution to the problem of 9 groups and 180 students. I ran it 200 times and it was always the best. The Monte Carlo swaps never improved upon the original solution. I suddenly felt silly that I had spent so long building an animation with flying pizza slices that was of no problem solving value. I rationalized my disappointment with the justification that I had a deeper sense of the problem having gone through all these steps. But that did not make me feel any less disappointed. I took a break from the problem for a few days.

When I picked the problem back up I started thinking about how students choose electives. Real students don’t usually have random preferences. Often some electives are much more popular than others. I started designing a preference generation algorithm based on defined weightings for each elective. I could randomly select those weightings and then use them for all the students.

generateRankings(){

let info = [];

for (let i in weightings){

info.push([weightings[i].value()/10, int(i)])

}

let prefs = [];

while (info.length > 0){

let tot = 0;

let sums = [];

for (let elem of info){

tot+=elem[0];

sums.push(tot);

}

let r = random()*tot

let choice = getPref(sums,r);

prefs.push(info[choice][1]);

info.splice(choice,1);

}

return prefs;

}

This time the Monte Carlo approach was advantageous. With the weightings included, the algorithm could not find the optimal solution. Suddenly my pizza animation was back in business. I added a menu bar on the right to control the popularity weightings and included some buttons to control the functionality of the simulation. Here is the final version.

I called this post random experiences mostly as a tribute to the random element of the Monte Carlo simulation. The random selection of potential students to swap and the random number r that determines whether a swap is made leads to a probabilistic simulation and has the ability to solve complex problems in a powerful efficient way. It is an incredibly insightful approach that draws on the normalized distribution of randomness in the world around us. However, my reference to random experiences also speaks to the somewhat circuitous path that I followed in trying to solve this problem. I began with a simple simulation. I moved to a much more complex approach. Then I thought that the simple solution was in fact right all along only to later realize that I needed both approaches to address the problem fully. Sometimes problem solving can seem random and is full of micro frustrations. But what I am learning through this writing process is that the journey is more interesting and satisfying than the final solution.