Skip to content

Commit 1967c60

Browse files
committed
(#66) Implement SPOTIS method
1 parent f9e5004 commit 1967c60

7 files changed

Lines changed: 198 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
### 0.8.4 (Upcoming release)
22

33
- Simplify code and documentation of CRADIS method.
4-
4+
- Implement SPOTIS method
55

66
### 0.8.3
77

docs/src/mcdms.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,4 +207,10 @@ JMcDM.ram
207207
## CRADIS
208208
```@docs
209209
JMcDM.cradis
210-
```
210+
```
211+
212+
## SPOTIS
213+
```@docs
214+
JMcDM.spotis
215+
```
216+

src/JMcDM.jl

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ include("lopcow.jl")
120120
include("ocra.jl")
121121
include("lmaw.jl")
122122
include("todim.jl")
123+
include("spotis.jl")
123124

124125
include("summary.jl")
125126

@@ -140,6 +141,7 @@ include("fuzzy/fuzzy.jl")
140141

141142
# imports from modules
142143
import .Topsis: topsis, TopsisMethod, TopsisResult
144+
import .SPOTIS: spotis, SpotisMethod, SPOTISResult
143145
import .WPM: wpm, WPMResult, WPMMethod
144146
import .WASPAS: waspas, WASPASResult, WaspasMethod
145147
import .VIKOR: vikor, VikorMethod, VikorResult
@@ -222,6 +224,7 @@ export LMAWMethod
222224
export TODIMMethod
223225
export RAMMethod
224226
export CradisMethod
227+
export SpotisMethod
225228

226229

227230
export MCDMSetting
@@ -262,6 +265,7 @@ export LMAWResult
262265
export TODIMResult
263266
export CILOSResult
264267
export IDOCRIWResult
268+
export SPOTISResult
265269
export RAMResult
266270
export CRADISResult
267271

@@ -325,6 +329,7 @@ export idocriw
325329
export ram
326330
export borda
327331
export cradis
332+
export spotis
328333

329334
#  export SCDM tools
330335
export laplace

src/mcdm.jl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,8 @@ function mcdm(
118118
lmaw(df, w, fns)
119119
elseif method isa PIVMethod
120120
piv(df, w, fns)
121+
elseif method isa SpotisMethod
122+
spotis(df, w, fns, lowerbounds=method.lowerbounds, upperbounds=method.upperbounds)
121123
else
122124
error("Method is not defined")
123125
end

src/spotis.jl

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
module SPOTIS
2+
3+
4+
import ..MCDMMethod, ..MCDMResult, ..MCDMSetting
5+
import ..Normalizations
6+
7+
using ..Utilities: weightise, colmins, colmaxs, rowsums, unitize
8+
9+
export spotis, SpotisMethod, SPOTISResult
10+
11+
struct SpotisMethod <: MCDMMethod
12+
lowerbounds::Vector{Float64}
13+
upperbounds::Vector{Float64}
14+
end
15+
16+
struct SPOTISResult <: MCDMResult
17+
decisionMat::Matrix
18+
normalizedDecisionMat::Matrix
19+
weights::Vector{Float64}
20+
weightedNormalizedDecisionMat::Matrix{Float64}
21+
scores::Vector{Float64}
22+
ranking::Vector{Int}
23+
bestIndex::Int
24+
end
25+
26+
27+
"""
28+
spotis(decisionMat, weights, dirs; lowerbounds=nothing, upperbounds=nothing)
29+
30+
# Description
31+
32+
Implements the SPOTIS Rank Reversal Free Method for Multi-Criteria Decision-Making Support
33+
34+
# Arguments
35+
36+
- `decisionMat::Matrix`: The decision matrix in type of Matrix.
37+
- `weights::Array{Float64,1}`: Array of weights for each criterion.
38+
- `dirs::Array{<:Function,1}`: Array of functions. The elements are either minimum or maximum.
39+
- `lowerbounds::Union{Nothing, Array{Float64,1}}`: Optional array of lower bounds for each criterion. If not provided, the minimum values of the decision matrix will be used.
40+
- `upperbounds::Union{Nothing, Array{Float64,1}}`: Optional array of upper bounds for each criterion. If not provided, the maximum values of the decision matrix will be used.
41+
42+
# Reference:
43+
44+
- Dezert, Jean, et al. "The SPOTIS rank reversal free method for multi-criteria decision-making support." 2020 IEEE 23rd international conference on information fusion (FUSION). IEEE, 2020.
45+
"""
46+
function spotis(
47+
decisionMat::Matrix,
48+
weights::Array{Float64,1},
49+
dirs::Array{F,1};
50+
lowerbounds::Union{Nothing, Array{Float64,1}}=nothing,
51+
upperbounds::Union{Nothing, Array{Float64,1}}=nothing)::SPOTISResult where {F<:Function}
52+
53+
nalternatives, ncriteria = size(decisionMat)
54+
55+
if length(weights) != ncriteria
56+
error("Length of weights must be equal to number of criteria")
57+
end
58+
59+
if length(dirs) != ncriteria
60+
error("Length of dirs must be equal to number of criteria")
61+
end
62+
63+
w = unitize(weights)
64+
65+
if lowerbounds === nothing
66+
lowerbounds = colmins(decisionMat)
67+
end
68+
if upperbounds === nothing
69+
upperbounds = colmaxs(decisionMat)
70+
end
71+
72+
if length(lowerbounds) != ncriteria || length(upperbounds) != ncriteria
73+
error("Length of lowerbounds/upperbounds must be equal to number of criteria")
74+
end
75+
76+
normalizedDecisionMat = Array{Float64}(undef, nalternatives, ncriteria)
77+
78+
@inbounds for j = 1:ncriteria
79+
lower = lowerbounds[j]
80+
upper = upperbounds[j]
81+
82+
if upper < lower
83+
error("upperbounds[$j] must be greater than or equal to lowerbounds[$j]")
84+
end
85+
86+
ideal = if dirs[j] == minimum
87+
lower
88+
elseif dirs[j] == maximum
89+
upper
90+
else
91+
error("Each function in dirs must be either minimum or maximum")
92+
end
93+
94+
span = upper - lower
95+
for i = 1:nalternatives
96+
normalizedDecisionMat[i, j] = if iszero(span)
97+
0.0
98+
else
99+
abs(decisionMat[i, j] - ideal) / span
100+
end
101+
end
102+
end
103+
104+
weightedNormalizedDecisionMat = weightise(normalizedDecisionMat, w)
105+
scores = rowsums(weightedNormalizedDecisionMat)
106+
ranking = sortperm(scores)
107+
bestIndex = first(ranking)
108+
109+
return SPOTISResult(
110+
decisionMat,
111+
normalizedDecisionMat,
112+
w,
113+
weightedNormalizedDecisionMat,
114+
scores,
115+
ranking,
116+
bestIndex)
117+
end
118+
119+
120+
121+
function spotis(
122+
setting::MCDMSetting;
123+
lowerbounds::Union{Nothing, Array{Float64,1}}=nothing,
124+
upperbounds::Union{Nothing, Array{Float64,1}}=nothing)::SPOTISResult
125+
126+
return spotis(
127+
setting.df,
128+
setting.weights,
129+
setting.fns,
130+
lowerbounds=lowerbounds,
131+
upperbounds=upperbounds,
132+
)
133+
end
134+
135+
136+
137+
138+
end # end of module SPOTIS

test/mcdm/testspotis.jl

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
@testset "SPOTIS" begin
2+
tol = 0.0002
3+
4+
# Example extracted from "The SPOTIS Rank Reversal Free Method" (Example 2, car selection).
5+
decmat = [
6+
15000.0 4.3 99.0 42.0 737.0
7+
15290.0 5.0 116.0 42.0 892.0
8+
15350.0 5.0 114.0 45.0 952.0
9+
15490.0 5.3 123.0 45.0 1120.0
10+
]
11+
12+
# Importance values [5, 4, 4, 1, 3] normalized internally by the method.
13+
raw_weights = [5.0, 4.0, 4.0, 1.0, 3.0]
14+
weights = raw_weights ./ sum(raw_weights)
15+
16+
# C1, C2, C3 are cost criteria. C4, C5 are benefit criteria.
17+
dirs = [minimum, minimum, minimum, maximum, maximum]
18+
19+
lowerbounds = [14000.0, 3.0, 80.0, 35.0, 650.0]
20+
upperbounds = [16000.0, 8.0, 140.0, 60.0, 1300.0]
21+
22+
result = spotis(decmat, weights, dirs, lowerbounds=lowerbounds, upperbounds=upperbounds)
23+
24+
@test result isa SPOTISResult
25+
@test isapprox(result.scores, [0.4779, 0.5781, 0.5558, 0.5801], atol = tol)
26+
@test result.ranking == [1, 3, 2, 4]
27+
@test result.bestIndex == 1
28+
29+
setting = MCDMSetting(decmat, weights, dirs)
30+
result2 = spotis(setting, lowerbounds=lowerbounds, upperbounds=upperbounds)
31+
32+
@test result2 isa SPOTISResult
33+
@test result2.bestIndex == result.bestIndex
34+
@test result2.ranking == result.ranking
35+
@test isapprox(result2.scores, result.scores, atol = tol)
36+
37+
method = SpotisMethod(lowerbounds, upperbounds)
38+
result3 = mcdm(setting, method)
39+
40+
@test result3 isa SPOTISResult
41+
@test result3.bestIndex == result.bestIndex
42+
@test result3.ranking == result.ranking
43+
@test isapprox(result3.scores, result.scores, atol = tol)
44+
end

test/testmcdm.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ include("mcdm/testrov.jl")
1111
include("mcdm/testsd.jl")
1212
include("mcdm/testgra.jl")
1313
include("mcdm/testtopsis.jl")
14+
include("mcdm/testspotis.jl")
1415
include("mcdm/testvikor.jl")
1516
include("mcdm/testelectre.jl")
1617
include("mcdm/testmoora.jl")

0 commit comments

Comments
 (0)