In [1]:
#This is for jupyter notebook
from IPython.display import display, Markdown

def print_header(text, level=2):
    display(Markdown(f"{'#'*level} {text}"))

Predicting Tomorrow's Weather From Yesterday's Data¶

This notebook builds a simple weather forecaster for Basel, Switzerland. It looks at 10 years of daily weather readings from 18 European cities and learns the patterns that go with hot/cold, humid/dry, and high/low pressure days in Basel. It then uses that learning to guess temperature, humidity, and pressure it has never seen before, and checks how close the guesses were.

Step 1: Get Our Tools Ready¶

In [2]:
%pip install pandas numpy matplotlib seaborn scikit-learn
Requirement already satisfied: pandas in /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/site-packages (3.0.5)
Requirement already satisfied: numpy in /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/site-packages (2.4.6)
Requirement already satisfied: matplotlib in /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/site-packages (3.11.1)
Requirement already satisfied: seaborn in /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/site-packages (0.13.2)
Requirement already satisfied: scikit-learn in /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/site-packages (1.9.0)
Requirement already satisfied: python-dateutil>=2.8.2 in /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/site-packages (from pandas) (2.9.0.post0)
Requirement already satisfied: contourpy>=1.0.1 in /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/site-packages (from matplotlib) (1.3.3)
Requirement already satisfied: cycler>=0.10 in /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/site-packages (from matplotlib) (0.12.1)
Requirement already satisfied: fonttools>=4.28.2 in /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/site-packages (from matplotlib) (4.63.0)
Requirement already satisfied: kiwisolver>=1.3.1 in /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/site-packages (from matplotlib) (1.5.0)
Requirement already satisfied: packaging>=20.0 in /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/site-packages (from matplotlib) (26.3)
Requirement already satisfied: pillow>=9 in /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/site-packages (from matplotlib) (12.3.0)
Requirement already satisfied: pyparsing>=3 in /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/site-packages (from matplotlib) (3.3.2)
Requirement already satisfied: scipy>=1.10.0 in /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/site-packages (from scikit-learn) (1.17.1)
Requirement already satisfied: joblib>=1.4.0 in /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/site-packages (from scikit-learn) (1.5.3)
Requirement already satisfied: narwhals>=2.0.1 in /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/site-packages (from scikit-learn) (2.24.0)
Requirement already satisfied: threadpoolctl>=3.5.0 in /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/site-packages (from scikit-learn) (3.6.0)
Requirement already satisfied: six>=1.5 in /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/site-packages (from python-dateutil>=2.8.2->pandas) (1.17.0)
Note: you may need to restart the kernel to use updated packages.

We use four free, well-known Python toolkits:

  • pandas — opens and organizes the weather data like a spreadsheet
  • numpy — does the number-crunching underneath
  • matplotlib / seaborn — draws the charts
  • scikit-learn — contains the actual prediction ("machine learning") algorithm

None of this is exotic — it's the standard starter kit for any data analysis in Python.

Step 2: Load the Weather Data¶

The dataset comes from Zenodo, a public research data archive, and holds one row per day from 2000–2010, with weather readings (temperature, humidity, pressure, wind, sunshine, etc.) for 18 European cities side by side.

In [3]:
import pandas as pd

# URL of Dataset on Zenodo, across locations in Europe
url = 'https://zenodo.org/records/4770937/files/weather_prediction_dataset.csv'

# Load Dataset
df = pd.read_csv(url)

# Display basic information about dataset
df.info()
<class 'pandas.DataFrame'>
RangeIndex: 3654 entries, 0 to 3653
Columns: 165 entries, DATE to TOURS_temp_max
dtypes: float64(150), int64(15)
memory usage: 4.6 MB
In [4]:
# Display Preview and Summary of Dataset

print_header("Dataset Preview")
display(df.head().style.set_caption("First 5 Rows").background_gradient(cmap='Blues'))

print_header("Dataset Summary")
display(df.describe().T.style.bar(color='#5fba7d'))

Dataset Preview¶

First 5 Rows
  DATE MONTH BASEL_cloud_cover BASEL_humidity BASEL_pressure BASEL_global_radiation BASEL_precipitation BASEL_sunshine BASEL_temp_mean BASEL_temp_min BASEL_temp_max BUDAPEST_cloud_cover BUDAPEST_humidity BUDAPEST_pressure BUDAPEST_global_radiation BUDAPEST_precipitation BUDAPEST_sunshine BUDAPEST_temp_mean BUDAPEST_temp_max DE_BILT_cloud_cover DE_BILT_wind_speed DE_BILT_wind_gust DE_BILT_humidity DE_BILT_pressure DE_BILT_global_radiation DE_BILT_precipitation DE_BILT_sunshine DE_BILT_temp_mean DE_BILT_temp_min DE_BILT_temp_max DRESDEN_cloud_cover DRESDEN_wind_speed DRESDEN_wind_gust DRESDEN_humidity DRESDEN_global_radiation DRESDEN_precipitation DRESDEN_sunshine DRESDEN_temp_mean DRESDEN_temp_min DRESDEN_temp_max DUSSELDORF_cloud_cover DUSSELDORF_wind_speed DUSSELDORF_wind_gust DUSSELDORF_humidity DUSSELDORF_pressure DUSSELDORF_global_radiation DUSSELDORF_precipitation DUSSELDORF_sunshine DUSSELDORF_temp_mean DUSSELDORF_temp_min DUSSELDORF_temp_max HEATHROW_cloud_cover HEATHROW_humidity HEATHROW_pressure HEATHROW_global_radiation HEATHROW_precipitation HEATHROW_sunshine HEATHROW_temp_mean HEATHROW_temp_min HEATHROW_temp_max KASSEL_wind_speed KASSEL_wind_gust KASSEL_humidity KASSEL_pressure KASSEL_global_radiation KASSEL_precipitation KASSEL_sunshine KASSEL_temp_mean KASSEL_temp_min KASSEL_temp_max LJUBLJANA_cloud_cover LJUBLJANA_wind_speed LJUBLJANA_humidity LJUBLJANA_pressure LJUBLJANA_global_radiation LJUBLJANA_precipitation LJUBLJANA_sunshine LJUBLJANA_temp_mean LJUBLJANA_temp_min LJUBLJANA_temp_max MAASTRICHT_cloud_cover MAASTRICHT_wind_speed MAASTRICHT_wind_gust MAASTRICHT_humidity MAASTRICHT_pressure MAASTRICHT_global_radiation MAASTRICHT_precipitation MAASTRICHT_sunshine MAASTRICHT_temp_mean MAASTRICHT_temp_min MAASTRICHT_temp_max MALMO_wind_speed MALMO_precipitation MALMO_temp_mean MALMO_temp_min MALMO_temp_max MONTELIMAR_wind_speed MONTELIMAR_humidity MONTELIMAR_pressure MONTELIMAR_global_radiation MONTELIMAR_precipitation MONTELIMAR_temp_mean MONTELIMAR_temp_min MONTELIMAR_temp_max MUENCHEN_cloud_cover MUENCHEN_wind_speed MUENCHEN_wind_gust MUENCHEN_humidity MUENCHEN_pressure MUENCHEN_global_radiation MUENCHEN_precipitation MUENCHEN_sunshine MUENCHEN_temp_mean MUENCHEN_temp_min MUENCHEN_temp_max OSLO_cloud_cover OSLO_wind_speed OSLO_wind_gust OSLO_humidity OSLO_pressure OSLO_global_radiation OSLO_precipitation OSLO_sunshine OSLO_temp_mean OSLO_temp_min OSLO_temp_max PERPIGNAN_wind_speed PERPIGNAN_humidity PERPIGNAN_pressure PERPIGNAN_global_radiation PERPIGNAN_precipitation PERPIGNAN_temp_mean PERPIGNAN_temp_min PERPIGNAN_temp_max ROMA_cloud_cover ROMA_humidity ROMA_pressure ROMA_global_radiation ROMA_sunshine ROMA_temp_mean ROMA_temp_min ROMA_temp_max SONNBLICK_cloud_cover SONNBLICK_humidity SONNBLICK_global_radiation SONNBLICK_precipitation SONNBLICK_sunshine SONNBLICK_temp_mean SONNBLICK_temp_min SONNBLICK_temp_max STOCKHOLM_cloud_cover STOCKHOLM_pressure STOCKHOLM_precipitation STOCKHOLM_sunshine STOCKHOLM_temp_mean STOCKHOLM_temp_min STOCKHOLM_temp_max TOURS_wind_speed TOURS_humidity TOURS_pressure TOURS_global_radiation TOURS_precipitation TOURS_temp_mean TOURS_temp_min TOURS_temp_max
