<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged scikit-learn</title>
<link>https://coderlegion.com/tag/scikit-learn</link>
<description>Powered by Question2Answer</description>
<item>
<title>Building Decision Trees with Pandas and Scikit-learn</title>
<link>https://coderlegion.com/24636/building-decision-trees-with-pandas-and-scikit-learn</link>
<description>&lt;h4 id=&quot;the-theory&quot;&gt;The Theory&lt;/h4&gt;
&lt;p&gt;So what's a Decision Tree Regressor and how does it work?&lt;br&gt;
It's a regression model so it returns continuous numerical values. It gets a prediction by asking a series of yes/no questions about the chosen features, building a binary tree structure where each question splits the data further until it reaches a prediction at the leaf node.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;It learns by choosing which questions to ask&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;So for a given feature, a &quot;split&quot; is a threshold value that divides the data into two groups, those above and those below.&lt;/p&gt;
&lt;p&gt;So say we wanted to make predictions on house price and we have features like the number of rooms, the size of the land, etc. Then at each node the algorithm tries &lt;strong&gt;every possible split across every feature&lt;/strong&gt; and picks the one that minimises the &lt;strong&gt;mean squared error (MSE)&lt;/strong&gt; of the resulting groups i.e. the split that produces two groups &lt;strong&gt;where house prices&lt;/strong&gt; within each group are as similar to each other as possible &lt;em&gt;( it sums the MSE from each set of two groups and picks the set with the lowest combined MSE).&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Where,&lt;/p&gt;
&lt;p&gt;\[\text{MSE} = \frac{1}{n} \sum_{i=1}^{n} (x_i - \bar{x})^2\]&lt;/p&gt;
&lt;p&gt;&lt;em&gt;example:&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;| &lt;code&gt;Split 1: Rooms &amp;lt; 2&lt;/code&gt; | &lt;code&gt;Group A [1]&lt;/code&gt; |              &lt;code&gt;         Group B [2, 3, 4, 5]&lt;/code&gt; |&lt;br&gt;
| &lt;code&gt;Split 2: Rooms &amp;lt; 3&lt;/code&gt; | &lt;code&gt;Group A [1, 2]&lt;/code&gt; | &lt;code&gt;Group B [3, 4, 5]&lt;/code&gt; |&lt;br&gt;
| &lt;code&gt;Split 3: Rooms &amp;lt; 4&lt;/code&gt; | &lt;code&gt;Group A [1, 2, 3]&lt;/code&gt; | &lt;code&gt;Group B [4, 5]&lt;/code&gt; |&lt;br&gt;
| &lt;code&gt;Split 4: Rooms &amp;lt; 5&lt;/code&gt; | &lt;code&gt;Group A [1, 2, 3,4]&lt;/code&gt; | &lt;code&gt;Group B [5]&lt;/code&gt; |&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What value does the leaf node actually hold?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This is the key bit. During training, multiple houses end up at each leaf node. The predicted value stored at that leaf is simply the &lt;strong&gt;mean house price of all training houses that landed there&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;How do we know if our model is any good?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A reasonable way to validate the model is to compare, on average, how close a prediction is to the actual value. We will use Mean Absolute Error (MAE).&lt;/p&gt;
&lt;p&gt;\[MAE = \frac{1}{n} \sum_{i=1}^{n} |y_i - \hat{y}_i|\]&lt;/p&gt;
&lt;p&gt;At this point you may be wondering why are we using MSE for training and MAE for validation. Can't we just pick one metric and use it for splitting the tree and validating it?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;We use MSE for training because it's faster than MAE&lt;/strong&gt;. During training, the tree has to calculate errors for millions of potential splits. Because MSE uses squared numbers, it unlocks a clever mathematical shortcut (variance reduction) that lets the computer calculate splits using a simple running tally.&lt;/p&gt;
&lt;p&gt;\[\sum (y_i - \bar{y})^2 = \sum y_i^2 - \frac{(\sum y_i)^2}{n}\]&lt;/p&gt;
&lt;p&gt;Look closely at the right side of the identity. There are no subtractions between individual data points and the mean. Instead, the entire formula is built out of only three components, The sum of all squared target values, the sum of all target values &lt;em&gt;(which is then squared)&lt;/em&gt; and the count n.&lt;/p&gt;
&lt;p&gt;The computer can keep a running tally of the &lt;strong&gt;sums&lt;/strong&gt;, the &lt;strong&gt;squared sums&lt;/strong&gt;, and the &lt;strong&gt;counts&lt;/strong&gt; as it slides through split points. It never has to look back at individual data points to calculate the error for a group.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;We validate with MAE because humans understand it&lt;/strong&gt;: MSE leaves you with an error in &quot;squared units&quot; &lt;em&gt;(like squared euros)&lt;/em&gt;, which would probably raise a few eyebrows at your local bank.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Limitations to consider.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Decision Tree tends to &lt;strong&gt;overfit&lt;/strong&gt; because it memorises the training data rather than learning general patterns. This may result in highly specific &quot;rules&quot; that only apply to historical data, causing it to struggle to generalise.&lt;/p&gt;
&lt;p&gt;When you train a decision tree, it uses a &lt;strong&gt;greedy algorithm&lt;/strong&gt;. This means at every single split, it only cares about making the absolute perfect decision for the data right in front of it &lt;em&gt;at that exact moment&lt;/em&gt;. It doesn't look ahead, and it doesn't plan for the future.&lt;/p&gt;
&lt;p&gt;If you don't stop it (by setting limits like &lt;code&gt;max_depth&lt;/code&gt;), the tree will keep asking questions until it has separated every single unique case in your training set into its own tiny leaf node.&lt;/p&gt;
&lt;p&gt;This is exactly why &lt;strong&gt;Ensemble Methods&lt;/strong&gt; like XGBoost exist - it builds hundreds of shallow trees and combines them, which is far more robust. Think of a Decision Tree as the foundation concept that XGBoost is built on top of.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;the-code&quot;&gt;The Code&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Learning path:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Explore the data and identify prediction target&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Transform - Clean the data&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Select features and Model type&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Train the model - find patterns from the data&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Predict and Evaluate the model&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Optimise the model&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;step-1-load-and-explore-the-data&quot;&gt;&lt;strong&gt;Step 1&lt;/strong&gt;: Load and Explore the data&lt;/h4&gt;
&lt;p&gt;Using Pandas to read from a csv file and return a pandas DataFrame.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import pandas as pd

housing_df = pd.read_csv('./datasets/housing_data.csv')

print(f&quot;Statistical Summary \n {housing_df.describe()}&quot;)

print(f&quot;Dataset Features \n {housing_df.columns}&quot;) 
&lt;/code&gt;&lt;/pre&gt;
&lt;details data-node-type=&quot;hn-details-summary&quot;&gt;
&lt;summary&gt;Output&lt;/summary&gt;
&lt;p&gt;&lt;code&gt;Statistical Summary&lt;/code&gt;&lt;/p&gt;&lt;p&gt;&lt;code&gt;Rooms Price ... Longtitude&lt;/code&gt;&lt;/p&gt;&lt;p&gt;&lt;code&gt;Propertycount&lt;/code&gt;&lt;/p&gt;&lt;p&gt;&lt;code&gt;count 13580.000000 1.358000e+04 ... 13580.000000 13580.000000&lt;/code&gt;&lt;/p&gt;&lt;p&gt;&lt;code&gt;mean 2.937997 1.075684e+06 ... 144.995216 7454.417378&lt;/code&gt;&lt;/p&gt;&lt;p&gt;&lt;code&gt;std 0.955748 6.393107e+05 ... 0.103916 4378.581772&lt;/code&gt;&lt;/p&gt;&lt;p&gt;&lt;code&gt;min 1.000000 8.500000e+04 ... 144.431810 249.000000&lt;/code&gt;&lt;/p&gt;&lt;p&gt;&lt;code&gt;25% 2.000000 6.500000e+05 ... 144.929600 4380.000000&lt;/code&gt;&lt;/p&gt;&lt;p&gt;&lt;code&gt;50% 3.000000 9.030000e+05 ... 145.000100 6555.000000&lt;/code&gt;&lt;/p&gt;&lt;p&gt;&lt;code&gt;75% 3.000000 1.330000e+06 ... 145.058305 10331.000000&lt;/code&gt;&lt;/p&gt;&lt;p&gt;&lt;code&gt;max 10.000000 9.000000e+06 ... 145.526350 21650.000000&lt;/code&gt;&lt;/p&gt;
&lt;/details&gt;&lt;details data-node-type=&quot;hn-details-summary&quot;&gt;
&lt;summary&gt;Output&lt;/summary&gt;
&lt;p&gt;&lt;code&gt;Dataset Features&lt;/code&gt;&lt;/p&gt;&lt;p&gt;&lt;code&gt;Index(['Suburb', 'Address', 'Rooms', 'Type', 'Price', 'Method', 'SellerG', 'Date', 'Distance', 'Postcode', 'Bedroom2', 'Bathroom', 'Car', 'Landsize', 'BuildingArea', 'YearBuilt', 'CouncilArea', 'Lattitude', 'Longtitude', 'Regionname', 'Propertycount'], dtype='object')&lt;/code&gt;&lt;/p&gt;
&lt;/details&gt;
&lt;h4 id=&quot;step-2-transform-the-data&quot;&gt;&lt;strong&gt;Step 2:&lt;/strong&gt; Transform the data&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;(i)&lt;/strong&gt; Remove any rows that have NaN &lt;em&gt;(missing) value ( axis: 0 -&amp;gt; rows, 1 -&amp;gt; columns )&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;clean_housing_df = housing_df.dropna(axis=0)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;(ii)&lt;/strong&gt; Select the &lt;em&gt;prediction target&lt;/em&gt; 'y' and the &lt;em&gt;features 'X'&lt;/em&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;y = clean_housing_df.Price
features = ['Rooms', 'Bathroom', 'Landsize', 'Longtitude', 'Lattitude']
X = clean_housing_df[features]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;(iii)&lt;/strong&gt; Split the data into a training set and validation set&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from sklearn.model_selection import train_test_split

