2 Working with DataTree
During a modern Bayesian analysis we usually generate many sets of data including posterior samples, prior/posterior predictive samples, statistics generated by the sampling method, etc. To keep all this data tidy and avoid confusion ArviZ relies on the data-structures provided by xarray (Hoyer and Hamman 2017). If you are not familiar with xarray this chapter introduces some basic elements in the context of Bayesian stats. For a deeper understanding we recommend that you check their xarray’s documentation, you may find xarray useful for problems outside Bayesian analysis.
We need to become familiar with 3 Data Structures:
DataArray: A labelled, N-dimensional array. In other words this is like NumPy but you can access the data using meaningful labels instead of numerical indexes. You may also think of this as the N-D generalization of a pandas/polarsSeries.Dataset: It is a dict-like container of DataArray objects aligned along any number of shared dimensions. You may also think of this as the N-D generalization of a pandas/polarsDataFrame.DataTree: This is a container of Datasets, each Dataset is associated with a group.
The best way to understand these data-structures is to explore them. In a real scenario DataTree will be directly generated by probabilistic programming languages (ppls), or the output of ppls will be transformed into DataTree, likely using ArviZ. But we don’t need to fit a model to play with DataTree objects, ArviZ comes equipped with a few DataTree objects. Let’s start by loading the centered_eight DataTree.
In the context of Bayesian statistics a DataTree has groups like posterior, observed_data, posterior_predictive, log_likelihood, etc.
('/',
'/posterior',
'/posterior_predictive',
'/log_likelihood',
'/sample_stats',
'/prior',
'/prior_predictive',
'/observed_data',
'/constant_data')
DataTrees used by ArviZ are expected to follow the InferenceData schema, which defines the groups and structure a DataTree should contain. For example, the posterior group stores posterior samples, while observed_data stores the observed data. Although the schema is not strictly enforced, following it is strongly recommended because many ArviZ functions assume this structure. When a DataTree follows the schema, most functions work out of the box; otherwise, additional preprocessing may be required.
DataTree/Dataset/DataArray objects have a nice HTML representation in supported environments like Jupyter notebooks.
<xarray.DataTree>
Group: /
├── Group: /posterior
│ Dimensions: (chain: 4, draw: 500, school: 8)
│ Coordinates:
│ * chain (chain) int64 32B 0 1 2 3
│ * draw (draw) int64 4kB 0 1 2 3 4 5 6 7 ... 493 494 495 496 497 498 499
│ * school (school) <U16 512B 'Choate' 'Deerfield' ... 'Mt. Hermon'
│ Data variables:
│ mu (chain, draw) float64 16kB 1.716 1.903 1.903 ... 5.409 7.721 10.24
│ theta (chain, draw, school) float64 128kB 2.317 1.45 ... 14.92 14.02
│ tau (chain, draw) float64 16kB 0.8775 0.8027 0.8027 ... 2.99 3.052
│ Attributes: (6)
├── Group: /posterior_predictive
│ Dimensions: (chain: 4, draw: 500, school: 8)
│ Coordinates:
│ * chain (chain) int64 32B 0 1 2 3
│ * draw (draw) int64 4kB 0 1 2 3 4 5 6 7 ... 493 494 495 496 497 498 499
│ * school (school) <U16 512B 'Choate' 'Deerfield' ... 'Mt. Hermon'
│ Data variables:
│ obs (chain, draw, school) float64 128kB 38.88 -14.98 ... 27.05 20.99
│ Attributes: (4)
├── Group: /log_likelihood
│ Dimensions: (chain: 4, draw: 500, school: 8)
│ Coordinates:
│ * chain (chain) int64 32B 0 1 2 3
│ * draw (draw) int64 4kB 0 1 2 3 4 5 6 7 ... 493 494 495 496 497 498 499
│ * school (school) <U16 512B 'Choate' 'Deerfield' ... 'Mt. Hermon'
│ Data variables:
│ obs (chain, draw, school) float64 128kB -5.093 -3.436 ... -3.269 -3.816
│ Attributes: (4)
├── Group: /sample_stats
│ Dimensions: (chain: 4, draw: 500)
│ Coordinates:
│ * chain (chain) int64 32B 0 1 2 3
│ * draw (draw) int64 4kB 0 1 2 3 4 5 ... 495 496 497 498 499
│ Data variables: (12/17)
│ step_size (chain, draw) float64 16kB 0.1427 0.1427 ... 0.1233
│ reached_max_treedepth (chain, draw) bool 2kB False False ... False False
│ perf_counter_start (chain, draw) float64 16kB 3.931e+04 ... 3.931e+04
│ energy_error (chain, draw) float64 16kB 1.896 -1.479 ... 0.1372
│ perf_counter_diff (chain, draw) float64 16kB 0.0004726 ... 0.001483
│ tree_depth (chain, draw) int64 16kB 2 3 3 3 3 5 ... 4 4 4 4 4 4
│ ... ...
│ index_in_trajectory (chain, draw) int64 16kB 1 -1 0 0 1 ... -6 9 13 7 -15
│ acceptance_rate (chain, draw) float64 16kB 0.05665 0.1429 ... 0.8901
│ smallest_eigval (chain, draw) float64 16kB nan nan nan ... nan nan
│ energy (chain, draw) float64 16kB 47.98 49.69 ... 60.07
│ diverging (chain, draw) bool 2kB False False ... False False
│ largest_eigval (chain, draw) float64 16kB nan nan nan ... nan nan
│ Attributes: (6)
├── Group: /prior
│ Dimensions: (chain: 1, draw: 500, school: 8)
│ Coordinates:
│ * chain (chain) int64 8B 0
│ * draw (draw) int64 4kB 0 1 2 3 4 5 6 7 ... 493 494 495 496 497 498 499
│ * school (school) <U16 512B 'Choate' 'Deerfield' ... 'Mt. Hermon'
│ Data variables:
│ theta (chain, draw, school) float64 32kB -8.435 24.12 ... 54.57 52.29
│ tau (chain, draw) float64 4kB 11.93 17.76 4.732 ... 2.231 3.319 93.69
│ mu (chain, draw) float64 4kB 4.714 3.853 1.709 ... -2.245 -2.435
│ Attributes: (4)
├── Group: /prior_predictive
│ Dimensions: (chain: 1, draw: 500, school: 8)
│ Coordinates:
│ * chain (chain) int64 8B 0
│ * draw (draw) int64 4kB 0 1 2 3 4 5 6 7 ... 493 494 495 496 497 498 499
│ * school (school) <U16 512B 'Choate' 'Deerfield' ... 'Mt. Hermon'
│ Data variables:
│ obs (chain, draw, school) float64 32kB 22.03 26.95 ... 58.23 39.78
│ Attributes: (4)
├── Group: /observed_data
│ Dimensions: (school: 8)
│ Coordinates:
│ * school (school) <U16 512B 'Choate' 'Deerfield' ... 'Mt. Hermon'
│ Data variables:
│ obs (school) float64 64B 28.0 8.0 -3.0 7.0 -1.0 1.0 18.0 12.0
│ Attributes: (4)
└── Group: /constant_data
Dimensions: (school: 8)
Coordinates:
* school (school) <U16 512B 'Choate' 'Deerfield' ... 'Mt. Hermon'
Data variables:
sigma (school) float64 64B 15.0 10.0 16.0 11.0 9.0 11.0 10.0 18.0
Attributes: (4)You should be able to interact with this HTML representation of a DataTree. For instance, you can expand each group by clicking on its name (or on folder symbol).
An important concept is that of dimensions and coordinates. The xarray documentation is full of examples related to maps. In that context, the dimensions would be like latitude and longitude, and the coordinates would be the actual values of latitude and longitude. But the idea is very general and applies to any kind of data.
Let’s see the dimensions and coordinates for the posterior in our dt object. We can see 3 dimensions chain, draw, and school.
chainis used to index the different chains of the MCMC sampler. Its coordinates are the integers[0, 1, 2, 3]because we have 4 chains.drawis used to index the iterations of the MCMC sampler. Its coordinates are the integers[0, 1, 2, ..., 499]because we have 500 draws per chain.schoolis problem-specific. The coordinates forschoolare the names of 8 schools,['Choate', 'Deerfield', 'Phillips Andover', 'Phillips Exeter', 'Hotchkiss', 'Lawrenceville', "St. Paul's", 'Mt. Hermon']
The dimensions chain and draw are common in MCMC sampling, and hence they are expected to be present in the posterior group of a DataTree generated from MCMC. The school dimension is specific to the model from which the posterior was generated, and it is not expected to be present in all DataTrees.
2.0.1 Get the dataset corresponding to a single group
We can access each group using a dictionary-like notation:
<xarray.DataTree 'posterior'>
Group: /posterior
Dimensions: (chain: 4, draw: 500, school: 8)
Coordinates:
* chain (chain) int64 32B 0 1 2 3
* draw (draw) int64 4kB 0 1 2 3 4 5 6 7 ... 493 494 495 496 497 498 499
* school (school) <U16 512B 'Choate' 'Deerfield' ... 'Mt. Hermon'
Data variables:
mu (chain, draw) float64 16kB 1.716 1.903 1.903 ... 5.409 7.721 10.24
theta (chain, draw, school) float64 128kB 2.317 1.45 ... 14.92 14.02
tau (chain, draw) float64 16kB 0.8775 0.8027 0.8027 ... 2.99 3.052
Attributes: (6)Alternatively, we can use the dot notation, as groups are attributes of the DataTree. For instance, to access the posterior group we can write:
The dot notation works at the group level and for Datasets and DataArrays as long as there is no conflict with a method or attribute of these objects. If there is a conflict, you can always use the dictionary-like notation.
Notice that we still get a DataTree. If you want the Dataset you can do, most of the time you probably want to do:
<xarray.DatasetView> Size: 165kB
Dimensions: (chain: 4, draw: 500, school: 8)
Coordinates:
* chain (chain) int64 32B 0 1 2 3
* draw (draw) int64 4kB 0 1 2 3 4 5 6 7 ... 493 494 495 496 497 498 499
* school (school) <U16 512B 'Choate' 'Deerfield' ... 'Mt. Hermon'
Data variables:
mu (chain, draw) float64 16kB 1.716 1.903 1.903 ... 5.409 7.721 10.24
theta (chain, draw, school) float64 128kB 2.317 1.45 ... 14.92 14.02
tau (chain, draw) float64 16kB 0.8775 0.8027 0.8027 ... 2.99 3.052
Attributes: (6)This will return an immutable Dataset-like view. If you want a new Dataset do dt["posterior"].to_dataset().
2.0.2 Get coordinate values
As we have seen, we have 8 schools with their names. If we want to programmatically access the names we can do
<xarray.DataArray 'school' (school: 8)> Size: 512B
array(['Choate', 'Deerfield', 'Phillips Andover', 'Phillips Exeter',
'Hotchkiss', 'Lawrenceville', "St. Paul's", 'Mt. Hermon'], dtype='<U16')
Coordinates:
* school (school) <U16 512B 'Choate' 'Deerfield' ... 'Mt. Hermon'Which returns a DataArray with the names of the schools. To obtain a NumPy array we can do
array(['Choate', 'Deerfield', 'Phillips Andover', 'Phillips Exeter',
'Hotchkiss', 'Lawrenceville', "St. Paul's", 'Mt. Hermon'],
dtype='<U16')
If we want to get the number of schools we can write:
Notice that we do not need to first obtain the NumPy array and then compute the length. When working with DataTree/sets/Arrays, you may feel tempted to reduce them to NumPy arrays, as you are more familiar with those. But for many problems that is not needed and for many other that is not even recommended as you may lose the benefit of working with labeled array-like structures.
2.0.3 Get a subset of chains
Because we have labels for the names of the schools we can use them to access their associated information. Labels are usually much easier to remember than numerical indices. For instance, to access the posterior samples of the school Choate we can write:
<xarray.Dataset> Size: 52kB
Dimensions: (chain: 4, draw: 500)
Coordinates:
* chain (chain) int64 32B 0 1 2 3
* draw (draw) int64 4kB 0 1 2 3 4 5 6 7 ... 493 494 495 496 497 498 499
school <U16 64B 'Choate'
Data variables:
mu (chain, draw) float64 16kB 1.716 1.903 1.903 ... 5.409 7.721 10.24
theta (chain, draw) float64 16kB 2.317 0.8892 0.8892 ... 7.086 10.46
tau (chain, draw) float64 16kB 0.8775 0.8027 0.8027 ... 2.99 3.052
Attributes: (6)The draw and chain coordinates are indexed using numbers, the following code will return the last draw from chain 1 and chain 2:
<xarray.Dataset> Size: 696B
Dimensions: (chain: 2, school: 8)
Coordinates:
* chain (chain) int64 16B 1 2
* school (school) <U16 512B 'Choate' 'Deerfield' ... 'Mt. Hermon'
draw int64 8B 499
Data variables:
mu (chain) float64 16B 4.127 7.041
theta (chain, school) float64 128B 4.474 14.26 -5.431 ... 7.081 6.192
tau (chain) float64 16B 6.8 4.773
Attributes: (6)Usually, in Bayesian statistics, we don’t need to access individual draws or chains, a more common operation is to select a range. For that purpose, we can use Python’s slice function. For example, the following line of code returns the first 200 draws from all chains:
<xarray.Dataset> Size: 66kB
Dimensions: (chain: 4, draw: 201, school: 8)
Coordinates:
* chain (chain) int64 32B 0 1 2 3
* draw (draw) int64 2kB 0 1 2 3 4 5 6 7 ... 194 195 196 197 198 199 200
* school (school) <U16 512B 'Choate' 'Deerfield' ... 'Mt. Hermon'
Data variables:
mu (chain, draw) float64 6kB 1.716 1.903 1.903 ... 5.705 7.428 7.149
theta (chain, draw, school) float64 51kB 2.317 1.45 2.086 ... 3.324 10.35
tau (chain, draw) float64 6kB 0.8775 0.8027 0.8027 ... 2.773 6.931
Attributes: (6)Using the slice function we can also remove the first 100 draws.
<xarray.Dataset> Size: 132kB
Dimensions: (chain: 4, draw: 400, school: 8)
Coordinates:
* chain (chain) int64 32B 0 1 2 3
* draw (draw) int64 3kB 100 101 102 103 104 105 ... 495 496 497 498 499
* school (school) <U16 512B 'Choate' 'Deerfield' ... 'Mt. Hermon'
Data variables:
mu (chain, draw) float64 13kB 6.497 1.921 3.822 ... 5.409 7.721 10.24
theta (chain, draw, school) float64 102kB 8.271 7.066 ... 14.92 14.02
tau (chain, draw) float64 13kB 1.386 2.267 1.846 ... 2.236 2.99 3.052
Attributes: (6)If we try the same operation with the entire DataTree, we will get an error. The reason being that some groups don’t have the draw dimension, for instance the observed_data group has only the school dimension. What we can do instead is to filter those group that have draw and then perform the selection and slicing.
<xarray.DataTree>
Group: /
├── Group: /posterior
│ Dimensions: (chain: 4, draw: 400, school: 8)
│ Coordinates:
│ * chain (chain) int64 32B 0 1 2 3
│ * draw (draw) int64 3kB 100 101 102 103 104 105 ... 495 496 497 498 499
│ * school (school) <U16 512B 'Choate' 'Deerfield' ... 'Mt. Hermon'
│ Data variables:
│ mu (chain, draw) float64 13kB 6.497 1.921 3.822 ... 5.409 7.721 10.24
│ theta (chain, draw, school) float64 102kB 8.271 7.066 ... 14.92 14.02
│ tau (chain, draw) float64 13kB 1.386 2.267 1.846 ... 2.236 2.99 3.052
│ Attributes: (6)
├── Group: /posterior_predictive
│ Dimensions: (chain: 4, draw: 400, school: 8)
│ Coordinates:
│ * chain (chain) int64 32B 0 1 2 3
│ * draw (draw) int64 3kB 100 101 102 103 104 105 ... 495 496 497 498 499
│ * school (school) <U16 512B 'Choate' 'Deerfield' ... 'Mt. Hermon'
│ Data variables:
│ obs (chain, draw, school) float64 102kB -17.56 4.567 ... 27.05 20.99
│ Attributes: (4)
├── Group: /log_likelihood
│ Dimensions: (chain: 4, draw: 400, school: 8)
│ Coordinates:
│ * chain (chain) int64 32B 0 1 2 3
│ * draw (draw) int64 3kB 100 101 102 103 104 105 ... 495 496 497 498 499
│ * school (school) <U16 512B 'Choate' 'Deerfield' ... 'Mt. Hermon'
│ Data variables:
│ obs (chain, draw, school) float64 102kB -4.492 -3.226 ... -3.269 -3.816
│ Attributes: (4)
├── Group: /sample_stats
│ Dimensions: (chain: 4, draw: 400)
│ Coordinates:
│ * chain (chain) int64 32B 0 1 2 3
│ * draw (draw) int64 3kB 100 101 102 103 ... 496 497 498 499
│ Data variables: (12/17)
│ step_size (chain, draw) float64 13kB 0.1427 0.1427 ... 0.1233
│ reached_max_treedepth (chain, draw) bool 2kB False False ... False False
│ perf_counter_start (chain, draw) float64 13kB 3.931e+04 ... 3.931e+04
│ energy_error (chain, draw) float64 13kB -0.9934 0.2091 ... 0.1372
│ perf_counter_diff (chain, draw) float64 13kB 0.0009694 ... 0.001483
│ tree_depth (chain, draw) int64 13kB 3 5 4 4 4 4 ... 4 4 4 4 4 4
│ ... ...
│ index_in_trajectory (chain, draw) int64 13kB 4 -8 -3 7 12 ... 9 13 7 -15
│ acceptance_rate (chain, draw) float64 13kB 0.9029 0.9153 ... 0.8901
│ smallest_eigval (chain, draw) float64 13kB nan nan nan ... nan nan
│ energy (chain, draw) float64 13kB 54.35 56.89 ... 60.07
│ diverging (chain, draw) bool 2kB False False ... False False
│ largest_eigval (chain, draw) float64 13kB nan nan nan ... nan nan
│ Attributes: (6)
├── Group: /prior
│ Dimensions: (chain: 1, draw: 400, school: 8)
│ Coordinates:
│ * chain (chain) int64 8B 0
│ * draw (draw) int64 3kB 100 101 102 103 104 105 ... 495 496 497 498 499
│ * school (school) <U16 512B 'Choate' 'Deerfield' ... 'Mt. Hermon'
│ Data variables:
│ theta (chain, draw, school) float64 26kB -9.565 2.252 ... 54.57 52.29
│ tau (chain, draw) float64 3kB 5.428 2.633 0.7054 ... 2.231 3.319 93.69
│ mu (chain, draw) float64 3kB 1.102 14.77 -7.669 ... -2.245 -2.435
│ Attributes: (4)
└── Group: /prior_predictive
Dimensions: (chain: 1, draw: 400, school: 8)
Coordinates:
* chain (chain) int64 8B 0
* draw (draw) int64 3kB 100 101 102 103 104 105 ... 495 496 497 498 499
* school (school) <U16 512B 'Choate' 'Deerfield' ... 'Mt. Hermon'
Data variables:
obs (chain, draw, school) float64 26kB -23.9 1.67 ... 58.23 39.78
Attributes: (4)If you check the object you will see that the groups posterior, posterior_predictive, log_likelihood, sample_stats, prior, and prior_predictive have now 400 draws compared to the original 500, while the group observed_data remains unaffected.
2.0.4 Combine chains and draws
Our primary goal usually is to obtain posterior samples and thus we aren’t concerned with chains and draws. In those cases, we can use the az.extract function. This combines the chain and draw into a sample coordinate which can make further operations easier. By default, az.extract works on the posterior, but you can specify other groups using the group argument.
<xarray.Dataset> Size: 209kB
Dimensions: (sample: 2000, school: 8)
Coordinates:
* sample (sample) object 16kB MultiIndex
* chain (sample) int64 16kB 0 0 0 0 0 0 0 0 0 0 0 ... 3 3 3 3 3 3 3 3 3 3 3
* draw (sample) int64 16kB 0 1 2 3 4 5 6 7 ... 493 494 495 496 497 498 499
* school (school) <U16 512B 'Choate' 'Deerfield' ... 'Mt. Hermon'
Data variables:
mu (sample) float64 16kB 1.716 1.903 1.903 1.903 ... 5.409 7.721 10.24
theta (school, sample) float64 128kB 2.317 0.8892 0.8892 ... 9.754 14.02
tau (sample) float64 16kB 0.8775 0.8027 0.8027 ... 2.236 2.99 3.052
Attributes: (7)You can achieve the same result using dt.posterior.stack(sample=("chain", "draw")). But extract can be more flexible because it takes care of the most common subsetting operations with MCMC samples. It can:
- Combine
chainsanddraws - Return a subset of variables (with optional filtering with regular expressions or string matching)
- Return a subset of samples. Moreover, by default, it returns a random subset to prevent getting non-representative samples due to bad mixing.
- Access any group
To get a subsample we can specify the number of samples we want with the num_samples argument. For instance, to get 100 samples we can write:
If you need to extract subsets from multiple groups, you should use a random seed. This will ensure that subsamples match. For example, if you do
You can inspect the samples in the posterior and ll variables and see that they match.
2.1 Add a new variable
We can add variables to existing groups. For instance, we may want to transform a parameter from the posterior. Like computing and adding the \(\log\) of the parameter \(\tau\) to the posterior group.
/tmp/ipykernel_108120/1785098962.py:1: FutureWarning: Deleting a single level of a MultiIndex is deprecated. Previously, this deleted all levels of a MultiIndex. Please also drop the following variables: {'sample'} to avoid an error in the future.
posterior["log_tau"] = np.log(posterior["tau"])
<xarray.Dataset> Size: 12kB
Dimensions: (sample: 100, school: 8)
Coordinates:
* sample (sample) object 800B MultiIndex
* chain (sample) int64 800B 3 1 0 2 1 0 2 1 3 1 3 ... 1 3 0 3 2 3 0 1 0 2 1
* draw (sample) int64 800B 80 443 428 346 215 341 ... 424 169 116 493 477
* school (school) <U16 512B 'Choate' 'Deerfield' ... 'Mt. Hermon'
Data variables:
mu (sample) float64 800B 1.769 1.749 2.423 7.356 ... 5.487 7.236 7.666
theta (school, sample) float64 6kB 0.756 8.756 9.273 ... 5.49 14.74 9.631
tau (sample) float64 800B 1.586 3.391 3.355 4.597 ... 1.298 8.146 2.599
log_tau (sample) float64 800B 0.4612 1.221 1.211 ... 0.2605 2.098 0.955
Attributes: (7)