{
"cells": [
{
"cell_type": "markdown",
"id": "ae929fb3-0316-494b-accb-1951756d4d32",
"metadata": {
"tags": []
},
"source": [
"# ML Demo: Predicting Air Quality w/ ASDI NOAA + OpenAQ Datasets\n",
"This demo consists of a Jupyter Notebook that connects to Amazon Sustainability Data Initiative (ASDI) datasets from NOAA and OpenAQ to build a Machine Learning (ML) model to predict air quality levels using weather data via a Binary Classification [AutoGluon](https://auto.gluon.ai/stable/index.html) model. The project's purpose is to demonstrate using two different types of ASDI datasets (files in Amazon S3 and HTTPS APIs) within a Jupyter Notebook, such as provided by Amazon SageMaker Studio Lab. This demo is NOT for scientific or health purposes.\n",
"\n",
"This Jupyter Notebook can be run for free using Amazon SageMaker Studio Lab and open-source Amazon Sustainability Data Initiative (ASDI) datasets without needing an AWS account.\n",
"\n",
"[](https://studiolab.sagemaker.aws/import/github/aws-samples/aws-smsl-predict-airquality-via-weather/blob/main/aq_by_weather.ipynb)\n",
"\n",
"## BACKGROUND: 1 out of 8 deaths in the world is due to poor air quality*\n",
"This demo explores correlations between weather and air quality since we know factors like temperatures, wind speeds, etc, affect certain air quality parameters. Predicting air quality can get into highly sophisticated ML techniques, but this demo shows how merging NOAA GSOD weather data with OpenAQ air quality data to build an ML model using AutoGluon (AutoML from AWS) can result in prediction accuracy of ~75-90% using Binary Classification models for various high-pollution areas and air quality parameters (mostly tested for 2.5 micron Particulate Matter and some Ground Level Ozone).\\\n",
"*Source: [OpenAQ.org](https://OpenAQ.org)\n",
"\n",
"## INSTRUCTIONS\n",
"- Configure your environment using the repo's environment.yml file or by running the *pip install* commands below.\n",
"- If you want to run this Notebook yourself, first use the _Clear All Outputs_ option.\n",
"- CELL #3: Review the Classes and Variables defined in this cell to see defaults and how ASDI access occurs.\n",
"- CELL #4: Review the pre-defined AQParams and AQScenarios in this cell. You can edit these and/or add your own.\n",
"- CELL #5: Select a scenario via the drop-down to use throughout the Notebook. This will drive the ML process.\n",
"- Step through the remaining cells in the Notebook to access data, merge data, and build a Binary Classification Autogluon model.\n",
"- NOTE: NOAA and OpenAQ input data is saved locally as CSV files and re-used in order to minimize unnecessary data access and compute resources. Be aware of this feature and clear the local data files, if your intention is to re-fetch the data."
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "5a41f10b-f9ed-433a-8d32-bb718f23d811",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Note: you may need to restart the kernel to use updated packages.\n",
"Note: you may need to restart the kernel to use updated packages.\n",
"Note: you may need to restart the kernel to use updated packages.\n",
"Note: you may need to restart the kernel to use updated packages.\n",
"Note: you may need to restart the kernel to use updated packages.\n",
"Note: you may need to restart the kernel to use updated packages.\n",
"Note: you may need to restart the kernel to use updated packages.\n",
"Note: you may need to restart the kernel to use updated packages.\n",
"Note: you may need to restart the kernel to use updated packages.\n"
]
}
],
"source": [
"# Set up your environment according to the repo's environment.yml file or run the following...\n",
"# Comment these out, once installed or otherwise not needed.\n",
"# This creates an empty pip_requirements.txt file used to suppress 'already satisfied' output.\n",
"import os\n",
"with open('pip_requirements.txt', mode='a'): pass\n",
"%pip install boto3 -r pip_requirements.txt | grep -v 'already satisfied'\n",
"%pip install pandas -r pip_requirements.txt | grep -v 'already satisfied'\n",
"%pip install numpy -r pip_requirements.txt | grep -v 'already satisfied'\n",
"%pip install requests -r pip_requirements.txt | grep -v 'already satisfied'\n",
"%pip install ipywidgets -r pip_requirements.txt | grep -v 'already satisfied'\n",
"%pip install scikit-learn -r pip_requirements.txt | grep -v 'already satisfied'\n",
"%pip install autogluon -r pip_requirements.txt | grep -v 'already satisfied'\n",
"%pip install matplotlib -r pip_requirements.txt | grep -v 'already satisfied'\n",
"%pip install nbconvert -r pip_requirements.txt | grep -v 'already satisfied'"
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "e844d780-d995-4bdc-98ae-570bc8253265",
"metadata": {},
"outputs": [],
"source": [
"# Import statements for packages used...\n",
"import os, glob, shutil, sys, requests, json\n",
"import boto3\n",
"import pandas as pd\n",
"import numpy as np\n",
"import matplotlib.pyplot as plt\n",
"import ipywidgets as widgets\n",
"\n",
"from botocore import UNSIGNED\n",
"from botocore.config import Config\n",
"from io import StringIO\n",
"from datetime import datetime\n",
"from types import SimpleNamespace\n",
"from IPython.display import clear_output\n",
"\n",
"# The following is required for matplotlib plots to display in some envs...\n",
"%matplotlib inline"
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "9ea427ea-8ce7-4dc5-80e1-c674f7f569ee",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Classes and Variables are ready.\n"
]
}
],
"source": [
"# CELL #3: Review the Variables and Classes defined in this cell to see defaults and how ASDI access occurs...\n",
"\n",
"# class AQParam => Used to define attributes for the (6) main OpenAQ parameters.\n",
"class AQParam:\n",
" def __init__(self, id, name, unit, unhealthyThresholdDefault, desc):\n",
" self.id = id\n",
" self.name = name\n",
" self.unit = unit\n",
" self.unhealthyThresholdDefault = unhealthyThresholdDefault\n",
" self.desc = desc\n",
" \n",
" def isValid(self):\n",
" if(self is not None and self.id > 0 and self.unhealthyThresholdDefault > 0.0 and \n",
" len(self.name) > 0 and len(self.unit) > 0 and len(self.desc) > 0):\n",
" return True\n",
" else:\n",
" return False\n",
" \n",
" def toJSON(self):\n",
" return json.dumps(self, default=lambda o: o.__dict__, sort_keys=True, indent=2)\n",
"\n",
"# class AQScenario => Defines an ML scenario including a Location w/ NOAA Weather Station ID \n",
"# and the target OpenAQ Param.\n",
"# Note: OpenAQ data mostly begins sometime in 2016, so using that as a default yearStart value.\n",
"class AQScenario:\n",
" def __init__(self, location=None, noaaStationID=None, aqParamTarget=None, unhealthyThreshold=None, \n",
" yearStart=2016, yearEnd=2022, aqRadiusMiles=10, featureColumnsToDrop=None):\n",
" self.location = location\n",
" self.name = location + \"_\" + aqParamTarget.name\n",
" self.noaaStationID = noaaStationID\n",
" self.noaaStationLat = 0.0\n",
" self.noaaStationLng = 0.0\n",
" self.openAqLocIDs = []\n",
" \n",
" self.aqParamTarget = aqParamTarget\n",
" \n",
" if unhealthyThreshold and unhealthyThreshold > 0.0:\n",
" self.unhealthyThreshold = unhealthyThreshold\n",
" else:\n",
" self.unhealthyThreshold = self.aqParamTarget.unhealthyThresholdDefault\n",
" \n",
" self.yearStart = yearStart\n",
" self.yearEnd = yearEnd\n",
" self.aqRadiusMiles = aqRadiusMiles\n",
" self.aqRadiusMeters = aqRadiusMiles * 1610 # Rough integer approximation is fine here.\n",
" \n",
" self.modelFolder = \"AutogluonModels\"\n",
" \n",
" def getSummary(self):\n",
" return f\"Scenario: {self.name} => {self.aqParamTarget.desc} ({self.aqParamTarget.name}) with UnhealthyThreshold > {self.unhealthyThreshold} {self.aqParamTarget.unit}\"\n",
" \n",
" def getModelPath(self):\n",
" return f\"{self.modelFolder}/aq_{self.name}_{self.yearStart}-{self.yearEnd}/\"\n",
" \n",
" def updateNoaaStationLatLng(self, noaagsod_df_row):\n",
" # Use a NOAA row to set Lat+Lng values used for the OpenAQ API requests...\n",
" if(noaagsod_df_row is not None and noaagsod_df_row['LATITUDE'] and noaagsod_df_row['LONGITUDE']):\n",
" self.noaaStationLat = noaagsod_df_row['LATITUDE']\n",
" self.noaaStationLng = noaagsod_df_row['LONGITUDE']\n",
" print(f\"NOAA Station Lat,Lng Updated for Scenario: {self.name} => {self.noaaStationLat},{self.noaaStationLng}\")\n",
" else:\n",
" print(\"NOAA Station Lat,Lng COULD NOT BE UPDATED.\")\n",
" \n",
" def isValid(self):\n",
" if(self is not None and self.aqParamTarget is not None and\n",
" self.yearStart > 0 and self.yearEnd > 0 and self.yearEnd >= self.yearStart and \n",
" self.aqRadiusMiles > 0 and self.aqRadiusMeters > 0 and self.unhealthyThreshold > 0.0 and \n",
" len(self.name) > 0 and len(self.noaaStationID) > 0):\n",
" return True\n",
" else:\n",
" return False\n",
" \n",
" def toJSON(self):\n",
" return json.dumps(self, default=lambda o: o.__dict__, sort_keys=True, indent=2)\n",
"\n",
"# class AQbyWeatherApp => Main app class with settings, AQParams, AQScenarios, and data access methods...\n",
"class AQbyWeatherApp:\n",
" def __init__(self, mlTargetLabel='isUnhealthy', mlEvalMetric='accuracy', mlTimeLimitSecs=None):\n",
" self.mlTargetLabel = mlTargetLabel\n",
" self.mlEvalMetric = mlEvalMetric\n",
" self.mlTimeLimitSecs = mlTimeLimitSecs\n",
" self.mlIgnoreColumns = ['DATE','NAME','LATITUDE','LONGITUDE','day','unit','average','parameter']\n",
" \n",
" self.defaultColumnsNOAA = ['DATE','NAME','LATITUDE','LONGITUDE',\n",
" 'DEWP','WDSP','MAX','MIN','PRCP','MONTH'] # Default relevant NOAA columns\n",
" self.defaultColumnsOpenAQ = ['day','parameter','unit','average'] # Default relevant OpenAQ columns\n",
" \n",
" self.aqParams = {} # A list to save AQParam objects\n",
" self.aqScenarios = {} # A list to save AQScenario objects\n",
" \n",
" self.selectedScenario = None\n",
" \n",
" def addAQParam(self, aqParam):\n",
" if aqParam and aqParam.isValid():\n",
" self.aqParams[aqParam.name] = aqParam\n",
" return True\n",
" else:\n",
" return False\n",
" \n",
" def addAQScenario(self, aqScenario):\n",
" if aqScenario and aqScenario.isValid():\n",
" self.aqScenarios[aqScenario.name] = aqScenario\n",
" if(self.selectedScenario is None):\n",
" self.selectedScenario = self.aqScenarios[next(iter(self.aqScenarios))] # Default selectedScenario to 1st item.\n",
" return True\n",
" else:\n",
" return False\n",
" \n",
" def getFilenameNOAA(self):\n",
" if self and self.selectedScenario and self.selectedScenario.isValid():\n",
" return f\"dataNOAA_{self.selectedScenario.name}_{self.selectedScenario.yearStart}-{self.selectedScenario.yearEnd}_{self.selectedScenario.noaaStationID}.csv\"\n",
" else:\n",
" return \"\"\n",
" \n",
" def getFilenameOpenAQ(self):\n",
" if self and self.selectedScenario and self.selectedScenario.isValid() and len(self.selectedScenario.openAqLocIDs) > 0:\n",
" idString = \"\"\n",
" for i in range(0, len(self.selectedScenario.openAqLocIDs)):\n",
" idString = idString + str(self.selectedScenario.openAqLocIDs[i]) + \"-\"\n",
" idString = idString[:-1]\n",
" return f\"dataOpenAQ_{self.selectedScenario.name}_{self.selectedScenario.yearStart}-{self.selectedScenario.yearEnd}_{idString}.csv\"\n",
" else:\n",
" return \"\"\n",
" \n",
" def getFilenameOther(self, prefix):\n",
" if self and self.selectedScenario and self.selectedScenario.isValid():\n",
" return f\"{prefix}_{self.selectedScenario.name}_{self.selectedScenario.yearStart}-{self.selectedScenario.yearEnd}.csv\"\n",
" \n",
" def getNoaaDataFrame(self):\n",
" # ASDI Dataset Name: NOAA GSOD\n",
" # ASDI Dataset URL : https://registry.opendata.aws/noaa-gsod/\n",
" # NOAA GSOD README : https://www.ncei.noaa.gov/data/global-summary-of-the-day/doc/readme.txt\n",
" # NOAA GSOD data in S3 is organized by year and Station ID values, so this is straight-forward\n",
" # Example S3 path format => s3://noaa-gsod-pds/{yyyy}/{stationid}.csv\n",
" # Let's start with a new DataFrame and load it from a local CSV or the NOAA data source...\n",
" noaagsod_df = pd.DataFrame()\n",
" filenameNOAA = self.getFilenameNOAA()\n",
"\n",
" if os.path.exists(filenameNOAA):\n",
" # Use local data file already accessed + prepared...\n",
" print('Loading NOAA GSOD data from local file: ', filenameNOAA)\n",
" noaagsod_df = pd.read_csv(filenameNOAA)\n",
" else:\n",
" # Access + prepare data and save to a local data file...\n",
" noaagsod_bucket = 'noaa-gsod-pds'\n",
" print(f'Accessing and preparing data from ASDI-hosted NOAA GSOD dataset in Amazon S3 (bucket: {noaagsod_bucket})...')\n",
" s3 = boto3.client('s3', config=Config(signature_version=UNSIGNED))\n",
"\n",
" for year in range(self.selectedScenario.yearStart, self.selectedScenario.yearEnd + 1):\n",
" key = f'{year}/{self.selectedScenario.noaaStationID}.csv' # Compute the key to get\n",
" csv_obj = s3.get_object(Bucket=noaagsod_bucket, Key=key) # Get the S3 object\n",
" csv_string = csv_obj['Body'].read().decode('utf-8') # Read object contents to a string\n",
" noaagsod_df = pd.concat([noaagsod_df, pd.read_csv(StringIO(csv_string))], ignore_index=True) # Use the string to build the DataFrame\n",
"\n",
" # Perform some Feature Engineering to append potentially useful columns to our dataset... (TODO: Add more to optimize...)\n",
" # It may be true that Month affects air quality (ie: seasonal considerations; tends to have correlation for certain areas)\n",
" noaagsod_df['MONTH'] = pd.to_datetime(noaagsod_df['DATE']).dt.month\n",
"\n",
" # Trim down to the desired key columns... (do this last in case engineered columns are to be removed)\n",
" noaagsod_df = noaagsod_df[self.defaultColumnsNOAA]\n",
" \n",
" return noaagsod_df\n",
" \n",
" def getOpenAqDataFrame(self):\n",
" # ASDI Dataset Name: OpenAQ\n",
" # ASDI Dataset URL : https://registry.opendata.aws/openaq/\n",
" # OpenAQ API Docs : https://docs.openaq.org/#/v2/\n",
" # OpenAQ S3 data is only organized by date folders, so each folder is large and contains all stations.\n",
" # Because of this, it's better to query ASDI OpenAQ data using the CloudFront-hosted API.\n",
" # Note that some days may not have values and will get filtered out via an INNER JOIN later.\n",
" # Let's start with a new DataFrame and load it from a local CSV or the NOAA data source...\n",
" aq_df = pd.DataFrame()\n",
" aq_reqUrlBase = \"https://api.openaq.org/v2\" # OpenAQ ASDI API Endpoint URL Base (ie: add /locations OR /averages)\n",
" \n",
" if self.selectedScenario.noaaStationLat == 0.0 or self.selectedScenario.noaaStationLng == 0.0:\n",
" print(\"NOAA Station Lat/Lng NOT DEFINED. CANNOT PROCEED\")\n",
" return aq_df\n",
" \n",
" if len(self.selectedScenario.openAqLocIDs) == 0:\n",
" # We must start by querying nearby OpenAQ Locations for their IDs...\n",
" print('Accessing ASDI-hosted OpenAQ Locations (HTTPS API)...')\n",
" aq_reqParams = {\n",
" 'limit': 10,\n",
" 'page': 1,\n",
" 'offset': 0,\n",
" 'sort': 'desc',\n",
" 'order_by': 'location',\n",
" 'parameter': self.selectedScenario.aqParamTarget.name,\n",
" 'coordinates': f'{self.selectedScenario.noaaStationLat},{self.selectedScenario.noaaStationLng}',\n",
" 'radius': self.selectedScenario.aqRadiusMeters,\n",
" 'isMobile': 'false',\n",
" 'sensorType': 'reference grade',\n",
" 'dumpRaw': 'false'\n",
" }\n",
" aq_resp = requests.get(aq_reqUrlBase + \"/locations\", aq_reqParams)\n",
" aq_data = aq_resp.json()\n",
" if aq_data['results'] and len(aq_data['results']) >= 1:\n",
" for i in range(0, len(aq_data['results'])):\n",
" self.selectedScenario.openAqLocIDs.append(aq_data['results'][i]['id'])\n",
" print(f\"OpenAQ Location IDs within {self.selectedScenario.aqRadiusMiles} miles ({self.selectedScenario.aqRadiusMeters}m) \" +\n",
" f\"of NOAA Station {self.selectedScenario.noaaStationID} at \" +\n",
" f\"{self.selectedScenario.noaaStationLat},{self.selectedScenario.noaaStationLng}: {self.selectedScenario.openAqLocIDs}\")\n",
" else:\n",
" print(f\"NO OpenAQ Location IDs found within {self.selectedScenario.aqRadiusMiles} miles ({self.selectedScenario.aqRadiusMeters}m) \" +\n",
" f\"of NOAA Station {self.selectedScenario.noaaStationID}. CANNOT PROCEED.\")\n",
" \n",
" if len(self.selectedScenario.openAqLocIDs) >= 1:\n",
" filenameOpenAQ = self.getFilenameOpenAQ()\n",
"\n",
" if os.path.exists(filenameOpenAQ):\n",
" # Use local data file already accessed + prepared...\n",
" print('Loading OpenAQ data from local file: ', filenameOpenAQ)\n",
" aq_df = pd.read_csv(filenameOpenAQ)\n",
" else:\n",
" # Access + prepare data (NOTE: calling OpenAQ API one year at a time to avoid timeouts)\n",
" print('Accessing ASDI-hosted OpenAQ Averages (HTTPS API)...')\n",
"\n",
" for year in range(self.selectedScenario.yearStart, self.selectedScenario.yearEnd + 1):\n",
" aq_reqParams = {\n",
" 'date_from': f'{year}-01-01',\n",
" 'date_to': f'{year}-12-31',\n",
" 'parameter': [self.selectedScenario.aqParamTarget.name],\n",
" 'limit': 366,\n",
" 'page': 1,\n",
" 'offset': 0,\n",
" 'sort': 'asc',\n",
" 'spatial': 'location',\n",
" 'temporal': 'day',\n",
" 'location': self.selectedScenario.openAqLocIDs,\n",
" 'group': 'true'\n",
" }\n",
" aq_resp = requests.get(aq_reqUrlBase + \"/averages\", aq_reqParams)\n",
" aq_data = aq_resp.json()\n",
" if aq_data['results'] and len(aq_data['results']) >= 1:\n",
" aq_df = pd.concat([aq_df, pd.json_normalize(aq_data['results'])], ignore_index=True)\n",
"\n",
" # Using the joined dataframe, trim down to the desired key columns...\n",
" aq_df = aq_df[self.defaultColumnsOpenAQ]\n",
"\n",
" # Perform some Label Engineering to add our binary classification label => {0=OKAY, 1=UNHEALTHY}\n",
" aq_df[self.mlTargetLabel] = np.where(aq_df['average'] <= self.selectedScenario.unhealthyThreshold, 0, 1)\n",
" \n",
" return aq_df\n",
" \n",
" def getMergedDataFrame(self, noaagsod_df, aq_df):\n",
" if len(noaagsod_df) > 0 and len(aq_df) > 0:\n",
" merged_df = pd.merge(noaagsod_df, aq_df, how=\"inner\", left_on=\"DATE\", right_on=\"day\")\n",
" merged_df = merged_df.drop(columns=self.mlIgnoreColumns)\n",
" return merged_df\n",
" else:\n",
" return pd.DataFrame()\n",
" \n",
" def getConfusionMatrixData(self, cm):\n",
" cmData = SimpleNamespace()\n",
" cmData.TN = cm[0][0]\n",
" cmData.TP = cm[1][1]\n",
" cmData.FN = cm[1][0]\n",
" cmData.FP = cm[0][1]\n",
" \n",
" cmData.TN_Rate = cmData.TN/(cmData.TN+cmData.FP)\n",
" cmData.TP_Rate = cmData.TP/(cmData.TP+cmData.FN)\n",
" cmData.FN_Rate = cmData.FN/(cmData.FN+cmData.TP)\n",
" cmData.FP_Rate = cmData.FP/(cmData.FP+cmData.TN)\n",
" \n",
" cmData.TN_Output = f\"True Negatives (TN): {cmData.TN} of {cmData.TN+cmData.FP} => {round(cmData.TN_Rate * 100, 2)}%\"\n",
" cmData.TP_Output = f\"True Positives (TP): {cmData.TP} of {cmData.TP+cmData.FN} => {round(cmData.TP_Rate * 100, 2)}%\"\n",
" cmData.FN_Output = f\"False Negatives (FN): {cmData.FN} of {cmData.FN+cmData.TP} => {round(cmData.FN_Rate * 100, 2)}%\"\n",
" cmData.FP_Output = f\"False Positives (FP): {cmData.FP} of {cmData.FP+cmData.TN} => {round(cmData.FP_Rate * 100, 2)}%\"\n",
" \n",
" return cmData\n",
" \n",
"print(\"Classes and Variables are ready.\")"
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "599478b3-5abd-4bc3-9888-d3dee71b1c09",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"AQbyWeather.aqParams: 6\n",
"AQbyWeather.aqScenarios: 15 (Default Selected: bakersfield_pm25)\n"
]
}
],
"source": [
"# CELL #4: Review the pre-defined AQParams and AQScenarios in this cell. You can edit these and/or use your own...\n",
"# AQParams are added with default thresholds, which can be overridden on a per-AQScenario basis.\n",
"# These AQParams are based on the OpenAQ /parameters API call where isCore=true (https://api.openaq.org/v2/parameters).\n",
"# Default thresholds where provided using data from EPA.gov (https://www.epa.gov/criteria-air-pollutants/naaqs-table).\n",
"# Confirm and adjust params or thresholds as needed for your needs... Not for scientific or health purposes.\n",
"\n",
"# Instantiate main App class with explicit mlTargetLabel and mlEvalMetric provided...\n",
"AQbyWeather = AQbyWeatherApp(mlTargetLabel='isUnhealthy', mlEvalMetric='accuracy')\n",
"\n",
"# Define and add new AQParams...\n",
"AQbyWeather.addAQParam(AQParam( 1, \"pm10\", \"µg/m³\", 150.0, \"Particulate Matter < 10 micrometers\"))\n",
"AQbyWeather.addAQParam(AQParam( 2, \"pm25\", \"µg/m³\", 12.0, \"Particulate Matter < 2.5 micrometers\"))\n",
"AQbyWeather.addAQParam(AQParam( 7, \"no2\", \"ppm\", 100.0, \"Nitrogen Dioxide\"))\n",
"AQbyWeather.addAQParam(AQParam( 8, \"co\", \"ppm\", 9.0, \"Carbon Monoxide\"))\n",
"AQbyWeather.addAQParam(AQParam( 9, \"so2\", \"ppm\", 75.0, \"Sulfur Dioxide\"))\n",
"AQbyWeather.addAQParam(AQParam(10, \"o3\", \"ppm\", 0.070, \"Ground Level Ozone\"))\n",
"\n",
"# Define available AQ Scenarios for certain locations with their associated NOAA GSOD StationID values...\n",
"# NOAA GSOD Station Search: https://www.ncei.noaa.gov/access/search/data-search/global-summary-of-the-day\n",
"# TODO: Someday consider how to OPTIONALLY append more scenarios via an optional JSON file\n",
"# (ie: without adding a dependecy outside the .ipynb file)\n",
"# NOTE: For Ozone Scenarios, we're generally using 0.035 ppm to override the default threshold.\n",
"AQbyWeather.addAQScenario(AQScenario(\"bakersfield\", \"72384023155\", AQbyWeather.aqParams[\"pm25\"], None))\n",
"AQbyWeather.addAQScenario(AQScenario(\"bakersfield\", \"72384023155\", AQbyWeather.aqParams[\"pm10\"], None)) # Attempt at pm10 prediction.\n",
"AQbyWeather.addAQScenario(AQScenario(\"bakersfield\", \"72384023155\", AQbyWeather.aqParams[\"o3\"], 0.035))\n",
"AQbyWeather.addAQScenario(AQScenario(\"fresno\", \"72389093193\", AQbyWeather.aqParams[\"pm25\"], None))\n",
"AQbyWeather.addAQScenario(AQScenario(\"fresno\", \"72389093193\", AQbyWeather.aqParams[\"o3\"], 0.035))\n",
"AQbyWeather.addAQScenario(AQScenario(\"visalia\", \"72389693144\", AQbyWeather.aqParams[\"pm25\"], None))\n",
"AQbyWeather.addAQScenario(AQScenario(\"visalia\", \"72389693144\", AQbyWeather.aqParams[\"o3\"], 0.035))\n",
"AQbyWeather.addAQScenario(AQScenario(\"san-jose\", \"72494693232\", AQbyWeather.aqParams[\"pm25\"], None))\n",
"AQbyWeather.addAQScenario(AQScenario(\"san-jose\", \"72494693232\", AQbyWeather.aqParams[\"o3\"], 0.035))\n",
"AQbyWeather.addAQScenario(AQScenario(\"los-angeles\", \"72287493134\", AQbyWeather.aqParams[\"pm25\"], None))\n",
"AQbyWeather.addAQScenario(AQScenario(\"los-angeles\", \"72287493134\", AQbyWeather.aqParams[\"o3\"], 0.035))\n",
"AQbyWeather.addAQScenario(AQScenario(\"phoenix\", \"72278023183\", AQbyWeather.aqParams[\"pm25\"], None))\n",
"AQbyWeather.addAQScenario(AQScenario(\"phoenix\", \"72278023183\", AQbyWeather.aqParams[\"o3\"], 0.035))\n",
"AQbyWeather.addAQScenario(AQScenario(\"fairbanks\", \"70261026411\", AQbyWeather.aqParams[\"pm25\"], None))\n",
"AQbyWeather.addAQScenario(AQScenario(\"lahore-pk\", \"41640099999\", AQbyWeather.aqParams[\"pm25\"], None))\n",
"\n",
"print(f\"AQbyWeather.aqParams: {str(len(AQbyWeather.aqParams))}\")\n",
"print(f\"AQbyWeather.aqScenarios: {str(len(AQbyWeather.aqScenarios))} (Default Selected: {AQbyWeather.selectedScenario.name})\")"
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "3395018f-87bd-4a7f-8ee5-7e90d38bfb04",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"*** CHOOSE YOUR OWN ADVENTURE HERE ***\n",
"Please select a Scenario via the following drop-down-list...\n",
"(NOTE: If you change Scenario, you must re-run remaining cells to see changes.)\n"
]
},
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "54d02042d8c74215baadef6fc83953d1",
"version_major": 2,
"version_minor": 0
},
"text/plain": [
"Dropdown(options=('bakersfield_pm25', 'bakersfield_pm10', 'bakersfield_o3', 'fresno_pm25', 'fresno_o3', 'visal…"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"# CELL #5: Select a Scenario via DROP DOWN LIST to use throughout the Notebook. This will drive the ML process...\n",
"# A default \"value\" is set to avoid issues. Change this default to run the Notebook from start-to-finish for that Scenario.\n",
"print(\"*** CHOOSE YOUR OWN ADVENTURE HERE ***\")\n",
"print(\"Please select a Scenario via the following drop-down-list...\")\n",
"print(\"(NOTE: If you change Scenario, you must re-run remaining cells to see changes.)\")\n",
"ddl = widgets.Dropdown(options=AQbyWeather.aqScenarios.keys(), \n",
" value=AQbyWeather.aqScenarios[\"bakersfield_pm25\"].name) # <-- DEFAULT / FULL-RUN VALUE\n",
"ddl"
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "ffed30d9-1725-405b-a87f-e2aac0be5634",
"metadata": {
"tags": []
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Scenario: bakersfield_pm25 => Particulate Matter < 2.5 micrometers (pm25) with UnhealthyThreshold > 12.0 µg/m³\n",
"{\n",
" \"aqParamTarget\": {\n",
" \"desc\": \"Particulate Matter < 2.5 micrometers\",\n",
" \"id\": 2,\n",
" \"name\": \"pm25\",\n",
" \"unhealthyThresholdDefault\": 12.0,\n",
" \"unit\": \"\\u00b5g/m\\u00b3\"\n",
" },\n",
" \"aqRadiusMeters\": 16100,\n",
" \"aqRadiusMiles\": 10,\n",
" \"location\": \"bakersfield\",\n",
" \"modelFolder\": \"AutogluonModels\",\n",
" \"name\": \"bakersfield_pm25\",\n",
" \"noaaStationID\": \"72384023155\",\n",
" \"noaaStationLat\": 0.0,\n",
" \"noaaStationLng\": 0.0,\n",
" \"openAqLocIDs\": [],\n",
" \"unhealthyThreshold\": 12.0,\n",
" \"yearEnd\": 2022,\n",
" \"yearStart\": 2016\n",
"}\n"
]
}
],
"source": [
"if ddl.value:\n",
" AQbyWeather.selectedScenario = AQbyWeather.aqScenarios[ddl.value]\n",
" print(AQbyWeather.selectedScenario.getSummary())\n",
" print(AQbyWeather.selectedScenario.toJSON())\n",
"else:\n",
" print(\"Please select a Scenario via the above drop-down-list.\")"
]
},
{
"cell_type": "code",
"execution_count": 7,
"id": "9c3a647c-0fee-4b49-8576-0ee355629f0e",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Scenario: bakersfield_pm25 => Particulate Matter < 2.5 micrometers (pm25) with UnhealthyThreshold > 12.0 µg/m³\n",
"Accessing and preparing data from ASDI-hosted NOAA GSOD dataset in Amazon S3 (bucket: noaa-gsod-pds)...\n",
"NOAA Station Lat,Lng Updated for Scenario: bakersfield_pm25 => 35.4344,-119.0542\n",
"noaagsod_df.shape = (2346, 10)\n"
]
},
{
"data": {
"text/html": [
"
\n",
"\n",
"
\n",
" \n",
"
\n",
"
\n",
"
DATE
\n",
"
NAME
\n",
"
LATITUDE
\n",
"
LONGITUDE
\n",
"
DEWP
\n",
"
WDSP
\n",
"
MAX
\n",
"
MIN
\n",
"
PRCP
\n",
"
MONTH
\n",
"
\n",
" \n",
" \n",
"
\n",
"
0
\n",
"
2016-01-01
\n",
"
BAKERSFIELD AIRPORT, CA US
\n",
"
35.43440
\n",
"
-119.05420
\n",
"
29.2
\n",
"
3.0
\n",
"
55.0
\n",
"
27.0
\n",
"
0.0
\n",
"
1
\n",
"
\n",
"
\n",
"
1
\n",
"
2016-01-02
\n",
"
BAKERSFIELD AIRPORT, CA US
\n",
"
35.43440
\n",
"
-119.05420
\n",
"
32.8
\n",
"
2.4
\n",
"
57.0
\n",
"
27.0
\n",
"
0.0
\n",
"
1
\n",
"
\n",
"
\n",
"
2
\n",
"
2016-01-03
\n",
"
BAKERSFIELD AIRPORT, CA US
\n",
"
35.43440
\n",
"
-119.05420
\n",
"
32.6
\n",
"
4.3
\n",
"
68.0
\n",
"
34.0
\n",
"
0.0
\n",
"
1
\n",
"
\n",
"
\n",
"
3
\n",
"
2016-01-04
\n",
"
BAKERSFIELD AIRPORT, CA US
\n",
"
35.43440
\n",
"
-119.05420
\n",
"
32.0
\n",
"
4.9
\n",
"
68.0
\n",
"
39.0
\n",
"
0.0
\n",
"
1
\n",
"
\n",
"
\n",
"
4
\n",
"
2016-01-05
\n",
"
BAKERSFIELD AIRPORT, CA US
\n",
"
35.43440
\n",
"
-119.05420
\n",
"
42.8
\n",
"
4.5
\n",
"
64.9
\n",
"
42.1
\n",
"
0.0
\n",
"
1
\n",
"
\n",
"
\n",
"
...
\n",
"
...
\n",
"
...
\n",
"
...
\n",
"
...
\n",
"
...
\n",
"
...
\n",
"
...
\n",
"
...
\n",
"
...
\n",
"
...
\n",
"
\n",
"
\n",
"
2341
\n",
"
2022-06-04
\n",
"
BAKERSFIELD AIRPORT, CA US
\n",
"
35.43424
\n",
"
-119.05524
\n",
"
50.5
\n",
"
7.3
\n",
"
91.0
\n",
"
64.0
\n",
"
0.0
\n",
"
6
\n",
"
\n",
"
\n",
"
2342
\n",
"
2022-06-05
\n",
"
BAKERSFIELD AIRPORT, CA US
\n",
"
35.43424
\n",
"
-119.05524
\n",
"
45.1
\n",
"
6.4
\n",
"
90.0
\n",
"
63.0
\n",
"
0.0
\n",
"
6
\n",
"
\n",
"
\n",
"
2343
\n",
"
2022-06-06
\n",
"
BAKERSFIELD AIRPORT, CA US
\n",
"
35.43424
\n",
"
-119.05524
\n",
"
58.7
\n",
"
7.5
\n",
"
90.0
\n",
"
63.0
\n",
"
0.0
\n",
"
6
\n",
"
\n",
"
\n",
"
2344
\n",
"
2022-06-07
\n",
"
BAKERSFIELD AIRPORT, CA US
\n",
"
35.43424
\n",
"
-119.05524
\n",
"
47.8
\n",
"
5.4
\n",
"
91.0
\n",
"
66.0
\n",
"
0.0
\n",
"
6
\n",
"
\n",
"
\n",
"
2345
\n",
"
2022-06-08
\n",
"
BAKERSFIELD AIRPORT, CA US
\n",
"
35.43424
\n",
"
-119.05524
\n",
"
43.5
\n",
"
4.3
\n",
"
91.9
\n",
"
75.9
\n",
"
0.0
\n",
"
6
\n",
"
\n",
" \n",
"
\n",
"
2346 rows × 10 columns
\n",
"
"
],
"text/plain": [
" DATE NAME LATITUDE LONGITUDE DEWP WDSP \\\n",
"0 2016-01-01 BAKERSFIELD AIRPORT, CA US 35.43440 -119.05420 29.2 3.0 \n",
"1 2016-01-02 BAKERSFIELD AIRPORT, CA US 35.43440 -119.05420 32.8 2.4 \n",
"2 2016-01-03 BAKERSFIELD AIRPORT, CA US 35.43440 -119.05420 32.6 4.3 \n",
"3 2016-01-04 BAKERSFIELD AIRPORT, CA US 35.43440 -119.05420 32.0 4.9 \n",
"4 2016-01-05 BAKERSFIELD AIRPORT, CA US 35.43440 -119.05420 42.8 4.5 \n",
"... ... ... ... ... ... ... \n",
"2341 2022-06-04 BAKERSFIELD AIRPORT, CA US 35.43424 -119.05524 50.5 7.3 \n",
"2342 2022-06-05 BAKERSFIELD AIRPORT, CA US 35.43424 -119.05524 45.1 6.4 \n",
"2343 2022-06-06 BAKERSFIELD AIRPORT, CA US 35.43424 -119.05524 58.7 7.5 \n",
"2344 2022-06-07 BAKERSFIELD AIRPORT, CA US 35.43424 -119.05524 47.8 5.4 \n",
"2345 2022-06-08 BAKERSFIELD AIRPORT, CA US 35.43424 -119.05524 43.5 4.3 \n",
"\n",
" MAX MIN PRCP MONTH \n",
"0 55.0 27.0 0.0 1 \n",
"1 57.0 27.0 0.0 1 \n",
"2 68.0 34.0 0.0 1 \n",
"3 68.0 39.0 0.0 1 \n",
"4 64.9 42.1 0.0 1 \n",
"... ... ... ... ... \n",
"2341 91.0 64.0 0.0 6 \n",
"2342 90.0 63.0 0.0 6 \n",
"2343 90.0 63.0 0.0 6 \n",
"2344 91.0 66.0 0.0 6 \n",
"2345 91.9 75.9 0.0 6 \n",
"\n",
"[2346 rows x 10 columns]"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"# GET NOAA GSOD WEATHER DATA...\n",
"print(AQbyWeather.selectedScenario.getSummary())\n",
"noaagsod_df = AQbyWeather.getNoaaDataFrame()\n",
"\n",
"if(len(noaagsod_df) >= 1):\n",
" # Update NOAA Station Lat/Lng...\n",
" AQbyWeather.selectedScenario.updateNoaaStationLatLng(noaagsod_df.iloc[0])\n",
" \n",
" # Save DataFrame to CSV...\n",
" noaagsod_df.to_csv(AQbyWeather.getFilenameNOAA(), index=False)\n",
"\n",
" # Output DataFrame properties...\n",
" print('noaagsod_df.shape =', noaagsod_df.shape)\n",
" display(noaagsod_df)"
]
},
{
"cell_type": "code",
"execution_count": 8,
"id": "2893d81f-a488-464d-b374-dafbfc0ae3fb",
"metadata": {
"tags": []
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Scenario: bakersfield_pm25 => Particulate Matter < 2.5 micrometers (pm25) with UnhealthyThreshold > 12.0 µg/m³\n",
"Accessing ASDI-hosted OpenAQ Locations (HTTPS API)...\n",
"OpenAQ Location IDs within 10 miles (16100m) of NOAA Station 72384023155 at 35.4344,-119.0542: [6885, 28777, 6883, 230814, 887]\n",
"Accessing ASDI-hosted OpenAQ Averages (HTTPS API)...\n",
"aq_df.shape = (2195, 5)\n"
]
},
{
"data": {
"text/html": [
"
\n",
"\n",
"
\n",
" \n",
"
\n",
"
\n",
"
day
\n",
"
parameter
\n",
"
unit
\n",
"
average
\n",
"
isUnhealthy
\n",
"
\n",
" \n",
" \n",
"
\n",
"
0
\n",
"
2016-03-06
\n",
"
pm25
\n",
"
µg/m³
\n",
"
2.0000
\n",
"
0
\n",
"
\n",
"
\n",
"
1
\n",
"
2016-03-10
\n",
"
pm25
\n",
"
µg/m³
\n",
"
10.5000
\n",
"
0
\n",
"
\n",
"
\n",
"
2
\n",
"
2016-03-11
\n",
"
pm25
\n",
"
µg/m³
\n",
"
5.4286
\n",
"
0
\n",
"
\n",
"
\n",
"
3
\n",
"
2016-03-12
\n",
"
pm25
\n",
"
µg/m³
\n",
"
4.1667
\n",
"
0
\n",
"
\n",
"
\n",
"
4
\n",
"
2016-03-13
\n",
"
pm25
\n",
"
µg/m³
\n",
"
6.0000
\n",
"
0
\n",
"
\n",
"
\n",
"
...
\n",
"
...
\n",
"
...
\n",
"
...
\n",
"
...
\n",
"
...
\n",
"
\n",
"
\n",
"
2190
\n",
"
2022-06-10
\n",
"
pm25
\n",
"
µg/m³
\n",
"
9.2045
\n",
"
0
\n",
"
\n",
"
\n",
"
2191
\n",
"
2022-06-11
\n",
"
pm25
\n",
"
µg/m³
\n",
"
9.4583
\n",
"
0
\n",
"
\n",
"
\n",
"
2192
\n",
"
2022-06-12
\n",
"
pm25
\n",
"
µg/m³
\n",
"
6.1667
\n",
"
0
\n",
"
\n",
"
\n",
"
2193
\n",
"
2022-06-13
\n",
"
pm25
\n",
"
µg/m³
\n",
"
3.5435
\n",
"
0
\n",
"
\n",
"
\n",
"
2194
\n",
"
2022-06-14
\n",
"
pm25
\n",
"
µg/m³
\n",
"
7.3000
\n",
"
0
\n",
"
\n",
" \n",
"
\n",
"
2195 rows × 5 columns
\n",
"
"
],
"text/plain": [
" day parameter unit average isUnhealthy\n",
"0 2016-03-06 pm25 µg/m³ 2.0000 0\n",
"1 2016-03-10 pm25 µg/m³ 10.5000 0\n",
"2 2016-03-11 pm25 µg/m³ 5.4286 0\n",
"3 2016-03-12 pm25 µg/m³ 4.1667 0\n",
"4 2016-03-13 pm25 µg/m³ 6.0000 0\n",
"... ... ... ... ... ...\n",
"2190 2022-06-10 pm25 µg/m³ 9.2045 0\n",
"2191 2022-06-11 pm25 µg/m³ 9.4583 0\n",
"2192 2022-06-12 pm25 µg/m³ 6.1667 0\n",
"2193 2022-06-13 pm25 µg/m³ 3.5435 0\n",
"2194 2022-06-14 pm25 µg/m³ 7.3000 0\n",
"\n",
"[2195 rows x 5 columns]"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"# GET OPENAQ AIR QUALITY DAILY AVERAGES DATA...\n",
"print(AQbyWeather.selectedScenario.getSummary())\n",
"aq_df = AQbyWeather.getOpenAqDataFrame() # Gets nearby Location IDs THEN gets associated daily averages.\n",
" \n",
"if len(aq_df) > 0:\n",
" # Output DataFrame properties...\n",
" print('aq_df.shape =', aq_df.shape)\n",
" display(aq_df)\n",
" aq_df.to_csv(AQbyWeather.getFilenameOpenAQ(), index=False)"
]
},
{
"cell_type": "markdown",
"id": "0a676054-ca63-47a2-832c-c136d1f0ecb4",
"metadata": {},
"source": [
"## Merging the NOAA and OpenAQ data into one dataframe...\n",
"We'll be using Autogluon's [_TabularPredictor_](https://auto.gluon.ai/stable/tutorials/tabular_prediction/index.html) on this merged dataframe to predict our target label column. For best results, we need to ensure we have enough data and that we have enough of each target label classification {0,1}.\n",
"\n",
"Review the shape of the merged_df table and the target label bar chart to determine if you have enough data and enough variation in the target label. Ideally, we should have thousands of total rows and at least hundreds of each class value.\n",
"\n",
"In the following cell, we can also visualize correlations between different columns in our merged_df table."
]
},
{
"cell_type": "code",
"execution_count": 9,
"id": "6a873736-65b4-41a5-82e0-794ef33188b3",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Scenario: bakersfield_pm25 => Particulate Matter < 2.5 micrometers (pm25) with UnhealthyThreshold > 12.0 µg/m³\n",
"merged_df.shape = (2184, 7)\n"
]
},
{
"data": {
"text/html": [
"
"
]
},
"metadata": {
"needs_background": "light"
},
"output_type": "display_data"
}
],
"source": [
"# Merge the NOAA GSOD weather data with our OpenAQ data by DATE...\n",
"# Perform another column drop to remove columns we don't want as features/inputs.\n",
"# This column removal will NOT be necessary once we can use Autogluon ignore_columns param (TBD).\n",
"print(AQbyWeather.selectedScenario.getSummary())\n",
"merged_df = AQbyWeather.getMergedDataFrame(noaagsod_df, aq_df)\n",
"\n",
"if(len(merged_df > 0)):\n",
" # Output DataFrame properties...\n",
" print('merged_df.shape =', merged_df.shape)\n",
" display(merged_df)\n",
" merged_df.groupby([AQbyWeather.mlTargetLabel]).size().plot(kind=\"bar\")\n",
" merged_df.to_csv(AQbyWeather.getFilenameOther(\"dataMERGED\"), index=False)\n",
"\n",
"# TODO: Explore other ways to join the data and/or handle null values..."
]
},
{
"cell_type": "code",
"execution_count": 10,
"id": "c5dee84f-da68-4b10-a075-a5cab419cd16",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Scenario: bakersfield_pm25 => Particulate Matter < 2.5 micrometers (pm25) with UnhealthyThreshold > 12.0 µg/m³\n"
]
},
{
"data": {
"image/png": "iVBORw0KGgoAAAANSUhEUgAAAlsAAAHYCAYAAAB6CS2aAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjUuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8qNh9FAAAACXBIWXMAAAsTAAALEwEAmpwYAAAwKUlEQVR4nO3debwnVX3n/9ebRhaDhiaNBAFBlhkXkFZ6mJ9kIwoJTozwcxiFmAgTHQajJi44aphRY+JIEqKGyE/SgwhukIgorcFHDy7EBRcabWgWCavSHaJii4qgSPfn90fVhS+Xe7u/dH9Pf+/t7+vZj3rcqnNOVZ2666c/51RVqgpJkiS1sc24OyBJkrQ1M9iSJElqyGBLkiSpIYMtSZKkhgy2JEmSGjLYkiRJashgawtKsi7JyiTXJrkqyWuTbNPXHZ7kh3391HJEkncmedXAMZYnOXtg+2+SvCbJPknu7fe7LslZU8cep0fQ/3uTfCPJ9Um+luTEgTa7Jflk/zm7LsklffmcvObZJKkkHxzY3jbJ95J8clq7jyf5yrSyM5K8aWD71CRntu/16Gzs+pOcmOTd/fpbktyT5HED7e/e8r3eNAM/69ck+UiSR89Q/okkOw/sc0qSb/b1VyR5cV9+WZIb+u//LyX592O6rAcM872c5JgkV/c/06uSHDNQd26SNUm277cXJbktyUEDv//WJrm1X/90//N+zbR+vCXJKZt5LZdvpP7uadsPfJ9uYJ/Dp/9cb0b/HrjG/tyPH6i7LcmiUZxHbc3ZP0xbqXuranFVPRU4EngO8OaB+i/09VPLp4EvAYcB9IHEIuCpA/scBkz9sri5qhYDTwOeAhzT8mKGNGz/b66qp1fVk4HjgFcl+a99m7cCl1bVwVX1FOANA/vPxWuezU+AA5Ps2G8fCawZbND/8T0E+MUk+w5U/U/gxCT79uUvBU5t3+WR2uj1T3Mn8NrmvWpj6mf9QOA+4OQZytcCLwdIcjLd5+PQ/vv52UAGjveiqjoYOA/46y10DRuywa9lkoOB04Gj+5/p5wGnJ3nawDHWAX84eNCqWjX1+w9YBryu3z6i1YVU1WGtjt3AicDjN9ZIc4/B1phU1XeBk4BXJMkGml4OPLNffypwDfDjJAv7/xU+Gfj6tGPf3++3/8g7/sgN0/+1gztU1S3Aa4A/7ot2B1YP1F89/SRz7Jo35BLgd/r144Hzp9U/H/gEcAFd0AlAVf2ILrh6d7+8qaruat3ZBjZ2/YPOAV6YZJfmvWrrC8z8ffllYI9+/U+Bl/VfZ6rqR1V13gz7fH6WY43Dhr6WpwD/u6puBeg/vh143UCbdwGvTrJt+67ObipzlWT3JJ8fyDz+2hD7nttnnS9PckuSYweqd0pyYZ+t/NDU7/kkhyT55yRXpsv0796X/7c+o3lVko9OZUMHznUssAT4UN/HqUD3lUm+3mcPn5RkmyQ3Jtm132+bJDdNbWs8DLbGqA8qFgBTQyW/locOI+5XVf8K3J/kCXRZoC8DX6ULYJYAq6rqvsHj9j+kzwZWbalrmc0w/af7n/90Xwee1K+fCbw3yefSDZ897H92c+maN+IC4LgkO9Bl4746rX7qj9b5/foDqup8YCHw2Kr6wBboawsbu/5Bd9MFXH+yJTrWQh9IPIdp35dJFtB9vy5L8ljgMf3vg4353enHGqMNfS2fClw5rf0KHprV/jbwReAPHsE59xv8HcmDGcNR+D1geZ9VOxhYOeR+uwO/CjwXOG2g/OnAq+gy7vsCv5LkUcDfAcdW1SF0399v69tfVFX/oc9gXg+8ZPAkVXUh3efwRX22796+6s6qegbwHuCUqloPfBB4UV9/BHBVVX1vyOtRA2P9H4Ue5gtV9dwZyi+nC1QOA95B97/hw4Af0g3TTdmv/wVUwMVV9am23R3asP0f9EC2r6qW90NnR9H94fpGkgP76rl6zTOqqquT7EMXSF0yWJdkN+AA4ItVVUl+nuTAqrqmr9+T7hf7+iQ7VdW8mcM0ZUPXP4szgJVJTm/asdHbsf++hC6z9d5p5XvQ/UG9FPiFIY73oST3ArcBrxxpTzfRJnwtZ/J24GLgn4ZsPzVtAOjmM23ieWdyBXBOHxB9vKpWbqDt4HvuPt4HONf1P8NTvlZVq/t+rgT2Ae4CDgQu7RNdC4A7+vYHJvkLYGdgJ2D5kP2+qP94JV1mHLog7mK67OEfAu8b8lhqxGBrjPoAYh3wXbrhtNlMzXs6iG4Y7na6uSw/4qE/RA/5RTSHDNv/QU+n+2MEQFWtBT4MfLifePrrdL9c5uo1b8gyuvkshwO/NFD+ArrM1a39L+LH0v0hm5qb9bd0c/ye3H8cHJKZT2a7/oepqruSfJh+btM8cu8s35f3VtXiPhO7HHh5VZ2R5O4k+24gu/WiqlrRrLebbrav5XV0cw+vGig7BLh2cOequrEPRF7QtJdDqKrPJ/l1uqHRc5O8o6reD9ybZLuBEYRd6OYTTvnZwHpmKV9H9/c2wLVV9Uwe7lzgmKq6Kt0NQocP2fWp80ydg6q6Pcl3kjwLOJQHs1waE4cRx6QfPz8LeHdt/G3gl9OlqNdW1bo+8NiZbihug3fSzBGPqP/9/5ZPp0u3k+RZefBurscA+9ENQcxX5wB/VlXTh4OOB46qqn2qah+6P07HASR5Dt1w8/uBPween+QpW67LIzXb9c/mHcB/Zyv6z2FV3UM3J/G1/VDj24Ez+yFFkuyU/m7EOW62r+XpwBv7n+Wpn+k/Bf5mhmO8jW6O11gl2Rv4TlX9H+Bs4Bl91T8Dv9+32ZEuMPzcJp7mBmDXJM/sj/eoJFNDq48B7ugza7MFRz/u2w3jbLrhxI9U1bpN7K9GxGBry9qxn2twLfBp4P8CfzZQP33O1tRky1V0d/ENPg5gFfDDqhr8H9ZcNUz/90v/6AfgH4Ezqmoq63UIsCLJ1XRzvs6uqiu2UN9HrqpWV9UZg2X9H6O9Gfgc9ZOKf5jkN+iGA/6oOj+hy2pt8PbzuWqm699I+zuBjwHbt+vVlldV3wCupguy30P3B/yKdI83+AKwfozdG8psX8t+CO71wCeSfJPupo//MdPQXFVdy7SbfMbkcOCqJN8AXkiXSYZuzuDz+wzcV+iCl89vygn67NixwF8muYpuXtjU3ZD/i27e25eAb85yiHOBs6ZNkJ/NMrrhSIcQ54BsPKkiSZLmkyRLgHdW1UbvqlR7W01aXpIkQZI3AC/DuVpzhpktSZKkhpyzJUmS1JDBliRJUkMGW5IkaauR5Jwk3820F5cP1Cfda5ZuSvey9GcM1J3Qv+7oxiQnjKpPBlvzQJKTxt2HcfC6J4vXPVkm9bphsq99CzmX7o0js3kO3ds6DqB7R/F7ANK9h/XNwH+kexjsm5MsHEWHDLbmh0n9wfS6J4vXPVkm9bphsq+9uf45aGs30ORo4P39cwu/Auyc7oXgvw1cWlVrq+oHdK/T2lDQNjSDLUmSNEn2oHtt3JTVfdls5ZvN52wNadEuC2qfvR41lnM/YY9tWXLwDmN5RseqtbuO47QALFi4kO332mss133QLt8bx2kBv95j68CYTOp1b7vzmK87G2/SyoKFO7P9E8Zz7ffdvvrOqtoiP+i//Zu/UN9fO/o3BV159c+uBX46ULS0qpaO/EQjZLA1pH32ehRfW77XuLuxxe3/4ZPH3YWx+NrvnTXuLozFfhdM5td7km1z/xijjjFat8OcfxtSE9965eu+taXO9f216/ja8ieM/LgLdr/xp1W1ZDMOsQYY/IO+Z1+2hoe+AHxP4LLNOM8DHEaUJEkjV8D6Bv9GYBnw4v6uxP+H7j29dwDLgd9KsrCfGP9bfdlmM7MlSZIaKNbVls8gJjmfLkO1KMlqujsMHwVQVWcBlwD/CbgJuAf4r33d2iR/DlzRH+qtVbWhifZDM9iSJElbjao6fiP1Bbx8lrpzgHNG3SeDLUmSNHLdMOLE3fsxI+dsSZIkNWRmS5IkNTGiCe3znsGWJEkauaJYVw4jgsOIkiRJTZnZkiRJTThBvmNmS5IkqSEzW5IkaeQKWGdmCzCzJUmS1JSZLUmS1IRztjoGW5IkaeQKfPRDz2FESZKkhsxsSZKkJnx+fMfMliRJUkNmtiRJ0sgV5aMfegZbkiRp9ArWGWsBDiNKkiQ1ZWZLkiSNXOEE+SlmtiRJkhoysyVJkhoI68i4OzEnGGxJkqSRK2C9E+QBhxElSZKamrOZrSTrgFXAo4D7gfcD76yq9UkOBy4Gbh3Y5RTgd4BvVdW7+mMsB26vqpf2238DrAEuAq4HbgC2Az4P/FFVOZdPkqQRcRixM2eDLeDeqloMkORxwIeBxwJv7uu/UFXPHdwhyc7AC4B3JdkGWNTvM+Uw4NX9+s1VtTjJtsBngWPogjBJkqSRmRfDiFX1XeAk4BVJNhQmXw48s19/KnAN8OMkC5NsDzwZ+Pq0Y9/f77f/yDsuSdKEKrrM1qiX+WguZ7YeoqpuSbIAeFxf9GtJVg40+c9VdXOS+5M8gS6L9WVgD7oA7IfAqqq6bzBeS/Jo4NnAm7bAZUiSNDHW1/wMjkZt3gRbM3jYMGLvcrpA6zDgHXTB1mF0wdaXBtrt1wdrBVxcVZ+afqAkJ9Fl1HjCHvP5UyVJksZl3kQQSfYF1gHfpRsOnM2X6IKrg+iGEW8HXgv8CHjfQLubp+aEzaaqlgJLAZYcvIM3sEqSNKSpYUTNkzlbSXYFzgLeXVUbC3ouB54LrK2qdVW1FtiZbijx8qYdlSRJmmYuZ7Z27If5ph798AG6YcEp0+ds/UVVXUj3uIhFdHcvTlkF7FRVdzbtsSRJAqAI6+ZHTqe5ORtsVdWCDdRdBvziLHXreOjjHqiqE6dt3wYcuLl9lCRJ2pg5G2xJkqT5zbsROwZbkiRp5Jwg/yAHUyVJkhoysyVJkhoI68qcDpjZkiRJasrMliRJGrkC1pvTAQy2JElSI06Q7xhySpIkNWRmS5IkjVyVE+Sn+FmQJElqyMyWJElqYr1ztgCDLUmS1ED3BHkH0MBhREmSpKbMbEmSpAacID/Fz4IkSVJDZrYkSdLIjfMJ8kmOAv4WWACcXVWnTat/J/Cb/eajgcdV1c593TpgVV/37ap63ub2x2BLkiRtNZIsAM4EjgRWA1ckWVZV1021qapXD7R/JfD0gUPcW1WLR9kngy1JktTEuhrLox8OBW6qqlsAklwAHA1cN0v744E3t+yQwZYkSRq5Iq0e/bAoyYqB7aVVtXRgew/g9oHt1cB/nOlASfYGngh8dqB4h/749wOnVdXHN7fDBluSJGk+ubOqlozoWMcBF1bVuoGyvatqTZJ9gc8mWVVVN2/OSQy2JElSE+vH8+iHNcBeA9t79mUzOQ54+WBBVa3pP96S5DK6+VybFWz56AdJkrQ1uQI4IMkTk2xHF1Atm94oyZOAhcCXB8oWJtm+X18E/Aqzz/UampktSZI0cuN6XU9V3Z/kFcByukc/nFNV1yZ5K7CiqqYCr+OAC6qqBnZ/MvD3SdbTJaROG7yLcVMZbEmSpJErMq67EamqS4BLppW9adr2W2bY73LgoFH3x2FESZKkhsxsSZKkJsb1BPm5xmBrSKvW7sr+Hz553N3Y4m76vbPG3YWx2O+CyftaA9R2tfFGW6nadjKvfbs7JvPPwK5Xjmd4a9y+Ne4OTKjJ/CmTJElNVcG68Tz6Yc4x2JIkSQ2E9UxmBnE6Q05JkqSGzGxJkqSRKxxGnOJnQZIkqSEzW5IkqYlxPEF+LvKzIEmS1JCZLUmSNHJFWD+m1/XMNQZbkiSpCYcRO34WJEmSGjKzJUmSRq6A9T76ATCzJUmS1JSZLUmS1EBY5+t6AIMtSZLUgMOID/KzIEmS1JCZLUmS1ITDiB0zW5IkSQ2Z2ZIkSSNXFeds9Qy2JElSE+sMtgCHESVJkpoysyVJkkaugPVOkAfMbEmSJDVlZkuSJDUQ52z1/CxIkiQ1ZGZLkiSNXPe6HudsgcGWJElqZJ0DaMAcHUZM8s4krxrYXp7k7IHtv0nymiT3JvlGkuuTfC3JiQNtdkvyySRXJbkuySV9+T79fiv78rOSzMnPgyRJmv/mambrS8ALgHf1gdAi4LED9YcBrwZurqqnAyTZF7goSarqfcBbgUur6m/7+qcN7H9zVS1Osi3wWeAY4KLG1yRJ0sQo4jBib65mdC4HntmvPxW4BvhxkoVJtgeeDKwd3KGqbgFeA/xxX7Q7sHqg/urpJ6mq+/tz7T/qC5AkSYI5GmxV1b8C9yd5Al0W68vAV+kCsCXAKuC+GXb9OvCkfv1M4L1JPpfk1CSPn944yaOBZ/fHkyRJI7SebUa+zEdzdRgRuozTYf3yDmCPfv2HdMOMM3kgX1lVy/uhxaOA5wDfSHJgX71fkpV0N0tcXFWfmvFgyUnASQALFi7c3OuRJGliVME6hxGBuR1sfYkuuDqIbhjxduC1wI+A982yz9OB66c2qmot8GHgw0k+Cfw6cCX9nK2NdaCqlgJLAbbfa6/a1AuRJEmTay7n4y4Hngusrap1feC0M91Q4uXTGyfZBzgd+Lt++1n9MCFJHgPsB3x7i/RckiSxvjLyZT6ay5mtVXR3IX54WtlOVXVnkp3ohgO/AewA/Bg4o6rO7dseArw7yf10QeXZVXVFH5RJkiRtEXM22KqqdTz0cQ9U1YkD67cBO25g/78G/nqG8tuAAx+2gyRJGpnu0Q9zeQBty5mzwZYkSZrf1jE/h/1GzZBTkiSpITNbkiRp5HwR9YPMbEmSJDVkZkuSJDXgBPkpfhYkSZIaMrMlSZKaWO/diICZLUmS1MDUuxFHvQwjyVFJbkhyU5I3zFB/YpLvJVnZLy8dqDshyY39csIoPhdmtiRJ0lYjyQLgTOBIYDVwRZJlVXXdtKb/UFWvmLbvLsCbgSV0N1Re2e/7g83pk5ktSZLUxPraZuTLEA4FbqqqW6rqPuAC4Oghu/zbwKVVtbYPsC4Fjtqkix9gsCVJkrYmewC3D2yv7sum+89Jrk5yYZK9HuG+j4jBliRJGrnu3YijX4BFSVYMLCdtQvc+AexTVU+jy16dN8prn845W5IkqYlGdyPeWVVLNlC/BthrYHvPvuwBVfX9gc2zgb8a2PfwaftetqkdnWJmS5IkbU2uAA5I8sQk2wHHAcsGGyTZfWDzecD1/fpy4LeSLEyyEPitvmyzmNmSJEkjN653I1bV/UleQRckLQDOqaprk7wVWFFVy4A/TvI84H5gLXBiv+/aJH9OF7ABvLWq1m5unwy2JEnSVqWqLgEumVb2poH1NwJvnGXfc4BzRtkfgy1JktSE70bsGGxJkqTRe/DuwYlnyClJktSQmS1JkjRyhS+inmJmS5IkqSEzW5IkqQnnbHXMbEmSJDVkZkuSJI3cuB5qOhcZbEmSpCYMtjoOI0qSJDVkZmtIB+3yPb72e2eNuxtb3H4XnDzuLozFzcdN3tcaJvfrDbDNTyfzf+A/f8z6cXdhLP7tsHH3YEwu2HKnKnyo6RQzW5IkSQ2Z2ZIkSU34UNOOwZYkSRq9coL8FIcRJUmSGjKzJUmSRs7nbD3IzJYkSVJDZrYkSVITZrY6BluSJGnkfM7WgxxGlCRJasjMliRJaqLMbAFmtiRJkpoysyVJkprwCfIdM1uSJEkNmdmSJEkjV76u5wEGW5IkqQknyHccRpQkSWrIzJYkSWrAh5pOMbMlSZLUkJktSZLUhHO2OgZbkiRp5ArvRpziMKIkSVJDZrYkSdLoVfesLZnZkiRJasrMliRJasJ3I3YMtiRJ0sgV3o04xWFESZKkhsxsSZKkBnyC/JR5mdlKUkk+OLC9bZLvJfnktHYfT/KVaWVnJHnTwPapSc5s32tJkjSJ5mtm6yfAgUl2rKp7gSOBNYMNkuwMHALcnWTfqrqlr/qfwMqBYO2lwNO3TLclSZocPvqhMy8zW71LgN/p148Hzp9W/3zgE8AFwHFThVX1I+BU4N398qaquqt1ZyVJ0mSaz8HWBcBxSXYAngZ8dVr9VAB2fr/+gKo6H1gIPLaqPjDbCZKclGRFkhXf+/66kXZekqStXVVGvsxH8zbYqqqrgX3oAqlLBuuS7AYcAHyxqv4F+HmSAwfq9wR2Bx6fZKcNnGNpVS2pqiW7/tKCBlchSdLWqcpga8q8DbZ6y4DTefgQ4gvoMle3JrmNB4OyKX8LvBn4x/6jJElSE/N1gvyUc4C7qmpVksMHyo8HjqqqLwMkeSLwaeDUJM8BHge8H3g0cHWS91XVdVu055IkbeV89ENnXme2qmp1VZ0xWJZkH2Bv4CsD7W4FfpjkN4B3AX9UnZ8Ar6ObKC9JkrYCSY5KckOSm5K8YYb61yS5LsnVST6TZO+BunVJVvbLslH0Z15mtqrqYfOsquoy4LJ+c48Z6p/Rr/77aeUXAReNtoeSJGkcj35IsgA4k+6xUKuBK5IsmzaC9Q1gSVXdk+RlwF8BL+zr7q2qxaPs07zObEmSpLlrTBPkDwVuqqpbquo+uqcXHP3QftXnquqefvMrwJ4jvfBpDLYkSdLWZA/g9oHt1cww4jXgJcCnBrZ36B/79JUkx4yiQ/NyGFGSJM1tRbNHNSxKsmJge2lVLd2UAyX5fWAJ8BsDxXtX1Zok+wKfTbKqqm7ejP4abEmSpHnlzqpasoH6NcBeA9t7Mu2VfgBJjqB7o8xvVNXPpsqrak3/8ZYkl9G90m+zgi2HESVJUhPVYBnCFcABSZ6YZDu6V/Y95K7CJE8H/h54XlV9d6B8YZLt+/VFwK8Am/1oKDNbkiRp9PonyG/x01bdn+QVwHJgAXBOVV2b5K3AiqpaBvw1sBPwkSQA366q5wFPBv4+yXq6hNRpo3gOp8GWJEnaqlTVJUx7lV9VvWlg/YhZ9rscOGjU/THYkiRJbYzhOVtzkXO2JEmSGjKzJUmSmhjHnK25yGBLkiQ1MY7X9cxFDiNKkiQ1ZGZLkiSNXOEw4hQzW5IkSQ2Z2ZIkSaNXgJktwMyWJElSU2a2JElSE96N2DHYkiRJbRhsAQ4jSpIkNWVmS5IkNRAf/dAzsyVJktSQmS1JktSGc7YAgy1JktRC+QT5KQ4jSpIkNWRmS5IkteEwImCwNbRVa3dlvwtOHnc3trjabjJ/Uibxaw1w83FnjbsLY7P/+ZP5NV8/oT/jrHd4S1uOwZYkSWrEoBYMtiRJUisTmjidzgnykiRJDZnZkiRJbZjZAsxsSZIkNWVmS5IkjV4BPtQUMLMlSZLUlJktSZLURDlnCzDYkiRJrRhsAQ4jSpIkNWVmS5IkteEEecDMliRJUlNmtiRJUhNxzhZgsCVJkloonCDfcxhRkiSpITNbkiSpgThBvmdmS5IkqSEzW5IkqQ3nbAEGW5IkqRWDLcBhREmSpKbMbEmSpDbMbAFmtiRJkpoysyVJkkav8NEPPTNbkiRJDZnZkiRJTfhuxI7BliRJasNgC3AYUZIkqamtIthKUkk+OLC9bZLvJflkv31iknf3629Jck+Sxw20v3vL91qSJLWQ5KgkNyS5KckbZqjfPsk/9PVfTbLPQN0b+/Ibkvz2KPqzVQRbwE+AA5Ps2G8fCazZQPs7gdc275UkSdqikiwAzgSeAzwFOD7JU6Y1ewnwg6raH3gn8Jf9vk8BjgOeChwF/H/98TbL1hJsAVwC/E6/fjxw/gbangO8MMkuzXslSdKESo1+GcKhwE1VdUtV3QdcABw9rc3RwHn9+oXAs5OkL7+gqn5WVbcCN/XH2yxbU7B1AXBckh2ApwFf3UDbu+kCrj/Z0AGTnJRkRZIV637yk9H1VJKkSVAZ/QKLpv4298tJ0866B3D7wPbqvmzGNlV1P/BD4JeG3PcR22ruRqyqq/sx1+PpslwbcwawMsnpGzjmUmApwPZ77eU9FZIkjd+dVbVk3J14JLamzBbAMuB0NjyECEBV3QV8GHh54z5JkjR5qtGycWuAvQa29+Th87gfaJNkW+AXge8Pue8jtrUFW+cAf1ZVq4Zs/w7gv7MVZfgkSZpwVwAHJHliku3oJrwvm9ZmGXBCv34s8Nmqqr78uP5uxScCBwBf29wObVVBRlWtphseHLb9nUk+Bry6Xa8kSZpQY5iAU1X3J3kFsBxYAJxTVdcmeSuwoqqWAe8FPpDkJmAtXUBG3+4fgeuA+4GXV9W6ze3TVhFsVdVOM5RdBlzWr58LnNuvv2Vau9cAr2nbQ0mSJs+4XtdTVZcwbf52Vb1pYP2nwH+ZZd+3AW8bZX+2tmFESZKkOWWryGxJkqQ5yPv4ATNbkiRJTZnZkiRJbZjZAsxsSZIkNWVmS5IkjdwjeJfhVs9gS5IktdG9y3DiOYwoSZLUkJktSZLUhsOIgJktSZKkpsxsSZKkJpwg3zHYkiRJbRhsAQ4jSpIkNWVmS5IkjZ7P2XqAmS1JkqSGzGxJkqQ2zGwBBluSJKkVgy3AYURJkqSmzGxJkqQmnCDfMbMlSZLUkMGWJElSQwZbkiRJDTlnS5IkteGcLcBgS5IkteAT5B/gMKIkSVJDZra0QbXtZP63ZJufZtxdGIv9zz953F0Ym5uOP2vcXRiLf/f+l427C2Ox7Y8n82d8i5vMPyEPY2ZLkiSpITNbkiSpDTNbgMGWJElqIDhBforDiJIkSQ2Z2ZIkSW2Y2QLMbEmSJDVlZkuSJI2eDzV9gMGWJElqw2ALcBhRkiSpKTNbkiSpDTNbgJktSZKkpsxsSZKkJpwg3zGzJUmS1JCZLUmS1IaZLcBgS5IktVAYbPUcRpQkSWrIzJYkSWrCCfIdM1uSJEkNmdmSJEltmNkCzGxJkqRGUqNfNqs/yS5JLk1yY/9x4QxtFif5cpJrk1yd5IUDdecmuTXJyn5ZPMx5DbYkSdKkeAPwmao6APhMvz3dPcCLq+qpwFHAu5LsPFD/uqpa3C8rhzmpwZYkSWqjGiyb52jgvH79POCYh3W56l+q6sZ+/V+B7wK7bs5JDbYkSdKk2K2q7ujX/w3YbUONkxwKbAfcPFD8tn548Z1Jth/mpE6QlyRJo9fuoaaLkqwY2F5aVUunNpJ8GvjlGfY79SHdq6pk9llgSXYHPgCcUFXr++I30gVp2wFLgdcDb91Yhw22JEnSyKVfGrizqpbMVllVR8xWl+Q7SXavqjv6YOq7s7R7LPBPwKlV9ZWBY09lxX6W5H3AKcN02GFESZI0KZYBJ/TrJwAXT2+QZDvgY8D7q+rCaXW79x9DN9/rmmFOarAlSZLamHsT5E8DjkxyI3BEv02SJUnO7tu8APh14MQZHvHwoSSrgFXAIuAvhjnpvBlGTLKO7uK2Ba6nG0O9Z1r5rcAfVNVd/T6nAC8Ffgr8HPi7qnp/ksuA3fvyu4E/rKobtuwVSZKkLamqvg88e4byFXTxAlX1QeCDs+z/rE0573zKbN3bP9PiQOA+4OQZytcCLwdIcjJwJHBoVS2m++QODh+/qKoOprv186+30DVIkjQx5tpDTcdlPgVbg74A7D9D+ZeBPfr1PwVeVlU/AqiqH1XVeTPs8/lZjiVJkrTZ5l2wlWRb4Dl0Q4eD5QvoslfL+rsIHlNVtwxxyN+dfqyBY56UZEWSFet+8pPN7LkkSRNm7s3ZGot5M2cL2DHJyn79C8B7p5XvQTeX61LgF4Y43oeS3AvcBrxypgb9czuWAmy/117z9EssSdKY+JcTmF/B1r393KsZy5M8GlgOvLyqzkhyd5J9N5DdelE/IU6SJKmZeTeMOJuqugf4Y+C1/VDj24Ez+yFFkuyU5MXj7KMkSROjweT4+TpBfj5ltjaqqr6R5GrgeOA9wE7AFUl+Tvfoh78ZZ/8kSdLkmTfBVlXtNEx5Vf3uwOZf9cv0fQ4faeckSdLDzdNM1KjNm2BLkiTNL/N12G/Utpo5W5IkSXORmS1JktSGmS3AzJYkSVJTZrYkSVITztnqGGxJkqTRm8ev1xk1hxElSZIaMrMlSZLaMLMFmNmSJElqysyWJEkaueAE+SlmtiRJkhoysyVJktowswUYbEmSpEZSRlvgMKIkSVJTZrYkSdLo+VDTB5jZkiRJasjMliRJasJHP3QMtiRJUhsGW4DDiJIkSU2Z2ZIkSU04jNgxsyVJktSQmS1JktSGmS3AYEuSJLVQDiNOcRhRkiSpITNbkiSpDTNbgJktSZKkpsxsPQLb3J9xd2GL2+6OyfwW+flj1o+7C2OxfrvJ/W/ov3v/y8bdhbH4lxe/Z9xdGIsnffEPxt2FrV5wztaUyfxLKkmS2iujLXAYUZIkqSkzW5IkqQmHETtmtiRJkhoysyVJkkav8NEPPTNbkiRJDZnZkiRJTWQyn6LzMAZbkiSpDYcRAYcRJUmSmjKzJUmSmvDRDx0zW5IkaSIk2SXJpUlu7D8unKXduiQr+2XZQPkTk3w1yU1J/iHJdsOc12BLkiSNXtG9rmfUy+Z5A/CZqjoA+Ey/PZN7q2pxvzxvoPwvgXdW1f7AD4CXDHNSgy1JktREavTLZjoaOK9fPw84ZuhrSQI8C7jwke5vsCVJkibFblV1R7/+b8Bus7TbIcmKJF9Jckxf9kvAXVV1f7+9GthjmJM6QV6SJLXRZoL8oiQrBraXVtXSqY0knwZ+eYb9Tn1I16oqmTVXtndVrUmyL/DZJKuAH25qhw22JEnSfHJnVS2ZrbKqjpitLsl3kuxeVXck2R347izHWNN/vCXJZcDTgY8COyfZts9u7QmsGabDDiNKkqSRC3NyztYy4IR+/QTg4of1O1mYZPt+fRHwK8B1VVXA54BjN7T/TAy2JEnS6LW4E3Hz70Y8DTgyyY3AEf02SZYkObtv82RgRZKr6IKr06rqur7u9cBrktxEN4frvcOc1GFESZI0Earq+8CzZyhfAby0X78cOGiW/W8BDn2k5zXYkiRJTfgE+Y7DiJIkSQ2Z2ZIkSW2Y2QLMbEmSJDVlZkuSJDXhnK2OwZYkSRq9AtYbbYHDiJIkSU2NLdhKUkk+OLC9bZLvJfnkQNkxSa5Ocn2SVQMvgyTJuUnWDD7lNcltSQ5KsrJf1ia5tV//dJJ9klwzrR9vSXLKFrhkSZImSzVY5qFxDiP+BDgwyY5VdS9wJAPvGEpyMHA6cGRV3ZrkicClSW6pqqv7ZuuAPwTeM7VfVa0CFvfHOBf4ZFVd2G/v0/qiJEmSBo17GPES4Hf69eOB8wfqTgH+d1XdCtB/fDvwuoE27wJencS5Z5IkzTFz8N2IYzHuYOsC4LgkOwBPA746UPdU4Mpp7Vf05VO+DXwR+INHcM79BoYZVwInP+JeS5KkjZt770Yci7FmhKrq6n5o73i6LNemeDvdW7f/acj2N1fV4qmNJG+ZrWGSk4CTALbdeeEmdk+SJE2ycWe2AJbRzc06f1r5dcAh08oOAa4dLKiqG4GVwAtG3bGqWlpVS6pqyTY7/cKoDy9J0lbNYcTOXJjrdA5wV1WtSnL4QPnpwEeSfLaqbuszYH8KHDvDMd7G8JktSZKkLWbswVZVrQbOmKF8ZZLXA59I8ijg58D/qKqVM7S9NsnXgWe07q8kSRrCPH5Uw6iNLdiqqp1mKLsMuGxg+yLgoln2P3Ha9vOHaHMbcOC0srcM2WVJkjSkAJmnE9pHbS7M2ZIkSdpqjX0YUZIkbaXWj7sDc4OZLUmSpIbMbEmSpCacs9UxsyVJktSQmS1JkjR6PvrhAQZbkiSpgfn7LsNRcxhRkiSpITNbkiSpifn6LsNRM7MlSZLUkJktSZLUhnO2AIMtSZLUQkF8gjzgMKIkSVJTZrYkSVIbDiMCZrYkSZKaMrMlSZLaMLEFGGxJkqRGfBF1x2FESZKkhsxsSZKkNsxsAWa2JEmSmjKzJUmSRq8AH2oKmNmSJElqysyWJEkauVDejdgz2JIkSW0YbAEOI0qSJDVlZkuSJLVhZgsw2BpeYN0Ok3dbxa5XZtxdGIt/O2zcPRiT9ZP59QbY9seTee1P+uIfjLsLY/HNX/3AuLswFgvG3YEJZbAlSZJGz0c/PMBgS5IkNeHdiB0nyEuSJDVksCVJktqoGv2yGZLskuTSJDf2HxfO0OY3k6wcWH6a5Ji+7twktw7ULR7mvAZbkiRpUrwB+ExVHQB8pt9+iKr6XFUtrqrFwLOAe4D/O9DkdVP1VbVymJMabEmSpAYaZLU2fw7Y0cB5/fp5wDEbaX8s8KmqumdzTmqwJUmSRq9oFWwtSrJiYDnpEfRqt6q6o1//N2C3jbQ/Djh/Wtnbklyd5J1Jth/mpN6NKEmS5pM7q2rJbJVJPg388gxVpw5uVFUlmTVVlmR34CBg+UDxG+mCtO2ApcDrgbdurMMGW5IkqY0xPGerqo6YrS7Jd5LsXlV39MHUdzdwqBcAH6uqnw8ceyor9rMk7wNOGaZPDiNKkqRJsQw4oV8/Abh4A22PZ9oQYh+gkSR0872uGeakZrYkSVITc/ChpqcB/5jkJcC36LJXJFkCnFxVL+239wH2Av552v4fSrIrEGAlcPIwJzXYkiRJE6Gqvg88e4byFcBLB7ZvA/aYod2zNuW8BluSJKmNuZfZGguDLUmSNHoFrDfYAifIS5IkNWVmS5IkNTCSJ75vFcxsSZIkNWRmS5IktWFmCzDYkiRJrRhsAQ4jSpIkNWVmS5IkjZ6PfniAmS1JkqSGzGxJkqQGCmr9uDsxJxhsSZKkNpwgDziMKEmS1JSZLUmSNHpOkH/ARjNbSS7fSP3d07ZPTPLujexzeJJPDtfFjfbvLUlOGTj34wfqbkuyaBTnkSRJ2hQbDbaq6rAt0ZERORF4/MYaSZKkLaBq9Ms8NExm6+7+4+5JPp9kZZJrkvzaEPuem+SMJJcnuSXJsQPVOyW5MMk3k3woSfp9Dknyz0muTLI8ye59+X9LckWSq5J8NMmjp53rWGAJ8KG+jzv2Va9M8vUkq5I8Kck2SW5Msmu/3zZJbpraliRJGqVHMkH+94DlVbUYOBhYOeR+uwO/CjwXOG2g/OnAq4CnAPsCv5LkUcDfAcdW1SHAOcDb+vYXVdV/qKqDgeuBlwyepKouBFYAL6qqxVV1b191Z1U9A3gPcEpVrQc+CLyorz8CuKqqvjfk9UiSpGGY2QIe2QT5K4Bz+oDo41W1cgNtBz8bH+8DnOuS7DZQ/rWqWg2QZCWwD3AXcCBwaZ/oWgDc0bc/MMlfADsDOwHLh+z3Rf3HK4Hn9+vnABcD7wL+EHjfTDsmOQk4CWDBwp2HPJ0kSeqeszU/g6NRGzqzVVWfB34dWAOcm+TFfdW9SbYbaLoLcOfA9s8G1jNL+Tq6wC/AtX1manFVHVRVv9W3ORd4RVUdBPwZsMOQXZ86z9Q5qKrbge8keRZwKPCpmXasqqVVtaSqlizYaachTydJkvSgoYOtJHsD36mq/wOcDTyjr/pn4Pf7NjsCLwA+t4n9uQHYNckz++M9KslT+7rHAHf0mbUXzbL/j/t2wzibbjjxI1W1bhP7K0mSZlLA+vWjX+ahRzJn63DgqiTfAF4I/G1f/ifA8/uhwK/QBS+f35TOVNV9wLHAXya5im5e2NTdkP8L+CrwJeCbsxziXOCsaRPkZ7OMbjhyxiFESZKkUdjonK2q2qn/eB5w3gz1a+gmv8+074mzHOsy4LKB8lcMrK+kG66cfqz30E1yn17+loH1jwIfHajeZ6BuBV3AOOVguonxswVukiRpczhnC5jQJ8gneQPwMmYfjpQkSZvLYAuY0HcjVtVpVbV3VX1x3H2RJElbt4nMbEmSpNbKdyP2JjKzJUmStKWY2ZIkSaNX0D3TXAZbkiSpDYcRAYcRJUmSmjKzJUmS2vDRD4CZLUmSpKbMbEmSpNGrmrfvMhw1M1uSJEkNmdmSJEltOGcLMNiSJEmNlMOIgMOIkiRJTZnZkiRJDZTDiD0zW5IkSQ2Z2ZIkSaNX+LqensGWJElqwxdRAw4jSpIkNWVmS5IkjVwB5TAiYGZLkiSpKTNbkiRp9Kqcs9UzsyVJkpqo9TXyZXMk+S9Jrk2yPsmSDbQ7KskNSW5K8oaB8icm+Wpf/g9JthvmvAZbkiRpUlwDPB/4/GwNkiwAzgSeAzwFOD7JU/rqvwTeWVX7Az8AXjLMSQ22JElSG7V+9MvmdKfq+qq6YSPNDgVuqqpbquo+4ALg6CQBngVc2Lc7DzhmmPMabEmSJD1oD+D2ge3VfdkvAXdV1f3TyjfKCfJDuu/21Xd+65Wv+9aYTr8IuHMcJx7XBffGdt1cMJazThnfdY+X1z1ZxnrdC8Z14s44r33vLXWiH/OD5Z+uCxc1OPQOSVYMbC+tqqVTG0k+DfzyDPudWlUXN+jPRhlsDamqdh3XuZOsqKpZJ/JtrbzuyeJ1T5ZJvW6YnGuvqqPGdN4jNvMQa4C9Brb37Mu+D+ycZNs+uzVVvlEOI0qSJD3oCuCA/s7D7YDjgGVVVcDngGP7dicAQ2XKDLYkSdJESPL/JlkNPBP4pyTL+/LHJ7kEoM9avQJYDlwP/GNVXdsf4vXAa5LcRDeH673DnNdhxPlh6cabbJW87snidU+WSb1umOxrH6uq+hjwsRnK/xX4TwPblwCXzNDuFrq7FR+RdFkxSZIkteAwoiRJUkMGW5IkSQ0ZbEmSJDVksCVJktSQwZYkSVJDBluSJEkNGWxJkiQ19P8DjsDDCNe8TPYAAAAASUVORK5CYII=\n",
"text/plain": [
"
"
]
},
"metadata": {
"needs_background": "light"
},
"output_type": "display_data"
}
],
"source": [
"# Visualize correlations in our merged dataframe...\n",
"print(AQbyWeather.selectedScenario.getSummary())\n",
"correlations = merged_df.corr()\n",
"fig = plt.figure(figsize=(10, 8))\n",
"ax = fig.add_subplot(111)\n",
"cax = ax.matshow(correlations, vmin=-1, vmax=1)\n",
"fig.colorbar(cax)\n",
"ticks = np.arange(0, len(merged_df.columns), 1)\n",
"ax.set_xticks(ticks)\n",
"ax.set_yticks(ticks)\n",
"ax.set_xticklabels(merged_df.columns)\n",
"ax.set_yticklabels(merged_df.columns)\n",
"plt.show()"
]
},
{
"cell_type": "markdown",
"id": "219a6c62-70eb-41ad-ae88-5ccd27f28aab",
"metadata": {},
"source": [
"## Begin ML on Merged Dataframe...\n",
"In order to build, validate, and test our ML model, we'll begin by creating three dataframes:\n",
"1. **train_df:** This is split from the full merged_df data as the larger chunk and used for training.\n",
"2. **validate_df:** This is split from the full merged_df data as the smaller chunk and used for validation.\n",
"3. **test_df:** This is the validation dataframe, but with the true target label values removed to enable model testing."
]
},
{
"cell_type": "code",
"execution_count": 11,
"id": "66bce085-f0ac-418f-b3e9-5e1da7cefbd9",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Scenario: bakersfield_pm25 => Particulate Matter < 2.5 micrometers (pm25) with UnhealthyThreshold > 12.0 µg/m³\n",
"Number of training samples: 1463\n",
"Number of validation samples: 721\n"
]
}
],
"source": [
"# Additional import statements for autogluon+sklearn and split out train_df + validate_df data...\n",
"print(AQbyWeather.selectedScenario.getSummary())\n",
"from autogluon.tabular import TabularPredictor\n",
"from sklearn.model_selection import train_test_split\n",
"from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay\n",
"train_df, validate_df = train_test_split(merged_df, test_size=0.33, random_state=1)\n",
"print('Number of training samples:', len(train_df))\n",
"print('Number of validation samples:', len(validate_df))"
]
},
{
"cell_type": "code",
"execution_count": 12,
"id": "cff0133a-7dfe-4187-8dda-026d0695ec92",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Scenario: bakersfield_pm25 => Particulate Matter < 2.5 micrometers (pm25) with UnhealthyThreshold > 12.0 µg/m³\n"
]
},
{
"data": {
"text/html": [
"
\n",
"\n",
"
\n",
" \n",
"
\n",
"
\n",
"
DEWP
\n",
"
WDSP
\n",
"
MAX
\n",
"
MIN
\n",
"
PRCP
\n",
"
MONTH
\n",
"
\n",
" \n",
" \n",
"
\n",
"
241
\n",
"
41.9
\n",
"
4.7
\n",
"
89.1
\n",
"
55.0
\n",
"
0.00
\n",
"
11
\n",
"
\n",
"
\n",
"
994
\n",
"
40.8
\n",
"
2.5
\n",
"
62.1
\n",
"
37.9
\n",
"
0.00
\n",
"
1
\n",
"
\n",
"
\n",
"
1820
\n",
"
42.2
\n",
"
5.5
\n",
"
90.0
\n",
"
63.0
\n",
"
0.00
\n",
"
5
\n",
"
\n",
"
\n",
"
1442
\n",
"
50.0
\n",
"
4.3
\n",
"
73.9
\n",
"
52.0
\n",
"
0.06
\n",
"
4
\n",
"
\n",
"
\n",
"
596
\n",
"
39.7
\n",
"
2.7
\n",
"
66.0
\n",
"
39.9
\n",
"
0.00
\n",
"
12
\n",
"
\n",
"
\n",
"
...
\n",
"
...
\n",
"
...
\n",
"
...
\n",
"
...
\n",
"
...
\n",
"
...
\n",
"
\n",
"
\n",
"
849
\n",
"
53.0
\n",
"
5.5
\n",
"
93.0
\n",
"
64.0
\n",
"
0.00
\n",
"
8
\n",
"
\n",
"
\n",
"
208
\n",
"
37.2
\n",
"
5.6
\n",
"
91.9
\n",
"
57.9
\n",
"
0.00
\n",
"
10
\n",
"
\n",
"
\n",
"
288
\n",
"
37.1
\n",
"
2.8
\n",
"
66.0
\n",
"
36.0
\n",
"
0.00
\n",
"
12
\n",
"
\n",
"
\n",
"
1226
\n",
"
39.3
\n",
"
6.8
\n",
"
99.0
\n",
"
62.1
\n",
"
0.00
\n",
"
9
\n",
"
\n",
"
\n",
"
2129
\n",
"
42.3
\n",
"
5.9
\n",
"
73.9
\n",
"
42.1
\n",
"
0.00
\n",
"
4
\n",
"
\n",
" \n",
"
\n",
"
721 rows × 6 columns
\n",
"
"
],
"text/plain": [
" DEWP WDSP MAX MIN PRCP MONTH\n",
"241 41.9 4.7 89.1 55.0 0.00 11\n",
"994 40.8 2.5 62.1 37.9 0.00 1\n",
"1820 42.2 5.5 90.0 63.0 0.00 5\n",
"1442 50.0 4.3 73.9 52.0 0.06 4\n",
"596 39.7 2.7 66.0 39.9 0.00 12\n",
"... ... ... ... ... ... ...\n",
"849 53.0 5.5 93.0 64.0 0.00 8\n",
"208 37.2 5.6 91.9 57.9 0.00 10\n",
"288 37.1 2.8 66.0 36.0 0.00 12\n",
"1226 39.3 6.8 99.0 62.1 0.00 9\n",
"2129 42.3 5.9 73.9 42.1 0.00 4\n",
"\n",
"[721 rows x 6 columns]"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"# Create the test_df data and remove the target label column...\n",
"print(AQbyWeather.selectedScenario.getSummary())\n",
"test_df=validate_df.drop([AQbyWeather.mlTargetLabel], axis=1)\n",
"display(test_df)"
]
},
{
"cell_type": "markdown",
"id": "be7097cd-6b74-4824-8eec-06d526ee7f67",
"metadata": {},
"source": [
"## Run AutoGluon TabularPredictor.fit(...)\n",
"- Instantiate TabularPredictor with explicit values for label, eval_metric, and path.\n",
"- Call TabularPredictor.fit(...) using our train_df tabular data as the main input."
]
},
{
"cell_type": "code",
"execution_count": 13,
"id": "499df50a-239b-4173-a91f-f86941d69b15",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"'Scenario: bakersfield_pm25 => Particulate Matter < 2.5 micrometers (pm25) with UnhealthyThreshold > 12.0 µg/m³'"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"Warning: path already exists! This predictor may overwrite an existing predictor! path=\"AutogluonModels/aq_bakersfield_pm25_2016-2022/\"\n",
"Presets specified: ['best_quality']\n",
"Beginning AutoGluon training ...\n",
"AutoGluon will save models to \"AutogluonModels/aq_bakersfield_pm25_2016-2022/\"\n",
"AutoGluon Version: 0.4.2\n",
"Python Version: 3.9.13\n",
"Operating System: Linux\n",
"Train Data Rows: 1463\n",
"Train Data Columns: 6\n",
"Label Column: isUnhealthy\n",
"Preprocessing data ...\n",
"AutoGluon infers your prediction problem is: 'binary' (because only two unique label-values observed).\n",
"\t2 unique label values: [0, 1]\n",
"\tIf 'binary' is not the correct problem_type, please manually specify the problem_type parameter during predictor init (You may specify problem_type as one of: ['binary', 'multiclass', 'regression'])\n",
"Selected class <--> label mapping: class 1 = 1, class 0 = 0\n",
"Using Feature Generators to preprocess the data ...\n",
"Fitting AutoMLPipelineFeatureGenerator...\n",
"\tAvailable Memory: 15321.24 MB\n",
"\tTrain Data (Original) Memory Usage: 0.07 MB (0.0% of available memory)\n",
"\tInferring data type of each feature based on column values. Set feature_metadata_in to manually specify special dtypes of the features.\n",
"\tStage 1 Generators:\n",
"\t\tFitting AsTypeFeatureGenerator...\n",
"\tStage 2 Generators:\n",
"\t\tFitting FillNaFeatureGenerator...\n",
"\tStage 3 Generators:\n",
"\t\tFitting IdentityFeatureGenerator...\n",
"\tStage 4 Generators:\n",
"\t\tFitting DropUniqueFeatureGenerator...\n",
"\tTypes of features in original data (raw dtype, special dtypes):\n",
"\t\t('float', []) : 5 | ['DEWP', 'WDSP', 'MAX', 'MIN', 'PRCP']\n",
"\t\t('int', []) : 1 | ['MONTH']\n",
"\tTypes of features in processed data (raw dtype, special dtypes):\n",
"\t\t('float', []) : 5 | ['DEWP', 'WDSP', 'MAX', 'MIN', 'PRCP']\n",
"\t\t('int', []) : 1 | ['MONTH']\n",
"\t0.0s = Fit runtime\n",
"\t6 features in original data used to generate 6 features in processed data.\n",
"\tTrain Data (Processed) Memory Usage: 0.07 MB (0.0% of available memory)\n",
"Data preprocessing and feature engineering runtime = 0.04s ...\n",
"AutoGluon will gauge predictive performance using evaluation metric: 'accuracy'\n",
"\tTo change this, specify the eval_metric parameter of Predictor()\n",
"AutoGluon will fit 2 stack levels (L1 to L2) ...\n",
"Fitting 13 L1 models ...\n",
"Fitting model: KNeighborsUnif_BAG_L1 ...\n",
"\t0.7416\t = Validation score (accuracy)\n",
"\t0.0s\t = Training runtime\n",
"\t0.01s\t = Validation runtime\n",
"Fitting model: KNeighborsDist_BAG_L1 ...\n",
"\t0.7573\t = Validation score (accuracy)\n",
"\t0.0s\t = Training runtime\n",
"\t0.01s\t = Validation runtime\n",
"Fitting model: LightGBMXT_BAG_L1 ...\n",
"\tFitting 8 child models (S1F1 - S1F8) | Fitting with ParallelLocalFoldFittingStrategy\n",
"2022-06-14 22:38:13,323\tWARNING services.py:1856 -- WARNING: The object store is using /tmp instead of /dev/shm because /dev/shm has only 4294967296 bytes available. This will harm performance! You may be able to free up space by deleting files in /dev/shm. If you are inside a Docker container, you can increase /dev/shm size by passing '--shm-size=4.77gb' to 'docker run' (or add it to the run_options list in a Ray cluster config). Make sure to set this to more than 30% of available RAM.\n",
"\t0.8373\t = Validation score (accuracy)\n",
"\t5.38s\t = Training runtime\n",
"\t0.04s\t = Validation runtime\n",
"Fitting model: LightGBM_BAG_L1 ...\n",
"\tFitting 8 child models (S1F1 - S1F8) | Fitting with ParallelLocalFoldFittingStrategy\n",
"\t0.8182\t = Validation score (accuracy)\n",
"\t6.25s\t = Training runtime\n",
"\t0.03s\t = Validation runtime\n",
"Fitting model: RandomForestGini_BAG_L1 ...\n",
"\t0.812\t = Validation score (accuracy)\n",
"\t0.77s\t = Training runtime\n",
"\t0.16s\t = Validation runtime\n",
"Fitting model: RandomForestEntr_BAG_L1 ...\n",
"\t0.8059\t = Validation score (accuracy)\n",
"\t0.67s\t = Training runtime\n",
"\t0.15s\t = Validation runtime\n",
"Fitting model: CatBoost_BAG_L1 ...\n",
"\tFitting 8 child models (S1F1 - S1F8) | Fitting with ParallelLocalFoldFittingStrategy\n",
"\t0.8278\t = Validation score (accuracy)\n",
"\t6.0s\t = Training runtime\n",
"\t0.05s\t = Validation runtime\n",
"Fitting model: ExtraTreesGini_BAG_L1 ...\n",
"\t0.8141\t = Validation score (accuracy)\n",
"\t0.77s\t = Training runtime\n",
"\t0.16s\t = Validation runtime\n",
"Fitting model: ExtraTreesEntr_BAG_L1 ...\n",
"\t0.8072\t = Validation score (accuracy)\n",
"\t0.69s\t = Training runtime\n",
"\t0.16s\t = Validation runtime\n",
"Fitting model: NeuralNetFastAI_BAG_L1 ...\n",
"\tFitting 8 child models (S1F1 - S1F8) | Fitting with ParallelLocalFoldFittingStrategy\n",
"\t0.8018\t = Validation score (accuracy)\n",
"\t12.3s\t = Training runtime\n",
"\t0.12s\t = Validation runtime\n",
"Fitting model: XGBoost_BAG_L1 ...\n",
"\tFitting 8 child models (S1F1 - S1F8) | Fitting with ParallelLocalFoldFittingStrategy\n",
"\t0.8148\t = Validation score (accuracy)\n",
"\t5.06s\t = Training runtime\n",
"\t0.07s\t = Validation runtime\n",
"Fitting model: NeuralNetTorch_BAG_L1 ...\n",
"\tFitting 8 child models (S1F1 - S1F8) | Fitting with ParallelLocalFoldFittingStrategy\n",
"\t0.8291\t = Validation score (accuracy)\n",
"\t17.65s\t = Training runtime\n",
"\t0.11s\t = Validation runtime\n",
"Fitting model: LightGBMLarge_BAG_L1 ...\n",
"\tFitting 8 child models (S1F1 - S1F8) | Fitting with ParallelLocalFoldFittingStrategy\n",
"\t0.8195\t = Validation score (accuracy)\n",
"\t8.32s\t = Training runtime\n",
"\t0.05s\t = Validation runtime\n",
"Fitting model: WeightedEnsemble_L2 ...\n",
"\t0.8373\t = Validation score (accuracy)\n",
"\t0.98s\t = Training runtime\n",
"\t0.0s\t = Validation runtime\n",
"Fitting 11 L2 models ...\n",
"Fitting model: LightGBMXT_BAG_L2 ...\n",
"\tFitting 8 child models (S1F1 - S1F8) | Fitting with ParallelLocalFoldFittingStrategy\n",
"\t0.8401\t = Validation score (accuracy)\n",
"\t6.48s\t = Training runtime\n",
"\t0.04s\t = Validation runtime\n",
"Fitting model: LightGBM_BAG_L2 ...\n",
"\tFitting 8 child models (S1F1 - S1F8) | Fitting with ParallelLocalFoldFittingStrategy\n",
"\t0.8483\t = Validation score (accuracy)\n",
"\t7.34s\t = Training runtime\n",
"\t0.04s\t = Validation runtime\n",
"Fitting model: RandomForestGini_BAG_L2 ...\n",
"\t0.8346\t = Validation score (accuracy)\n",
"\t0.83s\t = Training runtime\n",
"\t0.15s\t = Validation runtime\n",
"Fitting model: RandomForestEntr_BAG_L2 ...\n",
"\t0.8325\t = Validation score (accuracy)\n",
"\t0.77s\t = Training runtime\n",
"\t0.15s\t = Validation runtime\n",
"Fitting model: CatBoost_BAG_L2 ...\n",
"\tFitting 8 child models (S1F1 - S1F8) | Fitting with ParallelLocalFoldFittingStrategy\n",
"\t0.8442\t = Validation score (accuracy)\n",
"\t11.68s\t = Training runtime\n",
"\t0.04s\t = Validation runtime\n",
"Fitting model: ExtraTreesGini_BAG_L2 ...\n",
"\t0.8189\t = Validation score (accuracy)\n",
"\t0.73s\t = Training runtime\n",
"\t0.16s\t = Validation runtime\n",
"Fitting model: ExtraTreesEntr_BAG_L2 ...\n",
"\t0.8257\t = Validation score (accuracy)\n",
"\t0.68s\t = Training runtime\n",
"\t0.16s\t = Validation runtime\n",
"Fitting model: NeuralNetFastAI_BAG_L2 ...\n",
"\tFitting 8 child models (S1F1 - S1F8) | Fitting with ParallelLocalFoldFittingStrategy\n",
"\t0.8325\t = Validation score (accuracy)\n",
"\t12.15s\t = Training runtime\n",
"\t0.23s\t = Validation runtime\n",
"Fitting model: XGBoost_BAG_L2 ...\n",
"\tFitting 8 child models (S1F1 - S1F8) | Fitting with ParallelLocalFoldFittingStrategy\n",
"\t0.8476\t = Validation score (accuracy)\n",
"\t6.37s\t = Training runtime\n",
"\t0.07s\t = Validation runtime\n",
"Fitting model: NeuralNetTorch_BAG_L2 ...\n",
"\tFitting 8 child models (S1F1 - S1F8) | Fitting with ParallelLocalFoldFittingStrategy\n",
"\t0.8284\t = Validation score (accuracy)\n",
"\t11.78s\t = Training runtime\n",
"\t0.13s\t = Validation runtime\n",
"Fitting model: LightGBMLarge_BAG_L2 ...\n",
"\tFitting 8 child models (S1F1 - S1F8) | Fitting with ParallelLocalFoldFittingStrategy\n",
"\t0.8401\t = Validation score (accuracy)\n",
"\t13.94s\t = Training runtime\n",
"\t0.11s\t = Validation runtime\n",
"Fitting model: WeightedEnsemble_L3 ...\n",
"\t0.8483\t = Validation score (accuracy)\n",
"\t0.93s\t = Training runtime\n",
"\t0.0s\t = Validation runtime\n",
"AutoGluon training complete, total runtime = 177.76s ... Best model: \"WeightedEnsemble_L3\"\n",
"TabularPredictor saved. To load, use: predictor = TabularPredictor.load(\"AutogluonModels/aq_bakersfield_pm25_2016-2022/\")\n"
]
},
{
"data": {
"text/plain": [
""
]
},
"execution_count": 13,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"# Use AutoGluon TabularPredictor to fit a model for our training data...\n",
"display(AQbyWeather.selectedScenario.getSummary()) #Using display for consistent/sequential output order.\n",
"predictor = TabularPredictor(label=AQbyWeather.mlTargetLabel, \n",
" eval_metric=AQbyWeather.mlEvalMetric, \n",
" path=AQbyWeather.selectedScenario.getModelPath())\n",
"predictor.fit(train_data=train_df, time_limit=AQbyWeather.mlTimeLimitSecs, verbosity=2, presets='best_quality')"
]
},
{
"cell_type": "markdown",
"id": "021921b8-6133-46b3-aed5-8e733e627aab",
"metadata": {},
"source": [
"## Evaluating Our Autogluon Model...\n",
"- Use TabularPredictor.feature_importance(...) with our validation_df to get feature importance data.\\\n",
" (this data is displayed further below)\n",
"- Use TabularPredictor.leaderboard(...) with our validation_df to get individual model performance data.\\\n",
" (this data is displayed further below)\n",
"- Display TabularPredictor.evaluate(...) to output various model quality characteristics."
]
},
{
"cell_type": "code",
"execution_count": 14,
"id": "ace98288-c321-4ea9-ac20-b163d9753af1",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"'Scenario: bakersfield_pm25 => Particulate Matter < 2.5 micrometers (pm25) with UnhealthyThreshold > 12.0 µg/m³'"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"Computing feature importance via permutation shuffling for 6 features using 721 rows with 5 shuffle sets...\n",
"\t63.95s\t= Expected runtime (12.79s per shuffle set)\n",
"\t15.71s\t= Actual runtime (Completed 5 of 5 shuffle sets)\n",
"Evaluation: accuracy on test data: 0.7947295423023578\n",
"Evaluations on test data:\n",
"{\n",
" \"accuracy\": 0.7947295423023578,\n",
" \"balanced_accuracy\": 0.7923707440100882,\n",
" \"mcc\": 0.5820230438897135,\n",
" \"roc_auc\": 0.874984237074401,\n",
" \"f1\": 0.7620578778135049,\n",
" \"precision\": 0.7476340694006309,\n",
" \"recall\": 0.7770491803278688\n",
"}\n"
]
}
],
"source": [
"# Get dataframes for feature importance + model leaderboard AND get+display model evaluation...\n",
"display(AQbyWeather.selectedScenario.getSummary()) #Using display for consistent/sequential output order.\n",
"featureimp_df = predictor.feature_importance(validate_df)\n",
"leaderboard_df = predictor.leaderboard(validate_df, silent=True)\n",
"modelEvaluation = predictor.evaluate(validate_df, auxiliary_metrics=True)"
]
},
{
"cell_type": "code",
"execution_count": 15,
"id": "6d9a3a88-aab0-49f8-a6d4-ba20474bfd0b",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Scenario: bakersfield_pm25 => Particulate Matter < 2.5 micrometers (pm25) with UnhealthyThreshold > 12.0 µg/m³\n"
]
},
{
"data": {
"text/html": [
"
"
]
},
"metadata": {
"needs_background": "light"
},
"output_type": "display_data"
}
],
"source": [
"# View and Plot Feature Importance... (this various from Scenario to Scenario)\n",
"print(AQbyWeather.selectedScenario.getSummary())\n",
"display(featureimp_df)\n",
"featureimp_df.drop(columns=[\"n\"]).plot(kind=\"bar\", figsize=(12, 4), xlabel=\"feature\")"
]
},
{
"cell_type": "markdown",
"id": "7456a6b0-2236-47db-b6a8-dc11f7b3d35c",
"metadata": {},
"source": [
"## Using + Testing Our Model...\n",
"Now that we've built our Autogluon ML Model, we can use our test_df dataframe to make actual target label predictions and then compare those predictions against the known true label values to determine how the model is performing across each binary classification { 0=OKAY, 1=Unhealthy }. Further below, we'll plot a _Confusion Matrix_ and the **Summary** explains how to interpret results and covers **Key Learnings**."
]
},
{
"cell_type": "code",
"execution_count": 17,
"id": "af1ec4ac-5e75-406b-8fe1-980ecf8d6986",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Scenario: bakersfield_pm25 => Particulate Matter < 2.5 micrometers (pm25) with UnhealthyThreshold > 12.0 µg/m³\n"
]
},
{
"data": {
"text/plain": [
"241 1\n",
"994 1\n",
"1820 0\n",
"1442 0\n",
"596 1\n",
" ..\n",
"849 0\n",
"208 1\n",
"288 1\n",
"1226 0\n",
"2129 0\n",
"Name: isUnhealthy, Length: 721, dtype: int64"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"# Load + Use Our Model (this line is unnecessary, but shows how to load a built model)...\n",
"predictor = TabularPredictor.load(AQbyWeather.selectedScenario.getModelPath())\n",
"\n",
"# Make Predictions, which are saved to an array: y_pred\n",
"print(AQbyWeather.selectedScenario.getSummary())\n",
"y_pred = predictor.predict(test_df)\n",
"display(y_pred)"
]
},
{
"cell_type": "code",
"execution_count": 18,
"id": "efa500e0-4d0b-47be-95fd-6b45a5dea0f4",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Scenario: bakersfield_pm25 => Particulate Matter < 2.5 micrometers (pm25) with UnhealthyThreshold > 12.0 µg/m³\n"
]
},
{
"data": {
"text/plain": [
"241 1\n",
"994 1\n",
"1820 0\n",
"1442 0\n",
"596 1\n",
" ..\n",
"849 1\n",
"208 1\n",
"288 1\n",
"1226 0\n",
"2129 0\n",
"Name: isUnhealthy, Length: 721, dtype: int64"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"# Get true label values as an array: y_true\n",
"print(AQbyWeather.selectedScenario.getSummary())\n",
"y_true = validate_df[AQbyWeather.mlTargetLabel]\n",
"display(y_true)"
]
},
{
"cell_type": "code",
"execution_count": 19,
"id": "43d7e462-2462-4fb1-9b6c-c7cd6cd56554",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Scenario: bakersfield_pm25 => Particulate Matter < 2.5 micrometers (pm25) with UnhealthyThreshold > 12.0 µg/m³\n",
"True Negatives (TN): 336 of 416 => 80.77%\n",
"True Positives (TP): 237 of 305 => 77.7%\n",
"False Negatives (FN): 68 of 305 => 22.3%\n",
"False Positives (FP): 80 of 416 => 19.23%\n"
]
},
{
"data": {
"text/plain": [
""
]
},
"execution_count": 19,
"metadata": {},
"output_type": "execute_result"
},
{
"data": {
"image/png": "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\n",
"text/plain": [
"
"
]
},
"metadata": {
"needs_background": "light"
},
"output_type": "display_data"
}
],
"source": [
"# Get Confusion Matrix (CM), Get Additional CM Data, View CM...\n",
"# Learn More: https://towardsdatascience.com/confusion-matrix-what-is-it-e859e1bbecdc\n",
"# https://scikit-learn.org/stable/modules/generated/sklearn.metrics.confusion_matrix.html\n",
"print(AQbyWeather.selectedScenario.getSummary())\n",
"cm = confusion_matrix(y_true, y_pred)\n",
"\n",
"# Print Confusion Matrix data...\n",
"cmData = AQbyWeather.getConfusionMatrixData(cm)\n",
"print(cmData.TN_Output)\n",
"print(cmData.TP_Output)\n",
"print(cmData.FN_Output)\n",
"print(cmData.FP_Output)\n",
"\n",
"# Plot Confusion Matrix...\n",
"cmd = ConfusionMatrixDisplay(confusion_matrix=cm)\n",
"fig, ax = plt.subplots(figsize=(8, 6))\n",
"cmd.plot(ax=ax)"
]
},
{
"cell_type": "markdown",
"id": "152d9203-6987-4fbb-a33e-acef22cfb993",
"metadata": {},
"source": [
"## SUMMARY...\n",
"This demo showed how to use a SageMaker Studio Lab Jupyter Notebook to connect to open-source Amazon Sustainability Data Initiative (ASDI) datasets both via Amazon S3 (for NOAA weather data) and via HTTPS API (for OpenAQ air quality averages). The NOAA weather data and OpenAQ measurement averages are merged into a single table with irrelevant features dropped. This table is then used to build an Autogluon [TabularPredictor](https://auto.gluon.ai/stable/tutorials/tabular_prediction/index.html) ML model to predict a binary classification { 0=OKAY, 1=Unhealthy } based on weather features.\n",
"\n",
"### Key Learnings\n",
"- The relationship between weather and air quality varies geographically and this was confirmed.\n",
" - Feature Importance review shows differing factors in models built for different locations.\n",
" - Academic research indicates the relationship can change by season and an engineered “MONTH” feature was important for most models.\n",
" - An engineered “DAYOFWEEK” feature was expected to be important, but observed correlation was minimal and removing the feature improved most models.\n",
"- Predicting a target measurement was infeasible, but predicting a binary “OKAY” vs “unhealthy” classification based on threshold values yielded decent results (~75-90% accuracy metrics).\n",
"- For more on production-level air quality predictions, research how climatology and deeper statistical analysis are merged into 3D models that incorporate emissions models, meteorological models, and chemical models.\n",
"\n",
"### Confusion Matrix and Saving Final Results\n",
"The Confusion Matrix above plots the relationships between:\n",
"- True Positives (1=>1; see bottom-right)\n",
"- True Negatives (0=>0; see top-left)\n",
"- False Positives (1=>0; see top-right)\n",
"- False Negatives (0=>1; see bottom-left)\n",
"\n",
"_The validation dataframe with predictions appended as the final column are saved below as a CSV for additional analysis._"
]
},
{
"cell_type": "code",
"execution_count": 20,
"id": "c1c7d5e7-0d51-4170-b1dd-ba09b97632bf",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Scenario: bakersfield_pm25 => Particulate Matter < 2.5 micrometers (pm25) with UnhealthyThreshold > 12.0 µg/m³\n",
"Results saved to dataRESULTS_bakersfield_pm25_2016-2022.csv. DONE.\n"
]
},
{
"data": {
"text/html": [
"