XArray Introduction

Unidata Logo

XArray Introduction

Unidata Python Workshop


NumPy Logo

Questions

  1. What is XArray?
  2. How does XArray fit in with Numpy and Pandas?

Objectives

  1. Create a DataArray.
  2. Open netCDF data using XArray
  3. Subset the data.

XArray

XArray expands on the capabilities on NumPy arrays, providing a lot of streamlined data manipulation. It is similar in that respect to Pandas, but whereas Pandas excels at working with tabular data, XArray is focused on N-dimensional arrays of data (i.e. grids). Its interface is based largely on the netCDF data model (variables, attributes, and dimensions), but it goes beyond the traditional netCDF interfaces to provide functionality similar to netCDF-java's Common Data Model (CDM).

DataArray

The DataArray is one of the basic building blocks of XArray. It provides a NumPy ndarray-like object that expands to provide two critical pieces of functionality:

  1. Coordinate names and values are stored with the data, making slicing and indexing much more powerful
  2. It has a built-in container for attributes
In [1]:
# Convention for import to get shortened namespace
import numpy as np
import xarray as xr
In [2]:
# Create some sample "temperature" data
data = 283 + 5 * np.random.randn(5, 3, 4)
data
Out[2]:
array([[[287.16275442, 284.26927143, 284.17036716, 285.44852472],
        [287.07141116, 281.19409127, 283.63632611, 282.09786398],
        [282.98191477, 282.7754062 , 278.4147048 , 276.26890835]],

       [[277.12529841, 285.22685664, 283.78226904, 277.22760698],
        [279.69126369, 285.14504244, 282.10462103, 279.1711785 ],
        [287.63453605, 281.7700569 , 281.31537853, 296.4770727 ]],

       [[282.98627806, 287.17049424, 289.9124875 , 277.61349779],
        [275.76392719, 281.65337905, 279.88901109, 278.43549851],
        [274.15858351, 283.70183906, 293.02407879, 277.99434144]],

       [[281.35311007, 285.313102  , 284.17319522, 278.04255592],
        [281.91368896, 280.68259205, 287.01268106, 285.71052452],
        [277.08916085, 287.84454196, 283.38273541, 286.09016596]],

       [[277.66239665, 289.44934404, 281.57470883, 288.5271864 ],
        [279.28481539, 279.26095815, 284.80405539, 279.87808908],
        [280.36693253, 283.99591138, 280.99835323, 282.59517663]]])

Here we create a basic DataArray by passing it just a numpy array of random data. Note that XArray generates some basic dimension names for us.

In [3]:
temp = xr.DataArray(data)
temp
Out[3]:
<xarray.DataArray (dim_0: 5, dim_1: 3, dim_2: 4)>
array([[[287.16275442, 284.26927143, 284.17036716, 285.44852472],
        [287.07141116, 281.19409127, 283.63632611, 282.09786398],
        [282.98191477, 282.7754062 , 278.4147048 , 276.26890835]],

       [[277.12529841, 285.22685664, 283.78226904, 277.22760698],
        [279.69126369, 285.14504244, 282.10462103, 279.1711785 ],
        [287.63453605, 281.7700569 , 281.31537853, 296.4770727 ]],

       [[282.98627806, 287.17049424, 289.9124875 , 277.61349779],
        [275.76392719, 281.65337905, 279.88901109, 278.43549851],
        [274.15858351, 283.70183906, 293.02407879, 277.99434144]],

       [[281.35311007, 285.313102  , 284.17319522, 278.04255592],
        [281.91368896, 280.68259205, 287.01268106, 285.71052452],
        [277.08916085, 287.84454196, 283.38273541, 286.09016596]],

       [[277.66239665, 289.44934404, 281.57470883, 288.5271864 ],
        [279.28481539, 279.26095815, 284.80405539, 279.87808908],
        [280.36693253, 283.99591138, 280.99835323, 282.59517663]]])
Dimensions without coordinates: dim_0, dim_1, dim_2

We can also pass in our own dimension names:

