google-meridian 1.0.6__py3-none-any.whl → 1.0.8__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -270,6 +270,22 @@ class Summarizer:
270
270
  ) -> str:
271
271
  """Creates the HTML snippet for the Outcome Contrib card."""
272
272
  outcome = self._kpi_or_revenue()
273
+
274
+ channel_contrib_area_chart = formatter.ChartSpec(
275
+ id=summary_text.CHANNEL_CONTRIB_BY_TIME_CHART_ID,
276
+ description=summary_text.CHANNEL_CONTRIB_BY_TIME_CHART_DESCRIPTION.format(
277
+ outcome=outcome
278
+ ),
279
+ chart_json=media_summary.plot_channel_contribution_area_chart().to_json(),
280
+ )
281
+
282
+ channel_contrib_bump_chart = formatter.ChartSpec(
283
+ id=summary_text.CHANNEL_CONTRIB_RANK_CHART_ID,
284
+ description=summary_text.CHANNEL_CONTRIB_RANK_CHART_DESCRIPTION.format(
285
+ outcome=outcome
286
+ ),
287
+ chart_json=media_summary.plot_channel_contribution_bump_chart().to_json(),
288
+ )
273
289
  channel_drivers_chart = formatter.ChartSpec(
274
290
  id=summary_text.CHANNEL_DRIVERS_CHART_ID,
275
291
  description=summary_text.CHANNEL_DRIVERS_CHART_DESCRIPTION.format(
@@ -308,6 +324,8 @@ class Summarizer:
308
324
  channel_drivers_chart,
309
325
  spend_outcome_chart,
310
326
  outcome_contribution_chart,
327
+ channel_contrib_area_chart,
328
+ channel_contrib_bump_chart,
311
329
  ],
312
330
  )
313
331
 
@@ -318,7 +336,7 @@ class Summarizer:
318
336
  ascending: bool = False,
319
337
  ) -> pd.DataFrame:
320
338
  return (
321
- media_summary.paid_summary_metrics[metrics]
339
+ media_summary.get_paid_summary_metrics()[metrics]
322
340
  .sel(distribution=c.POSTERIOR, metric=c.MEAN)
323
341
  .drop_sel(channel=c.ALL_CHANNELS)
324
342
  .to_dataframe()
@@ -334,7 +352,7 @@ class Summarizer:
334
352
  ascending: bool = False,
335
353
  ) -> pd.DataFrame:
