Scientific Computing with Python Projects - Probability Calculator

Tell us what’s happening:
Describe your issue in detail here.

My code outputs a different probability each time it is ran, as I believe it should, but it never matches 0.272, which is the probability output of the last of the 3 tests my code is ran against. I haven’t added a random seed, as the directions instruct, but after refactoring my code a few times, I’ve come to a complete block. Can anyone see any discrepancies in the ‘experiment’ function, or anywhere else in my code that would through the probability off? It seems to pass all other tests except the last one. Any help would be greatly appreciated!

Your code so far

import copy
import random
import sys

sys.setrecursionlimit(3500)


class Hat:

  def __init__(self, **kwargs) -> None:
    self.contents = []

    if kwargs == {}:
      kwargs["black"] = 4

    for k, v in kwargs.items():
      for i in range(0, v):
        self.contents.append(k)

  def __str__(self) -> str:
    return f'{self.contents}'

  def draw(self, draw_count):
    if draw_count > len(self.contents):
      print(f'draw count exceeds amount of balls in hat ***** {self.contents}')
      return self.contents
    else:
      balls_drawn = []
      for i in range(0, draw_count):
        balls_taken = self.contents.pop(random.randrange(
            0, len(self.contents)))
        balls_drawn.append(balls_taken)

      return balls_drawn


actual_balls = {}
match_count = 0
experiment_count = 0


def experiment(hat, expected_balls, num_balls_drawn, num_experiments):
  global match_count
  global experiment_count

  if num_experiments < 1:
    print(
        f'Found: {actual_balls} {match_count}X\nexpected_balls: {expected_balls}\nmatch_count: {match_count}/{experiment_count}'
    )
    probability = match_count / experiment_count
    print(f'probability: {probability}')
    return probability
  else:
    # Using copy.deepcopy as to not affect the original instance
    hat_copy = copy.deepcopy(hat)
    balls_drawn = hat_copy.draw(num_balls_drawn)

    for k, v in expected_balls.items():
      if balls_drawn.count(k) >= v:
        actual_balls[k] = v

    if actual_balls == expected_balls:
      match_count += 1

    experiment_count += 1
    experiment(hat,
               expected_balls,
               num_balls_drawn,
               num_experiments=num_experiments - 1)


hat1 = Hat(blue=3, red=2, green=6)
#hat1.draw(4)
experiment(hat=hat1,
           expected_balls={
               "blue": 2,
               "green": 1
           },
           num_balls_drawn=4,
           num_experiments=1000)

Your browser information:

User Agent is: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36

Challenge: Scientific Computing with Python Projects - Probability Calculator

Link to the challenge:

1 Like

Consider if it’s possible to use the experiment function more than once - whether after finishing one experiment (completely, with xxx num_experiments), it’s possible to use experiment again without having the second result affected by first experiment.

1 Like

Hey @sanity , could you go further in detail? I thought the recursive function I have here uses the experiment function multiple times until all experiments have been ran. This experiment function begins with the same hat each time. Is this what you mean?

1 Like

That’s one full experiment - running all required num_experiments of individual experiments.

I’m talking about running multiple full experiments, with their own requirements (hat, expected_balls, num_balls_drawn, num_experiments).

1 Like

I don’t understand. The experiments ran are based on the num_experiments, so it must be ran multiple times. Sorry, I’m just having a hard time with this concept @sanity

1 Like

Assume experiment will be called twice ie.:

hat1 = Hat(blue=3, red=2, green=6)
experiment(hat=hat1,
           expected_balls={
               "blue": 2,
               "green": 1
           },
           num_balls_drawn=4,
           num_experiments=1000)

hat2 = Hat(blue=6, red=3, green=2)
experiment(hat=hat2,
           expected_balls={
               "blue": 1,
               "green": 2
           },
           num_balls_drawn=4,
           num_experiments=1000)

Right now result of the second call will be affected by the first one. It’s not possible to use the function multiple times and get accurate results.

1 Like

If the probability is supposed to be determined from a single hat, ran through multiple experiments, why would we create a second hat (hat2) ? How does the first hat affect the second hat when they’re two different hats with different amounts of each ball?

For example, if you want to determine the probability of getting at least two red balls and one green ball when you draw five balls from a hat containing six black, four red, and three green. To do this, you will perform N experiments, count how many times M you get at least two red balls and one green ball, and estimate the probability as M/N. Each experiment consists of starting with a hat containing the specified balls, drawing several balls, and checking if you got the balls you were attempting to draw.

Two things stuck out to me here in the instructions. It said “a” hat. So I took that to mean only one. Could you elaborate please?

1 Like

Note - 98% of the time, recursion is more trouble than its worth and you probably don’t want to use it.

You aren’t doing “true” recursion but instead using “faux” recursion with global variables, which breaks your function’s re-usability. This is causing all sorts of problems for you.

1 Like

One experiment is supposed to use single hat, yes. However that doesn’t mean the purpose of the function will end after a single use. It should be possible to use it multiple times, without each run affecting others.

1 Like

@JeremyLT that makes perfect sense. I have trouble with recursive functions, so I thought I’d try my hand at it in this project as practice. What is “faux” recursion by the way? Also, thanks for responding

1 Like

@sanity I think I understand, to some extent. So one experiment, done 1000 times, will use a single hat, correct? And a second hat, would be used 1000 times as well if called after the first hat

1 Like

Faux means you are “faking” recursion. You are making multiple function calls, but you are ignoring the return values and muddying your scope by putting all results in the global variable space.

This means in your code you are only ever allowed to call the experiment function once, but the tests call this function multiple times.

2 Likes

@JeremyLT Is there a way my code would still work recursively, or is it pretty much a dead end trying it this way?

1 Like

You can make it work with true recursion (no global variables) if you really want to - though this is not a problem I’d pick recursion for.

1 Like

Ok, I figured it out. I revamped my code to use a for loop instead, and then, outside of the second for loop, but within the first for loop (that loops thru the num_experiments), I incremented the match count only if the balls_found dictionary matched the expected_balls dictionary. This way, it matches the two dictionaries after successfully looping through all balls in the expected_balls .

This would have worked in the recursive function as well, but as you said, it’s definitely overkill lol. Thanks for all the help y’all @JeremyLT @sanity !

import copy
import random

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

        if kwargs == {}:
            kwargs["black"] = 4

        for k,v in kwargs.items():
            for i in range(0, v):
                self.contents.append(k)

    def __str__(self) -> str:
        return f'{self.contents}'

    def draw(self, draw_count):
        if draw_count > len(self.contents):
            print(f'draw count exceeds amount of balls in hat ***** {self.contents}')
            return self.contents
        else: 
            balls_drawn = []
            for i in range(0, draw_count):
                balls_taken = self.contents.pop(random.randrange(0, len(self.contents)))
                balls_drawn.append(balls_taken)

            return balls_drawn

def experiment(hat, expected_balls, num_balls_drawn, num_experiments):
    match_count = 0
    balls_found = {}
    print(f'Expected balls ---> {expected_balls}\n')
    for i in range(0, num_experiments):
        hat_copy = copy.deepcopy(hat)
        balls_drawn = hat_copy.draw(num_balls_drawn)
        for k,v in expected_balls.items():
            if balls_drawn.count(k) >= v:
                balls_found[k] = v
            else: 
                balls_found[k] = balls_drawn.count(k)
        print(f'Balls found: {balls_found}')
        match_count += 1 if balls_found == expected_balls else 0
        

    print(f'\nmatch_count: {match_count}/{num_experiments}\nprobability: {match_count/num_experiments}')
    return match_count/num_experiments

hat1 = Hat(blue=3,red=2,green=6)

experiment(
    hat=hat1,
    expected_balls={"blue":2,"green":1},
    num_balls_drawn=4,
    num_experiments=4
)```
3 Likes