haileyhalimj@gmail.com commited on
Commit
5da89c6
·
1 Parent(s): 272b077

cleaned demand filtering

Browse files
src/demand_filtering.py CHANGED
@@ -89,13 +89,6 @@ class DemandFilter:
89
  except Exception as e:
90
  print(f"Error loading data for filtering: {str(e)}")
91
  return False
92
-
93
- def too_high_demand_filter(self, product_id: str) -> bool:
94
- """
95
- Check if the demand for a product is too high.
96
- If too high, the product will be excluded from optimization.
97
- """
98
- return True
99
 
100
 
101
  def standalone_master_filter(self, product_id: str) -> Tuple[str, bool]:
@@ -121,6 +114,102 @@ class DemandFilter:
121
  return "unknown", False
122
  else:
123
  return "unclassified", False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124
 
125
  def is_product_ready_for_optimization(self, product_id: str) -> Tuple[bool, List[str]]:
126
  """
@@ -175,6 +264,13 @@ class DemandFilter:
175
  if self.speed_data is None or product_id not in self.speed_data:
176
  exclusion_reasons.append("Missing production speed data")
177
 
 
 
 
 
 
 
 
178
  is_ready = len(exclusion_reasons) == 0
179
  return is_ready, exclusion_reasons
180
 
@@ -199,8 +295,6 @@ class DemandFilter:
199
  excluded_demand = {}
200
  excluded_details = {}
201
 
202
- print("🔍 FILTERING DEMAND DATA FOR OPTIMIZATION")
203
-
204
  for product_id, demand in self.demand_data.items():
205
  is_ready, exclusion_reasons = self.is_product_ready_for_optimization(product_id)
206
 
@@ -215,27 +309,6 @@ class DemandFilter:
215
  # Sort products for consistent output
216
  included_products.sort()
217
  excluded_products.sort()
218
-
219
- # Print summary
220
- total_demand = sum(self.demand_data.values())
221
- included_total = sum(included_demand.values())
222
- excluded_total = sum(excluded_demand.values())
223
-
224
- print(f"✅ INCLUDED in optimization: {len(included_products)} products ({included_total:,} units)")
225
- print(f"🚫 EXCLUDED from optimization: {len(excluded_products)} products ({excluded_total:,} units)")
226
- print(f"📊 Total demand: {total_demand:,} units")
227
-
228
- # Print exclusion breakdown
229
- if excluded_products:
230
- print(f"\n📋 EXCLUSION BREAKDOWN:")
231
- reason_counts = {}
232
- for reasons in excluded_details.values():
233
- for reason in reasons:
234
- reason_counts[reason] = reason_counts.get(reason, 0) + 1
235
-
236
- for reason, count in reason_counts.items():
237
- print(f" • {reason}: {count} products")
238
-
239
  # Print data quality warnings for included products
240
  included_without_hierarchy = sum(1 for pid in included_products if self.standalone_master_filter(pid)[0] == "unclassified")
241
  if included_without_hierarchy > 0:
@@ -282,6 +355,9 @@ class DemandFilter:
282
 
283
  # Get production speed info
284
  has_speed_data = speed_data is not None and product_id in speed_data
 
 
 
285
 
286
  product_details[product_id] = {
287
  'demand': demand,
@@ -296,13 +372,16 @@ class DemandFilter:
296
  'has_line_assignment': line_assignment is not None,
297
  'has_staffing': (unicef_staff + humanizer_staff) > 0,
298
  'has_hierarchy': product_type != "unclassified",
299
- 'has_speed_data': has_speed_data
 
300
  }
301
 
302
  # Calculate data quality statistics for included products
303
  included_without_speed = sum(1 for pid in included_products if not product_details[pid]['has_speed_data'])
304
  included_without_hierarchy = sum(1 for pid in included_products if not product_details[pid]['has_hierarchy'])
305
 
 
 
306
  return {
307
  'included_count': len(included_products),
308
  'included_demand': sum(included_demand.values()),
@@ -316,7 +395,8 @@ class DemandFilter:
316
  'excluded_products': excluded_products,
317
  # Data quality metrics for included products
318
  'included_missing_speed_count': included_without_speed,
319
- 'included_missing_hierarchy_count': included_without_hierarchy
 
320
  }
321
 
322
 
 
89
  except Exception as e:
90
  print(f"Error loading data for filtering: {str(e)}")
91
  return False
 
 
 
 
 
 
 
92
 
93
 
94
  def standalone_master_filter(self, product_id: str) -> Tuple[str, bool]:
 
114
  return "unknown", False
115
  else:
116
  return "unclassified", False
117
+
118
+ def _get_line_type_capacity(self, line_type: int) -> int:
119
+ """
120
+ Calculate the total capacity in hours for a specific line type.
121
+
122
+ Args:
123
+ line_type: The line type ID (e.g., 6 for Long Line, 7 for Mini Load)
124
+
125
+ Returns:
126
+ int: Total capacity in hours for this line type
127
+ """
128
+ from src.config.optimization_config import get_line_cnt_per_type, get_max_hour_per_shift_per_person, get_active_shift_list, DATE_SPAN
129
+
130
+ line_cnt_per_type = get_line_cnt_per_type()
131
+ max_hours_per_shift_dict = get_max_hour_per_shift_per_person()
132
+ active_shifts = get_active_shift_list()
133
+
134
+ # Get line count for this specific line type
135
+ line_count = line_cnt_per_type.get(line_type, 0)
136
+
137
+ # Calculate total hours per day (sum of all active shift hours)
138
+ total_hours_per_day = sum(max_hours_per_shift_dict.get(shift, 0) for shift in active_shifts)
139
+
140
+ # Calculate available capacity hours
141
+ # Available hours = line_count × total_hours_per_day × days_in_period
142
+ available_hours = line_count * total_hours_per_day * len(DATE_SPAN)
143
+
144
+ return available_hours
145
+
146
+ def get_maximum_packaging_capacity(self) -> int:
147
+ """
148
+ Get the maximum packaging capacity across all line types.
149
+
150
+ Returns:
151
+ int: Maximum total capacity in hours across all lines
152
+ """
153
+ from src.config.optimization_config import get_line_cnt_per_type
154
+
155
+ line_cnt_per_type = get_line_cnt_per_type()
156
+ total_capacity = 0
157
+
158
+ for line_type, line_count in line_cnt_per_type.items():
159
+ if line_count > 0: # Only count active lines
160
+ line_capacity = self._get_line_type_capacity(line_type)
161
+ total_capacity += line_capacity
162
+
163
+ return total_capacity
164
+
165
+ def too_high_demand_filter(self, product_id: str) -> bool:
166
+ """
167
+ Check if the demand for a product is too high.
168
+
169
+ A product has "too high demand" when the total processing hours needed
170
+ exceeds the available capacity hours for the product's assigned line type.
171
+
172
+ NOTE: This method assumes all prerequisite data is available (demand > 0,
173
+ line assignment exists, speed data exists). The main filter function
174
+ should handle these edge cases.
175
+
176
+ Calculation:
177
+ - Processing hours needed = demand_quantity / production_speed_per_hour
178
+ - Available hours = line_count × hours_per_shift × shifts_per_day × days_in_period
179
+
180
+ Args:
181
+ product_id: The product ID to check
182
+
183
+ Returns:
184
+ bool: True if demand is too high (should be excluded), False otherwise
185
+ """
186
+ # Get demand for this product (assumes demand > 0, checked by main filter)
187
+ demand = self.demand_data.get(product_id, 0)
188
+ if demand <= 0:
189
+ return False
190
+ # Get line assignment for this product (assumes exists, checked by main filter)
191
+ if self.line_assignments is None or product_id not in self.line_assignments:
192
+ return False
193
+ line_type = self.line_assignments.get(product_id)
194
+
195
+ # Get production speed data (assumes exists, checked by main filter)
196
+ if self.speed_data is None or product_id not in self.speed_data:
197
+ return False
198
+ production_speed_per_hour = self.speed_data[product_id]
199
+
200
+ # Calculate processing hours needed
201
+ processing_hours_needed = demand / production_speed_per_hour
202
+
203
+ # Get available capacity for this specific line type
204
+ available_hours = self._get_line_type_capacity(line_type)
205
+
206
+ # Check if processing hours needed exceeds available capacity
207
+ is_too_high = processing_hours_needed > available_hours
208
+
209
+ if is_too_high:
210
+ print(f"⚠️ HIGH DEMAND WARNING: {product_id} needs {processing_hours_needed:.1f}h but only {available_hours:.1f}h available (line_type={line_type}, demand={demand}, speed={production_speed_per_hour:.1f}/h)")
211
+
212
+ return is_too_high
213
 
214
  def is_product_ready_for_optimization(self, product_id: str) -> Tuple[bool, List[str]]:
215
  """
 
