Search code examples
matplotlibjupyter-notebookpython-interactivematplotlib-widget

jupyter notebook: update plot interactively - function has constants and keyword arguments


I want to have an interactive plot in jupyter (4.0.6) notebook using matplotlib (1.5.1). The thing is that the static plot is created with a function that has four variables, two of them are constants, two of them are keyword arguments, and I want to interactively change the keyword arguments.

Is this possible, and if yes, how?

The conceptual code below shows the function that generates a plot make_figure(...) and the command to generate an interactive plot.

If I change the keyword arguments to variables, then I get the error message "interact() takes from 0 to 1 positional arguments but 3 were given"

conceptual code:

def make_figure(const_1, const_2, var_1=0.4, var_2=0.8):
    b = calc_b(var_1, var_2)
    c = calc_c(b, const_1, const_2)
    fig, ax = plt.subplots()
    N, bins, patches = ax.hist(c)


interact(make_figure, 
         const_1,
         const_2,
         var_1=(0.2, 0.4, 0.05),
         var_2=(0.75, 0.95, 0.05))

addition 20160325: code example

I am trying to create a histogram for marks for a class, dependent on the percentage necessary to achieve a 1.0 and a 4.0, respectively.

# setup some marks
ids_perc = np.random.random(33) 
print("number of entered marks: ", ids_perc.shape)

the main code for the histogram; main function: get_marks

# define possible marks
marks = np.array([1.0,
                  1.3,
                  1.7,
                  2.0,
                  2.3,
                  2.7,
                  3.0,
                  3.3,
                  3.7,
                  4.0,
                  5.0])
marks_possible = marks[::-1]

def get_perc_necessary(min_perc_one,
                       min_perc_four,
                       n_marks):
    """
    calculates an equally spaced array for percentage necessary to get a mark
    """
    delta = (min_perc_one - min_perc_four)/(n_marks-2-1)
    perc_necessary_raw = np.linspace(start=min_perc_four, 
                                     stop=min_perc_one, 
                                     num=n_marks-1)
    perc_necessary = np.append([0.0], np.round(perc_necessary_raw, decimals=2)) 
    return perc_necessary


def assign_marks(n_students,
                 perc_necessary,
                 achieved_perc,
                 marks_real):
    """
    get the mark for each student (with a certain achieved percentage)
    """
    final_marks = np.empty(n_students)

    for cur_i in range(n_students):
        idx = np.argmax(np.argwhere(perc_necessary <= achieved_perc[cur_i]))
        final_marks[cur_i] = marks_real[idx]

    return final_marks


def get_marks(achieved_perc = ids_perc,
              marks_real = marks_possible,                    
              min_perc_four = 0.15,
              min_perc_one = 0.85):

    n_marks = marks.shape[0]
#     print("n_marks: ", n_marks)
    n_students = achieved_perc.shape[0]
#     print("n_students: ", n_students)

    # -----------------------------
    # linear step between each mark
    perc_necessary = get_perc_necessary(min_perc_one,
                                        min_perc_four,
                                        n_marks)

    # test query: there need to be as many percentages as marks
    if perc_necessary.shape[0] != marks_real.shape[0]:
        print("the number of marks has to be equal the number of boundaries")
        raise Exception

    # ------------
    # assign marks 
    final_marks = assign_marks(n_students,
                               perc_necessary,
                               achieved_perc,
                               marks_real)    

    # ------------
    # create table
    fig, ax = plt.subplots()
    N, bins, patches = ax.hist(final_marks, 
                               align='mid', 
                               bins=np.append(marks,6.)) # bins=marks
    ax.xaxis.set_major_formatter(FormatStrFormatter('%0.1f'))
    bin_centers = 0.5 * np.diff(bins) + bins[:-1]
    ax.set_xticks(bin_centers)
    ax.set_xticklabels( marks )
    ax.set_xlabel("mark")
    ax.set_ylabel("number of marks")
    ax.set_ylim(0.0, 6.0)
    plt.grid(True)

Now, when I try to setup interact doing this

interact(get_marks, 
     min_perc_four=(0.2, 0.4, 0.05),
     min_perc_one=(0.75, 0.95, 0.05));

I get the error

