RGI-TOPO for RGI 7.0#

OGGM was used to generate the topography data used to compute the topographical attributes and the centerlines products for RGI v7.0.

Here we show how to access this data from OGGM.

Input parameters#

This notebook can be run as a script with parameters using papermill, but it is not necessary. The following cell contains the parameters you can choose from:

# The RGI Id of the glaciers you want to look for
# Use the original shapefiles or the GLIMS viewer to check for the ID: https://www.glims.org/maps/glims
rgi_id = 'RGI2000-v7.0-G-01-06486'  # Denali

# The default is to test for all sources available for this glacier
# Set to a list of source names to override this
sources = None
# Where to write the plots. Default is in the current working directory
plot_dir = f'outputs/{rgi_id}'
# The RGI version to use
# V62 is an unofficial modification of V6 with only minor, backwards compatible modifications
prepro_rgi_version = 62
# Size of the map around the glacier. Currently only 10 and 40 are available
prepro_border = 10
# Degree of processing level.  Currently only 1 is available.
from_prepro_level = 1

Check input and set up#

# The sources can be given as parameters
if sources is not None and isinstance(sources, str):
    sources = sources.split(',')
# Plotting directory as well
if not plot_dir:
    plot_dir = './' + rgi_id
import os
plot_dir = os.path.abspath(plot_dir)
from oggm import cfg, utils, workflow, tasks, graphics, GlacierDirectory
import pandas as pd
import numpy as np
import xarray as xr
import rioxarray as rioxr
import geopandas as gpd
import salem
import matplotlib.pyplot as plt
from mpl_toolkits.axes_grid1 import AxesGrid
import itertools

from oggm.utils import DEM_SOURCES
from oggm.workflow import init_glacier_directories
# Make sure the plot directory exists
utils.mkdir(plot_dir);
# Use OGGM to download the data
cfg.initialize()
cfg.PATHS['working_dir'] = utils.gettempdir(dirname='OGGM-RGITOPO-RGI7', reset=True)
cfg.PARAMS['use_intersects'] = False
2024-02-02 17:21:43: oggm.cfg: Reading default parameters from the OGGM `params.cfg` configuration file.
2024-02-02 17:21:43: oggm.cfg: Multiprocessing switched OFF according to the parameter file.
2024-02-02 17:21:43: oggm.cfg: Multiprocessing: using all available processors (N=4)
2024-02-02 17:21:43: oggm.cfg: PARAMS['use_intersects'] changed from `True` to `False`.

Download the data using OGGM utility functions#

Note that you could reach the same goal by downloading the data manually from

# URL of the preprocessed GDirs
gdir_url = 'https://cluster.klima.uni-bremen.de/~oggm/gdirs/oggm_v1.6/rgitopo/2023.1/'
# We use OGGM to download the data
gdir = init_glacier_directories([rgi_id], from_prepro_level=1, prepro_border=10,  prepro_rgi_version='70', prepro_base_url=gdir_url)[0]
2024-02-02 17:21:44: oggm.workflow: init_glacier_directories from prepro level 1 on 1 glaciers.
2024-02-02 17:21:44: oggm.workflow: Execute entity tasks [gdir_from_prepro] on 1 glaciers
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
Cell In[6], line 4
      2 gdir_url = 'https://cluster.klima.uni-bremen.de/~oggm/gdirs/oggm_v1.6/rgitopo/2023.1/'
      3 # We use OGGM to download the data
----> 4 gdir = init_glacier_directories([rgi_id], from_prepro_level=1, prepro_border=10,  prepro_rgi_version='70', prepro_base_url=gdir_url)[0]

File /usr/local/pyenv/versions/3.11.7/lib/python3.11/site-packages/oggm/workflow.py:426, in init_glacier_directories(rgidf, reset, force, from_prepro_level, prepro_border, prepro_rgi_version, prepro_base_url, from_tar, delete_tar)
    424     if cfg.PARAMS['dl_verify']:
    425         utils.get_dl_verify_data('cluster.klima.uni-bremen.de')
--> 426     gdirs = execute_entity_task(gdir_from_prepro, entities,
    427                                 from_prepro_level=from_prepro_level,
    428                                 prepro_border=prepro_border,
    429                                 prepro_rgi_version=prepro_rgi_version,
    430                                 base_url=prepro_base_url)
    431 else:
    432     # We can set the intersects file automatically here
    433     if (cfg.PARAMS['use_intersects'] and
    434             len(cfg.PARAMS['intersects_gdf']) == 0 and
    435             not from_tar):

File /usr/local/pyenv/versions/3.11.7/lib/python3.11/site-packages/oggm/workflow.py:191, in execute_entity_task(task, gdirs, **kwargs)
    187     if ng > 3:
    188         log.workflow('WARNING: you are trying to run an entity task on '
    189                      '%d glaciers with multiprocessing turned off. OGGM '
    190                      'will run faster with multiprocessing turned on.', ng)
--> 191     out = [pc(gdir) for gdir in gdirs]
    193 return out

File /usr/local/pyenv/versions/3.11.7/lib/python3.11/site-packages/oggm/workflow.py:191, in <listcomp>(.0)
    187     if ng > 3:
    188         log.workflow('WARNING: you are trying to run an entity task on '
    189                      '%d glaciers with multiprocessing turned off. OGGM '
    190                      'will run faster with multiprocessing turned on.', ng)
--> 191     out = [pc(gdir) for gdir in gdirs]
    193 return out

File /usr/local/pyenv/versions/3.11.7/lib/python3.11/site-packages/oggm/workflow.py:108, in _pickle_copier.__call__(self, arg)
    106 for func in self.call_func:
    107     func, kwargs = func
--> 108     res = self._call_internal(func, arg, kwargs)
    109 return res

File /usr/local/pyenv/versions/3.11.7/lib/python3.11/site-packages/oggm/workflow.py:102, in _pickle_copier._call_internal(self, call_func, gdir, kwargs)
     99     gdir, gdir_kwargs = gdir
    100     kwargs.update(gdir_kwargs)
--> 102 return call_func(gdir, **kwargs)

File /usr/local/pyenv/versions/3.11.7/lib/python3.11/site-packages/oggm/workflow.py:255, in gdir_from_prepro(entity, from_prepro_level, prepro_border, prepro_rgi_version, base_url)
    252 tar_base = utils.get_prepro_gdir(prepro_rgi_version, rid, prepro_border,
    253                                  from_prepro_level, base_url=base_url)
    254 from_tar = os.path.join(tar_base.replace('.tar', ''), rid + '.tar.gz')
--> 255 return oggm.GlacierDirectory(entity, from_tar=from_tar)

File /usr/local/pyenv/versions/3.11.7/lib/python3.11/site-packages/oggm/utils/_workflow.py:2720, in GlacierDirectory.__init__(self, rgi_entity, base_dir, reset, from_tar, delete_tar)
   2713 self.status = 'Glacier'
   2714 ttkeys = {0: 'Land-terminating',
   2715           1: 'Marine-terminating',
   2716           2: 'Lake-terminating',
   2717           3: 'Shelf-terminating',
   2718           9: 'Not assigned',
   2719           }
-> 2720 self.terminus_type = ttkeys[rgi_entity['term_type']]
   2721 if is_glacier_complex:
   2722     self.rgi_version = '70C'

KeyError: '9'
gdir
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[7], line 1
----> 1 gdir

NameError: name 'gdir' is not defined

Read the DEMs and store them all in a dataset#

if sources is None:
    sources = [src for src in os.listdir(gdir.dir) if src in utils.DEM_SOURCES]
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[8], line 2
      1 if sources is None:
----> 2     sources = [src for src in os.listdir(gdir.dir) if src in utils.DEM_SOURCES]