264
  if self.speed_data is None or product_id not in self.speed_data:
265
  exclusion_reasons.append("Missing production speed data")
266
 
267
+ # Check if demand is too high (only if we have all required data)
268
+ if self.too_high_demand_filter(product_id):
269
+ exclusion_reasons.append("Demand exceeds available production capacity")
270
+
271
+
272
+
273
+
274
  is_ready = len(exclusion_reasons) == 0
275
  return is_ready, exclusion_reasons
276
 
 
295
  excluded_demand = {}
296
  excluded_details = {}
297
 
 
 
298
  for product_id, demand in self.demand_data.items():
299
  is_ready, exclusion_reasons = self.is_product_ready_for_optimization(product_id)
300
 
 
309
  # Sort products for consistent output
310
  included_products.sort()
311
  excluded_products.sort()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
312
  # Print data quality warnings for included products
313
  included_without_hierarchy = sum(1 for pid in included_products if self.standalone_master_filter(pid)[0] == "unclassified")
314
  if included_without_hierarchy > 0:
 
355
 
356
  # Get production speed info
357
  has_speed_data = speed_data is not None and product_id in speed_data
358
+
359
+ # too high demand
360
+ has_too_high_demand = self.too_high_demand_filter(product_id)
361
 
362
  product_details[product_id] = {
363
  'demand': demand,
 
372
  'has_line_assignment': line_assignment is not None,
373
  'has_staffing': (unicef_staff + humanizer_staff) > 0,
374
  'has_hierarchy': product_type != "unclassified",
375
+ 'has_speed_data': has_speed_data,
376
+ 'has_too_high_demand': has_too_high_demand
377
  }
