PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.0.2
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.0.2
2.7.0 2.6.0 2.5.0 2.4.0 2.3.0 2.2.0 2.1.1 2.1.0 2.0.2 2.0.1 2.0.0 1.32.0 1.31.0 1.30.0 1.29.0 1.28.0 1.27.0 1.26.0 1.25.0 trunk 1.0.0 1.0.1 1.0.2 1.1.0 1.10.0 All 48 releases
thinkrank / includes / seo / class-seo-trend-analyzer.php

class-seo-trend-analyzer.php in ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO 2.0.2, at includes/seo/class-seo-trend-analyzer.php

671 lines 23.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * SEO Trend Analyzer Class
4 *
5 * Analyzes SEO performance trends from Google API data to identify patterns,
6 * changes, and growth opportunities. Processes traffic, keyword, and content
7 * performance data to generate intelligent trend insights.
8 *
9 * @package ThinkRank
10 * @subpackage SEO
11 * @since 1.0.0
12 */
13
14 declare(strict_types=1);
15
16 namespace ThinkRank\SEO;
17
18 // Prevent direct access
19 if (!defined('ABSPATH')) {
20 exit;
21 }
22
23 /**
24 * SEO Trend Analyzer Class
25 *
26 * Single Responsibility: Analyze SEO performance trends and patterns
27 * Following ThinkRank analyzer patterns from existing codebase
28 *
29 * @since 1.0.0
30 */
31 class SEO_Trend_Analyzer {
32
33 /**
34 * Minimum data points required for trend analysis
35 *
36 * @var int
37 */
38 private const MIN_DATA_POINTS = 2;
39
40 /**
41 * Significant change threshold percentage
42 *
43 * @var float
44 */
45 private const SIGNIFICANT_CHANGE_THRESHOLD = 5.0;
46
47 /**
48 * Analyze traffic trends from Google Analytics data
49 *
50 * @param array $current_data Current period traffic data
51 * @param array $historical_data Previous period traffic data
52 * @return array Traffic trend analysis
53 */
54 public function analyze_traffic_trends(array $current_data, array $historical_data): array {
55 $trends = [
56 'sessions' => $this->calculate_trend_metrics(
57 $historical_data['sessions'] ?? 0,
58 $current_data['sessions'] ?? 0,
59 'Sessions'
60 ),
61 'pageviews' => $this->calculate_trend_metrics(
62 $historical_data['pageviews'] ?? 0,
63 $current_data['pageviews'] ?? 0,
64 'Pageviews'
65 ),
66 'organic_traffic' => $this->analyze_organic_traffic_trend($current_data, $historical_data),
67 'bounce_rate' => $this->calculate_trend_metrics(
68 $historical_data['bounce_rate'] ?? 0,
69 $current_data['bounce_rate'] ?? 0,
70 'Bounce Rate',
71 true // Lower is better for bounce rate
72 ),
73 'avg_session_duration' => $this->calculate_trend_metrics(
74 $historical_data['avg_session_duration'] ?? 0,
75 $current_data['avg_session_duration'] ?? 0,
76 'Average Session Duration'
77 ),
78 'summary' => $this->generate_traffic_trend_summary($current_data, $historical_data)
79 ];
80
81 return $trends;
82 }
83
84 /**
85 * Analyze keyword performance trends from Search Console data
86 *
87 * @param array $search_console_data Search Console performance data
88 * @param string $date_range Date range for comparison
89 * @return array Keyword trend analysis
90 */
91 public function analyze_keyword_trends(array $search_console_data, string $date_range): array {
92 $trends = [
93 'total_clicks' => $this->extract_total_metrics($search_console_data, 'clicks'),
94 'total_impressions' => $this->extract_total_metrics($search_console_data, 'impressions'),
95 'average_ctr' => $this->calculate_average_ctr($search_console_data),
96 'average_position' => $this->calculate_average_position($search_console_data),
97 'top_gaining_keywords' => $this->identify_gaining_keywords($search_console_data),
98 'top_losing_keywords' => $this->identify_losing_keywords($search_console_data),
99 'new_keywords' => $this->identify_new_keywords($search_console_data),
100 'summary' => $this->generate_keyword_trend_summary($search_console_data)
101 ];
102
103 return $trends;
104 }
105
106 /**
107 * Analyze content performance trends
108 *
109 * @param array $analytics_data Google Analytics data
110 * @param array $search_data Search Console data
111 * @return array Content trend analysis
112 */
113 public function analyze_content_trends(array $analytics_data, array $search_data): array {
114 $trends = [
115 'top_gaining_pages' => $this->identify_gaining_pages($analytics_data, $search_data),
116 'top_losing_pages' => $this->identify_losing_pages($analytics_data, $search_data),
117 'mobile_vs_desktop' => $this->analyze_device_trends($search_data),
118 'content_type_performance' => $this->analyze_content_type_performance($analytics_data),
119 'summary' => $this->generate_content_trend_summary($analytics_data, $search_data)
120 ];
121
122 return $trends;
123 }
124
125 /**
126 * Calculate growth rates between current and previous periods
127 *
128 * @param array $current Current period data
129 * @param array $previous Previous period data
130 * @return array Growth rate calculations
131 */
132 public function calculate_growth_rates(array $current, array $previous): array {
133 $growth_rates = [];
134
135 foreach ($current as $metric => $current_value) {
136 $previous_value = $previous[$metric] ?? 0;
137 $growth_rates[$metric] = $this->calculate_percentage_change($previous_value, $current_value);
138 }
139
140 return $growth_rates;
141 }
142
143 /**
144 * Detect seasonal patterns in historical data
145 *
146 * @param array $historical_data Historical performance data
147 * @return array Seasonal pattern analysis
148 */
149 public function detect_seasonal_patterns(array $historical_data): array {
150 if (count($historical_data) < 12) {
151 return [
152 'patterns_detected' => false,
153 'message' => 'Insufficient data for seasonal analysis (minimum 12 data points required)'
154 ];
155 }
156
157 $patterns = [
158 'patterns_detected' => true,
159 'monthly_trends' => $this->analyze_monthly_patterns($historical_data),
160 'weekly_trends' => $this->analyze_weekly_patterns($historical_data),
161 'recommendations' => $this->generate_seasonal_recommendations($historical_data)
162 ];
163
164 return $patterns;
165 }
166
167 /**
168 * Calculate trend metrics for a specific metric
169 *
170 * @param float $previous Previous period value
171 * @param float $current Current period value
172 * @param string $metric_name Human-readable metric name
173 * @param bool $lower_is_better Whether lower values are better (e.g., bounce rate)
174 * @return array Trend metrics
175 */
176 private function calculate_trend_metrics(float $previous, float $current, string $metric_name, bool $lower_is_better = false): array {
177 $change = $current - $previous;
178 $percentage_change = $this->calculate_percentage_change($previous, $current);
179
180 $trend_direction = $this->determine_trend_direction($percentage_change, $lower_is_better);
181 $significance = $this->determine_significance($percentage_change);
182
183 return [
184 'previous' => $previous,
185 'current' => $current,
186 'change' => $change,
187 'percentage_change' => $percentage_change,
188 'trend_direction' => $trend_direction,
189 'significance' => $significance,
190 'metric_name' => $metric_name,
191 'interpretation' => $this->generate_trend_interpretation($metric_name, $percentage_change, $trend_direction)
192 ];
193 }
194
195 /**
196 * Calculate percentage change between two values
197 *
198 * @param float $previous Previous value
199 * @param float $current Current value
200 * @return float Percentage change
201 */
202 private function calculate_percentage_change(float $previous, float $current): float {
203 if ((float) $previous === 0.0) {
204 return $current > 0 ? 100.0 : 0.0;
205 }
206
207 return (($current - $previous) / $previous) * 100;
208 }
209
210 /**
211 * Determine trend direction
212 *
213 * @param float $percentage_change Percentage change value
214 * @param bool $lower_is_better Whether lower values are better
215 * @return string Trend direction (up, down, stable)
216 */
217 private function determine_trend_direction(float $percentage_change, bool $lower_is_better = false): string {
218 if (abs($percentage_change) < self::SIGNIFICANT_CHANGE_THRESHOLD) {
219 return 'stable';
220 }
221
222 if ($lower_is_better) {
223 return $percentage_change > 0 ? 'down' : 'up';
224 }
225
226 return $percentage_change > 0 ? 'up' : 'down';
227 }
228
229 /**
230 * Determine significance of change
231 *
232 * @param float $percentage_change Percentage change value
233 * @return string Significance level (high, medium, low)
234 */
235 private function determine_significance(float $percentage_change): string {
236 $abs_change = abs($percentage_change);
237
238 if ($abs_change >= 20) {
239 return 'high';
240 } elseif ($abs_change >= 10) {
241 return 'medium';
242 } else {
243 return 'low';
244 }
245 }
246
247 /**
248 * Generate trend interpretation text
249 *
250 * @param string $metric_name Metric name
251 * @param float $percentage_change Percentage change
252 * @param string $trend_direction Trend direction
253 * @return string Human-readable interpretation
254 */
255 private function generate_trend_interpretation(string $metric_name, float $percentage_change, string $trend_direction): string {
256 $abs_change = abs($percentage_change);
257
258 switch ($trend_direction) {
259 case 'up':
260 return sprintf('%s increased by %.1f%%', $metric_name, $abs_change);
261 case 'down':
262 return sprintf('%s decreased by %.1f%%', $metric_name, $abs_change);
263 case 'stable':
264 return sprintf('%s remained stable (%.1f%% change)', $metric_name, $percentage_change);
265 default:
266 return sprintf('%s changed by %.1f%%', $metric_name, $percentage_change);
267 }
268 }
269
270 /**
271 * Analyze organic traffic trend specifically
272 *
273 * @param array $current_data Current period data
274 * @param array $historical_data Historical period data
275 * @return array Organic traffic trend analysis
276 */
277 private function analyze_organic_traffic_trend(array $current_data, array $historical_data): array {
278 $current_organic = $current_data['organic_traffic']['organic_traffic'] ?? [];
279 $historical_organic = $historical_data['organic_traffic']['organic_traffic'] ?? [];
280
281 return $this->calculate_trend_metrics(
282 $historical_organic['sessions'] ?? 0,
283 $current_organic['sessions'] ?? 0,
284 'Organic Traffic Sessions'
285 );
286 }
287
288 /**
289 * Extract total metrics from Search Console data
290 *
291 * @param array $search_console_data Search Console data
292 * @param string $metric Metric to extract (clicks, impressions)
293 * @return array Total metrics with trend data
294 */
295 private function extract_total_metrics(array $search_console_data, string $metric): array {
296 $total = 0;
297 $rows = $search_console_data['rows'] ?? [];
298
299 foreach ($rows as $row) {
300 $total += $row[$metric] ?? 0;
301 }
302
303 return [
304 'total' => $total,
305 'metric' => $metric,
306 'data_points' => count($rows)
307 ];
308 }
309
310 /**
311 * Calculate average CTR from Search Console data
312 *
313 * @param array $search_console_data Search Console data
314 * @return array Average CTR analysis
315 */
316 private function calculate_average_ctr(array $search_console_data): array {
317 $rows = $search_console_data['rows'] ?? [];
318 $total_clicks = 0;
319 $total_impressions = 0;
320
321 foreach ($rows as $row) {
322 $total_clicks += $row['clicks'] ?? 0;
323 $total_impressions += $row['impressions'] ?? 0;
324 }
325
326 $average_ctr = $total_impressions > 0 ? ($total_clicks / $total_impressions) * 100 : 0;
327
328 return [
329 'average_ctr' => round($average_ctr, 2),
330 'total_clicks' => $total_clicks,
331 'total_impressions' => $total_impressions
332 ];
333 }
334
335 /**
336 * Calculate average position from Search Console data
337 *
338 * @param array $search_console_data Search Console data
339 * @return array Average position analysis
340 */
341 private function calculate_average_position(array $search_console_data): array {
342 $rows = $search_console_data['rows'] ?? [];
343 $total_position = 0;
344 $count = 0;
345
346 foreach ($rows as $row) {
347 if (isset($row['position']) && $row['position'] > 0) {
348 $total_position += $row['position'];
349 $count++;
350 }
351 }
352
353 $average_position = $count > 0 ? $total_position / $count : 0;
354
355 return [
356 'average_position' => round($average_position, 1),
357 'keywords_tracked' => $count
358 ];
359 }
360
361 /**
362 * Identify gaining keywords (improved performance)
363 *
364 * @param array $search_console_data Search Console data
365 * @return array Top gaining keywords
366 */
367 private function identify_gaining_keywords(array $search_console_data): array {
368 $rows = $search_console_data['rows'] ?? [];
369 $gaining_keywords = [];
370
371 foreach ($rows as $row) {
372 $clicks = $row['clicks'] ?? 0;
373 $impressions = $row['impressions'] ?? 0;
374 $position = $row['position'] ?? 0;
375
376 // Consider keywords with good performance metrics as "gaining"
377 if ($clicks > 10 && $position <= 10) {
378 $gaining_keywords[] = [
379 'keyword' => $row['keys'][0] ?? '',
380 'clicks' => $clicks,
381 'impressions' => $impressions,
382 'position' => round($position, 1),
383 'ctr' => round(($clicks / max($impressions, 1)) * 100, 2)
384 ];
385 }
386 }
387
388 // Sort by clicks descending
389 usort($gaining_keywords, function($a, $b) {
390 return $b['clicks'] <=> $a['clicks'];
391 });
392
393 return array_slice($gaining_keywords, 0, 10);
394 }
395
396 /**
397 * Identify losing keywords (declined performance)
398 *
399 * @param array $search_console_data Search Console data
400 * @return array Top losing keywords
401 */
402 private function identify_losing_keywords(array $search_console_data): array {
403 $rows = $search_console_data['rows'] ?? [];
404 $losing_keywords = [];
405
406 foreach ($rows as $row) {
407 $clicks = $row['clicks'] ?? 0;
408 $impressions = $row['impressions'] ?? 0;
409 $position = $row['position'] ?? 0;
410
411 // Consider keywords with poor performance as "losing"
412 if ($impressions > 100 && $position > 20) {
413 $losing_keywords[] = [
414 'keyword' => $row['keys'][0] ?? '',
415 'clicks' => $clicks,
416 'impressions' => $impressions,
417 'position' => round($position, 1),
418 'ctr' => round(($clicks / max($impressions, 1)) * 100, 2)
419 ];
420 }
421 }
422
423 // Sort by position descending (worst positions first)
424 usort($losing_keywords, function($a, $b) {
425 return $b['position'] <=> $a['position'];
426 });
427
428 return array_slice($losing_keywords, 0, 10);
429 }
430
431 /**
432 * Identify new keywords appearing in results
433 *
434 * @param array $search_console_data Search Console data
435 * @return array New keywords analysis
436 */
437 private function identify_new_keywords(array $search_console_data): array {
438 $rows = $search_console_data['rows'] ?? [];
439 $new_keywords = [];
440
441 foreach ($rows as $row) {
442 $clicks = $row['clicks'] ?? 0;
443 $impressions = $row['impressions'] ?? 0;
444
445 // Consider keywords with recent activity as potentially "new"
446 if ($clicks > 0 && $impressions > 50) {
447 $new_keywords[] = [
448 'keyword' => $row['keys'][0] ?? '',
449 'clicks' => $clicks,
450 'impressions' => $impressions,
451 'position' => round($row['position'] ?? 0, 1)
452 ];
453 }
454 }
455
456 // Sort by impressions descending
457 usort($new_keywords, function($a, $b) {
458 return $b['impressions'] <=> $a['impressions'];
459 });
460
461 return array_slice($new_keywords, 0, 5);
462 }
463
464 /**
465 * Generate traffic trend summary
466 *
467 * @param array $current_data Current period data
468 * @param array $historical_data Historical period data
469 * @return string Traffic trend summary
470 */
471 private function generate_traffic_trend_summary(array $current_data, array $historical_data): string {
472 $current_sessions = $current_data['sessions'] ?? 0;
473 $historical_sessions = $historical_data['sessions'] ?? 0;
474 $change = $this->calculate_percentage_change($historical_sessions, $current_sessions);
475
476 if (abs($change) < self::SIGNIFICANT_CHANGE_THRESHOLD) {
477 return 'Traffic remained stable with minimal changes across key metrics.';
478 }
479
480 $direction = $change > 0 ? 'increased' : 'decreased';
481 return sprintf('Traffic %s by %.1f%% compared to the previous period.', $direction, abs($change));
482 }
483
484 /**
485 * Generate keyword trend summary
486 *
487 * @param array $search_console_data Search Console data
488 * @return string Keyword trend summary
489 */
490 private function generate_keyword_trend_summary(array $search_console_data): string {
491 $total_clicks = $this->extract_total_metrics($search_console_data, 'clicks')['total'];
492 $average_position = $this->calculate_average_position($search_console_data)['average_position'];
493
494 if ($total_clicks > 1000) {
495 $performance = 'strong';
496 } elseif ($total_clicks > 100) {
497 $performance = 'moderate';
498 } else {
499 $performance = 'limited';
500 }
501
502 return sprintf('Keyword performance shows %s activity with %d total clicks and average position of %.1f.',
503 $performance, $total_clicks, $average_position);
504 }
505
506 /**
507 * Generate content trend summary
508 *
509 * @param array $analytics_data Analytics data
510 * @param array $search_data Search Console data
511 * @return string Content trend summary
512 */
513 private function generate_content_trend_summary(array $analytics_data, array $search_data): string {
514 $top_pages = $analytics_data['top_pages']['pages'] ?? [];
515 $page_count = count($top_pages);
516
517 if ($page_count > 10) {
518 return sprintf('Content performance is diverse with %d pages driving significant traffic.', $page_count);
519 } elseif ($page_count > 5) {
520 return sprintf('Content performance is concentrated among %d key pages.', $page_count);
521 } else {
522 return 'Content performance is limited to a few key pages, indicating opportunity for expansion.';
523 }
524 }
525
526 /**
527 * Identify gaining pages
528 *
529 * @param array $analytics_data Analytics data
530 * @param array $search_data Search Console data
531 * @return array Gaining pages analysis
532 */
533 private function identify_gaining_pages(array $analytics_data, array $search_data): array {
534 $pages = $analytics_data['top_pages']['pages'] ?? [];
535 $gaining_pages = [];
536
537 foreach ($pages as $page) {
538 if (($page['pageviews'] ?? 0) > 100) {
539 $gaining_pages[] = [
540 'path' => $page['path'] ?? '',
541 'title' => $page['title'] ?? '',
542 'pageviews' => $page['pageviews'] ?? 0,
543 'sessions' => $page['sessions'] ?? 0
544 ];
545 }
546 }
547
548 return array_slice($gaining_pages, 0, 5);
549 }
550
551 /**
552 * Identify losing pages
553 *
554 * @param array $analytics_data Analytics data
555 * @param array $search_data Search Console data
556 * @return array Losing pages analysis
557 */
558 private function identify_losing_pages(array $analytics_data, array $search_data): array {
559 // For now, return empty array as we need historical comparison data
560 // This would be enhanced with actual historical page performance data
561 return [];
562 }
563
564 /**
565 * Analyze device performance trends
566 *
567 * @param array $search_data Search Console data
568 * @return array Device trends analysis
569 */
570 private function analyze_device_trends(array $search_data): array {
571 $device_insights = $search_data['device_insights'] ?? [];
572 $devices = $device_insights['devices'] ?? [];
573
574 $mobile_performance = $devices['mobile'] ?? [];
575 $desktop_performance = $devices['desktop'] ?? [];
576
577 return [
578 'mobile' => [
579 'clicks' => $mobile_performance['clicks'] ?? 0,
580 'impressions' => $mobile_performance['impressions'] ?? 0,
581 'ctr' => $mobile_performance['ctr'] ?? 0
582 ],
583 'desktop' => [
584 'clicks' => $desktop_performance['clicks'] ?? 0,
585 'impressions' => $desktop_performance['impressions'] ?? 0,
586 'ctr' => $desktop_performance['ctr'] ?? 0
587 ],
588 'mobile_dominance' => ($mobile_performance['clicks'] ?? 0) > ($desktop_performance['clicks'] ?? 0)
589 ];
590 }
591
592 /**
593 * Analyze content type performance
594 *
595 * @param array $analytics_data Analytics data
596 * @return array Content type performance analysis
597 */
598 private function analyze_content_type_performance(array $analytics_data): array {
599 $pages = $analytics_data['top_pages']['pages'] ?? [];
600 $content_types = [
601 'blog' => 0,
602 'product' => 0,
603 'page' => 0,
604 'other' => 0
605 ];
606
607 foreach ($pages as $page) {
608 $path = $page['path'] ?? '';
609 $pageviews = $page['pageviews'] ?? 0;
610
611 if (strpos($path, '/blog/') !== false || strpos($path, '/post/') !== false) {
612 $content_types['blog'] += $pageviews;
613 } elseif (strpos($path, '/product/') !== false || strpos($path, '/shop/') !== false) {
614 $content_types['product'] += $pageviews;
615 } elseif (strpos($path, '/page/') !== false) {
616 $content_types['page'] += $pageviews;
617 } else {
618 $content_types['other'] += $pageviews;
619 }
620 }
621
622 return $content_types;
623 }
624
625 /**
626 * Analyze monthly patterns in historical data
627 *
628 * @param array $historical_data Historical data
629 * @return array Monthly pattern analysis
630 */
631 private function analyze_monthly_patterns(array $historical_data): array {
632 // Placeholder for monthly pattern analysis
633 // Would analyze seasonal trends by month
634 return [
635 'peak_months' => [],
636 'low_months' => [],
637 'seasonal_factor' => 1.0
638 ];
639 }
640
641 /**
642 * Analyze weekly patterns in historical data
643 *
644 * @param array $historical_data Historical data
645 * @return array Weekly pattern analysis
646 */
647 private function analyze_weekly_patterns(array $historical_data): array {
648 // Placeholder for weekly pattern analysis
649 // Would analyze day-of-week performance patterns
650 return [
651 'peak_days' => [],
652 'low_days' => [],
653 'weekend_factor' => 1.0
654 ];
655 }
656
657 /**
658 * Generate seasonal recommendations
659 *
660 * @param array $historical_data Historical data
661 * @return array Seasonal recommendations
662 */
663 private function generate_seasonal_recommendations(array $historical_data): array {
664 return [
665 'content_timing' => 'Optimize content publishing for peak performance periods',
666 'seasonal_content' => 'Prepare seasonal content in advance of peak periods',
667 'resource_allocation' => 'Allocate marketing resources during high-performance periods'
668 ];
669 }
670 }
671