|
1
|
|
|
""" |
|
2
|
|
|
Module for performing some statistic functions. |
|
3
|
|
|
""" |
|
4
|
|
|
|
|
5
|
|
|
import numpy as np |
|
6
|
|
|
from gammapy.stats.fit_statistics import cash, wstat |
|
7
|
|
|
from scipy.stats import chi2, norm |
|
8
|
|
|
|
|
9
|
|
|
__all__ = [ |
|
10
|
|
|
"check_model_preference_aic", |
|
11
|
|
|
"check_model_preference_lrt", |
|
12
|
|
|
"fetch_pivot_energy", |
|
13
|
|
|
"get_chi2_sig_pval", |
|
14
|
|
|
"get_goodness_of_fit_stats", |
|
15
|
|
|
"get_ts_target", |
|
16
|
|
|
] |
|
17
|
|
|
|
|
18
|
|
|
|
|
19
|
|
|
def get_chi2_sig_pval(test_stat, ndof): |
|
20
|
|
|
""" |
|
21
|
|
|
Using the log-likelihood value for a model fitting to data, along with the |
|
22
|
|
|
total degrees of freedom, evaluate the significance value in terms of gaussian |
|
23
|
|
|
distribution along with one-tailed p-value for the fitting statistics. |
|
24
|
|
|
|
|
25
|
|
|
In Gammapy, for 3D analysis, cash statistics is used, while for 1D analysis, |
|
26
|
|
|
wstat statistics is used. Check the documentation for more details |
|
27
|
|
|
https://docs.gammapy.org/1.3/user-guide/stats/index.html |
|
28
|
|
|
|
|
29
|
|
|
Parameters |
|
30
|
|
|
---------- |
|
31
|
|
|
test_stat: float |
|
32
|
|
|
The test statistic (-2 ln L) value of the fitting. |
|
33
|
|
|
ndof: int |
|
34
|
|
|
Total number of degrees of freedom. |
|
35
|
|
|
|
|
36
|
|
|
Returns |
|
37
|
|
|
------- |
|
38
|
|
|
chi2_sig: float |
|
39
|
|
|
significance (Chi2) of the likelihood of fit model estimated in |
|
40
|
|
|
Gaussian distribution. |
|
41
|
|
|
pval: float |
|
42
|
|
|
p-value for the model fitting |
|
43
|
|
|
|
|
44
|
|
|
""" |
|
45
|
|
|
pval = chi2.sf(test_stat, ndof) |
|
46
|
|
|
chi2_sig = norm.isf(pval / 2) |
|
47
|
|
|
|
|
48
|
|
|
return chi2_sig, pval |
|
49
|
|
|
|
|
50
|
|
|
|
|
51
|
|
|
def check_model_preference_lrt(test_stat_1, test_stat_2, ndof_1, ndof_2): |
|
52
|
|
|
""" |
|
53
|
|
|
Log-likelihood ratio test. Checking the preference of a "nested" spectral |
|
54
|
|
|
model2 (observed), over a primary model1. |
|
55
|
|
|
|
|
56
|
|
|
Parameters |
|
57
|
|
|
---------- |
|
58
|
|
|
test_stat_1: float |
|
59
|
|
|
The test statistic (-2 ln L) of the Fit result of the primary spectral model. |
|
60
|
|
|
test_stat_2: float |
|
61
|
|
|
The test statistic (-2 ln L) of the Fit result of the nested spectral model. |
|
62
|
|
|
ndof_1: int |
|
63
|
|
|
Number of degrees of freedom for the primary model |
|
64
|
|
|
ndof_2: int |
|
65
|
|
|
Number of degrees of freedom for the nested model |
|
66
|
|
|
|
|
67
|
|
|
Returns |
|
68
|
|
|
------- |
|
69
|
|
|
p_value: float |
|
70
|
|
|
p-value for the ratio of the likelihoods |
|
71
|
|
|
gaussian_sigmas: float |
|
72
|
|
|
significance (Chi2) of the ratio of the likelihoods estimated in |
|
73
|
|
|
Gaussian distribution. |
|
74
|
|
|
n_dof: int |
|
75
|
|
|
number of degrees of freedom or free parameters between primary and |
|
76
|
|
|
nested model. |
|
77
|
|
|
""" |
|
78
|
|
|
n_dof = ndof_1 - ndof_2 |
|
79
|
|
|
|
|
80
|
|
|
if n_dof < 1: |
|
81
|
|
|
print(f"DoF is lower in {ndof_1} compared to {ndof_2}") |
|
82
|
|
|
|
|
83
|
|
|
return np.nan, np.nan, n_dof |
|
84
|
|
|
|
|
85
|
|
|
gaussian_sigmas, p_value = get_chi2_sig_pval(test_stat_1 - test_stat_2, n_dof) |
|
86
|
|
|
|
|
87
|
|
|
return p_value, gaussian_sigmas, n_dof |
|
88
|
|
|
|
|
89
|
|
|
|
|
90
|
|
|
def check_model_preference_aic(list_stat, list_dof): |
|
91
|
|
|
""" |
|
92
|
|
|
Akaike Information Criterion (AIC) preference over a list of stat and DoF |
|
93
|
|
|
(degree of freedom) to get relative likelihood of a given list of best-fit |
|
94
|
|
|
models. |
|
95
|
|
|
|
|
96
|
|
|
Parameters |
|
97
|
|
|
---------- |
|
98
|
|
|
list_wstat: list |
|
99
|
|
|
List of stat or -2 Log likelihood values for a list of models. |
|
100
|
|
|
list_dof: list |
|
101
|
|
|
List of degrees of freedom or list of free parameters, for a list of models. |
|
102
|
|
|
|
|
103
|
|
|
Returns |
|
104
|
|
|
------- |
|
105
|
|
|
list_rel_p: list |
|
106
|
|
|
List of relative likelihood probabilities, for a list of models. |
|
107
|
|
|
""" |
|
108
|
|
|
list_aic_stat = [] |
|
109
|
|
|
for stat, dof in zip(list_stat, list_dof, strict=True): |
|
110
|
|
|
aic_stat = stat + 2 * dof |
|
111
|
|
|
list_aic_stat.append(aic_stat) |
|
112
|
|
|
list_aic_stat = np.array(list_aic_stat) |
|
113
|
|
|
|
|
114
|
|
|
aic_stat_min = np.min(list_aic_stat) |
|
115
|
|
|
|
|
116
|
|
|
list_b_stat = [] |
|
117
|
|
|
for aic in list_aic_stat: |
|
118
|
|
|
b_stat = np.exp((aic_stat_min - aic) / 2) |
|
119
|
|
|
list_b_stat.append(b_stat) |
|
120
|
|
|
list_b_stat = np.array(list_b_stat) |
|
121
|
|
|
|
|
122
|
|
|
list_rel_p = [] |
|
123
|
|
|
for b_stat in list_b_stat: |
|
124
|
|
|
rel_p = b_stat / np.sum(list_b_stat) |
|
125
|
|
|
list_rel_p.append(rel_p) |
|
126
|
|
|
list_rel_p = np.array(list_rel_p) |
|
127
|
|
|
|
|
128
|
|
|
return list_rel_p |
|
129
|
|
|
|
|
130
|
|
|
|
|
131
|
|
|
def get_goodness_of_fit_stats(datasets, instrument_spectral_info): |
|
132
|
|
|
""" |
|
133
|
|
|
Evaluating the Goodness of Fit statistics of the fitting of the model to |
|
134
|
|
|
the dataset. |
|
135
|
|
|
|
|
136
|
|
|
We first use the get_ts_target function to get the total test statistic for |
|
137
|
|
|
the (observed) best fit of the model to the data, and the (expected) |
|
138
|
|
|
perfect fit of model and data (model = data), for the given target source |
|
139
|
|
|
region/pixel. |
|
140
|
|
|
|
|
141
|
|
|
We then evaluate the total number of Degrees of Freedom for the Fit as the |
|
142
|
|
|
difference between the number of relevant energy bins used in the evaluation |
|
143
|
|
|
and the number of free model parameters. |
|
144
|
|
|
|
|
145
|
|
|
The fit statistics difference is used as the test statistic value for |
|
146
|
|
|
get_chi2_sig_pval function along with the total number of degrees of freedom |
|
147
|
|
|
to get the final statistics for the goodness of fit. |
|
148
|
|
|
|
|
149
|
|
|
The fit statistics information is updated in the dict object provided and |
|
150
|
|
|
a logging message is passed. |
|
151
|
|
|
|
|
152
|
|
|
Parameter |
|
153
|
|
|
--------- |
|
154
|
|
|
datasets: `gammapy.datasets.Datasets` |
|
155
|
|
|
List of Datasets object, which can contain 3D and/or 1D datasets |
|
156
|
|
|
instrument_spectral_info: dict |
|
157
|
|
|
Dict of information for storing relevant fit stats |
|
158
|
|
|
|
|
159
|
|
|
Return |
|
160
|
|
|
------ |
|
161
|
|
|
instrument_spectral_info: dict |
|
162
|
|
|
Filled Dict of information with relevant fit statistics |
|
163
|
|
|
stat_message: str |
|
164
|
|
|
String for logging the fit statistics |
|
165
|
|
|
""" |
|
166
|
|
|
stat_best_fit, stat_max_fit = get_ts_target(datasets) |
|
167
|
|
|
|
|
168
|
|
|
instrument_spectral_info["max_fit_stat"] = stat_max_fit |
|
169
|
|
|
instrument_spectral_info["best_fit_stat"] = stat_best_fit |
|
170
|
|
|
ndof = instrument_spectral_info["DoF"] |
|
171
|
|
|
stat_diff_gof = stat_best_fit - stat_max_fit |
|
172
|
|
|
|
|
173
|
|
|
fit_chi2_sig, fit_pval = get_chi2_sig_pval(stat_diff_gof, ndof) |
|
174
|
|
|
|
|
175
|
|
|
instrument_spectral_info["fit_chi2_sig"] = fit_chi2_sig |
|
176
|
|
|
instrument_spectral_info["fit_pval"] = fit_pval |
|
177
|
|
|
|
|
178
|
|
|
stat_message = "The Chi2/dof value of the goodness of Fit is " |
|
179
|
|
|
stat_message += f"{stat_diff_gof:.2f}/{ndof}\nand the p-value is {fit_pval:.3e} " |
|
180
|
|
|
stat_message += f"and in Significance {fit_chi2_sig:.2f} sigmas" |
|
181
|
|
|
stat_message += f"\nwith best fit TS (Observed) as {stat_best_fit:.3f} " |
|
182
|
|
|
stat_message += f"and max fit TS (Expected) as {stat_max_fit:.3f}" |
|
183
|
|
|
|
|
184
|
|
|
return instrument_spectral_info, stat_message |
|
185
|
|
|
|
|
186
|
|
|
|
|
187
|
|
|
def get_ts_target(datasets): |
|
188
|
|
|
""" |
|
189
|
|
|
From a given list of DL4 datasets, with assumed associated models, estimate |
|
190
|
|
|
the total test statistic values, in the given target source region/pixel, |
|
191
|
|
|
for the (observed) best fit of the model to the data, and the (expected) |
|
192
|
|
|
perfect fit of model and data (model = data). |
|
193
|
|
|
|
|
194
|
|
|
For consistency in the evaluation of the statistic values, we will use the |
|
195
|
|
|
basic Fit Statistic functions in Gammapy for Poisson Data: |
|
196
|
|
|
|
|
197
|
|
|
* `cash <https://docs.gammapy.org/1.3/api/gammapy.stats.cash.html>`_ |
|
198
|
|
|
|
|
199
|
|
|
* `wstat <https://docs.gammapy.org/1.3/api/gammapy.stats.wstat.html>`_ |
|
200
|
|
|
|
|
201
|
|
|
For the different type of Statistics used in Gammapy for 3D/1D datasets, |
|
202
|
|
|
and for our use case of getting the best fit and perfect fit, we will pass |
|
203
|
|
|
the appropriate values, by adapting to the following methods, |
|
204
|
|
|
|
|
205
|
|
|
* Best Fit (Observed): |
|
206
|
|
|
|
|
207
|
|
|
* `Cash stat_array <https://docs.gammapy.org/1.3/api/gammapy.datasets.MapDataset.html#gammapy.datasets.MapDataset.stat_array # noqa>`_ |
|
208
|
|
|
|
|
209
|
|
|
* `Wstat stat_array <https://docs.gammapy.org/1.3/api/gammapy.datasets.MapDatasetOnOff.html#gammapy.datasets.MapDatasetOnOff.stat_array # noqa>`_ |
|
210
|
|
|
|
|
211
|
|
|
* Perfect Fit (Expected): |
|
212
|
|
|
|
|
213
|
|
|
* `Cash stat_max <https://docs.gammapy.org/1.3/api/gammapy.stats.CashCountsStatistic.html#gammapy.stats.CashCountsStatistic.stat_max # noqa>`_ |
|
214
|
|
|
|
|
215
|
|
|
* `Wstat stat_max <https://docs.gammapy.org/1.3/api/gammapy.stats.WStatCountsStatistic.html#gammapy.stats.WStatCountsStatistic.stat_max # noqa>`_ |
|
216
|
|
|
|
|
217
|
|
|
Parameter |
|
218
|
|
|
--------- |
|
219
|
|
|
datasets: `gammapy.datasets.Datasets` |
|
220
|
|
|
List of Datasets object, which can contain 3D and/or 1D datasets |
|
221
|
|
|
|
|
222
|
|
|
Return |
|
223
|
|
|
------ |
|
224
|
|
|
stat_best_fit: float |
|
225
|
|
|
Total sum of test statistic of the best fit of model to data, summed |
|
226
|
|
|
over all energy bins. |
|
227
|
|
|
stat_max_fit: float |
|
228
|
|
|
Test statistic difference of the perfect fit of model to data summed |
|
229
|
|
|
over all energy bins. |
|
230
|
|
|
""" # noqa |
|
231
|
|
|
stat_best_fit = 0 |
|
232
|
|
|
stat_max_fit = 0 |
|
233
|
|
|
|
|
234
|
|
|
for data in datasets: |
|
235
|
|
|
match data.stat_type: |
|
236
|
|
|
case "cash": |
|
237
|
|
|
# Assuming that the Counts Map is created with the target source as its center |
|
238
|
|
|
region = data.counts.geom.center_skydir |
|
239
|
|
|
|
|
240
|
|
|
counts_on = (data.counts.copy() * data.mask).get_spectrum(region).data |
|
241
|
|
|
mu_on = (data.npred() * data.mask).get_spectrum(region).data |
|
242
|
|
|
|
|
243
|
|
|
stat_best_fit += np.nansum(cash(n_on=counts_on, mu_on=mu_on).ravel()) |
|
244
|
|
|
stat_max_fit += np.nansum(cash(n_on=counts_on, mu_on=counts_on).ravel()) |
|
245
|
|
|
|
|
246
|
|
|
case "wstat": |
|
247
|
|
|
# Assuming that the Counts Map is created with the target source as its center |
|
248
|
|
|
region = data.counts.geom.center_skydir |
|
249
|
|
|
|
|
250
|
|
|
counts_on = (data.counts.copy() * data.mask).get_spectrum(region).data |
|
251
|
|
|
counts_off = np.nan_to_num((data.counts_off * data.mask).get_spectrum(region)).data |
|
252
|
|
|
|
|
253
|
|
|
# alpha is evaluated by acceptance ratios, and |
|
254
|
|
|
# Background is evaluated with given alpha and counts_off, |
|
255
|
|
|
# but for alpha to be of the same shape (in the target region), |
|
256
|
|
|
# it will be reevaluated |
|
257
|
|
|
bkg = np.nan_to_num((data.background * data.mask).get_spectrum(region)) |
|
258
|
|
|
|
|
259
|
|
|
with np.errstate(invalid="ignore", divide="ignore"): |
|
260
|
|
|
alpha = bkg / counts_off |
|
261
|
|
|
mu_signal = np.nan_to_num((data.npred_signal() * data.mask).get_spectrum(region)).data |
|
262
|
|
|
max_pred = counts_on - bkg |
|
263
|
|
|
|
|
264
|
|
|
stat_best_fit += np.nansum(wstat(n_on=counts_on, n_off=counts_off, alpha=alpha, mu_sig=mu_signal)) |
|
265
|
|
|
stat_max_fit += np.nansum(wstat(n_on=counts_on, n_off=counts_off, alpha=alpha, mu_sig=max_pred)) |
|
266
|
|
|
|
|
267
|
|
|
case "chi2": |
|
268
|
|
|
# For FluxxPointsDataset |
|
269
|
|
|
stat_best_fit += np.nansum(data.stat_array()) |
|
270
|
|
|
stat_max_fit += len(data.data.dnde.data) |
|
271
|
|
|
|
|
272
|
|
|
return stat_best_fit, stat_max_fit |
|
273
|
|
|
|
|
274
|
|
|
|
|
275
|
|
|
def fetch_pivot_energy(analysis): |
|
276
|
|
|
""" |
|
277
|
|
|
Using an 'AsgardpyAnalysis' object to get the pivot energy for a given dataset |
|
278
|
|
|
and fit model, using the pivot_energy function. |
|
279
|
|
|
|
|
280
|
|
|
Returns |
|
281
|
|
|
------- |
|
282
|
|
|
pivot energy : `~astropy.units.Quantity` |
|
283
|
|
|
The energy at which the statistical error in the computed flux is smallest. |
|
284
|
|
|
If no minimum is found, NaN will be returned. |
|
285
|
|
|
""" |
|
286
|
|
|
# Check if DL4 datasets are created, and if not, only run steps till Fit |
|
287
|
|
|
if len(analysis.datasets) == 0: |
|
288
|
|
|
steps = [step for step in analysis.config.general.steps if step != "flux-points"] |
|
289
|
|
|
|
|
290
|
|
|
analysis.run(steps) |
|
291
|
|
|
else: |
|
292
|
|
|
analysis.run(["fit"]) |
|
293
|
|
|
|
|
294
|
|
|
analysis.get_correct_intrinsic_model() |
|
295
|
|
|
pivot = analysis.model_deabs.spectral_model.pivot_energy |
|
296
|
|
|
|
|
297
|
|
|
return pivot |
|
298
|
|
|
|