0 20000101 1 8 0.890000 1.028600 0.200000 0.030000 0.000000 2.900000 1.600000 3.900000 3 0.920000 1.026800 0.520000 0.000000 3.700000 -4.900000 -0.700000 7 2.500000 8.000000 0.970000 1.024000 0.110000 0.100000 0.000000 6.100000 3.500000 8.100000 8 3.200000 7.200000 0.890000 0.090000 0.320000 0.000000 1.000000 -1.800000 2.000000 8 2.500000 5.900000 0.920000 1.024000 0.120000 0.220000 0.000000 4.200000 2.500000 6.900000 7 0.940000 1.024500 0.180000 0.000000 0.400000 7.000000 4.900000 10.800000 2.500000 8.200000 0.930000 1.023700 0.060000 0.130000 0.000000 3.500000 1.500000 5.000000 6 0.400000 0.830000 1.029400 0.570000 0.000000 5.200000 -4.800000 -9.100000 -1.300000 8 3.100000 7.000000 0.980000 1.025100 0.060000 0.170000 0.000000 5.600000 4.100000 6.900000 2.500000 0.270000 2.900000 0.900000 3.600000 3.800000 0.850000 1.026900 0.300000 0.000000 5.500000 2.500000 8.500000 8 2.600000 9.400000 0.910000 1.027300 0.200000 0.200000 0.000000 1.700000 -0.500000 2.600000 7 0.900000 5.100000 0.940000 1.013000 0.040000 0.600000 0.000000 -5.000000 -8.600000 -3.200000 4.400000 0.710000 1.026700 0.600000 0.000000 12.200000 10.300000 14.000000 0 0.720000 1.024400 0.920000 8.400000 1.600000 3.000000 8.000000 7 0.890000 0.820000 1.340000 0.000000 -15.200000 -17.000000 -13.400000 8 1.016300 0.170000 0.000000 -2.300000 -9.300000 0.700000 1.600000 0.970000 1.027500 0.250000 0.040000 8.500000 7.200000 9.800000
1 20000102 1 8 0.870000 1.031800 0.250000 0.000000 0.000000 3.600000 2.700000 4.800000 8 0.940000 1.029700 0.140000 0.000000 0.400000 -3.600000 -1.900000 8 3.700000 9.000000 0.970000 1.026700 0.110000 0.000000 0.000000 7.300000 5.400000 8.700000 7 4.000000 8.800000 0.890000 0.230000 0.000000 0.400000 2.500000 1.400000 4.000000 6 3.000000 7.400000 0.870000 1.028300 0.190000 0.000000 0.700000 6.500000 2.700000 7.900000 7 0.890000 1.025300 0.200000 0.020000 0.700000 7.900000 5.000000 11.500000 2.900000 9.600000 0.920000 1.029000 0.330000 0.000000 2.900000 2.300000 0.300000 4.700000 6 0.400000 0.760000 1.031000 0.590000 0.000000 5.000000 -0.900000 -4.900000 2.000000 7 3.800000 9.000000 0.950000 1.028600 0.140000 0.000000 0.000000 6.200000 4.200000 7.500000 3.800000 0.000000 3.700000 1.000000 5.400000 5.800000 0.820000 1.028700 0.540000 0.000000 8.300000 6.800000 9.800000 6 2.100000 8.200000 0.900000 1.032100 0.660000 0.000000 6.100000 1.900000 -0.200000 5.800000 6 1.900000 5.700000 0.940000 1.007600 0.110000 0.000000 1.600000 -0.800000 -6.700000 2.400000 2.900000 0.670000 1.027800 0.960000 0.000000 9.800000 5.100000 14.600000 2 0.740000 1.026300 0.810000 6.500000 4.200000 0.000000 8.400000 5 0.860000 0.600000 0.390000 2.800000 -13.700000 -15.000000 -12.300000 8 1.010800 0.200000 0.000000 1.300000 0.500000 2.000000 2.000000 0.990000 1.029300 0.170000 0.160000 7.900000 6.600000 9.200000
2 20000103 1 5 0.810000 1.031400 0.500000 0.000000 3.700000 2.200000 0.100000 4.800000 6 0.950000 1.029500 0.190000 0.000000 0.000000 -0.800000 1.100000 8 6.100000 13.000000 0.940000 1.020300 0.110000 0.450000 0.000000 8.400000 6.400000 9.600000 7 5.400000 12.100000 0.790000 0.180000 0.000000 0.000000 4.200000 1.300000 5.100000 7 5.500000 14.300000 0.780000 1.023500 0.120000 0.280000 0.000000 7.700000 6.900000 9.100000 8 0.910000 1.018600 0.130000 0.600000 0.000000 9.400000 7.200000 9.500000 4.800000 11.900000 0.900000 1.025100 0.200000 0.010000 0.000000 3.500000 2.200000 4.600000 6 0.300000 0.830000 1.030900 0.510000 0.000000 2.400000 -0.300000 -1.800000 3.300000 7 7.400000 14.000000 0.870000 1.023600 0.150000 0.020000 0.900000 6.800000 6.100000 7.900000 4.300000 0.060000 5.600000 4.000000 6.900000 0.400000 0.920000 1.031600 0.530000 0.000000 3.200000 -2.100000 8.500000 7 2.100000 6.900000 0.920000 1.031700 0.280000 0.000000 0.400000 -0.400000 -3.300000 0.900000 6 1.700000 8.700000 0.880000 1.001600 0.040000 0.000000 0.000000 1.200000 -1.100000 3.800000 2.500000 0.850000 1.028800 0.930000 0.000000 8.600000 4.100000 13.200000 0 0.770000 1.028800 0.890000 0.000000 3.800000 11.100000 21.100000 3 0.410000 0.810000 0.000000 5.100000 -9.200000 -12.500000 -5.800000 7 1.007100 0.080000 1.800000 0.800000 -1.000000 2.800000 3.400000 0.910000 1.026700 0.270000 0.000000 8.100000 6.600000 9.600000
3 20000104 1 7 0.790000 1.026200 0.630000 0.350000 6.900000 3.900000 0.500000 7.500000 8 0.940000 1.025200 0.210000 0.000000 0.000000 -1.000000 0.100000 7 3.800000 15.000000 0.940000 1.014200 0.110000 1.090000 0.000000 6.400000 4.300000 9.400000 8 6.000000 14.400000 0.880000 0.110000 0.220000 0.000000 4.400000 3.400000 5.200000 7 6.000000 16.800000 0.870000 1.016200 0.120000 0.970000 0.000000 7.800000 6.600000 9.200000 5 0.890000 1.014800 0.340000 0.020000 2.900000 7.000000 4.400000 11.000000 4.500000 12.700000 0.940000 1.017400 0.060000 0.440000 0.000000 4.800000 3.500000 5.600000 2 0.400000 0.880000 1.026200 0.700000 0.000000 3.500000 -3.600000 -6.100000 0.400000 8 7.200000 15.000000 0.920000 1.016500 0.070000 1.330000 0.000000 7.300000 6.100000 9.000000 3.900000 0.750000 4.500000 3.000000 6.400000 1.100000 0.850000 1.027400 0.640000 0.000000 7.200000 2.300000 12.100000 6 2.700000 11.700000 0.750000 1.026000 0.580000 0.040000 4.500000 3.800000 -2.800000 6.600000 1 3.400000 11.800000 0.580000 0.998200 0.130000 0.000000 5.300000 2.100000 -0.500000 5.100000 1.500000 0.850000 1.026900 0.560000 0.020000 8.600000 4.300000 12.800000 1 0.850000 1.027300 0.890000 8.200000 6.000000 2.000000 10.000000 1 0.250000 1.050000 0.110000 8.700000 -5.600000 -7.000000 -4.200000 2 0.994700 0.000000 5.000000 3.500000 2.500000 4.600000 4.900000 0.950000 1.022200 0.110000 0.440000 8.600000 6.400000 10.800000
4 20000105 1 5 0.900000 1.024600 0.510000 0.070000 3.700000 6.000000 3.800000 8.600000 5 0.880000 1.023500 0.430000 0.000000 0.800000 0.200000 3.900000 3 4.000000 12.000000 0.900000 1.018300 0.480000 0.000000 6.500000 4.400000 1.400000 7.400000 2 5.600000 15.800000 0.760000 0.490000 0.000000 5.700000 1.800000 -0.500000 6.900000 4 4.500000 11.200000 0.800000 1.020300 0.510000 0.000000 6.500000 5.200000 0.400000 8.600000 5 0.850000 1.014200 0.250000 0.080000 1.300000 6.400000 1.900000 10.800000 2.400000 8.800000 0.840000 1.021000 0.480000 0.000000 6.700000 2.300000 0.200000 6.300000 4 0.600000 0.850000 1.027100 0.570000 0.000000 4.600000 -3.000000 -6.100000 1.100000 4 4.100000 10.000000 0.870000 1.020500 0.440000 0.000000 6.200000 5.200000 0.600000 8.400000 3.200000 0.030000 3.800000 2.500000 5.500000 3.400000 0.820000 1.023400 0.700000 0.000000 8.200000 1.500000 14.800000 5 3.300000 13.200000 0.870000 1.024800 0.260000 0.000000 0.200000 5.300000 4.300000 7.300000 8 1.200000 5.700000 0.940000 1.005500 0.050000 0.060000 0.000000 -0.700000 -4.000000 0.500000 2.600000 0.740000 1.021900 0.830000 0.020000 9.200000 3.600000 14.900000 2 0.920000 1.023800 0.740000 7.500000 5.000000 -1.200000 11.200000 4 0.770000 0.690000 0.170000 3.400000 -7.600000 -9.400000 -5.800000 5 1.007200 0.000000 2.200000 -0.600000 -1.800000 2.900000 3.600000 0.950000 1.020900 0.390000 0.040000 8.000000 6.400000 9.500000

Dataset Summary¶

  count mean std min 25% 50% 75% max
DATE 3654.000000 20045678.754242 28742.871733 20000101.000000 20020702.250000 20045666.000000 20070702.750000 20100101.000000
MONTH 3654.000000 6.520799 3.450083 1.000000 4.000000 7.000000 10.000000 12.000000
BASEL_cloud_cover 3654.000000 5.418446 2.325497 0.000000 4.000000 6.000000 7.000000 8.000000
BASEL_humidity 3654.000000 0.745107 0.107788 0.380000 0.670000 0.760000 0.830000 0.980000
BASEL_pressure 3654.000000 1.017876 0.007962 0.985600 1.013300 1.017700 1.022700 1.040800
BASEL_global_radiation 3654.000000 1.330380 0.935348 0.050000 0.530000 1.110000 2.060000 3.550000
BASEL_precipitation 3654.000000 0.234849 0.536267 0.000000 0.000000 0.000000 0.210000 7.570000
BASEL_sunshine 3654.000000 4.661193 4.330112 0.000000 0.500000 3.600000 8.000000 15.300000
BASEL_temp_mean 3654.000000 11.022797 7.414754 -9.300000 5.300000 11.400000 16.900000 29.000000
BASEL_temp_min 3654.000000 6.989135 6.653356 -16.000000 2.000000 7.300000 12.400000 20.800000
BASEL_temp_max 3654.000000 15.536782 8.721323 -5.700000 8.700000 15.800000 22.300000 38.600000
BUDAPEST_cloud_cover 3654.000000 4.890531 2.386442 0.000000 3.000000 5.000000 7.000000 8.000000
BUDAPEST_humidity 3654.000000 0.656505 0.149603 0.260000 0.540000 0.650000 0.770000 1.000000
BUDAPEST_pressure 3654.000000 1.016935 0.007795 0.989100 1.012100 1.016500 1.021475 1.043800
BUDAPEST_global_radiation 3654.000000 1.465487 0.977986 0.040000 0.580000 1.340000 2.310000 3.490000
BUDAPEST_precipitation 3654.000000 0.136442 0.408932 0.000000 0.000000 0.000000 0.030000 6.960000
BUDAPEST_sunshine 3654.000000 5.753229 4.475439 0.000000 1.100000 5.900000 9.600000 14.900000
BUDAPEST_temp_mean 3654.000000 12.174849 8.744451 -9.800000 5.100000 12.800000 19.300000 33.100000
BUDAPEST_temp_max 3654.000000 16.629091 9.981538 -6.600000 8.400000 17.400000 25.000000 40.100000
DE_BILT_cloud_cover 3654.000000 5.303229 2.279416 0.000000 4.000000 6.000000 7.000000 8.000000
DE_BILT_wind_speed 3654.000000 3.395293 1.422020 0.700000 2.300000 3.200000 4.200000 10.300000
DE_BILT_wind_gust 3654.000000 9.986316 3.582408 2.000000 7.000000 10.000000 12.000000 28.000000
DE_BILT_humidity 3654.000000 0.817882 0.097465 0.370000 0.760000 0.830000 0.890000 1.000000
DE_BILT_pressure 3654.000000 1.015299 0.009861 0.973200 1.009400 1.015700 1.021700 1.044900
DE_BILT_global_radiation 3654.000000 1.190903 0.870267 0.110000 0.410000 1.020000 1.860000 3.410000
DE_BILT_precipitation 3654.000000 0.236888 0.459495 0.000000 0.000000 0.010000 0.290000 4.250000
DE_BILT_sunshine 3654.000000 4.744444 3.995637 0.000000 1.100000 4.100000 7.500000 15.500000
DE_BILT_temp_mean 3654.000000 10.703530 6.190770 -7.900000 6.200000 11.000000 15.500000 26.900000
DE_BILT_temp_min 3654.000000 6.397099 5.639597 -14.400000 2.300000 6.800000 10.800000 20.800000
DE_BILT_temp_max 3654.000000 14.798604 7.210740 -4.700000 9.200000 14.900000 20.200000 35.700000
DRESDEN_cloud_cover 3654.000000 5.405036 2.194769 0.000000 4.000000 6.000000 7.000000 8.000000
DRESDEN_wind_speed 3654.000000 4.256924 1.775045 1.000000 2.900000 3.900000 5.200000 12.200000
DRESDEN_wind_gust 3654.000000 10.924576 4.031649 2.900000 8.000000 10.300000 13.200000 34.300000
DRESDEN_humidity 3654.000000 0.759023 0.132420 0.320000 0.670000 0.770000 0.860000 1.000000
DRESDEN_global_radiation 3654.000000 1.263432 0.936443 0.030000 0.440000 1.090000 1.980000 3.660000
DRESDEN_precipitation 3654.000000 0.175881 0.459725 0.000000 0.000000 0.000000 0.170000 15.800000
DRESDEN_sunshine 3654.000000 4.815736 4.426682 0.000000 0.600000 3.900000 8.200000 15.800000
DRESDEN_temp_mean 3654.000000 9.800629 7.854752 -16.300000 3.700000 10.200000 16.100000 30.400000
DRESDEN_temp_min 3654.000000 5.924056 6.934514 -20.400000 0.800000 6.300000 11.500000 23.500000
DRESDEN_temp_max 3654.000000 13.671346 9.038833 -13.600000 6.300000 13.900000 20.900000 36.400000
DUSSELDORF_cloud_cover 3654.000000 5.141762 2.115639 0.000000 4.000000 6.000000 7.000000 8.000000
DUSSELDORF_wind_speed 3654.000000 3.963738 1.718106 1.000000 2.600000 3.700000 5.000000 12.200000
DUSSELDORF_wind_gust 3654.000000 10.591680 3.884296 2.800000 7.800000 10.100000 12.700000 40.300000
DUSSELDORF_humidity 3654.000000 0.755744 0.111595 0.260000 0.690000 0.770000 0.840000 1.000000
DUSSELDORF_pressure 3654.000000 1.016000 0.009302 0.975900 1.010400 1.016200 1.021800 1.045000
DUSSELDORF_global_radiation 3654.000000 1.147362 0.880692 0.110000 0.380000 0.920000 1.780000 3.490000
DUSSELDORF_precipitation 3654.000000 0.218043 0.439578 0.000000 0.000000 0.010000 0.250000 5.740000
DUSSELDORF_sunshine 3654.000000 4.324111 4.209463 0.000000 0.400000 3.200000 7.300000 16.000000
DUSSELDORF_temp_mean 3654.000000 11.142009 6.689373 -11.100000 6.125000 11.500000 16.200000 29.200000
DUSSELDORF_temp_min 3654.000000 6.865736 6.150650 -19.900000 2.500000 7.300000 11.600000 21.500000
DUSSELDORF_temp_max 3654.000000 15.312014 7.778961 -8.500000 9.300000 15.400000 21.200000 38.500000
HEATHROW_cloud_cover 3654.000000 5.272031 2.011846 0.000000 4.000000 6.000000 7.000000 8.000000
HEATHROW_humidity 3654.000000 0.758358 0.102410 0.420000 0.690000 0.760000 0.840000 1.000000
HEATHROW_pressure 3654.000000 1.015192 0.010561 0.971500 1.009000 1.016000 1.022100 1.043800
HEATHROW_global_radiation 3654.000000 1.196970 0.881638 0.120000 0.430000 0.960000 1.860000 3.490000
HEATHROW_precipitation 3654.000000 0.178279 0.367572 0.000000 0.000000 0.020000 0.180000 3.660000
HEATHROW_sunshine 3654.000000 4.433498 3.982646 0.000000 0.600000 3.700000 7.200000 15.500000
HEATHROW_temp_mean 3654.000000 11.822386 5.610018 -2.200000 7.600000 11.700000 16.300000 29.000000
HEATHROW_temp_min 3654.000000 8.002737 5.230449 -6.800000 4.100000 8.250000 12.100000 20.600000
HEATHROW_temp_max 3654.000000 15.637438 6.385440 0.200000 10.800000 15.400000 20.500000 37.900000
KASSEL_wind_speed 3654.000000 2.478079 0.999386 0.000000 1.700000 2.300000 3.000000 7.600000
KASSEL_wind_gust 3654.000000 9.329557 3.373451 2.100000 6.900000 8.900000 11.200000 41.000000
KASSEL_humidity 3654.000000 0.785200 0.120909 0.340000 0.710000 0.800000 0.880000 1.000000
KASSEL_pressure 3654.000000 1.016373 0.009107 0.978600 1.010800 1.016500 1.022200 1.045900
KASSEL_global_radiation 3654.000000 1.183087 0.882655 0.030000 0.390000 1.020000 1.830000 3.470000
KASSEL_precipitation 3654.000000 0.202211 0.407147 0.000000 0.000000 0.010000 0.210000 5.420000
KASSEL_sunshine 3654.000000 4.136836 4.159699 0.000000 0.300000 2.900000 7.000000 15.000000
KASSEL_temp_mean 3654.000000 9.581007 7.203922 -14.500000 4.100000 9.800000 15.300000 28.400000
KASSEL_temp_min 3654.000000 5.586864 6.349421 -19.000000 0.900000 5.900000 10.600000 21.100000
KASSEL_temp_max 3654.000000 13.821702 8.546460 -12.100000 7.100000 13.900000 20.500000 36.700000
LJUBLJANA_cloud_cover 3654.000000 4.930213 2.367843 0.000000 3.000000 5.000000 7.000000 8.000000
LJUBLJANA_wind_speed 3654.000000 1.301423 0.629852 0.100000 0.800000 1.200000 1.600000 5.100000
LJUBLJANA_humidity 3654.000000 0.743013 0.137274 0.360000 0.640000 0.750000 0.860000 0.990000
LJUBLJANA_pressure 3654.000000 1.017947 0.007704 0.983300 1.013200 1.017500 1.022500 1.043700
LJUBLJANA_global_radiation 3654.000000 1.414031 1.000020 0.040000 0.530000 1.190000 2.270000 3.550000
LJUBLJANA_precipitation 3654.000000 0.367263 0.916321 0.000000 0.000000 0.000000 0.207500 8.640000
LJUBLJANA_sunshine 3654.000000 5.412397 4.507394 0.000000 0.600000 5.200000 9.000000 15.000000
LJUBLJANA_temp_mean 3654.000000 11.511604 8.250707 -10.800000 4.900000 11.800000 18.100000 28.400000
LJUBLJANA_temp_min 3654.000000 7.071757 7.355434 -16.200000 1.000000 7.800000 13.200000 21.500000
LJUBLJANA_temp_max 3654.000000 16.352053 9.509272 -7.500000 8.700000 16.800000 24.000000 37.300000
MAASTRICHT_cloud_cover 3654.000000 5.337712 2.401823 0.000000 4.000000 6.000000 7.000000 8.000000
MAASTRICHT_wind_speed 3654.000000 4.205720 1.883268 1.000000 2.800000 3.800000 5.300000 12.300000
MAASTRICHT_wind_gust 3654.000000 10.729338 4.069453 3.000000 8.000000 10.000000 13.000000 31.000000
MAASTRICHT_humidity 3654.000000 0.792003 0.110260 0.370000 0.720000 0.810000 0.880000 1.000000
MAASTRICHT_pressure 3654.000000 1.016035 0.009313 0.975700 1.010500 1.016300 1.022000 1.043400
MAASTRICHT_global_radiation 3654.000000 1.193410 0.902938 0.030000 0.400000 1.010000 1.860000 3.520000
MAASTRICHT_precipitation 3654.000000 0.220649 0.444137 0.000000 0.000000 0.010000 0.230000 5.870000
MAASTRICHT_sunshine 3654.000000 4.652354 4.015278 0.000000 1.000000 3.950000 7.400000 15.200000
MAASTRICHT_temp_mean 3654.000000 10.840230 6.604143 -12.100000 6.000000 11.200000 15.900000 28.800000
MAASTRICHT_temp_min 3654.000000 6.854871 5.954737 -16.200000 2.600000 7.200000 11.500000 21.300000
MAASTRICHT_temp_max 3654.000000 14.805939 7.653391 -7.800000 8.900000 15.000000 20.600000 36.300000
MALMO_wind_speed 3654.000000 2.918035 1.534168 0.000000 1.800000 2.700000 3.800000 9.500000
MALMO_precipitation 3654.000000 0.166732 0.395186 0.000000 0.000000 0.000000 0.150000 7.690000
MALMO_temp_mean 3654.000000 9.164970 6.897853 -13.800000 3.800000 9.150000 15.000000 24.700000
MALMO_temp_min 3654.000000 5.663246 6.477001 -19.500000 0.925000 5.800000 11.100000 19.700000
MALMO_temp_max 3654.000000 12.731773 7.849050 -7.500000 6.200000 12.700000 19.300000 31.400000
MONTELIMAR_wind_speed 3654.000000 3.680952 2.133979 0.000000 2.000000 3.100000 5.100000 13.200000
MONTELIMAR_humidity 3654.000000 0.690794 0.129024 0.340000 0.600000 0.690000 0.790000 0.980000
MONTELIMAR_pressure 3654.000000 1.017094 0.006988 0.986200 1.013200 1.017000 1.021100 1.038700
MONTELIMAR_global_radiation 3654.000000 1.647783 1.007065 0.020000 0.750000 1.520000 2.550000 3.640000
MONTELIMAR_precipitation 3654.000000 0.253426 0.910761 0.000000 0.000000 0.000000 0.040000 15.400000
MONTELIMAR_temp_mean 3654.000000 14.241215 7.193924 -4.000000 8.400000 14.200000 20.000000 30.800000
MONTELIMAR_temp_min 3654.000000 9.535222 6.326726 -8.800000 4.600000 9.600000 14.800000 24.900000
MONTELIMAR_temp_max 3654.000000 18.948741 8.557584 -2.000000 12.200000 18.800000 25.700000 41.100000
MUENCHEN_cloud_cover 3654.000000 5.226054 2.318547 0.000000 4.000000 6.000000 7.000000 8.000000
MUENCHEN_wind_speed 3654.000000 2.792255 1.315428 0.700000 1.900000 2.500000 3.300000 10.400000
MUENCHEN_wind_gust 3654.000000 9.769814 4.291187 2.600000 6.600000 8.700000 11.900000 30.900000
MUENCHEN_humidity 3654.000000 0.741946 0.132932 0.200000 0.650000 0.750000 0.840000 1.000000
MUENCHEN_pressure 3654.000000 1.017450 0.008226 0.984000 1.012600 1.017200 1.022500 1.044000
MUENCHEN_global_radiation 3654.000000 1.426429 0.983942 0.190000 0.580000 1.150000 2.230000 3.650000
MUENCHEN_precipitation 3654.000000 0.261700 0.599618 0.000000 0.000000 0.010000 0.280000 9.790000
MUENCHEN_sunshine 3654.000000 5.219814 4.594811 0.000000 0.700000 4.400000 8.900000 15.700000
MUENCHEN_temp_mean 3654.000000 10.051587 7.903211 -12.900000 3.900000 10.400000 16.300000 29.200000
MUENCHEN_temp_min 3654.000000 5.997126 7.055925 -16.400000 0.600000 6.300000 11.675000 22.000000
MUENCHEN_temp_max 3654.000000 14.540285 9.170164 -9.900000 7.300000 14.800000 21.875000 37.000000
OSLO_cloud_cover 3654.000000 5.608101 2.170706 0.000000 4.000000 6.000000 8.000000 8.000000
OSLO_wind_speed 3654.000000 2.663656 1.364321 0.000000 1.700000 2.400000 3.400000 11.000000
OSLO_wind_gust 3654.000000 9.094800 3.471479 1.500000 6.700000 8.700000 11.300000 27.300000
OSLO_humidity 3654.000000 0.723298 0.151112 0.240000 0.620000 0.750000 0.850000 1.000000
OSLO_pressure 3654.000000 1.011396 0.012005 0.959000 1.003700 1.011600 1.019400 1.051100
OSLO_global_radiation 3654.000000 1.047244 0.978529 0.010000 0.170000 0.705000 1.767500 3.530000
OSLO_precipitation 3654.000000 0.239792 0.512402 0.000000 0.000000 0.000000 0.240000 5.600000
OSLO_sunshine 3654.000000 4.848714 4.879549 0.000000 0.000000 3.900000 8.200000 24.000000
OSLO_temp_mean 3654.000000 7.198194 7.990930 -18.100000 1.100000 7.000000 13.900000 25.400000
OSLO_temp_min 3654.000000 3.845484 7.502278 -20.700000 -1.300000 3.700000 10.000000 20.700000
OSLO_temp_max 3654.000000 11.033443 9.002142 -15.600000 3.700000 10.900000 18.475000 33.000000
PERPIGNAN_wind_speed 3654.000000 4.669376 2.651377 0.800000 2.600000 3.900000 6.200000 16.300000
PERPIGNAN_humidity 3654.000000 0.651522 0.149114 0.220000 0.540000 0.650000 0.770000 0.970000
PERPIGNAN_pressure 3654.000000 1.016451 0.006809 0.983000 1.012800 1.016500 1.020400 1.036500
PERPIGNAN_global_radiation 3654.000000 1.711773 0.941671 0.030000 0.930000 1.570000 2.510000 3.660000
PERPIGNAN_precipitation 3654.000000 0.150733 0.772949 0.000000 0.000000 0.000000 0.020000 16.040000
PERPIGNAN_temp_mean 3654.000000 16.035468 6.476893 -0.600000 11.000000 15.800000 21.400000 31.800000
PERPIGNAN_temp_min 3654.000000 11.615900 6.373871 -5.900000 6.900000 11.500000 16.900000 26.300000
PERPIGNAN_temp_max 3654.000000 20.455337 6.974910 1.300000 14.900000 20.300000 26.000000 38.200000
ROMA_cloud_cover 3654.000000 3.520799 2.198344 0.000000 2.000000 3.000000 5.000000 8.000000
ROMA_humidity 3654.000000 0.735025 0.121004 0.180000 0.650000 0.740000 0.830000 0.990000
ROMA_pressure 3654.000000 1.015247 0.006743 0.982900 1.011400 1.015300 1.019200 1.039100
ROMA_global_radiation 3654.000000 1.568177 0.878505 0.050000 0.820000 1.505000 2.390000 3.330000
ROMA_sunshine 3654.000000 7.162397 4.015933 0.000000 3.900000 8.000000 10.500000 13.800000
ROMA_temp_mean 3654.000000 16.059579 6.941017 -0.700000 10.300000 15.900000 21.900000 31.500000
ROMA_temp_min 3654.000000 11.170115 6.424980 -4.400000 6.000000 11.100000 16.600000 25.000000
ROMA_temp_max 3654.000000 21.103229 7.626300 0.000000 14.800000 21.000000 27.400000 40.000000
SONNBLICK_cloud_cover 3654.000000 5.446907 2.437457 0.000000 4.000000 6.000000 8.000000 8.000000
SONNBLICK_humidity 3654.000000 0.853952 0.174900 0.100000 0.800000 0.930000 0.970000 1.000000
SONNBLICK_global_radiation 3654.000000 1.693919 0.898277 0.170000 0.930000 1.600000 2.300000 4.420000
SONNBLICK_precipitation 3654.000000 0.541475 0.771348 0.000000 0.000000 0.180000 0.840000 5.950000
SONNBLICK_sunshine 3654.000000 4.891078 4.470904 0.000000 0.000000 4.300000 8.700000 15.600000
SONNBLICK_temp_mean 3654.000000 -4.626327 6.987080 -26.600000 -9.400000 -4.400000 0.700000 13.800000
SONNBLICK_temp_min 3654.000000 -6.884319 7.120333 -30.300000 -11.800000 -6.400000 -1.100000 8.700000
SONNBLICK_temp_max 3654.000000 -2.352244 6.972886 -24.700000 -7.100000 -2.200000 2.700000 14.300000
STOCKHOLM_cloud_cover 3654.000000 5.245758 3.362460 -99.000000 4.000000 6.000000 7.000000 9.000000
STOCKHOLM_pressure 3654.000000 1.011074 0.033838 -0.099000 1.004525 1.012100 1.019800 1.051200
STOCKHOLM_precipitation 3654.000000 0.149039 0.345369 0.000000 0.000000 0.000000 0.130000 4.300000
STOCKHOLM_sunshine 3654.000000 5.101478 4.943148 -1.700000 0.200000 4.300000 8.700000 17.800000
STOCKHOLM_temp_mean 3654.000000 8.049808 7.829552 -17.000000 2.000000 7.900000 14.675000 26.200000
STOCKHOLM_temp_min 3654.000000 5.104215 7.250744 -19.700000 0.000000 5.000000 11.200000 21.200000
STOCKHOLM_temp_max 3654.000000 11.470635 8.950217 -14.500000 4.100000 11.000000 19.000000 32.900000
TOURS_wind_speed 3654.000000 3.677258 1.519866 0.700000 2.600000 3.400000 4.600000 10.800000
TOURS_humidity 3654.000000 0.781872 0.115572 0.330000 0.700000 0.800000 0.870000 1.000000
TOURS_pressure 3654.000000 1.016639 0.018885 0.000300 1.012100 1.017300 1.022200 1.041400
TOURS_global_radiation 3654.000000 1.369787 0.926472 0.050000 0.550000 1.235000 2.090000 3.560000
TOURS_precipitation 3654.000000 0.186100 0.422151 0.000000 0.000000 0.000000 0.160000 6.200000
TOURS_temp_mean 3654.000000 12.205802 6.467155 -6.200000 7.600000 12.300000 17.200000 31.200000
TOURS_temp_min 3654.000000 7.860536 5.692256 -13.000000 3.700000 8.300000 12.300000 22.600000
TOURS_temp_max 3654.000000 16.551779 7.714924 -3.100000 10.800000 16.600000 22.400000 39.800000