train_X, val_X, train_y, val_y = train_test_split(X, y, random_state=1)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;step-3-select-model-type-and-train-&quot;&gt;&lt;strong&gt;Step 3:&lt;/strong&gt; Select Model type and train.&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;(i)&lt;/strong&gt; Scikit-learn Decision Tree Regressor &lt;em&gt;( random state = 1. If the split results in a tie then breaking is deterministic )&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from sklearn.tree import DecisionTreeRegressor

housing_regressor_model = DecisionTreeRegressor(random_state=1)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;(ii)&lt;/strong&gt; Fit the model&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;housing_regressor_model.fit(train_X, train_y)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;step-4-predict&quot;&gt;&lt;strong&gt;Step 4:&lt;/strong&gt; Predict&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;predicted_home_prices = housing_regressor_model.predict(val_X)

print(predicted_home_prices)
&lt;/code&gt;&lt;/pre&gt;
&lt;details data-node-type=&quot;hn-details-summary&quot;&gt;
&lt;summary&gt;Output&lt;/summary&gt;
&lt;p&gt;&lt;code&gt;[ 503000. 1857000. 760000. ... 4200000. 800000. 1785000.]&lt;/code&gt;&lt;/p&gt;
&lt;/details&gt;
&lt;h4 id=&quot;step-5-validate&quot;&gt;&lt;strong&gt;Step 5:&lt;/strong&gt; Validate&lt;/h4&gt;
&lt;p&gt;Using the &lt;strong&gt;&lt;em&gt;Mean Absolute Error&lt;/em&gt;&lt;/strong&gt; metric as discussed above.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from sklearn.metrics import mean_absolute_error

mae = mean_absolute_error(val_y, predicted_home_prices)
print(mae)
&lt;/code&gt;&lt;/pre&gt;
&lt;details data-node-type=&quot;hn-details-summary&quot;&gt;
&lt;summary&gt;Output&lt;/summary&gt;
&lt;p&gt;251698.44673983214&lt;/p&gt;
&lt;/details&gt;
&lt;h4 id=&quot;step-6-optimise&quot;&gt;&lt;strong&gt;Step 6:&lt;/strong&gt; Optimise&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;How can we minimise the MAE?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;By using a process known as &lt;strong&gt;Hyperparameter Optimisation&lt;/strong&gt; .&lt;br&gt;
We can tune our decision tree using the &lt;code&gt;max_leaf_nodes&lt;/code&gt; parameter, which is passed to the model before training. This setting allows us to specify the upper limit of leaf nodes the model is allowed to create, helping us control the balance between underfitting (too few leaves) and overfitting (too many leaves).&lt;/p&gt;
&lt;p&gt;Let see if we can find a value for &lt;code&gt;max_leaf_nodes&lt;/code&gt; that minimise the MAE.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;mae_scores = []
candidate_levels = [10, 100, 200, 400, 500, 1000, 5000]

