A <- matrix(
c(
1, 3, 5,
1/3, 1, 2,
1/5, 1/2, 1
),
nrow = 3,
byrow = TRUE
)Analytic Hierarchy Process (AHP) in R
A beginner-friendly introduction to MCDA weight assignment
1 Why use AHP?
In a Multi-Criteria Decision Analysis (MCDA), we often need to combine several criteria into one final score.
For example, an ecological suitability model might include:
- temperature,
- precipitation,
- NDVI.
A simple option is to give every criterion the same weight:
\[ w_T = w_P = w_N = \frac{1}{3} \]
But sometimes we have biological knowledge or expert judgement suggesting that some criteria should contribute more strongly than others.
The Analytic Hierarchy Process (AHP) is one way to convert those judgements into a consistent set of weights.
The basic idea is:
Instead of assigning final weights directly, compare criteria two at a time.
The workflow is:
\[ \boxed{ \text{pairwise comparisons} \rightarrow \text{comparison matrix} \rightarrow \text{priority weights} \rightarrow \text{consistency check} } \]
2 The Saaty comparison scale
AHP commonly uses the Saaty scale for pairwise comparisons.
| Value | Interpretation |
|---|---|
| 1 | Equal importance |
| 3 | Moderate importance |
| 5 | Strong importance |
| 7 | Very strong importance |
| 9 | Extreme importance |
| 2, 4, 6, 8 | Intermediate values |
If criterion A is judged to be 3 times more important than criterion B:
\[ A/B = 3 \]
then the reverse comparison is:
\[ B/A = \frac{1}{3} \]
This reciprocal relationship is an important property of an AHP matrix.
3 Example with three criteria
We will use:
- Temperature
- Precipitation
- NDVI
Suppose an expert gives these judgements:
- Temperature is 3 times more important than precipitation.
- Temperature is 5 times more important than NDVI.
- Precipitation is 2 times more important than NDVI.
These are the only independent judgements we need for three criteria.
4 Build the comparison matrix in R
Add names to make the matrix easier to read.
criteria <- c(
"Temperature",
"Precipitation",
"NDVI"
)
rownames(A) <- criteria
colnames(A) <- criteria
A Temperature Precipitation NDVI
Temperature 1.0000000 3.0 5
Precipitation 0.3333333 1.0 2
NDVI 0.2000000 0.5 1
The matrix is:
\[ A= \begin{bmatrix} 1 & 3 & 5\\ 1/3 & 1 & 2\\ 1/5 & 1/2 & 1 \end{bmatrix} \]
Each cell answers:
How important is the criterion in the row compared with the criterion in the column?
For example:
A["Temperature", "Precipitation"][1] 3
returns 3, because temperature was judged to be 3 times more important than precipitation.
The reverse comparison is:
A["Precipitation", "Temperature"][1] 0.3333333
which gives approximately 0.3333333, or:
\[ \frac{1}{3} \]
5 Check the reciprocal structure
AHP matrices should satisfy:
\[ a_{ji}=\frac{1}{a_{ij}} \]
For example:
A["Temperature", "NDVI"][1] 5
A["NDVI", "Temperature"][1] 0.2
These should be 5 and 0.2, because:
\[ \frac{1}{5}=0.2 \]
6 Method 1: The classical AHP eigenvector method
The formal AHP method derives weights from the principal eigenvector of the comparison matrix.
Mathematically:
\[ A\mathbf{w}=\lambda_{\max}\mathbf{w} \]
where:
- \(A\) is the comparison matrix,
- \(\mathbf{w}\) is the weight vector,
- \(\lambda_{\max}\) is the largest eigenvalue.
In R:
eig <- eigen(A)
eigeigen() decomposition
$values
[1] 3.003694598+0.0000000i -0.001847299+0.1053282i -0.001847299-0.1053282i
$vectors
[,1] [,2] [,3]
[1,] 0.9281193+0i -0.92811934+0.0000000i -0.92811934+0.0000000i
[2,] 0.3287580+0i 0.16437900-0.2847128i 0.16437900+0.2847128i
[3,] 0.1746787+0i 0.08733937+0.1512762i 0.08733937-0.1512762i
The output contains the eigenvalues and eigenvectors.
The largest eigenvalue is:
lambda_max <- Re(eig$values[1])
lambda_max[1] 3.003695
Now extract the eigenvector associated with that eigenvalue:
principal_vector <- Re(eig$vectors[, 1])
principal_vector[1] 0.9281193 0.3287580 0.1746787
The values are then normalized so that they sum to 1.
weights <- principal_vector / sum(principal_vector)
names(weights) <- criteria
weights Temperature Precipitation NDVI
0.6483290 0.2296508 0.1220202
You should obtain approximately:
Temperature 0.648
Precipitation 0.230
NDVI 0.122
Check:
sum(weights)[1] 1
The result should be 1.
7 Interpret the weights
The final ranking is:
\[ Temperature > Precipitation > NDVI \]
with approximately:
\[ w_T=0.648,\qquad w_P=0.230,\qquad w_N=0.122 \]
The corresponding weighted MCDA would be:
\[ MCDA= 0.648T+ 0.230P+ 0.122N \]
The key point is that the expert did not directly assign those final weights.
The expert assigned the pairwise judgements:
T vs P = 3
T vs N = 5
P vs N = 2
and AHP transformed them into the final weight vector.
8 Method 2: A simplified approximation
AHP is also sometimes taught using a simpler calculation:
- sum each column,
- divide each value by its column total,
- take the mean of each row.
This is easier to calculate by hand and usually gives very similar results when the matrix is reasonably consistent.
First calculate the column sums:
column_sums <- colSums(A)
column_sums Temperature Precipitation NDVI
1.533333 4.500000 8.000000
Now normalize the columns:
A_normalized <- sweep(
A,
2,
column_sums,
"/"
)
A_normalized Temperature Precipitation NDVI
Temperature 0.6521739 0.6666667 0.625
Precipitation 0.2173913 0.2222222 0.250
NDVI 0.1304348 0.1111111 0.125
The sweep() function here means:
Divide each value in a column by the sum of that column.
Now calculate the row means:
weights_approx <- rowMeans(A_normalized)
weights_approx Temperature Precipitation NDVI
0.6479469 0.2298712 0.1221820
Compare the two approaches:
data.frame(
criterion = criteria,
eigenvector_weight = weights,
normalized_mean_weight = weights_approx
) criterion eigenvector_weight normalized_mean_weight
Temperature Temperature 0.6483290 0.6479469
Precipitation Precipitation 0.2296508 0.2298712
NDVI NDVI 0.1220202 0.1221820
The values should be almost identical.
For teaching purposes:
The eigenvector method is the formal AHP approach. The column-normalization method is a useful approximation.
9 Why do we need a consistency check?
Human judgements are not always perfectly consistent.
Suppose an expert says:
Temperature > Precipitation
Precipitation > NDVI
We would normally expect:
Temperature > NDVI
But a person might give judgements that do not follow this logic consistently.
AHP therefore calculates a Consistency Ratio.
10 Calculate the Consistency Index
The number of criteria is:
n <- nrow(A)
n[1] 3
The Consistency Index is:
\[ CI= \frac{\lambda_{\max}-n}{n-1} \]
In R:
CI <- (lambda_max - n) / (n - 1)
CI[1] 0.001847299
For a perfectly consistent matrix:
\[ \lambda_{\max}=n \]
and therefore:
\[ CI=0 \]
11 Calculate the Consistency Ratio
The Consistency Ratio compares our consistency with the consistency expected from random pairwise judgements.
It uses the Random Index (RI).
Common values are:
| Number of criteria, \(n\) | RI |
|---|---|
| 1 | 0.00 |
| 2 | 0.00 |
| 3 | 0.58 |
| 4 | 0.90 |
| 5 | 1.12 |
| 6 | 1.24 |
| 7 | 1.32 |
| 8 | 1.41 |
| 9 | 1.45 |
| 10 | 1.49 |
For three criteria:
RI <- 0.58Now calculate:
\[ CR= \frac{CI}{RI} \]
CR <- CI / RI
CR[1] 0.003184998
As a percentage:
CR * 100[1] 0.3184998
A commonly used rule is:
\[ CR < 0.10 \]
or less than 10%.
This suggests that the pairwise judgements are acceptably consistent.
12 Put the results into a table
results <- data.frame(
criterion = criteria,
weight = weights
)
results criterion weight
Temperature Temperature 0.6483290
Precipitation Precipitation 0.2296508
NDVI NDVI 0.1220202
Sort from highest to lowest weight:
results <- results[
order(results$weight, decreasing = TRUE),
]
results criterion weight
Temperature Temperature 0.6483290
Precipitation Precipitation 0.2296508
NDVI NDVI 0.1220202
13 Create a reusable AHP function
Once we understand the steps, we can combine them into one function.
calculate_ahp <- function(A) {
# Number of criteria
n <- nrow(A)
# Calculate eigenvalues and eigenvectors
eig <- eigen(A)
# Largest eigenvalue
lambda_max <- Re(eig$values[1])
# Principal eigenvector
principal_vector <- Re(eig$vectors[, 1])
# Normalize to obtain weights
weights <- principal_vector / sum(principal_vector)
# Add names
names(weights) <- rownames(A)
# Consistency Index
CI <- (lambda_max - n) / (n - 1)
# Random Index table
RI_table <- c(
0.00,
0.00,
0.58,
0.90,
1.12,
1.24,
1.32,
1.41,
1.45,
1.49
)
RI <- RI_table[n]
# Consistency Ratio
if (RI == 0) {
CR <- 0
} else {
CR <- CI / RI
}
return(
list(
weights = weights,
lambda_max = lambda_max,
CI = CI,
CR = CR
)
)
}Run the function:
ahp_result <- calculate_ahp(A)
ahp_result$weights
Temperature Precipitation NDVI
0.6483290 0.2296508 0.1220202
$lambda_max
[1] 3.003695
$CI
[1] 0.001847299
$CR
[1] 0.003184998
Inspect only the weights:
ahp_result$weights Temperature Precipitation NDVI
0.6483290 0.2296508 0.1220202
Inspect the Consistency Ratio:
ahp_result$CR[1] 0.003184998
14 Connect AHP weights to an MCDA
Suppose we already transformed our environmental variables into suitability scores:
temperature_suitability <- 0.8
precipitation_suitability <- 0.6
ndvi_suitability <- 0.7Now combine them with the AHP weights:
MCDA_score <-
weights["Temperature"] * temperature_suitability +
weights["Precipitation"] * precipitation_suitability +
weights["NDVI"] * ndvi_suitability
MCDA_scoreTemperature
0.7418678
This is a weighted linear combination:
\[ S= w_TT+ w_PP+ w_NN \]
AHP determines the weights.
The suitability functions determine the criterion scores.
These are different modelling decisions.
15 A deliberately inconsistent example
Now let us see why the consistency test is important.
Suppose an expert says:
- Temperature is 5 times more important than precipitation.
- Precipitation is 5 times more important than NDVI.
- NDVI is 3 times more important than temperature.
This creates a contradiction.
Build the matrix:
A_inconsistent <- matrix(
c(
1, 5, 1/3,
1/5, 1, 5,
3, 1/5, 1
),
nrow = 3,
byrow = TRUE
)
rownames(A_inconsistent) <- criteria
colnames(A_inconsistent) <- criteria
A_inconsistent Temperature Precipitation NDVI
Temperature 1.0 5.0 0.3333333
Precipitation 0.2 1.0 5.0000000
NDVI 3.0 0.2 1.0000000
Calculate AHP:
result_inconsistent <- calculate_ahp(
A_inconsistent
)
result_inconsistent$weights Temperature Precipitation NDVI
0.3914183 0.3301350 0.2784467
Now inspect the Consistency Ratio:
result_inconsistent$CR[1] 2.115767
This value should be much larger.
That tells us:
The pairwise comparisons should probably be reconsidered before using the weights in the final MCDA.
This is an important advantage of AHP over directly assigning weights.
16 Exercise: Four environmental criteria
Now build an AHP matrix for:
- Temperature
- Precipitation
- NDVI
- Vapour pressure
For \(n\) criteria, the number of independent comparisons is:
\[ \frac{n(n-1)}{2} \]
For four criteria:
\[ \frac{4(4-1)}{2}=6 \]
So you need six judgements:
- Temperature vs precipitation
- Temperature vs NDVI
- Temperature vs vapour pressure
- Precipitation vs NDVI
- Precipitation vs vapour pressure
- NDVI vs vapour pressure
Create a blank matrix:
A4 <- matrix(
NA,
nrow = 4,
ncol = 4
)
criteria4 <- c(
"Temperature",
"Precipitation",
"NDVI",
"Vapour pressure"
)
rownames(A4) <- criteria4
colnames(A4) <- criteria4Set the diagonal to 1:
diag(A4) <- 1Now enter the six independent judgements.
For example:
A4["Temperature", "Precipitation"] <- 3
A4["Temperature", "NDVI"] <- 5
A4["Temperature", "Vapour pressure"] <- 2Then enter the reciprocal values:
A4["Precipitation", "Temperature"] <- 1/3
A4["NDVI", "Temperature"] <- 1/5
A4["Vapour pressure", "Temperature"] <- 1/2Continue until the full matrix is complete.
Then run:
calculate_ahp(A4)17 Questions for interpretation
After calculating your weights, answer:
- Which criterion received the largest weight?
- Which received the smallest?
- Do all weights sum to 1?
- Is the Consistency Ratio below 0.10?
- Do the resulting weights make ecological sense?
- Which pairwise judgements are responsible for the strongest weight differences?
- Would you feel confident using these weights in the final MCDA?
- What assumptions are encoded in the pairwise comparisons?
18 AHP versus direct expert weighting
There is an important distinction.
18.1 Direct expert weighting
An expert might directly choose:
Temperature = 0.50
Precipitation = 0.25
NDVI = 0.15
Vapour pressure = 0.10
These weights may be reasonable, but there is no internal consistency check.
18.2 AHP
The expert instead makes pairwise judgements.
AHP then:
- organizes them into a comparison matrix,
- derives the weights mathematically,
- evaluates whether the judgements are internally consistent.
So:
\[ \boxed{ \text{expert knowledge} \rightarrow \text{pairwise comparisons} \rightarrow \text{AHP weights} } \]
19 Where AHP fits into ecological MCDA
It is useful to separate four different modelling decisions.
19.1 Step 1 — Criterion selection
Which variables should be included?
This can be informed by:
- scientific literature,
- ecological understanding,
- epidemiological understanding,
- expert knowledge.
19.2 Step 2 — Suitability transformation
How should each raw environmental variable be translated into suitability?
For example:
\[ Temperature \rightarrow Suitability \]
This may involve:
- thresholds,
- saturation values,
- sigmoid functions,
- saddle-shaped functions.
19.3 Step 3 — Weight assignment
How strongly should each criterion influence the final model?
This is where we might use:
- equal weighting,
- entropy weighting,
- direct expert weighting,
- AHP.
19.4 Step 4 — Aggregation
Finally, combine the criteria:
\[ S(x)= \sum_{j=1}^{n}w_jS_j(x) \]
where:
- \(S_j(x)\) is the suitability of criterion \(j\) at location \(x\),
- \(w_j\) is the weight assigned to criterion \(j\).
20 Main lesson
AHP is a structured method for translating expert judgements into criterion weights.
Its main strengths are that it makes the weighting assumptions:
- explicit,
- systematic,
- transparent,
- testable for consistency.
The key distinction is:
\[ \boxed{ \text{AHP does not determine biological importance from the data} } \]
Instead:
\[ \boxed{ \text{Biological knowledge} \rightarrow \text{pairwise judgements} \rightarrow \text{AHP} \rightarrow \text{weights} } \]
Those weights can then be used in an MCDA.
A useful final question is therefore:
How sensitive is the final suitability map to the expert judgements used to construct the AHP matrix?