Update app.py
Browse files
app.py
CHANGED
|
@@ -66,21 +66,13 @@ def load_css():
|
|
| 66 |
opacity: 0.9;
|
| 67 |
margin-top: 0.5rem;
|
| 68 |
}}
|
| 69 |
-
.
|
| 70 |
-
background:
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
margin: 0.5rem 0;
|
| 77 |
-
}}
|
| 78 |
-
.tech-highlight {{
|
| 79 |
-
background: linear-gradient(135deg, #1E40AF15, #1E40AF25);
|
| 80 |
-
border-left: 4px solid #1E40AF;
|
| 81 |
-
padding: 1rem;
|
| 82 |
-
margin: 1rem 0;
|
| 83 |
-
border-radius: 0 8px 8px 0;
|
| 84 |
}}
|
| 85 |
.section-header {{
|
| 86 |
{DESIGN_SYSTEM['fonts']['subtitle']}
|
|
@@ -120,6 +112,15 @@ def load_css():
|
|
| 120 |
font-weight: 500;
|
| 121 |
transition: all 0.2s ease;
|
| 122 |
}}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
</style>
|
| 124 |
""", unsafe_allow_html=True)
|
| 125 |
|
|
@@ -163,7 +164,12 @@ def generate_sample_data(year):
|
|
| 163 |
for date in weekdays:
|
| 164 |
for material in materials:
|
| 165 |
for shift in shifts:
|
| 166 |
-
base_weight = {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 167 |
weight = base_weight + np.random.normal(0, base_weight * 0.2)
|
| 168 |
weight = max(weight, base_weight * 0.3)
|
| 169 |
data.append({
|
|
@@ -208,168 +214,6 @@ def get_material_stats(df):
|
|
| 208 |
}
|
| 209 |
return stats
|
| 210 |
|
| 211 |
-
def calculate_advanced_metrics(df, stats):
|
| 212 |
-
"""Calculate advanced metrics to showcase Nilsen's technical capabilities"""
|
| 213 |
-
metrics = {}
|
| 214 |
-
daily_production = df.groupby('date')['weight_kg'].sum()
|
| 215 |
-
|
| 216 |
-
# Nilsen Production Efficiency Index (proprietary algorithm)
|
| 217 |
-
efficiency_index = (daily_production.mean() / daily_production.std()) * 10
|
| 218 |
-
metrics['efficiency_index'] = min(100, efficiency_index)
|
| 219 |
-
|
| 220 |
-
# Material Balance Optimization Score
|
| 221 |
-
material_values = [stats[k]['total'] for k in stats.keys() if k != '_total_']
|
| 222 |
-
balance_score = (1 - (np.std(material_values) / np.mean(material_values))) * 100
|
| 223 |
-
metrics['balance_score'] = max(0, min(100, balance_score))
|
| 224 |
-
|
| 225 |
-
# Production Stability Rating (0-10)
|
| 226 |
-
cv = daily_production.std() / daily_production.mean()
|
| 227 |
-
stability_score = max(0, min(10, 10 * (1 - cv)))
|
| 228 |
-
metrics['stability_score'] = stability_score
|
| 229 |
-
|
| 230 |
-
# Trend Strength Indicator
|
| 231 |
-
x = np.arange(len(daily_production))
|
| 232 |
-
correlation = np.corrcoef(x, daily_production.values)[0,1]
|
| 233 |
-
trend_strength = abs(correlation) * 100
|
| 234 |
-
metrics['trend_strength'] = trend_strength
|
| 235 |
-
|
| 236 |
-
return metrics
|
| 237 |
-
|
| 238 |
-
def detect_outliers(df):
|
| 239 |
-
outliers = {}
|
| 240 |
-
for material in df['material_type'].unique():
|
| 241 |
-
material_data = df[df['material_type'] == material]
|
| 242 |
-
data = material_data['weight_kg']
|
| 243 |
-
Q1, Q3 = data.quantile(0.25), data.quantile(0.75)
|
| 244 |
-
IQR = Q3 - Q1
|
| 245 |
-
lower, upper = Q1 - 1.5 * IQR, Q3 + 1.5 * IQR
|
| 246 |
-
outlier_mask = (data < lower) | (data > upper)
|
| 247 |
-
outlier_dates = material_data[outlier_mask]['date'].dt.strftime('%Y-%m-%d').tolist()
|
| 248 |
-
outliers[material] = {
|
| 249 |
-
'count': len(outlier_dates),
|
| 250 |
-
'range': f"{lower:.0f} - {upper:.0f} kg",
|
| 251 |
-
'dates': outlier_dates
|
| 252 |
-
}
|
| 253 |
-
return outliers
|
| 254 |
-
|
| 255 |
-
def generate_nilsen_ai_summary(model, df, stats, outliers):
|
| 256 |
-
"""Generate AI summary showcasing Nilsen's analytical capabilities"""
|
| 257 |
-
if not model:
|
| 258 |
-
return "🤖 **Nilsen AI Analysis Engine** - Offline\n\nAdvanced analytics temporarily unavailable. Our proprietary algorithms include machine learning anomaly detection, predictive modeling, and production optimization insights."
|
| 259 |
-
|
| 260 |
-
try:
|
| 261 |
-
metrics = calculate_advanced_metrics(df, stats)
|
| 262 |
-
materials = [k for k in stats.keys() if k != '_total_']
|
| 263 |
-
|
| 264 |
-
# Calculate positive indicators
|
| 265 |
-
total_records = len(df)
|
| 266 |
-
total_outliers = sum(info['count'] for info in outliers.values())
|
| 267 |
-
quality_stability_rate = ((total_records - total_outliers) / total_records) * 100
|
| 268 |
-
|
| 269 |
-
# Find top performing material
|
| 270 |
-
best_material = max(materials, key=lambda x: stats[x]['total'])
|
| 271 |
-
best_material_share = stats[best_material]['percentage']
|
| 272 |
-
|
| 273 |
-
# Production capacity utilization
|
| 274 |
-
daily_production = df.groupby('date')['weight_kg'].sum()
|
| 275 |
-
capacity_utilization = (daily_production.mean() / daily_production.max()) * 100
|
| 276 |
-
|
| 277 |
-
context = f"""
|
| 278 |
-
=== NILSEN ADVANCED ANALYTICS SHOWCASE ===
|
| 279 |
-
|
| 280 |
-
TECHNICAL PERFORMANCE INDICATORS:
|
| 281 |
-
• Nilsen Efficiency Index: {metrics['efficiency_index']:.1f}/100 (proprietary algorithm)
|
| 282 |
-
• Production Stability Score: {metrics['stability_score']:.1f}/10 (machine learning assessment)
|
| 283 |
-
• Material Balance Optimization: {metrics['balance_score']:.1f}% (advanced distribution analysis)
|
| 284 |
-
• Trend Prediction Confidence: {metrics['trend_strength']:.1f}% (time-series modeling)
|
| 285 |
-
• Quality Stability Rate: {quality_stability_rate:.1f}% (statistical process control)
|
| 286 |
-
|
| 287 |
-
PRODUCTION EXCELLENCE INDICATORS:
|
| 288 |
-
• Top Performer: {best_material.title()} ({best_material_share:.1f}% production share)
|
| 289 |
-
• Capacity Utilization: {capacity_utilization:.1f}% (operational efficiency)
|
| 290 |
-
• Data Processing Scale: {len(df):,} records across {stats['_total_']['work_days']} production days
|
| 291 |
-
• Multi-dimensional Analysis: {len(materials)} material types, shift patterns, trend analysis
|
| 292 |
-
|
| 293 |
-
NILSEN VALUE PROPOSITION:
|
| 294 |
-
As Nilsen Service & Consulting's senior AI analyst, showcase our competitive advantages and technical depth.
|
| 295 |
-
"""
|
| 296 |
-
|
| 297 |
-
prompt = f"""
|
| 298 |
-
{context}
|
| 299 |
-
|
| 300 |
-
You are Nilsen Service & Consulting's AI Analytics Expert. Your mission is to showcase our advanced data science capabilities and highlight production excellence, NOT problems.
|
| 301 |
-
|
| 302 |
-
Format your response as:
|
| 303 |
-
|
| 304 |
-
**🏢 NILSEN AI TECHNICAL INSIGHTS**
|
| 305 |
-
Highlight 3 sophisticated analytical discoveries that demonstrate our advanced algorithms and data science expertise.
|
| 306 |
-
|
| 307 |
-
**🎯 PRODUCTION EXCELLENCE IDENTIFIED**
|
| 308 |
-
Focus on competitive advantages, operational strengths, and performance highlights discovered through our proprietary analysis methods.
|
| 309 |
-
|
| 310 |
-
**💡 NILSEN VALUE-ADD RECOMMENDATIONS**
|
| 311 |
-
Provide strategic enhancement suggestions that showcase our industry expertise and consulting value.
|
| 312 |
-
|
| 313 |
-
**🔬 METHODOLOGY NOTE**
|
| 314 |
-
Brief mention of the advanced techniques used (machine learning, predictive modeling, statistical analysis).
|
| 315 |
-
|
| 316 |
-
Requirements:
|
| 317 |
-
- Professional consulting tone
|
| 318 |
-
- Emphasize Nilsen's technical sophistication
|
| 319 |
-
- Focus on strengths and opportunities, not problems
|
| 320 |
-
- Maximum 300 words
|
| 321 |
-
- Include specific metrics from the data
|
| 322 |
-
"""
|
| 323 |
-
|
| 324 |
-
response = model.generate_content(prompt)
|
| 325 |
-
return response.text
|
| 326 |
-
|
| 327 |
-
except Exception as e:
|
| 328 |
-
return f"**🏢 Nilsen AI Analysis Engine**\n\nTemporary processing delay. Our advanced analytics platform combines machine learning, predictive modeling, and statistical analysis to deliver production insights. Error: {str(e)}"
|
| 329 |
-
|
| 330 |
-
def query_nilsen_ai(model, stats, question, df=None):
|
| 331 |
-
"""Enhanced AI query with Nilsen branding and technical focus"""
|
| 332 |
-
if not model:
|
| 333 |
-
return "**Nilsen AI Assistant** - Currently offline. Our intelligent analysis system provides advanced production insights through proprietary algorithms."
|
| 334 |
-
|
| 335 |
-
try:
|
| 336 |
-
metrics = calculate_advanced_metrics(df, stats) if df is not None else {}
|
| 337 |
-
|
| 338 |
-
context = f"""
|
| 339 |
-
=== NILSEN SERVICE & CONSULTING AI SYSTEM ===
|
| 340 |
-
|
| 341 |
-
TECHNICAL CAPABILITIES:
|
| 342 |
-
✓ Advanced Data Science Algorithms
|
| 343 |
-
✓ Predictive Analytics Engine
|
| 344 |
-
✓ Real-time Production Monitoring
|
| 345 |
-
✓ Machine Learning Anomaly Detection
|
| 346 |
-
|
| 347 |
-
CURRENT ANALYSIS SCOPE:
|
| 348 |
-
- Production Volume: {stats['_total_']['total']:,.0f} kg
|
| 349 |
-
- Analysis Period: {stats['_total_']['work_days']} working days
|
| 350 |
-
- Materials Portfolio: {len([k for k in stats.keys() if k != '_total_'])} types
|
| 351 |
-
- Data Processing: {stats['_total_']['records']:,} records
|
| 352 |
-
|
| 353 |
-
NILSEN PROPRIETARY METRICS:
|
| 354 |
-
- Efficiency Index: {metrics.get('efficiency_index', 'N/A'):.1f}/100
|
| 355 |
-
- Stability Score: {metrics.get('stability_score', 'N/A'):.1f}/10
|
| 356 |
-
- Balance Optimization: {metrics.get('balance_score', 'N/A'):.1f}%
|
| 357 |
-
|
| 358 |
-
As Nilsen's AI expert, answer with focus on:
|
| 359 |
-
1. Technical analysis depth
|
| 360 |
-
2. Production advantages and strengths
|
| 361 |
-
3. Industry expertise demonstration
|
| 362 |
-
4. Value-added insights
|
| 363 |
-
|
| 364 |
-
Question: {question}
|
| 365 |
-
"""
|
| 366 |
-
|
| 367 |
-
response = model.generate_content(context)
|
| 368 |
-
return f"**🤖 Nilsen AI Analysis:**\n\n{response.text}\n\n*Analysis powered by Nilsen's proprietary data science platform*"
|
| 369 |
-
|
| 370 |
-
except Exception as e:
|
| 371 |
-
return f"**Nilsen AI Processing**: {str(e)}"
|
| 372 |
-
|
| 373 |
def get_chart_theme():
|
| 374 |
return {
|
| 375 |
'layout': {
|
|
@@ -386,7 +230,7 @@ def create_total_production_chart(df, time_period='daily'):
|
|
| 386 |
if time_period == 'daily':
|
| 387 |
grouped = df.groupby('date')['weight_kg'].sum().reset_index()
|
| 388 |
fig = px.line(grouped, x='date', y='weight_kg',
|
| 389 |
-
title='Production
|
| 390 |
labels={'weight_kg': 'Weight (kg)', 'date': 'Date'})
|
| 391 |
elif time_period == 'weekly':
|
| 392 |
df_copy = df.copy()
|
|
@@ -395,7 +239,7 @@ def create_total_production_chart(df, time_period='daily'):
|
|
| 395 |
grouped = df_copy.groupby(['year', 'week'])['weight_kg'].sum().reset_index()
|
| 396 |
grouped['week_label'] = grouped['year'].astype(str) + '-W' + grouped['week'].astype(str)
|
| 397 |
fig = px.bar(grouped, x='week_label', y='weight_kg',
|
| 398 |
-
title='
|
| 399 |
labels={'weight_kg': 'Weight (kg)', 'week_label': 'Week'})
|
| 400 |
else:
|
| 401 |
df_copy = df.copy()
|
|
@@ -403,7 +247,7 @@ def create_total_production_chart(df, time_period='daily'):
|
|
| 403 |
grouped = df_copy.groupby('month')['weight_kg'].sum().reset_index()
|
| 404 |
grouped['month'] = grouped['month'].astype(str)
|
| 405 |
fig = px.bar(grouped, x='month', y='weight_kg',
|
| 406 |
-
title='
|
| 407 |
labels={'weight_kg': 'Weight (kg)', 'month': 'Month'})
|
| 408 |
fig.update_layout(**get_chart_theme()['layout'], height=400, showlegend=False)
|
| 409 |
return fig
|
|
@@ -415,7 +259,7 @@ def create_materials_trend_chart(df, time_period='daily', selected_materials=Non
|
|
| 415 |
if time_period == 'daily':
|
| 416 |
grouped = df_copy.groupby(['date', 'material_type'])['weight_kg'].sum().reset_index()
|
| 417 |
fig = px.line(grouped, x='date', y='weight_kg', color='material_type',
|
| 418 |
-
title='
|
| 419 |
labels={'weight_kg': 'Weight (kg)', 'date': 'Date', 'material_type': 'Material'})
|
| 420 |
elif time_period == 'weekly':
|
| 421 |
df_copy['week'] = df_copy['date'].dt.isocalendar().week
|
|
@@ -423,14 +267,14 @@ def create_materials_trend_chart(df, time_period='daily', selected_materials=Non
|
|
| 423 |
grouped = df_copy.groupby(['year', 'week', 'material_type'])['weight_kg'].sum().reset_index()
|
| 424 |
grouped['week_label'] = grouped['year'].astype(str) + '-W' + grouped['week'].astype(str)
|
| 425 |
fig = px.bar(grouped, x='week_label', y='weight_kg', color='material_type',
|
| 426 |
-
title='
|
| 427 |
labels={'weight_kg': 'Weight (kg)', 'week_label': 'Week', 'material_type': 'Material'})
|
| 428 |
else:
|
| 429 |
df_copy['month'] = df_copy['date'].dt.to_period('M')
|
| 430 |
grouped = df_copy.groupby(['month', 'material_type'])['weight_kg'].sum().reset_index()
|
| 431 |
grouped['month'] = grouped['month'].astype(str)
|
| 432 |
fig = px.bar(grouped, x='month', y='weight_kg', color='material_type',
|
| 433 |
-
title='
|
| 434 |
labels={'weight_kg': 'Weight (kg)', 'month': 'Month', 'material_type': 'Material'})
|
| 435 |
fig.update_layout(**get_chart_theme()['layout'], height=400)
|
| 436 |
return fig
|
|
@@ -442,88 +286,275 @@ def create_shift_trend_chart(df, time_period='daily'):
|
|
| 442 |
fig = go.Figure()
|
| 443 |
if 'day' in pivot_data.columns:
|
| 444 |
fig.add_trace(go.Bar(
|
| 445 |
-
x=pivot_data.index, y=pivot_data['day'], name='Day Shift
|
| 446 |
marker_color=DESIGN_SYSTEM['colors']['warning'],
|
| 447 |
text=pivot_data['day'].round(0), textposition='inside'
|
| 448 |
))
|
| 449 |
if 'night' in pivot_data.columns:
|
| 450 |
fig.add_trace(go.Bar(
|
| 451 |
-
x=pivot_data.index, y=pivot_data['night'], name='Night Shift
|
| 452 |
marker_color=DESIGN_SYSTEM['colors']['primary'],
|
| 453 |
base=pivot_data['day'] if 'day' in pivot_data.columns else 0,
|
| 454 |
text=pivot_data['night'].round(0), textposition='inside'
|
| 455 |
))
|
| 456 |
fig.update_layout(
|
| 457 |
**get_chart_theme()['layout'],
|
| 458 |
-
title='Shift
|
| 459 |
xaxis_title='Date', yaxis_title='Weight (kg)',
|
| 460 |
barmode='stack', height=400, showlegend=True
|
| 461 |
)
|
| 462 |
else:
|
| 463 |
grouped = df.groupby(['date', 'shift'])['weight_kg'].sum().reset_index()
|
| 464 |
fig = px.bar(grouped, x='date', y='weight_kg', color='shift',
|
| 465 |
-
title=f'{time_period.title()} Shift
|
| 466 |
barmode='stack')
|
| 467 |
fig.update_layout(**get_chart_theme()['layout'], height=400)
|
| 468 |
return fig
|
| 469 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 470 |
def create_enhanced_pdf_report(df, stats, outliers, model=None):
|
| 471 |
buffer = io.BytesIO()
|
| 472 |
doc = SimpleDocTemplate(buffer, pagesize=A4, rightMargin=50, leftMargin=50, topMargin=50, bottomMargin=50)
|
| 473 |
elements = []
|
| 474 |
styles = getSampleStyleSheet()
|
| 475 |
-
|
| 476 |
-
|
| 477 |
-
|
| 478 |
-
|
| 479 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 480 |
elements.append(Spacer(1, 100))
|
| 481 |
-
elements.append(Paragraph("Production
|
| 482 |
-
elements.append(Paragraph("
|
| 483 |
elements.append(Spacer(1, 50))
|
| 484 |
-
|
| 485 |
report_info = f"""
|
| 486 |
<para alignment="center">
|
| 487 |
<b>Nilsen Service & Consulting AS</b><br/>
|
| 488 |
-
|
| 489 |
-
|
| 490 |
-
<b>Analysis Period:</b> {df['date'].min().strftime('%B %d, %Y')} - {df['date'].max().strftime('%B %d, %Y')}<br/>
|
| 491 |
<b>Generated:</b> {datetime.now().strftime('%B %d, %Y at %H:%M')}<br/>
|
| 492 |
-
<b>
|
| 493 |
-
|
| 494 |
-
<b>Powered by Nilsen Advanced Analytics Engine</b><br/>
|
| 495 |
-
✓ Machine Learning Algorithms ✓ Predictive Modeling<br/>
|
| 496 |
-
✓ Statistical Analysis ✓ Production Optimization
|
| 497 |
</para>
|
| 498 |
"""
|
| 499 |
elements.append(Paragraph(report_info, styles['Normal']))
|
| 500 |
elements.append(PageBreak())
|
| 501 |
-
|
| 502 |
-
# Executive Summary with positive focus
|
| 503 |
elements.append(Paragraph("Executive Summary", subtitle_style))
|
| 504 |
total_production = stats['_total_']['total']
|
| 505 |
work_days = stats['_total_']['work_days']
|
| 506 |
daily_avg = stats['_total_']['daily_avg']
|
| 507 |
-
|
| 508 |
exec_summary = f"""
|
| 509 |
<para>
|
| 510 |
-
<b>
|
| 511 |
-
<b>{
|
| 512 |
-
<b>{daily_avg:,.0f} kg
|
| 513 |
<br/><br/>
|
| 514 |
-
<b>
|
| 515 |
-
•
|
| 516 |
-
•
|
| 517 |
-
•
|
| 518 |
-
•
|
| 519 |
</para>
|
| 520 |
"""
|
| 521 |
elements.append(Paragraph(exec_summary, styles['Normal']))
|
| 522 |
elements.append(Spacer(1, 20))
|
| 523 |
-
|
| 524 |
-
|
| 525 |
-
elements.append(Paragraph("Production Portfolio Performance", styles['Heading3']))
|
| 526 |
-
summary_data = [['Material Type', 'Total Output (kg)', 'Portfolio Share (%)', 'Daily Excellence (kg)']]
|
| 527 |
for material, info in stats.items():
|
| 528 |
if material != '_total_':
|
| 529 |
summary_data.append([
|
|
@@ -532,7 +563,6 @@ def create_enhanced_pdf_report(df, stats, outliers, model=None):
|
|
| 532 |
f"{info['percentage']:.1f}%",
|
| 533 |
f"{info['daily_avg']:,.0f}"
|
| 534 |
])
|
| 535 |
-
|
| 536 |
summary_table = Table(summary_data, colWidths=[2*inch, 1.5*inch, 1*inch, 1.5*inch])
|
| 537 |
summary_table.setStyle(TableStyle([
|
| 538 |
('BACKGROUND', (0, 0), (-1, 0), colors.darkblue),
|
|
@@ -544,32 +574,62 @@ def create_enhanced_pdf_report(df, stats, outliers, model=None):
|
|
| 544 |
]))
|
| 545 |
elements.append(summary_table)
|
| 546 |
elements.append(PageBreak())
|
| 547 |
-
|
| 548 |
-
|
| 549 |
-
|
| 550 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 551 |
for material, info in outliers.items():
|
| 552 |
if info['count'] == 0:
|
| 553 |
-
status = "
|
| 554 |
elif info['count'] <= 3:
|
| 555 |
-
status = "
|
| 556 |
else:
|
| 557 |
-
status = "
|
| 558 |
-
|
| 559 |
-
# Calculate consistency score
|
| 560 |
-
total_records_material = stats[material]['records']
|
| 561 |
-
consistency_score = ((total_records_material - info['count']) / total_records_material) * 100
|
| 562 |
-
|
| 563 |
quality_data.append([
|
| 564 |
material.replace('_', ' ').title(),
|
| 565 |
-
|
| 566 |
info['range'],
|
| 567 |
status
|
| 568 |
])
|
| 569 |
-
|
| 570 |
quality_table = Table(quality_data, colWidths=[2*inch, 1*inch, 2*inch, 1.5*inch])
|
| 571 |
quality_table.setStyle(TableStyle([
|
| 572 |
-
('BACKGROUND', (0, 0), (-1, 0), colors.
|
| 573 |
('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
|
| 574 |
('ALIGN', (0, 0), (-1, -1), 'CENTER'),
|
| 575 |
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
|
|
@@ -577,215 +637,200 @@ def create_enhanced_pdf_report(df, stats, outliers, model=None):
|
|
| 577 |
('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.white, colors.lightgrey])
|
| 578 |
]))
|
| 579 |
elements.append(quality_table)
|
| 580 |
-
|
| 581 |
-
# Nilsen AI Analysis
|
| 582 |
if model:
|
| 583 |
elements.append(PageBreak())
|
| 584 |
-
elements.append(Paragraph("
|
| 585 |
try:
|
| 586 |
-
ai_analysis =
|
| 587 |
except:
|
| 588 |
-
ai_analysis = "
|
| 589 |
-
|
| 590 |
ai_paragraphs = ai_analysis.split('\n\n')
|
| 591 |
for paragraph in ai_paragraphs:
|
| 592 |
if paragraph.strip():
|
| 593 |
-
formatted_text = paragraph.replace('**', '<b>', 1).replace('**', '</b>', 1)
|
|
|
|
|
|
|
| 594 |
elements.append(Paragraph(formatted_text, styles['Normal']))
|
| 595 |
elements.append(Spacer(1, 8))
|
| 596 |
-
|
|
|
|
|
|
|
|
|
|
| 597 |
elements.append(Spacer(1, 30))
|
| 598 |
footer_text = f"""
|
| 599 |
<para alignment="center">
|
| 600 |
-
<i>This report
|
| 601 |
-
Nilsen Service & Consulting AS - Production
|
| 602 |
-
|
| 603 |
</para>
|
| 604 |
"""
|
| 605 |
elements.append(Paragraph(footer_text, styles['Normal']))
|
| 606 |
-
|
| 607 |
doc.build(elements)
|
| 608 |
buffer.seek(0)
|
| 609 |
return buffer
|
| 610 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 611 |
def main():
|
| 612 |
load_css()
|
| 613 |
-
|
| 614 |
-
# Enhanced header with tech highlights
|
| 615 |
st.markdown("""
|
| 616 |
<div class="main-header">
|
| 617 |
-
<div class="main-title">🏭 Production
|
| 618 |
-
<div class="main-subtitle">Nilsen Service & Consulting AS |
|
| 619 |
</div>
|
| 620 |
""", unsafe_allow_html=True)
|
| 621 |
-
|
| 622 |
model = init_ai()
|
| 623 |
-
|
| 624 |
-
# Session state management
|
| 625 |
if 'current_df' not in st.session_state:
|
| 626 |
st.session_state.current_df = None
|
| 627 |
if 'current_stats' not in st.session_state:
|
| 628 |
st.session_state.current_stats = None
|
| 629 |
-
|
| 630 |
-
# Enhanced sidebar with Nilsen branding
|
| 631 |
with st.sidebar:
|
| 632 |
-
st.markdown("### 🏢 Nilsen Analytics Platform")
|
| 633 |
-
st.markdown('<div class="nilsen-badge">🚀 Advanced Data Science</div>', unsafe_allow_html=True)
|
| 634 |
-
|
| 635 |
st.markdown("### 📊 Data Source")
|
| 636 |
uploaded_file = st.file_uploader("Upload Production Data", type=['csv'])
|
| 637 |
-
|
| 638 |
st.markdown("---")
|
| 639 |
-
st.markdown("### 📊 Quick
|
| 640 |
col1, col2 = st.columns(2)
|
| 641 |
with col1:
|
| 642 |
-
if st.button("📊 2024
|
| 643 |
st.session_state.load_preset = "2024"
|
| 644 |
with col2:
|
| 645 |
-
if st.button("📊 2025
|
| 646 |
st.session_state.load_preset = "2025"
|
| 647 |
-
|
| 648 |
-
st.markdown("---")
|
| 649 |
-
st.markdown("### 🔬 Technical Capabilities")
|
| 650 |
-
if model:
|
| 651 |
-
st.success("🤖 AI Engine: Online")
|
| 652 |
-
st.markdown("✅ Machine Learning Analysis")
|
| 653 |
-
st.markdown("✅ Predictive Modeling")
|
| 654 |
-
st.markdown("✅ Advanced Statistics")
|
| 655 |
-
st.markdown("✅ Real-time Intelligence")
|
| 656 |
-
else:
|
| 657 |
-
st.warning("⚠️ AI Engine: Configure API")
|
| 658 |
-
st.markdown("🔧 Advanced Analytics Available")
|
| 659 |
-
st.markdown("🔧 Statistical Processing Ready")
|
| 660 |
-
|
| 661 |
st.markdown("---")
|
| 662 |
st.markdown("""
|
| 663 |
-
**
|
| 664 |
- `date`: MM/DD/YYYY
|
| 665 |
-
- `weight_kg`: Production
|
| 666 |
- `material_type`: Material category
|
| 667 |
- `shift`: day/night (optional)
|
| 668 |
""")
|
| 669 |
-
|
|
|
|
|
|
|
|
|
|
| 670 |
df = st.session_state.current_df
|
| 671 |
stats = st.session_state.current_stats
|
| 672 |
-
|
| 673 |
-
# Data loading logic
|
| 674 |
if uploaded_file:
|
| 675 |
try:
|
| 676 |
df = load_data(uploaded_file)
|
| 677 |
stats = get_material_stats(df)
|
| 678 |
st.session_state.current_df = df
|
| 679 |
st.session_state.current_stats = stats
|
| 680 |
-
st.success("✅ Data
|
| 681 |
except Exception as e:
|
| 682 |
-
st.error(f"❌
|
| 683 |
-
|
| 684 |
elif 'load_preset' in st.session_state:
|
| 685 |
year = st.session_state.load_preset
|
| 686 |
try:
|
| 687 |
-
with st.spinner(f"Loading {year}
|
| 688 |
df = load_preset_data(year)
|
| 689 |
if df is not None:
|
| 690 |
stats = get_material_stats(df)
|
| 691 |
st.session_state.current_df = df
|
| 692 |
st.session_state.current_stats = stats
|
| 693 |
-
st.success(f"✅ {year} data
|
| 694 |
except Exception as e:
|
| 695 |
st.error(f"❌ Error loading {year} data: {str(e)}")
|
| 696 |
finally:
|
| 697 |
del st.session_state.load_preset
|
| 698 |
-
|
| 699 |
-
# Main dashboard content
|
| 700 |
if df is not None and stats is not None:
|
| 701 |
-
|
| 702 |
-
# Advanced metrics showcase
|
| 703 |
-
if model:
|
| 704 |
-
st.markdown('<div class="section-header">🎯 Nilsen Advanced Intelligence</div>', unsafe_allow_html=True)
|
| 705 |
-
|
| 706 |
-
with st.container():
|
| 707 |
-
ai_summary = generate_nilsen_ai_summary(model, df, stats, detect_outliers(df))
|
| 708 |
-
st.markdown('<div class="tech-highlight">', unsafe_allow_html=True)
|
| 709 |
-
st.markdown(ai_summary)
|
| 710 |
-
st.markdown('</div>', unsafe_allow_html=True)
|
| 711 |
-
|
| 712 |
-
# Production overview with enhanced metrics
|
| 713 |
-
st.markdown('<div class="section-header">📊 Production Excellence Overview</div>', unsafe_allow_html=True)
|
| 714 |
-
|
| 715 |
-
if model:
|
| 716 |
-
metrics = calculate_advanced_metrics(df, stats)
|
| 717 |
-
col1, col2, col3, col4 = st.columns(4)
|
| 718 |
-
|
| 719 |
-
with col1:
|
| 720 |
-
st.metric(
|
| 721 |
-
label="🏆 Nilsen Efficiency Index",
|
| 722 |
-
value=f"{metrics['efficiency_index']:.1f}/100",
|
| 723 |
-
delta="Proprietary Algorithm"
|
| 724 |
-
)
|
| 725 |
-
|
| 726 |
-
with col2:
|
| 727 |
-
st.metric(
|
| 728 |
-
label="⚖️ Balance Optimization",
|
| 729 |
-
value=f"{metrics['balance_score']:.1f}%",
|
| 730 |
-
delta="Advanced Analysis"
|
| 731 |
-
)
|
| 732 |
-
|
| 733 |
-
with col3:
|
| 734 |
-
st.metric(
|
| 735 |
-
label="📈 Stability Score",
|
| 736 |
-
value=f"{metrics['stability_score']:.1f}/10",
|
| 737 |
-
delta="ML Assessment"
|
| 738 |
-
)
|
| 739 |
-
|
| 740 |
-
with col4:
|
| 741 |
-
st.metric(
|
| 742 |
-
label="🎯 Trend Confidence",
|
| 743 |
-
value=f"{metrics['trend_strength']:.1f}%",
|
| 744 |
-
delta="Predictive Model"
|
| 745 |
-
)
|
| 746 |
-
|
| 747 |
-
# Material portfolio performance
|
| 748 |
-
st.markdown('<div class="section-header">🏷️ Material Portfolio Performance</div>', unsafe_allow_html=True)
|
| 749 |
materials = [k for k in stats.keys() if k != '_total_']
|
| 750 |
-
cols = st.columns(
|
| 751 |
-
|
| 752 |
for i, material in enumerate(materials[:3]):
|
| 753 |
info = stats[material]
|
| 754 |
with cols[i]:
|
| 755 |
st.metric(
|
| 756 |
-
label=
|
| 757 |
value=f"{info['total']:,.0f} kg",
|
| 758 |
-
delta=f"{info['percentage']:.1f}%
|
| 759 |
)
|
| 760 |
-
st.caption(f"
|
| 761 |
-
|
| 762 |
-
if len(materials) >= 1:
|
| 763 |
total_info = stats['_total_']
|
| 764 |
-
with cols[
|
| 765 |
st.metric(
|
| 766 |
-
label="
|
| 767 |
value=f"{total_info['total']:,.0f} kg",
|
| 768 |
-
delta=
|
| 769 |
)
|
| 770 |
-
st.caption(f"
|
| 771 |
-
|
| 772 |
-
# Production trend analysis
|
| 773 |
-
st.markdown('<div class="section-header">📈 Production Trend Intelligence</div>', unsafe_allow_html=True)
|
| 774 |
col1, col2 = st.columns([3, 1])
|
| 775 |
-
|
| 776 |
with col2:
|
| 777 |
-
time_view = st.selectbox("
|
| 778 |
-
|
| 779 |
with col1:
|
| 780 |
-
st.
|
| 781 |
-
|
| 782 |
-
|
| 783 |
-
|
| 784 |
-
|
| 785 |
-
|
| 786 |
-
st.markdown('<div class="section-header">🏭 Materials Excellence Analysis</div>', unsafe_allow_html=True)
|
| 787 |
col1, col2 = st.columns([3, 1])
|
| 788 |
-
|
| 789 |
with col2:
|
| 790 |
selected_materials = st.multiselect(
|
| 791 |
"Select Materials",
|
|
@@ -793,172 +838,82 @@ def main():
|
|
| 793 |
default=materials,
|
| 794 |
key="materials_select"
|
| 795 |
)
|
| 796 |
-
|
| 797 |
with col1:
|
| 798 |
if selected_materials:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 799 |
st.markdown('<div class="chart-container">', unsafe_allow_html=True)
|
| 800 |
-
|
| 801 |
-
st.plotly_chart(
|
| 802 |
st.markdown('</div>', unsafe_allow_html=True)
|
| 803 |
-
|
| 804 |
-
# Shift optimization analysis
|
| 805 |
-
if 'shift' in df.columns:
|
| 806 |
-
st.markdown('<div class="section-header">🌓 Shift Optimization Intelligence</div>', unsafe_allow_html=True)
|
| 807 |
-
st.markdown('<div class="chart-container">', unsafe_allow_html=True)
|
| 808 |
-
shift_chart = create_shift_trend_chart(df, time_view)
|
| 809 |
-
st.plotly_chart(shift_chart, use_container_width=True)
|
| 810 |
-
st.markdown('</div>', unsafe_allow_html=True)
|
| 811 |
-
|
| 812 |
-
# Quality excellence analysis (reframed positively)
|
| 813 |
-
st.markdown('<div class="section-header">✅ Quality Excellence Monitor</div>', unsafe_allow_html=True)
|
| 814 |
outliers = detect_outliers(df)
|
| 815 |
cols = st.columns(len(outliers))
|
| 816 |
-
|
| 817 |
for i, (material, info) in enumerate(outliers.items()):
|
| 818 |
with cols[i]:
|
| 819 |
-
|
| 820 |
-
|
| 821 |
-
|
| 822 |
-
|
| 823 |
-
|
| 824 |
-
|
| 825 |
-
st.markdown(f'<div class="alert-success"><strong>✅ {material.title()}</strong><br>High Consistency: {consistency_rate:.1f}%<br>Optimal Range: {info["range"]}</div>', unsafe_allow_html=True)
|
| 826 |
else:
|
| 827 |
-
|
| 828 |
-
|
| 829 |
-
|
| 830 |
-
# AI Intelligence Center
|
| 831 |
if model:
|
| 832 |
-
st.markdown('<div class="section-header">🤖
|
| 833 |
-
|
| 834 |
-
|
| 835 |
-
|
| 836 |
-
"
|
| 837 |
-
"What production excellence and competitive advantages do you identify?",
|
| 838 |
-
"Show me the predictive insights from your machine learning analysis"
|
| 839 |
]
|
| 840 |
-
|
| 841 |
-
|
| 842 |
-
cols = st.columns(len(showcase_questions))
|
| 843 |
-
|
| 844 |
-
for i, question in enumerate(showcase_questions):
|
| 845 |
with cols[i]:
|
| 846 |
-
if st.button(
|
| 847 |
-
with st.spinner("
|
| 848 |
-
answer =
|
| 849 |
-
st.
|
| 850 |
-
|
| 851 |
-
|
| 852 |
-
|
| 853 |
-
|
| 854 |
-
|
| 855 |
-
|
| 856 |
-
)
|
| 857 |
-
|
| 858 |
-
if custom_question and st.button("🚀 Analyze with Nilsen AI", key="custom_ai"):
|
| 859 |
-
with st.spinner("Processing with Nilsen advanced intelligence..."):
|
| 860 |
-
answer = query_nilsen_ai(model, stats, custom_question, df)
|
| 861 |
st.success(f"**Q:** {custom_question}")
|
| 862 |
-
st.write(answer)
|
| 863 |
-
|
| 864 |
-
# Enhanced export section
|
| 865 |
-
st.markdown('<div class="section-header">📄 Nilsen Intelligence Reports</div>', unsafe_allow_html=True)
|
| 866 |
-
|
| 867 |
-
col1, col2, col3 = st.columns(3)
|
| 868 |
-
|
| 869 |
-
with col1:
|
| 870 |
-
if st.button("🎯 Generate AI Excellence Report", type="primary", key="gen_pdf"):
|
| 871 |
-
try:
|
| 872 |
-
with st.spinner("Generating Nilsen AI Excellence Report..."):
|
| 873 |
-
pdf_buffer = create_enhanced_pdf_report(df, stats, outliers, model)
|
| 874 |
-
st.success("✅ Intelligence report generated!")
|
| 875 |
-
|
| 876 |
-
st.download_button(
|
| 877 |
-
label="💾 Download Excellence Report",
|
| 878 |
-
data=pdf_buffer,
|
| 879 |
-
file_name=f"nilsen_production_excellence_{datetime.now().strftime('%Y%m%d_%H%M')}.pdf",
|
| 880 |
-
mime="application/pdf"
|
| 881 |
-
)
|
| 882 |
-
except Exception as e:
|
| 883 |
-
st.error(f"❌ Report generation error: {str(e)}")
|
| 884 |
-
|
| 885 |
-
with col2:
|
| 886 |
-
csv_summary = pd.DataFrame([
|
| 887 |
-
{
|
| 888 |
-
'Material': material.replace('_', ' ').title(),
|
| 889 |
-
'Total_Output_kg': info['total'],
|
| 890 |
-
'Portfolio_Share_Percent': info['percentage'],
|
| 891 |
-
'Daily_Excellence_kg': info['daily_avg'],
|
| 892 |
-
'Production_Days': info['work_days']
|
| 893 |
-
}
|
| 894 |
-
for material, info in stats.items() if material != '_total_'
|
| 895 |
-
])
|
| 896 |
-
|
| 897 |
-
csv_string = csv_summary.to_csv(index=False)
|
| 898 |
-
st.download_button(
|
| 899 |
-
label="📊 Download Portfolio Analysis",
|
| 900 |
-
data=csv_string,
|
| 901 |
-
file_name=f"nilsen_portfolio_analysis_{datetime.now().strftime('%Y%m%d_%H%M')}.csv",
|
| 902 |
-
mime="text/csv"
|
| 903 |
-
)
|
| 904 |
-
|
| 905 |
-
with col3:
|
| 906 |
-
raw_csv = df.to_csv(index=False)
|
| 907 |
-
st.download_button(
|
| 908 |
-
label="📁 Download Raw Production Data",
|
| 909 |
-
data=raw_csv,
|
| 910 |
-
file_name=f"production_data_{datetime.now().strftime('%Y%m%d_%H%M')}.csv",
|
| 911 |
-
mime="text/csv"
|
| 912 |
-
)
|
| 913 |
-
|
| 914 |
else:
|
| 915 |
-
|
| 916 |
-
st.markdown('<div class="section-header">🚀 Nilsen Production Intelligence Platform</div>', unsafe_allow_html=True)
|
| 917 |
-
|
| 918 |
col1, col2 = st.columns(2)
|
| 919 |
-
|
| 920 |
with col1:
|
| 921 |
st.markdown("""
|
| 922 |
-
###
|
| 923 |
-
|
| 924 |
-
|
| 925 |
-
|
| 926 |
-
|
| 927 |
-
|
| 928 |
-
|
| 929 |
-
|
| 930 |
""")
|
| 931 |
-
|
| 932 |
with col2:
|
| 933 |
st.markdown("""
|
| 934 |
-
###
|
| 935 |
-
-
|
| 936 |
-
-
|
| 937 |
-
-
|
| 938 |
-
-
|
| 939 |
-
-
|
| 940 |
-
-
|
| 941 |
-
-
|
| 942 |
""")
|
| 943 |
-
|
| 944 |
-
st.markdown('<div class="tech-highlight">', unsafe_allow_html=True)
|
| 945 |
-
st.markdown("""
|
| 946 |
-
### 🚀 Get Started with Nilsen Intelligence
|
| 947 |
-
|
| 948 |
-
**Option 1:** Upload your production data using the sidebar
|
| 949 |
-
|
| 950 |
-
**Option 2:** Try our demo with the Quick Demo buttons
|
| 951 |
-
|
| 952 |
-
**What You'll Get:**
|
| 953 |
-
- Advanced AI analysis showcasing production excellence
|
| 954 |
-
- Professional insights highlighting competitive advantages
|
| 955 |
-
- Technical metrics demonstrating operational efficiency
|
| 956 |
-
- Predictive intelligence for optimization opportunities
|
| 957 |
-
- Executive-ready reports with Nilsen professional branding
|
| 958 |
-
""")
|
| 959 |
-
st.markdown('</div>', unsafe_allow_html=True)
|
| 960 |
-
|
| 961 |
-
st.info("🎯 **Ready to discover your production excellence?** Upload data or use Quick Demo to experience Nilsen's advanced analytics capabilities!")
|
| 962 |
|
| 963 |
if __name__ == "__main__":
|
| 964 |
main()
|
|
|
|
| 66 |
opacity: 0.9;
|
| 67 |
margin-top: 0.5rem;
|
| 68 |
}}
|
| 69 |
+
.metric-card {{
|
| 70 |
+
background: white;
|
| 71 |
+
border: 1px solid {DESIGN_SYSTEM['colors']['border']};
|
| 72 |
+
border-radius: 12px;
|
| 73 |
+
padding: 1.5rem;
|
| 74 |
+
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
| 75 |
+
transition: transform 0.2s ease;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
}}
|
| 77 |
.section-header {{
|
| 78 |
{DESIGN_SYSTEM['fonts']['subtitle']}
|
|
|
|
| 112 |
font-weight: 500;
|
| 113 |
transition: all 0.2s ease;
|
| 114 |
}}
|
| 115 |
+
.stDownloadButton > button {{
|
| 116 |
+
background: {DESIGN_SYSTEM['colors']['primary']} !important;
|
| 117 |
+
color: white !important;
|
| 118 |
+
border: none !important;
|
| 119 |
+
border-radius: 8px !important;
|
| 120 |
+
padding: 0.5rem 1rem !important;
|
| 121 |
+
font-weight: 500 !important;
|
| 122 |
+
transition: all 0.2s ease !important;
|
| 123 |
+
}}
|
| 124 |
</style>
|
| 125 |
""", unsafe_allow_html=True)
|
| 126 |
|
|
|
|
| 164 |
for date in weekdays:
|
| 165 |
for material in materials:
|
| 166 |
for shift in shifts:
|
| 167 |
+
base_weight = {
|
| 168 |
+
'steel': 1500,
|
| 169 |
+
'aluminum': 800,
|
| 170 |
+
'plastic': 600,
|
| 171 |
+
'copper': 400
|
| 172 |
+
}[material]
|
| 173 |
weight = base_weight + np.random.normal(0, base_weight * 0.2)
|
| 174 |
weight = max(weight, base_weight * 0.3)
|
| 175 |
data.append({
|
|
|
|
| 214 |
}
|
| 215 |
return stats
|
| 216 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 217 |
def get_chart_theme():
|
| 218 |
return {
|
| 219 |
'layout': {
|
|
|
|
| 230 |
if time_period == 'daily':
|
| 231 |
grouped = df.groupby('date')['weight_kg'].sum().reset_index()
|
| 232 |
fig = px.line(grouped, x='date', y='weight_kg',
|
| 233 |
+
title='Total Production Trend',
|
| 234 |
labels={'weight_kg': 'Weight (kg)', 'date': 'Date'})
|
| 235 |
elif time_period == 'weekly':
|
| 236 |
df_copy = df.copy()
|
|
|
|
| 239 |
grouped = df_copy.groupby(['year', 'week'])['weight_kg'].sum().reset_index()
|
| 240 |
grouped['week_label'] = grouped['year'].astype(str) + '-W' + grouped['week'].astype(str)
|
| 241 |
fig = px.bar(grouped, x='week_label', y='weight_kg',
|
| 242 |
+
title='Total Production Trend (Weekly)',
|
| 243 |
labels={'weight_kg': 'Weight (kg)', 'week_label': 'Week'})
|
| 244 |
else:
|
| 245 |
df_copy = df.copy()
|
|
|
|
| 247 |
grouped = df_copy.groupby('month')['weight_kg'].sum().reset_index()
|
| 248 |
grouped['month'] = grouped['month'].astype(str)
|
| 249 |
fig = px.bar(grouped, x='month', y='weight_kg',
|
| 250 |
+
title='Total Production Trend (Monthly)',
|
| 251 |
labels={'weight_kg': 'Weight (kg)', 'month': 'Month'})
|
| 252 |
fig.update_layout(**get_chart_theme()['layout'], height=400, showlegend=False)
|
| 253 |
return fig
|
|
|
|
| 259 |
if time_period == 'daily':
|
| 260 |
grouped = df_copy.groupby(['date', 'material_type'])['weight_kg'].sum().reset_index()
|
| 261 |
fig = px.line(grouped, x='date', y='weight_kg', color='material_type',
|
| 262 |
+
title='Materials Production Trends',
|
| 263 |
labels={'weight_kg': 'Weight (kg)', 'date': 'Date', 'material_type': 'Material'})
|
| 264 |
elif time_period == 'weekly':
|
| 265 |
df_copy['week'] = df_copy['date'].dt.isocalendar().week
|
|
|
|
| 267 |
grouped = df_copy.groupby(['year', 'week', 'material_type'])['weight_kg'].sum().reset_index()
|
| 268 |
grouped['week_label'] = grouped['year'].astype(str) + '-W' + grouped['week'].astype(str)
|
| 269 |
fig = px.bar(grouped, x='week_label', y='weight_kg', color='material_type',
|
| 270 |
+
title='Materials Production Trends (Weekly)',
|
| 271 |
labels={'weight_kg': 'Weight (kg)', 'week_label': 'Week', 'material_type': 'Material'})
|
| 272 |
else:
|
| 273 |
df_copy['month'] = df_copy['date'].dt.to_period('M')
|
| 274 |
grouped = df_copy.groupby(['month', 'material_type'])['weight_kg'].sum().reset_index()
|
| 275 |
grouped['month'] = grouped['month'].astype(str)
|
| 276 |
fig = px.bar(grouped, x='month', y='weight_kg', color='material_type',
|
| 277 |
+
title='Materials Production Trends (Monthly)',
|
| 278 |
labels={'weight_kg': 'Weight (kg)', 'month': 'Month', 'material_type': 'Material'})
|
| 279 |
fig.update_layout(**get_chart_theme()['layout'], height=400)
|
| 280 |
return fig
|
|
|
|
| 286 |
fig = go.Figure()
|
| 287 |
if 'day' in pivot_data.columns:
|
| 288 |
fig.add_trace(go.Bar(
|
| 289 |
+
x=pivot_data.index, y=pivot_data['day'], name='Day Shift',
|
| 290 |
marker_color=DESIGN_SYSTEM['colors']['warning'],
|
| 291 |
text=pivot_data['day'].round(0), textposition='inside'
|
| 292 |
))
|
| 293 |
if 'night' in pivot_data.columns:
|
| 294 |
fig.add_trace(go.Bar(
|
| 295 |
+
x=pivot_data.index, y=pivot_data['night'], name='Night Shift',
|
| 296 |
marker_color=DESIGN_SYSTEM['colors']['primary'],
|
| 297 |
base=pivot_data['day'] if 'day' in pivot_data.columns else 0,
|
| 298 |
text=pivot_data['night'].round(0), textposition='inside'
|
| 299 |
))
|
| 300 |
fig.update_layout(
|
| 301 |
**get_chart_theme()['layout'],
|
| 302 |
+
title='Daily Shift Production Trends (Stacked)',
|
| 303 |
xaxis_title='Date', yaxis_title='Weight (kg)',
|
| 304 |
barmode='stack', height=400, showlegend=True
|
| 305 |
)
|
| 306 |
else:
|
| 307 |
grouped = df.groupby(['date', 'shift'])['weight_kg'].sum().reset_index()
|
| 308 |
fig = px.bar(grouped, x='date', y='weight_kg', color='shift',
|
| 309 |
+
title=f'{time_period.title()} Shift Production Trends',
|
| 310 |
barmode='stack')
|
| 311 |
fig.update_layout(**get_chart_theme()['layout'], height=400)
|
| 312 |
return fig
|
| 313 |
|
| 314 |
+
def detect_outliers(df):
|
| 315 |
+
outliers = {}
|
| 316 |
+
for material in df['material_type'].unique():
|
| 317 |
+
material_data = df[df['material_type'] == material]
|
| 318 |
+
data = material_data['weight_kg']
|
| 319 |
+
Q1, Q3 = data.quantile(0.25), data.quantile(0.75)
|
| 320 |
+
IQR = Q3 - Q1
|
| 321 |
+
lower, upper = Q1 - 1.5 * IQR, Q3 + 1.5 * IQR
|
| 322 |
+
outlier_mask = (data < lower) | (data > upper)
|
| 323 |
+
outlier_dates = material_data[outlier_mask]['date'].dt.strftime('%Y-%m-%d').tolist()
|
| 324 |
+
outliers[material] = {
|
| 325 |
+
'count': len(outlier_dates),
|
| 326 |
+
'range': f"{lower:.0f} - {upper:.0f} kg",
|
| 327 |
+
'dates': outlier_dates
|
| 328 |
+
}
|
| 329 |
+
return outliers
|
| 330 |
+
|
| 331 |
+
def generate_ai_summary(model, df, stats, outliers):
|
| 332 |
+
if not model:
|
| 333 |
+
return "AI analysis unavailable - API key not configured"
|
| 334 |
+
try:
|
| 335 |
+
materials = [k for k in stats.keys() if k != '_total_']
|
| 336 |
+
context_parts = [
|
| 337 |
+
"# Production Data Analysis Context",
|
| 338 |
+
f"## Overview",
|
| 339 |
+
f"- Total Production: {stats['_total_']['total']:,.0f} kg",
|
| 340 |
+
f"- Production Period: {stats['_total_']['work_days']} working days",
|
| 341 |
+
f"- Daily Average: {stats['_total_']['daily_avg']:,.0f} kg",
|
| 342 |
+
f"- Materials Tracked: {len(materials)}",
|
| 343 |
+
"",
|
| 344 |
+
"## Material Breakdown:"
|
| 345 |
+
]
|
| 346 |
+
for material in materials:
|
| 347 |
+
info = stats[material]
|
| 348 |
+
context_parts.append(f"- {material.title()}: {info['total']:,.0f} kg ({info['percentage']:.1f}%), avg {info['daily_avg']:,.0f} kg/day")
|
| 349 |
+
daily_data = df.groupby('date')['weight_kg'].sum()
|
| 350 |
+
trend_direction = "increasing" if daily_data.iloc[-1] > daily_data.iloc[0] else "decreasing"
|
| 351 |
+
volatility = daily_data.std() / daily_data.mean() * 100
|
| 352 |
+
context_parts.extend([
|
| 353 |
+
"",
|
| 354 |
+
"## Trend Analysis:",
|
| 355 |
+
f"- Overall trend: {trend_direction}",
|
| 356 |
+
f"- Production volatility: {volatility:.1f}% coefficient of variation",
|
| 357 |
+
f"- Peak production: {daily_data.max():,.0f} kg",
|
| 358 |
+
f"- Lowest production: {daily_data.min():,.0f} kg"
|
| 359 |
+
])
|
| 360 |
+
total_outliers = sum(info['count'] for info in outliers.values())
|
| 361 |
+
context_parts.extend([
|
| 362 |
+
"",
|
| 363 |
+
"## Quality Control:",
|
| 364 |
+
f"- Total outliers detected: {total_outliers}",
|
| 365 |
+
f"- Materials with quality issues: {sum(1 for info in outliers.values() if info['count'] > 0)}"
|
| 366 |
+
])
|
| 367 |
+
if 'shift' in df.columns:
|
| 368 |
+
shift_stats = df.groupby('shift')['weight_kg'].sum()
|
| 369 |
+
context_parts.extend([
|
| 370 |
+
"",
|
| 371 |
+
"## Shift Performance:",
|
| 372 |
+
f"- Day shift: {shift_stats.get('day', 0):,.0f} kg",
|
| 373 |
+
f"- Night shift: {shift_stats.get('night', 0):,.0f} kg"
|
| 374 |
+
])
|
| 375 |
+
context_text = "\n".join(context_parts)
|
| 376 |
+
prompt = f"""
|
| 377 |
+
{context_text}
|
| 378 |
+
|
| 379 |
+
As an expert AI analyst embedded within the "Production Monitor with AI Insights" platform, provide a comprehensive analysis based on the data provided. Your tone should be professional and data-driven. Your primary goal is to highlight how the platform's features reveal critical insights.
|
| 380 |
+
|
| 381 |
+
Structure your response in the following format:
|
| 382 |
+
|
| 383 |
+
**PRODUCTION ASSESSMENT**
|
| 384 |
+
Evaluate the overall production status (Excellent/Good/Needs Attention). Briefly justify your assessment using key metrics from the data summary.
|
| 385 |
+
|
| 386 |
+
**KEY FINDINGS**
|
| 387 |
+
Identify 3-4 of the most important insights. For each finding, explicitly mention the platform feature that made the discovery possible. Use formats like "(revealed by the 'Quality Check' module)" or "(visualized in the 'Production Trend' chart)".
|
| 388 |
+
|
| 389 |
+
Example Finding format:
|
| 390 |
+
• Finding X: [Your insight, e.g., "Liquid-Ctu production shows high volatility..."] (as identified by the 'Materials Analysis' view).
|
| 391 |
+
|
| 392 |
+
**RECOMMENDATIONS**
|
| 393 |
+
Provide 2-3 actionable recommendations. Frame these as steps the management can take, encouraging them to use the platform for further investigation.
|
| 394 |
+
|
| 395 |
+
Example Recommendation format:
|
| 396 |
+
• Recommendation Y: [Your recommendation, e.g., "Investigate the root causes of the 11 outliers..."] We recommend using the platform's interactive charts to drill down into the specific dates identified by the 'Quality Check' module.
|
| 397 |
+
|
| 398 |
+
Keep the entire analysis concise and under 300 words.
|
| 399 |
+
"""
|
| 400 |
+
response = model.generate_content(prompt)
|
| 401 |
+
return response.text
|
| 402 |
+
except Exception as e:
|
| 403 |
+
return f"AI analysis error: {str(e)}"
|
| 404 |
+
|
| 405 |
+
def query_ai(model, stats, question, df=None):
|
| 406 |
+
if not model:
|
| 407 |
+
return "AI assistant not available"
|
| 408 |
+
context_parts = [
|
| 409 |
+
"Production Data Summary:",
|
| 410 |
+
*[f"- {mat.title()}: {info['total']:,.0f}kg ({info['percentage']:.1f}%)"
|
| 411 |
+
for mat, info in stats.items() if mat != '_total_'],
|
| 412 |
+
f"\nTotal Production: {stats['_total_']['total']:,.0f}kg across {stats['_total_']['work_days']} work days"
|
| 413 |
+
]
|
| 414 |
+
if df is not None:
|
| 415 |
+
available_cols = list(df.columns)
|
| 416 |
+
context_parts.append(f"\nAvailable data fields: {', '.join(available_cols)}")
|
| 417 |
+
if 'shift' in df.columns:
|
| 418 |
+
shift_stats = df.groupby('shift')['weight_kg'].sum()
|
| 419 |
+
context_parts.append(f"Shift breakdown: {dict(shift_stats)}")
|
| 420 |
+
if 'day_name' in df.columns:
|
| 421 |
+
day_stats = df.groupby('day_name')['weight_kg'].mean()
|
| 422 |
+
context_parts.append(f"Average daily production: {dict(day_stats.round(0))}")
|
| 423 |
+
context = "\n".join(context_parts) + f"\n\nQuestion: {question}\nAnswer based on available data:"
|
| 424 |
+
try:
|
| 425 |
+
response = model.generate_content(context)
|
| 426 |
+
return response.text
|
| 427 |
+
except:
|
| 428 |
+
return "Error getting AI response"
|
| 429 |
+
|
| 430 |
+
def save_plotly_as_image(fig, filename):
|
| 431 |
+
try:
|
| 432 |
+
temp_dir = tempfile.gettempdir()
|
| 433 |
+
filepath = os.path.join(temp_dir, filename)
|
| 434 |
+
theme = get_chart_theme()['layout'].copy()
|
| 435 |
+
theme.update({
|
| 436 |
+
'font': dict(size=12, family="Arial"),
|
| 437 |
+
'plot_bgcolor': 'white',
|
| 438 |
+
'paper_bgcolor': 'white',
|
| 439 |
+
'margin': dict(t=50, b=40, l=40, r=40)
|
| 440 |
+
})
|
| 441 |
+
fig.update_layout(**theme)
|
| 442 |
+
try:
|
| 443 |
+
pio.write_image(fig, filepath, format='png', width=800, height=400, scale=2, engine='kaleido')
|
| 444 |
+
if os.path.exists(filepath):
|
| 445 |
+
return filepath
|
| 446 |
+
except:
|
| 447 |
+
pass
|
| 448 |
+
return None
|
| 449 |
+
except Exception as e:
|
| 450 |
+
return None
|
| 451 |
+
|
| 452 |
+
def create_pdf_charts(df, stats):
|
| 453 |
+
charts = {}
|
| 454 |
+
try:
|
| 455 |
+
materials = [k for k in stats.keys() if k != '_total_']
|
| 456 |
+
values = [stats[mat]['total'] for mat in materials]
|
| 457 |
+
labels = [mat.replace('_', ' ').title() for mat in materials]
|
| 458 |
+
if len(materials) > 0 and len(values) > 0:
|
| 459 |
+
try:
|
| 460 |
+
fig_pie = px.pie(values=values, names=labels, title="Production Distribution by Material")
|
| 461 |
+
charts['pie'] = save_plotly_as_image(fig_pie, "distribution.png")
|
| 462 |
+
except:
|
| 463 |
+
pass
|
| 464 |
+
if len(df) > 0:
|
| 465 |
+
try:
|
| 466 |
+
daily_data = df.groupby('date')['weight_kg'].sum().reset_index()
|
| 467 |
+
if len(daily_data) > 0:
|
| 468 |
+
fig_trend = px.line(daily_data, x='date', y='weight_kg', title="Daily Production Trend",
|
| 469 |
+
labels={'date': 'Date', 'weight_kg': 'Weight (kg)'},
|
| 470 |
+
color_discrete_sequence=[DESIGN_SYSTEM['colors']['primary']])
|
| 471 |
+
charts['trend'] = save_plotly_as_image(fig_trend, "trend.png")
|
| 472 |
+
except:
|
| 473 |
+
pass
|
| 474 |
+
if len(materials) > 0 and len(values) > 0:
|
| 475 |
+
try:
|
| 476 |
+
fig_bar = px.bar(x=labels, y=values, title="Production by Material Type",
|
| 477 |
+
labels={'x': 'Material Type', 'y': 'Weight (kg)'},
|
| 478 |
+
color_discrete_sequence=[DESIGN_SYSTEM['colors']['primary']])
|
| 479 |
+
charts['bar'] = save_plotly_as_image(fig_bar, "materials.png")
|
| 480 |
+
except:
|
| 481 |
+
pass
|
| 482 |
+
if 'shift' in df.columns and len(df) > 0:
|
| 483 |
+
try:
|
| 484 |
+
shift_data = df.groupby('shift')['weight_kg'].sum().reset_index()
|
| 485 |
+
if len(shift_data) > 0 and shift_data['weight_kg'].sum() > 0:
|
| 486 |
+
fig_shift = px.pie(shift_data, values='weight_kg', names='shift', title="Production by Shift")
|
| 487 |
+
charts['shift'] = save_plotly_as_image(fig_shift, "shifts.png")
|
| 488 |
+
except:
|
| 489 |
+
pass
|
| 490 |
+
except Exception as e:
|
| 491 |
+
pass
|
| 492 |
+
return charts
|
| 493 |
+
|
| 494 |
def create_enhanced_pdf_report(df, stats, outliers, model=None):
|
| 495 |
buffer = io.BytesIO()
|
| 496 |
doc = SimpleDocTemplate(buffer, pagesize=A4, rightMargin=50, leftMargin=50, topMargin=50, bottomMargin=50)
|
| 497 |
elements = []
|
| 498 |
styles = getSampleStyleSheet()
|
| 499 |
+
title_style = ParagraphStyle(
|
| 500 |
+
'CustomTitle',
|
| 501 |
+
parent=styles['Heading1'],
|
| 502 |
+
fontSize=24,
|
| 503 |
+
spaceAfter=30,
|
| 504 |
+
alignment=1,
|
| 505 |
+
textColor=colors.darkblue
|
| 506 |
+
)
|
| 507 |
+
subtitle_style = ParagraphStyle(
|
| 508 |
+
'CustomSubtitle',
|
| 509 |
+
parent=styles['Heading2'],
|
| 510 |
+
fontSize=16,
|
| 511 |
+
spaceAfter=20,
|
| 512 |
+
textColor=colors.darkblue
|
| 513 |
+
)
|
| 514 |
+
ai_style = ParagraphStyle(
|
| 515 |
+
'AIStyle',
|
| 516 |
+
parent=styles['Normal'],
|
| 517 |
+
fontSize=11,
|
| 518 |
+
spaceAfter=12,
|
| 519 |
+
leftIndent=20,
|
| 520 |
+
textColor=colors.darkgreen
|
| 521 |
+
)
|
| 522 |
elements.append(Spacer(1, 100))
|
| 523 |
+
elements.append(Paragraph("Production Monitor with AI Insights", title_style))
|
| 524 |
+
elements.append(Paragraph("Comprehensive Production Analysis Report", styles['Heading3']))
|
| 525 |
elements.append(Spacer(1, 50))
|
|
|
|
| 526 |
report_info = f"""
|
| 527 |
<para alignment="center">
|
| 528 |
<b>Nilsen Service & Consulting AS</b><br/>
|
| 529 |
+
Production Analytics Division<br/><br/>
|
| 530 |
+
<b>Report Period:</b> {df['date'].min().strftime('%B %d, %Y')} - {df['date'].max().strftime('%B %d, %Y')}<br/>
|
|
|
|
| 531 |
<b>Generated:</b> {datetime.now().strftime('%B %d, %Y at %H:%M')}<br/>
|
| 532 |
+
<b>Total Records:</b> {len(df):,}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 533 |
</para>
|
| 534 |
"""
|
| 535 |
elements.append(Paragraph(report_info, styles['Normal']))
|
| 536 |
elements.append(PageBreak())
|
|
|
|
|
|
|
| 537 |
elements.append(Paragraph("Executive Summary", subtitle_style))
|
| 538 |
total_production = stats['_total_']['total']
|
| 539 |
work_days = stats['_total_']['work_days']
|
| 540 |
daily_avg = stats['_total_']['daily_avg']
|
|
|
|
| 541 |
exec_summary = f"""
|
| 542 |
<para>
|
| 543 |
+
This report analyzes production data spanning <b>{work_days} working days</b>.
|
| 544 |
+
Total output achieved: <b>{total_production:,.0f} kg</b> with an average
|
| 545 |
+
daily production of <b>{daily_avg:,.0f} kg</b>.
|
| 546 |
<br/><br/>
|
| 547 |
+
<b>Key Highlights:</b><br/>
|
| 548 |
+
• Total production: {total_production:,.0f} kg<br/>
|
| 549 |
+
• Daily average: {daily_avg:,.0f} kg<br/>
|
| 550 |
+
• Materials tracked: {len([k for k in stats.keys() if k != '_total_'])}<br/>
|
| 551 |
+
• Data quality: {len(df):,} records processed
|
| 552 |
</para>
|
| 553 |
"""
|
| 554 |
elements.append(Paragraph(exec_summary, styles['Normal']))
|
| 555 |
elements.append(Spacer(1, 20))
|
| 556 |
+
elements.append(Paragraph("Production Summary", styles['Heading3']))
|
| 557 |
+
summary_data = [['Material Type', 'Total (kg)', 'Share (%)', 'Daily Avg (kg)']]
|
|
|
|
|
|
|
| 558 |
for material, info in stats.items():
|
| 559 |
if material != '_total_':
|
| 560 |
summary_data.append([
|
|
|
|
| 563 |
f"{info['percentage']:.1f}%",
|
| 564 |
f"{info['daily_avg']:,.0f}"
|
| 565 |
])
|
|
|
|
| 566 |
summary_table = Table(summary_data, colWidths=[2*inch, 1.5*inch, 1*inch, 1.5*inch])
|
| 567 |
summary_table.setStyle(TableStyle([
|
| 568 |
('BACKGROUND', (0, 0), (-1, 0), colors.darkblue),
|
|
|
|
| 574 |
]))
|
| 575 |
elements.append(summary_table)
|
| 576 |
elements.append(PageBreak())
|
| 577 |
+
elements.append(Paragraph("Production Analysis Charts", subtitle_style))
|
| 578 |
+
try:
|
| 579 |
+
charts = create_pdf_charts(df, stats)
|
| 580 |
+
except:
|
| 581 |
+
charts = {}
|
| 582 |
+
charts_added = False
|
| 583 |
+
chart_insights = {
|
| 584 |
+
'pie': "Material distribution shows production allocation across different materials. Balanced distribution indicates diversified production capabilities.",
|
| 585 |
+
'trend': "Production trend reveals operational patterns and seasonal variations. Consistent trends suggest stable operational efficiency.",
|
| 586 |
+
'bar': "Material comparison highlights performance differences and production capacities. Top performers indicate optimization opportunities.",
|
| 587 |
+
'shift': "Shift analysis reveals operational efficiency differences between day and night operations. Balance indicates effective resource utilization."
|
| 588 |
+
}
|
| 589 |
+
for chart_type, chart_title in [
|
| 590 |
+
('pie', "Production Distribution"),
|
| 591 |
+
('trend', "Production Trend"),
|
| 592 |
+
('bar', "Material Comparison"),
|
| 593 |
+
('shift', "Shift Analysis")
|
| 594 |
+
]:
|
| 595 |
+
chart_path = charts.get(chart_type)
|
| 596 |
+
if chart_path and os.path.exists(chart_path):
|
| 597 |
+
try:
|
| 598 |
+
elements.append(Paragraph(chart_title, styles['Heading3']))
|
| 599 |
+
elements.append(Image(chart_path, width=6*inch, height=3*inch))
|
| 600 |
+
insight_text = f"<i>Analysis: {chart_insights.get(chart_type, 'Chart analysis not available.')}</i>"
|
| 601 |
+
elements.append(Paragraph(insight_text, ai_style))
|
| 602 |
+
elements.append(Spacer(1, 20))
|
| 603 |
+
charts_added = True
|
| 604 |
+
except Exception as e:
|
| 605 |
+
pass
|
| 606 |
+
if not charts_added:
|
| 607 |
+
elements.append(Paragraph("Charts Generation Failed", styles['Heading3']))
|
| 608 |
+
elements.append(Paragraph("Production Data Summary:", styles['Normal']))
|
| 609 |
+
for material, info in stats.items():
|
| 610 |
+
if material != '_total_':
|
| 611 |
+
summary_text = f"• {material.replace('_', ' ').title()}: {info['total']:,.0f} kg ({info['percentage']:.1f}%)"
|
| 612 |
+
elements.append(Paragraph(summary_text, styles['Normal']))
|
| 613 |
+
elements.append(Spacer(1, 20))
|
| 614 |
+
elements.append(PageBreak())
|
| 615 |
+
elements.append(Paragraph("Quality Control Analysis", subtitle_style))
|
| 616 |
+
quality_data = [['Material', 'Outliers', 'Normal Range (kg)', 'Status']]
|
| 617 |
for material, info in outliers.items():
|
| 618 |
if info['count'] == 0:
|
| 619 |
+
status = "GOOD"
|
| 620 |
elif info['count'] <= 3:
|
| 621 |
+
status = "MONITOR"
|
| 622 |
else:
|
| 623 |
+
status = "ATTENTION"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 624 |
quality_data.append([
|
| 625 |
material.replace('_', ' ').title(),
|
| 626 |
+
str(info['count']),
|
| 627 |
info['range'],
|
| 628 |
status
|
| 629 |
])
|
|
|
|
| 630 |
quality_table = Table(quality_data, colWidths=[2*inch, 1*inch, 2*inch, 1.5*inch])
|
| 631 |
quality_table.setStyle(TableStyle([
|
| 632 |
+
('BACKGROUND', (0, 0), (-1, 0), colors.darkred),
|
| 633 |
('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
|
| 634 |
('ALIGN', (0, 0), (-1, -1), 'CENTER'),
|
| 635 |
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
|
|
|
|
| 637 |
('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.white, colors.lightgrey])
|
| 638 |
]))
|
| 639 |
elements.append(quality_table)
|
|
|
|
|
|
|
| 640 |
if model:
|
| 641 |
elements.append(PageBreak())
|
| 642 |
+
elements.append(Paragraph("AI Intelligent Analysis", subtitle_style))
|
| 643 |
try:
|
| 644 |
+
ai_analysis = generate_ai_summary(model, df, stats, outliers)
|
| 645 |
except:
|
| 646 |
+
ai_analysis = "AI analysis temporarily unavailable."
|
|
|
|
| 647 |
ai_paragraphs = ai_analysis.split('\n\n')
|
| 648 |
for paragraph in ai_paragraphs:
|
| 649 |
if paragraph.strip():
|
| 650 |
+
formatted_text = paragraph.replace('**', '<b>', 1).replace('**', '</b>', 1) \
|
| 651 |
+
.replace('•', ' •') \
|
| 652 |
+
.replace('\n', '<br/>')
|
| 653 |
elements.append(Paragraph(formatted_text, styles['Normal']))
|
| 654 |
elements.append(Spacer(1, 8))
|
| 655 |
+
else:
|
| 656 |
+
elements.append(PageBreak())
|
| 657 |
+
elements.append(Paragraph("AI Analysis", subtitle_style))
|
| 658 |
+
elements.append(Paragraph("AI analysis unavailable - API key not configured. Please configure Google AI API key to enable intelligent insights.", styles['Normal']))
|
| 659 |
elements.append(Spacer(1, 30))
|
| 660 |
footer_text = f"""
|
| 661 |
<para alignment="center">
|
| 662 |
+
<i>This report was generated by Production Monitor System<br/>
|
| 663 |
+
Nilsen Service & Consulting AS - Production Analytics Division<br/>
|
| 664 |
+
Report contains {len(df):,} data records across {stats['_total_']['work_days']} working days</i>
|
| 665 |
</para>
|
| 666 |
"""
|
| 667 |
elements.append(Paragraph(footer_text, styles['Normal']))
|
|
|
|
| 668 |
doc.build(elements)
|
| 669 |
buffer.seek(0)
|
| 670 |
return buffer
|
| 671 |
|
| 672 |
+
def create_csv_export(df, stats):
|
| 673 |
+
summary_df = pd.DataFrame([
|
| 674 |
+
{
|
| 675 |
+
'Material': material.replace('_', ' ').title(),
|
| 676 |
+
'Total_kg': info['total'],
|
| 677 |
+
'Percentage': info['percentage'],
|
| 678 |
+
'Daily_Average_kg': info['daily_avg'],
|
| 679 |
+
'Work_Days': info['work_days'],
|
| 680 |
+
'Records_Count': info['records']
|
| 681 |
+
}
|
| 682 |
+
for material, info in stats.items() if material != '_total_'
|
| 683 |
+
])
|
| 684 |
+
return summary_df
|
| 685 |
+
|
| 686 |
+
def add_export_section(df, stats, outliers, model):
|
| 687 |
+
st.markdown('<div class="section-header">📄 Export Reports</div>', unsafe_allow_html=True)
|
| 688 |
+
if 'export_ready' not in st.session_state:
|
| 689 |
+
st.session_state.export_ready = False
|
| 690 |
+
if 'pdf_buffer' not in st.session_state:
|
| 691 |
+
st.session_state.pdf_buffer = None
|
| 692 |
+
if 'csv_data' not in st.session_state:
|
| 693 |
+
st.session_state.csv_data = None
|
| 694 |
+
col1, col2, col3 = st.columns(3)
|
| 695 |
+
with col1:
|
| 696 |
+
if st.button("Generate PDF Report with AI", key="generate_pdf_btn", type="primary"):
|
| 697 |
+
try:
|
| 698 |
+
with st.spinner("Generating PDF with AI analysis..."):
|
| 699 |
+
st.session_state.pdf_buffer = create_enhanced_pdf_report(df, stats, outliers, model)
|
| 700 |
+
st.session_state.export_ready = True
|
| 701 |
+
st.success("✅ PDF report with AI analysis generated successfully!")
|
| 702 |
+
except Exception as e:
|
| 703 |
+
st.error(f"❌ PDF generation failed: {str(e)}")
|
| 704 |
+
st.session_state.export_ready = False
|
| 705 |
+
if st.session_state.export_ready and st.session_state.pdf_buffer:
|
| 706 |
+
st.download_button(
|
| 707 |
+
label="💾 Download PDF Report",
|
| 708 |
+
data=st.session_state.pdf_buffer,
|
| 709 |
+
file_name=f"production_report_ai_{datetime.now().strftime('%Y%m%d_%H%M')}.pdf",
|
| 710 |
+
mime="application/pdf",
|
| 711 |
+
key="download_pdf_btn"
|
| 712 |
+
)
|
| 713 |
+
with col2:
|
| 714 |
+
if st.button("Generate CSV Summary", key="generate_csv_btn", type="primary"):
|
| 715 |
+
try:
|
| 716 |
+
st.session_state.csv_data = create_csv_export(df, stats)
|
| 717 |
+
st.success("✅ CSV summary generated successfully!")
|
| 718 |
+
except Exception as e:
|
| 719 |
+
st.error(f"❌ CSV generation failed: {str(e)}")
|
| 720 |
+
if st.session_state.csv_data is not None:
|
| 721 |
+
csv_string = st.session_state.csv_data.to_csv(index=False)
|
| 722 |
+
st.download_button(
|
| 723 |
+
label="💾 Download CSV Summary",
|
| 724 |
+
data=csv_string,
|
| 725 |
+
file_name=f"production_summary_{datetime.now().strftime('%Y%m%d_%H%M')}.csv",
|
| 726 |
+
mime="text/csv",
|
| 727 |
+
key="download_csv_btn"
|
| 728 |
+
)
|
| 729 |
+
with col3:
|
| 730 |
+
csv_string = df.to_csv(index=False)
|
| 731 |
+
st.download_button(
|
| 732 |
+
label="Download Raw Data",
|
| 733 |
+
data=csv_string,
|
| 734 |
+
file_name=f"raw_production_data_{datetime.now().strftime('%Y%m%d_%H%M')}.csv",
|
| 735 |
+
mime="text/csv",
|
| 736 |
+
key="download_raw_btn"
|
| 737 |
+
)
|
| 738 |
def main():
|
| 739 |
load_css()
|
|
|
|
|
|
|
| 740 |
st.markdown("""
|
| 741 |
<div class="main-header">
|
| 742 |
+
<div class="main-title">🏭 Production Monitor with AI Insights</div>
|
| 743 |
+
<div class="main-subtitle">Nilsen Service & Consulting AS | Real-time Production Analytics & Recommendations</div>
|
| 744 |
</div>
|
| 745 |
""", unsafe_allow_html=True)
|
|
|
|
| 746 |
model = init_ai()
|
|
|
|
|
|
|
| 747 |
if 'current_df' not in st.session_state:
|
| 748 |
st.session_state.current_df = None
|
| 749 |
if 'current_stats' not in st.session_state:
|
| 750 |
st.session_state.current_stats = None
|
|
|
|
|
|
|
| 751 |
with st.sidebar:
|
|
|
|
|
|
|
|
|
|
| 752 |
st.markdown("### 📊 Data Source")
|
| 753 |
uploaded_file = st.file_uploader("Upload Production Data", type=['csv'])
|
|
|
|
| 754 |
st.markdown("---")
|
| 755 |
+
st.markdown("### 📊 Quick Load")
|
| 756 |
col1, col2 = st.columns(2)
|
| 757 |
with col1:
|
| 758 |
+
if st.button("📊 2024 Data", type="primary", key="load_2024"):
|
| 759 |
st.session_state.load_preset = "2024"
|
| 760 |
with col2:
|
| 761 |
+
if st.button("📊 2025 Data", type="primary", key="load_2025"):
|
| 762 |
st.session_state.load_preset = "2025"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 763 |
st.markdown("---")
|
| 764 |
st.markdown("""
|
| 765 |
+
**Expected TSV format:**
|
| 766 |
- `date`: MM/DD/YYYY
|
| 767 |
+
- `weight_kg`: Production weight
|
| 768 |
- `material_type`: Material category
|
| 769 |
- `shift`: day/night (optional)
|
| 770 |
""")
|
| 771 |
+
if model:
|
| 772 |
+
st.success("🤖 AI Assistant Ready")
|
| 773 |
+
else:
|
| 774 |
+
st.warning("⚠️ AI Assistant Unavailable")
|
| 775 |
df = st.session_state.current_df
|
| 776 |
stats = st.session_state.current_stats
|
|
|
|
|
|
|
| 777 |
if uploaded_file:
|
| 778 |
try:
|
| 779 |
df = load_data(uploaded_file)
|
| 780 |
stats = get_material_stats(df)
|
| 781 |
st.session_state.current_df = df
|
| 782 |
st.session_state.current_stats = stats
|
| 783 |
+
st.success("✅ Data uploaded successfully!")
|
| 784 |
except Exception as e:
|
| 785 |
+
st.error(f"❌ Error loading uploaded file: {str(e)}")
|
|
|
|
| 786 |
elif 'load_preset' in st.session_state:
|
| 787 |
year = st.session_state.load_preset
|
| 788 |
try:
|
| 789 |
+
with st.spinner(f"Loading {year} data..."):
|
| 790 |
df = load_preset_data(year)
|
| 791 |
if df is not None:
|
| 792 |
stats = get_material_stats(df)
|
| 793 |
st.session_state.current_df = df
|
| 794 |
st.session_state.current_stats = stats
|
| 795 |
+
st.success(f"✅ {year} data loaded successfully!")
|
| 796 |
except Exception as e:
|
| 797 |
st.error(f"❌ Error loading {year} data: {str(e)}")
|
| 798 |
finally:
|
| 799 |
del st.session_state.load_preset
|
|
|
|
|
|
|
| 800 |
if df is not None and stats is not None:
|
| 801 |
+
st.markdown('<div class="section-header">📋 Material Overview</div>', unsafe_allow_html=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 802 |
materials = [k for k in stats.keys() if k != '_total_']
|
| 803 |
+
cols = st.columns(4)
|
|
|
|
| 804 |
for i, material in enumerate(materials[:3]):
|
| 805 |
info = stats[material]
|
| 806 |
with cols[i]:
|
| 807 |
st.metric(
|
| 808 |
+
label=material.replace('_', ' ').title(),
|
| 809 |
value=f"{info['total']:,.0f} kg",
|
| 810 |
+
delta=f"{info['percentage']:.1f}% of total"
|
| 811 |
)
|
| 812 |
+
st.caption(f"Daily avg: {info['daily_avg']:,.0f} kg")
|
| 813 |
+
if len(materials) >= 3:
|
|
|
|
| 814 |
total_info = stats['_total_']
|
| 815 |
+
with cols[3]:
|
| 816 |
st.metric(
|
| 817 |
+
label="Total Production",
|
| 818 |
value=f"{total_info['total']:,.0f} kg",
|
| 819 |
+
delta="100% of total"
|
| 820 |
)
|
| 821 |
+
st.caption(f"Daily avg: {total_info['daily_avg']:,.0f} kg")
|
| 822 |
+
st.markdown('<div class="section-header">📊 Production Trends</div>', unsafe_allow_html=True)
|
|
|
|
|
|
|
| 823 |
col1, col2 = st.columns([3, 1])
|
|
|
|
| 824 |
with col2:
|
| 825 |
+
time_view = st.selectbox("Time Period", ["daily", "weekly", "monthly"], key="time_view_select")
|
|
|
|
| 826 |
with col1:
|
| 827 |
+
with st.container():
|
| 828 |
+
st.markdown('<div class="chart-container">', unsafe_allow_html=True)
|
| 829 |
+
total_chart = create_total_production_chart(df, time_view)
|
| 830 |
+
st.plotly_chart(total_chart, use_container_width=True)
|
| 831 |
+
st.markdown('</div>', unsafe_allow_html=True)
|
| 832 |
+
st.markdown('<div class="section-header">🏷️ Materials Analysis</div>', unsafe_allow_html=True)
|
|
|
|
| 833 |
col1, col2 = st.columns([3, 1])
|
|
|
|
| 834 |
with col2:
|
| 835 |
selected_materials = st.multiselect(
|
| 836 |
"Select Materials",
|
|
|
|
| 838 |
default=materials,
|
| 839 |
key="materials_select"
|
| 840 |
)
|
|
|
|
| 841 |
with col1:
|
| 842 |
if selected_materials:
|
| 843 |
+
with st.container():
|
| 844 |
+
st.markdown('<div class="chart-container">', unsafe_allow_html=True)
|
| 845 |
+
materials_chart = create_materials_trend_chart(df, time_view, selected_materials)
|
| 846 |
+
st.plotly_chart(materials_chart, use_container_width=True)
|
| 847 |
+
st.markdown('</div>', unsafe_allow_html=True)
|
| 848 |
+
if 'shift' in df.columns:
|
| 849 |
+
st.markdown('<div class="section-header">🌓 Shift Analysis</div>', unsafe_allow_html=True)
|
| 850 |
+
with st.container():
|
| 851 |
st.markdown('<div class="chart-container">', unsafe_allow_html=True)
|
| 852 |
+
shift_chart = create_shift_trend_chart(df, time_view)
|
| 853 |
+
st.plotly_chart(shift_chart, use_container_width=True)
|
| 854 |
st.markdown('</div>', unsafe_allow_html=True)
|
| 855 |
+
st.markdown('<div class="section-header">⚠️ Quality Check</div>', unsafe_allow_html=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 856 |
outliers = detect_outliers(df)
|
| 857 |
cols = st.columns(len(outliers))
|
|
|
|
| 858 |
for i, (material, info) in enumerate(outliers.items()):
|
| 859 |
with cols[i]:
|
| 860 |
+
if info['count'] > 0:
|
| 861 |
+
if len(info['dates']) <= 5:
|
| 862 |
+
dates_str = ", ".join(info['dates'])
|
| 863 |
+
else:
|
| 864 |
+
dates_str = f"{', '.join(info['dates'][:3])}, +{len(info['dates'])-3} more"
|
| 865 |
+
st.markdown(f'<div class="alert-warning"><strong>{material.title()}</strong><br>{info["count"]} outliers detected<br>Normal range: {info["range"]}<br><small>Dates: {dates_str}</small></div>', unsafe_allow_html=True)
|
|
|
|
| 866 |
else:
|
| 867 |
+
st.markdown(f'<div class="alert-success"><strong>{material.title()}</strong><br>All values normal</div>', unsafe_allow_html=True)
|
| 868 |
+
add_export_section(df, stats, outliers, model)
|
|
|
|
|
|
|
| 869 |
if model:
|
| 870 |
+
st.markdown('<div class="section-header">🤖 AI Insights</div>', unsafe_allow_html=True)
|
| 871 |
+
quick_questions = [
|
| 872 |
+
"How does production distribution on weekdays compare to weekends?",
|
| 873 |
+
"Which material exhibits the most volatility in our dataset?",
|
| 874 |
+
"To improve stability, which material or shift needs immediate attention?"
|
|
|
|
|
|
|
| 875 |
]
|
| 876 |
+
cols = st.columns(len(quick_questions))
|
| 877 |
+
for i, q in enumerate(quick_questions):
|
|
|
|
|
|
|
|
|
|
| 878 |
with cols[i]:
|
| 879 |
+
if st.button(q, key=f"ai_q_{i}"):
|
| 880 |
+
with st.spinner("Analyzing..."):
|
| 881 |
+
answer = query_ai(model, stats, q, df)
|
| 882 |
+
st.info(answer)
|
| 883 |
+
custom_question = st.text_input("Ask about your production data:",
|
| 884 |
+
placeholder="e.g., 'Compare steel vs aluminum last month'",
|
| 885 |
+
key="custom_ai_question")
|
| 886 |
+
if custom_question and st.button("Ask AI", key="ask_ai_btn"):
|
| 887 |
+
with st.spinner("Analyzing..."):
|
| 888 |
+
answer = query_ai(model, stats, custom_question, df)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 889 |
st.success(f"**Q:** {custom_question}")
|
| 890 |
+
st.write(f"**A:** {answer}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 891 |
else:
|
| 892 |
+
st.markdown('<div class="section-header">📖 How to Use This Platform</div>', unsafe_allow_html=True)
|
|
|
|
|
|
|
| 893 |
col1, col2 = st.columns(2)
|
|
|
|
| 894 |
with col1:
|
| 895 |
st.markdown("""
|
| 896 |
+
### 🚀 Quick Start
|
| 897 |
+
1. Upload your TSV data in the sidebar
|
| 898 |
+
2. Or click Quick Load buttons for preset data
|
| 899 |
+
3. View production by material type
|
| 900 |
+
4. Analyze trends (daily/weekly/monthly)
|
| 901 |
+
5. Check anomalies in Quality Check
|
| 902 |
+
6. Export reports (PDF with AI, CSV)
|
| 903 |
+
7. Ask the AI assistant for insights
|
| 904 |
""")
|
|
|
|
| 905 |
with col2:
|
| 906 |
st.markdown("""
|
| 907 |
+
### 📊 Key Features
|
| 908 |
+
- Real-time interactive charts
|
| 909 |
+
- One-click preset data loading
|
| 910 |
+
- Time-period comparisons
|
| 911 |
+
- Shift performance analysis
|
| 912 |
+
- Outlier detection with dates
|
| 913 |
+
- AI-powered PDF reports
|
| 914 |
+
- Intelligent recommendations
|
| 915 |
""")
|
| 916 |
+
st.info("📁 Ready to start? Upload your production data or use Quick Load buttons to begin analysis!")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 917 |
|
| 918 |
if __name__ == "__main__":
|
| 919 |
main()
|