What these summary numbers mean:

  • count — how many days have a real reading (not blank)
  • mean — the average value across all those days
  • std — how spread out the values are; a bigger number means more day-to-day swing
  • min / max — the lowest and highest value ever recorded
  • 25% / 50% / 75% — if you lined up every value from lowest to highest, these are the values 1/4, 1/2, and 3/4 of the way along

Step 3: Check for Missing Data¶

Real-world sensors sometimes fail to record a reading. Before doing anything else, we check every column for blanks ("missing values") so we know how much clean-up is needed.

In [5]:
print_header("Data Types & Missing Values")
display(pd.DataFrame({
    'Data Type': df.dtypes,
    'Missing Values': df.isna().sum(),
    '% Missing': (df.isna().mean()*100).round(2)
}))

Data Types & Missing Values¶

Data Type Missing Values % Missing
DATE int64 0 0.0
MONTH int64 0 0.0
BASEL_cloud_cover int64 0 0.0
BASEL_humidity float64 0 0.0
BASEL_pressure float64 0 0.0
... ... ... ...
TOURS_global_radiation float64 0 0.0
TOURS_precipitation float64 0 0.0
TOURS_temp_mean float64 0 0.0
TOURS_temp_min float64 0 0.0
TOURS_temp_max float64 0 0.0

