artificial-intelligencerulesrule-engineclips

How to calculate asserts with 2 same parameters in CLIPS?


I am new of CLIPS. I would like to calculate the average of asserts with two same parameters. For example, if i have this template:

(deftemplate quiz
  (slot id (type INTEGER))
  (slot course(type STRING))
  (slot quizname (type STRING))
  (slot userid (type INTEGER))
  (slot firstname (type STRING))
  (slot average (type FLOAT))
  )

with these asserts:

(deffacts users
   (quiz (id 1) (course "Math") (quizname "Equations") (userid 1) (firstname "Mike") (average 70.00))
   (quiz (id 2) (course "Math") (quizname "Exercise") (userid 1) (firstname "Mike") (average 20.00))
   (quiz (id 3) (course "Math") (quizname "Sum") (userid 2) (firstname "Ronald") (average 90.00))
   (quiz (id 4) (course "Math") (quizname "Equations") (userid 2) (firstname "Ronald") (average 60.00))
   (quiz (id 9) (course "English") (quizname "Listening") (userid 5) (firstname "Lionel") (average 60.00))
   )

I'd like to build a rule that calculate averages of assert with same 'userid' and 'course' and write the results on a file. The answer that i'd like to have is:

Mike in math has 45.00; Ronald in math has 75.00; Lionel in english has 60.00

I tried this rule. I know is absolutely wrong:

(defrule averageStudent
=>
(bind ?sum 0)
(do-for-all-facts ((?f quiz)) TRUE
(bind ?sum (+ ?sum ?f:average)))
(bind ?sum (/ ?sum (length$ (find-all-facts ((?nFacts quiz)) (eq ?nFacts:userid) (eq ?nFacts:namequiz math)))))
(printout t ?firstname " in math has " ?sum crlf))

