Esempio n. 1
0
 def test_sample_arm_add_arm_with_variance_invalid(self):
     """Test that adding arms with variance causes a ValueError. Neither of the arms can have non-None variance."""
     arm = SampleArm(win=2, loss=1, total=500, variance=0.1)
     T.assert_raises(ValueError, arm.__add__,
                     SampleArm(win=2, loss=1, total=500, variance=None))
     arm = SampleArm(win=2, loss=1, total=500, variance=None)
     T.assert_raises(ValueError, arm.__add__,
                     SampleArm(win=2, loss=1, total=500, variance=0.1))
Esempio n. 2
0
 def test_historical_data_append_arms(self):
     """Test that appending arms to HistoricalData updates historical info correctly."""
     historical_info = copy.deepcopy(self.three_arms_test_case)
     historical_info.append_sample_arms(
         self.three_arms_two_winners_test_case.arms_sampled)
     expected_historical_info = HistoricalData(
         sample_arms={
             "arm1": SampleArm(win=4, loss=2, total=6),
             "arm2": SampleArm(win=3, loss=2, total=5),
             "arm3": SampleArm(win=0, loss=0, total=0),
         })
     assert historical_info.json_payload(
     ) == expected_historical_info.json_payload()
Esempio n. 3
0
    def test_sample_arm_iadd(self):
        """Test SampleArm's __iadd__ overload operator.

        Verify that after x += y, x gets the new value x + y and still retains its old id.

        """
        arm1 = SampleArm(win=2, loss=1, total=3)
        arm2 = SampleArm(win=3, loss=2, total=5)
        arm3 = arm1 + arm2
        arm1_old_id = id(arm1)
        arm1 += arm2
        arm1_new_id = id(arm1)
        assert arm1_old_id == arm1_new_id
        assert arm1.json_payload() == arm3.json_payload()
Esempio n. 4
0
    def test_sample_arm_iadd(self):
        """Test SampleArm's __iadd__ overload operator.

        Verify that after x += y, x gets the new value x + y and still retains its old id.

        """
        arm1 = SampleArm(win=2, loss=1, total=3)
        arm2 = SampleArm(win=3, loss=2, total=5)
        arm3 = arm1 + arm2
        arm1_old_id = id(arm1)
        arm1 += arm2
        arm1_new_id = id(arm1)
        assert arm1_old_id == arm1_new_id
        assert arm1.json_payload() == arm3.json_payload()
