<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ forecasting - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ Browse thousands of programming tutorials written by experts. Learn Web Development, Data Science, DevOps, Security, and get developer career advice. ]]>
        </description>
        <link>https://www.freecodecamp.org/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ forecasting - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Mon, 24 Aug 2026 07:41:11 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/tag/forecasting/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ How I Built My Own Forecasting Tool Using a Weather API ]]>
                </title>
                <description>
                    <![CDATA[ By Anton Lawrence Abrupt weather and climate change are things that everybody is dealing with. In fact, the vast majority of the global population relies on accurate, real-time weather data and forecasts to make informed decisions.    This has increa... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-i-built-my-own-forecasting-tool-using-a-weather-api/</link>
                <guid isPermaLink="false">66d45d978812486a37369c5d</guid>
                
                    <category>
                        <![CDATA[ api ]]>
                    </category>
                
                    <category>
                        <![CDATA[ forecasting ]]>
                    </category>
                
                    <category>
                        <![CDATA[ weather ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ freeCodeCamp ]]>
                </dc:creator>
                <pubDate>Mon, 11 May 2020 17:50:21 +0000</pubDate>
                <media:content url="https://cdn-media-2.freecodecamp.org/w1280/5f9c9b25740569d1a4ca29f2.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>By Anton Lawrence</p>
<p>Abrupt weather and climate change are things that everybody is dealing with. In fact, the vast majority of the global population relies on accurate, real-time <a target="_blank" href="https://www.climate.gov/maps-data/primer/processing-climate-data">weather data and forecasts to make informed decisions</a>.   </p>
<p>This has increased the importance of reliable Android and iOS weather apps. In this article, we will show you how to create a simple forecasting tool using NodeJS and a weather API.  </p>
<p>But before that, let’s go over the importance of weather apps.</p>
<h1 id="heading-why-do-we-need-weather-apps">Why Do We Need Weather Apps?</h1>
<p>A feature-rich <a target="_blank" href="https://en.wikipedia.org/wiki/Weather_forecasting">weather forecasting</a> app can provide great value to various industries. Some noteworthy benefits of weather applications include:</p>
<ul>
<li>Providing immediate access to local weather conditions and upcoming forecast, thus saving you time.</li>
<li>Providing real-time notifications about prevailing and expected weather conditions.</li>
<li>Helping governments and local administrations prepare for natural disasters and save lives.</li>
<li>Helping farmers take preventive measures.</li>
<li>Facilitating the global travel and tourism industry.</li>
<li>Providing clear weather forecasts which are crucial in the aviation and logistics industry.</li>
</ul>
<h1 id="heading-what-you-need-to-build-a-weather-app">What You Need to Build a Weather App</h1>
<p>Here are some of the things you will need to build a weather app successfully:  </p>
<ul>
<li>Familiarity working with JavaScript (Node.js)</li>
<li>A text editor such as Notepad or an IDE. My favorite is Visual Studio.</li>
<li>Access to a reliable weather API such as ClimaCell</li>
<li>Access to a map service</li>
<li>Knowledge of HTML, CSS, and Bootstrap</li>
</ul>
<p>Once you have these ready, you’re good to go.</p>
<h1 id="heading-overview-of-the-climacell-weather-api">Overview of the ClimaCell Weather API</h1>
<p><img src="https://paper-attachments.dropbox.com/s_09D7754335BDD40A8ECD55F1187847147F5C3FBE3BBD081087C52D1E8CDCDF32_1589167731846_climacell+api+weather.jpg" alt="Image" width="600" height="400" loading="lazy"></p>
<p>ClimaCell is a popular weather provider that offers hyper-accurate <a target="_blank" href="https://www.climate.gov/maps-data/dataset/past-weather-zip-code-data-table">historical weather data</a> as well as forecasts through an easy to consume API.</p>
<h1 id="heading-the-building-process">The Building Process</h1>
<p>In this section, I will show you how I created a forecasting app where a user enters their city or any other location by name and fetches weather data from the ClimaCell API. The API responds to the request by returning data, which is then displayed to the user.</p>
<h2 id="heading-install-nodejs-and-create-a-new-project">Install NodeJS and Create a New Project</h2>
<p>For this project, we will use Node.js — one of the most popular run-time environments for JavaScript. Node.js helps developers create quick web applications. It has a wide range of libraries and modules for creating advanced web applications.  </p>
<p>If you do not have Node.js on your device, you can install it from the <a target="_blank" href="https://nodejs.org/en/">official website</a>.<br>Once installed, we use this command to initialize npm - the default packet manager used by Node.js.  </p>
<p><code>$ npm init</code> </p>
<p>This creates our project, so you will be prompted to enter a few details such as package name, description, Git repository, and more.  </p>
<p>Next, we install the modules required to run our project. To generate a Node.js app skeleton, we use express - a framework for building Node.js web applications.  </p>
<p><code>$ npm install express</code>  </p>
<p>Installing the express framework helps you run the server, handle client requests, and connect the right HTML template with a response.    </p>
<p>Next, we will also install unirest - a simple yet powerful solution that allows you to request a library.   </p>
<p>It will help us make requests to the ClimaCell API and handle the responses.   </p>
<p>Use this command:  </p>
<p><code>npm install unirest</code>  </p>
<p>At this point, we’ve installed the necessary modules, and the project is ready.<br>Next, we generate a weather app using the express generator tool. On the command line, type this:  </p>
<p><code>express --view=pug weather-app-nodejs</code>  </p>
<p>You should now have a view like this on the command line:</p>
<p><img src="https://paper-attachments.dropbox.com/s_09D7754335BDD40A8ECD55F1187847147F5C3FBE3BBD081087C52D1E8CDCDF32_1589167675672_create+weather+app.png" alt="Image" width="600" height="400" loading="lazy"></p>
<h2 id="heading-get-the-climacell-weather-api">Get the ClimaCell Weather API</h2>
<p>To get access to the ClimaCell API, you will need to sign up for an account on their page. </p>
<p><img src="https://paper-attachments.dropbox.com/s_09D7754335BDD40A8ECD55F1187847147F5C3FBE3BBD081087C52D1E8CDCDF32_1589187768233_CLIMACELL+DASHBOARD.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p>Once you create an account, sign in to their Microweather API dashboard which looks like this:  </p>
<p><img src="https://paper-attachments.dropbox.com/s_09D7754335BDD40A8ECD55F1187847147F5C3FBE3BBD081087C52D1E8CDCDF32_1589188690033_dashboard.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p>On the dashboard, click on references to check the API endpoints. As you can see, the ClimaCell API has a number of endpoints including the short term forecast, hourly forecast, real time data, and more.</p>
<p><img src="https://paper-attachments.dropbox.com/s_09D7754335BDD40A8ECD55F1187847147F5C3FBE3BBD081087C52D1E8CDCDF32_1589191651855_API+endpoints.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p>Worth a mention is that each endpoint has its own code snippet. For example, here is the Node.js code snippet for getting real time weather data.</p>
<p><img src="https://paper-attachments.dropbox.com/s_09D7754335BDD40A8ECD55F1187847147F5C3FBE3BBD081087C52D1E8CDCDF32_1589192780403_carbon.png" alt="Image" width="600" height="400" loading="lazy">
<em>[Raw](https://carbon.now.sh/embed?bg=rgba(0%2C0%2C0%2C1)&amp;t=blackboard&amp;wt=none&amp;l=javascript&amp;ds=true&amp;dsyoff=20px&amp;dsblur=68px&amp;wc=true&amp;wa=true&amp;pv=56px&amp;ph=56px&amp;ln=false&amp;fl=1&amp;fm=Hack&amp;fs=14px&amp;lh=133%25&amp;si=false&amp;es=1x&amp;wm=false&amp;code=router.post(%27%252Fweather%27%252C%2520function(req%252C%2520res%252C%2520next)%257B%250A%250A%2520%2520let%2520city%2520%253D%2520req.body.city%253B%250A%250A%2520%2520url%2520%253D%2520url%252Bcity%252B%2522%2526%2522%252BappId%253B%250A%250A%2520request(url%252C%2520function%2520(error%252C%2520response%252C%2520body)%2520%257B%250A%250A%2520%2520%2520%2520%2520%2520body%2520%253D%2520JSON.parse(body)%253B%250A%250A%2520%2520%2520%2520%2520%2520if(error%2520%2526%2526%2520response.statusCode%2520!%253D%2520200)%257B%250A%250A%2520%2520%2520%2520%2520%2520%2520%2520throw%2520error%253B%250A%250A%2520%2520%2520%2520%2520%2520%257D%250A%250A%2520%2520%2520%2520let%2520country%2520%253D%2520(body.sys.country)%2520%253F%2520body.sys.country%2520%253A%2520%27%27%2520%253B%250A%250A%2520%2520%2520%2520let%2520var%2520request%2520%253D%2520require(%2522request%2522)%253B%250A%250Avar%2520options%2520%253D%2520%257B%250A%2520%2520method%253A%2520%27GET%27%252C%250A%2520%2520url%253A%2520%27https%253A%252F%252Fapi.climacell.co%252Fv3%252Fweather%252Frealtime%27%252C%250A%2520%2520qs%253A%2520%257Bapikey%253A%2520%279efRx8KrGKa6ME0ORWIJz7TaNjuRQAvb%27%257D%250A%257D%253B%250A%250Arequest(options%252C%2520function%2520(error%252C%2520response%252C%2520body)%2520%257B%250A%2520%2520if%2520(error)%2520throw%2520new%2520Error(error)%253B%250A%250A%2520%2520console.log(body)%253B%250A%257D)%253B%2520%253D%2520%2522For%2520city%2520%2522%252Bcity%252B%27%252C%2520country%2520%27%252Bcountry%253B%250A%250A%2520%2520%2520%2520res.render(%27index%27%252C%2520%257Bbody%2520%253A%2520body%252C%2520forecast%253A%2520forecast%257D)%253B%250A%250A%2520%2520%2520%257D)%253B%250A%250A%257D)%253B" rel="noreferrer nofollow noopener)</em></p>
<h2 id="heading-modifying-the-application">Modifying the application</h2>
<p>To call the ClimaCell API, we need to first edit some files. Here, you can use Notepad or open the project directory in your IDE for easier editing. It should appear as shown:</p>
<p><img src="https://paper-attachments.dropbox.com/s_09D7754335BDD40A8ECD55F1187847147F5C3FBE3BBD081087C52D1E8CDCDF32_1589180215929_repo.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p>We start modifying our files by adding bootstrap to layout.pug. Open the views directory and insert this snippet to the file.</p>
<p><img src="https://paper-attachments.dropbox.com/s_09D7754335BDD40A8ECD55F1187847147F5C3FBE3BBD081087C52D1E8CDCDF32_1589170180082_carbon+1.png" alt="Image" width="600" height="400" loading="lazy">
<em>[Raw](https://carbon.now.sh/embed?bg=rgba(0%2C0%2C0%2C1)&amp;t=blackboard&amp;wt=none&amp;l=javascript&amp;ds=true&amp;dsyoff=20px&amp;dsblur=68px&amp;wc=true&amp;wa=true&amp;pv=56px&amp;ph=56px&amp;ln=false&amp;fl=1&amp;fm=Hack&amp;fs=14px&amp;lh=133%25&amp;si=false&amp;es=1x&amp;wm=false&amp;code=doctype%2520html%250Ahtml(lang%253D%2522en%2522)%250A%2520%2520head%250A%2520%2520%2520%2520title%2520Weather%2520Forecast%2520Tool%2520using%2520Climacell%2520API%250A%2520%2520%2520%2520meta(charset%253D%2522utf-8%2522)%250A%2520%2520%2520%2520meta(name%253D%2522viewport%2522%2520content%253D%2522width%253Ddevice-width%252C%2520initial-scale%253D1%2522)%250A%2520%2520%2520%2520link(rel%253D%2522stylesheet%2522%2520href%253D%2522https%253A%252F%252Fmaxcdn.bootstrapcdn.com%252Fbootstrap%252F3.3.7%252Fcss%252Fbootstrap.min.css%2522)%250A%2520%2520%2520%2520script(src%253D%2522https%253A%252F%252Fajax.googleapis.com%252Fajax%252Flibs%252Fjquery%252F3.3.1%252Fjquery.min.js%2522)%250A%2520%2520%2520%2520script(src%253D%2522https%253A%252F%252Fmaxcdn.bootstrapcdn.com%252Fbootstrap%252F3.3.7%252Fjs%252Fbootstrap.min.js%2522)%250A%2520%2520body%250A%2520%2520%2520%2520block%2520content" rel="noreferrer nofollow noopener)</em></p>
<p>Next, we create a form by adding the snippet below to index.pug file.  </p>
<p><img src="https://paper-attachments.dropbox.com/s_09D7754335BDD40A8ECD55F1187847147F5C3FBE3BBD081087C52D1E8CDCDF32_1589170264156_carbon.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p>Notice how we use the <a target="_blank" href="https://en.wikipedia.org/wiki/POST_(HTTP)">HTTP post method</a> to send data to the server. The code above also sets the action parameter to weather route and adds the text input as “city.”<br>An input button to fetch the weather is also added.   </p>
<p>We now create an HTML table just below the form to display fetched weather records.</p>
<p><img src="https://paper-attachments.dropbox.com/s_09D7754335BDD40A8ECD55F1187847147F5C3FBE3BBD081087C52D1E8CDCDF32_1589171285500_carbon+2.png" alt="Image" width="600" height="400" loading="lazy">
<em>[Raw](https://carbon.now.sh/embed?bg=rgba(0%2C0%2C0%2C1)&amp;t=blackboard&amp;wt=none&amp;l=htmlmixed&amp;ds=true&amp;dsyoff=20px&amp;dsblur=68px&amp;wc=true&amp;wa=true&amp;pv=56px&amp;ph=56px&amp;ln=false&amp;fl=1&amp;fm=Hack&amp;fs=14px&amp;lh=133%25&amp;si=false&amp;es=1x&amp;wm=false&amp;code=.row%250A%2520%2520%2520%2520%2520%2520%2520.col-md-12%250A%2520%2520%2520%2520%2520%2520%2520%2520%2520p%250A%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520br%250A%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520br%250A%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520strong%2520Weather%2520Forecast%2520%2523%257Bforecast%257D%250A%2520%2520%2520%2520%2520%2520%2520%2520%2520table.table%250A%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520thead%250A%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520tr%250A%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520th%2520Longitude%2520%252F%2520Latitude%250A%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%250A%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520th%2520Pressure%250A%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520th%2520Temprature%250A%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520th%2520Humidty%250A%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520tbody%250A%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520if%2520body%250A%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520tr%250A%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520td%250A%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2523%257Bbody.coord.lon%257D%2520%252F%2520%2523%257Bbody.coord.lon%257D%250A%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520td%2520%250A%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2523%257Bbody.main.pressure%257D%250A%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520td%250A%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2523%257Bbody.main.temp%257D%250A%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520td%250A%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2523%257Bbody.main.humidity%257D%250A%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520else%250A%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520tr%250A%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520td(colspan%253D%25226%2522)%250A%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%2520%257C%2520Enter%2520city%2520name%2520and%2520click%2520Fetch%2520Weather%2520button" rel="noreferrer nofollow noopener)</em></p>
<p>Inserting the code snippet above creates a table that looks like this:</p>
<p><img src="https://paper-attachments.dropbox.com/s_09D7754335BDD40A8ECD55F1187847147F5C3FBE3BBD081087C52D1E8CDCDF32_1589196907929_Weather+1.png" alt="Image" width="600" height="400" loading="lazy"></p>
<h2 id="heading-calling-the-climacell-api">Calling the ClimaCell API</h2>
<p>To send requests to the ClimaCell API, we must install the <a target="_blank" href="https://nodejs.dev/making-http-requests-with-nodejs">request module</a>.  </p>
<p><code>npm i request --save</code>  </p>
<p>Next, we add the ClimaCell API credentials in the index.js file. Open the file in your routes directory and add the API key you obtained on the ClimaCell dashboard:  </p>
<p><img src="https://paper-attachments.dropbox.com/s_09D7754335BDD40A8ECD55F1187847147F5C3FBE3BBD081087C52D1E8CDCDF32_1589171891228_example+key.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p>Here is the code to add API credentials:  </p>
<p><img src="https://paper-attachments.dropbox.com/s_09D7754335BDD40A8ECD55F1187847147F5C3FBE3BBD081087C52D1E8CDCDF32_1589172288863_carbon+4.png" alt="Image" width="600" height="400" loading="lazy">
<em>[Raw](https://carbon.now.sh/embed?bg=rgba(0%2C0%2C0%2C1)&amp;t=blackboard&amp;wt=none&amp;l=javascript&amp;ds=true&amp;dsyoff=20px&amp;dsblur=68px&amp;wc=true&amp;wa=true&amp;pv=56px&amp;ph=56px&amp;ln=false&amp;fl=1&amp;fm=Hack&amp;fs=14px&amp;lh=133%25&amp;si=false&amp;es=1x&amp;wm=false&amp;code=let%2520url%2520%2520%2520%2520%253D%2520%27https%253A%252F%252Fclimacell-microweather-v1.p.rapidapi.com%252Fweather%252Frealtime%27%250Alet%2520appId%2520%2520%253D%2520%27appid%253DYOUR%2520API%2520KEY%27%253B%250Alet%2520units%2520%2520%253D%2520%27%2526units%253Dmetric%27%253B%2520%250Avar%2520request%2520%253D%2520require(%27request%27)%253B" rel="noreferrer nofollow noopener)</em></p>
<p>After adding the API credentials, we update the index route. This is done by replacing the code section in <strong>‘/’</strong> route in index.js file.</p>
<p><img src="https://paper-attachments.dropbox.com/s_09D7754335BDD40A8ECD55F1187847147F5C3FBE3BBD081087C52D1E8CDCDF32_1589172437273_carbon+5.png" alt="Image" width="600" height="400" loading="lazy">
<em>[Raw](https://carbon.now.sh/embed?bg=rgba(0%2C0%2C0%2C1)&amp;t=blackboard&amp;wt=none&amp;l=javascript&amp;ds=true&amp;dsyoff=20px&amp;dsblur=68px&amp;wc=true&amp;wa=true&amp;pv=56px&amp;ph=56px&amp;ln=false&amp;fl=1&amp;fm=Hack&amp;fs=14px&amp;lh=133%25&amp;si=false&amp;es=1x&amp;wm=false&amp;code=%252F<em>%2520GET%2520home%2520page.%2520</em>%252F%250Arouter.get(%27%252F%27%252C%2520function(req%252C%2520res%252C%2520next)%2520%257B%250A%2520res.render(%27index%27%252C%2520%257B%27body%27%253A%27%27%252C%2520forecast%253A%2520%27%27%257D)%253B%250A%257D)%253B" rel="noreferrer nofollow noopener)</em></p>
<p>We finish by creating the weather route in index.js.</p>
<p><img src="https://paper-attachments.dropbox.com/s_09D7754335BDD40A8ECD55F1187847147F5C3FBE3BBD081087C52D1E8CDCDF32_1589172832852_carbon+6.png" alt="Image" width="600" height="400" loading="lazy">
<em>[Raw](https://carbon.now.sh/embed?bg=rgba(0%2C0%2C0%2C1)&amp;t=blackboard&amp;wt=none&amp;l=javascript&amp;ds=true&amp;dsyoff=20px&amp;dsblur=68px&amp;wc=true&amp;wa=true&amp;pv=56px&amp;ph=56px&amp;ln=false&amp;fl=1&amp;fm=Hack&amp;fs=14px&amp;lh=133%25&amp;si=false&amp;es=1x&amp;wm=false&amp;code=router.post(%27%252Fweather%27%252C%2520function(req%252C%2520res%252C%2520next)%257B%250A%250A%2520%2520let%2520city%2520%253D%2520req.body.city%253B%250A%250A%2520%2520url%2520%253D%2520url%252Bcity%252B%2522%2526%2522%252BappId%253B%250A%250A%2520request(url%252C%2520function%2520(error%252C%2520response%252C%2520body)%2520%257B%250A%250A%2520%2520%2520%2520%2520%2520body%2520%253D%2520JSON.parse(body)%253B%250A%250A%2520%2520%2520%2520%2520%2520if(error%2520%2526%2526%2520response.statusCode%2520!%253D%2520200)%257B%250A%250A%2520%2520%2520%2520%2520%2520%2520%2520throw%2520error%253B%250A%250A%2520%2520%2520%2520%2520%2520%257D%250A%250A%2520%2520%2520%2520let%2520country%2520%253D%2520(body.sys.country)%2520%253F%2520body.sys.country%2520%253A%2520%27%27%2520%253B%250A%250A%2520%2520%2520%2520let%2520forecast%2520%253D%2520%2522For%2520city%2520%2522%252Bcity%252B%27%252C%2520country%2520%27%252Bcountry%253B%250A%250A%2520%2520%2520%2520res.render(%27index%27%252C%2520%257Bbody%2520%253A%2520body%252C%2520forecast%253A%2520forecast%257D)%253B%250A%250A%2520%2520%2520%257D)%253B%250A%250A%257D)%253B" rel="noreferrer nofollow noopener)</em></p>
<p>This code snippet enables data in the input form to be posted to the index route. Once the user enters a city name, it is assigned to the city variable using the request object.   </p>
<p>The URL is then appended with the city name and ID and the request sent to ClimaCell API.  </p>
<p>The ClimaCell API server response is returned as a JSON file, which is then parsed and fed to the output template.  </p>
<p>For instance, if the user was looking for Boston weather forecasts, the app will return this:</p>
<p><img src="https://paper-attachments.dropbox.com/s_09D7754335BDD40A8ECD55F1187847147F5C3FBE3BBD081087C52D1E8CDCDF32_1589196959089_WEATHER+2.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p>Note - The temperature in this example is shown in Kelvin and is equal to 50°F or 10°C.</p>
<h2 id="heading-add-maps-to-visualize-your-data">Add Maps to Visualize Your Data</h2>
<p>You can integrate interactive maps into your forecasting application to enhance user experience. This can be achieved by using a third-party map service provider for web applications.  </p>
<p>Mapbox is one such tool that helps developers create awesome weather maps for their applications. It integrates seamlessly with any weather app.  </p>
<p>To use the Mapbox, sign up on their website and <a target="_blank" href="https://www.mapbox.com/install/">check out their API</a>. There are integrations for Android, iOS, Web, and Unity. In this case, we choose Web integration for our tool.  </p>
<p>We can either install the Mapbox CDN or use the module bundler. Let's use the module bundler.  </p>
<p>The first step would be installing the package  </p>
<p><code>npm install Mapbox-gl –save</code>  </p>
<p>Next, we add the GL JS CSS file in the HTML file by including this snippet in the   </p>
<p><code>&lt;link href='https://api.mapbox.com/mapbox-gl-js/v1.8.1/mapbox-gl.css' rel='stylesheet' /&gt;</code>  </p>
<p>We can now add the map to our application. To do this, use the code snippet below.</p>
<p><img src="https://paper-attachments.dropbox.com/s_09D7754335BDD40A8ECD55F1187847147F5C3FBE3BBD081087C52D1E8CDCDF32_1589201956495_carbon+1.png" alt="Image" width="600" height="400" loading="lazy">
_[Raw](https://carbon.now.sh/embed?bg=rgba(0%2C0%2C0%2C1)&amp;t=blackboard&amp;wt=none&amp;l=javascript&amp;ds=true&amp;dsyoff=20px&amp;dsblur=68px&amp;wc=true&amp;wa=true&amp;pv=56px&amp;ph=56px&amp;ln=false&amp;fl=1&amp;fm=Hack&amp;fs=14px&amp;lh=133%25&amp;si=false&amp;es=1x&amp;wm=false&amp;code=var%2520mapboxgl%2520%253D%2520require(%27mapbox-gl%252Fdist%252Fmapbox-gl.js%27)%253B%250A%2520%250Amapboxgl.accessToken%2520%253D%2520%27pk.eyJ1IjoiZGlja3Nvbi1tIiwiYSI6ImNrOXphd3MzZDBlMXYzbHFwM2kwbmlvbmkifQ.VE3RRbb8l78w9kxfmh_9ew%27%253B%250Avar%2520map%2520%253D%2520new%2520mapboxgl.Map(%257B%250Acontainer%253A%2520%27CONTAINER_ELEMENT<em>ID%27%252C%250Astyle%253A%2520%27mapbox%253A%252F%252Fstyles%252Fmapbox%252Fstreets-v11%27%250A%257D)%253B%250A" rel="noreferrer nofollow noopener)</em></p>
<p>You can choose where to place the map by replacing the </p>
<p>“CONTAINER_ELEMENT_ID’ .  </p>
<p>Here is a sample map generated using Mapbox:</p>
<p><img src="https://paper-attachments.dropbox.com/s_09D7754335BDD40A8ECD55F1187847147F5C3FBE3BBD081087C52D1E8CDCDF32_1589202164225_map.png" alt="Image" width="600" height="400" loading="lazy"></p>
<h1 id="heading-whats-next">What’s next?</h1>
<p>At this point, much of the work is done, and your app can get weather forecasts for any city using the ClimaCell API. </p>
<p>However, you can consider adding more interactive features to your application or extending its functionality. </p>
<p>Here are some things you might want to do:</p>
<ul>
<li>Add a search function.</li>
<li>Improve the look of your user interface.</li>
<li>Query the application by ID or name.</li>
<li>Display a list of target cities and their respective IDs.</li>
<li>Add parameters to display additional weather data .</li>
<li>Integrate real-time notifications and warning signals.</li>
</ul>
<p>As you can see, the basic app building process is pretty simple and straightforward. By following the above process to leverage the power of a weather API, even beginner-level developers can get their weather application up and running in a matter of minutes.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Forecast Air Pollution with Recurrent Neural Networks ]]>
                </title>
                <description>
                    <![CDATA[ By Bert Carremans After the citizen science project of Curieuze Neuzen, I wanted to learn more about air pollution to see if I could make a data science project out of it. On the website of the European Environment Agency, you can find a huge amount ... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/forecasting-air-pollution-recurrent-neural-networks/</link>
                <guid isPermaLink="false">66d45dd736c45a88f96b7cb9</guid>
                
                    <category>
                        <![CDATA[ Deep Learning ]]>
                    </category>
                
                    <category>
                        <![CDATA[ forecasting ]]>
                    </category>
                
                    <category>
                        <![CDATA[ LSTM ]]>
                    </category>
                
                    <category>
                        <![CDATA[ neural networks ]]>
                    </category>
                
                    <category>
                        <![CDATA[ RNN ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ freeCodeCamp ]]>
                </dc:creator>
                <pubDate>Thu, 02 Jan 2020 21:52:42 +0000</pubDate>
                <media:content url="https://www.freecodecamp.org/news/content/images/2020/01/0_6XEYi4xnCQQGrEdC.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>By Bert Carremans</p>
<p>After the citizen science project of <a target="_blank" href="https://curieuzeneuzen.be/">Curieuze Neuzen</a>, I wanted to learn more about air pollution to see if I could make a data science project out of it. On the website of the <a target="_blank" href="https://www.eea.europa.eu/data-and-maps/data/aqereporting-8">European Environment Agency</a>, you can find a huge amount of data and information about air pollution.</p>
<p>In this notebook, we will focus on the air quality in <strong>Belgium</strong>, and more specifically on the pollution by <strong>sulphur dioxide (SO2)</strong>. The data can be downloaded via <a target="_blank" href="https://www.eea.europa.eu/data-and-maps/data/aqereporting-2/be">https://www.eea.europa.eu/data-and-maps/data/aqereporting-2/be</a>. </p>
<p>The zip file contains separate files for different air pollutants and aggregation levels. The first digit represents the pollutant ID as described in the <a target="_blank" href="http://dd.eionet.europa.eu/vocabulary/aq/pollutant">vocabulary</a>. The file used in this notebook is <strong>BE_1_2013–2015_aggregated_timeseries.csv.</strong> This is the SO2 pollution in Belgium, but you can also find similar data for other European countries.</p>
<p>Descriptions of the fields in the CSV files are available on the <a target="_blank" href="https://www.eea.europa.eu/data-and-maps/data/aqereporting-2/be.">data download page</a>. More background information on air pollutants can be found on <a target="_blank" href="https://nl.wikipedia.org/wiki/Luchtvervuiling">Wikipedia</a>.</p>
<h1 id="heading-project-set-up">Project Set-up</h1>
<pre><code class="lang-python"><span class="hljs-comment"># Importing packages</span>
<span class="hljs-keyword">from</span> pathlib <span class="hljs-keyword">import</span> Path
<span class="hljs-keyword">import</span> pandas <span class="hljs-keyword">as</span> pd
<span class="hljs-keyword">import</span> numpy <span class="hljs-keyword">as</span> np
<span class="hljs-keyword">import</span> pandas_profiling
%matplotlib inline
<span class="hljs-keyword">import</span> matplotlib.pyplot <span class="hljs-keyword">as</span> plt
<span class="hljs-keyword">import</span> warnings
warnings.simplefilter(action = <span class="hljs-string">'ignore'</span>, category = FutureWarning)
<span class="hljs-keyword">from</span> sklearn.preprocessing <span class="hljs-keyword">import</span> MinMaxScaler

<span class="hljs-keyword">from</span> keras.preprocessing.sequence <span class="hljs-keyword">import</span> TimeseriesGenerator
<span class="hljs-keyword">from</span> keras.models <span class="hljs-keyword">import</span> Sequential
<span class="hljs-keyword">from</span> keras.layers <span class="hljs-keyword">import</span> Dense, LSTM, SimpleRNN
<span class="hljs-keyword">from</span> keras.optimizers <span class="hljs-keyword">import</span> RMSprop
<span class="hljs-keyword">from</span> keras.callbacks <span class="hljs-keyword">import</span> ModelCheckpoint, EarlyStopping
<span class="hljs-keyword">from</span> keras.models <span class="hljs-keyword">import</span> model_from_json

<span class="hljs-comment"># Setting the project directory</span>
project_dir = Path(<span class="hljs-string">'/Users/bertcarremans/Data Science/Projecten/air_pollution_forecasting'</span>)
</code></pre>
<h1 id="heading-loading-the-data">Loading the data</h1>
<pre><code class="lang-python">date_vars = [<span class="hljs-string">'DatetimeBegin'</span>,<span class="hljs-string">'DatetimeEnd'</span>]

agg_ts = pd.read_csv(project_dir / <span class="hljs-string">'data/raw/BE_1_2013-2015_aggregated_timeseries.csv'</span>, sep=<span class="hljs-string">'\t'</span>, parse_dates=date_vars, date_parser=pd.to_datetime)
meta = pd.read_csv(project_dir / <span class="hljs-string">'data/raw/BE_2013-2015_metadata.csv'</span>, sep=<span class="hljs-string">'\t'</span>)

print(<span class="hljs-string">'aggregated timeseries shape:{}'</span>.format(agg_ts.shape))
print(<span class="hljs-string">'metadata shape:{}'</span>.format(meta.shape))
</code></pre>
<h1 id="heading-data-exploration">Data Exploration</h1>
<p>Let’s use <strong>pandas_profiling</strong> to inspect the data.</p>
<pre><code class="lang-python">pandas_profiling.ProfileReport(agg_ts)
</code></pre>
<p>I won’t show the output of pandas_profiling in this story in order not to clutter it with charts. But you can find it in my <a target="_blank" href="https://github.com/bertcarremans/air_pollution_forecasting">GitHub repo</a>.</p>
<p>The pandas_profiling report shows us the following:</p>
<ul>
<li>There are 6 constant variables. We can remove these from the data set.</li>
<li>No missing values exist, so probably we will not need to apply imputation.</li>
<li><strong>AirPollutionLevel</strong> has some zeroes, but this could be perfectly normal. On the other hand, these variables have some extreme values, which might be incorrect recordings of air pollution.</li>
<li>There are 53 <strong>AirQualityStations</strong>, which are probably the same as the <strong>SamplingPoints</strong>. <strong>AirQualityStationEoICode</strong> is simply a shorter code for the AirQualityStation, so that variable can also be removed.</li>
<li>There are 3 values for <strong>AirQualityNetwork</strong> (Brussels, Flanders and Wallonia). Most measurements come from Flanders.</li>
<li><strong>DataAggregationProcess</strong>: most rows contain data aggregated as the 24-hour mean of one day of measurements (P1D). More information on the other values can be found <a target="_blank" href="http://dd.eionet.europa.eu/vocabulary/aq/aggregationprocess">here</a>. In this project, we will only consider P1D values.</li>
<li><strong>DataCapture</strong>: Proportion of valid measurement time relative to the total measured time (time coverage) in the averaging period, expressed as a percentage. Almost all rows have about 100% of valid measurement time. Some rows have a DataCapture that is slightly lower than 100%.</li>
<li><strong>DataCoverage</strong>: Proportion of valid measurement included in the aggregation process within the averaging period, expressed as a percentage. In this data set, we have a minimum of 75%. According to the <a target="_blank" href="https://www.eea.europa.eu/data-and-maps/data/aqereporting-2/be">definition of this variable</a> values below 75% should not be included for air quality assessments, which explains why these rows are not present in the data set.</li>
<li><strong>TimeCoverage</strong>: highly correlated to DataCoverage and will be removed from the data.</li>
<li><strong>UnitOfAirPollutionLevel</strong>: 423 rows have a unit of <em>count</em>. To have a consistent target variable we will remove the records with this type of unit.</li>
<li><strong>DateTimeBegin</strong> and <strong>DateTimeEnd</strong>: the histogram does not provide enough detail here. This needs to be analyzed further.</li>
</ul>
<h1 id="heading-datetimebegin-and-datetimeend">DateTimeBegin and DateTimeEnd</h1>
<p>The histogram in the pandas_profiling combined multiple days per bin. Let’s look at a daily level how these variables behave.</p>
<h2 id="heading-multiple-aggregation-levels-per-date">Multiple aggregation levels per date</h2>
<ul>
<li><strong>DatetimeBegin</strong>: a large number of records on the 1st of January of 2013, 2014, 2015 and 1st of October of 2013 and 2014.</li>
<li><strong>DatetimeEnd</strong>: a large number of records on the 1st of January of 2014, 2015, 2016 and 1st of April of 2014 and 2015.</li>
</ul>
<pre><code class="lang-python">plt.figure(figsize=(<span class="hljs-number">20</span>,<span class="hljs-number">6</span>))
plt.plot(agg_ts.groupby(<span class="hljs-string">'DatetimeBegin'</span>).count(), <span class="hljs-string">'o'</span>, color=<span class="hljs-string">'skyblue'</span>)
plt.title(<span class="hljs-string">'Nb of measurements per DatetimeBegin'</span>)
plt.ylabel(<span class="hljs-string">'number of measurements'</span>)
plt.xlabel(<span class="hljs-string">'DatetimeBegin'</span>)
plt.show()
</code></pre>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/01/1_nP33p0hVPjLpxTFAwDQJyQ.png" alt="Image" width="600" height="400" loading="lazy">
<em>Number of rows per date</em></p>
<p>The outliers in the number of records are related to the multiple aggregation levels (DataAggregationProcess). The values in DataAggregationProcess on these dates reflect the time period between DatetimeBegin and DatetimeEnd. For example, the 1st of January 2013 is the start date of a one-year measurement period until the 1st of January 2014.</p>
<p>As we are only interested in the daily aggregation level, <strong>filtering out the other aggregation levels</strong> will solve this issue. We can also remove DatetimeEnd for that reason.</p>
<h2 id="heading-missing-timesteps-on-daily-aggregation-level">Missing timesteps on daily aggregation level</h2>
<p>As we can see below, <strong>not all SamplingPoints have data for all DatetimeBegin in the three-year period</strong>. These are most likely days where the DataCoverage variable was below 75%. So on these days, we do not have sufficient valid measurements. Later in this notebook, we will use measurements on prior days to predict the pollution on the current day.</p>
<p>To have similarly sized timesteps, we will need to insert rows for the missing DatetimeBegin per SamplingPoint. We will <strong>insert the measurement data of the next day with valid data</strong>.</p>
<p>Secondly, we will <strong>remove the SamplingPoints with too many missing timesteps</strong>. Here we will take an arbitrary number of 1.000 timesteps as the minimum number of required timesteps.</p>
<pre><code class="lang-python">ser_avail_days = agg_ts.groupby(<span class="hljs-string">'SamplingPoint'</span>).nunique()[<span class="hljs-string">'DatetimeBegin'</span>]
plt.figure(figsize=(<span class="hljs-number">8</span>,<span class="hljs-number">4</span>))
plt.hist(ser_avail_days.sort_values(ascending=<span class="hljs-literal">False</span>))
plt.ylabel(<span class="hljs-string">'Nb SamplingPoints'</span>)
plt.xlabel(<span class="hljs-string">'Nb of Unique DatetimeBegin'</span>)
plt.title(<span class="hljs-string">'Distribution of Samplingpoints by the Nb of available measurement days'</span>)
plt.show()
</code></pre>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/01/1_tcdKTqJJjjtBZ4QbNN2uzg.png" alt="Image" width="600" height="400" loading="lazy">
<em>Distribution of SamplingPoints by the number of available measurement days</em></p>
<h1 id="heading-data-preparation">Data Preparation</h1>
<h2 id="heading-data-cleaning">Data Cleaning</h2>
<p>Based on the data exploration, we will do the following to clean the data:</p>
<ul>
<li>Keeping only records with DataAggregationProcess of P1D</li>
<li>Removing records with UnitOfAirPollutionLevel of count</li>
<li>Removing unary variables and other redundant variables</li>
<li>Removing SamplingPoints which have less than 1000 measurement days</li>
</ul>
<pre><code class="lang-python">df = agg_ts.loc[agg_ts.DataAggregationProcess==<span class="hljs-string">'P1D'</span>, :] 
df = df.loc[df.UnitOfAirPollutionLevel!=<span class="hljs-string">'count'</span>, :]
df = df.loc[df.SamplingPoint.isin(ser_avail_days[ser_avail_days.values &gt;= <span class="hljs-number">1000</span>].index), :]
vars_to_drop = [<span class="hljs-string">'AirPollutant'</span>,<span class="hljs-string">'AirPollutantCode'</span>,<span class="hljs-string">'Countrycode'</span>,<span class="hljs-string">'Namespace'</span>,<span class="hljs-string">'TimeCoverage'</span>,<span class="hljs-string">'Validity'</span>,<span class="hljs-string">'Verification'</span>,<span class="hljs-string">'AirQualityStation'</span>,
               <span class="hljs-string">'AirQualityStationEoICode'</span>,<span class="hljs-string">'DataAggregationProcess'</span>,<span class="hljs-string">'UnitOfAirPollutionLevel'</span>, <span class="hljs-string">'DatetimeEnd'</span>, <span class="hljs-string">'AirQualityNetwork'</span>,
               <span class="hljs-string">'DataCapture'</span>, <span class="hljs-string">'DataCoverage'</span>]
df.drop(columns=vars_to_drop, axis=<span class="hljs-string">'columns'</span>, inplace=<span class="hljs-literal">True</span>)
</code></pre>
<h2 id="heading-inserting-rows-for-the-missing-timesteps">Inserting rows for the missing timesteps</h2>
<p>For each SamplingPoint, we will first insert (empty) rows for which we do not have a DatetimeBegin. This can be done by creating a complete multi-index with all SamplingPoints and over the range between the minimum and maximum DatetimeBegin. Then, <strong>reindex</strong> will insert the missing rows but with NaN for the columns.</p>
<p>Secondly, we use <strong>bfill</strong> and specify to impute the missing values with the values of the next row with valid data. The bfill method is applied to a groupby object to limit the backfilling within the rows of each SamplingPoint. That way we do not use the values of another SamplingPoint to fill in the missing values.</p>
<p>A samplepoint to test whether this operation worked correctly is _SPO-BETR223_00001<em>100</em> for the date <em>2013–01–29</em>.</p>
<pre><code class="lang-python">dates = list(pd.period_range(min(df.DatetimeBegin), max(df.DatetimeBegin), freq=<span class="hljs-string">'D'</span>).values)
samplingpoints = list(df.SamplingPoint.unique())

new_idx = []
<span class="hljs-keyword">for</span> sp <span class="hljs-keyword">in</span> samplingpoints:
    <span class="hljs-keyword">for</span> d <span class="hljs-keyword">in</span> dates:
        new_idx.append((sp, np.datetime64(d)))

df.set_index(keys=[<span class="hljs-string">'SamplingPoint'</span>, <span class="hljs-string">'DatetimeBegin'</span>], inplace=<span class="hljs-literal">True</span>)
df.sort_index(inplace=<span class="hljs-literal">True</span>)
df = df.reindex(new_idx)
<span class="hljs-comment">#print(df.loc['SPO-BETR223_00001_100','2013-01-29'])  # should contain NaN for the columns</span>

df[<span class="hljs-string">'AirPollutionLevel'</span>] = df.groupby(level=<span class="hljs-number">0</span>).AirPollutionLevel.bfill().fillna(<span class="hljs-number">0</span>)
<span class="hljs-comment">#print(df.loc['SPO-BETR223_00001_100','2013-01-29'])  # NaN are replaced by values of 2013-01-30</span>
print(<span class="hljs-string">'{} missing values'</span>.format(df.isnull().sum().sum()))
</code></pre>
<h2 id="heading-handling-multiple-time-series">Handling multiple time series</h2>
<p>Alright, now we have a data set that is cleaned and does not contain any missing values. One aspect that makes this data set particular is that we have data for <strong>multiple samplingpoints</strong>. So we have multiple time series.</p>
<p>One way to deal with that is to create <strong>dummy variables for the samplingpoints</strong> and use all records to train the model. Another way is to build a <strong>separate model per samplingpoint</strong>. </p>
<p>In this notebook, we will do the latter. We will, however, limit the notebook to do that for only one samplingpoint. But the same logic can be applied to every samplingpoint.</p>
<pre><code class="lang-python">df = df.loc[<span class="hljs-string">'SPO-BETR223_00001_100'</span>,:]
</code></pre>
<h2 id="heading-split-train-test-and-validation-set">Split train, test and validation set</h2>
<p>We split off a test set in order to evaluate the performance of the model. The test set will not be used during the training phase.</p>
<ul>
<li>train set: data until July 2014</li>
<li>validation set: 6 months between July 2014 and January 2015</li>
<li>test set: data of 2015</li>
</ul>
<pre><code class="lang-python">train = df.query(<span class="hljs-string">'DatetimeBegin &lt; "2014-07-01"'</span>)
valid = df.query(<span class="hljs-string">'DatetimeBegin &gt;= "2014-07-01" and DatetimeBegin &lt; "2015-01-01"'</span>)
test = df.query(<span class="hljs-string">'DatetimeBegin &gt;= "2015-01-01"'</span>)
</code></pre>
<h2 id="heading-scaling">Scaling</h2>
<pre><code class="lang-python"><span class="hljs-comment"># Save column names and indices to use when storing as csv</span>
cols = train.columns
train_idx = train.index
valid_idx = valid.index
test_idx = test.index

<span class="hljs-comment"># normalize the dataset</span>
scaler = MinMaxScaler(feature_range=(<span class="hljs-number">0</span>, <span class="hljs-number">1</span>))
train = scaler.fit_transform(train)
valid = scaler.transform(valid)
test = scaler.transform(test)
</code></pre>
<h2 id="heading-save-the-processed-datasets">Save the processed datasets</h2>
<p>That way we don’t need to redo the preprocessing every time we rerun the notebook.</p>
<pre><code class="lang-python">train = pd.DataFrame(train, columns=cols, index=train_idx)
valid = pd.DataFrame(valid, columns=cols, index=valid_idx)
test = pd.DataFrame(test, columns=cols, index=test_idx)

train.to_csv(<span class="hljs-string">'../data/processed/train.csv'</span>)
valid.to_csv(<span class="hljs-string">'../data/processed/valid.csv'</span>)
test.to_csv(<span class="hljs-string">'../data/processed/test.csv'</span>)
</code></pre>
<h1 id="heading-modeling">Modeling</h1>
<p>First, we read in the processed data sets. Secondly, we create a function to plot the training and validation loss for the different models we will build.</p>
<pre><code class="lang-python">train = pd.read_csv(<span class="hljs-string">'../data/processed/train.csv'</span>, header=<span class="hljs-number">0</span>, index_col=<span class="hljs-number">0</span>).values.astype(<span class="hljs-string">'float32'</span>)
valid = pd.read_csv(<span class="hljs-string">'../data/processed/valid.csv'</span>, header=<span class="hljs-number">0</span>, index_col=<span class="hljs-number">0</span>).values.astype(<span class="hljs-string">'float32'</span>)
test = pd.read_csv(<span class="hljs-string">'../data/processed/test.csv'</span>, header=<span class="hljs-number">0</span>, index_col=<span class="hljs-number">0</span>).values.astype(<span class="hljs-string">'float32'</span>)

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">plot_loss</span>(<span class="hljs-params">history, title</span>):</span>
    plt.figure(figsize=(<span class="hljs-number">10</span>,<span class="hljs-number">6</span>))
    plt.plot(history.history[<span class="hljs-string">'loss'</span>], label=<span class="hljs-string">'Train'</span>)
    plt.plot(history.history[<span class="hljs-string">'val_loss'</span>], label=<span class="hljs-string">'Validation'</span>)
    plt.title(title)
    plt.xlabel(<span class="hljs-string">'Nb Epochs'</span>)
    plt.ylabel(<span class="hljs-string">'Loss'</span>)
    plt.legend()
    plt.show()

    val_loss = history.history[<span class="hljs-string">'val_loss'</span>]
    min_idx = np.argmin(val_loss)
    min_val_loss = val_loss[min_idx]
    print(<span class="hljs-string">'Minimum validation loss of {} reached at epoch {}'</span>.format(min_val_loss, min_idx))
</code></pre>
<h2 id="heading-prepare-data-with-the-timeseriesgenerator">Prepare data with the TimeseriesGenerator</h2>
<p>The <a target="_blank" href="https://keras.io/preprocessing/sequence/#timeseriesgenerator">TimeseriesGenerator of Keras</a> helps us building the data in the correct format for modeling.</p>
<ul>
<li><strong>length:</strong> number of timesteps in the generated sequence. Here we want to look back an arbitrary number of _n<em>lag</em> timesteps. In reality, n_lag could depend on how the predictions will be used. Suppose the Belgian government can take some actions to reduce the SO2 pollution around a samplingpoint (for instance prohibit the entrance of diesel cars in a city for a certain period of time). And suppose the government needs 14 days before the corrective actions can go in effect. Then it would make sense to set n_lag to 14.</li>
<li><strong>sampling_rate:</strong> number of timesteps between successive timesteps in the generated sequence. We want to keep all timesteps, so we leave this to the default of 1.</li>
<li><strong>stride:</strong> this parameter influences how much the generated sequences will overlap. As we do not have much data, we leave it to the default of 1. This means that two sequences generated after one another overlap with all timesteps except one.</li>
<li><strong>batch_size:</strong> number of generated sequences in each batch</li>
</ul>
<pre><code class="lang-python">n_lag = <span class="hljs-number">14</span>

train_data_gen = TimeseriesGenerator(train, train, length=n_lag, sampling_rate=<span class="hljs-number">1</span>, stride=<span class="hljs-number">1</span>, batch_size = <span class="hljs-number">5</span>)
valid_data_gen = TimeseriesGenerator(train, train, length=n_lag, sampling_rate=<span class="hljs-number">1</span>, stride=<span class="hljs-number">1</span>, batch_size = <span class="hljs-number">1</span>)
test_data_gen = TimeseriesGenerator(test, test, length=n_lag, sampling_rate=<span class="hljs-number">1</span>, stride=<span class="hljs-number">1</span>, batch_size = <span class="hljs-number">1</span>)
</code></pre>
<h2 id="heading-recurrent-neural-networks">Recurrent Neural Networks</h2>
<p><strong>Traditional neural networks have no memory</strong>. Consequently, they do not take into account previous input when processing the current input. In sequential data sets, like time series, the information of previous time steps is typically relevant for predicting something in the current step. So a <strong>state</strong> about the previous time steps needs to be maintained.</p>
<p>In our case, the air pollution at time t might be influenced by air pollution in previous timesteps. So we need to take that into account. Recurrent Neural Networks or RNNs have an internal loop by which they maintain a state of previous timesteps. This state is then used for the prediction in the current timestep. The state is reset when a new sequence is being processed. </p>
<p>For an illustrated guide about RNNs, you should definitely read the <a target="_blank" href="https://towardsdatascience.com/illustrated-guide-to-recurrent-neural-networks-79e5eb8049c9">article by Michael Nguyen</a>.</p>
<p>For our case, we use a <a target="_blank" href="https://keras.io/layers/recurrent/#simplernn">SimpleRNN</a> of the Keras package. We also specify an <strong>EarlyStopping</strong> callback to stop the training when there were 10 epochs without any improvement in the validation loss. The <strong>ModelCheckpoint</strong> allows us to save the weights of the best model. The model architecture still needs to be saved separately.</p>
<pre><code class="lang-python">simple_rnn = Sequential()
simple_rnn.add(SimpleRNN(<span class="hljs-number">4</span>, input_shape=(n_lag, <span class="hljs-number">1</span>)))
simple_rnn.add(Dense(<span class="hljs-number">1</span>))
simple_rnn.compile(loss=<span class="hljs-string">'mae'</span>, optimizer=RMSprop())

checkpointer = ModelCheckpoint(filepath=<span class="hljs-string">'../model/simple_rnn_weights.hdf5'</span>
                               , verbose=<span class="hljs-number">0</span>
                               , save_best_only=<span class="hljs-literal">True</span>)
earlystopper = EarlyStopping(monitor=<span class="hljs-string">'val_loss'</span>
                             , patience=<span class="hljs-number">10</span>
                             , verbose=<span class="hljs-number">0</span>)
<span class="hljs-keyword">with</span> open(<span class="hljs-string">"../model/simple_rnn.json"</span>, <span class="hljs-string">"w"</span>) <span class="hljs-keyword">as</span> m:
    m.write(simple_rnn.to_json())

simple_rnn_history = simple_rnn.fit_generator(train_data_gen
                                              , epochs=<span class="hljs-number">100</span>
                                              , validation_data=valid_data_gen
                                              , verbose=<span class="hljs-number">0</span>
                                              , callbacks=[checkpointer, earlystopper])
plot_loss(simple_rnn_history, <span class="hljs-string">'SimpleRNN - Train &amp; Validation Loss'</span>)
</code></pre>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/01/1_5iUNaezYr3rlTqqmtVm_IA.png" alt="Image" width="600" height="400" loading="lazy">
<em>Training and validation loss for a SimpleRNN</em></p>
<h2 id="heading-long-short-term-memory-networks">Long Short Term Memory Networks</h2>
<p><strong>An RNN has a short memory</strong>. It has difficulty remembering information from many timesteps ago. This occurs when the sequences are very long. </p>
<p>In fact, it is due to the <strong>vanishing gradient problem</strong>. The gradients are values that update the weights of a neural network. When you have many timesteps in your RNN the gradient for the first layers becomes very tiny. As a result, the update of the weights of the first layers is negligible. This means that the RNN is not capable of learning what was in the early layers.</p>
<p>So we need a way to carry the information of the first layers to later layers. LSTMs are better suited to take into account long-term dependencies. Michael Nguyen wrote an excellent article with a <a target="_blank" href="https://towardsdatascience.com/illustrated-guide-to-lstms-and-gru-s-a-step-by-step-explanation-44e9eb85bf21">visual description of LSTMs</a>.</p>
<h2 id="heading-simple-lstm-model">Simple LSTM model</h2>
<pre><code class="lang-python">simple_lstm = Sequential()
simple_lstm.add(LSTM(<span class="hljs-number">4</span>, input_shape=(n_lag, <span class="hljs-number">1</span>)))
simple_lstm.add(Dense(<span class="hljs-number">1</span>))
simple_lstm.compile(loss=<span class="hljs-string">'mae'</span>, optimizer=RMSprop())

checkpointer = ModelCheckpoint(filepath=<span class="hljs-string">'../model/simple_lstm_weights.hdf5'</span>
                               , verbose=<span class="hljs-number">0</span>
                               , save_best_only=<span class="hljs-literal">True</span>)
earlystopper = EarlyStopping(monitor=<span class="hljs-string">'val_loss'</span>
                             , patience=<span class="hljs-number">10</span>
                             , verbose=<span class="hljs-number">0</span>)
<span class="hljs-keyword">with</span> open(<span class="hljs-string">"../model/simple_lstm.json"</span>, <span class="hljs-string">"w"</span>) <span class="hljs-keyword">as</span> m:
    m.write(simple_lstm.to_json())

simple_lstm_history = simple_lstm.fit_generator(train_data_gen
                                                , epochs=<span class="hljs-number">100</span>
                                                , validation_data=valid_data_gen
                                                , verbose=<span class="hljs-number">0</span>
                                                , callbacks=[checkpointer, earlystopper])
plot_loss(simple_lstm_history, <span class="hljs-string">'Simple LSTM - Train &amp; Validation Loss'</span>)
</code></pre>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/01/1_5jE6sI6yxkGxQovhcaEC2A.png" alt="Image" width="600" height="400" loading="lazy">
<em>Training and validation loss for a simple LSTM</em></p>
<h2 id="heading-stacked-lstm-model">Stacked LSTM model</h2>
<p>In this model, we will be stacking multiple LSTM layers. That way the model will learn other abstractions of input data over time. In other words, <strong>representing the input data at different time scales</strong>.</p>
<p>To do that in Keras, we need to specify the parameter <strong>return_sequences</strong> in the LSTM layer preceding another LSTM layer.</p>
<pre><code class="lang-python">stacked_lstm = Sequential()
stacked_lstm.add(LSTM(<span class="hljs-number">16</span>, input_shape=(n_lag, <span class="hljs-number">1</span>), return_sequences=<span class="hljs-literal">True</span>))
stacked_lstm.add(LSTM(<span class="hljs-number">8</span>, return_sequences=<span class="hljs-literal">True</span>))
stacked_lstm.add(LSTM(<span class="hljs-number">4</span>))
stacked_lstm.add(Dense(<span class="hljs-number">1</span>))
stacked_lstm.compile(loss=<span class="hljs-string">'mae'</span>, optimizer=RMSprop())

checkpointer = ModelCheckpoint(filepath=<span class="hljs-string">'../model/stacked_lstm_weights.hdf5'</span>
                               , verbose=<span class="hljs-number">0</span>
                               , save_best_only=<span class="hljs-literal">True</span>)
earlystopper = EarlyStopping(monitor=<span class="hljs-string">'val_loss'</span>
                             , patience=<span class="hljs-number">10</span>
                             , verbose=<span class="hljs-number">0</span>)
<span class="hljs-keyword">with</span> open(<span class="hljs-string">"../model/stacked_lstm.json"</span>, <span class="hljs-string">"w"</span>) <span class="hljs-keyword">as</span> m:
    m.write(stacked_lstm.to_json())

stacked_lstm_history = stacked_lstm.fit_generator(train_data_gen
                                                  , epochs=<span class="hljs-number">100</span>
                                                  , validation_data=valid_data_gen
                                                  , verbose=<span class="hljs-number">0</span>
                                                  , callbacks=[checkpointer, earlystopper])
plot_loss(stacked_lstm_history, <span class="hljs-string">'Stacked LSTM - Train &amp; Validation Loss'</span>)
</code></pre>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/01/1_0CBPciBeReSNz2FOfuZJuw.png" alt="Image" width="600" height="400" loading="lazy">
<em>Training and validation loss for a stacked LSTM</em></p>
<h1 id="heading-evaluating-performance">Evaluating Performance</h1>
<p>Based on the minimum validation losses the SimpleRNN seems to outperform the LSTM models, although the metrics are close to each other. </p>
<p>With the <strong>evaluate_generator</strong> method, we can evaluate the models on the test data (generator). This will give us the loss on the test data. We will first load the model architecture from the JSON files and the best model’s weights.</p>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">eval_best_model</span>(<span class="hljs-params">model</span>):</span>
    <span class="hljs-comment"># Load model architecture from JSON</span>
    model_architecture = open(<span class="hljs-string">'../model/'</span>+model+<span class="hljs-string">'.json'</span>, <span class="hljs-string">'r'</span>)
    best_model = model_from_json(model_architecture.read())
    model_architecture.close()
    <span class="hljs-comment"># Load best model's weights</span>
    best_model.load_weights(<span class="hljs-string">'../model/'</span>+model+<span class="hljs-string">'_weights.hdf5'</span>)
    <span class="hljs-comment"># Compile the best model</span>
    best_model.compile(loss=<span class="hljs-string">'mae'</span>, optimizer=RMSprop())
    <span class="hljs-comment"># Evaluate on test data</span>
    perf_best_model = best_model.evaluate_generator(test_data_gen)
    print(<span class="hljs-string">'Loss on test data for {} : {}'</span>.format(model, perf_best_model))

eval_best_model(<span class="hljs-string">'simple_rnn'</span>)
eval_best_model(<span class="hljs-string">'simple_lstm'</span>)
eval_best_model(<span class="hljs-string">'stacked_lstm'</span>)
</code></pre>
<ul>
<li>Loss on test data for simple_rnn: 0.01638169982337905</li>
<li>Loss on test data for simple_lstm: 0.015934137431135205</li>
<li>Loss on test data for stacked_lstm: 0.015420083056716116</li>
</ul>
<h1 id="heading-conclusion">Conclusion</h1>
<p>In this story, we used a Recurrent Neural Network and two different architectures for an LSTM. The <strong>best performance comes from the stacked LSTM</strong> consisting of a few hidden layers.</p>
<p>There are definitely a number of things worth investigating further that could improve the model's performance.</p>
<ul>
<li>Use the hourly data (another CSV file available on the EEA website) and try out <strong>other sampling strategies</strong> than the daily data.</li>
<li>Use the data about the <strong>other pollutants as features</strong> to predict SO2 pollution. Perhaps other pollutants are correlated to the SO2 pollution.</li>
<li>Construct other <strong>features based on the date</strong>. A nice write-up can be found in the <a target="_blank" href="https://github.com/drivendataorg/power-laws-forecasting/blob/master/3rd%20Place/Model_Documentation_and_Write_up.pdf">PDF of one of the winners</a> of the Power Laws Forecasting competition of <a target="_blank" href="https://www.drivendata.org/">Driven Data</a>.</li>
</ul>
<p>I’ve learned a lot about recurrent neural networks by doing this project. I hope you’ve enjoyed it. Feel free to leave any comments!</p>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
