Pattern 7: Multi-Dimensional Risk Assessment
Intent
Assess risk across multiple independent dimensions (payment, withdrawal, academic, etc.) rather than a single aggregate score, enabling targeted interventions for specific risk types and revealing patterns hidden by composite metrics.
Also Known As
- Disaggregated Risk Analysis
- Dimensional Risk Model
- Multi-Factor Risk Assessment
- Risk Profile Matrix
- Targeted Risk Scoring
Problem
A single health score hides which TYPES of problems exist.
Pattern 6 gives Sarah: "Martinez family score: 42/100, low tier."
But Sarah needs to know: - Are they at risk of leaving? (withdrawal risk) - Are they at risk of not paying? (payment risk) - Are students struggling academically? (academic risk) - Is engagement dropping dangerously? (disengagement risk)
A family can be: - High payment risk, low withdrawal risk (financially stressed but committed) - Low payment risk, high withdrawal risk (can afford it but not happy) - High academic risk, low other risks (student struggling, family engaged)
Different risks require different interventions: - Payment risk → Offer payment plan - Withdrawal risk → Personal meeting to address concerns - Academic risk → Connect with teacher, tutoring options - Disengagement risk → Re-engagement campaign
Single score masks this critical detail.
Context
When this pattern applies:
- Multiple distinct risk types exist in domain
- Different risks require different responses
- Risks can be independent (high in one, low in others)
- Want to prioritize intervention efforts by risk type
- Building targeted automated interventions
When this pattern may not be needed:
- Only one primary risk type matters
- All risks are highly correlated (single score sufficient)
- Interventions are same regardless of risk type
- Very simple domain with limited complexity
Forces
Competing concerns:
1. Granularity vs Complexity - More dimensions provide richer understanding - But too many dimensions overwhelm - Balance: 3-5 key risk dimensions
2. Independence vs Correlation - Want dimensions to be independent (measure different things) - But real-world risks correlate (payment issues → withdrawal) - Balance: Accept some correlation, focus on actionable distinctions
3. Prediction vs Explanation - Predictive models maximize accuracy - But may not align with intuitive risk categories - Balance: Start with business-logical dimensions, refine with data
4. Universal vs Domain-Specific - Want framework applicable across domains - But risk dimensions are domain-specific - Balance: Universal framework, domain-specific dimensions
5. Current vs Future Risk - Current risk: problems manifest now - Future risk: problems likely to develop - Balance: Assess both (immediate issues + predictive indicators)
Solution
Define 3-7 independent risk dimensions relevant to domain, score each 0-100, and create risk profile matrix.
Framework structure:
Risk Dimension = f(
Historical Behavior,
Current State,
Predictive Signals,
External Factors
)
For each dimension:
- Score: 0-100 (0 = no risk, 100 = imminent problem)
- Confidence: How certain is this assessment?
- Trend: Improving, stable, or worsening?
- Recommended Action: What to do about it?
Common dimensions by domain:
Homeschool Co-op: 1. Withdrawal Risk (will they leave?) 2. Payment Risk (will they pay late or not at all?) 3. Academic Risk (are students struggling?) 4. Disengagement Risk (are they disconnecting from community?)
Property Management: 1. Eviction Risk (will tenant need to be evicted?) 2. Lease Renewal Risk (will they renew or leave?) 3. Maintenance Cost Risk (will unit require expensive repairs?) 4. Payment Risk (late rent or non-payment?)
SaaS Product: 1. Churn Risk (will they cancel subscription?) 2. Expansion Risk (will they downgrade vs upgrade?) 3. Support Load Risk (will they require excessive support?) 4. Payment Risk (credit card failures, past-due invoices?)
Medical Practice: 1. No-Show Risk (will they miss appointments?) 2. Adherence Risk (will they follow treatment plan?) 3. Payment Risk (will they pay bills?) 4. Churn Risk (will they switch providers?)
Structure
Risk Assessment Tables
-- Store risk assessments
CREATE TABLE risk_assessments (
assessment_id INT PRIMARY KEY IDENTITY(1,1),
family_id INT NOT NULL,
-- Assessment metadata
assessment_date DATETIME2 DEFAULT GETDATE(),
assessment_period_days INT DEFAULT 90,
-- Risk dimensions (each 0-100)
withdrawal_risk DECIMAL(5,2),
payment_risk DECIMAL(5,2),
academic_risk DECIMAL(5,2),
disengagement_risk DECIMAL(5,2),
-- Confidence scores (0-1)
withdrawal_confidence DECIMAL(3,2),
payment_confidence DECIMAL(3,2),
academic_confidence DECIMAL(3,2),
disengagement_confidence DECIMAL(3,2),
-- Risk trends
withdrawal_trend VARCHAR(20), -- 'improving', 'stable', 'worsening'
payment_trend VARCHAR(20),
academic_trend VARCHAR(20),
disengagement_trend VARCHAR(20),
-- Overall risk profile
highest_risk_dimension VARCHAR(50),
requires_immediate_action BIT DEFAULT 0,
CONSTRAINT FK_risk_family FOREIGN KEY (family_id)
REFERENCES families(family_id),
CONSTRAINT UQ_family_risk UNIQUE (family_id)
);
-- Indexes
CREATE INDEX IX_withdrawal_risk ON risk_assessments(withdrawal_risk DESC);
CREATE INDEX IX_payment_risk ON risk_assessments(payment_risk DESC);
CREATE INDEX IX_immediate_action ON risk_assessments(requires_immediate_action)
WHERE requires_immediate_action = 1;
-- Store risk factors (what contributes to each dimension)
CREATE TABLE risk_factors (
factor_id INT PRIMARY KEY IDENTITY(1,1),
assessment_id INT NOT NULL,
risk_dimension VARCHAR(50),
factor_name VARCHAR(100),
factor_value DECIMAL(10,2),
contribution_to_risk DECIMAL(5,2), -- How much this factor adds to risk
CONSTRAINT FK_factor_assessment FOREIGN KEY (assessment_id)
REFERENCES risk_assessments(assessment_id)
);
Implementation
Multi-Dimensional Risk Calculator
class RiskAssessor {
constructor(db, config = {}) {
this.db = db;
this.lookbackDays = config.lookbackDays || 90;
}
async assessFamily(familyId) {
// Calculate each risk dimension
const withdrawalRisk = await this.assessWithdrawalRisk(familyId);
const paymentRisk = await this.assessPaymentRisk(familyId);
const academicRisk = await this.assessAcademicRisk(familyId);
const disengagementRisk = await this.assessDisengagementRisk(familyId);
// Determine highest risk
const risks = {
withdrawal: withdrawalRisk.score,
payment: paymentRisk.score,
academic: academicRisk.score,
disengagement: disengagementRisk.score
};
const highestRisk = Object.entries(risks)
.sort((a, b) => b[1] - a[1])[0];
// Determine if immediate action required (any dimension > 80)
const immediateAction = Object.values(risks).some(score => score > 80);
// Save to database
await this.saveAssessment(familyId, {
withdrawal: withdrawalRisk,
payment: paymentRisk,
academic: academicRisk,
disengagement: disengagementRisk,
highestRisk: highestRisk[0],
immediateAction
});
return {
familyId,
risks: {
withdrawal: withdrawalRisk,
payment: paymentRisk,
academic: academicRisk,
disengagement: disengagementRisk
},
highestRisk: highestRisk[0],
highestRiskScore: highestRisk[1],
immediateAction
};
}
async assessWithdrawalRisk(familyId) {
// Factors that predict withdrawal
const engagementMetrics = await this.db.query(`
SELECT engagement_score, score_velocity, score_delta
FROM family_engagement_metrics
WHERE family_id = ?
`, [familyId]);
const communicationHistory = await this.db.query(`
SELECT
COUNT(*) as total_communications,
SUM(CASE WHEN outcome IN ('opened', 'clicked', 'replied', 'engaged') THEN 1 ELSE 0 END) as positive_responses
FROM interaction_log
WHERE family_id = ?
AND interaction_timestamp >= DATE_SUB(NOW(), INTERVAL ? DAY)
AND interaction_type IN ('email_sent', 'sms_sent', 'phone_call_made')
`, [familyId, this.lookbackDays]);
const participationHistory = await this.db.query(`
SELECT
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
FROM interaction_log
WHERE family_id = ?
AND interaction_timestamp >= DATE_SUB(NOW(), INTERVAL ? DAY)
`, [familyId, this.lookbackDays]);
const factors = [];
let riskScore = 0;
// Factor 1: Low engagement score (30% weight)
const engScore = engagementMetrics[0]?.engagement_score || 50;
const engagementFactor = Math.max(0, 100 - engScore); // Invert: low score = high risk
riskScore += engagementFactor * 0.30;
factors.push({
name: 'low_engagement_score',
value: engScore,
contribution: engagementFactor * 0.30
});
// Factor 2: Declining engagement (25% weight)
const scoreVelocity = engagementMetrics[0]?.score_velocity;
const declineFactor = scoreVelocity === 'declining' ? 80 : (scoreVelocity === 'stable' ? 40 : 20);
riskScore += declineFactor * 0.25;
factors.push({
name: 'engagement_trend',
value: scoreVelocity,
contribution: declineFactor * 0.25
});
// Factor 3: Low communication responsiveness (20% weight)
const comm = communicationHistory[0];
const responseRate = comm.total_communications > 0
? (comm.positive_responses / comm.total_communications) * 100
: 50;
const commFactor = Math.max(0, 100 - responseRate);
riskScore += commFactor * 0.20;
factors.push({
name: 'communication_responsiveness',
value: responseRate,
contribution: commFactor * 0.20
});
// Factor 4: Low event participation (15% weight)
const part = participationHistory[0];
const attendanceRate = part.events_invited > 0
? (part.events_attended / part.events_invited) * 100
: 50;
const participationFactor = Math.max(0, 100 - attendanceRate);
riskScore += participationFactor * 0.15;
factors.push({
name: 'event_participation',
value: attendanceRate,
contribution: participationFactor * 0.15
});
// Factor 5: Tenure (newer families higher risk) (10% weight)
const tenureResult = await this.db.query(`
SELECT DATEDIFF(day, enrollment_date, NOW()) as days_enrolled
FROM families WHERE family_id = ?
`, [familyId]);
const daysEnrolled = tenureResult[0].days_enrolled;
const tenureFactor = daysEnrolled < 90 ? 80 : (daysEnrolled < 180 ? 50 : 20);
riskScore += tenureFactor * 0.10;
factors.push({
name: 'tenure_risk',
value: daysEnrolled,
contribution: tenureFactor * 0.10
});
// Determine confidence based on data availability
const confidence = this.calculateConfidence([
comm.total_communications,
part.events_invited,
engScore !== 50 ? 1 : 0
]);
// Determine trend
const trend = scoreVelocity === 'declining' ? 'worsening' :
scoreVelocity === 'improving' ? 'improving' : 'stable';
return {
score: Math.min(100, Math.max(0, riskScore)),
confidence: confidence,
trend: trend,
factors: factors,
recommendation: this.getWithdrawalRecommendation(riskScore)
};
}
async assessPaymentRisk(familyId) {
const paymentHistory = await this.db.query(`
SELECT
COUNT(*) as total_payments,
SUM(CASE WHEN outcome = 'paid_on_time' THEN 1 ELSE 0 END) as on_time_count,
SUM(CASE WHEN outcome = 'paid_late' THEN 1 ELSE 0 END) as late_count,
AVG(CASE WHEN outcome = 'paid_late'
THEN CAST(JSON_VALUE(metadata, '$.days_late') AS INT)
ELSE 0 END) as avg_days_late,
MAX(CASE WHEN outcome = 'paid_late'
THEN CAST(JSON_VALUE(metadata, '$.days_late') AS INT)
ELSE 0 END) as max_days_late
FROM interaction_log
WHERE family_id = ?
AND interaction_type = 'payment_received'
AND interaction_timestamp >= DATE_SUB(NOW(), INTERVAL 1 YEAR)
`, [familyId]);
const outstandingBalance = await this.db.query(`
SELECT
SUM(amount) as total_outstanding,
MAX(DATEDIFF(day, due_date, NOW())) as oldest_days_overdue
FROM invoices
WHERE family_id = ? AND status = 'unpaid'
`, [familyId]);
const factors = [];
let riskScore = 0;
const ph = paymentHistory[0];
if (ph.total_payments === 0) {
// New family, no history - moderate risk
return {
score: 50,
confidence: 0.3,
trend: 'stable',
factors: [{ name: 'no_payment_history', value: 0, contribution: 50 }],
recommendation: 'monitor_closely'
};
}
// Factor 1: On-time payment rate (40% weight)
const onTimeRate = (ph.on_time_count / ph.total_payments) * 100;
const onTimeFactor = Math.max(0, 100 - onTimeRate);
riskScore += onTimeFactor * 0.40;
factors.push({
name: 'on_time_rate',
value: onTimeRate,
contribution: onTimeFactor * 0.40
});
// Factor 2: Average lateness when late (30% weight)
const latenessFactor = Math.min(100, (ph.avg_days_late || 0) * 5); // 5 points per day
riskScore += latenessFactor * 0.30;
factors.push({
name: 'average_lateness',
value: ph.avg_days_late,
contribution: latenessFactor * 0.30
});
// Factor 3: Outstanding balance (20% weight)
const outstanding = outstandingBalance[0];
const hasOutstanding = outstanding.total_outstanding > 0;
const outstandingFactor = hasOutstanding ?
Math.min(100, (outstanding.oldest_days_overdue || 0) * 3) : 0;
riskScore += outstandingFactor * 0.20;
factors.push({
name: 'outstanding_balance',
value: outstanding.total_outstanding || 0,
contribution: outstandingFactor * 0.20
});
// Factor 4: Recent trend (10% weight)
const recentPayments = await this.db.query(`
SELECT outcome
FROM interaction_log
WHERE family_id = ?
AND interaction_type = 'payment_received'
AND interaction_timestamp >= DATE_SUB(NOW(), INTERVAL 3 MONTH)
ORDER BY interaction_timestamp DESC
LIMIT 3
`, [familyId]);
const recentLateCount = recentPayments.filter(p => p.outcome === 'paid_late').length;
const trendFactor = (recentLateCount / Math.max(1, recentPayments.length)) * 100;
riskScore += trendFactor * 0.10;
factors.push({
name: 'recent_trend',
value: recentLateCount,
contribution: trendFactor * 0.10
});
const confidence = this.calculateConfidence([ph.total_payments]);
const trend = recentLateCount > (ph.late_count / ph.total_payments * 3) ? 'worsening' :
recentLateCount < (ph.late_count / ph.total_payments * 3) ? 'improving' : 'stable';
return {
score: Math.min(100, Math.max(0, riskScore)),
confidence: confidence,
trend: trend,
factors: factors,
recommendation: this.getPaymentRecommendation(riskScore)
};
}
async assessAcademicRisk(familyId) {
// Student performance indicators
const attendanceData = await this.db.query(`
SELECT
s.student_id,
s.student_name,
COUNT(CASE WHEN a.status = 'present' THEN 1 END) as days_present,
COUNT(*) as total_days,
AVG(CASE WHEN a.status = 'absent' THEN 1 ELSE 0 END) as absence_rate
FROM students s
LEFT JOIN attendance a ON s.student_id = a.student_id
WHERE s.family_id = ?
AND a.attendance_date >= DATE_SUB(NOW(), INTERVAL ? DAY)
GROUP BY s.student_id, s.student_name
`, [familyId, this.lookbackDays]);
const gradeData = await this.db.query(`
SELECT
s.student_id,
AVG(g.grade_value) as avg_grade,
COUNT(CASE WHEN g.grade_value < 70 THEN 1 END) as failing_grades,
COUNT(*) as total_grades
FROM students s
LEFT JOIN grades g ON s.student_id = g.student_id
WHERE s.family_id = ?
AND g.grade_date >= DATE_SUB(NOW(), INTERVAL ? DAY)
GROUP BY s.student_id
`, [familyId, this.lookbackDays]);
if (attendanceData.length === 0) {
// No students or no data
return {
score: 0,
confidence: 0,
trend: 'stable',
factors: [],
recommendation: 'not_applicable'
};
}
const factors = [];
let riskScore = 0;
// Factor 1: Attendance rate (50% weight)
const avgAttendanceRate = attendanceData.reduce((sum, s) =>
sum + ((s.days_present / s.total_days) * 100), 0) / attendanceData.length;
const attendanceFactor = Math.max(0, 100 - avgAttendanceRate);
riskScore += attendanceFactor * 0.50;
factors.push({
name: 'attendance_rate',
value: avgAttendanceRate,
contribution: attendanceFactor * 0.50
});
// Factor 2: Grade performance (50% weight)
if (gradeData.length > 0) {
const avgGrade = gradeData.reduce((sum, s) => sum + s.avg_grade, 0) / gradeData.length;
const gradeFactor = Math.max(0, Math.min(100, (100 - avgGrade)));
riskScore += gradeFactor * 0.50;
factors.push({
name: 'grade_average',
value: avgGrade,
contribution: gradeFactor * 0.50
});
}
const confidence = this.calculateConfidence([
attendanceData.reduce((sum, s) => sum + s.total_days, 0),
gradeData.reduce((sum, s) => sum + s.total_grades, 0)
]);
return {
score: Math.min(100, Math.max(0, riskScore)),
confidence: confidence,
trend: 'stable', // Would need historical comparison
factors: factors,
recommendation: this.getAcademicRecommendation(riskScore)
};
}
async assessDisengagementRisk(familyId) {
// Similar to withdrawal but focused on engagement metrics
const engagementMetrics = await this.db.query(`
SELECT
communication_score,
platform_engagement_score,
participation_score,
score_velocity
FROM family_engagement_metrics
WHERE family_id = ?
`, [familyId]);
if (!engagementMetrics.length) {
return {
score: 50,
confidence: 0.2,
trend: 'stable',
factors: [],
recommendation: 'calculate_engagement_metrics'
};
}
const em = engagementMetrics[0];
const factors = [];
let riskScore = 0;
// Disengagement = low scores in engagement components
const commFactor = Math.max(0, 100 - em.communication_score);
riskScore += commFactor * 0.35;
factors.push({
name: 'communication_engagement',
value: em.communication_score,
contribution: commFactor * 0.35
});
const platformFactor = Math.max(0, 100 - em.platform_engagement_score);
riskScore += platformFactor * 0.35;
factors.push({
name: 'platform_usage',
value: em.platform_engagement_score,
contribution: platformFactor * 0.35
});
const participationFactor = Math.max(0, 100 - em.participation_score);
riskScore += participationFactor * 0.30;
factors.push({
name: 'participation',
value: em.participation_score,
contribution: participationFactor * 0.30
});
return {
score: Math.min(100, Math.max(0, riskScore)),
confidence: 0.8,
trend: em.score_velocity === 'declining' ? 'worsening' :
em.score_velocity === 'improving' ? 'improving' : 'stable',
factors: factors,
recommendation: this.getDisengagementRecommendation(riskScore)
};
}
calculateConfidence(dataPoints) {
// Confidence based on data availability
// More data points = higher confidence
const totalPoints = dataPoints.reduce((sum, val) => sum + (val > 0 ? 1 : 0), 0);
const maxPoints = dataPoints.length;
return Math.min(1.0, (totalPoints / maxPoints) * 0.8 + 0.2); // Min 0.2, max 1.0
}
getWithdrawalRecommendation(riskScore) {
if (riskScore >= 80) return 'urgent_personal_intervention';
if (riskScore >= 60) return 'schedule_check_in_call';
if (riskScore >= 40) return 'enhanced_engagement_campaign';
return 'monitor_normally';
}
getPaymentRecommendation(riskScore) {
if (riskScore >= 80) return 'immediate_payment_plan_offer';
if (riskScore >= 60) return 'early_reminder_with_payment_options';
if (riskScore >= 40) return 'standard_reminder_earlier_timing';
return 'standard_payment_process';
}
getAcademicRecommendation(riskScore) {
if (riskScore >= 70) return 'urgent_teacher_intervention';
if (riskScore >= 50) return 'parent_teacher_conference';
if (riskScore >= 30) return 'monitoring_and_support';
return 'normal_academic_track';
}
getDisengagementRecommendation(riskScore) {
if (riskScore >= 70) return 'personalized_re_engagement_campaign';
if (riskScore >= 50) return 'increased_communication_frequency';
if (riskScore >= 30) return 'engagement_incentives';
return 'maintain_standard_engagement';
}
async saveAssessment(familyId, assessment) {
const {withdrawal, payment, academic, disengagement, highestRisk, immediateAction} = assessment;
await this.db.query(`
INSERT INTO risk_assessments (
family_id,
withdrawal_risk, withdrawal_confidence, withdrawal_trend,
payment_risk, payment_confidence, payment_trend,
academic_risk, academic_confidence, academic_trend,
disengagement_risk, disengagement_confidence, disengagement_trend,
highest_risk_dimension, requires_immediate_action,
assessment_period_days
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT (family_id) DO UPDATE SET
withdrawal_risk = EXCLUDED.withdrawal_risk,
withdrawal_confidence = EXCLUDED.withdrawal_confidence,
withdrawal_trend = EXCLUDED.withdrawal_trend,
payment_risk = EXCLUDED.payment_risk,
payment_confidence = EXCLUDED.payment_confidence,
payment_trend = EXCLUDED.payment_trend,
academic_risk = EXCLUDED.academic_risk,
academic_confidence = EXCLUDED.academic_confidence,
academic_trend = EXCLUDED.academic_trend,
disengagement_risk = EXCLUDED.disengagement_risk,
disengagement_confidence = EXCLUDED.disengagement_confidence,
disengagement_trend = EXCLUDED.disengagement_trend,
highest_risk_dimension = EXCLUDED.highest_risk_dimension,
requires_immediate_action = EXCLUDED.requires_immediate_action,
assessment_date = GETDATE()
`, [
familyId,
withdrawal.score, withdrawal.confidence, withdrawal.trend,
payment.score, payment.confidence, payment.trend,
academic.score, academic.confidence, academic.trend,
disengagement.score, disengagement.confidence, disengagement.trend,
highestRisk, immediateAction ? 1 : 0,
this.lookbackDays
]);
}
}
module.exports = RiskAssessor;
Usage Example
const assessor = new RiskAssessor(db);
const assessment = await assessor.assessFamily(187);
console.log(`
Risk Assessment for Family ${assessment.familyId}:
WITHDRAWAL RISK: ${assessment.risks.withdrawal.score.toFixed(1)}/100
Confidence: ${(assessment.risks.withdrawal.confidence * 100).toFixed(0)}%
Trend: ${assessment.risks.withdrawal.trend}
Action: ${assessment.risks.withdrawal.recommendation}
PAYMENT RISK: ${assessment.risks.payment.score.toFixed(1)}/100
Confidence: ${(assessment.risks.payment.confidence * 100).toFixed(0)}%
Trend: ${assessment.risks.payment.trend}
Action: ${assessment.risks.payment.recommendation}
ACADEMIC RISK: ${assessment.risks.academic.score.toFixed(1)}/100
Confidence: ${(assessment.risks.academic.confidence * 100).toFixed(0)}%
Trend: ${assessment.risks.academic.trend}
Action: ${assessment.risks.academic.recommendation}
DISENGAGEMENT RISK: ${assessment.risks.disengagement.score.toFixed(1)}/100
Confidence: ${(assessment.risks.disengagement.confidence * 100).toFixed(0)}%
Trend: ${assessment.risks.disengagement.trend}
Action: ${assessment.risks.disengagement.recommendation}
HIGHEST RISK: ${assessment.highestRisk} (${assessment.highestRiskScore.toFixed(1)})
IMMEDIATE ACTION REQUIRED: ${assessment.immediateAction ? 'YES ⚠️' : 'No'}
`);
// Example output:
// Risk Assessment for Family 187:
// WITHDRAWAL RISK: 73.2/100
// Confidence: 85%
// Trend: worsening
// Action: schedule_check_in_call
// PAYMENT RISK: 42.1/100
// Confidence: 90%
// Trend: stable
// Action: standard_reminder_earlier_timing
// ACADEMIC RISK: 18.5/100
// Confidence: 75%
// Trend: stable
// Action: normal_academic_track
// DISENGAGEMENT RISK: 67.4/100
// Confidence: 80%
// Trend: worsening
// Action: increased_communication_frequency
// HIGHEST RISK: withdrawal (73.2)
// IMMEDIATE ACTION REQUIRED: No
Variations
By Domain
Property Management: - Eviction Risk - Lease Renewal Risk - Maintenance Cost Risk - Payment Risk - Neighbor Complaint Risk
SaaS: - Churn Risk - Expansion Risk (negative = downgrade, positive = upgrade) - Support Load Risk - Payment Risk - Usage Adoption Risk
Healthcare: - No-Show Risk - Adherence Risk - Readmission Risk - Payment Risk - Provider Switching Risk
By Industry Maturity
Simple (2-3 dimensions): Start with most critical risks: - Payment Risk - Churn Risk
Standard (4-5 dimensions): As shown in main implementation
Advanced (6+ dimensions): Add: - Referral Likelihood (positive indicator) - Expansion Potential (positive indicator) - Advocacy Risk (will they leave negative reviews?)
Consequences
Benefits
1. Targeted interventions "High payment risk, low withdrawal risk → Offer payment plan, don't worry about losing them"
2. Resource optimization Focus on highest-priority risk for each family, not generic interventions.
3. Reveals hidden patterns Family might score 70/100 overall but have 95/100 payment risk - single score hides this.
4. Better predictions Independent dimensions provide richer features for ML models.
5. Clearer communication "Your payment risk is high" is more actionable than "your score is low."
6. Confidence tracking Know which assessments are reliable vs uncertain.
Costs
1. Increased complexity 5 scores instead of 1. Harder to dashboard, harder to explain.
2. Potential confusion "Which risk should I prioritize?" Not always obvious.
3. Correlation issues Risks may be correlated (payment issues → withdrawal), reducing independence.
4. Computation overhead 5x more calculations than single score.
5. Configuration burden Each dimension needs weights, thresholds, recommendations.
Sample Code
Dashboard query - targeted risk queue:
async function getTargetedRiskQueue() {
return await db.query(`
SELECT
f.family_id,
f.family_name,
ra.withdrawal_risk,
ra.payment_risk,
ra.academic_risk,
ra.highest_risk_dimension,
ra.requires_immediate_action,
CASE ra.highest_risk_dimension
WHEN 'withdrawal' THEN 'Personal meeting'
WHEN 'payment' THEN 'Payment plan offer'
WHEN 'academic' THEN 'Teacher intervention'
WHEN 'disengagement' THEN 'Re-engagement campaign'
END as recommended_action
FROM risk_assessments ra
JOIN families f ON ra.family_id = f.family_id
WHERE f.enrolled_current_semester = 1
AND (
ra.requires_immediate_action = 1
OR ra.withdrawal_risk > 60
OR ra.payment_risk > 70
)
ORDER BY
ra.requires_immediate_action DESC,
GREATEST(ra.withdrawal_risk, ra.payment_risk, ra.academic_risk, ra.disengagement_risk) DESC
LIMIT 20
`);
}
Known Uses
Homeschool Co-op Intelligence Platform - 4 risk dimensions tracked - Prevented 6 withdrawals by identifying high withdrawal risk early - Prevented $1,800 in late payments through early payment risk intervention - Academic interventions helped 4 struggling students
Credit Scoring (inspiration) - FICO considers: payment history, utilization, credit age, new credit, mix - Multiple factors, weighted composite
Healthcare Risk Stratification - CMS uses multiple risk dimensions for Medicare patients - Readmission risk, adherence risk, cost risk assessed independently
Related Patterns
Requires: - Pattern 1: Universal Event Log - data source - Pattern 4: Interaction Outcome Classification - outcomes inform risk - Pattern 6: Composite Health Scoring - components feed into risk dimensions
Enables: - Pattern 15: Intervention Recommendation Engine - risk dimensions determine intervention - Pattern 22: Progressive Escalation Sequences - escalate based on specific risk type - Pattern 23: Triggered Interventions - trigger based on risk threshold
Enhanced by: - Pattern 11: Historical Pattern Matching - improve risk prediction - Pattern 12: Risk Stratification Models - ML models per dimension
References
Academic Foundations
- Provost, Foster, and Tom Fawcett (2013). Data Science for Business. O'Reilly. ISBN: 978-1449361327 - Chapter on risk assessment
- Kuhn, Max, and Kjell Johnson (2013). Applied Predictive Modeling. Springer. ISBN: 978-1461468486
- James, G., et al. (2013). An Introduction to Statistical Learning. Springer. http://faculty.marshall.usc.edu/gareth-james/ISL/ - Free PDF available
- Risk Analysis: Aven, T. (2015). Risk Analysis (2nd ed.). Wiley. ISBN: 978-1119057819
Healthcare Risk Stratification
- CMS Hierarchical Condition Categories (HCC): https://www.cms.gov/medicare/health-plans/medicareadvtgspecratestats/risk-adjustors - Medicare risk adjustment
- LACE Index: van Walraven, C., et al. (2010). "Derivation and validation of an index to predict early death or unplanned readmission after discharge from hospital." CMAJ. https://www.ncbi.nlm.nih.gov/pmc/articles/PMC2845681/
- Framingham Risk Score: https://framinghamheartstudy.org/fhs-risk-functions/cardiovascular-disease-10-year-risk/ - Cardiovascular risk
Credit Scoring
- FICO Score: https://www.fico.com/en/products/fico-score - Credit risk methodology
- VantageScore: https://vantagescore.com/ - Alternative credit scoring model
- Siddiqi, Naeem (2017). Intelligent Credit Scoring (2nd ed.). Wiley. ISBN: 978-1119282396
Practical Implementation
- Scikit-learn Multi-Output: https://scikit-learn.org/stable/modules/multioutput.html - Multi-dimensional predictions
- PyCaret: https://pycaret.org/ - Low-code ML library with risk modeling
- SHAP for Risk Models: https://github.com/slundberg/shap - Explainable AI for risk factors
Related Trilogy Patterns
- Pattern 6: Composite Health Scoring - Health and risk are complementary
- Pattern 8: Tier-Based Segmentation - Risk determines intervention intensity
- Pattern 9: Early Warning Signals - High risk triggers warnings
- Pattern 12: Risk Stratification Models - Formal ML risk models
- Volume 3, Pattern 6: Domain-Aware Validation - Domain-specific risk factors
Tools & Services
- SAS Risk Management: https://www.sas.com/en_us/software/risk-management.html - Enterprise risk platform
- Moody's Analytics: https://www.moodysanalytics.com/ - Financial risk modeling
- Quantexa: https://www.quantexa.com/ - Context intelligence for risk