Improving performance for social network simulation program

I'm an undergraduate student with little to no experience in formal computer science or coding, and I specialise in quantitative social science research. Our professor asked us to fabricate some network data to analyse because of the pandemic, and I asked permission to create a Python program that simulates social networks. I successfully created one, but it runs painfully slow for larger groups and longer time periods. I'd like to ask for help with removing inefficiencies, of which I'm sure there are plenty in the code. Any help would be appreciated, and thanks for the assistance.

import sys
import names
import random
import math
import numpy as np
import pandas as pd
from statistics import mean

# Logistic function to bind any inputs to output between [0, 1]
def exp(x):
return math.exp(x) / (1 + math.exp(x))

# Given two lists of values and two lists of maximum and minimum possible values, output a value based on distance
# based on dissimilarity of traits.
def distance(x, y):
max_dist = len(x)

dist = 0
for i in range(len(x)):
if x[i] != y[i]:
dist += 1

return dist - 4

class Student:

def __init__(self, name, studentID, schoolSize, numberClasses, fresh=False):
# Name, classID, and StudentID.  Student ID also functions as position in class roster.
self.name = name
self.ID = studentID

# Probability density function for chance you interact with other people in the class/generally.  Probability of
# interacting with others is zero.
# self.classPDF = []
# self.interactionPDF = [1 / (schoolSize - 1)] * schoolSize
self.classPDF = {}
self.interactionPDF = {studentID: 0}

# Introversion-extraversion factor, which is a random number from 0 to 1 that determines your chance
# at getting an extra chance to interact with people and how many tries you get to meet new people.
self.IEFactor = np.random.normal(loc=0.5, scale=0.1)
while self.IEFactor > 1 or self.IEFactor < 0:
self.IEFactor = np.random.normal(loc=0.5, scale=0.1)

# Trackers, which track total, successful, and failed interactions for each classmate.
# Final list tracks sentiment, which represents the strength and direction of a relationship.
self.interactionTracker = {}
self.successInteractions = {}
self.failedInteractions = {}
self.sentimentTracker = {}

# Various traits, drawn from UK census definitions.
self.gender = \
random.choices(["Male", "Female", "Trans Male", "Trans Female"], weights=[0.49, 0.49, 0.01, 0.01], k=1)
self.ethnicity = random.choices(["Asian", "Black", "Mixed", "White British", "White Other", "Other"],
weights=[0.185, 0.133, 0.05, 0.449, 0.149, 0.034], k=1)
self.income = random.choices(["Managerial, Administrative", "Intermediate", "Small Employers/Freelancers",
"Lower Supervisory/Technical", "Semi-Routine/Routine",
"Unemployed/Full-Time Students"],
weights=[0.304, 0.13, 0.093, 0.072, 0.259, 0.141], k=1)
self.birthday = random.randint(1, 365)

if fresh:
self.age = random.choices([14, 15, 16], weights=[0.10, 0.80, 0.10], k=1)
else:
self.age = random.choices([14, 15, 16, 17, 18, 19], weights=[0.025, 0.225, 0.225, 0.225, 0.225, 0.025],
k=1)
if self.age in [14, 15]:
grade = random.choices(["Freshman", "Sophomore", "Junior"], weights=[0.88, 0.10, 0.02], k=1)
elif self.age == 16:
grade = random.choices(["Freshman", "Sophomore", "Junior", "Senior"],
weights=[0.10, 0.88, 0.10, 0.02], k=1)
elif self.age == 17:
grade = random.choices(["Sophomore", "Junior", "Senior"], weights=[0.10, 0.80, 0.10], k=1)
else:
grade = random.choices(["Junior", "Senior"], weights=[0.10, 0.90], k=1)

assignedClass = random.choices(list(range(numberClasses)), weights=[1 / numberClasses] * (numberClasses),
k=1)
assignedClass = random.choices(list(range(numberClasses, 2 * numberClasses)),
weights=[1 / numberClasses] * (numberClasses), k=1)
assignedClass = random.choices(list(range(2 * numberClasses, 3 * numberClasses)),
weights=[1 / numberClasses] * (numberClasses), k=1)
else:
assignedClass = random.choices(list(range(3 * numberClasses, 4 * numberClasses)),
weights=[1 / numberClasses] * (numberClasses), k=1)

self.assignedClass = assignedClass

self.academics = random.choices(["A+", "A-", "B+", "B-", "C+", "C-", "D", "F"],
weights=[0.05, 0.05, 0.20, 0.20, 0.20, 0.20, 0.05, 0.05],
k=1)

