Repository: pandas-dev/pandas

Issue:
ENH: Expose symlog scaling in plotting API
The default log-scaled axes, activated by the `logx`, `logy`, and `loglog` methods to the Pandas plotting API, do the straightforward thing and take the log of 0 values. It then attempt to plot with these infinite logs, and makes the entire plot unusable without warning in the presence of 0s.

For example:

```python
draws = pd.DataFrame({'freq': np.random.zipf(1.7, 1000) - 1})
draws['rank'] = (-draws['freq']).rank()
draws.plot(x='rank', y='freq', kind='scatter', loglog=True)
```

Matplotlib provides another scale, the `symlog` scale, that makes a small region near 0 linear to avoid these problems. For quick-and-dirty 'look at my data on a log axis' plotting, `symlog` is significantly more useful.

I can access it like this:

```
draws = pd.DataFrame({'freq': np.random.zipf(1.7, 1000) - 1})
draws['rank'] = (-draws['freq']).rank()
p = draws.plot(x='rank', y='freq', kind='scatter', loglog=True)
p.set_xscale('symlog')
p.set_yscale('symlog')
p
```

Either making the `symlog` scale the default log scale for plotting, or supporting a `loglog='sym'` option, would make it significantly easier to do quick data inspection with Pandas' convenience plotting.


Discussion:
We probably wouldn't make it the default, unless matplotlib did as well.

A PR allowing 'sym' for logx, logy, and loglog seems fine.

On Mon, Jan 21, 2019 at 2:04 PM Michael Ekstrand <notifications@github.com>
wrote:

> The default log-scaled axes, activated by the logx, logy, and loglog
> methods to the Pandas plotting API, do the straightforward thing and take
> the log of 0 values. It then attempt to plot with these infinite logs, and
> makes the entire plot unusable without warning in the presence of 0s.
>
> Matplotlib provides another scale, the symlog scale, that makes a small
> region near 0 linear to avoid these problems. For quick-and-dirty 'look at
> my data on a log axis' plotting, symlog is significantly more useful.
>
> I can access it like this:
>
> ``
> dfp = nh_stat_df.plot(kind='scatter', x='nhelpful', y='count')
> dfp.set_xscale('symlog')
> dfp.set_yscale('symlog')
> dfp
>
>
> Either making the `symlog` scale the default log scale for plotting, or supporting a `loglog='sym'` option, would make it significantly easier to do quick data inspection with Pandas' convenience plotting.
>
> —
> You are receiving this because you are subscribed to this thread.
> Reply to this email directly, view it on GitHub
> <https://github.com/pandas-dev/pandas/issues/24867>, or mute the thread
> <https://github.com/notifications/unsubscribe-auth/ABQHIo11veYq_-C7dTx_0QVZHArxnFggks5vFh1GgaJpZM4aLc9p>
> .
>

@mdekstrand can you edit your post to include a minimal example? http://matthewrocklin.com/blog/work/2018/02/28/minimal-bug-reports. That way it'll be obvious when this can be closed.
@TomAugspurger done!

Resolving patch:
diff --git a/pandas/plotting/_core.py b/pandas/plotting/_core.py
--- a/pandas/plotting/_core.py
+++ b/pandas/plotting/_core.py
@@ -288,8 +288,10 @@ def _maybe_right_yaxis(self, ax, axes_num):
             if not self._has_plotted_object(orig_ax):  # no data on left y
                 orig_ax.get_yaxis().set_visible(False)
 
-            if self.logy or self.loglog:
+            if self.logy is True or self.loglog is True:
                 new_ax.set_yscale('log')
+            elif self.logy == 'sym' or self.loglog == 'sym':
+                new_ax.set_yscale('symlog')
             return new_ax
 
     def _setup_subplots(self):
@@ -311,10 +313,24 @@ def _setup_subplots(self):
 
         axes = _flatten(axes)
 
-        if self.logx or self.loglog:
+        valid_log = {False, True, 'sym', None}
+        input_log = {self.logx, self.logy, self.loglog}
+        if input_log - valid_log:
+            invalid_log = next(iter((input_log - valid_log)))
+            raise ValueError(
+                "Boolean, None and 'sym' are valid options,"
+                " '{}' is given.".format(invalid_log)
+            )
+
+        if self.logx is True or self.loglog is True:
             [a.set_xscale('log') for a in axes]
-        if self.logy or self.loglog:
+        elif self.logx == 'sym' or self.loglog == 'sym':
+            [a.set_xscale('symlog') for a in axes]
+
+        if self.logy is True or self.loglog is True:
             [a.set_yscale('log') for a in axes]
