### Install pricesensitivitymeter Source: https://max-alletsee.github.io/pricesensitivitymeter/index.html Commands to install the stable version from CRAN or the development version from GitHub. ```R # install the stable release from CRAN install.packages("pricesensitivitymeter") # install the development version from Github devtools::install_github("max-alletsee/pricesensitivitymeter") ``` -------------------------------- ### Set seed and load survey package for identical weights example Source: https://max-alletsee.github.io/pricesensitivitymeter/articles/weighted-data.html Prepare environment for the example that demonstrates identical results when weights are identical. Uses set.seed for reproducibility and loads the survey package. ```r set.seed(20) library(survey) ``` -------------------------------- ### Generate Example Data for PSM Analysis Source: https://max-alletsee.github.io/pricesensitivitymeter/articles/visualizing-psm-results.html Creates a sample dataset and performs a PSM analysis using the pricesensitivitymeter package. ```R library(pricesensitivitymeter) set.seed(20) toocheap <- round(rnorm(n = 250, mean = 8, sd = 1.5), digits = 2) cheap <- round(rnorm(n = 250, mean = 12, sd = 2), digits = 2) expensive <- round(rnorm(n = 250, mean = 13, sd = 1), digits = 2) tooexpensive <- round(rnorm(n = 250, mean = 15, sd = 1), digits = 2) # optional: purchase intent for "cheap" and "expensive price" (Newton Miller Smith Extension) pi_cheap <- sample(x = c(1:5), size = length(cheap), replace = TRUE, prob = c(0.1, 0.1, 0.2, 0.3, 0.3)) pi_expensive <- sample(x = c(1:5), size = length(expensive), replace = TRUE, prob = c(0.3, 0.3, 0.2, 0.1, 0.1)) data_psm <- data.frame(toocheap, cheap, expensive, tooexpensive, pi_cheap, pi_expensive) output_psm <- psm_analysis(toocheap = "toocheap", cheap = "cheap", expensive = "expensive", tooexpensive = "tooexpensive", data = data_psm, pi_cheap = "pi_cheap", pi_expensive = "pi_expensive", validate = TRUE) ``` -------------------------------- ### Generate and plot PSM analysis results Source: https://max-alletsee.github.io/pricesensitivitymeter/reference/psm_plot.html This example demonstrates how to prepare dummy data, run the psm_analysis function, and visualize the results using psm_plot. ```R # set up example data and run psm_analysis() tch <- round(rnorm(n = 250, mean = 5, sd = 0.5), digits = 2) ch <- round(rnorm(n = 250, mean = 8.5, sd = 0.5), digits = 2) ex <- round(rnorm(n = 250, mean = 13, sd = 0.75), digits = 2) tex <- round(rnorm(n = 250, mean = 17, sd = 1), digits = 2) output_psm_demo <- psm_analysis(toocheap = tch, cheap = ch, expensive = ex, tooexpensive = tex) # create the plot (note that ggplot's convention # is to *not* show it by default) if (FALSE) psm_result_plot <- psm_plot(output_psm_demo) # to show the plot, call the object (and maybe # additional ggplot functions if you like) psm_result_plot + ggplot2::theme_minimal() # \dontrun{} #> Error: object 'psm_result_plot' not found ``` -------------------------------- ### Run psm_analysis Source: https://max-alletsee.github.io/pricesensitivitymeter/index.html Create example data and perform the van Westendorp Price Sensitivity Meter analysis using the psm_analysis function. ```R ## creating example data tch <- round(rnorm(n = 250, mean = 8.5, sd = 1), digits = 2) ch <- round(rnorm(n = 250, mean = 10, sd = 1), digits = 2) ex <- round(rnorm(n = 250, mean = 12, sd = 0.75), digits = 2) tex <- round(rnorm(n = 250, mean = 13, sd = 1), digits = 2) data_psm_demo <- data.frame(tch, ch, ex, tex) library(pricesensitivitymeter) ## running the analysis output_psm_demo <- psm_analysis( toocheap = "tch", cheap = "ch", expensive = "ex", tooexpensive = "tex", data = data_psm_demo ) summary(output_psm_demo) #> Van Westendorp Price Sensitivity Meter Analysis #> #> Accepted Price Range: 9 - 12.48 #> Indifference Price Point: 10.94 #> Optimal Price Point: 10.64 #> #> --- #> 143 cases with individual price preferences were analyzed (unweighted data). #> Total data set consists of 250 cases. Analysis was limited to cases with transitive price preferences. #> (Removed: n = 107 / 43% of data) ``` -------------------------------- ### Run psm_analysis() with vectors and data.frame including Newton Miller Smith extension Source: https://max-alletsee.github.io/pricesensitivitymeter/reference/psm_analysis.html R example that first calls psm_analysis() with direct numeric vectors (toocheap, cheap, expensive, tooexpensive) and then with a data.frame that adds purchase intention columns (pi_cheap, pi_expensive) to enable the Newton Miller Smith extension. The output shows the accepted price range, indifference price point, optimal price point, and the Newton Miller Smith extension results for optimal reach and revenue. ```r set.seed(42) # standard van Westendorp Price Sensitivity Meter Analysis # input directly via vectors tch <- round(rnorm(n = 250, mean = 5, sd = 0.5), digits = 2) ch <- round(rnorm(n = 250, mean = 8.5, sd = 0.5), digits = 2) ex <- round(rnorm(n = 250, mean = 13, sd = 0.75), digits = 2) tex <- round(rnorm(n = 250, mean = 17, sd = 1), digits = 2) output_psm_demo1 <- psm_analysis(toocheap = tch, cheap = ch, expensive = ex, tooexpensive = tex) # additional analysis with Newton Miller Smith Extension # input via data.frame pint_ch <- sample(x = c(1:5), size = length(tex), replace = TRUE, prob = c(0.1, 0.1, 0.2, 0.3, 0.3)) pint_ex <- sample(x = c(1:5), size = length(tex), replace = TRUE, prob = c(0.3, 0.3, 0.2, 0.1, 0.1)) data_psm_demo <- data.frame(tch, ch, ex, tex, pint_ch, pint_ex) output_psm_demo2 <- psm_analysis(toocheap = "tch", cheap = "ch", expensive = "ex", tooexpensive = "tex", pi_cheap = "pint_ch", pi_expensive = "pint_ex", data = data_psm_demo) summary(output_psm_demo2) #> Van Westendorp Price Sensitivity Meter Analysis #> #> Accepted Price Range: 6.35 - 14.53 #> Indifference Price Point: 9.98 #> Optimal Price Point: 6.35 #> #> Newton Miller Smith Extension #> Price with Optimal Reach: 8.8 #> Price with Optimal Revenue: 9.4 #> #> --- #> 250 cases with individual price preferences were analyzed (unweighted data). ``` -------------------------------- ### Base R PSM plot with reduced subtitle font Source: https://max-alletsee.github.io/pricesensitivitymeter/articles/visualizing-psm-results.html Starts a base R plotting example for the same PSM plot. Sets the subtitle font size to 0.66 using par(cex.sub). The example is incomplete as shown. ```r par(cex.sub = 0.66) # reducing the font size of the subtitle ``` -------------------------------- ### Create skewed sample data for weighted PSM analysis Source: https://max-alletsee.github.io/pricesensitivitymeter/reference/psm_analysis_weighted.html Example R code that creates a data frame with 250 observations for the four price assessments (tch, ch, ex, tex) and a gender variable with a skewed distribution (2/3 male, 1/3 female). This data is intended for use with psm_analysis_weighted. ```R # assuming a skewed sample with only 1/3 women and 2/3 men input_data <- data.frame(tch = round(rnorm(n = 250, mean = 8, sd = 1.5), digits = 2), ch = round(rnorm(n = 250, mean = 12, sd = 2), digits = 2), ex = round(rnorm(n = 250, mean = 13, sd = 1), digits = 2), tex = round(rnorm(n = 250, mean = 15, sd = 1), digits = 2), gender = sample(x = c("male", "female"), size = 250, replace = TRUE, prob = c(2/3, 1/3))) ``` -------------------------------- ### Load pricesensitivitymeter package Source: https://max-alletsee.github.io/pricesensitivitymeter/articles/weighted-data.html Load the pricesensitivitymeter library to access psm_analysis_weighted() and other functions. ```r library(pricesensitivitymeter) ``` -------------------------------- ### Create alternative cheap and expensive variables with price steps Source: https://max-alletsee.github.io/pricesensitivitymeter/articles/interpolation-in-small-samples.html Creates alternative 'cheap' and 'expensive' variables by replacing values within a price range with the rounded mean of that range, introducing price steps. ```R pricestep_cheap <- which(cheap > 14.25 & cheap < 17.5) cheap_alt <- cheap cheap_alt[pricestep_cheap] <- round(mean(cheap_alt[pricestep_cheap]), digits = 2) pricestep_expensive <- which(expensive > 14 & expensive < 16) expensive_alt <- expensive expensive_alt[pricestep_expensive] <- round(mean(expensive_alt[pricestep_expensive]), digits = 2) ``` -------------------------------- ### Initialize input data for intransitive analysis Source: https://max-alletsee.github.io/pricesensitivitymeter/articles/weighted-data.html Creates a copy of the input data to prepare for manipulating values for intransitive price information. ```R input_data_3 <- input_data_2 ``` -------------------------------- ### Initialize PSM Plot Canvas Source: https://max-alletsee.github.io/pricesensitivitymeter/articles/visualizing-psm-results.html Sets up an empty plot canvas using the price range from the PSM output and adds gridlines. ```R plot(x = output_psm$data_vanwestendorp$price, y = output_psm$data_vanwestendorp$ecdf_toocheap, type = "n", xlab = "", ylab = "") grid() # adding gridlines ``` -------------------------------- ### Plot for Optimal Revenue with annotations Source: https://max-alletsee.github.io/pricesensitivitymeter/articles/visualizing-psm-results.html Creates a line plot of price versus revenue from a PSM analysis, adds gridlines, right-aligns a subtitle with sample size, and highlights the optimal price with a dotted vertical line and text label. ```R # b) Plot for Optimal Revenue # Main plot: Line, axis descriptions plot(x = output_psm$data_nms$price, y = output_psm$data_nms$revenue, type = "l", main = "Price Sensitivity Meter: Price for Optimal Revenue", xlab = "Price", ylab = "Revenue") grid() # adding gridlines # Right-align of subtitle adj.old <- par()$adj # saving old default value (= centered) par(adj = 1) # setting the new default to right-align title(sub = paste("Combined revenue for a sample of n =", output_psm$total_sample - output_psm$invalid_cases, "respondents\nData: Randomly generated")) # writing the actual subtitle par(adj = adj.old) # restore standard (so that next main title is again centered) # drawing additional line to highlight optimal price abline(v = output_psm$data_nms$price[which.max(output_psm$data_nms$revenue)], lty = "dotted") # annotating the optimal price text(x = output_psm$data_nms$price[which.max(output_psm$data_nms$revenue)], y = max(output_psm$data_nms$revenue), labels = paste("Optimal Price:", output_psm$price_optimal_revenue), pos = 4) ``` -------------------------------- ### Create a custom ggplot for Price Sensitivity Meter results Source: https://max-alletsee.github.io/pricesensitivitymeter/articles/visualizing-psm-results.html Builds a complete ggplot visualization from the output_psm object. It includes a shaded rectangle for the acceptable price range, four ECDF lines (too cheap, too expensive, not cheap, not expensive), and points for the Indifference Price Point (IDP) and Optimal Price Point (OPP). Requires the ggplot2 library. ```R library(ggplot2) # all plot elements without any labels psmplot <- ggplot(data = output_psm$data_vanwestendorp, aes(x = price)) + annotate(geom = "rect", # shaded background area for range of acceptable prices xmin = output_psm$pricerange_lower, xmax = output_psm$pricerange_upper, ymin = 0, ymax = Inf, fill="grey50", alpha = 0.3) + geom_line(aes(y = ecdf_toocheap, # line: too cheap colour = "too cheap", linetype = "too cheap"), linewidth = 1) + geom_line(aes(y = ecdf_tooexpensive, # line: too expensive colour = "too expensive", linetype = "too expensive"), linewidth = 1) + geom_line(aes(y = ecdf_not_cheap, # line: not cheap colour = "not cheap", linetype = "not cheap"), linewidth = 1) + geom_line(aes(y = ecdf_not_expensive, # line: not expensive colour = "not expensive", linetype = "not expensive"), linewidth = 1) + annotate(geom = "point", # Indifference Price Point (intersection of "cheap" and "expensive") x = output_psm$idp, y = approx(x = output_psm$data_vanwestendorp$price, y = output_psm$data_vanwestendorp$ecdf_not_cheap, xout = output_psm$idp)$y, size = 5, shape = 18, colour = "#009E73") + annotate(geom = "point", # Optimal Price Point (intersection of "too cheap" and "too expensive") x = output_psm$opp, y = approx(x = output_psm$data_vanwestendorp$price, y = output_psm$data_vanwestendorp$ecdf_toocheap, xout = output_psm$opp)$y, size = 3, shape = 17, colour = "#009E73") ``` -------------------------------- ### Run weighted PSM analysis and display summary Source: https://max-alletsee.github.io/pricesensitivitymeter/articles/weighted-data.html Run psm_analysis_weighted() with the survey design object, outputting the weighted PSM results and summary with accepted price range, indifference point, optimal point, and case count. ```r output_weighted_psm <- psm_analysis_weighted(toocheap = "tch", cheap = "ch", expensive = "ex", tooexpensive = "tex", design = input_design) summary(output_weighted_psm) #> Van Westendorp Price Sensitivity Meter Analysis #> #> Accepted Price Range: 11.9 - 15.62 #> Indifference Price Point: 13.68877 #> Optimal Price Point: 13.63 #> #> --- #> 228 cases with individual price preferences were analyzed (weighted data). #> Total data set consists of 250 cases. Analysis was limited to cases with transitive price preferences. #> (Removed: n = 22 / 9% of data) ``` -------------------------------- ### Create dataset with price acceptance and gender variable Source: https://max-alletsee.github.io/pricesensitivitymeter/articles/weighted-data.html Generates a data frame with 250 rows of random price acceptance values and a gender variable sampled with equal probability. The price acceptance variables are rounded to two decimals. ```R input_data_2 <- data.frame(tch = round(rnorm(n = 250, mean = 4, sd = 0.5), digits = 2), ch = round(rnorm(n = 250, mean = 8, sd = 0.5), digits = 2), ex = round(rnorm(n = 250, mean = 12, sd = 0.5), digits = 2), tex = round(rnorm(n = 250, mean = 16, sd = 0.5), digits = 2), gender = sample(x = c("male", "female"), size = 250, replace = TRUE, prob = c(0.5, 0.5))) ``` -------------------------------- ### showClass("psm") output Source: https://max-alletsee.github.io/pricesensitivitymeter/reference/psm-class.html Displays the class definition and slots of the psm S4 class. The output shows the data_input, validated, invalid_cases, total_sample, data_vanwestendorp, pricerange_lower, pricerange_upper, idp, opp, weighted, survey_design, NMS, data_nms, pi_scale, price_optimal_reach, and price_optimal_revenue slots. ```r showClass("psm") #> Class "psm" [package "pricesensitivitymeter"] #> #> Slots: #> #> Name: data_input validated invalid_cases #> Class: data.frame logical numeric #> #> Name: total_sample data_vanwestendorp pricerange_lower #> Class: numeric data.frame numeric #> #> Name: pricerange_upper idp opp #> Class: numeric numeric numeric #> #> Name: weighted survey_design NMS #> Class: logical survey.design2 logical #> #> Name: data_nms pi_scale price_optimal_reach #> Class: data.frame data.frame numeric #> #> Name: price_optimal_revenue #> Class: numeric ``` -------------------------------- ### Run psm_analysis with interpolation Source: https://max-alletsee.github.io/pricesensitivitymeter/articles/interpolation-in-small-samples.html Executes the price sensitivity meter analysis with interpolation enabled by setting the interpolate parameter to TRUE. ```R psm_interpolated <- psm_analysis(toocheap = toocheap, cheap = cheap_alt, expensive = expensive_alt, tooexpensive = tooexpensive, validate = TRUE, interpolate = TRUE) ``` -------------------------------- ### Weighted PSM analysis with Newton Miller Smith extension Source: https://max-alletsee.github.io/pricesensitivitymeter/articles/weighted-data.html Adds purchase intent variables (pi_ch, pi_ex) and runs psm_analysis_weighted() with pi_scale and pi_calibrated arguments. The output includes the Newton Miller Smith Extension with price with optimal reach (11.99) and optimal revenue (17.8). ```R # setting up dataset with purchase intent information input_data_3 <- input_data input_data_3$pi_ch <- sample(x = c(1:5), size = nrow(input_data_3), replace = TRUE, prob = c(0.1, 0.1, 0.2, 0.3, 0.3)) input_data_3$pi_ex <- sample(x = c(1:5), size = nrow(input_data_3), replace = TRUE, prob = c(0.3, 0.3, 0.2, 0.1, 0.1)) # re-creating the survey design object input_design_3 <- survey::svydesign(ids = ~ 1, probs = NULL, strata = input_data_3$gender, fpc = input_data_3$gender_pop, data = input_data_3) # running the weighted Price Sensitivity Meter analysis test_3 <- psm_analysis_weighted(toocheap = "tch", cheap = "ch", expensive = "ex", tooexpensive = "tex", design = input_design_3, pi_cheap = "pi_ch", pi_expensive = "pi_ex", pi_scale = 5:1, pi_calibrated = c(0.7, 0.5, 0.3, 0.1, 0)) summary(test_3) #> Van Westendorp Price Sensitivity Meter Analysis #> #> Accepted Price Range: 11.9 - 15.62 #> Indifference Price Point: 13.68877 #> Optimal Price Point: 13.63 #> #> Newton Miller Smith Extension #> Price with Optimal Reach: 11.99 #> Price with Optimal Revenue: 17.8 #> #> --- #> 228 cases with individual price preferences were analyzed (weighted data). #> Total data set consists of 250 cases. Analysis was limited to cases with transitive price preferences. #> (Removed: n = 22 / 9% of data) ``` -------------------------------- ### Create survey design object and run weighted vs unweighted PSM analysis Source: https://max-alletsee.github.io/pricesensitivitymeter/articles/weighted-data.html Creates a survey design object for post-stratification based on gender, then runs both weighted and unweighted Price Sensitivity Meter analyses. The summary outputs show the results differ slightly due to weighting after removing intransitive cases. ```R # creating the survey design object for post-stratification based on gender input_design_3 <- survey::svydesign(ids = ~ 1, # no clusters probs = NULL, # hence no cluster sampling probabilities, strata = input_data_3$gender, # stratified by gender fpc = input_data_3$gender_pop, # strata size in the population data = input_data_3) # data object used as input check_weighted_2 <- psm_analysis_weighted(toocheap = "tch", cheap = "ch", expensive = "ex", tooexpensive = "tex", design = input_design_3) check_unweighted_2 <- psm_analysis(toocheap = "tch", cheap = "ch", expensive = "ex", tooexpensive = "tex", data = input_data_3) # results should be different now summary(check_weighted_2) #> Van Westendorp Price Sensitivity Meter Analysis #> #> Accepted Price Range: 7.22 - 17.03 #> Indifference Price Point: 12.0725 #> Optimal Price Point: 8 #> #> --- #> 240 cases with individual price preferences were analyzed (weighted data). #> Total data set consists of 250 cases. Analysis was limited to cases with transitive price preferences. #> (Removed: n = 10 / 4% of data) summary(check_unweighted_2) #> Van Westendorp Price Sensitivity Meter Analysis #> #> Accepted Price Range: 7.22 - 17.03 #> Indifference Price Point: 12.0725 #> Optimal Price Point: 7.995 #> #> --- #> 240 cases with individual price preferences were analyzed (unweighted data). #> Total data set consists of 250 cases. Analysis was limited to cases with transitive price preferences. #> (Removed: n = 10 / 4% of data) ``` -------------------------------- ### Create dataset with price acceptance and biased gender variable Source: https://max-alletsee.github.io/pricesensitivitymeter/articles/weighted-data.html Generates a data frame with 250 rows of price acceptance values (tch, ch, ex, tex) drawn from normal distributions and a gender variable sampled with probabilities 2/3 male and 1/3 female. The price acceptance values for female respondents are then increased by 50%. ```R input_data <- data.frame(tch = round(rnorm(n = 250, mean = 8, sd = 0.5), digits = 2), ch = round(rnorm(n = 250, mean = 12, sd = 0.5), digits = 2), ex = round(rnorm(n = 250, mean = 13, sd = 0.5), digits = 2), tex = round(rnorm(n = 250, mean = 15, sd = 0.5), digits = 2), gender = sample(x = c("male", "female"), size = 250, replace = TRUE, prob = c(2/3, 1/3))) ``` ```R # for women: increasing the price acceptance by +50% input_data$tch[input_data$gender == "female"] <- input_data$tch[input_data$gender == "female"] * 1.5 input_data$ch[input_data$gender == "female"] <- input_data$ch[input_data$gender == "female"] * 1.5 input_data$ex[input_data$gender == "female"] <- input_data$ex[input_data$gender == "female"] * 1.5 input_data$tex[input_data$gender == "female"] <- input_data$tex[input_data$gender == "female"] * 1.5 ``` -------------------------------- ### Weighted PSM analysis with missing 'too cheap' data Source: https://max-alletsee.github.io/pricesensitivitymeter/articles/weighted-data.html Sets the 'too cheap' variable to NA and runs psm_analysis_weighted(). The lower bound of the price range and the optimal price point cannot be estimated, while the upper bound and indifference price point are estimated. The output shows an accepted price range of NA - 15.62 and an indifference price point of 13.68877. ```R # setting up data with NAs in "too cheap" variable input_data_2 <- input_data input_data_2$tch <- NA # create new sample design input_design_2 <- survey::svydesign(ids = ~ 1, # no clusters probs = NULL, # hence no cluster sampling probabilities, strata = input_data_2$gender, # stratified by gender fpc = input_data_2$gender_pop, # strata size in the population data = input_data_2) # data object used as input test_2 <- psm_analysis_weighted(toocheap = "tch", cheap = "ch", expensive = "ex", tooexpensive = "tex", design = input_design_2) summary(test_2) #> Van Westendorp Price Sensitivity Meter Analysis #> #> Accepted Price Range: NA - 15.62 #> Indifference Price Point: 13.68877 #> Optimal Price Point: NA #> #> --- #> 228 cases with individual price preferences were analyzed (weighted data). #> Total data set consists of 250 cases. Analysis was limited to cases with transitive price preferences. #> (Removed: n = 22 / 9% of data) ``` -------------------------------- ### Create biased sample and survey design object Source: https://max-alletsee.github.io/pricesensitivitymeter/articles/weighted-data.html Sets a random seed, loads the survey package, and creates a survey design object for a simulated sample with gender bias. Assumes simple random sampling and a population of 10,000 people. ```r set.seed(1976) library(survey) #> Loading required package: grid #> Loading required package: Matrix #> Loading required package: survival #> #> Attaching package: 'survey' #> The following object is masked from 'package:graphics': #> #> dotchart ``` -------------------------------- ### Create survey design object with gender population weights Source: https://max-alletsee.github.io/pricesensitivitymeter/articles/weighted-data.html Adds a 'gender_pop' column to the dataset, setting population counts for each gender by multiplying the sample counts by 40, to represent a hypothetical population of 10,000. This prepares the data for survey design. ```R input_data_2$gender_pop <- NA input_data_2$gender_pop[input_data_2$gender == "female"] <- sum(input_data_2$gender == "female") * 40 input_data_2$gender_pop[input_data_2$gender == "male"] <- sum(input_data_2$gender == "male") * 40 ``` -------------------------------- ### Create survey design object for post-stratification Source: https://max-alletsee.github.io/pricesensitivitymeter/articles/weighted-data.html Initializes a survey design object using the survey package, specifying strata and population sizes for weighting. ```R input_design_2 <- survey::svydesign(ids = ~ 1, # no clusters probs = NULL, # hence no cluster sampling probabilities, strata = input_data_2$gender, # stratified by gender fpc = input_data_2$gender_pop, # strata size in the population data = input_data_2) # data object used as input # quick check: there is only one weight for all our strata # if we would have different weights per gender, we would see two unique values here unique(weights(input_design_2, type = "analysis")) #> [1] 40 ``` -------------------------------- ### Plotting IDP and OPP with base R Source: https://max-alletsee.github.io/pricesensitivitymeter/articles/visualizing-psm-results.html Adds the Indifference Price Point (IDP) and Optimal Price Point (OPP) to an existing plot, along with a legend. ```R # adding the Indifference Price Point (intersection of "cheap" and "expensive") points(x = output_psm$idp, y = output_psm$data_vanwestendorp$ecdf_not_cheap[which.min(abs(output_psm$data_vanwestendorp$price - output_psm$idp))], cex = 2, pch = 18, col = "#009E73") text(x = output_psm$idp, y = output_psm$data_vanwestendorp$ecdf_not_cheap[which.min(abs(output_psm$data_vanwestendorp$price - output_psm$idp))], labels = paste("IDP:", output_psm$idp), pos = 4) # adding the Optimal Price Point (intersection of "too cheap" and "too expensive") points(x = output_psm$opp, y = output_psm$data_vanwestendorp$ecdf_toocheap[which.min(abs(output_psm$data_vanwestendorp$price - output_psm$opp))], cex = 2, pch = 17, col = "#009E73") text(x = output_psm$opp, y = output_psm$data_vanwestendorp$ecdf_toocheap[which.min(abs(output_psm$data_vanwestendorp$price - output_psm$opp))], labels = paste("OPP:", output_psm$opp), pos = 4) # adding legend legend("bottomleft", title = "Legend", c("too cheap", "not cheap", "not expensive", "too expensive"), lty = c("dotted", "solid", "solid", "dotted"), col = c("#009E73", "#009E73", "#D55E00", "#D55E00"), cex = 0.66) ``` -------------------------------- ### Compare weighted and unweighted analysis Source: https://max-alletsee.github.io/pricesensitivitymeter/articles/weighted-data.html Executes both weighted and unweighted analysis on the same dataset to verify consistency in results. ```R # running both weighted and unweighted analysis on the same data check_weighted_1 <- psm_analysis_weighted(toocheap = "tch", cheap = "ch", expensive = "ex", tooexpensive = "tex", design = input_design_2) check_unweighted_1 <- psm_analysis(toocheap = "tch", cheap = "ch", expensive = "ex", tooexpensive = "tex", data = input_data_2) # results should be identical summary(check_weighted_1) #> Van Westendorp Price Sensitivity Meter Analysis #> #> Accepted Price Range: 7.17 - 16.82 #> Indifference Price Point: 12.055 #> Optimal Price Point: 8 #> #> --- #> 250 cases with individual price preferences were analyzed (weighted data). summary(check_unweighted_1) #> Van Westendorp Price Sensitivity Meter Analysis #> #> Accepted Price Range: 7.17 - 16.82 #> Indifference Price Point: 12.055 #> Optimal Price Point: 7.995 #> #> --- #> 250 cases with individual price preferences were analyzed (unweighted data). ``` -------------------------------- ### Create survey design object for weighted PSM Source: https://max-alletsee.github.io/pricesensitivitymeter/reference/psm_analysis_weighted.html Creates a survey design object using the survey package, assuming gender is balanced equally in a population of 10000 (5000 per gender). The design uses no clusters, no cluster sampling probabilities, stratification by gender, and strata size in the population. ```R input_data$gender_pop <- 5000 input_design <- survey::svydesign(ids = ~ 1, # no clusters probs = NULL, # hence no cluster sampling probabilities, strata = input_data$gender, # stratified by gender fpc = input_data$gender_pop, # strata size in the population data = input_data) # data object used as input: no need to specify single variables ``` -------------------------------- ### Plot Optimal Revenue with ggplot2 Source: https://max-alletsee.github.io/pricesensitivitymeter/articles/visualizing-psm-results.html Uses ggplot2 to visualize the revenue curve and highlight the optimal price point with a dotted vertical line and text label. ```R ggplot(data = output_psm$data_nms, aes(x = price)) + geom_line(aes(y = revenue)) + # revenue curve geom_vline(xintercept = output_psm$price_optimal_revenue, linetype = "dotted") + # highlighting the optimal price geom_text(data = subset(output_psm$data_nms, revenue == max(revenue)), aes(x = price + 0.5, y = revenue), label = paste("Optimal Price:", output_psm$price_optimal_revenue), hjust = 0) + # labelling the optimal price labs(x = "Price", y = "Revenue", title = "Price Sensitivity Meter: Price for Optimal Revenue", caption = paste("Combined revenue for a sample of n =", output_psm$total_sample - output_psm$invalid_cases, "respondents\nData: Randomly generated")) + theme_minimal() ``` -------------------------------- ### Create random price variables for analysis Source: https://max-alletsee.github.io/pricesensitivitymeter/articles/interpolation-in-small-samples.html This snippet generates four sorted random variables representing different price categories (toocheap, tooexpensive, cheap, expensive) using a fixed seed for reproducibility. ```R set.seed(1976) # create random variables toocheap <- sort(round(rnorm(n = 50, mean = 8, sd = 1.5), digits = 2)) tooexpensive <- sort(round(rnorm(n = 50, mean = 20, sd = 1), digits = 2)) cheap <- sort(round(rnorm(n = 50, mean = 15, sd = 3), digits = 2)) expensive <- sort(round(rnorm(n = 50, mean = 16, sd = 3), digits = 2)) ``` -------------------------------- ### Plot interpolated price steps Source: https://max-alletsee.github.io/pricesensitivitymeter/articles/interpolation-in-small-samples.html Creates a ggplot visualization of the interpolated Van Westendorp data, including the Indifference Price Point (IDP). ```R psmplot_interpolated <- ggplot(data = psm_interpolated$data_vanwestendorp, aes(x = price)) + geom_line(aes(y = ecdf_not_cheap, # line: not cheap colour = "not cheap"), linewidth = 1) + geom_line(aes(y = ecdf_not_expensive, # line: not expensive colour = "not expensive"), linewidth = 1) + geom_rug() + annotate(geom = "point", # Indifference Price Point (intersection of "cheap" and "expensive") x = psm_interpolated$idp, y = psm_interpolated$data_vanwestendorp$ecdf_not_expensive[psm_interpolated$data_vanwestendorp$price == psm_interpolated$idp], size = 5, shape = 18, colour = "#D55E00") psmplot_interpolated + labs(x = "Price", y = "Share of Respondents (0-1)", title = "Price Sensitivity Meter Plot (with interpolation)") + scale_colour_manual(name = "Legend", values = c("not cheap" = "#009E73", "not expensive" = "#D55E00")) + annotate(geom = "text", # Label of Indifference Price Point x = psm_interpolated$idp + 1, y = psm_interpolated$data_vanwestendorp$ecdf_not_expensive[psm_interpolated$data_vanwestendorp$price == psm_interpolated$idp], label = paste("IDP: ", psm_interpolated$idp)) + coord_cartesian(xlim = c(10, 20)) + theme_minimal() ``` -------------------------------- ### psm_analysis() Source: https://max-alletsee.github.io/pricesensitivitymeter/reference/psm_analysis.html Performs a van Westendorp Price Sensitivity Meter (PSM) analysis on consumer price preferences. It takes respondent price preferences as input and estimates acceptable price ranges and price points. ```APIDOC ## psm_analysis() ### Description Performs a van Westendorp Price Sensitivity Meter (PSM) analysis on consumer price preferences. It takes respondent price preferences as input and estimates acceptable price ranges and price points. ### Method Function call ### Endpoint psm_analysis(toocheap, cheap, expensive, tooexpensive, data = NA, validate = TRUE, interpolate = FALSE, interpolation_steps = get_psm_constant("DEFAULT_INTERPOLATION_STEPS"), intersection_method = "min", acceptable_range = "original", pi_cheap = NA, pi_expensive = NA, pi_scale = get_psm_constant("NMS_DEFAULTS.PI_SCALE"), pi_calibrated = get_psm_constant("NMS_DEFAULTS.PI_CALIBRATED"), pi_calibrated_toocheap = get_psm_constant("NMS_DEFAULTS.PI_CALIBRATED_TOOCHEAP"), pi_calibrated_tooexpensive = get_psm_constant("NMS_DEFAULTS.PI_CALIBRATED_TOOEXPENSIVE")) ### Parameters #### Arguments - **toocheap, cheap, expensive, tooexpensive** (numeric vector or variable name) - Required - If a data.frame/matrix/tibble is provided in the `data` argument: names of the variables in the data.frame/matrix that contain the survey data on the respondents' "too cheap", "cheap", "expensive" and "too expensive" price preferences. If no data.frame/matrix/tibble is provided: numeric vectors that directly include this information. If the `toocheap` price was not assessed, a variable/vector of NAs can be used instead. This variable/vector needs to have the same length as the other survey information. If `toocheap` is NA for all cases, it is possible to calculate the Point of Marginal Expensiveness and the Indifference Price Point, but it is impossible to calculate the Point of Marginal Cheapness and the Optimal Price Point. - **data** (data.frame, matrix or tibble) - Optional - Contains the function's input data. If not provided, data can be provided directly as vectors in the "too cheap", "cheap", "expensive" and "too expensive" arguments. - **validate** (logical) - Optional - Should only respondents with consistent price preferences (too cheap < cheap < expensive < too expensive) be considered in the analysis? Default is TRUE. - **interpolate** (logical) - Optional - Should interpolation of the price curves be applied between the actual prices given by the respondents? If interpolation is enabled, the output appears less bumpy in regions with sparse price information. If the sample size is sufficiently large, interpolation should not be necessary. Default is FALSE. - **interpolation_steps** (numeric) - Optional - If `interpolate` is `TRUE`: the size of the interpolation steps. Set by default to 0.01, which should be appropriate for most goods in a price range of 0-50 USD/Euro. - **intersection_method** (character) - Optional - "min" (default), "max", "mean" or "median". Defines the method how to determine the price points (range, indifference price, optimal price) if there are multiple possible intersections of the price curves. "min" uses the lowest possible prices, "max" uses the highest possible prices, "mean" calculates the mean among all intersections and "median" uses the median of all possible intersections. - **acceptable_range** (character) - Optional - "original" (default) or "narrower". Defines which intersection is used to calculate the point of marginal cheapness and point of marginal expensiveness, which together form the range of acceptable prices. "original" uses the definition provided in van Westendorp's paper: The lower end of the price range (point of marginal cheapness) is defined as the intersection of "too cheap" and the inverse of the "cheap" curve. The upper end of the price range (point of marginal expensiveness) is defined as the intersection of "too expensive" and the inverse of the "expensive" curve. Alternatively, it is possible to use a "narrower" definition which is applied by some market research companies. Here, the lower end of the price range is defined as the intersection of the "expensive" and the "too cheap" curves and the upper end of the price range is defined as the intersection of the "too expensive" and the "cheap" curves. This leads to a narrower range of acceptable prices. Note that it is possible that the optimal price according to the Newton/Miller/Smith extension is higher than the upper end of the acceptable price range in the "narrower" definition. - **pi_cheap, pi_expensive** (variable name) - Optional - Only required for the Newton Miller Smith extension. If `data` argument is provided: names of the variables in the data.frame/matrix/tibble that contain the survey data on the respondents' purchase intent at their individual cheap/expensive price. - **pi_scale** (numeric) - Optional - Scale for purchase intent. Default is get_psm_constant("NMS_DEFAULTS.PI_SCALE"). - **pi_calibrated** (logical) - Optional - Whether purchase intent is calibrated. Default is get_psm_constant("NMS_DEFAULTS.PI_CALIBRATED"). - **pi_calibrated_toocheap** (numeric) - Optional - Calibration value for too cheap. Default is get_psm_constant("NMS_DEFAULTS.PI_CALIBRATED_TOOCHEAP"). - **pi_calibrated_tooexpensive** (numeric) - Optional - Calibration value for too expensive. Default is get_psm_constant("NMS_DEFAULTS.PI_CALIBRATED_TOOEXPENSIVE"). ### Request Example ```r psm_analysis( toocheap = "toocheap_var", cheap = "cheap_var", expensive = "expensive_var", tooexpensive = "tooexpensive_var", data = my_data, validate = TRUE, interpolate = FALSE, interpolation_steps = 0.01, intersection_method = "min", acceptable_range = "original", pi_cheap = NA, pi_expensive = NA, pi_scale = 5, pi_calibrated = TRUE, pi_calibrated_toocheap = 1, pi_calibrated_tooexpensive = 5 ) ``` ### Response #### Success Response Returns an object containing the results of the PSM analysis, including estimated acceptable price ranges and price points. #### Response Example ```r # Example output structure (not actual values) list( price_range = c(lower = 10, upper = 20), indifference_price = 15, optimal_price = 18 ) ``` ```