An open API service indexing awesome lists of open source software.

https://github.com/mrezkys/swiftprofilematching

A Swift Package for implementing Profile Matching Method - a popular technique used in Decision Support Systems (DSS).
https://github.com/mrezkys/swiftprofilematching

dss profile-matching swift swiftpackage swiftpackagemanager

Last synced: 9 months ago
JSON representation

A Swift Package for implementing Profile Matching Method - a popular technique used in Decision Support Systems (DSS).

Awesome Lists containing this project

README

          

# SwiftProfileMatching

A Swift Package for implementing Profile Matching Method - a popular technique used in Decision Support Systems (DSS).

## Overview

Profile Matching is a DSS method that compares alternatives against an ideal profile (criteria) to calculate scores and rankings. It's widely applicable in various decision-making scenarios:

- Employee recruitment and selection
- Supplier evaluation
- Product/service selection
- Technology assessment
- Performance appraisal
- Project prioritization
- Site location selection
- Scholarship recipient selection
- And many other multi-criteria decision-making scenarios

This package provides a complete implementation of the Profile Matching method with flexible configuration options designed to be general-purpose and adaptable to your specific needs.

## Development Status

> ⚠️ **Important Notice**: This library is currently under active development and testing. While the core functionality is implemented, you may encounter bugs or changes to the Package. Use in production environments with caution.
>
> **Current Limitations**:
> - Some components still contain hardcoded values that need to be made configurable
> - The gap calculation strategy needs more flexibility for custom implementations
> - Some normalization methods are still being refined
>
> We welcome feedback, bug reports, and contributions as we work toward a stable 1.0 release. Please check the issues section for known limitations and planned improvements.

## Features

- Calculate profile matching scores based on core and secondary factors
- Customizable gap calculation strategies
- Normalization utilities for input data with multiple methods
- Configuration options for weights, scoring methods, and score ranges
- Ranking tools to analyze and present results
- Comprehensive analysis tools (identifying strengths/weaknesses, influential criteria)
- Z-score normalization for comparative analysis

## Installation

### Swift Package Manager

Add the following to your `Package.swift` file:

```swift
dependencies: [
.package(url: "https://github.com/yourusername/SwiftProfileMatching.git", from: "1.0.0")
]
```

Or add it directly in Xcode:
1. File > Add Packages...
2. Enter the repository URL: `https://github.com/yourusername/SwiftProfileMatching.git`
3. Click "Add Package"

Quick Start

Here's a quick example of how to use the package:

```swift
import SwiftProfileMatching

// 1. Define your criteria
let criteria = [
ProfileMatching.Criterion(name: "Experience", targetValue: 4.0, type: .coreFactor, weight: 60),
ProfileMatching.Criterion(name: "Education", targetValue: 5.0, type: .coreFactor, weight: 40),
ProfileMatching.Criterion(name: "Communication", targetValue: 3.0, type: .secondaryFactor, weight: 70),
ProfileMatching.Criterion(name: "Teamwork", targetValue: 4.0, type: .secondaryFactor, weight: 30)
]

// 2. Create alternatives to evaluate
let alternatives = [
ProfileMatching.Alternative(
id: "A001",
name: "Candidate One",
criteriaValues: [
"Experience": 5.0,
"Education": 5.0,
"Communication": 4.0,
"Teamwork": 5.0
]
),
ProfileMatching.Alternative(
id: "A002",
name: "Candidate Two",
criteriaValues: [
"Experience": 3.0,
"Education": 5.0,
"Communication": 5.0,
"Teamwork": 3.0
]
)
]

// 3. Create the profile matching calculator with default configuration
// (Uses .continuous(type: .standard) gap calculation)
let profileMatching = ProfileMatching(criteria: criteria)

// 4. Alternatively, create with a custom configuration
// let config = ProfileMatchingConfiguration(
// gapCalculationStrategy: .continuous(type: .standard),
// coreFactorWeight: 0.6,
// secondaryFactorWeight: 0.4
// )
// let profileMatching = ProfileMatching(criteria: criteria, configuration: config)

// 5. Calculate matching results
let results = profileMatching.calculateMatching(for: alternatives)

// 6. Print detailed results for top candidate
print("Detailed results for \(results[0].alternative.name):")
print("- Core Factor Score: \(results[0].coreFactorScore)")
print("- Secondary Factor Score: \(results[0].secondaryFactorScore)")
print("- Final Score: \(results[0].finalScore)")
print("- Gap Details:")
for (criterion, score) in results[0].gapDetails {
print(" - \(criterion): \(score)")
}

// 7. Generate a ranking report
let report = RankingHelper.createReport(
from: results,
scoreFormat: .percentage
)

// 8. Print the summary
print("\nFinal ranking:")
for (index, result) in results.enumerated() {
print("\(index + 1). \(result.alternative.name): \(result.finalScore)")
}
```

The output will look something like:

```
Detailed results for Candidate One:
- Core Factor Score: 4.7
- Secondary Factor Score: 4.75
- Final Score: 4.72
- Gap Details:
- Experience: 4.5
- Education: 5.0
- Communication: 4.75
- Teamwork: 4.75

Final ranking:
1. Candidate One: 4.72
2. Candidate Two: 4.41
```

Advanced Usage


Screenshot 2025-03-18 at 16 26 18

### Custom Configuration

You can customize the profile matching calculation with a configuration:

```swift
let config = ProfileMatchingConfiguration(
gapCalculationStrategy: .continuous(type: .custom(
perfectMatchScore: 10.0,
exceedsPenalty: 1.0,
belowPenalty: 2.0,
maxScore: 10.0
)),
coreFactorWeight: 0.7,
secondaryFactorWeight: 0.3,
normalizationMethod: .global,
weightCalculation: .normalized,
scoreRange: (0.0, 10.0)
)

let profileMatching = ProfileMatching(criteria: criteria, configuration: config)
```

### Continuous Gap Calculation

For scenarios where you need formula-based gap calculations with smooth transitions between values, use the continuous gap calculation strategies:

```swift
// 1. Standard method (default)
// Different formulas for core and secondary factors
let standardConfig = ProfileMatchingConfiguration(
gapCalculationStrategy: .continuous(type: .standard),
// Other parameters...
)

// 2. Simple method
// Uses normalized absolute difference
let simpleConfig = ProfileMatchingConfiguration(
gapCalculationStrategy: .continuous(type: .simple),
// Other parameters...
)

// 3. Custom method with configurable parameters
let customConfig = ProfileMatchingConfiguration(
gapCalculationStrategy: .continuous(type: .custom(
perfectMatchScore: 10.0, // Score for perfect match (gap = 0)
exceedsPenalty: 1.0, // Penalty per unit when exceeding target
belowPenalty: 2.0, // Penalty per unit when below target
maxScore: 10.0 // Maximum possible score
)),
// Other parameters...
)
```

Understanding continuous calculation types:

1. **Standard**: Uses the classic Profile Matching formula with different calculations for core vs. secondary factors:
- For core factors: Stricter penalties for missing targets
- For secondary factors: More lenient penalties
- Perfect match always gets 5.0

2. **Simple**: Uses normalized absolute difference:
- Formula: 5.0 - min(5.0, abs(gap))
- Provides a linear penalty based on distance from target
- Good for simple cases where you want equal penalties for exceeding or falling below target

3. **Custom**: Fully configurable formula with parameters:
- `perfectMatchScore`: Score assigned for exact matches (gap = 0)
- `exceedsPenalty`: Penalty per unit when exceeding target (gap > 0)
- `belowPenalty`: Penalty per unit when below target (gap < 0)
- `maxScore`: Maximum possible score cap

Example scores using different continuous methods for gap value Δ:

