File size: 9,203 Bytes
b9a0f21
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
# Dimensioned Containers

So far we've seen how to [wrap data in elements](./01-Annotating_Data.ipynb) and [compose](./02-Composing_Elements.ipynb) those Elements into Overlays and Layout. In this guide will see how we can use containers to hold Elements and declare parameter spaces to explore multi-dimensional parameter spaces visually. These containers allow faceting your data by one or more variables and exploring the resulting parameter space with widgets, positioning plots on a grid or simply laying them out consecutively.  Here we will introduce the ``HoloMap``, ``NdOverlay``, ``NdLayout`` and ``GridSpace``, which make all this possible.


```python
import numpy as np
import holoviews as hv

from holoviews import opts

hv.extension('bokeh')

opts.defaults(opts.Curve(line_width=1))
```

### Declaring n-dimensional collections

Python users will be familiar with dictionaries as a way to collect data together in a conveniently accessible manner. Unlike NumPy arrays, dictionaries are sparse and do not have to be declared with a fixed size. The dimensioned types are therefore closely modeled on dictionaries but support n-dimensional keys.

Therefore they allow you to express a mapping between a multi-variable key and other viewable objects, letting you structure your multi-dimensional data to facilitate exploration. The key here can represent anything from an ID, a filename, a parameter controlling some custom processing to some category representing a subset of your data.

