4

Sometimes it is useful to present the coefficients of a regression with accompanying confidence intervals as a dot-and-whisker plot. See here for a motivation of this approach and some examples of what I am trying to achieve based on an R package dotwhisker designed to produce these plots. Here is an example from the linked R vignette: enter image description here I would like to replicate similar dot-and-whisker plots in PGFPLOTS.

PGFPLOTS supports box-and-whisker plots natively -- here is an example. A full description of the box-and-whisker feature of PGF is in section 5.12.1 of manual version 1.16.

Here is my best attempt:

\documentclass{article}
\usepackage{pgfplots}
\pgfplotsset{compat=1.16}
\usepgfplotslibrary{statistics}

\begin{document}
\begin{tikzpicture}
\begin{axis}[
    ytick={1,2,3},
    yticklabels={Group A, Group B, Group C},
    ]

\addplot+ [
boxplot prepared={
lower whisker=0.35, 
% lower quartile=,
median=0.6,
% upper quartile=, 
upper whisker=0.85,
},
]
coordinates {};

\addplot+ [
boxplot prepared={
lower whisker=0.55, 
% lower quartile=,
median=0.70,
% upper quartile=, 
upper whisker=0.85,
},
] 
coordinates {};

\addplot+ [
boxplot prepared={
lower whisker=0.85, 
% lower quartile=,
median=0.9,
% upper quartile=, 
upper whisker=0.95,
},
] 
coordinates {};
\end{axis}
\end{tikzpicture}

\end{document}

boxwhisker.PNG

There are several problems with my attempt:

  1. I am adapting the box and whisker feature to represent something that is not a box whisker. This is perhaps the wrong way to approach things. My kludge treats the median as the coefficient estimate, and treats the upper and lower whiskers as the upper and lower confidence intervals. I comment out the quartiles as I do not have an equivalent for these, and I do not want the box.
  2. Unlike the R package dotwhister, the graphics are not clear -- having vertical lines puts too much emphasis on the confidence bounds relative to the coefficient point estimate.
  3. It is not scalable. I would like to analyse 50 groups. Everytime I update my data I do not want to manually update the box plot entries in PGFPLOTS. I would prefer to be able to read from a .csv containing 50 rows for each group and four columns: group name, coefficient estimate, upper confidence interval, lower confidence interval.

Here is some randomly generated data (upper and lower are symmetric around point_est):

\begin{filecontents*}{dotwhisker.csv}
    group,point_est,upper,lower
    SCBQD6600C,0.318940138,0.782642805,-0.144762529
    GHECK1046A,0.541614386,1.425115639,-0.341886867
    ICOOO3242S,0.662666177,1.143455809,0.181876544
    PHOVQ7028A,0.148145345,0.239989182,0.056301508
    HSJEK0588Y,0.564368703,0.997673282,0.131064125
    CYVFG8255L,0.575908384,1.288811424,-0.136994656
    ZDYRJ3242S,0.413789006,0.639376662,0.18820135
    PXQSX1684J,0.418005222,0.974470232,-0.138459788
    VTCRK0417U,0.4153322,1.020437688,-0.189773288
    WSYWC4669M,0.366494326,0.756315385,-0.023326734
    BZPKZ2934L,0.428421095,0.792023892,0.064818298
    EGIPR1094A,0.350242033,0.598746704,0.101737362
    PQTFK6203U,0.383561916,0.660697282,0.10642655
    UYLGX7811M,0.509668823,1.037205877,-0.017868231
    ICEHA2251J,0.643924109,1.452395674,-0.164547457
\end{filecontents*}
6
  • To pre-suppose a comment, I know how to use R's tikzDevice package and would rather avoid that approach applied to the above R package for dot-whisker plots. I want to plot directly in PGFplots based on coefficient estimate and standard error.
    – Sav-econ
    Nov 23, 2019 at 17:58
  • I have no reason to prefer using the box whisker package in PGF. If it is easier to do in a scalable manner without box and whisker, please suggest an approach. Thanks.
    – Sav-econ
    Nov 24, 2019 at 12:49
  • Thanks fo adding the code! My main problem is that I am not really very firm in the backgrounds of this (even though I probably should) but if your question is if one could read out the values 0.55, 0.70 and 0.85 (or 0.55 and 0.15 for symmetric whiskers) from a csv file to create a series of \addplot+ [ boxplot prepared={ lower whisker=0.55, % lower quartile=, median=0.70, % upper quartile=, upper whisker=0.85, }, ] coordinates {}; which use these values, the answer is of course affirmative.
    – user194703
    Nov 24, 2019 at 12:57
  • Yes that would be what I am after as a start. Read a .csv where each row is a separate group, and there are four columns 1. group name 2. "median" 3. "upper whisker" 4. "lower whisker". I use quotations as this is not what is being represented statistically, but it is how we would interpret in terms of the box and whisker plot semantics. However, I say this is a start because I'd then be unsatisfied with the vertical line representations which are more confusing than having dots, see R dotwhisker package. Hence an alternative to the box whisker package in PGF might be more viable.
    – Sav-econ
    Nov 24, 2019 at 13:03
  • Can you provide a sketch of how these "whiskers" should look like?
    – user194703
    Nov 24, 2019 at 13:07

