Exemple #1
0
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
Exemple #2
0
    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)
Exemple #3
0
    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
Exemple #4
0
    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"
                                   ]),
        )
Exemple #5
0
    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"),
        )