Pattern 6: Composite Health Scoring
Intent
Transform raw interaction data into a single, actionable health score (0-100) that reflects overall relationship quality, predicts risk, and enables tier-based segmentation and prioritization.
Also Known As
- Engagement Score
- Health Index
- Relationship Score
- Vitality Metric
- Customer Health Score
Problem
Raw interaction data is overwhelming and hard to act on.
Pattern 1 (Universal Event Log) gives Sarah comprehensive data: - Martinez family: 247 logged interactions over 8 months - 23 emails sent, 6 opened (26% open rate) - 4 SMS sent, 0 replies - 2 phone calls, 1 answered - Portal: last login 47 days ago - Payments: 2 of 4 late (average 12 days) - Events: attended 2 of 8 (25%) - Volunteer hours: 0
But Sarah needs one number: "How healthy is this relationship?"
Without a composite score: - Can't quickly triage 100 families - Can't prioritize who needs attention - Can't compare families objectively - Can't track changes over time - Can't trigger automated actions
The challenge: Combine dozens of behavioral signals into single actionable metric.
Context
When this pattern applies:
- Multiple interaction types tracked (not just one metric)
- Need to prioritize attention across many entities
- Want to trigger automated interventions based on health
- Building predictive models (health score as feature)
- Leadership needs simple dashboard metric
When this pattern may not be needed:
- Only tracking one or two simple metrics (can use those directly)
- Very small scale where human intuition suffices (<20 entities)
- Relationships too complex for single score (may need multi-dimensional)
Forces
Competing concerns:
1. Simplicity vs Accuracy - Single score is actionable - But compressing complexity loses nuance - Balance: Composite score plus ability to drill down to components
2. Universal vs Domain-Specific - Want formula that works across domains - But each domain has unique health signals - Balance: Framework with domain-specific weights
3. Objective vs Subjective - Calculated scores are objective, consistent - But miss qualitative signals humans detect - Balance: Algorithmic score with manual override option
4. Absolute vs Relative - Score 75/100 - is that good? - Depends on context and distribution - Balance: Both absolute score and percentile rank
5. Current vs Trend - Current score shows status now - But trend shows direction (improving/declining) - Balance: Track both score and velocity
Solution
Build a composite health score from multiple weighted components:
Formula framework:
Health Score = Σ (Component Score × Component Weight)
Where:
- Each component measures specific behavior (0-100)
- Weights reflect relative importance
- Sum of weights = 1.0
- Final score scaled to 0-100
Common components:
1. Communication Responsiveness (20-30% weight) - Email open rate - Email click rate - SMS response rate - Phone answer rate - Response time to messages
2. Platform Engagement (15-25% weight) - Portal login frequency - Pages viewed per session - Actions taken - Time since last login - Feature usage breadth
3. Financial Health (20-30% weight) - Payment on-time rate - Days late when late - Outstanding balance age - Payment method reliability - Billing issue frequency
4. Participation/Activity (15-25% weight) - Event attendance rate - Volunteer hours - Community involvement - Content contribution - Referrals made
5. Relationship Tenure (5-15% weight) - Time since enrollment - Consistency over time - Historical engagement - Churn risk indicators
6. Sentiment/Satisfaction (optional 10-20%) - Survey responses - Support ticket sentiment - Review/rating scores - Qualitative feedback
Structure
Health Metrics Tables
-- Store calculated health scores
CREATE TABLE family_engagement_metrics (
metric_id INT PRIMARY KEY IDENTITY(1,1),
family_id INT NOT NULL,
-- Composite score
engagement_score DECIMAL(5,2) NOT NULL, -- 0-100
engagement_tier VARCHAR(20), -- 'high', 'medium', 'low', 'at_risk'
-- Component scores (each 0-100)
communication_score DECIMAL(5,2),
platform_engagement_score DECIMAL(5,2),
financial_health_score DECIMAL(5,2),
participation_score DECIMAL(5,2),
tenure_score DECIMAL(5,2),
-- Metadata
calculation_date DATETIME2 DEFAULT GETDATE(),
calculation_period_days INT DEFAULT 90, -- Lookback window
-- Change tracking
previous_score DECIMAL(5,2),
score_delta DECIMAL(5,2), -- Change since last calculation
score_velocity VARCHAR(20), -- 'improving', 'stable', 'declining'
-- Percentile (relative to all families)
percentile_rank DECIMAL(5,2), -- 0-100, 90 = better than 90% of families
CONSTRAINT FK_metrics_family FOREIGN KEY (family_id)
REFERENCES families(family_id),
CONSTRAINT UQ_family_metric UNIQUE (family_id)
);
-- Index for common queries
CREATE INDEX IX_engagement_score ON family_engagement_metrics(engagement_score DESC);
CREATE INDEX IX_engagement_tier ON family_engagement_metrics(engagement_tier);
CREATE INDEX IX_score_velocity ON family_engagement_metrics(score_velocity);
-- Store component calculation details
CREATE TABLE score_components (
component_id INT PRIMARY KEY IDENTITY(1,1),
metric_id INT NOT NULL,
component_name VARCHAR(100),
raw_value DECIMAL(10,2), -- Actual measured value
normalized_score DECIMAL(5,2), -- 0-100 normalized
weight DECIMAL(4,3), -- Weight in composite
contribution DECIMAL(5,2), -- weighted_score * weight
CONSTRAINT FK_component_metric FOREIGN KEY (metric_id)
REFERENCES family_engagement_metrics(metric_id)
);
Implementation
Health Score Calculator
class HealthScoreCalculator {
constructor(db, config = {}) {
this.db = db;
this.lookbackDays = config.lookbackDays || 90;
// Component weights (must sum to 1.0)
this.weights = config.weights || {
communication: 0.25,
platform: 0.20,
financial: 0.30,
participation: 0.15,
tenure: 0.10
};
// Validate weights sum to 1.0
const sum = Object.values(this.weights).reduce((a, b) => a + b, 0);
if (Math.abs(sum - 1.0) > 0.01) {
throw new Error(`Weights must sum to 1.0, got ${sum}`);
}
}
async calculateForFamily(familyId) {
const cutoffDate = this.getCutoffDate();
// Calculate each component
const components = {
communication: await this.calculateCommunicationScore(familyId, cutoffDate),
platform: await this.calculatePlatformScore(familyId, cutoffDate),
financial: await this.calculateFinancialScore(familyId, cutoffDate),
participation: await this.calculateParticipationScore(familyId, cutoffDate),
tenure: await this.calculateTenureScore(familyId, cutoffDate)
};
// Calculate composite score
let compositeScore = 0;
const componentDetails = [];
for (const [name, score] of Object.entries(components)) {
const weight = this.weights[name];
const contribution = score * weight;
compositeScore += contribution;
componentDetails.push({
component_name: name,
raw_value: score,
normalized_score: score,
weight: weight,
contribution: contribution
});
}
// Get previous score for delta calculation
const previous = await this.getPreviousScore(familyId);
const scoreDelta = previous ? compositeScore - previous.engagement_score : 0;
const velocity = this.determineVelocity(scoreDelta);
// Determine tier
const tier = this.determineTier(compositeScore);
// Calculate percentile
const percentile = await this.calculatePercentile(compositeScore);
// Save to database
await this.saveMetrics(familyId, {
engagement_score: compositeScore,
engagement_tier: tier,
components: components,
previous_score: previous?.engagement_score,
score_delta: scoreDelta,
score_velocity: velocity,
percentile_rank: percentile
}, componentDetails);
return {
familyId,
score: compositeScore,
tier,
delta: scoreDelta,
velocity,
percentile,
components,
componentDetails
};
}
getCutoffDate() {
const date = new Date();
date.setDate(date.getDate() - this.lookbackDays);
return date;
}
async calculateCommunicationScore(familyId, cutoffDate) {
const results = await this.db.query(`
SELECT
-- Email metrics
SUM(CASE WHEN interaction_type = 'email_sent' THEN 1 ELSE 0 END) as emails_sent,
SUM(CASE WHEN interaction_type = 'email_opened' THEN 1 ELSE 0 END) as emails_opened,
SUM(CASE WHEN interaction_type = 'email_clicked' THEN 1 ELSE 0 END) as emails_clicked,
-- SMS metrics
SUM(CASE WHEN interaction_type = 'sms_sent' THEN 1 ELSE 0 END) as sms_sent,
SUM(CASE WHEN interaction_type = 'sms_replied' THEN 1 ELSE 0 END) as sms_replied,
-- Phone metrics
SUM(CASE WHEN interaction_type = 'phone_call_made' THEN 1 ELSE 0 END) as calls_made,
SUM(CASE WHEN interaction_type = 'phone_call_made' AND outcome = 'answered' THEN 1 ELSE 0 END) as calls_answered
FROM interaction_log
WHERE family_id = ?
AND interaction_timestamp >= ?
`, [familyId, cutoffDate]);
const r = results[0];
// Calculate sub-scores
const emailScore = r.emails_sent > 0
? ((r.emails_opened / r.emails_sent) * 50 + (r.emails_clicked / r.emails_sent) * 50)
: 0;
const smsScore = r.sms_sent > 0
? (r.sms_replied / r.sms_sent) * 100
: 0;
const phoneScore = r.calls_made > 0
? (r.calls_answered / r.calls_made) * 100
: 0;
// Weighted average (email weighted higher if used more)
const totalOutreach = r.emails_sent + r.sms_sent + r.calls_made;
if (totalOutreach === 0) return 50; // No data = neutral score
const score = (
(emailScore * r.emails_sent) +
(smsScore * r.sms_sent) +
(phoneScore * r.calls_made)
) / totalOutreach;
return Math.min(100, Math.max(0, score));
}
async calculatePlatformScore(familyId, cutoffDate) {
const results = await this.db.query(`
SELECT
COUNT(DISTINCT DATE(interaction_timestamp)) as days_active,
COUNT(CASE WHEN interaction_type = 'portal_login' THEN 1 END) as logins,
COUNT(CASE WHEN interaction_type = 'portal_page_viewed' THEN 1 END) as pages_viewed,
COUNT(CASE WHEN interaction_type LIKE '%_downloaded' OR interaction_type LIKE '%_submitted' THEN 1 END) as actions_taken,
MAX(interaction_timestamp) as last_activity
FROM interaction_log
WHERE family_id = ?
AND channel = 'portal'
AND interaction_timestamp >= ?
`, [familyId, cutoffDate]);
const r = results[0];
if (!r.logins) return 0; // No portal use
// Multiple factors
const daysSinceActivity = Math.floor((Date.now() - new Date(r.last_activity)) / (1000 * 60 * 60 * 24));
const recencyScore = Math.max(0, 100 - (daysSinceActivity * 2)); // 2 points per day
const frequencyScore = Math.min(100, (r.days_active / this.lookbackDays) * 100 * 5); // 5x multiplier
const engagementDepth = r.logins > 0
? Math.min(100, (r.pages_viewed / r.logins) * 20 + (r.actions_taken / r.logins) * 30)
: 0;
// Weighted combination
const score = (recencyScore * 0.4) + (frequencyScore * 0.3) + (engagementDepth * 0.3);
return Math.min(100, Math.max(0, score));
}
async calculateFinancialScore(familyId, cutoffDate) {
const results = await this.db.query(`
SELECT
COUNT(*) as total_payments,
SUM(CASE WHEN outcome = 'paid_on_time' THEN 1 ELSE 0 END) as on_time_payments,
AVG(CASE WHEN outcome = 'paid_late' THEN CAST(JSON_VALUE(metadata, '$.days_late') AS INT) ELSE 0 END) as avg_days_late,
SUM(CASE WHEN outcome = 'unpaid' THEN 1 ELSE 0 END) as unpaid_count,
MAX(CASE WHEN outcome = 'unpaid' THEN DATEDIFF(day, interaction_timestamp, GETDATE()) ELSE 0 END) as oldest_unpaid_days
FROM interaction_log
WHERE family_id = ?
AND interaction_type = 'payment_received' OR interaction_type = 'payment_due'
AND interaction_timestamp >= ?
`, [familyId, cutoffDate]);
const r = results[0];
if (r.total_payments === 0) return 50; // No payment history = neutral
// On-time rate (most important)
const onTimeScore = (r.on_time_payments / r.total_payments) * 100;
// Penalty for late payments
const latenessPenalty = Math.min(50, r.avg_days_late * 2); // Max 50 point penalty
// Penalty for unpaid balance
const unpaidPenalty = r.unpaid_count > 0
? Math.min(40, r.unpaid_count * 10 + r.oldest_unpaid_days * 0.5)
: 0;
const score = onTimeScore - latenessPenalty - unpaidPenalty;
return Math.min(100, Math.max(0, score));
}
async calculateParticipationScore(familyId, cutoffDate) {
const results = await this.db.query(`
SELECT
-- Events
COUNT(CASE WHEN interaction_type = 'event_attended' THEN 1 END) as events_attended,
COUNT(CASE WHEN interaction_type = 'event_invited' THEN 1 END) as events_invited,
-- Volunteering
SUM(CASE WHEN interaction_type = 'volunteer_hours_logged'
THEN CAST(JSON_VALUE(metadata, '$.hours') AS DECIMAL)
ELSE 0 END) as volunteer_hours,
-- Community
COUNT(CASE WHEN interaction_type = 'referral_made' THEN 1 END) as referrals_made,
COUNT(CASE WHEN interaction_type = 'feedback_submitted' THEN 1 END) as feedback_given
FROM interaction_log
WHERE family_id = ?
AND interaction_timestamp >= ?
`, [familyId, cutoffDate]);
const r = results[0];
// Event attendance rate
const attendanceScore = r.events_invited > 0
? (r.events_attended / r.events_invited) * 100
: 50; // No invites = neutral
// Volunteer contribution
const volunteerScore = Math.min(100, (r.volunteer_hours || 0) * 10); // 10 points per hour, max 100
// Community contribution
const communityScore = Math.min(100, (r.referrals_made * 20) + (r.feedback_given * 10));
// Weighted average
const score = (attendanceScore * 0.5) + (volunteerScore * 0.3) + (communityScore * 0.2);
return Math.min(100, Math.max(0, score));
}
async calculateTenureScore(familyId, cutoffDate) {
const results = await this.db.query(`
SELECT
enrollment_date,
DATEDIFF(day, enrollment_date, GETDATE()) as days_enrolled
FROM families
WHERE family_id = ?
`, [familyId]);
const r = results[0];
const daysEnrolled = r.days_enrolled;
// Tenure bonus increases over time, plateaus at 2 years
// New families: lower score (higher churn risk)
// Established families: higher score (proven loyalty)
if (daysEnrolled < 30) return 20; // Very new
if (daysEnrolled < 90) return 40; // First semester
if (daysEnrolled < 180) return 60; // Second semester
if (daysEnrolled < 365) return 75; // First year
if (daysEnrolled < 730) return 90; // Second year
return 100; // 2+ years = maximum tenure score
}
determineTier(score) {
if (score >= 80) return 'high';
if (score >= 60) return 'medium';
if (score >= 40) return 'low';
return 'at_risk';
}
determineVelocity(scoreDelta) {
if (scoreDelta > 5) return 'improving';
if (scoreDelta < -5) return 'declining';
return 'stable';
}
async calculatePercentile(score) {
const result = await this.db.query(`
SELECT COUNT(*) as lower_count
FROM family_engagement_metrics
WHERE engagement_score < ?
`, [score]);
const totalResult = await this.db.query(`
SELECT COUNT(*) as total_count
FROM family_engagement_metrics
`);
const percentile = (result[0].lower_count / totalResult[0].total_count) * 100;
return Math.round(percentile);
}
async getPreviousScore(familyId) {
const result = await this.db.query(`
SELECT engagement_score, calculation_date
FROM family_engagement_metrics
WHERE family_id = ?
`, [familyId]);
return result[0] || null;
}
async saveMetrics(familyId, metrics, componentDetails) {
// Upsert main metrics
await this.db.query(`
INSERT INTO family_engagement_metrics (
family_id, engagement_score, engagement_tier,
communication_score, platform_engagement_score, financial_health_score,
participation_score, tenure_score,
previous_score, score_delta, score_velocity, percentile_rank,
calculation_period_days
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT (family_id) DO UPDATE SET
engagement_score = EXCLUDED.engagement_score,
engagement_tier = EXCLUDED.engagement_tier,
communication_score = EXCLUDED.communication_score,
platform_engagement_score = EXCLUDED.platform_engagement_score,
financial_health_score = EXCLUDED.financial_health_score,
participation_score = EXCLUDED.participation_score,
tenure_score = EXCLUDED.tenure_score,
previous_score = family_engagement_metrics.engagement_score,
score_delta = EXCLUDED.score_delta,
score_velocity = EXCLUDED.score_velocity,
percentile_rank = EXCLUDED.percentile_rank,
calculation_date = GETDATE()
`, [
familyId,
metrics.engagement_score,
metrics.engagement_tier,
metrics.components.communication,
metrics.components.platform,
metrics.components.financial,
metrics.components.participation,
metrics.components.tenure,
metrics.previous_score,
metrics.score_delta,
metrics.score_velocity,
metrics.percentile_rank,
this.lookbackDays
]);
}
async calculateForAllFamilies() {
const families = await this.db.query(`
SELECT family_id FROM families WHERE enrolled_current_semester = 1
`);
const results = [];
for (const family of families) {
const result = await this.calculateForFamily(family.family_id);
results.push(result);
}
return results;
}
}
module.exports = HealthScoreCalculator;
Usage Example
const calculator = new HealthScoreCalculator(db, {
lookbackDays: 90,
weights: {
communication: 0.25,
platform: 0.20,
financial: 0.30,
participation: 0.15,
tenure: 0.10
}
});
// Calculate for one family
const result = await calculator.calculateForFamily(187);
console.log(`
Family ${result.familyId} Health Report:
Overall Score: ${result.score.toFixed(1)}/100
Tier: ${result.tier}
Trend: ${result.velocity} (${result.delta > 0 ? '+' : ''}${result.delta.toFixed(1)} points)
Percentile: ${result.percentile}th (better than ${result.percentile}% of families)
Component Breakdown:
Communication: ${result.components.communication.toFixed(1)}
Platform: ${result.components.platform.toFixed(1)}
Financial: ${result.components.financial.toFixed(1)}
Participation: ${result.components.participation.toFixed(1)}
Tenure: ${result.components.tenure.toFixed(1)}
`);
// Calculate for all families (run nightly)
const allResults = await calculator.calculateForAllFamilies();
console.log(`Calculated health scores for ${allResults.length} families`);
Variations
By Domain
Homeschool Co-op:
weights: {
communication: 0.25,
platform: 0.20,
financial: 0.30, // Payment reliability crucial
participation: 0.15,
tenure: 0.10
}
Property Management:
weights: {
communication: 0.20,
platform: 0.10, // Less portal reliance
financial: 0.40, // Rent payment most critical
maintenance: 0.20, // Request frequency, issue resolution
tenure: 0.10
}
SaaS Product:
weights: {
communication: 0.15,
platform: 0.40, // Product usage most important
financial: 0.25, // Payment + expansion
support_tickets: 0.10,
tenure: 0.10
}
By Sophistication Level
Simple (3 components): - Activity (50%) - just using the product - Financial (30%) - paying on time - Tenure (20%) - how long they've been around
Standard (5 components): - As shown in main implementation
Advanced (7+ components): - Add sentiment analysis - Add NPS/satisfaction scores - Add feature adoption depth - Add referral quality - Add support interaction quality
By Calculation Frequency
Real-time: - Update on every interaction - Good for: immediate triggering, live dashboards - Cost: high compute, frequent writes
Daily: - Calculate nightly for all families - Good for: most use cases - Cost: moderate, predictable
Weekly: - Calculate once per week - Good for: low-urgency scenarios - Cost: minimal
On-demand: - Calculate when needed (viewing family profile) - Good for: small scale, infrequent access - Cost: varies with usage
Consequences
Benefits
1. Single actionable metric "Martinez family score: 28/100, at-risk tier, declining rapidly" - Sarah knows exactly who needs attention.
2. Objective prioritization Sort by score, work top-down. No guesswork, no favoritism.
3. Trend detection Score was 76 three months ago, now 28. Rapid decline triggers alert.
4. Tier-based automation - High tier (80-100): Minimal touch, celebrate success - Medium tier (60-79): Standard communications - Low tier (40-59): Increased engagement attempts - At-risk (<40): Urgent intervention, personal outreach
5. Predictive power Low scores predict churn. Intervene before withdrawal.
6. Dashboard simplicity Leadership sees one number, can drill down if needed.
7. Benchmark comparisons "Our average score is 68. Industry average is 72. We need improvement."
Costs
1. Oversimplification Complex relationships reduced to single number. Nuance lost.
2. Weight subjectivity Why 30% financial, not 35%? Weights are judgment calls.
3. Gaming potential If families learn the algorithm, they might game it (like credit scores).
4. Computation overhead Calculating scores for thousands of entities takes time and compute.
5. False precision Score of 73 vs 74 - is that real difference or noise?
6. Maintenance burden Weights may need tuning over time as business changes.
Sample Code
Dashboard query - who needs attention:
async function getAttentionQueue(limit = 10) {
return await db.query(`
SELECT
f.family_id,
f.family_name,
fem.engagement_score,
fem.engagement_tier,
fem.score_delta,
fem.score_velocity,
fem.calculation_date
FROM family_engagement_metrics fem
JOIN families f ON fem.family_id = f.family_id
WHERE f.enrolled_current_semester = 1
AND (
fem.engagement_tier = 'at_risk'
OR (fem.engagement_tier = 'low' AND fem.score_velocity = 'declining')
)
ORDER BY
CASE fem.engagement_tier
WHEN 'at_risk' THEN 1
WHEN 'low' THEN 2
ELSE 3
END,
fem.engagement_score ASC
LIMIT ?
`, [limit]);
}
Known Uses
Homeschool Co-op Intelligence Platform - 100 families scored nightly - Average score: 68/100 - Identified 12 at-risk families (scores <40) - Intervened with 9, prevented 6 withdrawals - ROI: 6 families × $450 = $2,700 saved
SaaS Companies (inspiration) - Gainsight: Customer health scores - ChurnZero: Engagement scoring - Totango: SuccessScore
CRM Platforms - Salesforce: Lead scoring - HubSpot: Contact scoring - Pipeline: Deal health scores
Related Patterns
Requires: - Pattern 1: Universal Event Log - raw data source - Pattern 3: Multi-Channel Tracking - complete interaction picture - Pattern 4: Interaction Outcome Classification - outcomes inform scoring
Enables: - Pattern 7: Multi-Dimensional Risk Assessment - health score is one dimension - Pattern 8: Tier-Based Segmentation - tiers determined by score - Pattern 9: Early Warning Signals - score changes trigger alerts - Pattern 15: Intervention Recommendation Engine - score determines intervention
Enhanced by: - Pattern 10: Engagement Velocity Tracking - rate of change matters - Pattern 12: Risk Stratification Models - predictive models use score as feature
References
Academic Foundations
- Mehta, Nick, Dan Steinman, and Lincoln Murphy (2016). Customer Success. Wiley. ISBN: 978-1119167969 - Health scoring for customer success
- Kaplan, Robert S., and David P. Norton (1996). The Balanced Scorecard. Harvard Business Press. ISBN: 978-0875846514 - Multi-dimensional measurement
- Niven, Paul R. (2014). Balanced Scorecard Evolution. Wiley. ISBN: 978-1118558713 - Advanced scorecard techniques
Health Scoring Methodologies
- Net Promoter Score (NPS): Reichheld, F.F. (2003). "The One Number You Need to Grow." Harvard Business Review. https://hbr.org/2003/12/the-one-number-you-need-to-grow
- FICO Credit Score: https://www.fico.com/en/products/fico-score - Inspiration for weighted composite scores
- MEWS (Modified Early Warning Score): https://www.ncbi.nlm.nih.gov/pmc/articles/PMC5369634/ - Healthcare composite scoring
Practical Implementation
- Customer Health Score Template: https://www.gainsight.com/guides/the-essential-guide-to-customer-health-scores/
- SaaS Metrics: https://www.forentrepreneurs.com/saas-metrics-2/ - Dave Skok's definitive guide
- Churn Prediction Models: Risselada, H., et al. (2010). "Staying Power of Churn Prediction Models." Journal of Interactive Marketing.
Machine Learning for Scoring
- Scikit-learn Ensemble Methods: https://scikit-learn.org/stable/modules/ensemble.html - Weighted combinations
- Feature Importance: Lundberg, S.M., & Lee, S.I. (2017). "A Unified Approach to Interpreting Model Predictions." https://arxiv.org/abs/1705.07874 - SHAP values
- Interpretable ML: Molnar, C. (2022). Interpretable Machine Learning. https://christophm.github.io/interpretable-ml-book/ - Free online book
Related Trilogy Patterns
- Pattern 7: Multi-Dimensional Risk - Risk scoring uses similar techniques
- Pattern 8: Tier-Based Segmentation - Health scores determine tiers
- Pattern 9: Early Warning Signals - Health score changes trigger alerts
- Pattern 12: Risk Stratification Models - Health as input feature
- Volume 3, Pattern 6: Domain-Aware Validation - Validate health components
Tools & Services
- Gainsight: https://www.gainsight.com/ - Customer success platform with health scoring
- ChurnZero: https://churnzero.net/ - Customer success automation
- Totango: https://www.totango.com/ - Customer success software
- Planhat: https://www.planhat.com/ - Customer platform with health metrics