NameError: name 'gdir' is not defined
print('RGI ID:', rgi_id)
print('Available DEM sources:', sources)
print('Plotting directory:', plot_dir)
RGI ID: RGI2000-v7.0-G-01-06486
Available DEM sources: None
Plotting directory: /__w/tutorials/tutorials/notebooks/tutorials/outputs/RGI2000-v7.0-G-01-06486
# We use xarray to store the data
ods = xr.Dataset()
for src in sources:
    demfile = os.path.join(gdir.dir, src) + '/dem.tif'
    with rioxr.open_rasterio(demfile) as ds:
        data = ds.sel(band=1).load() * 1.
        ods[src] = data.where(data > -100, np.NaN)
    
    sy, sx = np.gradient(ods[src], gdir.grid.dx, gdir.grid.dx)
    ods[src + '_slope'] = ('y', 'x'),  np.arctan(np.sqrt(sy**2 + sx**2))

with rioxr.open_rasterio(gdir.get_filepath('glacier_mask')) as ds:
    ods['mask'] = ds.sel(band=1).load()
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[10], line 3
      1 # We use xarray to store the data
      2 ods = xr.Dataset()
----> 3 for src in sources:
      4     demfile = os.path.join(gdir.dir, src) + '/dem.tif'
      5     with rioxr.open_rasterio(demfile) as ds:

