Volume 2: Organizational Intelligence Platforms

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

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

Credit Scoring

Practical Implementation

  • 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