philjosephcohen commited on
Commit
0ec53bf
·
1 Parent(s): 604e9c0

fix HR regression

Browse files
multi_agent_demo/deviations/bias_detector.py CHANGED
@@ -37,21 +37,8 @@ def detect_bias(
37
  # Identify protected/sensitive attributes (including numeric ones like age)
38
  protected_attributes = _identify_protected_attributes(parameter_groups)
39
 
40
- # Also check numeric attributes that might be protected (like age)
41
- numeric_protected = []
42
- for attr_name in parsed_data["attributes"].keys():
43
- attr_lower = attr_name.lower()
44
- if "age" in attr_lower and attr_name in metrics:
45
- numeric_protected.append(attr_name)
46
- protected_attributes.append(attr_name)
47
-
48
- # Create age groups for numeric age attributes
49
- for age_attr in numeric_protected:
50
- if age_attr in metrics:
51
- age_groups = _create_age_groups(traces, age_attr)
52
- if len(age_groups) >= 2:
53
- parameter_groups[f"{age_attr}_group"] = age_groups
54
- protected_attributes.append(f"{age_attr}_group")
55
 
56
  # For each metric, check if it varies significantly across parameter groups
57
  for metric_name, metric_values in metrics.items():
 
37
  # Identify protected/sensitive attributes (including numeric ones like age)
38
  protected_attributes = _identify_protected_attributes(parameter_groups)
39
 
40
+ # Note: Age grouping is now handled automatically in _group_by_parameters() in otel_parser.py
41
+ # No need for duplicate age binning logic here
 
 
 
 
 
 
 
 
 
 
 
 
 
42
 
43
  # For each metric, check if it varies significantly across parameter groups
44
  for metric_name, metric_values in metrics.items():
multi_agent_demo/deviations/otel_parser.py CHANGED
@@ -216,8 +216,20 @@ def _group_by_parameters(traces: List[Dict[str, Any]], attributes: Dict[str, Set
216
  parameter_groups = {}
217
 
218
  for attr_name, unique_values in attributes.items():
219
- # Skip numeric attributes (handled as metrics)
220
- if all(isinstance(v, (int, float)) for v in unique_values):
 
 
 
 
 
 
 
 
 
 
 
 
221
  continue
222
 
223
  # Only group by categorical attributes with reasonable cardinality
@@ -237,6 +249,88 @@ def _group_by_parameters(traces: List[Dict[str, Any]], attributes: Dict[str, Set
237
  return parameter_groups
238
 
239
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
240
  def _parse_timestamp(timestamp: Any) -> datetime:
241
  """
242
  Parse timestamp from various formats
 
216
  parameter_groups = {}
217
 
218
  for attr_name, unique_values in attributes.items():
219
+ # Check if this is a numeric attribute that should be binned
220
+ is_numeric = all(isinstance(v, (int, float)) for v in unique_values)
221
+
222
+ if is_numeric:
223
+ # Check if this is a protected numeric attribute (age, income, etc.)
224
+ attr_lower = attr_name.lower()
225
+ needs_binning = any(keyword in attr_lower for keyword in ['age', 'income', 'salary', 'tenure', 'experience', 'years'])
226
+
227
+ if needs_binning:
228
+ # Bin numeric values into categorical groups
229
+ groups = _bin_numeric_attribute(traces, attr_name, unique_values)
230
+ if len(groups) > 1:
231
+ parameter_groups[f"{attr_name}_group"] = groups
232
+ # Skip other numeric attributes (handled as metrics)
233
  continue
234
 
235
  # Only group by categorical attributes with reasonable cardinality
 
249
  return parameter_groups
250
 
251
 
252
+ def _bin_numeric_attribute(traces: List[Dict[str, Any]], attr_name: str, unique_values: Set) -> Dict[str, List[Dict[str, Any]]]:
253
+ """
254
+ Bin numeric attribute values into categorical groups for bias detection
255
+
256
+ For age: uses common age brackets (under_40, 40_and_over)
257
+ For other numeric attributes: uses quartiles or median split
258
+ """
259
+ attr_lower = attr_name.lower()
260
+ groups = defaultdict(list)
261
+
262
+ # Special handling for age
263
+ if 'age' in attr_lower:
264
+ for trace in traces:
265
+ attrs = trace.get("attributes", {})
266
+ if attr_name in attrs:
267
+ age = attrs[attr_name]
268
+ # Common age discrimination threshold
269
+ if age < 40:
270
+ groups["under_40"].append(trace)
271
+ else:
272
+ groups["40_and_over"].append(trace)
273
+
274
+ # Special handling for income/salary
275
+ elif 'income' in attr_lower or 'salary' in attr_lower:
276
+ # Use median split for income
277
+ values = sorted(unique_values)
278
+ median = values[len(values) // 2] if values else 0
279
+
280
+ for trace in traces:
281
+ attrs = trace.get("attributes", {})
282
+ if attr_name in attrs:
283
+ value = attrs[attr_name]
284
+ if value < median:
285
+ groups["below_median"].append(trace)
286
+ else:
287
+ groups["above_median"].append(trace)
288
+
289
+ # Special handling for experience/tenure (years)
290
+ elif 'years' in attr_lower or 'tenure' in attr_lower or 'experience' in attr_lower:
291
+ for trace in traces:
292
+ attrs = trace.get("attributes", {})
293
+ if attr_name in attrs:
294
+ years = attrs[attr_name]
295
+ if years < 5:
296
+ groups["0-5_years"].append(trace)
297
+ elif years < 10:
298
+ groups["5-10_years"].append(trace)
299
+ else:
300
+ groups["10+_years"].append(trace)
301
+
302
+ # Default: use quartile split
303
+ else:
304
+ values = sorted(unique_values)
305
+ if len(values) >= 4:
306
+ q1 = values[len(values) // 4]
307
+ q3 = values[3 * len(values) // 4]
308
+
309
+ for trace in traces:
310
+ attrs = trace.get("attributes", {})
311
+ if attr_name in attrs:
312
+ value = attrs[attr_name]
313
+ if value <= q1:
314
+ groups["low"].append(trace)
315
+ elif value >= q3:
316
+ groups["high"].append(trace)
317
+ else:
318
+ groups["medium"].append(trace)
319
+ else:
320
+ # Too few values, use median split
321
+ median = values[len(values) // 2] if values else 0
322
+ for trace in traces:
323
+ attrs = trace.get("attributes", {})
324
+ if attr_name in attrs:
325
+ value = attrs[attr_name]
326
+ if value < median:
327
+ groups["below_median"].append(trace)
328
+ else:
329
+ groups["above_median"].append(trace)
330
+
331
+ return dict(groups)
332
+
333
+
334
  def _parse_timestamp(timestamp: Any) -> datetime:
335
  """
336
  Parse timestamp from various formats