Repository: arviz-devs/arviz

Issue:
Add method to delete groups from InferenceData
## Tell us about it
Add a method to delete groups from InferenceData

With many groups InferenceData can get quite large, making it hard to add to VCS or email. Things like sample_stats might not be needed for all results sharing. 


## Thoughts on implementation
1. Delete the object from `az.InferenceData` with `del` method in python
2. Remove string in `az.InferenceData._groups`

Discussion:
canyon289: Looks good. Just could use a couple of tests

Patch:
diff --git a/arviz/data/datasets.py b/arviz/data/datasets.py
index afbb7bc..a40f7e8 100644
--- a/arviz/data/datasets.py
+++ b/arviz/data/datasets.py
@@ -61,7 +61,71 @@ See Gelman and Hill (2006) for details on the example, or
 https://docs.pymc.io/notebooks/multilevel_modeling.html#Correlations-among-levels
 by Chris Fonnesbeck for details on this implementation.
 """,
-    )
+    ),
+    "rugby": RemoteFileMetadata(
+        filename="rugby.nc",
+        url="https://ndownloader.figshare.com/files/16254359",
+        checksum="9eecd2c6317e45b0388dd97ae6326adecf94128b5a7d15a52c9fcfac0937e2a6",
+        description="""
+The Six Nations Championship is a yearly rugby competition between Italy, Ireland,
+Scotland, England, France and Wales. Fifteen games are played each year, representing
+all combinations of the six teams.
+
+This example uses and includes results from 2014 - 2017, comprising 60 total
+games. It models latent parameters for each team's attack and defense, as well
+as a parameter for home team advantage.
+
+See https://docs.pymc.io/notebooks/rugby_analytics.html by Peader Coyle
+for more details and references.
+""",
+    ),
+    "regression1d": RemoteFileMetadata(
+        filename="regression1d.nc",
+        url="https://ndownloader.figshare.com/files/16254899",
+        checksum="909e8ffe344e196dad2730b1542881ab5729cb0977dd20ba645a532ffa427278",
+        description="""
+A synthetic one dimensional linear regression dataset with latent slope,
+intercept, and noise ("eps"). One hundred data points, fit with PyMC3.
+
+True slope and intercept are included as deterministic variables.
+""",
+    ),
+    "regression10d": RemoteFileMetadata(
+        filename="regression10d.nc",
+        url="https://ndownloader.figshare.com/files/16255736",
+        checksum="c6716ec7e19926ad2a52d6ae4c1d1dd5ddb747e204c0d811757c8e93fcf9f970",
+        description="""
+A synthetic multi-dimensional (10 dimensions) linear regression dataset with
+latent weights ("w"), intercept, and noise ("eps"). Five hundred data points,
+fit with PyMC3.
+
+True weights and intercept are included as deterministic variables.
+""",
+    ),
+    "classification1d": RemoteFileMetadata(
+        filename="classification1d.nc",
+        url="https://ndownloader.figshare.com/files/16256678",
+        checksum="1cf3806e72c14001f6864bb69d89747dcc09dd55bcbca50aba04e9939daee5a0",
+        description="""
+A synthetic one dimensional logistic regression dataset with latent slope and
+intercept, passed into a Bernoulli random variable. One hundred data points,
+fit with PyMC3.
+
+True slope and intercept are included as deterministic variables.
+""",
+    ),
+    "classification10d": RemoteFileMetadata(
+        filename="classification10d.nc",
+        url="https://ndownloader.figshare.com/files/16256681",
+        checksum="16c9a45e1e6e0519d573cafc4d266d761ba347e62b6f6a79030aaa8e2fde1367",
+        description="""
+A synthetic multi dimensional (10 dimensions) logistic regression dataset with
+latent weights ("w") and intercept, passed into a Bernoulli random variable.
+Five hundred data points, fit with PyMC3.
+
+True weights and intercept are included as deterministic variables.
+""",
+    ),
 }
 
 
@@ -160,11 +224,14 @@ def load_arviz_data(dataset=None, data_home=None):
             )
         return from_netcdf(file_path)
     else:
-        raise ValueError(
-            "Dataset {} not found! The following are available:\n{}".format(
-                dataset, list_datasets()
+        if dataset is None:
+            return dict(itertools.chain(LOCAL_DATASETS.items(), REMOTE_DATASETS.items()))
+        else:
+            raise ValueError(
+                "Dataset {} not found! The following are available:\n{}".format(
+                    dataset, list_datasets()
+                )
             )
-        )
 
 
 def list_datasets():
diff --git a/arviz/data/inference_data.py b/arviz/data/inference_data.py
index 074af70..29c97db 100644
--- a/arviz/data/inference_data.py
+++ b/arviz/data/inference_data.py
@@ -42,6 +42,11 @@ class InferenceData:
             options="\n\t> ".join(self._groups)
         )
 
+    def __delattr__(self, group):
+        """Delete a group from the InferenceData object."""
+        self._groups.remove(group)
+        object.__delattr__(self, group)
+
     @staticmethod
     def from_netcdf(filename):
         """Initialize object from a netcdf file.