ValueError: array([ 0.22366653,  0.74206953,  0.47501716,  0.56536227,  0.54792759,
    0.60288287,  0.68548973,  0.576935  ,  0.84582243,  0.40709693,
    0.78600622,  0.2692508 ,  0.62524819,  0.62204851,  0.5421716 ,
    0.71836192,  0.97194698,  0.4054752 ,  0.2185643 ,  0.11786751,
    0.57947848,  0.88659768,  0.38803576,  0.66617254,  0.77663263,
    0.94364543,  0.23021637,  0.30899724,  0.08695842,  0.50296694,
    0.8164095 ,  0.77892531,  0.5542163 ]) cannot be transformed to a Widget

Why is this error looking at the variable ids_perc?


Solution

  • You need to assign your variables explicitly in interact(). For example, like this:

    const_1 = 1
    
    interact(make_figure, 
             const_1=const_1,
             const_2=2,
             var_1=(0.2, 0.4, 0.05),
             var_2=(0.75, 0.95, 0.05))
    

    Or (if possible) change the signature of make_figure to make those variables into keyword arguments, so that you can avoid passing them explicitly:

    def make_figure(const_1=1, const_2=2, var_1=0.4, var_2=0.8):
        ....
    
    interact(make_figure, 
             var_1=(0.2, 0.4, 0.05),
             var_2=(0.75, 0.95, 0.05))
    

    Here is MCWE that you can try:

    def calc_b(v1, v2):
        return v1 + v2
    
    def calc_c(v1, v2, v3):
        return [v1, v2, v3]
    
    def make_figure(const_1=1, const_2=2, var_1=0.4, var_2=0.8):
        b = calc_b(var_1, var_2)
        c = calc_c(b, const_1, const_2)
        fig, ax = plt.subplots()
        N, bins, patches = ax.hist(c)
    
    interact(make_figure, 
             var_1=(0.2, 0.4, 0.05),
             var_2=(0.75, 0.95, 0.05));
    

    This runs without any errors.

    On your addition 20160325:

    Every parameter you pass to interact will have to be representable by either one of (simplifying it somewhat):

    • a slider (for tuples, that represent (min, max) and scalar numbers)
    • selection box (for lists of strings and dictionaries)
    • check box (for booleans)
    • an input box (for strings)

    You are passing (implicitly by defining in your get_marks two parameters as np.arrays). So interact doesn't know how to represent that on a slider, hense the error.

    You have at least two options:

    1) to change the signature of the get_marks so that it takes parameters that interact will undetstand (see bullet list above)

    2) make another wrapper function that will take parameters that interact undetstands, but will call get_marks after converting those parameters to whatever get_marks needs.

    So just one extra step and you're done. ;-)

    UPDATE:

    Here is your code with wrapper that works for me. Note that get_marks_interact does not need to take all the params of get_marks and I don't pass lists as interact will have a problem with them (list should represent either a list of strings (for Dropdown Widgets) or list/tuple of [min, max] values (for slider)).

    def get_marks(min_perc_four = 0.15,
                  min_perc_one = 0.85,
                  marks=marks_possible,
                  ach_per=ids_perc):
    
        marks_real = marks #  [0]
        achieved_perc = ach_per #  [0]
    
        n_marks = marks_real.shape[0]
        print("n_marks: ", n_marks)
        n_students = achieved_perc.shape[0]
        print("n_students: ", n_students)
    
        # -----------------------------
        # linear step between each mark
        perc_necessary = get_perc_necessary(min_perc_one,
                                            min_perc_four,
                                            n_marks)
    
        # test query: there need to be as many percentages as marks
        if perc_necessary.shape[0] != marks_real.shape[0]:
            print("the number of marks has to be equal the number of boundaries")
            raise Exception
    
        # ------------
        # assign marks 
        final_marks = assign_marks(n_students,
                                   perc_necessary,
                                   achieved_perc,
                                   marks_real)
    
        # ------------
        # create table
        fig, ax = plt.subplots()
        N, bins, patches = ax.hist(final_marks, 
                                   align='mid', 
                                   bins=np.sort(np.append(marks, 6.))) # bins=marks
        ax.xaxis.set_major_formatter(FormatStrFormatter('%0.1f'))
        bin_centers = 0.5 * np.diff(bins) + bins[:-1]
        ax.set_xticks(bin_centers)
        ax.set_xticklabels( marks )
        ax.set_xlabel("mark")
        ax.set_ylabel("number of marks")
        ax.set_ylim(0.0, 6.0)
        plt.grid(True)
    
    def get_marks_interact(min_perc_four = 0.15, 
                           min_perc_one = 0.85,):
        return get_marks(min_perc_four, min_perc_one)
    
    interact(get_marks_wrapper, 
             min_perc_four=(0.2, 0.4, 0.05),
             min_perc_one=(0.75, 0.95, 0.05));