1 Answer 1

6

Here is a code that goes through a csv file and adds these "whiskers". The dimensions of the vertical lines are determined by /pgfplots/boxplot/box extend=0.1 (and /pgfplots/boxplot/whisker extend).

\documentclass{article}
\usepackage{pgfplots}
\usepackage{tikzlings}%<- for the second part of the answer ;-)
\usetikzlibrary{shapes.callouts}%<- for the second part of the answer
\usepackage{pgfplotstable}
\usepackage{filecontents}
\begin{filecontents*}{whiskers.dat}
0.6 0.35 0.85
0.7 0.55 0.85
0.9 0.85 0.95
\end{filecontents*}

\pgfplotsset{compat=1.16}
\usepgfplotslibrary{statistics}
\newcounter{iloop}
\newcommand*{\ReadOutElement}[4]{%
    \pgfplotstablegetelem{#2}{[index]#3}\of{#1}%
    \let#4\pgfplotsretval
}

\begin{document}
\begin{tikzpicture}
\pgfplotstableread[header=false]{whiskers.dat}{\datatable}   
\pgfplotstablegetrowsof{\datatable}
\pgfmathtruncatemacro{\numrows}{\pgfplotsretval}
\setcounter{iloop}{1}
\edef\MyLabels{Group A}
\loop\stepcounter{iloop}\edef\MyLabels{\MyLabels,Group \Alph{iloop}}%
\ifnum\value{iloop}<\numrows\repeat
\begin{axis}[
    ytick={1,...,\numrows},
    yticklabels/.expanded=\MyLabels,
    /pgfplots/boxplot/box extend=0.1,
    /pgfplots/boxplot/whisker extend=%
    \pgfkeysvalueof{/pgfplots/boxplot/box extend}*4,
    ]
\pgfplotsinvokeforeach{0,...,\the\numexpr\numrows-1}
{
\ReadOutElement{\datatable}{#1}{0}{\Median}
\ReadOutElement{\datatable}{#1}{1}{\Upper}
\ReadOutElement{\datatable}{#1}{2}{\Lower}
\edef\temp{\noexpand\addplot+ [mark=*,
boxplot prepared={
lower whisker=\Lower, 
% lower quartile=,
median=\Median,
% upper quartile=, 
upper whisker=\Upper,
},
] coordinates {(#1+1,\Median)};
}
\temp
}

\end{axis}
\end{tikzpicture}
\bigskip\bigskip

\begin{tikzpicture}
\marmot[whiskers,teeth]
\node[ellipse callout, fill=white,
    draw,
 font=\sffamily,align=center,%inner sep=-2pt,
 callout relative pointer={(-150:0.9)}] at (2,2.4) {How about\\ my whiskers?};
\end{tikzpicture}
\end{document}

enter image description here

Your larger data file works, too.

\documentclass{article}
\usepackage{pgfplots}
\usepackage{pgfplotstable}
\usepackage{filecontents}
\begin{filecontents*}{dotwhisker.csv}
    group,point_est,upper,lower
    RAMOF4414X,0.25858685,0.878367526,-0.161193826
    DOYDL6809E,0.555485848,1.400010767,-0.089039071
    YHYYL9849M,0.716888235,1.368127463,0.265649007
    JMQIG4509E,0.030860459,0.453325401,-0.191604482
    SQYRJ4202T,0.585824938,1.206768474,0.164881402
    OYSCJ0457U,0.601211178,1.362723257,0.0396991
    ZQAXZ5423I,0.385052008,0.892098437,0.078005579
    MDWEF7504F,0.390673629,1.063439815,-0.082092558
    IJUSO8696V,0.3871096,1.084017824,-0.109798624
    BQSBP8990Q,0.321992434,0.908002585,-0.064017717
    OCDWZ7363Q,0.40456146,0.981590932,0.027531988
    OHEEJ6147W,0.300322711,0.814591182,-0.01394576
    KCRLD3716J,0.344749221,0.875554365,0.013944077
    MFPJM1799W,0.512891763,1.177304879,0.048478648
    DIPEZ2121T,0.691898812,1.505729535,0.078068088
    LYOUI9349J,0.477942901,1.060060156,0.095825646
    VZFYT9091T,0.397798099,0.863976498,0.1316197
    OEQZM1870D,0.317760917,0.817961252,0.017560582
    VTGHR8934O,0.332565664,0.913827561,-0.048696233
    KMCVQ3734I,0.983167036,1.675403499,0.490930572
    ZALVK0344T,0.526817644,1.122164944,0.131470344
    PNXOL7981C,0.401862821,0.962308107,0.041417534
    PYIND4089E,0.411890426,0.88306747,0.140713381
    PNZTF5825U,0.591917888,1.201046391,0.182789386
    LPPRP3997H,0.361255951,1.008833148,-0.086321247
    VBUVF6797C,0.210359018,0.815565496,-0.194847461
    INHXV3841Z,0.614951144,1.31681457,0.113087718
    RPWRJ7560V,0.295268855,0.810041955,-0.019504244
    SCTCT3620G,0.49697234,1.070902402,0.123042278
    MAYYI9754J,0.31281028,0.774220754,0.051399806
\end{filecontents*}

\pgfplotsset{compat=1.16}
\usepgfplotslibrary{statistics}
\newcounter{iloop}
\newcommand*{\ReadOutElement}[4]{%
    \pgfplotstablegetelem{#2}{[index]#3}\of{#1}%
    \let#4\pgfplotsretval
}

\begin{document}
\begin{tikzpicture}
\pgfplotstableread[header=true,col sep=comma]{dotwhisker.csv}{\datatable}   
\pgfplotstablegetrowsof{\datatable}
\pgfmathtruncatemacro{\numrows}{\pgfplotsretval}
\ReadOutElement{\datatable}{0}{0}{\Label}
\edef\MyLabels{\Label}
\setcounter{iloop}{1}
\loop
\ReadOutElement{\datatable}{\number\value{iloop}}{0}{\Label}%
\edef\MyLabels{\MyLabels,\Label}%
\stepcounter{iloop}%
\ifnum\value{iloop}<\numrows\repeat
\begin{axis}[width=12cm,height=14cm,
    ytick={1,...,\numrows},
    yticklabels/.expanded=\MyLabels,
    /pgfplots/boxplot/box extend=0.1,
    /pgfplots/boxplot/whisker extend=%
    \pgfkeysvalueof{/pgfplots/boxplot/box extend}*4,
    ]
\pgfplotsinvokeforeach{0,...,\the\numexpr\numrows-1}
{
\ReadOutElement{\datatable}{#1}{1}{\Median}
\ReadOutElement{\datatable}{#1}{2}{\Upper}
\ReadOutElement{\datatable}{#1}{3}{\Lower}
\edef\temp{\noexpand\addplot+ [mark=*,
boxplot prepared={
lower whisker=\Lower, 
% lower quartile=,
median=\Median,
% upper quartile=, 
upper whisker=\Upper,
},
] coordinates {(#1+1,\Median)};
}
\temp
}

\end{axis}
\end{tikzpicture}
\end{document}

enter image description here

5
  • I updated an earlier dotwhisker.csv I provided to be symmetric contemporaneously to @Schrödinger'scat excellent answer which uses the original .csv. This answer works well regardless, except for automatically recognizing group names. Thanks.
    – Sav-econ
    Nov 24, 2019 at 14:58
  • 1
    Marmot is back! :)
    – CarLaTeX
    Nov 24, 2019 at 17:20
  • Yes. I just generated those strings randomly as names along with generating the numerical data randomly. I wanted to emphasize that it might not be as simple as looping over a numeric or alphabetic counter such as 'group A', 'group B' etc. My MWE was misleading in this sense. Of course, instead of random strings I could have put cities, countries, species or some other large categorial variable.
    – Sav-econ
    Nov 24, 2019 at 20:16
  • @Sav-econ The labels are in.
    – user194703
    Nov 24, 2019 at 20:22
  • 1
    @CarLaTeX YAY!!
    – manooooh
    Nov 25, 2019 at 0:40

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .