From eb8fd0d70b10d3b419bf50c03eb27a05aac1d2c5 Mon Sep 17 00:00:00 2001 From: Dimitris Poulopoulos Date: Thu, 28 Sep 2023 08:54:03 +0000 Subject: [PATCH 1/7] feast: Add a README File Add a fresh README that: - Describes the tutorial's focus. - Outlines prerequisites for getting started. - Guides users on execution. - Includes a references section. Signed-off-by: Dimitris Poulopoulos --- tutorials/feast/README.md | 40 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 tutorials/feast/README.md diff --git a/tutorials/feast/README.md b/tutorials/feast/README.md new file mode 100644 index 00000000..d4f2032c --- /dev/null +++ b/tutorials/feast/README.md @@ -0,0 +1,40 @@ +# Bike Sharing (Feast) + +This tutorial explores how to leverage Feast for generating training data and enhancing online model inference. In this +use-case, your goal is to train a ride-sharing driver satisfaction prediction model using a training dataset built +using Feast. + +1. [What You'll Need](#what-youll-need) +1. [Procedure](#procedure) +1. [References](#references) + +## What You'll Need + +For this tutorial, ensure you have: + +- Access to an HPE Ezmeral Unified Analytics cluster. + +## Procedure + +To complete the tutorial follow the steps below: + +1. Login to your Ezmeral Unified Analytics (EzUA) cluster, using your credentials. +1. Create a new Notebook server using the `jupyter-data-science` image. Request at least `4Gi` of memory for the + Notebook server. +1. Connect to the Notebook server and clone the repository locally. +1. Navigate to the tutorial's directory (ezua-tutorials/tutorials/feast). +1. Lauch a new terminal window and create a new conda environment using the specified `environment.yaml` file: + ``` + conda env create -f environment.yaml + ``` +1. Add the new conda environment as an ipykernel: + ``` + python -m ipykernel install --user --name=ride-sharing + ``` +1. Refresh your browser tab to access the updated environment. +1. Launch the `ride-sharing.ipynb` Notebook and execute the code cells. Make sure to select the `ride-sharing` + environment kernel. + +## References + +1. [Feast: Open Source Feature Store for Production ML](https://feast.dev/) \ No newline at end of file From da01d6a2bc6a6102fe3f6b395a8d792273b246f7 Mon Sep 17 00:00:00 2001 From: Dimitris Poulopoulos Date: Mon, 2 Oct 2023 09:36:03 +0000 Subject: [PATCH 2/7] wind-turbine: Enhance the Notebook user experience Enhance the Notebook user experience by: - Introducing a code cell to upload the dataset to its appropriate path prior to its use inside the Spark interactive session. This fixes the error where Spark tries to load the dataset from a location that does not exist. - Refining Notebook annotations for a clearer tutorial flow. Closes #64 Signed-off-by: Dimitris Poulopoulos --- .../images/spark-interactive-ui.png | Bin 0 -> 47858 bytes demos/wind-turbine/wind-turbine.ipynb | 190 ++++++++++-------- 2 files changed, 106 insertions(+), 84 deletions(-) create mode 100644 demos/wind-turbine/images/spark-interactive-ui.png diff --git a/demos/wind-turbine/images/spark-interactive-ui.png b/demos/wind-turbine/images/spark-interactive-ui.png new file mode 100644 index 0000000000000000000000000000000000000000..39512b38035cde8d35fda7e794a27ba8899ac49e GIT binary patch literal 47858 zcmcG$cUaR|w=d4;s`^s8k6MMF^b` zN+!^gKj;n#z_PYR%M3B3P)3Ak(cH(y!znK|B@e|cOnzQV_c zLmu97Ilz1W+x;810epN%o4@`0r43SWkB_fD`sTGO7NL%dyMmckL0Os{qA{YpM%}%p zd8xF#Jzd`UCjM8Z2eDv%wEO|);vP1#Uz%U`7vF%-ZT#padklSkQN)^Fh>moMTlsqUqJ{_{0@vuWYV`Z-z?r{TS%&lA=01v&aS3 z{9t+acG8EyTW&uD2rC&h1ViS!Tq|K;lG?|BDeZfrdoa=SkpHZxBzvTdHhtBbo;r)L zQ7BbA*TiN%Uy&g>EY;vZOm*lmGImtyF3`xs;)k5e4SDPO{4t5n>;2ReK5veLVyVJD z;XOY5f`4MdTi-t1Pp=vKB$+_hPeaXuSlklS(E0u45qP4Uu$TPJ{ryfP8{+HsS^8aVT{TEI99qFTj^0OWyc#itc6Or`CHhKrL~aH z$je`Sbo#7a7|O!gPPZASk7jLefMT=SIzzYfJ+3K4LNZ_QW?9@#gjqUhRH&Q{U6xgO zVMSbTzOG`+p4eU-f2s}g=Y7~aRqsJTkm^79y& zXD zQdrfb{&&od%kg0)2Mgc9YJDFnXBIe2O^dTt z{g6V*eXC(PE$uZyBgIvE1)qiYdw>sF!~C5^f+m5u`FubE8Z$NZlJvgfl!ov^BX>Mtqdr=KURAF!Vg#n6{Wq8@1^B84_sb1B)Esi&vGsfaP$(p&$4jPqsBz&6Q_pi( zA!HWb<-6f1-0|JD1)K4o4RzMwkP*9NUk5LqKRIO%Ufkeck-2L6^Z<7aL5iDoO6IR# zao<^g;>Kk8efg?h|LV*jCUy*e1zbndxcS;QqiLaxkdb!53+?aJXr6D;`+U*dfj7*A~T7$yxXrX#QXH z)?^~lcUzIw2`uz2*y0w2X!a~Qi|DeuK zZaixMY1#pLG)oi52DE>j)2SGOz5MKiu?gB*`syS2&D)I(HHZV5qBsR^wgWG8HEJC@vo{puzGAZ&Nzzz8LA_q)#6vgvrkOOyQ)ghU= zGYRG-@aek%yllMUmZg1ak=FpnYvKyOgoVNcsbN-`h9MQxn&bh(L50#q{JHGGl&{)g z^GN0J$K}B?Ng1M(V$t3{b^!rr_2G+HIsy`^=QM)wj97o+x;ppwT5!taPeEHDyZ?v^ zEx7i<7v2;u?M@t)2{|lVQYNd%4X}@XyQUoP8ZtAJ`hD+Mm+LC<71*&`%dDxMxWYpM z$QD~Cg7i@qo7f$#Q-(A@i;#2t(<;Q0(6*H0Up}T$P@24N%t8+pb!uFiaiC(_)xv6G zNt<(ish(c6J>sI5+@G5JN<*OVL|E^uE2RF?yjL0r=w6=UkG4*^C=yad&c@h^+ciC` zpduvgZf``b(aZd0;iz-izdFg-dYF4Lqr%-o9%bW~e*`hptVpTxs}JFsQrC!;#>p(f zM$74R%g2}izoFZK3-g^b;9CQu)%SK$!z3{;O8azaStDb2yq;D$1eaw{hNP1{ny~Av z1QcyyjVQi=ny0<7pFr(!^djI30iJL{0u&hstF%1XJi6M?W)3xY&2?PIIGrh7Hkxjf z?~uPwflc7IO5V6<6VIYfOL=<}?zh=BVoR~O#;w)KMZHUJs&WH&o~OC7)u?UlGw&&0 zCWKWnPgp`Jm7AE&e$hTNFi=iVH#_=Gc`SfM5fjUEOReRU)Wo_*%j%nT^Qs#L6sw|K z;{!e^#!_j`Ei&kci{jQrw`|zirfrMS_WI(E8zVWPB{bFL)!DZm4#dCC+T-wHVUrRw zi`(n4)s5r3=Qq3a9UyhgSW~1N>shA+q$OdN`CQXX*EZc
Zs)0ssLtkl8 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\n", "

