Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Reading, inspecting, and visualizing multidimensional datasets

Open In Colab

In the previous chapter, you learned that a data cube is a multidimensional matrix paired with explicit spatial and temporal labels. Understanding this structure conceptually is the first step. The next crucial skill is learning how to practically interact with these objects in Python.

Before you begin applying map algebra or calculating time series trends, you must know exactly what your data contains. This chapter provides a standard, reliable workflow for ingesting, inspecting, formatting, processing geospatial metadata, and exporting any multidimensional dataset using xarray.


1. Opening the cube

The function you use to open a data cube depends entirely on the storage format of the file on your hard drive or in the cloud. xarray acts as a universal reader, but you must choose the right tool for your specific file.

  • xr.open_dataset(): This is the standard function for reading native multidimensional formats like NetCDF (.nc) or HDF5 (.h5). It reads the file and returns a Dataset container, which can hold multiple aligned variables (like temperature and precipitation).

ds = xr.open_dataset("climate_data.nc")
  • xr.open_dataarray(): Use this when you are reading a NetCDF file that you know contains only one single variable. It skips the Dataset container and directly returns the DataArray.

da = xr.open_dataarray("single_variable.nc")
  • xr.open_zarr(): This is the dedicated function for reading cloud optimized Zarr stores (.zarr), allowing you to stream chunked data directly into memory.

ds_zarr = xr.open_zarr("cloud_data.zarr")
  • The GeoTIFF bridge: If you are reading a standard GIS raster (.tif), standard xarray will struggle to understand its spatial projection. By installing the rioxarray extension, you unlock a new reading engine. You can then load GeoTIFFs while perfectly preserving their Coordinate Reference System (CRS).

# !pip install rioxarray
import rioxarray

# Method 1: Using the xarray engine
da_tif = xr.open_dataarray("satellite_image.tif", engine="rasterio")

# Method 2: Using the rioxarray wrapper
da_tif = rioxarray.open_rasterio("satellite_image.tif")

For this chapter, we will use a built-in tutorial dataset to practice reading and inspecting these structures.

import xarray as xr

# Load a standard NetCDF tutorial dataset
ds = xr.tutorial.open_dataset("air_temperature")

2. Reading the xarray display

When you evaluate an xarray object in a Jupyter Notebook, you are presented with an interactive HTML summary. Learning to read this summary is an important debugging skill you can develop for multidimensional analysis.

ds
Loading...

If you run the cell above, you will see a structured output. Notice the interactive icons: you can click the arrows (▶) to expand sections, and the document icons (📄 or ≡) to peek at the actual data values and metadata without printing massive arrays to your screen.

  • Dimensions: This shows the shape of your cube. You will see lat: 25, lon: 53, and time: 2920. This instantly tells you the matrix has 25 rows, 53 columns, and 2920 time steps. Because dimensions are named, their physical order in the array does not matter.

  • Coordinates: These are the labels for the dimensions. Notice that the names are printed in bold font. This indicates they are “dimension coordinates” backed by an index, allowing for lightning fast data selection.

  • Data variables: This lists the actual physical measurements stored inside the container. Here, there is only one variable called air. It also shows the specific dimensions that variable relies on (time, lat, lon) and its data type (e.g., float64).

  • Indexes: This section confirms which coordinates have an underlying search index built in. In most cases, these are backed by a standard Pandas index.

  • Attributes: This section holds the global metadata. Expanding this reveals the dataset history, conventions, and units, providing the necessary context to trust the data you are analyzing.

Extracting a single variable

The display above shows the entire Dataset container. To actually analyze the temperature data, you need to extract the specific DataArray. xarray allows you to do this using standard dictionary syntax or a convenient dot notation shortcut.

# Extracting using dictionary syntax (safest)
da = ds["air"]

# Extracting using dot notation (fastest for typing)
da = ds.air
da
Loading...

If you print this new da object, you will see a very similar HTML display, but it now represents just the single variable rather than the whole container.

Interactive Explorer: Dataset Anatomy Visualizer.
Hover your mouse over the different rows in the simulated Jupyter HTML display to instantly reveal how the text-based summary maps directly to the physical axes, labels, data volume, and metadata of a 3D Data Cube. For improved visibility of the explorer, follow this link.


Concept Check: The Container vs. The Variable

Scenario: You have a NetCDF file containing daily measurements for both temperature and precipitation. You load it using data = xr.open_dataset("weather.nc"). If you want to check the specific units (e.g., Celsius vs. Kelvin) for the temperature data, where should you look?

