9
\$\begingroup\$

I'm trying to do a simulation on the role of luck in human achievement.

Here is what I intend to do with my code:

  1. Create 20000 instances of people with randomly generated scores for skill, experience and hard-work(0 to 99), and a luck value(0 to 100).
  2. Create a total score for each person, with 95% of the average of the skill, experience and hard-work, and 5% of their luck value.
  3. Select the top 10 persons and record their luck value.
  4. Iterate the procedure a specified amount of times and find the total average luck of all the people who came top ten in all of the iterations.

Please review the code and tell me if my code is correctly written for the above purpose. I'm skeptical about the pseudo-randomness random library interfering with the results.

import random
class candidate :
    def __init__(self):
        self.skill = random.uniform(0,99)
        self.exp = random.uniform(0,99)
        self.work = random.uniform(0,99)
        self.luck = random.uniform(0,100)
        self.total_score = ((self.skill+self.exp+self.work)/3)*.95+self.luck*0.05

candidate_list = []
candidate_selected_lucks = []
def select():
    for i in range (0,20000):
        candidate_list.append(candidate())
    for m in range(0,10):
        _max = 0.0
        for j in range (0,len(candidate_list)):
            if (candidate_list[j].total_score > _max):
                _max = candidate_list[j].total_score
                index = j
        candidate_selected_lucks .append(candidate_list[index].luck)
        print(m,"."," Score : ", candidate_list[index].total_score, " Luck : ", candidate_list[index].luck,"\n")
        candidate_list.pop(index)

def find_avg(candidate_selected_lucks):
    return sum(candidate_selected_lucks)/len(candidate_selected_lucks)

def simulate(N):
    for k in range(0,N):
        print ("Iteration ",str(k+1),". ")
        select()
        candidate_list.clear()
    print ("average luck is", find_avg(candidate_selected_lucks),"\n")
        
count = int(input("Enter no of simulations\n"))
simulate(count)


    
\$\endgroup\$
1
  • 2
    \$\begingroup\$ Some "meta-advice" here; if you say luck is 5% of a person's score, you're probably going to find that the luck score varies drastically and has an average near 50. It may be more interesting to repeat this experiment with different weights for luck, in which case you wouldn't want to hard-code the ...*0.95 and ...*0.05 values either. I'm not fluent in Python, but as a step one I'm sure you could allow luckWeight to be configured for each instance of Candidate. \$\endgroup\$ Commented Jan 19, 2021 at 14:12

3 Answers 3

7
\$\begingroup\$

Python has an official style-guide, PEP8. It recommends using PascalCase for classes, so your class should be called Candidate. It also recommends using spaces after a , when passing multiple arguments to a function.

The creation of the candidates is as simple as this:

candidate_list = [Candidate() for _ in range(20000)]

Note that the starting index of range is 0 by default and that I used the customary variable name _ for the unused loop variable.

This also removes having to clear the candidates list, since it is being created anew every time you call the select function.

To get the candidate with the largest score, just use:

best_candidate = max(candidate_list, key=lambda c: c.total_score)

This does not allow you to easily remove them from the list of candidates, though. For this you can use enumerate:

index, candidate = max(enumerate(candidate_list), key=lambda t: t[1].total_score)

However, since you are running a not quite small numerical simulation, you might want to look into using numpy, which would greatly speed up the calculations. Instead of having a Candidate class, just create arrays. Generating random numbers in bulk is vastly faster than doing it one at a time as well:

import numpy as np

iterations = 100
candidates = 20000
top_ten_luck = []

for _ in range(iterations):
    scores = np.random.randint(0, 99, size=(candidates, 3))
    luck = np.random.randint(0, 100, size=candidates)
    total_score = 0.95 * scores.mean(axis=1) + 0.05 * luck
    top_ten = np.argpartition(-total_score, 10)[:10]
    top_ten_luck.extend(luck[top_ten])

print(np.mean(top_ten))

This runs in less than a second (132ms on my machine, to be exact). The for loop can also be vectorized, but at that point you might run into memory constraints.

Note that I used numpy.argpartition to get the indicies of the top ten candidates, in order to only partially sort the data as much as needed and not further. We need to use -total_score because it only gives you minimal values. This should be both faster than poping the maximum value ten times as well as completely sorting the list.

\$\endgroup\$
4
  • \$\begingroup\$ Thank you for you input. \$\endgroup\$
    – User
    Commented Jan 19, 2021 at 2:38
  • 1
    \$\begingroup\$ May I ask what technique did you used to measure the time of execution? Did you use the time library? How accurate would it be? An interesting thing I found is that the average luck score converges around 70 to 75%, I ran 100, 1000, 10000, and 100,000 iterations. I never expected this result. Could this have to do with the pseudo randomness of the python random library? \$\endgroup\$
    – User
    Commented Jan 19, 2021 at 3:00
  • 1
    \$\begingroup\$ @ASWINVENU I used an interactive python terminal (ipython) and there the %timeit magic command. It reported the result as 132 ms ± 1.33 ms per loop (mean ± std. dev. of 7 runs, 10 loops each). \$\endgroup\$
    – Graipher
    Commented Jan 19, 2021 at 8:12
  • 1
    \$\begingroup\$ @ASWINVENU As to why that seems to be the limiting value, one would have to do some math. You might want to ask this question on one of our sister sites (Cross Validated might be the right place for this). But it will almost certainly not have anyhing to do with the random library. numpy uses a different one and I get similar results. \$\endgroup\$
    – Graipher
    Commented Jan 19, 2021 at 8:13
5
\$\begingroup\$
  • candidate class should be named Candidate
  • find_avg() is already covered by the built-in statistics.mean()
  • candidates can be created by a list comprehension
  • global variables such as candidate_list and candidate_selected_lucks can be replaced with return values
import random
from statistics import mean

class Candidate :
    def __init__(self):
        self.skill = random.uniform(0,99)
        self.exp = random.uniform(0,99)
        self.work = random.uniform(0,99)
        self.luck = random.uniform(0,100)
        self.total_score = ((self.skill+self.exp+self.work)/3)*.95+self.luck*0.05

def generate_winners():
    candidates = [Candidate() for _ in range(0,20000)]
    candidates_by_luck = sorted(candidates,
        key=lambda candidate: candidate.luck, reverse=True)
    winners = candidates_by_luck[:10]
    for idx, winner in enumerate(winners):
        print(f"{idx}. Score : {winner.total_score} Luck : {winner.luck}")
        print()
    return winners

def simulate(N):
    for k in range(0,N):
        print ("Iteration ",str(k+1),". ")
        winners = generate_winners()
    avg_luck = mean(winner.luck for winner in winners)
    print ("average luck is", avg_luck,"\n")
        
count = int(input("Enter no of simulations\n"))
simulate(count)
\$\endgroup\$
1
  • \$\begingroup\$ Appreciate that. \$\endgroup\$
    – User
    Commented Jan 19, 2021 at 2:38
0
\$\begingroup\$

No one else has mentioned: Taking the mean of three uniformly random variables from 0-99 is the same as just generating one uniformly random variable from 0-99 (excepting fractional remainders).

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.