Toggle navigation
Home
▼ Details
Products and pricing
Chart gallery
User stories
Text analytics
CDC NAMCS Library
Blog
Tutorials
Contact
Sign in
Post Editor
← All blog posts
View post
Save
<script src="/javascripts/lodash.js"></script> <script src="/javascripts/d3.v3.min.js"></script> <script type="text/javascript" src="/javascripts/lambertw.js"></script> <script src="//cdnjs.cloudflare.com/ajax/libs/KaTeX/0.3.0/katex.min.js" integrity="sha384-YHz3OPpEiyi/sO8G+CnFwlGoQan0XxC2X5R5+8YegwctRq2pp1XL16HCbnR9OINp" crossorigin="anonymous"></script> <link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/KaTeX/0.3.0/katex.min.css" integrity="sha384-S3Z2uALIv2vxM7pcFsnZXgr5cpBsIUD20AHUT9zi8aWpI9jkG3mRjrvD/yQcGyys" crossorigin="anonymous"><style> .axis path, .axis line { fill: none; stroke: black; shape-rendering: crispEdges; } .hover { stroke: #fc0; stroke-width: 4px; } .axis text { font-family: sans-serif; font-size: 11px; } .tw-bs .label { font-family: sans-serif; font-size: 14px ; font-weight: normal; } </style> <p>How do you find the optimal price for a good or service? Obviously, it depends what you mean by optimal. And pricing is a hugely complex problem. But if your goal is narrowly defined to maximize expected profit based on a discrete choice logit model, this page has an elegant new solution.</p> <p>We present a simple analytic formula for the optimal price in a discrete choice pricing model. Here, the optimal price is the one that maximizes the expected revenue (or profit), balancing the revenue versus the likelihood of purchase. This formula allows the optimal price to be quickly calculated precisely for each individual customer, for further analysis and action.</p> <!--more--> <h1>Background</h1> <p>Discrete choice models are commonly used in market research to model purchase behavior. There are many variations of the discrete choice models, but the central idea here is that the probability of purchase depends on the price:</P> <li><span class="katex">R_i</span> is the price, i.e. revenue received if an individual customer <span class="katex">i</span> makes a purchase.</li> <li><span class="katex">Pr(R_i)</span> is the probability that customer <span class="katex">i</span> purchases the product at that price.</li> <br> So the expected revenue is the revenue given purchase times the likelihood of purchase: <div class="katex" data-display="true" >E[R_i] = R_i \cdot Pr(R_i)</div> If the focus of the analysis is maximizing gross revenue, then <span class="katex">R_i</span> is the actual price paid by the customer. If instead, the focus is maximizing net revenue, then <span class="katex">R_i</span> is the marginal profit received after direct expenses (the actual price that the customer sees may be higher). Fixed costs, if any, are constant with respect to price, and so can be excluded from this analysis.</p> <h1>Discrete Choice Logit Model</h1> <p>In the discrete choice logit model, the likelihood of purchase depends on a latent construct "utility", using the non-linear logit transform: <div class="katex" data-display="true" >Pr(R_i) = \frac{1}{1 + e^{-u_i}}</div> The utility can't be directly observed, but may be inferred statistically from observed behavior. Typically, utility is a modeled as a linear function of price and other product/customer attributes:<p> <div class="katex" data-display="true" >u_i = \alpha - \beta * R_i</div> <p>where:</p> <ul> <li><span class="katex">\alpha</span> is the utility of the product if free, given all other product/consumer attributes.</li> <li><span class="katex">\beta</span> is decrease in product utility for each unit increase in price.</li> </ul> <p> The probability of purchase is thus a non-linear transform of price:</p> <div class="katex" data-display="true" >Pr(R_i) = \frac{1}{1 + e^{-(\alpha - \beta R_i)}}</div> <p>The discrete choice logit model is often useful in practice as it can be estimated via logistic regression from customer data, either via controlled experiement or from purchase history.<p> <h1>Optimal price</h1> <p>The optimal price <span class="katex">R_i^*</span> is the price which maximizes expected revenue from that customer : <div class="katex" data-display="true" > E[R_i] = R_i \cdot \frac{1}{1 + e^{-(\alpha - \beta R_i)}}</div> <p>This is a non-linear function. To find the optimal price, the most common approach is to simply try a number of different prices, and choose the one that yields the highest expected revenue. Algorithms such as Binary Search and Newton-Raphson can speed that up.</p> <p>It turns out this function can be maximized analytically. Per algebraic derivation by our intern <a href="http://liamcwalsh.herokuapp.com/discrete_choice"> Liam C. Walsh</a>, the optimal price (with respect to expected total revenue) is a simple formula in terms of the <a href="https://en.wikipedia.org/wiki/Lambert_W_function">Lambert W function</a>:<p> <div class="katex" data-display="true" >R_i^* = \frac { W(e^{\alpha-1}) + 1 } {\beta}</div></b> <h1>Lambert W</h1> <p>The Lambert W function is rooted in physics and widely implemented, e.g. in Mathematica, Matlab. We've ported the algorithm in the <a href="https://www.gnu.org/software/gsl/">GNU Scientific Library</a> from C to Javascript and published it to the <a href="https://github.com/protobi/lambertw">Protobi GitHub repo</a>. The graphs below are dynamically calculated in your browser using this library.</p> <p>Below is a graph of <span class=katex>W(e^{x})</span> vs.<span class=katex>x</span>. Note the graph is nearly linear for large values of <span class=katex>\alpha</span> (i.e. when demand is high at zero price) and approaches zero asymptotically for low values of <span class=katex>\alpha</span> (i.e. when demand is low even at zero price).</p> <div id="lambert">Plot W(exp(x)) vs x...</div> <h1>Probability of purchase vs price</h1> <p>The graph below shows the probability of purchase versus price for differing values of <span class=katex>\alpha</span> from -4.0 (i.e. unlikely to purchase even if free) to 6.0 (i.e. certain to purchase if free and even up to substantial prices), and holding the price sensitivity constant at <span class=katex>\beta</span> = 0.008.<p> <p>These values are chosen so that the curves are interesting over a price range of $0 to $1,000. Actually, these are very representative of the range of individual customers' base utilities and price sensitivity in an actual purchase behavior dataset. The formula is generally applicable to any feasible values of alpha and beta.<p> <p>Each curve can represent an individual customer. Some customers may be more likely to purchase a product than others, given their unique preferences and attributes. In fact, the origin of this analysis was precisely to estimate optimal discount offerings for individual customers, and the range of values here is very illustrative of the actual data.</p> <div id="probabilities"></div> <p>The dots superimposed on the graph show probability of purchase at the optimal price <span class="katex">R_i^*</span>.<p> <h1>Expected revenue vs price</h1> <p>The graph below shows the expected revenue versus price over the same values of <span class=katex>\alpha</span> from -4.0 (i.e. unlikely to purchase even if free) to 6.0 (i.e. certain to purchase if free and even up to substantial prices), and again holding price senstivity constant at <span class=katex>\beta</span> = 0.008. The dots superimposed show the expected revenue at the optimal price.</p> <div id="streamlines"></div> <h1>Discussion</h1> <p>It's interesting to see that for customers with higher values of latent utility, higher prices are optimal. But for customers who are increasingly unlikely to purchase even if free, the optimal price approaches a lower bound well above free. In fact, the minimum bound on optimal price is a function strictly of the price sensitivity:</p> <div class=katex data-display="true">R_i^* > \frac{1}{\beta} \forall \alpha</div> <p>Conceptually, the lower bound can be interpreted as the price equivalent to one unit of utility. So even if the customer doesn't want the product at all, it's still optimal to price it well above zero.<p> <p>It's also interesting to see that the expected revenue at the optimal price appears to be a linear function of the optimal price.</P> <div id="probabilities"></div><p> <h1>Summary</h1> <p>We present here a simple formula for optimal price in a discrete choice logit pricing model. It is applicable to a wide range of pricing studies. With simple adjustments, it can be applied to modeling discount instead of price. </P> <p>It can accommodate complex models with nesting and interaction effects. For most effects, interactions and nesting that do not involve price will be compiled into the <span class=katex>\alpha</span> term and constant for any one customer. Similarly, where these interactions and nesting do affect price, the customer has an individual <span class=katex>\beta</span> term.</p> <p>It is especially useful where price (or conversely discount) can be applied at the individual customer level, such as with hierarchical Bayes choice models, as this can avoid the need for complex or time consuming optimizations. </P> <p>Realistically, this formula may have been independently derived many times by others (as the Lambert W function itself has been). This is one of those things where we couldn't find that result in prior art, and it turned out to be easier (and way more fun) to derive it ourselves than to keep searching. Let us know at <a href="mailto:info@protobi.com">info@protobi.com</a> if you find this useful or know of a prior link.</p> <h1>Links</h1> R.M. Corless, G.H. Gonnet, D.E.G. Hare, D.M. Jeffrey and D. E. Knuth, <a href="https://cs.uwaterloo.ca/research/tr/1993/03/W.pdf">"On Lambert's W Function"</a>, Technical Report CS-93-03, University of Waterloo, January 1993. <br><br><br> <script> // draw streamlines var render_streamlines = function (el, attributes, rows) { console.log("Rendering streamlines"); var self = this; this.$el = $(el); this.el = this.$el[0] this.attributes = attributes; this.$el.html("<h1>Hi!</h1>"); var svg = this.svg = d3.select(this.el).html('').append('svg') .style('height', this.attributes.height) .style('width', this.attributes.width); xScale = d3.scale.linear() .domain(this.attributes.xAxisDomain) .range([this.attributes.margin.left, this.attributes.width - this.attributes.margin.right]); yScale = d3.scale.linear() .domain(this.attributes.yAxisDomain) .range([this.attributes.height - this.attributes.margin.bottom, this.attributes.margin.top]); xAxis = d3.svg.axis() .scale(xScale) .orient('bottom') .ticks(5) .tickFormat(d3.format(this.attributes.xAxisFmt)) ; yAxis = d3.svg.axis() .scale(yScale) .ticks(5) .orient('left') .tickFormat(d3.format(this.attributes.yAxisFmt)) ; svg.append('g') .attr('class', 'axis') .attr('transform', "translate(0, " + (this.attributes.height - this.attributes.margin.bottom) + ")") .call(xAxis); svg.append('g') .attr('class', 'axis') .attr('transform', "translate(" + this.attributes.margin.left + ", 0)") .call(yAxis); var gradient = svg.append("svg:defs") .append("svg:linearGradient") .attr("id", "gradient") .attr("x1", "0%") .attr("y1", "100%") .attr("x2", "100%") .attr("y2", "100%") gradient.append("svg:stop") .attr("offset", "0%") .attr("stop-color", "#36f") .attr("stop-opacity", 0.2); gradient.append("svg:stop") .attr("offset", "50%") .attr("stop-color", "#36f") .attr("stop-opacity", 1); gradient.append("svg:stop") .attr("offset", "100%") .attr("stop-color", "#fff") .attr("stop-opacity", 0.1); svg.append("text") .attr("class", "x label") .attr("text-anchor", "end") .attr("x", this.attributes.width / 2) .attr("y", this.attributes.height - 6) .text(this.attributes.xAxisTitle); svg.append("text") .attr("class", "y label") .attr("transform", "rotate(-90)") .attr("x", 0 - (this.attributes.height / 2)) .attr("dy", "1em") .style("text-anchor", "middle") .text(this.attributes.yAxisTitle); rows.forEach(function (row) { var line = d3.svg.line() .x(function (price) { return xScale(price); }) .y(function (price) { return yScale( self.attributes.yFunc(row.alpha, row.beta, price)); }); var prices = Array(50); for (var i = 0; i < prices.length; i++) { prices[i] = this.attributes.xAxisDomain[0] + (i/prices.length) * (this.attributes.xAxisDomain[1] - this.attributes.xAxisDomain[0]) } var line = svg.append("path") .attr("d", line(prices)) .attr("stroke", "blue") .attr("stroke-width", 1) .attr("fill", "none") .style("stroke", "url(#gradient)") .on("mouseover", function (d) { d3.select(this).classed("hover", true); }) .on("mouseout", function (d) { d3.select(this).classed("hover", false); }); var dot = svg.append('circle') .attr('cx', function () { return xScale(row.price); }) .attr('cy', function () { return yScale(self.attributes.yFunc(row.alpha, row.beta, row.price)); }) .attr('r', 2) .style('fill', "#36f") }); return this; } get_pstar = function (alpha, beta) { return ( We(alpha - 1) + 1) / beta; } var We = function (x) { var arg = Math.exp(x); var result = gsl_sf_lambert_W0_e(arg); return result.val; }; var logistic = function (alpha, beta, price) { var util = alpha - beta * price; var prob = 1 / (Math.exp(-util) + 1); return prob; } var rows = []; for (var i = 0; i < 10; i++) { rows[i] = { alpha: -4.0 + i * 1, beta: 0.008, coa: 1000, } rows[i].price = get_pstar(rows[i].alpha, rows[i].beta); } // Render once with expected profit render_streamlines("#streamlines", { width: 600, height: 400, xAxisTitle: 'Price', yAxisTitle: 'Expected Profit', yAxisDomain: [0,500], xAxisDomain: [0,1000], yAxisFmt: '$,0f', xAxisFmt: '$,.0f', yFunc : function(alpha, beta, price) { return price * logistic(alpha, beta, price)}, margin: { top: 20, left: 100, bottom: 60, right: 40 } }, rows); // Render once with probabilities render_streamlines("#probabilities", { width: 600, height: 400, xAxisTitle: 'Price', yAxisTitle: 'Probability of purchase', yAxisDomain: [0,1], xAxisDomain: [0,1000], yAxisFmt: '0%', xAxisFmt: '$,.0f', yFunc : function(alpha, beta, price) { return logistic(alpha, beta, price)}, margin: { top: 20, left: 100, bottom: 60, right: 40 } }, rows); var render_lambert = function (el, attributes, rows) { console.log("Rendering streamlines"); var self = this; this.$el = $(el); this.el = this.$el[0] this.attributes = attributes; this.$el.html("<h1>Hi!</h1>"); var svg = this.svg = d3.select(this.el).html('').append('svg') .style('height', this.attributes.height) .style('width', this.attributes.width); xScale = d3.scale.linear() .domain(this.attributes.xAxisDomain) .range([this.attributes.margin.left, this.attributes.width - this.attributes.margin.right]); yScale = d3.scale.linear() .domain(this.attributes.yAxisDomain) .range([this.attributes.height - this.attributes.margin.bottom, this.attributes.margin.top]); xAxis = d3.svg.axis() .scale(xScale) .orient('bottom') .ticks(5) .tickFormat(d3.format(this.attributes.xAxisFmt)) ; yAxis = d3.svg.axis() .scale(yScale) .ticks(5) .orient('left') .tickFormat(d3.format(this.attributes.yAxisFmt)) ; svg.append('g') .attr('class', 'axis') .attr('transform', "translate(0, " + (yScale(0)) + ")") .call(xAxis); svg.append('g') .attr('class', 'axis') .attr('transform', "translate(" + (xScale(0))+ ", 0)") .call(yAxis); var gradient = svg.append("svg:defs") .append("svg:linearGradient") .attr("id", "gradient") .attr("x1", "0%") .attr("y1", "100%") .attr("x2", "100%") .attr("y2", "100%") gradient.append("svg:stop") .attr("offset", "0%") .attr("stop-color", "#36f") .attr("stop-opacity", 0.2); gradient.append("svg:stop") .attr("offset", "50%") .attr("stop-color", "#36f") .attr("stop-opacity", 1); gradient.append("svg:stop") .attr("offset", "100%") .attr("stop-color", "#fff") .attr("stop-opacity", 0.1); svg.append("text") .attr("class", "x label") .attr("text-anchor", "end") .attr("x", this.attributes.width / 2) .attr("y", this.attributes.height - 6) .text(this.attributes.xAxisTitle); svg.append("text") .attr("class", "y label") .attr("transform", "rotate(-90)") .attr("x", 0 - (this.attributes.height / 2)) .attr("dy", "1em") .style("text-anchor", "middle") .html(this.attributes.yAxisTitle); var xvals = Array(50); for (var i = 0; i < xvals.length; i++) { xvals[i] = this.attributes.xAxisDomain[0] + (i / xvals.length) * (this.attributes.xAxisDomain[1] - this.attributes.xAxisDomain[0]) } var line = d3.svg.line() .x(function (xval) { return xScale(xval); }) .y(function (xval) { console.log(xval,this.attributes.yFunc(xval) ) return yScale(this.attributes.yFunc(xval)); }); var line = svg.append("path") .attr("d", line(xvals)) .attr("stroke", "blue") .attr("stroke-width", 1) .attr("fill", "none") .style("stroke", "url(#gradient)") .on("mouseover", function (d) { d3.select(this).classed("hover", true); }) .on("mouseout", function (d) { d3.select(this).classed("hover", false); }); ; return this; } // Render once with probabilities render_lambert("#lambert", { width: 600, height: 400, xAxisTitle: 'x', yAxisTitle: 'W(exp(x))', yAxisDomain: [0, 4], xAxisDomain: [-4, 6], yAxisFmt: '.0f', xAxisFmt: '.0f', yFunc: function (x) { return gsl_sf_lambert_W0(Math.exp(x)); }, margin: { top: 20, left: 100, bottom: 60, right: 40 } }, rows); // this formats the LaTeXs equation expressions $(function() { $(".katex").each(function() { console.log(this.innerHTML); katex.render(_.unescape(this.innerHTML), this, {displayMode: $(this).data('display') }); }); }); // returns pair of iid standard normal random variables // see http://stackoverflow.com/a/2751988/645715 function rng() { var r = Math.sqrt(-2 * Math.log(Math.random())); var theta = 2 * Math.PI * Math.random(); var x = r * Math.cos(theta); var y = r * Math.sin(theta); return [x,y]; } </script> <img src="/uploads/images/image-2015-7-15-price-2024-08-30-03-15-23.png" alt="" style="max-width: 400px"/>
Date
Status
Published
Draft
Slug
edit
Thumbnail
Categories
Manage
Release
Features
Datasets
Surveys
Tips
NAMCS
Applications
Crosstab
Tutorial
Design
Concepts
Segmentation
Examples
Blog Test Category
Delete
Convert to MD