A) In the global data.attrs dictionary.

B) In the data.coords list.

C) In the data["temperature"].attrs dictionary.


3. Renaming and formatting

Scientific datasets can come with obscure variable names, missing metadata, or poorly formatted coordinates. Before you start writing complex analysis code, it is highly recommended to clean your dataset. This makes your code self documenting and prevents errors later on.

Renaming variables and dimensions If a temperature variable is awkwardly named t2m or simply air, you can rename it to something descriptive. You use the .rename() method, passing a dictionary where the key is the old name and the value is the new name. This exact same approach works for renaming dimensions.

# Rename the variable 'air' to 'temperature'
ds_clean = ds.rename({"air": "temperature"})

# Verify the change
print(list(ds_clean.data_vars))
['temperature']

Modifying and assigning coordinates Sometimes, a dataset will load with plain integer indices instead of real world coordinates, or the coordinates might be in the wrong format (like strings instead of proper datetime objects). You can overwrite or assign entirely new coordinate arrays using dictionary syntax.

import pandas as pd

# Example: Overwriting the time coordinate with a newly generated Pandas date range
ds_clean.coords["time"] = pd.date_range("2013-01-01", "2014-12-31 18:00", freq="6h")

Updating attributes and metadata Attributes are dictionaries that hold arbitrary metadata. You might want to add missing information, like the physical units of your new temperature variable, or add a descriptive note to the entire dataset container.

# Add metadata to a specific variable
ds_clean["temperature"].attrs["units"] = "Kelvin"

# Add global metadata to the entire dataset container
ds_clean.attrs["project_author"] = "Spatial Data Science Lab"

# Check the new variable attributes
print(ds_clean["temperature"].attrs)
{'long_name': '4xDaily Air temperature at sigma level 995', 'units': 'Kelvin', 'precision': np.int16(2), 'GRIB_id': np.int16(11), 'GRIB_name': 'TMP', 'var_desc': 'Air temperature', 'dataset': 'NMC Reanalysis', 'level_desc': 'Surface', 'statistic': 'Individual Obs', 'parent_stat': 'Other', 'actual_range': array([185.16, 322.1 ], dtype=float32)}

By taking a few lines of code to rename variables, fix coordinates, and assign units, you transform a confusing raw file into a clean, professional data structure ready for analysis.


4. First checks for a cube

While the interactive HTML display is fantastic for visual inspection, you will often need to write automated scripts. Every time you load a new dataset, you should run a mental checklist of its properties using the following programmatic commands.

Standard array properties:

# Check the dimensions and their specific lengths
print(ds_clean.sizes)

# Check the available variables
print(list(ds_clean.data_vars))

# Check the global dataset attributes (e.g., to find the author or conventions)
print(ds_clean.attrs)
Frozen({'time': 2920, 'lat': 25, 'lon': 53})
['temperature']
{'Conventions': 'COARDS', 'title': '4x daily NMC reanalysis (1948)', 'description': 'Data is from NMC initialized reanalysis\n(4x/day).  These are the 0.9950 sigma level values.', 'platform': 'Model', 'references': 'http://www.esrl.noaa.gov/psd/data/gridded/data.ncep.reanalysis.html', 'project_author': 'Spatial Data Science Lab'}

Memory and safety checks: Data cubes can easily consume all your computer’s RAM. Before running heavy calculations, you should always check the size of your object.

# Calculate total memory size in Megabytes
memory_mb = ds_clean.nbytes / (1024**2)
print(f"Total size: {memory_mb:.2f} MB")
Total size: 29.54 MB

5. Geospatial operations

xarray is easily extensible. Third party packages can plug directly into xarray using a feature called accessors. Accessors allow you to use familiar dot notation (like .plot() or .dt()) to access entirely new toolkits.

The rioxarray package provides the .rio accessor. It acts as a bridge between the multidimensional power of xarray and the foundational geospatial algorithms of the rasterio and GDAL libraries.

Loading geospatial metadata

Let us load a new dataset to explore these features: a Copernicus Digital Elevation Model (DEM) subset over New Zealand. You can download the DEM here. We will use the rasterio engine to open the GeoTIFF.

import xarray as xr
import rioxarray

filepath = "data/Copernicus_DEM_NZ_subset.tif"
da_dem = xr.open_dataarray(filepath, engine="rasterio")