```
Gap (Δ) | Standard (Core) | Standard (Secondary) | Simple | Custom (10,1,2,10)
---------|----------------|---------------------|--------|------------------
-2.0 | 3.0 | 3.5 | 3.0 | 6.0
-1.0 | 4.0 | 4.25 | 4.0 | 8.0
0.0 | 5.0 | 5.0 | 5.0 | 10.0
+1.0 | 4.5 | 4.75 | 4.0 | 9.0
+2.0 | 4.0 | 4.5 | 3.0 | 8.0
```

### Discrete Gap Calculation

For scenarios where gap values should map to discrete scores (like categorical data), use the discrete gap calculation strategy with various handling methods:

```swift
// Create gap-score mapping from a dictionary
let gapScoreMap: [Double: Double] = [
-3.0: 1.0, // Far below target
-2.0: 2.0, // Below target
-1.0: 3.5, // Slightly below target
0.0: 5.0, // Perfect match
1.0: 4.0, // Slightly above target
2.0: 3.0, // Above target
3.0: 2.0 // Far above target
]

let mappingPairs = ProfileMatchingConfiguration.createDiscreteMapping(gapToScoreMap: gapScoreMap)

// Create configuration with different handling methods for undefined gaps:

// 1. Basic method (nearest neighbor approach)
let basicConfig = ProfileMatchingConfiguration(
gapCalculationStrategy: .discrete(mappingPairs: mappingPairs), // Uses .basic by default
// Other parameters...
)

// 2. Linear interpolation between defined points
let interpolationConfig = ProfileMatchingConfiguration(
gapCalculationStrategy: .discrete(mappingPairs: mappingPairs, handlingMethod: .interpolation),
// Other parameters...
)

// 3. Nearest neighbor (finds closest defined gap)
let nearestConfig = ProfileMatchingConfiguration(
gapCalculationStrategy: .discrete(mappingPairs: mappingPairs, handlingMethod: .nearestNeighbor),
// Other parameters...
)

// 4. Threshold method (uses defined point as the start of a range)
let thresholdConfig = ProfileMatchingConfiguration(
gapCalculationStrategy: .discrete(mappingPairs: mappingPairs, handlingMethod: .threshold),
// Other parameters...
)

// 5. Default value (uses specified score for undefined gaps)
let defaultConfig = ProfileMatchingConfiguration(
gapCalculationStrategy: .discrete(mappingPairs: mappingPairs, handlingMethod: .defaultValue(score: 2.0)),
// Other parameters...
)

// Or use convenience methods with handling method:

// Symmetric scoring (same penalty for above/below target)
let symmetricConfig = ProfileMatchingConfiguration.discreteSymmetric(
perfectMatchScore: 5.0,
maxGap: 5,
maxScore: 5.0,
handlingMethod: .threshold // Choose a handling method
)

// Asymmetric scoring (different penalties for above/below target)
let asymmetricConfig = ProfileMatchingConfiguration.discreteAsymmetric(
perfectMatchScore: 5.0,
maxGap: 5,
exceedPenalty: 0.5, // Lower penalty for exceeding target
belowPenalty: 1.0, // Higher penalty for falling below target
maxScore: 5.0,
handlingMethod: .interpolation // Choose a handling method
)
```

Understanding gap handling methods:

1. **Basic**: Simple nearest neighbor approach - finds closest defined gap value.
2. **Interpolation**: Uses linear interpolation between nearest defined points (good for smooth transitions).
3. **Nearest Neighbor**: Explicitly finds the closest defined gap by distance.
4. **Threshold**: Each defined gap value represents the start of a range up to the next defined gap.
5. **Default Value**: Uses a specified default score for any undefined gaps.

Example of different methods on an undefined gap value:

```swift
// For a gap of -1.5 with defined mappings at -2.0 (score 2.0) and -1.0 (score 3.5):
// - Interpolation: 2.75 (halfway between 2.0 and 3.5)
// - Nearest Neighbor: 2.0 (closer to -2.0 than -1.0)
// - Threshold: 2.0 (falls within range starting at -2.0)
// - Default Value: 1.0 (uses the specified default value)
```

### Normalizing Input Values

Use the Normalization utilities to standardize raw input values:

```swift
// Normalize values from different scales to the standard 0-5 scale
let rawValues = ["Criterion1": 75.0, "Criterion2": 80.0]

let ranges = [
"Criterion1": (min: 0.0, max: 100.0),
"Criterion2": (min: 0.0, max: 100.0)
]

let normalized = Normalization.normalizeCriteriaValues(
rawValues,
criteriaRanges: ranges,
targetMin: 0.0,
targetMax: 5.0
)
// Result: ["Criterion1": 3.75, "Criterion2": 4.0]

// Compare alternatives using Z-scores
let scores = ["A001": 4.5, "A002": 3.8, "A003": 4.1]
let zScores = Normalization.normalizeToZScores(scores)
// Compares each alternative's performance relative to the mean
```

### Analysis Tools

Identify strengths and weaknesses of alternatives:

```swift
let (strengths, weaknesses) = RankingHelper.identifyStrengthsAndWeaknesses(
for: result,
threshold: 4.0 // Criteria scored 4.0 or higher are strengths
)

print("Strengths: \(strengths)")
print("Weaknesses: \(weaknesses)")
```

Find which criteria had the most influence on the results:

```swift
let influential = RankingHelper.findMostInfluentialCriteria(from: results)

// Measure how well each criterion differentiates between alternatives
let differentiationPower = RankingHelper.calculateCriteriaDifferentiationPower(from: results)
```

### Formatting Results

Format scores in different ways:

```swift
// Create reports with different formatting options
let rawReport = RankingHelper.createReport(from: results, scoreFormat: .raw)
let percentReport = RankingHelper.createReport(from: results, scoreFormat: .percentage)
let starsReport = RankingHelper.createReport(from: results, scoreFormat: .stars(maxStars: 5))

// Format a specific score
print(rawReport.formatScore(4.5)) // "4.50"
print(percentReport.formatScore(4.5)) // "90.0%"
print(starsReport.formatScore(4.5)) // "⭐⭐⭐⭐⭐"
```

Theory

### Profile Matching Fundamentals

Profile Matching is a multi-criteria decision-making method used to evaluate alternatives by comparing their attribute values against an ideal profile. The process involves several key steps:

1. **Define criteria and target values** - Establish what factors are important and what their ideal values should be
2. **Categorize criteria** - Typically divided into "core factors" (essential criteria) and "secondary factors" (supporting criteria)
3. **Assign weights** - Determine the relative importance of each criterion
4. **Optional: Normalize input values** - Standardize values across different scales (optional, disabled by default)
5. **Calculate gap values** - Measure how closely each alternative matches the ideal profile for each criterion
6. **Calculate weighted scores** - Combine gap values using appropriate weighting schemes
7. **Rank alternatives** - Order alternatives based on their final scores

### Step-by-Step Process

Let's walk through a complete Profile Matching calculation using this package, assuming a job candidate selection scenario:

#### 1. Define Criteria and Target Values

First, we define what we're looking for by specifying criteria and their ideal values:

```swift
let criteria = [
ProfileMatching.Criterion(name: "Experience", targetValue: 4.0, type: .coreFactor, weight: 60),
ProfileMatching.Criterion(name: "Education", targetValue: 5.0, type: .coreFactor, weight: 40),
ProfileMatching.Criterion(name: "Communication", targetValue: 3.0, type: .secondaryFactor, weight: 70),
ProfileMatching.Criterion(name: "Teamwork", targetValue: 4.0, type: .secondaryFactor, weight: 30)
]
```

In this example:
- We have 4 criteria: Experience, Education, Communication, and Teamwork
- Values are on a scale of 0-5
- Experience and Education are core factors (essential requirements)
- Communication and Teamwork are secondary factors (desirable but not critical)

