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

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)