1313import numpy as np
1414import pandas as pd
1515
16+ from stats_utils import calculate_mean_confidence_interval , get_boxplot_legend_text
17+
1618
1719class SUSCalculator :
20+ # Map normalized user types to directory names
21+ USER_TYPE_TO_DIR = {
22+ 'end_user' : 'endusers' ,
23+ 'technician' : 'technicians'
24+ }
25+
1826 def __init__ (self , results_dir : str = "../results" ):
1927 self .results_dir = Path (results_dir )
2028 self .output_dir = self .results_dir / "aggregated_analysis" / "sus"
@@ -70,26 +78,32 @@ def calculate_sus_score(self, ratings: List[int]) -> float:
7078
7179
7280 def process_user_type (self , user_type : str ) -> List [Dict ]:
73- """Process all SUS files for a specific user type."""
74- sus_dir = self .results_dir / user_type / "sus"
81+ """Process all SUS files for a specific user type.
82+
83+ Args:
84+ user_type: Normalized user type ('end_user' or 'technician')
85+ """
86+ # Map normalized user type to directory name
87+ dir_name = self .USER_TYPE_TO_DIR .get (user_type , user_type )
88+ sus_dir = self .results_dir / dir_name / "sus"
7589 results = []
76-
90+
7791 if not sus_dir .exists ():
7892 print (f"Warning: SUS directory not found: { sus_dir } " )
7993 return results
80-
94+
8195 for sus_file in sus_dir .glob ("*_sus.md" ):
8296 parsed = self .parse_sus_file (sus_file )
8397 if parsed :
8498 score = self .calculate_sus_score (parsed ['ratings' ])
8599 results .append ({
86100 'participant_id' : parsed ['participant_id' ],
87- 'user_type' : user_type ,
101+ 'user_type' : user_type , # Use normalized name in output
88102 'ratings' : parsed ['ratings' ],
89103 'sus_score' : score ,
90104 'file_path' : parsed ['file_path' ]
91105 })
92-
106+
93107 return results
94108
95109 def generate_aggregated_stats (self , scores_by_type : Dict [str , List [float ]]) -> Dict :
@@ -122,33 +136,76 @@ def create_visualizations(self, all_results: List[Dict]):
122136 df = pd .DataFrame (all_results )
123137
124138 # Create figure with subplots (1x2): SUS boxplot + Summary
125- # Use 16:9 aspect ratio to better utilize horizontal space
126- fig = plt .figure (figsize = (12 , 6.75 ))
127- gs = fig .add_gridspec (1 , 2 , width_ratios = [1.5 , 1 ], wspace = 0.3 , top = 0.95 , bottom = 0.22 )
139+ # Compact layout with reduced dimensions
140+ fig = plt .figure (figsize = (10 , 5.5 ))
141+ gs = fig .add_gridspec (1 , 2 , width_ratios = [1.5 , 1 ], wspace = 0.3 , top = 0.95 , bottom = 0.10 )
128142 ax1 = fig .add_subplot (gs [0 , 0 ])
129143 ax2 = fig .add_subplot (gs [0 , 1 ])
130144
131145 # 1. Box plot by user type
132146 user_types = list (df ['user_type' ].unique ())
133147 box_data = [df [df ['user_type' ] == ut ]['sus_score' ].values for ut in user_types ]
134- box_plot = ax1 .boxplot (box_data , tick_labels = user_types , patch_artist = True ,
135- medianprops = dict (color = 'darkblue' , linewidth = 2.5 ))
148+
149+ # Map user types to proper labels
150+ user_type_labels = []
151+ for ut in user_types :
152+ if 'enduser' in ut .lower ():
153+ user_type_labels .append ('End user' )
154+ elif 'technician' in ut .lower ():
155+ user_type_labels .append ('Technician' )
156+ else :
157+ user_type_labels .append (ut .replace ('_' , ' ' ).capitalize ())
158+
159+ box_plot = ax1 .boxplot (box_data , tick_labels = user_type_labels , patch_artist = True ,
160+ showmeans = True , meanline = False ,
161+ medianprops = dict (color = 'darkblue' , linewidth = 2.5 ),
162+ meanprops = dict (marker = 'D' , markerfacecolor = 'red' ,
163+ markeredgecolor = 'black' , markersize = 6 ))
136164 ax1 .set_title ('SUS score distribution by user type' )
137165 ax1 .set_ylabel ('SUS score' )
138166 ax1 .axhline (y = 68 , color = 'orange' , linestyle = '--' , alpha = 0.7 , label = 'Average threshold' )
139167 ax1 .axhline (y = 80.3 , color = 'green' , linestyle = '--' , alpha = 0.7 , label = 'Excellent threshold' )
140- # Add median to legend
168+
169+ # Calculate 95% CI for mean using t-Student distribution
170+ means = []
171+ ci_lowers = []
172+ ci_uppers = []
173+ for data in box_data :
174+ mean_val , ci_lower , ci_upper = calculate_mean_confidence_interval (data )
175+ means .append (mean_val )
176+ ci_lowers .append (ci_lower )
177+ ci_uppers .append (ci_upper )
178+
179+ # Add confidence interval error bars
180+ x_positions = np .arange (1 , len (means ) + 1 )
181+ ci_errors = [np .array (means ) - np .array (ci_lowers ),
182+ np .array (ci_uppers ) - np .array (means )]
183+ ax1 .errorbar (x_positions , means , yerr = ci_errors ,
184+ fmt = 'none' , ecolor = 'darkgreen' , capsize = 5 , capthick = 2 ,
185+ linewidth = 2 , alpha = 0.8 , label = '95% CI' )
186+
187+ # Updated legend with all elements
141188 ax1 .plot ([], [], color = 'darkblue' , linewidth = 2.5 , label = 'Median' )
142- ax1 .legend ()
189+ ax1 .plot ([], [], marker = 'D' , color = 'red' , linestyle = 'None' ,
190+ markeredgecolor = 'black' , markersize = 6 , label = 'Mean' )
191+ ax1 .plot ([], [], marker = 'o' , markerfacecolor = 'none' , markeredgecolor = 'black' ,
192+ linestyle = 'None' , markersize = 5 , label = 'Outliers' )
193+ # Extend Y-axis to create space for legend at bottom-right
194+ ax1 .set_ylim (bottom = 50 )
195+ # Position legend in lower right with padding
196+ ax1 .legend (loc = 'lower right' , frameon = True , fontsize = 9 )
143197 ax1 .grid (True , alpha = 0.3 )
144-
145- # Color boxes
146- base_colors = ['lightblue' , 'lightcoral' , 'lightgreen' , 'wheat' , 'plum' , 'khaki' ]
147- colors = (base_colors * ((len (user_types ) // len (base_colors )) + 1 ))[: len (user_types )]
148- for patch , color in zip (box_plot ['boxes' ], colors ):
198+
199+ # Color boxes (consistent with task duration plots)
200+ user_type_colors = {
201+ 'end_user' : 'lightblue' ,
202+ 'technician' : 'lightcoral'
203+ }
204+ for patch , user_type in zip (box_plot ['boxes' ], user_types ):
205+ color = user_type_colors .get (user_type , 'lightgray' )
149206 patch .set_facecolor (color )
150207
151- # 2. Summary statistics table (text-based)
208+ # 2. Summary statistics table (text-based) with box plot elements below
152209 ax2 .axis ('off' )
153210 stats_lines = ["SUS statistics summary" , "" ]
154211 for user_type in user_types :
@@ -159,29 +216,31 @@ def create_visualizations(self, all_results: List[Dict]):
159216 min_val = user_data .min ()
160217 max_val = user_data .max ()
161218
162- stats_lines .append (f"{ user_type .replace ('_' , ' ' ).title ()} :" )
219+ # Map user types to proper labels
220+ if 'enduser' in user_type .lower ():
221+ user_type_label = 'End user'
222+ elif 'technician' in user_type .lower ():
223+ user_type_label = 'Technician'
224+ else :
225+ user_type_label = user_type .replace ('_' , ' ' ).capitalize ()
226+ stats_lines .append (f"{ user_type_label } :" )
163227 stats_lines .append (f" Count: { len (user_data )} " )
164228 stats_lines .append (f" Mean SUS: { user_data .mean ():.1f} " )
165229 stats_lines .append (f" Median: { median :.1f} " )
166230 stats_lines .append (f" Q1 (25th percentile): { q1 :.1f} " )
167231 stats_lines .append (f" Q3 (75th percentile): { q3 :.1f} " )
168232 stats_lines .append (f" Range: { min_val :.1f} -{ max_val :.1f} " )
169233 stats_lines .append ("" )
234+
235+ # Add box plot legend directly after statistics
236+ stats_lines .append ("" )
237+ legend_text = get_boxplot_legend_text ()
238+ stats_lines .extend (legend_text .split ("\n " ))
239+
170240 stats_text = "\n " .join (stats_lines )
171241 ax2 .text (0.05 , 0.95 , stats_text , transform = ax2 .transAxes , fontsize = 10 ,
172242 verticalalignment = 'top' , fontfamily = 'monospace' )
173243
174- # Add box plot legend explaining elements (positioned below the plot)
175- legend_text = (
176- "Box plot elements:\n "
177- "• Box edges: 25th (Q1) and 75th (Q3) percentiles (interquartile range, IQR)\n "
178- "• Blue line: Median (50th percentile)\n "
179- "• Whiskers: Extend to the most extreme data point within 1.5×IQR from box edges\n "
180- "• Circles: Outliers (values beyond 1.5×IQR from box edges)"
181- )
182- fig .text (0.5 , 0.04 , legend_text , ha = 'center' , fontsize = 10 ,
183- verticalalignment = 'bottom' , multialignment = 'left' )
184-
185244 # Save visualization
186245 output_file = self .output_dir / "sus_visualizations.png"
187246 plt .savefig (output_file , dpi = 300 , bbox_inches = 'tight' )
@@ -191,8 +250,9 @@ def create_visualizations(self, all_results: List[Dict]):
191250 def generate_reports (self ) -> Dict :
192251 """Generate comprehensive SUS analysis reports."""
193252 all_results = []
194-
195- user_types = ['endusers' , 'technicians' ]
253+
254+ # Use normalized user type names
255+ user_types = ['end_user' , 'technician' ]
196256
197257 for user_type in user_types :
198258 results = self .process_user_type (user_type )
0 commit comments