Esempio n. 5
0
class BanditTestCase(T.TestCase):

    """Base test case for the bandit library.

    This sets up arms for test cases and includes an integration test case for
    verifying that default values do not throw an error.

    """

    bandit_class = None  # Define in a subclass

    """Set up arms for test cases."""
    one_arm_test_case = HistoricalData(sample_arms={"arm1": BernoulliArm(win=0, loss=0, total=0)})
    two_unsampled_arms_test_case = HistoricalData(sample_arms={"arm1": BernoulliArm(win=0, loss=0, total=0), "arm2": BernoulliArm(win=0, loss=0, total=0)})
    two_arms_test_case = HistoricalData(sample_arms={"arm1": BernoulliArm(win=1, loss=0, total=1), "arm2": BernoulliArm(win=0, loss=0, total=0)})
    three_arms_test_case = HistoricalData(sample_arms={"arm1": SampleArm(win=2, loss=1, total=3), "arm2": SampleArm(win=1, loss=1, total=2), "arm3": SampleArm(win=0, loss=0, total=0)})
    three_arms_float_payoffs_test_case = HistoricalData(sample_arms={"arm1": SampleArm(win=2.2, loss=1.1, total=3), "arm2": SampleArm(win=2.1, loss=1.1, total=3), "arm3": SampleArm(win=0, loss=0, total=0)})
    three_arms_two_winners_test_case = HistoricalData(sample_arms={"arm1": SampleArm(win=2, loss=1, total=3), "arm2": SampleArm(win=2, loss=1, total=3), "arm3": SampleArm(win=0, loss=0, total=0)})
    three_arms_two_winners_no_unsampled_arm_test_case = HistoricalData(sample_arms={"arm1": SampleArm(win=2, loss=1, total=3), "arm2": SampleArm(win=2, loss=1, total=3), "arm3": SampleArm(win=0, loss=1, total=1)})
    three_arms_with_variance_no_unsampled_arm_test_case = HistoricalData(sample_arms={"arm1": SampleArm(win=2, loss=1, total=500, variance=0.1), "arm2": SampleArm(win=2, loss=1, total=500, variance=0.01), "arm3": SampleArm(win=2, loss=1, total=500, variance=0.001)})

    bernoulli_historical_infos_to_test = [
                                one_arm_test_case,
                                two_unsampled_arms_test_case,
                                two_arms_test_case,
                                ]

    historical_infos_to_test = [
                            three_arms_test_case,
                            three_arms_float_payoffs_test_case,
                            three_arms_two_winners_test_case,
                            three_arms_two_winners_no_unsampled_arm_test_case,
                            three_arms_with_variance_no_unsampled_arm_test_case,
                            ]
    historical_infos_to_test.extend(bernoulli_historical_infos_to_test)

    def _test_init_default(self):
        """Verify that default values do not throw and error. This is purely an integration test."""
        for historical_info in self.historical_infos_to_test:
            bandit = self.bandit_class(historical_info=historical_info)
            bandit.choose_arm(bandit.allocate_arms())

    def _test_one_arm(self, bandit):
        """Check that the one-arm case always returns the given arm as the winning arm and the allocation is 1.0."""
        bandit = self.bandit_class(self.one_arm_test_case)
        arms_to_allocations = bandit.allocate_arms()
        T.assert_dicts_equal(arms_to_allocations, {"arm1": 1.0})
        T.assert_equal(bandit.choose_arm(arms_to_allocations), "arm1")
def generate_initial_traffic():
    """Generate initial traffic allocations.

    ``active_arms``: list of coordinate-tuples corresponding to arms/cohorts currently being sampled

    ``sample_arms``: all arms from prev and current cohorts, keyed by coordinate-tuples.

    Arm refers specifically to a :class:`moe.bandit.data_containers.SampleArm`

    :return: (active_arms, sample_arms) tuple as described above
    :rtype: tuple

    """
    ctr_at_status_quo = true_click_through_rate(STATUS_QUO_PARAMETER)

    # We draw the clicks from a binomial distribution
    clicks = numpy.random.binomial(TRAFFIC_PER_DAY, ctr_at_status_quo)
    status_quo_sample_arm = SampleArm(
            win=clicks,
            total=TRAFFIC_PER_DAY,
            )

    # We start with the only active arm being the status quo
    active_arms = [tuple(STATUS_QUO_PARAMETER)]
    sample_arms = {
            tuple(STATUS_QUO_PARAMETER): status_quo_sample_arm,
            }
    return active_arms, sample_arms
