prefix . 'aieo_reco_arm_daily'; $rows = array(); $source = 'rollup'; if (self::table_exists($rollup)) { $rows = (array) $wpdb->get_results($wpdb->prepare( "SELECT surface, arm, SUM(impressions) impressions, SUM(clicks) clicks, SUM(suppressions) suppressions, SUM(assisted_adds) assisted_adds, SUM(assisted_purchases) assisted_purchases, SUM(sessions) sessions, SUM(click_sessions) click_sessions, SUM(add_sessions) add_sessions FROM `{$rollup}` WHERE stat_date >= %s GROUP BY surface, arm", substr($since, 0, 10) ), ARRAY_A); } if (empty($rows)) { $rows = self::live_rows($since); $source = 'live'; } $by_surface = array(); $totals = array(); foreach ($rows as $r) { $surface = (string) $r['surface']; $arm = (string) $r['arm']; if (!isset($by_surface[$surface])) { $by_surface[$surface] = array(); } $by_surface[$surface][$arm] = self::measures($r); if (!isset($totals[$arm])) { $totals[$arm] = self::measures(array()); } foreach (array('impressions', 'clicks', 'suppressions', 'assisted_adds', 'assisted_purchases', 'sessions', 'click_sessions', 'add_sessions') as $k) { $totals[$arm][$k] += isset($r[$k]) ? (int) $r[$k] : 0; } } $arms_order = class_exists('AIEO_Reco_Experiment') && method_exists('AIEO_Reco_Experiment', 'arms_for') ? AIEO_Reco_Experiment::arms_for($config['subject']) : array(AIEO_Reco_Experiment::ARM_CONTROL, AIEO_Reco_Experiment::ARM_TREATMENT); $multi = count($arms_order) > 2; $surfaces = array(); foreach ($by_surface as $surface => $arms) { $surfaces[] = array( 'surface' => $surface, 'arms' => $arms, 'ctr' => self::compare($arms, 'click_sessions', 'sessions'), 'add' => self::compare($arms, 'add_sessions', 'sessions'), 'ctr_imp' => self::rates($arms, 'clicks', 'impressions'), 'add_imp' => self::rates($arms, 'assisted_adds', 'impressions'), 'pairs' => $multi ? self::arm_pairs($arms, $arms_order) : array(), ); } usort($surfaces, static function ($a, $b) { $an = self::arm_total($a['arms'], 'impressions'); $bn = self::arm_total($b['arms'], 'impressions'); return ($bn === $an) ? strcmp($a['surface'], $b['surface']) : (($bn > $an) ? 1 : -1); }); return array( 'config' => $config, 'since' => $since, 'source' => $source, 'surfaces' => $surfaces, 'totals' => $totals, 'arms_order' => $arms_order, 'pairs' => $multi ? self::arm_pairs($totals, $arms_order) : array(), 'ctr' => self::compare($totals, 'click_sessions', 'sessions'), 'add' => self::compare($totals, 'add_sessions', 'sessions'), 'ctr_imp' => self::rates($totals, 'clicks', 'impressions'), 'add_imp' => self::rates($totals, 'assisted_adds', 'impressions'), 'unassigned' => isset($totals[AIEO_Reco_Experiment::ARM_NONE]) ? $totals[AIEO_Reco_Experiment::ARM_NONE] : self::measures(array()), ); } public static function compare(array $arms, $num, $den) { $c = isset($arms[AIEO_Reco_Experiment::ARM_CONTROL]) ? $arms[AIEO_Reco_Experiment::ARM_CONTROL] : null; $t = isset($arms[AIEO_Reco_Experiment::ARM_TREATMENT]) ? $arms[AIEO_Reco_Experiment::ARM_TREATMENT] : null; $n1 = $c ? (int) $c[$den] : 0; $n2 = $t ? (int) $t[$den] : 0; $x1 = $c ? (int) $c[$num] : 0; $x2 = $t ? (int) $t[$num] : 0; $core = self::compare_counts($x1, $n1, $x2, $n2); return array( 'control_n' => $n1, 'treatment_n' => $n2, 'control' => $core['base'], 'treatment' => $core['other'], 'diff' => $core['diff'], 'ci_low' => $core['ci_low'], 'ci_high' => $core['ci_high'], 'significant' => $core['significant'], 'mde' => $core['mde'], 'verdict' => str_replace( array('other_wins', 'base_wins'), array('treatment_wins', 'control_wins'), $core['verdict'] ), ); } public static function compare_pair(array $arms, $num, $den, $base_key, $other_key) { $b = isset($arms[$base_key]) ? $arms[$base_key] : null; $o = isset($arms[$other_key]) ? $arms[$other_key] : null; $out = self::compare_counts( $b ? (int) $b[$num] : 0, $b ? (int) $b[$den] : 0, $o ? (int) $o[$num] : 0, $o ? (int) $o[$den] : 0 ); $out['base_arm'] = $base_key; $out['other_arm'] = $other_key; return $out; } public static function compare_counts($x1, $n1, $x2, $n2) { $out = array( 'base_n' => $n1, 'other_n' => $n2, 'base' => ($n1 > 0) ? $x1 / $n1 : null, 'other' => ($n2 > 0) ? $x2 / $n2 : null, 'diff' => null, 'ci_low' => null, 'ci_high' => null, 'significant' => false, 'mde' => null, 'verdict' => 'no_data', ); if ($n1 < 1 || $n2 < 1) { return $out; } $p1 = $x1 / $n1; $p2 = $x2 / $n2; $d = $p2 - $p1; $se = sqrt(($p1 * (1 - $p1) / $n1) + ($p2 * (1 - $p2) / $n2)); $out['diff'] = $d; $out['ci_low'] = $d - self::Z_CI * $se; $out['ci_high'] = $d + self::Z_CI * $se; $out['significant'] = ($out['ci_low'] > 0) || ($out['ci_high'] < 0); $pbar = ($x1 + $x2) / ($n1 + $n2); $harmonic_n = (2 * $n1 * $n2) / ($n1 + $n2); $out['mde'] = ($harmonic_n > 0 && $pbar > 0 && $pbar < 1) ? self::Z_MDE * sqrt(2 * $pbar * (1 - $pbar) / $harmonic_n) : null; if ($out['significant']) { $out['verdict'] = ($d > 0) ? 'other_wins' : 'base_wins'; } elseif (null !== $out['mde'] && abs($d) < $out['mde']) { $out['verdict'] = 'underpowered'; } else { $out['verdict'] = 'no_difference'; } return $out; } private static function arm_pairs(array $arms, array $order) { $base = $order[0]; $out = array(); foreach (array_slice($order, 1) as $other) { $out[] = array( 'other' => $other, 'ctr' => self::compare_pair($arms, 'click_sessions', 'sessions', $base, $other), 'add' => self::compare_pair($arms, 'add_sessions', 'sessions', $base, $other), ); } return $out; } private static function measures(array $r) { return array( 'impressions' => isset($r['impressions']) ? (int) $r['impressions'] : 0, 'clicks' => isset($r['clicks']) ? (int) $r['clicks'] : 0, 'suppressions' => isset($r['suppressions']) ? (int) $r['suppressions'] : 0, 'assisted_adds' => isset($r['assisted_adds']) ? (int) $r['assisted_adds'] : 0, 'assisted_purchases' => isset($r['assisted_purchases']) ? (int) $r['assisted_purchases'] : 0, 'sessions' => isset($r['sessions']) ? (int) $r['sessions'] : 0, 'click_sessions' => isset($r['click_sessions']) ? (int) $r['click_sessions'] : 0, 'add_sessions' => isset($r['add_sessions']) ? (int) $r['add_sessions'] : 0, ); } private static function arm_total(array $arms, $key) { $n = 0; foreach ($arms as $arm => $m) { if (AIEO_Reco_Experiment::ARM_NONE === $arm) { continue; } $n += (int) $m[$key]; } return $n; } private static function live_rows($since) { global $wpdb; $reco = $wpdb->prefix . 'aieo_reco_events'; $addcart = $wpdb->prefix . 'aieo_addcart_events'; if (!self::table_exists($reco) || !self::table_exists($addcart)) { return array(); } if (!class_exists('AIEO_Display_Conditions')) { return array(); } $shown = AIEO_Display_Conditions::raw_types_sql('shown'); $engaged = AIEO_Display_Conditions::raw_types_sql('engaged'); $suppressed = AIEO_Display_Conditions::raw_types_sql('suppressed'); return (array) $wpdb->get_results($wpdb->prepare( "SELECT r.surface, r.arm, COUNT(DISTINCT CASE WHEN r.event_type IN ({$shown}) THEN r.id END) AS impressions, COUNT(DISTINCT CASE WHEN r.event_type IN ({$engaged}) THEN r.id END) AS clicks, COUNT(DISTINCT CASE WHEN r.event_type IN ({$suppressed}) THEN r.id END) AS suppressions, COUNT(DISTINCT CASE WHEN e.id IS NOT NULL THEN r.id END) AS assisted_adds, COUNT(DISTINCT CASE WHEN e.order_id > 0 THEN r.id END) AS assisted_purchases, -- Suppressed rows are the page views where the surface did -- NOT appear, so they can never contribute to a numerator. -- Counting them here would inflate every per-session rate. COUNT(DISTINCT CASE WHEN r.event_type NOT IN ({$suppressed}) THEN r.session_id END) AS sessions, COUNT(DISTINCT CASE WHEN r.event_type IN ({$engaged}) THEN r.session_id END) AS click_sessions, COUNT(DISTINCT CASE WHEN e.id IS NOT NULL THEN r.session_id END) AS add_sessions FROM `{$reco}` r LEFT JOIN `{$addcart}` e ON e.session_id = r.session_id AND e.product_id = r.target_pid AND e.ts_added > r.ts AND e.ts_added < DATE_ADD(r.ts, INTERVAL 7 DAY) AND r.event_type IN ('impression','reco_click') WHERE r.ts >= %s GROUP BY r.surface, r.arm", $since ), ARRAY_A); } private static function window_start(array $config, $days) { $days = max(1, (int) $days); $floor = gmdate('Y-m-d H:i:s', time() - $days * DAY_IN_SECONDS); if ('' !== $config['started_at'] && $config['started_at'] > $floor) { return $config['started_at']; } return $floor; } public static function rates(array $arms, $num, $den) { $c = isset($arms[AIEO_Reco_Experiment::ARM_CONTROL]) ? $arms[AIEO_Reco_Experiment::ARM_CONTROL] : null; $t = isset($arms[AIEO_Reco_Experiment::ARM_TREATMENT]) ? $arms[AIEO_Reco_Experiment::ARM_TREATMENT] : null; $n1 = $c ? (int) $c[$den] : 0; $n2 = $t ? (int) $t[$den] : 0; $p1 = ($n1 > 0) ? ((int) $c[$num]) / $n1 : null; $p2 = ($n2 > 0) ? ((int) $t[$num]) / $n2 : null; return array( 'control' => $p1, 'treatment' => $p2, 'control_n' => $n1, 'treatment_n' => $n2, 'diff' => (null !== $p1 && null !== $p2) ? ($p2 - $p1) : null, ); } private static function table_exists($table) { global $wpdb; return (string) $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $table)) === $table; } }