378
 
379
  # Calculate data quality statistics for included products
380
  included_without_speed = sum(1 for pid in included_products if not product_details[pid]['has_speed_data'])
381
  included_without_hierarchy = sum(1 for pid in included_products if not product_details[pid]['has_hierarchy'])
382
 
383
+ # Count products excluded due to too high demand
384
+ excluded_with_too_high_demand = sum(1 for pid in excluded_products if product_details[pid]['has_too_high_demand'])
385
  return {
386
  'included_count': len(included_products),
387
  'included_demand': sum(included_demand.values()),
 
395
  'excluded_products': excluded_products,
396
  # Data quality metrics for included products
397
  'included_missing_speed_count': included_without_speed,
398
+ 'included_missing_hierarchy_count': included_without_hierarchy,
399
+ 'excluded_with_too_high_demand_count': excluded_with_too_high_demand
400
  }
401
 
402
 
src/demand_validation_viz.py CHANGED
@@ -87,7 +87,12 @@ class DemandValidationViz:
87
  if not details['has_hierarchy']:
88
  issues.append("no_hierarchy_data")
89
  validation_status = f"⚠️ Data Issues: {', '.join(issues)}" if issues else "✅ Ready for optimization"
90
-
 
 
 
 
 
91
  results.append({
92
  'Product ID': product_id,
93
  'Demand': details['demand'],
@@ -108,6 +113,7 @@ class DemandValidationViz:
108
  'Excluded from Optimization': not details['is_included_in_optimization'],
109
  'Exclusion Reasons': ', '.join(details['exclusion_reasons']) if details['exclusion_reasons'] else '',
110
  'Data Quality Issues': ', '.join(issues) if details['is_included_in_optimization'] and 'issues' in locals() and issues else '',
 
111
  'Validation Status': validation_status
112
  })
