Merge branch 'fix/exercise-anomaly-detection-nil-values' into feature/ordered_exercise_collection

This commit is contained in:
Maximilian Grundke
2018-07-10 12:46:27 +02:00
13 changed files with 138 additions and 41 deletions

View File

@ -0,0 +1,32 @@
$(function() {
var grid = $('#tag-grid');
if ($.isController('external_users') && grid.isPresent()) {
var spinner = $('#loading');
var noElements = $('#no-elements');
var buildTagContainer = function(tag) {
return '\
<div class="tag">\
<div class="name">' + tag.key + '</div>\
<div class="progress">\
<div class="progress-bar" role="progressbar" style="width:' + tag.value + '%">' + tag.value + '%</div>\
</div>\
</div>';
};
var jqxhr = $.ajax(window.location.href + '/tag_statistics', {
dataType: 'json',
method: 'GET'
});
jqxhr.done(function(response) {
spinner.hide();
if (response.length === 0) {
noElements.show();
} else {
var elements = response.map(buildTagContainer);
grid.append(elements);
}
});
}
});

View File

@ -59,3 +59,34 @@ span.caret {
.markdown {
height: 200px;
}
.spinner {
width: 40px;
height: 40px;
background-color: #333;
margin: 100px auto;
-webkit-animation: sk-rotateplane 1.2s infinite ease-in-out;
animation: sk-rotateplane 1.2s infinite ease-in-out;
}
@-webkit-keyframes sk-rotateplane {
0% { -webkit-transform: perspective(120px) }
50% { -webkit-transform: perspective(120px) rotateY(180deg) }
100% { -webkit-transform: perspective(120px) rotateY(180deg) rotateX(180deg) }
}
@keyframes sk-rotateplane {
0% {
transform: perspective(120px) rotateX(0deg) rotateY(0deg);
-webkit-transform: perspective(120px) rotateX(0deg) rotateY(0deg)
}
50% {
transform: perspective(120px) rotateX(-180.1deg) rotateY(0deg);
-webkit-transform: perspective(120px) rotateX(-180.1deg) rotateY(0deg)
}
100% {
transform: perspective(120px) rotateX(-180deg) rotateY(-179.9deg);
-webkit-transform: perspective(120px) rotateX(-180deg) rotateY(-179.9deg);
}
}

View File

@ -0,0 +1,16 @@
#no-elements {
display: none;
}
#tag-grid {
display: grid;
grid-template-columns: 25% 25% 25% 25%;
grid-column-gap: 10px;
grid-row-gap: 15px;
.progress {
.progress-bar {
min-width: 2em;
}
}
}

View File

@ -120,34 +120,3 @@ tr.highlight {
}
}
}
.spinner {
width: 40px;
height: 40px;
background-color: #333;
margin: 100px auto;
-webkit-animation: sk-rotateplane 1.2s infinite ease-in-out;
animation: sk-rotateplane 1.2s infinite ease-in-out;
}
@-webkit-keyframes sk-rotateplane {
0% { -webkit-transform: perspective(120px) }
50% { -webkit-transform: perspective(120px) rotateY(180deg) }
100% { -webkit-transform: perspective(120px) rotateY(180deg) rotateX(180deg) }
}
@keyframes sk-rotateplane {
0% {
transform: perspective(120px) rotateX(0deg) rotateY(0deg);
-webkit-transform: perspective(120px) rotateX(0deg) rotateY(0deg)
}
50% {
transform: perspective(120px) rotateX(-180.1deg) rotateY(0deg);
-webkit-transform: perspective(120px) rotateX(-180.1deg) rotateY(0deg)
}
100% {
transform: perspective(120px) rotateX(-180deg) rotateY(-179.9deg);
-webkit-transform: perspective(120px) rotateX(-180deg) rotateY(-179.9deg);
}
}

View File

@ -63,4 +63,20 @@ class ExternalUsersController < ApplicationController
}
end
def tag_statistics
@user = ExternalUser.find(params[:id])
authorize!
statistics = []
tags = ProxyExercise.new().get_user_knowledge_and_max_knowledge(@user, @user.participations.uniq.compact)
tags[:user_topic_knowledge].each_pair do |key, value|
statistics.append({:key => key.name.to_s, :value => (100.0 / tags[:max_topic_knowledge][key] * value).round})
end
statistics.sort_by! {|item| -item[:value]}
respond_to do |format|
format.json { render(json: statistics) }
end
end
end

View File

@ -8,6 +8,7 @@ module User
has_many :exercises, as: :user
has_many :file_types, as: :user
has_many :submissions, as: :user
has_many :participations, through: :submissions, source: :exercise, as: :user
has_many :user_proxy_exercise_exercises, as: :user
has_many :user_exercise_interventions, as: :user
has_many :interventions, through: :user_exercise_interventions

