docs: def calculate_frequencies(data, binning, weights=None, validate_bins=True,
                            already_sorted=False, dtype=None):
      """Get frequencies and bin errors from the data.
  
      Parameters
      ----------
      data : array_like
          Data items to work on.
      binning : physt.binnings.BinningBase
          A set of bins.
      weights : array_like, optional
          Weights of the items.
      validate_bins : bool, optional
          If True (default), bins are validated to be in ascending order.
      already_sorted : bool, optional
          If True, the data being entered are already sorted, no need to sort them once more.
      dtype: Optional[type]
          Underlying type for the histogram.
          (If weights are specified, default is float. Otherwise long.)
  
      Returns
      -------
      frequencies : numpy.ndarray
          Bin contents
      errors2 : numpy.ndarray
          Error squares of the bins
      underflow : float
          Weight of items smaller than the first bin
      overflow : float
          Weight of items larger than the last bin
      stats: dict
          { sum: ..., sum2: ...}
  
      Note
      ----
      Checks that the bins are in a correct order (not necessarily consecutive).
      Does not check for numerical overflows in bins.
      """
  
      # TODO: Is it possible to merge with histogram_nd.calculate_frequencies?
      # TODO: What if data is None
      # TODO: Change stats into namedtuple
  
      # Statistics
      sum = 0.0
      sum2 = 0.0
  
      # Ensure correct binning
      bins = binning.bins  # bin_utils.make_bin_array(bins)
      if validate_bins:
          if bins.shape[0] == 0:
              raise RuntimeError("Cannot have histogram with 0 bins.")
          if not bin_utils.is_rising(bins):
              raise RuntimeError("Bins must be rising.")
  
      # Prepare 1D numpy array of data
      data = np.asarray(data)
      if data.ndim > 1:
          data = data.flatten()
  
      # Prepare 1D numpy array of weights
      if weights is not None:
          weights = np.asarray(weights)
          if weights.ndim > 1:
              weights = weights.flatten()
  
          # Check compatibility of weights
          if weights.shape != data.shape:
              raise RuntimeError("Weights must have the same shape as data.")
  
          # Ensure proper dtype for the bin contents
          if dtype is None:
              dtype = weights.dtype
  
      if dtype is None:
          dtype = int
      dtype = np.dtype(dtype)
      if dtype.kind in "iu" and weights is not None and weights.dtype.kind == "f":
          raise RuntimeError("Integer histogram requested but float weights entered.")
  
      # Data sorting
      if not already_sorted:
          args = np.argsort(data)     # Memory: another copy
          data = data[args]           # Memory: another copy
          if weights is not None:
              weights = weights[args]
          del args
  
      # Fill frequencies and errors
      frequencies = np.zeros(bins.shape[0], dtype=dtype)
      errors2 = np.zeros(bins.shape[0], dtype=dtype)
      for xbin, bin in enumerate(bins):
          start = np.searchsorted(data, bin[0], side="left")
          stop = np.searchsorted(data, bin[1], side="left")
  
          if xbin == 0:
              if weights is not None:
                  underflow = weights[0:start].sum()
              else:
                  underflow = start
          if xbin == len(bins) - 1:
              stop = np.searchsorted(data, bin[1], side="right")   # TODO: Understand and explain
              if weights is not None:
                  overflow = weights[stop:].sum()
              else:
                  overflow = data.shape[0] - stop
  
          if weights is not None:
              frequencies[xbin] = weights[start:stop].sum()
              errors2[xbin] = (weights[start:stop] ** 2).sum()
              sum += (data[start:stop] * weights[start:stop]).sum()
              sum2 += ((data[start:stop]) ** 2 * weights[start:stop]).sum()
          else:
              frequencies[xbin] = stop - start
              errors2[xbin] = stop - start
              sum += data[start:stop].sum()
              sum2 += (data[start:stop] ** 2).sum()
  
      # Underflow and overflow don't make sense for unconsecutive binning.
      if not bin_utils.is_consecutive(bins):
          underflow = np.nan
          overflow = np.nan
  
      stats = {"sum": sum, "sum2": sum2}
  
      return frequencies, errors2, underflow, overflow, stats