Marker style

How markers are rendered is determined by the employed graph scheme. Use marker options to change the default look. The marker options can be specified for each series separately, but also as global options (in which case they are taken as defaults for the individual series):

. sysuse auto, clear
(1978 automobile data)

. keep if rep78>=3
(10 observations deleted)

. regress mpg headroom i.rep i.foreign
(output omitted)

. estimates store m1

. regress mpg headroom i.rep##i.foreign
(output omitted)

. estimates store m2

. coefplot (m1, msymbol(D) mlcolor(magenta) mfcolor(magenta*.3)) ///
>          (m2, msymbol(S)) ///
>          , mfcolor(white) msize(large)
Code
markers/markeropts.svg

The overall style of markers and their confidence intervals can also be changed using the pstyle() option (pstyles are named composite styles provided by the graph scheme). By default, coefplot uses the first pstyle for the first series, the second pstyle for the second series, and so on. Here is an example in which the styles are changed:

. coefplot (m1, pstyle(p3)) (m2, pstyle(p4))
Code
markers/pstyle.svg

Specifying pstyle() changes the pstyle both for markers and confidence spikes. To use a different pstyle for the confidence spikes, specify option cipts(pstyle()):

. coefplot (m1, ciopts(pstyle(p2))) (m2, pstyle(p3) ciopts(pstyle(p4)))
Code
markers/pstyle2.svg

Markers only / CIs only

To print markers without confidence intervals, you can specify the noci option; to print only the confidence intervals without markers, type cionly:

. sysuse auto, clear
(1978 automobile data)

. regress price mpg trunk length turn
(output omitted)

. coefplot (., noci label(Markers only)) ///
>          (., cionly label(CIs only) key(ci)) ///
>     , drop(_cons)
Code
markers/nocicionly.svg

Series without markers are omitted from the legend. This is why option key(ci) has been specified. The option requests generating a legend key for the confidence interval.

[top]

Marker labels

Values of point estimates

To add the values of the point estimates as marker labels, use the mlabel option, possibly together with format() to set the display format:

. sysuse auto, clear
(1978 automobile data)

. keep if rep78>=3
(10 observations deleted)

. regress mpg headroom i.rep##i.foreign
(output omitted)

. coefplot, xline(0) mlabel format(%9.2g) mlabposition(12) mlabgap(*2)
Code
markers/mlabel.svg

Stata graphs do not support background colors for marker labels, which makes labels unreadable if you place them on top of the markers using mlabposition(0). However, here is a workaround. The trick is to add a second "confidence interval" that is a bar of fixed width:

. mata: st_matrix("e(box)", (st_matrix("e(b)"):-2 \ st_matrix("e(b)"):+2))

. coefplot, xline(0) mlabel format(%9.2g) mlabposition(0) msymbol(i) ///
>     ci(95 box) ciopts(recast(. rbar) barwidth(. 0.35) color(. white))
Code
markers/mlabel2.svg

The dot in the suboptions within ciopts() specifies the "default" style; see help stylelists.

Instead of physically adding the coordinates of the bars as matrix e(box) to the estimation set, the coordinates can also be generated on the fly by the transform() option. In the example below, a second set of confidence limits is requested (the specified level does not matter as long as its unique) that is then transformed to b±2 with the help of transform() (also see Accessing internal temporary variables):

. sysuse auto, clear
(1978 automobile data)

. keep if rep78>=3
(10 observations deleted)

. regress mpg headroom i.rep##i.foreign
(output omitted)

. coefplot, xline(0) mlabel format(%9.2g) mlabposition(0) msymbol(i) ///
>     ci(95 99) transform(* = "cond(@==@ll2, @b-2, cond(@==@ul2, @b+2, @))") ///
>     ciopts(recast(. rbar) barwidth(. 0.35) color(. white))
Code
markers/mlabel2b.svg

Here is a further example where a box is placed around the numbers:

. coefplot, xline(0) mlabel format(%9.2g) mlabposition(0) msymbol(i) ///
>     ci(95 99) transform(* = "cond(@==@ll2, @b-2, cond(@==@ul2, @b+2, @))") ///
>     ciopts(recast(. rbar) barwidth(. 0.35) fcolor(. white) lwidth(. medium))
Code
markers/mlabel3.svg

A bit unfortunate might be that due to the box the exact location of a coefficient can no longer be seen in the graph. Here is an example where an additional vertical spike is added to mark the point estimates.

. coefplot, xline(0) mlabel format(%9.2g) mlabposition(0) msymbol(i) ///
>     ci(95 (b b) 99) transform(* = "cond(@==@ll3, @b-2, cond(@==@ul3, @b+2, @))") ///
>     ciopts(recast(. rcap rbar) msize(. 7 .) barwidth(. . 0.35) ///
>         fcolor(. . white) lwidth(. . medium))
Code
markers/mlabel4.svg

In the example, a confidence interval of zero width is used to produce the vertical spikes ((b b) means to include a confidence interval whose lower and upper limits are both equal to e(b)).

The plot might still not be optimal since for the first coefficient, the confidence interval is hidden behind the marker label box. Plotting the confidence intervals as bars can, for example, solve this problem:

. coefplot, xline(0) mlabel format(%9.2g) mlabposition(0) msymbol(i) ///
>     ci(95 (b b) 99) transform(* = "cond(@==@ll3, @b-2, cond(@==@ul3, @b+2, @))") ///
>     ciopts(recast(rbar rcap rbar) bstyle(ci2 . .) msize(. 7 .) ///
>         barwidth(0.5 . 0.35) fcolor(. . white) lwidth(. . medium))
Code
markers/mlabel5.svg
[top]

String expressions

The default for the mlabel option is to display the values of the point estimates as marker labels. To display p-values instead of point estimates, for example, you could type mlabel(@pval) (see Accessing internal temporary variables for available @-variables). Furthermore, a string expression may be provided as an argument to mlabel() to construct more complicated marker labels, as in the following example:

. sysuse auto, clear
(1978 automobile data)

. keep if rep78>=3
(10 observations deleted)

. regress mpg headroom i.rep##i.foreign
(output omitted)

. coefplot, xline(0) mlabposition(1) mlabgap(*2) ///
>     mlabel("{it:p} = " + string(@pval,"%9.3f"))
Code
markers/strmlabel.svg

Here is a more complicated example that displays different symbols as marker labels depending on the size of the p-value:

. coefplot, xline(0) mlabposition(1) ///
>     mlabel(cond(@pval<.001, "***", ///
>            cond(@pval<.01, "**",   ///
>            cond(@pval<.05, "*",    ///
>            cond(@pval<.1, "+", ""))))) ///
>     note("+ p < .1, * p < .05, ** p < .01, *** p < .001")
Code
markers/strmlabel2.svg
[top]

Custom labels

To create custom labels for specific markers, you can use the mlabels() option. For example, the following graph includes information on hypotheses (positive effect or null effect) as marker labels:

. sysuse auto, clear
(1978 automobile data)

. regress price mpg trunk length turn if foreign==0
(output omitted)

. estimates store domestic

. regress price mpg trunk length turn if foreign==1
(output omitted)

. estimates store foreign

. coefplot (domestic, mlabels(length = 1 "+" * = 11 "0")) ///
>          (foreign, mlabels(trunk length = 1 "+" * = 11 "0")) ///
>     , drop(_cons) xline(0) note("Hypotheses: + positive effect; 0 no effect")
Code
markers/cmlabel.svg

Of course, you can also attach labels only to some of the coefficients and you can use marker label options to affect the rendering of the labels:

. coefplot ///
>     (domestic, mlabels(trunk = 12 "I don't like that this is not significant") ///
>         mlabangle(45) mlabgap(2) mlabsize(medium) mlabcolor(red)) ///
>     (foreign, mlabels(length = 1 "This is the {bf:only} significant effect!") ///
>         mlabtextstyle(small_label)) ///
>     , drop(_cons) xline(0) 
Code
markers/cmlabel2.svg
[top]

