14  practice

Download this dataset for the weather in Reykjavík, Iceland from January 1st to December 31st, 1990.

You can also explore windows and kernels in the smoothing lab.

Use the following code to load the dataset:

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt

filename = "../archive/data/Reykjaviik1990.csv"
df = pd.read_csv(filename, parse_dates=['time'], index_col='time')
# convert column temperature_2m from kelvin to celsius
df['temperature_2m'] = df['temperature_2m'] - 273.15
df
longitude latitude temperature_2m dewpoint_temperature_2m volumetric_soil_water_layer_1 volumetric_soil_water_layer_2 volumetric_soil_water_layer_3 total_precipitation surface_net_solar_radiation potential_evaporation
time
1990-01-01 00:00:00 -21.707789 64.072338 4.371652 277.083878 0.367294 0.347504 0.282730 0.012072 53890.0 -0.005887
1990-01-01 01:00:00 -21.707789 64.072338 4.441705 277.134430 0.362778 0.349655 0.283630 0.000704 0.0 -0.000216
1990-01-01 02:00:00 -21.707789 64.072338 4.378412 277.056793 0.361160 0.350479 0.284637 0.001431 0.0 -0.000445
1990-01-01 03:00:00 -21.707789 64.072338 4.262323 276.934082 0.360275 0.350983 0.285645 0.002198 0.0 -0.000681
1990-01-01 04:00:00 -21.707789 64.072338 4.136362 276.805954 0.360733 0.351486 0.286667 0.003100 0.0 -0.000920
... ... ... ... ... ... ... ... ... ... ...
1990-12-30 19:00:00 -21.707789 64.072338 -11.145605 257.818130 0.244446 0.246048 0.264557 0.000062 137160.0 -0.000774
1990-12-30 20:00:00 -21.707789 64.072338 -10.969962 258.685898 0.244370 0.245972 0.264374 0.000062 137160.0 -0.000778
1990-12-30 21:00:00 -21.707789 64.072338 -10.729712 259.601089 0.244278 0.245895 0.264175 0.000062 137160.0 -0.000776
1990-12-30 22:00:00 -21.707789 64.072338 -10.529898 260.376083 0.244247 0.245819 0.263992 0.000062 137160.0 -0.000770
1990-12-30 23:00:00 -21.707789 64.072338 -10.341437 261.098175 0.244247 0.245728 0.263824 0.000062 137160.0 -0.000761

8736 rows × 10 columns

fig, ax = plt.subplots()
ax.plot(df['temperature_2m']);

fig, ax = plt.subplots()
ax.plot(df['total_precipitation']);

14.1 smoothing

Make the following graphs. Click a question to reveal the steps, then open a possible solution for the code.

Select the temperature column and plot it against the datetime index. The loading code above has already converted temperature to Celsius. The supplied file ends on December 30, so the graph stops there.

a possible solution
temp = df['temperature_2m']
fig, ax = plt.subplots(figsize=(10, 4))
ax.plot(temp.index, temp, color='0.5')
ax.set(xlabel='Date', ylabel='Temperature (°C)', title='Temperature in Reykjavík, 1990')
plt.show()
  1. Plot the original observations.
  2. Calculate a rolling mean for each window width.
  3. Plot those means on the same axes.

Here we use centered windows of 1, 7, and 30 days. A wider window suppresses short fluctuations but also rounds peaks and hides short events. With hourly observations, min_periods=24 * days requires a complete window, leaving gaps at the edges.

a possible solution
temp = df['temperature_2m']
fig, ax = plt.subplots(figsize=(11, 4))
ax.plot(temp.index, temp, color='0.75', label='Original')
for days in [1, 7, 30]:
    smoothed = temp.rolling(f'{days}D', center=True, min_periods=24 * days).mean()
    ax.plot(smoothed.index, smoothed, label=f'{days}-day mean')
