diff --git a/circuit/25-1/3/DVJK_table.png b/circuit/25-1/3/DVJK_table.png new file mode 100644 index 0000000..3125ead Binary files /dev/null and b/circuit/25-1/3/DVJK_table.png differ diff --git a/circuit/25-1/3/TDCE_schema.png b/circuit/25-1/3/TDCE_schema.png new file mode 100644 index 0000000..ce3bc02 Binary files /dev/null and b/circuit/25-1/3/TDCE_schema.png differ diff --git a/circuit/25-1/3/TDCE_timing.png b/circuit/25-1/3/TDCE_timing.png new file mode 100644 index 0000000..bc1e562 Binary files /dev/null and b/circuit/25-1/3/TDCE_timing.png differ diff --git a/circuit/25-1/3/TJK.png b/circuit/25-1/3/TJK.png new file mode 100644 index 0000000..feebca7 Binary files /dev/null and b/circuit/25-1/3/TJK.png differ diff --git a/circuit/25-1/3/TJK_schema.png b/circuit/25-1/3/TJK_schema.png new file mode 100644 index 0000000..09dff24 Binary files /dev/null and b/circuit/25-1/3/TJK_schema.png differ diff --git a/circuit/25-1/3/TJK_timing.png b/circuit/25-1/3/TJK_timing.png new file mode 100644 index 0000000..beac02d Binary files /dev/null and b/circuit/25-1/3/TJK_timing.png differ diff --git a/circuit/25-1/3/TT.png b/circuit/25-1/3/TT.png new file mode 100644 index 0000000..4bd82c3 Binary files /dev/null and b/circuit/25-1/3/TT.png differ diff --git a/circuit/25-1/3/TT_schema.png b/circuit/25-1/3/TT_schema.png new file mode 100644 index 0000000..97acb3b Binary files /dev/null and b/circuit/25-1/3/TT_schema.png differ diff --git a/circuit/25-1/3/TT_table.png b/circuit/25-1/3/TT_table.png new file mode 100644 index 0000000..8a797fc Binary files /dev/null and b/circuit/25-1/3/TT_table.png differ diff --git a/circuit/25-1/3/TT_timing.png b/circuit/25-1/3/TT_timing.png new file mode 100644 index 0000000..3d0d2d8 Binary files /dev/null and b/circuit/25-1/3/TT_timing.png differ diff --git a/circuit/25-1/3/TT_transition.png b/circuit/25-1/3/TT_transition.png new file mode 100644 index 0000000..c5bf698 Binary files /dev/null and b/circuit/25-1/3/TT_transition.png differ diff --git a/circuit/25-1/3/lab3.pdf b/circuit/25-1/3/lab3.pdf new file mode 100644 index 0000000..5d63f78 Binary files /dev/null and b/circuit/25-1/3/lab3.pdf differ diff --git a/circuit/25-1/3/schema.png b/circuit/25-1/3/schema.png new file mode 100644 index 0000000..220e41c Binary files /dev/null and b/circuit/25-1/3/schema.png differ diff --git a/ds/25-1/2/1-00_introduction.ipynb b/ds/25-1/2/1-00_introduction.ipynb new file mode 100644 index 0000000..45668af --- /dev/null +++ b/ds/25-1/2/1-00_introduction.ipynb @@ -0,0 +1,278 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "19051402", + "metadata": { + "tags": [] + }, + "source": [ + "" + ] + }, + { + "cell_type": "markdown", + "id": "67ed6062", + "metadata": {}, + "source": [ + "# Fundamentals of Accelerated Data Science #" + ] + }, + { + "cell_type": "markdown", + "id": "a65f57f0", + "metadata": {}, + "source": [ + "## 00 - Introduction ##\n", + "Welcome to NVIDIA's Deep Learning Institute workshop on the Fundamentals of Accelerated Data Science. This interactive lab offers practical experience with every stage of the development process, empowering participants to tailor solutions for their unique applications." + ] + }, + { + "cell_type": "markdown", + "id": "50d32b6c", + "metadata": {}, + "source": [ + "**Learning Objectives**\n", + "
\n", + "In this workshop, you will learn: \n", + "* Overview of data science\n", + "* Demonstrations of data science workflows\n", + "* How acceleration is achieved\n", + "* How to design operations to maximize GPU acceleration\n", + "* Implications of acceleration" + ] + }, + { + "cell_type": "markdown", + "id": "3a02c2b6", + "metadata": {}, + "source": [ + "### JupyterLab ###\n", + "For this hands-on lab, we use [JupyterLab](https://jupyterlab.readthedocs.io/en/stable/) to manage our environment. The [JupyterLab Interface](https://jupyterlab.readthedocs.io/en/stable/user/interface.html) is a dashboard that provides access to interactive iPython notebooks, as well as the folder structure of our environment and a terminal window into the Ubuntu operating system. The first view includes a **menu bar** at the top, a **file browser** in the **left sidebar**, and a **main work area** that is initially open to this \"introduction\" notebook. \n", + "\n", + "

\n", + "\n", + "* The file browser can be navigated just like any other file explorer. A double click on any of the items will open a new tab with its content. \n", + "* The main work area includes tabbed views of open files that can be closed, moved, and edited as needed. \n", + "* The notebooks, including this one, consist of a series of content and code **cells**. To execute code in a code cell, press `Shift+Enter` or the `Run` button in the menu bar above, while a cell is highlighted. Sometimes, a content cell will get switched to editing mode. Executing the cell with `Shift+Enter` or the `Run` button will switch it back to a readable form.\n", + "* To interrupt cell execution, click the `Stop` button in the menu bar or navigate to the `Kernel` menu, and select `Interrupt Kernel`. \n", + "* We can use terminal commands in the notebook cells by prepending an exclamation point/bang(`!`) to the beginning of the command.\n", + "* We can create additional interactive cells by clicking the `+` button above, or by switching to command mode with `Esc` and using the keyboard shortcuts `a` (for new cell above) and `b` (for new cell below)." + ] + }, + { + "cell_type": "markdown", + "id": "4492c58d", + "metadata": {}, + "source": [ + "\n", + "### Exercise #1 - Practice ###\n", + "**Instructions**:
\n", + "* Try executing the simple print statement in the below cell.\n", + "* Then try executing the terminal command in the cell below." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e69a6515", + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "# DO NOT CHANGE THIS CELL\n", + "# activate this cell by selecting it with the mouse or arrow keys then use the keyboard shortcut [Shift+Enter] to execute\n", + "print('This is just a simple print statement.')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e54fe372", + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "# DO NOT CHANGE THIS CELL\n", + "!echo 'This is another simple print statement.'" + ] + }, + { + "cell_type": "markdown", + "id": "c2e5151b-4842-465e-a20d-bb64af66d011", + "metadata": {}, + "source": [ + "\n", + "### Exercise #2 - Available GPU Accelerators ###\n", + "The `nvidia-smi` (NVIDIA System Management Interface) command is a powerful utility for managing and monitoring NVIDIA GPU devices. It will print information about available GPUs, their current memory usage, and any processes currently utilizing them. \n", + "\n", + "**Instructions**:
\n", + "* Execute the below cell to learn about this environment's available GPUs. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "08d543eb-a951-4eb9-8107-b13c01b3ac46", + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "# DO NOT CHANGE THIS CELL\n", + "!nvidia-smi" + ] + }, + { + "cell_type": "markdown", + "id": "adee74e3-613a-4986-be34-ff3ae113ccc7", + "metadata": {}, + "source": [ + "**Note**: Currently, GPU memory usage is minimal, with no active processes utilizing the GPUs. Throughout our session, we'll employ this command to monitor memory consumption. When conducting GPU-based data analysis, it's advisable to maintain approximately 50% of GPU memory free, allowing for operations that may expand data stored on the device." + ] + }, + { + "cell_type": "markdown", + "id": "f0839f2e-dfe3-4d8f-8010-ed8445c171fb", + "metadata": {}, + "source": [ + "\n", + "### Exercise #3 - Magic Commands ###\n", + "The Jupyter environment come installed with *magic* commands, which can be recognized by the presence of `%` or `%%`. We will be using two magic commands liberally in this workshop: \n", + "* `%time`: prints summary information about how long it took to run code for a single line of code\n", + "* `%%time`: prints summary information about how long it took to run code for an entire cell\n", + "\n", + "**Instructions**:
\n", + "* Execute the below cell to import the `time` library. \n", + "* Execute the cell below to time the single line of code. \n", + "* Execute the cell below to time the entire cell. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b1c34489-7812-4ffe-bd2e-748a52903481", + "metadata": {}, + "outputs": [], + "source": [ + "# DO NOT CHANGE THIS CELL\n", + "from time import sleep" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "db1d5de9-f6e6-4984-8c32-f13b51aa27db", + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "# DO NOT CHANGE THIS CELL\n", + "# %time only times one line\n", + "%time sleep(2) \n", + "sleep(1)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "daf2f6f0-58a9-43a5-af8f-0b69b4a2a3a8", + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "%%time\n", + "# DO NOT CHANGE THIS CELL\n", + "# %%time will time the entire cell\n", + "sleep(1)\n", + "sleep(1)" + ] + }, + { + "cell_type": "markdown", + "id": "42ed873e-f7b5-4668-8e96-ce31d53d43b1", + "metadata": {}, + "source": [ + "\n", + "### Exercise #4 - Jupyter Kernels and GPU Memory ###\n", + "The compute backend for Jupyter is called the *kernel*. The Jupyter environment starts up a separate kernel for each new notebook. The many notebooks in this workshop are each intended to stand alone with regard to memory and computation. \n", + "\n", + "To ensure we have enough memory and compute for each notebook, we can clear the memory at the conclusion of each notebook in two ways: \n", + "1. Shut down the kernel with `ipykernel.kernelapp.IPKernelApp.do_shutdown()` or\n", + "2. Shut down the kernel through the *Running Terminals and Kernels* panel. \n", + "\n", + "**Instructions**:
\n", + "* Execute the below cell to shut down and restart the current kernel. \n", + "* Shut down the current kernel through the *Running Terminals and Kernels* panel.\n", + "\n", + "

\n", + "\n", + "**Note**: Restarting the kernel from the *Kernel* menu will only clear the memory for *the current notebook's kernel*, while notebooks other than the one we're working on may still have memory allocated for *their unique kernels*. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "98e05b77-6019-428b-8e18-a2477692ef6f", + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "# DO NOT CHANGE THIS CELL\n", + "import IPython\n", + "app = IPython.Application.instance()\n", + "app.kernel.do_shutdown(True)" + ] + }, + { + "cell_type": "markdown", + "id": "0321075e-433e-42d4-b849-de3fa17b54e1", + "metadata": {}, + "source": [ + "**Note**: Executing the provided code cell will shut down the kernel and activate a popup indicating that the kernel has restarted." + ] + }, + { + "cell_type": "markdown", + "id": "8e950df2", + "metadata": {}, + "source": [ + "**Well Done!** Let's move to the [next notebook](1-01_section_overview.ipynb). " + ] + }, + { + "cell_type": "markdown", + "id": "b604003a", + "metadata": {}, + "source": [ + "" + ] + } + ], + "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.15" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/ds/25-1/2/1-01_section_overview.ipynb b/ds/25-1/2/1-01_section_overview.ipynb new file mode 100644 index 0000000..0b4e2ef --- /dev/null +++ b/ds/25-1/2/1-01_section_overview.ipynb @@ -0,0 +1,78 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "b53a7b12-538d-4459-b82a-a35c8c417849", + "metadata": {}, + "source": [ + "" + ] + }, + { + "cell_type": "markdown", + "id": "ae497b71-bc43-471e-8970-88a1878e7cf9", + "metadata": {}, + "source": [ + "# Fundamentals of Accelerated Data Science # " + ] + }, + { + "cell_type": "markdown", + "id": "3a61cc06-80da-4f73-ba61-8ff1b5af71d8", + "metadata": {}, + "source": [ + "## 01 - Section Overview ##\n", + "\n", + "**Table of Contents**\n", + "This section focuses on data processing. We'll work with multiple datasets, conduct high-level analyses, and prepare the data for subsequent machine learning tasks. \n", + "
\n", + "* **1-01_section_overview.ipynb**\n", + "* **1-02_data_manipulation.ipynb**\n", + "* **1-03_memory_management.ipynb**\n", + "* **1-04_interoperability.ipynb**\n", + "* **1-05_grouping.ipynb**\n", + "* **1-06_data_visualization.ipynb**\n", + "* **1-07_etl.ipynb**\n", + "* **1-08_dask-cudf.ipynb**\n", + "* **1-09_cudf-polars.ipynb**" + ] + }, + { + "cell_type": "markdown", + "id": "9b1485a5-00e8-4495-85b0-b48671674818", + "metadata": {}, + "source": [ + "**Well Done!** Let's move to the [next notebook](1-02_data_manipulation.ipynb). " + ] + }, + { + "cell_type": "markdown", + "id": "81e47f0a-547e-4714-878d-34eb9b75c835", + "metadata": {}, + "source": [ + "" + ] + } + ], + "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.15" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/ds/25-1/2/1-02_data_manipulation.ipynb b/ds/25-1/2/1-02_data_manipulation.ipynb new file mode 100644 index 0000000..eac4803 --- /dev/null +++ b/ds/25-1/2/1-02_data_manipulation.ipynb @@ -0,0 +1,2005 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "b53a7b12-538d-4459-b82a-a35c8c417849", + "metadata": {}, + "source": [ + "" + ] + }, + { + "cell_type": "markdown", + "id": "ae497b71-bc43-471e-8970-88a1878e7cf9", + "metadata": {}, + "source": [ + "# Fundamentals of Accelerated Data Science # " + ] + }, + { + "cell_type": "markdown", + "id": "a149b6d1-1880-4a5d-9d71-f963d3097aa4", + "metadata": {}, + "source": [ + "## 02 - Data Manipulation ##\n", + "\n", + "**Table of Contents**\n", + "
\n", + "This notebook explores the fundamentals of data acquisition and manipulation using DataFrame APIs, covering essential techniques for handling and processing datasets. This notebook covers the below sections: \n", + "1. [Data Background](#Data-Background)\n", + "1. [cuDF and pandas](#cuDF-and-pandas)\n", + " * [pandas](#pandas)\n", + " * [cuDF](#cuDF)\n", + "3. [Data Acquisition](#Data-Acquisition)\n", + "4. [Initial Data Exploration](#Initial-Data-Exploration)\n", + "5. [Indexing and Data Selection with `.loc` Accessor](#Indexing-and-Data-Selection-with-.loc-Accessor)\n", + "6. [Basic Operations](#Basic-Operations)\n", + " * [Exercise #1 - Convert `county` Column to Title Case](#Exercise-#1---Convert-county-Column-to-Title-Case)\n", + "7. [Aggregation](#Aggregation)\n", + "8. [Applying User-Defined Functions (UDFs) with `.map()` and `.apply()`](#Applying-User-Defined-Functions-(UDFs)-with-.map()-and-.apply())\n", + "9. [Filtering with `.loc` and Boolean Mask](#Filtering-with-.loc-and-Boolean-Mask)\n", + " * [Exercise #2 - Counties North of Sunderland](#Exercise-#2---Counties-North-of-Sunderland)\n", + "10. [Creating New Columns](#Creating-New-Columns)\n", + "11. [pandas vs. cuDF](#pandas-vs.-cuDF)\n", + "12. [cuDF pandas](#cuDF-pandas)\n", + " * [Exercise #3 - Automatic Acceleration](#Exercise-#3---Automatic-Acceleration)" + ] + }, + { + "cell_type": "markdown", + "id": "8b739635-4883-40b2-94e9-7a08f853871c", + "metadata": {}, + "source": [ + "## Data Background ##\n", + "For this workshop, we will be reading almost 60 million records (corresponding to the entire population of England and Wales) which were synthesized from official UK census data. " + ] + }, + { + "cell_type": "markdown", + "id": "95e6bbed-1c08-4002-837c-392d5a12658f", + "metadata": {}, + "source": [ + "## cuDF and pandas ##" + ] + }, + { + "cell_type": "markdown", + "id": "050926cb-1dee-447a-9da8-49ebb1292d55", + "metadata": {}, + "source": [ + "### pandas ###\n", + "[pandas](https://pandas.pydata.org/) is a widely-used open-source library for data manipulation and analysis in Python. It provides high-performance, easy-to-use data structures and tools for working with structured data. It popularized the term DataFrame as a data structure for statistical computing. In data science, pandas is used for: \n", + "* **Data loading and writing**: reads from and writes to various file formats like CSV, Excel, JSON, and SQL databases\n", + "* **Data cleaning and processing/preprocessing**: helps users with handling missing data, merging datasets, and reshaping data\n", + "* **Data analysis**: performs grouping, aggregating, and statistical operations\n", + "\n", + "**Note**: Data preprocessing refers to the process of transforming raw data into a format that is more suitable for analysis and other downstream tasks. " + ] + }, + { + "cell_type": "markdown", + "id": "79e09f10-be1d-4ffe-9247-1e605e3f450f", + "metadata": {}, + "source": [ + "### cuDF ###\n", + "Similarly, [cuDF](https://docs.rapids.ai/api/cudf/stable/) is a Python GPU DataFrame library for loading, joining, aggregating, filtering, and otherwise manipulating data. cuDF is designed to accelerate data science workflows by utilizing the parallel processing power of GPUs, potentially offering significant speed improvements over CPU-based alternatives for large datasets. The key features of cuDF include: \n", + "* **GPU Acceleration**: leverages NVIDIA GPUs for fast data processing and analysis\n", + "* **pandas-like API**: provides users a familiar interface and transition to GPU-based computing\n", + "* **Integration with other RAPIDS libraries**: works seamlessly with other GPU-accelerated tools in the RAPIDS ecosystem" + ] + }, + { + "cell_type": "markdown", + "id": "ff5519e2-f77f-4160-b362-979301705733", + "metadata": {}, + "source": [ + "**Note**: Both Pandas and cuDF serve similar purposes in data manipulation and analysis, but cuDF is specifically optimized for GPU acceleration, making it particularly useful for working with large datasets where performance is critical." + ] + }, + { + "cell_type": "markdown", + "id": "770fb1d8-73c5-4c45-a1e4-599f66e6b833", + "metadata": {}, + "source": [ + "## Data Acquisition ##\n", + "In our context, data acquisition refers to the process of collecting and importing data from various sources into a Python environment for analysis, processing, and manipulation. Data can come from a variety of sources: \n", + "* Local file in various formats\n", + "* Databases\n", + "* APIs\n", + "* Web scraping\n", + "\n", + "It's worth noting that Python's rich ecosystem of libraries makes it versatile for acquiring data from various sources, allowing data scientists to work with diverse datasets efficiently. CPU processing will be responsible for acquiring data from APIs or Web Scraping. In most cases, network bandwidth will likely be the bottleneck. Furthermore, cuDF doesn't have a way to get transactions from SQL data bases directly into GPU memory. The recommended approach for reading data from a database is to first use CPU-based methods (i.e. pandas), then convert to cuDF for GPU-accelerated processing. \n", + "\n", + "Below we use the `head` linux command to display the beginning of the data file. This allows us to understand how to read the data correctly. " + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "247d2b96-1bce-4e26-89bd-d659df3528d7", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\"age\",\"sex\",\"county\",\"lat\",\"long\",\"name\"\n", + "0,\"m\",\"DARLINGTON\",54.53364379,-1.524400639,\"FRANCIS\"\n", + "0,\"m\",\"DARLINGTON\",54.42625551,-1.465313919,\"EDWARD\"\n", + "0,\"m\",\"DARLINGTON\",54.55520036,-1.496417277,\"TEDDY\"\n", + "0,\"m\",\"DARLINGTON\",54.54790635,-1.572341399,\"ANGUS\"\n" + ] + } + ], + "source": [ + "# DO NOT CHANGE THIS CELL\n", + "!head -n 5 data/uk_pop.csv" + ] + }, + { + "cell_type": "markdown", + "id": "be7bd168-eb68-45cc-8009-64569974a187", + "metadata": {}, + "source": [ + "One row will represent one person. We have information about their `age`, `sex`, `county`, location, and `name`. Using cuDF, the RAPIDS API providing a GPU-accelerated DataFrame, we can read data from [a variety of formats](https://rapidsai.github.io/projects/cudf/en/0.10.0/api.html#module-cudf.io.csv), including csv, json, parquet, feather, orc, and pandas DataFrames, among others. " + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "89c435b1-35d5-4971-ade1-549ae77d22db", + "metadata": {}, + "outputs": [], + "source": [ + "# DO NOT CHANGE THIS CELL\n", + "import cudf\n", + "import cupy as cp\n", + "import numpy as np\n", + "\n", + "from datetime import datetime\n", + "import random\n", + "import time" + ] + }, + { + "cell_type": "markdown", + "id": "9cb9faf3-4dc9-42bf-b481-98fb4155033e", + "metadata": {}, + "source": [ + "Below we read the data from a local csv file directly into GPU memory with the `read_csv()` function. " + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "a343a943-fd64-45f6-abd5-a991810cf5f3", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Duration: 3.67 seconds\n" + ] + } + ], + "source": [ + "# DO NOT CHANGE THIS CELL\n", + "start=time.time()\n", + "df=cudf.read_csv('./data/uk_pop.csv')\n", + "print(f'Duration: {round(time.time()-start, 2)} seconds')" + ] + }, + { + "cell_type": "markdown", + "id": "c406541c-884a-49c3-b5cb-7aaf21b60403", + "metadata": {}, + "source": [ + "**Note**: Because of the sophisticated GPU memory management behind the scenes in cuDF, the first data load into a fresh RAPIDS memory environment is sometimes substantially slower than subsequent loads. The [RAPIDS Memory Manager](https://github.com/rapidsai/rmm) is preparing additional memory to accommodate the array of data science operations that we may be interested in using on the data, rather than allocating and deallocating the memory repeatedly throughout the workflow. \n", + "\n", + "Below we get the general information about the DataFrame with the `DataFrame.info()` method. " + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "18cd5602-9129-4809-a95f-1e30940558c5", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "RangeIndex: 58479894 entries, 0 to 58479893\n", + "Data columns (total 6 columns):\n", + " # Column Dtype\n", + "--- ------ -----\n", + " 0 age int64\n", + " 1 sex object\n", + " 2 county object\n", + " 3 lat float64\n", + " 4 long float64\n", + " 5 name object\n", + "dtypes: float64(2), int64(1), object(3)\n", + "memory usage: 2.9 GB\n" + ] + } + ], + "source": [ + "# DO NOT CHANGE THIS CELL\n", + "df.info(memory_usage='deep')" + ] + }, + { + "cell_type": "markdown", + "id": "a8289385-f1ac-4ccd-8ba4-6b127b200b42", + "metadata": {}, + "source": [ + "The **DataFrame** is a two-dimensional labeled data structure. It's organized in rows and columns, similar to a spreadsheet or SQL table. Both rows and columns have labels. Rows are typically labeled with an index, while columns have column names. Data is aligned based on row and column labels when performing operations. This is useful for enabling highly efficient vectorized operations across columns or rows. A **Series** refers to a one-dimensional array and is typically associated with a single column of data with an index. \n", + "\n", + "There are ~60MM records across 6 columns. cuDF is able to read data from local files directly into the GPU very efficiently. By default, cuDF samples the dataset to infer the most appropriate data types for each columns. \n", + "\n", + "**Note**: The DataFrame has `.dtypes` and `.columns` attributes that can be used to get similar information. " + ] + }, + { + "cell_type": "markdown", + "id": "af6127ab-437e-4a60-b9bd-5f9671c10602", + "metadata": {}, + "source": [ + "## Initial Data Exploration ##\n", + "Now that we have some data loaded, let's do some initial exploration. \n", + "\n", + "Below we preview the DataFrame with the `DataFrame.head()` method. " + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "7faf372e-644c-4120-8080-779f3a23a152", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
agesexcountylatlongname
00mDARLINGTON54.533644-1.524401FRANCIS
10mDARLINGTON54.426256-1.465314EDWARD
20mDARLINGTON54.555200-1.496417TEDDY
30mDARLINGTON54.547906-1.572341ANGUS
40mDARLINGTON54.477639-1.605995CHARLIE
\n", + "
" + ], + "text/plain": [ + " age sex county lat long name\n", + "0 0 m DARLINGTON 54.533644 -1.524401 FRANCIS\n", + "1 0 m DARLINGTON 54.426256 -1.465314 EDWARD\n", + "2 0 m DARLINGTON 54.555200 -1.496417 TEDDY\n", + "3 0 m DARLINGTON 54.547906 -1.572341 ANGUS\n", + "4 0 m DARLINGTON 54.477639 -1.605995 CHARLIE" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# DO NOT CHANGE THIS CELL\n", + "df.head()" + ] + }, + { + "cell_type": "markdown", + "id": "ee4649b1-2730-47e9-a9d7-331fe7514241", + "metadata": {}, + "source": [ + "## Indexing and Data Selection with `.loc` Accessor ##\n", + "The `.loc` accessor in cuDF DataFrames is used for label-based indexing and selection of data. It allows us to access and manipulate data in a DataFrame based on row and column labels. We can use `DataFrame.loc[row_label(s), column_label(s)]` to access a group of rows and columns. When selecting multiple labels, a list (`[]`) is used. Furthermore, we can use the slicing operator (`:`, i.e. `start:end`) to specify a range of elements. " + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "40d63289-8f23-424c-b3f4-0b7098c9b5a1", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/plain": [ + "0" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "----------------------------------------\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
agesexcounty
00mDARLINGTON
10mDARLINGTON
20mDARLINGTON
\n", + "
" + ], + "text/plain": [ + " age sex county\n", + "0 0 m DARLINGTON\n", + "1 0 m DARLINGTON\n", + "2 0 m DARLINGTON" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "----------------------------------------\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
agesexcounty
00mDARLINGTON
10mDARLINGTON
20mDARLINGTON
30mDARLINGTON
40mDARLINGTON
50mDARLINGTON
\n", + "
" + ], + "text/plain": [ + " age sex county\n", + "0 0 m DARLINGTON\n", + "1 0 m DARLINGTON\n", + "2 0 m DARLINGTON\n", + "3 0 m DARLINGTON\n", + "4 0 m DARLINGTON\n", + "5 0 m DARLINGTON" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "----------------------------------------\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
agesexcountylatlongname
00mDARLINGTON54.533644-1.524401FRANCIS
10mDARLINGTON54.426256-1.465314EDWARD
20mDARLINGTON54.555200-1.496417TEDDY
30mDARLINGTON54.547906-1.572341ANGUS
40mDARLINGTON54.477639-1.605995CHARLIE
50mDARLINGTON54.522900-1.599255VICTOR
60mDARLINGTON54.501872-1.667874EAMONN
70mDARLINGTON54.554709-1.494506HARRY
80mDARLINGTON54.602288-1.586457HECTOR
90mDARLINGTON54.489992-1.652537THEODORE
100mDARLINGTON54.551315-1.519593MUHAMMAD
\n", + "
" + ], + "text/plain": [ + " age sex county lat long name\n", + "0 0 m DARLINGTON 54.533644 -1.524401 FRANCIS\n", + "1 0 m DARLINGTON 54.426256 -1.465314 EDWARD\n", + "2 0 m DARLINGTON 54.555200 -1.496417 TEDDY\n", + "3 0 m DARLINGTON 54.547906 -1.572341 ANGUS\n", + "4 0 m DARLINGTON 54.477639 -1.605995 CHARLIE\n", + "5 0 m DARLINGTON 54.522900 -1.599255 VICTOR\n", + "6 0 m DARLINGTON 54.501872 -1.667874 EAMONN\n", + "7 0 m DARLINGTON 54.554709 -1.494506 HARRY\n", + "8 0 m DARLINGTON 54.602288 -1.586457 HECTOR\n", + "9 0 m DARLINGTON 54.489992 -1.652537 THEODORE\n", + "10 0 m DARLINGTON 54.551315 -1.519593 MUHAMMAD" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# DO NOT CHANGE THIS CELL\n", + "# get first cell\n", + "display(df.loc[0, 'age'])\n", + "print('-'*40)\n", + "\n", + "# get multiple rows and columns\n", + "display(df.loc[[0, 1, 2], ['age', 'sex', 'county']])\n", + "print('-'*40)\n", + "\n", + "# slice a range of rows and columns\n", + "display(df.loc[0:5, 'age':'county'])\n", + "print('-'*40)\n", + "\n", + "# slice a range of rows and columns\n", + "display(df.loc[:10, :'name'])" + ] + }, + { + "cell_type": "markdown", + "id": "a451118f-b986-49b6-ae03-2526e44007a7", + "metadata": {}, + "source": [ + "**Note**: `df[column_label(s)]` is another way to access specific columns, similar to `df.loc[:, column_labels]`. " + ] + }, + { + "cell_type": "markdown", + "id": "055f8828-db5b-419a-aab5-bf149b9fd829", + "metadata": {}, + "source": [ + "## Basic Operations ##\n", + "cuDF support a wide range of operations for numerical data. Although strings are not a data type traditionally associated with GPUs, cuDF supports powerful accelerated string operations.\n", + "* Numerical operations:\n", + " * Arithmetic operations: addition, subtraction, multiplication, division\n", + "* String operations:\n", + " * Case conversion: `.upper()`, `.lower()`, `.title()`\n", + " * String manipulation: concatenation, substring, extraction, padding\n", + " * Pattern matching: `contains()`\n", + " * Splitting: `.split()`\n", + "* Comparison operations: greater than, less than, equal to, etc.\n", + "\n", + "These operations will be performed element-wise for each row. This allows for efficient **vectorized operations** across entire columns. These operations are implemented as vector operations instead of iteration because vector operations can be applied to entire arrays of data, instead of iterating through each element individually. Vectorization is significantly faster than iterating over elements, especially for large datasets. When operating on multiple columns, operations are aligned by index, ensuring that calculations are performed on the correct corresponding elements across columns. These element-wise operations are typically highly optimized and can be much faster than explicit loops, especially for large datasets. We can get the underlying array of data with the `.values` attribute. This is useful when we want to perform operations on the underlying data. \n", + "\n", + "**Note**: Iterating over a cuDF Series, DataFrame or Index is not supported. This is because iterating over data that resides on the GPU will yield extremely poor performance, as GPUs are optimized for highly parallel operations rather than sequential operations. \n", + "\n", + "Below we calculate the birth year for each person. " + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "d4286299-3a43-4e53-a9fb-04e1f20a40a4", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/plain": [ + "0 2025\n", + "1 2025\n", + "2 2025\n", + "3 2025\n", + "4 2025\n", + " ... \n", + "58479889 1935\n", + "58479890 1935\n", + "58479891 1935\n", + "58479892 1935\n", + "58479893 1935\n", + "Name: age, Length: 58479894, dtype: int64" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "array([2025, 2025, 2025, ..., 1935, 1935, 1935])" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# DO NOT CHANGE THIS CELL\n", + "# get current year\n", + "current_year=datetime.now().year\n", + "\n", + "# derive the birth year\n", + "display(current_year-df.loc[:, 'age'])\n", + "\n", + "# get the age array (CuPy for cuDF)\n", + "age_ary=df.loc[:, 'age'].values\n", + "\n", + "# derive the birth year\n", + "current_year-age_ary" + ] + }, + { + "cell_type": "markdown", + "id": "00213228-e32e-4a88-853e-eef53fad4da8", + "metadata": {}, + "source": [ + "When performing operations between a DataFrame and a scalar value, the scalar is \"broadcast\" to match the shape of the DataFrame, effectively applying it to each element. \n", + "\n", + "```\n", + "current_year - df.loc[:, 'age']\n", + "-------------------------------\n", + " (scalar) (array) \n", + " 2024, - 0\n", + " 2024, - 0\n", + " 2024, - 0\n", + " 2024, - 0\n", + " 2024, - 0\n", + " ... - ...\n", + "```\n", + "\n", + "This partially explains why cuDF provides significant performance improvements over pandas, especially for large datasets. The parallel processing architecture of GPUs are designed with thousands of small, specialized cores that can execute many operations simultaneously. This architecture is ideal for vectorized operations, which perform the same instruction on multiple data elements in parallel. " + ] + }, + { + "cell_type": "markdown", + "id": "760a2729-9c7a-4602-83ac-5e171cc4f5f9", + "metadata": {}, + "source": [ + "\n", + "### Exercise #1 - Convert `county` Column to Title Case ###\n", + "As it stands, all of the counties are UPPERCASE. We want to convert the `county` column to title case. \n", + "\n", + "**Instructions**:
\n", + "* Modify the `` only and execute the below cell to convert the `county` column to title case. " + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "5365b5b6-6a00-47e7-8839-0cc8b183c6d7", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "0 Darlington\n", + "1 Darlington\n", + "2 Darlington\n", + "3 Darlington\n", + "4 Darlington\n", + " ... \n", + "58479889 Newport\n", + "58479890 Newport\n", + "58479891 Newport\n", + "58479892 Newport\n", + "58479893 Newport\n", + "Name: county, Length: 58479894, dtype: object" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df['county'].str.title()" + ] + }, + { + "cell_type": "raw", + "id": "738892d3-4bab-4404-af83-b8623804ca5d", + "metadata": {}, + "source": [ + "\n", + "df['county'].str.title()" + ] + }, + { + "cell_type": "markdown", + "id": "35a2520b-5eec-4d59-a956-3f31b43a98b2", + "metadata": {}, + "source": [ + "Click ... for solution. " + ] + }, + { + "cell_type": "markdown", + "id": "d163438b-9993-41e7-856c-76101135a9ad", + "metadata": {}, + "source": [ + "Performing comparison operations or applying conditions create boolean values (`True`/`False`) that corresponds element-wise. \n", + "\n", + "Below we check if each person is an adult. " + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "481218f1-ec09-4776-bda6-b039ccc190ab", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/plain": [ + "0 False\n", + "1 False\n", + "2 False\n", + "3 False\n", + "4 False\n", + " ... \n", + "58479889 True\n", + "58479890 True\n", + "58479891 True\n", + "58479892 True\n", + "58479893 True\n", + "Name: age, Length: 58479894, dtype: bool" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# DO NOT CHANGE THIS CELL\n", + "df['age']>=18" + ] + }, + { + "cell_type": "markdown", + "id": "0b98c380-828a-404b-9fb2-1d215337eff0", + "metadata": {}, + "source": [ + "## Aggregation ##\n", + "Aggregation is an important operation for data science tasks, allowing us to summarize and analyze grouped data. It's commonly used for tasks like calculating totals, averages, counts, etc. cuDF supports common aggregations like `.sum()`, `.mean()`, `.min()`, `.max()`, `.count()`, `.std()`(standard deviation), etc. It also supports more advanced aggregations like `.quantile()` and `.corr()` (correlation). With the `axis` parameter, aggregation operations can be applied column-wise (`0`) or row-wise (`1`). \n", + "\n", + "When the aggregation is implemented as a vector operation, specifically a reduction operation, it is very efficient on the GPU becasue a large number of data elements can be processed simultaneously and in parallel. Column-wise operations also benefit from the [Apache Arrow columnar memory format](https://arrow.apache.org/docs/format/Columnar.html). \n", + "\n", + "

\n", + "\n", + "Below we calculate the arithmetic mean of `lat` and `long` to get an approximate center. " + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "bee8be82-8631-4863-a1fa-e46eed47e334", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/plain": [ + "lat 52.350600\n", + "long -1.304956\n", + "dtype: float64" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# DO NOT CHANGE THIS CELL\n", + "df[['lat', 'long']].mean()" + ] + }, + { + "cell_type": "markdown", + "id": "8a53f0f4-7dc5-40fd-af07-3e82d6556393", + "metadata": {}, + "source": [ + "## Applying User-Defined Functions (UDFs) with `.map()` and `.apply()` ##\n", + "The `.map()` and `.apply()` methods are the primary ways of applying user-defined functions element-wise, and row or column-wise, respectively. We can pass a callable function (built-in or user-defined) as the argument, which is then applied to the entire data structure. Not all operations can be vectorized, especially complex custom logic. In such cases, methods like `.apply()` or custom UDFs might be necessary.\n", + "\n", + "Below we use `.apply()` to check if each person is an adult. " + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "c65e80f8-1cc3-453c-85f2-910dab451228", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/plain": [ + "0 0\n", + "1 0\n", + "2 0\n", + "3 0\n", + "4 0\n", + " ..\n", + "58479889 1\n", + "58479890 1\n", + "58479891 1\n", + "58479892 1\n", + "58479893 1\n", + "Length: 58479894, dtype: int64" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Duration: 0.05 seconds\n" + ] + } + ], + "source": [ + "# DO NOT CHNAGE THIS CELL\n", + "# define a function to check if age is greater than or equal to 18\n", + "start=time.time()\n", + "def is_adult(row): \n", + " if row['age']>=18: \n", + " return 1\n", + " else: \n", + " return 0\n", + "\n", + "# derive the birth year\n", + "display(df.apply(is_adult, axis=1))\n", + "print(f'Duration: {round(time.time()-start, 2)} seconds')" + ] + }, + { + "cell_type": "markdown", + "id": "02828781-6f5b-49d5-87a5-aa5ef08adf15", + "metadata": {}, + "source": [ + "We can also use a [**lambda function**](https://docs.python.org/3/glossary.html#term-lambda) when the function is simple. Lambda functions are limited to a single expression but can include a conditional statement and mulitple arguments. " + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "31d6e7fb-f435-4b1f-8e74-e732cc406b51", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/plain": [ + "0 0\n", + "1 0\n", + "2 0\n", + "3 0\n", + "4 0\n", + " ..\n", + "58479889 1\n", + "58479890 1\n", + "58479891 1\n", + "58479892 1\n", + "58479893 1\n", + "Length: 58479894, dtype: int64" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Duration: 0.05 seconds\n" + ] + } + ], + "source": [ + "# DO NOT CHANGE THIS CELL\n", + "# derive the birth year\n", + "start=time.time()\n", + "display(df.apply(lambda x: 1 if x['age']>=18 else 0, axis=1))\n", + "print(f'Duration: {round(time.time()-start, 2)} seconds')" + ] + }, + { + "cell_type": "markdown", + "id": "475cc2f1-4dc9-4492-aab5-9e51ebf54ebb", + "metadata": {}, + "source": [ + "**Note**: The `.apply()` function in pandas accepts any user-defined function that can include arbitrary operations that are applied to each value of a Series and DataFrame. cuDF also supports `.apply()`, but it relies on Numba to JIT compile the UDF (not in scope) and execute it on the GPU. This can be extremely fast, but imposes a few limitations on what operations are allowed in the UDF. See the docs on [UDFs](https://docs.rapids.ai/api/cudf/stable/user_guide/guide-to-udfs/) for details." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "ecadefaa-380c-412c-87af-05c63d3f7871", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/plain": [ + "0 0\n", + "1 0\n", + "2 0\n", + "3 0\n", + "4 0\n", + " ..\n", + "58479889 1\n", + "58479890 1\n", + "58479891 1\n", + "58479892 1\n", + "58479893 1\n", + "Name: age, Length: 58479894, dtype: int64" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Duration: 0.02 seconds\n" + ] + } + ], + "source": [ + "# DO NOT CHANGE THIS CELL\n", + "# derive the birth year\n", + "start=time.time()\n", + "display((df['age']>=18).astype('int'))\n", + "print(f'Duration: {round(time.time()-start, 2)} seconds')" + ] + }, + { + "cell_type": "markdown", + "id": "84f0c41b-ad56-4cc7-b074-f2390f41cc70", + "metadata": {}, + "source": [ + "Below we use `Series.map()` to determine the number of characters in each person's name. " + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "c4a3e4e1-fd83-4024-bcbf-29216c11016f", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/plain": [ + "0 7\n", + "1 6\n", + "2 5\n", + "3 5\n", + "4 7\n", + " ..\n", + "58479889 5\n", + "58479890 8\n", + "58479891 7\n", + "58479892 7\n", + "58479893 8\n", + "Name: name, Length: 58479894, dtype: int32" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# DO NOT CHANGE THIS CELL\n", + "df['name'].map(lambda x: len(x))" + ] + }, + { + "cell_type": "markdown", + "id": "7c717ada-69b2-4982-b81b-8594af6d9bf1", + "metadata": {}, + "source": [ + "## Filtering with `.loc` and Boolean Mask ##\n", + "A boolean mask is an array of `True`/`False` values that corresponds element-wise to another array or data structure. It's used for filtering and selecting data based on certain conditions. In this context, the mask can be used to index or filter a DataFrame with `.loc`, selecting only the elements where the mask is `True`. \n", + "\n", + "**Note**: Boolean masking is often more efficient than iterative approaches, especially for large datasets, as it leverages vectorized operations. \n", + "\n", + "Below we use the `.loc` accessor and a boolean mask to filter people whose names start with an `E`. " + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "cf9cc540-1de6-4e50-986a-5bf9bd9056a6", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
agesexcountylatlongname
10mDARLINGTON54.426256-1.465314EDWARD
60mDARLINGTON54.501872-1.667874EAMONN
340mDARLINGTON54.483065-1.501312ETHAN
450mDARLINGTON54.640205-1.558986ELVIN
490mDARLINGTON54.575450-1.600592EDWARD
.....................
5847985990fNEWPORT51.576452-2.891774EDIE
5847986790fNEWPORT51.555083-3.080259ELEANOR
5847987190fNEWPORT51.515820-2.839532EMERSON
5847987590fNEWPORT51.510140-3.004406ELLA
5847988590fNEWPORT51.586575-2.799302ELIN
\n", + "

5081794 rows × 6 columns

\n", + "
" + ], + "text/plain": [ + " age sex county lat long name\n", + "1 0 m DARLINGTON 54.426256 -1.465314 EDWARD\n", + "6 0 m DARLINGTON 54.501872 -1.667874 EAMONN\n", + "34 0 m DARLINGTON 54.483065 -1.501312 ETHAN\n", + "45 0 m DARLINGTON 54.640205 -1.558986 ELVIN\n", + "49 0 m DARLINGTON 54.575450 -1.600592 EDWARD\n", + "... ... .. ... ... ... ...\n", + "58479859 90 f NEWPORT 51.576452 -2.891774 EDIE\n", + "58479867 90 f NEWPORT 51.555083 -3.080259 ELEANOR\n", + "58479871 90 f NEWPORT 51.515820 -2.839532 EMERSON\n", + "58479875 90 f NEWPORT 51.510140 -3.004406 ELLA\n", + "58479885 90 f NEWPORT 51.586575 -2.799302 ELIN\n", + "\n", + "[5081794 rows x 6 columns]" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# DO NOT CHANGE THIS CELL\n", + "boolean_mask=df['name'].str.startswith('E')\n", + "df.loc[boolean_mask]" + ] + }, + { + "cell_type": "markdown", + "id": "5d76a6dd-5d0b-4fd2-868a-235255375af0", + "metadata": {}, + "source": [ + "Multiple conditions can be combined using logical operators (`&` and `|`). \n", + "\n", + "**Note**: When using multiple conditions, it's important to wrap each condition in parentheses (`(` and `)`) to ensure correct order of operations. \n", + "\n", + "Below we use the `.loc` accessor and multiple conditions to filter adults whose names start with an `E`. " + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "03713403-6575-437d-99f0-c7f8ec3cb13b", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
agesexcountylatlongname
10mDARLINGTON54.426256-1.465314EDWARD
60mDARLINGTON54.501872-1.667874EAMONN
340mDARLINGTON54.483065-1.501312ETHAN
450mDARLINGTON54.640205-1.558986ELVIN
490mDARLINGTON54.575450-1.600592EDWARD
.....................
5847988990fNEWPORT51.626744-2.859381FREYA
5847989090fNEWPORT51.546043-2.897815GEORGINA
5847989190fNEWPORT51.605268-2.849656REBECCA
5847989290fNEWPORT51.554649-2.934364JESSICA
5847989390fNEWPORT51.578787-2.827954FLORENCE
\n", + "

47085782 rows × 6 columns

\n", + "
" + ], + "text/plain": [ + " age sex county lat long name\n", + "1 0 m DARLINGTON 54.426256 -1.465314 EDWARD\n", + "6 0 m DARLINGTON 54.501872 -1.667874 EAMONN\n", + "34 0 m DARLINGTON 54.483065 -1.501312 ETHAN\n", + "45 0 m DARLINGTON 54.640205 -1.558986 ELVIN\n", + "49 0 m DARLINGTON 54.575450 -1.600592 EDWARD\n", + "... ... .. ... ... ... ...\n", + "58479889 90 f NEWPORT 51.626744 -2.859381 FREYA\n", + "58479890 90 f NEWPORT 51.546043 -2.897815 GEORGINA\n", + "58479891 90 f NEWPORT 51.605268 -2.849656 REBECCA\n", + "58479892 90 f NEWPORT 51.554649 -2.934364 JESSICA\n", + "58479893 90 f NEWPORT 51.578787 -2.827954 FLORENCE\n", + "\n", + "[47085782 rows x 6 columns]" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# DO NOT CHANGE THIS CELL\n", + "df[(df['age']>=18) | (df['name'].str.startswith('E'))]" + ] + }, + { + "cell_type": "markdown", + "id": "69b7f7ef-b270-4eae-8852-d6f48bf83086", + "metadata": {}, + "source": [ + "\n", + "### Exercise #2 - Counties North of Sunderland ###\n", + "This exercise will require to use the `.loc` accessor, and several of the techniques described above. We want to identify the latitude of the northernmost resident of Sunderland county (the person with the maximum `lat` value), and then determine which counties have any residents north of this resident. Use the `Series.unique()` method of to de-duplicate the result.\n", + "\n", + "**Instructions**:
\n", + "* Modify the `` only and execute the below cell to identify counties north of Sunderland. " + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "id": "8c1394db-6bca-473b-a053-61a6066bd835", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "0 COUNTY DURHAM\n", + "1 NORTHUMBERLAND\n", + "2 GATESHEAD\n", + "3 NEWCASTLE UPON TYNE\n", + "4 NORTH TYNESIDE\n", + "5 SOUTH TYNESIDE\n", + "6 CUMBRIA\n", + "7 NORTH YORKSHIRE\n", + "Name: county, dtype: object" + ] + }, + "execution_count": 23, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sunderland_residents=df.loc[df['county'] =='SUNDERLAND']\n", + "northmost_sunderland_lat=sunderland_residents['lat'].max()\n", + "df.loc[df['lat'] > northmost_sunderland_lat]['county'].unique()" + ] + }, + { + "cell_type": "raw", + "id": "0cf99881-1a27-409a-822b-7e62b5953f3a", + "metadata": {}, + "source": [ + "\n", + "sunderland_residents=df.loc[df['county'] == 'SUNDERLAND']\n", + "northmost_sunderland_lat=sunderland_residents['lat'].max()\n", + "df.loc[df['lat'] > northmost_sunderland_lat]['county'].unique()" + ] + }, + { + "cell_type": "markdown", + "id": "0594efe7-97d4-4884-bffb-a26f5144ad54", + "metadata": {}, + "source": [ + "Click ... for solution. " + ] + }, + { + "cell_type": "markdown", + "id": "d5ead43e-37bc-4a3d-a64f-bb82ad01ad99", + "metadata": {}, + "source": [ + "## Creating New Columns ##" + ] + }, + { + "cell_type": "markdown", + "id": "45ada779-bc14-4fba-88d8-62c282345a63", + "metadata": {}, + "source": [ + "We can create new columns by assigning values to the column label. The new column should have the same number of rows as the existing DataFrame. Typically, we create new columns by performing operations on existing columns. \n", + "\n", + "Below we create a few additional columns. " + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "id": "977fdb2b-dbf1-4842-ab0f-31b9af65e0d1", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
agesexcountylatlongnamebirth_yearsex_normalizecounty_normalize
00mDARLINGTON54.533644-1.524401Francis2025MDarlington
10mDARLINGTON54.426256-1.465314Edward2025MDarlington
20mDARLINGTON54.555200-1.496417Teddy2025MDarlington
30mDARLINGTON54.547906-1.572341Angus2025MDarlington
40mDARLINGTON54.477639-1.605995Charlie2025MDarlington
\n", + "
" + ], + "text/plain": [ + " age sex county lat long name birth_year \\\n", + "0 0 m DARLINGTON 54.533644 -1.524401 Francis 2025 \n", + "1 0 m DARLINGTON 54.426256 -1.465314 Edward 2025 \n", + "2 0 m DARLINGTON 54.555200 -1.496417 Teddy 2025 \n", + "3 0 m DARLINGTON 54.547906 -1.572341 Angus 2025 \n", + "4 0 m DARLINGTON 54.477639 -1.605995 Charlie 2025 \n", + "\n", + " sex_normalize county_normalize \n", + "0 M Darlington \n", + "1 M Darlington \n", + "2 M Darlington \n", + "3 M Darlington \n", + "4 M Darlington " + ] + }, + "execution_count": 24, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# DO NOT CHANGE THIS CELL\n", + "# get current year\n", + "current_year=datetime.now().year\n", + "\n", + "# numerical operations\n", + "df['birth_year']=current_year-df['age']\n", + "\n", + "# string operations\n", + "df['sex_normalize']=df['sex'].str.upper()\n", + "df['county_normalize']=df['county'].str.title().str.replace(' ', '_')\n", + "df['name']=df['name'].str.title()\n", + "\n", + "# preview\n", + "df.head()" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "id": "66c5d332-a6ef-4f8d-9560-fa860ea1679a", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/plain": [ + "{'status': 'ok', 'restart': True}" + ] + }, + "execution_count": 25, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# DO NOT CHANGE THIS CELL\n", + "import IPython\n", + "app = IPython.Application.instance()\n", + "app.kernel.do_shutdown(True)" + ] + }, + { + "cell_type": "markdown", + "id": "cd7bed0a-d46d-402a-884e-e2e17c98738c", + "metadata": {}, + "source": [ + "## pandas vs. cuDF ##\n", + "Except for being much more performant with large datasets, cuDF looks and feels a lot like Pandas. By way of review, cuDF and pandas share the below similarities: \n", + "* **API similarity**: cuDF provides a pandas-like API that is familiar to data engineers and data scientists. It aims to implement many of the same functions and operations as pandas, allowing users to easily accelerate their existing pandas workflows.\n", + "* **Similar operations**: cuDF implements many common pandas operations such as filtering, joining, aggregating, and groupby.\n", + "\n", + "

\n", + "\n", + "Comparing to pandas, cuDF tends to perform better for large datasets because of the follow features: \n", + "* GPUs excel at parallel computation, which is advantageous for many data science and machine learning tasks.\n", + "* GPUs typically have much higher memory bandwidth than CPUs, allowing for faster data access in memory-bound operations.\n", + "* cuDF leverages GPU's ability to perform vectorized operations efficiently, which is particularly beneficial for large datasets.\n", + "* cuDF uses a columnar data format, which can lead to more efficient memory access patterns on GPUs. When performing data operations on cuDF Dataframes, column operations are typically much more performant than row-wise operations.\n", + "\n", + "**Note**: It's important to note that the performance advantage of cuDF over pandas can vary depending on the specific operation, data size, and hardware configuration. For smaller datasets or simpler operations, the overhead of GPU initialization might make pandas on CPU faster." + ] + }, + { + "cell_type": "markdown", + "id": "c9c00b32-f5f2-46de-a50a-fc54f3244dab", + "metadata": {}, + "source": [ + "## cuDF pandas ##\n", + "Starting with version `23.10.01`, cuDF introduced a **pandas accelerator mode** (`cudf.pandas`) that supports 100% of the pandas API. This mode allows users to accelerate pandas code on the GPU without requiring any code changes. Not all operations can be performed on the GPU. When using `cudf.pandas`, operations that can be accelerated will run on the GPU, while unsupported operations will automatically fall back to pandas on the CPU. For example, `.read_sql()`. this will first read sql with pandas and move the data to cuDF. \n", + "\n", + "There are two ways to activate cuDF pandas:\n", + "- Jupyter Magic Command\n", + "```\n", + "%load_ext cudf.pandas\n", + "import pandas\n", + "...\n", + "```\n", + "- Python Import\n", + "```\n", + "import cudf.pandas\n", + "cudf.pandas.install()\n", + "\n", + "import pandas as pd\n", + "...\n", + "```\n", + "\n", + "**Note**: There are no other changes required - this is useful to quickly accelerate existing workloads with minimum code change. More information about cuDF pandas can be found [here](https://docs.rapids.ai/api/cudf/stable/cudf_pandas/). \n", + "\n", + "cuDF pandas is a no code change accelerator for pandas for automatic acceleration of any supported pandas call. \n", + "\n", + "Below we run some basic DataFrame operations with pandas, before demonstrating how cudf pandas is enabled. " + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "5fed82ae-0ecb-4471-bb8f-060b1bf4542f", + "metadata": {}, + "outputs": [], + "source": [ + "# DO NOT CHANGE THIS CELL\n", + "# %load_ext cudf.pandas" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "7671791e-c491-4831-bd1b-956de6b455e5", + "metadata": {}, + "outputs": [], + "source": [ + "# DO NOT CHANGE THIS CELL\n", + "import pandas as pd\n", + "import time\n", + "from datetime import datetime" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "47c87c9f-5b97-4a0d-bfa7-a26c1369314f", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "ename": "ParserError", + "evalue": "Error tokenizing data. C error: Calling read(nbytes) on source failed. Try engine='python'.", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mParserError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[3], line 5\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[38;5;66;03m# %%cudf.pandas.line_profile\u001b[39;00m\n\u001b[1;32m 2\u001b[0m \u001b[38;5;66;03m# DO NOT CHANGE THIS CELL\u001b[39;00m\n\u001b[1;32m 3\u001b[0m start\u001b[38;5;241m=\u001b[39mtime\u001b[38;5;241m.\u001b[39mtime()\n\u001b[0;32m----> 5\u001b[0m df\u001b[38;5;241m=\u001b[39m\u001b[43mpd\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mread_csv\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;124;43m./data/uk_pop.csv\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[43m)\u001b[49m\n\u001b[1;32m 6\u001b[0m current_year\u001b[38;5;241m=\u001b[39mdatetime\u001b[38;5;241m.\u001b[39mnow()\u001b[38;5;241m.\u001b[39myear\n\u001b[1;32m 8\u001b[0m df[\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mbirth_year\u001b[39m\u001b[38;5;124m'\u001b[39m]\u001b[38;5;241m=\u001b[39mcurrent_year\u001b[38;5;241m-\u001b[39mdf[\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mage\u001b[39m\u001b[38;5;124m'\u001b[39m]\n", + "File \u001b[0;32m/opt/conda/lib/python3.10/site-packages/pandas/io/parsers/readers.py:1026\u001b[0m, in \u001b[0;36mread_csv\u001b[0;34m(filepath_or_buffer, sep, delimiter, header, names, index_col, usecols, dtype, engine, converters, true_values, false_values, skipinitialspace, skiprows, skipfooter, nrows, na_values, keep_default_na, na_filter, verbose, skip_blank_lines, parse_dates, infer_datetime_format, keep_date_col, date_parser, date_format, dayfirst, cache_dates, iterator, chunksize, compression, thousands, decimal, lineterminator, quotechar, quoting, doublequote, escapechar, comment, encoding, encoding_errors, dialect, on_bad_lines, delim_whitespace, low_memory, memory_map, float_precision, storage_options, dtype_backend)\u001b[0m\n\u001b[1;32m 1013\u001b[0m kwds_defaults \u001b[38;5;241m=\u001b[39m _refine_defaults_read(\n\u001b[1;32m 1014\u001b[0m dialect,\n\u001b[1;32m 1015\u001b[0m delimiter,\n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 1022\u001b[0m dtype_backend\u001b[38;5;241m=\u001b[39mdtype_backend,\n\u001b[1;32m 1023\u001b[0m )\n\u001b[1;32m 1024\u001b[0m kwds\u001b[38;5;241m.\u001b[39mupdate(kwds_defaults)\n\u001b[0;32m-> 1026\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43m_read\u001b[49m\u001b[43m(\u001b[49m\u001b[43mfilepath_or_buffer\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mkwds\u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[0;32m/opt/conda/lib/python3.10/site-packages/pandas/io/parsers/readers.py:626\u001b[0m, in \u001b[0;36m_read\u001b[0;34m(filepath_or_buffer, kwds)\u001b[0m\n\u001b[1;32m 623\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m parser\n\u001b[1;32m 625\u001b[0m \u001b[38;5;28;01mwith\u001b[39;00m parser:\n\u001b[0;32m--> 626\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mparser\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mread\u001b[49m\u001b[43m(\u001b[49m\u001b[43mnrows\u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[0;32m/opt/conda/lib/python3.10/site-packages/pandas/io/parsers/readers.py:1923\u001b[0m, in \u001b[0;36mTextFileReader.read\u001b[0;34m(self, nrows)\u001b[0m\n\u001b[1;32m 1916\u001b[0m nrows \u001b[38;5;241m=\u001b[39m validate_integer(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mnrows\u001b[39m\u001b[38;5;124m\"\u001b[39m, nrows)\n\u001b[1;32m 1917\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[1;32m 1918\u001b[0m \u001b[38;5;66;03m# error: \"ParserBase\" has no attribute \"read\"\u001b[39;00m\n\u001b[1;32m 1919\u001b[0m (\n\u001b[1;32m 1920\u001b[0m index,\n\u001b[1;32m 1921\u001b[0m columns,\n\u001b[1;32m 1922\u001b[0m col_dict,\n\u001b[0;32m-> 1923\u001b[0m ) \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_engine\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mread\u001b[49m\u001b[43m(\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;66;43;03m# type: ignore[attr-defined]\u001b[39;49;00m\n\u001b[1;32m 1924\u001b[0m \u001b[43m \u001b[49m\u001b[43mnrows\u001b[49m\n\u001b[1;32m 1925\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 1926\u001b[0m \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[38;5;167;01mException\u001b[39;00m:\n\u001b[1;32m 1927\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mclose()\n", + "File \u001b[0;32m/opt/conda/lib/python3.10/site-packages/pandas/io/parsers/c_parser_wrapper.py:234\u001b[0m, in \u001b[0;36mCParserWrapper.read\u001b[0;34m(self, nrows)\u001b[0m\n\u001b[1;32m 232\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[1;32m 233\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mlow_memory:\n\u001b[0;32m--> 234\u001b[0m chunks \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_reader\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mread_low_memory\u001b[49m\u001b[43m(\u001b[49m\u001b[43mnrows\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 235\u001b[0m \u001b[38;5;66;03m# destructive to chunks\u001b[39;00m\n\u001b[1;32m 236\u001b[0m data \u001b[38;5;241m=\u001b[39m _concatenate_chunks(chunks)\n", + "File \u001b[0;32mparsers.pyx:838\u001b[0m, in \u001b[0;36mpandas._libs.parsers.TextReader.read_low_memory\u001b[0;34m()\u001b[0m\n", + "File \u001b[0;32mparsers.pyx:905\u001b[0m, in \u001b[0;36mpandas._libs.parsers.TextReader._read_rows\u001b[0;34m()\u001b[0m\n", + "File \u001b[0;32mparsers.pyx:874\u001b[0m, in \u001b[0;36mpandas._libs.parsers.TextReader._tokenize_rows\u001b[0;34m()\u001b[0m\n", + "File \u001b[0;32mparsers.pyx:891\u001b[0m, in \u001b[0;36mpandas._libs.parsers.TextReader._check_tokenize_status\u001b[0;34m()\u001b[0m\n", + "File \u001b[0;32mparsers.pyx:2061\u001b[0m, in \u001b[0;36mpandas._libs.parsers.raise_parser_error\u001b[0;34m()\u001b[0m\n", + "\u001b[0;31mParserError\u001b[0m: Error tokenizing data. C error: Calling read(nbytes) on source failed. Try engine='python'." + ] + } + ], + "source": [ + "# %%cudf.pandas.line_profile\n", + "# DO NOT CHANGE THIS CELL\n", + "start=time.time()\n", + "\n", + "df=pd.read_csv('./data/uk_pop.csv')\n", + "current_year=datetime.now().year\n", + "\n", + "df['birth_year']=current_year-df['age']\n", + "\n", + "df['sex_normalize']=df['sex'].str.upper()\n", + "df['county_normalize']=df['county'].str.title().str.replace(' ', '_')\n", + "df['name']=df['name'].str.title()\n", + "\n", + "print(f'Duration: {round(time.time()-start, 2)} seconds')\n", + "\n", + "display(df.head())" + ] + }, + { + "cell_type": "markdown", + "id": "7ebe113c-9fc0-4da5-932c-0a68af0d5a31", + "metadata": {}, + "source": [ + "\n", + "### Exercise #3 - Automatic Acceleration ###\n", + "**Instructions**:
\n", + "* Go back to the top of this subsection, re-execute the cells and uncomment the `%load_ext` magic command to accelerate with cuDF pandas. \n", + "* Observe the acceleration. \n", + "* Go back to the top of this subsection, re-execute the cells and uncomment the `%%cudf.pandas.line_profile` magic command to use the line profiler. \n", + "* Observe the output from the line profiler. " + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "f1688462-783c-4fea-ae18-5d37524d26d8", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/plain": [ + "{'status': 'ok', 'restart': True}" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# DO NOT CHANGE THIS CELL\n", + "import IPython\n", + "app = IPython.Application.instance()\n", + "app.kernel.do_shutdown(True)" + ] + }, + { + "cell_type": "markdown", + "id": "0a691784-dac5-4485-89fb-5e405f10c05c", + "metadata": {}, + "source": [ + "**Well Done!** Let's move to the [next notebook](1-03_memory_management.ipynb). " + ] + }, + { + "cell_type": "markdown", + "id": "81e47f0a-547e-4714-878d-34eb9b75c835", + "metadata": {}, + "source": [ + "" + ] + } + ], + "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.15" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/ds/25-1/2/1-03_memory_management.ipynb b/ds/25-1/2/1-03_memory_management.ipynb new file mode 100644 index 0000000..6e8b53d --- /dev/null +++ b/ds/25-1/2/1-03_memory_management.ipynb @@ -0,0 +1,958 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "def31b0f-921a-43eb-9807-8b9b31eb7b32", + "metadata": {}, + "source": [ + "" + ] + }, + { + "cell_type": "markdown", + "id": "4a0fd4dd-f7be-4c90-8ddd-384a760ac04f", + "metadata": {}, + "source": [ + "# Fundamentals of Accelerated Data Science # " + ] + }, + { + "cell_type": "markdown", + "id": "6a8fdf2e-a481-455e-8a52-8be8472b63bf", + "metadata": {}, + "source": [ + "## 03 - Memory Management ##\n", + "\n", + "**Table of Contents**\n", + "
\n", + "This notebook explores the dynamics between data and memory. This notebook covers the below sections: \n", + "1. [Memory Management](#Memory-Management)\n", + " * [Memory Usage](#Memory-Usage)\n", + "2. [Data Types](#Data-Types)\n", + " * [Convert Data Types](#Convert-Data-Types)\n", + " * [Exercise #1 - Modify `dtypes`](#Exercise-#1---Modify-dtypes)\n", + " * [Categorical](#Categorical)\n", + "3. [Efficient Data Loading](#Efficient-Data-Loading)" + ] + }, + { + "cell_type": "markdown", + "id": "1b59367c-48bc-4c72-b1f4-4cfdfa5470cf", + "metadata": {}, + "source": [ + "## Memory Management ##\n", + "During the data acquisition process, data is transferred to memory in order to be operated on by the processor. Memory management is crucial for cuDF and GPU operations for several key reasons: \n", + "* **Limited GPU memory**: GPUs typically have less memory than CPUs, therefore efficient memory management is essential to maximize the use of available GPU memory, especially for large datasets.\n", + "* **Data transfer overhead**: Transferring data between CPU and GPU memory is relatively slow compared to GPU computation speed. Minimizing these transfers through smart memory management is critical for performance.\n", + "* **Performance tuning**: Understanding and optimizing memory usage is key to achieving peak performance in GPU-accelerated data processing tasks.\n", + "\n", + "When done correctly, keeping the data on the GPU can enable cuDF and the RAPIDS ecosystem to achieve significant performance improvements, handle larger datasets, and provide more efficient data processing capabilities. \n", + "\n", + "Below we import the data from the csv file. " + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "b7b8a623-f799-4dad-aca9-0e571bb6e527", + "metadata": {}, + "outputs": [], + "source": [ + "# DO NOT CHANGE THIS CELL\n", + "import pandas as pd\n", + "import random\n", + "import time" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "711d0a7f-8598-49fc-949c-5caf6029ce47", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
agesexcountylatlongname
00mDARLINGTON54.533644-1.524401FRANCIS
10mDARLINGTON54.426256-1.465314EDWARD
20mDARLINGTON54.555200-1.496417TEDDY
30mDARLINGTON54.547906-1.572341ANGUS
40mDARLINGTON54.477639-1.605995CHARLIE
\n", + "
" + ], + "text/plain": [ + " age sex county lat long name\n", + "0 0 m DARLINGTON 54.533644 -1.524401 FRANCIS\n", + "1 0 m DARLINGTON 54.426256 -1.465314 EDWARD\n", + "2 0 m DARLINGTON 54.555200 -1.496417 TEDDY\n", + "3 0 m DARLINGTON 54.547906 -1.572341 ANGUS\n", + "4 0 m DARLINGTON 54.477639 -1.605995 CHARLIE" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# DO NOT CHANGE THIS CELL\n", + "df=pd.read_csv('./data/uk_pop.csv')\n", + "\n", + "# preview\n", + "df.head()" + ] + }, + { + "cell_type": "markdown", + "id": "36416fd0-7081-42aa-bf31-d1231b81ec0b", + "metadata": {}, + "source": [ + "### Memory Usage ###\n", + "Memory utilization of a DataFrame depends on the date types for each column.\n", + "\n", + "

\n", + "\n", + "We can use `DataFrame.memory_usage()` to see the memory usage for each column (in bytes). Most of the common data types have a fixed size in memory, such as `int`, `float`, `datetime`, and `bool`. Memory usage for these data types is the respective memory requirement multiplied by the number of data points. For `string` data type, the memory usage reported _for pandas_ is the number of elements times 8 bytes. This accounts for the 64-bit required for the pointer that points to an address in memory but not the memory used for the actual string values. The actual memory required for a string value is 49 bytes plus an additional byte for each character. The `deep` parameter provides a more accurate memory usage report that accounts for the system-level memory consumption of the contained `string` data type. \n", + "\n", + "Below we get the memory usage. " + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "8378207b-2d9e-4102-8408-c2dddafc8a40", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Index 128\n", + "age 467839152\n", + "sex 3391833852\n", + "county 3934985133\n", + "lat 467839152\n", + "long 467839152\n", + "name 3666922374\n", + "dtype: int64" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# DO NOT CHANGE THIS CELL\n", + "# pandas memory utilization\n", + "mem_usage_df=df.memory_usage(deep=True)\n", + "mem_usage_df" + ] + }, + { + "cell_type": "markdown", + "id": "07c24bb1-c4f7-440c-a949-d4c57800ec61", + "metadata": {}, + "source": [ + "Below we define a `make_decimal()` function to convert memory size into units based on powers of 2. In contrast to units based on powers of 10, this customary convention is commonly used to report memory capacity. More information about the two definitions can be found [here](https://en.wikipedia.org/wiki/Byte#Multiple-byte_units). " + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "5ae42218-1547-49fd-9123-ab508a2b03de", + "metadata": {}, + "outputs": [], + "source": [ + "# DO NOT CHANGE THIS CELL\n", + "suffixes = ['B', 'kB', 'MB', 'GB', 'TB', 'PB']\n", + "def make_decimal(nbytes):\n", + " i=0\n", + " while nbytes >= 1024 and i < len(suffixes)-1:\n", + " nbytes/=1024.\n", + " i+=1\n", + " f=('%.2f' % nbytes).rstrip('0').rstrip('.')\n", + " return '%s %s' % (f, suffixes[i])" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "e6d4a613-3eea-4dce-8e71-39593ff6f226", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'11.55 GB'" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "make_decimal(mem_usage_df.sum())" + ] + }, + { + "cell_type": "markdown", + "id": "a352c0b2-65aa-4231-b753-556aca46ff49", + "metadata": {}, + "source": [ + "Below we calculate the memory usage manually based on the data types. " + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "630327b9-6dc1-4b70-9fdf-9f7763ec4d50", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Numerical columns use 467839152 bytes of memory\n" + ] + } + ], + "source": [ + "# DO NOT CHANGE THIS CELL\n", + "# get number of rows\n", + "num_rows=len(df)\n", + "\n", + "# 64-bit numbers uses 8 bytes of memory\n", + "print(f'Numerical columns use {num_rows*8} bytes of memory')" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "bb22b5f4-e38f-438e-9426-61746b509e50", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "county column uses 3934985133 bytes of memory.\n" + ] + } + ], + "source": [ + "# DO NOT CHANGE THIS CELL\n", + "# check random string-typed column\n", + "string_cols=[col for col in df.columns if df[col].dtype=='object' ]\n", + "column_to_check=random.choice(string_cols)\n", + "\n", + "overhead=49\n", + "pointer_size=8\n", + "\n", + "# nan==nan when value is not a number\n", + "# nan uses 32 bytes of memory\n", + "string_col_mem_usage_df=df[column_to_check].map(lambda x: len(x)+overhead+pointer_size if x else 32)\n", + "string_col_mem_usage=string_col_mem_usage_df.sum()\n", + "print(f'{column_to_check} column uses {string_col_mem_usage} bytes of memory.')" + ] + }, + { + "cell_type": "markdown", + "id": "94e393c2-c0d0-40ee-82d2-730c4667e9b8", + "metadata": {}, + "source": [ + "**Note**: The `string` data type is stored differently in cuDF than it is in pandas. More information about `libcudf` stores string data using the [Arrow format](https://arrow.apache.org/docs/format/Columnar.html#variable-size-binary-layout) can be found [here](https://developer.nvidia.com/blog/mastering-string-transformations-in-rapids-libcudf/). " + ] + }, + { + "cell_type": "markdown", + "id": "737ff50b-9426-4e08-a00a-d7ee69f48b9f", + "metadata": {}, + "source": [ + "## Data Types ##\n", + "By default, pandas (and cuDF) uses 64-bit for numerical values. Using 64-bit numbers provides the highest precision but many applications do not require 64-bit precision when aggregating over a very large number of data points. When possible, using 32-bit numbers reduces storage and memory requirements in half, and also typically greatly speeds up computations because only half as much data needs to be accessed in memory. " + ] + }, + { + "cell_type": "markdown", + "id": "0b77d450-c415-44b8-87ac-20ce616ec809", + "metadata": {}, + "source": [ + "### Convert Data Types ###\n", + "The `.astype()` method can be used to convert numerical data types to use different bit-size containers. Here we convert the `age` column from `int64` to `int8`. " + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "603f7c70-134e-4466-a790-8a18b9088ca6", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "age int8\n", + "sex object\n", + "county object\n", + "lat float64\n", + "long float64\n", + "name object\n", + "dtype: object" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# DO NOT CHANGE THIS CELL\n", + "df['age']=df['age'].astype('int8')\n", + "\n", + "df.dtypes" + ] + }, + { + "cell_type": "markdown", + "id": "973a6dd4-2aef-44d9-8b01-8853032eddae", + "metadata": {}, + "source": [ + "### Exercise #1 - Modify `dtypes` ###\n", + "**Instructions**:
\n", + "* Modify the `` only and execute the below cell to convert any 64-bit data types to their 32-bit counterparts." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "beb7d71b-6672-462e-b65c-a64dbe5f7a57", + "metadata": {}, + "outputs": [], + "source": [ + "df['lat']=df['lat'].astype('float32')\n", + "df['long']=df['long'].astype('float32')" + ] + }, + { + "cell_type": "raw", + "id": "3b44fb22-a0f1-4e43-a332-1ccbad50caee", + "metadata": {}, + "source": [ + "\n", + "df['lat']=df['lat'].astype('float32')\n", + "df['long']=df['long'].astype('float32')" + ] + }, + { + "cell_type": "markdown", + "id": "98b6542d-22cc-4926-b600-a3e052c37c96", + "metadata": {}, + "source": [ + "Click ... for solution. " + ] + }, + { + "cell_type": "markdown", + "id": "7b2cd622-977c-4915-a87f-2fe03c1793f5", + "metadata": {}, + "source": [ + "### Categorical ###\n", + "Categorical data is a type of data that represents discrete, distinct categories or groups. They can have a meaningful order or ranking but generally cannot be used for numerical operations. When appropriate, using the `categorical` data type can reduce memory usage and lead to faster operations. It can also be used to define and maintain a custom order of categories. \n", + "\n", + "Below we get the number of unique values in the string columns. " + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "f249e4b8-5d7a-4b44-ac15-bd3360a43f2a", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "sex 2\n", + "county 171\n", + "name 13212\n", + "dtype: int64" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# DO NOT CHANGE THIS CELL\n", + "df.select_dtypes(include='object').nunique()" + ] + }, + { + "cell_type": "markdown", + "id": "f1d8bd88-b39b-4043-9039-d8bd75fe851a", + "metadata": {}, + "source": [ + "Below we convert columns with few discrete values to `category`. The `category` data type has `.categories` and `codes` properties that are accessed through `.cat`. " + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "a99bebbf-2e5b-4720-96f9-9fd7d42d2fe8", + "metadata": {}, + "outputs": [], + "source": [ + "# DO NOT CHANGE THIS CELL\n", + "df['sex']=df['sex'].astype('category')\n", + "df['county']=df['county'].astype('category')" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "41b7b290-cfcf-4ff6-b6b4-454c19b44a62", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Index(['BARKING AND DAGENHAM', 'BARNET', 'BARNSLEY',\n", + " 'BATH AND NORTH EAST SOMERSET', 'BEDFORD', 'BEXLEY', 'BIRMINGHAM',\n", + " 'BLACKBURN WITH DARWEN', 'BLACKPOOL', 'BLAENAU GWENT',\n", + " ...\n", + " 'WESTMINSTER', 'WIGAN', 'WILTSHIRE', 'WINDSOR AND MAIDENHEAD', 'WIRRAL',\n", + " 'WOKINGHAM', 'WOLVERHAMPTON', 'WORCESTERSHIRE', 'WREXHAM', 'YORK'],\n", + " dtype='object', length=171)" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "----------------------------------------\n" + ] + }, + { + "data": { + "text/plain": [ + "0 37\n", + "1 37\n", + "2 37\n", + "3 37\n", + "4 37\n", + " ..\n", + "58479889 96\n", + "58479890 96\n", + "58479891 96\n", + "58479892 96\n", + "58479893 96\n", + "Length: 58479894, dtype: int16" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# DO NOT CHANGE THIS CELL\n", + "display(df['county'].cat.categories)\n", + "print('-'*40)\n", + "display(df['county'].cat.codes)" + ] + }, + { + "cell_type": "markdown", + "id": "737385ab-677c-4bef-a86a-10aa3119e29a", + "metadata": {}, + "source": [ + "**Note**: `.astype()` can also be used to convert data to `datetime` or `object` to enable datetime and string methods. " + ] + }, + { + "cell_type": "markdown", + "id": "552c47c2-0fbc-455e-8745-cb98fc777243", + "metadata": {}, + "source": [ + "## Efficient Data Loading ##\n", + "It is often advantageous to specify the most appropriate data types for each columns, based on range, precision requirement, and how they are used. " + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "c2b9f0c3-8598-4a28-9481-ce28fea7544b", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Index 128\n", + "age 467839152\n", + "sex 3391833852\n", + "county 3934985133\n", + "lat 467839152\n", + "long 467839152\n", + "name 3666922374\n", + "dtype: int64" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Loading 11.55 GB took 33.63 seconds.\n" + ] + } + ], + "source": [ + "# DO NOT CHANGE THIS CELL\n", + "start=time.time()\n", + "df=pd.read_csv('./data/uk_pop.csv')\n", + "duration=time.time()-start\n", + "\n", + "mem_usage_df=df.memory_usage(deep=True)\n", + "display(mem_usage_df)\n", + "\n", + "print(f'Loading {make_decimal(mem_usage_df.sum())} took {round(duration, 2)} seconds.')" + ] + }, + { + "cell_type": "markdown", + "id": "5729520e-3ed8-4ec6-ae1f-ba46d642f48d", + "metadata": {}, + "source": [ + "Below we enable `cuda.pandas` to see the difference. " + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "99aa0f32-4d2a-43a7-bec1-f1b88bcc37c2", + "metadata": {}, + "outputs": [], + "source": [ + "# DO NOT CHANGE THIS CELL\n", + "%load_ext cudf.pandas\n", + "\n", + "import pandas as pd\n", + "import time" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "2b724201-9ad1-4e9b-b712-f3b31bdc4104", + "metadata": {}, + "outputs": [], + "source": [ + "# DO NOT CHANGE THIS CELL\n", + "suffixes = ['B', 'kB', 'MB', 'GB', 'TB', 'PB']\n", + "def make_decimal(nbytes):\n", + " i=0\n", + " while nbytes >= 1024 and i < len(suffixes)-1:\n", + " nbytes/=1024.\n", + " i+=1\n", + " f=('%.2f' % nbytes).rstrip('0').rstrip('.')\n", + " return '%s %s' % (f, suffixes[i])" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "99bdd7b0-8563-41db-bd8e-3a7279394ede", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/plain": [ + "age 58479894\n", + "sex 58479908\n", + "county 58482446\n", + "lat 467839152\n", + "long 467839152\n", + "name 117096917\n", + "Index 0\n", + "dtype: int64" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Loading 1.14 GB took 2.13 seconds.\n" + ] + }, + { + "data": { + "text/html": [ + "
                                                                                                                   \n",
+       "                                             Total time elapsed: 2.705 seconds                                     \n",
+       "                                                                                                                   \n",
+       "                                                           Stats                                                   \n",
+       "                                                                                                                   \n",
+       "┏━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━━━━━━┓\n",
+       "┃ Line no.  Line                                                                      GPU TIME(s)  CPU TIME(s) ┃\n",
+       "┡━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━━━━━━┩\n",
+       "│ 2        │     start=time.time()                                                    │             │             │\n",
+       "│          │                                                                          │             │             │\n",
+       "│ 5        │     dtype_dict={                                                         │             │             │\n",
+       "│          │                                                                          │             │             │\n",
+       "│ 6        │         'age': 'int8',                                                   │             │             │\n",
+       "│          │                                                                          │             │             │\n",
+       "│ 7        │         'sex': 'category',                                               │             │             │\n",
+       "│          │                                                                          │             │             │\n",
+       "│ 8        │         'county': 'category',                                            │             │             │\n",
+       "│          │                                                                          │             │             │\n",
+       "│ 9        │         'lat': 'float64',                                                │             │             │\n",
+       "│          │                                                                          │             │             │\n",
+       "│ 10       │         'long': 'float64',                                               │             │             │\n",
+       "│          │                                                                          │             │             │\n",
+       "│ 11       │         'name': 'category'                                               │             │             │\n",
+       "│          │                                                                          │             │             │\n",
+       "│ 14       │     efficient_df=pd.read_csv('./data/uk_pop.csv', dtype=dtype_dict)      │ 1.728013188 │             │\n",
+       "│          │                                                                          │             │             │\n",
+       "│ 15       │     duration=time.time()-start                                           │             │             │\n",
+       "│          │                                                                          │             │             │\n",
+       "│ 17       │     mem_usage_df=efficient_df.memory_usage('deep')                       │ 0.005340174 │             │\n",
+       "│          │                                                                          │             │             │\n",
+       "│ 18       │     display(mem_usage_df)                                                │ 0.011073721 │ 0.006896915 │\n",
+       "│          │                                                                          │             │             │\n",
+       "│ 20       │     print(f'Loading {make_decimal(mem_usage_df.sum())} took {round(dura… │ 0.004693074 │             │\n",
+       "│          │                                                                          │             │             │\n",
+       "└──────────┴──────────────────────────────────────────────────────────────────────────┴─────────────┴─────────────┘\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[3m \u001b[0m\n", + "\u001b[3m Total time elapsed: 2.705 seconds \u001b[0m\n", + "\u001b[3m \u001b[0m\n", + "\u001b[3m Stats \u001b[0m\n", + "\u001b[3m \u001b[0m\n", + "┏━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━━━━━━┓\n", + "┃\u001b[1m \u001b[0m\u001b[1mLine no.\u001b[0m\u001b[1m \u001b[0m┃\u001b[1m \u001b[0m\u001b[1mLine \u001b[0m\u001b[1m \u001b[0m┃\u001b[1m \u001b[0m\u001b[1mGPU TIME(s)\u001b[0m\u001b[1m \u001b[0m┃\u001b[1m \u001b[0m\u001b[1mCPU TIME(s)\u001b[0m\u001b[1m \u001b[0m┃\n", + "┡━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━━━━━━┩\n", + "│ 2 │ \u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mstart\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m=\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mtime\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m.\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mtime\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m(\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m)\u001b[0m\u001b[48;2;39;40;34m \u001b[0m │ │ │\n", + "│ │ \u001b[48;2;39;40;34m \u001b[0m │ │ │\n", + "│ 5 │ \u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mdtype_dict\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m=\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m{\u001b[0m\u001b[48;2;39;40;34m \u001b[0m │ │ │\n", + "│ │ \u001b[48;2;39;40;34m \u001b[0m │ │ │\n", + "│ 6 │ \u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34mage\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m:\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34mint8\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m,\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[48;2;39;40;34m \u001b[0m │ │ │\n", + "│ │ \u001b[48;2;39;40;34m \u001b[0m │ │ │\n", + "│ 7 │ \u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34msex\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m:\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34mcategory\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m,\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[48;2;39;40;34m \u001b[0m │ │ │\n", + "│ │ \u001b[48;2;39;40;34m \u001b[0m │ │ │\n", + "│ 8 │ \u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34mcounty\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m:\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34mcategory\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m,\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[48;2;39;40;34m \u001b[0m │ │ │\n", + "│ │ \u001b[48;2;39;40;34m \u001b[0m │ │ │\n", + "│ 9 │ \u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34mlat\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m:\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34mfloat64\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m,\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[48;2;39;40;34m \u001b[0m │ │ │\n", + "│ │ \u001b[48;2;39;40;34m \u001b[0m │ │ │\n", + "│ 10 │ \u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34mlong\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m:\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34mfloat64\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m,\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[48;2;39;40;34m \u001b[0m │ │ │\n", + "│ │ \u001b[48;2;39;40;34m \u001b[0m │ │ │\n", + "│ 11 │ \u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34mname\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m:\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34mcategory\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[48;2;39;40;34m \u001b[0m │ │ │\n", + "│ │ \u001b[48;2;39;40;34m \u001b[0m │ │ │\n", + "│ 14 │ \u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mefficient_df\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m=\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mpd\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m.\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mread_csv\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m(\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m./data/uk_pop.csv\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m,\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mdtype\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m=\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mdtype_dict\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m)\u001b[0m\u001b[48;2;39;40;34m \u001b[0m │ 1.728013188 │ │\n", + "│ │ \u001b[48;2;39;40;34m \u001b[0m │ │ │\n", + "│ 15 │ \u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mduration\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m=\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mtime\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m.\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mtime\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m(\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m)\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m-\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mstart\u001b[0m\u001b[48;2;39;40;34m \u001b[0m │ │ │\n", + "│ │ \u001b[48;2;39;40;34m \u001b[0m │ │ │\n", + "│ 17 │ \u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mmem_usage_df\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m=\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mefficient_df\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m.\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mmemory_usage\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m(\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34mdeep\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m)\u001b[0m\u001b[48;2;39;40;34m \u001b[0m │ 0.005340174 │ │\n", + "│ │ \u001b[48;2;39;40;34m \u001b[0m │ │ │\n", + "│ 18 │ \u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mdisplay\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m(\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mmem_usage_df\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m)\u001b[0m\u001b[48;2;39;40;34m \u001b[0m │ 0.011073721 │ 0.006896915 │\n", + "│ │ \u001b[48;2;39;40;34m \u001b[0m │ │ │\n", + "│ 20 │ \u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mprint\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m(\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34mf\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34mLoading \u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m{\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mmake_decimal\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m(\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mmem_usage_df\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m.\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34msum\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m(\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m)\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m)\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m}\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m took \u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m{\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mround\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m(\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mdura…\u001b[0m │ 0.004693074 │ │\n", + "│ │ \u001b[48;2;39;40;34m \u001b[0m │ │ │\n", + "└──────────┴──────────────────────────────────────────────────────────────────────────┴─────────────┴─────────────┘\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "%%cudf.pandas.line_profile\n", + "# DO NOT CHANGE THIS CELL\n", + "start=time.time()\n", + "\n", + "# define data types for each column\n", + "dtype_dict={\n", + " 'age': 'int8', \n", + " 'sex': 'category', \n", + " 'county': 'category', \n", + " 'lat': 'float64', \n", + " 'long': 'float64', \n", + " 'name': 'category'\n", + "}\n", + " \n", + "efficient_df=pd.read_csv('./data/uk_pop.csv', dtype=dtype_dict)\n", + "duration=time.time()-start\n", + "\n", + "mem_usage_df=efficient_df.memory_usage('deep')\n", + "display(mem_usage_df)\n", + "\n", + "print(f'Loading {make_decimal(mem_usage_df.sum())} took {round(duration, 2)} seconds.')" + ] + }, + { + "cell_type": "markdown", + "id": "0f4607d8-6de3-4b27-96d4-a9720d268333", + "metadata": {}, + "source": [ + "We were able to load data faster and more efficiently. \n", + "\n", + "**Note**: Notice that the memory utilized on the GPU is larger than the memory used by the DataFrame. This is expected because there are intermediary processes that use some memory during the data loading process, specifically related to parsing the csv file in this case. \n", + "\n", + "```\n", + "+-----------------------------------------------------------------------------+\n", + "| NVIDIA-SMI 525.60.13 Driver Version: 525.60.13 CUDA Version: 12.0 |\n", + "|-------------------------------+----------------------+----------------------+\n", + "| GPU Name Persistence-M| Bus-Id Disp.A | Volatile Uncorr. ECC |\n", + "| Fan Temp Perf Pwr:Usage/Cap| Memory-Usage | GPU-Util Compute M. |\n", + "| | | MIG M. |\n", + "|===============================+======================+======================|\n", + "| 0 Tesla T4 Off | 00000000:00:1B.0 Off | 0 |\n", + "| N/A 32C P0 26W / 70W | 1378MiB / 15360MiB | 0% Default |\n", + "| | | N/A |\n", + "+-------------------------------+----------------------+----------------------+\n", + "| 1 Tesla T4 Off | 00000000:00:1C.0 Off | 0 |\n", + "| N/A 31C P0 26W / 70W | 168MiB / 15360MiB | 0% Default |\n", + "| | | N/A |\n", + "+-------------------------------+----------------------+----------------------+\n", + "| 2 Tesla T4 Off | 00000000:00:1D.0 Off | 0 |\n", + "| N/A 30C P0 26W / 70W | 168MiB / 15360MiB | 0% Default |\n", + "| | | N/A |\n", + "+-------------------------------+----------------------+----------------------+\n", + "| 3 Tesla T4 Off | 00000000:00:1E.0 Off | 0 |\n", + "| N/A 30C P0 26W / 70W | 168MiB / 15360MiB | 0% Default |\n", + "| | | N/A |\n", + "+-------------------------------+----------------------+----------------------+\n", + " \n", + "+-----------------------------------------------------------------------------+\n", + "| Processes: |\n", + "| GPU GI CI PID Type Process name GPU Memory |\n", + "| ID ID Usage |\n", + "|=============================================================================|\n", + "+-----------------------------------------------------------------------------+\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "92f7ee37-4acb-46aa-bb73-4c0139d3f6b8", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Tue Oct 21 08:08:25 2025 \n", + "+-----------------------------------------------------------------------------+\n", + "| NVIDIA-SMI 525.85.12 Driver Version: 525.85.12 CUDA Version: 12.0 |\n", + "|-------------------------------+----------------------+----------------------+\n", + "| GPU Name Persistence-M| Bus-Id Disp.A | Volatile Uncorr. ECC |\n", + "| Fan Temp Perf Pwr:Usage/Cap| Memory-Usage | GPU-Util Compute M. |\n", + "| | | MIG M. |\n", + "|===============================+======================+======================|\n", + "| 0 Tesla T4 On | 00000000:00:1B.0 Off | 0 |\n", + "| N/A 28C P0 24W / 70W | 11314MiB / 15360MiB | 0% Default |\n", + "| | | N/A |\n", + "+-------------------------------+----------------------+----------------------+\n", + "| 1 Tesla T4 On | 00000000:00:1C.0 Off | 0 |\n", + "| N/A 29C P0 25W / 70W | 168MiB / 15360MiB | 0% Default |\n", + "| | | N/A |\n", + "+-------------------------------+----------------------+----------------------+\n", + "| 2 Tesla T4 On | 00000000:00:1D.0 Off | 0 |\n", + "| N/A 28C P0 25W / 70W | 168MiB / 15360MiB | 0% Default |\n", + "| | | N/A |\n", + "+-------------------------------+----------------------+----------------------+\n", + "| 3 Tesla T4 On | 00000000:00:1E.0 Off | 0 |\n", + "| N/A 29C P0 24W / 70W | 168MiB / 15360MiB | 0% Default |\n", + "| | | N/A |\n", + "+-------------------------------+----------------------+----------------------+\n", + " \n", + "+-----------------------------------------------------------------------------+\n", + "| Processes: |\n", + "| GPU GI CI PID Type Process name GPU Memory |\n", + "| ID ID Usage |\n", + "|=============================================================================|\n", + "+-----------------------------------------------------------------------------+\n" + ] + } + ], + "source": [ + "# DO NOT CHANGE THIS CELL\n", + "!nvidia-smi" + ] + }, + { + "cell_type": "markdown", + "id": "c031d2c7-03cb-4ac7-a195-70fc25cb191d", + "metadata": {}, + "source": [ + "When loading data this way, we may be able to fit more data. The optimal dataset size depends on various factors including the specific operations being performed, the complexity of the workload, and the available GPU memory. To maximize acceleration, datasets should ideally fit within GPU memory, with ample space left for operations that can spike memory requirements. As a general rule of thumb, cuDF recommends data sets that are less than 50% of the GPU memory capacity. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ec6cefea-dc64-4f13-815e-081cd35651b9", + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "# DO NOT CHANGE THIS CELL\n", + "# 1 gigabytes = 1073741824 bytes\n", + "mem_capacity=16*1073741824\n", + "\n", + "mem_per_record=mem_usage_df.sum()/len(efficient_df)\n", + "\n", + "print(f'We can load {int(mem_capacity/2/mem_per_record)} rows.')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ddaaa1ac-66ec-4323-9842-2543c6d85e4e", + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "# DO NOT CHANGE THIS CELL\n", + "import IPython\n", + "app = IPython.Application.instance()\n", + "app.kernel.do_shutdown(True)" + ] + }, + { + "cell_type": "markdown", + "id": "658e9847-775f-4d12-af4e-8f896df4e6fe", + "metadata": {}, + "source": [ + "**Well Done!** Let's move to the [next notebook](1-04_interoperability.ipynb). " + ] + }, + { + "cell_type": "markdown", + "id": "b86451cf-60e6-4733-b431-1bc0bd586bc2", + "metadata": {}, + "source": [ + "" + ] + } + ], + "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.15" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/ds/25-1/2/1-04_interoperability.ipynb b/ds/25-1/2/1-04_interoperability.ipynb new file mode 100644 index 0000000..2ddabd0 --- /dev/null +++ b/ds/25-1/2/1-04_interoperability.ipynb @@ -0,0 +1,1269 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "b53a7b12-538d-4459-b82a-a35c8c417849", + "metadata": {}, + "source": [ + "" + ] + }, + { + "cell_type": "markdown", + "id": "ae497b71-bc43-471e-8970-88a1878e7cf9", + "metadata": {}, + "source": [ + "# Fundamentals of Accelerated Data Science # " + ] + }, + { + "cell_type": "markdown", + "id": "a149b6d1-1880-4a5d-9d71-f963d3097aa4", + "metadata": {}, + "source": [ + "## 04 - Interoperability of the GPU PyData Ecosystem ##\n", + "\n", + "**Table of Contents**\n", + "
\n", + "This notebook provides examples of how we can use cuDF and CuPy together to take advantage of CuPy array functionality (such as advanced linear algebra operations). This notebook covers the below sections: \n", + "1. [NumPy, SciPy, and CuPy](#NumPy,-SciPy,-and-CuPy)\n", + " * [cuDF vs. CuPy](#cuDF-vs.-CuPy)\n", + "2. [Working with CuPy](#Working-with-CuPy)\n", + "3. [Grid Converter](#Grid-Converter)\n", + " * [Lat/Long to OSGB Grid Converter with NumPy](#Lat/Long-to-OSGB-Grid-Converter-with-NumPy)\n", + " * [Lat/Long to OSGB Grid Converter with CuPy](#Lat/Long-to-OSGB-Grid-Converter-with-CuPy)\n", + " * [Exercise #1 - Adding Grid Coordinate Columns to Dataframe](#Exercise-#1---Adding-Grid-Coordinate-Columns-to-DataFrame)\n", + "4. [Boolean Array Indexing](#Boolean-Array-Indexing)" + ] + }, + { + "cell_type": "markdown", + "id": "3c0ac5b5-5cc1-4240-9375-297cf15c6fbc", + "metadata": {}, + "source": [ + "## NumPy, SciPy, and CuPy ##\n", + "Per it's own user guide, [NumPy](https://numpy.org/doc/stable/user/whatisnumpy.html) is the fundamental package for scientific computing in Python. It is a Python library that provides a **multidimensional array object**, various derived objects (such as masked arrays and matrices), and an assortment of routines for fast operations on arrays. These operations include mathematical, logical, shape manipulation, sorting, selecting, I/O, discrete Fourier transforms, basic linear algebra, basic statistical operations, random simulation and much more. While NumPy focuses on arrays, mathematical operations, and basic linear algebra, [SciPy](https://docs.scipy.org/doc/scipy-1.8.1/tutorial/general.html) builds on this foundation to provide additional functionality, especially in the domain of scientific computing and optimization. \n", + "\n", + "On the other hands, [CuPy](https://cupy.dev/) is an open-source array library for GPU-accelerated computing with Python. CuPy can be seen as a GPU-accelerated counterpart to NumPy, offering similar functionality and API with the added benefit of GPU acceleration for compatible workloads. While NumPy operates on CPU memory, CuPy primarily works with GPU memory, leveraging CUDA-enabled GPUs for computation. CuPy's interface is highly compatible with NumPy and SciPy. In most cases it can be used as a drop-in replacement. All we need to do is just replace `numpy` and `scipy` with `cupy` and `cupyx.scipy` in the Python code. This makes it easier for users familiar with NumPy to transition to GPU-accelerated computing. \n", + "\n", + "CuPy is designed to work seamlessly with other GPU-accelerated libraries in the RAPIDS ecosystem, similar to how NumPy works with pandas and other CPU-based libraries. By keeping data on the GPU throughout the workflow, we are able to reduce data transfer overhead between CPU and GPU memory. " + ] + }, + { + "cell_type": "markdown", + "id": "3e1483d0-c479-474a-a7fa-a31f4140268d", + "metadata": {}, + "source": [ + "### cuDF vs. CuPy ###\n", + "So far, the DataFrame we've worked with puts data in a structured, tabular format. This is useful when we need to perform DataFrame-like operations such as grouping, aggregating, filtering, and joining data. However, there might be use cases that requires working with multi-dimensional arrays or matrics, such as perfrming linear algebra operations or scientific computing tasks. For these instances, we would want to use libraries that are dedicated to performing these tasks, such as NumPy, SciPy, or CuPy. In other words, use cuDF when working with high-level (less abstract) data manipulation, and use CuPy when doing low-level numerical operations on multi-dimensional arrays. \n", + "\n", + "In practice, most data scientists work with both libraries, since most of their workflows involve DataFrame operations and array-based computations. For example, we might use cuDF for data loading and preprocessing, then convert to CuPy arrays for specific numerical computations, and convert back to cuDF for further analysis or output. cuDF and CuPy are designed to be interoperable, allowing us to easily convert between cuDF DataFrames/Series and CuPy arrays while keeping the data on the GPU. This enables us to create efficient workflows that take advntage of both libraries' strengths. " + ] + }, + { + "cell_type": "markdown", + "id": "2fb75cf7-2590-4638-8e92-6d90a8e34b63", + "metadata": {}, + "source": [ + "## Working with CuPy ##\n", + "There are several ways to use CuPy. From cuDF, the `DataFrame.values` property will return the CuPy representation of the data frame. Alternatively, we can also convert via the CUDA array interface using `DataFrame.to_cupy()`. In addition to these, we can also pass the Series to the `cupy.asarray()` function since cuDF Series exposes the CUDA array interface as the fastest approach. \n", + "\n", + "Below we demonstrate a **row-wise sum** on the DataFrame. cuDF’s support for row-wise operations isn’t mature, so we’d need to either transpose the DataFrame or write a UDF and explicitly calculate the sum across each row. Transposing could lead to hundreds of thousands of columns (which cuDF wouldn’t perform well with) depending on our data’s shape, and writing a UDF can be time intensive. By leveraging the interoperability of the GPU PyData ecosystem, this operation becomes very easy. " + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "4808029f-a5e6-4066-b125-84d63c3c6d43", + "metadata": {}, + "outputs": [], + "source": [ + "# DO NOT CHANGE THIS CELL\n", + "# import libraries\n", + "import cudf\n", + "import time" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "50653cc1-53a1-4141-b1fb-4ef3ef7994f4", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
abcd
00101001000
11111011001
22121021002
33131031003
44141041004
\n", + "
" + ], + "text/plain": [ + " a b c d\n", + "0 0 10 100 1000\n", + "1 1 11 101 1001\n", + "2 2 12 102 1002\n", + "3 3 13 103 1003\n", + "4 4 14 104 1004" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# DO NOT CHANGE THIS CELL\n", + "num_ele = 1000000\n", + "\n", + "df = cudf.DataFrame(\n", + " {\n", + " \"a\": range(num_ele),\n", + " \"b\": range(10, num_ele + 10),\n", + " \"c\": range(100, num_ele + 100),\n", + " \"d\": range(1000, num_ele + 1000)\n", + " }\n", + ")\n", + "\n", + "# preview\n", + "df.head()" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "b122f0a0-0354-43ed-9078-651d9761d6ed", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "0 1110\n", + "1 1114\n", + "2 1118\n", + "3 1122\n", + "4 1126\n", + " ... \n", + "999995 4001090\n", + "999996 4001094\n", + "999997 4001098\n", + "999998 4001102\n", + "999999 4001106\n", + "Length: 1000000, dtype: int64" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "0.3188893795013428" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# DO NOT CHANGE THIS CELL\n", + "start=time.time()\n", + "display(df.sum(axis=1))\n", + "time.time()-start" + ] + }, + { + "cell_type": "markdown", + "id": "d49ebab0-bff0-4b7e-aabd-c46430799983", + "metadata": {}, + "source": [ + "The same operation runs faster with CuPy. " + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "9b618d3b-9b6d-423d-beb7-98334a8b3339", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/plain": [ + "array([ 1110, 1114, 1118, ..., 4001098, 4001102, 4001106])" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "0.21471166610717773" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# DO NOT CHANGE THIS CELL\n", + "arr=df.values\n", + "\n", + "start=time.time()\n", + "# alternative approach\n", + "# arr=df.to_cupy()\n", + "\n", + "display(arr.sum(axis=1))\n", + "time.time()-start" + ] + }, + { + "cell_type": "markdown", + "id": "ae656ac1-d4a2-499f-a904-6d1409c6ba79", + "metadata": {}, + "source": [ + "When using cuDF pandas, we can use the `.values` property as well as the `cupy.asarray()` function. \n", + "\n", + "**Note**: We can use the `.to_numpy()` method to convert cuDF DataFrames or Series to NumPy arrays. " + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "84e218f5-b4ee-4d49-baff-eac5f1f76b3f", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/plain": [ + "{'status': 'ok', 'restart': True}" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# DO NOT CHANGE THIS CELL\n", + "import IPython\n", + "app = IPython.Application.instance()\n", + "app.kernel.do_shutdown(True)" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "44200db8-d133-44c5-9f71-2796ec37df5c", + "metadata": {}, + "outputs": [], + "source": [ + "# DO NOT CHANGE THIS CELL\n", + "%load_ext cudf.pandas\n", + "import pandas as pd\n", + "\n", + "import numpy as np\n", + "import cupy as cp\n", + "import time" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "e8016e6d-829d-4117-a37c-bae82431626c", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
abcd
00101001000
11111011001
22121021002
33131031003
44141041004
\n", + "
" + ], + "text/plain": [ + " a b c d\n", + "0 0 10 100 1000\n", + "1 1 11 101 1001\n", + "2 2 12 102 1002\n", + "3 3 13 103 1003\n", + "4 4 14 104 1004" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# DO NOT CHANGE THIS CELL\n", + "num_ele = 1000000\n", + "\n", + "df = pd.DataFrame(\n", + " {\n", + " \"a\": range(num_ele),\n", + " \"b\": range(10, num_ele + 10),\n", + " \"c\": range(100, num_ele + 100),\n", + " \"d\": range(1000, num_ele + 1000)\n", + " }\n", + ")\n", + "\n", + "# preview\n", + "df.head()" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "0d314943-69cb-4fcd-a01a-5fe4734ff0bf", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "array([ 1110, 1114, 1118, ..., 4001098, 4001102, 4001106])" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
                                                                       \n",
+       "                       Total time elapsed: 0.727 seconds               \n",
+       "                                                                       \n",
+       "                                     Stats                             \n",
+       "                                                                       \n",
+       "┏━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━━━━━━┓\n",
+       "┃ Line no.  Line                          GPU TIME(s)  CPU TIME(s) ┃\n",
+       "┡━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━━━━━━┩\n",
+       "│ 2        │     arr=df.values            │ 0.324871478 │             │\n",
+       "│          │                              │             │             │\n",
+       "│ 6        │     start=time.time()        │             │             │\n",
+       "│          │                              │             │             │\n",
+       "│ 8        │     display(arr.sum(axis=1)) │ 0.012115336 │             │\n",
+       "│          │                              │             │             │\n",
+       "│ 10       │     time.time()-start        │             │             │\n",
+       "│          │                              │             │             │\n",
+       "└──────────┴──────────────────────────────┴─────────────┴─────────────┘\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[3m \u001b[0m\n", + "\u001b[3m Total time elapsed: 0.727 seconds \u001b[0m\n", + "\u001b[3m \u001b[0m\n", + "\u001b[3m Stats \u001b[0m\n", + "\u001b[3m \u001b[0m\n", + "┏━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━━━━━━┓\n", + "┃\u001b[1m \u001b[0m\u001b[1mLine no.\u001b[0m\u001b[1m \u001b[0m┃\u001b[1m \u001b[0m\u001b[1mLine \u001b[0m\u001b[1m \u001b[0m┃\u001b[1m \u001b[0m\u001b[1mGPU TIME(s)\u001b[0m\u001b[1m \u001b[0m┃\u001b[1m \u001b[0m\u001b[1mCPU TIME(s)\u001b[0m\u001b[1m \u001b[0m┃\n", + "┡━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━━━━━━┩\n", + "│ 2 │ \u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34marr\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m=\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mdf\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m.\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mvalues\u001b[0m\u001b[48;2;39;40;34m \u001b[0m │ 0.324871478 │ │\n", + "│ │ \u001b[48;2;39;40;34m \u001b[0m │ │ │\n", + "│ 6 │ \u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mstart\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m=\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mtime\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m.\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mtime\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m(\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m)\u001b[0m\u001b[48;2;39;40;34m \u001b[0m │ │ │\n", + "│ │ \u001b[48;2;39;40;34m \u001b[0m │ │ │\n", + "│ 8 │ \u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mdisplay\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m(\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34marr\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m.\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34msum\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m(\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34maxis\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m=\u001b[0m\u001b[38;2;174;129;255;48;2;39;40;34m1\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m)\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m)\u001b[0m │ 0.012115336 │ │\n", + "│ │ \u001b[48;2;39;40;34m \u001b[0m │ │ │\n", + "│ 10 │ \u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mtime\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m.\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mtime\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m(\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m)\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m-\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mstart\u001b[0m\u001b[48;2;39;40;34m \u001b[0m │ │ │\n", + "│ │ \u001b[48;2;39;40;34m \u001b[0m │ │ │\n", + "└──────────┴──────────────────────────────┴─────────────┴─────────────┘\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "%%cudf.pandas.line_profile\n", + "# DO NOT CHANGE THIS CELL\n", + "arr=df.values\n", + "# alternative approach\n", + "# arr=cp.asarray(df)\n", + "\n", + "start=time.time()\n", + "\n", + "display(arr.sum(axis=1))\n", + "\n", + "time.time()-start" + ] + }, + { + "cell_type": "markdown", + "id": "5a97fac8-b555-4ec1-a555-9b5ab79c2fe1", + "metadata": {}, + "source": [ + "Just like we can do with NumPy and pandas, we can weave cuDF and CuPy together in the same workflow while keeping the data entirely on the GPU. We’re able to seamlessly move between data structures in this ecosystem, giving us enormous flexibility without sacrificing speed. If we’re working with RAPIDS cuDF but need a more linear-algebra oriented function that exists in CuPy, we can leverage the interoperability of the GPU PyData ecosystem to use that function. \n", + "\n", + "To convert a CuPy array to a cuDF DataFrame or Series, we can use their respective constructors. " + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "9cea340d-c9c4-468d-8431-015492682db9", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
abcdsum
001010010001110
111110110011114
221210210021118
331310310031122
441410410041126
\n", + "
" + ], + "text/plain": [ + " a b c d sum\n", + "0 0 10 100 1000 1110\n", + "1 1 11 101 1001 1114\n", + "2 2 12 102 1002 1118\n", + "3 3 13 103 1003 1122\n", + "4 4 14 104 1004 1126" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# DO NOT CHANGE THIS CELL\n", + "df['sum']=arr.sum(axis=1)\n", + "\n", + "df.head()" + ] + }, + { + "cell_type": "markdown", + "id": "2cfe3955-2849-451b-945a-c37955b91235", + "metadata": {}, + "source": [ + "## Grid Converter ##\n", + "Much of our data is provided with latitude and longitude coordinates, but for some of our tasks involving distance - identifying geographically dense clusters of infected people, locating the nearest hospital or clinic from a given person - it is convenient to have Cartesian grid coordinates instead. By using a region-specific map projection - in this case, the [Ordnance Survey Great Britain 1936](https://en.wikipedia.org/wiki/Ordnance_Survey_National_Grid) - we can compute local distances efficiently and with good accuracy." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "8dc82f3c-f1cb-436b-becb-a97635ec5f6a", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
agesexcountylatlongname
00mDARLINGTON54.533638-1.524400FRANCIS
10mDARLINGTON54.426254-1.465314EDWARD
20mDARLINGTON54.555199-1.496417TEDDY
30mDARLINGTON54.547909-1.572342ANGUS
40mDARLINGTON54.477638-1.605995CHARLIE
\n", + "
" + ], + "text/plain": [ + " age sex county lat long name\n", + "0 0 m DARLINGTON 54.533638 -1.524400 FRANCIS\n", + "1 0 m DARLINGTON 54.426254 -1.465314 EDWARD\n", + "2 0 m DARLINGTON 54.555199 -1.496417 TEDDY\n", + "3 0 m DARLINGTON 54.547909 -1.572342 ANGUS\n", + "4 0 m DARLINGTON 54.477638 -1.605995 CHARLIE" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# DO NOT CHANGE THIS CELL\n", + "dtype_dict={\n", + " 'age': 'int8', \n", + " 'sex': 'category', \n", + " 'county': 'category', \n", + " 'lat': 'float32', \n", + " 'long': 'float32', \n", + " 'name': 'category'\n", + "}\n", + " \n", + "df=pd.read_csv('./data/uk_pop.csv', dtype=dtype_dict)\n", + "df.head()" + ] + }, + { + "cell_type": "markdown", + "id": "f25cfe6a-9f31-4a13-b06f-c85a95637a44", + "metadata": {}, + "source": [ + "### Lat/Long to OSGB Grid Converter with NumPy ###\n", + "To perform coordinate conversion, we will create a function `latlong2osgbgrid` which accepts latitude/longitude coordinates and converts them to [OSGB36 coordinates](https://en.wikipedia.org/wiki/Ordnance_Survey_National_Grid): \"northing\" and \"easting\" values representing the point's Cartesian coordinate distances from the southwest corner of the grid.\n", + "\n", + "Immediately below is `latlong2osgbgrid`, which relies heavily on NumPy:" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "3e298328-a11c-4cda-ae15-eb292fed3a5c", + "metadata": {}, + "outputs": [], + "source": [ + "# https://www.ordnancesurvey.co.uk/docs/support/guide-coordinate-systems-great-britain.pdf\n", + "\n", + "def latlong2osgbgrid(lat, long, input_degrees=True):\n", + " '''\n", + " Converts latitude and longitude (ellipsoidal) coordinates into northing and easting (grid) coordinates, using a Transverse Mercator projection.\n", + " \n", + " Inputs:\n", + " lat: latitude coordinate (north)\n", + " long: longitude coordinate (east)\n", + " input_degrees: if True (default), interprets the coordinates as degrees; otherwise, interprets coordinates as radians\n", + " \n", + " Output:\n", + " (northing, easting)\n", + " '''\n", + " \n", + " if input_degrees:\n", + " lat = lat * np.pi/180\n", + " long = long * np.pi/180\n", + "\n", + " a = 6377563.396\n", + " b = 6356256.909\n", + " e2 = (a**2 - b**2) / a**2\n", + "\n", + " N0 = -100000 # northing of true origin\n", + " E0 = 400000 # easting of true origin\n", + " F0 = .9996012717 # scale factor on central meridian\n", + " phi0 = 49 * np.pi / 180 # latitude of true origin\n", + " lambda0 = -2 * np.pi / 180 # longitude of true origin and central meridian\n", + " \n", + " sinlat = np.sin(lat)\n", + " coslat = np.cos(lat)\n", + " tanlat = np.tan(lat)\n", + " \n", + " latdiff = lat-phi0\n", + " longdiff = long-lambda0\n", + "\n", + " n = (a-b) / (a+b)\n", + " nu = a * F0 * (1 - e2 * sinlat ** 2) ** -.5\n", + " rho = a * F0 * (1 - e2) * (1 - e2 * sinlat ** 2) ** -1.5\n", + " eta2 = nu / rho - 1\n", + " M = b * F0 * ((1 + n + 5/4 * (n**2 + n**3)) * latdiff - \n", + " (3*(n+n**2) + 21/8 * n**3) * np.sin(latdiff) * np.cos(lat+phi0) +\n", + " 15/8 * (n**2 + n**3) * np.sin(2*(latdiff)) * np.cos(2*(lat+phi0)) - \n", + " 35/24 * n**3 * np.sin(3*(latdiff)) * np.cos(3*(lat+phi0)))\n", + " I = M + N0\n", + " II = nu/2 * sinlat * coslat\n", + " III = nu/24 * sinlat * coslat ** 3 * (5 - tanlat ** 2 + 9 * eta2)\n", + " IIIA = nu/720 * sinlat * coslat ** 5 * (61-58 * tanlat**2 + tanlat**4)\n", + " IV = nu * coslat\n", + " V = nu / 6 * coslat**3 * (nu/rho - np.tan(lat)**2)\n", + " VI = nu / 120 * coslat ** 5 * (5 - 18 * tanlat**2 + tanlat**4 + 14 * eta2 - 58 * tanlat**2 * eta2)\n", + "\n", + " northing = I + II * longdiff**2 + III * longdiff**4 + IIIA * longdiff**6\n", + " easting = E0 + IV * longdiff + V * longdiff**3 + VI * longdiff**5\n", + "\n", + " return(northing, easting)" + ] + }, + { + "cell_type": "markdown", + "id": "a4ddcbe5-7524-4976-a94d-ff6753da3ad8", + "metadata": {}, + "source": [ + "### Lat/Long to OSGB Grid Converter with CuPy ###\n", + "In the following `latlong2osgbgrid_cupy`, we simply swap `cp` in for `np`. While CuPy supports a wide variety of powerful GPU-accelerated tasks, this simple technique of being able to swap in CuPy calls for NumPy calls makes it an incredibly powerful tool to have at our disposal." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "41ffa2a3-34ba-4664-9399-4b74cc28f623", + "metadata": {}, + "outputs": [], + "source": [ + "# https://www.ordnancesurvey.co.uk/docs/support/guide-coordinate-systems-great-britain.pdf\n", + "\n", + "def latlong2osgbgrid_cupy(lat, long, input_degrees=True):\n", + " '''\n", + " Converts latitude and longitude (ellipsoidal) coordinates into northing and easting (grid) coordinates, using a Transverse Mercator projection.\n", + " \n", + " Inputs:\n", + " lat: latitude coordinate (north)\n", + " long: longitude coordinate (east)\n", + " input_degrees: if True (default), interprets the coordinates as degrees; otherwise, interprets coordinates as radians\n", + " \n", + " Output:\n", + " (northing, easting)\n", + " '''\n", + " \n", + " if input_degrees:\n", + " lat = lat * cp.pi/180\n", + " long = long * cp.pi/180\n", + "\n", + " a = 6377563.396\n", + " b = 6356256.909\n", + " e2 = (a**2 - b**2) / a**2\n", + "\n", + " N0 = -100000 # northing of true origin\n", + " E0 = 400000 # easting of true origin\n", + " F0 = .9996012717 # scale factor on central meridian\n", + " phi0 = 49 * cp.pi / 180 # latitude of true origin\n", + " lambda0 = -2 * cp.pi / 180 # longitude of true origin and central meridian\n", + " \n", + " sinlat = cp.sin(lat)\n", + " coslat = cp.cos(lat)\n", + " tanlat = cp.tan(lat)\n", + " \n", + " latdiff = lat-phi0\n", + " longdiff = long-lambda0\n", + "\n", + " n = (a-b) / (a+b)\n", + " nu = a * F0 * (1 - e2 * sinlat ** 2) ** -.5\n", + " rho = a * F0 * (1 - e2) * (1 - e2 * sinlat ** 2) ** -1.5\n", + " eta2 = nu / rho - 1\n", + " M = b * F0 * ((1 + n + 5/4 * (n**2 + n**3)) * latdiff - \n", + " (3*(n+n**2) + 21/8 * n**3) * cp.sin(latdiff) * cp.cos(lat+phi0) +\n", + " 15/8 * (n**2 + n**3) * cp.sin(2*(latdiff)) * cp.cos(2*(lat+phi0)) - \n", + " 35/24 * n**3 * cp.sin(3*(latdiff)) * cp.cos(3*(lat+phi0)))\n", + " I = M + N0\n", + " II = nu/2 * sinlat * coslat\n", + " III = nu/24 * sinlat * coslat ** 3 * (5 - tanlat ** 2 + 9 * eta2)\n", + " IIIA = nu/720 * sinlat * coslat ** 5 * (61-58 * tanlat**2 + tanlat**4)\n", + " IV = nu * coslat\n", + " V = nu / 6 * coslat**3 * (nu/rho - cp.tan(lat)**2)\n", + " VI = nu / 120 * coslat ** 5 * (5 - 18 * tanlat**2 + tanlat**4 + 14 * eta2 - 58 * tanlat**2 * eta2)\n", + "\n", + " northing = I + II * longdiff**2 + III * longdiff**4 + IIIA * longdiff**6\n", + " easting = E0 + IV * longdiff + V * longdiff**3 + VI * longdiff**5\n", + "\n", + " return(northing, easting)" + ] + }, + { + "cell_type": "markdown", + "id": "8b9f1ea5-595d-4933-afb0-29fa75feea29", + "metadata": {}, + "source": [ + "Below we pass the latitude/longitude coordinates into the converter, which returns north and east values within the OSGB grid. " + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "c35f24b6-0a70-4ad0-897b-fdbb9dee3cec", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 479 ms, sys: 563 ms, total: 1.04 s\n", + "Wall time: 1.04 s\n" + ] + } + ], + "source": [ + "%%time\n", + "# DO NOT CHANGE THIS CELL\n", + "numpy_lat = np.asarray(df['lat'])\n", + "numpy_long = np.asarray(df['long'])" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "edc2feb1-29a8-420c-80ab-62fff620edaa", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 12.4 s, sys: 3.58 s, total: 16 s\n", + "Wall time: 16 s\n" + ] + } + ], + "source": [ + "%%time\n", + "# DO NOT CHANGE THIS CELL\n", + "n_numpy_array, e_numpy_array = latlong2osgbgrid(numpy_lat, numpy_long)" + ] + }, + { + "cell_type": "markdown", + "id": "b3f10d6f-2044-4517-904e-c9bd46350f17", + "metadata": {}, + "source": [ + "### Exercise #1 - Adding Grid Coordinate Columns to DataFrame ###\n", + "Now we will utilize `latlong2osgbgrid_cupy` to add `northing` and `easting` columns to `df`. We start by converting the two columns we need, `lat` and `long`, to CuPy arrays with the `cp.asarray()` function. Because cuDF and CuPy interface directly via the `__cuda_array_interface__`, the conversion can happen in nanoseconds. \n", + "**Instructions**:
\n", + "* Execute the below cell to create CuPy arrays for the `lat` and `long` columns. \n", + "* Modify the `` only and execute the cell below to use `latlong2osgbgrid_cupy` with `cupy_lat` and `cupy_long`, followed by add them as the `northing` and `easting` columns with the dtype `float32`. " + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "3febd9fb-fc44-41f6-9712-60902da18d22", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 656 μs, sys: 209 μs, total: 865 μs\n", + "Wall time: 880 μs\n" + ] + } + ], + "source": [ + "%%time\n", + "# DO NOT CHANGE THIS CELL\n", + "cupy_lat = cp.asarray(df['lat'])\n", + "cupy_long = cp.asarray(df['long'])" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "da67fb99-fdab-49c4-9480-a9b5546dbc95", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "age int8\n", + "sex category\n", + "county category\n", + "lat float32\n", + "long float32\n", + "name category\n", + "northing float32\n", + "easting float32\n", + "dtype: object\n", + "CPU times: user 242 ms, sys: 17.9 ms, total: 259 ms\n", + "Wall time: 367 ms\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
agesexcountylatlongnamenorthingeasting
00mDARLINGTON54.533638-1.524400FRANCIS515491.90625430772.15625
10mDARLINGTON54.426254-1.465314EDWARD503572.46875434685.87500
20mDARLINGTON54.555199-1.496417TEDDY517903.65625432565.53125
30mDARLINGTON54.547909-1.572342ANGUS517059.90625427660.65625
40mDARLINGTON54.477638-1.605995CHARLIE509228.68750425527.78125
\n", + "
" + ], + "text/plain": [ + " age sex county lat long name northing \\\n", + "0 0 m DARLINGTON 54.533638 -1.524400 FRANCIS 515491.90625 \n", + "1 0 m DARLINGTON 54.426254 -1.465314 EDWARD 503572.46875 \n", + "2 0 m DARLINGTON 54.555199 -1.496417 TEDDY 517903.65625 \n", + "3 0 m DARLINGTON 54.547909 -1.572342 ANGUS 517059.90625 \n", + "4 0 m DARLINGTON 54.477638 -1.605995 CHARLIE 509228.68750 \n", + "\n", + " easting \n", + "0 430772.15625 \n", + "1 434685.87500 \n", + "2 432565.53125 \n", + "3 427660.65625 \n", + "4 425527.78125 " + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%%time\n", + "n_cupy_array, e_cupy_array = latlong2osgbgrid_cupy(cupy_lat, cupy_long)\n", + "df['northing'] = n_cupy_array.astype('float32')\n", + "df['easting'] = e_cupy_array.astype('float32')\n", + "print(df.dtypes)\n", + "df.head()" + ] + }, + { + "cell_type": "raw", + "id": "4c65c727-18a4-47a6-9e43-acba85212834", + "metadata": { + "scrolled": true + }, + "source": [ + "\n", + "%%time\n", + "n_cupy_array, e_cupy_array = latlong2osgbgrid_cupy(cupy_lat, cupy_long)\n", + "df['northing'] = pd.Series(n_cupy_array).astype('float32')\n", + "df['easting'] = e_cupy_array.astype('float32')\n", + "print(df.dtypes)\n", + "df.head()" + ] + }, + { + "cell_type": "markdown", + "id": "82640e5c-ae3a-495b-bc48-df9b46f5d4b1", + "metadata": {}, + "source": [ + "Click ... for solution. " + ] + }, + { + "cell_type": "markdown", + "id": "d0e92cb7-c138-49ae-9aa4-e55f76235b98", + "metadata": {}, + "source": [ + "## Boolean Array Indexing ##\n", + "Below we use `np.logical_and` for element-wise boolean selection." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "61d7946d-f239-4c5b-9c38-2057aa88a085", + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "# DO NOT CHANGE THIS CELL\n", + "start=time.time()\n", + "display(df.loc[np.logical_and(df['name'].str.startswith('E'), df['name'].str.endswith('D'))].head())\n", + "print(f'Duration: {round(time.time()-start, 2)} seconds')" + ] + }, + { + "cell_type": "markdown", + "id": "3f768cdc-3412-4256-921e-ffccfdcd6058", + "metadata": {}, + "source": [ + "Below we use the CuPy for boolean selection. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "04ac5517-c69b-4355-81e0-1e94deddcb2e", + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "%%cudf.pandas.line_profile\n", + "# DO NOT CHANGE THIS CELL\n", + "start=time.time()\n", + "display(df.loc[cp.logical_and(df['name'].str.startswith('E'), df['name'].str.endswith('D'))].head())\n", + "print(f'Duration: {round(time.time()-start, 2)} seconds')" + ] + }, + { + "cell_type": "markdown", + "id": "c2cab4f4-eff4-4b09-a68d-dc1c089bfa72", + "metadata": {}, + "source": [ + "**Note**: String array is not yet implemented in CuPy. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7321b4f6-61c6-4968-a77b-3d3ed4cde745", + "metadata": {}, + "outputs": [], + "source": [ + "# DO NOT CHANGE THIS CELL\n", + "import IPython\n", + "app = IPython.Application.instance()\n", + "app.kernel.do_shutdown(True)" + ] + }, + { + "cell_type": "markdown", + "id": "f9d91047-2067-42f7-89b5-e9915eefb1de", + "metadata": {}, + "source": [ + "**Well Done!** Let's move to the [next notebook](1-05_grouping.ipynb). " + ] + }, + { + "cell_type": "markdown", + "id": "ec10008d-0c52-4f35-ad4a-969a892c35b8", + "metadata": {}, + "source": [ + "" + ] + } + ], + "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.15" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/ds/25-1/2/1-05_grouping.ipynb b/ds/25-1/2/1-05_grouping.ipynb new file mode 100644 index 0000000..16ce4cf --- /dev/null +++ b/ds/25-1/2/1-05_grouping.ipynb @@ -0,0 +1,1085 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "b53a7b12-538d-4459-b82a-a35c8c417849", + "metadata": {}, + "source": [ + "" + ] + }, + { + "cell_type": "markdown", + "id": "ae497b71-bc43-471e-8970-88a1878e7cf9", + "metadata": {}, + "source": [ + "# Fundamentals of Accelerated Data Science # " + ] + }, + { + "cell_type": "markdown", + "id": "a149b6d1-1880-4a5d-9d71-f963d3097aa4", + "metadata": {}, + "source": [ + "## 05 - Grouping ##\n", + "\n", + "**Table of Contents**\n", + "
\n", + "This notebook discusses and demonstrates how grouping in used in data science. This notebook covers the below sections: \n", + "1. [Grouping](#Grouping)\n", + " * [Split, Apply, and Combine](#Split,-Apply,-and-Combine)\n", + " * [Exercise #1 - Average Age Per County](#Exercise-#1---Average-Age-Per-County)\n", + "2. [Binning](#Binning)\n", + " * [Exercise #2 - Using the Profiler](#Exercise-#2---Using-the-Profiler)\n", + "3. [Advanced Groupby Operations](#Advanced-Groupby-Operations)\n", + " * [`.apply()`](#.apply())\n", + " * [`.transform()`](#.transform())\n", + "4. [Pivot Table](#Pivot-Table)" + ] + }, + { + "cell_type": "markdown", + "id": "f4f2e800-7d61-4370-8a96-ebef3a6d0c0a", + "metadata": {}, + "source": [ + "## Grouping ##\n", + "In data science, we often would like to split data into groups and perform further analysis on them such as: \n", + "* Aggregate based on the grouping\n", + "* Compare metrics across different groups\n", + "* Understand patterns in data across different groups\n", + "* Remove duplicates or fill missing values based on group-level information\n", + "* Create new features based on group-level statistics\n", + "* Integrate with visualization" + ] + }, + { + "cell_type": "markdown", + "id": "93d943bf-aaad-42c8-9b2c-70c8b6c0bf44", + "metadata": {}, + "source": [ + "Below we load in our dataset. " + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "89c435b1-35d5-4971-ade1-549ae77d22db", + "metadata": {}, + "outputs": [], + "source": [ + "# DO NOT CHANGE THIS CELL\n", + "%load_ext cudf.pandas\n", + "import pandas as pd\n", + "import time" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "8dc82f3c-f1cb-436b-becb-a97635ec5f6a", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
agesexcountylatlongname
00mDARLINGTON54.533638-1.524400FRANCIS
10mDARLINGTON54.426254-1.465314EDWARD
20mDARLINGTON54.555199-1.496417TEDDY
30mDARLINGTON54.547909-1.572342ANGUS
40mDARLINGTON54.477638-1.605995CHARLIE
\n", + "
" + ], + "text/plain": [ + " age sex county lat long name\n", + "0 0 m DARLINGTON 54.533638 -1.524400 FRANCIS\n", + "1 0 m DARLINGTON 54.426254 -1.465314 EDWARD\n", + "2 0 m DARLINGTON 54.555199 -1.496417 TEDDY\n", + "3 0 m DARLINGTON 54.547909 -1.572342 ANGUS\n", + "4 0 m DARLINGTON 54.477638 -1.605995 CHARLIE" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# DO NOT CHANGE THIS CELL\n", + "dtype_dict={\n", + " 'age': 'int8', \n", + " 'sex': 'category', \n", + " 'county': 'category', \n", + " 'lat': 'float32', \n", + " 'long': 'float32', \n", + " 'name': 'category'\n", + "}\n", + " \n", + "df=pd.read_csv('./data/uk_pop.csv', dtype=dtype_dict)\n", + "df.head()" + ] + }, + { + "cell_type": "markdown", + "id": "c8bbdeac-9134-443b-9b38-7a980b19decf", + "metadata": {}, + "source": [ + "## Split, Apply, and Combine ##\n", + "We use the `.groupby()` method to to group large amounts of data and compute operations on these groups. A groupby operation involves some combination of splitting the object, applying a function, and combining the results. cuDF implements record grouping in a manner comparable to Pandas, but with some notable differences. \n", + "\n", + "

\n", + "\n", + "cuDF supports a number of common `DataFrameGroupBy` computations and descriptive statistics, such as `.size()`, `.mean()`, `.count()`, `.cov()`, `.cumprod()`, `.cumsum()`, `.max()`, `.min()`, `.nunique()`. \n", + "\n", + "**Note**: More information about how `.groupby()` behaves for pandas and how it differs from cuDF can be found in the links below: \n", + "* [pandas](https://pandas.pydata.org/docs/user_guide/groupby.html)\n", + "* [cuDF](https://docs.rapids.ai/api/cudf/stable/user_guide/groupby/)\n", + "\n", + "Below we find the number of people in each county. " + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "72b646d3-b175-4a4e-804f-43afcb6910a2", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/plain": [ + "county\n", + "BARKING AND DAGENHAM 211998\n", + "BARNET 392140\n", + "BARNSLEY 245199\n", + "BATH AND NORTH EAST SOMERSET 192106\n", + "BEDFORD 171623\n", + " ... \n", + "WOKINGHAM 167979\n", + "WOLVERHAMPTON 262008\n", + "WORCESTERSHIRE 592057\n", + "WREXHAM 136126\n", + "YORK 209893\n", + "Length: 171, dtype: int64" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# DO NOT CHANGE THIS CELL\n", + "df.groupby('county').size()" + ] + }, + { + "cell_type": "markdown", + "id": "a4775215-c6d4-49b0-ba8d-4d10a7e9f434", + "metadata": {}, + "source": [ + "**Note**: The results is unsorted. We can sort the output using the `.sort_index()` or `.sort_values()` method. \n", + "\n", + "Below we count the number of people with the most and least popular names. " + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "6b22bb2b-8b86-45cb-ba26-3b845be5ac00", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/plain": [ + "name\n", + "AKASHDEEP 213\n", + "DALHA 214\n", + "BOGOMIL 215\n", + "REMMY 217\n", + "KAIYAAN 219\n", + " ... \n", + "GEORGE 459096\n", + "HARRY 459346\n", + "AMELIA 460659\n", + "OLIVIA 483789\n", + "OLIVER 576135\n", + "Length: 13212, dtype: int64" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# DO NOT CHANGE THIS CELL\n", + "df.groupby('name').size().sort_values()" + ] + }, + { + "cell_type": "markdown", + "id": "fb83bfcd-d9b2-434d-a0b3-d8d435901ece", + "metadata": {}, + "source": [ + "Below we find the approximate centers of each county using `.groupby().mean()`. When performing groupby operations, we should **only** include columns that are being used. " + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "9f0efddf-7c0a-4fbc-8c88-b0fad245b05f", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
latlong
county
BARKING AND DAGENHAM51.6210480.129583
BARNET51.812552-0.218212
BARNSLEY53.571907-1.548719
BATH AND NORTH EAST SOMERSET51.354965-2.486675
BEDFORD52.145476-0.454973
.........
WOKINGHAM51.459665-0.899371
WOLVERHAMPTON52.716848-2.127595
WORCESTERSHIRE52.057991-2.209184
WREXHAM53.000804-2.991959
YORK53.992329-1.073789
\n", + "

171 rows × 2 columns

\n", + "
" + ], + "text/plain": [ + " lat long\n", + "county \n", + "BARKING AND DAGENHAM 51.621048 0.129583\n", + "BARNET 51.812552 -0.218212\n", + "BARNSLEY 53.571907 -1.548719\n", + "BATH AND NORTH EAST SOMERSET 51.354965 -2.486675\n", + "BEDFORD 52.145476 -0.454973\n", + "... ... ...\n", + "WOKINGHAM 51.459665 -0.899371\n", + "WOLVERHAMPTON 52.716848 -2.127595\n", + "WORCESTERSHIRE 52.057991 -2.209184\n", + "WREXHAM 53.000804 -2.991959\n", + "YORK 53.992329 -1.073789\n", + "\n", + "[171 rows x 2 columns]" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
                                                                                                                   \n",
+       "                                             Total time elapsed: 0.540 seconds                                     \n",
+       "                                                                                                                   \n",
+       "                                                           Stats                                                   \n",
+       "                                                                                                                   \n",
+       "┏━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━━━━━━┓\n",
+       "┃ Line no.  Line                                                                      GPU TIME(s)  CPU TIME(s) ┃\n",
+       "┡━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━━━━━━┩\n",
+       "│ 3        │     county_center_df=df[['county', 'lat', 'long']].groupby('county')[['… │ 0.096013666 │             │\n",
+       "│          │                                                                          │             │             │\n",
+       "│ 4        │     display(county_center_df)                                            │ 0.177155495 │             │\n",
+       "│          │                                                                          │             │             │\n",
+       "└──────────┴──────────────────────────────────────────────────────────────────────────┴─────────────┴─────────────┘\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[3m \u001b[0m\n", + "\u001b[3m Total time elapsed: 0.540 seconds \u001b[0m\n", + "\u001b[3m \u001b[0m\n", + "\u001b[3m Stats \u001b[0m\n", + "\u001b[3m \u001b[0m\n", + "┏━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━━━━━━┓\n", + "┃\u001b[1m \u001b[0m\u001b[1mLine no.\u001b[0m\u001b[1m \u001b[0m┃\u001b[1m \u001b[0m\u001b[1mLine \u001b[0m\u001b[1m \u001b[0m┃\u001b[1m \u001b[0m\u001b[1mGPU TIME(s)\u001b[0m\u001b[1m \u001b[0m┃\u001b[1m \u001b[0m\u001b[1mCPU TIME(s)\u001b[0m\u001b[1m \u001b[0m┃\n", + "┡━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━━━━━━┩\n", + "│ 3 │ \u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mcounty_center_df\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m=\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mdf\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m[\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m[\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34mcounty\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m,\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34mlat\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m,\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34mlong\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m]\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m]\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m.\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mgroupby\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m(\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34mcounty\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m)\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m[\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m[\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m…\u001b[0m │ 0.096013666 │ │\n", + "│ │ \u001b[48;2;39;40;34m \u001b[0m │ │ │\n", + "│ 4 │ \u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mdisplay\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m(\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mcounty_center_df\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m)\u001b[0m\u001b[48;2;39;40;34m \u001b[0m │ 0.177155495 │ │\n", + "│ │ \u001b[48;2;39;40;34m \u001b[0m │ │ │\n", + "└──────────┴──────────────────────────────────────────────────────────────────────────┴─────────────┴─────────────┘\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "%%cudf.pandas.line_profile\n", + "# DO NOT CHANGE THIS CELL\n", + "\n", + "county_center_df=df[['county', 'lat', 'long']].groupby('county')[['lat', 'long']].mean()\n", + "display(county_center_df)" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "b99556b3-b75d-4755-9b03-141d4addb023", + "metadata": {}, + "outputs": [], + "source": [ + "# DO NOT CHANGE THIS CELL\n", + "county_center_df.columns=['lat_county_center', 'long_county_center']\n", + "county_center_df.to_csv('county_centroid.csv')" + ] + }, + { + "cell_type": "markdown", + "id": "49a498f4-b298-446c-8255-60def6d3b0ed", + "metadata": {}, + "source": [ + "### Exercise #1 - Average Age Per County ###\n", + "We would like to find the average age for each county. We will need to use both `.groupby()` and `.sort_values()`. Using the `.mean()` method on the data grouped by `county`, identify the 5 counties with the highest average age. \n", + "\n", + "**Instructions**:
\n", + "* Modify the `` only and execute the below cell find the average age for each county. " + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "0772c391-4ad6-4fcc-8754-97b575bca1c5", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "county\n", + "DORSET 46.577193\n", + "ISLE OF WIGHT 46.149253\n", + "CONWY 45.854473\n", + "POWYS 45.849366\n", + "ISLES OF SCILLY 45.467440\n", + "Name: age, dtype: float64" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df[['county', 'age']].groupby('county')['age']\\\n", + " .mean()\\\n", + " .sort_values(ascending=False)\\\n", + " .head()" + ] + }, + { + "cell_type": "raw", + "id": "cd43f771-0ccb-426d-8746-0c6ad63deff4", + "metadata": { + "scrolled": true + }, + "source": [ + "\n", + "%%cudf.pandas.line_profile\n", + "# DO NOT CHANGE THIS CELL\n", + "display(\n", + " df[['county', 'age']].groupby('county')['age']\\\n", + " .mean()\\\n", + " .sort_values(ascending=False)\\\n", + " .head()\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "0c4a3e80-c5cb-410f-a5ba-fde705594c66", + "metadata": {}, + "source": [ + "Click ... for solution. " + ] + }, + { + "cell_type": "markdown", + "id": "73fc5851-33dc-42a2-b086-6b5d7d2d65bb", + "metadata": {}, + "source": [ + "## Binning ##\n", + "When grouping continuous numerical data, it is sometimes helpful to bin numbers into discrete intervals or buckets. There are primarily two ways of binning: \n", + "* Equal-width binning: divide the range into equal-sized intervals\n", + "* Custom binning: define custom bins based on domain knowledge or specific criteria\n", + "\n", + "The `.cut()` function can be used to bin values into discrete intervals" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "ccf6ff71-40e5-46d6-9d1a-0e3d60fd0b51", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/plain": [ + "age_bucket\n", + "0 7874941\n", + "1 6630853\n", + "2 7758863\n", + "3 7691036\n", + "4 7598003\n", + "5 7712976\n", + "6 6124213\n", + "7 4530946\n", + "8 2558063\n", + "dtype: int64" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
                                                                                                                   \n",
+       "                                             Total time elapsed: 0.530 seconds                                     \n",
+       "                                                                                                                   \n",
+       "                                                           Stats                                                   \n",
+       "                                                                                                                   \n",
+       "┏━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━━━━━━┓\n",
+       "┃ Line no.  Line                                                                      GPU TIME(s)  CPU TIME(s) ┃\n",
+       "┡━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━━━━━━┩\n",
+       "│ 3        │     bins=[0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100]                    │             │             │\n",
+       "│          │                                                                          │             │             │\n",
+       "│ 5        │     df['age_bucket']=pd.cut(df['age'].values, bins=bins, right=True, in… │ 0.143198584 │             │\n",
+       "│          │                                                                          │             │             │\n",
+       "│ 6        │     display(df.groupby('age_bucket').size())                             │ 0.049102073 │ 0.006468820 │\n",
+       "│          │                                                                          │             │             │\n",
+       "└──────────┴──────────────────────────────────────────────────────────────────────────┴─────────────┴─────────────┘\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[3m \u001b[0m\n", + "\u001b[3m Total time elapsed: 0.530 seconds \u001b[0m\n", + "\u001b[3m \u001b[0m\n", + "\u001b[3m Stats \u001b[0m\n", + "\u001b[3m \u001b[0m\n", + "┏━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━━━━━━┓\n", + "┃\u001b[1m \u001b[0m\u001b[1mLine no.\u001b[0m\u001b[1m \u001b[0m┃\u001b[1m \u001b[0m\u001b[1mLine \u001b[0m\u001b[1m \u001b[0m┃\u001b[1m \u001b[0m\u001b[1mGPU TIME(s)\u001b[0m\u001b[1m \u001b[0m┃\u001b[1m \u001b[0m\u001b[1mCPU TIME(s)\u001b[0m\u001b[1m \u001b[0m┃\n", + "┡━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━━━━━━┩\n", + "│ 3 │ \u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mbins\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m=\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m[\u001b[0m\u001b[38;2;174;129;255;48;2;39;40;34m0\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m,\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;174;129;255;48;2;39;40;34m10\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m,\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;174;129;255;48;2;39;40;34m20\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m,\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;174;129;255;48;2;39;40;34m30\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m,\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;174;129;255;48;2;39;40;34m40\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m,\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;174;129;255;48;2;39;40;34m50\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m,\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;174;129;255;48;2;39;40;34m60\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m,\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;174;129;255;48;2;39;40;34m70\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m,\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;174;129;255;48;2;39;40;34m80\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m,\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;174;129;255;48;2;39;40;34m90\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m,\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;174;129;255;48;2;39;40;34m100\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m]\u001b[0m\u001b[48;2;39;40;34m \u001b[0m │ │ │\n", + "│ │ \u001b[48;2;39;40;34m \u001b[0m │ │ │\n", + "│ 5 │ \u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mdf\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m[\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34mage_bucket\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m]\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m=\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mpd\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m.\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mcut\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m(\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mdf\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m[\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34mage\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m]\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m.\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mvalues\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m,\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mbins\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m=\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mbins\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m,\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mright\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m=\u001b[0m\u001b[38;2;102;217;239;48;2;39;40;34mTrue\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m,\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34min…\u001b[0m │ 0.143198584 │ │\n", + "│ │ \u001b[48;2;39;40;34m \u001b[0m │ │ │\n", + "│ 6 │ \u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mdisplay\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m(\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mdf\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m.\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mgroupby\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m(\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34mage_bucket\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m)\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m.\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34msize\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m(\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m)\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m)\u001b[0m\u001b[48;2;39;40;34m \u001b[0m │ 0.049102073 │ 0.006468820 │\n", + "│ │ \u001b[48;2;39;40;34m \u001b[0m │ │ │\n", + "└──────────┴──────────────────────────────────────────────────────────────────────────┴─────────────┴─────────────┘\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "%%cudf.pandas.line_profile\n", + "# DO NOT CHANGE THIS CELL\n", + "\n", + "bins=[0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100]\n", + "\n", + "df['age_bucket']=pd.cut(df['age'].values, bins=bins, right=True, include_lowest=True, labels=False)\n", + "display(df.groupby('age_bucket').size())" + ] + }, + { + "cell_type": "markdown", + "id": "db8ec7e5-f6f4-4041-8187-578a15c73808", + "metadata": {}, + "source": [ + "### Exercise #2 - Using the Profiler ###\n", + "cuDF pandas will attempt to use the GPU whenever possible and fall back to CPU for certain operations. Running the code with the `cudf.pandas.line_profile` magic command generates a report showing which operations used the GPU and which used the CPU. \n", + "\n", + "**Instructions**:
\n", + "* Notice that the below cell is a very similar operation as before, except that it uses the `range()` function for the `bins` parameter. As it stands, this is not supported in cuDF. \n", + "* Execute the cell below to run the binning operation on the CPU.\n", + "* Compare the time it takes to run the similar operation above. " + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "991c437f-afee-408e-9144-006c4313f9f3", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/plain": [ + "age_bucket\n", + "0 7874941\n", + "1 6630853\n", + "2 7758863\n", + "3 7691036\n", + "4 7598003\n", + "5 7712976\n", + "6 6124213\n", + "7 4530946\n", + "8 2558063\n", + "dtype: int64" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
                                                                                                                   \n",
+       "                                             Total time elapsed: 1.720 seconds                                     \n",
+       "                                                                                                                   \n",
+       "                                                           Stats                                                   \n",
+       "                                                                                                                   \n",
+       "┏━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━━━━━━┓\n",
+       "┃ Line no.  Line                                                                      GPU TIME(s)  CPU TIME(s) ┃\n",
+       "┡━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━━━━━━┩\n",
+       "│ 3        │     df['age_bucket']=pd.cut(df['age'].values, bins=range(0, 100, 10), r… │ 0.331830367 │ 1.018851685 │\n",
+       "│          │                                                                          │             │             │\n",
+       "│ 4        │     display(df.groupby('age_bucket').size())                             │ 0.075955960 │ 0.006406995 │\n",
+       "│          │                                                                          │             │             │\n",
+       "└──────────┴──────────────────────────────────────────────────────────────────────────┴─────────────┴─────────────┘\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[3m \u001b[0m\n", + "\u001b[3m Total time elapsed: 1.720 seconds \u001b[0m\n", + "\u001b[3m \u001b[0m\n", + "\u001b[3m Stats \u001b[0m\n", + "\u001b[3m \u001b[0m\n", + "┏━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━━━━━━┓\n", + "┃\u001b[1m \u001b[0m\u001b[1mLine no.\u001b[0m\u001b[1m \u001b[0m┃\u001b[1m \u001b[0m\u001b[1mLine \u001b[0m\u001b[1m \u001b[0m┃\u001b[1m \u001b[0m\u001b[1mGPU TIME(s)\u001b[0m\u001b[1m \u001b[0m┃\u001b[1m \u001b[0m\u001b[1mCPU TIME(s)\u001b[0m\u001b[1m \u001b[0m┃\n", + "┡━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━━━━━━┩\n", + "│ 3 │ \u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mdf\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m[\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34mage_bucket\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m]\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m=\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mpd\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m.\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mcut\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m(\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mdf\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m[\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34mage\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m]\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m.\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mvalues\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m,\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mbins\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m=\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mrange\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m(\u001b[0m\u001b[38;2;174;129;255;48;2;39;40;34m0\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m,\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;174;129;255;48;2;39;40;34m100\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m,\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;174;129;255;48;2;39;40;34m10\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m)\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m,\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mr…\u001b[0m │ 0.331830367 │ 1.018851685 │\n", + "│ │ \u001b[48;2;39;40;34m \u001b[0m │ │ │\n", + "│ 4 │ \u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mdisplay\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m(\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mdf\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m.\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mgroupby\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m(\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34mage_bucket\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m)\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m.\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34msize\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m(\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m)\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m)\u001b[0m\u001b[48;2;39;40;34m \u001b[0m │ 0.075955960 │ 0.006406995 │\n", + "│ │ \u001b[48;2;39;40;34m \u001b[0m │ │ │\n", + "└──────────┴──────────────────────────────────────────────────────────────────────────┴─────────────┴─────────────┘\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "%%cudf.pandas.line_profile\n", + "# DO NOT CHANGE THIS CELL\n", + "\n", + "df['age_bucket']=pd.cut(df['age'].values, bins=range(0, 100, 10), right=True, include_lowest=True, labels=False)\n", + "display(df.groupby('age_bucket').size())" + ] + }, + { + "cell_type": "markdown", + "id": "95ef539b-6ab4-4f28-b84e-8b83f32d0c0c", + "metadata": {}, + "source": [ + "**Note**: The profiler can help us identify parts of our code that could be rewritten to be more GPU-friendly. " + ] + }, + { + "cell_type": "markdown", + "id": "7c76d718-693c-4550-8eec-3de742565a9d", + "metadata": {}, + "source": [ + "## Advanced Groupby Operations ##\n", + "We can also use function application helpers on `DataFrameGroupBy` instances: \n", + "* `DataFrameGroupby.aggregate()` / `Groupby.agg()`(alias): used when we have specific computation for different columns or more than one computation on the same column\n", + "* `DataFrameGroupby.apply()`: used when we want to perform a specific user-defined function to each group\n", + "* `DataFrameGroupby.transform()`: used when the resulting values should be broadcast across the whole group and return a same-indexed dataframe" + ] + }, + { + "cell_type": "markdown", + "id": "059bded6-fb78-4129-b723-6fa1de7ed8f0", + "metadata": {}, + "source": [ + "### `.apply()` ###\n", + "The `.apply()` method will **sequentially** apply the function group-wise and concatenate the results together. We can pass a callable function to be performed on the entire DataFrame for each group. \n", + "\n", + "Below we calculate the distance of each person from their respective county center. " + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "6906dc46-32fd-45f4-8be1-ff30d944d226", + "metadata": {}, + "outputs": [], + "source": [ + "# DO NOT CHANGE THIS CELL\n", + "\n", + "# define distance function\n", + "def distance(lat_1, long_1, lat_2, long_2): \n", + " return ((lat_2-lat_1)**2+(long_2-long_1)**2)**0.5" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "63906fdc-10a5-4239-951e-e9551ff8d60b", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/opt/conda/lib/python3.10/site-packages/cudf/core/groupby/groupby.py:1449: RuntimeWarning: GroupBy.apply() performance scales poorly with number of groups. Got 171 groups. Some functions may perform better by passing engine='jit'\n", + " warnings.warn(\n" + ] + }, + { + "data": { + "text/html": [ + "
                                                                                                                   \n",
+       "                                            Total time elapsed: 19.501 seconds                                     \n",
+       "                                                                                                                   \n",
+       "                                                           Stats                                                   \n",
+       "                                                                                                                   \n",
+       "┏━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━━━━━━┓\n",
+       "┃ Line no.  Line                                                                      GPU TIME(s)  CPU TIME(s) ┃\n",
+       "┡━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━━━━━━┩\n",
+       "│ 3        │     distance_df=df.groupby('county')[['lat', 'long']].apply(lambda x: d… │ 2.894860505 │             │\n",
+       "│          │                                                                          │             │             │\n",
+       "│ 4        │     df['R_1']=distance_df.reset_index(level=0, drop=True)                │ 0.501578658 │             │\n",
+       "│          │                                                                          │             │             │\n",
+       "└──────────┴──────────────────────────────────────────────────────────────────────────┴─────────────┴─────────────┘\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[3m \u001b[0m\n", + "\u001b[3m Total time elapsed: 19.501 seconds \u001b[0m\n", + "\u001b[3m \u001b[0m\n", + "\u001b[3m Stats \u001b[0m\n", + "\u001b[3m \u001b[0m\n", + "┏━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━━━━━━┓\n", + "┃\u001b[1m \u001b[0m\u001b[1mLine no.\u001b[0m\u001b[1m \u001b[0m┃\u001b[1m \u001b[0m\u001b[1mLine \u001b[0m\u001b[1m \u001b[0m┃\u001b[1m \u001b[0m\u001b[1mGPU TIME(s)\u001b[0m\u001b[1m \u001b[0m┃\u001b[1m \u001b[0m\u001b[1mCPU TIME(s)\u001b[0m\u001b[1m \u001b[0m┃\n", + "┡━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━━━━━━┩\n", + "│ 3 │ \u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mdistance_df\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m=\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mdf\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m.\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mgroupby\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m(\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34mcounty\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m)\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m[\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m[\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34mlat\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m,\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34mlong\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m]\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m]\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m.\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mapply\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m(\u001b[0m\u001b[38;2;102;217;239;48;2;39;40;34mlambda\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mx\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m:\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34md…\u001b[0m │ 2.894860505 │ │\n", + "│ │ \u001b[48;2;39;40;34m \u001b[0m │ │ │\n", + "│ 4 │ \u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mdf\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m[\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34mR_1\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m]\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m=\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mdistance_df\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m.\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mreset_index\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m(\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mlevel\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m=\u001b[0m\u001b[38;2;174;129;255;48;2;39;40;34m0\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m,\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mdrop\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m=\u001b[0m\u001b[38;2;102;217;239;48;2;39;40;34mTrue\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m)\u001b[0m\u001b[48;2;39;40;34m \u001b[0m │ 0.501578658 │ │\n", + "│ │ \u001b[48;2;39;40;34m \u001b[0m │ │ │\n", + "└──────────┴──────────────────────────────────────────────────────────────────────────┴─────────────┴─────────────┘\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "%%cudf.pandas.line_profile\n", + "# DO NOT CHANGE THIS CELL\n", + "\n", + "distance_df=df.groupby('county')[['lat', 'long']].apply(lambda x: distance(x['lat'], x['long'], x['lat'].mean(), x['long'].mean()))\n", + "df['R_1']=distance_df.reset_index(level=0, drop=True)" + ] + }, + { + "cell_type": "markdown", + "id": "447dcea4-3fc7-4501-9f67-17801f914875", + "metadata": {}, + "source": [ + "We can also define the function in-line. " + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "97a963d9-e61c-4af6-bd32-89a5b892b09b", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/opt/conda/lib/python3.10/site-packages/cudf/core/groupby/groupby.py:1449: RuntimeWarning: GroupBy.apply() performance scales poorly with number of groups. Got 171 groups. Some functions may perform better by passing engine='jit'\n", + " warnings.warn(\n" + ] + }, + { + "data": { + "text/html": [ + "
                                                                                                                   \n",
+       "                                            Total time elapsed: 19.424 seconds                                     \n",
+       "                                                                                                                   \n",
+       "                                                           Stats                                                   \n",
+       "                                                                                                                   \n",
+       "┏━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━━━━━━┓\n",
+       "┃ Line no.  Line                                                                      GPU TIME(s)  CPU TIME(s) ┃\n",
+       "┡━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━━━━━━┩\n",
+       "│ 3        │     df['R_2']=df.groupby('county')[['lat', 'long']].apply(lambda x: ((x… │ 3.403693881 │             │\n",
+       "│          │                                                                          │             │             │\n",
+       "└──────────┴──────────────────────────────────────────────────────────────────────────┴─────────────┴─────────────┘\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[3m \u001b[0m\n", + "\u001b[3m Total time elapsed: 19.424 seconds \u001b[0m\n", + "\u001b[3m \u001b[0m\n", + "\u001b[3m Stats \u001b[0m\n", + "\u001b[3m \u001b[0m\n", + "┏━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━━━━━━┓\n", + "┃\u001b[1m \u001b[0m\u001b[1mLine no.\u001b[0m\u001b[1m \u001b[0m┃\u001b[1m \u001b[0m\u001b[1mLine \u001b[0m\u001b[1m \u001b[0m┃\u001b[1m \u001b[0m\u001b[1mGPU TIME(s)\u001b[0m\u001b[1m \u001b[0m┃\u001b[1m \u001b[0m\u001b[1mCPU TIME(s)\u001b[0m\u001b[1m \u001b[0m┃\n", + "┡━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━━━━━━┩\n", + "│ 3 │ \u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mdf\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m[\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34mR_2\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m]\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m=\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mdf\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m.\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mgroupby\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m(\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34mcounty\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m)\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m[\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m[\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34mlat\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m,\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34mlong\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m]\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m]\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m.\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mapply\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m(\u001b[0m\u001b[38;2;102;217;239;48;2;39;40;34mlambda\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mx\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m:\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m(\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m(\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mx\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m…\u001b[0m │ 3.403693881 │ │\n", + "│ │ \u001b[48;2;39;40;34m \u001b[0m │ │ │\n", + "└──────────┴──────────────────────────────────────────────────────────────────────────┴─────────────┴─────────────┘\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "%%cudf.pandas.line_profile\n", + "# DO NOT CHANGE THIS CELL\n", + "\n", + "df['R_2']=df.groupby('county')[['lat', 'long']].apply(lambda x: ((x['lat'].mean()-x['lat'])**2+(x['long'].mean()-x['long'])**2)**0.5).reset_index(level=0, drop=True)" + ] + }, + { + "cell_type": "markdown", + "id": "e15cfd17-9b05-4de4-9c9a-fa9167f2d05f", + "metadata": {}, + "source": [ + "**Note**: This is quite slow due to the iterative nature of the `.apply()` method. " + ] + }, + { + "cell_type": "markdown", + "id": "7b7dc9d6-8c1d-4e1f-a6c3-85d036c62c26", + "metadata": {}, + "source": [ + "### `.transform()` ###\n", + "The `.transform()` method aggregates each group, and broadcasts the result to the group size, resulting in a DataFrame that is the same size and index as the input DataFrame. Underneath the hood, the `.transform()` method passes each column individually as a Series to the function. \n", + "\n", + "Below we group the DataFrame by `county` and transform the columns `lat` and `long` using `mean`. We will subtract the transformed mean from the original columns, then apply the distance formula to calculate the resulting distance. By keeping the DataFrame the same shape, we can perform cuDF operations quickly, resulting in performance gain. " + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "b1a3b520-1da5-4486-b024-ff9c4d3e1df3", + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "# DO NOT CHANGE THIS CELL\n", + "# make data types more precise\n", + "df[['lat', 'long']]=df[['lat', 'long']].astype('float64')" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "d6aa82c0-e7df-4f6a-837a-a9bdd3657787", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/html": [ + "
                                                                                                                   \n",
+       "                                             Total time elapsed: 0.873 seconds                                     \n",
+       "                                                                                                                   \n",
+       "                                                           Stats                                                   \n",
+       "                                                                                                                   \n",
+       "┏━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━━━━━━┓\n",
+       "┃ Line no.  Line                                                                      GPU TIME(s)  CPU TIME(s) ┃\n",
+       "┡━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━━━━━━┩\n",
+       "│ 3        │     c=['lat', 'long']                                                    │             │             │\n",
+       "│          │                                                                          │             │             │\n",
+       "│ 4        │     df['R_3']=((df[c] - df.groupby('county')[c].transform('mean')) ** 2 │ 0.732016207 │             │\n",
+       "│          │                                                                          │             │             │\n",
+       "└──────────┴──────────────────────────────────────────────────────────────────────────┴─────────────┴─────────────┘\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[3m \u001b[0m\n", + "\u001b[3m Total time elapsed: 0.873 seconds \u001b[0m\n", + "\u001b[3m \u001b[0m\n", + "\u001b[3m Stats \u001b[0m\n", + "\u001b[3m \u001b[0m\n", + "┏━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━━━━━━┓\n", + "┃\u001b[1m \u001b[0m\u001b[1mLine no.\u001b[0m\u001b[1m \u001b[0m┃\u001b[1m \u001b[0m\u001b[1mLine \u001b[0m\u001b[1m \u001b[0m┃\u001b[1m \u001b[0m\u001b[1mGPU TIME(s)\u001b[0m\u001b[1m \u001b[0m┃\u001b[1m \u001b[0m\u001b[1mCPU TIME(s)\u001b[0m\u001b[1m \u001b[0m┃\n", + "┡━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━━━━━━┩\n", + "│ 3 │ \u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mc\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m=\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m[\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34mlat\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m,\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34mlong\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m]\u001b[0m\u001b[48;2;39;40;34m \u001b[0m │ │ │\n", + "│ │ \u001b[48;2;39;40;34m \u001b[0m │ │ │\n", + "│ 4 │ \u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mdf\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m[\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34mR_3\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m]\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m=\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m(\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m(\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mdf\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m[\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mc\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m]\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m-\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mdf\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m.\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mgroupby\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m(\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34mcounty\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m)\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m[\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mc\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m]\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m.\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mtransform\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m(\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34mmean\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m)\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m)\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m*\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m*\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;174;129;255;48;2;39;40;34m2\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m…\u001b[0m │ 0.732016207 │ │\n", + "│ │ \u001b[48;2;39;40;34m \u001b[0m │ │ │\n", + "└──────────┴──────────────────────────────────────────────────────────────────────────┴─────────────┴─────────────┘\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "%%cudf.pandas.line_profile\n", + "# DO NOT CHANGE THIS CELL\n", + "\n", + "c=['lat', 'long']\n", + "df['R_3']=((df[c] - df.groupby('county')[c].transform('mean')) ** 2).sum(axis=1) ** 0.5" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "05a40a5f-138b-4535-a86b-d07271d8fa1c", + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "df.head()" + ] + }, + { + "cell_type": "markdown", + "id": "858b309c-4533-4798-a22c-fe6858257cbc", + "metadata": {}, + "source": [ + "Although the `.apply()` method is more flexible and can handle complex operations, it is generally slower. On the other hand, the `.transform()` method can be much faster. When we design the procedures to use vector operations, we will realize significant performance benefits. \n", + "\n", + "**Note**: `Groupby.apply()` doesn't scale well with the number of groups, therefore this performance difference will be more pronounced with higher number of groups. " + ] + }, + { + "cell_type": "markdown", + "id": "7bc715eb-e357-4a92-8778-a144007a9697", + "metadata": {}, + "source": [ + "## Pivot Table ##\n", + "Pivot tables allow us to summarize and aggregate large datasets into a more manageable format for analysis. When using `DataFrame.pivot_table()`, we provide the `index`, `columns`, and `values` arguments, as well as `aggfunc`. This will group the data based on `index` and `columns`, and perform the aggregation on `values`. We can apply multiple aggregation functions, which is generally faster and more memory-efficient than manual grouping and aggregation for large datasets. \n", + "\n", + "Below we create a pivot table that counts the number of each sex in each county. Furthermore, we derive the percentage of the total for each county. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a6c8cd25-9892-458e-a9a0-8af10647f9c7", + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "%%cudf.pandas.line_profile\n", + "# DO NOT CHANGE THIS CELL\n", + "\n", + "pvt_tbl=df[['county', 'sex', 'name']].pivot_table(index=['county'], columns=['sex'], values='name', aggfunc='count')\n", + "pvt_tbl=pvt_tbl.apply(lambda x: x/sum(x), axis=1)\n", + "display(pvt_tbl)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9ba52ec8-18bd-4f30-a3dd-ccbdface4bb4", + "metadata": {}, + "outputs": [], + "source": [ + "import IPython\n", + "app = IPython.Application.instance()\n", + "app.kernel.do_shutdown(True)" + ] + }, + { + "cell_type": "markdown", + "id": "9e99bf3f-be76-4ebf-aed3-624d4e8695ac", + "metadata": {}, + "source": [ + "**Well Done!** Let's move to the [next notebook](1-06_data_visualization.ipynb). " + ] + }, + { + "cell_type": "markdown", + "id": "81e47f0a-547e-4714-878d-34eb9b75c835", + "metadata": {}, + "source": [ + "" + ] + } + ], + "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.15" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/ds/25-1/2/1-06_data_visualization.ipynb b/ds/25-1/2/1-06_data_visualization.ipynb new file mode 100644 index 0000000..bd66b87 --- /dev/null +++ b/ds/25-1/2/1-06_data_visualization.ipynb @@ -0,0 +1,3435 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "b53a7b12-538d-4459-b82a-a35c8c417849", + "metadata": {}, + "source": [ + "" + ] + }, + { + "cell_type": "markdown", + "id": "ae497b71-bc43-471e-8970-88a1878e7cf9", + "metadata": {}, + "source": [ + "# Fundamentals of Accelerated Data Science # " + ] + }, + { + "cell_type": "markdown", + "id": "a149b6d1-1880-4a5d-9d71-f963d3097aa4", + "metadata": {}, + "source": [ + "## 06 - Data Visualization ##\n", + "\n", + "**Table of Contents**\n", + "
\n", + "This notebook demonstrates the basics of data visualization for large datasets. This notebook covers the below sections: \n", + "1. [Data Visualization](#Data-Visualization)\n", + "2. [Bar Chart](#Bar-Chart)\n", + " * [Histogram](#Histogram)\n", + " * [Exercise #1 - Bar Chart](#Exercise-#1---Bar-Chart)\n", + "3. [Scatter Plot](#Scatter-Plot)\n", + "4. [Line Chart](#Line-Chart)\n", + "5. [Datashader](#Datashader)\n", + " * [Datashader Accelerated by GPU](#Datashader-Accelerated-by-GPU)\n", + "6. [Interactive Visualization](#Interactive-Visualization)\n", + " * [cuxfilter and Dashboard](#cuxfilter-and-Dashboard)\n", + "6. [Other Libraries](#Other-Libraries)" + ] + }, + { + "cell_type": "markdown", + "id": "39f0f08f-92a2-4bfc-b8bc-5904aa70b5fc", + "metadata": {}, + "source": [ + "## Data Visualization ##\n", + "Data visualization is an important part of data science for several reasons: \n", + "* **Data exploration**: enables data scientists to explore data and quickly identify patterns, trends, and outliers that may not be apparent when looking at raw data in tabular format\n", + "* **Interpretation**: transforms large and complex datasets into more digestible visual formats, making it easier to comprehend vast amounts of information\n", + "* **Communication**: helps data scientists communicate complex insights to stakeholders in an easy-to-understand visual format, making data more accessible to non-technical audiences\n", + "\n", + "Below is the simple dashboard we will create in this notebook: \n", + "\n", + "

" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "3300e580-f39d-4147-8ad8-dfbf611ad323", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
agesexcountylatlongname
00mDARLINGTON54.533638-1.524400FRANCIS
10mDARLINGTON54.426254-1.465314EDWARD
20mDARLINGTON54.555199-1.496417TEDDY
30mDARLINGTON54.547909-1.572342ANGUS
40mDARLINGTON54.477638-1.605995CHARLIE
\n", + "
" + ], + "text/plain": [ + " age sex county lat long name\n", + "0 0 m DARLINGTON 54.533638 -1.524400 FRANCIS\n", + "1 0 m DARLINGTON 54.426254 -1.465314 EDWARD\n", + "2 0 m DARLINGTON 54.555199 -1.496417 TEDDY\n", + "3 0 m DARLINGTON 54.547909 -1.572342 ANGUS\n", + "4 0 m DARLINGTON 54.477638 -1.605995 CHARLIE" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%load_ext cudf.pandas\n", + "# DO NOT CHANGE THIS CELL\n", + "import pandas as pd\n", + "\n", + "dtype_dict={\n", + " 'age': 'int8', \n", + " 'sex': 'object', \n", + " 'county': 'object', \n", + " 'lat': 'float32', \n", + " 'long': 'float32', \n", + " 'name': 'object'\n", + "}\n", + " \n", + "df=pd.read_csv('./data/uk_pop.csv', dtype=dtype_dict)\n", + "df.head()" + ] + }, + { + "cell_type": "markdown", + "id": "db58461b-5877-4768-8586-a46765381b6b", + "metadata": {}, + "source": [ + "## Bar Chart ##\n", + "Bar charts are used to show and compare categorical data. It represent numercial values with rectangular bars where the length or height of each bar corresponds to the value it represents. \n", + "\n", + "Below we show the top 5 counties with the most people. " + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "a0acbf39-b10c-4998-96d7-dde142d844e1", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAiMAAAIKCAYAAADxiU9PAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMywgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/GU6VOAAAACXBIWXMAAA9hAAAPYQGoP6dpAAA7H0lEQVR4nO3df3zP9eL///trG5tfm5AxDfNjmjPJGRUiIzSlnEOcVPPzRPN7qYzy6xJOP/isjkjZjBJyEskOVmrLz6I5CaHIlM0a2Q8/xrbX9w9vr2+zTXvJ6/XY9rpdL5fX5dLz1173V6/kvsfz8Xw+LVar1SoAAABD3EwHAAAAro0yAgAAjKKMAAAAoygjAADAKMoIAAAwijICAACMoowAAACjKCMAAMAoyggAADCKMgIAAIwqV2UkKSlJvXv3lp+fnywWi9auXWv3z7BarXrttdcUGBgoT09P+fv7a/bs2Tc/LAAAKBUP0wHsce7cObVu3VpDhgxR3759b+hnjBs3Tps3b9Zrr72mVq1aKTMzUxkZGTc5KQAAKC1LeX1QnsVi0UcffaQ+ffrY1l26dEkvvPCCli9frrNnzyo4OFgvv/yyunTpIkk6ePCg7rjjDn333Xdq0aKFmeAAAKCQcnWa5o8MGTJE27Zt08qVK/Xtt9/q0Ucf1QMPPKAjR45IktavX68mTZrok08+UUBAgBo3bqzhw4frzJkzhpMDAOC6KkwZ+fHHH7VixQqtXr1anTp1UtOmTTVx4kTde++9WrJkiSTp6NGjOn78uFavXq1ly5YpLi5Oe/bsUb9+/QynBwDAdZWrOSPX880338hqtSowMLDQ+tzcXNWuXVuSVFBQoNzcXC1btsy2X0xMjEJCQnTo0CFO3QAAYECFKSMFBQVyd3fXnj175O7uXmhb9erVJUn169eXh4dHocISFBQkSUpJSaGMAABgQIUpI23atFF+fr7S09PVqVOnYvfp2LGj8vLy9OOPP6pp06aSpMOHD0uSGjVq5LSsAADg/1eurqbJycnRDz/8IOlK+Zg3b55CQ0NVq1YtNWzYUE888YS2bdumuXPnqk2bNsrIyNCWLVvUqlUr9erVSwUFBWrXrp2qV6+u6OhoFRQUaNSoUfL29tbmzZsNfzoAAFxTuSojX3zxhUJDQ4usHzRokOLi4nT58mW99NJLWrZsmX755RfVrl1b7du314wZM9SqVStJ0smTJzVmzBht3rxZ1apVU1hYmObOnatatWo5++MAAACVszICAAAqngpzaS8AACifysUE1oKCAp08eVI1atSQxWIxHQcAAJSC1WpVdna2/Pz85OZW8vhHuSgjJ0+elL+/v+kYAADgBpw4cUK33XZbidvLRRmpUaOGpCsfxtvb23AaAABQGllZWfL397f9PV6SclFGrp6a8fb2powAAFDO/NEUCyawAgAAoygjAADAKMoIAAAwijICAACMsruMJCUlqXfv3vLz85PFYtHatWv/8Jjc3FxNmTJFjRo1kqenp5o2barY2NgbyQsAACoYu6+mOXfunFq3bq0hQ4aob9++pTqmf//+OnXqlGJiYtSsWTOlp6crLy/P7rAAAKDisbuMhIWFKSwsrNT7b9y4UYmJiTp69KjtYXSNGze2920BAEAF5fA5Ix9//LHatm2rV155RQ0aNFBgYKAmTpyoCxculHhMbm6usrKyCr0AAEDF5PCbnh09elRbt26Vl5eXPvroI2VkZCgiIkJnzpwpcd7InDlzNGPGDEdHAwAAZYDDR0YKCgpksVi0fPly3XXXXerVq5fmzZunuLi4EkdHoqKilJmZaXudOHHC0TEBAIAhDh8ZqV+/vho0aCAfHx/buqCgIFmtVv38889q3rx5kWM8PT3l6enp6GgAAKAMcPjISMeOHXXy5Enl5OTY1h0+fFhubm7XfYIfAABwDXaXkZycHO3du1d79+6VJB07dkx79+5VSkqKpCunWMLDw237Dxw4ULVr19aQIUN04MABJSUl6dlnn9XQoUNVpUqVm/MpAABAuWV3Gdm9e7fatGmjNm3aSJIiIyPVpk0bTZ06VZKUmppqKyaSVL16dSUkJOjs2bNq27atHn/8cfXu3VtvvPHGTfoIAACgPLNYrVar6RB/JCsrSz4+PsrMzJS3t7fpOAAAoBRK+/e3wyewlieNJ20wHeFP++lfD5qOAACAXXhQHgAAMIoyAgAAjKKMAAAAoygjAADAKMoIAAAwijICAACMoowAAACjKCMAAMAoyggAADCKMgIAAIyijAAAAKMoIwAAwCjKCAAAMIoyAgAAjKKMAAAAoygjAADAKMoIAAAwijICAACMoowAAACjPEwHAIrTeNIG0xH+tJ/+9aDpCABQLjAyAgAAjKKMAAAAoygjAADAKMoIAAAwijIC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REREsdvi4uKKrBszZozGjBlzI28FAAAqOJ5NAwAAjKKMAAAAoygjAADAKMoIAAAwijICAACMoowAAACjKCMAAMAoyggAADCKMgIAAIyijAAAAKMoIwAAwCjKCAAAMIoyAgAAjKKMAAAAoygjAADAKMoIAAAwijICAACMoowAAACjKCMAAMAoyggAADCKMgIAAIyijAAAAKMoIwAAwCjKCAAAMIoyAgAAjKKMAAAAoygjAADAKMoIAAAwijICAACMoowAAACjKCMAAMAoyggAADCKMgIAAIyijAAAAKNuqIwsWLBAAQEB8vLyUkhIiL788svr7p+bm6spU6aoUaNG8vT0VNOmTRUbG3tDgQEAQMXiYe8Bq1at0vjx47VgwQJ17NhRixYtUlhYmA4cOKCGDRsWe0z//v116tQpxcTEqFmzZkpPT1deXt6fDg8AAMo/u8vIvHnzNGzYMA0fPlySFB0drU2bNmnhwoWaM2dOkf03btyoxMREHT16VLVq1ZIkNW7c+LrvkZubq9zcXNtyVlaWvTEBAEA5YddpmkuXLmnPnj3q0aNHofU9evTQ9u3biz3m448/Vtu2bfXKK6+oQYMGCgwM1MSJE3XhwoUS32fOnDny8fGxvfz9/e2JCQAAyhG7RkYyMjKUn58vX1/fQut9fX2VlpZW7DFHjx7V1q1b5eXlpY8++kgZGRmKiIjQmTNnSpw3EhUVpcjISNtyVlYWhQQAgArK7tM0kmSxWAotW63WIuuuKigokMVi0fLly+Xj4yPpyqmefv366c0331SVKlWKHOPp6SlPT88biQYAAMoZu07T1KlTR+7u7kVGQdLT04uMllxVv359NWjQwFZEJCkoKEhWq1U///zzDUQGAAAViV1lpHLlygoJCVFCQkKh9QkJCerQoUOxx3Ts2FEnT55UTk6Obd3hw4fl5uam22677QYiAwCAisTu+4xERkZq8eLFio2N1cGDBzVhwgSlpKRo5MiRkq7M9wgPD7ftP3DgQNWuXVtDhgzRgQMHlJSUpGeffVZDhw4t9hQNAABwLXbPGRkwYIBOnz6tmTNnKjU1VcHBwYqPj1ejRo0kSampqUpJSbHtX716dSUkJGjMmDFq27atateurf79++ull166eZ8CAACUWzc0gTUiIkIRERHFbouLiyuy7vbbby9yagcAAEDi2TQAAMAwyggAADCK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Fb3zzjvq37+/goODtXr1aj300EOGU7qG7du368EHH1RmZqYsFovatm2rJUuWqE+fPiooKNALL7ygoUOHmo6J/1Phr3SywmlOnz5tHT9+vNXT09PauXNn644dO0xHcgkWi8V66tQp23L16tWtP/74o205LS3NarFYTERzOffcc4917Nix1uzsbOvcuXOtFovFGhgYaE1MTDQdzeV07drVOmDAAOu+ffusEyZMsFosFmtAQIB16dKl1oKCAtPxXEpp/h/l5uZmIprTMGfECS5cuKB58+bp1VdfVePGjTV79mz16tXLdCyXce2VTTVq1ND//vc/lzofW1bUrFlTX331lQIDA5WXlycvLy+tX79eYWFhpqO5nDp16igxMVF/+ctfdP78edWoUUMrV67Uo48+ajqay3Fzc9OWLVtUq1YtSVKHDh30wQcf6LbbbpN05XRa9+7dK/T/ozhN40D5+fl65513NGPGDHl5eenf//63nnjiiYo/3AaUICsry3aFmYeHh6pUqaLAwECzoVzUmTNnbKcuq1atqqpVq6pNmzaGU7mua+/YffV05e/v2F2RUUYc5IMPPtALL7ygzMxMTZ48WU8//bTtZkJwLu76Wbb8/plNVqtVhw4d0rlz5wrtwzObHM9isSg7O1teXl62v+zOnz9f5IFs3t7ehhK6jmPHjpmOYBynaRzEzc1NVapU0WOPPXbdP8zz5s1zYirX5ObmJh8fH1sBOXv2rLy9vQvd9TMrK6tCD4GWFW5ubrbf9K7FM5uc6+p3cdW1v33zXcCZGBlxkM6dO//hpbv8du4cS5YsMR0B/4ffAMuOzz//3HQElNKaNWs0ffp0ffvtt6ajOAwjIwAAGPbOO+9o8+bNqlSpksaNG6e7775bW7Zs0TPPPKNDhw7pySef1KJFi0zHdBjKCFzSxYsXtWrVKp07d07du3dX8+bNTUdyCaX9zY45I4537dyQkjBnxPFee+01TZ48WXfccYcOHjwoSZoyZYrmzZunMWPGaNSoUapTp47hlI5FGXGQli1bauvWrbZLtZ566inNmjXLNns9PT1djRs31vnz503GdAnPPvusLl26pNdff12SdOnSJd19993av3+/qlatqry8PCUkJPCgPCe43pyRq5in4BzXzhm5FnNGnCcoKEjPPvushg4dqi+++EJdu3ZV165d9Z///Mdlnm9GGXGQ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8kvt3LlT7du3t91fQbpyD5g2bdoYTOg6wsPDNWrUKO3fv19btmzR7bffrpCQENv27du3Kzg42GBC1/H//t//u+72UaNGafLkyYqPj3dSIuejjKDCs1gsRYZAue21GSNGjJCHh4c++eQTde7cuch9R06ePFnoVAEc5/nnn9f58+e1Zs0a1atXT6tXry60fdu2bfrHP/5hKB1+b8SIEerZs6fpGA7F1TSo8Nzc3BQWFmabq7B+/Xp17drVdg1/bm6uNm7cqPz8fJMxgTInLy/PdqdimHPkyBHddddd+u2330xHcRj+K0OFV5pr+MPDw50Vx6UVFBRo7ty5Wrt2rS5fvqz7779fU6dOlZeXl+lo+J0DBw4oJiZG7733nk6dOmU6jsvbvHmzAgMDTcdwKMoIKjxue112vPzyy3rhhRfUrVs3ValSRfPmzVNGRobefvtt09FcXk5OjlauXKmYmBh9/fXXuueeezRp0iTTsVzCxx9/XOz6zMxMff3114qJiVFcXJxzQzkZp2kAOE2LFi00btw4RURESLpyo60+ffrowoULzOMxZOvWrVq8eLE+/PBDBQQE6MCBA0pMTFTHjh1NR3MZJd12v0aNGrr99ts1ceJEPfroo05O5VyMjABwmuPHj+uhhx6yLffs2VNWq1UnT57kAZJO9sorryg2NlY5OTl67LHHtHXrVrVu3VqVKlXi+UBOVlBQYDqCcdyBFYDTXLp0SVWqVLEtWywWVa5cmacmGzB58mT17dtXx48f16uvvqrWrVubjuSydu3apf/+97+F1i1btkwBAQGqW7eunnrqqQr/Z4SREQBO9eKLL6pq1aq25UuXLmnWrFmFbrA1b948E9FcysyZMxUXF6d3331Xjz32mJ588knuK2LItGnTFBoaqrCwMEnSvn37NGzYMA0ePFhBQUF69dVX5efnp+nTp5sN6kDMGQHgNF26dPnDuSEWi0VbtmxxUiIkJiYqNjZWH374oZo2bar9+/czZ8TJ6tevr/Xr19ueFTRlyhQlJiZq69atkq48UHLatGk6cOCAyZgORRkBACg7O1vLly/XkiVLtGfPHt11113q16+fIiMjTUer8Ly8vHTkyBHb82fuvfdePfDAA3rhhRckST/99JNatWql7OxskzEdijkjAADVqFFDI0eO1K5du5ScnKy77rpL//rXv0zHcgm+vr46duyYpCunLb/55hu1b9/etj07O1uVKlUyFc8pGBkB4DQzZ84s1X5Tp051cBKUxuXLlyv8X4JlwYgRI7Rv3z69/PLLWrt2rZYuXaqTJ0+qcuXKkqTly5crOjpaX3/9teGkjkMZAeA0bm5u8vPzU926dUt8erLFYtE333zj5GSuZ8uWLRo9erR27twpb2/vQtsyMzPVoUMHvfXWW+rUqZOhhK7j119/1d///ndt27ZN1atX19KlS/W3v/3Ntr1bt2665557NGvWLIMpHYsyAsBpevXqpc8//1w9e/bU0KFD9eCDD8rd3d10LJf08MMPKzQ0VBMmTCh2+xtvvKHPP/9cH330kZOTua7MzExVr169yJ+JM2fOqHr16raRkoqIMgLAqVJTUxUXF6e4uDhlZWUpPDxcQ4cOVYsWLUxHcymNGjXSxo0bFRQUVOz277//Xj169FBKSoqTk8EVMYEVgFPVr19fUVFROnTokFatWqX09HS1a9dOHTt21IULF0zHcxmnTp267nwQDw8P/frrr05MBFdGGQFgTLt27RQaGqqgoCAlJyfr8uXLpiO5jAYNGmjfvn0lbv/2229Vv359JyaCK6OMAHC6HTt26J///Kfq1aunf//73xo0aJBOnjxZZCIlHKdXr16aOnWqLl68WGTbhQsXNG3atELPEQIciTkjAJzmlVde0ZIlS3T69Gk9/vjjGjp0qFq1amU6lks6deqU/vrXv8rd3V2jR49WixYtZLFYdPDgQb355pvKz8/XN998I19fX9NR4QIoIwCcxs3NTQ0bNtRDDz103SsDeDaNcxw/flxPP/20Nm3aZLvU2mKxqGfPnlqwYIEaN25sNiBcBmUEgNOU5tk0kvT55587IQ2u+u233/TDDz/IarWqefPmuuWWW0xHgouhjAAAAKM8TAcAgKv27dunmJgYRUdHm45S4Q0dOvQP97FYLIqJiXFCGrg6yggAo7KysrRixQrFxMRo9+7duuOOO0xHcgm//fZbidvy8/P16aefKjc3lzICp6CMADAiMTFRMTEx+vDDD3Xx4kU9++yzev/999WsWTPT0VxCSbd5X7dunSZPnixPT08eWAin4T4jAJwmNTVVs2fPVrNmzfSPf/xDderUUWJiotzc3BQeHk4RMWjbtm269957NXDgQD300EM6evSoJk2aZDoWXAQjIwCcJiAgQI8++qjefPNN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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# DO NOT CHANGE THIS CELL\n", + "df.groupby('county').size().sort_values(ascending=False).head().plot(kind='bar')" + ] + }, + { + "cell_type": "markdown", + "id": "8b6d94bc-7006-4e73-accb-2649d7dec596", + "metadata": {}, + "source": [ + "### Histogram ###\n", + "Bar charts can also be used to show the distribution of data points across different subgroups. This is referred to as a historgram, which is done by counting the number of occurrences (frequency distribution) of each unique value in a dataset. It is used to visualize the shape, center, and spread of a dataset. " + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "b804a51e-63b7-4389-8dd5-3beea5a5950b", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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AACwhrAAAADWEBYAAMAawgIAAFhDWAAAAGsICwAAYA1hAQAArCEsAACANYQFAACwhrAAAADWEBYAAMAawgIAAFhDWAAAAGsICwAAYA1hAQAArCEsAACANYQFAACwhrAAAADWEBYAAMAawgIAAFhDWAAAAGsICwAAYA1hAQAArCEsAACANYQFAACwJqiwuPfee+VwOAKWZs2ahWs2AAAQZZzBHnDGGWdo6dKl/vWYmBirAwEAgOgVdFg4nU7uUgAAgOMK+hmLkpISpaamKj09XYMHD9bOnTt/cX+v16vy8vKABQAA1E5BhcVZZ52lmTNn6q233tLUqVO1b98+nXPOOfrqq68qPSY/P18JCQn+xe12V3loAABQMwUVFtnZ2br88svVsWNHXXDBBVqwYIEkacaMGZUek5eXp7KyMv/i8XiqNjEAAKixgn7G4qcaNGigjh07qqSkpNJ9XC6XXC5XVU4DAACiRJV+j4XX69WWLVuUkpJiax4AABDFggqLW2+9Ve+++64+/fRTrVq1SldccYXKy8s1dOjQcM0HAACiSFBvhXz++ecaMmSIDhw4oCZNmujss89WYWGhWrZsGa75AABAFAkqLObMmROuOQAAQC3Ad4UAAABrCAsAAGANYQEAAKwhLAAAgDWEBQAAsIawAAAA1hAWAADAGsICAABYQ1gAAABrCAsAAGANYQEAAKwhLAAAgDWEBQAAsIawAAAA1gT1tekAAERK2u0LqvV8uyb1q9bz1RbcsQAAANYQFgAAwBrCAgAAWENYAAAAawgLAABgDWEBAACsISwAAIA1hAUAALCGsAAAANYQFgAAwBrCAgAAWENYAAAAawgLAABgDWEBAACsISwAAIA1hAUAALCGsAAAANYQFgAAwJoqhUV+fr4cDofGjBljaRwAABDNQg6L1atX65lnnlFGRobNeQAAQBQLKSwOHTqkq666SlOnTtVJJ51keyYAABClQgqL3Nxc9evXTxdccIHteQAAQBRzBnvAnDlztGbNGq1evfqE9vd6vfJ6vf718vLyYE8JAACiRFB3LDwej0aPHq1Zs2YpNjb2hI7Jz89XQkKCf3G73SENCgAAar6gwqK4uFj79+9X586d5XQ65XQ69e677+rxxx+X0+lURUXFMcfk5eWprKzMv3g8HmvDAwCAmiWot0LOP/98bdiwIeC1YcOGqW3btho3bpxiYmKOOcblcsnlclVtSgAAEBWCCotGjRqpQ4cOAa81aNBASUlJx7wOAAB+e/jNmwAAwJqgPxXyc8uXL7cwBgAAqA24YwEAAKwhLAAAgDWEBQAAsIawAAAA1hAWAADAGsICAABYQ1gAAABrCAsAAGANYQEAAKwhLAAAgDWEBQAAsIawAAAA1hAWAADAGsICAABYQ1gAAABrCAsAAGANYQEAAKwhLAAAgDWEBQAAsIawAAAA1hAWAADAGsICAABYQ1gAAABrCAsAAGANYQEAAKwhLAAAgDWEBQAAsIawAAAA1hAWAADAGsICAABYQ1gAAABrCAsAAGANYQEAAKwhLAAAgDVBhUVBQYEyMjIUHx+v+Ph4de3aVQsXLgzXbAAAIMoEFRbNmzfXpEmTVFRUpKKiIp133nm69NJLtWnTpnDNBwAAoogzmJ1zcnIC1h988EEVFBSosLBQZ5xxhtXBAABA9AkqLH6qoqJCL730kg4fPqyuXbvanAkAAESpoMNiw4YN6tq1q77//ns1bNhQ8+bNU/v27Svd3+v1yuv1+tfLy8tDmxQAANR4QX8q5PTTT9e6detUWFioESNGaOjQodq8eXOl++fn5yshIcG/uN3uKg0MAABqrqDDol69emrdurWysrKUn5+vM888U4899lil++fl5amsrMy/eDyeKg0MAABqrpCfsfiRMSbgrY6fc7lccrlcVT0NAACIAkGFxR133KHs7Gy53W4dPHhQc+bM0fLly7Vo0aJwzQcAAKJIUGHxxRdf6JprrtHevXuVkJCgjIwMLVq0SH369AnXfAAAIIoEFRbPPvtsuOYAAAC1AN8VAgAArCEsAACANYQFAACwhrAAAADWEBYAAMAawgIAAFhDWAAAAGsICwAAYA1hAQAArCEsAACANYQFAACwhrAAAADWEBYAAMCaoL7dFAAA2Jd2+4JqPd+uSf3C9rO5YwEAAKwhLAAAgDWEBQAAsIawAAAA1hAWAADAGsICAABYQ1gAAABrCAsAAGANYQEAAKwhLAAAgDWEBQAAsIawAAAA1hAWAADAGsICAABYQ1gAAABrCAsAAGANYQEAAKwhLAAAgDWEBQAAsIawAAAA1gQVFvn5+erSpYsaNWqkpk2b6rLLLtPWrVvDNRsAAIgyQYXFu+++q9zcXBUWFmrJkiU6evSo+vbtq8OHD4drPgAAEEWcwey8aNGigPVp06apadOmKi4u1rnnnmt1MAAAEH2CCoufKysrkyQlJiZWuo/X65XX6/Wvl5eXV+WUAACgBgv54U1jjMaOHavu3burQ4cOle6Xn5+vhIQE/+J2u0M9JQAAqOFCDotRo0Zp/fr1mj179i/ul5eXp7KyMv/i8XhCPSUAAKjhQnor5KabbtJrr72m9957T82bN//FfV0ul1wuV0jDAQCA6BJUWBhjdNNNN2nevHlavny50tPTwzUXAACIQkGFRW5url544QW9+uqratSokfbt2ydJSkhIUFxcXFgGBAAA0SOoZywKCgpUVlamXr16KSUlxb/MnTs3XPMBAIAoEvRbIQAAAJXhu0IAAIA1hAUAALCGsAAAANYQFgAAwBrCAgAAWENYAAAAawgLAABgDWEBAACsISwAAIA1hAUAALCGsAAAANYQFgAAwBrCAgAAWENYAAAAawgLAABgDWEBAACsISwAAIA1hAUAALCGsAAAANYQFgAAwBrCAgAAWENYAAAAawgLAABgDWEBAACsISwAAIA1hAUAALCGsAAAANYQFgAAwBrCAgAAWENYAAAAawgLAABgDWEBAACsISwAAIA1hAUAALAm6LB47733lJOTo9TUVDkcDs2fPz8MYwEAgGgUdFgcPnxYZ555pp588slwzAMAAKKYM9gDsrOzlZ2dHY5ZAABAlAs6LILl9Xrl9Xr96+Xl5eE+JQAAiJCwP7yZn5+vhIQE/+J2u8N9SgAAECFhD4u8vDyVlZX5F4/HE+5TAgCACAn7WyEul0sulyvcpwEAADUAv8cCAABYE/Qdi0OHDmn79u3+9U8//VTr1q1TYmKiWrRoYXU4AAAQXYIOi6KiIvXu3du/PnbsWEnS0KFDNX36dGuDAQCA6BN0WPTq1UvGmHDMAgAAohzPWAAAAGsICwAAYA1hAQAArCEsAACANYQFAACwhrAAAADWEBYAAMAawgIAAFhDWAAAAGsICwAAYA1hAQAArCEsAACANYQFAACwhrAAAADWEBYAAMAawgIAAFhDWAAAAGsICwAAYA1hAQAArCEsAACANYQFAACwhrAAAADWEBYAAMAawgIAAFhDWAAAAGsICwAAYA1hAQAArCEsAACANYQFAACwhrAAAADWEBYAAMAawgIAAFhDWAAAAGtCCot//vOfSk9PV2xsrDp37qz333/f9lwAACAKBR0Wc+fO1ZgxY3TnnXdq7dq16tGjh7Kzs1VaWhqO+QAAQBQJOiymTJmi66+/XjfccIPatWunRx99VG63WwUFBeGYDwAARJGgwuLIkSMqLi5W3759A17v27evVq5caXUwAAAQfZzB7HzgwAFVVFQoOTk54PXk5GTt27fvuMd4vV55vV7/ellZmSSpvLw8qEF93m+D2r+qgp2vKmrztUnVe31cmz1cmx21+dok/l1pSzRc24/HGGN+eUcThN27dxtJZuXKlQGvT5gwwZx++unHPWb8+PFGEgsLCwsLC0stWDwezy+2QlB3LE4++WTFxMQcc3di//79x9zF+FFeXp7Gjh3rX/f5fPr666+VlJQkh8MRzOmDVl5eLrfbLY/Ho/j4+LCeq7pxbdGpNl+bVLuvj2uLTlybPcYYHTx4UKmpqb+4X1BhUa9ePXXu3FlLlizRgAED/K8vWbJEl1566XGPcblccrlcAa81btw4mNNWWXx8fK37P9SPuLboVJuvTard18e1RSeuzY6EhIRf3SeosJCksWPH6pprrlFWVpa6du2qZ555RqWlpRo+fHhIQwIAgNoj6LC48sor9dVXX+n+++/X3r171aFDB7355ptq2bJlOOYDAABRJOiwkKSRI0dq5MiRtmexzuVyafz48ce8FVMbcG3RqTZfm1S7r49ri05cW/VzmF/93AgAAMCJ4UvIAACANYQFAACwhrAAAADWEBYArOGRLQAhfSqkpvr8889VUFCglStXat++fXI4HEpOTtY555yj4cOHy+12R3pEoFZzuVz6+OOP1a5du0iPAiBCas2nQj744ANlZ2fL7Xarb9++Sk5OljFG+/fv15IlS+TxeLRw4UJ169Yt0qOGhcfj0fjx4/Xcc89FepSgfffddyouLlZiYqLat28fsO3777/Xiy++qGuvvTZC01Xdli1bVFhYqK5du6pt27b65JNP9Nhjj8nr9erqq6/WeeedF+kRg/bTX9P/U4899piuvvpqJSUlSZKmTJlSnWOFzTfffKMZM2aopKREKSkpGjp0aNT+RWXt2rVq3Lix0tPTJUmzZs1SQUGBSktL1bJlS40aNUqDBw+O8JShuemmmzRo0CD16NEj0qOExRNPPKGioiL169dPgwYN0r///W/l5+fL5/PpD3/4g+6//345nTXgfkEwX0JWk2VlZZkxY8ZUun3MmDEmKyurGieqXuvWrTN16tSJ9BhB27p1q2nZsqVxOBymTp06pmfPnmbPnj3+7fv27YvK6/rRwoULTb169UxiYqKJjY01CxcuNE2aNDEXXHCBOf/8843T6TTLli2L9JhBczgcplOnTqZXr14Bi8PhMF26dDG9evUyvXv3jvSYIUtJSTEHDhwwxhizc+dO06xZM9OsWTPTp08f07x5c5OQkGC2bNkS4SlDk5mZad5++21jjDFTp041cXFx5uabbzYFBQVmzJgxpmHDhubZZ5+N8JSh+fHfI6eddpqZNGmS2bt3b6RHsub+++83jRo1Mpdffrlp1qyZmTRpkklKSjITJkwwEydONE2aNDH33HNPpMc0xhhTa8IiNjbWfPLJJ5Vu37Jli4mNja3Giex69dVXf3F55JFHovIP4Msuu8z079/ffPnll6akpMTk5OSY9PR089lnnxljoj8sunbtau68805jjDGzZ882J510krnjjjv82++44w7Tp0+fSI0XsokTJ5r09PRjosjpdJpNmzZFaCp7HA6H+eKLL4wxxgwePNj06tXLHD582BhjzPfff2/69+9vrrjiikiOGLL69ev7//nKzMw0Tz/9dMD2559/3rRv3z4So1WZw+EwS5cuNaNHjzYnn3yyqVu3rrnkkkvM66+/bioqKiI9XpW0atXKvPzyy8aY//1FMiYmxsyaNcu//ZVXXjGtW7eO1HgBak1YpKenm+eee67S7c8995xJT0+vxons+rHEHQ5HpUs0/gHctGlTs379+oDXRo4caVq0aGF27NgR9WERHx9vSkpKjDHGVFRUGKfTaYqLi/3bN2zYYJKTkyM1XpV89NFHpk2bNuavf/2rOXLkiDGmdobF8QKqsLDQNG/ePBKjVVlSUpIpKioyxvzvn79169YFbN++fbuJi4uLxGhV9tP/3Y4cOWLmzp1rLrzwQhMTE2NSU1PNHXfc4f/nMdrExcX5g9AYY+rWrWs2btzoX9+1a5epX79+JEY7Rq35VMitt96q4cOHa9SoUXr11VdVWFioVatW6dVXX9WoUaM0YsQI3XbbbZEeM2QpKSl6+eWX5fP5jrusWbMm0iOG5LvvvjvmPcF//OMfuuSSS9SzZ09t27YtQpPZV6dOHcXGxgZ8u2+jRo1UVlYWuaGqoEuXLiouLtaXX36prKwsbdiwQQ6HI9JjWfPjtXi9XiUnJwdsS05O1pdffhmJsaosOztbBQUFkqSePXvqP//5T8D2F198Ua1bt47EaFbVrVtXgwYN0qJFi7Rz507deOONev7553X66adHerSQNGvWTJs3b5YklZSUqKKiwr8uSZs2bVLTpk0jNV6gSJeNTXPmzDFnnXWWcTqd/r/FO51Oc9ZZZ5m5c+dGerwqycnJMXfffXel29etW2ccDkc1TmRHly5dzMyZM4+7LTc31zRu3Diq71hkZGSYhQsX+tc3bNhgfvjhB//6+++/H9V30n40e/Zsk5ycbOrUqVNr7lh07NjRZGZmmoYNG5pXXnklYPu7775rTjnllAhNVzW7d+82aWlp5txzzzVjx441cXFxpnv37ubGG2805557rqlXr55ZsGBBpMcMyU/vWByPz+czixcvrsaJ7LnzzjtNkyZNzA033GDS09NNXl6eadGihSkoKDBPPfWUcbvd5pZbbon0mMYYY2rA46P2XHnllbryyiv1ww8/6MCBA5Kkk08+WXXr1o3wZFX3t7/9TYcPH650e+vWrfXOO+9U40R2DBgwQLNnz9Y111xzzLYnn3xSPp9PTz31VAQms2PEiBGqqKjwr3fo0CFg+8KFC6PyUyE/N3jwYHXv3l3FxcW14puOx48fH7Bev379gPXXX389aj95kJqaqrVr12rSpEl6/fXXZYzRRx99JI/Ho27dumnFihXKysqK9JghadmypWJiYird7nA41KdPn2qcyJ777rtPcXFxKiws1F/+8heNGzdOGRkZuu222/Ttt98qJydHDzzwQKTHlFSLPm4KAAAir9Y8YwEAACKPsAAAANYQFgAAwBrCAgAAWENYAAhJr169NGbMmLCe495771WnTp3Ceg4AdhEWAGqt6dOnB/xCMgDhR1gAAABrCAsgSi1atEjdu3dX48aNlZSUpP79+2vHjh3+7StXrlSnTp0UGxurrKwszZ8/Xw6HQ+vWrfPvs3nzZl188cVq2LChkpOTdc011/h/udyJOHr0qEaNGuWf4a677tJPfzWOw+HQ/PnzA45p3Lixpk+f7l///PPPNXjwYCUmJqpBgwbKysrSqlWrjnu+Tz/9VK1bt9aIESPk8/l05MgR3XbbbTrllFPUoEEDnXXWWVq+fLkkafny5Ro2bJjKysrkcDjkcDh07733nvC1AQgNYQFEqcOHD2vs2LFavXq1li1bpjp16mjAgAHy+Xw6ePCgcnJy1LFjR61Zs0YPPPCAxo0bF3D83r171bNnT3Xq1ElFRUVatGiRvvjiCw0aNOiEZ5gxY4acTqdWrVqlxx9/XI888oj+9a9/nfDxhw4dUs+ePbVnzx699tpr+vjjj3XbbbfJ5/Mds+/GjRvVrVs3DRw4UAUFBapTp46GDRumFStWaM6cOVq/fr0GDhyoiy66SCUlJTrnnHP06KOPKj4+Xnv37tXevXt16623nvBsAEIU2d8oDsCW/fv3G0lmw4YNpqCgwCQlJZnvvvvOv33q1KlGklm7dq0xxpi7777b9O3bN+BneDweI8ls3br1V8/Xs2dP065dO+Pz+fyvjRs3zrRr186/LsnMmzcv4LiEhAQzbdo0Y4wxTz/9tGnUqJH56quvjnuO8ePHmzPPPNOsXLnSJCYmmr///e/+bdu3bzcOh8Ps3r074Jjzzz/f5OXlGWOMmTZtmklISPjVawFgT636rhDgt2THjh26++67VVhYqAMHDvj/ll9aWqqtW7cqIyNDsbGx/v1///vfBxxfXFysd955Rw0bNjzuz27Tps2vznD22WcHfKNp165d9fDDD6uiouIXv7PhR+vWrVNmZqYSExMr3ae0tFQXXHCBJkyYoFtuucX/+po1a2SMOWZOr9erpKSkXz03gPAgLIAolZOTI7fbralTpyo1NVU+n08dOnTQkSNHZIw55ivMzc++Fsjn8yknJ0eTJ08+5menpKRYmdHhcBxz3h9++MH/n+Pi4n71ZzRp0kSpqamaM2eOrr/+esXHx0v63/wxMTEqLi4+JmKOF0sAqgfPWABR6KuvvtKWLVt011136fzzz1e7du30zTff+Le3bdtW69evl9fr9b9WVFQU8DN+97vfadOmTUpLS1Pr1q0DlgYNGpzQHIWFhcesn3baaf4/6Js0aaK9e/f6t5eUlOjbb7/1r2dkZGjdunX6+uuvKz1HXFyc3njjDcXGxurCCy/UwYMHJUmZmZmqqKjQ/v37j5m/WbNmkqR69eoFfLssgPAjLIAodNJJJykpKUnPPPOMtm/frrfffltjx471b//jH/8on8+nP//5z9qyZYveeustPfTQQ5Lkv5ORm5urr7/+WkOGDNFHH32knTt3avHixbruuutO+A9jj8ejsWPHauvWrZo9e7aeeOIJjR492r/9vPPO05NPPqk1a9aoqKhIw4cPV926df3bhwwZombNmumyyy7TihUrtHPnTr388sv68MMPA87ToEEDLViwQE6nU9nZ2Tp06JDatGmjq666Stdee61eeeUVffrpp1q9erUmT56sN998U5KUlpamQ4cOadmyZTpw4EBA1AAIk8g+4gEgVEuWLDHt2rUzLpfLZGRkmOXLlwc8LLlixQqTkZFh6tWrZzp37mxeeOEFI8l88skn/p+xbds2M2DAANO4cWMTFxdn2rZta8aMGRPwQGZlevbsaUaOHGmGDx9u4uPjzUknnWRuv/32gGN3795t+vbtaxo0aGBOO+008+abbwY8vGmMMbt27TKXX365iY+PN/Xr1zdZWVlm1apVxpj/f3jzRwcPHjTnnHOO6dGjhzl06JA5cuSIueeee0xaWpqpW7euadasmRkwYIBZv369/5jhw4ebpKQkI8mMHz8+tP+yAZwwhzE/ewMUQK30/PPP+3+vw4k82wAAoeDhTaCWmjlzplq1aqVTTjlFH3/8scaNG6dBgwYRFQDCirAAaql9+/bpnnvu0b59+5SSkqKBAwfqwQcfPKFjS0tL1b59+0q3b968WS1atLA1KoBahLdCABzj6NGj2rVrV6Xb09LS5HTy9xIAxyIsAACANXzcFAAAWENYAAAAawgLAABgDWEBAACsISwAAIA1hAUAALCGsAAAANYQFgAAwJr/A1je/r7Do28GAAAAAElFTkSuQmCC", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# DO NOT CHANGE THIS CELL\n", + "bins=[0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100]\n", + "df['age_bucket']=pd.cut(df['age'], bins=bins, right=True, include_lowest=True, labels=False)\n", + "df.groupby('age_bucket').size().plot(kind='bar')" + ] + }, + { + "cell_type": "markdown", + "id": "88341062-8ecc-4264-8c42-bee7ae173c05", + "metadata": {}, + "source": [ + "### ExerciseExercise #1 - Bar Chart ###\n", + "We would like to find the distribution of sex in the population. \n", + "\n", + "**Instructions**:
\n", + "* Modify the `` only and execute the below cell plot the number of each sex in our dataset. " + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "d0d37093-e329-498d-b748-7fa3fb792303", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAiMAAAG/CAYAAACKZtcUAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMywgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/GU6VOAAAACXBIWXMAAA9hAAAPYQGoP6dpAAAdrUlEQVR4nO3df3DX9X3A8de3gIFTEg/ahKQGDFUziui40KtpBXR0YWFz60l7Xs8rzB+7y05lkuMogWtXO7u4yTzKqlAqP+Y5W28NOncyS64ScBW7BcL0ZuR0BcPRRBa3S5RuCT+++8MjtywB/SLhTcLjcff54/P5vt983587vvL0+/l8v99MNpvNBgBAIp9IvQAA4OImRgCApMQIAJCUGAEAkhIjAEBSYgQASEqMAABJiREAICkxAgAkJUYAgKSGVYzs2rUrbrnlligpKYlMJhPPPvtsTvO//e1vRyaTGbBdeumlQ7NgAOBDDasYOXr0aFx//fXx/e9//6zmL1u2LNrb2/ttn/3sZ+OrX/3qOV4pAPBRDasYqa6ujgcffDBuvfXWQR/v7e2N5cuXx6c//em49NJL4/Of/3w0NTX1PX7ZZZfFpEmT+rZ33nknXn/99bjrrrvO0xkAAP/f6NQLOJfuuOOOOHjwYPz4xz+OkpKSeOaZZ+J3fud34rXXXourr756wPjHH388rrnmmpg9e3aC1QIAEcPsnZEz+fd///f40Y9+FH/3d38Xs2fPjs985jOxbNmyuPHGG2Pz5s0Dxvf09MTf/u3felcEABIbMe+M7N27N7LZbFxzzTX9jvf09MTEiRMHjN+6dWu89957sWjRovO1RABgECMmRk6ePBmjRo2KPXv2xKhRo/o9dtlllw0Y//jjj8fv/d7vxaRJk87XEgGAQYyYGJk5c2acOHEijhw58qH3gBw4cCB27NgRzz333HlaHQBwOsMqRt5///146623+vYPHDgQ+/btiwkTJsQ111wTt99+eyxatCj+6q/+KmbOnBmdnZ3x4osvxowZM2LBggV98zZt2hTFxcVRXV2d4jQAgP8jk81ms6kX8VE1NTXFzTffPOD44sWLY8uWLXHs2LF48MEH44knnojDhw/HxIkTo7KyMh544IGYMWNGRHxwOWfKlCmxaNGi+O53v3u+TwEA+H+GVYwAACPPiPloLwAwPIkRACCpYXED68mTJ+NXv/pVjB8/PjKZTOrlAAAfQTabjffeey9KSkriE584/fsfwyJGfvWrX0VpaWnqZQAAZ+HQoUNxxRVXnPbxYREj48ePj4gPTiY/Pz/xagCAj6K7uztKS0v7/h0/nWERI6cuzeTn54sRABhmPuwWCzewAgBJiREAICkxAgAkJUYAgKTECACQVE4xsm7durjuuuv6PtVSWVkZ//iP/3jGOTt37oyKiooYO3ZsTJ06NdavX/+xFgwAjCw5xcgVV1wRDz30UDQ3N0dzc3P81m/9VvzBH/xB/Nu//dug4w8cOBALFiyI2bNnR0tLS6xcuTKWLFkSDQ0N52TxAMDw97F/tXfChAnx8MMPx1133TXgsW984xvx3HPPRWtra9+xmpqa+Nd//dfYvXv3R36O7u7uKCgoiK6uLt8zAgDDxEf99/us7xk5ceJE/PjHP46jR49GZWXloGN2794dVVVV/Y7Nnz8/mpub49ixY6f9s3t6eqK7u7vfBgCMTDnHyGuvvRaXXXZZ5OXlRU1NTTzzzDPx2c9+dtCxHR0dUVRU1O9YUVFRHD9+PDo7O0/7HPX19VFQUNC3+V0aABi5co6R8vLy2LdvX7zyyivxx3/8x7F48eJ4/fXXTzv+/38F7KmrQmf6ati6urro6urq2w4dOpTrMgGAYSLn36a55JJL4qqrroqIiFmzZsW//Mu/xPe+9734wQ9+MGDspEmToqOjo9+xI0eOxOjRo2PixImnfY68vLzIy8vLdWkAwDD0sb9nJJvNRk9Pz6CPVVZWRmNjY79j27dvj1mzZsWYMWM+7lMDACNATjGycuXKeOmll+LgwYPx2muvxapVq6KpqSluv/32iPjg8sqiRYv6xtfU1MTbb78dtbW10draGps2bYqNGzfGsmXLzu1ZAADDVk6Xad555534+te/Hu3t7VFQUBDXXXddvPDCC/Hbv/3bERHR3t4ebW1tfePLyspi27ZtsXTp0nj00UejpKQk1q5dGwsXLjy3ZzGCXbni+dRL4Dw6+NDvpl4CwHn3sb9n5Hy4mL9nRIxcXMQIMJIM+feMAACcC2IEAEhKjAAASYkRACApMQIAJCVGAICkcv46eADODR/dv7j46P7peWcEAEhKjAAASYkRACApMQIAJCVGAICkxAgAkJQYAQCSEiMAQFJiBABISowAAEmJEQAgKTECACQlRgCApMQIAJCUGAEAkhIjAEBSYgQASEqMAABJiREAICkxAgAkJUYAgKTECACQlBgBAJISIwBAUmIEAEhKjAAASYkRACApMQIAJCVGAICkxAgAkJQYAQCSEiMAQFJiBABISowAAEmJEQAgKTECACQlRgCApMQIAJCUGAEAkhIjAEBSYgQASEqMAABJ5RQj9fX18bnPfS7Gjx8fhYWF8eUvfzn2799/xjlNTU2RyWQGbG+88cbHWjgAMDLkFCM7d+6Me+65J1555ZVobGyM48ePR1VVVRw9evRD5+7fvz/a29v7tquvvvqsFw0AjByjcxn8wgsv9NvfvHlzFBYWxp49e2LOnDlnnFtYWBiXX355zgsEAEa2j3XPSFdXV0RETJgw4UPHzpw5M4qLi2PevHmxY8eOM47t6emJ7u7ufhsAMDKddYxks9mora2NG2+8Ma699trTjisuLo4NGzZEQ0NDbN26NcrLy2PevHmxa9eu086pr6+PgoKCvq20tPRslwkAXOByukzzf917773x6quvxj/90z+dcVx5eXmUl5f37VdWVsahQ4di9erVp720U1dXF7W1tX373d3dggQARqizemfkvvvui+eeey527NgRV1xxRc7zb7jhhnjzzTdP+3heXl7k5+f32wCAkSmnd0ay2Wzcd9998cwzz0RTU1OUlZWd1ZO2tLREcXHxWc0FAEaWnGLknnvuiaeeeir+/u//PsaPHx8dHR0REVFQUBDjxo2LiA8usRw+fDieeOKJiIhYs2ZNXHnllTF9+vTo7e2NJ598MhoaGqKhoeEcnwoAMBzlFCPr1q2LiIibbrqp3/HNmzfHH/7hH0ZERHt7e7S1tfU91tvbG8uWLYvDhw/HuHHjYvr06fH888/HggULPt7KAYARIefLNB9my5Yt/faXL18ey5cvz2lRAMDFw2/TAABJiREAICkxAgAkJUYAgKTECACQlBgBAJISIwBAUmIEAEhKjAAASYkRACApMQIAJCVGAICkxAgAkJQYAQCSEiMAQFJiBABISowAAEmJEQAgKTECACQlRgCApMQIAJCUGAEAkhIjAEBSYgQASEqMAABJiREAICkxAgAkJUYAgKTECACQlBgBAJISIwBAUmIEAEhKjAAASYkRACApMQIAJCVGAICkxAgAkJQYAQCSEiMAQFJiBABISowAAEmJEQAgKTECACQlRgCApMQIAJCUGAEAkhIjAEBSYgQASCqnGKmvr4/Pfe5zMX78+CgsLIwvf/nLsX///g+dt3PnzqioqIixY8fG1KlTY/369We9YABgZMkpRnbu3Bn33HNPvPLKK9HY2BjHjx+PqqqqOHr06GnnHDhwIBYsWBCzZ8+OlpaWWLlyZSxZsiQaGho+9uIBgOFvdC6DX3jhhX77mzdvjsLCwtizZ0/MmTNn0Dnr16+PyZMnx5o1ayIiYtq0adHc3ByrV6+OhQsXnt2qAYAR42PdM9LV1RURERMmTDjtmN27d0dVVVW/Y/Pnz4/m5uY4duzYoHN6enqiu7u73wYAjExnHSPZbDZqa2vjxhtvjGuvvfa04zo6OqKoqKjfsaKiojh+/Hh0dnYOOqe+vj4KCgr6ttLS0rNdJgBwgTvrGLn33nvj1VdfjR/96EcfOjaTyfTbz2azgx4/pa6uLrq6uvq2Q4cOne0yAYALXE73jJxy3333xXPPPRe7du2KK6644oxjJ02aFB0dHf2OHTlyJEaPHh0TJ04cdE5eXl7k5eWdzdIAgGEmp3dGstls3HvvvbF169Z48cUXo6ys7EPnVFZWRmNjY79j27dvj1mzZsWYMWNyWy0AMOLkFCP33HNPPPnkk/HUU0/F+PHjo6OjIzo6OuK///u/+8bU1dXFokWL+vZramri7bffjtra2mhtbY1NmzbFxo0bY9myZefuLACAYSunGFm3bl10dXXFTTfdFMXFxX3b008/3Temvb092tra+vbLyspi27Zt0dTUFL/5m78Zf/ZnfxZr1671sV4AICJyvGfk1I2nZ7Jly5YBx+bOnRt79+7N5akAgIuE36YBAJISIwBAUmIEAEhKjAAASYkRACApMQIAJCVGAICkxAgAkJQYAQCSEiMAQFJiBABISowAAEmJEQAgKTECACQlRgCApMQIAJCUGAEAkhIjAEBSYgQASEqMAABJiREAICkxAgAkJUYAgKTECACQlBgBAJISIwBAUmIEAEhKjAAASYkRACApMQIAJCVGAICkxAgAkJQYAQCSEiMAQFJiBABISowAAEmJEQAgKTECACQlRgCApMQIAJCUGAEAkhIjAEBSYgQASEqMAABJiREAICkxAgAkJUYAgKTECACQlBgBAJLKOUZ27doVt9xyS5SUlEQmk4lnn332jOObmpoik8kM2N54442zXTMAMIKMznXC0aNH4/rrr4877rgjFi5c+JHn7d+/P/Lz8/v2P/WpT+X61ADACJRzjFRXV0d1dXXOT1RYWBiXX355zvMAgJHtvN0zMnPmzCguLo558+bFjh07zji2p6cnuru7+20AwMg05DFSXFwcGzZsiIaGhti6dWuUl5fHvHnzYteuXaedU19fHwUFBX1baWnpUC8TAEgk58s0uSovL4/y8vK+/crKyjh06FCsXr065syZM+icurq6qK2t7dvv7u4WJAAwQiX5aO8NN9wQb7755mkfz8vLi/z8/H4bADAyJYmRlpaWKC4uTvHUAMAFJufLNO+//3689dZbffsHDhyIffv2xYQJE2Ly5MlRV1cXhw8fjieeeCIiItasWRNXXnllTJ8+PXp7e+PJJ5+MhoaGaGhoOHdnAQAMWznHSHNzc9x88819+6fu7Vi8eHFs2bIl2tvbo62tre/x3t7eWLZsWRw+fDjGjRsX06dPj+effz4WLFhwDpYPAAx3OcfITTfdFNls9rSPb9mypd/+8uXLY/ny5TkvDAC4OPhtGgAgKTECACQlRgCApMQIAJCUGAEAkhIjAEBSYgQASEqMAABJiREAICkxAgAkJUYAgKTECACQlBgBAJISIwBAUmIEAEhKjAAASYkRACApMQIAJCVGAICkxAgAkJQYAQCSEiMAQFJiBABISowAAEmJEQAgKTECACQlRgCApMQIAJCUGAEAkhIjAEBSYgQASEqMAABJiREAICkxAgAkJUYAgKTECACQlBgBAJISIwBAUmIEAEhKjAAASYkRACApMQIAJCVGAICkxAgAkJQYAQCSEiMAQFJiBABISowAAEnlHCO7du2KW265JUpKSiKTycSzzz77oXN27twZFRUVMXbs2Jg6dWqsX7/+bNYKAIxAOcfI0aNH4/rrr4/vf//7H2n8gQMHYsGCBTF79uxoaWmJlStXxpIlS6KhoSHnxQIAI8/oXCdUV1dHdXX1Rx6/fv36mDx5cqxZsyYiIqZNmxbNzc2xevXqWLhwYa5PDwCMMEN+z8ju3bujqqqq37H58+dHc3NzHDt2bNA5PT090d3d3W8DAEamIY+Rjo6OKCoq6nesqKgojh8/Hp2dnYPOqa+vj4KCgr6ttLR0qJcJACRyXj5Nk8lk+u1ns9lBj59SV1cXXV1dfduhQ4eGfI0AQBo53zOSq0mTJkVHR0e/Y0eOHInRo0fHxIkTB52Tl5cXeXl5Q700AOACMOTvjFRWVkZjY2O/Y9u3b49Zs2bFmDFjhvrpAYALXM4x8v7778e+ffti3759EfHBR3f37dsXbW1tEfHBJZZFixb1ja+pqYm33347amtro7W1NTZt2hQbN26MZcuWnZszAACGtZwv0zQ3N8fNN9/ct19bWxsREYsXL44tW7ZEe3t7X5hERJSVlcW2bdti6dKl8eijj0ZJSUmsXbvWx3oBgIg4ixi56aab+m5AHcyWLVsGHJs7d27s3bs316cCAC4CfpsGAEhKjAAASYkRACApMQIAJCVGAICkxAgAkJQYAQCSEiMAQFJiBABISowAAEmJEQAgKTECACQlRgCApMQIAJCUGAEAkhIjAEBSYgQASEqMAABJiREAICkxAgAkJUYAgKTECACQlBgBAJISIwBAUmIEAEhKjAAASYkRACApMQIAJCVGAICkxAgAkJQYAQCSEiMAQFJiBABISowAAEmJEQAgKTECACQlRgCApMQIAJCUGAEAkhIjAEBSYgQASEqMAABJiREAICkxAgAkJUYAgKTECACQlBgBAJISIwBAUmIEAEjqrGLksccei7Kyshg7dmxUVFTESy+9dNqxTU1NkclkBmxvvPHGWS8aABg5co6Rp59+Ou6///5YtWpVtLS0xOzZs6O6ujra2trOOG///v3R3t7et1199dVnvWgAYOTIOUYeeeSRuOuuu+Luu++OadOmxZo1a6K0tDTWrVt3xnmFhYUxadKkvm3UqFFnvWgAYOTIKUZ6e3tjz549UVVV1e94VVVVvPzyy2ecO3PmzCguLo558+bFjh07zji2p6cnuru7+20AwMiUU4x0dnbGiRMnoqioqN/xoqKi6OjoGHROcXFxbNiwIRoaGmLr1q1RXl4e8+bNi127dp32eerr66OgoKBvKy0tzWWZAMAwMvpsJmUymX772Wx2wLFTysvLo7y8vG+/srIyDh06FKtXr445c+YMOqeuri5qa2v79ru7uwUJAIxQOb0z8slPfjJGjRo14F2QI0eODHi35ExuuOGGePPNN0/7eF5eXuTn5/fbAICRKacYueSSS6KioiIaGxv7HW9sbIwvfOELH/nPaWlpieLi4lyeGgAYoXK+TFNbWxtf//rXY9asWVFZWRkbNmyItra2qKmpiYgPLrEcPnw4nnjiiYiIWLNmTVx55ZUxffr06O3tjSeffDIaGhqioaHh3J4JADAs5Rwjt912W7z77rvxne98J9rb2+Paa6+Nbdu2xZQpUyIior29vd93jvT29sayZcvi8OHDMW7cuJg+fXo8//zzsWDBgnN3FgDAsJXJZrPZ1Iv4MN3d3VFQUBBdXV0X3f0jV654PvUSOI8OPvS7qZfAeeT1fXG5GF/fH/Xfb79NAwAkJUYAgKTECACQlBgBAJISIwBAUmIEAEhKjAAASYkRACApMQIAJCVGAICkxAgAkJQYAQCSEiMAQFJiBABISowAAEmJEQAgKTECACQlRgCApMQIAJCUGAEAkhIjAEBSYgQASEqMAABJiREAICkxAgAkJUYAgKTECACQlBgBAJISIwBAUmIEAEhKjAAASYkRACApMQIAJCVGAICkxAgAkJQYAQCSEiMAQFJiBABISowAAEmJEQAgKTECACQlRgCApMQIAJCUGAEAkhIjAEBSYgQASEqMAABJiREAIKmzipHHHnssysrKYuzYsVFRUREvvfTSGcfv3LkzKioqYuzYsTF16tRYv379WS0WABh5co6Rp59+Ou6///5YtWpVtLS0xOzZs6O6ujra2toGHX/gwIFYsGBBzJ49O1paWmLlypWxZMmSaGho+NiLBwCGv5xj5JFHHom77ror7r777pg2bVqsWbMmSktLY926dYOOX79+fUyePDnWrFkT06ZNi7vvvjvuvPPOWL169cdePAAw/I3OZXBvb2/s2bMnVqxY0e94VVVVvPzyy4PO2b17d1RVVfU7Nn/+/Ni4cWMcO3YsxowZM2BOT09P9PT09O13dXVFRER3d3cuyx0RTvb8OvUSOI8uxr/jFzOv74vLxfj6PnXO2Wz2jONyipHOzs44ceJEFBUV9TteVFQUHR0dg87p6OgYdPzx48ejs7MziouLB8ypr6+PBx54YMDx0tLSXJYLw07BmtQrAIbKxfz6fu+996KgoOC0j+cUI6dkMpl++9lsdsCxDxs/2PFT6urqora2tm//5MmT8Z//+Z8xceLEMz4PI0N3d3eUlpbGoUOHIj8/P/VygHPI6/viks1m47333ouSkpIzjsspRj75yU/GqFGjBrwLcuTIkQHvfpwyadKkQcePHj06Jk6cOOicvLy8yMvL63fs8ssvz2WpjAD5+fn+YwUjlNf3xeNM74icktMNrJdccklUVFREY2Njv+ONjY3xhS98YdA5lZWVA8Zv3749Zs2aNej9IgDAxSXnT9PU1tbG448/Hps2bYrW1tZYunRptLW1RU1NTUR8cIll0aJFfeNramri7bffjtra2mhtbY1NmzbFxo0bY9myZefuLACAYSvne0Zuu+22ePfdd+M73/lOtLe3x7XXXhvbtm2LKVOmREREe3t7v+8cKSsri23btsXSpUvj0UcfjZKSkli7dm0sXLjw3J0FI0peXl786Z/+6YBLdcDw5/XNYDLZD/u8DQDAEPLbNABAUmIEAEhKjAAASYkRknj11Vfj5MmTqZcBwAVAjJDEzJkzo7OzMyIipk6dGu+++27iFQGQihghicsvvzwOHDgQEREHDx70LgnAReysfpsGPq6FCxfG3Llzo7i4ODKZTMyaNStGjRo16Nhf/vKX53l1wLn2P//zP/Hqq6/GkSNHBvzPx+///u8nWhUXCjFCEhs2bIhbb7013nrrrViyZEn80R/9UYwfPz71soAh8MILL8SiRYv6Ls3+X5lMJk6cOJFgVVxIfOkZyd1xxx2xdu1aMQIj1FVXXRXz58+Pb33rW6f9UVUubmIEgCGVn58fLS0t8ZnPfCb1UrhAuYEVgCH1la98JZqamlIvgwuYd0YAGFK//vWv46tf/Wp86lOfihkzZsSYMWP6Pb5kyZJEK+NCIUYAGFKPP/541NTUxLhx42LixImRyWT6HstkMj4xhxgBYGhNmjQplixZEitWrIhPfMLdAQzkbwUAQ6q3tzduu+02IcJp+ZsBwJBavHhxPP3006mXwQXMl54BMKROnDgRf/mXfxk//elP47rrrhtwA+sjjzySaGVcKNwzAsCQuvnmm0/7WCaTiRdffPE8roYLkRgBAJJyzwgAkJQYAQCSEiMAQFJiBABISowAAEmJEQAgKTECACQlRoAh85Of/CRmzJjR92utX/rSl+Lo0aMREbF58+aYNm1ajB07Nn7jN34jHnvssb55d955Z1x33XXR09MTERHHjh2LioqKuP3225OcBzC0xAgwJNrb2+NrX/ta3HnnndHa2hpNTU1x6623RjabjR/+8IexatWq+O53vxutra3x53/+5/HNb34z/uZv/iYiItauXRtHjx6NFStWRETEN7/5zejs7OwXLMDI4RtYgSGxd+/eqKioiIMHD8aUKVP6PTZ58uT4i7/4i/ja177Wd+zBBx+Mbdu2xcsvvxwREbt37465c+fGihUror6+Pn72s5/FnDlzzus5AOeHGAGGxIkTJ2L+/Pnxz//8zzF//vyoqqqKr3zlK3H8+PEoLCyMcePG9ftJ+ePHj0dBQUG88847fcdWrlwZ9fX18Y1vfCMeeuihFKcBnAd+tRcYEqNGjYrGxsZ4+eWXY/v27fHXf/3XsWrVqviHf/iHiIj44Q9/GJ///OcHzDnl5MmT8fOf/zxGjRoVb7755nldO3B+uWcEGDKZTCa++MUvxgMPPBAtLS1xySWXxM9//vP49Kc/Hb/85S/jqquu6reVlZX1zX344YejtbU1du7cGT/96U9j8+bNCc8EGEreGQGGxC9+8Yv42c9+FlVVVVFYWBi/+MUv4j/+4z9i2rRp8e1vfzuWLFkS+fn5UV1dHT09PdHc3Bz/9V//FbW1tbFv37741re+FT/5yU/ii1/8Ynzve9+LP/mTP4m5c+fG1KlTU58acI65ZwQYEq2trbF06dLYu3dvdHd3x5QpU+K+++6Le++9NyIinnrqqXj44Yfj9ddfj0svvTRmzJgR999/f1RXV0dFRUXceOON8YMf/KDvz7v11lvjnXfeiV27dvW7nAMMf2IEAEjKPSMAQFJiBABISowAAEmJEQAgKTECACQlRgCApMQIAJCUGAEAkhIjAEBSYgQASEqMAABJ/S8FsYTs8F9ulgAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "df.groupby('sex').size().plot(kind='bar')" + ] + }, + { + "cell_type": "raw", + "id": "b52410a7-777f-4b0a-862f-9fed419d9c79", + "metadata": {}, + "source": [ + "\n", + "df.groupby('sex').size().plot(kind='bar')" + ] + }, + { + "cell_type": "markdown", + "id": "5e49a376-90f6-4ae1-9a47-fae469a5d1da", + "metadata": {}, + "source": [ + "Click ... for solution. " + ] + }, + { + "cell_type": "markdown", + "id": "676bd687-b998-4724-bda5-e1d68307bb24", + "metadata": {}, + "source": [ + "## Scatter Plot ##\n", + "The scatter plot is used to show the relationship between two variables in a dataset. It can also be used to display coordinates of each data point to help identify outliers or clusters. " + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "e170e8ba-5dbf-428d-b00a-0880d8bdcfad", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
agesexcountylatlongnameage_bucket
4340079640fNOTTINGHAMSHIRE53.054501-0.943481ZOYA3
3250588810fNOTTINGHAMSHIRE52.995071-0.938463LINA0
3351294013fLEICESTERSHIRE52.967442-1.849579TEYANA1
6753721mSOMERSET51.138855-2.919084HARRISON0
2102473356mBURY53.628117-2.325484JACK5
........................
1361868236mWALTHAM FOREST51.599720-0.043070ETHAN3
1102931530mSALFORD53.466831-2.422284WILLIAM2
5680222980fNOTTINGHAM52.972649-1.233015ERIN7
295927522fSALFORD53.518764-2.454344SIRAT0
3358553013fHERTFORDSHIRE51.604103-0.386183MATILDA1
\n", + "

1000 rows × 7 columns

\n", + "
" + ], + "text/plain": [ + " age sex county lat long name age_bucket\n", + "43400796 40 f NOTTINGHAMSHIRE 53.054501 -0.943481 ZOYA 3\n", + "32505888 10 f NOTTINGHAMSHIRE 52.995071 -0.938463 LINA 0\n", + "33512940 13 f LEICESTERSHIRE 52.967442 -1.849579 TEYANA 1\n", + "675372 1 m SOMERSET 51.138855 -2.919084 HARRISON 0\n", + "21024733 56 m BURY 53.628117 -2.325484 JACK 5\n", + "... ... .. ... ... ... ... ...\n", + "13618682 36 m WALTHAM FOREST 51.599720 -0.043070 ETHAN 3\n", + "11029315 30 m SALFORD 53.466831 -2.422284 WILLIAM 2\n", + "56802229 80 f NOTTINGHAM 52.972649 -1.233015 ERIN 7\n", + "29592752 2 f SALFORD 53.518764 -2.454344 SIRAT 0\n", + "33585530 13 f HERTFORDSHIRE 51.604103 -0.386183 MATILDA 1\n", + "\n", + "[1000 rows x 7 columns]" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# DO NOT CHANGE THIS CELL\n", + "# sample a very small percentage of the data\n", + "small_df=df.sample(1000)\n", + "display(small_df)\n", + "small_df.plot(kind='scatter', x='lat', y='long')" + ] + }, + { + "cell_type": "markdown", + "id": "8e0dbf2c-9bbf-4621-bce6-661ede296af9", + "metadata": {}, + "source": [ + "## Line Chart ##\n", + "Line charts are good for connecting individual data points to show trends. It's useful for visualizing changes, trends, and patterns over time. \n", + "\n", + "The scatter plot doesn't scale well with the number of data points. When the data becomes large, the scatter plot take a long time to complete. Below is line chart of the compute time for different data sizes. \n", + "\n", + "

\n", + "\n", + "**Note**: Below is the code used to produce this image. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ce18692d-b36f-4275-b056-fd1ec1170abc", + "metadata": {}, + "outputs": [], + "source": [ + "# import time\n", + "# import matplotlib.pyplot as plt\n", + "\n", + "# fig, ax=plt.subplots()\n", + "# exec_times={}\n", + "\n", + "# for size in (5*(10**i) for i in range(1, 8)): \n", + "# start=time.time()\n", + "# df.sample(size).plot(kind='scatter', x='long', y='lat', ax=ax)\n", + "# duration=time.time()-start\n", + "# exec_times[size]=duration\n", + "# ax.clear()\n", + "\n", + "# ax.plot(exec_times.keys(), exec_times.values(), marker='o')\n", + "# ax.set_xscale('log')\n", + "# ax.set_xlabel('Data Size')\n", + "# ax.set_ylabel('Execution Time')\n", + "# ax.set_title(\"Scatter Plot Doesn't Scale Well With Data Size\")" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "44e65ae7-f223-455f-a463-10f1e193ef64", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'status': 'ok', 'restart': True}" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# DO NOT CHANGE THIS CELL\n", + "import IPython\n", + "app = IPython.Application.instance()\n", + "app.kernel.do_shutdown(True)" + ] + }, + { + "cell_type": "markdown", + "id": "b26686d7-fb05-49a0-9006-036810d86160", + "metadata": {}, + "source": [ + "## Datashader ##\n", + "[Datashader](https://datashader.org/#) is an open-source Python library for analyzing and visualizing large datasets. Specifically, Datashader is designed to \"rasterize\" or \"aggregate\" datasets into regular grids that can be analyzed further or viewed as images, making it simple and quick to see the properties and patterns of data. \n", + "\n", + "Plotting for big data is challenging because rendering a large number of points takes a long time. Datashader shifts the burden of visualization from rendering to computing. Underneath the hood, it turns a long list of (x, y) points into a 2D histogram instead of plotting each point individually. Furthermore, this aggregation can be accelerated through parallel computing. The resulting gridded data structure is then turn into an image, using color to show the magnitude, before being embedding into a plotting program. \n", + "\n", + "Datashader generates a plot using a five-step [pipeline](https://datashader.org/getting_started/Pipeline.html): \n", + "\n", + "

\n", + "\n", + "Below we demonstrate how Datashader is used. " + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "a7bd466f-0e40-4b40-bd50-82c9fdc17e2f", + "metadata": {}, + "outputs": [], + "source": [ + "# DO NOT CHANGE THIS CELL\n", + "import time\n", + "import matplotlib.pyplot as plt\n", + "\n", + "import datashader as ds\n", + "import datashader.transfer_functions as tf" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "813eb18b-5234-4a1f-ae05-58bcf8750e9f", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
agesexcountylatlongname
00mDARLINGTON54.533646-1.524401FRANCIS
10mDARLINGTON54.426254-1.465314EDWARD
20mDARLINGTON54.555199-1.496417TEDDY
30mDARLINGTON54.547905-1.572341ANGUS
40mDARLINGTON54.477638-1.605994CHARLIE
\n", + "
" + ], + "text/plain": [ + " age sex county lat long name\n", + "0 0 m DARLINGTON 54.533646 -1.524401 FRANCIS\n", + "1 0 m DARLINGTON 54.426254 -1.465314 EDWARD\n", + "2 0 m DARLINGTON 54.555199 -1.496417 TEDDY\n", + "3 0 m DARLINGTON 54.547905 -1.572341 ANGUS\n", + "4 0 m DARLINGTON 54.477638 -1.605994 CHARLIE" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# DO NOT CHANGE THIS CELL\n", + "import pandas as pd\n", + "\n", + "dtype_dict={\n", + " 'age': 'int8', \n", + " 'sex': 'object', \n", + " 'county': 'object', \n", + " 'lat': 'float32', \n", + " 'long': 'float32', \n", + " 'name': 'object'\n", + "}\n", + " \n", + "df=pd.read_csv('./data/uk_pop.csv', dtype=dtype_dict)\n", + "df.head()" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "5f0085a4-97fa-494f-a525-792f2b65593e", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "
<xarray.DataArray (lat: 600, long: 600)> Size: 1MB\n",
+       "array([[0, 0, 0, ..., 0, 0, 0],\n",
+       "       [0, 0, 0, ..., 0, 0, 0],\n",
+       "       [0, 0, 0, ..., 0, 0, 0],\n",
+       "       ...,\n",
+       "       [0, 0, 0, ..., 0, 0, 0],\n",
+       "       [0, 0, 0, ..., 0, 0, 0],\n",
+       "       [0, 0, 0, ..., 0, 0, 0]], dtype=uint32)\n",
+       "Coordinates:\n",
+       "  * long     (long) float64 5kB -6.361 -6.346 -6.331 ... 2.662 2.677 2.693\n",
+       "  * lat      (lat) float64 5kB 49.52 49.54 49.55 49.56 ... 56.23 56.24 56.26\n",
+       "Attributes:\n",
+       "    x_range:  (-6.368374347686768, 2.7000913619995117)\n",
+       "    y_range:  (49.519046783447266, 56.261409759521484)
" + ], + "text/plain": [ + " Size: 1MB\n", + "array([[0, 0, 0, ..., 0, 0, 0],\n", + " [0, 0, 0, ..., 0, 0, 0],\n", + " [0, 0, 0, ..., 0, 0, 0],\n", + " ...,\n", + " [0, 0, 0, ..., 0, 0, 0],\n", + " [0, 0, 0, ..., 0, 0, 0],\n", + " [0, 0, 0, ..., 0, 0, 0]], dtype=uint32)\n", + "Coordinates:\n", + " * long (long) float64 5kB -6.361 -6.346 -6.331 ... 2.662 2.677 2.693\n", + " * lat (lat) float64 5kB 49.52 49.54 49.55 49.56 ... 56.23 56.24 56.26\n", + "Attributes:\n", + " x_range: (-6.368374347686768, 2.7000913619995117)\n", + " y_range: (49.519046783447266, 56.261409759521484)" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Duration: 1.28 seconds\n" + ] + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# DO NOT CHANGE THIS CELL\n", + "start=time.time()\n", + "\n", + "# get points\n", + "ds_points_pandas=ds.Canvas().points(df,'long','lat')\n", + "display(ds_points_pandas)\n", + "\n", + "# plot points\n", + "plt.imshow(tf.shade(ds_points_pandas))\n", + "\n", + "print(f'Duration: {round(time.time()-start, 2)} seconds')" + ] + }, + { + "cell_type": "markdown", + "id": "09268b18-7e81-46e7-978b-964fe56cda2e", + "metadata": {}, + "source": [ + "### Datashader Accelerated by GPU ###\n", + "Datashader can be accelerated by assigning the computation to a GPU. As previously mentioned, the GPU typically has far more (though individually less powerful) cores available than a CPU does, and for highly parallelizable computations like those in Datashader a GPU can typically achieve much faster performance at a given price point than a CPU or distributed set of CPUs can. The DataFrame from cuDF can be used as a replacement for rasterization. The performance benefits are significant since the entire data-processing pipeline is executed on the GPU and there is no bottleneck from data transfer. " + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "557e70e8-9eaf-4d6b-9048-8a1b433943bd", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
agesexcountylatlongname
00mDARLINGTON54.533638-1.524400FRANCIS
10mDARLINGTON54.426254-1.465314EDWARD
20mDARLINGTON54.555199-1.496417TEDDY
30mDARLINGTON54.547909-1.572342ANGUS
40mDARLINGTON54.477638-1.605995CHARLIE
\n", + "
" + ], + "text/plain": [ + " age sex county lat long name\n", + "0 0 m DARLINGTON 54.533638 -1.524400 FRANCIS\n", + "1 0 m DARLINGTON 54.426254 -1.465314 EDWARD\n", + "2 0 m DARLINGTON 54.555199 -1.496417 TEDDY\n", + "3 0 m DARLINGTON 54.547909 -1.572342 ANGUS\n", + "4 0 m DARLINGTON 54.477638 -1.605995 CHARLIE" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# DO NOT CHANGE THIS CELL\n", + "import cudf\n", + "\n", + "dtype_dict={\n", + " 'age': 'int8', \n", + " 'sex': 'object', \n", + " 'county': 'object', \n", + " 'lat': 'float32', \n", + " 'long': 'float32', \n", + " 'name': 'object'\n", + "}\n", + " \n", + "gdf=cudf.read_csv('./data/uk_pop.csv', dtype=dtype_dict)\n", + "gdf.head()" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "6a5b3eb3-40f2-46fb-a45b-5450f45ff398", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "
<xarray.DataArray (lat: 600, long: 600)> Size: 1MB\n",
+       "array([[0, 0, 0, ..., 0, 0, 0],\n",
+       "       [0, 0, 0, ..., 0, 0, 0],\n",
+       "       [0, 0, 0, ..., 0, 0, 0],\n",
+       "       ...,\n",
+       "       [0, 0, 0, ..., 0, 0, 0],\n",
+       "       [0, 0, 0, ..., 0, 0, 0],\n",
+       "       [0, 0, 0, ..., 0, 0, 0]], dtype=uint32)\n",
+       "Coordinates:\n",
+       "  * long     (long) float64 5kB -6.361 -6.346 -6.331 ... 2.662 2.677 2.693\n",
+       "  * lat      (lat) float64 5kB 49.52 49.54 49.55 49.56 ... 56.23 56.24 56.26\n",
+       "Attributes:\n",
+       "    x_range:  (-6.368374, 2.7000911)\n",
+       "    y_range:  (49.51904, 56.26141)
" + ], + "text/plain": [ + " Size: 1MB\n", + "array([[0, 0, 0, ..., 0, 0, 0],\n", + " [0, 0, 0, ..., 0, 0, 0],\n", + " [0, 0, 0, ..., 0, 0, 0],\n", + " ...,\n", + " [0, 0, 0, ..., 0, 0, 0],\n", + " [0, 0, 0, ..., 0, 0, 0],\n", + " [0, 0, 0, ..., 0, 0, 0]], dtype=uint32)\n", + "Coordinates:\n", + " * long (long) float64 5kB -6.361 -6.346 -6.331 ... 2.662 2.677 2.693\n", + " * lat (lat) float64 5kB 49.52 49.54 49.55 49.56 ... 56.23 56.24 56.26\n", + "Attributes:\n", + " x_range: (-6.368374, 2.7000911)\n", + " y_range: (49.51904, 56.26141)" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Duration: 0.08 seconds\n" + ] + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAakAAAGiCAYAAABd6zmYAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMywgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/GU6VOAAAACXBIWXMAAA9hAAAPYQGoP6dpAAEAAElEQVR4nOzdd5gUVdbA4V9VdU6Tc84MOTMzZFBEBMwJs5hzWHXV3XXd9dPVVXddc85iQBGzopJzzgwzTM45de6q+v5oHRwBRQQGtd7nmcfp6tvdp3qwT99b594rqKqqotFoNBrNMUjs7QA0Go1GozkQLUlpNBqN5pilJSmNRqPRHLO0JKXRaDSaY5aWpDQajUZzzNKSlEaj0WiOWVqS0mg0Gs0xS0tSGo1GozlmaUlKo9FoNMcsLUlpNBqN5pjVq0nqqaeeIi0tDZPJxLBhw1i6dGlvhqPRaDSaY0yvJal33nmHm266ibvvvpuNGzcyduxYTjzxRCoqKnorJI1Go9EcY4TeWmB21KhRDB06lKeffrr7WG5uLqeccgoPPPBAb4Sk0Wg0mmOMrjde1OfzsX79ev785z/3OD5lyhRWrFixT3uv14vX6+2+rSgKLS0tREREIAjCEY9Xo9FoNIeXqqp0dnYSHx+PKB54UK9XklRTUxOyLBMTE9PjeExMDHV1dfu0f+CBB7j33nuPVngajUajOUoqKytJTEw84P29kqS+9+NekKqq++0Z3Xnnndxyyy3dt9vb20lOTmYM09ChP+JxajQajebwCuBnGZ9ht9t/sl2vJKnIyEgkSdqn19TQ0LBP7wrAaDRiNBr3Oa5Dj07QkpRGo9H85nxXDfFzl2x6pbrPYDAwbNgwFixY0OP4ggULKCgo6I2QND8iGI1IDscPDgg0XHds/W2ksDAEXa8OBmg0miOs10rQb7nlFl544QVeeukldu7cyc0330xFRQVXXXVVb4XUazrPzuvtEPYhJifgzs/ee0BViXlmTe8FtB8dk7KRIiN6OwyNRnME9drX0LPPPpvm5mb+8Y9/UFtbS//+/fnss89ISUnprZB6Tej6euTeDuJH5KISDEUlPY6pgcBRe33RakWIi0YuLj1gG+v7qzl6EWk0mt7QqytOXHPNNZSVleH1elm/fj3jxo3rzXB6zU99EB+qpivzu3/XpSbjOm3UftsFJg9DHJR72F//1xLMJvxxob0dhkaj6WXagP7vVPRb21C++z1QUY2toan79g/pl21DlQ+uH6dLT0U1G1HMeqTqJgK1+04XOFzkpmbEpc1ImWmg1yHvLDpir6XRaI5d2gKzv0Md5+YhRobvPaDIKC7XftuqXu9+h/FEiwUpKqpnW6eL0jMiEF0+VI93n8fsjzxhKOrowQcd+/d0qckACF0uhM79x67RaH7/tCT1WyNKe3//cemmICBFRSH5VQLlVT3v0htouzCfnyUIIAiIsdH4+iX1uEuubyD53hXIO3Yjt7YeVLjSog0IK7fuG/vPaCmIByDQ0ESgqvrnY/4lxzUazW+GlqR+Y+qvHdX94dt4ZR6iyRS8QxBouDYff99ETE1+qu7Yew1Kl5ZCx2lDifyqBNFiQRjWb7/PreYPwn3yCJovzSNQUoa0aMNPByNKkDfwZ2NuuGYUgk5H/XXBmKSsdKSY6J98TNgnOxAH96Xhmv1fS+s2cgCBSUP3OayLi6Xt/GOvalKj0fwy2jWpw6Tpinyi39h8wGG1gyVFhIMsI3d0gRK8VqSLjUG1WaieEUf80xtQgIq/FWBoh87pg3DsaEXetYeIbR7ExRsRhvUj5b1WZIJVcni8BEwCqqxAdiq7L7GRI/elM9NOa7ZE0iPrUb1ehJWbMQNWux1Vb0D1+342XsUo/ew3negnVqACcYtb8UwdQVu6nrDdXvT1DT/xRkioOpHoJ4JrOYp2O6LVQqCuvkczYfNuDJK0z/W2QG0doa/XdT+28ez+RLywcp+XEYxGBJ0Oxen8mbPQaDS9QUtSh0nUy+tRDuJD/ec0nJKDsUMhdGkZgbp6lDGD2Xm2EQRI+jKAr6AfikEkYFWJW+HDsGgzsiyDqvJ9z8cfaqLjXj/h08GX14eqCQbS/rEeNTedkjNCiFus4A81YWgL4I4HISsNddsupL7ZqAYd7gQrbRl6QkoDGNr86Bs6kQuL9w1WkREXb+xxyH3KSCyfbqLr5CHo3AqWpYXIHR3B5lt2YdgqEg2g7q+MYy+5tRXW7R1SdI/rQ8MQPUn3/ShJ5WYQsBsRl2788VPsDbOzk8hX17O/5f7FzFS8MTZ0367/yXg0Gk3v0Ib7DpOD6XVAsCBBl54KQMN1BXScm4eUnYGUk4kuJQlXrIBt7hrU8BAari2gaZCF2GUC9mIJ0a9SMkvAsruRrFebac0O9gLqbswHQaD2lgJK5wzCVNpM855g4YSpsI7MZyvwjx1A44gwQopBFQVKTtdj3l5N8qcK7f1CAXClhFA9KRTjZ+uwNCgY2vwYShtQ9Tqq7yhANJmou7EAcWAfpH45IAg0z85HzR/UfX62wlbErFQcW5qwFLdSPbs/9dcXBIcGVTXYO1RkpL7ZSFnp6JIS0SUlBocA++XQeFU+4ndreUnZGdT8KbjKhfHTtSTdtwJECSk3K3h/ZhrsKvnJBFV3Y8FP/n3k7YVagtJojmFaT+owEPv3wRtrRf/1z3/YCVYr3uRwhPhQIje5EJdtQibYAzE1+lD1IEgSu+6wkXXRCgS9AVWWMU4ZSsAikvWiB6WmDusCG6H3OUCvxx2j0nnWKAQFxCILbcNMmBqD3z98qVGIAYWof5ZiD+ipej0dx4cbsb/jpfGifCLmbkGe0p+2C/KJ/LYcnyMZQZLwOgQaRhixl6YQ/eRKwjNHsOu/A0HwY6sJQfKrWAolXHECkZsCqIBoMqGWVlJ61xBUEVL/spLYwuLgcb0OdVBf/A4DxtW76cgNRfSriNc20LwgHleCQtbASixPhdJ0Zn8i39qIJzmUpBd39pjoLEgSXTlhmAslii6LJXpDDJZ6L36rDktxC/LuPT3e79jHgsOFgtGImJFCV1Yo5vnH1soZGo3mwHpt08Nfo6Ojg5CQECZw8jGxwKygNyBIIorHc5APECi9P4/Uj1yoBpGASaIjRU/ALBC93kXTQAvxn1YSKK/EM2MkzliJqDc3k7hQYPE3A0EAhOD6jIEYH6Nz9rB8czYxy0UiFlVSeVYKiR/XodbU03juQEQ/tE9zIhRa8Sb6iYppJ/LiNvB6kTs7EQf2ofi8UAAeOfVV/nPdLADM60vpGpOBfV01Ye86KXmsD4ZOGWesjvCXgtd3xMF9aRwRQsQLq2i4Op/wXV6MG4pBp8PfLxndut3UXzAQU5tK8wCBqA0K1SfJmCoMhO1UqButkvNiB/5wM61ZRhS9QPyHZdScnErkFhe6Lh+C29c93CjodFTeNpKkr9oRdlfgmpCLZcEWEEVUn2//5fR2O01n9sdvE4h7cbN2/UmjOQYEVD+LmE97ezuOH64T+iPacN9hoPp93QlKMBqRoqJQxgxGGTvkAA9QyfpfKYKsUH6CCWOLl6iX16PvUim+UE/bCC87/hqDOLAPTf11JF9QTP1Fg1i4dADDx+9CNqm8cfb/OGHyBnIfaGP7G30x1+gYeuMmUuY1kzyzFDq6qLtoEBFbupD8KuEOJ0JAIGl+8E/eelwGQlgIdfP6UHizBdmoMnPyap6rHk/lcXp0Lhk1MYamfjrcfeNYtbIPoqxi3VGPqVVBys2i+o4CqqaE4owTgmv7remgeoKBjsl9KLwrC12bB2SZqGdWYn97FeZ6AXO9F3u4E9EL9ndWkftQFWWnhoEKkg+in1yBc2ACjooA1eMtBOxGVJOe5tn5SNkZCP2ziVnrRSyrRbRagu+5yUjjeYO6E5QUGtI9ZCharZCWgCpC/DfNP5mgdEkH3tNGo9H0Di1JHWZSVCSu4alIbj+S27/fNs7TR+HJTaD4XCsRW1Vac22IZhPNY3zobH76XL2dUbkl9H15N+4Emc0bMmjrq7DorIfZ9XofAAwoVLlCyXyrgplXLiZ6g581zw3h8fgVzM/6lOKbMoh7Yxstf/MSMAo07IrCMLSVb556mpWD36H+RB87b4vnuKRCEFRGDt/NZRHLqJyXRvQ6FUUv8sT850CAhiEGUKG5r0Tx5QlYP9uEvLMIe6WCO0Yh9aM2ANQNOwnbpWL/fCvhWwWUzTuDyVsQQJSIeXwl4rJNRP/X3P1eyLFhJH/WScAsMfHGlcgThmL8fC22jVVEbgtQNcmMYtZjqwmw+x4HbQ94EVTomJBF4Z/S8IaIyG3tOEp93df65Oxkym4eAIKAkJJA0QVh6Dwq8vbCn/zbNU1I+tVzq9wnj/zZ8nqNRnPwftPDfWMm3EMg2oalzveTF897w/fbXHxf2SbodDC4D1JjOw1Pmom6Harul0i6xU3T6DgiNrTgSnGgc8vsuUBE6NQRk91IQXQps8JXYRJksvUGsj+5inlTnuCCTZewddRb3a+3xefBpei5aO0l3D7wK6p84eSaq3m6fAJ1bQ5UFfxeHUNSK5mb8TUn7JxO5aJk3AkBItdIuKMEvINcpDwnUnKqgZzn23BmOKg6TuD2yZ/w/H9nYq8OUD1eR9arLQQcJpxJZvROmeZ+euzlCpHXlNH6WAquSBFnIqT8LTgkWP7uAIYnVrLrxVxiFtaiigK0d6HGR+JMs+MJlWjPgoxX6qGjCyUpGm+kGZ07QFe8EVesiCpA4sd1yGFWWLMVQW+g+F9DyX62EXn3HqSIcFSnCzU3A9HpQbGbUNdvD773w/ujbtgZLNoQBITh/QFQ120LFnN8R5eWQuvIOOzvrDr0v3tMNEpL20EX0mg0f1R/iOG+fzz+Issfe5Y/v/IartNG9dz/6CiTHI6e38JFoTtBSaEhIIgE7AZqTkoialYdgVAzvs1hFN3v4LTbv2bnjQ6qJks4b2/HEuImfqnKSQnbmb9oJMOMBq7dfS5rvAK7pz/DDbvP4cXBr9IgOxmz5TQABhpM5JkklhY8TYtsxaUYOMvWzsJ+85mZuZVdY17nuYLXuCJuMQDDwitQJZhdsISI8yswjmki5BszaQ8Ge1aN+WFM+ecSFp38CP/+YgZ+m4Bl1R706Z3svDYEfVUznUkilz4yj9gTKln66FPclfQp7jAR79QOAlYVKTKCupsKiHA4WbWyD2OuWUv0W82Ev9ZK4aOJeOJsCDI0TfIiG1WKLo+h/NJM9pxp55L/zqPsJBMBkwDjW4l/fB3Fs2NQDBL+44bRfsbQ4EW5hiYAnAWZ7LlnCLLDgD/GQfG59mBCGtKPyuMdCOJ3fxtBxO8w4HcY9v7tQkPoOnMUit180AlKCg3Z73G5vkFLUBrNYfSb7km17k7HYQ/m2XbFzQV7TkU+hyO68OmBtF6cT8S7303mFSWaLx1J9LvbcRfk0JGsI+rVDbiPH4Rtcw2el0XKN8WTOaeTjkw7//3X4/TXq9zTMApFFfj0kzyeOf8ZJpgVuhQPNtFEg+zktO0X8GyfN7mt9HQ+zv4ESRC77/+l/KpMk+xmVuF5LOw3n+N3zqB8dSLTTljLZ1+N4D9nvswNK8/FstmMqUXFek4tVZvjGD92KwvX9CPjfR+JDxbzcvJSAP7SMIA5W4cjiLBl/LP0++oadA164lbJiP7gPzFLWQeFl4VhaBOxVqm0Z4GtSqB9pAd9pRF9p8CCax5i9Ie3kvylguP2SkaH7+G1t44n6d9rKH2zL44vrXQlC5wwYw0fLx5O5rsuRE8AddtuBIOBrqkDSLi1iDUbskj5RMawYCOCKPzkNiONV+WjigLRz6zunkAtORy4C3IwfLEW70kjMH+ztUdhTPPsfCJfXXtUty/RaH5PDrYn9btJUgAD15xLwnkVvVK9JWWlI3h8qJ2dyG3tKGOH0JVkpH60SsLX0HJ+F36/RMalxTSfOYiwCyvRXaRSfGUy75z/X2QEhhkN9F91HutHvULuB9dRcvqzRyze2kAX9bKewUZj97E9/i6aFSNtsoUBhlZm/OM29E6Vt/71MJcVzaLhi0QGnbaDXS/n0j7RzeMj53Bv0XS6vonBNdhNUnQrkqhQUhrDoOwKnH+Lp3qMGYZ0cGbWRua/MJ6/3/AaNy87m4hlBiZeu4pSZwTb6+I4NXMza68fStNtHqIeNCK6A7T2d+CKEbDUqwTM4A0X8IWoSJldqCokPGOgpa+RfrN20HycD7VvOt4IE2VnCOQ+2IQvKYzGgSYSXt5G/bn9iFnUiFxYjBQVReHdGRibxODcq++IFgtibDTVJ8UT+8w6xJQE5OJSpOwMlJJyxIzUfSY1Sw4HgsP+8+sLajSaHv4Qw31e1Y9X9eNXZTIXXUzYC/ajnqAEnQ4EgY6BUfjSo/APSGf3UyMJWHXUjVUQvAL2GyrRLwpBFFXqLxxE4yiZ3cVxtOclsuriR/CoOhY7++BXZTaOeg0dEgXDfvoi/69VFLCxyJXT49g3rmwWO/swxeInTmejz6U7Cbm8kvN3XshXuR9y62Vz6WOrY8HfHmFIciU3vHspAFtveYrdE1+k+et4wowuBL3C5QmLqZhiQjeila9GPkOd10Gfc3YxxlSP0Knj4bueZd43eexpicRuCfZQii/Sc2LyDmrHWJEf7mDyLcvZestTRFxaTstQmYcve5HV5z9CXlIZfWPryPzXDroSVbKsDdRfNIhRL26kbpQBwSdS/E8HZScZiH1sBU2n9SPqlQ109I9A0OkovjmT9LkeYtYFC1tEi4W2C/NR+6ZTPDuOmMdXoPp93ft8ybv3oAYCFP7Vvk9hhRAWgj858pD/DoLe8PONNJo/sN/0ZN7RL1+NAROeKIWsv2w6+HlKh1HFnSMxNoPkV7F/Xojq89G3NAaltY0+VUnsvjSUDq8Jd4zKq8NfYfbG61k6/VEeaxrLpxX5vNGRywm2HdwSXsJd9UPIMDUwO6SON1IXHdG4x5lgnKmsx7FZ9hJkVMDMdp+bMaFFvFRawNwBLyMJNmJ17VT5wpl6162cdvvXPHr+fOIkC99/13EPdjE342vejg7j5rmXEDu8jk6PkUSdjWavlZsTvkIBzAldfN3Zj2/P/jfPNBcwt3Awf4lax9v6EcxZncdn1zxMkk7ELBj4S8Mg/pn6IX+94hzuXXIJHad3sj3/Tfq8cDXeKJmIItg6Op6uJJizYziZTxVi+kBkU2kSqV8Fh+Ii5+9C9noxtgVQAwHilwcQFBXDF2sJTBqGbuEGIj7agWAyET7UjS4thUBp+T7vWc71Zcg/GngIlFcilFce8H0OTB6G7psDT/Kuv3J49/qEGo1mX7/p4b7ensyri4vFlx2H36bDvGgHu57oQ85TPsTSGlSvj13/zkUICGD3ozcF2Dj6BQa/cSNbL/gfI9ZeyJaRc/jIaWGAoYE0va3XzmN/9vi7mNsxhFvCd6EXgltsbPF5aFNMbHanUGApIlcPYzZcQEAR2TjiTV7piGeHK57jQ7aTrm9BVgUy9cbux//Q460pvFc1lMERVVwWuZSBBhO7/U7K/KG8UBfcobnLb+TE6O1cGLKLEW/cwozjV3N8yHamWrw83ppCvd9BoqGFdtnCs4smEbVGxB0lEFIm0zBMZMiEQtYvy8HUKGBqUbGdW4Ph+PJgkUtcNNUnRhO7qgtWbQGCE5P3/FlH5hVl1F7Qn5hn16AGAsHFg+dsQ3TYCVTX/KL3Uc0fhLBy86/8a2g0vz9/iGtSvZ2k2i7Mx9Qqo+8MoBhE3JE6wpdWUXhjIuaMDj4d9hxn3fUnzvrzVyxrySDL3shIawmn2zqQVQVJOLZHW08rPp5n0j4kWrL2OH5H/WBOdGxhgllB/m6h2O/P5YfndVbJZB5Nnk+ibv8J+Pu2U3edxPycDzm58BS+6PMp9zb2JddczenWViRBpDbQxeiPb+X1E59mtGnve3Z+2QRq/pZB6RkSm6Y/xkavlUs/vZxTR6+l3BVO/X8ycEeIBMwC4Tu8lE/Tk/xlAGOLl+KzrUSvDU4obrsgH0FViVhUSeGNyWT/qxC5pXVveboQnKzcdmE+xg4Z27I9yE3NALSfn0fIG4desq7R/FH9Ia5J9QYpNAQpO4Ous/LwnNaGpbIL0SfTnqZn4m0r8KVEkvqpj64mKzGSkcapXlIMTdj0Xt7bOpTx5trg8wgipf4uRn9XQn4s+iBzwT4JCuDBmE1MMO9NTj9Mtj/8/d30bw6YoH7Y9os+n2IU9Pw99SNe64ikyhNGp2zuvr9NEVEllf6GnrsBv5G6iH89/wz3TPiQZZ4wQkU3+hg329ri2flZNjXjBCZfs5LOdAX9t5sg2ktHqh4UhZzHqlDF4BwqR5mHiKXVqH4/afPdyM0tCEP77r3+9F2yCp+3DUNbAKW9szuGiKV7Cya+n3+l0WgOn9/0NakjSZ4wFJ3Tj7o2uKus5HCgqioYjRReFUXGXDf2e0VKznAQSPVgNHWy/K952IrL2XVHGuZykfubBrNn0ssALGz3EhrqJPIHH/ppehvLB37QK+d3LMozSXzSEcd/Er7pUVafa7BQOvM5ILhSxV8aBpBmbORiRw1XbzuP4xJ34zQauX7dLEqOe+m7B8G/WzJ49fUTsPqh6r0+4IbOCS66khzErrbScUYnlidN6Lp8uHJj0bkDyHqR5qvzCSkNYLbbkTs6EC0WGmcNImptG1UTTaQs8iHa7SidnQTKK2m6Mp+ASSB8l48flkGIJhNqIKCVqWs0v4LWkzoA3bItsHFn9+3aC/pT9Pd+CAYD4dsEdLurEEur0DkFEt/Wc372WhqG6ij8UxrJ/WsZeOIujMLeDydRUPhw8Iu9cSq/KfdFb/3ZeV93Ra7nQkc1kiDy7ZBXeTBmE9eGVrJx0pN4VT/XVAd35L0tfA9iABJf2MaA2FoUn8TAxGoyXq3HuqeN1aNexBWrp+JEO6blu/CEG3DGGRB9YF67h5YZfQFQXC6i3tiIsmUXqfcHiyAaz+7fXZkX/epG4p/fhOHLdT3idE8egJiecrjfIo3mD0VLUgfw42/Aca9vI6pvI57sGKK/qgCC227YKxQkn8Lza8ZiGtZC8axn8MsS68pS2NKZwHJPcFjsuJDtxEjG/b6W5pexiAb0gsTxO2dQ7N9blBEimtEhsbk5nv+2pgJw5eyPaX83knCDi9KpL1DdFULh1dHs+rONvDWz8Z3bgt+mUnvJAFxR3w0vTnZDWAghb60GgpN9hcQ4pBAHqjc45Bi9qA4pNhrRakXxePa7I7Px07Wo1XWoBYOCUxU0Gs0vpiWpn+E6bRS6hHjkjg6Mj4dTcrqENysGJSUWJSoUc7PM/c88i6XEgC8gsdyjkB7ShKrCEEclbzYHN92baXVhPAa2Ffk9+aTPBwwz9pxnJAkiywd+wE1hZcwqncijX51E7Z4ovlg9iNvqhtCyIZqwHQJ9k2txNllIC20BQUXyqBjbVcLe3UDMPCMdg6MRDAYEoxGdG3xJYRAdiWi10jw7n45B0XiyYxDDQrtfu3l2/j7zqMTwMKomWxHMZn5LOs7NQwoL6+0wNBotSf2YFBWFOLBP9237t7uQ6xsQ7XbaMvWY6nVICzcg2wyUzwilZqyODsXEc5c9gbQihF3eeO6N/4wQh4s7Iop4KkGr/DoS1nt9nF408yfb/CvpY6RYF2tmPsqqkx9lgKUSS/9WHrvrScKNTs4Yvo71u9LQOYOl6x1pIi3nDCVkUQmKJFB831CK7xuK5FMx1Hchh1spfDqH6JXNmBt86BZu6rHSRPT83T0WrAXwZkST9M8VKJ2dPw7vmBb2RSFye0dvh6HRaEkKvlsO57v9hwSDHtm2d1hObmtHDQRQOjuJ+d8KjC1Qc1sBXX/uQJDhxKlrmWLxk2eEuTf8m8cKJ5Kmt7Fh+Du9dTp/CMOMBj7J/vwn28gqbB37IpGSlWjJyt9XnczGEW+zsKsvS7flkGFqIDO9DjnLhTdSIXxXgLBXV6K6XHjPbyFxYYDMu9bjswsoRj0oKqErjOAPIBuD/+voUpLQxcbQeFV+d1n6DwkB5Yic/5Emt7Z2r2Oo0fQmLUkBndMG0DDru/Jho4GymXuTFoCUm0Vg0jBcp40i5snVWGsVXF/EYGpSqXCG8YXLyF8bBnNTyZmsH/FGL52F5sf+UXsihf69H7Rxsa0A3Bmxg9TUBi4JKaO4LIYTsnaCALYdzbhPHkn9hQNxeYzUFujoPHUojnI/nZk2OjKtmNpUdt0Qhc4ZAEWm9Pwk3AOTiHpu/1vSi8s2HY1T1Wh+t7QkBTi+2knM3OBaeYGSMrKerER1u7vv7+wTTsNQI16HiC45gfA1jchmaB/v4cK4ldz60myMYoB52fPRCxIfOm20K+4DvZzmKHk5ObiSxfeWD/yAD5023KqP3NB6+nxxNdYwN4/EL2PG2HWYXuigaorA3295FfunNqzVUD8Kys6Cvn/aRleCSMNMD7n3lyOs3IyUmUbam1WYixrRJcQRmDQMKTcL0WLpfk0pJxPn6aMITBrG7qdH/upNFY8kQadD7N/n5xtqNEeRlqQIlhgrHV0ANF6dT6Cyqkdln6IXiJ5ahTNeoPD/Ikh4vQ5VBLHcxA53AvGTK6nxhuBXZfyqzKKOPri0oZJjziK3yM0Lz+We+gI+39aP84atZlvem3jUAJ8X92Xb8kxS58vcuHQW6ZcX0jrSx6MzX+OTyY+z7PNBOMpkoj4xgaoiWq3IkXZ8ieHIEXaUMBvt6Qb80TYEu627PF0uLMb6/mp0364n++o1+1yz+p5o3XfS9CETJYQfrG6PIFB3c8HPPkzQ6fDGHlwcosVyTCdcze+HlqQABuUQGB0c7ot5bUvP+wSBuplejNfocSfIOJaZaPZaEP0QSPDy6s5RjIncg0MX3NdpjVegyWsj7idWWtD0jtEmP1unPc4/Y1axa8oz3BcdnKg9u3QGQqGV+05/i8rj9KQnN1DVGUryBxJ3vHExzzaNg36duKIlQj/cQqC+gd3PZlM93kbNOEtwh18g5ttaxMUbURKjqLxt+EGXnQs6HY2zBh6285Sy0vBM/tHzHcSlMcXjQf/1gRfD/aHW0wYiRYQfQnQazS+jJSlAXb8dadEGABSnE0FvoOHagmD5cWICiW/rob2TWaNXYDuljqZH0pg2awXPjH4d0yobV4ev5d+xwe3rR5vEI76CuebQ6AUJm2jCIhp6TAcYGlJJ0jceFrT251+nvEmb24TLp+eGR95G54YqVyiBPTbahviovH4wZf/Mw2AMoIqQ8l4d1XcUIAQU5Ag7bRfmI9W3Eb/cTcWdIwHQpSbTenF+cLhPlBAH5SJFRnS/vhoIEPH8ysN2nnJhMcbP1u49oKokfnTw+13p0lN/tk3IG6v2Wyii0Rxuf9gkpYwd0qPU/Hu69FQ8xw0i5rl1iA4Hu69PoiNZR8PMDL55cDSW6dV4woITSL9sH0DeuRv3u76d5tjnV4NDsteGbyLkvkrqPXZmWltZM/Rtlgx9lenW4Ifwv1PmoXML2COdTDhjPaJPQBQVsqbuwfCCE9kMu2eHUzHVTugba6ifmoxuQzHhO2UEnY62kfGgQvbVa3CeNpw954YSyEkCMfjvqP28PBquLei+fdgJAq0j4w66eevI2L03fhBT+/l5iKZfvgu0RvNr/GGTlG5TMewu2+e4XF2LZUM5hU8Mou6MTLIe2o25WSFst4e6cQqD1vgwnVsX3OZ9Tz+eSFh29IPX/GqL3CLZH18NgF9VqOoM5U9JX3BuyQk83JLD6EduYeTDNzLytC08VH88St8uov9rZltrHKkfteEtcTAotIpHUt/njYv+iyOrFXVgcC5Uaz8VNTsZT6hI3dUj8doFIj/aBYA7QiRis0rdKAsNV48CIPzLIuI/KAnePohEpYuNQcrNAkAZM/jnT1ZVsb/z8/P1vl8Zw/723rY/jCn8yz0oXu+BHq7RHBF/2CSldHYi/KDMHKBjVh4MyAK3hz637MBRFqD+9GzcESK6ZjfRKyS+eTKfjk/jaPLZ+HTU0917JeVtOoNWed+lcTTHpv6GTu4Y9ykAkZKVVYPnMtzgY09LJKc6NrHoloeJnl7J9THf8M+4r3l02Lt4w/V0fBBH2d0S4dshWt/B9Jdv5+KnbqJ/VC2eZjNSejI5z7cQsBtxRwu4Y1WcCQJFT6ZQ9OpQol5cS90EmcQ3i4l7pxApKgq5qZlAbR3RT66g88wReE8aQec5eT9dmPD9fdLeNlJEOC2X5u+3bcN1ewsn6q8P/i5PHIr3pBFIYWGIJhMB274rokQ/uaJ7vpTc2Ij3xOH7HYH4/nV+OIyp0RwOf9gkBdA4I7PHB4GxTcYXasQ1Ogd5cBaiX8HYphK3sJHyk8MJe3cDXckQOr2G0o4IZhee3z1ktHjQHMIky4FeSnOMiZSsXBXa8zrNNr/AgOgasvVWwiQLC3I/ploOYdSnN1NgbKHmNB/ZFxRy76CP0DsVZtkLuW/WGziTZB5M+IxBueW8tvAN2vuFETBLuONlLprxLRNnbCD2PSN06Cm9ZwR9/1FN/SkZFN6VRfnlWSBKOM8I9qrs763F+PkGAkYB8QBLKbWOT0PesRsAcfHG7uNycwsRr63d9wGqSuySlu6tRGKfXoOg01FTYGLYP9bTNiUHslORvDJyXn90aSl4ZowMbg75I8YvNqBsLQRRouvMUT3uE41Gmk/MPvg/gkZzEP7QSSr8pZXdJcFSvxyMX2xA//V6TF9vpmqSBdPmCkI3NrLrz3ZiJlRT9K8hzJi+ivKKSNIczTyd/RZ6QaLU38VmXy+fjOZXG2nU81rKkh7HBhiaSM+sQxQE9kx6mbfTvsUhenDGSkzeeAkAEemthEtGrk/8mkjJiuGKOmou9KFvE6nxhpJpbsB0XQ3m+C78KV7qp6XQOdGJEhog9dUypPRkQjbUI/bvQ8c5I/BOHUrYqytRXK799loahwjseSSPyrn96Torj5rbC7rnN3VPnRAlpH45weOCwJ5ZYQi+4H2FTw3BO3kwiQudbL1hIB1pIogihj0NtGWZCcSEYNtah+L2dD9eHNgH0Wql8YqRoKpIOemEbG/tjqnuxgKE9GTC5uxNklJUFLq4H1zfOgBdajJSaMgv+2P1IqlfzpG7fqjZxx86SUFwzx8EAVeKA0EM9qpUvw9zvUrbpAx88SGELzHS7LQgxHj45KN8dE16Ig1dvN6ax/TdJ1IjW9jhTejlM9EcCTUBM3Xt9n2O//X6N9gw/B1Ot3Wwdui7GAU9EaKLM/Ycx8J+8/m84Emmn7iayyKXIAoKC3I/5s2hLzFr4FraM0G3zUbsAh077k6k6IpY6o6LozE/DL9ZwG8VEa1WWi7Jx5XiQLTbabpy7zBe+l1rSFikEP2SGVUEX4iKM9OBFBYW/OmbTfusEQx+Yye7Lwul9cI8RO/eEYPwuHaaBhloHGpFt6Oc2NUeAHxp0VjrA0jtbgJlFah+H640B6LFQkdOCIrTSdQzK5Fys4h5uQ7Bs/ebmeRVcSeHIOh0e2MNc6CG/3TyES0WArGhCIdzntiBCAJNV+xnOPQXciU7EKRfmKQEQSs6OUR/+CTVdupgdLExGD9b22MCb0iJD2O7jKCqKAbw+XSYN1rwZbgJRAT4V8x6vqzqw/Pp7zHaJHKxo6EXz0JzpOSZJLbnv0mIuHfobarFy+m2fRdf7WfQ8UzqfG6qHc75Oy7i3pgViIKKSfADcPn2C7gnahNiALzhCnVT/IRvlhg3biuSF9yRAi0DVdrO6UJxOgnb7cZc5WTPXf2J/aqm+3UCEwfTlq7D9M0WwpZVYmgT8IRKGD40kPqVG1QVV7RIgrEVW2o7hnPrcZSoUFxBYNIwomfVEv/QCmJWd9JwWh+kxZvJe3UTnXd2YllZjDMjDHniUESrFWe0RNE/BlB70t7/N3bdaad+holAaXn3sdjFTRjrXSgeD9FvBeeNybv3IG8v/Mn3t+7SwXjDjASqa3ocV0cPPuA8LMnhQBk/5Cefl7yBSDHRPY+pKtFztv304w6C8fO1qP5fNnQiRYTTcubPxAzoEuIRhvU71NB+l/7wScoxZxWB2jp0aSnoUpIAaL4sH2e8AW5tCBZMrO2EnXaMLSovj36Z2aOWstSjY/2wd4nT2br3jNL8sekFiUjJymBrBSsHvY9NNBEqBsgy1gGwdui76AWJwkue5rapHzMpt5ATr17Gt1tyOe6m5aRMKWPW+OUY9QEar8pH1+bGG21m6vHr8KZGIAzphy4xAeOmUqI3uGk+byjuPrGYmlQsDQG2bkxj1939aR8QQcACDy+fSuz9OqorI4i+pAwAd5QeeUA69qWRNAy34QsR0MXF8NV9Y7E95MDwoYHGITqqrvZj/cKMM0kg69U2ohfocZ4xiuoP+hG60ohgNFB/XbBX0nVWHorNhNQWXLVFjAij9aJ8dHGxFD2WR+Vfeq52IRiN3cOAsgHU74o/dIkJCDoduoR4qiZYUD1edEmJ+xSQqD4f+iYXdTceeBUNqcUJbk/3bV1qMrqEeFTP/qsTv/9//+cIw/vjmzrioNr+kNzUTOjrPz8XTnW5kVqdv/j5f8/+8Enqe43j42nNSwBRIuKFlYTudlK5KZ7OnBAEX4CATaG1v4pLMbKtM56/FJ3S/dgHKk7qvcA1x5wf9qqTdTYmm3sukTV5x0yuCq3muaQlTLTvYNLAncxZO4qde+J5a1kB7g0RdGSquFIcVEzV8emSYZRNM9LvhZ20jEtCae+gI82EIIOh3UfKpUVUTdSh6xJQdAJj7lxFwKKS9o7CnrOsxC+QcOg9dE0dQOoNhRSfbSba1EXAImBsUym8JZna6X5KTzbgDuiJGVfNrjGv0+CyYx7aTOEVIUQsq8bx9S68xQ5MrSr2d4JrUzZcW4CiA7HDjTsrCmX8EPSvegl7bRVKTDiiH8J2B7/EdZ6Th2i3I4aG4ByahDiwD5HbfMjGYBLqGJmIYDbTeFwKyQ+uQXE6actPRDB8t2fYd8lK8XiQtxcS+9iKA/4N5N17kDv29nZbCuLpHJ6IGLLv0C1A89gfDdcfoLJSXbcNwxdrCUwehpSVvv8X/xXLRcmtrQRKyoI3tOtegJakuoW9shL7O6uovy5YsdSeaSF2lYJjSQldGSGoMV4S+tZzy6YzOTNqHcsGftD92EmRPz2kodH80H8y3wXgispx3Pzklexqi2bK4G3ExLeR+3/l6Aa1EZ7bjOlPNWS+4yJpgYwjp4WPCgdyx71vUP9+Jg0TffgcAmNeXEvt/zKQ7TJRGxXKT4G524fgD5VpHGpEifTjOq+NISEVCFc2sLEmkZemP0e8sQ3vyC7e//u/Oe+4pczsvwU1zM+D6e/zWNbebWa6XEZUvcKOO+OwfKxHTXTjmdXK1IhtCN8NINSNUWgYF0X/+zdjqGih8ck0hMF9Eeuaybp3O7Z3g/OuQheWoDhdyPUNGD9dS3vfUAytXmzlwZ6D5YPVKJ2dhG/poHVWsLdie3dV927ILZfkIUVGIFosCEN+2ZBY2Cc7sJZ29FglQy0Y1P27J0wEQUDNDx5znzyiey7a/hg3lKBWBocodanJwR4fIA7KxTt1+C+K7UDqrx2lrY8I/OH3tG4/L4+IRRXdY+KxT69DjIygNUdAUESsHzQB6eyZ9DIAE7efzDZ3IgOMa8nWBy/2tsu/rV1XNb3r+5XZX0xexqBxSSwa8B73Nw1g6fIhKC8IeDeH4LSqeFJ06AdZac8Epc3KRYNWMdRYR0JIO7oPw4jY1snL6RM47c7VDLBUEntcO681jObUyA2MNdey1hvB7ZtPx2Twc1v4Hm4L3wPATp+LNGMjhWNfA2zE6du4PGw10UM7yTWI5C64ipAwJ1OSd2GK9fPa8tGUnvIc9zflMDNqEwB7vDF0JatkvdKEtS6cwX/ewCerh+KYLuEa3YXhZTPuvmmYWmUsK3YT6JuKM8KI6eMGih/Nw1olYmlQkE06Tnx6MU99MYXMuzdScdswUh7eQOhGDw3XFiAGVHQuaByp0Ofu7TjH9UH0B7Njw50FJD7QszclRYQjt7YjOWzIbe1IYWHIbW2oqfGUnRpOWnU4FZf3QfJC1CYPhu/uT5hXTvOsUZibZVpuLiDhkzqUsqqez9vcErzxgx4dgKqTEASBrrPy0HfJCEpwwWA1EOhOsIci5vED9xT/SARVPcCyzMewjo4OQkJCmMDJ6A5hS3ZBb8A5fQiWeasRdLoeBRNSZhqu7EiMTR5EbwBl807EwX05+a3FXBVazegtp2HT+9izKREp0cWOMa8gCVqHVHNoXIoPi2jo8fuIDWexeHBwX7Lxm84nwuLkX2kfMNho5PLK0VwctZQUnYsztl3Mf/u8w433XMff7nmZOQ15xJnasUg+2gNm+lpqaPA72NCWxOz4pdT5Q5kdUnfAWC6pGMuTid/Q78trEDt02NLa2TxyDl7V32Otw1UemVmfXoM5votlI5/nnvrxXBSxnHvKT6ZoeSpZo8vYuSkFIdpD4pt6qsfpMLQJJD64GmFoLkOf30Kz38rO/xtAw1AdQ4/bydrlfci4ez27Hx6KuU4keoOP9jQ90S+ux3P8IBBANoi4I0SilzaiPO2mckFKzyQlSrRcPJKo+YW0TslG8qv4zSLhH2yh4byB2KsCdMXpiP5kD7tvTyfnP5U0HJ+MzqvimLMaQZLoOnkYzgvbaatxkPtoE3JRCQB1NxcQtsuP8fO1iFYrracOQOdRsc5djTgoFyGgoBTuQcxIJRBhpX6klbDCYPsf0qWl4E2JwBeqw/zh/vcg+6MIqH4WMZ/29nYc+5mT970/ZJJCEJAy07r/AUJwY0N5VzGS3U7pzf2RTSqZb7TQNCIcb4hAV5rCIye9wV+fv5CcGbu5P3k+Z2+ajdXo6zH0p9EcTv1Xnce2vDcBeLA5i2c3jkWUVIonBnv2rbKLYR/cwvBhRTR7rDyQ8T7pOh+RkpWdPhdWUSFZZ6Mi0IVTEck1HHjC+VcuPRPNHvSCxCK3iEX0Eir6ukcMfmiJB4YbfFTJfqYtvxaxzEzIwGaGRldS0hnJ3amfcsV7V2JoE3Bm+AndpKdtQAAUEGwBxEYD0WtBNgg05MnY4ztRl4ehG9NC5+4wUj/z0tLHiCoIuOJUUv+6EikrHcHjQzXqaR0ZQ+iHW9jzl0Fk/mt7j+tPAJ1n59Hvlq1UXxBL7b/1XJS5iic/OZGsf+/G3y8ZIaCiGERacoyElAVouNwF60KQTZD8hRNh1RZQVZquzMdWI1N5goClSiL5oyYCIWaGPLWZJf/OwzFnNcroQZTNNCMkO0l5WkLq8CHVNePJTcBU2kSgtBzBaERMTkDw+AjEhSG1dCEXlx7Gfym/PQebpP6Yw32q2iNBATgzwrBUWBDCQgiYVbL+tYP6s/sS/fpm6i4dTOZNq7ir+UKEUe2E6D3ESCJDY6p4LmkJ2qU9zZHyfYICKHZFc9GgVZwRsh4IDjGHSRa2nP5fzILhux69/rsfeiSk5IPYOmaKxQ8EL9ZPMCu82xWJSfCTrd93ua9xJgAD2aKBLeOeY7zjfNYODV5ru6RiLI9WTWH+OY/sjeG72qJ7G/uiICCrIpkn1fOPVTO4fOgyXtpawOmzVvDu2hH8Zfo8So+P4pMXx1Jw4QaiDJ0sWV5AxQkSGe+6EVSV5gEC7emDiV/uxzU2B1UMFo1YP16PFBVJ3Uk+ppmbKcroS4i5kefemsaJp65l24KBNAwz4repiAEBa5VKS66eucOeZ3rlLRgbRfSVTdTPzqMzBdRMF0PTi9H/OYf2NJHOnDAaznGT4LUTtrkVQkMpP96CKiqkz9qO89ThtOWZSXhwO20z0wgRI9HFhuJz6FH0IrZtwZ6sXFwKgkDb+XlEfLUHuV6bwnIg2qfrd0yfrME1qR8VZyeR+rkHub2DqA1d7PnbIAJmEO12PFkePOV24kztHL/5Ql5MXqYN9WmOmueTlnNP1A76GXpeAz2r6DR2+Q//wq9n2dqZaXXxeGsKe/xdB2x3WfkJXJ/5bfftl5OX8kL6+2Tq9268eGvtUABuiljPOSFr+bwyl1GmMjYe9wR3RRZSPPFlltRlIFkDFJhLCNG5uOv6N5kcsoNGn52qSTpG5+1AUFWkpk4y7ttC6tO7kNwK+nY/gWubsH+zk5obR9I6LpUtk55iom0HyX8rpGZ9HN9c+RD/iVtN1SQD6sh2MsaU47epuKd1YK+QebhuCgm59Tx+6bM8vOw9Ui8qAmB2/xUs+2wQZZepjLt2NX6rQOaN9Sxf1J9dt1tJ+tIDgkrMGlDz+lN9ooxnoBvvtBG0jPLjjDegr2zCsr0WT7hEoKwCYWMhgUnD0CUm0DxIBZ//sP/tfk/+mD2pAzB9vAbTldmw0gqCiNjmRHLZSVjYQf2b8VhWmHjs8mdplm187O/f2+Fq/qB+OAQI8FnOZ3zfszoSkgzNWH6iyOyttIX7HPvx9jVZ5noAQkQzXslJbkQDUZJKiLi3t7dy0PsANMkqT28Yz90jPiPLWMet0V/zf+d+y3xnKpuedLG1NZ4os5Gd7/Vh9HkbKLkiA987MXS8C+Mi1rOmIZmp22ZRUx/K46PfYvwpu4nT2Vjv9WFsFXhx6Cs8WnMCT5z6ElMtXpYPUQgVvfRL/v49tDA342vuddTwzIoJSA6F4okvk/HNJYSf00JxTjrnnriEFdeO4KvzByCEKtRMUEFnIP4riZDFFZRemQmeAAEz+FKjQBAImMAzYyS2dRV4jCKVZyWT8pm3u7hCioqCUPs+ozx/dH/Ma1I/JAg9tvRuuzCfhgKZvg/Uonq8dI5Ow7a7nb6v7mbeihFcNG4pry4Zy6KTHzmoIRSN5nCTVeUP1YO/p7EfIiqtAQt9zLU8uPJEjuu/k8ujF3HdjllYnghF8imUTdejivDnKR8x2xGszPvh+3RJxVjujPtiv9fYfqhBdnJN2cnckPA1F392JYtm9vx/fdzWU7EZvHhlHREmJ7uaokm8SybyxTqui/2GS567EeuYRuxGL1a9j67/S0Tf4cMbYaTgvtWsvHMUnjCJ8KWV7L4hmayHdiM3t+CbMoymQQbiH/pRVZ8g0HHuKELe37jfakGpXw6BUDPC8k2H/ib3Aq1w4iC1XZBP5Lfl3SXoUkQ4O/+ZiegVyXzHRd0oGwErBCwqAZvKizOe4/XGAp5KWtij4kmj0Rw5XYqHejnAG20jsUheTrZvYV7HYN4pHcq/+81lgzuVOSXDOT99LbeE/7qeiF+V2en3M9BgYrvPzSstBSQbW4jRtzHBXMOJmy7h2yGvstrj4JrV5/Px6Ce5u+Jk/pn8ESk6gSvKT2RbYywvD3qVM766llvGfMXC5my2VcejVliIWynTnqbD2Kpir/BhrOuk9KxIwnYpNA4RSL/jBytTCAIVf8snbqWP9lQ9kc/tu2qFaLUi6HXIbe2/6ryPtoNNUn+cr2MHEPr6ymCCEoTg1gQ6HTnPO4nYLOCKNxP72Ar8NpWcsaUYEpx83jGQb7fk4lL89F91XvdWHRqN5sixiSYy9DbuidrBbeF7yNZbaQlY2TD8HSabZW4L38PaYXNo8v/60Q29IHXPZetnMPPv2I2MtxZyx8Kz8KgqekmhSZYZY3Jy6YAVREkqfez19NEbsYkm1i7twydDnydG8iH4RMJ1XYQZ3IQssBC7WqE5V8f5s7/EHS1QOcWAKzWEmLV+/GYBVYTiN4bQMSsPz4yR1M3rQ9LXLvRt3n0TlCgh2u0oTucBE1TbhflImWm/+j3pTX/4JPU9yW6n+eR+yPUNqBu3E761A3O9l6o7CwjdDWXz04l+1cx7m4eRnl7PPfXjuSBrTfemhxqN5uh6MGZTj9uSIHJ/zJYj8lr99Aa2n/QkyTobXw98g8caJ7LQ4+CuyEIiJSv3x2zpHlrceP5/SNbZOGPbxaw/5T/8ZdFpZFvrMXYoqFc04koJUO6OxG+F+KUBuuJ1tFzeha3GT9I3ASK/NBF1ZRn6rgCdbRZUUaDsZBuC0dgjppaLR9J+0k+vvKHzqAiB3/YX6V+cpJYsWcKMGTOIj49HEAQ+/PDDHverqsrf//534uPjMZvNTJgwge3bt/do4/V6uf7664mMjMRqtTJz5kyqqqroTXJHR/cCkFJmGlJDO/ryRuJWeTC1yoSUynTFSzjWG+l6NYFYQwfXhm1lkVvL8xrN750kiN2Trm2iif/Fr+Ukiwev6mfQmnN7tLWJwV7YqsFzCZMsfDn1v5zq2MTDDz3Fh/1e5+njXuWR+GUMnFyIpbiVideswvBJKG2ZBvw3NWNuClDb6QAVcv/RTPlJJtLfad3nelTUyibCVtf+5BYgtndXESirOMzvxtH1iz9hnU4ngwYN4oknntjv/Q899BCPPvooTzzxBGvXriU2Npbjjz+ezs7O7jY33XQT8+bN4+2332bZsmV0dXUxffp0ZPnYyPiunEiUtnYC1TXoVmxHf30drdkSISU+uvLc5FyznWXNGbQpAT5sG9rb4Wo0ml5iFPRsHjnngPf7VZmp82/hy66+5JmCq+RPtXgxCnruTfyYXXc7uD5yKbHnl9E+wstjOW/zyfOPI38agfvPbThzo8h8rRlfpJWiJ0YFJzTrg8lS3lmEJz0SwR5cNFfQ6Wi49sArw/9W/arCCUEQmDdvHqeccgoQ7EXFx8dz0003cccddwDBXlNMTAwPPvggV155Je3t7URFRfH6669z9tlnA1BTU0NSUhKfffYZJ5xwws++7mGt7vsJurhYmienoneq2L7eQcmf+2OrAE+EwLKrH9a2i9doND9ri89Dik7tsSfZDy3xwO27zgDgtX6vMrd9KAXWIkJFN2fNuQlDu0D0ei+qJGDZUUfDcUlEbOuiLdtKyJurup/HM30kFdMg+5rfxnJLvVI4UVpaSl1dHVOmTOk+ZjQaGT9+PCtWBMsq169fj9/v79EmPj6e/v37d7f5Ma/XS0dHR4+fI+q7xSK7hiejigLGNj9CfAwZrzXinNyF365SJ0PaR1cwbuupRzYWjUbzmzbQYDpgggKwCj7ibe3E29r5pHMAr2zPY6TRQ7wugD/Ox6Qz19J+YycV58s0j09E0UPC46W0Zff8+LYuLyLnJRfNs3/97sPHksOapOrqgkt+xMTE9DgeExPTfV9dXR0Gg4GwsLADtvmxBx54gJCQkO6fpKSD26DsUAk6PQGTgOmTtYS+sQZ9o4vCKyNpHRZJxh0dTJy8iWlf38Ad4z5lYf/3j2gsGo3m922Y0cAHmQv4IHMBt4SXsHbsM8zaM4NmWWB0zh6+nTuCGFsXmyc+TctJbmw1MksXDSBtXluP5/EMS6f4XBu22gDC8P6Ig/v+5OtKYWG4Th11BM/s8DgiV/2FH++kqar7HPuxn2pz55130t7e3v1TWVl52GI9kIZxfhg1gPZZIyg5O4zsuzZhag7gSYtk3YuDsYa7eXDViSgER0tf64jEq2rLm2g0ml/HJhj5a/LH1Ms2dr6SS8iEOtJtzdxYdRzXD1xEe7qOrCcrETtcVN5dgGfGSCSHg7ILVZK+CmCu6kSqakQoCRaj1d1YgKDT7ZO0lM5OHGuO/Gfpr3VYk1RsbHBL6B/3iBoaGrp7V7Gxsfh8PlpbWw/Y5seMRiMOh6PHz5Gk+n1kX7YOaUcZIW+sIu2DDjpmDkYxiLhi9IgzmrG9bwdZ4MziaWS8exVrOjNolL14VT9+VWbEhrOOaIwajeb3SRJEhhkNhIpurKfX0eY0c3vMN6ysSmWUpRhvKMixYey+LxR/iErlmQHUQIA+t1Vh/GYTuy8ORbVbUdzB/a5iH1sBkoQ73oqg27sSnhoIdC9icCw7rEkqLS2N2NhYFixY0H3M5/OxePFiCgqCVSfDhg1Dr9f3aFNbW8u2bdu62xwr6s/pF9ytc/MuuuJEnLESjjmriDyjgoBJIDW1gS1lCRhaRaw6L481jeUTZwR6QeLrwa/2dvgajeY3bIBBj0XvY2RCBRMW3kB+YhlnL7oKX5hC8U06HHYX8Utk4j7WU/JyJjv/mYKYlUbitwru9HDUITk9nq9+pB4xJ6OXzubQ/eIFZru6uiguLu6+XVpayqZNmwgPDyc5OZmbbrqJ+++/n6ysLLKysrj//vuxWCzMmjULgJCQEGbPns2tt95KREQE4eHh/OlPf2LAgAEcd9xxh+/MDoHr1FHYd7Ug7wyugBz53MrgttCBAIlzy8BoQBmUi9jcgaBC60cJWM3gilP4cPdA/E1m/n3qRpZ7FIYYtEm+Go3m0EmCyBd9PgXg7bBtROk6WFqagSHFg6vDRGuLjf/776v8q+REHHPi6EwVkHcWISUMw7h6N8oPpv0gy6S909j92fZb8otL0BctWsTEiRP3OX7RRRfxyiuvoKoq9957L88++yytra2MGjWKJ598kv79964a7vF4uO2223jrrbdwu91MnjyZp5566qALIo5YCbooIWWk4MqKwPhZcEdN/5Th6Lr8qHoRb6iezgQdIaV+mq9wEmF1keZo5oWkxXSpXgZ/egOIMGnATu6J/0JbgFaj0RwWTbKTO6pPoN1vYm7G1wDd18DndCbw5pUnUTrdhOSDmLUyOpeCodWLunYrgtGI6g+AcmzMQ/2etsDsIXCfPBJDRwDRKyOs2HzAdi2X5ONMFJh15re8tGE0f8+bz4WOpu7Vk1u9Fp7OmvOzqy1rNBrNoRqy9hziHB2EG1003ZBIya0SoqQgiiqxz5kwVXUg79jNnkfySFogY/hi7c8/6VGkLTB7CMzz12DYWo6u3XPANpV/LSDq492kzqllxdkDkBoM3PvZGfRdcT56BPo5ank5+63uBFUR6CJ3+QVH6xQ0Gs0fQIPs5NuhLwGwfEcmo17ciKoKZEY3IcsiHUl66sdGIEVGkHHrKiyFDTRelU/99cfWdf+DoW16+CNyUzM0NR/wfkutyq6/Z5L7cC2B4lLiVoSh75DJebCYqyumMyGskPFf3sy9Y+ehILKmM4+nh755wOfTaDSaX+qN9gFE6jqYn/0xSrZCp+LjjYaxEA++ZhOWJpnqiSLtf84i5xE9zaPjiHlzG4rTtd/nc54+CvvnW1Fc+7+/N2k9qV8o4oWV5D5az46/RaOOHozfLFI2Q8/u2/uyvSEWv6rj4pHL+aqlP3OqR7KtJY4JZqW3w9ZoNL8j3++ZJSIwbPXFREpWXp75DGWfp4GkUnVagPglKjn/qSCQEo2pRQ4WUigyzZfnU/mXnj2qkHU1KB7vz04A7g1aT+oXEIxGkGUCJWVkX1qGFBWFmppB5s2rkCLCEZb24dlVJ+GOVSDSy+oJTxApWfnQaWNu43AeTfp0n221NRqN5lCs60rjPHsD2/KCIzW7vPG44xSuGf0tT28YT0eShGVeLUJ9AyZJwnvCcEzVXUQ8v5KIHz1XoDw4qdcdb8W46eiex8/RCicOkmgyUfbnoT3LOAUB0WhE8XhouTQfc7OM3yLChY3UN4QQHdXBzMStLKjvQ7PTgiSobBzx9lGJV6PR/PE0yU4ebBzDp+/no3eBIkHMOg+KQcSweCvIMmogAIBot+MbkY3u2/W9EqtWOPEr6FKSeszM1qWloAYCpL3X3GOegRQdRdO5Q6i7sYDwl1Zinr+GjlQRj09PyfEv8VCfuQy3lFCzKp4tI+cwNmEPL7bH9sYpaTSaP4BIycrcdcPxxMpEr3Njm1yPzulH//VGpLiY7gQFoPqCW9fX3XhsF1Now3370TkkDmtLG+p3k+FaR8ZhLy1H3l7Ys6EsY+hSCH95dfehxAdW4D5lJLm+C0iLbObNzLmEDG0ib9MZyIrI/4au5ZSiE9i+Kp3Lpn3NHRG/vcl1Go3m2DUop4Jkawsf64YwxNpBW1gYytShuEwilrIKECVQZFSvF3nHbmJ37O7tkH+S1pPaD/OHa3rM1ra/s6pnA0FAHT0YAgGMrQF+zFLtJu26erjcyCnX3ITutQgsj4agfz2cj5wWtm5IY8SYXTy7aBIuxXeEz0aj0fyR/F/Kh3xRnIs1xkn7vck0X+OkNUeP5YPgl+nWC0ciRUX1cpQHT+tJHQpBRBVA9QcwtPtQAdFqpeG8gQSsAhFbvcgjUxECKnV5Er5wmVEDS+k8z8Z/r52FOlPlhrivuWLa4u4tqXOXX8CXo57GowraJGCNRnNIdvudXF14Pv8b/g5ftA/go5nD6XOXDHIDMiDoDUQtriHQ3IIUGQGBAIrTjeo/dr8saz2pQ6HIiMs2oTidqGu3Bg85nUS9tJaYVU5qxhkwNXrw20QGjCvCUqGj4n/ZVJyRSNn04Jp+eSaJHH0Hl1eOBiAxvA2TIHDZrvORVa1kXaPR/HKX7ryAj/u9xYOlU/lv3DpUgwL+AHvOj6LznDzK7xyO9fUumi8ZSdP0bLom5CAlHNvXybUkdZiIVist89PwhxjIeK4CVm0hdEUlm1ZlkfhNJ80DBeKWOxk5rIiIDSJ31Q9k8vO3s+zzQbzf5SDR2saFRWfR1Gkle+Fs1nuP3W82Go3m2LRs4AeEiGYW9ptPztILSc+qo/DaaMIKVVpzRL697CE2LckmenEdkZ8W4w6XCJRV9HbYP0lLUoeJ4nQSer8Vy656miYlU/OnAgLVNWTctpKG4XYiNykUn2ti/dIclFOb2XDFIGSjCgqcaGlidVUKRZuScDdaUDr1DDMGhwEvLB9H2kdX4FJ8jN5yGgBdikfrbWk0mv3yqn7yN5/OovynKK+PIGGRwvTbFzFm2mYKFtxE4iI/O2+LxN83kfCXVvZ4rGixgCDQdMWxswW9dk3qMBGtVhr7mIlYXkF4YzMRksT3aw5Hr+vElWjBUSQR8+RqRJMRxeUifZcdZJlBthtJ+dzLnlky5jA3J6VvZ5Fb5LadZ9DUaCc9s468dRfxzuAXAQsDPrueB8bP5Rx760+FpNFo/oCMgp75/V8jWrIxPLWcotmRvLV7OJ4uI5I5QPVYM6cNX83mtwfv89j6iwYR9fRKouds41j5Gqz1pA6RFBaGFBrSfVtxOol4cWX373JHx97Ga7biihKJe2UrZf8cSfNZg9DFxVJ34QDaZw4kfW4X1eNNZL7qx1dqxyL58KkSUdYuLh66kqHhlUgLwjhp8XUAPDbxTY6zVO03rp0+FxO3n3zkTlyj0RzzSvwmAP6a8Ck54Y14nAZ0NQYGJlajSrD9slwM2yppubRnjynq6e8+w364F1Uv03pSh0hJj0cIKNDWflDtI59diQKE7VAJeWs1yqBcwop8eMJ0SO1uYtaYqDzeQvYrzbwpjuXd9vGIg9pxGDyUPZ3N6BvXs7M9BoCZVhdgRVYVJKHn94xcg4WF/eYf5rPVaDS/FV7Vz78qp/Nh1pc83zyWvvZaVjqzkJM8bF2RiagCWwuRZZnwlxr3PlAQ4BhcgEjrSR0idf12lM07f/HjQt5cFfyHUFSOae0eQheVUHqfGWOTh5R7VhBwmEj5zI95eDPzhj/H+mU5SD6ocoVSvjGhu6BizJbTuL1u+OE+LY1G8xtnFPR8mPUlAP+NW8epjo2clb8GmoxIPvDbVDyfJdJ2QR7CsH7dj/OdMBxhSL8DPW2v0ZLU0SIISGFh3TcVp5O6c/qgtLShX2WnrsCO/7hhVE+0okoCOknhhM9vJuvhYhJuKOK59A+4d+a71MkOZFWhaU0Mj8RtoEF2ahOCNRrNPoauOxuv6mfGRzcRo+/ggZPmcNXpnyPIoP9nGKG7ulAMewfTDF+sRd24vRcj3j9tgdmjRDSZ2HPPEDJfqkcuCi6zL+gNwUl0goDrlJHYv9lJxTX98Yar2EsgYBVwR6mIMhw/ZQMbmxNoWxLL5NPWMt5RSGPATpE7hkkhOzjJcuCNGjUazR+PS/FhEQ2M2HAWHRsjmD5tNdNCN3P1e1eg6xJI+roLVm3ptfi0BWaPAYLRiJSVDoDi8ZDxTjtCQO6+X/X7cJ4xipIH89C5ZFSfD8kNWc/V4Z7chTNRIXw79BtbzKfrB+H26ZGNsLM9lr+8cT5jLcU8ErdBS1AajWYfFtHAEg98PuhlRL/AB5uH8o+bZmOuFXCUKXTc46T9/Dy800YA4D1xRI/hv2OFlqR+DUEI7jF1AKLRiDs9vPu2KgnBi5M/YJ27mvTbVyKbRQSTkdjHVyMXl2JcagcBuhIFKl7PpM8t22hrsZH28Fa6XkzA1AThoswlFWP5yGk5Yqeo0Wh+u5Z09aFRFpg4YwOiQcb8xSb0TpWI5bXono9E51bpSA4O+Rk/X4u6vudw3099vh0tWpL6FaToKFrPGnrA++WODgxfrsN3QrDAQV2/nUBJ2T7tfCcMxx0u0TU+B2FIn2BbARK/UUh8YAURz69EcbkIWWNk7PIG/BaB4RdtZvJzt7OmOpkTLAdXYajRaP5Y/hK5i5daRnNb9NfoSk3sfrE/X9zzMMd/shlzvRf7F9uIfmHtfh8rhYXRcu6BP9+OFi1J/QpyfQOhr6/82XbGFm/377qUJDpm5e1zf/hLK7EtKULYXYEuNoaYtU5sG6tBENAlJdJ8WT6xy1pYUN8Hv10g11qLJ0bmP4PeZdjqi3EpPgatObf7Odd4/fvEoS21pNH88WSYGpAE8Ico6CuMnHDfn3h2x1iE5cH1R6W4/a/dJ7e2EvbKz3++HWlakjoKvl+EFqBpfCKOOav3e38gNxnSEujMS0G3p5aWsUkIOj1NE5IwdqgIZTXUL06gK0Xh+XenkvKJzAOl09g46jUuLjuRqSk7uzdV/FvpKT1ew6/K/LWs5zGNRvP7937tUK4rPYP1p/4Hxx6wVwVwfGTrvr9pQtI+jxEH5cLIAUczzAPSktRRFvntd4s57ucfgLBiM7UTwvE6JASdDsecVah+H6GvryR0RSVy31QEGVI/9pP0jQtBVjk/cTWDn7ieTZWJlDojeHDTCfhVmS/6fNrjud/tiuampAX77WFpNJrfL7POz87qWFZ7w2gaKWP6ciMdGQJSdgbi4L77Hw0qqULaVX70g90PLUkdZYGq6uB/7Yb93h/zvxWEvraSQHVNcG6Vw0HDtQXUn5CMO9ZEynO7KD1NhypA6WkSD3x2CrpRrQiiytqd6Ug7rdTK7n2e969fnfHdRVQ7tYGuI3qOGo3m2LG1JIH/Gz6PO7adhmOXjobLRhC9PoAcbiVg+9HnkCjRfHk+otXSc2m3XqQlqd6gqui+Wb/PYWFIv2DJuiDgOnUUkt1O88n9iHlhPVFvbwEVGk/O4fgRWyg5zYSgCCg6iPyfBXGHjdTUBix1KpcXncOF5eMAeLItiaHrzub4UVu4InwlffVNXF5y5tE+Y41G00u2Hv8kJ1jqGBpbRUhZgNbBMuc++CmqICAu29SjrS4+FkOnSqC+ocdxQW/AM2PkUYx6Ly1JHUOk2iZoaQNVxb6rBbmjg9DXV6J6vSguF7ZdLbQd56by/HiUMD+qqNLnqSZEv4Iv001jp43IsyvRSzLrP+oPwGhzMZdkrOKsiDWEizrS9DY+yf68d09Uo9EcNR90JVLw5K2srkpBvrqJ3JwqXvz3THQtTjrPyUPKTOtuG6iqxv72qh5r+Ek5maiyjHV3S2+EryWp3iDoDfu9HairR24O/kOQdxb1fJCqIhcWY7N6aB0WRfYl6xEsMh39Iyg+XwfNRt4a+iItbgtvZn6AIS/4PNl6gdkhRUw2y9hEE2O2nEar7DryJ6nRaI4J59kb+Prqh/AX22ncFMOuylhOuHEZZWdGE/LhJtDr8E858DqgHf0jQJGRC4uPXtA/oK2C3gvazh5KxNelBOrqAai/cjjRT6xAys1CcHkIlFce8LHxN3twpweLH2xbjDiWFCEG0mgYKnLNrTci2EQG196AY7ueAcosvLtCsPZtZeOItwH4W+bHnLbrXB7LeoddvhjOsmlzrDSa36vn2uP5ujmXHQ2xyGaVc8atwCgGWHXRYFLdDRT9YwgZd6/HWKE/4P5R1vdXH+Ceo0PrSfWCkDdWdScogOgnVgAgeHzg23/1nS42BkQJpb4RY5Obov+NImK7j66CNPwWEVVScSwqonmQSs5zHmxT6xAFFdEn4HQHe2pbfB7Gm13cnLoAvyoSKrqoCnRpPSuN5nfIq/pZ2Z5Btq2Be/t/jGqS+eY/o3l73gSE6kbU8ioy5nSg+n0oTmdvh3tAWpI6hgRKywnU1nXf7jwnr3sosG1cGpLNiuJ04k6wkvtQFaYVhchGEcfbq4nYqlIzqw9CrAd1/Q7s0yvQfxKKvUyFUivnlE7ikq0XUi97uemzC5ERmGLx81rbMBZ7onvrlDUazRHwXHs89zcNZsmqfsxZOJo7PppFyofQPFAlYFFBVVA8nmNy1fMf05JUL5OyM3rs8Ft/Q0H3+n5hy6tQA36knEwEWUXu6KD++gKsm6qRG5tQnC5CV1TiPH0kTae4ae8XIGaeEalPBoXPDkY2CRicKpmvN7NmXTanpmxhbsdA/jn1PUYa9XhVP4mGZk6x7i1Jf7E9Flk9VjaO1mg0h2KadTdXha1mz1nP8NrJT9FneDnmWifmjA5GjdlJ6bU56JISKftn/j6Pbb0oH90BVqHoDVqS6mVKiAXBZkM0mfCeOIKYNV2IFguIEoHKKjzTgysUfz8uHPP4CgLVNcgj++KfNBilqRlFEsi4ooScF914HSIlZ0dy0sCtxL25k7azO8l+vYSpBZv4qjaXtx47gX+8exaZiy5msdvCfR/0LEff4YqnVhv+02h+095sH8I7ncEK3ydrJ9P4Uiq7rrbi2xFC83E+EhZ7cA6MI/nr4A4KosVC82XBhBX26soeIzq9TUtSvUxduxXVasY9aQCirCIEFNqnD0AXEwWA+YtNyEWl+E4Yjmi1dj+uub8Jb7ieijczGH3HaprO7E/ZbWBtkEl7cBNfFOVS+lwirnYzK+rSKOqIomFVHH0u2UnAppL9907+ct9lfHLewz3iuTFqCbOLzmGN18+Tbfsul6LRaI59N4XvYIZtG3fUD6bohT40jPMjmGVkA3RNHYAQUECBupFmdLExKC4XUW9s7O2w90vb9PAYUXFPAeE7FVBVAmYRQVYRVAhfWUugtBzfCcPpSNWjSAJ6p4q90kfJeQLWMDf6b4LDhXGLmgGoPi4Cvx0CVhVrlUBHpoIqQmKfeoy6ACU1kcRFtVO/KYZBo4s4MXIbb1SN4rGsdxhoMAFQFeiiKmAmzyT12nui0WgO3T2N/fjyobG0Z4j4HQp9R5RR/mE6cY+voe7qkcQ+tx7f2P4YV+9G6ew86vFpmx72FkEAcf8f7IIuWPEvGI00z86n9eJ8BJ0OQacjrFChdrxKw3ARyafSli2idypkza1GysnEG6ajLUfFEw0R723G0Ozi5MGbcNZZidzmJqzQi7yzGJpa6XfWToaftA3bd8sE5jzdiGqRqd4ZQ+mmBHTlJlq6LAwoKKa6K4SXywtY2G9+d4ICSNTZtASl0fyG+FWZRW6RK6uCw3bvzxlP8tVFeGIDoArsXJWGogM1ECDm8RWoXi/6r9ejdHUd8DPrWKDNkzrMdKnJtI2Iw/buKkSLBWVgJqzagmix0HD+ICKfW0nr2UPxRAlIHmi6aATuGAF3nIyhWcRRAp5QgbgVPsx/ruG/cesYdOJInrrhCTa403hy+ziKns/h5D6bWdGQhmALUHGNjCT5CQsfgaXWy453zMSs6sRzeyeLRj3LaTMu4J7U+Yw1l7DVF8uf1p6BTlB5L+NLHm9LJ1a371ypexv7ck/Ujl54BzUazaG4pmocY0MK+XvsAha5HfhCVHY0xCJ6RaLXqZhaA5i3VtF4cT4R723pLjsX++XgTrFj/HT/+0r1Ni1JHQZSTDTyd2tdBUrLsZWWI8VEI5hNlB9vI7UpDXlPGYIMyvghRHy2G/34TMzX1OD261F8ev4v91Ny9A0YBIWztlzK0PhiFtVk0qV48IVAuOjh0RVTSE+rp3l+Ip8V5ZE7qYiGJgcxoZ1EWzpZPykdfYuJzNcbKTk3Er/Hw8ytFzE8qhKT4CdDbyND38UpE175LnKRvsZqYnWdgIlW2YUflWjJiknUVkvXaH5Lnk9aDsC4rRdwUfJKLj7pW95+ZTLSyC7CljfjT4pE9XiRfGqPZY+UbbswbuutqH+elqQOg8ZpGUTO6aDzpEHBKjxRomlqBgDR6/0IXj+i0UhbrkpzvkROugnzvT7Eu8NonmqDfp0s78zi6dYJNDqttJeEsUTIZNXQOegFE49d8Dz/bZgMksq4qGK2nuWiy2/kg8wF3GodyqSQHUw1uyAdTi2exusXzmPoomtQOvV8Mv5VIiUrflXmtOKpfJC5oEfsUyx+wMSVVflEGbqI1HdyU1gZd0QU7edMNRrNse6b/nN5rSOBF7+ZCEkKZ+RsofTtCLpOrkSwmDE3BVBcv50KXi1JHQbhL69EESVC1tUQAFouGom5VcYVKaHv8FP+WAhJf7MhR/gJXWek4+skUu8vZOOCXCaetIHJITv487rTWDLmCd7oGMQ786cwfeI29EJwnHiKxc9k83Jed+zhOEsJlzWfzcSo3QA8ErfhuyhENnm9nBC1nRDRjMEYICqqjRAxeJ1JL0jclLhgP9EHXRG1mFw9WMT9byGi0WiOfV+59LxQN5k6p4Oc55opvNvGe1uGYt1mIjGxFbWwFPNWhQDQcF0BMc+sQQ0Eejvsn6RV9x2k5svyiXpjI4onOK/g+5UgVP/eLdkFoxExKZ6OQdE4vt1Nwxl9aM8E2axwfN4W1tUnsX7Yu6R/cCWn5q8lx1JHhTeC+6K37vc1Aa6syufU8A1MtXgP2OZ7HzktGASZqRYv11WP4omE1XQpHvSChPF3UgWp0Wj21SQ7iZSsVAW68KqwwJnD27dOo/kKJ7fmfs3zfz2Vmokq2des6e1Qux1sdZ+WpA6SaLH06CKrowcjyAqs2tJ9rP6GAkKL/ViWFXL66t2kGhq5/uUrCSlRKLh1DddFLuHfDcfxf7Hf4hBNSMLPF1d2KR6Mgr67V/VL3Vo7lFH2PdpCshrN79QSD9x639V89Pd/U/DVTaTME6gt0JH8uRvFKNGabSD6pQ0IkoRgtVJ9fhYJL27DNSYH4xcbQJF7Je6DTVLacN9B+vEYrrB8U88GooTfCq5r2zB+3skrd59MV7xE6ullxE3vYJR9D/E6I6MdRYRJloN+XZto+vlGP2HvcKBGo/k9+dRlIkrqRFGNJF1cTKRkZsMJ/2P8jj8hucEbYaBpgA53kp8orxcVEDOTCdvlR/X5MFd1ovwGlkDTktThIAhIDhuhxTL6iX4800fgCRHRn9iIogq8mLzsu4Z6zrM392qoGo3m9+G+opMYGFHDzPCN5IeXAFAS0NGZGUDXJVGbLxGzVsawSkGeOBRVEGgYbiT+4dUoigxbdvXyGRwcbTLvIdKlpaBLSUIc3BfnaSNpfzsCQYHa1XG0p+rIv3YdvBtJaWNEb4eq0Wh+h1YOep9nE1dyksXDc1vGMvKB66kOhCKoAukfOMnNL8UVJWJscKJfV4RxXRGJ/9tA6f+NRJea3NvhHzQtSR2ilvw42kbGEwgxougEWlbG0txP4pTpK1Ent5JlbiDy4nI+y3uqt0PVaDS/Iy7Fx7D1Z/U4JggqKLDTk8DfJs/D/u8adi1PI3bubpQtuxDDQ6me3R9VVUmb14XSuP8RHSkq6micwi+iJalD5JizmpCFxZSdaMJS78MbruCJDzDRvpOk0Dam2bbzZPq73FZ+am+HqtFofkcsooEVQ9/scWzruBdYdfdjvPfYcWzoSsE1O4TMx/bQPikLgKZxiYSUBBAzUmDN1gNuctg4I/OIx/9LaUnqEOliohHMZrKfrkIIKEwfvR6pU+LqRRewoyIOnyqSprftM3lWo9Fofi2joOfdrhBkVSF9waWs9uoZtvpiMi8pRC/ICJ1OBEHAZxWQIsKJXFaDragNecfu4P50RuN+nzf8pZVH+Ux+nlY4cYiUmHD2nBNKSBFErmtlxdPD+fJv/8ap6jj1w5swCMd+1YxGo/nt+qatL2/X2bh62GIue/dqdF0Cq1MsZL4RoPgfOrJe8aPzqLhGZaB3Big/wUT6vAF4HQaMtQ3I3p5zLwOTh2FocqFs3tlLZ7R/Wk/qELUMDEXfKTD62rW0/ivAfXe+xIlv3cZadyqKSSFVd/Bl5hqNRvNLPZu4khfT5jPUXEYg3os0rI2/jPmY+uFmvp7yHzrSTKgSVJwj44o2kDSymrIZNnQLN+EuyNnn+fTLtqFuP/aWQ/tFSeqBBx5gxIgR2O12oqOjOeWUUygsLOzRRlVV/v73vxMfH4/ZbGbChAls3769Rxuv18v1119PZGQkVquVmTNnUlVV9evP5kgSJcTBfRGtVgSdDt159Yw5ZSOfFPbHIMlcu2oW18z8nGnWYp4+7tWDmqir0Wg0v0aYZCFJ18G03O1sGPk6s0Pq2HLrU+gFkLwqEYsqkXQKzliJydGFSG6ByrtHYWjx0HBdQff2QaLdTvkdw3oskSRFhHfv1tubftEn6eLFi7n22mtZtWoVCxYsIBAIMGXKFJw/uAj30EMP8eijj/LEE0+wdu1aYmNjOf744+n8waZaN910E/PmzePtt99m2bJldHV1MX36dGS5d2Y+/yRBAEC0Wig5IwTfqD54ThiC7W9W9tyRS/+EWvqH12Iy+7gprIw4ne2gljDSaDSawyFbb+WJhNXc0zCEr1x6vKqf8V/cTO04aMtPJOxLM+YmhaV5EYSOryNgVZFKaoibs4vOU4cBoPp8hO/qeYlCbm4h4oXev0b1q5ZFamxsJDo6msWLFzNu3DhUVSU+Pp6bbrqJO+64Awj2mmJiYnjwwQe58soraW9vJyoqitdff52zzz4bgJqaGpKSkvjss8844YQTfvZ1j+aySB2z8ghfWkXtSUnEvLKRov8bjCALCApYKwRevf1R3m0bwVXhK0nU2Y5oLBqNRnMgFYEuQkSJENHMdp+bTL2OqyonMdxRxlsVI3CcXo+QHI9q1FM3JpSojS70e2oJ1NX3SrxHZWfe9vbgenDh4eEAlJaWUldXx5QpU7rbGI1Gxo8fz4oVKwBYv349fr+/R5v4+Hj69+/f3ebHvF4vHR0dPX6OFsdbq5AbGol5fQuKx0PGratI+9CFowjahvu4cud5fPr8WC1BaTSaXpWssxEimgHoZzBTE/CSbG7huWdnYHw4jOqrBqGYDFRMCyao+hEWAvUNCEP6seetwUi5WQes+utNh1zdp6oqt9xyC2PGjKF///4A1NXVARATE9OjbUxMDOXl5d1tDAYDYWFh+7T5/vE/9sADD3Dvvfceaqi/iv+4YYiyCipIi4Lr4AkrNhOxSkJQR9LQP4rXbn0CrQZFo9EcS075z+10pimEeVSqLvMzLWszCwMjidwaQLergtiV7aCqiF4/sR/YKLzSRPYLEt5EO+a1e5CbW3r7FIBfkaSuu+46tmzZwrJly/a5T/juOs73VFXd59iP/VSbO++8k1tuuaX7dkdHB0lJSYcQ9S9nLmpAqW/s3qKDkQPQNXZQeWoC8298iBnrr6QuEAocvd6dRqPR/Jy7r3mTaKmTzFM72OyL5N83XIAhRkXfGcA7OA1vmI7mvhLJ/1iBdQdkzgUFkKKGwq+oD9Clp6LUNRy2jRUP6ev/9ddfz0cffcTChQtJTEzsPh4bGwuwT4+ooaGhu3cVGxuLz+ejtbX1gG1+zGg04nA4evwcLYHyyr0JCkh9opiIt1oJmGHaS7eTGNrG6bYOJm4/mVb5t7PbpUaj+X07y9bOBLNCos7GeFMbtRd5aRrrp2qSkYYhRuryBKadvAopM43my/KRIiNAEAiYJQSz+ZBf15sSjmA59Mf/2C9KUqqqct111/HBBx/w7bffkpaW1uP+tLQ0YmNjWbBg7yoLPp+PxYsXU1BQAMCwYcPQ6/U92tTW1rJt27buNsey3X/rz4rlfYld60MV4YXMdwB4K+fNX7QFh0aj0RwtNtFE4djXuHjYCmLWyLgGu7FntTFv+2BKZ8XhjBeoPy0bXWIC3hCJ2lPSfv5JD0BauAG56fDt9vCLhvuuvfZa3nrrLebPn4/dbu/uMYWEhGA2mxEEgZtuuon777+frKwssrKyuP/++7FYLMyaNau77ezZs7n11luJiIggPDycP/3pTwwYMIDjjjvusJ3Y4SQM7483woThy3VYNleS6o0n9v9K0LttzNp5Aa0uM1tHvdXbYWo0Gk23nT4X1xSdy8J+8wG4pjqPGlcIted5+c/w97jjzYu56NRFjB+9i3+Vn0ij04pvRySyQSDuw1J+uKm8LiGeQG19r2yQ+ItK0A90zejll1/m4osvBoK9rXvvvZdnn32W1tZWRo0axZNPPtldXAHg8Xi47bbbeOutt3C73UyePJmnnnrqoK8z9cbOvAAIArrkRJz9Y6nN13H76fO40FGNiKBN3tVoNMccWVV6fDb5VZmz90yl9N0sUKEjU8FSK5Lw6BoCYwZSNsNA/FKFxkE64pd58YbpsM5dTdeZo3B8ti24MK0g0HZ+HqGv/7o5VNr28YeZ5HBQf04/Yt7bRclNucSt8FF/uQefV8+9wz/SNjPUaDTHvAGrZ+H3S5ybs56tHfFsqY5nfNoellWko1trJ3luDarTBaEOaGrB3zcF/bYy5B/VEOgS4glU1/yqWI7KPKk/CkFvQJVlYt7eDpFhpH7cgbm0lYi3rFw5aAkFpnKaZCejt5zW26FqNBrNPvI3n06r7GLrqLfYNeZ17onaQbatgZ1jX+H5pOWoKggBKDsnHrm+AbmwmLqzclAMIhj27Qj82gT1S2iroB+E6huHE7HTjztCR1eCAAI4Sm04z27n2c3jmD56K2mShS/7vwWYejtcjUaj6WHBgDexiT0Lu2q9ISioSIAkKYSWBEi8rYja9cMxr9lD3LwSmo9LQ/dNQ+8E/R2tJ3UQkj5tpLZAR/j7W0h+YitxKz1Y63z0j6qjb2ItkhAcMbWJWoLSaDTHnv19NvW11dBn4WXMLJpKVmQTggyb6+IZ/eBqiI0kUFdPxOIqqu7sWXXddmE+usSEoxW6lqQOhryzKFi2ObEfQnwMnYlGSk7VM8hRRZatAZeiw6v6u9u7FF8vRqvRaDQ/7wz7ZtQ2A1t3JpNsbeGrZ57E1WFi6V/y8SSFoIwdQumFycQvcyNFhNNySXBF9NDXVhKoqkYwGhH0hiMep5akDpJ5/hosC7ejlFQQ8VkhxhaJj/4xiQ9WD+f+6mkMePWG7uTU/8PrWeL5mSfUaDSaXiCrCvc09iNRZ2bJzEcIi2+nyWvjrrpRJH8g4beJSF6ZgEUi5X9b0bW7ifwkQNTyvcN+gUnDqLl2GA2zhx3xeLUkdZAkh4OG8wai+n0oXU7SxpdhbA1grtHR7jWDANJ3Jfp/P+59Bui11Sc0Gs2xKVrfgV6QSNTZiLI6qXU52NYWj3V3C6FbWuhMMtJytZOGWf1Rtuyi4boklNLK7sfrXH7iHl1B1DNHfisPLUkdJLmjg8jnVwGger3Id0QgyirhO2UqFyVz7rQlGAU9/VaexwZnqrb6hEajOSZJgsi1oXsTzpe5n+AJ6KhekIzr8QCO55toGAFdpSEYOlRqbisg4DCi+n1IMdF4po+EVVu6H99+ft4RjVdLUr/ED6aUSbsrMNR3EZjdTFihwvtzxnN/Uw66JSFcHrEUgEFrzu1xrUqj0WiORVemLeWpK55iUf8PSbc0YWwOpgbjJXXYKxUkZ/BzTGlpw7auvMdjI5ZWH9HYtBL0QyS3tdN4Tl+ipq9BEMsICQvjXedkbCfVccueM/msz0dsHjkHOIorYmg0Gs0hKPbEcIatAjBhkXwIMuTcuxNVVrA6fMixYQh2O4rTRcOJ6UR/JXXPlQqUV/70k/9KWk/qV4h5ZSMoMp7jh+B/24QvBCbGFlG8OZHsb2fzoVPbCFGj0Rz77oveytyuZBa5RTplE+YmleqL+9F0Rn86hyfiSrRSd8EAdHExSD6VQE0t3pNGIOiOfD9HS1I/of6GAviJfbAUj4eOc/OomCbifySWk05bydyPxiD6BOxrzZT5IpFV5ShGrNFoNIdmgLGKFF0H73+djydcwNCh4rcLhN9Sjm13KzHPrSFQXYMqCegS4rGUttN18jCk3KwjGpe2dt9hIPXLIfWVcsqmWii6PYcho3ezuTqByJAumtbHoAow44TVPBK3obdD1Wg0mp90TXUeXy0ezMVTFvHSwgnYS0TinlyDf/wgDE0u1O1FqIHAzz/Rz9DW7juKlOIyFpZl4crLIPu/pTjPt5J+WSlunx5U6D+mmEp3WG+HqdFoND/rsfjlEOfljojtpPWrwVarUH/FSOpGGqGwlLK/jSAwaRhSdsZRiUcrnPgFPDNGYi1pR95e2H2s8+w8wtbUEvuCkfLpIlmf1tF6cT7m5lju7vM2xWkxpBiaOMfecxXhJR5QVJEJ5r3DgQ2yEz2CVr6u0Wh6xSqPzP2VJ/FS/sssdJvwBnR47QLRT61AtNsRkuIRZAFjbQdyuDV4OURVaZ6dT/QHu/ZZLf1w0Ib7fonv/iA9iBIoMqLJRMfMwSgSON5ZC0DDvCzaK0OQwr1sHPcMA766jqtGLOaZ5RORukRki8Jpo9ZxZtha8kwSDzZn7TehaTQazdHiVf0MfeJGUl4owvWmlcptscQvU2lPlTC0q1jrZUyfrKHtgnzC3l6P6vd1fw7+Etpw35Gwv3yuyIiD+6L4/ISursbQpSCIAvXXjiLkOTv6dpGvRj/BMk8IAzKqONWxCYBXT3uK+dP+x0dfjyJH7wXgjoiig05Qu/1O1ni1OVgajebwMgp6vrz6Ifxvm6huCoUoL46NdVhrFaJXNtOWpUOXmED45lZUWUbKyaTr9OGIg3KPSDxakjoM3PFWmi8ZidLYTOXpMoLBQMwTKwmYRWJXypxx/21cvfgCir5JZ+rCG7CU62iUHZy5+goUo8oGn737uTZ5vUzeMZPaQFf3sSbZiUvxkb/5dL5xS8wsmkqdbGWZM4cJ207phTPWaDS/Z4k6G4oqoN9p4ez+66mZlkD76V10ZYUS+9+V1JySgivZgXvmMAqvisTxxQ48cUdmyo2WpA4D42driZqzGcXlIufaXbSeNhApIpz2VImK0xRccQKCTsEXrmBxeAhYVZ4bMQzLEhu6LoF7ik6mz7ILuKRiLAMMemRFZNpDt3NDzQi8qp8TN1+CJAhkhDRz+47TeSn9fcaZ4JbwEublvtXbp6/RaH4n9vi7eLA5WFJu13uJ3Brg7a3D8USB6Rs7tlVlyOOH4HOAIKuIfhVBAdeEXAxfrD0iMWlJ6jBRXMEFZRWnk5A3ViHYbcQ/vILsS9fhC1GwbTaR9WonyX/xo3MK1FzYj+gnV5D5Ui2Os5qwfGtj1WcDuKJyHM45ccR9Vc9ni4fxdFsWCfZ2Cu69gbWVyejfCeeMnefxZmcEmQsvQRQEFrm1P6NGo/n1QkQBj6LnvqY+1DntVE5XiItuQ/RC1NMr2fVQIrUFJpI/bcW8eDs1Y3Rkv9CM6eM1Rywm7dPtSBAl2kbE0XpRPogSOc80EbXZSyDECIJA4gMriPnfChAlAiVlNJ7RDzEAgT4uqvK6aMsF1Wwg49ZVfHjb8fiucmBuUTAa/XjPbMNh9PDG2ScQssTEnbUTufSzy8mccxWl/i5t8rBGozlkdtFAvc/BeSHrWDxoDucNX019cwjRk6rxnjSCy4Ysx5Xip+qEMBouGETURhV5Z1HwwaJ0RGLSqvuOICkqCrmxEcFoRDQaUVUV/H4UT3CzqYZrCoh5fh1ieCgoKoIkEqirR4qMoPq8HGIfW0H7eXmYLqrD+Bc7rsRgaborSkTRC3SmKTiyWvl3v7nIqshVX16KapQpPvE5HmzO5a7Iwp+ITqPRaHoauOZctoycwxqvn7O/voa/jPmYVR0Z7HyoP4HZzXS4TCSEtbNnWwJ97tuD3NgIgKA30HDZMKKePvitOw62uk9LUkfB98nql2q8Kp/olzdgWeBg64pMMv+5hfJbBmFshchTKyndmIC5XiT35EIeTJrPtJdvRzao9Bldyvb1qdgqRJTxbWwdpV230mg0B2+VR8aj6rlk8aVM6ruLAfYqZti2cc7fb0PnVXG8tQopMgK5qZn66wuIeXwF8Ms+67QS9GNI44zMQ3pc9AtrUb1eyt/MJBAqI4aFkvyvdVjqZfbsjCflcz+eCJUNK7OZPP9WAiYVVQSH3kPMaph4wRrWjXz1MJ+NRqP5PbuuehQxkptn6iaALLB63kAeWzOZGWuvQu9WCXl3HVLfbHbfmUXXmaOIfXrv9ajG6Zk/ud7podCS1FEQ/tKBu8C6tBSq7iyg6Yp8JIcDsX+f7vu+Xx8rYBbo+6865Lp66q4ejm3uGvr8ZReCopLyhRc5NEDqJzKDRhdx9fQvWb0sl7pxKktr0un8bkt7jUajORhfFfehRrZwVewi5h73JGfOWoSp3IjpGzs6t4Lu6yh2/dlG5hudhK6tpfa6kQDo4mKJ+qR4//NJfwUtSfUyf2woqa+UEPncSuSODlyp+3Z7Y/+7gkBZBXL+AGJXdICqIre1EzBJ1Iw2kZTcRMo/CnFdYueDqsGcOGkdYcmtWA1+JA7vtxqNRvP7tnv8qww3yqTrO8jUywyyVOALVUifVYSpycfuVanYHW6ahjsIlFUQ/9wmAJSIUAi1/+RzHwotSfUi0WpFMUkEauu6j5k+2X8pZ2DSMPTbymDz7r2Pl1XCihQq90RR/FBf3JmRnJ20no/XD+aunC94uc/r3N84+kifhkaj+Z15oT2dE164nZO2nYdPlUh/34vrJB+6wkqMzQKR/7MQMAkIOh2FT+USmDQMSiqQi0oOeyxakupFaiCAvtF1UG0NTU4Ulyu4TtZ3JFcAd4RA9jVrsJZ3UT1ex383TuLBie8Sq2tDL8D7K0YeqfA1Gs3v1LWhlXgzPdQ0hPLnz89lxP/WI89zUHNuDv4RnSCAvktFiokm83kZQ7OLuksH03FuHm0X5qNLSTpssWhJqhepXi/Ktl0H13ZnCW1nDumei9B6UT66jUUY24Ljv8L2PURsVcm5uYq7583isvUXYRIEsvtWHbH4NRrN79eSCf+jILOE9A99vL15BLuL4vGGQ+S7FvIfWUNrPxX5dYGOVBPuRBvuKAjd3kbo66toGZ3w8y9wkPOqtBL03xBdbAzVZ2YQ88RKdLExwTlV0VHgsFH4txBCl5kQZBADYG6W6YqX8DkEtt78FLKqcFH5JK6L/YY805GZdKfRaH77XuuIZJq1nDkdfbkqtISVXonL37yatNEVAOypi0LSyST/R8SZaCJkwS68wzKRDSLGVi+s2nJQr1N99VB2PnWXVoL+exKoqw/OR1DV4HUsVUWub0AuKiHzgo1EL28lfIeLyA+2Y65xonODO1bh7c7ghostXgse9Y+T1DUazS/XLlt5tCmft/8+lZ1+P+NMMGRSIe5HE1D/HE76rE1k3NaG5PJhbJdRnG4kj4x1SzW68gbaz887qNeJeWr1QbXTNj38HdAlxOPNikX1ypSebEGY3o9AiILUBcZmkbkNwzjH/jUXJyynLhACaPtVaTSa/bs+rBy/KvPNA4Wc/s7NFF3wNHVOBxXTQFAspISPwGkTsc5djXLiCOSCflRNNOO3J5P+gYe2LJHwQ1zAYH+0ntRvRPPl+Uhh+9+CXmlrpzbfhCfaSPJXPpIXeNGFewjfBp5oBYvOh6wqlPsiuWv1qXhVbR8qjUZzYHpBYpypk+fPeBaAa1IXkp5dR8gOicrjJKxVbsT+fajL12Eorid8h0LqJz4Ug0jS127Uzs7DFouWpH4jIp5fecCtmRWnk8QHV9OeqiP/kTWUnGIg6642+l67jWHDi3gm6Wu6VC9r21IpOe4ljIKedsV9lM9Ao9H8llhEAxPMCu2Kmw8ah1FaGEd7PxlVhIBVj2w3ErsygGo2Uj9CwBlrwOfQIS7fQsOFQw5bHFqS+r1QZGIfW8HGk4Nr9tVMS2DJyn44L7Qz6M0buaRkJqVtETTJTu5vymHihot7O2KNRvMbMHHDxdR0haCLcJOY2QAi1A83Im0sROeWKTs7jrRPPDjeXo1tcSFq/gBi3t7+k8/pnzL8oF9fS1K/YbqUJHSJwVJPQaej4boClMZmQkplrHUyOU81UH1SPKYmgY27UpmdvpySgIHnV45jw/B3ejl6jUbzW7Bh+Dv4ZAmbxUu4ycWIEbvJnLaHwmf7InkCpD61E31tcCUcYqJQDCLoddTcXkDXWXlI/XL2eU5Dq/egX19LUr9Bnhkj0cXG0DgxkebxwUlzqiwTvaaT0jsGY/1/9u47PIpqfeD4d2a276b3TkindwiiqGBBERV7FyvWi/3q9XfVey332nu9dlTEggVUEBSUmhAILRAS0nvf7Gb7zPz+WA1GsKAUgfk8Tx7Z2dmZc9ZN3p0z57zvZ0VYP1oDXQ7m3voIV126gIRvJF7ZcQRjjHrKp750gHug0Wj+6moCTs6tPBaAWKuTr4a/xobyFOqeyGLT+nQyXlFgzSbaT8lF3r4DgI7R0XjD9fiG9CN+lRvb3NXIW3YtGaQWbvrd7dCC1EHIuroSub2TiDdWEfbOaqSs/jjPHIO0rZrE771U3jMagLqLsjhpxXWssafTlA/ODVHYFTcXVE2mLuA8wL3QaDR/ZScXXcWshK8B2L4snfe7B2CqMeBMlAhN76J9sBnXaWMwOBSknGClh6hva2g804ffqiNg2TvrMbUp6AchubUVQW9ANJlwTR6CtaIL2wdrkAHjmu2kuTNQJgyjZ7gbxW6gw2sh+RuFniu6GL74enQmP65kLfGsRqPZvWc60zgnYx2Zeg9zHMl4U3w8UTSJxC0ynnCR0OdsVJ+kEjBLxK7zUHFBDNb6GNzRAtFfKpjmr95rbdGupA5SwoAM/PkDMH+1DqG7B+XI4GyazlMH4gvVUzfZQsq7OqLWSXC1mdopYHk9gtT3ReRGCzO2XoRT8XB1Xf4B7olGo/mrmRlewW1Rm7iz4XgiJSdrJj+N1Gik/uQAnQNVqi+SyXmoAoNdxdBgJ2a9QvR6J2kLugh/f91ebYsWpA5SyoattOeZEHQ6FHs3DRPMSNFRRGyyYylvJ/ULB5aqLmxnNbLj31YG5tZy7D3Lybq3BKK9TE4opcQvMS1y/YHuikaj+Qv5zgNe1Y9R0OMIGPmoYxSn3n4LliYBsTOYsSZ8uQnCQjB1KWy/OgbLvDU4+1lxJ1j7JMHeG7QgdZASh+bRna2gqiqKw0FotUJPfgaqQYdcVkHDxBDsg6Oorosm424n/awd3BVdzMOJX7Nj0uu8XTyWla4sTrZ4DnRXNBrNX8innSPoUALUBJwUfZ/Dos0DSbq2HMOkNsJLBQQFVAG6B0dj+2w9Gbesxn7BOMIWbcX4ZeFujynodHRc9sdGbbQgdZBSt1WQ+0gNqjc4lTNiURldmTqETWUAJC11oOggdKOB+pPiqTw7ntxPr2PMO7fgVf0sPeZpZkVUAXB3y+AD1Q2NRvMX81jCOlJ1NpIkC3POeQqD1UeEwc3F6WvoHKASndfGVTd+hjdMRPX7kOJiCanxItu7d3u8wKSRqIEAMfN+X8WHn9MmThykVK+XQH1D72O5rZ34J1ai6nTU3zEeU4eKfnoLbruVtJhOjGd4uDRsOffElAB6UnU7E83G6Xf/4dJoNIcvSRAZaTRg/dZK2fQYSrtiMTeJWAf6+N/j0wit8eGaPpa641XynupE/nlBDVFCFxuN2yahg1/MmPNbtCupQ4wYEYE7TuHE65YzNrYa21IrVWuT+SBjIWnGNtK/ugK/Kvd5zQ0R1QeotRqN5q9uzf89S7jRzft5b5M4uZZPc+fijRBw39JFwCSQfW0R8tayXV4nmk10TkzHE/7npqJrQeogJYwa1PvvujvH4z5tDL4TRrH13+lk3b6Wz6sGsbIpndizazB0CWQuupIPm0YSHdvNR87oA9hyjUbzVzapZBobfR4GrLyQjCUzWOK2cErsBu6oP4kIo4shi68DARKs3XhDBQLHDEPQG2i5bnyf4yg9PYS8v5qIN1f9qfZow30HKW+UCcMP/w5YVRxJOsLLfQheEdFiIeJlG9WnCMQ86Mb9gAdBEZif/SUtcg8vd46EEK1ch0aj2dWSAZ/RGAjw3diXeK5jNHeVnIZ3VRTxa7yE3FNH2nsCrliVss+zUCKgaayRiIjhiP59Uz9Xu5I6SBkWru39t7lFIPa5lZiaXWTP7qHi1oG0D9ST9387KLsuBex6Nk5+DoBYycrd0X/sBqZGozn0yarC6ZsvJVqyck9MCaoq4ItQaRpnpPW5dOqP0eMNF3Bm+Zl0RiF6B9g+Lyb2rfV4ThkDwt5NFKAFqUNA3NMrARCb2ulJsfDAue+AALWX5nDP6XN5ccpr2ETTAW6lRqM5GEiCyOphH+JV/QwpOI9uh5mwMnjisldwRYsEEr2oEhibdCz8ahSO/gqq14vi8WCtsIOqIoWHIcXFBo+X1R/n2b+vWu/uaEHqENB+eXD9gS8rkaZxIi/XHkXPAC89QzwcZ6nhf01HHeAWajSag41d8ZGfWMWOY19HP72F6z6+AvfRDrZOeomE7+1EbVLo/+AGMm/amQKpN5lsbDRKalxwW1kFtrl/PE2SFqQOQsKoQUjhYb2PzR0KypHDaTzCjJDionxDMi9OeIuCY57h1M0XM7f/EiC4knypW/tfrtFodvVZj4Ui785sEbGSlZeSV3Ff6wBa2kKxNApsm/A2r9v7MXPOp7jiJFrPGxrcWZR6U7MByNt37FGm81+jTZw4CAkBBZSdNylNrV4kr0zKk+voOn0YYkDlzqzTyYpso73LxlafizyDBQMysqCgfTfRaDQ/ZxL86AWF47aewpMZcxloMHN+5TF0XhOH9V8eLrz8O/p/fRlim4HwEgF3mkrG8xXIRiOCJFF2joGwoeOJfW5VsLbUXrJHf61eeOEFhgwZQmhoKKGhoeTn5/Pll1/2Pq+qKvfeey+JiYmYzWaOPvpotmzpW6HR6/Vyww03EB0djdVqZdq0adTV1e2d3hwmlOIS5O6dC3DF5cWohZtQPB6600Vyb9lC6LOhpJg7SYvt4Jrt5wMwziRxhEkLUBqNZlfHW/wMMZhItXZiEoJrKd/st5hts2yEvx5CuSuW+C8MDBm9g86BKqlfunANTUFMSUT1B4hbIRD7/Jq9GqBgD4NUcnIy//nPf1i7di1r167l2GOP5dRTT+0NRA8//DCPP/44zz77LIWFhcTHx3PcccfhcDh6jzFr1izmzZvHnDlzWL58OU6nk6lTpyLL8i+dVgO0X5mPFBHRZ5s4JBdECdfpY3GeNRYAyQNr3x/C0Q+v5JTw9dR/m0KoMZif78G2HO5vy93vbddoNAePBJOd9+0jyfx2BuPXn8eyyU8y+p9rWTN7OJ25Ip1eCzq3gL6qGXOdg203xtI6YyThW7tB2f3f8fbL85Gio/5QewRV/XNhLzIykkceeYTLLruMxMREZs2axR133AEEr5ri4uL473//y9VXX43dbicmJoa3336bc845B4CGhgZSUlL44osvOOGEE37XObu7uwkLC+NoTkUn6H/7BYco34mjsRTX0J3fD9uXG1BlBYCus0fQfGyAwuOfIlqycn39WJ5NWnOAW6vRaP7qCrx+br3pOhwzuvl42P+IlCS+7ElkYecgtj09kLPuWsRnDUNwvp+A3q2Se+MWNrwxiJgX93zBbkD1s5RPsdvthIaG/uJ+f3jsR5Zl5syZQ09PD/n5+VRWVtLU1MTxxx/fu4/RaGTixImsXBmcIl1UVITf7++zT2JiIoMGDerdZ3e8Xi/d3d19fg5nypHDkaIiMa3YRsm9aejcCj0nDkEZM4D6m0bRMVgAv8joBTfRKbu4MfYbHunIONDN1mg0f3Ehgp9B/9jIZZmrOPGt2xj2+d+4793zWPP5YHoSRLb2JFC7JZ7cy7diag/QPEVHzMsFCDod3imj90mb9jhIbdq0CZvNhtFoZObMmcybN48BAwbQ1NQEQFxcXJ/94+Liep9ramrCYDAQ8bNhq5/uszsPPfQQYWFhvT8pKSl72uxDir65G9XjRXE4SPkSFJ2AI1mHrtVB0jfdqALkDy4jcYnAMk8s4SIMNdUA4FJ8FHj9B7gHGo3mr8SvyqzwKMRIKnGGbuyymUknrkdQBO4470PcGT5kI6x7dQimFpHm2/thLqpE7uyk6YaxqLKMud7x2yf6A/Y4SOXk5FBcXMzq1au55ppruOSSSygpKel9XvjZamNVVXfZ9nO/tc+dd96J3W7v/amtrd3TZh/UdP379fmWIm/fAekpKBOHY/60ANP8AroGBth6cxTeKBOqpFKwMpemfIHXGiYQK1k53hIMTB2Kj1dajj5APdFoNH9FXtXPc42TOHPrBTT7QrkgbC0XR61Acog8OO8MEFSMnSrRL68i+b9rcCUYaTorG4D4p1ch6PQoG/dNJps9DlIGg4HMzExGjRrFQw89xNChQ3nqqaeIj48H2OWKqKWlpffqKj4+Hp/PR+fPUrb/dJ/dMRqNvTMKf/w5nMj1jVhWbe+7sbwK3bpyAKSoSPp/KGPboaN9kIGw7QLmZoGBI6u4LHE5F1Yd3fuyZJ2NV1JW7MfWazSavzqbaOLd9G95Iftdhtlq2OCLp8ofzbTj1hC/RgafiPnUZlpn5mM/bzSWJh8hdcFJElJ2Btv/N2iXiV17y5+ej6yqKl6vl/T0dOLj4/n66697n/P5fCxbtozx44PZcUeOHIler++zT2NjI5s3b+7dR7Mr1etF7rL32dZ0xQiUH2dNCgKGNhfJzxWT8lEdkhfCKmU63BZuLTiLuvuzGF54Ltv9PQeg9RqN5mDQIvfgUPV4FT03L7yAuxafzbyVo/Fc2UlOVgMNVdE4+kPkwjJaRpixfl9K++X5yKXl5N5chWzvRoqJ2evt2qPFvHfddRdTpkwhJSUFh8PBnDlzWLp0KV999RWCIDBr1iwefPBBsrKyyMrK4sEHH8RisXD++cF1OmFhYVx++eXccsstREVFERkZya233srgwYOZPHnyXu/coUiKjsI1uj9xLxbw47RMua0d2tpR84fis+iwZ0Hal14Cj0YgTjYw9oFVNHpCydZbD2jbNRrNX9fkoivwF0dwzMnruOTI73lj1QREv8gxiWWsvXUkWS4vtSdY6T46k+SPqgl02Yl+sxAV6Dw+m4gvt9J6SiaRr7ft1bVSexSkmpubueiii2hsbCQsLIwhQ4bw1VdfcdxxxwFw++2343a7ufbaa+ns7GTs2LEsWrSIkJCQ3mM88cQT6HQ6zj77bNxuN5MmTeKNN95Akv5cYazDheJwYt3cSCAQ6LNdMBrZMd1C9r+3EHFLLG11MdhOb8JfG8Un8/MpvfyFA9RijUZzMFAUEWmInWUfj2DgyaXkPd1F6RWRFN4xiuaxBmKKRRKWezFVdxKoqwdA/eHvUOh7q5EBv23vZkCHvbBO6kDQ1kntJJpMKD4/UqiNbU9lkHtLNY2vxhD9uJmmMWbyTi3tzd2n0Wg0P+VUPBy17hK+HfEGw+b/DdElooQHODJvO21XJFB9ahRJy1zoNlVQccsgZJNK2pdepKXrdn9AQUA0GlE8nt889z5fJ6U5MKTwsN5EjmJICG3nDYcxA6m+diARK4y0Tssm/IUQjPc14R7uorIrihsb9s36BY1G89e32iPzjmP32R5m1pzI7TmLCBPNFJ78BDOP/5rkzyXazw1D3lJK4vdu9NWtbHsgj4hSBdmkIi1b/4vn0sXH0TV92F5tv5Zg9iCj+vyoOoGGW8eT+OhKIl9fhRQeRlp3HJVnRSMNtdPl0XNhRDWp1k6eSPye2d2H97oyjeZwFiO5kWnl59ckb3THUtUdSWtYKBlLTiN8uQnnsT0k9cgIbwbofCOftuEqufc4yXnFjrq1Anf0qF88jxQRgdrjIvTdP16WY3e0K6mDjOJyETBLpM7egf2CcYgmE3KXHXlrGWkPriU1opOyo9/gnpgSnk9ajVHQc3nYLy+U1mg0h7YMvY0cvZup26cAcG7lsVT6nXzcPIIlg98nx9iA0GwktDqAuMWGN1xHTkgzreNkVJNM22kDUDZuQ/X7egusdl6aj6D7yTWOINByRi72Ewfs9fZrV1IHIeMXhQSAqCXgOHEI3hCJgBlCz2ogP7wSWVV4qjOTmyMrDnRTNRrNAfaOI4oV3eN4pN9HgIV/JX9Oos7I8PBa3uxOwymbiMjtIOWIJnpeHEjK37bz5bxxhNkhvNyPubYTIScTwR8At4dAYxOuOIHWF4aTc/1GxH4pOAZGETt3C6oso+zl9mtB6iAWaGrGusSFfl4EzjeTcL+RgOvm4Kwbu2w+wK3TaDR/Bada68kyNPFc6zHEG+3cHb2NIq+PMmcspcRRWJGGrcjM2pBoUkscNP87g5TbaoizdFN/dxbVp0WS9lkHAII/GDLiCzx02I0AyKXlWErLkQFECSk8bJd1nX+GNtx3kKubOZjmr5OxZwqElzjINTciCSL3xWz57RdrNJpD3kJXLOd/fAP/jP+G+XWD8KsyZ82/gViTg6Lvc9DVmHDHq/R7ZgvNo0MIWETUu6NYsWYA+u820e/9JuQQE3JpOfaxSQBI364j9tUiVK+3z7mkmCjsx+3dckDaldRByH7hOKK+r6d+WgoxG3xUXyyjBkSOeauAmeH1B7p5Go3mL+QMWzdnnPciRV49LW2hfOkKwdgq8e1bYxDiVL6/+FH+r3EyS+VhhA5tw3JKGagqWWsNiOkpqI0t6GWFhmvHE1XioeuifMLfXoXq9+1yLrm5BdsHLXu1/dqV1EEobPZqArUNJL65mYBZRHHpqDj+VWZFluBVtQznGs3hZFzxmbtse7yjP8917ZzV61Q8LOvJ5fnx7/BV1xBmz3iS7qE+YosUnusYQ/mdA0geW09XSRQIIu1X5iOGh+HuHwl+P4GKKmKfX4m0dB3hb+957ag/Q7uSOkgoE4ejKyxFcbkAEIbmUjktDF+Wm23HvAzoeax9EBbJy6yIqgPaVo1Gs//MG/QGYOuz7arwHytTmAA4YfP5ZIS1sbQ9m01lyWxYNAz9CAFjl49FDbl0jzGSY3CT9XITsiIT+/4Wms8bSMxrRTRdOYrY53+53t++pmWcOEgIowfTeEQIyR9U0TYpjYgSB9svsvHBtKfprwsQIVkOdBM1Gs1+ttXnIk2nwyIa+my/r3UAFsnLVNsmUnQiNtFEpd9JrWxjxiczUcICDLinEX9aDAGzRP1EA5ElKmFlTrpybXgiROKeCQYm0WJBsJiDOUL3Ii3jxCFGLdxE/FOrCNQ3ED67AMETQDUovNl+BOt8Ib99AI1Gc8h5smUSlQG5zza/KnNPTAm3Re7gpC9nMbs7g07ZxZkP3Mat/7qG+FUqol1HIDESX6ietiFG+v1zNaFz1tA5IITwOWsxdu28dhHjY/EO6Rd88JO6f2JICM6zx+3zPmpB6mCiqohWK9tfHs6260P576T3mb9yBJPM8m+/VqPRHBL8qsyLXcFZdi8lr2KgIbjc5PKaCVxdl8+wZ29gh9/Jdx4Y8J9GtrkT+LwnlfAdPnyhArYP1hBdLJD4dBUNE3UkPL4S0Wym9epxRH9TgxoIEP1Nde/5AtEh6L4pAqD5hvzeQKX0uAj/vmqf91cLUgcJQW+g7ep8BEnCFOZFCvFzkqUZwvx80mP77QNoNJpDRp0vko0+D0dsnE6x14tdcfNq6nJuiVvMq1c+w7S1V9MUCKN9QhIVzmjuXTON7lQDiV8103VxPqoIG94cRNp8NwCNVwwj/t0tvdnNAynReE4ZQ+el+fhDdw4lxj29cmcZDkUm0Ljvs9loEycOEqrfR8wb65C9XuL/Z8SZpOemzEkUHPsMYaIJ0EqdaDSHije6Y0nUdXK8Zeds3cc7+jOneiQFwz/g/thNyKqBCJObc2bP4txp33FfzBZO+OZG9E0GjjhmM//45HyS2wNs2p5C0lciDccoRBfqaR2tEL9cwNokE7DpMQA6l4p/eAb6+i7k8koo2IJZFDBL0i5rofY37UrqIPLjh0XyKrSPUKi9Lp0x825GL2gBSqM5VCxy6RlgrCdL3wnAAldwht4EaykWvZ9FLj3pn16FU/UyLrKSghmPc2n4GsbcdQ0Gi5+kZQF6AgYCYQFaRugZcG89nTkSCcvAmR1G7lMthH6yHuOSDZjL2wCIeXcDbTe78aX8UAJekVEDgQMeoEALUgcVZcIwhJEDUSWBzHc8VEwPwdSqBSiN5lDgVf3IqsLs1nz663zESQZkVeG9luDkhCydn573EviscwRHDttGhyzz+aPHMLs7G0mA7Ku24us0YVi0jq2f5ZDzPw/h5Qpb70ghebEDY2eAzmwJubwSdWg26sjc4FWTIKD6fMSeug3p23UgCLRfnn+A342dtCB1ENGt2w6by9EtKcKRbsbUJhAY6DzQzdJoNL/Dq/Z45jrDmOsMA+DulsG9z3XKLnIXzWRyyelU2KPxqypHFF0MwOx+S6kLOBn/6q10Z8DaJ4dzf9IXPNh0Am3DVf737ClMKbyalUU52Cp0KEcMQZGg7EYdrmiRnNccCH4FVSci/VCLUOrxITqDGSOkAdl4jh++s6GqSuzH2/bPm/I7aEHqIKK4XMHLb0GgbaoHa6OCHJCwK+4D3TSNRvMbonROwkUX4WJwQb5F9LHF52ZIwXmIgsAFwwtwvpdIx/J4amUj9w/8hCHPXs+r9nhOXnclr1/8DFOmFPL3e2dzc/VpVNyZS+L3KuYOhQeGfMpxYzdialORjSIxG/xkX1tO7PMrERrbED0+jG1u4p8Mrn0SunsQ7U6k6CiU8iqMXxTiO3E0wvCBAMidnQfsffo5bTHvwUoQ0MXFUvLvVESHxI5zXzzQLdJoNHug2OvlhdajeT5pBZIgMqtxFKeEr2eCycOTHQNIM7Qxv30oG5oTCTV7aNoWy6unvMxDVSfheziemhN1jBxdRv1TmTSPE4gqFnDHCkSUBmgeraPfJ3bU9VuQcjLxx4Ygfh+sqOs6fSwhS7cjd3binTIa6+ZGArV1wanl+zEcaIt5D3WqSs3FGZw5ci3GVCdnV0w60C360162Jx7oJmg0e83uPs9Zb1/T++9hRiMvJa/ije5EMubMpNwRg1/VMWH9Bfxv0xG8VHMUtyYsxOM20LkiHiHWw513X4V0rhfRr5L5vovtc3NonCCQ9XY30UtriS3y4LeJxBf4EUorEQfl0nJUDM5kY+95QwvrkHOCef2MXxYGAxTs1wC1J7QgdRBLfmETm64cQOKzBpx+I3bFjUvZNTPxvuBV/XTKrr1yLFlVaJN7+Kp1IDv8TgavOb/3ucaAkxa5Z6+cR6PZH9rkHiZsnM57daOZXn4co9edzXceOG7rKeh6BLb43AxafQEAJ247mQe+PpW0L/zUfJbO9Z/MwBeQCDj11GxJ4NInbkIQVBjazfv5L9N0jEzjOVm0DjMilVRhaVbIfbEddtTSclwK0tJ1hMwtRPSrCDodgQgzIbUB/FYB1+ljEQflEqhvwBtlRDAaf70jgPu0MUhZ/ffxO/brtCB1EGs7axDClh347uyk49VUrq4+mde7M/bpOTtlF7MaR7HMbeHvjX2v3pa6Rd7ojv3dx/KrMlfX5VMTcDHms5vZvDKTO2tPxTY3lKyll3J3y2BO33wpE1+9TbvvpjloREtWFgx6h8UD5pFo7uaNQW9y+7YzeSd7DquveoyzX74F6bvg5In3sj4gvF8X/375lWBNp/k+cqJb0Id6yXjfTWSJl0sHriZQFsIt115H7Pc6PFGQ9GIx257OIuyLLTRNjAZZxtz+Q+YZRUa/aC1ydzfi9+sxfFVIzOz1WOevRykJluEwdPhQRgbrPrlPHfOLfbEsKEbeUf2Lz+8PWpA6iEW+toqeE4fgeysOW52PML2bk6xb9+k5WxWVT4uGc7zFz9db89jqczF4zfms8Ch0KRb+tXB6n4ByX+sA/tue1ft4ZNHZtMk9LHWLnF42lXpXOP9pPg7RJXLpSd9QvDwbe6aIYZOFlbePZVRMLaoAw+b/bY8CoEazv+zwO6n0Oynw+sl8byYAYaKZT3rCWTJ/JJGiTLfLxMTVMxn//C0MPXkrlmaFaWUnUhbQMyGxgguXXUnmO12oksDaDZkkvmWk/EITzaONvDd7EpJHwNDtx9weoP+bdSguF7mPOFEcDuKXNKP6fHSn6Sh/Yve59BSPBykqAslmBUBYtQFh5QYArDu6f7Fvqt8HyoFNu6ZlnDjImT8poOPv40meWcOKj4dzmmc4K259HBFxl8zIf5ZX9RMiqEh2HV+5jEzILidKUpk7/H9cectNdORKkChzTfVJnBy9kZeqjqLdaWHjuLc5v3ISMxO+ZfWI97ireQKffTkO0ScQv8bPNl0K6mSVxbceSbLiQ9/tQ+rswZUZReUp4ehf6MLbYuVfq0/h0uNf3at90mj+rBXufphEPysdWVgz7IwsOhuTPkBrlw1/sp8uRUQQVDwNVjKXuSiIykEcLGC9J5FzT7+ByI0CeR+VsO3fwSub9I8D6JYUkbclBfuoRKwfrcE1fSyyUcLeX49pUQOIEnLJdhAEtt8XSva1VkwdCvEvrOOX7iz50+PRtXYjuFyogUDvdmXzX2e6+e5os/sOctLAnOANz9ZOHBP6o0pQf7yCrkPHY2e8iYLIu81jmdt/yZ86j6wqZH51FYlJHTS3hyH36DDX6ome0MjV/b7jnqJTEKvMmFsFkudW0XxiGuEVXlqHmejOCZDUr436+kgkcwABEASV/o8r7DjDRiBc5tTR6/i0YAR5d5fTflIOOq9KyMdrKX1uOOYoN2wIRQhA1NGNTIjdwYNxG/fOG6jR/EH3tQ7gnpgSlrpFREHhybrj2LQyEwEYNmE7V8R/x9WLZyDa/Cg+iVOGbGDxvNEkrPbSmWUgYVEjVY9YiXjXRsiiEhzHD8CeLuEc4CNv1nbqZwwi6a2tu0wH77gsH3O7jLnRDQWbCEwaiaIXMRfuoPG8XGKf/fXaT52X5BP9RTlya+s+fHd+2++d3addSR3sFAU67MhtbZibE0EQyHnBj+3pZu587VICFpXQ4e1MLz+OG5OWoBcCXPnq9Txz2Uu92dO3+lzkGXbWo5pUMo2rU5dxts0OwHceuHz1ZdyR/yXv3nkyGZ8WUPHffKx1KuYpNbx46plEhYiEv7UKMSSErc9kE7ZWQNflRRVNmGt19GyMJ3d5B4KsIocYafuHB8En0W+BB32Hi8VTx5CzpBvF3k3bcBVLk4hNlsl6y48rwYorRsUVL+AJ6LBI+2dyiEazO7KqsCPgxiQG8+qJgkKZN57yjmhCqkE9sZN0aztXf3MpF4xbxSfvHYm+B5TBIqoe/CESnWP8SL4ETIvAYPchpCbSkSsRWqWQ+OhaZCD+qZX8fKCt+cbxwSSvP6FbEsxQLkOfACXFxdJ2YgYRb/atpBvx5qpdjvtXpl1JHWKkiAh6JmThCZcIn72awLEjOOaJFby2dCL95suc8eRCZoTuwCIasCturq4+mWZXCIsHzEMSgrcor60fR5yhG6dspLIninWVqSR/pEPyKDQcqSf9viI6zx0JAigSxMwvp2dcOqbPCwAITBqJsboD56AYulN1GDtVTF0ytvX1vVmW2y/PJ+r1AirvH0P/eU7UtZt3ToH9oRSAZ+poGsdLCDLEr5GpnSyCoJI1sJ6FefP3/5ur0RCcvXfpjjOZn/0lEMwkYRL9LOwYyIodGWQlttA6JxVfuIArQSHtywAoKoa7mrB7TbyW9za3HnEmNRf0I7xcxlrppOKcUPrfsw7V60UwGnFMG4btgzW7nlyUeu8ROc4dR8jcQupvG0vSMies3s3owk/2/6vRrqQOQ7qkRNBJ2IobaLw2hXBVRbekiC8eOJrcdS3Q1smTxZN4Rnc0dw35kse2HYdOkhkVV0vWkis4Jns7KxYNZt1lT3F66XTa301B8kPuyibqT0kg+dMGMksVAgE/0csbaDoukdgCO0p3N3pHANFqRZAkdpykJ+dJPzqnTNKnjaj2bjqn5IF+58ct6tVVSAOyyfjXehSPp3e7OCgX0eVB7XZgK27AmJeKJ1qlI0+H3qny2nnP41G1Lyaa/cOl+Dii6GLWj55D5tJLGZZSR7KlqzdAORUPr999KqHX1+LyGzBsN1NnCUdvEHANcaN2GDCXNiPHhFG9LI2Y8Y2c++StJLavI/HhBgCEoXmk/31V772klhkjiFvZibK7Bv0k4ESsaSSgyKR82YHQ0IoMNP1tPPFPrdzt/gcrLUgdQtRQKxXnRZN2bwHpd9b1bld0EIgJQWpoJvuWJjx5STyZcxaqRUB1qdQuVZEuMaJkCxg7BfIfm0XCUjv+owX8gKCoxD+xksBPzhWorCb65ereXyQhoCAkxEJXN6IvuHK9+mQ9uU+C3GUn9L3VfV4P4Iuzoa/V03bxcGJeK0QNBPDGW9F365HMRgJbSkmZZ0B8xc2WkhTEED+3bDubHq+BTWPf3bdvpkYDWEQD60fPAeDKwSs4O3Q96fpg/Ta/KuNRZfJu38z/JSwkVWfjKPV0amujyFrXQ8I3brZfZgRRxBNjpt+jG/BMyMNzmZMGYQTxT6wEQcCdaMNSE0H7KbmEzy4g7o31IEkgCIhmM4pr9+sRA5XBqeHKxp0TH/oEqEOENtx3iBEtlt1/qEWJtiuC6yFiXitk+6tDyLpkHaLFQtOlw4h/exMNlw3e7YdciorEPygNcdn6Xz13xcP5pC70YVi1FcXlCrbF7f7Nlew/tlmXkowcG4Ygq5RdFEr6J170TXa8qRFUXgSvHPkGIwwOZFSiJevvf1M0mr3gwbYczgkrIuOHIDWu+EzyIpup6wlHf62R8nutiJJC9PsWrB+toezNEeRcsw2hXzKoKo7cSIydflRRoCPXSOzzP/ld+yEgCSmJOAZE4Y4Sifuyms4JqYS8v/oA9Xjf+r3DfVqQOkyJQ/NQNuy6pkqKiqTl9Byi/rfzZqtotSIkxSNv37FP2yRFReLPS8VQ3wmqytabEjnjyDWIgoojYOL/4haToNOqEGv2DVlVGFZw4S9epX/lMjLK2NH7Bem0shPYsD2V9A9Uao/Vk/1yI1tnxZH6lYKlsIpAdhLCimK6Ls6nJ1Eg6T8r0aWlIMeEBe/BHua03H2aX/XTANUxY2ftGLm9o0+AAlB6enAMikYXH7dP2yS3d+CJNaK0tBGoriX5W4V5347FIvpocIVpAUqzT0mC2Bug/KqMrCqcVHoStzSO4GV7IseYnUwvubD3+U3r08mdVUJ3ip7+o2tJntOMpUHC/P02ym/JRNfSDaJE5EY7vjAV+4XjQJKQTdpdlj2hvVsaYr7Y8ZtTUkOWlSHbf3ll+t6gi48jZGsH7iMHoErQMkLixKPWkWNqpNtm2qfn1mh+6qraozk9qogdK9PIO7GJh5ZO5SGDwuWjl+NUPIyYfRNKRICQhSZ2fA+upam4tiQRJsm4j8wl89HtyG3tdF6aT/SCcrKeaqf+7AwCFVWIFQe6dwcXbbhPs9+JISGoPh+q14toCa7PUlwudPFxqFHhdA2KoO10FzcPWcL/npiGwaGicyvc/djrHG/xH+DWaw4nD7bl8NrXx3DB5O+Z/c2RiHEeAg49MSt1SH6VjjyBjEe2IERH4hgcS9tgHaY2FckLUZsciOW1yF32fdK2luvGE/v8qr9s9vLfog33af6yfKOzETPSAFAHZqAMyiBw7EiQJNpHRNIyzUvWXd2UuBLRnd7Kokef5IvnnmaS2XuAW6453OxwxWBIc/Jl3QCGjtzBf0Z9TNpnICgQOqeQiG0q7vxshn5UQfLtZSQv6SHhkwraJviR2rrpOTJnl2O6Th/b++Xsz4h7qeCgDVB7QgtSmn1KFx+HfMyIvtu+KQrmHQPUwk2ILj+msmaqL+yHuT2AYZuZsLftLKnOZvWwD7GJJmyiiU96wplRc+SB6IbmEFbs9bLD7+x9XBdwMq74TOY4IjgruhCP08iHg1/nqsTveO76s+nK0GPqlGm/fAyuOJHmMQaKrhlGyft56Oxu6s/OIHm+hBJqwRUjIcX1TYwcWtyE4vnzX7h+mn/vUKYFKc0+pUaF055n6s0i0XFZ/i7fIt1pISjtHXijVCY8uJpjpxXxXOoCtuS/02e/M2zdvJ76/X5ru+bwsMWXyMeOoZy5YzIAdkVCEhU+axvGIEM7Gyc/x8SvbuI/N1yM/usiEv5XTPsgHSOvLCZ+jYvUL7uROl3EPb0SuWQ78S8VoXMruNJCif5gM41nZCLogrf/RYsluL7pJ4tshdGDUSYM+9U2SqGhdF6S/6v7HKq0IKXZp+QtpcH1ID8MS5jbZZQhmehSkgGQsjOwlHfiPHEwsWtV3v1+PG5Zz/hXbkVWd7vmXqP5Qx5sy6Eu4Nxl+wUh7cyK2M4r/T5jqVvkkn/fjEkXoMNr4erx5zBk3t/Ifs1L4wRdMKuLJGFuVln1/nD8IXrKZumRS8v7HFMIKJg+L6DjtEFYWmWUsYMAaL5k6K4NKy5FKvz1Ejuyw0HUh4dnUmUtSGn2K2uVA6ndieoO1pwSetzIYWZkvUDoti5UnUr1ndnEFfrJXHjVfqs0rDm05Xx/MV5Vh1XY/Z+857oyOHf72Wz3xTNu5jq+ypuHQZKpnJFG1oB66m+XkdwCzVNS2fZ0NtHvrSfp6w6qzoD4T3eWxBGtVqSIcIytwQX14W+vwvbBGoQVxQDEvLBql3Or/uAkoh/pkpOCOff67KSi9ByeFaq1IKXZb6S8LALhJuTySuS2dgACjc003hVANghUT4tE8IkId7dy0eOfE7dER6OsBSnNn/fB2Jfp9FuIkCz4VZlZjaNY4VGYun0KNzaM5uklJxBl6uGhVSchCgq5H1zHjoX9Cd+uUFaSRGBzKCkPrkHnUolbokdVVcROJ7oOHd7QnX9GhaR4/BkJu10o/3vZxyUjmrUlFz/SpqBr/jTRaoWsNJTikl/dT5eehhxpwxNvwVznRDXoaDgqBGe6TFxGG0fF7yDd2MrM8Hpcio9G2debgkaj2RuKvD7OXHIt8yc/w4f2kbyxLp/Ybwz8459vstqZycIXjsBoV/FECOhOaSPyHiM1f4d+f+9BLq9ElxBP0ynpxLxeFKxa+4PAsSPR2z2oRVtgzGCEdVtRxgzsrX4rWiyoeenB5zWANgVds49I4WG0X973Bq4YE4Vi0iEYjcGA9RPKkcNRx/8wDu/zI3oCWFaWo2zYiqITSXpxA3mPNNDtMvHl7PG0BULI/d81DP7gRs7eeBlFXh+V/l3vI2g0e2Krz8V3Hjjn4xu5Of9rziu+jAV1A4lcbcAbLvB/z13KR18cQed4H+3TXfjCBHIjm6meGkrSM3qEHjdlb44gkBaL5AVUBUGnQwwJAaDyUhXZEhz2U4w6EEQUo4QUHhYcuhMEFIOWO+GP0IKUZo/IXXai3yzss615UhIUbKFu1kj8o/uuCxFXbERYvTm4DkqWUStraT8ll54zxyKs2UzdNUNRwm3EvWQmfrULe8DMmsseZ91ZT5AQ4sAkyMwovRCvqi3i1fx+97QOpPgn93lO+mIW/7j1aoQED6eGbKGnKowpySVEbXYT/+Jaeka7Sf/XOuK/0mNYa8PSrFL3f1kIMkjdPtSIUHKu2YYzxYzOq+I8dSRSShI77hqEFBpK9uWbEb8PJmAWv1+P6vchfbuOrhPzkCLDUXp6EFZtOFBvx0FNC1Ka300akA3suj4j6tVVoMikzmtGWrqu74sUGRQZU1kzSreDHXcPJeqTLYRubANVQdFD6eWh1F/uo/4oC4/ErydMNHNEwRXMz/6Shc6BTIwrw6gN62r2wMmhxZz93iyurD0CAH2El8YzfJjWWbi5+jREn8ANkQWUXayn+h+j6PeKgJiSiCNFxBupEvHmarpudCIbVKTWLhAE2s8ais6tEr6hndBNbQQqq8l6pRHF7ekz9PdTIXNW995//b2knJ1T1jVakNLsAWdW+C7bRNPOG7yq2dC7cFfXvx/bXxyDd8poGDOY7tFJuCcO5PQpq0CvC2ZUF0SUEQ7emPoS4lYb/5oxmxa5B5fiY/3YtwC4IaKMu6KL90f3NIeISr+Te6tOpfDix3klZQW3NI7g+MxtSFUmxpy5kQiDG9misNITgzHCg6ETmsaZKLsynrSTKzG3CLinjSbC4iZxRQClswt5SykRb65C1YG8tay3IkCgoqp36G9vcfWPQDAYfnvHw4Q2cULzhwl6A62XjST6pVW9jwVJRPF4EHQ6pOgolPgoBF8AX5wN2SDSerWb6FctCAEVw8K1vavxuyek0zpMJH6NTM1JcN+xH3NxaBvPdaXQGbByd/S2X2uKRtNLVhXaFTcnrr+MdaPeZ9DqC+hpsvLhlGe5eN0MhIIw9A7wHttN2sUVqHnpVJwZSuZ/S6i9YiCx672IAYX2PBMxLwY/2/7jR6H/umi3aYiUCcMQZFUbzttD2sQJzT4nRUcS/UpB72PV70MwGGi7Kh81ECDQ1IzY3o1gdyIEVOon6kn5l0pHrp6aE3TYv8hk6339UOMiCVtdiyrBeQ8v4OaJC7k4tA2A68JrtQCl2SOSIBIrWXH2mOi/+DJcDTZiCiQ2eZPRfRdG/BoPARvEvGkBRaH6lDAkt4BgNpP01Frc0Xqke1oQf1IawNDp/cU8eeLyYi1A7UNakNLslpSZHpzs8CvsR6Qh2X4ym08QUGUZY3fwl1nQG2ifmEygvgF/qI6UxT7KLgzF1K6SNLAZw8uRGJt1lF0STueRqeQeUcl/l57MhaHBNSYXVx/VJ6eaRvN7nFR6En5V5qTsLej0MhcduZzognbmDEolYbkdb6QeZ3qAhvN8dE8bRr/HNpH671U4R6UiRoajSiCc1E7UKzsX3qqFmw5gjw5v2nCfZrdEkwnBakFu79i5ccxgKPjJL6soIYwcgFBSAZmplF4VStYNa9HFxRBobOqdqu6JETC3qPhtAnHPrKLz4nGoIuhdKhEratl6ZzLjh5dyVfxSOmQbJ1vs6AWJrT4X/fV6bdKE5nd5xxHFSZZaqgMSw4xGOmUXI5fcQP83VQx1XVReEI+c00NuYjPOB5LpytATudWL5PLv/FyPGYw/1ICppgvaO/t+/gFh1CC8USYMC9f+Znuk8DBIjOtNpqzpSxvu0/wu/uNHIQ4bsMt2xePZ5RfUH7rrzVx/iAEhOYEd54bTb14AVAW1x4UUHkbUq6uIenUVPRl+Yj4tBYKLGv02gahNDhpP9FN/ehr6DpGhobXMaR/H3a9ezHpfMGdfnsFCq6yV59D8PnW+SPyoDDMaAYiQLLx71MvUTzRR818zid97Sb9gMz33JaHv8nL7rDk0jzFRPdWGMHJg8CAFm9AvLkK1GBH0ejpm5CNFRPSeQ127uTdASZnpuKaP7X3OedZYdP1SdzZIp0OxGvd9xw9xfypIPfTQQwiCwKxZs3q3qarKvffeS2JiImazmaOPPpotW/qusvZ6vdxwww1ER0djtVqZNm0adXV1f6Ypmj/I8E0xysbS395REPCHBGcwqeOHBhPEKnKw7Mb2HfT/ZxH6xUU4zh7L9n8OoOq6gXhPHk3dXeMx1evpnB1B7NoekGV8x9q59r2PMdQbcCWp3H7mPLyKnjWvDIcxds6bdwNL3BKyqnBOycX7+B3QHCruiCojVrLSGHAyq3EUAP+oOB1/lpu0mx3UHGdAGT8YQ1sPFGziP8+dR8qr2/Cm+FDX973vqRSXEGhqJurddcidnbs9X/fQWKyfr+99HPLJegLVtb2P5bZ2bZhwL/jDQaqwsJCXX36ZIUOG9Nn+8MMP8/jjj/Pss89SWFhIfHw8xx13HA6Ho3efWbNmMW/ePObMmcPy5ctxOp1MnToVWf6tIuaavU0NBPqUDfjlHVVCtgWvrPS17aj2bqTMdAS9ASk7g/q/jcJ51lg8kQJxBeAb6KJhgo6EVR4sDSotbaHsONNM7d9G8MLwd1jv6odiAEWvcqRlB8fYSujuD48Pmct7pz9Dlt6OJIisGPLxPn4HNIeSHX4nDbKBU8LXM8cRQf+Qdj4+4gVKH4wkbaEXVSciNAc/x6oIxEWT+rEU/MKVnoYycTj+yTvvxYopiQj63U8HDy3p6LM+SvX7DosihPvbHwpSTqeTCy64gFdeeYWIn14KqypPPvkk//jHP5g+fTqDBg3izTffxOVy8e677wJgt9t59dVXeeyxx5g8eTLDhw9n9uzZbNq0icWLF++dXmn2Kl1CPD1njkUurQBRQm5sQnY4sA+PpfG6UXQPiiKqxI+pI0BIrUxnjkj8x0ZEP+gLShH9kPCJgez/24TOBXf+fSafvHw0oeWw9KxHuWLbhYwyymy76Dlmt+Yzu308K90pB7rbmoNQkTeJMl8c77bmU9TTD4Brtp1P/MdGKk7X0zrMhNLRBUD8kytx5ETgtwT/DLqzYtBvqUG/uKj3eK6caARTcMiu54yxwVIdP5C3lu2VNgujBqEeMWyvHOtQ9IeC1HXXXcfJJ5/M5MmT+2yvrKykqamJ448/vneb0Whk4sSJrFy5EoCioiL8fn+ffRITExk0aFDvPj/n9Xrp7u7u86PZP6S8LDCbCF20Ffe0kZQ9MZqeaSORsjPwRIr4wsAbKtIwQYcvVEfTRR6sY9voiZXwhalsfzEbQ4/K/Q+/Qt3sNI6bsQpLg4fLrl3AsTNXYxIEZue9jVHQ80lPOKE6L/fFLWOqtfFAd11zEIqVgiM2hU0pnBG+lpmx3+J/L46GiYAqkLSwlZbLR/ZeHVnmrSHk/dUA6BetRW5rRz56RO/iXOOCQpQfRoFCv95KoLF57zd6UxlSkbbM4pfscZCaM2cO69at46GHHtrluaamJgDi4uL6bI+Li+t9rqmpCYPB0OcK7Of7/NxDDz1EWFhY709KivYte19puW587y+wLj4OAjJypI2OaQMIWVND0jIVyydrkUvLif+0guiNMp0DVVKW+Ajd0ExEiIu2+jDsOQrWWpH+/4OeOJExRg/qmnDG2XZQfpGeGm8kAy31bPCFcur6K/Cqfs6wdfNs0hoiJAs2UStVoNkzR28+jX+UncbT/zyH5Kvb2eJNIlHyEVXUSdYNazDYBTwpYfhDglWidf379ZkU8SNdjx9V2XXYTu7u/n1D43tI9XpRPJ69ftxDxR4FqdraWv72t78xe/ZsTKZf/iMi/FAq/Eeqqu6y7ed+bZ8777wTu93e+1NbW7vb/TR/Xuzzq3rH2TsnpqM2NKOu3UzYO2sINDZh+XgN/FAxt+uodPQ9MnJkgKj7qvCmRWJ+OgJ9p47+83xEbgvgSDESf3o1f286AmVUN8/XHE1aeivrOlK4IKSRh6umsHbUu9o0c82ftmTgx3w3+EPeePgxSu/sz2NbJmMSRJqPjECXEE/sOpnof1aSsLwH1e+j6pxEyu/I3SVzv1q4aZ8EI80fs0dBqqioiJaWFkaOHIlOp0On07Fs2TKefvppdDpd7xXUz6+IWlpaep+Lj4/H5/PR+bMZMz/d5+eMRiOhoaF9fjT7yE9u/IZts6P6/AijBoGqoouPQxyUS/lbw5DysrB9WIih1U3/2SqFpel4IvXIRoGATaFqqoGnnnmGdx54lNywZkJ1HkYl1dD9ThLN9hBmZ7/LIreVhXnzkXZTLfVle+Iu2zSaXyMJIpIg8r0rg8uP/ZZTMjYz8clbiS10IHd0ErK6mu6bEtHXBRO+Jj+0kqxna1Dc2lXMX9keBalJkyaxadMmiouLe39GjRrFBRdcQHFxMf379yc+Pp6vv/669zU+n49ly5Yxfvx4AEaOHIler++zT2NjI5s3b+7dR/PX4EoJQdDr6Mq10X3eOJTYCHxxVhSPjtKropBCbdSeGEbefzeT9pGAK1akbYgObAFCKkQuePkmuhQDK5vS+e6efK6OW0rhAy+w9Yi3calQ4d39lxKAEpcWpDS/X2PAyQlbp7Ld38MTb0ynxR9CZU8Uoh+c/awIeRm0ntAfR7qV1mN33i4I1NVrV01/cX8648TRRx/NsGHDePLJJwH473//y0MPPcTrr79OVlYWDz74IEuXLqW0tJSQHwqEXXPNNcyfP5833niDyMhIbr31Vtrb2ykqKkKSpN88p5ZxYh8QBDxTR2P6fGcuvvbL8zE6VGwfrEE0Gqm8cwSKXiXzqR3suD4DxQh6h4BsUoncouKKERl67mZ22KN5f8BbTN80g1GxtWRbmjgzZDMJkmW3V00azZ814Llr+W7mI9xRfwIbXxqM5FdpPdGLVGsicXmA+qN0RGwFR6pA2ud21PVahdwD7fdmnNjrRUtuv/123G431157LZ2dnYwdO5ZFixb1BiiAJ554Ap1Ox9lnn43b7WbSpEm88cYbvytAafYRVcVaYeen3ymjXi9Ays1AVlU6zh6OpSn4fUZubiF1YSKuRBNdGQK+KBnZIOGNhBiDk+/rsinJjKBg+Ac/OZpWBl6z97XJPUzZMIN7L3mHMNFEsyeE0Avq0QkK4ivJdOYIWMo76b+gnPo7xhOxXUFqbKO3IpogIGX0Qy6vPJDd0PwKLXefBgBBp0MNBOi8NJ/I2YWoskzXReOInLe5dwqu45xx9CSKODJkJJfI8PHbKSzrh9ipR7HKiD0SSmiAsXkVlHycS+HNT2kTIjT7nFf1s94r8mTjcWxYlEv/txvYeksceY82se1vCcSshahldWy/PoWsh0v7pPsS9AZ6ThkenBCk2a+03H2a302XlkL3mcE0MtGfl6IGAkh5WQiyiuL8IQv5mMF0ZYqEVQa/gwYi/bT/sx8hm42cNrEAdCqXT/6WypP+x5z0b3DHqsyoOv6XTqnR7BWVficD37uBLsVCwfosJpy0gdrTE1FFlUBlNdYaEZ1bpeGUVLIeKtklH6Xq92kB6i9OC1KHKdFkQvrh24vq8RIyJ7ig8cdfYrlkO5GLK3CcM5ayp8ciyAqh1Qq1J6voot3oW/R4I3TccdX7XBG1HESVFn8IjYFgUDPndPFu+re/2gav6md44bn7sJeaQ9lWn4s4yYCY5KZdtvH3SZ+jqAKmNhWE4EL05LfLsH5cQMwGF3KX/UA3WfMHaEHqMCWkJeMdnQVA25QMPFPHIEVH9dmn7cQMmsdBzv/sqOu30e+a7Qg+kQEJzcSOaCbyxmqeLJvEtWXnsem45yh4eBSvdwXznhWNnv2bbTAKegpGvbP3O6c5LJy87Hqe68pj61Gv817jGGaE1vLNllxCq70MzasmEG6h4/gMRIsFYUXxgW6u5g/SgtRhSi4tpyvTgBgSQsQbq7AV16PYHYhD85Cy+qMcOZz2oSpirAdHdhidF41h/dIcBL/AltX9GRjZyMjwGgKyiCioDFt2Dfm3FzAjvIgx689CL/y+STC/dz+N5qcWufR8MvF5Xtp4JJ+7QhkXWcmQl28g764aBFXFf76EICsYuxWUnp7dlqPRHBy0IHUYi35pVe+kiEBdParfhyfBRiAmhIYjzQiyQOw8I4okEPF2AdZaiNgscM4Jyym9bxCSoGB9K4yGzjBOzyvm6ujvSdDZKBj+AW1yzwHuneZQttmTwsutE5nYv5w71k1nWWsWnhQfrpFpRDxQg9rTA2tLepdUtA0Pw3n2uD7HkHIy8Z48+kA0X7MHtCB1GBP0Bnwn9v0lNXxViLByA/3erQNBRedRib22ElSFuLc34jrRQavPRs0JIg7ZxFOPPkPx+Neo7InCoeycyTdlw4z93R3NIeY7T7Da7o/uah5Cyw9ffvyqxFffDWfZikGYVtpoXpBC3h0VmJZspO6ZLLon56JLS8Z3wigEo5HIN1YT+lkxAKLVSmDSSOSySkyLNx6Irmn2gBakDlFiSAhSTMyv7tN85Sj0dh+6/v2AYDFD+ZgR9Jw5lqZnTJwxaTUtF7vZVJRO4035iLHRuDvMXBC9Ckuyk3xbOXl68Ksyk6O2kqNXeo9dOGLuvuye5jCgqCKyKnB5zQTmOsMYaKln4uqZPNeVwrz/TiJmQCsRWwT8IZD0yiYUezdNV40kdIeTsLWN1E1LwtTUg5iaBKram8RVDQQwNjpAkVG93mAgGz34APdW80u0IHWIEuJjCGT9emqh2OdXIqzZTOm/IxCMRjryLEhL12P9cA1xt8h8vDCfuNdMCCqEVcq0H5FA3j+qmPHJTP475CNOszqxiAaa5QD/XX4So1dduX86pznk+VWZLsVCWyCUV1OXk6TrZEV3FpflreTjG44n/LJarklfhj0TfBEKQnwMqqIS98xKxPJaSq9LIvnTOtRtFXQP6ftlTfV6kUu29z42LFyrVdD9C9MW8x6mdPFxyAnRtI4JRe9U6coRCCsDS2sAQ4eHQIiByotVEubrmXz3cipdUeRam3EpBiJ1PTyz+lhuHb+QC0O3EyaageCU4DyD5QD3THMomLp9Cltr45k+sJgjQspI1HUSLvo4tWAmWbGtbN6URt5/6vCnxaDqBESvjDPVgm3uajxTx2DvryNqsxfdN0W/fTLNAaEt5j1ciVJvjZy2q/IRLb8cNCqnh2JpVeh37Xb6fdpN5Ecb0Dv8lF2vx1jdQVpiO21nukk1tJNs6uLjZ45lRWt/kg3tPDFxDora9+OjBSjN3vJp1gJ01SYeiV+PrIqECH6mrZmJp9VM5YL+JH4LzmFJ5D61hYBZh+j0YZu7Gik6CoPdT9wzqxBl5TfPI5pMtM7M3w890vxR2pXUIUaKjsJ+bBa2uat7Ux3tjq5/PyouTqTfY5tQPV7UgJ/tL44iMa2dJJud2uezMHbJmJZsxPVZIv434xD9KkuffB69IDFh43Rywlt4KGkhsZJ1t+fQaP4Mr+rvk1Yr57VrEAMCnrgAgiJgS+omUBhBIEQlbXQd0gmNtF80mph5JfRMyME0v+BXjr7Tr/2eaPadA5ZgVnNgyW3t2OYG6+Xs7hdPiomh6qos0t+oImmph6pbBhNSpeKOFch52Y5YbafHaMSS48dzeyeW5UastxkpvVrliiOXMtcZy91LpyM5Je4/7RO+6knj4tC2/d1NzSGs2OvljBUzWTPxWea7o5jXNgJXQE/Sd35qJ+nJvqaA7vPGEb5FRWguB0BNiKbt4mRiFuxA6XFj29zUm0TWO2U0phYXatHuM59rAeqvTRvuO4SoRwyDcUN+dR+5tZXkpS6QZaSl60j71E6/K7YTvcGHuq4Eua0dx6hkHKkGupbF4xuZSdX/6Sk97XleKTiSjoANRAjN6iTf5KXAkbFf+qY5NDUGnEwqmQaAS/GR+c41rHBnMjl7G4+35fPkHefxz6QFrN/Wj+opOrIeCAaa0PdWgwJb7+mH6nBSNS0cU5eC3NxC26Ujkesaes9h/LLwFwOU5q9PG+47VIgSnpNGYl5Y3Fv+/ffqmJGPPQeiNqqEvrsa0WSC3P6UnxuGqV0gcmuA4x78jqsiioiVrNgVN7c3HMtLyav2UWc0hwtZVehU3ET/MGS8w++kVTYz460beOni57EKPs789lrMlQZ8uW7iPjVim7sadfxQuvubkbwqIZ8VI5pNqD4/isuFGBLSu0hd89elTZw43KgK5vqe3wxQTbPGo+uXCoAuJRkpLhZBhdSFXlQxeK+q7voRqIKALa+TV699ioQ7y3lt/XgmvnIb51ceQ5hoptNn4fzKY/ZHzzSHIFlVGLzmfCRB7A1QABl6G+NMEluvep5t3kQeqD0ZfYsed5oPfZmZ1tPd6FKS2XG2mbYpXsytfpBl5C47isuFFB6GIIno4uOQcjJBENClJB/Anmr+LO1K6hAnHz0CQ2M3cmlw7F6KjsJxZCaWT9diP280raMgb3g1Xc+m0jxaRAxAwvIA9cfoQIWEYU2Igkq8tZscWzPROic3RFQf4F5pDnayGpx592OlZr8qoxek3v9eXjOBG+KWECf5OeLTWyDUT25qE83vpeGJEnAnyuQ+204g2tYneWzXRfmoItga/bTnGUh4sQjHqcOxzV19ILqp+RXalZQGAEPxDtTqut7HjefkYKt0UHfHWCI+2YSpRSRwWzSJs8rJeqUJySVgbuwBBWLWqbSuSqC6PoqS1jhKnXFagNLsFSeXnsImnx+Az3os5HxyLe84ohhbdD4AqeYO7q2ZxsR3b0OM9JE6V6L2i37oT2sl+VsnufdXIpeW75LdPPKTzUR9vBn9orXEP7US1evVAtRBTgtShzi5y96bDgYg9rmVqILAj2n2kh9aieALsG51FrR3YepQqTgjjLAy6MoUyTlmB5UnvMrGMe9R2hZLzuvX4FQ8DC047wD1SHMwu6N5GJnvzWRBzucMMxoZve5sbik8m6zceqq90RSOfI8hBefhUgycEruBqI0qpo1mrJubSHxsDbwbjT/UwLaHk5GyM9ClpSBFRfYeX3E4EIwGOi/V1j4dKrQgdZCRoiLxnTDqTx1DXb+FuCI/9VcPBaDsdhPR68E/qB+xa52IAYH773yN/FM2UvptBudWHgvA2lHv8vmFj3JT/SRWjXqTq+vyexN+ajS/x/2xRRSd80TvMN+yYbPZPPEV7F4Ty9szaJRd/G/oW1T2RGEVvTSPhZ70AKrJCIpM2OzVdKfoybm+DNWgp+TOBFqm5/Q9SSCAqVM+AL3T7AtakDrIKM4eLKUtf/j1gtGIlNUfQ6cPU0fwdmT2fd04UkUkp4/qk0LwpPkYYezgnOg1yCaVI8J3AFAVcNGlGIIJZkUD50WtIUw07JV+aQ4PekHiW3cwl94Ov5PygIJR0NNSFk24wU2yzsblxZfQ6bXwyOPnkvGhl+RFAg0nxLLj3WFI0VFEvRosMaNs3kbeEx1EvVpA09/G956jfsZAbFvbkQbmBDOw5GUdqO5q9gJtMe9BRvV6CVTV/OHXi0Yj7v6RGBauJWJlcLW9XF5FfEE4Yl0L6U83UH1NHmN9N5HYrw0U6G9sxql42OaPpj1g6128e7RZAbSJK5o982n7cN5qNDE0rB6T6CctYiMVZ71IY8BJ1tu3cdyx6/l2/gjkdJXTr1vFB68eS1h1gPiXtoLR2JshouX68YRV+rF02Il/Zk3v8eOfWIkMOM8eR0i5Dlf/cIxbD1x/NX+OdiV1mJG7uzEsXAsE60nV3j6GlmvHoltSRPfE/rSfkougQGipjrSQTmLWK1y/5CLOLT+dJMmuZZfQ7JFO2cX9bbl9tt2X+CVbm+NxKQbefP84jttwMbc0juCoObchBuDLDYOQzSr6boEPKoYjm8C2qood/xrBtmcyabhxDOr4obhjVcxLS9h6fz/EQbteLbmiRVSvF+OCwv3VXc0+oAWpw0zL9eMRdD9cQKsKKQvthFYHQJRoHyjROlrBb1MZc+4Goo1OJt+9nFNGFuOXJWQE/KpMsdd7YDuhOWjoBZF0YyuDVl/Qu80kCAxNrGfetqGUXPs8uZHNzFs1Gtms4Ev0MeDfzcQWKsQVeJmRtQrJB9WXZ5L1WitZl6wjqsRP7XFWUha5cU8cQPbVhSgbt6FLiN/52SZYigaCaZGE4QP3e981e4c23HcY6Loon/C3g9khYp9fg6oqSNkZuNMj8ETpkPUC8vRRmNrBm+GHbiOrPxxKyklVOH1Gvhs8D3+CjF4wUBdw8mD9qcztv2SX85ywdSoL8+bv7+5p/sJsookLQto5d+zb/PidOFayYpb8fJj/Eo2BAN0+M9YaCV+ISsQaParLQ9M4EVWnZ113Kkmvb0G2dyOrKsLowRibXcSttWCobkOsDS6v0CUnsf3GVLIe8oIo0H10FtaPgkOAxi8LOegWg2p6aVdSByldfBy69LTftW/0tzvvYbVcO5buc8eiGvRYNtTSmSvQdaILZ4KE5FXJeEUlpAKSvu0mwdyNXpLJfG8mOV9fxXNdKUx+/XZe67dgt+d5sP/He6VvmkPPj7P5bmsazvTy41i2ZiCPNh7P+AU30+yyMf2CZRx34jqcSSKCQU/kZhC9Aq0zk1AyU5DCw5HyshBdPrqzQ7AW1VDyfwm9x/elx5L1SDlyZyf1F+YStmbn2kBd/35IcbH7vc+avUO7kjpYGQ2oZmPvQyk0FNnhgN0kEAnU1QPB8vChVQHcURJNEyNRdJEEMtxkPKFQc5uDB4Z/wCv1E3G7rTgcsYiCymkJG3imLRy1ycJocwWLZzyMTbTttkkjjdpMP83uDVp9Ad+PfoVH4tdTE3DycdQgRpsrWBeXQltnCLMGFXJl1TSU0d1sTUwl954SohfoEPR6uiakEd5oRrYaEbvdBEwCgeYW0ufuTHckG0XE1lYA4p9ayU/zmqtmI4Jfy3R+sNLSIh0i7BeMI/KLUuTOzl/eSZQQRAEEEUQBAM+xQ6ieDrHf6Yj8ZDNVtwxm6rRVrL1rFIPvL2bRgtFEjGkm1ODF6TcgCSpfDHwPm2jaTz3THKw+67HgUoycG9LJrMZRfFE2gO1HvcUOv5MTlt/A34Z9g1028/anx+CLlkmbr2JP12NqVxAUCFzcjn9BDF3D/OQ91olcVon7lJGYP/1Znagxg9G1dhOo1LKhHEy0tEiHmbB3ViN3diIYjSgTh+++Iq8igyAipSTCkGyk6CgAEheJOJMFuk8cgD/LzbKnx9EyXM+CrYN47ILXWDX0I/6RPp+r+33Hd4PnYRNNtMg9FHj9+7mXmr+yTtlF/8WXURNw4ldlZq06lyxDMw+25dATMBI310xjwMm0F2/HtNHMp9dPYuG9EzEN7iJ1AbQO1WOrl3GkiYTMXUP0rcHj5t64KZh7UlWwbevoPV/T38YjZfVH19yF2tmFrn8/BKPxF1qnOVhpQeoQI5pN1E8wI4aG9G7TpaXgPm0MjBmMb+JgnANjaRoXgj81BtPiDQgKxK73E7Z4Ozl/b+Xjfz2CYVwHaqeB0cZgAcXPu4ZzhLmq95hVAQPfOgfs7+5p/qJkVcEi6rl91EJKfFEoKJROeoVL1s/glZUTqXeF0XiGD5MgYmlSid7oR9flpeEogYzINqzb27HWq4R8u42I7TLOM8dQc2oMsc+vxDNxUDCjuar2JkqG4LCec0A0juEJkBhHT04Molm7wj/UaMN9hxApqz+CrBCoqOqzXdDpEMxm8AevfBxTh2JpCObza73DS/w/QLHoCdgMGAq3U/rAAFSjQkipnmMvKODJhLX7uyuag8xHzlC+tefxbNIapm6fwpbtyUg2P9kJLXS+nIr9DCe6VaEYO1SiirupOj2Ufg+sC3429TrK7xhA+DZQRYj9th61owuMRuTWVkSrFdXn320ZGtFqpfGyocS/uHaP66hpDixtuO8wJPj84Nt1CE4NBIJpZDweFI8H64drUAwSrSOs2OvCaH5QpeoUG/mPF9By3iDMjRK6ED/uUS5aPCG4FB+f9Nj4pMdGY8B5AHqm+as7w9bNs0lrqAs4qfg6nTNHrUVXZuHT7M8ZNGsTupWhqCJ0Z0L0c/UMm1RK9Z0jaZoxlOZzBpD5VhuyEUQZ8AeQu7tRUmPxThmNGGKj6Zrd56tUenqIe2alFqAOYdrsvkNIoLr2N/eRIiJwHJONZV4Bsd+JRB85BF2XiLqlkHfTRhM1rZ2w2ZHYseCJUaiOjKAyIHPL/AtBheuOX8TNkRX7oTeav5pO2cWVVdP4MGPxL+4zpegqIo9sosNn5a5z5nJf6zA2PjcE5ygZc6NE6pduVtpySVqmwiCI2ObFE61H8PoJqQvgDZMI1DfgPXk0uh4Z0a/QOTGduGe0KtCHKy1IHWbkrGSaR4tIg/PJOraCwPRqdtyUQ8SgUfR/3YfUY6RiuoCxC3ac+yK3NQ3n+rJzefjkd5lobuxTRVVzeLGJRu5M/gIILjUo9nqREWiSQ7lx9XncMPxbXD1GoqwuSp4axJaL4km02Wk5yg+qgKBIIAlkvu9GX9NGY34qpqIKTFHh7Lg0gf7P7cBqMBAArEU1qIEAqCr69o5fbZfm0KYFqcOMP9xIv89cdGeY6dyUhvMcicyXaug8Ipn2WS7yE6so35ZHT5xIi9xDh89KVVUst+44ly9OfBKT4NGmnx9GZFWhU3ETLVnRCxIjjVLvc8XeFOJ1dm5YeT63j1rIM2+filkB/WoY/cQaPvl6HD2tsaQXe1HvaKXGHMmOgQJqQE/25Q1kvh+Bb1g6um4vKYvcOMf1w/xpAS3XjUfnUrE2BRD9CvrFWpA6nGlB6jCjXxScBBFpz8adGkZMsYeGU1KJf6OYnvhhLImOYO0ljzFyyQ2M+/gWzMkOtkx5DgWF9x39qfNFck9MyQHuhWZ/2RFwc3PlmczP/hKn4uGupiPZ0pXAUTHlfNWQx8cD3+TEvBJeqxyPJ07h5AlFLLKMxuqIJXILmDoDtA4zovskiZy5ZdRcloXkJbjovHgbvqkj0H2/EVFRsUgSKpD4RT1tExIxLd4AoKU0OsxpEycOQd3njdslZZKuXyqC/icZIeqasBTXILn8OCa4UFwukt7cimyECQVXMiarEtUi4/dL+JHZ7NNzeViTFqAOM9l6K/Ozv+St7mhuqp/E5LAtHBVTztuLJtL9fRzNsp44QzcWvR/JLbC9OxZrg4pH1jHs+mLaLu3Bme2na3CApjMz8YeoCEpwWYQ6Ig/x2hYEg4GmG8ei+n1IMTGoXd2Ev70K1e+j+cpRiKbglbsuPa3vZ1hzWNCupA4xHZflE/lGAQGlb2VSx9B4bHYHikMJjvUnxeFOCqVukgH9tmD2CaVfIhkf9RCwmujwpHLykxsJ17t4vH0UzoCRcQnrDkSXNAeAV/VzwpazWDroEwAuCGnhvJBm9IJErLSJNw1H4UqVmdc9gq8fOhJ9j4I0QmD71mRie1R0okKi0Y6nJoTcO4up+vsIwip8xLywFikulp6RaRi/KMR4PChA/JPBjOVKciyi1w8/ZE6JfX4lyg9tcg6Mw9reiazN5DusaOukDjFSdBRyW/su2zsvySf6i3Iaz8oi7uUCSl8cRtgGA4nvbgNBIJCVTNnlehBB1MuEFJixj/BiCfWwcuz/CBPNuxzTr8o80j6Au6JL90fXNPuYU/HwQtdAbosMVmKu9DsJEQXO2nY+k+JKafPbeDJhLUMLzuOYlDLmLx1F9oga+tk6WFKRjV4vE25x09gaRkyUg+aaSIyRbsSNIURvCqBzKxibnDQdGYmxSyHsndUoRw5HXLExmA3lZ+SjRyAt1b4YHaq0dVKHqR8DVPMN4/tsj3hzFf6cJKI3uVEDAQb8u5nwcj+CXo9id7DjbDMJX+tIS24jPsaO9ygHSfN1xIT08Lo9jxFrz+Hqunzuah7S57gmUUuNdKgQEbGIwauUozadzv2NJ/JI2xGYdX6mhRZzc8xStvt7uCp7OZ+uG072iy3Uf9KPGIODkEVWLJ+F0uG0ELfAyF1ZX6DvkPC2m4kv8GFbXo6+y4ti0mPqUAh7r5CGW8cjG0V6Th+12xLvol/ZZZvm8KMN9x2i4p5bs8s2ccUGHOeMJUSUUE1Gqk8VkC5MxLAtg4EjKtjmTYd1CcSvlknslqk+WSDSr+doSymro/pT7wrn+awV/PjdRi9I2pqpQ4hXDbCqK4PR5gpGR1fzn/hgRVt9XDGf9ETzcdsICr4eSOzYJjZNeYYTUs7HKLfy1aNHkXhFJSVr+xH2dQgh76/mEe9FZK2qQA0E6JqcFZxG3t6BMmEY4Vu6UBSZpG/tKCY9+iXrkHczoCOsKN7P74Dmr0gb7jtMSAOyUStqEONjUfU6aG5DdjjQpaVgH5mArcaFJ9qE3yZi+7AA74mj8NzQyZjYahYtGM3D579Bgz+C0ebKPiU5irw+LEKAPMNuEtpqDip+VabICzl6L2O+v5ZAt4GoQgnjWc00b45l2NhyKt/OwnWck8gPrDgTRFxJKhl3FdJ1zigUvYCpS8ZnFYlaVosvIxZPpAF3lEjU/37/YlxdUiJtk9IIf2vX18hHj0D0K1oAOwRow30a2i/P782G7ouzgV5PoKqGQHQIQnQkqCqBqhp0boW2oTZqThJxJksIksSw+9fj9Bj5YvtAHjhvNn9bfCH/XTmF1e4MABa59FxeM4EqfzQNcsivNUNzEGiTe9ALEuNMEjIqa496gcRvROxZ0LEqnmFjy9k+LxtPlEDMO2YG3LIJxQiiFxynj6QzT8ATJaDoBDpzBUruTcBQ0UrId2XBACVKSOFhtF6T31viXbRaEa27Lg5vPCWN8LdX77ad0tJ1WoA6zGhB6hAW894GFJcLAOnbdSgOBwDCqg00Hr+zqqnxi0I6B6lkv+HEOdKN54ThLJkzBt+2UBLfN3DnutM4dcw6bFEuHv1uCk7Fw/07pvJ9ZQYTzY1MMu9601tzcJmyYQZ2xc1tTcOZsmEGeiQ8EQJiAD697BHWF2USv8aFpVklZGMLS1YPJnGpg9TFHvQ9Cr4EP4oOXNEi6fevI++OCgJJkWy7JwtBb0DKSsd1RDah1QEYnAOCQMuFQ2g9b8gu5TXi3t642+KdP6UeMQwpJmZfviWavwjtntQh7McA9VPNN4wn/oUCYl7sO5SS80IbanUdim8Q9UfqMHUAIjSPkaDCyudNozjv6BWcPqwoWE9qdQInnlyI5Yfh1mKvl0y9qmWjOEgVjpjL4DWX43EbuGXY12z0SXQOUhgxbAdf9+RibBPR17UTuaIWdUA2/ef50LV2Yx8Zz6T/W86ay4cjtbUQqKpBBWSvF11oCFk3bkSKj0OpbcD4Q5mNH8NP9Eur+jz+kdLT0+ex+9QxWHd0o2ze1rtN19KN6vHso3dD81eiXUkdZgwOFVVRkXIyCUwa2btdLi1HDQTIvmwtGfesQwhAwnKZCZM2MWJiKapOJc/cwFnzb8Cl+NhwxVM8nVjIi125vNUdzRsdR1Ab0GZjHcw2jX2XzRNfYYS5ivM/vw5zopMdHdG89dBUQqpV2iYmI4WHYX88gGIQCVTX4UyUeHv5BNS1mwlU1ew8mCDQOSZ4te4ZkIwYFdm7HUHoc17fCaOQMtN/sV3mTwv6BCgAuayid2RAc2jTgtQhSjly+G63Ry8oB0VGqapFNogoE3/Yb8xg6m8agxQVifhVFNYmBVUHm14YTPnrOZx7xCpOstTyxskvMd8VQ97iq3muK4WpIZs4xVrDkwlrtckTB6EWuYcTtk6lLuCkRe4hd8G1rHRlEZXRgVochvJNJO1DVfJnFaJIsPWJTEwPR6Bz+Gm8aSxxz64hY64P6ccg9CNVJeT94H0l3TdFBGrrAPBPGoEwcmCfXc1rylBqG/ZLfzUHHy1IHap+4f+s3NoKgOr1oncFUEUBQacjEGIg+cVNyJ12/PfGEbGoDNv35Vib/CjTOhAFlVvrj+c/1Sfx7O3nEPm9kUe/m4KEivizb8aag0vd4lTWeBKJlaw8evT7REpO5g5+DWGYHccID6oI370ympjPt5P2oUhPgp6aE4MTHqTIcHR2LwDtV+Yjhvz6JBr94iLUtZt7H0vRUchddlSvd5d9W64fv8s2zeFHm4KuoeXa8dgaZSzz1iCFh1F25wBstQJ6h4qggu78ZpJsdhqezCT5pjKKF+fii5SJ3ChyzDWrGWipZ1NPMldELdeupg5CflVGL+zMbp4xdyaKLYBolLln9Ocs6hjEqjW5SB6B5CU+GscbidkYwLaiksazswipD2D+pABBpwum3NoDnZfkE/lu0W6LFv6R42kOHtoUdM1uCUYjUk4mAK0zg998ExY1Ypm3BnX8UHzDM8h+upruTIWIt1YTvbyRxh0x1D6XRejWLuofzcIbFyA1pxljl8LXr+djEvzMLx/EMlcWHzlD8asycxwRTNg4nR1+rZLvX91PAxTA2jMeR9+iJ3W2xKOvnE243g3RXvrP7ca0qZaUxT3oHTJ4vcS/tQlPmIQuLQUpaeeMUUSJ8sfH/ea5I95c9YtVdbUApQEtSB12BIMBb3IYADEvrkJxOJDLK4PPrdyAobiS5hPTyLy5kM6Lx6G2dZD3n3qsTT4qzo6i7jiV4QMq4akYmk71YWlViJSclB75Ft905LKsOwe/KrOkawDLh3xMht52ILur+R2cigdZDU56sStuNvhsJK4I0DrcQNxJtax7dDip7+gIhBlRuuzU3qSgCiCEhVJ/1WDccQJyTBi+lCjKnhuLODQPVIXkbxSkiAi6Lso/wD3UHMy04T5NX4KA45yxhK9tRm1sgYwUyi4OJ/1zL22DzcSudVJ7nI3c48rIsLVR6oijv62Nx+ILGLPuXIpGzj3QPdDsoenlx/GPlAU82zyJbZ2xfDdkLtv9Pk5fNRPdFitpT22i9NksnjniXW5/9TJ8g12kvipRcY5I+odqb40yCN5jUrrsO6+CBAHRYtllWrlGs0+G++69914EQejzEx8f3/u8qqrce++9JCYmYjabOfroo9myZUufY3i9Xm644Qaio6OxWq1MmzaNurq6PeyeZm/S9Uvd+UBVCf+2gqrzEqi7bijN4yOw1YiookDEdh/br9YTMKtsaUxgoKWecIOLNq+NN7oTOTapjGllJ/JWdzRt8q//UVrh0aar/1V8nPk1flXimPCtZIS1c339BC75980EvDoEGaIWSlw2fCWzCs4laVkP2Xd1YGh3o+vQYVpfTcdl+YhWa28G/sbrxuw8uKpqAUrzp+zxcN/AgQNpbGzs/dm0aVPvcw8//DCPP/44zz77LIWFhcTHx3Pcccfh+Ml6hlmzZjFv3jzmzJnD8uXLcTqdTJ06FVnWshbsNz+bjVd/SjLuU3f+YfEOSsEboRBX6CHmpdUICvhtOhrHGzDUG8j413qSX9Izp2E0Vd1RbG2P5YElp2KRfOSENHPP0umM+fYGIHhT/qd+HFZ6qObkfdxJzZ44//srafRHMLvfUl5KXkX/GdvBoSOu0Eubx8riplw+G/883kgj3n7RONNDUFM8yG1tRL62Cs+EPOouyQEg/qmVB7g3mkPJHmec0Ol0fa6efqSqKk8++ST/+Mc/mD59OgBvvvkmcXFxvPvuu1x99dXY7XZeffVV3n77bSZPngzA7NmzSUlJYfHixZxwwgl/sjua38N51ljC1jX33ouKf3EtYlgIP4YT3ZIickviCTQ24T9+FJJHxfR5Aelb06GlHeeJQ6g9RSHea+KslPV8155FhzGMt4ryEZwSoandOGpCeas7midKJ7N+9BzmOsMo98TT5Avl6cRCjtVqUP2l6IwBLg5bDwTvIb6d/hW5264hYJUQ7o+n/ngD91hOQfIpGLbWQV4yaquRrovGEbGlG9OyzSSvMaJ91dTsbXt8JVVWVkZiYiLp6emce+65VFQESzVUVlbS1NTE8ccf37uv0Whk4sSJrFwZ/GZVVFSE3+/vs09iYiKDBg3q3Wd3vF4v3d3dfX40v493ymjEIbl9ttnmru4NUEBwdlUgAOLOWV5qeAieU8agX7SW0CofUngYcnklTecPRNYLZL0WoOfrOBa35hJn7ubsUYWMzanAltpNVlQrxg4JBZH1o+cA4JDNnBdWxN1xS8lbcRF2edciipoDZ92El9H/cIWd8c0MjIKe/x71AW0XuRj7WCFqiod2j5WaSwO4h6fRMMFETCHY6nxsv8yG4vEgd9n7HHOXBb4azR+wR1dSY8eO5a233iI7O5vm5mbuv/9+xo8fz5YtW2hqagIgLi6uz2vi4uKorq4GoKmpCYPBQERExC77/Pj63XnooYe477779qSpmh8YF65DUX/7/o/9+DzCllUgN7cAIG8rx1Qa/A5j3lKPfVIu1o/WEPtqEagKqixjzhhHkyMEl99ATWkccZltqMsjCDmzjrvPe58LQtp5xxGFSfBzeVgTYKNN7iEm1Ml9MVt+pTWa/W1+TwKlngTuiSkhPCx4D+mF6qMxLwphzqB8MgY2oD/djnhXEmF3VmCc1I4aCCBFRxGTlL3L8bwnjaYzW99bFl6j+aP26EpqypQpnHHGGQwePJjJkyezYMECIDis9yPhZ/c7VFXdZdvP/dY+d955J3a7vfentrZ2T5p9eFPk38woDT9cXf0QoACkkBCq7htD2dNjUezdhK1vDj6hKoj90yh/bCydA+CYpDLq1yWQ90QzgbmxuOMVnktewnGWGlZ4FEabaqjw7cxWHS1Z+W7wPCBY7sOp7EwS+pHzl2f4aPatc0M6uSemBKB3huZL2e9y7o2LuOLopSRbu6icNYiMdzvxXWmj/eLRSANzIDYK2UDw3z9hLWkm4YWiYMVdQaBplpY9QvPH/Kl1UlarlcGDB1NWVtZ7n+rnV0QtLS29V1fx8fH4fD46Ozt/cZ/dMRqNhIaG9vnR7BtSRAT2C8chhIWSutBNyiIVwWLG0y8KYfhAnKeNZNvfokn6TiHx+wBfzhtH1lvtdI2K5+gbVtN/noeRL8/i7K0XcOs/rkVWBRY0DOaYLaf2Tpq4rWk4HzlD+bp7EK1yAK8aLEH/XvOYX2saC1wmbmwYvc/fAw2cVHoSelQWNQ/g1cXHsPqLwSR/66Z9RARyWQXRczaw49xIEASi3yrCkROOaDLRMSO4JipQVYMgCPRkRoAgaldUmj/sTwUpr9fL1q1bSUhIID09nfj4eL7++uve530+H8uWLWP8+OC3qJEjR6LX6/vs09jYyObNm3v30ex9vhNG/e595a4uIj7dQqC2DnF5Mab5Bcht7ei+KUJ0ebFVOsn5ewnmTwowLigkcbmH+uOjCfuqhHnfjKV5lAVPupfqyhjSrt/OSd/ewMmJm9CJCqu9wYWjXkXHFEsbj8SvZ55jCE93Bu+ZVXRG/WrbJpsd3Be37E+9F5pf91Z3dLDsSkgrJgFez3qP0HKRuEI/4vJiwre7aLgln+6TB5P+STeqXqLq/0YSWtyM4vEQ89EPw7iCQPOM4Zg+LwhezWs0f9AeBalbb72VZcuWUVlZyZo1azjzzDPp7u7mkksuQRAEZs2axYMPPsi8efPYvHkzl156KRaLhfPPPx+AsLAwLr/8cm655RaWLFnC+vXrufDCC3uHDzX7hrFj1+SdECzT/dPJEkBwXcvPSiDokpMAEOwOpHZHn+cNxTuw1cu0nT6QhBUq3QP9hEa4+PD45/AE9EgdehY0DObS5JVcVngpI5ZfxZffjGKzPzi861clCrv6sdXn4rsRb/Y576SSaWz07RwONAp6IiQtN+C+5FDMfO/KZpStkhJ/GBUBG55omPnkh/RMH4PtPw0oeggpd1J5Wig9aTYGHVNGy9EJeKaOQf5xUpOqEvPC7y8Zr9H8kj0KUnV1dZx33nnk5OQwffp0DAYDq1evJi0tDYDbb7+dWbNmce211zJq1Cjq6+tZtGgRIT/JjPzEE09w2mmncfbZZ3PEEUdgsVj4/PPPkSTpl06r+ZPUwk273d52bBodl/7yEJv9gnF0XJZP+9EpAChxkfgTInausxIE5OxUQkvtRLy1mrqTFJJS24kPcXDe6ivZWJyOogPx4Sj+ueAsBEElN6GFgvMe47KX/gbAHVFlOP1Gnmk9lhJ/38/AV3nzGGIw9Q4T/tQJW6fudrvmz2nxh/LE4imsdmTyWM0JPFA5laKrnuS77hxsnxZhvzcVb7TCjvNCyXhqO74QEedkB2IA/DYR0WKh8xItDZJm79HSIh3GxJAQam4YTPKDfe8XSDmZ0NKOYDGDKAZrAQkCDbflk/LyFhzH5BIwC7hif7jXMGYwFGyi58yxuGJE4r7vRDXrCX2igZFhNbxYOJG89AZSrJ2kmDopcSRwYtQmLghpYWbdkVwZu5QxRj3b/T3UBkKZZJZZ7ZG5v2Yq87O/ZHjhuawY+RYW0dDbxiKvj5FGw8+7pNkLnIqHZjnA7K4xfN2YS31DJA8d8RFfdw6k8IMhpLxfTfnMVDL/V0/D1GT8Nkj+2o7U0E6guZXqe8aSdo92D0rz67Qs6JrfpDgcxK31IQ4b0He7zQgGPYH6BpTWNqS4WAB0TpC77FjmrcGeLhK9yUvrNfn4Qw0gCIQu3kbMC6tQNm9DLdxE1f+yeeu94wiPcvJixly+mz+c1zfms2FBHp+0DKdRdlHaFcv5H97I0ILzcCk6WgOh3NY0nNnt4zGIAZ7pTOOM9OI+AQrYbYDyqv7fTMek+W020USG3sY9MSV8O/gDxC4dS7oGYJb8pMypouriNPzhCqpOImH2FpKXOBBrWgg0NoEiBwOUKNF6jXZFpfnztCB1mDN8U4yysW/2B7VoS+90dP+4AZTdlAFA3KtFeE4Zg6DTkfpoET0JehLmVaBfXASqSuXfBiKMHIiaP5S6u8bj6Cfg6udnSmoJR39+Cy9d+jzhYT0Y7OC5Mozp/3cb/8r8lPfPfIpoWw/DjEamWhtp9obwWOJyZvdfwMzwCrY547mvdQDFuymM91PLPSb+2TRp37xRh7glbolX7btmkhmz9gJSvpZJM7ez6V9DcQ9MxBeuEl0o4k8IZ8ctA2D91t5imr0UmbiVXbssJNdo9pQ23HeYEofk4koLDc6++hW+E0Zh7PD23tcSB+Ui+PxUnxFHv7er8eTEI7kCCKs2IEVEUHljHp6EAKFbdTgyZfL+U0vJ/QkIPTr6f+QnYJHQ39xEQ1coanEYqgD6EZ0YPw2n7SgfIREu/Osi2DrzeQAe7+jPZw1DeCDzYx6pnUKE0cWt8YsIEWVSdTYWuEycbPH8Whc0v0NNwEmXomOIwcQCl4nJZgdjCi8mI7KNrJBW5n2Vj3VAJ5bZYeg8Kn6riM6t0Hy+h8w7u1GaW3dJJCtFR4Ek9Vl/p9H8SBvu0/wqZeO23wxQAIaFa/tOvKisxZUVSb+3q9l+fSrGonKEVRsAkDs76fdIMZaYHqI3e8m+qYjSm1LJed6HziFSdZIRa3E9O+pi0C0PwxMn8+aMp5ALIhh77TrMFUYsH4aRdnQwQ8n08uOYYtvM0kGfcIRJxKLz8Xrq96x2p1Pii0JWFd5r+e3CeprflqqzMcRgAuCV+okYBT2XZK5hfUk6joCJqI0qkU9baTwSmsZJhH64FvOnBfQ7ZyPe1EjE0F3Lxstt7cjNLQh6A+1XakN/mj9GC1Ka3ySFhwUnRwD+0TmYFq4nUFdP1n9KkO198yiqskzsy2b0321CDQTIeaIGsaoJVYLM9x3UndkPS4iXntEu9DFu7jn1IhQ9LP1wJBHbZMyXNFK5IpW7WwbzdL95ZOqN3NM6kJftiSgIPNiWwzkhVZxo8fJ8Vzr/SZ7Pfa0DehcEa/68ys5gzr1kQztJ/dp4JnEl3vM6kQ0i6Z8GiF8t4zh9ZO/+0tJ1AAjDB6JMHN7nWFJUJO0XjiT2w237rf2aQ4sWpDS/SZUVRE8wCOh6/KiKii4pEaXHTcsPN8ftF4xDl5IMioqx3dNbEjxQ30DtJVlkzHWgrt1M/FMrSZq+hcwZWxFLbDgzw0j+xk3qq6W0DxZpXJlE6iIP7y4fz8yKs9jq9/PhnInUeKOofTKL1zfnc3rpWbzYlUSkzslmXxRxejui9lHea1aMep0tPjd3fHcWeklm0pbpOJxmutN0iD6FpnEStg/WAKBLSUYXH4cakBG9fiRX35LvcnsHka+vQv5ZlhmN5vfSfrM1v0lxOFA2Br8Jq4WbQJFpnZyG6vcR+8IqpLhYDD0Kgdo6VL8Pde3m3tcKowaR9K0dde1mpJxMlCOD37QbrxnJ6BM3E7KpFfH79bjG9CdpqY+ATcFxp4PIYpFIYw+nLr4eT4zCe0uPoOV0L0smPEv90hRmhtdzrq2Vx6qPZ2Z4PXqh7xqr7f4erqw9Yv+9SQe5T3psPN7RH4DNPj1PtxxLXkYD/8t+h5rtcZjWW4h7ZzPtt7lIWLkzEDVOTaH01nQEkxG5ZPsvrsnTaP4oLUhp/pCIN3/IJqCq1F2QSUhhsLqyFBpK21U77z+I5bWIZcGEwIEoK85kIwBJX7fT+PcMlMoahJED8YVK2NMNfH/Wowizo+kcqNJwVyZ5/2mHWC+SSyDlDR1n/eM2dCM7GVd8JgNev473sufgUny844hi0OoLes+bLOm5IW7Jfno3Dn7jTc2cHrIRgHEmic0dCVyQuIZTCmcyYugOUj+sp/T+gYifRmJdV4s0MIeae8aT8HkN2Y9UoLS2HeAeaA5VWpDS/GkJj68kUN8AgNzdTUh9AGlAsHyD3GWn6fyBIEpITi+mjuC3cMWko+4YM9sfH0nVqaE0nuAncpubG6pPo3m8StZsB4pepG5aAharl0Cognl7C50nufCUhNNWEo1pUBetskCz7GNFdxabx73T2yaLaOidCKD5bbGSlXR9sOChX5V5f8BbPLz1eD4f/SJFm/vjS43k3KNWonerVF3aHznESPp7zagWE3JzC4pHm2Gp2Te0KeiavU7QG1BluTexqGgyBf+IiRKCJAXvVwkCgsGAaDTimJxHyLIy5I5O2q4aR9zH5ahJMXQ95GVQZBPffjeEmHUqik7g24ee4q3udDyqns6AlVRDO0XOfpR1x/B13ucHuOcHv+e6UrAHLJS5YkkydfHtA+MJuaaO0u1JCF4R0Q/pn3pBVRGXF+M4ZxxGu4xsFDB/+tuzRTWaH/3eKeh7XD5eo/ktP06aEHQ6mmeOIfbZYIocyWZFsJgJNDWDqqJ6vcheL435In5LNvoelfhPKwi0tiKpCu1dyWx+azBZJV24H3FTXR7LlJnXY765nsHhDczbOgzDVjMDTtjOYxkf0CLLxErWA9n1g95IUxXJOjcJURZGrT2frhNk0h5JhOkqhPuQFQFDVStVF6WSVhZL6HYHosuLqtfxezIp/nzdnUbzW7ThPs0+owYCvQEKgNgo/OnxCDpdMEmtKIEgkHFHAUa7Qv1pfmrPzwhecen1qDUWDA4FT4KNuk3xpM1XqZ0i0PNMMiOtVRRNfJ4HL3mLwaENnLbgb1T4teG9P+LH6ftHbTqdPIOPZJ2N47eeBosiiY7vpno66Nt1pL8lkDRfR6CunuQHVyI3t6Cu34Kyowpl8w9TzH/4f/pLdll3p9H8Bi1IafYbubwSaWM5bTNGo+uXivPM0ajjh6KMH4x12TZyri8j6ZVNtF0xhp5hKWQ9sp2a0xS8YRJHHrGF6jNVjC0SHRc6efJf5zD1hlk8VTWJu6M3s/SUxxhnkvCrMg+25fx2YzS9zio/hQKvn6bOEN7rzuSTHhuOt5OIW9PNCclbEQwyog/qjjEgqCAO3vn+uk8dQ/vFOwtR+o4bjvCzXJAazZ+hBSnNn9Zy/fhf/fb8IykuFsXlIvaTcgKV1djmrkZYUYy4vDhYhygtic5TBxK3rBVrcS1yRxfmKgP2/iLLtmWT+7SL1PtWEveCCd85ndSdqOB5LYElbiMnFsykMeAke+HVFHcn0ym79kPPD273tA7kmc40Psv6KpiF/qi3kBG557mLiVlcTenVZj5YMIHxWRUY2wXiRjdhK7cjVDUgZfUPXvEqEPPZdgT9Dwl/f8fnQKPZE1qQ0vxpsc+vgd8x/6bqykwCx46g4/iM3T6vbN6GbBDoGB0dzKitKlgaVfq938DqSU+z/eLgzVWdK0BncyhHDC7D3Orn6sUzyE+u4rmOfKQOHSnmTj7vSd2rfTwU/TN6E9eGV/Y+PrtiElt7Epl80WpiP3JwxxFfMPLobbSfIJNyeiW8EEP8y/V4R2VRel0s8lFDibqtEsdRmUiJccAPw3nFJQeqS5pDkDa7T/OnBI4dic7lh9UbEUYNwhttxvBV4W73FUNCEEymnRmzBQFxSC6KUYfU40PeUgqCgC4thUBVDYgSzdePRRXAFw6eVB8pn4u0DdLhSZDJ+089qs9HwzmZOMe5eHz0XKZZXdzdMpg4fTc3RFT3nvs7D/TTOUnV2fbDu3Jw+cplZJSxg381H81/4lew3GPlmlUXErrGjDNFJWw7uOIFVAn6z24gUFEFgBQRgZqWgKIFJc0foCWY1ewXum+KYHVwEai6dvMuAUqZMAzGDQn+2+HYpaSDO9FGT4oFb3wweIgWSzBAQbDcw9MriX9qJan3rWTAPxvoytBh7IS8B2vpmJBMxbWZ9DtjB6PSavCoega8cC2tPht6Ibgea/S6swHY5k2kVdaKJAJ9am7JqsK3jjwcisrChaOwKz5GGLs4OW8zoVUBho4vI/ajEo48dT2qTkWub+x9rerzaQFKs89pQUqzT0lrShDW/sIfMlXF+GUh1o/WoFtSBEDb2UMQrX2nkYtWK4FJIwnUN5D49FoCZmiamkb4Z5swtcOOzzPonq7jpZqJeCMV/h73NTPCqgD4cujrAFwV1qBV8v3BlA0zev+9ze9la3c8tbKN86cu47i1V3P8A7fy7/jvsJa20fBsJjv+v707D4+iyhc+/q2qXtPZ941ASAiEfQ0EF0BwhZEZFVfEcR0Y8cLVWRyv7wvvjDNwxxln9LovgzozylURXEAFlX0JkIUlgQDZSDALCdnT3emuOu8fLY0RVFCQpDmf58kDOed0df2qefpHVf3qnIcGsqs+ifgcL+L4ml6qRt1tQ7tsVzFb6LxqDJJ0NskkJZ01NfPGn9QmPJ0Ir/cUo0+mDh1AUL3uX5eo8Y5sTAnxCK+XuhFWtIw0FIuZybdsp6UvqJERWBsF+rgWjDfN/LbPR2SOqOCtlhGMemoeGRtmEa05qPa2kZU/g9UdZm4slYsiPp75jv/vHqFysC6GY3owceZm+kQeI3pXBxfvuBvhsHHs+nZUr0LoH4Npizf5SsyBYz/PIu69ki7bFbqOrar1R41FCnzynpTUfakajbOyiHh160ld9b/IJvgLHY9DJWLjYYr+bxLBB81EHPQS9PEuGm4bSeMgwaFbnj8PO969XVN8DSbVICWokayQEna2pbL+n2MQkxqZ1ruQgpmZNA2OwDurAfWNKML+vQ2A9uvHErJyF2pcDK0jErCvkDNMSN+fvCcl9XyGTvTKQ6fsin5hK+4wX4LyflFN7GYT4SU6k36/mYp/ZdDaW0EoUOVtO+23u1Cer6pqDuP3Ke+xprQ/hc5k/paQQ8TUL2hrsbPhj9nsmx9K+vwiPKtisLQZkDUEU0I8oXsbMFwuhNWCM1L77jeSpLNAJimpW/t6oUXDPdlooaHUzR1P2L9z/BPbRq0spmasysG2WPr+x1EswxpJWa2zqj2Dt9rCyN51PQVu97cmLYPAfsZHFwZlnjZ2Z73JcKuVzeOfo6QtmnJvBxXlMah1FmwNHjIfrWDztoE4J7Titamg+o6LEWyl/r5s9AMlRP7j5LNbSToXZJKSzhvXtCzfqr9f0fGzsahBQacc33rTOKJf3YHe0kLi8nI6rxwNgBYSQuOVGfR7uYaa/5fme8ZqYwT23x5hU1M6i5+4lfGxZdywbB5/qp3yjfvzaHRgrh6rC4MbSqZQp3cw+cOH2ObyTfy7pHko1U+lcfl7D6FYdVJHHKG5j5XqG9Lp/ZGXPrfuo2ZqJ+4I3/IqIreQmH+c+vECSTpXZJKSzpvgPdUY7c4ubaEFNRguN6rDgZae2qUvPLcW4fXSeeVokpc3Uj/Ydz/SOziVo9Nc7P/PWABq5o8n8elcvI/EUH9PAs0ZArvmgQQ3uxsSmVVxKf2XzMEjfF/W9Xq7v1Q9EGmKyq7KZP7ecDErpj5JutlFs+HkubVT+P3il7HEd5D5SA2WWTqdoQpBP6nB1O5FGZDOgN834thbjdHYxNE52bT+bBTiouGY+vY532FJFwg5C7p03ngrKk9uK/M9gKvYbXgSwlG/cktKP+SbHcHyyU72m7NI/NA3eW1nuIX0Wfkodjv7H88k86816G43ytZdiKAgQg9Fsemzcfzq8VXMDC0hWLXBnRsAjbfawnj03Tnk3PZX4NRncD1Jm+HCqphPWqn4svRi/juugGZDEKY6cAsPk8YUEqK6SIhoQY+NQKtvZtKs7by3YyR9bTqVl0eS8nEbbNuNGhREzHO+S3xaXCyd/RJRS89HhNKFRlb3ST2SFhGBZ1Bv1E0F/rbyx7KJ2C/wOBSiX9gKikLjh+mMjq3kodhPuXPegxy53kPJ5CX+17iFhyqvmzRzYMxEcefhS5gZs4XJdr1L+zqnytq2TP69+lLenfE3bs27G313GO4I3wIbIeUqYWVemvuYiNzXSc1YC7YGiHtxJ8LTydE52SeSVEwM3vRElK27uryHYrbQOWEI5k9zf5xgpR5NVvdJAe3wfZmYj52YRFaLiaH3qg681i8TFNA0cxzXpxRwfeQOrtjwAA2ZJi5KL/EvTTFy501k587kb3Unnp1yCw9Dt9/y4wZzFlR72zjsbWNJykYm23V0YbDd7YuzwO0mz9mHLEcJ0y/L4bpl8xmZUElYVh1J6wWqB4KPGARvLSd52WHsOQdJXVpL4oeV1MweDapGzHNbOTo72/egtduNqaFrAYo6dAAdU4djPSon9pXOLnm5T+qRkv57C189V3APTUHtNAgr7/S3Rby5g2XaFN5zCaz9VFyxBnWXuhjy+j18OP5ZmsrDcfRq5W+JW/AIMCsaKipxISe+gD1CP+nSWXe03tmLFsPOfWG+asdr9l/Lod3JLJr6JmsaB5Nbm8z7w19hSUck0fmwVR+EYRakNnaSuMlE8IEm2rP6cHiaIGNONarLzf4XBxL92YkLLTHPbz2xsGFLS5f3N3bvx76b01r4UJLOhDyTks6/cUO/9xIPpqRE1OEDOTLBgifE5J9eyTk9i8ZbxxDzSRnhq4roDDcY8EQVYtQA9Fo7M/Lv4fYJm0iNPMaAt+9nzOIH+EVVNmZF4+9pb/m3P+i1ubiFh2eaep2VUM+mz5wahZ2+wpObQxr9CarE00Z1awhEuxlkqWFi+H7yRv8vl235Jfs/zKB5ejt9f7OV+C0CT7AJd6hGXXYUQoU+y6F27lgMl4vM3xwh5pNSMPRv2w1JOqdkkpLOO932A07oTRrCrCFUOHZfG2pICADmNp2oD/YjOjsRQqA5FfB4EKpCxj+aaO+w8sYnl9L2x2TuvGwd2hX17P/DECYVTmeQxU6Zp41qbxvTr9rGxfm3cdAZd5aiPTsu2n0dJZ1xuETXs7w6vZ1Xjo2nc3c4uyY9R5iqs7ujF4Of/CWDE6vRbRCxzIFituCMUrF9sJ3w17cSXuom6ONdmNo8xD3lK0jx1tTiran1b7v+vuxvfDxAks4VWTghdSuK1UrH1cNOe8odrX86RogNCvaj9kulIzUc66odaJn9OHBPFCLOTfrMfBSzBeE5cSmw9I3hpN97kMtzqnlh+ZWMmbyPWmcIhw4mUHbti1y652f0CW3gF3HrGGHxEqSePDntY/UDuDpk93mZuLZOb+dXVVfzeu8N/raZ5RPZvD+de0dv5NdRRSxp6cXfCy/DdSSYhE2CYzd24GqykfnnBvSDpV2OiZaRhhEWhNixB1NSIu5+8Wjr8rq859ePoST9ELJwQuqRhMdL8N6j3z0QQFGouD4WtbLON4ntkVochTUAGKWH6f90NclLzV9ut5MDz2Wh9etL3dzx9FvYStP0Iby89Cp6feZmdFg5FduTUXSF/v+YQ+2OeOpdwbiEr5x7TN6NvNIcj0fo9H3nF7zfHsSysuEsqrqGOw9f8r1ivfPwJbzVFoYuDFa0BzMk51b/s1tfdbz/qyJUG1tK03i0bgirO8wM3DKTbZszuW1kDq+uuowRObNYXT+QlMhGYnYqeO0q7A0hcbUKJg0tIw335GGI7GE0zxxH4+gYjjyio9psGDHhNAyy+d/LlNob1eGQCUo6L2SSkroXQ/c/D/WdhCD5T1vQa+sAUEJD8CRF+rrcbrxlFQgTqDYb7TeM5ckp/6LstniaM3U60iPRzQq2BoHqMVj5i4nEjqilz3s6Ses6iSnwlQCYFR238NA/so67w2pQUSDMw//cdROuvEhuiM3lYFMMBW73SQlmVsWlfNxhpc1w8af6/syvHs11hy739y9J2ciNwc0YCFYeG8aykS9x/aGpeITur0Acuv0WDARv1I4FoMPoRBcGl+y6iZgPraysGMTsj+4i5tUgDJvBlt+MxRPlRewMo/3yNo40h9GWrBD9WQWxeV/ORq8bOPtGYt9ZipZfTNi/ttEZrGB7PwwhBEZBEXEvnygjd6ZFo4aGnPlnKUlngUxSUsDwVh1B2dL12R37iu0UPz+Ihps6eOrem0l9ch9BlRr2w63UjzJ8pdUjgij7qY3QB01oTh1zm4eOGJX6V3sTrrpoMrxs35BJmaeNvxzrj+2gjbLZsOSO/+GR1TfyRXEst7z6n4zcfrv/fe88fAm7apOYZG/jJ/tuwkDhkpADvJi64qT9NisaL/XaTJrJztCwI/R/536yc2cC4HKZMTDYUdSX11uiGfTR/Wx0mVg++FViZpfj3hnJ9Rdtp/IKBXOLSmeohrnBhKkdvpg9kl53VJG8eCvV1/bGsbEYZ7SKUVqB9bNd1F6fwf6nB2NcPJz41UeIWVboXy/q2M0j0aJ8Cd8dYQK5Fpd0nsgkJfU4nitGo4wZclpjldGDCcm3knJTEUeH2dAbG0lavAVKD9P/5WbU4QMJqtXRozwoHS5MzU4Ut45igJjRwPRP59JqqPx2+nLy3Im8/4fJhJYZaOU2btt8D0unPc3CK98h996/My6xgsJOJ48fSyO3Jpmlw1/BJbxUHY1AFyoO1U205mBIzq1Ue9sYV3ADGRtmccDjWz9LU1SWFo0ipHczDw/4mKz8GRy49HUu33sjYweVsL01jccufReXMLPe2YtHUz4AAbkPjyI2vQHRt4P2WA3DJAg9rJP8TgVf/HwwADG5bbhGpxPz3Fa0hHiEp5PoF7bSe7mC44/VuNJiUEKC/VWWEa9tRW84BkDw2zknVkuWpB+ZfE5K6nHMq3dyutU+Yude4vM0MHSsTSdepdhttKaH0ZCp4UrUmTp4Lx8/MILkz3RsdU7CD3ViD24le3gZdyx8iIahAhRQxoLQIOUjD5bfHOXuZ+dRMP9pNMVCeVski/Sr+V3iR6wN68/UdQ9w+4htjEipxG2YaNKD0IWTgqx/oSnBbB72Fn851p8rP5kPCqAK9l/1HNP2X8dE+xe8GtSOR+iEWl3ULUilMsLEwic+59ab70fZuofq+bcQ1CzQrSp19aFER7WiNdqJ/XUOisXCsetGEPd0Digq7CzC/GUpef3EXkQsraN5xkhC39iG8yON9ltTccakELbiGIbLddY/M0n6vmR1nxRYVA1l1EDEjj3+pqNzsonb3Aj4HjoFUEwmtOgoPH3jqRnnoNe7VaCqONOjqZpkRqgQXSBoGKrgqFSILnRydJgdU7vg0Yf/yV9KriA2qJWaZ9PwzDxGjKONxpdSCN/bxP75wQxJq+Ky6P1cEnTAX/33TFMvPqgZSkJQC0tSNgLwVlsYT5ZOZlL8QcyKzoKYIq49eBXeOaEYDiuHbnagJLhIeMtCv4eL2P/XQegWBcWA1hmtONstxH9gpTlVxR0lyHjqsO+y54hBqB1u9t8fhSnGhVbsIGXhlhPHSVFg7BC0hjaE3eI/Ls7pWQQfakYvLP4xPi3pAna61X3yTEoKOJ4Qi/8fthYeRszz2/BeNAxLSY1/RgTh9eKtqcUcZEfrdHB0YhKhFW7Kr9VI+swguKyVsp+FE7/NS8Osdg6NtpP5YCF6cwv/J3IW4Yd0SvrE0zLBS6LJy6GaGFIPtaOUf0FoZBKLei9nh6s3NsV39lLlbaPUGcPvU1dw54vzGJLVi7dGvEyUppHgaOHTv1xE542NrKvrR83GJNwPuzBXWhGKwBHkxhlhx655cHzh5sHX3mR+3o1YNoXRp8DNF3PaCVvhIOKAQDh9D/d2xtipnBJK5hNH8JYfRrFaUWy2E2dJQqA1diCCrOgOi38lLft725GP7krdibwnJQUWQ8f0+YnKtLoZA1GDg2npa/etM/UV+sSRuFMiCS33cnSsTvDCI0TlqTT31TAKiujz2E4cZS2E/28ISSs16mYMBCFI3NhGTbaK9ZggvNBE6IyjmAodHLrFgXNZBHaLh19ffhv7nIm0CxMzyydy1bO/4a6ozTz4q7lYmwSmz8K5d/9MwlUnifZmbA06nk1RVOUmElZiINpMrLn9caLzFSJeDObYZBebXxuFmrOXV2svIv41G4qAslnQ544SvHaFupudFD+agSkpEfPqnaQu70CYv0zXA9Pxjs0EwJSchLhoOHrxIdpTQ1C27v7RPh5JOlPycp8UcLS4WPB6/Tf+TzmmfzpHroklLqcdc5lvVgVv71iUnUXUzMkiNredIxMcKAKS/pyDlt4H/UAJpr59MELsHPi1jeBcO61pOuGFKqEVXvov3EsvWyMVzijKftufo8NsGFZwRwjsmU0YmyJwJhgkbBJUXSkwN2loToVrrt3GuhfGYlgUElceoeTOROK3eam4VsHUrGFYBTE7IWpdJZ2vqZTVRpGxoIX6i+IIrXCjrc3zx1R9eSxxW5sRuYWY4uMouzuNXotyqJ07lrintmBKTqLkvhTSXqrEW1mFltkPfd/B73WcjUtGgArq+vzv9Xrpwna6l/tkkpICjjp8IIrb861fvh3XjSVo+XZMqb3xRodgWDQsXzThLS339f9sLEdHqPRe1Q7bTpxpOKdn0fjzNsJfD8Ha5KF6nI1ef/EtZ9F60zhCV+RT/ctRGGZI+ttO1N5JJPzrKDU3R4JX58ADKRhJLmz2TlJ+fhh9SF/C/1xF23ThT6qmvn04eG8CaX/YRdXc4cTmuukMMxGaX8O++Qlk/CoP51XDCVqz+zuLHI7PEnH8T3HRcMyH6/FWVv3wAy1JP4CccUK6YBkFRV0SlD5xJIqp6+3XoHdzQAi8peWohaU0Ztr9CQogaHkOqY/vRdlZ1OV17lCNXo94CFqeg7Y2j97P76N+1ii00FCqp+iULhiJpVmQ/FQe9bNGobg91NwQhqtvDPt+3Yt+i4pIWGFhUFwNNbcPQd1eSNEH/WmZ2M//Hq4+USRs1hGZqVgbBZrTS/Ane/FWVDFgwT5q7xuN7YPt35qgjEtGoFit1N7nW2qj9t7RACibC/BWVmFcPFzOwyf1CDJJSQHP1O5BGN98wcBobyfqpa2YkhK7tre2UvFoVpe28H9uRd93EFNCPFp0FEZHB1GvbKV1SiZKh4bmVggt901qa20xEE4n3qojAKS97QKrFa9Nobw5kvh/5KGmphBWZuCoPLEOk6nDg73WidrRidoJWocHhPDNxtHUTOwzvio9LTzsGxON1tEJhvCNNXRin93Std/pQeiyRELq/mSSkgKe2LHntJabqJ/c+6S2kHJB24yxvsX+ABSFllvH0TC5Dx1j+6IlxgNgq3OjCIgp8GL6PBfhdhP8dg7HrvKdIZk6PLhirRz4TV8MTSFyehmGy4V+sJTgt3Ng+x60qEicP82CbbsRO/ag7zuIIsDYtQ+j4+TFBD1D+qL0ST51zLmF3zrXnsg9MbuEJHVn8p6UdMFTHQ5qbx9KzPO+FX1NSYmgKP4zIFNyEu3DErHVdPgKEnol++/paAMzaMsIx75iO9UPjafXG6V4q2uo++V44l7JRYuPxQgPRq09hvB6Ucxm9KRo3FE2LJ/s9O+DPnEkANbiL7pUIWqhoZTNH0zqC4f8cxR+H6b4OLBa8FZUfu9tSNLZJO9JSdJpMtrb/QkKQDjsiKATs4B7q45gXbkDkVvo+/0rRQd60QH/siKmDuFff0nVBWpIMN6KSjyRdoyWVvT6BoyWVkTePlpTzKg233s0zcrGsqccbV2eP0E13JvtSywWMymr2844QSlWK3X3j0cxfznnnt2GcNjP7MBIUjcgk5QkfY1+oAT9QMlpj1fMFtxTx2CvN3z3joD490qpn5oBgLY2z3+5ruWawZgS44n5Z76/8CHyrXz0Y41dthm7/AB1V/dFr2/oUl14uoTb92qMjwAACJFJREFUTXiJBwb7Ljd6yyrQiw6c8XYk6XyTSUqSzgYDwoqa/L96a2qJeG0rWv902q8f628PfjsHb9WRLpV5hssFQmCKj+PYndkA6PUNRC45cXanRUWi9U+n4e7s094ly8c7EPmFPyAoSTr/5LRIknSaFJPJt7jiKeh29ZTz3enFh3AUHzr1BlUNxImzL29NLZFLak85VCTFgqIQ9crWU/ZLUqCSZ1KSdJpqZ2edsl14On3PXZ0h19RRaAMzTmussXs/xq59Z/wexx0vzJCknkYmKUk6TbFPb/nuQcepGlpMzLcOsX2w/YxnG2+bMRYtPfWMXgOgeozvHiRJ3ZBMUpJ0DqiOIBqnpJ317Qa/sx29pPyMX6dsLjjr+yJJPwaZpCTpHDBaWwl9c9vZ37AQ1PzH6RdPSFJP1yMLJ44/f+zFw2kv0SpJ54HWP43mgREEL9/53YNPU/Tf13Pq8g1J6jm8eIAT3+ffpEcmqdbWVgA2seo874kkfYfiL38kSTql1tZWwsLCvrG/R06LZBgGxcXFDBw4kMrKym+dUiOQtLS00KtXrwsqZrgw474QYwYZ94UUtxCC1tZWEhMTUdVvvvPUI8+kVFUlKSkJgNDQ0AvmQz3uQowZLsy4L8SYQcZ9ofi2M6jjZOGEJEmS1G3JJCVJkiR1Wz02SVmtVhYsWIDVaj3fu/KjuRBjhgsz7gsxZpBxX2hxn44eWTghSZIkXRh67JmUJEmSFPhkkpIkSZK6LZmkJEmSpG5LJilJkiSp25JJSpIkSeq2emSSevbZZ0lNTcVmszFq1Cg2btx4vnfpB9mwYQM/+clPSExMRFEUVqxY0aVfCMHChQtJTEzEbrczceJECgu7Lgvudrt54IEHiI6OxuFwcO2111JVVfUjRnFmFi1axJgxYwgJCSE2Npaf/vSnFBd3neQu0OJ+7rnnGDp0qH9WgezsbD766CN/f6DFeyqLFi1CURTmz5/vbwvEuBcuXIiiKF1+4uPj/f2BGPM5I3qYpUuXCrPZLF566SVRVFQk5s2bJxwOh6ioqDjfu/a9rVq1SvzXf/2XWLZsmQDE8uXLu/QvXrxYhISEiGXLlok9e/aIm266SSQkJIiWlhb/mNmzZ4ukpCSxZs0akZeXJyZNmiSGDRsmvF7vjxzN6bnyyivFkiVLxN69e0VBQYGYOnWqSElJEW1tbf4xgRb3+++/L1auXCmKi4tFcXGxeOSRR4TZbBZ79+4VQgRevF+3fft20adPHzF06FAxb948f3sgxr1gwQIxaNAgUV1d7f+pq6vz9wdizOdKj0tSWVlZYvbs2V3aBgwYIB5++OHztEdn19eTlGEYIj4+XixevNjf5nK5RFhYmHj++eeFEEI0NTUJs9ksli5d6h9z5MgRoaqq+Pjjj3+0ff8h6urqBCDWr18vhLhw4o6IiBAvv/xywMfb2toq+vXrJ9asWSMmTJjgT1KBGveCBQvEsGHDTtkXqDGfKz3qcl9nZye5ublcccUVXdqvuOIKtmw5g6W9e5CysjJqamq6xGy1WpkwYYI/5tzcXDweT5cxiYmJDB48uMccl+bmZgAiIyOBwI9b13WWLl1Ke3s72dnZAR/v/fffz9SpU5kyZUqX9kCO++DBgyQmJpKamsrNN99MaWkpENgxnws9ahb0+vp6dF0nLi6uS3tcXBw1NTXnaa/OreNxnSrmiooK/xiLxUJERMRJY3rCcRFC8OCDD3LxxRczePBgIHDj3rNnD9nZ2bhcLoKDg1m+fDkDBw70f/EEWrwAS5cuJS8vjx07dpzUF6if89ixY3n99dfJyMigtraWxx57jPHjx1NYWBiwMZ8rPSpJHacoSpffhRAntQWa7xNzTzkuc+fOZffu3WzatOmkvkCLu3///hQUFNDU1MSyZcu44447WL9+vb8/0OKtrKxk3rx5rF69GpvN9o3jAi3uq6++2v/3IUOGkJ2dTVpaGq+99hrjxo0DAi/mc6VHXe6Ljo5G07ST/idRV1d30v9KAsXxiqBvizk+Pp7Ozk4aGxu/cUx39cADD/D++++zdu1akpOT/e2BGrfFYiE9PZ3Ro0ezaNEihg0bxpNPPhmw8ebm5lJXV8eoUaMwmUyYTCbWr1/PU089hclk8u93oMX9dQ6HgyFDhnDw4MGA/azPlR6VpCwWC6NGjWLNmjVd2tesWcP48ePP016dW6mpqcTHx3eJubOzk/Xr1/tjHjVqFGazucuY6upq9u7d222PixCCuXPn8u677/L555+TmprapT9Q4/46IQRutztg4508eTJ79uyhoKDA/zN69Ghuu+02CgoK6Nu3b0DG/XVut5t9+/aRkJAQsJ/1OXM+qjV+iOMl6K+88oooKioS8+fPFw6HQ5SXl5/vXfveWltbRX5+vsjPzxeAeOKJJ0R+fr6/rH7x4sUiLCxMvPvuu2LPnj3illtuOWW5anJysvj0009FXl6euOyyy7p1ueqcOXNEWFiYWLduXZcy3Y6ODv+YQIv7d7/7ndiwYYMoKysTu3fvFo888ohQVVWsXr1aCBF48X6Tr1b3CRGYcT/00ENi3bp1orS0VGzbtk1MmzZNhISE+L+nAjHmc6XHJSkhhHjmmWdE7969hcViESNHjvSXLfdUa9euFcBJP3fccYcQwleyumDBAhEfHy+sVqu49NJLxZ49e7psw+l0irlz54rIyEhht9vFtGnTxOHDh89DNKfnVPECYsmSJf4xgRb3XXfd5f93GxMTIyZPnuxPUEIEXrzf5OtJKhDjPv7ck9lsFomJieK6664ThYWF/v5AjPlcketJSZIkSd1Wj7onJUmSJF1YZJKSJEmSui2ZpCRJkqRuSyYpSZIkqduSSUqSJEnqtmSSkiRJkrotmaQkSZKkbksmKUmSJKnbkklKkiRJ6rZkkpIkSZK6LZmkJEmSpG7r/wPPc0YJvI96kQAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# DO NOT CHANGE THIS CELL\n", + "start=time.time()\n", + "\n", + "# get points\n", + "ds_points_cudf=ds.Canvas().points(gdf,'long','lat')\n", + "display(ds_points_cudf)\n", + "\n", + "# plot points\n", + "plt.imshow(tf.shade(ds_points_cudf))\n", + "\n", + "print(f'Duration: {round(time.time()-start, 2)} seconds')" + ] + }, + { + "cell_type": "markdown", + "id": "98b1c4a7-9b60-4970-859f-b9ffedd4315c", + "metadata": {}, + "source": [ + "**Note**: Please re-execute the above cell if it took more than a few seconds for the more accurate compute time. " + ] + }, + { + "cell_type": "markdown", + "id": "2e664fb6-7482-49d0-bc60-efaefd2184d3", + "metadata": {}, + "source": [ + "## Interactive Visualization ##\n", + "Data visualization is crucial in data science as it bridges the gap between complex data and human understanding, making insights more accessible, actionable, and impactful throughout the data science process. Bringing interactivity in data visualization further enables: \n", + "* **Discovery**: enables discovery of hidden patterns, trends, and outliers that may not be apparant in static visualizations\n", + "* **Enhanced understanding**: allows users to view data from multiple perspective and levels of detail\n", + "* **Customization**: provides the ability to rapidly filter, sort, and aggregate data, leading to a more impactful presentation" + ] + }, + { + "cell_type": "markdown", + "id": "084911da-fb99-43e8-bff6-e11f032b4c9e", + "metadata": {}, + "source": [ + "### cuxfilter and Dashboard ###\n", + "cuxfilter enables GPU accelerated cross-filtering dashboards, which is ideal for multi-chart exploratory data analysis. Cross-filtering lets users interact with one chart and apply that interaction as a filter to other charts in the dashboard. \n", + "\n", + "cuxfilter acts as a connector library, which provides the connections between different visualization libraries and a GPU DataFrame without much hassle. This also allows users to use charts from different libraries in a single dashboard, while also providing the interaction. Currently, cuxfilter supports: \n", + "* [Bokeh](https://bokeh.org/) Charts\n", + " * Bar chart\n", + " * Line chart\n", + " * Choropleth\n", + "* [Datashader](https://datashader.org/) Charts\n", + " * Line\n", + " * Scatter\n", + "* [Panel Widgets](https://panel.holoviz.org/api/panel.widgets.html)\n", + " * Range\n", + " * Float\n", + " * Int\n", + " * Dropdown\n", + " * Multiselect" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "f1777e6a-a717-49d4-9b38-e855d987ca92", + "metadata": {}, + "outputs": [], + "source": [ + "import cuxfilter as cxf\n", + "\n", + "# factorize county for multiselect widget\n", + "gdf['county'], county_names = gdf['county'].factorize()\n", + "county_map = dict(zip(list(range(len(county_names))), county_names.to_arrow()))" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "680c06ed-f8c6-4de5-b366-78f0c1092aeb", + "metadata": {}, + "outputs": [], + "source": [ + "# create cuxfilter DataFrame\n", + "cxf_data = cxf.DataFrame.from_dataframe(gdf)\n", + "\n", + "# create Datashader scatter plot\n", + "scatter_chart = cxf.charts.scatter(x='long', y='lat')" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "c2d8a372-6e14-442c-a61d-f5ea44fc7f6c", + "metadata": {}, + "outputs": [], + "source": [ + "# create Bokeh bar charts\n", + "chart_3=cxf.charts.bar('age')\n", + "chart_2=cxf.charts.bar('sex')" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "fb700802-45b8-4558-b58b-c2a96cfda78b", + "metadata": {}, + "outputs": [], + "source": [ + "# define layout\n", + "layout_array=[[1, 2, 2], \n", + " [3, 2, 2]]" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "a7964e08-1e77-4431-83b1-ef0e798d32f5", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/javascript": [ + "(function(root) {\n", + " function now() {\n", + " return new Date();\n", + " }\n", + "\n", + " const force = true;\n", + " const py_version = '3.6.2'.replace('rc', '-rc.').replace('.dev', '-dev.');\n", + " const reloading = false;\n", + " const Bokeh = root.Bokeh;\n", + "\n", + " // Set a timeout for this load but only if we are not already initializing\n", + " if (typeof (root._bokeh_timeout) === \"undefined\" || (force || !root._bokeh_is_initializing)) {\n", + " root._bokeh_timeout = Date.now() + 5000;\n", + " root._bokeh_failed_load = false;\n", + " }\n", + "\n", + " function run_callbacks() {\n", + " try {\n", + " root._bokeh_onload_callbacks.forEach(function(callback) {\n", + " if (callback != null)\n", + " callback();\n", + " });\n", + " } finally {\n", + " delete root._bokeh_onload_callbacks;\n", + " }\n", + " console.debug(\"Bokeh: all callbacks have finished\");\n", + " }\n", + "\n", + " function load_libs(css_urls, js_urls, js_modules, js_exports, callback) {\n", + " if (css_urls == null) css_urls = [];\n", + " if (js_urls == null) js_urls = [];\n", + " if (js_modules == null) js_modules = [];\n", + " if (js_exports == null) js_exports = {};\n", + "\n", + " root._bokeh_onload_callbacks.push(callback);\n", + "\n", + " if (root._bokeh_is_loading > 0) {\n", + " // Don't load bokeh if it is still initializing\n", + " console.debug(\"Bokeh: BokehJS is being loaded, scheduling callback at\", now());\n", + " return null;\n", + " } else if (js_urls.length === 0 && js_modules.length === 0 && Object.keys(js_exports).length === 0) {\n", + " // There is nothing to load\n", + " run_callbacks();\n", + " return null;\n", + " }\n", + "\n", + " function on_load() {\n", + " root._bokeh_is_loading--;\n", + " if (root._bokeh_is_loading === 0) {\n", + " console.debug(\"Bokeh: all BokehJS libraries/stylesheets loaded\");\n", + " run_callbacks()\n", + " }\n", + " }\n", + " window._bokeh_on_load = on_load\n", + "\n", + " function on_error(e) {\n", + " const src_el = e.srcElement\n", + " console.error(\"failed to load \" + (src_el.href || src_el.src));\n", + " }\n", + "\n", + " const skip = [];\n", + " if (window.requirejs) {\n", + " window.requirejs.config({'packages': {}, 'paths': {'h3': 'https://cdn.jsdelivr.net/npm/h3-js@4.1.0/dist/h3-js.umd', 'deck-gl': 'https://cdn.jsdelivr.net/npm/deck.gl@9.0.20/dist.min', 'deck-json': 'https://cdn.jsdelivr.net/npm/@deck.gl/json@9.0.20/dist.min', 'loader-csv': 'https://cdn.jsdelivr.net/npm/@loaders.gl/csv@4.2.2/dist/dist.min', 'loader-json': 'https://cdn.jsdelivr.net/npm/@loaders.gl/json@4.2.2/dist/dist.min', 'loader-tiles': 'https://cdn.jsdelivr.net/npm/@loaders.gl/3d-tiles@4.2.2/dist/dist.min', 'mapbox-gl': 'https://api.mapbox.com/mapbox-gl-js/v3.0.1/mapbox-gl', 'carto': 'https://cdn.jsdelivr.net/npm/@deck.gl/carto@^9.0.20/dist.min'}, 'shim': {'deck-json': {'deps': ['deck-gl']}, 'deck-gl': {'deps': ['h3']}}});\n", + " require([\"h3\"], function(h3) {\n", + " window.h3 = h3\n", + " on_load()\n", + " })\n", + " require([\"deck-gl\"], function(deck) {\n", + " window.deck = deck\n", + " on_load()\n", + " })\n", + " require([\"deck-json\"], function() {\n", + " on_load()\n", + " })\n", + " require([\"loader-csv\"], function() {\n", + " on_load()\n", + " })\n", + " require([\"loader-json\"], function() {\n", + " on_load()\n", + " })\n", + " require([\"loader-tiles\"], function() {\n", + " on_load()\n", + " })\n", + " require([\"mapbox-gl\"], function(mapboxgl) {\n", + " window.mapboxgl = mapboxgl\n", + " on_load()\n", + " })\n", + " require([\"carto\"], function() {\n", + " on_load()\n", + " })\n", + " root._bokeh_is_loading = css_urls.length + 8;\n", + " } else {\n", + " root._bokeh_is_loading = css_urls.length + js_urls.length + js_modules.length + Object.keys(js_exports).length;\n", + " }\n", + "\n", + " const existing_stylesheets = []\n", + " const links = document.getElementsByTagName('link')\n", + " for (let i = 0; i < links.length; i++) {\n", + " const link = links[i]\n", + " if (link.href != null) {\n", + " existing_stylesheets.push(link.href)\n", + " }\n", + " }\n", + " for (let i = 0; i < css_urls.length; i++) {\n", + " const url = css_urls[i];\n", + " const escaped = encodeURI(url)\n", + " if (existing_stylesheets.indexOf(escaped) !== -1) {\n", + " on_load()\n", + " continue;\n", + " }\n", + " const element = document.createElement(\"link\");\n", + " element.onload = on_load;\n", + " element.onerror = on_error;\n", + " element.rel = \"stylesheet\";\n", + " element.type = \"text/css\";\n", + " element.href = url;\n", + " console.debug(\"Bokeh: injecting link tag for BokehJS stylesheet: \", url);\n", + " document.body.appendChild(element);\n", + " } if (((window.deck !== undefined) && (!(window.deck instanceof HTMLElement))) || window.requirejs) {\n", + " var urls = ['https://cdn.holoviz.org/panel/1.5.4/dist/bundled/deckglplot/h3-js@4.1.0/dist/h3-js.umd.js', 'https://cdn.holoviz.org/panel/1.5.4/dist/bundled/deckglplot/deck.gl@9.0.20/dist.min.js', 'https://cdn.holoviz.org/panel/1.5.4/dist/bundled/deckglplot/@deck.gl/json@9.0.20/dist.min.js', 'https://cdn.holoviz.org/panel/1.5.4/dist/bundled/deckglplot/@loaders.gl/csv@4.2.2/dist/dist.min.js', 'https://cdn.holoviz.org/panel/1.5.4/dist/bundled/deckglplot/@loaders.gl/json@4.2.2/dist/dist.min.js', 'https://cdn.holoviz.org/panel/1.5.4/dist/bundled/deckglplot/@loaders.gl/3d-tiles@4.2.2/dist/dist.min.js', 'https://cdn.holoviz.org/panel/1.5.4/dist/bundled/deckglplot/mapbox-gl-js/v3.0.1/mapbox-gl.js', 'https://cdn.holoviz.org/panel/1.5.4/dist/bundled/deckglplot/maplibre-gl/dist/maplibre-gl.js'];\n", + " for (var i = 0; i < urls.length; i++) {\n", + " skip.push(encodeURI(urls[i]))\n", + " }\n", + " } if (((window.mapboxgl !== undefined) && (!(window.mapboxgl instanceof HTMLElement))) || window.requirejs) {\n", + " var urls = ['https://cdn.holoviz.org/panel/1.5.4/dist/bundled/deckglplot/@deck.gl/carto@^9.0.20/dist.min.js'];\n", + " for (var i = 0; i < urls.length; i++) {\n", + " skip.push(encodeURI(urls[i]))\n", + " }\n", + " } var existing_scripts = []\n", + " const scripts = document.getElementsByTagName('script')\n", + " for (let i = 0; i < scripts.length; i++) {\n", + " var script = scripts[i]\n", + " if (script.src != null) {\n", + " existing_scripts.push(script.src)\n", + " }\n", + " }\n", + " for (let i = 0; i < js_urls.length; i++) {\n", + " const url = js_urls[i];\n", + " const escaped = encodeURI(url)\n", + " if (skip.indexOf(escaped) !== -1 || existing_scripts.indexOf(escaped) !== -1) {\n", + " if (!window.requirejs) {\n", + " on_load();\n", + " }\n", + " continue;\n", + " }\n", + " const element = document.createElement('script');\n", + " element.onload = on_load;\n", + " element.onerror = on_error;\n", + " element.async = false;\n", + " element.src = url;\n", + " console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n", + " document.head.appendChild(element);\n", + " }\n", + " for (let i = 0; i < js_modules.length; i++) {\n", + " const url = js_modules[i];\n", + " const escaped = encodeURI(url)\n", + " if (skip.indexOf(escaped) !== -1 || existing_scripts.indexOf(escaped) !== -1) {\n", + " if (!window.requirejs) {\n", + " on_load();\n", + " }\n", + " continue;\n", + " }\n", + " var element = document.createElement('script');\n", + " element.onload = on_load;\n", + " element.onerror = on_error;\n", + " element.async = false;\n", + " element.src = url;\n", + " element.type = \"module\";\n", + " console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n", + " document.head.appendChild(element);\n", + " }\n", + " for (const name in js_exports) {\n", + " const url = js_exports[name];\n", + " const escaped = encodeURI(url)\n", + " if (skip.indexOf(escaped) >= 0 || root[name] != null) {\n", + " if (!window.requirejs) {\n", + " on_load();\n", + " }\n", + " continue;\n", + " }\n", + " var element = document.createElement('script');\n", + " element.onerror = on_error;\n", + " element.async = false;\n", + " element.type = \"module\";\n", + " console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n", + " element.textContent = `\n", + " import ${name} from \"${url}\"\n", + " window.${name} = ${name}\n", + " window._bokeh_on_load()\n", + " `\n", + " document.head.appendChild(element);\n", + " }\n", + " if (!js_urls.length && !js_modules.length) {\n", + " on_load()\n", + " }\n", + " };\n", + "\n", + " function inject_raw_css(css) {\n", + " const element = document.createElement(\"style\");\n", + " element.appendChild(document.createTextNode(css));\n", + " document.body.appendChild(element);\n", + " }\n", + "\n", + " const js_urls = [\"https://cdn.holoviz.org/panel/1.5.4/dist/bundled/deckglplot/h3-js@4.1.0/dist/h3-js.umd.js\", \"https://cdn.holoviz.org/panel/1.5.4/dist/bundled/deckglplot/deck.gl@9.0.20/dist.min.js\", \"https://cdn.holoviz.org/panel/1.5.4/dist/bundled/deckglplot/@deck.gl/json@9.0.20/dist.min.js\", \"https://cdn.holoviz.org/panel/1.5.4/dist/bundled/deckglplot/@loaders.gl/csv@4.2.2/dist/dist.min.js\", \"https://cdn.holoviz.org/panel/1.5.4/dist/bundled/deckglplot/@loaders.gl/json@4.2.2/dist/dist.min.js\", \"https://cdn.holoviz.org/panel/1.5.4/dist/bundled/deckglplot/@loaders.gl/3d-tiles@4.2.2/dist/dist.min.js\", \"https://cdn.holoviz.org/panel/1.5.4/dist/bundled/deckglplot/mapbox-gl-js/v3.0.1/mapbox-gl.js\", \"https://cdn.holoviz.org/panel/1.5.4/dist/bundled/deckglplot/maplibre-gl/dist/maplibre-gl.js\", \"https://cdn.holoviz.org/panel/1.5.4/dist/bundled/deckglplot/@deck.gl/carto@^9.0.20/dist.min.js\", \"https://cdn.holoviz.org/panel/1.5.4/dist/bundled/reactiveesm/es-module-shims@^1.10.0/dist/es-module-shims.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-3.6.2.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-gl-3.6.2.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-widgets-3.6.2.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-tables-3.6.2.min.js\", \"https://cdn.holoviz.org/panel/1.5.4/dist/panel.min.js\"];\n", + " const js_modules = [];\n", + " const js_exports = {};\n", + " const css_urls = [\"https://cdn.holoviz.org/panel/1.5.4/dist/bundled/deckglplot/mapbox-gl-js/v3.0.1/mapbox-gl.css?v=1.5.4\", \"https://cdn.holoviz.org/panel/1.5.4/dist/bundled/deckglplot/maplibre-gl@4.4.1/dist/maplibre-gl.css?v=1.5.4\"];\n", + " const inline_js = [ function(Bokeh) {\n", + " inject_raw_css(\"\\n.dataframe table{\\n border: none;\\n}\\n\\n.panel-df table{\\n width: 100%;\\n border-collapse: collapse;\\n border: none;\\n}\\n.panel-df td{\\n white-space: nowrap;\\n overflow: auto;\\n text-overflow: ellipsis;\\n}\\n\");\n", + " }, function(Bokeh) {\n", + " inject_raw_css(\"\\n.multi-select{\\n color: white;\\n z-index: 100;\\n background: rgba(44,43,43,0.5);\\n border-radius: 1px;\\n width: 120px !important;\\n height: 30px !important;\\n}\\n.multi-select > .bk {\\n padding: 5px;\\n width: 120px !important;\\n height: 30px !important;\\n}\\n\\n.deck-chart {\\n z-index: 10;\\n position: initial !important;\\n}\\n\");\n", + " }, function(Bokeh) {\n", + " inject_raw_css(\"\\n.center-header {\\n text-align: center\\n}\\n.bk-input-group {\\n padding: 10px;\\n}\\n#sidebar {\\n padding-top: 10px;\\n}\\n.custom-widget-box {\\n margin-top: 20px;\\n padding: 5px;\\n border: None !important;\\n}\\n.custom-widget-box > p {\\n margin: 0px;\\n}\\n.bk-input-group {\\n color: None !important;\\n}\\n.indicator {\\n text-align: center;\\n}\\n.widget-card {\\n margin: 5px 10px;\\n}\\n.number-card {\\n margin: 5px 10px;\\n text-align: center;\\n}\\n.number-card-value {\\n width: 100%;\\n margin: 0px;\\n}\\n\");\n", + " }, function(Bokeh) {\n", + " Bokeh.set_log_level(\"info\");\n", + " },\n", + " function(Bokeh) {\n", + " (function(root, factory) {\n", + " factory(root[\"Bokeh\"]);\n", + " })(this, function(Bokeh) {\n", + " let define;\n", + " return (function outer(modules, entry) {\n", + " if (Bokeh != null) {\n", + " return Bokeh.register_plugin(modules, entry);\n", + " } else {\n", + " throw new Error(\"Cannot find Bokeh. You have to load it prior to loading plugins.\");\n", + " }\n", + " })\n", + " ({\n", + " \"custom/main\": function(require, module, exports) {\n", + " const models = {\n", + " \"CustomInspectTool\": require(\"custom/cuxfilter.charts.datashader.custom_extensions.graph_inspect_widget.custom_inspect_tool\").CustomInspectTool\n", + " };\n", + " require(\"base\").register_models(models);\n", + " module.exports = models;\n", + " },\n", + " \"custom/cuxfilter.charts.datashader.custom_extensions.graph_inspect_widget.custom_inspect_tool\": function(require, module, exports) {\n", + " \"use strict\";\n", + " var _a;\n", + " Object.defineProperty(exports, \"__esModule\", { value: true });\n", + " exports.CustomInspectTool = exports.CustomInspectToolView = void 0;\n", + " const inspect_tool_1 = require(\"models/tools/inspectors/inspect_tool\");\n", + " class CustomInspectToolView extends inspect_tool_1.InspectToolView {\n", + " connect_signals() {\n", + " super.connect_signals();\n", + " this.on_change([this.model.properties.active], () => {\n", + " this.model._active = this.model.active;\n", + " });\n", + " }\n", + " }\n", + " exports.CustomInspectToolView = CustomInspectToolView;\n", + " CustomInspectToolView.__name__ = \"CustomInspectToolView\";\n", + " class CustomInspectTool extends inspect_tool_1.InspectTool {\n", + " constructor(attrs) {\n", + " super(attrs);\n", + " }\n", + " }\n", + " exports.CustomInspectTool = CustomInspectTool;\n", + " _a = CustomInspectTool;\n", + " CustomInspectTool.__name__ = \"CustomInspectTool\";\n", + " (() => {\n", + " _a.prototype.default_view = CustomInspectToolView;\n", + " _a.define(({ Boolean }) => ({\n", + " _active: [Boolean, true]\n", + " }));\n", + " _a.register_alias(\"customInspect\", () => new _a());\n", + " })();\n", + " //# sourceMappingURL=graph_inspect_widget.py:CustomInspectTool.js.map\n", + " }\n", + " }, \"custom/main\");\n", + " ;\n", + " });\n", + "\n", + " },\n", + "function(Bokeh) {} // ensure no trailing comma for IE\n", + " ];\n", + "\n", + " function run_inline_js() {\n", + " if ((root.Bokeh !== undefined) || (force === true)) {\n", + " for (let i = 0; i < inline_js.length; i++) {\n", + " try {\n", + " inline_js[i].call(root, root.Bokeh);\n", + " } catch(e) {\n", + " if (!reloading) {\n", + " throw e;\n", + " }\n", + " }\n", + " }\n", + " // Cache old bokeh versions\n", + " if (Bokeh != undefined && !reloading) {\n", + " var NewBokeh = root.Bokeh;\n", + " if (Bokeh.versions === undefined) {\n", + " Bokeh.versions = new Map();\n", + " }\n", + " if (NewBokeh.version !== Bokeh.version) {\n", + " Bokeh.versions.set(NewBokeh.version, NewBokeh)\n", + " }\n", + " root.Bokeh = Bokeh;\n", + " }\n", + " } else if (Date.now() < root._bokeh_timeout) {\n", + " setTimeout(run_inline_js, 100);\n", + " } else if (!root._bokeh_failed_load) {\n", + " console.log(\"Bokeh: BokehJS failed to load within specified timeout.\");\n", + " root._bokeh_failed_load = true;\n", + " }\n", + " root._bokeh_is_initializing = false\n", + " }\n", + "\n", + " function load_or_wait() {\n", + " // Implement a backoff loop that tries to ensure we do not load multiple\n", + " // versions of Bokeh and its dependencies at the same time.\n", + " // In recent versions we use the root._bokeh_is_initializing flag\n", + " // to determine whether there is an ongoing attempt to initialize\n", + " // bokeh, however for backward compatibility we also try to ensure\n", + " // that we do not start loading a newer (Panel>=1.0 and Bokeh>3) version\n", + " // before older versions are fully initialized.\n", + " if (root._bokeh_is_initializing && Date.now() > root._bokeh_timeout) {\n", + " // If the timeout and bokeh was not successfully loaded we reset\n", + " // everything and try loading again\n", + " root._bokeh_timeout = Date.now() + 5000;\n", + " root._bokeh_is_initializing = false;\n", + " root._bokeh_onload_callbacks = undefined;\n", + " root._bokeh_is_loading = 0\n", + " console.log(\"Bokeh: BokehJS was loaded multiple times but one version failed to initialize.\");\n", + " load_or_wait();\n", + " } else if (root._bokeh_is_initializing || (typeof root._bokeh_is_initializing === \"undefined\" && root._bokeh_onload_callbacks !== undefined)) {\n", + " setTimeout(load_or_wait, 100);\n", + " } else {\n", + " root._bokeh_is_initializing = true\n", + " root._bokeh_onload_callbacks = []\n", + " const bokeh_loaded = root.Bokeh != null && (root.Bokeh.version === py_version || (root.Bokeh.versions !== undefined && root.Bokeh.versions.has(py_version)));\n", + " if (!reloading && !bokeh_loaded) {\n", + " if (root.Bokeh) {\n", + " root.Bokeh = undefined;\n", + " }\n", + " console.debug(\"Bokeh: BokehJS not loaded, scheduling load and callback at\", now());\n", + " }\n", + " load_libs(css_urls, js_urls, js_modules, js_exports, function() {\n", + " console.debug(\"Bokeh: BokehJS plotting callback run at\", now());\n", + " run_inline_js();\n", + " });\n", + " }\n", + " }\n", + " // Give older versions of the autoload script a head-start to ensure\n", + " // they initialize before we start loading newer version.\n", + " setTimeout(load_or_wait, 100)\n", + "}(window));" + ], + "application/vnd.holoviews_load.v0+json": "(function(root) {\n function now() {\n return new Date();\n }\n\n const force = true;\n const py_version = '3.6.2'.replace('rc', '-rc.').replace('.dev', '-dev.');\n const reloading = false;\n const Bokeh = root.Bokeh;\n\n // Set a timeout for this load but only if we are not already initializing\n if (typeof (root._bokeh_timeout) === \"undefined\" || (force || !root._bokeh_is_initializing)) {\n root._bokeh_timeout = Date.now() + 5000;\n root._bokeh_failed_load = false;\n }\n\n function run_callbacks() {\n try {\n root._bokeh_onload_callbacks.forEach(function(callback) {\n if (callback != null)\n callback();\n });\n } finally {\n delete root._bokeh_onload_callbacks;\n }\n console.debug(\"Bokeh: all callbacks have finished\");\n }\n\n function load_libs(css_urls, js_urls, js_modules, js_exports, callback) {\n if (css_urls == null) css_urls = [];\n if (js_urls == null) js_urls = [];\n if (js_modules == null) js_modules = [];\n if (js_exports == null) js_exports = {};\n\n root._bokeh_onload_callbacks.push(callback);\n\n if (root._bokeh_is_loading > 0) {\n // Don't load bokeh if it is still initializing\n console.debug(\"Bokeh: BokehJS is being loaded, scheduling callback at\", now());\n return null;\n } else if (js_urls.length === 0 && js_modules.length === 0 && Object.keys(js_exports).length === 0) {\n // There is nothing to load\n run_callbacks();\n return null;\n }\n\n function on_load() {\n root._bokeh_is_loading--;\n if (root._bokeh_is_loading === 0) {\n console.debug(\"Bokeh: all BokehJS libraries/stylesheets loaded\");\n run_callbacks()\n }\n }\n window._bokeh_on_load = on_load\n\n function on_error(e) {\n const src_el = e.srcElement\n console.error(\"failed to load \" + (src_el.href || src_el.src));\n }\n\n const skip = [];\n if (window.requirejs) {\n window.requirejs.config({'packages': {}, 'paths': {'h3': 'https://cdn.jsdelivr.net/npm/h3-js@4.1.0/dist/h3-js.umd', 'deck-gl': 'https://cdn.jsdelivr.net/npm/deck.gl@9.0.20/dist.min', 'deck-json': 'https://cdn.jsdelivr.net/npm/@deck.gl/json@9.0.20/dist.min', 'loader-csv': 'https://cdn.jsdelivr.net/npm/@loaders.gl/csv@4.2.2/dist/dist.min', 'loader-json': 'https://cdn.jsdelivr.net/npm/@loaders.gl/json@4.2.2/dist/dist.min', 'loader-tiles': 'https://cdn.jsdelivr.net/npm/@loaders.gl/3d-tiles@4.2.2/dist/dist.min', 'mapbox-gl': 'https://api.mapbox.com/mapbox-gl-js/v3.0.1/mapbox-gl', 'carto': 'https://cdn.jsdelivr.net/npm/@deck.gl/carto@^9.0.20/dist.min'}, 'shim': {'deck-json': {'deps': ['deck-gl']}, 'deck-gl': {'deps': ['h3']}}});\n require([\"h3\"], function(h3) {\n window.h3 = h3\n on_load()\n })\n require([\"deck-gl\"], function(deck) {\n window.deck = deck\n on_load()\n })\n require([\"deck-json\"], function() {\n on_load()\n })\n require([\"loader-csv\"], function() {\n on_load()\n })\n require([\"loader-json\"], function() {\n on_load()\n })\n require([\"loader-tiles\"], function() {\n on_load()\n })\n require([\"mapbox-gl\"], function(mapboxgl) {\n window.mapboxgl = mapboxgl\n on_load()\n })\n require([\"carto\"], function() {\n on_load()\n })\n root._bokeh_is_loading = css_urls.length + 8;\n } else {\n root._bokeh_is_loading = css_urls.length + js_urls.length + js_modules.length + Object.keys(js_exports).length;\n }\n\n const existing_stylesheets = []\n const links = document.getElementsByTagName('link')\n for (let i = 0; i < links.length; i++) {\n const link = links[i]\n if (link.href != null) {\n existing_stylesheets.push(link.href)\n }\n }\n for (let i = 0; i < css_urls.length; i++) {\n const url = css_urls[i];\n const escaped = encodeURI(url)\n if (existing_stylesheets.indexOf(escaped) !== -1) {\n on_load()\n continue;\n }\n const element = document.createElement(\"link\");\n element.onload = on_load;\n element.onerror = on_error;\n element.rel = \"stylesheet\";\n element.type = \"text/css\";\n element.href = url;\n console.debug(\"Bokeh: injecting link tag for BokehJS stylesheet: \", url);\n document.body.appendChild(element);\n } if (((window.deck !== undefined) && (!(window.deck instanceof HTMLElement))) || window.requirejs) {\n var urls = ['https://cdn.holoviz.org/panel/1.5.4/dist/bundled/deckglplot/h3-js@4.1.0/dist/h3-js.umd.js', 'https://cdn.holoviz.org/panel/1.5.4/dist/bundled/deckglplot/deck.gl@9.0.20/dist.min.js', 'https://cdn.holoviz.org/panel/1.5.4/dist/bundled/deckglplot/@deck.gl/json@9.0.20/dist.min.js', 'https://cdn.holoviz.org/panel/1.5.4/dist/bundled/deckglplot/@loaders.gl/csv@4.2.2/dist/dist.min.js', 'https://cdn.holoviz.org/panel/1.5.4/dist/bundled/deckglplot/@loaders.gl/json@4.2.2/dist/dist.min.js', 'https://cdn.holoviz.org/panel/1.5.4/dist/bundled/deckglplot/@loaders.gl/3d-tiles@4.2.2/dist/dist.min.js', 'https://cdn.holoviz.org/panel/1.5.4/dist/bundled/deckglplot/mapbox-gl-js/v3.0.1/mapbox-gl.js', 'https://cdn.holoviz.org/panel/1.5.4/dist/bundled/deckglplot/maplibre-gl/dist/maplibre-gl.js'];\n for (var i = 0; i < urls.length; i++) {\n skip.push(encodeURI(urls[i]))\n }\n } if (((window.mapboxgl !== undefined) && (!(window.mapboxgl instanceof HTMLElement))) || window.requirejs) {\n var urls = ['https://cdn.holoviz.org/panel/1.5.4/dist/bundled/deckglplot/@deck.gl/carto@^9.0.20/dist.min.js'];\n for (var i = 0; i < urls.length; i++) {\n skip.push(encodeURI(urls[i]))\n }\n } var existing_scripts = []\n const scripts = document.getElementsByTagName('script')\n for (let i = 0; i < scripts.length; i++) {\n var script = scripts[i]\n if (script.src != null) {\n existing_scripts.push(script.src)\n }\n }\n for (let i = 0; i < js_urls.length; i++) {\n const url = js_urls[i];\n const escaped = encodeURI(url)\n if (skip.indexOf(escaped) !== -1 || existing_scripts.indexOf(escaped) !== -1) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\n }\n const element = document.createElement('script');\n element.onload = on_load;\n element.onerror = on_error;\n element.async = false;\n element.src = url;\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n document.head.appendChild(element);\n }\n for (let i = 0; i < js_modules.length; i++) {\n const url = js_modules[i];\n const escaped = encodeURI(url)\n if (skip.indexOf(escaped) !== -1 || existing_scripts.indexOf(escaped) !== -1) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\n }\n var element = document.createElement('script');\n element.onload = on_load;\n element.onerror = on_error;\n element.async = false;\n element.src = url;\n element.type = \"module\";\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n document.head.appendChild(element);\n }\n for (const name in js_exports) {\n const url = js_exports[name];\n const escaped = encodeURI(url)\n if (skip.indexOf(escaped) >= 0 || root[name] != null) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\n }\n var element = document.createElement('script');\n element.onerror = on_error;\n element.async = false;\n element.type = \"module\";\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n element.textContent = `\n import ${name} from \"${url}\"\n window.${name} = ${name}\n window._bokeh_on_load()\n `\n document.head.appendChild(element);\n }\n if (!js_urls.length && !js_modules.length) {\n on_load()\n }\n };\n\n function inject_raw_css(css) {\n const element = document.createElement(\"style\");\n element.appendChild(document.createTextNode(css));\n document.body.appendChild(element);\n }\n\n const js_urls = [\"https://cdn.holoviz.org/panel/1.5.4/dist/bundled/deckglplot/h3-js@4.1.0/dist/h3-js.umd.js\", \"https://cdn.holoviz.org/panel/1.5.4/dist/bundled/deckglplot/deck.gl@9.0.20/dist.min.js\", \"https://cdn.holoviz.org/panel/1.5.4/dist/bundled/deckglplot/@deck.gl/json@9.0.20/dist.min.js\", \"https://cdn.holoviz.org/panel/1.5.4/dist/bundled/deckglplot/@loaders.gl/csv@4.2.2/dist/dist.min.js\", \"https://cdn.holoviz.org/panel/1.5.4/dist/bundled/deckglplot/@loaders.gl/json@4.2.2/dist/dist.min.js\", \"https://cdn.holoviz.org/panel/1.5.4/dist/bundled/deckglplot/@loaders.gl/3d-tiles@4.2.2/dist/dist.min.js\", \"https://cdn.holoviz.org/panel/1.5.4/dist/bundled/deckglplot/mapbox-gl-js/v3.0.1/mapbox-gl.js\", \"https://cdn.holoviz.org/panel/1.5.4/dist/bundled/deckglplot/maplibre-gl/dist/maplibre-gl.js\", \"https://cdn.holoviz.org/panel/1.5.4/dist/bundled/deckglplot/@deck.gl/carto@^9.0.20/dist.min.js\", \"https://cdn.holoviz.org/panel/1.5.4/dist/bundled/reactiveesm/es-module-shims@^1.10.0/dist/es-module-shims.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-3.6.2.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-gl-3.6.2.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-widgets-3.6.2.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-tables-3.6.2.min.js\", \"https://cdn.holoviz.org/panel/1.5.4/dist/panel.min.js\"];\n const js_modules = [];\n const js_exports = {};\n const css_urls = [\"https://cdn.holoviz.org/panel/1.5.4/dist/bundled/deckglplot/mapbox-gl-js/v3.0.1/mapbox-gl.css?v=1.5.4\", \"https://cdn.holoviz.org/panel/1.5.4/dist/bundled/deckglplot/maplibre-gl@4.4.1/dist/maplibre-gl.css?v=1.5.4\"];\n const inline_js = [ function(Bokeh) {\n inject_raw_css(\"\\n.dataframe table{\\n border: none;\\n}\\n\\n.panel-df table{\\n width: 100%;\\n border-collapse: collapse;\\n border: none;\\n}\\n.panel-df td{\\n white-space: nowrap;\\n overflow: auto;\\n text-overflow: ellipsis;\\n}\\n\");\n }, function(Bokeh) {\n inject_raw_css(\"\\n.multi-select{\\n color: white;\\n z-index: 100;\\n background: rgba(44,43,43,0.5);\\n border-radius: 1px;\\n width: 120px !important;\\n height: 30px !important;\\n}\\n.multi-select > .bk {\\n padding: 5px;\\n width: 120px !important;\\n height: 30px !important;\\n}\\n\\n.deck-chart {\\n z-index: 10;\\n position: initial !important;\\n}\\n\");\n }, function(Bokeh) {\n inject_raw_css(\"\\n.center-header {\\n text-align: center\\n}\\n.bk-input-group {\\n padding: 10px;\\n}\\n#sidebar {\\n padding-top: 10px;\\n}\\n.custom-widget-box {\\n margin-top: 20px;\\n padding: 5px;\\n border: None !important;\\n}\\n.custom-widget-box > p {\\n margin: 0px;\\n}\\n.bk-input-group {\\n color: None !important;\\n}\\n.indicator {\\n text-align: center;\\n}\\n.widget-card {\\n margin: 5px 10px;\\n}\\n.number-card {\\n margin: 5px 10px;\\n text-align: center;\\n}\\n.number-card-value {\\n width: 100%;\\n margin: 0px;\\n}\\n\");\n }, function(Bokeh) {\n Bokeh.set_log_level(\"info\");\n },\n function(Bokeh) {\n (function(root, factory) {\n factory(root[\"Bokeh\"]);\n })(this, function(Bokeh) {\n let define;\n return (function outer(modules, entry) {\n if (Bokeh != null) {\n return Bokeh.register_plugin(modules, entry);\n } else {\n throw new Error(\"Cannot find Bokeh. You have to load it prior to loading plugins.\");\n }\n })\n ({\n \"custom/main\": function(require, module, exports) {\n const models = {\n \"CustomInspectTool\": require(\"custom/cuxfilter.charts.datashader.custom_extensions.graph_inspect_widget.custom_inspect_tool\").CustomInspectTool\n };\n require(\"base\").register_models(models);\n module.exports = models;\n },\n \"custom/cuxfilter.charts.datashader.custom_extensions.graph_inspect_widget.custom_inspect_tool\": function(require, module, exports) {\n \"use strict\";\n var _a;\n Object.defineProperty(exports, \"__esModule\", { value: true });\n exports.CustomInspectTool = exports.CustomInspectToolView = void 0;\n const inspect_tool_1 = require(\"models/tools/inspectors/inspect_tool\");\n class CustomInspectToolView extends inspect_tool_1.InspectToolView {\n connect_signals() {\n super.connect_signals();\n this.on_change([this.model.properties.active], () => {\n this.model._active = this.model.active;\n });\n }\n }\n exports.CustomInspectToolView = CustomInspectToolView;\n CustomInspectToolView.__name__ = \"CustomInspectToolView\";\n class CustomInspectTool extends inspect_tool_1.InspectTool {\n constructor(attrs) {\n super(attrs);\n }\n }\n exports.CustomInspectTool = CustomInspectTool;\n _a = CustomInspectTool;\n CustomInspectTool.__name__ = \"CustomInspectTool\";\n (() => {\n _a.prototype.default_view = CustomInspectToolView;\n _a.define(({ Boolean }) => ({\n _active: [Boolean, true]\n }));\n _a.register_alias(\"customInspect\", () => new _a());\n })();\n //# sourceMappingURL=graph_inspect_widget.py:CustomInspectTool.js.map\n }\n }, \"custom/main\");\n ;\n });\n\n },\nfunction(Bokeh) {} // ensure no trailing comma for IE\n ];\n\n function run_inline_js() {\n if ((root.Bokeh !== undefined) || (force === true)) {\n for (let i = 0; i < inline_js.length; i++) {\n try {\n inline_js[i].call(root, root.Bokeh);\n } catch(e) {\n if (!reloading) {\n throw e;\n }\n }\n }\n // Cache old bokeh versions\n if (Bokeh != undefined && !reloading) {\n var NewBokeh = root.Bokeh;\n if (Bokeh.versions === undefined) {\n Bokeh.versions = new Map();\n }\n if (NewBokeh.version !== Bokeh.version) {\n Bokeh.versions.set(NewBokeh.version, NewBokeh)\n }\n root.Bokeh = Bokeh;\n }\n } else if (Date.now() < root._bokeh_timeout) {\n setTimeout(run_inline_js, 100);\n } else if (!root._bokeh_failed_load) {\n console.log(\"Bokeh: BokehJS failed to load within specified timeout.\");\n root._bokeh_failed_load = true;\n }\n root._bokeh_is_initializing = false\n }\n\n function load_or_wait() {\n // Implement a backoff loop that tries to ensure we do not load multiple\n // versions of Bokeh and its dependencies at the same time.\n // In recent versions we use the root._bokeh_is_initializing flag\n // to determine whether there is an ongoing attempt to initialize\n // bokeh, however for backward compatibility we also try to ensure\n // that we do not start loading a newer (Panel>=1.0 and Bokeh>3) version\n // before older versions are fully initialized.\n if (root._bokeh_is_initializing && Date.now() > root._bokeh_timeout) {\n // If the timeout and bokeh was not successfully loaded we reset\n // everything and try loading again\n root._bokeh_timeout = Date.now() + 5000;\n root._bokeh_is_initializing = false;\n root._bokeh_onload_callbacks = undefined;\n root._bokeh_is_loading = 0\n console.log(\"Bokeh: BokehJS was loaded multiple times but one version failed to initialize.\");\n load_or_wait();\n } else if (root._bokeh_is_initializing || (typeof root._bokeh_is_initializing === \"undefined\" && root._bokeh_onload_callbacks !== undefined)) {\n setTimeout(load_or_wait, 100);\n } else {\n root._bokeh_is_initializing = true\n root._bokeh_onload_callbacks = []\n const bokeh_loaded = root.Bokeh != null && (root.Bokeh.version === py_version || (root.Bokeh.versions !== undefined && root.Bokeh.versions.has(py_version)));\n if (!reloading && !bokeh_loaded) {\n if (root.Bokeh) {\n root.Bokeh = undefined;\n }\n console.debug(\"Bokeh: BokehJS not loaded, scheduling load and callback at\", now());\n }\n load_libs(css_urls, js_urls, js_modules, js_exports, function() {\n console.debug(\"Bokeh: BokehJS plotting callback run at\", now());\n run_inline_js();\n });\n }\n }\n // Give older versions of the autoload script a head-start to ensure\n // they initialize before we start loading newer version.\n setTimeout(load_or_wait, 100)\n}(window));" + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/javascript": [ + "\n", + "if ((window.PyViz === undefined) || (window.PyViz instanceof HTMLElement)) {\n", + " window.PyViz = {comms: {}, comm_status:{}, kernels:{}, receivers: {}, plot_index: []}\n", + "}\n", + "\n", + "\n", + " function JupyterCommManager() {\n", + " }\n", + "\n", + " JupyterCommManager.prototype.register_target = function(plot_id, comm_id, msg_handler) {\n", + " if (window.comm_manager || ((window.Jupyter !== undefined) && (Jupyter.notebook.kernel != null))) {\n", + " var comm_manager = window.comm_manager || Jupyter.notebook.kernel.comm_manager;\n", + " comm_manager.register_target(comm_id, function(comm) {\n", + " comm.on_msg(msg_handler);\n", + " });\n", + " } else if ((plot_id in window.PyViz.kernels) && (window.PyViz.kernels[plot_id])) {\n", + " window.PyViz.kernels[plot_id].registerCommTarget(comm_id, function(comm) {\n", + " comm.onMsg = msg_handler;\n", + " });\n", + " } else if (typeof google != 'undefined' && google.colab.kernel != null) {\n", + " google.colab.kernel.comms.registerTarget(comm_id, (comm) => {\n", + " var messages = comm.messages[Symbol.asyncIterator]();\n", + " function processIteratorResult(result) {\n", + " var message = result.value;\n", + " console.log(message)\n", + " var content = {data: message.data, comm_id};\n", + " var buffers = []\n", + " for (var buffer of message.buffers || []) {\n", + " buffers.push(new DataView(buffer))\n", + " }\n", + " var metadata = message.metadata || {};\n", + " var msg = {content, buffers, metadata}\n", + " msg_handler(msg);\n", + " return messages.next().then(processIteratorResult);\n", + " }\n", + " return messages.next().then(processIteratorResult);\n", + " })\n", + " }\n", + " }\n", + "\n", + " JupyterCommManager.prototype.get_client_comm = function(plot_id, comm_id, msg_handler) {\n", + " if (comm_id in window.PyViz.comms) {\n", + " return window.PyViz.comms[comm_id];\n", + " } else if (window.comm_manager || ((window.Jupyter !== undefined) && (Jupyter.notebook.kernel != null))) {\n", + " var comm_manager = window.comm_manager || Jupyter.notebook.kernel.comm_manager;\n", + " var comm = comm_manager.new_comm(comm_id, {}, {}, {}, comm_id);\n", + " if (msg_handler) {\n", + " comm.on_msg(msg_handler);\n", + " }\n", + " } else if ((plot_id in window.PyViz.kernels) && (window.PyViz.kernels[plot_id])) {\n", + " var comm = window.PyViz.kernels[plot_id].connectToComm(comm_id);\n", + " comm.open();\n", + " if (msg_handler) {\n", + " comm.onMsg = msg_handler;\n", + " }\n", + " } else if (typeof google != 'undefined' && google.colab.kernel != null) {\n", + " var comm_promise = google.colab.kernel.comms.open(comm_id)\n", + " comm_promise.then((comm) => {\n", + " window.PyViz.comms[comm_id] = comm;\n", + " if (msg_handler) {\n", + " var messages = comm.messages[Symbol.asyncIterator]();\n", + " function processIteratorResult(result) {\n", + " var message = result.value;\n", + " var content = {data: message.data};\n", + " var metadata = message.metadata || {comm_id};\n", + " var msg = {content, metadata}\n", + " msg_handler(msg);\n", + " return messages.next().then(processIteratorResult);\n", + " }\n", + " return messages.next().then(processIteratorResult);\n", + " }\n", + " }) \n", + " var sendClosure = (data, metadata, buffers, disposeOnDone) => {\n", + " return comm_promise.then((comm) => {\n", + " comm.send(data, metadata, buffers, disposeOnDone);\n", + " });\n", + " };\n", + " var comm = {\n", + " send: sendClosure\n", + " };\n", + " }\n", + " window.PyViz.comms[comm_id] = comm;\n", + " return comm;\n", + " }\n", + " window.PyViz.comm_manager = new JupyterCommManager();\n", + " \n", + "\n", + "\n", + "var JS_MIME_TYPE = 'application/javascript';\n", + "var HTML_MIME_TYPE = 'text/html';\n", + "var EXEC_MIME_TYPE = 'application/vnd.holoviews_exec.v0+json';\n", + "var CLASS_NAME = 'output';\n", + "\n", + "/**\n", + " * Render data to the DOM node\n", + " */\n", + "function render(props, node) {\n", + " var div = document.createElement(\"div\");\n", + " var script = document.createElement(\"script\");\n", + " node.appendChild(div);\n", + " node.appendChild(script);\n", + "}\n", + "\n", + "/**\n", + " * Handle when a new output is added\n", + " */\n", + "function handle_add_output(event, handle) {\n", + " var output_area = handle.output_area;\n", + " var output = handle.output;\n", + " if ((output.data == undefined) || (!output.data.hasOwnProperty(EXEC_MIME_TYPE))) {\n", + " return\n", + " }\n", + " var id = output.metadata[EXEC_MIME_TYPE][\"id\"];\n", + " var toinsert = output_area.element.find(\".\" + CLASS_NAME.split(' ')[0]);\n", + " if (id !== undefined) {\n", + " var nchildren = toinsert.length;\n", + " var html_node = toinsert[nchildren-1].children[0];\n", + " html_node.innerHTML = output.data[HTML_MIME_TYPE];\n", + " var scripts = [];\n", + " var nodelist = html_node.querySelectorAll(\"script\");\n", + " for (var i in nodelist) {\n", + " if (nodelist.hasOwnProperty(i)) {\n", + " scripts.push(nodelist[i])\n", + " }\n", + " }\n", + "\n", + " scripts.forEach( function (oldScript) {\n", + " var newScript = document.createElement(\"script\");\n", + " var attrs = [];\n", + " var nodemap = oldScript.attributes;\n", + " for (var j in nodemap) {\n", + " if (nodemap.hasOwnProperty(j)) {\n", + " attrs.push(nodemap[j])\n", + " }\n", + " }\n", + " attrs.forEach(function(attr) { newScript.setAttribute(attr.name, attr.value) });\n", + " newScript.appendChild(document.createTextNode(oldScript.innerHTML));\n", + " oldScript.parentNode.replaceChild(newScript, oldScript);\n", + " });\n", + " if (JS_MIME_TYPE in output.data) {\n", + " toinsert[nchildren-1].children[1].textContent = output.data[JS_MIME_TYPE];\n", + " }\n", + " output_area._hv_plot_id = id;\n", + " if ((window.Bokeh !== undefined) && (id in Bokeh.index)) {\n", + " window.PyViz.plot_index[id] = Bokeh.index[id];\n", + " } else {\n", + " window.PyViz.plot_index[id] = null;\n", + " }\n", + " } else if (output.metadata[EXEC_MIME_TYPE][\"server_id\"] !== undefined) {\n", + " var bk_div = document.createElement(\"div\");\n", + " bk_div.innerHTML = output.data[HTML_MIME_TYPE];\n", + " var script_attrs = bk_div.children[0].attributes;\n", + " for (var i = 0; i < script_attrs.length; i++) {\n", + " toinsert[toinsert.length - 1].childNodes[1].setAttribute(script_attrs[i].name, script_attrs[i].value);\n", + " }\n", + " // store reference to server id on output_area\n", + " output_area._bokeh_server_id = output.metadata[EXEC_MIME_TYPE][\"server_id\"];\n", + " }\n", + "}\n", + "\n", + "/**\n", + " * Handle when an output is cleared or removed\n", + " */\n", + "function handle_clear_output(event, handle) {\n", + " var id = handle.cell.output_area._hv_plot_id;\n", + " var server_id = handle.cell.output_area._bokeh_server_id;\n", + " if (((id === undefined) || !(id in PyViz.plot_index)) && (server_id !== undefined)) { return; }\n", + " var comm = window.PyViz.comm_manager.get_client_comm(\"hv-extension-comm\", \"hv-extension-comm\", function () {});\n", + " if (server_id !== null) {\n", + " comm.send({event_type: 'server_delete', 'id': server_id});\n", + " return;\n", + " } else if (comm !== null) {\n", + " comm.send({event_type: 'delete', 'id': id});\n", + " }\n", + " delete PyViz.plot_index[id];\n", + " if ((window.Bokeh !== undefined) & (id in window.Bokeh.index)) {\n", + " var doc = window.Bokeh.index[id].model.document\n", + " doc.clear();\n", + " const i = window.Bokeh.documents.indexOf(doc);\n", + " if (i > -1) {\n", + " window.Bokeh.documents.splice(i, 1);\n", + " }\n", + " }\n", + "}\n", + "\n", + "/**\n", + " * Handle kernel restart event\n", + " */\n", + "function handle_kernel_cleanup(event, handle) {\n", + " delete PyViz.comms[\"hv-extension-comm\"];\n", + " window.PyViz.plot_index = {}\n", + "}\n", + "\n", + "/**\n", + " * Handle update_display_data messages\n", + " */\n", + "function handle_update_output(event, handle) {\n", + " handle_clear_output(event, {cell: {output_area: handle.output_area}})\n", + " handle_add_output(event, handle)\n", + "}\n", + "\n", + "function register_renderer(events, OutputArea) {\n", + " function append_mime(data, metadata, element) {\n", + " // create a DOM node to render to\n", + " var toinsert = this.create_output_subarea(\n", + " metadata,\n", + " CLASS_NAME,\n", + " EXEC_MIME_TYPE\n", + " );\n", + " this.keyboard_manager.register_events(toinsert);\n", + " // Render to node\n", + " var props = {data: data, metadata: metadata[EXEC_MIME_TYPE]};\n", + " render(props, toinsert[0]);\n", + " element.append(toinsert);\n", + " return toinsert\n", + " }\n", + "\n", + " events.on('output_added.OutputArea', handle_add_output);\n", + " events.on('output_updated.OutputArea', handle_update_output);\n", + " events.on('clear_output.CodeCell', handle_clear_output);\n", + " events.on('delete.Cell', handle_clear_output);\n", + " events.on('kernel_ready.Kernel', handle_kernel_cleanup);\n", + "\n", + " OutputArea.prototype.register_mime_type(EXEC_MIME_TYPE, append_mime, {\n", + " safe: true,\n", + " index: 0\n", + " });\n", + "}\n", + "\n", + "if (window.Jupyter !== undefined) {\n", + " try {\n", + " var events = require('base/js/events');\n", + " var OutputArea = require('notebook/js/outputarea').OutputArea;\n", + " if (OutputArea.prototype.mime_types().indexOf(EXEC_MIME_TYPE) == -1) {\n", + " register_renderer(events, OutputArea);\n", + " }\n", + " } catch(err) {\n", + " }\n", + "}\n" + ], + "application/vnd.holoviews_load.v0+json": "\nif ((window.PyViz === undefined) || (window.PyViz instanceof HTMLElement)) {\n window.PyViz = {comms: {}, comm_status:{}, kernels:{}, receivers: {}, plot_index: []}\n}\n\n\n function JupyterCommManager() {\n }\n\n JupyterCommManager.prototype.register_target = function(plot_id, comm_id, msg_handler) {\n if (window.comm_manager || ((window.Jupyter !== undefined) && (Jupyter.notebook.kernel != null))) {\n var comm_manager = window.comm_manager || Jupyter.notebook.kernel.comm_manager;\n comm_manager.register_target(comm_id, function(comm) {\n comm.on_msg(msg_handler);\n });\n } else if ((plot_id in window.PyViz.kernels) && (window.PyViz.kernels[plot_id])) {\n window.PyViz.kernels[plot_id].registerCommTarget(comm_id, function(comm) {\n comm.onMsg = msg_handler;\n });\n } else if (typeof google != 'undefined' && google.colab.kernel != null) {\n google.colab.kernel.comms.registerTarget(comm_id, (comm) => {\n var messages = comm.messages[Symbol.asyncIterator]();\n function processIteratorResult(result) {\n var message = result.value;\n console.log(message)\n var content = {data: message.data, comm_id};\n var buffers = []\n for (var buffer of message.buffers || []) {\n buffers.push(new DataView(buffer))\n }\n var metadata = message.metadata || {};\n var msg = {content, buffers, metadata}\n msg_handler(msg);\n return messages.next().then(processIteratorResult);\n }\n return messages.next().then(processIteratorResult);\n })\n }\n }\n\n JupyterCommManager.prototype.get_client_comm = function(plot_id, comm_id, msg_handler) {\n if (comm_id in window.PyViz.comms) {\n return window.PyViz.comms[comm_id];\n } else if (window.comm_manager || ((window.Jupyter !== undefined) && (Jupyter.notebook.kernel != null))) {\n var comm_manager = window.comm_manager || Jupyter.notebook.kernel.comm_manager;\n var comm = comm_manager.new_comm(comm_id, {}, {}, {}, comm_id);\n if (msg_handler) {\n comm.on_msg(msg_handler);\n }\n } else if ((plot_id in window.PyViz.kernels) && (window.PyViz.kernels[plot_id])) {\n var comm = window.PyViz.kernels[plot_id].connectToComm(comm_id);\n comm.open();\n if (msg_handler) {\n comm.onMsg = msg_handler;\n }\n } else if (typeof google != 'undefined' && google.colab.kernel != null) {\n var comm_promise = google.colab.kernel.comms.open(comm_id)\n comm_promise.then((comm) => {\n window.PyViz.comms[comm_id] = comm;\n if (msg_handler) {\n var messages = comm.messages[Symbol.asyncIterator]();\n function processIteratorResult(result) {\n var message = result.value;\n var content = {data: message.data};\n var metadata = message.metadata || {comm_id};\n var msg = {content, metadata}\n msg_handler(msg);\n return messages.next().then(processIteratorResult);\n }\n return messages.next().then(processIteratorResult);\n }\n }) \n var sendClosure = (data, metadata, buffers, disposeOnDone) => {\n return comm_promise.then((comm) => {\n comm.send(data, metadata, buffers, disposeOnDone);\n });\n };\n var comm = {\n send: sendClosure\n };\n }\n window.PyViz.comms[comm_id] = comm;\n return comm;\n }\n window.PyViz.comm_manager = new JupyterCommManager();\n \n\n\nvar JS_MIME_TYPE = 'application/javascript';\nvar HTML_MIME_TYPE = 'text/html';\nvar EXEC_MIME_TYPE = 'application/vnd.holoviews_exec.v0+json';\nvar CLASS_NAME = 'output';\n\n/**\n * Render data to the DOM node\n */\nfunction render(props, node) {\n var div = document.createElement(\"div\");\n var script = document.createElement(\"script\");\n node.appendChild(div);\n node.appendChild(script);\n}\n\n/**\n * Handle when a new output is added\n */\nfunction handle_add_output(event, handle) {\n var output_area = handle.output_area;\n var output = handle.output;\n if ((output.data == undefined) || (!output.data.hasOwnProperty(EXEC_MIME_TYPE))) {\n return\n }\n var id = output.metadata[EXEC_MIME_TYPE][\"id\"];\n var toinsert = output_area.element.find(\".\" + CLASS_NAME.split(' ')[0]);\n if (id !== undefined) {\n var nchildren = toinsert.length;\n var html_node = toinsert[nchildren-1].children[0];\n html_node.innerHTML = output.data[HTML_MIME_TYPE];\n var scripts = [];\n var nodelist = html_node.querySelectorAll(\"script\");\n for (var i in nodelist) {\n if (nodelist.hasOwnProperty(i)) {\n scripts.push(nodelist[i])\n }\n }\n\n scripts.forEach( function (oldScript) {\n var newScript = document.createElement(\"script\");\n var attrs = [];\n var nodemap = oldScript.attributes;\n for (var j in nodemap) {\n if (nodemap.hasOwnProperty(j)) {\n attrs.push(nodemap[j])\n }\n }\n attrs.forEach(function(attr) { newScript.setAttribute(attr.name, attr.value) });\n newScript.appendChild(document.createTextNode(oldScript.innerHTML));\n oldScript.parentNode.replaceChild(newScript, oldScript);\n });\n if (JS_MIME_TYPE in output.data) {\n toinsert[nchildren-1].children[1].textContent = output.data[JS_MIME_TYPE];\n }\n output_area._hv_plot_id = id;\n if ((window.Bokeh !== undefined) && (id in Bokeh.index)) {\n window.PyViz.plot_index[id] = Bokeh.index[id];\n } else {\n window.PyViz.plot_index[id] = null;\n }\n } else if (output.metadata[EXEC_MIME_TYPE][\"server_id\"] !== undefined) {\n var bk_div = document.createElement(\"div\");\n bk_div.innerHTML = output.data[HTML_MIME_TYPE];\n var script_attrs = bk_div.children[0].attributes;\n for (var i = 0; i < script_attrs.length; i++) {\n toinsert[toinsert.length - 1].childNodes[1].setAttribute(script_attrs[i].name, script_attrs[i].value);\n }\n // store reference to server id on output_area\n output_area._bokeh_server_id = output.metadata[EXEC_MIME_TYPE][\"server_id\"];\n }\n}\n\n/**\n * Handle when an output is cleared or removed\n */\nfunction handle_clear_output(event, handle) {\n var id = handle.cell.output_area._hv_plot_id;\n var server_id = handle.cell.output_area._bokeh_server_id;\n if (((id === undefined) || !(id in PyViz.plot_index)) && (server_id !== undefined)) { return; }\n var comm = window.PyViz.comm_manager.get_client_comm(\"hv-extension-comm\", \"hv-extension-comm\", function () {});\n if (server_id !== null) {\n comm.send({event_type: 'server_delete', 'id': server_id});\n return;\n } else if (comm !== null) {\n comm.send({event_type: 'delete', 'id': id});\n }\n delete PyViz.plot_index[id];\n if ((window.Bokeh !== undefined) & (id in window.Bokeh.index)) {\n var doc = window.Bokeh.index[id].model.document\n doc.clear();\n const i = window.Bokeh.documents.indexOf(doc);\n if (i > -1) {\n window.Bokeh.documents.splice(i, 1);\n }\n }\n}\n\n/**\n * Handle kernel restart event\n */\nfunction handle_kernel_cleanup(event, handle) {\n delete PyViz.comms[\"hv-extension-comm\"];\n window.PyViz.plot_index = {}\n}\n\n/**\n * Handle update_display_data messages\n */\nfunction handle_update_output(event, handle) {\n handle_clear_output(event, {cell: {output_area: handle.output_area}})\n handle_add_output(event, handle)\n}\n\nfunction register_renderer(events, OutputArea) {\n function append_mime(data, metadata, element) {\n // create a DOM node to render to\n var toinsert = this.create_output_subarea(\n metadata,\n CLASS_NAME,\n EXEC_MIME_TYPE\n );\n this.keyboard_manager.register_events(toinsert);\n // Render to node\n var props = {data: data, metadata: metadata[EXEC_MIME_TYPE]};\n render(props, toinsert[0]);\n element.append(toinsert);\n return toinsert\n }\n\n events.on('output_added.OutputArea', handle_add_output);\n events.on('output_updated.OutputArea', handle_update_output);\n events.on('clear_output.CodeCell', handle_clear_output);\n events.on('delete.Cell', handle_clear_output);\n events.on('kernel_ready.Kernel', handle_kernel_cleanup);\n\n OutputArea.prototype.register_mime_type(EXEC_MIME_TYPE, append_mime, {\n safe: true,\n index: 0\n });\n}\n\nif (window.Jupyter !== undefined) {\n try {\n var events = require('base/js/events');\n var OutputArea = require('notebook/js/outputarea').OutputArea;\n if (OutputArea.prototype.mime_types().indexOf(EXEC_MIME_TYPE) == -1) {\n register_renderer(events, OutputArea);\n }\n } catch(err) {\n }\n}\n" + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/javascript": [ + "(function(root) {\n", + " function now() {\n", + " return new Date();\n", + " }\n", + "\n", + " const force = false;\n", + " const py_version = '3.6.2'.replace('rc', '-rc.').replace('.dev', '-dev.');\n", + " const reloading = true;\n", + " const Bokeh = root.Bokeh;\n", + "\n", + " // Set a timeout for this load but only if we are not already initializing\n", + " if (typeof (root._bokeh_timeout) === \"undefined\" || (force || !root._bokeh_is_initializing)) {\n", + " root._bokeh_timeout = Date.now() + 5000;\n", + " root._bokeh_failed_load = false;\n", + " }\n", + "\n", + " function run_callbacks() {\n", + " try {\n", + " root._bokeh_onload_callbacks.forEach(function(callback) {\n", + " if (callback != null)\n", + " callback();\n", + " });\n", + " } finally {\n", + " delete root._bokeh_onload_callbacks;\n", + " }\n", + " console.debug(\"Bokeh: all callbacks have finished\");\n", + " }\n", + "\n", + " function load_libs(css_urls, js_urls, js_modules, js_exports, callback) {\n", + " if (css_urls == null) css_urls = [];\n", + " if (js_urls == null) js_urls = [];\n", + " if (js_modules == null) js_modules = [];\n", + " if (js_exports == null) js_exports = {};\n", + "\n", + " root._bokeh_onload_callbacks.push(callback);\n", + "\n", + " if (root._bokeh_is_loading > 0) {\n", + " // Don't load bokeh if it is still initializing\n", + " console.debug(\"Bokeh: BokehJS is being loaded, scheduling callback at\", now());\n", + " return null;\n", + " } else if (js_urls.length === 0 && js_modules.length === 0 && Object.keys(js_exports).length === 0) {\n", + " // There is nothing to load\n", + " run_callbacks();\n", + " return null;\n", + " }\n", + "\n", + " function on_load() {\n", + " root._bokeh_is_loading--;\n", + " if (root._bokeh_is_loading === 0) {\n", + " console.debug(\"Bokeh: all BokehJS libraries/stylesheets loaded\");\n", + " run_callbacks()\n", + " }\n", + " }\n", + " window._bokeh_on_load = on_load\n", + "\n", + " function on_error(e) {\n", + " const src_el = e.srcElement\n", + " console.error(\"failed to load \" + (src_el.href || src_el.src));\n", + " }\n", + "\n", + " const skip = [];\n", + " if (window.requirejs) {\n", + " window.requirejs.config({'packages': {}, 'paths': {'h3': 'https://cdn.jsdelivr.net/npm/h3-js@4.1.0/dist/h3-js.umd', 'deck-gl': 'https://cdn.jsdelivr.net/npm/deck.gl@9.0.20/dist.min', 'deck-json': 'https://cdn.jsdelivr.net/npm/@deck.gl/json@9.0.20/dist.min', 'loader-csv': 'https://cdn.jsdelivr.net/npm/@loaders.gl/csv@4.2.2/dist/dist.min', 'loader-json': 'https://cdn.jsdelivr.net/npm/@loaders.gl/json@4.2.2/dist/dist.min', 'loader-tiles': 'https://cdn.jsdelivr.net/npm/@loaders.gl/3d-tiles@4.2.2/dist/dist.min', 'mapbox-gl': 'https://api.mapbox.com/mapbox-gl-js/v3.0.1/mapbox-gl', 'carto': 'https://cdn.jsdelivr.net/npm/@deck.gl/carto@^9.0.20/dist.min'}, 'shim': {'deck-json': {'deps': ['deck-gl']}, 'deck-gl': {'deps': ['h3']}}});\n", + " require([\"h3\"], function(h3) {\n", + " window.h3 = h3\n", + " on_load()\n", + " })\n", + " require([\"deck-gl\"], function(deck) {\n", + " window.deck = deck\n", + " on_load()\n", + " })\n", + " require([\"deck-json\"], function() {\n", + " on_load()\n", + " })\n", + " require([\"loader-csv\"], function() {\n", + " on_load()\n", + " })\n", + " require([\"loader-json\"], function() {\n", + " on_load()\n", + " })\n", + " require([\"loader-tiles\"], function() {\n", + " on_load()\n", + " })\n", + " require([\"mapbox-gl\"], function(mapboxgl) {\n", + " window.mapboxgl = mapboxgl\n", + " on_load()\n", + " })\n", + " require([\"carto\"], function() {\n", + " on_load()\n", + " })\n", + " root._bokeh_is_loading = css_urls.length + 8;\n", + " } else {\n", + " root._bokeh_is_loading = css_urls.length + js_urls.length + js_modules.length + Object.keys(js_exports).length;\n", + " }\n", + "\n", + " const existing_stylesheets = []\n", + " const links = document.getElementsByTagName('link')\n", + " for (let i = 0; i < links.length; i++) {\n", + " const link = links[i]\n", + " if (link.href != null) {\n", + " existing_stylesheets.push(link.href)\n", + " }\n", + " }\n", + " for (let i = 0; i < css_urls.length; i++) {\n", + " const url = css_urls[i];\n", + " const escaped = encodeURI(url)\n", + " if (existing_stylesheets.indexOf(escaped) !== -1) {\n", + " on_load()\n", + " continue;\n", + " }\n", + " const element = document.createElement(\"link\");\n", + " element.onload = on_load;\n", + " element.onerror = on_error;\n", + " element.rel = \"stylesheet\";\n", + " element.type = \"text/css\";\n", + " element.href = url;\n", + " console.debug(\"Bokeh: injecting link tag for BokehJS stylesheet: \", url);\n", + " document.body.appendChild(element);\n", + " } if (((window.deck !== undefined) && (!(window.deck instanceof HTMLElement))) || window.requirejs) {\n", + " var urls = ['https://cdn.holoviz.org/panel/1.5.4/dist/bundled/deckglplot/h3-js@4.1.0/dist/h3-js.umd.js', 'https://cdn.holoviz.org/panel/1.5.4/dist/bundled/deckglplot/deck.gl@9.0.20/dist.min.js', 'https://cdn.holoviz.org/panel/1.5.4/dist/bundled/deckglplot/@deck.gl/json@9.0.20/dist.min.js', 'https://cdn.holoviz.org/panel/1.5.4/dist/bundled/deckglplot/@loaders.gl/csv@4.2.2/dist/dist.min.js', 'https://cdn.holoviz.org/panel/1.5.4/dist/bundled/deckglplot/@loaders.gl/json@4.2.2/dist/dist.min.js', 'https://cdn.holoviz.org/panel/1.5.4/dist/bundled/deckglplot/@loaders.gl/3d-tiles@4.2.2/dist/dist.min.js', 'https://cdn.holoviz.org/panel/1.5.4/dist/bundled/deckglplot/mapbox-gl-js/v3.0.1/mapbox-gl.js', 'https://cdn.holoviz.org/panel/1.5.4/dist/bundled/deckglplot/maplibre-gl/dist/maplibre-gl.js'];\n", + " for (var i = 0; i < urls.length; i++) {\n", + " skip.push(encodeURI(urls[i]))\n", + " }\n", + " } if (((window.mapboxgl !== undefined) && (!(window.mapboxgl instanceof HTMLElement))) || window.requirejs) {\n", + " var urls = ['https://cdn.holoviz.org/panel/1.5.4/dist/bundled/deckglplot/@deck.gl/carto@^9.0.20/dist.min.js'];\n", + " for (var i = 0; i < urls.length; i++) {\n", + " skip.push(encodeURI(urls[i]))\n", + " }\n", + " } var existing_scripts = []\n", + " const scripts = document.getElementsByTagName('script')\n", + " for (let i = 0; i < scripts.length; i++) {\n", + " var script = scripts[i]\n", + " if (script.src != null) {\n", + " existing_scripts.push(script.src)\n", + " }\n", + " }\n", + " for (let i = 0; i < js_urls.length; i++) {\n", + " const url = js_urls[i];\n", + " const escaped = encodeURI(url)\n", + " if (skip.indexOf(escaped) !== -1 || existing_scripts.indexOf(escaped) !== -1) {\n", + " if (!window.requirejs) {\n", + " on_load();\n", + " }\n", + " continue;\n", + " }\n", + " const element = document.createElement('script');\n", + " element.onload = on_load;\n", + " element.onerror = on_error;\n", + " element.async = false;\n", + " element.src = url;\n", + " console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n", + " document.head.appendChild(element);\n", + " }\n", + " for (let i = 0; i < js_modules.length; i++) {\n", + " const url = js_modules[i];\n", + " const escaped = encodeURI(url)\n", + " if (skip.indexOf(escaped) !== -1 || existing_scripts.indexOf(escaped) !== -1) {\n", + " if (!window.requirejs) {\n", + " on_load();\n", + " }\n", + " continue;\n", + " }\n", + " var element = document.createElement('script');\n", + " element.onload = on_load;\n", + " element.onerror = on_error;\n", + " element.async = false;\n", + " element.src = url;\n", + " element.type = \"module\";\n", + " console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n", + " document.head.appendChild(element);\n", + " }\n", + " for (const name in js_exports) {\n", + " const url = js_exports[name];\n", + " const escaped = encodeURI(url)\n", + " if (skip.indexOf(escaped) >= 0 || root[name] != null) {\n", + " if (!window.requirejs) {\n", + " on_load();\n", + " }\n", + " continue;\n", + " }\n", + " var element = document.createElement('script');\n", + " element.onerror = on_error;\n", + " element.async = false;\n", + " element.type = \"module\";\n", + " console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n", + " element.textContent = `\n", + " import ${name} from \"${url}\"\n", + " window.${name} = ${name}\n", + " window._bokeh_on_load()\n", + " `\n", + " document.head.appendChild(element);\n", + " }\n", + " if (!js_urls.length && !js_modules.length) {\n", + " on_load()\n", + " }\n", + " };\n", + "\n", + " function inject_raw_css(css) {\n", + " const element = document.createElement(\"style\");\n", + " element.appendChild(document.createTextNode(css));\n", + " document.body.appendChild(element);\n", + " }\n", + "\n", + " const js_urls = [\"https://cdn.holoviz.org/panel/1.5.4/dist/bundled/deckglplot/h3-js@4.1.0/dist/h3-js.umd.js\", \"https://cdn.holoviz.org/panel/1.5.4/dist/bundled/deckglplot/deck.gl@9.0.20/dist.min.js\", \"https://cdn.holoviz.org/panel/1.5.4/dist/bundled/deckglplot/@deck.gl/json@9.0.20/dist.min.js\", \"https://cdn.holoviz.org/panel/1.5.4/dist/bundled/deckglplot/@loaders.gl/csv@4.2.2/dist/dist.min.js\", \"https://cdn.holoviz.org/panel/1.5.4/dist/bundled/deckglplot/@loaders.gl/json@4.2.2/dist/dist.min.js\", \"https://cdn.holoviz.org/panel/1.5.4/dist/bundled/deckglplot/@loaders.gl/3d-tiles@4.2.2/dist/dist.min.js\", \"https://cdn.holoviz.org/panel/1.5.4/dist/bundled/deckglplot/mapbox-gl-js/v3.0.1/mapbox-gl.js\", \"https://cdn.holoviz.org/panel/1.5.4/dist/bundled/deckglplot/maplibre-gl/dist/maplibre-gl.js\", \"https://cdn.holoviz.org/panel/1.5.4/dist/bundled/deckglplot/@deck.gl/carto@^9.0.20/dist.min.js\", \"https://cdn.holoviz.org/panel/1.5.4/dist/bundled/reactiveesm/es-module-shims@^1.10.0/dist/es-module-shims.min.js\"];\n", + " const js_modules = [];\n", + " const js_exports = {};\n", + " const css_urls = [\"https://cdn.holoviz.org/panel/1.5.4/dist/bundled/deckglplot/mapbox-gl-js/v3.0.1/mapbox-gl.css?v=1.5.4\", \"https://cdn.holoviz.org/panel/1.5.4/dist/bundled/deckglplot/maplibre-gl@4.4.1/dist/maplibre-gl.css?v=1.5.4\"];\n", + " const inline_js = [ function(Bokeh) {\n", + " inject_raw_css(\"\\n.dataframe table{\\n border: none;\\n}\\n\\n.panel-df table{\\n width: 100%;\\n border-collapse: collapse;\\n border: none;\\n}\\n.panel-df td{\\n white-space: nowrap;\\n overflow: auto;\\n text-overflow: ellipsis;\\n}\\n\");\n", + " }, function(Bokeh) {\n", + " inject_raw_css(\"\\n.multi-select{\\n color: white;\\n z-index: 100;\\n background: rgba(44,43,43,0.5);\\n border-radius: 1px;\\n width: 120px !important;\\n height: 30px !important;\\n}\\n.multi-select > .bk {\\n padding: 5px;\\n width: 120px !important;\\n height: 30px !important;\\n}\\n\\n.deck-chart {\\n z-index: 10;\\n position: initial !important;\\n}\\n\");\n", + " }, function(Bokeh) {\n", + " inject_raw_css(\"\\n.center-header {\\n text-align: center\\n}\\n.bk-input-group {\\n padding: 10px;\\n}\\n#sidebar {\\n padding-top: 10px;\\n}\\n.custom-widget-box {\\n margin-top: 20px;\\n padding: 5px;\\n border: None !important;\\n}\\n.custom-widget-box > p {\\n margin: 0px;\\n}\\n.bk-input-group {\\n color: None !important;\\n}\\n.indicator {\\n text-align: center;\\n}\\n.widget-card {\\n margin: 5px 10px;\\n}\\n.number-card {\\n margin: 5px 10px;\\n text-align: center;\\n}\\n.number-card-value {\\n width: 100%;\\n margin: 0px;\\n}\\n\");\n", + " }, function(Bokeh) {\n", + " Bokeh.set_log_level(\"info\");\n", + " },\n", + " function(Bokeh) {\n", + " (function(root, factory) {\n", + " factory(root[\"Bokeh\"]);\n", + " })(this, function(Bokeh) {\n", + " let define;\n", + " return (function outer(modules, entry) {\n", + " if (Bokeh != null) {\n", + " return Bokeh.register_plugin(modules, entry);\n", + " } else {\n", + " throw new Error(\"Cannot find Bokeh. You have to load it prior to loading plugins.\");\n", + " }\n", + " })\n", + " ({\n", + " \"custom/main\": function(require, module, exports) {\n", + " const models = {\n", + " \"CustomInspectTool\": require(\"custom/cuxfilter.charts.datashader.custom_extensions.graph_inspect_widget.custom_inspect_tool\").CustomInspectTool\n", + " };\n", + " require(\"base\").register_models(models);\n", + " module.exports = models;\n", + " },\n", + " \"custom/cuxfilter.charts.datashader.custom_extensions.graph_inspect_widget.custom_inspect_tool\": function(require, module, exports) {\n", + " \"use strict\";\n", + " var _a;\n", + " Object.defineProperty(exports, \"__esModule\", { value: true });\n", + " exports.CustomInspectTool = exports.CustomInspectToolView = void 0;\n", + " const inspect_tool_1 = require(\"models/tools/inspectors/inspect_tool\");\n", + " class CustomInspectToolView extends inspect_tool_1.InspectToolView {\n", + " connect_signals() {\n", + " super.connect_signals();\n", + " this.on_change([this.model.properties.active], () => {\n", + " this.model._active = this.model.active;\n", + " });\n", + " }\n", + " }\n", + " exports.CustomInspectToolView = CustomInspectToolView;\n", + " CustomInspectToolView.__name__ = \"CustomInspectToolView\";\n", + " class CustomInspectTool extends inspect_tool_1.InspectTool {\n", + " constructor(attrs) {\n", + " super(attrs);\n", + " }\n", + " }\n", + " exports.CustomInspectTool = CustomInspectTool;\n", + " _a = CustomInspectTool;\n", + " CustomInspectTool.__name__ = \"CustomInspectTool\";\n", + " (() => {\n", + " _a.prototype.default_view = CustomInspectToolView;\n", + " _a.define(({ Boolean }) => ({\n", + " _active: [Boolean, true]\n", + " }));\n", + " _a.register_alias(\"customInspect\", () => new _a());\n", + " })();\n", + " //# sourceMappingURL=graph_inspect_widget.py:CustomInspectTool.js.map\n", + " }\n", + " }, \"custom/main\");\n", + " ;\n", + " });\n", + "\n", + " },\n", + "function(Bokeh) {} // ensure no trailing comma for IE\n", + " ];\n", + "\n", + " function run_inline_js() {\n", + " if ((root.Bokeh !== undefined) || (force === true)) {\n", + " for (let i = 0; i < inline_js.length; i++) {\n", + " try {\n", + " inline_js[i].call(root, root.Bokeh);\n", + " } catch(e) {\n", + " if (!reloading) {\n", + " throw e;\n", + " }\n", + " }\n", + " }\n", + " // Cache old bokeh versions\n", + " if (Bokeh != undefined && !reloading) {\n", + " var NewBokeh = root.Bokeh;\n", + " if (Bokeh.versions === undefined) {\n", + " Bokeh.versions = new Map();\n", + " }\n", + " if (NewBokeh.version !== Bokeh.version) {\n", + " Bokeh.versions.set(NewBokeh.version, NewBokeh)\n", + " }\n", + " root.Bokeh = Bokeh;\n", + " }\n", + " } else if (Date.now() < root._bokeh_timeout) {\n", + " setTimeout(run_inline_js, 100);\n", + " } else if (!root._bokeh_failed_load) {\n", + " console.log(\"Bokeh: BokehJS failed to load within specified timeout.\");\n", + " root._bokeh_failed_load = true;\n", + " }\n", + " root._bokeh_is_initializing = false\n", + " }\n", + "\n", + " function load_or_wait() {\n", + " // Implement a backoff loop that tries to ensure we do not load multiple\n", + " // versions of Bokeh and its dependencies at the same time.\n", + " // In recent versions we use the root._bokeh_is_initializing flag\n", + " // to determine whether there is an ongoing attempt to initialize\n", + " // bokeh, however for backward compatibility we also try to ensure\n", + " // that we do not start loading a newer (Panel>=1.0 and Bokeh>3) version\n", + " // before older versions are fully initialized.\n", + " if (root._bokeh_is_initializing && Date.now() > root._bokeh_timeout) {\n", + " // If the timeout and bokeh was not successfully loaded we reset\n", + " // everything and try loading again\n", + " root._bokeh_timeout = Date.now() + 5000;\n", + " root._bokeh_is_initializing = false;\n", + " root._bokeh_onload_callbacks = undefined;\n", + " root._bokeh_is_loading = 0\n", + " console.log(\"Bokeh: BokehJS was loaded multiple times but one version failed to initialize.\");\n", + " load_or_wait();\n", + " } else if (root._bokeh_is_initializing || (typeof root._bokeh_is_initializing === \"undefined\" && root._bokeh_onload_callbacks !== undefined)) {\n", + " setTimeout(load_or_wait, 100);\n", + " } else {\n", + " root._bokeh_is_initializing = true\n", + " root._bokeh_onload_callbacks = []\n", + " const bokeh_loaded = root.Bokeh != null && (root.Bokeh.version === py_version || (root.Bokeh.versions !== undefined && root.Bokeh.versions.has(py_version)));\n", + " if (!reloading && !bokeh_loaded) {\n", + " if (root.Bokeh) {\n", + " root.Bokeh = undefined;\n", + " }\n", + " console.debug(\"Bokeh: BokehJS not loaded, scheduling load and callback at\", now());\n", + " }\n", + " load_libs(css_urls, js_urls, js_modules, js_exports, function() {\n", + " console.debug(\"Bokeh: BokehJS plotting callback run at\", now());\n", + " run_inline_js();\n", + " });\n", + " }\n", + " }\n", + " // Give older versions of the autoload script a head-start to ensure\n", + " // they initialize before we start loading newer version.\n", + " setTimeout(load_or_wait, 100)\n", + "}(window));" + ], + "application/vnd.holoviews_load.v0+json": "(function(root) {\n function now() {\n return new Date();\n }\n\n const force = false;\n const py_version = '3.6.2'.replace('rc', '-rc.').replace('.dev', '-dev.');\n const reloading = true;\n const Bokeh = root.Bokeh;\n\n // Set a timeout for this load but only if we are not already initializing\n if (typeof (root._bokeh_timeout) === \"undefined\" || (force || !root._bokeh_is_initializing)) {\n root._bokeh_timeout = Date.now() + 5000;\n root._bokeh_failed_load = false;\n }\n\n function run_callbacks() {\n try {\n root._bokeh_onload_callbacks.forEach(function(callback) {\n if (callback != null)\n callback();\n });\n } finally {\n delete root._bokeh_onload_callbacks;\n }\n console.debug(\"Bokeh: all callbacks have finished\");\n }\n\n function load_libs(css_urls, js_urls, js_modules, js_exports, callback) {\n if (css_urls == null) css_urls = [];\n if (js_urls == null) js_urls = [];\n if (js_modules == null) js_modules = [];\n if (js_exports == null) js_exports = {};\n\n root._bokeh_onload_callbacks.push(callback);\n\n if (root._bokeh_is_loading > 0) {\n // Don't load bokeh if it is still initializing\n console.debug(\"Bokeh: BokehJS is being loaded, scheduling callback at\", now());\n return null;\n } else if (js_urls.length === 0 && js_modules.length === 0 && Object.keys(js_exports).length === 0) {\n // There is nothing to load\n run_callbacks();\n return null;\n }\n\n function on_load() {\n root._bokeh_is_loading--;\n if (root._bokeh_is_loading === 0) {\n console.debug(\"Bokeh: all BokehJS libraries/stylesheets loaded\");\n run_callbacks()\n }\n }\n window._bokeh_on_load = on_load\n\n function on_error(e) {\n const src_el = e.srcElement\n console.error(\"failed to load \" + (src_el.href || src_el.src));\n }\n\n const skip = [];\n if (window.requirejs) {\n window.requirejs.config({'packages': {}, 'paths': {'h3': 'https://cdn.jsdelivr.net/npm/h3-js@4.1.0/dist/h3-js.umd', 'deck-gl': 'https://cdn.jsdelivr.net/npm/deck.gl@9.0.20/dist.min', 'deck-json': 'https://cdn.jsdelivr.net/npm/@deck.gl/json@9.0.20/dist.min', 'loader-csv': 'https://cdn.jsdelivr.net/npm/@loaders.gl/csv@4.2.2/dist/dist.min', 'loader-json': 'https://cdn.jsdelivr.net/npm/@loaders.gl/json@4.2.2/dist/dist.min', 'loader-tiles': 'https://cdn.jsdelivr.net/npm/@loaders.gl/3d-tiles@4.2.2/dist/dist.min', 'mapbox-gl': 'https://api.mapbox.com/mapbox-gl-js/v3.0.1/mapbox-gl', 'carto': 'https://cdn.jsdelivr.net/npm/@deck.gl/carto@^9.0.20/dist.min'}, 'shim': {'deck-json': {'deps': ['deck-gl']}, 'deck-gl': {'deps': ['h3']}}});\n require([\"h3\"], function(h3) {\n window.h3 = h3\n on_load()\n })\n require([\"deck-gl\"], function(deck) {\n window.deck = deck\n on_load()\n })\n require([\"deck-json\"], function() {\n on_load()\n })\n require([\"loader-csv\"], function() {\n on_load()\n })\n require([\"loader-json\"], function() {\n on_load()\n })\n require([\"loader-tiles\"], function() {\n on_load()\n })\n require([\"mapbox-gl\"], function(mapboxgl) {\n window.mapboxgl = mapboxgl\n on_load()\n })\n require([\"carto\"], function() {\n on_load()\n })\n root._bokeh_is_loading = css_urls.length + 8;\n } else {\n root._bokeh_is_loading = css_urls.length + js_urls.length + js_modules.length + Object.keys(js_exports).length;\n }\n\n const existing_stylesheets = []\n const links = document.getElementsByTagName('link')\n for (let i = 0; i < links.length; i++) {\n const link = links[i]\n if (link.href != null) {\n existing_stylesheets.push(link.href)\n }\n }\n for (let i = 0; i < css_urls.length; i++) {\n const url = css_urls[i];\n const escaped = encodeURI(url)\n if (existing_stylesheets.indexOf(escaped) !== -1) {\n on_load()\n continue;\n }\n const element = document.createElement(\"link\");\n element.onload = on_load;\n element.onerror = on_error;\n element.rel = \"stylesheet\";\n element.type = \"text/css\";\n element.href = url;\n console.debug(\"Bokeh: injecting link tag for BokehJS stylesheet: \", url);\n document.body.appendChild(element);\n } if (((window.deck !== undefined) && (!(window.deck instanceof HTMLElement))) || window.requirejs) {\n var urls = ['https://cdn.holoviz.org/panel/1.5.4/dist/bundled/deckglplot/h3-js@4.1.0/dist/h3-js.umd.js', 'https://cdn.holoviz.org/panel/1.5.4/dist/bundled/deckglplot/deck.gl@9.0.20/dist.min.js', 'https://cdn.holoviz.org/panel/1.5.4/dist/bundled/deckglplot/@deck.gl/json@9.0.20/dist.min.js', 'https://cdn.holoviz.org/panel/1.5.4/dist/bundled/deckglplot/@loaders.gl/csv@4.2.2/dist/dist.min.js', 'https://cdn.holoviz.org/panel/1.5.4/dist/bundled/deckglplot/@loaders.gl/json@4.2.2/dist/dist.min.js', 'https://cdn.holoviz.org/panel/1.5.4/dist/bundled/deckglplot/@loaders.gl/3d-tiles@4.2.2/dist/dist.min.js', 'https://cdn.holoviz.org/panel/1.5.4/dist/bundled/deckglplot/mapbox-gl-js/v3.0.1/mapbox-gl.js', 'https://cdn.holoviz.org/panel/1.5.4/dist/bundled/deckglplot/maplibre-gl/dist/maplibre-gl.js'];\n for (var i = 0; i < urls.length; i++) {\n skip.push(encodeURI(urls[i]))\n }\n } if (((window.mapboxgl !== undefined) && (!(window.mapboxgl instanceof HTMLElement))) || window.requirejs) {\n var urls = ['https://cdn.holoviz.org/panel/1.5.4/dist/bundled/deckglplot/@deck.gl/carto@^9.0.20/dist.min.js'];\n for (var i = 0; i < urls.length; i++) {\n skip.push(encodeURI(urls[i]))\n }\n } var existing_scripts = []\n const scripts = document.getElementsByTagName('script')\n for (let i = 0; i < scripts.length; i++) {\n var script = scripts[i]\n if (script.src != null) {\n existing_scripts.push(script.src)\n }\n }\n for (let i = 0; i < js_urls.length; i++) {\n const url = js_urls[i];\n const escaped = encodeURI(url)\n if (skip.indexOf(escaped) !== -1 || existing_scripts.indexOf(escaped) !== -1) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\n }\n const element = document.createElement('script');\n element.onload = on_load;\n element.onerror = on_error;\n element.async = false;\n element.src = url;\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n document.head.appendChild(element);\n }\n for (let i = 0; i < js_modules.length; i++) {\n const url = js_modules[i];\n const escaped = encodeURI(url)\n if (skip.indexOf(escaped) !== -1 || existing_scripts.indexOf(escaped) !== -1) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\n }\n var element = document.createElement('script');\n element.onload = on_load;\n element.onerror = on_error;\n element.async = false;\n element.src = url;\n element.type = \"module\";\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n document.head.appendChild(element);\n }\n for (const name in js_exports) {\n const url = js_exports[name];\n const escaped = encodeURI(url)\n if (skip.indexOf(escaped) >= 0 || root[name] != null) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\n }\n var element = document.createElement('script');\n element.onerror = on_error;\n element.async = false;\n element.type = \"module\";\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n element.textContent = `\n import ${name} from \"${url}\"\n window.${name} = ${name}\n window._bokeh_on_load()\n `\n document.head.appendChild(element);\n }\n if (!js_urls.length && !js_modules.length) {\n on_load()\n }\n };\n\n function inject_raw_css(css) {\n const element = document.createElement(\"style\");\n element.appendChild(document.createTextNode(css));\n document.body.appendChild(element);\n }\n\n const js_urls = [\"https://cdn.holoviz.org/panel/1.5.4/dist/bundled/deckglplot/h3-js@4.1.0/dist/h3-js.umd.js\", \"https://cdn.holoviz.org/panel/1.5.4/dist/bundled/deckglplot/deck.gl@9.0.20/dist.min.js\", \"https://cdn.holoviz.org/panel/1.5.4/dist/bundled/deckglplot/@deck.gl/json@9.0.20/dist.min.js\", \"https://cdn.holoviz.org/panel/1.5.4/dist/bundled/deckglplot/@loaders.gl/csv@4.2.2/dist/dist.min.js\", \"https://cdn.holoviz.org/panel/1.5.4/dist/bundled/deckglplot/@loaders.gl/json@4.2.2/dist/dist.min.js\", \"https://cdn.holoviz.org/panel/1.5.4/dist/bundled/deckglplot/@loaders.gl/3d-tiles@4.2.2/dist/dist.min.js\", \"https://cdn.holoviz.org/panel/1.5.4/dist/bundled/deckglplot/mapbox-gl-js/v3.0.1/mapbox-gl.js\", \"https://cdn.holoviz.org/panel/1.5.4/dist/bundled/deckglplot/maplibre-gl/dist/maplibre-gl.js\", \"https://cdn.holoviz.org/panel/1.5.4/dist/bundled/deckglplot/@deck.gl/carto@^9.0.20/dist.min.js\", \"https://cdn.holoviz.org/panel/1.5.4/dist/bundled/reactiveesm/es-module-shims@^1.10.0/dist/es-module-shims.min.js\"];\n const js_modules = [];\n const js_exports = {};\n const css_urls = [\"https://cdn.holoviz.org/panel/1.5.4/dist/bundled/deckglplot/mapbox-gl-js/v3.0.1/mapbox-gl.css?v=1.5.4\", \"https://cdn.holoviz.org/panel/1.5.4/dist/bundled/deckglplot/maplibre-gl@4.4.1/dist/maplibre-gl.css?v=1.5.4\"];\n const inline_js = [ function(Bokeh) {\n inject_raw_css(\"\\n.dataframe table{\\n border: none;\\n}\\n\\n.panel-df table{\\n width: 100%;\\n border-collapse: collapse;\\n border: none;\\n}\\n.panel-df td{\\n white-space: nowrap;\\n overflow: auto;\\n text-overflow: ellipsis;\\n}\\n\");\n }, function(Bokeh) {\n inject_raw_css(\"\\n.multi-select{\\n color: white;\\n z-index: 100;\\n background: rgba(44,43,43,0.5);\\n border-radius: 1px;\\n width: 120px !important;\\n height: 30px !important;\\n}\\n.multi-select > .bk {\\n padding: 5px;\\n width: 120px !important;\\n height: 30px !important;\\n}\\n\\n.deck-chart {\\n z-index: 10;\\n position: initial !important;\\n}\\n\");\n }, function(Bokeh) {\n inject_raw_css(\"\\n.center-header {\\n text-align: center\\n}\\n.bk-input-group {\\n padding: 10px;\\n}\\n#sidebar {\\n padding-top: 10px;\\n}\\n.custom-widget-box {\\n margin-top: 20px;\\n padding: 5px;\\n border: None !important;\\n}\\n.custom-widget-box > p {\\n margin: 0px;\\n}\\n.bk-input-group {\\n color: None !important;\\n}\\n.indicator {\\n text-align: center;\\n}\\n.widget-card {\\n margin: 5px 10px;\\n}\\n.number-card {\\n margin: 5px 10px;\\n text-align: center;\\n}\\n.number-card-value {\\n width: 100%;\\n margin: 0px;\\n}\\n\");\n }, function(Bokeh) {\n Bokeh.set_log_level(\"info\");\n },\n function(Bokeh) {\n (function(root, factory) {\n factory(root[\"Bokeh\"]);\n })(this, function(Bokeh) {\n let define;\n return (function outer(modules, entry) {\n if (Bokeh != null) {\n return Bokeh.register_plugin(modules, entry);\n } else {\n throw new Error(\"Cannot find Bokeh. You have to load it prior to loading plugins.\");\n }\n })\n ({\n \"custom/main\": function(require, module, exports) {\n const models = {\n \"CustomInspectTool\": require(\"custom/cuxfilter.charts.datashader.custom_extensions.graph_inspect_widget.custom_inspect_tool\").CustomInspectTool\n };\n require(\"base\").register_models(models);\n module.exports = models;\n },\n \"custom/cuxfilter.charts.datashader.custom_extensions.graph_inspect_widget.custom_inspect_tool\": function(require, module, exports) {\n \"use strict\";\n var _a;\n Object.defineProperty(exports, \"__esModule\", { value: true });\n exports.CustomInspectTool = exports.CustomInspectToolView = void 0;\n const inspect_tool_1 = require(\"models/tools/inspectors/inspect_tool\");\n class CustomInspectToolView extends inspect_tool_1.InspectToolView {\n connect_signals() {\n super.connect_signals();\n this.on_change([this.model.properties.active], () => {\n this.model._active = this.model.active;\n });\n }\n }\n exports.CustomInspectToolView = CustomInspectToolView;\n CustomInspectToolView.__name__ = \"CustomInspectToolView\";\n class CustomInspectTool extends inspect_tool_1.InspectTool {\n constructor(attrs) {\n super(attrs);\n }\n }\n exports.CustomInspectTool = CustomInspectTool;\n _a = CustomInspectTool;\n CustomInspectTool.__name__ = \"CustomInspectTool\";\n (() => {\n _a.prototype.default_view = CustomInspectToolView;\n _a.define(({ Boolean }) => ({\n _active: [Boolean, true]\n }));\n _a.register_alias(\"customInspect\", () => new _a());\n })();\n //# sourceMappingURL=graph_inspect_widget.py:CustomInspectTool.js.map\n }\n }, \"custom/main\");\n ;\n });\n\n },\nfunction(Bokeh) {} // ensure no trailing comma for IE\n ];\n\n function run_inline_js() {\n if ((root.Bokeh !== undefined) || (force === true)) {\n for (let i = 0; i < inline_js.length; i++) {\n try {\n inline_js[i].call(root, root.Bokeh);\n } catch(e) {\n if (!reloading) {\n throw e;\n }\n }\n }\n // Cache old bokeh versions\n if (Bokeh != undefined && !reloading) {\n var NewBokeh = root.Bokeh;\n if (Bokeh.versions === undefined) {\n Bokeh.versions = new Map();\n }\n if (NewBokeh.version !== Bokeh.version) {\n Bokeh.versions.set(NewBokeh.version, NewBokeh)\n }\n root.Bokeh = Bokeh;\n }\n } else if (Date.now() < root._bokeh_timeout) {\n setTimeout(run_inline_js, 100);\n } else if (!root._bokeh_failed_load) {\n console.log(\"Bokeh: BokehJS failed to load within specified timeout.\");\n root._bokeh_failed_load = true;\n }\n root._bokeh_is_initializing = false\n }\n\n function load_or_wait() {\n // Implement a backoff loop that tries to ensure we do not load multiple\n // versions of Bokeh and its dependencies at the same time.\n // In recent versions we use the root._bokeh_is_initializing flag\n // to determine whether there is an ongoing attempt to initialize\n // bokeh, however for backward compatibility we also try to ensure\n // that we do not start loading a newer (Panel>=1.0 and Bokeh>3) version\n // before older versions are fully initialized.\n if (root._bokeh_is_initializing && Date.now() > root._bokeh_timeout) {\n // If the timeout and bokeh was not successfully loaded we reset\n // everything and try loading again\n root._bokeh_timeout = Date.now() + 5000;\n root._bokeh_is_initializing = false;\n root._bokeh_onload_callbacks = undefined;\n root._bokeh_is_loading = 0\n console.log(\"Bokeh: BokehJS was loaded multiple times but one version failed to initialize.\");\n load_or_wait();\n } else if (root._bokeh_is_initializing || (typeof root._bokeh_is_initializing === \"undefined\" && root._bokeh_onload_callbacks !== undefined)) {\n setTimeout(load_or_wait, 100);\n } else {\n root._bokeh_is_initializing = true\n root._bokeh_onload_callbacks = []\n const bokeh_loaded = root.Bokeh != null && (root.Bokeh.version === py_version || (root.Bokeh.versions !== undefined && root.Bokeh.versions.has(py_version)));\n if (!reloading && !bokeh_loaded) {\n if (root.Bokeh) {\n root.Bokeh = undefined;\n }\n console.debug(\"Bokeh: BokehJS not loaded, scheduling load and callback at\", now());\n }\n load_libs(css_urls, js_urls, js_modules, js_exports, function() {\n console.debug(\"Bokeh: BokehJS plotting callback run at\", now());\n run_inline_js();\n });\n }\n }\n // Give older versions of the autoload script a head-start to ensure\n // they initialize before we start loading newer version.\n setTimeout(load_or_wait, 100)\n}(window));" + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/javascript": [ + "\n", + "if ((window.PyViz === undefined) || (window.PyViz instanceof HTMLElement)) {\n", + " window.PyViz = {comms: {}, comm_status:{}, kernels:{}, receivers: {}, plot_index: []}\n", + "}\n", + "\n", + "\n", + " function JupyterCommManager() {\n", + " }\n", + "\n", + " JupyterCommManager.prototype.register_target = function(plot_id, comm_id, msg_handler) {\n", + " if (window.comm_manager || ((window.Jupyter !== undefined) && (Jupyter.notebook.kernel != null))) {\n", + " var comm_manager = window.comm_manager || Jupyter.notebook.kernel.comm_manager;\n", + " comm_manager.register_target(comm_id, function(comm) {\n", + " comm.on_msg(msg_handler);\n", + " });\n", + " } else if ((plot_id in window.PyViz.kernels) && (window.PyViz.kernels[plot_id])) {\n", + " window.PyViz.kernels[plot_id].registerCommTarget(comm_id, function(comm) {\n", + " comm.onMsg = msg_handler;\n", + " });\n", + " } else if (typeof google != 'undefined' && google.colab.kernel != null) {\n", + " google.colab.kernel.comms.registerTarget(comm_id, (comm) => {\n", + " var messages = comm.messages[Symbol.asyncIterator]();\n", + " function processIteratorResult(result) {\n", + " var message = result.value;\n", + " console.log(message)\n", + " var content = {data: message.data, comm_id};\n", + " var buffers = []\n", + " for (var buffer of message.buffers || []) {\n", + " buffers.push(new DataView(buffer))\n", + " }\n", + " var metadata = message.metadata || {};\n", + " var msg = {content, buffers, metadata}\n", + " msg_handler(msg);\n", + " return messages.next().then(processIteratorResult);\n", + " }\n", + " return messages.next().then(processIteratorResult);\n", + " })\n", + " }\n", + " }\n", + "\n", + " JupyterCommManager.prototype.get_client_comm = function(plot_id, comm_id, msg_handler) {\n", + " if (comm_id in window.PyViz.comms) {\n", + " return window.PyViz.comms[comm_id];\n", + " } else if (window.comm_manager || ((window.Jupyter !== undefined) && (Jupyter.notebook.kernel != null))) {\n", + " var comm_manager = window.comm_manager || Jupyter.notebook.kernel.comm_manager;\n", + " var comm = comm_manager.new_comm(comm_id, {}, {}, {}, comm_id);\n", + " if (msg_handler) {\n", + " comm.on_msg(msg_handler);\n", + " }\n", + " } else if ((plot_id in window.PyViz.kernels) && (window.PyViz.kernels[plot_id])) {\n", + " var comm = window.PyViz.kernels[plot_id].connectToComm(comm_id);\n", + " comm.open();\n", + " if (msg_handler) {\n", + " comm.onMsg = msg_handler;\n", + " }\n", + " } else if (typeof google != 'undefined' && google.colab.kernel != null) {\n", + " var comm_promise = google.colab.kernel.comms.open(comm_id)\n", + " comm_promise.then((comm) => {\n", + " window.PyViz.comms[comm_id] = comm;\n", + " if (msg_handler) {\n", + " var messages = comm.messages[Symbol.asyncIterator]();\n", + " function processIteratorResult(result) {\n", + " var message = result.value;\n", + " var content = {data: message.data};\n", + " var metadata = message.metadata || {comm_id};\n", + " var msg = {content, metadata}\n", + " msg_handler(msg);\n", + " return messages.next().then(processIteratorResult);\n", + " }\n", + " return messages.next().then(processIteratorResult);\n", + " }\n", + " }) \n", + " var sendClosure = (data, metadata, buffers, disposeOnDone) => {\n", + " return comm_promise.then((comm) => {\n", + " comm.send(data, metadata, buffers, disposeOnDone);\n", + " });\n", + " };\n", + " var comm = {\n", + " send: sendClosure\n", + " };\n", + " }\n", + " window.PyViz.comms[comm_id] = comm;\n", + " return comm;\n", + " }\n", + " window.PyViz.comm_manager = new JupyterCommManager();\n", + " \n", + "\n", + "\n", + "var JS_MIME_TYPE = 'application/javascript';\n", + "var HTML_MIME_TYPE = 'text/html';\n", + "var EXEC_MIME_TYPE = 'application/vnd.holoviews_exec.v0+json';\n", + "var CLASS_NAME = 'output';\n", + "\n", + "/**\n", + " * Render data to the DOM node\n", + " */\n", + "function render(props, node) {\n", + " var div = document.createElement(\"div\");\n", + " var script = document.createElement(\"script\");\n", + " node.appendChild(div);\n", + " node.appendChild(script);\n", + "}\n", + "\n", + "/**\n", + " * Handle when a new output is added\n", + " */\n", + "function handle_add_output(event, handle) {\n", + " var output_area = handle.output_area;\n", + " var output = handle.output;\n", + " if ((output.data == undefined) || (!output.data.hasOwnProperty(EXEC_MIME_TYPE))) {\n", + " return\n", + " }\n", + " var id = output.metadata[EXEC_MIME_TYPE][\"id\"];\n", + " var toinsert = output_area.element.find(\".\" + CLASS_NAME.split(' ')[0]);\n", + " if (id !== undefined) {\n", + " var nchildren = toinsert.length;\n", + " var html_node = toinsert[nchildren-1].children[0];\n", + " html_node.innerHTML = output.data[HTML_MIME_TYPE];\n", + " var scripts = [];\n", + " var nodelist = html_node.querySelectorAll(\"script\");\n", + " for (var i in nodelist) {\n", + " if (nodelist.hasOwnProperty(i)) {\n", + " scripts.push(nodelist[i])\n", + " }\n", + " }\n", + "\n", + " scripts.forEach( function (oldScript) {\n", + " var newScript = document.createElement(\"script\");\n", + " var attrs = [];\n", + " var nodemap = oldScript.attributes;\n", + " for (var j in nodemap) {\n", + " if (nodemap.hasOwnProperty(j)) {\n", + " attrs.push(nodemap[j])\n", + " }\n", + " }\n", + " attrs.forEach(function(attr) { newScript.setAttribute(attr.name, attr.value) });\n", + " newScript.appendChild(document.createTextNode(oldScript.innerHTML));\n", + " oldScript.parentNode.replaceChild(newScript, oldScript);\n", + " });\n", + " if (JS_MIME_TYPE in output.data) {\n", + " toinsert[nchildren-1].children[1].textContent = output.data[JS_MIME_TYPE];\n", + " }\n", + " output_area._hv_plot_id = id;\n", + " if ((window.Bokeh !== undefined) && (id in Bokeh.index)) {\n", + " window.PyViz.plot_index[id] = Bokeh.index[id];\n", + " } else {\n", + " window.PyViz.plot_index[id] = null;\n", + " }\n", + " } else if (output.metadata[EXEC_MIME_TYPE][\"server_id\"] !== undefined) {\n", + " var bk_div = document.createElement(\"div\");\n", + " bk_div.innerHTML = output.data[HTML_MIME_TYPE];\n", + " var script_attrs = bk_div.children[0].attributes;\n", + " for (var i = 0; i < script_attrs.length; i++) {\n", + " toinsert[toinsert.length - 1].childNodes[1].setAttribute(script_attrs[i].name, script_attrs[i].value);\n", + " }\n", + " // store reference to server id on output_area\n", + " output_area._bokeh_server_id = output.metadata[EXEC_MIME_TYPE][\"server_id\"];\n", + " }\n", + "}\n", + "\n", + "/**\n", + " * Handle when an output is cleared or removed\n", + " */\n", + "function handle_clear_output(event, handle) {\n", + " var id = handle.cell.output_area._hv_plot_id;\n", + " var server_id = handle.cell.output_area._bokeh_server_id;\n", + " if (((id === undefined) || !(id in PyViz.plot_index)) && (server_id !== undefined)) { return; }\n", + " var comm = window.PyViz.comm_manager.get_client_comm(\"hv-extension-comm\", \"hv-extension-comm\", function () {});\n", + " if (server_id !== null) {\n", + " comm.send({event_type: 'server_delete', 'id': server_id});\n", + " return;\n", + " } else if (comm !== null) {\n", + " comm.send({event_type: 'delete', 'id': id});\n", + " }\n", + " delete PyViz.plot_index[id];\n", + " if ((window.Bokeh !== undefined) & (id in window.Bokeh.index)) {\n", + " var doc = window.Bokeh.index[id].model.document\n", + " doc.clear();\n", + " const i = window.Bokeh.documents.indexOf(doc);\n", + " if (i > -1) {\n", + " window.Bokeh.documents.splice(i, 1);\n", + " }\n", + " }\n", + "}\n", + "\n", + "/**\n", + " * Handle kernel restart event\n", + " */\n", + "function handle_kernel_cleanup(event, handle) {\n", + " delete PyViz.comms[\"hv-extension-comm\"];\n", + " window.PyViz.plot_index = {}\n", + "}\n", + "\n", + "/**\n", + " * Handle update_display_data messages\n", + " */\n", + "function handle_update_output(event, handle) {\n", + " handle_clear_output(event, {cell: {output_area: handle.output_area}})\n", + " handle_add_output(event, handle)\n", + "}\n", + "\n", + "function register_renderer(events, OutputArea) {\n", + " function append_mime(data, metadata, element) {\n", + " // create a DOM node to render to\n", + " var toinsert = this.create_output_subarea(\n", + " metadata,\n", + " CLASS_NAME,\n", + " EXEC_MIME_TYPE\n", + " );\n", + " this.keyboard_manager.register_events(toinsert);\n", + " // Render to node\n", + " var props = {data: data, metadata: metadata[EXEC_MIME_TYPE]};\n", + " render(props, toinsert[0]);\n", + " element.append(toinsert);\n", + " return toinsert\n", + " }\n", + "\n", + " events.on('output_added.OutputArea', handle_add_output);\n", + " events.on('output_updated.OutputArea', handle_update_output);\n", + " events.on('clear_output.CodeCell', handle_clear_output);\n", + " events.on('delete.Cell', handle_clear_output);\n", + " events.on('kernel_ready.Kernel', handle_kernel_cleanup);\n", + "\n", + " OutputArea.prototype.register_mime_type(EXEC_MIME_TYPE, append_mime, {\n", + " safe: true,\n", + " index: 0\n", + " });\n", + "}\n", + "\n", + "if (window.Jupyter !== undefined) {\n", + " try {\n", + " var events = require('base/js/events');\n", + " var OutputArea = require('notebook/js/outputarea').OutputArea;\n", + " if (OutputArea.prototype.mime_types().indexOf(EXEC_MIME_TYPE) == -1) {\n", + " register_renderer(events, OutputArea);\n", + " }\n", + " } catch(err) {\n", + " }\n", + "}\n" + ], + "application/vnd.holoviews_load.v0+json": "\nif ((window.PyViz === undefined) || (window.PyViz instanceof HTMLElement)) {\n window.PyViz = {comms: {}, comm_status:{}, kernels:{}, receivers: {}, plot_index: []}\n}\n\n\n function JupyterCommManager() {\n }\n\n JupyterCommManager.prototype.register_target = function(plot_id, comm_id, msg_handler) {\n if (window.comm_manager || ((window.Jupyter !== undefined) && (Jupyter.notebook.kernel != null))) {\n var comm_manager = window.comm_manager || Jupyter.notebook.kernel.comm_manager;\n comm_manager.register_target(comm_id, function(comm) {\n comm.on_msg(msg_handler);\n });\n } else if ((plot_id in window.PyViz.kernels) && (window.PyViz.kernels[plot_id])) {\n window.PyViz.kernels[plot_id].registerCommTarget(comm_id, function(comm) {\n comm.onMsg = msg_handler;\n });\n } else if (typeof google != 'undefined' && google.colab.kernel != null) {\n google.colab.kernel.comms.registerTarget(comm_id, (comm) => {\n var messages = comm.messages[Symbol.asyncIterator]();\n function processIteratorResult(result) {\n var message = result.value;\n console.log(message)\n var content = {data: message.data, comm_id};\n var buffers = []\n for (var buffer of message.buffers || []) {\n buffers.push(new DataView(buffer))\n }\n var metadata = message.metadata || {};\n var msg = {content, buffers, metadata}\n msg_handler(msg);\n return messages.next().then(processIteratorResult);\n }\n return messages.next().then(processIteratorResult);\n })\n }\n }\n\n JupyterCommManager.prototype.get_client_comm = function(plot_id, comm_id, msg_handler) {\n if (comm_id in window.PyViz.comms) {\n return window.PyViz.comms[comm_id];\n } else if (window.comm_manager || ((window.Jupyter !== undefined) && (Jupyter.notebook.kernel != null))) {\n var comm_manager = window.comm_manager || Jupyter.notebook.kernel.comm_manager;\n var comm = comm_manager.new_comm(comm_id, {}, {}, {}, comm_id);\n if (msg_handler) {\n comm.on_msg(msg_handler);\n }\n } else if ((plot_id in window.PyViz.kernels) && (window.PyViz.kernels[plot_id])) {\n var comm = window.PyViz.kernels[plot_id].connectToComm(comm_id);\n comm.open();\n if (msg_handler) {\n comm.onMsg = msg_handler;\n }\n } else if (typeof google != 'undefined' && google.colab.kernel != null) {\n var comm_promise = google.colab.kernel.comms.open(comm_id)\n comm_promise.then((comm) => {\n window.PyViz.comms[comm_id] = comm;\n if (msg_handler) {\n var messages = comm.messages[Symbol.asyncIterator]();\n function processIteratorResult(result) {\n var message = result.value;\n var content = {data: message.data};\n var metadata = message.metadata || {comm_id};\n var msg = {content, metadata}\n msg_handler(msg);\n return messages.next().then(processIteratorResult);\n }\n return messages.next().then(processIteratorResult);\n }\n }) \n var sendClosure = (data, metadata, buffers, disposeOnDone) => {\n return comm_promise.then((comm) => {\n comm.send(data, metadata, buffers, disposeOnDone);\n });\n };\n var comm = {\n send: sendClosure\n };\n }\n window.PyViz.comms[comm_id] = comm;\n return comm;\n }\n window.PyViz.comm_manager = new JupyterCommManager();\n \n\n\nvar JS_MIME_TYPE = 'application/javascript';\nvar HTML_MIME_TYPE = 'text/html';\nvar EXEC_MIME_TYPE = 'application/vnd.holoviews_exec.v0+json';\nvar CLASS_NAME = 'output';\n\n/**\n * Render data to the DOM node\n */\nfunction render(props, node) {\n var div = document.createElement(\"div\");\n var script = document.createElement(\"script\");\n node.appendChild(div);\n node.appendChild(script);\n}\n\n/**\n * Handle when a new output is added\n */\nfunction handle_add_output(event, handle) {\n var output_area = handle.output_area;\n var output = handle.output;\n if ((output.data == undefined) || (!output.data.hasOwnProperty(EXEC_MIME_TYPE))) {\n return\n }\n var id = output.metadata[EXEC_MIME_TYPE][\"id\"];\n var toinsert = output_area.element.find(\".\" + CLASS_NAME.split(' ')[0]);\n if (id !== undefined) {\n var nchildren = toinsert.length;\n var html_node = toinsert[nchildren-1].children[0];\n html_node.innerHTML = output.data[HTML_MIME_TYPE];\n var scripts = [];\n var nodelist = html_node.querySelectorAll(\"script\");\n for (var i in nodelist) {\n if (nodelist.hasOwnProperty(i)) {\n scripts.push(nodelist[i])\n }\n }\n\n scripts.forEach( function (oldScript) {\n var newScript = document.createElement(\"script\");\n var attrs = [];\n var nodemap = oldScript.attributes;\n for (var j in nodemap) {\n if (nodemap.hasOwnProperty(j)) {\n attrs.push(nodemap[j])\n }\n }\n attrs.forEach(function(attr) { newScript.setAttribute(attr.name, attr.value) });\n newScript.appendChild(document.createTextNode(oldScript.innerHTML));\n oldScript.parentNode.replaceChild(newScript, oldScript);\n });\n if (JS_MIME_TYPE in output.data) {\n toinsert[nchildren-1].children[1].textContent = output.data[JS_MIME_TYPE];\n }\n output_area._hv_plot_id = id;\n if ((window.Bokeh !== undefined) && (id in Bokeh.index)) {\n window.PyViz.plot_index[id] = Bokeh.index[id];\n } else {\n window.PyViz.plot_index[id] = null;\n }\n } else if (output.metadata[EXEC_MIME_TYPE][\"server_id\"] !== undefined) {\n var bk_div = document.createElement(\"div\");\n bk_div.innerHTML = output.data[HTML_MIME_TYPE];\n var script_attrs = bk_div.children[0].attributes;\n for (var i = 0; i < script_attrs.length; i++) {\n toinsert[toinsert.length - 1].childNodes[1].setAttribute(script_attrs[i].name, script_attrs[i].value);\n }\n // store reference to server id on output_area\n output_area._bokeh_server_id = output.metadata[EXEC_MIME_TYPE][\"server_id\"];\n }\n}\n\n/**\n * Handle when an output is cleared or removed\n */\nfunction handle_clear_output(event, handle) {\n var id = handle.cell.output_area._hv_plot_id;\n var server_id = handle.cell.output_area._bokeh_server_id;\n if (((id === undefined) || !(id in PyViz.plot_index)) && (server_id !== undefined)) { return; }\n var comm = window.PyViz.comm_manager.get_client_comm(\"hv-extension-comm\", \"hv-extension-comm\", function () {});\n if (server_id !== null) {\n comm.send({event_type: 'server_delete', 'id': server_id});\n return;\n } else if (comm !== null) {\n comm.send({event_type: 'delete', 'id': id});\n }\n delete PyViz.plot_index[id];\n if ((window.Bokeh !== undefined) & (id in window.Bokeh.index)) {\n var doc = window.Bokeh.index[id].model.document\n doc.clear();\n const i = window.Bokeh.documents.indexOf(doc);\n if (i > -1) {\n window.Bokeh.documents.splice(i, 1);\n }\n }\n}\n\n/**\n * Handle kernel restart event\n */\nfunction handle_kernel_cleanup(event, handle) {\n delete PyViz.comms[\"hv-extension-comm\"];\n window.PyViz.plot_index = {}\n}\n\n/**\n * Handle update_display_data messages\n */\nfunction handle_update_output(event, handle) {\n handle_clear_output(event, {cell: {output_area: handle.output_area}})\n handle_add_output(event, handle)\n}\n\nfunction register_renderer(events, OutputArea) {\n function append_mime(data, metadata, element) {\n // create a DOM node to render to\n var toinsert = this.create_output_subarea(\n metadata,\n CLASS_NAME,\n EXEC_MIME_TYPE\n );\n this.keyboard_manager.register_events(toinsert);\n // Render to node\n var props = {data: data, metadata: metadata[EXEC_MIME_TYPE]};\n render(props, toinsert[0]);\n element.append(toinsert);\n return toinsert\n }\n\n events.on('output_added.OutputArea', handle_add_output);\n events.on('output_updated.OutputArea', handle_update_output);\n events.on('clear_output.CodeCell', handle_clear_output);\n events.on('delete.Cell', handle_clear_output);\n events.on('kernel_ready.Kernel', handle_kernel_cleanup);\n\n OutputArea.prototype.register_mime_type(EXEC_MIME_TYPE, append_mime, {\n safe: true,\n index: 0\n });\n}\n\nif (window.Jupyter !== undefined) {\n try {\n var events = require('base/js/events');\n var OutputArea = require('notebook/js/outputarea').OutputArea;\n if (OutputArea.prototype.mime_types().indexOf(EXEC_MIME_TYPE) == -1) {\n register_renderer(events, OutputArea);\n }\n } catch(err) {\n }\n}\n" + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.holoviews_exec.v0+json": "", + "text/html": [ + "
\n", + "
\n", + "
\n", + "" + ] + }, + "metadata": { + "application/vnd.holoviews_exec.v0+json": { + "id": "3c27968b-3c60-4718-a309-e03e7467f97d" + } + }, + "output_type": "display_data" + }, + { + "data": {}, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": {}, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.holoviews_exec.v0+json": "", + "text/html": [ + "
\n", + "
\n", + "
\n", + "" + ], + "text/plain": [ + "GridSpec(ncols=12, nrows=5)\n", + " [0] GridSpec(height=800, ncols=12, nrows=5, sizing_mode='fixed', width=1200)\n", + " [0] HoloViews(DynamicMap, height=320, sizing_mode='stretch_both', width=400)\n", + " [1] HoloViews(DynamicMap, height=800, sizing_mode='stretch_both', width=800)\n", + " [2] HoloViews(DynamicMap, height=480, sizing_mode='stretch_both', width=400)\n", + " [1] WidgetBox(styles={'border-color': '...})\n", + " [0] Number(css_classes=['indicator'], default_color='#2B2B2B', font_size='18pt', format='{value:,}', name='Datapoints Selected', sizing_mode='stretch_width', title_size='14pt', value=58479894)\n", + " [1] Progress(sizing_mode='stretch_width', styles={'--success-bg-color': '...}, value=100)\n", + " [2] Column(min_height=500, sizing_mode='stretch_width')\n", + " [0] MultiChoice(name='county', options={" + ] + } + ], + "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.15" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/ds/25-1/2/1-07_etl.ipynb b/ds/25-1/2/1-07_etl.ipynb new file mode 100644 index 0000000..5592496 --- /dev/null +++ b/ds/25-1/2/1-07_etl.ipynb @@ -0,0 +1,1123 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "4b2efdc2-313c-493b-9d6c-432ae77d342c", + "metadata": {}, + "source": [ + "" + ] + }, + { + "cell_type": "markdown", + "id": "b754f1a2-24e8-44d4-ae6a-257483573434", + "metadata": {}, + "source": [ + "# Fundamentals of Accelerated Data Science # " + ] + }, + { + "cell_type": "markdown", + "id": "881e48fa-78ac-4d92-a8bb-d419c14df9e9", + "metadata": {}, + "source": [ + "## 07 - Extract, Transform, and Load ##\n", + "\n", + "**Table of Contents**\n", + "
\n", + "In this notebook, we will go through the basics of extract, transform, and load. This notebook covers the below sections: \n", + "1. [Extract, Transform, and Load (ETL)](#Extract,-Transform,-and-Load-(ETL))\n", + " * [Extract](#Extract)\n", + " * [Transform](#Transform)\n", + " * [Load](#Load)\n", + "2. [Save to Parquet Format](#Save-to-Parquet-Format)\n", + " * [Reading from Parquet](#Reading-from-Parquet)\n", + "3. [Accelerated ETL for Downstream Tasks](#Accelerated-ETL-for-Downstream-Tasks)" + ] + }, + { + "cell_type": "markdown", + "id": "a083cd6e-73f4-42b8-be04-bdd2bdeb5185", + "metadata": {}, + "source": [ + "## Extract, Transform, and Load (ETL) ##\n", + "An important but perhaps not as highly glorified use case of RAPIDS is extract, transform, and load, or ETL for short. It is a data integration process used to combine data from multiple sources into a single, consistent data store. It's primary goals are: \n", + "* Consolidates data from multiple sources into a single, consistent format\n", + "* Improves data quality through cleaning and validation\n", + "* Enables more efficient data analysis and reporting\n", + "* Supports data-driven decision making" + ] + }, + { + "cell_type": "markdown", + "id": "a9d5d73e-1665-4706-8840-6e14c2fabaa7", + "metadata": {}, + "source": [ + "### Extract ###\n", + "**Extract** is the first step where data is collected from various source systems. These sources could include: \n", + "* Static files (csv, json)\n", + "* SQL RDBMS\n", + "* Webpages\n", + "* API\n", + "\n", + "**Note**: cuDF doesn't have a way to get transactions from external SQL databases directly to GPU. The workaround is reading with pandas and create cuDF dataframe with `cudf.from_pandas()`. " + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "17be68c6-49c0-429d-86d2-c2cb711a6dc3", + "metadata": {}, + "outputs": [], + "source": [ + "%load_ext cudf.pandas\n", + "# DO NOT CHANGE THIS CELL\n", + "import pandas as pd\n", + "import time" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "37c75532-1bb6-4aab-9cee-3215d7137cee", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
agesexcountylatlongname
00mDARLINGTON54.533638-1.524400FRANCIS
10mDARLINGTON54.426254-1.465314EDWARD
20mDARLINGTON54.555199-1.496417TEDDY
30mDARLINGTON54.547909-1.572342ANGUS
40mDARLINGTON54.477638-1.605995CHARLIE
\n", + "
" + ], + "text/plain": [ + " age sex county lat long name\n", + "0 0 m DARLINGTON 54.533638 -1.524400 FRANCIS\n", + "1 0 m DARLINGTON 54.426254 -1.465314 EDWARD\n", + "2 0 m DARLINGTON 54.555199 -1.496417 TEDDY\n", + "3 0 m DARLINGTON 54.547909 -1.572342 ANGUS\n", + "4 0 m DARLINGTON 54.477638 -1.605995 CHARLIE" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# DO NOT CHANGE THIS CELL\n", + "dtype_dict={\n", + " 'age': 'int8', \n", + " 'sex': 'object', \n", + " 'county': 'object', \n", + " 'lat': 'float32', \n", + " 'long': 'float32', \n", + " 'name': 'object'\n", + "}\n", + " \n", + "df=pd.read_csv('./data/uk_pop.csv', dtype=dtype_dict)\n", + "df.head()" + ] + }, + { + "cell_type": "markdown", + "id": "dd97bfdd-5ead-4cd7-bc17-0e1eeb4aa62b", + "metadata": {}, + "source": [ + "When importing data, it is important to only include columns that are relevant to reduce the memory and compute burden. \n", + "\n", + "Below we read in the county centroid data. " + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "d064ff45-4cf4-48cd-9cbe-3f3f177fb875", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
countylat_county_centerlong_county_center
0BARKING AND DAGENHAM51.6210480.129583
1BARNET51.812552-0.218212
2BARNSLEY53.571907-1.548719
3BATH AND NORTH EAST SOMERSET51.354965-2.486675
4BEDFORD52.145476-0.454973
\n", + "
" + ], + "text/plain": [ + " county lat_county_center long_county_center\n", + "0 BARKING AND DAGENHAM 51.621048 0.129583\n", + "1 BARNET 51.812552 -0.218212\n", + "2 BARNSLEY 53.571907 -1.548719\n", + "3 BATH AND NORTH EAST SOMERSET 51.354965 -2.486675\n", + "4 BEDFORD 52.145476 -0.454973" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "centroid_df=pd.read_csv('county_centroid.csv')\n", + "centroid_df.columns=['county', 'lat_county_center', 'long_county_center']\n", + "centroid_df.head()" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "2dbf6f18-f28d-45ff-a4c4-1f50bfcf5860", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/html": [ + "
                                                                                             \n",
+       "                                  Total time elapsed: 0.559 seconds                          \n",
+       "                                                                                             \n",
+       "                                                Stats                                        \n",
+       "                                                                                             \n",
+       "┏━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━━━━━━┓\n",
+       "┃ Line no.  Line                                                GPU TIME(s)  CPU TIME(s) ┃\n",
+       "┡━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━━━━━━┩\n",
+       "│ 1        │     combined_df=df.merge(centroid_df, on='county') │ 0.347158869 │             │\n",
+       "│          │                                                    │             │             │\n",
+       "└──────────┴────────────────────────────────────────────────────┴─────────────┴─────────────┘\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[3m \u001b[0m\n", + "\u001b[3m Total time elapsed: 0.559 seconds \u001b[0m\n", + "\u001b[3m \u001b[0m\n", + "\u001b[3m Stats \u001b[0m\n", + "\u001b[3m \u001b[0m\n", + "┏━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━━━━━━┓\n", + "┃\u001b[1m \u001b[0m\u001b[1mLine no.\u001b[0m\u001b[1m \u001b[0m┃\u001b[1m \u001b[0m\u001b[1mLine \u001b[0m\u001b[1m \u001b[0m┃\u001b[1m \u001b[0m\u001b[1mGPU TIME(s)\u001b[0m\u001b[1m \u001b[0m┃\u001b[1m \u001b[0m\u001b[1mCPU TIME(s)\u001b[0m\u001b[1m \u001b[0m┃\n", + "┡━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━━━━━━┩\n", + "│ 1 │ \u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mcombined_df\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m=\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mdf\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m.\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mmerge\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m(\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mcentroid_df\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m,\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mon\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m=\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34mcounty\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m)\u001b[0m │ 0.347158869 │ │\n", + "│ │ \u001b[48;2;39;40;34m \u001b[0m │ │ │\n", + "└──────────┴────────────────────────────────────────────────────┴─────────────┴─────────────┘\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "%%cudf.pandas.line_profile\n", + "combined_df=df.merge(centroid_df, on='county')" + ] + }, + { + "cell_type": "markdown", + "id": "3294d6e0-00d9-4e33-91d8-2dc02db51512", + "metadata": {}, + "source": [ + "### Transform ###\n", + "During the **Transform** step, the extract data is cleaned, validated, and converted into a suitable format for analysis. \n", + "\n", + "Below we add a new column, representing each persons's distance from their respective county center. " + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "1e6ce3e0-625b-49ec-a755-2bfddee41431", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/html": [ + "
                                                                                                                   \n",
+       "                                             Total time elapsed: 2.406 seconds                                     \n",
+       "                                                                                                                   \n",
+       "                                                           Stats                                                   \n",
+       "                                                                                                                   \n",
+       "┏━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━━━━━━┓\n",
+       "┃ Line no.  Line                                                                      GPU TIME(s)  CPU TIME(s) ┃\n",
+       "┡━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━━━━━━┩\n",
+       "│ 1        │     c=['lat', 'long']                                                    │             │             │\n",
+       "│          │                                                                          │             │             │\n",
+       "│ 2        │     combined_df['R']=((combined_df[c] - combined_df.groupby('county')[c… │ 2.261978734 │             │\n",
+       "│          │                                                                          │             │             │\n",
+       "└──────────┴──────────────────────────────────────────────────────────────────────────┴─────────────┴─────────────┘\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[3m \u001b[0m\n", + "\u001b[3m Total time elapsed: 2.406 seconds \u001b[0m\n", + "\u001b[3m \u001b[0m\n", + "\u001b[3m Stats \u001b[0m\n", + "\u001b[3m \u001b[0m\n", + "┏━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━━━━━━┓\n", + "┃\u001b[1m \u001b[0m\u001b[1mLine no.\u001b[0m\u001b[1m \u001b[0m┃\u001b[1m \u001b[0m\u001b[1mLine \u001b[0m\u001b[1m \u001b[0m┃\u001b[1m \u001b[0m\u001b[1mGPU TIME(s)\u001b[0m\u001b[1m \u001b[0m┃\u001b[1m \u001b[0m\u001b[1mCPU TIME(s)\u001b[0m\u001b[1m \u001b[0m┃\n", + "┡━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━━━━━━┩\n", + "│ 1 │ \u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mc\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m=\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m[\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34mlat\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m,\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34mlong\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m]\u001b[0m\u001b[48;2;39;40;34m \u001b[0m │ │ │\n", + "│ │ \u001b[48;2;39;40;34m \u001b[0m │ │ │\n", + "│ 2 │ \u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mcombined_df\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m[\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34mR\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m]\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m=\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m(\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m(\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mcombined_df\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m[\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mc\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m]\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m-\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mcombined_df\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m.\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mgroupby\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m(\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34mcounty\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m)\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m[\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mc\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m…\u001b[0m │ 2.261978734 │ │\n", + "│ │ \u001b[48;2;39;40;34m \u001b[0m │ │ │\n", + "└──────────┴──────────────────────────────────────────────────────────────────────────┴─────────────┴─────────────┘\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "%%cudf.pandas.line_profile\n", + "c=['lat', 'long']\n", + "combined_df['R']=((combined_df[c] - combined_df.groupby('county')[c].transform('mean')) ** 2).sum(axis=1) ** 0.5" + ] + }, + { + "cell_type": "markdown", + "id": "7e676b1b-143d-4cf4-a534-8c25dafac1a6", + "metadata": {}, + "source": [ + "Using joins to get lookup values can be faster than deriving those. It is not uncommon to store group statistics for this purpose. " + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "4339b0c7-c391-48f4-9ba3-00f79ba50b5b", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/html": [ + "
                                                                                                                   \n",
+       "                                             Total time elapsed: 0.830 seconds                                     \n",
+       "                                                                                                                   \n",
+       "                                                           Stats                                                   \n",
+       "                                                                                                                   \n",
+       "┏━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━━━━━━┓\n",
+       "┃ Line no.  Line                                                                      GPU TIME(s)  CPU TIME(s) ┃\n",
+       "┡━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━━━━━━┩\n",
+       "│ 3        │     centroid_df=pd.read_csv('county_centroid.csv')                       │ 0.005319933 │             │\n",
+       "│          │                                                                          │             │             │\n",
+       "│ 6        │     combined_df=df.merge(centroid_df, on='county', suffixes=['', '_coun… │ 0.452281164 │             │\n",
+       "│          │                                                                          │             │             │\n",
+       "│ 9        │     combined_df['R']=((combined_df['lat']-combined_df['lat_county_cente… │ 0.185010254 │             │\n",
+       "│          │                                                                          │             │             │\n",
+       "└──────────┴──────────────────────────────────────────────────────────────────────────┴─────────────┴─────────────┘\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[3m \u001b[0m\n", + "\u001b[3m Total time elapsed: 0.830 seconds \u001b[0m\n", + "\u001b[3m \u001b[0m\n", + "\u001b[3m Stats \u001b[0m\n", + "\u001b[3m \u001b[0m\n", + "┏━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━━━━━━┓\n", + "┃\u001b[1m \u001b[0m\u001b[1mLine no.\u001b[0m\u001b[1m \u001b[0m┃\u001b[1m \u001b[0m\u001b[1mLine \u001b[0m\u001b[1m \u001b[0m┃\u001b[1m \u001b[0m\u001b[1mGPU TIME(s)\u001b[0m\u001b[1m \u001b[0m┃\u001b[1m \u001b[0m\u001b[1mCPU TIME(s)\u001b[0m\u001b[1m \u001b[0m┃\n", + "┡━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━━━━━━┩\n", + "│ 3 │ \u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mcentroid_df\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m=\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mpd\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m.\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mread_csv\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m(\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34mcounty_centroid.csv\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m)\u001b[0m\u001b[48;2;39;40;34m \u001b[0m │ 0.005319933 │ │\n", + "│ │ \u001b[48;2;39;40;34m \u001b[0m │ │ │\n", + "│ 6 │ \u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mcombined_df\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m=\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mdf\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m.\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mmerge\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m(\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mcentroid_df\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m,\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mon\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m=\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34mcounty\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m,\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34msuffixes\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m=\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m[\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m,\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m_coun…\u001b[0m │ 0.452281164 │ │\n", + "│ │ \u001b[48;2;39;40;34m \u001b[0m │ │ │\n", + "│ 9 │ \u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mcombined_df\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m[\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34mR\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m]\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m=\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m(\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m(\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mcombined_df\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m[\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34mlat\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m]\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m-\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mcombined_df\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m[\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34mlat_county_cente…\u001b[0m │ 0.185010254 │ │\n", + "│ │ \u001b[48;2;39;40;34m \u001b[0m │ │ │\n", + "└──────────┴──────────────────────────────────────────────────────────────────────────┴─────────────┴─────────────┘\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "%%cudf.pandas.line_profile\n", + "\n", + "# read in centroid data\n", + "centroid_df=pd.read_csv('county_centroid.csv')\n", + "\n", + "# merge \n", + "combined_df=df.merge(centroid_df, on='county', suffixes=['', '_county_center'])\n", + "\n", + "# calculate distance from county center\n", + "combined_df['R']=((combined_df['lat']-combined_df['lat_county_center'])**2+(combined_df['long']-combined_df['long_county_center'])**2)**0.5" + ] + }, + { + "cell_type": "markdown", + "id": "aa017ea0-4ad2-4184-9669-40747e31381a", + "metadata": {}, + "source": [ + "Below we filter the data to only include adults. " + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "917aa1ed-1778-40d4-bbbc-5892935ce7cd", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
agesexcountylatlongnamelat_county_centerlong_county_centerR
2244337860mDARLINGTON54.542645-1.611084ELLIOTT54.51356-1.568020.051966
2244337960mDARLINGTON54.547031-1.517514SANTINO54.51356-1.568020.060590
2244338060mDARLINGTON54.470505-1.527565LAWRENCE54.51356-1.568020.059079
2244338160mDARLINGTON54.520664-1.620248THEO54.51356-1.568020.052709
2244338260mDARLINGTON54.569939-1.498693CHARLIE54.51356-1.568020.089358
\n", + "
" + ], + "text/plain": [ + " age sex county lat long name \\\n", + "22443378 60 m DARLINGTON 54.542645 -1.611084 ELLIOTT \n", + "22443379 60 m DARLINGTON 54.547031 -1.517514 SANTINO \n", + "22443380 60 m DARLINGTON 54.470505 -1.527565 LAWRENCE \n", + "22443381 60 m DARLINGTON 54.520664 -1.620248 THEO \n", + "22443382 60 m DARLINGTON 54.569939 -1.498693 CHARLIE \n", + "\n", + " lat_county_center long_county_center R \n", + "22443378 54.51356 -1.56802 0.051966 \n", + "22443379 54.51356 -1.56802 0.060590 \n", + "22443380 54.51356 -1.56802 0.059079 \n", + "22443381 54.51356 -1.56802 0.052709 \n", + "22443382 54.51356 -1.56802 0.089358 " + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
                                                                                          \n",
+       "                                Total time elapsed: 0.574 seconds                         \n",
+       "                                                                                          \n",
+       "                                              Stats                                       \n",
+       "                                                                                          \n",
+       "┏━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━━━━━━┓\n",
+       "┃ Line no.  Line                                             GPU TIME(s)  CPU TIME(s) ┃\n",
+       "┡━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━━━━━━┩\n",
+       "│ 2        │     senior_df_filter=combined_df['age'] >= 60   │ 0.008445255 │             │\n",
+       "│          │                                                 │             │             │\n",
+       "│ 3        │     senior_df=combined_df.loc[senior_df_filter] │ 0.081001887 │             │\n",
+       "│          │                                                 │             │             │\n",
+       "│ 5        │     display(senior_df.head())                   │ 0.287346369 │             │\n",
+       "│          │                                                 │             │             │\n",
+       "└──────────┴─────────────────────────────────────────────────┴─────────────┴─────────────┘\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[3m \u001b[0m\n", + "\u001b[3m Total time elapsed: 0.574 seconds \u001b[0m\n", + "\u001b[3m \u001b[0m\n", + "\u001b[3m Stats \u001b[0m\n", + "\u001b[3m \u001b[0m\n", + "┏━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━━━━━━┓\n", + "┃\u001b[1m \u001b[0m\u001b[1mLine no.\u001b[0m\u001b[1m \u001b[0m┃\u001b[1m \u001b[0m\u001b[1mLine \u001b[0m\u001b[1m \u001b[0m┃\u001b[1m \u001b[0m\u001b[1mGPU TIME(s)\u001b[0m\u001b[1m \u001b[0m┃\u001b[1m \u001b[0m\u001b[1mCPU TIME(s)\u001b[0m\u001b[1m \u001b[0m┃\n", + "┡━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━━━━━━┩\n", + "│ 2 │ \u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34msenior_df_filter\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m=\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mcombined_df\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m[\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34mage\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m]\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m>\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m=\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;174;129;255;48;2;39;40;34m60\u001b[0m\u001b[48;2;39;40;34m \u001b[0m │ 0.008445255 │ │\n", + "│ │ \u001b[48;2;39;40;34m \u001b[0m │ │ │\n", + "│ 3 │ \u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34msenior_df\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m=\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mcombined_df\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m.\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mloc\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m[\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34msenior_df_filter\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m]\u001b[0m │ 0.081001887 │ │\n", + "│ │ \u001b[48;2;39;40;34m \u001b[0m │ │ │\n", + "│ 5 │ \u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mdisplay\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m(\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34msenior_df\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m.\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mhead\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m(\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m)\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m)\u001b[0m\u001b[48;2;39;40;34m \u001b[0m │ 0.287346369 │ │\n", + "│ │ \u001b[48;2;39;40;34m \u001b[0m │ │ │\n", + "└──────────┴─────────────────────────────────────────────────┴─────────────┴─────────────┘\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "%%cudf.pandas.line_profile\n", + "\n", + "senior_df_filter=combined_df['age'] >= 60\n", + "senior_df=combined_df.loc[senior_df_filter]\n", + "\n", + "display(senior_df.head())" + ] + }, + { + "cell_type": "markdown", + "id": "bb474531-389e-44aa-a6b3-8de205d9cb0b", + "metadata": {}, + "source": [ + "### Load ###\n", + "The final **Load** step is where the transformed data is loaded into a target system. The target system can be a database or a file. The key is to develop a system that is efficient for downstream tasks. " + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "6f637229-1770-439b-9702-6356723c96f8", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
agesexcountylatlongnamelat_county_centerlong_county_centerR
2244337860mDARLINGTON54.542645-1.611084ELLIOTT54.51356-1.568020.051966
2244337960mDARLINGTON54.547031-1.517514SANTINO54.51356-1.568020.060590
2244338060mDARLINGTON54.470505-1.527565LAWRENCE54.51356-1.568020.059079
2244338160mDARLINGTON54.520664-1.620248THEO54.51356-1.568020.052709
2244338260mDARLINGTON54.569939-1.498693CHARLIE54.51356-1.568020.089358
\n", + "
" + ], + "text/plain": [ + " age sex county lat long name \\\n", + "22443378 60 m DARLINGTON 54.542645 -1.611084 ELLIOTT \n", + "22443379 60 m DARLINGTON 54.547031 -1.517514 SANTINO \n", + "22443380 60 m DARLINGTON 54.470505 -1.527565 LAWRENCE \n", + "22443381 60 m DARLINGTON 54.520664 -1.620248 THEO \n", + "22443382 60 m DARLINGTON 54.569939 -1.498693 CHARLIE \n", + "\n", + " lat_county_center long_county_center R \n", + "22443378 54.51356 -1.56802 0.051966 \n", + "22443379 54.51356 -1.56802 0.060590 \n", + "22443380 54.51356 -1.56802 0.059079 \n", + "22443381 54.51356 -1.56802 0.052709 \n", + "22443382 54.51356 -1.56802 0.089358 " + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "senior_df.head()" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "348eac86-3d67-42e2-9c7f-e0acb14021cd", + "metadata": {}, + "outputs": [], + "source": [ + "# DO NOT CHANGE THIS CELL\n", + "senior_df.to_csv('senior_df.csv', index=False)" + ] + }, + { + "cell_type": "markdown", + "id": "e25ae20b-7eea-4775-9743-d43cbf6877a7", + "metadata": {}, + "source": [ + "**Note**: If the downstream task involves querying and analyzing the data further, the csv file format may not be the best choice. " + ] + }, + { + "cell_type": "markdown", + "id": "08413512-47f9-4543-a80c-442f1500b49a", + "metadata": {}, + "source": [ + "\n", + "## Save to Parquet Format ##\n", + "After processing the data, we persist it for later use. [Apache Parquet](https://parquet.apache.org/) is a columnar binary format and has become the de-facto standard for the storage of large volumes of tabular data. Converting to Parquet file format is important and csv files should generally be avoided in data products. While the csv file format is convenient and human-readable, importing csv files requires reading and parsing entire records, which can be a bottleneck. In fact, many developers will start their analysis by first converting csv files to the Parquet file format. There are many reasons to use Parquet format for analytics: \n", + "* The columnar nature of Parquet files allows for column pruning, which often yields big query performance gains. \n", + "* It uses metadata to store the schema and supports more advanced data types such as categorical, datetimes, and more. This means that importing data would not require schema inference or manual schema specification. \n", + "* It captures metadata related to row-group level statistics for each column. This enables predicate pushdown filtering, which is a form of query pushdown that allows computations to happen at the “database layer” instead of the “execution engine layer”. In this case, the database layer is Parquet files in a filesystem, and the execution engine is Dask. \n", + "* It supports flexible compression options, making it more compact to store and more portable than a database. \n", + "\n", + "We will use `.to_parquet(path)`[[doc]](https://docs.dask.org/en/stable/generated/dask.dataframe.to_parquet.html#dask-dataframe-to-parquet) to write to Parquet files. By default, files will be created in the specified output directory using the convention `part.0.parquet`, `part.1.parquet`, `part.2.parquet`, ... and so on for each partition in the DataFrame. This can be changed using the `name_function` parameter. Ouputting multiple files lets Dask write to multiple files in parallel, which is faster than writing to a single file. \n", + "\n", + "

\n", + "\n", + "When working with large datasets, decoding and encoding is often an expensive task. This challenge tends to compound as the data size grows. A common pattern in data science is to subset the dataset by columns, row slices, or both. Moving these filtering operations to the read phase of the workflow can: 1) reduce I/O time, and 2) reduce the amount of memory required, which is important for GPUs where memory can be a limiting factor. Parquet file format enables filtered reading through **column pruning** and **statistic-based predicate filtering** to skip portions of the data that are irrelevant to the problem. Below are some tips for writing Parquet files: \n", + "* When writing data, sorting the data by the columns that expect the most filters to be applied or columns with the highest cardinality can lead to meaningful performance benefits. The metadata calculated for each row group will enable predicate pushdown filters to the fullest extent. \n", + "* Writing Parquet format, which requires reprocessing entire data sets, can be expensive. The format works remarkably well for read-intensive applications and low latency data storage and retrieval. \n", + "* Partitions in Dask DataFrame can write out files in parallel, so multiple Parquet files are written simultaneously.\n", + "\n", + "Below we write the data into Parquet format, after sorting by the county. " + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "35007914-f7af-4083-a42a-fc02048c7ec4", + "metadata": {}, + "outputs": [], + "source": [ + "# DO NOT CHANGE THIS CELL\n", + "senior_df=senior_df.sort_values('county')\n", + "\n", + "senior_df.to_parquet('senior_df.parquet', index=False)" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "7e82b557-a2c2-4a42-96d0-35174370aaee", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/plain": [ + "{'status': 'ok', 'restart': True}" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# DO NOT CHANGE THIS CELL\n", + "import IPython\n", + "app = IPython.Application.instance()\n", + "app.kernel.do_shutdown(True)" + ] + }, + { + "cell_type": "markdown", + "id": "2a3438a6-02d8-4ea0-824f-08c4bcac1063", + "metadata": {}, + "source": [ + "### Reading from Parquet ###\n", + "Querying data in Parquet format can be significantly more performant, especially as the size of the data increases. \n", + "\n", + "Below we read from both the csv format and Parquet format for comparison. " + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "11c2aab8-3f67-4a57-8eaa-b5d4b224a7b0", + "metadata": {}, + "outputs": [], + "source": [ + "%load_ext cudf.pandas\n", + "import pandas as pd\n", + "import time" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "3876ba06-ef43-4849-8a16-58ee2f3a8423", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/html": [ + "
                                                                                                                   \n",
+       "                                             Total time elapsed: 0.245 seconds                                     \n",
+       "                                                                                                                   \n",
+       "                                                           Stats                                                   \n",
+       "                                                                                                                   \n",
+       "┏━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━━━━━━┓\n",
+       "┃ Line no.  Line                                                                      GPU TIME(s)  CPU TIME(s) ┃\n",
+       "┡━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━━━━━━┩\n",
+       "│ 2        │     sel=[('county', '=', 'BLACKPOOL')]                                   │             │             │\n",
+       "│          │                                                                          │             │             │\n",
+       "│ 3        │     parquet_df=pd.read_parquet('senior_df.parquet', columns=['age', 'se… │ 0.046496972 │             │\n",
+       "│          │                                                                          │             │             │\n",
+       "│ 4        │     parquet_df=parquet_df.loc[parquet_df['county']=='BLACKPOOL']         │ 0.014774265 │             │\n",
+       "│          │                                                                          │             │             │\n",
+       "└──────────┴──────────────────────────────────────────────────────────────────────────┴─────────────┴─────────────┘\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[3m \u001b[0m\n", + "\u001b[3m Total time elapsed: 0.245 seconds \u001b[0m\n", + "\u001b[3m \u001b[0m\n", + "\u001b[3m Stats \u001b[0m\n", + "\u001b[3m \u001b[0m\n", + "┏━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━━━━━━┓\n", + "┃\u001b[1m \u001b[0m\u001b[1mLine no.\u001b[0m\u001b[1m \u001b[0m┃\u001b[1m \u001b[0m\u001b[1mLine \u001b[0m\u001b[1m \u001b[0m┃\u001b[1m \u001b[0m\u001b[1mGPU TIME(s)\u001b[0m\u001b[1m \u001b[0m┃\u001b[1m \u001b[0m\u001b[1mCPU TIME(s)\u001b[0m\u001b[1m \u001b[0m┃\n", + "┡━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━━━━━━┩\n", + "│ 2 │ \u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34msel\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m=\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m[\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m(\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34mcounty\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m,\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m=\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m,\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34mBLACKPOOL\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m)\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m]\u001b[0m\u001b[48;2;39;40;34m \u001b[0m │ │ │\n", + "│ │ \u001b[48;2;39;40;34m \u001b[0m │ │ │\n", + "│ 3 │ \u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mparquet_df\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m=\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mpd\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m.\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mread_parquet\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m(\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34msenior_df.parquet\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m,\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mcolumns\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m=\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m[\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34mage\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m,\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34mse…\u001b[0m │ 0.046496972 │ │\n", + "│ │ \u001b[48;2;39;40;34m \u001b[0m │ │ │\n", + "│ 4 │ \u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mparquet_df\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m=\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mparquet_df\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m.\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mloc\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m[\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mparquet_df\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m[\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34mcounty\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m]\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m==\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34mBLACKPOOL\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m]\u001b[0m\u001b[48;2;39;40;34m \u001b[0m │ 0.014774265 │ │\n", + "│ │ \u001b[48;2;39;40;34m \u001b[0m │ │ │\n", + "└──────────┴──────────────────────────────────────────────────────────────────────────┴─────────────┴─────────────┘\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "%%cudf.pandas.line_profile\n", + "\n", + "sel=[('county', '=', 'BLACKPOOL')]\n", + "parquet_df=pd.read_parquet('senior_df.parquet', columns=['age', 'sex', 'county', 'lat', 'long', 'name', 'R'], filters=sel)\n", + "parquet_df=parquet_df.loc[parquet_df['county']=='BLACKPOOL']" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "9c67aaf7-4dda-4bf5-9205-fd097d567ea4", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/plain": [ + "array(['BLACKPOOL'], dtype=object)" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "parquet_df['county'].unique()" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "61a225a7-3106-48a3-b100-24c829a6089c", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/html": [ + "
                                                                                                                   \n",
+       "                                             Total time elapsed: 0.697 seconds                                     \n",
+       "                                                                                                                   \n",
+       "                                                           Stats                                                   \n",
+       "                                                                                                                   \n",
+       "┏━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━━━━━━┓\n",
+       "┃ Line no.  Line                                                                      GPU TIME(s)  CPU TIME(s) ┃\n",
+       "┡━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━━━━━━┩\n",
+       "│ 2        │     df=pd.read_csv('./senior_df.csv', usecols=['age', 'sex', 'county',  │ 0.535917163 │             │\n",
+       "│          │                                                                          │             │             │\n",
+       "│ 3        │     df=df.loc[df['county']=='BLACKPOOL']                                 │ 0.017859971 │             │\n",
+       "│          │                                                                          │             │             │\n",
+       "└──────────┴──────────────────────────────────────────────────────────────────────────┴─────────────┴─────────────┘\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[3m \u001b[0m\n", + "\u001b[3m Total time elapsed: 0.697 seconds \u001b[0m\n", + "\u001b[3m \u001b[0m\n", + "\u001b[3m Stats \u001b[0m\n", + "\u001b[3m \u001b[0m\n", + "┏━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━━━━━━┓\n", + "┃\u001b[1m \u001b[0m\u001b[1mLine no.\u001b[0m\u001b[1m \u001b[0m┃\u001b[1m \u001b[0m\u001b[1mLine \u001b[0m\u001b[1m \u001b[0m┃\u001b[1m \u001b[0m\u001b[1mGPU TIME(s)\u001b[0m\u001b[1m \u001b[0m┃\u001b[1m \u001b[0m\u001b[1mCPU TIME(s)\u001b[0m\u001b[1m \u001b[0m┃\n", + "┡━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━━━━━━┩\n", + "│ 2 │ \u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mdf\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m=\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mpd\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m.\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mread_csv\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m(\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m./senior_df.csv\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m,\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34musecols\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m=\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m[\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34mage\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m,\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34msex\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m,\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34mcounty\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m,\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m…\u001b[0m │ 0.535917163 │ │\n", + "│ │ \u001b[48;2;39;40;34m \u001b[0m │ │ │\n", + "│ 3 │ \u001b[38;2;248;248;242;48;2;39;40;34m \u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mdf\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m=\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mdf\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m.\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mloc\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m[\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34mdf\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m[\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34mcounty\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m]\u001b[0m\u001b[38;2;255;70;137;48;2;39;40;34m==\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34mBLACKPOOL\u001b[0m\u001b[38;2;230;219;116;48;2;39;40;34m'\u001b[0m\u001b[38;2;248;248;242;48;2;39;40;34m]\u001b[0m\u001b[48;2;39;40;34m \u001b[0m │ 0.017859971 │ │\n", + "│ │ \u001b[48;2;39;40;34m \u001b[0m │ │ │\n", + "└──────────┴──────────────────────────────────────────────────────────────────────────┴─────────────┴─────────────┘\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "%%cudf.pandas.line_profile\n", + "\n", + "df=pd.read_csv('./senior_df.csv', usecols=['age', 'sex', 'county', 'lat', 'long', 'name', 'R'])\n", + "df=df.loc[df['county']=='BLACKPOOL']" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "b1b5fff0-ccfa-47fe-b821-d68c5858f844", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "array(['BLACKPOOL'], dtype=object)" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df['county'].unique()" + ] + }, + { + "cell_type": "markdown", + "id": "8cdf4ff5-c182-41e5-b17d-3c0cd02dd9d5", + "metadata": {}, + "source": [ + "## Accelerated ETL for Downstream Tasks ##\n", + "Accelerating the ETL process is important for data science as it provides the below benefits: \n", + "* **Timely insights**: Faster ETL allows for more up-to-date data analysis, enabling data scientists to work with the most current information.\n", + "* **Increased productivity**: Reduced processing time means data scientists can spend more time on analysis and model development rather than waiting for data to be ready.\n", + "* **Handling larger datasets**: Accelerated ETL processes can manage larger volumes of data more efficiently.\n", + "* **Cost efficiency**: Accelerated ETL can reduce computational resources and time, leading to lower infrastructure costs.\n", + "\n", + "

" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5105158c-ce64-4b8c-9ee7-6d5d684ea5cf", + "metadata": {}, + "outputs": [], + "source": [ + "import IPython\n", + "app = IPython.Application.instance()\n", + "app.kernel.do_shutdown(True)" + ] + }, + { + "cell_type": "markdown", + "id": "c88554b0-23c6-4316-912e-f962b4c97456", + "metadata": {}, + "source": [ + "**Well Done!** Let's move to the [next notebook](1-08_dask-cudf.ipynb). " + ] + }, + { + "cell_type": "markdown", + "id": "391f35d9-6768-4bf3-8d96-b706351c2ad6", + "metadata": {}, + "source": [ + "" + ] + } + ], + "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.15" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/ds/25-1/2/1-08_cudf-polars.ipynb b/ds/25-1/2/1-08_cudf-polars.ipynb new file mode 100644 index 0000000..48d81a2 --- /dev/null +++ b/ds/25-1/2/1-08_cudf-polars.ipynb @@ -0,0 +1,1220 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "734557aa-90fb-468f-9ed0-0e6f295bb9eb", + "metadata": {}, + "source": [ + "" + ] + }, + { + "cell_type": "markdown", + "id": "6dbb572c-1291-4011-9ed4-120eb2ec7b29", + "metadata": {}, + "source": [ + "# Fundamentals of Accelerated Data Science # " + ] + }, + { + "cell_type": "markdown", + "id": "377ba9f0-0acc-4574-9a43-c475cfd52dd5", + "metadata": {}, + "source": [ + "## 08 - Introduction to cuDF Polars ##\n", + "\n", + "**Table of Contents**\n", + "
\n", + "This notebook briefly introduces Polars and covers the new GPU engine. This notebook covers the below sections: \n", + "1. [Introduction to Polars](#Introduction-to-Polars)\n", + " * [Installation](#Installation)\n", + " * [Creating a DataFrame](#Creating-a-DataFrame)\n", + " * [Running Basic Operations](#Running-Basic-Operations)\n", + " * [Pandas Comparison](#Pandas-Comparison)\n", + " * [cuDF Pandas Comparison](#cuDF-Pandas-Comparison)\n", + "2. [Basic Polars Operations](#Basic-Polars-Operations)\n", + " * [Polars Eager Execution API Reference](#Polars-Eager-Execution-API-Reference)\n", + " * [Exercise #1 - Load Data](#Exercise-#1---Load-Data)\n", + " * [Exercise #2 - Calculate Average Age of Population](#Exercise-#2---Calculate-Average-Age-of-Population)\n", + " * [Exercise #3 - Group By and Aggregation](#Exercise-#3---Group-By-and-Aggregation)\n", + " * [Exercise #4 - Gender Distribution](#Exercise-#4---Gender-Distribution)\n", + "4. [Lazy Execution](#Lazy-Execution)\n", + " * [Polars Lazy Execution API Reference](#Polars-Lazy-Execution-API-Reference)\n", + " * [Execution Graph](#Execution-Graph)\n", + " * [Exercise #5 - Creating a Lazy Dataframe](#Exercise-#5---Creating-a-Lazy-Dataframe)\n", + " * [Exercise #6 - Query Creation](#Exercise-#6---Query-Creation)\n", + "5. [cuDF Polars](#cuDF-Polars)\n", + " * [Accelerate Previous Code](#Accelerate-Previous-Code)\n", + " * [Verify Results Across Engines](#Verify-Results-Across-Engines)\n", + " * [Fallback](#Fallback)\n", + " * [Exercise #7 - Enable GPU Engine](#Exercise-#7---Enable-GPU-Engine)" + ] + }, + { + "cell_type": "markdown", + "id": "e7dd978a-a785-4b67-822e-048d56e231e2", + "metadata": {}, + "source": [ + "## Introduction to Polars ##\n", + "Polars is a data analysis and manipulation library that is designed for large data processing (10-100GB) on a single GPU and is known for its speed and memory efficiency. While Pandas makes use of eager execution, Polars additionally has the capability for lazy execution through the built-in query optimizer and makes use of zero-copy optimization techniques. Due to these improvements, Polars typically performs common operations 5-10x faster than Pandas, and requires 2-4 times less RAM. NVIDIA brings hardware acceleration to Polars through a new GPU engine named cuDF Polars, which is available as a pip install." + ] + }, + { + "cell_type": "markdown", + "id": "a2187687-9a53-44f9-ba55-ba9e1081cac6", + "metadata": {}, + "source": [ + "### Creating a DataFrame ###\n", + "Now let's see how the syntax looks! We will create a dataframe to use within Polars." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "be96aa2e-f1fc-4521-bf11-89505a2981ac", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Time Taken: 0.7802 seconds\n" + ] + } + ], + "source": [ + "import polars as pl\n", + "import time\n", + "\n", + "start_time = time.time()\n", + "\n", + "polars_df = pl.read_csv('./data/uk_pop.csv')\n", + "\n", + "polars_time = time.time() - start_time\n", + "\n", + "print(f\"Time Taken: {polars_time:.4f} seconds\")" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "90dedd84-0792-4a54-9380-4b3a020bccd1", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "shape: (5, 6)
agesexcountylatlongname
i64strstrf64f64str
0"m""DARLINGTON"54.533644-1.524401"FRANCIS"
0"m""DARLINGTON"54.426256-1.465314"EDWARD"
0"m""DARLINGTON"54.5552-1.496417"TEDDY"
0"m""DARLINGTON"54.547906-1.572341"ANGUS"
0"m""DARLINGTON"54.477639-1.605995"CHARLIE"
" + ], + "text/plain": [ + "shape: (5, 6)\n", + "┌─────┬─────┬────────────┬───────────┬───────────┬─────────┐\n", + "│ age ┆ sex ┆ county ┆ lat ┆ long ┆ name │\n", + "│ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- │\n", + "│ i64 ┆ str ┆ str ┆ f64 ┆ f64 ┆ str │\n", + "╞═════╪═════╪════════════╪═══════════╪═══════════╪═════════╡\n", + "│ 0 ┆ m ┆ DARLINGTON ┆ 54.533644 ┆ -1.524401 ┆ FRANCIS │\n", + "│ 0 ┆ m ┆ DARLINGTON ┆ 54.426256 ┆ -1.465314 ┆ EDWARD │\n", + "│ 0 ┆ m ┆ DARLINGTON ┆ 54.5552 ┆ -1.496417 ┆ TEDDY │\n", + "│ 0 ┆ m ┆ DARLINGTON ┆ 54.547906 ┆ -1.572341 ┆ ANGUS │\n", + "│ 0 ┆ m ┆ DARLINGTON ┆ 54.477639 ┆ -1.605995 ┆ CHARLIE │\n", + "└─────┴─────┴────────────┴───────────┴───────────┴─────────┘" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "polars_df.head()" + ] + }, + { + "cell_type": "markdown", + "id": "12c66adc-5ac0-49a1-95fe-dc78fbb3950a", + "metadata": {}, + "source": [ + "### Running Basic Operations ###\n", + "That was simple- now let's try running a few operations on the dataset! We will be loading the dataset again for a fair comparison with Pandas later." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "7fc2280f-a89c-42dd-befc-39ffce2f01ec", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "shape: (5, 6)\n", + "┌─────┬─────┬──────────────────────────┬───────────┬───────────┬───────┐\n", + "│ age ┆ sex ┆ county ┆ lat ┆ long ┆ name │\n", + "│ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- │\n", + "│ i64 ┆ str ┆ str ┆ f64 ┆ f64 ┆ str │\n", + "╞═════╪═════╪══════════════════════════╪═══════════╪═══════════╪═══════╡\n", + "│ 1 ┆ f ┆ EAST RIDING OF YORKSHIRE ┆ 53.737344 ┆ -0.638535 ┆ ZYRAH │\n", + "│ 1 ┆ f ┆ SHEFFIELD ┆ 53.35529 ┆ -1.669447 ┆ ZYRAH │\n", + "│ 1 ┆ f ┆ LINCOLNSHIRE ┆ 53.164176 ┆ 0.015812 ┆ ZYRAH │\n", + "│ 1 ┆ f ┆ WORCESTERSHIRE ┆ 52.258629 ┆ -2.31696 ┆ ZYRAH │\n", + "│ 1 ┆ f ┆ HERTFORDSHIRE ┆ 51.731816 ┆ -0.377476 ┆ ZYRAH │\n", + "└─────┴─────┴──────────────────────────┴───────────┴───────────┴───────┘\n", + "Time Taken: 6.6280 seconds\n" + ] + } + ], + "source": [ + "start_time = time.time()\n", + "\n", + "#load data\n", + "polars_df = pl.read_csv('./data/uk_pop.csv')\n", + "\n", + "# Filter for ages above 0\n", + "filtered_df = polars_df.filter(pl.col('age') > 0.0)\n", + "\n", + "#Sort by name\n", + "sorted_df = filtered_df.sort('name', descending=True)\n", + "\n", + "print(sorted_df.head())\n", + "polars_time = time.time() - start_time\n", + "print(f\"Time Taken: {polars_time:.4f} seconds\")" + ] + }, + { + "cell_type": "markdown", + "id": "bf06893b-a772-4c15-a867-e4cf6aa0984b", + "metadata": {}, + "source": [ + "### Pandas Comparison ###\n", + "Let's see how long this would've taken in Pandas." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "99468ce0-ecb1-4da2-9e96-44c41c73605a", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Time Taken: 148.7865 seconds\n", + "\n" + ] + } + ], + "source": [ + "import pandas as pd\n", + "import time\n", + "start_time = time.time()\n", + "pandas_df = pd.read_csv('./data/uk_pop.csv')\n", + "\n", + "filtered_df = pandas_df[pandas_df['age'] > 0.0]\n", + "\n", + "sorted_df = filtered_df.sort_values(by=['name'], ascending=False)\n", + "\n", + "pandas_time = time.time() - start_time\n", + "print(f\"Time Taken: {pandas_time:.4f} seconds\\n\")" + ] + }, + { + "cell_type": "markdown", + "id": "d5f25cae-1ee8-462e-a487-68af7d9004ce", + "metadata": {}, + "source": [ + "### cuDF Pandas Comparison ###\n", + "Wow! That took quite some time to execute. Let's see if we can run it faster with cuDF Pandas." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "23484488-4d35-4359-804d-80bb60463d2e", + "metadata": {}, + "outputs": [], + "source": [ + "# Activate cuDF Pandas\n", + "%load_ext cudf.pandas\n", + "import pandas as pd" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "714b14dd-0ebd-4814-a341-75cbf8e44fd5", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Time Taken for cuDF Pandas: 5.0425 seconds\n", + "\n" + ] + } + ], + "source": [ + "import pandas as pd\n", + "import time\n", + "start_time = time.time()\n", + "pandas_df = pd.read_csv('./data/uk_pop.csv')\n", + "\n", + "filtered_df = pandas_df[pandas_df['age'] > 0.0]\n", + "\n", + "sorted_df = filtered_df.sort_values(by=['name'], ascending=False)\n", + "\n", + "pandas_time = time.time() - start_time\n", + "print(f\"Time Taken for cuDF Pandas: {pandas_time:.4f} seconds\\n\")" + ] + }, + { + "cell_type": "markdown", + "id": "d9a3692e-fe36-4bb2-bf1d-84ce5d4834f7", + "metadata": {}, + "source": [ + "**Note**: Even with cuDF Pandas, we sometimes notice that the performance can be slower than Polars." + ] + }, + { + "cell_type": "markdown", + "id": "dd499b82-9b82-472b-934c-5d7e34743458", + "metadata": {}, + "source": [ + "## Basic Polars Operations ##\n", + "Please refer to the following API reference guide to complete the exercises below.\n", + "\n", + "1. Load data\n", + "2. Calculate average age of population\n", + "3. Group By and Aggregation\n", + "4. Gender Distribution" + ] + }, + { + "cell_type": "markdown", + "id": "b3972070-b02e-4870-90c2-bcae0c43f82e", + "metadata": { + "jp-MarkdownHeadingCollapsed": true + }, + "source": [ + "### Polars Eager Execution API Reference ###\n", + "\n", + "**DataFrame**\n", + "\n", + "The main data structure for eager execution in Polars.\n", + "\n", + "- `pl.DataFrame(data)`: Create a DataFrame from data\n", + "- `pl.read_csv(file)`: Read CSV file into DataFrame\n", + "- `pl.read_parquet(file)`: Read Parquet file into DataFrame\n", + "\n", + "**Key Methods**\n", + "\n", + "- `filter(mask)`: Filter rows based on a boolean mask\n", + "- `select(columns)`: Select specific columns\n", + "- `with_columns(expressions)`: Add or modify columns\n", + "- `group_by(columns)`: Group by specified columns\n", + "- `agg(aggregations)`: Perform aggregations on grouped data\n", + "- `sort(columns)`: Sort the data by specified columns\n", + "- `join(other, on)`: Join with another DataFrame\n", + "\n", + "**Expressions**\n", + "\n", + "Used to define operations on columns:\n", + "\n", + "- `pl.col(\"column\")`: Reference a column\n", + "- `pl.lit(value)`: Create a literal value\n", + "- `pl.when(predicate).then(value).otherwise(other)`: Conditional expression\n", + "\n", + "**Series Operations**\n", + "\n", + "- `series.sum()`: Calculate sum of series\n", + "- `series.mean()`: Calculate mean of series\n", + "- `series.max()`: Find maximum value in series\n", + "- `series.min()`: Find minimum value in series\n", + "- `series.sort()`: Sort series values\n", + "\n", + "**Data Types**\n", + "\n", + "- `pl.Int64`: 64-bit integer\n", + "- `pl.Float64`: 64-bit float\n", + "- `pl.Utf8`: String\n", + "- `pl.Boolean`: Boolean\n", + "- `pl.Date`: Date\n", + "\n", + "**Utilities**\n", + "\n", + "- `pl.concat([df1, df2])`: Concatenate DataFrames\n", + "- `df.describe()`: Generate summary statistics\n", + "- `df.to_csv(file)`: Write DataFrame to CSV\n", + "- `df.to_parquet(file)`: Write DataFrame to Parquet\n", + "\n", + "The eager API executes operations immediately, providing direct access to results. It's suitable for interactive data exploration and smaller datasets." + ] + }, + { + "cell_type": "markdown", + "id": "d87e5c16-46df-4a5b-8454-64fc7bb761a4", + "metadata": {}, + "source": [ + "### Exercise #1 - Load Data ###\n", + "Load the csv file into a Dataframe using Polars." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "e138575b-8817-44e0-981c-128b07d6bf8f", + "metadata": {}, + "outputs": [], + "source": [ + "import polars as pl\n", + "my_df = pl.read_csv('./data/uk_pop.csv')" + ] + }, + { + "cell_type": "markdown", + "id": "51b230dd-7c17-46fb-be96-1dd11dfbf90d", + "metadata": {}, + "source": [ + "### Exercise #2 - Calculate Average Age of Population ###\n", + "Now, filter for individuals aged 65 and above, and sort by ascending age." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "d22e37d3-a0c0-4b05-891a-c74086a79e59", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "shape: (5, 6)
agesexcountylatlongname
i64strstrf64f64str
0"m""DARLINGTON"54.533644-1.524401"FRANCIS"
0"m""DARLINGTON"54.426256-1.465314"EDWARD"
0"m""DARLINGTON"54.5552-1.496417"TEDDY"
0"m""DARLINGTON"54.547906-1.572341"ANGUS"
0"m""DARLINGTON"54.477639-1.605995"CHARLIE"
" + ], + "text/plain": [ + "shape: (5, 6)\n", + "┌─────┬─────┬────────────┬───────────┬───────────┬─────────┐\n", + "│ age ┆ sex ┆ county ┆ lat ┆ long ┆ name │\n", + "│ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- │\n", + "│ i64 ┆ str ┆ str ┆ f64 ┆ f64 ┆ str │\n", + "╞═════╪═════╪════════════╪═══════════╪═══════════╪═════════╡\n", + "│ 0 ┆ m ┆ DARLINGTON ┆ 54.533644 ┆ -1.524401 ┆ FRANCIS │\n", + "│ 0 ┆ m ┆ DARLINGTON ┆ 54.426256 ┆ -1.465314 ┆ EDWARD │\n", + "│ 0 ┆ m ┆ DARLINGTON ┆ 54.5552 ┆ -1.496417 ┆ TEDDY │\n", + "│ 0 ┆ m ┆ DARLINGTON ┆ 54.547906 ┆ -1.572341 ┆ ANGUS │\n", + "│ 0 ┆ m ┆ DARLINGTON ┆ 54.477639 ┆ -1.605995 ┆ CHARLIE │\n", + "└─────┴─────┴────────────┴───────────┴───────────┴─────────┘" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "my_df = my_df.filter(pl.col('age') <= 65)\n", + "my_df = my_df.sort('age', descending=False)\n", + "my_df.head()" + ] + }, + { + "cell_type": "markdown", + "id": "2af1bb9b-91bc-4665-a371-699b66067cd2", + "metadata": {}, + "source": [ + "### Exercise #3 - Group By and Aggregation ###\n", + "Next, group by county and calculate the total population and average age." + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "191a03a2-f420-4669-96ed-ddb290ce09f7", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "shape: (5, 3)
countypopulationaverage age
stru32f64
"KENT"127099133.024753
"ESSEX"119072633.357306
"HAMPSHIRE"109629033.899235
"BIRMINGHAM"100237629.690752
"HERTFORDSHIRE"99404232.660137
" + ], + "text/plain": [ + "shape: (5, 3)\n", + "┌───────────────┬────────────┬─────────────┐\n", + "│ county ┆ population ┆ average age │\n", + "│ --- ┆ --- ┆ --- │\n", + "│ str ┆ u32 ┆ f64 │\n", + "╞═══════════════╪════════════╪═════════════╡\n", + "│ KENT ┆ 1270991 ┆ 33.024753 │\n", + "│ ESSEX ┆ 1190726 ┆ 33.357306 │\n", + "│ HAMPSHIRE ┆ 1096290 ┆ 33.899235 │\n", + "│ BIRMINGHAM ┆ 1002376 ┆ 29.690752 │\n", + "│ HERTFORDSHIRE ┆ 994042 ┆ 32.660137 │\n", + "└───────────────┴────────────┴─────────────┘" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "by_county = my_df.group_by('county').agg([\n", + " pl.len().alias('population'),\n", + " pl.mean('age').alias('average age'),\n", + "]).sort('population', descending=True)\n", + "by_county.head()" + ] + }, + { + "cell_type": "markdown", + "id": "fb6e2475-3823-43a7-b75b-460e971da230", + "metadata": {}, + "source": [ + "### Exercise #4 - Gender Distribution ###\n", + "Lastly, let's calculate the percentage of males to females in the sample data." + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "id": "1fa57226-f174-4967-a75a-b11d9df8765f", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "shape: (2, 3)
sexlenpercentage
stru32f64
"f"2407637749.763676
"m"2430505150.236324
" + ], + "text/plain": [ + "shape: (2, 3)\n", + "┌─────┬──────────┬────────────┐\n", + "│ sex ┆ len ┆ percentage │\n", + "│ --- ┆ --- ┆ --- │\n", + "│ str ┆ u32 ┆ f64 │\n", + "╞═════╪══════════╪════════════╡\n", + "│ f ┆ 24076377 ┆ 49.763676 │\n", + "│ m ┆ 24305051 ┆ 50.236324 │\n", + "└─────┴──────────┴────────────┘" + ] + }, + "execution_count": 29, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "by_sex = my_df\\\n", + " .group_by('sex')\\\n", + " .agg(pl.len())\\\n", + " .with_columns((pl.col('len') / my_df.shape[0] * 100).alias('percentage'))\n", + "by_sex.head()" + ] + }, + { + "cell_type": "markdown", + "id": "3b44f79a-a542-492d-ae01-b1ee6f9203dc", + "metadata": {}, + "source": [ + "## Lazy Execution ##\n", + "Polars utilizes a technique called lazy execution to perform operations. Unlike eager execution, where operations are performed immediately, Polars defines and stores operations in a computational graph that isn't executed until explicitly required. This allows Polars to optimize the sequence of operations to minimize computation overhead and apply optimization techniques such as: applying filters early (predicate pushdown), selecting only necessary columns (projection pushdown), and executing operations in parallel. To make use of lazy execution in polars, a \"LazyFrame\" data structure is used.\n", + "\n", + "Now, lets run the same operations with lazy execution and visualize the graph!" + ] + }, + { + "cell_type": "markdown", + "id": "0a709c71-6fae-4eca-8236-853e3d97e12a", + "metadata": {}, + "source": [ + "### Polars Lazy Execution API Reference ###\n", + "\n", + "**LazyFrame**\n", + "\n", + "The main entry point for lazy execution in Polars. Created from a DataFrame or data source.\n", + "\n", + "- `pl.LazyFrame(data)`: Create a LazyFrame from data.\n", + "- `df.lazy()`: Convert a DataFrame to LazyFrame.\n", + "\n", + "**Key Methods**\n", + "\n", + "- `filter(predicate)`: Filter rows based on a condition.\n", + "- `select(columns)`: Select specific columns.\n", + "- `with_columns(expressions)`: Add or modify columns.\n", + "- `group_by(columns)`: Group by specified columns.\n", + "- `agg(aggregations)`: Perform aggregations on grouped data.\n", + "- `sort(columns)`: Sort the data by specified columns.\n", + "- `join(other, on)`: Join with another LazyFrame.\n", + "- `collect()`: Execute the lazy query and return a DataFrame.\n", + "\n", + "**Expressions**\n", + "\n", + "Used to define operations on columns:\n", + "\n", + "- `pl.col(\"column\")`: Reference a column.\n", + "- `pl.lit(value)`: Create a literal value.\n", + "- `pl.when(predicate).then(value).otherwise(other)`: Define a conditional expression.\n", + "\n", + "**Execution**\n", + "\n", + "- `collect()`: Execute and return a DataFrame.\n", + "- `fetch(n)`: Execute and return the first n rows.\n", + "- `describe_plan()`: Show the query plan for optimization insights.\n", + "- `explain()`: Explain the query execution process.\n", + "\n", + "**Optimization**\n", + "\n", + "- `cache()`: Cache intermediate results for faster access.\n", + "- `optimize()`: Apply query optimizations to improve performance.\n", + "\n", + "The lazy API allows building complex queries that are optimized before execution, enabling better performance for large datasets." + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "id": "f183fa43-99e2-45dc-930c-44fee40d9135", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "shape: (5, 6)\n", + "┌─────┬─────┬──────────────────────────┬───────────┬───────────┬───────┐\n", + "│ age ┆ sex ┆ county ┆ lat ┆ long ┆ name │\n", + "│ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- │\n", + "│ i64 ┆ str ┆ str ┆ f64 ┆ f64 ┆ str │\n", + "╞═════╪═════╪══════════════════════════╪═══════════╪═══════════╪═══════╡\n", + "│ 1 ┆ f ┆ EAST RIDING OF YORKSHIRE ┆ 53.737344 ┆ -0.638535 ┆ ZYRAH │\n", + "│ 1 ┆ f ┆ SHEFFIELD ┆ 53.35529 ┆ -1.669447 ┆ ZYRAH │\n", + "│ 1 ┆ f ┆ LINCOLNSHIRE ┆ 53.164176 ┆ 0.015812 ┆ ZYRAH │\n", + "│ 1 ┆ f ┆ WORCESTERSHIRE ┆ 52.258629 ┆ -2.31696 ┆ ZYRAH │\n", + "│ 1 ┆ f ┆ HERTFORDSHIRE ┆ 51.731816 ┆ -0.377476 ┆ ZYRAH │\n", + "└─────┴─────┴──────────────────────────┴───────────┴───────────┴───────┘\n", + "Time Taken: 6.8741 seconds\n" + ] + } + ], + "source": [ + "import polars as pl\n", + "import time\n", + "\n", + "start_time = time.time()\n", + "\n", + "# Create a lazy DataFrame\n", + "lazy_df = pl.scan_csv('./data/uk_pop.csv')\n", + "\n", + "# Define the lazy operations\n", + "lazy_result = (\n", + " lazy_df\n", + " .filter(pl.col('age') > 0.0)\n", + " .sort('name', descending=True)\n", + ")\n", + "\n", + "# Execute the lazy query and collect the results\n", + "result = lazy_result.collect()\n", + "\n", + "print(result.head())\n", + "polars_time = time.time() - start_time\n", + "print(f\"Time Taken: {polars_time:.4f} seconds\")" + ] + }, + { + "cell_type": "markdown", + "id": "0ae1e01f-da85-453f-a708-37afca4753f6", + "metadata": {}, + "source": [ + "### Execution Graph ###" + ] + }, + { + "cell_type": "markdown", + "id": "4affd1a7-80b2-4f9b-88d8-26a7008f6dc9", + "metadata": {}, + "source": [ + "Let's see how the unoptimized execution graph looks." + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "id": "d43e9843-ef8e-48aa-b49c-482454d6e93f", + "metadata": {}, + "outputs": [ + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + "polars_query\n", + "\n", + "\n", + "\n", + "p1\n", + "\n", + "SORT BY [col("name")]\n", + "\n", + "\n", + "\n", + "p2\n", + "\n", + "FILTER BY [(col("age").cast(Unknown(Float))) > (dyn float: 0.0)]\n", + "\n", + "\n", + "\n", + "p1--p2\n", + "\n", + "\n", + "\n", + "\n", + "p3\n", + "\n", + "Csv SCAN [./data/uk_pop.csv]\n", + "π */6;\n", + "\n", + "\n", + "\n", + "p2--p3\n", + "\n", + "\n", + "\n", + "" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Show unoptimized Graph\n", + "lazy_result.show_graph(optimized=False)" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "id": "6f5883f3-6238-4f98-970b-f06adabfb50e", + "metadata": {}, + "outputs": [ + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + "polars_query\n", + "\n", + "\n", + "\n", + "p1\n", + "\n", + "SORT BY [col("name")]\n", + "\n", + "\n", + "\n", + "p2\n", + "\n", + "Csv SCAN [./data/uk_pop.csv]\n", + "π */6;\n", + "σ [(col("age").cast(Unknown(Float))) > (dyn float: 0.0)]\n", + "\n", + "\n", + "\n", + "p1--p2\n", + "\n", + "\n", + "\n", + "" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Show optimized Graph\n", + "lazy_result.show_graph(optimized=True)" + ] + }, + { + "cell_type": "markdown", + "id": "7d0f6912-9b19-4473-80d3-4223271efe26", + "metadata": {}, + "source": [ + "As we can see, during execution, Polars ran the age filter in parallel with reading the csv to save time! These type of optimizations is part of the reason why Polars is such a powerful Data Science tool." + ] + }, + { + "cell_type": "markdown", + "id": "5301bcfb-634e-4659-b413-e8279c9bc2ce", + "metadata": {}, + "source": [ + "### Exercise #5 - Creating a Lazy Dataframe ###\n", + "First, let's load the csv as a lazy dataframe." + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "id": "f096b49a-1395-4ddb-bf28-f6e04fb8b4f1", + "metadata": {}, + "outputs": [], + "source": [ + "reg_df = pl.read_csv('./data/uk_pop.csv')\n", + "#lazy_df = pl.scan_csv\n", + "lazy_df = reg_df.lazy()" + ] + }, + { + "cell_type": "markdown", + "id": "3581e118-7902-4d04-b4b4-9887c6ff73ba", + "metadata": {}, + "source": [ + "### Exercise #6 - Query Creation ###\n", + "Now, let's create a query to find the 5 most common names for individuals under 30. " + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "id": "1615956e-c6b5-485c-85d3-c0cf89cedbca", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "shape: (5, 2)
namelen
stru32
"OLIVER"218505
"GEORGE"174261
"HARRY"173862
"OLIVIA"171424
"AMELIA"163302
" + ], + "text/plain": [ + "shape: (5, 2)\n", + "┌────────┬────────┐\n", + "│ name ┆ len │\n", + "│ --- ┆ --- │\n", + "│ str ┆ u32 │\n", + "╞════════╪════════╡\n", + "│ OLIVER ┆ 218505 │\n", + "│ GEORGE ┆ 174261 │\n", + "│ HARRY ┆ 173862 │\n", + "│ OLIVIA ┆ 171424 │\n", + "│ AMELIA ┆ 163302 │\n", + "└────────┴────────┘" + ] + }, + "execution_count": 40, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "most5 = lazy_df\\\n", + " .filter(pl.col('age') < 30)\\\n", + " .group_by('name')\\\n", + " .agg(pl.len())\\\n", + " .sort('len', descending=True)\\\n", + " .limit(5)\n", + "most5.collect()" + ] + }, + { + "cell_type": "markdown", + "id": "07cf331d-0e3d-407c-9990-c7c0873565da", + "metadata": {}, + "source": [ + "## cuDF Polars ##\n", + "cuDF Polars is built directly into the Polars Lazy API. The only requirement is to pass engine=\"gpu\" to the collect operation. Polars also allows defining an instance of the GPU engine for greater customization!" + ] + }, + { + "cell_type": "code", + "execution_count": 42, + "id": "9c6506b1-5c96-4b43-9d53-5826b5a8a44d", + "metadata": {}, + "outputs": [], + "source": [ + "lazy_df = pl.scan_csv('./data/uk_pop.csv').collect(engine=\"gpu\")" + ] + }, + { + "cell_type": "markdown", + "id": "676d16d0-9da0-48a1-a9cc-dc60bde2b223", + "metadata": {}, + "source": [ + "Now let's try defining our own engine object!" + ] + }, + { + "cell_type": "code", + "execution_count": 43, + "id": "66672414-a47d-48c4-9ad9-5329ae159765", + "metadata": {}, + "outputs": [], + "source": [ + "import polars as pl\n", + "import time\n", + "\n", + "gpu_engine = pl.GPUEngine(\n", + " device=0, # This is the default\n", + " raise_on_fail=True, # Fail loudly if we can't run on the GPU.\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 44, + "id": "a939efd7-6c74-456b-baf0-8a2d9905956f", + "metadata": {}, + "outputs": [], + "source": [ + "lazy_df = pl.scan_csv('./data/uk_pop.csv').collect(engine=gpu_engine)" + ] + }, + { + "cell_type": "markdown", + "id": "7eafdfb6-92b0-4f0e-a2ea-90a2ee8a222c", + "metadata": {}, + "source": [ + "Now that the GPU is warmed up, let's try accelerating the same code as before! Notice that we added an engine parameter to the collect call." + ] + }, + { + "cell_type": "markdown", + "id": "52995c50-bf64-44be-a22e-ba41c778f509", + "metadata": {}, + "source": [ + "### Accelerate Previous Code ###" + ] + }, + { + "cell_type": "code", + "execution_count": 45, + "id": "69e5074d-d15b-471a-a9dd-e1f7a52013a5", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "shape: (5, 6)\n", + "┌─────┬─────┬──────────────────────────┬───────────┬───────────┬───────┐\n", + "│ age ┆ sex ┆ county ┆ lat ┆ long ┆ name │\n", + "│ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- │\n", + "│ i64 ┆ str ┆ str ┆ f64 ┆ f64 ┆ str │\n", + "╞═════╪═════╪══════════════════════════╪═══════════╪═══════════╪═══════╡\n", + "│ 1 ┆ f ┆ EAST RIDING OF YORKSHIRE ┆ 53.737344 ┆ -0.638535 ┆ ZYRAH │\n", + "│ 1 ┆ f ┆ SHEFFIELD ┆ 53.35529 ┆ -1.669447 ┆ ZYRAH │\n", + "│ 1 ┆ f ┆ LINCOLNSHIRE ┆ 53.164176 ┆ 0.015812 ┆ ZYRAH │\n", + "│ 1 ┆ f ┆ WORCESTERSHIRE ┆ 52.258629 ┆ -2.31696 ┆ ZYRAH │\n", + "│ 1 ┆ f ┆ HERTFORDSHIRE ┆ 51.731816 ┆ -0.377476 ┆ ZYRAH │\n", + "└─────┴─────┴──────────────────────────┴───────────┴───────────┴───────┘\n", + "Time Taken: 5.9919 seconds\n" + ] + } + ], + "source": [ + "start_time = time.time()\n", + "\n", + "# Create a lazy DataFrame\n", + "lazy_df = pl.scan_csv('./data/uk_pop.csv')\n", + "\n", + "# Define the lazy operations\n", + "lazy_result = (\n", + " lazy_df\n", + " .filter(pl.col('age') > 0.0)\n", + " .sort('name', descending=True)\n", + ")\n", + "\n", + "# Switch to gpu_engine\n", + "result = lazy_result.collect(engine=gpu_engine)\n", + "\n", + "print(result.head())\n", + "polars_time = time.time() - start_time\n", + "print(f\"Time Taken: {polars_time:.4f} seconds\")" + ] + }, + { + "cell_type": "markdown", + "id": "025606e3-9d16-416b-9501-b4cf702fb317", + "metadata": {}, + "source": [ + "### Verify Results Across Engines ###\n", + "How do we know the results are the same with both the CPU and GPU engine? Luckily with Polars, we can execute the same query across both and compare results using the built in testing module! " + ] + }, + { + "cell_type": "code", + "execution_count": 46, + "id": "759ad96e-6bc1-4c79-bc50-ab89430fdb42", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The test frames are equal\n" + ] + } + ], + "source": [ + "from polars.testing import assert_frame_equal\n", + "\n", + "# Run on the CPU\n", + "result_cpu = lazy_result.collect()\n", + "\n", + "# Run on the GPU\n", + "result_gpu = lazy_result.collect(engine=\"gpu\")\n", + "\n", + "# assert both result are equal - Will error if not equal, return None otherwise\n", + "if (assert_frame_equal(result_gpu, result_cpu) == None):\n", + " print(\"The test frames are equal\")" + ] + }, + { + "cell_type": "markdown", + "id": "5b4325cd-0e2d-4cae-8f71-83967708c7b9", + "metadata": {}, + "source": [ + "### Fallback ###\n", + "What happens when an operation isn't supported? " + ] + }, + { + "cell_type": "code", + "execution_count": 47, + "id": "30401f1d-45ad-4365-a00f-e5e79e01412f", + "metadata": {}, + "outputs": [ + { + "ename": "ComputeError", + "evalue": "'cuda' conversion failed: NotImplementedError: ('Query execution with GPU not possible: unsupported operations.\\nThe errors were:\\n- NotImplementedError: rolling mean', [NotImplementedError('rolling mean')])", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mComputeError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[47], line 2\u001b[0m\n\u001b[1;32m 1\u001b[0m result \u001b[38;5;241m=\u001b[39m (\n\u001b[0;32m----> 2\u001b[0m \u001b[43mlazy_df\u001b[49m\n\u001b[1;32m 3\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mwith_columns\u001b[49m\u001b[43m(\u001b[49m\u001b[43mpl\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mcol\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;124;43mage\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[43m)\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mrolling_mean\u001b[49m\u001b[43m(\u001b[49m\u001b[43mwindow_size\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;241;43m7\u001b[39;49m\u001b[43m)\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43malias\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;124;43mage_rolling_mean\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[43m)\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 4\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mfilter\u001b[49m\u001b[43m(\u001b[49m\u001b[43mpl\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mcol\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;124;43mage\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[43m)\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m>\u001b[39;49m\u001b[43m \u001b[49m\u001b[38;5;241;43m0.0\u001b[39;49m\u001b[43m)\u001b[49m\u001b[43m \u001b[49m\n\u001b[1;32m 5\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mcollect\u001b[49m\u001b[43m(\u001b[49m\u001b[43mengine\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mgpu_engine\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 6\u001b[0m )\n\u001b[1;32m 7\u001b[0m \u001b[38;5;28mprint\u001b[39m(result[::\u001b[38;5;241m7\u001b[39m])\n", + "File \u001b[0;32m/opt/conda/lib/python3.10/site-packages/polars/lazyframe/frame.py:2029\u001b[0m, in \u001b[0;36mLazyFrame.collect\u001b[0;34m(self, type_coercion, predicate_pushdown, projection_pushdown, simplify_expression, slice_pushdown, comm_subplan_elim, comm_subexpr_elim, cluster_with_columns, collapse_joins, no_optimization, streaming, engine, background, _eager, **_kwargs)\u001b[0m\n\u001b[1;32m 2027\u001b[0m \u001b[38;5;66;03m# Only for testing purposes\u001b[39;00m\n\u001b[1;32m 2028\u001b[0m callback \u001b[38;5;241m=\u001b[39m _kwargs\u001b[38;5;241m.\u001b[39mget(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mpost_opt_callback\u001b[39m\u001b[38;5;124m\"\u001b[39m, callback)\n\u001b[0;32m-> 2029\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m wrap_df(\u001b[43mldf\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mcollect\u001b[49m\u001b[43m(\u001b[49m\u001b[43mcallback\u001b[49m\u001b[43m)\u001b[49m)\n", + "\u001b[0;31mComputeError\u001b[0m: 'cuda' conversion failed: NotImplementedError: ('Query execution with GPU not possible: unsupported operations.\\nThe errors were:\\n- NotImplementedError: rolling mean', [NotImplementedError('rolling mean')])" + ] + } + ], + "source": [ + "result = (\n", + " lazy_df\n", + " .with_columns(pl.col('age').rolling_mean(window_size=7).alias('age_rolling_mean'))\n", + " .filter(pl.col('age') > 0.0) \n", + " .collect(engine=gpu_engine)\n", + ")\n", + "print(result[::7])" + ] + }, + { + "cell_type": "markdown", + "id": "bad531f3-ecfc-4214-b3c7-6506a3321e11", + "metadata": {}, + "source": [ + "We intially constructed the GPU engine with raise_on_fail=True to ensure all operations ran on GPU. But as we can see, the rolling mean operation is not currently supported, which results in the query not executing. To enable fallback, we can simply change the raise_on_fail parameter to False." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "96b95c34-d3ea-494a-95b9-5d2fd8d49938", + "metadata": {}, + "outputs": [], + "source": [ + "gpu_engine_with_fallback = pl.GPUEngine(\n", + " device=0, # This is the default\n", + " raise_on_fail=False, # Fallback to CPU if we can't run on the GPU (this is the default)\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "a7e15a86-dad3-4b71-8ccb-c03b61a35e3c", + "metadata": {}, + "source": [ + "Now let's try this query again." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "73c30f3c-4075-44aa-8715-648b49fec1e9", + "metadata": {}, + "outputs": [], + "source": [ + "result = (\n", + " lazy_df\n", + " .with_columns(pl.col('age').rolling_mean(window_size=7).alias('age_rolling_mean'))\n", + " .filter(pl.col('age') > 0.0) \n", + " .collect(engine=gpu_engine_with_fallback)\n", + ")\n", + "print(result[::7])" + ] + }, + { + "cell_type": "markdown", + "id": "19be5382-ba87-4bc3-8862-df73049598f7", + "metadata": {}, + "source": [ + "### Exercise #7 - Enable GPU Engine ###\n", + "The below code calculates the average latitude and longitude for each county. Let's try enabling the GPU Engine for this query!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "94243bde-3a3a-4ce9-887c-13d0b0875750", + "metadata": {}, + "outputs": [], + "source": [ + "# Create the lazy query with column pruning\n", + "lazy_query = (\n", + " lazy_df\n", + " .select([\"county\", \"lat\", \"long\"]) # Column pruning: select only necessary columns\n", + " .group_by(\"county\")\n", + " .agg([\n", + " pl.col(\"lat\").mean().alias(\"avg_latitude\"),\n", + " pl.col(\"long\").mean().alias(\"avg_longitude\")\n", + " ])\n", + " .sort(\"county\")\n", + ")\n", + "\n", + "# Execute the query\n", + "result = lazy_query.collect()\n", + "\n", + "print(\"\\nAverage latitude and longitude for each county:\")\n", + "print(result.head()) # Display first few rows" + ] + }, + { + "cell_type": "raw", + "id": "42acb118-cb9d-478f-9636-694a5ddcb071", + "metadata": { + "jupyter": { + "source_hidden": true + } + }, + "source": [ + "\n", + "# Create the lazy query with column pruning\n", + "lazy_query = (\n", + " lazy_df\n", + " .select([\"county\", \"lat\", \"long\"]) # Column pruning: select only necessary columns\n", + " .group_by(\"county\")\n", + " .agg([\n", + " pl.col(\"lat\").mean().alias(\"avg_latitude\"),\n", + " pl.col(\"long\").mean().alias(\"avg_longitude\")\n", + " ])\n", + " .sort(\"county\")\n", + ")\n", + "\n", + "# Execute the query\n", + "result = lazy_query.collect(engine=\"gpu\")\n", + "\n", + "print(\"\\nAverage latitude and longitude for each county:\")\n", + "print(result.head()) # Display first few rows" + ] + }, + { + "cell_type": "markdown", + "id": "b942cf78-b3cc-4fa1-8d62-b355e20dd2ae", + "metadata": {}, + "source": [ + "Click ... for solution. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cf1158c3-8429-4637-a9c2-e8ca91a59965", + "metadata": {}, + "outputs": [], + "source": [ + "import IPython\n", + "app = IPython.Application.instance()\n", + "app.kernel.do_shutdown(True)" + ] + }, + { + "cell_type": "markdown", + "id": "cdecd196-8e0d-4b36-a428-4411bd480778", + "metadata": {}, + "source": [ + "**Well Done!**" + ] + }, + { + "cell_type": "markdown", + "id": "ed99fbcd-6e89-401f-b0cc-0b4fe3f112ad", + "metadata": {}, + "source": [ + "" + ] + } + ], + "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.15" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/ds/25-1/2/1-09_dask-cudf.ipynb b/ds/25-1/2/1-09_dask-cudf.ipynb new file mode 100644 index 0000000..e5e5dc1 --- /dev/null +++ b/ds/25-1/2/1-09_dask-cudf.ipynb @@ -0,0 +1,978 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Fundamentals of Accelerated Data Science # " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Transition Path: cuDF provides a way for users to scale their pandas workflows as data sizes grow, offering a middle ground between single-threaded pandas and distributed computing solutions like Dask or Apache Spark ." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 09 - Introduction to Dask cuDF ##\n", + "\n", + "**Table of Contents**\n", + "
\n", + "[Dask](https://dask.org/) cuDF can be used to distribute dataframe operations to multiple GPUs. In this notebook we will introduce some key Dask concepts, learn how to setup a Dask cluster for utilizing multiple GPUs, and see how to perform simple dataframe operations on distributed Dask dataframes. This notebook covers the below sections: \n", + "1. [An Introduction to Dask](#An-Introduction-to-Dask)\n", + "2. [Setting up a Dask Scheduler](#Setting-up-a-Dask-Scheduler)\n", + " * [Obtaining the Local IP Address](#Obtaining-the-Local-IP-Address)\n", + " * [Starting a `LocalCUDACluster`](#Starting-a-LocalCUDACluster)\n", + " * [Instantiating a Client Connection](#Instantiating-a-Client-Connection)\n", + " * [The Dask Dashboard](#The-Dask-Dashboard)\n", + "3. [Reading Data with Dask cuDF](#Reading-Data-with-Dask-cuDF)\n", + "4. [Computational Graph](#Computational-Graph)\n", + " * [Visualizing the Computational Graph](#Visualizing-the-Computational-Graph)\n", + " * [Extending the Computational Graph](#Extending-the-Computational-Graph)\n", + " * [Computing with the Computational Graph](#Computing-with-the-Computational-Graph)\n", + " * [Persisting Data in the Cluster](#Persisting-Data-in-the-Cluster)\n", + "6. [Initial Data Exploration with Dask cuDF](#Initial-Data-Exploration-with-Dask-cuDF)\n", + " * [Exercise #1 - Counties North of Sunderland with Dask](#Exercise-#1---Counties-North-of-Sunderland-with-Dask)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## An Introduction to Dask ##\n", + "[Dask](https://dask.org/) is a Python library for parallel computing. In Dask programming, we create computational graphs that define code we **would like** to execute, and then, give these computational graphs to a Dask scheduler which evaluates them lazily, and efficiently, in parallel. \n", + "\n", + "In addition to using multiple CPU cores or threads to execute computational graphs in parallel, Dask schedulers can also be configured to execute computational graphs on multiple CPUs, or, as we will do in this workshop, multiple GPUs. As a result, Dask programming facilitates operating on data sets that are larger than the memory of a single compute resource.\n", + "\n", + "Because Dask computational graphs can consist of arbitrary Python code, they provide [a level of control and flexibility superior to many other systems](https://docs.dask.org/en/latest/spark.html) that can operate on massive data sets. However, we will focus for this workshop primarily on the Dask DataFrame, one of several data structures whose operations and methods natively utilize Dask's parallel scheduling:\n", + "* Dask DataFrame, which closely resembles the Pandas DataFrame\n", + "* Dask Array, which closely resembles the NumPy ndarray\n", + "* Dask Bag, a set which allows duplicates and can hold heterogeneously-typed data\n", + "\n", + "In particular, we will use a Dask-cuDF dataframe, which combines the interface of Dask with the GPU power of cuDF for distributed dataframe operations on multiple GPUs. We will now turn our attention to utilizing all 4 NVIDIA V100 GPUs in this environment for operations on an 18GB UK population data set that would not fit into the memory of a single 16GB GPU." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Setting up a Dask Scheduler ##\n", + "We begin by starting a Dask scheduler which will take care to distribute our work across the 4 available GPUs. In order to do this we need to start a `LocalCUDACluster` instance, using our host machine's IP, and then instantiate a client that can communicate with the cluster." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Obtaining the Local IP Address ###" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import subprocess # we will use this to obtain our local IP using the following command\n", + "cmd = \"hostname --all-ip-addresses\"\n", + "\n", + "process = subprocess.Popen(cmd.split(), stdout=subprocess.PIPE)\n", + "output, error = process.communicate()\n", + "IPADDR = str(output.decode()).split()[0]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Starting a `LocalCUDACluster` ###\n", + "`dask_cuda` provides utilities for Dask and CUDA (the \"cu\" in cuDF) interactions." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2025-10-21 13:31:13,108 - distributed.scheduler - WARNING - Removing worker 'tcp://172.18.0.2:44687' caused the cluster to lose already computed task(s), which will be recomputed elsewhere: {('read_csv-910ec886221afde30c768158c33b486c', 16), ('read_csv-910ec886221afde30c768158c33b486c', 67), ('read_csv-910ec886221afde30c768158c33b486c', 0), ('read_csv-910ec886221afde30c768158c33b486c', 41), ('read_csv-910ec886221afde30c768158c33b486c', 54), ('read_csv-910ec886221afde30c768158c33b486c', 9), ('read_csv-910ec886221afde30c768158c33b486c', 38), ('read_csv-910ec886221afde30c768158c33b486c', 5), ('read_csv-910ec886221afde30c768158c33b486c', 34), ('read_csv-910ec886221afde30c768158c33b486c', 12), ('read_csv-910ec886221afde30c768158c33b486c', 2), ('read_csv-910ec886221afde30c768158c33b486c', 27), ('read_csv-910ec886221afde30c768158c33b486c', 62), ('read_csv-910ec886221afde30c768158c33b486c', 46), ('read_csv-910ec886221afde30c768158c33b486c', 30), ('read_csv-910ec886221afde30c768158c33b486c', 59), ('read_csv-910ec886221afde30c768158c33b486c', 23)} (stimulus_id='handle-worker-cleanup-1761053473.108198')\n", + "2025-10-21 13:31:13,110 - distributed.scheduler - WARNING - Removing worker 'tcp://172.18.0.2:35977' caused the cluster to lose already computed task(s), which will be recomputed elsewhere: {('read_csv-910ec886221afde30c768158c33b486c', 29), ('read_csv-910ec886221afde30c768158c33b486c', 48), ('read_csv-910ec886221afde30c768158c33b486c', 32), ('read_csv-910ec886221afde30c768158c33b486c', 10), ('read_csv-910ec886221afde30c768158c33b486c', 51), ('read_csv-910ec886221afde30c768158c33b486c', 25), ('read_csv-910ec886221afde30c768158c33b486c', 60), ('read_csv-910ec886221afde30c768158c33b486c', 44), ('read_csv-910ec886221afde30c768158c33b486c', 14), ('read_csv-910ec886221afde30c768158c33b486c', 57), ('read_csv-910ec886221afde30c768158c33b486c', 18), ('read_csv-910ec886221afde30c768158c33b486c', 8), ('read_csv-910ec886221afde30c768158c33b486c', 66), ('read_csv-910ec886221afde30c768158c33b486c', 21), ('read_csv-910ec886221afde30c768158c33b486c', 36), ('read_csv-910ec886221afde30c768158c33b486c', 4), ('read_csv-910ec886221afde30c768158c33b486c', 55)} (stimulus_id='handle-worker-cleanup-1761053473.1105292')\n", + "2025-10-21 13:31:13,112 - distributed.scheduler - WARNING - Removing worker 'tcp://172.18.0.2:39371' caused the cluster to lose already computed task(s), which will be recomputed elsewhere: {('read_csv-910ec886221afde30c768158c33b486c', 7), ('read_csv-910ec886221afde30c768158c33b486c', 58), ('read_csv-910ec886221afde30c768158c33b486c', 3), ('read_csv-910ec886221afde30c768158c33b486c', 26), ('read_csv-910ec886221afde30c768158c33b486c', 61), ('read_csv-910ec886221afde30c768158c33b486c', 22), ('read_csv-910ec886221afde30c768158c33b486c', 19), ('read_csv-910ec886221afde30c768158c33b486c', 15), ('read_csv-910ec886221afde30c768158c33b486c', 50), ('read_csv-910ec886221afde30c768158c33b486c', 47), ('read_csv-910ec886221afde30c768158c33b486c', 53), ('read_csv-910ec886221afde30c768158c33b486c', 37), ('read_csv-910ec886221afde30c768158c33b486c', 43), ('read_csv-910ec886221afde30c768158c33b486c', 11), ('read_csv-910ec886221afde30c768158c33b486c', 40), ('read_csv-910ec886221afde30c768158c33b486c', 65), ('read_csv-910ec886221afde30c768158c33b486c', 33)} (stimulus_id='handle-worker-cleanup-1761053473.1126676')\n", + "2025-10-21 13:31:13,114 - distributed.scheduler - WARNING - Removing worker 'tcp://172.18.0.2:36291' caused the cluster to lose already computed task(s), which will be recomputed elsewhere: {('read_csv-910ec886221afde30c768158c33b486c', 52), ('read_csv-910ec886221afde30c768158c33b486c', 13), ('read_csv-910ec886221afde30c768158c33b486c', 42), ('read_csv-910ec886221afde30c768158c33b486c', 45), ('read_csv-910ec886221afde30c768158c33b486c', 6), ('read_csv-910ec886221afde30c768158c33b486c', 35), ('read_csv-910ec886221afde30c768158c33b486c', 64), ('read_csv-910ec886221afde30c768158c33b486c', 31), ('read_csv-910ec886221afde30c768158c33b486c', 28), ('read_csv-910ec886221afde30c768158c33b486c', 63), ('read_csv-910ec886221afde30c768158c33b486c', 24), ('read_csv-910ec886221afde30c768158c33b486c', 56), ('read_csv-910ec886221afde30c768158c33b486c', 17), ('read_csv-910ec886221afde30c768158c33b486c', 1), ('read_csv-910ec886221afde30c768158c33b486c', 20), ('read_csv-910ec886221afde30c768158c33b486c', 49), ('read_csv-910ec886221afde30c768158c33b486c', 39), ('read_csv-910ec886221afde30c768158c33b486c', 68)} (stimulus_id='handle-worker-cleanup-1761053473.1145272')\n" + ] + } + ], + "source": [ + "from dask_cuda import LocalCUDACluster\n", + "cluster = LocalCUDACluster(ip=IPADDR)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Instantiating a Client Connection ###\n", + "The `dask.distributed` library gives us distributed functionality, including the ability to connect to the CUDA Cluster we just created. The `progress` import will give us a handy progress bar we can utilize below." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "from dask.distributed import Client, progress\n", + "\n", + "client = Client(cluster)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### The Dask Dashboard" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Dask ships with a very helpful dashboard that in our case runs on port `8787`. Open a new browser tab now and copy this lab's URL into it, replacing `/lab/lab` with `:8787` (so it ends with `.com:8787`). This should open the Dask dashboard, currently idle." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Reading Data with Dask cuDF ##\n", + "With `dask_cudf` we can create a dataframe from several file formats (including from multiple files and directly from cloud storage like S3), from cuDF dataframes, from Pandas dataframes, and even from vanilla CPU Dask dataframes. Here we will create a Dask cuDF dataframe from the local csv file `pop5x_1-07.csv`, which has similar features to the `pop.csv` files you have already been using, except scaled up to 5 times larger (18GB), representing a population of almost 300 million, nearly the size of the entire United States." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "18G data/uk_pop5x.csv\n" + ] + } + ], + "source": [ + "# get the file size of `pop5x_1-07.csv` in GB\n", + "!ls -sh data/uk_pop5x.csv" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We import dask_cudf (and other RAPIDS components when necessary) after setting up the cluster to ensure that they establish correctly inside the CUDA context it creates." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "import dask_cudf" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "ddf = dask_cudf.read_csv('./data/uk_pop5x.csv', dtype=['float32', 'str', 'str', 'float32', 'float32', 'str'])" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/plain": [ + "age float32\n", + "sex object\n", + "county object\n", + "lat float32\n", + "long float32\n", + "name object\n", + "dtype: object" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ddf.dtypes" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Computational Graph ##\n", + "As mentioned above, when programming with Dask, we create computational graphs that we **would eventually like** to be executed. We can already observe this behavior in action: in calling `dask_cudf.read_csv` we have indicated that **would eventually like** to read the entire contents of `pop5x_1-07.csv`. However, Dask will not ask the scheduler execute this work until we explicitly indicate that we would like it do so.\n", + "\n", + "Observe the memory usage for each of the 4 GPUs by executing the following cell, and notice that the GPU memory usage is not nearly large enough to indicate that the entire 18GB file has been read into memory:" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Tue Oct 21 13:29:09 2025 \n", + "+-----------------------------------------------------------------------------+\n", + "| NVIDIA-SMI 525.85.12 Driver Version: 525.85.12 CUDA Version: 12.0 |\n", + "|-------------------------------+----------------------+----------------------+\n", + "| GPU Name Persistence-M| Bus-Id Disp.A | Volatile Uncorr. ECC |\n", + "| Fan Temp Perf Pwr:Usage/Cap| Memory-Usage | GPU-Util Compute M. |\n", + "| | | MIG M. |\n", + "|===============================+======================+======================|\n", + "| 0 Tesla T4 On | 00000000:00:1B.0 Off | 0 |\n", + "| N/A 30C P0 26W / 70W | 14956MiB / 15360MiB | 0% Default |\n", + "| | | N/A |\n", + "+-------------------------------+----------------------+----------------------+\n", + "| 1 Tesla T4 On | 00000000:00:1C.0 Off | 0 |\n", + "| N/A 30C P0 26W / 70W | 168MiB / 15360MiB | 0% Default |\n", + "| | | N/A |\n", + "+-------------------------------+----------------------+----------------------+\n", + "| 2 Tesla T4 On | 00000000:00:1D.0 Off | 0 |\n", + "| N/A 30C P0 26W / 70W | 168MiB / 15360MiB | 0% Default |\n", + "| | | N/A |\n", + "+-------------------------------+----------------------+----------------------+\n", + "| 3 Tesla T4 On | 00000000:00:1E.0 Off | 0 |\n", + "| N/A 29C P0 26W / 70W | 168MiB / 15360MiB | 0% Default |\n", + "| | | N/A |\n", + "+-------------------------------+----------------------+----------------------+\n", + " \n", + "+-----------------------------------------------------------------------------+\n", + "| Processes: |\n", + "| GPU GI CI PID Type Process name GPU Memory |\n", + "| ID ID Usage |\n", + "|=============================================================================|\n", + "+-----------------------------------------------------------------------------+\n" + ] + } + ], + "source": [ + "!nvidia-smi" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Visualizing the Computational Graph ###\n", + "Computational graphs that have not yet been executed provide the `.visualize` method that, when used in a Jupyter environment such as this one, will display the computational graph, including how Dask intends to go about distributing the work. Thus, we can visualize how the `read_csv` operation will be distributed by Dask by executing the following cell:" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "%3\n", + "\n", + "\n", + "\n", + "-6332770613817605186\n", + "\n", + "ReadCSV\n", + "\n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ddf.visualize(format='svg') # This visualization is very large, and using `format='svg'` will make it easier to view." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As you can see, when we indicate for Dask to actually execute this operation, it will parallelize the work across the 4 GPUs in something like 69 parallel partitions. We can see the exact number of partitions with the `npartitions` property:" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/plain": [ + "69" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ddf.npartitions" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Extending the Computational Graph ###\n", + "The concept of constructing computational graphs with arbitrary operations before executing them is a core part of Dask. Let's add some operations to the existing computational graph and visualize it again.\n", + "\n", + "After running the next cell, although it will take some scrolling to get a clear sense of it (the challenges of distributed data analytics!), you can see that the graph already constructed for `read_csv` now continues upward. It selects the `age` column across all partitions (visualized as `getitem`) and eventually performs the `.mean()` reduction (visualized as `series-sum-chunk`, `series-sum-agg`, `count-chunk`, `sum-agg` and `true-div`)." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "%3\n", + "\n", + "\n", + "\n", + "2336549067836068764\n", + "\n", + "Sum(Projection)\n", + "\n", + "\n", + "\n", + "553658985626135620\n", + "\n", + "Projection(ReadCSV, age)\n", + "\n", + "\n", + "\n", + "553658985626135620->2336549067836068764\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "-6332770613817605186\n", + "\n", + "ReadCSV\n", + "\n", + "\n", + "\n", + "-6332770613817605186->553658985626135620\n", + "\n", + "\n", + "\n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "mean_age = ddf['age'].sum()\n", + "mean_age.visualize(format='svg')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Computing with the Computational Graph ###\n", + "There are several ways to indicate to Dask that we would like to perform the computations described in the computational graphs we have constructed. The first we will show is the `.compute` method, which will return the output of the computation as an object in one GPU's memory - no longer distributed across GPUs.\n", + "\n", + "**NOTE**: This value is actually a [*future*](https://docs.python.org/3/library/concurrent.futures.html) that it can be immediately used in code, even before it completes evaluating. While this can be tremendously useful in many scenarios, we will not need in this workshop to do anything fancy with the futures we generate except to wait for them to evaluate so we can visualize their values.\n", + "\n", + "Below we send the computational graph we have created to the Dask scheduler to be executed in parallel on our 4 GPUs. If you have the Dask Dashboard open on another tab from before, you can watch it while the operation completes. Because our graph involves reading the entire 18GB data set (as we declared when adding `read_csv` to the call graph), you can expect the operation to take a little time. If you closely watch the dashboard, you will see that Dask begins follow-on calculations for `mean` even while data is still being read into memory." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "11732293000.0" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "mean_age.compute()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Persisting Data in the Cluster ###\n", + "As you can see, the previous operation, which read the entire 18GB csv into the GPUs' memory, did not retain the data in memory after completing the computational graph:" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Tue Oct 21 13:31:04 2025 \n", + "+-----------------------------------------------------------------------------+\n", + "| NVIDIA-SMI 525.85.12 Driver Version: 525.85.12 CUDA Version: 12.0 |\n", + "|-------------------------------+----------------------+----------------------+\n", + "| GPU Name Persistence-M| Bus-Id Disp.A | Volatile Uncorr. ECC |\n", + "| Fan Temp Perf Pwr:Usage/Cap| Memory-Usage | GPU-Util Compute M. |\n", + "| | | MIG M. |\n", + "|===============================+======================+======================|\n", + "| 0 Tesla T4 On | 00000000:00:1B.0 Off | 0 |\n", + "| N/A 30C P0 26W / 70W | 14094MiB / 15360MiB | 0% Default |\n", + "| | | N/A |\n", + "+-------------------------------+----------------------+----------------------+\n", + "| 1 Tesla T4 On | 00000000:00:1C.0 Off | 0 |\n", + "| N/A 30C P0 26W / 70W | 690MiB / 15360MiB | 0% Default |\n", + "| | | N/A |\n", + "+-------------------------------+----------------------+----------------------+\n", + "| 2 Tesla T4 On | 00000000:00:1D.0 Off | 0 |\n", + "| N/A 30C P0 26W / 70W | 690MiB / 15360MiB | 0% Default |\n", + "| | | N/A |\n", + "+-------------------------------+----------------------+----------------------+\n", + "| 3 Tesla T4 On | 00000000:00:1E.0 Off | 0 |\n", + "| N/A 29C P0 26W / 70W | 690MiB / 15360MiB | 0% Default |\n", + "| | | N/A |\n", + "+-------------------------------+----------------------+----------------------+\n", + " \n", + "+-----------------------------------------------------------------------------+\n", + "| Processes: |\n", + "| GPU GI CI PID Type Process name GPU Memory |\n", + "| ID ID Usage |\n", + "|=============================================================================|\n", + "+-----------------------------------------------------------------------------+\n" + ] + } + ], + "source": [ + "!nvidia-smi" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "A typical Dask workflow, which we will utilize, is to persist data we would like to work with to the cluster and then perform fast operations on that persisted data. We do this with the `.persist` method. From the [Dask documentation](https://distributed.dask.org/en/latest/manage-computation.html#client-persist):\n", + "\n", + ">The `.persist` method submits the task graph behind the Dask collection to the scheduler, obtaining Futures for all of the top-most tasks (for example one Future for each Pandas [*or cuDF*] DataFrame in a Dask[*-cudf*] DataFrame). It then returns a copy of the collection pointing to these futures instead of the previous graph. This new collection is semantically equivalent but now points to actively running data rather than a lazy graph.\n", + "\n", + "Below we persist `ddf` to the cluster so that it will reside in GPU memory for us to perform fast operations on. " + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [], + "source": [ + "ddf = ddf.persist()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As you can see by executing `nvidia-smi` (after letting the `persist` finish), each GPU now has parts of the distributed dataframe in its memory:" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Tue Oct 21 13:31:08 2025 \n", + "+-----------------------------------------------------------------------------+\n", + "| NVIDIA-SMI 525.85.12 Driver Version: 525.85.12 CUDA Version: 12.0 |\n", + "|-------------------------------+----------------------+----------------------+\n", + "| GPU Name Persistence-M| Bus-Id Disp.A | Volatile Uncorr. ECC |\n", + "| Fan Temp Perf Pwr:Usage/Cap| Memory-Usage | GPU-Util Compute M. |\n", + "| | | MIG M. |\n", + "|===============================+======================+======================|\n", + "| 0 Tesla T4 On | 00000000:00:1B.0 Off | 0 |\n", + "| N/A 32C P0 33W / 70W | 14218MiB / 15360MiB | 46% Default |\n", + "| | | N/A |\n", + "+-------------------------------+----------------------+----------------------+\n", + "| 1 Tesla T4 On | 00000000:00:1C.0 Off | 0 |\n", + "| N/A 32C P0 32W / 70W | 3768MiB / 15360MiB | 19% Default |\n", + "| | | N/A |\n", + "+-------------------------------+----------------------+----------------------+\n", + "| 2 Tesla T4 On | 00000000:00:1D.0 Off | 0 |\n", + "| N/A 31C P0 32W / 70W | 3804MiB / 15360MiB | 24% Default |\n", + "| | | N/A |\n", + "+-------------------------------+----------------------+----------------------+\n", + "| 3 Tesla T4 On | 00000000:00:1E.0 Off | 0 |\n", + "| N/A 31C P0 32W / 70W | 3764MiB / 15360MiB | 45% Default |\n", + "| | | N/A |\n", + "+-------------------------------+----------------------+----------------------+\n", + " \n", + "+-----------------------------------------------------------------------------+\n", + "| Processes: |\n", + "| GPU GI CI PID Type Process name GPU Memory |\n", + "| ID ID Usage |\n", + "|=============================================================================|\n", + "+-----------------------------------------------------------------------------+\n" + ] + } + ], + "source": [ + "!nvidia-smi" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Running `ddf.visualize` now shows that we no longer have operations in our task graph, only partitions of data, ready for us to perform operations:" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "%3\n", + "\n", + "\n", + "\n", + "-4538719848559110466\n", + "\n", + "FromGraph\n", + "\n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ddf.visualize(format='svg')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Computing operations on this data will now be much faster:" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/plain": [ + "40.1241924549316" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ddf['age'].mean().compute()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Initial Data Exploration with Dask cuDF ##\n", + "The beauty of Dask is that working with your data, even though it is distributed and massive, is a lot like working with smaller in-memory data sets." + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
agesexcountylatlongname
00.0mDarlington54.549641-1.493884HARRISON
10.0mDarlington54.523945-1.401142LAKSH
20.0mDarlington54.561127-1.690068MUHAMMAD
30.0mDarlington54.542988-1.543216GRAYSON
40.0mDarlington54.532101-1.569116FINLAY
\n", + "
" + ], + "text/plain": [ + " age sex county lat long name\n", + "0 0.0 m Darlington 54.549641 -1.493884 HARRISON\n", + "1 0.0 m Darlington 54.523945 -1.401142 LAKSH\n", + "2 0.0 m Darlington 54.561127 -1.690068 MUHAMMAD\n", + "3 0.0 m Darlington 54.542988 -1.543216 GRAYSON\n", + "4 0.0 m Darlington 54.532101 -1.569116 FINLAY" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ddf.head() # As a convenience, no need to `.compute` the `head()` method" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "age 292399470\n", + "sex 292399470\n", + "county 292399470\n", + "lat 292399470\n", + "long 292399470\n", + "name 292399470\n", + "dtype: int64" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ddf.count().compute()" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "age float32\n", + "sex object\n", + "county object\n", + "lat float32\n", + "long float32\n", + "name object\n", + "dtype: object" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ddf.dtypes" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Exercise #1 - Counties North of Sunderland with Dask ###\n", + "Here we ask you to revisit an earlier exercise, but on the distributed data set. Hopefully, it's clear how similar the code is for single-GPU dataframes and distributed dataframes with Dask.\n", + "\n", + "Identify the latitude of the northernmost resident of Sunderland county (the person with the maximum `lat` value), and then determine which counties have any residents north of this resident. Use the `unique` method of a cudf `Series` to de-duplicate the result.\n", + "\n", + "**Instructions**:
\n", + "* Modify the `` only and execute the below cell to identify counties north of Sunderland. " + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "ename": "NameError", + "evalue": "name 'ddf' is not defined", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[1], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m sunderland_residents \u001b[38;5;241m=\u001b[39m \u001b[43mddf\u001b[49m\u001b[38;5;241m.\u001b[39mloc[[\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mcounty\u001b[39m\u001b[38;5;124m'\u001b[39m], [\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mSUNDERLAND\u001b[39m\u001b[38;5;124m'\u001b[39m]]\n\u001b[1;32m 2\u001b[0m northmost_sunderland_lat \u001b[38;5;241m=\u001b[39m sunderland_residents[\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mlat\u001b[39m\u001b[38;5;124m'\u001b[39m]\u001b[38;5;241m.\u001b[39mmax()\n\u001b[1;32m 3\u001b[0m counties_with_pop_north_of \u001b[38;5;241m=\u001b[39m ddf\u001b[38;5;241m.\u001b[39mloc[ddf[\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mlat\u001b[39m\u001b[38;5;124m'\u001b[39m] \u001b[38;5;241m>\u001b[39m northmost_sunderland_lat][\u001b[38;5;124m'\u001b[39m\u001b[38;5;124mcounty\u001b[39m\u001b[38;5;124m'\u001b[39m]\u001b[38;5;241m.\u001b[39munique()\n", + "\u001b[0;31mNameError\u001b[0m: name 'ddf' is not defined" + ] + } + ], + "source": [ + "sunderland_residents = ddf.loc[['county'], ['SUNDERLAND']]\n", + "northmost_sunderland_lat = sunderland_residents['lat'].max()\n", + "counties_with_pop_north_of = ddf.loc[ddf['lat'] > northmost_sunderland_lat]['county'].unique()\n", + "results=counties_with_pop_north_of.compute()\n", + "results.head()" + ] + }, + { + "cell_type": "raw", + "metadata": { + "jupyter": { + "source_hidden": true + } + }, + "source": [ + "\n", + "sunderland_residents = ddf.loc[ddf['county'] == 'Sunderland']\n", + "northmost_sunderland_lat = sunderland_residents['lat'].max()\n", + "counties_with_pop_north_of = ddf.loc[ddf['lat'] > northmost_sunderland_lat]['county'].unique()\n", + "results=counties_with_pop_north_of.compute()\n", + "results.head()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Click ... for solution. " + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'status': 'ok', 'restart': True}" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import IPython\n", + "app = IPython.Application.instance()\n", + "app.kernel.do_shutdown(True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Well Done!** Let's move to the [next notebook](1-09_cudf-polars.ipynb). " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "" + ] + } + ], + "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.15" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/ds/25-1/2/county_centroid.csv b/ds/25-1/2/county_centroid.csv new file mode 100644 index 0000000..8ceea83 --- /dev/null +++ b/ds/25-1/2/county_centroid.csv @@ -0,0 +1,172 @@ +county,lat_county_center,long_county_center +BARKING AND DAGENHAM,51.621048311776526,0.12958319845588165 +BARNET,51.81255163972051,-0.21821206632197684 +BARNSLEY,53.57190690010971,-1.5487193565226611 +BATH AND NORTH EAST SOMERSET,51.35496548780361,-2.486675162410336 +BEDFORD,52.145475839485385,-0.4549734374180617 +BEXLEY,51.33625605642689,0.14633321710015448 +BIRMINGHAM,52.12178304394528,-1.881329432771379 +BLACKBURN WITH DARWEN,53.63718763008419,-2.463700844959783 +BLACKPOOL,53.882118373353435,-3.0229009637127167 +BLAENAU GWENT,51.75159582861159,-3.1862426125686745 +BOLTON,53.73813128127497,-2.4794091133678147 +BRACKNELL FOREST,51.457925145468295,-0.7336441271286038 +BRADFORD,53.972113267048044,-1.8738762931122748 +BRENT,51.761695309784,-0.2756927203781798 +BRIDGEND,51.522888539164526,-3.6137468421270604 +BRIGHTON AND HOVE,50.94890407892698,-0.1507807253912774 +"BRISTOL, CITY OF",51.53203785026057,-2.5774864859032594 +BROMLEY,51.2251371203518,0.03905163114984023 +BUCKINGHAMSHIRE,51.92925587759856,-0.8053996183750294 +BURY,53.61553432785575,-2.3088650595977023 +CAERPHILLY,51.62781255006381,-3.1973649865483735 +CALDERDALE,53.769761331289686,-1.9616103771384508 +CAMBRIDGESHIRE,52.1333820427886,-0.23503728806014595 +CAMDEN,51.69346289078886,-0.1629412552292679 +CARDIFF,51.56635588939404,-3.222317281083218 +CARMARTHENSHIRE,51.92106862577838,-4.211293704149962 +CENTRAL BEDFORDSHIRE,51.99983427713095,-0.4775810785914261 +CEREDIGION,52.297905934896974,-3.9524382809074967 +CHESHIRE EAST,53.209779668583735,-2.2923524120906538 +CHESHIRE WEST AND CHESTER,53.12468649229667,-2.703640874356098 +CITY OF LONDON,51.515869084539396,-0.09345024349003202 +CONWY,53.125451225027945,-3.7469275629154897 +CORNWALL,50.2491094902892,-4.642072961722217 +COUNTY DURHAM,54.46928915708376,-1.840983172985692 +COVENTRY,52.20619163815314,-1.5190329484575433 +CROYDON,51.33122440611814,-0.07773715861848832 +CUMBRIA,54.470582575648244,-2.902600383252353 +DARLINGTON,54.51355967194039,-1.5680201999230523 +DENBIGHSHIRE,53.07313542431554,-3.347662396412462 +DERBY,52.98317870391253,-1.471762916352353 +DERBYSHIRE,52.96237103431297,-1.6019383162802616 +DEVON,50.75993290464059,-3.6572707805745353 +DONCASTER,53.579077870304175,-1.1091519021581622 +DORSET,50.80117614559981,-2.4141088997141975 +DUDLEY,52.466075739334926,-2.101688961593882 +EALING,51.69946371446451,-0.31413253292570953 +EAST RIDING OF YORKSHIRE,53.9506321883079,-0.6619808168243948 +EAST SUSSEX,50.8319515317622,0.33441692286193403 +ENFIELD,51.79829813489722,-0.08133941451400101 +ESSEX,51.61177562858481,0.5408806396014519 +FLINTSHIRE,53.18448452051185,-3.176529270275655 +GATESHEAD,54.984104331680726,-1.6867966327256207 +GLOUCESTERSHIRE,51.95116469210396,-2.152140175011601 +GREENWICH,51.298529627584855,0.05009798110429057 +GWYNEDD,52.90798692199907,-3.815807248465912 +HACKNEY,51.715573990309835,-0.06047668080560671 +HALTON,53.37945371869939,-2.6885285111965866 +HAMMERSMITH AND FULHAM,51.45669431471315,-0.21734862391196488 +HAMPSHIRE,51.35882747857323,-1.2472236572124424 +HARINGEY,51.71488485869694,-0.10670896820865851 +HARROW,51.69502976226169,-0.3360141730528605 +HARTLEPOOL,54.67019690697325,-1.2702881849113061 +HAVERING,51.68803382335829,0.23538931286606415 +"HEREFORDSHIRE, COUNTY OF",52.05661428266539,-2.7394973894756567 +HERTFORDSHIRE,51.97545351306396,-0.2768104374496038 +HILLINGDON,51.67744993832507,-0.44168376669816023 +HOUNSLOW,51.31550103034914,-0.37851470463324743 +ISLE OF ANGLESEY,53.27637540915653,-4.323495411729392 +ISLE OF WIGHT,50.62684579406237,-1.3335589426514434 +ISLES OF SCILLY,49.923857744201605,-6.302263516809768 +ISLINGTON,51.66454658738323,-0.10992970115558956 +KENSINGTON AND CHELSEA,51.49977592399342,-0.18981078381787103 +KENT,51.066980402556894,0.72177006521006 +"KINGSTON UPON HULL, CITY OF",53.894135701816644,-0.30380941990063115 +KINGSTON UPON THAMES,51.42789080754545,-0.28368404321251495 +KIRKLEES,53.84779145117579,-1.7808194218728275 +KNOWSLEY,53.48284092504563,-2.8329791954991275 +LAMBETH,51.252923290285565,-0.11380231585035454 +LANCASHIRE,53.39410422518683,-2.460896340904076 +LEEDS,53.55494339794778,-1.5074406609781625 +LEICESTER,52.7035904712036,-1.1304165681356237 +LEICESTERSHIRE,52.372384242153444,-1.3774821236258858 +LEWISHAM,51.26146486742923,-0.017302263531446847 +LINCOLNSHIRE,53.019325697607805,-0.23840017404638325 +LIVERPOOL,53.51161042331058,-2.9133522899513755 +LUTON,51.96794156247519,-0.4231450525783596 +MANCHESTER,53.618174414336764,-2.2337215842169944 +MEDWAY,51.32754494250598,0.5632336335498731 +MERTHYR TYDFIL,51.749169200604825,-3.36403864047987 +MERTON,51.37364806533906,-0.18868296177359278 +MIDDLESBROUGH,54.5098082464691,-1.211038279554591 +MILTON KEYNES,52.01693552290149,-0.7406232665194876 +MONMOUTHSHIRE,51.78143655329183,-2.9039386644643197 +NEATH PORT TALBOT,51.59538437854254,-3.7458617902677283 +NEWCASTLE UPON TYNE,55.00208530426788,-1.652806624671881 +NEWHAM,51.75154898367921,0.027418339450078835 +NEWPORT,51.53253056059282,-2.8977514562758477 +NORFOLK,52.3032223796034,0.9647662889518414 +NORTH EAST LINCOLNSHIRE,53.50967645052903,-0.13922750148994814 +NORTH LINCOLNSHIRE,53.57540769163687,-0.5237063875323392 +NORTH SOMERSET,51.35265217208383,-2.754333708085771 +NORTH TYNESIDE,55.00390319683472,-1.5092377782362794 +NORTH YORKSHIRE,54.037083506236726,-1.5496083229591298 +NORTHAMPTONSHIRE,52.090056204873584,-0.8673643733062965 +NORTHUMBERLAND,55.268382697315424,-2.075107564148198 +NOTTINGHAM,52.95517248670217,-1.166635297324727 +NOTTINGHAMSHIRE,53.03298887412134,-1.006945929298795 +OLDHAM,53.659965283524954,-2.052688245629671 +OXFORDSHIRE,51.93769526591072,-1.2911207463303098 +PEMBROKESHIRE,51.87232817560273,-4.908191395785854 +PETERBOROUGH,52.62511626981561,-0.2689975241368676 +PLYMOUTH,50.29446598251615,-4.112955625237552 +PORTSMOUTH,50.91433206435089,-1.0702659081823802 +POWYS,52.35028728472521,-3.4364646802117074 +READING,51.48972751726377,-0.9907195716377762 +REDBRIDGE,51.74619394585629,0.0701000048233879 +REDCAR AND CLEVELAND,54.52674848959172,-1.0057471172413288 +RICHMOND UPON THAMES,51.40228740909276,-0.28924251316631455 +ROCHDALE,53.67734692115036,-2.14815188340053 +ROTHERHAM,53.27571588878268,-1.2866084213986422 +RUTLAND,52.66741819281054,-0.6255844565552813 +SALFORD,53.39900474827836,-2.3848977331687684 +SANDWELL,52.58696674791831,-2.007627650605722 +SEFTON,53.41754419091054,-2.9918998460398845 +SHEFFIELD,53.594572416421464,-1.5427564265432459 +SHROPSHIRE,52.68421414164122,-2.7366875706426375 +SLOUGH,51.500375556628576,-0.5761037634462686 +SOLIHULL,52.36591301434561,-1.7157174664625492 +SOMERSET,51.15203995716832,-3.2953379430424437 +SOUTH GLOUCESTERSHIRE,51.619868102630875,-2.469430184260059 +SOUTH TYNESIDE,54.994706019365786,-1.4469508035803413 +SOUTHAMPTON,50.984805930473584,-1.4002768042215858 +SOUTHEND-ON-SEA,51.562157807336284,0.7069905953535786 +SOUTHWARK,51.26247572937943,-0.07306483663823536 +ST. HELENS,53.442240723358644,-2.7032424159534347 +STAFFORDSHIRE,52.54946704767607,-2.027491119365553 +STOCKPORT,53.243567817667724,-2.1248973952531918 +STOCKTON-ON-TEES,54.60356568786033,-1.3063893005278557 +STOKE-ON-TRENT,53.0018684063432,-2.1588155163720084 +SUFFOLK,52.07327606663186,1.049040133490474 +SUNDERLAND,54.95658521287448,-1.433572135990224 +SURREY,51.75817482314145,-0.3386369800762059 +SUTTON,51.33189096687447,-0.17228958486126392 +SWANSEA,51.734320352502984,-3.967180818043868 +SWINDON,51.64295753076632,-1.7336382187066433 +TAMESIDE,53.4185402114593,-2.0769462404028474 +TELFORD AND WREKIN,52.709149095326744,-2.4894724871905916 +THURROCK,51.508227793073466,0.33492786371540356 +TORBAY,50.494049197230815,-3.5551646045072913 +TORFAEN,51.69896506141925,-3.0509328418360218 +TOWER HAMLETS,51.68485859523772,-0.03638140322291906 +TRAFFORD,53.314621144815334,-2.3656560688750687 +VALE OF GLAMORGAN,51.477096810804674,-3.3980039155600954 +WAKEFIELD,53.81677380462442,-1.4208545508030999 +WALSALL,52.742742908764974,-1.9703315889024553 +WALTHAM FOREST,51.723501987712325,-0.01886180175957716 +WANDSWORTH,51.24653418036352,-0.2001743797936436 +WARRINGTON,53.338554119123636,-2.561564052456012 +WARWICKSHIRE,52.04847200574421,-1.5686356193411675 +WEST BERKSHIRE,51.472960442069805,-1.2740171035533379 +WEST SUSSEX,51.11473921001523,-0.4593527537340543 +WESTMINSTER,51.613346179755915,-0.15298252171750404 +WIGAN,53.58763891955546,-2.5723844100365545 +WILTSHIRE,51.48575283497703,-1.926537553406791 +WINDSOR AND MAIDENHEAD,51.494612540256846,-0.6753936432282348 +WIRRAL,53.237217504292545,-3.0650813262796417 +WOKINGHAM,51.45966460093226,-0.8993706058495408 +WOLVERHAMPTON,52.71684834050869,-2.127594624973283 +WORCESTERSHIRE,52.05799103802506,-2.209184250840713 +WREXHAM,53.00080440180421,-2.991958507191866 +YORK,53.99232942499273,-1.073788787620359