Category: Sales

5 Steps to Transform your Sales

Today’s business environment is complex, uncertain and fast moving and efficiently targeting growth remains a challenge. Sales people certainly gain valuable instincts and skills from experience but the right data insights at the right time can make all the difference. Transform sales by organizing and segmenting your outlets so the best opportunities are prioritized you can save time whilst providing a higher success rate.

That’s all very well and good but where do you even begin the process of sales transformation?

How To Transform Sales:

1. Data

Getting the right data to transform sales 

It always starts with the data. Attaining a good Market Universe data set means better targeting within your Market Universe. Here you have a few options;

  • Buy a data set.
  • These are usually estimated with low coverage and not very rich in detail.

  • Conduct a census.
  • Very expensive and not easily repeatable.

  • Use new dynamic data methods, (web and big data)
  • Sounds complex and reputation for high cost.

    Finding the right data and using it efficiently is not a one-off problem and so we at Sales Align would advocate working with meaningful technologies, such as those within the Big Data umbrella, in a purposeful way with repeatable results. There are trials to navigate when it comes to these new data methods from data harvesting, size, enrichment and other preparation issues. So, whether that’s an in-house team or another provider you do need to work with people who have expertise.

    Whichever of the 3 options you choose you should understand that getting the right data is the first step to transform sales.

    2. Analysis for Prioritisation

    Analysis for Sales Prioritisation & Sales transform 

    So now that you have the right data how do you assess your coverage and plan your priorities? What are your key data drivers that help you decide your focus? For example, setting keywords that easily describe the outlet type can help you choose which to target and when. If you have social media data, you can use this to create popularity indicators. Area density could also be a key metric in decision-making.

    With the first two data options (Data Set or Census) you are a bit more limited as the data is static. This means you can’t see how the data changes in different time periods, such as when an outlet grows in popularity on social media. When you know the data is up-to-date and more accurate you can be more confident acting on the analysis.

    You can do many things with your analysis, whether that’s keeping it offline in spreadsheets (and silos 🙂 ) or in a tool that visualises the data and results. You should ideally find a good method for all stakeholders to interact with the data rather than just leaving it in the hands of the analysts.

    3. Optimise Your Focus with Segmentation

    Optimise Your Focus with Segmentation & Transform Sales 

    Custom targeting and segmentation exercises using your data can take your sales prioritisation one step further. You’re probably already engaging in segmentation to some degree but by using your data to define certain “types”, “segments” and “social media ranks” you can create target outlet lists that fit certain profiles. Additionally, if you want to get sophisticated, start grouping your previously set keywords into themes e.g “cheap”, “high-end” “garden” and create specific campaigns that align with marketing needs.

    To truly transform sales, even with all this great insight, it’s important to ensure you have a good process set up for sharing your lists and other information with your team so you can set them into action! Also think about how you monitor the progress of working through your targeted lists and segments.

    4. Keeping Up-to-Date – Look Out For Outlet Churn

    Keeping Up-to-Date - Transform Sales & Look Out For Outlet Churn 

    Becoming more aware of outlet churn can quickly make a positive impact on your sales performance. With accurate location information from your data and frequent updates that inform you of closed and new outlets you can manage your time more efficiently. Keep an eye on the evolution of churn in different segments, for example when a particular type of outlet grows in popularity on social media. Considering new outlets typically churn 4-8% per annum this could save you a lot of time & costs.

    5. Actionable Strategy

    Transform Sales with Actionable Strategy 

    The way you combine your experience with certain data-points is crucial in defining your strategic plan. Whether that’s planning how to grow by segment, targeting occasions or optimising call lists, it’s still the people who put this into action. Review the way you connect your strategy, your data and your people.

    With better use of good quality data in your strategy and in the way your team interacts with it you can transform sales. Saving time focusing on outlets that matter means saving time and costs. Not to mention higher potential sales!

    This article is only an outline of the beginning steps towards transforming your sales with modern data methods to optimize your sales effectiveness. We have often taken these steps together with our clients and that has given us experience but also a curated method. We are happy to share our successes and if you wish to see how our tool Sales Align comfortably enables all the above, please click here.

    Posted on February 12, 2018 by Danielle Mosimann

    Using Simulation to Determine Sample Sizes for a Study of Store Sales

    Suppose a client wants to estimate the total sales value of widgets in a large number of stores. To do this, they will survey a sample of that population of stores. You need to provide the client with advice on choosing a suitable sample size.

    Unfortunately, the client has little information to help you. They know that there are 1,000 stores that sell widgets. But they have no idea what the average store sales might be. All they know from previous studies is that the sales tend to be very right skew: Most stores sell very few widgets and very few stores sell a lot of widgets.

    This is a fairly typical situation. We deal with a lot of sales data at AlignAlytics and typically find sales volumes and values (allowing for sale price to vary between sellers) to be very right-skew. Sales volumes are often well described by a Poisson distribution; A Pareto or a chi-square distribution often works well for sales values.

    So, let’s suppose the client tells us that they expect the sales value per store to be distributed something like this:

    Histogram of Sales

    That looks very much like a chi-square distribution with two degrees of freedom. So we run the following R code:

     
    # Create the distribution function.
    r_dist_fn <- function(n) rchisq(n, 2)
    
    # Get the dataframe of confidence intervals.
    df_ci <- estimate_ci_from_sample(r_dist_fn, pop_size=c(1000), min_sample=10, max_sample=50, n_simulations=100000, confidence=c(50, 80, 90, 95))
    

    That gives us a dataframe with rows that look like this:

    Confidence Intervals Dataframe

    This tells us, for instance, that if we use a sample of 20 stores, there is a 90% chance that the total sales in the population of stores is between approximately 72% and 150% of the estimate based on the sample.

    Here's a bit of code that graphs that dataframe of confidence intervals:

     
    par(ask=TRUE)
    for (pop in sort(unique(df_ci$population_size))){
       
       # Subset df_ci by population size and get the confidence intervals calculated for that subset.
       df_ci_sub <- df_ci[df_ci$population_size==pop,]
       confidence_interval <- sort(unique(df_ci_sub$confidence))
    
       # Create an empty plot of the required size.
       plot(x=c(min(df_ci_sub$sample_size), max(df_ci_sub$sample_size)), 
            y=c(min(df_ci_sub$pop_total_div_estimated_total_ci_lower), max(df_ci_sub$pop_total_div_estimated_total_ci_upper)), 
            main=paste0("Confidence Intervals (", paste(confidence_interval, collapse="%, "), "%) for Population Total / Sample Total (Population: ", pop, ")"),
            type='n', xlab="Sample Size", ylab="Population Total / Sample Total")
       
       # Loop across the confidence intervals.
       for (ci in confidence_interval){
    
          # Graph a confidence interval.
          df_ci_sub_sub <- df_ci_sub[df_ci_sub$confidence==ci,]   
          polygon(c(df_ci_sub_sub$sample_size,                            rev(df_ci_sub_sub$sample_size)), 
                  c(df_ci_sub_sub$pop_total_div_estimated_total_ci_lower, rev(df_ci_sub_sub$pop_total_div_estimated_total_ci_upper)), 
                  col=rgb(0, 0, 1, 0.2), border=TRUE)
          
       }
    
       # Draw a horizontal line at y=1.
       lines(y=c(1, 1), x=c(min(df_ci_sub_sub$sample_size), max(df_ci_sub_sub$sample_size)))   
    }
    par(ask=FALSE)
    

    And here's the output:

    Confidence bands for Population Total / Sample Total

    In the above graph, the widest confidence interval is the 95% interval, the thinnest (closest to the horizontal line at y=1) is the 50% confidence interval.

    So, as before, there is a 90% chance that the total sales in the population of stores is between approximately 72% and 150% of the estimate based on the sample:

    Confidence bands for Population Total / Sample Total with lines showing 90% interval for a 20 sample size

    Using the above graph and the dataframe of confidence intervals, the client should be able to choose a sensible sample size. This will involve balancing the cost of increasing the sample size against the accuracy improvement achieved by doing so.

    Finally, here's the estimate_ci_from_sample function used above:

     
    estimate_ci_from_sample <- function(r_dist_fn, pop_size, min_sample, max_sample, n_simulations=1000, confidence=c(50, 80, 90, 95)){
       # Returns a dataframe of confidence intervals for the sum of a population of real numbers (values given by r_dist_fn) divided by
       # the sum of a sample from that population.
       #
       # r_dist_fn:     A function taking one parameter, n, and returning n random samples from a distribution.
       # pop_size:      A vector of population sizes, e.g. c(100, 1000, 2000).
       # min_sample:    If min_sample is in (0, 1) the minimum sample size is a fraction of the population size. If min_sample is a
       #                positive integer, the minimum sample size is a fixed number (= min_sample).
       # max_sample:    If max_sample is in (0, 1) the maximum sample size is a fraction of the population size. If max_sample is a
       #                positive integer, the maximum sample size is a fixed number (= max_sample).
       # confidence:    A vector of the required confidence intervals, e.g. c(50, 80, 90, 95).
       # n_simulations: The number of simulations to run per population size + sample size combination. The higher this is, the more
       #                accurate the results but the slower the calculation. 
       
       # Useful functions.
       is_int <- function(x) x %% 1 == 0
       sample_int <- function(spl, pop_size) ifelse(is_int(spl), min(spl, pop_size), round(spl * pop_size))
       
       # Check the min_sample and max_sample parameters.
       if (min_sample <= 0 || (min_sample > 1 && !is_int(min_sample))) stop("min_sample must be in (0, 1) or be an integer in [1, inf).")
       if (max_sample <= 0 || (max_sample > 1 && !is_int(max_sample))) stop("max_sample must be in (0, 1) or be an integer in [1, inf).")
       if (is_int(min_sample) == is_int(max_sample) && max_sample < min_sample) stop("max_sample should be greater than or equal to min_sample.")
       
       # Create the dataframe to hold the results.
       df_ci <- data.frame()
    
       for (population_size in pop_size){
    
          # Determine the sample size range.
          sample_int_min <- sample_int(min_sample, population_size)
          sample_int_max <- sample_int(max_sample, population_size)
          
          # Yes, it can happen that sample_int_min > sample_int_max, despite the parameter checks, above.
          if (sample_int_min <= sample_int_max){
          
             for (sample_size in seq(sample_int_min, sample_int_max)){
                
                cat(paste0("\nCalculating ", n_simulations, " ", sample_size, "-size samples for population size ", population_size, "."))
                
                # Calculate the pop_total_div_estimated_total vector.
                pop_total_div_estimated_total <- c(NA, n_simulations)
                for (i_sim in 1:n_simulations){
                   population <- r_dist_fn(population_size)
                   sample_from_pop <- sample(population, sample_size)
                   pop_total_div_estimated_total[i_sim] <- sum(population) / (population_size * mean(sample_from_pop))
                }
                
                # Loop across the required confidence levels.
                for (conf in confidence){
                   # Calculate the confidence interval.
                   alpha <- (100 - conf) / 100
                   ci <- quantile(pop_total_div_estimated_total, probs=c(alpha / 2, 1 - alpha / 2))
                   
                   # Add a row to the dataframe.
                   df_ci_row <- data.frame(population_size                        = population_size, 
                                           sample_size                            = sample_size, 
                                           confidence                             = conf, 
                                           pop_total_div_estimated_total_ci_lower = ci[1], 
                                           pop_total_div_estimated_total_ci_upper = ci[2])
                   df_ci <- rbind(df_ci, df_ci_row) 
                }
                
             } # Ends sample_size for loop.
    
          } # Ends if.
    
       } # Ends population_size for loop.
    
       return(df_ci)
    }
    
    Posted on May 26, 2016 by Danielle Mosimann

    The Changing Face of Vendor Analytics

    Our most recent vendor project was an interesting change in direction compared to several vendor related projects we have previously worked on. We were asked to build out a vendor reporting capability that went beyond simple spend analytics and also brought in data from online sources such as Twitter, Google, Bloomberg, Reuters and Facebook.

    This project brought forward interesting trends not just in the area of vendor analytics but also in how datasets that underpin traditional reporting areas such as sales and budgeting are likely to expand in scope to include more and more data from online sources.

    Some companies are constantly meeting vendors and they need to make sure that they are asking the right questions and signing off the correct deals in these meetings. For this they need their staff to understand more than just the historical spending with the vendor. They need to know how that company is represented in the news, what key events the vendor has been involved in, such as mergers or financial results, and what people are saying about them.

    Historically BI solutions have focused on summarizing and visualizing the internal data side of the business – sales, spending, CRM… Users would then supplement this with their own knowledge and research of customers, competitors and suppliers to build-up an understanding of their environment. Recently however, our aim was to improve how users gather information from research. In order to achieve this, a BI solution needs to capture a wide variety of data sources which are then analysed, aggregated and presented back to the user in an easily understood way. Then, by automatically combining this with spend data you can also allow users to better understand the relationships between datasets.

    New role of BI Solutions

    In order to pull all this together, there are 4 key areas that need to be built out:

      • Text mining – you need to find a way of summarizing the large amounts of unstructured content that are brought in from online data sources – after reviewing several options we went with AlchemyAPI.
      • Data mashing – a more traditional database layer is needed to combine summary results from the unstructured data with internal vendor spend data – for this we stuck with SQL Server.
      • Reporting layer – To deliver the solution we used Tableau to create a series of reports that allowed users to interact with the combined data.

     

    Our final architecture looked something like this:

    Data flow architecture

     

    This project has led to several useful findings:

      • Overall the area of vendor analytics is enhanced by blending the spend data with online data sources. Events such as a vendor being acquired by another company, a successful project collaboration or a sales event need to be visible by the output from a tool.
      • The ability of AlchemyAPI to mine insights from text content is critical. This includes sentiment analysis but also tackles entity extraction – the process of relating people, places, companies and events to articles.
      • With AlchemyAPI you don’t have to store the content of every article (which is also why we chose it as the best tool for text analytics). You can simply send AlchemyAPI the URL to the relevant article and they analyse the content – other solutions require you to capture the full content or an article and send it to their applications.
      • ElasticSearch delivers what is needed from a NoSQL database with its flexibility to store and analyse large scale unstructured data from multiple sources. Its ability to allow multiple processes to collate and analyze data, simultaneously in real time, gives it significant advantages over other data storage solutions.
      • ElasticSearch delivers what is needed from a NoSQL database with its flexibility to store and analyse large scale unstructured data from multiple sources. Its ability to allow multiple processes to collate and analyze data, simultaneously in real time, gives it significant advantages over other data storage solutions.
      • Having built several solutions in Tableau we are aware of its traditional strengths. However, for this kind of project it is the ability to store web links in a dashboard which users can then access that is particularly useful. So if a spike in negative sentiment occurs for a supplier, a user can quickly navigate from a trend chart in Tableau to a summary of the articles content, again stored in Tableau, to ultimately to the most useful articles online.

     

    In conclusion, we found that the area of vendor analytics can be enhanced by combining traditional spend data with online content. The process of combining unstructured online data with spend and sales data is likely to become the norm in future BI developments as companies seek to fill in the gaps that internal data cannot answer on their own.

     

    Author: Angus Urquhart

    Posted on March 1, 2016 by Danielle Mosimann

    Seasonal Decomposition of Time Series by Loess—An Experiment

    Let’s run a simple experiment to see how well the stl() function of the R statistical programming language decomposes time-series data.

    An Example

    First, we plot some sales data:

     
    sales<-c(39,  73,  41,  76,  75,  47,   4,  53,  40,  47,  31,  33,
             58,  85,  61,  98,  90,  59,  34,  74,  78,  74,  56,  55,
             91, 125,  96, 135, 131, 103,  86, 116, 117, 128, 113, 123)
    time.series <- ts(data=sales, frequency = 12, start=c(2000, 1), end=c(2002, 12))
    plot(time.series, xlab="Time", ylab="Sales (USD)", main="Widget Sales Over Time")
    

    Observe the annual seasonality in the data:

    A time series of widget sales

    We apply R's stl() function ("seasonal and trend decomposition using Loess") to the sales data:

     
    decomposed  <- stl(time.series, s.window="periodic")
    plot(decomposed)
    

    This decomposes the sales data as the sum of a seasonal, a trend and a noise/remainder time-series:

    A time series of widget sales decomposed into seasonal, trend and noise/remainder components

    We may easily extract the component time series:

     
    decomposed <- stl(time.series, s.window="periodic")
    seasonal   <- decomposed$time.series[,1]
    trend	   <- decomposed$time.series[,2]
    remainder  <- decomposed$time.series[,3]
    

    This allows us to plot the seasonally-adjusted sales:

     
    plot(trend+remainder,
    main="Widget Sales over Time, Seasonally Adjusted",
    ylab="Sales (USD)")
    

    A time series of seasonally-adjusted widget sales

    An Experiment

    How well does stl() extract trend and seasonality from data? We run three simple graphical investigations.

    Case 1: Strong seasonality and low, normally-distributed homoskedastic noise

    An experiment in decomposition by Loess of a time series showing strong seasonality and low, normally-distributed homoskedastic noise

    The left side of each of the above images shows, from top to bottom:

    1. Generated sales data.
    2. The trend component from which the data was generated.
    3. The seasonal component from which the data was generated.
    4. The noise/remainder component from which the data was generated.

    The right side shows:

    1. Generated sales data.
    2. The trend component identified by stl().
    3. The seasonal component identified by stl().
    4. The noise/remainder component identified by stl().

    Note the close match between the two trend components and between the two seasonal components. This indicates that stl() works well in this instance.

    Case 2: Weak seasonality and high, normally-distributed homoskedastic noise

    An experiment in decomposition by Loess of a time series showing weak seasonality and high, normally-distributed homoskedastic noise

    Again, stl() appears to work quite well.

    Case 3: Weak seasonality and high, normally-distributed heteroskedastic noise

    An experiment in decomposition by Loess of a time series showing weak seasonality and high, normally-distributed heteroskedastic noise

    And stl() still seems to work fairly well. This is heartening, as it's common for the variance in a time series to increase as its mean rises—as is the case here.

    How stl() Works

    When calling stl() with s.window="periodic", the seasonal component for January is simply the mean of all January values. Similarly, the seasonal component for February is simply the mean of all February
    values, etc. Otherwise, the seasonal component is calculated using loess smoothing (discussed below).

    Having calculated the seasonal component, the seasonally-adjusted data (the original data minus the seasonal component) is loess-smoothed to determine the trend.

    The remainder/noise is then the original data minus the seasonal and trend components.

    The stl() function is quite flexible:

    • The seasonality does not have to run across a year. Any period may be used for this.
    • The decomposition process can accommodate seasonality that changes over time.
    • A robust decomposition process is available that is less affected by outliers than is the default.

    An Introduction to Loess Smoothing

    Loess ("locally-weighted scatterplot smoothing") uses local regression to remove "jaggedness" from data.

    1. A window of a specified width is placed over the data. The wider the window, the smoother the resulting loess curve.
    2. A regression line (or curve) is fitted to the observations that fall within the window, the points closest to the centre of the window being weighted to have the greatest effect on the calculation of the regression line.
    3. The weighting is reduced on those points within the window that are furthest from the regression line. The regression is re-run and weights are again re-calculated. This process is repeated several times.
    4. We thereby obtain a point on the loess curve. This is the point on the regression line at the centre of the window.
    5. The loess curve is calculated by moving the window across the data. Each point on the resulting loess curve is the intersection of a regression line and a vertical line at the centre of such a window.

    To calculate a loess curve using R:

     
    plot(cars$speed, cars$dist, main="Car Speed and Stopping Distance", xlab="Speed (mph)", ylab="Stopping Distance (ft)")
    lines(lowess(cars$speed, cars$dist), col="red")
    

    A scatterplot example of Loess fitting

    Generating Test Data

    Here, for completeness, is the code I used to generate the graphs I used in my tests:

     
    # Parameters
    start.ym <- c(2000, 1)
    end.ym   <- c(2012,12)
    n.years  <- 13
    
    # Set the seed for the randomisation
    set.seed(5)
    
    # Create the 2nd derivative of the sales trend
    ddtrend.sales <- qnorm(runif(12*n.years, 0.1, 0.90), mean=0, sd=0.4)
    
    # Create the 1st derivative of the sales trend from the 2nd derivative
    dtrend.sales    <- rep(NA, 12*n.years)
    dtrend.sales[1] <- 0
    for (i in 2:(12*n.years)) dtrend.sales[i] <- dtrend.sales[i-1] + ddtrend.sales[i]
    
    # Create the sales trend from the 1st derivative
    trend.sales    <- rep(NA, 12*n.years)
    trend.sales[1] <- 30
    for (i in 2:(12*n.years)){
       trend.sales[i] <- trend.sales[i-1] + dtrend.sales[i]
       if (trend.sales[i] < 0) trend.sales[i] = 0
    }
    
    # Create the seasonality
    seasonality <- rep(c(10, 30, 22, 32, 26, 14, 2, -15, -14, -13, -16, -2), 13)
    
    # Create the random noise, normally distributed
    noise <- qnorm(runif(12*n.years, 0.01, 0.99), mean=0, sd=18)
    
    # To make the noise heteroskedastic, uncomment the following line
    # noise <- noise * seq(1, 10, (10-1)/(12*n.years-1))
    
    # Create the sales
    sales <- trend.sales + seasonality + noise
    
    # Put everything into a data frame
    df.sales <- data.frame(sales, trend.sales, dtrend.sales, ddtrend.sales, seasonality, noise)
    
    # Set graphical parameters and the layout
    par(mar = c(0, 4, 0, 2)) # bottom, left, top, right
    layout(matrix(c(1,5,2,6,3,7,4,8), 4, 2, byrow = TRUE), widths=c(1,1,1,1,1,1,1,1), heights=c(1,1,1,1,1,1,1,1))
    
    # Plot sales
    tseries <- ts(data=df.sales$sales, frequency = 12, start=start.ym, end=end.ym)
    plot(tseries, ylab="Sales (USD, 1000's)", main="", xaxt="n")
    
    # Plot the trend
    tseries <- ts(data=df.sales$trend.sales, frequency = 12, start=start.ym, end=end.ym)
    plot(tseries, ylab="Actual Sales Trend (USD, 1000's)", main="", xaxt="n")
    
    # Plot the seasonality
    tseries <- ts(data=df.sales$seasonality, frequency = 12, start=start.ym, end=end.ym)
    plot(tseries, ylab="Actual Sales Seasonality (USD, 1000's)", main="", xaxt="n")
    
    # Plot the noise
    tseries <- ts(data=df.sales$noise, frequency = 12, start=start.ym, end=end.ym)
    plot(tseries, ylab="Actual Sales Noise (USD, 1000's)", main="", xaxt="n")
    
    # Decompose the sales time series
    undecomposed   <- ts(data=df.sales$sales, frequency = 12, start=start.ym, end=end.ym)
    decomposed     <- stl(undecomposed, s.window="periodic")
    seasonal 	   <- decomposed$time.series[,1]
    trend	       <- decomposed$time.series[,2]
    remainder	   <- decomposed$time.series[,3]
    
    # Plot sales
    tseries <- ts(data=df.sales$sales, frequency = 12, start=start.ym, end=end.ym)
    plot(tseries, ylab="Sales (USD, 1000's)", main="", xaxt="n")
    
    # Plot the decomposed trend
    tseries <- ts(data=trend, frequency = 12, start=start.ym, end=end.ym)
    plot(tseries, ylab="Est. Sales Trend (USD, 1000's)", main="", xaxt="n")
    
    # Plot the decomposed seasonality
    tseries <- ts(data=seasonal, frequency = 12, start=start.ym, end=end.ym)
    plot(tseries, ylab="Est. Sales Seasonality (USD, 1000's)", main="", xaxt="n")
    
    # Plot the decomposed noise
    tseries <- ts(data=remainder, frequency = 12, start=start.ym, end=end.ym)
    plot(tseries, ylab="Est. Sales Noise (USD, 1000's)", main="", xaxt="n")
    

    Author: Peter Rosenmai

    Posted on June 14, 2014 by Danielle Mosimann