Solution

  • You need to construct something to hold all of the firstname/course combinations that you want to generate averages for. In the actions of the rule you'd have to iterate over the quiz facts and generate one multifield variable containing ("Mike" "Ronald" "Lionel") and another containing ("Math" "Math" "English"). Then you'd have to iterate these lists using the indices from 1 to the length of the lists and then use the nth$ function to pull out the name and the course from each list. Then you could use the fact query functions to pull out the averages for each name/course and compute the total average.

    Rather than do all that, you could write a rule which determines all of the name/course for which an average is needed and assert that as a fact:

    (deftemplate compute
       (slot course (type STRING))
       (slot firstname (type STRING)))
       
    (defrule determine-course-name
       (logical (compute-averages))
       (quiz (course ?course) (firstname ?name))
       (not (compute (course ?course) (firstname ?name)))
       =>  
       (assert (compute (course ?course) (firstname ?name))))
    

    Then you can write a rule which just computes the average for one name/course:

    (defrule compute-average
       (compute (course ?course) (firstname ?name))
       =>
       (bind ?sum 0)
       (bind ?count 0)
       (do-for-all-facts ((?f quiz))
                         (and (eq ?f:course ?course)
                              (eq ?f:firstname ?name))
          (bind ?sum (+ ?sum ?f:average))
          (bind ?count (+ ?count 1)))
       (format t "%s in %s has %0.2f%n" ?name ?course (/ ?sum ?count)))
    

    The determine-course-name rule won't initially be activated:

             CLIPS (6.4 2/9/21)
    CLIPS> 
    (deftemplate quiz
      (slot id (type INTEGER))
      (slot course (type STRING))
      (slot quizname (type STRING))
      (slot userid (type INTEGER))
      (slot firstname (type STRING))
      (slot average (type FLOAT)))
    CLIPS> 
    (deffacts users
       (quiz (id 1) (course "Math") (quizname "Equations") (userid 1) (firstname "Mike") (average 70.00))
       (quiz (id 2) (course "Math") (quizname "Exercise") (userid 1) (firstname "Mike") (average 20.00))
       (quiz (id 3) (course "Math") (quizname "Sum") (userid 2) (firstname "Ronald") (average 90.00))
       (quiz (id 4) (course "Math") (quizname "Equations") (userid 2) (firstname "Ronald") (average 60.00))
       (quiz (id 9) (course "English") (quizname "Listening") (userid 5) (firstname "Lionel") (average 60.00)))
    CLIPS>    
    (deftemplate compute
       (slot course (type STRING))
       (slot firstname (type STRING)))
    CLIPS>    
    (defrule determine-course-name
       (logical (compute-averages))
       (quiz (course ?course) (firstname ?name))
       (not (compute (course ?course) (firstname ?name)))
       =>  
       (assert (compute (course ?course) (firstname ?name))))
    CLIPS>    
    (defrule compute-average
       (compute (course ?course) (firstname ?name))
       =>
       (bind ?sum 0)
       (bind ?count 0)
       (do-for-all-facts ((?f quiz))
                         (and (eq ?f:course ?course)
                              (eq ?f:firstname ?name))
          (bind ?sum (+ ?sum ?f:average))
          (bind ?count (+ ?count 1)))
        
       (format t "%s in %s has %0.2f%n" ?name ?course (/ ?sum ?count)))
    CLIPS> (reset)
    CLIPS> (facts)
    f-1     (quiz (id 1) (course "Math") (quizname "Equations") (userid 1) (firstname "Mike") (average 70.0))
    f-2     (quiz (id 2) (course "Math") (quizname "Exercise") (userid 1) (firstname "Mike") (average 20.0))
    f-3     (quiz (id 3) (course "Math") (quizname "Sum") (userid 2) (firstname "Ronald") (average 90.0))
    f-4     (quiz (id 4) (course "Math") (quizname "Equations") (userid 2) (firstname "Ronald") (average 60.0))
    f-5     (quiz (id 9) (course "English") (quizname "Listening") (userid 5) (firstname "Lionel") (average 60.0))
    For a total of 5 facts.
    CLIPS> (agenda)
    CLIPS> 
    

    Once you assert (compute-averages), the rules can then calculate the averages:

    CLIPS> (assert (compute-averages))
    <Fact-6>
    CLIPS> (agenda)
    0      determine-course-name: f-6,f-5,*
    0      determine-course-name: f-6,f-4,*
    0      determine-course-name: f-6,f-3,*
    0      determine-course-name: f-6,f-2,*
    0      determine-course-name: f-6,f-1,*
    For a total of 5 activations.
    CLIPS> (run)
    Lionel in English has 60.00
    Ronald in Math has 75.00
    Mike in Math has 45.00
    CLIPS>
    

    The intermediate compute facts are still present, but since these are logically dependent on the (compute-averages) fact, you can remove them by retracting that fact if you need them cleaned up:

    CLIPS> (facts)
    f-1     (quiz (id 1) (course "Math") (quizname "Equations") (userid 1) (firstname "Mike") (average 70.0))
    f-2     (quiz (id 2) (course "Math") (quizname "Exercise") (userid 1) (firstname "Mike") (average 20.0))
    f-3     (quiz (id 3) (course "Math") (quizname "Sum") (userid 2) (firstname "Ronald") (average 90.0))
    f-4     (quiz (id 4) (course "Math") (quizname "Equations") (userid 2) (firstname "Ronald") (average 60.0))
    f-5     (quiz (id 9) (course "English") (quizname "Listening") (userid 5) (firstname "Lionel") (average 60.0))
    f-6     (compute-averages)
    f-7     (compute (course "English") (firstname "Lionel"))
    f-8     (compute (course "Math") (firstname "Ronald"))
    f-9     (compute (course "Math") (firstname "Mike"))
    For a total of 9 facts.
    CLIPS> (retract 6)
    CLIPS> (facts)
    f-1     (quiz (id 1) (course "Math") (quizname "Equations") (userid 1) (firstname "Mike") (average 70.0))
    f-2     (quiz (id 2) (course "Math") (quizname "Exercise") (userid 1) (firstname "Mike") (average 20.0))
    f-3     (quiz (id 3) (course "Math") (quizname "Sum") (userid 2) (firstname "Ronald") (average 90.0))
    f-4     (quiz (id 4) (course "Math") (quizname "Equations") (userid 2) (firstname "Ronald") (average 60.0))
    f-5     (quiz (id 9) (course "English") (quizname "Listening") (userid 5) (firstname "Lionel") (average 60.0))
    For a total of 5 facts.
    CLIPS>