def get_discordance(comparables_a, comparables_perf_a, comparables_b, comparables_perf_b, criteria, thresholds, pref_directions, use_pre_veto): # 'partial' in this function's name has nothing to do w/ functools.partial def _get_partial_discordance(x, y, criterion, use_pre_veto): _omega = partial(omega, pref_directions, criterion) _get_linear = partial(get_linear, pref_directions, criterion, x, y) p = _get_linear(thresholds[criterion].get('preference', 0)) v = _get_linear(thresholds[criterion].get('veto')) cv = _get_linear(thresholds[criterion].get('counter_veto')) crossed = False # crossed 'counter_veto' threshold if not v: return (0.0, crossed) if cv is not None and _omega(x, y) > cv: crossed = True if use_pre_veto: # originally (i.e. w/o pre_veto) pv == p pv = _get_linear(thresholds[criterion].get('pre_veto', p)) if _omega(x, y) > -pv: pd = 0.0 elif _omega(x, y) <= -v: pd = 1.0 else: pd = (_omega(y, x) - pv) / (v - pv) else: if _omega(x, y) >= -p: pd = 0.0 elif _omega(x, y) < -v: pd = 1.0 else: pd = (_omega(x, y) + p) / (p - v) return (pd, crossed) two_way_comparison = True if comparables_a != comparables_b else False partial_discordance = Vividict() cv_crossed = Vividict() for a in comparables_a: for b in comparables_b: for criterion in criteria: x = comparables_perf_a[a][criterion] y = comparables_perf_b[b][criterion] pd, crossed = _get_partial_discordance(x, y, criterion, use_pre_veto) partial_discordance[a][b][criterion] = pd cv_crossed[a][b][criterion] = crossed if two_way_comparison: x = comparables_perf_b[b][criterion] y = comparables_perf_a[a][criterion] pd, crossed = _get_partial_discordance( x, y, criterion, use_pre_veto) partial_discordance[b][a][criterion] = pd cv_crossed[b][a][criterion] = crossed return partial_discordance, cv_crossed
def get_concordance(comparables_a, comparables_perf_a, comparables_b, comparables_perf_b, criteria, thresholds, pref_directions, weights): # 'partial' in this function's name has nothing to do w/ functools.partial def _get_partial_concordance(x, y, criterion): _omega = partial(omega, pref_directions, criterion) _get_linear = partial(get_linear, pref_directions, criterion, x, y) p = _get_linear(thresholds[criterion].get('preference', 0)) q = _get_linear(thresholds[criterion].get('indifference', 0)) if _omega(x, y) < -p: return 0.0 elif _omega(x, y) >= -q: return 1.0 else: return (_omega(x, y) + p) / (p - q) def _get_aggregated_concordance(x, y): sum_of_weights = sum([weights[criterion] for criterion in criteria]) s = sum([ weights[criterion] * partial_concordances[x][y][criterion] for criterion in criteria ]) concordance = s / sum_of_weights return concordance two_way_comparison = True if comparables_a != comparables_b else False # compute partial concordances partial_concordances = Vividict() for a in comparables_a: for b in comparables_b: for criterion in criteria: pc = _get_partial_concordance(comparables_perf_a[a][criterion], comparables_perf_b[b][criterion], criterion) partial_concordances[a][b][criterion] = pc if two_way_comparison: pc = _get_partial_concordance( comparables_perf_b[b][criterion], comparables_perf_a[a][criterion], criterion, ) partial_concordances[b][a][criterion] = pc # aggregate partial concordances aggregated_concordances = Vividict() for a in comparables_a: for b in comparables_b: aggregated_concordances[a][b] = _get_aggregated_concordance(a, b) if two_way_comparison: aggregated_concordances[b][a] = _get_aggregated_concordance( b, a) return aggregated_concordances
def assign_class(alternatives, categories_rank, categories_profiles, outranking): # sort categories by their rank, but we want the worst one on the 'left' # - hence 'reverse=True' categories = [i[0] for i in sorted(categories_rank.items(), key=lambda x: x[1], reverse=True)] exploitation = Vividict() for alternative in alternatives: # conjuctive ('pessimistic' - from 'best' to 'worst') conjuctive_idx = 0 for profile_idx, profile in list(enumerate(categories_profiles))[::-1]: relation = get_relation_type(alternative, profile, outranking) if relation in ('indifference', 'preference'): conjuctive_idx = profile_idx + 1 break else: continue # disjunctive ('optimistic' - from 'worst' to 'best') disjunctive_idx = len(categories_profiles) for profile_idx, profile in enumerate(categories_profiles): relation = get_relation_type(profile, alternative, outranking) if relation == 'preference': disjunctive_idx = profile_idx break else: continue exploitation[alternative] = (categories[conjuctive_idx], categories[disjunctive_idx]) return exploitation
def get_outranking(comparables_a, comparables_b, credibility, cut_threshold): outranking = Vividict() for a in comparables_a: for b in comparables_b: if credibility[a][b] >= cut_threshold: outranking[a][b] = True if credibility[b][a] >= cut_threshold: outranking[b][a] = True return outranking
def get_discordance(comparables_a, comparables_perf_a, comparables_b, comparables_perf_b, criteria, thresholds, pref_directions): def _get_partial_discordances(x, y, criterion): v = get_linear(pref_directions, criterion, x, y, thresholds[criterion].get('veto')) if v is None: d = 0 elif pref_directions[criterion] == 'max': d = 0 if (y < x + v) else 1 else: d = 0 if (y > x - v) else 1 return d def _get_aggregated_discordance(x, y): d_aggregated = 0 for d_partial in partial_discordances[x][y].itervalues(): if d_partial == 1: d_aggregated = 1 break return d_aggregated two_way_comparison = True if comparables_a != comparables_b else False partial_discordances = Vividict() for a in comparables_a: for b in comparables_b: for criterion in criteria: pc = _get_partial_discordances( comparables_perf_a[a][criterion], comparables_perf_b[b][criterion], criterion) partial_discordances[a][b][criterion] = pc if two_way_comparison: pc = _get_partial_discordances( comparables_perf_b[b][criterion], comparables_perf_a[a][criterion], criterion) partial_discordances[b][a][criterion] = pc discordance = Vividict() for a in comparables_a: for b in comparables_b: discordance[a][b] = _get_aggregated_discordance(a, b) if two_way_comparison: discordance[b][a] = _get_aggregated_discordance(b, a) return discordance, partial_discordances
def get_credibility(comparables_a, comparables_b, concordance, discordance, with_denominator, only_max_discordance, cv_crossed): def _get_credibility_idx(x, y, num_crossed, only_max_discordance): discordance_values = discordance[x][y].values() if set(discordance_values) == set([0]): # only zeros c_idx = concordance[x][y] elif 1 in discordance_values: # at least one '1' if not concordance[x][y] < 1: raise RuntimeError("When discordance == 1, " "concordance must be < 1.") c_idx = 0.0 elif only_max_discordance and not with_denominator: c_idx = concordance[x][y] * (1 - max(discordance_values)) else: factors = [] for d in discordance_values: if with_denominator: if d > concordance[x][y]: factor = (1 - d) / (1 - concordance[x][y]) else: factor = None else: factor = (1 - d) if factor: factors.append(factor) if factors == []: c_idx = concordance[x][y] else: discordance_aggr = reduce(lambda f1, f2: f1 * f2, factors) c_idx = (concordance[x][y] * discordance_aggr**(1 - num_crossed / num_total)) return c_idx two_way_comparison = True if comparables_a != comparables_b else False # 'num_total' == total number of criteria. # Instead of this monstrosity below, maybe it would be better to provide # 'criteria.xml' as another input..? num_total = len(discordance.values()[0].values()[0].keys()) credibility = Vividict() for a in comparables_a: for b in comparables_b: num_crossed = len(cv_crossed[a][b]) credibility[a][b] = _get_credibility_idx(a, b, num_crossed, only_max_discordance) if two_way_comparison: credibility[b][a] = _get_credibility_idx( b, a, num_crossed, only_max_discordance) return credibility
def get_credibility(comparables_a, comparables_perf_a, comparables_b, comparables_perf_b, criteria, pref_directions, thresholds): """np, nq, ni, no - number of criteria where: n_p(a, b) - 'a' is strictly preferred over 'b' n_q(a, b) - 'a' is weakly preferred over 'b' n_i(a, b) - 'a' is indifferent than 'b', but 'a' has better performance n_o(a, b) - 'a' is indifferent than 'b' and both have the same performances """ def _check_diff(diff, criterion): if ((pref_directions[criterion] == 'max' and diff > 0) or (pref_directions[criterion] == 'min' and diff < 0)): diff = abs(diff) elif diff == 0: pass else: diff = None return diff def _check_for_veto(performances_x, performances_y): veto = False for c in criteria: if thresholds[c].get('veto') is None: continue diff = _check_diff(performances_x[c] - performances_y[c], c) if diff > thresholds[c]['veto']: veto = True break return veto def _get_criteria_counts(x, y, performances_x, performances_y): np = nq = ni = no = 0 for c in criteria: diff = _check_diff(performances_x[c] - performances_y[c], c) if diff: if diff >= thresholds[c].get('preference', 0): # diff >= p np += 1 elif diff > thresholds[c].get('indifference', 0): # q > diff < p nq += 1 else: # diff <= q ni += 1 elif diff == 0: no += 1 return {'np': np, 'nq': nq, 'ni': ni, 'no': no} def _get_cred_values(comparables_x, comparables_y, comparables_perf_x, comparables_perf_y, credibility): for x in comparables_x: for y in comparables_y: if x == y: credibility[x][y] = 1.0 continue # let's abbreviate these two for convenience cc_xy = criteria_counts[x][y] cc_yx = criteria_counts[y][x] if (cc_yx['np'] + cc_yx['nq'] == 0 and cc_yx['ni'] < cc_xy['np'] + cc_xy['nq'] + cc_xy['ni']): credibility[x][y] = 1.0 continue elif cc_yx['np'] == 0: sum_cc_yx = cc_yx['nq'] + cc_yx['ni'] sum_cc_xy = cc_xy['np'] + cc_xy['nq'] + cc_xy['ni'] if cc_yx['nq'] <= cc_xy['np'] and sum_cc_yx < sum_cc_xy: credibility[x][y] = 0.8 continue elif cc_yx['nq'] <= cc_xy['np'] + cc_xy['nq']: credibility[x][y] = 0.6 continue else: credibility[x][y] = 0.4 elif cc_yx['np'] <= 1 and cc_xy['np'] >= len(criteria) // 2: veto = _check_for_veto(comparables_perf_y[y], comparables_perf_x[x]) if not veto: credibility[x][y] = 0.2 continue else: credibility[x][y] = 0.0 continue else: credibility[x][y] = 0.0 continue return credibility two_way_comparison = True if comparables_a != comparables_b else False criteria_counts = Vividict() for a in comparables_a: for b in comparables_b: cc = _get_criteria_counts(a, b, comparables_perf_a[a], comparables_perf_b[b]) criteria_counts[a][b] = cc if two_way_comparison: cc = _get_criteria_counts(b, a, comparables_perf_b[b], comparables_perf_a[a]) criteria_counts[b][a] = cc credibility = Vividict() credibility = _get_cred_values(comparables_a, comparables_b, comparables_perf_a, comparables_perf_b, credibility) if two_way_comparison: credibility = _get_cred_values(comparables_b, comparables_a, comparables_perf_b, comparables_perf_a, credibility) return credibility
def get_concordance(comparables_a, comparables_perf_a, comparables_b, comparables_perf_b, criteria, thresholds, pref_directions, weights, reinforcement_factors): # 'partial' in this function's name has nothing to do w/ functools.partial def _get_partial_concordance(x, y, criterion): _omega = partial(omega, pref_directions, criterion) _get_linear = partial(get_linear, pref_directions, criterion, x, y) p = _get_linear(thresholds[criterion].get('preference', 0)) q = _get_linear(thresholds[criterion].get('indifference', 0)) rp = _get_linear(thresholds[criterion].get('reinforced_preference')) crossed = False # crossed 'reinforced_preference' threshold if rp is not None and _omega(x, y) > rp: crossed = True if _omega(x, y) < -p: pc = 0.0 elif _omega(x, y) >= -q: pc = 1.0 else: pc = (_omega(x, y) + p) / (p - q) return (pc, crossed) def _get_aggregated_concordance(x, y, rp_crossed): sum_of_weights = sum([ weights[criterion] * rp_crossed.get((x, y, criterion), 1) for criterion in criteria ]) s = sum([ weights[criterion] * rp_crossed.get( (x, y, criterion), 1) * partial_concordances[x][y][criterion] for criterion in criteria ]) concordance = s / sum_of_weights return concordance two_way_comparison = True if comparables_a != comparables_b else False # compute partial concordances partial_concordances = Vividict() rp_crossed = {} for a in comparables_a: for b in comparables_b: for criterion in criteria: x = comparables_perf_a[a][criterion] y = comparables_perf_b[b][criterion] pc, crossed = _get_partial_concordance(x, y, criterion) if crossed: # it may be better to just throw an error here if there's # no reinforcement factor defined (although using '1' as a # default value makes sense too) rf = reinforcement_factors.get(criterion, 1) rp_crossed.update({(a, b, criterion): rf}) partial_concordances[a][b][criterion] = pc if two_way_comparison: x = comparables_perf_b[b][criterion] y = comparables_perf_a[a][criterion] pc, crossed = _get_partial_concordance(x, y, criterion) if crossed: rf = reinforcement_factors.get(criterion, 1) rp_crossed.update({(b, a, criterion): rf}) partial_concordances[b][a][criterion] = pc # aggregate partial concordances aggregated_concordances = Vividict() for a in comparables_a: for b in comparables_b: C = _get_aggregated_concordance(a, b, rp_crossed) aggregated_concordances[a][b] = C if two_way_comparison: C = _get_aggregated_concordance(b, a, rp_crossed) aggregated_concordances[b][a] = C return aggregated_concordances
def get_concordance(comparables_a, comparables_perf_a, comparables_b, comparables_perf_b, criteria, thresholds, pref_directions, weights, interactions, z_function): def _check_net_balance(interactions, weights): int_weak = interactions.get('weakening', []) int_antag = interactions.get('antagonistic', []) int_chained = chain(int_weak, int_antag) criteria_affected = set([i[0] for i in int_chained]) for criterion in criteria_affected: weak_sum = sum([abs(i[2]) for i in int_weak if i[0] == criterion]) antag_sum = sum([i[2] for i in int_antag if i[0] == criterion]) net_balance = weights[criterion] - weak_sum + antag_sum if net_balance <= 0: raise RuntimeError( "Positive net balance condition is not " "fulfilled for criterion '{}'.".format(criterion)) # XXX this function is exactly the same as in ElectreConcordance # 'partial' in this function's name has nothing to do w/ functools.partial def _get_partial_concordance(x, y, criterion): _omega = partial(omega, pref_directions, criterion) _get_linear = partial(get_linear, pref_directions, criterion, x, y) p = _get_linear(thresholds[criterion].get('preference', 0)) q = _get_linear(thresholds[criterion].get('indifference', 0)) if _omega(x, y) < -p: return 0.0 elif _omega(x, y) >= -q: return 1.0 else: return (_omega(x, y) + p) / (p - q) # I don't like those cryptic variables' names here (ch, ci, cj, _cki, # _ckj, _kij, _kih) - they all come from math equations def _get_aggregated_concordance(x, y): if x == y: aggregated_concordance = 1.0 else: sum_cki = sum( [partial_concordances[x][y][c] * weights[c] for c in criteria]) sum_kij = float(0) for interaction_name in ('strengthening', 'weakening'): for interaction in interactions.get(interaction_name, []): ci = partial_concordances[x][y][interaction[0]] cj = partial_concordances[x][y][interaction[1]] sum_kij += Z(ci, cj) * interaction[2] sum_kih = 0.0 for interaction in interactions.get('antagonistic', []): ci = partial_concordances[x][y][interaction[0]] ch = partial_concordances[y][x][interaction[1]] sum_kih += Z(ci, ch) * interaction[2] sum_ki = sum(weights.values()) K = sum_ki + sum_kij - sum_kih aggregated_concordance = (sum_cki + sum_kij - sum_kih) / K return aggregated_concordance # some initial checks _check_net_balance(interactions, weights) if z_function == 'multiplication': Z = lambda x, y: x * y elif z_function == 'minimum': Z = lambda x, y: min(x, y) else: raise RuntimeError("Invalid Z function: '{}'.".format(z_function)) # XXX this block below is exactly the same as in ElectreConcordance two_way_comparison = True if comparables_a != comparables_b else False # compute partial concordances partial_concordances = Vividict() for a in comparables_a: for b in comparables_b: for criterion in criteria: pc = _get_partial_concordance(comparables_perf_a[a][criterion], comparables_perf_b[b][criterion], criterion) partial_concordances[a][b][criterion] = pc if two_way_comparison: pc = _get_partial_concordance( comparables_perf_b[b][criterion], comparables_perf_a[a][criterion], criterion) partial_concordances[b][a][criterion] = pc # aggregate partial concordances aggregated_concordances = Vividict() for a in comparables_a: for b in comparables_b: aggregated_concordances[a][b] = _get_aggregated_concordance(a, b) if two_way_comparison: aggregated_concordances[b][a] = _get_aggregated_concordance( b, a) return aggregated_concordances