API reference¶
Core types¶
Core input and output types.
Everything the caller builds by hand lives here. The types validate at construction rather than at scoring time, so a mistake is reported where it was made — a ragged matrix says it is ragged, instead of surfacing later as “setting an array element with a sequence” from deep inside numpy.
- exception mcdakit.types.McdaError[source]¶
Bases:
ValueErrorRaised when an input cannot describe a well-formed decision problem.
- class mcdakit.types.Criterion(name, weight=1.0, direction='benefit', bounds=None, preference_shape='usual', q=0.0, p=0.0, s=0.0)[source]¶
Bases:
objectOne column of the decision matrix.
- Parameters:
name (str) – Human label, used in results and messages. Need not be unique, though duplicate names make a sensitivity report harder to read.
weight (float) – Relative importance. Any non-negative scale: weights are normalised to sum to one wherever a method needs them to, so 40/30/20/10 and 0.4/0.3/0.2/0.1 are the same input.
direction (str) –
"benefit"when larger values are better (quality, capacity),"cost"when smaller values are better (price, lead time).bounds (tuple | None) –
(lo, hi)for the range this criterion can take, independent of the options currently on the table — a budget ceiling, a rating scale, an acceptable delivery window. Optional for every method except SPOTIS, whose reversal-freedom depends on them; seemcdakit.methods.spotis.spotis().preference_shape (str) – PROMETHEE preference function and its thresholds. Ignored by the other methods.
qis the indifference threshold,pthe preference threshold,sthe Gaussian parameter.q (float) – PROMETHEE preference function and its thresholds. Ignored by the other methods.
qis the indifference threshold,pthe preference threshold,sthe Gaussian parameter.p (float) – PROMETHEE preference function and its thresholds. Ignored by the other methods.
qis the indifference threshold,pthe preference threshold,sthe Gaussian parameter.s (float) – PROMETHEE preference function and its thresholds. Ignored by the other methods.
qis the indifference threshold,pthe preference threshold,sthe Gaussian parameter.
- class mcdakit.types.Decision(matrix, criteria, labels=None)[source]¶
Bases:
objectA decision matrix, its criteria and its option labels.
matrixis options (rows) by criteria (columns), holding the raw figures as measured — euros, days, ratings. Cost criteria are not pre-inverted by the caller; the methods handle direction themselves.
- class mcdakit.types.Result(decision, method, scores, reversal_free=False, warnings=())[source]¶
Bases:
objectThe outcome of ranking one decision under one method.
scoresis in the original option order and always reads higher-is-better, whatever sign convention the underlying method uses internally.rankingis sorted best first.
Ranking¶
The scoring entry points: rank() and compare_methods().
- mcdakit.ranking.METHODS = ('simple_scoring', 'weighted_scoring', 'saw', 'topsis', 'vikor', 'electre', 'promethee', 'copras', 'waspas', 'wpm', 'aras', 'cocoso', 'codas', 'edas', 'mabac', 'marcos', 'spotis')¶
Every registered method name — built-in and plugin alike. A live view of the registry, not a snapshot.
- mcdakit.ranking.as_decision(matrix, criteria=None, labels=None)[source]¶
Accept either a
Decisionor the pieces to build one.
- mcdakit.ranking.score(decision, method='weighted_scoring', normalization=None, **kwargs)[source]¶
Raw scores for one method, without wrapping them in a
Result.Returns
(scores, reversal_free, messages). Mostly useful internally — the sensitivity analysis re-scores the same problem hundreds of times and has no use for the surrounding object.Dispatch goes through the registry, so a method registered by a third-party package is reached the same way a built-in is. Nothing here branches on a method’s name; what a method needs, it declares.
- mcdakit.ranking.rank(matrix, criteria=None, method='weighted_scoring', labels=None, normalization=None, **kwargs)[source]¶
Rank the options in a decision matrix.
- Parameters:
matrix – Options (rows) by criteria (columns), holding raw figures as measured. Cost criteria are handled by the
directionon eachCriterion; do not pre-invert them. ADecisionmay be passed instead, in which casecriteriaandlabelsmust be omitted.method (str) – Any registered method name; see
mcdakit.methods.names(). The default,weighted_scoring, is the least surprising;spotisis the one whose ranking cannot reverse.normalization (str | None)
- Returns:
With
.winner,.ranking,.scores, and — for SPOTIS —.reversal_freeand any.warnings.- Return type:
Examples
>>> from mcdakit import Criterion, rank >>> criteria = [ ... Criterion("Price", weight=0.6, direction="cost"), ... Criterion("Quality", weight=0.4), ... ] >>> result = rank([[3.0, 8], [2.0, 6]], criteria, labels=["A", "B"]) >>> result.winner 'B'
- mcdakit.ranking.compare_methods(matrix, criteria=None, labels=None, methods=None, **kwargs)[source]¶
Rank the same problem under every method.
Returns
{method_name: Result}. When the methods disagree, that disagreement is the finding: it says the options are close enough that modelling choices decide the outcome, and collapsing it into a single number would hide the most useful thing on the table.See
agreement()for a one-line summary of the same output.
- mcdakit.ranking.agreement(results)[source]¶
How much a
compare_methods()mapping actually agrees.winnerscounts how many methods picked each option;consensusis the option most methods picked, orNonewhen two or more options tie for the lead;tiedlists the joint leaders;unanimoussays whether every method agreed on the whole order, not merely the winner.A tie returns
Nonerather than an arbitrary pick. Reporting one of two equally-supported options as the consensus would conceal exactly the disagreement this function exists to surface, and a caller readingconsensusalone would never learn the vote was split.
- mcdakit.ranking.reversal_check(matrix, criteria=None, method='weighted_scoring', labels=None, **kwargs)[source]¶
Does dropping a losing option change the order of the survivors?
Removes each non-winning option in turn, re-ranks what is left, and compares the survivors’ order with their order in the full problem. This is the question behind the whole package: a ranking that changes when an option nobody chose is taken off the table was never really a ranking of the others.
Returns
{"reversed": bool, "cases": [...]}, where each case names the option removed, the order before, and the order after.
Language-model assistance¶
Opt-in: reached as from mcdakit.ai import propose_criteria, not from the
package namespace, so that using a model is a visible choice in the importing
module. No extra dependency is required.
Language-model assistance, with every suggestion checked before it counts.
An LLM is useful in MCDA for the part that is genuinely hard to automate: turning a described problem into a structured one. Naming plausible criteria, proposing a direction for each, and drafting a first set of weights are tasks where a model’s breadth helps and where a wrong answer is cheap to spot.
It is not useful as an authority. A model that returns weights returns them with the same confidence whether they are considered or invented, and nothing in the output distinguishes the two. A package whose stated purpose is to refuse answers it cannot stand behind must not make an exception here.
So this module inverts the usual arrangement. The model proposes; the package verifies, and reports what the proposal is worth:
every suggestion is parsed into the package’s own validated types, so a malformed or out-of-range answer is rejected rather than absorbed;
proposed weights are run through
sensitivity(), so the caller is told whether the recommendation would even survive the model being somewhat wrong;the same question is asked more than once and the answers compared, so a model that is guessing can be distinguished from one that is consistent;
nothing is applied automatically. Every function returns a proposal the caller must choose to accept.
The provider is injected rather than imported. mcdakit acquires no LLM
dependency: the caller supplies any callable mapping a prompt to a string,
which may wrap any vendor’s client, a local model, or a fixture in a test.
>>> from mcdakit.ai import propose_criteria
>>> reply = '''[{"name": "Price", "direction": "cost", "weight": 0.6},
... {"name": "Quality", "direction": "benefit", "weight": 0.4}]'''
>>> proposal = propose_criteria("choosing a supplier", ask=lambda p: reply)
>>> [c.name for c in proposal.criteria]
['Price', 'Quality']
>>> proposal.accepted
False
Because the model is injected, this module is deterministic under test and adds no network dependency to the package.
- exception mcdakit.ai.AiError[source]¶
Bases:
McdaErrorA model’s reply could not be turned into a valid proposal.
- class mcdakit.ai.AiProposal(criteria, rejected=(), agreement=None, raw=(), accepted=False)[source]¶
Bases:
objectCriteria a model suggested, and what the package makes of them.
- Variables:
criteria (tuple) – Validated
Criterionobjects. A suggestion that failed validation never reaches this list.rejected (tuple) – Suggestions that were discarded, each with the reason. Reported rather than dropped: a model that proposed six criteria of which two were malformed is a different signal from one that proposed four.
agreement (float | None) – When the model was asked more than once, the share of criteria names common to every reply. Low agreement indicates the model is improvising, and is worth more than any single answer.
accepted (bool) – Always
False. A proposal is a suggestion; applying it is the caller’s decision, made by constructing aDecisionfrom it.
- Parameters:
- class mcdakit.ai.ComparisonProposal(judgements, weights, criteria, consistency_ratio, consistent, agreement=None, accepted=False)[source]¶
Bases:
objectPairwise judgements a model made, and whether they cohere.
The value of routing an LLM through pairwise comparison rather than asking for weights directly is that the answer is checkable. Saying price matters three times quality, quality three times delivery, and price twice delivery is a contradiction, and Saaty’s consistency ratio detects it without anyone knowing what the right weights were.
- Variables:
- Parameters:
- class mcdakit.ai.PanelProposal(participants, personas, criteria, labels, spread, rejected=(), accepted=False)[source]¶
Bases:
objectA panel of simulated experts, and how far apart they were.
Human expert panels are expensive to convene, hard to assemble across disciplines, and — because the same panel cannot be run twice — not reproducible. A simulated panel is reproducible by construction and costs a prompt, at the price of being a model’s impression of an expert rather than an expert.
That trade is only acceptable if the output is scrutinised, so the participants returned here are ordinary
Participantobjects and go through the samegroup_rank()anddisagreement()machinery as a real panel. A simulated panel that agrees suspiciously closely is itself a finding, andspreadis what shows it.- Variables:
participants (tuple) – One per persona, ready for
group_rank().spread (float) – Mean standard deviation of the scores across personas. Near zero means the personas did not actually differ, and the panel added nothing over asking once.
- Parameters:
- class mcdakit.ai.WeightProposal(weights, criteria, stability=<factory>, disagrees_with=(), agreement=None, accepted=False)[source]¶
Bases:
objectWeights a model suggested, checked against the data.
- Variables:
weights (numpy.ndarray) – Normalised to sum to one.
stability (dict) – The
sensitivity()report obtained using these weights. This is the point of the exercise: a model’s weights may be plausible and still produce a recommendation that a small correction overturns, and the caller should know that before relying on them.disagrees_with (tuple) – Objective schemes whose weights order the criteria differently. A model that ranks importance unlike every data-driven scheme is not necessarily wrong, but the divergence is worth surfacing.
- Parameters:
- mcdakit.ai.critique_weights(weights, criteria, matrix)[source]¶
Assess weights from any source against the data.
Separated from
propose_weights()so that weights elicited from a person, taken from a previous study, or produced by a model this package never saw can be held to the same standard. Returns the sensitivity report and the objective schemes that order the criteria differently.
- mcdakit.ai.narrate(result, ask, sensitivity_report=None)[source]¶
Turn a result into a paragraph, and check the model kept to the figures.
The model is given the computed facts and asked to write them up. It is not asked to analyse anything: everything in the prompt has already been calculated, so the only failure available to it is misstating a number — and that is checkable.
Every numeric literal in the reply is therefore compared against the figures supplied. Any that does not appear is listed in
unsupported_numbers, which is the thing worth reading before pasting the text into a report.
- mcdakit.ai.propose_comparisons(problem, criteria, ask, samples=1)[source]¶
Ask a model to compare criteria pairwise, and check it contradicts itself no more than Saaty allows.
Asking for pairwise judgements rather than for weights directly is the point: is price more important than quality, and by how much is a question with a checkable answer, whereas a weight vector is not. The model’s replies are assembled into a reciprocal matrix and put through
ahp_weights(), so a model that answers inconsistently is detected by arithmetic rather than by inspection.
- mcdakit.ai.propose_criteria(problem, ask, n=5, samples=1)[source]¶
Ask a model to suggest criteria for a described problem.
- Parameters:
problem (str) – A description in plain language.
ask (Callable[[str], str]) – Any callable mapping a prompt to the model’s reply. The package imposes no vendor and acquires no dependency.
samples (int) – How many times to ask. Above one, the replies are compared and the share of criteria common to all is reported as
agreement. Asking twice costs twice as much and is usually worth it: a model that names different criteria each time is improvising, which no single reply would reveal.n (int)
- Returns:
Validated criteria, the suggestions that failed validation and why, and the agreement across replies. Nothing is applied.
- Return type:
- mcdakit.ai.propose_weights(problem, criteria, ask, matrix=None, samples=1)[source]¶
Ask a model to weight known criteria, and check the answer.
Where
matrixis supplied, the proposed weights are used to rank the problem and the result is passed throughsensitivity(), so the caller learns not only what the model suggested but whether the resulting recommendation is fragile. The weights are additionally compared against the objective schemes ofmcdakit.weighting; any that order the criteria differently are named indisagrees_with.
- mcdakit.ai.simulate_panel(problem, criteria, personas, labels, ask)[source]¶
Ask a model to score the problem once per expert persona.
Each persona is prompted separately and scores every option on every criterion, producing a
Participant. The result is an ordinary panel: combine it withgroup_rank(), which reports both aggregations and where the personas disagreed.A persona whose reply is malformed or the wrong shape is discarded with its reason rather than silently dropped, since a panel of three that became a panel of two is a different panel.
Providers¶
Ready-made ask callables. These speak each vendor’s HTTP API through the
standard library, so no provider SDK is required and NumPy remains the only
runtime dependency.
Ready-made ask callables for the common language-model providers.
mcdakit.ai takes any Callable[[str], str], which is deliberately the
smallest interface that can work and commits the package to no vendor. The
cost of that generality is boilerplate: every user writes the same adapter,
looks up where their SDK buries the reply text, and gets no retry when a rate
limit arrives mid-run.
These adapters remove that work without spending the package’s independence.
No provider SDK is required. Each adapter speaks the vendor’s HTTP API
directly through urllib from the standard library, so
pip install mcdakit
is enough to reach OpenAI, Anthropic, Google or a local model. NumPy remains the only runtime dependency. Where a vendor SDK is installed it is not used: one code path is easier to trust than two that must agree.
from mcdakit.ai import propose_criteria
from mcdakit.ai_providers import anthropic, ollama
ask = anthropic() # reads ANTHROPIC_API_KEY
ask = ollama("llama3") # local, no key, nothing leaves the machine
proposal = propose_criteria("choosing a supplier", ask=ask)
Every adapter returns a plain function, so it can be wrapped, logged, cached or replaced by a fixture in a test exactly like a hand-written one.
- exception mcdakit.ai_providers.ProviderError[source]¶
Bases:
McdaErrorA provider could not be reached, or refused the request.
- mcdakit.ai_providers.anthropic(model='claude-sonnet-5', *, api_key=None, base_url='https://api.anthropic.com/v1', temperature=0.0, max_tokens=2048, timeout=60.0, retries=3)[source]¶
An
askcallable backed by Anthropic’s messages API.Reads
ANTHROPIC_API_KEYunlessapi_keyis given.
- mcdakit.ai_providers.google(model='gemini-2.0-flash', *, api_key=None, base_url='https://generativelanguage.googleapis.com/v1beta', temperature=0.0, max_tokens=2048, timeout=60.0, retries=3)[source]¶
An
askcallable backed by Google’s Gemini API.Reads
GOOGLE_API_KEY, thenGEMINI_API_KEY, unlessapi_keyis given.
- mcdakit.ai_providers.load_env(path='.env', *, override=False)[source]¶
Read
KEY=valuelines from a file into the environment.Keeping API keys in a
.envfile rather than in source is the usual practice, and the parsing is short enough that requiringpython-dotenvfor it would cost more than it saves. Searches the given path, then each parent directory, so it works from anywhere in a project.from mcdakit.ai_providers import anthropic, load_env load_env() # reads ./.env, or the nearest one above it ask = anthropic() # ANTHROPIC_API_KEY now set
Understands
KEY=value,export KEY=value, quoted values, blank lines and#comments. A variable already set in the environment is left alone unlessoverrideis true — the real environment should win over a file in a checkout, or deploying becomes surprising.Returns the names it set, and their values, so a caller can see what came from the file. Returns an empty mapping when no file is found: a missing
.envis the normal case in production, not an error.Never commit the file.
.envis in this project’s.gitignorefor that reason; do the same in yours.
- mcdakit.ai_providers.ollama(model='llama3', *, base_url='http://localhost:11434', temperature=0.0, timeout=120.0, retries=3)[source]¶
An
askcallable backed by a local Ollama server.No API key and no network: the problem description, the criteria and the measurements stay on the machine. That matters for decisions involving commercially sensitive data, which is a large share of real MCDA work.
The timeout is longer than the hosted default because a local model on CPU is markedly slower than an API call.
ask = ollama("llama3") ask = ollama("mixtral", base_url="http://gpu-box:11434")
- mcdakit.ai_providers.openai(model='gpt-4o', *, api_key=None, base_url='https://api.openai.com/v1', temperature=0.0, max_tokens=2048, timeout=60.0, retries=3)[source]¶
An
askcallable backed by OpenAI’s chat completions API.Reads
OPENAI_API_KEYunlessapi_keyis given.temperaturedefaults to 0. The module asks a model the same question more than once to measure whether it is consistent, and a temperature above zero would make the replies differ for reasons that have nothing to do with the model’s confidence.
- mcdakit.ai_providers.openai_compatible(model, base_url, *, api_key=None, temperature=0.0, max_tokens=2048, timeout=60.0, retries=3, provider='OpenAI-compatible server')[source]¶
An
askcallable for any server exposing/chat/completions.That interface has become the common denominator, so this one adapter reaches vLLM, LM Studio, llama.cpp’s server, Together, Groq, OpenRouter, DeepSeek, Mistral and most others — including models running on your own machine, where
api_keyis usually unnecessary.ask = openai_compatible("mistral-7b", "http://localhost:8000/v1")
Explaining a result¶
Why did this option win?
A ranking is a conclusion, and a conclusion nobody can interrogate is difficult to act on or to defend. Transparency is increasingly treated in the decision-analysis literature as a precondition for trusting a computed result rather than as a presentational extra.
This module answers two questions about a ranking the package has already produced:
Which criteria carried the result? Each criterion’s contribution to an option’s score, and how much of the score it accounts for.
Why did A beat B? The margin between two options, decomposed by criterion, showing which criteria favoured the winner, which worked against it, and whether any single one was decisive.
Two ways of attributing a score are used, because only one of them is exact and it does not apply everywhere:
exactThe method is a weighted sum of normalised values, so an option’s score is the sum of its per-criterion contributions. Nothing is approximated and the parts add up to the whole. Applies to
weighted_scoring,saw,simple_scoringandspotis.leave_one_outThe method is not additive — TOPSIS is a ratio of distances, VIKOR takes a maximum, ELECTRE and PROMETHEE compare options pairwise — so no exact decomposition exists. Each criterion’s importance is instead measured by removing it and re-scoring: the contribution is how much the score changes in its absence. These values are attributions, not components, and do not sum to the score.
Which was used is reported on every result, because presenting an approximation as though it were a decomposition would be the same overstatement the rest of this package exists to avoid.
>>> from mcdakit import Criterion, rank
>>> from mcdakit.explain import explain
>>> criteria = [Criterion("Price", 0.6, "cost"), Criterion("Quality", 0.4)]
>>> result = rank([[3.0, 8.0], [2.0, 6.0]], criteria, labels=["A", "B"])
>>> report = explain(result)
>>> report.basis
'exact'
- class mcdakit.explain.Contribution(criterion, value, weight, contribution, share)[source]¶
Bases:
objectOne criterion’s part in one option’s score.
- class mcdakit.explain.Explanation(option, method, score, basis, contributions, dominant)[source]¶
Bases:
objectWhy one option scored what it did.
- Variables:
basis (str) –
"exact"when the contributions sum to the score, or"leave_one_out"when they are attributions obtained by removing each criterion in turn. The distinction is reported rather than smoothed over: only the first is a decomposition.dominant (str) – The criterion contributing most. For a close decision this is the one worth checking first.
- Parameters:
- class mcdakit.explain.Margin(winner, loser, method, margin, basis, rows, decisive=None)[source]¶
Bases:
objectWhy one option beat another, criterion by criterion.
- Parameters:
- mcdakit.explain.compare(result, winner, loser)[source]¶
Explain why one option outranked another.
The margin between two options is decomposed by criterion, so a reader can see which criteria favoured the winner and which worked against it. A criterion is reported as decisive when reversing its contribution alone would reverse the outcome — the case where a single disputed judgement determines the recommendation, and the one most worth knowing about.
Group decisions¶
Decisions made by several people.
Two people scoring the same options rarely agree, and the interesting question is not only “what did the group decide?” but “how much did they disagree, and does the answer survive that disagreement?” A consensus ranking that hides a 6-2 split is the same failure as a point estimate that hides its error bars.
There are two honest ways to combine several stakeholders, and they answer different questions:
Aggregate the scores, then rank once (aggregate_scores()). Treats the
group as one better-informed judge: average what each person thought each
option was worth, then run a method over the averages. Right when the
participants are measuring the same thing and their differences are noise.
Rank separately, then combine the rankings (aggregate_rankings()).
Treats each participant as a voter with their own legitimate view. Right when
the differences are real preferences rather than measurement error, and when
you do not want one person’s outlying score to drag the average.
They can disagree, and when they do that is a finding about the group, not a
defect. group_rank() reports both by default.
- class mcdakit.group.Participant(name, matrix, weight=1.0)[source]¶
Bases:
objectOne stakeholder’s view of the same decision.
- Parameters:
name (str) – Who this is. Used in the disagreement report.
matrix – Their scores. Must cover the same options and criteria as everyone else, in the same order — see
group_rank()for why.weight (float) – How much this participant counts. Equal by default. A chair with a casting vote, or a domain expert weighted above a generalist, is expressed here rather than by duplicating their row.
- mcdakit.group.aggregate_rankings(participants, criteria, method='weighted_scoring', labels=None, **kwargs)[source]¶
Rank for each participant separately, then combine the rankings.
Borda count over each participant’s ordering: an option placed first by someone earns
n - 1points from them, second earnsn - 2, and so on, weighted by that participant’s weight. Ties share the average of the positions they span, so a tie cannot invent a preference nobody expressed.Unlike
aggregate_scores()this is insensitive to how far apart the scores were — only to the order each person put them in. That is a weakness when the magnitudes are meaningful and a strength when one participant’s outlying score would otherwise drag the mean.Returns
{"scores", "order", "per_participant"}, whereper_participantmaps each name to their ownResult.
- mcdakit.group.aggregate_scores(participants, criteria, labels=None)[source]¶
Average the participants’ scores into one decision matrix.
The weighted mean, cell by cell. Treats the group as a single better-informed judge, which is the right model when the participants are estimating the same underlying quantity and differ only by noise.
It is the wrong model when they hold genuinely different preferences: an average of 9 and 2 is 5.5, a value neither person would recognise, and nothing in the resulting ranking records that they disagreed. Pair it with
disagreement(), which is whatgroup_rank()does.
- mcdakit.group.disagreement(participants, criteria, labels=None)[source]¶
How far apart the participants were, per option and per criterion.
Reported as the standard deviation of the scores each cell received, unweighted — the question is how much the people differed, not how much the chair’s opinion outweighs the rest.
A mean of 5.5 that everybody agreed on and one that averages 9 and 2 are the same number and completely different findings. This is what tells them apart.
Returns
overall,by_option,by_criterion, andmost_contestednaming the option and criterion with the widest spread.
- mcdakit.group.group_rank(participants, criteria, method='weighted_scoring', labels=None, **kwargs)[source]¶
Rank a decision made by several people, and say how much they agreed.
Runs both aggregations — averaging the scores, and combining each participant’s ranking — because they answer different questions and can disagree. When they do, that disagreement is a finding about the group: it means the answer depends on whether you treat the participants as co-estimators of one truth or as voters with distinct views.
- Returns:
resultA
Resultfrom ranking the averaged scores. The headline answer.bordaThe rank-aggregated view, from
aggregate_rankings().agreeWhether the two aggregations chose the same winner.
disagreementThe spread report from
disagreement().per_participantEach participant’s own
Result.unanimousWhether every participant, ranking alone, picked the same winner.
- Return type:
- Parameters:
Examples
>>> from mcdakit import Criterion >>> from mcdakit.group import Participant, group_rank >>> criteria = [Criterion("Price", 0.5, "cost"), Criterion("Quality", 0.5)] >>> alice = Participant("Alice", [[2.0, 8.0], [3.0, 6.0]]) >>> bob = Participant("Bob", [[2.0, 5.0], [3.0, 9.0]]) >>> out = group_rank([alice, bob], criteria, labels=["A", "B"]) >>> out["agree"] True
Sensitivity analysis¶
How far the weights can move before the winner changes.
A ranking is only as good as its weights, and weights are guessed. The question that follows immediately — and that most MCDA tools never answer — is how far someone would have to disagree with those guesses before the answer changes. A winner that survives a 30% shift in every weight is a different finding from one that flips at 4%, even when both are printed as “first”.
- mcdakit.sensitivity.FRAGILE_THRESHOLD = 0.1¶
Below this tolerance the decision is fragile enough to say so out loud.
- mcdakit.sensitivity.ROBUST_THRESHOLD = 0.25¶
At or above this, the winner is robust to any plausible re-weighting.
- mcdakit.sensitivity.BISECTION_STEPS = 40¶
Bisection steps. 40 halvings take the interval to roughly 1e-12 of its original width, far below any weight difference that could matter.
- mcdakit.sensitivity.sensitivity(result)[source]¶
Per-criterion weight tolerance for a ranking.
For each criterion, how far its weight must move — up or down, as a share of the total weight — before a different option wins, and which option takes over. The smallest such move across all criteria is the decision’s overall tolerance.
- Returns:
dict –
overallSmallest tolerance across the criteria, or
Noneif no single weight change can unseat the winner.level"fragile"under 10%,"moderate"under 25%,"robust"at or above,"immovable"when nothing flips it.weakestName of the criterion carrying that smallest tolerance.
rowsOne dict per criterion:
name,weight,increase,decrease,tolerance,direction,flips_to.
Which *way* the weight has to move matters as much as how far (“Quality)
can move by 7.5%” is unusable without knowing that means down.
Only one weight moves at a time. A simultaneous shift in several could
unseat the winner sooner, so this is an upper bound on stability, not a
guarantee — a “robust” result is robust against single-criterion
disagreement, which is the form disagreement usually takes in practice.
- Parameters:
result (Result)
- Return type:
Examples
>>> from mcdakit import Criterion, rank, sensitivity >>> criteria = [Criterion("Price", 0.6, "cost"), Criterion("Quality", 0.4)] >>> result = rank([[3.0, 8], [2.0, 6]], criteria, labels=["A", "B"]) >>> sensitivity(result)["level"] in { ... "fragile", "moderate", "robust", "immovable"} True
Weighting¶
Deriving criterion weights from the data.
Weights are the softest input in any MCDA model. ahp_weights()
elicits them from a person, which is the right approach when someone genuinely
holds a view about what matters. Often nobody does, or the analyst wants a
starting point that is not an opinion — and the field’s answer is to read the
weights off the decision matrix itself.
The premise of every scheme here: a criterion on which all the options score alike cannot separate them, so it carries no information and deserves little weight. They differ in how they measure that, and in whether they account for criteria that duplicate each other.
>>> import numpy as np
>>> from mcdakit import entropy_weights
>>> matrix = np.array([[5.0, 1.0], [5.0, 9.0], [5.0, 5.0]])
>>> w = entropy_weights(matrix)
>>> bool(w[0] < 1e-9), round(float(w[1]), 4)
(True, 1.0)
The first criterion is identical for every option, so it gets no weight at all. That is the point.
These are objective in the sense of being computed rather than asserted, not in the sense of being right. A criterion can be uninformative in the current shortlist and still be the one that matters — a budget everyone happens to meet is not thereby unimportant. Treat these as a defensible default and a check on elicited weights, never as a replacement for knowing what the decision is about.
- mcdakit.weighting.WEIGHTINGS: dict = {'critic': (<function critic_weights>, 'CRITIC; weight by contrast and independence from other criteria.'), 'entropy': (<function entropy_weights>, 'Shannon entropy; weight by how much a criterion separates the options.'), 'equal': (<function equal_weights>, 'Every criterion equally important.'), 'std': (<function std_weights>, 'Standard deviation; weight by spread.')}¶
Name to
(function, summary). Insertion-ordered; plugins append.
- mcdakit.weighting.available_weightings()[source]¶
{name: summary}for every registered scheme.- Return type:
- mcdakit.weighting.critic_weights(matrix, directions=None)[source]¶
CRITIC — CRiteria Importance Through Intercriteria Correlation.
Weights by contrast and independence: a criterion earns weight for varying a lot, and loses it for saying the same thing as the others.
\[C_j = \sigma_j \sum_k (1 - r_{jk})\]where \(\sigma_j\) is the standard deviation of the min-max normalised column and \(r_{jk}\) the correlation between columns j and k.
This is what distinguishes it from
std_weights(): two criteria measuring nearly the same thing — list price and total cost, say — would each get full weight under a variance-only scheme, effectively counting that dimension twice. CRITIC splits it between them.Diakoulaki, Mavrotas and Papayannakis, Determining objective weights in multiple criteria problems: the CRITIC method, Computers & Operations Research 22(7), 1995.
- mcdakit.weighting.derive_weights(matrix, scheme='entropy', directions=None)[source]¶
Weights from a decision matrix, by name or by callable.
Returns one weight per criterion, summing to one.
- Parameters:
directions (Sequence[str] | None) – Optional
"benefit"/"cost"per criterion. Supplying them orients the matrix first, which matters forcritic_weights(), where the sign of a correlation depends on it: a cost and a benefit criterion that move together look opposed until one is flipped.scheme (str)
- Return type:
Examples
>>> from mcdakit import derive_weights >>> matrix = [[2.0, 8.0], [3.0, 6.0], [4.0, 4.0]] >>> w = derive_weights(matrix, "equal") >>> [round(float(x), 3) for x in w] [0.5, 0.5]
- mcdakit.weighting.entropy_weights(matrix, directions=None)[source]¶
Shannon entropy weights.
Normalise each column to a probability distribution, measure its entropy, and weight by how far that falls short of maximum. A column where every option scores alike has maximum entropy and therefore zero weight; a column that separates the options sharply has low entropy and high weight.
\[p_{ij} = \frac{x_{ij}}{\sum_i x_{ij}} \qquad e_j = -k \sum_i p_{ij} \ln p_{ij} \qquad w_j \propto 1 - e_j\]with \(k = 1/\ln m\) so that \(e_j \in [0, 1]\).
Values are shifted to be non-negative first, since the logarithm requires it and a criterion measured on a scale that goes negative — a temperature, a profit-or-loss — is otherwise unusable.
Shannon (1948); the MCDA application is due to Zeleny (1982).
- mcdakit.weighting.equal_weights(matrix, directions=None)[source]¶
Every criterion equally important.
The honest default when nobody has expressed a preference, and the baseline the others should be compared against: if an objective scheme does not beat equal weights on your problem, it is adding complexity rather than information.
- mcdakit.weighting.get_weighting(name)[source]¶
Look up a scheme by name, or raise listing what is available.
- mcdakit.weighting.register_weighting(name, function=None, *, summary='', replace=False)[source]¶
Register a weighting scheme. Usable as a decorator.
A scheme takes
(matrix, directions)and returns one weight per criterion. It need not normalise —derive_weights()does that — but it must return non-negative, finite values.
- mcdakit.weighting.std_weights(matrix, directions=None)[source]¶
Standard-deviation weights.
Weight each criterion by how much the options vary on it. The simplest objective scheme, and the easiest to explain: a criterion everyone scores the same tells you nothing, so it gets nothing.
Unlike
entropy_weights()it is sensitive to the units a criterion is measured in — a price in euros varies more than the same price in thousands — so it suits matrices already on comparable scales, such as ratings, better than raw mixed units.
- mcdakit.WEIGHTINGS¶
Registered weighting schemes, as
{name: (function, summary)}. Useavailable_weightings()for the readable form.
AHP¶
AHP — deriving weights from pairwise comparisons.
Saaty, The Analytic Hierarchy Process, McGraw-Hill, 1980.
Weights are the softest input in any MCDA model, and typing them directly is the worst way to produce them: people cannot reliably say a criterion is worth 0.35 rather than 0.40, but they can say price matters somewhat more than quality. AHP turns those judgements into weights and — the part that makes it worth the extra questions — measures whether the judgements contradict each other.
- mcdakit.ahp.SAATY_SCALE = {0.1111111111111111: 'Extremely less important', 0.14285714285714285: 'Very strongly less important', 0.2: 'Strongly less important', 0.3333333333333333: 'Moderately less important', 1.0: 'Equally important', 3.0: 'Moderately more important', 5.0: 'Strongly more important', 7.0: 'Very strongly more important', 9.0: 'Extremely more important'}¶
Saaty’s fundamental scale, as ratio and meaning.
- mcdakit.ahp.RANDOM_INDEX = {1: 0.0, 2: 0.0, 3: 0.58, 4: 0.9, 5: 1.12, 6: 1.24, 7: 1.32, 8: 1.41, 9: 1.45, 10: 1.49, 11: 1.51, 12: 1.48, 13: 1.56, 14: 1.57, 15: 1.59}¶
the average consistency index of a randomly filled n x n reciprocal matrix. The consistency ratio is CI / RI, which is what makes it comparable across problem sizes.
- Type:
Saaty’s random consistency index by matrix order
- mcdakit.ahp.CONSISTENCY_LIMIT = 0.1¶
above this, at least one judgement contradicts the others badly enough that the derived weights should not be relied on.
- Type:
Saaty’s rule of thumb
- mcdakit.ahp.comparison_matrix(n, judgements)[source]¶
Build a reciprocal matrix from
{(i, j): ratio}judgements.Only the upper triangle need be given: setting
(i, j)to 3 sets(j, i)to 1/3 automatically, which is what makes the matrix reciprocal and the exercise tractable — n(n-1)/2 questions rather than n². Unjudged pairs default to 1 (equally important).
- mcdakit.ahp.priorities(matrix)[source]¶
Principal eigenvector by row geometric mean, normalised to sum to one.
The geometric mean is Saaty’s standard approximation and — unlike an iterative eigensolver — always returns something sensible for the small, well-formed matrices this is used on. It also keeps the numpy-only rule: no scipy needed.
- mcdakit.ahp.consistency_ratio(matrix, weights=None)[source]¶
Saaty’s consistency ratio,
CI / RI.Zero for a perfectly consistent set of judgements — one where saying price is 3x quality and quality is 3x delivery is matched by saying price is 9x delivery. Returns 0.0 for orders below three, where a single judgement has nothing to contradict.
- mcdakit.ahp.ahp_weights(judgements, names=None)[source]¶
Weights from pairwise comparisons, with their consistency ratio.
judgementsis either a full reciprocal matrix, or a{(i, j): ratio}mapping of the upper triangle — in which casenames(or an explicitn) is needed to size it.Returns
{"weights", "consistency_ratio", "consistent", "names"}.The consistency ratio is reported, never enforced. An inconsistent matrix is a signal to revisit the judgements, not an error: sometimes the inconsistency is real and the analyst wants to proceed anyway, and taking that decision away from them would be presumptuous.
Examples
>>> from mcdakit import ahp_weights >>> out = ahp_weights({(0, 1): 3, (0, 2): 9, (1, 2): 3}, ... names=["Price", "Quality", "Delivery"]) >>> round(out["consistency_ratio"], 6) 0.0 >>> [round(float(w), 4) for w in out["weights"]] [0.6923, 0.2308, 0.0769]
- class mcdakit.ahp.AhpResult(scores, weights, local_priorities, names, labels, consistency, inconsistent)[source]¶
Bases:
objectA full AHP hierarchy, and how coherent the judgements behind it were.
- Variables:
scores (numpy.ndarray) – One priority per alternative, summing to one. Higher is better.
weights (numpy.ndarray) – The criterion weights derived from the criteria comparison matrix.
local_priorities (numpy.ndarray) – Alternatives by criteria: each column is the alternatives’ priorities under one criterion.
consistency (dict) –
{"criteria": ratio, criterion_name: ratio, ...}. Reported for every matrix separately, because a hierarchy is only as sound as its least coherent judgement set and an average would hide exactly that.inconsistent (tuple) – Names of the matrices exceeding
CONSISTENCY_LIMIT. Empty when every judgement set holds together.
- Parameters:
- mcdakit.ahp.ahp_rank(criteria_comparisons, alternative_comparisons, names=None, labels=None)[source]¶
Rank alternatives by pairwise comparison throughout the hierarchy.
This is AHP as Saaty defined it, and it differs from every other method in this package in what it asks for. There is no decision matrix: instead the alternatives are compared against each other, pair by pair, separately under each criterion. That is what makes the method usable on criteria nobody can measure — a supplier’s reputation, the quality of a design — where asking is A better than B on this, and by how much is answerable and asking for a number is not.
The price is quadratic: \(n(n-1)/2\) judgements per criterion, plus the criteria themselves. The compensation is that the judgements can be checked. A person who says A is three times B, B three times C, and then A twice C has contradicted themselves, and the consistency ratio detects it without anyone knowing what the right answer was.
- Parameters:
criteria_comparisons – A reciprocal matrix comparing the criteria, or a
{(i, j): ratio}mapping of its upper triangle.alternative_comparisons (Sequence) – One comparison matrix per criterion, in the same order as the criteria, each comparing the alternatives under that criterion.
names (Sequence[str] | None) – Criterion and alternative names, for the report.
labels (Sequence[str] | None) – Criterion and alternative names, for the report.
- Returns:
With the ranking, the weights, and a consistency ratio for every matrix. Inconsistency is reported and never enforced: whether a contradiction is fatal is the analyst’s judgement, not the library’s.
- Return type:
Examples
>>> import numpy as np >>> from mcdakit import ahp_rank >>> criteria = np.array([[1.0, 3.0], [1 / 3, 1.0]]) >>> per_criterion = [ ... np.array([[1.0, 5.0], [1 / 5, 1.0]]), ... np.array([[1.0, 1 / 3], [3.0, 1.0]]), ... ] >>> result = ahp_rank(criteria, per_criterion, labels=["A", "B"]) >>> result.winner 'A'
:func:~mcdakit.ahp_weights uses pairwise comparison to obtain criterion
weights, which then feed a method that scores a numeric decision matrix.
:func:~mcdakit.ahp_rank is the full method: alternatives are compared
pairwise under each criterion too, so no decision matrix is needed at all.
The extension contract¶
The extension contract: what a ranking method is.
A method is an object that turns a Decision into one
score per option, higher meaning better. Everything a method needs to say
about itself — its name, whether it wants the matrix oriented for it, whether
it can promise reversal-freedom — it says here, declaratively, so the library
never has to special-case it by name.
Writing one:
from mcdakit import Method, ScoringContext, register
import numpy as np
class Median(Method):
name = "median"
summary = "Median score across criteria, ignoring weights."
def score(self, ctx: ScoringContext) -> np.ndarray:
return np.median(ctx.data, axis=1)
register(Median())
See Extending mcdakit for the full guide.
- class mcdakit.methods.base.Method[source]¶
Bases:
objectBase class for a ranking method.
Subclasses set
nameand implementscore(). Everything else has a working default.- Variables:
name (str) – The string callers pass as
method=. Must be unique across the registry; lowercase with underscores by convention.summary (str) – One line describing the method, shown by
available().citation (str) – Where the method comes from. This is a research-adjacent library and will be read by people who know the papers — give them the reference.
wants (mcdakit.methods.base.Wants) – Which form of the matrix
score()receives. SeeWants.reversal_free (bool) – Whether the method is rank-reversal-free by construction. Only SPOTIS sets this among the built-ins. A method whose guarantee is conditional should leave this
Falseand return aMethodResultsaying so per call.
- requires_positive: bool = False¶
Whether the method is defined only for strictly positive values. WASPAS divides by them and raises them to fractional powers; COPRAS normalises by a column total. Declaring the restriction lets the conformance suite check what the method actually promises rather than demanding a score for an input on which the method has no meaning.
- normalization: str | None = None¶
The normalisation this method is defined with — a name from
mcdakit.normalization, orNonefor a method that does not normalise. Callers override it withrank(..., normalization=...), which is a real modelling choice: the ranking can change. Read it inscore()throughctx.normalize(data).
- score(ctx)[source]¶
Score every option. Higher must mean better.
Return either an array of one score per option, or a
MethodResultwhen there is a caveat to report.A method whose natural measure is smallest-is-best — VIKOR’s
Q, SPOTIS’s distance — must negate before returning, and say so in its docstring. Getting this backwards produces a confident, exactly inverted ranking, which is the worst failure mode available here.- Parameters:
ctx (ScoringContext)
- validate(decision)[source]¶
Raise if this method cannot handle the problem. Optional.
Called before
score(). Use it to refuse a problem outright — a method needing at least three options, say. Prefer aMethodResultwarning for degradations that are merely regrettable rather than fatal.- Parameters:
decision (Decision)
- Return type:
None
- class mcdakit.methods.base.MethodResult(scores, reversal_free=False, warnings=())[source]¶
Bases:
objectWhat a method returns when a bare score vector is not enough.
Returning a plain
np.ndarrayfromMethod.score()is equivalent to returningMethodResult(scores), and is what almost every method should do. Use this form to report a caveat the caller ought to see.- Variables:
scores (numpy.ndarray) – One score per option, higher is better.
reversal_free (bool) –
Trueonly if this ranking genuinely cannot change when an option is added or removed. Never set it optimistically: it is the strongest claim this library makes.warnings (tuple) – Messages describing any way the result fell short of what the method normally promises.
- Parameters:
- class mcdakit.methods.base.ScoringContext(data, weights, decision, options=<factory>, _normalization=None, _read=<factory>)[source]¶
Bases:
objectEverything a method is given when asked to score.
Bundling these into one object rather than a long parameter list is what lets the contract grow — a future field is additive, and no existing method’s signature has to change to receive it.
- Variables:
data (np.ndarray) – The decision matrix, oriented or raw according to the method’s
Method.wants.weights (np.ndarray) – Criterion weights as the caller gave them, unnormalised. Methods that need them to sum to one should normalise, and must handle a total of zero;
normalized_weightsdoes both.decision (Decision) – The original problem, for the rare method that needs something not surfaced here. Reach for this last: depending on it couples a method to the shape of
Decision.options (dict) – Keyword arguments the caller passed through
rank(..., **options).
- Parameters:
- data: np.ndarray¶
- weights: np.ndarray¶
- property opts: dict¶
The caller’s keyword arguments, marked as consumed.
A method that accepts
rank(..., v=0.5)-style options must read them through this rather thanoptionsdirectly, so that an option nobody accepts is reported instead of vanishing.
- property criteria: tuple¶
The
Criterionobjects, in column order.These carry
direction, so a method reading them to decide cost-versus-benefit has the same double-handling risk asdirections. Reading a criterion’sbounds,preference_shapeor name is safe and common; only itsdirectionis the trap.
- property directions: list¶
"benefit"or"cost"per criterion, in column order.Reading this while
Method.wantsisWants.ORIENTEDis almost always a bug: the cost columns have already been flipped for you, so handling direction again inverts them back.mcdakit.rank()raises if it sees that combination — seeWants.
- normalize(data=None)[source]¶
Apply the normalisation in force for this call.
The caller’s
normalization=if they gave one, otherwise the method’s own default. Use this rather than normalising inline, so a method’s scheme stays swappable and every zero-column guard lives in one place.
- property normalization¶
the caller’s override, or the method’s.
- Type:
The scheme in force
- class mcdakit.methods.base.Wants(*values)[source]¶
Bases:
EnumWhich form of the decision matrix a method wants to be handed.
Most methods assume larger is better and want cost criteria already flipped for them; a few — SPOTIS above all — must see the figures as measured, because they resolve direction themselves against fixed bounds.
- ORIENTED = 'oriented'¶
Cost columns mirrored via
max + min - xso higher is better throughout. The default, and what every classical method expects.
- RAW = 'raw'¶
The matrix exactly as the caller supplied it. The method is then responsible for reading
ctx.directionsitself.
The registry¶
The method registry: how names resolve to implementations.
Methods are looked up by name at call time rather than hardcoded into
rank(), so a method defined outside this package is a
first-class citizen — indistinguishable from a built-in once registered.
Two ways in. Register directly, for a method defined in your own program:
from mcdakit import register
register(MyMethod())
Or declare an entry point, for a method you distribute as a package. It is then discovered automatically the first time anything asks the registry a question, with no import or call needed by the user:
# in your pyproject.toml
[project.entry-points."mcdakit.methods"]
my_method = "my_package.methods:MyMethod"
The entry point may name either a Method
subclass (instantiated with no arguments) or an already-built instance.
- mcdakit.methods.registry.ENTRY_POINT_GROUP = 'mcdakit.methods'¶
The entry-point group third-party packages advertise methods under.
- exception mcdakit.methods.registry.DuplicateMethodError[source]¶
Bases:
McdaErrorA second method tried to claim a name already in use.
- exception mcdakit.methods.registry.PluginLoadError[source]¶
Bases:
UserWarningA package advertised a method that could not be loaded.
A warning rather than an exception: one broken plugin must not stop the library — and every other plugin — from working. Its own category so a strict caller can promote it with
warnings.simplefilter("error", PluginLoadError).
- mcdakit.methods.registry.available()[source]¶
{name: summary}for every registered method.The human-facing counterpart to
names()— what to print when someone asks what this installation can do.- Return type:
- mcdakit.methods.registry.methods()[source]¶
Iterate over the registered method instances.
- Return type:
- mcdakit.methods.registry.names()[source]¶
Every registered method name, in registration order.
- Return type:
- mcdakit.methods.registry.register(method, *, replace=False)[source]¶
Add a method to the registry, and return it.
- Parameters:
method (Method) – An instance, not a class — methods may carry configuration, and registering an instance keeps that possibility open.
replace (bool) – Overwrite an existing registration of the same name. Off by default: silently shadowing a method would make
rank(..., method="topsis")mean different things in different processes, which is exactly the kind of ambiguity this library exists to remove.
- Raises:
McdaError – If
methodis not aMethod, has no name, or the name is taken andreplaceis false.- Return type:
- mcdakit.methods.registry.unregister(name)[source]¶
Remove a method. Raises if it was not registered.
- Parameters:
name (str)
- Return type:
None
At the top level these are re-exported under names that read better next to the rest of the API:
- mcdakit.METHODS¶
Every registered method name — built-in and plugin alike. A live view of the registry rather than a snapshot, so a method registered at any point is immediately visible. Behaves like the tuple it reads as:
in, iteration, indexing,lenand==all work.
- mcdakit.get_method(name)¶
Alias of
mcdakit.methods.registry.get().
- mcdakit.has_method(name)¶
Alias of
mcdakit.methods.registry.has().
- mcdakit.method_names()¶
Alias of
mcdakit.methods.registry.names().
- mcdakit.BUILTIN_METHODS¶
The seventeen
Methodclasses this package ships, in reading order: simplest first, the reversal-free one last.
Conformance testing¶
A conformance suite for method authors.
Writing a ranking method is easy; writing one that is correct is not, and the failure modes are quiet. A method whose scores read smallest-is-best produces a confident, exactly inverted ranking. One that divides by a zero-variance criterion produces NaN on some inputs and not others. Neither announces itself.
This module is the check to run before publishing a method. It is deliberately about properties every sound method shares, not about any particular algorithm — so it can tell you that something is wrong without knowing what you implemented.
Use it in your own test suite:
from mcdakit.testing import check_method
def test_my_method_conforms():
check_method(MyMethod())
or from a terminal:
python -m mcdakit.testing my_package.methods:MyMethod
The suite is a floor, not a ceiling. Passing it means your method is well-behaved, not that its arithmetic matches the paper — for that you still need a worked example with values verified outside your own code.
- exception mcdakit.testing.ConformanceError[source]¶
Bases:
AssertionErrorA method violated a property every sound method should hold.
- mcdakit.testing.check_method(method, *, strict=True)[source]¶
Assert that
methodupholds every property a sound method should.- Parameters:
- Raises:
ConformanceError – Listing every property that failed, not merely the first.
- Return type:
None
Examples
>>> import numpy as np >>> from mcdakit import Method >>> from mcdakit.testing import check_method >>> class Total(Method): ... name = "doctest_total" ... summary = "Weighted total." ... citation = "n/a" ... def score(self, ctx): ... return ctx.data @ ctx.normalized_weights >>> check_method(Total())
Methods¶
SPOTIS — Stable Preference Ordering Towards Ideal Solution.
Dezert, Tchamova, Han and Tacnet, The SPOTIS Rank Reversal Free Method for Multi-Criteria Decision-Making Support, FUSION 2020.
The one method here that cannot reverse rank. Every other method in this package measures options against each other in some way — TOPSIS against the best and worst present, VIKOR against the observed range, weighted scoring against the observed span — so removing an option moves the yardstick and can reorder the options that remain. SPOTIS measures each option against a fixed ideal point derived from bounds the analyst sets in advance. Nothing an option does can move another option’s score, so no addition or removal can change the order of the rest.
- exception mcdakit.methods.spotis.BoundsWarning[source]¶
Bases:
UserWarningA SPOTIS ranking fell back to observed data instead of fixed bounds.
Its own category so callers can turn it into an error with
warnings.simplefilter("error", BoundsWarning)— worth doing in any pipeline where the reversal-freedom guarantee is the reason SPOTIS was chosen.
- mcdakit.methods.spotis.spotis(data, weights, directions, bounds=None, warn=True)[source]¶
Rank-reversal-free scoring, returned negated so higher is better.
\[d_{ij} = \frac{|S_{ij} - S^*_j|}{|S_j^{max} - S_j^{min}|} \qquad d(A_i, S^*) = \sum_j w_j \, d_{ij}\]S*is the ideal point: the best value each criterion could take, read off the bounds rather than off the options —hifor a benefit criterion,lofor a cost. The denominator is the bounded span, also fixed. Sod_ijdepends only on optioni, and every other option is irrelevant to it.SPOTIS’s native distance is smallest-is-best. This function returns
-dso that, like every other method here, larger scores rank higher. Scores are therefore in[-1, 0], where0is an option sitting exactly on the ideal point. Read the sign before comparing these numbers with published SPOTIS tables, which report the positive distance.- Parameters:
data (ndarray) – Raw, un-oriented matrix. Unlike the other methods, SPOTIS handles direction itself through the choice of ideal point, and orienting the matrix first would break the correspondence with the bounds.
bounds (Sequence | None) – One
(lo, hi)pair per criterion, orNonefor a criterion whose range is not known.weights (ndarray)
warn (bool)
- Returns:
reversal_freeisFalsewhen any criterion had to fall back to observed min/max, along with a message naming which. In that case the method is not reversal-free — the fallback bounds move when the option set moves, which is exactly the mechanism SPOTIS exists to avoid. Silently returning a number that looks authoritative would be the worst failure this package could have, so the shortfall is both warned about and reported in the return value.- Return type:
(scores, reversal_free, messages)
- class mcdakit.methods.spotis.Spotis[source]¶
Bases:
MethodSPOTIS as a registered method.
The one built-in that declares
Wants.RAW: it must see the figures as measured, because it resolves criterion direction itself against the fixed bounds rather than against the other options.reversal_freeisTrueas a statement about the method, but each call reports its own verdict — a problem with missing bounds comes backreversal_free=Falsewith a warning, because the guarantee is a property of the inputs as much as of the algorithm.- score(ctx)[source]¶
Score every option. Higher must mean better.
Return either an array of one score per option, or a
MethodResultwhen there is a caveat to report.A method whose natural measure is smallest-is-best — VIKOR’s
Q, SPOTIS’s distance — must negate before returning, and say so in its docstring. Getting this backwards produces a confident, exactly inverted ranking, which is the worst failure mode available here.- Parameters:
ctx (ScoringContext)
- Return type:
TOPSIS — Technique for Order of Preference by Similarity to Ideal Solution.
Hwang and Yoon, Multiple Attribute Decision Making: Methods and Applications, Springer, 1981.
- mcdakit.methods.topsis.topsis(data, weights, normalization='vector')[source]¶
Closeness to the ideal solution, in
[0, 1], higher is better.Vector-normalise (
r_ij = x_ij / sqrt(sum_i x_ij^2)), weight, then take each option’s Euclidean distance to the best and worst attainable points. The score isd- / (d+ + d-): the share of an option’s total distance that lies on the good side.Normalisation is vector normalisation, Hwang and Yoon’s original choice. This is worth stating because it is not universal: pymcdm defaults to min-max normalisation here, and the two give genuinely different scores on the same data. Neither is wrong — normalisation is a modelling decision, not a preprocessing detail — but comparing numbers across libraries without knowing which was used will mislead.
Both ideal points are read off the options present, which is why TOPSIS reverses rank at a measurable rate — around 32% in this package’s own benchmark when the last-placed option is removed. That is inherent to the method, not a defect in this implementation; use
spotiswhen the ranking must be stable under a changing shortlist.
- class mcdakit.methods.topsis.Topsis[source]¶
Bases:
MethodTOPSIS as a registered method.
- normalization: str | None = 'vector'¶
The normalisation this method is defined with — a name from
mcdakit.normalization, orNonefor a method that does not normalise. Callers override it withrank(..., normalization=...), which is a real modelling choice: the ranking can change. Read it inscore()throughctx.normalize(data).
- score(ctx)[source]¶
Score every option. Higher must mean better.
Return either an array of one score per option, or a
MethodResultwhen there is a caveat to report.A method whose natural measure is smallest-is-best — VIKOR’s
Q, SPOTIS’s distance — must negate before returning, and say so in its docstring. Getting this backwards produces a confident, exactly inverted ranking, which is the worst failure mode available here.- Parameters:
ctx (ScoringContext)
- Return type:
PROMETHEE II — Preference Ranking Organization METHod for Enrichment Evaluation.
Brans and Vincke, A Preference Ranking Organisation Method: The PROMETHEE Method for Multiple Criteria Decision-Making, Management Science 31(6), 1985.
- mcdakit.methods.promethee.preference_degree(difference, shape='usual', q=0.0, p=0.0, s=0.0)[source]¶
How strongly one option is preferred over another on one criterion.
Brans and Vincke define six shapes. The default,
usual, treats any difference however small as total preference — so 4200 beats 4201 exactly as decisively as it beats 9000. That is right for a rating out of ten and wrong for money and days, which is why the others exist.usualType I
step at zero
ushapeType II
indifferent up to
q, then totalvshapeType III
linear from zero to
plevelType IV
indifferent to
q, half up top, then totallinearType V
indifferent to
q, linear top, then totalgaussianType VI
smooth, governed by
sdifferenceis (this option) minus (the other) after cost criteria have been oriented, so a difference at or below zero is never a preference.
- mcdakit.methods.promethee.preference_matrix(data, weights, shapes=None)[source]¶
Aggregated preference of every option over every other.
shapesis one(shape, q, p, s)tuple per criterion; omitting it uses the usual criterion throughout.Normalising by the weight total keeps the flows in
[-1, 1]whatever scale the weights were entered on, so 40/30/20/10 and 0.4/0.3/0.2/0.1 produce identical flows rather than differing by a factor of a hundred.
- mcdakit.methods.promethee.flows(matrix)[source]¶
(positive, negative, net)flows from a preference matrix.The positive flow is how much an option outranks the field, the negative flow how much the field outranks it, and the net flow their difference — PROMETHEE II’s complete ranking.
- Parameters:
matrix (ndarray)
- mcdakit.methods.promethee.promethee(data, weights, shapes=None)[source]¶
Net flows, in
[-1, 1], higher is better.
- mcdakit.methods.promethee.shapes_from(criteria)[source]¶
(shape, q, p, s)per criterion, orNoneif all are the default.Returning
Nonewhen nothing was configured keeps PROMETHEE on its simple path, so a problem that never touched preference functions scores bit-identically to one built before they existed.- Return type:
tuple | None
- class mcdakit.methods.promethee.Promethee[source]¶
Bases:
MethodPROMETHEE II as a registered method.
Reads each criterion’s
preference_shape,q,pandsoff the problem, so the six preference functions need no extra plumbing at the call site.- score(ctx)[source]¶
Score every option. Higher must mean better.
Return either an array of one score per option, or a
MethodResultwhen there is a caveat to report.A method whose natural measure is smallest-is-best — VIKOR’s
Q, SPOTIS’s distance — must negate before returning, and say so in its docstring. Getting this backwards produces a confident, exactly inverted ranking, which is the worst failure mode available here.- Parameters:
ctx (ScoringContext)
- Return type:
VIKOR — VIseKriterijumska Optimizacija I Kompromisno Resenje.
Opricovic and Tzeng, Compromise solution by MCDM methods: A comparative analysis of VIKOR and TOPSIS, European Journal of Operational Research 156(2), 2004.
- mcdakit.methods.vikor.vikor(data, weights, v=0.5)[source]¶
Compromise ranking, returned negated so higher is better.
VIKOR’s native
Qmeasures regret, so the smallestQis the best option. Every method in this package reads higher-is-better, so-Qis returned. Scores are therefore in[-1, 0].Sis group utility (the weighted sum of normalised regret) andRis individual regret (its maximum).vtrades one against the other:v = 1maximises group utility alone,v = 0minimises the worst single criterion, and the customary0.5weighs them equally.
- class mcdakit.methods.vikor.Vikor[source]¶
Bases:
MethodVIKOR as a registered method.
Accepts
vthroughrank(..., v=...): the weight given to group utility against worst-case regret.- score(ctx)[source]¶
Score every option. Higher must mean better.
Return either an array of one score per option, or a
MethodResultwhen there is a caveat to report.A method whose natural measure is smallest-is-best — VIKOR’s
Q, SPOTIS’s distance — must negate before returning, and say so in its docstring. Getting this backwards produces a confident, exactly inverted ranking, which is the worst failure mode available here.- Parameters:
ctx (ScoringContext)
- Return type:
ELECTRE I — ELimination Et Choix Traduisant la REalite.
Roy, Classement et choix en presence de points de vue multiples: la methode ELECTRE, RIRO 8, 1968.
- mcdakit.methods.electre.electre(data, weights, normalization='vector')[source]¶
Net outranking flow — how many options
ioutranks, minus how many outranki. Higher is better; scores are whole numbers.Option
ioutranksjwhen the criteria favouringicarry enough weight (concordance) and no criterion is against it too strongly (discordance). Both thresholds are the mean of their matrix, the usual choice when the analyst has not set them by hand.The scores are counts, so ties are common on small problems — three options can easily each outrank exactly one other. A tie here means ELECTRE genuinely does not separate them, not that information was lost.
- class mcdakit.methods.electre.Electre[source]¶
Bases:
MethodELECTRE I as a registered method.
- normalization: str | None = 'vector'¶
The normalisation this method is defined with — a name from
mcdakit.normalization, orNonefor a method that does not normalise. Callers override it withrank(..., normalization=...), which is a real modelling choice: the ranking can change. Read it inscore()throughctx.normalize(data).
- score(ctx)[source]¶
Score every option. Higher must mean better.
Return either an array of one score per option, or a
MethodResultwhen there is a caveat to report.A method whose natural measure is smallest-is-best — VIKOR’s
Q, SPOTIS’s distance — must negate before returning, and say so in its docstring. Getting this backwards produces a confident, exactly inverted ranking, which is the worst failure mode available here.- Parameters:
ctx (ScoringContext)
- Return type:
Additive scoring methods: simple, weighted, and SAW.
- mcdakit.methods.scoring.simple_scoring(data, weights=None)[source]¶
Sum of raw scores per option, ignoring weights entirely.
Useful only as a baseline and when every criterion is on the same scale. On mixed units it is meaningless — a price in thousands drowns a rating out of ten — and
weighted_scoringshould be used instead.weightsis accepted and ignored so every method shares one signature.
- mcdakit.methods.scoring.weighted_scoring(data, weights, normalization='minmax')[source]¶
Weighted sum over min-max normalised scores.
Each criterion is first scaled to
[0, 1]across the options being compared, then weighted. Without that step the criterion with the largest numbers dominates regardless of its weight: a price spanning ~900 against a quality rating spanning ~3 means a 40% price weight behaves like 99%.r_ij = (x_ij - min_j) / (max_j - min_j), thensum_j w_j * r_ij.A criterion every option scores identically carries no information; it is given a flat 1.0 rather than dividing by zero.
- mcdakit.methods.scoring.saw(data, weights, normalization='max')[source]¶
Simple Additive Weighting (Churchman and Ackoff, 1954).
r_ij = x_ij / max_j, thensum_j w_j * r_ij. Differs fromweighted_scoringin normalising by the column maximum alone rather than the observed range, which keeps ratios between values intact but means the worst option is not pinned to zero.
- class mcdakit.methods.scoring.SimpleScoring[source]¶
Bases:
MethodUnweighted sum of the oriented scores.
- score(ctx)[source]¶
Score every option. Higher must mean better.
Return either an array of one score per option, or a
MethodResultwhen there is a caveat to report.A method whose natural measure is smallest-is-best — VIKOR’s
Q, SPOTIS’s distance — must negate before returning, and say so in its docstring. Getting this backwards produces a confident, exactly inverted ranking, which is the worst failure mode available here.- Parameters:
ctx (ScoringContext)
- Return type:
- class mcdakit.methods.scoring.WeightedScoring[source]¶
Bases:
MethodWeighted sum over min-max normalised scores.
- normalization: str | None = 'minmax'¶
The normalisation this method is defined with — a name from
mcdakit.normalization, orNonefor a method that does not normalise. Callers override it withrank(..., normalization=...), which is a real modelling choice: the ranking can change. Read it inscore()throughctx.normalize(data).
- score(ctx)[source]¶
Score every option. Higher must mean better.
Return either an array of one score per option, or a
MethodResultwhen there is a caveat to report.A method whose natural measure is smallest-is-best — VIKOR’s
Q, SPOTIS’s distance — must negate before returning, and say so in its docstring. Getting this backwards produces a confident, exactly inverted ranking, which is the worst failure mode available here.- Parameters:
ctx (ScoringContext)
- Return type:
- class mcdakit.methods.scoring.Saw[source]¶
Bases:
MethodSimple Additive Weighting.
- normalization: str | None = 'max'¶
The normalisation this method is defined with — a name from
mcdakit.normalization, orNonefor a method that does not normalise. Callers override it withrank(..., normalization=...), which is a real modelling choice: the ranking can change. Read it inscore()throughctx.normalize(data).
- score(ctx)[source]¶
Score every option. Higher must mean better.
Return either an array of one score per option, or a
MethodResultwhen there is a caveat to report.A method whose natural measure is smallest-is-best — VIKOR’s
Q, SPOTIS’s distance — must negate before returning, and say so in its docstring. Getting this backwards produces a confident, exactly inverted ranking, which is the worst failure mode available here.- Parameters:
ctx (ScoringContext)
- Return type:
COPRAS — COmplex PRoportional ASsessment.
Zavadskas, Kaklauskas and Sarka, The new method of multicriteria complex proportional assessment of projects, Technological and Economic Development of Economy 1(3), 1994.
- mcdakit.methods.copras.copras(data, weights, directions)[source]¶
Proportional assessment of benefit against cost. Higher is better.
Sum-normalise each column, weight it, and total the benefit and cost contributions separately:
\[S^{+}_i = \sum_{j \in benefit} w_j r_{ij} \qquad S^{-}_i = \sum_{j \in cost} w_j r_{ij} \qquad Q_i = S^{+}_i + \frac{S^{-}_{min} \sum_i S^{-}_i} {S^{-}_i \sum_i (S^{-}_{min} / S^{-}_i)}\]The second term is the point of the method: it is inversely proportional to an alternative’s cost total, so a smaller cost raises the score. An implementation that adds \(S^{-}\) directly would reward expense instead of penalising it, and would still produce a plausible-looking ranking.
Unlike most methods here, COPRAS resolves criterion direction itself and so takes the matrix as measured. Values must be non-negative, which sum-normalisation requires; the method is defined for ratio-scale data.
With no cost criterion the second term is undefined and the method reduces to a weighted sum, which is what is returned.
- class mcdakit.methods.copras.Copras[source]¶
Bases:
MethodCOPRAS as a registered method.
- score(ctx)[source]¶
Score every option. Higher must mean better.
Return either an array of one score per option, or a
MethodResultwhen there is a caveat to report.A method whose natural measure is smallest-is-best — VIKOR’s
Q, SPOTIS’s distance — must negate before returning, and say so in its docstring. Getting this backwards produces a confident, exactly inverted ranking, which is the worst failure mode available here.- Parameters:
ctx (ScoringContext)
- Return type:
WASPAS — Weighted Aggregated Sum Product Assessment.
Zavadskas, Turskis, Antucheviciene and Zakarevicius, Optimization of weighted aggregated sum product assessment, Elektronika ir Elektrotechnika 122(6), 2012.
- mcdakit.methods.waspas.waspas(data, weights, directions, lambda_=0.5)[source]¶
A blend of the weighted sum and weighted product models.
\[Q_i = \lambda \sum_j w_j r_{ij} + (1 - \lambda) \prod_j r_{ij}^{\,w_j}\]where \(r\) is linearly normalised — divided by the column maximum for a benefit criterion, and the column minimum divided by the value for a cost criterion.
The two halves fail differently, which is the method’s point. The sum is fully compensatory: a very poor score on one criterion can be offset by good scores elsewhere. The product is not, because a near-zero value drags the whole product down however good the rest are. Blending them at \(\lambda = 0.5\) splits the difference; \(\lambda = 1\) recovers the weighted sum and \(\lambda = 0\) the weighted product.
Like COPRAS, the method resolves criterion direction itself and takes the matrix as measured. Values must be positive: the cost normalisation divides by them, and the product raises them to fractional powers.
- mcdakit.methods.waspas.wpm(data, weights, directions)[source]¶
The weighted product model, also called MEW.
\[S_i = \prod_j r_{ij}^{\,w_j}\]Bridgman, Dimensional Analysis (1922); Miller and Starr, Executive Decisions and Operations Research (1969).
Where the weighted sum lets a strength pay for a weakness, the product does not: one value near zero drags the whole score down however good the rest are. That non-compensatory behaviour is the reason to choose it, and it matters most when a criterion represents something disqualifying rather than merely undesirable — a supplier who cannot meet a safety threshold is not redeemed by a low price.
This is
waspas()at \(\lambda = 0\), and is implemented as that call so the two cannot drift apart. It is exposed separately because the method has its own name and literature, and nobody searching for WPM would think to look for it inside a WASPAS parameter.
- class mcdakit.methods.waspas.Waspas[source]¶
Bases:
MethodWASPAS as a registered method.
Accepts
lambda_throughrank(..., lambda_=...): the weight given to the sum against the product.- requires_positive: bool = True¶
Whether the method is defined only for strictly positive values. WASPAS divides by them and raises them to fractional powers; COPRAS normalises by a column total. Declaring the restriction lets the conformance suite check what the method actually promises rather than demanding a score for an input on which the method has no meaning.
- score(ctx)[source]¶
Score every option. Higher must mean better.
Return either an array of one score per option, or a
MethodResultwhen there is a caveat to report.A method whose natural measure is smallest-is-best — VIKOR’s
Q, SPOTIS’s distance — must negate before returning, and say so in its docstring. Getting this backwards produces a confident, exactly inverted ranking, which is the worst failure mode available here.- Parameters:
ctx (ScoringContext)
- Return type:
- class mcdakit.methods.waspas.Wpm[source]¶
Bases:
MethodThe weighted product model as a registered method.
Takes no
lambda_: fixing it at zero is what distinguishes this fromWaspas.- requires_positive: bool = True¶
Whether the method is defined only for strictly positive values. WASPAS divides by them and raises them to fractional powers; COPRAS normalises by a column total. Declaring the restriction lets the conformance suite check what the method actually promises rather than demanding a score for an input on which the method has no meaning.
- score(ctx)[source]¶
Score every option. Higher must mean better.
Return either an array of one score per option, or a
MethodResultwhen there is a caveat to report.A method whose natural measure is smallest-is-best — VIKOR’s
Q, SPOTIS’s distance — must negate before returning, and say so in its docstring. Getting this backwards produces a confident, exactly inverted ranking, which is the worst failure mode available here.- Parameters:
ctx (ScoringContext)
- Return type:
ARAS — Additive Ratio Assessment.
Zavadskas and Turskis (2010).
The method takes the matrix as measured and resolves criterion direction itself, so it is never handed an oriented matrix.
- mcdakit.methods.aras.aras(data, weights, directions)[source]¶
ARAS — Additive Ratio ASsessment. Higher is better.
An optimal alternative is constructed from the best value available on each criterion, appended to the matrix, and every alternative is scored as a ratio of its weighted sum to the optimum’s. The result is therefore a degree of utility in
(0, 1], where 1 is attainable only by matching the best value on every criterion at once.Zavadskas and Turskis, A new additive ratio assessment (ARAS) method in multicriteria decision-making, Technological and Economic Development of Economy 16(2), 2010.
- class mcdakit.methods.aras.Aras[source]¶
Bases:
MethodARAS as a registered method.
- requires_positive: bool = True¶
Whether the method is defined only for strictly positive values. WASPAS divides by them and raises them to fractional powers; COPRAS normalises by a column total. Declaring the restriction lets the conformance suite check what the method actually promises rather than demanding a score for an input on which the method has no meaning.
- score(ctx)[source]¶
Score every option. Higher must mean better.
Return either an array of one score per option, or a
MethodResultwhen there is a caveat to report.A method whose natural measure is smallest-is-best — VIKOR’s
Q, SPOTIS’s distance — must negate before returning, and say so in its docstring. Getting this backwards produces a confident, exactly inverted ranking, which is the worst failure mode available here.- Parameters:
ctx (ScoringContext)
- Return type:
COCOSO — Combined Compromise Solution.
Yazdani et al. (2019).
The method takes the matrix as measured and resolves criterion direction itself, so it is never handed an oriented matrix.
- mcdakit.methods.cocoso.cocoso(data, weights, directions, lambda_=0.5)[source]¶
COCOSO — COmbined COmpromise SOlution. Higher is better.
Three aggregation strategies are computed — an arithmetic mean of the weighted sum and weighted product, a sum of relative scores, and a balanced compromise — and combined. Using three rather than one is the method’s premise: an alternative that leads on all three is robust to the choice of aggregation, and one that leads on only a single strategy is visibly not.
Yazdani, Zarate, Zavadskas and Turskis, A combined compromise solution (CoCoSo) method for multi-criteria decision-making problems, Management Decision 57(9), 2019.
- class mcdakit.methods.cocoso.Cocoso[source]¶
Bases:
MethodCOCOSO as a registered method.
- score(ctx)[source]¶
Score every option. Higher must mean better.
Return either an array of one score per option, or a
MethodResultwhen there is a caveat to report.A method whose natural measure is smallest-is-best — VIKOR’s
Q, SPOTIS’s distance — must negate before returning, and say so in its docstring. Getting this backwards produces a confident, exactly inverted ranking, which is the worst failure mode available here.- Parameters:
ctx (ScoringContext)
- Return type:
CODAS — Combinative Distance-based Assessment.
Keshavarz Ghorabaee et al. (2016).
The method takes the matrix as measured and resolves criterion direction itself, so it is never handed an oriented matrix.
- mcdakit.methods.codas.codas(data, weights, directions, tau=0.02)[source]¶
CODAS — COmbinative Distance-based ASsessment. Higher is better.
Alternatives are ranked by Euclidean distance from the worst possible point, with Taxicab distance used to separate any pair whose Euclidean distances are within
tauof each other. The threshold exists because Euclidean distance alone leaves near-ties that are arbitrary rather than meaningful.Keshavarz Ghorabaee, Zavadskas, Turskis and Antucheviciene, A new combinative distance-based assessment (CODAS) method for multi-criteria decision-making, Economic Computation and Economic Cybernetics Studies and Research 50(3), 2016.
- class mcdakit.methods.codas.Codas[source]¶
Bases:
MethodCODAS as a registered method.
- score(ctx)[source]¶
Score every option. Higher must mean better.
Return either an array of one score per option, or a
MethodResultwhen there is a caveat to report.A method whose natural measure is smallest-is-best — VIKOR’s
Q, SPOTIS’s distance — must negate before returning, and say so in its docstring. Getting this backwards produces a confident, exactly inverted ranking, which is the worst failure mode available here.- Parameters:
ctx (ScoringContext)
- Return type:
EDAS — Evaluation based on Distance from Average Solution.
Keshavarz Ghorabaee et al. (2015).
The method takes the matrix as measured and resolves criterion direction itself, so it is never handed an oriented matrix.
- mcdakit.methods.edas.edas(data, weights, directions)[source]¶
EDAS — Evaluation based on Distance from Average Solution.
Higher is better. Each alternative is measured by how far it rises above the column average and how far it falls below it, rather than against an ideal. That suits problems where the alternatives are broadly comparable and an ideal point would be a fiction.
Keshavarz Ghorabaee, Zavadskas, Olfat and Turskis, Multi-criteria inventory classification using a new method of evaluation based on distance from average solution, Informatica 26(3), 2015.
- class mcdakit.methods.edas.Edas[source]¶
Bases:
MethodEDAS as a registered method.
- score(ctx)[source]¶
Score every option. Higher must mean better.
Return either an array of one score per option, or a
MethodResultwhen there is a caveat to report.A method whose natural measure is smallest-is-best — VIKOR’s
Q, SPOTIS’s distance — must negate before returning, and say so in its docstring. Getting this backwards produces a confident, exactly inverted ranking, which is the worst failure mode available here.- Parameters:
ctx (ScoringContext)
- Return type:
MABAC — Multi-Attributive Border Approximation area Comparison.
Pamucar and Cirovic (2015).
The method takes the matrix as measured and resolves criterion direction itself, so it is never handed an oriented matrix.
- mcdakit.methods.mabac.mabac(data, weights, directions)[source]¶
MABAC — Multi-Attributive Border Approximation area Comparison.
Higher is better, and the sign is meaningful: a positive total places an alternative in the upper approximation area, above the geometric mean of the field, and a negative total places it below. Unlike a closeness coefficient, the zero point is interpretable.
Pamucar and Cirovic, The selection of transport and handling resources in logistics centers using Multi-Attributive Border Approximation area Comparison (MABAC), Expert Systems with Applications 42(6), 2015.
- class mcdakit.methods.mabac.Mabac[source]¶
Bases:
MethodMABAC as a registered method.
- score(ctx)[source]¶
Score every option. Higher must mean better.
Return either an array of one score per option, or a
MethodResultwhen there is a caveat to report.A method whose natural measure is smallest-is-best — VIKOR’s
Q, SPOTIS’s distance — must negate before returning, and say so in its docstring. Getting this backwards produces a confident, exactly inverted ranking, which is the worst failure mode available here.- Parameters:
ctx (ScoringContext)
- Return type:
MARCOS — Measurement of Alternatives and Ranking according to COmpromise Solution.
Stevic et al. (2020).
The method takes the matrix as measured and resolves criterion direction itself, so it is never handed an oriented matrix.
- mcdakit.methods.marcos.marcos(data, weights, directions)[source]¶
MARCOS — Measurement of Alternatives and Ranking according to COmpromise Solution. Higher is better.
Both an ideal and an anti-ideal alternative are appended to the matrix, and each alternative is scored by its utility relative to the two at once. Bounding the problem from both ends is what distinguishes it from methods referencing only the ideal.
Stevic, Pamucar, Puska and Chatterjee, Sustainable supplier selection in healthcare industries using a new MCDM method: MARCOS, Computers & Industrial Engineering 140, 2020.
- class mcdakit.methods.marcos.Marcos[source]¶
Bases:
MethodMARCOS as a registered method.
- score(ctx)[source]¶
Score every option. Higher must mean better.
Return either an array of one score per option, or a
MethodResultwhen there is a caveat to report.A method whose natural measure is smallest-is-best — VIKOR’s
Q, SPOTIS’s distance — must negate before returning, and say so in its docstring. Getting this backwards produces a confident, exactly inverted ranking, which is the worst failure mode available here.- Parameters:
ctx (ScoringContext)
- Return type:
Normalisation¶
Normalisation schemes, and how to add your own.
Normalisation is not a preprocessing detail. Putting a price spanning 900 and a rating spanning 3 onto a common scale is a modelling decision: which scheme you pick changes the numbers, sometimes the ranking, and — as the rank-reversal literature shows — how stable that ranking is when the option set changes. Two methods that differ only in normalisation are two different methods.
So the schemes are named, documented, cited and swappable, rather than hardcoded inside each algorithm:
>>> from mcdakit import rank, Criterion
>>> criteria = [Criterion("A", 1.0), Criterion("B", 1.0)]
>>> a = rank([[1.0, 9.0], [9.0, 1.0]], criteria, method="topsis")
>>> b = rank([[1.0, 9.0], [9.0, 1.0]], criteria, method="topsis",
... normalization="minmax")
>>> a.method, b.method
('topsis', 'topsis')
Every scheme takes a matrix that is already oriented — larger is better — and returns one of the same shape. A column carrying no information (every option scoring alike, or all zeros) is given a constant rather than a division by zero: it cannot discriminate, and saying so with a flat value is honest where a NaN would poison the whole ranking.
Adding one is the same shape as adding a method:
from mcdakit import register_normalization
@register_normalization("softmax", summary="Exponential, sums to one.")
def softmax(data):
shifted = np.exp(data - data.max(axis=0))
return shifted / shifted.sum(axis=0)
- mcdakit.normalization.NORMALIZATIONS: dict = {'max': (<function max_normalization>, 'x / max; preserves ratios, worst option not pinned to zero.'), 'minmax': (<function minmax_normalization>, '(x - min) / (max - min); observed range onto [0, 1].'), 'sum': (<function sum_normalization>, 'x / sum(x); each value as a share of the column total.'), 'vector': (<function vector_normalization>, 'x / sqrt(sum(x^2)); unit-insensitive, preserves ratios.')}¶
Name to
(function, summary). Insertion-ordered; plugins append.
- mcdakit.normalization.available_normalizations()[source]¶
{name: summary}for every registered scheme.- Return type:
- mcdakit.normalization.get_normalization(name)[source]¶
Look up a scheme by name, or raise listing what is available.
- mcdakit.normalization.max_normalization(data)[source]¶
x / max— SAW’s scheme.Keeps ratios intact and does not pin the worst option to zero. More stable than min-max under option removal, because only the maximum matters and the option removed is usually not it.
- mcdakit.normalization.minmax_normalization(data)[source]¶
(x - min) / (max - min)— the observed range mapped onto[0, 1].The most intuitive scheme and the most reversal-prone: the best option always scores 1 and the worst always 0, so removing either rescales everyone else. This is the mechanism behind most of the rank reversal this package measures.
- mcdakit.normalization.normalize_weights(weights)[source]¶
Scale weights to sum to one, or return
Noneif the total is zero.Every method needs this, and each carried its own copy of the guard.
Nonerather than a fallback keeps the decision with the caller: the methods return a flat zero vector, which says “nothing here can be ranked” rather than inventing preferences nobody expressed.Going through
mcdakit.rank()the zero case cannot arise — aDecisionrefuses all-zero weights. The guard is for the algorithm functions, which are public and callable on hand-built arrays.
- mcdakit.normalization.register_normalization(name, function=None, *, summary='', replace=False)[source]¶
Register a normalisation scheme. Usable as a decorator.
- Parameters:
name (str) – What callers pass as
normalization=.function (Callable | None) – Takes an oriented matrix, returns one of the same shape. Omit to use as a decorator.
replace (bool) – Overwrite an existing scheme of this name. Off by default, for the same reason methods refuse it: silently shadowing one would make the same call mean different things in different processes.
summary (str)
- mcdakit.normalization.sum_normalization(data)[source]¶
x / sum(x)— each column becomes a share of its total.Used by COPRAS and several outranking methods. Every option influences every other’s score, so it is sensitive to the option set.
- mcdakit.normalization.vector_normalization(data)[source]¶
x / sqrt(sum(x^2))— Hwang and Yoon’s original choice for TOPSIS.Preserves the ratios between values and is insensitive to the units the column was measured in. Because the denominator depends on every value in the column, adding or removing an option changes every other option’s normalised score.
Hwang and Yoon, Multiple Attribute Decision Making, 1981.
Constants¶
- mcdakit.FRAGILE_THRESHOLD¶
0.10. Below this weight tolerance,sensitivity()calls a decision fragile: a re-weighting smaller than a tenth of the total unseats the winner.
- mcdakit.ROBUST_THRESHOLD¶
0.25. At or above this, the winner survives any plausible single-criterion disagreement and is reported as robust.
- mcdakit.CONSISTENCY_LIMIT¶
0.10. Saaty’s rule of thumb: above this consistency ratio the pairwise judgements contradict each other badly enough that the derived weights should not be relied on. Reported, never enforced.
- mcdakit.RANDOM_INDEX¶
Saaty’s random consistency index by matrix order — the average consistency index of a randomly filled n × n reciprocal matrix. Dividing by it is what makes the consistency ratio comparable across problem sizes.
- mcdakit.SAATY_SCALE¶
The fundamental 1–9 comparison scale, as
{ratio: meaning}.
- mcdakit.NORMALIZATIONS¶
Registered normalisation schemes, as
{name: (function, summary)}. Useavailable_normalizations()for the readable form.
Orientation¶
Turning cost criteria into benefit criteria.
Every classical method in this package is written for benefit criteria — larger is better. Real decision matrices are mostly not: price and lead time are the two most common criteria in procurement and both are costs. Without a correction, a buyer entering genuine figures gets the most expensive, slowest option ranked first: a confident and completely inverted answer.
Flipping once, here, rather than inside each algorithm keeps the method implementations untouched and gives the correction a single place to be right.
- mcdakit.orientation.orient(data, directions)[source]¶
Mirror every cost column so that higher is better throughout.
The transform is
max + min - x, which reflects the column about its own midpoint. Two properties make it the right choice here:it preserves the spacing between values, so methods that care about intervals see the same structure they would have seen on a benefit criterion;
it keeps everything non-negative when the input was, which TOPSIS’s and ELECTRE’s vector normalisation and SAW’s division by the column maximum all rely on.
Note it is not value-preserving across different option sets: the midpoint depends on which options are present. That dependence is one of the mechanisms behind rank reversal, and is precisely what SPOTIS avoids by working from fixed bounds instead — see
mcdakit.methods.spotis.spotis().