Skip to content

Commit e36bc42

Browse files
committed
ft: added preliminary implementation of the brier score test, and an unit_test that only evaluates a forecast and plot the result.
1 parent 083c5ca commit e36bc42

File tree

2 files changed

+172
-0
lines changed

2 files changed

+172
-0
lines changed

csep/core/brier_evaluations.py

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
import numpy
2+
import scipy.stats
3+
import scipy.spatial
4+
5+
from csep.models import EvaluationResult
6+
from csep.core.exceptions import CSEPCatalogException
7+
8+
9+
def bier_score_ndarray(forecast, observations):
10+
""" Computes the brier score
11+
"""
12+
13+
observations = (observations >= 1).astype(int)
14+
prob_success = 1 - scipy.stats.poisson.cdf(0, forecast)
15+
brier = []
16+
17+
for p, o in zip(prob_success.ravel(), observations.ravel()):
18+
19+
if o == 0:
20+
brier.append(-2 * p ** 2)
21+
else:
22+
brier.append(-2 * (p - 1) ** 2)
23+
brier = numpy.sum(brier)
24+
25+
for n_dim in observations.shape:
26+
brier /= n_dim
27+
28+
return brier
29+
30+
31+
def _simulate_catalog(sim_cells, sampling_weights, sim_fore, random_numbers=None):
32+
# Modified this code to generate simulations in a way that every cell gets one earthquake
33+
# Generate uniformly distributed random numbers in [0,1), this
34+
if random_numbers is None:
35+
# Reset simulation array to zero, but don't reallocate
36+
sim_fore.fill(0)
37+
num_active_cells = 0
38+
while num_active_cells < sim_cells:
39+
random_num = numpy.random.uniform(0,1)
40+
loc = numpy.searchsorted(sampling_weights, random_num, side='right')
41+
if sim_fore[loc] == 0:
42+
sim_fore[loc] = 1
43+
num_active_cells = num_active_cells + 1
44+
else:
45+
# Find insertion points using binary search inserting to satisfy a[i-1] <= v < a[i]
46+
pnts = numpy.searchsorted(sampling_weights, random_numbers, side='right')
47+
# Create simulated catalog by adding to the original locations
48+
numpy.add.at(sim_fore, pnts, 1)
49+
50+
assert sim_fore.sum() == sim_cells, "simulated the wrong number of events!"
51+
return sim_fore
52+
53+
54+
def _brier_score_test(forecast_data, observed_data, num_simulations=1000, random_numbers=None,
55+
seed=None, use_observed_counts=True, verbose=True):
56+
""" Computes binary conditional-likelihood test from CSEP using an efficient simulation based approach.
57+
58+
Args:
59+
60+
"""
61+
62+
# Array-masking that avoids log singularities:
63+
forecast_data = numpy.ma.masked_where(forecast_data <= 0.0, forecast_data)
64+
65+
# set seed for the likelihood test
66+
if seed is not None:
67+
numpy.random.seed(seed)
68+
69+
# used to determine where simulated earthquake should be placed, by definition of cumsum these are sorted
70+
sampling_weights = numpy.cumsum(forecast_data.ravel()) / numpy.sum(forecast_data)
71+
72+
# data structures to store results
73+
sim_fore = numpy.zeros(sampling_weights.shape)
74+
simulated_brier = []
75+
n_active_cells = len(numpy.unique(numpy.nonzero(observed_data.ravel())))
76+
77+
# main simulation step in this loop
78+
for idx in range(num_simulations):
79+
if use_observed_counts:
80+
num_cells_to_simulate = int(n_active_cells)
81+
82+
if random_numbers is None:
83+
sim_fore = _simulate_catalog(num_cells_to_simulate,
84+
sampling_weights,
85+
sim_fore)
86+
else:
87+
sim_fore = _simulate_catalog(num_cells_to_simulate,
88+
sampling_weights,
89+
sim_fore,
90+
random_numbers=random_numbers[idx, :])
91+
92+
# compute Brier score
93+
current_brier = bier_score_ndarray(forecast_data.data, sim_fore)
94+
95+
# append to list of simulated Brier score
96+
simulated_brier.append(current_brier)
97+
98+
# just be verbose
99+
if verbose:
100+
if (idx + 1) % 100 == 0:
101+
print(f'... {idx + 1} catalogs simulated.')
102+
103+
# observed Brier score
104+
obs_brier = bier_score_ndarray(forecast_data.data, observed_data)
105+
106+
# quantile score
107+
qs = numpy.sum(simulated_brier <= obs_brier) / num_simulations
108+
109+
# float, float, list
110+
return qs, obs_brier, simulated_brier
111+
112+
113+
def brier_score_test(gridded_forecast,
114+
observed_catalog,
115+
num_simulations=1000,
116+
seed=None,
117+
random_numbers=None,
118+
verbose=False):
119+
""" Performs the Brier conditional test on Gridded Forecast using an Observed Catalog.
120+
121+
Normalizes the forecast so the forecasted rate are consistent with the observations. This modification
122+
eliminates the strong impact differences in the number distribution have on the forecasted rates.
123+
124+
"""
125+
126+
# grid catalog onto spatial grid
127+
try:
128+
_ = observed_catalog.region.magnitudes
129+
except CSEPCatalogException:
130+
observed_catalog.region = gridded_forecast.region
131+
gridded_catalog_data = observed_catalog.spatial_magnitude_counts()
132+
133+
# simply call likelihood test on catalog data and forecast
134+
qs, obs_brier, simulated_brier = _brier_score_test(
135+
gridded_forecast.data,
136+
gridded_catalog_data,
137+
num_simulations=num_simulations,
138+
seed=seed,
139+
random_numbers=random_numbers,
140+
use_observed_counts=True,
141+
verbose=verbose
142+
)
143+
144+
# populate result data structure
145+
result = EvaluationResult()
146+
result.test_distribution = simulated_brier
147+
result.name = 'Brier score-Test'
148+
result.observed_statistic = obs_brier
149+
result.quantile = qs
150+
result.sim_name = gridded_forecast.name
151+
result.obs_name = observed_catalog.name
152+
result.status = 'normal'
153+
result.min_mw = numpy.min(gridded_forecast.magnitudes)
154+
155+
return result
156+

tests/test_brier.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import csep
2+
import numpy
3+
from csep.core.brier_evaluations import brier_score_test
4+
from csep.utils.plots import plot_consistency_test
5+
from datetime import datetime
6+
7+
forecast = csep.load_gridded_forecast(csep.datasets.hires_ssm_italy_fname)
8+
9+
start = datetime(2010, 1, 1)
10+
end = datetime(2015, 1, 1)
11+
cat = csep.query_bsi(start, end, min_magnitude=5.0)
12+
cat.filter_spatial(region=csep.regions.italy_csep_region(
13+
magnitudes=numpy.arange(5.0, 9.0, 0.1)), in_place=True)
14+
15+
result = brier_score_test(forecast, cat)
16+
plot_consistency_test(result, show=True)

0 commit comments

Comments
 (0)