Skip to content

Commit d51cf3d

Browse files
authored
Merge pull request #1160 from drgrice1/weighted-avg-problem-grader
Make the `avg_problem_grader` just be the weighted grader.
2 parents 1d641b5 + fac0c89 commit d51cf3d

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)