@@ -73,16 +78,18 @@ class InferenceData:
                     groups[group] = data
         return InferenceData(**groups)
 
-    def to_netcdf(self, filename, compress=True):
+    def to_netcdf(self, filename, compress=True, groups=None):
         """Write InferenceData to file using netcdf4.
 
         Parameters
         ----------
         filename : str
             Location to write to
-        compress : bool
+        compress : bool, optional
             Whether to compress result. Note this saves disk space, but may make
             saving and loading somewhat slower (default: True).
+        groups : list, optional
+            Write only these groups to netcdf file.
 
         Returns
         -------
@@ -91,7 +98,11 @@ class InferenceData:
         """
         mode = "w"  # overwrite first, then append
         if self._groups:  # check's whether a group is present or not.
-            for group in self._groups:
+            if groups is None:
+                groups = self._groups
+            else:
+                groups = [group for group in self._groups if group in groups]
+            for group in groups:
                 data = getattr(self, group)
                 kwargs = {}
                 if compress:
diff --git a/arviz/plots/compareplot.py b/arviz/plots/compareplot.py
index 47bef56..dce807c 100644
--- a/arviz/plots/compareplot.py
+++ b/arviz/plots/compareplot.py
@@ -9,6 +9,7 @@ def plot_compare(
     insample_dev=True,
     plot_standard_error=True,
     plot_ic_diff=True,
+    order_by_rank=True,
     figsize=None,
     textsize=None,
     plot_kwargs=None,
@@ -28,7 +29,7 @@ def plot_compare(
 
     Parameters
     ----------
-    comp_df: pd.DataFrame
+    comp_df : pd.DataFrame
         Result of the `az.compare()` method
     insample_dev : bool, optional
         Plot in-sample deviance, that is the value of the information criteria without the
@@ -38,6 +39,8 @@ def plot_compare(
     plot_ic_diff : bool, optional
         Plot standard error of the difference in information criteria between each model
          and the top-ranked model. Defaults to True
+    order_by_rank : bool
+        If True (default) ensure the best model is used as reference.
     figsize : tuple, optional
         If None, size is (6, num of models) inches
     textsize: float
@@ -101,6 +104,9 @@ def plot_compare(
             " information criterion: {}".format(_information_criterion)
         )
 
+    if order_by_rank:
+        comp_df.sort_values(by="rank", inplace=True)
+
     if plot_ic_diff:
         yticks_labels[0] = comp_df.index[0]
         yticks_labels[2::2] = comp_df.index[1:]
diff --git a/arviz/plots/forestplot.py b/arviz/plots/forestplot.py
index 6225148..85a1ba3 100644
--- a/arviz/plots/forestplot.py
+++ b/arviz/plots/forestplot.py
@@ -210,9 +210,7 @@ def plot_forest(
     for ax in axes:
         ax.grid(False)
         # Remove ticklines on y-axes
-        for ticks in ax.yaxis.get_major_ticks():
-            ticks.tick1line.set_visible = False
-            ticks.tick2line.set_visible = False
+        ax.tick_params(axis="y", left=False, right=False)
 
         for loc, spine in ax.spines.items():
             if loc in ["left", "right"]:
@@ -574,7 +572,7 @@ class VarHandler:
             values = values[np.isfinite(values)]
 
             if ridgeplot_kind == "auto":
-                kind = "hist" if all(np.mod(values, 1) == 0) else "density"
+                kind = "hist" if np.all(np.mod(values, 1) == 0) else "density"
             else:
                 kind = ridgeplot_kind
 
diff --git a/arviz/stats/stats.py b/arviz/stats/stats.py
index 9732643..95d23b6 100644
--- a/arviz/stats/stats.py
+++ b/arviz/stats/stats.py
@@ -88,8 +88,9 @@ def compare(
 
     Returns
     -------
-    A DataFrame, ordered from lowest to highest IC. The index reflects the key with which the
-    models are passed to this function. The columns are:
+    A DataFrame, ordered from best to worst model (measured by information criteria).
+    The index reflects the key with which the models are passed to this function. The columns are:
+    rank : The rank-order of the models. 0 is the best.
     IC : Information Criteria (WAIC or LOO).
         Smaller IC indicates higher out-of-sample predictive fit ("better" model). Default WAIC.
         If `scale == log` higher IC indicates higher out-of-sample predictive fit ("better" model).
@@ -146,7 +147,17 @@ def compare(
         ic_func = waic
         df_comp = pd.DataFrame(
             index=names,
-            columns=["waic", "p_waic", "d_waic", "weight", "se", "dse", "warning", "waic_scale"],
+            columns=[
+                "rank",
+                "waic",
+                "p_waic",
+                "d_waic",
+                "weight",
+                "se",
+                "dse",
+                "warning",
+                "waic_scale",
+            ],
         )
         scale_col = "waic_scale"
 
@@ -154,7 +165,17 @@ def compare(
         ic_func = loo
         df_comp = pd.DataFrame(
             index=names,
-            columns=["loo", "p_loo", "d_loo", "weight", "se", "dse", "warning", "loo_scale"],
+            columns=[
+                "rank",
+                "loo",
+                "p_loo",
+                "d_loo",
+                "weight",
+                "se",
+                "dse",
+                "warning",
+                "loo_scale",
+            ],
         )
         scale_col = "loo_scale"
 
@@ -175,6 +196,7 @@ def compare(
         ics = ics.append([ic_func(dataset, pointwise=True, scale=scale)])
     ics.index = names
     ics.sort_values(by=ic, inplace=True, ascending=ascending)
+    ics[ic_i] = ics[ic_i].apply(lambda x: x.values.flatten())
 
     if method.lower() == "stacking":
         rows, cols, ic_i_val = _ic_matrix(ics, ic_i)
@@ -242,14 +264,15 @@ def compare(
         for idx, val in enumerate(ics.index):
             res = ics.loc[val]
             if scale_value < 0:
-                diff = (res[ic_i] - min_ic_i_val).values
+                diff = res[ic_i] - min_ic_i_val
             else:
-                diff = (min_ic_i_val - res[ic_i]).values
+                diff = min_ic_i_val - res[ic_i]
             d_ic = np.sum(diff)
             d_std_err = np.sqrt(len(diff) * np.var(diff))
             std_err = ses.loc[val]
             weight = weights[idx]
             df_comp.at[val] = (
+                idx,
                 res[ic],
                 res[p_ic],
                 d_ic,
diff --git a/examples/plot_forest_ridge.py b/examples/plot_forest_ridge.py
index 64f8890..c9cbd0b 100644
--- a/examples/plot_forest_ridge.py
+++ b/examples/plot_forest_ridge.py
@@ -8,12 +8,13 @@ import arviz as az
 
 az.style.use('arviz-darkgrid')
 
-non_centered_data = az.load_arviz_data('non_centered_eight')
-fig, axes = az.plot_forest(non_centered_data,
+rugby_data = az.load_arviz_data('rugby')
+fig, axes = az.plot_forest(rugby_data,
                            kind='ridgeplot',
-                           var_names=['theta'],
+                           var_names=['defs'],
+                           linewidth=4,
                            combined=True,
-                           ridgeplot_overlap=3,
-                           colors='white',
-                           figsize=(9, 7))
-axes[0].set_title('Estimated theta for 8 schools model')
+                           ridgeplot_overlap=1.5,
+                           colors='blue',
+                           figsize=(9, 4))
+axes[0].set_title('Relative defensive strength\nof Six Nation rugby teams')
diff --git a/examples/plot_loo_pit_ecdf.py b/examples/plot_loo_pit_ecdf.py
index 8b51e4d..94fae97 100644
--- a/examples/plot_loo_pit_ecdf.py
+++ b/examples/plot_loo_pit_ecdf.py
@@ -1,6 +1,6 @@
 """
 LOO-PIT ECDF Plot
-=========
+=================
 
 _thumb: .5, .7
 """
diff --git a/examples/plot_loo_pit_overlay.py b/examples/plot_loo_pit_overlay.py
index e8b27f9..28e2c61 100644
--- a/examples/plot_loo_pit_overlay.py
+++ b/examples/plot_loo_pit_overlay.py
@@ -1,6 +1,6 @@
 """
 LOO-PIT Overlay Plot
-=========
+====================
 
 _thumb: .5, .7
 """


Test patch:
diff --git a/arviz/tests/test_data.py b/arviz/tests/test_data.py
index c3a57f7..a883d5b 100644
--- a/arviz/tests/test_data.py
+++ b/arviz/tests/test_data.py
@@ -318,6 +318,45 @@ def test_sel_method(inplace):
             assert np.all(dataset.draw.values == np.arange(200, ndraws))
 
 
+@pytest.mark.parametrize("use", ("del", "delattr"))
+def test_del_method(use):
+    # create inference data object
+    data = np.random.normal(size=(4, 500, 8))
+    idata = from_dict(
+        posterior={"a": data[..., 0], "b": data},
+        sample_stats={"a": data[..., 0], "b": data},
+        observed_data={"b": data[0, 0, :]},
+        posterior_predictive={"a": data[..., 0], "b": data},
+    )
+
+    # assert inference data object has all attributes
+    test_dict = {
+        "posterior": ("a", "b"),
+        "sample_stats": ("a", "b"),
+        "observed_data": ["b"],
+        "posterior_predictive": ("a", "b"),
+    }
+    fails = check_multiple_attrs(test_dict, idata)
+    assert not fails
+    # assert _groups attribute contains all groups
+    groups = getattr(idata, "_groups")
+    assert all([group in groups for group in test_dict])
+
+    # Use del method
+    if use == "del":
+        del idata.sample_stats
+    else:
+        delattr(idata, "sample_stats")
+
+    # assert attribute has been removed
+    test_dict.pop("sample_stats")
+    fails = check_multiple_attrs(test_dict, idata)
+    assert not fails
+    assert not hasattr(idata, "sample_stats")
+    # assert _groups attribute has been updated
+    assert "sample_stats" not in getattr(idata, "_groups")
+
+
 class TestNumpyToDataArray:
     def test_1d_dataset(self):
         size = 100
@@ -628,37 +667,79 @@ class TestDataNetCDF:
         )
 
     def test_io_function(self, data, eight_schools_params):
+        # create inference data and assert all attributes are present
         inference_data = self.get_inference_data(  # pylint: disable=W0612
             data, eight_schools_params
         )
-        assert hasattr(inference_data, "posterior")
+        test_dict = {
+            "posterior": ["eta", "theta", "mu", "tau"],
+            "posterior_predictive": ["eta", "theta", "mu", "tau"],
+            "sample_stats": ["eta", "theta", "mu", "tau"],
+            "prior": ["eta", "theta", "mu", "tau"],
+            "prior_predictive": ["eta", "theta", "mu", "tau"],
+            "sample_stats_prior": ["eta", "theta", "mu", "tau"],
+            "observed_data": ["J", "y", "sigma"],
+        }
+        fails = check_multiple_attrs(test_dict, inference_data)
+        assert not fails
+
+        # check filename does not exist and save InferenceData
         here = os.path.dirname(os.path.abspath(__file__))
         data_directory = os.path.join(here, "saved_models")
         filepath = os.path.join(data_directory, "io_function_testfile.nc")
         # az -function
         to_netcdf(inference_data, filepath)
+
+        # Assert InferenceData has been saved correctly
         assert os.path.exists(filepath)
         assert os.path.getsize(filepath) > 0
         inference_data2 = from_netcdf(filepath)
-        assert hasattr(inference_data2, "posterior")
+        fails = check_multiple_attrs(test_dict, inference_data2)
+        assert not fails
         os.remove(filepath)
         assert not os.path.exists(filepath)
 
-    def test_io_method(self, data, eight_schools_params):
+    @pytest.mark.parametrize("groups_arg", [False, True])
+    def test_io_method(self, data, eight_schools_params, groups_arg):
+        # create InferenceData and check it has been properly created
         inference_data = self.get_inference_data(  # pylint: disable=W0612
             data, eight_schools_params
         )
-        assert hasattr(inference_data, "posterior")
+        test_dict = {
+            "posterior": ["eta", "theta", "mu", "tau"],
+            "posterior_predictive": ["eta", "theta", "mu", "tau"],
+            "sample_stats": ["eta", "theta", "mu", "tau"],
+            "prior": ["eta", "theta", "mu", "tau"],
+            "prior_predictive": ["eta", "theta", "mu", "tau"],
+            "sample_stats_prior": ["eta", "theta", "mu", "tau"],
+            "observed_data": ["J", "y", "sigma"],
+        }
+        fails = check_multiple_attrs(test_dict, inference_data)
+        assert not fails
+
+        # check filename does not exist and use to_netcdf method
         here = os.path.dirname(os.path.abspath(__file__))
         data_directory = os.path.join(here, "saved_models")
         filepath = os.path.join(data_directory, "io_method_testfile.nc")
         assert not os.path.exists(filepath)
         # InferenceData method
-        inference_data.to_netcdf(filepath)
+        inference_data.to_netcdf(
+            filepath, groups=("posterior", "observed_data") if groups_arg else None
+        )
+
+        # assert file has been saved correctly
         assert os.path.exists(filepath)
         assert os.path.getsize(filepath) > 0
         inference_data2 = InferenceData.from_netcdf(filepath)
-        assert hasattr(inference_data2, "posterior")
+        if groups_arg:  # if groups arg, update test dict to contain only saved groups
+            test_dict = {
+                "posterior": ["eta", "theta", "mu", "tau"],
+                "observed_data": ["J", "y", "sigma"],
+            }
+            assert not hasattr(inference_data2, "sample_stats")
+        fails = check_multiple_attrs(test_dict, inference_data2)
+        assert not fails
+
         os.remove(filepath)
         assert not os.path.exists(filepath)