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

A <- matrix(
  c(
    1,   3,   5,
    1/3, 1,   2,
    1/5, 1/2, 1
  ),
  nrow = 3,
  byrow = TRUE
)

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)

eig
eigen() 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:

  1. sum each column,
  2. divide each value by its column total,
  3. 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.58

Now 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.7

Now combine them with the AHP weights:

MCDA_score <-
  weights["Temperature"] * temperature_suitability +
  weights["Precipitation"] * precipitation_suitability +
  weights["NDVI"] * ndvi_suitability

MCDA_score
Temperature 
  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:

  1. Temperature vs precipitation
  2. Temperature vs NDVI
  3. Temperature vs vapour pressure
  4. Precipitation vs NDVI
  5. Precipitation vs vapour pressure
  6. 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) <- criteria4

Set the diagonal to 1:

diag(A4) <- 1

Now enter the six independent judgements.

For example:

A4["Temperature", "Precipitation"] <- 3
A4["Temperature", "NDVI"] <- 5
A4["Temperature", "Vapour pressure"] <- 2

Then enter the reciprocal values:

A4["Precipitation", "Temperature"] <- 1/3
A4["NDVI", "Temperature"] <- 1/5
A4["Vapour pressure", "Temperature"] <- 1/2

Continue until the full matrix is complete.

Then run:

calculate_ahp(A4)

17 Questions for interpretation

After calculating your weights, answer:

  1. Which criterion received the largest weight?
  2. Which received the smallest?
  3. Do all weights sum to 1?
  4. Is the Consistency Ratio below 0.10?
  5. Do the resulting weights make ecological sense?
  6. Which pairwise judgements are responsible for the strongest weight differences?
  7. Would you feel confident using these weights in the final MCDA?
  8. 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:

  1. organizes them into a comparison matrix,
  2. derives the weights mathematically,
  3. 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?