Probability Calculator: Experiment always failing to draw successfully

I am currently working on the probability project and I am having issues with my logic. Here is the repl: https://replit.com/@Michael_Nicol/Probability-Calculator#prob_calculator.py

The section of the code I am having the most trouble with is the experiment function

My logic for experiment draw is that the draw fails if one of the colors drawn is not in expected or the amount drawn does not exactly match each color.

I put some print statements to understand the code better for each draw:

Formatted Drawn Contents: 
{'green': 2, 'blue': 1, 'red': 1}
Wanted for Experiment
{'blue': 2, 'green': 1}
- Not equal:  green     2     1
- Not equal:  blue     1     2
- Not in:  red    dict_keys(['blue', 'green'])
Total Success:  0 

Here is part of the code that controls part of this logic. Looking at the repl.it is best to understand the context, but the fault seems to be logical more than anything. I did 2,000 draws and not one passes multiple times. Is my approach wrong?

success = True
# Loop through the drawn contents
    for colorKey in dict.keys(drawOutputDir):
      # If the color we drew was not expected.
      if (colorKey not in dict.keys(expected_balls)):
        expected_flag = False
        print("- Not in: ", colorKey,"  ", dict.keys(expected_balls))
        # Otherwise, if the amount drew for the color is not the amount expected for that color
      elif drawOutputDir[colorKey] is not expected_balls[colorKey]:
        print("- Not equal: ",colorKey,"   ", drawOutputDir[colorKey], "   ",expected_balls[colorKey])
        expected_flag = False
      else:
        print("Made it through: ",colorKey)
     # If any color fails, the entire draw is a failure
    if expected_flag:
      success+=1

That’s a weird way to compare numbers…
Anyway, that logic looks ok. So the poblem might be in the draw-method.

As for the draw…
First if someone wants to draw more balls than are in the hat you return an empty array. If I want to draw 4 times out of a hat with only 2 balls, I still get 2 balls.

randNum = random.randint(0, (len(self.contents)-1)-(len(drawn_contents)-1 if len(drawn_contents) > 0 else 0))

…what is that? Nothing here makes sense.

1 Like

I stripped out all your logging and dropped in my draw method. That changed the numbers some (I think), so you may still have problems in the draw despite it passing the tests. I added some logging of supposed good and bad draws and got:

good: draw: ['green', 'green', 'blue', 'blue'] goal: {'blue': 2, 'red': 1}
good: draw: ['red', 'green', 'blue', 'blue'] goal: {'blue': 2, 'red': 1}
good: draw: ['green', 'red', 'green', 'green'] goal: {'blue': 2, 'red': 1}
bad: draw: ['red', 'green', 'blue', 'green'] goal: {'blue': 2, 'red': 1}
good: draw: ['red', 'blue', 'green', 'blue'] goal: {'blue': 2, 'red': 1}
good: draw: ['blue', 'blue', 'green', 'blue'] goal: {'blue': 2, 'red': 1}
good: draw: ['blue', 'green', 'blue', 'green'] goal: {'blue': 2, 'red': 1}
bad: draw: ['green', 'green', 'red', 'blue'] goal: {'blue': 2, 'red': 1}
good: draw: ['green', 'blue', 'green', 'blue'] goal: {'blue': 2, 'red': 1}
good: draw: ['blue', 'blue', 'red', 'green'] goal: {'blue': 2, 'red': 1}

As you can tell, it’s not right a lot.

What I would do is try to simplify the implementation. You’re copying stuff in the draw and the experiment. I would only copy in one place and since the draw method is called draw() and not draw_from_copy() it wouldn’t be there. It should be simple; you just need a new hat for each experiment.

The other thing I would do is to implement a test function to see if the draw is good or not so that you can test and debug it until you know it works. Then, it’s straightforward to combine performing and testing one draw per hat per experiment.

1 Like

Yeah, it didn’t. I fixed it to make more sense. I also fixed the empty array issue, drawing all balls removes all the balls and then returns them. Here is my current draw method:

  def draw(self, num):
    print("Current Hat: ")
    print(self.hat)
    print("Drawing from: ", self.contents)
    drawn_contents = []
    # If the user wants to draw more balls then possible
    if (num >= len(self.contents)):
      drawn_contents = copy.copy(self.contents)
      self.contents = []
      return drawn_contents
    # Draw a random ball num amount of times
    for drawnNum in range(0, num):
      # Grab a random number from 0 to the
      # length of self.contents-1
      randNum = random.randint(0, len(self.contents)-1)
      print("Picked random number: ", randNum, " and drew: ", self.contents[randNum])
      drawn_contents.append(self.contents[randNum])
      # remove the selected element
      self.contents.pop(randNum)
    return drawn_contents

I also updated the logic for my experiment method. I am not sure if you saw this change or not since I did it while you were making your post.

