Functions as a switch statement, creating a new series out of logical
    conditions specified by 2-item lists where the left-hand item is the
    logical condition and the right-hand item is the value where that
    condition is true.

    Conditions should go from the most specific to the most general. A
    conditional that appears earlier in the series will "overwrite" one that
    appears later. Think of it like a series of if-else statements.

    The logicals and values of the condition pairs must be all the same
    length, or length 1. Logicals can be vectors of booleans or a single
    boolean (`True`, for example, can be the logical statement for the
    final conditional to catch all remaining.).

    Args:
        *conditions: Each condition should be a list with two values. The first
            value is a boolean or vector of booleans that specify indices in
            which the condition is met. The second value is a vector of values
            or single value specifying the outcome where that condition is met.

    Example:
        df = pd.DataFrame({
            'num':np.arange(16)
        })
        df >> mutate(strnum=case_when([X.num % 15 == 0, 'fizzbuzz'],
                                      [X.num % 3 == 0, 'fizz'],
                                      [X.num % 5 == 0, 'buzz'],
                                      [True, X.num.astype(str)]))

            num    strnum
        0     0  fizzbuzz
        1     1         1
        2     2         2
        3     3      fizz
        4     4         4
        5     5      buzz
        6     6      fizz
        7     7         7
        8     8         8
        9     9      fizz
        10   10      buzz
        11   11        11
        12   12      fizz
        13   13        13
        14   14        14
        15   15  fizzbuzz

def case_when(*conditions):
    """
    Functions as a switch statement, creating a new series out of logical
    conditions specified by 2-item lists where the left-hand item is the
    logical condition and the right-hand item is the value where that
    condition is true.

    Conditions should go from the most specific to the most general. A
    conditional that appears earlier in the series will "overwrite" one that
    appears later. Think of it like a series of if-else statements.

    The logicals and values of the condition pairs must be all the same
    length, or length 1. Logicals can be vectors of booleans or a single
    boolean (`True`, for example, can be the logical statement for the
    final conditional to catch all remaining.).

    Args:
        *conditions: Each condition should be a list with two values. The first
            value is a boolean or vector of booleans that specify indices in
            which the condition is met. The second value is a vector of values
            or single value specifying the outcome where that condition is met.

    Example:
        df = pd.DataFrame({
            'num':np.arange(16)
        })
        df >> mutate(strnum=case_when([X.num % 15 == 0, 'fizzbuzz'],
                                      [X.num % 3 == 0, 'fizz'],
                                      [X.num % 5 == 0, 'buzz'],
                                      [True, X.num.astype(str)]))

            num    strnum
        0     0  fizzbuzz
        1     1         1
        2     2         2
        3     3      fizz
        4     4         4
        5     5      buzz
        6     6      fizz
        7     7         7
        8     8         8
        9     9      fizz
        10   10      buzz
        11   11        11
        12   12      fizz
        13   13        13
        14   14        14
        15   15  fizzbuzz
    """

    lengths = []
    for logical, outcome in conditions:
        if isinstance(logical, collections.Iterable):
            lengths.append(len(logical))
        if isinstance(outcome, collections.Iterable) and not isinstance(outcome, str):
            lengths.append(len(outcome))
    unique_lengths = np.unique(lengths)
    assert len(unique_lengths) == 1
    output_len = unique_lengths[0]

    output = []
    for logical, outcome in conditions:
        if isinstance(logical, bool):
            logical = np.repeat(logical, output_len)
        if isinstance(logical, pd.Series):
            logical = logical.values
        if not isinstance(outcome, collections.Iterable) or isinstance(outcome, str):
            outcome = pd.Series(np.repeat(outcome, output_len))
        outcome[~logical] = np.nan
        output.append(outcome)

    return coalesce(*output)