As a simple example we will use a function which varies with multiple parameters, in this case a function which generates a simple frequency modulation signal, with two main parameters a carrier frequency and a modulation frequency (see [Wikipedia](https://en.wikipedia.org/wiki/Frequency_modulation)). All we have to know here is that the function generates a ``Curve`` based on the parameters that we give it.


```python
def fm_modulation(f_carrier=220, f_mod=220, mod_index=1, length=0.1, sampleRate=2000):
    sampleInc = 1.0/sampleRate
    x = np.arange(0,length, sampleInc)
    y = np.sin(2*np.pi*f_carrier*x + mod_index*np.sin(2*np.pi*f_mod*x))
    return hv.Curve((x, y), 'Time', 'Amplitude')
```

Next we have to declare the parameter space we want to explore. Combinatorial parameter spaces can quickly get very large so here we will declare just 5 carrier frequencies and 5 modulation frequencies. If we have many more parameters or a larger set of values we could instead use a ``DynamicMap``, which does lazy evaluation and is introduced in the next section on [Live Data](./07-Live_Data.ipynb).

However when working with relatively small datasets a ``HoloMap`` is preferred as ``HoloMap``s can be exported to static HTML. To declare the data we use a dictionary comprehension to compute an FM modulation curve for each combination of carrier and modulation frequencies and then pass that dictionary to a newly declared ``HoloMap``, which declares our two parameters as key dimensions. If we want to customize the starting value of the widgets a ``default`` value may be supplied on the ``Dimension`` objects:


```python
f_carrier = np.linspace(20, 60, 3)
f_mod = np.linspace(20, 100, 5)

curve_dict = {(fc, fm): fm_modulation(fc, fm) for fc in f_carrier for fm in f_mod}

kdims = [hv.Dimension(('f_carrier', 'Carrier frequency'), default=40),
         hv.Dimension(('f_mod', 'Modulation frequency'), default=60)]
holomap = hv.HoloMap(curve_dict, kdims=kdims)
holomap.opts(opts.Curve(width=600))
```

Any numeric key dimension values will automatically generate sliders while any non-numeric value will present you with a dropdown widget to select between the values. A HoloMap is therefore an easy way to quickly explore a parameter space. Since only one ``Curve`` is on the screen at the same time it is difficult to compare the data, we can however easily facet the data in other ways.

## Casting between n-dimensional containers

Exploring a parameter space using widgets is one of the most flexible approaches but as we noted above it also makes it difficult to compare between different parameter values. HoloViews therefore supplies several container objects, which behave similarly to a ``HoloMap`` but have a different visual representation:

* NdOverlay - An n-dimensional container which overlays the elements
* NdLayout  - An n-dimensional container which displays the data in separate plot axes and adds titles for each value
* GridSpace - A 1D or 2D container which lays out up to two dimensions on a grid.

Since all these classes share the same baseclass we can trivially cast between them, e.g. we can cast the ``HoloMap`` to a ``GridSpace``.


```python
grid = hv.GridSpace(holomap)
grid.opts(
    opts.GridSpace(plot_size=75),
    opts.Curve(width=100))
```

Similarly we can select just a few values and lay the data out in an ``NdLayout``:


```python
ndlayout = hv.NdLayout(grid[20, 20:81])
ndlayout.opts(opts.Curve(width=500, height=200)).cols(2)
```

## Faceting by dimension

Casting between container types as we did above allows us to facet all dimensions but often it is more desirable to facet a specific dimension in some way. We may for example want to overlay the carrier frequencies, while still having a slider vary the modulation frequencies. For this purpose ``HoloMap`` and ``DynamicMap`` have ``.overlay``, ``.grid`` and ``.layout`` methods, which accept one or more dimensions as input, e.g. here we overlay the carrier frequencies:


```python
holomap.overlay('f_carrier').opts(width=600)
```

We can chain these faceting operations but we have to be careful about the order. We should always ``overlay`` first and only then use ``.grid`` or ``.layout`` . To better understand why that is, look at the [Building Composite objects](./06-Building_Composite_Objects.ipynb) guide, for now it suffices to say that objects are built to nest in a specific order. Here we will ``overlay`` the carrier frequencies and ``grid`` the modulation frequency:


```python
gridspace = holomap.overlay('f_carrier').grid('f_mod')
gridspace.opts(opts.Curve(width=200,height=200))
```

This ability to facet data in different ways becomes incredibly useful when working with [tabular](./08-Tabular_Datasets.ipynb) and [gridded](./09-Gridded_Datasets.ipynb) datasets, which vary by multiple dimensions. By faceting the data by one or more dimensions and utilizing the wide range of elements we can quickly explore huge and complex datasets particularly when working with using ``DynamicMap``, which allows you to define dynamic and lazy [data processing pipelines](./14-Data_Pipelines.ipynb), which visualize themselves and allow you to build complex interactive dashboards.

## Methods

### Selecting by value

Just like ``Elements`` dimensioned containers can be sliced and indexed by value using the regular indexing syntax and the ``select`` method. As a simple example we can index the carrier frequency 20 and modulation frequency 40 using both the indexing and select syntax:


```python
layout = holomap[20, 40] + holomap.select(f_carrier=20, f_mod=40)
layout.opts(opts.Curve(width=400))
```

For more detail on indexing both Elements and containers see the [Indexing and Selecting user guide](./10-Indexing_and_Selecting_Data.ipynb).

### Collapsing an n-dimensional container

If we inspect one of the containers we created in the examples above we can clearly see the structure and the dimensionality of each container and the underlying ``Curve`` elements:


```python
print(grid)
```

Since this simply represents a multi-dimensional parameter space we can collapse this datastructure into a single `Dataset` containing columns for each of the dimensions we have declared:


```python
ds = grid.collapse()
ds.data.head()
```

A ``Dataset`` is an element type which does not itself have a visual representation and may contain any type of data supported by HoloViews interfaces.

### Reindexing containers

Something we might want to do quite frequently is to change the order of dimensions in a dimensioned container. Let us inspect the key dimensions on our HoloMap from above:


```python
holomap.kdims
```

Using the ``reindex`` method we can easily change the order of dimensions:


```python
reindexed = holomap.reindex(['f_mod', 'f_carrier'])
reindexed.kdims
```

### Add dimensions to a container

Another common operation is adding a new dimension to a HoloMap, which can be useful when you want to merge two HoloMaps which vary by yet another dimension. The ``add_dimension`` method takes the new dimension name, the position in the list of key dimensions and the actual value as arguments:


```python
new_holomap = holomap.add_dimension('New dimension', dim_pos=0, dim_val=1)
new_holomap.kdims
```

As you can see the 'New dimension' was added at position zero.

## Onwards

As we have seen Dimensioned containers make it easy to explore multi-dimensional datasets by mapping the dimensions onto visual layers (`NdOverlay`), 2D grid axes (`GridSpace`), widgets (`HoloMap`) or an arbitrary layout (`NdLayout`). In the [next section](06-Building_Composite_Objects.ipynb) we will discover how to construct composite objects of heterogeneous data.