From c0d6818367ed114b8ba18855f2004c3179b0006b Mon Sep 17 00:00:00 2001 From: Maksym Zhytnikov <63515947+Maxxx-zh@users.noreply.github.com> Date: Mon, 18 Mar 2024 12:50:48 +0200 Subject: [PATCH] [FSTORE-1207] AirQuality LLM project (#250) * Function Calling & AirQuality FunctionCalling Chatbot --- .../1_air_quality_feature_backfill.ipynb | 976 +++++++++++++----- .../2_air_quality_feature_pipeline.ipynb | 942 +++++++++++++++-- .../3_air_quality_training_pipeline.ipynb | 632 +++++++++--- .../4_air_quality_batch_inference.ipynb | 402 ++++++-- .../air_quality/5_function_calling.ipynb | 776 ++++++++++++++ advanced_tutorials/air_quality/app_gradio.py | 104 ++ .../air_quality/app_streamlit.py | 99 ++ .../air_quality/feature_pipeline.py | 158 --- .../air_quality/features/__init__.py | 0 .../air_quality/features/air_quality.py | 6 +- advanced_tutorials/air_quality/functions.py | 392 ------- .../functions/air_quality_data_retrieval.py | 164 +++ .../air_quality/functions/common_functions.py | 25 + .../functions/context_engineering.py | 191 ++++ .../air_quality/functions/llm_chain.py | 202 ++++ .../functions/parse_air_quality.py | 79 ++ .../air_quality/functions/parse_weather.py | 81 ++ .../air_quality/requirements.txt | 19 +- 18 files changed, 4125 insertions(+), 1123 deletions(-) create mode 100644 advanced_tutorials/air_quality/5_function_calling.ipynb create mode 100644 advanced_tutorials/air_quality/app_gradio.py create mode 100644 advanced_tutorials/air_quality/app_streamlit.py delete mode 100644 advanced_tutorials/air_quality/feature_pipeline.py delete mode 100644 advanced_tutorials/air_quality/features/__init__.py delete mode 100644 advanced_tutorials/air_quality/functions.py create mode 100644 advanced_tutorials/air_quality/functions/air_quality_data_retrieval.py create mode 100644 advanced_tutorials/air_quality/functions/common_functions.py create mode 100644 advanced_tutorials/air_quality/functions/context_engineering.py create mode 100644 advanced_tutorials/air_quality/functions/llm_chain.py create mode 100644 advanced_tutorials/air_quality/functions/parse_air_quality.py create mode 100644 advanced_tutorials/air_quality/functions/parse_weather.py diff --git a/advanced_tutorials/air_quality/1_air_quality_feature_backfill.ipynb b/advanced_tutorials/air_quality/1_air_quality_feature_backfill.ipynb index f203e073..1ce0301e 100644 --- a/advanced_tutorials/air_quality/1_air_quality_feature_backfill.ipynb +++ b/advanced_tutorials/air_quality/1_air_quality_feature_backfill.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "markdown", - "id": "73ee3ec9", + "id": "32cd155d", "metadata": {}, "source": [ "# **Hopsworks Feature Store** \n", @@ -12,49 +12,61 @@ "**Note**: This tutorial does not support Google Colab.\n", "\n", "## ๐Ÿ—’๏ธ This notebook is divided into the following sections:\n", - "1. Fetch historical data\n", - "2. Connect to the Hopsworks feature store\n", - "3. Create feature groups and insert them to the feature store\n", + "\n", + "1. Fetch historical data.\n", + "2. Connect to the Hopsworks feature store.\n", + "3. Create feature groups and insert them to the feature store.\n", "\n", "![tutorial-flow](../../images/01_featuregroups.png)" ] }, { "cell_type": "markdown", - "id": "f04d5c5e", + "id": "ce71c0b2", "metadata": {}, "source": [ - "### ๐Ÿ“ Imports" + "## ๐Ÿ“ Imports" ] }, { "cell_type": "code", - "execution_count": null, - "id": "a03d0127", + "execution_count": 1, + "id": "f92001bd", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[33mWARNING: Running pip as the 'root' user can result in broken permissions and conflicting behaviour with the system package manager. It is recommended to use a virtual environment instead: https://pip.pypa.io/warnings/venv\u001b[0m\u001b[33m\n", + "\u001b[0m\u001b[31mERROR: pip's dependency resolver does not currently take into account all the packages that are installed. This behaviour is the source of the following dependency conflicts.\n", + "tensorflow 2.11.0 requires protobuf<3.20,>=3.9.2, but you have protobuf 4.25.3 which is incompatible.\n", + "tensorboard 2.11.2 requires protobuf<4,>=3.9.2, but you have protobuf 4.25.3 which is incompatible.\n", + "ray 2.0.0 requires protobuf<4.0.0,>=3.15.3, but you have protobuf 4.25.3 which is incompatible.\u001b[0m\u001b[31m\n", + "\u001b[0m\u001b[33mWARNING: Running pip as the 'root' user can result in broken permissions and conflicting behaviour with the system package manager. It is recommended to use a virtual environment instead: https://pip.pypa.io/warnings/venv\u001b[0m\u001b[33m\n", + "\u001b[0m" + ] + } + ], "source": [ - "!pip install -U hopsworks --quiet\n", - "!pip install geopy folium streamlit-folium --q" + "!pip install -r requirements.txt --quiet\n", + "!pip install -U hopsworks --quiet" ] }, { "cell_type": "code", - "execution_count": null, - "id": "cd165941", + "execution_count": 2, + "id": "e974d9d5", "metadata": {}, "outputs": [], "source": [ - "import datetime\n", - "import time\n", - "import requests\n", "import json\n", "\n", "import pandas as pd\n", "import folium\n", "\n", "from features import air_quality\n", - "from functions import *\n", + "from functions.common_functions import convert_date_to_unix\n", "\n", "import warnings\n", "warnings.filterwarnings(\"ignore\")" @@ -62,15 +74,7 @@ }, { "cell_type": "markdown", - "id": "ba9903fc", - "metadata": {}, - "source": [ - "---" - ] - }, - { - "cell_type": "markdown", - "id": "b7a1965a-0da7-4263-a68a-8b2e8cb753f1", + "id": "88d519dd", "metadata": {}, "source": [ "## ๐ŸŒ Representing the Target cities " @@ -78,8 +82,8 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "bd578db1-69e7-4230-b3f2-807b8056283a", + "execution_count": 3, + "id": "e0f7a26b", "metadata": { "tags": [] }, @@ -95,8 +99,8 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "ea972c52-bfad-465d-b1e1-50eeff99b482", + "execution_count": 5, + "id": "f8063796", "metadata": {}, "outputs": [], "source": [ @@ -109,13 +113,13 @@ " location=coords,\n", " popup=city_name,\n", " ).add_to(my_map)\n", - "my_map" + "#my_map" ] }, { "cell_type": "code", "execution_count": null, - "id": "fb5ecf81-647b-490a-92b1-f7e963413710", + "id": "fcde29f7", "metadata": {}, "outputs": [], "source": [ @@ -125,7 +129,7 @@ }, { "cell_type": "markdown", - "id": "2246ca9d", + "id": "2a2d3674", "metadata": {}, "source": [ "## ๐ŸŒซ Processing Air Quality data" @@ -133,7 +137,7 @@ }, { "cell_type": "markdown", - "id": "b4a1c5d1", + "id": "b081d3f2", "metadata": {}, "source": [ "### [๐Ÿ‡ช๐Ÿ‡บ EEA](https://discomap.eea.europa.eu/map/fme/AirQualityExport.htm)\n", @@ -142,12 +146,39 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "96b8be01-6286-4886-8043-56e0e49b314e", + "execution_count": 6, + "id": "986686f5", "metadata": { "tags": [] }, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "{'Amsterdam': [52.37, 4.89],\n", + " 'Athina': [37.98, 23.73],\n", + " 'Berlin': [52.52, 13.39],\n", + " 'Gdansk': [54.37, 18.61],\n", + " 'Krakรณw': [50.06, 19.94],\n", + " 'London': [51.51, -0.13],\n", + " 'Madrid': [40.42, -3.7],\n", + " 'Marseille': [43.3, 5.37],\n", + " 'Milano': [45.46, 9.19],\n", + " 'Mรผnchen': [48.14, 11.58],\n", + " 'Napoli': [40.84, 14.25],\n", + " 'Paris': [48.85, 2.35],\n", + " 'Sevilla': [37.39, -6.0],\n", + " 'Stockholm': [59.33, 18.07],\n", + " 'Tallinn': [59.44, 24.75],\n", + " 'Varna': [43.21, 27.92],\n", + " 'Wien': [48.21, 16.37]}" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# EU Cities \n", "target_cities[\"EU\"]" @@ -155,47 +186,96 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "5bb2a868-5f3a-4065-b651-318c24826b97", + "execution_count": 12, + "id": "be358330", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "โ›ณ๏ธ Size of this dataframe: (63548, 3)\n", + "โ›ณ๏ธ Missing Values: 0\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", + "
city_namedatepm2_5
11887Gdansk2014-09-2123.0
17498Krakรณw2019-10-2356.0
42593Paris2016-08-047.0
\n", + "
" + ], + "text/plain": [ + " city_name date pm2_5\n", + "11887 Gdansk 2014-09-21 23.0\n", + "17498 Krakรณw 2019-10-23 56.0\n", + "42593 Paris 2016-08-04 7.0" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# Read the CSV file from the specified URL into a pandas DataFrame\n", - "df_eu = pd.read_csv(\"https://repo.hops.works/dev/davit/air_quality/backfill_pm2_5_eu.csv\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "5620df22-f744-4550-a81a-7e5d71aae542", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "# Check for missing values in the 'df_eu' DataFrame\n", - "df_eu.isna().sum().sum()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "b0e23728-a01d-45bc-bf25-4a9c77f21d66", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ + "df_eu = pd.read_csv(\"https://repo.hops.works/dev/davit/air_quality/backfill_pm2_5_eu.csv\")\n", + "\n", "# Print the size of the 'df_eu' DataFrame (number of rows and columns)\n", "print(\"โ›ณ๏ธ Size of this dataframe:\", df_eu.shape)\n", "\n", + "# Check for missing values in the 'df_eu' DataFrame\n", + "print(f'โ›ณ๏ธ Missing Values: {df_eu.isna().sum().sum()}')\n", + "\n", "# Display a random sample of three rows from the 'df_eu' DataFrame\n", "df_eu.sample(3)" ] }, { "cell_type": "markdown", - "id": "c2e45567-dd6b-4e5e-a153-82a2f4f32fbc", + "id": "f02141bd", "metadata": {}, "source": [ "### [๐Ÿ‡บ๐Ÿ‡ธ USEPA](https://aqs.epa.gov/aqsweb/documents/data_api.html#daily)\n", @@ -206,12 +286,35 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "c4952759-0fb9-4229-8b78-2e37cffb144d", + "execution_count": 13, + "id": "87c439b7", "metadata": { "tags": [] }, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "{'Albuquerque': [35.08, -106.65],\n", + " 'Atlanta': [33.75, -84.39],\n", + " 'Chicago': [41.88, -87.62],\n", + " 'Columbus': [39.96, -83.0],\n", + " 'Dallas': [32.78, -96.8],\n", + " 'Denver': [39.74, -104.98],\n", + " 'Houston': [29.76, -95.37],\n", + " 'Los Angeles': [34.05, -118.24],\n", + " 'New York': [40.71, -74.01],\n", + " 'Phoenix-Mesa': [33.66, -112.04],\n", + " 'Salt Lake City': [40.76, -111.89],\n", + " 'San Francisco': [37.78, -122.42],\n", + " 'Tampa': [27.95, -82.46]}" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# US Cities \n", "target_cities[\"US\"]" @@ -219,49 +322,98 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "c6aceaee-9431-48fd-818a-41fbdd07575c", + "execution_count": 16, + "id": "3429aebd", "metadata": { "tags": [] }, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "โ›ณ๏ธ Size of this dataframe: (46037, 3)\n", + "โ›ณ๏ธ Missing Values: 0\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", + "
datecity_namepm2_5
214762015-01-14Houston11.3
263212018-11-28Los Angeles7.8
430022014-09-01Tampa11.8
\n", + "
" + ], + "text/plain": [ + " date city_name pm2_5\n", + "21476 2015-01-14 Houston 11.3\n", + "26321 2018-11-28 Los Angeles 7.8\n", + "43002 2014-09-01 Tampa 11.8" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# Read the CSV file from the specified URL into a pandas DataFrame\n", - "df_us = pd.read_csv(\"https://repo.hops.works/dev/davit/air_quality/backfill_pm2_5_us.csv\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "4e7ff20e-8a1a-4fa3-b801-71beead7b5f2", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "# Check for missing values in the 'df_us' DataFrame\n", - "df_us.isna().sum().sum()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "3818e3e1-8674-4634-9023-92be8410fba5", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ + "df_us = pd.read_csv(\"https://repo.hops.works/dev/davit/air_quality/backfill_pm2_5_us.csv\")\n", + "\n", "# Print the size of the 'df_us' DataFrame (number of rows and columns)\n", "print(\"โ›ณ๏ธ Size of this dataframe:\", df_us.shape)\n", "\n", + "# Check for missing values in the 'df_us' DataFrame\n", + "print(f'โ›ณ๏ธ Missing Values: {df_us.isna().sum().sum()}')\n", + "\n", "# Display a random sample of three rows from the 'df_us' DataFrame\n", "df_us.sample(3)" ] }, { "cell_type": "markdown", - "id": "25557752-31c8-4da9-a52c-4415c4d20ae3", + "id": "5ee7b660", "metadata": {}, "source": [ "### ๐Ÿข Processing special city - `Seattle`\n", @@ -271,72 +423,135 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "2f54d2cb-991c-47cb-a686-76c9f7a87170", + "execution_count": 15, + "id": "f401130e", "metadata": { "tags": [] }, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "{'Bellevue-SE 12th St': [47.60086, -122.1484],\n", + " 'DARRINGTON - FIR ST (Darrington High School)': [48.2469, -121.6031],\n", + " 'KENT - JAMES & CENTRAL': [47.38611, -122.23028],\n", + " 'LAKE FOREST PARK TOWNE CENTER': [47.755, -122.2806],\n", + " 'MARYSVILLE - 7TH AVE (Marysville Junior High)': [48.05432, -122.17153],\n", + " 'NORTH BEND - NORTH BEND WAY': [47.49022, -121.77278],\n", + " 'SEATTLE - BEACON HILL': [47.56824, -122.30863],\n", + " 'SEATTLE - DUWAMISH': [47.55975, -122.33827],\n", + " 'SEATTLE - SOUTH PARK #2': [47.53091, -122.3208],\n", + " 'Seattle-10th & Weller': [47.59722, -122.31972],\n", + " 'TACOMA - ALEXANDER AVE': [47.2656, -122.3858],\n", + " 'TACOMA - L STREET': [47.1864, -122.4517],\n", + " 'Tacoma-S 36th St': [47.22634, -122.46256],\n", + " 'Tukwila Allentown': [47.49854, -122.27839],\n", + " 'Tulalip-Totem Beach Rd': [48.06534, -122.28519]}" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "target_cities[\"Seattle\"]" ] }, { "cell_type": "code", - "execution_count": null, - "id": "31c8505d-68bc-40b6-be0f-42d8532dbd48", + "execution_count": 17, + "id": "5ac26217", "metadata": { "tags": [] }, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "โ›ณ๏ธ Size of this dataframe: (46479, 3)\n", + "โ›ณ๏ธ Missing Values: 0\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", + "
city_namedatepm2_5
8709SEATTLE - BEACON HILL2015-11-059.5
6634DARRINGTON - FIR ST (Darrington High School)2014-06-241.7
45134NORTH BEND - NORTH BEND WAY2023-01-120.3
\n", + "
" + ], + "text/plain": [ + " city_name date pm2_5\n", + "8709 SEATTLE - BEACON HILL 2015-11-05 9.5\n", + "6634 DARRINGTON - FIR ST (Darrington High School) 2014-06-24 1.7\n", + "45134 NORTH BEND - NORTH BEND WAY 2023-01-12 0.3" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# Read the CSV file from the specified URL into a pandas DataFrame\n", - "df_seattle = pd.read_csv(\"https://repo.hops.works/dev/davit/air_quality/backfill_pm2_5_seattle.csv\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "2f6583c9-3b2a-41c6-a020-aeede88c4867", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "# Check for missing values in the 'df_seattle' DataFrame\n", - "df_seattle.isna().sum().sum()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "065a5b03-28f7-475c-9c6a-4340388157d8", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ + "df_seattle = pd.read_csv(\"https://repo.hops.works/dev/davit/air_quality/backfill_pm2_5_seattle.csv\")\n", + "\n", "# Print the size of the 'df_seattle' DataFrame (number of rows and columns)\n", "print(\"โ›ณ๏ธ Size of this dataframe:\", df_seattle.shape)\n", + "\n", + "# Check for missing values in the 'df_seattle' DataFrame\n", + "print(f'โ›ณ๏ธ Missing Values: {df_seattle.isna().sum().sum()}')\n", + "\n", + "# Display a random sample of three rows\n", "df_seattle.sample(3)" ] }, - { - "cell_type": "code", - "execution_count": null, - "id": "e3b17ca4-0e9d-4207-ad62-90ea9c157def", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "# Value Counts\n", - "df_seattle['city_name'].value_counts()" - ] - }, { "cell_type": "markdown", - "id": "c278a55d-f083-4f95-b292-92e545b9c408", + "id": "e23a6e68", "metadata": {}, "source": [ "### ๐ŸŒŸ All together" @@ -344,12 +559,94 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "0d55ae92-4bf9-43ae-8841-6767f5f68bec", + "execution_count": 19, + "id": "d913087f", "metadata": { "tags": [] }, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "โ›ณ๏ธ DF shape: (156064, 3)\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", + "
city_namedatepm2_5
106487Tampa2014-06-309.3
12453Gdansk2016-04-099.0
101342Salt Lake City2020-04-295.8
46538Sevilla2017-02-128.0
117821Seattle-10th & Weller2015-12-125.7
\n", + "
" + ], + "text/plain": [ + " city_name date pm2_5\n", + "106487 Tampa 2014-06-30 9.3\n", + "12453 Gdansk 2016-04-09 9.0\n", + "101342 Salt Lake City 2020-04-29 5.8\n", + "46538 Sevilla 2017-02-12 8.0\n", + "117821 Seattle-10th & Weller 2015-12-12 5.7" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# Concatenate the DataFrames df_eu, df_us, and df_seattle along the rows and reset the index\n", "df_air_quality = pd.concat(\n", @@ -365,18 +662,18 @@ }, { "cell_type": "markdown", - "id": "22896049-441d-4baf-b717-415123cb39d7", + "id": "268791c4", "metadata": { "tags": [] }, "source": [ - "### ๐Ÿ›  Feature Engineering" + "## ๐Ÿ›  Feature Engineering" ] }, { "cell_type": "code", - "execution_count": null, - "id": "140b468a-e0c2-44a1-8e44-4cf393407eca", + "execution_count": 20, + "id": "aff7a97b", "metadata": { "tags": [] }, @@ -388,12 +685,23 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "87dc89c0-72a7-4be6-b4e4-03d5d32be546", + "execution_count": 21, + "id": "1d45e480", "metadata": { "tags": [] }, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "0" + ] + }, + "execution_count": 21, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# Apply feature engineering to the df_air_quality DataFrame using the air_quality.feature_engineer_aq() function\n", "df_air_quality = air_quality.feature_engineer_aq(df_air_quality)\n", @@ -407,12 +715,23 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "94f67c89-6b39-4748-b4be-6ed3c9d57f96", + "execution_count": 22, + "id": "02c8e1e5", "metadata": { "tags": [] }, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "(154533, 31)" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# Print the shape (number of rows and columns) of the df_air_quality DataFrame\n", "df_air_quality.shape" @@ -420,12 +739,32 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "ed9bc7f1-d62e-4b1f-97af-6ecd30fe4b67", + "execution_count": 23, + "id": "4c627429", "metadata": { "tags": [] }, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "Index(['city_name', 'date', 'pm2_5', 'pm_2_5_previous_1_day',\n", + " 'pm_2_5_previous_2_day', 'pm_2_5_previous_3_day',\n", + " 'pm_2_5_previous_4_day', 'pm_2_5_previous_5_day',\n", + " 'pm_2_5_previous_6_day', 'pm_2_5_previous_7_day', 'mean_7_days',\n", + " 'mean_14_days', 'mean_28_days', 'std_7_days', 'exp_mean_7_days',\n", + " 'exp_std_7_days', 'std_14_days', 'exp_mean_14_days', 'exp_std_14_days',\n", + " 'std_28_days', 'exp_mean_28_days', 'exp_std_28_days', 'year',\n", + " 'day_of_month', 'month', 'day_of_week', 'is_weekend', 'sin_day_of_year',\n", + " 'cos_day_of_year', 'sin_day_of_week', 'cos_day_of_week'],\n", + " dtype='object')" + ] + }, + "execution_count": 23, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# Retrieve and display the column names of the df_air_quality DataFrame\n", "df_air_quality.columns" @@ -433,15 +772,7 @@ }, { "cell_type": "markdown", - "id": "88a9e0ef-e9d2-4e3c-91af-c4e619b8c906", - "metadata": {}, - "source": [ - "---" - ] - }, - { - "cell_type": "markdown", - "id": "4687e802", + "id": "4296b629", "metadata": { "tags": [] }, @@ -451,42 +782,124 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "c46283b4", + "execution_count": 27, + "id": "52e4eb11", "metadata": {}, - "outputs": [], + "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", + "
city_namedatetemperature_maxtemperature_minprecipitation_sumrain_sumsnowfall_sumprecipitation_hourswind_speed_maxwind_gusts_maxwind_direction_dominant
0Amsterdam2013-01-019.25.510.210.20.014.032.062.6255
1Amsterdam2013-01-027.85.60.50.50.02.022.939.6251
2Amsterdam2013-01-0310.38.22.02.00.06.022.239.2255
\n", + "
" + ], + "text/plain": [ + " city_name date temperature_max temperature_min precipitation_sum \\\n", + "0 Amsterdam 2013-01-01 9.2 5.5 10.2 \n", + "1 Amsterdam 2013-01-02 7.8 5.6 0.5 \n", + "2 Amsterdam 2013-01-03 10.3 8.2 2.0 \n", + "\n", + " rain_sum snowfall_sum precipitation_hours wind_speed_max \\\n", + "0 10.2 0.0 14.0 32.0 \n", + "1 0.5 0.0 2.0 22.9 \n", + "2 2.0 0.0 6.0 22.2 \n", + "\n", + " wind_gusts_max wind_direction_dominant \n", + "0 62.6 255 \n", + "1 39.6 251 \n", + "2 39.2 255 " + ] + }, + "execution_count": 27, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# Read the CSV file from the specified URL into a pandas DataFrame for weather data\n", - "df_weather = pd.read_csv(\"https://repo.hops.works/dev/davit/air_quality/backfill_weather.csv\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "1921b61c-d002-417e-88a6-9fe1cad0a7d4", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "# Count the occurrences of each unique value in the 'city_name' column of the df_weather DataFrame\n", - "df_weather.city_name.value_counts()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "8d5dcd0a", - "metadata": {}, - "outputs": [], - "source": [ + "df_weather = pd.read_csv(\"https://repo.hops.works/dev/davit/air_quality/backfill_weather.csv\")\n", + "\n", "# Display the first three rows of the df_weather DataFrame\n", "df_weather.head(3)" ] }, { "cell_type": "markdown", - "id": "cc9b7ad6", + "id": "cd0d4d7b", "metadata": {}, "source": [ "---" @@ -494,8 +907,8 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "a8f886c3-a5ac-4370-a6a2-22838ab7409e", + "execution_count": 28, + "id": "ec1e91c1", "metadata": { "tags": [] }, @@ -516,26 +929,29 @@ }, { "cell_type": "markdown", - "id": "f2ebd846-0420-4e4c-8a5b-0827fa91c693", + "id": "472f7eb5", "metadata": {}, "source": [ - "---" - ] - }, - { - "cell_type": "markdown", - "id": "cb6f83ba", - "metadata": {}, - "source": [ - "### ๐Ÿ”ฎ Connecting to Hopsworks Feature Store " + "## ๐Ÿ”ฎ Connecting to Hopsworks Feature Store " ] }, { "cell_type": "code", - "execution_count": null, - "id": "dd068240", + "execution_count": 29, + "id": "410f0b7b", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Connected. Call `.close()` to terminate connection gracefully.\n", + "\n", + "Logged in to project, explore it here https://snurran.hops.works/p/5242\n", + "Connected. Call `.close()` to terminate connection gracefully.\n" + ] + } + ], "source": [ "import hopsworks\n", "\n", @@ -546,7 +962,7 @@ }, { "cell_type": "markdown", - "id": "63d8c3b9", + "id": "6176991c", "metadata": {}, "source": [ "## ๐Ÿช„ Creating Feature Groups" @@ -554,7 +970,7 @@ }, { "cell_type": "markdown", - "id": "4a2515c4", + "id": "370bbf0b", "metadata": {}, "source": [ "### ๐ŸŒซ Air Quality Data" @@ -562,13 +978,62 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "9d7088a8", + "execution_count": 30, + "id": "b5a58bfc", "metadata": { "scrolled": true, "tags": [] }, - "outputs": [], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "DeprecationWarning: Providing event_time as a single-element list is deprecated and will be dropped in future versions. Provide the feature_name string instead.\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Feature Group created successfully, explore it at \n", + "https://snurran.hops.works/p/5242/fs/5190/fg/5194\n" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "6677d288bc0746a48faf802be7032e40", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Uploading Dataframe: 0.00% | | Rows 0/154533 | Elapsed Time: 00:00 | Remaining Time: ?" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Launching job: air_quality_1_offline_fg_materialization\n", + "Job started successfully, you can follow the progress at \n", + "https://snurran.hops.works/p/5242/jobs/named/air_quality_1_offline_fg_materialization/executions\n" + ] + }, + { + "data": { + "text/plain": [ + "(, None)" + ] + }, + "execution_count": 30, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# Get or create feature group\n", "air_quality_fg = fs.get_or_create_feature_group(\n", @@ -577,23 +1042,14 @@ " version=1,\n", " primary_key=['unix_time','city_name'],\n", " event_time=[\"unix_time\"],\n", - ") " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "7e04a975-bb58-42e2-9abd-90e68ae37864", - "metadata": {}, - "outputs": [], - "source": [ + ") \n", "# Insert data\n", "air_quality_fg.insert(df_air_quality)" ] }, { "cell_type": "markdown", - "id": "a73a9029", + "id": "09cbe8aa", "metadata": {}, "source": [ "### ๐ŸŒฆ Weather Data" @@ -601,10 +1057,52 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "acc2b799", + "execution_count": 31, + "id": "089f45b7", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Feature Group created successfully, explore it at \n", + "https://snurran.hops.works/p/5242/fs/5190/fg/5195\n" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "7328db1bd3b84e769e7b4ac88c1416a6", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Uploading Dataframe: 0.00% | | Rows 0/168975 | Elapsed Time: 00:00 | Remaining Time: ?" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Launching job: weather_1_offline_fg_materialization\n", + "Job started successfully, you can follow the progress at \n", + "https://snurran.hops.works/p/5242/jobs/named/weather_1_offline_fg_materialization/executions\n" + ] + }, + { + "data": { + "text/plain": [ + "(, None)" + ] + }, + "execution_count": 31, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# Get or create feature group\n", "weather_fg = fs.get_or_create_feature_group(\n", @@ -613,27 +1111,17 @@ " version=1,\n", " primary_key=['unix_time','city_name'],\n", " event_time=[\"unix_time\"],\n", - ") " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "9583b4d1-e2e3-4f56-9e5d-23caa0c49457", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ + ") \n", "# Insert data\n", "weather_fg.insert(df_weather)" ] }, { "cell_type": "markdown", - "id": "87c668dd", + "id": "34f5ffec", "metadata": {}, "source": [ + "---\n", "## โญ๏ธ **Next:** Part 02: Feature Pipeline \n", " \n", "\n", @@ -643,7 +1131,7 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3 (ipykernel)", + "display_name": "Python 3", "language": "python", "name": "python3" }, @@ -657,7 +1145,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.18" + "version": "3.10.13" } }, "nbformat": 4, diff --git a/advanced_tutorials/air_quality/2_air_quality_feature_pipeline.ipynb b/advanced_tutorials/air_quality/2_air_quality_feature_pipeline.ipynb index 580e8fc7..8f159c75 100644 --- a/advanced_tutorials/air_quality/2_air_quality_feature_pipeline.ipynb +++ b/advanced_tutorials/air_quality/2_air_quality_feature_pipeline.ipynb @@ -2,19 +2,21 @@ "cells": [ { "cell_type": "markdown", - "id": "dd094af7", + "id": "f16a717d", "metadata": {}, "source": [ "# **Hopsworks Feature Store** - Part 02: Feature Pipeline\n", "\n", "## ๐Ÿ—’๏ธ This notebook is divided into the following sections:\n", - "1. Parse Data\n", - "2. Feature Group Insertion" + "\n", + "1. Fetch Feature Groups. \n", + "2. Parse Data.\n", + "3. Feature Group Insertion." ] }, { "cell_type": "markdown", - "id": "a7dcc328", + "id": "37facd6e", "metadata": {}, "source": [ "### ๐Ÿ“ Imports" @@ -22,19 +24,21 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "364e961e", + "execution_count": 1, + "id": "77d2fbe5", "metadata": {}, "outputs": [], "source": [ "import datetime\n", "import time\n", - "import requests\n", "import pandas as pd\n", "import json\n", "\n", "from features import air_quality\n", - "from functions import *\n", + "from functions.parse_air_quality import get_aqi_data_from_open_meteo\n", + "from functions.parse_weather import get_weather_data_from_open_meteo\n", + "from functions.common_functions import *\n", + "\n", "\n", "import warnings\n", "warnings.filterwarnings(\"ignore\")" @@ -42,8 +46,8 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "50d04cc5-6788-4a4c-9f87-c2e00b5fce49", + "execution_count": 2, + "id": "c14c97e6", "metadata": { "tags": [] }, @@ -57,12 +61,23 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "b0d2261f-8907-44f4-9f1a-bd9ec5e1556f", + "execution_count": 3, + "id": "5b67e039", "metadata": { "tags": [] }, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "(datetime.date(2024, 3, 17), '2024-03-17')" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# Getting the current date\n", "today = datetime.date.today()\n", @@ -73,7 +88,7 @@ }, { "cell_type": "markdown", - "id": "d406b01d", + "id": "0c5ebe2a", "metadata": {}, "source": [ "### ๐Ÿ”ฎ Connecting to Hopsworks Feature Store " @@ -81,16 +96,35 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "8ba3cb02", + "execution_count": 4, + "id": "730eb857", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Connected. Call `.close()` to terminate connection gracefully.\n", + "\n", + "Logged in to project, explore it here https://snurran.hops.works/p/5242\n", + "Connected. Call `.close()` to terminate connection gracefully.\n" + ] + } + ], "source": [ "import hopsworks\n", "\n", "project = hopsworks.login()\n", - "fs = project.get_feature_store() \n", - "\n", + "fs = project.get_feature_store() " + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "ddd400ad", + "metadata": {}, + "outputs": [], + "source": [ "# Retrieve feature groups\n", "air_quality_fg = fs.get_feature_group(\n", " name='air_quality',\n", @@ -104,15 +138,7 @@ }, { "cell_type": "markdown", - "id": "c7f61053-a8c0-48a7-afa4-0e8733d2a54a", - "metadata": {}, - "source": [ - "---" - ] - }, - { - "cell_type": "markdown", - "id": "459ee37e-7e74-4051-97f6-2e03f9cac9d8", + "id": "f992009d", "metadata": {}, "source": [ "## ๐ŸŒซ Filling gaps in Air Quality data (PM2.5)" @@ -120,12 +146,21 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "76ae9dd9-ab28-41d1-8478-5af27b7f767e", + "execution_count": 6, + "id": "37058d7f", "metadata": { "tags": [] }, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Finished: Reading data from Hopsworks, using ArrowFlight (2.30s) \n", + "Finished: Reading data from Hopsworks, using ArrowFlight (1.48s) \n" + ] + } + ], "source": [ "# Read data from feature groups\n", "df_air_quality = air_quality_fg.read()\n", @@ -134,8 +169,8 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "03063bc6-b58f-47f4-bfc6-8020ec196478", + "execution_count": 7, + "id": "cee48adb", "metadata": { "tags": [] }, @@ -154,12 +189,21 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "5e868bdf-e91a-410a-b654-a315c605f3dc", + "execution_count": 8, + "id": "49b9e259", "metadata": { "tags": [] }, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "โ›ณ๏ธ Last update for Paris: 2024-03-15\n", + "โ›ณ๏ธ Last update for Columbus: 2024-03-15\n" + ] + } + ], "source": [ "# Accessing the last updated date for the city of Paris\n", "paris_last_date = last_dates_aq.get(\"Paris\", \"Not available\")\n", @@ -172,9 +216,22 @@ "print(\"โ›ณ๏ธ Last update for Columbus:\", columbus_last_date)" ] }, + { + "cell_type": "code", + "execution_count": 9, + "id": "f658d581", + "metadata": {}, + "outputs": [], + "source": [ + "for city, date in last_dates_aq.items():\n", + " city_last_date = datetime.datetime.strptime(date, \"%Y-%m-%d\").date()\n", + " if (today - city_last_date) <= datetime.timedelta(days=28):\n", + " last_dates_aq[city] = (city_last_date - datetime.timedelta(days=28)).strftime(\"%Y-%m-%d\")" + ] + }, { "cell_type": "markdown", - "id": "77c4ee8d-7f7e-4bd0-a97b-c3ac0d7db50f", + "id": "f6df0f7f", "metadata": {}, "source": [ "### ๐Ÿง™๐Ÿผโ€โ™‚๏ธ Parsing PM2.5 data" @@ -182,13 +239,159 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "112a7974-37cb-4195-bc71-328af428c491", + "execution_count": 10, + "id": "cc68ab56", "metadata": { "scrolled": true, "tags": [] }, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Processed PM2_5 for Amsterdam since 2024-02-16 till 2024-03-17.\n", + "Took 0.11 sec.\n", + "\n", + "Processed PM2_5 for Athina since 2024-02-16 till 2024-03-17.\n", + "Took 0.11 sec.\n", + "\n", + "Processed PM2_5 for Berlin since 2024-02-16 till 2024-03-17.\n", + "Took 0.11 sec.\n", + "\n", + "Processed PM2_5 for Gdansk since 2024-02-16 till 2024-03-17.\n", + "Took 0.11 sec.\n", + "\n", + "Processed PM2_5 for Krakรณw since 2024-02-16 till 2024-03-17.\n", + "Took 0.11 sec.\n", + "\n", + "Processed PM2_5 for London since 2024-02-16 till 2024-03-17.\n", + "Took 0.11 sec.\n", + "\n", + "Processed PM2_5 for Madrid since 2024-02-16 till 2024-03-17.\n", + "Took 0.1 sec.\n", + "\n", + "Processed PM2_5 for Marseille since 2024-02-16 till 2024-03-17.\n", + "Took 0.1 sec.\n", + "\n", + "Processed PM2_5 for Milano since 2024-02-16 till 2024-03-17.\n", + "Took 0.11 sec.\n", + "\n", + "Processed PM2_5 for Mรผnchen since 2024-02-16 till 2024-03-17.\n", + "Took 0.11 sec.\n", + "\n", + "Processed PM2_5 for Napoli since 2024-02-16 till 2024-03-17.\n", + "Took 0.11 sec.\n", + "\n", + "Processed PM2_5 for Paris since 2024-02-16 till 2024-03-17.\n", + "Took 0.11 sec.\n", + "\n", + "Processed PM2_5 for Sevilla since 2024-02-16 till 2024-03-17.\n", + "Took 0.11 sec.\n", + "\n", + "Processed PM2_5 for Stockholm since 2024-02-16 till 2024-03-17.\n", + "Took 0.11 sec.\n", + "\n", + "Processed PM2_5 for Tallinn since 2024-02-16 till 2024-03-17.\n", + "Took 0.1 sec.\n", + "\n", + "Processed PM2_5 for Varna since 2024-02-16 till 2024-03-17.\n", + "Took 0.1 sec.\n", + "\n", + "Processed PM2_5 for Wien since 2024-02-16 till 2024-03-17.\n", + "Took 0.1 sec.\n", + "\n", + "Processed PM2_5 for Albuquerque since 2024-02-16 till 2024-03-17.\n", + "Took 0.1 sec.\n", + "\n", + "Processed PM2_5 for Atlanta since 2024-02-16 till 2024-03-17.\n", + "Took 0.1 sec.\n", + "\n", + "Processed PM2_5 for Chicago since 2024-02-16 till 2024-03-17.\n", + "Took 0.11 sec.\n", + "\n", + "Processed PM2_5 for Columbus since 2024-02-16 till 2024-03-17.\n", + "Took 0.11 sec.\n", + "\n", + "Processed PM2_5 for Dallas since 2024-02-16 till 2024-03-17.\n", + "Took 0.1 sec.\n", + "\n", + "Processed PM2_5 for Denver since 2024-02-16 till 2024-03-17.\n", + "Took 0.1 sec.\n", + "\n", + "Processed PM2_5 for Houston since 2024-02-16 till 2024-03-17.\n", + "Took 0.1 sec.\n", + "\n", + "Processed PM2_5 for Los Angeles since 2024-02-16 till 2024-03-17.\n", + "Took 0.1 sec.\n", + "\n", + "Processed PM2_5 for New York since 2024-02-16 till 2024-03-17.\n", + "Took 0.1 sec.\n", + "\n", + "Processed PM2_5 for Phoenix-Mesa since 2024-02-16 till 2024-03-17.\n", + "Took 0.1 sec.\n", + "\n", + "Processed PM2_5 for Salt Lake City since 2024-02-16 till 2024-03-17.\n", + "Took 0.11 sec.\n", + "\n", + "Processed PM2_5 for San Francisco since 2024-02-16 till 2024-03-17.\n", + "Took 0.1 sec.\n", + "\n", + "Processed PM2_5 for Tampa since 2024-02-16 till 2024-03-17.\n", + "Took 0.1 sec.\n", + "\n", + "Processed PM2_5 for Bellevue-SE 12th St since 2024-02-16 till 2024-03-17.\n", + "Took 0.1 sec.\n", + "\n", + "Processed PM2_5 for DARRINGTON - FIR ST (Darrington High School) since 2024-02-16 till 2024-03-17.\n", + "Took 0.1 sec.\n", + "\n", + "Processed PM2_5 for KENT - JAMES & CENTRAL since 2024-02-16 till 2024-03-17.\n", + "Took 0.1 sec.\n", + "\n", + "Processed PM2_5 for LAKE FOREST PARK TOWNE CENTER since 2024-02-16 till 2024-03-17.\n", + "Took 0.1 sec.\n", + "\n", + "Processed PM2_5 for MARYSVILLE - 7TH AVE (Marysville Junior High) since 2024-02-16 till 2024-03-17.\n", + "Took 0.1 sec.\n", + "\n", + "Processed PM2_5 for NORTH BEND - NORTH BEND WAY since 2024-02-16 till 2024-03-17.\n", + "Took 0.1 sec.\n", + "\n", + "Processed PM2_5 for SEATTLE - BEACON HILL since 2024-02-16 till 2024-03-17.\n", + "Took 0.11 sec.\n", + "\n", + "Processed PM2_5 for SEATTLE - DUWAMISH since 2024-02-16 till 2024-03-17.\n", + "Took 0.11 sec.\n", + "\n", + "Processed PM2_5 for SEATTLE - SOUTH PARK #2 since 2024-02-16 till 2024-03-17.\n", + "Took 0.11 sec.\n", + "\n", + "Processed PM2_5 for Seattle-10th & Weller since 2024-02-16 till 2024-03-17.\n", + "Took 0.13 sec.\n", + "\n", + "Processed PM2_5 for TACOMA - ALEXANDER AVE since 2024-02-16 till 2024-03-17.\n", + "Took 0.1 sec.\n", + "\n", + "Processed PM2_5 for TACOMA - L STREET since 2024-02-16 till 2024-03-17.\n", + "Took 0.66 sec.\n", + "\n", + "Processed PM2_5 for Tacoma-S 36th St since 2024-02-16 till 2024-03-17.\n", + "Took 0.11 sec.\n", + "\n", + "Processed PM2_5 for Tukwila Allentown since 2024-02-16 till 2024-03-17.\n", + "Took 0.11 sec.\n", + "\n", + "Processed PM2_5 for Tulalip-Totem Beach Rd since 2024-02-16 till 2024-03-17.\n", + "Took 0.11 sec.\n", + "\n", + "----------------------------------------------------------------\n", + "Parsed new PM2.5 data for ALL locations up to 2024-03-17.\n", + "Took 5.44 sec.\n", + "\n" + ] + } + ], "source": [ "# Storing the current time as the start time of the cell execution\n", "start_of_cell = time.time()\n", @@ -223,17 +426,78 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "1afdc6a5", + "execution_count": 11, + "id": "09db1460", "metadata": {}, - "outputs": [], + "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", + "
city_namedatepm2_5
1392Tulalip-Totem Beach Rd2024-03-1514.5
1393Tulalip-Totem Beach Rd2024-03-1613.7
1394Tulalip-Totem Beach Rd2024-03-1715.1
\n", + "
" + ], + "text/plain": [ + " city_name date pm2_5\n", + "1392 Tulalip-Totem Beach Rd 2024-03-15 14.5\n", + "1393 Tulalip-Totem Beach Rd 2024-03-16 13.7\n", + "1394 Tulalip-Totem Beach Rd 2024-03-17 15.1" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "df_aq_raw.tail(3)" ] }, { "cell_type": "markdown", - "id": "250d9daf-83fa-49f1-bcd8-4efaeb90b99c", + "id": "7cbabf34", "metadata": { "tags": [] }, @@ -243,8 +507,8 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "140b468a-e0c2-44a1-8e44-4cf393407eca", + "execution_count": 12, + "id": "100f0f2d", "metadata": { "tags": [] }, @@ -256,27 +520,200 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "acc181a9-6183-45ec-aed2-8ee684e13b39", + "execution_count": 13, + "id": "91ded83e", "metadata": { "tags": [] }, - "outputs": [], + "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", + "
city_namedatepm2_5pm_2_5_previous_1_daypm_2_5_previous_2_daypm_2_5_previous_3_daypm_2_5_previous_4_daypm_2_5_previous_5_daypm_2_5_previous_6_daypm_2_5_previous_7_day...exp_std_28_daysyearday_of_monthmonthday_of_weekis_weekendsin_day_of_yearcos_day_of_yearsin_day_of_weekcos_day_of_week
1392Athina2024-03-1725.625.820.214.88.410.39.617.2...5.3762532024173610.9700640.24285-0.7818310.62349
1393Los Angeles2024-03-1728.121.515.112.716.411.027.036.3...7.6965752024173610.9700640.24285-0.7818310.62349
1394Milano2024-03-1743.423.616.846.632.417.021.210.4...22.0443942024173610.9700640.24285-0.7818310.62349
\n", + "

