AHPy
A Python implementation of the Analytic Hierarchy Process
Install / Use
/learn @PhilipGriffith/AHPyREADME
AHPy
AHPy is an implementation of the Analytic Hierarchy Process (AHP), a method used to structure, synthesize and evaluate the elements of a decision problem. Developed by Thomas Saaty in the 1970s, AHP's broad use in fields well beyond that of operational research is a testament to its simple yet powerful combination of psychology and mathematics.
AHPy attempts to provide a library that is not only simple to use, but also capable of intuitively working within the numerous conceptual frameworks to which the AHP can be applied. For this reason, general terms have been preferred to more specific ones within the programming interface.
Installing AHPy
AHPy is available on the Python Package Index (PyPI):
python -m pip install ahpy
AHPy requires Python 3.7+, as well as numpy and scipy.
Table of Contents
Examples
Relative consumption of drinks in the United States
Purchasing a vehicle reprised: normalized weights and the Compose class
Details
Examples
The easiest way to learn how to use AHPy is to see it used, so this README begins with worked examples of gradually increasing complexity.
Relative consumption of drinks in the United States
This example is often used in Saaty's expositions of the AHP as a brief but clear demonstration of the method; it's what first opened my eyes to the broad usefulness of the AHP (as well as the wisdom of crowds!). The version I'm using here is from his 2008 article 'Decision making with the analytic hierarchy process'. If you're unfamiliar with the example, 30 participants were asked to compare the relative consumption of drinks in the United States. For instance, they believed that coffee was consumed much more than wine, but at the same rate as milk. The matrix derived from their answers was as follows:
||Coffee|Wine|Tea|Beer|Soda|Milk|Water| |-|:-:|:-:|:-:|:-:|:-:|:-:|:-:| |Coffee|1|9|5|2|1|1|1/2| |Wine|1/9|1|1/3|1/9|1/9|1/9|1/9| |Tea|1/5|3|1|1/3|1/4|1/3|1/9| |Beer|1/2|9|3|1|1/2|1|1/3| |Soda|1|9|4|2|1|2|1/2| |Milk|1|9|3|1|1/2|1|1/3| |Water|2|9|9|3|2|3|1|
The table below shows the relative consumption of drinks as computed using the AHP, given this matrix, together with the actual relative consumption of drinks as obtained from U.S. Statistical Abstracts:
|:exploding_head:|Coffee|Wine|Tea|Beer|Soda|Milk|Water| |-|:-:|:-:|:-:|:-:|:-:|:-:|:-:| |AHP|0.177|0.019|0.042|0.116|0.190|0.129|0.327| |Actual|0.180|0.010|0.040|0.120|0.180|0.140|0.330|
We can recreate this analysis with AHPy using the following code:
>>> drink_comparisons = {('coffee', 'wine'): 9, ('coffee', 'tea'): 5, ('coffee', 'beer'): 2, ('coffee', 'soda'): 1,
('coffee', 'milk'): 1, ('coffee', 'water'): 1 / 2,
('wine', 'tea'): 1 / 3, ('wine', 'beer'): 1 / 9, ('wine', 'soda'): 1 / 9,
('wine', 'milk'): 1 / 9, ('wine', 'water'): 1 / 9,
('tea', 'beer'): 1 / 3, ('tea', 'soda'): 1 / 4, ('tea', 'milk'): 1 / 3,
('tea', 'water'): 1 / 9,
('beer', 'soda'): 1 / 2, ('beer', 'milk'): 1, ('beer', 'water'): 1 / 3,
('soda', 'milk'): 2, ('soda', 'water'): 1 / 2,
('milk', 'water'): 1 / 3}
>>> drinks = ahpy.Compare(name='Drinks', comparisons=drink_comparisons, precision=3, random_index='saaty')
>>> print(drinks.target_weights)
{'water': 0.327, 'soda': 0.19, 'coffee': 0.177, 'milk': 0.129, 'beer': 0.116, 'tea': 0.042, 'wine': 0.019}
>>> print(drinks.consistency_ratio)
0.022
- First, we create a dictionary of pairwise comparisons using the values from the matrix above.<br>
- We then create a Compare object, initializing it with a unique name and the dictionary we just made. We also change the precision and random index so that the results match those provided by Saaty.<br>
- Finally, we print the Compare object's target weights and consistency ratio to see the results of our analysis.
Brilliant!
Choosing a leader
This example can be found in an appendix to an older version of the Wikipedia entry for AHP. The names have been changed in a nod to the original saying, but the input comparison values remain the same.
N.B.
You may notice that in some cases AHPy's results will not match those on the Wikipedia page. This is not an error in AHPy's calculations, but rather a result of the method used to compute the values shown in the Wikipedia examples:
You can duplicate this analysis at this online demonstration site...IMPORTANT: The demo site is designed for convenience, not accuracy. The priorities it returns may differ somewhat from those returned by rigorous AHP calculations.
In this example, we'll be judging job candidates by their experience, education, charisma and age. Therefore, we need to compare each potential leader to the others, given each criterion...
>>> experience_comparisons = {('Moll', 'Nell'): 1/4, ('Moll', 'Sue'): 4, ('Nell', 'Sue'): 9}
>>> education_comparisons = {('Moll', 'Nell'): 3, ('Moll', 'Sue'): 1/5, ('Nell', 'Sue'): 1/7}
>>> charisma_comparisons = {('Moll', 'Nell'): 5, ('Moll', 'Sue'): 9, ('Nell', 'Sue'): 4}
>>> age_comparisons = {('Moll', 'Nell'): 1/3, ('Moll', 'Sue'): 5, ('Nell', 'Sue'): 9}
...as well as compare the importance of each criterion to the others:
>>> criteria_comparisons = {('Experience', 'Education'): 4, ('Experience', 'Charisma'): 3, ('Experience', 'Age'): 7,
('Education', 'Charisma'): 1/3, ('Education', 'Age'): 3,
('Charisma', 'Age'): 5}
Before moving on, it's important to note that the order of the elements that form the dictionaries' keys is meaningful. For example, using Saaty's scale, the comparison ('Experience', 'Education'): 4 means that "Experience is moderately+ more important than Education."
Now that we've created all of the necessary pairwise comparison dictionaries, we'll create their corresponding Compare objects and use the dictionaries as input:
>>> experience = ahpy.Compare('Experience', experience_comparisons, precision=3, random_index='saaty')
>>> education = ahpy.Compare('Education', education_comparisons, precision=3, random_index='saaty')
>>> charisma = ahpy.Compare('Charisma', charisma_comparisons, precision=3, random_index='saaty')
>>> age = ahpy.Compare('Age', age_comparisons, precision=3, random_index='saaty')
>>> criteria = ahpy.Compare('Criteria', criteria_comparisons, precision=3, random_index='saaty')
Notice that the names of the Experience, Education, Charisma and Age objects are repeated in the criteria_comparisons dictionary above. This is necessary in order to properly link the Compare objects together into a hierarchy, as shown next.
In the final step, we need to link the Compare objects together into a hierarchy, such that Criteria is the parent object and the other objects form its children:
>>> criteria.add_children([experience, education, charisma, age])
Now that the hierarchy represents the decision problem, we can print the target weights of the parent Criteria object to see the results of the analysis:
>>> print(criteria.target_weights)
{'Nell': 0.493, 'Moll': 0.358, 'Sue': 0.15}
We can also print the local and global weights of the elements within any of the other Compare objects, as well as the consistency ratio of their comparisons:
>>> print(experience.local_weights)
{'Nell': 0.717, 'Moll': 0.217, 'Sue': 0.066}
>>> print(experience.consistency_ratio)
0.035
>>> print(education.global_weights)
{'Sue': 0.093, 'Moll': 0.024, 'Nell': 0.01}
>>> print(education.consistency_ratio)
0.062
The global and local weights of the Compare objects themselves are likewise available:
>>> print(experience.global_weight)
0.548
>>> print(education.local_weight)
0.127
Calling report() on a Compare object provides a standard way to learn information about the object. In the code below, the variable report contains a Python dictionary of important information, while the show=True argument prints the same information to the console in JSON format:
>>> report = criteria.report(show=True)
{
"Criteria": {
"global_weight": 1.0,
"local_weight": 1.0,
"target_weights": {
"Nell": 0.493,
"Moll": 0.358,
"Sue": 0.15
},
"elements": {
"global_weights": {
"Experience": 0.548,
"Charisma": 0.27,
"Education": 0.127,
"Age": 0.056
},
"local_weights": {
"Experience": 0.548,
"Charisma": 0
Related Skills
node-connect
345.9kDiagnose OpenClaw node connection and pairing failures for Android, iOS, and macOS companion apps
claude-opus-4-5-migration
106.4kMigrate prompts and code from Claude Sonnet 4.0, Sonnet 4.5, or Opus 4.1 to Opus 4.5
frontend-design
106.4kCreate distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
model-usage
345.9kUse CodexBar CLI local cost usage to summarize per-model usage for Codex or Claude, including the current (most recent) model or a full model breakdown. Trigger when asked for model-level usage/cost data from codexbar, or when you need a scriptable per-model summary from codexbar cost JSON.