#### 2. Create Alternatives to Evaluate

Next, we define our alternatives to evaluate:

```swift
let alternatives = [
ProfileMatching.Alternative(
id: "A001",
name: "John Smith",
criteriaValues: [
"Experience": 5.0, // Exceeds target
"Education": 5.0, // Matches target
"Communication": 4.0, // Exceeds target
"Teamwork": 5.0 // Exceeds target
]
),
ProfileMatching.Alternative(
id: "A002",
name: "Jane Doe",
criteriaValues: [
"Experience": 3.0, // Below target
"Education": 5.0, // Matches target
"Communication": 5.0, // Exceeds target
"Teamwork": 3.0 // Below target
]
)
]
```

#### 3. Configure Profile Matching (Optional)

By default, the package uses standard settings, but you can optionally configure various aspects including normalization:

```swift
// Optional: Create a custom configuration with normalization enabled
let config = ProfileMatchingConfiguration(
gapCalculationStrategy: .continuous(type: .standard),
coreFactorWeight: 0.6,
secondaryFactorWeight: 0.4,
normalizationMethod: .global, // Enable normalization
weightCalculation: .direct,
scoreRange: (0.0, 5.0)
)

// Initialize with custom configuration
let profileMatching = ProfileMatching(criteria: criteria, configuration: config)

// Or use default configuration (no normalization)
// let profileMatching = ProfileMatching(criteria: criteria)
```

#### 4. Optional: Normalize Input Values

