Skip to content

Commit

Permalink
concordance and discordance
Browse files Browse the repository at this point in the history
  • Loading branch information
leliel12 committed Feb 21, 2016
1 parent 17489b0 commit 8b10b66
Show file tree
Hide file tree
Showing 3 changed files with 262 additions and 1 deletion.
33 changes: 32 additions & 1 deletion skcriteria/common/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,40 @@ def criteriarr(criteria):

def is_mtx(mtx, size=None):
try:
a, b = mtx.shape
a, b = mtx.shapeW
if size and (a, b) != size:
return False
except:
return False
return True


def nearest(array, value, side=None):
# based on: http://stackoverflow.com/a/2566508
# http://stackoverflow.com/a/3230123
# http://stackoverflow.com/a/17119267
if side not in (None, "gt", "lt"):
msg = "'side' must be None, 'gt' or 'lt'. Found {}".format(side)
raise ValueError(msg)

raveled = np.ravel(array)
cleaned = raveled[~np.isnan(raveled)]

if side is None:
idx = np.argmin(np.abs(cleaned-value))

else:
masker, decisor = (
(np.ma.less_equal, np.argmin)
if side == "gt" else
(np.ma.greater_equal, np.argmax))

diff = cleaned - value
mask = masker(diff, 0)
if np.all(mask):
return None

masked_diff = np.ma.masked_array(diff, mask)
idx = decisor(masked_diff)

return cleaned[idx]
108 changes: 108 additions & 0 deletions skcriteria/electre.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-

# License: 3 Clause BSD
# http://scikit-criteria.org/


# =============================================================================
# FUTURE
# =============================================================================

from __future__ import unicode_literals


__doc__ = """
Data from Tzeng and Huang et al, 2011 [TZENG2011]_
References
----------
.. [TZENG2011] Tzeng, G. H., & Huang, J. J. (2011). Multiple
attribute decision making: methods and applications. CRC press.
"""


# =============================================================================
# IMPORTS
# =============================================================================

import numpy as np

from skcriteria.common import norm, util, rank


# =============================================================================
# UTILS
# =============================================================================

def concordance(nmtx, ncriteria, nweights):

mtx_criteria = np.tile(ncriteria, (len(nmtx), 1))
mtx_weight = np.tile(nweights, (len(nmtx), 1))
mtx_concordance = np.empty((len(nmtx), len(nmtx)))

for idx, row in enumerate(nmtx):
difference = row - nmtx
outrank = (
((mtx_criteria == util.MAX) & (difference >= 0)) |
((mtx_criteria == util.MIN) & (difference <= 0))
)
filter_weights = mtx_weight * outrank.astype(int)
new_row = np.sum(filter_weights, axis=1)
mtx_concordance[idx] = new_row

np.fill_diagonal(mtx_concordance, np.nan)
mean = np.nanmean(mtx_concordance)
p = util.nearest(mtx_concordance, mean, side="gt")

return mtx_concordance, mean, p


def discordance(nmtx, ncriteria, nweights):

mtx_criteria = np.tile(ncriteria, (len(nmtx), 1))
mtx_weight = np.tile(nweights, (len(nmtx), 1))
mtx_discordance = np.empty((len(nmtx), len(nmtx)))
ranges = np.max(nmtx, axis=0) - np.min(nmtx, axis=0)

for idx, row in enumerate(nmtx):
difference = nmtx - row
worsts = (
((mtx_criteria == util.MAX) & (difference >= 0)) |
((mtx_criteria == util.MIN) & (difference <= 0))
)
filter_difference = np.abs(difference * worsts)
delta = filter_difference / ranges
new_row = np.max(delta, axis=1)
mtx_discordance[idx] = new_row

np.fill_diagonal(mtx_discordance, np.nan)
mean = np.nanmean(mtx_discordance)
q = util.nearest(mtx_discordance, mean, side="lt")

return mtx_discordance, mean, q


# =============================================================================
# ELECTRE
# =============================================================================

def electre1(mtx, criteria, weights=1):

# This guarantee the criteria array consistency
ncriteria = util.criteriarr(criteria)

# validate the matrix is the matrix
nmtx = np.asarray(mtx)
if not util.is_mtx(nmtx):
raise ValueError("'mtx' is not a matrix")

# normalize weights
nweights = norm.sum(weights) if weights is not None else 1

# get the concordance matrix
mtx_concordance = concordance(nmtx, nweights)

122 changes: 122 additions & 0 deletions skcriteria/tests/test_electre.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-

