Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add detection utilities à la BgStats #569

Merged
merged 13 commits into from Jun 13, 2016
1 change: 1 addition & 0 deletions gammapy/data/__init__.py
Expand Up @@ -18,3 +18,4 @@ class InvalidDataError(Exception):
from .utils import *
from .observation_summary import *
from .pointing import *
from .observation_stats import *
217 changes: 217 additions & 0 deletions gammapy/data/observation_stats.py
@@ -0,0 +1,217 @@
# Licensed under a 3-clause BSD style license - see LICENSE.rst
from __future__ import absolute_import, division, print_function, unicode_literals
import astropy.units as u
from ..stats import Stats
from gammapy.stats import significance_on_off

__all__ = [
'ObservationStats',
'ObservationStatsList'
]


class ObservationStats(Stats):
"""Observation statistics.

Class allowing to summarize observation
(`~gammapy.data.DataStoreObservation) statistics
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One more fail on travis-ci in the docs build:
https://travis-ci.org/gammapy/gammapy/jobs/137211316#L1660

/home/travis/build/gammapy/gammapy/build/lib.linux-x86_64-2.7/gammapy/data/observation_stats.py:docstring of gammapy.data.ObservationStats:5: WARNING: Inline interpreted text or phrase reference start-string without end-string.

There's a missing backtick here, it should be:

(`~gammapy.data.DataStoreObservation`) statistics


Parameters
----------
n_on : int
Number of on events
n_off : int
Number of off events
a_on : float
Relative background exposure of the on region
a_off : float
Relative background exposure of the off region
obs_id : int
Id of the obervation
livetime : float
Livetime of the observation
alpha : float
Normalisation between the on and the off exposure
bg_rate : float
Background rate (/min)
gamma_rate : float
Gamma rate (/min)
"""

def __init__(self,
n_on=None, n_off=None, a_on=None, a_off=None,
obs_id=None, livetime=None, alpha=None,
gamma_rate=None, bg_rate=None):
super(ObservationStats, self).__init__(
n_on=n_on,
n_off=n_off,
a_on=a_on,
a_off=a_off
)

self.obs_id = obs_id
self.livetime = livetime
self.alpha_obs = alpha
self.gamma_rate = gamma_rate
self.bg_rate = bg_rate

@classmethod
def from_target(cls, obs, target, bg_estimate):
"""
Auxiliary constructor from an osbervation, a target
a background estimate

Parameters
----------
obs_table : `~gammapy.data.ObservationTable`
Observation index table
target_pos : `~astropy.coordinates.SkyCoord`
Target position
bg_method : str
Background estimation method
"""
n_on = cls._get_on_events(obs, target)
n_off = len(bg_estimate.off_events)
a_on = bg_estimate.a_on
a_off = bg_estimate.a_off

obs_id = obs.obs_id
livetime = obs.observation_live_time_duration
alpha = a_on / a_off

gamma_rate = n_on / livetime.to(u.min)
bg_rate = (alpha * n_off) / livetime.to(u.min)
stats = cls(n_on=n_on,
n_off=n_off,
a_on=a_on,
a_off=a_off,
obs_id=obs_id,
livetime=livetime,
alpha=alpha,
gamma_rate=gamma_rate,
bg_rate=bg_rate)
return stats

@property
def alpha(self):
"""Override member function from `~gammapy.stats.Stats`
to take into account weighted alpha by number of Off events
"""
return self.alpha_obs

@property
def sigma(self):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that property should be moved to the base class, no?
https://gammapy.readthedocs.io/en/latest/api/gammapy.stats.Stats.html

Note that it will still call the alpha from the subclass ... maybe you can just try moving it to the base class and see if all your tests still pass?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the class stats/Stats the property alpha returns the ratio of the exposures (a_on/a_off). When dealing with stacking of multiple observations, we usually average the normalisations with the number of off events, not with the mean acceptance. So I guess we need to re-implement the property alpha in the ObservationStats to deal with the weighting, right ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yes, I forgot about that tricky part for the alpha computation.

Note that at some point I added this standalone function:
http://docs.gammapy.org/en/latest/api/gammapy.stats.combine_stats.html

This has some version of the alpha computation for two observations (probably not debugged / tested).
It should somehow be integrated with the Stats class better.
Maybe it's possible to have a stack classmethod on the base Stats class as well, i.e. follow the design pattern you implemented for ObservationStats?

So I guess we need to re-implement the property alpha in the ObservationStats to deal with the weighting, right ?

I'm not sure. Currently your design is such that the stacked ObservationStats doesn't contain arrays of n_on, a_on, ... but just the stacked values, right? Then the weighting has to happen at stacking time, not alpha computation time, no?

Is it clear to you how to handle / resolve this?
If no, I could have a closer look and think about it, or we delay this issue to a future pull request.
Let me know what you prefer ...

"""Li-Ma significance for observation statistics (`float`)
"""
sigma = significance_on_off(
self.n_on, self.n_off, self.alpha, method='lima')
return sigma

@staticmethod
def _get_on_events(obs, target):
"""Number of ON events in the region of interest (`int`)
"""
idx = target.on_region.contains(obs.events.radec)
on_events = obs.events[idx]
return len(on_events)

@classmethod
def stack(cls, stats_list):
"""Stack statistics from a list of
`~gammapy.data.ObservationStats` and returns a new instance
of `~gammapy.data.ObservationStats`

Parameters
----------
stats_list : list
List of observation statistics `~gammapy.data.ObservationStats`

Returns
-------
total_stats : `~gammapy.data.ObservationStats`
Statistics for stacked observation
"""
n_on = 0
n_off = 0
a_on = 0
a_on_backup = 0
a_off = 0
a_off_backup = 0
obs_id = list()
livetime = 0
alpha = 0
alpha_backup = 0
gamma_rate = 0
bg_rate = 0
for stats in stats_list:
livetime += stats.livetime
n_on += stats.n_on
n_off += stats.n_off
a_on += stats.a_on * stats.n_off
a_on_backup += stats.a_on * stats.livetime.value
a_off += stats.a_off * stats.n_off
a_off_backup += stats.a_off * stats.livetime.value
alpha += stats.alpha * stats.n_off
alpha_backup += stats.alpha * stats.livetime.value
obs_id.append(stats.obs_id)
gamma_rate += stats.n_on - stats.alpha * stats.n_off
bg_rate += stats.n_off * stats.alpha

a_on /= n_off
a_off /= n_off
alpha /= n_off

# if no off events the weighting of alpha is done
# with the livetime
if n_off == 0:
alpha = alpha_backup / livetime.value
a_on = a_on_backup / livetime.value
a_off = a_off_backup / livetime.value

gamma_rate /= livetime.to(u.min)
bg_rate /= livetime.to(u.min)

total_stats = cls(
n_on=n_on,
n_off=n_off,
a_on=a_on,
a_off=a_off,
obs_id=obs_id,
livetime=livetime,
alpha=alpha,
gamma_rate=gamma_rate,
bg_rate=bg_rate
)
return total_stats

def __add__(self, other):
"""Add statistics from two observations
and returns new instance of `~gammapy.data.ObservationStats`
"""
return ObservationStats.stack(self, other)

def __str__(self):
"""Observation statistics report (`str`)
"""
ss = '*** Observation summary report ***\n'
if type(self.obs_id) is list:
obs_str = '[{0}-{1}]'.format(self.obs_id[0], self.obs_id[-1])
else:
obs_str = '{}'.format(self.obs_id)
ss += 'Observation Id: {}\n'.format(obs_str)
ss += 'Livetime: {}\n'.format(self.livetime.to(u.h))
ss += 'On events: {}\n'.format(self.n_on)
ss += 'Off events: {}\n'.format(self.n_off)
ss += 'Alpha: {}\n'.format(self.alpha)
ss += 'Bkg events in On region: {}\n'.format(self.background)
ss += 'Excess: {}\n'.format(self.excess)
ss += 'Gamma rate: {}\n'.format(self.gamma_rate)
ss += 'Bkg rate: {}\n'.format(self.bg_rate)
ss += 'Sigma: {}\n'.format(self.sigma)

return ss


class ObservationStatsList(list):
"""List of `~gammapy.data.ObservationList`
"""