Skip to content

Commit 83cd660

Browse files
andyl7anThe Meridian Authors
authored and
The Meridian Authors
committed
[Summarizer] Add channel contribution area chart
PiperOrigin-RevId: 739326212
1 parent 875e0a5 commit 83cd660

File tree

6 files changed

+317
-89
lines changed

6 files changed

+317
-89
lines changed

CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ To release a new version (e.g. from `1.0.0` -> `2.0.0`):
2323

2424
## [Unreleased]
2525

26+
* Update contribution calculation methods in `MediaSummary` with
27+
`aggregate_times` parameter to support granular time.
28+
2629
* Add a `new_data` argument to `analyzer.optimal_freq()`.
2730

2831
## [1.0.7] - 2025-03-19

meridian/analysis/summarizer.py

+11-3
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,13 @@ def _create_outcome_contrib_card_html(
270270
) -> str:
271271
"""Creates the HTML snippet for the Outcome Contrib card."""
272272
outcome = self._kpi_or_revenue()
273+
channel_contribution_by_time_chart = formatter.ChartSpec(
274+
id=summary_text.CHANNEL_CONTRIB_BY_TIME_CHART_ID,
275+
description=summary_text.CHANNEL_CONTRIB_BY_TIME_CHART_DESCRIPTION.format(
276+
outcome=outcome
277+
),
278+
chart_json=media_summary.plot_channel_contribution_area_chart().to_json(),
279+
)
273280
channel_drivers_chart = formatter.ChartSpec(
274281
id=summary_text.CHANNEL_DRIVERS_CHART_ID,
275282
description=summary_text.CHANNEL_DRIVERS_CHART_DESCRIPTION.format(
@@ -305,6 +312,7 @@ def _create_outcome_contrib_card_html(
305312
CHANNEL_CONTRIB_CARD_SPEC,
306313
insights,
307314
[
315+
channel_contribution_by_time_chart,
308316
channel_drivers_chart,
309317
spend_outcome_chart,
310318
outcome_contribution_chart,
@@ -318,7 +326,7 @@ def _get_sorted_posterior_mean_metrics_df(
318326
ascending: bool = False,
319327
) -> pd.DataFrame:
320328
return (
321-
media_summary.paid_summary_metrics[metrics]
329+
media_summary.paid_summary_metrics()[metrics]
322330
.sel(distribution=c.POSTERIOR, metric=c.MEAN)
323331
.drop_sel(channel=c.ALL_CHANNELS)
324332
.to_dataframe()
@@ -334,7 +342,7 @@ def _get_sorted_posterior_median_metrics_df(
334342
ascending: bool = False,
335343
) -> pd.DataFrame:
336344
return (
337-
media_summary.paid_summary_metrics[metrics]
345+
media_summary.paid_summary_metrics()[metrics]
338346
.sel(distribution=c.POSTERIOR, metric=c.MEDIAN)
339347
.drop_sel(channel=c.ALL_CHANNELS)
340348
.to_dataframe()
@@ -479,7 +487,7 @@ def _select_optimal_rf_data(
479487
rf_channels = reach_frequency.optimal_frequency_data.rf_channel
480488
assert rf_channels.size > 0
481489
# This will raise KeyError if not all `rf_channels` can be found in here:
482-
rf_channel_spends = media_summary.paid_summary_metrics[c.SPEND].sel(
490+
rf_channel_spends = media_summary.paid_summary_metrics()[c.SPEND].sel(
483491
channel=rf_channels
484492
)
485493
most_spend_rf_channel = rf_channel_spends.idxmax()

meridian/analysis/summarizer_test.py

+27-3
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,9 @@ def _stub_plotters(self):
140140
self.reach_frequency.plot_optimal_frequency().to_json.return_value = '{}'
141141

142142
def _stub_media_summary_plotters(self, media_summary):
143+
media_summary.plot_channel_contribution_area_chart().to_json.return_value = (
144+
'{}'
145+
)
143146
media_summary.plot_contribution_waterfall_chart().to_json.return_value = (
144147
'{}'
145148
)
@@ -152,7 +155,9 @@ def _stub_media_summary_plotters(self, media_summary):
152155

153156
def _stub_for_insights(self):
154157
self.media_metrics = test_utils.generate_paid_summary_metrics()
155-
self.media_summary.paid_summary_metrics = self.media_metrics
158+
self.media_summary.paid_summary_metrics = mock.MagicMock(
159+
return_value=self.media_metrics
160+
)
156161

157162
frequency_data = test_utils.generate_optimal_frequency_data(
158163
channel_prefix='rf_ch', num_channels=2
@@ -365,6 +370,12 @@ def test_output_card_static_chart_spec(self, card_spec):
365370
(
366371
summary_text.CHANNEL_CONTRIB_CARD_ID,
367372
[
373+
(
374+
summary_text.CHANNEL_CONTRIB_BY_TIME_CHART_ID,
375+
summary_text.CHANNEL_CONTRIB_BY_TIME_CHART_DESCRIPTION.format(
376+
outcome=c.REVENUE
377+
),
378+
),
368379
(
369380
summary_text.CHANNEL_DRIVERS_CHART_ID,
370381
summary_text.CHANNEL_DRIVERS_CHART_DESCRIPTION.format(
@@ -781,6 +792,10 @@ def test_channel_contrib_card_plotters_called(self):
781792
media_summary.plot_contribution_pie_chart().to_json.return_value = (
782793
f'["{mock_spec_3}"]'
783794
)
795+
mock_spec_4 = 'revenue_area_chart'
796+
media_summary.plot_channel_contribution_area_chart().to_json.return_value = (
797+
f'["{mock_spec_4}"]'
798+
)
784799

785800
summary_html_dom = self._get_output_model_results_summary_html_dom(
786801
self.summarizer_revenue,
@@ -789,6 +804,7 @@ def test_channel_contrib_card_plotters_called(self):
789804
media_summary.plot_contribution_waterfall_chart(),
790805
media_summary.plot_spend_vs_contribution(),
791806
media_summary.plot_contribution_pie_chart(),
807+
media_summary.plot_channel_contribution_area_chart(),
792808
]:
793809
mock_plot.to_json.assert_called_once()
794810

@@ -812,8 +828,16 @@ def test_channel_contrib_card_plotters_called(self):
812828
mock_spec_3_exists = any(
813829
[mock_spec_3 in script_text for script_text in script_texts]
814830
)
831+
mock_spec_4_exists = any(
832+
[mock_spec_4 in script_text for script_text in script_texts]
833+
)
815834
self.assertTrue(
816-
all([mock_spec_1_exists, mock_spec_2_exists, mock_spec_3_exists])
835+
all([
836+
mock_spec_1_exists,
837+
mock_spec_2_exists,
838+
mock_spec_3_exists,
839+
mock_spec_4_exists,
840+
])
817841
)
818842

819843
def test_channel_contrib_card_insights(self):
@@ -998,7 +1022,7 @@ def test_response_curves_card_insights_multiple_channels(self):
9981022
media_summary = self.media_summary
9991023
reach_frequency = self.reach_frequency
10001024

1001-
media_summary.paid_summary_metrics.spend.data = [
1025+
media_summary.paid_summary_metrics().spend.data = [
10021026
100, # 'ch_0'
10031027
200, # 'ch_1'
10041028
300, # 'ch_2'

meridian/analysis/summary_text.py

+8
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,14 @@
4242
understand what drove your {outcome}. {lead_channels} drove the most overall
4343
{outcome}."""
4444

45+
CHANNEL_CONTRIB_BY_TIME_CHART_ID = 'channel-contrib-by-time-chart'
46+
CHANNEL_CONTRIB_BY_TIME_CHART_TITLE = (
47+
'Contribution by baseline and marketing channels'
48+
)
49+
CHANNEL_CONTRIB_BY_TIME_CHART_DESCRIPTION = """Note: This graphic encompasses all of
50+
your {outcome} drivers, but breaks down your marketing {outcome} by the baseline
51+
and all channels."""
52+
4553
CHANNEL_DRIVERS_CHART_ID = 'channel-drivers-chart'
4654
CHANNEL_DRIVERS_CHART_TITLE = 'Contribution by baseline and marketing channels'
4755
CHANNEL_DRIVERS_CHART_DESCRIPTION = """Note: This graphic encompasses all of

0 commit comments

Comments
 (0)