array(), 'rates' => array() ); global $wpdb; $t = $wpdb->prefix . 'aieo_order_products_analytics'; if ( ! self::table( $t ) ) { return self::$cache; } $cutoff = gmdate( 'Y-m-d H:i:s', time() - ( self::WINDOW_DAYS * DAY_IN_SECONDS ) ); $rows = $wpdb->get_results( $wpdb->prepare( "SELECT g.pid, g.orders, g.sole, g.opener, g.closer FROM ( SELECT l.product_id AS pid, COUNT(DISTINCT l.order_id) AS orders, COUNT(DISTINCT CASE WHEN o.n_products = 1 THEN l.order_id END) AS sole, COUNT(DISTINCT CASE WHEN o.n_products > 1 AND l.order_sequence = 1 THEN l.order_id END) AS opener, COUNT(DISTINCT CASE WHEN o.n_products > 1 AND l.order_sequence = o.max_seq THEN l.order_id END) AS closer FROM `{$t}` l INNER JOIN ( SELECT order_id, COUNT(DISTINCT product_id) AS n_products, MAX(order_sequence) AS max_seq FROM `{$t}` WHERE date_created_gmt >= %s GROUP BY order_id ) o ON o.order_id = l.order_id WHERE l.date_created_gmt >= %s AND l.product_id > 0 GROUP BY l.product_id HAVING COUNT(DISTINCT l.order_id) >= %d ) g INNER JOIN `{$wpdb->posts}` p ON p.ID = g.pid AND p.post_type = 'product' AND p.post_status = 'publish' ORDER BY g.orders DESC, g.pid ASC LIMIT %d", $cutoff, $cutoff, self::MIN_ORDERS, self::MAX_POPULATION ), ARRAY_A ); if ( ! $rows ) { return self::$cache; } if ( count( $rows ) >= self::MAX_POPULATION ) { error_log( sprintf( 'AIEO_Basket_Role: population cap reached (%d products); products beyond it carry no basket-role component.', self::MAX_POPULATION ) ); } $composite = array(); $detail = array(); foreach ( $rows as $r ) { $pid = (int) $r['pid']; $n = (int) $r['orders']; if ( $pid <= 0 || $n <= 0 ) { continue; } $sole = (float) $r['sole'] / $n; $opener = (float) $r['opener'] / $n; $closer = (float) $r['closer'] / $n; $composite[ $pid ] = ( self::W_SOLE * $sole ) + ( self::W_OPENER * $opener ) + ( self::W_CLOSER * $closer ); $detail[ $pid ] = array( 'orders' => $n, 'sole' => $sole, 'opener' => $opener, 'closer' => $closer, 'composite' => $composite[ $pid ], ); } if ( ! $composite ) { return self::$cache; } $catalogue_mean = array_sum( $composite ) / count( $composite ); if ( $catalogue_mean <= 0 ) { return self::$cache; } $assigned = self::assign_terms( array_keys( $composite ) ); $sum = array(); $cnt = array(); foreach ( $assigned as $pid => $tid ) { $sum[ $tid ] = ( isset( $sum[ $tid ] ) ? $sum[ $tid ] : 0.0 ) + $composite[ $pid ]; $cnt[ $tid ] = ( isset( $cnt[ $tid ] ) ? $cnt[ $tid ] : 0 ) + 1; } foreach ( $composite as $pid => $rate ) { $reference = $catalogue_mean; if ( isset( $assigned[ $pid ] ) ) { $tid = $assigned[ $pid ]; if ( isset( $cnt[ $tid ] ) && ( $cnt[ $tid ] - 1 ) >= self::CAT_MIN_PRODUCTS ) { $peer = $sum[ $tid ] - $rate; if ( $peer > 0 ) { $reference = $peer / ( $cnt[ $tid ] - 1 ); } } } if ( $reference <= 0 ) { $reference = $catalogue_mean; } $n = $detail[ $pid ]['orders']; $shrunk = ( ( $n * $rate ) + ( self::PRIOR_ORDERS * $reference ) ) / ( $n + self::PRIOR_ORDERS ); $lift = max( -self::MAX_LOG_LIFT, min( self::MAX_LOG_LIFT, log( $shrunk / $reference ) ) ); self::$cache['lifts'][ $pid ] = $lift; $detail[ $pid ]['reference'] = $reference; $detail[ $pid ]['lift'] = $lift; self::$cache['rates'][ $pid ] = $detail[ $pid ]; } return self::$cache; } private static function assign_terms( $pids ) { global $wpdb; if ( ! $pids ) { return array(); } $parent = array(); $tcount = array(); foreach ( (array) $wpdb->get_results( "SELECT term_id, parent, count FROM `{$wpdb->term_taxonomy}` WHERE taxonomy = 'product_cat'", ARRAY_A ) as $r ) { $parent[ (int) $r['term_id'] ] = (int) $r['parent']; $tcount[ (int) $r['term_id'] ] = (int) $r['count']; } if ( ! $parent ) { return array(); } $ph = implode( ',', array_fill( 0, count( $pids ), '%d' ) ); $rel = $wpdb->get_results( $wpdb->prepare( "SELECT tr.object_id AS pid, tt.term_id FROM `{$wpdb->term_relationships}` tr INNER JOIN `{$wpdb->term_taxonomy}` tt ON tt.term_taxonomy_id = tr.term_taxonomy_id WHERE tt.taxonomy = 'product_cat' AND tr.object_id IN ({$ph})", $pids ), ARRAY_A ); $assigned = array(); $depth = array(); foreach ( (array) $rel as $r ) { $pid = (int) $r['pid']; $tid = (int) $r['term_id']; if ( ! isset( $parent[ $tid ] ) ) { continue; } if ( ! isset( $depth[ $tid ] ) ) { $d = 0; $walk = $tid; $seen = array(); while ( ! empty( $parent[ $walk ] ) && ! isset( $seen[ $walk ] ) ) { $seen[ $walk ] = true; $walk = $parent[ $walk ]; $d++; } $depth[ $tid ] = $d; } if ( ! isset( $assigned[ $pid ] ) ) { $assigned[ $pid ] = $tid; continue; } $cur = $assigned[ $pid ]; if ( $depth[ $tid ] > $depth[ $cur ] || ( $depth[ $tid ] === $depth[ $cur ] && $tcount[ $tid ] > $tcount[ $cur ] ) || ( $depth[ $tid ] === $depth[ $cur ] && $tcount[ $tid ] === $tcount[ $cur ] && $tid < $cur ) ) { $assigned[ $pid ] = $tid; } } return $assigned; } private static function table( $t ) { global $wpdb; return (string) $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $t ) ) === $t; } }