15. Data Storytelling
Chapter 15 of 18 · 20 min
Data storytelling transforms raw analysis into compelling narratives that drive decisions. Effective stories combine data with context, emotion, and clear visual communication.
Building a Narrative Structure
Every data story needs a structure:
def generate_narrative(analysis_results, audience='executives'):
"""Generate narrative from analysis results."""
prompt = f"""Create a data story narrative from these analysis results.
Audience: {audience}
Key findings:
{analysis_results['key_metrics']}
Trends identified:
{analysis_results['trends']}
Anomalies detected:
{analysis_results['anomalies']}
Structure: Hook → Context → Discovery → Implication → Call to action
Tone: Professional but engaging"""
response = ollama.chat(model='llama3.2', messages=[
{'role': 'user', 'content': prompt}
])
return response['message']['content']
Choosing the Right Visualization
Different stories need different charts:
# Comparison: Bar chart
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
# Bar for category comparison
df.groupby('region')['revenue'].sum().sort_values().plot(kind='barh', ax=axes[0])
axes[0].set_title('Revenue by Region')
axes[0].set_xlabel('Revenue ($)')
# Composition: Stacked bar
pivot = df.groupby(['region', 'product_line'])['revenue'].sum().unstack()
pivot.plot(kind='bar', stacked=True, ax=axes[1])
axes[1].set_title('Revenue Composition by Region')
axes[1].legend(title='Product Line', bbox_to_anchor=(1.05, 1))
Annotating Key Points
fig, ax = plt.subplots(figsize=(12, 6))
ax.plot(df.index, df['revenue'], linewidth=2)
# Annotate significant events
annotations = [
('2024-02-15', 'Product Launch', (30, 50)),
('2024-06-01', 'Market Shift', (20, -30)),
('2024-09-20', 'Recovery', (-50, 20))
]
for date, label, offset in annotations:
ax.annotate(label, xy=(pd.Timestamp(date), df.loc[date, 'revenue']),
xytext=offset, textcoords='offset points',
arrowprops=dict(arrowstyle='->', color='gray'),
fontsize=10, color='darkred')
ax.set_title('Revenue Trajectory with Key Events')
Dashboard Composition
import matplotlib.gridspec as gridspec
fig = plt.figure(figsize=(16, 10))
gs = gridspec.GridSpec(3, 3, figure=fig, hspace=0.3, wspace=0.3)
# KPI cards at top
ax1 = fig.add_subplot(gs[0, 0])
ax1.text(0.5, 0.7, f"${df['revenue'].sum()/1e6:.1f}M", fontsize=24, ha='center', fontweight='bold')
ax1.text(0.5, 0.3, 'Total Revenue', fontsize=12, ha='center', color='gray')
ax1.axis('off')
ax2 = fig.add_subplot(gs[0, 1])
ax2.text(0.5, 0.7, f"{len(df[df['anomaly']])}", fontsize=24, ha='center', fontweight='bold')
ax2.text(0.5, 0.3, 'Anomalies', fontsize=12, ha='center', color='gray')
ax2.axis('off')
# Main chart
ax3 = fig.add_subplot(gs[1, :])
ax3.plot(df.index, df['revenue'])
ax3.fill_between(df.index, df['revenue'], alpha=0.3)
ax3.set_title('Revenue Over Time')
Exporting Story for Sharing
def export_story_to_html(data, narrative, visualizations):
html = f"""
<html>
<head><title>Data Analysis Report</title></head>
<body>
<h1>Executive Summary</h1>
<div class='narrative'>{narrative}</div>
<h2>Key Charts</h2>
{''.join([f"<img src='{v}'/>" for v in visualizations])}
</body>
</html>"""
with open('report.html', 'w') as f:
f.write(html)
EXERCISE
Create a 4-slide data story from your analysis: (1) hook with KPI card, (2) trend line with annotations, (3) anomaly callout, (4) call to action summary. Export as HTML.