Source code for moe.bandit.data_containers

# -*- coding: utf-8 -*-
"""Data containers convenient for/used to interact with bandit members."""
import pprint

import numpy


[docs]class SampleArm(object): """An arm (name, win, loss, total, variance) sampled from the objective function we are modeling/optimizing. This class is a representation of a "Sample Arm," which is defined by the four data members listed here. SampleArm is a convenient way of communicating data to the rest of the bandit library (via the HistoricalData container); it also provides a convenient grouping for interactive introspection. Users are not required to use SampleArm, iterables with the same data layout will suffice. :ivar win: (*float64 >= 0.0*) The amount won from playing this arm :ivar loss: (*float64 >= 0.0*) The amount loss from playing this arm :ivar total: (*int >= 0*) The number of times we have played this arm :ivar variance: (*float >= 0.0*) The variance of this arm, if there is no variance it is equal to None """ __slots__ = ('_win', '_loss', '_total', '_variance') def __init__(self, win=0.0, loss=0.0, total=0, variance=None): """Allocate and construct a new instance with the specified data fields; see class docstring for input descriptions.""" self._win = win self._loss = loss self._total = total self._variance = variance self.validate() def __str__(self): """Pretty print this object as a dict.""" return pprint.pformat(self.json_payload()) def __add__(self, sample_arm_to_add): """Overload Add operator to add ``sample_arm_to_add`` sampled arm results to this arm. :param sample_arm_to_add: arm samples to add to this arm :type sample_arm_to_add: a SampleArm object :return: new SampleArm that is a result of adding two arms :rtype: SampleArm :raise: ValueError when ``arm.variance`` or self.variance is not None. """ if self._variance is not None or sample_arm_to_add.variance is not None: raise ValueError('Cannot add arms when variance is not None! Please combine arms manually.') result = SampleArm(win=self._win + sample_arm_to_add.win, loss=self._loss + sample_arm_to_add.loss, total=self._total + sample_arm_to_add.total) result.validate() return result __radd__ = __add__ def __iadd__(self, sample_arm_to_add): """Overload in-place Add operator to add ``sample_arm_to_add`` sampled arm results to this arm in-place. :param sample_arm_to_add: arm samples to add to this arm :type sample_arm_to_add: a SampleArm object :return: this arm after adding ``sample_arm_to_add`` :rtype: SampleArm :raise: ValueError when ``arm.variance`` or self.variance is not None. """ if self._variance is not None or sample_arm_to_add.variance is not None: raise ValueError('Cannot add arms when variance is not None! Please combine arms manually.') self._win += sample_arm_to_add.win self._loss += sample_arm_to_add.loss self._total += sample_arm_to_add.total self.validate() return self
[docs] def json_payload(self): """Convert the sample_arm into a dict to be consumed by json for a REST request.""" return { 'win': self.win, 'loss': self.loss, 'total': self.total, 'variance': self.variance, }
[docs] def validate(self): """Check this SampleArm passes basic validity checks: all values are finite. :raises ValueError: if any member data is non-finite or out of range """ # check that all values are finite if self.win < 0.0 or not numpy.isfinite(self.win): raise ValueError('win = {0} is non-finite or negative!'.format(self.win)) if self.loss < 0.0 or not numpy.isfinite(self.loss): raise ValueError('loss = {0} is non-finite or negative!'.format(self.loss)) if self.total < 0 or not numpy.isfinite(self.total): raise ValueError('total = {0} is non-finite or negative!'.format(self.total)) if self.variance is not None and (self.variance < 0.0 or not numpy.isfinite(self.variance)): raise ValueError('variance = {0} is non-finite or negative!'.format(self.variance)) if self.total == 0 and not (self.win == 0.0 and self.loss == 0.0): raise ValueError('win or loss is not 0 when total is 0!') if self.variance is None and self.win > self.total: raise ValueError('win cannot be greater than total when default variance computation is used! Please specify variance.')
@property
[docs] def win(self): """Return the amount win, always greater than or equal to zero.""" return self._win
@property
[docs] def loss(self): """Return the amount loss, always greater than or equal to zero.""" return self._loss
@property
[docs] def total(self): """Return the total number of tries, always a non-negative integer.""" return self._total
@property
[docs] def variance(self): """Return the variance of sampled tries, always greater than or equal to zero, if there is no variance it is equal to None.""" return self._variance
[docs]class BernoulliArm(SampleArm): """A Bernoulli arm (name, win, loss, total, variance) sampled from the objective function we are modeling/optimizing. A Bernoulli arm has payoff 1 for a success and 0 for a failure. See more details on Bernoulli distribution at http://en.wikipedia.org/wiki/Bernoulli_distribution See superclass :class:`~moe.bandit.data_containers.SampleArm` for more details. """
[docs] def validate(self): """Check this Bernoulli arm is a valid Bernoulli arm. Also check that this BernoulliArm passes basic validity checks: all values are finite. A Bernoulli arm has payoff 1 for a success and 0 for a failure. See more details on Bernoulli distribution at http://en.wikipedia.org/wiki/Bernoulli_distribution :raises ValueError: if any member data is non-finite or out of range or the arm is not a valid Bernoulli arm """ super(BernoulliArm, self).validate() if self.loss != 0.0: raise ValueError('loss = {0} is not zero! This is not a Bernoulli arm'.format(self.loss)) if self.win > self.total: raise ValueError('win = {0} > total = {1}! This is not a Bernoulli arm'.format(self.win, self.total))
[docs]class HistoricalData(object): """A data container for storing the historical data from an entire experiment in a layout convenient for this library. Users will likely find it most convenient to store experiment historical data of arms in tuples of (win, loss, total, variance); for example, these could be the columns of a database row, part of an ORM, etc. The SampleArm class (above) provides a convenient representation of this input format, but users are *not* required to use it. :ivar _arms_sampled: (*dict*) mapping of arm names to already-sampled arms """ __slots__ = ('_arms_sampled') def __init__(self, sample_arms=None, validate=True): """Create a HistoricalData object tracking the state of an experiment (already-sampled arms). :param sample_arms: the already-sampled arms: names, wins, losses, and totals :type sample_arms: a dictionary of (arm name, SampleArm) key-value pairs :param validate: whether to sanity-check the input sample_arms :type validate: boolean """ if sample_arms is None: sample_arms = {} if validate: self.validate_sample_arms(sample_arms) self._arms_sampled = sample_arms def __str__(self, pretty_print=True): """String representation of this HistoricalData object. pretty-print'ing produces output that is easily read by humans. Disabling it prints the member arrays to the screen in full precision; this is convenient for pasting into C++ or other debugging purposes. :param pretty_print: enable pretty-printing for formatted, human-readable output :type pretty_print: bool :return: string representation :rtype: string """ if pretty_print: return pprint.pformat(self.json_payload()) else: return repr(self.json_payload())
[docs] def json_payload(self): """Construct a json serializeable and MOE REST recognizeable dictionary of the historical data.""" json_arms_sampled = {} for name, arm in self._arms_sampled.iteritems(): json_arms_sampled[name] = arm.json_payload() return {'arms_sampled': json_arms_sampled}
@staticmethod
[docs] def validate_sample_arms(sample_arms): """Check that sample_arms passes basic validity checks: all values are finite. :param sample_arms: already-sampled arms: names, wins, losses, and totals :type sample_arms: a dictionary of (arm name, SampleArm) key-value pairs :return: True if inputs are valid :rtype: boolean """ if sample_arms: for arm in sample_arms.itervalues(): arm.validate()
[docs] def append_sample_arms(self, sample_arms, validate=True): """Append the contents of ``sample_arms`` to the data members of this class. This method first validates the arms and then updates the historical data. The result of combining two valid arms is always a valid arm. :param sample_arms: the already-sampled arms: wins, losses, and totals :type sample_arms: a dictionary of (arm name, SampleArm) key-value pairs :param validate: whether to sanity-check the input sample_arms :type validate: boolean """ if not sample_arms: return if validate: self.validate_sample_arms(sample_arms) self._update_historical_data(sample_arms)
def _update_historical_data(self, sample_arms): """Add arm sampled results from ``sample_arms`` into this object's data member. :param sample_arms: the already-sampled arms: wins, losses, and totals :type sample_arms: dictionary of (arm name, SampleArm) key-value pairs """ for name, arm in sample_arms.iteritems(): if name in self._arms_sampled: self._arms_sampled[name] += arm else: self._arms_sampled[name] = arm @property
[docs] def num_arms(self): """Return the number of sampled arms.""" return len(self._arms_sampled)
@property
[docs] def arms_sampled(self): """Return the arms_sampled, a dictionary of (arm name, SampleArm) key-value pairs.""" return self._arms_sampled