for max_leafs in candidate_levels:
    # Initialize and fit the model with the current candidate size
    model = DecisionTreeRegressor(max_leaf_nodes=max_leafs, random_state=0)
    model.fit(train_X, train_y)
    
    # Predict and evaluate
    preds_val = model.predict(val_X)
    mae = mean_absolute_error(val_y, preds_val)
    
    # Store the result as a sublist: [error, node_count]
    mae_scores.append([mae, max_leafs])
    print(f&quot;Max leaf nodes: {max_leafs} | MAE: {mae:.2f}&quot;)

# Find the sublist with the lowest MAE score
smallest_mae = min(mae_scores, key=lambda x: x[0])
print(f&quot;\nThe optimal choice for max_leaf_nodes is: {smallest_mae[1]} (MAE: {smallest_mae[0]:.2f})&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;details data-node-type=&quot;hn-details-summary&quot;&gt;
&lt;summary&gt;Output&lt;/summary&gt;
&lt;p&gt;&lt;code&gt;Max leaf nodes: 10 | MAE: 320570.21&lt;/code&gt;&lt;/p&gt;&lt;p&gt;&lt;code&gt;Max leaf nodes: 100 | MAE: 256533.25&lt;/code&gt;&lt;/p&gt;&lt;p&gt;&lt;code&gt;Max leaf nodes: 200 | MAE: 244453.42&lt;/code&gt;&lt;/p&gt;&lt;p&gt;&lt;code&gt;Max leaf nodes: 400 | MAE: 243238.90&lt;/code&gt;&lt;/p&gt;&lt;p&gt;&lt;code&gt;Max leaf nodes: 500 | MAE: 241920.26&lt;/code&gt;&lt;/p&gt;&lt;p&gt;&lt;code&gt;Max leaf nodes: 1000 | MAE: 243230.84&lt;/code&gt;&lt;/p&gt;&lt;p&gt;&lt;code&gt;Max leaf nodes: 5000 | MAE: 256050.02&lt;/code&gt;&lt;/p&gt;&lt;p&gt;&lt;code&gt;The optimal choice for max_leaf_nodes is: 500 (MAE: 241920.26)&lt;/code&gt;&lt;/p&gt;&lt;p&gt;&lt;/p&gt;
&lt;/details&gt;
</description>
<category>Tutorials</category>
<guid isPermaLink="true">https://coderlegion.com/24636/building-decision-trees-with-pandas-and-scikit-learn</guid>
<pubDate>Fri, 14 Aug 2026 14:10:38 +0000</pubDate>
</item>
<item>
<title>Weather Service Project (Part 3): Predicting the Future with AI and OpenWeatherMap</title>
<link>https://coderlegion.com/10251/weather-service-project-part-3-predicting-the-future-with-ai-and-openweathermap</link>
<description>&lt;p&gt;In the &lt;a rel=&quot;nofollow&quot; href=&quot;https://datalaria.com/en/posts/weather-service-part-1/&quot;&gt;first part of this series&lt;/a&gt;, we set up the backbone of our global weather service, collecting raw data using Python and GitHub Actions. Then, in &lt;a rel=&quot;nofollow&quot; href=&quot;https://datalaria.com/en/posts/weather-service-part-2/&quot;&gt;Part 2&lt;/a&gt;, we transformed that raw data into a beautiful, interactive dashboard, leveraging GitHub Pages/Netlify, JavaScript, PapaParse.js, and Chart.js.&lt;/p&gt;
&lt;p&gt;Now, it's time for the grand finale: adding predictive power to our Weather Service. We'll explore how to augment our historical data visualization with actual forecasts. This installment focuses on a dual approach: integrating an official, reliable forecast from a professional service (OpenWeatherMap) and, more excitingly, building and training our very own simple AI model (Linear Regression) to predict tomorrow's weather based on the historical data we've meticulously collected. Finally, we'll visualize both forecasts on our dashboard, allowing for a direct comparison and a real-world test of our AI's accuracy.&lt;/p&gt;
&lt;p&gt;Let's turn our data into a crystal ball! &lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://datalaria.com/en/posts/app_openweather_part3_ai_prediction/AI_App_Weather_Image_Predictions.png&quot; alt=&quot;Conceptual image of Weather Service Predictions&quot;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;the-predictive-core-openweathermap-and-our-custom-ai&quot;&gt;The Predictive Core: OpenWeatherMap and Our Custom AI&lt;/h4&gt;
&lt;p&gt;The goal for this predictive functionality was twofold:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Official Forecast&lt;/strong&gt;: Obtain a reliable, multi-day forecast from a professional weather service (OpenWeatherMap - OWM).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Custom AI Prediction&lt;/strong&gt;: Create our own simple AI model (Linear Regression) trained on the historical data we've collected, to predict the next day's weather.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Visualization &amp;amp; Comparison&lt;/strong&gt;: Display and compare both forecasts to gauge the accuracy and performance of our custom AI model.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h4 id=&quot;1-backend-logic-read-weather-py-gets-smarter&quot;&gt;1. ⚙️ Backend Logic: &lt;code&gt;read_weather.py&lt;/code&gt; Gets Smarter&lt;/h4&gt;
&lt;p&gt;Our &lt;code&gt;read_weather.py&lt;/code&gt; script, previously responsible for data collection, now expands its role to gather data from both OWM and our historical archives, consolidating everything into a single &lt;code&gt;predicciones.json&lt;/code&gt; file.&lt;/p&gt;
&lt;h5 id=&quot;step-1-fetching-openweathermap-s-5-day-forecast&quot;&gt;Step 1: Fetching OpenWeatherMap's 5-Day Forecast&lt;/h5&gt;
&lt;p&gt;We decided that in addition to our 1-day AI prediction, a 5-day forecast from OWM would provide valuable context.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;API Endpoint&lt;/strong&gt;: We opted for the free &lt;code&gt;data/2.5/forecast&lt;/code&gt; API (since OneCall 3.0 required a payment method).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Data Processing&lt;/strong&gt;: This API returns data in 3-hour blocks. We had to add Python logic to:&lt;ul&gt;
&lt;li&gt;Iterate over the list of ~40 forecasts.&lt;/li&gt;
&lt;li&gt;Group them by day (ignoring the current day).&lt;/li&gt;
&lt;li&gt;For each of the next 5 days, calculate the maximum, minimum, and average temperature from all 3-hour blocks within that day.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Result&lt;/strong&gt;: A list of 5 objects (one per day) containing OWM's max, min, and average temperature predictions.&lt;/li&gt;
&lt;/ul&gt;
&lt;h5 id=&quot;step-2-implementing-our-ai-model-1-day-prediction-&quot;&gt;Step 2: Implementing Our AI Model (1-Day Prediction)&lt;/h5&gt;
&lt;p&gt;This is the core of our &quot;homemade AI.&quot; For each city:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Data Loading&lt;/strong&gt;: We used &lt;code&gt;pandas&lt;/code&gt; to read the city's historical CSV file (e.g., &lt;code&gt;datos/Madrid.csv&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Feature Engineering&lt;/strong&gt;: Since we had multiple readings per day, the most crucial step was transforming this data:&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Resampling&lt;/strong&gt;: We used &lt;code&gt;df.resample('D')&lt;/code&gt; from &lt;code&gt;pandas&lt;/code&gt; to group data by day, calculating the actual daily aggregates (e.g., &lt;code&gt;temp_max&lt;/code&gt;, &lt;code&gt;temp_min&lt;/code&gt;, &lt;code&gt;avg_temp&lt;/code&gt;, &lt;code&gt;avg_humidity&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Feature Creation (X)&lt;/strong&gt;: We created new &quot;shifted&quot; columns (&lt;code&gt;.shift(1)&lt;/code&gt;) so that each row (representing a day) contained the previous day's data (e.g., &lt;code&gt;temp_max_lag1&lt;/code&gt;, &lt;code&gt;avg_humidity_lag1&lt;/code&gt;). We also added &lt;code&gt;day_of_year&lt;/code&gt; to capture seasonality.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Target Creation (y)&lt;/strong&gt;: We defined what we wanted to predict (e.g., the actual &lt;code&gt;temp_max&lt;/code&gt; of the current day).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Training 3 Models&lt;/strong&gt;: Instead of one, we trained three independent Linear Regression models (&lt;code&gt;scikit-learn&lt;/code&gt;):&lt;ul&gt;
&lt;li&gt;&lt;code&gt;model_max&lt;/code&gt;: Trained with &lt;code&gt;y = df_clean['temp_max']&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;model_min&lt;/code&gt;: Trained with &lt;code&gt;y = df_clean['temp_min']&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;model_avg&lt;/code&gt;: Trained with &lt;code&gt;y = df_clean['avg_temp']&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Prediction&lt;/strong&gt;:&lt;ul&gt;
&lt;li&gt;We took the last row of aggregated data (representing &quot;today's&quot; data).&lt;/li&gt;
&lt;li&gt;Fed this data to the 3 models to predict &quot;tomorrow's&quot; values.&lt;/li&gt;
&lt;li&gt;We included a safeguard (&lt;code&gt;MIN_RECORDS_FOR_IA = 10&lt;/code&gt;) so the model only attempts to predict if it has sufficient historical data (e.g., 10 clean days).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h5 id=&quot;step-3-consolidate-and-save&quot;&gt;Step 3: Consolidate and Save&lt;/h5&gt;
&lt;p&gt;The script combines the results from Steps 1 and 2 into a JSON structure and saves it to &lt;code&gt;predicciones.json&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;Madrid&quot;: {
    &quot;pred_owm_5day&quot;: [ 
      { &quot;date&quot;: &quot;...&quot;, &quot;max&quot;: 15.0, &quot;min&quot;: 10.0, &quot;avg&quot;: 12.5 }, 
      ... (5 days) ...
    ],
    &quot;pred_ia&quot;: {
      &quot;max&quot;: 14.8,
      &quot;min&quot;: 7.5,
      &quot;avg&quot;: 11.2,
      &quot;records&quot;: 120
    }
  },
  &quot;A Coruña&quot;: {
      ...
      &quot;pred_ia&quot;: { &quot;max&quot;: null, &quot;min&quot;: null, &quot;avg&quot;: null, &quot;records&quot;: 9 } // Example of insufficient data
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h4 id=&quot;2-frontend-logic-index-html-visualizes-the-future&quot;&gt;2.  Frontend Logic: &lt;code&gt;index.html&lt;/code&gt; Visualizes the Future&lt;/h4&gt;
&lt;p&gt;The frontend is responsible for loading this &lt;code&gt;predicciones.json&lt;/code&gt; file and presenting it in a visually appealing and informative way.&lt;/p&gt;
&lt;h5 id=&quot;step-1-data-loading&quot;&gt;Step 1: Data Loading&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;loadPredictions()&lt;/code&gt;: We created a new &lt;code&gt;async&lt;/code&gt; function that runs once during initialization (before &lt;code&gt;updateDashboard&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;allPredictionsCache&lt;/code&gt;: This function loads &lt;code&gt;predicciones.json&lt;/code&gt; and saves it into this new global variable so that all visualization functions have access to it.&lt;/li&gt;
&lt;/ul&gt;
&lt;h5 id=&quot;step-2-visualization-in-the-super-cards-kpis-&quot;&gt;Step 2: Visualization in the &quot;Super-Cards&quot; (KPIs)&lt;/h5&gt;
&lt;p&gt;We wanted a direct and clear comparison.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;OWM 5-Day Forecast&lt;/strong&gt;:&lt;ul&gt;
&lt;li&gt;We created a helper function &lt;code&gt;buildForecastHTML()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;This function takes the &lt;code&gt;pred_owm_5day&lt;/code&gt; list and generates an HTML block with a list of the 5 days and their max/min temperatures (e.g., &quot;Sat, Nov 9: 15.1°C / 10.0°C&quot;).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AI 1-Day Forecast (Comparison)&lt;/strong&gt;:&lt;ul&gt;
&lt;li&gt;We created a second helper function &lt;code&gt;buildIAForecastHTML()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;This function takes the &lt;code&gt;pred_ia&lt;/code&gt; object and the first day of the OWM forecast (&lt;code&gt;pred_owm_5day[0]&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Comparison Logic&lt;/strong&gt;: For max, min, and average temperatures, it displays the AI's value and then, next to it, the difference from OWM.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Visual Impact&lt;/strong&gt;: The difference is colored red (if our AI predicts warmer) or blue (if it predicts cooler), giving us an immediate visual cue of our model's deviation.&lt;/li&gt;
&lt;li&gt;It also handles the &quot;Insufficient Data&quot; case (&lt;code&gt;${ia_preds.records}/${MIN_RECORDS_FOR_IA}&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;updateKPIs()&lt;/code&gt;: The card template was modified to call these two new functions, displaying both forecast blocks.&lt;/li&gt;
&lt;/ul&gt;
&lt;h5 id=&quot;step-3-visualization-in-the-charts&quot;&gt;Step 3: Visualization in the Charts&lt;/h5&gt;
&lt;p&gt;We wanted the forecasts to be integrated directly into the existing charts.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Evolution Chart (Dotted Line)&lt;/strong&gt;:&lt;ul&gt;
&lt;li&gt;In &lt;code&gt;updateChart()&lt;/code&gt;, we added a new dataset for each city.&lt;/li&gt;
&lt;li&gt;This dataset uses the average of the OWM prediction (&lt;code&gt;pred_owm_5day&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;We applied the style &lt;code&gt;borderDash: [5, 5]&lt;/code&gt; to draw it as a dotted line.&lt;/li&gt;
&lt;li&gt;We &quot;stitched&quot; the beginning of this line to the last real data point to make it appear as a seamless continuation.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Variation Chart (Striped Bars)&lt;/strong&gt;:&lt;ul&gt;
&lt;li&gt;In &lt;code&gt;updateVariationChart()&lt;/code&gt;, we added another dataset for each city.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;y&lt;/code&gt; data for this set is &lt;code&gt;day.max - day.min&lt;/code&gt; (the variation) from the OWM forecast.&lt;/li&gt;
&lt;li&gt;For styling, we created a helper function &lt;code&gt;createStripedPattern()&lt;/code&gt; that draws a striped pattern on a canvas.&lt;/li&gt;
&lt;li&gt;We used this pattern as the &lt;code&gt;backgroundColor&lt;/code&gt; for the forecast bars, differentiating them from the solid bars of real data.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h4 id=&quot;conclusion-part-3-&quot;&gt;Conclusion (Part 3)&lt;/h4&gt;
&lt;p&gt;With this final installment, our Weather Service project is complete! We've successfully integrated both professional 5-day forecasts from OpenWeatherMap and a custom 1-day AI prediction model, all powered by our collected historical data. The frontend now provides a rich, interactive experience that not only visualizes past weather but also offers a glimpse into the future, complete with a comparative analysis of our AI's performance.&lt;/p&gt;
&lt;p&gt;This journey has covered everything from backend data collection, automation with GitHub Actions, static site hosting with Netlify, to dynamic frontend development with vanilla JavaScript, advanced data parsing with PapaParse.js, interactive charting with Chart.js, and finally, dipping our toes into Machine Learning for predictive analytics.&lt;/p&gt;
&lt;p&gt;We've built a robust, serverless, and insightful application entirely on free services. The possibilities for expansion (e.g., more complex ML models, different data sources, user accounts) are endless, but for now, we have a fully functional weather oracle!&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;references-and-links-of-interest-&quot;&gt;References and Links of Interest:&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Complete Web Service&lt;/strong&gt;: You can see the final project in action here: &lt;a rel=&quot;nofollow&quot; href=&quot;https://datalaria.com/apps/weather/&quot;&gt;https://datalaria.com/apps/weather/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Project GitHub Repository&lt;/strong&gt;: Explore the source code and project structure in my repository: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Dalaez/app_weather&quot;&gt;https://github.com/Dalaez/app_weather&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;OpenWeatherMap API&lt;/strong&gt;: &lt;a rel=&quot;nofollow&quot; href=&quot;https://openweathermap.org/api&quot;&gt;https://openweathermap.org/api&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pandas&lt;/strong&gt;: Python Data Analysis Library: &lt;a rel=&quot;nofollow&quot; href=&quot;https://pandas.pydata.org/&quot;&gt;https://pandas.pydata.org/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Scikit-learn&lt;/strong&gt;: Machine Learning in Python: &lt;a rel=&quot;nofollow&quot; href=&quot;https://scikit-learn.org/&quot;&gt;https://scikit-learn.org/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PapaParse.js&lt;/strong&gt;: Fast in-browser CSV parser for JavaScript: &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.papaparse.com/&quot;&gt;https://www.papaparse.com/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/10251/weather-service-project-part-3-predicting-the-future-with-ai-and-openweathermap</guid>
<pubDate>Thu, 22 Jan 2026 06:49:58 +0000</pubDate>
</item>
</channel>
</rss>