Commit a209c539 authored by Anze Vavpetic's avatar Anze Vavpetic
Browse files

several decision support package fixes: attribute properties are now set in a...

several decision support package fixes: attribute properties are now set in a separate dialog; added the option of setting a custom range for each attribute; unusable attributes (=discrete) are reported to the user; if no label attribute is available, the widgets use indices; minor refactoring
parent 812de2cd
......@@ -6,5 +6,11 @@ Decision support interaction views.
from django.shortcuts import render
def decision_support_wsm(request, input_dict, output_dict, widget):
attributes = [att.name for att in input_dict['data'].domain.features]
return render(request, 'interactions/wsm.html', {'widget':widget, 'attributes':attributes})
from wsm import WeightedSumModel
data = input_dict['data']
model = WeightedSumModel(data)
attributes = sorted(model.ranges.items())
unusable = sorted(model.unusable)
return render(request, 'interactions/wsm.html',
{'widget' : widget, 'attributes' : attributes,
'unusable_attributes' : unusable})
......@@ -3,60 +3,32 @@ Decision support library functions.
@author: Anze Vavpetic <anze.vavpetic@ijs.si>
'''
def decision_support_wsm(input_dict):
output_dict = input_dict
output_dict['model'] = None
return output_dict
class WeightedSumModel:
'''
Weighted sum model.
'''
def __init__(self, data, weights, smaller_is_better=None):
self.data = data
self.weights = weights
self.smaller_is_better = smaller_is_better if smaller_is_better else set()
self.name = 'Weighted sum model'
def __call__(self, weights=None):
import Orange
from Orange.feature import Type
if weights == None:
weights = self.weights
# New augmented table
norm_data = Orange.data.Table(self.data)
newid = min(norm_data.domain.get_metas().keys()) - 1
score_attr = Orange.feature.Continuous('score')
norm_data.domain.add_meta(newid, score_attr)
norm_data.add_meta_attribute(score_attr)
# Normalize the attributes column-wise
for att in norm_data.domain:
if att.var_type == Type.Continuous:
col = [ex[att] for ex in norm_data]
col_norm = float(sum(col))
for ex in norm_data:
if att.var_type == Type.Continuous:
ex[att] = ex[att] / col_norm
# Use the inverse of an attr. value if smaller values should be treated as 'better'.
inverse = lambda x, att: 1-x if att in self.smaller_is_better else x
for ex in norm_data:
score = sum([inverse(ex[att], att.name) * weights.get(att.name, 1) for att in norm_data.domain.features if att.var_type == Type.Continuous])
ex['score'] = score
return norm_data
def decision_support_wsm_finished(postdata, input_dict, output_dict):
# Fetch the data and the weights from the form.
from wsm import WeightedSumModel
# Fetch the data from the form.
data = input_dict['data']
attributes = [att.name for att in data.domain.features]
weights = {}
widget_id = postdata['widget_id'][0]
smaller_is_better = set()
for att in attributes:
weights[att]=int(postdata['weight'+str(widget_id)+str(att)][0])
if postdata.has_key('smallerIsBetter'+str(widget_id)+str(att)):
smaller_is_better.add(att)
widget_id = int(postdata['widget_id'][0])
minimize = set()
ranges = {}
for idx, att in enumerate(attributes):
weights[att] = int(postdata['weight-%d-%d' % (widget_id, idx)][0])
direction = postdata['direction-%d-%d' % (widget_id, idx)][0]
if direction == 'min':
minimize.add(att)
lower_bound = float(postdata['rangeMin-%d-%d' % (widget_id, idx)][0])
upper_bound = float(postdata['rangeMax-%d-%d' % (widget_id, idx)][0])
ranges[att] = (lower_bound, upper_bound)
# Instantiate a WeightedSumModel model.
kt = WeightedSumModel(data, weights, smaller_is_better=smaller_is_better)
kt = WeightedSumModel(data, user_weights=weights, minimize=minimize,
ranges=ranges)
output_dict = {}
output_dict['data'] = kt()
output_dict['model'] = kt
......
......@@ -2,100 +2,215 @@
<div>
Use the sliders to adjust the weights.<br><br>
<form>
<table cellpadding="0" cellspacing="0" border="0" class="display" id="weightSel{{widget.pk}}">
<table cellpadding="0" cellspacing="0" border="0" class="display" id="weight-sel-{{widget.pk}}">
<thead>
<tr>
<th>Attribute</th>
<th>Smaller attribute values are better</th>
<th>Properties</th>
<th>Weight</th>
<th>Weight (numeric)</th>
</tr>
</thead>
<tbody>
{% for att in attributes %}
{% for att, def_range in attributes %}
{% with count=forloop.counter0 pk=widget.pk %}
<tr>
<td>
{{att}}
</td>
<td>
<input id="smallerIsBetter{{widget.pk}}{{att}}" name="smallerIsBetter{{widget.pk}}{{att}}" type="checkbox" value="true" style="display:inline;" />
<span id="display-direction-{{pk}}-{{count}}">Maximize</span>,
Range: [<span id="display-rangeMin-{{pk}}-{{count}}">{{def_range.0|floatformat:2}}</span>,
<span id="display-rangeMax-{{pk}}-{{count}}">{{def_range.1|floatformat:2}}</span>]
<a href="" id="edit-properties-{{pk}}-{{count}}">edit</a>
<input id="direction-{{pk}}-{{count}}" name="direction-{{pk}}-{{count}}" type="hidden" value="max"/>
<input id="rangeMin-{{pk}}-{{count}}" name="rangeMin-{{pk}}-{{count}}" type="hidden" value="{{def_range.0}}"/>
<input id="rangeMax-{{pk}}-{{count}}" name="rangeMax-{{pk}}-{{count}}" type="hidden" value="{{def_range.1}}"/>
</td>
<td>
<div style="width:200px;" id="slider{{widget.pk}}{{att}}"></div>
<script type="text/javascript">
// Slider code.
$("#slider{{widget.pk}}{{att}}").slider({
value:0,
min:0,
max:100,
slide: function( event, ui ) {
$( "#weight{{widget.pk}}{{att}}").val( $("#slider{{widget.pk}}{{att}}").slider("value") );
},
stop: function ( event, ui ) {
$( "#weight{{widget.pk}}{{att}}").val( $("#slider{{widget.pk}}{{att}}").slider("value") );
}
});
// Change the slider position when manually editing the weight.
$("#weight{{widget.pk}}{{att}}").change(function() {
$("#slider{{widget.pk}}{{att}}").slider("value", $("#weight{{widget.pk}}{{att}}").val());
});
</script>
</td>
<td>
<input type="number" style="width:30px; display:inline;" name="weight{{widget.pk}}{{att}}" id="weight{{widget.pk}}{{att}}" value="0"/>
<div class="weight-slider" style="width:150px; float: left" id="slider-{{pk}}-{{count}}"></div>
<input type="number" style="width:40px; display:inline;" name="weight-{{pk}}-{{count}}" id="weight-{{pk}}-{{count}}" value="0"/>
</td>
</tr>
{% endwith %}
{% endfor %}
</tbody>
</table>
{% if unusable_attributes %}
<p>The following non-continues variables are unusable, as this model supports only continuous attributes.</p>
{% endif %}
{% for att in unusable_attributes %}
<ul>
<li>{{att}}</li>
</ul>
{% endfor %}
<input type="hidden" name="widget_id" value="{{widget.pk}}"/>
</form>
<br/>
<button id="normalizeButton{{widget.pk}}">Normalize weights!</button>
<button id="resetButton{{widget.pk}}">Reset!</button>
<script type="text/javascript">
// Data table code.
$('#weightSel{{widget.pk}}').dataTable( {
"bJQueryUI": true,
"bPaginate" : false,
"bSort" : false,
"bFilter" : false
});
// Normalization code.
$("#normalizeButton{{widget.pk}}").button().click(function() {
var weight_elements = $('[id^="weight{{widget.pk}}"]');
var weight_sum = 0;
weight_elements.each(function(index) {
weight_sum += parseFloat($(this).val());
});
if (weight_sum > 0) {
weight_elements.each(function(index) {
//round(100 * parseFloat($(this).val()) / weight_sum)
$(this).val(Math.round(100 * parseFloat($(this).val()) / weight_sum));
$(this).change();
});
<button id="normalize-button-{{widget.pk}}">Normalize weights!</button>
<button id="reset-button-{{widget.pk}}">Reset!</button>
</div>
<div id="edit-attribute-properties-{{widget.pk}}">
<form>
<fieldset>
<label for="direction">Maximize or minimize the attribute</label>
<select name="direction" id="dialog-att-direction-{{widget.pk}}" class="text ui-widget-content ui-corner-all">
<option value="max">Maximize</option>
<option value="min">Minimize</option>
</select>
<label for="lower-bound">Range lower bound</label>
<input type="text" name="lower-bound" id="dialog-att-lower-bound-{{widget.pk}}" value="" class="text ui-widget-content ui-corner-all" />
<label for="upper-bound">Range upper bound</label>
<input type="text" name="upper-bound" id="dialog-att-upper-bound-{{widget.pk}}" value="" class="text ui-widget-content ui-corner-all" />
</fieldset>
</form>
<div id="dialog-error-{{widget.pk}}" class="dialog-error">
</div>
</div>
<!-- Style -->
<style type="text/css">
.weight-slider {
float: left;
margin-right: 10px;
}
.dialog-error {
margin-top: 15px;
color: red;
}
</style>
<!-- Behaviour -->
<script type="text/javascript">
// Dialog
$('#edit-attribute-properties-{{widget.pk}}').dialog({
autoOpen: false,
resizable: false,
height: 300,
modal: true,
buttons: {
'Cancel' : function() {
$(this).dialog('close');
},
'Apply' : function() {
$(this).dialog('close');
var attId = $(this).data('attId');
// Get the edited options.
var dir_select = $('#dialog-att-direction-{{widget.pk}}')
.find('option:selected');
var direction = dir_select
.text();
var dir_value = dir_select
.val();
var rangeMin = $('#dialog-att-lower-bound-{{widget.pk}}')
.val();
var rangeMax = $('#dialog-att-upper-bound-{{widget.pk}}')
.val();
rangeMin = parseFloat(rangeMin);
rangeMax = parseFloat(rangeMax);
// Check for sane range values
if (isNaN(rangeMin) || isNaN(rangeMax)) {
$('#dialog-error-{{widget.pk}}').text('Problem: Illegal range value.');
$(this).dialog('open');
return;
} else if (rangeMin > rangeMax) {
$('#dialog-error-{{widget.pk}}').text('Problem: Lower range value is larger than the upper.');
$(this).dialog('open');
return;
}
// Save them
$('#display-direction-' + attId).text(direction);
$('#display-rangeMin-' + attId).text(rangeMin);
$('#display-rangeMax-' + attId).text(rangeMax);
$('#direction-' + attId).val(dir_value);
$('#rangeMin-' + attId).val(rangeMin);
$('#rangeMax-' + attId).val(rangeMax);
// Clear errors
$('#dialog-error-{{widget.pk}}').text('');
}
}
});
// Slider code.
{% for att, def_range in attributes %}
{% with count=forloop.counter0 pk=widget.pk %}
$('#edit-properties-{{pk}}-{{count}}').on('click', function(e){
e.preventDefault();
// Current values
var attId = '{{pk}}-{{count}}';
var direction = $('#display-direction-' + attId).text();
var rangeMin = $('#display-rangeMin-' + attId).text();
var rangeMax = $('#display-rangeMax-' + attId).text();
// Set the current values
$('#dialog-att-direction-{{pk}}')
.find('option')
.filter(function() {
return $(this).text() == direction;
})
.attr('selected', true);
$('#dialog-att-lower-bound-{{pk}}').val(rangeMin);
$('#dialog-att-upper-bound-{{pk}}').val(rangeMax);
// Show the dialog
$('#edit-attribute-properties-{{widget.pk}}')
.data('attId', attId)
.dialog('option', 'title', 'Properties of "{{att}}"')
.dialog('open');
});
$("#slider-{{pk}}-{{count}}").slider({
value:0,
min:0,
max:100,
slide: function( event, ui ) {
$("#weight-{{pk}}-{{count}}").val($("#slider-{{pk}}-{{count}}").slider("value"));
},
stop: function ( event, ui ) {
$("#weight-{{pk}}-{{count}}").val($("#slider-{{pk}}-{{count}}").slider("value"));
}
});
// Change the slider position when manually editing the weight.
$("#weight-{{pk}}-{{count}}").on('change', function() {
$("#slider-{{pk}}-{{count}}").slider("value", $("#weight-{{pk}}-{{count}}").val());
});
{% endwith %}
{% endfor %}
// Data table code.
$('#weight-sel-{{widget.pk}}').dataTable( {
"bJQueryUI": true,
"bPaginate" : false,
"bSort" : false,
"bFilter" : false
});
// Normalization code.
$("#normalize-button-{{widget.pk}}").button().click(function() {
var weight_elements = $('[id^="weight-{{widget.pk}}"]');
var weight_sum = 0;
weight_elements.each(function(index) {
weight_sum += parseFloat($(this).val());
});
// Reset button code.
$("#resetButton{{widget.pk}}").button().click(function() {
$('[id^="weight{{widget.pk}}"]').each(function() {
$(this).val(0);
if (weight_sum > 0) {
weight_elements.each(function(index) {
//round(100 * parseFloat($(this).val()) / weight_sum)
$(this).val(Math.round(100 * parseFloat($(this).val()) / weight_sum));
$(this).change();
});
}
});
// Reset button code.
$("#reset-button-{{widget.pk}}").button().click(function() {
$('[id^="weight-{{widget.pk}}"]').each(function() {
$(this).val(0);
$(this).change();
});
$('#attributeProperties{{widget.pk}}').editable(function(value, settings) {
// Update the hidden input value
var id = $(this).attr("id");
// Update the value
alteredCells[id] = value;
$('#alteredCells{{widget.pk}}').val(JSON.stringify(alteredCells));
// On edit just return the new value (no need to make a server call).
return value;
}, {
width: '100%'
});
</script>
</div>
});
</script>
</div>
\ No newline at end of file
......@@ -65,7 +65,7 @@
color: '#000000',
connectorColor: '#000000',
formatter: function() {
return '<b>'+ this.point.name +'</b>: '+ this.percentage +' %';
return '<b>'+ this.point.name +'</b>: '+ this.percentage.toFixed(3) +' %';
}
}
}
......@@ -108,7 +108,7 @@
tooltip: {
formatter: function() {
return ''+
this.series.name +': '+ this.y + '%';
this.series.name +': '+ this.y.toFixed(3) + '%';
}
},
plotOptions: {
......@@ -155,7 +155,7 @@
tooltip: {
formatter: function() {
return ''+
this.series.name +': '+ this.y;
this.series.name +': '+ this.y.toFixed(3);
}
},
plotOptions: {
......@@ -201,7 +201,7 @@
tooltip: {
formatter: function() {
return ''+
this.series.name +': '+ this.y ;
this.series.name +': '+ this.y.toFixed(3);
}
},
credits: {
......
......@@ -4,7 +4,7 @@ Select the attribute to perform sensitivity analysis on.<br><br>
<form>
<div id="radio" style="margin-bottom: 10px; text-align: center;">
{% for att in attributes %}
<input type="radio" class="target_att" name="target{{widget.pk}}" id="target{{widget.pk}}{{att}}" value="{{att}}" {% if forloop.first %}checked="checked"{% endif %}> <label for="target{{widget.pk}}{{att}}">{{att}}</label></input>
<input type="radio" class="target_att" name="target{{widget.pk}}" id="target{{widget.pk}}{{forloop.counter0}}" value="{{att}}" {% if forloop.first %}checked="checked"{% endif %}> <label for="target{{widget.pk}}{{forloop.counter0}}">{{att}}</label></input>
{% endfor %}
</div>
<input type="hidden" name="widget_id" value="{{widget.pk}}"/>
......
......@@ -5,6 +5,7 @@ Decision support visualization views.
'''
from django.shortcuts import render
import json
from collections import defaultdict
def decision_support_sensitivity_analysis_viewer(request, input_dict, output_dict, widget):
'''
......@@ -13,24 +14,27 @@ def decision_support_sensitivity_analysis_viewer(request, input_dict, output_dic
@author: Anze Vavpetic, 2012
'''
model = input_dict['model']
attributes = [att.name for att in input_dict['model'].data.domain.features]
attributes = sorted(model.ranges.keys())
data_points = {}
domain = range(0, 101, 10)
# Compute for each attribute
for target_att in attributes:
y, ex_data = [], {}
# For collecting scores for each example across different weights
for ex in model.data:
ex_data[ex['label'].value] = []
alt_scores = []
ex_data = defaultdict(list)
weights = dict(model.user_weights)
# Compute the scores for each weight
for w in domain:
model.weights[target_att] = w
ds = model()
for ex in ds:
ex_data[ex['label'].value].append([w, ex['score'].value])
for ex in model.data:
y.append({'name' : ex['label'].value, 'data' : ex_data[ex['label'].value]})
data_points[target_att] = y
for w in range(0, 101, 10):
weights[target_att] = w
for idx, ex in enumerate(model(weights=weights)):
label = model.label(idx)
ex_data[label].append([w, ex['score'].value])
for idx, ex in enumerate(model.data):
label = model.label(idx)
data = ex_data[label]
alt_scores.append({'name' : label, 'data' : data})
data_points[target_att] = alt_scores
return render(request, 'visualizations/sensitivity_analysis.html',
{'widget' : widget,
......@@ -46,15 +50,15 @@ def decision_support_charts_viewer(request, input_dict, output_dict, widget):
@author: Anze Vavpetic, 2012
'''
model = input_dict['model']
norm_data = model()
weight_shares = [ [att, weight] for att, weight in model.weights.items() ]
attributes = sorted(model.weights.keys())
alternatives = [ex['label'].value for ex in norm_data]
weights_bar = [{ 'data' : [model.weights[att] for att in attributes] }]
values_column = [{ 'data' : [ex['score'].value for ex in norm_data] }]
alt_data = [{'name' : ex['label'].value,
scores = model()
weight_shares = model.user_weights.items()
attributes = sorted(model.ranges.keys())
alternatives = [model.label(idx) for idx, ex in enumerate(scores)]
weights_bar = [{'data' : [model.user_weights[att] for att in attributes]}]
values_column = [{'data' : [ex['score'].value for ex in scores] }]
alt_data = [{'name' : model.label(idx),
'data' : [ex[att].value for att in attributes]}
for ex in norm_data ]
for idx, ex in enumerate(scores)]
return render(request, 'visualizations/ds_charts.html',
{'widget' : widget,
'model_name' : model.name,
......
import orange
class WeightedSumModel:
'''
Weighted sum decision support model.
'''
def __init__(self, data, user_weights=None, minimize=None, ranges=None):
self.data = data
self.user_weights = user_weights
self.minimize = minimize if minimize else set()
self.ranges = ranges if ranges else self.default_ranges()
self.unusable = self.unusable_attributes()
self.labels = self.generate_labels()
self.name = 'Weighted sum model'
def default_ranges(self):
"""Calculates the default ranges of the attributes"""
data_np = self.data.to_numpy()[0]
ranges = {}
for i, att in enumerate(self.data.domain.features):
if att.varType == orange.VarTypes.Continuous:
ranges[att.name] = (min(data_np[:,i]), max(data_np[:,i]))
return ranges
def unusable_attributes(self):
unusable = []
for att in self.data.domain.features:
if att.varType != orange.VarTypes.Continuous:
unusable.append(att.name)
return unusable
def generate_labels(self):
labels = []
has_labels = 'label' in self.data.domain
for idx, ex in enumerate(self.data):
if has_labels:
labels.append(ex['label'].value)
else:
labels.append(idx)
return labels
def label(self, idx):
return self.labels[idx]
def __call__(self, weights=None):
if not weights:
weights = self.user_weights
# New augmented table
norm_data = orange.ExampleTable(self.data)
newid = min(norm_data.domain.get_metas().keys(),0) - 1
score_attr = orange.FloatVariable('score')
norm_data.domain.add_meta(newid, score_attr)
norm_data.add_meta_attribute(score_attr)
# Normalize the attributes column-wise
for att, (lower_bound, upper_bound) in self.ranges.items():
for ex in norm_data:
ex[att] = ex[att] / (upper_bound - lower_bound)
# Use the inverse of an attr. value if smaller values should be treated as 'better'.
inverse = lambda x, att: 1-x if att in self.minimize else x
for ex in norm_data:
score = sum([inverse(ex[att], att) * weights.get(att, 1)
for att in self.ranges.keys()])
ex['score'] = score
return norm_data
\ No newline at end of file
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment