1
\$\begingroup\$

This is a basic simulation of an organization that fires the bottom 5th percentile of the workers at each level of the organization, and then promotes the lower levels to fill the empty positions.

The mesa imports are from a package that is developed for agent-based modeling in Python. You can install it with:

$ pip install mesa

Mesa documentation

In the FireBottom5 class definition, there is a method called fire.

The list comprehension:

mlevel_neighbors = [em for em in self.schedule.agents if em.level == m.level]

seems to be taking a long time to complete. Please help me understand what the performance issue is.

import itertools
from mesa import Agent, Model
from mesa.datacollection import DataCollector
from mesa.time import BaseScheduler
from operator import attrgetter
import numpy as np
from itertools import repeat
import random

RUN_YEARS = 200
RUN_TIMES = 20
ORG_LEVELS = 10



def GenBoundedRandomNormal(meanVal,stdDev,lowerBound,upperBound):
    aRand = random.gauss(meanVal,stdDev) # could also use: normalvariate()but gauss () is slightly faster.
    while (aRand < lowerBound or aRand > upperBound):
        aRand = random.gauss(meanVal,stdDev)
    return aRand

class Employee(Agent):

    def __init__(self, unique_id, model, **kwargs):
        super().__init__(unique_id, model)
        if kwargs:
            self.level = kwargs
            self.level = self.level["level"]
        else:
            self.level = 0
        self.effort = GenBoundedRandomNormal(.5, .2, .1, .9)
        self.ability = GenBoundedRandomNormal(.5, .2, .1, .9)
        self.firm_years = 0
        self.level_years = 0
        self.work_history = 0
        self.promotions = 0

    def retire(self):
        if self.firm_years > 40:
            self.model.schedule.remove(self)
            self.model.promote(self.level - 1)

    def update_effort(self):
        lvl_nes = [ne for ne in self.model.schedule.agents if ne.level == self.level]
        lvl_effort = [le.effort for le in lvl_nes]
        avg_lvl_effort = np.mean(lvl_effort)
        if self.effort < avg_lvl_effort:
            self.effort += (self.effort * .1)
        if self.effort > avg_lvl_effort:
            self.effort -= (self.effort * .1)

    def step(self):
        self.firm_years += 1
        self.level_years += 1
        self.work_history = self.work_history + (self.effort + self.ability)
        self.update_effort()
        self.retire()

def calc_avg_ability_top(model):
    eal_top = [ea.ability for ea in model.schedule.agents if ea.level == model.num_levels]
    return (np.sum(eal_top)/len(eal_top))

def calc_avg_ability_mid(model):
    eal_mid = [ea.ability for ea in model.schedule.agents if ea.level == (model.num_levels/2)]
    return (np.sum(eal_mid)/len(eal_mid))

def calc_avg_ability_low(model):
    eal_low = [ea.ability for ea in model.schedule.agents if ea.level == 1]
    return (np.sum(eal_low)/len(eal_low))

def calc_avg_effort_top(model):
    eel_top = [ee.effort for ee in model.schedule.agents if ee.level == model.num_levels]
    return (np.sum(eel_top)/len(eel_top))

def calc_avg_effort_mid(model):
    eel_mid = [ee.effort for ee in model.schedule.agents if ee.level == (model.num_levels/2)]
    return (np.sum(eel_mid)/len(eel_mid))

def calc_avg_effort_low(model):
    eel_low = [ee.effort for ee in model.schedule.agents if ee.level == 1]
    return (np.sum(eel_low)/len(eel_low))


class FireBottom5(Model):
    id_gen = itertools.count(1)

    def __init__(self, N):
        self.schedule = BaseScheduler(self)
        self.datacollector = DataCollector(
                model_reporters={"Top Level Ability":calc_avg_ability_top,
                                 "Mid Level Ability":calc_avg_ability_mid,
                                 "Low Level Ability":calc_avg_ability_low,
                                 "Top Level Effort":calc_avg_effort_top,
                                 "Mid Level Effort":calc_avg_effort_mid,
                                 "Low Level Effort":calc_avg_effort_low})

        #variable to set the number of levels in the organization
        #organization will have num_levels * num_levels positions
        self.num_levels = N
        #list of possible levels
        self.level_list = [x for x in range(1000)]
        #slice of possible levels skipping every other
        self.i = self.level_list[1::2]
        #list of numbers of each level, each index is a level
        self.multiply = self.i[:self.num_levels]
        """reverse the list so higher number is at beginning
        this makes it easier to determine the maximum number of years
        an agent can be in one position"""
        self.multiply.sort(reverse=True)
        #list of repeat objects that will repeat x when asked for next()
        self.repeats = [repeat(x) for x in self.level_list[1:len(self.multiply)+1]]
        #list for the organization pandas series index
        self.level_ref = []


        #for each repeat object in repeats list
        for x in self.repeats:
            #loop over range the size of x (size of x because multiply[x-1] = x)
            for y in range(self.multiply[next(x)-1]):
                #append x to index_ref list
                self.level_ref.append(next(x))
                del(y)

        for n in self.level_ref:
            employee = Employee(next(self.id_gen), self, level=n)
            self.schedule.add(employee)

    def fire(self):
        f_list = []
        for m in self.schedule.agents:
            mlevel_neighbors = [em for em in self.schedule.agents if em.level == m.level]
            neighbors_wh = [ew.work_history for ew in mlevel_neighbors]
            whp = np.percentile(neighbors_wh, 5)
            if m.work_history < whp:
                f_list.append(m)
            for ef in f_list:
                if ef.level == 1:
                    self.schedule.add(Employee(next(self.id_gen), self, level=1))
                if ef.level > 1:
                    continue
                    self.promote(ef.level-1)
                self.schedule.remove(ef)

    def promote(self, level):
        l = level
        while l > 0:
            p_list = [pe for pe in self.schedule.agents if pe.level == l]
            p_list.sort(key=attrgetter("work_history"), reverse=True)
            eprom = p_list[0]
            eprom.level += 1
            eprom.level_years = 0
            eprom.work_history = 0
            eprom.promotions += 1
            if l == 1:
                self.schedule.add(Employee(next(self.id_gen), self, level=1))
            l -= 1

    def step(self):
        self.schedule.step()
        self.fire()
        self.datacollector.collect(self)

    def run(self, run_years):
        for step in range(run_years):
            print(step)
            self.step()


class Sim(object):
    def __init__(self, run_times):
        self.run_times = run_times

    def run(self):
        for t in range(self.run_times):
            model = FireBottom5(ORG_LEVELS)
            model.run(RUN_YEARS)
            mvdf = model.datacollector.get_model_vars_dataframe()
            mvdf.to_csv("FireBottom5_NumLevels"+str(model.num_levels)+"Run"+str(t+1)+".csv")
            mvdf.plot()

if __name__ == '__main__':
    sim = Sim(RUN_TIMES)
    sim.run()
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.