Skip to content

Commit

Permalink
Refactor notebook into package
Browse files Browse the repository at this point in the history
  • Loading branch information
gwenwindflower committed Dec 22, 2022
1 parent 49f878d commit b43c745
Show file tree
Hide file tree
Showing 20 changed files with 159,187 additions and 0 deletions.
937 changes: 937 additions & 0 deletions customers.csv

Large diffs are not rendered by default.

938 changes: 938 additions & 0 deletions data/customers.csv

Large diffs are not rendered by default.

96,174 changes: 96,174 additions & 0 deletions data/items.csv

Large diffs are not rendered by default.

60,262 changes: 60,262 additions & 0 deletions data/orders.csv

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions data/products.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
,sku,name,type,price,description
0,JAF-001,nutellaphone who dis?,jaffle,1100,nutella and banana jaffle
1,JAF-002,doctor stew,jaffle,1100,house-made beef stew jaffle
2,JAF-003,the krautback,jaffle,1200,lamb and pork bratwurst with house-pickled cabbage sauerkraut and mustard
3,JAF-004,flame impala,jaffle,1400,"pulled pork and pineapple al pastor marinated in ghost pepper sauce, kevin parker's favorite! "
4,JAF-005,mel-bun,jaffle,1200,"melon and minced beef bao, in a jaffle, savory and sweet"
5,BEV-001,tangaroo,beverage,600,mango and tangerine smoothie
6,BEV-002,chai and mighty,beverage,500,oatmilk chai latte with protein boost
7,BEV-003,vanilla ice,beverage,600,iced coffee with house-made french vanilla syrup
8,BEV-004,for richer or pourover ,beverage,700,daily selection of single estate beans for a delicious hot pourover
9,BEV-005,adele-ade,beverage,400,"a kiwi and lime agua fresca, hello from the other side of thirst"
6 changes: 6 additions & 0 deletions data/stores.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
,id,name,opened_at,tax_rate
0,4df82ef5-0cc0-4e67-a538-a0065c18d115,Philadelphia,2016-09-01T00:00:00,0.06
1,4bd23c5f-7a47-4242-99c2-3e34197c8942,Brooklyn,2017-03-12T00:00:00,0.04
2,4ef73a58-a12c-4058-b1ae-14e531f58798,Chicago,2018-04-29T00:00:00,0.0625
3,ca0d2635-72cc-4ac2-b65e-9993972ea055,San Francisco,2018-05-09T00:00:00,0.075
4,e8f4b489-d780-42df-9ed9-e9defe4bd524,New Orleans,2019-03-10T00:00:00,0.04
66 changes: 66 additions & 0 deletions data/supplies.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
,id,name,cost,perishable,sku
0,SUP-001,compostable cutlery - knife,7,False,JAF-001
1,SUP-002,cutlery - fork,7,False,JAF-001
2,SUP-003,serving boat,11,False,JAF-001
3,SUP-004,napkin,4,False,JAF-001
4,SUP-009,bread,33,True,JAF-001
5,SUP-011,nutella,46,True,JAF-001
6,SUP-012,banana,13,True,JAF-001
7,SUP-001,compostable cutlery - knife,7,False,JAF-002
8,SUP-002,cutlery - fork,7,False,JAF-002
9,SUP-003,serving boat,11,False,JAF-002
10,SUP-004,napkin,4,False,JAF-002
11,SUP-009,bread,33,True,JAF-002
12,SUP-010,cheese,20,True,JAF-002
13,SUP-013,beef stew,169,True,JAF-002
14,SUP-001,compostable cutlery - knife,7,False,JAF-003
15,SUP-002,cutlery - fork,7,False,JAF-003
16,SUP-003,serving boat,11,False,JAF-003
17,SUP-004,napkin,4,False,JAF-003
18,SUP-009,bread,33,True,JAF-003
19,SUP-010,cheese,20,True,JAF-003
20,SUP-014,lamb and pork bratwurst,234,True,JAF-003
21,SUP-015,house-pickled cabbage sauerkraut,43,True,JAF-003
22,SUP-016,mustard,7,True,JAF-003
23,SUP-001,compostable cutlery - knife,7,False,JAF-004
24,SUP-002,cutlery - fork,7,False,JAF-004
25,SUP-003,serving boat,11,False,JAF-004
26,SUP-004,napkin,4,False,JAF-004
27,SUP-009,bread,33,True,JAF-004
28,SUP-010,cheese,20,True,JAF-004
29,SUP-017,pulled pork,215,True,JAF-004
30,SUP-018,pineapple,26,True,JAF-004
31,SUP-021,ghost pepper sauce,20,True,JAF-004
32,SUP-001,compostable cutlery - knife,7,False,JAF-005
33,SUP-002,cutlery - fork,7,False,JAF-005
34,SUP-003,serving boat,11,False,JAF-005
35,SUP-004,napkin,4,False,JAF-005
36,SUP-009,bread,33,True,JAF-005
37,SUP-010,cheese,20,True,JAF-005
38,SUP-019,melon,33,True,JAF-005
39,SUP-020,minced beef,124,True,JAF-005
40,SUP-005,16oz compostable clear cup,13,False,BEV-001
41,SUP-006,16oz compostable clear lid,4,False,BEV-001
42,SUP-007,biodegradable straw,13,False,BEV-001
43,SUP-022,mango,32,True,BEV-001
44,SUP-023,tangerine,20,True,BEV-001
45,SUP-005,16oz compostable clear cup,13,False,BEV-002
46,SUP-006,16oz compostable clear lid,4,False,BEV-002
47,SUP-007,biodegradable straw,13,False,BEV-002
48,SUP-008,chai mix,98,True,BEV-002
49,SUP-024,oatmilk,11,True,BEV-002
50,SUP-025,whey protein,36,True,BEV-002
51,SUP-005,16oz compostable clear cup,13,False,BEV-003
52,SUP-006,16oz compostable clear lid,4,False,BEV-003
53,SUP-007,biodegradable straw,13,False,BEV-003
54,SUP-026,coffee,52,True,BEV-003
55,SUP-027,french vanilla syrup,72,True,BEV-003
56,SUP-005,16oz compostable clear cup,13,False,BEV-004
57,SUP-006,16oz compostable clear lid,4,False,BEV-004
58,SUP-007,biodegradable straw,13,False,BEV-004
59,SUP-026,coffee,52,True,BEV-004
60,SUP-005,16oz compostable clear cup,13,False,BEV-005
61,SUP-006,16oz compostable clear lid,4,False,BEV-005
62,SUP-007,biodegradable straw,13,False,BEV-005
63,SUP-028,kiwi,20,True,BEV-005
64,SUP-029,lime,13,True,BEV-005
4 changes: 4 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
# Add your own dependencies to this file.
numpy==1.24.0
pandas==1.5.2
Faker==15.3.4
tqdm==4.64.1
86 changes: 86 additions & 0 deletions src/curves.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import datetime

