Repository: statsmodels/statsmodels (Python)

Issue:
ENH: allow to plot OR or RR instead of logOR and logRR with `plot_forest`
With `statsmodels.stats.meta_analysis` `effectsize_2proportions`, we get the log odds ratio or log risk ratio when use the corresponding statistic option, and we can get the OR and RR, as well as their corresponding CI, by using `exp`
`np.exp(res_eyding.summary_frame())`
But we cannot plot the OR or RR with `plot_forest`.


Hints:
good idea

Just changing the plot would only be one line (or 3 total)

(I just saw that we don't specify a plot title. That seems to be currently up to the user.
AFAICS, the plot doesn't specify which effect size is used.)
What would be a good name for an option in `plot_forest`?
`use_exp=False`
anti-log or reverse log would be descriptive names

Stats uses `eform` to convert log-or and similar
#2235 my "eform" function only uses generic names like `transform`

another missing:
the plot_forest doesn't have `alpha=0.05, use_t=None` to forward options to summary_frame method
`use_exp=False` makes a lot of sense

for the plot title, xlabels etc, it's no big deal we can always use `plt.gca().set_title('blabla')`
question while I'm checking the unit tests:
is `res.use_t`  True for the combine results with log-or and similar proportion effect sizes?

I get a warning in the unit test, that `use_t=True` needs nobs

Patch:
diff --git a/statsmodels/stats/meta_analysis.py b/statsmodels/stats/meta_analysis.py
--- a/statsmodels/stats/meta_analysis.py
+++ b/statsmodels/stats/meta_analysis.py
@@ -276,13 +276,31 @@ def summary_frame(self, alpha=0.05, use_t=None):
         results = pd.DataFrame(res, index=labels, columns=col_names)
         return results
 
-    def plot_forest(self, ax=None, **kwds):
+    def plot_forest(self, alpha=0.05, use_t=None, use_exp=False,
+                    ax=None, **kwds):
         """Forest plot with means and confidence intervals
 
         Parameters
         ----------
         ax : None or matplotlib axis instance
             If ax is provided, then the plot will be added to it.
+        alpha : float in (0, 1)
+            Significance level for confidence interval. Nominal coverage is
+            ``1 - alpha``.
+        use_t : None or bool
+            If use_t is None, then the attribute `use_t` determines whether
+            normal or t-distribution is used for confidence intervals.
+            Specifying use_t overrides the attribute.
+            If use_t is false, then confidence intervals are based on the
+            normal distribution. If it is true, then the t-distribution is
+            used.
+        use_exp : bool
+            If `use_exp` is True, then the effect size and confidence limits
+            will be exponentiated. This transform log-odds-ration into
+            odds-ratio, and similarly for risk-ratio.
+        ax : AxesSubplot, optional
+            If given, this axes is used to plot in instead of a new figure
+            being created.
         kwds : optional keyword arguments
             Keywords are forwarded to the dot_plot function that creates the
             plot.
@@ -297,7 +315,9 @@ def plot_forest(self, ax=None, **kwds):
 
         """
         from statsmodels.graphics.dotplots import dot_plot
-        res_df = self.summary_frame()
+        res_df = self.summary_frame(alpha=alpha, use_t=use_t)
+        if use_exp:
+            res_df = np.exp(res_df[["eff", "ci_low", "ci_upp"]])
         hw = np.abs(res_df[["ci_low", "ci_upp"]] - res_df[["eff"]].values)
         fig = dot_plot(points=res_df["eff"], intervals=hw,
                        lines=res_df.index, line_order=res_df.index, **kwds)


Test patch:
diff --git a/statsmodels/stats/tests/test_meta.py b/statsmodels/stats/tests/test_meta.py
--- a/statsmodels/stats/tests/test_meta.py
+++ b/statsmodels/stats/tests/test_meta.py
@@ -11,6 +11,7 @@
 
 import numpy as np
 import pandas as pd
+import pytest
 
 from numpy.testing import assert_equal, assert_allclose
 
@@ -353,3 +354,14 @@ def test_basic(self):
                       var_weights=weights)
         res_glm = mod_glm.fit()
         assert_allclose(res_glm.params, res2.TE_random, rtol=1e-13)
+
+    @pytest.mark.matplotlib
+    def test_plot(self):
+        # smoke tests
+        res1 = self.res1
+        # `use_t=False` avoids warning about missing nobs for use_t is true
+        res1.plot_forest(use_t=False)
+        res1.plot_forest(use_exp=True, use_t=False)
+        res1.plot_forest(alpha=0.01, use_t=False)
+        with pytest.raises(TypeError, match="unexpected keyword"):
+            res1.plot_forest(junk=5, use_t=False)