TypeError: 'NoneType' object is not iterable
# Decide on the number of plots and figure size
ns = len(sources)
x_size = 12
n_cols = 3
n_rows = -(-ns // n_cols)
y_size = x_size / n_cols * n_rows
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[11], line 2
      1 # Decide on the number of plots and figure size
----> 2 ns = len(sources)
      3 x_size = 12
      4 n_cols = 3

TypeError: object of type 'NoneType' has no len()

Raw topography data#

smap = salem.graphics.Map(gdir.grid, countries=False)
smap.set_shapefile(gdir.read_shapefile('outlines'))
smap.set_plot_params(cmap='topo')
smap.set_lonlat_contours(add_tick_labels=False)
smap.set_plot_params(vmin=np.nanquantile([ods[s].min() for s in sources], 0.25),
                     vmax=np.nanquantile([ods[s].max() for s in sources], 0.75))

fig = plt.figure(figsize=(x_size, y_size))
grid = AxesGrid(fig, 111,
                nrows_ncols=(n_rows, n_cols),
                axes_pad=0.7,
                cbar_mode='each',
                cbar_location='right',
                cbar_pad=0.1
                )

for i, s in enumerate(sources):
    data = ods[s]
    smap.set_data(data)
    ax = grid[i]
    smap.visualize(ax=ax, addcbar=False, title=s)
    if np.isnan(data).all():
        grid[i].cax.remove()
        continue
    cax = grid.cbar_axes[i]
    smap.colorbarbase(cax)
    
# take care of uneven grids
if ax != grid[-1] and not grid[-1].title.get_text():
    grid[-1].remove()
    grid[-1].cax.remove()
if ax != grid[-2] and not grid[-2].title.get_text():
    grid[-2].remove()
    grid[-2].cax.remove()

plt.savefig(os.path.join(plot_dir, 'dem_topo_color.png'), dpi=150, bbox_inches='tight')
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[12], line 1
----> 1 smap = salem.graphics.Map(gdir.grid, countries=False)
      2 smap.set_shapefile(gdir.read_shapefile('outlines'))
      3 smap.set_plot_params(cmap='topo')

NameError: name 'gdir' is not defined

Shaded relief#

fig = plt.figure(figsize=(x_size, y_size))
grid = AxesGrid(fig, 111,
                nrows_ncols=(n_rows, n_cols),
                axes_pad=0.7,
                cbar_location='right',
                cbar_pad=0.1
                )
smap.set_plot_params(cmap='Blues')
smap.set_shapefile()
for i, s in enumerate(sources):
    data = ods[s].copy().where(np.isfinite(ods[s]), 0)
    smap.set_data(data * 0)
    ax = grid[i]
    smap.set_topography(data)
    smap.visualize(ax=ax, addcbar=False, title=s)
    
# take care of uneven grids
if ax != grid[-1] and not grid[-1].title.get_text():
    grid[-1].remove()
    grid[-1].cax.remove()
if ax != grid[-2] and not grid[-2].title.get_text():
    grid[-2].remove()
    grid[-2].cax.remove()

plt.savefig(os.path.join(plot_dir, 'dem_topo_shade.png'), dpi=150, bbox_inches='tight')
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[13], line 1
----> 1 fig = plt.figure(figsize=(x_size, y_size))
      2 grid = AxesGrid(fig, 111,
      3                 nrows_ncols=(n_rows, n_cols),
      4                 axes_pad=0.7,
      5                 cbar_location='right',
      6                 cbar_pad=0.1
      7                 )
      8 smap.set_plot_params(cmap='Blues')

NameError: name 'x_size' is not defined

Slope#

fig = plt.figure(figsize=(x_size, y_size))
grid = AxesGrid(fig, 111,
                nrows_ncols=(n_rows, n_cols),
                axes_pad=0.7,
                cbar_mode='each',
                cbar_location='right',
                cbar_pad=0.1
                )

smap.set_topography();
smap.set_plot_params(vmin=0, vmax=0.7, cmap='Blues')

for i, s in enumerate(sources):
    data = ods[s + '_slope']
    smap.set_data(data)
    ax = grid[i]
    smap.visualize(ax=ax, addcbar=False, title=s + ' (slope)')
    cax = grid.cbar_axes[i]
    smap.colorbarbase(cax)
    
# take care of uneven grids
if ax != grid[-1] and not grid[-1].title.get_text():
    grid[-1].remove()
    grid[-1].cax.remove()
if ax != grid[-2] and not grid[-2].title.get_text():
    grid[-2].remove()
    grid[-2].cax.remove()

plt.savefig(os.path.join(plot_dir, 'dem_slope.png'), dpi=150, bbox_inches='tight')
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[14], line 1
----> 1 fig = plt.figure(figsize=(x_size, y_size))
      2 grid = AxesGrid(fig, 111,
      3                 nrows_ncols=(n_rows, n_cols),
      4                 axes_pad=0.7,
   (...)
      7                 cbar_pad=0.1
      8                 )
     10 smap.set_topography();

NameError: name 'x_size' is not defined

Some simple statistics about the DEMs#

df = pd.DataFrame()
for s in sources:
    df[s] = ods[s].data.flatten()[ods.mask.data.flatten() == 1]

dfs = pd.DataFrame()
for s in sources:
    dfs[s] = ods[s + '_slope'].data.flatten()[ods.mask.data.flatten() == 1]
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[15], line 2
      1 df = pd.DataFrame()
----> 2 for s in sources:
      3     df[s] = ods[s].data.flatten()[ods.mask.data.flatten() == 1]
      5 dfs = pd.DataFrame()

TypeError: 'NoneType' object is not iterable
dfs = df.describe()
dfs.loc['range'] = dfs.loc['max'] - dfs.loc['min']
dfs
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[16], line 1
----> 1 dfs = df.describe()
      2 dfs.loc['range'] = dfs.loc['max'] - dfs.loc['min']
      3 dfs

File /usr/local/pyenv/versions/3.11.7/lib/python3.11/site-packages/pandas/core/generic.py:11970, in NDFrame.describe(self, percentiles, include, exclude)
  11728 @final
  11729 def describe(
  11730     self,
   (...)
  11733     exclude=None,
  11734 ) -> Self:
  11735     """
  11736     Generate descriptive statistics.
  11737 
   (...)
  11968     max            NaN      3.0
  11969     """
> 11970     return describe_ndframe(
  11971         obj=self,
  11972         include=include,
  11973         exclude=exclude,
  11974         percentiles=percentiles,
  11975     ).__finalize__(self, method="describe")

File /usr/local/pyenv/versions/3.11.7/lib/python3.11/site-packages/pandas/core/methods/describe.py:91, in describe_ndframe(obj, include, exclude, percentiles)
     87     describer = SeriesDescriber(
     88         obj=cast("Series", obj),
     89     )
     90 else:
---> 91     describer = DataFrameDescriber(
     92         obj=cast("DataFrame", obj),
     93         include=include,
     94         exclude=exclude,
     95     )
     97 result = describer.describe(percentiles=percentiles)
     98 return cast(NDFrameT, result)

File /usr/local/pyenv/versions/3.11.7/lib/python3.11/site-packages/pandas/core/methods/describe.py:162, in DataFrameDescriber.__init__(self, obj, include, exclude)
    159 self.exclude = exclude
    161 if obj.ndim == 2 and obj.columns.size == 0:
--> 162     raise ValueError("Cannot describe a DataFrame without columns")
    164 super().__init__(obj)

ValueError: Cannot describe a DataFrame without columns

Comparison matrix plot#

# Table of differences between DEMS
df_diff = pd.DataFrame()
done = []
for s1, s2 in itertools.product(sources, sources):
    if s1 == s2:
        continue
    if (s2, s1) in done:
        continue
    df_diff[s1 + '-' + s2] = df[s1] - df[s2]
    done.append((s1, s2))
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[17], line 4
      2 df_diff = pd.DataFrame()
      3 done = []
----> 4 for s1, s2 in itertools.product(sources, sources):
      5     if s1 == s2:
      6         continue

TypeError: 'NoneType' object is not iterable
# Decide on plot levels
max_diff = df_diff.quantile(0.99).max()
base_levels = np.array([-8, -5, -3, -1.5, -1, -0.5, -0.2, -0.1, 0, 0.1, 0.2, 0.5, 1, 1.5, 3, 5, 8])
if max_diff < 10:
    levels = base_levels
elif max_diff < 100:
    levels = base_levels * 10
elif max_diff < 1000:
    levels = base_levels * 100
else:
    levels = base_levels * 1000
levels = [l for l in levels if abs(l) < max_diff]
if max_diff > 10:
    levels = [int(l) for l in levels]
levels
[]
smap.set_plot_params(levels=levels, cmap='PuOr', extend='both')
smap.set_shapefile(gdir.read_shapefile('outlines'))

fig = plt.figure(figsize=(14, 14))
grid = AxesGrid(fig, 111,
                nrows_ncols=(ns - 1, ns - 1),
                axes_pad=0.3,
                cbar_mode='single',
                cbar_location='right',
                cbar_pad=0.1
                )
done = []
for ax in grid:
    ax.set_axis_off()
for s1, s2 in itertools.product(sources, sources):
    if s1 == s2:
        continue
    if (s2, s1) in done:
        continue
    data = ods[s1] - ods[s2]
    ax = grid[sources.index(s1) * (ns - 1) + sources[1:].index(s2)]
    ax.set_axis_on()
    smap.set_data(data)
    smap.visualize(ax=ax, addcbar=False)
    done.append((s1, s2))
    ax.set_title(s1 + '-' + s2, fontsize=8)
    
cax = grid.cbar_axes[0]
smap.colorbarbase(cax);

plt.savefig(os.path.join(plot_dir, 'dem_diffs.png'), dpi=150, bbox_inches='tight')
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[19], line 1
----> 1 smap.set_plot_params(levels=levels, cmap='PuOr', extend='both')
      2 smap.set_shapefile(gdir.read_shapefile('outlines'))
      4 fig = plt.figure(figsize=(14, 14))

NameError: name 'smap' is not defined

Comparison scatter plot#

import seaborn as sns
sns.set(style="ticks")

l1, l2 = (utils.nicenumber(df.min().min(), binsize=50, lower=True), 
          utils.nicenumber(df.max().max(), binsize=50, lower=False))

def plot_unity(xdata, ydata, **kwargs):
    points = np.linspace(l1, l2, 100)
    plt.gca().plot(points, points, color='k', marker=None,
                   linestyle=':', linewidth=3.0)

g = sns.pairplot(df.dropna(how='all', axis=1).dropna(), plot_kws=dict(s=50, edgecolor="C0", linewidth=1));
g.map_offdiag(plot_unity)
for asx in g.axes:
    for ax in asx:
        ax.set_xlim((l1, l2))
        ax.set_ylim((l1, l2))

plt.savefig(os.path.join(plot_dir, 'dem_scatter.png'), dpi=150, bbox_inches='tight')
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[20], line 12
      8     points = np.linspace(l1, l2, 100)
      9     plt.gca().plot(points, points, color='k', marker=None,
     10                    linestyle=':', linewidth=3.0)
---> 12 g = sns.pairplot(df.dropna(how='all', axis=1).dropna(), plot_kws=dict(s=50, edgecolor="C0", linewidth=1));
     13 g.map_offdiag(plot_unity)
     14 for asx in g.axes:

File /usr/local/pyenv/versions/3.11.7/lib/python3.11/site-packages/seaborn/axisgrid.py:2119, in pairplot(data, hue, hue_order, palette, vars, x_vars, y_vars, kind, diag_kind, markers, height, aspect, corner, dropna, plot_kws, diag_kws, grid_kws, size)
   2117 # Set up the PairGrid
   2118 grid_kws.setdefault("diag_sharey", diag_kind == "hist")
-> 2119 grid = PairGrid(data, vars=vars, x_vars=x_vars, y_vars=y_vars, hue=hue,
   2120                 hue_order=hue_order, palette=palette, corner=corner,
   2121                 height=height, aspect=aspect, dropna=dropna, **grid_kws)
   2123 # Add the markers here as PairGrid has figured out how many levels of the
   2124 # hue variable are needed and we don't want to duplicate that process
   2125 if markers is not None:

File /usr/local/pyenv/versions/3.11.7/lib/python3.11/site-packages/seaborn/axisgrid.py:1272, in PairGrid.__init__(self, data, hue, vars, x_vars, y_vars, hue_order, palette, hue_kws, corner, diag_sharey, height, aspect, layout_pad, despine, dropna)
   1269 self.square_grid = self.x_vars == self.y_vars
   1271 if not x_vars:
-> 1272     raise ValueError("No variables found for grid columns.")
   1273 if not y_vars:
   1274     raise ValueError("No variables found for grid rows.")

ValueError: No variables found for grid columns.

Table statistics#

df.describe()
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[21], line 1
----> 1 df.describe()

File /usr/local/pyenv/versions/3.11.7/lib/python3.11/site-packages/pandas/core/generic.py:11970, in NDFrame.describe(self, percentiles, include, exclude)
  11728 @final
  11729 def describe(
  11730     self,
   (...)
  11733     exclude=None,
  11734 ) -> Self:
  11735     """
  11736     Generate descriptive statistics.
  11737 
   (...)
  11968     max            NaN      3.0
  11969     """
> 11970     return describe_ndframe(
  11971         obj=self,
  11972         include=include,
  11973         exclude=exclude,
  11974         percentiles=percentiles,
  11975     ).__finalize__(self, method="describe")

File /usr/local/pyenv/versions/3.11.7/lib/python3.11/site-packages/pandas/core/methods/describe.py:91, in describe_ndframe(obj, include, exclude, percentiles)
     87     describer = SeriesDescriber(
     88         obj=cast("Series", obj),
     89     )
     90 else:
---> 91     describer = DataFrameDescriber(
     92         obj=cast("DataFrame", obj),
     93         include=include,
     94         exclude=exclude,
     95     )
     97 result = describer.describe(percentiles=percentiles)
     98 return cast(NDFrameT, result)

File /usr/local/pyenv/versions/3.11.7/lib/python3.11/site-packages/pandas/core/methods/describe.py:162, in DataFrameDescriber.__init__(self, obj, include, exclude)
    159 self.exclude = exclude
    161 if obj.ndim == 2 and obj.columns.size == 0:
--> 162     raise ValueError("Cannot describe a DataFrame without columns")
    164 super().__init__(obj)

ValueError: Cannot describe a DataFrame without columns
df.corr()
df_diff.describe()
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[23], line 1
----> 1 df_diff.describe()

File /usr/local/pyenv/versions/3.11.7/lib/python3.11/site-packages/pandas/core/generic.py:11970, in NDFrame.describe(self, percentiles, include, exclude)
  11728 @final
  11729 def describe(
  11730     self,
   (...)
  11733     exclude=None,
  11734 ) -> Self:
  11735     """
  11736     Generate descriptive statistics.
  11737 
   (...)
  11968     max            NaN      3.0
  11969     """
> 11970     return describe_ndframe(
  11971         obj=self,
  11972         include=include,
  11973         exclude=exclude,
  11974         percentiles=percentiles,
  11975     ).__finalize__(self, method="describe")

File /usr/local/pyenv/versions/3.11.7/lib/python3.11/site-packages/pandas/core/methods/describe.py:91, in describe_ndframe(obj, include, exclude, percentiles)
     87     describer = SeriesDescriber(
     88         obj=cast("Series", obj),
     89     )
     90 else:
---> 91     describer = DataFrameDescriber(
     92         obj=cast("DataFrame", obj),
     93         include=include,
     94         exclude=exclude,
     95     )
     97 result = describer.describe(percentiles=percentiles)
     98 return cast(NDFrameT, result)

File /usr/local/pyenv/versions/3.11.7/lib/python3.11/site-packages/pandas/core/methods/describe.py:162, in DataFrameDescriber.__init__(self, obj, include, exclude)
    159 self.exclude = exclude
    161 if obj.ndim == 2 and obj.columns.size == 0:
--> 162     raise ValueError("Cannot describe a DataFrame without columns")
    164 super().__init__(obj)

ValueError: Cannot describe a DataFrame without columns
df_diff.abs().describe()
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[24], line 1
----> 1 df_diff.abs().describe()

File /usr/local/pyenv/versions/3.11.7/lib/python3.11/site-packages/pandas/core/generic.py:11970, in NDFrame.describe(self, percentiles, include, exclude)
  11728 @final
  11729 def describe(
  11730     self,
   (...)
  11733     exclude=None,
  11734 ) -> Self:
  11735     """
  11736     Generate descriptive statistics.
  11737 
   (...)
  11968     max            NaN      3.0
  11969     """
> 11970     return describe_ndframe(
  11971         obj=self,
  11972         include=include,
  11973         exclude=exclude,
  11974         percentiles=percentiles,
  11975     ).__finalize__(self, method="describe")

File /usr/local/pyenv/versions/3.11.7/lib/python3.11/site-packages/pandas/core/methods/describe.py:91, in describe_ndframe(obj, include, exclude, percentiles)
     87     describer = SeriesDescriber(
     88         obj=cast("Series", obj),
     89     )
     90 else:
---> 91     describer = DataFrameDescriber(
     92         obj=cast("DataFrame", obj),
     93         include=include,
     94         exclude=exclude,
     95     )
     97 result = describer.describe(percentiles=percentiles)
     98 return cast(NDFrameT, result)

File /usr/local/pyenv/versions/3.11.7/lib/python3.11/site-packages/pandas/core/methods/describe.py:162, in DataFrameDescriber.__init__(self, obj, include, exclude)
    159 self.exclude = exclude
    161 if obj.ndim == 2 and obj.columns.size == 0:
--> 162     raise ValueError("Cannot describe a DataFrame without columns")
    164 super().__init__(obj)

ValueError: Cannot describe a DataFrame without columns

What’s next?#