docs: def map(func, data, num_workers=None):
      # type: (callable, Iterable, Optional[int]) -> Iterable
      """ Map an iterable using multithreading
      >>> s = pd.Series(range(120, 0, -1))
      >>> s2 = map(lambda i, x: x ** 3.75, s)
      >>> isinstance(s2, type(s))
      True
      >>> len(s) == len(s2)
      True
      >>> (s2 == s.map(lambda x: x ** 3.75)).all()
      True
      >>> s = list(range(120, 0, -1))
      >>> s2 = map(lambda i, x: x ** 3.75, s)
      >>> isinstance(s2, type(s))
      True
      >>> len(s) == len(s2)
      True
      >>> all(x ** 3.75 == s2[i] for i, x in enumerate(s))
      True
      >>> s = dict(enumerate(range(120, 0, -1)))
      >>> s2 = map(lambda i, x: x ** 3.75, s)
      >>> isinstance(s2, type(s))
      True
      >>> len(s) == len(s2)
      True
      >>> all(x ** 3.75 == s2[i] for i, x in s.items())
      True
      """
      backend = ThreadPool(n_workers=num_workers)
      iterable = None
      # pd.Series didn't have .items() until pandas 0.21,
      # so iteritems for older versions
      for method in ('iterrows', 'iteritems', 'items'):
          if hasattr(data, method):
              iterable = getattr(data, method)()
              break
      if iterable is None:
          iterable = enumerate(data)
  
      mapped = {}
  
      def collect(key):
          def process(res):
              mapped[key] = res
          return process
  
      for key, value in iterable:
          backend.submit(func, key, value, callback=collect(key))
  
      backend.shutdown()
  
      if isinstance(data, pd.DataFrame):
          return pd.DataFrame.from_dict(
              mapped, orient='index').reindex(data.index)
      elif isinstance(data, pd.Series):
          return pd.Series(mapped).reindex(data.index)
      elif isinstance(data, list):
          return [mapped[i] for i in range(len(data))]
      else:
          # in Python, hash(<int>) := <int>, so guaranteed to be in order for list
          # and tuple. For other types
          return type(data)(mapped)