mathiasleys commited on
Commit
a8905e8
โ€ข
1 Parent(s): d35ddb1

Move explanation to Medium blog post

Browse files
app.py CHANGED
@@ -1,15 +1,12 @@
1
  """Streamlit entrypoint"""
2
 
3
- import base64
4
  import time
5
 
6
  import numpy as np
7
  import streamlit as st
8
- import sympy
9
 
10
  from helpers.thompson_sampling import ThompsonSampler
11
 
12
- eta, a, p, D, profit, var_cost, fixed_cost = sympy.symbols("eta a p D Profit varcost fixedcost")
13
  np.random.seed(42)
14
 
15
  st.set_page_config(
@@ -27,265 +24,12 @@ st.set_page_config(
27
  st.title("Dynamic Pricing")
28
  st.subheader("Setting optimal prices with Bayesian stats ๐Ÿ“ˆ")
29
 
30
- # (0) Intro
31
- st.header("Why care about dynamic pricing? ๐Ÿ’ญ")
32
-
33
- st.markdown("""Dynamic pricing aims to actively adapt product prices based on insights about
34
- customer behaviour. \n
35
- This pricing strategy has proven exceptionally effective in a wide range of industries: from
36
- e-commerce (e.g., eBay) to airlines (e.g., Delta Airlines) and from retail (e.g., Walmart) to
37
- utilities (e.g., Tampa Electric) as it allows to **adjust prices to changes in demand patterns**.""")
38
- st.markdown("""It is hardly surprising that recent times of extraordinary uncertainty and volatility
39
- caused a surge in adoption of dynamic pricing strategies with an [estimated 21% of e-commerce
40
- businesses reportedly already using dynamic pricing](https://www.statista.com/statistics/1174557/dynamic-pricing-ecommerce-companies-worldwide/)
41
- and an additional 15% planning to adopt the strategy in the upcoming year.""")
42
- st.markdown("""To find a big success story, we should look no further than Amazon who (on average)
43
- change their products' prices once every 10 minutes. They attribute roughly [25% of their e-commerce
44
- profits](https://dzone.com/articles/big-data-analytics-delivering-business-value-at-am)
45
- to their pricing strategy.""")
46
- st.markdown("""In what follows we'll discuss the working principles & challenges in the field of
47
- dynamic pricing, followed by an interactive demo and some final considerations.""")
48
-
49
- # (1) Basics
50
- st.header("Back to basics ๐Ÿ")
51
-
52
- st.markdown("The beginning is usually a good place to start so we'll kick things off there.")
53
- st.markdown("""The one crucial piece information we need in order to find the optimal price is
54
- **how demand behaves over different price points**. \nIf we can make a decent guess of what we
55
- can expect demand to be for a wide range of prices, we can figure out which price optimizes our
56
- target (i.e., revenue, profit, ...).""")
57
- st.markdown("""For the keen economists amongst you, this is beginning to sound a lot like a
58
- **demand curve**.""")
59
-
60
- st.markdown("""Estimating a demand curve, sounds easy enough right? \nLet's assume we have
61
- demand with constant price elasticity; so a certain percent change in price will cause a
62
- constant percent change in demand, independent of the price level. In economics, this is often used
63
- as a proxy for demand curves in the wild.""")
64
- st.markdown("So our demand data looks something like this:")
65
- st.image("assets/images/ideal_case_demand.png")
66
- st.markdown("""Alright now we can get out our trusted regression toolbox and fit a nice curve
67
- through the data because we know that our constant-elasticity demand function has this form:""")
68
- st.latex(sympy.latex(sympy.Eq(sympy.Function(D)(p), a*p**(-eta), evaluate=False)))
69
- st.write("with shape parameter a and price elasticity ฮท")
70
- st.image("assets/images/ideal_case_demand_fitted.png")
71
- st.markdown("""Now that we have a reasonable estimate of our demand function, we can derive our
72
- expected profit at different price points because we know the following holds:""")
73
- st.latex(f"{profit} = {p}*{sympy.Function(D)(p)} - [{var_cost}*{sympy.Function(D)(p)} + {fixed_cost}]")
74
- st.image("assets/images/ideal_case_profit_curve.png")
75
- st.markdown("""Note that fixed costs (e.g., rent, insurance, etc.), per definition, don't vary when
76
- demand or price changes. Therefore, fixed costs have no influence on the behavior of dynamic pricing
77
- algorithms.""")
78
- st.markdown("""Finally we can dust off our good old high-school math book and find the
79
- price which we expect will optimize profit which was ultimately the goal of all this.""")
80
- st.image("assets/images/ideal_case_optimal_profit.png")
81
- st.markdown("""Voilร  there you have it: we should price this product at 4.24 and we can expect
82
- a bottom-line profit of 7.34""")
83
- st.markdown("So can we kick back & relax now? \nWell, there are a few issues with what we just did.")
84
-
85
- # (2) Dynamic demand curves
86
- st.header("The demands they are a-changin' ๐ŸŽธ")
87
- st.markdown("""We arrive at our first bit of bad news: unfortunately, you can't just estimate a
88
- demand curve once and be done with it. \nWhy? Because demand is influenced by many factors (e.g.,
89
- market trends, competitor actions, human behavior, etc.) that tend to change a lot over time.""")
90
- st.write("Below you can see an (exaggerated) example of what we're talking about:")
91
-
92
- with open("assets/images/dynamic_demand.gif", "rb") as file_:
93
- contents = file_.read()
94
- data_url = base64.b64encode(contents).decode("utf-8")
95
-
96
- st.markdown(
97
- f'<img src="data:image/gif;base64,{data_url}" alt="dynamic demand">',
98
- unsafe_allow_html=True,
99
- )
100
- st.markdown("""Now, you may think we can solve this issue by periodically re-estimating the demand
101
- curve. \nAnd you would be very right! But also very wrong as this leads us nicely to the
102
- next issue.""")
103
-
104
- # (3) Constrained data
105
- st.header("Where are we getting this data anyways? ๐Ÿ–ฅ๏ธ")
106
- st.markdown("""So far, we have assumed that we get (and keep getting) data on demand levels at
107
- different price points. \n
108
- Not only is this assumption **unrealistic**, it is also very **undesirable**""")
109
- st.markdown("""Why? Because getting demand data on a wide spectrum of price points implies that
110
- we are spending a significant amount of time setting prices that are either too high or too low! \n
111
- Which is ironically exactly the opposite of what we set out to achieve.""")
112
- st.markdown("In practice, our demand observations will rather look something like this:")
113
- st.image("assets/images/realistic_demand.png")
114
- st.markdown("""As we can see, we have tried three price points in the past (โ‚ฌ7.5, โ‚ฌ10 and โ‚ฌ11) and
115
- collected demand data.""")
116
- st.markdown("""On a side note: keep in mind that we still assume the same latent demand curve and
117
- optimal price point of โ‚ฌ4.24 \n
118
- So (for the sake of the example) we have been massively overpricing our product in the past.""")
119
- st.image("assets/images/realistic_demand_latent_curve.png")
120
- st.markdown("""This limited data brings along a major challenge in estimating the demand curve
121
- though. \n
122
- Intuitively, it makes sense that we can make a reasonable estimate of expected demand at โ‚ฌ8 or โ‚ฌ9,
123
- given the observed demand at โ‚ฌ7.5 and โ‚ฌ10. \nBut can we extrapolate further to โ‚ฌ2 or โ‚ฌ20 with the
124
- same reasonable confidence? Probably not.""")
125
- st.markdown("""This is a nice example of a very well-known problem in statistics called the
126
- **\"exploration-exploitation trade-off\"** \n
127
- ๐Ÿ‘‰ **Exploration**: We want to explore the demand for a diverse enough range of price points
128
- so that we can accurately estimate our demand curve. \n
129
- ๐Ÿ‘‰ **Exploitation**: We want to exploit all the knowledge we have gained through exploring and
130
- actually do what we set out to do: set our price at an optimal level.""")
131
-
132
- # (4) Thompson sampling explanation
133
- st.header("Enter: Thompson Sampling ๐Ÿ“Š")
134
- st.markdown("""As we mentioned, this is a well-known problem in statistics. So luckily for us,
135
- there is a pretty neat solution in the form of **Thompson sampling**!""")
136
- st.markdown("""Basically instead of estimating one demand function based on the data available to
137
- us, we will estimate a probability distribution of demand functions or simply put, for every
138
- possible demand function that fits our functional form (i.e. constant elasticity)
139
- we will estimate the probability that it is the correct one, given our data.""")
140
- st.markdown("""Or mathematically speaking, we will place a prior distribution on the parameters
141
- that define our demand function and update these priors to posterior distributions via Bayes rule,
142
- thus obtaining a posterior distribution for our demand function""")
143
- st.markdown("""Thompson sampling then entails just sampling a demand function out of this
144
- distribution, calculating the optimal price given this demand function, observing demand for this
145
- new price point and using this information to refine our demand function estimates.""")
146
- st.image("assets/images/flywheel_1.png")
147
- st.markdown("""So: \n
148
- ๐Ÿ‘‰ When we are **less certain** of our estimates, we will sample more diverse demand functions,
149
- which means that we will also explore more diverse price points. Thus, we will **explore**. \n
150
- ๐Ÿ‘‰ When we are **more certain** of our estimates, we will sample a demand function close to
151
- the real one & set a price close to the optimal price more often. Thus, we will **exploit**.""")
152
-
153
- st.markdown("""With that said, we'll take another look at our constrained data and see whether
154
- Thompson sampling gets us any closer to the optimal price of โ‚ฌ4.24""")
155
- st.image("assets/images/realistic_demand_latent_curve.png")
156
- st.markdown("""Let's start working our mathemagic: \n
157
- We'll start off by placing semi-informed priors on the parameters that make up our
158
- demand function.""")
159
-
160
- st.latex(f"{sympy.latex(a)} \sim N(ฮผ=0,ฯƒ=2)")
161
- st.latex(f"{sympy.latex(eta)} \sim N(ฮผ=0.5,ฯƒ=0.5)")
162
- st.latex("sd \sim Exp(\lambda=1)")
163
- st.latex(f"{sympy.latex(D)}|P=p \sim N(ฮผ={sympy.latex(a*p**(-eta))},ฯƒ=sd)")
164
-
165
- st.markdown("""These priors are semi-informed because we have the prior knowledge that
166
- price elasticity is most likely between 0 and 1. As for the other parameters, we have little
167
- knowledge about them so we can place a pretty uninformative prior.""")
168
- st.markdown("If that made sense to you, great. If it didn't, don't worry about it")
169
-
170
- st.markdown("""Now that are priors are taken care of, we can update these beliefs by incorporating
171
- the data at the โ‚ฌ7.5, โ‚ฌ10 and โ‚ฌ11 price levels we have available to us.""")
172
- st.markdown("The resulting demand & profit curve distributions look a little something like this:")
173
- st.image(["assets/images/posterior_demand.png", "assets/images/posterior_profit.png"])
174
-
175
- st.markdown("""It's time to sample one demand curve out of this posterior distribution. \n
176
- The lucky curve is:""")
177
- st.image("assets/images/posterior_demand_sample.png")
178
- st.markdown("This results in the following expected profit curve")
179
- st.image("assets/images/posterior_profit_sample.png")
180
- st.markdown("""And eventually we arrive at a new price: โ‚ฌ5.25! Which is indeed considerably closer
181
- to the actual optimal price of โ‚ฌ4.24""")
182
- st.markdown("Now that we have our first updated price point, why stop there?")
183
- st.markdown("""With \"pure\" Thompson sampling, we would sample a new demand curve (and thus price
184
- point) out of the posterior distribution every time. But since we are mainly interested in seeing
185
- the convergence behavior of Thompson sampling, let's simulate 10 demand points at this fixed โ‚ฌ5.25
186
- price point.""")
187
- st.image("assets/images/updated_prices_demand.png")
188
- st.markdown("""We know the drill by now. \n
189
- Let's recalculate our posteriors with this extra information.""")
190
- st.image(["assets/images/posterior_demand_2.png", "assets/images/posterior_profit_2.png"])
191
- st.markdown("""We immediately notice that the demand (and profit) posteriors are much less spread
192
- apart this time around which implies that we are more confident in our predictions.""")
193
- st.markdown("Now, we can sample just one curve from the distribution.")
194
- st.image(["assets/images/posterior_demand_sample_2.png", "assets/images/posterior_profit_sample_2.png"])
195
- st.markdown("""And finally we arrive at a price point of โ‚ฌ4.04 which is eerily close to
196
- the actual optimum of โ‚ฌ4.24""")
197
-
198
- # (5) Extra topics
199
- st.header("Some things to think about ๐Ÿค”")
200
-
201
- st.markdown("""Because we have purposefully kept the example above quite simple, you may still be
202
- wondering what happens when added complexities show up. \n
203
- Let's discuss some of those concerns FAQ-style:""")
204
-
205
- st.subheader("๐Ÿ‘‰ Isn't this constant-elasticity model a bit too simple to work in practice?")
206
- st.markdown("Brief answer: usually yes it is.")
207
- st.markdown("""Luckily, more flexible methods exist. \n
208
- We would recommend to use Gaussian Processes. We won't go into how these work here but the main idea
209
- is that it doesn't impose a restrictive functional form onto the demand function but rather lets
210
- the data speak for itself.""")
211
-
212
- with open("assets/images/gaussian_process.gif", "rb") as file_:
213
- contents = file_.read()
214
- data_url = base64.b64encode(contents).decode("utf-8")
215
-
216
- st.markdown(
217
- f'<img src="data:image/gif;base64,{data_url}" alt="gaussian process">',
218
- unsafe_allow_html=True,
219
- )
220
- st.markdown("""If you do want to learn more, we recommend these links:
221
- [1](https://distill.pub/2019/visual-exploration-gaussian-processes/),
222
- [2](https://thegradient.pub/gaussian-process-not-quite-for-dummies/),
223
- [3](https://sidravi1.github.io/blog/2018/05/15/latent-gp-and-binomial-likelihood)""")
224
-
225
- st.subheader("""๐Ÿ‘‰ Price optimization is much more complex than just optimizing a simple profit function?""")
226
- st.markdown("""It sure is. In reality, there are many added complexities that come into play, such
227
- as inventory/capacity constraints, complex cost structures, ...""")
228
- st.markdown("""The nice thing about our setup is that it consists of three components that you can
229
- change pretty much independently from each other. \n
230
- This means that you can make the price optimization pillar arbitrarily custom/complex. As long as
231
- it takes in a demand function and spits out a price.""")
232
- st.image("assets/images/flywheel_2.png")
233
- st.markdown("You can tune the other two steps as much as you like too.")
234
- st.image("assets/images/flywheel_3.png")
235
-
236
- st.subheader("๐Ÿ‘‰ Changing prices has a huge impact. How can I mitigate this during experimentation?")
237
- st.markdown("There are a few things we can do to minimize risk:")
238
- st.markdown("""๐Ÿ‘‰ **A/B testing**: You can do a gradual roll-out of the new pricing system where a
239
- small (but increasing) percentage of your transactions are based on this new system. This allows you
240
- to start small & track/grow the impact over time.""")
241
- st.markdown("""๐Ÿ‘‰ **Limit products**: Similarly to A/B testing, you can also segment on the
242
- product-level. For instance, you can start gradually rolling out dynamic pricing for one product
243
- type and extend this over time.""")
244
- st.markdown("""๐Ÿ‘‰ **Bound price range**: Theoretically, Thompson sampling in its purest form can
245
- lead to any arbitrary price point (albeit with an increasingly low probability). In order to limit
246
- the risk here, you can simply place a upper/lower bound on the price range you are comfortable
247
- experimenting in.""")
248
- st.markdown("""On top of all this, Bayesian methods (by design) explicitly quantify uncertainty.
249
- This allows you to have a very concrete view on the variance of our demand estimates""")
250
-
251
- st.subheader("๐Ÿ‘‰ What if I have multiple products that can cannibalize each other?")
252
- st.markdown("Here it really depends")
253
- st.markdown("""๐Ÿ‘‰ **If you have a handful of products**, we can simply reformulate our objective while
254
- keeping our methods analogous. \n
255
- Instead of tuning one price to optimize profit for the demand function of one product, we tune N
256
- prices to optimize profit for the joint demand function of N products. This joint demand function
257
- can then account for correlations in demand within products.""")
258
- st.markdown("""๐Ÿ‘‰ **If you have hundreds, thousands or more products**, we're sure you can imagine that
259
- the procedure described above becomes increasingly infeasible. \n
260
- A practical alternative is to group substitutable products into "baskets" and define the "price of
261
- the basket" as the average price of all products in the basket. \n
262
- If we assume that the products in baskets are subtitutable but the products in different baskets are
263
- not, we can optimize basket prices indepedently from one another. \n
264
- Finally, if we also assume that cannibalization remains constant if the ratio of prices remains
265
- constant, we can calculate individual product prices as a fixed ratio of its basket price. \n""")
266
- st.markdown("""For example, if a "burger basket" consists of a hamburger (โ‚ฌ1) and a cheeseburger
267
- (โ‚ฌ3), then the "burger price" is ((โ‚ฌ1 + โ‚ฌ3) / 2 =) โ‚ฌ2. So a hamburger costs 50% of the burger price
268
- and a cheeseburger costs 150% of the burger price. \n
269
- If we change the burger's price to โ‚ฌ3, a hamburger will cost (50% * โ‚ฌ3 =) โ‚ฌ1.5 and a cheeseburger
270
- will cost (150% * โ‚ฌ3 =) โ‚ฌ4.5 because we assume that the cannibalization effect between hamburgers &
271
- cheeseburgers is the same when hamburgers cost โ‚ฌ1 & cheeseburgers cost โ‚ฌ3 and when hamburgers cost
272
- โ‚ฌ1.5 & cheeseburgers cost โ‚ฌ4.5""")
273
- st.image("assets/images/cannibalization.png")
274
-
275
- st.subheader("๐Ÿ‘‰ Is dynamic pricing even relevant for slow-selling products?")
276
- st.markdown("""The boring answer is that it depends. It depends on how dynamic the market is, the
277
- quality of the prior information, ...""")
278
- st.markdown("""But obviously this isn't very helpful. \nIn general, we notice that you can already
279
- get quite far with limited data, especially if you have an accurate prior belief on how the demand
280
- likely behaves.""")
281
- st.markdown("""For reference, in our simple example where we showed a Thompson sampling update, we
282
- were already able to gain a lot of confidence in our estimates with just 10 extra demand
283
- observations.""")
284
-
285
- # (6) Thompson sampling demo
286
  st.header("Demo time ๐ŸŽฎ")
287
- st.markdown("Now that we have covered the theory, you can go ahead and try it our for yourself!")
288
- st.markdown("""You will notice a few things: \n
 
 
 
289
  ๐Ÿ‘‰ As you increase price elasticity, the demand becomes more sensitive to price changes and thus the
290
  profit-optimizing price becomes lower (& vice versa). \n
291
  ๐Ÿ‘‰ As you decrease price elasticity, our demand observations at โ‚ฌ7.5, โ‚ฌ10 and โ‚ฌ11 become
@@ -293,6 +37,8 @@ increasingly larger and increasingly more variable (as their variance is a const
293
  absolute value). This causes our demand posterior to become increasingly wider and thus Thompson
294
  sampling will lead to more exploration.
295
  """)
 
 
296
 
297
  thompson_sampler = ThompsonSampler()
298
  demo_button = st.checkbox(
 
1
  """Streamlit entrypoint"""
2
 
 
3
  import time
4
 
5
  import numpy as np
6
  import streamlit as st
 
7
 
8
  from helpers.thompson_sampling import ThompsonSampler
9
 
 
10
  np.random.seed(42)
11
 
12
  st.set_page_config(
 
24
  st.title("Dynamic Pricing")
25
  st.subheader("Setting optimal prices with Bayesian stats ๐Ÿ“ˆ")
26
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  st.header("Demo time ๐ŸŽฎ")
28
+ st.markdown("""In this demo you will see \n
29
+ ๐Ÿ‘‰ How Bayesian demand function estimates are created based on sales data \n
30
+ ๐Ÿ‘‰ How Thompson sampling will generate concrete price points from these Bayesian estimates \n
31
+ ๐Ÿ‘‰ The impact of price elasticity on Bayesian demand estimation""")
32
+ st.markdown("""You will notice: \n
33
  ๐Ÿ‘‰ As you increase price elasticity, the demand becomes more sensitive to price changes and thus the
34
  profit-optimizing price becomes lower (& vice versa). \n
35
  ๐Ÿ‘‰ As you decrease price elasticity, our demand observations at โ‚ฌ7.5, โ‚ฌ10 and โ‚ฌ11 become
 
37
  absolute value). This causes our demand posterior to become increasingly wider and thus Thompson
38
  sampling will lead to more exploration.
39
  """)
40
+ st.markdown("""If you are looking for more insights into how dynamic pricing is done in practice,
41
+ check out our blog post here: https://medium.com/ml6team/dynamic-pricing-in-practice-99fe2216a93d""")
42
 
43
  thompson_sampler = ThompsonSampler()
44
  demo_button = st.checkbox(
assets/images/cannibalization.png DELETED
Binary file (66.5 kB)
 
assets/images/dynamic_demand.gif DELETED

Git LFS Details

  • SHA256: dbc3058a104149d179a787527590e407192ff26b7cd0142a81855317eab81416
  • Pointer size: 132 Bytes
  • Size of remote file: 2.39 MB
assets/images/flywheel_1.png DELETED
Binary file (46.3 kB)
 
assets/images/flywheel_2.png DELETED
Binary file (67.6 kB)
 
assets/images/flywheel_3.png DELETED
Binary file (112 kB)
 
assets/images/gaussian_process.gif DELETED

Git LFS Details

  • SHA256: 49c0bd1e0a0e4584a100934aa9f7f28ba9257dad84d990e7a42f9911fcb1569f
  • Pointer size: 132 Bytes
  • Size of remote file: 1.09 MB
assets/images/ideal_case_demand.png DELETED
Binary file (8.93 kB)
 
assets/images/ideal_case_demand_fitted.png DELETED
Binary file (14 kB)
 
assets/images/ideal_case_optimal_profit.png DELETED
Binary file (13.3 kB)
 
assets/images/ideal_case_profit_curve.png DELETED
Binary file (10.9 kB)
 
assets/images/posterior_demand.png DELETED
Binary file (28.3 kB)
 
assets/images/posterior_demand_2.png DELETED
Binary file (29.6 kB)
 
assets/images/posterior_demand_sample.png DELETED
Binary file (12 kB)
 
assets/images/posterior_demand_sample_2.png DELETED
Binary file (12.7 kB)
 
assets/images/posterior_profit.png DELETED
Binary file (31 kB)
 
assets/images/posterior_profit_2.png DELETED
Binary file (30.8 kB)
 
assets/images/posterior_profit_sample.png DELETED
Binary file (13.2 kB)
 
assets/images/posterior_profit_sample_2.png DELETED
Binary file (14.3 kB)
 
assets/images/realistic_demand.png DELETED
Binary file (6.9 kB)
 
assets/images/realistic_demand_latent_curve.png DELETED
Binary file (10.1 kB)
 
assets/images/updated_prices_demand.png DELETED
Binary file (7.15 kB)