def build_uganda_populations(circus): seeder = seed_provider(12345) cells = circus.create_population(name="cells", ids_gen=SequencialGenerator(prefix="CELL_"), size=200) latitude_generator = FakerGenerator(method="latitude", seed=next(seeder)) cells.create_attribute("latitude", init_gen=latitude_generator) longitude_generator = FakerGenerator(method="longitude", seed=next(seeder)) cells.create_attribute("longitude", init_gen=longitude_generator) # the cell "health" is its probability of accepting a call. By default # let's says it's one expected failure every 1000 calls healthy_level_gen = build_healthy_level_gen(next(seeder)) cells.create_attribute(name="HEALTH", init_gen=healthy_level_gen) city_gen = FakerGenerator(method="city", seed=next(seeder)) cities = circus.create_population(name="cities", size=200, ids_gen=city_gen) cell_city_rel = cities.create_relationship("CELLS") cell_city_df = make_random_assign(cells.ids, cities.ids, next(seeder)) cell_city_rel.add_relations( from_ids=cell_city_df["chosen_from_set2"], to_ids=cell_city_df["set1"]) pop_gen = ParetoGenerator(xmin=10000, a=1.4, seed=next(seeder)) cities.create_attribute("population", init_gen=pop_gen) timer_config = CyclicTimerProfile( profile=[1, .5, .2, .15, .2, .4, 3.8, 7.2, 8.4, 9.1, 9.0, 8.3, 8.1, 7.7, 7.4, 7.8, 8.0, 7.9, 9.7, 10.4, 10.5, 8.8, 5.7, 2.8], profile_time_steps="1h", start_date=pd.Timestamp("6 June 2016 00:00:00")) return cells, cities, timer_config
def connect_dealers_to_distributors(self, dealers, distributors): # let's be simple: each dealer has only one provider distributor_rel = dealers.create_relationship("DISTRIBUTOR") state = np.random.RandomState(next(self.seeder)) assigned = state.choice(a=distributors.ids, size=dealers.size, replace=True) distributor_rel.add_relations(from_ids=dealers.ids, to_ids=assigned) # We're also adding a "bulk buy size" attribute to each dealer that defines # how many SIM are bought at one from the distributor. bulk_gen = ParetoGenerator(xmin=500, a=1.5, force_int=True, seed=next(self.seeder)) dealers.create_attribute("BULK_BUY_SIZE", init_gen=bulk_gen)
def add_uganda_geography(self, force_build=False): """ Loads the cells definition from Uganda + adds 2 stories to control """ logging.info(" adding Uganda Geography") seeder = seed_provider(12345) if force_build: uganda_cells, uganda_cities, timer_config = build_uganda_populations( self) else: uganda_cells = db.load_population(namespace="uganda", population_id="cells") uganda_cities = db.load_population(namespace="uganda", population_id="cities") timer_config = db.load_timer_gen_config("uganda", "cell_repair_timer_profile") repair_n_fix_timer = CyclicTimerGenerator( clock=self.clock, seed=next(self.seeder), config=timer_config) unhealthy_level_gen = build_unhealthy_level_gen(next(seeder)) healthy_level_gen = build_healthy_level_gen(next(seeder)) # tendency is inversed in case of broken cell: it's probability of # accepting a call is much lower # same profiler for breakdown and repair: they are both related to # typical human activity logging.info(" adding Uganda Geography6") cell_break_down_story = self.create_story( name="cell_break_down", initiating_population=uganda_cells, member_id_field="CELL_ID", timer_gen=repair_n_fix_timer, # fault activity is very low: most cell tend never to break down ( # hopefully...) activity_gen=ParetoGenerator(xmin=5, a=1.4, seed=next(self.seeder)) ) cell_repair_story = self.create_story( name="cell_repair_down", initiating_population=uganda_cells, member_id_field="CELL_ID", timer_gen=repair_n_fix_timer, # repair activity is much higher activity_gen=ParetoGenerator(xmin=100, a=1.2, seed=next(self.seeder)), # repair is not re-scheduled at the end of a repair, but only triggered # from a "break-down" story auto_reset_timer=False ) cell_break_down_story.set_operations( unhealthy_level_gen.ops.generate(named_as="NEW_HEALTH_LEVEL"), uganda_cells.get_attribute("HEALTH").ops.update( member_id_field="CELL_ID", copy_from_field="NEW_HEALTH_LEVEL"), cell_repair_story.ops.reset_timers(member_id_field="CELL_ID"), self.clock.ops.timestamp(named_as="TIME"), operations.FieldLogger(log_id="cell_status", cols=["TIME", "CELL_ID", "NEW_HEALTH_LEVEL"]), ) cell_repair_story.set_operations( healthy_level_gen.ops.generate(named_as="NEW_HEALTH_LEVEL"), uganda_cells.get_attribute("HEALTH").ops.update( member_id_field="CELL_ID", copy_from_field="NEW_HEALTH_LEVEL"), self.clock.ops.timestamp(named_as="TIME"), # note that both stories are contributing to the same # "cell_status" log operations.FieldLogger(log_id="cell_status", cols=["TIME", "CELL_ID", "NEW_HEALTH_LEVEL"]), ) return uganda_cells, uganda_cities
def add_communications(self, subs, sims, cells): """ Adds Calls and SMS story, which in turn may trigger topups story. """ logging.info("Adding calls and sms story ") # generators for topups and call duration voice_duration_generator = NumpyRandomGenerator(method="choice", a=range(20, 240), seed=next(self.seeder)) # call and sms timer generator, depending on the day of the week call_timegen = HighWeekDaysTimerGenerator(clock=self.clock, seed=next(self.seeder)) # probability of doing a topup, with high probability when the depended # variable (i.e. the main account value, see below) gets close to 0 recharge_trigger = DependentTriggerGenerator( value_to_proba_mapper=operations.logistic(k=-0.01, x0=1000), seed=next(self.seeder)) # call activity level, under normal and "excited" states normal_call_activity = ParetoGenerator(xmin=10, a=1.2, seed=next(self.seeder)) excited_call_activity = ParetoGenerator(xmin=100, a=1.1, seed=next(self.seeder)) # after a call or SMS, excitability is the probability of getting into # "excited" mode (i.e., having a shorted expected delay until next call excitability_gen = NumpyRandomGenerator(method="beta", a=7, b=3, seed=next(self.seeder)) subs.create_attribute(name="EXCITABILITY", init_gen=excitability_gen) # same "basic" trigger, without any value mapper flat_trigger = DependentTriggerGenerator(seed=next(self.seeder)) back_to_normal_prob = NumpyRandomGenerator(method="beta", a=3, b=7, seed=next(self.seeder)) # Calls and SMS stories themselves calls = self.create_story(name="calls", initiating_population=subs, member_id_field="A_ID", timer_gen=call_timegen, activity_gen=normal_call_activity, states={ "excited": { "activity": excited_call_activity, "back_to_default_probability": back_to_normal_prob } }) sms = self.create_story(name="sms", initiating_population=subs, member_id_field="A_ID", timer_gen=call_timegen, activity_gen=normal_call_activity, states={ "excited": { "activity": excited_call_activity, "back_to_default_probability": back_to_normal_prob } }) # common logic between Call and SMS: selecting A and B + their related # fields compute_ab_fields = Chain( self.clock.ops.timestamp(named_as="DATETIME"), # selects a B party subs.get_relationship("FRIENDS").ops.select_one(from_field="A_ID", named_as="B_ID", one_to_one=True), # fetches information about all SIMs of A and B subs.get_relationship("SIMS").ops.select_all(from_field="A_ID", named_as="A_SIMS"), sims.ops.lookup(id_field="A_SIMS", select={ "OPERATOR": "OPERATORS_A", "MSISDN": "MSISDNS_A", "MAIN_ACCT": "MAIN_ACCTS_A" }), subs.get_relationship("SIMS").ops.select_all(from_field="B_ID", named_as="B_SIMS"), sims.ops.lookup(id_field="B_SIMS", select={ "OPERATOR": "OPERATORS_B", "MSISDN": "MSISDNS_B" }), # A selects the sims and related values based on the best match # between the sims of A and B operations.Apply(source_fields=[ "MSISDNS_A", "OPERATORS_A", "A_SIMS", "MAIN_ACCTS_A", "MSISDNS_B", "OPERATORS_B", "B_SIMS" ], named_as=[ "MSISDN_A", "OPERATOR_A", "SIM_A", "MAIN_ACCT_OLD", "MSISDN_B", "OPERATOR_B", "SIM_B" ], f=select_sims), operations.Apply(source_fields=["OPERATOR_A", "OPERATOR_B"], named_as="TYPE", f=compute_cdr_type), ) # Both CELL_A and CELL_B might drop the call based on their current HEALTH compute_cell_status = Chain( # some static fields subs.ops.lookup(id_field="A_ID", select={ "CELL": "CELL_A", "EXCITABILITY": "EXCITABILITY_A" }), subs.ops.lookup(id_field="B_ID", select={ "CELL": "CELL_B", "EXCITABILITY": "EXCITABILITY_B" }), cells.ops.lookup(id_field="CELL_A", select={"HEALTH": "CELL_A_HEALTH"}), cells.ops.lookup(id_field="CELL_B", select={"HEALTH": "CELL_B_HEALTH"}), flat_trigger.ops.generate(observed_field="CELL_A_HEALTH", named_as="CELL_A_ACCEPTS"), flat_trigger.ops.generate(observed_field="CELL_B_HEALTH", named_as="CELL_B_ACCEPTS"), operations.Apply( source_fields=["CELL_A_ACCEPTS", "CELL_B_ACCEPTS"], named_as="STATUS", f=compute_call_status)) # update the main account based on the value of this CDR update_accounts = Chain( operations.Apply(source_fields=["MAIN_ACCT_OLD", "VALUE"], named_as="MAIN_ACCT_NEW", f=np.subtract, f_args="series"), sims.get_attribute("MAIN_ACCT").ops.update( member_id_field="SIM_A", copy_from_field="MAIN_ACCT_NEW"), ) # triggers the topup story if the main account is low trigger_topups = Chain( # A subscribers with low account are now more likely to topup the # SIM they just used to make a call recharge_trigger.ops.generate(observed_field="MAIN_ACCT_NEW", named_as="SHOULD_TOP_UP"), self.get_story("topups").ops.force_act_next( member_id_field="SIM_A", condition_field="SHOULD_TOP_UP"), ) # get BOTH sms and Call "bursty" after EITHER a call or an sms get_bursty = Chain( # Trigger to get into "excited" mode because A gave a call or sent an # SMS flat_trigger.ops.generate(observed_field="EXCITABILITY_A", named_as="A_GETTING_BURSTY"), calls.ops.transit_to_state(member_id_field="A_ID", condition_field="A_GETTING_BURSTY", state="excited"), sms.ops.transit_to_state(member_id_field="A_ID", condition_field="A_GETTING_BURSTY", state="excited"), # Trigger to get into "excited" mode because B received a call flat_trigger.ops.generate(observed_field="EXCITABILITY_B", named_as="B_GETTING_BURSTY"), # transiting to excited mode, according to trigger value calls.ops.transit_to_state(member_id_field="B_ID", condition_field="B_GETTING_BURSTY", state="excited"), sms.ops.transit_to_state(member_id_field="B_ID", condition_field="B_GETTING_BURSTY", state="excited"), # # B party need to have their time reset explicitally since they were # not active at this round. A party will be reset automatically calls.ops.reset_timers(member_id_field="B_ID"), sms.ops.reset_timers(member_id_field="B_ID"), ) calls.set_operations( compute_ab_fields, compute_cell_status, ConstantGenerator(value="VOICE").ops.generate(named_as="PRODUCT"), voice_duration_generator.ops.generate(named_as="DURATION"), operations.Apply(source_fields=["DURATION", "DATETIME", "TYPE"], named_as="VALUE", f=compute_call_value), update_accounts, trigger_topups, get_bursty, # final CDRs operations.FieldLogger(log_id="voice_cdr", cols=[ "DATETIME", "MSISDN_A", "MSISDN_B", "STATUS", "DURATION", "VALUE", "CELL_A", "OPERATOR_A", "CELL_B", "OPERATOR_B", "TYPE", "PRODUCT" ]), ) sms.set_operations( compute_ab_fields, compute_cell_status, ConstantGenerator(value="SMS").ops.generate(named_as="PRODUCT"), operations.Apply(source_fields=["DATETIME", "TYPE"], named_as="VALUE", f=compute_sms_value), update_accounts, trigger_topups, get_bursty, # final CDRs operations.FieldLogger(log_id="sms_cdr", cols=[ "DATETIME", "MSISDN_A", "MSISDN_B", "STATUS", "VALUE", "CELL_A", "OPERATOR_A", "CELL_B", "OPERATOR_B", "TYPE", "PRODUCT" ]), )
def add_agent_holidays_story(self, agents): """ Adds stories that reset to 0 the activity level of the purchases story of some populations """ logging.info("Adding 'holiday' periods for agents ") # TODO: this is a bit weird, I think what I'd need is a profiler that would # return duration (i.e timer count) with probability related to time # until next typical holidays :) # We could call this YearProfile though the internal mechanics would be # different than week and day profiler holiday_time_gen = HighWeekDaysTimerGenerator(clock=self.clock, seed=next(self.seeder)) # TODO: we'd obviously have to adapt those weight to longer periods # thought this interface is not very intuitive # => create a method where can can specify the expected value of the # inter-event interval, and convert that into an activity holiday_start_activity = ParetoGenerator(xmin=.25, a=1.2, seed=next(self.seeder)) holiday_end_activity = ParetoGenerator(xmin=150, a=1.2, seed=next(self.seeder)) going_on_holidays = self.create_story( name="agent_start_holidays", initiating_population=agents, member_id_field="AGENT", timer_gen=holiday_time_gen, activity_gen=holiday_start_activity) returning_from_holidays = self.create_story( name="agent_stops_holidays", initiating_population=agents, member_id_field="AGENT", timer_gen=holiday_time_gen, activity_gen=holiday_end_activity, auto_reset_timer=False) going_on_holidays.set_operations( self.get_story("purchases").ops.transit_to_state( member_id_field="AGENT", state="on_holiday"), returning_from_holidays.ops.reset_timers(member_id_field="AGENT"), # just for the logs self.clock.ops.timestamp(named_as="TIME"), ConstantGenerator(value="going").ops.generate(named_as="STATES"), operations.FieldLogger(log_id="holidays"), ) returning_from_holidays.set_operations( self.get_story("purchases").ops.transit_to_state( member_id_field="AGENT", state="default"), # just for the logs self.clock.ops.timestamp(named_as="TIME"), ConstantGenerator(value="returning").ops.generate( named_as="STATES"), operations.FieldLogger(log_id="holidays"), )