In [4]:
temp = xr.DataArray(data, dims=['time', 'lat', 'lon'])
temp
Out[4]:
<xarray.DataArray (time: 5, lat: 3, lon: 4)>
array([[[287.16275442, 284.26927143, 284.17036716, 285.44852472],
        [287.07141116, 281.19409127, 283.63632611, 282.09786398],
        [282.98191477, 282.7754062 , 278.4147048 , 276.26890835]],

       [[277.12529841, 285.22685664, 283.78226904, 277.22760698],
        [279.69126369, 285.14504244, 282.10462103, 279.1711785 ],
        [287.63453605, 281.7700569 , 281.31537853, 296.4770727 ]],

       [[282.98627806, 287.17049424, 289.9124875 , 277.61349779],
        [275.76392719, 281.65337905, 279.88901109, 278.43549851],
        [274.15858351, 283.70183906, 293.02407879, 277.99434144]],

       [[281.35311007, 285.313102  , 284.17319522, 278.04255592],
        [281.91368896, 280.68259205, 287.01268106, 285.71052452],
        [277.08916085, 287.84454196, 283.38273541, 286.09016596]],

       [[277.66239665, 289.44934404, 281.57470883, 288.5271864 ],
        [279.28481539, 279.26095815, 284.80405539, 279.87808908],
        [280.36693253, 283.99591138, 280.99835323, 282.59517663]]])
Dimensions without coordinates: time, lat, lon

This is already improved upon from a numpy array, because we have names for each of the dimensions (or axes in NumPy parlance). Even better, we can take arrays representing the values for the coordinates for each of these dimensions and associate them with the data when we create the DataArray.

In [5]:
# Use pandas to create an array of datetimes
import pandas as pd
times = pd.date_range('2018-01-01', periods=5)
times
Out[5]:
DatetimeIndex(['2018-01-01', '2018-01-02', '2018-01-03', '2018-01-04',
               '2018-01-05'],
              dtype='datetime64[ns]', freq='D')
In [6]:
# Sample lon/lats
lons = np.linspace(-120, -60, 4)
lats = np.linspace(25, 55, 3)

When we create the DataArray instance, we pass in the arrays we just created:

In [7]:
temp = xr.DataArray(data, coords=[times, lats, lons], dims=['time', 'lat', 'lon'])
temp
Out[7]:
<xarray.DataArray (time: 5, lat: 3, lon: 4)>
array([[[287.16275442, 284.26927143, 284.17036716, 285.44852472],
        [287.07141116, 281.19409127, 283.63632611, 282.09786398],
        [282.98191477, 282.7754062 , 278.4147048 , 276.26890835]],

       [[277.12529841, 285.22685664, 283.78226904, 277.22760698],
        [279.69126369, 285.14504244, 282.10462103, 279.1711785 ],
        [287.63453605, 281.7700569 , 281.31537853, 296.4770727 ]],

       [[282.98627806, 287.17049424, 289.9124875 , 277.61349779],
        [275.76392719, 281.65337905, 279.88901109, 278.43549851],
        [274.15858351, 283.70183906, 293.02407879, 277.99434144]],

       [[281.35311007, 285.313102  , 284.17319522, 278.04255592],
        [281.91368896, 280.68259205, 287.01268106, 285.71052452],
        [277.08916085, 287.84454196, 283.38273541, 286.09016596]],

       [[277.66239665, 289.44934404, 281.57470883, 288.5271864 ],
        [279.28481539, 279.26095815, 284.80405539, 279.87808908],
        [280.36693253, 283.99591138, 280.99835323, 282.59517663]]])
Coordinates:
  * time     (time) datetime64[ns] 2018-01-01 2018-01-02 ... 2018-01-05
  * lat      (lat) float64 25.0 40.0 55.0
  * lon      (lon) float64 -120.0 -100.0 -80.0 -60.0

...and we can also set some attribute metadata:

In [8]:
temp.attrs['units'] = 'kelvin'
temp.attrs['standard_name'] = 'air_temperature'