Labels at end of confidence intervals

As seen above, marker labels will always be placed at the values of the point estimates. In some situations, however, it may be preferable to place the labels, say, at the upper end of the confidence intervals. coefplot has no option to change the anchor of marker labels, but here is a workaround using the addplot() option with internal temporary variables:

. sysuse auto, clear
(1978 automobile data)

. regress price mpg trunk length turn if foreign==0
(output omitted)

. estimates store domestic

. regress price mpg trunk length turn if foreign==1
(output omitted)

. estimates store foreign

. coefplot domestic foreign, drop(_cons) xline(0) ///
>     addplot(scatter @at @ul, ms(i) mlabel(@b) mlabformat("%9.1f") ///
>         mlabcolor(black) mlabpos(2))
Code
markers/cilabel.svg

The approach works well if you just want to print the values of the estimates. For more complex labels, first create the labels employing the usual mlabel() option, but suppress their display by setting the color to none. The created labels will be stored in temporary variable @mlbl. You can then use this variable in the addplot() option:

. coefplot domestic foreign, drop(_cons) xline(0) ///
>     mlabel("{it:p} = " + string(@pval,"%9.3f")) mlabcolor(none) ///
>     addplot(scatter @at @ul, ms(i) mlabel(@mlbl) mlabcolor(black) mlabpos(2))
Code
markers/cilabel1.svg

If you want to use different styles for the labels depending on model, include multiple plots in the addplot() option. The relevant internal variable to select the models is called @plot

. coefplot domestic foreign, drop(_cons) xline(0) ///
>     mlabel("{it:p} = " + string(@pval,"%9.3f"))  mlabcolor(none) ///
>     addplot(scatter @at @ul if @plot==1, ms(i) mlabel(@mlbl) pstyle(p1) ///
>          || scatter @at @ul if @plot==2, ms(i) mlabel(@mlbl) pstyle(p2))
Code
markers/cilabel2.svg

Note that the approach also works with custom labels generated by option mlabels():

. coefplot (domestic, mlabels(length = 0 "+" trunk = 0 "0")) ///
>          (foreign, mlabels(trunk length = 0 "+")) ///
>     , drop(_cons) xline(0) note("Hypotheses: + positive effect; 0 no effect") ///
>     mlabcolor(none) ///
>     addplot(scatter @at @ul, ms(i) mlabel(@mlbl) mlabcolor(black))
Code
markers/cilabel3.svg
[top]

Marker labels as axis labels

Nino Landler asked on Statalist whether it is possible to print the values of the estimates on the right axis of a plot. This is not directly possible, but you can write a little program to call coefplot without drawing a graph and collect the labels. The labels can then be included as axis labels in a second call to coefplot. The program could look about as follows:

. capt program drop coefplot_mlbl

. *! version 1.0.0  10jun2021  Ben Jann
. program coefplot_mlbl, sclass
  1.     _parse comma plots 0 : 0
  2.     syntax [, MLabel(passthru) * ]
  3.     if `"`mlabel'"'=="" local mlabel mlabel(string(@b))
  4.     preserve
  5.     qui coefplot `plots', `options' `mlabel' generate replace nodraw
  6.     sreturn clear
  7.     tempvar touse
  8.     qui gen byte `touse' = __at<.
  9.     mata: st_global("s(mlbl)", ///
>         invtokens((strofreal(st_data(.,"__at","`touse'")) :+ " " :+ ///
>         "`" :+ `"""' :+ st_sdata(.,"__mlbl","`touse'") :+ `"""' :+ "'")'))
 10.     sreturn local plots `"`plots'"'
 11.     sreturn local options `"`options'"'
 12. end

. sysuse auto, clear
(1978 automobile data)

. regress price mpg trunk length turn if foreign==0
(output omitted)

. estimates store D

. regress price mpg trunk length turn if foreign==1
(output omitted)

. estimates store F

. coefplot_mlbl D F, drop(_cons) xline(0)

. sreturn list