3 rows ร— 31 columns

\n", + "
" + ], + "text/plain": [ + " city_name date pm2_5 pm_2_5_previous_1_day \\\n", + "1392 Athina 2024-03-17 25.6 25.8 \n", + "1393 Los Angeles 2024-03-17 28.1 21.5 \n", + "1394 Milano 2024-03-17 43.4 23.6 \n", + "\n", + " pm_2_5_previous_2_day pm_2_5_previous_3_day pm_2_5_previous_4_day \\\n", + "1392 20.2 14.8 8.4 \n", + "1393 15.1 12.7 16.4 \n", + "1394 16.8 46.6 32.4 \n", + "\n", + " pm_2_5_previous_5_day pm_2_5_previous_6_day pm_2_5_previous_7_day \\\n", + "1392 10.3 9.6 17.2 \n", + "1393 11.0 27.0 36.3 \n", + "1394 17.0 21.2 10.4 \n", + "\n", + " ... exp_std_28_days year day_of_month month day_of_week \\\n", + "1392 ... 5.376253 2024 17 3 6 \n", + "1393 ... 7.696575 2024 17 3 6 \n", + "1394 ... 22.044394 2024 17 3 6 \n", + "\n", + " is_weekend sin_day_of_year cos_day_of_year sin_day_of_week \\\n", + "1392 1 0.970064 0.24285 -0.781831 \n", + "1393 1 0.970064 0.24285 -0.781831 \n", + "1394 1 0.970064 0.24285 -0.781831 \n", + "\n", + " cos_day_of_week \n", + "1392 0.62349 \n", + "1393 0.62349 \n", + "1394 0.62349 \n", + "\n", + "[3 rows x 31 columns]" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# Applying a feature engineering function 'feature_engineer_aq' to the 'df_aq_update' DataFrame\n", "df_aq_update = air_quality.feature_engineer_aq(df_aq_raw)\n", "\n", "# Dropping rows with missing values in the 'df_aq_update' DataFrame\n", "df_aq_update = df_aq_update.dropna()\n", + "\n", "df_aq_update.tail(3)" ] }, { "cell_type": "code", - "execution_count": null, - "id": "0364873c", + "execution_count": 14, + "id": "a387ac8a", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "0" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# Checking the total number of missing values in the 'df_aq_update' DataFrame\n", "df_aq_update.isna().sum().sum()" @@ -284,12 +721,23 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "94f67c89-6b39-4748-b4be-6ed3c9d57f96", + "execution_count": 15, + "id": "bb4b8914", "metadata": { "tags": [] }, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "(135, 31)" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# Retrieving the dimensions (number of rows and columns) of the 'df_aq_update' DataFrame\n", "df_aq_update.shape" @@ -297,15 +745,7 @@ }, { "cell_type": "markdown", - "id": "d74f5622-6f57-47b9-ac0b-dfb6617847b2", - "metadata": {}, - "source": [ - "---" - ] - }, - { - "cell_type": "markdown", - "id": "95a34c64-5b94-4c4f-b03d-14e12a106f25", + "id": "fe63cf55", "metadata": {}, "source": [ "## ๐ŸŒฆ Filling gaps in Weather data" @@ -313,8 +753,8 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "46009853-160c-467e-abb0-3145d27c57dc", + "execution_count": 16, + "id": "36a7388e", "metadata": { "tags": [] }, @@ -333,7 +773,7 @@ }, { "cell_type": "markdown", - "id": "1fd15812-a3a9-488c-879e-181c7b815357", + "id": "6c868dae", "metadata": { "tags": [] }, @@ -343,13 +783,159 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "ef027d28-3443-4c7c-9e85-783625301a14", + "execution_count": 17, + "id": "0ad03c2d", "metadata": { "scrolled": true, "tags": [] }, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Parsed weather for Amsterdam since 2024-03-15 till 2024-03-17.\n", + "Took 2.11 sec.\n", + "\n", + "Parsed weather for Athina since 2024-03-15 till 2024-03-17.\n", + "Took 2.1 sec.\n", + "\n", + "Parsed weather for Berlin since 2024-03-15 till 2024-03-17.\n", + "Took 2.1 sec.\n", + "\n", + "Parsed weather for Gdansk since 2024-03-15 till 2024-03-17.\n", + "Took 2.1 sec.\n", + "\n", + "Parsed weather for Krakรณw since 2024-03-15 till 2024-03-17.\n", + "Took 2.1 sec.\n", + "\n", + "Parsed weather for London since 2024-03-15 till 2024-03-17.\n", + "Took 2.1 sec.\n", + "\n", + "Parsed weather for Madrid since 2024-03-15 till 2024-03-17.\n", + "Took 2.11 sec.\n", + "\n", + "Parsed weather for Marseille since 2024-03-15 till 2024-03-17.\n", + "Took 2.11 sec.\n", + "\n", + "Parsed weather for Milano since 2024-03-15 till 2024-03-17.\n", + "Took 2.1 sec.\n", + "\n", + "Parsed weather for Mรผnchen since 2024-03-15 till 2024-03-17.\n", + "Took 2.1 sec.\n", + "\n", + "Parsed weather for Napoli since 2024-03-15 till 2024-03-17.\n", + "Took 2.11 sec.\n", + "\n", + "Parsed weather for Paris since 2024-03-15 till 2024-03-17.\n", + "Took 2.11 sec.\n", + "\n", + "Parsed weather for Sevilla since 2024-03-15 till 2024-03-17.\n", + "Took 2.1 sec.\n", + "\n", + "Parsed weather for Stockholm since 2024-03-15 till 2024-03-17.\n", + "Took 2.1 sec.\n", + "\n", + "Parsed weather for Tallinn since 2024-03-15 till 2024-03-17.\n", + "Took 2.1 sec.\n", + "\n", + "Parsed weather for Varna since 2024-03-15 till 2024-03-17.\n", + "Took 2.11 sec.\n", + "\n", + "Parsed weather for Wien since 2024-03-15 till 2024-03-17.\n", + "Took 2.1 sec.\n", + "\n", + "Parsed weather for Albuquerque since 2024-03-15 till 2024-03-17.\n", + "Took 2.1 sec.\n", + "\n", + "Parsed weather for Atlanta since 2024-03-15 till 2024-03-17.\n", + "Took 2.1 sec.\n", + "\n", + "Parsed weather for Chicago since 2024-03-15 till 2024-03-17.\n", + "Took 2.1 sec.\n", + "\n", + "Parsed weather for Columbus since 2024-03-15 till 2024-03-17.\n", + "Took 2.1 sec.\n", + "\n", + "Parsed weather for Dallas since 2024-03-15 till 2024-03-17.\n", + "Took 2.1 sec.\n", + "\n", + "Parsed weather for Denver since 2024-03-15 till 2024-03-17.\n", + "Took 2.1 sec.\n", + "\n", + "Parsed weather for Houston since 2024-03-15 till 2024-03-17.\n", + "Took 2.11 sec.\n", + "\n", + "Parsed weather for Los Angeles since 2024-03-15 till 2024-03-17.\n", + "Took 2.1 sec.\n", + "\n", + "Parsed weather for New York since 2024-03-15 till 2024-03-17.\n", + "Took 2.1 sec.\n", + "\n", + "Parsed weather for Phoenix-Mesa since 2024-03-15 till 2024-03-17.\n", + "Took 2.1 sec.\n", + "\n", + "Parsed weather for Salt Lake City since 2024-03-15 till 2024-03-17.\n", + "Took 2.1 sec.\n", + "\n", + "Parsed weather for San Francisco since 2024-03-15 till 2024-03-17.\n", + "Took 2.11 sec.\n", + "\n", + "Parsed weather for Tampa since 2024-03-15 till 2024-03-17.\n", + "Took 2.1 sec.\n", + "\n", + "Parsed weather for Bellevue-SE 12th St since 2024-03-15 till 2024-03-17.\n", + "Took 2.1 sec.\n", + "\n", + "Parsed weather for DARRINGTON - FIR ST (Darrington High School) since 2024-03-15 till 2024-03-17.\n", + "Took 2.11 sec.\n", + "\n", + "Parsed weather for KENT - JAMES & CENTRAL since 2024-03-15 till 2024-03-17.\n", + "Took 2.11 sec.\n", + "\n", + "Parsed weather for LAKE FOREST PARK TOWNE CENTER since 2024-03-15 till 2024-03-17.\n", + "Took 2.12 sec.\n", + "\n", + "Parsed weather for MARYSVILLE - 7TH AVE (Marysville Junior High) since 2024-03-15 till 2024-03-17.\n", + "Took 2.12 sec.\n", + "\n", + "Parsed weather for NORTH BEND - NORTH BEND WAY since 2024-03-15 till 2024-03-17.\n", + "Took 2.11 sec.\n", + "\n", + "Parsed weather for SEATTLE - BEACON HILL since 2024-03-15 till 2024-03-17.\n", + "Took 2.11 sec.\n", + "\n", + "Parsed weather for SEATTLE - DUWAMISH since 2024-03-15 till 2024-03-17.\n", + "Took 2.11 sec.\n", + "\n", + "Parsed weather for SEATTLE - SOUTH PARK #2 since 2024-03-15 till 2024-03-17.\n", + "Took 2.11 sec.\n", + "\n", + "Parsed weather for Seattle-10th & Weller since 2024-03-15 till 2024-03-17.\n", + "Took 2.11 sec.\n", + "\n", + "Parsed weather for TACOMA - ALEXANDER AVE since 2024-03-15 till 2024-03-17.\n", + "Took 2.1 sec.\n", + "\n", + "Parsed weather for TACOMA - L STREET since 2024-03-15 till 2024-03-17.\n", + "Took 2.1 sec.\n", + "\n", + "Parsed weather for Tacoma-S 36th St since 2024-03-15 till 2024-03-17.\n", + "Took 2.1 sec.\n", + "\n", + "Parsed weather for Tukwila Allentown since 2024-03-15 till 2024-03-17.\n", + "Took 2.11 sec.\n", + "\n", + "Parsed weather for Tulalip-Totem Beach Rd since 2024-03-15 till 2024-03-17.\n", + "Took 2.1 sec.\n", + "\n", + "----------------------------------------------------------------\n", + "Parsed new weather data for ALL cities up to 2024-03-17.\n", + "Took 94.79 sec.\n", + "\n" + ] + } + ], "source": [ "# Storing the current time as the start time of the cell execution\n", "start_of_cell = time.time()\n", @@ -388,8 +974,8 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "a7bff400-a2fb-48a3-a07b-5bd2a0469cd7", + "execution_count": 18, + "id": "de4d4870", "metadata": { "tags": [] }, @@ -410,12 +996,119 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "11752b30-2f40-4668-9813-2a90199c62b8", + "execution_count": 19, + "id": "82d7bc88", "metadata": { "tags": [] }, - "outputs": [], + "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", + "
city_namedatetemperature_maxtemperature_minprecipitation_sumrain_sumsnowfall_sumprecipitation_hourswind_speed_maxwind_gusts_maxwind_direction_dominantunix_time
132Tulalip-Totem Beach Rd2024-03-1515.34.20.00.00.00.010.822.33431710460800000
133Tulalip-Totem Beach Rd2024-03-1621.34.80.00.00.00.09.825.93361710547200000
134Tulalip-Totem Beach Rd2024-03-1722.09.20.00.00.00.011.414.0931710633600000
\n", + "
" + ], + "text/plain": [ + " city_name date temperature_max temperature_min \\\n", + "132 Tulalip-Totem Beach Rd 2024-03-15 15.3 4.2 \n", + "133 Tulalip-Totem Beach Rd 2024-03-16 21.3 4.8 \n", + "134 Tulalip-Totem Beach Rd 2024-03-17 22.0 9.2 \n", + "\n", + " precipitation_sum rain_sum snowfall_sum precipitation_hours \\\n", + "132 0.0 0.0 0.0 0.0 \n", + "133 0.0 0.0 0.0 0.0 \n", + "134 0.0 0.0 0.0 0.0 \n", + "\n", + " wind_speed_max wind_gusts_max wind_direction_dominant unix_time \n", + "132 10.8 22.3 343 1710460800000 \n", + "133 9.8 25.9 336 1710547200000 \n", + "134 11.4 14.0 93 1710633600000 " + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# Converting the 'date' column in the 'df_aq_update' DataFrame to string format\n", "df_aq_update.date = df_aq_update.date.astype(str)\n", @@ -430,15 +1123,7 @@ }, { "cell_type": "markdown", - "id": "792dd383", - "metadata": {}, - "source": [ - "---" - ] - }, - { - "cell_type": "markdown", - "id": "5aef353d", + "id": "7b5640f9", "metadata": { "tags": [] }, @@ -448,10 +1133,44 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "f81bb922", + "execution_count": 20, + "id": "fd72be07", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "ad45db9921ea44f392976d59e79f3999", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Uploading Dataframe: 0.00% | | Rows 0/135 | Elapsed Time: 00:00 | Remaining Time: ?" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Launching job: air_quality_1_offline_fg_materialization\n", + "Job started successfully, you can follow the progress at \n", + "https://snurran.hops.works/p/5242/jobs/named/air_quality_1_offline_fg_materialization/executions\n" + ] + }, + { + "data": { + "text/plain": [ + "(, None)" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# Insert new data\n", "air_quality_fg.insert(df_aq_update)" @@ -459,10 +1178,44 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "be0c498e", + "execution_count": 21, + "id": "cdffe4a9", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "afe7919348224c17b0f680c8c8067507", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Uploading Dataframe: 0.00% | | Rows 0/135 | Elapsed Time: 00:00 | Remaining Time: ?" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Launching job: weather_1_offline_fg_materialization\n", + "Job started successfully, you can follow the progress at \n", + "https://snurran.hops.works/p/5242/jobs/named/weather_1_offline_fg_materialization/executions\n" + ] + }, + { + "data": { + "text/plain": [ + "(, None)" + ] + }, + "execution_count": 21, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# Insert new data\n", "weather_fg.insert(df_weather_update)" @@ -470,19 +1223,20 @@ }, { "cell_type": "markdown", - "id": "b50c64a1", + "id": "d03605ea", "metadata": {}, "source": [ + "---\n", "## โญ๏ธ **Next:** Part 03: Training Pipeline\n", " \n", "\n", - "In the following notebook you will read from a feature group and create training dataset within the feature store\n" + "In the following notebook you will create a feature view, create a training dataset, train a model and save it in the Hopsworks Model Registry." ] } ], "metadata": { "kernelspec": { - "display_name": "Python 3 (ipykernel)", + "display_name": "Python 3", "language": "python", "name": "python3" }, @@ -496,7 +1250,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.18" + "version": "3.10.11" }, "vscode": { "interpreter": { diff --git a/advanced_tutorials/air_quality/3_air_quality_training_pipeline.ipynb b/advanced_tutorials/air_quality/3_air_quality_training_pipeline.ipynb index 2104e1f5..2dc9af7b 100644 --- a/advanced_tutorials/air_quality/3_air_quality_training_pipeline.ipynb +++ b/advanced_tutorials/air_quality/3_air_quality_training_pipeline.ipynb @@ -2,28 +2,29 @@ "cells": [ { "cell_type": "markdown", - "id": "7eb83ff8", + "id": "3d8d5c4a", "metadata": { "tags": [] }, "source": [ "# **Hopsworks Feature Store** - Part 03: Training Pipeline\n", "\n", - "This notebook explains how to read from a feature group and create training dataset within the feature store\n", + "This notebook explains how to create a feature view, create a training dataset, train a model and save it in the Hopsworks Model Registry.\n", "\n", "## ๐Ÿ—’๏ธ This notebook is divided into the following sections:\n", "\n", - "1. Fetch Feature Groups\n", - "2. Define Transformation functions\n", - "4. Create Feature Views\n", - "5. Create Training Dataset with training, validation and test splits\n", + "1. Fetch Feature Groups.\n", + "2. Create a Feature View.\n", + "3. Create a Training Dataset.\n", + "4. Train a model.\n", + "5. Save trained model in the Model Registry.\n", "\n", "![part2](../../images/02_training-dataset.png) " ] }, { "cell_type": "markdown", - "id": "f3b5f602-a575-49a8-bce9-a997cca936e0", + "id": "e89e0ed8", "metadata": {}, "source": [ "### ๐Ÿ“ Imports" @@ -31,32 +32,25 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "ad609eec-0b46-445f-a0f5-5657e5f69866", - "metadata": {}, - "outputs": [], - "source": [ - "!pip install xgboost --q" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "b3f2ac81-423a-4380-8fd6-b70aa55eb864", + "execution_count": 1, + "id": "7e858f8a", "metadata": { "tags": [] }, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2024-03-12 15:53:54,685 INFO: generated new fontManager\n" + ] + } + ], "source": [ "import os\n", - "import datetime\n", - "import time\n", - "import json\n", - "import pickle\n", "import joblib\n", "\n", "import pandas as pd\n", - "import numpy as np\n", "\n", "import matplotlib.pyplot as plt\n", "import seaborn as sns\n", @@ -73,7 +67,7 @@ }, { "cell_type": "markdown", - "id": "a0b3bcd1", + "id": "e4d834bc", "metadata": {}, "source": [ "## ๐Ÿ“ก Connecting to Hopsworks Feature Store " @@ -81,10 +75,21 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "89ad779f", + "execution_count": 2, + "id": "817cdef7", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Connected. Call `.close()` to terminate connection gracefully.\n", + "\n", + "Logged in to project, explore it here https://snurran.hops.works/p/5242\n", + "Connected. Call `.close()` to terminate connection gracefully.\n" + ] + } + ], "source": [ "import hopsworks\n", "\n", @@ -95,8 +100,8 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "735a083e", + "execution_count": 3, + "id": "dff51a06", "metadata": {}, "outputs": [], "source": [ @@ -113,18 +118,16 @@ }, { "cell_type": "markdown", - "id": "be427dca", + "id": "45881fbc", "metadata": {}, "source": [ - "--- \n", - "\n", - "## ๐Ÿ– Feature View Creation and Retrieving " + "## ๐Ÿ– Feature View Creation and Retrieval " ] }, { "cell_type": "code", - "execution_count": null, - "id": "cc3192d3", + "execution_count": 4, + "id": "0d4e6eba", "metadata": {}, "outputs": [], "source": [ @@ -137,8 +140,8 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "b3b8ba7b-b0ab-4ea5-b050-f8e1faf43c27", + "execution_count": 5, + "id": "1d5cf648", "metadata": { "scrolled": true, "tags": [] @@ -151,7 +154,7 @@ }, { "cell_type": "markdown", - "id": "d83a1681", + "id": "82c5b7be", "metadata": {}, "source": [ "`Feature Views` stands between **Feature Groups** and **Training Dataset**. ะกombining **Feature Groups** we can create **Feature Views** which store a metadata of our data. Having **Feature Views** we can create **Training Dataset**.\n", @@ -175,8 +178,8 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "403df0b4", + "execution_count": 6, + "id": "c0d7fec3", "metadata": {}, "outputs": [], "source": [ @@ -190,19 +193,17 @@ }, { "cell_type": "markdown", - "id": "0c723c54", + "id": "8f12f3ac", "metadata": {}, "source": [ - "For now `Feature View` is saved in Hopsworks and you can retrieve it using `FeatureStore.get_feature_view()`." + "For now, your `Feature View` is saved in Hopsworks and you can retrieve it using `FeatureStore.get_feature_view()`." ] }, { "cell_type": "markdown", - "id": "6e1187a2", + "id": "72aeb854", "metadata": {}, "source": [ - "---\n", - "\n", "## ๐Ÿ‹๏ธ Training Dataset Creation\n", "\n", "In Hopsworks training data is a query where the projection (set of features) is determined by the parent FeatureView with an optional snapshot on disk of the data returned by the query.\n", @@ -228,10 +229,25 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "2f5bcf22-6ff1-4995-a8c3-11a1dab396a7", + "execution_count": 7, + "id": "317668a8", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Finished: Reading data from Hopsworks, using ArrowFlight (12.56s) \n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "VersionWarning: Incremented version to `2`.\n" + ] + } + ], "source": [ "X, _ = feature_view.training_data(\n", " description = 'Air Quality dataset',\n", @@ -240,15 +256,7 @@ }, { "cell_type": "markdown", - "id": "c995b340-5ba6-4116-b8b6-86ca34f0a0ab", - "metadata": {}, - "source": [ - "---" - ] - }, - { - "cell_type": "markdown", - "id": "95783124-8303-47c5-bd15-2804efa15611", + "id": "a18b3733", "metadata": {}, "source": [ "## ๐Ÿงฌ Modeling" @@ -256,25 +264,25 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "5b937dec", + "execution_count": 8, + "id": "16c721ea", "metadata": {}, "outputs": [], "source": [ - "# Creating a LabelEncoder object\n", + "# Create a LabelEncoder object\n", "label_encoder = LabelEncoder()\n", "\n", - "# Fitting the encoder to the data in the 'city_name' column\n", + "# Fit the encoder to the data in the 'city_name' column\n", "label_encoder.fit(X[['city_name']])\n", "\n", - "# Transforming the 'city_name' column data using the fitted encoder\n", + "# Transform the 'city_name' column data using the fitted encoder\n", "encoded = label_encoder.transform(X[['city_name']])" ] }, { "cell_type": "code", - "execution_count": null, - "id": "97cdb6bb-6c9c-44b7-9171-1b420bae9181", + "execution_count": 9, + "id": "ee1a5c8c", "metadata": { "tags": [] }, @@ -292,90 +300,342 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "4df41c7d-00bd-4203-90a1-8cc298508d68", + "execution_count": 10, + "id": "612ef824", "metadata": { "tags": [] }, "outputs": [], "source": [ - "# Extracting the target variable 'pm2_5' from the DataFrame 'X' and assigning it to the variable 'y'\n", + "# Extract the target variable 'pm2_5' from the DataFrame 'X' and assigning it to the variable 'y'\n", "y = X.pop('pm2_5')" ] }, { "cell_type": "code", - "execution_count": null, - "id": "d0299506-195f-4ebc-b43e-347fe59db31c", + "execution_count": 11, + "id": "02950e70", "metadata": { "tags": [] }, - "outputs": [], - "source": [ - "# Splitting the data into training and testing sets using the train_test_split function\n", + "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", + "
pm_2_5_previous_1_daypm_2_5_previous_2_daypm_2_5_previous_3_daypm_2_5_previous_4_daypm_2_5_previous_5_daypm_2_5_previous_6_daypm_2_5_previous_7_daymean_7_daysmean_14_daysmean_28_days...temperature_maxtemperature_minprecipitation_sumrain_sumsnowfall_sumprecipitation_hourswind_speed_maxwind_gusts_maxwind_direction_dominantcity_name_encoded
1058180.00.02.03.06.06.07.03.4285716.8571435.142857...5.13.71.61.60.06.027.247.9639
669794.73.73.98.015.314.210.68.6285718.8500009.428571...0.0-6.80.00.00.00.019.345.06036
12222310.012.04.010.03.04.04.06.71428610.00000010.964286...1.5-1.90.00.00.00.013.524.127911
\n", + "

3 rows ร— 38 columns

\n", + "
" + ], + "text/plain": [ + " pm_2_5_previous_1_day pm_2_5_previous_2_day pm_2_5_previous_3_day \\\n", + "105818 0.0 0.0 2.0 \n", + "66979 4.7 3.7 3.9 \n", + "122223 10.0 12.0 4.0 \n", + "\n", + " pm_2_5_previous_4_day pm_2_5_previous_5_day pm_2_5_previous_6_day \\\n", + "105818 3.0 6.0 6.0 \n", + "66979 8.0 15.3 14.2 \n", + "122223 10.0 3.0 4.0 \n", + "\n", + " pm_2_5_previous_7_day mean_7_days mean_14_days mean_28_days ... \\\n", + "105818 7.0 3.428571 6.857143 5.142857 ... \n", + "66979 10.6 8.628571 8.850000 9.428571 ... \n", + "122223 4.0 6.714286 10.000000 10.964286 ... \n", + "\n", + " temperature_max temperature_min precipitation_sum rain_sum \\\n", + "105818 5.1 3.7 1.6 1.6 \n", + "66979 0.0 -6.8 0.0 0.0 \n", + "122223 1.5 -1.9 0.0 0.0 \n", + "\n", + " snowfall_sum precipitation_hours wind_speed_max wind_gusts_max \\\n", + "105818 0.0 6.0 27.2 47.9 \n", + "66979 0.0 0.0 19.3 45.0 \n", + "122223 0.0 0.0 13.5 24.1 \n", + "\n", + " wind_direction_dominant city_name_encoded \n", + "105818 6 39 \n", + "66979 60 36 \n", + "122223 279 11 \n", + "\n", + "[3 rows x 38 columns]" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Split the data into training and testing sets using the train_test_split function\n", "X_train, X_test, y_train, y_test = train_test_split(\n", - " X, y, test_size=0.2, random_state=42)" + " X, \n", + " y, \n", + " test_size=0.2, \n", + " random_state=42,\n", + ")\n", + "\n", + "X_train.head(3)" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "b4ddfaeb", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "105818 2.0\n", + "66979 9.8\n", + "122223 11.0\n", + "Name: pm2_5, dtype: float64" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "y_train.head(3)" + ] + }, + { + "cell_type": "markdown", + "id": "59e85ea3", + "metadata": {}, + "source": [ + "## ๐Ÿƒ๐Ÿปโ€โ™‚๏ธ Model Training" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "44a6893f", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
XGBRegressor(base_score=None, booster=None, callbacks=None,\n",
+       "             colsample_bylevel=None, colsample_bynode=None,\n",
+       "             colsample_bytree=None, device=None, early_stopping_rounds=None,\n",
+       "             enable_categorical=False, eval_metric=None, feature_types=None,\n",
+       "             gamma=None, grow_policy=None, importance_type=None,\n",
+       "             interaction_constraints=None, learning_rate=None, max_bin=None,\n",
+       "             max_cat_threshold=None, max_cat_to_onehot=None,\n",
+       "             max_delta_step=None, max_depth=None, max_leaves=None,\n",
+       "             min_child_weight=None, missing=nan, monotone_constraints=None,\n",
+       "             multi_strategy=None, n_estimators=None, n_jobs=None,\n",
+       "             num_parallel_tree=None, random_state=None, ...)
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
" + ], + "text/plain": [ + "XGBRegressor(base_score=None, booster=None, callbacks=None,\n", + " colsample_bylevel=None, colsample_bynode=None,\n", + " colsample_bytree=None, device=None, early_stopping_rounds=None,\n", + " enable_categorical=False, eval_metric=None, feature_types=None,\n", + " gamma=None, grow_policy=None, importance_type=None,\n", + " interaction_constraints=None, learning_rate=None, max_bin=None,\n", + " max_cat_threshold=None, max_cat_to_onehot=None,\n", + " max_delta_step=None, max_depth=None, max_leaves=None,\n", + " min_child_weight=None, missing=nan, monotone_constraints=None,\n", + " multi_strategy=None, n_estimators=None, n_jobs=None,\n", + " num_parallel_tree=None, random_state=None, ...)" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Create an instance of the XGBoost Regressor\n", + "xgb_regressor = XGBRegressor()\n", + "\n", + "# Fit the XGBoost Regressor to the training data\n", + "xgb_regressor.fit(X_train, y_train)" ] }, { "cell_type": "markdown", - "id": "8fd4e24e-7f02-4944-a309-6475b65e7846", + "id": "6335331f", "metadata": {}, "source": [ - "### โš–๏ธ Model Validation" + "## โš–๏ธ Model Validation" ] }, { "cell_type": "code", - "execution_count": null, - "id": "4e5be9f8-0f88-4a7e-8fc1-65ec8b02920d", + "execution_count": 14, + "id": "405bb3d6", "metadata": { "tags": [] }, - "outputs": [], - "source": [ - "# Storing the current time as the start time of the cell execution\n", - "start_of_cell = time.time()\n", - "\n", - "# Creating an instance of the XGBoost Regressor\n", - "xgb_regressor = XGBRegressor()\n", - "\n", - "# Fitting the XGBoost Regressor to the training data\n", - "xgb_regressor.fit(X_train, y_train)\n", - "\n", - "# Predicting target values on the test set\n", + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "โ›ณ๏ธ MSE: 29.739315036119873\n", + "โ›ณ๏ธ RMSE: 5.453376480321148\n", + "โ›ณ๏ธ R^2: 0.7422035343350755\n" + ] + } + ], + "source": [ + "# Predict target values on the test set\n", "y_pred = xgb_regressor.predict(X_test)\n", "\n", - "# Calculating Mean Squared Error (MSE) using sklearn\n", + "# Calculate Mean Squared Error (MSE) using sklearn\n", "mse = mean_squared_error(y_test, y_pred)\n", - "print(\"MSE:\", mse)\n", + "print(\"โ›ณ๏ธ MSE:\", mse)\n", "\n", - "# Calculating Root Mean Squared Error (RMSE) using sklearn\n", + "# Calculate Root Mean Squared Error (RMSE) using sklearn\n", "rmse = mean_squared_error(y_test, y_pred, squared=False)\n", - "print(\"RMSE:\", rmse)\n", + "print(\"โ›ณ๏ธ RMSE:\", rmse)\n", "\n", - "# Calculating R squared using sklearn\n", + "# Calculate R squared using sklearn\n", "r2 = r2_score(y_test, y_pred)\n", - "print(\"R squared:\", r2)\n", - "\n", - "# Storing the current time as the end time of the cell execution\n", - "end_of_cell = time.time()\n", - "\n", - "# Printing information about the execution, including the time taken\n", - "print(f\"Took {round(end_of_cell - start_of_cell, 2)} sec.\\n\")" + "print(\"โ›ณ๏ธ R^2:\", r2)" ] }, { "cell_type": "code", - "execution_count": null, - "id": "ac31f9fb-7904-416a-9938-e85320340412", + "execution_count": 15, + "id": "b19bd4aa", "metadata": { "tags": [] }, "outputs": [], "source": [ - "# Creating a DataFrame 'df_' to store true and predicted values for evaluation\n", - "df_ = pd.DataFrame({\n", + "# Create a DataFrame 'df_' to store true and predicted values for evaluation\n", + "df_pred = pd.DataFrame({\n", " \"y_true\": y_test,\n", " \"y_pred\": y_pred,\n", "})" @@ -383,55 +643,71 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "f2fc8448-2150-4cfd-803d-afbd4845b59e", + "execution_count": 16, + "id": "9a72e8f5", "metadata": { "tags": [] }, - "outputs": [], - "source": [ - "# Creating a residual plot using Seaborn\n", - "residplot = sns.residplot(data=df_, x=\"y_true\", y=\"y_pred\", color='orange')\n", - "\n", - "# Adding title, xlabel, and ylabel to the residual plot\n", + "outputs": [ + { + "data": { + "image/png": "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KQwaNAgDBgzAihUrKr1+//79kZGR4XmcnJyMnJwcrzY5OTlITk4u9xoGgwEGg6HS1yIiIqKGIajBUWJiIhITE/1qe/LkSQwaNAh9+vTBypUrodFUnvTKyspCixYtPI/T0tLw3XffYdq0aZ5jGRkZSEtLC7jvRERE1DCFxFT+kydPYuDAgUhNTcVLL72Es2fPes65sz7vvvsu9Ho9evfuDQD47LPP8M477+Dtt9/2tJ06dSr+8pe/4OWXX8bIkSPx4YcfYseOHX5loYiIiKhxCIngKCMjA4cOHcKhQ4fQqlUrr3PqlQiee+45HD16FGFhYejcuTM++ugj3HbbbZ7zAwYMwPvvv48nn3wSc+bMQceOHfHFF19wjSMiIiLyCNl1joKF6xwRERGFnkaxzhERERFRbWBwRERERKTC4IiIiIhIhcERERERkQqDIyIiIiIVBkdEREREKgyOiIiIiFQYHBERERGpMDgiIiIiUmFwRERERKTC4IiIiIhIhcERERERkQqDIyIiIiIVBkdEREREKgyOiIiIiFQYHBERERGpMDgiIiIiUmFwRERERKTC4IiIiIhIhcERERERkQqDIyIiIiIVBkdEREREKgyOiIiIiFQYHBERERGpMDgiIiIiUmFwRERERKTC4IiIiIhIhcERERERkQqDIyIiIiIVBkdEREREKgyOiIiIiFQYHBERERGpMDgiIiIiUmFwRERERKQSMsHRjTfeiNatWyM8PBwtWrTAvffei1OnTnm12bNnD4xGI8LDw5GSkoJFixaVuc7q1avRuXNnhIeHo3v37lizZk1dvQUiIiIKASETHA0aNAgff/wxDh48iE8//RR//PEHbrvtNs95s9mMYcOGITU1FTt37sTixYsxf/58rFixwtPmp59+wpgxYzBhwgTs3r0bo0ePxujRo7Fv375gvCUiIiKqhxQhhAh2J6riv//9L0aPHg2LxQKdToc333wTc+fOhclkgl6vBwDMmjULX3zxBbKzswEAd9xxBwoLC/H11197rnP11VejV69eWL58uV+vazabERcXh/z8fMTGxtb8GyMiIqIaF8jnd8hkjtRyc3Px3nvvYcCAAdDpdACAzMxMXHfddZ7ACADS09Nx8OBBXLhwwdNm6NChXtdKT09HZmZmua9lsVhgNpu9voiIiKjhCqng6IknnkBUVBSaNm2KY8eO4csv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4uKETqcT4eHhIi4uTuj1emEwGERcXJx48MEHA/zpEZEagyMiqhFbt24VAMTnn3/u8/wrr7wiAIicnBwhhAwMhg8f7tXmjjvuECNGjBBCCPHyyy+Lyy+/XFit1jLXOnr0qNBqteLkyZNex4cMGSJmz54thJDBEQCRlZXl1Wbq1Kli8ODBnsfr168XBoPBE9T4MnLkSDFz5kzP49I7rgshygRHd911l7j++uu92jz22GOia9eunsepqaninnvu8Tx2Op2iefPm4s033yy3LzabTRw+fFj88ssvQqfTiV9++UUcOnRIREdHiy1btojDhw+Ls2fPlvt8Iqoch9WIqEYJIfxum5aWVubxgQMHAAC33347ioqK0K5dO0ycOBGff/457HY7AGDv3r1wOBy4/PLLER0d7fnasmUL/vjjD8/19Ho9evTo4fUad999NzZv3oxTp04BAN577z2MHDkS8fHxAGRt0nPPPYfu3bujSZMmiI6Oxvr16wMudD5w4ECZYa5rrrkGv//+OxwOh+eYun+KoiA5ORlnzpwp97phYWFo06YNsrOz0bdvX/To0QMmkwlJSUm47rrr0KZNGzRr1iygvhKRt7Bgd4CIGoYOHTpAURQcOHAAN998c5nzBw4cQEJCAhITE/26XkpKCg4ePIgNGzYgIyMDDz30EBYvXowtW7bg4sWL0Gq12LlzJ7RardfzoqOjPX+OiIgoUyDdt29ftG/fHh9++CEefPBBfP7551i1apXn/OLFi/Haa6/h1VdfRffu3REVFYVp06bBarUGcDf8p9PpvB4rilJh4Xi3bt1w9OhR2Gw2OJ1OREdHw263w263Izo6Gqmpqdi/f3+t9JWosWBwREQ1omnTprj++uvxxhtvYPr06YiIiPCcM5lMeO+993Dfffd5BStbt271usbWrVvRpUsXz+OIiAiMGjUKo0aNwuTJk9G5c2fs3bsXvXv3hsPhwJkzZ2A0GgPu691334333nsPrVq1gkajwciRIz3nfvzxR9x000245557AABOpxO//fYbunbt6mmj1+u9sj++dOnSBT/++KPXsR9//BGXX355mYAuEGvWrIHNZsOQIUOwaNEi9OnTB3feeSfGjRuH4cOHlwm2iChwHFYjohqzbNkyWCwWpKen4/vvv8fx48exbt06XH/99bjsssvwwgsveLX/8ccfsWjRIvz22294/fXXsXr1akydOhWAnG32v//7v9i3bx/+/PNP/N///R8iIiKQmpqKyy+/HHfffTfuu+8+fPbZZzh8+DB+/vlnLFiwAN98802l/bz77ruxa9cuvPDCC7jttttgMBg85zp27IiMjAz89NNPOHDgACZNmoScnByv57dp0wbbtm3DkSNHcO7cOZ+ZnpkzZ+K7777Dc889h99++w3vvvsuli1bhkcffbQqt9YjNTUV0dHRyMnJwU033YSUlBTs378ft956Kzp06IDU1NRqXZ+IGBwRUQ3q2LEjduzYgXbt2uFvf/sb2rdvj/vvvx+DBg1CZmYmmjRp4tV+5syZ2LFjB3r37o3nn38er7zyCtLT0wEA8fHx+Ne//oVrrrkGPXr0wIYNG/DVV1+hadOmAICVK1fivvvuw8yZM9GpUyeMHj0a27dv92udnw4dOqBfv37Ys2cP7r77bq9zTz75JK688kqkp6dj4MCBSE5OxujRo73aPProo9BqtejatSsSExN91iNdeeWV+Pjjj/Hhhx/iiiuuwLx58/Dss89i3LhxAdxR3zZv3oy+ffsiPDwcP//8M1q1aoUWLVpU+7pEJCkikOpJIiIiogaOmSMiIiIiFQZHRERERCoMjoiIiIhUGBwRERERqTA4IiIiIlJhcERERESkwuCIiIiISIXBEREREZEKgyMiIiIiFQZHRERERCoMjoiIiIhUGBwRERERqfx/mxEnsOo3SocAAAAASUVORK5CYII=\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Create a residual plot using Seaborn\n", + "residplot = sns.residplot(data=df_pred, x=\"y_true\", y=\"y_pred\", color='orange')\n", + "\n", + "# Add title, xlabel, and ylabel to the residual plot\n", "plt.title('Model Residuals')\n", "plt.xlabel('Observation #')\n", "plt.ylabel('Error')\n", "\n", - "# Displaying the residual plot\n", + "# Display the residual plot\n", "plt.show()\n", "\n", - "# Getting the figure from the residual plot and displaying it separately\n", + "# Get the figure from the residual plot and displaying it separately\n", "fig = residplot.get_figure()\n", "fig.show()" ] }, { "cell_type": "code", - "execution_count": null, - "id": "5ae4e226-7a93-4e4d-8131-c1de62a7b6f9", + "execution_count": 17, + "id": "d5157113", "metadata": { "tags": [] }, - "outputs": [], - "source": [ - "# Plotting feature importances using the plot_importance function from XGBoost\n", - "# 'xgb_regressor' is the trained XGBoost Regressor\n", - "# Setting 'max_num_features' to 25 to display the top 25 most important features\n", - "plot_importance(xgb_regressor, max_num_features=25)" - ] - }, - { - "cell_type": "markdown", - "id": "3dcea831-6c21-4396-a0ce-0631d21d1875", - "metadata": {}, - "source": [ - "---" + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Plot feature importances using the plot_importance function from XGBoost\n", + "plot_importance(\n", + " xgb_regressor, \n", + " max_num_features=25, # Display the top 25 most important features\n", + ")\n", + "plt.show()" ] }, { "cell_type": "markdown", - "id": "c066fe79-315e-4b85-b2ab-32d503679dc7", + "id": "0977f9fe", "metadata": { "tags": [] }, @@ -443,12 +719,20 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "a787ec40-6bd7-4950-aa5d-bf004e1e5ade", + "execution_count": 18, + "id": "e2fd7e49", "metadata": { "tags": [] }, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Connected. Call `.close()` to terminate connection gracefully.\n" + ] + } + ], "source": [ "# Retrieve the model registry\n", "mr = project.get_model_registry()" @@ -456,7 +740,7 @@ }, { "cell_type": "markdown", - "id": "7d240dc7-8a02-47b2-9667-7483508b2d24", + "id": "04886431", "metadata": {}, "source": [ "### โš™๏ธ Model Schema" @@ -464,7 +748,7 @@ }, { "cell_type": "markdown", - "id": "5c658df3-56a4-450b-90ee-127d0afe5b74", + "id": "86eb61ed", "metadata": {}, "source": [ "The model needs to be set up with a [Model Schema](https://docs.hopsworks.ai/machine-learning-api/latest/generated/model_schema/), which describes the inputs and outputs for a model.\n", @@ -474,8 +758,8 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "cd3f3751", + "execution_count": 19, + "id": "8a98e889", "metadata": { "scrolled": true }, @@ -484,47 +768,79 @@ "from hsml.schema import Schema\n", "from hsml.model_schema import ModelSchema\n", "\n", - "# Creating input and output schemas using the 'Schema' class for features (X) and target variable (y)\n", + "# Create input and output schemas using the 'Schema' class for features (X) and target variable (y)\n", "input_schema = Schema(X)\n", "output_schema = Schema(y)\n", "\n", - "# Creating a model schema using 'ModelSchema' with the input and output schemas\n", + "# Create a model schema using 'ModelSchema' with the input and output schemas\n", "model_schema = ModelSchema(input_schema=input_schema, output_schema=output_schema)\n", "\n", - "# Converting the model schema to a dictionary representation\n", + "# Convert the model schema to a dictionary representation\n", "schema_dict = model_schema.to_dict()" ] }, { "cell_type": "code", - "execution_count": null, - "id": "d2777f5e", + "execution_count": 20, + "id": "a3d26b4d", "metadata": { "scrolled": true }, "outputs": [], "source": [ - "# Creating a directory for the model artifacts if it doesn't exist\n", + "# Create a directory for the model artifacts if it doesn't exist\n", "model_dir = \"air_quality_model\"\n", "if os.path.isdir(model_dir) == False:\n", " os.mkdir(model_dir)\n", "\n", - "# Saving the label encoder and XGBoost regressor as joblib files in the model directory\n", + "# Save the label encoder and XGBoost regressor as joblib files in the model directory\n", "joblib.dump(label_encoder, model_dir + '/label_encoder.pkl')\n", "joblib.dump(xgb_regressor, model_dir + '/xgboost_regressor.pkl')\n", "\n", - "# Saving the residual plot figure as an image in the model directory\n", + "# Save the residual plot figure as an image in the model directory\n", "fig.savefig(model_dir + \"/residplot.png\")" ] }, { "cell_type": "code", - "execution_count": null, - "id": "41f6811e", + "execution_count": 21, + "id": "ac2d8166", "metadata": {}, - "outputs": [], - "source": [ - "# Creating a Python model in the model registry named 'air_quality_xgboost_model'\n", + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "db8f5c6580f0428fa7ac741fe6bd7f89", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + " 0%| | 0/6 [00:00 **Hopsworks Feature Store** - Part 04: Batch Inference\n", @@ -10,12 +10,14 @@ "## ๐Ÿ—’๏ธ This notebook is divided into the following sections:\n", "\n", "1. Load batch data.\n", - "2. Predict using model from Model Registry." + "2. Retrieve your trained model from the Model Registry.\n", + "3. Load batch data.\n", + "4. Predict batch data." ] }, { "cell_type": "markdown", - "id": "8855ee1a", + "id": "0cbefa72", "metadata": {}, "source": [ "## ๐Ÿ“ Imports" @@ -23,31 +25,41 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "019c9226", + "execution_count": 1, + "id": "2635641d", "metadata": {}, "outputs": [], "source": [ "import joblib\n", "import datetime\n", - "import time\n", "import pandas as pd" ] }, { "cell_type": "markdown", - "id": "ce2fe8a8", + "id": "97e466ff", "metadata": {}, "source": [ - "## ๐Ÿ“ก Connecting to Hopsworks Feature Store " + "## ๐Ÿ“ก Connect to Hopsworks Feature Store " ] }, { "cell_type": "code", - "execution_count": null, - "id": "39f83bc9", + "execution_count": 2, + "id": "dd83456c", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Connected. Call `.close()` to terminate connection gracefully.\n", + "\n", + "Logged in to project, explore it here https://snurran.hops.works/p/5242\n", + "Connected. Call `.close()` to terminate connection gracefully.\n" + ] + } + ], "source": [ "import hopsworks\n", "\n", @@ -58,16 +70,16 @@ }, { "cell_type": "markdown", - "id": "87485ee0", + "id": "88a9587d", "metadata": {}, "source": [ - "## โš™๏ธ Feature View Retrieval\n" + "## โš™๏ธ Feature View Retrieval" ] }, { "cell_type": "code", - "execution_count": null, - "id": "e622d6b4", + "execution_count": 3, + "id": "fa7f8ed2", "metadata": {}, "outputs": [], "source": [ @@ -80,18 +92,26 @@ }, { "cell_type": "markdown", - "id": "e1dac8b6", + "id": "20bc6944", "metadata": {}, "source": [ - "## ๐Ÿ—„ Model Registry\n" + "## ๐Ÿ—„ Model Registry" ] }, { "cell_type": "code", - "execution_count": null, - "id": "ca35a9f4", + "execution_count": 4, + "id": "33c4f742", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Connected. Call `.close()` to terminate connection gracefully.\n" + ] + } + ], "source": [ "# Retrieve the model registry\n", "mr = project.get_model_registry()" @@ -99,47 +119,99 @@ }, { "cell_type": "markdown", - "id": "6f3589dc", + "id": "f887c4ba", "metadata": {}, "source": [ - "## ๐Ÿช Retrieving model from Model Registry" + "## ๐Ÿช Retrieve model from Model Registry" ] }, { "cell_type": "code", - "execution_count": null, - "id": "6ac8014f", + "execution_count": 5, + "id": "9f52593a", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Downloading model artifact (0 dirs, 6 files)... DONE\r" + ] + } + ], "source": [ - "# Retrieving the 'air_quality_xgboost_model' from the model registry\n", + "# Retrieve the 'air_quality_xgboost_model' from the model registry\n", "retrieved_model = mr.get_model(\n", " name=\"air_quality_xgboost_model\",\n", " version=1,\n", ")\n", "\n", - "# Downloading the saved model artifacts to a local directory\n", + "# Download the saved model artifacts to a local directory\n", "saved_model_dir = retrieved_model.download()" ] }, { "cell_type": "code", - "execution_count": null, - "id": "3812f78d", + "execution_count": 6, + "id": "020d13b0", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "
XGBRegressor(base_score=None, booster=None, callbacks=None,\n",
+       "             colsample_bylevel=None, colsample_bynode=None,\n",
+       "             colsample_bytree=None, device=None, early_stopping_rounds=None,\n",
+       "             enable_categorical=False, eval_metric=None, feature_types=None,\n",
+       "             gamma=None, grow_policy=None, importance_type=None,\n",
+       "             interaction_constraints=None, learning_rate=None, max_bin=None,\n",
+       "             max_cat_threshold=None, max_cat_to_onehot=None,\n",
+       "             max_delta_step=None, max_depth=None, max_leaves=None,\n",
+       "             min_child_weight=None, missing=nan, monotone_constraints=None,\n",
+       "             multi_strategy=None, n_estimators=None, n_jobs=None,\n",
+       "             num_parallel_tree=None, random_state=None, ...)
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
" + ], + "text/plain": [ + "XGBRegressor(base_score=None, booster=None, callbacks=None,\n", + " colsample_bylevel=None, colsample_bynode=None,\n", + " colsample_bytree=None, device=None, early_stopping_rounds=None,\n", + " enable_categorical=False, eval_metric=None, feature_types=None,\n", + " gamma=None, grow_policy=None, importance_type=None,\n", + " interaction_constraints=None, learning_rate=None, max_bin=None,\n", + " max_cat_threshold=None, max_cat_to_onehot=None,\n", + " max_delta_step=None, max_depth=None, max_leaves=None,\n", + " min_child_weight=None, missing=nan, monotone_constraints=None,\n", + " multi_strategy=None, n_estimators=None, n_jobs=None,\n", + " num_parallel_tree=None, random_state=None, ...)" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "# Loading the XGBoost regressor model and label encoder from the saved model directory\n", + "# Load the XGBoost regressor model and label encoder from the saved model directory\n", "retrieved_xgboost_model = joblib.load(saved_model_dir + \"/xgboost_regressor.pkl\")\n", "retrieved_encoder = joblib.load(saved_model_dir + \"/label_encoder.pkl\")\n", "\n", - "# Displaying the retrieved XGBoost regressor model\n", + "# Display the retrieved XGBoost regressor model\n", "retrieved_xgboost_model" ] }, { "cell_type": "markdown", - "id": "9a762442", + "id": "b8e37bb1", "metadata": {}, "source": [ "## โœจ Load Batch Data of last days\n", @@ -149,38 +221,59 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "4bd49291", + "execution_count": 7, + "id": "733a1355", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "'2024-02-11'" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "# Getting the current date\n", + "# Get the current date\n", "today = datetime.date.today()\n", "\n", - "# Calculating a date threshold 30 days ago from the current date\n", + "# Calculate a date threshold 30 days ago from the current date\n", "date_threshold = today - datetime.timedelta(days=30)\n", "\n", - "# Converting the date threshold to a string format\n", + "# Convert the date threshold to a string format\n", "str(date_threshold)" ] }, { "cell_type": "code", - "execution_count": null, - "id": "3990e55f", + "execution_count": 8, + "id": "5a5c283b", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Finished: Reading data from Hopsworks, using ArrowFlight (7.68s) \n" + ] + } + ], "source": [ - "# Initializing batch scoring\n", + "# Initialize batch scoring\n", "feature_view.init_batch_scoring(1)\n", "\n", - "# Retrieving batch data from the feature view with a start time set to the date threshold\n", - "batch_data = feature_view.get_batch_data(start_time=date_threshold)" + "# Retrieve batch data from the feature view with a start time set to the date threshold\n", + "batch_data = feature_view.get_batch_data(\n", + " start_time=date_threshold,\n", + ")" ] }, { "cell_type": "markdown", - "id": "36f82c4a", + "id": "00a46a76", "metadata": {}, "source": [ "### ๐Ÿค– Making the predictions" @@ -188,44 +281,219 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "a10ff736", + "execution_count": 9, + "id": "f1e09066", "metadata": {}, - "outputs": [], + "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", + "
pm_2_5_previous_1_daypm_2_5_previous_2_daypm_2_5_previous_3_daypm_2_5_previous_4_daypm_2_5_previous_5_daypm_2_5_previous_6_daypm_2_5_previous_7_daymean_7_daysmean_14_daysmean_28_days...temperature_maxtemperature_minprecipitation_sumrain_sumsnowfall_sumprecipitation_hourswind_speed_maxwind_gusts_maxwind_direction_dominantcity_name_encoded
05.316.216.315.425.017.210.215.08571415.24285712.939286...14.28.40.00.000.00.034.759.430220
112.217.512.114.512.316.814.314.24285719.85714318.432143...13.110.520.920.900.022.040.164.821224
211.68.018.212.79.96.812.511.3857149.9214298.271429...10.85.29.113.650.05.014.843.218030
\n", + "

3 rows ร— 38 columns

\n", + "
" + ], + "text/plain": [ + " pm_2_5_previous_1_day pm_2_5_previous_2_day pm_2_5_previous_3_day \\\n", + "0 5.3 16.2 16.3 \n", + "1 12.2 17.5 12.1 \n", + "2 11.6 8.0 18.2 \n", + "\n", + " pm_2_5_previous_4_day pm_2_5_previous_5_day pm_2_5_previous_6_day \\\n", + "0 15.4 25.0 17.2 \n", + "1 14.5 12.3 16.8 \n", + "2 12.7 9.9 6.8 \n", + "\n", + " pm_2_5_previous_7_day mean_7_days mean_14_days mean_28_days ... \\\n", + "0 10.2 15.085714 15.242857 12.939286 ... \n", + "1 14.3 14.242857 19.857143 18.432143 ... \n", + "2 12.5 11.385714 9.921429 8.271429 ... \n", + "\n", + " temperature_max temperature_min precipitation_sum rain_sum \\\n", + "0 14.2 8.4 0.0 0.00 \n", + "1 13.1 10.5 20.9 20.90 \n", + "2 10.8 5.2 9.1 13.65 \n", + "\n", + " snowfall_sum precipitation_hours wind_speed_max wind_gusts_max \\\n", + "0 0.0 0.0 34.7 59.4 \n", + "1 0.0 22.0 40.1 64.8 \n", + "2 0.0 5.0 14.8 43.2 \n", + "\n", + " wind_direction_dominant city_name_encoded \n", + "0 302 20 \n", + "1 212 24 \n", + "2 180 30 \n", + "\n", + "[3 rows x 38 columns]" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "# Transforming the 'city_name' column in the batch data using the retrieved label encoder\n", + "# Transform the 'city_name' column in the batch data using the retrieved label encoder\n", "encoded = retrieved_encoder.transform(batch_data['city_name'])\n", "\n", - "# Concatenating the label-encoded 'city_name' with the original batch data\n", + "# Concatenate the label-encoded 'city_name' with the original batch data\n", "X_batch = pd.concat([batch_data, pd.DataFrame(encoded)], axis=1)\n", "\n", - "# Dropping unnecessary columns ('date', 'city_name', 'unix_time') from the batch data\n", + "# Drop unnecessary columns ('date', 'city_name', 'unix_time') from the batch data\n", "X_batch = X_batch.drop(columns=['date', 'city_name', 'unix_time'])\n", "\n", - "# Renaming the newly added column with label-encoded city names to 'city_name_encoded'\n", + "# Rename the newly added column with label-encoded city names to 'city_name_encoded'\n", "X_batch = X_batch.rename(columns={0: 'city_name_encoded'})\n", "\n", - "# Extracting the target variable 'pm2_5' from the batch data\n", - "y_batch = X_batch.pop('pm2_5')" + "# Extract the target variable 'pm2_5' from the batch data\n", + "y_batch = X_batch.pop('pm2_5')\n", + "\n", + "X_batch.head(3)" ] }, { "cell_type": "code", - "execution_count": null, - "id": "b597ea2b", + "execution_count": 10, + "id": "5149127e", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "array([ 5.9190893, 6.7028375, 7.9574 , 15.73646 , 8.050383 ],\n", + " dtype=float32)" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "# Making predictions on the batch data using the retrieved XGBoost regressor model\n", + "# Make predictions on the batch data using the retrieved XGBoost regressor model\n", "predictions = retrieved_xgboost_model.predict(X_batch)\n", "\n", - "# Displaying the first 5 predictions\n", + "# Display the first 5 predictions\n", "predictions[:5]" ] }, { "cell_type": "markdown", - "id": "80e2b142", + "id": "ccbc0bb6", "metadata": {}, "source": [ "---\n", @@ -234,17 +502,17 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "c208069a", + "execution_count": 11, + "id": "d784e05e", "metadata": {}, "outputs": [], "source": [ - "!python3 -m streamlit run streamlit_app.py" + "# !python3 -m streamlit run streamlit_app.py" ] }, { "cell_type": "markdown", - "id": "c97c7f97", + "id": "569fc146", "metadata": {}, "source": [ "---\n", @@ -260,7 +528,7 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3 (ipykernel)", + "display_name": "Python 3", "language": "python", "name": "python3" }, @@ -274,7 +542,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.18" + "version": "3.10.11" } }, "nbformat": 4, diff --git a/advanced_tutorials/air_quality/5_function_calling.ipynb b/advanced_tutorials/air_quality/5_function_calling.ipynb new file mode 100644 index 00000000..1f18f187 --- /dev/null +++ b/advanced_tutorials/air_quality/5_function_calling.ipynb @@ -0,0 +1,776 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "574f4ea0", + "metadata": {}, + "source": [ + "## ๐Ÿ“ Imports" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "1d2db28d", + "metadata": {}, + "outputs": [], + "source": [ + "import joblib\n", + "\n", + "from functions.llm_chain import load_model, get_llm_chain, generate_response" + ] + }, + { + "cell_type": "markdown", + "id": "6b079783", + "metadata": {}, + "source": [ + "## ๐Ÿ”ฎ Connect to Hopsworks Feature Store " + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "d1aff226", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Connected. Call `.close()` to terminate connection gracefully.\n", + "\n", + "Logged in to project, explore it here https://snurran.hops.works/p/5242\n", + "Connected. Call `.close()` to terminate connection gracefully.\n" + ] + } + ], + "source": [ + "import hopsworks\n", + "\n", + "project = hopsworks.login()\n", + "\n", + "fs = project.get_feature_store() " + ] + }, + { + "cell_type": "markdown", + "id": "3ce1a7b7", + "metadata": {}, + "source": [ + "## โš™๏ธ Feature View Retrieval" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "ae227ec2", + "metadata": {}, + "outputs": [], + "source": [ + "# Retrieve the 'air_quality_fv' feature view\n", + "feature_view = fs.get_feature_view(\n", + " name='air_quality_fv',\n", + " version=1,\n", + ")\n", + "\n", + "# Initialize batch scoring\n", + "feature_view.init_batch_scoring(1)" + ] + }, + { + "cell_type": "markdown", + "id": "5199d607", + "metadata": {}, + "source": [ + "## ๐Ÿช Retrieve AirQuality Model from Model Registry" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "f7661fcf", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Connected. Call `.close()` to terminate connection gracefully.\n", + "Downloading model artifact (0 dirs, 6 files)... DONE\r" + ] + } + ], + "source": [ + "# Retrieve the model registry\n", + "mr = project.get_model_registry()\n", + "\n", + "# Retrieve the 'air_quality_xgboost_model' from the model registry\n", + "retrieved_model = mr.get_model(\n", + " name=\"air_quality_xgboost_model\",\n", + " version=1,\n", + ")\n", + "\n", + "# Download the saved model artifacts to a local directory\n", + "saved_model_dir = retrieved_model.download()" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "b5b41d51", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
XGBRegressor(base_score=None, booster=None, callbacks=None,\n",
+       "             colsample_bylevel=None, colsample_bynode=None,\n",
+       "             colsample_bytree=None, device=None, early_stopping_rounds=None,\n",
+       "             enable_categorical=False, eval_metric=None, feature_types=None,\n",
+       "             gamma=None, grow_policy=None, importance_type=None,\n",
+       "             interaction_constraints=None, learning_rate=None, max_bin=None,\n",
+       "             max_cat_threshold=None, max_cat_to_onehot=None,\n",
+       "             max_delta_step=None, max_depth=None, max_leaves=None,\n",
+       "             min_child_weight=None, missing=nan, monotone_constraints=None,\n",
+       "             multi_strategy=None, n_estimators=None, n_jobs=None,\n",
+       "             num_parallel_tree=None, random_state=None, ...)
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
" + ], + "text/plain": [ + "XGBRegressor(base_score=None, booster=None, callbacks=None,\n", + " colsample_bylevel=None, colsample_bynode=None,\n", + " colsample_bytree=None, device=None, early_stopping_rounds=None,\n", + " enable_categorical=False, eval_metric=None, feature_types=None,\n", + " gamma=None, grow_policy=None, importance_type=None,\n", + " interaction_constraints=None, learning_rate=None, max_bin=None,\n", + " max_cat_threshold=None, max_cat_to_onehot=None,\n", + " max_delta_step=None, max_depth=None, max_leaves=None,\n", + " min_child_weight=None, missing=nan, monotone_constraints=None,\n", + " multi_strategy=None, n_estimators=None, n_jobs=None,\n", + " num_parallel_tree=None, random_state=None, ...)" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Load the XGBoost regressor model and label encoder from the saved model directory\n", + "model_air_quality = joblib.load(saved_model_dir + \"/xgboost_regressor.pkl\")\n", + "encoder = joblib.load(saved_model_dir + \"/label_encoder.pkl\")\n", + "\n", + "# Display the retrieved XGBoost regressor model\n", + "model_air_quality" + ] + }, + { + "cell_type": "markdown", + "id": "c07382c8", + "metadata": {}, + "source": [ + "## โฌ‡๏ธ LLM Loading" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "6d790560", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Special tokens have been added in the vocabulary, make sure the associated word embeddings are fine-tuned or trained.\n", + "Special tokens have been added in the vocabulary, make sure the associated word embeddings are fine-tuned or trained.\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2024-03-17 13:56:41,741 INFO: We will use 90% of the memory on device 0 for storing the model, and 10% for the buffer to avoid OOM. You can set `max_memory` in to a higher value to use more memory (at your own risk).\n" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "1113d3cc0aa64e7fba248fdcf2b9055a", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Loading checkpoint shards: 0%| | 0/2 [00:00โ›“๏ธ LangChain" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "44d106da", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "DeprecationWarning: `np.bool8` is a deprecated alias for `np.bool_`. (Deprecated NumPy 1.24)\n" + ] + } + ], + "source": [ + "# Create and configure a language model chain.\n", + "llm_chain = get_llm_chain(\n", + " model_llm, \n", + " tokenizer,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "9b5b7257", + "metadata": {}, + "source": [ + "## ๐Ÿงฌ Model Inference\n" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "0fb93740", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "๐Ÿ—“๏ธ Today's date: Sunday, 2024-03-17\n", + "๐Ÿ“– \n", + "\n", + "Hello! How can I help you with air quality today?\n" + ] + } + ], + "source": [ + "QUESTION7 = \"Hi!\"\n", + "\n", + "response7 = generate_response(\n", + " QUESTION7,\n", + " feature_view,\n", + " model_llm, \n", + " tokenizer,\n", + " model_air_quality,\n", + " encoder,\n", + " llm_chain,\n", + " verbose=True,\n", + ")\n", + "\n", + "print(response7)" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "f7e1bbc4", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "๐Ÿ—“๏ธ Today's date: Sunday, 2024-03-17\n", + "๐Ÿ“– \n", + "\n", + "I am an AI Air Quality assistant, here to help you with any air quality-related questions or concerns you may have. I can provide information on current and historical air quality data for your location, offer advice on whether it's safe to go outside, and suggest ways to improve air quality. How can I help you today?\n" + ] + } + ], + "source": [ + "QUESTION = \"Who are you?\"\n", + "\n", + "response = generate_response(\n", + " QUESTION,\n", + " feature_view,\n", + " model_llm,\n", + " tokenizer,\n", + " model_air_quality,\n", + " encoder,\n", + " llm_chain,\n", + " verbose=True,\n", + ")\n", + "\n", + "print(response)" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "307f2d8f", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Finished: Reading data from Hopsworks, using ArrowFlight (7.49s) \n", + "๐Ÿ—“๏ธ Today's date: Sunday, 2024-03-17\n", + "๐Ÿ“– Air Quality Measurements for New York:\n", + "Date: 2024-01-10; Air Quality: 7.2\n", + "Date: 2024-01-11; Air Quality: 5.9\n", + "Date: 2024-01-12; Air Quality: 10.8\n", + "Date: 2024-01-13; Air Quality: 5.9\n", + "Date: 2024-01-14; Air Quality: 5.1\n", + "\n", + "The average air quality in New York from January 10th to January 14th was 6.8. This indicates that the air quality was generally safe for most people to breathe, but individuals with respiratory issues may still need to take precautions.\n" + ] + } + ], + "source": [ + "QUESTION1 = \"What was the average air quality from 2024-01-10 till 2024-01-14 in New York?\"\n", + "\n", + "response1 = generate_response(\n", + " QUESTION1, \n", + " feature_view, \n", + " model_llm, \n", + " tokenizer, \n", + " model_air_quality, \n", + " encoder,\n", + " llm_chain,\n", + " verbose=True,\n", + ")\n", + "\n", + "print(response1)" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "4d39a38b", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Finished: Reading data from Hopsworks, using ArrowFlight (7.40s) \n", + "๐Ÿ—“๏ธ Today's date: Sunday, 2024-03-17\n", + "๐Ÿ“– Air Quality Measurements for New York:\n", + "Date: 2024-01-10; Air Quality: 7.2\n", + "Date: 2024-01-11; Air Quality: 5.9\n", + "Date: 2024-01-12; Air Quality: 10.8\n", + "Date: 2024-01-13; Air Quality: 5.9\n", + "Date: 2024-01-14; Air Quality: 5.1\n", + "\n", + "The maximum air quality in New York from January 10th to January 14th was on January 12th with an air quality of 10.8. This indicates that the air quality on that day was not safe for most people, especially those with respiratory issues, and it would be advisable to limit outdoor activities.\n" + ] + } + ], + "source": [ + "QUESTION11 = \"When and what was the maximum air quality from 2024-01-10 till 2024-01-14 in New York?\"\n", + "\n", + "response11 = generate_response(\n", + " QUESTION11, \n", + " feature_view, \n", + " model_llm,\n", + " tokenizer,\n", + " model_air_quality,\n", + " encoder,\n", + " llm_chain,\n", + " verbose=True,\n", + ")\n", + "\n", + "print(response11)" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "36ac09a2", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Finished: Reading data from Hopsworks, using ArrowFlight (7.47s) \n", + "๐Ÿ—“๏ธ Today's date: Sunday, 2024-03-17\n", + "๐Ÿ“– Air Quality Measurements for New York:\n", + "Date: 2024-01-10; Air Quality: 7.2\n", + "Date: 2024-01-11; Air Quality: 5.9\n", + "Date: 2024-01-12; Air Quality: 10.8\n", + "Date: 2024-01-13; Air Quality: 5.9\n", + "Date: 2024-01-14; Air Quality: 5.1\n", + "\n", + "The minimum air quality in New York from January 10th to January 14th was on January 11th with an air quality of 5.9. This indicates that the air quality on that day was generally safe for most people to breathe, but individuals with respiratory issues may still need to take precautions.\n" + ] + } + ], + "source": [ + "QUESTION12 = \"When and what was the minimum air quality from 2024-01-10 till 2024-01-14 in New York?\"\n", + "\n", + "response12 = generate_response(\n", + " QUESTION12, \n", + " feature_view, \n", + " model_llm, \n", + " tokenizer, \n", + " model_air_quality, \n", + " encoder,\n", + " llm_chain,\n", + " verbose=True,\n", + ")\n", + "\n", + "print(response12)" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "b80fa4f4", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Finished: Reading data from Hopsworks, using ArrowFlight (7.39s) \n", + "๐Ÿ—“๏ธ Today's date: Sunday, 2024-03-17\n", + "๐Ÿ“– Air Quality Measurements for London:\n", + "Date: 2024-03-16; Air Quality: 9.5\n", + "\n", + "The air quality yesterday in London was 9.5, which indicates that the air quality was generally safe for most people to breathe, but individuals with respiratory issues may still need to take precautions.\n" + ] + } + ], + "source": [ + "QUESTION2 = \"What was the air quality yesterday in London?\"\n", + "\n", + "response2 = generate_response(\n", + " QUESTION2, \n", + " feature_view, \n", + " model_llm, \n", + " tokenizer, \n", + " model_air_quality, \n", + " encoder,\n", + " llm_chain,\n", + " verbose=True,\n", + ")\n", + "\n", + "print(response2)" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "397d5168", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Finished: Reading data from Hopsworks, using ArrowFlight (7.47s) \n", + "๐Ÿ—“๏ธ Today's date: Sunday, 2024-03-17\n", + "๐Ÿ“– Air Quality Measurements for London:\n", + "Date: 2024-03-17; Air Quality: 7.6\n", + "Date: 2024-03-18; Air Quality: 9.88\n", + "Date: 2024-03-19; Air Quality: 9.18\n", + "Date: 2024-03-20; Air Quality: 9.34\n", + "Date: 2024-03-21; Air Quality: 9.37\n", + "Date: 2024-03-22; Air Quality: 9.37\n", + "Date: 2024-03-23; Air Quality: 9.37\n", + "\n", + "The air quality in London on 2024-03-23 is expected to be 9.37, which indicates that the air quality is generally safe for most people to breathe, but individuals with respiratory issues may still need to take precautions.\n" + ] + } + ], + "source": [ + "QUESTION3 = \"What will the air quality be like in London in 2024-03-23?\"\n", + "\n", + "response3 = generate_response(\n", + " QUESTION3, \n", + " feature_view, \n", + " model_llm, \n", + " tokenizer,\n", + " model_air_quality,\n", + " encoder,\n", + " llm_chain,\n", + " verbose=True,\n", + ")\n", + "\n", + "print(response3)" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "5d3ffba1", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Finished: Reading data from Hopsworks, using ArrowFlight (7.39s) \n", + "๐Ÿ—“๏ธ Today's date: Sunday, 2024-03-17\n", + "๐Ÿ“– Air Quality Measurements for Chicago:\n", + "Date: 2024-03-17; Air Quality: 6.1\n", + "Date: 2024-03-18; Air Quality: 8.37\n", + "Date: 2024-03-19; Air Quality: 7.39\n", + "\n", + "The air quality in Chicago the day after tomorrow, on 2024-03-19, is expected to be 7.39, which indicates that the air quality is generally safe for most people to breathe, but individuals with respiratory issues may still need to take precautions.\n" + ] + } + ], + "source": [ + "QUESTION4 = \"What will the air quality be like in Chicago the day after tomorrow?\"\n", + "\n", + "response4 = generate_response(\n", + " QUESTION4, \n", + " feature_view, \n", + " model_llm, \n", + " tokenizer, \n", + " model_air_quality, \n", + " encoder,\n", + " llm_chain,\n", + " verbose=True,\n", + ")\n", + "\n", + "print(response4)" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "0dce8283", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Finished: Reading data from Hopsworks, using ArrowFlight (7.38s) \n", + "๐Ÿ—“๏ธ Today's date: Sunday, 2024-03-17\n", + "๐Ÿ“– Air Quality Measurements for London:\n", + "Date: 2024-03-17; Air Quality: 7.6\n", + "\n", + "The air quality in London on Sunday, 2024-03-17, is expected to be 7.6, which indicates that the air quality is generally safe for most people to breathe, but individuals with respiratory issues may still need to take precautions.\n" + ] + } + ], + "source": [ + "QUESTION5 = \"What will the air quality be like in London on Sunday?\"\n", + "\n", + "response5 = generate_response(\n", + " QUESTION5, \n", + " feature_view, \n", + " model_llm, \n", + " tokenizer, \n", + " model_air_quality, \n", + " encoder,\n", + " llm_chain,\n", + " verbose=True,\n", + ")\n", + "\n", + "print(response5)" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "5e5fc2e4", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Finished: Reading data from Hopsworks, using ArrowFlight (7.58s) \n", + "๐Ÿ—“๏ธ Today's date: Sunday, 2024-03-17\n", + "๐Ÿ“– Air Quality Measurements for London:\n", + "Date: 2024-03-17; Air Quality: 7.6\n", + "Date: 2024-03-18; Air Quality: 9.88\n", + "Date: 2024-03-19; Air Quality: 9.18\n", + "Date: 2024-03-20; Air Quality: 9.34\n", + "Date: 2024-03-21; Air Quality: 9.37\n", + "\n", + "The air quality in London on March 21 is expected to be 9.37, which indicates that the air quality is generally safe for most people to breathe, but individuals with respiratory issues may still need to take precautions.\n" + ] + } + ], + "source": [ + "QUESTION7 = \"What will the air quality be like on March 21 in London?\"\n", + "\n", + "response7 = generate_response(\n", + " QUESTION7, \n", + " feature_view,\n", + " model_llm,\n", + " tokenizer, \n", + " model_air_quality, \n", + " encoder,\n", + " llm_chain,\n", + " verbose=True,\n", + ")\n", + "\n", + "print(response7)" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "fde239f6", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "๐Ÿ—“๏ธ Today's date: Sunday, 2024-03-17\n", + "๐Ÿ“– \n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "UserWarning: You seem to be using the pipelines sequentially on GPU. In order to maximize efficiency please use a dataset\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "The air quality level is not dangerous, but individuals with respiratory issues may still need to take precautions.\n" + ] + } + ], + "source": [ + "QUESTION = \"Is this air quality level dangerous?\"\n", + "\n", + "response = generate_response(\n", + " QUESTION, \n", + " feature_view, \n", + " model_llm, \n", + " tokenizer,\n", + " model_air_quality, \n", + " encoder,\n", + " llm_chain,\n", + " verbose=True,\n", + ")\n", + "\n", + "print(response)" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "1c0873b6", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "๐Ÿ—“๏ธ Today's date: Sunday, 2024-03-17\n", + "๐Ÿ“– \n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "UserWarning: You seem to be using the pipelines sequentially on GPU. In order to maximize efficiency please use a dataset\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Of course! Air quality levels are typically measured on a scale, and the specific scale can vary depending on the location and the organization providing the measurements. Generally, air quality levels are categorized into different ranges, with each range corresponding to a specific level of air quality. Here is a general overview of air quality levels:\n", + "\n", + "1. Good (0-50): The air quality is considered good, and it is safe for most people to breathe.\n", + "2. Moderate (51-100): The air quality is acceptable, but it may cause a slight irritation to some people with respiratory issues.\n", + "3. Poor (101-150): The air quality is considered unhealthy for sensitive groups, such as children, the elderly, and those with respiratory issues. It is advisable for these groups to limit their outdoor activities.\n", + "4. Very Poor (151-200): The air quality is considered unhealthy, and it may cause respiratory issues for most people. Outdoor activities should be limited.\n", + "5. Hazardous (over 200): The air quality is considered hazardous and can cause severe respiratory issues for everyone. Outdoor activities should be strictly avoided.\n", + "\n", + "These categories may vary slightly depending on the location and the organization providing the measurements, but they generally provide a good understanding of the air quality levels.\n" + ] + } + ], + "source": [ + "QUESTION = \"Can you please explain different air quality levels?\"\n", + "\n", + "response = generate_response(\n", + " QUESTION, \n", + " feature_view, \n", + " model_llm, \n", + " tokenizer,\n", + " model_air_quality, \n", + " encoder,\n", + " llm_chain,\n", + " verbose=True,\n", + ")\n", + "\n", + "print(response)" + ] + }, + { + "cell_type": "markdown", + "id": "1fd12ab8", + "metadata": {}, + "source": [ + "---" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "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.11" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/advanced_tutorials/air_quality/app_gradio.py b/advanced_tutorials/air_quality/app_gradio.py new file mode 100644 index 00000000..40bcf53c --- /dev/null +++ b/advanced_tutorials/air_quality/app_gradio.py @@ -0,0 +1,104 @@ +import gradio as gr +from transformers import pipeline +import numpy as np +import hopsworks +import joblib +from functions.llm_chain import load_model, get_llm_chain, generate_response + +# Initialize the ASR pipeline +transcriber = pipeline("automatic-speech-recognition", model="openai/whisper-base.en") + +def connect_to_hopsworks(): + # Initialize Hopsworks feature store connection + project = hopsworks.login() + fs = project.get_feature_store() + + # Retrieve the model registry + mr = project.get_model_registry() + + # Retrieve the 'air_quality_fv' feature view + feature_view = fs.get_feature_view( + name="air_quality_fv", + version=1, + ) + + # Initialize batch scoring + feature_view.init_batch_scoring(1) + + # Retrieve the 'air_quality_xgboost_model' from the model registry + retrieved_model = mr.get_model( + name="air_quality_xgboost_model", + version=1, + ) + + # Download the saved model artifacts to a local directory + saved_model_dir = retrieved_model.download() + + # Load the XGBoost regressor model and label encoder from the saved model directory + model_air_quality = joblib.load(saved_model_dir + "/xgboost_regressor.pkl") + encoder = joblib.load(saved_model_dir + "/label_encoder.pkl") + + return feature_view, model_air_quality, encoder + + +def retrieve_llm_chain(): + + # Load the LLM and its corresponding tokenizer. + model_llm, tokenizer = load_model() + + # Create and configure a language model chain. + llm_chain = get_llm_chain( + model_llm, + tokenizer, + ) + + return model_llm, tokenizer, llm_chain + + +# Retrieve the feature view, air quality model and encoder for the city_name column +feature_view, model_air_quality, encoder = connect_to_hopsworks() + +# Load the LLM and its corresponding tokenizer and configure a language model chain +model_llm, tokenizer, llm_chain = retrieve_llm_chain() + +def transcribe(audio): + sr, y = audio + y = y.astype(np.float32) + if y.ndim > 1 and y.shape[1] > 1: + y = np.mean(y, axis=1) + y /= np.max(np.abs(y)) + return transcriber({"sampling_rate": sr, "raw": y})["text"] + +def generate_query_response(user_query): + response = generate_response( + user_query, + feature_view, + model_llm, + tokenizer, + model_air_quality, + encoder, + llm_chain, + verbose=False, + ) + return response + +def handle_input(text_input=None, audio_input=None): + if audio_input is not None: + user_query = transcribe(audio_input) + else: + user_query = text_input + + if user_query: + return generate_query_response(user_query) + else: + return "Please provide input either via text or voice." + +iface = gr.Interface( + fn=handle_input, + inputs=[gr.Textbox(placeholder="Type here or use voice input..."), gr.Audio()], + outputs="text", + title="๐ŸŒค๏ธ AirQuality AI Assistant ๐Ÿ’ฌ", + description="Ask your questions about air quality or use your voice to interact." +) + +iface.launch(share=True) diff --git a/advanced_tutorials/air_quality/app_streamlit.py b/advanced_tutorials/air_quality/app_streamlit.py new file mode 100644 index 00000000..4dfa6d0c --- /dev/null +++ b/advanced_tutorials/air_quality/app_streamlit.py @@ -0,0 +1,99 @@ +import streamlit as st +import hopsworks +import joblib +from functions.llm_chain import load_model, get_llm_chain, generate_response +import warnings +warnings.filterwarnings('ignore') + +st.title("๐ŸŒค๏ธ AirQuality AI assistant ๐Ÿ’ฌ") + +@st.cache_resource() +def connect_to_hopsworks(): + # Initialize Hopsworks feature store connection + project = hopsworks.login() + fs = project.get_feature_store() + + # Retrieve the model registry + mr = project.get_model_registry() + + # Retrieve the 'air_quality_fv' feature view + feature_view = fs.get_feature_view( + name="air_quality_fv", + version=1, + ) + + # Initialize batch scoring + feature_view.init_batch_scoring(1) + + # Retrieve the 'air_quality_xgboost_model' from the model registry + retrieved_model = mr.get_model( + name="air_quality_xgboost_model", + version=1, + ) + + # Download the saved model artifacts to a local directory + saved_model_dir = retrieved_model.download() + + # Load the XGBoost regressor model and label encoder from the saved model directory + model_air_quality = joblib.load(saved_model_dir + "/xgboost_regressor.pkl") + encoder = joblib.load(saved_model_dir + "/label_encoder.pkl") + + return feature_view, model_air_quality, encoder + + +@st.cache_resource() +def retrieve_llm_chain(): + + # Load the LLM and its corresponding tokenizer. + model_llm, tokenizer = load_model() + + # Create and configure a language model chain. + llm_chain = get_llm_chain( + model_llm, + tokenizer, + ) + + return model_llm, tokenizer, llm_chain + + +# Retrieve the feature view, air quality model and encoder for the city_name column +feature_view, model_air_quality, encoder = connect_to_hopsworks() + +# Load the LLM and its corresponding tokenizer and configure a language model chain +model_llm, tokenizer, llm_chain = retrieve_llm_chain() + +# Initialize chat history +if "messages" not in st.session_state: + st.session_state.messages = [] + +# Display chat messages from history on app rerun +for message in st.session_state.messages: + with st.chat_message(message["role"]): + st.markdown(message["content"]) + +# React to user input +if user_query := st.chat_input("How can I help you?"): + # Display user message in chat message container + st.chat_message("user").markdown(user_query) + # Add user message to chat history + st.session_state.messages.append({"role": "user", "content": user_query}) + + st.write('โš™๏ธ Generating Response...') + + # Generate a response to the user query + response = generate_response( + user_query, + feature_view, + model_llm, + tokenizer, + model_air_quality, + encoder, + llm_chain, + verbose=False, + ) + + # Display assistant response in chat message container + with st.chat_message("assistant"): + st.markdown(response) + # Add assistant response to chat history + st.session_state.messages.append({"role": "assistant", "content": response}) diff --git a/advanced_tutorials/air_quality/feature_pipeline.py b/advanced_tutorials/air_quality/feature_pipeline.py deleted file mode 100644 index 0cee9c43..00000000 --- a/advanced_tutorials/air_quality/feature_pipeline.py +++ /dev/null @@ -1,158 +0,0 @@ -import datetime -import time -import requests -import pandas as pd -import json -import hopsworks - -from functions import * - -import warnings -warnings.filterwarnings("ignore") - -from dotenv import load_dotenv -load_dotenv() - -import os - -# Get the value of the PARAMETER environment variable -continent = os.environ.get('CONTINENT') - - -file_path = os.path.join(os.getcwd(), 'advanced_tutorials', 'air_quality', 'target_cities.json') -with open(file_path) as json_file: - target_cities = json.load(json_file) - - -def get_batch_data_from_fs(td_version, date_threshold): - print(f"Retrieving the Batch data since {date_threshold}") - feature_view.init_batch_scoring(training_dataset_version=td_version) - - batch_data = feature_view.get_batch_data(start_time=date_threshold) - return batch_data - - -def parse_aq_data(last_dates_dict, today): - start_of_cell = time.time() - df_aq_raw = pd.DataFrame() - - print("Parsing started...") - # for continent in target_cities: - for city_name, coords in target_cities[continent].items(): - df_ = get_aqi_data_from_open_meteo(city_name=city_name, - coordinates=coords, - start_date=last_dates_dict[city_name], - end_date=str(today)) - df_aq_raw = pd.concat([df_aq_raw, df_]).reset_index(drop=True) - end_of_cell = time.time() - print("-" * 64) - print(f"Parsed new PM2.5 data for ALL locations up to {str(today)}.") - print(f"Took {round(end_of_cell - start_of_cell, 2)} sec.\n") - return df_aq_raw - - -def parse_weather(last_dates_dict, today): - df_weather_update = pd.DataFrame() - start_of_cell = time.time() - - print("Parsing started...") - # for continent in target_cities: - for city_name, coords in target_cities[continent].items(): - df_ = get_weather_data_from_open_meteo(city_name=city_name, - coordinates=coords, - start_date=last_dates_dict[city_name], - end_date=str(today), - forecast=True) - df_weather_update = pd.concat([df_weather_update, df_]).reset_index(drop=True) - - end_of_cell = time.time() - print(f"Parsed new weather data for ALL cities up to {str(today)}.") - print(f"Took {round(end_of_cell - start_of_cell, 2)} sec.\n") - return df_weather_update - - - -if __name__=="__main__": - project = hopsworks.login() - fs = project.get_feature_store() - print("โœ… Logged in successfully!") - - feature_view = fs.get_feature_view( - name='air_quality_fv', - version=1 - ) - - # I am going to load data for of last 60 days (for feature engineering) - today = datetime.date.today() - date_threshold = today - datetime.timedelta(days=60) - - print("Getting the batch data...") - batch_data = get_batch_data_from_fs(td_version=1, - date_threshold=date_threshold) - - print("Retreived batch data.") - - - last_dates_dict = batch_data[["date", "city_name"]].groupby("city_name").max() - last_dates_dict.date = last_dates_dict.date.astype(str) - # here is a dictionary with city names as keys and last updated date as values - last_dates_dict = last_dates_dict.to_dict()["date"] - - df_aq_raw = parse_aq_data(last_dates_dict, today) - - # we need the previous data to calculate aggregation functions - df_aq_update = pd.concat([ - batch_data[df_aq_raw.columns], - df_aq_raw - ]).reset_index(drop=True) - df_aq_update = df_aq_update.drop_duplicates(subset=['city_name', 'date']) - - print(df_aq_update.tail(7)) - - print('\n๐Ÿ›  Feature Engineering the PM2.5') - - ### - df_aq_update['date'] = pd.to_datetime(df_aq_update['date']) - df_aq_update = feature_engineer_aq(df_aq_update) - df_aq_update = df_aq_update.dropna() - - print(df_aq_update.groupby("city_name").max().tail(7)) - print("โœ… Success!") - ### - - print(3 * "-") - print('\n๐ŸŒค๐Ÿ“† Parsing Weather data') - - df_weather_update = parse_weather(last_dates_dict, today) - print(df_weather_update.groupby("city_name").max().tail(7)) - print("โœ… Successfully parsed!") - - df_aq_update.date = df_aq_update.date.astype(str) - df_weather_update.date = df_weather_update.date.astype(str) - - print("Connecting to feature groups...") - air_quality_fg = fs.get_or_create_feature_group( - name = 'air_quality', - version = 1 - ) - weather_fg = fs.get_or_create_feature_group( - name = 'weather', - version = 1 - ) - - df_aq_update.date = pd.to_datetime(df_aq_update.date) - df_weather_update.date = pd.to_datetime(df_weather_update.date) - - df_aq_update["unix_time"] = df_aq_update["date"].apply(convert_date_to_unix) - df_weather_update["unix_time"] = df_weather_update["date"].apply(convert_date_to_unix) - - df_aq_update.date = df_aq_update.date.astype(str) - df_weather_update.date = df_weather_update.date.astype(str) - - air_quality_fg.insert(df_aq_update) - print("Created job to insert parsed PM2.5 data into FS...") - print("Inserting into air_quality fg.") - - weather_fg.insert(df_weather_update) - print("Created job to insert parsed weather data into FS...") - print("Inserting into weather fg.") diff --git a/advanced_tutorials/air_quality/features/__init__.py b/advanced_tutorials/air_quality/features/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/advanced_tutorials/air_quality/features/air_quality.py b/advanced_tutorials/air_quality/features/air_quality.py index 40fc8d1e..4cd4fed0 100644 --- a/advanced_tutorials/air_quality/features/air_quality.py +++ b/advanced_tutorials/air_quality/features/air_quality.py @@ -18,7 +18,6 @@ def shift_pm_2_5(df: pd.DataFrame, days: int = 5) -> pd.DataFrame: """ for shift_value in range(1, days + 1): df[f'pm_2_5_previous_{shift_value}_day'] = df.groupby('city_name')['pm2_5'].shift(shift_value) - df = df.dropna() return df @@ -227,8 +226,9 @@ def feature_engineer_aq(df: pd.DataFrame) -> pd.DataFrame: for i in [7, 14, 28]: for func in [moving_std, exponential_moving_average, exponential_moving_std]: df_res = func(df_res, i) - - df_res = df_res.sort_values(by=["date", "pm2_5"]).dropna() + + + df_res = df_res.sort_values(by=["date", "pm2_5"]) df_res = df_res.reset_index(drop=True) df_res['year'] = year(df_res['date']) diff --git a/advanced_tutorials/air_quality/functions.py b/advanced_tutorials/air_quality/functions.py deleted file mode 100644 index e0d46205..00000000 --- a/advanced_tutorials/air_quality/functions.py +++ /dev/null @@ -1,392 +0,0 @@ -import os -import datetime -import time -import requests -import pandas as pd -import json - -from geopy.geocoders import Nominatim - - -def convert_date_to_unix(x): - """ - Convert datetime to unix time in milliseconds. - """ - dt_obj = datetime.datetime.strptime(str(x), '%Y-%m-%d %H:%M:%S') - dt_obj = int(dt_obj.timestamp() * 1000) - return dt_obj - - -def get_city_coordinates(city_name: str): - """ - Takes city name and returns its latitude and longitude (rounded to 2 digits after dot). - """ - # Initialize Nominatim API (for getting lat and long of the city) - geolocator = Nominatim(user_agent="MyApp") - city = geolocator.geocode(city_name) - - latitude = round(city.latitude, 2) - longitude = round(city.longitude, 2) - - return latitude, longitude - - -##################################### EEA -def convert_to_daily(df, pollutant: str): - """ - Returns DataFrame where pollutant column is resampled to days and rounded. - """ - res_df = df.copy() - # convert dates in 'time' column - res_df["date"] = pd.to_datetime(res_df["date"]) - - # I want data daily, not hourly (mean per each day = 1 datarow per 1 day) - res_df = res_df.set_index('date') - res_df = res_df[pollutant].resample('1d').mean().reset_index() - res_df[pollutant] = res_df[pollutant].fillna(res_df[pollutant].median()) - res_df[pollutant] = res_df[pollutant].apply(lambda x: round(x, 0)) - - return res_df - - -def find_fullest_csv(csv_links: list, year: str): - candidates = [link for link in csv_links if str(year) in link] - biggest_df = pd.read_csv(candidates[0]) - for link in candidates[1:]: - _df = pd.read_csv(link) - if len(biggest_df) < len(_df): - biggest_df = _df - return biggest_df - - -def get_air_quality_from_eea( - city_name: str, - pollutant: str, - start_year: str, - end_year: str, - ): - """ - Takes city name, daterange and returns pandas DataFrame with daily air quality data. - It parses data by 1-year batches, so please specify years, not dates. (example: "2014", "2022"...) - - EEA means European Environmental Agency. So it has data for Europe Union countries ONLY. - """ - start_of_cell = time.time() - - params = { - 'CountryCode': '', - 'CityName': city_name, - 'Pollutant': pollutant.upper(), - 'Year_from': start_year, - 'Year_to': end_year, - 'Station': '', - 'Source': 'All', - 'Samplingpoint': '', - 'Output': 'TEXT', - 'UpdateDate': '', - 'TimeCoverage': 'Year' - } - - # observations endpoint - base_url = "https://fme.discomap.eea.europa.eu/fmedatastreaming/AirQualityDownload/AQData_Extract.fmw?" - try: - response = requests.get(base_url, params=params) - except ConnectionError: - response = requests.get(base_url, params=params) - - response.encoding = response.apparent_encoding - csv_links = response.text.split("\r\n") - - res_df = pd.DataFrame() - target_year = int(start_year) - - for year in range(int(start_year), int(end_year) + 1): - try: - # find the fullest, the biggest csv file with observations for this particular year - _df = find_fullest_csv(csv_links, year) - # append it to res_df - res_df = pd.concat([res_df, _df]) - except IndexError: - print(f"!! Missing data for {year} for {city} city.") - pass - - pollutant = pollutant.lower() - if pollutant == "pm2.5": - pollutant = "pm2_5" - - res_df = res_df.rename(columns={ - 'DatetimeBegin': 'date', - 'Concentration': pollutant - }) - - # cut timezones info - res_df['date'] = res_df['date'].apply(lambda x: x[:-6]) - # convert dates in 'time' column - res_df['date'] = pd.to_datetime(res_df['date']) - - res_df = convert_to_daily(res_df, pollutant) - - res_df['city_name'] = city_name - res_df = res_df[['city_name', 'date', pollutant.lower()]] - - end_of_cell = time.time() - - print(f"Processed {pollutant.upper()} for {city_name} since {start_year} till {end_year}.") - print(f"Took {round(end_of_cell - start_of_cell, 2)} sec.\n") - - return res_df - - - -##################################### USEPA -city_code_dict = {} -pollutant_dict = { - 'CO': '42101', - 'SO2': '42401', - 'NO2': '42602', - 'O3': '44201', - 'PM10': '81102', - 'PM2.5': '88101' -} - -def get_city_code(city_name: str): - "Encodes city name to be used later for data parsing using USEPA." - if city_code_dict: - city_full = [i for i in city_code_dict.keys() if city_name in i][0] - return city_code_dict[city_full] - else: - params = { - "email": "test@aqs.api", - "key": "test" - } - response = requests.get("https://aqs.epa.gov/data/api/list/cbsas?", params) - response_json = response.json() - data = response_json["Data"] - for item in data: - city_code_dict[item['value_represented']] = item['code'] - - return get_city_code(city_name) - - -def get_air_quality_from_usepa( - city_name: str, - pollutant: str, - start_date: str, - end_date: str - ): - """ - Takes city name, daterange and returns pandas DataFrame with daily air quality data. - - USEPA means United States Environmental Protection Agency. So it has data for US ONLY. - """ - start_of_cell = time.time() - res_df = pd.DataFrame() - - for start_date_, end_date_ in make_date_intervals(start_date, end_date): - params = { - "email": "test@aqs.api", - "key": "test", - "param": pollutant_dict[pollutant.upper().replace("_", ".")], # encoded pollutant - "bdate": start_date_, - "edate": end_date_, - "cbsa": get_city_code(city_name) # Core-based statistical area - } - - # observations endpoint - base_url = "https://aqs.epa.gov/data/api/dailyData/byCBSA?" - - response = requests.get(base_url, params=params) - response_json = response.json() - - df_ = pd.DataFrame(response_json["Data"]) - - pollutant = pollutant.lower() - if pollutant == "pm2.5": - pollutant = "pm2_5" - df_ = df_.rename(columns={ - 'date_local': 'date', - 'arithmetic_mean': pollutant - }) - - # convert dates in 'date' column - df_['date'] = pd.to_datetime(df_['date']) - df_['city_name'] = city_name - df_ = df_[['city_name', 'date', pollutant]] - res_df = pd.concat([res_df, df_]) - - # there are duplicated rows (several records for the same day and station). get rid of it. - res_df = res_df.groupby(['date', 'city_name'], as_index=False)[pollutant].mean() - res_df[pollutant] = round(res_df[pollutant], 1) - - end_of_cell = time.time() - print(f"Processed {pollutant.upper()} for {city_name} since {start_date} till {end_date}.") - print(f"Took {round(end_of_cell - start_of_cell, 2)} sec.\n") - - return res_df - - -def make_date_intervals(start_date, end_date): - start_dt = datetime.datetime.strptime(start_date, '%Y-%m-%d') - end_dt = datetime.datetime.strptime(end_date, '%Y-%m-%d') - date_intervals = [] - for year in range(start_dt.year, end_dt.year + 1): - year_start = datetime.datetime(year, 1, 1) - year_end = datetime.datetime(year, 12, 31) - interval_start = max(start_dt, year_start) - interval_end = min(end_dt, year_end) - if interval_start < interval_end: - date_intervals.append((interval_start.strftime('%Y%m%d'), interval_end.strftime('%Y%m%d'))) - return date_intervals - -##################################### Weather Open Meteo -def get_weather_data_from_open_meteo( - city_name: str, - start_date: str, - end_date: str, - coordinates: list = None, - forecast: bool = False, - ): - """ - Takes [city name OR coordinates] and returns pandas DataFrame with weather data. - - Examples of arguments: - coordinates=(47.755, -122.2806), start_date="2023-01-01" - """ - start_of_cell = time.time() - - if coordinates: - latitude, longitude = coordinates - else: - latitude, longitude = get_city_coordinates(city_name=city_name) - - params = { - 'latitude': latitude, - 'longitude': longitude, - 'daily': ["temperature_2m_max", "temperature_2m_min", - "precipitation_sum", "rain_sum", "snowfall_sum", - "precipitation_hours", "windspeed_10m_max", - "windgusts_10m_max", "winddirection_10m_dominant"], - 'timezone': "Europe/London", - 'start_date': start_date, - 'end_date': end_date, - } - - if forecast: - # historical forecast endpoint - base_url = 'https://api.open-meteo.com/v1/forecast' - else: - # historical observations endpoint - base_url = 'https://archive-api.open-meteo.com/v1/archive' - - try: - response = requests.get(base_url, params=params) - time.sleep(2) - except ConnectionError: - response = requests.get(base_url, params=params) - - response_json = response.json() - - res_df = pd.DataFrame(response_json["daily"]) - res_df["city_name"] = city_name - - # rename columns - res_df = res_df.rename(columns={ - "time": "date", - "temperature_2m_max": "temperature_max", - "temperature_2m_min": "temperature_min", - "windspeed_10m_max": "wind_speed_max", - "winddirection_10m_dominant": "wind_direction_dominant", - "windgusts_10m_max": "wind_gusts_max" - }) - - # change columns order - res_df = res_df[ - ['city_name', 'date', 'temperature_max', 'temperature_min', - 'precipitation_sum', 'rain_sum', 'snowfall_sum', - 'precipitation_hours', 'wind_speed_max', - 'wind_gusts_max', 'wind_direction_dominant'] - ] - - # convert dates in 'date' column - res_df["date"] = pd.to_datetime(res_df["date"]) - end_of_cell = time.time() - print(f"Parsed weather for {city_name} since {start_date} till {end_date}.") - print(f"Took {round(end_of_cell - start_of_cell, 2)} sec.\n") - - return res_df - - -##################################### Air Quality data from Open Meteo -def get_aqi_data_from_open_meteo( - city_name: str, - start_date: str, - end_date: str, - coordinates: list = None, - pollutant: str = "pm2_5" - ): - """ - Takes [city name OR coordinates] and returns pandas DataFrame with AQI data. - - Examples of arguments: - ... - coordinates=(47.755, -122.2806), - start_date="2023-01-01", - pollutant="no2" - ... - """ - start_of_cell = time.time() - - if coordinates: - latitude, longitude = coordinates - else: - latitude, longitude = get_city_coordinates(city_name=city_name) - - pollutant = pollutant.lower() - if pollutant == "pm2.5": - pollutant = "pm2_5" - - # make it work with both "no2" and "nitrogen_dioxide" passed. - if pollutant == "no2": - pollutant = "nitrogen_dioxide" - - params = { - 'latitude': latitude, - 'longitude': longitude, - 'hourly': [pollutant], - 'start_date': start_date, - 'end_date': end_date, - 'timezone': "Europe/London" - } - - # base endpoint - base_url = "https://air-quality-api.open-meteo.com/v1/air-quality" - try: - response = requests.get(base_url, params=params) - except ConnectionError: - response = requests.get(base_url, params=params) - response_json = response.json() - res_df = pd.DataFrame(response_json["hourly"]) - - # convert dates - res_df["time"] = pd.to_datetime(res_df["time"]) - - # resample to days - res_df = res_df.groupby(res_df['time'].dt.date).mean(numeric_only=True).reset_index() - res_df[pollutant] = round(res_df[pollutant], 1) - - # rename columns - res_df = res_df.rename(columns={ - "time": "date" - }) - - res_df["city_name"] = city_name - - # change columns order - res_df = res_df[ - ['city_name', 'date', pollutant] - ] - end_of_cell = time.time() - print(f"Processed {pollutant.upper()} for {city_name} since {start_date} till {end_date}.") - print(f"Took {round(end_of_cell - start_of_cell, 2)} sec.\n") - - return res_df \ No newline at end of file diff --git a/advanced_tutorials/air_quality/functions/air_quality_data_retrieval.py b/advanced_tutorials/air_quality/functions/air_quality_data_retrieval.py new file mode 100644 index 00000000..8f9afd6c --- /dev/null +++ b/advanced_tutorials/air_quality/functions/air_quality_data_retrieval.py @@ -0,0 +1,164 @@ +import pandas as pd +from typing import Any, Dict, List +import datetime +import pandas as pd + + +def transform_data(data, encoder): + """ + Transform the input data by encoding the 'city_name' column and dropping unnecessary columns. + + Args: + - data (DataFrame): Input data to be transformed. + - encoder (LabelEncoder): Label encoder object to encode 'city_name'. + + Returns: + - data_transformed (DataFrame): Transformed data with 'city_name_encoded' and dropped columns. + """ + + # Create a copy of the input data to avoid modifying the original data + data_transformed = data.copy() + + # Transform the 'city_name' column in the batch data using the retrieved label encoder + data_transformed['city_name_encoded'] = encoder.transform(data_transformed['city_name']) + + # Drop unnecessary columns from the batch data + data_transformed = data_transformed.drop(columns=['unix_time', 'pm2_5', 'city_name', 'date']) + + return data_transformed + + +def get_data_for_date(date: str, city_name: str, feature_view, model, encoder) -> pd.DataFrame: + """ + Retrieve data for a specific date and city from a feature view. + + Args: + date (str): The date in the format "%Y-%m-%d". + city_name (str): The name of the city to retrieve data for. + feature_view: The feature view object. + model: The machine learning model used for prediction. + encoder (LabelEncoder): Label encoder object to encode 'city_name'. + + Returns: + pd.DataFrame: A DataFrame containing data for the specified date and city. + """ + # Convert date string to datetime object + date_datetime = datetime.datetime.strptime(date, "%Y-%m-%d").date() + + # Retrieve batch data for the specified date range + batch_data = feature_view.get_batch_data( + start_time=date_datetime, + end_time=date_datetime + datetime.timedelta(days=1), + ) + + # Filter batch data for the specified city + batch_data_filtered = batch_data[batch_data['city_name'] == city_name] + + return batch_data_filtered[['date', 'pm2_5']].sort_values('date').reset_index(drop=True) + + +def get_data_in_date_range(date_start: str, date_end: str, city_name: str, feature_view, model, encoder) -> pd.DataFrame: + """ + Retrieve data for a specific date range and city from a feature view. + + Args: + date_start (str): The start date in the format "%Y-%m-%d". + date_end (str): The end date in the format "%Y-%m-%d". + city_name (str): The name of the city to retrieve data for. + feature_view: The feature view object. + model: The machine learning model used for prediction. + encoder (LabelEncoder): Label encoder object to encode 'city_name'. + + Returns: + pd.DataFrame: A DataFrame containing data for the specified date range and city. + """ + # Convert date strings to datetime objects + date_start_dt = datetime.datetime.strptime(date_start, "%Y-%m-%d").date() + date_end_dt = datetime.datetime.strptime(date_end, "%Y-%m-%d").date() + + # Retrieve batch data for the specified date range + batch_data = feature_view.get_batch_data( + start_time=date_start_dt, + end_time=date_end_dt + datetime.timedelta(days=1), + ) + + # Filter batch data for the specified city + batch_data_filtered = batch_data[batch_data['city_name'] == city_name] + + return batch_data_filtered[['date', 'pm2_5']].sort_values('date').reset_index(drop=True) + + +def get_future_data(date: str, city_name: str, feature_view, model, encoder) -> pd.DataFrame: + """ + Predicts future PM2.5 data for a specified date and city using a given feature view and model. + + Args: + date (str): The target future date in the format 'YYYY-MM-DD'. + city_name (str): The name of the city for which the prediction is made. + feature_view: The feature view used to retrieve batch data. + model: The machine learning model used for prediction. + encoder (LabelEncoder): Label encoder object to encode 'city_name'. + + Returns: + pd.DataFrame: A DataFrame containing predicted PM2.5 values for each day starting from the target date. + + """ + # Get today's date + today = datetime.date.today() + + # Convert the target date string to a datetime object + date_in_future = datetime.datetime.strptime(date, "%Y-%m-%d").date() + + # Calculate the difference in days between today and the target date + difference_in_days = (date_in_future - today).days + + # Retrieve batch data for the specified date range + batch_data = feature_view.get_batch_data( + start_time=today, + end_time=today + datetime.timedelta(days=1), + ) + + # Filter batch data for the specified city + batch_data_filtered = batch_data[batch_data['city_name'] == city_name] + + # Transform batch data + batch_data_transformed = transform_data(batch_data_filtered, encoder) + + # Initialize a DataFrame to store predicted PM2.5 values + try: + pm2_5_value = batch_data_filtered['pm2_5'].values[0] + except (IndexError, TypeError): + # If accessing pm2_5 values fails, return a message indicating the feature pipeline needs updating + return "Data is not available. Ask user to run the feature pipeline to update data." + else: + # Initialize a DataFrame to store predicted PM2.5 values + predicted_pm2_5_df = pd.DataFrame({ + 'date': [today.strftime("%Y-%m-%d")], + 'pm2_5': pm2_5_value, + }) + + # Iterate through each day starting from tomorrow up to the target date + for day_number in range(1, difference_in_days + 1): + + # Calculate the date for the current future day + date_future_day = (today + datetime.timedelta(days=day_number)).strftime("%Y-%m-%d") + + # Predict PM2.5 for the current day + predicted_pm2_5 = model.predict(batch_data_transformed) + + # Update previous day PM2.5 values in the batch data for the next prediction + batch_data_transformed['pm_2_5_previous_7_day'] = batch_data_transformed['pm_2_5_previous_6_day'] + batch_data_transformed['pm_2_5_previous_6_day'] = batch_data_transformed['pm_2_5_previous_5_day'] + batch_data_transformed['pm_2_5_previous_5_day'] = batch_data_transformed['pm_2_5_previous_4_day'] + batch_data_transformed['pm_2_5_previous_4_day'] = batch_data_transformed['pm_2_5_previous_3_day'] + batch_data_transformed['pm_2_5_previous_3_day'] = batch_data_transformed['pm_2_5_previous_2_day'] + batch_data_transformed['pm_2_5_previous_2_day'] = batch_data_transformed['pm_2_5_previous_1_day'] + batch_data_transformed['pm_2_5_previous_1_day'] = predicted_pm2_5 + + # Append the predicted PM2.5 value for the current day to the DataFrame + predicted_pm2_5_df = predicted_pm2_5_df._append({ + 'date': date_future_day, + 'pm2_5': predicted_pm2_5[0], + }, ignore_index=True) + + return predicted_pm2_5_df diff --git a/advanced_tutorials/air_quality/functions/common_functions.py b/advanced_tutorials/air_quality/functions/common_functions.py new file mode 100644 index 00000000..98767a09 --- /dev/null +++ b/advanced_tutorials/air_quality/functions/common_functions.py @@ -0,0 +1,25 @@ +import datetime +from geopy.geocoders import Nominatim + + +def convert_date_to_unix(x): + """ + Convert datetime to unix time in milliseconds. + """ + dt_obj = datetime.datetime.strptime(str(x), '%Y-%m-%d %H:%M:%S') + dt_obj = int(dt_obj.timestamp() * 1000) + return dt_obj + + +def get_city_coordinates(city_name: str): + """ + Takes city name and returns its latitude and longitude (rounded to 2 digits after dot). + """ + # Initialize Nominatim API (for getting lat and long of the city) + geolocator = Nominatim(user_agent="MyApp") + city = geolocator.geocode(city_name) + + latitude = round(city.latitude, 2) + longitude = round(city.longitude, 2) + + return latitude, longitude \ No newline at end of file diff --git a/advanced_tutorials/air_quality/functions/context_engineering.py b/advanced_tutorials/air_quality/functions/context_engineering.py new file mode 100644 index 00000000..4b3bd4dc --- /dev/null +++ b/advanced_tutorials/air_quality/functions/context_engineering.py @@ -0,0 +1,191 @@ +import xml.etree.ElementTree as ET +import re +import inspect +from typing import get_type_hints +import json +import datetime +import torch +import sys +import pandas as pd +from functions.air_quality_data_retrieval import get_data_for_date, get_data_in_date_range, get_future_data +from typing import Any, Dict, List + + +def get_type_name(t: Any) -> str: + """Get the name of the type.""" + name = str(t) + if "list" in name or "dict" in name: + return name + else: + return t.__name__ + + +def serialize_function_to_json(func: Any) -> str: + """Serialize a function to JSON.""" + signature = inspect.signature(func) + type_hints = get_type_hints(func) + + function_info = { + "name": func.__name__, + "description": func.__doc__, + "parameters": { + "type": "object", + "properties": {} + }, + "returns": type_hints.get('return', 'void').__name__ + } + + for name, _ in signature.parameters.items(): + param_type = get_type_name(type_hints.get(name, type(None))) + function_info["parameters"]["properties"][name] = {"type": param_type} + + return json.dumps(function_info, indent=2) + + +def generate_hermes(prompt: str, model_llm, tokenizer) -> str: + """Retrieves a function name and extracts function parameters based on the user query.""" + fn = """{"name": "function_name", "arguments": {"arg_1": "value_1", "arg_2": value_2, ...}}""" + example = """{"name": "get_data_in_date_range", "arguments": {"date_start": "2024-01-10", "date_end": "2024-01-14", "city_name": "New York"}}""" + + prompt = f"""<|im_start|>system +You are a helpful assistant with access to the following functions: + +{serialize_function_to_json(get_data_for_date)} + +{serialize_function_to_json(get_data_in_date_range)} + +{serialize_function_to_json(get_future_data)} + +###INSTRUCTIONS: +- You need to choose one function to use and retrieve paramenters for this function from the user input. +- If the user query contains 'will', it is very likely that you will need to use the get_future_data function. +- Do not include feature_view, model and encoder parameters. +- Dates should be provided in the format YYYY-MM-DD. +- Generate an 'No Function needed' string if the user query does not require function calling. + +IMPORTANT: Today is {datetime.date.today().strftime("%A")}, {datetime.date.today()}. + +To use one of there functions respond STRICTLY with: + + {fn} + + +###EXAMPLES + +EXAMPLE 1: +- User: Hi! +- AI Assiatant: No Function needed. + +EXAMPLE 2: +- User: Is it good or bad? +- AI Assiatant: No Function needed. + +EXAMPLE 3: +- User: When and what was the minimum air quality from 2024-01-10 till 2024-01-14 in New York? +- AI Assistant: + + {example} + + +<|im_end|> +<|im_start|>user +{prompt}<|im_end|> +<|im_start|>assistant""" + + tokens = tokenizer(prompt, return_tensors="pt").to(model_llm.device) + input_size = tokens.input_ids.numel() + with torch.inference_mode(): + generated_tokens = model_llm.generate( + **tokens, + use_cache=True, + do_sample=True, + temperature=0.2, + top_p=1.0, + top_k=0, + max_new_tokens=512, + eos_token_id=tokenizer.eos_token_id, + pad_token_id=tokenizer.eos_token_id, + ) + + return tokenizer.decode( + generated_tokens.squeeze()[input_size:], + skip_special_tokens=True, + ) + + +def extract_function_calls(completion: str) -> List[Dict[str, Any]]: + """Extract function calls from completion.""" + completion = completion.strip() + pattern = r"((.*?))" + match = re.search(pattern, completion, re.DOTALL) + if not match: + return None + + multiplefn = match.group(1) + root = ET.fromstring(multiplefn) + functions = root.findall("functioncall") + + return [json.loads(fn.text) for fn in functions] + + +def invoke_function(function, feature_view, model, encoder) -> pd.DataFrame: + """Invoke a function with given arguments.""" + # Extract function name and arguments from input_data + function_name = function['name'] + arguments = function['arguments'] + + # Using Python's getattr function to dynamically call the function by its name and passing the arguments + function_output = getattr(sys.modules[__name__], function_name)( + **arguments, + feature_view=feature_view, + model=model, + encoder=encoder, + ) + + if type(function_output) == str: + return function_output + + # Round the 'pm2_5' value to 2 decimal places + function_output['pm2_5'] = function_output['pm2_5'].apply(round, ndigits=2) + return function_output + + +def get_context_data(user_query: str, feature_view, model_llm, tokenizer, model_air_quality, encoder) -> str: + """ + Retrieve context data based on user query. + + Args: + user_query (str): The user query. + feature_view: Feature View for data retrieval. + model_llm: The language model. + tokenizer: The tokenizer. + model_air_quality: The air quality model. + encoder: The encoder. + + Returns: + str: The context data. + """ + # Generate a response using LLM + completion = generate_hermes( + user_query, + model_llm, + tokenizer, + ) + + # Extract function calls from the completion + functions = extract_function_calls(completion) + + # If function calls were found + if functions: + # Invoke the function with provided arguments + data = invoke_function(functions[0], feature_view, model_air_quality, encoder) + # Return formatted data as string + if isinstance(data, pd.DataFrame): + return f'Air Quality Measurements for {functions[0]["arguments"]["city_name"]}:\n' + '\n'.join( + [f'Date: {row["date"]}; Air Quality: {row["pm2_5"]}' for _, row in data.iterrows()] + ) + # Return message if data is not updated + return data + + # If no function calls were found, return an empty string + return '' diff --git a/advanced_tutorials/air_quality/functions/llm_chain.py b/advanced_tutorials/air_quality/functions/llm_chain.py new file mode 100644 index 00000000..6d0833ac --- /dev/null +++ b/advanced_tutorials/air_quality/functions/llm_chain.py @@ -0,0 +1,202 @@ +import transformers +from transformers import AutoTokenizer, AutoModelForCausalLM, BitsAndBytesConfig +from langchain.llms import HuggingFacePipeline +from langchain.prompts import PromptTemplate +from langchain.chains.llm import LLMChain +from langchain.memory import ConversationBufferWindowMemory +import torch +import datetime +from typing import Any, Dict, Union +from functions.context_engineering import get_context_data + + +def load_model(model_id: str = "teknium/OpenHermes-2.5-Mistral-7B") -> tuple: + """ + Load the LLM and its corresponding tokenizer. + + Args: + model_id (str, optional): Identifier for the pre-trained model. Defaults to "teknium/OpenHermes-2.5-Mistral-7B". + + Returns: + tuple: A tuple containing the loaded model and tokenizer. + """ + + # Load the tokenizer for Mistral-7B-Instruct model + tokenizer = AutoTokenizer.from_pretrained( + model_id, + ) + + # Set the pad token to the unknown token to handle padding + tokenizer.pad_token = tokenizer.unk_token + + # Set the padding side to "right" to prevent warnings during tokenization + tokenizer.padding_side = "right" + + # BitsAndBytesConfig int-4 config + bnb_config = BitsAndBytesConfig( + load_in_4bit=True, + bnb_4bit_use_double_quant=True, + bnb_4bit_quant_type="nf4", + bnb_4bit_compute_dtype=torch.bfloat16, + ) + + # Load the Mistral-7B-Instruct model with quantization configuration + model_llm = AutoModelForCausalLM.from_pretrained( + model_id, + device_map="auto", + quantization_config=bnb_config, + ) + + # Configure the pad token ID in the model to match the tokenizer's pad token ID + model_llm.config.pad_token_id = tokenizer.pad_token_id + + return model_llm, tokenizer + + +def get_prompt_template(): + """ + Retrieve a template for generating prompts in a conversational AI system. + + Returns: + str: A string representing the template for generating prompts. + This template includes placeholders for system information, + instructions, previous conversation, context, date and user query. + """ + prompt_template = """<|im_start|>system +You are a helpful Air Quality assistant. +Provide your answers based on the provided context table which consists of the dates and air quality indicators for the city provided by user. + +INSTRUCTIONS: +- If you don't know the answer, you will respond politely that you cannot help. +- Use the provided table with air quality indicators for city provided by user to generate your answer. +- You answer should be at least one sentence. +- Do not show any calculations to the user. +- If the user asks for the air quality level in specific range, you can calculate an average air quality level. +- Make sure that you use correct air quality indicators for the required date. +- Add a description of the air quality level, such as whether it is safe, whether to go for a walk, etc. +- If user asks more general question, use your last responses in the chat history as a context. +<|im_end|> + +Previous conversation: +{chat_history} + +### CONTEXT: +{context} + +IMPORTANT: Today is {date_today}. + +<|im_start|>user +{question}<|im_end|> +<|im_start|>assistant""" + return prompt_template + + +def get_llm_chain(model_llm, tokenizer): + """ + Create and configure a language model chain. + + Args: + model_llm: The pre-trained language model for text generation. + tokenizer: The tokenizer corresponding to the language model. + + Returns: + LLMChain: The configured language model chain. + """ + # Create a text generation pipeline using the loaded model and tokenizer + text_generation_pipeline = transformers.pipeline( + model=model_llm, # The pre-trained language model for text generation + tokenizer=tokenizer, # The tokenizer corresponding to the language model + task="text-generation", # Specify the task as text generation + use_cache=True, + do_sample=True, + temperature=0.4, + top_p=1.0, + top_k=0, + max_new_tokens=512, + eos_token_id=tokenizer.eos_token_id, + pad_token_id=tokenizer.eos_token_id, + ) + + # Create a Hugging Face pipeline for Mistral LLM using the text generation pipeline + mistral_llm = HuggingFacePipeline( + pipeline=text_generation_pipeline, + ) + + # Create prompt from prompt template + prompt = PromptTemplate( + input_variables=["context", "question", "date_today", "chat_history"], + template=get_prompt_template(), + ) + + # Create a ConversationBufferWindowMemory with specified configuration + memory = ConversationBufferWindowMemory( + k=3, # Number of turns to remember in the conversation buffer + memory_key="chat_history", # Key to store the conversation history in memory + input_key="question", # Key to access the input question in the conversation + ) + + # Create LLM chain + llm_chain = LLMChain( + llm=mistral_llm, + prompt=prompt, + verbose=False, + memory=memory, + ) + + return llm_chain + + +def generate_response( + user_query: str, + feature_view, + model_llm, + tokenizer, + model_air_quality, + encoder, + llm_chain, + verbose: bool = False, +) -> str: + """ + Generate response to user query using LLM chain and context data. + + Args: + user_query (str): The user's query. + feature_view: Feature view for data retrieval. + model_llm: Language model for text generation. + tokenizer: Tokenizer for processing text. + model_air_quality: Model for predicting air quality. + encoder: Label Encoder for the city_name column. + llm_chain: LLM Chain. + verbose (bool): Whether to print verbose information. Defaults to False. + + Returns: + str: Generated response to the user query. + """ + + # Get context data based on user query + context = get_context_data( + user_query, + feature_view, + model_llm, + tokenizer, + model_air_quality, + encoder, + ) + + # Get today's date in a readable format + date_today = f'{datetime.date.today().strftime("%A")}, {datetime.date.today()}' + + # Print today's date and context information if verbose mode is enabled + if verbose: + print(f"๐Ÿ—“๏ธ Today's date: {date_today}") + print(f'๐Ÿ“– {context}') + + # Invoke the language model chain with relevant context + model_output = llm_chain.invoke({ + "context": context, + "date_today": date_today, + "question": user_query, + }) + + # Return the generated text from the model output + return model_output['text'] diff --git a/advanced_tutorials/air_quality/functions/parse_air_quality.py b/advanced_tutorials/air_quality/functions/parse_air_quality.py new file mode 100644 index 00000000..06dd41fe --- /dev/null +++ b/advanced_tutorials/air_quality/functions/parse_air_quality.py @@ -0,0 +1,79 @@ +import time +from functions.common_functions import * +import requests +import pandas as pd + + +def get_aqi_data_from_open_meteo( + city_name: str, + start_date: str, + end_date: str, + coordinates: list = None, + pollutant: str = "pm2_5" + ): + """ + Takes [city name OR coordinates] and returns pandas DataFrame with AQI data. + + Examples of arguments: + ... + coordinates=(47.755, -122.2806), + start_date="2023-01-01", + pollutant="no2" + ... + """ + start_of_cell = time.time() + + if coordinates: + latitude, longitude = coordinates + else: + latitude, longitude = get_city_coordinates(city_name=city_name) + + pollutant = pollutant.lower() + if pollutant == "pm2.5": + pollutant = "pm2_5" + + # make it work with both "no2" and "nitrogen_dioxide" passed. + if pollutant == "no2": + pollutant = "nitrogen_dioxide" + + params = { + 'latitude': latitude, + 'longitude': longitude, + 'hourly': [pollutant], + 'start_date': start_date, + 'end_date': end_date, + 'timezone': "Europe/London" + } + + # base endpoint + base_url = "https://air-quality-api.open-meteo.com/v1/air-quality" + try: + response = requests.get(base_url, params=params) + except ConnectionError: + response = requests.get(base_url, params=params) + response_json = response.json() + res_df = pd.DataFrame(response_json["hourly"]) + + # convert dates + res_df["time"] = pd.to_datetime(res_df["time"]) + + # resample to days + res_df = res_df.groupby(res_df['time'].dt.date).mean(numeric_only=True).reset_index() + res_df[pollutant] = round(res_df[pollutant], 1) + + # rename columns + res_df = res_df.rename(columns={ + "time": "date" + }) + + res_df["city_name"] = city_name + + # change columns order + res_df = res_df[ + ['city_name', 'date', pollutant] + ] + end_of_cell = time.time() + print(f"Processed {pollutant.upper()} for {city_name} since {start_date} till {end_date}.") + print(f"Took {round(end_of_cell - start_of_cell, 2)} sec.\n") + + return res_df \ No newline at end of file diff --git a/advanced_tutorials/air_quality/functions/parse_weather.py b/advanced_tutorials/air_quality/functions/parse_weather.py new file mode 100644 index 00000000..bbebc34c --- /dev/null +++ b/advanced_tutorials/air_quality/functions/parse_weather.py @@ -0,0 +1,81 @@ +import time +from functions.common_functions import * +import requests +import pandas as pd + + +def get_weather_data_from_open_meteo( + city_name: str, + start_date: str, + end_date: str, + coordinates: list = None, + forecast: bool = False, + ): + """ + Takes [city name OR coordinates] and returns pandas DataFrame with weather data. + + Examples of arguments: + coordinates=(47.755, -122.2806), start_date="2023-01-01" + """ + start_of_cell = time.time() + + if coordinates: + latitude, longitude = coordinates + else: + latitude, longitude = get_city_coordinates(city_name=city_name) + + params = { + 'latitude': latitude, + 'longitude': longitude, + 'daily': ["temperature_2m_max", "temperature_2m_min", + "precipitation_sum", "rain_sum", "snowfall_sum", + "precipitation_hours", "windspeed_10m_max", + "windgusts_10m_max", "winddirection_10m_dominant"], + 'timezone': "Europe/London", + 'start_date': start_date, + 'end_date': end_date, + } + + if forecast: + # historical forecast endpoint + base_url = 'https://api.open-meteo.com/v1/forecast' + else: + # historical observations endpoint + base_url = 'https://archive-api.open-meteo.com/v1/archive' + + try: + response = requests.get(base_url, params=params) + time.sleep(2) + except ConnectionError: + response = requests.get(base_url, params=params) + + response_json = response.json() + + res_df = pd.DataFrame(response_json["daily"]) + res_df["city_name"] = city_name + + # rename columns + res_df = res_df.rename(columns={ + "time": "date", + "temperature_2m_max": "temperature_max", + "temperature_2m_min": "temperature_min", + "windspeed_10m_max": "wind_speed_max", + "winddirection_10m_dominant": "wind_direction_dominant", + "windgusts_10m_max": "wind_gusts_max" + }) + + # change columns order + res_df = res_df[ + ['city_name', 'date', 'temperature_max', 'temperature_min', + 'precipitation_sum', 'rain_sum', 'snowfall_sum', + 'precipitation_hours', 'wind_speed_max', + 'wind_gusts_max', 'wind_direction_dominant'] + ] + + # convert dates in 'date' column + res_df["date"] = pd.to_datetime(res_df["date"]) + end_of_cell = time.time() + print(f"Parsed weather for {city_name} since {start_date} till {end_date}.") + print(f"Took {round(end_of_cell - start_of_cell, 2)} sec.\n") + + return res_df \ No newline at end of file diff --git a/advanced_tutorials/air_quality/requirements.txt b/advanced_tutorials/air_quality/requirements.txt index 9d933db1..edafbefb 100644 --- a/advanced_tutorials/air_quality/requirements.txt +++ b/advanced_tutorials/air_quality/requirements.txt @@ -1,7 +1,12 @@ -hopsworks -geopy -pandas -numpy -streamlit -streamlit-folium -joblib \ No newline at end of file +geopy==2.4.1 +joblib==1.2.0 +xgboost==2.0.3 +transformers==4.38.2 +protobuf==3.20.0 +langchain==0.1.10 +flask-sqlalchemy==3.1.1 +bitsandbytes==0.42.0 +accelerate==0.27.2 +streamlit==1.31.1 +sentencepiece==0.2.0 +gradio==4.21.0