def run_time_consuming_experiment(allocations, sample_arms, verbose=False):
    """Run the time consuming or expensive experiment.

    .. Note::

        Obtaining the value of the objective function is assmumed to be either time consuming or expensive,
        where every evaluation is precious and we want to find the optimal set of parameters with as few calls as possible.

    This experiment runs a simulation of user traffic over various parameters and users.
    It simulates an A/B testing experiment framework.

    For each arm/cohort, we compute the true CTR. From bandits, we know the allocation of
    traffic for that arm. So the simulation involves running (TRAFFIC_PER_DAY * allocation)
    Bernoulli trials each with probability = true CTR. Then since we cannot know the true CTR,
    we only work with the observed CTR and variance (as computed in
    :func:~`moe_examples.blog_post_example_ab_testing.objective_function`).

    :param allocations: traffic allocations for the ``active_arms``, one for each tuple in ``active_arms``.
      These are the cohorts for the experiment round--the parameters being tested and on what portion of traffic.
    :type allocations: dict
    :param sample_arms: all arms from prev and current cohorts, keyed by coordinate-tuples
      Arm refers specifically to a :class:`moe.bandit.data_containers.SampleArm`
    :type sample_arms: dict
    :param verbose: whether to print status messages to stdout
    :type verbose: bool

    """
    arm_updates = {}
    for arm_point, arm_allocation in allocations.items():
        # Find the true, underlying CTR at the point
        ctr_at_point = true_click_through_rate(arm_point)
        # Calculate how much user traffic is allocated to this point
        traffic_for_point = int(TRAFFIC_PER_DAY * arm_allocation)
        # Simulate the number of clicks this point will garner
        clicks = numpy.random.binomial(
                traffic_for_point,
                ctr_at_point,
                )
        # Create a SampleArm with the assoicated simulated data
        sample_arm_for_day = SampleArm(
                win=clicks,
                total=traffic_for_point,
                )
        # Store the information about the simulated experiment
        arm_updates[arm_point] = sample_arm_for_day
        sample_arms[arm_point] = sample_arms[arm_point] + sample_arm_for_day

    if verbose:
        print("Updated the samples with:")
        for arm_name, sample_arm in arm_updates.items():
            print("\t{0}: {1}".format(arm_name, sample_arm))

    return sample_arms, arm_updates
Esempio n. 8
0
    def test_sample_arm_add_arm_with_variance_invalid(self):
        """Test that adding arms with variance causes a ValueError. Neither of the arms can have non-None variance."""
        with pytest.raises(ValueError):
            arm = SampleArm(win=2, loss=1, total=500, variance=0.1)
            arm.__add__(SampleArm(win=2, loss=1, total=500, variance=None))

        with pytest.raises(ValueError):
            arm = SampleArm(win=2, loss=1, total=500, variance=None)
            arm.__add__(SampleArm(win=2, loss=1, total=500, variance=0.1))
Esempio n. 9
0
    def test_sample_arm_add(self):
        """Test SampleArm's __add__ overload operator."""
        arm1 = SampleArm(win=2, loss=1, total=3)
        arm2 = SampleArm(win=3, loss=2, total=5)
        arm3 = arm1 + arm2
        T.assert_equals(arm3.json_payload(), SampleArm(win=5, loss=3, total=8).json_payload())
        # Verify that the + operator does not modify arm1 and arm2
        T.assert_equals(arm1.json_payload(), SampleArm(win=2, loss=1, total=3).json_payload())
        T.assert_equals(arm2.json_payload(), SampleArm(win=3, loss=2, total=5).json_payload())

        arm1 += arm2
        arm2 += arm1
        T.assert_equals(arm1.json_payload(), SampleArm(win=5, loss=3, total=8).json_payload())
        T.assert_equals(arm2.json_payload(), SampleArm(win=8, loss=5, total=13).json_payload())
        # Verify that modifying arm1 and arm2 does not change arm3
        T.assert_equals(arm3.json_payload(), SampleArm(win=5, loss=3, total=8).json_payload())
Esempio n. 10
0
    def validator(self, node, cstruct):
        """Raise an exception if the node value (cstruct) is not a valid dictionary of (arm name, SingleArm) key-value pairs. Default value for loss is 0. Default value for variance of an arm is None.

        :param node: the node being validated (usually self)
        :type node: colander.SchemaNode subclass instance
        :param cstruct: the value being validated
        :type cstruct: dictionary of (arm name, SingleArm) key-value pairs

        """
        for arm_name, sample_arm in cstruct.items():
            if 'loss' not in sample_arm:
                sample_arm['loss'] = 0
            if 'variance' not in sample_arm:
                sample_arm['variance'] = None
            if not (set(sample_arm.keys()) == set(
                [s.lstrip('_') for s in SampleArm.__slots__])):
                raise colander.Invalid(
                    node,
                    msg='Value = {:s} must be a valid SampleArm.'.format(
                        sample_arm))
            SampleArm(sample_arm['win'], sample_arm['loss'],
                      sample_arm['total'], sample_arm['variance'])
