<NME> test_asym_logit.py
<BEF> """
Tests for the asym_logit.py file. These tests do not include tests of
the functions that perform the mathematical calculations necessary to estimate
the multinomial Asymmetric Logit model.
"""
import warnings
import unittest
from collections import OrderedDict
from copy import deepcopy

import numpy as np
import numpy.testing as npt
import pandas as pd
from scipy.sparse import csr_matrix
from scipy.sparse import diags

import pylogit.asym_logit as asym

try:
    # in Python 3 range returns an iterator instead of list
    # to maintain backwards compatibility use "old" version of range
    from past.builtins import xrange, range
except ImportError:
    pass

class GenericTestCase(unittest.TestCase):
    """
    Defines the common setUp method used for the different type of tests.
    """

    def setUp(self):
        # The set up being used is one where there are two choice situations,
        # The first having three alternatives, and the second having only two
        # alternatives. There is one generic variable. Two alternative
        # specific constants and all three shape parameters are used.

        # Create the betas to be used during the tests
        self.fake_betas = np.array([-0.6])

        # Create the fake outside intercepts to be used during the tests
        self.fake_intercepts = np.array([1, 0.5])

        # Create names for the intercept parameters
        self.fake_intercept_names = ["ASC 1", "ASC 2"]

        # Record the position of the intercept that is not being estimated
        self.fake_intercept_ref_pos = 2

        # Create the shape parameters to be used during the tests. Note that
        # these are the reparameterized shape parameters, thus they will be
        # exponentiated in the fit_mle process and various calculations.
        self.fake_shapes = np.array([-1, 1])

        # Create names for the intercept parameters
        self.fake_shape_names = ["Shape 1", "Shape 2"]

        # Record the position of the shape parameter that is being constrained
        self.fake_shape_ref_pos = 2

        # Calculate the 'natural' shape parameters
        self.natural_shapes = asym._convert_eta_to_c(self.fake_shapes,
                                                     self.fake_shape_ref_pos)

        # Create an array of all model parameters
        self.fake_all_params = np.concatenate((self.fake_shapes,
                                               self.fake_intercepts,
                                               self.fake_betas))

        # The mapping between rows and alternatives is given below.
        self.fake_rows_to_alts = csr_matrix(np.array([[1, 0, 0],
                                                      [0, 1, 0],
                                                      [0, 0, 1],
                                                      [1, 0, 0],
                                                      [0, 0, 1]]))

        # Create the fake design matrix with columns denoting X
        # The intercepts are not included because they are kept outside the
        # index in the scobit model.
        self.fake_design = np.array([[1],
                                     [2],
                                     [3],
                                     [1.5],
                                     [3.5]])

        # Create the index array for this set of choice situations
        self.fake_index = self.fake_design.dot(self.fake_betas)

        # Create the needed dataframe for the Asymmetric Logit constructor
        self.fake_df = pd.DataFrame({"obs_id": [1, 1, 1, 2, 2],
                                     "alt_id": [1, 2, 3, 1, 3],
                                     "choice": [0, 1, 0, 0, 1],
                                     "x": self.fake_design[:, 0],
                                     "intercept": [1 for i in range(5)]})

        # Record the various column names
        self.alt_id_col = "alt_id"
        self.obs_id_col = "obs_id"
        self.choice_col = "choice"

        # Create the index specification  and name dictionaryfor the model
        self.fake_specification = OrderedDict()
        self.fake_names = OrderedDict()
        self.fake_specification["x"] = [[1, 2, 3]]
        self.fake_names["x"] = ["x (generic coefficient)"]

        # Bundle args and kwargs used to construct the Asymmetric Logit model.
        self.constructor_args = [self.fake_df,
                                 self.alt_id_col,
                                 self.obs_id_col,
                                 self.choice_col,
                                 self.fake_specification]

        # Create a variable for the kwargs being passed to the constructor
        self.constructor_kwargs = {"intercept_ref_pos":
                                   self.fake_intercept_ref_pos,
                                   "shape_ref_pos": self.fake_shape_ref_pos,
                                   "names": self.fake_names,
                                   "intercept_names":
                                   self.fake_intercept_names,
                                   "shape_names": self.fake_shape_names}

        # Initialize a basic Asymmetric Logit model whose coefficients will be
        # estimated.
        self.model_obj = asym.MNAL(*self.constructor_args,
                                   **self.constructor_kwargs)

        return None