113
 
@@ -134,7 +140,8 @@ class DemandValidationViz:
134
  'no_hierarchy': len(included_df[included_df['Has Hierarchy Data'] == "❌"]),
135
  'standalone_masters': analysis['standalone_masters_count'],
136
  'total_unicef_needed': sum(p['unicef_staff'] for p in analysis['product_details'].values()),
137
- 'total_humanizer_needed': sum(p['humanizer_staff'] for p in analysis['product_details'].values())
 
138
  }
139
 
140
 
@@ -191,7 +198,8 @@ def display_demand_validation():
191
  delta=None if stats['no_speed'] == 0 else "Will use default")
192
  col4.metric("No Hierarchy Data", stats['no_hierarchy'],
193
  delta=None if stats['no_hierarchy'] == 0 else "Issue")
194
-
 
195
  # ===== INCLUDED PRODUCTS TABLE =====
196
  included_df = validation_df[validation_df['Excluded from Optimization'] == False].copy()
197
  excluded_df = validation_df[validation_df['Excluded from Optimization'] == True].copy()
 
87
  if not details['has_hierarchy']:
88
  issues.append("no_hierarchy_data")
89
  validation_status = f"⚠️ Data Issues: {', '.join(issues)}" if issues else "✅ Ready for optimization"
90
+
91
+
92
+
93
+ if details['has_too_high_demand']:
94
+ issues.append("too_high_demand")
95
+ validation_status = f"⚠️ Data Issues: {', '.join(issues)}" if issues else "✅ Ready for optimization"
96
  results.append({
97
  'Product ID': product_id,
98
  'Demand': details['demand'],
 
113
  'Excluded from Optimization': not details['is_included_in_optimization'],
114
  'Exclusion Reasons': ', '.join(details['exclusion_reasons']) if details['exclusion_reasons'] else '',
115
  'Data Quality Issues': ', '.join(issues) if details['is_included_in_optimization'] and 'issues' in locals() and issues else '',
116
+ 'Has Too High Demand': "✅" if details['has_too_high_demand'] else "❌",
117
  'Validation Status': validation_status
118
  })
119
 
 
140
  'no_hierarchy': len(included_df[included_df['Has Hierarchy Data'] == "❌"]),
141
  'standalone_masters': analysis['standalone_masters_count'],
142
  'total_unicef_needed': sum(p['unicef_staff'] for p in analysis['product_details'].values()),
143
+ 'total_humanizer_needed': sum(p['humanizer_staff'] for p in analysis['product_details'].values()),
144
+ 'excluded_with_too_high_demand': analysis['excluded_with_too_high_demand_count']
145
  }
146
 
147
 
 
198
  delta=None if stats['no_speed'] == 0 else "Will use default")
199
  col4.metric("No Hierarchy Data", stats['no_hierarchy'],
200
  delta=None if stats['no_hierarchy'] == 0 else "Issue")
201
+ col5.metric("Excluded: Too High Demand", stats['excluded_with_too_high_demand'],
202
+ delta=None if stats['excluded_with_too_high_demand'] == 0 else "Excluded")
203
  # ===== INCLUDED PRODUCTS TABLE =====
204
  included_df = validation_df[validation_df['Excluded from Optimization'] == False].copy()
205
  excluded_df = validation_df[validation_df['Excluded from Optimization'] == True].copy()