Scoring System Documentation#
Last Updated: 2026-03-04
Overview#
Drydown uses an ideal score anchoring system to calculate personalized review scores. Instead of multiplying weights, the system calculates how close a review is to the user's ideal preferences across all rating properties.
Core Concept#
Users set preferences (1-5 scale) that map to their "ideal" ratings. When viewing a review, the system:
- Calculates the distance between each review property and the user's ideal
- Applies a power 1.5 curve to convert distance → fit percentage
- Weights each property by importance
- Sums weighted fits to produce final score (0-5 scale)
Preference Mapping#
presenceStyle → Projection/Sillage Ideals#
| Value | Statement | Ideal Projection | Ideal Sillage |
|---|---|---|---|
| 1 | "Skin scent only — just for me" | 1 | 1 |
| 2 | "Close company — noticeable only up close" | 2 | 2 |
| 3 | "Balanced — depends on the occasion" | 3 | 3 |
| 4 | "Room presence — I enjoy being noticed" | 4 | 4 |
| 5 | "Announce myself — I like to make an entrance" | 5 | 5 |
longevityPriority → Longevity Ideal#
| Value | Statement | Ideal Longevity |
|---|---|---|
| 1 | "Not important — I enjoy reapplying" | 1 |
| 2 | "Nice to have — but not a dealbreaker" | 2 |
| 3 | "Moderately important" | 3 |
| 4 | "Very important — I want it to last" | 4 |
| 5 | "Essential — all-day performance" | 5 |
complexityPreference → Complexity Ideal#
| Value | Statement | Ideal Complexity |
|---|---|---|
| 1 | "Simple and focused — clean scents" | 1 |
| 2 | "Mostly simple — with maybe a twist" | 2 |
| 3 | "Balanced — both simple and complex" | 3 |
| 4 | "Layered — discovering notes" | 4 |
| 5 | "Intricate — depth and evolution" | 5 |
scoringApproach → Property Importance Adjustments#
| Value | Statement | Effect |
|---|---|---|
| 1 | "Pure instinct — gut reaction matters most" | overallRating importance × 1.3 |
| 2 | "Mostly instinct — but I consider details" | overallRating importance × 1.3 |
| 3 | "Balanced — feeling and analysis equal" | No adjustments |
| 4 | "Mostly analytical — weigh each aspect" | Technical properties × 1.1-1.2 |
| 5 | "Fully analytical — numbers tell the story" | Technical properties × 1.1-1.2 |
Technical properties boosted (scoringApproach ≥ 4):
complexity× 1.2longevity× 1.2sillage× 1.1midProjection× 1.1openingProjection× 1.1
Quality Rating Ideals#
All quality ratings always use ideal = 5:
openingRatingdrydownRatingendRatingoverallRating
Everyone wants "best" quality, so these properties measure distance from perfection.
Property Importance Rankings#
Base importance values (before scoringApproach adjustments):
| Rank | Property | Importance | Category |
|---|---|---|---|
| 1 | midProjection | 1.5 | Performance |
| 2 | openingProjection | 1.45 | Performance |
| 3 | complexity | 1.4 | Technical |
| 4 | longevity | 1.35 | Technical |
| 5 | sillage | 1.3 | Performance |
| 6 | drydownRating | 1.25 | Quality |
| 7 | openingRating | 1.2 | Quality |
| 8 | endRating | 1.15 | Quality |
| 9 | overallRating | 1.1 | Gut check |
Design rationale:
- Projection metrics matter most (user feedback priority)
- Technical properties (complexity, longevity) come next
- Quality ratings follow
- Overall gut check is least important (captured by other ratings)
Distance to Fit Conversion#
The power 1.5 curve penalizes mismatches moderately (more than linear, less than squared):
normalizedDistance = Math.abs(actual - ideal) / 4
fit = Math.pow(1 - normalizedDistance, 1.5)
| Distance | Normalized | Fit % | Interpretation |
|---|---|---|---|
| 0 | 0.00 | 100% | Perfect match |
| 1 | 0.25 | 65% | Close match |
| 2 | 0.50 | 35% | Moderate mismatch |
| 3 | 0.75 | 13% | Poor match |
| 4 | 1.00 | 0% | Opposite of ideal |
Score Calculation Algorithm#
function calculateIdealBasedScore(review, preferences) {
// 1. Map preferences → ideal scores
const idealScores = preferencesToIdealScores(preferences)
// 2. Adjust importance based on scoringApproach
const importance = adjustImportanceForScoringApproach(
preferences.scoringApproach
)
// 3. Calculate weighted fit for each property
let totalWeightedFit = 0
let totalImportance = 0
for (const property of ALL_PROPERTIES) {
if (review[property] === undefined) continue
const distance = Math.abs(review[property] - idealScores[property])
const normalizedDistance = distance / 4
const fit = Math.pow(1 - normalizedDistance, 1.5)
const weightedFit = fit * importance[property]
totalWeightedFit += weightedFit
totalImportance += importance[property]
}
// 4. Normalize to 0-5 scale
const score = (totalWeightedFit / totalImportance) * 5
return Math.round(score * 1000) / 1000
}
Personalized Score Display#
Users can toggle between two score perspectives:
"Their perspective" (scoreLens='theirs')#
Shows the reviewer's original score (stored in weightedScore field).
"Your perspective" (scoreLens='mine')#
Recalculates the score using the viewer's ideal preferences.
Special case: Own reviews always show original score (no recalculation).
API Reference#
Core Functions#
calculateIdealBasedScore(review, preferences): number#
Calculates score based on distance from user's ideal preferences.
Parameters:
review: Review record with rating propertiespreferences:UserPreferencesForScoringobject
Returns: Score from 0-5 (rounded to 3 decimal places)
getPersonalizedScore(review, userPreferences?, isOwnReview): { score, isPersonalized }#
Get display score with personalization support.
Parameters:
review: Review recorduserPreferences: Optional user preferencesisOwnReview: Whether review belongs to current user
Returns:
score: Calculated score (0-5)isPersonalized: Whether score was recalculated with user preferences
Helper Functions#
preferencesToIdealScores(preferences): IdealScores#
Converts user preferences to ideal score values.
adjustImportanceForScoringApproach(scoringApproach): PropertyImportance#
Adjusts property importance based on scoring approach preference.
Examples#
Example 1: Matching Preferences#
User preferences:
{
presenceStyle: 5, // "Announce myself"
longevityPriority: 5, // "All-day essential"
complexityPreference: 4, // "Layered"
scoringApproach: 3 // Balanced
}
Review ratings:
{
openingProjection: 5, // Room-filling
midProjection: 5, // Strong presence
sillage: 5, // Noticeable trail
longevity: 5, // All-day lasting
complexity: 4, // Very layered
// ... other ratings
}
Result: High score (4-5 range) because review closely matches user ideals.
Example 2: Mismatched Preferences#
User preferences:
{
presenceStyle: 1, // "Skin scent only"
longevityPriority: 2, // "Nice to have"
complexityPreference: 1, // "Simple and focused"
scoringApproach: 1 // "Pure instinct"
}
Same review as above
Result: Low score (2-3 range) because review is opposite of user ideals (projection distance = 4, complexity distance = 3).
Example 3: Partial Mismatch#
User preferences:
{
presenceStyle: 5, // Matches review
longevityPriority: 1, // "Not important"
complexityPreference: 5, // "Intricate"
scoringApproach: 3
}
Review ratings:
{
openingProjection: 5, // ✓ Matches ideal
midProjection: 5, // ✓ Matches ideal
sillage: 5, // ✓ Matches ideal
longevity: 5, // ✗ Doesn't matter (user ideal = 1)
complexity: 2, // ✗ Poor match (user ideal = 5)
// ... other ratings
}
Result: Moderate score (3-4 range). Projection matches perfectly, but longevity "overperformance" and low complexity reduce fit.
Migration from Weight-Based System#
No migration needed! The new system:
- Uses the same 4 preferences (no schema changes)
- Keeps the same UI (no interface changes)
- Maintains backward compatibility (existing data works unchanged)
The only change is how preferences are interpreted by the scoring algorithm.