def create_portfolio(): offer_channel = Categorical(('web', 'email', 'mobile', 'social'), (1, 1, 1, 0)) offer_type = Categorical(('bogo', 'discount', 'informational'), (1, 0, 0)) discount_a = Offer(0, valid_from=0, valid_until=4 * 7 * 24, difficulty=5, reward=5, channel=offer_channel, offer_type=offer_type) offer_channel = Categorical(('web', 'email', 'mobile', 'social'), (1, 1, 1, 1)) offer_type = Categorical(('bogo', 'discount', 'informational'), (0, 1, 0)) discount_b = Offer(0, valid_from=0, valid_until=4 * 7 * 24, difficulty=5, reward=2, channel=offer_channel, offer_type=offer_type) portfolio = (discount_a, discount_b) return portfolio
def setUp(self): self.world = World(real_time_tick=0.200) person_0 = Person('20170101') person_1 = Person('20170202') person_2 = Person('20170707') offer_a = Offer(0) offer_b = Offer(1) self.delimiter = '|' deliveries_path = 'data/delivery' self.deliveries_file_name = 'data/test_deliveries.csv' self.deliveries = [(person_0.id, offer_a.id), (person_1.id, offer_a.id), (person_2.id, offer_b.id)] with open(self.deliveries_file_name, 'w') as deliveries_file: for delivery in self.deliveries: print >> deliveries_file, self.delimiter.join( map(str, delivery)) self.population = Population( self.world, people=(person_0, person_1, person_2), portfolio=(offer_a, offer_b), deliveries_path=deliveries_path, transcript_file_name='data/transcript.json')
def create_portfolio(): offer_a = Offer(0) offer_b = Offer(1) portfolio = (offer_a, offer_b) return portfolio
def test_view_offer(self): person_view_offer_sensitivity = Categorical( ['background', 'offer_age', 'web', 'email', 'mobile', 'social'], [0, -1, 1, 1, 1, 1]) offer_channel = Categorical(('web', 'email', 'mobile', 'social'), (1, 1, 1, 1)) offer_type = Categorical(('bogo', 'discount', 'informational'), (0, 1, 0)) discount = Offer(0, valid_from=10, valid_until=20, difficulty=10, reward=2, channel=offer_channel, offer_type=offer_type) person = Person(became_member_on='20170716', view_offer_sensitivity=person_view_offer_sensitivity) person.last_unviewed_offer = discount world = copy.deepcopy(self.world) world.world_time = 0 person.view_offer(world) self.assertTrue(True)
def from_dict(person_dict): history = list() for event_dict in person_dict.get('history'): event_type = event_dict.get('type') if event_type == 'event': event = Event.from_dict(event_dict) elif event_type == 'offer': event = Offer.from_dict(event_dict) elif event_type == 'transaction': event = Transaction.from_dict(event_dict) else: raise ValueError( 'ERROR - Event type not recognized ({}).'.format( event_type)) history.append(event) person = Person( person_dict.get('became_member_on'), \ id=person_dict.get('id'), \ dob=person_dict.get('dob'), \ gender=person_dict.get('gender'), \ income=person_dict.get('income'), \ taste=Categorical.from_dict(person_dict.get('taste')), \ marketing_segment=Categorical.from_dict(person_dict.get('marketing_segment')), \ last_transaction=person_dict.get('last_transaction'), \ last_unviewed_offer=person_dict.get('last_unviewed_offer'), \ last_viewed_offer=person_dict.get('last_viewed_offer'), \ history=history, \ view_offer_sensitivity=Categorical.from_dict(person_dict.get('view_offer_sensitivity')), \ make_purchase_sensitivity=Categorical.from_dict(person_dict.get('make_purchase_sensitivity')), \ purchase_amount_sensitivity=Categorical.from_dict(person_dict.get('purchase_amount_sensitivity'))) return person
def setUp(self): self.offer = Offer(10, channel=Categorical( ('web', 'email', 'mobile', 'social'), (0, 1, 1, 1)), offer_type=Categorical( ('bogo', 'discount', 'informational'), (0, 0, 1))) self.transaction = Transaction(20, amount=1.00) self.world = World() self.person = Person(became_member_on='20170101', history=[self.offer, self.transaction])
def from_dict(population_dict): population = Population( World.from_dict(population_dict.get('world')), people=[ Person.from_dict(person_dict) for person_dict in population_dict.get('people') ], portfolio=[ Offer.from_dict(offer_dict) for offer_dict in population_dict.get('portfolio') ], deliveries_path=population_dict.get('deliveries_path'), transcript_file_name=population_dict.get('transcript_file_name')) return population
def read_offer_portfolio(self, portfolio_file_name): """Read in an offer portfolio from a file. An offer portfolio file contains one json object per line. Each json object represents a single offer. """ with open(portfolio_file_name, 'r') as portfolio_file: for line in portfolio_file: offer_json = line.strip() # skip blank lines if offer_json != '': offer = Offer.from_json(offer_json) offer_id = offer['id'] if offer_id not in self.offer_portfolio: self.offer_portfolio[id] = offer else: raise ValueError( 'ERROR - Offer id {} is not unique. It is already present in the offer portfolio.' .format(offer_id))
def __init__(self, became_member_on, **kwargs): """Initialize Person. became_member_on: date (cannot be missing - assigned when an individual becomes a member) kwargs: dob: date (default = 19010101 - sound silly? it happens in the real world and skews age distributions) gender: M, F, O (O = other, e.g., decline to state, does not identify, etc.) income: positive int, None taste: categorical('sweet', 'sour', 'salty', 'bitter', 'umami') marketing_segment: categorical('front page', 'local', 'entertainment', 'sports', 'opinion', 'comics') offer_sensitivity: categorical make_purchase_sensitivity: ??? """ valid_kwargs = { 'id', 'dob', 'gender', 'income', 'taste', 'marketing_segment', 'last_transaction', 'last_unviewed_offer', 'last_viewed_offer', 'history', 'view_offer_sensitivity', 'make_purchase_sensitivity', 'purchase_amount_sensitivity' } kwargs_name_set = set(kwargs.keys()) assert kwargs_name_set.issubset( valid_kwargs), 'ERROR - Invalid kwargs: {}'.format( kwargs_name_set.difference(valid_kwargs)) ###################### # Intrinsic Attributes ###################### self.id = kwargs.get('id') if kwargs.get( 'id') is not None else uuid.uuid4().hex self.dt_fmt = '%Y%m%d' try: datetime.datetime.strptime(kwargs.get('dob'), self.dt_fmt) self.dob = kwargs.get('dob') except: self.dob = '19010101' self.gender = kwargs.get('gender') try: datetime.datetime.strptime(became_member_on, self.dt_fmt) self.became_member_on = became_member_on except: raise ValueError( 'ERROR - became_member_on has invalid format (should be: {}). became_member_on={}' .format(self.dt_fmt, became_member_on)) self.income = kwargs.get('income') default_taste = Categorical(self.taste_names) kwargs_taste = kwargs.get('taste') if kwargs_taste is not None: assert default_taste.compare_names( kwargs_taste ), 'ERROR - keyword argument taste must have names = {}'.format( default_taste.names) self.taste = kwargs_taste self.taste.set_order(default_taste.names) else: self.taste = default_taste default_marketing_segment = Categorical(self.marketing_segment_names) kwargs_marketing_segment = kwargs.get('marketing_segment') if kwargs_marketing_segment is not None: assert default_marketing_segment.compare_names( kwargs_marketing_segment ), 'ERROR - keyword argument marketing_segment must have names = {}'.format( default_marketing_segment.names) self.marketing_segment = kwargs_marketing_segment self.marketing_segment.set_order(kwargs_marketing_segment.names) else: self.marketing_segment = default_marketing_segment ###################### # Extrinsic Attributes ###################### # only allow one offer to be active at a time, and only count as active if it's been viewed (no accidental # winners) - if offers have overlapping validity periods, then last received is the winner # Person has a short memory. Only the most recently received offer can be viewed (a newly received offer will # supplant it), and only the most recently viewed offer can influence Person's behavior (view another and Person # forgets). However, whether an offer is viewed or not, the user can still accidentally win by making a # sufficient purchase. If two offers are open simultanrously, then Person can get double credit (win both) with # a single purchase. self.last_transaction = kwargs.get('last_transaction') self.last_unviewed_offer = kwargs.get('last_unviewed_offer') self.last_viewed_offer = kwargs.get('last_viewed_offer') ######### # History ######### # A list of events. Since events have a timestamp, this is equivalent to a time series. kwargs_history = kwargs.get('history', list()) # note that all(list()) returns True assert all(map(lambda e: isinstance(e, Event), kwargs_history) ), 'ERROR - Not all items in history are of type Event.' self.history = kwargs_history ################### # Sensitivity ################### # view_offer_sensitivity view_offer_sensitivity_names = numpy.concatenate((numpy.array( ('background', 'offer_age')), Offer(0).channel.names)) default_view_offer_sensitivity = Categorical( view_offer_sensitivity_names) default_view_offer_sensitivity.set('offer_age', -1) kwargs_view_offer_sensitivity = kwargs.get('view_offer_sensitivity', None) if kwargs_view_offer_sensitivity is not None: assert default_view_offer_sensitivity.compare_names( kwargs_view_offer_sensitivity ), 'ERROR - keyword argument view_offer_sensitivity must have names = {}'.format( default_view_offer_sensitivity.names) self.view_offer_sensitivity = copy.deepcopy( kwargs_view_offer_sensitivity) self.view_offer_sensitivity.set_order( default_view_offer_sensitivity.names) else: self.view_offer_sensitivity = default_view_offer_sensitivity # make_purchase_sensitivity make_purchase_sensitivity_names = numpy.array( ('background', 'time_since_last_transaction', 'last_viewed_offer_strength', 'viewed_active_offer')) default_make_purchase_sensitivity = Categorical( make_purchase_sensitivity_names) default_make_purchase_sensitivity.set('time_since_last_viewed_offer', -1) kwargs_make_purchase_sensitivity = kwargs.get( 'make_purchase_sensitivity', None) if kwargs_make_purchase_sensitivity is not None: assert default_make_purchase_sensitivity.compare_names( kwargs_make_purchase_sensitivity ), 'ERROR - keyword argument make_purchase_sensitivity must have names = {}'.format( default_make_purchase_sensitivity.names) self.make_purchase_sensitivity = copy.deepcopy( kwargs_make_purchase_sensitivity) self.make_purchase_sensitivity.set_order( default_make_purchase_sensitivity.names) else: self.make_purchase_sensitivity = default_make_purchase_sensitivity # purchase_amount_sensitivity purchase_amount_sensitivity_names = numpy.concatenate( (numpy.array( ('background', 'income_adjusted_purchase_sensitivity')), self.marketing_segment_names, self.taste_names)) default_purchase_amount_sensitivity = Categorical( purchase_amount_sensitivity_names) kwargs_purchase_amount_sensitivity = kwargs.get( 'purchase_amount_sensitivity', None) if kwargs_purchase_amount_sensitivity is not None: assert default_purchase_amount_sensitivity.compare_names( kwargs_purchase_amount_sensitivity ), 'ERROR - keyword argument purchase_amount_sensitivity must have names = {}'.format( default_purchase_amount_sensitivity.names) default_purchase_amount_sensitivity.set( 'income_adjusted_purchase_sensitivity', 1) self.purchase_amount_sensitivity = copy.deepcopy( kwargs_purchase_amount_sensitivity) self.purchase_amount_sensitivity.set_order( default_purchase_amount_sensitivity.names) else: self.purchase_amount_sensitivity = default_purchase_amount_sensitivity logging.info('Person initialized')
def make_purchase(self, world): """Person decides whether to make a purcahse or not and the size of the purchase. Includes outliers, e.g., due to large group orders vs. individual orders. Depends on time of day, segment, income, how long since last purchase, offers """ # logging.debug('Made purchase decision at time t = {}'.format(world.world_time)) # How long since last transaction if self.last_transaction is not None: time_since_last_transaction = world.world_time - self.last_transaction.timestamp else: time_since_last_transaction = 0 # How long since last viewed offer offer = self.last_viewed_offer if offer is not None: time_since_last_viewed_offer = world.world_time - offer.timestamp last_viewed_offer_duration = offer.valid_until - offer.timestamp viewed_active_offer = 1 if offer.is_active(world.world_time) else 0 offer_channel_weights = offer.channel.weights else: # never viewed an offer, so as if it's been forever time_since_last_viewed_offer = Constants.END_OF_TIME - Constants.BEGINNING_OF_TIME last_viewed_offer_duration = 0 viewed_active_offer = 0 offer_channel_weights = Offer( Constants.BEGINNING_OF_TIME).channel.zeros # as time since last offer increases, the effect should go to zero: x_max = T, f_of_x_max = 0 # the offer view is most powerful immediately: x_min = 0, f_of_x_min = 1 # therefore we have a function that should decrease from 1 to 0 as x increases from 0 to T # also, the sensitivity should be positive (the negative effect lies in the state variable) # T is the time at which the viewed offer no longer has an effect # let's make this 3 days after the offer expires = offer_length + 24/float(world.world_time_tick) * 3 last_viewed_offer_strength = self.bounded_response( time_since_last_viewed_offer, min_x=0, max_x=last_viewed_offer_duration + 24 / float(world.world_time_tick) * 3, f_of_min_x=1.0, f_of_max_x=0.0) beta = self.make_purchase_sensitivity.weights x = numpy.array((1, time_since_last_transaction, last_viewed_offer_strength, viewed_active_offer)) p = 1.0 / (1.0 + numpy.exp(-numpy.dot(beta, x))) # logging.debug(' beta = {}'.format(beta)) # logging.debug(' x = {}'.format(x)) # logging.debug(' beta*x = {}'.format(beta*x)) # logging.debug('dot(beta, x) = {}'.format(numpy.dot(beta, x))) # logging.debug(' p = {}'.format(p)) # flip a coin to decide if a purchase was made made_purchase = True if numpy.random.random() < p else False if made_purchase: # logging.debug('Made purchase') # Determine if this is an outlier order or regular order if numpy.random.random() < self.outlier_frequency: purchase_amount = self.outlier_purchase_amount(world) else: purchase_amount = self.purchase_amount(world) transaction = Transaction(world.world_time, amount=purchase_amount) self.history.append(transaction) self.last_transaction = transaction else: transaction = None return transaction