Pobability calculator unittest fail

Hey everyone, hope you are all having a good day.
So I’m working on the probability calculator, and I keep getting the same unittest error that my output doesn’t match the expected output:

FAIL: test_prob_experiment (test_module.UnitTests)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/runner/boilerplate-probability-calculator/test_module.py", line 26, in test_prob_experiment
    self.assertAlmostEqual(actual, expected, delta = 0.01, msg = 'Expected experiment method to return a different probability.')
AssertionError: 0.096 != 0.272 within 0.01 delta (0.17600000000000002 difference) : Expected experiment method to return a different probability.

----------------------------------------------------------------------
Ran 3 tests in 0.004s

FAILED (failures=1)

I know what the error is about, but I tried so many different ways and the probability still doesnt match. Here is the code I wrote that in my head makes the most sense:

def experiment(hat=None, expected_balls=None, num_balls_drawn=None, num_experiments=None):
    hat_copy = copy.copy(hat)
    match = 0
    verify = []

    # looping the amount of experiments
    for i in range(num_experiments):
        # getting random balls
        balls = [random.choices(hat_copy.contents, k=num_balls_drawn)]

        # loop through expected balls: ball represents each ball
        for ball in expected_balls:
            # getting value of each expected ball: the amount of times it is expected to show
            value = expected_balls.get(ball)
            # if the random balls list has at least the number of balls expected from a color
            # append True to the verify list for later use, else append false
            if balls[0].count(ball) >= value:
                verify.append(True)
            else:
                verify.append(False)

            # when the length of verify equals the length of expected balls, meaning that verify contains the result 
            # of the encounter of each ball in expected_balls (True or False) 
            if len(verify) == len(expected_balls):
                # if all elements in verify equal True than add 1 to match, match stands for the number of times
                # there is a match in expected_balls and the random balls
                if all(verify):
                    match += 1
                # empty verify so next experiment will identical
                verify = []
    
    # divide match (number of times there was a match) and the number of experiments realized
    probability = match / num_experiments

    return probability

I tried my best to explain the function with comments in the code, so you can understand.
I am really stuck becaue this seems correct to me, but maybe you can see something that I don’t.

Here is a link to all the code: https://replit.com/@SolGeller/boilerplate-probability-calculator#prob_calculator.py

Thanks a lot in advance!!!

According to the documentation, this draws with replacement, which is what you don’t want. Since you already have a draw() method for Hat, why not use that?

1 Like

Hey, thank you a lot for your quick response, this makes total sense. But, it’s still not working, do you think i need to change more code in order for this to work?

Add some print() statements here in both branches to watch what happens as the experiment occurs:

Since your probabilities are low, you may can start with just the false side (your code is rejecting more than it should). I printed expected_balls, value, balls, and balls[0].count(ball) and then I looked for good draws that were counted as bad and other problems. When I found the replacement issue, it was because the code was producing sets of balls in the last test that were missing the test ball, even though it should have drawn all the balls in the hat.

The only thing that jumps out is that I would set verify = [] at the top of the outer loop instead of at the end in a conditional so that every experiment started the same, thus avoiding any weird cases where you finish an experiment and the lengths are not equal.

Hey Jeremy, thank you for taking your time to help me solve the problem. Anyway I printed out what you told me and turns out the problem is in the draw
method i think. Because for example, if the function says, draw 20 balls, and the hat has only 10, what should the draw mehod do? Should it randomly choose ten balls from the hat, and after the hat is left with nothing, return all the balls back and than choose ten more balls and add them to the drawn_balls, keeping the ten balls added previously? I am saying this because my draw method chooses random balls, adds them to drawn_balls and when the hat is left with no balls i return all balls to the hat but remove them from the drawn_balls list too. I hope you understand what im saying. Thank you again for your help!!!

It’s hidden in the spec somewhere, but if there are more balls requested than available, you should return them all. I think the last experiment test checks this by initializing the hat with 19 balls and drawing 20, which is why that probability is 1 (and draw() should return all 19 balls).

1 Like

