From a29630949d928574aa7396632f5b95da30ad601d Mon Sep 17 00:00:00 2001 From: Joel Ostblom Date: Thu, 2 Feb 2023 13:42:02 -0800 Subject: [PATCH 01/44] Replace ggplot terminology --- source/intro.md | 2 +- source/viz.md | 44 ++++++++++++++++++++++---------------------- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/source/intro.md b/source/intro.md index a4f51df2..c5a32ac1 100644 --- a/source/intro.md +++ b/source/intro.md @@ -806,7 +806,7 @@ import altair as alt The fundamental object in `altair` is the `Chart`, which takes a data frame as a single argument: `alt.Chart(ten_lang)`. With a chart object in hand, we can now specify how we would like the data to be visualized. -We first indicate what kind of geometric mark we want to use to represent the data. Here we set the mark attribute +We first indicate what kind of graphical *mark* we want to use to represent the data. Here we set the mark attribute of the chart object using the `Chart.mark_bar` function, because we want to create a bar chart. Next, we need to encode the variables of the data frame using the `x` (represents the x-axis position of the points) and diff --git a/source/viz.md b/source/viz.md index 297dd7ad..265d5e42 100644 --- a/source/viz.md +++ b/source/viz.md @@ -47,12 +47,12 @@ By the end of the chapter, readers will be able to do the following: - Given a visualization and a question, evaluate the effectiveness of the visualization and suggest improvements to better answer the question. - Referring to the visualization, communicate the conclusions in non-technical terms. - Identify rules of thumb for creating effective visualizations. -- Define the two key aspects of altair objects: - - mark objects - - encodings +- Define the two key aspects of altair charts: + - graphical marks + - encoding channels - Use the altair library in Python to create and refine the above visualizations using: - - mark objects: `mark_point`, `mark_line`, `mark_bar` - - encodings : `x`, `y`, `fill`, `color`, `shape` + - graphical marks: `mark_point`, `mark_line`, `mark_bar` + - encoding channels: `x`, `y`, `fill`, `color`, `shape` - subplots: `facet` - Describe the difference in raster and vector output formats. - Use `chart.save()` to save visualizations in `.png` and `.svg` format. @@ -85,7 +85,7 @@ As with most coding tasks, it is totally fine (and quite common) to make mistakes and iterate a few times before you find the right visualization for your data and question. There are many different kinds of plotting graphics available to use (see Chapter 5 of *Fundamentals of Data Visualization* {cite:p}`wilkeviz` for a directory). -The types of plot that we introduce in this book are shown in {numref}`plot_sketches`; +The types of plots that we introduce in this book are shown in {numref}`plot_sketches`; which one you should select depends on your data and the question you want to answer. In general, the guiding principles of when to use each type of plot @@ -255,20 +255,20 @@ Scatter plots show the data as individual points with `x` (horizontal axis) and `y` (vertical axis) coordinates. Here, we will use the measurement date as the `x` coordinate and the CO$_{\text{2}}$ concentration as the `y` coordinate. -We create a plot object with the `alt.Chart()` function. +We create a chart with the `alt.Chart()` function. There are a few basic aspects of a plot that we need to specify: -```{index} altair; geometric object, altair; geometric encoding, geometric object, geometric encoding +```{index} altair; graphical mark, altair; encoding channel ``` -- The name of the **data frame** object to visualize. +- The name of the **data frame** to visualize. - Here, we specify the `co2_df` data frame as an argument to `alt.Chart` -- The **geometric object**, which specifies how the mapped data should be displayed. - - To create a geometric object, we use `Chart.mark_*` methods (see the +- The **graphical mark**, which specifies how the mapped data should be displayed. + - To create a graphical mark, we use `Chart.mark_*` methods (see the [altair reference](https://altair-viz.github.io/user_guide/marks.html) - for a list of geometric objects). + for a list of graphical mark). - Here, we use the `mark_point` function to visualize our data as a scatter plot. -- The **geometric encoding**, which tells `altair` how the columns in the data frame map to properties of the visualization. +- The **encoding channels**, which tells `altair` how the columns in the data frame map to visual properties in the chart. - To create an encoding, we use the `encode` function. - The `encode` method builds a key-value mapping between encoding channels (such as x, y) to fields in the dataset, accessed by field name (column names) - Here, we set the `x` axis of the plot to the `date_measured` variable, @@ -466,7 +466,7 @@ In our data visualization, this would be when we map data to the axes in the `encode` function. Then we add our key visual subjects to the painting. In our data visualization, -this would be the geometric objects (e.g., `mark_point`, `mark_line`, etc.). +this would be the graphical marks (e.g., `mark_point`, `mark_line`, etc.). And finally, we work on adding details and refinements to the painting. In our data visualization this would be when we fine tune axis labels, change the font, adjust the point size, and do other related things. @@ -504,7 +504,7 @@ neither of the variables here have a natural order to them. So a scatter plot is likely to be the most appropriate visualization. Let's create a scatter plot using the `altair` package with the `waiting` variable on the horizontal axis, the `eruptions` -variable on the vertical axis, and the `mark_point` geometric object. +variable on the vertical axis, and `mark_point` as the graphical mark. By default, `altair` draws only the outline of each point. If we would like to fill them in, we pass the argument `filled=True` to `mark_point`. In place of `mark_point(filled=True)`, we can also use `mark_circle`. @@ -996,7 +996,7 @@ Scatter plot of percentage of Canadians reporting a language as their mother ton In {numref}`can_lang_plot_legend`, the points are colored with the default `altair` color palette. This is an appropriate choice for most situations. In Altair, there are many themes available, which can be viewed [in the documentation](https://altair-viz.github.io/user_guide/customization.html#customizing-colors). To change the color scheme, -we add the `scheme` argument in the `scale` of the `color` argument in `altair` layer indicating the palette we want to use. +we add the `scheme` argument in the `scale` of the `color` encoding to indicate the palette we want to use. ```{index} color palette; color blindness simulator ``` @@ -1298,8 +1298,8 @@ helps us visualize how a particular variable is distributed in a data set by separating the data into bins, and then using vertical bars to show how many data points fell in each bin. -To create a histogram in `altair` we will use the `mark_bar` geometric -object, setting the `x` axis to the `Speed` measurement variable and `y` axis to `"count()"`. +To create a histogram in `altair` we will use the `mark_bar` graphical +mark, setting the `x` axis to the `Speed` measurement variable and `y` axis to `"count()"`. There is no `"count()"` column-name in `morley_df`; we use `"count()"` to tell `altair` that we want to count the number of values in the `Speed` column in each bin. As usual, @@ -1324,7 +1324,7 @@ glue("morley_hist", morley_hist, display=False) Histogram of Michelson's speed of light data. ::: -#### Adding layers to an `altair` plot object +#### Adding layers to an `altair` chart ```{index} altair; +; mark_rule ``` @@ -1356,8 +1356,8 @@ To add the dashed line on top of the histogram, we using the `+` operator. Adding features to a plot using the `+` operator is known as *layering* in `altair`. This is a very powerful feature of `altair`; you -can continue to iterate on a single plot object, adding and refining -one layer at a time. If you stored your plot as a named object +can continue to iterate on a single chart, adding and refining +one layer at a time. If you stored your chart as a variable using the assignment symbol (`=`), you can add to it using the `+` operator. Below we add a vertical line created using `mark_rule` to the last plot we created, `morley_hist`, using the `+` operator. @@ -1614,7 +1614,7 @@ experiments did quite an admirable job given the technology available at the tim When you create a histogram in `altair`, by default, it tries to choose a reasonable number of bins. Naturally, this is not always the right number to use. You can set the number of bins yourself by using -the `maxbins` argument in the `mark_bar` geometric object. +the `maxbins` argument in the `mark_bar` graphical object. But what number of bins is the right one to use? Unfortunately there is no hard rule for what the right bin number From acbed46db9287f4ecc2c81c77c3b820bcb96401b Mon Sep 17 00:00:00 2001 From: Joel Ostblom Date: Thu, 2 Feb 2023 13:43:21 -0800 Subject: [PATCH 02/44] Correct explanation of maxbins --- source/viz.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/viz.md b/source/viz.md index 265d5e42..489a1457 100644 --- a/source/viz.md +++ b/source/viz.md @@ -1614,7 +1614,7 @@ experiments did quite an admirable job given the technology available at the tim When you create a histogram in `altair`, by default, it tries to choose a reasonable number of bins. Naturally, this is not always the right number to use. You can set the number of bins yourself by using -the `maxbins` argument in the `mark_bar` graphical object. +the `maxbins` argument inside `alt.Bin`. But what number of bins is the right one to use? Unfortunately there is no hard rule for what the right bin number From c78b6596350f1aa7e73240ddc48818ce2f6446f5 Mon Sep 17 00:00:00 2001 From: Joel Ostblom Date: Thu, 2 Feb 2023 13:44:27 -0800 Subject: [PATCH 03/44] Clarify language and use altair terminology --- source/intro.md | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/source/intro.md b/source/intro.md index c5a32ac1..5615f9c5 100644 --- a/source/intro.md +++ b/source/intro.md @@ -804,13 +804,12 @@ import altair as alt +++ -The fundamental object in `altair` is the `Chart`, which takes a data frame as a single argument: `alt.Chart(ten_lang)`. +The fundamental object in `altair` is the `Chart`, which takes a data frame as an argument: `alt.Chart(ten_lang)`. With a chart object in hand, we can now specify how we would like the data to be visualized. We first indicate what kind of graphical *mark* we want to use to represent the data. Here we set the mark attribute of the chart object using the `Chart.mark_bar` function, because we want to create a bar chart. -Next, we need to encode the variables of the data frame using -the `x` (represents the x-axis position of the points) and -`y` (represents the y-axis position of the points) *channels*. We use the `encode()` +Next, we need to *encode* the variables of the data frame using +the `x` and `y` *channels* (which represent the x-axis and y-axis position of the points). We use the `encode()` function to handle this: we specify that the `language` column should correspond to the x-axis, and that the `mother_tongue` column should correspond to the y-axis. @@ -853,7 +852,7 @@ Bar plot of the ten Aboriginal languages most often reported by Canadian residen ```{index} see: .; chaining methods ``` -### Formatting `altair` objects +### Formatting `altair` charts It is exciting that we can already visualize our data to help answer our question, but we are not done yet! We can (and should) do more to improve the @@ -873,7 +872,7 @@ Canadian Residents)" would be much more informative. Adding additional labels to our visualizations that we create in `altair` is one common and easy way to improve and refine our data visualizations. We can add titles for the axes in the `altair` objects using `alt.X` and `alt.Y` with the `title` argument to make -the axes titles more informative. +the axes titles more informative (you will learn more about `alt.X` and `alt.Y` in the visualization chapter). Again, since we are specifying words (e.g. `"Mother Tongue (Number of Canadian Residents)"`) as arguments to `alt.X` and `alt.Y`, we surround them with double quotation marks. We can do many other modifications From dd6c49df5fcfaf46fa724aa36dbb4fccf3acdaba Mon Sep 17 00:00:00 2001 From: Joel Ostblom Date: Thu, 2 Feb 2023 13:44:44 -0800 Subject: [PATCH 04/44] Make use of parenthesis consistent within the chapter --- source/intro.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/source/intro.md b/source/intro.md index 5615f9c5..8feca2a2 100644 --- a/source/intro.md +++ b/source/intro.md @@ -884,8 +884,8 @@ barplot_mother_tongue = ( .mark_bar().encode( x=alt.X('language', title='Language'), y=alt.Y('mother_tongue', title='Mother Tongue (Number of Canadian Residents)') - )) - + ) +) ``` @@ -919,8 +919,8 @@ barplot_mother_tongue_axis = ( .mark_bar().encode( x=alt.X('mother_tongue', title='Mother Tongue (Number of Canadian Residents)'), y=alt.Y('language', title='Language') - )) - + ) +) ``` ```{code-cell} ipython3 @@ -955,8 +955,8 @@ ordered_barplot_mother_tongue = ( .mark_bar().encode( x=alt.X('mother_tongue', title='Mother Tongue (Number of Canadian Residents)'), y=alt.Y('language', sort='x', title='Language') - )) - + ) +) ``` +++ From 456473a8824375cabe9612310efb792710a11016 Mon Sep 17 00:00:00 2001 From: Joel Ostblom Date: Thu, 2 Feb 2023 14:51:19 -0800 Subject: [PATCH 05/44] Consistently use multiline syntax --- source/viz.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/source/viz.md b/source/viz.md index 489a1457..5eb4f844 100644 --- a/source/viz.md +++ b/source/viz.md @@ -169,7 +169,7 @@ understand and remember your message quickly. ``` This section will cover examples of how to choose and refine a visualization given a data set and a question that you want to answer, -and then how to create the visualization in Python using `altair`. To use the `altair` package, we need to import the `altair` package. We will also import `pandas` to use for reading in the data. +and then how to create the visualization in Python using `altair`. To use the `altair` package, we need to first import it. We will also import `pandas` to use for reading in the data. ```{code-cell} ipython3 import pandas as pd @@ -217,7 +217,8 @@ To get started, we will read and inspect the data: ```{code-cell} ipython3 # mauna loa carbon dioxide data co2_df = pd.read_csv( - "data/mauna_loa_data.csv", parse_dates=['date_measured'] + "data/mauna_loa_data.csv", + parse_dates=['date_measured'] ) co2_df ``` @@ -1117,7 +1118,8 @@ The result is shown in {numref}`islands_bar`. ```{code-cell} ipython3 islands_bar = alt.Chart(islands_df).mark_bar().encode( - x="landmass", y="size" + x="landmass", + y="size" ) ``` @@ -1152,7 +1154,8 @@ swapping the `x` and `y` variables. islands_top12 = islands_df.nlargest(12, "size") islands_bar_top = alt.Chart(islands_top12).mark_bar().encode( - x="size", y="landmass" + x="size", + y="landmass" ) ``` From 57dcac37c676c9f7f3238de0475e81024d8c7836 Mon Sep 17 00:00:00 2001 From: Joel Ostblom Date: Thu, 2 Feb 2023 14:51:56 -0800 Subject: [PATCH 06/44] Explain alt.X and alt.Y and be consistent in using it only when there are no options passed --- source/classification1.md | 4 ++-- source/regression2.md | 2 +- source/viz.md | 46 ++++++++++++++++++++++----------------- 3 files changed, 29 insertions(+), 23 deletions(-) diff --git a/source/classification1.md b/source/classification1.md index a3497335..a4de3695 100644 --- a/source/classification1.md +++ b/source/classification1.md @@ -1857,8 +1857,8 @@ prediction_plot = ( alt.Chart(prediction_table) .mark_point(opacity=0.05, filled=True, size=300) .encode( - x=alt.X("Area"), - y=alt.Y("Smoothness"), + x="Area", + y="Smoothness", color=alt.Color("Class", title="Diagnosis"), ) ) diff --git a/source/regression2.md b/source/regression2.md index 2ffd3f81..b80d0dbb 100644 --- a/source/regression2.md +++ b/source/regression2.md @@ -274,7 +274,7 @@ for i in range(len(slope_l)): ) ) .mark_line(color=line_color_l[i]) - .encode(x=alt.X("x"), y=alt.Y("y")) + .encode(x="x", y="y") ) several_lines_plot diff --git a/source/viz.md b/source/viz.md index 5eb4f844..d5ffec33 100644 --- a/source/viz.md +++ b/source/viz.md @@ -265,23 +265,29 @@ There are a few basic aspects of a plot that we need to specify: - The name of the **data frame** to visualize. - Here, we specify the `co2_df` data frame as an argument to `alt.Chart` - The **graphical mark**, which specifies how the mapped data should be displayed. - - To create a graphical mark, we use `Chart.mark_*` methods (see the - [altair reference](https://altair-viz.github.io/user_guide/marks.html) - for a list of graphical mark). + - To create a graphical mark, we use `Chart.mark_*` methods (see the + [altair reference](https://altair-viz.github.io/user_guide/marks.html) + for a list of graphical mark). - Here, we use the `mark_point` function to visualize our data as a scatter plot. - The **encoding channels**, which tells `altair` how the columns in the data frame map to visual properties in the chart. - To create an encoding, we use the `encode` function. - The `encode` method builds a key-value mapping between encoding channels (such as x, y) to fields in the dataset, accessed by field name (column names) - - Here, we set the `x` axis of the plot to the `date_measured` variable, - and on the `y` axis, we plot the `ppm` variable. We use `alt.X` and - `alt.Y` which allow you to control properties of the `x` and `y` axes. - - For the y-axis, we also provided the argument - `scale=alt.Scale(zero=False)`. By default, `altair` chooses the y-limits - based on the data and will keep `y=0` in view. That would make it - difficult to see any trends in our data since the smallest value is >300 - ppm. So by providing `scale=alt.Scale(zero=False)`, we tell altair to - choose a reasonable lower bound based on our data, and that lower bound - doesn't have to be zero. + - Here, we set the `x` axis of the plot to the `date_measured` variable, + and on the `y` axis, we plot the `ppm` variable. + - For the y-axis, we also provided the argument + `scale=alt.Scale(zero=False)`. By default, `altair` chooses the y-limits + based on the data and will keep `y=0` in view. + This is often a helpful default, but here it makes it + difficult to see any trends in our data since the smallest value is >300 + ppm. So by providing `scale=alt.Scale(zero=False)`, we tell altair to + choose a reasonable lower bound based on our data, and that lower bound + doesn't have to be zero. + - To change the properties of the encoding channels, + we need to leverage the helper functions `alt.Y` and `alt.X`. + These helpers have the role of customizing things like order, titles, and scales. + Here, we use `alt.Y` to change the domain of the y-axis, + so that it starts from the lowest value in the `date_measured` column + rather than from zero. ```{code-cell} ipython3 :tags: ["remove-cell"] @@ -290,7 +296,7 @@ from myst_nb import glue ```{code-cell} ipython3 co2_scatter = alt.Chart(co2_df).mark_point().encode( - x=alt.X("date_measured"), + x="date_measured", y=alt.Y("ppm", scale=alt.Scale(zero=False)) ) ``` @@ -335,7 +341,7 @@ with just the default arguments: ```{code-cell} ipython3 co2_line = alt.Chart(co2_df).mark_line().encode( - x=alt.X("date_measured"), + x="date_measured", y=alt.Y("ppm", scale=alt.Scale(zero=False)) ) ``` @@ -1310,8 +1316,8 @@ let's use the default arguments just to see how things look. ```{code-cell} ipython3 morley_hist = alt.Chart(morley_df).mark_bar().encode( - x=alt.X("Speed"), - y=alt.Y("count()") + x="Speed", + y="count()" ) ``` @@ -1405,9 +1411,9 @@ to make the bars slightly translucent. ```{code-cell} ipython3 morley_hist_colored = alt.Chart(morley_df).mark_bar(opacity=0.5).encode( - x=alt.X("Speed"), - y=alt.Y("count()"), - color=alt.Color("Expt") + x="Speed", + y="count()", + color="Expt" ) morley_hist_colored = morley_hist_colored + v_line From a4d6edd6fc0c8c4d1cd3303f573bdf93040ef7a4 Mon Sep 17 00:00:00 2001 From: Joel Ostblom Date: Thu, 2 Feb 2023 15:17:52 -0800 Subject: [PATCH 07/44] Simplify plot by extending the range of the data instead of manipulating the axis ticks --- source/viz.md | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/source/viz.md b/source/viz.md index d5ffec33..f5e0c892 100644 --- a/source/viz.md +++ b/source/viz.md @@ -412,26 +412,21 @@ visualization slightly. Note that it is totally fine to use a small number of visualizations to answer different aspects of the question you are trying to answer. We will accomplish this by using *scale*, another important feature of `altair` that easily transforms the different -variables and set limits. We scale the horizontal axis using the `alt.Scale(domain=['1990', '1993'])` by restricting the x-axis values between 1990 and 1994, -and the vertical axis with the `alt.Scale(zero=False)` function, to not start the y-axis with zero. +variables and set limits. In particular, here, we will use the `alt.Scale` function to zoom in -on just five years of data (say, 1990-1994). The +on just a few years of data (say, 1990-1995). The `domain` argument takes a list of length two to specify the upper and lower bounds to limit the axis. We also added the argument `clip=True` to `mark_line`. This tells `altair` -to "clip" the data outside of the domain that we set so that it doesn't +to "clip" (remove) the data outside of the specified domain that we set so that it doesn't extend past the plot area. -Finally, we will use `axis=alt.Axis(tickCount=4)` to add the lines corresponding to each -year in the background to create the final visualization. This helps us to -better visualise the change with each year. ```{code-cell} ipython3 co2_line_scale = alt.Chart(co2_df).mark_line(clip=True).encode( x=alt.X( "date_measured", - title="Measurement Date", - axis=alt.Axis(tickCount=4), - scale=alt.Scale(domain=['1990', '1994']) + scale=alt.Scale(domain=['1990', '1995']), + title="Measurement Date" ), y=alt.Y( "ppm", @@ -450,7 +445,7 @@ glue('co2_line_scale', co2_line_scale, display=False) :figwidth: 700px :name: co2_line_scale -Line plot of atmospheric concentration of CO$_{2}$ from 1990 to 1994. +Line plot of atmospheric concentration of CO$_{2}$ from 1990 to 1995. ::: Interesting! It seems that each year, the atmospheric CO$_{\text{2}}$ increases From 8a95df1a6413031fe5c60e2a6cbaeb8f06099486 Mon Sep 17 00:00:00 2001 From: Joel Ostblom Date: Thu, 2 Feb 2023 17:13:56 -0800 Subject: [PATCH 08/44] Clarify the differences between mark_shape and mark_circle and introduce them explicitly --- source/viz.md | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/source/viz.md b/source/viz.md index f5e0c892..921efee5 100644 --- a/source/viz.md +++ b/source/viz.md @@ -455,8 +455,6 @@ there are two seasons: summer from May through October, and winter from November through April. Therefore, the oscillating pattern in CO$_{\text{2}}$ matches up fairly closely with the two seasons. - - A useful analogy to constructing a data visualization is painting a picture. We start with a blank canvas, and the first thing we do is prepare the surface @@ -507,13 +505,10 @@ So a scatter plot is likely to be the most appropriate visualization. Let's create a scatter plot using the `altair` package with the `waiting` variable on the horizontal axis, the `eruptions` variable on the vertical axis, and `mark_point` as the graphical mark. -By default, `altair` draws only the outline of each point. If we would -like to fill them in, we pass the argument `filled=True` to `mark_point`. In -place of `mark_point(filled=True)`, we can also use `mark_circle`. The result is shown in {numref}`faithful_scatter`. ```{code-cell} ipython3 -faithful_scatter = alt.Chart(faithful).mark_point(filled=True).encode( +faithful_scatter = alt.Chart(faithful).mark_point().encode( x="waiting", y="eruptions" ) @@ -540,7 +535,7 @@ In order to refine the visualization, we need only to add axis labels and make the font more readable. ```{code-cell} ipython3 -faithful_scatter_labels = alt.Chart(faithful).mark_circle().encode( +faithful_scatter_labels = alt.Chart(faithful).mark_point().encode( x=alt.X("waiting", title="Waiting Time (mins)"), y=alt.Y("eruptions", title="Eruption Duration (mins)") ) @@ -562,7 +557,7 @@ Scatter plot of waiting time and eruption time with clearer axes and labels. We can change the size of the point and color of the plot by specifying `mark_circle(size=10, color="black")`. ```{code-cell} ipython3 -faithful_scatter_labels_black = alt.Chart(faithful).mark_circle(size=10, color="black").encode( +faithful_scatter_labels_black = alt.Chart(faithful).mark_point(size=10, color="black").encode( x=alt.X("waiting", title="Waiting Time (mins)"), y=alt.Y("eruptions", title="Eruption Duration (mins)") ) @@ -611,14 +606,22 @@ can_lang ```{code-cell} ipython3 :tags: ["remove-cell"] -can_lang = can_lang[(can_lang['most_at_home']>0) & (can_lang['mother_tongue']>0)] +can_lang = can_lang[(can_lang['most_at_home'] > 0) & (can_lang['mother_tongue'] > 0)] ``` ```{index} altair; mark_circle ``` We will begin with a scatter plot of the `mother_tongue` and `most_at_home` columns from our data frame. -The resulting plot is shown in {numref}`can_lang_plot` +As you have seen in the scatter plots in the previous section +the default behavior of `mark_point` is to draw the outline of each point. +If we would like to fill them in, +we can pass the argument `filled=True` to `mark_point` +or use the shortcut `mark_circle`. +Whether to fill points or not is mostly a matter of personal preferences, +although hollow points can make it easier to see individual points +when there are many overlapping points in a chart. +The resulting plot is shown in {numref}`can_lang_plot`. ```{code-cell} ipython3 can_lang_plot = alt.Chart(can_lang).mark_circle().encode( @@ -627,7 +630,6 @@ can_lang_plot = alt.Chart(can_lang).mark_circle().encode( ) ``` - ```{code-cell} ipython3 :tags: ["remove-cell"] glue('can_lang_plot', can_lang_plot, display=False) @@ -1009,12 +1011,14 @@ We also set the `shape` aesthetic mapping to the `category` variable as well; this makes the scatter point shapes different for each category. This kind of visual redundancy—i.e., conveying the same information with both scatter point color and shape—can further improve the clarity and accessibility of your visualization. +Note that we are switching back to the use of `mark_point` here +since `mark_circle` does not support the `shape` encoding +and will always show up as a filled circle. + You can use this [color blindness simulator](https://www.color-blindness.com/coblis-color-blindness-simulator/) to check if your visualizations are color-blind friendly. The default color palattes in `altair` are color-blind friendly (one more reason to stick with the defaults!). -Note that we are switching back to the use of `mark_point` so that -we can specify the `shape` attribute. This cannot be done with `mark_circle`. ```{code-cell} ipython3 From 91e65ab7aaec0a2e7c1f5bb7f0eb1871d73112c7 Mon Sep 17 00:00:00 2001 From: Joel Ostblom Date: Thu, 2 Feb 2023 17:18:00 -0800 Subject: [PATCH 09/44] Clarify explanation of multiline titles --- source/viz.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/source/viz.md b/source/viz.md index 921efee5..a5f0d5ef 100644 --- a/source/viz.md +++ b/source/viz.md @@ -648,10 +648,12 @@ Scatter plot of number of Canadians reporting a language as their mother tongue To make an initial improvement in the interpretability of {numref}`can_lang_plot`, we should replace the default axis -names with more informative labels. We can add a line break in -the axis names so that some of the words are printed on a new line. This will -make the axes labels on the plots more readable. To do this, we pass the title as a list. Each element of the list will be on a new line. -We should also increase the font size to further +names with more informative labels. +To make the axes labels on the plots more readable, +we can print long labels over multiple lines. +To achieve this, we specify the title as a list of strings +where each string in the list will correspond to a new line of text. +We can also increase the font size to further improve readability. ```{code-cell} ipython3 From 3f50ead6eccb747b55b9f0c5011f256a316db803 Mon Sep 17 00:00:00 2001 From: Joel Ostblom Date: Thu, 2 Feb 2023 17:19:12 -0800 Subject: [PATCH 10/44] Emphasize operation by putting it first --- source/viz.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/source/viz.md b/source/viz.md index a5f0d5ef..dcae7f78 100644 --- a/source/viz.md +++ b/source/viz.md @@ -719,8 +719,8 @@ to Canada's two official languages by filtering the data: ```{code-cell} ipython3 :tags: ["output_scroll"] can_lang.loc[ - (can_lang['language']=='English') | - (can_lang['language']=='French') + (can_lang['language']=='English') + | (can_lang['language']=='French') ] ``` From 2ef115f5092c981b33fd3b418162879b3242d6c9 Mon Sep 17 00:00:00 2001 From: Joel Ostblom Date: Tue, 14 Feb 2023 12:00:41 -0800 Subject: [PATCH 11/44] Explain the formatting of the log plot more carefully and improve the final version by addressing the missing tick label --- source/viz.md | 41 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/source/viz.md b/source/viz.md index dcae7f78..39c07fca 100644 --- a/source/viz.md +++ b/source/viz.md @@ -753,13 +753,11 @@ can_lang_plot_log = alt.Chart(can_lang).mark_circle().encode( "most_at_home", title=["Language spoken most at home", "(number of Canadian residents)"], scale=alt.Scale(type="log"), - axis=alt.Axis(tickCount=7) ), y=alt.Y( "mother_tongue", title=["Mother tongue", "(number of Canadian residents)"], scale=alt.Scale(type="log"), - axis=alt.Axis(tickCount=7) ) ).configure_axis(titleFontSize=12) ``` @@ -776,6 +774,45 @@ glue('can_lang_plot_log', can_lang_plot_log, display=False) Scatter plot of number of Canadians reporting a language as their mother tongue vs the primary language at home with log adjusted x and y axes. ::: +You will notice two things in the chart above, +changing the axis to log creates many axis ticks and gridlines, +which makes the appearance of the chart rather noisy +and it is hard to focus on the data. +You can also see that the second last tick label is missing on the x-axis; +Altair dropped it because there wasn't space to fit in all the large numbers next to each other. +It is also hard to see if the label for 100,000,000 is for the last or second last tick. +To fix these issue, +we can limit the number of ticks and gridlines to only include the seven major ones, +and change the number formatting to include a suffix which makes the labels shorter. + +```{code-cell} ipython3 +can_lang_plot_log_revised = alt.Chart(can_lang).mark_circle().encode( + x=alt.X( + "most_at_home", + title=["Language spoken most at home", "(number of Canadian residents)"], + scale=alt.Scale(type="log"), + axis=alt.Axis(tickCount=7, format='s') + ), + y=alt.Y( + "mother_tongue", + title=["Mother tongue", "(number of Canadian residents)"], + scale=alt.Scale(type="log"), + axis=alt.Axis(tickCount=7, format='s') + ) +).configure_axis(titleFontSize=12) +``` + +```{code-cell} ipython3 +:tags: ["remove-cell"] +glue('can_lang_plot_log_revised', can_lang_plot_log_revised, display=False) +``` + +:::{glue:figure} can_lang_plot_log_revised +:figwidth: 700px +:name: can_lang_plot_log_revised + +Scatter plot of number of Canadians reporting a language as their mother tongue vs the primary language at home with log adjusted x and y axes. Only the major gridlines are shown and the suffix "k" indicate 1,000 ("kilo"), while the suffix "M" indicates 1,000,000 ("million"). +::: ```{code-cell} ipython3 From 77022c64c234ec622dadba5047cb84fc71981bef Mon Sep 17 00:00:00 2001 From: Joel Ostblom Date: Tue, 14 Feb 2023 12:01:17 -0800 Subject: [PATCH 12/44] Clarify which number is the canadian population --- source/viz.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/source/viz.md b/source/viz.md index 39c07fca..a3d442ff 100644 --- a/source/viz.md +++ b/source/viz.md @@ -848,9 +848,10 @@ you can clearly see the mutated output from the table. ``` ```{code-cell} ipython3 +canadian_population = 35_151_728 can_lang = can_lang.assign( - mother_tongue_percent=(can_lang['mother_tongue']/35151728) * 100, - most_at_home_percent=(can_lang['most_at_home']/35151728) * 100 + mother_tongue_percent=(can_lang['mother_tongue'] / canadian_population) * 100, + most_at_home_percent=(can_lang['most_at_home'] / canadian_population) * 100 ) can_lang[['mother_tongue_percent', 'most_at_home_percent']] From fbb9a764ba6d45c25fc7197e008304c7a485bc70 Mon Sep 17 00:00:00 2001 From: Joel Ostblom Date: Tue, 14 Feb 2023 12:01:56 -0800 Subject: [PATCH 13/44] Increase the chart dimensions to fit all the x axis labels --- source/viz.md | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/source/viz.md b/source/viz.md index a3d442ff..89bac0ef 100644 --- a/source/viz.md +++ b/source/viz.md @@ -854,18 +854,17 @@ can_lang = can_lang.assign( most_at_home_percent=(can_lang['most_at_home'] / canadian_population) * 100 ) can_lang[['mother_tongue_percent', 'most_at_home_percent']] - ``` -Finally, we will edit the visualization to use the percentages we just computed +Next, we will edit the visualization to use the percentages we just computed (and change our axis labels to reflect this change in units). {numref}`can_lang_plot_percent` displays the final result. - - +Here all the tick labels fit by default so we are not changing the labels to include suffixes +(note that suffixes can also be harder to understand for small quantities, +so they are best to avoid for small numbers unless you are communicating to a technical audience). ```{code-cell} ipython3 - can_lang_plot_percent = alt.Chart(can_lang).mark_circle().encode( x=alt.X( "most_at_home_percent", @@ -884,7 +883,8 @@ can_lang_plot_percent = alt.Chart(can_lang).mark_circle().encode( ```{code-cell} ipython3 :tags: ["remove-cell"] -glue('can_lang_plot_percent', can_lang_plot_percent, display=False) +# Increasing the dimensions makes all the ticks fit in jupyter book (the fit with the default dimensions in jupyterlab) +glue('can_lang_plot_percent', can_lang_plot_percent.properties(height=320, width=420), display=False) ``` :::{glue:figure} can_lang_plot_percent @@ -976,7 +976,8 @@ can_lang_plot_category=alt.Chart(can_lang).mark_circle().encode( ```{code-cell} ipython3 :tags: ["remove-cell"] -glue('can_lang_plot_category', can_lang_plot_category, display=False) +# Increasing the dimensions makes all the ticks fit in jupyter book (the fit with the default dimensions in jupyterlab) +glue('can_lang_plot_category', can_lang_plot_category.properties(height=320, width=420), display=False) ``` :::{glue:figure} can_lang_plot_category @@ -1028,7 +1029,8 @@ can_lang_plot_legend = alt.Chart(can_lang).mark_circle().encode( ```{code-cell} ipython3 :tags: ["remove-cell"] -glue('can_lang_plot_legend', can_lang_plot_legend, display=False) +# Increasing the dimensions makes all the ticks fit in jupyter book (the fit with the default dimensions in jupyterlab) +glue('can_lang_plot_legend', can_lang_plot_legend.properties(height=320, width=420), display=False) ``` :::{glue:figure} can_lang_plot_legend @@ -1091,7 +1093,8 @@ can_lang_plot_theme = alt.Chart(can_lang).mark_point(filled=True).encode( ```{code-cell} ipython3 :tags: ["remove-cell"] -glue('can_lang_plot_theme', can_lang_plot_theme, display=False) +# Increasing the dimensions makes all the ticks fit in jupyter book (the fit with the default dimensions in jupyterlab) +glue('can_lang_plot_theme', can_lang_plot_theme.properties(height=320, width=420), display=False) ``` :::{glue:figure} can_lang_plot_theme From 3f54c1a018857f366c06a04569e0e01803eba0bd Mon Sep 17 00:00:00 2001 From: Joel Ostblom Date: Tue, 14 Feb 2023 12:05:44 -0800 Subject: [PATCH 14/44] Remove legend titles and lay labels out vertically when on top The old behavior seems to be a replica of the R plot which was due to how did ggplot handles legends --- source/viz.md | 37 +++++++++++-------------------------- 1 file changed, 11 insertions(+), 26 deletions(-) diff --git a/source/viz.md b/source/viz.md index 89bac0ef..1b840d3a 100644 --- a/source/viz.md +++ b/source/viz.md @@ -951,9 +951,8 @@ which they belong. We can add the argument `color` to the `encode` function, sp that the `category` column should color the points. Adding this argument will color the points according to their group and add a legend at the side of the plot. - - - +Since the labels of the language category as descriptive of their own, +we can remove the title of the legend to reduce visual clutter without reducing the effectiveness of the chart. ```{code-cell} ipython3 can_lang_plot_category=alt.Chart(can_lang).mark_circle().encode( @@ -969,7 +968,7 @@ can_lang_plot_category=alt.Chart(can_lang).mark_circle().encode( scale=alt.Scale(type="log"), axis=alt.Axis(tickCount=7) ), - color="category" + color=alt.Color("category", title='') ).configure_axis(titleFontSize=12) ``` @@ -991,15 +990,9 @@ Scatter plot of percentage of Canadians reporting a language as their mother ton Another thing we can adjust is the location of the legend. This is a matter of preference and not critical for the visualization. We move the legend title using the `alt.Legend` function -with the arguments `legendX`, `legendY` and `direction` -arguments of the `theme` function. -Here we set the `direction` to `"vertical"` so that the legend items remain -vertically stacked on top of each other. The default `direction` is horizontal, which works well for many cases, but -for this particular visualization -because the legend labels are quite long, it is a bit cleaner if we move the -legend above the plot instead. - - +and specify that we want it on the top of the chart. +This automatically changes the legend items to be laid out horizontally instead of vertically, +but we could also keep the vertical layout by specifying `direction='vertical'` inside `alt.Legend`. ```{code-cell} ipython3 can_lang_plot_legend = alt.Chart(can_lang).mark_circle().encode( @@ -1017,12 +1010,8 @@ can_lang_plot_legend = alt.Chart(can_lang).mark_circle().encode( ), color=alt.Color( "category", - legend=alt.Legend( - orient='none', - legendX=0, - legendY=-90, - direction='vertical' - ) + title='', + legend=alt.Legend(orient='top') ) ).configure_axis(titleFontSize=12) ``` @@ -1073,18 +1062,14 @@ can_lang_plot_theme = alt.Chart(can_lang).mark_point(filled=True).encode( ), y=alt.Y( "mother_tongue_percent", - title="Mother tongue(percentage of Canadian residents)", + title=["Mother tongue", "(percentage of Canadian residents)"], scale=alt.Scale(type="log"), axis=alt.Axis(tickCount=7) ), color=alt.Color( "category", - legend=alt.Legend( - orient='none', - legendX=0, - legendY=-90, - direction='vertical' - ), + title='', + legend=alt.Legend(orient='top'), scale=alt.Scale(scheme='dark2') ), shape="category" From 10a970f1f98e961cddad3e8adb3ac2f56901f456 Mon Sep 17 00:00:00 2001 From: Joel Ostblom Date: Tue, 14 Feb 2023 12:16:15 -0800 Subject: [PATCH 15/44] Clarify text around color schemes --- source/viz.md | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/source/viz.md b/source/viz.md index 1b840d3a..0b6e0f4a 100644 --- a/source/viz.md +++ b/source/viz.md @@ -1030,28 +1030,30 @@ Scatter plot of percentage of Canadians reporting a language as their mother ton ::: In {numref}`can_lang_plot_legend`, the points are colored with -the default `altair` color palette. This is an appropriate choice for most situations. In Altair, there are many themes available, which can be viewed [in the documentation](https://altair-viz.github.io/user_guide/customization.html#customizing-colors). To change the color scheme, -we add the `scheme` argument in the `scale` of the `color` encoding to indicate the palette we want to use. +the default `altair` color scheme, which is called `'tableau10'`. This is an appropriate choice for most situations and is also easy to read for people with reduced color vision. +In general, the color schemes that are used by default in Altair are adapted to the type of data that is displayed and selected to be easy to interpret both for people with good and reduced color vision. +If you are unsure about a certain color combination, you can use +this [color blindness simulator](https://www.color-blindness.com/coblis-color-blindness-simulator/) to check +if your visualizations are color-blind friendly. ```{index} color palette; color blindness simulator ``` +All the available color schemes and information on how to create your own, can be viewed [in the documentation](https://altair-viz.github.io/user_guide/customization.html#customizing-colors). +To change the color scheme of our chart, +we can add the `scheme` argument in the `scale` of the `color` encoding. Below we pick the `"dark2"` theme, with the result shown in {numref}`can_lang_plot_theme` We also set the `shape` aesthetic mapping to the `category` variable as well; -this makes the scatter point shapes different for each category. This kind of +this makes the scatter point shapes different for each language category. This kind of visual redundancy—i.e., conveying the same information with both scatter point color and shape—can -further improve the clarity and accessibility of your visualization. +further improve the clarity and accessibility of your visualization, +but can add visual noise if there are many different shapes and colors, +so it should be used with care. Note that we are switching back to the use of `mark_point` here since `mark_circle` does not support the `shape` encoding and will always show up as a filled circle. -You can use -this [color blindness simulator](https://www.color-blindness.com/coblis-color-blindness-simulator/) to check -if your visualizations are color-blind friendly. -The default color palattes in `altair` are color-blind friendly (one more reason to stick with the defaults!). - - ```{code-cell} ipython3 can_lang_plot_theme = alt.Chart(can_lang).mark_point(filled=True).encode( x=alt.X( From 409cc6d4a2442e6ced08ac3d691a11bfb549a4c9 Mon Sep 17 00:00:00 2001 From: Joel Ostblom Date: Tue, 14 Feb 2023 12:30:03 -0800 Subject: [PATCH 16/44] Show how to add a tooltip and explain the advantages that brings --- source/viz.md | 53 +++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 51 insertions(+), 2 deletions(-) diff --git a/source/viz.md b/source/viz.md index 0b6e0f4a..2ba5a49c 100644 --- a/source/viz.md +++ b/source/viz.md @@ -1091,14 +1091,63 @@ glue('can_lang_plot_theme', can_lang_plot_theme.properties(height=320, width=420 Scatter plot of percentage of Canadians reporting a language as their mother tongue vs the primary language at home colored by language category with color-blind friendly colors. ::: -From the visualization in {numref}`can_lang_plot_theme`, +The chart above gives a good indication of how the different language categories differ, +and this information is sufficient to answer our research question. +But what if we want to know exactly which language correspond to which point in the chart? +With a regular visualization library this would not be possible, +as adding text labels for each individual language +would add a lot of visual noise and make the chart difficult to interpret. +However, since Altair is an interactive visualization library we can add information on demand +via the `Tooltip` encoding channel, +so that text labels for each point show up once we hover over it with the mouse pointer. +Here we also add the exact values of the variables on the x and y-axis to the tooltip. + +```{code-cell} ipython3 +can_lang_plot_tooltip = alt.Chart(can_lang).mark_point(filled=True, size=50).encode( + x=alt.X( + "most_at_home_percent", + title=["Language spoken most at home", "(percentage of Canadian residents)"], + scale=alt.Scale(type="log"), + axis=alt.Axis(tickCount=7) + ), + y=alt.Y( + "mother_tongue_percent", + title=["Mother tongue", "(percentage of Canadian residents)"], + scale=alt.Scale(type="log"), + axis=alt.Axis(tickCount=7) + ), + color=alt.Color( + "category", + title='', + legend=alt.Legend(orient='top'), + scale=alt.Scale(scheme='dark2') + ), + shape="category", + tooltip=alt.Tooltip(['language', 'mother_tongue', 'most_at_home']) +).configure_axis(titleFontSize=12) +``` + +```{code-cell} ipython3 +:tags: ["remove-cell"] +# Increasing the dimensions makes all the ticks fit in jupyter book (the fit with the default dimensions in jupyterlab) +glue('can_lang_plot_tooltip', can_lang_plot_tooltip.properties(height=320, width=420), display=False) +``` + +:::{glue:figure} can_lang_plot_tooltip +:figwidth: 700px +:name: can_lang_plot_tooltip + +Scatter plot of percentage of Canadians reporting a language as their mother tongue vs the primary language at home colored by language category with color-blind friendly colors. Hover over the data points with the mouse pointer to see additional information. +::: + +From the visualization in {numref}`can_lang_plot_tooltip`, we can now clearly see that the vast majority of Canadians reported one of the official languages as their mother tongue and as the language they speak most often at home. What do we see when considering the second part of our exploratory question? Do we see a difference in the relationship between languages spoken as a mother tongue and as a primary language at home across the higher-level language categories? -Based on {numref}`can_lang_plot_theme`, there does not +Based on {numref}`can_lang_plot_tooltip`, there does not appear to be much of a difference. For each higher-level language category, there appears to be a strong, positive, and linear relationship between From 968d11a5e592c851e2015f598a48e3a140ce806e Mon Sep 17 00:00:00 2001 From: Joel Ostblom Date: Tue, 14 Feb 2023 17:18:44 -0800 Subject: [PATCH 17/44] Include note about the danger of using barplots for measures of central tendency --- source/viz.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/source/viz.md b/source/viz.md index 2ba5a49c..982038f7 100644 --- a/source/viz.md +++ b/source/viz.md @@ -1189,10 +1189,16 @@ Here, we have a data frame of Earth's landmasses, and are trying to compare their sizes. The right type of visualization to answer this question is a bar plot. In a bar plot, the height of the bar represents the value of a summary statistic -(usually a size, count, proportion or percentage). +(usually a size, count, sum, proportion or percentage). They are particularly useful for comparing summary statistics between different groups of a categorical variable. +> **Note:** Although bar charts are often used to display mean and median values, +> they are generally a poor choice for these types of visualizations and should be avoided. +> The reason for this is that they hide all the variation in the data and only show a single value. +> Instead it's better to show the distribution of all the individual data points +> (potentially together with an additional visual mark for the mean or median). + ```{index} altair; mark_bar ``` From a74406b2cf1be37bfeb894ba19e3bfd1593fe34f Mon Sep 17 00:00:00 2001 From: Joel Ostblom Date: Tue, 14 Feb 2023 17:19:28 -0800 Subject: [PATCH 18/44] Clarify explanation of bar plot section --- source/viz.md | 53 +++++++++++++++++++++++---------------------------- 1 file changed, 24 insertions(+), 29 deletions(-) diff --git a/source/viz.md b/source/viz.md index 982038f7..37f1b62f 100644 --- a/source/viz.md +++ b/source/viz.md @@ -1233,9 +1233,9 @@ question we asked was only about the largest landmasses; let's make the plot a little bit clearer by keeping only the largest 12 landmasses. We do this using the `nlargest` function; the first argument is the number of rows we want and the second is the name of the column we want to use for comparing who is -largest. Then to help us make sure the labels have enough -space, we'll use horizontal bars instead of vertical ones. We do this by -swapping the `x` and `y` variables. +largest. Then to help make the landmass labels easier to read +we'll swap the `x` and `y` variables, +so that the landmass labels are on the y-axis and we don't have to tilt our head to read them. ```{index} pandas.DataFrame; nlargest ``` @@ -1262,41 +1262,36 @@ Bar plot of size for Earth's largest 12 landmasses. ::: -The plot in {numref}`islands_bar_top` is definitely clearer now, -and allows us to answer our question -("Which are the top 7 largest landmasses continents?") in the affirmative. -But the question could be made clearer from the plot -by organizing the bars not by alphabetical order -but by size, and to color them based on whether they are a continent. -The data for this is stored in the `landmass_type` column. -To use this to color the bars, -we use the `color` argument to color the bars according to the `landmass_type` +The plot in {numref}`islands_bar_top` is clearer now, +and allows us to answer our question: +"Which are the top 7 largest landmasses continents?". +However, we could still improve this visualization +by organizing the bars by landmass size rather than by alphabetical order. +We could also color the bars based on whether they are a continent or not +to add additional information to the chart. To organize the landmasses by their `size` variable, -we will use the `altair` `sort` function -in encoding for `y` axis to organize the landmasses by their `size` variable, which is encoded on the x-axis. -To sort the landmasses by their size(denoted on `x` axis), we use `sort='x'`. This plots the values on `y` axis +we will use the altair `sort` function +in the y-encoding of the chart. +Since the `size` variable is encoded in the x channel of the chart, +we specify `sort='x'` inside `alt.Y`. +This plots the values on `y` axis in the ascending order of `x` axis values. -We do this here so that the largest bar will be closest to the axis line, -which is more visually appealing. If instead, we want to sort the values on `y-axis` in descending order of `x-axis`, we need to specify `sort='-x'`. +This creates a chart where the largest bar is the closest to the axis line, +which is generally the most visually appealing when sorting bars. +If instead, +we want to sort the values on `y-axis` in descending order of `x-axis`, +we can add a minus sign to reverse the order and specify `sort='-x'`. ```{index} altair; sort ``` -To label the x and y axes, we will use the `alt.X` and `alt.Y` function -The default label is the name of the column being mapped to `color`. Here that -would be `landmass_type`; -however `landmass_type` is not proper English (and so is less readable). -Thus we use the `title` argument inside `alt.Color` to change that to `"Type"`. -Finally, we again use the `configure_axis` function -to change the font size. - ```{code-cell} ipython3 islands_plot_sorted = alt.Chart(islands_top12).mark_bar().encode( - x=alt.X("size",title="Size (1000 square mi)"), - y=alt.Y("landmass", title="Landmass", sort="x"), - color=alt.Color("landmass_type", title="Type") -).configure_axis(titleFontSize=12) + x="size", + y=alt.Y("landmass", sort="x"), + color=alt.Color("landmass_type") +) ``` ```{code-cell} ipython3 From a50ea8d22c22103aaa9097721d14ce51b9e58f92 Mon Sep 17 00:00:00 2001 From: Joel Ostblom Date: Tue, 14 Feb 2023 17:19:57 -0800 Subject: [PATCH 19/44] Remove redundant titles --- source/viz.md | 36 +++++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/source/viz.md b/source/viz.md index 37f1b62f..16f3d92c 100644 --- a/source/viz.md +++ b/source/viz.md @@ -1307,22 +1307,36 @@ Bar plot of size for Earth's largest 12 landmasses colored by whether its a cont ::: -The plot in {numref}`islands_plot_sorted` is now a very effective +The plot in {numref}`islands_plot_sorted` is now an effective visualization for answering our original questions. Landmasses are organized by their size, and continents are colored differently than other landmasses, -making it quite clear that continents are the largest seven landmasses. -We can make one more finishing touch in {numref}`islands_plot_titled`: we will -add a title to the chart by specifying `title` argument in the `alt.Chart` function. -Note that plot titles are not always required; usually plots appear as part +making it quite clear that all the seven largest landmasses are continents. + +To add some finishing touches to the chart +we will add a title to the chart by specifying `title` argument inside `alt.Chart`. +A good plot title should usually contain the take home message that you want readers of the chart to focus on, +e.g. "The Earth's seven largest landmasses are all continents", +but it could sometimes be more general, e.g. "The twelve largest landmasses on Earth". +Note that plot titles are not always required; e.g. if plots appear as part of other media (e.g., in a slide presentation, on a poster, in a paper) where the title may be redundant with the surrounding context. - -```{code-cell} ipython3 -islands_plot_titled = alt.Chart(islands_top12, title="Largest 12 landmasses on Earth").mark_bar().encode( +For categorical encodings, +such as the color and y channels in our chart, +it is often not necessary to include the axis title +as the labels of the categories are enough by themselves. +Particularly in this case where the title clearly states +that we are landmasses, +the titles are redundant and we can remove them. + +```{code-cell} ipython3 +islands_plot_titled = alt.Chart( + islands_top12, + title="The Earth's seven largest landmasses are all continents" +).mark_bar().encode( x=alt.X("size",title="Size (1000 square mi)"), - y=alt.Y("landmass", title="Landmass", sort="x"), - color=alt.Color("landmass_type", title="Type") -).configure_axis(titleFontSize=12) + y=alt.Y("landmass", title="", sort="x"), + color=alt.Color("landmass_type", title="") +) ``` ```{code-cell} ipython3 From a0b111e8ab34420fd46bcef1bed84d5e3f957442 Mon Sep 17 00:00:00 2001 From: Joel Ostblom Date: Tue, 14 Feb 2023 17:20:14 -0800 Subject: [PATCH 20/44] Properly explain what a histogram is --- source/viz.md | 51 +++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 43 insertions(+), 8 deletions(-) diff --git a/source/viz.md b/source/viz.md index 16f3d92c..a9f77032 100644 --- a/source/viz.md +++ b/source/viz.md @@ -1397,23 +1397,58 @@ we need to visualize the distribution of the measurements We can do this using a *histogram*. A histogram helps us visualize how a particular variable is distributed in a data set -by separating the data into bins, +by grouping the values into bins, and then using vertical bars to show how many data points fell in each bin. -To create a histogram in `altair` we will use the `mark_bar` graphical -mark, setting the `x` axis to the `Speed` measurement variable and `y` axis to `"count()"`. -There is no `"count()"` column-name in `morley_df`; we use `"count()"` to tell `altair` -that we want to count the number of values in the `Speed` column in each bin. -As usual, -let's use the default arguments just to see how things look. +To understand how to create a histogram in `altair`, +let's start by creating a bar chart +just like we did in the previous section. +Note that this time, +we are setting the `y` encoding to `"count()"`. +There is no `"count()"` column-name in `morley_df`; +we use `"count()"` to tell `altair` +that we want to count the number of occurrences of each value in along the x-axis +(which we encoded as the `Speed` column). ```{code-cell} ipython3 -morley_hist = alt.Chart(morley_df).mark_bar().encode( +morley_bars = alt.Chart(morley_df).mark_bar().encode( x="Speed", y="count()" ) ``` +```{code-cell} ipython3 +:tags: ["remove-cell"] +glue("morley_bars", morley_bars, display=False) +``` + +:::{glue:figure} morley_bars +:figwidth: 700px +:name: morley_bars + +A bar chart of Michelson's speed of light data. +::: + +The bar chart above gives us an indication of +which values are more common than others, +but because the bars are so thin it's hard to get a sense for the +overall distribution of the data. +We don't really care about how many occurrences there are of each exact `Speed` value, +but rather where most of the `Speed` values fall in general. +To more effectively communicate this information +we can group the x-axis into bins (or "buckets") +and then count how many `Speed` values fall within each bin. +A bar chart that represent the count of values +for a binned quantitative variable is called a histogram. + + +```{code-cell} ipython3 +morley_hist = alt.Chart(morley_df).mark_bar().encode( + x=alt.X("Speed", bin=True), + y="count()" +) +``` + ```{code-cell} ipython3 :tags: ["remove-cell"] glue("morley_hist", morley_hist, display=False) From f1fc0b4f02bf116f1277c9d2b1b190536fdc63e1 Mon Sep 17 00:00:00 2001 From: Joel Ostblom Date: Tue, 14 Feb 2023 17:35:03 -0800 Subject: [PATCH 21/44] Add example on how to use maxbins Previously it was just silently introduced it seems --- source/viz.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/source/viz.md b/source/viz.md index a9f77032..c64912be 100644 --- a/source/viz.md +++ b/source/viz.md @@ -1461,6 +1461,34 @@ glue("morley_hist", morley_hist, display=False) Histogram of Michelson's speed of light data. ::: +We can change the number of bins by using the `maxbins` parameter. +In `altair`, +the default number of bins +or slightly more than the default +are usually good choices. +But since the number of bins impacts what your data distribution looks like +it is a good idea to try out a few different values, +when exploring your data. + +```{code-cell} ipython3 +morley_hist_maxbins = alt.Chart(morley_df).mark_bar().encode( + x=alt.X("Speed", bin=alt.Bin(maxbins=30)), + y="count()" +) +``` + +```{code-cell} ipython3 +:tags: ["remove-cell"] +glue("morley_hist_maxbins", morley_hist_maxbins, display=False) +``` + +:::{glue:figure} morley_hist_maxbins +:figwidth: 700px +:name: morley_hist_maxbins + +Histogram of Michelson's speed of light data. +::: + #### Adding layers to an `altair` chart ```{index} altair; +; mark_rule From 0860de3e6a522c9b9e27502b1ad361d0b4355e7a Mon Sep 17 00:00:00 2001 From: Joel Ostblom Date: Tue, 14 Feb 2023 17:35:30 -0800 Subject: [PATCH 22/44] Restructure text and modify line appearance to be more readable --- source/viz.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/source/viz.md b/source/viz.md index c64912be..a282efa9 100644 --- a/source/viz.md +++ b/source/viz.md @@ -1502,33 +1502,33 @@ In order to visualize the true speed of light, we will add a vertical line with the `mark_rule` function. To draw a vertical line with `mark_rule`, we need to specify where on the x-axis the line should be drawn. -We can do this by providing `x=alt.datum(792.458)`. The value `792.458` -is the true value of light speed -minus 299,000. Using `alt.datum` tells altair that we have a single datum -(number) that we would like plotted. -We would also like to fine tune this vertical line, -styling it so that it is dashed, -we do this by setting `strokeDash=[3]`. Note that you could also -change the thickness of the line by providing `size=2` if you wanted to. +We can do this by providing `x=alt.datum(792.458)`, +where the value `792.458` is the true speed of light minus 299,000 +and `alt.datum` tells altair that we have a single datum +(number) that we would like plotted (rather than a column in the data frame). Similarly, a horizontal line can be plotted using the `y` axis encoding and the dataframe with one value, which would act as the be the y-intercept. Note that *vertical lines* are used to denote quantities on the *horizontal axis*, while *horizontal lines* are used to denote quantities on the *vertical axis*. +To fine tune the appearance of this vertical line, +we can change it from a solid to a dashed line with `strokeDash=[5]`, +where `5` indicates the length of each dash. We also +change the thickness of the line by specifying `size=2`. To add the dashed line on top of the histogram, we **add** the `mark_rule` chart to the `morley_hist` using the `+` operator. Adding features to a plot using the `+` operator is known as *layering* in `altair`. -This is a very powerful feature of `altair`; you +This is a very powerful feature; you can continue to iterate on a single chart, adding and refining one layer at a time. If you stored your chart as a variable using the assignment symbol (`=`), you can add to it using the `+` operator. Below we add a vertical line created using `mark_rule` -to the last plot we created, `morley_hist`, using the `+` operator. +to the `morley_hist` we created previously. ```{code-cell} ipython3 -v_line = alt.Chart().mark_rule(strokeDash=[3]).encode( +v_line = alt.Chart().mark_rule(strokeDash=[5], size=2).encode( x=alt.datum(792.458) ) @@ -1545,7 +1545,7 @@ glue("morley_hist_line", morley_hist_line, display=False) :figwidth: 700px :name: morley_hist_line -Histogram of Michelson's speed of light data with vertical line indicating true speed of light. +Histogram of Michelson's speed of light data with vertical line indicating the true speed of light. ::: In {numref}`morley_hist_line`, From 0cc933f56d9e56e4ade4e62f6c3ab80162a5af2d Mon Sep 17 00:00:00 2001 From: Joel Ostblom Date: Tue, 14 Feb 2023 18:34:43 -0800 Subject: [PATCH 23/44] Simplify how to facet charts and improve the explanations --- source/viz.md | 115 ++++++++++++++++++++++++-------------------------- 1 file changed, 56 insertions(+), 59 deletions(-) diff --git a/source/viz.md b/source/viz.md index a282efa9..e4fd07cd 100644 --- a/source/viz.md +++ b/source/viz.md @@ -1527,8 +1527,14 @@ using the assignment symbol (`=`), you can add to it using the `+` operator. Below we add a vertical line created using `mark_rule` to the `morley_hist` we created previously. +> **Note:** Technically we could have left out the data argument +> when creating the rule chart +> since we're not using any values from the `morley_df` data frame, +> but we will need it later when we facet this layered chart, +> so we are including it here already. + ```{code-cell} ipython3 -v_line = alt.Chart().mark_rule(strokeDash=[5], size=2).encode( +v_line = alt.Chart(morley_df).mark_rule(strokeDash=[5], size=2).encode( x=alt.datum(792.458) ) @@ -1559,15 +1565,10 @@ where counts from different experiments are stacked on top of each other in different colors. We can create a histogram colored by the `Expt` variable by adding it to the `color` argument. -We make sure the different colors can be seen -(despite them all sitting on top of each other) -by setting the `opacity` argument in `mark_bar` to `0.5` -to make the bars slightly translucent. - ```{code-cell} ipython3 -morley_hist_colored = alt.Chart(morley_df).mark_bar(opacity=0.5).encode( - x="Speed", +morley_hist_colored = alt.Chart(morley_df).mark_bar().encode( + x=alt.X("Speed", bin=True), y="count()", color="Expt" ) @@ -1591,8 +1592,8 @@ Histogram of Michelson's speed of light data colored by experiment. ```{index} integer ``` -Alright great, {numref}`morley_hist_colored` looks... wait a second! We are not able to distinguish -between different Experiments in the histogram! What is going on here? Well, if you +Alright great, {numref}`morley_hist_colored` looks... wait a second! We are not able to easily distinguish +between the colors of the different Experiments in the histogram! What is going on here? Well, if you recall from the {ref}`wrangling` chapter, the *data type* you use for each variable can influence how Python and `altair` treats it. Here, we indeed have an issue with the data types in the `morley` data frame. In particular, the `Expt` column @@ -1609,13 +1610,19 @@ To fix this issue we can convert the `Expt` variable into a `nominal` (i.e., categorical) type variable by adding a suffix `:N` to the `Expt` variable. Adding the `:N` suffix ensures that `altair` will treat a variable as a categorical variable, and -hence use a discrete color map in visualizations. +hence use a discrete color map in visualizations +([read more about data types in the altair documentation](https://altair-viz.github.io/user_guide/encoding.html#encoding-data-types)). We also specify the `stack=False` argument in the `y` encoding so -that the bars are not stacked on top of each other. +that the bars are not stacked on top of each other, +but instead share the same baseline. +We make sure the different colors can be seen +despite them sitting in front of each other +by setting the `opacity` argument in `mark_bar` to `0.5` +to make the bars slightly translucent. ```{code-cell} ipython3 -morley_hist_categorical = alt.Chart(morley_df).mark_bar(opacity=0.5).encode( - x=alt.X("Speed", bin=alt.Bin(maxbins=50)), +morley_hist_categorical = alt.Chart(morley_df).mark_bar().encode( + x=alt.X("Speed", bin=True), y=alt.Y("count()", stack=False), color=alt.Color("Expt:N") ) @@ -1647,32 +1654,24 @@ grid of separate histogram plots. ```{index} altair; facet ``` -We use the `facet` function to create a plot +We can use the `facet` function to create a chart that has multiple subplots arranged in a grid. The argument to `facet` specifies the variable(s) used to split the plot -into subplots (`Expt`), the data frame we are working with `morley_df`, and -how to split them (i.e., into rows or columns). In this example, we choose to -have our plots in a single column (`columns=1`). This makes it easier for -us to compare along the `x`-axis as our vertical-line is in the same -horizontal position. If instead you wanted to use a single row, you could -specify `rows=1`. - -There is another important change we have to make. When -we define `morley_hist`, we no longer supply `morley_df` as an -argument to `alt.Chart`. This is because `facet` takes care of separating -the data by `Expt` and providing it to each of the facet sub-plots. +into subplots (`Expt` in the code below), +and how many columns there should be in the grid. +In this example, we chose to +arrange our plots in a single column (`columns=1`) since this makes it easier for +us to compare the location of the histograms along the `x`-axis +in the different subplots. +We also reduce the height of each chart +so that they all fit in the same view. ```{code-cell} ipython3 -morley_hist = alt.Chart().mark_bar(opacity=0.5).encode( - x=alt.X("Speed", bin=alt.Bin(maxbins=50)), - y=alt.Y("count()", stack=False), - color=alt.Color("Expt:N") -).properties(height=100, width=400) - -morley_hist_facet = (morley_hist + v_line).facet( +morley_hist_facet = morley_hist_categorical.properties( + height=100 +).facet( "Expt", - data=morley_df, columns=1 ) ``` @@ -1690,13 +1689,13 @@ Histogram of Michelson's speed of light data split vertically by experiment. ::: The visualization in {numref}`morley_hist_facet` -now makes it quite clear how accurate the different experiments were +makes it clear how accurate the different experiments were with respect to one another. -The most variable measurements came from Experiment 1. -There the measurements ranged from about 650–1050 km/sec. -The least variable measurements came from Experiment 2. -There, the measurements ranged from about 750–950 km/sec. -The most different experiments still obtained quite similar results! +The most variable measurements came from Experiment 1, +where the measurements ranged from about 650–1050 km/sec. +The least variable measurements came from Experiment 2, +where the measurements ranged from about 750–950 km/sec. +The most different experiments still obtained quite similar overall results! ```{index} altair; alt.X, altair; alt.Y, altair; configure_axis ``` @@ -1714,43 +1713,41 @@ function to transform our data into a relative measure of accuracy rather than absolute measurements. ```{code-cell} ipython3 - -morley_rel = morley_df -morley_rel = morley_rel.assign( - relative_accuracy=( - 100 *((299000 + morley_df["Speed"]) - 299792.458) / (299792.458) +speed_of_light = 299792.458 +morley_df = morley_df.assign( + relative_error=( + 100 * (299000 + morley_df["Speed"] - speed_of_light) / speed_of_light ) ) - -morley_rel +morley_df ``` ```{code-cell} ipython3 -v_line = alt.Chart().mark_rule( - strokeDash=[3]).encode( - x=alt.datum(0) -) - -morley_hist = alt.Chart().mark_bar(opacity=0.6).encode( +morley_hist_rel = alt.Chart(morley_df).mark_bar().encode( x=alt.X( - "relative_accuracy", - bin=alt.Bin(maxbins=120), + "relative_error", + bin=True, title="Relative Accuracy (%)" ), y=alt.Y( "count()", - stack=False, title="# Measurements" ), color=alt.Color( "Expt:N", title="Experiment ID" ) -).properties(height=100, width=400) +) + +# Recreating v_line to indicate that the speed of light is at 0% relative error +v_line = alt.Chart(morley_df).mark_rule(strokeDash=[5], size=2).encode( + x=alt.datum(0) +) -morley_hist_relative = (morley_hist + v_line).facet( +morley_hist_relative = (morley_hist_rel + v_line).properties( + height=100 +).facet( "Expt", - data=morley_rel, columns=1, title="Histogram of relative accuracy of Michelson’s speed of light data" ) From b58e077eb702e01cdf0cac762bd6abf3f4b79f65 Mon Sep 17 00:00:00 2001 From: Joel Ostblom Date: Wed, 15 Feb 2023 09:56:28 -0800 Subject: [PATCH 24/44] Move all the maxbins sections into one place --- source/viz.md | 65 ++++++++++++++++++++++----------------------------- 1 file changed, 28 insertions(+), 37 deletions(-) diff --git a/source/viz.md b/source/viz.md index e4fd07cd..32d86199 100644 --- a/source/viz.md +++ b/source/viz.md @@ -1461,34 +1461,6 @@ glue("morley_hist", morley_hist, display=False) Histogram of Michelson's speed of light data. ::: -We can change the number of bins by using the `maxbins` parameter. -In `altair`, -the default number of bins -or slightly more than the default -are usually good choices. -But since the number of bins impacts what your data distribution looks like -it is a good idea to try out a few different values, -when exploring your data. - -```{code-cell} ipython3 -morley_hist_maxbins = alt.Chart(morley_df).mark_bar().encode( - x=alt.X("Speed", bin=alt.Bin(maxbins=30)), - y="count()" -) -``` - -```{code-cell} ipython3 -:tags: ["remove-cell"] -glue("morley_hist_maxbins", morley_hist_maxbins, display=False) -``` - -:::{glue:figure} morley_hist_maxbins -:figwidth: 700px -:name: morley_hist_maxbins - -Histogram of Michelson's speed of light data. -::: - #### Adding layers to an `altair` chart ```{index} altair; +; mark_rule @@ -1773,12 +1745,31 @@ experiments did quite an admirable job given the technology available at the tim #### Choosing a binwidth for histograms -When you create a histogram in `altair`, by default, it tries to choose a reasonable number of bins. -Naturally, this is not always the right number to use. -You can set the number of bins yourself by using -the `maxbins` argument inside `alt.Bin`. -But what number of bins is the right one to use? +When you create a histogram in `altair`, it tries to choose a reasonable number of bins. +We can change the number of bins by using the `maxbins` parameter +inside `alt.Bin`. +```{code-cell} ipython3 +morley_hist_maxbins = alt.Chart(morley_df).mark_bar().encode( + x=alt.X("Speed", bin=alt.Bin(maxbins=30)), + y="count()" +) +``` + +```{code-cell} ipython3 +:tags: ["remove-cell"] +glue("morley_hist_maxbins", morley_hist_maxbins, display=False) +``` + +:::{glue:figure} morley_hist_maxbins +:figwidth: 700px +:name: morley_hist_maxbins + +Histogram of Michelson's speed of light data. +::: + + +But what number of bins is the right one to use? Unfortunately there is no hard rule for what the right bin number or width is. It depends entirely on your problem; the *right* number of bins or bin width is @@ -1871,10 +1862,10 @@ morley_hist_5 = alt.Chart().mark_bar(opacity=0.9).encode( ).properties(height=100, width=200) morley_hist_max_bins = (( - (morley_hist_default + v_line).facet(row="Expt:N", data=morley_rel, title="default maxbins") | - (morley_hist_200 + v_line).facet(row="Expt:N", data=morley_rel, title="maxBins=200")) & - ((morley_hist_70 + v_line).facet(row="Expt:N", data=morley_rel, title="maxBins=70") | - (morley_hist_5 + v_line).facet(row="Expt:N", data=morley_rel, title="maxBins=5") + (morley_hist_default + v_line).facet(row="Expt:N", data=morley_df, title="default maxbins") | + (morley_hist_200 + v_line).facet(row="Expt:N", data=morley_df, title="maxBins=200")) & + ((morley_hist_70 + v_line).facet(row="Expt:N", data=morley_df, title="maxBins=70") | + (morley_hist_5 + v_line).facet(row="Expt:N", data=morley_df, title="maxBins=5") )) ``` From 1df06f06264a6a83c754f5d21ff6c8fa3ae37d7e Mon Sep 17 00:00:00 2001 From: Joel Ostblom Date: Wed, 15 Feb 2023 10:37:02 -0800 Subject: [PATCH 25/44] Change the assignment method of a single column the regular syntax instaed of using assign and overwriting the entire df --- source/viz.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/source/viz.md b/source/viz.md index 32d86199..accc5e28 100644 --- a/source/viz.md +++ b/source/viz.md @@ -1686,10 +1686,8 @@ absolute measurements. ```{code-cell} ipython3 speed_of_light = 299792.458 -morley_df = morley_df.assign( - relative_error=( - 100 * (299000 + morley_df["Speed"] - speed_of_light) / speed_of_light - ) +morley_df['relative_error'] = ( + 100 * (299000 + morley_df["Speed"] - speed_of_light) / speed_of_light ) morley_df ``` From 59a08c94598512becd13036a2f0094a2362d4067 Mon Sep 17 00:00:00 2001 From: Joel Ostblom Date: Wed, 15 Feb 2023 10:37:25 -0800 Subject: [PATCH 26/44] Update language to reflect that we are actually looking at relative error rather than relative accuracy --- source/viz.md | 38 ++++++++++++++++++++------------------ 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/source/viz.md b/source/viz.md index accc5e28..3f4014d1 100644 --- a/source/viz.md +++ b/source/viz.md @@ -1681,8 +1681,8 @@ subtly, even though it is easy to compare the experiments on this plot to one another, it is hard to get a sense of just how accurate all the experiments were overall. For example, how accurate is the value 800 on the plot, relative to the true speed of light? To answer this question, we'll use the `assign` -function to transform our data into a relative measure of accuracy rather than -absolute measurements. +function to transform our data into a relative measure of error rather than +an absolute measurement. ```{code-cell} ipython3 speed_of_light = 299792.458 @@ -1697,7 +1697,7 @@ morley_hist_rel = alt.Chart(morley_df).mark_bar().encode( x=alt.X( "relative_error", bin=True, - title="Relative Accuracy (%)" + title="Relative error (%)" ), y=alt.Y( "count()", @@ -1719,7 +1719,7 @@ morley_hist_relative = (morley_hist_rel + v_line).properties( ).facet( "Expt", columns=1, - title="Histogram of relative accuracy of Michelson’s speed of light data" + title="Histogram of relative error of Michelson’s speed of light data" ) ``` @@ -1733,7 +1733,7 @@ glue("morley_hist_relative", morley_hist_relative, display=True) :figwidth: 700px :name: morley_hist_relative -Histogram of relative accuracy split vertically by experiment with clearer axes and labels +Histogram of relative error split vertically by experiment with clearer axes and labels ::: Wow, impressive! These measurements of the speed of light from 1879 had errors @@ -1787,16 +1787,18 @@ In this case, we can see that both the default number of bins and the `maxbins=70` of are effective for helping to answer our question. On the other hand, the `maxbins=200` and `maxbins=5` are too small and too big, respectively. - - - ```{code-cell} ipython3 :tags: ["remove-cell"] +# Weirdly things fail if v_line is not included or if v_line is defined with data +v_line = alt.Chart().mark_rule( + strokeDash=[3]).encode( + x=alt.datum(0) +) morley_hist_default = alt.Chart().mark_bar(opacity=0.9).encode( x=alt.X( - "relative_accuracy", - title="Relative Accuracy (%)" + "relative_error", + title="Relative error (%)" ), y=alt.Y( "count()", @@ -1811,9 +1813,9 @@ morley_hist_default = alt.Chart().mark_bar(opacity=0.9).encode( morley_hist_200 = alt.Chart().mark_bar(opacity=0.9).encode( x=alt.X( - "relative_accuracy", + "relative_error", bin=alt.Bin(maxbins=200), - title="Relative Accuracy (%)" + title="Relative error (%)" ), y=alt.Y( "count()", @@ -1827,9 +1829,9 @@ morley_hist_200 = alt.Chart().mark_bar(opacity=0.9).encode( morley_hist_70 = alt.Chart().mark_bar(opacity=0.9).encode( x=alt.X( - "relative_accuracy", + "relative_error", bin=alt.Bin(maxbins=70), - title="Relative Accuracy (%)" + title="Relative error (%)" ), y=alt.Y( "count()", @@ -1844,9 +1846,9 @@ morley_hist_70 = alt.Chart().mark_bar(opacity=0.9).encode( morley_hist_5 = alt.Chart().mark_bar(opacity=0.9).encode( x=alt.X( - "relative_accuracy", + "relative_error", bin=alt.Bin(maxbins=5), - title="Relative Accuracy (%)" + title="Relative error (%)" ), y=alt.Y( "count()", @@ -1943,8 +1945,8 @@ roughly 299,792.458 kilometers per second. (2) But how accurately were we first able to measure this fundamental physical constant, and did certain experiments produce more accurate results than others? (3) To better understand this, we plotted data from 5 experiments by Michelson in 1879, each with 20 trials, as -histograms stacked on top of one another. The horizontal axis shows the -accuracy of the measurements relative to the true speed of light as we know it +histograms stacked on top of one another. The horizontal axis shows the +error of the measurements relative to the true speed of light as we know it today, expressed as a percentage. From this visualization, you can see that most results had relative errors of at most 0.05%. You can also see that experiments 1 and 3 had measurements that were the farthest from the true From b6a364f421e69927ca994a31f8b827a46536063f Mon Sep 17 00:00:00 2001 From: Joel Ostblom Date: Wed, 15 Feb 2023 10:39:20 -0800 Subject: [PATCH 27/44] Name the new column according to the naming scheme of the existing columns All columns in the dataframe should follow the same naming scheme --- source/viz.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/source/viz.md b/source/viz.md index 3f4014d1..9250f453 100644 --- a/source/viz.md +++ b/source/viz.md @@ -1686,7 +1686,7 @@ an absolute measurement. ```{code-cell} ipython3 speed_of_light = 299792.458 -morley_df['relative_error'] = ( +morley_df['RelativeError'] = ( 100 * (299000 + morley_df["Speed"] - speed_of_light) / speed_of_light ) morley_df @@ -1695,7 +1695,7 @@ morley_df ```{code-cell} ipython3 morley_hist_rel = alt.Chart(morley_df).mark_bar().encode( x=alt.X( - "relative_error", + "RelativeError", bin=True, title="Relative error (%)" ), @@ -1797,7 +1797,7 @@ v_line = alt.Chart().mark_rule( morley_hist_default = alt.Chart().mark_bar(opacity=0.9).encode( x=alt.X( - "relative_error", + "RelativeError", title="Relative error (%)" ), y=alt.Y( @@ -1813,7 +1813,7 @@ morley_hist_default = alt.Chart().mark_bar(opacity=0.9).encode( morley_hist_200 = alt.Chart().mark_bar(opacity=0.9).encode( x=alt.X( - "relative_error", + "RelativeError", bin=alt.Bin(maxbins=200), title="Relative error (%)" ), @@ -1829,7 +1829,7 @@ morley_hist_200 = alt.Chart().mark_bar(opacity=0.9).encode( morley_hist_70 = alt.Chart().mark_bar(opacity=0.9).encode( x=alt.X( - "relative_error", + "RelativeError", bin=alt.Bin(maxbins=70), title="Relative error (%)" ), @@ -1846,7 +1846,7 @@ morley_hist_70 = alt.Chart().mark_bar(opacity=0.9).encode( morley_hist_5 = alt.Chart().mark_bar(opacity=0.9).encode( x=alt.X( - "relative_error", + "RelativeError", bin=alt.Bin(maxbins=5), title="Relative error (%)" ), From 820b47b2a23e80b0dc3564cd1bf083be57b5f322 Mon Sep 17 00:00:00 2001 From: Joel Ostblom Date: Wed, 15 Feb 2023 10:41:57 -0800 Subject: [PATCH 28/44] Change first maxbins example to also use relative error --- source/viz.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/viz.md b/source/viz.md index 9250f453..0151923b 100644 --- a/source/viz.md +++ b/source/viz.md @@ -1749,7 +1749,7 @@ inside `alt.Bin`. ```{code-cell} ipython3 morley_hist_maxbins = alt.Chart(morley_df).mark_bar().encode( - x=alt.X("Speed", bin=alt.Bin(maxbins=30)), + x=alt.X("RelativeError", bin=alt.Bin(maxbins=30)), y="count()" ) ``` From c9113eead8ceacff8ed1e54c9afd393b90d3b297 Mon Sep 17 00:00:00 2001 From: Joel Ostblom Date: Wed, 15 Feb 2023 11:02:56 -0800 Subject: [PATCH 29/44] Simplify logic of last figure and make it easier to read --- source/viz.md | 115 ++++++++++++++++++++++---------------------------- 1 file changed, 50 insertions(+), 65 deletions(-) diff --git a/source/viz.md b/source/viz.md index 0151923b..912845f0 100644 --- a/source/viz.md +++ b/source/viz.md @@ -1789,49 +1789,11 @@ On the other hand, the `maxbins=200` and `maxbins=5` are too small and too big, ```{code-cell} ipython3 :tags: ["remove-cell"] -# Weirdly things fail if v_line is not included or if v_line is defined with data -v_line = alt.Chart().mark_rule( - strokeDash=[3]).encode( - x=alt.datum(0) -) - -morley_hist_default = alt.Chart().mark_bar(opacity=0.9).encode( - x=alt.X( - "RelativeError", - title="Relative error (%)" - ), - y=alt.Y( - "count()", - stack=False, - title="# Measurements" - ), - color=alt.Color( - "Expt:N", - title="Experiment ID" - ) -).properties(height=100, width=200) - -morley_hist_200 = alt.Chart().mark_bar(opacity=0.9).encode( - x=alt.X( - "RelativeError", - bin=alt.Bin(maxbins=200), - title="Relative error (%)" - ), - y=alt.Y( - "count()", - stack=False, - title="# Measurements" - ), - color=alt.Color( - "Expt:N", title="Experiment ID" - ) -).properties(height=100, width=200) - -morley_hist_70 = alt.Chart().mark_bar(opacity=0.9).encode( +morley_hist_default = alt.Chart(morley_df).mark_bar().encode( x=alt.X( "RelativeError", - bin=alt.Bin(maxbins=70), - title="Relative error (%)" + title="Relative error (%)", + bin=True ), y=alt.Y( "count()", @@ -1840,33 +1802,56 @@ morley_hist_70 = alt.Chart().mark_bar(opacity=0.9).encode( ), color=alt.Color( "Expt:N", - title="Experiment ID" + title="Experiment ID", + legend=None ) -).properties(height=100, width=200) - -morley_hist_5 = alt.Chart().mark_bar(opacity=0.9).encode( - x=alt.X( - "RelativeError", - bin=alt.Bin(maxbins=5), - title="Relative error (%)" +).properties(height=100, width=250) + +morley_hist_max_bins = alt.vconcat( + alt.hconcat( + (morley_hist_default + v_line).facet( + 'Expt', + columns=1, + title=alt.TitleParams('Default (bin=True)', fontSize=16, anchor='middle', dx=15) + ), + (morley_hist_default.encode( + x=alt.X( + "RelativeError", + bin=alt.Bin(maxbins=5), + title="Relative error (%)" + ) + ) + v_line).facet( + 'Expt', + columns=1, + title=alt.TitleParams('maxbins=5', fontSize=16, anchor='middle', dx=15) + ), ), - y=alt.Y( - "count()", - stack=False, - title="# Measurements" + alt.hconcat( + (morley_hist_default.encode( + x=alt.X( + "RelativeError", + bin=alt.Bin(maxbins=70), + title="Relative error (%)" + ) + ) + v_line).facet( + 'Expt', + columns=1, + title=alt.TitleParams('maxbins=70', fontSize=16, anchor='middle', dx=15) + ), + (morley_hist_default.encode( + x=alt.X( + "RelativeError", + bin=alt.Bin(maxbins=200), + title="Relative error (%)" + ) + ) + v_line).facet( + 'Expt', + columns=1, + title=alt.TitleParams('maxbins=200', fontSize=16, anchor='middle', dx=15) + ) ), - color=alt.Color( - "Expt:N", - title="Experiment ID" - ) -).properties(height=100, width=200) - -morley_hist_max_bins = (( - (morley_hist_default + v_line).facet(row="Expt:N", data=morley_df, title="default maxbins") | - (morley_hist_200 + v_line).facet(row="Expt:N", data=morley_df, title="maxBins=200")) & - ((morley_hist_70 + v_line).facet(row="Expt:N", data=morley_df, title="maxBins=70") | - (morley_hist_5 + v_line).facet(row="Expt:N", data=morley_df, title="maxBins=5") -)) + spacing=50 +) ``` ```{code-cell} ipython3 From d93e6a4ab6754804a381abcf2d31e14bd4ecf4b5 Mon Sep 17 00:00:00 2001 From: Joel Ostblom Date: Wed, 15 Feb 2023 11:19:29 -0800 Subject: [PATCH 30/44] Standardize visualization syntax across chapters I went with what was used in most places, including the visualization chapter. This is also the style in the altiar documentation. We can discuss if we want to change this but for now it makes sense to at least have the same syntax everywhere instead of a mix which could be confusing. --- source/classification1.md | 24 ++++++----------- source/classification2.md | 57 +++++++++++++++------------------------ source/intro.md | 41 ++++++++++------------------ source/regression1.md | 40 +++++++++------------------ source/regression2.md | 18 +++++-------- 5 files changed, 63 insertions(+), 117 deletions(-) diff --git a/source/classification1.md b/source/classification1.md index a4de3695..ed0de113 100644 --- a/source/classification1.md +++ b/source/classification1.md @@ -289,14 +289,10 @@ perimeter and concavity variables. Recall that `altair's` default palette is colorblind-friendly, so we can stick with that here. ```{code-cell} ipython3 -perim_concav = ( - alt.Chart(cancer) - .mark_circle() - .encode( - x=alt.X("Perimeter", title="Perimeter (standardized)"), - y=alt.Y("Concavity", title="Concavity (standardized)"), - color=alt.Color("Class", title="Diagnosis"), - ) +perim_concav = alt.Chart(cancer).mark_circle().encode( + x=alt.X("Perimeter", title="Perimeter (standardized)"), + y=alt.Y("Concavity", title="Concavity (standardized)"), + color=alt.Color("Class", title="Diagnosis"), ) perim_concav ``` @@ -1440,14 +1436,10 @@ rare_cancer = pd.concat(( cancer[cancer["Class"] == 'Malignant'].head(3) )) -rare_plot = ( - alt.Chart(rare_cancer) - .mark_circle() - .encode( - x=alt.X("Perimeter", title="Perimeter (standardized)"), - y=alt.Y("Concavity", title="Concavity (standardized)"), - color=alt.Color("Class", title="Diagnosis"), - ) +rare_plot = alt.Chart(rare_cancer).mark_circle().encode( + x=alt.X("Perimeter", title="Perimeter (standardized)"), + y=alt.Y("Concavity", title="Concavity (standardized)"), + color=alt.Color("Class", title="Diagnosis"), ) rare_plot ``` diff --git a/source/classification2.md b/source/classification2.md index e365ff99..d2783b7f 100644 --- a/source/classification2.md +++ b/source/classification2.md @@ -330,16 +330,11 @@ cancer['Class'] = cancer['Class'].replace({ # create scatter plot of tumor cell concavity versus smoothness, # labeling the points be diagnosis class -perim_concav = ( - alt.Chart(cancer) - .mark_circle() - .encode( - x="Smoothness", - y="Concavity", - color=alt.Color("Class", title="Diagnosis"), - ) +perim_concav = alt.Chart(cancer).mark_circle().encode( + x="Smoothness", + y="Concavity", + color=alt.Color("Class", title="Diagnosis"), ) - perim_concav ``` @@ -1081,19 +1076,15 @@ as shown in {numref}`fig:06-find-k`. ```{code-cell} ipython3 :tags: [remove-output] -accuracy_vs_k = ( - alt.Chart(accuracies_grid) - .mark_line(point=True) - .encode( - x=alt.X( - "n_neighbors", - title="Neighbors", - ), - y=alt.Y( - "mean_test_score", - title="Accuracy estimate", - scale=alt.Scale(domain=(0.85, 0.90)), - ), +accuracy_vs_k = alt.Chart(accuracies_grid).mark_line(point=True).encode( + x=alt.X( + "n_neighbors", + title="Neighbors", + ), + y=alt.Y( + "mean_test_score", + title="Accuracy estimate", + scale=alt.Scale(domain=(0.85, 0.90)), ) ) @@ -1170,19 +1161,15 @@ large_accuracies_grid = pd.DataFrame( ).cv_results_ ) -large_accuracy_vs_k = ( - alt.Chart(large_accuracies_grid) - .mark_line(point=True) - .encode( - x=alt.X( - "param_kneighborsclassifier__n_neighbors", - title="Neighbors", - ), - y=alt.Y( - "mean_test_score", - title="Accuracy estimate", - scale=alt.Scale(domain=(0.60, 0.90)), - ), +large_accuracy_vs_k = alt.Chart(large_accuracies_grid).mark_line(point=True).encode( + x=alt.X( + "param_kneighborsclassifier__n_neighbors", + title="Neighbors", + ), + y=alt.Y( + "mean_test_score", + title="Accuracy estimate", + scale=alt.Scale(domain=(0.60, 0.90)), ) ) diff --git a/source/intro.md b/source/intro.md index 8feca2a2..fa556a11 100644 --- a/source/intro.md +++ b/source/intro.md @@ -879,12 +879,9 @@ words (e.g. `"Mother Tongue (Number of Canadian Residents)"`) as arguments to to format the plot further, and we will explore these in the {ref}`viz` chapter. ```{code-cell} ipython3 -barplot_mother_tongue = ( - alt.Chart(ten_lang) - .mark_bar().encode( - x=alt.X('language', title='Language'), - y=alt.Y('mother_tongue', title='Mother Tongue (Number of Canadian Residents)') - ) +barplot_mother_tongue = alt.Chart(ten_lang).mark_bar().encode( + x=alt.X('language', title='Language'), + y=alt.Y('mother_tongue', title='Mother Tongue (Number of Canadian Residents)') ) ``` @@ -914,12 +911,9 @@ To accomplish this, we will swap the x and y coordinate axes: ```{code-cell} ipython3 -barplot_mother_tongue_axis = ( - alt.Chart(ten_lang) - .mark_bar().encode( - x=alt.X('mother_tongue', title='Mother Tongue (Number of Canadian Residents)'), - y=alt.Y('language', title='Language') - ) +barplot_mother_tongue_axis = alt.Chart(ten_lang).mark_bar().encode( + x=alt.X('mother_tongue', title='Mother Tongue (Number of Canadian Residents)'), + y=alt.Y('language', title='Language') ) ``` @@ -950,12 +944,9 @@ the `sort` argument, which orders a variable (here `language`) based on the values of the variable(`mother_tongue`) on the `x-axis`. ```{code-cell} ipython3 -ordered_barplot_mother_tongue = ( - alt.Chart(ten_lang) - .mark_bar().encode( - x=alt.X('mother_tongue', title='Mother Tongue (Number of Canadian Residents)'), - y=alt.Y('language', sort='x', title='Language') - ) +ordered_barplot_mother_tongue = alt.Chart(ten_lang).mark_bar().encode( + x=alt.X('mother_tongue', title='Mother Tongue (Number of Canadian Residents)'), + y=alt.Y('language', sort='x', title='Language') ) ``` @@ -1027,17 +1018,13 @@ ten_lang = ( can_lang.loc[can_lang["category"] == "Aboriginal languages", ["language", "mother_tongue"]] .sort_values(by="mother_tongue", ascending=False) .head(10) - ) +) # create the visualization -ten_lang_plot = ( - alt.Chart(ten_lang) - .mark_bar().encode( - x=alt.X('mother_tongue', title='Mother Tongue (Number of Canadian Residents)'), - y=alt.Y('language', sort='x', title='Language') - )) - - +ten_lang_plot = alt.Chart(ten_lang).mark_bar().encode( + x=alt.X('mother_tongue', title='Mother Tongue (Number of Canadian Residents)'), + y=alt.Y('language', sort='x', title='Language') +) ``` ```{code-cell} ipython3 diff --git a/source/regression1.md b/source/regression1.md index 2368aec7..c170f5f5 100644 --- a/source/regression1.md +++ b/source/regression1.md @@ -182,13 +182,9 @@ want to predict (sale price) on the y-axis. ```{code-cell} ipython3 :tags: [remove-output] -eda = ( - alt.Chart(sacramento) - .mark_circle() - .encode( - x=alt.X("sqft", title="House size (square feet)", scale=alt.Scale(zero=False)), - y=alt.Y("price", title="Price (USD)", axis=alt.Axis(format='$,.0f')), - ) +eda = alt.Chart(sacramento).mark_circle().encode( + x=alt.X("sqft", title="House size (square feet)", scale=alt.Scale(zero=False)), + y=alt.Y("price", title="Price (USD)", axis=alt.Axis(format='$,.0f')), ) eda @@ -257,13 +253,9 @@ the sale price? ```{code-cell} ipython3 :tags: [remove-output] -small_plot = ( - alt.Chart(small_sacramento) - .mark_circle() - .encode( - x=alt.X("sqft", title="House size (square feet)", scale=alt.Scale(zero=False)), - y=alt.Y("price", title="Price (USD)", axis=alt.Axis(format='$,.0f')), - ) +small_plot = alt.Chart(small_sacramento).mark_circle().encode( + x=alt.X("sqft", title="House size (square feet)", scale=alt.Scale(zero=False)), + y=alt.Y("price", title="Price (USD)", axis=alt.Axis(format='$,.0f')), ) # add an overlay to the base plot @@ -868,13 +860,9 @@ sacr_preds = sacr_preds.assign( ) # the base plot: the training data scatter plot -base_plot = ( - alt.Chart(sacramento_train) - .mark_circle() - .encode( - x=alt.X("sqft", title="House size (square feet)", scale=alt.Scale(zero=False)), - y=alt.Y("price", title="Price (USD)", axis=alt.Axis(format="$,.0f")), - ) +base_plot = alt.Chart(sacramento_train).mark_circle().encode( + x=alt.X("sqft", title="House size (square feet)", scale=alt.Scale(zero=False)), + y=alt.Y("price", title="Price (USD)", axis=alt.Axis(format="$,.0f")), ) # add the prediction layer @@ -932,13 +920,9 @@ to help predict the sale price of a house. ```{code-cell} ipython3 :tags: [remove-output] -plot_beds = ( - alt.Chart(sacramento) - .mark_circle() - .encode( - x=alt.X("beds", title="Number of Bedrooms"), - y=alt.Y("price", title="Price (USD)", axis=alt.Axis(format="$,.0f")), - ) +plot_beds = alt.Chart(sacramento).mark_circle().encode( + x=alt.X("beds", title="Number of Bedrooms"), + y=alt.Y("price", title="Price (USD)", axis=alt.Axis(format="$,.0f")), ) plot_beds diff --git a/source/regression2.md b/source/regression2.md index b80d0dbb..fc6d2eb4 100644 --- a/source/regression2.md +++ b/source/regression2.md @@ -480,17 +480,13 @@ linear regression predicted line of best fit. ```{code-cell} ipython3 :tags: [remove-output] -lm_plot_final = ( - alt.Chart(sacramento_train) - .mark_circle() - .encode( - x=alt.X("sqft", title="House size (square feet)", scale=alt.Scale(zero=False)), - y=alt.Y( - "price", - title="Price (USD)", - axis=alt.Axis(format="$,.0f"), - scale=alt.Scale(zero=False), - ), +lm_plot_final = alt.Chart(sacramento_train).mark_circle().encode( + x=alt.X("sqft", title="House size (square feet)", scale=alt.Scale(zero=False)), + y=alt.Y( + "price", + title="Price (USD)", + axis=alt.Axis(format="$,.0f"), + scale=alt.Scale(zero=False), ) ) From b3e01aff556d30ddc82d8e848893b8e04831d32a Mon Sep 17 00:00:00 2001 From: Joel Ostblom Date: Wed, 15 Feb 2023 11:19:38 -0800 Subject: [PATCH 31/44] Commit changes to saved svg chart --- source/img/faithful_plot.svg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/img/faithful_plot.svg b/source/img/faithful_plot.svg index 21282faf..4c8430a8 100644 --- a/source/img/faithful_plot.svg +++ b/source/img/faithful_plot.svg @@ -1 +1 @@ -0102030405060708090100Waiting Time (mins)0.00.51.01.52.02.53.03.54.04.55.05.5Eruption Duration (mins) \ No newline at end of file +0102030405060708090100Waiting Time (mins)0.00.51.01.52.02.53.03.54.04.55.05.5Eruption Duration (mins) \ No newline at end of file From 461371e24965f9e2beff4456be1e23016a26b237 Mon Sep 17 00:00:00 2001 From: Joel Ostblom Date: Wed, 15 Feb 2023 11:24:01 -0800 Subject: [PATCH 32/44] Add explicit note regarding code syntax --- source/intro.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/source/intro.md b/source/intro.md index fa556a11..c2bc7705 100644 --- a/source/intro.md +++ b/source/intro.md @@ -864,7 +864,9 @@ example above, Python uses the column name `mother_tongue` as the label for the y axis, but most people will not know what that is. And even if they did, they will not know how we measured this variable, or the group of people on which the measurements were taken. An axis label that reads "Mother Tongue (Number of -Canadian Residents)" would be much more informative. +Canadian Residents)" would be much more informative. To make the code easier to +read, we're spreading it out over multiple lines just as we did in the previous +section with pandas. ```{index} plot; labels, plot; axis labels ``` From a6d2768a3f2cf2fd9cd37c834b2e11442cc4086a Mon Sep 17 00:00:00 2001 From: Joel Ostblom Date: Wed, 15 Feb 2023 11:27:39 -0800 Subject: [PATCH 33/44] Explicitly mention re-using chart variable --- source/viz.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/source/viz.md b/source/viz.md index 912845f0..d0ce03a9 100644 --- a/source/viz.md +++ b/source/viz.md @@ -1637,6 +1637,8 @@ us to compare the location of the histograms along the `x`-axis in the different subplots. We also reduce the height of each chart so that they all fit in the same view. +Note that we are re-using the chart we created just above, +instead of re-creating the same chart from scratch. ```{code-cell} ipython3 From b06dcaa26cc0a9dae2cb5f909fb9fe9fb11a5b04 Mon Sep 17 00:00:00 2001 From: Joel Ostblom Date: Wed, 15 Feb 2023 11:49:10 -0800 Subject: [PATCH 34/44] Extend faded prediction area to the axes limits in stead of having a white border around it --- source/classification1.md | 32 +++++++++++++++++++------------- source/classification2.md | 26 ++++++++++++++++++++++---- 2 files changed, 41 insertions(+), 17 deletions(-) diff --git a/source/classification1.md b/source/classification1.md index ed0de113..8cc99bcb 100644 --- a/source/classification1.md +++ b/source/classification1.md @@ -1546,10 +1546,10 @@ knn.fit(X=rare_cancer.loc[:, ["Perimeter", "Concavity"]], y=rare_cancer["Class"] # create a prediction pt grid per_grid = np.linspace( - rare_cancer["Perimeter"].min(), rare_cancer["Perimeter"].max(), 50 + rare_cancer["Perimeter"].min() * 1.05, rare_cancer["Perimeter"].max() * 1.05, 50 ) con_grid = np.linspace( - rare_cancer["Concavity"].min(), rare_cancer["Concavity"].max(), 50 + rare_cancer["Concavity"].min() * 1.05, rare_cancer["Concavity"].max() * 1.05, 50 ) pcgrid = np.array(np.meshgrid(per_grid, con_grid)).reshape(2, -1).T pcgrid = pd.DataFrame(pcgrid, columns=["Perimeter", "Concavity"]) @@ -1585,14 +1585,16 @@ prediction_plot = ( "Perimeter", title="Perimeter (standardized)", scale=alt.Scale( - domain=(rare_cancer["Perimeter"].min(), rare_cancer["Perimeter"].max()) + domain=(rare_cancer["Perimeter"].min() * 1.05, rare_cancer["Perimeter"].max() * 1.05), + nice=False ), ), y=alt.Y( "Concavity", title="Concavity (standardized)", scale=alt.Scale( - domain=(rare_cancer["Concavity"].min(), rare_cancer["Concavity"].max()) + domain=(rare_cancer["Concavity"].min() * 1.05, rare_cancer["Concavity"].max() * 1.05), + nice=False ), ), color=alt.Color("Class", title="Diagnosis"), @@ -1676,14 +1678,16 @@ rare_plot = ( "Perimeter", title="Perimeter (standardized)", scale=alt.Scale( - domain=(rare_cancer["Perimeter"].min(), rare_cancer["Perimeter"].max()) + domain=(rare_cancer["Perimeter"].min() * 1.05, rare_cancer["Perimeter"].max() * 1.05), + nice=False ), ), y=alt.Y( "Concavity", title="Concavity (standardized)", scale=alt.Scale( - domain=(rare_cancer["Concavity"].min(), rare_cancer["Concavity"].max()) + domain=(rare_cancer["Concavity"].min() * 1.05, rare_cancer["Concavity"].max() * 1.05), + nice=False ), ), color=alt.Color("Class", title="Diagnosis"), @@ -1800,10 +1804,10 @@ import numpy as np # create the grid of area/smoothness vals, and arrange in a data frame are_grid = np.linspace( - unscaled_cancer["Area"].min(), unscaled_cancer["Area"].max(), 50 + unscaled_cancer["Area"].min() * 0.95, unscaled_cancer["Area"].max() * 1.05, 50 ) smo_grid = np.linspace( - unscaled_cancer["Smoothness"].min(), unscaled_cancer["Smoothness"].max(), 50 + unscaled_cancer["Smoothness"].min() * 0.95, unscaled_cancer["Smoothness"].max() * 1.05, 50 ) asgrid = np.array(np.meshgrid(are_grid, smo_grid)).reshape(2, -1).T asgrid = pd.DataFrame(asgrid, columns=["Area", "Smoothness"]) @@ -1827,17 +1831,19 @@ unscaled_plot = ( "Area", title="Area", scale=alt.Scale( - domain=(unscaled_cancer["Area"].min(), unscaled_cancer["Area"].max()) - ), + domain=(unscaled_cancer["Area"].min() * 0.95, unscaled_cancer["Area"].max() * 1.05), + nice=False + ) ), y=alt.Y( "Smoothness", title="Smoothness", scale=alt.Scale( domain=( - unscaled_cancer["Smoothness"].min(), - unscaled_cancer["Smoothness"].max(), - ) + unscaled_cancer["Smoothness"].min() * 0.95, + unscaled_cancer["Smoothness"].max() * 1.05, + ), + nice=False ), ), color=alt.Color("Class", title="Diagnosis"), diff --git a/source/classification2.md b/source/classification2.md index d2783b7f..7a6795ce 100644 --- a/source/classification2.md +++ b/source/classification2.md @@ -1256,10 +1256,10 @@ y = cancer_train["Class"] # create a prediction pt grid smo_grid = np.linspace( - cancer_train["Smoothness"].min(), cancer_train["Smoothness"].max(), 100 + cancer_train["Smoothness"].min() * 0.95, cancer_train["Smoothness"].max() * 1.05, 100 ) con_grid = np.linspace( - cancer_train["Concavity"].min(), cancer_train["Concavity"].max(), 100 + cancer_train["Concavity"].min() - 0.025, cancer_train["Concavity"].max() * 1.05, 100 ) scgrid = np.array(np.meshgrid(smo_grid, con_grid)).reshape(2, -1).T scgrid = pd.DataFrame(scgrid, columns=["Smoothness", "Concavity"]) @@ -1281,8 +1281,26 @@ for k in [1, 7, 20, 300]: ) .mark_point(opacity=0.2, filled=True, size=20) .encode( - x=alt.X("Smoothness"), - y=alt.Y("Concavity"), + x=alt.X( + "Smoothness", + scale=alt.Scale( + domain=( + cancer_train["Smoothness"].min() * 0.95, + cancer_train["Smoothness"].max() * 1.05 + ), + nice=False + ) + ), + y=alt.Y( + "Concavity", + scale=alt.Scale( + domain=( + cancer_train["Concavity"].min() -0.025, + cancer_train["Concavity"].max() * 1.05 + ), + nice=False + ) + ), color=alt.Color("Class", title="Diagnosis"), ) ) From 3f8ac8021bc6b9ac278140fc1516509943fd8099 Mon Sep 17 00:00:00 2001 From: Joel Ostblom Date: Wed, 15 Feb 2023 15:12:03 -0800 Subject: [PATCH 35/44] Commit changes to saved png chart --- source/img/faithful_plot.png | Bin 29858 -> 39745 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/source/img/faithful_plot.png b/source/img/faithful_plot.png index a0e986de8dc6bba53cb89d28a8b37265bf7ec208..aef941e1ac2c84f2ccc54c34042997894c3af398 100644 GIT binary patch literal 39745 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 literal 29858 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 From 3f2805d262fca03467a048f17cf206b9edaa926f Mon Sep 17 00:00:00 2001 From: Trevor Campbell Date: Fri, 7 Jul 2023 09:23:57 -0700 Subject: [PATCH 36/44] viz ref in intro --- source/intro.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/intro.md b/source/intro.md index c2bc7705..ac32aae3 100644 --- a/source/intro.md +++ b/source/intro.md @@ -874,7 +874,7 @@ section with pandas. Adding additional labels to our visualizations that we create in `altair` is one common and easy way to improve and refine our data visualizations. We can add titles for the axes in the `altair` objects using `alt.X` and `alt.Y` with the `title` argument to make -the axes titles more informative (you will learn more about `alt.X` and `alt.Y` in the visualization chapter). +the axes titles more informative (you will learn more about `alt.X` and `alt.Y` in the {ref}`viz` chapter). Again, since we are specifying words (e.g. `"Mother Tongue (Number of Canadian Residents)"`) as arguments to `alt.X` and `alt.Y`, we surround them with double quotation marks. We can do many other modifications From 40d1eb6d4aa9222c67ab3d742234149d6151b3d2 Mon Sep 17 00:00:00 2001 From: Joel Ostblom Date: Wed, 12 Jul 2023 15:22:04 -0700 Subject: [PATCH 37/44] Fix scale to not squish horizontally --- source/classification2.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/classification2.md b/source/classification2.md index 7a6795ce..7fa24e8c 100644 --- a/source/classification2.md +++ b/source/classification2.md @@ -331,7 +331,7 @@ cancer['Class'] = cancer['Class'].replace({ # labeling the points be diagnosis class perim_concav = alt.Chart(cancer).mark_circle().encode( - x="Smoothness", + x=alt.X("Smoothness", scale=alt.Scale(zero=False)), y="Concavity", color=alt.Color("Class", title="Diagnosis"), ) From 379c54f03603dbdf6b05313e3d6b9702f4893b74 Mon Sep 17 00:00:00 2001 From: Joel Ostblom Date: Wed, 12 Jul 2023 15:22:46 -0700 Subject: [PATCH 38/44] Improve language --- source/viz.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/source/viz.md b/source/viz.md index d0ce03a9..97ee51a4 100644 --- a/source/viz.md +++ b/source/viz.md @@ -613,7 +613,7 @@ can_lang = can_lang[(can_lang['most_at_home'] > 0) & (can_lang['mother_tongue'] ``` We will begin with a scatter plot of the `mother_tongue` and `most_at_home` columns from our data frame. -As you have seen in the scatter plots in the previous section +As we have seen in the scatter plots in the previous section, the default behavior of `mark_point` is to draw the outline of each point. If we would like to fill them in, we can pass the argument `filled=True` to `mark_point` @@ -771,7 +771,7 @@ glue('can_lang_plot_log', can_lang_plot_log, display=False) :figwidth: 700px :name: can_lang_plot_log -Scatter plot of number of Canadians reporting a language as their mother tongue vs the primary language at home with log adjusted x and y axes. +Scatter plot of number of Canadians reporting a language as their mother tongue vs the primary language at home with log-adjusted x and y axes. ::: You will notice two things in the chart above, @@ -811,7 +811,7 @@ glue('can_lang_plot_log_revised', can_lang_plot_log_revised, display=False) :figwidth: 700px :name: can_lang_plot_log_revised -Scatter plot of number of Canadians reporting a language as their mother tongue vs the primary language at home with log adjusted x and y axes. Only the major gridlines are shown and the suffix "k" indicate 1,000 ("kilo"), while the suffix "M" indicates 1,000,000 ("million"). +Scatter plot of number of Canadians reporting a language as their mother tongue vs the primary language at home with log-adjusted x and y axes. Only the major gridlines are shown. The suffix "k" indicates 1,000 ("kilo"), while the suffix "M" indicates 1,000,000 ("million"). ::: From d992bc76532b25aa59a849c225bde77662aa4d86 Mon Sep 17 00:00:00 2001 From: Joel Ostblom Date: Wed, 12 Jul 2023 15:22:55 -0700 Subject: [PATCH 39/44] Add explanation of underlines in numbers --- source/viz.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/source/viz.md b/source/viz.md index 97ee51a4..67b7dc95 100644 --- a/source/viz.md +++ b/source/viz.md @@ -843,6 +843,10 @@ language as their mother tongue and primary language at home for all the languages in the `can_lang` data set. Since the new columns are appended to the end of the data table, we selected the new columns after the transformation so you can clearly see the mutated output from the table. +Note that we formatted the number for the Canadian population +using `_` so that it is easier to read; +this does not affect how Python interprets the number +and is just added for readability. ```{index} pandas.DataFrame; assign, pandas.DataFrame; [[]] ``` From cdbf5b8aa111cf0627fb9eb6ec0aec473e1c9507 Mon Sep 17 00:00:00 2001 From: Joel Ostblom Date: Wed, 12 Jul 2023 15:37:44 -0700 Subject: [PATCH 40/44] Improve language --- source/viz.md | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/source/viz.md b/source/viz.md index 67b7dc95..bd184861 100644 --- a/source/viz.md +++ b/source/viz.md @@ -864,9 +864,10 @@ Next, we will edit the visualization to use the percentages we just computed (and change our axis labels to reflect this change in units). {numref}`can_lang_plot_percent` displays the final result. -Here all the tick labels fit by default so we are not changing the labels to include suffixes -(note that suffixes can also be harder to understand for small quantities, -so they are best to avoid for small numbers unless you are communicating to a technical audience). +Here all the tick labels fit by default so we are not changing the labels to include suffixes. +Note that suffixes can also be harder to understand, +so it is often advisable to avoid them (particularly for small quantities) +unless you are communicating to a technical audience. ```{code-cell} ipython3 can_lang_plot_percent = alt.Chart(can_lang).mark_circle().encode( @@ -1043,11 +1044,11 @@ if your visualizations are color-blind friendly. ```{index} color palette; color blindness simulator ``` -All the available color schemes and information on how to create your own, can be viewed [in the documentation](https://altair-viz.github.io/user_guide/customization.html#customizing-colors). +All the available color schemes and information on how to create your own can be viewed [in the Altair documentation](https://altair-viz.github.io/user_guide/customization.html#customizing-colors). To change the color scheme of our chart, we can add the `scheme` argument in the `scale` of the `color` encoding. Below we pick the `"dark2"` theme, with the result shown -in {numref}`can_lang_plot_theme` +in {numref}`can_lang_plot_theme`. We also set the `shape` aesthetic mapping to the `category` variable as well; this makes the scatter point shapes different for each language category. This kind of visual redundancy—i.e., conveying the same information with both scatter point color and shape—can @@ -1193,7 +1194,7 @@ Here, we have a data frame of Earth's landmasses, and are trying to compare their sizes. The right type of visualization to answer this question is a bar plot. In a bar plot, the height of the bar represents the value of a summary statistic -(usually a size, count, sum, proportion or percentage). +(usually a size, count, sum, proportion, or percentage). They are particularly useful for comparing summary statistics between different groups of a categorical variable. @@ -1239,7 +1240,7 @@ the `nlargest` function; the first argument is the number of rows we want and the second is the name of the column we want to use for comparing who is largest. Then to help make the landmass labels easier to read we'll swap the `x` and `y` variables, -so that the landmass labels are on the y-axis and we don't have to tilt our head to read them. +so that the labels are on the y-axis and we don't have to tilt our head to read them. ```{index} pandas.DataFrame; nlargest ``` @@ -1266,13 +1267,15 @@ Bar plot of size for Earth's largest 12 landmasses. ::: -The plot in {numref}`islands_bar_top` is clearer now, +The plot in {numref}`islands_bar_top` is definitely clearer now, and allows us to answer our question: -"Which are the top 7 largest landmasses continents?". +"Which of the top 7 largest landmasses are continents?". However, we could still improve this visualization -by organizing the bars by landmass size rather than by alphabetical order. -We could also color the bars based on whether they are a continent or not -to add additional information to the chart. +by organizing the bars by landmass size rather than by alphabetical order +and by coloring the bars based on whether they correspond to a continent. +The data for this is stored in the `landmass_type` column. +TO use this to color the bars, +we set `color` encoding to `landmass_type`. To organize the landmasses by their `size` variable, we will use the altair `sort` function @@ -1283,7 +1286,7 @@ This plots the values on `y` axis in the ascending order of `x` axis values. This creates a chart where the largest bar is the closest to the axis line, which is generally the most visually appealing when sorting bars. -If instead, +If instead we want to sort the values on `y-axis` in descending order of `x-axis`, we can add a minus sign to reverse the order and specify `sort='-x'`. From 5ed32f004104c373f96d480c4d801c2585413b3f Mon Sep 17 00:00:00 2001 From: Joel Ostblom Date: Thu, 13 Jul 2023 09:59:29 -0700 Subject: [PATCH 41/44] Clarify question wording --- source/viz.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/source/viz.md b/source/viz.md index bd184861..743c662c 100644 --- a/source/viz.md +++ b/source/viz.md @@ -1268,8 +1268,9 @@ Bar plot of size for Earth's largest 12 landmasses. The plot in {numref}`islands_bar_top` is definitely clearer now, -and allows us to answer our question: -"Which of the top 7 largest landmasses are continents?". +and allows us to answer our initial questions: +"Are the seven continents Earth's largest landmasses?" +and "Which are the next few largest landmasses?". However, we could still improve this visualization by organizing the bars by landmass size rather than by alphabetical order and by coloring the bars based on whether they correspond to a continent. From b531709d844fb3f1d87d53f35915b1acd3342f40 Mon Sep 17 00:00:00 2001 From: Joel Ostblom Date: Thu, 13 Jul 2023 17:38:15 -0700 Subject: [PATCH 42/44] Remove bar chart explanation --- source/viz.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/source/viz.md b/source/viz.md index 743c662c..1bb57226 100644 --- a/source/viz.md +++ b/source/viz.md @@ -1198,12 +1198,6 @@ In a bar plot, the height of the bar represents the value of a summary statistic They are particularly useful for comparing summary statistics between different groups of a categorical variable. -> **Note:** Although bar charts are often used to display mean and median values, -> they are generally a poor choice for these types of visualizations and should be avoided. -> The reason for this is that they hide all the variation in the data and only show a single value. -> Instead it's better to show the distribution of all the individual data points -> (potentially together with an additional visual mark for the mean or median). - ```{index} altair; mark_bar ``` From 03b6d36253fc673ba178b1aba974bca69b25a4b9 Mon Sep 17 00:00:00 2001 From: Joel Ostblom Date: Thu, 13 Jul 2023 17:38:20 -0700 Subject: [PATCH 43/44] Fix wording --- source/viz.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/source/viz.md b/source/viz.md index 1bb57226..7ba35489 100644 --- a/source/viz.md +++ b/source/viz.md @@ -1269,8 +1269,8 @@ However, we could still improve this visualization by organizing the bars by landmass size rather than by alphabetical order and by coloring the bars based on whether they correspond to a continent. The data for this is stored in the `landmass_type` column. -TO use this to color the bars, -we set `color` encoding to `landmass_type`. +To use this to color the bars, +we set the `color` encoding to `landmass_type`. To organize the landmasses by their `size` variable, we will use the altair `sort` function From 888c07e348c9c193fa93e8891a690a36220a31c2 Mon Sep 17 00:00:00 2001 From: Joel Ostblom Date: Thu, 13 Jul 2023 17:42:21 -0700 Subject: [PATCH 44/44] Remove title section --- source/viz.md | 39 --------------------------------------- 1 file changed, 39 deletions(-) diff --git a/source/viz.md b/source/viz.md index 7ba35489..735297fd 100644 --- a/source/viz.md +++ b/source/viz.md @@ -1314,45 +1314,6 @@ visualization for answering our original questions. Landmasses are organized by their size, and continents are colored differently than other landmasses, making it quite clear that all the seven largest landmasses are continents. -To add some finishing touches to the chart -we will add a title to the chart by specifying `title` argument inside `alt.Chart`. -A good plot title should usually contain the take home message that you want readers of the chart to focus on, -e.g. "The Earth's seven largest landmasses are all continents", -but it could sometimes be more general, e.g. "The twelve largest landmasses on Earth". -Note that plot titles are not always required; e.g. if plots appear as part -of other media (e.g., in a slide presentation, on a poster, in a paper) where -the title may be redundant with the surrounding context. -For categorical encodings, -such as the color and y channels in our chart, -it is often not necessary to include the axis title -as the labels of the categories are enough by themselves. -Particularly in this case where the title clearly states -that we are landmasses, -the titles are redundant and we can remove them. - -```{code-cell} ipython3 -islands_plot_titled = alt.Chart( - islands_top12, - title="The Earth's seven largest landmasses are all continents" -).mark_bar().encode( - x=alt.X("size",title="Size (1000 square mi)"), - y=alt.Y("landmass", title="", sort="x"), - color=alt.Color("landmass_type", title="") -) -``` - -```{code-cell} ipython3 -:tags: ["remove-cell"] -glue('islands_plot_titled', islands_plot_titled, display=True) -``` - -:::{glue:figure} islands_plot_titled -:figwidth: 700px -:name: islands_plot_titled - -Bar plot of size for Earth's largest 12 landmasses with a title. -::: - ### Histograms: the Michelson speed of light data set ```{index} Michelson speed of light