macros:
            s(options) : "drop(_cons) xline(0)"
              s(plots) : "D F"
               s(mlbl) : ".8333333 `"-186.1083"' 1.166667 `"-55.72135"' 1.833333 `"-60.."
Code

The idea is that you specify the full coefplot syntax already when collecting the labels to ensure that the collected labels are consistent with the graph you want to create. The labels are returned in s(mlbl); the corresponding coefplot command is returned in two parts in s(plots) and s(options). To draw the graph including the labels you can type:

. coefplot_mlbl D F, drop(_cons) xline(0)

. coefplot `s(plots)', `s(options)' ///
>     ymlabel(`s(mlbl)', angle(0) notick axis(2)) ///
>     yaxis(1 2) yscale(alt) yscale(axis(2) alt noline) ///
>     ylabel(none, axis(2)) yti("", axis(2))
Code
markers/axismlbl2.svg

Note that the s() returns will only be available immediately after running coefplot_mlbl.

The precise contents of the labels can be determined by specifying option mlabel() (or mlabels()) in the usual way when calling coefplot_mlbl:

. coefplot_mlbl D F, drop(_cons) xline(0) mlabel("p = "+string(@pval,"%9.3f"))

. coefplot `s(plots)', `s(options)' ///
>     ymlabel(`s(mlbl)', angle(0) notick axis(2)) ///
>     yaxis(1 2) yscale(alt) yscale(axis(2) alt noline) ///
>     ylabel(none, axis(2)) yti("", axis(2))
Code
markers/axismlbl3.svg

The syntax of the final coefplot command is quite complicated because a secondary axis has to be created. An idea could be to write a little wrapper so that you can produce many plots without having to repeat these options. Here is a corresponding program:

. capt program drop coefplot_ymlbl

. *! version 1.0.0  10jun2021  Ben Jann
. program coefplot_ymlbl
  1.     _parse comma plots 0 : 0
  2.     syntax [, MLabel(str asis) * ]
  3.     _parse comma mlspec mlopts : mlabel
  4.     local mlopts = substr(`"`mlopts'"', 2, .) // remove leading comma
  5.     if `"`mlspec'"'!="" local mlabel mlabel(`mlspec')
  6.     else                local mlabel
  7.     coefplot_mlbl `plots', `options' `mlabel'
  8.     coefplot `plots',  ///
>         yaxis(1 2) yscale(alt) yscale(axis(2) alt noline) ///
>         ylabel(none, axis(2)) yti("", axis(2)) ///
>         ymlabel(`s(mlbl)', axis(2) notick angle(0) `mlopts') `options'
  9. end
Code

You can then type

. coefplot_ymlbl D F, drop(_cons) xline(0)
Code
markers/axismlbl5.svg

or, for example,

. coefplot_ymlbl D F, drop(_cons) xline(0) mlabel("p = "+string(@pval,"%9.3f"))
Code
markers/axismlbl6.svg

Of course you may need to tweak the program a bit so that it creates the graphs you have in mind.

The coefplot_mlbl program defined above does not make a difference between the models in the plot. If you want to style the labels differently depending on model, you need to modify the program so that it returns multiple lists of labels. Here is an example:

. capt program drop coefplot_mlbl2

. *! version 1.0.0  10jun2021  Ben Jann
. program coefplot_mlbl2, sclass
  1.     _parse comma plots 0 : 0
  2.     syntax [, MLabel(passthru) * ]
  3.     if `"`mlabel'"'=="" local mlabel mlabel(string(@b))
  4.     preserve
  5.     qui coefplot `plots', `options' `mlabel' generate replace nodraw
  6.     sreturn clear
  7.     tempvar touse
  8.     qui gen byte `touse' = 0
  9.     local nplots = r(n_plots)
 10.     forv i = 1/`nplots' {
 11.         qui replace `touse' = __plot==`i' & __at<.
 12.         mata: st_global("s(mlbl`i')", ///
>             invtokens((strofreal(st_data(.,"__at","`touse'")) :+ " " :+ ///
>             "`" :+ `"""' :+ st_sdata(.,"__mlbl","`touse'") :+ `"""' :+ "'")'))
 13.     }
 14.     sreturn local plots `"`plots'"'
 15.     sreturn local options `"`options'"'
 16. end

. coefplot_mlbl2 D F, drop(_cons) xline(0) mlabel("p = "+string(@pval,"%9.3f"))

. coefplot `s(plots)', `s(options)' ///
>     ymlabel(`s(mlbl1)', angle(0) notick axis(2) add custom labcolor(navy)) ///
>     ymlabel(`s(mlbl2)', angle(0) notick axis(2) add custom labcolor(maroon)) ///
>     yaxis(1 2) yscale(alt) yscale(axis(2) alt noline) ///
>     ylabel(none, axis(2)) yti("", axis(2))
Code
markers/axismlbl7.svg
[top]

Weighted markers

To scale the size of markers use the weight() option. In the following example, the sizes of the marker symbols are proportional to the inverse of the standard errors:

. sysuse auto, clear
(1978 automobile data)

. regress price mpg trunk length turn
(output omitted)

. coefplot, weight(1/@se) ms(oh) drop(_cons) xline(0) 
Code
markers/weight.svg

@se is an internal variable provided by coefplot. See Accessing internal temporary variables in the help file for the list of available internal variables. Additional internal variables can be made available through the aux() option. Here is an example in which the markers for group means are scaled by the number of observations in each group:

. sysuse auto, clear
(1978 automobile data)

. version 15: mean price, over(rep78)

Mean estimation                             Number of obs = 69

            1: rep78 = 1
            2: rep78 = 2
            3: rep78 = 3
            4: rep78 = 4
            5: rep78 = 5

--------------------------------------------------------------
        Over |       Mean   Std. err.     [95% conf. interval]
-------------+------------------------------------------------
price        |
           1 |     4564.5      369.5      3827.174    5301.826
           2 |   5967.625   1265.494      3442.372    8492.878
           3 |   6429.233   643.5995       5144.95    7713.516
           4 |     6071.5   402.9585      5267.409    6875.591
           5 |       5913   788.6821      4339.209    7486.791
--------------------------------------------------------------

. matrix list e(_N)

e(_N)[1,5]
     price:  price:  price:  price:  price:
         1       2       3       4       5
r1       2       8      30      18      11

. coefplot, ciopts(recast(rcap)) aux(_N) weight(@aux1) mfcolor(*.6)
Code
markers/weight2.svg

The mean command returns the group sizes in vector e(_N) from where they can be read by typing aux(_N).

If you apply the weight() option in a graph that contains multiple series, marker sizes will be computed for each series separately. Consider the following example:

. sysuse auto, clear
(1978 automobile data)

. regress price mpg trunk length turn if foreign==0
(output omitted)

. estimates store domestic

. regress price mpg trunk length turn if foreign==1
(output omitted)

. estimates store foreign

. coefplot domestic foreign, weight(1/@se) ms(oh) drop(_cons) xline(0)
Code
markers/weight3.svg

The sizes of the marker can be compared within series, that is, within series they are proportional to the inverse of the standard errors. Between series, however, this relation does not hold. Here is a picture in which the same information is displayed while applying weight() to all markers at once (see the help file for information on options asequation and swapnames):

. coefplot (domestic foreign), asequation swapnames nolabel ///
>     weight(1/@se) ms(oh) drop(_cons) xline(0)
Code
markers/weight4.svg

As is evident, to be comparable, the sizes of the markers for domestic have to be made a bit larger. A trick to make marker sizes comparable across series is to include all data in each series, so that the same information is used to determine the marker sizes, but then suppress the extra estimates by setting them to missing using the transform() option:

. coefplot (foreign, transform(* = .) \ domestic) ///
>          (domestic, transform(* = .) \ foreign) ///
>     , weight(1/@se) ms(oh) drop(_cons) xline(0)
(transform missing for some coefficients or CIs)
Code
markers/weight5.svg