ax.set(xlabel='Date', ylabel='Temperature (°C)', title='Different smoothing windows')
ax.legend()
plt.show()

Use the same window for all three statistics. The mean describes the average temperature in the window; the minimum and maximum describe its extremes. Their separation shows the range of temperatures inside the window, not the uncertainty of the mean.

a possible solution
temp = df['temperature_2m']
window = temp.rolling('7D', center=True, min_periods=7 * 24)
fig, ax = plt.subplots(figsize=(11, 4))
ax.plot(temp.index, temp, color='0.8', label='Original')
ax.plot(temp.index, window.mean(), label='7-day mean')
ax.plot(temp.index, window.min(), label='7-day minimum')
ax.plot(temp.index, window.max(), label='7-day maximum')
ax.set(xlabel='Date', ylabel='Temperature (°C)', title='Rolling mean, minimum and maximum')
ax.legend()
plt.show()

To accumulate precipitation amounts, use sum: add the amounts within each seven-day window. A mean would give the average amount per observation.

The code below uses a trailing window, so each result describes the previous seven days. It assumes each value is the amount for its sampling interval. For another rainfall product, check whether values represent interval amounts, rates, or running cumulative totals before choosing the calculation.

a possible solution
rain = df['total_precipitation']
weekly_rain = rain.rolling('7D', min_periods=7 * 24).sum()
fig, ax = plt.subplots(figsize=(11, 4))
ax.plot(weekly_rain.index, weekly_rain)
ax.set(xlabel='Date', ylabel='7-day precipitation total (source units)',
       title='Precipitation accumulated over the previous seven days')
plt.show()

14.2 custom function

Write the following custom functions. Click a question to reveal the steps and code.

Compare each valid temperature with zero. This produces True for freezing observations and False for the others. Summing these values counts True as 1 and False as 0.

a possible solution
def count_freezing(temperature):
    valid = temperature.dropna()
    return (valid < 0).sum()

print(count_freezing(df['temperature_2m']))

Divide the number of freezing observations by the number of valid observations. This is also an estimate of the fraction of time below freezing when observations are equally spaced, as in this hourly dataset.

Exclude missing values from the denominator. If there are no valid observations, return NaN. With irregular sampling, a fraction of rows would not necessarily be a fraction of time.

a possible solution
def fraction_freezing(temperature):
    valid = temperature.dropna()
    if valid.empty:
        return np.nan
    return (valid < 0).sum() / len(valid)

print(fraction_freezing(df['temperature_2m']))
  1. Run the two function definitions above.
  2. Pass each function to rolling(...).apply(...).
  3. Find the end of the window with the largest freezing fraction using idxmax().

Here we use trailing windows and require 720 hourly observations, so the first partial month cannot be mistaken for a complete 30-day result. The interval is open on the left and closed on the right: (end − 30 days, end]. If several windows tie for the maximum, idxmax() returns the first.

a possible solution
temp = df['temperature_2m']
window = temp.rolling('30D', min_periods=30 * 24)
freezing_count = window.apply(count_freezing, raw=False)
freezing_fraction = window.apply(fraction_freezing, raw=False)

end = freezing_fraction.idxmax()
start = end - pd.Timedelta(days=30)
print(f'Highest freezing fraction: {freezing_fraction.loc[end]:.1%}')
print(f'Window: after {start}, up to and including {end}')

fig, axes = plt.subplots(2, 1, sharex=True, figsize=(11, 6))
axes[0].plot(freezing_count.index, freezing_count)
axes[0].set(ylabel='Freezing observations', title='Freezing during the previous 30 days')
axes[1].plot(freezing_fraction.index, freezing_fraction)
axes[1].set(xlabel='Date', ylabel='Fraction below 0 °C', ylim=(0, 1))
for ax in axes:
    ax.axvline(end, color='tab:red', linestyle='--', label='First highest-fraction window')
    ax.legend()
plt.tight_layout()
plt.show()