# Note that inheritance is used to share the setUp method.
class ChoiceObjectTests(GenericTestCase):
    """
    Defines the tests for the Asymmetric Logit model's `__init__` function and
    its class methods.
    """

    def test_shape_ignore_msg_in_constructor(self):
        """
        Ensures that a UserWarning is raised when the 'shape_ref_pos' keyword
        argument is not an integer. This warns people against expecting all of
        the shape parameters of the Asymmetric Logit model to be identified.
        It also alerts users that they are using a Asymmetric Logit model when
        they might have been expecting to instantiate a different choice model.
        """
        # Create a variable for the kwargs being passed to the constructor
        kwargs = deepcopy(self.constructor_kwargs)

        for pos, item in enumerate(['foo', None]):
            kwargs["shape_ref_pos"] = item

            # Check that the warning has been created.
            # Test to ensure that the ValueError is raised when using a
            # shape_ref_pos kwarg with an incorrect number of parameters
            self.assertRaises(ValueError, asym.MNAL,
                              *self.constructor_args, **kwargs)

        return None

    def test_ridge_warning_in_fit_mle(self):
        """
        Ensure that a UserWarning is raised when one passes the ridge keyword
        argument to the `fit_mle` method of an Asymmetric Logit model object.
        """
        # Create a variable for the fit_mle function's kwargs.
        # The print_res = False arguments are to make sure strings aren't
        # printed to the console unnecessarily.
        kwargs = {"ridge": 0.5,
                  "print_res": False,
                  "method": "bfgs"}

        # Test to make sure that the ridge warning message is printed when
        # using the ridge keyword argument
        with warnings.catch_warnings(record=True) as w:
            # Use this filter to always trigger the  UserWarnings
            warnings.simplefilter('always', UserWarning)

            self.model_obj.fit_mle(self.fake_all_params, **kwargs)
            self.assertGreaterEqual(len(w), 1)
            self.assertIsInstance(w[0].category, type(UserWarning))
            self.assertIn(asym._ridge_warning_msg, str(w[0].message))

        return None

    def test_init_shapes_length_error_in_fit_mle(self):
        """
        Ensures that ValueError is raised if init_shapes has wrong length.
        """
        # Create a variable for the arguments to the fit_mle function.
        # Note `None` is the argument passed when using the init_shapes,
        # init_intercepts and init_coefs keyword arguments.
        fit_args = [None]

        # Create base set of kwargs for fit_mle function
        kwargs = {"init_intercepts": self.fake_intercepts,
                  "init_coefs": self.fake_betas,
                  "print_res": False}

        for i in [1, -1]:
            # This will ensure we have too many or too few shape parameters.
            num_shapes = self.fake_rows_to_alts.shape[1] - 1 + i
            kwargs["init_shapes"] = np.arange(num_shapes)

            # Test to ensure that the ValueError is raised when using an
            # init_shapes kwarg with an incorrect number of parameters
            self.assertRaises(ValueError, self.model_obj.fit_mle,
                              *fit_args, **kwargs)

    def test_init_intercepts_length_error_in_fit_mle(self):
        """
        Ensures that ValueError is raised if init_intercepts has wrong length.
        """
        # Create a variable for the arguments to the fit_mle function.
        # Note `None` is the argument passed when using the init_shapes,
        # init_intercepts and init_coefs keyword arguments.
        fit_args = [None]

        # Create base set of kwargs for fit_mle function
        kwargs = {"init_shapes": self.fake_shapes,
                  "init_coefs": self.fake_betas,
                  "print_res": False}

        for i in [1, -1]:
            # This will ensure we have too many or too few intercepts
            num_intercepts = self.fake_rows_to_alts.shape[1] - 1 + i
            kwargs["init_intercepts"] = np.arange(num_intercepts)

            # Test to ensure that the ValueError when using an init_intercepts
            # kwarg with an incorrect number of parameters
            self.assertRaises(ValueError, self.model_obj.fit_mle,
                              *fit_args, **kwargs)

        return None

    def test_init_coefs_length_error_in_fit_mle(self):
        """
        Ensures that ValueError is raised if init_coefs has wrong length.
        """
        # Create a variable for the arguments to the fit_mle function.
        # Note `None` is the argument passed when using the init_shapes,
        # init_intercepts and init_coefs keyword arguments.
        fit_args = [None]

        # Create base set of kwargs for fit_mle function
        kwargs = {"init_shapes": self.fake_shapes,
                  "init_intercepts": self.fake_intercepts,
                  "print_res": False}

        # Note there is only one beta, so we can't go lower than zero betas.
        for i in [1, -1]:
            # This will ensure we have too many or too few intercepts
            num_coefs = self.fake_betas.shape[0] + i
            kwargs["init_coefs"] = np.arange(num_coefs)

            # Test to ensure that the ValueError when using an init_intercepts
            # kwarg with an incorrect number of parameters
            self.assertRaises(ValueError, self.model_obj.fit_mle,
                              *fit_args, **kwargs)

        return None

    def test_insufficient_initial_values_in_fit_mle(self):
        """
        Ensure that value errors are raised if neither init_vals OR
        (init_shapes and init_coefs) are passed.
        """
        # Create a variable for the arguments to the fit_mle function.
        # Note `None` is the argument passed when using the init_shapes,
        # init_intercepts and init_coefs keyword arguments.
        fit_args = [None]

        # Create base set of incorrect kwargs for fit_mle function
        kwargs = {"init_shapes": None,
                  "init_intercepts": self.fake_intercepts,
                  "init_coefs": None,
                  "print_res": False}

        kwargs_2 = {"init_shapes": self.fake_shapes,
                    "init_intercepts": self.fake_intercepts,
                    "init_coefs": None,
                    "print_res": False}

        kwargs_3 = {"init_shapes": None,
                    "init_intercepts": self.fake_intercepts,
                    "init_coefs": self.fake_betas,
                    "print_res": False}

        for bad_kwargs in [kwargs, kwargs_2, kwargs_3]:
            # Test to ensure that the ValueError when not passing
            # kwarg with an incorrect number of parameters
            self.assertRaises(ValueError, self.model_obj.fit_mle,
                              *fit_args, **bad_kwargs)

        return None

    def test_keyword_argument_constructor_in_fit_mle(self):
        """
        Ensures that the init_vals object can be successfully created from the
        various init_shapes, init_intercepts, and init_coefs arguments.
        """
        # Create a variable for the arguments to the fit_mle function.
        # Note `None` is the argument passed when using the init_shapes,
        # init_intercepts and init_coefs keyword arguments.
        fit_args = [None]

        # Create base set of incorrect kwargs for fit_mle function (note the
        # ridge is the thing that is incorrect)
        kwargs_1 = {"init_shapes": self.fake_shapes,
                    "init_intercepts": self.fake_intercepts,
                    "init_coefs": self.fake_betas,
                    "ridge": "foo",
                    "print_res": False}

        kwargs_2 = {"init_shapes": self.fake_shapes,
                    "init_coefs": self.fake_betas,
                    "ridge": "foo",
                    "print_res": False}

        # Test to ensure that the raised ValueError is printed when using
        # either of these two kwargs. This ensures that we were able to
        # create the init_vals object since the ridge error check is after
        # the creation of this argurment.
        for kwargs in [kwargs_1, kwargs_2]:
            self.assertRaisesRegexp(TypeError,
                                    "ridge",
                                    self.model_obj.fit_mle,
                                    *fit_args,
                                    **kwargs)

        return None

    def test_init_vals_length_error_in_fit_mle(self):
        """
        Ensures that ValueError is raised if init_vals has wrong length.
        """
        # Note there is only one beta, so we can't go lower than zero betas.
        original_intercept_ref_position = self.fake_intercept_ref_pos
        for intercept_ref_position in [None, original_intercept_ref_position]:
            self.model_obj.intercept_ref_position = intercept_ref_position
            for i in [1, -1]:
                # This will ensure we have too many or too few intercepts
                num_coefs = self.fake_betas.shape[0] + i

                # Test to ensure that the ValueError when using an
                # init_intercepts kwarg with an incorrect number of parameters
                self.assertRaisesRegexp(ValueError,
                                        "dimension",
                                        self.model_obj.fit_mle,
                                        np.arange(num_coefs),
                                        print_res=False)

        return None

    def test_just_point_kwarg(self):
        # Alias the function being tested
        func = self.model_obj.fit_mle
        # Get the necessary kwargs
        kwargs = {"just_point": True}
        # Get the function results
        func_result = func(self.fake_all_params, **kwargs)
        # Perform the desired tests to make sure we get back a dictionary with
        # an "x" key in it and a value that is a ndarray.
        self.assertIsInstance(func_result, dict)
        self.assertIn("x", func_result)
        self.assertIsInstance(func_result["x"], np.ndarray)
        return None