import numpy as np


class Curve:
@classmethod
def eval(cls, date):
x = cls.TranslateDomain(date)
x_mod = x % len(cls.Domain)
x_translated = cls.Domain[x_mod]
return cls.Expr(x_translated)


class AnnualCurve(Curve):
Domain = np.linspace(0, 2 * np.pi, 365)
TranslateDomain = lambda date: date.timetuple().tm_yday
Expr = lambda x: (np.cos(x) + 1) / 10 + 0.8


class WeekendCurve(Curve):
Domain = tuple(range(6))
TranslateDomain = lambda date: date.weekday() - 1
Expr = lambda x: 0.6 if x >= 6 else 1


class GrowthCurve(Curve):
Domain = tuple(range(500))
TranslateDomain = lambda date: (date.year - 2016) * 12 + date.month
# ~ aim for ~20% growth/year
Expr = lambda x: 1 + (x / 12) * 0.2


class Day(object):
EPOCH = datetime.datetime(year=2016, month=9, day=1)
SEASONAL_MONTHLY_CURVE = AnnualCurve()
WEEKEND_CURVE = WeekendCurve()
GROWTH_CURVE = GrowthCurve()

def __init__(self, date_index, minutes=0):
self.date_index = date_index
self.date = self.EPOCH + datetime.timedelta(days=date_index, minutes=minutes)

self.day_of_week = self._get_day_of_week(self.date)
self.is_weekend = self._is_weekend(self.date)
self.season = self._get_season(self.date)

self.effects = [
self.SEASONAL_MONTHLY_CURVE.eval(self.date),
self.WEEKEND_CURVE.eval(self.date),
self.GROWTH_CURVE.eval(self.date),
]

def at_minute(self, minutes):
return Day(self.date_index, minutes=minutes)

def get_effect(self):
total = 1
for effect in self.effects:
total = total * effect
return total

# weekend_effect = 0.8 if date.is_weekend else 1
# summer_effect = 0.7 if date.season == 'summer' else 1

def _get_day_of_week(self, date):
return date.weekday()

def _is_weekend(self, date):
# 5 + 6 are weekends
return date.weekday() >= 5

def _get_season(self, date):
month_no = date.month
day_no = date.day

if month_no in (1, 2) or (month_no == 3 and day_no < 21):
return "winter"
elif month_no in (3, 4, 5) or (month_no == 6 and day_no < 21):
return "spring"
elif month_no in (6, 7, 8) or (month_no == 9 and day_no < 21):
return "summer"
elif month_no in (9, 10, 11) or (month_no == 12 and day_no < 21):
return "fall"
else:
return "winter"
166 changes: 166 additions & 0 deletions src/customers/customers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import random
import uuid

import numpy as np
from faker import Faker

from customers.order import Order
from stores.inventory import Inventory

fake = Faker()
Faker.seed(123456789)


class Customer(object):
def __init__(self, store):
self.customer_id = str(uuid.uuid4())
self.store = store
self.name = fake.name()
self.favorite_number = int(np.random.rand() * 100)

def p_buy_season(self, day):
return self.store.p_buy(day)

def p_buy(self, day):
p_buy_season = self.p_buy_season(day)
p_buy_persona = self.p_buy_persona(day)
p_buy_on_day = (p_buy_season * p_buy_persona) ** 0.5
return p_buy_on_day