165 rows × 3 columns

Step 4: Clean the Data¶

Four clean-up steps happen here, in order:

  1. Throw out physically impossible readings — the check below; this turned out to matter enormously
  2. Remove incomplete days — any day missing a reading is dropped, since a forecaster cannot learn from a blank
  3. Remove the raw calendar date — a number like 20000101 looks meaningful to a person but is meaningless to the model as a plain number, so we drop it and keep MONTH instead, which does capture the season
  4. Check which readings move together — see the correlation chart below

4.1 Hunting for impossible readings¶

Checking for blanks is the obvious clean-up step, and almost every tutorial stops there. But a sensor can also fail by reporting a number that is simply wrong — and a blank-check will never notice, because a wrong number is still a number.

Air pressure at sea level is always somewhere around 1 bar; it has never been recorded below about 0.87 or above 1.09. So any reading far outside that window is not weather, it is a broken sensor. Let's look.

In [6]:
# Air pressure at sea level physically cannot fall outside roughly 0.87-1.09 bar.
# Anything beyond that is a sensor failure, not weather.
PRESSURE_MIN, PRESSURE_MAX = 0.87, 1.09
pressure_cols = [c for c in df.columns if c.endswith('_pressure')]

implausible = pd.Series(False, index=df.index)
for col in pressure_cols:
    bad = (df[col] < PRESSURE_MIN) | (df[col] > PRESSURE_MAX)
    if bad.any():
        print(f"{col}: {bad.sum()} impossible reading(s) -> {df.loc[bad, col].tolist()}")
    implausible |= bad

print(f"\nFound {implausible.sum()} corrupted days out of {len(df)} "
      f"({implausible.sum() / len(df) * 100:.2f}% of the data)")

# Remove them - a handful of bad rows can do enormous damage, as we will see
df = df[~implausible].reset_index(drop=True)
print(f"{len(df)} days remain")
STOCKHOLM_pressure: 3 impossible reading(s) -> [-0.099, -0.099, -0.099]
TOURS_pressure: 1 impossible reading(s) -> [0.0003]

Found 4 corrupted days out of 3654 (0.11% of the data)
3650 days remain

Why this tiny clean-up matters so much. Only four days out of 3,654 carried a corrupted pressure reading — values like -0.099 bar (negative pressure, which cannot exist) and 0.0003 bar (a near-vacuum). That is roughly one hundredth of one percent of the data.

Yet leaving those four rows in was enough to make several models score worse than useless. Because the model multiplies each input by a weight, one absurd input produces one absurd output — a single day's pressure estimate missed by over 700 hPa, which by itself dragged an entire model's score below zero.

The lesson is worth remembering: dropna() only catches blanks, not nonsense. A handful of bad numbers, invisible in any summary table, can quietly ruin an entire analysis.

In [7]:
# 1. Handle Missing Values
rows_before = len(df)
df = df.dropna() # Drop rows with missing values
rows_after = len(df)
print(f"Dropped {rows_before - rows_after} rows with missing values ({rows_before} -> {rows_after})")
Dropped 0 rows with missing values (3650 -> 3650)
In [8]:
# 2. Drop DATE (raw int date has no meaningful magnitude for a linear model; MONTH already captures season)
df = df.drop('DATE', axis=1)
In [9]:
# 3. Visualize Correlation Matrix
import seaborn as sns
import matplotlib.pyplot as plt

plt.figure(figsize=(12, 10))
sns.heatmap(df.corr(numeric_only=True), cmap='coolwarm', annot=False)
plt.title("Correlation Matrix of Numerical Features")
plt.show()
No description has been provided for this image

What is a correlation matrix?

It's a grid that shows, for every pair of weather readings, how closely they move together — from -1 (perfectly opposite) to +1 (perfectly together), with 0 meaning no relationship at all.

Value Meaning
+1.0 Move perfectly together
+0.7 to +0.9 Strong same-direction relationship
+0.4 to +0.6 Moderate same-direction relationship
+0.1 to +0.3 Weak same-direction relationship
0 No relationship
-0.1 to -0.3 Weak opposite-direction relationship
-0.4 to -0.6 Moderate opposite-direction relationship
-0.7 to -0.9 Strong opposite-direction relationship
-1.0 Move perfectly opposite

This helps us sanity-check the data before training — e.g. we'd expect sunshine and temperature to move together.

In [10]:
# 4. Feature Selection
# Define Features and Target Variable
# Each target is dropped from every feature set, including the OTHER two targets,
# so no model can see another target's answer as an input feature
targets = ['BASEL_temp_mean', 'BASEL_humidity', 'BASEL_pressure']

X1 = df.drop(columns=targets) #axis is column
y1 = df['BASEL_temp_mean']

X2 = df.drop(columns=targets)
y2 = df['BASEL_humidity']

X3 = df.drop(columns=targets)
y3 = df['BASEL_pressure']

Step 5: Set Aside Recent Years for Testing¶

We split the data by time, not at random: the model learns from the earliest 80% of days (2000 to the start of 2008) and is tested on the most recent 20% (2008 to 2010), which it never sees during training.

Why this matters: if we shuffled the days randomly, the model could learn from 2009 and then be tested on 2003 — effectively studying the future to answer questions about the past. No real forecaster gets that luxury, so a random split would flatter the results. Training on the past and testing on the future is the honest way.

We also build the feature sets so that no model can peek at the other two targets. When predicting Basel's humidity, the model is never shown Basel's actual temperature or pressure for that same day.

In [11]:
# Chronological split: rows are already in date order, so the first 80% of rows
# are the earliest days and the last 20% are the most recent.
split_idx = int(len(df) * 0.8)

# Split Data Temperature
X1_train, X1_test = X1.iloc[:split_idx], X1.iloc[split_idx:]
y1_train, y1_test = y1.iloc[:split_idx], y1.iloc[split_idx:]

# Split Data Humidity
X2_train, X2_test = X2.iloc[:split_idx], X2.iloc[split_idx:]
y2_train, y2_test = y2.iloc[:split_idx], y2.iloc[split_idx:]

# Split Data Pressure
X3_train, X3_test = X3.iloc[:split_idx], X3.iloc[split_idx:]
y3_train, y3_test = y3.iloc[:split_idx], y3.iloc[split_idx:]

print(f"Training on {split_idx} earliest days, testing on {len(df) - split_idx} most recent days")
Training on 2920 earliest days, testing on 730 most recent days

Step 6: Train Three Separate Forecasters¶

We train three independent models, one for temperature, one for humidity, one for pressure. Each one uses Linear Regression — a straightforward technique that looks for the best straight-line relationship between the input readings (other cities' weather, month, etc.) and the target it's trying to predict.

In [12]:
from sklearn.linear_model import LinearRegression

# Init Temperature Model
model_temp = LinearRegression()
# Train Temperature Model
model_temp.fit(X1_train, y1_train)

# Init Humidity Model
model_humidity = LinearRegression()
# Train Humidity Model
model_humidity.fit(X2_train, y2_train)

# Init Pressure Model
model_pressure = LinearRegression()
# Train Pressure Model
model_pressure.fit(X3_train, y3_train)
Out[12]:
LinearRegression()
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
Parameters
fit_intercept fit_intercept: bool, default=True

Whether to calculate the intercept for this model. If set
to False, no intercept will be used in calculations
(i.e. data is expected to be centered).
True
copy_X copy_X: bool, default=True

If True, X will be copied; else, it may be overwritten.
True
tol tol: float, default=1e-6

The precision of the solution (`coef_`) is determined by `tol` which
specifies the convergence criterion of the underlying solver. `tol` is
set as `atol` and `btol` of :func:`scipy.sparse.linalg.lsqr` when
fitting on sparse training data. `tol` is set as `cond` of
:func:`scipy.linalg.lstsq` when fitting on dense training data.

.. versionadded:: 1.7
.. versionchanged:: 1.9
Now supported on dense data, interpreted as the `cond` parameter.
1e-06
n_jobs n_jobs: int, default=None

The number of jobs to use for the computation. This will only provide
speedup in case of sufficiently large problems, that is if firstly
`n_targets > 1` and secondly `X` is sparse or if `positive` is set
to `True`. ``None`` means 1 unless in a
:obj:`joblib.parallel_backend` context. ``-1`` means using all
processors. See :term:`Glossary <n_jobs>` for more details.
None
positive positive: bool, default=False

When set to ``True``, forces the coefficients to be positive. This
option is only supported for dense arrays.

For a comparison between a linear regression model with positive constraints
on the regression coefficients and a linear regression without such constraints,
see :ref:`sphx_glr_auto_examples_linear_model_plot_nnls.py`.

.. versionadded:: 0.24
False
Fitted attributes
Name Type Value
coef_ coef_: array of shape (n_features, ) or (n_targets, n_features)

Estimated coefficients for the linear regression problem.
If multiple targets are passed during the fit (y 2D), this
is a 2D array of shape (n_targets, n_features), while if only
one target is passed, this is a 1D array of length n_features.
ndarray[float64](161,) [ 0.,-0., 0.,...,-0., 0., 0.]
feature_names_in_ feature_names_in_: ndarray of shape (`n_features_in_`,)

Names of features seen during :term:`fit`. Defined only when `X`
has feature names that are all strings.

.. versionadded:: 1.0
ndarray[object](161,) ['MONTH','BASEL_cloud_cover','BASEL_global_radiation',..., 'TOURS_temp_mean','TOURS_temp_min','TOURS_temp_max']
intercept_ intercept_: float or array of shape (n_targets,)

Independent term in the linear model. Set to 0.0 if
`fit_intercept = False`.
float64 -0.000392
n_features_in_ n_features_in_: int

Number of features seen during :term:`fit`.

.. versionadded:: 0.24
int 161
rank_ rank_: int

Rank of matrix `X`. Only available when `X` is dense.
int 161
singular_ singular_: array of shape (min(X, y),)

Singular values of `X`. Only available when `X` is dense.
ndarray[float64](161,) [2731.99, 628.38, 521.04,..., 0.03, 0.02, 0.01]

Step 7: Check How Good the Estimates Are¶

Now we ask each model to estimate the test days it never saw, and compare its answers to what actually happened.

A note on units. The raw dataset stores humidity as a fraction (0.89 meaning 89%) and pressure in bar (1.0286 meaning 1028.6 hPa). Scores in those raw units are hard to interpret, so below we convert every error back into units a person actually recognises: °C, %, and hPa.

In [13]:
from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score

# Convert each target back into human-readable units:
#   temperature is already in degrees C
#   humidity is stored as a fraction, so x100 gives percent
#   pressure is stored in bar, so x1000 gives hPa
UNITS = {
    'Temperature': ('°C', 1),
    'Humidity':    ('%', 100),
    'Pressure':    ('hPa', 1000),
}

def evaluate(name, y_true, y_pred):
    """Print MAE (in real units), MSE and R2 for one target."""
    unit, factor = UNITS[name]
    mae = mean_absolute_error(y_true, y_pred) * factor
    mse = mean_squared_error(y_true, y_pred)
    r2 = r2_score(y_true, y_pred)

    print(f"{name} Evaluation")
    print(f"  Average error (MAE): {mae:.2f} {unit}")
    print(f"  Mean Squared Error:  {mse:.6g}")
    print(f"  R2 Score:            {r2:.4f}")
    print()
    return {'target': name, 'mae': mae, 'unit': unit, 'r2': r2}

y1_pred = model_temp.predict(X1_test)
y2_pred = model_humidity.predict(X2_test)
y3_pred = model_pressure.predict(X3_test)

results = [
    evaluate('Temperature', y1_test, y1_pred),
    evaluate('Humidity',    y2_test, y2_pred),
    evaluate('Pressure',    y3_test, y3_pred),
]
Temperature Evaluation
  Average error (MAE): 0.38 °C
  Mean Squared Error:  0.239053
  R2 Score:            0.9956

Humidity Evaluation
  Average error (MAE): 3.54 %
  Mean Squared Error:  0.00207507
  R2 Score:            0.7952

Pressure Evaluation
  Average error (MAE): 0.54 hPa
  Mean Squared Error:  4.74644e-07
  R2 Score:            0.9935

How to read the scores:

  • Average error (MAE) — the most human-readable one. It is the typical size of the model's mistake, in real units. "1.2 °C" means that on an average day the estimate was off by about 1.2 degrees.
  • Mean Squared Error (MSE) — similar idea, but mistakes are squared before averaging, so a few big misses count much more heavily than many small ones. Lower is better. It is not in readable units, which is exactly why MAE is shown alongside it.
  • R² Score — a 0-to-1 score for how much of the day-to-day pattern the model explains. 1.0 means it explains everything; 0 means it is no better than always guessing the long-term average.

A good score on its own does not prove a model is useful, though — that needs a comparison, which is the next step.

Step 8: See the Predictions vs. Reality¶

Each chart below plots what actually happened (x-axis) against what the model predicted (y-axis) for every test day. The red dashed line is where a perfect forecaster would land — the closer the blue dots hug that line, the more accurate the model.

In [14]:
import matplotlib.pyplot as plt

def plot_regression(y_true, y_pred, title, unit):
    plt.figure(figsize=(6, 6)) # Set figure size
    plt.scatter(y_true, y_pred, color='skyblue', edgecolors='k', alpha=0.7)
    max_val = max(max(y_true), max(y_pred))
    min_val = min(min(y_true), min(y_pred))
    plt.plot([min_val, max_val], [min_val, max_val], 'r--', label='Ideal Prediction')

    plt.xlabel(f'Actual Values ({unit})')
    plt.ylabel(f'Predicted Values ({unit})')
    plt.title(title)
    plt.legend()
    plt.grid(True)
    plt.axis('equal')
    plt.tight_layout()
    plt.show()

# Temperature Plot (°C)
plot_regression(y1_test, y1_pred, 'Actual Temperature vs Predicted Temperature', '°C')

# Humidity Plot (%)
plot_regression(y2_test, y2_pred, 'Actual Humidity vs Predicted Humidity', '%')

# Pressure Plot (hPa)
plot_regression(y3_test, y3_pred, 'Actual Pressure vs Predicted Pressure', 'hPa')
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image

Step 9: Is the Model Actually Better Than a Lazy Guess?¶

A high score means nothing on its own. Before trusting any model you have to ask: could something far simpler have done just as well?

So we compare against a seasonal average baseline - a "model" that ignores all the clever inputs and simply answers with the historical average for that month. If our model cannot clearly beat that, it is not earning its keep.

In [15]:
# Seasonal average baseline: for each month, use the average value seen in TRAINING data only
def seasonal_baseline(y_train, X_train, X_test):
    month_avg = y_train.groupby(X_train['MONTH']).mean()
    return X_test['MONTH'].map(month_avg)

print("Model vs. seasonal-average baseline (lower average error is better)\n")

comparison = []
for name, (y_tr, X_tr, X_te, y_te, y_pr) in {
    'Temperature': (y1_train, X1_train, X1_test, y1_test, y1_pred),
    'Humidity':    (y2_train, X2_train, X2_test, y2_test, y2_pred),
    'Pressure':    (y3_train, X3_train, X3_test, y3_test, y3_pred),
}.items():
    unit, factor = UNITS[name]
    base_pred = seasonal_baseline(y_tr, X_tr, X_te)

    model_mae = mean_absolute_error(y_te, y_pr) * factor
    base_mae = mean_absolute_error(y_te, base_pred) * factor
    improvement = (1 - model_mae / base_mae) * 100

    print(f"{name}")
    print(f"  Seasonal average guess: off by {base_mae:.2f} {unit}")
    print(f"  Our model:              off by {model_mae:.2f} {unit}")
    print(f"  -> {improvement:.0f}% smaller error than the lazy guess")
    print()
    comparison.append({'target': name, 'unit': unit,
                       'baseline_mae': base_mae, 'model_mae': model_mae,
                       'improvement': improvement})
Model vs. seasonal-average baseline (lower average error is better)

Temperature
  Seasonal average guess: off by 2.81 °C
  Our model:              off by 0.38 °C
  -> 86% smaller error than the lazy guess

Humidity
  Seasonal average guess: off by 6.93 %
  Our model:              off by 3.54 %
  -> 49% smaller error than the lazy guess

Pressure
  Seasonal average guess: off by 6.41 hPa
  Our model:              off by 0.54 hPa
  -> 92% smaller error than the lazy guess

Step 10: A Real Forecast - Predicting Tomorrow¶

Everything up to this point does something subtly different from forecasting. The models above estimate Basel's weather for the same day, using other cities' readings from that same day. That is genuinely useful - it is how you would fill in a broken sensor - but it is not predicting the future. It is more like looking around the room to work out the temperature where you are standing.

A real forecast has to use only what is known today to say something about tomorrow. So here we rebuild the problem honestly:

  • Inputs: every reading from today - including Basel's own temperature, humidity and pressure, which a real forecaster obviously knows
  • Target: Basel's weather the following day

We also compare against the toughest simple baseline in weather forecasting: persistence, which just guesses "tomorrow will be the same as today." That is a genuinely hard benchmark to beat, because weather really is sluggish from one day to the next.

In [16]:
# Build the next-day problem: today's readings (row t) predict Basel's weather on row t+1
X_next = df.iloc[:-1].reset_index(drop=True)          # today's conditions, all cities
next_targets = {t: df[t].iloc[1:].reset_index(drop=True) for t in targets}

split_next = int(len(X_next) * 0.8)
Xn_train, Xn_test = X_next.iloc[:split_next], X_next.iloc[split_next:]

print("Forecasting tomorrow from today (model vs. 'tomorrow = today' persistence)\n")

forecast_results = []
forecast_preds = {}
for name, col in [('Temperature', 'BASEL_temp_mean'),
                  ('Humidity', 'BASEL_humidity'),
                  ('Pressure', 'BASEL_pressure')]:
    unit, factor = UNITS[name]
    y_next = next_targets[col]
    yn_train, yn_test = y_next.iloc[:split_next], y_next.iloc[split_next:]

    model = LinearRegression()
    model.fit(Xn_train, yn_train)
    pred = model.predict(Xn_test)

    # Persistence baseline: tomorrow will be whatever today was
    persistence = Xn_test[col]

    model_mae = mean_absolute_error(yn_test, pred) * factor
    persist_mae = mean_absolute_error(yn_test, persistence) * factor
    r2 = r2_score(yn_test, pred)

    print(f"{name}")
    print(f"  'Same as today' guess: off by {persist_mae:.2f} {unit}")
    print(f"  Our forecast:          off by {model_mae:.2f} {unit}  (R2 = {r2:.3f})")
    print(f"  -> {(1 - model_mae / persist_mae) * 100:.0f}% smaller error than persistence")
    print()

    forecast_preds[name] = (yn_test, pred)
    forecast_results.append({'target': name, 'unit': unit, 'r2': r2,
                             'model_mae': model_mae, 'persist_mae': persist_mae})
Forecasting tomorrow from today (model vs. 'tomorrow = today' persistence)

Temperature
  'Same as today' guess: off by 1.64 °C
  Our forecast:          off by 1.30 °C  (R2 = 0.950)
  -> 21% smaller error than persistence

Humidity
  'Same as today' guess: off by 6.71 %
  Our forecast:          off by 5.49 %  (R2 = 0.508)
  -> 18% smaller error than persistence

Pressure
  'Same as today' guess: off by 3.64 hPa
  Our forecast:          off by 2.84 hPa  (R2 = 0.821)
  -> 22% smaller error than persistence

Notice how much lower these scores are than the same-day numbers. That drop is the honest cost of actually predicting the future - and it is exactly why you should be suspicious of any weather model reporting near-perfect accuracy.

Step 11: Which Cities Tell Us Most About Basel?¶

A linear model assigns a weight to every input. By looking at which weights are largest, we can ask a genuinely interesting question: whose weather is the best clue to Basel's?

One catch: the inputs are on wildly different scales (cloud cover runs 0-8, pressure sits near 1.03). Comparing raw weights would be meaningless, so we scale each weight by how much that reading actually varies. The result estimates how much each input really moves the answer.

In [17]:
import numpy as np

# Weight each coefficient by the spread of its feature, so inputs on different
# scales can be compared fairly. Result is roughly "degrees C of influence".
influence = (pd.Series(model_temp.coef_, index=X1.columns).abs()
             * X1_train.std()).sort_values(ascending=False)

top = influence.head(12).sort_values()

plt.figure(figsize=(8, 6))
plt.barh(top.index, top.values, color='#2e6f6a')
plt.xlabel('Influence on the temperature estimate (°C)')
plt.title("Which readings tell us most about Basel's temperature?")
plt.tight_layout()
plt.show()

print("Top 5 most influential readings:")
for rank, (feature, value) in enumerate(influence.head(5).items(), start=1):
    print(f"  {rank}. {feature}  ({value:.2f} °C of influence)")
No description has been provided for this image
Top 5 most influential readings:
  1. TOURS_temp_mean  (3.73 °C of influence)
  2. BASEL_temp_max  (3.51 °C of influence)
  3. BASEL_temp_min  (2.74 °C of influence)
  4. PERPIGNAN_temp_mean  (2.31 °C of influence)
  5. TOURS_temp_max  (2.13 °C of influence)

Step 12: When Does the Forecast Struggle?¶

An average error hides a lot. A model can be excellent in summer and unreliable in winter, and a single number would never show it. So we break the next-day temperature forecast's error down month by month.

In [18]:
# Average forecast error for each calendar month, using the next-day temperature model
yn_test_temp, pred_temp = forecast_preds['Temperature']
errors = pd.DataFrame({
    'month': Xn_test['MONTH'].values,
    'error': np.abs(yn_test_temp.values - pred_temp),
})
monthly = errors.groupby('month')['error'].mean()

month_names = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
               'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']