When rioxarray opens a file, it automatically parses the spatial metadata embedded in the GeoTIFF. You unlock a powerful suite of GIS metadata checks using the .rio accessor.

# Check the Coordinate Reference System (CRS)
print(da_dem.rio.crs)

# Check the spatial resolution (pixel width and height)
print(da_dem.rio.resolution())

# Check the geographic bounding box (xmin, ymin, xmax, ymax)
print(da_dem.rio.bounds())

# Check the designated 'nodata' value used for missing pixels
print(da_dem.rio.nodata)
EPSG:32759
(30.0, -30.0)
(399660.0, 5147640.0, 464760.0, 5208780.0)
None

The output for the CRS typically reveals an EPSG code (European Petroleum Survey Group). Understanding your resolution and bounding box is critical before merging this DEM with other datasets, as they must align perfectly in space.

Reprojection

If you want to map this data alongside standard web maps or datasets in a different coordinate system, you must reproject the dataset into a new CRS. For example, converting it to Web Mercator (EPSG:3857).

# Reproject to Web Mercator
da_reprojected = da_dem.rio.reproject("EPSG:3857")

print(da_reprojected.rio.crs)
EPSG:3857

If you print the sizes of da_dem and da_reprojected, you will notice that the dimensions have changed. This is an important rule of spatial data science: reprojection resamples your data. The original grid must be warped and interpolated to fit the newly defined spatial grid.

Handling NoData values

Elevation models and satellite imagery frequently contain missing pixels around the edges of the swath or over the ocean. These are recorded using a designated NoData value. You can assign and clean these values so they do not skew your mathematical calculations or color ramps later.

# Explicitly assign a NoData value to the dataset memory
da_clean = da_reprojected.rio.set_nodata(-9999)

# Mask the NoData values (writes them safely to the object attributes)
da_clean = da_clean.rio.write_nodata(-9999, inplace=True)

6. Exporting data

Once you have renamed your variables, reprojected your coordinates, and cleaned your NoData values, you will likely want to save the processed data back to disk. Xarray makes writing data just as easy as reading it.

Exporting to multidimensional formats

The recommended way to store standard xarray data structures (like the climate datasets from earlier in the chapter) is NetCDF. This format guarantees that all your variables, coordinates, and metadata attributes are perfectly preserved in a single file.

If you are working with massive datasets or cloud environments, you should export to Zarr instead. Zarr implements chunked, compressed arrays that stream beautifully from services like Amazon S3 or Google Cloud Storage.

# Save a cleaned Dataset container to a local NetCDF file
ds_clean.to_netcdf("processed_climate_data.nc")

# Save to a local Zarr store (directory)
ds_clean.to_zarr("processed_climate_data.zarr", mode="w")
/Users/wulf/miniconda3/envs/sds210/lib/python3.12/site-packages/IPython/core/interactiveshell.py:3701: SerializationWarning: saving variable temperature with floating point data as an integer dtype without any _FillValue to use for NaNs
  exec(code_obj, self.user_global_ns, self.user_ns)
---------------------------------------------------------------------------
ModuleNotFoundError                       Traceback (most recent call last)
File ~/miniconda3/envs/sds210/lib/python3.12/site-packages/xarray/core/utils.py:1379, in attempt_import(module)
   1378 try:
-> 1379     return importlib.import_module(module)
   1380 except ImportError as e:

File ~/miniconda3/envs/sds210/lib/python3.12/importlib/__init__.py:90, in import_module(name, package)
     89         level += 1
---> 90 return _bootstrap._gcd_import(name[level:], package, level)

File <frozen importlib._bootstrap>:1387, in _gcd_import(name, package, level)

File <frozen importlib._bootstrap>:1360, in _find_and_load(name, import_)

File <frozen importlib._bootstrap>:1324, in _find_and_load_unlocked(name, import_)

ModuleNotFoundError: No module named 'zarr'

The above exception was the direct cause of the following exception:

ImportError                               Traceback (most recent call last)
Cell In[13], line 5
      2 ds_clean.to_netcdf("processed_climate_data.nc")
      4 # Save to a local Zarr store (directory)
----> 5 ds_clean.to_zarr("processed_climate_data.zarr", mode="w")