self.traits = [self.name, self.IEFactor, self.gender, self.ethnicity, self.income, self.age, self.grade, self.assignedClass,

def __hash__(self):

def __eq__(self, other):
return self.ID == other.ID

def __str__(self) -> str:
return [self.name, self.ID]

def __repr__(self) -> str:
return self.name + " -> " + str(self.ID)

# Class object for classroom.
class Classroom:

def __init__(self, numberClasses, schoolSize):
self.activeRoster = []
self.passiveRoster = []
self.classRosters = [[]] * (4 * numberClasses)
self.size = len(self.activeRoster)
self.day = 0;

for i in range(schoolSize):
student = Student(names.get_full_name(), i, schoolSize = schoolSize, numberClasses = numberClasses)
self.activeRoster.append(student)
self.passiveRoster.append(student)
self.classRosters[student.assignedClass].append(student)
student.classID = len(self.classRosters[student.assignedClass]) - 1

for student in self.activeRoster:
for other_student in self.activeRoster:
student.sentimentTracker[other_student] = 0
student.failedInteractions[other_student] = 0
student.successInteractions[other_student] = 0
student.interactionTracker[other_student] = 0
if student == other_student:
student.interactionPDF[other_student] = 0
student.classPDF[other_student] = 0
else:
student.interactionPDF[other_student] = 1 / (len(self.activeRoster) - 1)
student.classPDF[other_student] = 1 / (len(self.classRosters[student.assignedClass]) - 1)

def roster(self):
return self.roster

def dayElapses(self):

interaction_limit = {}
for student in self.activeRoster:
interaction_limit[student] = int(student.IEFactor * 20)

for classRoster in self.classRosters:
for student in classRoster:
print(student.name + " is in class!")
interactions_remaining = random.choices([1, 2], weights=[1 - student.IEFactor, student.IEFactor], k=1)[
0]
while interactions_remaining > 0:
self.conversation(student, classRoster, student.classPDF, interaction_limit)
interactions_remaining -= 1

for student in self.activeRoster:
interactions_remaining = random.choices([1, 2], weights=[1 - student.IEFactor, student.IEFactor], k=1)
print(student.name + " is on break!")
while interactions_remaining > 0:
self.conversation(student, self.activeRoster, student.interactionPDF, interaction_limit)
interactions_remaining -= 1

self.statusChanges(self.day)
self.day += 1

def conversation(self, student, roster, PDF, interaction_limit):

partner = self.discover(student, roster, PDF, interaction_limit)
if partner is None:
return

self.interact(student, partner)

def discover(self, student, roster, PDF, interaction_limit):

try_limit = int(student.IEFactor * 10)
partner = random.choices(list(PDF.keys()), weights=list(PDF.values()), k=1)
print("Wanna talk, " + partner.name + "?")
while partner not in roster or interaction_limit[partner] <= 0 or partner.ID == student.ID:
print("I guess not.")
partner = random.choices(list(PDF.keys()), weights=list(PDF.values()), k=1)
print("Wanna talk, " + partner.name + "?")
try_limit -= 1
if try_limit == 0:
return None

interaction_limit[student] -= 1
interaction_limit[partner] -= 1
return partner

def interact(self, student, partner, fam=0.0005, dec=1, discrim=5):

print(distance(student.traits, partner.traits))
print(student.sentimentTracker[partner])
chance_of_success = exp(student.sentimentTracker[partner] + partner.sentimentTracker[student] * (distance(student.traits, partner.traits) / 4))
chances_of_success = [1 - chance_of_success, chance_of_success]
print(chances_of_success)
interaction_status = random.choices([0, 1], weights=chances_of_success, k=1)

if interaction_status == 0:
print("That didn't go so well.")
student.interactionTracker[partner] += 1
student.failedInteractions[partner] += 1
student.interactionPDF[partner] -= student.interactionPDF[partner] * exp(self.size)
student.sentimentTracker[partner] -= 1

partner.interactionTracker[student] += 1
partner.failedInteractions[student] += 1
partner.interactionPDF[student] -= partner.interactionPDF[student] * exp(self.size)
partner.sentimentTracker[student] -= 1

else:
print("That went great!")
student.successInteractions[partner] += 1
student.interactionTracker[partner] += 1
student.interactionPDF[partner] += discrim + exp(self.size)
student.sentimentTracker[partner] += 1

partner.interactionTracker[student] += 1
partner.failedInteractions[student] += 1
partner.interactionPDF[student] += partner.interactionPDF[student] * exp(self.size)
partner.sentimentTracker[student] += 1

student_sum = sum(student.interactionPDF.values())
other_sum = sum(partner.interactionPDF.values())
for k, v in student.interactionPDF.items():
student.interactionPDF[k] = v/student_sum
for k, v in partner.interactionPDF.items():
partner.interactionPDF[k] = v/other_sum

for student in self.activeRoster:
for student in student.sentimentTracker.keys():
if student.sentimentTracker[student] > 1:
student.sentimentTracker[student] -= 1 * dec
elif student.sentimentTracker[student] < 1:
student.sentimentTracker[student] += 2 * dec

def statusChanges(self, day):

for student in self.passiveRoster:
if student.birthday == day or (day - student.birthday) % 365 == 0:
student.age += 1

if day != 0 and day % 365 == 0:
self.yearChanges()

def yearChanges(self):

newActiveRoster = []
self.classRosters = [[]] * (4 * self.classesPerGrade)

# Promoting all students and changing their classes, if they aren't seniors.
# Graduating seniors.  Adding new freshmen.  Removing seniors from active and class rosters.

for student in self.activeRoster:
if student.grade in ["Freshman", "Sophomore", "Junior"]:
student.academics = self.academics = random.choices(["A+", "A-", "B+", "B-", "C+", "C-", "D", "F"],
weights=[0.05, 0.05, 0.20, 0.20, 0.20, 0.20, 0.05,
0.05],
k=1)

student.assignedClass = \
student.assignedClass = \
else:
student.assignedClass = \

self.classRosters[student.assignedClass].append(student)
newActiveRoster.append(student)

else:

new_students = (len(self.passiveRoster) / 4) + \
random.randint(int(-len(self.passiveRoster) / 16), int(len(self.passiveRoster) / 16))
id = len(self.passiveRoster) - 1
for i in range(new_students):
student = Student(names.get_full_name(), id, self.size, self.classesPerGrade, fresh=True)
self.activeRoster.append(student)
self.passiveRoster.append(student)
self.classRosters[student.assignedClass].append(student)
for other_student in self.activeRoster:
if student.ID == other_student.ID:
student.interactionPDF[other_student] = 0
student.classPDF[other_student] = 0
student.sentimentTracker[other_student] = 0
student.failedInteractions[other_student] = 0
student.successInteractions[other_student] = 0
student.interactionTracker[other_student] = 0
else:
student.interactionPDF[other_student] = 1 / (len(self.activeRoster) - 1)
other_student.interactionPDF[student] = mean(other_student.interactionPDF.values())

student.sentimentTracker[other_student] = 0
student.failedInteractions[other_student] = 0
student.successInteractions[other_student] = 0
student.interactionTracker[other_student] = 0

other_student.sentimentTracker[student] = 0
other_student.failedInteractions[student] = 0
other_student.successInteractions[student] = 0
other_student.interactionTracker[student] = 0

if other_student in self.classRosters[student.assignedClass]:
student.classPDF[other_student] = 1 / (len(self.classRosters[student.assignedClass]) - 1)
other_student.classPDF[student] = mean(student.classPDF.values())

def main():
# Check command line arguments
if len(sys.argv) != 4:
sys.exit("Usage: python sim.py class_size number_of_days classes_per_grade")

print("Generating classroom.")
working_classroom = Classroom(numberClasses=int(sys.argv), schoolSize=int(sys.argv))

print("Setting up interactions.")
print(sys.argv)
for i in range(int(sys.argv)):
working_classroom.dayElapses()

root_list = []
supplementary_list = []

for student in working_classroom.passiveRoster:
root_list.append(list(student.sentimentTracker.values()))
supplementary_list.append(student.traits)

root_list = np.array(root_list)
print(root_list)
print(len(root_list))

df = pd.DataFrame(data=root_list)
df.to_csv('output.csv')

supplementary_list = pd.DataFrame(data=supplementary_list)
supplementary_list.to_csv('supplementary.csv')

if __name__ == '__main__':
main()

The code above produces an adjacency matrix for social network analysis in R, as well as a supplementary set of information on the network participants. While I am primarily looking for ways to speed up the programme for large datasets, opinions on the structure of the programme are also appreciated. This is my first time coding such a project independently, and so I look forward to learning from your criticism. Thanks for the assistance.

Sincerely, Charles