Skip to content

Commit 82490d2

Browse files
committed
Make the avg_problem_grader just be the weighted grader.
The weighted grader really is just a more advanced version of the average problem grader that allows weights and "credit". If weights are not assigned then a weight of 1 is assumed for each answer. So if no weights are assigned then the weighted grader returns exactly the same thing as the previous average problem grader. The `weightedGrader.pl` macro should be considered deprecated. The `WEIGHTED_ANS`, `NAMED_WEIGHTED_ANS`, and `CREDIT_ANS` methods should no longer be used (although they will still work even with the new average problem grader). Instead of calling `WEIGHTED_ANS` or `NAMED_WEIGHTED_ANS`, pass `weight => n` to the `cmp` method to assign a weight to an answer. Instead of calling `CREDIT_ANS` pass `credit => $answer1` or `credit => [ $answer1, $answer2, ... ]` to the `cmp` method. That is effectively what those methods do anyway. Note that the other answers need to be assigned a name using the `NEW_ANS_NAME` method. Note that if the `weightedGrader.pl` macro is loaded and `install_weighted_grader` is not called, then using the `WEIGHTED_ANS`, `NAMED_WEIGHTED_ANS`, and `CREDIT_ANS` methods will work with this since the implementation is completely compatible with the `weighted_grader` defined in the macro. Also if the macro is loaded and `install_weighted_grader` is called, then the macro will continue to work as before. The options can either be set using the macro method, or as described in the previous paragraph (except the `credit` option must be an array reference with the macro weighted grader). There is one essential difference in this implementation from the previous weighted grader when the `credit` option is used. The previous weighted grader would mark optional answers correct, but not change their scores. This results in the feedback for those answers showing them as incorrect and the message shown above the problem stating that not all answers are correct, even though the overall problem score for the attempt is reported as 100%. The documentation in the `weightedGrader.pl` macro states that "When credit IS given, the blank answer is still marked as incorrect in the grey answer report at the top of the page, but the student gets awarded the points for that answer anyway (with no other indication). It is possible to cause the blank to be marked as correct, but this seemed confusing to the students." However, perhaps due to changes in feedback, it actually seemed much more confusing to me for the answers to be marked incorrect and have the red incorrect feedback and message above stating not all answers correct with the message way below that is often ignored by students stating that the score for the attempt is 100%. So this does what is suggested in the documentation and actually changes the scores for the optional answers. Furthermore, a message is added to those answers stating, "This answer was marked correct because the primary answer is correct." I am not sold on that wording, but this seems much clearer to me. Another option would be to not set the scores of the optional parts, but set the `$problem_result{msg}` informing the user what is going on. That message is displayed immediately below the problem as "Note: $problem_result{msg}". Since that is closer to the problem and not way down below all of the submit buttons that might be enough?
1 parent e35b78d commit 82490d2

File tree

2 files changed

+168
-63
lines changed

2 files changed

+168
-63
lines changed

lib/WeBWorK/PG/Translator.pm

Lines changed: 53 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1088,46 +1088,71 @@ sub rf_avg_problem_grader {
10881088
}
10891089

