Bayesian analysis of sensory profiling data, part 2

[This article was first published on Wiekvoet, and kindly contributed to R-bloggers]. (You can report issue about the content on this page here)
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.

Last week I made the core of a Bayesian model for sensory profiling data. This week the extras need to be added. That is, there are a bunch of extra interactions and the error is dependent on panelists and descriptors.
Note that where last week I pointed to influence of Procrustes and STATIS in these models, I probably should have mentioned Per Brockhoff’s work too.


See last week


A few features were added compared to last week: Round effect was averaged over all descriptors. It is now dependent on descriptors. As normalization, the sum of the round effects within a descriptor is fixed to be 0. Similar, a shift effect was defined per panelist. It is now per panelist*descriptor combination, again normalized to sum to 0 by descriptor. Residual error was defined as one variable for all data. It is now descriptor and panelist dependent. I decided to add variances there. It turned out to be quite a complex model. Running time was about 100 samples per minute on my hardware: too long to sit there waiting for it, but could fit in during a meeting or lunch.
model1 <-

data {
    int npanelist;
       int nobs;
    int nsession;
    int nround;
    int ndescriptor;
    int nproduct;
       vector[nobs] y;
       int panelist[nobs];
       int product[nobs];
    int descriptor[nobs];
    int rounds[nobs];
    real maxy;
parameters {
    matrix [nproduct,ndescriptor] profile;
    vector[npanelist] shift[ndescriptor];
    vector [npanelist] logsensitivity;
    vector [nround] roundeffect[ndescriptor];
    real varr;
    vector [npanelist] lpanelistvar;
    vector [ndescriptor] ldescriptorvar;
transformed parameters {
    vector [nobs] expect;
    vector[npanelist] sensitivity;
    real mlogsens;
    real mlpanelistvar;
    real mldescriptorvar;
    real mroundeff[ndescriptor];
    real meanshift[ndescriptor];
    vector [nobs] sigma;

    mlogsens <- mean(logsensitivity);
    for (i in 1:ndescriptor) {
       mroundeff[i] <- mean(roundeffect[i]);
       meanshift[i] <- mean(shift[i]);
    mlpanelistvar <- mean(lpanelistvar);
    mldescriptorvar <- mean(ldescriptorvar);        for (i in 1:npanelist) {
        sensitivity[i] <- pow(10,logsensitivity[i]-mlogsens);
    for (i in 1:nobs) {
        expect[i] <- profile[product[i],descriptor[i]]
            + shift[descriptor[i],panelist[i]]-meanshift[descriptor[i]]
            + roundeffect[descriptor[i],rounds[i]]-mroundeff[descriptor[i]];
        sigma[i] <- sqrt(varr
model {
    logsensitivity ~ normal(0,0.1);
    for (i in 1: ndescriptor) {
       roundeffect[i] ~ normal(0,maxy/10);
       shift[i] ~ normal(0,maxy/10);
       ldescriptorvar[i] ~ normal(0,1);
    for (i in 1:npanelist)
       lpanelistvar[i] ~ normal(0,1);
    y ~ normal(expect,sigma);
generated quantities    {
    vector [npanelist] panelistsd;
    vector [ndescriptor] descriptorsd;
    for (i in 1:npanelist) {
        panelistsd[i] <- sqrt(exp(lpanelistvar[i]));
    for (i in 1:ndescriptor) {
         descriptorsd[i] <- sqrt(exp(ldescriptorvar[i]));

model call

pars <- c('profile','shift','roundeffect','sensitivity',
datainchoc <- with(choc,list(

fitchoc <- stan(model_code = model1,
        data = datainchoc,
        iter = 500,
        chains = 4)


I do not think there is much point in showing all printed output. However the summary plot is interesting. There is something with the eight level of some of the factors, a few extra samples might be not unwelcome.
The error is more dependent on panelist than on descriptor, panelists 7, 20 and 29 might benefit from some training. 


The code for profile has been slightly modified, last week I only used a few of the samples. For me the intervals look nice and sharp. Other than that choc3 is very different from the others, more sweet, milk, less cocoa. Choc1 very bitter and cocoa.


It is premature based on one data set, but rounds do seem to have minimal effect. If this was structural on more data sets, this term might be removed.

Panelists’ shift

The plot shows that shift is important and this is a factor which should be in the model. Getting this under control might cost more than it is worth.

Code for plots

samples <- extract(fitchoc,'profile')$profile
nsamp <- dim(samples)[1]
profile <- expand.grid(Product=levels(choc$Product),
profile$des <- as.numeric(profile$Descriptor)
profile$prod <- as.numeric(profile$Product)
profs <-,function(i){
            subsamples <- c(samples[1:nsamp,profile$prod[i],profile$des[i]])
names(profs) <- c('score','lmin','lmax')
profile <- cbind(profile,profs)
p1 <- ggplot(profile, aes(y=score, x=des,color=Product))
p1 + geom_path() +
                labels=levels(profile$Descriptor)) +
    xlab(”) +
    geom_ribbon(aes(ymin=lmin, ymax=lmax,fill=Product),
                    alpha=.15,linetype=’blank’) +
samples <- extract(fitchoc,'roundeffect')$roundeffect
roundeffect <- expand.grid(Round=levels(choc$Rank),
roundeffect$des <- as.numeric(roundeffect$Descriptor)
roundeffect$round <- as.numeric(roundeffect$Round)
rounds <-,function(i){
            subsamples <- c(samples[1:nsamp,roundeffect$des[i],roundeffect$round[i]])
names(rounds) <- c('score','lmin','lmax')
roundeffect <- cbind(roundeffect,rounds)
p1 <- ggplot(roundeffect, aes(y=score, x=des,color=Round))
p1 + geom_path() +
                labels=levels(roundeffect$Descriptor)) +
        xlab(”) +
        geom_ribbon(aes(ymin=lmin, ymax=lmax,fill=Round),
                alpha=.15,linetype=’blank’) +
samples <- extract(fitchoc,'shift')$shift
shift <- expand.grid(Panelist=levels(choc$Panelist),
shift$des <- as.numeric(shift$Descriptor)
shift$pnlst <- as.numeric(shift$Panelist)
shifts <-,function(i){
                            subsamples <- c(samples[1:nsamp,shift$des[i],shift$pnlst[i]])
names(shifts) <- c('score','lmin','lmax')
shift <- cbind(shift,shifts)
p1 <- ggplot(shift, aes(y=score, x=des))
p1 + geom_path() +
                labels=levels(shift$Descriptor)) +
        xlab(”) +
        geom_ribbon(aes(ymin=lmin, ymax=lmax),
                alpha=.15,linetype=’blank’) +
        coord_flip() +
        facet_wrap(~ Panelist)

To leave a comment for the author, please follow the link and comment on their blog: Wiekvoet. offers daily e-mail updates about R news and tutorials about learning R and many other topics. Click here if you're looking to post or find an R/data-science job.
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.

Never miss an update!
Subscribe to R-bloggers to receive
e-mails with the latest R posts.
(You will not see this message again.)

Click here to close (This popup will not appear again)