What would be the best use-cases for the map(), apply() and applymap() functions in python and can you submit an example piece of code that shows the differences in execution time between the different functions along with a short explanation of what causes them ?
`map()` is a core python function while `apply()` and `applymap()` are both pandas functions. 
* `map()` applies a function to each element in a list one by one, and can be used for simple operations like squaring every element, but it can also take class methods if operations require memory.
* `apply()` applies a function to every row or column in a pandas dataframe, and can be more useful for operations that require working on entire rows or columns.
* `applymap()` is much closer to the python map function, but specialized to work with pandas dataframes. It's better when you want to run a map function, but you have a pandas dataframe and not a list. Note that his operates on every element, so a 2d matrix would still send elements to the operation, while map would run on sub-lists in a nested list.

Here's some python code that returns the times for operation on a large list for all the different functions:
```python
import time
import pandas as pd
import random


class MathClass(object):
    def __init__(self):
        self.running_max = 0

    def running_max_percent(self, n):
        percent = 100 * n / self.running_max if self.running_max else float("inf")
        if abs(n) > self.running_max:
            self.running_max = abs(n)
        return percent


numbers = [i for i in range(100000)]
random.shuffle(numbers)
df = pd.DataFrame(numbers)
df2 = pd.DataFrame(numbers)


def get_map_time():
    """
    >>> get_map_time()
    'time for map: 0.02s'
    
    Since this operation goes through almost no converstions to numpy, pandas, c++, etc., it's the simplest and 
    therefore the fastest.
    """
    example_math = MathClass()
    t0 = time.time()
    result = map(example_math.running_max_percent, numbers)
    _ = list(result)
    t1 = time.time()

    return f"time for map: {t1 - t0:.2f}s"


def get_apply_time():
    """
    >>> get_apply_time()
    'time for apply: 0.43s'
    
    This operation was made to work on entire rows or columns, so individually picking out one element out of a row 
    every time slows things down a bit. However, since raw=true sends numpy arrays, this might actually be faster for 
    some operations that work on entire rows or columns at once.
    """
    global df
    example_math = MathClass()
    t0 = time.time()
    df = df.apply(example_math.running_max_percent, axis=1, raw=True)
    t1 = time.time()
    example_math.running_max = 0

    return f"time for apply: {t1 - t0:.2f}s"


def get_applymap_time():
    """
    >>> get_applymap_time()
    'time for applymap: 0.03s'
    
    This operation was made to work more like map, so it's closer to the same time, but it's still less efficient than 
    just sending the numbers to the operations because you're converting a number from python, to numpy or pandas, to a 
    python number, then back to pandas.
    """
    global df2
    example_math = MathClass()
    t0 = time.time()
    df2 = df2.applymap(example_math.running_max_percent)
    t1 = time.time()

    return f"time for applymap: {t1 - t0:.2f}s"

```