docs: def convert_long_to_wide(long_data,
                           ind_vars,
                           alt_specific_vars,
                           subset_specific_vars,
                           obs_id_col,
                           alt_id_col,
                           choice_col,
                           alt_name_dict=None,
                           null_value=np.nan):
      """
      Converts a 'long format' dataframe of cross-sectional discrete choice data
      into a 'wide format' version of the same data.
  
      Parameters
      ----------
      long_data : pandas dataframe.
          Contains one row for each available alternative for each observation.
          Should have the specified `[obs_id_col, alt_id_col, choice_col]` column
          headings. The dtypes of all columns should be numeric.
      ind_vars : list of strings.
          Each element should be a column heading in `long_data` that denotes a
          variable that varies across observations but not across alternatives.
      alt_specific_vars : list of strings.
          Each element should be a column heading in `long_data` that denotes a
          variable that varies not only across observations but also across all
          alternatives.
      subset_specific_vars : dict.
          Each key should be a string that is a column heading of `long_data`.
          Each value should be a list of alternative ids denoting the subset of
          alternatives which the variable (i.e. the key) over actually varies.
          These variables should vary across individuals and across some
          alternatives.
      obs_id_col : str.
          Denotes the column in `long_data` that contains the observation ID
          values for each row.
      alt_id_col : str.
          Denotes the column in `long_data` that contains the alternative ID
          values for each row.
      choice_col : str.
          Denotes the column in `long_data` that 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.
      alt_name_dict : dict or None, optional
          If not None, should be a dictionary whose keys are the possible values
          in `long_data[alt_id_col].unique()`. The values should be the name
          that one wants to associate with each alternative id. Default == None.
      null_value : int, float, long, or `np.nan`, optional.
          The passed value will be used to fill cells in the wide format
          dataframe when that cell is unknown for a given individual. This is
          most commonly the case when there is a variable that varies across
          alternatives and one of the alternatives is not available for a given
          indvidual. The `null_value` will be inserted for that individual for
          that variable. Default == `np.nan`.
  
      Returns
      -------
      final_wide_df : pandas dataframe.
          Will contain one row per observational unit. Will contain an
          observation id column of the same name as `obs_id_col`. Will also
          contain a choice column of the same name as `choice_col`. Will contain
          one availability column per unique, observed alternative in the
          dataset. Will contain one column per variable in `ind_vars`. Will
          contain one column per alternative per variable in `alt_specific_vars`.
          Will contain one column per specified alternative per variable in
          `subset_specific_vars`.
      """
      ##########
      # Check that all columns of long_data are being
      # used in the conversion to wide format
      ##########
      num_vars_accounted_for = sum([len(x) for x in
                                    [ind_vars, alt_specific_vars,
                                     subset_specific_vars,
                                     [obs_id_col, alt_id_col, choice_col]]])
  
      ensure_all_columns_are_used(num_vars_accounted_for, long_data)
  
      ##########
      # Check that all columns one wishes to use are actually in long_data
      ##########
      ensure_columns_are_in_dataframe(ind_vars,
                                      long_data,
                                      col_title="ind_vars",
                                      data_title='long_data')
  
      ensure_columns_are_in_dataframe(alt_specific_vars,
                                      long_data,
                                      col_title="alt_specific_vars",
                                      data_title='long_data')
  
      ensure_columns_are_in_dataframe(subset_specific_vars.keys(),
                                      long_data,
                                      col_title="subset_specific_vars",
                                      data_title='long_data')
  
      identifying_cols = [choice_col, obs_id_col, alt_id_col]
      identifying_col_string = "[choice_col, obs_id_col, alt_id_col]"
      ensure_columns_are_in_dataframe(identifying_cols,
                                      long_data,
                                      col_title=identifying_col_string,
                                      data_title='long_data')
  
      ##########
      # Make sure that each observation-alternative pair is unique
      ##########
      check_dataframe_for_duplicate_records(obs_id_col, alt_id_col, long_data)
  
      ##########
      # Make sure each observation chose an alternative that's available.
      ##########
      # Make sure that the number of chosen alternatives equals the number of
      # individuals.
      ensure_num_chosen_alts_equals_num_obs(obs_id_col, choice_col, long_data)
  
      ##########
      # Check that the alternative ids in the alt_name_dict are actually the
      # alternative ids used in the long_data alt_id column.
      ##########
      if alt_name_dict is not None:
          check_type_and_values_of_alt_name_dict(alt_name_dict,
                                                 alt_id_col,
                                                 long_data)
  
      ##########
      # Figure out how many rows/columns should be in the wide format dataframe
      ##########
      # Note that the number of rows in wide format is the number of observations
      num_obs = long_data[obs_id_col].unique().shape[0]
  
      # Figure out the total number of possible alternatives for the dataset
      num_alts = long_data[alt_id_col].unique().shape[0]
  
      ############
      # Calculate the needed number of colums
      ############
      # For each observation, there is at least one column-- the observation id,
      num_cols = 1
      # We should have one availability column per alternative in the dataset
      num_cols += num_alts
      # We should also have one column to record the choice of each observation
      num_cols += 1
      # We should also have one column for each individual specific variable
      num_cols += len(ind_vars)
      # We should also have one column for each alternative specific variable,
      # for each alternative
      num_cols += len(alt_specific_vars) * num_alts
      # We should have one column for each subset alternative specific variable
      # for each alternative over which the variable varies
      for col in subset_specific_vars:
          num_cols += len(subset_specific_vars[col])
  
      ##########
      # Create the columns of the new dataframe
      ##########
      #####
      # Create the individual specific variable columns,
      # along with the observation id column
      #####
      new_df = long_data[[obs_id_col] + ind_vars].drop_duplicates()
  
      # Reset the index so that the index is not based on long_data
      new_df.reset_index(inplace=True)
  
      #####
      # Create the choice column in the wide data format
      #####
      new_df[choice_col] = long_data.loc[long_data[choice_col] == 1,
                                         alt_id_col].values
  
      #####
      # Create the availability columns
      #####
      # Get the various long form mapping matrices
      mapping_res = create_long_form_mappings(long_data,
                                              obs_id_col,
                                              alt_id_col)
      row_to_obs = mapping_res["rows_to_obs"]
      row_to_alt = mapping_res["rows_to_alts"]
  
      # Get the matrix of observations (rows) to available alternatives (columns)
      obs_to_alt = row_to_obs.T.dot(row_to_alt).todense()
  
      # Determine the unique alternative IDs in the order used in obs_to_alt
      alt_id_values = long_data[alt_id_col].values
      all_alternatives = np.sort(np.unique(alt_id_values))
  
      # Create the names for the availability columns
      if alt_name_dict is None:
          availability_col_names = ["availability_{}".format(int(x))
                                    for x in all_alternatives]
      else:
          availability_col_names = ["availability_{}".format(alt_name_dict[x])
                                    for x in all_alternatives]
  
      # Create a dataframe containing the availability columns for this dataset
      availability_df = pd.DataFrame(obs_to_alt,
                                     columns=availability_col_names)
  
      #####
      # Create the alternative specific and subset
      # alternative specific variable columns
      #####
      # For each alternative specific variable, create a wide format dataframe
      alt_specific_dfs = []
      for col in alt_specific_vars + list(subset_specific_vars.keys()):
          # Get the relevant values from the long format dataframe
          relevant_vals = long_data[col].values[:, None]
          # Create an wide format array of the relevant values
          obs_to_var = row_to_obs.T.dot(row_to_alt.multiply(relevant_vals))
          # Ensure that the wide format array is an ndarray with of dtype float
          if issparse(obs_to_var):
              obs_to_var = obs_to_var.toarray()
          # Ensure that obs_to_var has a float dtype
          obs_to_var = obs_to_var.astype(float)
  
          # Place a null value in columns where the alternative is not available
          # to a given observation
          if (obs_to_alt == 0).any():
              obs_to_var[np.where(obs_to_alt == 0)] = null_value
  
          # Create column names for the alternative specific variable columns
          if alt_name_dict is None:
              obs_to_var_names = ["{}_{}".format(col, int(x))
                                  for x in all_alternatives]
          else:
              obs_to_var_names = ["{}_{}".format(col, alt_name_dict[x])
                                  for x in all_alternatives]
  
          # Subset obs_to_vars and obs_to_var_names if col is in
          # subset_specific_vars
          if col in subset_specific_vars:
              # Calculate the relevant column indices for
              # the specified subset of alternatives
              relevant_alt_ids = subset_specific_vars[col]
              relevant_col_idx = np.where(np.in1d(all_alternatives,
                                                  relevant_alt_ids))[0]
          else:
              relevant_col_idx = None
  
          # Create a dataframe containing the alternative specific variables
          # or the subset alternative specific variables for the given
          # variable in the long format dataframe
          if relevant_col_idx is None:
              obs_to_var_df = pd.DataFrame(obs_to_var,
                                           columns=obs_to_var_names)
          else:
              obs_to_var_df = pd.DataFrame(obs_to_var[:, relevant_col_idx],
                                           columns=[obs_to_var_names[x] for
                                                    x in relevant_col_idx])
  
          # Store the current alternative specific variable columns/dataframe
          alt_specific_dfs.append(obs_to_var_df)
  
      # Combine all of the various alternative specific variable dataframes
      final_alt_specific_df = pd.concat(alt_specific_dfs, axis=1)
  
      ##########
      # Construct the final wide format dataframe to be returned
      ##########
      final_wide_df = pd.concat([new_df[[obs_id_col]],
                                 new_df[[choice_col]],
                                 availability_df,
                                 new_df[ind_vars],
                                 final_alt_specific_df],
                                axis=1)
  
      # Make sure one has the correct number of rows and columns in
      # the final dataframe
      if final_wide_df.shape != (num_obs, num_cols):
          msg_1 = "There is an error with the dataframe that will be returned"
          msg_2 = "The shape of the dataframe should be {}".format((num_obs,
                                                                    num_cols))
          msg_3 = "Instead, the returned dataframe will have shape: {}"
          total_msg = msg_1 + msg_2 + msg_3.format(final_wide_df.shape)
          warnings.warn(total_msg)
  
      # Return the wide format dataframe
      return final_wide_df