Back to Blog
5 min read

Smart Narratives in Power BI: Automated Data Storytelling

Smart Narratives in Power BI: Automated Data Storytelling

Smart Narratives automatically generate text summaries of your data, transforming numbers into stories. This guide covers implementation and customization.

Understanding Smart Narratives

SMART_NARRATIVE_FEATURES = {
    "automatic_summaries": [
        "Key metrics and trends",
        "Comparisons and rankings",
        "Anomaly callouts",
        "Percentage changes"
    ],
    "customization": [
        "Define custom phrases",
        "Control which metrics appear",
        "Set conditional text",
        "Add dynamic calculations"
    ],
    "formatting": [
        "Bullet points and lists",
        "Bold/italic emphasis",
        "Value formatting",
        "Conditional colors"
    ]
}

Creating Smart Narratives

# Smart Narrative configuration

NARRATIVE_CONFIGURATION = {
    "basic_setup": {
        "visual_type": "smartNarrative",
        "data_bindings": {
            "summarize_values": ["Total Sales", "Order Count", "Avg Order Value"]
        },
        "settings": {
            "auto_generate": True,
            "max_sentences": 5
        }
    },

    "custom_template": """
    Total sales for {{selected_period}} reached {{Total Sales:$#,##0}},
    {{#if YoY_positive}}up{{else}}down{{/if}} {{YoY Change:%}} compared to last year.

    Key highlights:
    - Top region: {{Top Region}} with {{Top Region Sales:$#,##0}}
    - Best product: {{Top Product}} ({{Top Product Units:#,##0}} units)
    - Average order value: {{Avg Order Value:$#,##0}}

    {{#if has_anomalies}}
    Note: Unusual activity detected in {{Anomaly Region}} on {{Anomaly Date}}.
    {{/if}}
    """
}

Building Custom Narratives

from typing import List, Dict
import anthropic

class SmartNarrativeBuilder:
    """Build smart narratives from data"""

    def __init__(self):
        self.client = anthropic.Anthropic()

    def generate_narrative(
        self,
        metrics: Dict[str, float],
        comparisons: Dict[str, Dict],
        context: str
    ) -> str:
        """Generate narrative from metrics and comparisons"""

        prompt = f"""Generate a professional business narrative based on this data:

Context: {context}

Current Metrics:
{self._format_metrics(metrics)}

Comparisons:
{self._format_comparisons(comparisons)}

Requirements:
- Write 3-4 sentences
- Lead with the most important finding
- Include specific numbers with proper formatting
- Mention trends and changes
- Use professional business language

Narrative:"""

        response = self.client.messages.create(
            model="claude-3-haiku-20240307",
            max_tokens=300,
            messages=[{"role": "user", "content": prompt}]
        )

        return response.content[0].text.strip()

    def generate_highlights(
        self,
        data: List[Dict],
        metric_name: str,
        top_n: int = 3
    ) -> str:
        """Generate highlights for top performers"""

        sorted_data = sorted(data, key=lambda x: x[metric_name], reverse=True)
        top = sorted_data[:top_n]
        bottom = sorted_data[-1:]

        prompt = f"""Create a brief highlight summary:

Top {top_n} by {metric_name}:
{self._format_list(top, metric_name)}

Lowest:
{self._format_list(bottom, metric_name)}

Write 2-3 sentences highlighting the top performers and noting any concerns about the lowest."""

        response = self.client.messages.create(
            model="claude-3-haiku-20240307",
            max_tokens=200,
            messages=[{"role": "user", "content": prompt}]
        )

        return response.content[0].text.strip()

    def explain_change(
        self,
        metric_name: str,
        current: float,
        previous: float,
        contributing_factors: List[Dict]
    ) -> str:
        """Explain why a metric changed"""

        change_pct = ((current - previous) / previous) * 100

        prompt = f"""{metric_name} changed from {previous:,.2f} to {current:,.2f} ({change_pct:+.1f}%).

Contributing factors:
{self._format_factors(contributing_factors)}

Explain this change in 2-3 sentences, focusing on the main drivers."""

        response = self.client.messages.create(
            model="claude-3-haiku-20240307",
            max_tokens=200,
            messages=[{"role": "user", "content": prompt}]
        )

        return response.content[0].text.strip()

    def _format_metrics(self, metrics: Dict) -> str:
        return "\n".join(f"- {k}: {v:,.2f}" for k, v in metrics.items())

    def _format_comparisons(self, comparisons: Dict) -> str:
        lines = []
        for metric, data in comparisons.items():
            change = ((data["current"] - data["previous"]) / data["previous"]) * 100
            lines.append(f"- {metric}: {data['current']:,.2f} vs {data['previous']:,.2f} ({change:+.1f}%)")
        return "\n".join(lines)

    def _format_list(self, items: List[Dict], metric: str) -> str:
        return "\n".join(
            f"- {item.get('name', 'Item')}: {item[metric]:,.2f}"
            for item in items
        )

    def _format_factors(self, factors: List[Dict]) -> str:
        return "\n".join(
            f"- {f['factor']}: {f['impact']:+,.2f} ({f['percentage']:+.1f}%)"
            for f in factors
        )

