|
41 | 41 | "source": [ |
42 | 42 | "## Overplotting\n", |
43 | 43 | "\n", |
44 | | - "Let's consider plotting data that comes from two categories, here plotted in blue and red as A and B below. When the two categories are overlaid, the result can be very different depending on which one is plotted first:" |
| 44 | + "Let's consider plotting data that comes from two categories, here plotted in blue and red as **A** and **B** below. When the two categories are overlaid, the result can be very different depending on which one is plotted first:" |
45 | 45 | ] |
46 | 46 | }, |
47 | 47 | { |
|
67 | 67 | "cell_type": "markdown", |
68 | 68 | "metadata": {}, |
69 | 69 | "source": [ |
70 | | - "Plots C and D shown the same distribution of points, yet they give a very different impression of which category is more common, which can lead to incorrect decisions based on this data. Actually, both are equally common in this case. The cause for this problem is simply occlusion:" |
| 70 | + "Plots **C** and **D** shown the same distribution of points, yet they give a very different impression of which category is more common, which can lead to incorrect decisions based on this data. Actually, both are equally common in this case. The cause for this problem is simply occlusion:" |
71 | 71 | ] |
72 | 72 | }, |
73 | 73 | { |
|
91 | 91 | "\n", |
92 | 92 | "## Saturation\n", |
93 | 93 | "\n", |
94 | | - "You can reduce problems with overplotting by using transparency, via the alpha parameter provided in most plotting programs. E.g. if alpha is 0.1, full brightness will be achieved only when 10 points overlap, reducing the effects of plot ordering but making it harder to see individual points:" |
| 94 | + "You can reduce problems with overplotting by using transparency/opacity, via the alpha parameter provided to control opacity in most plotting programs. E.g. if alpha is 0.1, full brightness will be achieved only when 10 points overlap, reducing the effects of plot ordering but making it harder to see individual points:" |
95 | 95 | ] |
96 | 96 | }, |
97 | 97 | { |
|
110 | 110 | "cell_type": "markdown", |
111 | 111 | "metadata": {}, |
112 | 112 | "source": [ |
113 | | - "Here C and D look fairly similar (as they should, since the distributions are identical), but there are still a few locations that have reached **saturation**, a problem that will occur when more than 10 points overlap. With multiple categories as here, saturation leads to overplotting problems, because only the last 10 points plotted will affect the final color. With a single category, saturation simply obscures differences in density. For instance, 10, 20, and 2000 points overlapping will all look the same visually, for alpha=0.1.\n", |
| 113 | + "Here **C** and **D** look fairly similar (as they should, since the distributions are identical), but there are still a few locations that have reached **saturation**, a problem that will occur when more than 10 points overlap. With multiple categories as here, saturation leads to overplotting problems, because only the last 10 points plotted will affect the final color. With a single category, saturation simply obscures differences in density. For instance, 10, 20, and 2000 points overlapping will all look the same visually, for alpha=0.1.\n", |
114 | 114 | "\n", |
115 | 115 | "The biggest problem with using alpha to avoid saturation is that the correct value depends on the dataset -- e.g. if there are more points overlapping, a manually adjusted alpha setting that worked well for a previous dataset will systematically misrepresent the new dataset:" |
116 | 116 | ] |
|
139 | 139 | "cell_type": "markdown", |
140 | 140 | "metadata": {}, |
141 | 141 | "source": [ |
142 | | - "Here C and D again look very different, yet represent the same distributions. The correct alpha also depends on the dot size, because that affects the amount of overlap. With smaller dots, C and D look more similar, but the dots are now difficult to see because they are too transparent for this size:" |
| 142 | + "Here **C** and **D** again look very different, yet represent the same distributions. The correct alpha also depends on the dot size, because that affects the amount of overlap. With smaller dots, **C** and **D** look more similar, but the dots are now difficult to see because they are too transparent for this size:" |
143 | 143 | ] |
144 | 144 | }, |
145 | 145 | { |
|
162 | 162 | "\n", |
163 | 163 | "## Binning problems\n", |
164 | 164 | "\n", |
165 | | - "For large enough datasets, plotting every point as above is not always practical. 2D histograms visualized as heatmaps offer a practical way to visualized data compactly, and can also address issues like saturation directly (by effectively auto-ranging the alpha parameter based on the bin with the highest count). Heatmaps can approximate a probability density function, averaging out noise or irrelevant variations to reveal an underlying distribution.\n", |
| 165 | + "For large enough datasets, plotting every point as above is not always practical. 2D histograms visualized as heatmaps offer a practical way to visualize data compactly, and can also address issues like saturation directly (by effectively auto-ranging the alpha parameter based on the bin with the highest count). Heatmaps can approximate a probability density function, averaging out noise or irrelevant variations to reveal an underlying distribution.\n", |
166 | 166 | "\n", |
167 | 167 | "Here, let's consider a sum of two normal distributions slightly offset from each other:" |
168 | 168 | ] |
|
175 | 175 | }, |
176 | 176 | "outputs": [], |
177 | 177 | "source": [ |
178 | | - "%%opts Image.Blues (cmap=\"Blues\") Image.Reds (cmap=\"Reds\") Image (cmap=\"Blues\") Image {+axiswise} Points (s=2)\n", |
| 178 | + "%%opts Image (cmap=\"Blues\") Image {+axiswise} Points (s=2)\n", |
179 | 179 | "\n", |
180 | | - "num=600\n", |
181 | | - "np.random.seed(42)\n", |
182 | | - "offset=1.5\n", |
183 | | - "blue_coords = (np.random.normal( offset,size=num), np.random.normal( offset*0,size=num))\n", |
184 | | - "red_coords = (np.random.normal(-offset,size=num), np.random.normal(-offset*0,size=num))\n", |
185 | | - "merged = (np.hstack((blue_coords[0],red_coords[0])),np.hstack((blue_coords[1],red_coords[1])))\n", |
| 180 | + "def distribution(num=100):\n", |
| 181 | + " np.random.seed(42)\n", |
| 182 | + " offset=1.5\n", |
| 183 | + " blue_coords = (np.random.normal( offset,size=num), np.random.normal( offset*0,size=num))\n", |
| 184 | + " red_coords = (np.random.normal(-offset,size=num), np.random.normal(-offset*0,size=num))\n", |
| 185 | + " merged = (np.hstack((blue_coords[0],red_coords[0])),np.hstack((blue_coords[1],red_coords[1])))\n", |
| 186 | + " return merged\n", |
186 | 187 | "\n", |
187 | 188 | "def heatmap(coords,bins=10):\n", |
188 | 189 | " hist= np.histogram2d(coords[0], coords[1], bins=bins)\n", |
189 | 190 | " return hv.Image(hist[0][:,::-1].T)\n", |
190 | 191 | "\n", |
191 | | - "(hv.Points(merged) + [heatmap(merged,bins) for bins in [8,10,20,100,1000]]).cols(3)" |
| 192 | + "dist=distribution(num=600)\n", |
| 193 | + "(hv.Points(dist) + [heatmap(dist,bins) for bins in [8,10,20,100,1000]]).cols(3)" |
| 194 | + ] |
| 195 | + }, |
| 196 | + { |
| 197 | + "cell_type": "markdown", |
| 198 | + "metadata": {}, |
| 199 | + "source": [ |
| 200 | + "As you can see, the distribution looks very different depending on the number of bins used -- at some heatmap resolutions, the two underlying groups can be distinguished, but in others the shape is unclear. In plot **F** the data is only dimly visible at all, due to the small pixel-sized bins that make multiple counts per bin unlikely. I.e., in **F** nearly all the bins look like they are empty, but in fact there are many non-empty bins, they just happen to have fewer dots than certain other bins that randomly happen to have high overlap between datapoints. This **undersaturation** problem, where values falsely appear to be zero (most of the white pixels in **F**) because of plotting parameter settings, can hide data just as seriously as **oversaturation**, leading to incorrect conclusions about the shape of the data. (The data is certainly not just a few dozen points as it appears in **F**!) Clearly, the bin size is now another important parameter that needs manual adjustment. \n", |
| 201 | + "\n", |
| 202 | + "A key insight is that these problems vary with the amount of data:" |
192 | 203 | ] |
193 | 204 | }, |
194 | 205 | { |
|
199 | 210 | }, |
200 | 211 | "outputs": [], |
201 | 212 | "source": [ |
202 | | - "hv.archive.export()" |
| 213 | + "%%opts Image (cmap=\"Blues\") Image {+axiswise} Points (s=2)\n", |
| 214 | + "\n", |
| 215 | + "dist=distribution(num=60000)\n", |
| 216 | + "(hv.Points(dist) + [heatmap(dist,bins) for bins in [8,10,20,100,1000]]).cols(3)" |
203 | 217 | ] |
204 | 218 | }, |
205 | 219 | { |
206 | 220 | "cell_type": "markdown", |
207 | 221 | "metadata": {}, |
208 | 222 | "source": [ |
209 | | - "As you can see, the distribution looks very different depending on the number of bins used -- at some heatmap resolutions, the two underlying groups can be distinguished, but in others the shape is unclear. In plot F the data is only dimly visible at all, due to the small pixel-sized bins that make multiple counts per bin unlikely. In F nearly all the bins look like they are empty, but in fact there are many non-empty bins, they just happen to have fewer dots than a few other random bins with high overlap between datapoints. This **undersaturation** problem, where values falsely appear to be zero because of plotting parameter settings, can hide data just as seriously as **oversaturation**, leading to incorrect conclusions about the shape of the data.\n", |
| 223 | + "As you can see, the points-based scatterplot (**A**) has gotten worse with more data -- overplotting now completely obscures the shape of the distribution. But most of the heatmap approaches improve with the additional data, because the extra points lead to a better approximation of the underlying distribution. Even **F** now conveys some information about the shape, though it's still suffering from undersaturation.\n", |
210 | 224 | "\n", |
211 | | - "Clearly, the bin size is now an important parameter that needs manual adjustment. Yet for truly large data, there's not usually any plot like A available where all datapoints can be seen -- how do you know when your bin size is appropriate for a given dataset? Usually the answer is \"trial and error\", which is awkward and time consuming for large data. The result is...\n", |
| 225 | + "Clearly, for truly large data, a scatterplot like **A** will rarely be useful or practical, so how does one know when the right bin size has been chosen, or whether there is enough data to reveal the distribution (as in **f**)? Usually the answer is \"trial and error\", which is awkward and time consuming for large data. The result is...\n", |
212 | 226 | "\n", |
213 | 227 | "## For big data, you don't know when the viz is lying\n", |
214 | 228 | "\n", |
215 | 229 | "I.e., visualization is supposed to help you explore and understand your data, but if your visualizations are systematically misrepresenting your data because of overplotting, saturation, undersaturation, and inappropriate binning, then you won't be able to discover the real qualities of your data and will be unable to make the right decisions.\n", |
216 | 230 | "\n", |
217 | 231 | "The [`datashader`](https://github.com/bokeh/datashader) library has been designed to overcome many of the above problems, by automatically calculating appropriate parameters based on the data itself, and by allowing interactive visualizations of even truly large datasets with millions or billions of data points so that their structure can be revealed." |
218 | 232 | ] |
| 233 | + }, |
| 234 | + { |
| 235 | + "cell_type": "code", |
| 236 | + "execution_count": null, |
| 237 | + "metadata": { |
| 238 | + "collapsed": false |
| 239 | + }, |
| 240 | + "outputs": [], |
| 241 | + "source": [ |
| 242 | + "hv.archive.export()" |
| 243 | + ] |
219 | 244 | } |
220 | 245 | ], |
221 | 246 | "metadata": { |
|
0 commit comments