plt.figure(figsize=(9, 5))
plt.bar([month_names[m - 1] for m in monthly.index], monthly.values, color='#2e6f6a')
plt.axhline(monthly.mean(), color='#c26b5f', linestyle='--',
            label=f'Year-round average ({monthly.mean():.2f} °C)')
plt.ylabel('Average error (°C)')
plt.title('Next-day temperature forecast error by month')
plt.legend()
plt.tight_layout()
plt.show()

print(f"Hardest month: {month_names[monthly.idxmax() - 1]} "
      f"(off by {monthly.max():.2f} °C on average)")
print(f"Easiest month: {month_names[monthly.idxmin() - 1]} "
      f"(off by {monthly.min():.2f} °C on average)")
No description has been provided for this image
Hardest month: Dec (off by 1.46 °C on average)
Easiest month: Sep (off by 1.09 °C on average)

Step 13: What We Learned¶

Pulling the whole analysis together:

  1. Filling in the same day is easy. Given what 17 other European cities recorded today, Basel's temperature and pressure for that same day can be reconstructed almost exactly. Regional weather moves as one connected system, so the neighbours give the answer away.

  2. Humidity is the stubborn one. It scores noticeably worse than temperature and pressure in every version of the experiment, because local effects like fog, rain and cloud cover can change it quickly without the whole region shifting.

  3. Predicting tomorrow is a completely different, much harder problem. Once the model may only use today's information, accuracy drops sharply - and beating the plain "tomorrow will be like today" guess is a real challenge. This gap is the single most important result here.

  4. Always compare to a lazy baseline. A 99% score sounds impressive until you notice a trivial rule scores nearly as well. Baselines are what turn a number into evidence.

  5. How you split the data changes the answer. Testing on random days lets a model peek at the future. Testing on the most recent years - as we do here - is harder and far more honest.

Limitations worth stating plainly: this covers one city over 2000-2010, uses a straight-line model, and forecasts only one day ahead. It is a learning exercise in how to evaluate a model honestly, not a working weather service.