File ~/miniconda3/envs/sds210/lib/python3.12/site-packages/xarray/core/dataset.py:2377, in Dataset.to_zarr(self, store, chunk_store, mode, synchronizer, group, encoding, compute, consolidated, append_dim, region, safe_chunks, align_chunks, storage_options, zarr_version, zarr_format, write_empty_chunks, chunkmanager_store_kwargs)
   2194 """Write dataset contents to a zarr group.
   2195 
   2196 Zarr chunks are determined in the following way:
   (...)   2373     The I/O user guide, with more details and examples.
   2374 """
   2375 from xarray.backends.writers import to_zarr
-> 2377 return to_zarr(  # type: ignore[call-overload,misc]
   2378     self,
   2379     store=store,
   2380     chunk_store=chunk_store,
   2381     storage_options=storage_options,
   2382     mode=mode,
   2383     synchronizer=synchronizer,
   2384     group=group,
   2385     encoding=encoding,
   2386     compute=compute,
   2387     consolidated=consolidated,
   2388     append_dim=append_dim,
   2389     region=region,
   2390     safe_chunks=safe_chunks,
   2391     align_chunks=align_chunks,
   2392     zarr_version=zarr_version,
   2393     zarr_format=zarr_format,
   2394     write_empty_chunks=write_empty_chunks,
   2395     chunkmanager_store_kwargs=chunkmanager_store_kwargs,
   2396 )

File ~/miniconda3/envs/sds210/lib/python3.12/site-packages/xarray/backends/writers.py:773, in to_zarr(dataset, store, chunk_store, mode, synchronizer, group, encoding, compute, consolidated, append_dim, region, safe_chunks, align_chunks, storage_options, zarr_version, zarr_format, write_empty_chunks, chunkmanager_store_kwargs)
    770 if encoding is None:
    771     encoding = {}
--> 773 zstore = get_writable_zarr_store(
    774     store,
    775     chunk_store=chunk_store,
    776     mode=mode,
    777     synchronizer=synchronizer,
    778     group=group,
    779     consolidated=consolidated,
    780     append_dim=append_dim,
    781     region=region,
    782     safe_chunks=safe_chunks,
    783     align_chunks=align_chunks,
    784     storage_options=storage_options,
    785     zarr_version=zarr_version,
    786     zarr_format=zarr_format,
    787     write_empty_chunks=write_empty_chunks,
    788 )
    790 dataset = zstore._validate_and_autodetect_region(dataset)
    791 zstore._validate_encoding(encoding)

File ~/miniconda3/envs/sds210/lib/python3.12/site-packages/xarray/backends/writers.py:666, in get_writable_zarr_store(store, chunk_store, mode, synchronizer, group, consolidated, append_dim, region, safe_chunks, align_chunks, storage_options, zarr_version, zarr_format, write_empty_chunks)
    663     already_consolidated = False
    664     consolidate_on_close = consolidated or consolidated is None
--> 666 return backends.ZarrStore.open_group(
    667     store=mapper,
    668     mode=mode,
    669     synchronizer=synchronizer,
    670     group=group,
    671     consolidated=already_consolidated,
    672     consolidate_on_close=consolidate_on_close,
    673     chunk_store=chunk_mapper,
    674     append_dim=append_dim,
    675     write_region=region,
    676     safe_chunks=safe_chunks,
    677     align_chunks=align_chunks,
    678     zarr_version=zarr_version,
    679     zarr_format=zarr_format,
    680     write_empty=write_empty_chunks,
    681     **kwargs,
    682 )

File ~/miniconda3/envs/sds210/lib/python3.12/site-packages/xarray/backends/zarr.py:714, in ZarrStore.open_group(cls, store, mode, synchronizer, group, consolidated, consolidate_on_close, chunk_store, storage_options, append_dim, write_region, safe_chunks, align_chunks, zarr_version, zarr_format, use_zarr_fill_value_as_mask, write_empty, cache_members)
    688 @classmethod
    689 def open_group(
    690     cls,
   (...)    707     cache_members: bool = True,
    708 ):
    709     (
    710         zarr_group,
    711         consolidate_on_close,
    712         close_store_on_close,
    713         use_zarr_fill_value_as_mask,
--> 714     ) = _get_open_params(
    715         store=store,
    716         mode=mode,
    717         synchronizer=synchronizer,
    718         group=group,
    719         consolidated=consolidated,
    720         consolidate_on_close=consolidate_on_close,
    721         chunk_store=chunk_store,
    722         storage_options=storage_options,
    723         zarr_version=zarr_version,
    724         use_zarr_fill_value_as_mask=use_zarr_fill_value_as_mask,
    725         zarr_format=zarr_format,
    726     )
    728     return cls(
    729         zarr_group,
    730         mode,
   (...)    739         cache_members=cache_members,
    740     )

File ~/miniconda3/envs/sds210/lib/python3.12/site-packages/xarray/backends/zarr.py:1819, in _get_open_params(store, mode, synchronizer, group, consolidated, consolidate_on_close, chunk_store, storage_options, zarr_version, use_zarr_fill_value_as_mask, zarr_format)
   1817     import zarr
   1818 else:
-> 1819     zarr = attempt_import("zarr")
   1821 # zarr doesn't support pathlib.Path objects yet. zarr-python#601
   1822 if isinstance(store, os.PathLike):

File ~/miniconda3/envs/sds210/lib/python3.12/site-packages/xarray/core/utils.py:1381, in attempt_import(module)
   1379     return importlib.import_module(module)
   1380 except ImportError as e:
-> 1381     raise ImportError(
   1382         f"The {install_name} package is required {reason}"
   1383         " but could not be imported."
   1384         " Please install it with your package manager (e.g. conda or pip)."
   1385     ) from e

ImportError: The zarr package is required for working with Zarr stores but could not be imported. Please install it with your package manager (e.g. conda or pip).

Exporting to geospatial formats

If you isolated a single 2D spatial variable (like our New Zealand DEM) and need to share it with a colleague who uses standard GIS software like QGIS or ArcGIS, you should export it as a GeoTIFF.

Because standard xarray does not understand GeoTIFF formatting natively, you must use the .rio accessor to handle the export.

# Save the reprojected and cleaned DEM back to a GeoTIFF file
da_clean.rio.to_raster("reprojected_nz_dem.tif")

7. Exercise

It is time to practice the complete ingestion, inspection, and export workflows for both multidimensional climate data and geospatial raster data.

Task A: The Climate Workflow (NetCDF)

  1. Open: Load the rasm tutorial dataset using xr.tutorial.open_dataset().

  2. Rename: Inspect the data variables. Rename Tair to air_temp.

  3. Check memory: Calculate and print the total memory size of your renamed dataset in Megabytes (MB).

  4. Export: Save your cleaned dataset to a new NetCDF file named rasm_cleaned.nc.

Task B: The Geospatial Workflow (GeoTIFF)

  1. Open: Load the New Zealand DEM (data/Copernicus_DEM_NZ_subset.tif) using the rasterio engine.

  2. Inspect CRS: Programmatically print the original Coordinate Reference System of the DEM.

  3. Reproject: Reproject the data to WGS 84 (EPSG:4326).

  4. Export: Save the reprojected array to a new GeoTIFF named nz_dem_wgs84.tif.

Starter code:

# Install required libraries
!pip install cftime rioxarray

import xarray as xr
import rioxarray

# --- TASK A: Climate Workflow ---
# 1. Open the 'rasm' dataset
# ...

# 2. Rename 'Tair' to 'air_temp'
# ...

# 3. Print the memory size in MB
# ...

# 4. Export to NetCDF
# ...


# --- TASK B: Geospatial Workflow ---
filepath = "data/Copernicus_DEM_NZ_subset.tif"

# 1. Open the DEM
# ...

# 2. Print the CRS
# ...

# 3. Reproject to EPSG:4326
# ...

# 4. Export to GeoTIFF
# ...

8. Summary

Whenever you receive a new dataset, you should apply a standardized workflow to safely ingest, clean, format, and export your data cubes before diving into deep analysis:

  1. Ingest: Load the file using the appropriate tool (xr.open_dataset for NetCDF/HDF5, xr.open_zarr for cloud stores, or the rasterio engine for GeoTIFFs).

  2. Inspect: Explore the interactive HTML display to understand the shape, dimension coordinates, physical variables, and underlying metadata. Always check the memory footprint (.nbytes) to ensure your computer can handle the arrays.

  3. Format: Clean up the dataset by renaming messy variables with .rename(), reassigning coordinate values, and updating missing attributes (like physical units).

  4. Process Geospatial Data: Use the .rio accessor to inspect spatial properties, mask NoData pixels, and use .rio.reproject() to warp grids into matching Coordinate Reference Systems.

  5. Export: Save your verified and cleaned data safely to disk using .to_netcdf(), .to_zarr(), or .rio.to_raster().

With a clean, memory-safe, and spatially aligned dataset sitting on your hard drive, you are now ready to start slicing, aggregating, and analyzing the cube in the upcoming chapters.