If normalization is enabled in the configuration (it's disabled by default), the package will automatically normalize input values before gap calculation.

**Mathematical Formula:**

$$\text{Normalized Value} = \frac{\text{Value} - \text{Min}}{\text{Max} - \text{Min}} \times (\text{TargetMax} - \text{TargetMin}) + \text{TargetMin}$$

SwiftProfileMatching supports three normalization approaches:

1. **No Normalization (`none`)** - Use raw values directly (default)
2. **Global Normalization (`global`)** - Standardize values across all alternatives
3. **Local Normalization (`local`)** - Normalize each criterion separately

When enabled, the package internally performs normalization:

```swift
// This happens internally if normalization is enabled
// Excerpt from calculateMatchingForAlternative() method
let processedValues = normalizeInputValues(alternative.criteriaValues)
```

In our default example, normalization is skipped since we're using the same 0-5 scale for all criteria.

#### 5. Calculate Gap Values

For each criterion, we calculate how closely the alternative matches the target.

**Mathematical Formula (Standard Method):**

For core factors:

For perfect match (Δ = 0):
$$g_i = 5.0$$

For exceeding target (Δ > 0):
$$g_i = \min(4.5, 5.0 - 0.5 \times \Delta)$$

For below target (Δ < 0):
$$g_i = \max(0, 5.0 + \Delta)$$

For secondary factors:

For perfect match (Δ = 0):
$$g_i = 5.0$$

For exceeding target (Δ > 0):
$$g_i = \min(5.0, 5.0 - 0.25 \times \Delta)$$

For below target (Δ < 0):
$$g_i = \max(0, 5.0 + 0.75 \times \Delta)$$

Where $\Delta = \text{Actual Value} - \text{Target Value}$

**Custom Gap Calculation Method:**

For perfect match (Δ = 0):
$$g_i = P$$

For exceeding target (Δ > 0):
$$g_i = \max(0, \min(M, P - E \times \Delta))$$

For below target (Δ < 0):
$$g_i = \max(0, \min(M, P + B \times \Delta))$$

Where:
- $P$ = Perfect match score
- $E$ = Exceeds penalty
- $B$ = Below penalty
- $M$ = Maximum possible score
- $\Delta = \text{Actual Value} - \text{Target Value}$

**Discrete Gap Calculation Method:**

Instead of using continuous functions, this method uses explicit gap-to-score mappings:

$$g_i = \text{score from mapping table where gap} = \Delta$$

For gaps between defined values, linear interpolation is used:

$$g_i = s_1 + (s_2 - s_1) \times \frac{\Delta - g_1}{g_2 - g_1}$$

Where:
- $s_1, s_2$ = Scores for the two closest defined gaps
- $g_1, g_2$ = Gap values that bracket the actual gap
- $\Delta = \text{Actual Value} - \text{Target Value}$

This allows for arbitrary and non-linear scoring patterns that can't be expressed with simple mathematical formulas.

For John Smith:
- Experience: Target 4.0, Actual 5.0, Gap = +1.0
- Core factor exceeding target: 5.0 - (0.5 × 1.0) = 4.5
- Education: Target 5.0, Actual 5.0, Gap = 0
- Perfect match: 5.0
- Communication: Target 3.0, Actual 4.0, Gap = +1.0
- Secondary factor exceeding target: 5.0 - (0.25 × 1.0) = 4.75
- Teamwork: Target 4.0, Actual 5.0, Gap = +1.0
- Secondary factor exceeding target: 5.0 - (0.25 × 1.0) = 4.75

For Jane Doe:
- Experience: Target 4.0, Actual 3.0, Gap = -1.0
- Core factor below target: 5.0 + (-1.0) = 4.0
- Education: Target 5.0, Actual 5.0, Gap = 0
- Perfect match: 5.0
- Communication: Target 3.0, Actual 5.0, Gap = +2.0
- Secondary factor exceeding target: 5.0 - (0.25 × 2.0) = 4.5
- Teamwork: Target 4.0, Actual 3.0, Gap = -1.0
- Secondary factor below target: 5.0 + (0.75 × -1.0) = 4.25

These calculations are performed by the `calculateGap()` method:

```swift
// The package internally calculates the gap values
for criterion in criteria {
if let value = processedValues[criterion.name] {
let gap = calculateGap(targetValue: criterion.targetValue,
actualValue: value,
type: criterion.type)
gapDetails[criterion.name] = gap
}
}
```

#### 6. Calculate Core Factor and Secondary Factor Scores

Next, we calculate weighted scores for core and secondary factors separately.

**Mathematical Formula:**

$$\text{Factor Score} = \frac{\sum_{i=1}^{n} (w_i \times g_i)}{\sum_{i=1}^{n} w_i}$$

Where:
- $w_i$ = Weight of criterion $i$ (in decimal form)
- $g_i$ = Gap value for criterion $i$
- $n$ = Number of criteria in the factor group

For John Smith:
- Core Factor Score:
- Experience (weight 60%): 4.5
- Education (weight 40%): 5.0
- Weighted Average: (4.5 × 0.6) + (5.0 × 0.4) = 4.7
- Secondary Factor Score:
- Communication (weight 70%): 4.75
- Teamwork (weight 30%): 4.75
- Weighted Average: (4.75 × 0.7) + (4.75 × 0.3) = 4.75

For Jane Doe:
- Core Factor Score:
- Experience (weight 60%): 4.0
- Education (weight 40%): 5.0
- Weighted Average: (4.0 × 0.6) + (5.0 × 0.4) = 4.4
- Secondary Factor Score:
- Communication (weight 70%): 4.5
- Teamwork (weight 30%): 4.25
- Weighted Average: (4.5 × 0.7) + (4.25 × 0.3) = 4.425

The package automatically handles these weighted calculations:

```swift
// Calculated by the package through the calculateWeightedScore() method
let coreFactorScore = calculateWeightedScore(for: coreFactors, using: gapDetails)
let secondaryFactorScore = calculateWeightedScore(for: secondaryFactors, using: gapDetails)
```

#### 7. Calculate Final Scores

Finally, we combine core and secondary factor scores with their respective weights.

**Mathematical Formula:**

$$\text{Final Score} = (W_{cf} \times \text{CF}) + (W_{sf} \times \text{SF})$$

Where:
- $W_{cf}$ = Weight for core factors (typically 0.6 or 60%)
- $\text{CF}$ = Core factor score
- $W_{sf}$ = Weight for secondary factors (typically 0.4 or 40%)
- $\text{SF}$ = Secondary factor score

For John Smith:
- Final Score: (4.7 × 0.6) + (4.75 × 0.4) = 2.82 + 1.9 = 4.72

For Jane Doe:
- Final Score: (4.4 × 0.6) + (4.425 × 0.4) = 2.64 + 1.77 = 4.41

The package performs this calculation:

```swift
// Calculated by the package
finalScore = (configuration.coreFactorWeight * coreFactorScore) +
(configuration.secondaryFactorWeight * secondaryFactorScore)
```

#### 8. Rank and Analyze

The package then sorts alternatives by final score:

```swift
// John Smith: 4.72
// Jane Doe: 4.41
let results = profileMatching.calculateMatching(for: alternatives)
// results[0] would be John Smith
// results[1] would be Jane Doe
```

For deeper analysis, we can:

```swift
// Generate a formatted report
let report = RankingHelper.createReport(from: results)

// Find strengths and weaknesses
let (strengths, weaknesses) = RankingHelper.identifyStrengthsAndWeaknesses(
for: results[0], // John Smith
threshold: 4.5 // Criteria with scores above 4.5 are strengths
)
// strengths = ["Education", "Communication", "Teamwork"]
// weaknesses = []

// Identify most influential criteria
let influential = RankingHelper.findMostInfluentialCriteria(from: results)
// Would show which criteria created the most differentiation
```

### Core vs. Secondary Factors

Profile Matching distinguishes between different types of criteria:

- **Core Factors** - Essential criteria that are critical to the decision (e.g., education level for a job position)
- **Secondary Factors** - Supporting criteria that are beneficial but not critical (e.g., communication skills)

These are implemented as the `GapType` enum:

```swift
public enum GapType: Sendable {
case coreFactor
case secondaryFactor
}
```

### Weighted Scoring

Once gap values are calculated, they are combined using weighted averages. The package supports two weighting approaches:

1. **Direct Weights** - Use weights as provided (must sum to 100%)
2. **Normalized Weights** - Automatically normalize weights to ensure they sum to 100%

```swift
// Implemented in calculateWeightedScore()
var totalWeight = 0.0
var weightedSum = 0.0

for criterion in criteria {
guard let gapValue = gapDetails[criterion.name] else { continue }

// Convert percentage weights to decimal
let normalizedWeight = criterion.weight / 100.0
weightedSum += normalizedWeight * gapValue
totalWeight += normalizedWeight
}

// Normalize by total weight
return totalWeight > 0 ? weightedSum / totalWeight : 0
```

### Ranking and Analysis

After scoring alternatives, additional analysis can provide deeper insights:

1. **Influential Criteria Analysis** - Identify which criteria had the most impact on differentiation
2. **Strengths and Weaknesses** - Highlight areas where alternatives perform particularly well or poorly
3. **Z-Score Analysis** - Compare alternatives against the statistical distribution of all alternatives

```swift
// Example of differentiation power calculation
public static func calculateCriteriaDifferentiationPower(from results: [MatchingResult]) -> [String: Double] {
// Find all unique criteria
var allCriteria = Set()
for result in results {
allCriteria.formUnion(result.gapDetails.keys)
}

// Calculate standard deviation for each criterion
var differentiationPower = [String: Double]()

for criterion in allCriteria {
let values = results.compactMap { $0.gapDetails[criterion] }
guard values.count > 1 else {
differentiationPower[criterion] = 0.0
continue
}

// Calculate standard deviation
let mean = values.reduce(0.0, +) / Double(values.count)
let variance = values.reduce(0.0) { sum, value in
let diff = value - mean
return sum + (diff * diff)
} / Double(values.count)

let stdDev = sqrt(variance)
differentiationPower[criterion] = stdDev
}

return differentiationPower
}
```