835 lines
23 KiB
Python
835 lines
23 KiB
Python
|
|
#!/usr/bin/env python3
|
|||
|
|
"""
|
|||
|
|
Create PowerPoint: When the Data Surprised Us
|
|||
|
|
Counterintuitive Findings from the Field - 45 minute internal presentation
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
import os
|
|||
|
|
import sys
|
|||
|
|
|
|||
|
|
from pptx import Presentation
|
|||
|
|
from pptx.util import Inches, Pt
|
|||
|
|
from pptx.dml.color import RGBColor
|
|||
|
|
from pptx.enum.text import PP_ALIGN, MSO_ANCHOR
|
|||
|
|
from pptx.enum.shapes import MSO_SHAPE
|
|||
|
|
|
|||
|
|
# Create presentation
|
|||
|
|
prs = Presentation()
|
|||
|
|
prs.slide_width = Inches(13.333)
|
|||
|
|
prs.slide_height = Inches(7.5)
|
|||
|
|
|
|||
|
|
# Color scheme - Engaging, puzzle-like feel
|
|||
|
|
DEEP_PURPLE = RGBColor(48, 25, 88) # Mystery color
|
|||
|
|
BRIGHT_TEAL = RGBColor(0, 168, 168) # Reveal color
|
|||
|
|
QUESTION_YELLOW = RGBColor(255, 200, 0) # Question accent
|
|||
|
|
DARK_GRAY = RGBColor(51, 51, 51)
|
|||
|
|
MED_GRAY = RGBColor(128, 128, 128)
|
|||
|
|
LIGHT_GRAY = RGBColor(235, 235, 235)
|
|||
|
|
WHITE = RGBColor(255, 255, 255)
|
|||
|
|
CORRECT_GREEN = RGBColor(46, 139, 87) # For good results
|
|||
|
|
WRONG_RED = RGBColor(178, 34, 34) # For bad results
|
|||
|
|
|
|||
|
|
|
|||
|
|
def add_title_slide(title, subtitle=""):
|
|||
|
|
"""Title slide with mystery theme"""
|
|||
|
|
slide = prs.slides.add_slide(prs.slide_layouts[6])
|
|||
|
|
|
|||
|
|
bg = slide.shapes.add_shape(
|
|||
|
|
MSO_SHAPE.RECTANGLE, 0, 0, prs.slide_width, prs.slide_height
|
|||
|
|
)
|
|||
|
|
bg.fill.solid()
|
|||
|
|
bg.fill.fore_color.rgb = DEEP_PURPLE
|
|||
|
|
bg.line.fill.background()
|
|||
|
|
|
|||
|
|
# Question marks decoration
|
|||
|
|
for i, pos in enumerate([(1, 1), (11, 1.5), (2, 5.5), (10, 5)]):
|
|||
|
|
q = slide.shapes.add_textbox(
|
|||
|
|
Inches(pos[0]), Inches(pos[1]), Inches(1.5), Inches(1.5)
|
|||
|
|
)
|
|||
|
|
tf = q.text_frame
|
|||
|
|
p = tf.paragraphs[0]
|
|||
|
|
p.text = "?"
|
|||
|
|
p.font.size = Pt(72)
|
|||
|
|
p.font.bold = True
|
|||
|
|
p.font.color.rgb = RGBColor(80, 50, 120) # Slightly lighter purple
|
|||
|
|
|
|||
|
|
title_box = slide.shapes.add_textbox(
|
|||
|
|
Inches(0.5), Inches(2.5), Inches(12.333), Inches(1.5)
|
|||
|
|
)
|
|||
|
|
tf = title_box.text_frame
|
|||
|
|
p = tf.paragraphs[0]
|
|||
|
|
p.text = title
|
|||
|
|
p.font.size = Pt(52)
|
|||
|
|
p.font.bold = True
|
|||
|
|
p.font.color.rgb = WHITE
|
|||
|
|
p.alignment = PP_ALIGN.CENTER
|
|||
|
|
|
|||
|
|
if subtitle:
|
|||
|
|
sub_box = slide.shapes.add_textbox(
|
|||
|
|
Inches(0.5), Inches(4.3), Inches(12.333), Inches(0.8)
|
|||
|
|
)
|
|||
|
|
tf = sub_box.text_frame
|
|||
|
|
p = tf.paragraphs[0]
|
|||
|
|
p.text = subtitle
|
|||
|
|
p.font.size = Pt(26)
|
|||
|
|
p.font.color.rgb = BRIGHT_TEAL
|
|||
|
|
p.alignment = PP_ALIGN.CENTER
|
|||
|
|
|
|||
|
|
return slide
|
|||
|
|
|
|||
|
|
|
|||
|
|
def add_puzzle_intro_slide(puzzle_num, title):
|
|||
|
|
"""Puzzle section introduction"""
|
|||
|
|
slide = prs.slides.add_slide(prs.slide_layouts[6])
|
|||
|
|
|
|||
|
|
bg = slide.shapes.add_shape(
|
|||
|
|
MSO_SHAPE.RECTANGLE, 0, 0, prs.slide_width, prs.slide_height
|
|||
|
|
)
|
|||
|
|
bg.fill.solid()
|
|||
|
|
bg.fill.fore_color.rgb = DEEP_PURPLE
|
|||
|
|
bg.line.fill.background()
|
|||
|
|
|
|||
|
|
# Puzzle number
|
|||
|
|
num_box = slide.shapes.add_textbox(
|
|||
|
|
Inches(0.5), Inches(2.5), Inches(12.333), Inches(1)
|
|||
|
|
)
|
|||
|
|
tf = num_box.text_frame
|
|||
|
|
p = tf.paragraphs[0]
|
|||
|
|
p.text = f"PUZZLE {puzzle_num}"
|
|||
|
|
p.font.size = Pt(28)
|
|||
|
|
p.font.color.rgb = QUESTION_YELLOW
|
|||
|
|
p.alignment = PP_ALIGN.CENTER
|
|||
|
|
|
|||
|
|
# Title
|
|||
|
|
title_box = slide.shapes.add_textbox(
|
|||
|
|
Inches(0.5), Inches(3.3), Inches(12.333), Inches(1.5)
|
|||
|
|
)
|
|||
|
|
tf = title_box.text_frame
|
|||
|
|
p = tf.paragraphs[0]
|
|||
|
|
p.text = title
|
|||
|
|
p.font.size = Pt(48)
|
|||
|
|
p.font.bold = True
|
|||
|
|
p.font.color.rgb = WHITE
|
|||
|
|
p.alignment = PP_ALIGN.CENTER
|
|||
|
|
|
|||
|
|
return slide
|
|||
|
|
|
|||
|
|
|
|||
|
|
def add_setup_slide(title, content_lines):
|
|||
|
|
"""Setup slide for puzzle - the scenario"""
|
|||
|
|
slide = prs.slides.add_slide(prs.slide_layouts[6])
|
|||
|
|
|
|||
|
|
# Header
|
|||
|
|
header = slide.shapes.add_shape(
|
|||
|
|
MSO_SHAPE.RECTANGLE, 0, 0, prs.slide_width, Inches(1.0)
|
|||
|
|
)
|
|||
|
|
header.fill.solid()
|
|||
|
|
header.fill.fore_color.rgb = DEEP_PURPLE
|
|||
|
|
header.line.fill.background()
|
|||
|
|
|
|||
|
|
title_box = slide.shapes.add_textbox(
|
|||
|
|
Inches(0.5), Inches(0.2), Inches(12), Inches(0.7)
|
|||
|
|
)
|
|||
|
|
tf = title_box.text_frame
|
|||
|
|
p = tf.paragraphs[0]
|
|||
|
|
p.text = title
|
|||
|
|
p.font.size = Pt(32)
|
|||
|
|
p.font.bold = True
|
|||
|
|
p.font.color.rgb = WHITE
|
|||
|
|
|
|||
|
|
# Content
|
|||
|
|
content_box = slide.shapes.add_textbox(
|
|||
|
|
Inches(0.8), Inches(1.4), Inches(11.5), Inches(5.8)
|
|||
|
|
)
|
|||
|
|
tf = content_box.text_frame
|
|||
|
|
tf.word_wrap = True
|
|||
|
|
|
|||
|
|
for i, line in enumerate(content_lines):
|
|||
|
|
if i == 0:
|
|||
|
|
p = tf.paragraphs[0]
|
|||
|
|
else:
|
|||
|
|
p = tf.add_paragraph()
|
|||
|
|
p.text = line
|
|||
|
|
p.font.size = Pt(24)
|
|||
|
|
p.font.color.rgb = DARK_GRAY
|
|||
|
|
p.space_after = Pt(12)
|
|||
|
|
|
|||
|
|
return slide
|
|||
|
|
|
|||
|
|
|
|||
|
|
def add_guess_slide(question):
|
|||
|
|
"""Interactive question slide"""
|
|||
|
|
slide = prs.slides.add_slide(prs.slide_layouts[6])
|
|||
|
|
|
|||
|
|
bg = slide.shapes.add_shape(
|
|||
|
|
MSO_SHAPE.RECTANGLE, 0, 0, prs.slide_width, prs.slide_height
|
|||
|
|
)
|
|||
|
|
bg.fill.solid()
|
|||
|
|
bg.fill.fore_color.rgb = QUESTION_YELLOW
|
|||
|
|
bg.line.fill.background()
|
|||
|
|
|
|||
|
|
q_box = slide.shapes.add_textbox(
|
|||
|
|
Inches(1), Inches(2.5), Inches(11.333), Inches(2.5)
|
|||
|
|
)
|
|||
|
|
tf = q_box.text_frame
|
|||
|
|
tf.word_wrap = True
|
|||
|
|
p = tf.paragraphs[0]
|
|||
|
|
p.text = question
|
|||
|
|
p.font.size = Pt(40)
|
|||
|
|
p.font.bold = True
|
|||
|
|
p.font.color.rgb = DEEP_PURPLE
|
|||
|
|
p.alignment = PP_ALIGN.CENTER
|
|||
|
|
|
|||
|
|
# "What do you think?" prompt
|
|||
|
|
prompt_box = slide.shapes.add_textbox(
|
|||
|
|
Inches(1), Inches(5.5), Inches(11.333), Inches(0.8)
|
|||
|
|
)
|
|||
|
|
tf = prompt_box.text_frame
|
|||
|
|
p = tf.paragraphs[0]
|
|||
|
|
p.text = "Take a guess..."
|
|||
|
|
p.font.size = Pt(24)
|
|||
|
|
p.font.italic = True
|
|||
|
|
p.font.color.rgb = DEEP_PURPLE
|
|||
|
|
p.alignment = PP_ALIGN.CENTER
|
|||
|
|
|
|||
|
|
return slide
|
|||
|
|
|
|||
|
|
|
|||
|
|
def add_reveal_slide(title, reveal_data, is_surprising=True):
|
|||
|
|
"""The big reveal slide"""
|
|||
|
|
slide = prs.slides.add_slide(prs.slide_layouts[6])
|
|||
|
|
|
|||
|
|
# Header - teal for reveal
|
|||
|
|
header = slide.shapes.add_shape(
|
|||
|
|
MSO_SHAPE.RECTANGLE, 0, 0, prs.slide_width, Inches(1.0)
|
|||
|
|
)
|
|||
|
|
header.fill.solid()
|
|||
|
|
header.fill.fore_color.rgb = BRIGHT_TEAL
|
|||
|
|
header.line.fill.background()
|
|||
|
|
|
|||
|
|
title_box = slide.shapes.add_textbox(
|
|||
|
|
Inches(0.5), Inches(0.2), Inches(12), Inches(0.7)
|
|||
|
|
)
|
|||
|
|
tf = title_box.text_frame
|
|||
|
|
p = tf.paragraphs[0]
|
|||
|
|
p.text = "THE REVEAL: " + title
|
|||
|
|
p.font.size = Pt(28)
|
|||
|
|
p.font.bold = True
|
|||
|
|
p.font.color.rgb = WHITE
|
|||
|
|
|
|||
|
|
# Reveal content
|
|||
|
|
reveal_box = slide.shapes.add_textbox(
|
|||
|
|
Inches(1), Inches(1.5), Inches(11.333), Inches(5.5)
|
|||
|
|
)
|
|||
|
|
tf = reveal_box.text_frame
|
|||
|
|
tf.word_wrap = True
|
|||
|
|
|
|||
|
|
for i, line in enumerate(reveal_data):
|
|||
|
|
if i == 0:
|
|||
|
|
p = tf.paragraphs[0]
|
|||
|
|
else:
|
|||
|
|
p = tf.add_paragraph()
|
|||
|
|
p.text = line
|
|||
|
|
p.font.size = Pt(26)
|
|||
|
|
p.font.color.rgb = DARK_GRAY
|
|||
|
|
p.space_after = Pt(16)
|
|||
|
|
|
|||
|
|
return slide
|
|||
|
|
|
|||
|
|
|
|||
|
|
def add_comparison_slide(
|
|||
|
|
title, left_title, left_data, left_result, right_title, right_data, right_result
|
|||
|
|
):
|
|||
|
|
"""Side-by-side comparison with results"""
|
|||
|
|
slide = prs.slides.add_slide(prs.slide_layouts[6])
|
|||
|
|
|
|||
|
|
# Header
|
|||
|
|
header = slide.shapes.add_shape(
|
|||
|
|
MSO_SHAPE.RECTANGLE, 0, 0, prs.slide_width, Inches(1.0)
|
|||
|
|
)
|
|||
|
|
header.fill.solid()
|
|||
|
|
header.fill.fore_color.rgb = BRIGHT_TEAL
|
|||
|
|
header.line.fill.background()
|
|||
|
|
|
|||
|
|
title_box = slide.shapes.add_textbox(
|
|||
|
|
Inches(0.5), Inches(0.2), Inches(12), Inches(0.7)
|
|||
|
|
)
|
|||
|
|
tf = title_box.text_frame
|
|||
|
|
p = tf.paragraphs[0]
|
|||
|
|
p.text = title
|
|||
|
|
p.font.size = Pt(28)
|
|||
|
|
p.font.bold = True
|
|||
|
|
p.font.color.rgb = WHITE
|
|||
|
|
|
|||
|
|
# Left side
|
|||
|
|
left_box = slide.shapes.add_shape(
|
|||
|
|
MSO_SHAPE.ROUNDED_RECTANGLE, Inches(0.5), Inches(1.3), Inches(6), Inches(4.5)
|
|||
|
|
)
|
|||
|
|
left_box.fill.solid()
|
|||
|
|
left_box.fill.fore_color.rgb = LIGHT_GRAY
|
|||
|
|
left_box.line.color.rgb = CORRECT_GREEN if left_result == "GOOD" else WRONG_RED
|
|||
|
|
left_box.line.width = Pt(4)
|
|||
|
|
|
|||
|
|
left_title_box = slide.shapes.add_textbox(
|
|||
|
|
Inches(0.8), Inches(1.5), Inches(5.5), Inches(0.6)
|
|||
|
|
)
|
|||
|
|
tf = left_title_box.text_frame
|
|||
|
|
p = tf.paragraphs[0]
|
|||
|
|
p.text = left_title
|
|||
|
|
p.font.size = Pt(22)
|
|||
|
|
p.font.bold = True
|
|||
|
|
p.font.color.rgb = DARK_GRAY
|
|||
|
|
|
|||
|
|
left_data_box = slide.shapes.add_textbox(
|
|||
|
|
Inches(0.8), Inches(2.2), Inches(5.5), Inches(2.8)
|
|||
|
|
)
|
|||
|
|
tf = left_data_box.text_frame
|
|||
|
|
tf.word_wrap = True
|
|||
|
|
for i, line in enumerate(left_data):
|
|||
|
|
if i == 0:
|
|||
|
|
p = tf.paragraphs[0]
|
|||
|
|
else:
|
|||
|
|
p = tf.add_paragraph()
|
|||
|
|
p.text = line
|
|||
|
|
p.font.size = Pt(18)
|
|||
|
|
p.font.color.rgb = DARK_GRAY
|
|||
|
|
p.space_after = Pt(6)
|
|||
|
|
|
|||
|
|
left_result_box = slide.shapes.add_textbox(
|
|||
|
|
Inches(0.8), Inches(5.2), Inches(5.5), Inches(0.5)
|
|||
|
|
)
|
|||
|
|
tf = left_result_box.text_frame
|
|||
|
|
p = tf.paragraphs[0]
|
|||
|
|
p.text = left_result
|
|||
|
|
p.font.size = Pt(24)
|
|||
|
|
p.font.bold = True
|
|||
|
|
p.font.color.rgb = CORRECT_GREEN if left_result == "GOOD" else WRONG_RED
|
|||
|
|
p.alignment = PP_ALIGN.CENTER
|
|||
|
|
|
|||
|
|
# Right side
|
|||
|
|
right_box = slide.shapes.add_shape(
|
|||
|
|
MSO_SHAPE.ROUNDED_RECTANGLE, Inches(6.833), Inches(1.3), Inches(6), Inches(4.5)
|
|||
|
|
)
|
|||
|
|
right_box.fill.solid()
|
|||
|
|
right_box.fill.fore_color.rgb = LIGHT_GRAY
|
|||
|
|
right_box.line.color.rgb = CORRECT_GREEN if right_result == "GOOD" else WRONG_RED
|
|||
|
|
right_box.line.width = Pt(4)
|
|||
|
|
|
|||
|
|
right_title_box = slide.shapes.add_textbox(
|
|||
|
|
Inches(7.1), Inches(1.5), Inches(5.5), Inches(0.6)
|
|||
|
|
)
|
|||
|
|
tf = right_title_box.text_frame
|
|||
|
|
p = tf.paragraphs[0]
|
|||
|
|
p.text = right_title
|
|||
|
|
p.font.size = Pt(22)
|
|||
|
|
p.font.bold = True
|
|||
|
|
p.font.color.rgb = DARK_GRAY
|
|||
|
|
|
|||
|
|
right_data_box = slide.shapes.add_textbox(
|
|||
|
|
Inches(7.1), Inches(2.2), Inches(5.5), Inches(2.8)
|
|||
|
|
)
|
|||
|
|
tf = right_data_box.text_frame
|
|||
|
|
tf.word_wrap = True
|
|||
|
|
for i, line in enumerate(right_data):
|
|||
|
|
if i == 0:
|
|||
|
|
p = tf.paragraphs[0]
|
|||
|
|
else:
|
|||
|
|
p = tf.add_paragraph()
|
|||
|
|
p.text = line
|
|||
|
|
p.font.size = Pt(18)
|
|||
|
|
p.font.color.rgb = DARK_GRAY
|
|||
|
|
p.space_after = Pt(6)
|
|||
|
|
|
|||
|
|
right_result_box = slide.shapes.add_textbox(
|
|||
|
|
Inches(7.1), Inches(5.2), Inches(5.5), Inches(0.5)
|
|||
|
|
)
|
|||
|
|
tf = right_result_box.text_frame
|
|||
|
|
p = tf.paragraphs[0]
|
|||
|
|
p.text = right_result
|
|||
|
|
p.font.size = Pt(24)
|
|||
|
|
p.font.bold = True
|
|||
|
|
p.font.color.rgb = CORRECT_GREEN if right_result == "GOOD" else WRONG_RED
|
|||
|
|
p.alignment = PP_ALIGN.CENTER
|
|||
|
|
|
|||
|
|
return slide
|
|||
|
|
|
|||
|
|
|
|||
|
|
def add_lesson_slide(lesson_text):
|
|||
|
|
"""Key lesson from the puzzle"""
|
|||
|
|
slide = prs.slides.add_slide(prs.slide_layouts[6])
|
|||
|
|
|
|||
|
|
bg = slide.shapes.add_shape(
|
|||
|
|
MSO_SHAPE.RECTANGLE, 0, 0, prs.slide_width, prs.slide_height
|
|||
|
|
)
|
|||
|
|
bg.fill.solid()
|
|||
|
|
bg.fill.fore_color.rgb = DEEP_PURPLE
|
|||
|
|
bg.line.fill.background()
|
|||
|
|
|
|||
|
|
# "LESSON" label
|
|||
|
|
label_box = slide.shapes.add_textbox(
|
|||
|
|
Inches(0.5), Inches(2.3), Inches(12.333), Inches(0.5)
|
|||
|
|
)
|
|||
|
|
tf = label_box.text_frame
|
|||
|
|
p = tf.paragraphs[0]
|
|||
|
|
p.text = "THE LESSON"
|
|||
|
|
p.font.size = Pt(18)
|
|||
|
|
p.font.color.rgb = BRIGHT_TEAL
|
|||
|
|
p.alignment = PP_ALIGN.CENTER
|
|||
|
|
|
|||
|
|
# Lesson text
|
|||
|
|
lesson_box = slide.shapes.add_textbox(
|
|||
|
|
Inches(1), Inches(3), Inches(11.333), Inches(2.5)
|
|||
|
|
)
|
|||
|
|
tf = lesson_box.text_frame
|
|||
|
|
tf.word_wrap = True
|
|||
|
|
p = tf.paragraphs[0]
|
|||
|
|
p.text = f'"{lesson_text}"'
|
|||
|
|
p.font.size = Pt(36)
|
|||
|
|
p.font.italic = True
|
|||
|
|
p.font.color.rgb = WHITE
|
|||
|
|
p.alignment = PP_ALIGN.CENTER
|
|||
|
|
|
|||
|
|
return slide
|
|||
|
|
|
|||
|
|
|
|||
|
|
def add_content_slide(title, bullets):
|
|||
|
|
"""Standard content slide"""
|
|||
|
|
slide = prs.slides.add_slide(prs.slide_layouts[6])
|
|||
|
|
|
|||
|
|
header = slide.shapes.add_shape(
|
|||
|
|
MSO_SHAPE.RECTANGLE, 0, 0, prs.slide_width, Inches(1.0)
|
|||
|
|
)
|
|||
|
|
header.fill.solid()
|
|||
|
|
header.fill.fore_color.rgb = DEEP_PURPLE
|
|||
|
|
header.line.fill.background()
|
|||
|
|
|
|||
|
|
title_box = slide.shapes.add_textbox(
|
|||
|
|
Inches(0.5), Inches(0.2), Inches(12), Inches(0.7)
|
|||
|
|
)
|
|||
|
|
tf = title_box.text_frame
|
|||
|
|
p = tf.paragraphs[0]
|
|||
|
|
p.text = title
|
|||
|
|
p.font.size = Pt(32)
|
|||
|
|
p.font.bold = True
|
|||
|
|
p.font.color.rgb = WHITE
|
|||
|
|
|
|||
|
|
content_box = slide.shapes.add_textbox(
|
|||
|
|
Inches(0.8), Inches(1.4), Inches(11.5), Inches(5.8)
|
|||
|
|
)
|
|||
|
|
tf = content_box.text_frame
|
|||
|
|
tf.word_wrap = True
|
|||
|
|
|
|||
|
|
for i, bullet in enumerate(bullets):
|
|||
|
|
if i == 0:
|
|||
|
|
p = tf.paragraphs[0]
|
|||
|
|
else:
|
|||
|
|
p = tf.add_paragraph()
|
|||
|
|
p.text = f" {bullet}"
|
|||
|
|
p.font.size = Pt(22)
|
|||
|
|
p.font.color.rgb = DARK_GRAY
|
|||
|
|
p.space_after = Pt(14)
|
|||
|
|
|
|||
|
|
return slide
|
|||
|
|
|
|||
|
|
|
|||
|
|
def add_stat_slide(stat, description):
|
|||
|
|
"""Big statistic reveal"""
|
|||
|
|
slide = prs.slides.add_slide(prs.slide_layouts[6])
|
|||
|
|
|
|||
|
|
bg = slide.shapes.add_shape(
|
|||
|
|
MSO_SHAPE.RECTANGLE, 0, 0, prs.slide_width, prs.slide_height
|
|||
|
|
)
|
|||
|
|
bg.fill.solid()
|
|||
|
|
bg.fill.fore_color.rgb = BRIGHT_TEAL
|
|||
|
|
bg.line.fill.background()
|
|||
|
|
|
|||
|
|
stat_box = slide.shapes.add_textbox(
|
|||
|
|
Inches(0.5), Inches(2), Inches(12.333), Inches(2)
|
|||
|
|
)
|
|||
|
|
tf = stat_box.text_frame
|
|||
|
|
p = tf.paragraphs[0]
|
|||
|
|
p.text = stat
|
|||
|
|
p.font.size = Pt(96)
|
|||
|
|
p.font.bold = True
|
|||
|
|
p.font.color.rgb = WHITE
|
|||
|
|
p.alignment = PP_ALIGN.CENTER
|
|||
|
|
|
|||
|
|
desc_box = slide.shapes.add_textbox(
|
|||
|
|
Inches(1), Inches(4.3), Inches(11.333), Inches(1.5)
|
|||
|
|
)
|
|||
|
|
tf = desc_box.text_frame
|
|||
|
|
tf.word_wrap = True
|
|||
|
|
p = tf.paragraphs[0]
|
|||
|
|
p.text = description
|
|||
|
|
p.font.size = Pt(28)
|
|||
|
|
p.font.color.rgb = WHITE
|
|||
|
|
p.alignment = PP_ALIGN.CENTER
|
|||
|
|
|
|||
|
|
return slide
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ============================================================================
|
|||
|
|
# BUILD THE PRESENTATION
|
|||
|
|
# ============================================================================
|
|||
|
|
|
|||
|
|
# Slide 1: Title
|
|||
|
|
add_title_slide(
|
|||
|
|
"When the Data Surprised Us", "Counterintuitive Findings from the Field"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# Slide 2: Rules
|
|||
|
|
add_content_slide(
|
|||
|
|
"The Rules of the Game",
|
|||
|
|
[
|
|||
|
|
"1. I'll show you a scenario",
|
|||
|
|
"2. You guess what happened",
|
|||
|
|
"3. I reveal the data",
|
|||
|
|
"4. We discuss why it matters",
|
|||
|
|
"",
|
|||
|
|
"Fair warning: Some of these will mess with your head.",
|
|||
|
|
"That's the point.",
|
|||
|
|
],
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# ============================================================================
|
|||
|
|
# PUZZLE 1: The Beautiful Wreck
|
|||
|
|
# ============================================================================
|
|||
|
|
|
|||
|
|
add_puzzle_intro_slide("1", "The Beautiful Wreck")
|
|||
|
|
|
|||
|
|
add_setup_slide(
|
|||
|
|
"The Setup: Two Cars After Frontal Crash Tests",
|
|||
|
|
[
|
|||
|
|
"Same test. Same speed. Same barrier.",
|
|||
|
|
"",
|
|||
|
|
"CAR A: Destroyed. Front end pushed back almost to firewall.",
|
|||
|
|
" Hood crumpled like paper. Looks catastrophic.",
|
|||
|
|
"",
|
|||
|
|
"CAR B: Relatively intact. Minimal visible damage.",
|
|||
|
|
" Structure held. Passenger compartment looks good.",
|
|||
|
|
],
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
add_guess_slide("Which car would you rather be in during a crash?")
|
|||
|
|
|
|||
|
|
add_comparison_slide(
|
|||
|
|
"The Reveal: Appearances Deceive",
|
|||
|
|
"CAR A (Destroyed)",
|
|||
|
|
[
|
|||
|
|
"HIC: 485",
|
|||
|
|
"Chest deflection: 38mm",
|
|||
|
|
"Femur load: 4.2 kN",
|
|||
|
|
"",
|
|||
|
|
"All measurements well within limits",
|
|||
|
|
],
|
|||
|
|
"GOOD",
|
|||
|
|
"CAR B (Intact)",
|
|||
|
|
[
|
|||
|
|
"HIC: 890",
|
|||
|
|
"Chest deflection: 58mm",
|
|||
|
|
"Femur load: 9.1 kN",
|
|||
|
|
"",
|
|||
|
|
"Multiple measurements near limits",
|
|||
|
|
],
|
|||
|
|
"MARGINAL",
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
add_content_slide(
|
|||
|
|
"Why This Happens",
|
|||
|
|
[
|
|||
|
|
"Crush zones are SUPPOSED to crush",
|
|||
|
|
"That's their job - absorb energy by deforming",
|
|||
|
|
"",
|
|||
|
|
"Car A: Crumpled beautifully, extended deceleration pulse",
|
|||
|
|
" More time to slow down = less force on occupant",
|
|||
|
|
"",
|
|||
|
|
"Car B: Too stiff, didn't crumple enough",
|
|||
|
|
" Short, sharp deceleration = wall of g-forces",
|
|||
|
|
"",
|
|||
|
|
"Looking at the car tells you nothing about safety",
|
|||
|
|
],
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
add_lesson_slide(
|
|||
|
|
"Never judge a crash by looking at the car. Judge it by looking at the data."
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# ============================================================================
|
|||
|
|
# PUZZLE 2: The Survivor's Paradox
|
|||
|
|
# ============================================================================
|
|||
|
|
|
|||
|
|
add_puzzle_intro_slide("2", "The Survivor's Paradox")
|
|||
|
|
|
|||
|
|
add_setup_slide(
|
|||
|
|
"The Setup: The Rollover Problem",
|
|||
|
|
[
|
|||
|
|
"Rollovers are only 3% of crashes",
|
|||
|
|
"But they account for 33% of fatalities",
|
|||
|
|
"",
|
|||
|
|
"The obvious solution: Stronger roofs!",
|
|||
|
|
"",
|
|||
|
|
"In 2009, NHTSA doubled the roof crush requirement:",
|
|||
|
|
"From 1.5× vehicle weight to 3× vehicle weight",
|
|||
|
|
],
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
add_guess_slide("How much did stronger roofs reduce rollover fatalities?")
|
|||
|
|
|
|||
|
|
add_reveal_slide(
|
|||
|
|
"Essentially Unchanged",
|
|||
|
|
[
|
|||
|
|
"Despite DOUBLING roof strength requirements...",
|
|||
|
|
"",
|
|||
|
|
"Rollover fatalities: Barely moved",
|
|||
|
|
"",
|
|||
|
|
"How is that possible?",
|
|||
|
|
"Wasn't roof crush the problem?",
|
|||
|
|
],
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
add_content_slide(
|
|||
|
|
"The Data Revealed the Real Problem",
|
|||
|
|
[
|
|||
|
|
"70%+ of rollover fatalities involve EJECTION",
|
|||
|
|
"",
|
|||
|
|
"Occupants weren't killed by roof crushing",
|
|||
|
|
"They were killed by being thrown from the vehicle",
|
|||
|
|
"",
|
|||
|
|
"Once outside: Ground. Other cars. Poles.",
|
|||
|
|
"The vehicle itself rolling over them.",
|
|||
|
|
"",
|
|||
|
|
"Roof crush was secondary. Ejection was primary.",
|
|||
|
|
],
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
add_content_slide(
|
|||
|
|
"The Real Solution",
|
|||
|
|
[
|
|||
|
|
"Side curtain airbags with rollover sensors",
|
|||
|
|
"Stay inflated for 5-6 seconds through multiple rolls",
|
|||
|
|
"Keep occupants INSIDE the vehicle",
|
|||
|
|
"",
|
|||
|
|
"Result: 41% reduction in ejection",
|
|||
|
|
"",
|
|||
|
|
"The data pointed to a completely different solution",
|
|||
|
|
"than the intuitive one",
|
|||
|
|
],
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
add_lesson_slide(
|
|||
|
|
"The obvious solution isn't always the right solution. Let the data tell you what's actually happening."
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# ============================================================================
|
|||
|
|
# PUZZLE 3: The Luxury Failure
|
|||
|
|
# ============================================================================
|
|||
|
|
|
|||
|
|
add_puzzle_intro_slide("3", "The Luxury Failure")
|
|||
|
|
|
|||
|
|
add_setup_slide(
|
|||
|
|
"The Setup: Premium Brands",
|
|||
|
|
[
|
|||
|
|
"BMW. Mercedes-Benz. Audi. Lexus.",
|
|||
|
|
"",
|
|||
|
|
"Most expensive cars on the road",
|
|||
|
|
"Most prestigious brands in the world",
|
|||
|
|
"Decades of safety engineering",
|
|||
|
|
"",
|
|||
|
|
"All passed moderate overlap test with flying colors",
|
|||
|
|
"",
|
|||
|
|
"2012: IIHS introduces small overlap test",
|
|||
|
|
"25% overlap instead of 40%. Rigid barrier.",
|
|||
|
|
],
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
add_guess_slide("How did the luxury brands perform?")
|
|||
|
|
|
|||
|
|
add_content_slide(
|
|||
|
|
"The Results",
|
|||
|
|
[
|
|||
|
|
"BMW 5-Series: MARGINAL",
|
|||
|
|
"Mercedes C-Class: MARGINAL",
|
|||
|
|
"Audi A4: MARGINAL",
|
|||
|
|
"Lexus ES 350: POOR",
|
|||
|
|
"",
|
|||
|
|
"Only 3 of 13 midsize cars earned 'Good'",
|
|||
|
|
"",
|
|||
|
|
"The most expensive vehicles on the road",
|
|||
|
|
"couldn't pass a new crash test",
|
|||
|
|
],
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
add_content_slide(
|
|||
|
|
"What the Data Showed",
|
|||
|
|
[
|
|||
|
|
"Crash forces bypassed main structural rails",
|
|||
|
|
"Wheels pushed back into footwells",
|
|||
|
|
"Firewalls collapsed",
|
|||
|
|
"Steering columns displaced into occupants",
|
|||
|
|
"",
|
|||
|
|
"Dummy data: Elevated HIC, critical femur loads,",
|
|||
|
|
"feet trapped in crushed metal",
|
|||
|
|
"",
|
|||
|
|
"Engineered for the 40% test. Vulnerable at 25%.",
|
|||
|
|
],
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
add_lesson_slide(
|
|||
|
|
"Engineering to pass a test is not the same as engineering for safety."
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# ============================================================================
|
|||
|
|
# PUZZLE 4: The 46.2g Man
|
|||
|
|
# ============================================================================
|
|||
|
|
|
|||
|
|
add_puzzle_intro_slide("4", "The 46.2g Man")
|
|||
|
|
|
|||
|
|
add_setup_slide(
|
|||
|
|
"The Setup: 1954",
|
|||
|
|
[
|
|||
|
|
"The jet age is dawning. Pilots flying faster than ever.",
|
|||
|
|
"Many are dying - not from crashes, from ejections.",
|
|||
|
|
"",
|
|||
|
|
"Medical authorities believed:",
|
|||
|
|
"'Humans cannot survive more than 18g of deceleration'",
|
|||
|
|
"",
|
|||
|
|
"If true, high-speed ejection was a death sentence.",
|
|||
|
|
"",
|
|||
|
|
"One researcher decided to test this assumption.",
|
|||
|
|
"Not with dummies. With himself.",
|
|||
|
|
],
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
add_content_slide(
|
|||
|
|
"Colonel John Paul Stapp",
|
|||
|
|
[
|
|||
|
|
"Air Force flight surgeon",
|
|||
|
|
"The bravest - or craziest - researcher in safety history",
|
|||
|
|
"",
|
|||
|
|
"Strapped himself into a rocket sled",
|
|||
|
|
"Ran test after test, pushing higher g-forces",
|
|||
|
|
"",
|
|||
|
|
"December 10, 1954:",
|
|||
|
|
"The ultimate test",
|
|||
|
|
],
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
add_stat_slide(
|
|||
|
|
"46.2g", "632 mph to zero in 1.4 seconds\nMore than 2.5× the 'fatal' threshold"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
add_content_slide(
|
|||
|
|
"He Survived",
|
|||
|
|
[
|
|||
|
|
"Walked away - temporarily blinded",
|
|||
|
|
"Burst blood vessels in his eyes",
|
|||
|
|
"But alive and conscious",
|
|||
|
|
"",
|
|||
|
|
"Proved humans could survive far more than believed",
|
|||
|
|
"Revolutionized aviation safety",
|
|||
|
|
"Made ejection seats viable at higher speeds",
|
|||
|
|
"",
|
|||
|
|
"Laid the foundation for automotive crash testing:",
|
|||
|
|
"How do we design so occupants never exceed their limits?",
|
|||
|
|
],
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
add_lesson_slide("Assumptions are not data. Test it. Measure it. Know for certain.")
|
|||
|
|
|
|||
|
|
# ============================================================================
|
|||
|
|
# PUZZLE 5: The Airbag Arms Race
|
|||
|
|
# ============================================================================
|
|||
|
|
|
|||
|
|
add_puzzle_intro_slide("5", "The Airbag Arms Race")
|
|||
|
|
|
|||
|
|
add_setup_slide(
|
|||
|
|
"The Setup: Airbag History",
|
|||
|
|
[
|
|||
|
|
"1970: Airbags first proposed",
|
|||
|
|
"1984: First automatic restraint requirements",
|
|||
|
|
"1998: Airbags mandatory in all vehicles",
|
|||
|
|
"2000s: 'Advanced' airbags with occupant classification",
|
|||
|
|
"",
|
|||
|
|
"Each generation more sophisticated:",
|
|||
|
|
"More sensors. More stages. More intelligence.",
|
|||
|
|
],
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
add_guess_slide(
|
|||
|
|
"How much faster did airbag technology advance after mandates vs. before?"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
add_reveal_slide(
|
|||
|
|
"Regulation Created Innovation",
|
|||
|
|
[
|
|||
|
|
"Pre-mandate (1970-1984): Minimal innovation",
|
|||
|
|
"Why invest in something not required?",
|
|||
|
|
"",
|
|||
|
|
"Post-mandate: Explosion of patents and technology",
|
|||
|
|
"Multi-stage inflators, occupant sensors, position detection",
|
|||
|
|
"Side airbags, curtain airbags, knee airbags",
|
|||
|
|
"",
|
|||
|
|
"The regulation created the market",
|
|||
|
|
"The market drove innovation",
|
|||
|
|
"The innovation saved lives",
|
|||
|
|
],
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
add_stat_slide(
|
|||
|
|
"~2,800",
|
|||
|
|
"Lives saved by airbags per year in the U.S.\nFrom zero in 1985 to thousands today",
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
add_lesson_slide(
|
|||
|
|
"Measurement isn't just about understanding what happened. It's about enabling what comes next."
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# ============================================================================
|
|||
|
|
# CLOSING
|
|||
|
|
# ============================================================================
|
|||
|
|
|
|||
|
|
add_content_slide(
|
|||
|
|
"The Meta-Lesson",
|
|||
|
|
[
|
|||
|
|
"Beautiful Wreck: Crush zones work - deformation is safety",
|
|||
|
|
"Survivor's Paradox: Ejection, not crush, was the killer",
|
|||
|
|
"Luxury Failure: We'd optimized for the wrong test",
|
|||
|
|
"46.2g Man: Assumptions limited our designs",
|
|||
|
|
"Airbag Arms Race: Mandates drove innovation",
|
|||
|
|
"",
|
|||
|
|
"In every case, data didn't just describe - it ENABLED",
|
|||
|
|
"It pointed the way forward",
|
|||
|
|
],
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
add_content_slide(
|
|||
|
|
"Implications for DTS",
|
|||
|
|
[
|
|||
|
|
"Our instrumentation is the source of these surprises",
|
|||
|
|
"",
|
|||
|
|
"When data contradicts expectations, it's because:",
|
|||
|
|
"Someone measured something that hadn't been measured",
|
|||
|
|
"Or measured it more precisely",
|
|||
|
|
"Or measured it in a new context",
|
|||
|
|
"",
|
|||
|
|
"Every sensor we design, every DAQ we build",
|
|||
|
|
"is potentially the source of the next breakthrough",
|
|||
|
|
],
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
add_lesson_slide(
|
|||
|
|
"What assumptions are we making today that data will overturn tomorrow?"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# Questions slide
|
|||
|
|
add_title_slide("Questions?", "Ben - Application Engineer")
|
|||
|
|
|
|||
|
|
# ============================================================================
|
|||
|
|
# Save
|
|||
|
|
# ============================================================================
|
|||
|
|
|
|||
|
|
output_file = "When_the_Data_Surprised_Us.pptx"
|
|||
|
|
prs.save(output_file)
|
|||
|
|
print(f"Presentation created: {output_file}")
|
|||
|
|
print(f"Total slides: {len(prs.slides)}")
|