diff --git a/Dockerfile b/Dockerfile index 8e57cda2..1a31ee60 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,6 +18,7 @@ RUN mamba install --quiet --yes \ 'numpy' \ 'jinja2' \ 'altair_data_server' \ + 'altair_saver' \ 'click' \ 'ibis-framework' \ 'ghp-import' \ diff --git a/build_html.sh b/build_html.sh index a7a3f798..f68c05c9 100755 --- a/build_html.sh +++ b/build_html.sh @@ -1,2 +1,2 @@ chmod -R o+w source/ -docker run --rm -v $(pwd):/home/jovyan ubcdsci/py-intro-to-ds:202212191809333bdc71 /bin/bash -c "jupyter-book build source" +docker run --rm -v $(pwd):/home/jovyan ubcdsci/py-intro-to-ds:20230104230634037f38 /bin/bash -c "jupyter-book build source" diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 7e821e45..00000000 --- a/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -jupyter-book -matplotlib -numpy diff --git a/source/_config.yml b/source/_config.yml index 93a176b0..779e497b 100644 --- a/source/_config.yml +++ b/source/_config.yml @@ -1,11 +1,7 @@ -####################################################################################### -# Config file for EOSC211 jupyter book -####################################################################################### # Book settings - -title: DSCĪ™ 100 +title: "Data Science: A First Introduction (Python Edition)" author: UBC -copyright: "2021" # Copyright year to be placed in the footer +copyright: "2022" # Copyright year to be placed in the footer logo: "" # A path to the book logo # Patterns to skip when building the book. Can be glob-style (e.g. "*skip.ipynb") exclude_patterns: [_build, Thumbs.db, .DS_Store, "*.ipynb_checkpoints"] @@ -15,10 +11,10 @@ only_build_toc_files: true ####################################################################################### # Execution settings execute: - execute_notebooks: "cache" # Whether to execute notebooks at build time. Must be one of ("auto", "force", "cache", "off") + execute_notebooks: "auto" # Whether to execute notebooks at build time. Must be one of ("auto", "force", "cache", "off") cache: "" # A path to the jupyter cache that will be used to store execution artifacts. Defaults to `_build/.jupyter_cache/` # exclude_patterns: [] # A list of patterns to *skip* in execution (e.g. a notebook that takes a really long time) - timeout: 30 # The maximum time (in seconds) each notebook cell is allowed to run. + timeout: 90 # The maximum time (in seconds) each notebook cell is allowed to run. run_in_temp: false # If `True`, then a temporary directory will be created and used as the command working directory (cwd), # otherwise the notebook's parent directory will be the cwd. @@ -65,19 +61,15 @@ latex: latex_engine: pdflatex # one of 'pdflatex', 'xelatex' (recommended for unicode), 'luatex', 'platex', 'uplatex' use_jupyterbook_latex: true # use sphinx-jupyterbook-latex for pdf builds as default - ####################################################################################### - # Launch button settings launch_buttons: binderhub_url: "" - - repository: - url: https://github.com/phaustin/eosc211_students # The URL to your book's repository - path_to_book: "" # A path to your book's folder, relative to the repository root. - branch: e211_live_main # Which branch of the repository should be used when creating links + url: https://github.com/UBC-DSCI/introduction-to-datascience-python # The URL to your book's repository + path_to_book: "source" # A path to your book's folder, relative to the repository root. + branch: production # Which branch of the repository should be used when creating links ####################################################################################### # Advanced and power-user settings diff --git a/source/_toc.yml b/source/_toc.yml index 61dfc676..58497d23 100644 --- a/source/_toc.yml +++ b/source/_toc.yml @@ -1,15 +1,18 @@ format: jb-book root: index.md -options: - numbered: true parts: -- caption: First draft +- caption: Front Matter chapters: - file: preface-text.md - - file: foreword-text.md + #- file: foreword.md - file: acknowledgements.md + - file: acknowledgements-python.md - file: authors.md - - file: setup.md + - file: editors.md + #- file: setup.md +- caption: Chapters + numbered: 3 + chapters: - file: intro.md - file: reading.md - file: wrangling.md @@ -20,5 +23,7 @@ parts: - file: regression2.md - file: clustering.md - file: inference.md - - file: references.md +- caption: Appendix + chapters: - file: appendixA.md + #- file: references.md diff --git a/source/acknowledgements-python.md b/source/acknowledgements-python.md new file mode 100644 index 00000000..dc687718 --- /dev/null +++ b/source/acknowledgements-python.md @@ -0,0 +1,25 @@ +--- +jupytext: + cell_metadata_filter: -all + formats: py:percent,md:myst,ipynb + text_representation: + extension: .md + format_name: myst + format_version: 0.13 + jupytext_version: 1.13.8 +kernelspec: + display_name: Python 3 (ipykernel) + language: python + name: python3 +--- + +# Acknowledgments for the Python Edition + +We'd like to thank everyone that has contributed to the development of +[*Data Science: A First Introduction (Python Edition)*](https://ubc-dsci.github.io/introduction-to-datascience-python/). +This is an open source Python translation of the original [*Data Science: A First Introduction*](https://datasciencebook.ca); +the original focused on the R programming language. Both of these books are +used to teach DSCI 100, a new introductory data science course +at the University of British Columbia (UBC). + +We will finalize this acknowledgements section after the book is complete! diff --git a/source/acknowledgements.md b/source/acknowledgements.md index e0ec1699..82ecc5c7 100644 --- a/source/acknowledgements.md +++ b/source/acknowledgements.md @@ -13,7 +13,7 @@ kernelspec: name: python3 --- -# Acknowledgments -- TBD +# Acknowledgments We'd like to thank everyone that has contributed to the development of [*Data Science: A First Introduction*](https://datasciencebook.ca). diff --git a/source/appendixA.md b/source/appendixA.md index a1e1bcc3..7e57bf72 100644 --- a/source/appendixA.md +++ b/source/appendixA.md @@ -13,9 +13,7 @@ kernelspec: name: python3 --- -# Appendix - -# Downloading files from JupyterHub {#appendixA} +# Downloading files from JupyterHub This section will help you save your work from a JupyterHub web-based platform to your own computer. diff --git a/source/authors.md b/source/authors.md index b6465c76..7e6dc803 100644 --- a/source/authors.md +++ b/source/authors.md @@ -13,7 +13,7 @@ kernelspec: name: python3 --- -# About the authors -- TBD +# About the authors **Tiffany Timbers** is an Assistant Professor of Teaching in the Department of Statistics and Co-Director for the Master of Data Science program (Vancouver diff --git a/source/editors.md b/source/editors.md new file mode 100644 index 00000000..dedb5171 --- /dev/null +++ b/source/editors.md @@ -0,0 +1,51 @@ +--- +jupytext: + cell_metadata_filter: -all + formats: py:percent,md:myst,ipynb + text_representation: + extension: .md + format_name: myst + format_version: 0.13 + jupytext_version: 1.13.8 +kernelspec: + display_name: Python 3 (ipykernel) + language: python + name: python3 +--- + +# About the editors of the Python Edition + +**Trevor Campbell** is an Assistant Professor in the Department of Statistics at +the University of British Columbia. His research focuses on automated, scalable +Bayesian inference algorithms, Bayesian nonparametrics, streaming data, and +Bayesian theory. He was previously a postdoctoral associate advised by Tamara +Broderick in the Computer Science and Artificial Intelligence Laboratory +(CSAIL) and Institute for Data, Systems, and Society (IDSS) at MIT, a Ph.D. +candidate under Jonathan How in the Laboratory for Information and Decision +Systems (LIDS) at MIT, and before that he was in the Engineering Science +program at the University of Toronto. + ++++ + +**Lindsey Heagy** is an Assistant Professor in the Department of Earth, Ocean, and Atmospheric +Sciences and director of the Geophysical Inversion Facility at the University of British Columbia. +Her research combines computational methods in numerical simulations, inversions, and machine +learning to answer questions about the subsurface of the Earth. Primary applications include +mineral exploration, carbon sequestration, groundwater and environmental studies. She +completed her BSc at the University of Alberta, her PhD at the University of British Columbia, +and held a Postdoctoral research position at the University of California Berkeley prior to +starting her current position at UBC. + ++++ + +**Joel Ostblom** is an Assistant Professor of Teaching in the Department of +Statistics at the University of British Columbia. +During his PhD, Joel developed a passion for data science and reproducibility +through the development of quantitative image analysis pipelines for studying +stem cell and developmental biology. He has since co-created or lead the +development of several courses and workshops at the University of Toronto and +is now an assistant professor of teaching in the statistics department at the +University of British Columbia. Joel cares deeply about spreading data literacy +and excitement over programmatic data analysis, which is reflected in his +contributions to open source projects and data science learning resources. You +can read more about Joel on his [personal page](https://joelostblom.com/). diff --git a/source/img/altair_syntax.png b/source/img/altair_syntax.png new file mode 100644 index 00000000..55676cdb Binary files /dev/null and b/source/img/altair_syntax.png differ diff --git a/source/img/code-figures.pptx b/source/img/code-figures.pptx new file mode 100644 index 00000000..e671a57b Binary files /dev/null and b/source/img/code-figures.pptx differ diff --git a/source/img/completion_menu.png b/source/img/completion_menu.png new file mode 100644 index 00000000..1de73d77 Binary files /dev/null and b/source/img/completion_menu.png differ diff --git a/source/img/data_frame_slides_cdn/data_frame_slides_cdn.001.jpeg b/source/img/data_frame_slides_cdn/data_frame_slides_cdn.001.jpeg index fa1e065d..276300cc 100644 Binary files a/source/img/data_frame_slides_cdn/data_frame_slides_cdn.001.jpeg and b/source/img/data_frame_slides_cdn/data_frame_slides_cdn.001.jpeg differ diff --git a/source/img/data_frame_slides_cdn/data_frame_slides_cdn.002.jpeg b/source/img/data_frame_slides_cdn/data_frame_slides_cdn.002.jpeg index 4fcf0966..b29831ee 100644 Binary files a/source/img/data_frame_slides_cdn/data_frame_slides_cdn.002.jpeg and b/source/img/data_frame_slides_cdn/data_frame_slides_cdn.002.jpeg differ diff --git a/source/img/data_frame_slides_cdn/data_frame_slides_cdn.004.jpeg b/source/img/data_frame_slides_cdn/data_frame_slides_cdn.004.jpeg index ae68f0d2..8675de1e 100644 Binary files a/source/img/data_frame_slides_cdn/data_frame_slides_cdn.004.jpeg and b/source/img/data_frame_slides_cdn/data_frame_slides_cdn.004.jpeg differ diff --git a/source/img/faithful_plot.png b/source/img/faithful_plot.png index fa93f603..a0e986de 100644 Binary files a/source/img/faithful_plot.png and b/source/img/faithful_plot.png differ diff --git a/source/img/faithful_plot.svg b/source/img/faithful_plot.svg index cf6ae779..21282faf 100644 --- a/source/img/faithful_plot.svg +++ b/source/img/faithful_plot.svg @@ -1,346 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -2 -3 -4 -5 - - - - - - - - - -50 -60 -70 -80 -90 -Waiting time to next eruption - (minutes) -Eruption time - (minutes) - - +0102030405060708090100Waiting Time (mins)0.00.51.01.52.02.53.03.54.04.55.05.5Eruption Duration (mins) \ No newline at end of file diff --git a/source/img/filter_rows.png b/source/img/filter_rows.png new file mode 100644 index 00000000..5d15ca4f Binary files /dev/null and b/source/img/filter_rows.png differ diff --git a/source/img/filter_rows_and_columns.png b/source/img/filter_rows_and_columns.png new file mode 100644 index 00000000..124a7dc4 Binary files /dev/null and b/source/img/filter_rows_and_columns.png differ diff --git a/source/img/help_dialog.png b/source/img/help_dialog.png new file mode 100644 index 00000000..c2197ab7 Binary files /dev/null and b/source/img/help_dialog.png differ diff --git a/source/img/pivot_functions/pivot_functions.001.jpeg b/source/img/pivot_functions/pivot_functions.001.jpeg index f72151ba..fc5123f3 100644 Binary files a/source/img/pivot_functions/pivot_functions.001.jpeg and b/source/img/pivot_functions/pivot_functions.001.jpeg differ diff --git a/source/img/pivot_functions/pivot_functions.002.jpeg b/source/img/pivot_functions/pivot_functions.002.jpeg index 5e83772e..961c0813 100644 Binary files a/source/img/pivot_functions/pivot_functions.002.jpeg and b/source/img/pivot_functions/pivot_functions.002.jpeg differ diff --git a/source/img/read_csv_function.png b/source/img/read_csv_function.png new file mode 100644 index 00000000..4593eaa9 Binary files /dev/null and b/source/img/read_csv_function.png differ diff --git a/source/img/select_columns.png b/source/img/select_columns.png new file mode 100644 index 00000000..f316180d Binary files /dev/null and b/source/img/select_columns.png differ diff --git a/source/img/sort_values.png b/source/img/sort_values.png new file mode 100644 index 00000000..770ce22d Binary files /dev/null and b/source/img/sort_values.png differ diff --git a/source/img/summarize/summarize.001.jpeg b/source/img/summarize/summarize.001.jpeg index 1ffbaa57..7960e61e 100644 Binary files a/source/img/summarize/summarize.001.jpeg and b/source/img/summarize/summarize.001.jpeg differ diff --git a/source/img/summarize/summarize.002.jpeg b/source/img/summarize/summarize.002.jpeg index 5a6dbbd0..97995520 100644 Binary files a/source/img/summarize/summarize.002.jpeg and b/source/img/summarize/summarize.002.jpeg differ diff --git a/source/img/summarize/summarize.003.jpeg b/source/img/summarize/summarize.003.jpeg index a9d50b07..0a97f6be 100644 Binary files a/source/img/summarize/summarize.003.jpeg and b/source/img/summarize/summarize.003.jpeg differ diff --git a/source/img/summarize/summarize.004.jpeg b/source/img/summarize/summarize.004.jpeg index f3553dba..476ad698 100644 Binary files a/source/img/summarize/summarize.004.jpeg and b/source/img/summarize/summarize.004.jpeg differ diff --git a/source/img/summarize/summarize.005.jpeg b/source/img/summarize/summarize.005.jpeg index b2b1b2ca..d1a4f710 100644 Binary files a/source/img/summarize/summarize.005.jpeg and b/source/img/summarize/summarize.005.jpeg differ diff --git a/source/img/wrangling/pandas_dataframe_series-3.png b/source/img/wrangling/pandas_dataframe_series-3.png index a93bf397..6a2eea54 100644 Binary files a/source/img/wrangling/pandas_dataframe_series-3.png and b/source/img/wrangling/pandas_dataframe_series-3.png differ diff --git a/source/img/wrangling/pandas_dataframe_series.png b/source/img/wrangling/pandas_dataframe_series.png index 285a6559..75ffc893 100644 Binary files a/source/img/wrangling/pandas_dataframe_series.png and b/source/img/wrangling/pandas_dataframe_series.png differ diff --git a/source/img/wrangling/pandas_melt_args_labels.png b/source/img/wrangling/pandas_melt_args_labels.png index a1f9bd98..a24eb439 100644 Binary files a/source/img/wrangling/pandas_melt_args_labels.png and b/source/img/wrangling/pandas_melt_args_labels.png differ diff --git a/source/img/wrangling/pandas_melt_wide-long.png b/source/img/wrangling/pandas_melt_wide-long.png index 994e32a7..03b30975 100644 Binary files a/source/img/wrangling/pandas_melt_wide-long.png and b/source/img/wrangling/pandas_melt_wide-long.png differ diff --git a/source/img/wrangling/pandas_pivot_args_labels.png b/source/img/wrangling/pandas_pivot_args_labels.png index 7d57644c..0f961aaf 100644 Binary files a/source/img/wrangling/pandas_pivot_args_labels.png and b/source/img/wrangling/pandas_pivot_args_labels.png differ diff --git a/source/img/wrangling/pandas_pivot_long-wide.png b/source/img/wrangling/pandas_pivot_long-wide.png index 994e0510..faff307b 100644 Binary files a/source/img/wrangling/pandas_pivot_long-wide.png and b/source/img/wrangling/pandas_pivot_long-wide.png differ diff --git a/source/index.md b/source/index.md index 304a3606..be402176 100644 --- a/source/index.md +++ b/source/index.md @@ -13,19 +13,21 @@ kernelspec: name: python3 --- -# Welcome -- TBD +# Welcome! -This is the [website](https://datasciencebook.ca/) for *Data Science: A First Introduction*. +This is the [website](https://ubc-dsci.github.io/introduction-to-datascience-python/) for *Data Science: A First Introduction (Python Edition)*. You can read the web version of the book on this site. Click a section in the table of contents on the left side of the page to navigate to it. If you are on a mobile device, -you may need to open the table of contents first by clicking the menu button on +you may need to open the table of contents first by clicking the menu button on the top left of the page. -You can purchase a PDF or print copy of the book -on the [CRC Press website](https://www.routledge.com/Data-Science-A-First-Introduction/Timbers-Campbell-Lee/p/book/9780367524685) or on [Amazon](https://www.amazon.com/Data-Science-First-Introduction-Chapman/dp/0367532174/ref=sr_[…]qid=1644637450&sprefix=data+science+timber%2Caps%2C166&sr=8-1). + +For the R version of the textbook, please visit https://datasciencebook.ca. +You can purchase a PDF or print copy of the R version of the book +on the [CRC Press website](https://www.routledge.com/Data-Science-A-First-Introduction/Timbers-Campbell-Lee/p/book/9780367524685) or +on [Amazon](https://www.amazon.com/Data-Science-First-Introduction-Chapman/dp/0367532174/ref=sr_[…]qid=1644637450&sprefix=data+science+timber%2Caps%2C166&sr=8-1). -This work by [Tiffany Timbers](https://www.tiffanytimbers.com/), [Trevor Campbell](https://trevorcampbell.me/), -and [Melissa Lee](https://www.stat.ubc.ca/users/melissa-lee) is licensed under +This work by [Tiffany Timbers](https://www.tiffanytimbers.com/), [Trevor Campbell](https://trevorcampbell.me/), +and [Melissa Lee](https://www.stat.ubc.ca/users/melissa-lee) is licensed under a [Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License](http://creativecommons.org/licenses/by-nc-sa/4.0/). - diff --git a/source/intro.md b/source/intro.md index 9683b4ef..bad9f768 100644 --- a/source/intro.md +++ b/source/intro.md @@ -24,9 +24,9 @@ from myst_nb import glue This chapter provides an introduction to data science and the Python programming language. The goal here is to get your hands dirty right from the start! We will walk through an entire data analysis, -and along the way introduce different types of data analysis question, some fundamental programming +and along the way introduce different types of data analysis question, some fundamental programming concepts in Python, and the basics of loading, cleaning, and visualizing data. In the following chapters, we will -dig into each of these steps in much more detail; but for now, let's jump in to see how much we can do +dig into each of these steps in much more detail; but for now, let's jump in to see how much we can do with data science! ## Chapter learning objectives @@ -38,7 +38,8 @@ By the end of the chapter, readers will be able to do the following: - Read tabular data with `read_csv`. - Use `help()` to access help and documentation tools in Python. - Create new variables and objects in Python. -- Create and organize subsets of tabular data using `[]`, `loc[]`, and `sort_values` +- Create and organize subsets of tabular data using `[]`, `loc[]`, and `sort_values`. +- Chain multiple operations in sequence. - Visualize data with an `altair` bar plot. ## Canadian languages data set @@ -47,7 +48,7 @@ By the end of the chapter, readers will be able to do the following: ``` In this chapter, we will walk through a full analysis of a data set relating to -languages spoken at home by Canadian residents. Many Indigenous peoples exist in Canada +languages spoken at home by Canadian residents. Many Indigenous peoples exist in Canada with their own cultures and languages; these languages are often unique to Canada and not spoken anywhere else in the world {cite:p}`statcan2018mothertongue`. Sadly, colonization has led to the loss of many of these languages. For instance, generations of @@ -55,18 +56,18 @@ children were not allowed to speak their mother tongue (the first language an individual learns in childhood) in Canadian residential schools. Colonizers also renamed places they had "discovered" {cite:p}`wilson2018`. Acts such as these have significantly harmed the continuity of Indigenous languages in Canada, and -some languages are considered "endangered" as few people report speaking them. -To learn more, please see *Canadian Geographic*'s article, "Mapping Indigenous Languages in -Canada" {cite:p}`walker2017`, -*They Came for the Children: Canada, Aboriginal -peoples, and Residential Schools* {cite:p}`children2012` -and the *Truth and Reconciliation Commission of Canada's* +some languages are considered "endangered" as few people report speaking them. +To learn more, please see *Canadian Geographic*'s article, "Mapping Indigenous Languages in +Canada" {cite:p}`walker2017`, +*They Came for the Children: Canada, Aboriginal +peoples, and Residential Schools* {cite:p}`children2012` +and the *Truth and Reconciliation Commission of Canada's* *Calls to Action* {cite:p}`calls2015`. -The data set we will study in this chapter is taken from -[the `canlang` R data package](https://ttimbers.github.io/canlang/) +The data set we will study in this chapter is taken from +[the `canlang` R data package](https://ttimbers.github.io/canlang/) {cite:p}`timbers2020canlang`, which has -population language data collected during the 2016 Canadian census {cite:p}`cancensus2016`. +population language data collected during the 2016 Canadian census {cite:p}`cancensus2016`. In this data, there are 214 languages recorded, each having six different properties: 1. `category`: Higher-level language category, describing whether the language is an Official Canadian language, an Aboriginal (i.e., Indigenous) language, or a Non-Official and Non-Aboriginal language. @@ -78,15 +79,15 @@ In this data, there are 214 languages recorded, each having six different proper According to the census, more than 60 Aboriginal languages were reported as being spoken in Canada. Suppose we want to know which are the most common; -then we might ask the following question, which we wish to answer using our data: +then we might ask the following question, which we wish to answer using our data: *Which ten Aboriginal languages were most often reported in 2016 as mother -tongues in Canada, and how many people speak each of them?* +tongues in Canada, and how many people speak each of them?* ```{index} data science; good practices ``` -> **Note:** Data science cannot be done without +> **Note:** Data science cannot be done without > a deep understanding of the data and > problem domain. In this book, we have simplified the data sets used in our > examples to concentrate on methods and fundamental concepts. But in real @@ -96,15 +97,15 @@ tongues in Canada, and how many people speak each of them?* > about *how* the data were collected, which affects the conclusions you can > draw. If your data are biased, then your results will be biased! -## Asking a question +## Asking a question Every good data analysis begins with a *question*—like the above—that you aim to answer using data. As it turns out, there are actually a number of different *types* of question regarding data: descriptive, exploratory, inferential, predictive, causal, and mechanistic, all of which are defined in {numref}`questions-table`. {cite:p}`leek2015question,peng2015art` -Carefully formulating a question as early as possible in your analysis—and -correctly identifying which type of question it is—will guide your overall approach to +Carefully formulating a question as early as possible in your analysis—and +correctly identifying which type of question it is—will guide your overall approach to the analysis as well as the selection of appropriate tools. ```{index} question; data analysis, descriptive question; definition, exploratory question; definition @@ -138,12 +139,12 @@ the analysis as well as the selection of appropriate tools. * - Mechanistic - A question that asks about the underlying mechanism of the observed patterns, trends, or relationships (i.e., how does it happen?) - How does wealth lead to voting for a certain political party in Canadian elections? - + ``` -In this book, you will learn techniques to answer the -first four types of question: descriptive, exploratory, predictive, and inferential; +In this book, you will learn techniques to answer the +first four types of question: descriptive, exploratory, predictive, and inferential; causal and mechanistic questions are beyond the scope of this book. In particular, you will learn how to apply the following analysis tools: @@ -153,25 +154,25 @@ In particular, you will learn how to apply the following analysis tools: ```{index} clustering; overview, estimation; overview ``` -1. **Summarization:** computing and reporting aggregated values pertaining to a data set. +1. **Summarization:** computing and reporting aggregated values pertaining to a data set. Summarization is most often used to answer descriptive questions, and can occasionally help with answering exploratory questions. -For example, you might use summarization to answer the following question: +For example, you might use summarization to answer the following question: *What is the average race time for runners in this data set?* Tools for summarization are covered in detail in the {ref}`reading` and {ref}`wrangling` chapters, but appear regularly throughout the text. -1. **Visualization:** plotting data graphically. +1. **Visualization:** plotting data graphically. Visualization is typically used to answer descriptive and exploratory questions, but plays a critical supporting role in answering all of the types of question in {numref}`questions-table`. For example, you might use visualization to answer the following question: -*Is there any relationship between race time and age for runners in this data set?* +*Is there any relationship between race time and age for runners in this data set?* This is covered in detail in the {ref}`viz` chapter, but again appears regularly throughout the book. 3. **Classification:** predicting a class or category for a new observation. Classification is used to answer predictive questions. For example, you might use classification to answer the following question: *Given measurements of a tumor's average cell area and perimeter, is the tumor benign or malignant?* Classification is covered in the {ref}`classification` and {ref}`classification2` chapters. -4. **Regression:** predicting a quantitative value for a new observation. +4. **Regression:** predicting a quantitative value for a new observation. Regression is also used to answer predictive questions. For example, you might use regression to answer the following question: *What will be the race time for a 20-year-old runner who weighs 50kg?* @@ -181,22 +182,22 @@ data set. Clustering is often used to answer exploratory questions. For example, you might use clustering to answer the following question: *What products are commonly bought together on Amazon?* Clustering is covered in the {ref}`clustering` chapter. -6. **Estimation:** taking measurements for a small number of items from a large group - and making a good guess for the average or proportion for the large group. Estimation +6. **Estimation:** taking measurements for a small number of items from a large group + and making a good guess for the average or proportion for the large group. Estimation is used to answer inferential questions. For example, you might use estimation to answer the following question: *Given a survey of cellphone ownership of 100 Canadians, what proportion -of the entire Canadian population own Android phones?* +of the entire Canadian population own Android phones?* Estimation is covered in the {ref}`inference` chapter. -Referring to {numref}`questions-table`, our question about +Referring to {numref}`questions-table`, our question about Aboriginal languages is an example of a *descriptive question*: we are summarizing the characteristics of a data set without further interpretation. And referring to the list above, it looks like we should use visualization and perhaps some summarization to answer the question. So in the remainder -of this chapter, we will work towards making a visualization that shows +of this chapter, we will work towards making a visualization that shows us the ten most common Aboriginal languages in Canada and their associated counts, -according to the 2016 census. +according to the 2016 census. ## Loading a tabular data set @@ -204,7 +205,7 @@ according to the 2016 census. ``` A data set is, at its core essence, a structured collection of numbers and characters. -Aside from that, there are really no strict rules; data sets can come in +Aside from that, there are really no strict rules; data sets can come in many different forms! Perhaps the most common form of data set that you will find in the wild, however, is *tabular data*. Think spreadsheets in Microsoft Excel: tabular data are rectangular-shaped and spreadsheet-like, as shown in {numref}`img-spreadsheet-vs-data frame`. In this book, we will focus primarily on tabular data. @@ -216,14 +217,14 @@ Since we are using Python for data analysis in this book, the first step for us load the data into Python. When we load tabular data into Python, it is represented as a *data frame* object. {numref}`img-spreadsheet-vs-data frame` shows that a Python data frame is very similar to a spreadsheet. We refer to the rows as **observations**; these are the things that we -collect the data on, e.g., voters, cities, etc. We refer to the columns as +collect the data on, e.g., voters, cities, etc. We refer to the columns as **variables**; these are the characteristics of those observations, e.g., voters' political -affiliations, cities' populations, etc. +affiliations, cities' populations, etc. ```{figure} img/spreadsheet_vs_df.png --- -height: 400px +height: 500px name: img-spreadsheet-vs-data frame --- A spreadsheet versus a data frame in Python @@ -239,7 +240,7 @@ The first kind of data file that we will learn how to load into Python as a data frame is the *comma-separated values* format (`.csv` for short). These files have names ending in `.csv`, and can be opened and saved using common spreadsheet programs like Microsoft Excel and Google Sheets. For example, the -`.csv` file named `can_lang.csv` +`.csv` file named `can_lang.csv` is included with [the code for this book](https://github.com/UBC-DSCI/introduction-to-datascience-python/tree/main/source/data). If we were to open this data in a plain text editor (a program like Notepad that just shows text with no formatting), we would see each row on its own line, and each entry in the table separated by a comma: @@ -264,7 +265,7 @@ To load this data into Python so that we can do things with it (e.g., perform analyses or create data visualizations), we will need to use a *function.* A function is a special word in Python that takes instructions (we call these *arguments*) and does something. The function we will use to load a `.csv` file -into Python is called `read_csv`. In its most basic +into Python is called `read_csv`. In its most basic use-case, `read_csv` expects that the data file: - has column names (or *headers*), @@ -280,14 +281,14 @@ Below you'll see the code used to load the data into Python using the `read_csv` function. Note that the `read_csv` function is not included in the base installation of Python, meaning that it is not one of the primary functions ready to use when you install Python. Therefore, you need to load it from somewhere else -before you can use it. The place from which we will load it is called a Python *package*. +before you can use it. The place from which we will load it is called a Python *package*. A Python package is a collection of functions that can be used in addition to the built-in Python package functions once loaded. The `read_csv` function, in -particular, can be made accessible by loading +particular, can be made accessible by loading [the `pandas` Python package](https://pypi.org/project/pandas/) {cite:p}`reback2020pandas,mckinney-proc-scipy-2010` using the `import` command. The `pandas` package contains many -functions that we will use throughout this book to load, clean, wrangle, -and visualize data. +functions that we will use throughout this book to load, clean, wrangle, +and visualize data. +++ @@ -296,25 +297,23 @@ import pandas as pd ``` This command has two parts. The first is `import pandas`, which loads the `pandas` package. -The second is `as pd`, which give the `pandas` package the much shorter *alias* (another name) `pd`. +The second is `as pd`, which give the `pandas` package the much shorter *alias* (another name) `pd`. We can now use the `read_csv` function by writing `pd.read_csv`, i.e., the package name, then a dot, then the function name. You can see why we gave `pandas` a shorter alias; if we had to type `pandas.` before every function we wanted to use, our code would become much longer and harder to read! -Now that the `pandas` package is loaded, we can use the `read_csv` function by passing +Now that the `pandas` package is loaded, we can use the `read_csv` function by passing it a single argument: the name of the file, `"can_lang.csv"`. We have to put quotes around file names and other letters and words that we use in our code to distinguish it from the special words (like functions!) that make up the Python programming language. The file's name is the only argument we need to provide because our file satisfies everything else that the `read_csv` function expects in the default use-case. {numref}`img-read-csv` describes how we use the `read_csv` -to read data into Python. - -**(FIGURE 1.2 FROM R BOOK IS NOT MISSING, BUT STILL R VERSION. NEEDS PD.READ_CSV)** +to read data into Python. -```{figure} img/read_csv_function.jpeg +```{figure} img/read_csv_function.png --- -height: 200px +height: 220px name: img-read-csv --- Syntax for the `read_csv` function @@ -323,6 +322,7 @@ Syntax for the `read_csv` function +++ ```{code-cell} ipython3 +:tags: ["output_scroll"] pd.read_csv("data/can_lang.csv") ``` @@ -332,11 +332,11 @@ pd.read_csv("data/can_lang.csv") ## Naming things in Python When we loaded the 2016 Canadian census language data -using `read_csv`, we did not give this data frame a name. -Therefore the data was just printed on the screen, -and we cannot do anything else with it. That isn't very useful. -What would be more useful would be to give a name -to the data frame that `read_csv` outputs, +using `read_csv`, we did not give this data frame a name. +Therefore the data was just printed on the screen, +and we cannot do anything else with it. That isn't very useful. +What would be more useful would be to give a name +to the data frame that `read_csv` outputs, so that we can refer to it later for analysis and visualization. ```{index} see: =; assignment symbol @@ -345,7 +345,7 @@ so that we can refer to it later for analysis and visualization. ```{index} assignment symbol, string ``` -The way to assign a name to a value in Python is via the *assignment symbol* `=`. +The way to assign a name to a value in Python is via the *assignment symbol* `=`. On the left side of the assignment symbol you put the name that you want to use, and on the right side of the assignment symbol you put the value that you want the name to refer to. @@ -360,17 +360,17 @@ my_number = 1 + 2 name = "Alice" ``` -Note that when -we name something in Python using the assignment symbol, `=`, -we do not need to surround the name we are creating with quotes. This is +Note that when +we name something in Python using the assignment symbol, `=`, +we do not need to surround the name we are creating with quotes. This is because we are formally telling Python that this special word denotes the value of whatever is on the right-hand side. Only characters and words that act as *values* on the right-hand side of the assignment -symbol—e.g., the file name `"data/can_lang.csv"` that we specified before, or `"Alice"` above—need +symbol—e.g., the file name `"data/can_lang.csv"` that we specified before, or `"Alice"` above—need to be surrounded by quotes. After making the assignment, we can use the special name words we have created in -place of their values. For example, if we want to do something with the value `3` later on, +place of their values. For example, if we want to do something with the value `3` later on, we can just use `my_number` instead. Let's try adding 2 to `my_number`; you will see that Python just interprets this as adding 2 and 3: @@ -397,7 +397,7 @@ SyntaxError: cannot assign to operator ```{index} object; naming convention ``` -There are certain conventions for naming objects in Python. +There are certain conventions for naming objects in Python. When naming an object we suggest using only lowercase letters, numbers and underscores `_` to separate the words in a name. Python is case sensitive, which means that `Letter` and @@ -408,23 +408,24 @@ remember what each name in your code represents. We recommend following the **PEP 8** naming conventions outlined in the *[PEP 8](https://peps.python.org/pep-0008/)* {cite:p}`pep8-style-guide`. Let's now use the assignment symbol to give the name `can_lang` to the 2016 Canadian census language data frame that we get from -`read_csv`. +`read_csv`. ```{code-cell} ipython3 can_lang = pd.read_csv("data/can_lang.csv") ``` Wait a minute, nothing happened this time! Where's our data? -Actually, something did happen: the data was loaded in -and now has the name `can_lang` associated with it. -And we can use that name to access the data frame and do things with it. -For example, we can type the name of the data frame to print both the first few rows +Actually, something did happen: the data was loaded in +and now has the name `can_lang` associated with it. +And we can use that name to access the data frame and do things with it. +For example, we can type the name of the data frame to print both the first few rows and the last few rows. The three dots (`...`) indicate that there are additional rows that are not printed. -You will also see that the number of observations (i.e., rows) and -variables (i.e., columns) are printed just underneath the data frame (214 rows and 6 columns in this case). +You will also see that the number of observations (i.e., rows) and +variables (i.e., columns) are printed just underneath the data frame (214 rows and 6 columns in this case). Printing a few rows from data frame like this is a handy way to get a quick sense for what is contained in it. ```{code-cell} ipython3 +:tags: ["output_scroll"] can_lang ``` @@ -435,8 +436,8 @@ can_lang Now that we've loaded our data into Python, we can start wrangling the data to find the ten Aboriginal languages that were most often reported -in 2016 as mother tongues in Canada. In particular, we want to construct -a table with the ten Aboriginal languages that have the largest +in 2016 as mother tongues in Canada. In particular, we want to construct +a table with the ten Aboriginal languages that have the largest counts in the `mother_tongue` column. The first step is to extract from our `can_lang` data only those rows that correspond to Aboriginal languages, and then the second step is to keep only the `language` and `mother_tongue` columns. @@ -457,8 +458,8 @@ and then use `loc[]` to do both in our analysis of the Aboriginal languages data Looking at the `can_lang` data above, we see the column `category` contains different high-level categories of languages, which include "Aboriginal languages", "Non-Official & Non-Aboriginal languages" and "Official languages". To answer -our question we want to filter our data set so we restrict our attention -to only those languages in the "Aboriginal languages" category. +our question we want to filter our data set so we restrict our attention +to only those languages in the "Aboriginal languages" category. ```{index} pandas.DataFrame; [], filter, logical statement, logical statement; equivalency operator, string ``` @@ -476,20 +477,18 @@ column---denoted by `can_lang["category"]`---with the value `"Aboriginal languag You will learn about many other kinds of logical statement in the {ref}`wrangling` chapter. Similar to when we loaded the data file and put quotes around the file name, here we need to put quotes around both `"Aboriginal languages"` and `"category"`. Using -quotes tells Python that this is a *string value* (e.g., a column name, or word data) -and not one of the special words that makes up the Python programming language, +quotes tells Python that this is a *string value* (e.g., a column name, or word data) +and not one of the special words that makes up the Python programming language, or one of the names we have given to objects in the code we have already written. > **Note:** In Python, single quotes (`'`) and double quotes (`"`) are generally -> treated the same. So we could have written `'Aboriginal languages'` instead +> treated the same. So we could have written `'Aboriginal languages'` instead > of `"Aboriginal languages"` above, or `'category'` instead of `"category"`. > Try both out for yourself! -**(This figure is wrong-- should be for [] operation below)** - -```{figure} img/read_csv_function.jpeg +```{figure} img/filter_rows.png --- -height: 200px +height: 220px name: img-filter --- Syntax for using the `[]` operation to filter rows. @@ -499,6 +498,7 @@ This operation returns a data frame that has all the columns of the input data f but only those rows corresponding to Aboriginal languages that we asked for in the logical statement. ```{code-cell} ipython3 +:tags: ["output_scroll"] can_lang[can_lang["category"] == "Aboriginal languages"] ``` @@ -513,16 +513,14 @@ We can also use the `[]` operation to select columns from a data frame. We again first type the name of the data frame---here, `can_lang`---followed by square brackets. Inside the square brackets, we provide a *list* of column names. In Python, we denote a *list* using square brackets, where -each item is separated by a comma (`,`). So if we are interested in +each item is separated by a comma (`,`). So if we are interested in selecting only the `language` and `mother_tongue` columns from our original `can_lang` data frame, we put the list `["language", "mother_tongue"]` containing those two column names inside the square brackets of the `[]` operation. -**(This figure is wrong-- should be for [] operation below)** - -```{figure} img/read_csv_function.jpeg +```{figure} img/select_columns.png --- -height: 200px +height: 220px name: img-select --- Syntax for using the `[]` operation to select columns. @@ -549,30 +547,30 @@ The syntax is very similar to the `[]` operation we have already covered: we wil essentially combine both our row filtering and column selection steps from before. In particular, we first write the name of the data frame---`can_lang` again---then follow that with the `.loc[]` method. Inside the square brackets, -we write our row filtering logical statement, +we write our row filtering logical statement, then a comma, then our list of columns to select. -**(This figure is wrong-- should be for .loc[] operation below)** - -```{figure} img/read_csv_function.jpeg +```{figure} img/filter_rows_and_columns.png --- -height: 200px +height: 220px name: img-loc --- Syntax for using the `loc[]` operation to filter rows and select columns. ``` ```{code-cell} ipython3 -aboriginal_lang = can_lang.loc[can_lang["category"] == "Aboriginal languages", ["language", "mother_tongue"]] +aboriginal_lang = can_lang.loc[ + can_lang["category"] == "Aboriginal languages", ["language", "mother_tongue"] +] ``` -There is one very important thing to notice in this code example. +There is one very important thing to notice in this code example. The first is that we used the `loc[]` operation on the `can_lang` data frame by writing `can_lang.loc[]`---first the data frame name, then a dot, then `loc[]`. There's that dot again! If you recall, earlier in this chapter we used the `read_csv` function from `pandas` (aliased as `pd`), and wrote `pd.read_csv`. The dot means that the thing on the left (`pd`, i.e., the `pandas` package) *provides* the thing on the right (the `read_csv` function). In the case of `can_lang.loc[]`, the thing on the left (the `can_lang` data frame) -*provides* the thing on the right (the `loc[]` operation). In Python, -both packages (like `pandas`) *and* objects (like our `can_lang` data frame) can provide functions +*provides* the thing on the right (the `loc[]` operation). In Python, +both packages (like `pandas`) *and* objects (like our `can_lang` data frame) can provide functions and other objects that we access using the dot syntax. At this point, if we have done everything correctly, `aboriginal_lang` should be a data frame @@ -585,7 +583,7 @@ aboriginal_lang ``` We can see the original `can_lang` data set contained 214 rows with multiple kinds of `category`. The data frame -`aboriginal_lang` contains only 67 rows, and looks like it only contains Aboriginal languages. +`aboriginal_lang` contains only 67 rows, and looks like it only contains Aboriginal languages. So it looks like the `loc[]` operation gave us the result we wanted! ### Using `sort_values` to order and `head` to select rows by value @@ -598,7 +596,7 @@ with only the Aboriginal languages in the data set and their associated counts. However, we want to know the **ten** languages that are spoken most often. As a next step, we will order the `mother_tongue` column from largest to smallest value and then extract only the top ten rows. This is where the `sort_values` -and `head` functions come to the rescue! +and `head` functions come to the rescue! The `sort_values` function allows us to order the rows of a data frame by the values of a particular column. We need to specify the column name @@ -609,7 +607,13 @@ language, we will use the `sort_values` function to order the rows in our arrange the rows in descending order (from largest to smallest), so we specify the argument `ascending` as `False`. -**(FIGURE 1.5 FROM R BOOK MISSING HERE)** +```{figure} img/sort_values.png +--- +height: 220px +name: img-sort-values +--- +Syntax for using `sort_values` to arrange rows in decending order. +``` ```{code-cell} ipython3 arranged_lang = aboriginal_lang.sort_values(by='mother_tongue', ascending=False) @@ -619,7 +623,7 @@ arranged_lang Next, we will obtain the ten most common Aboriginal languages by selecting only the first ten rows of the `arranged_lang` data frame. We do this using the `head` function, and specifying the argument -`10`. +`10`. ```{code-cell} ipython3 @@ -627,16 +631,134 @@ ten_lang = arranged_lang.head(10) ten_lang ``` -We have now answered our initial question by generating this table! +## Combining analysis steps with chaining and multiline expressions + +```{index} chaining methods +``` + +It took us 3 steps to find the ten Aboriginal languages most often reported in +2016 as mother tongues in Canada. Starting from the `can_lang` data frame, we: + +1) used `loc` to filter the rows so that only the + `Aboriginal languages` category remained, and selected the + `language` and `mother_tongue` columns, +2) used `sort_values` to sort the rows by `mother_tongue` in descending order, and +3) obtained only the top 10 values using `head`. + +One way of performing these steps is to just write +multiple lines of code, storing temporary, intermediate objects as you go. +```{code-cell} ipython3 +aboriginal_lang = can_lang.loc[can_lang["category"] == "Aboriginal languages", ["language", "mother_tongue"]] +arranged_lang_sorted = aboriginal_lang.sort_values(by='mother_tongue', ascending=False) +ten_lang = arranged_lang_sorted.head(10) +``` + +```{index} multi-line expression +``` + +You might find that code hard to read. You're not wrong; it is! +There are two main issues with readability here. First, each line of code is quite long. +It is hard to keep track of what methods are being called, and what arguments were used. +Second, each line introduces a new temporary object. In this case, both `aboriginal_lang` and `arranged_lang_sorted` +are just temporary results on the way to producing the `ten_lang` data frame. +This makes the code hard to read, as one has to trace where each temporary object +goes, and hard to understand, since introducing many named objects also suggests that they +are of some importance, when really they are just intermediates. +The need to call multiple methods in a sequence to process a data frame is +quite common, so this is an important issue to address! + +To solve the first problem, we can actually split the long expressions above across +multiple lines. Although in most cases, a single expression in Python must be contained +in a single line of code, there are a small number of situations where lets us do this. +Let's rewrite this code in a more readable format using multiline expressions. + +```{code-cell} ipython3 +aboriginal_lang = can_lang.loc[ + can_lang["category"] == "Aboriginal languages", ["language", "mother_tongue"] +] +arranged_lang_sorted = aboriginal_lang.sort_values( + by='mother_tongue', ascending=False +) +ten_lang = arranged_lang_sorted.head(10) +``` + +This code is the same as the code we showed earlier; you can see the same +sequence of methods and arguments is used. But long expressions are split +across multiple lines when they would otherwise get long and unwieldy, +improving the readability of the code. +How does Python know when to keep +reading on the next line for a single expression? +For the line starting with `aboriginal_lang = ...`, Python sees that the line ends with a left +bracket symbol `[`, and knows that our +expression cannot end until we close it with an appropriate corresponding right bracket symbol `]`. +We put the same two arguments as we did before, and then +the corresponding right bracket appears after `["language", "mother_tongue"]`). +For the line starting with `arranged_lang_sorted = ...`, Python sees that the line ends with a left parenthesis symbol `(`, +and knows the expression cannot end until we close it with the corresponding right parenthesis symbol `)`. +Again we use the same two arguments as before, and then the +corresponding right parenthesis appears right after `ascending=False`. +In both cases, Python keeps reading the next line to figure out +what the rest of the expression is. We could, of course, +put all of the code on one line of code, but splitting it across +multiple lines helps a lot with code readability. + +We still have to handle the issue that each line of code---i.e., each step in the analysis---introduces +a new temporary object. To address this issue, we can *chain* multiple operations together without +assigning intermediate objects. The key idea of chaining is that the *output* of +each step in the analysis is a data frame, which means that you can just directly keep calling methods +that operate on the output of each step in a sequence! This simplifies the code and makes it +easier to read. The code below demonstrates the use of both multiline expressions and chaining together. +The code is now much cleaner, and the `ten_lang` data frame that we get is equivalent to the one +from the messy code above! + +```{code-cell} ipython3 +# obtain the 10 most common Aboriginal languages +ten_lang = ( + can_lang.loc[ + can_lang["category"] == "Aboriginal languages", + ["language", "mother_tongue"] + ] + .sort_values(by="mother_tongue", ascending=False) + .head(10) +) +ten_lang +``` + +Let's parse this new block of code piece by piece. +The code above starts with a left parenthesis, `(`, and so Python +knows to keep reading to subsequent lines until it finds the corresponding +right parenthesis symbol `)`. The `loc` method performs the filtering and selecting steps as before. The line after this +starts with a period (`.`) that "chains" the output of the `loc` step with the next operation, +`sort_values`. Since the output of `loc` is a data frame, we can use the `sort_values` method on it +without first giving it a name! That is what the `.sort_values` does on the next line. +Finally, we once again "chain" together the output of `sort_values` with `head` to ask for the 10 +most common languages. Finally, the right parenthesis `)` corresponding to the very first left parenthesis +appears on the second last line, completing the multiline expression. +Instead of creating intermediate objects, with chaining, we take the output of +one operation and use that to perform the next operation. In doing so, we remove the need to create and +store intermediates. This can help with readability by simplifying the code. + +Now that we've shown you chaining as an alternative to storing +temporary objects and composing code, does this mean you should *never* store +temporary objects or compose code? Not necessarily! +There are times when temporary objects are handy to keep around. +For example, you might store a temporary object before feeding it into a plot function +so you can iteratively change the plot without having to +redo all of your data transformations. +Chaining many functions can be overwhelming and difficult to debug; +you may want to store a temporary object midway through to inspect your result +before moving on with further steps. + +We have now answered our initial question by generating the `ten_lang` table! Are we done? Well, not quite; tables are almost never the best way to present the result of your analysis to your audience. Even the simple table above with only two columns presents some difficulty: for example, you have to scrutinize -the table quite closely to get a sense for the relative numbers of speakers of -each language. When you move on to more complicated analyses, this issue only -gets worse. In contrast, a *visualization* would convey this information in a much -more easily understood format. +the table quite closely to get a sense for the relative numbers of speakers of +each language. When you move on to more complicated analyses, this issue only +gets worse. In contrast, a *visualization* would convey this information in a much +more easily understood format. Visualizations are a great tool for summarizing information to help you -effectively communicate with your audience. +effectively communicate with your audience. ## Exploring data with visualizations @@ -644,7 +766,7 @@ effectively communicate with your audience. ``` Creating effective data visualizations is an essential component of any data -analysis. In this section we will develop a visualization of the +analysis. In this section we will develop a visualization of the ten Aboriginal languages that were most often reported in 2016 as mother tongues in Canada, as well as the number of people that speak each of them. @@ -670,9 +792,9 @@ formally introduce tidy data in the {ref}`wrangling` chapter. We will make a bar plot to visualize our data. A bar plot is a chart where the lengths of the bars represent certain values, like counts or proportions. We will make a bar plot using the `mother_tongue` and `language` columns from our -`ten_lang` data frame. To create a bar plot of these two variables using the +`ten_lang` data frame. To create a bar plot of these two variables using the `altair` package, we must specify the data frame, which variables -to put on the x and y axes, and what kind of plot to create. +to put on the x and y axes, and what kind of plot to create. First, we need to import the `altair` package. ```{code-cell} ipython3 @@ -683,16 +805,22 @@ 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 +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 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 +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()` 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. -**(FIGURE 1.6 FROM R BOOK IS MISSING)** +```{figure} img/altair_syntax.png +--- +height: 220px +name: img-altair +--- +Syntax for using `altair` to make a bar chart. +``` +++ @@ -700,12 +828,9 @@ and that the `mother_tongue` column should correspond to the y-axis. :tags: [] barplot_mother_tongue = ( - alt.Chart(ten_lang) - .mark_bar().encode( - x="language", - y="mother_tongue" - )) - + alt.Chart(ten_lang).mark_bar().encode(x="language", y="mother_tongue") +) + ``` @@ -728,20 +853,6 @@ Bar plot of the ten Aboriginal languages most often reported by Canadian residen ```{index} see: .; chaining methods ``` -```{index} multi-line expression -``` - -> **Note:** The vast majority of the -> time, a single expression in Python must be contained in a single line of code. -> However, there *are* a small number of situations in which you can have a -> single Python expression span multiple lines. Above is one such case: here, Python sees that we put a left -> parenthesis symbol `(` on the first line right after the assignment symbol `=`, and knows that our -> expression cannot end until we close it with an appropriate corresponding right parenthesis symbol `)`. -> So Python keeps reading the next line to figure out -> what the rest of the expression is. We could, of course, -> put all of the code on one line of code, but splitting it across -> multiple lines helps a lot with code readability. - ### Formatting `altair` objects It is exciting that we can already visualize our data to help answer our @@ -760,8 +871,8 @@ 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 +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. Again, since we are specifying words (e.g. `"Mother Tongue (Number of Canadian Residents)"`) as arguments to @@ -795,7 +906,7 @@ Bar plot of the ten Aboriginal languages most often reported by Canadian residen ::: -The result is shown in {numref}`barplot-mother-tongue-labs`. +The result is shown in {numref}`barplot-mother-tongue-labs`. This is already quite an improvement! Let's tackle the next major issue with the visualization in {numref}`barplot-mother-tongue-labs`: the vertical x axis labels, which are currently making it difficult to read the different language names. @@ -830,14 +941,14 @@ Horizontal bar plot of the ten Aboriginal languages most often reported by Canad ```{index} altair; sort ``` -Another big step forward, as shown in {numref}`barplot-mother-tongue-labs-axis`! There +Another big step forward, as shown in {numref}`barplot-mother-tongue-labs-axis`! There are no more serious issues with the visualization. Now comes time to refine the visualization to make it even more well-suited to answering the question we asked earlier in this chapter. For example, the visualization could be made more transparent by organizing the bars according to the number of Canadian residents reporting each language, rather than in alphabetical order. We can reorder the bars using the `sort` argument, which orders a variable (here `language`) based on the -values of the variable(`mother_tongue`) on the `x-axis`. +values of the variable(`mother_tongue`) on the `x-axis`. ```{code-cell} ipython3 ordered_barplot_mother_tongue = ( @@ -864,7 +975,7 @@ glue('barplot-mother-tongue-reorder', ordered_barplot_mother_tongue, display=Tru :name: barplot-mother-tongue-reorder Bar plot of the ten Aboriginal languages most often reported by Canadian residents as their mother tongue with bars reordered. -::: +::: {numref}`barplot-mother-tongue-reorder` provides a very clear and well-organized @@ -878,7 +989,7 @@ n.o.s. with over 60,000 Canadian residents reporting it as their mother tongue. > Cree languages include the following categories: Cree n.o.s., Swampy Cree, > Plains Cree, Woods Cree, and a 'Cree not included elsewhere' category (which > includes Moose Cree, Northern East Cree and Southern East Cree) -> {cite:p}`language2016`. +> {cite:p}`language2016`. ### Putting it all together @@ -890,12 +1001,12 @@ n.o.s. with over 60,000 Canadian residents reporting it as their mother tongue. In the block of code below, we put everything from this chapter together, with a few modifications. In particular, we have combined all of our steps into one expression -split across multiple lines using the left and right parenthesis symbols `(` and `)`. -We have also provided *comments* next to +split across multiple lines using the left and right parenthesis symbols `(` and `)`. +We have also provided *comments* next to many of the lines of code below using the -hash symbol `#`. When Python sees a `#` sign, it +hash symbol `#`. When Python sees a `#` sign, it will ignore all of the text that -comes after the symbol on that line. So you can use comments to explain lines +comes after the symbol on that line. So you can use comments to explain lines of code for others, and perhaps more importantly, your future self! It's good practice to get in the habit of commenting your code to improve its readability. @@ -905,7 +1016,7 @@ performed an entire data science workflow with a highly effective data visualization! We asked a question, loaded the data into Python, wrangled the data (using `[]`, `loc[]`, `sort_values`, and `head`) and created a data visualization to help answer our question. In this chapter, you got a quick taste of the data -science workflow; continue on with the next few chapters to learn each of +science workflow; continue on with the next few chapters to learn each of these steps in much more detail! ```{code-cell} ipython3 @@ -956,16 +1067,16 @@ Bar plot of the ten Aboriginal languages most often reported by Canadian residen ```{index} see: __doc__; documentation ``` -There are many Python functions in the `pandas` package (and beyond!), and +There are many Python functions in the `pandas` package (and beyond!), and nobody can be expected to remember what every one of them does -or all of the arguments we have to give them. Fortunately, Python provides -the `help` function, which -provides an easy way to pull up the documentation for -most functions quickly. To use the `help` function to access the documentation, you +or all of the arguments we have to give them. Fortunately, Python provides +the `help` function, which +provides an easy way to pull up the documentation for +most functions quickly. To use the `help` function to access the documentation, you just put the name of the function you are curious about as an argument inside the `help` function. For example, if you had forgotten what the `pd.read_csv` function did or exactly what arguments to pass in, you could run the following -code: +code: ```{code-cell} ipython3 :tags: ["remove-output"] @@ -973,11 +1084,11 @@ help(pd.read_csv) ``` {numref}`help_read_csv` shows the documentation that will pop up, -including a high-level description of the function, its arguments, +including a high-level description of the function, its arguments, a description of each, and more. Note that you may find some of the text in the documentation a bit too technical right now. Fear not: as you work through this book, many of these terms will be introduced -to you, and slowly but surely you will become more adept at understanding and navigating +to you, and slowly but surely you will become more adept at understanding and navigating documentation like that shown in {numref}`help_read_csv`. But do keep in mind that the documentation is not written to *teach* you about a function; it is just there as a reference to *remind* you about the different arguments and usage of functions that you have already learned about elsewhere. @@ -994,14 +1105,55 @@ The documentation for the read_csv function including a high-level description, +++ -If you are working in a Jupyter Lab environment, there are also two more convenient -ways to access documentation for functions. **JOEL ADD TEXT AND IMAGES HERE**. +If you are working in a Jupyter Lab environment, there are some conveniences that will help you lookup function names +and access the documentation. +You can type the first characters of the function you want to use, +and then press Tab to bring up small menu +that shows you all the available functions +that starts with those characters. +This is helpful both for remembering function names +and to prevent typos. + ++++ + +```{figure} img/completion_menu.png +--- +height: 400px +name: completion_menu +--- +The suggestions that are shown after typing `pd.read` and pressing Tab. +``` + ++++ + +To get more info on the function you want to use, +you can type out the full name +and then hold Shift while pressing Tab +to bring up a help dialogue including the same information as when using `help()`. + ++++ + +```{figure} img/help_dialog.png +--- +height: 400px +name: help_dialog +--- +The help dialog that is shown after typing `pd.read_csv` and then pressing Shift + Tab. +``` + ++++ +Finally, +it can be helpful to have this help dialog open at all times, +especially when you start out learning about programming and data science. +You can achieve this by clicking on the `Help` text +in the menu bar at the top +and then selecting `Show Contextual Help`. ## Exercises -Practice exercises for the material covered in this chapter -can be found in the accompanying +Practice exercises for the material covered in this chapter +can be found in the accompanying [worksheets repository](https://github.com/UBC-DSCI/data-science-a-first-intro-python-worksheets#readme) in the "Python and Pandas" row. You can launch an interactive version of the worksheet in your browser by clicking the "launch binder" button. diff --git a/source/preface-text.md b/source/preface-text.md index 75ae6344..139fe55c 100644 --- a/source/preface-text.md +++ b/source/preface-text.md @@ -13,11 +13,16 @@ kernelspec: name: python3 --- -# Preface -- TBD +# Preface + +```{index} data science, auditable, reproducible +``` + + This textbook aims to be an approachable introduction to the world of data science. -In this book, we define **data science** \index{data science!definition} as the process of generating -insight from data through **reproducible** \index{reproducible} and **auditable** \index{auditable} processes. +In this book, we define **data science** as the process of generating +insight from data through **reproducible** and **auditable** processes. If you analyze some data and give your analysis to a friend or colleague, they should be able to re-run the analysis from start to finish and get the same result you did (*reproducibility*). They should also be able to see and understand all the steps in the analysis, as well as the history of how @@ -29,19 +34,17 @@ At a high level, in this book, you will learn how to (1) identify common problems in data science, and (2) solve those problems with reproducible and auditable workflows. -Figure \@ref(fig:img-chapter-overview) summarizes what you will learn in each chapter -of this book. -Throughout, you will learn how to use the R programming language [@Rlanguage] to perform +{numref}`preface-overview-fig` summarizes what you will learn in each chapter +of this book. Throughout, you will learn how to use the [Python programming language](https://www.python.org/) to perform all the tasks associated with data analysis. You will -spend the first four chapters learning how to use R to load, clean, wrangle +spend the first four chapters learning how to use Python to load, clean, wrangle (i.e., restructure the data into a usable format) and visualize data while answering descriptive and exploratory data analysis questions. In the next six chapters, you will learn how to answer predictive, exploratory, and inferential data analysis questions with common methods in data science, including classification, regression, clustering, and estimation. In the final chapters -(\@ref(getting-started-with-jupyter)–\@ref(move-to-your-own-machine)), -you will learn how to combine R code, formatted text, and images +you will learn how to combine Python code, formatted text, and images in a single coherent document with Jupyter, use version control for collaboration, and install and configure the software needed for data science on your own computer. If you are reading this book as part of a course that you are @@ -51,20 +54,26 @@ But if you are reading this independently, you may want to jump to these last th early before going on to make sure your computer is set up in such a way that you can try out the example code that we include throughout the book. -```{r img-chapter-overview, echo = FALSE, message = FALSE, warning = FALSE, fig.cap = "Where are we going?", out.width="100%", fig.retina = 2, fig.align = "center"} -knitr::include_graphics("img/chapter_overview.jpeg") +```{figure} img/chapter_overview.jpeg +--- +height: 400px +name: preface-overview-fig +--- +Where are we going? ``` + + Each chapter in the book has an accompanying worksheet that provides exercises to help you practice the concepts you will learn. We strongly recommend that you work through the worksheet when you finish reading each chapter before moving on to the next chapter. All of the worksheets are available at -[https://github.com/UBC-DSCI/data-science-a-first-intro-worksheets#readme](https://github.com/UBC-DSCI/data-science-a-first-intro-worksheets#readme); +[https://github.com/UBC-DSCI/data-science-a-first-intro-python-worksheets#readme](https://github.com/UBC-DSCI/data-science-a-first-intro-python-worksheets#readme); the "Exercises" section at the end of each chapter points you to the right worksheet for that chapter. For each worksheet, you can either launch an interactive version of the worksheet in your browser by clicking the "launch binder" button, or preview a non-interactive version of the worksheet by clicking "view worksheet." If you instead decide to download the worksheet and run it on your own machine, make sure to follow the instructions for computer setup -found in Chapter \@ref(move-to-your-own-machine). This will ensure that the automated feedback +found in the {ref}`move-to-your-own-machine` chapter. This will ensure that the automated feedback and guidance that the worksheets provide will function as intended. diff --git a/source/reading.md b/source/reading.md index 4febd2cd..4182df15 100644 --- a/source/reading.md +++ b/source/reading.md @@ -16,7 +16,7 @@ kernelspec: # Reading in data locally and from the web -## Overview +## Overview ```{index} see: loading; reading ``` @@ -46,10 +46,10 @@ By the end of the chapter, readers will be able to do the following: - **U**niform **R**esource **L**ocator (URL) - Read data into Python using an absolute path, relative path and a URL. - Compare and contrast the following functions: - - `read_csv` + - `read_csv` - `read_excel` - Match the following `pandas` `read_csv` function arguments to their descriptions: - - `filepath_or_buffer` + - `filepath_or_buffer` - `sep` - `names` - `skiprows` @@ -76,7 +76,7 @@ This chapter will discuss the different functions we can use to import data into Python, but before we can talk about *how* we read the data into Python with these functions, we first need to talk about *where* the data lives. When you load a data set into Python, you first need to tell Python where those files live. The file -could live on your computer (*local*) or somewhere on the internet (*remote*). +could live on your computer (*local*) or somewhere on the internet (*remote*). The place where the file lives on your computer is called the "path". You can think of the path as directions to the file. There are two kinds of paths: @@ -90,7 +90,7 @@ in respect to the computer's filesystem base (or root) folder. Suppose our computer's filesystem looks like the picture in {numref}`Filesystem`, and we are working in a -file titled `worksheet_02.ipynb`. If we want to +file titled `worksheet_02.ipynb`. If we want to read the `.csv` file named `happiness_report.csv` into Python, we could do this using either a relative or an absolute path. We show both choices below. @@ -124,24 +124,24 @@ happy_data = pd.read_csv("/home/dsci-100/worksheet_02/data/happiness_report.csv" +++ -So which one should you use? Generally speaking, to ensure your code can be run -on a different computer, you should use relative paths. An added bonus is that -it's also less typing! Generally, you should use relative paths because the file's -absolute path (the names of -folders between the computer's root `/` and the file) isn't usually the same -across different computers. For example, suppose Fatima and Jayden are working on a -project together on the `happiness_report.csv` data. Fatima's file is stored at +So which one should you use? Generally speaking, to ensure your code can be run +on a different computer, you should use relative paths. An added bonus is that +it's also less typing! Generally, you should use relative paths because the file's +absolute path (the names of +folders between the computer's root `/` and the file) isn't usually the same +across different computers. For example, suppose Fatima and Jayden are working on a +project together on the `happiness_report.csv` data. Fatima's file is stored at ``` /home/Fatima/project/data/happiness_report.csv ``` -while Jayden's is stored at +while Jayden's is stored at ``` /home/Jayden/project/data/happiness_report.csv ``` - + Even though Fatima and Jayden stored their files in the same place on their computers (in their home folders), the absolute paths are different due to their different usernames. If Jayden has code that loads the @@ -154,10 +154,10 @@ relative paths will work on both! ``` Your file could be stored locally, as we discussed, or it could also be -somewhere on the internet (remotely). For this purpose we use a +somewhere on the internet (remotely). For this purpose we use a *Uniform Resource Locator (URL)*, i.e., a web address that looks something like https://google.com/. URLs indicate the location of a resource on the internet and -helps us retrieve that resource. +helps us retrieve that resource. ## Reading tabular data from a plain text file into Python @@ -168,26 +168,26 @@ helps us retrieve that resource. ``` Now that we have learned about *where* data could be, we will learn about *how* -to import data into Python using various functions. Specifically, we will learn how +to import data into Python using various functions. Specifically, we will learn how to *read* tabular data from a plain text file (a document containing only text) *into* Python and *write* tabular data to a file *out of* Python. The function we use to do this depends on the file's format. For example, in the last chapter, we learned about using the `read_csv` function from `pandas` when reading `.csv` (**c**omma-**s**eparated **v**alues) files. In that case, the *separator* that divided our columns was a -comma (`,`). We only learned the case where the data matched the expected defaults -of the `read_csv` function -(column names are present, and commas are used as the separator between columns). -In this section, we will learn how to read +comma (`,`). We only learned the case where the data matched the expected defaults +of the `read_csv` function +(column names are present, and commas are used as the separator between columns). +In this section, we will learn how to read files that do not satisfy the default expectations of `read_csv`. ```{index} Canadian languages; canlang data ``` -Before we jump into the cases where the data aren't in the expected default format +Before we jump into the cases where the data aren't in the expected default format for `pandas` and `read_csv`, let's revisit the more straightforward case where the defaults hold, and the only argument we need to give to the function -is the path to the file, `data/can_lang.csv`. The `can_lang` data set contains -language data from the 2016 Canadian census. +is the path to the file, `data/can_lang.csv`. The `can_lang` data set contains +language data from the 2016 Canadian census. We put `data/` before the file's name when we are loading the data set because this data set is located in a sub-folder, named `data`, relative to where we are running our Python code. @@ -209,18 +209,19 @@ Non-Official & Non-Aboriginal languages,Amharic,22465,12785,200,33670 ```{index} pandas ``` -And here is a review of how we can use `read_csv` to load it into Python. First we +And here is a review of how we can use `read_csv` to load it into Python. First we load the `pandas` package to gain access to useful -functions for reading the data. +functions for reading the data. ```{code-cell} ipython3 -import pandas as pd +import pandas as pd ``` Next we use `read_csv` to load the data into Python, and in that call we specify the relative path to the file. ```{code-cell} ipython3 +:tags: ["output_scroll"] canlang_data = pd.read_csv("data/can_lang.csv") canlang_data ``` @@ -269,19 +270,20 @@ ParserError: Error tokenizing data. C error: Expected 1 fields in line 4, saw 6 ```{index} read function; skiprows argument ``` -To successfully read data like this into Python, the `skiprows` -argument can be useful to tell Python +To successfully read data like this into Python, the `skiprows` +argument can be useful to tell Python how many rows to skip before it should start reading in the data. In the example above, we would set this value to 3 to read and load the data correctly. ```{code-cell} ipython3 +:tags: ["output_scroll"] canlang_data = pd.read_csv("data/can_lang_meta-data.csv", skiprows=3) canlang_data ``` How did we know to skip three rows? We looked at the data! The first three rows -of the data had information we didn't need to import: +of the data had information we didn't need to import: ```code Data source: https://ttimbers.github.io/canlang/ @@ -289,13 +291,13 @@ Data originally published in: Statistics Canada Census of Population 2016. Reproduced and distributed on an as-is basis with their permission. ``` -The column names began at row 4, so we skipped the first three rows. +The column names began at row 4, so we skipped the first three rows. ### Using the `sep` argument for different separators Another common way data is stored is with tabs as the separator. Notice the data file, `can_lang.tsv`, has tabs in between the columns instead of -commas. +commas. ```code category language mother_tongue most_at_home most_at_work lang_known @@ -318,26 +320,27 @@ Non-Official & Non-Aboriginal languages Amharic 22465 12785 200 33670 ```{index} tsv, read function; read_tsv ``` -To read in `.tsv` (**t**ab **s**eparated **v**alues) files, we can set the `sep` argument +To read in `.tsv` (**t**ab **s**eparated **v**alues) files, we can set the `sep` argument in the `read_csv` function to the *tab character* `\t`. ```{index} escape character ``` -> **Note:** `\t` is an example of an *escaped character*, +> **Note:** `\t` is an example of an *escaped character*, > which always starts with a backslash (`\`). -> Escaped characters are used to represent non-printing characters +> Escaped characters are used to represent non-printing characters > (like the tab) or characters with special meanings (such as quotation marks). ```{code-cell} ipython3 +:tags: ["output_scroll"] canlang_data = pd.read_csv("data/can_lang.tsv", sep="\t") canlang_data ``` Let's compare the data frame here to the resulting data frame in Section {ref}`readcsv` after using `read_csv`. Notice anything? They look the same; they have -the same number of columns and rows, and have the same column names! +the same number of columns and rows, and have the same column names! So even though we needed to use different arguments depending on the file format, our resulting data frame (`canlang_data`) in both cases was the same. @@ -365,7 +368,7 @@ Non-Official & Non-Aboriginal languages Amharic 22465 12785 200 33670 ``` Data frames in Python need to have column names. Thus if you read in data that -don't have column names, Python will assign names automatically. In this example, +don't have column names, Python will assign names automatically. In this example, Python assigns each column a name of `0, 1, 2, 3, 4, 5`. To read this data into Python, we specify the first argument as the path to the file (as done with `read_csv`), and then provide @@ -374,9 +377,10 @@ and finally set `header = None` to tell `pandas` that the data file does not contain its own column names. ```{code-cell} ipython3 +:tags: ["output_scroll"] canlang_data = pd.read_csv( - "data/can_lang_no_cols.tsv", - sep = "\t", + "data/can_lang_no_cols.tsv", + sep = "\t", header = None ) canlang_data @@ -387,10 +391,10 @@ canlang_data It is best to rename your columns manually in this scenario. The current column names (`0, 1`, etc.) are problematic for two reasons: first, because they not very descriptive names, which will make your analysis -confusing; and second, because your column names should generally be *strings*, but are currently *integers*. +confusing; and second, because your column names should generally be *strings*, but are currently *integers*. To rename your columns, you can use the `rename` function -from the [pandas package](https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.rename.html#). -The argument of the `rename` function is `columns`, which takes a mapping between the old column names and the new column names. +from the [pandas package](https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.rename.html#). +The argument of the `rename` function is `columns`, which takes a mapping between the old column names and the new column names. In this case, we want to rename the old columns (`0, 1, ..., 5`) in the `canlang_data` data frame to more descriptive names. To specify the mapping, we create a *dictionary*: a Python object that represents @@ -400,6 +404,7 @@ Below, we create a dictionary called `col_map` that maps the old column names in names, and then pass it to the `rename` function. ```{code-cell} ipython3 +:tags: ["output_scroll"] col_map = { 0 : "category", 1 : "language", @@ -415,10 +420,11 @@ canlang_data_renamed ```{index} read function; names argument ``` -The column names can also be assigned to the data frame immediately upon reading it from the file by passing a -list of column names to the `names` argument in `read_csv`. +The column names can also be assigned to the data frame immediately upon reading it from the file by passing a +list of column names to the `names` argument in `read_csv`. ```{code-cell} ipython3 +:tags: ["output_scroll"] canlang_data = pd.read_csv( "data/can_lang_no_cols.tsv", sep="\t", @@ -448,6 +454,7 @@ path on our local computer. All other arguments that we use are the same as when using these functions with a local file on our computer. ```{code-cell} ipython3 +:tags: ["output_scroll"] url = "https://raw.githubusercontent.com/UBC-DSCI/introduction-to-datascience-python/reading/source/data/can_lang.csv" pd.read_csv(url) canlang_data = pd.read_csv(url) @@ -497,8 +504,8 @@ t 8f??3wn ?Pd(??J-?E???7?'t(?-GZ?????y???c~N?g[^_r?4 yG?O ?K??G? - - + + ]TUEe??O??c[???????6q??s??d?m???\???H?^????3} ?rZY? ?:L60?^?????XTP+?|? X?a??4VT?,D?Jq ``` @@ -509,11 +516,12 @@ X?a??4VT?,D?Jq This type of file representation allows Excel files to store additional things that you cannot store in a `.csv` file, such as fonts, text formatting, graphics, multiple sheets and more. And despite looking odd in a plain text -editor, we can read Excel spreadsheets into Python using the `pandas` package's `read_excel` -function developed specifically for this +editor, we can read Excel spreadsheets into Python using the `pandas` package's `read_excel` +function developed specifically for this purpose. ```{code-cell} ipython3 +:tags: ["output_scroll"] canlang_data = pd.read_excel("data/can_lang.xlsx") canlang_data ``` @@ -522,13 +530,13 @@ If the `.xlsx` file has multiple sheets, you have to use the `sheet_name` argume to specify the sheet number or name. This functionality is useful when a single sheet contains multiple tables (a sad thing that happens to many Excel spreadsheets since this makes reading in data more difficult). You can also specify cell ranges using the -`usecols` argument (e.g., `usecols="A:D"` for including columns from `A` to `D`). +`usecols` argument (e.g., `usecols="A:D"` for including columns from `A` to `D`). As with plain text files, you should always explore the data file before importing it into Python. Exploring the data beforehand helps you decide which arguments you need to load the data into Python successfully. If you do not have the Excel program on your computer, you can use other programs to preview the -file. Examples include Google Sheets and Libre Office. +file. Examples include Google Sheets and Libre Office. In {numref}`read_func` we summarize the `read_csv` and `read_excel` functions we covered in this chapter. We also include the arguments for data separated by @@ -547,20 +555,20 @@ European countries). * - Comma (`,`) separated files - `read_csv` - just the file path -* - Tab (`\t`) separated files +* - Tab (`\t`) separated files - `read_csv` - `sep="\t"` * - Missing header - `read_csv` - `header=None` * - European-style numbers, semicolon (`;`) separators - - `read_csv` + - `read_csv` - `sep=";"`, `thousands="."`, `decimal=","` * - Excel files (`.xlsx`) - `read_excel` - `sheet_name`, `usecols` - - + + ``` ## Reading data from a database @@ -576,7 +584,7 @@ different relational database management systems each have their own advantages and limitations. Almost all employ SQL (*structured query language*) to obtain data from the database. But you don't need to know SQL to analyze data from a database; several packages have been written that allow you to connect to -relational databases and use the Python programming language +relational databases and use the Python programming language to obtain data. In this book, we will give examples of how to do this using Python with SQLite and PostgreSQL databases. @@ -588,8 +596,8 @@ using Python with SQLite and PostgreSQL databases. SQLite is probably the simplest relational database system that one can use in combination with Python. SQLite databases are self-contained and usually stored and accessed locally on one computer. Data is usually stored in -a file with a `.db` extension (or sometimes a `.sqlite` extension). -Similar to Excel files, these are not plain text files and cannot be read in a plain text editor. +a file with a `.db` extension (or sometimes a `.sqlite` extension). +Similar to Excel files, these are not plain text files and cannot be read in a plain text editor. ```{index} database; connect, ibis, ibis; ibis ``` @@ -598,18 +606,18 @@ Similar to Excel files, these are not plain text files and cannot be read in a p ``` The first thing you need to do to read data into Python from a database is to -connect to the database. For an SQLite database, we will do that using +connect to the database. For an SQLite database, we will do that using the `connect` function from the `sqlite` backend in the `ibis` package. This command does not read in the data, but simply tells Python where the database is and opens up a communication channel that Python can use to send SQL commands to the database. -> **Note:** There is another database package in python called `sqlalchemy`. +> **Note:** There is another database package in python called `sqlalchemy`. > That package is a bit more mature than `ibis`, -> so if you want to dig deeper into working with databases in Python, that is a good next -> package to learn about. We will work with `ibis` in this book, as it -> provides a more modern and friendlier syntax that is more like `pandas` for data analysis code. +> so if you want to dig deeper into working with databases in Python, that is a good next +> package to learn about. We will work with `ibis` in this book, as it +> provides a more modern and friendlier syntax that is more like `pandas` for data analysis code. ```{code-cell} ipython3 import ibis @@ -621,7 +629,7 @@ conn = ibis.sqlite.connect("data/can_lang.db") ``` Often relational databases have many tables; thus, in order to retrieve -data from a database, you need to know the name of the table +data from a database, you need to know the name of the table in which the data is stored. You can get the names of all the tables in the database using the `list_tables` function: @@ -636,22 +644,22 @@ tables The `list_tables` function returned only one name---`"can_lang"`---which tells us that there is only one table in this database. To reference a table in the -database (so that we can perform operations like selecting columns and filtering rows), we +database (so that we can perform operations like selecting columns and filtering rows), we use the `table` function from the `conn` object. The object returned by the `table` function allows us to work with data stored in databases as if they were just regular `pandas` data frames; but secretly, behind -the scenes, `ibis` will turn your commands into SQL queries! +the scenes, `ibis` will turn your commands into SQL queries! ```{code-cell} ipython3 canlang_table = conn.table("can_lang") -canlang_table +canlang_table ``` ```{index} database; count, ibis; count ``` Although it looks like we might have obtained the whole data frame from the database, we didn't! -It's a *reference*; the data is still stored only in the SQLite database. The `canlang_table` object +It's a *reference*; the data is still stored only in the SQLite database. The `canlang_table` object is an `AlchemyTable` (`ibis` is using `sqlalchemy` under the hood!), which, when printed, tells you which columns are available in the table. But unlike a usual `pandas` data frame, we do not immediately know how many rows are in the table. In order to find out how many @@ -665,7 +673,7 @@ canlang_table.count() ```{index} execute, ibis; execute ``` -Wait a second...this isn't the number of rows in the database. In fact, we haven't actually sent our +Wait a second...this isn't the number of rows in the database. In fact, we haven't actually sent our SQL query to the database yet! We need to explicitly tell `ibis` when we want to send the query. The reason for this is that databases are often more efficient at working with (i.e., selecting, filtering, joining, etc.) large data sets than Python. And typically, the database will not even @@ -693,23 +701,24 @@ str(canlang_table.count().compile()) The output above shows the SQL code that is sent to the database. When we write `canlang_table.count().execute()` in Python, in the background, the `execute` function is translating the Python code into SQL, sending that SQL to the database, and then translating the -response for us. So `ibis` does all the hard work of translating from Python to SQL and back for us; -we can just stick with Python! +response for us. So `ibis` does all the hard work of translating from Python to SQL and back for us; +we can just stick with Python! The `ibis` package provides lots of `pandas`-like tools for working with database tables. -For example, we can look at the first few rows of the table by using the `head` function---and +For example, we can look at the first few rows of the table by using the `head` function---and we won't forget to `execute` to see the result! ```{index} database; head, ibis; ``` ```{code-cell} ipython3 +:tags: ["output_scroll"] canlang_table.head(10).execute() ``` You can see that `ibis` actually returned a `pandas` data frame to us after we executed the query, which is very convenient for working with the data after getting it from the database. -So now that we have the `canlang_table` table reference for the 2016 Canadian Census data in hand, we +So now that we have the `canlang_table` table reference for the 2016 Canadian Census data in hand, we can mostly continue onward as if it were a regular data frame. For example, let's do the same exercise from Chapter 1: we will obtain only those rows corresponding to Aboriginal languages, and keep only the `language` and `mother_tongue` columns. @@ -723,7 +732,7 @@ to obtain only certain rows. Below we filter the data to include only Aboriginal canlang_table_filtered = canlang_table[canlang_table["category"] == "Aboriginal languages"] canlang_table_filtered ``` -Above you can see that we have not yet executed this command; `canlang_table_filtered` is just showing +Above you can see that we have not yet executed this command; `canlang_table_filtered` is just showing the first part of our query (the part that starts with `Selection[r0]` above). We didn't call `execute` because we are not ready to bring the data into Python yet. We can still use the database to do some work to obtain *only* the small amount of data we want to work with locally @@ -746,7 +755,7 @@ aboriginal_lang_data `ibis` provides many more functions (not just the `[]` operation) that you can use to manipulate the data within the database before calling -`execute` to obtain the data in Python. But `ibis` does not provide *every* function +`execute` to obtain the data in Python. But `ibis` does not provide *every* function that we need for analysis; we do eventually need to call `execute`. For example, `ibis` does not provide the `tail` function to look at the last rows in a database, even though `pandas` does. @@ -755,6 +764,7 @@ rows in a database, even though `pandas` does. ``` ```{code-cell} ipython3 +:tags: ["output_scroll"] canlang_table_selected.tail(6) ``` @@ -768,14 +778,14 @@ But be very careful using `execute`: databases are often *very* big, and reading an entire table into Python might take a long time to run or even possibly crash your machine. So make sure you select and filter the database table to reduce the data to a reasonable size before using `execute` to read it into Python! - -### Reading data from a PostgreSQL database + +### Reading data from a PostgreSQL database ```{index} database; PostgreSQL ``` PostgreSQL (also called Postgres) is a very popular -and open-source option for relational database software. +and open-source option for relational database software. Unlike SQLite, PostgreSQL uses a client–server database engine, as it was designed to be used and accessed on a network. This means that you have to provide more information @@ -790,13 +800,13 @@ need to include when you call the `connect` function is listed below: Below we demonstrate how to connect to a version of the `can_mov_db` database, which contains information about Canadian movies. -Note that the `host` (`fakeserver.stat.ubc.ca`), `user` (`user0001`), and -`password` (`abc123`) below are *not real*; you will not actually +Note that the `host` (`fakeserver.stat.ubc.ca`), `user` (`user0001`), and +`password` (`abc123`) below are *not real*; you will not actually be able to connect to a database using this information. ```python conn = ibis.postgres.connect( - database = "can_mov_db", + database = "can_mov_db", host = "fakeserver.stat.ubc.ca", port = 5432, user = "user0001", @@ -819,7 +829,7 @@ conn.list_tables() We see that there are 10 tables in this database. Let's first look at the `"ratings"` table to find the lowest rating that exists in the `can_mov_db` -database. +database. ```python ratings_table = conn.table("ratings") @@ -887,18 +897,18 @@ then use `ibis` to translate `pandas`-like commands (the `[]` operation, `head`, etc.) into SQL queries that the database understands, and then finally `execute` them. And not all `pandas` commands can currently be translated via `ibis` into database queries. So you might be wondering: why should we use -databases at all? +databases at all? Databases are beneficial in a large-scale setting: - They enable storing large data sets across multiple computers with backups. - They provide mechanisms for ensuring data integrity and validating input. - They provide security and data access control. -- They allow multiple users to access data simultaneously +- They allow multiple users to access data simultaneously and remotely without conflicts and errors. - For example, there are billions of Google searches conducted daily in 2021 {cite:p}`googlesearches`. - Can you imagine if Google stored all of the data - from those searches in a single `.csv` file!? Chaos would ensue! + For example, there are billions of Google searches conducted daily in 2021 {cite:p}`googlesearches`. + Can you imagine if Google stored all of the data + from those searches in a single `.csv` file!? Chaos would ensue! ## Writing data from Python to a `.csv` file @@ -910,7 +920,7 @@ that has changed (through selecting columns, filtering rows, etc.) to a file to share it with others or use it for another step in the analysis. The most straightforward way to do this is to use the `to_csv` function from the `pandas` package. The default -arguments are to use a comma (`,`) as the separator, and to include column names +arguments are to use a comma (`,`) as the separator, and to include column names in the first row. We also specify `index = False` to tell `pandas` not to print row numbers in the `.csv` file. Below we demonstrate creating a new version of the Canadian languages data set without the "Official languages" category according to the @@ -921,18 +931,18 @@ no_official_lang_data = canlang_data[canlang_data["category"] != "Official langu no_official_lang_data.to_csv("data/no_official_languages.csv", index=False) ``` -% ## Obtaining data from the web -% +% ## Obtaining data from the web +% % > **Note:** This section is not required reading for the remainder of the textbook. It % > is included for those readers interested in learning a little bit more about % > how to obtain different types of data from the web. -% +% % ```{index} see: application programming interface; API % ``` -% +% % ```{index} API % ``` -% +% % Data doesn't just magically appear on your computer; you need to get it from % somewhere. Earlier in the chapter we showed you how to access data stored in a % plain text, spreadsheet-like format (e.g., comma- or tab-separated) from a web @@ -946,16 +956,16 @@ no_official_lang_data.to_csv("data/no_official_languages.csv", index=False) % data they have access to, and *how much* data they can access. Typically, the % website owner will give you a *token* (a secret string of characters somewhat % like a password) that you have to provide when accessing the API. -% +% % ```{index} web scraping, CSS, HTML % ``` -% +% % ```{index} see: hypertext markup language; HTML % ``` -% +% % ```{index} see: cascading style sheet; CSS % ``` -% +% % Another interesting thought: websites themselves *are* data! When you type a % URL into your browser window, your browser asks the *web server* (another % computer on the internet whose job it is to respond to requests for the @@ -963,117 +973,117 @@ no_official_lang_data.to_csv("data/no_official_languages.csv", index=False) % data into something you can see. If the website shows you some information that % you're interested in, you could *create* a data set for yourself by copying and % pasting that information into a file. This process of taking information -% directly from what a website displays is called +% directly from what a website displays is called % *web scraping* (or sometimes *screen scraping*). Now, of course, copying and pasting % information manually is a painstaking and error-prone process, especially when % there is a lot of information to gather. So instead of asking your browser to % translate the information that the web server provides into something you can % see, you can collect that data programmatically—in the form of -% **h**yper**t**ext **m**arkup **l**anguage -% (HTML) -% and **c**ascading **s**tyle **s**heet (CSS) code—and process it +% **h**yper**t**ext **m**arkup **l**anguage +% (HTML) +% and **c**ascading **s**tyle **s**heet (CSS) code—and process it % to extract useful information. HTML provides the % basic structure of a site and tells the webpage how to display the content % (e.g., titles, paragraphs, bullet lists etc.), whereas CSS helps style the -% content and tells the webpage how the HTML elements should -% be presented (e.g., colors, layouts, fonts etc.). -% +% content and tells the webpage how the HTML elements should +% be presented (e.g., colors, layouts, fonts etc.). +% % This subsection will show you the basics of both web scraping % with the [`BeautifulSoup` Python package](https://beautiful-soup-4.readthedocs.io/en/latest/) {cite:p}`beautifulsoup` % and accessing the Twitter API % using the [`tweepy` Python package](https://github.com/tweepy/tweepy) {cite:p}`tweepy`. -% +% % +++ -% +% % ### Web scraping -% +% % #### HTML and CSS selectors -% +% % ```{index} web scraping, HTML; selector, CSS; selector, Craiglist % ``` -% +% % When you enter a URL into your browser, your browser connects to the % web server at that URL and asks for the *source code* for the website. -% This is the data that the browser translates +% This is the data that the browser translates % into something you can see; so if we % are going to create our own data by scraping a website, we have to first understand % what that data looks like! For example, let's say we are interested % in knowing the average rental price (per square foot) of the most recently -% available one-bedroom apartments in Vancouver +% available one-bedroom apartments in Vancouver % on [Craiglist](https://vancouver.craigslist.org). When we visit the Vancouver Craigslist -% website and search for one-bedroom apartments, +% website and search for one-bedroom apartments, % we should see something similar to {numref}`fig:craigslist-human`. -% +% % +++ -% +% % ```{figure} img/craigslist_human.png % :name: fig:craigslist-human -% +% % Craigslist webpage of advertisements for one-bedroom apartments. % ``` -% +% % +++ -% +% % Based on what our browser shows us, it's pretty easy to find the size and price % for each apartment listed. But we would like to be able to obtain that information % using Python, without any manual human effort or copying and pasting. We do this by % examining the *source code* that the web server actually sent our browser to -% display for us. We show a snippet of it below; the -% entire source +% display for us. We show a snippet of it below; the +% entire source % is [included with the code for this book](https://github.com/UBC-DSCI/introduction-to-datascience-python/blob/main/source/img/website_source.txt): -% +% % ```html % % $800 -% +% % % 1br - % -% +% % (13768 108th Avenue) -% +% % % map % -% +% % % hide this posting % -% +% % % restore % restore this posting % -% +% % %

% %
  • -% +% % $2285 % % ``` -% +% % Oof...you can tell that the source code for a web page is not really designed % for humans to understand easily. However, if you look through it closely, you % will find that the information we're interested in is hidden among the muck. % For example, near the top of the snippet % above you can see a line that looks like -% +% % ```html % $800 % ``` -% +% % That is definitely storing the price of a particular apartment. With some more % investigation, you should be able to find things like the date and time of the % listing, the address of the listing, and more. So this source code most likely % contains all the information we are interested in! -% +% % ```{index} HTML; tag % ``` -% +% % Let's dig into that line above a bit more. You can see that % that bit of code has an *opening tag* (words between `<` and `>`, like % ``) and a *closing tag* (the same with a slash, like ``). HTML @@ -1087,86 +1097,86 @@ no_official_lang_data.to_csv("data/no_official_languages.csv", index=False) % apartment prices, maybe we can look for all the tags with the `"result-price"` % class, and grab the information between the opening and closing tag. Indeed, % take a look at another line of the source snippet above: -% +% % ```html % $2285 % ``` -% +% % It's yet another price for an apartment listing, and the tags surrounding it % have the `"result-price"` class. Wonderful! Now that we know what pattern we % are looking for—a dollar amount between opening and closing tags that have the -% `"result-price"` class—we should be able to use code to pull out all of the +% `"result-price"` class—we should be able to use code to pull out all of the % matching patterns from the source code to obtain our data. This sort of "pattern" % is known as a *CSS selector* (where CSS stands for **c**ascading **s**tyle **s**heet). -% -% The above was a simple example of "finding the pattern to look for"; many +% +% The above was a simple example of "finding the pattern to look for"; many % websites are quite a bit larger and more complex, and so is their website % source code. Fortunately, there are tools available to make this process -% easier. For example, -% [SelectorGadget](https://selectorgadget.com/) is -% an open-source tool that simplifies identifying the generating -% and finding of CSS selectors. +% easier. For example, +% [SelectorGadget](https://selectorgadget.com/) is +% an open-source tool that simplifies identifying the generating +% and finding of CSS selectors. % At the end of the chapter in the additional resources section, we include a link to -% a short video on how to install and use the SelectorGadget tool to -% obtain CSS selectors for use in web scraping. -% After installing and enabling the tool, you can click the -% website element for which you want an appropriate selector. For +% a short video on how to install and use the SelectorGadget tool to +% obtain CSS selectors for use in web scraping. +% After installing and enabling the tool, you can click the +% website element for which you want an appropriate selector. For % example, if we click the price of an apartment listing, we % find that SelectorGadget shows us the selector `.result-price` % in its toolbar, and highlights all the other apartment % prices that would be obtained using that selector ({numref}`fig:sg1`). -% +% % ```{figure} img/sg1.png % :name: fig:sg1 -% +% % Using the SelectorGadget on a Craigslist webpage to obtain the CCS selector useful for obtaining apartment prices. % ``` -% +% % If we then click the size of an apartment listing, SelectorGadget shows us % the `span` selector, and highlights many of the lines on the page; this indicates that the -% `span` selector is not specific enough to capture only apartment sizes ({numref}`fig:sg3`). -% +% `span` selector is not specific enough to capture only apartment sizes ({numref}`fig:sg3`). +% % ```{figure} img/sg3.png % :name: fig:sg3 -% +% % Using the SelectorGadget on a Craigslist webpage to obtain a CCS selector useful for obtaining apartment sizes. % ``` -% +% % To narrow the selector, we can click one of the highlighted elements that -% we *do not* want. For example, we can deselect the "pic/map" links, +% we *do not* want. For example, we can deselect the "pic/map" links, % resulting in only the data we want highlighted using the `.housing` selector ({numref}`fig:sg2`). -% +% % ```{figure} img/sg2.png % :name: fig:sg2 -% +% % Using the SelectorGadget on a Craigslist webpage to refine the CCS selector to one that is most useful for obtaining apartment sizes. % ``` -% +% % So to scrape information about the square footage and rental price % of apartment listings, we need to use % the two CSS selectors `.housing` and `.result-price`, respectively. % The selector gadget returns them to us as a comma-separated list (here % `.housing , .result-price`), which is exactly the format we need to provide to % Python if we are using more than one CSS selector. -% +% % **Stop! Are you allowed to scrape that website?** -% +% % ```{index} web scraping; permission % ``` -% +% % +++ -% +% % *Before* scraping data from the web, you should always check whether or not % you are *allowed* to scrape it! There are two documents that are important % for this: the `robots.txt` file and the Terms of Service % document. If we take a look at [Craigslist's Terms of Service document](https://www.craigslist.org/about/terms.of.use), -% we find the following text: *"You agree not to copy/collect CL content +% we find the following text: *"You agree not to copy/collect CL content % via robots, spiders, scripts, scrapers, crawlers, or any automated or manual equivalent (e.g., by hand)."* % So unfortunately, without explicit permission, we are not allowed to scrape the website. -% +% % ```{index} Wikipedia % ``` -% +% % What to do now? Well, we *could* ask the owner of Craigslist for permission to scrape. % However, we are not likely to get a response, and even if we did they would not likely give us permission. % The more realistic answer is that we simply cannot scrape Craigslist. If we still want @@ -1174,122 +1184,122 @@ no_official_lang_data.to_csv("data/no_official_languages.csv", index=False) % To continue learning how to scrape data from the web, let's instead % scrape data on the population of Canadian cities from Wikipedia. % We have checked the [Terms of Service document](https://foundation.wikimedia.org/wiki/Terms_of_Use/en), -% and it does not mention that web scraping is disallowed. +% and it does not mention that web scraping is disallowed. % We will use the SelectorGadget tool to pick elements that we are interested in -% (city names and population counts) and deselect others to indicate that we are not +% (city names and population counts) and deselect others to indicate that we are not % interested in them (province names), as shown in {numref}`fig:sg4`. -% +% % ```{figure} img/selectorgadget-wiki-updated.png % :name: fig:sg4 -% +% % Using the SelectorGadget on a Wikipedia webpage. % ``` -% +% % We include a link to a short video tutorial on this process at the end of the chapter % in the additional resources section. SelectorGadget provides in its toolbar % the following list of CSS selectors to use: -% +% % +++ -% +% % ```code -% td:nth-child(8) , -% td:nth-child(6) , -% td:nth-child(4) , +% td:nth-child(8) , +% td:nth-child(6) , +% td:nth-child(4) , % .mw-parser-output div tr+ tr td:nth-child(2) % ``` -% +% % +++ -% +% % Now that we have the CSS selectors that describe the properties of the elements % that we want to target (e.g., has a tag name `price`), we can use them to find % certain elements in web pages and extract data. -% +% % +++ -% +% % **Using `pandas.read_html`** -% +% % +++ -% +% % The easiest way to read a table from HTML is to use [`pandas.read_html`](https://pandas.pydata.org/docs/reference/api/pandas.read_html.html). We can see that the Wikipedia page of "Canada" has 18 tables. -% +% % ```{code-cell} ipython3 % :tags: [remove-output] -% +% % canada_wiki = pd.read_html("https://en.wikipedia.org/wiki/Canada") % len(canada_wiki) % ``` -% +% % ``` % 18 % ``` -% +% % +++ -% +% % With some inspection, we find that the table that shows the population of the most populated provinces is of index 1. -% +% % ```{code-cell} ipython3 % :tags: [remove-output] -% +% % df = canada_wiki[1] % df.columns = df.columns.droplevel() % df % ``` -% +% % ```{code-cell} ipython3 % :tags: [remove-input] -% +% % df = pd.read_csv("data/canada-wiki-read_html.csv", index_col=0) % df % ``` -% +% % **Using `BeautifulSoup`** -% +% % ```{index} BeautifulSoup, requests % ``` -% +% % Now that we have our CSS selectors we can use the `requests` and `BeautifulSoup` Python packages to scrape our desired data from the website. We start by loading the packages: -% +% % ```{code-cell} ipython3 % import requests % from bs4 import BeautifulSoup % ``` -% +% % Next, we tell Python what page we want to scrape by providing the webpage's URL in quotations to the function `requests.get` and pass it into the `BeautifulSoup` function for parsing: -% +% % ```{code-cell} ipython3 % wiki = requests.get("https://en.wikipedia.org/wiki/Canada") % page = BeautifulSoup(wiki.content, "html.parser") % ``` -% +% % The `requests.get` function sends a `GET` request to the specified URL and returns the server's response to the HTTP request (*i.e.* a `requests.Response` object). The `BeautifulSoup` function takes the content of the response and returns the HTML source code itself, which we have % stored in the `page` variable. Next, we use the `select` method of the page object along with the CSS selectors we obtained from the SelectorGadget tool. Make sure to surround the selectors with quotation marks; `select` expects that -% argument is a string. It selects *nodes* from the HTML document that +% argument is a string. It selects *nodes* from the HTML document that % match the CSS selectors you specified. A *node* is an HTML tag pair (e.g., % `` and `` which defines the cell of a table) combined with the content % stored between the tags. For our CSS selector `td:nth-child(6)`, an example % node that would be selected would be: -% +% % +++ -% +% % ``` % % London % % ``` -% +% % +++ -% +% % We store the result of the `select` function in the `population_nodes` variable. Note that it returns a list, and we slice the list to only print the first 5 elements. -% +% % ```{code-cell} ipython3 % :tags: [remove-output] -% +% % population_nodes = page.select( % "td:nth-child(8) , td:nth-child(6) , td:nth-child(4) , .mw-parser-output div td:nth-child(2)" % ) % population_nodes[:5] % ``` -% +% % ``` % [Toronto, % 6,202,225, @@ -1298,27 +1308,27 @@ no_official_lang_data.to_csv("data/no_official_languages.csv", index=False) % , % Montreal] % ``` -% +% % +++ -% -% Next we extract the meaningful data—in other words, we get rid of the HTML code syntax and tags—from +% +% Next we extract the meaningful data—in other words, we get rid of the HTML code syntax and tags—from % the nodes using the `get_text` % function. In the case of the example % node above, `get_text` function returns `"London"`. -% +% % ```{code-cell} ipython3 % :tags: [remove-output] -% +% % [row.get_text() for row in population_nodes][:5] % ``` -% +% % ``` % ['Toronto', '6,202,225', 'London', '543,551\n', 'Montreal'] % ``` -% +% % +++ -% -% Fantastic! We seem to have extracted the data of interest from the +% +% Fantastic! We seem to have extracted the data of interest from the % raw HTML source code. But we are not quite done; the data % is not yet in an optimal format for data analysis. Both the city names and % population are encoded as characters in a single vector, instead of being in a @@ -1328,14 +1338,14 @@ no_official_lang_data.to_csv("data/no_official_languages.csv", index=False) % dealing with numbers), and some even contain a line break character at the end % (`\n`). In Chapter {ref}`wrangling`, we will learn more about how to *wrangle* data % such as this into a more useful format for data analysis using Python. -% +% % +++ -% +% % ### Using an API -% +% % ```{index} API % ``` -% +% % Rather than posting a data file at a URL for you to download, many websites these days % provide an API that must be accessed through a programming language like Python. The benefit of this % is that data owners have much more control over the data they provide to users. However, unlike @@ -1343,87 +1353,87 @@ no_official_lang_data.to_csv("data/no_official_languages.csv", index=False) % has its own API designed especially for its own use case. Therefore we will just provide one example % of accessing data through an API in this book, with the hope that it gives you enough of a basic % idea that you can learn how to use another API if needed. -% +% % ```{index} API; tweepy, tweepy, Twitter, API; token % ``` -% +% % +++ -% +% % In particular, in this book we will show you the basics of how to use % the `tweepy` package in Python to access % data from the Twitter API. `tweepy` requires the [Twitter Developer Portal](https://developer.twitter.com/en/portal/dashboard) and you will need to get tokens and secrets from that, through which your access to the data will then be authenticated and controlled. -% +% % +++ -% +% % First, we go to the [Twitter Developer Portal](https://developer.twitter.com/en/portal/dashboard) and sign up an account if you do not have one yet. Note that you will need a valid phone number to associate with your developer account. After filling out the basic information, we will get the *essential access* to the Twitter API. Then we can create an app and hit the "get key" button, and we will get the API key and API key secret of the app (along with the bearer token which will not be used in this demonstration). **We need to store the key and secret at a safe place, and make sure do not show them to anyone else (also do not accidentally push it to the GitHub repository).** If you lose the key, you can always regenerate it. Next, we go to the "Keys and tokens" tab of the app, and generate an access token and an access token secret. **Save the access token and the access token secret at a safe place as well.** Your app will look something like {numref}`fig:twitter-API-keys-tokens`. -% +% % +++ -% +% % ```{figure} img/twitter-API-keys-tokens.png % :name: fig:twitter-API-keys-tokens -% -% Generating the API key-secret pair and the access token-secret pair in Twitter API. +% +% Generating the API key-secret pair and the access token-secret pair in Twitter API. % ``` -% +% % +++ -% +% % Once you get the access keys and secrets, you can follow along with the examples that we show here. % To get started, load the `tweepy` package and authenticate our access to the Twitter developer portal account. -% +% % ```{code-cell} ipython3 % :tags: [remove-output] -% +% % import tweepy -% +% % # replace these with the api key, api key secret, access token and access token secret % # generated on your own -% api_key = "8OxHWiIWjy8M39LvnC8OfSXrj" +% api_key = "8OxHWiIWjy8M39LvnC8OfSXrj" % api_key_secret = "scqjRqX5stoy4pYB5Zu52tCBKzhGLDh5nRqTEM6CMoLRkRLR8F" -% +% % access_token = "1556029189484007425-mYwaDCI1WnCxjuMt0jb2UYD2ns8BYB" % access_token_secret = "pDG4Ta7giYLY3mablPhd6y9bB5y2Aer1Cn18rihIJFBB7" -% +% % # Authenticate to Twitter % auth = tweepy.OAuthHandler(api_key, api_key_secret) % auth.set_access_token(access_token, access_token_secret) -% +% % api = tweepy.API(auth) -% +% % try: % api.verify_credentials() % print("Successful Authentication") % except: % print("Failed authentication") % ``` -% +% % ``` % Successful Authentication % ``` -% +% % +++ -% -% `tweepy` provides an extensive set of functions to search -% Twitter for tweets, users, their followers, and more. -% Let's construct a small data set of the last 200 tweets and +% +% `tweepy` provides an extensive set of functions to search +% Twitter for tweets, users, their followers, and more. +% Let's construct a small data set of the last 200 tweets and % retweets from the [@scikit_learn](https://twitter.com/scikit_learn) account. A few of the most recent tweets % are shown in {numref}`fig:01-scikit-learn-twitter`. -% +% % +++ -% +% % ```{figure} img/scikit-learn-twitter.png % :name: fig:01-scikit-learn-twitter -% +% % The `scikit-learn` account Twitter feed. % ``` -% +% % +++ -% +% % **Stop! Think about your API usage carefully!** -% +% % When you access an API, you are initiating a transfer of data from a web server % to your computer. Web servers are expensive to run and do not have infinite resources. -% If you try to ask for *too much data* at once, you can use up a huge amount of the server's bandwidth. -% If you try to ask for data *too frequently*—e.g., if you +% If you try to ask for *too much data* at once, you can use up a huge amount of the server's bandwidth. +% If you try to ask for data *too frequently*—e.g., if you % make many requests to the server in quick succession—you can also bog the server down and make % it unable to talk to anyone else. Most servers have mechanisms to revoke your access if you are not % careful, but you should try to prevent issues from happening in the first place by being extra careful @@ -1432,19 +1442,19 @@ no_official_lang_data.to_csv("data/no_official_languages.csv", index=False) % Be careful not to overrun your quota! In this example, we should take a look at % [the Twitter website](https://developer.twitter.com/en/docs/twitter-api/rate-limits) to see what limits % we should abide by when using the API. -% +% % +++ -% +% % **Using `tweepy`** -% +% % After checking the Twitter website, it seems like asking for 200 tweets one time is acceptable. % So we can use the `user_timeline` function to ask for the last 200 tweets from the [@scikit_learn](https://twitter.com/scikit_learn) account. -% +% % ```{code-cell} ipython3 % :tags: [remove-output] -% +% % userID = "scikit_learn" -% +% % scikit_learn_tweets = api.user_timeline( % screen_name=userID, % count=200, @@ -1452,69 +1462,69 @@ no_official_lang_data.to_csv("data/no_official_languages.csv", index=False) % tweet_mode="extended", % ) % ``` -% +% % Let's take a look at the first 3 most recent tweets of [@scikit_learn](https://twitter.com/scikit_learn) through accessing the attributes of tweet data dictionary: -% +% % ```{code-cell} ipython3 % :tags: [remove-output] -% +% % for info in scikit_learn_tweets[:3]: % print("ID: {}".format(info.id)) % print(info.created_at) % print(info.full_text) % print("\n") % ``` -% +% % ``` % ID: 1555686128971403265 % 2022-08-05 22:44:11+00:00 % scikit-learn 1.1.2 is out on https://t.co/lSpi4eDc2t and conda-forge! -% +% % This is a small maintenance release that fixes a couple of regressions: % https://t.co/Oa84ES0qpG -% -% +% +% % ID: 1549321048943988737 % 2022-07-19 09:11:37+00:00 % RT @MarenWestermann: @scikit_learn It is worth highlighting that this scikit-learn sprint is seeing the highest participation of women out… -% -% +% +% % ID: 1548339716465930244 % 2022-07-16 16:12:09+00:00 % @StefanieMolin @theBodlina @RichardKlima We continue pulling requests here in Dublin. Putting some Made in Ireland code in the scikit-learn codebase šŸ‡®šŸ‡Ŗ . Current stats: 18 PRs opened, 12 merged šŸš€ https://t.co/ccWy8vh8YI % ``` -% +% % +++ -% +% % A full list of available attributes provided by Twitter API can be found [here](https://developer.twitter.com/en/docs/twitter-api/v1/data-dictionary/object-model/tweet). -% +% % +++ -% +% % For the demonstration purpose, let's only use a % few variables of interest: `created_at`, `user.screen_name`, `retweeted`, % and `full_text`, and construct a `pandas` DataFrame using the extracted information. -% +% % ```{code-cell} ipython3 % :tags: [remove-output] -% +% % columns = ["time", "user", "is_retweet", "text"] % data = [] % for tweet in scikit_learn_tweets: % data.append( % [tweet.created_at, tweet.user.screen_name, tweet.retweeted, tweet.full_text] % ) -% +% % scikit_learn_tweets_df = pd.DataFrame(data, columns=columns) % scikit_learn_tweets_df % ``` -% +% % ```{code-cell} ipython3 % :tags: [remove-input] -% +% % scikit_learn_tweets_df = pd.read_csv("data/reading_api_df.csv", index_col=0) % scikit_learn_tweets_df % ``` -% +% % If you look back up at the image of the [@scikit_learn](https://twitter.com/scikit_learn) Twitter page, you will % recognize the text of the most recent few tweets in the above data frame. In % other words, we have successfully created a small data set using the Twitter @@ -1522,21 +1532,21 @@ no_official_lang_data.to_csv("data/no_official_languages.csv", index=False) % the extracted information can be easily converted into a `pandas` data frame (although not *every* API will provide data in such a nice format). % From this point onward, the `scikit_learn_tweets_df` data frame is stored on your % machine, and you can play with it to your heart's content. For example, you can use -% `pandas.to_csv` to save it to a file and `pandas.read_csv` to read it into Python again later; +% `pandas.to_csv` to save it to a file and `pandas.read_csv` to read it into Python again later; % and after reading the next few chapters you will have the skills to % compute the percentage of retweets versus tweets, find the most oft-retweeted -% account, make visualizations of the data, and much more! If you decide that you want -% to ask the Twitter API for more data +% account, make visualizations of the data, and much more! If you decide that you want +% to ask the Twitter API for more data % (see [the `tweepy` page](https://github.com/tweepy/tweepy) % for more examples of what is possible), just be mindful as usual about how much % data you are requesting and how frequently you are making requests. -% +% % +++ ## Exercises -Practice exercises for the material covered in this chapter -can be found in the accompanying +Practice exercises for the material covered in this chapter +can be found in the accompanying [worksheets repository](https://github.com/UBC-DSCI/data-science-a-first-intro-python-worksheets#readme) in the "Reading in data locally and from the web" row. You can launch an interactive version of the worksheet in your browser by clicking the "launch binder" button. @@ -1548,7 +1558,7 @@ and guidance that the worksheets provide will function as intended. ## Additional resources -- The [`pandas` documentation](https://pandas.pydata.org/docs/getting_started/index.html) +- The [`pandas` documentation](https://pandas.pydata.org/docs/getting_started/index.html) provides the documentation for many of the reading functions we cover in this chapter. It is where you should look if you want to learn more about the functions in this chapter, the full set of arguments you can use, and other related functions. diff --git a/source/references.md b/source/references.md index c25e6545..942a1a56 100644 --- a/source/references.md +++ b/source/references.md @@ -13,6 +13,6 @@ kernelspec: name: python3 --- -`r if (knitr:::is_html_output()) ' -# References -- TBD -'` +# References + + diff --git a/source/viz.md b/source/viz.md index b9c6c0bc..5522124e 100644 --- a/source/viz.md +++ b/source/viz.md @@ -12,38 +12,55 @@ kernelspec: name: python3 --- +```{code-cell} ipython3 +:tags: [remove-cell] + +# ignore warnings from altair + +import warnings +def warn(*args, **kwargs): + pass +warnings.warn = warn +``` + + + (viz)= # Effective data visualization -## Overview +## Overview This chapter will introduce concepts and tools relating to data visualization beyond what we have seen and practiced so far. We will focus on guiding principles for effective data visualization and explaining visualizations independent of any particular tool or programming language. In the process, we will cover some specifics of creating visualizations (scatter plots, bar -plots, line plots, and histograms) for data using Python. +plots, line plots, and histograms) for data using Python. ## Chapter learning objectives By the end of the chapter, readers will be able to do the following: - +- Describe when to use the following kinds of visualizations to answer specific questions using a data set: + - scatter plots + - line plots + - bar plots + - histogram plots - Given a data set and a question, select from the above plot types and use Python to create a visualization that best answers the question. - 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. +- Identify rules of thumb for creating effective visualizations. - Define the two key aspects of altair objects: - mark objects - encodings - 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 - - subplots: facet + - mark objects: `mark_point`, `mark_line`, `mark_bar` + - encodings : `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. ## Choosing the visualization -#### *Ask a question, and answer it* {-} +#### *Ask a question, and answer it* ```{index} question; visualization ``` @@ -58,22 +75,22 @@ Imagine your visualization as part of a poster presentation for a project; even if you aren't standing at the poster explaining things, an effective visualization will convey your message to the audience. -Recall the different data analysis questions -from Chapter \@ref(intro). -With the visualizations we will cover in this chapter, -we will be able to answer *only descriptive and exploratory* questions. -Be careful to not answer any *predictive, inferential, causal* -*or mechanistic* questions with the visualizations presented here, -as we have not learned the tools necessary to do that properly just yet. +Recall the different data analysis questions +from the {ref}`intro` chapter. +With the visualizations we will cover in this chapter, +we will be able to answer *only descriptive and exploratory* questions. +Be careful to not answer any *predictive, inferential, causal* +*or mechanistic* questions with the visualizations presented here, +as we have not learned the tools necessary to do that properly just yet. 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` -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 +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`; +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 are as follows: ```{index} visualization; line, visualization; histogram, visualization; scatter, visualization; bar, distribution @@ -106,13 +123,13 @@ alternative. +++ ## Refining the visualization -#### *Convey the message, minimize noise* {-} +#### *Convey the message, minimize noise* Just being able to make a visualization in Python with `altair` (or any other tool for that matter) doesn't mean that it effectively communicates your message to others. Once you have selected a broad type of visualization to use, you will have to refine it to suit your particular need. Some rules of thumb for doing -this are listed below. They generally fall into two classes: you want to +this are listed below. They generally fall into two classes: you want to *make your visualization convey your message*, and you want to *reduce visual noise* as much as possible. Humans have limited cognitive ability to process information; both of these types of refinement aim to reduce the mental load on @@ -126,9 +143,9 @@ understand and remember your message quickly. - Ensure the text, symbols, lines, etc., on your visualization are big enough to be easily read. - Ensure the data are clearly visible; don't hide the shape/distribution of the data behind other objects (e.g., a bar). - Make sure to use color schemes that are understandable by those with - colorblindness (a surprisingly large fraction of the overall + colorblindness (a surprisingly large fraction of the overall population—from about 1% to 10%, depending on sex and ancestry {cite:p}`deebblind`). - For example, [Color Schemes](https://vega.github.io/vega/docs/schemes/) + For example, [Color Schemes](https://altair-viz.github.io/user_guide/customization.html#customizing-colors) provides the ability to pick such color schemes, and you can check your visualizations after you have created them by uploading to online tools such as a [color blindness simulator](https://www.color-blindness.com/coblis-color-blindness-simulator/). @@ -136,7 +153,7 @@ understand and remember your message quickly. **Minimize noise** -- Use colors sparingly. Too many different colors can be distracting, create false patterns, and detract from the message. +- Use colors sparingly. Too many different colors can be distracting, create false patterns, and detract from the message. - Be wary of overplotting. Overplotting is when marks that represent the data overlap, and is problematic as it prevents you from seeing how many data points are represented in areas of the visualization where this occurs. If your @@ -147,14 +164,14 @@ understand and remember your message quickly. +++ -## Creating visualizations with `altair` +## Creating visualizations with `altair` #### *Build the visualization iteratively* ```{index} altair ``` -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` in order to support reading and other data related operations. +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. ```{code-cell} ipython3 import pandas as pd @@ -172,12 +189,12 @@ from myst_nb import glue ```{index} Mauna Loa ``` -The [Mauna Loa CO$_{\text{2}}$ data set](https://www.esrl.noaa.gov/gmd/ccgg/trends/data.html), -curated by Dr. Pieter Tans, NOAA/GML +The [Mauna Loa CO$_{\text{2}}$ data set](https://www.esrl.noaa.gov/gmd/ccgg/trends/data.html), +curated by Dr. Pieter Tans, NOAA/GML and Dr. Ralph Keeling, Scripps Institution of Oceanography, -records the atmospheric concentration of carbon dioxide -(CO$_{\text{2}}$, in parts per million) -at the Mauna Loa research station in Hawaii +records the atmospheric concentration of carbon dioxide +(CO$_{\text{2}}$, in parts per million) +at the Mauna Loa research station in Hawaii from 1959 onward {cite:p}`maunadata`. For this book, we are going to focus on the last 40 years of the data set, 1980-2020. @@ -185,7 +202,7 @@ For this book, we are going to focus on the last 40 years of the data set, ```{index} question; visualization ``` -**Question:** Does the concentration of atmospheric CO$_{\text{2}}$ change over time, +**Question:** Does the concentration of atmospheric CO$_{\text{2}}$ change over time, and are there any interesting patterns to note? ```{code-cell} ipython3 @@ -197,63 +214,75 @@ mauna_loa = mauna_loa[['date_measured', 'ppm']].query('ppm>0 and date_measured>" mauna_loa.to_csv("data/mauna_loa_data.csv", index=False) ``` - - 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']) +co2_df = pd.read_csv( + "data/mauna_loa_data.csv", parse_dates=['date_measured'] +) co2_df ``` ```{code-cell} ipython3 -co2_df.dtypes +co2_df.info() ``` -We see that there are two columns in the `co2_df` data frame; `date_measured` and `ppm`. -The `date_measured` column holds the date the measurement was taken, +We see that there are two columns in the `co2_df` data frame; `date_measured` and `ppm`. +The `date_measured` column holds the date the measurement was taken, and is of type `datetime64`. -The `ppm` column holds the value of CO$_{\text{2}}$ in parts per million -that was measured on each date, and is type `float64`. +The `ppm` column holds the value of CO$_{\text{2}}$ in parts per million +that was measured on each date, and is type `float64`; this is the usual +type for decimal numbers. > **Note:** `read_csv` was able to parse the `date_measured` column into the -> `datetime` vector type because it was entered -> in the international standard date format, -> called ISO 8601, which lists dates as `year-month-day` and we used `parse_dates=True`. -> `datetime` vectors are `double` vectors with special properties that allow +> `datetime` vector type because it was entered +> in the international standard date format, +> called ISO 8601, which lists dates as `year-month-day` and we used `parse_dates=True`. +> `datetime` vectors are `double` vectors with special properties that allow > them to handle dates correctly. -> For example, `datetime` type vectors allow functions like `altair` -> to treat them as numeric dates and not as character vectors, -> even though they contain non-numeric characters +> For example, `datetime` type vectors allow functions like `altair` +> to treat them as numeric dates and not as character vectors, +> even though they contain non-numeric characters > (e.g., in the `date_measured` column in the `co2_df` data frame). -> This means Python will not accidentally plot the dates in the wrong order -> (i.e., not alphanumerically as would happen if it was a character vector). -> More about dates and times can be viewed [here](https://wesmckinney.com/book/time-series.html) - -Since we are investigating a relationship between two variables -(CO$_{\text{2}}$ concentration and date), -a scatter plot is a good place to start. -Scatter plots show the data as individual points with `x` (horizontal axis) +> This means Python will not accidentally plot the dates in the wrong order +> (i.e., not alphanumerically as would happen if it was a character vector). +> More about dates and times can be viewed [here](https://wesmckinney.com/book/time-series.html). + +Since we are investigating a relationship between two variables +(CO$_{\text{2}}$ concentration and date), +a scatter plot is a good place to start. +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. -while using the `altair` package, We create a plot object with the `alt.Chart()` function. +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. 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 ``` - The name of the **data frame** object to visualize. - - Here, we specify the `co2_df` data frame as an argument to the `alt.Chart()` function + - 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 [altair reference](https://altair-viz.github.io/user_guide/marks.html) for a list of geometric objects). + - To create a geometric object, we use `Chart.mark_*` methods (see the + [altair reference](https://altair-viz.github.io/user_guide/marks.html) + for a list of geometric objects). - 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. - - 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 plot `x` axis to the `date_measured` variable, and the plot `y` axis to the `ppm` variable. + - 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. ```{code-cell} ipython3 :tags: ["remove-cell"] @@ -262,10 +291,9 @@ from myst_nb import glue ```{code-cell} ipython3 co2_scatter = alt.Chart(co2_df).mark_point().encode( - x = "date_measured", - y = alt.Y("ppm", scale=alt.Scale(zero=False))) - - + x=alt.X("date_measured"), + y=alt.Y("ppm", scale=alt.Scale(zero=False)) +) ``` ```{code-cell} ipython3 @@ -273,30 +301,27 @@ co2_scatter = alt.Chart(co2_df).mark_point().encode( glue('co2_scatter', co2_scatter, display=False) ``` -:::{glue:figure} co2_scatter -:figwidth: 700px +:::{glue:figure} co2_scatter +:figwidth: 700px :name: co2_scatter Scatter plot of atmospheric concentration of CO$_{2}$ over time. ::: - -> **Note:** We can change the size of the point and color of the plot by specifying `mark_point(size=10, color='black')`. - -Certainly, the visualization in {numref}`co2_scatter` -shows a clear upward trend +The visualization in {numref}`co2_scatter` +shows a clear upward trend in the atmospheric concentration of CO$_{\text{2}}$ over time. -This plot answers the first part of our question in the affirmative, -but that appears to be the only conclusion one can make -from the scatter visualization. +This plot answers the first part of our question in the affirmative, +but that appears to be the only conclusion one can make +from the scatter visualization. One important thing to note about this data is that one of the variables we are exploring is time. -Time is a special kind of quantitative variable -because it forces additional structure on the data—the -data points have a natural order. -Specifically, each observation in the data set has a predecessor -and a successor, and the order of the observations matters; changing their order +Time is a special kind of quantitative variable +because it forces additional structure on the data—the +data points have a natural order. +Specifically, each observation in the data set has a predecessor +and a successor, and the order of the observations matters; changing their order alters their meaning. In situations like this, we typically use a line plot to visualize the data. Line plots connect the sequence of `x` and `y` coordinates @@ -305,27 +330,25 @@ of the observations with line segments, thereby emphasizing their order. ```{index} altair; mark_line ``` -We can create a line plot in `altair` using the `mark_line` function. -Let's now try to visualize the `co2_df` as a line plot -with just the default arguments: +We can create a line plot in `altair` using the `mark_line` function. +Let's now try to visualize the `co2_df` as a line plot +with just the default arguments: ```{code-cell} ipython3 -co2_line = alt.Chart(co2_df).mark_line(color='black').encode( - x = "date_measured", - y = alt.Y("ppm", scale=alt.Scale(zero=False))) - - +co2_line = alt.Chart(co2_df).mark_line().encode( + x=alt.X("date_measured"), + y=alt.Y("ppm", scale=alt.Scale(zero=False)) +) ``` + ```{code-cell} ipython3 :tags: ["remove-cell"] glue('co2_line', co2_line, display=False) ``` - - :::{glue:figure} co2_line -:figwidth: 700px +:figwidth: 700px :name: co2_line Line plot of atmospheric concentration of CO$_{2}$ over time. @@ -342,7 +365,7 @@ be a better choice for answering the question than the scatter plot was. The comparison between these two visualizations also illustrates a common issue with scatter plots: often, the points are shown too close together or even on top of one another, muddling information that would otherwise be clear -(*overplotting*). +(*overplotting*). ```{index} altair; alt.X, altair; alt.Y, altair; configure_axis ``` @@ -352,14 +375,14 @@ to refine things. This plot is fairly straightforward, and there is not much visual noise to remove. But there are a few things we must do to improve clarity, such as adding informative axis labels and making the font a more readable size. To add axis labels, we use the `title` argument along with `alt.X` and `alt.Y` functions. To -change the font size, we use the `configure_axis` function with the `titleFontSize` argument: +change the font size, we use the `configure_axis` function with the +`titleFontSize` argument. ```{code-cell} ipython3 -co2_line_labels = alt.Chart(co2_df).mark_line(color='black').encode( - x = alt.X("date_measured", title = "Year"), - y = alt.Y("ppm", scale=alt.Scale(zero=False), title = "Atmospheric CO2 (ppm)")).configure_axis( - titleFontSize=12) - +co2_line_labels = alt.Chart(co2_df).mark_line().encode( + x=alt.X("date_measured", title="Year"), + y=alt.Y("ppm", scale=alt.Scale(zero=False), title="Atmospheric CO2 (ppm)") +).configure_axis(titleFontSize=12) ``` ```{code-cell} ipython3 @@ -368,13 +391,13 @@ glue('co2_line_labels', co2_line_labels, display=False) ``` :::{glue:figure} co2_line_labels -:figwidth: 700px +:figwidth: 700px :name: co2_line_labels Line plot of atmospheric concentration of CO$_{2}$ over time with clearer axes and labels. ::: -> **Note:** The `configure_` function in `altair` is complex and supports many other functionalities, which can be viewed [here](https://altair-viz.github.io/user_guide/configuration.html) +> **Note:** The `configure_*` function in `altair` supports many other functionalities for customizing visualizations, for example updating the size of the plot, changing the font color, or many other options that can be viewed [here](https://altair-viz.github.io/user_guide/configuration.html). ```{index} altair; alt.Scale ``` @@ -382,28 +405,35 @@ Line plot of atmospheric concentration of CO$_{2}$ over time with clearer axes a Finally, let's see if we can better understand the oscillation by changing the 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*, +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. -In particular, here, we will use the `alt.Scale()` function to zoom in -on just five years of data (say, 1990-1994). -`domain` argument takes a list of length two -to specify the upper and lower bounds to limit the axis. - -```{code-cell} ipython3 - - -co2_line_scale = alt.Chart(co2_df).mark_line(color='black', clip=True).encode( - x=alt.X("date_measured", title="Measurement Date", axis=alt.Axis(tickCount=4), scale=alt.Scale(domain=['1990', '1994'])), - y=alt.Y("ppm", scale=alt.Scale(zero=False), title="Atmospheric CO2 (ppm)") -).configure_axis( - titleFontSize=12 -) - - - - +In particular, here, we will use the `alt.Scale` function to zoom in +on just five years of data (say, 1990-1994). 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 +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']) + ), + y=alt.Y( + "ppm", + scale=alt.Scale(zero=False), + title="Atmospheric CO2 (ppm)" + ) +).configure_axis(titleFontSize=12) ``` ```{code-cell} ipython3 @@ -412,52 +442,55 @@ glue('co2_line_scale', co2_line_scale, display=False) ``` :::{glue:figure} co2_line_scale -:figwidth: 700px +:figwidth: 700px :name: co2_line_scale Line plot of atmospheric concentration of CO$_{2}$ from 1990 to 1994. ::: -Interesting! It seems that each year, the atmospheric CO$_{\text{2}}$ increases until it reaches its peak somewhere around April, decreases until around late September, -and finally increases again until the end of the year. In Hawaii, 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. +Interesting! It seems that each year, the atmospheric CO$_{\text{2}}$ increases +until it reaches its peak somewhere around April, decreases until around late +September, and finally increases again until the end of the year. In Hawaii, +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. -As you might have noticed from the code used to create the final visualization -of the `co2_df` data frame, we used `axis=alt.Axis(tickCount=4)` to add the lines in the background to better visualise and map the values on the axis to the plot. 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 -for our painting by adding primer. -In our data visualization this is akin to calling `alt.Chart` +We start with a blank canvas, +and the first thing we do is prepare the surface +for our painting by adding primer. +In our data visualization this is akin to calling `alt.Chart` and specifying the data set we will be using. -Next, we sketch out the background of the painting. -In our data visualization, +Next, we sketch out the background of the painting. +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, +In our data visualization, this would be the geometric objects (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. + + ### Scatter plots: the Old Faithful eruption time data set ```{index} Old Faithful ``` -The `faithful` data set contains measurements -of the waiting time between eruptions +The `faithful` data set contains measurements +of the waiting time between eruptions and the subsequent eruption duration (in minutes) of the Old Faithful -geyser in Yellowstone National Park, Wyoming, United States. +geyser in Yellowstone National Park, Wyoming, United States. First, we will read the data and then answer the following question: ```{index} question; visualization ``` -**Question:** Is there a relationship between the waiting time before an eruption -and the duration of the eruption? +**Question:** Is there a relationship between the waiting time before an eruption +and the duration of the eruption? ```{code-cell} ipython3 faithful = pd.read_csv("data/faithful.csv") @@ -465,25 +498,25 @@ faithful ``` -Here again, we investigate the relationship between two quantitative variables -(waiting time and eruption time). -But if you look at the output of the data frame, +Here again, we investigate the relationship between two quantitative variables +(waiting time and eruption time). +But if you look at the output of the data frame, you'll notice that unlike time in the Mauna Loa CO$_{\text{2}}$ data set, 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` +package with the `waiting` variable on the horizontal axis, the `eruptions` variable on the vertical axis, and the `mark_point` geometric object. +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(color='black', filled=True).encode( - x = "waiting", - y = "eruptions" +faithful_scatter = alt.Chart(faithful).mark_point(filled=True).encode( + x="waiting", + y="eruptions" ) - ``` ```{code-cell} ipython3 @@ -491,8 +524,8 @@ faithful_scatter = alt.Chart(faithful).mark_point(color='black', filled=True).en glue('faithful_scatter', faithful_scatter, display=False) ``` -:::{glue:figure} faithful_scatter -:figwidth: 700px +:::{glue:figure} faithful_scatter +:figwidth: 700px :name: faithful_scatter Scatter plot of waiting time and eruption time. @@ -502,35 +535,51 @@ We can see in {numref}`faithful_scatter` that the data tend to fall into two groups: one with short waiting and eruption times, and one with long waiting and eruption times. Note that in this case, there is no overplotting: the points are generally nicely visually separated, and the pattern they form -is clear. Also, note that to make the points solid, we used `filled=True` as argument of the `mark_point` function. In place of `mark_point(filled=True)`, we can also use `mark_circle()`. +is clear. In order to refine the visualization, we need only to add axis -labels and make the font more readable: - - +labels and make the font more readable. ```{code-cell} ipython3 -faithful_scatter_labels = alt.Chart(faithful).mark_circle(color='black').encode( - x = alt.X("waiting", title = "Waiting Time (mins)"), - y = alt.Y("eruptions", title = "Eruption Duration (mins)") +faithful_scatter_labels = alt.Chart(faithful).mark_circle().encode( + x=alt.X("waiting", title="Waiting Time (mins)"), + y=alt.Y("eruptions", title="Eruption Duration (mins)") ) - - - ``` - ```{code-cell} ipython3 :tags: ["remove-cell"] -glue('faithful_scatter_labels', faithful_scatter_labels, display=False) +glue("faithful_scatter_labels", faithful_scatter_labels, display=False) ``` :::{glue:figure} faithful_scatter_labels -:figwidth: 700px +:figwidth: 700px :name: faithful_scatter_labels 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( + x=alt.X("waiting", title="Waiting Time (mins)"), + y=alt.Y("eruptions", title="Eruption Duration (mins)") +) +``` + +```{code-cell} ipython3 +:tags: ["remove-cell"] +glue('faithful_scatter_labels_black', faithful_scatter_labels_black, display=False) +``` + +:::{glue:figure} faithful_scatter_labels_black +:figwidth: 700px +:name: faithful_scatter_labels_black + +Scatter plot of waiting time and eruption time with black points. +::: + +++ ### Axis transformation and colored scatter plots: the Canadian languages data set @@ -538,15 +587,15 @@ Scatter plot of waiting time and eruption time with clearer axes and labels. ```{index} Canadian languages ``` -Recall the `can_lang` data set {cite:p}`timbers2020canlang` from Chapters {ref}`intro`, {ref}`reading`, and {ref}`wrangling`, -which contains counts of languages from the 2016 +Recall the `can_lang` data set {cite:p}`timbers2020canlang` from the {ref}`intro`, {ref}`reading`, and {ref}`wrangling` chapters. +It contains counts of languages from the 2016 Canadian census. ```{index} question; visualization ``` **Question:** Is there a relationship between -the percentage of people who speak a language as their mother tongue and +the percentage of people who speak a language as their mother tongue and the percentage for whom that is the primary language spoken at home? And is there a pattern in the strength of this relationship in the higher-level language categories (Official languages, Aboriginal languages, or @@ -555,7 +604,9 @@ non-official and non-Aboriginal languages)? To get started, we will read and inspect the data: ```{code-cell} ipython3 -can_lang = pd.read_csv("data/can_lang.csv") +:tags: ["output_scroll"] +can_lang = pd.read_csv("data/can_lang.csv") +can_lang ``` ```{code-cell} ipython3 @@ -570,11 +621,10 @@ We will begin with a scatter plot of the `mother_tongue` and `most_at_home` colu The resulting plot is shown in {numref}`can_lang_plot` ```{code-cell} ipython3 - -can_lang_plot = alt.Chart(can_lang).mark_circle(color='black').encode( - x = "most_at_home", - y = "mother_tongue") - +can_lang_plot = alt.Chart(can_lang).mark_circle().encode( + x="most_at_home", + y="mother_tongue" +) ``` @@ -584,7 +634,7 @@ glue('can_lang_plot', can_lang_plot, display=False) ``` :::{glue:figure} can_lang_plot -:figwidth: 700px +:figwidth: 700px :name: can_lang_plot Scatter plot of number of Canadians reporting a language as their mother tongue vs the primary language at home @@ -593,21 +643,27 @@ Scatter plot of number of Canadians reporting a language as their mother tongue ```{index} escape character ``` -To make an initial improvement in the interpretability -of {numref}`can_lang_plot`, we should +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 +We should also increase the font size to further improve readability. ```{code-cell} ipython3 -can_lang_plot_labels = alt.Chart(can_lang).mark_circle(color='black').encode( - x = alt.X("most_at_home",title = ["Language spoken most at home", "(number of Canadian residents)"]), - y = alt.Y("mother_tongue", scale=alt.Scale(zero=False), title = ["Mother tongue", "(number of Canadian residents)"])).configure_axis( - titleFontSize=12) - +can_lang_plot_labels = alt.Chart(can_lang).mark_circle().encode( + x=alt.X( + "most_at_home", + title=["Language spoken most at home", "(number of Canadian residents)"] + ), + y=alt.Y( + "mother_tongue", + scale=alt.Scale(zero=False), + title=["Mother tongue", "(number of Canadian residents)"] + ) +).configure_axis(titleFontSize=12) ``` ```{code-cell} ipython3 @@ -616,7 +672,7 @@ glue('can_lang_plot_labels', can_lang_plot_labels, display=False) ``` :::{glue:figure} can_lang_plot_labels -:figwidth: 700px +:figwidth: 700px :name: can_lang_plot_labels Scatter plot of number of Canadians reporting a language as their mother tongue vs the primary language at home with x and y labels. @@ -628,11 +684,11 @@ Scatter plot of number of Canadians reporting a language as their mother tongue ```{code-cell} ipython3 :tags: ["remove-cell"] import numpy as np -numlang_speakers_max = max(can_lang['mother_tongue']) +numlang_speakers_max=int(max(can_lang['mother_tongue'])) print(numlang_speakers_max) -numlang_speakers_min = min(can_lang['mother_tongue']) +numlang_speakers_min = int(min(can_lang['mother_tongue'])) print(numlang_speakers_min) -log_result = np.floor(np.log(numlang_speakers_max/numlang_speakers_min)) +log_result = int(np.floor(np.log10(numlang_speakers_max/numlang_speakers_min))) print(log_result) glue("numlang_speakers_max", numlang_speakers_max) glue("numlang_speakers_min", numlang_speakers_min) @@ -644,11 +700,11 @@ much more readable and interpretable now. However, the scatter points themselves some work; most of the 214 data points are bunched up in the lower left-hand side of the visualization. The data is clumped because many more people in Canada speak English or French (the two points in -the upper right corner) than other languages. -In particular, the most common mother tongue language -has {glue:}`numlang_speakers_max` speakers, +the upper right corner) than other languages. +In particular, the most common mother tongue language +has {glue:}`numlang_speakers_max` speakers, while the least common has only {glue:}`numlang_speakers_min`. -That's a {glue:}`log_result` -decimal-place difference +That's a six-decimal-place difference in the magnitude of these two numbers! We can confirm that the two points in the upper right-hand corner correspond to Canada's two official languages by filtering the data: @@ -657,14 +713,18 @@ to Canada's two official languages by filtering the data: ``` ```{code-cell} ipython3 -can_lang.loc[(can_lang['language']=='English') | (can_lang['language']=='French')] +:tags: ["output_scroll"] +can_lang.loc[ + (can_lang['language']=='English') | + (can_lang['language']=='French') +] ``` ```{index} logarithmic scale, altair; logarithmic scaling ``` Recall that our question about this data pertains to *all* languages; -so to properly answer our question, +so to properly answer our question, we will need to adjust the scale of the axes so that we can clearly see all of the scatter points. In particular, we will improve the plot by adjusting the horizontal @@ -672,23 +732,32 @@ and vertical axes so that they are on a **logarithmic** (or **log**) scale. Log scaling is useful when your data take both *very large* and *very small* values, because it helps space out small values and squishes larger values together. For example, $\log_{10}(1) = 0$, $\log_{10}(10) = 1$, $\log_{10}(100) = 2$, and $\log_{10}(1000) = 3$; -on the logarithmic scale, +on the logarithmic scale, the values 1, 10, 100, and 1000 are all the same distance apart! -So we see that applying this function is moving big values closer together +So we see that applying this function is moving big values closer together and moving small values farther apart. -Note that if your data can take the value 0, logarithmic scaling may not +Note that if your data can take the value 0, logarithmic scaling may not be appropriate (since `log10(0) = -inf` in Python). There are other ways to transform -the data in such a case, but these are beyond the scope of the book. +the data in such a case, but these are beyond the scope of the book. We can accomplish logarithmic scaling in the `altair` visualization using the argument `type="log"` in the scale functions. ```{code-cell} ipython3 -can_lang_plot_log = alt.Chart(can_lang).mark_circle(color='black').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)), - 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) - +can_lang_plot_log = 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) + ), + 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) ``` ```{code-cell} ipython3 @@ -697,7 +766,7 @@ glue('can_lang_plot_log', can_lang_plot_log, display=False) ``` :::{glue:figure} can_lang_plot_log -:figwidth: 700px +: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. @@ -716,16 +785,16 @@ glue("result", result) ``` -Similar to some of the examples in Chapter {ref}`wrangling`, -we can convert the counts to percentages to give them context +Similar to some of the examples in the chapter on {ref}`wrangling`, +we can convert the counts to percentages to give them context and make them easier to understand. -We can do this by dividing the number of people reporting a given language -as their mother tongue or primary language at home -by the number of people who live in Canada and multiplying by 100\%. -For example, -the percentage of people who reported that their mother tongue was English -in the 2016 Canadian census -was {glue:}`english_mother_tongue` / {glue:}`census_popn` $\times$ +We can do this by dividing the number of people reporting a given language +as their mother tongue or primary language at home +by the number of people who live in Canada and multiplying by 100\%. +For example, +the percentage of people who reported that their mother tongue was English +in the 2016 Canadian census +was {glue:}`english_mother_tongue` / {glue:}`census_popn` $\times$ `100` \% = {glue:}`result`\% Below we use `assign` to calculate the percentage of people reporting a given @@ -738,14 +807,16 @@ you can clearly see the mutated output from the table. ``` ```{code-cell} ipython3 -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) +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 +) can_lang[['mother_tongue_percent', 'most_at_home_percent']] ``` Finally, we will edit the visualization to use the percentages we just computed -(and change our axis labels to reflect this change in +(and change our axis labels to reflect this change in units). {numref}`can_lang_plot_percent` displays the final result. @@ -753,11 +824,20 @@ the final result. ```{code-cell} ipython3 -can_lang_plot_percent = alt.Chart(can_lang).mark_circle(color='black').encode( - x = alt.X("most_at_home_percent",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_percent", title = ["Mother tongue", "(number of Canadian residents)"], scale=alt.Scale(type="log"), axis=alt.Axis(tickCount=7))).configure_axis( - titleFontSize=12) - +can_lang_plot_percent = alt.Chart(can_lang).mark_circle().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) + ) +).configure_axis(titleFontSize=12) ``` ```{code-cell} ipython3 @@ -766,7 +846,7 @@ glue('can_lang_plot_percent', can_lang_plot_percent, display=False) ``` :::{glue:figure} can_lang_plot_percent -:figwidth: 700px +:figwidth: 700px :name: can_lang_plot_percent Scatter plot of percentage of Canadians reporting a language as their mother tongue vs the primary language at home. @@ -774,46 +854,46 @@ Scatter plot of percentage of Canadians reporting a language as their mother ton {numref}`can_lang_plot_percent` is the appropriate visualization to use to answer the first question in this section, i.e., -whether there is a relationship between the percentage of people who speak +whether there is a relationship between the percentage of people who speak a language as their mother tongue and the percentage for whom that is the primary language spoken at home. To fully answer the question, we need to use {numref}`can_lang_plot_percent` -to assess a few key characteristics of the data: +to assess a few key characteristics of the data: ```{index} relationship; positive negative none ``` -- **Direction:** if the y variable tends to increase when the x variable increases, then y has a **positive** relationship with x. If - y tends to decrease when x increases, then y has a **negative** relationship with x. If y does not meaningfully increase or decrease - as x increases, then y has **little or no** relationship with x. +- **Direction:** if the y variable tends to increase when the x variable increases, then y has a **positive** relationship with x. If + y tends to decrease when x increases, then y has a **negative** relationship with x. If y does not meaningfully increase or decrease + as x increases, then y has **little or no** relationship with x. ```{index} relationship; strong weak ``` - **Strength:** if the y variable *reliably* increases, decreases, or stays flat as x increases, - then the relationship is **strong**. Otherwise, the relationship is **weak**. Intuitively, + then the relationship is **strong**. Otherwise, the relationship is **weak**. Intuitively, the relationship is strong when the scatter points are close together and look more like a "line" or "curve" than a "cloud." ```{index} relationship; linear nonlinear ``` -- **Shape:** if you can draw a straight line roughly through the data points, the relationship is **linear**. Otherwise, it is **nonlinear**. +- **Shape:** if you can draw a straight line roughly through the data points, the relationship is **linear**. Otherwise, it is **nonlinear**. -In {numref}`can_lang_plot_percent`, we see that -as the percentage of people who have a language as their mother tongue increases, -so does the percentage of people who speak that language at home. +In {numref}`can_lang_plot_percent`, we see that +as the percentage of people who have a language as their mother tongue increases, +so does the percentage of people who speak that language at home. Therefore, there is a **positive** relationship between these two variables. Furthermore, because the points in {numref}`can_lang_plot_percent` are fairly close together, and the points look more like a "line" than a "cloud", -we can say that this is a **strong** relationship. -And finally, because drawing a straight line through these points in +we can say that this is a **strong** relationship. +And finally, because drawing a straight line through these points in {numref}`can_lang_plot_percent` would fit the pattern we observe quite well, we say that the relationship is **linear**. Onto the second part of our exploratory data analysis question! -Recall that we are interested in knowing whether the strength -of the relationship we uncovered +Recall that we are interested in knowing whether the strength +of the relationship we uncovered in {numref}`can_lang_plot_percent` depends on the higher-level language category (Official languages, Aboriginal languages, and non-official, non-Aboriginal languages). @@ -821,24 +901,34 @@ One common way to explore this is to color the data points on the scatter plot we have already created by group. For example, given that we have the higher-level language category for each language recorded in the 2016 Canadian census, we can color the points in -our previous +our previous scatter plot to represent each language's higher-level language category. Here we want to distinguish the values according to the `category` group with which they belong. We can add the argument `color` to the `encode` function, specifying 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. +plot. ```{code-cell} ipython3 -can_lang_plot_category = alt.Chart(can_lang).mark_circle().encode( - x = alt.X("most_at_home_percent", 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_percent", title = ["Mother tongue", "(number of Canadian residents)"], scale=alt.Scale(type="log"), axis=alt.Axis(tickCount=7)), - color = "category").configure_axis( - titleFontSize=12) +can_lang_plot_category=alt.Chart(can_lang).mark_circle().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="category" +).configure_axis(titleFontSize=12) ``` @@ -848,37 +938,50 @@ glue('can_lang_plot_category', can_lang_plot_category, display=False) ``` :::{glue:figure} can_lang_plot_category -:figwidth: 700px +:figwidth: 700px :name: can_lang_plot_category Scatter plot of percentage of Canadians reporting a language as their mother tongue vs the primary language at home colored by language category. ::: -The legend in {numref}`can_lang_plot_category` -takes up valuable plot area. -We can improve this by moving the legend title using the `alt.Legend` function +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 won't work -not work well for this particular visualization -because the legend labels are quite long -and would run off the page if displayed this way. +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. ```{code-cell} ipython3 can_lang_plot_legend = alt.Chart(can_lang).mark_circle().encode( - x = alt.X("most_at_home_percent",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_percent", title = ["Mother tongue", "(number 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'))).configure_axis( - titleFontSize=12) - - + 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", + legend=alt.Legend( + orient='none', + legendX=0, + legendY=-90, + direction='vertical' + ) + ) +).configure_axis(titleFontSize=12) ``` ```{code-cell} ipython3 @@ -887,49 +990,59 @@ glue('can_lang_plot_legend', can_lang_plot_legend, display=False) ``` :::{glue:figure} can_lang_plot_legend -:figwidth: 700px +:figwidth: 700px :name: can_lang_plot_legend Scatter plot of percentage of Canadians reporting a language as their mother tongue vs the primary language at home colored by language category with the legend edited. ::: In {numref}`can_lang_plot_legend`, the points are colored with -the default `altair` color palette. But what if you want to use different -colors? In Altair, there are many themes available, which can be viewed [here](https://vega.github.io/vega/docs/schemes/) - -To change the color scheme, +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. ```{index} color palette; color blindness simulator ``` -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. - 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 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: We cannot use different shapes with `mark_circle`, it can only be used with `mark_point` +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 can_lang_plot_theme = alt.Chart(can_lang).mark_point(filled=True).encode( - x = alt.X("most_at_home_percent",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_percent", 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'), - scale=alt.Scale(scheme='dark2')), - shape = "category").configure_axis( - titleFontSize=12) - + 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", + legend=alt.Legend( + orient='none', + legendX=0, + legendY=-90, + direction='vertical' + ), + scale=alt.Scale(scheme='dark2') + ), + shape="category" +).configure_axis(titleFontSize=12) ``` ```{code-cell} ipython3 @@ -938,26 +1051,26 @@ glue('can_lang_plot_theme', can_lang_plot_theme, display=False) ``` :::{glue:figure} can_lang_plot_theme -:figwidth: 700px +:figwidth: 700px :name: can_lang_plot_theme 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`, -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? +From the visualization in {numref}`can_lang_plot_theme`, +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? +at home across the higher-level language categories? Based on {numref}`can_lang_plot_theme`, 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 -the percentage of people who speak a language as their mother tongue -and the percentage who speak it as their primary language at home. -The relationship looks similar regardless of the category. +For each higher-level language category, +there appears to be a strong, positive, and linear relationship between +the percentage of people who speak a language as their mother tongue +and the percentage who speak it as their primary language at home. +The relationship looks similar regardless of the category. Does this mean that this relationship is positive for all languages in the world? And further, can we use this data visualization on its own to predict how many people @@ -966,15 +1079,15 @@ it as their primary language at home? The answer to both these questions is "no!" However, with exploratory data analysis, we can create new hypotheses, ideas, and questions (like the ones at the beginning of this paragraph). Answering those questions often involves doing more complex analyses, and sometimes -even gathering additional data. We will see more of such complex analyses later on in -this book. +even gathering additional data. We will see more of such complex analyses later on in +this book. ### Bar plots: the island landmass data set ```{index} Island landmasses ``` -The `islands.csv` data set contains a list of Earth's landmasses as well as their area (in thousands of square miles) {cite:p}`islandsdata`. +The `islands.csv` data set contains a list of Earth's landmasses as well as their area (in thousands of square miles) {cite:p}`islandsdata`. ```{index} question; visualization ``` @@ -984,14 +1097,15 @@ The `islands.csv` data set contains a list of Earth's landmasses as well as thei To get started, we will read and inspect the data: ```{code-cell} ipython3 +:tags: ["output_scroll"] islands_df = pd.read_csv("data/islands.csv") islands_df ``` -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 +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). They are particularly useful for comparing summary statistics between different groups of a categorical variable. @@ -1000,13 +1114,13 @@ groups of a categorical variable. ``` We specify that we would like to use a bar plot -via the `mark_bar` function in `altair`. -The result is shown in {numref}`islands_bar` +via the `mark_bar` function in `altair`. +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" +) ``` ```{code-cell} ipython3 @@ -1015,7 +1129,7 @@ glue('islands_bar', islands_bar, display=False) ``` :::{glue:figure} islands_bar -:figwidth: 700px +:figwidth: 400px :name: islands_bar Bar plot of all Earth's landmasses' size with squished labels. @@ -1024,59 +1138,56 @@ Bar plot of all Earth's landmasses' size with squished labels. Alright, not bad! The plot in {numref}`islands_bar` is definitely the right kind of visualization, as we can clearly see and compare sizes of landmasses. The major issues are that the smaller landmasses' sizes -are hard to distinguish, and the names of the landmasses are tilted by default to fit in the labels. But remember that the +are hard to distinguish, and the plot is so wide that we can't compare them all! But remember that the 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 `sort_values` function followed by the `iloc` property. Then to help us make sure the labels have enough +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: +swapping the `x` and `y` variables. -```{index} pandas.DataFrame; sort_values, pandas.DataFrame; iloc[] +```{index} pandas.DataFrame; nlargest ``` ```{code-cell} ipython3 -islands_top12 = islands_df.sort_values(by = "size", ascending=False).iloc[:12] +islands_top12 = islands_df.nlargest(12, "size") -islands_bar_sorted = alt.Chart(islands_top12).mark_bar().encode( - x = "size", y = "landmass") +islands_bar_top = alt.Chart(islands_top12).mark_bar().encode( + x="size", y="landmass" +) ``` ```{code-cell} ipython3 :tags: ["remove-cell"] -glue('islands_bar_sorted', islands_bar_sorted, display=True) +glue('islands_bar_top', islands_bar_top, display=True) ``` -:::{glue:figure} islands_bar_sorted -:figwidth: 700px -:name: islands_bar_sorted +:::{glue:figure} islands_bar_top +:figwidth: 700px +:name: islands_bar_top Bar plot of size for Earth's largest 12 landmasses. ::: - - -The plot in {numref}`islands_bar_sorted` is definitely clearer now, -and allows us to answer our question -("are the top 7 largest landmasses continents?") in the affirmative. -But the question could be made clearer from the plot +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, +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` -To organize the landmasses by their `size` variable, +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 -in the ascending order of `x` axis values. - +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. - -> **Note:** If we want to sort the values on `y-axis` in descending order of `x-axis`, -> we need to specify `sort='-x'`. +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'`. ```{index} altair; sort ``` @@ -1085,16 +1196,16 @@ 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 +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(color='black').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) +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) ``` ```{code-cell} ipython3 @@ -1103,7 +1214,7 @@ glue('islands_plot_sorted', islands_plot_sorted, display=True) ``` :::{glue:figure} islands_plot_sorted -:figwidth: 700px +:figwidth: 700px :name: islands_plot_sorted Bar plot of size for Earth's largest 12 landmasses colored by whether its a continent with clearer axes and labels. @@ -1114,166 +1225,202 @@ The plot in {numref}`islands_plot_sorted` is now a very 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 +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( + 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) +``` + +```{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 ``` -The `morley` data set -contains measurements of the speed of light +The `morley` data set +contains measurements of the speed of light collected in experiments performed in 1879. -Five experiments were performed, -and in each experiment, 20 runs were performed—meaning that -20 measurements of the speed of light were collected +Five experiments were performed, +and in each experiment, 20 runs were performed—meaning that +20 measurements of the speed of light were collected in each experiment {cite:p}`lightdata`. - -Because the speed of light is a very large number +Because the speed of light is a very large number (the true value is 299,792.458 km/sec), the data is coded to be the measured speed of light minus 299,000. This coding allows us to focus on the variations in the measurements, which are generally much smaller than 299,000. If we used the full large speed measurements, the variations in the measurements would not be noticeable, making it difficult to study the differences between the experiments. -Note that we convert the `morley` data to a tibble to take advantage of the nicer print output -these specialized data frames provide. ```{index} question; visualization ``` -**Question:** Given what we know now about the speed of +**Question:** Given what we know now about the speed of light (299,792.458 kilometres per second), how accurate were each of the experiments? First, we read in the data. ```{code-cell} ipython3 morley_df = pd.read_csv("data/morley.csv") +morley_df ``` ```{index} distribution, altair; histogram ``` -In this experimental data, -Michelson was trying to measure just a single quantitative number -(the speed of light). -The data set contains many measurements of this single quantity. -To tell how accurate the experiments were, -we need to visualize the distribution of the measurements -(i.e., all their possible values and how often each occurs). -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, -and then using vertical bars to show how many data points fell in each bin. +In this experimental data, +Michelson was trying to measure just a single quantitative number +(the speed of light). +The data set contains many measurements of this single quantity. +To tell how accurate the experiments were, +we need to visualize the distribution of the measurements +(i.e., all their possible values and how often each occurs). +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, +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()`. As usual, +object, 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. ```{code-cell} ipython3 morley_hist = alt.Chart(morley_df).mark_bar().encode( - x = alt.X("Speed"), - y='count()') + x=alt.X("Speed"), + y=alt.Y("count()") +) ``` ```{code-cell} ipython3 :tags: ["remove-cell"] -glue('morley_hist', morley_hist, display=False) +glue("morley_hist", morley_hist, display=False) ``` :::{glue:figure} morley_hist -:figwidth: 700px +:figwidth: 700px :name: morley_hist Histogram of Michelson's speed of light data. ::: -```{index} altair; mark_rule +#### Adding layers to an `altair` plot object + +```{index} altair; +; mark_rule ``` -{numref}`morley_hist` is a great start. -However, -we cannot tell how accurate the measurements are using this visualization +{numref}`morley_hist` is a great start. +However, +we cannot tell how accurate the measurements are using this visualization unless we can see the true value. -In order to visualize the true speed of light, +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 creating a dataframe with just one column with value `792.458`, which is the true value of light speed -minus 299,000 and encoding it in the `x` axis; this ensures it is coded the same way as the -measurements in the `morley` data frame. -We would also like to fine tune this vertical line, -styling it so that it is dashed and 1 point in thickness. -A point is a measurement unit commonly used with fonts, -and 1 point is about 0.353 mm. -We do this by setting `strokeDash=[3,3]` and `size = 1`, respectively. - -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 add the dashed line on top of the histogram, we will use the `+` operator. This concept is also known as layering in altair.(This is covered in the later sections of the chapter). Here, we add the `mark_rule` chart on the `morley_hist` chart of the form `mark_bar` +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. +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 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 +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 +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. ```{code-cell} ipython3 -v_line = alt.Chart(pd.DataFrame({'x': [792.458]})).mark_rule( - strokeDash=[3,3], size=1).encode( - x='x') - - -final_plot = morley_hist + v_line +v_line = alt.Chart().mark_rule(strokeDash=[3]).encode( + x=alt.datum(792.458) +) +morley_hist_line = morley_hist + v_line ``` ```{code-cell} ipython3 :tags: ["remove-cell"] -glue('final_plot_viz', final_plot, display=False) +glue("morley_hist_line", morley_hist_line, display=False) ``` -:::{glue:figure} final_plot_viz -:figwidth: 700px -:name: final_plot_viz +:::{glue:figure} morley_hist_line +:figwidth: 700px +:name: morley_hist_line Histogram of Michelson's speed of light data with vertical line indicating true speed of light. ::: -In {numref}`final_plot_viz`, -we still cannot tell which experiments (denoted in the `Expt` column) -led to which measurements; -perhaps some experiments were more accurate than others. -To fully answer our question, -we need to separate the measurements from each other visually. -We can try to do this using a *colored* histogram, -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. +In {numref}`morley_hist_line`, +we still cannot tell which experiments (denoted by the `Expt` column) +led to which measurements; +perhaps some experiments were more accurate than others. +To fully answer our question, +we need to separate the measurements from each other visually. +We can try to do this using a *colored* histogram, +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 = alt.X("Speed"), - y=alt.Y('count()'), - color = "Expt") + x=alt.X("Speed"), + y=alt.Y("count()"), + color=alt.Color("Expt") +) -final_plot_colored = morley_hist_colored + v_line +morley_hist_colored = morley_hist_colored + v_line ``` ```{code-cell} ipython3 :tags: ["remove-cell"] -glue('final_plot_colored', final_plot_colored, display=True) +glue('morley_hist_colored', morley_hist_colored, display=True) ``` -:::{glue:figure} final_plot_colored -:figwidth: 700px -:name: final_plot_colored +:::{glue:figure} morley_hist_colored +:figwidth: 700px +:name: morley_hist_colored Histogram of Michelson's speed of light data colored by experiment. ::: @@ -1281,48 +1428,52 @@ Histogram of Michelson's speed of light data colored by experiment. ```{index} integer ``` -Alright great, {numref}`final_plot_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 -recall from Chapter {ref}`wrangling`, the *data type* you use for each variable +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 +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 -is currently an *integer*. But we want to treat it as a -*category*, i.e., there should be one category per type of experiment. +is currently an *integer*---specifically, an `int64` type. But we want to treat it as a +*category*, i.e., there should be one category per type of experiment. +```{code-cell} ipython3 +morley_df.info() +``` ```{index} nominal, altair; :N ``` -To fix this issue we can convert the `Expt` variable into a `nominal`(categorical) type -variable by adding a suffix `:N`(where `N` stands for nominal type variable) with the `Expt` variable. -By doing this, we are ensuring that `altair` will treat this variable as a categorical variable, -and the color will be mapped discretely. Here, we also mention `stack=False`, so that the bars are not stacked on top of each other. +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. +We also specify the `stack=False` argument in the `y` encoding so +that the bars are not stacked on top of each other. ```{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)), - y=alt.Y('count()', stack=False), - color = "Expt:N") - -final_plot_categorical = morley_hist_categorical + v_line + x=alt.X("Speed", bin=alt.Bin(maxbins=50)), + y=alt.Y("count()", stack=False), + color=alt.Color("Expt:N") +) +morley_hist_categorical = morley_hist_categorical + v_line ``` ```{code-cell} ipython3 :tags: ["remove-cell"] -glue('final_plot_categorical', final_plot_categorical, display=True) +glue('morley_hist_categorical', morley_hist_categorical, display=True) ``` -:::{glue:figure} final_plot_categorical -:figwidth: 700px -:name: final_plot_categorical +:::{glue:figure} morley_hist_categorical +:figwidth: 700px +:name: morley_hist_categorical Histogram of Michelson's speed of light data colored by experiment as a categorical variable. ::: - - Unfortunately, the attempt to separate out the experiment number visually has -created a bit of a mess. All of the colors in {numref}`final_plot_categorical` are blending together, and although it is +created a bit of a mess. All of the colors in {numref}`morley_hist_categorical` are blending together, and although it is possible to derive *some* insight from this (e.g., experiments 1 and 3 had some of the most incorrect measurements), it isn't the clearest way to convey our message and answer the question. Let's try a different strategy of creating @@ -1333,43 +1484,52 @@ grid of separate histogram plots. ```{index} altair; facet ``` -We use the `facet` function to create a plot +We use the `facet` function to create a plot that has multiple subplots arranged in a grid. -The argument to `facet` specifies the variable(s) used to split the plot -into subplots, and how to split them (i.e., into rows or columns). -If the plot is to be split horizontally, into rows, -then the `rows` argument is used. -If the plot is to be split vertically, into columns, -then the `columns` argument is used. -Both the `rows` and `columns` arguments take the column names on which to split the data when creating the subplots. - -```{code-cell} ipython3 - -morley_hist = alt.Chart(morley_df).mark_bar(opacity = 0.5).encode( - x = alt.X("Speed", bin=alt.Bin(maxbins=50)), - y=alt.Y('count()', stack=False), - color = "Expt:N").properties(height=100, width=300) - -final_plot_facet = (morley_hist + v_line).facet(row = 'Expt:N', data = morley_df) - +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. + +```{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( + "Expt", + data=morley_df, + columns=1 +) ``` ```{code-cell} ipython3 :tags: ["remove-cell"] -glue('final_plot_facet', final_plot_facet, display=True) +glue('morley_hist_facet', morley_hist_facet, display=True) ``` -:::{glue:figure} final_plot_facet -:figwidth: 700px -:name: final_plot_facet +:::{glue:figure} morley_hist_facet +:figwidth: 700px +:name: morley_hist_facet Histogram of Michelson's speed of light data split vertically by experiment. ::: -The visualization in {numref}`final_plot_facet` -now makes it quite clear how accurate the different experiments were -with respect to one another. -The most variable measurements came from Experiment 1. +The visualization in {numref}`morley_hist_facet` +now makes it quite 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. @@ -1378,76 +1538,104 @@ The most different experiments still obtained quite similar results! ```{index} altair; alt.X, altair; alt.Y, altair; configure_axis ``` -There are two finishing touches to make this visualization even clearer. First and foremost, we need to add informative axis labels -using the `alt.X` and `alt.Y` function, and increase the font size to make it readable using the `configure_axis` function. Second, and perhaps more 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: +There are three finishing touches to make this visualization even clearer. +First and foremost, we need to add informative axis labels using the `alt.X` +and `alt.Y` function, and increase the font size to make it readable using the +`configure_axis` function. We can also add a title; for a `facet` plot, this is +done by providing the `title` to the facet function. Finally, and perhaps most +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. ```{code-cell} ipython3 morley_rel = morley_df -morley_rel = morley_rel.assign(relative_accuracy = 100 * - ((299000 + morley_df['Speed']) - 299792.458) / (299792.458) ) +morley_rel = morley_rel.assign( + relative_accuracy=( + 100 *((299000 + morley_df["Speed"]) - 299792.458) / (299792.458) + ) +) morley_rel ``` ```{code-cell} ipython3 -v_line = alt.Chart(pd.DataFrame({'x': [0]})).mark_rule( - strokeDash=[3,3], size=2).encode( - x='x') -morley_hist = alt.Chart().mark_bar(opacity=0.6).encode( - x = alt.X("relative_accuracy", bin=alt.Bin(maxbins=120), title = "Relative Accuracy (%)"), - y=alt.Y('count()', stack=False, title = "# Measurements"), - color = alt.Color("Expt:N", title = "Experiment ID")).properties(height=100, width= 400) +v_line = alt.Chart().mark_rule( + strokeDash=[3]).encode( + x=alt.datum(0) +) -final_plot_relative = (morley_hist + v_line).facet(row='Expt:N', data=morley_rel) +morley_hist = alt.Chart().mark_bar(opacity=0.6).encode( + x=alt.X( + "relative_accuracy", + bin=alt.Bin(maxbins=120), + title="Relative Accuracy (%)" + ), + y=alt.Y( + "count()", + stack=False, + title="# Measurements" + ), + color=alt.Color( + "Expt:N", + title="Experiment ID" + ) +).properties(height=100, width=400) + +morley_hist_relative = (morley_hist + v_line).facet( + "Expt", + data=morley_rel, + columns=1, + title="Histogram of relative accuracy of Michelson’s speed of light data" +) ``` ```{code-cell} ipython3 :tags: ["remove-cell"] -glue('final_plot_relative', final_plot_relative, display=True) +glue("morley_hist_relative", morley_hist_relative, display=True) ``` -:::{glue:figure} final_plot_relative -:figwidth: 700px -:name: final_plot_relative +:::{glue:figure} morley_hist_relative +:figwidth: 700px +:name: morley_hist_relative Histogram of relative accuracy split vertically by experiment with clearer axes and labels ::: -Wow, impressive! These measurements of the speed of light from 1879 had errors around *0.05%* of the true speed. {numref}`final_plot_relative` shows you that even though experiments 2 and 5 were perhaps the most accurate, all of the experiments did quite an -admirable job given the technology available at the time. +Wow, impressive! These measurements of the speed of light from 1879 had errors +around *0.05%* of the true speed. {numref}`morley_hist_relative` shows you that +even though experiments 2 and 5 were perhaps the most accurate, all of the +experiments did quite an admirable job given the technology available at the time. #### Choosing a binwidth for histograms -When you create a histogram in `altair`, the default number of bins used is 30. +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. - -But what number of bins is the right one to use? +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 -the one that *helps you answer the question* you asked. -Choosing the correct setting for your problem +or width is. It depends entirely on your problem; the *right* number of bins +or bin width is +the one that *helps you answer the question* you asked. +Choosing the correct setting for your problem is something that commonly takes iteration. - It's usually a good idea to try out several `maxbins` to see which one most clearly captures your data in the context of the question you want to answer. -To get a sense for how different bin affect visualizations, +To get a sense for how different bin affect visualizations, let's experiment with the histogram that we have been working on in this section. -In {numref}`final_plot_max_bins`, -we compare the default setting with three other histograms where we set the +In {numref}`morley_hist_max_bins`, +we compare the default setting with three other histograms where we set the `maxbins` to 200, 70 and 5. -In this case, we can see that both the default number of bins -and the `maxbins=70` of are effective for helping answer our question. +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. @@ -1456,117 +1644,94 @@ On the other hand, the `maxbins=200` and `maxbins=5` are too small and too big, ```{code-cell} ipython3 :tags: ["remove-cell"] -morley_hist_default = alt.Chart(morley_rel).mark_bar(opacity=0.6).encode( - x = alt.X("relative_accuracy", title = "Relative Accuracy (%)"), - y=alt.Y('count()', stack=False, title = "# Measurements"), - color = alt.Color("Expt:N", title = "Experiment ID")).properties(height=100, width=400) - -morley_hist_200 = alt.Chart(morley_rel).mark_bar(opacity=0.6).encode( - x = alt.X("relative_accuracy", bin=alt.Bin(maxbins=200), title = "Relative Accuracy (%)"), - y=alt.Y('count()', stack=False, title = "# Measurements"), - color = alt.Color("Expt:N", title = "Experiment ID")).properties(height=100, width=400) -morley_hist_70 = alt.Chart(morley_rel).mark_bar(opacity=0.6).encode( - x = alt.X("relative_accuracy", bin=alt.Bin(maxbins=70), title = "Relative Accuracy (%)"), - y=alt.Y('count()', stack=False, title = "# Measurements"), - color = alt.Color("Expt:N", title = "Experiment ID")).properties(height=100, width=400) - -morley_hist_5 = alt.Chart(morley_rel).mark_bar(opacity=0.6).encode( - x = alt.X("relative_accuracy", bin=alt.Bin(maxbins=5), title = "Relative Accuracy (%)"), - y=alt.Y('count()', stack=False, title = "# Measurements"), - color = alt.Color("Expt:N", title = "Experiment ID")).properties(height=100, width=300) - - - - - -final_plot_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 = alt.Chart().mark_bar(opacity=0.9).encode( + x=alt.X( + "relative_accuracy", + title="Relative Accuracy (%)" + ), + 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( + "relative_accuracy", + bin=alt.Bin(maxbins=200), + title="Relative Accuracy (%)" + ), + 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( + x=alt.X( + "relative_accuracy", + bin=alt.Bin(maxbins=70), + title="Relative Accuracy (%)" + ), + y=alt.Y( + "count()", + stack=False, + title="# Measurements" + ), + color=alt.Color( + "Expt:N", + title="Experiment ID" + ) +).properties(height=100, width=200) + +morley_hist_5 = alt.Chart().mark_bar(opacity=0.9).encode( + x=alt.X( + "relative_accuracy", + bin=alt.Bin(maxbins=5), + title="Relative Accuracy (%)" + ), + y=alt.Y( + "count()", + stack=False, + title="# Measurements" + ), + 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_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") +)) ``` ```{code-cell} ipython3 :tags: ["remove-cell"] -glue('final_plot_max_bins', final_plot_max_bins, display=True) +glue("morley_hist_max_bins", morley_hist_max_bins, display=True) ``` -:::{glue:figure} final_plot_max_bins -:figwidth: 700px -:name: final_plot_max_bins +:::{glue:figure} morley_hist_max_bins +:figwidth: 700px +:name: morley_hist_max_bins Effect of varying number of max bins on histograms. ::: -#### Adding layers to a `altair` plot object {-} - -```{index} altair; + -``` - -One of the powerful features of `altair` is that 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 -using the assignment symbol (`=`), you can -add to it using the `+` operator. -For example, if we wanted to add a vertical line to the last plot we created (`morley_hist`), -we can use the `+` operator to add a vertical line chart layer with the `mark_rule` function. -The result is shown in {numref}`morley_hist_layer`. - -```{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 = "Expt:N") - -v_line = alt.Chart(pd.DataFrame({'x': [792.458]})).mark_rule( - strokeDash=[3,3], size=1).encode( - x='x') -morley_hist_layer = morley_hist_colored + v_line - -``` - -```{code-cell} ipython3 -:tags: ["remove-cell"] -glue('morley_hist_layer', morley_hist_layer, display=True) -``` - -:::{glue:figure} morley_hist_layer -:figwidth: 700px -:name: morley_hist_layer - -Histogram of Michelson's speed of light data colored by experiment with layered vertical line. -::: - - -We can also add a title to the chart by specifying `title` argument in the `alt.Chart` function - - -```{code-cell} ipython3 -morley_hist_title = alt.Chart(morley_df, title = "Histogram of Michelson's speed of light data colored by experiment").mark_bar(opacity=0.5).encode( - x = alt.X("Speed"), - y=alt.Y('count()'), - color = "Expt:N") - - -``` -```{code-cell} ipython3 -:tags: ["remove-cell"] -glue('morley_hist_title', morley_hist_title, display=True) -``` - -:::{glue:figure} morley_hist_title -:figwidth: 700px -:name: morley_hist_title - -Histogram of Michelson's speed of light data colored with title -::: - - -> **Note:** Good visualization titles clearly communicate -> the take home message to the audience. Typically, -> that is the answer to the question you posed before making the visualization. - ## Explaining the visualization -#### *Tell a story* {-} +#### *Tell a story* Typically, your visualization will not be shown entirely on its own, but rather it will be part of a larger presentation. Further, visualizations can provide @@ -1575,24 +1740,24 @@ conclusion. For example, you could use an exploratory visualization in the opening of the presentation to motivate your choice of a more detailed data analysis / model, a visualization of the results of your analysis to show what your analysis has uncovered, or even one at the end of a presentation to help -suggest directions for future work. +suggest directions for future work. ```{index} visualization; explanation ``` Regardless of where it appears, a good way to discuss your visualization is as -a story: +a story: -1) Establish the setting and scope, and describe why you did what you did. +1) Establish the setting and scope, and describe why you did what you did. 2) Pose the question that your visualization answers. Justify why the question is important to answer. -3) Answer the question using your visualization. Make sure you describe *all* aspects of the visualization (including describing the axes). But you +3) Answer the question using your visualization. Make sure you describe *all* aspects of the visualization (including describing the axes). But you can emphasize different aspects based on what is important to answer your question: - **trends (lines):** Does a line describe the trend well? If so, the trend is *linear*, and if not, the trend is *nonlinear*. Is the trend increasing, decreasing, or neither? Is there a periodic oscillation (wiggle) in the trend? Is the trend noisy (does the line "jump around" a lot) or smooth? - **distributions (scatters, histograms):** How spread out are the data? Where are they centered, roughly? Are there any obvious "clusters" or "subgroups", which would be visible as multiple bumps in the histogram? - **distributions of two variables (scatters):** Is there a clear / strong relationship between the variables (points fall in a distinct pattern), a weak one (points fall in a pattern but there is some noise), or no discernible relationship (the data are too noisy to make any conclusion)? - - **amounts (bars):** How large are the bars relative to one another? Are there patterns in different groups of bars? + - **amounts (bars):** How large are the bars relative to one another? Are there patterns in different groups of bars? 4) Summarize your findings, and use them to motivate whatever you will discuss next. Below are two examples of how one might take these four steps in describing the example visualizations that appeared earlier in this chapter. @@ -1601,7 +1766,7 @@ Each of the steps is denoted by its numeral in parentheses, e.g. (3). ```{index} Mauna Loa ``` -**Mauna Loa Atmospheric CO$_{\text{2}}$ Measurements:** (1) Many +**Mauna Loa Atmospheric CO$_{\text{2}}$ Measurements:** (1) Many current forms of energy generation and conversion—from automotive engines to natural gas power plants—rely on burning fossil fuels and produce greenhouse gases, typically primarily carbon dioxide (CO$_{\text{2}}$), as a @@ -1639,7 +1804,7 @@ result. (4) It would be worth further investigating the differences between these experiments to see why they produced different results. ## Saving the visualization -#### *Choose the right output format for your needs* {-} +#### *Choose the right output format for your needs* ```{index} see: bitmap; raster graphics ``` @@ -1653,7 +1818,7 @@ such as file size/type limitations (e.g., if you are submitting your visualization as part of a conference paper or to a poster printing shop) and where it will be displayed (e.g., online, in a paper, on a poster, on a billboard, in talk slides). Generally speaking, images come in two flavors: -*raster* formats +*raster* formats and *vector* formats. ```{index} raster graphics; file types @@ -1667,21 +1832,22 @@ is not noticeable. *Lossless* formats, on the other hand, allow a perfect display of the original image. - *Common file types:* - + - [JPEG](https://en.wikipedia.org/wiki/JPEG) (`.jpg`, `.jpeg`): lossy, usually used for photographs - [PNG](https://en.wikipedia.org/wiki/Portable_Network_Graphics) (`.png`): lossless, usually used for plots / line drawings - + - [BMP](https://en.wikipedia.org/wiki/BMP_file_format) (`.bmp`): lossless, raw image data, no compression (rarely used) + - [TIFF](https://en.wikipedia.org/wiki/TIFF) (`.tif`, `.tiff`): typically lossless, no compression, used mostly in graphic arts, publishing - *Open-source software:* [GIMP](https://www.gimp.org/) ```{index} vector graphics; file types ``` -**Vector** images are represented as a collection of mathematical -objects (lines, surfaces, shapes, curves). When the computer displays the image, it +**Vector** images are represented as a collection of mathematical +objects (lines, surfaces, shapes, curves). When the computer displays the image, it redraws all of the elements using their mathematical formulas. - *Common file types:* - [SVG](https://en.wikipedia.org/wiki/Scalable_Vector_Graphics) (`.svg`): general-purpose use - + - [EPS](https://en.wikipedia.org/wiki/Encapsulated_PostScript) (`.eps`), general-purpose use (rarely used) - *Open-source software:* [Inkscape](https://inkscape.org/) Raster and vector images have opposing advantages and disadvantages. A raster @@ -1693,7 +1859,7 @@ computer has to draw all the elements each time it is displayed. For example, if you have a scatter plot with 1 million points stored as an SVG file, it may take your computer some time to open the image. On the other hand, you can zoom into / scale up vector graphics as much as you like without the image looking -bad, while raster images eventually start to look "pixelated." +bad, while raster images eventually start to look "pixelated." ```{index} PDF ``` @@ -1703,52 +1869,37 @@ bad, while raster images eventually start to look "pixelated." > **Note:** The portable document format [PDF](https://en.wikipedia.org/wiki/PDF) (`.pdf`) is commonly used to > store *both* raster and vector formats. If you try to open a PDF and it's taking a long time -> to load, it may be because there is a complicated vector graphics image that your computer is rendering. - -Let's learn how to save plot images to these different file formats using a -scatter plot of -the [Old Faithful data set](https://www.stat.cmu.edu/~larry/all-of-statistics/=data/faithful.dat) -{cite:p}`faithfuldata`, -shown in {numref}`faithful_scatter_labels` +> to load, it may be because there is a complicated vector graphics image that your computer is rendering. -Now that we have a named `altair` plot object, we can use the `chart.save` function -to save a file containing this image. -`chart.save` works by taking the path to the directory where you would like to save the file -(e.g., `img/filename.png` to save a file named `filename` to the `img` directory), -The kind of image to save is specified by the file extension. -For example, -to create a PNG image file, we specify that the file extension is `.png`. -Below we demonstrate how to save PNG and SVG file types -for the `faithful_scater_labels` plot: +Let's learn how to save plot images to `.png` and `.svg` file formats using the +`faithful_scatter_labels` scatter plot of the [Old Faithful data set](https://www.stat.cmu.edu/~larry/all-of-statistics/=data/faithful.dat) +{cite:p}`faithfuldata` that we created earlier, shown in {numref}`faithful_scatter_labels`. +To save the plot to a file, we can use the `save` +method. The `save` method takes the path to the filename where you would like to +save the file (e.g., `img/filename.png` to save a file named `filename.png` to the `img` directory). +The kind of image to save is specified by the file extension. For example, to +create a PNG image file, we specify that the file extension is `.png`. Below +we demonstrate how to save PNG and SVG file types for the +`faithful_scatter_labels` plot. ```{code-cell} ipython3 -:tags: ["remove-cell"] -!pip install altair_saver -``` - - -```{code-cell} ipython3 -#!pip install altair_saver #uncomment and run in jupyter notebook to install altair_saver, if not already installed from altair_saver import save -faithful_scatter_labels.save("faithful_plot.png") -faithful_scatter_labels.save("faithful_plot.svg") +faithful_scatter_labels.save("img/faithful_plot.png") +faithful_scatter_labels.save("img/faithful_plot.svg") ``` ```{code-cell} ipython3 -:tags: ["remove-cell"] import os -png_size = os.path.getsize("data/faithful_plot.png")/1000000 -svg_size = os.path.getsize("data/faithful_plot.svg")/1000000 +import numpy as np +png_size = np.round(os.path.getsize("img/faithful_plot.png")/(1024*1024), 2) +svg_size = np.round(os.path.getsize("img/faithful_plot.svg")/(1024*1024), 2) -# glue("png_size", png_size) -# glue("svg_size", svg_size) +glue("png_size", png_size) +glue("svg_size", svg_size) ``` - - - ```{list-table} File sizes of the scatter plot of the Old Faithful data set when saved as different file formats. :header-rows: 1 :name: png-vs-svg-table @@ -1758,30 +1909,24 @@ svg_size = os.path.getsize("data/faithful_plot.svg")/1000000 - Image size * - Raster - PNG - - {glue:}`png_size` + - {glue:}`png_size` MB * - Vector - SVG - - {glue:}`svg_size` + - {glue:}`svg_size` MB ``` - - Take a look at the file sizes in {numref}`png-vs-svg-table` -Wow, that's quite a difference! Notice that for such a simple plot with few -graphical elements (points), the vector graphics format (SVG) is over 100 times -smaller than the uncompressed raster images. - -In {numref}`png-vs-svg`, we also show what +Wow, that's quite a difference! In this case, the `.png` image is almost 4 times +smaller than the `.svg` image. Since there are a decent number of points in the plot, +the vector graphics format image (`.svg`) is bigger than the raster image (`.png`), which +just stores the image data itself. +In {numref}`png-vs-svg`, we show what the images look like when we zoom in to a rectangle with only 3 data points. You can see why vector graphics formats are so useful: because they're just based on mathematical formulas, vector graphics can be scaled up to arbitrary sizes. This makes them great for presentation media of all sizes, from papers to posters to billboards. - - - - ```{figure} img/png-vs-svg.png --- height: 400px @@ -1792,15 +1937,15 @@ Zoomed in `faithful`, raster (PNG, left) and vector (SVG, right) formats. ## Exercises -Practice exercises for the material covered in this chapter -can be found in the accompanying -[worksheets repository](https://github.com/UBC-DSCI/data-science-a-first-intro-worksheets#readme) +Practice exercises for the material covered in this chapter +can be found in the accompanying +[worksheets repository](https://github.com/UBC-DSCI/data-science-a-first-intro-python-worksheets#readme) in the "Effective data visualization" row. You can launch an interactive version of the worksheet in your browser by clicking the "launch binder" button. You can also preview a non-interactive version of the worksheet by clicking "view worksheet." If you instead decide to download the worksheet and run it on your own machine, make sure to follow the instructions for computer setup -found in Chapter \@ref(move-to-your-own-machine). This will ensure that the automated feedback +found in the {ref}`move-to-your-own-machine` chapter. This will ensure that the automated feedback and guidance that the worksheets provide will function as intended. ## Additional resources @@ -1814,7 +1959,7 @@ and guidance that the worksheets provide will function as intended. a wealth of information on designing effective visualizations. It is not specific to any particular programming language or library. If you want to improve your visualization skills, this is the next place to look. -- The [dates and times](https://wesmckinney.com/book/time-series.html){cite:p}`mckinney2012python` +- The [dates and times](https://wesmckinney.com/book/time-series.html){cite:p}`mckinney2012python` chapter is where you should look if you want to learn about `date` and `time`, including how to create them, and how to use them to effectively handle durations, etc ## References diff --git a/source/wrangling.md b/source/wrangling.md index 4f0a4573..d9e33cc1 100644 --- a/source/wrangling.md +++ b/source/wrangling.md @@ -41,71 +41,33 @@ By the end of the chapter, readers will be able to do the following: - Define the term "tidy data". - Discuss the advantages of storing data in a tidy data format. - - Define what lists, series and data frames are in Python, and describe how they relate to + - Define what series and data frames are in Python, and describe how they relate to each other. - Describe the common types of data in Python and their uses. - Recall and use the following functions for their intended data wrangling tasks: - - `.agg` - - `.apply` - - `.assign` - - `.groupby` - - `.melt` - - `.pivot` - - `.str.split` + - `agg` + - `apply` + - `assign` + - `groupby` + - `melt` + - `pivot` + - `str.split` - Recall and use the following operators for their intended data wrangling tasks: - - `==` + - `==` - `in` - `and` - `or` - - `df[]` - - `.iloc[]` - - `.loc[]` + - `[]` + - `loc[]` + - `iloc[]` -```{code-cell} ipython3 ---- -jupyter: - source_hidden: true -tags: [remove-cell] ---- -# By the end of the chapter, readers will be able to do the following: - -# - Define the term "tidy data". -# - Discuss the advantages of storing data in a tidy data format. -# - Define what vectors, lists, and data frames are in R, and describe how they relate to -# each other. -# - Describe the common types of data in R and their uses. -# - Recall and use the following functions for their -# intended data wrangling tasks: -# - `across` -# - `c` -# - `filter` -# - `group_by` -# - `select` -# - `map` -# - `mutate` -# - `pull` -# - `pivot_longer` -# - `pivot_wider` -# - `rowwise` -# - `separate` -# - `summarize` -# - Recall and use the following operators for their -# intended data wrangling tasks: -# - `==` -# - `%in%` -# - `!` -# - `&` -# - `|` -# - `|>` and `%>%` -``` - -## Data frames, series, and lists - -In Chapters {ref}`intro` and {ref}`reading`, *data frames* were the focus: +## Data frames and series + +In the chapters on {ref}`intro` and {ref}`reading`, *data frames* were the focus: we learned how to import data into Python as a data frame, and perform basic operations on data frames in Python. -In the remainder of this book, this pattern continues. The vast majority of tools we use will require +In the remainder of this book, this pattern continues. The vast majority of tools we use will require that data are represented as a `pandas` **data frame** in Python. Therefore, in this section, we will dig more deeply into what data frames are and how they are represented in Python. This knowledge will be helpful in effectively utilizing these objects in our data analyses. @@ -147,46 +109,31 @@ data set. There are 13 entities in the data set in total, corresponding to the ```{figure} img/data_frame_slides_cdn/data_frame_slides_cdn.004.jpeg :name: fig:02-obs -:figclass: caption-hack +:figclass: figure A data frame storing data regarding the population of various regions in Canada. In this example data frame, the row that corresponds to the observation for the city of Vancouver is colored yellow, and the column that corresponds to the population variable is colored blue. ``` -```{code-cell} ipython3 -:tags: [remove-cell] - -# The following cell was removed because there is no "vector" in Python. -``` - -+++ {"tags": ["remove-cell"]} - -Python stores the columns of a data frame as either -*lists* or *vectors*. For example, the data frame in Figure -{numref}`fig:02-vectors` has three vectors whose names are `region`, `year` and -`population`. The next two sections will explain what lists and vectors are. - -```{figure} img/data_frame_slides_cdn/data_frame_slides_cdn.005.jpeg -:name: fig:02-vectors -:figclass: caption-hack - -Data frame with three vectors. -``` - -+++ - ### What is a series? ```{index} pandas.Series ``` -In Python, `pandas` **series** are arrays with labels. They are strictly 1-dimensional and can contain any data type (integers, strings, floats, etc), including a mix of them (objects); -Python has several different basic data types, as shown in {numref}`tab:datatype-table`. -You can create a `pandas` series using the `pd.Series()` function. For -example, to create the vector `region` as shown in -{numref}`fig:02-series`, you can write: +In Python, `pandas` **series** are are objects that can contain one or more elements (like a list). +They are a single column, are ordered, can be indexed, and can contain any data type. +The `pandas` package uses `Series` objects to represent the columns in a data frame. +`Series` can contain a mix of data types, but it is good practice to only include a single type in a series +because all observations of one variable should be the same type. +Python +has several different basic data types, as shown in +{numref}`tab:datatype-table`. +You can create a `pandas` series using the +`pd.Series()` function. For example, to create the series `region` as shown +in {numref}`fig:02-series`, you can write the following. ```{code-cell} ipython3 import pandas as pd + region = pd.Series(["Toronto", "Montreal", "Vancouver", "Calgary", "Ottawa"]) region ``` @@ -195,46 +142,11 @@ region ```{figure} img/wrangling/pandas_dataframe_series.png :name: fig:02-series -:figclass: caption-hack +:figclass: figure Example of a `pandas` series whose type is string. ``` -+++ {"tags": ["remove-cell"]} - -### What is a vector? - -In R, **vectors** \index{vector}\index{atomic vector|see{vector}} are objects that can contain one or more elements. The vector -elements are ordered, and they must all be of the same **data type**; -R has several different basic data types, as shown in {numref}`tab:datatype-table`. -Figure \@ref(fig:02-vector) provides an example of a vector where all of the elements are -of character type. -You can create vectors in R using the `c` function \index{c function} (`c` stands for "concatenate"). For -example, to create the vector `region` as shown in Figure -\@ref(fig:02-vector), you would write: - -``` {r} -year <- c("Toronto", "Montreal", "Vancouver", "Calgary", "Ottawa") -year -``` - -> **Note:** Technically, these objects are called "atomic vectors." In this book -> we have chosen to call them "vectors," which is how they are most commonly -> referred to in the R community. To be totally precise, "vector" is an umbrella term that -> encompasses both atomic vector and list objects in R. But this creates a -> confusing situation where the term "vector" could -> mean "atomic vector" *or* "the umbrella term for atomic vector and list," -> depending on context. Very confusing indeed! So to keep things simple, in -> this book we *always* use the term "vector" to refer to "atomic vector." -> We encourage readers who are enthusiastic to learn more to read the -> Vectors chapter of *Advanced R* [@wickham2019advanced]. - -``` {r 02-vector, echo = FALSE, message = FALSE, warning = FALSE, fig.cap = "Example of a vector whose type is character.", fig.retina = 2, out.width = "100%"} -image_read("img/data_frame_slides_cdn/data_frame_slides_cdn.007.jpeg") %>% - image_crop("3632x590") -``` - -+++ ```{code-cell} ipython3 :tags: [remove-cell] @@ -265,76 +177,30 @@ image_read("img/data_frame_slides_cdn/data_frame_slides_cdn.007.jpeg") %>% ```{table} Basic data types in Python :name: tab:datatype-table -| English name | Type name | Type Category | Description | Example | -| :-------------------- | :--------- | :------------- | :-------------------------------------------- | :----------------------------------------- | -| integer | `int` | Numeric Type | positive/negative whole numbers | `42` | -| floating point number | `float` | Numeric Type | real number in decimal form | `3.14159` | -| boolean | `bool` | Boolean Values | true or false | `True` | -| string | `str` | Sequence Type | text | `"Can I have a cheezburger?"` | -| list | `list` | Sequence Type | a collection of objects - mutable & ordered | `['Ali', 'Xinyi', 'Miriam']` | -| tuple | `tuple` | Sequence Type | a collection of objects - immutable & ordered | `('Thursday', 6, 9, 2018)` | -| dictionary | `dict` | Mapping Type | mapping of key-value pairs | `{'name':'DSCI', 'code':100, 'credits':2}` | -| none | `NoneType` | Null Object | represents no value | `None` | +| Data type | Abbreviation | Description | Example | +| :-------------------- | :----------- | :-------------------------------------------- | :----------------------------------------- | +| integer | `int` | positive/negative/zero whole numbers | `42` | +| floating point number | `float` | real number in decimal form | `3.14159` | +| boolean | `bool` | true or false | `True` | +| string | `str` | text | `"Hello World"` | +| none | `NoneType` | represents no value | `None` | ``` +++ -It is important in Python to make sure you represent your data with the correct type. -Many of the `pandas` functions we use in this book treat -the various data types differently. You should use integers and float types -(which both fall under the "numeric" umbrella type) to represent numbers and perform -arithmetic. Strings are used to represent data that should -be thought of as "text", such as words, names, paths, URLs, and more. -There are other basic data types in Python, such as *set* -and *complex*, but we do not use these in this textbook. - -```{code-cell} ipython3 -:tags: [remove-cell] - -# It is important in R to make sure you represent your data with the correct type. -# Many of the `tidyverse` functions we use in this book treat -# the various data types differently. You should use integers and double types -# (which both fall under the "numeric" umbrella type) to represent numbers and perform -# arithmetic. Doubles are more common than integers in R, though; for instance, a double data type is the -# default when you create a vector of numbers using `c()`, and when you read in -# whole numbers via `read_csv`. Characters are used to represent data that should -# be thought of as "text", such as words, names, paths, URLs, and more. Factors help us -# encode variables that represent *categories*; a factor variable takes one of a discrete -# set of values known as *levels* (one for each category). The levels can be ordered or unordered. Even though -# factors can sometimes *look* like characters, they are not used to represent -# text, words, names, and paths in the way that characters are; in fact, R -# internally stores factors using integers! There are other basic data types in R, such as *raw* -# and *complex*, but we do not use these in this textbook. -``` - -### What is a list? +It is important in Python to make sure you represent your data with the correct type. +Many of the `pandas` functions we use in this book treat +the various data types differently. You should use `int` and `float` types +to represent numbers and perform arithmetic. The `int` type is for integers that have no decimal point, +while the `float` type is for numbers that have a decimal point. +The `bool` type are boolean variables that can only take on one of two values: `True` or `False`. +The `string` type is used to represent data that should +be thought of as "text", such as words, names, paths, URLs, and more. +A `NoneType` is a special type in Python that is used to indicate no value; this can occur, +for example, when you have missing data. +There are other basic data types in Python, but we will generally +not use these in this textbook. -```{index} list -``` - -Lists are built-in objects in Python that have multiple, ordered elements. -`pandas` series can be treated as lists with labels (indices). - -```{code-cell} ipython3 -:tags: [remove-cell] - -# Lists \index{list} are also objects in R that have multiple, ordered elements. -# Vectors and lists differ by the requirement of element type -# consistency. All elements within a single vector must be of the same type (e.g., -# all elements are characters), whereas elements within a single list can be of -# different types (e.g., characters, integers, logicals, and even other lists). -``` - -+++ {"tags": ["remove-cell"]} - -```{figure} img/data_frame_slides_cdn/data_frame_slides_cdn.008.jpeg -:name: fig:02-vec-vs-list -:figclass: caption-hack - -A vector versus a list. -``` - -+++ ### What does this have to do with data frames? @@ -343,41 +209,26 @@ A vector versus a list. ```{index} data frame; definition ``` -A data frame is really just series stuck together that follows two rules: - -1. Each element itself is a series. -2. Each element (series) must have the same length. - -Not all columns in a data frame need to be of the same type. +A data frame is really just a collection of series that are stuck together, +where each series corresponds to one column and all must have the same length. +But not all columns in a data frame need to be of the same type. {numref}`fig:02-dataframe` shows a data frame where -the columns are series of different types. +the columns are series of different types. But each element *within* +one column should usually be the same type, since the values for a single variable +are usually all of the same type. For example, if the variable is the name of a city, +that name should be a string, whereas if the variable is a year, that should be an +integer. So even though series let you put different types in them, it is most common +(and good practice!) to have just one type per column. +++ {"tags": []} ```{figure} img/wrangling/pandas_dataframe_series-3.png :name: fig:02-dataframe -:figclass: caption-hack +:figclass: figure -Data frame and vector types. +Data frame and series types. ``` -```{code-cell} ipython3 -:tags: [remove-cell] - -# A data frame \index{data frame!definition} is really a special kind of list that follows two rules: - -# 1. Each element itself must either be a vector or a list. -# 2. Each element (vector or list) must have the same length. - -# Not all columns in a data frame need to be of the same type. -# Figure \@ref(fig:02-dataframe) shows a data frame where -# the columns are vectors of different types. -# But remember: because the columns in this example are *vectors*, -# the elements must be the same data type *within each column.* -# On the other hand, if our data frame had *list* columns, there would be no such requirement. -# It is generally much more common to use *vector* columns, though, -# as the values for a single variable are usually all of the same type. -``` ```{index} type ``` @@ -386,46 +237,72 @@ Data frame and vector types. > For example we can check the class of the Canadian languages data set, > `can_lang`, we worked with in the previous chapters and we see it is a `pandas.core.frame.DataFrame`. -```{code-cell} ipython3 -:tags: [remove-cell] - -# The functions from the `tidyverse` package that we use often give us a -# special class of data frame called a *tibble*. Tibbles have some additional \index{tibble} -# features and benefits over the built-in data frame object. These include the -# ability to add useful attributes (such as grouping, which we will discuss later) -# and more predictable type preservation when subsetting. -# Because a tibble is just a data frame with some added features, -# we will collectively refer to both built-in R data frames and -# tibbles as data frames in this book. - -# > **Note:** You can use the function `class` \index{class} on a data object to assess whether a data -# > frame is a built-in R data frame or a tibble. If the data object is a data -# > frame, `class` will return `"data.frame"`. If the data object is a -# > tibble it will return `"tbl_df" "tbl" "data.frame"`. You can easily convert -# > built-in R data frames to tibbles using the `tidyverse` `as_tibble` function. -# > For example we can check the class of the Canadian languages data set, -# > `can_lang`, we worked with in the previous chapters and we see it is a tibble. -``` ```{code-cell} ipython3 can_lang = pd.read_csv("data/can_lang.csv") type(can_lang) ``` -Lists, Series and DataFrames are basic types of *data structure* in Python, which -are core to most data analyses. We summarize them in -{numref}`tab:datastructure-table`. There are several other data structures in the Python programming -language (*e.g.,* matrices), but these are beyond the scope of this book. +### Data structures in Python -+++ +The `Series` and `DataFrame` types are *data structures* in Python, which +are core to most data analyses. +The functions from `pandas` that we use often give us back a `DataFrame` +or a `Series` depending on the operation. Because +`Series` are essentially simple `DataFrames`, we will refer +to both `DataFrames` and `Series` as "data frames" in the text. +There are other types that represent data structures in Python. +We summarize the most common ones in {numref}`tab:datastruc-table`. ```{table} Basic data structures in Python -:name: tab:datastructure-table +:name: tab:datastruc-table | Data Structure | Description | -| --- |------------ | -| list | An 1D ordered collection of values that can store multiple data types at once. | -| Series | An 1D ordered collection of values *with labels* that can store multiple data types at once. | -| DataFrame | A 2D labeled data structure with columns of potentially different types. | +| --- | ----------- | +| list | An ordered collection of values that can store multiple data types at once. | +| dict | A labeled data structure where `keys` are paired with `values` | +| Series | An ordered collection of values *with labels* that can store multiple data types at once. | +| DataFrame | A labeled data structure with `Series` columns of potentially different types. | +``` + +A `list` is an ordered collection of values. To create a list, we put the contents of the list in between +square brackets `[]`, where each item of the list is separated by a comma. A `list` can contain values +of different types. The example below contains six `str` entries. + +```{code-cell} ipython3 +cities = ["Toronto", "Vancouver", "Montreal", "Calgary", "Ottawa", "Winnipeg"] +cities +``` +A list can directly be converted to a pandas `Series`. +```{code-cell} ipython3 +cities_series = pd.Series(cities) +cities_series +``` + +A `dict`, or dictionary, contains pairs of "keys" and "values." +You use a key to look up its corresponding value. Dictionaries are created +using curly brackets `{}`. Each entry starts with the +key on the left, followed by a colon symbol `:`, and then the value. +A dictionary can have multiple key-value pairs, each separted by a comma. +Keys can take a wide variety of types (`int` and `str` are commonly used), and values can take any type; +the key-value pairs in a dictionary can all be of different types, too. + In the example below, +we create a dictionary that has two keys: `"cities"` and `"population"`. +The values associated with each are lists. +```{code-cell} ipython3 +population_in_2016 = { + "cities": ["Toronto", "Vancouver", "Montreal", "Calgary", "Ottawa", "Winnipeg"], + "population": [2235145, 1027613, 1823281, 544870, 571146, 321484] +} +population_in_2016 +``` +A dictionary can be converted to a data frame. Keys +become the column names, and the values become the entries in +those columns. Dictionaries on their own are quite simple objects; it is preferable to work with a data frame +because then we have access to the built-in functionality in +`pandas` (e.g. `loc[]`, `[]`, and many functions that we will discuss in the upcoming sections)! +```{code-cell} ipython3 +population_in_2016 = pd.DataFrame(population_in_2016) +population_in_2016 ``` +++ @@ -435,9 +312,10 @@ language (*e.g.,* matrices), but these are beyond the scope of this book. ```{index} tidy data; definition ``` -There are many ways a tabular data set can be organized. This chapter will focus -on introducing the **tidy data** format of organization and how to make your raw -(and likely messy) data tidy. A tidy data frame satisfies +There are many ways a tabular data set can be organized. The data frames we +have looked at so far have all been using the **tidy data** format of +organization. This chapter will focus on introducing the tidy data format and +how to make your raw (and likely messy) data tidy. A tidy data frame satisfies the following three criteria {cite:p}`wickham2014tidy`: - each row is a single observation, @@ -445,14 +323,14 @@ the following three criteria {cite:p}`wickham2014tidy`: - each value is a single cell (i.e., its entry in the data frame is not shared with another value). -{numref}`fig:02-tidy-image` demonstrates a tidy data set that satisfies these +{numref}`fig:02-tidy-image` demonstrates a tidy data set that satisfies these three criteria. +++ {"tags": []} ```{figure} img/tidy_data/tidy_data.001-cropped.jpeg :name: fig:02-tidy-image -:figclass: caption-hack +:figclass: figure Tidy data satisfies three criteria. ``` @@ -464,8 +342,8 @@ Tidy data satisfies three criteria. There are many good reasons for making sure your data are tidy as a first step in your analysis. The most important is that it is a single, consistent format that nearly every function -in the `pandas` recognizes. No matter what the variables and observations -in your data represent, as long as the data frame +in the `pandas` recognizes. No matter what the variables and observations +in your data represent, as long as the data frame is tidy, you can manipulate it, plot it, and analyze it using the same tools. If your data is *not* tidy, you will have to write special bespoke code in your analysis that will not only be error-prone, but hard for others to understand. @@ -486,23 +364,23 @@ below! +++ -### Tidying up: going from wide to long using `.melt` +### Tidying up: going from wide to long using `melt` ```{index} pandas.DataFrame; melt ``` -One task that is commonly performed to get data into a tidy format -is to combine values that are stored in separate columns, +One task that is commonly performed to get data into a tidy format +is to combine values that are stored in separate columns, but are really part of the same variable, into one. -Data is often stored this way -because this format is sometimes more intuitive for human readability +Data is often stored this way +because this format is sometimes more intuitive for human readability and understanding, and humans create data sets. -In {numref}`fig:02-wide-to-long`, -the table on the left is in an untidy, "wide" format because the year values -(2006, 2011, 2016) are stored as column names. -And as a consequence, -the values for population for the various cities -over these years are also split across several columns. +In {numref}`fig:02-wide-to-long`, +the table on the left is in an untidy, "wide" format because the year values +(2006, 2011, 2016) are stored as column names. +And as a consequence, +the values for population for the various cities +over these years are also split across several columns. For humans, this table is easy to read, which is why you will often find data stored in this wide format. However, this format is difficult to work with @@ -518,19 +396,24 @@ greatly simplified once the data is tidied. Another problem with data in this format is that we don't know what the numbers under each year actually represent. Do those numbers represent -population size? Land area? It's not clear. -To solve both of these problems, -we can reshape this data set to a tidy data format +population size? Land area? It's not clear. +To solve both of these problems, +we can reshape this data set to a tidy data format by creating a column called "year" and a column called "population." This transformation—which makes the data "longer"—is shown as the right table in -{numref}`fig:02-wide-to-long`. +{numref}`fig:02-wide-to-long`. Note that the number of entries in our data frame +can change in this transformation. The "untidy" data has 5 rows and 3 columns for +a total of 15 entries, whereas the "tidy" data on the right has 15 rows and 2 columns +for a total of 30 entries. +++ {"tags": []} ```{figure} img/pivot_functions/pivot_functions.001.jpeg :name: fig:02-wide-to-long -:figclass: caption-hack +:figclass: figure + + Melting data from a wide to long data format. ``` @@ -540,63 +423,64 @@ Melting data from a wide to long data format. ```{index} Canadian languages ``` -We can achieve this effect in Python using the `.melt` function from the `pandas` package. -The `.melt` function combines columns, -and is usually used during tidying data -when we need to make the data frame longer and narrower. -To learn how to use `.melt`, we will work through an example with the +We can achieve this effect in Python using the `melt` function from the `pandas` package. +The `melt` function combines columns, +and is usually used during tidying data +when we need to make the data frame longer and narrower. +To learn how to use `melt`, we will work through an example with the `region_lang_top5_cities_wide.csv` data set. This data set contains the -counts of how many Canadians cited each language as their mother tongue for five +counts of how many Canadians cited each language as their mother tongue for five major Canadian cities (Toronto, MontrĆ©al, Vancouver, Calgary and Edmonton) from -the 2016 Canadian census. -To get started, +the 2016 Canadian census. +To get started, we will use `pd.read_csv` to load the (untidy) data. ```{code-cell} ipython3 +:tags: ["output_scroll"] lang_wide = pd.read_csv("data/region_lang_top5_cities_wide.csv") lang_wide ``` -What is wrong with the untidy format above? -The table on the left in {numref}`fig:img-pivot-longer-with-table` +What is wrong with the untidy format above? +The table on the left in {numref}`fig:img-pivot-longer-with-table` represents the data in the "wide" (messy) format. -From a data analysis perspective, this format is not ideal because the values of -the variable *region* (Toronto, MontrĆ©al, Vancouver, Calgary and Edmonton) +From a data analysis perspective, this format is not ideal because the values of +the variable *region* (Toronto, MontrĆ©al, Vancouver, Calgary and Edmonton) are stored as column names. Thus they are not easily accessible to the data analysis functions we will apply to our data set. Additionally, the *mother tongue* variable values are spread across multiple columns, which will prevent us from doing any desired visualization or statistical tasks until we combine them into one column. For -instance, suppose we want to know the languages with the highest number of +instance, suppose we want to know the languages with the highest number of Canadians reporting it as their mother tongue among all five regions. This -question would be tough to answer with the data in its current format. -We *could* find the answer with the data in this format, +question would be tough to answer with the data in its current format. +We *could* find the answer with the data in this format, though it would be much easier to answer if we tidy our -data first. If mother tongue were instead stored as one column, -as shown in the tidy data on the right in +data first. If mother tongue were instead stored as one column, +as shown in the tidy data on the right in {numref}`fig:img-pivot-longer-with-table`, -we could simply use one line of code (`df["mother_tongue"].max()`) +we could simply use one line of code (`df["mother_tongue"].max()`) to get the maximum value. +++ {"tags": []} ```{figure} img/wrangling/pandas_melt_wide-long.png :name: fig:img-pivot-longer-with-table -:figclass: caption-hack +:figclass: figure -Going from wide to long with the `.melt` function. +Going from wide to long with the `melt` function. ``` +++ -{numref}`fig:img-pivot-longer` details the arguments that we need to specify -in the `.melt` function to accomplish this data transformation. +{numref}`fig:img-pivot-longer` details the arguments that we need to specify +in the `melt` function to accomplish this data transformation. +++ {"tags": []} ```{figure} img/wrangling/pandas_melt_args_labels.png :name: fig:img-pivot-longer -:figclass: caption-hack +:figclass: figure Syntax for the `melt` function. ``` @@ -609,29 +493,29 @@ Syntax for the `melt` function. ```{index} see: :; column range ``` -We use `.melt` to combine the Toronto, MontrĆ©al, +We use `melt` to combine the Toronto, MontrĆ©al, Vancouver, Calgary, and Edmonton columns into a single column called `region`, and create a column called `mother_tongue` that contains the count of how many Canadians report each language as their mother tongue for each metropolitan -area. We specify `value_vars` to be all -the columns between Toronto and Edmonton: +area ```{code-cell} ipython3 +:tags: ["output_scroll"] lang_mother_tidy = lang_wide.melt( id_vars=["category", "language"], - value_vars=["Toronto", "MontrĆ©al", "Vancouver", "Calgary", "Edmonton"], var_name="region", value_name="mother_tongue", ) - lang_mother_tidy ``` > **Note**: In the code above, the call to the -> `.melt` function is split across several lines. This is allowed in -> certain cases; for example, when calling a function as above, as long as the -> line ends with a comma `,` Python knows to keep reading on the next line. -> Splitting long lines like this across multiple lines is encouraged +> `melt` function is split across several lines. Recall from +> the {ref}`intro` chapter that this is allowed in +> certain cases. For example, when calling a function as above, the input +> arguments are between parentheses `()` and Python knows to keep reading on +> the next line. Each line ends with a comma `,` making it easier to read. +> Splitting long lines like this across multiple lines is encouraged > as it helps significantly with code readability. Generally speaking, you should > limit each line of code to about 80 characters. @@ -648,7 +532,7 @@ been met: +++ (pivot-wider)= -### Tidying up: going from long to wide using `.pivot` +### Tidying up: going from long to wide using `pivot` ```{index} pandas.DataFrame; pivot ``` @@ -656,17 +540,17 @@ been met: Suppose we have observations spread across multiple rows rather than in a single row. For example, in {numref}`fig:long-to-wide`, the table on the left is in an untidy, long format because the `count` column contains three variables -(population, commuter, and incorporated count) and information about each observation -(here, population, commuter, and incorporated counts for a region) is split across three rows. -Remember: one of the criteria for tidy data +(population, commuter, and incorporated count) and information about each observation +(here, population, commuter, and incorporated counts for a region) is split across three rows. +Remember: one of the criteria for tidy data is that each observation must be in a single row. Using data in this format—where two or more variables are mixed together in a single column—makes it harder to apply many usual `pandas` functions. -For example, finding the maximum number of commuters +For example, finding the maximum number of commuters would require an additional step of filtering for the commuter values before the maximum can be computed. -In comparison, if the data were tidy, +In comparison, if the data were tidy, all we would have to do is compute the maximum value for the commuter column. To reshape this untidy data set to a tidy (and in this case, wider) format, we need to create columns called "population", "commuters", and "incorporated." @@ -676,62 +560,64 @@ This is illustrated in the right table of {numref}`fig:long-to-wide`. ```{figure} img/pivot_functions/pivot_functions.002.jpeg :name: fig:long-to-wide -:figclass: caption-hack +:figclass: figure Going from long to wide data. ``` +++ -To tidy this type of data in Python, we can use the `.pivot` function. -The `.pivot` function generally increases the number of columns (widens) -and decreases the number of rows in a data set. -To learn how to use `.pivot`, -we will work through an example -with the `region_lang_top5_cities_long.csv` data set. -This data set contains the number of Canadians reporting +To tidy this type of data in Python, we can use the `pivot` function. +The `pivot` function generally increases the number of columns (widens) +and decreases the number of rows in a data set. +To learn how to use `pivot`, +we will work through an example +with the `region_lang_top5_cities_long.csv` data set. +This data set contains the number of Canadians reporting the primary language at home and work for five major cities (Toronto, MontrĆ©al, Vancouver, Calgary and Edmonton). ```{code-cell} ipython3 +:tags: ["output_scroll"] lang_long = pd.read_csv("data/region_lang_top5_cities_long.csv") lang_long ``` -What makes the data set shown above untidy? -In this example, each observation is a language in a region. -However, each observation is split across multiple rows: -one where the count for `most_at_home` is recorded, -and the other where the count for `most_at_work` is recorded. -Suppose the goal with this data was to +What makes the data set shown above untidy? +In this example, each observation is a language in a region. +However, each observation is split across multiple rows: +one where the count for `most_at_home` is recorded, +and the other where the count for `most_at_work` is recorded. +Suppose the goal with this data was to visualize the relationship between the number of -Canadians reporting their primary language at home and work. +Canadians reporting their primary language at home and work. Doing that would be difficult with this data in its current form, since these two variables are stored in the same column. {numref}`fig:img-pivot-wider-table` shows how this data -will be tidied using the `.pivot` function. +will be tidied using the `pivot` function. +++ {"tags": []} ```{figure} img/wrangling/pandas_pivot_long-wide.png :name: fig:img-pivot-wider-table -:figclass: caption-hack +:figclass: figure -Going from long to wide with the `.pivot` function. +Going from long to wide with the `pivot` function. ``` +++ -{numref}`fig:img-pivot-wider` details the arguments that we need to specify -in the `.pivot` function. +{numref}`fig:img-pivot-wider` details the arguments that we need to specify in the `pivot` function. + +**TODO make figure match code below** +++ {"tags": []} ```{figure} img/wrangling/pandas_pivot_args_labels.png :name: fig:img-pivot-wider -:figclass: caption-hack +:figclass: figure -Syntax for the `.pivot` function. +Syntax for the `pivot` function. ``` +++ @@ -739,8 +625,11 @@ Syntax for the `.pivot` function. We will apply the function as detailed in {numref}`fig:img-pivot-wider`. ```{code-cell} ipython3 +:tags: ["output_scroll"] lang_home_tidy = lang_long.pivot( - index=["region", "category", "language"], columns=["type"], values=["count"] + index=["region", "category", "language"], + columns=["type"], + values=["count"] ).reset_index() lang_home_tidy.columns = [ @@ -753,11 +642,30 @@ lang_home_tidy.columns = [ lang_home_tidy ``` +In the first step, note that we added a call to `reset_index`. When `pivot` is called with +multiple column names passed to the `index`, those entries become the "name" of each row that +would be used when you filter rows with `[]` or `loc` rather than just simple numbers. This +can be confusing... What `reset_index` does is sets us back with the usual expected behaviour +where each row is "named" with an integer. This is a subtle point, but the main take-away is that +when you call `pivot`, it is a good idea to call `reset_index` afterwards. + +The second operation we applied is to rename the columns. When we perform the `pivot` +operation, it keeps the original column name `"count"` and adds the `"type"` as a second column name. +Having two names for a column can be confusing! So we rename giving each column only one name. + +We can print out some useful information about our data frame using the `info` function. +In the first row it tells us the `type` of `lang_home_tidy` (it is a `pandas` `DataFrame`). The second +row tells us how many rows there are: 1070, and to index those rows, you can use numbers between +0 and 1069 (remember that Python starts counting at 0!). Next, there is a print out about the data +colums. Here there are 5 columns total. The little table it prints out tells you the name of each +column, the number of non-null values (e.g. the number of entries that are not missing values), and +the type of the entries. Finally the last two rows summarize the types of each column and how much +memory the data frame is using on your computer. ```{code-cell} ipython3 -lang_home_tidy.dtypes +lang_home_tidy.info() ``` -The data above is now tidy! We can go through the three criteria again to check +The data is now tidy! We can go through the three criteria again to check that this data is a tidy data set. 1. All the statistical variables are their own columns in the data frame (i.e., @@ -768,43 +676,45 @@ that this data is a tidy data set. frame is not shared with another value). You might notice that we have the same number of columns in the tidy data set as -we did in the messy one. Therefore `.pivot` didn't really "widen" the data. +we did in the messy one. Therefore `pivot` didn't really "widen" the data. This is just because the original `type` column only had -two categories in it. If it had more than two, `.pivot` would have created +two categories in it. If it had more than two, `pivot` would have created more columns, and we would see the data set "widen." + +++ (str-split)= -### Tidying up: using `.str.split` to deal with multiple delimiters +### Tidying up: using `str.split` to deal with multiple delimiters ```{index} pandas.Series; str.split, delimiter ``` -Data are also not considered tidy when multiple values are stored in the same +Data are also not considered tidy when multiple values are stored in the same cell. The data set we show below is even messier than the ones we dealt with above: the `Toronto`, `MontrĆ©al`, `Vancouver`, `Calgary` and `Edmonton` columns contain the number of Canadians reporting their primary language at home and -work in one column separated by the delimiter (`/`). The column names are the +work in one column separated by the delimiter (`/`). The column names are the values of a variable, *and* each value does not have its own cell! To turn this messy data into tidy data, we'll have to fix these issues. ```{code-cell} ipython3 +:tags: ["output_scroll"] lang_messy = pd.read_csv("data/region_lang_top5_cities_messy.csv") lang_messy ``` -First we’ll use `.melt` to create two columns, `region` and `value`, -similar to what we did previously. +First we’ll use `melt` to create two columns, `region` and `value`, +similar to what we did previously. The new `region` columns will contain the region names, -and the new column `value` will be a temporary holding place for the -data that we need to further separate, i.e., the +and the new column `value` will be a temporary holding place for the +data that we need to further separate, i.e., the number of Canadians reporting their primary language at home and work. ```{code-cell} ipython3 +:tags: ["output_scroll"] lang_messy_longer = lang_messy.melt( id_vars=["category", "language"], - value_vars=["Toronto", "MontrĆ©al", "Vancouver", "Calgary", "Edmonton"], var_name="region", value_name="value", ) @@ -812,40 +722,73 @@ lang_messy_longer = lang_messy.melt( lang_messy_longer ``` -Next we'll use `.str.split` to split the `value` column into two columns. -One column will contain only the counts of Canadians -that speak each language most at home, -and the other will contain the counts of Canadians -that speak each language most at work for each region. +Next we'll split the `value` column into two columns. +In basic Python, if we wanted to split the string `"50/0"` into two numbers `["50", "0"]` +we would use the `split` method on the string, and specify that the split should be made +on the slash character `"/"`. +```{code-cell} ipython3 +"50/0".split("/") +``` + +The `pandas` package provides similar functions that we can access +by using the `str` method. So, to split all of the entries for an entire +column in a data frame, we would use the `str.split` method. +Once we use this method, +one column will contain only the counts of Canadians +that speak each language most at home, +and the other will contain the counts of Canadians +that speak each language most at work for each region. {numref}`fig:img-separate` -outlines what we need to specify to use `.str.split`. +outlines what we need to specify to use `str.split`. +++ {"tags": []} ```{figure} img/wrangling/str-split_args_labels.png :name: fig:img-separate -:figclass: caption-hack +:figclass: figure -Syntax for the `.str.split` function. +Syntax for the `str.split` function. ``` +We will do this in multiple steps. First, we create a new object +that contains two columns. We will set the `expand` argument to `True` +to tell `pandas` that we want to expand the output into two columns. + ```{code-cell} ipython3 -tidy_lang = ( - pd.concat( - (lang_messy_longer, lang_messy_longer["value"].str.split("/", expand=True)), - axis=1, - ) - .rename(columns={0: "most_at_home", 1: "most_at_work"}) - .drop(columns=["value"]) -) +split_counts = lang_messy_longer["value"].str.split("/", expand=True) +split_counts +``` +Since we only operated on the `value` column, the `split_counts` data frame +doesn't have the rest of the columns (`language`, `region`, etc.) +that were in our original data frame. We don't want to lose this information, so +we will contatenate (combine) the original data frame with `split_counts` using +the `concat` function from `pandas`. The `concat` function *concatenates* data frames +along an axis. By default, it concatenates the data frames vertically along `axis=0` yielding a single +*taller* data frame. Since we want to concatenate our old columns to our +new `split_counts` data frame (to obtain a *wider* data frame), we will specify `axis=1`. +```{code-cell} ipython3 +:tags: ["output_scroll"] +tidy_lang = pd.concat( + [lang_messy_longer, split_counts], + axis=1, +) tidy_lang ``` +Next, we will rename our newly created columns (currently called +`0` and `1`) to the more meaningful names `"most_at_home"` and `"most_at_work"`, +and drop the `value` column from our data frame using the `drop` method. + ```{code-cell} ipython3 -tidy_lang.dtypes +:tags: ["output_scroll"] +tidy_lang = ( + tidy_lang.rename(columns={0: "most_at_home", 1: "most_at_work"}) + .drop(columns=["value"]) +) +tidy_lang ``` - +Note that we could have chained these steps together to make our code more compact. Is this data set now tidy? If we recall the three criteria for tidy data: - each row is a single observation, @@ -853,57 +796,36 @@ Is this data set now tidy? If we recall the three criteria for tidy data: - each value is a single cell. We can see that this data now satisfies all three criteria, making it easier to -analyze. But we aren't done yet! Notice in the table, all of the variables are -"object" data types. Object data types are columns of strings or columns with mixed types. In the previous example in Section {ref}`pivot-wider`, the -`most_at_home` and `most_at_work` variables were `int64` (integer)—you can -verify this by calling `df.dtypes`—which is a type -of numeric data. This change is due to the delimiter (`/`) when we read in this -messy data set. Python read these columns in as string types, and by default, -`.str.split` will return columns as object data types. - -It makes sense for `region`, `category`, and `language` to be stored as a -object type. However, suppose we want to apply any functions that treat the -`most_at_home` and `most_at_work` columns as a number (e.g., finding rows -above a numeric threshold of a column). -In that case, -it won't be possible to do if the variable is stored as a `object`. -Fortunately, the `pandas.to_numeric` function provides a natural way to fix problems -like this: it will convert the columns to the best numeric data types. - +analyze. But we aren't done yet! Although we can't see it in the data frame above, all of the variables are actually +"object" data types. We can check this using the `info` method. ```{code-cell} ipython3 -:tags: [remove-cell] - -# We can see that this data now satisfies all three criteria, making it easier to -# analyze. But we aren't done yet! Notice in the table above that the word -# `` appears beneath each of the column names. The word under the column name -# indicates the data type of each column. Here all of the variables are -# "character" data types. Recall, character data types are letter(s) or digits(s) -# surrounded by quotes. In the previous example in Section \@ref(pivot-wider), the -# `most_at_home` and `most_at_work` variables were `` (double)—you can -# verify this by looking at the tables in the previous sections—which is a type -# of numeric data. This change is due to the delimiter (`/`) when we read in this -# messy data set. R read these columns in as character types, and by default, -# `separate` will return columns as character data types. - -# It makes sense for `region`, `category`, and `language` to be stored as a -# character (or perhaps factor) type. However, suppose we want to apply any functions that treat the -# `most_at_home` and `most_at_work` columns as a number (e.g., finding rows -# above a numeric threshold of a column). -# In that case, -# it won't be possible to do if the variable is stored as a `character`. -# Fortunately, the `separate` function provides a natural way to fix problems -# like this: we can set `convert = TRUE` to convert the `most_at_home` -# and `most_at_work` columns to the correct data type. +tidy_lang.info() ``` +Object columns in `pandas` data frames are columns of strings or columns with +mixed types. In the previous example in the section on {ref}`pivot-wider`, the +`most_at_home` and `most_at_work` variables were `int64` (integer), which is a type of numeric data. +This change is due to the delimiter (`/`) when we read in this messy data set. +Python read these columns in as string types, and by default, `str.split` will +return columns with the `object` data type. + +It makes sense for `region`, `category`, and `language` to be stored as an +`object` type. However, suppose we want to apply any functions that treat the +`most_at_home` and `most_at_work` columns as a number (e.g., finding rows +above a numeric threshold of a column). +That won't be possible if the variable is stored as a `object`. +Fortunately, the `pandas.to_numeric` function provides a natural way to fix problems +like this: it will convert the columns to the best numeric data types. + ```{code-cell} ipython3 +:tags: ["output_scroll"] tidy_lang["most_at_home"] = pd.to_numeric(tidy_lang["most_at_home"]) tidy_lang["most_at_work"] = pd.to_numeric(tidy_lang["most_at_work"]) tidy_lang ``` ```{code-cell} ipython3 -tidy_lang.dtypes +tidy_lang.info() ``` Now we see `most_at_home` and `most_at_work` columns are of `int64` data types, @@ -911,122 +833,35 @@ indicating they are integer data types (i.e., numbers)! +++ -(loc-iloc)= -## Using `.loc[]` and `.iloc[]` to extract a range of columns - -```{index} pandas.DataFrame; loc[] -``` - -Now that the `tidy_lang` data is indeed *tidy*, we can start manipulating it -using the powerful suite of functions from the `pandas`. -For the first example, recall `.loc[]` from Chapter {ref}`intro`, -which lets us create a subset of columns from a data frame. -Suppose we wanted to select only the columns `language`, `region`, -`most_at_home` and `most_at_work` from the `tidy_lang` data set. Using what we -learned in Chapter {ref}`intro`, we would pass all of these column names into the square brackets: - -```{code-cell} ipython3 -selected_columns = tidy_lang.loc[:, ["language", "region", "most_at_home", "most_at_work"]] -selected_columns -``` - -```{index} pandas.DataFrame; iloc[], column range -``` - -Here we wrote out the names of each of the columns. However, this method is -time-consuming, especially if you have a lot of columns! Another approach is to -index with integers. `.iloc[]` make it easier for -us to select columns. For instance, we can use `.iloc[]` to choose a -range of columns rather than typing each column name out. To do this, we use the -colon (`:`) operator to denote the range. For example, to get all the columns in -the `tidy_lang` data frame from `language` to `most_at_work`, we pass `:` before the comma indicating we want to retrieve all rows, and `1:` after the comma indicating we want only columns from index 1 (*i.e.* `language`) and afterwords. - -```{code-cell} ipython3 -:tags: [remove-cell] - -# Here we wrote out the names of each of the columns. However, this method is -# time-consuming, especially if you have a lot of columns! Another approach is to -# use a "select helper". Select helpers are operators that make it easier for -# us to select columns. For instance, we can use a select helper to choose a -# range of columns rather than typing each column name out. To do this, we use the -# colon (`:`) operator to denote the range. For example, to get all the columns in \index{column range} -# the `tidy_lang` data frame from `language` to `most_at_work` we pass -# `language:most_at_work` as the second argument to the `select` function. -``` - -```{code-cell} ipython3 -column_range = tidy_lang.iloc[:, 1:] -column_range -``` - -Notice that we get the same output as we did above, -but with less (and clearer!) code. This type of operator -is especially handy for large data sets. - -```{index} pandas.Series; str.startswith -``` - -Suppose instead we wanted to extract columns that followed a particular pattern -rather than just selecting a range. For example, let's say we wanted only to select the -columns `most_at_home` and `most_at_work`. There are other functions that allow -us to select variables based on their names. In particular, we can use the `.str.startswith` method -to choose only the columns that start with the word "most": - -```{code-cell} ipython3 -tidy_lang.loc[:, tidy_lang.columns.str.startswith('most')] -``` - -```{index} pandas.Series; str.contains -``` - -We could also have chosen the columns containing an underscore `_` by using the -`.str.contains("_")`, since we notice -the columns we want contain underscores and the others don't. - -```{code-cell} ipython3 -tidy_lang.loc[:, tidy_lang.columns.str.contains('_')] -``` - -There are many different functions that help with selecting -variables based on certain criteria. -The additional resources section at the end of this chapter -provides a comprehensive resource on these functions. - -```{code-cell} ipython3 -:tags: [remove-cell] - -# There are many different `select` helpers that select -# variables based on certain criteria. -# The additional resources section at the end of this chapter -# provides a comprehensive resource on `select` helpers. -``` - -## Using `df[]` to extract rows +## Using `[]` to extract rows or columns -Next, we revisit the `df[]` from Chapter {ref}`intro`, -which lets us create a subset of rows from a data frame. -Recall the argument to the `df[]`: -column names or a logical statement evaluated to either `True` or `False`; -`df[]` works by returning the rows where the logical statement evaluates to `True`. -This section will highlight more advanced usage of the `df[]` function. +Now that the `tidy_lang` data is indeed *tidy*, we can start manipulating it +using the powerful suite of functions from the `pandas`. +We revisit the `[]` from the chapter on {ref}`intro`, +which lets us create a subset of rows from a data frame. +Recall the argument to `[]`: +a list of column names, or a logical statement that evaluates to either `True` or `False`, +where `[]` returns the rows where the logical statement evaluates to `True`. +This section will highlight more advanced usage of the `[]` function. In particular, this section provides an in-depth treatment of the variety of logical statements -one can use in the `df[]` to select subsets of rows. +one can use in the `[]` to select subsets of rows. +++ ### Extracting rows that have a certain value with `==` Suppose we are only interested in the subset of rows in `tidy_lang` corresponding to the official languages of Canada (English and French). -We can extract these rows by using the *equivalency operator* (`==`) -to compare the values of the `category` column -with the value `"Official languages"`. -With these arguments, `df[]` returns a data frame with all the columns -of the input data frame -but only the rows we asked for in the logical statement, i.e., +We can extract these rows by using the *equivalency operator* (`==`) +to compare the values of the `category` column +with the value `"Official languages"`. +With these arguments, `[]` returns a data frame with all the columns +of the input data frame +but only the rows we asked for in the logical statement, i.e., those where the `category` column holds the value `"Official languages"`. We name this data frame `official_langs`. ```{code-cell} ipython3 +:tags: ["output_scroll"] official_langs = tidy_lang[tidy_lang["category"] == "Official languages"] official_langs ``` @@ -1034,30 +869,34 @@ official_langs ### Extracting rows that do not have a certain value with `!=` What if we want all the other language categories in the data set *except* for -those in the `"Official languages"` category? We can accomplish this with the `!=` +those in the `"Official languages"` category? We can accomplish this with the `!=` operator, which means "not equal to". So if we want to find all the rows where the `category` does *not* equal `"Official languages"` we write the code below. ```{code-cell} ipython3 +:tags: ["output_scroll"] tidy_lang[tidy_lang["category"] != "Official languages"] ``` (filter-and)= ### Extracting rows satisfying multiple conditions using `&` -Suppose now we want to look at only the rows -for the French language in MontrĆ©al. -To do this, we need to filter the data set -to find rows that satisfy multiple conditions simultaneously. +Suppose now we want to look at only the rows +for the French language in MontrĆ©al. +To do this, we need to filter the data set +to find rows that satisfy multiple conditions simultaneously. We can do this with the ampersand symbol (`&`), which -is interpreted by Python as "and". -We write the code as shown below to filter the `official_langs` data frame -to subset the rows where `region == "MontrĆ©al"` -*and* the `language == "French"`. +is interpreted by Python as "and". +We write the code as shown below to filter the `official_langs` data frame +to subset the rows where `region == "MontrĆ©al"` +*and* `language == "French"`. ```{code-cell} ipython3 -tidy_lang[(tidy_lang["region"] == "MontrĆ©al") & (tidy_lang["language"] == "French")] +tidy_lang[ + (tidy_lang["region"] == "MontrĆ©al") & + (tidy_lang["language"] == "French") +] ``` +++ {"tags": []} @@ -1065,37 +904,39 @@ tidy_lang[(tidy_lang["region"] == "MontrĆ©al") & (tidy_lang["language"] == "Fren ### Extracting rows satisfying at least one condition using `|` Suppose we were interested in only those rows corresponding to cities in Alberta -in the `official_langs` data set (Edmonton and Calgary). +in the `official_langs` data set (Edmonton and Calgary). We can't use `&` as we did above because `region` -cannot be both Edmonton *and* Calgary simultaneously. -Instead, we can use the vertical pipe (`|`) logical operator, -which gives us the cases where one condition *or* -another condition *or* both are satisfied. +cannot be both Edmonton *and* Calgary simultaneously. +Instead, we can use the vertical pipe (`|`) logical operator, +which gives us the cases where one condition *or* +another condition *or* both are satisfied. In the code below, we ask Python to return the rows where the `region` columns are equal to "Calgary" *or* "Edmonton". ```{code-cell} ipython3 official_langs[ - (official_langs["region"] == "Calgary") | (official_langs["region"] == "Edmonton") + (official_langs["region"] == "Calgary") | + (official_langs["region"] == "Edmonton") ] ``` -### Extracting rows with values in a list using `.isin()` +### Extracting rows with values in a list using `isin` -Next, suppose we want to see the populations of our five cities. -Let's read in the `region_data.csv` file -that comes from the 2016 Canadian census, -as it contains statistics for number of households, land area, population +Next, suppose we want to see the populations of our five cities. +Let's read in the `region_data.csv` file +that comes from the 2016 Canadian census, +as it contains statistics for number of households, land area, population and number of dwellings for different regions. ```{code-cell} ipython3 +:tags: ["output_scroll"] region_data = pd.read_csv("data/region_data.csv") region_data ``` -To get the population of the five cities -we can filter the data set using the `.isin` method. -The `.isin` method is used to see if an element belongs to a list. +To get the population of the five cities +we can filter the data set using the `isin` method. +The `isin` method is used to see if an element belongs to a list. Here we are filtering for rows where the value in the `region` column matches any of the five cities we are intersted in: Toronto, MontrĆ©al, Vancouver, Calgary, and Edmonton. @@ -1106,7 +947,7 @@ five_cities = region_data[region_data["region"].isin(city_names)] five_cities ``` -> **Note:** What's the difference between `==` and `.isin`? Suppose we have two +> **Note:** What's the difference between `==` and `isin`? Suppose we have two > Series, `seriesA` and `seriesB`. If you type `seriesA == seriesB` into Python it > will compare the series element by element. Python checks if the first element of > `seriesA` equals the first element of `seriesB`, the second element of @@ -1114,7 +955,7 @@ five_cities > `seriesA.isin(seriesB)` compares the first element of `seriesA` to all the > elements in `seriesB`. Then the second element of `seriesA` is compared > to all the elements in `seriesB`, and so on. Notice the difference between `==` and -> `.isin` in the example below. +> `isin` in the example below. ```{code-cell} ipython3 pd.Series(["Vancouver", "Toronto"]) == pd.Series(["Toronto", "Vancouver"]) @@ -1124,25 +965,6 @@ pd.Series(["Vancouver", "Toronto"]) == pd.Series(["Toronto", "Vancouver"]) pd.Series(["Vancouver", "Toronto"]).isin(pd.Series(["Toronto", "Vancouver"])) ``` -```{code-cell} ipython3 -:tags: [remove-cell] - -# > **Note:** What's the difference between `==` and `%in%`? Suppose we have two -# > vectors, `vectorA` and `vectorB`. If you type `vectorA == vectorB` into R it -# > will compare the vectors element by element. R checks if the first element of -# > `vectorA` equals the first element of `vectorB`, the second element of -# > `vectorA` equals the second element of `vectorB`, and so on. On the other hand, -# > `vectorA %in% vectorB` compares the first element of `vectorA` to all the -# > elements in `vectorB`. Then the second element of `vectorA` is compared -# > to all the elements in `vectorB`, and so on. Notice the difference between `==` and -# > `%in%` in the example below. -# > -# >``` {r} -# >c("Vancouver", "Toronto") == c("Toronto", "Vancouver") -# >c("Vancouver", "Toronto") %in% c("Toronto", "Vancouver") -# >``` -``` - ### Extracting rows above or below a threshold using `>` and `<` ```{code-cell} ipython3 @@ -1152,1262 +974,828 @@ glue("census_popn", "{0:,.0f}".format(35151728)) glue("most_french", "{0:,.0f}".format(2669195)) ``` -We saw in Section {ref}`filter-and` that -{glue:text}`most_french` people reported -speaking French in MontrĆ©al as their primary language at home. -If we are interested in finding the official languages in regions -with higher numbers of people who speak it as their primary language at home -compared to French in MontrĆ©al, then we can use `df[]` to obtain rows -where the value of `most_at_home` is greater than -{glue:text}`most_french`. +We saw in the section on {ref}`filter-and` that +{glue:text}`most_french` people reported +speaking French in MontrĆ©al as their primary language at home. +If we are interested in finding the official languages in regions +with higher numbers of people who speak it as their primary language at home +compared to French in MontrĆ©al, then we can use `[]` to obtain rows +where the value of `most_at_home` is greater than +{glue:text}`most_french`. We use the `>` symbol to look for values *above* a threshold, +and the `<` symbol to look for values *below* a threshold. The `>=` and `<=` +symbols similarly look for *equal to or above* a threshold and *equal to or below* a threshold. ```{code-cell} ipython3 official_langs[official_langs["most_at_home"] > 2669195] ``` -This operation returns a data frame with only one row, indicating that when -considering the official languages, -only English in Toronto is reported by more people -as their primary language at home +This operation returns a data frame with only one row, indicating that when +considering the official languages, +only English in Toronto is reported by more people +as their primary language at home than French in MontrĆ©al according to the 2016 Canadian census. -+++ {"tags": []} +### Extracting rows using `query` -(pandas-assign)= -## Using `.assign` to modify or add columns +You can also extract rows above, below, equal or not-equal to a threshold using the +`query` method. For example the following gives us the same result as when we used +`official_langs[official_langs["most_at_home"] > 2669195]`. -+++ +```{code-cell} ipython3 +official_langs.query("most_at_home > 2669195") +``` -### Using `.assign` to modify columns +The query (criteria we are using to select values) is input as a string. The `query` method +is less often used than the earlier approaches we introduced, but it can come in handy +to make long chains of filtering operations a bit easier to read. -```{index} pandas.DataFrame; df[] +(loc-iloc)= +## Using `loc[]` to filter rows and select columns. +```{index} pandas.DataFrame; loc[] ``` -In Section {ref}`str-split`, -when we first read in the `"region_lang_top5_cities_messy.csv"` data, -all of the variables were "object" data types. -During the tidying process, -we used the `pandas.to_numeric` function -to convert the `most_at_home` and `most_at_work` columns -to the desired integer (i.e., numeric class) data types and then used `df[]` to overwrite columns. -But suppose we didn't use the `df[]`, -and needed to modify the columns some other way. -Below we create such a situation -so that we can demonstrate how to use `.assign` -to change the column types of a data frame. -`.assign` is a useful function to modify or create new data frame columns. +The `[]` operation is only used when you want to filter rows or select columns; +it cannot be used to do both operations at the same time. This is where `loc[]` +comes in. For the first example, recall `loc[]` from Chapter {ref}`intro`, +which lets us create a subset of columns from a data frame. +Suppose we wanted to select only the columns `language`, `region`, +`most_at_home` and `most_at_work` from the `tidy_lang` data set. Using what we +learned in the chapter on {ref}`intro`, we would pass all of these column names into the square brackets. ```{code-cell} ipython3 -lang_messy = pd.read_csv("data/region_lang_top5_cities_messy.csv") -lang_messy_longer = lang_messy.melt( - id_vars=["category", "language"], - value_vars=["Toronto", "MontrĆ©al", "Vancouver", "Calgary", "Edmonton"], - var_name="region", - value_name="value", -) -tidy_lang_obj = ( - pd.concat( - (lang_messy_longer, lang_messy_longer["value"].str.split("/", expand=True)), - axis=1, - ) - .rename(columns={0: "most_at_home", 1: "most_at_work"}) - .drop(columns=["value"]) -) -official_langs_obj = tidy_lang_obj[tidy_lang_obj["category"] == "Official languages"] - -official_langs_obj +:tags: ["output_scroll"] +selected_columns = tidy_lang.loc[:, ["language", "region", "most_at_home", "most_at_work"]] +selected_columns ``` +We pass `:` before the comma indicating we want to retrieve all rows, and the list indicates +the columns that we want. + +Note that we could obtain the same result by stating that we would like all of the columns +from `language` through `most_at_work`. Instead of passing a list of all of the column +names that we want, we can ask for the range of columns `"language":"most_at_work"`, which +you can read as "The columns from `language` to `most_at_work`". ```{code-cell} ipython3 -official_langs_obj.dtypes +:tags: ["output_scroll"] +selected_columns = tidy_lang.loc[:, "language":"most_at_work"] +selected_columns ``` -To use the `.assign` method, again we first specify the object to be the data set, -and in the following arguments, -we specify the name of the column we want to modify or create -(here `most_at_home` and `most_at_work`), an `=` sign, -and then the function we want to apply (here `pandas.to_numeric`). -In the function we want to apply, -we refer to the column upon which we want it to act -(here `most_at_home` and `most_at_work`). -In our example, we are naming the columns the same -names as columns that already exist in the data frame -("most\_at\_home", "most\_at\_work") -and this will cause `.assign` to *overwrite* those columns -(also referred to as modifying those columns *in-place*). -If we were to give the columns a new name, -then `.assign` would create new columns with the names we specified. -`.assign`'s general syntax is detailed in {numref}`fig:img-assign`. - -+++ {"tags": []} - -```{figure} img/wrangling/pandas_assign_args_labels.png -:name: fig:img-assign -:figclass: caption-hack +Similarly, you can ask for all of the columns including and after `language` by doing the following -Syntax for the `.assign` function. +```{code-cell} ipython3 +:tags: ["output_scroll"] +selected_columns = tidy_lang.loc[:, "language":] +selected_columns ``` -+++ +By not putting anything after the `:`, python reads this as "from `language` until the last column". +Although the notation for selecting a range using `:` is convienent because less code is required, +it must be used carefully. If you were to re-order columns or add a column to the data frame, the +output would change. Using a list is more explicit and less prone to potential confusion. -Below we use `.assign` to convert the columns `most_at_home` and `most_at_work` -to numeric data types in the `official_langs` data set as described in -{numref}`fig:img-assign`: +Suppose instead we wanted to extract columns that followed a particular pattern +rather than just selecting a range. For example, let's say we wanted only to select the +columns `most_at_home` and `most_at_work`. There are other functions that allow +us to select variables based on their names. In particular, we can use the `.str.startswith` method +to choose only the columns that start with the word "most": ```{code-cell} ipython3 -official_langs_numeric = official_langs_obj.assign( - most_at_home=pd.to_numeric(official_langs_obj["most_at_home"]), - most_at_work=pd.to_numeric(official_langs_obj["most_at_work"]), -) - -official_langs_numeric +tidy_lang.loc[:, tidy_lang.columns.str.startswith('most')] ``` -```{code-cell} ipython3 -official_langs_numeric.dtypes +```{index} pandas.Series; str.contains ``` -Now we see that the `most_at_home` and `most_at_work` columns are both `int64` (which is a numeric data type)! +We could also have chosen the columns containing an underscore `_` by using the +`.str.contains("_")`, since we notice +the columns we want contain underscores and the others don't. -+++ +```{code-cell} ipython3 +tidy_lang.loc[:, tidy_lang.columns.str.contains('_')] +``` -### Using `.assign` to create new columns +There are many different functions that help with selecting +variables based on certain criteria. +The additional resources section at the end of this chapter +provides a comprehensive resource on these functions. ```{code-cell} ipython3 :tags: [remove-cell] -number_most_home = int( - official_langs[ - (official_langs["language"] == "English") - & (official_langs["region"] == "Toronto") - ]["most_at_home"] -) - -toronto_popn = int(region_data[region_data["region"] == "Toronto"]["population"]) - -glue("number_most_home", "{0:,.0f}".format(number_most_home)) -glue("toronto_popn", "{0:,.0f}".format(toronto_popn)) -glue("prop_eng_tor", "{0:.2f}".format(number_most_home / toronto_popn)) +# There are many different `select` helpers that select +# variables based on certain criteria. +# The additional resources section at the end of this chapter +# provides a comprehensive resource on `select` helpers. ``` -We can see in the table that -{glue:text}`number_most_home` people reported -speaking English in Toronto as their primary language at home, according to -the 2016 Canadian census. What does this number mean to us? To understand this -number, we need context. In particular, how many people were in Toronto when -this data was collected? From the 2016 Canadian census profile, the population -of Toronto was reported to be -{glue:text}`toronto_popn` people. -The number of people who report that English is their primary language at home -is much more meaningful when we report it in this context. -We can even go a step further and transform this count to a relative frequency -or proportion. -We can do this by dividing the number of people reporting a given language -as their primary language at home by the number of people who live in Toronto. -For example, -the proportion of people who reported that their primary language at home -was English in the 2016 Canadian census was {glue:text}`prop_eng_tor` -in Toronto. - -Let's use `.assign` to create a new column in our data frame -that holds the proportion of people who speak English -for our five cities of focus in this chapter. -To accomplish this, we will need to do two tasks -beforehand: - -1. Create a list containing the population values for the cities. -2. Filter the `official_langs` data frame -so that we only keep the rows where the language is English. - -To create a list containing the population values for the five cities -(Toronto, MontrĆ©al, Vancouver, Calgary, Edmonton), -we will use the `[]` (recall that we can also use `list()` to create a list): - -```{code-cell} ipython3 -city_pops = [5928040, 4098927, 2463431, 1392609, 1321426] -city_pops +## Using `iloc[]` to extract a range of columns +```{index} pandas.DataFrame; iloc[], column range ``` - -And next, we will filter the `official_langs` data frame -so that we only keep the rows where the language is English. -We will name the new data frame we get from this `english_langs`: +Another approach for selecting columns is to use `iloc[]`, +which provides the ability to index with integers rather than the names of the columns. +For example, the column names of the `tidy_lang` data frame are +`['category', 'language', 'region', 'most_at_home', 'most_at_work']`. +Using `iloc[]`, you can ask for the `language` column by requesting the +column at index `1` (remember that Python starts counting at `0`, so the second item `'language'` +has index `1`!). ```{code-cell} ipython3 -english_langs = official_langs[official_langs["language"] == "English"] -english_langs +column = tidy_lang.iloc[:, 1] +column ``` -Finally, we can use `.assign` to create a new column, -named `most_at_home_proportion`, that will have value that corresponds to -the proportion of people reporting English as their primary -language at home. -We will compute this by dividing the column by our vector of city populations. +You can also ask for multiple columns, just like we did with `[]`. We pass `:` before +the comma, indicating we want to retrieve all rows, and `1:` after the comma +indicating we want columns after and including index 1 (*i.e.* `language`). ```{code-cell} ipython3 -english_langs = english_langs.assign( - most_at_home_proportion=english_langs["most_at_home"] / city_pops -) - -english_langs +column_range = tidy_lang.iloc[:, 1:] +column_range ``` -In the computation above, we had to ensure that we ordered the `city_pops` vector in the -same order as the cities were listed in the `english_langs` data frame. -This is because Python will perform the division computation we did by dividing -each element of the `most_at_home` column by each element of the -`city_pops` list, matching them up by position. -Failing to do this would have resulted in the incorrect math being performed. +The `iloc[]` method is less commonly used, and needs to be used with care. +For example, it is easy to +accidentally put in the wrong integer index! If you did not correctly remember +that the `language` column was index `1`, and used `2` instead, your code +would end up having a bug that might be quite hard to track down. -> **Note:** In more advanced data wrangling, -> one might solve this problem in a less error-prone way though using -> a technique called "joins". -> We link to resources that discuss this in the additional -> resources at the end of this chapter. +```{index} pandas.Series; str.startswith +``` -+++ ++++ {"tags": []} - +## Aggregating data +++ -## Combining functions by chaining the methods +### Calculating summary statistics on individual columns -```{index} chaining methods +```{index} summarize ``` -In Python, we often have to call multiple methods in a sequence to process a data -frame. The basic ways of doing this can become quickly unreadable if there are -many steps. For example, suppose we need to perform three operations on a data -frame called `data`: +As a part of many data analyses, we need to calculate a summary value for the +data (a *summary statistic*). +Examples of summary statistics we might want to calculate +are the number of observations, the average/mean value for a column, +the minimum value, etc. +Oftentimes, +this summary statistic is calculated from the values in a data frame column, +or columns, as shown in {numref}`fig:summarize`. -1) add a new column `new_col` that is double another `old_col`, -2) filter for rows where another column, `other_col`, is more than 5, and -3) select only the new column `new_col` for those rows. ++++ {"tags": []} -One way of performing these three steps is to just write -multiple lines of code, storing temporary objects as you go: +```{figure} img/summarize/summarize.001.jpeg +:name: fig:summarize +:figclass: figure -```{code-cell} ipython3 -:tags: [remove-cell] +Calculating summary statistics on one or more column(s) in `pandas` generally +creates a series or data frame containing the summary statistic(s) for each column +being summarized. The darker, top row of each table represents column headers. +``` -# ## Combining functions using the pipe operator, `|>` ++++ -# In R, we often have to call multiple functions in a sequence to process a data -# frame. The basic ways of doing this can become quickly unreadable if there are -# many steps. For example, suppose we need to perform three operations on a data -# frame called `data`: \index{pipe}\index{aaapipesymb@\vert{}>|see{pipe}} -``` +We will start by showing how to compute the minimum and maximum number of Canadians reporting a particular +language as their primary language at home. First, a reminder of what `region_lang` looks like: ```{code-cell} ipython3 -:tags: [remove-cell] - -data = pd.DataFrame({"old_col": [1, 2, 5, 0], "other_col": [1, 10, 3, 6]}) -``` - -```{code-cell} ipython3 -:tags: [remove-output] - -output_1 = data.assign(new_col=data["old_col"] * 2) -output_2 = output_1[output_1["other_col"] > 5] -output = output_2.loc[:, "new_col"] +:tags: ["output_scroll"] +region_lang = pd.read_csv("data/region_lang.csv") +region_lang ``` -This is difficult to understand for multiple reasons. The reader may be tricked -into thinking the named `output_1` and `output_2` objects are important for some -reason, while they are just temporary intermediate computations. Further, the -reader has to look through and find where `output_1` and `output_2` are used in -each subsequent line. - -+++ - -Chaining the sequential functions solves this problem, resulting in cleaner and -easier-to-follow code. -The code below accomplishes the same thing as the previous -two code blocks: +We use `.min` to calculate the minimum +and `.max` to calculate maximum number of Canadians +reporting a particular language as their primary language at home, +for any region. ```{code-cell} ipython3 -:tags: [remove-output] - -output = ( - data.assign(new_col=data["old_col"] * 2) - .query("other_col > 5") - .loc[:, "new_col"] -) +region_lang["most_at_home"].min() ``` ```{code-cell} ipython3 -:tags: [remove-cell] - -# ``` {r eval = F} -# output <- select(filter(mutate(data, new_col = old_col * 2), -# other_col > 5), -# new_col) -# ``` -# Code like this can also be difficult to understand. Functions compose (reading -# from left to right) in the *opposite order* in which they are computed by R -# (above, `mutate` happens first, then `filter`, then `select`). It is also just a -# really long line of code to read in one go. - -# The *pipe operator* (`|>`) solves this problem, resulting in cleaner and -# easier-to-follow code. `|>` is built into R so you don't need to load any -# packages to use it. -# You can think of the pipe as a physical pipe. It takes the output from the -# function on the left-hand side of the pipe, and passes it as the first argument -# to the function on the right-hand side of the pipe. -# The code below accomplishes the same thing as the previous -# two code blocks: -``` - -> **Note:** You might also have noticed that we split the function calls across -> lines, similar to when we did this earlier in the chapter -> for long function calls. Again, this is allowed and recommended, especially when -> the chained function calls create a long line of code. Doing this makes -> your code more readable. When you do this, it is important to use parentheses -> to tell Python that your code is continuing onto the next line. +region_lang["most_at_home"].max() +``` ```{code-cell} ipython3 :tags: [remove-cell] - -# > **Note:** You might also have noticed that we split the function calls across -# > lines after the pipe, similar to when we did this earlier in the chapter -# > for long function calls. Again, this is allowed and recommended, especially when -# > the piped function calls create a long line of code. Doing this makes -# > your code more readable. When you do this, it is important to end each line -# > with the pipe operator `|>` to tell R that your code is continuing onto the -# > next line. - -# > **Note:** In this textbook, we will be using the base R pipe operator syntax, `|>`. -# > This base R `|>` pipe operator was inspired by a previous version of the pipe -# > operator, `%>%`. The `%>%` pipe operator is not built into R -# > and is from the `magrittr` R package. -# > The `tidyverse` metapackage imports the `%>%` pipe operator via `dplyr` -# > (which in turn imports the `magrittr` R package). -# > There are some other differences between `%>%` and `|>` related to -# > more advanced R uses, such as sharing and distributing code as R packages, -# > however, these are beyond the scope of this textbook. -# > We have this note in the book to make the reader aware that `%>%` exists -# > as it is still commonly used in data analysis code and in many data science -# > books and other resources. -# > In most cases these two pipes are interchangeable and either can be used. - -# \index{pipe}\index{aaapipesymbb@\%>\%|see{pipe}} -``` - -### Chaining `df[]` and `.loc` - -+++ - -Let's work with the tidy `tidy_lang` data set from Section {ref}`str-split`, -which contains the number of Canadians reporting their primary language at home -and work for five major cities -(Toronto, MontrĆ©al, Vancouver, Calgary, and Edmonton): - -```{code-cell} ipython3 -tidy_lang +glue("lang_most_people", "{0:,.0f}".format(int(region_lang["most_at_home"].max()))) ``` -Suppose we want to create a subset of the data with only the languages and -counts of each language spoken most at home for the city of Vancouver. To do -this, we can use the `df[]` and `.loc`. First, we use `df[]` to -create a data frame called `van_data` that contains only values for Vancouver. - +From this we see that there are some languages in the data set that no one speaks +as their primary language at home. We also see that the most commonly spoken +primary language at home is spoken by +{glue:text}`lang_most_people` people. If instead we wanted to know the +total number of people in the survey, we could use the `sum` summary statistic method. ```{code-cell} ipython3 -van_data = tidy_lang[tidy_lang["region"] == "Vancouver"] -van_data +region_lang["most_at_home"].sum() ``` -We then use `.loc` on this data frame to keep only the variables we want: +Other handy summary statistics include the `mean`, `median` and `std` for +computing the mean, median, and standard deviation of observations, respectively. +We can also compute multiple statistics at once using `agg` to "aggregate" results. +For example, if we wanted to +compute both the `min` and `max` at once, we could use `agg` with the argument `['min', 'max']`. +Note that `agg` outputs a `Series` object. ```{code-cell} ipython3 -van_data_selected = van_data.loc[:, ["language", "most_at_home"]] -van_data_selected +region_lang["most_at_home"].agg(["min", "max"]) ``` -Although this is valid code, there is a more readable approach we could take by -chaining the operations. With chaining, we do not need to create an intermediate -object to store the output from `df[]`. Instead, we can directly call `.loc` upon the -output of `df[]`: +The `pandas` package also provides the `describe` method, +which is a handy function that computes many common summary statistics at once; it +gives us a *summary* of a variable. ```{code-cell} ipython3 -van_data_selected = tidy_lang[tidy_lang["region"] == "Vancouver"].loc[ - :, ["language", "most_at_home"] -] - -van_data_selected +region_lang["most_at_home"].describe() ``` -```{code-cell} ipython3 -:tags: [remove-cell] +In addition to the summary methods we introduced earlier, the `describe` method +outputs a `count` (the total number of observations, or rows, in our data frame), +as well as the 25th, 50th, and 75th percentiles. +{numref}`tab:basic-summary-statistics` provides an overview of some of the useful +summary statistics that you can compute with `pandas`. -# But wait...Why do the `select` and `filter` function calls -# look different in these two examples? -# Remember: when you use the pipe, -# the output of the first function is automatically provided -# as the first argument for the function that comes after it. -# Therefore you do not specify the first argument in that function call. -# In the code above, -# the first line is just the `tidy_lang` data frame with a pipe. -# The pipe passes the left-hand side (`tidy_lang`) to the first argument of the function on the right (`filter`), -# so in the `filter` function you only see the second argument (and beyond). -# Then again after `filter` there is a pipe, which passes the result of the `filter` step -# to the first argument of the `select` function. +```{table} Basic summary statistics +:name: tab:basic-summary-statistics +| Function | Description | +| -------- | ----------- | +| `count` | The number of observations (rows) | +| `mean` | The mean of the observations | +| `median` | The median value of the observations | +| `std` | The standard deviation of the observations | +| `max` | The largest value in a column | +| `min` | The smallest value in a column | +| `sum` | The sum of all observations | +| `agg` | Aggregate multiple statistics together | +| `describe` | a summary | ``` -As you can see, both of these approaches—with and without chaining—give us the same output, but the second -approach is clearer and more readable. - +++ - -### Chaining more than two functions - +++ -Chaining can be used with any method in Python. -Additionally, we can chain together more than two functions. -For example, we can chain together three functions to: - -- extract rows (`df[]`) to include only those where the counts of the language most spoken at home are greater than 10,000, -- extract only the columns (`.loc`) corresponding to `region`, `language` and `most_at_home`, and -- sort the data frame rows in order (`.sort_values`) by counts of the language most spoken at home -from smallest to largest. -```{index} pandas.DataFrame; sort_values -``` +> **Note:** In `pandas`, the value `NaN` is often used to denote missing data. +> By default, when `pandas` calculates summary statistics (e.g., `max`, `min`, `sum`, etc), +> it ignores these values. If you look at the documentation for these functions, you will +> see an input variable `skipna`, which by default is set to `skipna=True`. This means that +> `pandas` will skip `NaN` values when computing statistics. -As we saw in Chapter {ref}`intro`, -we can use the `.sort_values` function -to order the rows in the data frame by the values of one or more columns. -Here we pass the column name `most_at_home` to sort the data frame rows by the values in that column, in ascending order. +### Calculating summary statistics on data frames +What if you want to calculate summary statistics on an entire data frame? Well, +it turns out that the functions in {numref}`tab:basic-summary-statistics` +can be applied to a whole data frame! +For example, we can ask for the number of rows that each column has using `count`. ```{code-cell} ipython3 -large_region_lang = ( - tidy_lang[tidy_lang["most_at_home"] > 10000] - .loc[:, ["region", "language", "most_at_home"]] - .sort_values(by="most_at_home") -) - -large_region_lang +region_lang.count() ``` - +Not surprisingly, they are all the same. We could also ask for the `mean`, but +some of the columns in `region_lang` contain string data with words like `"Vancouver"` +and `"Halifax"`---for these columns there is no way for `pandas` to compute the mean. +So we provide the keyword `numeric_only=True` so that it only computes the mean of columns with numeric values. This +is also needed if you want the `sum` or `std`. ```{code-cell} ipython3 -:tags: [remove-cell] - -# You will notice above that we passed `tidy_lang` as the first argument of the `filter` function. -# We can also pipe the data frame into the same sequence of functions rather than -# using it as the first argument of the first function. These two choices are equivalent, -# and we get the same result. -# ``` {r} -# large_region_lang <- tidy_lang |> -# filter(most_at_home > 10000) |> -# select(region, language, most_at_home) |> -# arrange(most_at_home) - -# large_region_lang -# ``` -``` - -Now that we've shown you chaining as an alternative to storing -temporary objects and composing code, does this mean you should *never* store -temporary objects or compose code? Not necessarily! -There are times when you will still want to do these things. -For example, you might store a temporary object before feeding it into a plot function -so you can iteratively change the plot without having to -redo all of your data transformations. -Additionally, chaining many functions can be overwhelming and difficult to debug; -you may want to store a temporary object midway through to inspect your result -before moving on with further steps. - -+++ - -## Aggregating data with `.assign`, `.agg` and `.apply` - -+++ - -### Calculating summary statistics on whole columns - -```{index} summarize -``` - -As a part of many data analyses, we need to calculate a summary value for the -data (a *summary statistic*). -Examples of summary statistics we might want to calculate -are the number of observations, the average/mean value for a column, -the minimum value, etc. -Oftentimes, -this summary statistic is calculated from the values in a data frame column, -or columns, as shown in {numref}`fig:summarize`. - -+++ {"tags": []} - -```{figure} img/summarize/summarize.001.jpeg -:name: fig:summarize -:figclass: caption-hack - -Calculating summary statistics on one or more column(s). In its simplest use case, it creates a new data frame with a single row containing the summary statistic(s) for each column being summarized. The darker, top row of each table represents the column headers. +region_lang.mean(numeric_only=True) ``` - -+++ - -We can use `.assign` as mentioned in Section {ref}`pandas-assign` along with proper summary functions to create a aggregated column. - -First a reminder of what `region_lang` looks like: - +If we ask for the `min` or the `max`, `pandas` will give you the smallest or largest number +for columns with numeric values. For columns with text, it will return the +least repeated value for `min` and the most repeated value for `max`. Again, +if you only want the minimum and maximum value for +numeric columns, you can provide `numeric_only=True`. ```{code-cell} ipython3 -:tags: [remove-cell] - -# A useful `dplyr` function for calculating summary statistics is `summarize`, -# where the first argument is the data frame and subsequent arguments -# are the summaries we want to perform. -# Here we show how to use the `summarize` function to calculate the minimum -# and maximum number of Canadians -# reporting a particular language as their primary language at home. -# First a reminder of what `region_lang` looks like: +region_lang.max() ``` - ```{code-cell} ipython3 -region_lang = pd.read_csv("data/region_lang.csv") -region_lang +region_lang.min() ``` -We apply `min` to calculate the minimum -and `max` to calculate maximum number of Canadians -reporting a particular language as their primary language at home, -for any region, and `.assign` a column name to each: - -```{code-cell} ipython3 -:tags: [remove-cell] +Similarly, if there are only some columns for which you would like to get summary statistics, +you can first use `loc[]` and then ask for the summary statistic. An example of this is illustrated in {numref}`fig:summarize-across`. +Later, we will talk about how you can also use a more general function, `apply`, to accomplish this. -pd.DataFrame(region_lang["most_at_home"].agg(["min", "max"])).T +```{figure} img/summarize/summarize.003.jpeg +:name: fig:summarize-across +:figclass: figure -# pd.DataFrame(region_lang["most_at_home"].agg(["min", "max"])).T.rename( -# columns={"min": "min_most_at_home", "max": "max_most_at_home"} -# ) +`loc[]` or `apply` is useful for efficiently calculating summary statistics on +many columns at once. The darker, top row of each table represents the column +headers. ``` +Lets say that we want to know +the mean and standard deviation of all of the columns between `"mother_tongue"` and `"lang_known"`. +We use `loc[]` to specify the columns and then `agg` to ask for both the `mean` and `std`. ```{code-cell} ipython3 -:tags: [] - -lang_summary = pd.DataFrame() -lang_summary = lang_summary.assign(min_most_at_home=[min(region_lang["most_at_home"])]) -lang_summary = lang_summary.assign(max_most_at_home=[max(region_lang["most_at_home"])]) -lang_summary +region_lang.loc[:, "mother_tongue":"lang_known"].agg(["mean", "std"]) ``` -```{code-cell} ipython3 -:tags: [remove-cell] -glue("lang_most_people", "{0:,.0f}".format(int(lang_summary["max_most_at_home"]))) -``` -From this we see that there are some languages in the data set that no one speaks -as their primary language at home. We also see that the most commonly spoken -primary language at home is spoken by -{glue:text}`lang_most_people` -people. +## Performing operations on groups of rows using `groupby` +++ -### Calculating summary statistics when there are `NaN`s - -```{index} missing data +```{index} pandas.DataFrame; groupby ``` +What happens if we want to know how languages vary by region? In this case, +we need a new tool that lets us group rows by region. This can be achieved +using the `groupby` function in `pandas`. Pairing summary functions +with `groupby` lets you summarize values for subgroups within a data set, +as illustrated in {numref}`fig:summarize-groupby`. +For example, we can use `groupby` to group the regions of the `tidy_lang` data +frame and then calculate the minimum and maximum number of Canadians +reporting the language as the primary language at home +for each of the regions in the data set. + ++++ {"tags": []} + +```{figure} img/summarize/summarize.002.jpeg +:name: fig:summarize-groupby +:figclass: figure -```{index} see: NaN; missing data +A summary statistic function paired with `groupby` is useful for calculating that statistic +on one or more column(s) for each group. It +creates a new data frame with one row for each group +and one column for each summary statistic.The darker, top row of each table +represents the column headers. The gray, blue, and green colored rows +correspond to the rows that belong to each of the three groups being +represented in this cartoon example. ``` -In `pandas` DataFrame, the value `NaN` is often used to denote missing data. -Many of the base python statistical summary functions -(e.g., `max`, `min`, `sum`, etc) will return `NaN` -when applied to columns containing `NaN` values. -Usually that is not what we want to happen; -instead, we would usually like Python to ignore the missing entries -and calculate the summary statistic using all of the other non-`NaN` values -in the column. -Fortunately `pandas` provides many equivalent methods (e.g., `.max`, `.min`, `.sum`, etc) to -these summary functions while providing an extra argument `skipna` that lets -us tell the function what to do when it encounters `NaN` values. -In particular, if we specify `skipna=True` (default), the function will ignore -missing values and return a summary of all the non-missing entries. -We show an example of this below. ++++ -First we create a new version of the `region_lang` data frame, -named `region_lang_na`, that has a seemingly innocuous `NaN` -in the first row of the `most_at_home` column: +The `groupby` function takes at least one argument—the columns to use in the +grouping. Here we use only one column for grouping (`region`). ```{code-cell} ipython3 -:tags: [remove-cell] - -# In data frames in R, the value `NA` is often used to denote missing data. -# Many of the base R statistical summary functions -# (e.g., `max`, `min`, `mean`, `sum`, etc) will return `NA` -# when applied to columns containing `NA` values. \index{missing data}\index{NA|see{missing data}} -# Usually that is not what we want to happen; -# instead, we would usually like R to ignore the missing entries -# and calculate the summary statistic using all of the other non-`NA` values -# in the column. -# Fortunately many of these functions provide an argument `na.rm` that lets -# us tell the function what to do when it encounters `NA` values. -# In particular, if we specify `na.rm = TRUE`, the function will ignore -# missing values and return a summary of all the non-missing entries. -# We show an example of this combined with `summarize` below. +region_lang.groupby("region")["most_at_home"].agg(["min", "max"]) ``` +Notice that `groupby` converts a `DataFrame` object to a `DataFrameGroupBy` +object, which contains information about the groups of the data frame. We can +then apply aggregating functions to the `DataFrameGroupBy` object. This can be handy if you would like to perform multiple operations and assign +each output to its own object. ```{code-cell} ipython3 -:tags: [remove-cell] - -region_lang_na = region_lang.copy() -region_lang_na.loc[0, "most_at_home"] = np.nan +region_lang.groupby("region") ``` +You can also pass multiple column names to `groupby`. For example, if we wanted to +know about how the different categories of languages (Aboriginal, Non-Official & +Non-Aboriginal, and Official) are spoken at home in different regions, we would pass a +list including `region` and `category` to `groupby`. ```{code-cell} ipython3 -region_lang_na +region_lang.groupby(["region", "category"])["most_at_home"].agg(["min", "max"]) ``` -Now if we apply the Python built-in summary function as above, -we see that we no longer get the minimum and maximum returned, -but just an `NaN` instead! - +You can also ask for grouped summary statistics on the whole data frame ```{code-cell} ipython3 -lang_summary_na = pd.DataFrame() -lang_summary_na = lang_summary_na.assign( - min_most_at_home=[min(region_lang_na["most_at_home"])] -) -lang_summary_na = lang_summary_na.assign( - max_most_at_home=[max(region_lang_na["most_at_home"])] -) -lang_summary_na +:tags: ["output_scroll"] +region_lang.groupby("region").agg(["min", "max"]) ``` -We can fix this by using the `pandas` Series methods (*i.e.* `.min` and `.max`) with `skipna=True` as explained above: - +If you want to ask for only some columns, for example +the columns between `"most_at_home"` and `"lang_known"`, +you might think about first applying `groupby` and then `loc`; +but `groupby` returns a `DataFrameGroupBy` object, which does not +work with `loc`. The other option is to do things the other way around: +first use `loc`, then use `groupby`. +This usually does work, but you have to be careful! For example, +in our case, if we try using `loc` and then `groupby`, we get an error. ```{code-cell} ipython3 -lang_summary_na = pd.DataFrame() -lang_summary_na = lang_summary_na.assign( - min_most_at_home=[region_lang_na["most_at_home"].min(skipna=True)] -) -lang_summary_na = lang_summary_na.assign( - max_most_at_home=[region_lang_na["most_at_home"].max(skipna=True)] -) -lang_summary_na +:tags: [remove-output] +region_lang.loc[:, "most_at_home":"lang_known"].groupby("region").max() +``` +``` +KeyError: 'region' +``` +This is because when we use `loc` we selected only the columns between +`"most_at_home"` and `"lang_known"`, which doesn't include `"region"`! +Instead, we need to call `loc` with a list of column names that +includes `region`, and then use `groupby`. +```{code-cell} ipython3 +:tags: ["output_scroll"] +region_lang.loc[ + :, + ["region", "mother_tongue", "most_at_home", "most_at_work", "lang_known"] +].groupby("region").max() ``` - -### Calculating summary statistics for groups of rows +++ -```{index} pandas.DataFrame; groupby -``` - -A common pairing with summary functions is `.groupby`. Pairing these functions -together can let you summarize values for subgroups within a data set, -as illustrated in {numref}`fig:summarize-groupby`. -For example, we can use `.groupby` to group the regions of the `tidy_lang` data frame and then calculate the minimum and maximum number of Canadians -reporting the language as the primary language at home -for each of the regions in the data set. +## Apply functions across multiple columns with `apply` -```{code-cell} ipython3 -:tags: [remove-cell] +### Apply a function to each column with `apply` -# A common pairing with `summarize` is `group_by`. Pairing these functions \index{group\_by} -# together can let you summarize values for subgroups within a data set, -# as illustrated in Figure \@ref(fig:summarize-groupby). -# For example, we can use `group_by` to group the regions of the `tidy_lang` data frame and then calculate the minimum and maximum number of Canadians -# reporting the language as the primary language at home -# for each of the regions in the data set. +An alternative to aggregating on a data frame +for applying a function to many columns is the `apply` method. +Let's again find the maximum value of each column of the +`region_lang` data frame, but using `apply` with the `max` function this time. +We focus on the two arguments of `apply`: +the function that you would like to apply to each column, and the `axis` along +which the function will be applied (`0` for columns, `1` for rows). +Note that `apply` does not have an argument +to specify *which* columns to apply the function to. +Therefore, we will use the `loc[]` before calling `apply` +to choose the columns for which we want the maximum. -# (ref:summarize-groupby) `summarize` and `group_by` is useful for calculating summary statistics on one or more column(s) for each group. It creates a new data frame—with one row for each group—containing the summary statistic(s) for each column being summarized. It also creates a column listing the value of the grouping variable. The darker, top row of each table represents the column headers. The gray, blue, and green colored rows correspond to the rows that belong to each of the three groups being represented in this cartoon example. +```{code-cell} ipython3 +region_lang.loc[:, "most_at_home":"most_at_work"].apply(max) ``` +We can use `apply` for much more than summary statistics. +Sometimes we need to apply a function to many columns in a data frame. +For example, we would need to do this when converting units of measurements across many columns. +We illustrate such a data transformation in {numref}`fig:mutate-across`. + +++ {"tags": []} -```{figure} img/summarize/summarize.002.jpeg -:name: fig:summarize-groupby -:figclass: caption-hack +```{figure} img/summarize/summarize.005.jpeg +:name: fig:mutate-across +:figclass: figure -Calculating summary statistics on one or more column(s) for each group. It creates a new data frame—with one row for each group—containing the summary statistic(s) for each column being summarized. It also creates a column listing the value of the grouping variable. The darker, top row of each table represents the column headers. The gray, blue, and green colored rows correspond to the rows that belong to each of the three groups being represented in this cartoon example. +`apply` is useful for applying functions across many columns. The darker, top row of each table represents the column headers. ``` +++ -The `.groupby` function takes at least one argument—the columns to use in the -grouping. Here we use only one column for grouping (`region`), but more than one -can also be used. To do this, pass a list of column names to the `by` argument. +For example, +imagine that we wanted to convert all the numeric columns +in the `region_lang` data frame from `int64` type to `int32` type +using the `.as_type` function. +When we revisit the `region_lang` data frame, +we can see that this would be the columns from `mother_tongue` to `lang_known`. ```{code-cell} ipython3 -region_summary = pd.DataFrame() -region_summary = region_summary.assign( - min_most_at_home=region_lang.groupby(by="region")["most_at_home"].min(), - max_most_at_home=region_lang.groupby(by="region")["most_at_home"].max() -).reset_index() - -region_summary.columns = ["region", "min_most_at_home", "max_most_at_home"] -region_summary +:tags: ["output_scroll"] +region_lang ``` -`pandas` also has a convenient method `.agg` (shorthand for `.aggregate`) that allows us to apply multiple aggregate functions in one line of code. We just need to pass in a list of function names to `.agg` as shown below. +```{index} pandas.DataFrame; apply, pandas.DataFrame; loc[] +``` +To accomplish such a task, we can use `apply`. +As we did above, +we again use `loc[]` to specify the columns +as well as the `apply` to specify the function we want to apply on these columns. +Now, we need a way to tell `apply` what function to perform to each column +so that we can convert them from `int64` to `int32`. We will use what is called +a `lambda` function in python; `lambda` functions are just regular functions, +except that you don't need to give them a name. +That means you can pass them as an argument into `apply` easily! +Let's consider a simple example of a `lambda` function that +multiplies a number by two. ```{code-cell} ipython3 -region_summary = ( - region_lang.groupby(by="region")["most_at_home"].agg(["min", "max"]).reset_index() -) -region_summary.columns = ["region", "min_most_at_home", "max_most_at_home"] -region_summary +lambda x: 2*x ``` - -Notice that `.groupby` converts a `DataFrame` object to a `DataFrameGroupBy` object, which contains information about the groups of the dataframe. We can then apply aggregating functions to the `DataFrameGroupBy` object. - +We define a `lambda` function in the following way. We start with the syntax `lambda`, which is a special word +that tells Python "what follows is +a function." Following this, we then state the name of the arguments of the function. +In this case, we just have one argument named `x`. After the list of arguments, we put a +colon `:`. And finally after the colon are the instructions: take the value provided and multiply it by 2. +Let's call our shiny new `lambda` function with the argument `2` (so the output should be `4`). +Just like a regular function, we pass its argument between parentheses `()` symbols. ```{code-cell} ipython3 -:tags: [remove-cell] - -# Notice that `group_by` on its own doesn't change the way the data looks. -# In the output below, the grouped data set looks the same, -# and it doesn't *appear* to be grouped by `region`. -# Instead, `group_by` simply changes how other functions work with the data, -# as we saw with `summarize` above. +(lambda x: 2*x)(2) ``` +> **Note:** Because we didn't give the `lambda` function a name, we have to surround it with +> parentheses too if we want to call it. Otherwise, if we wrote something like `lambda x: 2*x(2)`, Python would get confused +> and think that `(2)` was part of the instructions that comprise the `lambda` function. +> As long as we don't want to call the `lambda` function ourselves, we don't need those parentheses. For example, +> we can pass a `lambda` function as an argument to `apply` without any parentheses. +Returning to our example, let's use `apply` to convert the columns `"mother_tongue":"lang_known"` +to `int32`. To accomplish this we create a `lambda` function that takes one argument---a single column +of the data frame, which we will name `col`---and apply the `astype` method to it. +Then the `apply` method will use that `lambda` function on every column we specify via `loc[]`. ```{code-cell} ipython3 -region_lang.groupby("region") +region_lang_nums = region_lang.loc[:, "mother_tongue":"lang_known"].apply(lambda col: col.astype("int32")) +region_lang_nums.info() ``` +You can now see that the columns from `mother_tongue` to `lang_known` are type `int32`. +You can also see that `apply` returns a data frame with the same number of columns and rows +as the input data frame. The only thing `apply` does is use the `lambda` function argument +on each of the specified columns. -### Calculating summary statistics on many columns +### Apply a function row-wise with `apply` -+++ +What if you want to apply a function across columns but within one row? +We illustrate such a data transformation in {numref}`fig:rowwise`. -Sometimes we need to summarize statistics across many columns. -An example of this is illustrated in {numref}`fig:summarize-across`. -In such a case, using summary functions alone means that we have to -type out the name of each column we want to summarize. -In this section we will meet two strategies for performing this task. -First we will see how we can do this using `.iloc[]` to slice the columns before applying summary functions. -Then we will also explore how we can use a more general iteration function, -`.apply`, to also accomplish this. ++++ {"tags": []} -```{code-cell} ipython3 -:tags: [remove-cell] +```{figure} img/summarize/summarize.004.jpeg +:name: fig:rowwise +:figclass: figure -# Sometimes we need to summarize statistics across many columns. -# An example of this is illustrated in Figure \@ref(fig:summarize-across). -# In such a case, using `summarize` alone means that we have to -# type out the name of each column we want to summarize. -# In this section we will meet two strategies for performing this task. -# First we will see how we can do this using `summarize` + `across`. -# Then we will also explore how we can use a more general iteration function, -# `map`, to also accomplish this. +`apply` is useful for applying functions across columns within one row. The +darker, top row of each table represents the column headers. ``` -+++ {"tags": []} ++++ -```{figure} img/summarize/summarize.003.jpeg -:name: fig:summarize-across -:figclass: caption-hack +For instance, suppose we want to know the maximum value between `mother_tongue`, +and `lang_known` for each language and region +in the `region_lang_nums` data set. +In other words, we want to apply the `max` function *row-wise.* +In order to tell `apply` that we want to work row-wise (as opposed to acting on each column +individually, which is the default behavior), we just specify the argument `axis=1`. +For example, in the case of the `max` function, this tells Python that we would like +the `max` within each row of the input, as opposed to being applied on each column. -`.iloc[]` or `.apply` is useful for efficiently calculating summary statistics on many columns at once. The darker, top row of each table represents the column headers. +```{code-cell} ipython3 +region_lang_nums.apply(max, axis=1) ``` -+++ +We see that we get a column, which is the maximum value between `mother_tongue`, +`most_at_home`, `most_at_work` and `lang_known` for each language +and region. It is often the case that we want to include a column result +from using `apply` row-wise as a new column in the data frame, so that we can make +plots or continue our analysis. To make this happen, +we will use `assign` to create a new column. This is discussed in the next section. -#### Aggregating on a data frame for calculating summary statistics on many columns +(pandas-assign)= +## Using `assign` to modify or add columns -+++ -```{index} column range +```{index} pandas.DataFrame; [] ``` -Recall that in the Section {ref}`loc-iloc`, we can use `.iloc[]` to extract a range of columns with indices. Here we demonstrate finding the maximum value -of each of the numeric -columns of the `region_lang` data set through pairing `.iloc[]` and `.max`. This means that the -summary methods (*e.g.* `.min`, `.max`, `.sum` etc.) can be used for data frames as well. +### Using `assign` to create new columns -```{code-cell} ipython3 -pd.DataFrame(region_lang.iloc[:, 3:].max(axis=0)).T -``` +When we compute summary statistics with `agg` or apply functions using `apply` +those give us new data frames. But what if we want to append that information +to an existing data frame? This is where we make use of the `assign` method. +For example, say we wanted the maximum values of the `region_lang_nums` data frame, +and to create a new data frame consisting of all the columns of `region_lang` as well as that additional column. +To do this, we will (1) compute the maximum of those columns using `apply`, +and (2) use `assign` to assign values to create a new column in the `region_lang` data frame. +Note that `assign` does not by default modify the data frame itself; it creates a copy +with the new column added to it. +To use the `assign` method, we specify one argument for each column we want to create. +In this case we want to create one new column named `maximum`, so the argument +to `assign` begins with `maximum = `. +Then after the `=`, we specify what the contents of that new column +should be. In this case we use `apply` just as we did in the previous section to give us the maximum values. +Remember to specify `axis=1` in the `apply` method so that we compute the row-wise maximum value. ```{code-cell} ipython3 ---- -jupyter: - source_hidden: true -tags: [remove-cell] ---- -# To summarize statistics across many columns, we can use the -# `summarize` function we have just recently learned about. -# However, in such a case, using `summarize` alone means that we have to -# type out the name of each column we want to summarize. -# To do this more efficiently, we can pair `summarize` with `across` \index{across} -# and use a colon `:` to specify a range of columns we would like \index{column range} -# to perform the statistical summaries on. -# Here we demonstrate finding the maximum value -# of each of the numeric -# columns of the `region_lang` data set. - -# ``` {r 02-across-data} -# region_lang |> -# summarize(across(mother_tongue:lang_known, max)) -# ``` - -# > **Note:** Similar to when we use base R statistical summary functions -# > (e.g., `max`, `min`, `mean`, `sum`, etc) with `summarize` alone, -# > the use of the `summarize` + `across` functions paired -# > with base R statistical summary functions -# > also return `NA`s when we apply them to columns that -# > contain `NA`s in the data frame. \index{missing data} -# > -# > To avoid this, again we need to add the argument `na.rm = TRUE`, -# > but in this case we need to use it a little bit differently. -# > In this case, we need to add a `,` and then `na.rm = TRUE`, -# > after specifying the function we want `summarize` + `across` to apply, -# > as illustrated below: -# > -# > ``` {r} -# > region_lang_na |> -# > summarize(across(mother_tongue:lang_known, max, na.rm = TRUE)) -# > ``` -``` - -(apply-summary)= -#### `.apply` for calculating summary statistics on many columns - -+++ - -```{index} pandas.DataFrame; apply +:tags: ["output_scroll"] +region_lang.assign( + maximum = region_lang_nums.apply(max, axis=1) +) ``` +This gives us a new data frame that looks like the `region_lang` data frame, +except that it has an additional column named `maximum`. +The `maximum` column contains +the maximum value between `mother_tongue`, +`most_at_home`, `most_at_work` and `lang_known` for each language +and region, just as we specified! -An alternative to aggregating on a dataframe -for applying a function to many columns is the `.apply` method. -Let's again find the maximum value of each column of the -`region_lang` data frame, but using `.apply` with the `max` function this time. -We focus on the two arguments of `.apply`: -the function that you would like to apply to each column, and the `axis` along which the function will be applied (`0` for columns, `1` for rows). -Note that `.apply` does not have an argument -to specify *which* columns to apply the function to. -Therefore, we will use the `.iloc[]` before calling `.apply` -to choose the columns for which we want the maximum. ```{code-cell} ipython3 ---- -jupyter: - source_hidden: true -tags: [remove-cell] ---- -# An alternative to `summarize` and `across` -# for applying a function to many columns is the `map` family of functions. \index{map} -# Let's again find the maximum value of each column of the -# `region_lang` data frame, but using `map` with the `max` function this time. -# `map` takes two arguments: -# an object (a vector, data frame or list) that you want to apply the function to, -# and the function that you would like to apply to each column. -# Note that `map` does not have an argument -# to specify *which* columns to apply the function to. -# Therefore, we will use the `select` function before calling `map` -# to choose the columns for which we want the maximum. -``` - -```{code-cell} ipython3 -pd.DataFrame(region_lang.iloc[:, 3:].apply(max, axis=0)).T -``` - -```{index} missing data -``` - -> **Note:** Similar to when we use base Python statistical summary functions -> (e.g., `max`, `min`, `sum`, etc.) when there are `NaN`s, -> `.apply` functions paired with base Python statistical summary functions -> also return `NaN` values when we apply them to columns that -> contain `NaN` values. -> -> To avoid this, again we need to use the `pandas` variants of summary functions (*i.e.* -> `.max`, `.min`, `.sum`, etc.) with `skipna=True`. -> When we use this with `.apply`, we do this by constructing a anonymous function that calls -> the `.max` method with `skipna=True`, as illustrated below: - -```{code-cell} ipython3 -pd.DataFrame( - region_lang_na.iloc[:, 3:].apply(lambda col: col.max(skipna=True), axis=0) -).T -``` - -The `.apply` function is generally quite useful for solving many problems -involving repeatedly applying functions in Python. -Additionally, a variant of `.apply` is `.applymap`, -which can be used to apply functions element-wise. -To learn more about these functions, see the additional resources -section at the end of this chapter. - -+++ {"jp-MarkdownHeadingCollapsed": true, "tags": ["remove-cell"]} - - - -+++ {"tags": []} - -## Apply functions across many columns with `.apply` - -Sometimes we need to apply a function to many columns in a data frame. -For example, we would need to do this when converting units of measurements across many columns. -We illustrate such a data transformation in {numref}`fig:mutate-across`. +:tags: [remove-cell] -+++ {"tags": []} +number_most_home = int( + official_langs[ + (official_langs["language"] == "English") & + (official_langs["region"] == "Toronto") + ]["most_at_home"] +) -```{figure} img/summarize/summarize.005.jpeg -:name: fig:mutate-across -:figclass: caption-hack +toronto_popn = int(region_data[region_data["region"] == "Toronto"]["population"]) -`.apply` is useful for applying functions across many columns. The darker, top row of each table represents the column headers. +glue("number_most_home", "{0:,.0f}".format(number_most_home)) +glue("toronto_popn", "{0:,.0f}".format(toronto_popn)) +glue("prop_eng_tor", "{0:.2f}".format(number_most_home / toronto_popn)) ``` -+++ - -For example, -imagine that we wanted to convert all the numeric columns -in the `region_lang` data frame from `int64` type to `int32` type -using the `.as_type` function. -When we revisit the `region_lang` data frame, -we can see that this would be the columns from `mother_tongue` to `lang_known`. +As another example, we might ask the question: "What proportion of +the population reported English as their primary language at home in the 2016 census?" +For example, in Toronto, {glue:text}`number_most_home` people reported +speaking English as their primary language at home, and the +population of Toronto was reported to be +{glue:text}`toronto_popn` people. So the proportion of people reporting English +as their primary language in Toronto in the 2016 census was {glue:text}`prop_eng_tor`. +How could we figure this out starting from the `region_lang` data frame? -```{code-cell} ipython3 ---- -jupyter: - source_hidden: true -tags: [remove-cell] ---- -# For example, -# imagine that we wanted to convert all the numeric columns -# in the `region_lang` data frame from double type to integer type -# using the `as.integer` function. -# When we revisit the `region_lang` data frame, -# we can see that this would be the columns from `mother_tongue` to `lang_known`. +First, we need to filter the `region_lang` data frame +so that we only keep the rows where the language is English. +We will also restrict our attention to the five major cities +in the `five_cities` data frame: Toronto, MontrĆ©al, Vancouver, Calgary, and Edmonton. +We will filter to keep only those rows pertaining to the English language +and pertaining to the five aforementioned cities. To combine these two logical statements +we will use the `&` symbol. +and with the `[]` operation, + `"English"` as the `language` and filter the rows, +and name the new data frame `english_langs`. +```{code-cell} ipython3 +:tags: ["output_scroll"] +english_lang = region_lang[ + (region_lang["language"] == "English") & + (region_lang["region"].isin(five_cities["region"])) +] +english_lang ``` +Okay, now we have a data frame that pertains only to the English language +and the five cities mentioned earlier. +In order to compute the proportion of the population speaking English in each of these cities, +we need to add the population data from the `five_cities` data frame. ```{code-cell} ipython3 -region_lang +five_cities ``` - -```{index} pandas.DataFrame; apply, pandas.DataFrame; iloc[] +The data frame above shows that the populations of the five cities in 2016 were +5928040 (Toronto), 4098927 (MontrĆ©al), 2463431 (Vancouver), 1392609 (Calgary), and 1321426 (Edmonton). +We will add this information to our data frame in a new column named `city_pops` by using `assign`. +Once again we specify the new column name (`city_pops`) as the argument, followed by the equal symbol `=`, +and finally the data in the column. +Note that the order of the rows in the `english_lang` data frame is MontrĆ©al, Toronto, Calgary, Edmonton, Vancouver. +So we will create a column called `city_pops` where we list the populations of those cities in that +order, and add it to our data frame. +Also note that we write `english_lang = ` on the left so that the newly created data frame overwrites our +old `english_lang` data frame; remember that by default, like other `pandas` functions, `assign` does not +modify the original data frame directly! +```{code-cell} ipython3 +:tags: ["output_scroll"] +english_lang = english_lang.assign( + city_pops=[4098927, 5928040, 1392609, 1321426, 2463431] +) +english_lang +``` +> **Note**: Inserting data manually in this is generally very error-prone and is not recommended. +> We do it here to demonstrate another usage of `assign` that does not involve `apply`. +> But in more advanced data wrangling, +> one would solve this problem in a less error-prone way using +> the `merge` function, which lets you combine two data frames. We will show you an +> example using `merge` at the end of the chapter! + +Now we have a new column with the population for each city. Finally, we calculate the +proportion of people who speak English the most at home by taking the ratio of the columns +`most_at_home` and `city_pops`. We will again add this to our data frame using `assign`. +```{code-cell} ipython3 +:tags: ["output_scroll"] +english_lang.assign( + proportion=english_lang["most_at_home"]/english_lang["city_pops"] +) ``` -To accomplish such a task, we can use `.apply`. -This works in a similar way for column selection, -as we saw when we used in Section {ref}`apply-summary` earlier. -As we did above, -we again use `.iloc` to specify the columns -as well as the `.apply` to specify the function we want to apply on these columns. -However, a key difference here is that we are not using aggregating function here, -which means that we get back a data frame with the same number of rows. - -```{code-cell} ipython3 ---- -jupyter: - source_hidden: true -tags: [remove-cell] ---- -# To accomplish such a task, we can use `mutate` paired with `across`. \index{across} -# This works in a similar way for column selection, -# as we saw when we used `summarize` + `across` earlier. -# As we did above, -# we again use `across` to specify the columns using `select` syntax -# as well as the function we want to apply on the specified columns. -# However, a key difference here is that we are using `mutate`, -# which means that we get back a data frame with the same number of rows. -``` -```{code-cell} ipython3 -region_lang.dtypes -``` ++++ -```{code-cell} ipython3 -region_lang_int32 = region_lang.iloc[:, 3:].apply(lambda col: col.astype('int32'), axis=0) -region_lang_int32 = pd.concat((region_lang.iloc[:, :3], region_lang_int32), axis=1) -region_lang_int32 -``` -```{code-cell} ipython3 -region_lang_int32.dtypes -``` +### Using `assign` to modify columns -We see that we get back a data frame -with the same number of columns and rows. -The only thing that changes is the transformation we applied -to the specified columns (here `mother_tongue` to `lang_known`). -+++ +In the section on {ref}`str-split`, +when we first read in the `"region_lang_top5_cities_messy.csv"` data, +all of the variables were "object" data types. +During the tidying process, +we used the `pandas.to_numeric` function +to convert the `most_at_home` and `most_at_work` columns +to the desired integer (i.e., numeric class) data types and then used `[]` to overwrite columns. +We can do the same thing using `assign`. + +Below we use `assign` to convert the columns `most_at_home` and `most_at_work` +to numeric data types in the `official_langs` data set as described in +{numref}`fig:img-assign`. In our example, we are naming the columns the same +names as columns that already exist in the data frame +(`"most_at_home"`, `"most_at_work"`) +and this will cause `assign` to *overwrite* those columns +(also referred to as modifying those columns *in-place*). +If we were to give the columns a new name, +then `assign` would create new columns with the names we specified. +The syntax is detailed in {numref}`fig:img-assign`. -## Apply functions across columns within one row with `.apply` +```{code-cell} ipython3 +:tags: ["output_scroll"] +official_langs_numeric = official_langs.assign( + most_at_home=pd.to_numeric(official_langs["most_at_home"]), + most_at_work=pd.to_numeric(official_langs["most_at_work"]), +) -What if you want to apply a function across columns but within one row? -We illustrate such a data transformation in {numref}`fig:rowwise`. +official_langs_numeric +``` +++ {"tags": []} -```{figure} img/summarize/summarize.004.jpeg -:name: fig:rowwise -:figclass: caption-hack +```{figure} img/wrangling/pandas_assign_args_labels.png +:name: fig:img-assign +:figclass: figure -`.apply` is useful for applying functions across columns within one row. The darker, top row of each table represents the column headers. +Syntax for the `assign` function. ``` +++ -For instance, suppose we want to know the maximum value between `mother_tongue`, -`most_at_home`, `most_at_work` -and `lang_known` for each language and region -in the `region_lang` data set. -In other words, we want to apply the `max` function *row-wise.* -Before we use `.apply`, we will again use `.iloc` to select only the count columns -so we can see all the columns in the data frame's output easily in the book. -So for this demonstration, the data set we are operating on looks like this: ```{code-cell} ipython3 ---- -jupyter: - source_hidden: true -tags: [remove-cell] ---- -# For instance, suppose we want to know the maximum value between `mother_tongue`, -# `most_at_home`, `most_at_work` -# and `lang_known` for each language and region -# in the `region_lang` data set. -# In other words, we want to apply the `max` function *row-wise.* -# We will use the (aptly named) `rowwise` function in combination with `mutate` -# to accomplish this task. - -# Before we apply `rowwise`, we will `select` only the count columns \index{rowwise} -# so we can see all the columns in the data frame's output easily in the book. -# So for this demonstration, the data set we are operating on looks like this: +official_langs_numeric.info() ``` +Now we see that the `most_at_home` and `most_at_work` columns are both `int64` (which is a numeric data type)! +Note that we were careful here and created a new data frame object `official_langs_numeric`. Since `assign` has +the power to overwrite the entries of a column, it is a good idea to create a new data frame object so that if +you make a mistake, you can start again from the original data frame. + ++++ + + +### Using `assign` to create a new data frame + ```{code-cell} ipython3 -region_lang.iloc[:, 3:] -``` +:tags: [remove-cell] -Now we use `.apply` with argument `axis=1`, to tell Python that we would like -the `max` function to be applied across, and within, a row, -as opposed to being applied on a column -(which is the default behavior of `.apply`): +english_lang = region_lang[region_lang["language"] == "English"] +five_cities = ["Toronto", "MontrĆ©al", "Vancouver", "Calgary", "Edmonton"] +english_lang = english_lang[english_lang["region"].isin(five_cities)] +english_lang +``` +Sometimes you want to create a new data frame. You can use `assign` to create a data frame from scratch. +Lets return to the example of wanting to compute the proportions of people who speak English +most at home in Toronto, MontrĆ©al, Vancouver, Calgary, Edmonton. Before adding new columns, we filtered +our `region_lang` to create the `english_lang` data frame containing only English speakers in the five cities +of interest. ```{code-cell} ipython3 ---- -jupyter: - source_hidden: true -tags: [remove-cell] ---- -# Now we apply `rowwise` before `mutate`, to tell R that we would like -# the mutate function to be applied across, and within, a row, -# as opposed to being applied on a column -# (which is the default behavior of `mutate`): +:tags: ["output_scroll"] +english_lang ``` +We then wanted to add the populations of these cities as a column using `assign` +(Toronto: 5928040, MontrĆ©al: 4098927, Vancouver: 2463431, +Calgary: 1392609, and Edmonton: 1321426). We had to be careful to add those populations in the +right order, and it could be easy to make a mistake this way. An alternative approach, that we demonstrate here +is to (1) create a new, empty data frame, (2) use `assign` to assign the city names and populations in that +data frame, and (3) use `merge` to combine the two data frames, recognizing that the "regions" are the same. +We create a new, empty data frame by calling `pd.DataFrame` with no arguments. +We then use `assign` to add the city names in a column called `"region"` +and their populations in a column called `"population"`. ```{code-cell} ipython3 -region_lang_rowwise = region_lang.assign( - maximum=region_lang.iloc[:, 3:].apply(max, axis=1) +city_populations = pd.DataFrame().assign( + region=["Toronto", "MontrĆ©al", "Vancouver", "Calgary", "Edmonton"], + population=[5928040, 4098927, 2463431, 1392609, 1321426] ) - -region_lang_rowwise +city_populations ``` - -We see that we get an additional column added to the data frame, -named `maximum`, which is the maximum value between `mother_tongue`, -`most_at_home`, `most_at_work` and `lang_known` for each language -and region. - +This new data frame has the same `region` column as the `english_lang` data frame. The order of +the cities is different, but that is okay! We can use the `merge` function in `pandas` to say +we would like to combine the two data frames by matching the `region` between them. The argument +`on="region"` tells pandas we would like to use the `region` column to match up the entries. ```{code-cell} ipython3 ---- -jupyter: - source_hidden: true -tags: [remove-cell] ---- -# Similar to `group_by`, -# `rowwise` doesn't appear to do anything when it is called by itself. -# However, we can apply `rowwise` in combination -# with other functions to change how these other functions operate on the data. -# Notice if we used `mutate` without `rowwise`, -# we would have computed the maximum value across *all* rows -# rather than the maximum value for *each* row. -# Below we show what would have happened had we not used -# `rowwise`. In particular, the same maximum value is reported -# in every single row; this code does not provide the desired result. - -# ```{r} -# region_lang |> -# select(mother_tongue:lang_known) |> -# mutate(maximum = max(c(mother_tongue, -# most_at_home, -# most_at_home, -# lang_known))) -# ``` +:tags: ["output_scroll"] +english_lang = english_lang.merge(city_populations, on="region") +english_lang ``` +You can see that the populations for each city are correct (e.g. MontrĆ©al: 4098927, Toronto: 5928040), +and we could proceed to with our analysis from here. ## Summary -Cleaning and wrangling data can be a very time-consuming process. However, +Cleaning and wrangling data can be a very time-consuming process. However, it is a critical step in any data analysis. We have explored many different -functions for cleaning and wrangling data into a tidy format. -{numref}`tab:summary-functions-table` summarizes some of the key wrangling -functions we learned in this chapter. In the following chapters, you will -learn how you can take this tidy data and do so much more with it to answer your +functions for cleaning and wrangling data into a tidy format. +{numref}`tab:summary-functions-table` summarizes some of the key wrangling +functions we learned in this chapter. In the following chapters, you will +learn how you can take this tidy data and do so much more with it to answer your burning data science questions! +++ -```{table} Summary of wrangling functions +```{table} Summary of wrangling functions :name: tab:summary-functions-table | Function | Description | -| --- | ----------- | -| `.agg` | calculates aggregated summaries of inputs | -| `.apply` | allows you to apply function(s) to multiple columns/rows | -| `.assign` | adds or modifies columns in a data frame | -| `.groupby` | allows you to apply function(s) to groups of rows | -| `.iloc` | subsets columns/rows of a data frame using integer indices | -| `.loc` | subsets columns/rows of a data frame using labels | -| `.melt` | generally makes the data frame longer and narrower | -| `.pivot` | generally makes a data frame wider and decreases the number of rows | -| `.str.split` | splits up a string column into multiple columns | -``` - -```{code-cell} ipython3 ---- -jupyter: - source_hidden: true -tags: [remove-cell] ---- -# ## Summary - -# Cleaning and wrangling data can be a very time-consuming process. However, -# it is a critical step in any data analysis. We have explored many different -# functions for cleaning and wrangling data into a tidy format. -# Table \@ref(tab:summary-functions-table) summarizes some of the key wrangling -# functions we learned in this chapter. In the following chapters, you will -# learn how you can take this tidy data and do so much more with it to answer your -# burning data science questions! - -# \newpage - -# Table: (#tab:summary-functions-table) Summary of wrangling functions - -# | Function | Description | -# | --- | ----------- | -# | `across` | allows you to apply function(s) to multiple columns | -# | `filter` | subsets rows of a data frame | -# | `group_by` | allows you to apply function(s) to groups of rows | -# | `mutate` | adds or modifies columns in a data frame | -# | `map` | general iteration function | -# | `pivot_longer` | generally makes the data frame longer and narrower | -# | `pivot_wider` | generally makes a data frame wider and decreases the number of rows | -# | `rowwise` | applies functions across columns within one row | -# | `separate` | splits up a character column into multiple columns | -# | `select` | subsets columns of a data frame | -# | `summarize` | calculates summaries of inputs | +| --- | ----------- | +| `agg` | calculates aggregated summaries of inputs | +| `apply` | allows you to apply function(s) to multiple columns/rows | +| `assign` | adds or modifies columns in a data frame | +| `groupby` | allows you to apply function(s) to groups of rows | +| `iloc` | subsets columns/rows of a data frame using integer indices | +| `loc` | subsets columns/rows of a data frame using labels | +| `melt` | generally makes the data frame longer and narrower | +| `merge` | combine two data frames | +| `pivot` | generally makes a data frame wider and decreases the number of rows | +| `str.split` | splits up a string column into multiple columns | ``` ## Exercises -Practice exercises for the material covered in this chapter -can be found in the accompanying -[worksheets repository](https://github.com/UBC-DSCI/data-science-a-first-intro-worksheets#readme) +Practice exercises for the material covered in this chapter +can be found in the accompanying +[worksheets repository](https://github.com/UBC-DSCI/data-science-a-first-intro-python-worksheets#readme) in the "Cleaning and wrangling data" row. You can launch an interactive version of the worksheet in your browser by clicking the "launch binder" button. You can also preview a non-interactive version of the worksheet by clicking "view worksheet." If you instead decide to download the worksheet and run it on your own machine, make sure to follow the instructions for computer setup -found in Chapter {ref}`move-to-your-own-machine`. This will ensure that the automated feedback +found in the chapter on {ref}`move-to-your-own-machine`. This will ensure that the automated feedback and guidance that the worksheets provide will function as intended. +++ {"tags": []} -## Additional resources +## Additional resources - The [`pandas` package documentation](https://pandas.pydata.org/docs/reference/index.html) is another resource to learn more about the functions in this @@ -2417,58 +1805,15 @@ and guidance that the worksheets provide will function as intended. - *Python for Data Analysis* {cite:p}`mckinney2012python` has a few chapters related to data wrangling that go into more depth than this book. For example, the [data wrangling chapter](https://wesmckinney.com/book/data-wrangling.html) covers tidy data, - `.melt` and `.pivot`, but also covers missing values - and additional wrangling functions (like `.stack`). The [data + `melt` and `pivot`, but also covers missing values + and additional wrangling functions (like `stack`). The [data aggregation chapter](https://wesmckinney.com/book/data-aggregation.html) covers - `.groupby`, aggregating functions, `.apply`, etc. + `groupby`, aggregating functions, `apply`, etc. - You will occasionally encounter a case where you need to iterate over items in a data frame, but none of the above functions are flexible enough to do what you want. In that case, you may consider using [a for loop](https://wesmckinney.com/book/python-basics.html#control_for) {cite:p}`mckinney2012python`. -```{code-cell} ipython3 ---- -jp-MarkdownHeadingCollapsed: true -jupyter: - source_hidden: true -tags: [remove-cell] ---- -# ## Additional resources - -# - As we mentioned earlier, `tidyverse` is actually an *R -# meta package*: it installs and loads a collection of R packages that all -# follow the tidy data philosophy we discussed above. One of the `tidyverse` -# packages is `dplyr`—a data wrangling workhorse. You have already met many -# of `dplyr`'s functions -# (`select`, `filter`, `mutate`, `arrange`, `summarize`, and `group_by`). -# To learn more about these functions and meet a few more useful -# functions, we recommend you check out Chapters 5-9 of the [STAT545 online notes](https://stat545.com/). -# of the data wrangling, exploration, and analysis with R book. -# - The [`dplyr` R package documentation](https://dplyr.tidyverse.org/) [@dplyr] is -# another resource to learn more about the functions in this -# chapter, the full set of arguments you can use, and other related functions. -# The site also provides a very nice cheat sheet that summarizes many of the -# data wrangling functions from this chapter. -# - Check out the [`tidyselect` R package page](https://tidyselect.r-lib.org/index.html) -# [@tidyselect] for a comprehensive list of `select` helpers. -# These helpers can be used to choose columns in a data frame when paired with the `select` function -# (and other functions that use the `tidyselect` syntax, such as `pivot_longer`). -# The [documentation for `select` helpers](https://tidyselect.r-lib.org/reference/select_helpers.html) -# is a useful reference to find the helper you need for your particular problem. -# - *R for Data Science* [@wickham2016r] has a few chapters related to -# data wrangling that go into more depth than this book. For example, the -# [tidy data chapter](https://r4ds.had.co.nz/tidy-data.html) covers tidy data, -# `pivot_longer`/`pivot_wider` and `separate`, but also covers missing values -# and additional wrangling functions (like `unite`). The [data -# transformation chapter](https://r4ds.had.co.nz/transform.html) covers -# `select`, `filter`, `arrange`, `mutate`, and `summarize`. And the [`map` -# functions chapter](https://r4ds.had.co.nz/iteration.html#the-map-functions) -# provides more about the `map` functions. -# - You will occasionally encounter a case where you need to iterate over items -# in a data frame, but none of the above functions are flexible enough to do -# what you want. In that case, you may consider using [a for -# loop](https://r4ds.had.co.nz/iteration.html#iteration). -``` ## References @@ -2476,4 +1821,4 @@ tags: [remove-cell] ```{bibliography} :filter: docname in docnames -``` \ No newline at end of file +``` diff --git a/unused/install/conda-linux-64.lock b/unused/install/conda-linux-64.lock deleted file mode 100644 index ba3c46fd..00000000 --- a/unused/install/conda-linux-64.lock +++ /dev/null @@ -1,270 +0,0 @@ -# Generated by conda-lock. -# platform: linux-64 -# input_hash: 89f580fffc52744967507b06c57aba086f847ee041e119e07deb7ff6508a1608 -@EXPLICIT -https://conda.anaconda.org/t//conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2#d7c89558ba9fa0495403155b64376d81 -https://conda.anaconda.org/t//conda-forge/linux-64/ca-certificates-2021.10.8-ha878542_0.tar.bz2#575611b8a84f45960e87722eeb51fa26 -https://conda.anaconda.org/t//conda-forge/linux-64/ld_impl_linux-64-2.36.1-hea4e1c9_2.tar.bz2#bd4f2e711b39af170e7ff15163fe87ee -https://conda.anaconda.org/t//conda-forge/linux-64/libgfortran5-11.2.0-h5c6108e_16.tar.bz2#ff034874d96195a5c5be34200689b5b7 -https://conda.anaconda.org/t//conda-forge/linux-64/libstdcxx-ng-11.2.0-he4da1e4_16.tar.bz2#8cfd1cd3273ff187be91b868ddf9a636 -https://conda.anaconda.org/t//conda-forge/linux-64/pandoc-2.18-ha770c72_0.tar.bz2#518b07342786b362238d22f76789ed59 -https://conda.anaconda.org/t//conda-forge/noarch/pybind11-abi-4-hd8ed1ab_3.tar.bz2#878f923dd6acc8aeb47a75da6c4098be -https://conda.anaconda.org/t//conda-forge/noarch/tzdata-2022a-h191b570_0.tar.bz2#84be5301069417a2221187d2f435e0f7 -https://conda.anaconda.org/t//conda-forge/linux-64/libgfortran-ng-11.2.0-h69a702a_16.tar.bz2#27974aad841c189854df09426b1b9fac -https://conda.anaconda.org/t//conda-forge/linux-64/libgomp-11.2.0-h1d223b6_16.tar.bz2#e935fb0c92c6ffb63c736a2012604d72 -https://conda.anaconda.org/t//conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2#73aaf86a425cc6e73fcf236a5a46396d -https://conda.anaconda.org/t//conda-forge/linux-64/libgcc-ng-11.2.0-h1d223b6_16.tar.bz2#71feb63a30085cbce51847d5ef1f769d -https://conda.anaconda.org/t//conda-forge/linux-64/bzip2-1.0.8-h7f98852_4.tar.bz2#a1fd65c7ccbf10880423d82bca54eb54 -https://conda.anaconda.org/t//conda-forge/linux-64/c-ares-1.18.1-h7f98852_0.tar.bz2#f26ef8098fab1f719c91eb760d63381a -https://conda.anaconda.org/t//conda-forge/linux-64/expat-2.4.8-h27087fc_0.tar.bz2#e1b07832504eeba765d648389cc387a9 -https://conda.anaconda.org/t//conda-forge/linux-64/icu-70.1-h27087fc_0.tar.bz2#87473a15119779e021c314249d4b4aed -https://conda.anaconda.org/t//conda-forge/linux-64/keyutils-1.6.1-h166bdaf_0.tar.bz2#30186d27e2c9fa62b45fb1476b7200e3 -https://conda.anaconda.org/t//conda-forge/linux-64/libev-4.33-h516909a_1.tar.bz2#6f8720dff19e17ce5d48cfe7f3d2f0a3 -https://conda.anaconda.org/t//conda-forge/linux-64/libffi-3.4.2-h7f98852_5.tar.bz2#d645c6d2ac96843a2bfaccd2d62b3ac3 -https://conda.anaconda.org/t//conda-forge/linux-64/libiconv-1.16-h516909a_0.tar.bz2#5c0f338a513a2943c659ae619fca9211 -https://conda.anaconda.org/t//conda-forge/linux-64/libnsl-2.0.0-h7f98852_0.tar.bz2#39b1328babf85c7c3a61636d9cd50206 -https://conda.anaconda.org/t//conda-forge/linux-64/libopenblas-0.3.20-pthreads_h78a6416_0.tar.bz2#9b6d0781953c9e353faee494336cc229 -https://conda.anaconda.org/t//conda-forge/linux-64/libsodium-1.0.18-h36c2ea0_1.tar.bz2#c3788462a6fbddafdb413a9f9053e58d -https://conda.anaconda.org/t//conda-forge/linux-64/libuuid-2.32.1-h7f98852_1000.tar.bz2#772d69f030955d9646d3d0eaf21d859d -https://conda.anaconda.org/t//conda-forge/linux-64/libuv-1.43.0-h7f98852_0.tar.bz2#b34d856aa7e06ebd79bded72ef4afc16 -https://conda.anaconda.org/t//conda-forge/linux-64/libzlib-1.2.11-h166bdaf_1014.tar.bz2#757138ba3ddc6777b82e91d9ff62e7b9 -https://conda.anaconda.org/t//conda-forge/linux-64/lz4-c-1.9.3-h9c3ff4c_1.tar.bz2#fbe97e8fa6f275d7c76a09e795adc3e6 -https://conda.anaconda.org/t//conda-forge/linux-64/lzo-2.10-h516909a_1000.tar.bz2#bb14fcb13341b81d5eb386423b9d2bac -https://conda.anaconda.org/t//conda-forge/linux-64/ncurses-6.3-h27087fc_1.tar.bz2#4acfc691e64342b9dae57cf2adc63238 -https://conda.anaconda.org/t//conda-forge/linux-64/openssl-1.1.1o-h166bdaf_0.tar.bz2#6172048796b123e542945d998f5150b7 -https://conda.anaconda.org/t//conda-forge/linux-64/pcre-8.45-h9c3ff4c_0.tar.bz2#c05d1820a6d34ff07aaaab7a9b7eddaa -https://conda.anaconda.org/t//conda-forge/linux-64/reproc-14.2.3-h7f98852_0.tar.bz2#1e16d4142b016b6a5ebdeb3d6d33aaf4 -https://conda.anaconda.org/t//conda-forge/linux-64/xz-5.2.5-h516909a_1.tar.bz2#33f601066901f3e1a85af3522a8113f9 -https://conda.anaconda.org/t//conda-forge/linux-64/yaml-0.2.5-h7f98852_2.tar.bz2#4cb3ad778ec2d5a7acbdf254eb1c42ae -https://conda.anaconda.org/t//conda-forge/linux-64/yaml-cpp-0.6.3-he1b5a44_4.tar.bz2#8e873da49d14b584bed5a09084a68136 -https://conda.anaconda.org/t//conda-forge/linux-64/gettext-0.19.8.1-h73d1719_1008.tar.bz2#af49250eca8e139378f8ff0ae9e57251 -https://conda.anaconda.org/t//conda-forge/linux-64/libblas-3.9.0-14_linux64_openblas.tar.bz2#fb31fbbde682414550bbe15e3964420f -https://conda.anaconda.org/t//conda-forge/linux-64/libedit-3.1.20191231-he28a2e2_2.tar.bz2#4d331e44109e3f0e19b4cb8f9b82f3e1 -https://conda.anaconda.org/t//conda-forge/linux-64/libsolv-0.7.22-h6239696_0.tar.bz2#461963bb499e58bae159a898600f8792 -https://conda.anaconda.org/t//conda-forge/linux-64/perl-5.32.1-2_h7f98852_perl5.tar.bz2#09ba115862623f00962e9809ea248f1a -https://conda.anaconda.org/t//conda-forge/linux-64/readline-8.1-h46c0cb4_0.tar.bz2#5788de3c8d7a7d64ac56c784c4ef48e6 -https://conda.anaconda.org/t//conda-forge/linux-64/reproc-cpp-14.2.3-h9c3ff4c_0.tar.bz2#1fc15d3b393b62192d3eeade92b61610 -https://conda.anaconda.org/t//conda-forge/linux-64/tk-8.6.12-h27826a3_0.tar.bz2#5b8c42eb62e9fc961af70bdd6a26e168 -https://conda.anaconda.org/t//conda-forge/linux-64/zeromq-4.3.4-h9c3ff4c_1.tar.bz2#21743a8d2ea0c8cfbbf8fe489b0347df -https://conda.anaconda.org/t//conda-forge/linux-64/zlib-1.2.11-h166bdaf_1014.tar.bz2#def3b82d1a03aa695bb38ac1dd072ff2 -https://conda.anaconda.org/t//conda-forge/linux-64/zstd-1.5.2-ha95c52a_0.tar.bz2#5222b231b1ef49a7f60d40b363469b70 -https://conda.anaconda.org/t//conda-forge/linux-64/krb5-1.19.3-h3790be6_0.tar.bz2#7d862b05445123144bec92cb1acc8ef8 -https://conda.anaconda.org/t//conda-forge/linux-64/libcblas-3.9.0-14_linux64_openblas.tar.bz2#1b41ea4c32014d878e84de4e5690df7a -https://conda.anaconda.org/t//conda-forge/linux-64/libglib-2.70.2-h174f98d_4.tar.bz2#d44314ffae96b17657fbf3f8e47b04fc -https://conda.anaconda.org/t//conda-forge/linux-64/liblapack-3.9.0-14_linux64_openblas.tar.bz2#13367ebd0243a949cee7564b13c3cd42 -https://conda.anaconda.org/t//conda-forge/linux-64/libnghttp2-1.47.0-h727a467_0.tar.bz2#a22567abfea169ff8048506b1ca9b230 -https://conda.anaconda.org/t//conda-forge/linux-64/libssh2-1.10.0-ha56f1ee_2.tar.bz2#6ab4eaa11ff01801cffca0a27489dc04 -https://conda.anaconda.org/t//conda-forge/linux-64/libxml2-2.9.14-h22db469_0.tar.bz2#7d623237b73d93dd856b5dd0f5fedd6b -https://conda.anaconda.org/t//conda-forge/linux-64/nodejs-17.9.0-h96d913c_0.tar.bz2#760b5a71dbaaf438e83824f55b2dbb9e -https://conda.anaconda.org/t//conda-forge/linux-64/pcre2-10.37-h032f7d1_0.tar.bz2#6469e4602e914febe6f057ad2271a54e -https://conda.anaconda.org/t//conda-forge/linux-64/sqlite-3.38.5-h4ff8645_0.tar.bz2#a1448f0c31baec3946d2dcf09f905c9e -https://conda.anaconda.org/t//conda-forge/linux-64/dbus-1.13.6-h5008d03_3.tar.bz2#ecfff944ba3960ecb334b9a2663d708d -https://conda.anaconda.org/t//conda-forge/linux-64/libarchive-3.5.2-hccf745f_1.tar.bz2#c777ce221e0f3f1aade66074405d042e -https://conda.anaconda.org/t//conda-forge/linux-64/libcurl-7.83.0-h7bff187_0.tar.bz2#e2d939fa77fe69cd50f751961f17786a -https://conda.anaconda.org/t//conda-forge/linux-64/libxslt-1.1.33-h8affb1d_4.tar.bz2#47437b917a43a9650f0d14d7a54753a4 -https://conda.anaconda.org/t//conda-forge/linux-64/python-3.9.12-h9a8a25e_1_cpython.tar.bz2#06dadf5df9d340439c2aa32e15099d31 -https://conda.anaconda.org/t//conda-forge/noarch/alabaster-0.7.12-py_0.tar.bz2#2489a97287f90176ecdc3ca982b4b0a0 -https://conda.anaconda.org/t//conda-forge/noarch/appdirs-1.4.4-pyh9f0ad1d_0.tar.bz2#5f095bc6454094e96f146491fd03633b -https://conda.anaconda.org/t//conda-forge/noarch/argh-0.26.2-pyh9f0ad1d_1002.tar.bz2#0af89261f0352895e1c1000d306b3dc7 -https://conda.anaconda.org/t//conda-forge/noarch/attrs-21.4.0-pyhd8ed1ab_0.tar.bz2#f70280205d7044c8b8358c8de3190e5d -https://conda.anaconda.org/t//conda-forge/noarch/backcall-0.2.0-pyh9f0ad1d_0.tar.bz2#6006a6d08a3fa99268a2681c7fb55213 -https://conda.anaconda.org/t//conda-forge/noarch/backports-1.0-py_2.tar.bz2#0da16b293affa6ac31812376f8eb79dd -https://conda.anaconda.org/t//conda-forge/noarch/cachy-0.3.0-py_0.tar.bz2#808c46dc56ae4a796830129aaf1b51ec -https://conda.anaconda.org/t//conda-forge/noarch/charset-normalizer-2.0.12-pyhd8ed1ab_0.tar.bz2#1f5b32dabae0f1893ae3283dac7f799e -https://conda.anaconda.org/t//conda-forge/noarch/colorama-0.4.4-pyh9f0ad1d_0.tar.bz2#c08b4c1326b880ed44f3ffb04803332f -https://conda.anaconda.org/t//conda-forge/noarch/crashtest-0.3.1-pyhd8ed1ab_0.tar.bz2#b8477552274c1cfdb533e954c76523f1 -https://conda.anaconda.org/t//conda-forge/linux-64/curl-7.83.0-h7bff187_0.tar.bz2#81e39fb3ae82be7e8d2dd7046f393588 -https://conda.anaconda.org/t//conda-forge/noarch/dataclasses-0.8-pyhc8e2a94_3.tar.bz2#a362b2124b06aad102e2ee4581acee7d -https://conda.anaconda.org/t//conda-forge/noarch/decorator-5.1.1-pyhd8ed1ab_0.tar.bz2#43afe5ab04e35e17ba28649471dd7364 -https://conda.anaconda.org/t//conda-forge/noarch/defusedxml-0.7.1-pyhd8ed1ab_0.tar.bz2#961b3a227b437d82ad7054484cfa71b2 -https://conda.anaconda.org/t//conda-forge/noarch/distlib-0.3.4-pyhd8ed1ab_0.tar.bz2#7b50d840543d9cdae100e91582c33035 -https://conda.anaconda.org/t//conda-forge/noarch/entrypoints-0.4-pyhd8ed1ab_0.tar.bz2#3cf04868fee0a029769bd41f4b2fbf2d -https://conda.anaconda.org/t//conda-forge/noarch/executing-0.8.3-pyhd8ed1ab_0.tar.bz2#8d70f4543c1f701b946f85e9f9a00800 -https://conda.anaconda.org/t//conda-forge/noarch/filelock-3.6.0-pyhd8ed1ab_0.tar.bz2#6e03ca6c7b47a4152a2b12c6eee3bd32 -https://conda.anaconda.org/t//conda-forge/noarch/flit-core-3.7.1-pyhd8ed1ab_0.tar.bz2#f93822cba5c20161560661988a88f2c0 -https://conda.anaconda.org/t//conda-forge/noarch/idna-3.3-pyhd8ed1ab_0.tar.bz2#40b50b8b030f5f2f22085c062ed013dd -https://conda.anaconda.org/t//conda-forge/noarch/imagesize-1.3.0-pyhd8ed1ab_0.tar.bz2#be807e7606fff9436e5e700f6bffb7c6 -https://conda.anaconda.org/t//conda-forge/noarch/ipython_genutils-0.2.0-py_1.tar.bz2#5071c982548b3a20caf70462f04f5287 -https://conda.anaconda.org/t//conda-forge/noarch/jeepney-0.8.0-pyhd8ed1ab_0.tar.bz2#9800ad1699b42612478755a2d26c722d -https://conda.anaconda.org/t//conda-forge/noarch/json5-0.9.5-pyh9f0ad1d_0.tar.bz2#10759827a94e6b14996e81fb002c0bda -https://conda.anaconda.org/t//conda-forge/noarch/jupyterlab_widgets-1.1.0-pyhd8ed1ab_0.tar.bz2#e963a4a39cf442dbe5503f66edda083d -https://conda.anaconda.org/t//conda-forge/linux-64/libmamba-0.23.0-hd8a31e3_1.tar.bz2#f2493e48d81be2e9d0e4d4e719e31e08 -https://conda.anaconda.org/t//conda-forge/noarch/lockfile-0.12.2-py_1.tar.bz2#c104d98e09c47519950cffb8dd5b4f10 -https://conda.anaconda.org/t//conda-forge/noarch/nest-asyncio-1.5.5-pyhd8ed1ab_0.tar.bz2#dc36c992aec485c0efff619ed2e63957 -https://conda.anaconda.org/t//conda-forge/noarch/pandocfilters-1.5.0-pyhd8ed1ab_0.tar.bz2#457c2c8c08e54905d6954e79cb5b5db9 -https://conda.anaconda.org/t//conda-forge/noarch/parso-0.8.3-pyhd8ed1ab_0.tar.bz2#17a565a0c3899244e938cdf417e7b094 -https://conda.anaconda.org/t//conda-forge/noarch/pastel-0.2.1-pyhd8ed1ab_0.tar.bz2#a4eea5bff523f26442405bc5d1f52adb -https://conda.anaconda.org/t//conda-forge/noarch/pickleshare-0.7.5-py_1003.tar.bz2#415f0ebb6198cc2801c73438a9fb5761 -https://conda.anaconda.org/t//conda-forge/noarch/pkginfo-1.8.2-pyhd8ed1ab_0.tar.bz2#c776a1cd5745674c28c20a5498cafa89 -https://conda.anaconda.org/t//conda-forge/noarch/platformdirs-2.5.1-pyhd8ed1ab_0.tar.bz2#d5df87964a39f67c46a5448f4e78d9b6 -https://conda.anaconda.org/t//conda-forge/noarch/prometheus_client-0.14.1-pyhd8ed1ab_0.tar.bz2#b7fa7d86530b8de805268e48988eb483 -https://conda.anaconda.org/t//conda-forge/noarch/ptyprocess-0.7.0-pyhd3deb0d_0.tar.bz2#359eeb6536da0e687af562ed265ec263 -https://conda.anaconda.org/t//conda-forge/noarch/pure_eval-0.2.2-pyhd8ed1ab_0.tar.bz2#6784285c7e55cb7212efabc79e4c2883 -https://conda.anaconda.org/t//conda-forge/noarch/pycparser-2.21-pyhd8ed1ab_0.tar.bz2#076becd9e05608f8dc72757d5f3a91ff -https://conda.anaconda.org/t//conda-forge/noarch/pylev-1.4.0-pyhd8ed1ab_0.tar.bz2#edf8651c4379d9d1495ad6229622d150 -https://conda.anaconda.org/t//conda-forge/noarch/pyparsing-3.0.8-pyhd8ed1ab_0.tar.bz2#7f5738c49fdccd0fc755bfd25a5ea66c -https://conda.anaconda.org/t//conda-forge/noarch/python-fastjsonschema-2.15.3-pyhd8ed1ab_0.tar.bz2#fae309d1cc996da1f63de9d321e65e27 -https://conda.anaconda.org/t//conda-forge/linux-64/python_abi-3.9-2_cp39.tar.bz2#39adde4247484de2bb4000122fdcf665 -https://conda.anaconda.org/t//conda-forge/noarch/pytz-2022.1-pyhd8ed1ab_0.tar.bz2#b87d66d6d3991d988fb31510c95a9267 -https://conda.anaconda.org/t//conda-forge/noarch/send2trash-1.8.0-pyhd8ed1ab_0.tar.bz2#edab14119efe85c3bf131ad747e9005c -https://conda.anaconda.org/t//conda-forge/noarch/shellingham-1.4.0-pyh44b312d_0.tar.bz2#437655338696f9d0dfdb0a024e66b255 -https://conda.anaconda.org/t//conda-forge/noarch/six-1.16.0-pyh6c4a22f_0.tar.bz2#e5f25f8dbc060e9a8d912e432202afc2 -https://conda.anaconda.org/t//conda-forge/noarch/smmap-3.0.5-pyh44b312d_0.tar.bz2#3a8dc70789709aa315325d5df06fb7e4 -https://conda.anaconda.org/t//conda-forge/noarch/snowballstemmer-2.2.0-pyhd8ed1ab_0.tar.bz2#4d22a9315e78c6827f806065957d566e -https://conda.anaconda.org/t//conda-forge/noarch/soupsieve-2.3.1-pyhd8ed1ab_0.tar.bz2#d821b295c4bd18ad27e1e19543a5784a -https://conda.anaconda.org/t//conda-forge/noarch/sphinxcontrib-applehelp-1.0.2-py_0.tar.bz2#20b2eaeaeea4ef9a9a0d99770620fd09 -https://conda.anaconda.org/t//conda-forge/noarch/sphinxcontrib-devhelp-1.0.2-py_0.tar.bz2#68e01cac9d38d0e717cd5c87bc3d2cc9 -https://conda.anaconda.org/t//conda-forge/noarch/sphinxcontrib-htmlhelp-2.0.0-pyhd8ed1ab_0.tar.bz2#77dad82eb9c8c1525ff7953e0756d708 -https://conda.anaconda.org/t//conda-forge/noarch/sphinxcontrib-jsmath-1.0.1-py_0.tar.bz2#67cd9d9c0382d37479b4d306c369a2d4 -https://conda.anaconda.org/t//conda-forge/noarch/sphinxcontrib-qthelp-1.0.3-py_0.tar.bz2#d01180388e6d1838c3e1ad029590aa7a -https://conda.anaconda.org/t//conda-forge/noarch/sphinxcontrib-serializinghtml-1.1.5-pyhd8ed1ab_2.tar.bz2#9ff55a0901cf952f05c654394de76bf7 -https://conda.anaconda.org/t//conda-forge/noarch/threadpoolctl-3.1.0-pyh8a188c0_0.tar.bz2#a2995ee828f65687ac5b1e71a2ab1e0c -https://conda.anaconda.org/t//conda-forge/noarch/toml-0.10.2-pyhd8ed1ab_0.tar.bz2#f832c45a477c78bebd107098db465095 -https://conda.anaconda.org/t//conda-forge/noarch/toolz-0.11.2-pyhd8ed1ab_0.tar.bz2#f348d1590550371edfac5ed3c1d44f7e -https://conda.anaconda.org/t//conda-forge/noarch/traitlets-5.2.0-pyhd8ed1ab_0.tar.bz2#b81786ff00b93d07560ea21d98a2b266 -https://conda.anaconda.org/t//conda-forge/noarch/typing-3.10.0.0-pyhd8ed1ab_0.tar.bz2#e6573ac68718f17b9d4f5c8eda3190f2 -https://conda.anaconda.org/t//conda-forge/noarch/typing_extensions-4.2.0-pyha770c72_1.tar.bz2#f0f7e024f94e23d3bfee0ab777bf335a -https://conda.anaconda.org/t//conda-forge/noarch/uc-micro-py-1.0.1-pyhd8ed1ab_0.tar.bz2#3ddf6684d9b274a12c94e509ca45656c -https://conda.anaconda.org/t//conda-forge/noarch/webencodings-0.5.1-py_1.tar.bz2#3563be4c5611a44210d9ba0c16113136 -https://conda.anaconda.org/t//conda-forge/noarch/websocket-client-1.3.2-pyhd8ed1ab_0.tar.bz2#da6f472c62b4eda0caf05e223729efcd -https://conda.anaconda.org/t//conda-forge/noarch/wheel-0.37.1-pyhd8ed1ab_0.tar.bz2#1ca02aaf78d9c70d9a81a3bed5752022 -https://conda.anaconda.org/t//conda-forge/noarch/zipp-3.8.0-pyhd8ed1ab_0.tar.bz2#050b94cf4a8c760656e51d2d44e4632c -https://conda.anaconda.org/t//conda-forge/noarch/asttokens-2.0.5-pyhd8ed1ab_0.tar.bz2#74badce16f060701fee55c39332f5253 -https://conda.anaconda.org/t//conda-forge/noarch/babel-2.10.1-pyhd8ed1ab_0.tar.bz2#2ec70a4a964b696170d730466c668f60 -https://conda.anaconda.org/t//conda-forge/noarch/beautifulsoup4-4.11.1-pyha770c72_0.tar.bz2#eeec8814bd97b2681f708bb127478d7d -https://conda.anaconda.org/t//conda-forge/linux-64/certifi-2021.10.8-py39hf3d152e_2.tar.bz2#eb728d82a814f6f4d1a62db2422e004e -https://conda.anaconda.org/t//conda-forge/linux-64/cffi-1.15.0-py39h4bc2ebd_0.tar.bz2#f6191bf565dee581e77549d63737751c -https://conda.anaconda.org/t//conda-forge/linux-64/click-8.1.3-py39hf3d152e_0.tar.bz2#40edd9ebc04e4b4ec27c1008e5e3f99d -https://conda.anaconda.org/t//conda-forge/noarch/clikit-0.6.2-pyh9f0ad1d_0.tar.bz2#159273f717a11e53b2656f8b6521a5e2 -https://conda.anaconda.org/t//conda-forge/linux-64/debugpy-1.6.0-py39h5a03fae_0.tar.bz2#93ec11c7d7b7a2ff559f653dc9ca1e2b -https://conda.anaconda.org/t//conda-forge/linux-64/docutils-0.16-py39hf3d152e_3.tar.bz2#4f0fa7459a1f40a969aaad418b1c428c -https://conda.anaconda.org/t//conda-forge/linux-64/git-2.35.3-pl5321h36853c3_0.tar.bz2#a4033955267d65d8579d5af863641d97 -https://conda.anaconda.org/t//conda-forge/noarch/gitdb-4.0.9-pyhd8ed1ab_0.tar.bz2#40fc6b14a45dee3a3fd9f302d026108e -https://conda.anaconda.org/t//conda-forge/linux-64/greenlet-1.1.2-py39h5a03fae_2.tar.bz2#d7d11bc86d42472816cfb0f9f27da0ad -https://conda.anaconda.org/t//conda-forge/noarch/html5lib-1.1-pyh9f0ad1d_0.tar.bz2#b2355343d6315c892543200231d7154a -https://conda.anaconda.org/t//conda-forge/linux-64/importlib-metadata-4.11.3-py39hf3d152e_1.tar.bz2#fbcc5eab83e938242d70834ad70a8f20 -https://conda.anaconda.org/t//conda-forge/noarch/importlib_resources-5.7.1-pyhd8ed1ab_0.tar.bz2#8a50c32f48abec73bc3dd4df0d133892 -https://conda.anaconda.org/t//conda-forge/linux-64/jedi-0.18.1-py39hf3d152e_1.tar.bz2#941922f60b68bc6768c9360afb94d138 -https://conda.anaconda.org/t//conda-forge/linux-64/jupyter_core-4.9.2-py39hf3d152e_0.tar.bz2#f5b7cd7077a4aa49aa2615c50cf28e2c -https://conda.anaconda.org/t//conda-forge/noarch/latexcodec-2.0.1-pyh9f0ad1d_0.tar.bz2#8d67904973263afd2985ba56aa2d6bb4 -https://conda.anaconda.org/t//conda-forge/linux-64/libmambapy-0.23.0-py39hd55135b_1.tar.bz2#c724e99766ee68979062fc63ab6db058 -https://conda.anaconda.org/t//conda-forge/noarch/linkify-it-py-1.0.3-pyhd8ed1ab_0.tar.bz2#ba4b07f6a132c77eb69ede31a6ed790b -https://conda.anaconda.org/t//conda-forge/linux-64/lxml-4.8.0-py39hb9d737c_3.tar.bz2#b2e9b0f987bd242c80d7dfda003a2c39 -https://conda.anaconda.org/t//conda-forge/noarch/markdown-it-py-1.1.0-pyhd8ed1ab_0.tar.bz2#84e8dfb1a9e6a824f32fd45b867271ca -https://conda.anaconda.org/t//conda-forge/linux-64/markupsafe-2.1.1-py39hb9d737c_1.tar.bz2#7cda413e43b252044a270c2477031c5c -https://conda.anaconda.org/t//conda-forge/noarch/matplotlib-inline-0.1.3-pyhd8ed1ab_0.tar.bz2#be3bfd435802d2c768c6b2439f325f3d -https://conda.anaconda.org/t//conda-forge/linux-64/mistune-0.8.4-py39h3811e60_1005.tar.bz2#95eb8cbf40bccdcb34888c9e56371570 -https://conda.anaconda.org/t//conda-forge/linux-64/msgpack-python-1.0.3-py39hf939315_1.tar.bz2#9d47ff7dffb54ed6b10bd4e5087af505 -https://conda.anaconda.org/t//conda-forge/linux-64/numpy-1.22.3-py39hc58783e_2.tar.bz2#e682ad4e85c7fda7dd0f0283d3b2ae8e -https://conda.anaconda.org/t//conda-forge/noarch/packaging-20.9-pyh44b312d_0.tar.bz2#be69a38e912054a62dc82cc3c7711a64 -https://conda.anaconda.org/t//conda-forge/noarch/pexpect-4.8.0-pyh9f0ad1d_2.tar.bz2#5909e7b978141dd80d28dbf9de627827 -https://conda.anaconda.org/t//conda-forge/linux-64/poetry-core-1.0.8-py39hf3d152e_1.tar.bz2#bfefe349de77edb720cb4688821ff78e -https://conda.anaconda.org/t//conda-forge/linux-64/psutil-5.9.0-py39hb9d737c_1.tar.bz2#078ad072b9d417cbe620455a2a0e3394 -https://conda.anaconda.org/t//conda-forge/linux-64/pycosat-0.6.3-py39hb9d737c_1010.tar.bz2#b7d981539b1a880d19c6a158104a3fa1 -https://conda.anaconda.org/t//conda-forge/linux-64/pyrsistent-0.18.1-py39hb9d737c_1.tar.bz2#e2575d7508c7933047544ac7a15e021d -https://conda.anaconda.org/t//conda-forge/linux-64/pysocks-1.7.1-py39hf3d152e_5.tar.bz2#d34b97a2386932b97c7cb80916a673e7 -https://conda.anaconda.org/t//conda-forge/noarch/python-dateutil-2.8.2-pyhd8ed1ab_0.tar.bz2#dd999d1cc9f79e67dbb855c8924c7984 -https://conda.anaconda.org/t//conda-forge/linux-64/pyyaml-6.0-py39hb9d737c_4.tar.bz2#dcc47a3b751508507183d17e569805e5 -https://conda.anaconda.org/t//conda-forge/linux-64/pyzmq-22.3.0-py39headdf64_2.tar.bz2#79fce3734cb80a666837952b6a712ddb -https://conda.anaconda.org/t//conda-forge/linux-64/ruamel.yaml.clib-0.2.6-py39hb9d737c_1.tar.bz2#a0fabd69dd35bb24ec84d28dc01c3c5b -https://conda.anaconda.org/t//conda-forge/linux-64/ruamel_yaml-0.15.80-py39h3811e60_1006.tar.bz2#c9e96b53141c8b1bc214e6a90611e2ca -https://conda.anaconda.org/t//conda-forge/linux-64/setuptools-62.1.0-py39hf3d152e_0.tar.bz2#199fa7c3b8ea037543cce82563918a59 -https://conda.anaconda.org/t//conda-forge/linux-64/sniffio-1.2.0-py39hf3d152e_3.tar.bz2#e2cb114a39b27ef1687a0c2c3e793cf6 -https://conda.anaconda.org/t//conda-forge/noarch/tinycss2-1.1.1-pyhd8ed1ab_0.tar.bz2#5d280406501e79dc7aa9c9ac31d25a80 -https://conda.anaconda.org/t//conda-forge/noarch/tomlkit-0.10.2-pyha770c72_0.tar.bz2#482e5775f80665a7c9f76cd72a66eae8 -https://conda.anaconda.org/t//conda-forge/linux-64/tornado-6.1-py39hb9d737c_3.tar.bz2#5e13a2d214ed4184969df363a1aab420 -https://conda.anaconda.org/t//conda-forge/noarch/tqdm-4.64.0-pyhd8ed1ab_0.tar.bz2#6642233f341e1900d0c8e6eddb979c14 -https://conda.anaconda.org/t//conda-forge/noarch/typing-extensions-4.2.0-hd8ed1ab_1.tar.bz2#6d9d7480c5780514779967be2ee8b963 -https://conda.anaconda.org/t//conda-forge/linux-64/virtualenv-20.14.1-py39hf3d152e_0.tar.bz2#6d7e213edf6669391700946201a63bc0 -https://conda.anaconda.org/t//conda-forge/linux-64/anyio-3.5.0-py39hf3d152e_0.tar.bz2#b0d75a9d3fd02ec079504aabb7cd7ec3 -https://conda.anaconda.org/t//conda-forge/linux-64/argon2-cffi-bindings-21.2.0-py39hb9d737c_2.tar.bz2#76139de3552a2046135eb0b2d02a9c85 -https://conda.anaconda.org/t//conda-forge/noarch/backports.functools_lru_cache-1.6.4-pyhd8ed1ab_0.tar.bz2#c5b3edc62d6309088f4970b3eaaa65a6 -https://conda.anaconda.org/t//conda-forge/noarch/bleach-5.0.0-pyhd8ed1ab_0.tar.bz2#2a2ae7c56b8f72caba261363407b484a -https://conda.anaconda.org/t//conda-forge/linux-64/brotlipy-0.7.0-py39hb9d737c_1004.tar.bz2#05a99367d885ec9990f25e74128a8a08 -https://conda.anaconda.org/t//conda-forge/noarch/cleo-0.8.1-pyhd8ed1ab_2.tar.bz2#4c82b11a3d06031bd58e7d869f53d965 -https://conda.anaconda.org/t//conda-forge/noarch/click-default-group-1.2.2-pyhd8ed1ab_1.tar.bz2#72a46ffc25701c173932fd55cf0965d3 -https://conda.anaconda.org/t//conda-forge/noarch/click-log-0.3.2-pyh9f0ad1d_0.tar.bz2#3a64d156136fad977df1b81a24b57ac0 -https://conda.anaconda.org/t//conda-forge/linux-64/conda-package-handling-1.8.1-py39hb9d737c_1.tar.bz2#1fadb17b68893d479b0a01981570a494 -https://conda.anaconda.org/t//conda-forge/linux-64/cryptography-36.0.2-py39hd97740a_1.tar.bz2#dbd00b111b182f40ecf998c8289fc4a2 -https://conda.anaconda.org/t//conda-forge/noarch/ghp-import-2.1.0-pyhd8ed1ab_0.tar.bz2#6d8d61116031a3f5b1f32e7899785866 -https://conda.anaconda.org/t//conda-forge/noarch/gitpython-3.1.27-pyhd8ed1ab_0.tar.bz2#20acbaab17a50ac9b64138eb9a0e1af8 -https://conda.anaconda.org/t//conda-forge/noarch/importlib_metadata-4.11.3-hd8ed1ab_1.tar.bz2#bd6b6ae37c03e68061574d5e32fe5bd1 -https://conda.anaconda.org/t//conda-forge/noarch/jinja2-3.0.3-pyhd8ed1ab_0.tar.bz2#036d872c653780cb26e797e2e2f61b4c -https://conda.anaconda.org/t//conda-forge/noarch/joblib-1.1.0-pyhd8ed1ab_0.tar.bz2#07d1b5c8cde14d95998fd4767e1e62d2 -https://conda.anaconda.org/t//conda-forge/noarch/jsonschema-3.2.0-pyhd8ed1ab_3.tar.bz2#66125e28711d8ffc04a207a2b170316d -https://conda.anaconda.org/t//conda-forge/noarch/jupyter_client-7.3.1-pyhd8ed1ab_0.tar.bz2#38481a37ead8c37d2ad7b52d3bc2b0a7 -https://conda.anaconda.org/t//conda-forge/noarch/mdit-py-plugins-0.2.8-pyhd8ed1ab_0.tar.bz2#49236fcd746a124eb56d326f79e1d46d -https://conda.anaconda.org/t//conda-forge/linux-64/pandas-1.4.2-py39h1832856_1.tar.bz2#264505bcd299b8d564195cfb3e6038f0 -https://conda.anaconda.org/t//conda-forge/noarch/pip-22.0.4-pyhd8ed1ab_0.tar.bz2#b1239ce8ef2a1eec485c398a683c5bff -https://conda.anaconda.org/t//conda-forge/noarch/portpicker-1.5.0-pyhd8ed1ab_0.tar.bz2#5f9595b4b3f50a0d572b0a7c8b4293c7 -https://conda.anaconda.org/t//conda-forge/noarch/pybtex-0.24.0-pyhd8ed1ab_2.tar.bz2#2099b86a7399c44c0c61cdb6de6915ba -https://conda.anaconda.org/t//conda-forge/linux-64/pydantic-1.9.0-py39hb9d737c_1.tar.bz2#5e0f9261bada9bc3cbd4269240993f0b -https://conda.anaconda.org/t//conda-forge/noarch/pygments-2.12.0-pyhd8ed1ab_0.tar.bz2#cb27e2ded147e5bcc7eafc1c6d343cb3 -https://conda.anaconda.org/t//conda-forge/linux-64/ruamel.yaml-0.17.21-py39hb9d737c_1.tar.bz2#2b94cf785616198b112170b9838262a4 -https://conda.anaconda.org/t//conda-forge/linux-64/scipy-1.8.0-py39hee8e79c_1.tar.bz2#8cc32d8307f5a8eb319e242c61d259ec -https://conda.anaconda.org/t//conda-forge/linux-64/sqlalchemy-1.4.36-py39hb9d737c_0.tar.bz2#e432937ad2a6822f770c1a925a08200c -https://conda.anaconda.org/t//conda-forge/noarch/stack_data-0.2.0-pyhd8ed1ab_0.tar.bz2#8c0ce3e6bf18a0c810125aef58a2a6f3 -https://conda.anaconda.org/t//conda-forge/linux-64/terminado-0.13.3-py39hf3d152e_1.tar.bz2#bebf6da1adb04ed902ff335e61438a6e -https://conda.anaconda.org/t//conda-forge/linux-64/watchdog-2.1.7-py39hf3d152e_1.tar.bz2#1c40e7ce24039941617ef32b80e04114 -https://conda.anaconda.org/t//conda-forge/noarch/altair-4.2.0-pyhd8ed1ab_1.tar.bz2#2867acfe48ceb3630b163632914720d9 -https://conda.anaconda.org/t//conda-forge/noarch/argon2-cffi-21.3.0-pyhd8ed1ab_0.tar.bz2#a0b402db58f73aaab8ee0ca1025a362e -https://conda.anaconda.org/t//conda-forge/linux-64/click-completion-0.5.2-py39hf3d152e_3.tar.bz2#af5cc0d8d34180fca8b8fa7ba438c9b2 -https://conda.anaconda.org/t//conda-forge/noarch/jupyterlab_pygments-0.2.2-pyhd8ed1ab_0.tar.bz2#243f63592c8e449f40cd42eb5cf32f40 -https://conda.anaconda.org/t//conda-forge/noarch/nbformat-5.4.0-pyhd8ed1ab_0.tar.bz2#770f6659243e2c79a0b8488b0e463bd1 -https://conda.anaconda.org/t//conda-forge/linux-64/pybtex-docutils-1.0.1-py39hf3d152e_1.tar.bz2#d1febb9b765260fe964d3d4e3cc22479 -https://conda.anaconda.org/t//conda-forge/noarch/pyopenssl-22.0.0-pyhd8ed1ab_0.tar.bz2#1d7e241dfaf5475e893d4b824bb71b44 -https://conda.anaconda.org/t//conda-forge/linux-64/scikit-learn-1.0.2-py39h4dfa638_0.tar.bz2#389a22c8af77c8575a8a848fef79790e -https://conda.anaconda.org/t//conda-forge/linux-64/secretstorage-3.3.2-py39hf3d152e_1.tar.bz2#fd79e4bf83b5a19ceda3508a2adc2eb1 -https://conda.anaconda.org/t//conda-forge/noarch/wcwidth-0.2.5-pyh9f0ad1d_2.tar.bz2#5266fcd697043c59621fda522b3d78ee -https://conda.anaconda.org/t//conda-forge/noarch/altair_data_server-0.4.1-py_0.tar.bz2#5412ec3d2792d3e2f18e075cb05ffdaf -https://conda.anaconda.org/t//conda-forge/noarch/jupytext-1.13.8-pyh4b9bcc7_0.tar.bz2#aba00353637aa69640ec2ae150dc592d -https://conda.anaconda.org/t//conda-forge/linux-64/keyring-23.4.0-py39hf3d152e_2.tar.bz2#ad48fe501f6b4804e8c15a474f9c8968 -https://conda.anaconda.org/t//conda-forge/noarch/nbclient-0.5.13-pyhd8ed1ab_0.tar.bz2#3edde88a191701cf052216c4ba353a83 -https://conda.anaconda.org/t//conda-forge/noarch/prompt-toolkit-3.0.29-pyha770c72_0.tar.bz2#9e720b57b22ef3032b4fb081697819dd -https://conda.anaconda.org/t//conda-forge/noarch/urllib3-1.26.9-pyhd8ed1ab_0.tar.bz2#0ea179ee251aa7100807c35bc0252693 -https://conda.anaconda.org/t//conda-forge/linux-64/ipython-8.3.0-py39hf3d152e_0.tar.bz2#731e626303529ba6205adb1014686858 -https://conda.anaconda.org/t//conda-forge/noarch/nbconvert-core-6.5.0-pyhd8ed1ab_0.tar.bz2#42f74c4b38a099025167e76a7437edf1 -https://conda.anaconda.org/t//conda-forge/noarch/requests-2.27.1-pyhd8ed1ab_0.tar.bz2#7c1c427246b057b8fa97200ecdb2ed62 -https://conda.anaconda.org/t//conda-forge/noarch/cachecontrol-0.12.11-pyhd8ed1ab_0.tar.bz2#6eefee9888f33f150b5d44d616b1a613 -https://conda.anaconda.org/t//conda-forge/linux-64/conda-4.12.0-py39hf3d152e_0.tar.bz2#fb54573fc3909e06c3f289e0fbf9ca3d -https://conda.anaconda.org/t//conda-forge/noarch/ensureconda-1.4.2-pyhd8ed1ab_0.tar.bz2#1bf97b25d058482fd73e62b1cdb932ef -https://conda.anaconda.org/t//conda-forge/linux-64/ipykernel-6.13.0-py39hef51801_0.tar.bz2#9fd2a497e68e4c715d9fdf3f36b44072 -https://conda.anaconda.org/t//conda-forge/noarch/jupyter_server-1.17.0-pyhd8ed1ab_0.tar.bz2#276b3b45443e2a84cfb4d128cb86c350 -https://conda.anaconda.org/t//conda-forge/noarch/nbconvert-pandoc-6.5.0-pyhd8ed1ab_0.tar.bz2#d7421adfc67100021d87032447066129 -https://conda.anaconda.org/t//conda-forge/noarch/pooch-1.6.0-pyhd8ed1ab_0.tar.bz2#6429e1d1091c51f626b5dcfdd38bf429 -https://conda.anaconda.org/t//conda-forge/noarch/requests-toolbelt-0.9.1-py_0.tar.bz2#402668adee8fcba9a9c265cdc2a88f5a -https://conda.anaconda.org/t//conda-forge/noarch/sphinx-4.5.0-pyh6c4a22f_0.tar.bz2#46b38d88c4270ff9ba78a89c83c66345 -https://conda.anaconda.org/t//conda-forge/noarch/jupyter-server-mathjax-0.2.5-pyhc268e32_0.tar.bz2#0393370c2dec5e92e1727a8650f908f7 -https://conda.anaconda.org/t//conda-forge/noarch/jupyterlab_server-2.13.0-pyhd8ed1ab_1.tar.bz2#ecead930bfd8c0f629c5b8bf5c1e3508 -https://conda.anaconda.org/t//conda-forge/linux-64/mamba-0.23.0-py39hfa8f2c8_1.tar.bz2#5aeaa3d80ebfbbf408d46daf81bcd447 -https://conda.anaconda.org/t//conda-forge/noarch/myst-parser-0.15.2-pyhd8ed1ab_0.tar.bz2#0c2976e0a1af80ce224388da557eeece -https://conda.anaconda.org/t//conda-forge/noarch/nbconvert-6.5.0-pyhd8ed1ab_0.tar.bz2#156c180588e38b9f41758058824ec50f -https://conda.anaconda.org/t//conda-forge/noarch/notebook-shim-0.1.0-pyhd8ed1ab_0.tar.bz2#3a8e2c7dcc674f2cb0784f1faba57055 -https://conda.anaconda.org/t//conda-forge/linux-64/poetry-1.1.13-py39hf3d152e_1.tar.bz2#a10e45641e7ef946b8c4802c35e7fd44 -https://conda.anaconda.org/t//conda-forge/noarch/pydata-sphinx-theme-0.7.2-pyhd8ed1ab_0.tar.bz2#123f5c52d6b4117225af45a52ec34997 -https://conda.anaconda.org/t//conda-forge/noarch/sphinx-comments-0.0.3-pyh9f0ad1d_0.tar.bz2#2ae3ce35de0c1cec45c94182694f8d1b -https://conda.anaconda.org/t//conda-forge/noarch/sphinx-copybutton-0.5.0-pyhd8ed1ab_0.tar.bz2#4c969cdd5191306c269490f7ff236d9c -https://conda.anaconda.org/t//conda-forge/noarch/sphinx-external-toc-0.2.4-pyhd8ed1ab_0.tar.bz2#91ae8770569b73f25e1127526c1329ed -https://conda.anaconda.org/t//conda-forge/noarch/sphinx-jupyterbook-latex-0.4.6-pyhd8ed1ab_0.tar.bz2#6e4a69a0c8adbb48178fdf3efa24fa4c -https://conda.anaconda.org/t//conda-forge/noarch/sphinx-multitoc-numbering-0.1.3-pyhd8ed1ab_0.tar.bz2#40749a4d0f0d2e11c65fb26c1cd16a90 -https://conda.anaconda.org/t//conda-forge/noarch/sphinx-panels-0.6.0-pyhd8ed1ab_0.tar.bz2#6eec6480601f5d15babf9c3b3987f34a -https://conda.anaconda.org/t//conda-forge/noarch/sphinx-thebe-0.1.2-pyhd8ed1ab_0.tar.bz2#1d4fdd342aa955085a0f21e26bb585f7 -https://conda.anaconda.org/t//conda-forge/noarch/sphinx-togglebutton-0.3.1-pyhd8ed1ab_0.tar.bz2#71418887aa6599ea2935f4958e5e1d15 -https://conda.anaconda.org/t//conda-forge/noarch/sphinxcontrib-bibtex-2.4.2-pyhd8ed1ab_0.tar.bz2#d826ac2b3edfe7a8113596c2023f092b -https://conda.anaconda.org/t//conda-forge/noarch/conda-lock-1.0.5-pyhd8ed1ab_0.tar.bz2#7544764bbf4941cc954bb80911f3b201 -https://conda.anaconda.org/t//conda-forge/noarch/nbdime-3.1.1-pyhd8ed1ab_0.tar.bz2#38dc061ffabe665b79f4c7c52cefa809 -https://conda.anaconda.org/t//conda-forge/noarch/notebook-6.4.11-pyha770c72_0.tar.bz2#da25720a88aa3cbb3e16df740783da74 -https://conda.anaconda.org/t//conda-forge/noarch/sphinx-book-theme-0.1.10-pyhd8ed1ab_1.tar.bz2#194ec0159031da65b653c665bb1678f6 -https://conda.anaconda.org/t//conda-forge/noarch/jupyter-cache-0.4.3-pyhd8ed1ab_0.tar.bz2#03cd9218c96d513854bfc8714eaf9451 -https://conda.anaconda.org/t//conda-forge/noarch/jupyter_contrib_core-0.3.3-py_2.tar.bz2#4704781b0a914d67e4ae8e9f9f5c37a0 -https://conda.anaconda.org/t//conda-forge/noarch/nbclassic-0.3.7-pyhd8ed1ab_0.tar.bz2#a8a7139140a7512c90514444444a4991 -https://conda.anaconda.org/t//conda-forge/linux-64/widgetsnbextension-3.6.0-py39hf3d152e_0.tar.bz2#eb9f42c0ef3263a1d7bad8b0deb446f5 -https://conda.anaconda.org/t//conda-forge/noarch/ipywidgets-7.7.0-pyhd8ed1ab_0.tar.bz2#a3d2ccd3d9f9fcb65765c22f500529b4 -https://conda.anaconda.org/t//conda-forge/linux-64/jupyter_highlight_selected_word-0.2.0-py39hf3d152e_1005.tar.bz2#a8cf0e4e8a3289e920c640e4c8ac843a -https://conda.anaconda.org/t//conda-forge/noarch/jupyter_latex_envs-1.4.6-pyhd8ed1ab_1002.tar.bz2#4b888fd7d6b4cdb6736878b2cf8ea951 -https://conda.anaconda.org/t//conda-forge/noarch/jupyter_nbextensions_configurator-0.4.1-pyhd8ed1ab_2.tar.bz2#19a2fa481008976df3ed8ce5d4dfb8fa -https://conda.anaconda.org/t//conda-forge/noarch/jupyterlab-3.4.0-pyhd8ed1ab_0.tar.bz2#79d9efa21a8dbe3f9fdbe8069a61ac26 -https://conda.anaconda.org/t//conda-forge/noarch/jupyter-sphinx-0.3.2-pyhd8ed1ab_1.tar.bz2#a47ca0f91417e5d29d075ca416254466 -https://conda.anaconda.org/t//conda-forge/noarch/jupyter_contrib_nbextensions-0.5.1-pyhd8ed1ab_2.tar.bz2#ee3820cb73867efb8c928b93e55f4de3 -https://conda.anaconda.org/t//conda-forge/noarch/myst-nb-0.13.2-pyhd8ed1ab_0.tar.bz2#800e968e63eb593c651a14907fd82d26 -https://conda.anaconda.org/t//conda-forge/noarch/jupyter-book-0.12.3-pyhd8ed1ab_0.tar.bz2#1d6c5efa323c2cfd6196af5524aa5b78 diff --git a/unused/install/conda-osx-64.lock b/unused/install/conda-osx-64.lock deleted file mode 100644 index f5d2c64f..00000000 --- a/unused/install/conda-osx-64.lock +++ /dev/null @@ -1,259 +0,0 @@ -# Generated by conda-lock. -# platform: osx-64 -# input_hash: 866774b8507a84d7b8151db0d5e21fce1654997b6927d18dbd4bd16e2a387ee1 -@EXPLICIT -https://conda.anaconda.org/t//conda-forge/osx-64/bzip2-1.0.8-h0d85af4_4.tar.bz2#37edc4e6304ca87316e160f5ca0bd1b5 -https://conda.anaconda.org/t//conda-forge/osx-64/c-ares-1.18.1-h0d85af4_0.tar.bz2#00b3e98a61e6430808fe7a2534681f28 -https://conda.anaconda.org/t//conda-forge/osx-64/ca-certificates-2021.10.8-h033912b_0.tar.bz2#bb82d0243db9882b509702ecb69e38f0 -https://conda.anaconda.org/t//conda-forge/osx-64/libcxx-14.0.3-hc203e6f_0.tar.bz2#169cb4aae14ca89851deda1756b2ebf7 -https://conda.anaconda.org/t//conda-forge/osx-64/libev-4.33-haf1e3a3_1.tar.bz2#79dc2be110b2a3d1e97ec21f691c50ad -https://conda.anaconda.org/t//conda-forge/osx-64/libffi-3.4.2-h0d85af4_5.tar.bz2#ccb34fb14960ad8b125962d3d79b31a9 -https://conda.anaconda.org/t//conda-forge/osx-64/libiconv-1.16-haf1e3a3_0.tar.bz2#c5fab167412a52e491c8e11453ae016f -https://conda.anaconda.org/t//conda-forge/osx-64/libsodium-1.0.18-hbcb3906_1.tar.bz2#24632c09ed931af617fe6d5292919cab -https://conda.anaconda.org/t//conda-forge/osx-64/libuv-1.43.0-h0d85af4_0.tar.bz2#f68a2895786d91cdae79116b2b014592 -https://conda.anaconda.org/t//conda-forge/osx-64/libzlib-1.2.11-h6c3fc93_1014.tar.bz2#49f20ed86f7ed34204be74fbb2868c60 -https://conda.anaconda.org/t//conda-forge/osx-64/llvm-openmp-14.0.3-ha654fa7_0.tar.bz2#321df8a58df1427266e9f9c18af3f7f5 -https://conda.anaconda.org/t//conda-forge/osx-64/lzo-2.10-haf1e3a3_1000.tar.bz2#0b6bca372a95d6c602c7a922e928ce79 -https://conda.anaconda.org/t//conda-forge/osx-64/ncurses-6.3-h96cf925_1.tar.bz2#76217ebfbb163ff2770a261f955a5861 -https://conda.anaconda.org/t//conda-forge/osx-64/pandoc-2.18-h694c41f_0.tar.bz2#42f9f041e9d250ee3bc7332b8d28e0a2 -https://conda.anaconda.org/t//conda-forge/osx-64/perl-5.32.1-2_h0d85af4_perl5.tar.bz2#dd13a8c2fac0cd8e102fcdc7bca1f077 -https://conda.anaconda.org/t//conda-forge/noarch/pybind11-abi-4-hd8ed1ab_3.tar.bz2#878f923dd6acc8aeb47a75da6c4098be -https://conda.anaconda.org/t//conda-forge/osx-64/reproc-14.2.3-h0d85af4_0.tar.bz2#6f87f4707e4daf5823ce56dce5d9fbea -https://conda.anaconda.org/t//conda-forge/noarch/tzdata-2022a-h191b570_0.tar.bz2#84be5301069417a2221187d2f435e0f7 -https://conda.anaconda.org/t//conda-forge/osx-64/xz-5.2.5-haf1e3a3_1.tar.bz2#41116deb499e9bc58048c297d6403ce6 -https://conda.anaconda.org/t//conda-forge/osx-64/yaml-0.2.5-h0d85af4_2.tar.bz2#d7e08fcf8259d742156188e8762b4d20 -https://conda.anaconda.org/t//conda-forge/osx-64/expat-2.4.8-h96cf925_0.tar.bz2#529d357c143fb98b9af77d687f82a3e0 -https://conda.anaconda.org/t//conda-forge/osx-64/gettext-0.19.8.1-hd1a6beb_1008.tar.bz2#28c370fc39becf486601d9e491a5e184 -https://conda.anaconda.org/t//conda-forge/osx-64/icu-70.1-h96cf925_0.tar.bz2#376635049e9b9b0bb875efd39dcd7b3b -https://conda.anaconda.org/t//conda-forge/osx-64/libedit-3.1.20191231-h0678c8f_2.tar.bz2#6016a8a1d0e63cac3de2c352cd40208b -https://conda.anaconda.org/t//conda-forge/osx-64/libgfortran5-9.3.0-h6c81a4c_23.tar.bz2#a6956ceb628b14594613cefee5127a7a -https://conda.anaconda.org/t//conda-forge/osx-64/libsolv-0.7.22-hd9580d2_0.tar.bz2#068ed0617893ecbccbf65a32ea1e8056 -https://conda.anaconda.org/t//conda-forge/osx-64/lz4-c-1.9.3-he49afe7_1.tar.bz2#05c08241b66631c00ca4f9e0b75320bc -https://conda.anaconda.org/t//conda-forge/osx-64/openssl-1.1.1o-hfe4f2af_0.tar.bz2#655048e118f0b7029e5c216a1d7a6189 -https://conda.anaconda.org/t//conda-forge/osx-64/readline-8.1-h05e3726_0.tar.bz2#2832e9b6a7caa7cb192fcda6cfcd8871 -https://conda.anaconda.org/t//conda-forge/osx-64/reproc-cpp-14.2.3-he49afe7_0.tar.bz2#7dafcfaa471cd16cbd73832cefc39770 -https://conda.anaconda.org/t//conda-forge/osx-64/tk-8.6.12-h5dbffcc_0.tar.bz2#8e9480d9c47061db2ed1b4ecce519a7f -https://conda.anaconda.org/t//conda-forge/osx-64/yaml-cpp-0.6.3-hb1e8313_4.tar.bz2#f56440cd47d05468d6fb2020f98002d8 -https://conda.anaconda.org/t//conda-forge/osx-64/zeromq-4.3.4-he49afe7_1.tar.bz2#1972d732b123ed04b60fd21e94f0b178 -https://conda.anaconda.org/t//conda-forge/osx-64/zlib-1.2.11-h6c3fc93_1014.tar.bz2#98b82f6a8de694bc6259f2d1a69bc02b -https://conda.anaconda.org/t//conda-forge/osx-64/krb5-1.19.3-hb49756b_0.tar.bz2#e60363be26ab2a74326c06195d638447 -https://conda.anaconda.org/t//conda-forge/osx-64/libgfortran-5.0.0-9_3_0_h6c81a4c_23.tar.bz2#60f48cef2d50674e0428c5579b6c3f66 -https://conda.anaconda.org/t//conda-forge/osx-64/libnghttp2-1.47.0-h942079c_0.tar.bz2#86fc370e607a269b64ac6fa5d29e55e8 -https://conda.anaconda.org/t//conda-forge/osx-64/libssh2-1.10.0-h52ee1ee_2.tar.bz2#8c8f3804e8e252b47443cfe8e40eddf9 -https://conda.anaconda.org/t//conda-forge/osx-64/libxml2-2.9.14-h08a9926_0.tar.bz2#3f1b05fc03318121ba2c5eabbf28be2f -https://conda.anaconda.org/t//conda-forge/osx-64/nodejs-17.9.0-h3cde592_0.tar.bz2#8148f6cc0609bc58be41ecf4563c4280 -https://conda.anaconda.org/t//conda-forge/osx-64/pcre2-10.37-ha16e1b2_0.tar.bz2#a3be43be1fcee194d1e82e5ca2ce47bc -https://conda.anaconda.org/t//conda-forge/osx-64/sqlite-3.38.5-hd9f0692_0.tar.bz2#258c39c5e2eff8b8b29d1a027e4e1b5a -https://conda.anaconda.org/t//conda-forge/osx-64/zstd-1.5.2-h582d3a0_0.tar.bz2#df9ed22d1725b8fecaede2d579fd6bc4 -https://conda.anaconda.org/t//conda-forge/osx-64/libarchive-3.5.2-h2b60450_1.tar.bz2#598a850147b8622e8348cbf6578effaf -https://conda.anaconda.org/t//conda-forge/osx-64/libcurl-7.83.0-h372c54d_0.tar.bz2#189d7b818b1edae0199fd3f9dfdc072e -https://conda.anaconda.org/t//conda-forge/osx-64/libopenblas-0.3.20-openmp_hb3cd9ec_0.tar.bz2#d862e4a5c6e7bf0bc9d66a38f5c73142 -https://conda.anaconda.org/t//conda-forge/osx-64/libxslt-1.1.33-h5bff336_4.tar.bz2#885c26849179f63d56d3d0f1d52dbd2a -https://conda.anaconda.org/t//conda-forge/osx-64/python-3.9.12-h8b4d769_1_cpython.tar.bz2#ebd20128c3a2f2fe98c9e6562cf43f65 -https://conda.anaconda.org/t//conda-forge/noarch/alabaster-0.7.12-py_0.tar.bz2#2489a97287f90176ecdc3ca982b4b0a0 -https://conda.anaconda.org/t//conda-forge/noarch/appdirs-1.4.4-pyh9f0ad1d_0.tar.bz2#5f095bc6454094e96f146491fd03633b -https://conda.anaconda.org/t//conda-forge/noarch/appnope-0.1.3-pyhd8ed1ab_0.tar.bz2#54ac328d703bff191256ffa1183126d1 -https://conda.anaconda.org/t//conda-forge/noarch/argh-0.26.2-pyh9f0ad1d_1002.tar.bz2#0af89261f0352895e1c1000d306b3dc7 -https://conda.anaconda.org/t//conda-forge/noarch/attrs-21.4.0-pyhd8ed1ab_0.tar.bz2#f70280205d7044c8b8358c8de3190e5d -https://conda.anaconda.org/t//conda-forge/noarch/backcall-0.2.0-pyh9f0ad1d_0.tar.bz2#6006a6d08a3fa99268a2681c7fb55213 -https://conda.anaconda.org/t//conda-forge/noarch/backports-1.0-py_2.tar.bz2#0da16b293affa6ac31812376f8eb79dd -https://conda.anaconda.org/t//conda-forge/noarch/cachy-0.3.0-py_0.tar.bz2#808c46dc56ae4a796830129aaf1b51ec -https://conda.anaconda.org/t//conda-forge/noarch/charset-normalizer-2.0.12-pyhd8ed1ab_0.tar.bz2#1f5b32dabae0f1893ae3283dac7f799e -https://conda.anaconda.org/t//conda-forge/noarch/colorama-0.4.4-pyh9f0ad1d_0.tar.bz2#c08b4c1326b880ed44f3ffb04803332f -https://conda.anaconda.org/t//conda-forge/noarch/crashtest-0.3.1-pyhd8ed1ab_0.tar.bz2#b8477552274c1cfdb533e954c76523f1 -https://conda.anaconda.org/t//conda-forge/osx-64/curl-7.83.0-h372c54d_0.tar.bz2#70cc7e2acae7f738c6c7afa94a2b7233 -https://conda.anaconda.org/t//conda-forge/noarch/dataclasses-0.8-pyhc8e2a94_3.tar.bz2#a362b2124b06aad102e2ee4581acee7d -https://conda.anaconda.org/t//conda-forge/noarch/decorator-5.1.1-pyhd8ed1ab_0.tar.bz2#43afe5ab04e35e17ba28649471dd7364 -https://conda.anaconda.org/t//conda-forge/noarch/defusedxml-0.7.1-pyhd8ed1ab_0.tar.bz2#961b3a227b437d82ad7054484cfa71b2 -https://conda.anaconda.org/t//conda-forge/noarch/distlib-0.3.4-pyhd8ed1ab_0.tar.bz2#7b50d840543d9cdae100e91582c33035 -https://conda.anaconda.org/t//conda-forge/noarch/entrypoints-0.4-pyhd8ed1ab_0.tar.bz2#3cf04868fee0a029769bd41f4b2fbf2d -https://conda.anaconda.org/t//conda-forge/noarch/executing-0.8.3-pyhd8ed1ab_0.tar.bz2#8d70f4543c1f701b946f85e9f9a00800 -https://conda.anaconda.org/t//conda-forge/noarch/filelock-3.6.0-pyhd8ed1ab_0.tar.bz2#6e03ca6c7b47a4152a2b12c6eee3bd32 -https://conda.anaconda.org/t//conda-forge/noarch/flit-core-3.7.1-pyhd8ed1ab_0.tar.bz2#f93822cba5c20161560661988a88f2c0 -https://conda.anaconda.org/t//conda-forge/noarch/idna-3.3-pyhd8ed1ab_0.tar.bz2#40b50b8b030f5f2f22085c062ed013dd -https://conda.anaconda.org/t//conda-forge/noarch/imagesize-1.3.0-pyhd8ed1ab_0.tar.bz2#be807e7606fff9436e5e700f6bffb7c6 -https://conda.anaconda.org/t//conda-forge/noarch/ipython_genutils-0.2.0-py_1.tar.bz2#5071c982548b3a20caf70462f04f5287 -https://conda.anaconda.org/t//conda-forge/noarch/json5-0.9.5-pyh9f0ad1d_0.tar.bz2#10759827a94e6b14996e81fb002c0bda -https://conda.anaconda.org/t//conda-forge/noarch/jupyterlab_widgets-1.1.0-pyhd8ed1ab_0.tar.bz2#e963a4a39cf442dbe5503f66edda083d -https://conda.anaconda.org/t//conda-forge/osx-64/libblas-3.9.0-14_osx64_openblas.tar.bz2#7440571e6f75b795ebc25a71429ca99d -https://conda.anaconda.org/t//conda-forge/osx-64/libmamba-0.23.0-h2d3d89a_1.tar.bz2#0b9cec67dcaa4a72263b5f5fd123050f -https://conda.anaconda.org/t//conda-forge/noarch/lockfile-0.12.2-py_1.tar.bz2#c104d98e09c47519950cffb8dd5b4f10 -https://conda.anaconda.org/t//conda-forge/noarch/nest-asyncio-1.5.5-pyhd8ed1ab_0.tar.bz2#dc36c992aec485c0efff619ed2e63957 -https://conda.anaconda.org/t//conda-forge/noarch/pandocfilters-1.5.0-pyhd8ed1ab_0.tar.bz2#457c2c8c08e54905d6954e79cb5b5db9 -https://conda.anaconda.org/t//conda-forge/noarch/parso-0.8.3-pyhd8ed1ab_0.tar.bz2#17a565a0c3899244e938cdf417e7b094 -https://conda.anaconda.org/t//conda-forge/noarch/pastel-0.2.1-pyhd8ed1ab_0.tar.bz2#a4eea5bff523f26442405bc5d1f52adb -https://conda.anaconda.org/t//conda-forge/noarch/pickleshare-0.7.5-py_1003.tar.bz2#415f0ebb6198cc2801c73438a9fb5761 -https://conda.anaconda.org/t//conda-forge/noarch/pkginfo-1.8.2-pyhd8ed1ab_0.tar.bz2#c776a1cd5745674c28c20a5498cafa89 -https://conda.anaconda.org/t//conda-forge/noarch/platformdirs-2.5.1-pyhd8ed1ab_0.tar.bz2#d5df87964a39f67c46a5448f4e78d9b6 -https://conda.anaconda.org/t//conda-forge/noarch/prometheus_client-0.14.1-pyhd8ed1ab_0.tar.bz2#b7fa7d86530b8de805268e48988eb483 -https://conda.anaconda.org/t//conda-forge/noarch/ptyprocess-0.7.0-pyhd3deb0d_0.tar.bz2#359eeb6536da0e687af562ed265ec263 -https://conda.anaconda.org/t//conda-forge/noarch/pure_eval-0.2.2-pyhd8ed1ab_0.tar.bz2#6784285c7e55cb7212efabc79e4c2883 -https://conda.anaconda.org/t//conda-forge/noarch/pycparser-2.21-pyhd8ed1ab_0.tar.bz2#076becd9e05608f8dc72757d5f3a91ff -https://conda.anaconda.org/t//conda-forge/noarch/pylev-1.4.0-pyhd8ed1ab_0.tar.bz2#edf8651c4379d9d1495ad6229622d150 -https://conda.anaconda.org/t//conda-forge/noarch/pyparsing-3.0.8-pyhd8ed1ab_0.tar.bz2#7f5738c49fdccd0fc755bfd25a5ea66c -https://conda.anaconda.org/t//conda-forge/noarch/python-fastjsonschema-2.15.3-pyhd8ed1ab_0.tar.bz2#fae309d1cc996da1f63de9d321e65e27 -https://conda.anaconda.org/t//conda-forge/osx-64/python_abi-3.9-2_cp39.tar.bz2#262f557ee8ca777fe2190956038024cd -https://conda.anaconda.org/t//conda-forge/noarch/pytz-2022.1-pyhd8ed1ab_0.tar.bz2#b87d66d6d3991d988fb31510c95a9267 -https://conda.anaconda.org/t//conda-forge/noarch/send2trash-1.8.0-pyhd8ed1ab_0.tar.bz2#edab14119efe85c3bf131ad747e9005c -https://conda.anaconda.org/t//conda-forge/noarch/shellingham-1.4.0-pyh44b312d_0.tar.bz2#437655338696f9d0dfdb0a024e66b255 -https://conda.anaconda.org/t//conda-forge/noarch/six-1.16.0-pyh6c4a22f_0.tar.bz2#e5f25f8dbc060e9a8d912e432202afc2 -https://conda.anaconda.org/t//conda-forge/noarch/smmap-3.0.5-pyh44b312d_0.tar.bz2#3a8dc70789709aa315325d5df06fb7e4 -https://conda.anaconda.org/t//conda-forge/noarch/snowballstemmer-2.2.0-pyhd8ed1ab_0.tar.bz2#4d22a9315e78c6827f806065957d566e -https://conda.anaconda.org/t//conda-forge/noarch/soupsieve-2.3.1-pyhd8ed1ab_0.tar.bz2#d821b295c4bd18ad27e1e19543a5784a -https://conda.anaconda.org/t//conda-forge/noarch/sphinxcontrib-applehelp-1.0.2-py_0.tar.bz2#20b2eaeaeea4ef9a9a0d99770620fd09 -https://conda.anaconda.org/t//conda-forge/noarch/sphinxcontrib-devhelp-1.0.2-py_0.tar.bz2#68e01cac9d38d0e717cd5c87bc3d2cc9 -https://conda.anaconda.org/t//conda-forge/noarch/sphinxcontrib-htmlhelp-2.0.0-pyhd8ed1ab_0.tar.bz2#77dad82eb9c8c1525ff7953e0756d708 -https://conda.anaconda.org/t//conda-forge/noarch/sphinxcontrib-jsmath-1.0.1-py_0.tar.bz2#67cd9d9c0382d37479b4d306c369a2d4 -https://conda.anaconda.org/t//conda-forge/noarch/sphinxcontrib-qthelp-1.0.3-py_0.tar.bz2#d01180388e6d1838c3e1ad029590aa7a -https://conda.anaconda.org/t//conda-forge/noarch/sphinxcontrib-serializinghtml-1.1.5-pyhd8ed1ab_2.tar.bz2#9ff55a0901cf952f05c654394de76bf7 -https://conda.anaconda.org/t//conda-forge/noarch/threadpoolctl-3.1.0-pyh8a188c0_0.tar.bz2#a2995ee828f65687ac5b1e71a2ab1e0c -https://conda.anaconda.org/t//conda-forge/noarch/toml-0.10.2-pyhd8ed1ab_0.tar.bz2#f832c45a477c78bebd107098db465095 -https://conda.anaconda.org/t//conda-forge/noarch/toolz-0.11.2-pyhd8ed1ab_0.tar.bz2#f348d1590550371edfac5ed3c1d44f7e -https://conda.anaconda.org/t//conda-forge/noarch/traitlets-5.2.0-pyhd8ed1ab_0.tar.bz2#b81786ff00b93d07560ea21d98a2b266 -https://conda.anaconda.org/t//conda-forge/noarch/typing-3.10.0.0-pyhd8ed1ab_0.tar.bz2#e6573ac68718f17b9d4f5c8eda3190f2 -https://conda.anaconda.org/t//conda-forge/noarch/typing_extensions-4.2.0-pyha770c72_1.tar.bz2#f0f7e024f94e23d3bfee0ab777bf335a -https://conda.anaconda.org/t//conda-forge/noarch/uc-micro-py-1.0.1-pyhd8ed1ab_0.tar.bz2#3ddf6684d9b274a12c94e509ca45656c -https://conda.anaconda.org/t//conda-forge/noarch/webencodings-0.5.1-py_1.tar.bz2#3563be4c5611a44210d9ba0c16113136 -https://conda.anaconda.org/t//conda-forge/noarch/websocket-client-1.3.2-pyhd8ed1ab_0.tar.bz2#da6f472c62b4eda0caf05e223729efcd -https://conda.anaconda.org/t//conda-forge/noarch/wheel-0.37.1-pyhd8ed1ab_0.tar.bz2#1ca02aaf78d9c70d9a81a3bed5752022 -https://conda.anaconda.org/t//conda-forge/noarch/zipp-3.8.0-pyhd8ed1ab_0.tar.bz2#050b94cf4a8c760656e51d2d44e4632c -https://conda.anaconda.org/t//conda-forge/noarch/asttokens-2.0.5-pyhd8ed1ab_0.tar.bz2#74badce16f060701fee55c39332f5253 -https://conda.anaconda.org/t//conda-forge/noarch/babel-2.10.1-pyhd8ed1ab_0.tar.bz2#2ec70a4a964b696170d730466c668f60 -https://conda.anaconda.org/t//conda-forge/noarch/beautifulsoup4-4.11.1-pyha770c72_0.tar.bz2#eeec8814bd97b2681f708bb127478d7d -https://conda.anaconda.org/t//conda-forge/osx-64/certifi-2021.10.8-py39h6e9494a_2.tar.bz2#087647d724d2a4c2859e203965f8f532 -https://conda.anaconda.org/t//conda-forge/osx-64/cffi-1.15.0-py39he338e87_0.tar.bz2#eb43d870c7a57ac917962fbb68455677 -https://conda.anaconda.org/t//conda-forge/osx-64/click-8.1.3-py39h6e9494a_0.tar.bz2#c33ff559cea7a89797decd6db3716b77 -https://conda.anaconda.org/t//conda-forge/noarch/clikit-0.6.2-pyh9f0ad1d_0.tar.bz2#159273f717a11e53b2656f8b6521a5e2 -https://conda.anaconda.org/t//conda-forge/osx-64/debugpy-1.6.0-py39hfd1d529_0.tar.bz2#43ec1c95677f4b111336cf091bdd064a -https://conda.anaconda.org/t//conda-forge/osx-64/docutils-0.16-py39h6e9494a_3.tar.bz2#c251f3d6defc0e68aef889b41e43df60 -https://conda.anaconda.org/t//conda-forge/osx-64/git-2.35.3-pl5321h33a4a8a_0.tar.bz2#f6e47a7e75c9b7fe6f14c97ee60918d6 -https://conda.anaconda.org/t//conda-forge/noarch/gitdb-4.0.9-pyhd8ed1ab_0.tar.bz2#40fc6b14a45dee3a3fd9f302d026108e -https://conda.anaconda.org/t//conda-forge/osx-64/greenlet-1.1.2-py39hfd1d529_2.tar.bz2#18ffa500d722e7d42705f5c75c003b80 -https://conda.anaconda.org/t//conda-forge/noarch/html5lib-1.1-pyh9f0ad1d_0.tar.bz2#b2355343d6315c892543200231d7154a -https://conda.anaconda.org/t//conda-forge/osx-64/importlib-metadata-4.11.3-py39h6e9494a_1.tar.bz2#9795d43694c096de21d356d286e34320 -https://conda.anaconda.org/t//conda-forge/noarch/importlib_resources-5.7.1-pyhd8ed1ab_0.tar.bz2#8a50c32f48abec73bc3dd4df0d133892 -https://conda.anaconda.org/t//conda-forge/osx-64/jedi-0.18.1-py39h6e9494a_1.tar.bz2#17fd05dd01cc6e30715af4a5703cf0ef -https://conda.anaconda.org/t//conda-forge/osx-64/jupyter_core-4.9.2-py39h6e9494a_0.tar.bz2#cbeae523c59b27307332bd81c08c1a2b -https://conda.anaconda.org/t//conda-forge/noarch/latexcodec-2.0.1-pyh9f0ad1d_0.tar.bz2#8d67904973263afd2985ba56aa2d6bb4 -https://conda.anaconda.org/t//conda-forge/osx-64/libcblas-3.9.0-14_osx64_openblas.tar.bz2#09a2b714708e1c4320266fb878c54a3c -https://conda.anaconda.org/t//conda-forge/osx-64/liblapack-3.9.0-14_osx64_openblas.tar.bz2#154dd37ef8a9c421bee629d030550a30 -https://conda.anaconda.org/t//conda-forge/osx-64/libmambapy-0.23.0-py39h3f08081_1.tar.bz2#081304f648bd313d010a9db591470d48 -https://conda.anaconda.org/t//conda-forge/noarch/linkify-it-py-1.0.3-pyhd8ed1ab_0.tar.bz2#ba4b07f6a132c77eb69ede31a6ed790b -https://conda.anaconda.org/t//conda-forge/osx-64/lxml-4.8.0-py39h63b48b0_3.tar.bz2#76e4798438383445c8b6c2462d20812c -https://conda.anaconda.org/t//conda-forge/noarch/markdown-it-py-1.1.0-pyhd8ed1ab_0.tar.bz2#84e8dfb1a9e6a824f32fd45b867271ca -https://conda.anaconda.org/t//conda-forge/osx-64/markupsafe-2.1.1-py39h63b48b0_1.tar.bz2#478672fe1f0c0fafc4e99152cd7e33fe -https://conda.anaconda.org/t//conda-forge/noarch/matplotlib-inline-0.1.3-pyhd8ed1ab_0.tar.bz2#be3bfd435802d2c768c6b2439f325f3d -https://conda.anaconda.org/t//conda-forge/osx-64/mistune-0.8.4-py39h89e85a6_1005.tar.bz2#761ee1d4bd35121fc36ec1a2b065cb99 -https://conda.anaconda.org/t//conda-forge/osx-64/msgpack-python-1.0.3-py39h7248d28_1.tar.bz2#d1f902b64e344a541dd2ecb6db544175 -https://conda.anaconda.org/t//conda-forge/noarch/packaging-20.9-pyh44b312d_0.tar.bz2#be69a38e912054a62dc82cc3c7711a64 -https://conda.anaconda.org/t//conda-forge/noarch/pexpect-4.8.0-pyh9f0ad1d_2.tar.bz2#5909e7b978141dd80d28dbf9de627827 -https://conda.anaconda.org/t//conda-forge/osx-64/poetry-core-1.0.8-py39h6e9494a_1.tar.bz2#d0e3d3795047ca328eeb399ea8118b56 -https://conda.anaconda.org/t//conda-forge/osx-64/psutil-5.9.0-py39h63b48b0_1.tar.bz2#7b458eead81bae468eb2f5c4a1d7c141 -https://conda.anaconda.org/t//conda-forge/osx-64/pycosat-0.6.3-py39h63b48b0_1010.tar.bz2#dfe087a3c51af3f3be906b8300fd03f2 -https://conda.anaconda.org/t//conda-forge/osx-64/pyrsistent-0.18.1-py39h63b48b0_1.tar.bz2#f30e91859ff641f638eeb49e1823c207 -https://conda.anaconda.org/t//conda-forge/osx-64/pysocks-1.7.1-py39h6e9494a_5.tar.bz2#e6845a71941ffc957c9e4ac0c4c88edd -https://conda.anaconda.org/t//conda-forge/noarch/python-dateutil-2.8.2-pyhd8ed1ab_0.tar.bz2#dd999d1cc9f79e67dbb855c8924c7984 -https://conda.anaconda.org/t//conda-forge/osx-64/pyyaml-6.0-py39h63b48b0_4.tar.bz2#d7aa7f40b99d7bc9e06b20a613d6a7b0 -https://conda.anaconda.org/t//conda-forge/osx-64/pyzmq-22.3.0-py39hc2dc7ec_2.tar.bz2#bfa4a6b43ccbd64fee3157d70274ddeb -https://conda.anaconda.org/t//conda-forge/osx-64/ruamel.yaml.clib-0.2.6-py39h63b48b0_1.tar.bz2#9318cdefc8c0bb7514870afa9aa72e37 -https://conda.anaconda.org/t//conda-forge/osx-64/ruamel_yaml-0.15.80-py39h89e85a6_1006.tar.bz2#a5d158eaeef49d101b6bee913b7f07a3 -https://conda.anaconda.org/t//conda-forge/osx-64/setuptools-62.1.0-py39h6e9494a_0.tar.bz2#d16c2aa67a8f229126295d265e644e88 -https://conda.anaconda.org/t//conda-forge/osx-64/sniffio-1.2.0-py39h6e9494a_3.tar.bz2#ecdb9ed5ed5e8fa34dd7786387c6263a -https://conda.anaconda.org/t//conda-forge/noarch/tinycss2-1.1.1-pyhd8ed1ab_0.tar.bz2#5d280406501e79dc7aa9c9ac31d25a80 -https://conda.anaconda.org/t//conda-forge/noarch/tomlkit-0.10.2-pyha770c72_0.tar.bz2#482e5775f80665a7c9f76cd72a66eae8 -https://conda.anaconda.org/t//conda-forge/osx-64/tornado-6.1-py39h63b48b0_3.tar.bz2#82c1e73cdc3ae881ef28d56a3a58225c -https://conda.anaconda.org/t//conda-forge/noarch/tqdm-4.64.0-pyhd8ed1ab_0.tar.bz2#6642233f341e1900d0c8e6eddb979c14 -https://conda.anaconda.org/t//conda-forge/noarch/typing-extensions-4.2.0-hd8ed1ab_1.tar.bz2#6d9d7480c5780514779967be2ee8b963 -https://conda.anaconda.org/t//conda-forge/osx-64/virtualenv-20.14.1-py39h6e9494a_0.tar.bz2#dc2e42e8457a7f53d98efbdded613328 -https://conda.anaconda.org/t//conda-forge/osx-64/anyio-3.5.0-py39h6e9494a_0.tar.bz2#b898a6cf2e081a521dd58285745b0762 -https://conda.anaconda.org/t//conda-forge/osx-64/argon2-cffi-bindings-21.2.0-py39h63b48b0_2.tar.bz2#8e0ec40d50d73eb7d63819c4b1939753 -https://conda.anaconda.org/t//conda-forge/noarch/backports.functools_lru_cache-1.6.4-pyhd8ed1ab_0.tar.bz2#c5b3edc62d6309088f4970b3eaaa65a6 -https://conda.anaconda.org/t//conda-forge/noarch/bleach-5.0.0-pyhd8ed1ab_0.tar.bz2#2a2ae7c56b8f72caba261363407b484a -https://conda.anaconda.org/t//conda-forge/osx-64/brotlipy-0.7.0-py39h63b48b0_1004.tar.bz2#7b42f41f7606f46a6b00ff4ac3c71b76 -https://conda.anaconda.org/t//conda-forge/noarch/cleo-0.8.1-pyhd8ed1ab_2.tar.bz2#4c82b11a3d06031bd58e7d869f53d965 -https://conda.anaconda.org/t//conda-forge/noarch/click-default-group-1.2.2-pyhd8ed1ab_1.tar.bz2#72a46ffc25701c173932fd55cf0965d3 -https://conda.anaconda.org/t//conda-forge/noarch/click-log-0.3.2-pyh9f0ad1d_0.tar.bz2#3a64d156136fad977df1b81a24b57ac0 -https://conda.anaconda.org/t//conda-forge/osx-64/conda-package-handling-1.8.1-py39h63b48b0_1.tar.bz2#09d59c368b74774aeb23183752bcb99f -https://conda.anaconda.org/t//conda-forge/osx-64/cryptography-36.0.2-py39h1644bb1_1.tar.bz2#aff910cb455e3885efc3e2e2070355b6 -https://conda.anaconda.org/t//conda-forge/noarch/ghp-import-2.1.0-pyhd8ed1ab_0.tar.bz2#6d8d61116031a3f5b1f32e7899785866 -https://conda.anaconda.org/t//conda-forge/noarch/gitpython-3.1.27-pyhd8ed1ab_0.tar.bz2#20acbaab17a50ac9b64138eb9a0e1af8 -https://conda.anaconda.org/t//conda-forge/noarch/importlib_metadata-4.11.3-hd8ed1ab_1.tar.bz2#bd6b6ae37c03e68061574d5e32fe5bd1 -https://conda.anaconda.org/t//conda-forge/noarch/jinja2-3.0.3-pyhd8ed1ab_0.tar.bz2#036d872c653780cb26e797e2e2f61b4c -https://conda.anaconda.org/t//conda-forge/noarch/joblib-1.1.0-pyhd8ed1ab_0.tar.bz2#07d1b5c8cde14d95998fd4767e1e62d2 -https://conda.anaconda.org/t//conda-forge/noarch/jsonschema-3.2.0-pyhd8ed1ab_3.tar.bz2#66125e28711d8ffc04a207a2b170316d -https://conda.anaconda.org/t//conda-forge/noarch/jupyter_client-7.3.1-pyhd8ed1ab_0.tar.bz2#38481a37ead8c37d2ad7b52d3bc2b0a7 -https://conda.anaconda.org/t//conda-forge/noarch/mdit-py-plugins-0.2.8-pyhd8ed1ab_0.tar.bz2#49236fcd746a124eb56d326f79e1d46d -https://conda.anaconda.org/t//conda-forge/osx-64/numpy-1.22.3-py39h214027c_2.tar.bz2#1188d955cb2a6808aac16a3d6de99c8f -https://conda.anaconda.org/t//conda-forge/noarch/pip-22.0.4-pyhd8ed1ab_0.tar.bz2#b1239ce8ef2a1eec485c398a683c5bff -https://conda.anaconda.org/t//conda-forge/noarch/portpicker-1.5.0-pyhd8ed1ab_0.tar.bz2#5f9595b4b3f50a0d572b0a7c8b4293c7 -https://conda.anaconda.org/t//conda-forge/noarch/pybtex-0.24.0-pyhd8ed1ab_2.tar.bz2#2099b86a7399c44c0c61cdb6de6915ba -https://conda.anaconda.org/t//conda-forge/osx-64/pydantic-1.9.0-py39h63b48b0_1.tar.bz2#86b0524a36495228560db966b4db2e9d -https://conda.anaconda.org/t//conda-forge/noarch/pygments-2.12.0-pyhd8ed1ab_0.tar.bz2#cb27e2ded147e5bcc7eafc1c6d343cb3 -https://conda.anaconda.org/t//conda-forge/osx-64/ruamel.yaml-0.17.21-py39h63b48b0_1.tar.bz2#71bfd0949c4080c77609f70f230ec875 -https://conda.anaconda.org/t//conda-forge/osx-64/sqlalchemy-1.4.36-py39h701faf5_0.tar.bz2#5cfa799d0bf4fe4a71d78c177bccaadb -https://conda.anaconda.org/t//conda-forge/noarch/stack_data-0.2.0-pyhd8ed1ab_0.tar.bz2#8c0ce3e6bf18a0c810125aef58a2a6f3 -https://conda.anaconda.org/t//conda-forge/osx-64/terminado-0.13.3-py39h6e9494a_1.tar.bz2#ed379b461931b5a65c45d1d7d899f327 -https://conda.anaconda.org/t//conda-forge/osx-64/watchdog-2.1.7-py39h147bbb7_1.tar.bz2#b5219ca4f9ff36da6997c66db7a2945c -https://conda.anaconda.org/t//conda-forge/noarch/argon2-cffi-21.3.0-pyhd8ed1ab_0.tar.bz2#a0b402db58f73aaab8ee0ca1025a362e -https://conda.anaconda.org/t//conda-forge/osx-64/click-completion-0.5.2-py39h6e9494a_3.tar.bz2#9aa698e05da37cc193be450d56d974e9 -https://conda.anaconda.org/t//conda-forge/noarch/jupyterlab_pygments-0.2.2-pyhd8ed1ab_0.tar.bz2#243f63592c8e449f40cd42eb5cf32f40 -https://conda.anaconda.org/t//conda-forge/osx-64/keyring-23.4.0-py39h6e9494a_2.tar.bz2#ffd5711dc153e8682bdb13a104544516 -https://conda.anaconda.org/t//conda-forge/noarch/nbformat-5.4.0-pyhd8ed1ab_0.tar.bz2#770f6659243e2c79a0b8488b0e463bd1 -https://conda.anaconda.org/t//conda-forge/osx-64/pandas-1.4.2-py39hbd61c47_1.tar.bz2#774ebb7e6c43776b8927eb043bb46577 -https://conda.anaconda.org/t//conda-forge/osx-64/pybtex-docutils-1.0.1-py39h6e9494a_1.tar.bz2#f000ab7da2a0177808ebc7225b4490e5 -https://conda.anaconda.org/t//conda-forge/noarch/pyopenssl-22.0.0-pyhd8ed1ab_0.tar.bz2#1d7e241dfaf5475e893d4b824bb71b44 -https://conda.anaconda.org/t//conda-forge/osx-64/scipy-1.8.0-py39h056f1c0_1.tar.bz2#1f27c3576dc5d94c2e089bd2a9e47f16 -https://conda.anaconda.org/t//conda-forge/noarch/wcwidth-0.2.5-pyh9f0ad1d_2.tar.bz2#5266fcd697043c59621fda522b3d78ee -https://conda.anaconda.org/t//conda-forge/noarch/altair-4.2.0-pyhd8ed1ab_1.tar.bz2#2867acfe48ceb3630b163632914720d9 -https://conda.anaconda.org/t//conda-forge/noarch/jupytext-1.13.8-pyh4b9bcc7_0.tar.bz2#aba00353637aa69640ec2ae150dc592d -https://conda.anaconda.org/t//conda-forge/noarch/nbclient-0.5.13-pyhd8ed1ab_0.tar.bz2#3edde88a191701cf052216c4ba353a83 -https://conda.anaconda.org/t//conda-forge/noarch/prompt-toolkit-3.0.29-pyha770c72_0.tar.bz2#9e720b57b22ef3032b4fb081697819dd -https://conda.anaconda.org/t//conda-forge/osx-64/scikit-learn-1.0.2-py39hd4eea88_0.tar.bz2#bffc17fe77541f4c7bf0ae766b69ea36 -https://conda.anaconda.org/t//conda-forge/noarch/urllib3-1.26.9-pyhd8ed1ab_0.tar.bz2#0ea179ee251aa7100807c35bc0252693 -https://conda.anaconda.org/t//conda-forge/noarch/altair_data_server-0.4.1-py_0.tar.bz2#5412ec3d2792d3e2f18e075cb05ffdaf -https://conda.anaconda.org/t//conda-forge/osx-64/ipython-8.3.0-py39h6e9494a_0.tar.bz2#3d1dfb144dae1eba434cade94fb5f1e5 -https://conda.anaconda.org/t//conda-forge/noarch/nbconvert-core-6.5.0-pyhd8ed1ab_0.tar.bz2#42f74c4b38a099025167e76a7437edf1 -https://conda.anaconda.org/t//conda-forge/noarch/requests-2.27.1-pyhd8ed1ab_0.tar.bz2#7c1c427246b057b8fa97200ecdb2ed62 -https://conda.anaconda.org/t//conda-forge/noarch/cachecontrol-0.12.11-pyhd8ed1ab_0.tar.bz2#6eefee9888f33f150b5d44d616b1a613 -https://conda.anaconda.org/t//conda-forge/osx-64/conda-4.12.0-py39h6e9494a_0.tar.bz2#8aa039a63e9d764ea3758530b4388066 -https://conda.anaconda.org/t//conda-forge/noarch/ensureconda-1.4.2-pyhd8ed1ab_0.tar.bz2#1bf97b25d058482fd73e62b1cdb932ef -https://conda.anaconda.org/t//conda-forge/osx-64/ipykernel-6.13.0-py39h71a6800_0.tar.bz2#79211930da8afc1c75f8033711f3b60d -https://conda.anaconda.org/t//conda-forge/noarch/jupyter_server-1.17.0-pyhd8ed1ab_0.tar.bz2#276b3b45443e2a84cfb4d128cb86c350 -https://conda.anaconda.org/t//conda-forge/noarch/nbconvert-pandoc-6.5.0-pyhd8ed1ab_0.tar.bz2#d7421adfc67100021d87032447066129 -https://conda.anaconda.org/t//conda-forge/noarch/pooch-1.6.0-pyhd8ed1ab_0.tar.bz2#6429e1d1091c51f626b5dcfdd38bf429 -https://conda.anaconda.org/t//conda-forge/noarch/requests-toolbelt-0.9.1-py_0.tar.bz2#402668adee8fcba9a9c265cdc2a88f5a -https://conda.anaconda.org/t//conda-forge/noarch/sphinx-4.5.0-pyh6c4a22f_0.tar.bz2#46b38d88c4270ff9ba78a89c83c66345 -https://conda.anaconda.org/t//conda-forge/noarch/jupyter-server-mathjax-0.2.5-pyhc268e32_0.tar.bz2#0393370c2dec5e92e1727a8650f908f7 -https://conda.anaconda.org/t//conda-forge/noarch/jupyterlab_server-2.13.0-pyhd8ed1ab_1.tar.bz2#ecead930bfd8c0f629c5b8bf5c1e3508 -https://conda.anaconda.org/t//conda-forge/osx-64/mamba-0.23.0-py39ha435c47_1.tar.bz2#e9f0f616c303e9bc41ef43f509a167f2 -https://conda.anaconda.org/t//conda-forge/noarch/myst-parser-0.15.2-pyhd8ed1ab_0.tar.bz2#0c2976e0a1af80ce224388da557eeece -https://conda.anaconda.org/t//conda-forge/noarch/nbconvert-6.5.0-pyhd8ed1ab_0.tar.bz2#156c180588e38b9f41758058824ec50f -https://conda.anaconda.org/t//conda-forge/noarch/notebook-shim-0.1.0-pyhd8ed1ab_0.tar.bz2#3a8e2c7dcc674f2cb0784f1faba57055 -https://conda.anaconda.org/t//conda-forge/osx-64/poetry-1.1.13-py39h6e9494a_1.tar.bz2#32d8696f9943cca10d86a74181407615 -https://conda.anaconda.org/t//conda-forge/noarch/pydata-sphinx-theme-0.7.2-pyhd8ed1ab_0.tar.bz2#123f5c52d6b4117225af45a52ec34997 -https://conda.anaconda.org/t//conda-forge/noarch/sphinx-comments-0.0.3-pyh9f0ad1d_0.tar.bz2#2ae3ce35de0c1cec45c94182694f8d1b -https://conda.anaconda.org/t//conda-forge/noarch/sphinx-copybutton-0.5.0-pyhd8ed1ab_0.tar.bz2#4c969cdd5191306c269490f7ff236d9c -https://conda.anaconda.org/t//conda-forge/noarch/sphinx-external-toc-0.2.4-pyhd8ed1ab_0.tar.bz2#91ae8770569b73f25e1127526c1329ed -https://conda.anaconda.org/t//conda-forge/noarch/sphinx-jupyterbook-latex-0.4.6-pyhd8ed1ab_0.tar.bz2#6e4a69a0c8adbb48178fdf3efa24fa4c -https://conda.anaconda.org/t//conda-forge/noarch/sphinx-multitoc-numbering-0.1.3-pyhd8ed1ab_0.tar.bz2#40749a4d0f0d2e11c65fb26c1cd16a90 -https://conda.anaconda.org/t//conda-forge/noarch/sphinx-panels-0.6.0-pyhd8ed1ab_0.tar.bz2#6eec6480601f5d15babf9c3b3987f34a -https://conda.anaconda.org/t//conda-forge/noarch/sphinx-thebe-0.1.2-pyhd8ed1ab_0.tar.bz2#1d4fdd342aa955085a0f21e26bb585f7 -https://conda.anaconda.org/t//conda-forge/noarch/sphinx-togglebutton-0.3.1-pyhd8ed1ab_0.tar.bz2#71418887aa6599ea2935f4958e5e1d15 -https://conda.anaconda.org/t//conda-forge/noarch/sphinxcontrib-bibtex-2.4.2-pyhd8ed1ab_0.tar.bz2#d826ac2b3edfe7a8113596c2023f092b -https://conda.anaconda.org/t//conda-forge/noarch/conda-lock-1.0.5-pyhd8ed1ab_0.tar.bz2#7544764bbf4941cc954bb80911f3b201 -https://conda.anaconda.org/t//conda-forge/noarch/nbdime-3.1.1-pyhd8ed1ab_0.tar.bz2#38dc061ffabe665b79f4c7c52cefa809 -https://conda.anaconda.org/t//conda-forge/noarch/notebook-6.4.11-pyha770c72_0.tar.bz2#da25720a88aa3cbb3e16df740783da74 -https://conda.anaconda.org/t//conda-forge/noarch/sphinx-book-theme-0.1.10-pyhd8ed1ab_1.tar.bz2#194ec0159031da65b653c665bb1678f6 -https://conda.anaconda.org/t//conda-forge/noarch/jupyter-cache-0.4.3-pyhd8ed1ab_0.tar.bz2#03cd9218c96d513854bfc8714eaf9451 -https://conda.anaconda.org/t//conda-forge/noarch/jupyter_contrib_core-0.3.3-py_2.tar.bz2#4704781b0a914d67e4ae8e9f9f5c37a0 -https://conda.anaconda.org/t//conda-forge/noarch/nbclassic-0.3.7-pyhd8ed1ab_0.tar.bz2#a8a7139140a7512c90514444444a4991 -https://conda.anaconda.org/t//conda-forge/osx-64/widgetsnbextension-3.6.0-py39h6e9494a_0.tar.bz2#2b05800adc9c5d71f8c792167bf0556d -https://conda.anaconda.org/t//conda-forge/noarch/ipywidgets-7.7.0-pyhd8ed1ab_0.tar.bz2#a3d2ccd3d9f9fcb65765c22f500529b4 -https://conda.anaconda.org/t//conda-forge/osx-64/jupyter_highlight_selected_word-0.2.0-py39h6e9494a_1005.tar.bz2#c054e75a5b8524199cfb8f194acf5832 -https://conda.anaconda.org/t//conda-forge/noarch/jupyter_latex_envs-1.4.6-pyhd8ed1ab_1002.tar.bz2#4b888fd7d6b4cdb6736878b2cf8ea951 -https://conda.anaconda.org/t//conda-forge/noarch/jupyter_nbextensions_configurator-0.4.1-pyhd8ed1ab_2.tar.bz2#19a2fa481008976df3ed8ce5d4dfb8fa -https://conda.anaconda.org/t//conda-forge/noarch/jupyterlab-3.4.0-pyhd8ed1ab_0.tar.bz2#79d9efa21a8dbe3f9fdbe8069a61ac26 -https://conda.anaconda.org/t//conda-forge/noarch/jupyter-sphinx-0.3.2-pyhd8ed1ab_1.tar.bz2#a47ca0f91417e5d29d075ca416254466 -https://conda.anaconda.org/t//conda-forge/noarch/jupyter_contrib_nbextensions-0.5.1-pyhd8ed1ab_2.tar.bz2#ee3820cb73867efb8c928b93e55f4de3 -https://conda.anaconda.org/t//conda-forge/noarch/myst-nb-0.13.2-pyhd8ed1ab_0.tar.bz2#800e968e63eb593c651a14907fd82d26 -https://conda.anaconda.org/t//conda-forge/noarch/jupyter-book-0.12.3-pyhd8ed1ab_0.tar.bz2#1d6c5efa323c2cfd6196af5524aa5b78 diff --git a/unused/install/environment.yml b/unused/install/environment.yml deleted file mode 100644 index c05e6868..00000000 --- a/unused/install/environment.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: dsci100 -channels: - - eoas_ubc - - conda-forge -dependencies: - - altair - - altair_data_server - - click - - conda-lock - - ghp-import - - git - - jupyterlab - - jupytext - - jupyter_contrib_nbextensions - - mamba - - nodejs - - notebook - - numpy - - pandas - - pip - - python<3.10 - - jinja2==3.0.3 - - scikit-learn - - openpyxl -# conda-lock --kind explicit --file environment.yml -p linux-64 diff --git a/unused/install/requirements.txt b/unused/install/requirements.txt deleted file mode 100644 index 56cee422..00000000 --- a/unused/install/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -git+https://github.com/eoas-ubc/jb_tools.git@master diff --git a/unused/notes/Readme_githubio.md b/unused/notes/Readme_githubio.md deleted file mode 100644 index a267c4d0..00000000 --- a/unused/notes/Readme_githubio.md +++ /dev/null @@ -1,61 +0,0 @@ -# building an html book for github.io - -## Setup - -1. Fork https://github.com/UBC-DSCI/introduction-to-datascience-python -2. Upload a ssh public key to your github account -3. Edit your .ssh/config to allow ssh read/write to github with your user id -- using a nickname like phaustin for your github host entry: - -```bash - Host phaustin - HostName github.com - User git - IdentityFile ~/.ssh/new_pha_git - IdentitiesOnly yes -``` - -4. Clone your fork using ssh: - -```bash - git clone phaustin:phaustin/introduction-to-datascience-python -``` - -5. Add the upstream remote: - -```bash - git remote add upstream phaustin:UBC-DSCI/introduction-to-datascience-python -``` - -6. Fetch the upstream branches - -```bash - git fetch upstream -``` - -6. Create the topic branch: - -```bash - git checkout -b classification1 origin/classification1 -``` - -7. If necessary (i.e. if someone has pushed new changes to main), rebase origin on upstream - -```bash - git rebase upstream/classification1 -``` - -8. Build the book and push the html to gh-pages - -```bash - jb build source - ./push_html.sh -``` - -9. In the setting for your forked repo, turn on github pages and set it to source the gh-pages branch - -10. Point your browser to the fork's github.io address: - - https://phaustin.github.io/introduction-to-datascience-python/classification1.html# - - -** note that it might take a minute or two for github to overwrite the old html on their server. There can also be browser cache issues. I use the "clear cache" extension on chrome and also do a hard refresh: see https://fabricdigital.co.nz/blog/how-to-hard-refresh-your-browser-and-clear-cache. I also sometimes write a version number in the top header so I know that I'm looking at the current version. You can also use an incognito window to be sure you've got a fresh session. diff --git a/unused/notes/Readme_install.md b/unused/notes/Readme_install.md deleted file mode 100644 index 78f92680..00000000 --- a/unused/notes/Readme_install.md +++ /dev/null @@ -1,72 +0,0 @@ -# building the book - -## fork or clone the repository - -* git clone https://github.com/UBC-DSCI/introduction-to-datascience-python.git - cd introduction-to-datascience-python - git checkout -b myst - -## installing the build environment - -1) Download miniforge from https://github.com/conda-forge/miniforge/releases - -2) install and activate the base environment in a shell - -3) do: - - cd install - mamba env create --name dsci --file environment.yml - conda activate dsci - pip install -r requirements.txt - npm install -g live-server - -This should give you an environment with jupyter-book, scikit-learn, and a live reload server which can automatically build the book following https://github.com/eoas-ubc/jb_tools/blob/master/tools_demo/Readme_conda.md - -4) If you'd like to produce and use a lock file, do: - - conda-lock --kind explicit --file environment.yml -p ostype - - where ostype is one of `linux-64`, `win-64` or `osx-64`. Then use the lockfile with: - - mamba activate base - mamba install --name dsci --file conda-lock-ostype - -## building the book - -5) do: - - cd source - jb build . - -6) view the html in `./_build/html` - -## publishing the book - -7) do: - - cd introduction-to-datascience-python - ./push_html.sh - - this will update the github.io version at: https://phaustin.github.io/introduction-to-datascience-python/intro.html - - -## working with livereload - -To have the book built locally when you change a file: - -8) in one terminal, start a source file watcher: - - cd introduction-to-datascience-python - ebp-watch jb source - -9) in another terminal, use live-server to open a local browser tab with yoru book - - live-server source/_build/html/ - -After you do this, changing any file in the source folder should trigger a jb build and browser refresh. - - - - - - diff --git a/unused/notes/Readme_overview.md b/unused/notes/Readme_overview.md deleted file mode 100644 index 944b668c..00000000 --- a/unused/notes/Readme_overview.md +++ /dev/null @@ -1,40 +0,0 @@ -# OCESE summer notes - -## resource links - -### DSCI 100 - -* the instructor repo (ubc enterprise github) https://github.ubc.ca/dsci-100-instructor -* the student repo) https://github.com/ubc-dsci/dsci-100-student - -### python textbook repo and forks - -* https://github.com/ubc-dsci/introduction-to-datascience-python -* https://github.com/lheagy/introduction-to-datascience-python -* https://github.com/phaustin/introduction-to-datascience-python - -#### textbook branches - -* dev: https://github.com/ubc-dsci/introduction-to-datascience-python/tree/dev -* chapter 2 (reading,navya): https://github.com/ubc-dsci/introduction-to-datascience-python/tree/reading - * tutorial: https://github.ubc.ca/UBC-DSCI/dsci-100-instructor/tree/py_tutorial_reading - * worksheet: https://github.ubc.ca/UBC-DSCI/dsci-100-instructor/tree/py_worksheet_reading - * issue: https://github.com/UBC-DSCI/introduction-to-datascience-python/issues/4 -* chapter 5 (classification1, gloria): https://github.com/ubc-dsci/introduction-to-datascience-python/tree/classification1 - * tutorial: https://github.ubc.ca/UBC-DSCI/dsci-100-instructor/tree/py_tutorial_classification1 - * worksheet: https://github.ubc.ca/UBC-DSCI/dsci-100-instructor/tree/py_worksheet_classification1 - * issue: https://github.com/UBC-DSCI/introduction-to-datascience-python/issues/3 - - -### R textbook - -* the R textbook repo: https://github.com/ubc-dsci/introduction-to-datascience -* rendered textbook (R version): https://datasciencebook.ca/ - - -### nbgrader - -* rudaux: https://github.com/UBC-DSCI/rudaux -* autotest (private): https://github.com/ubc-dsci/autotest - - diff --git a/unused/notes/figure-caption.ipynb b/unused/notes/figure-caption.ipynb deleted file mode 100644 index b6d0b2f6..00000000 --- a/unused/notes/figure-caption.ipynb +++ /dev/null @@ -1,252 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Test notebook for figure captions" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - ":::{figure-md} figception\n", - "\"figs\"\n", - "\n", - "A fig of a fig.\n", - ":::" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Testing a ref to a fig here {ref}`figception` and a numbered fig here {numref}`figception`." - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 1, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from matplotlib import rcParams, cycler\n", - "import matplotlib.pyplot as plt\n", - "import numpy as np\n", - "plt.ion()" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
    " - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], - "source": [ - "# Fixing random state for reproducibility\n", - "np.random.seed(19680801)\n", - "\n", - "N = 10\n", - "data = [np.logspace(0, 1, 100) + np.random.randn(100) + ii for ii in range(N)]\n", - "data = np.array(data).T\n", - "cmap = plt.cm.coolwarm\n", - "rcParams['axes.prop_cycle'] = cycler(color=cmap(np.linspace(0, 1, N)))\n", - "\n", - "\n", - "from matplotlib.lines import Line2D\n", - "custom_lines = [Line2D([0], [0], color=cmap(0.), lw=4),\n", - " Line2D([0], [0], color=cmap(.5), lw=4),\n", - " Line2D([0], [0], color=cmap(1.), lw=4)]\n", - "\n", - "fig, ax = plt.subplots(figsize=(10, 5))\n", - "lines = ax.plot(data)\n", - "ax.legend(custom_lines, ['Cold', 'Medium', 'Hot']);" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "```{figure} data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7\n", - ":name: mpl\n", - ":figclass: caption-hack\n", - "\n", - "Testing an mpl plot\n", - "```" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Ref to the mpl fig here {ref}`mpl` and {numref}`mpl`." - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - "
    \n", - "" - ], - "text/plain": [ - "alt.Chart(...)" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "import altair as alt\n", - "from vega_datasets import data\n", - "\n", - "source = data.cars()\n", - "\n", - "alt.Chart(source).mark_circle(size=60).encode(\n", - " x='Horsepower',\n", - " y='Miles_per_Gallon',\n", - " color='Origin',\n", - " tooltip=['Name', 'Origin', 'Horsepower', 'Miles_per_Gallon']\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "```{figure} data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7\n", - ":name: altair\n", - ":figclass: caption-hack\n", - "\n", - "Testing an Altair plot.\n", - "```" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Ref to the mpl fig here {ref}`altair` and {numref}`altair`." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "celltoolbar": "Edit Metadata", - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.4" - }, - "widgets": { - "application/vnd.jupyter.widget-state+json": { - "state": {}, - "version_major": 2, - "version_minor": 0 - } - } - }, - "nbformat": 4, - "nbformat_minor": 4 -}