336
354
  return (
337
- media_summary.paid_summary_metrics[metrics]
355
+ media_summary.get_paid_summary_metrics()[metrics]
338
356
  .sel(distribution=c.POSTERIOR, metric=c.MEDIAN)
339
357
  .drop_sel(channel=c.ALL_CHANNELS)
340
358
  .to_dataframe()
@@ -479,7 +497,7 @@ class Summarizer:
479
497
  rf_channels = reach_frequency.optimal_frequency_data.rf_channel
480
498
  assert rf_channels.size > 0
481
499
  # 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(
500
+ rf_channel_spends = media_summary.get_paid_summary_metrics()[c.SPEND].sel(
483
501
  channel=rf_channels
484
502
  )
485
503
  most_spend_rf_channel = rf_channel_spends.idxmax()
@@ -20,7 +20,9 @@ MODEL_RESULTS_TITLE = 'Marketing Mix Modeling Report'
20
20
  MODEL_FIT_CARD_ID = 'model-fit'
21
21
  MODEL_FIT_CARD_TITLE = 'Model fit'
22
22
  MODEL_FIT_INSIGHTS_FORMAT = """Model fit is a measure of how well your MMM fits
23
- your current data used to train the model."""
23
+ the data used to train the model. The best model for causal inference may differ
24
+ from the best fitting model, because causal inference models must also estimate
25
+ the unobserved baseline."""
24
26
 
25
27
  EXPECTED_ACTUAL_OUTCOME_CHART_ID = 'expected-actual-outcome-chart'
26
28
  EXPECTED_ACTUAL_OUTCOME_CHART_TITLE = 'Expected {outcome} vs. actual {outcome}'
@@ -42,6 +44,23 @@ CHANNEL_CONTRIB_INSIGHTS_FORMAT = """Your channel contributions help you
42
44
  understand what drove your {outcome}. {lead_channels} drove the most overall
43
45
  {outcome}."""
44
46
 
47
+ CHANNEL_CONTRIB_BY_TIME_CHART_ID = 'channel-contrib-by-time-chart'
48
+ CHANNEL_CONTRIB_BY_TIME_CHART_TITLE = (
49
+ 'Contribution over time by baseline and marketing channels'
50
+ )
51
+ CHANNEL_CONTRIB_BY_TIME_CHART_DESCRIPTION = """Note: This chart shows the
52
+ estimated incremental {outcome} attributed to each channel and the baseline over
53
+ the selected time period. It helps visualize how contributions have changed."""
54
+
55
+ CHANNEL_CONTRIB_RANK_CHART_ID = 'channel-contrib-rank-chart'
56
+ CHANNEL_CONTRIB_RANK_CHART_TITLE = (
57
+ 'Contribution rank over time by baseline and marketing channels'
58
+ )
59
+ CHANNEL_CONTRIB_RANK_CHART_DESCRIPTION = """Note: This chart shows the relative
60
+ rank of each channel's contribution, including the baseline, based on
61
+ incremental {outcome} at the end of each quarter. Rank 1 represents the highest
62
+ contribution."""
63
+
45
64
  CHANNEL_DRIVERS_CHART_ID = 'channel-drivers-chart'
46
65
  CHANNEL_DRIVERS_CHART_TITLE = 'Contribution by baseline and marketing channels'
47
66
  CHANNEL_DRIVERS_CHART_DESCRIPTION = """Note: This graphic encompasses all of
@@ -28,6 +28,7 @@ limitations under the License.
28
28
  const opt = {
29
29
  mode: 'vega-lite',
30
30
  width: 'container',
31
+ autosize: { type: 'fit', contains: 'padding' }
31
32
  };
32
33
  const spec = JSON.parse({{ chart_json|tojson }});
33
34
  const chartDiv = document.getElementById('{{ id }}');
@@ -1813,32 +1813,6 @@ MROI_MEDIA_AND_RF_USE_POSTERIOR_BY_REACH = np.array([
1813
1813
  [0.2282, 0.3271, 0.1354, 1.8194, 0.5598],
1814
1814
  ],
1815
1815
  ])
1816
- MROI_MEDIA_AND_RF_NEW_TIMES_DATA = np.array([
1817
- [
1818
- [1.4194793, 1.4562954, 0.42110616, 0.46693847, 2.224705],
1819
- [1.4079778, 1.4408227, 0.42190424, 0.46729392, 2.2684476],
1820
- [1.4133729, 1.455379, 0.42965493, 0.46928722, 2.2630167],
1821
- [1.3786758, 1.4669218, 0.4399548, 0.466466, 2.2499163],
1822
- [1.3667594, 1.4595027, 0.430115, 0.46791035, 2.2718313],
1823
- [1.3636469, 1.4838042, 0.43774834, 0.46620503, 2.228661],
1824
- [1.3479362, 1.5108197, 0.42130333, 0.46699694, 2.2348554],
1825
- [1.3684787, 1.5252702, 0.4246083, 0.4682973, 2.2124028],
1826
- [1.3500556, 1.5139565, 0.42371163, 0.46387878, 2.2052019],
1827
- [1.3509449, 1.5152782, 0.42376328, 0.46421173, 2.2056532],
1828
- ],
1829
- [
1830
- [0.19265468, 0.3131754, 0.11835674, 1.7264867, 0.45867893],
1831
- [0.19271582, 0.31310934, 0.11833327, 1.7265227, 0.45878735],
1832
- [0.1927399, 0.3133164, 0.11834618, 1.7260367, 0.45711297],
1833
- [0.19282141, 0.31354108, 0.11858677, 1.724129, 0.45773327],
1834
- [0.19184875, 0.31352347, 0.11844476, 1.724147, 0.4579198],
1835
- [0.19213778, 0.3136204, 0.11846119, 1.7245249, 0.4575424],
1836
- [0.19298446, 0.3144178, 0.11840369, 1.7243268, 0.45999327],
1837
- [0.19473709, 0.3146425, 0.1182382, 1.7254068, 0.46479526],
1838
- [0.19482231, 0.31446627, 0.11806685, 1.7242908, 0.46568453],
1839
- [0.19479823, 0.31506982, 0.11783329, 1.7254248, 0.46606192],
1840
- ],
1841
- ])
1842
1816
  MROI_MEDIA_ONLY_USE_PRIOR = np.array([[
1843
1817
  [1.0740, 1.3019, 0.7984],
1844
1818
  [0.8990, 0.4201, 0.7120],
@@ -2025,32 +1999,6 @@ SAMPLE_ROI_NEW_DATA = np.array([
2025
1999
  [4.11828271, 1.58598067],
2026
2000
  ],
2027
2001
  ])
2028
- ROI_NEW_TIMES_DATA = np.array([
2029
- [
2030
- [1.6587073, 1.6422542, 0.5339541, 0.46695647, 2.2247107],
2031
- [1.6448834, 1.6238545, 0.5349834, 0.46725115, 2.268456],
2032
- [1.6512104, 1.6399834, 0.54364294, 0.46939594, 2.2630484],
2033
- [1.6124083, 1.6505513, 0.55594736, 0.46645072, 2.2499137],
2034
- [1.597995, 1.6415955, 0.5437353, 0.46789172, 2.2718024],
2035
- [1.5946435, 1.6684949, 0.5538136, 0.46621037, 2.2286298],
2036
- [1.5757265, 1.6986614, 0.5317232, 0.4669749, 2.2348094],
2037
- [1.5993005, 1.7154359, 0.535261, 0.4682574, 2.2124054],
2038
- [1.5770503, 1.7027655, 0.5336275, 0.4639002, 2.2051802],
2039
- [1.5781163, 1.704347, 0.53381217, 0.4641966, 2.2056897],
2040
- ],
2041
- [
2042
- [0.22893494, 0.54465544, 0.17262405, 1.726445, 0.4586947],
2043
- [0.22897178, 0.54465, 0.17261527, 1.7264631, 0.45878458],
2044
- [0.22901268, 0.5448365, 0.17263621, 1.7260604, 0.45709604],
2045
- [0.22900137, 0.54496145, 0.17280822, 1.7242719, 0.4577607],
2046
- [0.22795935, 0.54490256, 0.17272605, 1.7241275, 0.4579781],
2047
- [0.22818668, 0.5450325, 0.1726518, 1.7245975, 0.45759043],
2048
- [0.22910665, 0.5459215, 0.17257686, 1.7243998, 0.4600002],
2049
- [0.23114449, 0.54561883, 0.17233835, 1.7253716, 0.46480042],
2050
- [0.23121877, 0.5451343, 0.17214388, 1.7242151, 0.46571606],
2051
- [0.23116706, 0.54571074, 0.17181544, 1.7254194, 0.4660912],
2052
- ],
2053
- ])
2054
2002
  SAMPLE_ROI_KPI = np.array([
2055
2003
  [
2056
2004
  [0.4906, 0.3487],
@@ -2617,9 +2565,12 @@ SAMPLE_BASELINE_PCT_OF_CONTRIBUTION_NON_PAID = np.array([
2617
2565
  [-1.204522e-02, -4.079909e02],
2618
2566
  [1.350970e-02, -2.750774e02],
2619
2567
  ])
2620
- ADSTOCK_DECAY_CI_HI = np.array([1.0, 1.0, 0.8295, 0.9728, 0.6880])
2621
- ADSTOCK_DECAY_CI_LO = np.array([1.0, 1.0, 0.8128, 0.6194, 0.6607])
2622
- ADSTOCK_DECAY_MEAN = np.array([1.0, 1.0, 0.8214, 0.8359, 0.6748])
2568
+ ADSTOCK_DECAY_CI_HI = np.array([1.0, 1.0, 0.8658, 0.9709, 0.7496])
2569
+ ADSTOCK_DECAY_CI_LO = np.array([1.0, 1.0, 0.8328, 0.5749, 0.6936])
2570
+ ADSTOCK_DECAY_MEAN = np.array([1.0, 1.0, 0.8493, 0.8630, 0.7215])
2571
+ ORGANIC_ADSTOCK_DECAY_CI_HI = np.array([1.0, 0.9636, 0.9291, 0.8962, 0.8650])
2572
+ ORGANIC_ADSTOCK_DECAY_CI_LO = np.array([1.0, 0.6623, 0.4394, 0.2920, 0.1944])
2573
+ ORGANIC_ADSTOCK_DECAY_MEAN = np.array([1.0, 0.8076, 0.6633, 0.5537, 0.4693])
2623
2574
  HILL_CURVES_CI_HI = np.array([0.0, 0.0, 0.00098, 0.00895, 0.00195])
2624
2575
  HILL_CURVES_CI_LO = np.array([0.0, 0.0, 0.00085, 0.00322, 0.00169])
2625
2576
  HILL_CURVES_MEAN = np.array([0.0, 0.0, 0.00091, 0.00606, 0.00183])
@@ -2864,16 +2815,13 @@ def generate_predictive_accuracy_table(
2864
2815
  shape = [len(metric), len(geo_granularity)]
2865
2816
  dims = [c.METRIC, c.GEO_GRANULARITY]
2866
2817
  coords = {
2867
- c.METRIC: ([c.METRIC], metric),
2868
- c.GEO_GRANULARITY: ([c.GEO_GRANULARITY], geo_granularity),
2818
+ c.METRIC: metric,
2819
+ c.GEO_GRANULARITY: geo_granularity,
2869
2820
  }
2870
2821
  if with_holdout:
2871
2822
  shape.append(len(evaluation_set))
2872
2823
  dims.append(c.EVALUATION_SET_VAR)
2873
- coords[c.EVALUATION_SET_VAR] = (
2874
- [c.EVALUATION_SET_VAR],
2875
- evaluation_set,
2876
- )
2824
+ coords[c.EVALUATION_SET_VAR] = evaluation_set
2877
2825
  np.random.seed(0)
2878
2826
  value = np.random.lognormal(0, 1, size=shape)
2879
2827
  ds = xr.Dataset(
@@ -2969,7 +2917,7 @@ def generate_paid_summary_metrics() -> xr.Dataset:
2969
2917
  )
2970
2918
 
2971
2919
 
2972
- def generate_all_summary_metrics() -> xr.Dataset:
2920
+ def generate_all_summary_metrics(aggregate_times: bool = True) -> xr.Dataset:
2973
2921
  """Helper method to generate simulated summary metrics data."""
2974
2922
  channel = (
2975
2923
  [f"ch_{i}" for i in range(3)]
@@ -2981,33 +2929,38 @@ def generate_all_summary_metrics() -> xr.Dataset:
2981
2929
  channel.append(c.ALL_CHANNELS)
2982
2930
  metric = [c.MEAN, c.MEDIAN, c.CI_LO, c.CI_HI]
2983
2931
  distribution = [c.PRIOR, c.POSTERIOR]
2932
+ time = pd.date_range("2023-01-01", freq="W-SUN", periods=5).format(
2933
+ formatter=lambda x: x.strftime("%Y-%m-%d")
2934
+ )
2984
2935
 
2985
2936
  np.random.seed(0)
2986
- shape = (len(channel), len(metric), len(distribution))
2937
+
2938
+ if aggregate_times:
2939
+ shape = (len(channel), len(metric), len(distribution))
2940
+ dims = [c.CHANNEL, c.METRIC, c.DISTRIBUTION]
2941
+ else:
2942
+ shape = (len(time), len(channel), len(metric), len(distribution))
2943
+ dims = [c.TIME, c.CHANNEL, c.METRIC, c.DISTRIBUTION]
2944
+
2987
2945
  incremental_outcome = np.random.lognormal(10, 1, size=shape)
2988
2946
  effectiveness = np.random.lognormal(1, 1, size=shape)
2989
2947
  pct_of_contribution = np.random.randint(low=0, high=50, size=shape)
2990
2948
 
2949
+ coords = {
2950
+ c.CHANNEL: channel,
2951
+ c.METRIC: metric,
2952
+ c.DISTRIBUTION: distribution,
2953
+ }
2954
+ if not aggregate_times:
2955
+ coords[c.TIME] = time
2956
+
2991
2957
  return xr.Dataset(
2992
2958
  data_vars={
2993
- c.INCREMENTAL_OUTCOME: (
2994
- [c.CHANNEL, c.METRIC, c.DISTRIBUTION],
2995
- incremental_outcome,
2996
- ),
2997
- c.PCT_OF_CONTRIBUTION: (
2998
- [c.CHANNEL, c.METRIC, c.DISTRIBUTION],
2999
- pct_of_contribution,
3000
- ),
3001
- c.EFFECTIVENESS: (
3002
- [c.CHANNEL, c.METRIC, c.DISTRIBUTION],
3003
- effectiveness,
3004
- ),
3005
- },
3006
- coords={
3007
- c.CHANNEL: channel,
3008
- c.METRIC: metric,
3009
- c.DISTRIBUTION: distribution,
2959
+ c.INCREMENTAL_OUTCOME: (dims, incremental_outcome),
2960
+ c.PCT_OF_CONTRIBUTION: (dims, pct_of_contribution),
2961
+ c.EFFECTIVENESS: (dims, effectiveness),
3010
2962
  },
2963
+ coords=coords,
3011
2964
  attrs={c.CONFIDENCE_LEVEL: c.DEFAULT_CONFIDENCE_LEVEL},
3012
2965
  )
3013
2966
 
@@ -3063,14 +3016,8 @@ def generate_predictive_accuracy_data(holdout_id: bool = False) -> xr.Dataset:
3063
3016
 
3064
3017
  xr_dims = [c.METRIC, c.GEO_GRANULARITY]
3065
3018
  xr_coords = {
3066
- c.METRIC: (
3067
- [c.METRIC],
3068
- [c.R_SQUARED, c.MAPE, c.WMAPE],
3069
- ),
3070
- c.GEO_GRANULARITY: (
3071
- [c.GEO_GRANULARITY],
3072
- [c.GEO, c.NATIONAL],
3073
- ),
3019
+ c.METRIC: [c.R_SQUARED, c.MAPE, c.WMAPE],
3020
+ c.GEO_GRANULARITY: [c.GEO, c.NATIONAL],
3074
3021
  }
3075
3022
  rsquared_arr = [np.random.uniform(0.0, 1.0) for _ in range(2)]
3076
3023
  mape_arr = [np.random.uniform(0.0, 1.0) for _ in range(2)]
@@ -3095,10 +3042,7 @@ def generate_predictive_accuracy_data(holdout_id: bool = False) -> xr.Dataset:
3095
3042
  )
3096
3043
 
3097
3044
  xr_dims.append(c.EVALUATION_SET_VAR)
3098
- xr_coords[c.EVALUATION_SET_VAR] = (
3099
- [c.EVALUATION_SET_VAR],
3100
- list(c.EVALUATION_SET),
3101
- )
3045
+ xr_coords[c.EVALUATION_SET_VAR] = list(c.EVALUATION_SET)
3102
3046
  xr_data = {c.VALUE: (xr_dims, stacked_total)}
3103
3047
 
3104
3048
  return xr.Dataset(data_vars=xr_data, coords=xr_coords)
@@ -3194,18 +3138,22 @@ def generate_optimal_frequency_data(
3194
3138
 
3195
3139
  def generate_hill_curves_dataframe() -> pd.DataFrame:
3196
3140
  """Helper method to generate simulated hill curve data."""
3197
- channel_names = [f"ch_{i}" for i in range(3)] + [
3198
- f"rf_ch_{i}" for i in range(2)
3199
- ]
3141
+ channel_names = (
3142
+ [f"ch_{i}" for i in range(3)]
3143
+ + [f"rf_ch_{i}" for i in range(2)]
3144
+ + [f"organic_ch_{i}" for i in range(2)]
3145
+ )
3200
3146
  channel_array = []
3201
3147
  channel_type_array = []
3202
- for i, channel in enumerate(channel_names):
3148
+ for channel_name in channel_names:
3203
3149
  for _ in range(100):
3204
- channel_array.append(channel)
3205
- if i <= 3:
3150
+ channel_array.append(channel_name)
3151
+ if channel_name.startswith("ch_"):
3206
3152
  channel_type_array.append(c.MEDIA)
3207
- else:
3153
+ elif channel_name.startswith("rf_ch_"):
3208
3154
  channel_type_array.append(c.RF)
3155
+ elif channel_name.startswith("organic_ch_"):
3156
+ channel_type_array.append(c.ORGANIC_MEDIA)
3209
3157
 
3210
3158
  np.random.seed(0)
3211
3159
  media_units_array = [