View File

@ -214,8 +214,8 @@ class ProxyExercise < ActiveRecord::Base
ex.tags.each do |t|
tags_counter[t] += 1
tag_diminishing_return_factor = tag_diminishing_return_function(tags_counter[t], all_used_tags_with_count[t])
tag_ratio = ex.exercise_tags.where(tag: t).first.factor.to_f / ex.exercise_tags.inject(0){|sum, et| sum += et.factor }.to_f
Rails.logger.debug("tag: #{t}, factor: #{ex.exercise_tags.where(tag: t).first.factor}, sumall: #{ex.exercise_tags.inject(0){|sum, et| sum += et.factor }}")
tag_ratio = ex.exercise_tags.where(tag: t).first.factor.to_f / ex.exercise_tags.inject(0){|sum, et| sum + et.factor }.to_f
Rails.logger.debug("tag: #{t}, factor: #{ex.exercise_tags.where(tag: t).first.factor}, sumall: #{ex.exercise_tags.inject(0){|sum, et| sum + et.factor }}")
Rails.logger.debug("tag #{t}, count #{tags_counter[t]}, max: #{all_used_tags_with_count[t]}, factor: #{tag_diminishing_return_factor}")
Rails.logger.debug("tag_ratio #{tag_ratio}")
topic_knowledge_ratio = ex.expected_difficulty * tag_ratio
@ -226,11 +226,10 @@ class ProxyExercise < ActiveRecord::Base
end
{user_topic_knowledge: topic_knowledge_loss_user, max_topic_knowledge: topic_knowledge_max}
end
private :get_user_knowledge_and_max_knowledge
def tag_diminishing_return_function(count_tag, total_count_tag)
total_count_tag += 1 # bonus exercise comes on top
return 1/(1+(Math::E**(-3/(0.5*total_count_tag)*(count_tag-0.5*total_count_tag))))
1 / (1 + (Math::E**(-3 / (0.5 * total_count_tag) * (count_tag - 0.5 * total_count_tag))))
end
def select_easiest_exercise(exercises)

View File

@ -2,4 +2,8 @@ class ExternalUserPolicy < AdminOnlyPolicy
def statistics?
admin?
end
def tag_statistics?
admin?
end
end

View File

@ -4,5 +4,12 @@ h1 = @user.name
//= row(label: 'external_user.email', value: @user.email)
= row(label: 'external_user.consumer', value: link_to(@user.consumer, @user.consumer))
br
= link_to(t('shared.statistics'), statistics_external_user_path(@user))
h4 = link_to(t('.exercise_statistics'), statistics_external_user_path(@user))
h4 = t('.tag_statistics')
#loading
.spinner
= t('.loading_tag_statistics')
#no-elements
= t('.empty_tag_statistics')
#tag-grid

View File

@ -380,6 +380,11 @@ de:
score: Bewertung
runs: Abgaben
worktime: Arbeitszeit
show:
loading_tag_statistics: "Lade Lernbereichstatistiken"
tag_statistics: "Lernbereichstatistiken"
empty_tag_statistics: "Keine Statistiken verfügbar"
exercise_statistics: "Aufgabenstatistiken"
files:
roles:
main_file: Hauptdatei

View File

@ -380,6 +380,11 @@ en:
score: Score
runs: Submissions
worktime: Working Time
show:
loading_tag_statistics: "Loading tag statistics..."
tag_statistics: "Tag Statistics"
empty_tag_statistics: "No statistics available"
exercise_statistics: "Exercise Statistics"
files:
roles:
main_file: Main File

View File

@ -128,6 +128,9 @@ Rails.application.routes.draw do
resources :external_users, only: [:index, :show], concerns: :statistics do
resources :exercises, concerns: :statistics
member do
get :tag_statistics
end
end
namespace :code_ocean do

View File

@ -61,16 +61,25 @@ namespace :detect_exercise_anomalies do
.having('count(exercises_with_submissions.id) > ?', number_of_exercises)
end
def find_anomalies(collection)
def collect_working_times(collection)
working_times = {}
collection.exercises.each do |exercise|
puts "\t\t> #{exercise.title}"
working_times[exercise.id] = get_average_working_time(exercise)
end
average = working_times.values.reduce(:+) / working_times.size
working_times.select do |exercise_id, working_time|
working_time > average * MAX_TIME_FACTOR or working_time < average * MIN_TIME_FACTOR
working_times
end
def find_anomalies(collection)
working_times = collect_working_times(collection)
values = working_times.values.reject {|value| value.nil?}
if working_times.size > 0
average = values.reduce(:+) / working_times.size
return working_times.select do |_, working_time|
working_time > average * MAX_TIME_FACTOR or working_time < average * MIN_TIME_FACTOR
end
end
{}
end
def get_average_working_time(exercise)