#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¶
%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.
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
# 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¶
| 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.
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:
- Throw out physically impossible readings — the check below; this turned out to matter enormously
- Remove incomplete days — any day missing a reading is dropped, since a forecaster cannot learn from a blank
- Remove the raw calendar date — a number like
20000101looks meaningful to a person but is meaningless to the model as a plain number, so we drop it and keepMONTHinstead, which does capture the season - 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.
# 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.
# 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)
# 2. Drop DATE (raw int date has no meaningful magnitude for a linear model; MONTH already captures season)
df = df.drop('DATE', axis=1)
# 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()
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.
# 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.
# 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.
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)
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
Fitted attributes
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.
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.
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')
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.
# 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.
# 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.
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)")
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.
# 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)")
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:
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.
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.
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.
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.
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.