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

\$\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.

\$\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\$
    – C. Babbage
    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\$
    – AJNeufeld
    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\$
    – C. Babbage
    Feb 7, 2021 at 3:00

Your Answer

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

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