diff --git a/src/modelskill/matching.py b/src/modelskill/matching.py index 38e5c913a..30e0e3709 100644 --- a/src/modelskill/matching.py +++ b/src/modelskill/matching.py @@ -378,6 +378,16 @@ def _match_single_obs( return None matched_data.attrs["weight"] = obs.weight + # The dummy is extracted from the whole observation, but its mean must be the + # mean of what is compared, which is known only after matching. + obs_mean = float(matched_data["Observation"].mean()) + for m in models: + if isinstance(m, DummyModelResult) and m.strategy == "mean": + matched_data[m.name] = matched_data[m.name].copy( + data=np.full(matched_data[m.name].shape, obs_mean) + ) + raw_mod_data[m.name].data[m.name][:] = obs_mean + return Comparer(matched_data=matched_data, raw_mod_data=raw_mod_data) diff --git a/src/modelskill/model/dummy.py b/src/modelskill/model/dummy.py index 58a33c790..ddc99dcb6 100644 --- a/src/modelskill/model/dummy.py +++ b/src/modelskill/model/dummy.py @@ -22,7 +22,12 @@ class DummyModelResult: name : str, optional The name of the model result, by default 'dummy' strategy : str, optional - The strategy to use, 'mean' uses the mean of the observation, 'constant' uses the value given in data, by default 'constant' + The strategy to use, by default 'constant' + + - 'mean': the mean of the observation values that are compared. In `match()` + this is the mean over the time steps shared with the other models, so the + dummy has r2 = 0. For a long-term mean, pass it as `data`. + - 'constant': the value given in `data` Examples -------- diff --git a/tests/test_match.py b/tests/test_match.py index 0d0032269..7ff7f7089 100644 --- a/tests/test_match.py +++ b/tests/test_match.py @@ -918,13 +918,32 @@ def test_compare_model_vs_dummy_for_track(mr1, o3): cmp2 = ms.match(obs=o3, mod=[mr1, mr]) - # not identical to above since it is evaluated on a subset of the data - assert cmp2.score()["dummy"] == pytest.approx(1.225945) + # the mean is taken over the subset compared with SW_1, so r2 is still 0 + assert cmp2.score(metric="r2")["dummy"] == pytest.approx(0.0) # better than dummy 🙂 assert cmp2.score()["SW_1"] == pytest.approx(0.3524703) +def test_dummy_mean_is_mean_of_compared_observations(): + time = pd.date_range("2020-01-01", periods=10, freq="h") + obs = ms.PointObservation( + pd.Series(np.arange(10.0), index=time), name="obs", x=0.0, y=0.0 + ) + # model covers only the last 4 time steps + mod = ms.PointModelResult( + pd.Series(np.arange(6.0, 10.0) + 0.5, index=time[6:]), name="mod" + ) + dummy = ms.DummyModelResult(name="dummy", strategy="mean") + + cmp = ms.match(obs=obs, mod=[mod, dummy]) + + assert cmp.n_points == 4 + assert cmp.data["dummy"].values == pytest.approx([7.5] * 4) + assert cmp.score(metric="r2")["dummy"] == pytest.approx(0.0) + assert cmp.raw_mod_data["dummy"].values == pytest.approx([7.5] * 10) + + def test_match_obs_model_pos_args_wrong_order_helpful_error_message(): # match is pretty helpful in converting strings or dataset # so we need to use a ModelResult to trigger the error