def get_order(self, day):
items = self.get_order_items(day)
order_time_delta = self.get_order_time(day)

order_time = day.at_minute(order_time_delta + self.store.opens_at(day))
if not self.store.is_open_at(order_time):
return None

return Order(self, items, self.store, order_time)

def get_order_items(self, day):
raise NotImplemented()

def get_order_time(self, store, day):
raise NotImplemented()

def p_buy_persona(self, day):
raise NotImplemented()

def sim_day(self, day):
p_buy = self.p_buy(day)
p_buy_threshold = np.random.random()

if p_buy_threshold < p_buy:
return self.get_order(day)
else:
return None

def to_dict(self):
return {
"id": self.customer_id,
"name": self.name,
}


class RemoteWorker(Customer):
"This person works from a coffee shop"

def p_buy_persona(self, day):
buy_propensity = (self.favorite_number / 100) * 0.4
return 0.001 if day.is_weekend else buy_propensity

def get_order_time(self, day):
# most likely to order in the morning
# exponentially less likely to order in the afternoon
avg_time = 420
order_time = np.random.normal(loc=avg_time, scale=180)
return max(0, int(order_time))

def get_order_items(self, day):
num_drinks = 1
food = []

if random.random() > 0.7:
num_drinks = 2

if random.random() > 0.7:
food = Inventory.get_food(1)

return Inventory.get_drink(num_drinks) + food


class BrunchCrowd(Customer):
"Do you sell mimosas?"

def p_buy_persona(self, day):
buy_propensity = 0.2 + (self.favorite_number / 100) * 0.2
return buy_propensity if day.is_weekend else 0

def get_order_time(self, day):
# most likely to order in the early afternoon
avg_time = 300 + ((self.favorite_number - 50) / 50) * 120
order_time = np.random.normal(loc=avg_time, scale=120)
return max(0, int(order_time))

def get_order_items(self, day):
num_customers = 1 + int(self.favorite_number / 20)
return Inventory.get_drink(num_customers) + Inventory.get_food(num_customers)


class Commuter(Customer):
"the regular, thanks"

def p_buy_persona(self, day):
buy_propensity = 0.5 + (self.favorite_number / 100) * 0.3
return 0.001 if day.is_weekend else buy_propensity

def get_order_time(self, day):
# most likely to order in the morning
# exponentially less likely to order in the afternoon
avg_time = 60
order_time = np.random.normal(loc=avg_time, scale=30)
return max(0, int(order_time))

def get_order_items(self, day):
return Inventory.get_drink(1)


class Student(Customer):
"coffee might help"

def p_buy_persona(self, day):
if day.season == "summer":
return 0
else:
buy_propensity = 0.1 + (self.favorite_number / 100) * 0.4
return buy_propensity

def get_order_time(self, day):
# later is better
avg_time = 9 * 60
order_time = np.random.normal(loc=avg_time, scale=120)
return max(0, int(order_time))

def get_order_items(self, day):
food = []
if random.random() > 0.5:
food = Inventory.get_food(1)

return Inventory.get_drink(1) + food


class Casuals(Customer):
"just popping in"

def p_buy_persona(self, day):
return 0.1

def get_order_time(self, day):
avg_time = 5 * 60
order_time = np.random.normal(loc=avg_time, scale=120)
return max(0, int(order_time))

def get_order_items(self, day):
num_drinks = int(random.random() * 10 / 3)
num_food = int(random.random() * 10 / 3)
return Inventory.get_drink(num_drinks) + Inventory.get_food(num_food)
33 changes: 33 additions & 0 deletions src/customers/order.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import uuid

from customers.order_item import OrderItem


class Order(object):
def __init__(self, customer, items, store, day):
self.order_id = str(uuid.uuid4())
self.customer = customer
self.items = [OrderItem(self.order_id, item) for item in items]
self.store = store
self.day = day
self.subtotal = sum(i.item.price for i in self.items)
self.tax_paid = store.tax_rate * self.subtotal
self.order_total = self.subtotal + self.tax_paid

def __str__(self):
return f"{self.customer.name} bought {str(self.items)} at {self.day}"

def to_dict(self):
return {
"id": self.order_id,
"customer": self.customer.customer_id,
"ordered_at": self.day.date.isoformat(),
# "order_month": self.day.date.strftime("%Y-%m"),
"store_id": self.store.store_id,
"subtotal": int(self.subtotal * 100),
"tax_paid": int(self.tax_paid * 100),
"order_total": int(self.order_total * 100),
}

def items_to_dict(self):
return [i.to_dict(self.order_id) for i in self.items]
15 changes: 15 additions & 0 deletions src/customers/order_item.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import uuid


class OrderItem(object):
def __init__(self, order_id, item):
self.item_id = str(uuid.uuid4())
self.order_id = order_id
self.item = item

def to_dict(self):
return {
"id": self.item_id,
"order_id": self.order_id,
"sku": self.item.sku,
}
Loading

0 comments on commit b43c745

Please sign in to comment.