@@ -33,6 +33,11 @@ def __init__(self, cube, dimensions, slice_idx):
3333 self ._dimensions = dimensions
3434 self ._slice_idx = slice_idx
3535
36+ @lazyproperty
37+ def audience_ratio (self ):
38+ """_AudienceRatio measure object for this cube-result."""
39+ return _AudienceRatio (self ._dimensions , self , self ._cube_measures )
40+
3641 @lazyproperty
3742 def column_comparable_counts (self ):
3843 """_ColumnComparableCounts measure object for this cube-result."""
@@ -830,6 +835,76 @@ def _weighted_base_blocks(self):
830835 return self ._second_order_measures .column_weighted_bases .blocks
831836
832837
838+ class _AudienceRatio (_ColumnProportions ):
839+ """Provides the audience-ratio measure for a matrix.
840+
841+ This measure (also known as "Index") is a 2D np.float64 ndarray of the ratio of the
842+ proportions of the first column to the proportions of the second column.
843+
844+ Audience ratio (Index) is a convenient way of showing the ratio of the Target % and
845+ the Control % when we have a compared group analysis (aka profiles analysis).
846+
847+ NOTE: At the moment we only support 2 compare groups, means that we can only have 2
848+ column max in the analysis. In case of ncol>2 we will return a 2D ndarray of nans as
849+ if it were a non-valid audience ratio.
850+ """
851+
852+ @lazyproperty
853+ def _base_values (self ):
854+ """2D ndarray np.float64 of the base values of the audience ratio"""
855+ # --- do not propagate divide-by-zero warnings to stderr ---
856+ with np .errstate (divide = "ignore" , invalid = "ignore" ):
857+ if not self ._can_compute_measure :
858+ return np .full (self ._count_blocks [0 ][0 ].shape , np .nan )
859+ base_values = self ._count_blocks [0 ][0 ] / self ._weighted_base_blocks [0 ][0 ]
860+ values = (base_values [:, 0 ] / base_values [:, 1 ]) * 100
861+ return np .column_stack ((values , np .full_like (values , np .nan )))
862+
863+ @lazyproperty
864+ def _can_compute_measure (self ):
865+ """Bool indicating whether audience ratio is computable.
866+
867+ If there are more than 2 columns, we return nans as a non-valid measure values
868+ """
869+ return False if self ._count_blocks [0 ][0 ].shape [- 1 ] != 2 else True
870+
871+ @lazyproperty
872+ def _intersections (self ):
873+ """(n_row_subtotals, n_col_subtotals) ndarray of intersection values.
874+
875+ An intersection value arises where a row-subtotal crosses a column-subtotal.
876+
877+ Always nan because the audience ratio is not defined for the intersection
878+ """
879+ # --- do not propagate divide-by-zero warnings to stderr ---
880+ return np .full (self ._count_blocks [1 ][1 ].shape , np .nan )
881+
882+ @lazyproperty
883+ def _subtotal_columns (self ):
884+ """2D np.float64 ndarray of audience ratio values.
885+
886+ This is the second "block" and has the shape (n_rows, n_col_subtotals).
887+
888+ Always empty because the audience ratio has no subtotal columns
889+ """
890+ # --- do not propagate divide-by-zero warnings to stderr ---
891+ return np .empty (self ._count_blocks [0 ][1 ].shape )
892+
893+ @lazyproperty
894+ def _subtotal_rows (self ):
895+ """2D np.float64 ndarray of audience ratio values.
896+
897+ This is the third "block" and has the shape (n_row_subtotals, n_cols).
898+ """
899+ # --- do not propagate divide-by-zero warnings to stderr ---
900+ with np .errstate (divide = "ignore" , invalid = "ignore" ):
901+ if not self ._can_compute_measure :
902+ return np .full (self ._count_blocks [1 ][0 ].shape , np .nan )
903+ base_values = self ._count_blocks [1 ][0 ] / self ._weighted_base_blocks [1 ][0 ]
904+ subtotal_rows = (base_values [:, 0 ] / base_values [:, 1 ]) * 100
905+ return np .column_stack ((subtotal_rows , np .full_like (subtotal_rows , np .nan )))
906+
907+
833908class _ColumnProportionsSmoothed (_ColumnProportions , _SmoothedMeasure ):
834909 """Provides the smoothed column-proportions measure for a matrix.
835910
0 commit comments