File size: 7,460 Bytes
df31aa1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
"""
Unit tests for ML explainability module
"""

import pytest
import sys
import os

sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

from explainability import (
    SHAPExplainer,
    CounterfactualExplainer,
    RecommendationGenerator,
    ExplanationAggregator,
)


class TestSHAPExplainer:
    """Tests for SHAPExplainer class"""

    @pytest.fixture
    def explainer(self):
        return SHAPExplainer()

    def test_explain_returns_required_fields(self, explainer):
        """Test that explanation contains required fields"""
        features = {
            "complexity_normalized": 0.6,
            "pri_attention_demand": 0.5,
            "trait_conscientiousness": 0.7,
        }
        prediction = 0.75
        
        explanation = explainer.explain(features, prediction)
        
        assert "shap_values" in explanation
        assert "base_value" in explanation
        assert "feature_ranking" in explanation
        assert "prediction" in explanation

    def test_shap_values_exist_for_features(self, explainer):
        """Test that SHAP values are computed for input features"""
        features = {
            "complexity_normalized": 0.6,
            "pri_attention_demand": 0.5,
            "trait_conscientiousness": 0.7,
        }
        prediction = 0.75
        
        explanation = explainer.explain(features, prediction)
        
        # Should have SHAP values for the features we provided
        assert len(explanation["shap_values"]) > 0

    def test_feature_ranking_ordered_by_importance(self, explainer):
        """Test that features are ranked by importance"""
        features = {
            "complexity_normalized": 1.0,  # High complexity
            "pri_attention_demand": 0.1,
            "trait_conscientiousness": 0.3,
        }
        prediction = 0.4
        
        explanation = explainer.explain(features, prediction)
        ranking = explanation["feature_ranking"]
        
        # Check that ranking is sorted by absolute impact
        impacts = [abs(r["impact"]) for r in ranking]
        assert impacts == sorted(impacts, reverse=True)


class TestCounterfactualExplainer:
    """Tests for CounterfactualExplainer class"""

    @pytest.fixture
    def explainer(self):
        return CounterfactualExplainer()

    def test_generate_counterfactuals(self, explainer):
        """Test counterfactual generation"""
        features = {"complexity_normalized": 1.0, "pri_attention_demand": 0.8}
        prediction = 0.4
        target = 0.7
        
        counterfactuals = explainer.generate_counterfactuals(features, prediction, target)
        
        assert isinstance(counterfactuals, list)
        # Should generate some counterfactuals
        assert len(counterfactuals) >= 0  # May be empty if no improvements possible

    def test_counterfactuals_when_already_meeting_target(self, explainer):
        """Test counterfactuals when prediction already meets target"""
        features = {"complexity_normalized": 0.3, "pri_attention_demand": 0.6}
        prediction = 0.8  # Already above target
        target = 0.7
        
        counterfactuals = explainer.generate_counterfactuals(features, prediction, target)
        
        # Should return message that target is already met
        assert len(counterfactuals) > 0


class TestRecommendationGenerator:
    """Tests for RecommendationGenerator class"""

    @pytest.fixture
    def generator(self):
        return RecommendationGenerator()

    def test_generate_recommendations(self, generator):
        """Test recommendation generation"""
        features = {
            "complexity_normalized": 0.8,
            "time_pressure": 0.5,
            "trait_conscientiousness": 0.6,
        }
        prediction = 0.4
        stress_level = 8
        difficulty = "HARD"
        
        recommendations = generator.generate_recommendations(features, prediction, stress_level, difficulty)
        
        assert isinstance(recommendations, list)
        assert len(recommendations) > 0
        for rec in recommendations:
            assert "title" in rec
            assert "description" in rec
            assert "priority" in rec

    def test_high_stress_triggers_recommendations(self, generator):
        """Test that high stress triggers appropriate recommendations"""
        features = {
            "complexity_normalized": 0.5,
            "time_pressure": 0.2,
        }
        prediction = 0.7
        stress_level = 9  # High stress
        difficulty = "MODERATE"
        
        recommendations = generator.generate_recommendations(features, prediction, stress_level, difficulty)
        
        # Recommendations should exist
        assert len(recommendations) > 0

    def test_low_probability_triggers_recommendations(self, generator):
        """Test that low probability triggers task-related recommendations"""
        features = {
            "complexity_normalized": 0.9,  # High complexity
            "time_pressure": 0.6,
        }
        prediction = 0.3
        stress_level = 4
        difficulty = "HARD"
        
        recommendations = generator.generate_recommendations(features, prediction, stress_level, difficulty)
        
        # Should have recommendations
        assert len(recommendations) > 0
        
        # At least one should be high priority
        priorities = [r["priority"] for r in recommendations]
        assert "high" in priorities or "medium" in priorities


class TestExplanationAggregator:
    """Tests for ExplanationAggregator class"""

    @pytest.fixture
    def aggregator(self):
        return ExplanationAggregator()

    def test_generate_full_explanation(self, aggregator):
        """Test that aggregator creates comprehensive explanation"""
        features = {
            "complexity_normalized": 0.6,
            "trait_conscientiousness": 0.7,
            "time_pressure": 0.4,
        }
        prediction = {
            "completion_probability": 0.65,
            "stress_level": 6,
            "difficulty_level": "MODERATE",
        }
        task_data = {
            "title": "Test Task",
            "category": "WORK",
        }
        
        explanation = aggregator.generate_full_explanation(features, prediction, task_data)
        
        assert isinstance(explanation, dict)
        assert "prediction_summary" in explanation
        assert "feature_attribution" in explanation
        assert "recommendations" in explanation

    def test_generate_full_explanation_with_low_probability(self, aggregator):
        """Test explanation generation for low probability task"""
        features = {
            "complexity_normalized": 0.9,
            "trait_conscientiousness": 0.4,
            "time_pressure": 0.7,
        }
        prediction = {
            "completion_probability": 0.35,
            "stress_level": 8,
            "difficulty_level": "HARD",
        }
        task_data = {
            "title": "Difficult Task",
            "category": "ACADEMIC",
        }
        
        explanation = aggregator.generate_full_explanation(features, prediction, task_data)
        
        # Should have counterfactual scenarios for low probability
        assert "counterfactual_scenarios" in explanation
        
        # Should have recommendations
        assert len(explanation["recommendations"]) > 0


if __name__ == "__main__":
    pytest.main([__file__, "-v"])