10901090
sub avg_problem_grader {
1091-
my ($rh_evaluated_answers, $rh_problem_state, %form_options) = @_;
1091+
my ($answers, $problem_state, %form_options) = @_;
10921092

1093-
my %evaluated_answers = %{$rh_evaluated_answers};
1093+
my %problem_result = (score => 0, errors => '', type => 'avg_problem_grader', msg => '');
10941094

1095-
# By default the old problem state is simply passed back out again.
1096-
my %problem_state = %$rh_problem_state;
1095+
$problem_result{msg} = maketext('You can earn partial credit on this problem.') if keys %$answers > 1;
10971096

1098-
# Initial setup of the answer
1099-
my $total = 0;
1100-
my %problem_result = (
1101-
score => 0,
1102-
errors => '',
1103-
type => 'avg_problem_grader',
1104-
msg => '',
1105-
);
1097+
# Return unless answers have been submitted.
1098+
return (\%problem_result, $problem_state) unless $form_options{answers_submitted} == 1;
1099+
1100+
my %credit;
1101+
1102+
# Get the score for each answer (error if can't recognize the answer format).
1103+
for my $ans_name (keys %$answers) {
1104+
if (ref($answers->{$ans_name}) =~ m/^(HASH|AnswerHash)$/) {
1105+
$credit{$ans_name} = $answers->{$ans_name}{score} // 0;
1106+
} else {
1107+
$problem_result{error} = "Error: Answer $ans_name is not a hash: $answers->{$ans_name}";
1108+
die "Error: Answer |$ans_name| is not a hash reference\n"
1109+
. $answers->{$ans_name}
1110+
. "\nThis probably means that the answer evaluator for this answer is not working correctly.";
1111+
}
1112+
}
11061113

1107-
my $count = keys %evaluated_answers;
1108-
$problem_result{msg} = 'You can earn partial credit on this problem.' if $count > 1;
1114+
# Mark any optional answers as correct, if the goal answers are right and the optional answers are blank.
1115+
for my $ans_name (keys %$answers) {
1116+
if ($credit{$ans_name} == 1 && defined $answers->{$ans_name}{credit}) {
1117+
for my $credit_name (
1118+
ref($answers->{$ans_name}{credit}) eq 'ARRAY'
1119+
? @{ $answers->{$ans_name}{credit} }
1120+
: $answers->{$ans_name}{credit})
1121+
{
1122+
if (!defined $answers->{$credit_name}{student_ans}
1123+
|| $answers->{$credit_name}{student_ans} =~ m/^\s*$/)
1124+
{
1125+
$answers->{$credit_name}{score} = 1;
1126+
$answers->{$credit_name}{ans_message} =
1127+
maketext('This answer was marked correct because the primary answer is correct.');
1128+
$credit{$credit_name} = 1;
1129+
}
1130+
}
1131+
}
1132+
}
11091133

1110-
return (\%problem_result, \%problem_state) unless $form_options{answers_submitted} == 1;
1134+
my ($score, $total) = (0, 0);
11111135

1112-
# Answers have been submitted -- process them.
1113-
for my $ans_name (keys %evaluated_answers) {
1114-
$total += $evaluated_answers{$ans_name}{score};
1136+
# Add up the weighted scores
1137+
for my $ans_name (keys %$answers) {
1138+
my $weight = $answers->{$ans_name}{weight} // 1;
1139+
$total += $weight;
1140+
$score += $weight * $credit{$ans_name};
11151141
}
11161142

1117-
# Calculate score rounded to three places to avoid roundoff problems
1118-
$problem_result{score} = $count ? $total / $count : 0;
1119-
$problem_state{recorded_score} //= 0;
1143+
$problem_result{score} = $total ? $score / $total : 0;
11201144

1121-
# Increase recorded score if the current score is greater.
1122-
$problem_state{recorded_score} = $problem_result{score}
1123-
if $problem_result{score} > $problem_state{recorded_score};
1145+
++$problem_state->{num_of_correct_ans} if $score == $total;
1146+
++$problem_state->{num_of_incorrect_ans} if $score < $total;
1147+
$problem_state->{recorded_score} //= 0;
11241148

1125-
++$problem_state{num_of_correct_ans} if $total == $count;
1126-
++$problem_state{num_of_incorrect_ans} if $total < $count;
1149+
# Increase recorded score if the current score is greater.
1150+
$problem_state->{recorded_score} = $problem_result{score}
1151+
if $problem_result{score} > $problem_state->{recorded_score};
11271152

1128-
warn "Error in grading this problem the total $total is larger than $count" if $total > $count;
1153+
warn "Error in grading this problem: The score $score is larger than the total $total." if $score > $total;
11291154

1130-
return (\%problem_result, \%problem_state);
1155+
return (\%problem_result, $problem_state);
11311156
}
11321157

11331158
=head2 post_process_content

macros/core/PGanswermacros.pl

Lines changed: 115 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1587,62 +1587,142 @@ sub std_problem_grader2 {
15871587

15881588
=head3 C<avg_problem_grader>
15891589
1590-
This grader gives a grade depending on how many questions from the problem are correct. (The highest
1591-
grade is the one that is kept. One can never lower the recorded grade on a problem by repeating it.)
1592-
Many professors (and almost all students :-) ) prefer this grader.
1590+
This grader gives a "weighted" average score to the problem and is the default
1591+
grader.
1592+
1593+
The grader can be selected by calling
15931594
15941595
install_problem_grader(~~&avg_problem_grader);
15951596
1597+
However, since this is the default grader, that is not necessary to use this
1598+
grader.
1599+
1600+
Each answer is assigned a weight (the default is 1). The score is then the sum
1601+
of the product of the weights and scores for the correct answers divided by the
1602+
total of the weights for all answers. (To assign weights as percentages, use
1603+
integers that add up to 100. For example, use 40 and 60 for the weights for two
1604+
answers.) Assign weights to answers using the C<cmp> option C<< weight => n >>.
1605+
For example, in PGML create the answer rule with
1606+
1607+
[_]{$answer}{10}{ cmp_options => { weight => 40 } }
1608+
1609+
With the classic C<ANS> method call
1610+
1611+
ANS($answer->cmp(weight => 40);
1612+
1613+
This grader also allows for one "goal" answer that is answered correctly to
1614+
automatically give credit for one or more other "optional" answers. This way, if
1615+
there are several "optional" answers leading up to the "goal" answer, and the
1616+
student produces the "goal" answer by some other means and does not answer the
1617+
"optional" answers, the student can be given full credit for the problem anyway.
1618+
To use this feature use the C<credit> option of the C<cmp> method for the "goal"
1619+
answer. For example, C<< credit => $answer1Name >> or C<< credit => [
1620+
$answer1Name, $answer2Name, ... ] >>, where C<$answer1Name>, C<$answer2Name>,
1621+
etc., are the names of the "optional" answers that will be given credit if the
1622+
"goal" answer is correct. Note that the other answers must be assigned names
1623+
either by calling C<NAMED_ANS_RULE> and C<NAMED_ANS>, or by creating the answer
1624+
rule in PGML with C<[_]{$answer1}{15}{$answer1Name}>, for example. The answer
1625+
names should be generated by calling C<NEW_ANS_NAME> (for example,
1626+
C<$answer1Name = NEW_ANS_NAME()>) rather than being made up. Otherwise the
1627+
problem will fail to work in many situations (for example, in tests). For
1628+
example, to set this up in PGML use
1629+
1630+
BEGIN_PGML
1631+
Optional Answer 1: [_]{$answer1}{10}{$answer1Name = NEW_ANS_NAME()}
1632+
1633+
Optional Answer 2: [_]{$answer2}{10}{$answer2Name = NEW_ANS_NAME()}
1634+
1635+
Goal: [_]{$answer3}{10}{ cmp_options => { credit => [ $answer1Name, $answer2Name ] } }
1636+
END_PGML
1637+
1638+
Note that the C<credit> and C<weight> options can be used together. For example:
1639+
1640+
BEING_PGML
1641+
Optional Answer: [_]{$optional}{10}{$optionalName = NEW_ANS_NAME()}{{ weight => 20 }}
1642+
1643+
Goal: [_]{$goalAnswer}{10}{ cmp_options => { credit => $optionalName, weight => 80 } }
1644+
END_PGML
1645+
1646+
This way, if the "optional" answer is correct but the "goal" answer is not, the
1647+
problem score will be 20%, but if the "goal" answer is correct, the problem
1648+
score will be 100%.
1649+
1650+
One caveat to keep in mind is that credit is given to an "optional" answer ONLY
1651+
if the answer is left blank (or is actually correct). Credit will NOT be given
1652+
if an "optional" answer is incorrect, even if the "goal" answer IS correct.
1653+
1654+
When credit is given to an "optional" answer due to the "goal" answer being
1655+
correct, a message will be added to the "optional" answer stating, "This answer
1656+
was marked correct because the primary answer is correct."
1657+
15961658
=cut
15971659

15981660
sub avg_problem_grader {
1599-
my ($rh_evaluated_answers, $rh_problem_state, %form_options) = @_;
1661+
my ($answers, $problem_state, %form_options) = @_;
16001662

1601-
my %evaluated_answers = %{$rh_evaluated_answers};
1663+
my %problem_result = (score => 0, errors => '', type => 'avg_problem_grader', msg => '');
16021664

1603-
# By default the old problem state is simply passed back out again.
1604-
my %problem_state = %$rh_problem_state;
1605-
1606-
# Initial setup of the answer.
1607-
my $total = 0;
1608-
my %problem_result = (
1609-
score => 0,
1610-
errors => '',
1611-
type => 'avg_problem_grader',
1612-
msg => '',
1613-
);
1614-
my $count = keys %evaluated_answers;
1615-
$problem_result{msg} = maketext('You can earn partial credit on this problem.') if $count > 1;
1665+
$problem_result{msg} = maketext('You can earn partial credit on this problem.') if keys %$answers > 1;
16161666

16171667
# Return unless answers have been submitted.
1618-
return (\%problem_result, \%problem_state) unless $form_options{answers_submitted} == 1;
1668+
return (\%problem_result, $problem_state) unless $form_options{answers_submitted} == 1;
16191669

1620-
# Answers have been submitted -- process them.
1621-
for my $ans_name (keys %evaluated_answers) {
1622-
if (ref $evaluated_answers{$ans_name} eq 'HASH' || ref $evaluated_answers{$ans_name} eq 'AnswerHash') {
1623-
$total += $evaluated_answers{$ans_name}{score} // 0;
1670+
my %credit;
1671+
1672+
# Get the score for each answer (error if can't recognize the answer format).
1673+
for my $ans_name (keys %$answers) {
1674+
if (ref($answers->{$ans_name}) =~ m/^(HASH|AnswerHash)$/) {
1675+
$credit{$ans_name} = $answers->{$ans_name}{score} // 0;
16241676
} else {
1677+
$problem_result{error} = "Error: Answer $ans_name is not a hash: $answers->{$ans_name}";
16251678
die "Error: Answer |$ans_name| is not a hash reference\n"
1626-
. $evaluated_answers{$ans_name}
1627-
. 'This probably means that the answer evaluator for this answer is not working correctly.';
1628-
$problem_result{error} = "Error: Answer $ans_name is not a hash: $evaluated_answers{$ans_name}";
1679+
. $answers->{$ans_name}
1680+
. "\nThis probably means that the answer evaluator for this answer is not working correctly.";
16291681
}
16301682
}
16311683

1632-
# Calculate the score.
1633-
$problem_result{score} = $total / $count if $count;
1684+
# Mark any optional answers as correct, if the goal answers are right and the optional answers are blank.
1685+
for my $ans_name (keys %$answers) {
1686+
if ($credit{$ans_name} == 1 && defined $answers->{$ans_name}{credit}) {
1687+
for my $credit_name (
1688+
ref($answers->{$ans_name}{credit}) eq 'ARRAY'
1689+
? @{ $answers->{$ans_name}{credit} }
1690+
: $answers->{$ans_name}{credit})
1691+
{
1692+
if (!defined $answers->{$credit_name}{student_ans}
1693+
|| $answers->{$credit_name}{student_ans} =~ m/^\s*$/)
1694+
{
1695+
$answers->{$credit_name}{score} = 1;
1696+
$answers->{$credit_name}{ans_message} =
1697+
maketext('This answer was marked correct because the primary answer is correct.');
1698+
$credit{$credit_name} = 1;
1699+
}
1700+
}
1701+
}
1702+
}
16341703

1635-
++$problem_state{num_of_correct_ans} if $total == $count;
1636-
++$problem_state{num_of_incorrect_ans} if $total < $count;
1637-
$problem_state{recorded_score} //= 0;
1704+
my ($score, $total) = (0, 0);
1705+
1706+
# Add up the weighted scores
1707+
for my $ans_name (keys %$answers) {
1708+
my $weight = $answers->{$ans_name}{weight} // 1;
1709+
$total += $weight;
1710+
$score += $weight * $credit{$ans_name};
1711+
}
1712+
1713+
$problem_result{score} = $total ? $score / $total : 0;
1714+
1715+
++$problem_state->{num_of_correct_ans} if $score == $total;
1716+
++$problem_state->{num_of_incorrect_ans} if $score < $total;
1717+
$problem_state->{recorded_score} //= 0;
16381718

16391719
# Increase recorded score if the current score is greater.
1640-
$problem_state{recorded_score} = $problem_result{score}
1641-
if $problem_result{score} > $problem_state{recorded_score};
1720+
$problem_state->{recorded_score} = $problem_result{score}
1721+
if $problem_result{score} > $problem_state->{recorded_score};
16421722

1643-
warn "Error in grading this problem the total $total is larger than $count" if $total > $count;
1723+
warn "Error in grading this problem: The score $score is larger than the total $total." if $score > $total;
16441724

1645-
return (\%problem_result, \%problem_state);
1725+
return (\%problem_result, $problem_state);
16461726
}
16471727

16481728
=head2 Utility subroutines

0 commit comments

Comments
 (0)