Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make the avg_problem_grader just be the weighted grader. #1160

Open
wants to merge 1 commit into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 53 additions & 28 deletions lib/WeBWorK/PG/Translator.pm
Original file line number Diff line number Diff line change
Expand Up @@ -1088,46 +1088,71 @@ sub rf_avg_problem_grader {
}

sub avg_problem_grader {
my ($rh_evaluated_answers, $rh_problem_state, %form_options) = @_;
my ($answers, $problem_state, %form_options) = @_;

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

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

# Initial setup of the answer
my $total = 0;
my %problem_result = (
score => 0,
errors => '',
type => 'avg_problem_grader',
msg => '',
);
# Return unless answers have been submitted.
return (\%problem_result, $problem_state) unless $form_options{answers_submitted} == 1;

my %credit;

# Get the score for each answer (error if can't recognize the answer format).
for my $ans_name (keys %$answers) {
if (ref($answers->{$ans_name}) =~ m/^(HASH|AnswerHash)$/) {
$credit{$ans_name} = $answers->{$ans_name}{score} // 0;
} else {
$problem_result{error} = "Error: Answer $ans_name is not a hash: $answers->{$ans_name}";
die "Error: Answer |$ans_name| is not a hash reference\n"
. $answers->{$ans_name}
. "\nThis probably means that the answer evaluator for this answer is not working correctly.";
}
}

my $count = keys %evaluated_answers;
$problem_result{msg} = 'You can earn partial credit on this problem.' if $count > 1;
# Mark any optional answers as correct, if the goal answers are right and the optional answers are blank.
for my $ans_name (keys %$answers) {
if ($credit{$ans_name} == 1 && defined $answers->{$ans_name}{credit}) {
for my $credit_name (
ref($answers->{$ans_name}{credit}) eq 'ARRAY'
? @{ $answers->{$ans_name}{credit} }
: $answers->{$ans_name}{credit})
{
if (!defined $answers->{$credit_name}{student_ans}
|| $answers->{$credit_name}{student_ans} =~ m/^\s*$/)
{
$answers->{$credit_name}{score} = 1;
$answers->{$credit_name}{ans_message} =
maketext('This answer was marked correct because the primary answer is correct.');
$credit{$credit_name} = 1;
}
}
}
}

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

# Answers have been submitted -- process them.
for my $ans_name (keys %evaluated_answers) {
$total += $evaluated_answers{$ans_name}{score};
# Add up the weighted scores
for my $ans_name (keys %$answers) {
my $weight = $answers->{$ans_name}{weight} // 1;
$total += $weight;
$score += $weight * $credit{$ans_name};
}

# Calculate score rounded to three places to avoid roundoff problems
$problem_result{score} = $count ? $total / $count : 0;
$problem_state{recorded_score} //= 0;
$problem_result{score} = $total ? $score / $total : 0;

# Increase recorded score if the current score is greater.
$problem_state{recorded_score} = $problem_result{score}
if $problem_result{score} > $problem_state{recorded_score};
++$problem_state->{num_of_correct_ans} if $score == $total;
++$problem_state->{num_of_incorrect_ans} if $score < $total;
$problem_state->{recorded_score} //= 0;

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

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

return (\%problem_result, \%problem_state);
return (\%problem_result, $problem_state);
}

=head2 post_process_content
Expand Down
150 changes: 115 additions & 35 deletions macros/core/PGanswermacros.pl
Original file line number Diff line number Diff line change
Expand Up @@ -1587,62 +1587,142 @@ sub std_problem_grader2 {

=head3 C<avg_problem_grader>

This grader gives a grade depending on how many questions from the problem are correct. (The highest
grade is the one that is kept. One can never lower the recorded grade on a problem by repeating it.)
Many professors (and almost all students :-) ) prefer this grader.
This grader gives a "weighted" average score to the problem and is the default
grader.

The grader can be selected by calling

install_problem_grader(~~&avg_problem_grader);

However, since this is the default grader, that is not necessary to use this
grader.

Each answer is assigned a weight (the default is 1). The score is then the sum
of the product of the weights and scores for the correct answers divided by the
total of the weights for all answers. (To assign weights as percentages, use
integers that add up to 100. For example, use 40 and 60 for the weights for two
answers.) Assign weights to answers using the C<cmp> option C<< weight => n >>.
For example, in PGML create the answer rule with

[_]{$answer}{10}{ cmp_options => { weight => 40 } }

With the classic C<ANS> method call

ANS($answer->cmp(weight => 40);

This grader also allows for one "goal" answer that is answered correctly to
automatically give credit for one or more other "optional" answers. This way, if
there are several "optional" answers leading up to the "goal" answer, and the
student produces the "goal" answer by some other means and does not answer the
"optional" answers, the student can be given full credit for the problem anyway.
To use this feature use the C<credit> option of the C<cmp> method for the "goal"
answer. For example, C<< credit => $answer1Name >> or C<< credit => [
$answer1Name, $answer2Name, ... ] >>, where C<$answer1Name>, C<$answer2Name>,
etc., are the names of the "optional" answers that will be given credit if the
"goal" answer is correct. Note that the other answers must be assigned names
either by calling C<NAMED_ANS_RULE> and C<NAMED_ANS>, or by creating the answer
rule in PGML with C<[_]{$answer1}{15}{$answer1Name}>, for example. The answer
names should be generated by calling C<NEW_ANS_NAME> (for example,
C<$answer1Name = NEW_ANS_NAME()>) rather than being made up. Otherwise the
problem will fail to work in many situations (for example, in tests). For
example, to set this up in PGML use

BEGIN_PGML
Optional Answer 1: [_]{$answer1}{10}{$answer1Name = NEW_ANS_NAME()}

Optional Answer 2: [_]{$answer2}{10}{$answer2Name = NEW_ANS_NAME()}

Goal: [_]{$answer3}{10}{ cmp_options => { credit => [ $answer1Name, $answer2Name ] } }
END_PGML

Note that the C<credit> and C<weight> options can be used together. For example:

BEING_PGML
Optional Answer: [_]{$optional}{10}{$optionalName = NEW_ANS_NAME()}{{ weight => 20 }}

Goal: [_]{$goalAnswer}{10}{ cmp_options => { credit => $optionalName, weight => 80 } }
END_PGML

This way, if the "optional" answer is correct but the "goal" answer is not, the
problem score will be 20%, but if the "goal" answer is correct, the problem
score will be 100%.

One caveat to keep in mind is that credit is given to an "optional" answer ONLY
if the answer is left blank (or is actually correct). Credit will NOT be given
if an "optional" answer is incorrect, even if the "goal" answer IS correct.

When credit is given to an "optional" answer due to the "goal" answer being
correct, a message will be added to the "optional" answer stating, "This answer
was marked correct because the primary answer is correct."

=cut

sub avg_problem_grader {
my ($rh_evaluated_answers, $rh_problem_state, %form_options) = @_;
my ($answers, $problem_state, %form_options) = @_;

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

# By default the old problem state is simply passed back out again.
my %problem_state = %$rh_problem_state;

# Initial setup of the answer.
my $total = 0;
my %problem_result = (
score => 0,
errors => '',
type => 'avg_problem_grader',
msg => '',
);
my $count = keys %evaluated_answers;
$problem_result{msg} = maketext('You can earn partial credit on this problem.') if $count > 1;
$problem_result{msg} = maketext('You can earn partial credit on this problem.') if keys %$answers > 1;

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

# Answers have been submitted -- process them.
for my $ans_name (keys %evaluated_answers) {
if (ref $evaluated_answers{$ans_name} eq 'HASH' || ref $evaluated_answers{$ans_name} eq 'AnswerHash') {
$total += $evaluated_answers{$ans_name}{score} // 0;
my %credit;

# Get the score for each answer (error if can't recognize the answer format).
for my $ans_name (keys %$answers) {
if (ref($answers->{$ans_name}) =~ m/^(HASH|AnswerHash)$/) {
$credit{$ans_name} = $answers->{$ans_name}{score} // 0;
} else {
$problem_result{error} = "Error: Answer $ans_name is not a hash: $answers->{$ans_name}";
die "Error: Answer |$ans_name| is not a hash reference\n"
. $evaluated_answers{$ans_name}
. 'This probably means that the answer evaluator for this answer is not working correctly.';
$problem_result{error} = "Error: Answer $ans_name is not a hash: $evaluated_answers{$ans_name}";
. $answers->{$ans_name}
. "\nThis probably means that the answer evaluator for this answer is not working correctly.";
}
}

# Calculate the score.
$problem_result{score} = $total / $count if $count;
# Mark any optional answers as correct, if the goal answers are right and the optional answers are blank.
for my $ans_name (keys %$answers) {
if ($credit{$ans_name} == 1 && defined $answers->{$ans_name}{credit}) {
for my $credit_name (
ref($answers->{$ans_name}{credit}) eq 'ARRAY'
? @{ $answers->{$ans_name}{credit} }
: $answers->{$ans_name}{credit})
{
if (!defined $answers->{$credit_name}{student_ans}
|| $answers->{$credit_name}{student_ans} =~ m/^\s*$/)
{
$answers->{$credit_name}{score} = 1;
$answers->{$credit_name}{ans_message} =
maketext('This answer was marked correct because the primary answer is correct.');
$credit{$credit_name} = 1;
}
}
}
}

++$problem_state{num_of_correct_ans} if $total == $count;
++$problem_state{num_of_incorrect_ans} if $total < $count;
$problem_state{recorded_score} //= 0;
my ($score, $total) = (0, 0);

# Add up the weighted scores
for my $ans_name (keys %$answers) {
my $weight = $answers->{$ans_name}{weight} // 1;
$total += $weight;
$score += $weight * $credit{$ans_name};
}

$problem_result{score} = $total ? $score / $total : 0;

++$problem_state->{num_of_correct_ans} if $score == $total;
++$problem_state->{num_of_incorrect_ans} if $score < $total;
$problem_state->{recorded_score} //= 0;

# Increase recorded score if the current score is greater.
$problem_state{recorded_score} = $problem_result{score}
if $problem_result{score} > $problem_state{recorded_score};
$problem_state->{recorded_score} = $problem_result{score}
if $problem_result{score} > $problem_state->{recorded_score};

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

return (\%problem_result, \%problem_state);
return (\%problem_result, $problem_state);
}

=head2 Utility subroutines
Expand Down