temp
Out[8]:
<xarray.DataArray (time: 5, lat: 3, lon: 4)>
array([[[287.16275442, 284.26927143, 284.17036716, 285.44852472],
        [287.07141116, 281.19409127, 283.63632611, 282.09786398],
        [282.98191477, 282.7754062 , 278.4147048 , 276.26890835]],

       [[277.12529841, 285.22685664, 283.78226904, 277.22760698],
        [279.69126369, 285.14504244, 282.10462103, 279.1711785 ],
        [287.63453605, 281.7700569 , 281.31537853, 296.4770727 ]],

       [[282.98627806, 287.17049424, 289.9124875 , 277.61349779],
        [275.76392719, 281.65337905, 279.88901109, 278.43549851],
        [274.15858351, 283.70183906, 293.02407879, 277.99434144]],

       [[281.35311007, 285.313102  , 284.17319522, 278.04255592],
        [281.91368896, 280.68259205, 287.01268106, 285.71052452],
        [277.08916085, 287.84454196, 283.38273541, 286.09016596]],

       [[277.66239665, 289.44934404, 281.57470883, 288.5271864 ],
        [279.28481539, 279.26095815, 284.80405539, 279.87808908],
        [280.36693253, 283.99591138, 280.99835323, 282.59517663]]])
Coordinates:
  * time     (time) datetime64[ns] 2018-01-01 2018-01-02 ... 2018-01-05
  * lat      (lat) float64 25.0 40.0 55.0
  * lon      (lon) float64 -120.0 -100.0 -80.0 -60.0
Attributes:
    units:          kelvin
    standard_name:  air_temperature

Notice what happens if we perform a mathematical operaton with the DataArray: the coordinate values persist, but the attributes are lost. This is done because it is very challenging to know if the attribute metadata is still correct or appropriate after arbitrary arithmetic operations.

In [9]:
# For example, convert Kelvin to Celsius
temp - 273.15
Out[9]:
<xarray.DataArray (time: 5, lat: 3, lon: 4)>
array([[[14.01275442, 11.11927143, 11.02036716, 12.29852472],
        [13.92141116,  8.04409127, 10.48632611,  8.94786398],
        [ 9.83191477,  9.6254062 ,  5.2647048 ,  3.11890835]],

       [[ 3.97529841, 12.07685664, 10.63226904,  4.07760698],
        [ 6.54126369, 11.99504244,  8.95462103,  6.0211785 ],
        [14.48453605,  8.6200569 ,  8.16537853, 23.3270727 ]],

       [[ 9.83627806, 14.02049424, 16.7624875 ,  4.46349779],
        [ 2.61392719,  8.50337905,  6.73901109,  5.28549851],
        [ 1.00858351, 10.55183906, 19.87407879,  4.84434144]],

       [[ 8.20311007, 12.163102  , 11.02319522,  4.89255592],
        [ 8.76368896,  7.53259205, 13.86268106, 12.56052452],
        [ 3.93916085, 14.69454196, 10.23273541, 12.94016596]],

       [[ 4.51239665, 16.29934404,  8.42470883, 15.3771864 ],
        [ 6.13481539,  6.11095815, 11.65405539,  6.72808908],
        [ 7.21693253, 10.84591138,  7.84835323,  9.44517663]]])
Coordinates:
  * time     (time) datetime64[ns] 2018-01-01 2018-01-02 ... 2018-01-05
  * lat      (lat) float64 25.0 40.0 55.0
  * lon      (lon) float64 -120.0 -100.0 -80.0 -60.0

Selection

We can use the .sel method to select portions of our data based on these coordinate values, rather than using indices (this is similar to the CDM).

In [10]:
temp.sel(time='2018-01-02')
Out[10]:
<xarray.DataArray (lat: 3, lon: 4)>
array([[277.12529841, 285.22685664, 283.78226904, 277.22760698],
       [279.69126369, 285.14504244, 282.10462103, 279.1711785 ],
       [287.63453605, 281.7700569 , 281.31537853, 296.4770727 ]])
Coordinates:
    time     datetime64[ns] 2018-01-02
  * lat      (lat) float64 25.0 40.0 55.0
  * lon      (lon) float64 -120.0 -100.0 -80.0 -60.0
Attributes:
    units:          kelvin
    standard_name:  air_temperature

.sel has the flexibility to also perform nearest neighbor sampling, taking an optional tolerance:

In [11]:
from datetime import timedelta
temp.sel(time='2018-01-07', method='nearest', tolerance=timedelta(days=2))
Out[11]:
<xarray.DataArray (lat: 3, lon: 4)>
array([[277.66239665, 289.44934404, 281.57470883, 288.5271864 ],
       [279.28481539, 279.26095815, 284.80405539, 279.87808908],
       [280.36693253, 283.99591138, 280.99835323, 282.59517663]])
