Spaces:
Runtime error
Runtime error
"""Streamlit entrypoint""" | |
import base64 | |
import time | |
import numpy as np | |
import streamlit as st | |
import sympy | |
from helpers.thompson_sampling import ThompsonSampler | |
eta, a, p, D, profit, var_cost, fixed_cost = sympy.symbols("eta a p D Profit varcost fixedcost") | |
np.random.seed(42) | |
st.set_page_config( | |
page_title="Dynamic Pricing", | |
page_icon="๐ธ", | |
layout="centered", | |
initial_sidebar_state="auto", | |
menu_items={ | |
'Get help': None, | |
'Report a bug': None, | |
'About': "https://www.ml6.eu/", | |
} | |
) | |
st.title("Dynamic Pricing") | |
st.subheader("Setting optimal prices with Bayesian stats ๐") | |
# (0) Intro | |
st.header("Why care about dynamic pricing? ๐ญ") | |
st.markdown("""Dynamic pricing aims to actively adapt product prices based on insights about | |
customer behaviour. \n | |
This pricing strategy has proven exceptionally effective in a wide range of industries: from | |
e-commerce (e.g., eBay) to airlines (e.g., Delta Airlines) and from retail (e.g., Walmart) to | |
utilities (e.g., Tampa Electric) as it allows to **adjust prices to changes in demand patterns**.""") | |
st.markdown("""It is hardly surprising that recent times of extraordinary uncertainty and volatility | |
caused a surge in adoption of dynamic pricing strategies with an [estimated 21% of e-commerce | |
businesses reportedly already using dynamic pricing](https://www.statista.com/statistics/1174557/dynamic-pricing-ecommerce-companies-worldwide/) | |
and an additional 15% planning to adopt the strategy in the upcoming year.""") | |
st.markdown("""To find a big success story, we should look no further than Amazon who (on average) | |
change their products' prices once every 10 minutes. They attribute roughly [25% of their e-commerce | |
profits](https://dzone.com/articles/big-data-analytics-delivering-business-value-at-am) | |
to their pricing strategy.""") | |
st.markdown("""In what follows we'll discuss the working principles & challenges in the field of | |
dynamic pricing, followed by an interactive demo and some final considerations.""") | |
# (1) Basics | |
st.header("Back to basics ๐") | |
st.markdown("The beginning is usually a good place to start so we'll kick things off there.") | |
st.markdown("""The one crucial piece information we need in order to find the optimal price is | |
**how demand behaves over different price points**. \nIf we can make a decent guess of what we | |
can expect demand to be for a wide range of prices, we can figure out which price optimizes our | |
target (i.e., revenue, profit, ...).""") | |
st.markdown("""For the keen economists amongst you, this is beginning to sound a lot like a | |
**demand curve**.""") | |
st.markdown("""Estimating a demand curve, sounds easy enough right? \nLet's assume we have | |
demand with constant price elasticity; so a certain percent change in price will cause a | |
constant percent change in demand, independent of the price level. In economics, this is often used | |
as a proxy for demand curves in the wild.""") | |
st.markdown("So our demand data looks something like this:") | |
st.image("assets/images/ideal_case_demand.png") | |
st.markdown("""Alright now we can get out our trusted regression toolbox and fit a nice curve | |
through the data because we know that our constant-elasticity demand function has this form:""") | |
st.latex(sympy.latex(sympy.Eq(sympy.Function(D)(p), a*p**(-eta), evaluate=False))) | |
st.write("with shape parameter a and price elasticity ฮท") | |
st.image("assets/images/ideal_case_demand_fitted.png") | |
st.markdown("""Now that we have a reasonable estimate of our demand function, we can derive our | |
expected profit at different price points because we know the following holds:""") | |
st.latex(f"{profit} = {p}*{sympy.Function(D)(p)} - [{var_cost}*{sympy.Function(D)(p)} + {fixed_cost}]") | |
st.image("assets/images/ideal_case_profit_curve.png") | |
st.markdown("""Note that fixed costs (e.g., rent, insurance, etc.), per definition, don't vary when | |
demand or price changes. Therefore, fixed costs have no influence on the behavior of dynamic pricing | |
algorithms.""") | |
st.markdown("""Finally we can dust off our good old high-school math book and find the | |
price which we expect will optimize profit which was ultimately the goal of all this.""") | |
st.image("assets/images/ideal_case_optimal_profit.png") | |
st.markdown("""Voilร there you have it: we should price this product at 4.24 and we can expect | |
a bottom-line profit of 7.34""") | |
st.markdown("So can we kick back & relax now? \nWell, there are a few issues with what we just did.") | |
# (2) Dynamic demand curves | |
st.header("The demands they are a-changin' ๐ธ") | |
st.markdown("""We arrive at our first bit of bad news: unfortunately, you can't just estimate a | |
demand curve once and be done with it. \nWhy? Because demand is influenced by many factors (e.g., | |
market trends, competitor actions, human behavior, etc.) that tend to change a lot over time.""") | |
st.write("Below you can see an (exaggerated) example of what we're talking about:") | |
with open("assets/images/dynamic_demand.gif", "rb") as file_: | |
contents = file_.read() | |
data_url = base64.b64encode(contents).decode("utf-8") | |
st.markdown( | |
f'<img src="data:image/gif;base64,{data_url}" alt="dynamic demand">', | |
unsafe_allow_html=True, | |
) | |
st.markdown("""Now, you may think we can solve this issue by periodically re-estimating the demand | |
curve. \nAnd you would be very right! But also very wrong as this leads us nicely to the | |
next issue.""") | |
# (3) Constrained data | |
st.header("Where are we getting this data anyways? ๐ฅ๏ธ") | |
st.markdown("""So far, we have assumed that we get (and keep getting) data on demand levels at | |
different price points. \n | |
Not only is this assumption **unrealistic**, it is also very **undesirable**""") | |
st.markdown("""Why? Because getting demand data on a wide spectrum of price points implies that | |
we are spending a significant amount of time setting prices that are either too high or too low! \n | |
Which is ironically exactly the opposite of what we set out to achieve.""") | |
st.markdown("In practice, our demand observations will rather look something like this:") | |
st.image("assets/images/realistic_demand.png") | |
st.markdown("""As we can see, we have tried three price points in the past (โฌ7.5, โฌ10 and โฌ11) and | |
collected demand data.""") | |
st.markdown("""On a side note: keep in mind that we still assume the same latent demand curve and | |
optimal price point of โฌ4.24 \n | |
So (for the sake of the example) we have been massively overpricing our product in the past.""") | |
st.image("assets/images/realistic_demand_latent_curve.png") | |
st.markdown("""This limited data brings along a major challenge in estimating the demand curve | |
though. \n | |
Intuitively, it makes sense that we can make a reasonable estimate of expected demand at โฌ8 or โฌ9, | |
given the observed demand at โฌ7.5 and โฌ10. \nBut can we extrapolate further to โฌ2 or โฌ20 with the | |
same reasonable confidence? Probably not.""") | |
st.markdown("""This is a nice example of a very well-known problem in statistics called the | |
**\"exploration-exploitation trade-off\"** \n | |
๐ **Exploration**: We want to explore the demand for a diverse enough range of price points | |
so that we can accurately estimate our demand curve. \n | |
๐ **Exploitation**: We want to exploit all the knowledge we have gained through exploring and | |
actually do what we set out to do: set our price at an optimal level.""") | |
# (4) Thompson sampling explanation | |
st.header("Enter: Thompson Sampling ๐") | |
st.markdown("""As we mentioned, this is a well-known problem in statistics. So luckily for us, | |
there is a pretty neat solution in the form of **Thompson sampling**!""") | |
st.markdown("""Basically instead of estimating one demand function based on the data available to | |
us, we will estimate a probability distribution of demand functions or simply put, for every | |
possible demand function that fits our functional form (i.e. constant elasticity) | |
we will estimate the probability that it is the correct one, given our data.""") | |
st.markdown("""Or mathematically speaking, we will place a prior distribution on the parameters | |
that define our demand function and update these priors to posterior distributions via Bayes rule, | |
thus obtaining a posterior distribution for our demand function""") | |
st.markdown("""Thompson sampling then entails just sampling a demand function out of this | |
distribution, calculating the optimal price given this demand function, observing demand for this | |
new price point and using this information to refine our demand function estimates.""") | |
st.image("assets/images/flywheel_1.png") | |
st.markdown("""So: \n | |
๐ When we are **less certain** of our estimates, we will sample more diverse demand functions, | |
which means that we will also explore more diverse price points. Thus, we will **explore**. \n | |
๐ When we are **more certain** of our estimates, we will sample a demand function close to | |
the real one & set a price close to the optimal price more often. Thus, we will **exploit**.""") | |
st.markdown("""With that said, we'll take another look at our constrained data and see whether | |
Thompson sampling gets us any closer to the optimal price of โฌ4.24""") | |
st.image("assets/images/realistic_demand_latent_curve.png") | |
st.markdown("""Let's start working our mathemagic: \n | |
We'll start off by placing semi-informed priors on the parameters that make up our | |
demand function.""") | |
st.latex(f"{sympy.latex(a)} \sim N(ฮผ=0,ฯ=2)") | |
st.latex(f"{sympy.latex(eta)} \sim N(ฮผ=0.5,ฯ=0.5)") | |
st.latex("sd \sim Exp(\lambda=1)") | |
st.latex(f"{sympy.latex(D)}|P=p \sim N(ฮผ={sympy.latex(a*p**(-eta))},ฯ=sd)") | |
st.markdown("""These priors are semi-informed because we have the prior knowledge that | |
price elasticity is most likely between 0 and 1. As for the other parameters, we have little | |
knowledge about them so we can place a pretty uninformative prior.""") | |
st.markdown("If that made sense to you, great. If it didn't, don't worry about it") | |
st.markdown("""Now that are priors are taken care of, we can update these beliefs by incorporating | |
the data at the โฌ7.5, โฌ10 and โฌ11 price levels we have available to us.""") | |
st.markdown("The resulting demand & profit curve distributions look a little something like this:") | |
st.image(["assets/images/posterior_demand.png", "assets/images/posterior_profit.png"]) | |
st.markdown("""It's time to sample one demand curve out of this posterior distribution. \n | |
The lucky curve is:""") | |
st.image("assets/images/posterior_demand_sample.png") | |
st.markdown("This results in the following expected profit curve") | |
st.image("assets/images/posterior_profit_sample.png") | |
st.markdown("""And eventually we arrive at a new price: โฌ5.25! Which is indeed considerably closer | |
to the actual optimal price of โฌ4.24""") | |
st.markdown("Now that we have our first updated price point, why stop there?") | |
st.markdown("""With \"pure\" Thompson sampling, we would sample a new demand curve (and thus price | |
point) out of the posterior distribution every time. But since we are mainly interested in seeing | |
the convergence behavior of Thompson sampling, let's simulate 10 demand points at this fixed โฌ5.25 | |
price point.""") | |
st.image("assets/images/updated_prices_demand.png") | |
st.markdown("""We know the drill by now. \n | |
Let's recalculate our posteriors with this extra information.""") | |
st.image(["assets/images/posterior_demand_2.png", "assets/images/posterior_profit_2.png"]) | |
st.markdown("""We immediately notice that the demand (and profit) posteriors are much less spread | |
apart this time around which implies that we are more confident in our predictions.""") | |
st.markdown("Now, we can sample just one curve from the distribution.") | |
st.image(["assets/images/posterior_demand_sample_2.png", "assets/images/posterior_profit_sample_2.png"]) | |
st.markdown("""And finally we arrive at a price point of โฌ4.04 which is eerily close to | |
the actual optimum of โฌ4.24""") | |
# (5) Extra topics | |
st.header("Some things to think about ๐ค") | |
st.markdown("""Because we have purposefully kept the example above quite simple, you may still be | |
wondering what happens when added complexities show up. \n | |
Let's discuss some of those concerns FAQ-style:""") | |
st.subheader("๐ Isn't this constant-elasticity model a bit too simple to work in practice?") | |
st.markdown("Brief answer: usually yes it is.") | |
st.markdown("""Luckily, more flexible methods exist. \n | |
We would recommend to use Gaussian Processes. We won't go into how these work here but the main idea | |
is that it doesn't impose a restrictive functional form onto the demand function but rather lets | |
the data speak for itself.""") | |
with open("assets/images/gaussian_process.gif", "rb") as file_: | |
contents = file_.read() | |
data_url = base64.b64encode(contents).decode("utf-8") | |
st.markdown( | |
f'<img src="data:image/gif;base64,{data_url}" alt="gaussian process">', | |
unsafe_allow_html=True, | |
) | |
st.markdown("""If you do want to learn more, we recommend these links: | |
[1](https://distill.pub/2019/visual-exploration-gaussian-processes/), | |
[2](https://thegradient.pub/gaussian-process-not-quite-for-dummies/), | |
[3](https://sidravi1.github.io/blog/2018/05/15/latent-gp-and-binomial-likelihood)""") | |
st.subheader("""๐ Price optimization is much more complex than just optimizing a simple profit function?""") | |
st.markdown("""It sure is. In reality, there are many added complexities that come into play, such | |
as inventory/capacity constraints, complex cost structures, ...""") | |
st.markdown("""The nice thing about our setup is that it consists of three components that you can | |
change pretty much independently from each other. \n | |
This means that you can make the price optimization pillar arbitrarily custom/complex. As long as | |
it takes in a demand function and spits out a price.""") | |
st.image("assets/images/flywheel_2.png") | |
st.markdown("You can tune the other two steps as much as you like too.") | |
st.image("assets/images/flywheel_3.png") | |
st.subheader("๐ Changing prices has a huge impact. How can I mitigate this during experimentation?") | |
st.markdown("There are a few things we can do to minimize risk:") | |
st.markdown("""๐ **A/B testing**: You can do a gradual roll-out of the new pricing system where a | |
small (but increasing) percentage of your transactions are based on this new system. This allows you | |
to start small & track/grow the impact over time.""") | |
st.markdown("""๐ **Limit products**: Similarly to A/B testing, you can also segment on the | |
product-level. For instance, you can start gradually rolling out dynamic pricing for one product | |
type and extend this over time.""") | |
st.markdown("""๐ **Bound price range**: Theoretically, Thompson sampling in its purest form can | |
lead to any arbitrary price point (albeit with an increasingly low probability). In order to limit | |
the risk here, you can simply place a upper/lower bound on the price range you are comfortable | |
experimenting in.""") | |
st.markdown("""On top of all this, Bayesian methods (by design) explicitly quantify uncertainty. | |
This allows you to have a very concrete view on the variance of our demand estimates""") | |
st.subheader("๐ What if I have multiple products that can cannibalize each other?") | |
st.markdown("Here it really depends") | |
st.markdown("""๐ **If you have a handful of products**, we can simply reformulate our objective while | |
keeping our methods analogous. \n | |
Instead of tuning one price to optimize profit for the demand function of one product, we tune N | |
prices to optimize profit for the joint demand function of N products. This joint demand function | |
can then account for correlations in demand within products.""") | |
st.markdown("""๐ **If you have hundreds, thousands or more products**, we're sure you can imagine that | |
the procedure described above becomes increasingly infeasible. \n | |
A practical alternative is to group substitutable products into "baskets" and define the "price of | |
the basket" as the average price of all products in the basket. \n | |
If we assume that the products in baskets are subtitutable but the products in different baskets are | |
not, we can optimize basket prices indepedently from one another. \n | |
Finally, if we also assume that cannibalization remains constant if the ratio of prices remains | |
constant, we can calculate individual product prices as a fixed ratio of its basket price. \n""") | |
st.markdown("""For example, if a "burger basket" consists of a hamburger (โฌ1) and a cheeseburger | |
(โฌ3), then the "burger price" is ((โฌ1 + โฌ3) / 2 =) โฌ2. So a hamburger costs 50% of the burger price | |
and a cheeseburger costs 150% of the burger price. \n | |
If we change the burger's price to โฌ3, a hamburger will cost (50% * โฌ3 =) โฌ1.5 and a cheeseburger | |
will cost (150% * โฌ3 =) โฌ4.5 because we assume that the cannibalization effect between hamburgers & | |
cheeseburgers is the same when hamburgers cost โฌ1 & cheeseburgers cost โฌ3 and when hamburgers cost | |
โฌ1.5 & cheeseburgers cost โฌ4.5""") | |
st.image("assets/images/cannibalization.png") | |
st.subheader("๐ Is dynamic pricing even relevant for slow-selling products?") | |
st.markdown("""The boring answer is that it depends. It depends on how dynamic the market is, the | |
quality of the prior information, ...""") | |
st.markdown("""But obviously this isn't very helpful. \nIn general, we notice that you can already | |
get quite far with limited data, especially if you have an accurate prior belief on how the demand | |
likely behaves.""") | |
st.markdown("""For reference, in our simple example where we showed a Thompson sampling update, we | |
were already able to gain a lot of confidence in our estimates with just 10 extra demand | |
observations.""") | |
# (6) Thompson sampling demo | |
st.header("Demo time ๐ฎ") | |
st.markdown("Now that we have covered the theory, you can go ahead and try it our for yourself!") | |
st.markdown("""You will notice a few things: \n | |
๐ As you increase price elasticity, the demand becomes more sensitive to price changes and thus the | |
profit-optimizing price becomes lower (& vice versa). \n | |
๐ As you decrease price elasticity, our demand observations at โฌ7.5, โฌ10 and โฌ11 become | |
increasingly larger and increasingly more variable (as their variance is a constant fraction of the | |
absolute value). This causes our demand posterior to become increasingly wider and thus Thompson | |
sampling will lead to more exploration. | |
""") | |
thompson_sampler = ThompsonSampler() | |
demo_button = st.checkbox( | |
label='Ready for the Demo? ๐น๏ธ', | |
help="Starts interactive Thompson sampling demo" | |
) | |
elasticity = st.slider( | |
"Adjust latent elasticity", | |
key="latent_elasticity", | |
min_value=0.05, | |
max_value=0.95, | |
value=0.25, | |
step=0.05, | |
) | |
while demo_button: | |
thompson_sampler.run() | |
time.sleep(1) | |