+        elif self.logy == 'sym' or self.loglog == 'sym':
+            [a.set_yscale('symlog') for a in axes]
 
         self.fig = fig
         self.axes = axes
@@ -1909,12 +1925,18 @@ def _plot(data, x=None, y=None, subplots=False,
         Place legend on axis subplots
     style : list or dict
         matplotlib line style per column
-    logx : bool, default False
-        Use log scaling on x axis
-    logy : bool, default False
-        Use log scaling on y axis
-    loglog : bool, default False
-        Use log scaling on both x and y axes
+    logx : bool or 'sym', default False
+        Use log scaling or symlog scaling on x axis
+        .. versionchanged:: 0.25.0
+
+    logy : bool or 'sym' default False
+        Use log scaling or symlog scaling on y axis
+        .. versionchanged:: 0.25.0
+
+    loglog : bool or 'sym', default False
+        Use log scaling or symlog scaling on both x and y axes
+        .. versionchanged:: 0.25.0
+
     xticks : sequence
         Values to use for the xticks
     yticks : sequence


Test patch:
diff --git a/pandas/tests/plotting/test_frame.py b/pandas/tests/plotting/test_frame.py
--- a/pandas/tests/plotting/test_frame.py
+++ b/pandas/tests/plotting/test_frame.py
@@ -248,16 +248,34 @@ def test_plot_xy(self):
         # TODO add MultiIndex test
 
     @pytest.mark.slow
-    def test_logscales(self):
+    @pytest.mark.parametrize("input_log, expected_log", [
+        (True, 'log'),
+        ('sym', 'symlog')
+    ])
+    def test_logscales(self, input_log, expected_log):
         df = DataFrame({'a': np.arange(100)}, index=np.arange(100))
-        ax = df.plot(logy=True)
-        self._check_ax_scales(ax, yaxis='log')
 
-        ax = df.plot(logx=True)
-        self._check_ax_scales(ax, xaxis='log')
+        ax = df.plot(logy=input_log)
+        self._check_ax_scales(ax, yaxis=expected_log)
+        assert ax.get_yscale() == expected_log
+
+        ax = df.plot(logx=input_log)
+        self._check_ax_scales(ax, xaxis=expected_log)
+        assert ax.get_xscale() == expected_log
+
+        ax = df.plot(loglog=input_log)
+        self._check_ax_scales(ax, xaxis=expected_log, yaxis=expected_log)
+        assert ax.get_xscale() == expected_log
+        assert ax.get_yscale() == expected_log
+
+    @pytest.mark.parametrize("input_param", ["logx", "logy", "loglog"])
+    def test_invalid_logscale(self, input_param):
+        # GH: 24867
+        df = DataFrame({'a': np.arange(100)}, index=np.arange(100))
 
-        ax = df.plot(loglog=True)
-        self._check_ax_scales(ax, xaxis='log', yaxis='log')
+        msg = "Boolean, None and 'sym' are valid options, 'sm' is given."
+        with pytest.raises(ValueError, match=msg):
+            df.plot(**{input_param: "sm"})
 
     @pytest.mark.slow
     def test_xcompat(self):
diff --git a/pandas/tests/plotting/test_series.py b/pandas/tests/plotting/test_series.py
--- a/pandas/tests/plotting/test_series.py
+++ b/pandas/tests/plotting/test_series.py
@@ -571,16 +571,21 @@ def test_df_series_secondary_legend(self):
         tm.close()
 
     @pytest.mark.slow
-    def test_secondary_logy(self):
+    @pytest.mark.parametrize("input_logy, expected_scale", [
+        (True, 'log'),
+        ('sym', 'symlog')
+    ])
+    def test_secondary_logy(self, input_logy, expected_scale):
         # GH 25545
         s1 = Series(np.random.randn(30))
         s2 = Series(np.random.randn(30))
 
-        ax1 = s1.plot(logy=True)
-        ax2 = s2.plot(secondary_y=True, logy=True)
+        # GH 24980
+        ax1 = s1.plot(logy=input_logy)
+        ax2 = s2.plot(secondary_y=True, logy=input_logy)
 
-        assert ax1.get_yscale() == 'log'
-        assert ax2.get_yscale() == 'log'
+        assert ax1.get_yscale() == expected_scale
+        assert ax2.get_yscale() == expected_scale
 
     @pytest.mark.slow
     def test_plot_fails_with_dupe_color_and_style(self):