# As before, inheritance is used to share the setUp method.
class HelperFuncTests(GenericTestCase):

        return None

    # def test_asym_utility_transform(self):
    #     """
    #     Ensures that `_asym_utility_transform()` returns correct results
    #     """
    #     # Create a set of systematic utilities that will test the function for
    #     # correct calculations, for proper dealing with overflow, and for
    #     # proper dealing with underflow.

    #     # The first and third elements tests general calculation.
    #     # The second element of index_array should lead to the transformation
    #     # equaling the 'natural' shape parameter for alternative 2.
    #     # The fourth element should test what happens with underflow and should
    #     # lead to max_comp_value.
    #     # The fifth element should test what happens with overflow and should
    #     # lead to -1.0 * max_comp_value
    #     index_array = np.array([1, 0, -1, 1e400, -1e400])

    #     # We can use a the following two arrays of the shape parameters to test
    #     # the overflow and underflow capabilities with respect to the shape
    #     # parameters.
    #     test_shapes_2 = np.array([-800, 0])
    #     test_shapes_3 = np.array([800, 0])

    #     # Figure out the value of the 'natural' shape parameters
    #     natural_shapes_2 = asym._convert_eta_to_c(test_shapes_2,
    #                                               self.fake_shape_ref_pos)
    #     natural_shapes_3 = asym._convert_eta_to_c(test_shapes_3,
    #                                               self.fake_shape_ref_pos)

    #     # Crerate the array of expected results when using shape parameters
    #     # of 'normal' magnitudes.
    #     intercept_1 = self.fake_intercepts[0]
    #     intercept_2 = self.fake_intercepts[1]
    #     intercept_3 = 0

    #     result_1 = (intercept_1 +
    #                 np.log(self.natural_shapes[0]) * (1 - index_array[0]))

    #     result_2 = intercept_2 + np.log(self.natural_shapes[1])

    #     result_3 = (intercept_3 +
    #                 np.log(self.natural_shapes[2]) -
    #                 np.log(1 - self.natural_shapes[2]) * index_array[2])

    #     expected_results = np.array([result_1,
    #                                  result_2,
    #                                  result_3,
    #                                  asym.max_comp_value + intercept_1,
    #                                  - asym.max_comp_value])[:, None]

    #     # Crerate the array of expected results when using shape parameters
    #     # of 'abnormally' small magnitudes.
    #     result_2_2 = intercept_2 + np.log(natural_shapes_2[1])
    #     result_3_2 = (intercept_3 +
    #                   np.log(natural_shapes_2[2]) -
    #                   np.log(1 - natural_shapes_2[2]) * index_array[2])

    #     expected_results_2 = np.array([-asym.max_comp_value,
    #                                    result_2_2,
    #                                    result_3_2,
    #                                    -asym.max_comp_value + intercept_1,
    #                                    -asym.max_comp_value])[:, None]

    #     # Crerate the array of expected results when using shape parameters
    #     # of 'abnormally' large magnitudes.
    #     expected_results_3 = np.array([0 + intercept_1,
    #                                    -asym.max_comp_value + intercept_2,
    #                                    -asym.max_comp_value + intercept_3,
    #                                    0 + intercept_1,
    #                                    -asym.max_comp_value])[:, None]

    #     #####
    #     # Perform various rounds of checking
    #     #####
    #     # Use the utility transformation function, round_1
    #     alt_id_vals = self.fake_df[self.alt_id_col].values
    #     args = [index_array,
    #             alt_id_vals,
    #             self.fake_rows_to_alts,
    #             self.fake_shapes,
    #             self.fake_intercepts]
    #     kwargs = {"intercept_ref_pos": self.fake_intercept_ref_pos,
    #               "shape_ref_position": self.fake_shape_ref_pos}
    #     func_results = asym._asym_utility_transform(*args, **kwargs)

    #     # Use the utility transformation function, round_2
    #     args[3] = test_shapes_2
    #     func_results_2 = asym._asym_utility_transform(*args, **kwargs)

    #     # Use the utility transformation function, round_3
    #     args[3] = test_shapes_3
    #     func_results_3 = asym._asym_utility_transform(*args, **kwargs)

    #     # Check the correctness of the results
    #     all_results = [(func_results, expected_results),
    #                    (func_results_2, expected_results_2),
    #                    (func_results_3, expected_results_3)]

    #     for pos, (test_results, correct_results) in enumerate(all_results):
    #         print("Result pair {}".format(pos))
    #         print("Function results: {}".format(test_results))
    #         print("Correct results: {}".format(correct_results))
    #         self.assertIsInstance(test_results, np.ndarray)
    #         self.assertEqual(len(test_results.shape), 2)
    #         self.assertEqual(test_results.shape[1], correct_results.shape[1])
    #         self.assertEqual(test_results.shape[0], correct_results.shape[0])
    #         npt.assert_allclose(test_results, correct_results)

    #     return None

    # def test_asym_transform_deriv_v(self):
    #     """
    #     Tests basic behavior of the asym_transform_deriv_v.
    #     """
    #     # Note the index has a value that is small and a value that is large to
    #     # test whether or not the function correctly deals with underflow and
    #     # overflow when calculating the derivative.
    #     test_index = np.array([-2, 0, 2, -3000, 800])
    #     # Note we use a compressed sparse-row matrix so that we can easily
    #     # convert the output matrix to a numpy array using the '.A' attribute.
    #     num_rows = test_index.shape[0]
    #     test_output = diags(np.ones(num_rows),
    #                         0, format='csr')

    #     # Bundle the arguments needed for _asym_transform_deriv_v()
    #     args = [test_index,
    #             self.fake_df[self.alt_id_col].values,
    #             self.fake_rows_to_alts,
    #             self.fake_shapes]

    #     # Get the derivative using the function defined in asym_logit.py.
    #     derivative = asym._asym_transform_deriv_v(*args,
    #                                                   output_array=test_output)

    #     # Initialize an array of correct results
    #     # Note the second element is by design where each of the terms in the
    #     # derivative should evaluate to 1 and we get 1 / 1 = 1.
    #     correct_derivatives = np.array([np.nan,
    #                                     (1 + np.exp(self.fake_shapes[1])) / 2,
    #                                     np.nan,
    #                                     np.exp(self.fake_shapes[0]),
    #                                     1.0])

    #     # Calculate 'by hand' what the correct results should be
    #     for i in [0, 2]:
    #         shape = np.exp(self.fake_shapes[i])
    #         index_val = test_index[i]
    #         correct_derivatives[i] = ((1 + np.exp(-index_val))**-1 +
    #                                    shape / (1 + np.exp(shape * index_val)))

    #     self.assertIsInstance(derivative, type(test_output))
    #     self.assertEqual(len(derivative.shape), 2)
    #     self.assertEqual(derivative.shape, (num_rows, num_rows))
    #     npt.assert_allclose(correct_derivatives,
    #                         np.diag(derivative.A))

    #     return None

    def test_asym_transform_deriv_alpha(self):
        """

        expected_results_3 = np.array([0 + intercept_1,
                                       result_2_3,
                                       result_3_3,
                                       0 + intercept_1,
                                       -asym.max_comp_value])[:, None]

        #####
        # Perform various rounds of checking
        #####
        # Use the utility transformation function, round_1
        alt_id_vals = self.fake_df[self.alt_id_col].values
        args = [index_array,
                alt_id_vals,
                self.fake_rows_to_alts,
                self.fake_shapes,
                self.fake_intercepts]
        kwargs = {"intercept_ref_pos": self.fake_intercept_ref_pos,
                  "shape_ref_position": self.fake_shape_ref_pos}
        func_results = asym._asym_utility_transform(*args, **kwargs)

        return None

    # def test_asym_transform_deriv_shape(self):
    #     """
    #     Ensures that the asym_transform_deriv_shape() function provides
    #     correct results, and handles all forms of overflow and underflow
    #     correctly.
    #     """
    #     # Note the index has a value that is small and a value that is large to
    #     # test whether or not the function correctly uses L'Hopital's rule to
    #     # deal with underflow and overflow from the index when calculating the
    #     # derivative. When the index is large, the derivative should be the
    #     # negative of the inverse of the 'natural' shape parameter. When the
    #     # index is small the derivative should be max_comp_value (theoretically
    #     # inf). When the shape is large, the derivative should be the negative
    #     # of the natural log of 1 + exp(-index).
    #     test_index = np.array([-10, 0, 2, -10000, 1e8])

    #     # Note we use a compressed sparse-row matrix so that we can easily
    #     # convert the output matrix to a numpy array using the '.A' attribute.
    #     num_rows = test_index.shape[0]
    #     test_output = diags(np.ones(num_rows),
    #                         0, format='csr')

    #     # Create new test shapes to use. We make the first shape parameter big
    #     # to test overflow handling from this situation and L'Hopital's rule.
    #     new_shapes = deepcopy(self.fake_shapes)
    #     new_shapes[0] = np.log(80)
    #     natural_shapes = np.exp(new_shapes)

    #     # Bundle the arguments needed for _scobit_transform_deriv_v()
    #     args = [test_index,
    #             self.fake_df[self.alt_id_col].values,
    #             self.fake_rows_to_alts,
    #             new_shapes]
    #     kwargs = {"output_array": test_output}

    #     # Get the derivative using the function defined in asym_logit.py.
    #     derivative = asym._asym_transform_deriv_shape(*args, **kwargs)

    #     # Initialize an array of correct results
    #     # Note the second element is by design so the derivative equals 2ln(2)
    #     # Also, we precompute the results and multiply by natural shapes to
    #     # account for the jacobian when computing the gradient with respect to
    #     # reparameterized shape parameters.
    #     result_1 = (test_index[0] /
    #                 (1 + np.exp(natural_shapes[0] * test_index[0])))
    #     result_3 = (test_index[2] /
    #                 (1 + np.exp(natural_shapes[2] * test_index[2])))
    #     correct_derivatives = np.array([result_1 * natural_shapes[0],
    #                                     0,
    #                                     result_3 * natural_shapes[2],
    #                                     test_index[3] * natural_shapes[0],
    #                                     0])

    #     self.assertIsInstance(derivative, type(test_output))
    #     self.assertEqual(len(derivative.shape), 2)
    #     self.assertEqual(derivative.shape, (num_rows, num_rows))
    #     npt.assert_allclose(correct_derivatives,
    #                         np.diag(derivative.A))

    #     return None
            self.assertEqual(len(test_results.shape), 2)
            self.assertEqual(test_results.shape[1], 2)
            self.assertEqual(test_results.shape[0], correct_results.shape[0])
            for col in [0, 1]:
                npt.assert_allclose(test_results[:, col][:, None],
                                    correct_results)

        return None

    def test_asym_transform_deriv_v(self):
        """
        Tests basic behavior of the asym_transform_deriv_v. Essentially the
        only things that can go wrong is underflow or overflow from the shape
        parameters going to zero or 1.0.
        """
        # Declare  set of index values to be tested
        test_index = np.array([1, 0, -1, -2, 2])
        # Figure out how many alternatives are there
        num_alts = self.fake_rows_to_alts.shape[1]
        # Test what happens with large shape parameters (that result in
        # 'natural' shape parameters near 1.0)
        large_shapes = np.array([800, 0])
        large_results = np.array([0,
                                  -np.log(asym.min_comp_value),
                                  np.log(num_alts - 1),
                                  asym.max_comp_value,
                                  -np.log(asym.min_comp_value)])
        # Test what happens with large shape parameters (that result in
        # 'natural' shape parameters near 1.0)
        small_shapes = np.array([-800, 0])
        natural_small_shapes = asym._convert_eta_to_c(small_shapes,
                                                      self.fake_shape_ref_pos)
        small_results = np.array([-np.log(natural_small_shapes[0]),
                                  -np.log(natural_small_shapes[1]),
                                  (np.log(num_alts - 1) -
                                   np.log(1 - natural_small_shapes[2])),
                                  (np.log(num_alts - 1) -
                                   np.log(1 - natural_small_shapes[0])),
                                  -np.log(natural_small_shapes[2])])

        # Bundle the arguments needed for _asym_transform_deriv_v()
        args = [test_index,
                self.fake_df[self.alt_id_col].values,
                self.fake_rows_to_alts,
                self.fake_shapes]

        # Create the needed output array
        num_rows = test_index.shape[0]
        output = diags(np.ones(num_rows), 0, format='csr')

        # Validate the results from asym_transform_deriv_v
        for shape_array, results in [(large_shapes, large_results),
                                     (small_shapes, small_results)]:
            # Get the reslts from asym_transform_deriv_v
            args[-1] = shape_array
            kwargs = {"ref_position": self.fake_shape_ref_pos,
                      "output_array": output}
            derivative = asym._asym_transform_deriv_v(*args, **kwargs)

            # Ensure the results are as expected
            self.assertIsInstance(derivative, type(output))
            self.assertEqual(len(derivative.shape), 2)
            self.assertEqual(derivative.shape, (num_rows, num_rows))
            npt.assert_allclose(np.diag(derivative.A), results)

        return None

    def test_asym_transform_deriv_alpha(self):
        """
        Ensures that asym_transform_deriv_alpha returns the `output_array`
        kwarg.
        """
        # Bundle the args for the function being tested
        args = [self.fake_index,
                self.fake_df[self.alt_id_col].values,
                self.fake_rows_to_alts,
                self.fake_intercepts]

        # Take the relevant columns of rows_to_alts, as would be done with
        # outside intercepts, or test None for when there are no outside
        # intercepts.
        for test_output in [None, self.fake_rows_to_alts[:, [0, 1]]]:
            kwargs = {"output_array": test_output}
            derivative_results = asym._asym_transform_deriv_alpha(*args,
                                                                  **kwargs)
            if test_output is None:
                self.assertIsNone(derivative_results)
            else:
                npt.assert_allclose(test_output.A, derivative_results.A)

        return None

    def test_calc_deriv_c_with_respect_to_eta(self):
        """
        Ensure the calculation of the derivative of the natural shape
        parameters with respect to the reparameterized shape parameters is
        correct.
        """
        # Specify some shape parameters to test the function
        natural_test_shapes = np.array([0.5, 0.25, 0.25])
        # Specify an alternative to serve as the reference parameter
        ref_position = 2
        # Create an output array that will contain the derivative
        output = np.zeros((natural_test_shapes.shape[0],
                           natural_test_shapes.shape[0] - 1))

        # Record the correct derivatives
        interim_results = np.array([[0.5 - 0.25, -0.5 * 0.25, -0.5 * 0.25],
                                    [-0.5 * 0.25, 0.25 - 0.0625, -0.25**2],
                                    [-0.5 * 0.25, -0.25**2, 0.25 - 0.0625]])
        correct_results = interim_results[:, [0, 1]]

        # Get the results from the function
        deriv_args = (natural_test_shapes, ref_position)
        kwargs = {'output_array': output}
        function_results = asym._calc_deriv_c_with_respect_to_eta(*deriv_args,
                                                                  **kwargs)

        # Ensure that the results are what I expect them to be
        self.assertIsInstance(function_results, np.ndarray)
        self.assertEqual(len(function_results.shape), 2)
        self.assertEqual(function_results.shape, output.shape)
        npt.assert_allclose(function_results, correct_results)

        return None

    def test_asym_transform_deriv_shape(self):
        """
        Ensures that the asym_transform_deriv_shape() function provides
        correct results, and handles all forms of overflow and underflow
        correctly.
        """
        # Create index values to test the function with.
        test_index = np.array([1, 0, -2, -1e400, 1e400])

        # Figure out how many alternatives are there
        num_alts = self.fake_rows_to_alts.shape[1]

        # Test what happens with shape parameters of 'normal' magnitude
        regular_shapes = self.fake_shapes
        natural_reg_shapes = self.natural_shapes

        result_2 = (natural_reg_shapes[2]**-1 +
                    test_index[2] / (1.0 - natural_reg_shapes[2]))

        regular_results = np.array([0,
                                    natural_reg_shapes[1]**-1,
                                    result_2,
                                    -asym.max_comp_value,
                                    -asym.max_comp_value])

        # Test what happens with large shape parameters (that result in
        # 'natural' shape parameters near 1.0)
        large_shapes = np.array([800, 0])
        natural_large_shapes = asym._convert_eta_to_c(large_shapes,
                                                      self.fake_shape_ref_pos)
        large_results = np.array([0,
                                  asym.max_comp_value,
                                  asym.max_comp_value,
                                  -asym.max_comp_value,
                                  -asym.max_comp_value])

        # Test what happens with large shape parameters (that result in
        # 'natural' shape parameters near 1.0)
        small_index = test_index.copy()
        small_index[0] = 0.5

        small_shapes = np.array([-800, 0])
        natural_small_shapes = asym._convert_eta_to_c(small_shapes,
                                                      self.fake_shape_ref_pos)
        result_3_3 = (natural_small_shapes[2]**-1 +
                      test_index[2] / (1 - natural_small_shapes[2]))
        small_results = np.array([asym.max_comp_value,
                                  natural_small_shapes[1]**-1,
                                  result_3_3,
                                  -asym.max_comp_value,
                                  -asym.max_comp_value])

        # Bundle the arguments needed for _asym_transform_deriv_shape()
        args = [test_index,
                self.fake_df[self.alt_id_col].values,
                self.fake_rows_to_alts,
                self.fake_shapes]

        # Create the needed output array for the overall derivative with
        # respect to the reparameterized shape parameters
        num_rows = test_index.shape[0]
        output = np.matrix(np.zeros((num_rows, num_alts - 1)), dtype=float)

        # Create the needed output array for the intermediate derivative of the
        # 'natural' shape parameters with respect to the reparameterized shape
        # parameters.
        deriv_output = np.zeros((num_alts, num_alts - 1), dtype=float)

        # Create the array needed for dh_dc
        dh_dc_array = self.fake_rows_to_alts.copy()

        # Create a function that will take the reparameterized shapes and the
        # shape reference position and return the derivative of the natural
        # shapes with respect to the reparameterized shapes.
        interim_deriv_func = partial(asym._calc_deriv_c_with_respect_to_eta,
                                     output_array=deriv_output)

        # Take note of the kwargs needed for asym_transform_deriv_shape()
        deriv_kwargs = {"ref_position": self.fake_shape_ref_pos,
                        "dh_dc_array": dh_dc_array,
                        "fill_dc_d_eta": interim_deriv_func,
                        "output_array": output}

        # Validate the results from asym_transform_deriv_shape
        test_arrays = [(regular_shapes, natural_reg_shapes, regular_results),
                       (large_shapes, natural_large_shapes, large_results),
                       (small_shapes, natural_small_shapes, small_results)]
        for shape_array, natural_shape_array, interim_results in test_arrays:
            if shape_array[0] == small_shapes[0]:
                args[0] = small_index

            # Get the reslts from asym_transform_deriv_shape()
            args[-1] = shape_array
            derivative = asym._asym_transform_deriv_shape(*args,
                                                          **deriv_kwargs)

            # Calculate the derivative of the 'natural' shape parameters with
            # espect to the reparameterized shape parameters.
            interim_args = (natural_shape_array, self.fake_shape_ref_pos)
            kwargs = {"output_array": deepcopy(deriv_output)}
            dc_d_eta = asym._calc_deriv_c_with_respect_to_eta(*interim_args,
                                                              **kwargs)

            # Place the interim results in the correct shape
            interim_results = interim_results[:, None]
            interim_results = self.fake_rows_to_alts.multiply(interim_results)

            # Calculate the correct results
            results = interim_results.A.dot(dc_d_eta)

            # Ensure the results are as expected
            self.assertIsInstance(derivative, type(output))
            self.assertEqual(len(derivative.shape), 2)
            self.assertEqual(derivative.shape, (num_rows, num_alts - 1))
            npt.assert_allclose(derivative.A, results)

        return None

