Discussion Forum

Is this scenario appropriate to model with typeDB

I’m newbie with typeDB, I don’t know if the following scenario is suitable for modeling with typeDB:
The students have examination results for 10 courses, the corresponding mark for each course are as follows:
[ 90 -100 ] → A
[ 80 - 89 ] → B
[ 60 - 79 ] → C
else [ 0 - 59] → D

if a student get 6A+ and no D, then he or she is considered outstanding"
else if 4A+ and no D, then “good”
else if no D, then “passed”
else “failed”

So those are the rules for scoring and classifying, and my input data is the scores of a student (entity) in each course (percentage scale) , the output is a label of classification, how do I model and process this case in typeDB?
Further, is it feasible if I want to save the classification rules as a scheme that I can modify and manage?

Thank you for any suggestion and help.

Kind regards,
Joylix

I rewrite the case so that it could be read more clearly:

Students have test scores for 10 courses, the corresponding mark for each course are as follows:

[ 90-100 ] --> A
[ 76 - 89 ] --> B
[ 60 - 75 ] --> C
[ 0   - 59 ] --> D

if a student get more than 6A and no D, then he or she is considered "Outstanding"
else if 4A+ and no D, then "Good"
else if no D, then "Passed"
else "Failed"

So those are the relevant rules, now a student named “Bob” has Scores like follows:
Bob_test0.score = 90 #–>A
Bob_test1.score = 91 #–>A
Bob_test2.score = 92 #–>A
Bob_test3.score = 93 #–>A
Bob_test4.score = 94 #–>A
Bob_test5.score = 95 #–>A
Bob_test6.score = 96 #–>A
Bob_test7.score = 87 #–>B
Bob_test8.score = 78 #–>B
Bob_test8.score = 69 #–>C

So Bob get 7A, and no D, a query or stored procedure should get the result of :
Bob.rating = “Outstanding”

I don’t know if I can write and store some business rules (for grade and rating in the example above) in typeDB, specific data (such as Bob’s scores) is then processed according to rules through typeDB’s general stored procedures or query statements.
This user functional requirement is important and typical for many business scenarios and domains.
Thanks to the experts at Vaticle for providing professional guidance!

Best regards,
Joylix

I’v got it done by Cypher.
The initial data looks like this:
student {“name”:“Bob”,“rating”:“null”}
Test {“name”:“Test0”,“score”:90,“grade”:“null”}

1

First, the corresponding test grade is obtained according to the rules:
###---------------------set each test.grade
MATCH (student:Student)-[:tookTest]->(test:Test)
set test.grade=
case
when 90<= test.score <=100 then “A”
when 75<= test.score < 90 then “B”
when 60<= test.score < 75 then “C”
else “D”
end

Then, get student’s rating according to the total grades:

###----------------------set student.rating
MATCH (s:Student)
CALL {
 WITH s
 MATCH (s:Student)-[:tookTest]->(test:Test)
 return apoc.coll.occurrences(collect(test.grade), 'A') as Anum,
        apoc.coll.occurrences(collect(test.grade), 'D') as Dnum
}
set s.rating=
	case 
	  when  0<Dnum then "Failed"
                  when  6<Anum and Dnum =0 then "Outstanding"
                  when  4<Anum<=6 and Dnum =0 then "Good"
                  else "Passed"
                end
Return s.name,  s.rating

and I got result like this:

s.name           s.rating 
Bob              Outstanding

I don’t know if there’s an easier way to do that in typeDB. Also, how to allow users to store rules in graphs and run them when needed, such as:

if    condition1  then 
      action1
else if   condition2 then
      action2
else
      defaultAction

Here the conditions are the logical relationships of Node,relationship, and attributes. the action is usually a set of Property assignment operation. This will be very useful.

First of all, your domain is super fun to model! You’ll see below why!

Unfortunately, I think until we have subqueries or aggregations in TypeQL, we cannot actually implement what you’re looking for, mostly because of the “6A+” and “4A+” pieces. I’ll illustrate the bits we can do first.

the classification of grades is easy (and can be modeled in multiple interesting ways):

when {
  $s isa student; 
  $course (attendee: $s, class: $c) isa course-attendance, has exam-result $m;
  $m > 90; $m < 100;
  $A "A" isa grade;
} then {
  (attendee: $s, course: $course, grade: $A) isa course-outcome;
}