Esempio n. 11
0
    def test_sample_arm_add(self):
        """Test SampleArm's __add__ overload operator."""
        arm1 = SampleArm(win=2, loss=1, total=3)
        arm2 = SampleArm(win=3, loss=2, total=5)
        arm3 = arm1 + arm2
        assert arm3.json_payload() == SampleArm(win=5, loss=3,
                                                total=8).json_payload()

        # Verify that the + operator does not modify arm1 and arm2
        assert arm1.json_payload() == SampleArm(win=2, loss=1,
                                                total=3).json_payload()
        assert arm2.json_payload() == SampleArm(win=3, loss=2,
                                                total=5).json_payload()

        arm1 += arm2
        arm2 += arm1
        assert arm1.json_payload() == SampleArm(win=5, loss=3,
                                                total=8).json_payload()
        assert arm2.json_payload() == SampleArm(win=8, loss=5,
                                                total=13).json_payload()
        # Verify that modifying arm1 and arm2 does not change arm3
        assert arm3.json_payload() == SampleArm(win=5, loss=3,
                                                total=8).json_payload()
def generate_new_arms(active_arms, sample_arms, verbose=False):
    """Find optimal new parameters to sample to get ``active_arms`` up to ``NUMBER_OF_ACTIVE_COHORTS``.

    This is done in the following way:
        1) Find the initial allocations of all active parameters
        2) MOE suggests new, optimal arms/parameters to sample, given all previous samples, using Bayesian Global Optimization
        3) The new parameters are allocated traffic according to a Multi-Armed Bandit policy for all ``active_arms``
        4) If the objective function for a given parameter is too low (with high statistical significance), it is turned off
        5) Steps 2-4 are repeated until there are ``NUMBER_OF_ACTIVE_COHORTS`` parameters being sampled with non-zero traffic

    :param active_arms: list of coordinate-tuples corresponding to arms/cohorts currently being sampled
    :type active_arms: list of tuple
    :param sample_arms: all arms from prev and current cohorts, keyed by coordinate-tuples
      Arm refers specifically to a :class:`moe.bandit.data_containers.SampleArm`
    :type sample_arms: dict
    :param verbose: whether to print status messages to stdout
    :type verbose: bool
    :return: (allocations, active_arms, sample_arms) describing the next round of experiments to run

      ``allocations``: dict of traffic allocations, indexed by the ``active_arms`` return value

      ``active_arms``: new active arms to run in the next round of our experiment; same format as the input

      ``sample_arms``: the sample arms input updated with the newly generated active arms; same format as the input
    :rtype: tuple

    """
    # 1) Find initial allocations of all active parameters
    experiment = moe_experiment_from_sample_arms(sample_arms)
    allocations = get_allocations(active_arms, sample_arms)

    # Loop while we have too few arms
    while len(active_arms) < NUMBER_OF_ACTIVE_COHORTS:
        # 2) MOE suggests new, optimal arms/parameters to sample, given all previous samples, using Bayesian Global Optimization
        new_points_to_sample = find_new_points_to_sample(
                experiment,
                num_points=NUMBER_OF_ACTIVE_COHORTS - len(active_arms),
                verbose=verbose,
                )

        # Add the new points to the list of active_arms
        for new_point_to_sample in new_points_to_sample:
            sample_arms[tuple(new_point_to_sample)] = SampleArm()
            active_arms.append(tuple(new_point_to_sample))

        # 3) The new parameters are allocated traffic according to a Multi-Armed Bandit policy for all ``active_arms``
        allocations = get_allocations(
                active_arms,
                sample_arms,
                verbose=verbose,
                )

        # 4) If the objective function for a given parameter is too low (with high statistical significance), it is turned off
        # Remove arms that have no traffic from active_arms
        active_arms = prune_arms(
                active_arms,
                sample_arms,
                verbose=verbose,
                )

    return allocations, active_arms, sample_arms