<MSG> Wrote yet more tests for asym_logit.py.

<DFF> @@ -7,6 +7,7 @@ import warnings
 import unittest
 from collections import OrderedDict
 from copy import deepcopy
+from functools import partial
 
 import numpy as np
 import numpy.testing as npt
@@ -370,163 +371,181 @@ class HelperFuncTests(GenericTestCase):
 
         return None
 
-    # def test_asym_utility_transform(self):
-    #     """
-    #     Ensures that `_asym_utility_transform()` returns correct results
-    #     """
-    #     # Create a set of systematic utilities that will test the function for
-    #     # correct calculations, for proper dealing with overflow, and for
-    #     # proper dealing with underflow.
-
-    #     # The first and third elements tests general calculation.
-    #     # The second element of index_array should lead to the transformation
-    #     # equaling the 'natural' shape parameter for alternative 2.
-    #     # The fourth element should test what happens with underflow and should
-    #     # lead to max_comp_value.
-    #     # The fifth element should test what happens with overflow and should
-    #     # lead to -1.0 * max_comp_value
-    #     index_array = np.array([1, 0, -1, 1e400, -1e400])
-
-    #     # We can use a the following two arrays of the shape parameters to test
-    #     # the overflow and underflow capabilities with respect to the shape
-    #     # parameters.
-    #     test_shapes_2 = np.array([-800, 0])
-    #     test_shapes_3 = np.array([800, 0])
-
-    #     # Figure out the value of the 'natural' shape parameters
-    #     natural_shapes_2 = asym._convert_eta_to_c(test_shapes_2,
-    #                                               self.fake_shape_ref_pos)
-    #     natural_shapes_3 = asym._convert_eta_to_c(test_shapes_3,
-    #                                               self.fake_shape_ref_pos)
-
-    #     # Crerate the array of expected results when using shape parameters
-    #     # of 'normal' magnitudes.
-    #     intercept_1 = self.fake_intercepts[0]
-    #     intercept_2 = self.fake_intercepts[1]
-    #     intercept_3 = 0
-
-    #     result_1 = (intercept_1 +
-    #                 np.log(self.natural_shapes[0]) * (1 - index_array[0]))
-
-    #     result_2 = intercept_2 + np.log(self.natural_shapes[1])
-
-    #     result_3 = (intercept_3 +
-    #                 np.log(self.natural_shapes[2]) -
-    #                 np.log(1 - self.natural_shapes[2]) * index_array[2])
-
-    #     expected_results = np.array([result_1,
-    #                                  result_2,
-    #                                  result_3,
-    #                                  asym.max_comp_value + intercept_1,
-    #                                  - asym.max_comp_value])[:, None]
-
-    #     # Crerate the array of expected results when using shape parameters
-    #     # of 'abnormally' small magnitudes.
-    #     result_2_2 = intercept_2 + np.log(natural_shapes_2[1])
-    #     result_3_2 = (intercept_3 +
-    #                   np.log(natural_shapes_2[2]) -
-    #                   np.log(1 - natural_shapes_2[2]) * index_array[2])
-
-    #     expected_results_2 = np.array([-asym.max_comp_value,
-    #                                    result_2_2,
-    #                                    result_3_2,
-    #                                    -asym.max_comp_value + intercept_1,
-    #                                    -asym.max_comp_value])[:, None]
-
-    #     # Crerate the array of expected results when using shape parameters
-    #     # of 'abnormally' large magnitudes.
-    #     expected_results_3 = np.array([0 + intercept_1,
-    #                                    -asym.max_comp_value + intercept_2,
-    #                                    -asym.max_comp_value + intercept_3,
-    #                                    0 + intercept_1,
-    #                                    -asym.max_comp_value])[:, None]
-
-    #     #####
-    #     # Perform various rounds of checking
-    #     #####
-    #     # Use the utility transformation function, round_1
-    #     alt_id_vals = self.fake_df[self.alt_id_col].values
-    #     args = [index_array,
-    #             alt_id_vals,
-    #             self.fake_rows_to_alts,
-    #             self.fake_shapes,
-    #             self.fake_intercepts]
-    #     kwargs = {"intercept_ref_pos": self.fake_intercept_ref_pos,
-    #               "shape_ref_position": self.fake_shape_ref_pos}
-    #     func_results = asym._asym_utility_transform(*args, **kwargs)
-
-    #     # Use the utility transformation function, round_2
-    #     args[3] = test_shapes_2
-    #     func_results_2 = asym._asym_utility_transform(*args, **kwargs)
-
-    #     # Use the utility transformation function, round_3
-    #     args[3] = test_shapes_3
-    #     func_results_3 = asym._asym_utility_transform(*args, **kwargs)
-
-    #     # Check the correctness of the results
-    #     all_results = [(func_results, expected_results),
-    #                    (func_results_2, expected_results_2),
-    #                    (func_results_3, expected_results_3)]
-
-    #     for pos, (test_results, correct_results) in enumerate(all_results):
-    #         print("Result pair {}".format(pos))
-    #         print("Function results: {}".format(test_results))
-    #         print("Correct results: {}".format(correct_results))
-    #         self.assertIsInstance(test_results, np.ndarray)
-    #         self.assertEqual(len(test_results.shape), 2)
-    #         self.assertEqual(test_results.shape[1], correct_results.shape[1])
-    #         self.assertEqual(test_results.shape[0], correct_results.shape[0])
-    #         npt.assert_allclose(test_results, correct_results)
-
-    #     return None
-
-    # def test_asym_transform_deriv_v(self):
-    #     """
-    #     Tests basic behavior of the asym_transform_deriv_v.
-    #     """
-    #     # Note the index has a value that is small and a value that is large to
-    #     # test whether or not the function correctly deals with underflow and
-    #     # overflow when calculating the derivative.
-    #     test_index = np.array([-2, 0, 2, -3000, 800])
-    #     # Note we use a compressed sparse-row matrix so that we can easily
-    #     # convert the output matrix to a numpy array using the '.A' attribute.
-    #     num_rows = test_index.shape[0]
-    #     test_output = diags(np.ones(num_rows),
-    #                         0, format='csr')
-
-    #     # Bundle the arguments needed for _asym_transform_deriv_v()
-    #     args = [test_index,
-    #             self.fake_df[self.alt_id_col].values,
-    #             self.fake_rows_to_alts,
-    #             self.fake_shapes]
-
-    #     # Get the derivative using the function defined in asym_logit.py.
-    #     derivative = asym._asym_transform_deriv_v(*args,
-    #                                                   output_array=test_output)
-
-    #     # Initialize an array of correct results
-    #     # Note the second element is by design where each of the terms in the
-    #     # derivative should evaluate to 1 and we get 1 / 1 = 1.
-    #     correct_derivatives = np.array([np.nan,
-    #                                     (1 + np.exp(self.fake_shapes[1])) / 2,
-    #                                     np.nan,
-    #                                     np.exp(self.fake_shapes[0]),
-    #                                     1.0])
-
-    #     # Calculate 'by hand' what the correct results should be
-    #     for i in [0, 2]:
-    #         shape = np.exp(self.fake_shapes[i])
-    #         index_val = test_index[i]
-    #         correct_derivatives[i] = ((1 + np.exp(-index_val))**-1 +
-    #                                    shape / (1 + np.exp(shape * index_val)))
-
-    #     self.assertIsInstance(derivative, type(test_output))
-    #     self.assertEqual(len(derivative.shape), 2)
-    #     self.assertEqual(derivative.shape, (num_rows, num_rows))
-    #     npt.assert_allclose(correct_derivatives,
-    #                         np.diag(derivative.A))
-
-    #     return None
+    def test_asym_utility_transform(self):
+        """
+        Ensures that `_asym_utility_transform()` returns correct results
+        """
+        # Create a set of systematic utilities that will test the function for
+        # correct calculations, for proper dealing with overflow, and for
+        # proper dealing with underflow.
+
+        # The first and third elements tests general calculation.
+        # The second element of index_array should lead to the transformation
+        # equaling the 'natural' shape parameter for alternative 2.
+        # The fourth element should test what happens with underflow and should
+        # lead to max_comp_value.
+        # The fifth element should test what happens with overflow and should
+        # lead to -1.0 * max_comp_value
+        index_array = np.array([1, 0, -1, 1e400, -1e400])
+
+        # We can use a the following array of the shape parameters to test
+        # the underflow capabilities with respect to the shape
+        # parameters.
+        test_shapes_2 = np.array([-800, 0])
+
+        test_shapes_3 = np.array([800, 0])
+
+        # Figure out the value of the 'natural' shape parameters
+        natural_shapes_2 = asym._convert_eta_to_c(test_shapes_2,
+                                                  self.fake_shape_ref_pos)
+        natural_shapes_3 = asym._convert_eta_to_c(test_shapes_3,
+                                                  self.fake_shape_ref_pos)
+
+        # Crerate the array of expected results when using shape parameters
+        # of 'normal' magnitudes.
+        intercept_1 = self.fake_intercepts[0]
+        intercept_2 = self.fake_intercepts[1]
+        intercept_3 = 0
+
+        result_1 = (intercept_1 +
+                    np.log(self.natural_shapes[0]) * (1 - index_array[0]))
+
+        result_2 = intercept_2 + np.log(self.natural_shapes[1])
+
+        # Note the division by 2 is due to the 'J - 1' term. See the original
+        # definition of the transformation.
+        result_3 = (intercept_3 +
+                    np.log(self.natural_shapes[2]) -
+                    np.log((1 - self.natural_shapes[2]) / 2) * index_array[2])
+
+        expected_results = np.array([result_1,
+                                     result_2,
+                                     result_3,
+                                     asym.max_comp_value + intercept_1,
+                                     - asym.max_comp_value])[:, None]
+
+        # Crerate the array of expected results when using shape parameters
+        # of 'abnormally' small magnitudes.
+        # Note the division by 2 is due to the 'J - 1' term. See the original
+        # definition of the transformation.
+        result_2_2 = intercept_2 + np.log(natural_shapes_2[1])
+        result_3_2 = (intercept_3 +
+                      np.log(natural_shapes_2[2]) -
+                      np.log((1 - natural_shapes_2[2]) / 2) * index_array[2])
+
+        # Note the '0' comes from (1-1) * ln(shape)
+        expected_results_2 = np.array([0 + intercept_1,
+                                       result_2_2,
+                                       result_3_2,
+                                       asym.max_comp_value + intercept_1,
+                                       -asym.max_comp_value])[:, None]
+
+        # Create the array of expected results when using shape parameters
+        # of 'abnormally' large magnitudes.
+        result_2_3 = intercept_2 + np.log(natural_shapes_3[1])
+        result_3_3 = (intercept_3 +
+                      np.log(natural_shapes_3[2]) -
+                      np.log((1 - natural_shapes_3[2]) / 2) * index_array[2])
+
+        expected_results_3 = np.array([0 + intercept_1,
+                                       result_2_3,
+                                       result_3_3,
+                                       0 + intercept_1,
+                                       -asym.max_comp_value])[:, None]
+
+        #####
+        # Perform various rounds of checking
+        #####
+        # Use the utility transformation function, round_1
+        alt_id_vals = self.fake_df[self.alt_id_col].values
+        args = [index_array,
+                alt_id_vals,
+                self.fake_rows_to_alts,
+                self.fake_shapes,
+                self.fake_intercepts]
+        kwargs = {"intercept_ref_pos": self.fake_intercept_ref_pos,
+                  "shape_ref_position": self.fake_shape_ref_pos}
+        func_results = asym._asym_utility_transform(*args, **kwargs)
+
+        # Use the utility transformation function, round_2
+        args[3] = test_shapes_2
+        func_results_2 = asym._asym_utility_transform(*args, **kwargs)
+
+        # Use the utility transformation function, round_3
+        args[3] = test_shapes_3
+        func_results_3 = asym._asym_utility_transform(*args, **kwargs)
+
+        # Check the correctness of the results
+        all_results = [(func_results, expected_results),
+                       (func_results_2, expected_results_2),
+                       (func_results_3, expected_results_3)]
+
+        for pos, (test_results, correct_results) in enumerate(all_results):
+            self.assertIsInstance(test_results, np.ndarray)
+            self.assertEqual(len(test_results.shape), 2)
+            self.assertEqual(test_results.shape[1], correct_results.shape[1])
+            self.assertEqual(test_results.shape[0], correct_results.shape[0])
+            npt.assert_allclose(test_results, correct_results)
+
+        return None
+
+    def test_asym_transform_deriv_v(self):
+        """
+        Tests basic behavior of the asym_transform_deriv_v. Essentially the
+        only things that can go wrong is underflow or overflow from the shape
+        parameters going to zero or 1.0.
+        """
+        # Declare  set of index values to be tested
+        test_index = np.array([1, 0, -1, -2, 2])
+        # Figure out how many alternatives are there
+        num_alts = self.fake_rows_to_alts.shape[1]
+        # Test what happens with large shape parameters (that result in
+        # 'natural' shape parameters near 1.0)
+        large_shapes = np.array([800, 0])
+        large_results = np.array([0,
+                                  -np.log(asym.min_comp_value),
+                                  np.log(num_alts - 1),
+                                  asym.max_comp_value,
+                                  -np.log(asym.min_comp_value)])
+        # Test what happens with large shape parameters (that result in
+        # 'natural' shape parameters near 1.0)
+        small_shapes = np.array([-800, 0])
+        natural_small_shapes = asym._convert_eta_to_c(small_shapes,
+                                                      self.fake_shape_ref_pos)
+        small_results = np.array([-np.log(natural_small_shapes[0]),
+                                  -np.log(natural_small_shapes[1]),
+                                  (np.log(num_alts - 1) -
+                                   np.log(1 - natural_small_shapes[2])),
+                                  (np.log(num_alts - 1) -
+                                   np.log(1 - natural_small_shapes[0])),
+                                  -np.log(natural_small_shapes[2])])
+
+        # Bundle the arguments needed for _asym_transform_deriv_v()
+        args = [test_index,
+                self.fake_df[self.alt_id_col].values,
+                self.fake_rows_to_alts,
+                self.fake_shapes]
+
+        # Create the needed output array
+        num_rows = test_index.shape[0]
+        output = diags(np.ones(num_rows), 0, format='csr')
+
+        # Validate the results from asym_transform_deriv_v
+        for shape_array, results in [(large_shapes, large_results),
+                                     (small_shapes, small_results)]:
+            # Get the reslts from asym_transform_deriv_v
+            args[-1] = shape_array
+            kwargs = {"ref_position": self.fake_shape_ref_pos,
+                      "output_array": output}
+            derivative = asym._asym_transform_deriv_v(*args, **kwargs)
+
+            # Ensure the results are as expected
+            self.assertIsInstance(derivative, type(output))
+            self.assertEqual(len(derivative.shape), 2)
+            self.assertEqual(derivative.shape, (num_rows, num_rows))
+            npt.assert_allclose(np.diag(derivative.A), results)
+
+        return None
 
     def test_asym_transform_deriv_alpha(self):
         """
@@ -553,63 +572,154 @@ class HelperFuncTests(GenericTestCase):
 
         return None
 
-    # def test_asym_transform_deriv_shape(self):
-    #     """
-    #     Ensures that the asym_transform_deriv_shape() function provides
-    #     correct results, and handles all forms of overflow and underflow
-    #     correctly.
-    #     """
-    #     # Note the index has a value that is small and a value that is large to
-    #     # test whether or not the function correctly uses L'Hopital's rule to
-    #     # deal with underflow and overflow from the index when calculating the
-    #     # derivative. When the index is large, the derivative should be the
-    #     # negative of the inverse of the 'natural' shape parameter. When the
-    #     # index is small the derivative should be max_comp_value (theoretically
-    #     # inf). When the shape is large, the derivative should be the negative
-    #     # of the natural log of 1 + exp(-index).
-    #     test_index = np.array([-10, 0, 2, -10000, 1e8])
-
-    #     # Note we use a compressed sparse-row matrix so that we can easily
-    #     # convert the output matrix to a numpy array using the '.A' attribute.
-    #     num_rows = test_index.shape[0]
-    #     test_output = diags(np.ones(num_rows),
-    #                         0, format='csr')
-
-    #     # Create new test shapes to use. We make the first shape parameter big
-    #     # to test overflow handling from this situation and L'Hopital's rule.
-    #     new_shapes = deepcopy(self.fake_shapes)
-    #     new_shapes[0] = np.log(80)
-    #     natural_shapes = np.exp(new_shapes)
-
-    #     # Bundle the arguments needed for _scobit_transform_deriv_v()
-    #     args = [test_index,
-    #             self.fake_df[self.alt_id_col].values,
-    #             self.fake_rows_to_alts,
-    #             new_shapes]
-    #     kwargs = {"output_array": test_output}
-
-    #     # Get the derivative using the function defined in asym_logit.py.
-    #     derivative = asym._asym_transform_deriv_shape(*args, **kwargs)
-
-    #     # Initialize an array of correct results
-    #     # Note the second element is by design so the derivative equals 2ln(2)
-    #     # Also, we precompute the results and multiply by natural shapes to
-    #     # account for the jacobian when computing the gradient with respect to
-    #     # reparameterized shape parameters.
-    #     result_1 = (test_index[0] /
-    #                 (1 + np.exp(natural_shapes[0] * test_index[0])))
-    #     result_3 = (test_index[2] /
-    #                 (1 + np.exp(natural_shapes[2] * test_index[2])))
-    #     correct_derivatives = np.array([result_1 * natural_shapes[0],
-    #                                     0,
-    #                                     result_3 * natural_shapes[2],
-    #                                     test_index[3] * natural_shapes[0],
-    #                                     0])
-
-    #     self.assertIsInstance(derivative, type(test_output))
-    #     self.assertEqual(len(derivative.shape), 2)
-    #     self.assertEqual(derivative.shape, (num_rows, num_rows))
-    #     npt.assert_allclose(correct_derivatives,
-    #                         np.diag(derivative.A))
-
-    #     return None
+    def test_calc_deriv_c_with_respect_to_eta(self):
+        """
+        Ensure the calculation of the derivative of the natural shape
+        parameters with respect to the reparameterized shape parameters is
+        correct.
+        """
+        # Specify some shape parameters to test the function
+        natural_test_shapes = np.array([0.5, 0.25, 0.25])
+        # Specify an alternative to serve as the reference parameter
+        ref_position = 2
+        # Create an output array that will contain the derivative
+        output = np.zeros((natural_test_shapes.shape[0],
+                           natural_test_shapes.shape[0] - 1))
+
+        # Record the correct derivatives
+        interim_results = np.array([[0.5 - 0.25, -0.5 * 0.25, -0.5 * 0.25],
+                                    [-0.5 * 0.25, 0.25 - 0.0625, -0.25**2],
+                                    [-0.5 * 0.25, -0.25**2, 0.25 - 0.0625]])
+        correct_results = interim_results[:, [0, 1]]
+
+        # Get the results from the function
+        deriv_args = (natural_test_shapes, ref_position)
+        kwargs = {'output_array': output}
+        function_results = asym._calc_deriv_c_with_respect_to_eta(*deriv_args,
+                                                                  **kwargs)
+
+        # Ensure that the results are what I expect them to be
+        self.assertIsInstance(function_results, np.ndarray)
+        self.assertEqual(len(function_results.shape), 2)
+        self.assertEqual(function_results.shape, output.shape)
+        npt.assert_allclose(function_results, correct_results)
+
+        return None
+
+    def test_asym_transform_deriv_shape(self):
+        """
+        Ensures that the asym_transform_deriv_shape() function provides
+        correct results, and handles all forms of overflow and underflow
+        correctly.
+        """
+        # Create index values to test the function with.
+        test_index = np.array([1, 0, -2, -1e400, 1e400])
+
+        # Figure out how many alternatives are there
+        num_alts = self.fake_rows_to_alts.shape[1]
+
+        # Test what happens with shape parameters of 'normal' magnitude
+        regular_shapes = self.fake_shapes
+        natural_reg_shapes = self.natural_shapes
+
+        result_2 = (natural_reg_shapes[2]**-1 +
+                    test_index[2] / (1.0 - natural_reg_shapes[2]))
+
+        regular_results = np.array([0,
+                                    natural_reg_shapes[1]**-1,
+                                    result_2,
+                                    -asym.max_comp_value,
+                                    -asym.max_comp_value])
+
+        # Test what happens with large shape parameters (that result in
+        # 'natural' shape parameters near 1.0)
+        large_shapes = np.array([800, 0])
+        natural_large_shapes = asym._convert_eta_to_c(large_shapes,
+                                                      self.fake_shape_ref_pos)
+        large_results = np.array([0,
+                                  asym.max_comp_value,
+                                  asym.max_comp_value,
+                                  -asym.max_comp_value,
+                                  -asym.max_comp_value])
+
+        # Test what happens with large shape parameters (that result in
+        # 'natural' shape parameters near 1.0)
+        small_index = test_index.copy()
+        small_index[0] = 0.5
+
+        small_shapes = np.array([-800, 0])
+        natural_small_shapes = asym._convert_eta_to_c(small_shapes,
+                                                      self.fake_shape_ref_pos)
+        result_3_3 = (natural_small_shapes[2]**-1 +
+                      test_index[2] / (1 - natural_small_shapes[2]))
+        small_results = np.array([asym.max_comp_value,
+                                  natural_small_shapes[1]**-1,
+                                  result_3_3,
+                                  -asym.max_comp_value,
+                                  -asym.max_comp_value])
+
+        # Bundle the arguments needed for _asym_transform_deriv_shape()
+        args = [test_index,
+                self.fake_df[self.alt_id_col].values,
+                self.fake_rows_to_alts,
+                self.fake_shapes]
+
+        # Create the needed output array for the overall derivative with
+        # respect to the reparameterized shape parameters
+        num_rows = test_index.shape[0]
+        output = np.matrix(np.zeros((num_rows, num_alts - 1)), dtype=float)
+
+        # Create the needed output array for the intermediate derivative of the
+        # 'natural' shape parameters with respect to the reparameterized shape
+        # parameters.
+        deriv_output = np.zeros((num_alts, num_alts - 1), dtype=float)
+
+        # Create the array needed for dh_dc
+        dh_dc_array = self.fake_rows_to_alts.copy()
+
+        # Create a function that will take the reparameterized shapes and the
+        # shape reference position and return the derivative of the natural
+        # shapes with respect to the reparameterized shapes.
+        interim_deriv_func = partial(asym._calc_deriv_c_with_respect_to_eta,
+                                     output_array=deriv_output)
+
+        # Take note of the kwargs needed for asym_transform_deriv_shape()
+        deriv_kwargs = {"ref_position": self.fake_shape_ref_pos,
+                        "dh_dc_array": dh_dc_array,
+                        "fill_dc_d_eta": interim_deriv_func,
+                        "output_array": output}
+
+        # Validate the results from asym_transform_deriv_shape
+        test_arrays = [(regular_shapes, natural_reg_shapes, regular_results),
+                       (large_shapes, natural_large_shapes, large_results),
+                       (small_shapes, natural_small_shapes, small_results)]
+        for shape_array, natural_shape_array, interim_results in test_arrays:
+            if shape_array[0] == small_shapes[0]:
+                args[0] = small_index
+
+            # Get the reslts from asym_transform_deriv_shape()
+            args[-1] = shape_array
+            derivative = asym._asym_transform_deriv_shape(*args,
+                                                          **deriv_kwargs)
+
+            # Calculate the derivative of the 'natural' shape parameters with
+            # espect to the reparameterized shape parameters.
+            interim_args = (natural_shape_array, self.fake_shape_ref_pos)
+            kwargs = {"output_array": deepcopy(deriv_output)}
+            dc_d_eta = asym._calc_deriv_c_with_respect_to_eta(*interim_args,
+                                                              **kwargs)
+
+            # Place the interim results in the correct shape
+            interim_results = interim_results[:, None]
+            interim_results = self.fake_rows_to_alts.multiply(interim_results)
+
+            # Calculate the correct results
+            results = interim_results.A.dot(dc_d_eta)
+
+            # Ensure the results are as expected
+            self.assertIsInstance(derivative, type(output))
+            self.assertEqual(len(derivative.shape), 2)
+            self.assertEqual(derivative.shape, (num_rows, num_alts - 1))
+            npt.assert_allclose(derivative.A, results)
+
+        return None

Wrote yet more tests for asym_logit.py.