so here we’re checking the exam-result outcome attached to a course relation. We also look up some pre-inserted grade instance (you need to pre-create A, B…), and create a course outcome relation that points at the course and the grade attribute! (so we’re using an attribute on a relation in the when, and creating a nested relation that also has an attribute role player :slight_smile:

You’d create a couple variants of these to create your grading rules.

The step we’re currently unable to serve is the count of how many "A"s a student got. eg. we can’t do this:
[making up some syntax]

set a-count $as as match $s isa student; (attendee: $x, grade: $g) isa course-outcome; $g "A" isa grade; count $g;
set b-count $as as match $s isa student; (attendee: $x, grade: $g) isa course-outcome; $g "B" isa grade; count $g;
...

in a rule or in a match query at this time.

The closest you can get is to hard-code the rules as follows:

when {
  $s isa student; $g "A" isa grade;
  $outcome-1 (attendee: $s, grade: $g) isa course-outcome;
  $outcome-2 (attendee: $s, grade: $g) isa course-outcome;
  $outcome-3 (attendee: $s, grade: $g) isa course-outcome;
  $outcome-4 (attendee: $s, grade: $g) isa course-outcome;
  $outcome-5 (attendee: $s, grade: $g) isa course-outcome;
  $outcome-6 (attendee: $s, grade: $g) isa course-outcome;
  not { $outcome-1 is $outcome-2;};  not { $outcome-1 is $outcome-3;};   not { $outcome-1 is $outcome-4;}; 
  not { $outcome-1 is $outcome-5;};   not { $outcome-1 is $outcome-6;}; 
  not { $outcome-2 is $outcome-3;};   not { $outcome-2 is $outcome-4;};  not { $outcome-2 is $outcome-5;};   
  not { $outcome-2 is $outcome-6;}; 
  not { $outcome-3 is $outcome-4;}; not { $outcome-3 is $outcome-5;};   not { $outcome-3 is $outcome-6;}; 
  not { $outcome-4 is $outcome-5;};   not { $outcome-4 is $outcome-6;}; 
  not { $outcome-5 is $outcome-6;}; 
[ some other constraint for "D"]
} then {
 $s has classification "outstanding";
}

then you can chain the other rules:

when {
  $s isa student; $g "A" isa grade;
  not { $s has classification "outstanding"; };
  $outcome-1 (attendee: $s, grade: $g) isa course-outcome;
  $outcome-2 (attendee: $s, grade: $g) isa course-outcome;
  $outcome-3 (attendee: $s, grade: $g) isa course-outcome;
  $outcome-4 (attendee: $s, grade: $g) isa course-outcome;
  not { $outcome-1 is $outcome-2;};  not { $outcome-1 is $outcome-3;};   not { $outcome-1 is $outcome-4;}; 
  not { $outcome-2 is $outcome-3;};   not { $outcome-2 is $outcome-4;}; 
  not { $outcome-3 is $outcome-4;};
[ some other constraint for "D"]
} then {
 $s has classification "good";
}

etc.
then you can chain the other rules:

when {
  $s isa student; $g "A" isa grade;
  not { $s has classificaton "outstanding"; };
  not { $s has classification "good"; };
  $outcome-1 (attendee: $s, grade: $g) isa course-outcome;
  $outcome-2 (attendee: $s, grade: $g) isa course-outcome;
  not { $outcome-1 is $outcome-2;};  not { $outcome-1 is $outcome-3;}; 
  not { $outcome-2 is $outcome-3;};  
[ some other constraint for "D"]
} then {
 $s has classification "pass";
}

this is mildly horrific, but may work… although I expect it to hit the reasoning engine quite hard if you have a decent amount of data. Super interesting case though!

Dear joshua, thank you for your attention and reply to this question.
Although I achieved the desired functionality in two steps in Neo4J, but there is still no implementation of storing rules in graphs.
And this looks “mildly horrific” in typeDB as you said .
I want to emphasize that in my application scenario, there are only two fixed rules (no more else), users can be allowed to change the range in the rule. For example, gradeA is changed to 95-100; rating “outstanding” is changed to 8B+…
So I want to store this rule in the graph, such as:

:range1 a :range;
          :minInclusive 90;
          :maxInclusive 100.

:range2 a :range;
          :minInclusive 75;
          :maxExclusive 90.

:rule1 :about :grading;
          :hasRuleBranch [  :order  1 ;
                                       :condition [:score :in :range1];
                                       :result  "grade_A" ;  
                                     ];
           :hasRuleBranch [  :order  2 ;
                                        :condition [:score :in :range2];
                                        :result  "grade_B" ;  
                                     ];
            ......

And vast amounts of data about students’ scores are stored on remote servers,
So we can get the student’s rating by calling a stored procedure or service,
It also allows users to modify the grading criteria without changing the business logic.
Of course, I try to do this with pure RDF, and then do query with SPARQL ,but I haven’t succeeded so far.
It would be great if typeDB could be designed to do these tasks simply, because this scenario is widely used in many fields, so I think it is very meaningful to solve this problem.

Kind regards,
Joylix

I store the students’ score data in RDF:

    @prefix : <http://example.org/#> .
    :Bob a :Student;
         :tookTest :Test0,:Test1,:Test2 ,:Test3 .
    :Test0 :score 90 .
    :Test1 :score 81 .
    :Test2 :score 62 .
    :Test3 :score 32 .

and I used the following SPARQL query to get the grade of the student’s Test:

prefix : <http://example.org/#>
select ?student ?test ?grade
    WHERE {  
      ?student :tookTest ?test .
      ?test :score ?score .
      BIND ( IF ( ?score >= 90 , "A", IF ( ?score>=75, "B", IF ( ?score>=60, "C", "D" ) ) ) AS ?grade )
      }

And I got the right result as such:

student,                  test,                       grade
http://example.org/#Bob,  http://example.org/#Test3,  D
http://example.org/#Bob,  http://example.org/#Test2,  C
http://example.org/#Bob,  http://example.org/#Test1,  B
http://example.org/#Bob,  http://example.org/#Test0,  A

How to store this rule data in RDF (along with the score) rather than in SPARQL, so that the rule data can be defined and modified by the user, The server side reads the rule data to generate the appropriate SPARQL query.
My initial thoughts are as follows:

 :range1 a :range ;
         :grade "A" ;
         :lowerLimit 90 ;
         :upperLimit 100.
:range2 a :range ;
         :grade "B" ;
         :lowerLimit 75 ;
         :upperLimit 89 .
:range3 a :range ;
         :grade "C" ;
         :lowerLimit 60 ;
         :upperLimit 74 .
:range4 a :range ;
         :grade "D" ;
         :lowerLimit 0 ;
         :upperLimit 59 .
:rule1 :hasRange :range1,:range2, :range3, :range4 .

But how do I now write the right SPARQL to read the rule and complete the grade transformation? And how do I handle with inclusive and exclusive of the boundary of limit?

I don’t know about OWL stuff, but I think what you’ll want to do for now is move some of the aggregations to the application layer. But you can relatively easily encode the grading schemas with this kind of rule.

Then in the application layer just query for the number of A’s and D’s a student has and write your series of if-else there :slight_smile:

That should do everything you wanted succinctly and modifiably

I can do count(A) and if-else logic at the application level. I have to write this rule in the data file because I don’t want to include business logic in the application layer. Maybe I can use eval(“if count(A)>=…” ), but it is best to do the presentation of the business logic in the data layer (It would be great if graphs could implement this storage), It is then allowed to implement it at the application level. If record all students’ scores and the corresponding grading rules in a text file, The data is then read by Python and the grade or rating are calculated , that is also OK, but graphs are clearly better for visualization.
I don’t know if I’ve made my intentions clear. looking forward to more progress on this issue.
Thank you for your reply and suggestions!

you could evaluate the aggregations at the application layer and re-insert them:

a_count = count_as(student_id)
d_count = cound_ds(student_id)

update_student_grade_counds(student_id, a_count, d_count)

get_classification(student_id)


def update_student_grade_counds(student_id, a_count, d_count):
  # use a query like this:
  # first delete their old a_count and d_count if they have one
  # match $x isa student, has id {student_id}, has a_count $a; delete $x has $a;
  # match $x isa student, has id {student_id}, has d_count $d; delete $x has $d;
  # match $x isa student, has id {student_id}; insert $x has a_count {a_count}, has d_count {d_count};

this way you can use rules to compute the a’s and d’s, and the classification based on the a’s and d’s, but you query and insert the aggregated values in between