Probability Calculator Project Help

I think I did the project correctly even though the test for the experiment function fails.

In Replit I always get 0.293, which fails due to their expected outcome of 0.272. When I run the exact code in Jupyter Notebook, sometimes I get the number very close to 0.272, sometimes an exact match. I get a range of numbers from approximately 0.260-0.294. Which to me makes sense, since drawing at random will not yield the same results everytime(like the project ReadMe notes). I’m open to suggestions for anything I may have missed or improvements to code.

Code:

import copy
import random
# Consider using the modules imported above.

class Hat:
    
    def __init__(self,**kwargs):
        self.kwargs = kwargs
        self.contents = []
        
        
        for key,value in self.kwargs.items():
            for i in range(0,value):
                self.contents.append(key)
               
    
    def draw(self, num_balls):
        
        self.content_copy = self.contents.copy() 
        
        if num_balls > len(self.contents):
            return(self.contents)
        
        else:
            draws = []
            
            
            for x in range(num_balls):
                draws.append(self.contents.pop(random.randrange(len(self.contents))))
            
           
            return(draws)
        
        
      

def experiment(hat, expected_balls, num_balls_drawn, num_experiments):
    
    #track the number of times an experiment resulted in having all the expected balls drawn
    count = 0
    
    
    
    for x in range(0,num_experiments):
        
        #set up empty dictionary to track balls that are drawn
        drawn = {}
        
        placeholder = copy.deepcopy(expected_balls)
        
        draw = hat.draw(num_balls_drawn)

        hat.contents = hat.content_copy
        
        #iterate through the drawn balls list and store the number of times drawn in drawn dictionary
        for y in draw:
            if y in drawn:
                drawn[y] += 1
            else:
                drawn[y] = 1
        
        #iterate through the keys of drawn, check if the value in drawn is equal to or greater than the amount in placeholder
        for i in list(placeholder.keys()):
        
            #if the amount drawn for the color is 
            if i in drawn:
                if drawn[i] >= placeholder[i]:
                    placeholder[i] = True
                else:
                    placeholder[i] = False
                    
            else:
                continue
        
        if set(list(placeholder.values())) == {True}:
            count += 1
        
        else:
            continue
            
    return(count/num_experiments)

Tests are using specific seed number, to ensure random values from random module are repeatable during testing and therefore it’s possible to test if result is as expected. This means, with the seed number used in testing function returned 0.293, while expected answer is 0.272.

I haven’t looked enough at the code, to be sure where is the problem, but often there are discrepancies between specification and how draw method is handling some edge cases (ie. when number of balls to draw is higher than number of balls in hat).

Something funny is going on with your copy. I would copy the hat and draw from the copy.

The placeholder is to keep track of how the drawn balls compare to expected_balls, without changing the original contents of expected_balls.

I set the hat.contents to the hat.content_copy after the draw to restore the contents to the original state, for the next experiment. Before I added this code, I noticed the contents were different in each experiment, due to the draw function removing the object drawn from the contents list.

You are misusing the copy. Like I said, you need to copy the entire hat on each test and draw from the copy.

I believe I accounted for the edge case in my draw method. It simply returns all the contents if the number of balls to draw is greater than the number of items in contents.

Your logic is pretty complex here - much more than it needs to be.

For a test, the instructions say

  1. copy the hat
  2. draw from the copied hat
  3. check if the draw has the expected balls

You should not copy the expected balls.

You should not modify the internal contents of the hat from outside of the hat object.

Some issues I spotted:

  • There’s no need to assign kwargs as an instance attribute when it’s never used outside of __init__()
  • There’s no need to specify 0 as the start for range(), it’s the default
  • As stated above, change+restore would be better accomplished by making a (deep) copy of hat
  • When not actually using the iteration variable, replace it with an underscore _ to make that clear
  • dict.keys() can be iterated over directly, no need to convert to a list first
if set(list(placeholder.values())) == {True}:

This line is probably the source of your test failure. My guess is that because 0 == False and 1 == True in Python, something like {1, True} is being evaluated as equal to {True}, causing a false positive.

Thank you all for the suggestions! You have truly been helpful and the project is now officially done. I have incorporated some of the changes you have suggested.

-copied the entire hat object and drew from copied hat
-used “_” when not using iteration variable
if set(list(placeholder.values())) == {True}:
was setting off false positives when I ran a debug. great catch.

I ended up replacing the True with “Yes” and False with “No” instead. That took care of the false positives.

Well done! For reference, here’s my solution:

class Hat:
    def __init__(self, **kwargs):
        self.contents = []

        for ball_type, num in kwargs.items():
            self.contents.extend([ball_type] * num)

    def draw(self, num_balls_drawn):
        if num_balls_drawn > len(self.contents):
            return self.contents[:]
        else:
            return [
                self.contents.pop(random.randrange(len(self.contents)))
                for _ in range(num_balls_drawn)
            ]

def experiment(hat, expected_balls, num_balls_drawn, num_experiments):
    num_matches = 0

    for _ in range(num_experiments):
        drawn_balls = copy.deepcopy(hat).draw(num_balls_drawn)
        match = all(
            drawn_balls.count(ball_type) >= num_expected
            for ball_type, num_expected in expected_balls.items()
        )
        if match:
            num_matches += 1

    return num_matches/num_experiments

wow that match = all() really saves alot of time and memory. very good to note going forward.
Thanks