Hello again Jeremy, I think I understand what the problem is right now. So lets say I want to draw 20 balls, so my draw method will draw all the balls in the hat, so as you said, every time there is a draw bigger than the number of balls in the hat, the probability will be 100%. But, as you will see in the following code, the problem happens with other probabilities because, lets say I draw 4 balls from a hat that has 8. Then, I draw 4. Now my hat will be empty and the next draw returns an empty list because I can’t figure out how to fill up the balls_left list again while the loop is going:

    def draw(self, n_draws):
        balls_drawn = []
        extra = self.extra
        balls_left = self.contents
        extra.sort()
        balls_left.sort()
        for i in range(n_draws):
            if not balls_left:
                for ele in extra:
                    balls_left.append(ele)
            else:
                rand_ball = random.choice(balls_left)
                balls_left.remove(rand_ball)
                balls_drawn.append(rand_ball)

        return balls_drawn


hat3 = Hat(blue=1, red=2, green=5)
print(hat3.draw(4))
print(hat3.draw(1))
print(hat3.draw(4))
print(hat3.contents)


def experiment(hat=None, expected_balls=None, num_balls_drawn=None, num_experiments=None):
    hat_copy = copy.copy(hat)
    match = 0
    verify = []

    for i in range(num_experiments):
        balls = hat_copy.draw(num_balls_drawn)

        for k in expected_balls:
            value = expected_balls.get(k)
            if balls.count(k) >= value:
                verify.append(True)
            else:
                verify.append(False)
            if len(verify) == len(expected_balls):
                if all(verify):
                    match += 1
                verify = []

    probability = match / num_experiments

    return probability

Here are the printed statements from above:

['red', 'green', 'red', 'blue']
['green']
['green', 'green', 'green']
['blue', 'green', 'green', 'green', 'green', 'green', 'red', 'red']

So basically on the last draw it only draws 3 instead of 4.

Here are my probabilities of the two tests:

hat = app.Hat(blue=3,red=2,green=6)
probability = app.experiment(
    hat=hat,
    expected_balls={"blue":2,"green":1},
    num_balls_drawn=4,
    num_experiments=1000)
print("Probability 1:", probability)

hat1 = app.Hat(yellow=5,red=1,green=3,blue=9,test=1)
probability2 = app.experiment(
    hat=hat1, expected_balls={"yellow":2,"blue":3,"test":1},
    num_balls_drawn=20,
    num_experiments=100)
print("Probability 2:", probability2)

Probability 1: 0.207
Probability 2: 1.0

Probability 1 varies from floats in between 0.185 to 0.230
Probability 2stays the same.

If you have time and help me with this it would be awesome. Thank you for your time and effort.

The hat is actually just good for one experiment (draw), so you create a hat, do the draw, evaluate success or failure, and then repeat with a new hat. If the hat is built with 8 balls, and the experiment draws 4, the simulation should evaluate success or failure and then recreate the hat for the next experiment. This is because the probabilities that are being simulated are for drawing x balls from y balls in a hat, not drawing x balls followed by y balls from z balls in a hat, etc. If you run this experiment enough times, then the calculated probability should converge onto the analytical probability (which is discussed elsewhere in the forums, and it does).

When you are doing the experiments, you need to work on a copy of the hat for every experiment (in other words, each time through the loop). This code

makes a copy before the loop and tries to keep using it.

1 Like

Thank you a lot, I understand now. I really apreciate your help and your time. So basically I solved the problem and passed the tests by changing my draw method to this:

    def draw(self, n_draws):
        balls_drawn = []
        extra = self.extra
        balls_left = self.contents
        if n_draws > len(balls_left):
            balls_left.clear()
            for ele in extra:
                balls_left.append(ele)
            if n_draws > len(balls_left):
                return extra
            else:
                rand_balls = random.sample(balls_left, n_draws)
                for ele in rand_balls:
                    balls_left.remove(ele)
                    balls_drawn.append(ele)
        else:
            rand_balls = random.sample(balls_left, n_draws)

            for ele in rand_balls:
                balls_left.remove(ele)
                balls_drawn.append(ele)

        return balls_drawn

and i also implemented the tip you gave me with the copy, although it didnt make much of a difference on the output, I really don’t know why. But maybe it is because I used a different method to extract random balls from the hat (random.sample()) instead of looping for n_draws times, this made it easier to return all the balls to the hat if the number of draws exeeded.
Anyway, have a nice day and thanks for the help!