top of page

The Weather API That Forgot to Ask for a Key — Part II

  • Writer: Mic
    Mic
  • Jul 10
  • 19 min read

Updated: Jul 12

In the last post we looked at a decision problem, namely when to go to the beach. This time we will first look at a pattern problem by looking at how seasons can be identified and then at the forecast archive for an evaluation problem.

The two questions we are looking at are

Can historical data reveal recurring seasons without calendar labels?

and

How quickly does a temperature forecast lose accuracy as lead time increases?

The complete code can be found as the second and third project in this GitHub repo.

Forecast time has several meanings. The valid time is when predicted weather should occur. The issue time is when the model run was produced. The lead time is the distance between them. Verification must keep those concepts aligned, or a perfectly tidy merge can compare the wrong prediction with the wrong hour.

I guess ChatGPT thinks the request is a bird or something...

Project 2: Let the Data Invent Sydney’s Seasons

There are calender rules for seasons in Sydney, things like: Summer begins in December, autumn begins in March, winter begins in June, and spring begins in September.

Unfortunately Sydney’s weather is not always that organised. A warm April can feel suspiciously like summer. A wet January may have more in common with a rainy autumn month than with the Decembers surrounding it. Instead of assigning seasons from the calendar, this project asks a different question:

What kinds of months repeatedly appear in Sydney’s historical weather data?

To answer that, the script downloads daily historical weather data, converts it into monthly observations, and uses K-means clustering to group months with similar conditions.


Importing the Tools

The majority of libraries we need are the same as in the project in the previous post. The main addition is Scikit-learn for standardising weather variables and running K-means clustering.

from sklearn.cluster import KMeans
from sklearn.preprocessing import StandardScaler

Defining the Historical Record

Similar to the previous project we need location and time zone, but we have to change our API to use the historical weather API and a historical weather model. Since we are looking at historical data, we take the 1 January 1950 all the way through 31 December 2025, giving it several decades of monthly weather to compare.

ARCHIVE_URL = "https://archive-api.open-meteo.com/v1/archive"

SYDNEY_LATITUDE = -33.8688
SYDNEY_LONGITUDE = 151.2093
DEFAULT_TIMEZONE = "Australia/Sydney"
HISTORICAL_MODEL = "era5"

HISTORY_START_DATE = "1950-01-01"
HISTORY_END_DATE = "2025-12-31"

The selected model is ERA5. ERA5 is a reanalysis dataset rather than a single weather station record. It combines observations with a numerical weather model to reconstruct past atmospheric conditions consistently across time and space.

That distinction matters. The script is analysing a model-assisted historical reconstruction for the Sydney coordinates, not a table of direct measurements from one thermometer sitting somewhere near Town Hall since 1950.


Choosing the Number of Weather Regimes

We fix a number of global settings

We tell K-means how many observation groups we would like via NUMBER_OF_REGIMES. We will stick to the usual 4, but if you experiment a bit here, it might be better to go with more or less. Choosing the number of clusters is a modelling decision, not a discovery handed down by the atmosphere.

The value KMEANS_INITIALISATIONS controls how many different starting arrangements K-means tries. K-means begins with candidate cluster centres, and poor starting positions can lead to a weaker solution. Trying 30 initial arrangements makes the result less dependent on one unlucky starting point.

We set a random state for reproducibility and ROLLING_WINDOW_YEARS is used later to smooth the annual frequency of the warmest regime over ten years.


Selecting the Weather Variables

We have to make a choice on what we base our definition of season on.

Each daily observation contains four weather measurements:

  • mean air temperature at two metres above the ground;

  • total daily precipitation;

  • total sunshine duration;

  • maximum wind speed at ten metres above the ground.

These variables describe different aspects of what a month feels like. Temperature alone would mostly divide the year into cooler and warmer periods. Adding rainfall, sunshine, and wind allows two months with similar temperatures to end up in different clusters.

For example, a mild, sunny, calm month may be placed in a different regime from a mild, wet, windy month.

The monthly versions of those variables receive slightly clearer names after aggregation:

These are the actual features passed to the clustering model.

We can reuse the API request function from the previous project to request these weather variables.


Downloading the Daily ERA5 History

This will look very similar to our fetch function from the previous project, the main difference is that we have a start and end date, as well as a model that is used.

The request parameters are assembled from the global configuration.

We again assemble the requested parameters in a comma-separated string:

temperature_2m_mean,precipitation_sum,sunshine_duration,wind_speed_10m_max

The API returns its daily measurements as parallel arrays inside the daily section. Passing that dictionary to pd.DataFrame() converts it into a table with one row per date. As in the previous project we convert the "time" column to datetime.

As a measure of usability we note that Open-Meteo returns sunshine duration in seconds. Dividing by 3,600 converts it into hours:

daily_history["sunshine_hours"] = (
    daily_history["sunshine_duration"] / 3600
)

Hours are easier to interpret when the monthly regime profiles are printed or exported.


Turning Daily Weather into Monthly Observations

K-means needs one row for every item being clustered. In this project, the items are months rather than individual days.

First, the date becomes the DataFrame index. Then we resample using MS, which means “month start”. Pandas groups all daily observations belonging to the same calendar month and labels the resulting row with the first day of that month. We have to make a choice here how we aggregate the different measurements. For most this is pretty straight forward as just the mean, but rainfall on the other hand is accumulated. Note that for wind, this is not the mean of all measured wind, but the mean of the maximum wind speeds of each day.

After aggregation, dropna() removes months that do not have a complete collection of values for the selected features.

The calendar month and year are then extracted into separate columns. They are not used to build the clusters. They are retained so the results can later be analysed by month of year and by historical year.

This separation is important: K-means does not know that January and February are supposed to be summer. It sees only temperature, rainfall, sunshine, and wind. It has to make up its own mind which months to group together.


Why the Weather Features Need Standardisation

The clustering begins by copying the monthly table and standardising the four features.

The weather variables use different units and numerical ranges. Temperature may usually lie somewhere between roughly 10 and 30 degrees Celsius. Monthly rainfall can range from almost zero to several hundred millimetres. Sunshine and wind use yet another pair of scales.

K-means calculates distances between observations. Without standardisation, a variable with larger numerical values can dominate those distances simply because of its units.

StandardScaler transforms each feature so that it has approximately:

  • a mean of zero;

  • a standard deviation of one.

After standardisation, being unusually wet can contribute to the clustering on a scale comparable to being unusually warm, sunny, or windy.

The transformed values are used only for fitting the model. The original values remain in the DataFrame so the resulting cluster profiles can still be expressed in degrees, millimetres, hours, and wind-speed units.


Assigning the K-Means Clusters

Now we can start with the actual clustering.

We initialize K-means to for four cluster centres in the standardised feature space.

Each monthly observation is assigned to the nearest centre. Months in the same cluster therefore have relatively similar combinations of temperature, rainfall, sunshine, and wind.

fit_predict() performs two operations at once:

  1. it fits the cluster model;

  2. it returns the assigned cluster for each row.

The initial labels are arbitrary integers such as 0, 1, 2, and 3. Cluster 0 is not inherently colder, wetter, or more important than cluster 3. The numbering depends on how the algorithm happened to construct its centres.

This is why the script calls the first result weather_regime_raw.

For reproducible clustering, the model definition includes the configured random seed.


Giving the Arbitrary Clusters a Useful Order

Raw K-means labels are not convenient for a something where we expect existing meaning. A cluster numbered 3 might be the coldest while cluster 0 is the warmest.

The script therefore calculates the average weather profile of every raw cluster and then sorts the profiles primarily by mean temperature:

The coolest raw cluster appears first and the warmest appears last. Rainfall acts as a secondary sorting value if two profiles have the same mean temperature.

A mapping translates the raw labels into the ordered values 1 through 4:

After this step:

  • regime 1 is the coolest cluster;

  • regime 4 is the warmest cluster;

  • regimes 2 and 3 are the two intermediate clusters.

The ordering makes the output easier to compare between runs and easier to read in the final heatmap. It does not make the clusters objectively equal to winter, spring, autumn, and summer. In particular, calling regime 2 “Spring” and regime 3 “Autumn” is a narrative interpretation. Temperature ordering alone cannot distinguish spring from autumn because both can occupy similar temperature ranges with a slight nod towards spring being colder.

The rainfall, sunshine, and wind profiles should be inspected before assigning strong seasonal names. Safer initial labels would be “Cool”, “Mild A”, “Mild B”, and “Warm”, followed by more descriptive names once the cluster profiles are understood. This naming of course falls apart if we move away from the traiditonal 4 seasons.


Creating Human-Readable Cluster Profiles

The final part of the clustering function calculates the average original weather values for every ordered regime.

These profiles answer questions such as:

  • How warm is the typical month in this regime?

  • How much rain does it receive on average?

  • How many sunshine hours does an average day have?

  • How windy are its days on average?