Coordinates:
    time     datetime64[ns] 2018-01-05
  * lat      (lat) float64 25.0 40.0 55.0
  * lon      (lon) float64 -120.0 -100.0 -80.0 -60.0
Attributes:
    units:          kelvin
    standard_name:  air_temperature
EXERCISE: .interp() works similarly to .sel(). Using .interp(), get an interpolated time series "forecast" for Boulder (40°N, 105°W) or your favorite latitude/longitude location. (Documentation for interp here).
In [12]:
# YOUR CODE GOES HERE
SOLUTION
In [13]:
# %load solutions/interp_solution.py

# Cell content replaced by load magic replacement.
temp.interp(lon=-105, lat=40)
Out[13]:
<xarray.DataArray (time: 5)>
array([282.66342124, 283.78159776, 280.18101608, 280.99036628,
       279.26692246])
Coordinates:
  * time     (time) datetime64[ns] 2018-01-01 2018-01-02 ... 2018-01-05
    lon      int64 -105
    lat      int64 40
Attributes:
    units:          kelvin
    standard_name:  air_temperature

Slicing with Selection

In [14]:
temp.sel(time=slice('2018-01-01', '2018-01-03'), lon=slice(-110, -70), lat=slice(25, 45))
Out[14]:
<xarray.DataArray (time: 3, lat: 2, lon: 2)>
array([[[284.26927143, 284.17036716],
        [281.19409127, 283.63632611]],

       [[285.22685664, 283.78226904],
        [285.14504244, 282.10462103]],

       [[287.17049424, 289.9124875 ],
        [281.65337905, 279.88901109]]])
Coordinates:
  * time     (time) datetime64[ns] 2018-01-01 2018-01-02 2018-01-03
  * lat      (lat) float64 25.0 40.0
  * lon      (lon) float64 -100.0 -80.0
Attributes:
    units:          kelvin
    standard_name:  air_temperature

.loc

All of these operations can also be done within square brackets on the .loc attribute of the DataArray. This permits a much more numpy-looking syntax, though you lose the ability to specify the names of the various dimensions. Instead, the slicing must be done in the correct order.

In [15]:
# As done above
temp.loc['2018-01-02']
Out[15]:
<xarray.DataArray (lat: 3, lon: 4)>
array([[277.12529841, 285.22685664, 283.78226904, 277.22760698],
       [279.69126369, 285.14504244, 282.10462103, 279.1711785 ],
       [287.63453605, 281.7700569 , 281.31537853, 296.4770727 ]])
Coordinates:
    time     datetime64[ns] 2018-01-02
  * lat      (lat) float64 25.0 40.0 55.0
  * lon      (lon) float64 -120.0 -100.0 -80.0 -60.0
Attributes:
    units:          kelvin
    standard_name:  air_temperature
In [16]:
temp.loc['2018-01-01':'2018-01-03', 25:45, -110:-70]
Out[16]:
<xarray.DataArray (time: 3, lat: 2, lon: 2)>
array([[[284.26927143, 284.17036716],
        [281.19409127, 283.63632611]],

       [[285.22685664, 283.78226904],
        [285.14504244, 282.10462103]],

       [[287.17049424, 289.9124875 ],
        [281.65337905, 279.88901109]]])
Coordinates:
  * time     (time) datetime64[ns] 2018-01-01 2018-01-02 2018-01-03
  * lat      (lat) float64 25.0 40.0
  * lon      (lon) float64 -100.0 -80.0
Attributes:
    units:          kelvin
    standard_name:  air_temperature

This does not work however:

temp.loc[-110:-70, 25:45,'2018-01-01':'2018-01-03']

Opening netCDF data

With its close ties to the netCDF data model, XArray also supports netCDF as a first-class file format. This means it has easy support for opening netCDF datasets, so long as they conform to some of XArray's limitations (such as 1-dimensional coordinates).

In [17]:
# Open sample North American Reanalysis data in netCDF format
ds = xr.open_dataset('../../data/NARR_19930313_0000.nc')
ds
Out[17]:
<xarray.Dataset>
Dimensions:                       (isobaric1: 29, time1: 1, x: 268, y: 119)
Coordinates:
  * time1                         (time1) datetime64[ns] 1993-03-13
  * isobaric1                     (isobaric1) float32 100.0 125.0 ... 1000.0
  * y                             (y) float32 -3116.548 -3084.0852 ... 714.08594
  * x                             (x) float32 -3324.4707 ... 5343.1504
