docs: def panel_predict(self,
                        data,
                        num_draws,
                        return_long_probs=True,
                        choice_col=None,
                        seed=None):
          """
          Parameters
          ----------
          data : string or pandas dataframe.
              If string, data should be an absolute or relative path to a CSV
              file containing the long format data to be predicted with this
              choice model. Note long format has one row per available
              alternative for each observation. If pandas dataframe, the
              dataframe should be in long format.
          num_draws : int.
              Should be greater than zero. Denotes the number of draws being
              made from each mixing distribution for the random coefficients.
          return_long_probs : bool, optional.
              Indicates whether or not the long format probabilites (a 1D numpy
              array with one element per observation per available alternative)
              should be returned. Default == True.
          choice_col : str or None, optonal.
              Denotes the column in long_form which contains a one if the
              alternative pertaining to the given row was the observed outcome
              for the observation pertaining to the given row and a zero
              otherwise. If passed, then an array of probabilities of just the
              chosen alternative for each observation will be returned.
              Default == None.
          seed : int or None, optional.
              If an int is passed, it should be greater than zero. Denotes the
              value to be used in seeding the random generator used to generate
              the draws from the mixing distributions of each random coefficient.
              Default == None.
  
          Returns
          -------
          numpy array or tuple of two numpy arrays.
              - If `choice_col` is passed AND `return_long_probs` is True, then
                the tuple `(chosen_probs, pred_probs_long)` is returned.
              - If `return_long_probs` is True and `choice_col` is None, then
                only `pred_probs_long` is returned.
              - If `choice_col` is passed and `return_long_probs` is False then
                `chosen_probs` is returned.
  
              `chosen_probs` is a 1D numpy array of shape (num_observations,).
              Each element is the probability of the corresponding observation
              being associated with its realized outcome.
  
              `pred_probs_long` is a 1D numpy array with one element per
              observation per available alternative for that observation. Each
              element is the probability of the corresponding observation being
              associated with that row's corresponding alternative.
  
          Notes
          -----
          It is NOT valid to have `choice_col == None` and
          `return_long_probs == False`.
          """
          # Ensure that the function arguments are valid
          if choice_col is None and not return_long_probs:
              msg = "choice_col is None AND return_long_probs == False"
              raise ValueError(msg)
  
          # Get the dataframe of observations we'll be predicting on
          dataframe = get_dataframe_from_data(data)
  
          # Determine the conditions under which we will add an intercept column
          # to our long format dataframe.
          condition_1 = "intercept" in self.specification
          condition_2 = "intercept" not in dataframe.columns
  
          if condition_1 and condition_2:
              dataframe["intercept"] = 1.0
  
          # Make sure the necessary columns are in the long format dataframe
          for column in [self.alt_id_col,
                         self.obs_id_col,
                         self.mixing_id_col]:
              if column is not None and column not in dataframe.columns:
                  msg = "{} not in data.columns".format(column)
                  raise ValueError(msg)
  
          # Get the new column of alternative IDs and get the new design matrix
          new_alt_IDs = dataframe[self.alt_id_col].values
  
          new_design_res = create_design_matrix(dataframe,
                                                self.specification,
                                                self.alt_id_col,
                                                names=self.name_spec)
          new_design_2d = new_design_res[0]
  
          # Get the new mappings between the alternatives and observations
          mapping_res = create_long_form_mappings(dataframe,
                                                  self.obs_id_col,
                                                  self.alt_id_col,
                                                  choice_col=choice_col,
                                                  nest_spec=self.nest_spec,
                                                  mix_id_col=self.mixing_id_col)
  
          new_rows_to_obs = mapping_res["rows_to_obs"]
          new_rows_to_alts = mapping_res["rows_to_alts"]
          new_chosen_to_obs = mapping_res["chosen_row_to_obs"]
          new_rows_to_mixers = mapping_res["rows_to_mixers"]
  
          # Determine the coefficients being used for prediction.
          # Note that I am making an implicit assumption (for now) that the
          # kernel probabilities are coming from a logit-type model.
          new_index_coefs = self.coefs.values
          new_intercepts = (self.intercepts.values if self.intercepts
                            is not None else None)
          new_shape_params = (self.shapes.values if self.shapes
                              is not None else None)
  
          # Get the draws for each random coefficient
          num_mixing_units = new_rows_to_mixers.shape[1]
          draw_list = mlc.get_normal_draws(num_mixing_units,
                                           num_draws,
                                           len(self.mixing_pos),
                                           seed=seed)
  
          # Calculate the 3D design matrix for the prediction.
          design_args = (new_design_2d,
                         draw_list,
                         self.mixing_pos,
                         new_rows_to_mixers)
          new_design_3d = mlc.create_expanded_design_for_mixing(*design_args)
          # Calculate the desired probabilities for the mixed logit model.
          prob_args = (new_index_coefs,
                       new_design_3d,
                       new_alt_IDs,
                       new_rows_to_obs,
                       new_rows_to_alts,
                       mnl_utility_transform)
          prob_kwargs = {"intercept_params": new_intercepts,
                         "shape_params": new_shape_params,
                         "return_long_probs": True}
          # Note that I am making an implicit assumption (for now) that the
          # kernel probabilities are coming from a logit-type model.
          new_kernel_probs = general_calc_probabilities(*prob_args,
                                                        **prob_kwargs)
  
          # Initialize and calculate the weights needed for prediction with
          # "individualized" coefficient distributions. Should have shape
          # (new_row_to_mixer.shape[1], num_draws)
          weights_per_ind_per_draw = (1.0 / num_draws *
                                      np.ones((new_rows_to_mixers.shape[1],
                                               num_draws)))
  
          ##########
          # Create an array denoting the observation ids that are present in both
          # the dataset to be predicted and the dataset used for model estimation
          ##########
          # Get the old mixing ids
          old_mixing_id_long = self.data[self.mixing_id_col].values
          # Get the new mixing ids
          new_mixing_id_long = dataframe[self.mixing_id_col].values
          # Get the unique individual ids from the original and preserve order
          orig_unique_id_idx_old = np.sort(np.unique(old_mixing_id_long,
                                                     return_index=True)[1])
          orig_unique_id_idx_new = np.sort(np.unique(new_mixing_id_long,
                                                     return_index=True)[1])
          # Get the unique ids, in their original order of appearance
          orig_order_unique_ids_old = old_mixing_id_long[orig_unique_id_idx_old]
          orig_order_unique_ids_new = new_mixing_id_long[orig_unique_id_idx_new]
  
          # Figure out which long format rows have ids are common to both
          # datasets
          old_repeat_mixing_id_idx = np.in1d(old_mixing_id_long,
                                             orig_order_unique_ids_new)
          # Figure out which unique ids are in both datasets
          old_unique_mix_id_repeats = np.in1d(orig_order_unique_ids_old,
                                              orig_order_unique_ids_new)
          new_unique_mix_id_repeats = np.in1d(orig_order_unique_ids_new,
                                              orig_order_unique_ids_old)
  
          # Get the 2d design matrix used to estimate the model, and filter it
          # to only those individuals for whom we are predicting new choice
          # situations.
          past_design_2d = self.design[old_repeat_mixing_id_idx, :]
  
          ##########
          # Appropriately filter the old mapping matrix that maps rows of the
          # long format design matrix to unique mixing units.
          ##########
          orig_mappings = self.get_mappings_for_fit()
          past_mappings = self.__filter_past_mappings(orig_mappings,
                                                      old_repeat_mixing_id_idx)
  
          # Create the 3D design matrix for those choice situations, using the
          # draws that were just taken from the mixing distributions of interest.
          past_draw_list = [x[new_unique_mix_id_repeats, :] for x in draw_list]
          design_args = (past_design_2d,
                         past_draw_list,
                         self.mixing_pos,
                         past_mappings["rows_to_mixers"])
          past_design_3d = mlc.create_expanded_design_for_mixing(*design_args)
  
          # Get the kernel probabilities of each of the alternatives for each
          # each of the previoius choice situations, given the current draws of
          # of the random coefficients
          prob_args = (new_index_coefs,
                       past_design_3d,
                       self.alt_IDs[old_repeat_mixing_id_idx],
                       past_mappings["rows_to_obs"],
                       past_mappings["rows_to_alts"],
                       mnl_utility_transform)
          prob_kwargs = {"return_long_probs": True}
          past_kernel_probs = mlc.general_calc_probabilities(*prob_args,
                                                             **prob_kwargs)
  
          ##########
          # Calculate the old sequence probabilities of all the individual's
          # for whom we have recorded observations and for whom we are predicting
          # future choice situations
          ##########
          past_choices = self.choices[old_repeat_mixing_id_idx]
          sequence_args = (past_kernel_probs,
                           past_choices,
                           past_mappings["rows_to_mixers"])
          seq_kwargs = {"return_type": 'all'}
          old_sequence_results = mlc.calc_choice_sequence_probs(*sequence_args,
                                                                **seq_kwargs)
          # Note sequence_probs_per_draw should have shape
          past_sequence_probs_per_draw = old_sequence_results[1]
          # Calculate the weights for each individual who has repeat observations
          # in the previously observed dataset
          past_weights = (past_sequence_probs_per_draw /
                          past_sequence_probs_per_draw.sum(axis=1)[:, None])
          # Rearrange the past weights to match the current ordering of the
          # unique observations
          rel_new_ids = orig_order_unique_ids_new[new_unique_mix_id_repeats]
          num_rel_new_id = rel_new_ids.shape[0]
          new_unique_mix_id_repeats_2d = rel_new_ids.reshape((num_rel_new_id, 1))
          rel_old_ids = orig_order_unique_ids_old[old_unique_mix_id_repeats]
          num_rel_old_id = rel_old_ids.shape[0]
          old_unique_mix_id_repeats_2d = rel_old_ids.reshape((1, num_rel_old_id))
          new_to_old_repeat_ids = csr_matrix(new_unique_mix_id_repeats_2d ==
                                             old_unique_mix_id_repeats_2d)
          past_weights = new_to_old_repeat_ids.dot(past_weights)
  
          # Map these weights to earlier initialized weights
          weights_per_ind_per_draw[new_unique_mix_id_repeats, :] = past_weights
  
          # Create a 'long' format version of the weights array. This version
          # should have the same number of rows as the new kernel probs but the
          # same number of columns as the weights array (aka the number of draws)
          weights_per_draw = new_rows_to_mixers.dot(weights_per_ind_per_draw)
  
          # Calculate the predicted probabilities of each alternative for each
          # choice situation being predicted
          pred_probs_long = (weights_per_draw * new_kernel_probs).sum(axis=1)
          # Note I am assuming pred_probs_long should be 1D (as should be the
          # case if we are predicting with one set of betas and one 2D data
          # object)
          pred_probs_long = pred_probs_long.ravel()
  
          # Format the returned objects according to the user's desires.
          if new_chosen_to_obs is None:
              chosen_probs = None
          else:
              # chosen_probs will be of shape (num_observations,)
              chosen_probs = new_chosen_to_obs.transpose().dot(pred_probs_long)
              if len(chosen_probs.shape) > 1 and chosen_probs.shape[1] > 1:
                  pass
              else:
                  chosen_probs = chosen_probs.ravel()
  
          # Return the long form and chosen probabilities if desired
          if return_long_probs and chosen_probs is not None:
              return chosen_probs, pred_probs_long
          # If working with predictions, return just the long form probabilities
          elif return_long_probs and chosen_probs is None:
              return pred_probs_long
          # If estimating the model and storing fitted probabilities or
          # testing the model on data for which we know the chosen alternative,
          # just return the chosen probabilities.
          elif chosen_probs is not None:
              return chosen_probs