2
\$\begingroup\$

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
 self.graduated = False
 # 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)[0]
 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)[0]
 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)[0]
 self.birthday = random.randint(1, 365)
 if fresh:
 self.age = random.choices([14, 15, 16], weights=[0.10, 0.80, 0.10], k=1)[0]
 self.grade = "Freshman"
 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)[0]
 if self.age in [14, 15]:
 grade = random.choices(["Freshman", "Sophomore", "Junior"], weights=[0.88, 0.10, 0.02], k=1)[0]
 elif self.age == 16:
 grade = random.choices(["Freshman", "Sophomore", "Junior", "Senior"],
 weights=[0.10, 0.88, 0.10, 0.02], k=1)[0]
 elif self.age == 17:
 grade = random.choices(["Sophomore", "Junior", "Senior"], weights=[0.10, 0.80, 0.10], k=1)[0]
 else:
 grade = random.choices(["Junior", "Senior"], weights=[0.10, 0.90], k=1)[0]
 self.grade = grade
 if self.grade == "Freshman":
 assignedClass = random.choices(list(range(numberClasses)), weights=[1 / numberClasses] * (numberClasses),
 k=1)[0]
 elif self.grade == "Sophomore":
 assignedClass = random.choices(list(range(numberClasses, 2 * numberClasses)),
 weights=[1 / numberClasses] * (numberClasses), k=1)[0]
 elif self.grade == "Junior":
 assignedClass = random.choices(list(range(2 * numberClasses, 3 * numberClasses)),
 weights=[1 / numberClasses] * (numberClasses), k=1)[0]
 else:
 assignedClass = random.choices(list(range(3 * numberClasses, 4 * numberClasses)),
 weights=[1 / numberClasses] * (numberClasses), k=1)[0]
 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)[0]
 self.traits = [self.name, self.IEFactor, self.gender, self.ethnicity, self.income, self.age, self.grade, self.assignedClass,
 self.academics, self.graduated]
 def __hash__(self):
 return hash(str(self.name + self.academics + self.grade))
 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.classesPerGrade = 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)[0]
 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)[0]
 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)[0]
 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)[0]
 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)[0]
 if student.grade == "Freshman":
 student.grade == "Sophomore"
 student.assignedClass = \
 random.choices(list(range(self.classesPerGrade + 1, 2 * self.classesPerGrade + 1)),
 weights=[1 / self.classesPerGrade] * self.classesPerGrade - 1, k=1)[0]
 elif student.grade == "Sophomore":
 student.grade == "Junior"
 student.assignedClass = \
 random.choices(list(range(2 * self.classesPerGrade + 1, 3 * self.classesPerGrade + 1)),
 weights=[1 / self.classesPerGrade] * self.classesPerGrade - 1, k=1)[0]
 else:
 student.grade == "Senior"
 student.assignedClass = \
 random.choices(list(range(3 * self.classesPerGrade + 1, 4 * self.classesPerGrade + 1)),
 weights=[1 / self.classesPerGrade] * self.classesPerGrade - 1, k=1)[0]
 self.classRosters[student.assignedClass].append(student)
 newActiveRoster.append(student)
 else:
 student.grade == "Graduated"
 student.graduated = True
 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[3]), schoolSize=int(sys.argv[1]))
 print("Setting up interactions.")
 print(sys.argv)
 for i in range(int(sys.argv[2])):
 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

asked Feb 5, 2021 at 18:58
\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

In distance(x, y), the value max_dist is never used.

The function could be "simplified" to one line:

def distance(x, y):
 return sum(xi != yi for xi, yi in zip(x, y)) - 4

In class Classroom, the function roster(self) returns self.roster ... the function that was just called? This doesn't seem useful at all.

answered Feb 6, 2021 at 6:54
\$\endgroup\$
3
  • \$\begingroup\$ Thanks for the input, that was actually a good call. I thought I had properly scrubbed the code of relics, but it appears not. Thanks a lot. By the way, I've made some revisions to the code after editing since the behaviour appears to be bugged in some ways. May I edit the program presented above? \$\endgroup\$ Commented Feb 6, 2021 at 23:01
  • \$\begingroup\$ Editing your code after any answer has been posted would (likely) invalidate the answer(s), so is against the rules of this site. It would be better to post a new question. \$\endgroup\$ Commented Feb 7, 2021 at 2:21
  • \$\begingroup\$ Thanks. The edits weren't that severe anyway, just changing the probabilities of getting into particular incomes or classes. \$\endgroup\$ Commented Feb 7, 2021 at 3:00

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

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

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.