Dynamic Product Pricing Using Python

by Pritish Jadhav - Sun, 03 Jan 2021
Tags: #python #Data Science #Ecommerce #Machine Learning

Dynamic Price Optimization - Single Product

  • The year 2020 was arguably one of the most difficult years on the professional as well as personal fronts. The COVID-19 pandemic hit us hard and forced us to seek safe havens by practically giving up on socializing.
  • The pandemic also severely affected business and economic growth across industries and nations.
  • It also changed the way we have been conducting businesses until now. The lockdown and physical distancing measures presented a unique challenge to the brick and mortar retail stores that rely heavily on foot traffic for moving their products.
  • The pandemic has forced people to adopt online shopping more comprehensively for their daily needs.
  • The whole shift in paradigm has prompted businesses to start building an online presence to sustain, survive, and eventually grow in the post-pandemic world.
  • However, selling online is not new and trivial. Brands have been selling online for quite some time and the competition in certain categories is often cut-throat.
  • Product pricing plays a pivotal role at various stages of a product lifecycle and has a direct impact on a brand's bottom line.
  • In this blog post, we shall use the explore-exploit strategy for determining the optimal price for a SINGLE product.


In [6]:
import numpy as np
import pandas as pd
import random
import matplotlib.pyplot as plt
from typing import NamedTuple, List
from mypy_extensions import TypedDict
from collections import defaultdict
import ipywidgets as widgets
import scipy.stats as stats
from ipywidgets import interact, interactive, fixed, interact_manual

from IPython.display import HTML
display(HTML('<style>.prompt{width: 0px; min-width: 0px; visibility: collapse}</style>'))
display(HTML("<style>.container { width:100% !important; }</style>"))

A true demand model (Unobserved in Real Life) -

  • To kick things off, lets define a mathematical model for determining the "true" demand for the product under consideration.
  • It is important to note that this true demand is unobserved in a real-life scenario.
  • We will use this model to generate observed values that will be used by the algorithm for updating the beliefs.
  • We shall use a simple linear model with a slope and intercept to define the true demand.



$demand(price|\theta) = {\theta}_1 + {\theta}_2 * price $

Where
${\theta_1}$ -> intercept
${\theta_2}$ -> slope
  • Things in the wild are hardly linear but hopefully, this will lay the foundation for the problem that we are trying to solve.

  • For the sake of this blog post, we shall choose ${\theta_1} = 50$ and ${\theta_2} = -7$. Please note that these values have been chosen heuristically.

$\therefore demand(price|{\theta}) = 50 - 7* price$
In [ ]:
theta_1 = 50
theta_2 = -7

plot_prices = np.linspace(1.99, 4.99, 400)

# Visualize the true demand model 

true_demand = theta_1 + (theta_2 * plot_prices)

plt.figure(figsize = (5,5))
plt.plot(plot_prices, true_demand, label = "true demand")
plt.plot(plot_prices, plot_prices * true_demand, label = "revenue")
plt.axvline(x = 3.56, linestyle='--',color = 'r')

plt.plot(3.56, 25.017, '--bo', color = 'red')
# plt.annotate((3.56, 25.017), [3.56, 25.017])

plt.plot(3.56, 89.28, '--bo', color = 'red')
plt.annotate((3.56, 89.28), xy = [3.56, 89.28],
             xytext=(30, -50),
             textcoords = "offset points", 
             arrowprops=dict(arrowstyle="->", color='red'))
plt.annotate((3.56, 25.017), xy = [3.56, 25.017],
             xytext=(30, 50),
             textcoords = "offset points", 
             arrowprops=dict(arrowstyle="->", color='red'))

plt.legend()
In [ ]:
print(true_demand[np.argmax(true_demand * plot_prices)])
print(plot_prices[np.argmax(true_demand * plot_prices)])

np.max(true_demand * plot_prices)

A Note on the true optimal price -

  • To keep things real, we shall be testing a finite set of prices for our product under consideration.
  • The prices to be tested are \$2.99, \\$3.99, \$4.99, \\$5.99.
  • It can be seen from the above graph that the price of \$3.99 is the most optimal with a demand of 50 - 7*3.99 = 22.07 and revenue of \\$88.05.
  • So at the end of this blog, our algorthm should be able to explore all prices but exploit the price of \$3.99 i.e select \\$3.99 more often as compared to other price points.

Algorithm Walkthrough -

In this section, let's discuss a few approaches and finalize the algorithm that we would adopt for solving the problem -

The Greedy approach -

  • A naive and greedy way of determining the optimal price of a product would be to collect the historical data and choose the price that maximizes revenue.
  • However, this approach is suboptimal because it prevents us from choosing prices that do not have enough data. As a result, we will never know if the selected price is indeed the most optimal.
  • Hence the greedy algorithm is designed for maximizing the short-term gains while missing out on exploring all viable options.

The $\epsilon$ Greedy Algorithm -

  • The $\epsilon$ greedy algorithm alleviates the critical drawback of the greedy algorithm by adopting the greedy approach with probability $1- \epsilon$ and explores with a probability $\epsilon$.
  • Typically, the value of $\epsilon$ is chosen to be small.
  • In the exploration phase, the algortihm would allocate experimental actions using a uniform distribution. Therefore, it would choose each experimental action an equal number of times.
  • As a result,the $\epsilon$-greedy algorithm fails to discount actions with a very low probability of being optimal.

Thompson Sampling to the rescue -

  • The Thompson sampling approach solves the drawbacks from earlier mentioned approaches where we kick off the process by attaching prior beliefs to each of the available options.
  • At every time step, we select the optimal option, observe and update the belief.
  • For determining the optimal price for a product using Thompson sampling, we would assume the demand to be a Poisson process with its parameter $\lambda$ being gamma distributed with parameters $\alpha$ and $\beta$.
  • Then, the pseudocode for Thompson sampling in the context of Dynamic Pricing for a single product would be -

Psuedocode -

  • Define a prior distribution $p(\lambda) \sim gamma(\alpha, \beta)$ for each of the price points to be tested.

  • At each time step t-

  1. sample demands for each price points i.e d $\sim$ p($\lambda)$
  2. Find the optimal price -
    $price^*$ = $argmax_{price}$ [price * demand]
  3. Offer the optimal price and observe the true demand ($d_t$).
  4. Update the belief for the selected price point using -
$\alpha$ = $\alpha + d_t$
$\beta = \beta +1 $
In [ ]:
# define the prices to be tested 

prices_to_test = np.arange(2.49, 5.99, 1)

# define the prior values for the alpha and beta that define a gamma distribution
alpha_0 = 30.00     
beta_0 = 1.00

def sample_true_demand(price: float) -> float:
    """
    np.poisson.random -> https://numpy.org/doc/stable/reference/random/generated/numpy.random.poisson.html
    """
    demand = theta_1 + theta_2 * price
    return np.random.poisson(demand, 1)[0]


class priceParams(TypedDict):
    price: float
    alpha: float
    beta: float

        
p_lambdas = []
for price in prices_to_test:
    p_lambdas.append(
        priceParams(
            price=price, 
            alpha=alpha_0, 
            beta=beta_0
        )
    )
    
   
class OptimalPriceResult(NamedTuple):
    
    price: float
    price_index: int
        
def get_optimal_price(prices: List[float], demands: List[float]) -> OptimalPriceResult:
    index = np.argmax(prices * demands)
    return OptimalPriceResult(price_index = index, price = prices[index])

def sample_demands_from_model(p_lambdas: List[priceParams]) -> List[float]:
    
    return list(map(lambda v: np.random.gamma(v['alpha'], 1/v['beta']), p_lambdas))

def plot_distributions(gamma_distributions: List[priceParams], iteration: int):
    
    x = np.arange(0, 50, 0.10)
    for dist in p_lambdas:
        y = stats.gamma.pdf(x, a=dist["alpha"], scale= 1/dist["beta"])
        plt.plot(x, y, label = dist["price"])
        plt.xlabel("demand")
        plt.ylabel("pdf")
    plt.title(f"PDFs after Iteration: {iteration}")
    plt.legend(loc="upper right")
    plt.show()
In [ ]:
# Thompson sampling for solving the explore-exploit dilema. 


price_counts = defaultdict(lambda: 0)

for t in range(200):
    demands = sample_demands_from_model(p_lambdas)

    optimal_price_res = get_optimal_price(prices_to_test, demands)
    
    # increase the count for the price
    price_counts[optimal_price_res.price] += 1

    # offer the selected price and observe demand
    demand_t = sample_true_demand(optimal_price_res.price)

    # update model parameters
    v = p_lambdas[optimal_price_res.price_index]
    v['alpha'] += demand_t
    v['beta'] += 1
    
    if t%10 == 0:
        plot_distributions(p_lambdas, t)






Next Steps -

  • The concepts discussed in this blog post lays the foundation for building more complex demand distribution models instead of sticking to linear relationships.
  • It is worth exploring different techniques including MCMC simulations for building and simulating complex distributions.
  • The above formulation works for a single product but in real-world applications, we would want to experiment for multiple products along with a bunch of constraints.

Comments