# Usage
builder = SmartNarrativeBuilder()

narrative = builder.generate_narrative(
    metrics={
        "Total Revenue": 4250000,
        "Order Count": 12500,
        "Avg Order Value": 340
    },
    comparisons={
        "Total Revenue": {"current": 4250000, "previous": 3800000},
        "Order Count": {"current": 12500, "previous": 11200}
    },
    context="Q1 2024 Sales Performance Report"
)
print(narrative)

Conditional Narratives

# Templates with conditional logic

CONDITIONAL_TEMPLATES = {
    "performance_summary": {
        "template": """
        {{period_name}} performance:
        {{#if above_target}}
        Excellent results! We exceeded target by {{excess_pct}}%, reaching {{actual_value}}.
        {{else if near_target}}
        We came close to our target, achieving {{achievement_pct}}% of goal.
        {{else}}
        We fell short of our target, achieving only {{achievement_pct}}% of goal. Key improvement areas:
        {{#each improvement_areas}}
        - {{this}}
        {{/each}}
        {{/if}}
        """,

        "data_needed": [
            "period_name",
            "actual_value",
            "target_value",
            "improvement_areas"
        ]
    },

    "trend_description": {
        "template": """
        {{metric_name}} has been {{trend_direction}} over the past {{period_count}} periods.
        {{#if consistent}}
        The trend has been consistent, with an average {{trend_direction}}ly change of {{avg_change}}.
        {{else}}
        However, the trend shows some volatility:
        - Highest: {{max_value}} ({{max_period}})
        - Lowest: {{min_value}} ({{min_period}})
        {{/if}}
        """,

        "data_needed": [
            "metric_name",
            "trend_direction",
            "period_count",
            "avg_change",
            "max_value",
            "min_value"
        ]
    }
}

def apply_conditional_template(template: str, data: Dict) -> str:
    """Apply conditional logic to template"""
    import re

    result = template

    # Handle if/else blocks
    if_pattern = r'\{\{#if (\w+)\}\}(.*?)\{\{else\}\}(.*?)\{\{/if\}\}'

    def replace_if(match):
        condition = match.group(1)
        if_true = match.group(2)
        if_false = match.group(3)
        return if_true.strip() if data.get(condition) else if_false.strip()

    result = re.sub(if_pattern, replace_if, result, flags=re.DOTALL)

    # Handle simple placeholders
    for key, value in data.items():
        if isinstance(value, (int, float)):
            result = result.replace(f"{{{{{key}}}}}", f"{value:,.2f}")
        else:
            result = result.replace(f"{{{{{key}}}}}", str(value))

    return result.strip()

Dynamic Narrative Updates

class DynamicNarrativeManager:
    """Manage narratives that update with data changes"""

    def __init__(self):
        self.builder = SmartNarrativeBuilder()
        self.cache = {}

    def get_narrative(
        self,
        report_id: str,
        filters: Dict,
        force_refresh: bool = False
    ) -> str:
        """Get narrative for current filter context"""

        cache_key = f"{report_id}:{hash(frozenset(filters.items()))}"

        if not force_refresh and cache_key in self.cache:
            return self.cache[cache_key]

        # Fetch current data based on filters
        data = self._fetch_data(report_id, filters)

        # Generate appropriate narrative
        if self._is_comparison_view(filters):
            narrative = self._generate_comparison_narrative(data)
        elif self._is_drill_down(filters):
            narrative = self._generate_detail_narrative(data)
        else:
            narrative = self._generate_overview_narrative(data)

        self.cache[cache_key] = narrative
        return narrative

    def _is_comparison_view(self, filters: Dict) -> bool:
        return "compare_to" in filters

    def _is_drill_down(self, filters: Dict) -> bool:
        return len(filters) > 2

    def _fetch_data(self, report_id: str, filters: Dict) -> Dict:
        # Implement data fetching logic
        return {}

    def _generate_comparison_narrative(self, data: Dict) -> str:
        return self.builder.generate_narrative(
            metrics=data.get("metrics", {}),
            comparisons=data.get("comparisons", {}),
            context="Comparison Analysis"
        )

    def _generate_detail_narrative(self, data: Dict) -> str:
        return self.builder.generate_highlights(
            data=data.get("details", []),
            metric_name=data.get("primary_metric", "value"),
            top_n=3
        )

    def _generate_overview_narrative(self, data: Dict) -> str:
        return self.builder.generate_narrative(
            metrics=data.get("metrics", {}),
            comparisons=data.get("comparisons", {}),
            context="Overview"
        )

Conclusion

Smart Narratives transform data into accessible stories. Combine Power BI’s built-in capabilities with custom AI-generated narratives for comprehensive automated reporting.

Michael John Peña

Michael John Peña

Senior Data Engineer based in Sydney. Writing about data, cloud, and technology.