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 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 create_people_0(n): profile_optout_rate = 0.1 min_tenure = 0 max_tenure = 365 mean_age = 365 * 30 std_age = 365 * 2 gender_rates = [0.49, 0.49, 0.02] min_income = 50000 max_income = 75000 beta = 1.0 / 0.0004 g = lambda x: 1.0 / (1.0 + numpy.exp(-x)) g_inv = lambda y: numpy.log(y / (1.0 - y)) people = list() for i in range(n): became_member_on = (now - datetime.timedelta( days=numpy.random.choice(range(min_tenure, max_tenure))) ).strftime(dt_fmt) if numpy.random.random() < 1.0 - profile_optout_rate: # must be at least 18 to join dob = (now - datetime.timedelta(days=int( max(365.25 * 18, (numpy.random.normal(mean_age, std_age))))) ).strftime(dt_fmt) # three values + missing gender = numpy.random.choice(['M', 'F', 'O'], p=gender_rates) income = None for i in range(25): x = max(25000, numpy.random.exponential(beta)) if min_income <= x <= max_income: income = x break else: dob = None gender = None income = None person_view_offer_sensitivity = Categorical( ['background', 'offer_age', 'web', 'email', 'mobile', 'social'], [ g_inv(0.20) - 4, -abs(g_inv(0.01) / float(1 * 24 * 30)), 0, 1, 1, 2 ]) person_make_purchase_sensitivity = Categorical([ 'background', 'time_since_last_transaction', 'last_viewed_offer_strength', 'viewed_active_offer' ], [g_inv(1.0 / 24.0), abs(g_inv(0.10) / float(1 * 24 * 30)), 1, 1]) person_purchase_amount_sensitivity = Categorical([ 'background', 'income_adjusted_purchase_sensitivity', 'front page', 'local', 'entertainment', 'sports', 'opinion', 'comics', 'sweet', 'sour', 'salty', 'bitter', 'umami' ], [1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) people.append( Person( became_member_on, dob=dob, gender=gender, income=income, view_offer_sensitivity=person_view_offer_sensitivity, make_purchase_sensitivity=person_make_purchase_sensitivity, purchase_amount_sensitivity=person_purchase_amount_sensitivity) ) return people
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')