# License: 3 Clause BSD
# http://scikit-criteria.org/


# =============================================================================
# FUTURE
# =============================================================================

from __future__ import unicode_literals


# =============================================================================
# DOC
# =============================================================================

__doc__ = """test electre methods"""


# =============================================================================
# IMPORTS
# =============================================================================

import random

import numpy as np

from . import core

from ..common import norm, util
from .. import electre


# =============================================================================
# BASE CLASS
# =============================================================================

class ElectreTest(core.SKCriteriaTestCase):

def test_concordance(self):

# Data From:
# Cebrián, L. I. G., & Porcar, A. M. (2009). Localización empresarial
# en Aragón: Una aplicación empírica de la ayuda a la decisión
# multicriterio tipo ELECTRE I y III. Robustez de los resultados
# obtenidos.
# Revista de Métodos Cuantitativos para la Economía y la Empresa,
# (7), 31-56.
nmtx = norm.sum([
[6, 5, 28, 5, 5],
[4, 2, 25, 10, 9],
[5, 7, 35, 9, 6],
[6, 1, 27, 6, 7],
[6, 8, 30, 7, 9],
[5, 6, 26, 4, 8]
], axis=0)
ncriteria = util.criteriarr([1, 1, -1, 1, 1])
nweights = norm.sum([0.25, 0.25, 0.1, 0.2, 0.2])
results = [
[np.nan, 0.5000, 0.3500, 0.5000, 0.3500, 0.4500],
[0.5000, np.nan, 0.5000, 0.7500, 0.5000, 0.5000],
[0.6500, 0.5000, np.nan, 0.4500, 0.2000, 0.7000],
[0.7500, 0.2500, 0.5500, np.nan, 0.3500, 0.4500],
[0.9000, 0.7000, 0.8000, 0.9000, np.nan, 0.9000],
[0.5500, 0.5000, 0.5500, 0.5500, 0.1000, np.nan]
]
result_mean, result_p = 0.5400, 0.5500
concordance, mean, p = electre.concordance(nmtx, ncriteria, nweights)
self.assertAllClose(concordance, results, atol=1.e-3)
self.assertAllClose(mean, result_mean, atol=1.e-3)
self.assertAllClose(p, result_p, atol=1.e-3)

def test_discordance(self):
# Data From:
# Cebrián, L. I. G., & Porcar, A. M. (2009). Localización empresarial
# en Aragón: Una aplicación empírica de la ayuda a la decisión
# multicriterio tipo ELECTRE I y III. Robustez de los resultados
# obtenidos.
# Revista de Métodos Cuantitativos para la Economía y la Empresa,
# (7), 31-56.
nmtx = norm.sum([
[6, 5, 28, 5, 5],
[4, 2, 25, 10, 9],
[5, 7, 35, 9, 6],
[6, 1, 27, 6, 7],
[6, 8, 30, 7, 9],
[5, 6, 26, 4, 8]
], axis=0)
ncriteria = util.criteriarr([1, 1, -1, 1, 1])
nweights = norm.sum([0.25, 0.25, 0.1, 0.2, 0.2])
results = [
[np.nan, 1.0000, 0.6667, 0.5000, 1.0000, 0.7500],
[1.0000, np.nan, 0.7143, 1.0000, 1.0000, 0.5714],
[0.7000, 1.0000, np.nan, 0.8000, 0.7500, 0.9000],
[0.5714, 0.6667, 0.8571, np.nan, 1.0000, 0.7143],
[0.2000, 0.5000, 0.3333, 0.3000, np.nan, 0.4000],
[0.5000, 1.0000, 0.8333, 0.5000, 0.5000, np.nan]
]
result_mean, result_p = 0.7076, 0.70
discordance, mean, p = electre.discordance(nmtx, ncriteria, nweights)
self.assertAllClose(discordance, results, atol=1.e-3)
self.assertAllClose(mean, result_mean, atol=1.e-3)
self.assertAllClose(p, result_p, atol=1.e-3)

def _test_electre1(self):
#~ Data from:
#~ Tzeng, G. H., & Huang, J. J. (2011). Multiple
#~ attribute decision making: methods and applications. CRC press.
VD, D, U, S, VS = 1, 2, 3, 4, 5
mtx = [
[U, D, S, VS],
[D, VS, D, S],
[D, VD, S, D],
[U, VS, U, S]
]
criteria = [1, 1, 1, 1]
weights = [.35, .15, .20, .30]
electre.electre1(mtx, criteria, weights)


0 comments on commit 8b10b66

Please sign in to comment.