Data variables:
    u-component_of_wind_isobaric  (time1, isobaric1, y, x) float32 ...
    LambertConformal_Projection   int32 ...
    lat                           (y, x) float64 ...
    lon                           (y, x) float64 ...
    Geopotential_height_isobaric  (time1, isobaric1, y, x) float32 ...
    v-component_of_wind_isobaric  (time1, isobaric1, y, x) float32 ...
    Temperature_isobaric          (time1, isobaric1, y, x) float32 ...
Attributes:
    Originating_or_generating_Center:     US National Weather Service, Nation...
    Originating_or_generating_Subcenter:  North American Regional Reanalysis ...
    GRIB_table_version:                   0,131
    Generating_process_or_model:          North American Regional Reanalysis ...
    Conventions:                          CF-1.6
    history:                              Read using CDM IOSP GribCollection v3
    featureType:                          GRID
    History:                              Translated to CF-1.0 Conventions by...
    geospatial_lat_min:                   10.753308882144761
    geospatial_lat_max:                   46.8308828962289
    geospatial_lon_min:                   -153.88242040519995
    geospatial_lon_max:                   -42.666108129242815

This returns a Dataset object, which is a container that contains one or more DataArrays, which can also optionally share coordinates. We can then pull out individual fields:

In [18]:
ds.isobaric1
Out[18]:
<xarray.DataArray 'isobaric1' (isobaric1: 29)>
array([ 100.,  125.,  150.,  175.,  200.,  225.,  250.,  275.,  300.,  350.,
        400.,  450.,  500.,  550.,  600.,  650.,  700.,  725.,  750.,  775.,
        800.,  825.,  850.,  875.,  900.,  925.,  950.,  975., 1000.],
      dtype=float32)
Coordinates:
  * isobaric1  (isobaric1) float32 100.0 125.0 150.0 ... 950.0 975.0 1000.0
Attributes:
    units:                   hPa
    long_name:               Isobaric surface
    positive:                down
    Grib_level_type:         100
    _CoordinateAxisType:     Pressure
    _CoordinateZisPositive:  down

or

In [19]:
ds['isobaric1']
Out[19]:
<xarray.DataArray 'isobaric1' (isobaric1: 29)>
array([ 100.,  125.,  150.,  175.,  200.,  225.,  250.,  275.,  300.,  350.,
        400.,  450.,  500.,  550.,  600.,  650.,  700.,  725.,  750.,  775.,
        800.,  825.,  850.,  875.,  900.,  925.,  950.,  975., 1000.],
      dtype=float32)
Coordinates:
  * isobaric1  (isobaric1) float32 100.0 125.0 150.0 ... 950.0 975.0 1000.0
Attributes:
    units:                   hPa
    long_name:               Isobaric surface
    positive:                down
    Grib_level_type:         100
    _CoordinateAxisType:     Pressure
    _CoordinateZisPositive:  down

Datasets also support much of the same subsetting operations as DataArray, but will perform the operation on all data:

In [20]:
ds_1000 = ds.sel(isobaric1=1000.0)
ds_1000
Out[20]:
<xarray.Dataset>
Dimensions:                       (time1: 1, x: 268, y: 119)
Coordinates:
  * time1                         (time1) datetime64[ns] 1993-03-13
    isobaric1                     float32 1000.0
  * y                             (y) float32 -3116.548 -3084.0852 ... 714.08594
  * x                             (x) float32 -3324.4707 ... 5343.1504
Data variables:
    u-component_of_wind_isobaric  (time1, y, x) float32 ...
    LambertConformal_Projection   int32 ...
    lat                           (y, x) float64 ...
    lon                           (y, x) float64 ...
    Geopotential_height_isobaric  (time1, y, x) float32 ...
    v-component_of_wind_isobaric  (time1, y, x) float32 ...
    Temperature_isobaric          (time1, y, x) float32 ...
Attributes:
    Originating_or_generating_Center:     US National Weather Service, Nation...
    Originating_or_generating_Subcenter:  North American Regional Reanalysis ...
    GRIB_table_version:                   0,131
    Generating_process_or_model:          North American Regional Reanalysis ...
    Conventions:                          CF-1.6
    history:                              Read using CDM IOSP GribCollection v3
    featureType:                          GRID
    History:                              Translated to CF-1.0 Conventions by...
    geospatial_lat_min:                   10.753308882144761
    geospatial_lat_max:                   46.8308828962289
    geospatial_lon_min:                   -153.88242040519995
    geospatial_lon_max:                   -42.666108129242815
In [21]:
ds_1000.Temperature_isobaric
Out[21]:
<xarray.DataArray 'Temperature_isobaric' (time1: 1, y: 119, x: 268)>
array([[[294.09436, 294.1256 , ..., 298.6725 , 298.71936],
        [294.07874, 294.14124, ..., 298.6256 , 298.5475 ],
        ...,
        [276.86   , 276.84436, ..., 289.11   , 289.01624],
        [277.01624, 276.82874, ..., 289.0475 , 288.96936]]], dtype=float32)
Coordinates:
  * time1      (time1) datetime64[ns] 1993-03-13
    isobaric1  float32 1000.0
  * y          (y) float32 -3116.548 -3084.0852 -3051.622 ... 681.6229 714.08594
  * x          (x) float32 -3324.4707 -3292.0078 ... 5310.6875 5343.1504
Attributes:
    long_name:           Temperature @ Isobaric surface
    units:               K
    description:         Temperature
    grid_mapping:        LambertConformal_Projection
    Grib_Variable_Id:    VAR_7-15-131-11_L100
    Grib1_Center:        7
    Grib1_Subcenter:     15
    Grib1_TableVersion:  131
    Grib1_Parameter:     11
    Grib1_Level_Type:    100
    Grib1_Level_Desc:    Isobaric surface

Aggregation operations

Not only can you use the named dimensions for manual slicing and indexing of data, but you can also use it to control aggregation operations, like sum:

In [22]:
u_winds = ds['u-component_of_wind_isobaric']
u_winds.std(dim=['x', 'y'])
Out[22]:
<xarray.DataArray 'u-component_of_wind_isobaric' (time1: 1, isobaric1: 29)>
array([[ 8.673963 , 10.212325 , 11.556413 , 12.254429 , 13.372146 ,
        15.472462 , 16.091969 , 15.846294 , 15.195834 , 13.936979 ,
        12.93888  , 12.060708 , 10.972139 ,  9.722328 ,  8.853286 ,
         8.257241 ,  7.679721 ,  7.4516497,  7.2352104,  7.039894 ,
         6.883371 ,  6.7821493,  6.7088237,  6.6865997,  6.7247376,
         6.745023 ,  6.6859775,  6.5107226,  5.972262 ]], dtype=float32)
Coordinates:
  * time1      (time1) datetime64[ns] 1993-03-13
  * isobaric1  (isobaric1) float32 100.0 125.0 150.0 ... 950.0 975.0 1000.0
EXERCISE: Using the sample dataset, calculate the mean temperature profile (temperature as a function of pressure) over Colorado within this dataset. For this exercise, consider the bounds of Colorado to be:
  • x: -182km to 424km
  • y: -1450km to -990km
(37°N to 41°N and 102°W to 109°W projected to Lambert Conformal projection coordinates)
In [23]:
# YOUR CODE GOES HERE
SOLUTION
In [24]:
# %load solutions/mean_profile.py

# Cell content replaced by load magic replacement.
temps = ds.Temperature_isobaric
co_temps = temps.sel(x=slice(-182, 424), y=slice(-1450, -990))
prof = co_temps.mean(dim=['x', 'y'])
prof
Out[24]:
<xarray.DataArray 'Temperature_isobaric' (time1: 1, isobaric1: 29)>
array([[215.078  , 215.76935, 217.243  , 217.82663, 215.83487, 216.10933,
        219.99902, 224.66118, 228.80576, 234.88701, 238.78503, 242.66309,
        246.44807, 249.26636, 250.84995, 253.37354, 257.0429 , 259.08398,
        260.97955, 262.98364, 264.82138, 266.5198 , 268.22467, 269.7471 ,
        271.18216, 272.66815, 274.13037, 275.54718, 276.97675]],
      dtype=float32)
Coordinates:
  * time1      (time1) datetime64[ns] 1993-03-13
  * isobaric1  (isobaric1) float32 100.0 125.0 150.0 ... 950.0 975.0 1000.0

Resources

There is much more in the XArray library. To learn more, visit the XArray Documentation