Hi Stavros,
That’s a very nice write-up! I think I can illustrate where things went wrong. It’s a subtle mistake that is easy to make when more than one fuzzy universes share the same length.
The problem is in Step 2. When you combine two membership functions with OR, you must take care. fuzz.fuzzy_or allows different universes to be combined, but this is not always meaningful. It’s written this way mostly to allow two fuzzy membership functions which correspond to the same universe variable (but in slightly different overlapping regions, or at different resolutions) to be combined. When you use it to combine membership functions from the universes “food” and “service”, you lose the ability to input these variables separately.
To solve this problem you need a slightly different approach - which turns out to be a bit simpler. Step 1 is fine; I’ll modify starting at Step 2.
Step 2: Apply fuzzy inputs to membership functions
I recommend the use of fuzz.interp_membership so you can input arbitrary values on the input universe. If you wanted service == 4.36 and food == 7.42, this is possible with fuzz.interp_membership but will generate errors if you simply did foo_r[food == 7.42]. For existing values, the results are equivalent.
# Here I'll use your example service == 2 and food == 4
# These return a single value, which is combined using the rules in Step 3
# and operates on tip membership functions in Step 4
food_1 = fuzz.interp_membership(food, foo_r, 4.)
food_3 = fuzz.interp_membership(food, foo_r, 4.)
service_1 = fuzz.interp_membership(service, ser_p, 2)
service_2 = fuzz.interp_membership(service, ser_g, 2)
service_3 = fuzz.interp_membership(service, ser_e, 2)
Step 3a: Determine the weight for each rule from fuzzy antecedents (first half of “Apply Implication Method” in Matlab tutorial)
# First rule is OR - this is a max operator
rule1 = np.fmax(food_1, service_1) # Doable with inbuilt Python max(), but np.fmax is more general
rule2 = service_2 # No combination, this is just passed
rule3 = np.fmax(food_3, service_3)
Step 3b: Apply implication operator (min or product - these are simple, not inbuilt operators)
# Product is simple multiplication of weight w/fuzzy membership function
# min is np.fmin(weight, membership)
# Here I use product because that appears to be your preference
imp1 = rule1 * tip_ch
imp2 = rule2 * tip_ave
imp3 = rule3 * tip_gen
Step 4: Aggregate all outputs
This simply combines the three results from Step 3b; the actual command is the same as your Step 4 (using nested np.fmax)
aggregate_membership = np.fmax(imp1, np.fmax(imp2, imp3))
Step 5: Defuzzify to determine tip
tip = fuzz.defuzz(tip, aggregate_membership, 'centroid')
# My result: 19.4
The result of 19.4 seems like a somewhat high tip for relatively poor service (2) and food (4), but I think this is because your tip universe variable is operating on the range [0, 101]. If this were scaled to a more reasonable tip range of [0, 25] in units of bill percent, the result would be close to 5% - a low tip for a poor experience!
This may seem slightly more complicated than before, but I hope you can see how it’s entirely possible to automate this programmatically in a function which would accept service and food as inputs and return a tip percentage. Let me know if anything was unclear.
Hope that helps,
Josh