The function returns two DataFrames, result contains every monthly observation and its assigned regime, while profiles contains one summary row for each regime.

Keeping both is useful. The monthly table supports historical analysis, while the profile table helps explain what the clusters actually represent.


Measuring Which Regime Appears in Each Calendar Month

The first half of the figure asks how frequently each regime appears in January, February, March, and so on.

pd.crosstab() counts how often each combination of calendar month and regime occurs.

The crucial argument is here normalize="index", which converts the counts within each calendar month into proportions. Thus if the historical data contains 76 Januaries, and 55 of them belong to the warmest regime. The corresponding heatmap value would be 72.4%.

The reindex() call ensures that all twelve months and all four cluster columns appear, even if a particular combination has no observations.

The table might conceptually look like this:

weather_regime     1      2      3      4
calendar_month
1                0.00   0.03   0.18   0.79
2                0.00   0.05   0.21   0.74
3                0.01   0.20   0.52   0.27
...

This is the main evidence for whether the data-defined regimes line up neatly with the calendar seasons or spill across their boundaries.


Identifying the Warmest Regime

The warmest regime is identified from the cluster profiles rather than setting it to cluster 4.

Calculating it explicitly is still safer than hard-coding that assumption into the later analysis,

idxmax() returns the regime whose profile has the highest mean temperature.


Measuring the Warmest Regime Year by Year

The second panel asks whether warm-regime months have become more or less frequent through the historical record.

The script creates a temporary indicator column:

monthly_weather["weather_regime"] == hottest_regime

This produces True for months assigned to the warmest regime and False for all other months.

Converting those values to floating-point numbers turns them into ones and zeroes:

Warmest regime     -> 1.0
Other regime       -> 0.0

The observations are then grouped by year and averaged.

Because each complete year contains twelve monthly observations, the result is the share of months assigned to the warmest regime.

For example:

0.25 = 3 of the 12 months
0.50 = 6 of the 12 months
0.75 = 9 of the 12 months

This is a frequency measure. It does not directly show how hot the year was. A year could have the usual number of warm-regime months while those months themselves were unusually hot.


Smoothing the Long-Term Pattern

Individual years can jump around considerably, so the script also calculates a centred ten-year rolling average.

With a ten-year window, each smoothed point combines roughly a decade of annual values.

The parameter center=True places the rolling value near the middle of its time window rather than at the final year. This makes the line easier to interpret as a long-run pattern, although it also means each point incorporates years on both sides.

We also add, min_periods to allow the calculation to continue near the beginning and end of the historical record, where a complete centred ten-year window is not available.

The rolling line reduces year-to-year noise and makes gradual changes in regime frequency more visible.

It should still be treated as a descriptive result. The clustering and rolling average do not, by themselves, test whether a trend is statistically significant or establish why it occurred.


Building the Heatmap

The month-by-regime table is converted into a heatmap with imshow():

The table is transposed before plotting via regime_by_month.T. That places the regimes on the vertical axis and the calendar months on the horizontal axis.

The colour scale runs from zero to one because every cell represents a proportion. A value close to one means that a regime dominates that calendar month across the historical record. A value close to zero means that the combination is rare.

The script also writes sufficiently large percentages directly into the heatmap cells:

Values below 12% are omitted to prevent the panel from filling with tiny labels. This threshold changes only the annotations. The low values remain present in the heatmap colours. Note that we also change the colours of the label from white to black if the percentage is 55% or higher to make it more readable.


Plotting the Long-Term Warm-Regime Frequency

The second panel contains both the unsmoothed annual values and the rolling average.

The thin line preserves the individual annual observations. The thicker rolling line highlights the long-term pattern.

Showing both is preferable to showing only the smoothed version. The reader can see how much annual variation was removed and avoid mistaking the rolling average for the original data.

The remaining commands in the plotting function set axis labels, titles, tick labels, colours, figure spacing, and other presentation details.


Running the Complete Workflow

We run the complete workflow by first fetching the data

daily_history = fetch_daily_history() 

Then we build the monthly weather data

monthly_weather = build_monthly_weather(daily_history) 

We then use the clustering

monthly_weather, cluster_profiles = (
	cluster_weather_regimes( 
		monthly_weather 
	) 
)

And finally we construct the plots

figure_path = plot_weather_regimes( 
	monthly_weather, 
	cluster_profiles, 
)

The full script also saves the different the dataframes daily_history, monthly_weather, and cluster_profiles as csv files.


What the Clusters Actually Mean

The most important part of this project comes after the algorithm finishes. K-means does not discover labels such as “summer” or “winter”. It finds groups of monthly observations that are close together in a four-dimensional standardised feature space.

The cluster profiles are therefore essential. A regime should be interpreted from its average temperature, rainfall, sunshine, and wind rather than from its cluster number.

The heatmap then shows when those combinations tend to occur.

A strong four-season pattern would produce four relatively concentrated bands, with each regime dominating a distinct part of the year. A less tidy result would show regimes crossing conventional seasonal boundaries or appearing in several separated months.

That untidiness is not a failure of the model. It may be the most interesting result.

The calendar gives every month exactly one season. The weather is under no contractual obligation to do the same. Running the script gives something along the following line

 We can clearly see that spring and autumn cannot really be distinguished with this. But it does show that they are very short, as everyone in Sydney knows very well. What we can see in the second graph is that the share of months that one would consider to be summer is relatively stable if we look at a 10 year average, with a slight tendency to go down, but the ranges of values per year that appear seem quite stable.


Important Modelling Limitations

This is an exploratory clustering project, so the output should not be treated as an official classification of Sydney’s climate.

The number of clusters is chosen in advance. A different value may reveal a simpler or more detailed structure.

K-means also assumes that clusters can be represented by centres and compared using Euclidean distance. Real weather patterns may have shapes that do not fit that assumption.

The selected variables and aggregation rules influence the result. Adding humidity, minimum temperature, cloud cover, or extreme rainfall could produce different regimes.

Finally, the labels “Winter”, “Spring”, “Autumn”, and “Summer” are attached after clustering. The algorithm itself sees no calendar order and no conventional seasons. In particular, the two intermediate-temperature clusters should be inspected carefully before deciding which one deserves to be called spring and which one deserves to be called autumn.

The strength of the project is not that K-means delivers a final answer. It gives us a reproducible way to ask whether Sydney’s historical weather organises itself as neatly as the calendar suggests. We could of course also produce a plot to see how the average temperature of a regime changed over time. This would give us a plot of the form

We can see how the average temperatures for the "summer" and "winter" regime are slowly creeping up. We can also see some obscurities that show up, for example in 1999, with our random state, no months was considered to be part of the "hottest" regime. Most likely this was a particularly mild year in every regard, since the winter also does not seem to be particularly cold. If we instead focus on the month of December in general, as a benchmark for "summer" we obtain a plot of the form

Where we can also see a slow increase, but again, this type of plot is not enough to show that temperatures increase. For this we would have to look into much more details like number of high temperature days, length of high temperature periods and many other factors.


Project 3: How Fast Does a Forecast Go Stale?

Weather forecasts are usually presented as if every number deserves equal confidence, basically you just see "Tomorrow: 24°C" and then "5 days later: 24°C".

The numbers look equally precise, but they are not equally reliable. A forecast made one day before an event should normally be more accurate than one made seven days beforehand. The interesting question is how quickly that accuracy deteriorates.

This project measures that deterioration for hourly Sydney temperatures. It retrieves archived forecasts from Open-Meteo’s Previous Model Runs API, compares them with ERA5-Land reanalysis temperatures, and calculates three common forecast-verification metrics:

  • Bias, which shows whether the forecasts tend to be too warm or too cold.

  • Mean absolute error, which measures the typical size of an error.

  • Root mean squared error, which gives more weight to unusually large mistakes.

The script also checks whether accuracy changes between seasons and whether particularly hot conditions are more difficult to predict.


Imports and Project Configuration

The project uses requests for downloading data, pandas for restructuring and aggregating it, NumPy for calculations, and Matplotlib for the final figure. Similar to the project from the previous post we combine two API endpoints

PREVIOUS_RUNS_URL = "https://previous-runs-api.open-meteo.com/v1/forecast"
ARCHIVE_URL = "https://archive-api.open-meteo.com/v1/archive"

The Previous Model Runs API provides historical forecasts. These are not historical observations, they are forecasts that were available before the weather happened.

The Archive API provides historical weather data. In this project, it supplies ERA5-Land reanalysis temperatures that act as the reference values.

As usual we define location coordinates and timzone as well as a start and ende date, for the latter two we use whole year 2025. Finally we need the lead times for the forecasts, which we define as one through seven days.

LEAD_DAYS = tuple(range(1, 8))

The reference model is fixed to ERA5-Land.

REFERENCE_MODEL = "era5_land"

The archived forecast model can either be left unspecified or fixed to a supported model.

FORECAST_MODEL: str | None = None

When this is None, Open-Meteo uses its archived best-match forecast configuration. Supplying a model name instead makes the comparison model-specific. The project also defines what counts as unusually hot weather.

HOT_TEMPERATURE_QUANTILE = 0.95

A value of 0.95 means that temperatures in the highest 5% of the reference distribution are labelled as hot. This way we adapt to the actual temperature distribution rather than relying on an arbitrary fixed threshold. We will not fix the exact parameter we will request as they will depend on the precise lead time we use. For all of them, we can reuse the API request function from the previous projects.


Defining Sydney’s Seasons

Meteorological seasons in Australia do not line up with those in the Northern Hemisphere.

The script maps each month to its Australian season.

MONTH_TO_SEASON = {
    12: "Summer",
    1: "Summer",
    2: "Summer",
    3: "Autumn",
    4: "Autumn",
    5: "Autumn",
    6: "Winter",
    7: "Winter",
    8: "Winter",
    9: "Spring",
    10: "Spring",
    11: "Spring",
}

SEASON_ORDER = ["Summer", "Autumn", "Winter", "Spring"]

The dictionary is later used to attach a season to every hourly observation.

SEASON_ORDER is not needed for the calculations, but it keeps the final plot in a familiar chronological order instead of allowing alphabetical sorting to put Autumn first.


Downloading Forecasts from Previous Model Runs

Open-Meteo names its historical lead-time variables according to how many days before the valid time the forecast was made.

The script constructs those variable names automatically.

For lead times from one to seven days, this creates names such as:

temperature_2m_previous_day1
temperature_2m_previous_day2
temperature_2m_previous_day3

and so on through day seven. The request parameters are then assembled as in the projects before.

A fixed forecast model is included only when one has been configured. Finally, the data is requested and converted, we omit the code for the conversion function. It does the obvious check that there is hourly data and converts time data to datetime formats.

    payload = request_json(PREVIOUS_RUNS_URL, params)
    return hourly_to_dataframe(payload)

The resulting table is wide. Each row represents one valid hour, while each forecast lead time occupies a separate column.

Conceptually, it looks like this:

time

previous day 1

previous day 2

previous day 3

2025-01-01 00:00

21.3

20.8

22.1

2025-01-01 01:00

21.0

20.5

21.9

The temperatures all refer to the same valid hour. The difference is when each prediction was made, one day in advance all the way to seven days in advance.


Downloading the Reference Temperature

The reference request uses the same location, date range, and time zone.

The returned temperature column is renamed immediately to avoid confusion.

It should be made clear again that ERA5-Land is a reanalysis product rather than a direct thermometer reading. Reanalysis combines observations with a numerical weather model to produce a consistent historical estimate. That makes it convenient and reproducible, but it should not automatically be treated as perfect ground truth.

A formal operational verification study would normally prefer quality-controlled station observations when suitable data is available.


Turning the Forecast Table into Verification Rows

The forecast response is initially stored in wide format:

time | day1 forecast | day2 forecast | ... | day7 forecast

That layout is useful for viewing the raw response but inconvenient for grouped analysis. We want one row for every combination of valid time and lead day:

time | lead_day | forecast_temperature

The first step is to rebuild the expected variable names and check that they were actually returned.

This validation matters because model archives are not always complete. A missing forecast column should produce a clear error rather than silently disappearing from the analysis.

The table is then reshaped with melt().

After melting, a row might look like this:

time

forecast_variable

forecast_temperature

2025-01-01 00:00

temperature_2m_previous_day3

22.1

The lead day is still hidden inside the variable name. A regular expression extracts it and converts it to an integer.


Matching Forecasts with Reference Values

The long forecast table is merged with the ERA5-Land table using valid time.

The merge has a few deliberate choices. An inner join keeps only timestamps available in both datasets. There is little value in retaining a forecast when no reference value exists for evaluating it.

The many_to_one validation describes the expected relationship:

  • There are many forecast rows for each time because every time has several lead days.

  • There should be only one reference temperature for each time.

If the reference table unexpectedly contains duplicate timestamps, pandas raises an error instead of multiplying rows during the merge.

Missing temperatures are removed next.

The script should also check if anything remains, since an empty table can occur when a model archive does not cover the selected period or when the two API responses contain no matching timestamps.


Calculating Forecast Errors

Each verification row now contains:

  • A valid time.

  • A lead day.

  • A forecast temperature.

  • A reference temperature.

