Count number of registers in interval & location
Consider not expanding a dataframe inside the for loop but build a list or dictionary and then concatenate all dataframe elements outside loop. 

Expanding objects within a loop causes substantial memory resources to allocate before and after blocks with lots of copying of objects to do so. Running one call on outside should substantially run faster as virtually no copying is done.

Specifically change:

output = pd.DataFrame()


To a list:

output = []


And then append to list inside loop and then pd.concat(list) outside loop.

def time_features(df, time_key, T, location_key, output_key):
    """
    Create features based on time such as: how many BDs are open in the same GRA at this moment (hour)?
    """ 
    from datetime import date
    assert np.issubdtype(df[time_key], np.datetime64)
    output = []

    grouped = df.groupby(location_key)
    for name, group in grouped:
        # initialize times registers open as 1, close as -1
        start_times = group.copy()
        start_times[time_key] = group[time_key]-pd.Timedelta(hours=T)
        start_times[output_key] =  1

        aux = group.copy()
        all_times = start_times.copy()
        aux[output_key] = -1  
        all_times = all_times.append(aux, ignore_index=True)

        # sort by time and perform a cumulative sum to get opened registers
        # (subtract 1 since you don't want to include the current time as opened)
        all_times = all_times.sort_values(by=time_key)
        all_times[output_key] = all_times[output_key].cumsum() - 1

        # revert the index back to original order, and truncate closed times
        all_times = all_times.sort_index().iloc[:len(all_times)//2]
        # APPEND TO LIST
        output.append(all_times)

    # CONCATENATE ALL DF ELEMENTS
    final_df = pd.concat(output, ignore_index=True)

    return final_df