Step 1 creates a function that doesn't accept any extra arguments. The upper and lower bounds must be hardcoded into the function itself, which isn't very flexible. Step 2 shows the results of this aggregation.
We create a more flexible function in step 3 that allows users to define both the lower and upper bounds dynamically. Step 4 is where the magic of *args and **kwargs come into play. In this particular example, we pass two non-keyword arguments, 1,000 and 10,000, to the agg method. Pandas passes these two arguments respectively to the low and high parameters of pct_between.
There are a few ways we could achieve the same result in step 4. We could have explicitly used the parameter names with the following command to produce the same result:
>>> college.groupby(['STABBR', 'RELAFFIL'])['UGDS']
.agg(pct_between, high=10000, low=1000).head(9)
The order of the keyword arguments doesn't matter as long as they come after the function name. Further still, we can mix non-keyword and keyword arguments as long as the keyword arguments come last:
>>> college.groupby(['STABBR', 'RELAFFIL'])['UGDS']
.agg(pct_between, 1000, high=10000).head(9)
For ease of understanding, it's probably best to include all the parameter names in the order that they are defined in the function signature.