That is enough to calculate the basic error.

For example:

Forecast: 27°C
Reference: 25°C
Error: +2°C

For ease of reference also store the absolute and squared error in respective columns.


Attaching Seasons to Each Hour

The timestamp provides the month, and the month provides the season.

This way, every forecast-reference pair now carries a label such as "Summer" or "Winter".

This allows the same error calculations to be repeated separately for each season.

The aim is not simply to see whether winter temperatures are lower. The temperatures themselves are already accounted for in the error calculation. The question is whether the forecast misses are systematically larger in one season than another.


Identifying the Hottest Five Percent of Hours

The script calculates the 95th percentile of all reference temperatures.

Temperatures at or above that threshold are labelled as belonging to the hottest 5%.

Using a quantile has two advantages. First, it guarantees a useful number of hot cases even when the analysis period changes. Second, it defines extreme heat relative to the local dataset. The disadvantage is that “hottest 5%” is a statistical definition, not a health or meteorological warning threshold. It should not be interpreted as meaning that every included hour was dangerously hot.

This completes the construction of the verification table.


Calculating Forecast Skill

The script creates three summary tables in a function calculate_skill_tables.

The first table groups all observations by forecast lead day.

This produces one row for each lead time. We obtain the mean bias, so simply the mean of the error, the mean of the absolute error and the root of the mean squared error. We also store the sample size.

The second dataframe seasonal_skill, is obtained in nearly the same way, except for grouping by both season and lead_day.

The third dataframe extreme_skill, is obtained grouping by both temperature_group and lead_day.


Plotting Overall Forecast Error

The final figure contains two panels. The first panel plots overall MAE, RMSE, and bias against lead time. The second panel plots seasonal MAE. With the data this looks as follows

We only plot the mean absolute error for the seasons here. The script will then also save the various dataframes as CSV files.


What the Project Actually Measures

The project does not ask whether a forecast for a particular calendar date was correct in some general sense. It creates thousands of individual forecast-reference comparisons.

For every valid hour, it asks:

What temperature was predicted one day beforehand?
What temperature was predicted two days beforehand?
...
What temperature was predicted seven days beforehand?
What reference temperature was later assigned to that hour?

Each comparison becomes one verification row. Those rows are then grouped by lead time, season, or temperature category. That structure is what makes it possible to move from individual forecast errors to broader statements about forecast skill.

A result might show that one-day forecasts have an MAE of around 1°C while seven-day forecasts have an MAE closer to 2.5°C. It might also show that the model has almost no overall bias but tends to underestimate temperatures during the hottest hours.

Those are different kinds of forecast behaviour, and no single metric captures all of them. We only plotted two possibilities above.


Important Limitation: Reference Does Not Mean Perfect

ERA5-Land provides a practical reference because it is spatially consistent, freely accessible, and available through the same API ecosystem. It is nevertheless a model-assisted reconstruction of historical conditions.

The analysis therefore measures agreement between archived forecasts and ERA5-Land. It does not directly measure agreement with a calibrated thermometer at Sydney Observatory, the airport, or another weather station. That distinction matters most when interpreting small differences. A change in MAE of several degrees would be substantial. A difference of a few hundredths of a degree may say more about the reference dataset and sampling choices than about meaningful forecast performance.

For a more rigorous extension, the same verification workflow could be reused with quality-controlled station observations. The reshaping, merging, error calculations, and grouped summaries would remain almost unchanged.


From Patterns to Predictions

The second and third projects examine the same weather data from opposite directions.

The weather-regime project looks backward and asks whether Sydney’s months form recurring patterns without relying on traditional season labels. The forecast-skill project looks at predictions after their valid time and measures how error changes with lead time.

Together, they show two useful ways to move beyond simply downloading weather data: historical records can reveal structure, while forecast archives can test performance.

Neither result is permanent truth. The clusters depend on the selected variables, scaling, number of regimes, and historical period. Forecast accuracy depends on the model, reference data, location, season, and analysis window.

That sensitivity is part of the analysis. The assumptions are visible and can be changed: four clusters can become five, new variables can be added, ERA5-Land can be replaced with station observations, and one year of forecast verification can become ten.

Open-Meteo handles the data plumbing, but the interesting work begins after the JSON arrives.

Sydney may still refuse to behave like a city with four orderly seasons, and next week’s forecast may still change tomorrow.

At least now both forms of disobedience can be exported to CSV.


Comments


bottom of page