Skip to content

4.2. Charts

Tables are a powerful way to share results with stakeholders, but often you’ll also want to visualise the same data. The good news is that once you’ve aggregated your data for tables, half the work is already done for charts - both start from the same summarised dataset.

The process is similar:

Aggregate the data to the level you want to analyse (e.g. by Driver Age, Area, or Vehicle Group).

Pass the aggregated results into a plotting library to create visuals.

Whereas great_tables gave us polished static outputs, charting libraries let us go further - adding interactivity, multiple views of the same data, and dashboards.

In Python you’ll commonly see:

  • matplotlib: the foundation library, very flexible but low-level.

  • seaborn: built on matplotlib, great for quick statistical charts.

  • plotly: interactive charts out of the box, ideal for sharing and dashboarding.

Here we’ll focus on Plotly, since it produces interactive visuals that are easy to embed in reports or expand into dashboards with Plotly Dash. Like great_tables, it’s highly customisable and can give professional-looking outputs straight from Python code.

Charting function

The function below creates a dual-axis chart for frequency analysis:

Bars show exposure.

Lines show actual vs predicted frequency.

This mirrors the tables we built earlier, but presents the comparison visually so trends and deviations stand out more clearly.

def plot_aggregated_data(visual_data: pl.DataFrame, feature: str, target: str, prediction: str, exposure: str) -> None:

    fig = go.Figure()

    # Bar: Exposure
    fig.add_trace(go.Bar(
        x=visual_data[feature],
        y=visual_data[exposure],
        name=exposure,
        marker_color='lightskyblue',
        yaxis='y1'
    ))

    # Line: ClaimCount
    fig.add_trace(go.Scatter(
        x=visual_data[feature],
        y=visual_data[target],
        name=target,
        mode='lines+markers',
        line=dict(color='firebrick'),
        yaxis='y2'
    ))

    # Line: gbm_predictions
    fig.add_trace(go.Scatter(
        x=visual_data[feature],
        y=visual_data[prediction],
        name="Frequency Predictions",
        mode='lines+markers',
        line=dict(color='green', dash='dot'),
        yaxis='y2'
    ))

    # Layout with dual y-axes
    fig.update_layout(
        title=f"Frequency - Actuals vs Prediction - {feature}",
        xaxis=dict(title=feature),
        yaxis=dict(
            title=exposure,
            side="left",
            showgrid=False
        ),
        yaxis2=dict(
            title="Frequency",
            overlaying="y",
            side="right"
        ),
        barmode='group',
        legend=dict(x=0.01, y=0.99)
    )

    return fig

Running it looks like this:

plot_aggregated_data(visual_data = aggregated_data, 
                        feature = feature, 
                        target = 'Frequency', 
                        prediction = 'FrequencyPrediction',
                        exposure = 'Exposure')

Which generates the below plot.