If the drawn contents has at least amount required (drew 5 blue and required 2 blue) for all required, then it will pass.

 expected_flag = True
    # Loop through the drawn contents
    for colorKey in dict.keys(drawOutputDir):
      # Update: Now it just checks if the required balls are at least preseant
      if colorKey in dict.keys(expected_balls) and drawOutputDir[colorKey] < expected_balls[colorKey]:
        print("- Not equal: ",colorKey,"   ", drawOutputDir[colorKey], "   ",expected_balls[colorKey])
        expected_flag = False
      else:
        print("Made it through: ",colorKey)
     # If any color fails, the entire draw is a failure
    if expected_flag:
      success+=1

Here is a example of the output:

Current Hat: 
{'red': 2, 'orange': 0, 'black': 0, 'blue': 3, 'yellow': 0, 'green': 6, 'pink': 0, 'striped': 0}
Drawing from:  ['red', 'red', 'blue', 'blue', 'blue', 'green', 'green', 'green', 'green', 'green', 'green']
Picked random number:  3  and drew:  blue
Picked random number:  3  and drew:  blue
Picked random number:  0  and drew:  red
Picked random number:  2  and drew:  green
Drawn Contents: 
['blue', 'blue', 'red', 'green']
Formatted Drawn Contents: 
{'blue': 2, 'red': 1, 'green': 1}
Wanted for Experiment
{'blue': 2, 'green': 1}
Made it through:  blue
Made it through:  red
Made it through:  green
Success (total):  47 Out of  96 

Now the probability is too high.

The draw method works as per the instructions. It leaves hat unaffected, and only used contents. @jeremy.a.gray, how can I tell if a draw is good or not? I put more prints as seen above and it shows complete randomness in every draw.

The Hat class should have a draw method that accepts an argument indicating the number of balls to draw from the hat. This method should remove balls at random from contents and return those balls as a list of strings. The balls should not go back into the hat during the draw, similar to an urn experiment without replacement. If the number of balls to draw exceeds the available quantity, return all the balls.

The experiment works as per the instructions. It checks if it at least has the required amount, not exact.

For example, let’s say that you want to determine the probability of getting at least 2 red balls and 1 green ball when you draw 5 balls from a hat containing 6 black, 4 red, and 3 green. To do this, we perform N experiments, count how many times M we get at least 2 red balls and 1 green ball, and estimate the probability as M/N

The draw method appears to be fine. Like I said, it was a maybe since I changed a lot of your code to debug the successful draw checks.

The only way to debug your code for detecting a good draw is to compare the expected draw, the actual draw, and whether your code thinks it’s good or not. That’s how I generated the output above with your code. I did the same thing again with your current code and using my known working good draw function to produce the following output from your code (snipped):

i think it's good
actually bad: exp: {'blue': 2, 'red': 1} draw: {'green': 2, 'blue': 2}
i think it's good
actually bad: exp: {'blue': 2, 'red': 1} draw: {'green': 2, 'red': 2}
i think it's good
actually bad: exp: {'blue': 2, 'red': 1} draw: {'green': 1, 'blue': 3}

So you’re still getting spurious results that would lead to high probabilities (your code thinks it’s good but it’s not). Interestingly, these bad results are happening when one required color is not present in the draw.

2 Likes

Thank you it passed, next time I should use print statements to search for expected verse results rather than assuming my results were correct. You were right, it was not checking if the color was completely missing, but only if the color had the correct amount. If it was missing, it was never checked and therefore assumed to be passing.

In the tests on line 27 it has the following test ball.

hat = prob_calculator.Hat(yellow=5,red=1,green=3,blue=9,test=1)

Why is this? This caused a error in my code since test was a unknown input. I added it to the tracking dictionary of potential balls to fix the problem, but is their a way I could’ve prevented this? A way to access all the arguments of a python function so it can handle a unlimited amount of ball types?

It’s kind of implied in the description in the readme that you need to be able to take any color of ball, which without specifying a color set means you need to take almost any identifier as a color. So instead of using a known set of arguments, you have to use the catch-alls, args and kwargs.

args is for arguments that don’t really have a name associated with them and get used in functions that add a list of numbers like sum(*args) and the function loops over everything in args (doesn’t have to be addition; can be any type of list processing). kwargs (key word arguments) is similar, but they are key-value pairs that get transformed into the kwargs dict in your function. So your constructor could be

def __init__(self, **kwargs):
    self.contents = []
    for k, v in kwargs.items():
        # do some stuff with colors k and numbers v
        # in retrospect, we should use color and number
        # instead of k and v but that's the convention
...

instead of

def __init__(self, red=0, orange=0, #...to the end of the 64 count box...):
    self.contents = []
...

Then, if your color is striped or test or bazinga, it doesn’t matter.

Lots of ink has been spilled on the web about python args and kwargs. They are ubiquitous in most python code.

1 Like