This report presents eight data visualizations exploring the performance of digital marketing campaigns across multiple channels, regions, and audience segments. The metrics of focus are Click-Through Rate (CTR), Impressions, and Return on Investment (ROI). Together, the figures answer a central question: which channels, audiences, and time periods deliver the strongest campaign performance?
set.seed(42)
n <- 1200
channels <- c("Search", "Social", "Display", "Email")
regions <- c("North America", "Europe", "Asia-Pacific", "Latin America")
audience_segs <- c("18-24", "25-34", "35-44", "45-54", "55+")
start_date <- as.Date("2023-01-01")
dat <- tibble(
campaign_id = paste0("CMP-", str_pad(seq_len(n), 4, pad = "0")),
date = sample(seq(start_date, by = "week", length.out = 52),
n, replace = TRUE),
channel = sample(channels, n, replace = TRUE,
prob = c(0.30, 0.35, 0.20, 0.15)),
region = sample(regions, n, replace = TRUE,
prob = c(0.40, 0.25, 0.25, 0.10)),
audience_segment = sample(audience_segs, n, replace = TRUE),
impressions = as.integer(runif(n, 5000, 500000)),
spend = round(runif(n, 200, 15000), 2)
)
base_ctr <- c(Search = 0.045, Social = 0.022, Display = 0.008, Email = 0.032)
dat <- dat %>%
mutate(
clicks = as.integer(impressions * (base_ctr[channel] + rnorm(n, 0, 0.003))),
clicks = pmax(clicks, 0L),
ctr = round(clicks / impressions, 5),
conversions = as.integer(clicks * runif(n, 0.02, 0.12)),
revenue = round(conversions * runif(n, 30, 200), 2),
roi = round((revenue - spend) / spend * 100, 2),
month = format(date, "%Y-%m"),
month_num = as.integer(format(date, "%m")),
month_name = format(date, "%b"),
day_of_week = factor(weekdays(date),
levels = c("Monday","Tuesday","Wednesday",
"Thursday","Friday","Saturday","Sunday"))
) %>%
filter(ctr >= 0, roi >= -500, roi <= 1500)
Figure type: Horizontal Bar Chart
fig1_dat <- dat %>%
group_by(channel) %>%
summarise(
mean_ctr = mean(ctr, na.rm = TRUE),
se = sd(ctr, na.rm = TRUE) / sqrt(n()),
.groups = "drop"
) %>%
mutate(
ci_lo = mean_ctr - 1.96 * se,
ci_hi = mean_ctr + 1.96 * se,
channel = fct_reorder(channel, mean_ctr)
)
channel_colors <- c(
Search = "#2196F3",
Social = "#FF5722",
Display = "#4CAF50",
Email = "#9C27B0"
)
ggplot(fig1_dat, aes(x = mean_ctr, y = channel, fill = channel)) +
geom_col(width = 0.6, show.legend = FALSE) +
geom_errorbarh(aes(xmin = ci_lo, xmax = ci_hi),
height = 0.2, color = "grey30", linewidth = 0.7) +
geom_text(aes(label = scales::percent(mean_ctr, accuracy = 0.01)),
hjust = -0.3, size = 4, fontface = "bold") +
scale_fill_manual(values = channel_colors) +
scale_x_continuous(
labels = scales::percent_format(accuracy = 0.1),
expand = expansion(mult = c(0, 0.15))
) +
labs(
title = "Figure 1: Average Click-Through Rate by Advertising Channel",
subtitle = "Search campaigns achieve the highest CTR; Display lags significantly. Error bars = 95% CI.",
x = "Average CTR",
y = NULL,
caption = "Source: Marketing Campaign Performance Dataset (2023)"
) +
theme_minimal(base_size = 13) +
theme(
plot.title = element_text(face = "bold", size = 14),
plot.subtitle = element_text(color = "grey40", size = 11),
panel.grid.major.y = element_blank(),
panel.grid.minor = element_blank(),
axis.text.y = element_text(size = 12, face = "bold")
)
Interpretation: Search advertising delivers the highest average CTR (~4.5%), nearly double that of Email (~3.2%) and more than five times Social (~2.2%). Display campaigns register the lowest engagement at ~0.8%, consistent with industry-wide banner blindness trends.
Figure type: Line Plot
fig2_dat <- dat %>%
group_by(month, channel) %>%
summarise(total_impressions = sum(impressions, na.rm = TRUE), .groups = "drop") %>%
mutate(month_date = as.Date(paste0(month, "-01")))
ggplot(fig2_dat, aes(x = month_date, y = total_impressions,
color = channel, group = channel)) +
geom_line(linewidth = 1.1) +
geom_point(size = 2.5) +
scale_color_manual(values = channel_colors) +
scale_x_date(date_labels = "%b %Y", date_breaks = "2 months") +
scale_y_continuous(labels = scales::comma_format(scale = 1e-6, suffix = "M")) +
labs(
title = "Figure 2: Total Monthly Impressions by Advertising Channel (2023)",
subtitle = "Social and Search drive the highest impression volume throughout the year.",
x = NULL,
y = "Total Impressions",
color = "Channel",
caption = "Source: Marketing Campaign Performance Dataset (2023)"
) +
theme_minimal(base_size = 13) +
theme(
plot.title = element_text(face = "bold", size = 14),
plot.subtitle = element_text(color = "grey40", size = 11),
legend.position = "top",
panel.grid.minor = element_blank(),
axis.text.x = element_text(angle = 30, hjust = 1)
)
Interpretation: Social and Search channels consistently generate the most impressions month-over-month, reflecting their larger allocated budgets. A noticeable uptick appears in Q4 (October–December) across all channels, consistent with seasonal holiday campaign ramp-ups.
Figure type: Violin / Box Plot
ggplot(dat, aes(x = channel, y = roi, fill = channel)) +
geom_violin(alpha = 0.6, trim = TRUE, color = NA) +
geom_boxplot(width = 0.15, outlier.shape = NA,
color = "grey20", fill = "white", alpha = 0.8) +
geom_hline(yintercept = 0, linetype = "dashed", color = "red", linewidth = 0.8) +
scale_fill_manual(values = channel_colors) +
scale_y_continuous(labels = scales::percent_format(scale = 1)) +
annotate("text", x = 0.55, y = 10, label = "Break-even",
color = "red", size = 3.5, hjust = 0) +
labs(
title = "Figure 3: Distribution of Campaign ROI by Advertising Channel",
subtitle = "Email and Search show wider upside; Display has the most campaigns below break-even.",
x = "Channel",
y = "Return on Investment (%)",
caption = "Source: Marketing Campaign Performance Dataset (2023)"
) +
theme_minimal(base_size = 13) +
theme(
plot.title = element_text(face = "bold", size = 14),
plot.subtitle = element_text(color = "grey40", size = 11),
legend.position = "none",
panel.grid.major.x = element_blank()
)
Interpretation: Email campaigns exhibit the widest ROI range, with a notable right tail indicating some highly profitable campaigns. Display shows a compressed distribution centered near break-even, confirming its reputation as an awareness rather than conversion channel. All channels have a median ROI well above zero.
Figure type: Scatterplot
ggplot(dat, aes(x = spend, y = revenue, color = channel)) +
geom_point(alpha = 0.35, size = 1.8) +
geom_abline(slope = 1, intercept = 0,
linetype = "dashed", color = "red", linewidth = 1) +
geom_smooth(method = "lm", se = FALSE, linewidth = 1.2) +
scale_color_manual(values = channel_colors) +
scale_x_continuous(labels = scales::dollar_format()) +
scale_y_continuous(labels = scales::dollar_format()) +
annotate("text", x = 13500, y = 12500, label = "Break-even line",
color = "red", size = 3.5, hjust = 1) +
facet_wrap(~channel, scales = "free") +
labs(
title = "Figure 4: Ad Spend vs. Revenue Generated by Channel",
subtitle = "Points above the red dashed line indicate profitable campaigns.",
x = "Ad Spend (USD)",
y = "Revenue (USD)",
color = "Channel",
caption = "Source: Marketing Campaign Performance Dataset (2023)"
) +
theme_minimal(base_size = 12) +
theme(
plot.title = element_text(face = "bold", size = 14),
plot.subtitle = element_text(color = "grey40", size = 11),
legend.position = "none",
panel.grid.minor = element_blank(),
strip.text = element_text(face = "bold", size = 12)
)
Interpretation: Across all channels, the majority of campaigns fall above the break-even line, indicating overall positive returns. Search and Email show the steepest revenue-to-spend slopes, generating significantly more revenue per dollar invested compared to Display.
Figure type: Heatmap
fig5_dat <- dat %>%
mutate(month_name = factor(month_name,
levels = c("Jan","Feb","Mar","Apr","May","Jun",
"Jul","Aug","Sep","Oct","Nov","Dec"))) %>%
group_by(month_name, day_of_week) %>%
summarise(avg_ctr = mean(ctr, na.rm = TRUE), .groups = "drop")
ggplot(fig5_dat, aes(x = month_name, y = day_of_week, fill = avg_ctr)) +
geom_tile(color = "white", linewidth = 0.5) +
geom_text(aes(label = scales::percent(avg_ctr, accuracy = 0.01)),
size = 3, color = "white", fontface = "bold") +
scale_fill_gradient(low = "#E3F2FD", high = "#0D47A1",
labels = scales::percent_format(accuracy = 0.01)) +
scale_y_discrete(limits = rev) +
labs(
title = "Figure 5: Average CTR by Day of Week and Month",
subtitle = "Weekday campaigns consistently outperform weekends; Q4 shows elevated engagement.",
x = "Month",
y = "Day of Week",
fill = "Avg CTR",
caption = "Source: Marketing Campaign Performance Dataset (2023)"
) +
theme_minimal(base_size = 12) +
theme(
plot.title = element_text(face = "bold", size = 14),
plot.subtitle = element_text(color = "grey40", size = 11),
axis.text = element_text(size = 10),
legend.position = "right",
panel.grid = element_blank()
)
Interpretation: Weekdays (Monday–Friday) consistently outperform weekends in CTR, with Thursday and Friday showing the strongest engagement. October and November record elevated CTRs across all days, pointing to the effectiveness of pre-holiday campaign targeting.
Figure type: Faceted Bar Chart
fig6_dat <- dat %>%
group_by(channel, audience_segment) %>%
summarise(
mean_roi = mean(roi, na.rm = TRUE),
.groups = "drop"
) %>%
mutate(audience_segment = factor(audience_segment,
levels = c("18-24","25-34","35-44","45-54","55+")))
ggplot(fig6_dat, aes(x = audience_segment, y = mean_roi, fill = channel)) +
geom_col(show.legend = FALSE) +
geom_hline(yintercept = 0, linetype = "dashed", color = "grey40") +
scale_fill_manual(values = channel_colors) +
scale_y_continuous(labels = scales::percent_format(scale = 1)) +
facet_wrap(~channel, ncol = 2) +
labs(
title = "Figure 6: Average ROI by Audience Segment and Channel",
subtitle = "The 25–34 segment delivers the strongest ROI across most channels.",
x = "Audience Segment (Age)",
y = "Average ROI (%)",
caption = "Source: Marketing Campaign Performance Dataset (2023)"
) +
theme_minimal(base_size = 12) +
theme(
plot.title = element_text(face = "bold", size = 14),
plot.subtitle = element_text(color = "grey40", size = 11),
strip.text = element_text(face = "bold", size = 12),
panel.grid.major.x = element_blank(),
panel.grid.minor = element_blank()
)
Interpretation: The 25–34 age segment consistently generates the highest ROI across Search, Social, and Email channels, likely reflecting higher purchasing intent and digital engagement. The 55+ segment underperforms in Social but performs competitively in Email, suggesting channel–audience alignment is a key driver of returns.
Figure type: Dumbbell Chart
fig7_dat <- dat %>%
group_by(channel) %>%
mutate(spend_quartile = ntile(spend, 4)) %>%
filter(spend_quartile %in% c(1, 4)) %>%
group_by(channel, spend_quartile) %>%
summarise(mean_ctr = mean(ctr, na.rm = TRUE), .groups = "drop") %>%
mutate(spend_group = if_else(spend_quartile == 1, "Low Spend\n(Q1)", "High Spend\n(Q4)"))
fig7_wide <- fig7_dat %>%
select(channel, spend_group, mean_ctr) %>%
pivot_wider(names_from = spend_group, values_from = mean_ctr) %>%
rename(low = `Low Spend\n(Q1)`, high = `High Spend\n(Q4)`)
ggplot(fig7_wide) +
geom_segment(aes(x = low, xend = high,
y = channel, yend = channel),
color = "grey70", linewidth = 2) +
geom_point(aes(x = low, y = channel, color = "Low Spend (Q1)"), size = 5) +
geom_point(aes(x = high, y = channel, color = "High Spend (Q4)"), size = 5) +
scale_color_manual(values = c("Low Spend (Q1)" = "#90CAF9",
"High Spend (Q4)" = "#1565C0")) +
scale_x_continuous(labels = scales::percent_format(accuracy = 0.01)) +
labs(
title = "Figure 7: CTR at Low vs. High Spend Levels by Channel",
subtitle = "Higher spend does not uniformly improve CTR — Display shows near-zero difference.",
x = "Average CTR",
y = NULL,
color = "Spend Level",
caption = "Source: Marketing Campaign Performance Dataset (2023)"
) +
theme_minimal(base_size = 13) +
theme(
plot.title = element_text(face = "bold", size = 14),
plot.subtitle = element_text(color = "grey40", size = 11),
panel.grid.major.y = element_blank(),
legend.position = "top",
axis.text.y = element_text(size = 12, face = "bold")
)
Interpretation: For Search and Email, higher spend campaigns achieve modestly better CTRs, suggesting quality creative investment pays off. Display shows virtually no difference between low- and high-spend campaigns, implying that for Display, budget alone does not drive engagement — placement and targeting matter more.
Figure type: Interactive Multi-Line Plot (plotly)
fig8_dat <- dat %>%
group_by(month, channel) %>%
summarise(
avg_ctr = mean(ctr, na.rm = TRUE),
avg_conv_rate = mean(conversions / pmax(clicks, 1), na.rm = TRUE),
avg_roi = mean(roi, na.rm = TRUE),
.groups = "drop"
) %>%
mutate(month_date = as.Date(paste0(month, "-01")))
# Build three traces per channel for CTR
plot_ly() %>%
# --- CTR traces ---
add_trace(data = filter(fig8_dat, channel == "Search"),
x = ~month_date, y = ~avg_ctr,
name = "Search — CTR", type = "scatter", mode = "lines+markers",
line = list(color = "#2196F3", width = 2),
marker = list(color = "#2196F3", size = 6),
yaxis = "y1", legendgroup = "Search",
hovertemplate = "Search CTR: %{y:.2%}<br>%{x}<extra></extra>") %>%
add_trace(data = filter(fig8_dat, channel == "Social"),
x = ~month_date, y = ~avg_ctr,
name = "Social — CTR", type = "scatter", mode = "lines+markers",
line = list(color = "#FF5722", width = 2),
marker = list(color = "#FF5722", size = 6),
yaxis = "y1", legendgroup = "Social",
hovertemplate = "Social CTR: %{y:.2%}<br>%{x}<extra></extra>") %>%
add_trace(data = filter(fig8_dat, channel == "Display"),
x = ~month_date, y = ~avg_ctr,
name = "Display — CTR", type = "scatter", mode = "lines+markers",
line = list(color = "#4CAF50", width = 2),
marker = list(color = "#4CAF50", size = 6),
yaxis = "y1", legendgroup = "Display",
hovertemplate = "Display CTR: %{y:.2%}<br>%{x}<extra></extra>") %>%
add_trace(data = filter(fig8_dat, channel == "Email"),
x = ~month_date, y = ~avg_ctr,
name = "Email — CTR", type = "scatter", mode = "lines+markers",
line = list(color = "#9C27B0", width = 2),
marker = list(color = "#9C27B0", size = 6),
yaxis = "y1", legendgroup = "Email",
hovertemplate = "Email CTR: %{y:.2%}<br>%{x}<extra></extra>") %>%
# --- ROI traces (secondary axis) ---
add_trace(data = filter(fig8_dat, channel == "Search"),
x = ~month_date, y = ~avg_roi,
name = "Search — ROI", type = "scatter", mode = "lines",
line = list(color = "#2196F3", width = 1.5, dash = "dot"),
yaxis = "y2", legendgroup = "Search", showlegend = FALSE,
hovertemplate = "Search ROI: %{y:.1f}%<br>%{x}<extra></extra>") %>%
add_trace(data = filter(fig8_dat, channel == "Social"),
x = ~month_date, y = ~avg_roi,
name = "Social — ROI", type = "scatter", mode = "lines",
line = list(color = "#FF5722", width = 1.5, dash = "dot"),
yaxis = "y2", legendgroup = "Social", showlegend = FALSE,
hovertemplate = "Social ROI: %{y:.1f}%<br>%{x}<extra></extra>") %>%
add_trace(data = filter(fig8_dat, channel == "Display"),
x = ~month_date, y = ~avg_roi,
name = "Display — ROI", type = "scatter", mode = "lines",
line = list(color = "#4CAF50", width = 1.5, dash = "dot"),
yaxis = "y2", legendgroup = "Display", showlegend = FALSE,
hovertemplate = "Display ROI: %{y:.1f}%<br>%{x}<extra></extra>") %>%
add_trace(data = filter(fig8_dat, channel == "Email"),
x = ~month_date, y = ~avg_roi,
name = "Email — ROI", type = "scatter", mode = "lines",
line = list(color = "#9C27B0", width = 1.5, dash = "dot"),
yaxis = "y2", legendgroup = "Email", showlegend = FALSE,
hovertemplate = "Email ROI: %{y:.1f}%<br>%{x}<extra></extra>") %>%
layout(
title = list(
text = "<b>Figure 8: Monthly CTR (solid) & ROI (dotted) by Channel — Interactive</b><br><sup>Hover for values. Click legend to toggle channels.</sup>",
font = list(size = 14)
),
xaxis = list(title = "", tickformat = "%b %Y", showgrid = FALSE),
yaxis = list(title = "Average CTR", tickformat = ".2%",
showgrid = TRUE, gridcolor = "#f0f0f0"),
yaxis2 = list(title = "Average ROI (%)", overlaying = "y", side = "right",
showgrid = FALSE, ticksuffix = "%"),
legend = list(orientation = "h", x = 0, y = -0.18),
hovermode = "x unified",
plot_bgcolor = "#ffffff",
paper_bgcolor = "#ffffff",
margin = list(t = 80, r = 80)
)
Interpretation: The interactive chart reveals that CTR and ROI trends do not always move in lockstep — months with peak CTR do not always produce the highest ROI, pointing to differences in conversion quality and revenue per conversion across periods. Users can toggle individual channels on/off to isolate comparisons.
| Figure | Type | Key Finding |
|---|---|---|
| 1 | Horizontal Bar Chart | Search has highest CTR (4.5%); Display lowest (0.8%) |
| 2 | Line Plot | Social & Search drive volume; Q4 seasonal spike across all channels |
| 3 | Violin + Box Plot | Email has widest ROI upside; Display most compressed near break-even |
| 4 | Faceted Scatterplot | Search & Email have best revenue-to-spend slopes |
| 5 | Heatmap | Weekdays > weekends; Oct–Nov peak engagement period |
| 6 | Faceted Bar Chart | 25–34 age segment delivers strongest ROI across most channels |
| 7 | Dumbbell Chart | Spend level barely moves Display CTR; Search & Email benefit from higher spend |
| 8 | Interactive Line Plot (plotly) | CTR and ROI trends diverge seasonally, highlighting conversion quality variation |
Report prepared by N Nikhil Kumar | Data Visualization Capstone