(Image generated by Stable Diffusion)

\n", "\n", "\n", + "Wind turbines hold tremendous potential as a sustainable source of energy, capable of supplying a substantial portion of\n", + "the world's power needs. However, the inherent unpredictability of power generation poses a challenge when it comes to\n", + "optimizing this process.\n", "\n", - "Wind turbines hold tremendous potential as a sustainable source of energy,\n", - "capable of supplying a substantial portion of the world's power needs. However,\n", - "the inherent unpredictability of power generation poses a challenge when it\n", - "comes to optimizing this process.\n", + "Fortunately, you have a powerful tool at your disposal: Machine Learning. By leveraging advanced algorithms and data\n", + "analysis, you can develop models that accurately predict the power production of wind turbines. This enables you to\n", + "optimize the power generation process and overcome the challenges associated with its ingrained variability.\n", "\n", - "Fortunately, we have a powerful tool at our disposal: machine learning. By\n", - "leveraging advanced algorithms and data analysis, we can develop models that\n", - "accurately predict the power production of wind turbines. This enables us to\n", - "optimize the power generation process and overcome the challenges associated\n", - "with its ingrained variability." + "## Table of Contents:\n", + "\n", + "* [Create a Spark Interactive Session](#create-a-spark-interactive-session)\n", + "* [Load the Dataset](#load-the-dataset)\n", + "* [Data Exploration](#data-exploration)\n", + "* [Training a GBT Regression](#training-a-gbt-regressor)" ] }, { @@ -42,38 +42,31 @@ "source": [ "## Create a Spark Interactive Session\n", "\n", - "Let's begin! In this demo you'll be using Livy to create and manage an interactive\n", - "Spark session. Livy is an open-source REST service that enables remote and interactive\n", - "analytics on Apache Spark clusters. It provides a way to interact with Spark clusters\n", - "programmatically using a REST API, allowing you to submit Spark jobs, run interactive\n", - "queries, and manage Spark sessions.\n", - "\n", - "First you need to connect to the Livy endpoint and create a new Spark interactive session.\n", - "This session will allow you to interact with Spark using your familiar notebook\n", - "environment, and execute Spark code to perform data processing tasks in an interactive manner.\n", + "Let's begin! In this demo you use Livy to create and manage an interactive Spark session. Livy is an open-source REST\n", + "service that enables remote and interactive analytics on Apache Spark clusters. It provides a way to interact with Spark\n", + "clusters programmatically using a REST API, allowing you to submit Spark jobs, run interactive queries, and manage Spark\n", + "sessions.\n", "\n", - "The Spark interactive session is particularly useful for exploratory data analysis,\n", - "prototyping, and iterative development. It allows you to interactively work with large datasets,\n", - "perform transformations, apply analytical operations, and build machine learning models using\n", - "Spark's distributed computing capabilities. This is exactly what you'll do in this Notebook!\n", + "First, you need to connect to the Livy endpoint and create a new Spark interactive session. The Spark interactive\n", + "session is particularly useful for exploratory data analysis, prototyping, and iterative development. It allows you to\n", + "interactively work with large datasets, perform transformations, apply analytical operations, and build machine learning\n", + "models using Spark's distributed computing capabilities. This is exactly what you do in this Notebook!\n", "\n", - "To communicate with Livy and manage your sessions you'll be using sparkmagic, an open-source\n", - "tool that provides a Jupyter kernel extension. Sparkmagic integrates with Livy, to provide\n", - "the underlying communication layer between the Jupyter kernel and the Spark cluster.\n", + "To communicate with Livy and manage your sessions you use Sparkmagic, an open-source tool that provides a Jupyter kernel\n", + "extension. Sparkmagic integrates with Livy, to provide the underlying communication layer between the Jupyter kernel and\n", + "the Spark cluster.\n", "\n", "Execute the cell below and:\n", "\n", - "1. Select `Add Endpoint`\n", - "1. Select `Basic_Access`, paste your Livy endpoint and authenticate with your credentials\n", - "1. Select `Create Session`\n", - "1. Provide a name select `python` and click `Create Session`\n", + "1. Select `Add Endpoint`.\n", + "1. Select `Basic_Access`, paste your Livy endpoint, and authenticate with your credentials.\n", + "1. Select `Create Session`.\n", + "1. Provide a name, select the `python` language, and click `Create Session`.\n", "\n", - "When your session is ready the `Manage Sessions` pane will become active, providing you the session ID.\n", - "The session state will become `idle` which means that you are good to go!\n", + "Give it a few minutes for your session to initialize. Once ready, the Manage Sessions pane will activate, displaying\n", + "your session ID. When the session state turns to idle, you're all set. This process can take up to five minutes.\n", "\n", - "> To configure Sparkmagic, you can make use of a config.json file located at ~/.sparkmagic/config.json.\n", - "> If you're running this notebook in a server that was created using the jupyter-data-science image,\n", - "> the default settings should suffice, and no additional configuration is required." + "> To further configure Sparkmagic, you can make use of a config.json file located at ~/.sparkmagic/config.json." ] }, { @@ -96,16 +89,48 @@ "tags": [] }, "source": [ - "You are now prepared to embark on your first interaction with Livy.\n", - "Whenever you initiate a cell with the `%%spark` magic command, the code within\n", - "that cell will not be executed by your IPython kernel. Instead, it will be executed\n", - "within a Spark context managed by Livy. Rest assured, Livy will handle everything\n", - "seamlessly and provide you with a response containing the desired results.\n", + "You can also use the EzUA UI to view the logs and code executions:\n", + "\n", + "![spark-interactive-ui](images/spark-interactive-ui.png)\n", + "\n", + "You are now prepared to embark on your first interaction with Livy. Whenever you initiate a cell with the `%%spark`\n", + "magic command, the code within that cell is not executed locally. Instead, it is executed within a Spark context managed\n", + "by Livy. Livy handles everything seamlessly and provide you with a response containing the results.\n", "\n", - "Moreover, with Sparkmagic at your side, you can leave the networking part to it. Sparkmagic\n", - "takes care of all the intricate details, effortlessly creating and sending requests to\n", - "the Livy server. Finally, it seamlessly renders the response within the Jupyter user interface,\n", - "ensuring a smooth and hassle-free experience." + "With Sparkmagic at your side, you can leave the networking part to it. Sparkmagic takes care of all the intricate\n", + "details, effortlessly creating and sending requests to the Livy server. Finally, it seamlessly renders the response\n", + "within the Jupyter user interface, ensuring a smooth and hassle-free experience.\n", + "\n", + "Before you begin, let's make sure that the dataset is copied in the right location:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e78f2f94-4ad9-41c9-a7f6-cef590163e62", + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import shutil\n", + "\n", + "# create the `auto-spark` folder if it does not exist\n", + "os.makedirs(f\"{os.getenv('HOME')}/shared/auto-spark/\", exist_ok=True)\n", + "\n", + "# copy the file to the right location\n", + "dataset_file = \"dataset/T1.csv\"\n", + "destination = f\"{os.getenv('HOME')}/shared/auto-spark/T1.csv\"\n", + "shutil.copyfile(dataset_file, destination)" + ] + }, + { + "cell_type": "markdown", + "id": "d478d922-8640-43a6-8802-c6bc577f508d", + "metadata": { + "tags": [] + }, + "source": [ + "Next, let's import the libraries you use inside the Spark session and get a handle on it." ] }, { @@ -177,11 +202,9 @@ "source": [ "## Load the Dataset\n", "\n", - "It's time to load the dataset, which comes in a convenient CSV format.\n", - "You'll leverage the spark session you created to load it as a DataFrame. Once loaded,\n", - "you can delve into its contents by examining the first five rows and its schema.\n", - "Additionally, you'll get an idea of the dataset's size by printing the number of\n", - "examples available to us." + "It's time to load the dataset, which comes in a convenient CSV format. You leverage the spark session you created to\n", + "load it as a DataFrame. Once loaded, you can delve into its contents by examining the first five rows and its schema. \n", + "Additionally, you get an idea of the dataset's size by printing the number of examples available to us." ] }, { @@ -194,7 +217,9 @@ "outputs": [], "source": [ "%%spark\n", - "spark_df = spark.read.csv('file:///mounts/shared-volume/shared/spark/T1.csv', header=True, inferSchema=True)" + "spark_df = spark.read.csv(\n", + " 'file:///mounts/shared-volume/shared/auto-spark/T1.csv',\n", + " header=True, inferSchema=True)" ] }, { @@ -204,10 +229,9 @@ "tags": [] }, "source": [ - "Next, you should cache the dataset in memory. The `cache()` method is used to persist\n", - "or cache the contents of a DataFrame, Dataset, or RDD (Resilient Distributed Dataset)\n", - "in memory. Caching data in memory can significantly improve the performance of iterative\n", - "algorithms or repeated computations by avoiding the need to recompute or fetch the data\n", + "Next, you should cache the dataset in memory. The `cache()` method is used to persist or cache the contents of a\n", + "DataFrame, Dataset, or RDD (Resilient Distributed Dataset) in memory. Caching data in memory can significantly improve\n", + "the performance of iterative algorithms or repeated computations by avoiding the need to recompute or fetch the data\n", "from disk." ] }, @@ -244,11 +268,10 @@ "tags": [] }, "source": [ - "In this experiment, the objective is to predict the power production of a wind turbine\n", - "(`lv activepower (kw)`) based on the dataset's other features. These features include\n", - "the current date and hour, the wind speed, and the wind direction. By analyzing the\n", - "relationships between these variables, you aim to uncover valuable insights and create\n", - "a predictive model that can estimate the power output of the turbine." + "In this experiment, the objective is to predict the power production of a wind turbine (`lv activepower (kw)`) based on\n", + "the other features. These features include the current date and hour, the wind speed, and the wind direction. By\n", + "analyzing the relationships between these variables, you aim to uncover valuable insights and create a predictive model\n", + "that can estimate the power output of the turbine." ] }, { @@ -260,8 +283,8 @@ "source": [ "## Data exploration\n", "\n", - "Let's start exploring and transforming the dataset. First step, separate the `date/time`\n", - "column into two separate columns, one for the month and one for the hour of the day." + "Let's start exploring and transforming the dataset. First step, separate the `date/time` column into two distinct\n", + "columns, one for the month and one for the hour of the day." ] }, { @@ -292,9 +315,9 @@ "tags": [] }, "source": [ - "Moving forward, let's examine some essential statistical characteristics of our features,\n", - "specifically the mean and standard deviation. However, it's important to note that these statistics\n", - "are relevant only for the wind speed, theoretical power curve, and active power variables." + "Moving forward, let's examine some essential statistical characteristics of the features, specifically the mean and\n", + "standard deviation. However, it's important to note that these statistics are relevant only for the wind speed,\n", + "theoretical power curve, and active power variables." ] }, { @@ -318,10 +341,9 @@ "tags": [] }, "source": [ - "Let's extract a random sample from the dataset and start creating a visual model.\n", - "By visualizing the data, we can gain a deeper understanding of its patterns, trends, and relationships.\n", - "Through this visual exploration, we'll uncover valuable insights that can guide us in our analysis\n", - "and decision-making processes." + "Let's extract a random sample from the dataset and start creating a visual model. By visualizing the data, you can gain\n", + "a deeper understanding of its patterns, trends, and relationships. Through this visual exploration, you uncover valuable\n", + "insights that can guide you through your analysis and decision-making processes." ] }, { @@ -405,6 +427,7 @@ "sample_df[columns].corr()\n", "plt.clf()\n", "sns.pairplot(sample_df[columns], markers='*');\n", + "\n", "%matplot plt" ] }, @@ -627,7 +650,7 @@ " sns.boxplot(x=df[each])\n", " # plt.title(each)\n", " i += 1\n", - " \n", + "\n", "%matplot plt" ] }, @@ -753,11 +776,10 @@ "source": [ "# Training a GBT Regressor\n", "\n", - "You are now ready to train our GBT regresson. To begin, you'll carefully specify\n", - "the features and the label for our model. Then, we'll split the dataset into training\n", - "and test subsets to ensure robust evaluation of our model's performance.\n", - "Finally, we'll initiate the training process, allowing our GBT regressor to learn\n", - "from the training data and make accurate predictions." + "You are now ready to train the GBT regresson. To begin, you carefully specify the features and the label for the model.\n", + "Then, you split the dataset into training and test subsets to ensure robust evaluation of the model's performance.\n", + "Finally, you initiate the training process, allowing the GBT regressor to learn from the training data, and generate\n", + "accurate predictions." ] }, { @@ -837,7 +859,7 @@ "outputs": [], "source": [ "%%spark\n", - "gbm_model.write().overwrite().save(\"file:///mounts/shared-volume/user/spark/GBM.model\")" + "gbm_model.write().overwrite().save(\"file:///mounts/shared-volume/shared/auto-spark/GBM.model\")" ] }, { @@ -850,7 +872,7 @@ "outputs": [], "source": [ "%%spark\n", - "gbm_model_dtap = GBTRegressionModel.load(\"file:///mounts/shared-volume/user/spark/GBM.model\")" + "gbm_model_dtap = GBTRegressionModel.load(\"file:///mounts/shared-volume/shared/auto-spark/GBM.model\")" ] }, { @@ -919,9 +941,9 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3 (ipykernel)", + "display_name": "wind-turbine", "language": "python", - "name": "python3" + "name": "wind-turbine" }, "language_info": { "codemirror_mode": { From 39c04290a73ad527e603c02b93baade576eab1d0 Mon Sep 17 00:00:00 2001 From: Dimitris Poulopoulos Date: Tue, 3 Oct 2023 16:36:54 +0300 Subject: [PATCH 3/7] wind-turbine: Remove the redundant Python script Eliminate the reduntant Python script that duplicates the code from the notebook, offering no additional value to this tutorial. Signed-off-by: Dimitris Poulopoulos --- demos/wind-turbine/wind-turbine.py | 377 ----------------------------- 1 file changed, 377 deletions(-) delete mode 100644 demos/wind-turbine/wind-turbine.py diff --git a/demos/wind-turbine/wind-turbine.py b/demos/wind-turbine/wind-turbine.py deleted file mode 100644 index 337999ec..00000000 --- a/demos/wind-turbine/wind-turbine.py +++ /dev/null @@ -1,377 +0,0 @@ -import pyspark -import numpy as np -import pandas as pd -import matplotlib.pyplot as plt -import seaborn as sns - -from math import radians -from warnings import filterwarnings - -from pyspark.sql import SparkSession -from pyspark.conf import SparkConf -from pyspark import SparkContext -from pyspark.sql.functions import substring -from pyspark.sql.types import IntegerType -from pyspark.sql import functions as F -from pyspark.ml.feature import VectorAssembler -from pyspark.ml.regression import GBTRegressor -from pyspark.ml.regression import GBTRegressionModel -from pyspark.ml.evaluation import RegressionEvaluator -from py4j.java_gateway import java_import - - -sns.set_style('white') -filterwarnings('ignore') - -java_import(spark._sc._jvm, "org.apache.spark.sql.api.python.*") - -# Reading Dataset -spark_df = spark.read.csv( - 'file:///mounts/shared-volume/spark/T1.csv', - header=True, inferSchema=True) - -# Caching the dataset -spark_df.cache() - -# Converting all the column names to lower case -spark_df = spark_df.toDF(*[c.lower() for c in spark_df.columns]) - -print('Show the first 5 rows') -print(spark_df.show(5)) - -print('What are the variable data types?') -print(spark_df.printSchema()) - -print('How many observations do we have?') -print(spark_df.count()) - -# Extracting a substring from columns to create month and hour variables -spark_df = spark_df.withColumn("month", substring("date/time", 4, 2)) -spark_df = spark_df.withColumn("hour", substring("date/time", 12, 2)) - -# Converting string month and hour variables to integer -spark_df = spark_df.withColumn('month', spark_df.month.cast(IntegerType())) -spark_df = spark_df.withColumn('hour', spark_df.hour.cast(IntegerType())) - -print(spark_df.show(5)) - -# Exploratory Data Analysis -pd.options.display.float_format = '{:.2f}'.format -spark_df.select('wind speed (m/s)', 'theoretical_power_curve (kwh)', - 'lv activepower (kw)').toPandas().describe() - -# Taking a random sample from the big data -sample_df = spark_df.sample(withReplacement=False, - fraction=0.1, seed=42).toPandas() - -# Visualizing the distributions with the sample data -columns = ['wind speed (m/s)', 'wind direction (deg)', 'month', 'hour', - 'theoretical_power_curve (kwh)', 'lv activepower (kw)'] -i = 1 -plt.figure(figsize=(10, 12)) -for each in columns: - plt.subplot(3, 2, i) - sample_df[each].plot.hist(bins=12) - plt.title(each) - i += 1 - -# Question: Is there any difference between the months -# for average power production? - -plt.clf() - -# Average power production by month -monthly = (spark_df.groupby('month') - .mean('lv activepower (kw)') - .sort('avg(lv activepower (kw))') - .toPandas()) -sns.barplot(x='month', y='avg(lv activepower (kw))', data=monthly) -plt.title('Months and Average Power Production') -print(monthly) - -# Question: Is there any difference between the hours for average power -# production? - -plt.clf() - -# Average power production by hour -hourly = (spark_df.groupby('hour') - .mean('lv activepower (kw)') - .sort('avg(lv activepower (kw))') - .toPandas()) -sns.barplot(x='hour', y='avg(lv activepower (kw))', data=hourly) -plt.title('Hours and Average Power Production') -print(hourly) - -# Question: Is there any correlation between the wind speed, -# wind direction and power production? -pd.set_option('display.max_columns', None) -sample_df[columns].corr() -plt.clf() -sns.pairplot(sample_df[columns], markers='*') - -# Question: What is the average power production level for -# different wind speeds? -# Finding average power production for 5 m/s wind speed increments -wind_speed = [] -avg_power = [] -for i in [0, 5, 10, 15, 20]: - avg_value = (spark_df.filter((spark_df['wind speed (m/s)'] > i) - & (spark_df['wind speed (m/s)'] <= i+5)) - .agg({'lv activepower (kw)': 'mean'}) - .collect()[0][0]) - avg_power.append(avg_value) - wind_speed.append(str(i) + '-' + str(i+5)) - -plt.clf() -sns.barplot(x=wind_speed, y=avg_power, color='orange') -plt.title('Avg Power Production for 5 m/s Wind Speed Increments') -plt.xlabel('Wind Speed') -plt.ylabel('Average Power Production') - -# Question: What is the power production for different wind directions and -# speeds? - -# Creating the polar diagram -plt.clf() -plt.figure(figsize=(8, 8)) -ax = plt.subplot(111, polar=True) -# Inside circles are the wind speed and marker color and size represents the -# amount of power production -sns.scatterplot(x=[radians(x) for x in sample_df['wind direction (deg)']], - y=sample_df['wind speed (m/s)'], - size=sample_df['lv activepower (kw)'], - hue=sample_df['lv activepower (kw)'], - alpha=0.7, legend=None) -# Setting the polar diagram's top represents the North -ax.set_theta_zero_location('N') -# Setting -1 to start the wind direction clockwise -ax.set_theta_direction(-1) -# Setting wind speed labels in a better position to see -ax.set_rlabel_position(110) -plt.title('Wind Speed - Wind Direction - Power Production Diagram') -plt.ylabel(None) - -# Question: Does the manufacturer's theoritical power production curve fit -# well with the real production? - -plt.clf() -plt.figure(figsize=(10, 6)) -sns.scatterplot(x='wind speed (m/s)', y='lv activepower (kw)', color='orange', - label='Real Production', alpha=0.5, data=sample_df) -sns.lineplot(x='wind speed (m/s)', y='theoretical_power_curve (kwh)', - color='blue', label='Theoritical Production', data=sample_df) -plt.title('Wind Speed and Power Production Chart') -plt.ylabel('Power Production (kw)') - -# Question: What is the wind speed threshold value for zero theorical power? -# Filter the big data where the real and theoritical power productions are -# equal to 0. -zero_theo_power = (spark_df.filter( - (spark_df['lv activepower (kw)'] == 0) - & (spark_df['theoretical_power_curve (kwh)'] == 0)) - .toPandas()) - -print(zero_theo_power[['wind speed (m/s)', 'theoretical_power_curve (kwh)', - 'lv activepower (kw)']].sample(5)) -plt.clf() -# Let's see the wind speed distribution for 0 power production -zero_theo_power['wind speed (m/s)'].hist() -plt.title('Wind Speed Distribution for 0 Power Production') -plt.xlabel('Wind speed (m/s)') -plt.ylabel('Counts for 0 Power Production') - -# Question: Why there aren't any power production in some observations while -# the wind speed is higher than 3 m/s? - -# Observations for the wind speed > 3m/s and power production = 0, -# While theoritically there should be power production -zero_power = spark_df.filter((spark_df['lv activepower (kw)'] == 0) - & (spark_df['theoretical_power_curve (kwh)'] != 0) - & (spark_df['wind speed (m/s)'] > 3)).toPandas() -print(zero_power.head()) -print('No of Observations (while Wind Speed > 3 m/s' - ' and Power Production = 0): ', len(zero_power)) - -plt.clf() -zero_power['wind speed (m/s)'].plot.hist(bins=8) -plt.xlabel('Wind Speed (m/s)') -plt.ylabel('Counts for Zero Production') -plt.title('Wind Speed Counts for Zero Power Production') -plt.xticks(ticks=np.arange(4, 18, 2)) - -# Let's see the monthly distribution for zero power production. - -plt.clf() -sns.countplot(zero_power['month']) - -# Excluding the observations meeting the filter criterias -spark_df = spark_df.filter(~((spark_df['lv activepower (kw)'] == 0) - & (spark_df['theoretical_power_curve (kwh)'] != 0) - & (spark_df['wind speed (m/s)'] > 3))) -spark_df.show(20) - -# Question: Is there any other outliers? - -columns = ['wind speed (m/s)', 'wind direction (deg)', - 'theoretical_power_curve (kwh)', 'lv activepower (kw)'] -i = 1 -plt.clf() -plt.figure(figsize=(20, 3)) -for each in columns: - df = spark_df.select(each).toPandas() - plt.subplot(1, 4, i) - # plt.boxplot(df) - sns.boxplot(x=df[each]) - plt.title(each) - i += 1 - -# We will find the upper and lower threshold values for the wind speed data, -# and analyze the outliers. - -# Create a pandas df for visualization -wind_speed = spark_df.select('wind speed (m/s)').toPandas() - -# Defining the quantiles and interquantile range -Q1 = wind_speed['wind speed (m/s)'].quantile(0.25) -Q3 = wind_speed['wind speed (m/s)'].quantile(0.75) -IQR = Q3-Q1 -# Defining the lower and upper threshold values -lower = Q1 - 1.5*IQR -upper = Q3 + 1.5*IQR - -print('Quantile (0.25): ', Q1, ' Quantile (0.75): ', Q3) -print('Lower threshold: ', lower, ' Upper threshold: ', upper) - -# Fancy indexing for outliers -outlier_tf = ((wind_speed['wind speed (m/s)'] < lower) - | (wind_speed['wind speed (m/s)'] > upper)) - -print('Total Number of Outliers: ', - len(wind_speed['wind speed (m/s)'][outlier_tf])) -print('--'*15) -print('Some Examples of Outliers:') -print(wind_speed['wind speed (m/s)'][outlier_tf].sample(10)) - -# Out of 47033, there is only 407 observations while the wind speed is over -# 19 m/s. -# Now Lets see average power production for these high wind speed -(spark_df.select('wind speed (m/s)', 'lv activepower (kw)') - .filter(spark_df['wind speed (m/s)'] >= 19) - .agg({'lv activepower (kw)': 'mean'}) - .show()) - -spark_df = spark_df.withColumn('wind speed (m/s)', - F.when(F.col('wind speed (m/s)') > 19.447, 19) - .otherwise(F.col('wind speed (m/s)'))) -spark_df.count() - -# Question: What are the general criterias for power production? - -# High level power production -(spark_df.filter( - ((spark_df['month'] == 3) | (spark_df['month'] == 8) | (spark_df['month'] == 11)) - & ((spark_df['hour'] >= 16) | (spark_df['hour'] <= 24)) - & ((spark_df['wind direction (deg)'] > 0) | (spark_df['wind direction (deg)'] < 90)) - & ((spark_df['wind direction (deg)'] > 180) | (spark_df['wind direction (deg)'] < 225))) - .agg({'lv activepower (kw)': 'mean'}) - .show()) - -# Low level power production -(spark_df.filter( - (spark_df['month'] == 7) - & ((spark_df['hour'] >= 9) | (spark_df['hour'] <= 11)) - & ((spark_df['wind direction (deg)'] > 90) | (spark_df['wind direction (deg)'] < 160))) - .agg({'lv activepower (kw)': 'mean'}) - .show()) - - -# Data Preparation for ML Algorithms -# Preparing the independent variables (Features) -# Converting lv activepower (kw) variable as label -spark_df = spark_df.withColumn('label', spark_df['lv activepower (kw)']) - -# Defining the variables to be used -variables = ['month', 'hour', 'wind speed (m/s)', 'wind direction (deg)'] -vectorAssembler = VectorAssembler(inputCols=variables, - outputCol='features') -va_df = vectorAssembler.transform(spark_df) - -# Combining features and label column -final_df = va_df.select('features', 'label') -final_df.show(10) - -# Train Test Split -splits = final_df.randomSplit([0.8, 0.2]) -train_df = splits[0] -test_df = splits[1] - -print('Train dataset: ', train_df.count()) -print('Test dataset : ', test_df.count()) - -# Creating the Initial Model -# Creating the gbm regressor object -gbm = GBTRegressor(featuresCol='features', labelCol='label') - -# Training the model with train data -gbm_model = gbm.fit(train_df) - -# Predicting using the test data -y_pred = gbm_model.transform(test_df) - -# Initial look at the target and predicted values -y_pred.select('label', 'prediction').show(20) - -# Store our model in user shared volume -gbm_model.write().overwrite().save( - 'file:///mounts/shared-volume/spark/GBM.model') - -gbm_model_dtap = GBTRegressionModel.load( - "file:///mounts/shared-volume/spark/GBM.model") - -# Let's evaluate our model's success -# Initial model success -evaluator = RegressionEvaluator(predictionCol='prediction', labelCol='label') - -print('R2:\t', evaluator.evaluate(y_pred, {evaluator.metricName: 'r2'})) -print('MAE:\t', evaluator.evaluate(y_pred, {evaluator.metricName: 'mae'})) -print('RMSE:\t', evaluator.evaluate(y_pred, {evaluator.metricName: 'rmse'})) - -# Comparing Real, Theoritical and Predicted Power Productions - -# Converting sample_df back to Spark dataframe -eva_df = spark.createDataFrame(sample_df) - -# Converting lv activepower (kw) variable as label -eva_df = eva_df.withColumn('label', eva_df['lv activepower (kw)']) - -# Defining the variables to be used -variables = ['month', 'hour', 'wind speed (m/s)', 'wind direction (deg)'] -vectorAssembler = VectorAssembler(inputCols=variables, outputCol='features') -vec_df = vectorAssembler.transform(eva_df) - -# Combining features and label column -vec_df = vec_df.select('features', 'label') - -# Using ML model to predict -preds = gbm_model.transform(vec_df) -preds_df = preds.select('label', 'prediction').toPandas() - -# Compining dataframes to compare -frames = [sample_df[['wind speed (m/s)', 'theoretical_power_curve (kwh)']], - preds_df] -sample_data = pd.concat(frames, axis=1) - -plt.clf() -# Visualizing real, theoritical and predicted power production -plt.figure(figsize=(10, 7)) -sns.scatterplot(x='wind speed (m/s)', y='label', alpha=0.5, - label='Real Power', data=sample_data) -sns.scatterplot(x='wind speed (m/s)', y='prediction', alpha=0.7, - label='Predicted Power', marker='o', data=sample_data) -sns.lineplot(x='wind speed (m/s)', y='theoretical_power_curve (kwh)', - label='Theoritical Power', color='purple', data=sample_data) -plt.title('Wind Turbine Power Production Prediction') -plt.ylabel('Power Production (kw)') -plt.legend() - From 17566118b8bb1ae1bfe7cb7d017f5c847fe8314d Mon Sep 17 00:00:00 2001 From: Dimitris Poulopoulos Date: Mon, 2 Oct 2023 09:38:40 +0000 Subject: [PATCH 4/7] wind-turbine: Enhance the README File Enhance the README file by: - Introducing a 'Procedure' section that walks the user through the necessary steps for a successful run. - Incorporating a "How it Works" section that elucidates what Livy and Sparkmagic are, and how they collaboratively streamline interactions with a Spark cluster. - Including a 'References' section, providing links for extended reading. Signed-off-by: Dimitris Poulopoulos --- demos/wind-turbine/README.md | 77 ++++++++++++++++++++++++------------ 1 file changed, 51 insertions(+), 26 deletions(-) diff --git a/demos/wind-turbine/README.md b/demos/wind-turbine/README.md index 95349b6b..2f866fce 100644 --- a/demos/wind-turbine/README.md +++ b/demos/wind-turbine/README.md @@ -1,39 +1,64 @@ -# Wind Turbine (Spark Demo) +# Wind Turbine (Spark - Livy - Sparkmagic) + +In this demonstration, you use Spark to explore a dataset and train a Gradient-Boosted Tree (GBT) regressor that +leverages various features, such as wind speed and direction, to estimate the power output of a wind turbine. ![wind-farm](images/wind-farm.jpg) -Welcome! In this experiment we delve into the world of wind turbines -and harness the power of machine learning to predict their energy production! -In this demonstration, we will be using Spark to explore the training dataset -and train a Gradient-Boosted Tree (GBT) regressor that will utilize various -features, such as wind speed and direction, to estimate the power output of a -wind turbine. +Wind turbines hold tremendous potential as a sustainable source of energy, capable of supplying a substantial portion +of the world's power needs. However, the inherent unpredictability of power generation poses a challenge when it comes +to optimizing this process. -Wind turbines hold tremendous potential as a sustainable source of energy, -capable of supplying a substantial portion of the world's power needs. However, -the inherent unpredictability of power generation poses a challenge when it -comes to optimizing this process. +Fortunately, you have a powerful tool at our disposal: Machine Learning (ML). By leveraging advanced algorithms and data +analysis, you can develop models that accurately predict the power production of wind turbines. This enables you to +optimize the power generation process and overcome the challenges associated with its ingrained variability. -Fortunately, we have a powerful tool at our disposal: machine learning. By -leveraging advanced algorithms and data analysis, we can develop models that -accurately predict the power production of wind turbines. This enables us to -optimize the power generation process and overcome the challenges associated -with its ingrained variability. +1. [What You'll Need](#what-youll-need) +1. [Procedure](#procedure) +1. [How it Works](#how-it-works) +1. [References](#references) ## What You'll Need -To complete the tutorial follow the steps below: +For this tutorial, ensure you have: + +- Access to an HPE Ezmeral Unified Analytics cluster. + +## Procedure + +To complete this tutorial follow the steps below: + +1. Login to your Ezmeral Unified Analytics (EzUA) cluster, using your credentials. +1. Create a new Notebook server using the `jupyter-data-science` image. Request at least `4Gi` of memory for the + Notebook server. +1. Connect to the Notebook server and clone the repository locally. +1. Navigate to the tutorial's directory (`ezua-tutorials/demos/wind-turbine`). +1. Launch a new terminal window and create a new conda environment using the specified `environment.yaml` file: + ``` + conda env create -f environment.yaml + ``` +1. Add the new conda environment as an ipykernel: + ``` + python -m ipykernel install --user --name=wind-turbine + ``` +1. Refresh your browser tab to access the updated environment. +1. Launch the `wind-turbine.ipynb` notebook file and follow the instructions. Make sure to select the `wind-turbine` + environment kernel. -1. Login to you EzAF cluster. -1. Create a new notebook server using the `jupyter-data-science` image. -1. Clone the repository locally. -1. Launch the `wind-turbine.ipynb` notebook file and follow the instructions. +## How it Works -> It is recommended to create a Notebook server with more than 3Gi of memory +In this tutorial, you use Livy and Sparkmagic to remotely execute Python code in a Spark cluster. Livy is an open-source +REST service that enables remote and interactive analytics on Apache Spark clusters. It provides a way to interact with +Spark clusters programmatically using a REST API, allowing you to submit Spark jobs, run interactive queries, and manage +Spark sessions. -> If you created your notebook server using the `jupyter-data-science` image -> you should be good to go. However, you can always create a separate conda -> environment for this tutorial using the provided `environment.yaml` file. -> To create a separate enviroment run `conda env create -f environment.yaml`. +To communicate with Livy and manage your sessions you use Sparkmagic, an open-source tool that provides a Jupyter kernel +extension. Sparkmagic integrates with Livy, to provide the underlying communication layer between the Jupyter kernel and +the Spark cluster. +## References +1. [Spark: Unified engine for large-scale data analytics](https://spark.apache.org/) +1. [Livy: A REST Service for Apache Spark](https://livy.apache.org/) +1. [Sparkmagic: Jupyter magics and kernels for working with remote Spark clusters](https://github.com/jupyter-incubator/sparkmagic) +1. [Wind Turbine Scada Dataset](https://www.kaggle.com/datasets/berkerisen/wind-turbine-scada-dataset/data) \ No newline at end of file From e288854b4c05a90db3c2af23d4165965ce3b8e72 Mon Sep 17 00:00:00 2001 From: Erdinc Kaya Date: Sat, 7 Oct 2023 00:13:08 +0100 Subject: [PATCH 5/7] improve README --- demos/wind-turbine/README.md | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/demos/wind-turbine/README.md b/demos/wind-turbine/README.md index 2f866fce..d5c1f025 100644 --- a/demos/wind-turbine/README.md +++ b/demos/wind-turbine/README.md @@ -34,16 +34,20 @@ To complete this tutorial follow the steps below: 1. Connect to the Notebook server and clone the repository locally. 1. Navigate to the tutorial's directory (`ezua-tutorials/demos/wind-turbine`). 1. Launch a new terminal window and create a new conda environment using the specified `environment.yaml` file: - ``` + + ```bash conda env create -f environment.yaml ``` + 1. Add the new conda environment as an ipykernel: - ``` + + ```bash python -m ipykernel install --user --name=wind-turbine ``` + 1. Refresh your browser tab to access the updated environment. 1. Launch the `wind-turbine.ipynb` notebook file and follow the instructions. Make sure to select the `wind-turbine` - environment kernel. + environment kernel. ## How it Works @@ -61,4 +65,4 @@ the Spark cluster. 1. [Spark: Unified engine for large-scale data analytics](https://spark.apache.org/) 1. [Livy: A REST Service for Apache Spark](https://livy.apache.org/) 1. [Sparkmagic: Jupyter magics and kernels for working with remote Spark clusters](https://github.com/jupyter-incubator/sparkmagic) -1. [Wind Turbine Scada Dataset](https://www.kaggle.com/datasets/berkerisen/wind-turbine-scada-dataset/data) \ No newline at end of file +1. [Wind Turbine Scada Dataset](https://www.kaggle.com/datasets/berkerisen/wind-turbine-scada-dataset/data) From 65091b23daf7abefb4d27e180d5bcd2eb11a135a Mon Sep 17 00:00:00 2001 From: Erdinc Kaya Date: Sat, 7 Oct 2023 00:14:16 +0100 Subject: [PATCH 6/7] wind-turbine: update notebook for SSO and user --- demos/wind-turbine/wind-turbine.ipynb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/demos/wind-turbine/wind-turbine.ipynb b/demos/wind-turbine/wind-turbine.ipynb index 5973406e..e09e0297 100644 --- a/demos/wind-turbine/wind-turbine.ipynb +++ b/demos/wind-turbine/wind-turbine.ipynb @@ -59,7 +59,7 @@ "Execute the cell below and:\n", "\n", "1. Select `Add Endpoint`.\n", - "1. Select `Basic_Access`, paste your Livy endpoint, and authenticate with your credentials.\n", + "1. Select `Single Sign-On`, keep existing Livy endpoint (should look like http://livy-0.livy-svc.spark.svc.cluster.local:8998).\n", "1. Select `Create Session`.\n", "1. Provide a name, select the `python` language, and click `Create Session`.\n", "\n", @@ -859,7 +859,7 @@ "outputs": [], "source": [ "%%spark\n", - "gbm_model.write().overwrite().save(\"file:///mounts/shared-volume/shared/auto-spark/GBM.model\")" + "gbm_model.write().overwrite().save('file:////mounts/shared-volume/user/auto-spark/GBM.model')" ] }, { @@ -872,7 +872,7 @@ "outputs": [], "source": [ "%%spark\n", - "gbm_model_dtap = GBTRegressionModel.load(\"file:///mounts/shared-volume/shared/auto-spark/GBM.model\")" + "gbm_model_dtap = GBTRegressionModel.load(\"file:///mounts/shared-volume/user/auto-spark/GBM.model\")" ] }, { From f4dd608b5342378c0ebdf31547dffba9c71c7302 Mon Sep 17 00:00:00 2001 From: Prasad Adireddi Date: Thu, 14 Nov 2024 11:46:58 +0530 Subject: [PATCH 7/7] Update environment.yaml --- tutorials/mlflow/environment.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/tutorials/mlflow/environment.yaml b/tutorials/mlflow/environment.yaml index 90b1defe..6ba99172 100644 --- a/tutorials/mlflow/environment.yaml +++ b/tutorials/mlflow/environment.yaml @@ -1,7 +1,6 @@ name: bike-sharing channels: - conda-forge - - defaults dependencies: - python=3.8 - graphviz [conda-forge]