fix(cua): do not press back after typing — adb input text does not show keyboard, back navigates away
This commit is contained in:
662
.agents/skills/app-store-optimization/scripts/ab_test_planner.py
Normal file
662
.agents/skills/app-store-optimization/scripts/ab_test_planner.py
Normal file
@@ -0,0 +1,662 @@
|
||||
"""
|
||||
A/B testing module for App Store Optimization.
|
||||
Plans and tracks A/B tests for metadata and visual assets.
|
||||
"""
|
||||
|
||||
from typing import Dict, List, Any, Optional
|
||||
import math
|
||||
|
||||
|
||||
class ABTestPlanner:
|
||||
"""Plans and tracks A/B tests for ASO elements."""
|
||||
|
||||
# Minimum detectable effect sizes (conservative estimates)
|
||||
MIN_EFFECT_SIZES = {
|
||||
'icon': 0.10, # 10% conversion improvement
|
||||
'screenshot': 0.08, # 8% conversion improvement
|
||||
'title': 0.05, # 5% conversion improvement
|
||||
'description': 0.03 # 3% conversion improvement
|
||||
}
|
||||
|
||||
# Statistical confidence levels
|
||||
CONFIDENCE_LEVELS = {
|
||||
'high': 0.95, # 95% confidence
|
||||
'standard': 0.90, # 90% confidence
|
||||
'exploratory': 0.80 # 80% confidence
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize A/B test planner."""
|
||||
self.active_tests = []
|
||||
|
||||
def design_test(
|
||||
self,
|
||||
test_type: str,
|
||||
variant_a: Dict[str, Any],
|
||||
variant_b: Dict[str, Any],
|
||||
hypothesis: str,
|
||||
success_metric: str = 'conversion_rate'
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Design an A/B test with hypothesis and variables.
|
||||
|
||||
Args:
|
||||
test_type: Type of test ('icon', 'screenshot', 'title', 'description')
|
||||
variant_a: Control variant details
|
||||
variant_b: Test variant details
|
||||
hypothesis: Expected outcome hypothesis
|
||||
success_metric: Metric to optimize
|
||||
|
||||
Returns:
|
||||
Test design with configuration
|
||||
"""
|
||||
test_design = {
|
||||
'test_id': self._generate_test_id(test_type),
|
||||
'test_type': test_type,
|
||||
'hypothesis': hypothesis,
|
||||
'variants': {
|
||||
'a': {
|
||||
'name': 'Control',
|
||||
'details': variant_a,
|
||||
'traffic_split': 0.5
|
||||
},
|
||||
'b': {
|
||||
'name': 'Variation',
|
||||
'details': variant_b,
|
||||
'traffic_split': 0.5
|
||||
}
|
||||
},
|
||||
'success_metric': success_metric,
|
||||
'secondary_metrics': self._get_secondary_metrics(test_type),
|
||||
'minimum_effect_size': self.MIN_EFFECT_SIZES.get(test_type, 0.05),
|
||||
'recommended_confidence': 'standard',
|
||||
'best_practices': self._get_test_best_practices(test_type)
|
||||
}
|
||||
|
||||
self.active_tests.append(test_design)
|
||||
return test_design
|
||||
|
||||
def calculate_sample_size(
|
||||
self,
|
||||
baseline_conversion: float,
|
||||
minimum_detectable_effect: float,
|
||||
confidence_level: str = 'standard',
|
||||
power: float = 0.80
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Calculate required sample size for statistical significance.
|
||||
|
||||
Args:
|
||||
baseline_conversion: Current conversion rate (0-1)
|
||||
minimum_detectable_effect: Minimum effect size to detect (0-1)
|
||||
confidence_level: 'high', 'standard', or 'exploratory'
|
||||
power: Statistical power (typically 0.80 or 0.90)
|
||||
|
||||
Returns:
|
||||
Sample size calculation with duration estimates
|
||||
"""
|
||||
alpha = 1 - self.CONFIDENCE_LEVELS[confidence_level]
|
||||
beta = 1 - power
|
||||
|
||||
# Expected conversion for variant B
|
||||
expected_conversion_b = baseline_conversion * (1 + minimum_detectable_effect)
|
||||
|
||||
# Z-scores for alpha and beta
|
||||
z_alpha = self._get_z_score(1 - alpha / 2) # Two-tailed test
|
||||
z_beta = self._get_z_score(power)
|
||||
|
||||
# Pooled standard deviation
|
||||
p_pooled = (baseline_conversion + expected_conversion_b) / 2
|
||||
sd_pooled = math.sqrt(2 * p_pooled * (1 - p_pooled))
|
||||
|
||||
# Sample size per variant
|
||||
n_per_variant = math.ceil(
|
||||
((z_alpha + z_beta) ** 2 * sd_pooled ** 2) /
|
||||
((expected_conversion_b - baseline_conversion) ** 2)
|
||||
)
|
||||
|
||||
total_sample_size = n_per_variant * 2
|
||||
|
||||
# Estimate duration based on typical traffic
|
||||
duration_estimates = self._estimate_test_duration(
|
||||
total_sample_size,
|
||||
baseline_conversion
|
||||
)
|
||||
|
||||
return {
|
||||
'sample_size_per_variant': n_per_variant,
|
||||
'total_sample_size': total_sample_size,
|
||||
'baseline_conversion': baseline_conversion,
|
||||
'expected_conversion_improvement': minimum_detectable_effect,
|
||||
'expected_conversion_b': expected_conversion_b,
|
||||
'confidence_level': confidence_level,
|
||||
'statistical_power': power,
|
||||
'duration_estimates': duration_estimates,
|
||||
'recommendations': self._generate_sample_size_recommendations(
|
||||
n_per_variant,
|
||||
duration_estimates
|
||||
)
|
||||
}
|
||||
|
||||
def calculate_significance(
|
||||
self,
|
||||
variant_a_conversions: int,
|
||||
variant_a_visitors: int,
|
||||
variant_b_conversions: int,
|
||||
variant_b_visitors: int
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Calculate statistical significance of test results.
|
||||
|
||||
Args:
|
||||
variant_a_conversions: Conversions for control
|
||||
variant_a_visitors: Visitors for control
|
||||
variant_b_conversions: Conversions for variation
|
||||
variant_b_visitors: Visitors for variation
|
||||
|
||||
Returns:
|
||||
Significance analysis with decision recommendation
|
||||
"""
|
||||
# Calculate conversion rates
|
||||
rate_a = variant_a_conversions / variant_a_visitors if variant_a_visitors > 0 else 0
|
||||
rate_b = variant_b_conversions / variant_b_visitors if variant_b_visitors > 0 else 0
|
||||
|
||||
# Calculate improvement
|
||||
if rate_a > 0:
|
||||
relative_improvement = (rate_b - rate_a) / rate_a
|
||||
else:
|
||||
relative_improvement = 0
|
||||
|
||||
absolute_improvement = rate_b - rate_a
|
||||
|
||||
# Calculate standard error
|
||||
se_a = math.sqrt(rate_a * (1 - rate_a) / variant_a_visitors) if variant_a_visitors > 0 else 0
|
||||
se_b = math.sqrt(rate_b * (1 - rate_b) / variant_b_visitors) if variant_b_visitors > 0 else 0
|
||||
se_diff = math.sqrt(se_a**2 + se_b**2)
|
||||
|
||||
# Calculate z-score
|
||||
z_score = absolute_improvement / se_diff if se_diff > 0 else 0
|
||||
|
||||
# Calculate p-value (two-tailed)
|
||||
p_value = 2 * (1 - self._standard_normal_cdf(abs(z_score)))
|
||||
|
||||
# Determine significance
|
||||
is_significant_95 = p_value < 0.05
|
||||
is_significant_90 = p_value < 0.10
|
||||
|
||||
# Generate decision
|
||||
decision = self._generate_test_decision(
|
||||
relative_improvement,
|
||||
is_significant_95,
|
||||
is_significant_90,
|
||||
variant_a_visitors + variant_b_visitors
|
||||
)
|
||||
|
||||
return {
|
||||
'variant_a': {
|
||||
'conversions': variant_a_conversions,
|
||||
'visitors': variant_a_visitors,
|
||||
'conversion_rate': round(rate_a, 4)
|
||||
},
|
||||
'variant_b': {
|
||||
'conversions': variant_b_conversions,
|
||||
'visitors': variant_b_visitors,
|
||||
'conversion_rate': round(rate_b, 4)
|
||||
},
|
||||
'improvement': {
|
||||
'absolute': round(absolute_improvement, 4),
|
||||
'relative_percentage': round(relative_improvement * 100, 2)
|
||||
},
|
||||
'statistical_analysis': {
|
||||
'z_score': round(z_score, 3),
|
||||
'p_value': round(p_value, 4),
|
||||
'is_significant_95': is_significant_95,
|
||||
'is_significant_90': is_significant_90,
|
||||
'confidence_level': '95%' if is_significant_95 else ('90%' if is_significant_90 else 'Not significant')
|
||||
},
|
||||
'decision': decision
|
||||
}
|
||||
|
||||
def track_test_results(
|
||||
self,
|
||||
test_id: str,
|
||||
results_data: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Track ongoing test results and provide recommendations.
|
||||
|
||||
Args:
|
||||
test_id: Test identifier
|
||||
results_data: Current test results
|
||||
|
||||
Returns:
|
||||
Test tracking report with next steps
|
||||
"""
|
||||
# Find test
|
||||
test = next((t for t in self.active_tests if t['test_id'] == test_id), None)
|
||||
if not test:
|
||||
return {'error': f'Test {test_id} not found'}
|
||||
|
||||
# Calculate significance
|
||||
significance = self.calculate_significance(
|
||||
results_data['variant_a_conversions'],
|
||||
results_data['variant_a_visitors'],
|
||||
results_data['variant_b_conversions'],
|
||||
results_data['variant_b_visitors']
|
||||
)
|
||||
|
||||
# Calculate test progress
|
||||
total_visitors = results_data['variant_a_visitors'] + results_data['variant_b_visitors']
|
||||
required_sample = results_data.get('required_sample_size', 10000)
|
||||
progress_percentage = min((total_visitors / required_sample) * 100, 100)
|
||||
|
||||
# Generate recommendations
|
||||
recommendations = self._generate_tracking_recommendations(
|
||||
significance,
|
||||
progress_percentage,
|
||||
test['test_type']
|
||||
)
|
||||
|
||||
return {
|
||||
'test_id': test_id,
|
||||
'test_type': test['test_type'],
|
||||
'progress': {
|
||||
'total_visitors': total_visitors,
|
||||
'required_sample_size': required_sample,
|
||||
'progress_percentage': round(progress_percentage, 1),
|
||||
'is_complete': progress_percentage >= 100
|
||||
},
|
||||
'current_results': significance,
|
||||
'recommendations': recommendations,
|
||||
'next_steps': self._determine_next_steps(
|
||||
significance,
|
||||
progress_percentage
|
||||
)
|
||||
}
|
||||
|
||||
def generate_test_report(
|
||||
self,
|
||||
test_id: str,
|
||||
final_results: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Generate final test report with insights and recommendations.
|
||||
|
||||
Args:
|
||||
test_id: Test identifier
|
||||
final_results: Final test results
|
||||
|
||||
Returns:
|
||||
Comprehensive test report
|
||||
"""
|
||||
test = next((t for t in self.active_tests if t['test_id'] == test_id), None)
|
||||
if not test:
|
||||
return {'error': f'Test {test_id} not found'}
|
||||
|
||||
significance = self.calculate_significance(
|
||||
final_results['variant_a_conversions'],
|
||||
final_results['variant_a_visitors'],
|
||||
final_results['variant_b_conversions'],
|
||||
final_results['variant_b_visitors']
|
||||
)
|
||||
|
||||
# Generate insights
|
||||
insights = self._generate_test_insights(
|
||||
test,
|
||||
significance,
|
||||
final_results
|
||||
)
|
||||
|
||||
# Implementation plan
|
||||
implementation_plan = self._create_implementation_plan(
|
||||
test,
|
||||
significance
|
||||
)
|
||||
|
||||
return {
|
||||
'test_summary': {
|
||||
'test_id': test_id,
|
||||
'test_type': test['test_type'],
|
||||
'hypothesis': test['hypothesis'],
|
||||
'duration_days': final_results.get('duration_days', 'N/A')
|
||||
},
|
||||
'results': significance,
|
||||
'insights': insights,
|
||||
'implementation_plan': implementation_plan,
|
||||
'learnings': self._extract_learnings(test, significance)
|
||||
}
|
||||
|
||||
def _generate_test_id(self, test_type: str) -> str:
|
||||
"""Generate unique test ID."""
|
||||
import time
|
||||
timestamp = int(time.time())
|
||||
return f"{test_type}_{timestamp}"
|
||||
|
||||
def _get_secondary_metrics(self, test_type: str) -> List[str]:
|
||||
"""Get secondary metrics to track for test type."""
|
||||
metrics_map = {
|
||||
'icon': ['tap_through_rate', 'impression_count', 'brand_recall'],
|
||||
'screenshot': ['tap_through_rate', 'time_on_page', 'scroll_depth'],
|
||||
'title': ['impression_count', 'tap_through_rate', 'search_visibility'],
|
||||
'description': ['time_on_page', 'scroll_depth', 'tap_through_rate']
|
||||
}
|
||||
return metrics_map.get(test_type, ['tap_through_rate'])
|
||||
|
||||
def _get_test_best_practices(self, test_type: str) -> List[str]:
|
||||
"""Get best practices for specific test type."""
|
||||
practices_map = {
|
||||
'icon': [
|
||||
'Test only one element at a time (color vs. style vs. symbolism)',
|
||||
'Ensure icon is recognizable at small sizes (60x60px)',
|
||||
'Consider cultural context for global audience',
|
||||
'Test against top competitor icons'
|
||||
],
|
||||
'screenshot': [
|
||||
'Test order of screenshots (users see first 2-3)',
|
||||
'Use captions to tell story',
|
||||
'Show key features and benefits',
|
||||
'Test with and without device frames'
|
||||
],
|
||||
'title': [
|
||||
'Test keyword variations, not major rebrand',
|
||||
'Keep brand name consistent',
|
||||
'Ensure title fits within character limits',
|
||||
'Test on both search and browse contexts'
|
||||
],
|
||||
'description': [
|
||||
'Test structure (bullet points vs. paragraphs)',
|
||||
'Test call-to-action placement',
|
||||
'Test feature vs. benefit focus',
|
||||
'Maintain keyword density'
|
||||
]
|
||||
}
|
||||
return practices_map.get(test_type, ['Test one variable at a time'])
|
||||
|
||||
def _estimate_test_duration(
|
||||
self,
|
||||
required_sample_size: int,
|
||||
baseline_conversion: float
|
||||
) -> Dict[str, Any]:
|
||||
"""Estimate test duration based on typical traffic levels."""
|
||||
# Assume different daily traffic scenarios
|
||||
traffic_scenarios = {
|
||||
'low': 100, # 100 page views/day
|
||||
'medium': 1000, # 1000 page views/day
|
||||
'high': 10000 # 10000 page views/day
|
||||
}
|
||||
|
||||
estimates = {}
|
||||
for scenario, daily_views in traffic_scenarios.items():
|
||||
days = math.ceil(required_sample_size / daily_views)
|
||||
estimates[scenario] = {
|
||||
'daily_page_views': daily_views,
|
||||
'estimated_days': days,
|
||||
'estimated_weeks': round(days / 7, 1)
|
||||
}
|
||||
|
||||
return estimates
|
||||
|
||||
def _generate_sample_size_recommendations(
|
||||
self,
|
||||
sample_size: int,
|
||||
duration_estimates: Dict[str, Any]
|
||||
) -> List[str]:
|
||||
"""Generate recommendations based on sample size."""
|
||||
recommendations = []
|
||||
|
||||
if sample_size > 50000:
|
||||
recommendations.append(
|
||||
"Large sample size required - consider testing smaller effect size or increasing traffic"
|
||||
)
|
||||
|
||||
if duration_estimates['medium']['estimated_days'] > 30:
|
||||
recommendations.append(
|
||||
"Long test duration - consider higher minimum detectable effect or focus on high-impact changes"
|
||||
)
|
||||
|
||||
if duration_estimates['low']['estimated_days'] > 60:
|
||||
recommendations.append(
|
||||
"Insufficient traffic for reliable testing - consider user acquisition or broader targeting"
|
||||
)
|
||||
|
||||
if not recommendations:
|
||||
recommendations.append("Sample size and duration are reasonable for this test")
|
||||
|
||||
return recommendations
|
||||
|
||||
def _get_z_score(self, percentile: float) -> float:
|
||||
"""Get z-score for given percentile (approximation)."""
|
||||
# Common z-scores
|
||||
z_scores = {
|
||||
0.80: 0.84,
|
||||
0.85: 1.04,
|
||||
0.90: 1.28,
|
||||
0.95: 1.645,
|
||||
0.975: 1.96,
|
||||
0.99: 2.33
|
||||
}
|
||||
return z_scores.get(percentile, 1.96)
|
||||
|
||||
def _standard_normal_cdf(self, z: float) -> float:
|
||||
"""Approximate standard normal cumulative distribution function."""
|
||||
# Using error function approximation
|
||||
t = 1.0 / (1.0 + 0.2316419 * abs(z))
|
||||
d = 0.3989423 * math.exp(-z * z / 2.0)
|
||||
p = d * t * (0.3193815 + t * (-0.3565638 + t * (1.781478 + t * (-1.821256 + t * 1.330274))))
|
||||
|
||||
if z > 0:
|
||||
return 1.0 - p
|
||||
else:
|
||||
return p
|
||||
|
||||
def _generate_test_decision(
|
||||
self,
|
||||
improvement: float,
|
||||
is_significant_95: bool,
|
||||
is_significant_90: bool,
|
||||
total_visitors: int
|
||||
) -> Dict[str, Any]:
|
||||
"""Generate test decision and recommendation."""
|
||||
if total_visitors < 1000:
|
||||
return {
|
||||
'decision': 'continue',
|
||||
'rationale': 'Insufficient data - continue test to reach minimum sample size',
|
||||
'action': 'Keep test running'
|
||||
}
|
||||
|
||||
if is_significant_95:
|
||||
if improvement > 0:
|
||||
return {
|
||||
'decision': 'implement_b',
|
||||
'rationale': f'Variant B shows {improvement*100:.1f}% improvement with 95% confidence',
|
||||
'action': 'Implement Variant B'
|
||||
}
|
||||
else:
|
||||
return {
|
||||
'decision': 'keep_a',
|
||||
'rationale': 'Variant A performs better with 95% confidence',
|
||||
'action': 'Keep current version (A)'
|
||||
}
|
||||
|
||||
elif is_significant_90:
|
||||
if improvement > 0:
|
||||
return {
|
||||
'decision': 'implement_b_cautiously',
|
||||
'rationale': f'Variant B shows {improvement*100:.1f}% improvement with 90% confidence',
|
||||
'action': 'Consider implementing B, monitor closely'
|
||||
}
|
||||
else:
|
||||
return {
|
||||
'decision': 'keep_a',
|
||||
'rationale': 'Variant A performs better with 90% confidence',
|
||||
'action': 'Keep current version (A)'
|
||||
}
|
||||
|
||||
else:
|
||||
return {
|
||||
'decision': 'inconclusive',
|
||||
'rationale': 'No statistically significant difference detected',
|
||||
'action': 'Either keep A or test different hypothesis'
|
||||
}
|
||||
|
||||
def _generate_tracking_recommendations(
|
||||
self,
|
||||
significance: Dict[str, Any],
|
||||
progress: float,
|
||||
test_type: str
|
||||
) -> List[str]:
|
||||
"""Generate recommendations for ongoing test."""
|
||||
recommendations = []
|
||||
|
||||
if progress < 50:
|
||||
recommendations.append(
|
||||
f"Test is {progress:.0f}% complete - continue collecting data"
|
||||
)
|
||||
|
||||
if progress >= 100:
|
||||
if significance['statistical_analysis']['is_significant_95']:
|
||||
recommendations.append(
|
||||
"Sufficient data collected with significant results - ready to conclude test"
|
||||
)
|
||||
else:
|
||||
recommendations.append(
|
||||
"Sample size reached but no significant difference - consider extending test or concluding"
|
||||
)
|
||||
|
||||
return recommendations
|
||||
|
||||
def _determine_next_steps(
|
||||
self,
|
||||
significance: Dict[str, Any],
|
||||
progress: float
|
||||
) -> str:
|
||||
"""Determine next steps for test."""
|
||||
if progress < 100:
|
||||
return f"Continue test until reaching 100% sample size (currently {progress:.0f}%)"
|
||||
|
||||
decision = significance.get('decision', {}).get('decision', 'inconclusive')
|
||||
|
||||
if decision == 'implement_b':
|
||||
return "Implement Variant B and monitor metrics for 2 weeks"
|
||||
elif decision == 'keep_a':
|
||||
return "Keep Variant A and design new test with different hypothesis"
|
||||
else:
|
||||
return "Test inconclusive - either keep A or design new test"
|
||||
|
||||
def _generate_test_insights(
|
||||
self,
|
||||
test: Dict[str, Any],
|
||||
significance: Dict[str, Any],
|
||||
results: Dict[str, Any]
|
||||
) -> List[str]:
|
||||
"""Generate insights from test results."""
|
||||
insights = []
|
||||
|
||||
improvement = significance['improvement']['relative_percentage']
|
||||
|
||||
if significance['statistical_analysis']['is_significant_95']:
|
||||
insights.append(
|
||||
f"Strong evidence: Variant B {'improved' if improvement > 0 else 'decreased'} "
|
||||
f"conversion by {abs(improvement):.1f}% with 95% confidence"
|
||||
)
|
||||
|
||||
insights.append(
|
||||
f"Tested {test['test_type']} changes: {test['hypothesis']}"
|
||||
)
|
||||
|
||||
# Add context-specific insights
|
||||
if test['test_type'] == 'icon' and improvement > 5:
|
||||
insights.append(
|
||||
"Icon change had substantial impact - visual first impression is critical"
|
||||
)
|
||||
|
||||
return insights
|
||||
|
||||
def _create_implementation_plan(
|
||||
self,
|
||||
test: Dict[str, Any],
|
||||
significance: Dict[str, Any]
|
||||
) -> List[Dict[str, str]]:
|
||||
"""Create implementation plan for winning variant."""
|
||||
plan = []
|
||||
|
||||
if significance.get('decision', {}).get('decision') == 'implement_b':
|
||||
plan.append({
|
||||
'step': '1. Update store listing',
|
||||
'details': f"Replace {test['test_type']} with Variant B across all platforms"
|
||||
})
|
||||
plan.append({
|
||||
'step': '2. Monitor metrics',
|
||||
'details': 'Track conversion rate for 2 weeks to confirm sustained improvement'
|
||||
})
|
||||
plan.append({
|
||||
'step': '3. Document learnings',
|
||||
'details': 'Record insights for future optimization'
|
||||
})
|
||||
|
||||
return plan
|
||||
|
||||
def _extract_learnings(
|
||||
self,
|
||||
test: Dict[str, Any],
|
||||
significance: Dict[str, Any]
|
||||
) -> List[str]:
|
||||
"""Extract key learnings from test."""
|
||||
learnings = []
|
||||
|
||||
improvement = significance['improvement']['relative_percentage']
|
||||
|
||||
learnings.append(
|
||||
f"Testing {test['test_type']} can yield {abs(improvement):.1f}% conversion change"
|
||||
)
|
||||
|
||||
if test['test_type'] == 'title':
|
||||
learnings.append(
|
||||
"Title changes affect search visibility and user perception"
|
||||
)
|
||||
elif test['test_type'] == 'screenshot':
|
||||
learnings.append(
|
||||
"First 2-3 screenshots are critical for conversion"
|
||||
)
|
||||
|
||||
return learnings
|
||||
|
||||
|
||||
def plan_ab_test(
|
||||
test_type: str,
|
||||
variant_a: Dict[str, Any],
|
||||
variant_b: Dict[str, Any],
|
||||
hypothesis: str,
|
||||
baseline_conversion: float
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Convenience function to plan an A/B test.
|
||||
|
||||
Args:
|
||||
test_type: Type of test
|
||||
variant_a: Control variant
|
||||
variant_b: Test variant
|
||||
hypothesis: Test hypothesis
|
||||
baseline_conversion: Current conversion rate
|
||||
|
||||
Returns:
|
||||
Complete test plan
|
||||
"""
|
||||
planner = ABTestPlanner()
|
||||
|
||||
test_design = planner.design_test(
|
||||
test_type,
|
||||
variant_a,
|
||||
variant_b,
|
||||
hypothesis
|
||||
)
|
||||
|
||||
sample_size = planner.calculate_sample_size(
|
||||
baseline_conversion,
|
||||
planner.MIN_EFFECT_SIZES.get(test_type, 0.05)
|
||||
)
|
||||
|
||||
return {
|
||||
'test_design': test_design,
|
||||
'sample_size_requirements': sample_size
|
||||
}
|
||||
482
.agents/skills/app-store-optimization/scripts/aso_scorer.py
Normal file
482
.agents/skills/app-store-optimization/scripts/aso_scorer.py
Normal file
@@ -0,0 +1,482 @@
|
||||
"""
|
||||
ASO scoring module for App Store Optimization.
|
||||
Calculates comprehensive ASO health score across multiple dimensions.
|
||||
"""
|
||||
|
||||
from typing import Dict, List, Any, Optional
|
||||
|
||||
|
||||
class ASOScorer:
|
||||
"""Calculates overall ASO health score and provides recommendations."""
|
||||
|
||||
# Score weights for different components (total = 100)
|
||||
WEIGHTS = {
|
||||
'metadata_quality': 25,
|
||||
'ratings_reviews': 25,
|
||||
'keyword_performance': 25,
|
||||
'conversion_metrics': 25
|
||||
}
|
||||
|
||||
# Benchmarks for scoring
|
||||
BENCHMARKS = {
|
||||
'title_keyword_usage': {'min': 1, 'target': 2},
|
||||
'description_length': {'min': 500, 'target': 2000},
|
||||
'keyword_density': {'min': 2, 'optimal': 5, 'max': 8},
|
||||
'average_rating': {'min': 3.5, 'target': 4.5},
|
||||
'ratings_count': {'min': 100, 'target': 5000},
|
||||
'keywords_top_10': {'min': 2, 'target': 10},
|
||||
'keywords_top_50': {'min': 5, 'target': 20},
|
||||
'conversion_rate': {'min': 0.02, 'target': 0.10}
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize ASO scorer."""
|
||||
self.score_breakdown = {}
|
||||
|
||||
def calculate_overall_score(
|
||||
self,
|
||||
metadata: Dict[str, Any],
|
||||
ratings: Dict[str, Any],
|
||||
keyword_performance: Dict[str, Any],
|
||||
conversion: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Calculate comprehensive ASO score (0-100).
|
||||
|
||||
Args:
|
||||
metadata: Title, description quality metrics
|
||||
ratings: Rating average and count
|
||||
keyword_performance: Keyword ranking data
|
||||
conversion: Impression-to-install metrics
|
||||
|
||||
Returns:
|
||||
Overall score with detailed breakdown
|
||||
"""
|
||||
# Calculate component scores
|
||||
metadata_score = self.score_metadata_quality(metadata)
|
||||
ratings_score = self.score_ratings_reviews(ratings)
|
||||
keyword_score = self.score_keyword_performance(keyword_performance)
|
||||
conversion_score = self.score_conversion_metrics(conversion)
|
||||
|
||||
# Calculate weighted overall score
|
||||
overall_score = (
|
||||
metadata_score * (self.WEIGHTS['metadata_quality'] / 100) +
|
||||
ratings_score * (self.WEIGHTS['ratings_reviews'] / 100) +
|
||||
keyword_score * (self.WEIGHTS['keyword_performance'] / 100) +
|
||||
conversion_score * (self.WEIGHTS['conversion_metrics'] / 100)
|
||||
)
|
||||
|
||||
# Store breakdown
|
||||
self.score_breakdown = {
|
||||
'metadata_quality': {
|
||||
'score': metadata_score,
|
||||
'weight': self.WEIGHTS['metadata_quality'],
|
||||
'weighted_contribution': round(metadata_score * (self.WEIGHTS['metadata_quality'] / 100), 1)
|
||||
},
|
||||
'ratings_reviews': {
|
||||
'score': ratings_score,
|
||||
'weight': self.WEIGHTS['ratings_reviews'],
|
||||
'weighted_contribution': round(ratings_score * (self.WEIGHTS['ratings_reviews'] / 100), 1)
|
||||
},
|
||||
'keyword_performance': {
|
||||
'score': keyword_score,
|
||||
'weight': self.WEIGHTS['keyword_performance'],
|
||||
'weighted_contribution': round(keyword_score * (self.WEIGHTS['keyword_performance'] / 100), 1)
|
||||
},
|
||||
'conversion_metrics': {
|
||||
'score': conversion_score,
|
||||
'weight': self.WEIGHTS['conversion_metrics'],
|
||||
'weighted_contribution': round(conversion_score * (self.WEIGHTS['conversion_metrics'] / 100), 1)
|
||||
}
|
||||
}
|
||||
|
||||
# Generate recommendations
|
||||
recommendations = self.generate_recommendations(
|
||||
metadata_score,
|
||||
ratings_score,
|
||||
keyword_score,
|
||||
conversion_score
|
||||
)
|
||||
|
||||
# Assess overall health
|
||||
health_status = self._assess_health_status(overall_score)
|
||||
|
||||
return {
|
||||
'overall_score': round(overall_score, 1),
|
||||
'health_status': health_status,
|
||||
'score_breakdown': self.score_breakdown,
|
||||
'recommendations': recommendations,
|
||||
'priority_actions': self._prioritize_actions(recommendations),
|
||||
'strengths': self._identify_strengths(self.score_breakdown),
|
||||
'weaknesses': self._identify_weaknesses(self.score_breakdown)
|
||||
}
|
||||
|
||||
def score_metadata_quality(self, metadata: Dict[str, Any]) -> float:
|
||||
"""
|
||||
Score metadata quality (0-100).
|
||||
|
||||
Evaluates:
|
||||
- Title optimization
|
||||
- Description quality
|
||||
- Keyword usage
|
||||
"""
|
||||
scores = []
|
||||
|
||||
# Title score (0-35 points)
|
||||
title_keywords = metadata.get('title_keyword_count', 0)
|
||||
title_length = metadata.get('title_length', 0)
|
||||
|
||||
title_score = 0
|
||||
if title_keywords >= self.BENCHMARKS['title_keyword_usage']['target']:
|
||||
title_score = 35
|
||||
elif title_keywords >= self.BENCHMARKS['title_keyword_usage']['min']:
|
||||
title_score = 25
|
||||
else:
|
||||
title_score = 10
|
||||
|
||||
# Adjust for title length usage
|
||||
if title_length > 25: # Using most of available space
|
||||
title_score += 0
|
||||
else:
|
||||
title_score -= 5
|
||||
|
||||
scores.append(min(title_score, 35))
|
||||
|
||||
# Description score (0-35 points)
|
||||
desc_length = metadata.get('description_length', 0)
|
||||
desc_quality = metadata.get('description_quality', 0.0) # 0-1 scale
|
||||
|
||||
desc_score = 0
|
||||
if desc_length >= self.BENCHMARKS['description_length']['target']:
|
||||
desc_score = 25
|
||||
elif desc_length >= self.BENCHMARKS['description_length']['min']:
|
||||
desc_score = 15
|
||||
else:
|
||||
desc_score = 5
|
||||
|
||||
# Add quality bonus
|
||||
desc_score += desc_quality * 10
|
||||
scores.append(min(desc_score, 35))
|
||||
|
||||
# Keyword density score (0-30 points)
|
||||
keyword_density = metadata.get('keyword_density', 0.0)
|
||||
|
||||
if self.BENCHMARKS['keyword_density']['min'] <= keyword_density <= self.BENCHMARKS['keyword_density']['optimal']:
|
||||
density_score = 30
|
||||
elif keyword_density < self.BENCHMARKS['keyword_density']['min']:
|
||||
# Too low - proportional scoring
|
||||
density_score = (keyword_density / self.BENCHMARKS['keyword_density']['min']) * 20
|
||||
else:
|
||||
# Too high (keyword stuffing) - penalty
|
||||
excess = keyword_density - self.BENCHMARKS['keyword_density']['optimal']
|
||||
density_score = max(30 - (excess * 5), 0)
|
||||
|
||||
scores.append(density_score)
|
||||
|
||||
return round(sum(scores), 1)
|
||||
|
||||
def score_ratings_reviews(self, ratings: Dict[str, Any]) -> float:
|
||||
"""
|
||||
Score ratings and reviews (0-100).
|
||||
|
||||
Evaluates:
|
||||
- Average rating
|
||||
- Total ratings count
|
||||
- Review velocity
|
||||
"""
|
||||
average_rating = ratings.get('average_rating', 0.0)
|
||||
total_ratings = ratings.get('total_ratings', 0)
|
||||
recent_ratings = ratings.get('recent_ratings_30d', 0)
|
||||
|
||||
# Rating quality score (0-50 points)
|
||||
if average_rating >= self.BENCHMARKS['average_rating']['target']:
|
||||
rating_quality_score = 50
|
||||
elif average_rating >= self.BENCHMARKS['average_rating']['min']:
|
||||
# Proportional scoring between min and target
|
||||
proportion = (average_rating - self.BENCHMARKS['average_rating']['min']) / \
|
||||
(self.BENCHMARKS['average_rating']['target'] - self.BENCHMARKS['average_rating']['min'])
|
||||
rating_quality_score = 30 + (proportion * 20)
|
||||
elif average_rating >= 3.0:
|
||||
rating_quality_score = 20
|
||||
else:
|
||||
rating_quality_score = 10
|
||||
|
||||
# Rating volume score (0-30 points)
|
||||
if total_ratings >= self.BENCHMARKS['ratings_count']['target']:
|
||||
rating_volume_score = 30
|
||||
elif total_ratings >= self.BENCHMARKS['ratings_count']['min']:
|
||||
# Proportional scoring
|
||||
proportion = (total_ratings - self.BENCHMARKS['ratings_count']['min']) / \
|
||||
(self.BENCHMARKS['ratings_count']['target'] - self.BENCHMARKS['ratings_count']['min'])
|
||||
rating_volume_score = 15 + (proportion * 15)
|
||||
else:
|
||||
# Very low volume
|
||||
rating_volume_score = (total_ratings / self.BENCHMARKS['ratings_count']['min']) * 15
|
||||
|
||||
# Rating velocity score (0-20 points)
|
||||
if recent_ratings > 100:
|
||||
velocity_score = 20
|
||||
elif recent_ratings > 50:
|
||||
velocity_score = 15
|
||||
elif recent_ratings > 10:
|
||||
velocity_score = 10
|
||||
else:
|
||||
velocity_score = 5
|
||||
|
||||
total_score = rating_quality_score + rating_volume_score + velocity_score
|
||||
|
||||
return round(min(total_score, 100), 1)
|
||||
|
||||
def score_keyword_performance(self, keyword_performance: Dict[str, Any]) -> float:
|
||||
"""
|
||||
Score keyword ranking performance (0-100).
|
||||
|
||||
Evaluates:
|
||||
- Top 10 rankings
|
||||
- Top 50 rankings
|
||||
- Ranking trends
|
||||
"""
|
||||
top_10_count = keyword_performance.get('top_10', 0)
|
||||
top_50_count = keyword_performance.get('top_50', 0)
|
||||
top_100_count = keyword_performance.get('top_100', 0)
|
||||
improving_keywords = keyword_performance.get('improving_keywords', 0)
|
||||
|
||||
# Top 10 score (0-50 points) - most valuable rankings
|
||||
if top_10_count >= self.BENCHMARKS['keywords_top_10']['target']:
|
||||
top_10_score = 50
|
||||
elif top_10_count >= self.BENCHMARKS['keywords_top_10']['min']:
|
||||
proportion = (top_10_count - self.BENCHMARKS['keywords_top_10']['min']) / \
|
||||
(self.BENCHMARKS['keywords_top_10']['target'] - self.BENCHMARKS['keywords_top_10']['min'])
|
||||
top_10_score = 25 + (proportion * 25)
|
||||
else:
|
||||
top_10_score = (top_10_count / self.BENCHMARKS['keywords_top_10']['min']) * 25
|
||||
|
||||
# Top 50 score (0-30 points)
|
||||
if top_50_count >= self.BENCHMARKS['keywords_top_50']['target']:
|
||||
top_50_score = 30
|
||||
elif top_50_count >= self.BENCHMARKS['keywords_top_50']['min']:
|
||||
proportion = (top_50_count - self.BENCHMARKS['keywords_top_50']['min']) / \
|
||||
(self.BENCHMARKS['keywords_top_50']['target'] - self.BENCHMARKS['keywords_top_50']['min'])
|
||||
top_50_score = 15 + (proportion * 15)
|
||||
else:
|
||||
top_50_score = (top_50_count / self.BENCHMARKS['keywords_top_50']['min']) * 15
|
||||
|
||||
# Coverage score (0-10 points) - based on top 100
|
||||
coverage_score = min((top_100_count / 30) * 10, 10)
|
||||
|
||||
# Trend score (0-10 points) - are rankings improving?
|
||||
if improving_keywords > 5:
|
||||
trend_score = 10
|
||||
elif improving_keywords > 0:
|
||||
trend_score = 5
|
||||
else:
|
||||
trend_score = 0
|
||||
|
||||
total_score = top_10_score + top_50_score + coverage_score + trend_score
|
||||
|
||||
return round(min(total_score, 100), 1)
|
||||
|
||||
def score_conversion_metrics(self, conversion: Dict[str, Any]) -> float:
|
||||
"""
|
||||
Score conversion performance (0-100).
|
||||
|
||||
Evaluates:
|
||||
- Impression-to-install conversion rate
|
||||
- Download velocity
|
||||
"""
|
||||
conversion_rate = conversion.get('impression_to_install', 0.0)
|
||||
downloads_30d = conversion.get('downloads_last_30_days', 0)
|
||||
downloads_trend = conversion.get('downloads_trend', 'stable') # 'up', 'stable', 'down'
|
||||
|
||||
# Conversion rate score (0-70 points)
|
||||
if conversion_rate >= self.BENCHMARKS['conversion_rate']['target']:
|
||||
conversion_score = 70
|
||||
elif conversion_rate >= self.BENCHMARKS['conversion_rate']['min']:
|
||||
proportion = (conversion_rate - self.BENCHMARKS['conversion_rate']['min']) / \
|
||||
(self.BENCHMARKS['conversion_rate']['target'] - self.BENCHMARKS['conversion_rate']['min'])
|
||||
conversion_score = 35 + (proportion * 35)
|
||||
else:
|
||||
conversion_score = (conversion_rate / self.BENCHMARKS['conversion_rate']['min']) * 35
|
||||
|
||||
# Download velocity score (0-20 points)
|
||||
if downloads_30d > 10000:
|
||||
velocity_score = 20
|
||||
elif downloads_30d > 1000:
|
||||
velocity_score = 15
|
||||
elif downloads_30d > 100:
|
||||
velocity_score = 10
|
||||
else:
|
||||
velocity_score = 5
|
||||
|
||||
# Trend bonus (0-10 points)
|
||||
if downloads_trend == 'up':
|
||||
trend_score = 10
|
||||
elif downloads_trend == 'stable':
|
||||
trend_score = 5
|
||||
else:
|
||||
trend_score = 0
|
||||
|
||||
total_score = conversion_score + velocity_score + trend_score
|
||||
|
||||
return round(min(total_score, 100), 1)
|
||||
|
||||
def generate_recommendations(
|
||||
self,
|
||||
metadata_score: float,
|
||||
ratings_score: float,
|
||||
keyword_score: float,
|
||||
conversion_score: float
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Generate prioritized recommendations based on scores."""
|
||||
recommendations = []
|
||||
|
||||
# Metadata recommendations
|
||||
if metadata_score < 60:
|
||||
recommendations.append({
|
||||
'category': 'metadata_quality',
|
||||
'priority': 'high',
|
||||
'action': 'Optimize app title and description',
|
||||
'details': 'Add more keywords to title, expand description to 1500-2000 characters, improve keyword density to 3-5%',
|
||||
'expected_impact': 'Improve discoverability and ranking potential'
|
||||
})
|
||||
elif metadata_score < 80:
|
||||
recommendations.append({
|
||||
'category': 'metadata_quality',
|
||||
'priority': 'medium',
|
||||
'action': 'Refine metadata for better keyword targeting',
|
||||
'details': 'Test variations of title/subtitle, optimize keyword field for Apple',
|
||||
'expected_impact': 'Incremental ranking improvements'
|
||||
})
|
||||
|
||||
# Ratings recommendations
|
||||
if ratings_score < 60:
|
||||
recommendations.append({
|
||||
'category': 'ratings_reviews',
|
||||
'priority': 'high',
|
||||
'action': 'Improve rating quality and volume',
|
||||
'details': 'Address top user complaints, implement in-app rating prompts, respond to negative reviews',
|
||||
'expected_impact': 'Better conversion rates and trust signals'
|
||||
})
|
||||
elif ratings_score < 80:
|
||||
recommendations.append({
|
||||
'category': 'ratings_reviews',
|
||||
'priority': 'medium',
|
||||
'action': 'Increase rating velocity',
|
||||
'details': 'Optimize timing of rating requests, encourage satisfied users to rate',
|
||||
'expected_impact': 'Sustained rating quality'
|
||||
})
|
||||
|
||||
# Keyword performance recommendations
|
||||
if keyword_score < 60:
|
||||
recommendations.append({
|
||||
'category': 'keyword_performance',
|
||||
'priority': 'high',
|
||||
'action': 'Improve keyword rankings',
|
||||
'details': 'Target long-tail keywords with lower competition, update metadata with high-potential keywords, build backlinks',
|
||||
'expected_impact': 'Significant improvement in organic visibility'
|
||||
})
|
||||
elif keyword_score < 80:
|
||||
recommendations.append({
|
||||
'category': 'keyword_performance',
|
||||
'priority': 'medium',
|
||||
'action': 'Expand keyword coverage',
|
||||
'details': 'Target additional related keywords, test seasonal keywords, localize for new markets',
|
||||
'expected_impact': 'Broader reach and more discovery opportunities'
|
||||
})
|
||||
|
||||
# Conversion recommendations
|
||||
if conversion_score < 60:
|
||||
recommendations.append({
|
||||
'category': 'conversion_metrics',
|
||||
'priority': 'high',
|
||||
'action': 'Optimize store listing for conversions',
|
||||
'details': 'Improve screenshots and icon, strengthen value proposition in description, add video preview',
|
||||
'expected_impact': 'Higher impression-to-install conversion'
|
||||
})
|
||||
elif conversion_score < 80:
|
||||
recommendations.append({
|
||||
'category': 'conversion_metrics',
|
||||
'priority': 'medium',
|
||||
'action': 'Test visual asset variations',
|
||||
'details': 'A/B test different icon designs and screenshot sequences',
|
||||
'expected_impact': 'Incremental conversion improvements'
|
||||
})
|
||||
|
||||
return recommendations
|
||||
|
||||
def _assess_health_status(self, overall_score: float) -> str:
|
||||
"""Assess overall ASO health status."""
|
||||
if overall_score >= 80:
|
||||
return "Excellent - Top-tier ASO performance"
|
||||
elif overall_score >= 65:
|
||||
return "Good - Competitive ASO with room for improvement"
|
||||
elif overall_score >= 50:
|
||||
return "Fair - Needs strategic improvements"
|
||||
else:
|
||||
return "Poor - Requires immediate ASO overhaul"
|
||||
|
||||
def _prioritize_actions(
|
||||
self,
|
||||
recommendations: List[Dict[str, Any]]
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Prioritize actions by impact and urgency."""
|
||||
# Sort by priority (high first) and expected impact
|
||||
priority_order = {'high': 0, 'medium': 1, 'low': 2}
|
||||
|
||||
sorted_recommendations = sorted(
|
||||
recommendations,
|
||||
key=lambda x: priority_order[x['priority']]
|
||||
)
|
||||
|
||||
return sorted_recommendations[:3] # Top 3 priority actions
|
||||
|
||||
def _identify_strengths(self, score_breakdown: Dict[str, Any]) -> List[str]:
|
||||
"""Identify areas of strength (scores >= 75)."""
|
||||
strengths = []
|
||||
|
||||
for category, data in score_breakdown.items():
|
||||
if data['score'] >= 75:
|
||||
strengths.append(
|
||||
f"{category.replace('_', ' ').title()}: {data['score']}/100"
|
||||
)
|
||||
|
||||
return strengths if strengths else ["Focus on building strengths across all areas"]
|
||||
|
||||
def _identify_weaknesses(self, score_breakdown: Dict[str, Any]) -> List[str]:
|
||||
"""Identify areas needing improvement (scores < 60)."""
|
||||
weaknesses = []
|
||||
|
||||
for category, data in score_breakdown.items():
|
||||
if data['score'] < 60:
|
||||
weaknesses.append(
|
||||
f"{category.replace('_', ' ').title()}: {data['score']}/100 - needs improvement"
|
||||
)
|
||||
|
||||
return weaknesses if weaknesses else ["All areas performing adequately"]
|
||||
|
||||
|
||||
def calculate_aso_score(
|
||||
metadata: Dict[str, Any],
|
||||
ratings: Dict[str, Any],
|
||||
keyword_performance: Dict[str, Any],
|
||||
conversion: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Convenience function to calculate ASO score.
|
||||
|
||||
Args:
|
||||
metadata: Metadata quality metrics
|
||||
ratings: Ratings data
|
||||
keyword_performance: Keyword ranking data
|
||||
conversion: Conversion metrics
|
||||
|
||||
Returns:
|
||||
Complete ASO score report
|
||||
"""
|
||||
scorer = ASOScorer()
|
||||
return scorer.calculate_overall_score(
|
||||
metadata,
|
||||
ratings,
|
||||
keyword_performance,
|
||||
conversion
|
||||
)
|
||||
@@ -0,0 +1,577 @@
|
||||
"""
|
||||
Competitor analysis module for App Store Optimization.
|
||||
Analyzes top competitors' ASO strategies and identifies opportunities.
|
||||
"""
|
||||
|
||||
from typing import Dict, List, Any, Optional
|
||||
from collections import Counter
|
||||
import re
|
||||
|
||||
|
||||
class CompetitorAnalyzer:
|
||||
"""Analyzes competitor apps to identify ASO opportunities."""
|
||||
|
||||
def __init__(self, category: str, platform: str = 'apple'):
|
||||
"""
|
||||
Initialize competitor analyzer.
|
||||
|
||||
Args:
|
||||
category: App category (e.g., "Productivity", "Games")
|
||||
platform: 'apple' or 'google'
|
||||
"""
|
||||
self.category = category
|
||||
self.platform = platform
|
||||
self.competitors = []
|
||||
|
||||
def analyze_competitor(
|
||||
self,
|
||||
app_data: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Analyze a single competitor's ASO strategy.
|
||||
|
||||
Args:
|
||||
app_data: Dictionary with app_name, title, description, rating, ratings_count, keywords
|
||||
|
||||
Returns:
|
||||
Comprehensive competitor analysis
|
||||
"""
|
||||
app_name = app_data.get('app_name', '')
|
||||
title = app_data.get('title', '')
|
||||
description = app_data.get('description', '')
|
||||
rating = app_data.get('rating', 0.0)
|
||||
ratings_count = app_data.get('ratings_count', 0)
|
||||
keywords = app_data.get('keywords', [])
|
||||
|
||||
analysis = {
|
||||
'app_name': app_name,
|
||||
'title_analysis': self._analyze_title(title),
|
||||
'description_analysis': self._analyze_description(description),
|
||||
'keyword_strategy': self._extract_keyword_strategy(title, description, keywords),
|
||||
'rating_metrics': {
|
||||
'rating': rating,
|
||||
'ratings_count': ratings_count,
|
||||
'rating_quality': self._assess_rating_quality(rating, ratings_count)
|
||||
},
|
||||
'competitive_strength': self._calculate_competitive_strength(
|
||||
rating,
|
||||
ratings_count,
|
||||
len(description)
|
||||
),
|
||||
'key_differentiators': self._identify_differentiators(description)
|
||||
}
|
||||
|
||||
self.competitors.append(analysis)
|
||||
return analysis
|
||||
|
||||
def compare_competitors(
|
||||
self,
|
||||
competitors_data: List[Dict[str, Any]]
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Compare multiple competitors and identify patterns.
|
||||
|
||||
Args:
|
||||
competitors_data: List of competitor data dictionaries
|
||||
|
||||
Returns:
|
||||
Comparative analysis with insights
|
||||
"""
|
||||
# Analyze each competitor
|
||||
analyses = []
|
||||
for comp_data in competitors_data:
|
||||
analysis = self.analyze_competitor(comp_data)
|
||||
analyses.append(analysis)
|
||||
|
||||
# Extract common keywords across competitors
|
||||
all_keywords = []
|
||||
for analysis in analyses:
|
||||
all_keywords.extend(analysis['keyword_strategy']['primary_keywords'])
|
||||
|
||||
common_keywords = self._find_common_keywords(all_keywords)
|
||||
|
||||
# Identify keyword gaps (used by some but not all)
|
||||
keyword_gaps = self._identify_keyword_gaps(analyses)
|
||||
|
||||
# Rank competitors by strength
|
||||
ranked_competitors = sorted(
|
||||
analyses,
|
||||
key=lambda x: x['competitive_strength'],
|
||||
reverse=True
|
||||
)
|
||||
|
||||
# Analyze rating distribution
|
||||
rating_analysis = self._analyze_rating_distribution(analyses)
|
||||
|
||||
# Identify best practices
|
||||
best_practices = self._identify_best_practices(ranked_competitors)
|
||||
|
||||
return {
|
||||
'category': self.category,
|
||||
'platform': self.platform,
|
||||
'competitors_analyzed': len(analyses),
|
||||
'ranked_competitors': ranked_competitors,
|
||||
'common_keywords': common_keywords,
|
||||
'keyword_gaps': keyword_gaps,
|
||||
'rating_analysis': rating_analysis,
|
||||
'best_practices': best_practices,
|
||||
'opportunities': self._identify_opportunities(
|
||||
analyses,
|
||||
common_keywords,
|
||||
keyword_gaps
|
||||
)
|
||||
}
|
||||
|
||||
def identify_gaps(
|
||||
self,
|
||||
your_app_data: Dict[str, Any],
|
||||
competitors_data: List[Dict[str, Any]]
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Identify gaps between your app and competitors.
|
||||
|
||||
Args:
|
||||
your_app_data: Your app's data
|
||||
competitors_data: List of competitor data
|
||||
|
||||
Returns:
|
||||
Gap analysis with actionable recommendations
|
||||
"""
|
||||
# Analyze your app
|
||||
your_analysis = self.analyze_competitor(your_app_data)
|
||||
|
||||
# Analyze competitors
|
||||
competitor_comparison = self.compare_competitors(competitors_data)
|
||||
|
||||
# Identify keyword gaps
|
||||
your_keywords = set(your_analysis['keyword_strategy']['primary_keywords'])
|
||||
competitor_keywords = set(competitor_comparison['common_keywords'])
|
||||
missing_keywords = competitor_keywords - your_keywords
|
||||
|
||||
# Identify rating gap
|
||||
avg_competitor_rating = competitor_comparison['rating_analysis']['average_rating']
|
||||
rating_gap = avg_competitor_rating - your_analysis['rating_metrics']['rating']
|
||||
|
||||
# Identify description length gap
|
||||
avg_competitor_desc_length = sum(
|
||||
len(comp['description_analysis']['text'])
|
||||
for comp in competitor_comparison['ranked_competitors']
|
||||
) / len(competitor_comparison['ranked_competitors'])
|
||||
your_desc_length = len(your_analysis['description_analysis']['text'])
|
||||
desc_length_gap = avg_competitor_desc_length - your_desc_length
|
||||
|
||||
return {
|
||||
'your_app': your_analysis,
|
||||
'keyword_gaps': {
|
||||
'missing_keywords': list(missing_keywords)[:10],
|
||||
'recommendations': self._generate_keyword_recommendations(missing_keywords)
|
||||
},
|
||||
'rating_gap': {
|
||||
'your_rating': your_analysis['rating_metrics']['rating'],
|
||||
'average_competitor_rating': avg_competitor_rating,
|
||||
'gap': round(rating_gap, 2),
|
||||
'action_items': self._generate_rating_improvement_actions(rating_gap)
|
||||
},
|
||||
'content_gap': {
|
||||
'your_description_length': your_desc_length,
|
||||
'average_competitor_length': int(avg_competitor_desc_length),
|
||||
'gap': int(desc_length_gap),
|
||||
'recommendations': self._generate_content_recommendations(desc_length_gap)
|
||||
},
|
||||
'competitive_positioning': self._assess_competitive_position(
|
||||
your_analysis,
|
||||
competitor_comparison
|
||||
)
|
||||
}
|
||||
|
||||
def _analyze_title(self, title: str) -> Dict[str, Any]:
|
||||
"""Analyze title structure and keyword usage."""
|
||||
parts = re.split(r'[-:|]', title)
|
||||
|
||||
return {
|
||||
'title': title,
|
||||
'length': len(title),
|
||||
'has_brand': len(parts) > 0,
|
||||
'has_keywords': len(parts) > 1,
|
||||
'components': [part.strip() for part in parts],
|
||||
'word_count': len(title.split()),
|
||||
'strategy': 'brand_plus_keywords' if len(parts) > 1 else 'brand_only'
|
||||
}
|
||||
|
||||
def _analyze_description(self, description: str) -> Dict[str, Any]:
|
||||
"""Analyze description structure and content."""
|
||||
lines = description.split('\n')
|
||||
word_count = len(description.split())
|
||||
|
||||
# Check for structural elements
|
||||
has_bullet_points = '•' in description or '*' in description
|
||||
has_sections = any(line.isupper() for line in lines if len(line) > 0)
|
||||
has_call_to_action = any(
|
||||
cta in description.lower()
|
||||
for cta in ['download', 'try', 'get', 'start', 'join']
|
||||
)
|
||||
|
||||
# Extract features mentioned
|
||||
features = self._extract_features(description)
|
||||
|
||||
return {
|
||||
'text': description,
|
||||
'length': len(description),
|
||||
'word_count': word_count,
|
||||
'structure': {
|
||||
'has_bullet_points': has_bullet_points,
|
||||
'has_sections': has_sections,
|
||||
'has_call_to_action': has_call_to_action
|
||||
},
|
||||
'features_mentioned': features,
|
||||
'readability': 'good' if 50 <= word_count <= 300 else 'needs_improvement'
|
||||
}
|
||||
|
||||
def _extract_keyword_strategy(
|
||||
self,
|
||||
title: str,
|
||||
description: str,
|
||||
explicit_keywords: List[str]
|
||||
) -> Dict[str, Any]:
|
||||
"""Extract keyword strategy from metadata."""
|
||||
# Extract keywords from title
|
||||
title_keywords = [word.lower() for word in title.split() if len(word) > 3]
|
||||
|
||||
# Extract frequently used words from description
|
||||
desc_words = re.findall(r'\b\w{4,}\b', description.lower())
|
||||
word_freq = Counter(desc_words)
|
||||
frequent_words = [word for word, count in word_freq.most_common(15) if count > 2]
|
||||
|
||||
# Combine with explicit keywords
|
||||
all_keywords = list(set(title_keywords + frequent_words + explicit_keywords))
|
||||
|
||||
return {
|
||||
'primary_keywords': title_keywords,
|
||||
'description_keywords': frequent_words[:10],
|
||||
'explicit_keywords': explicit_keywords,
|
||||
'total_unique_keywords': len(all_keywords),
|
||||
'keyword_focus': self._assess_keyword_focus(title_keywords, frequent_words)
|
||||
}
|
||||
|
||||
def _assess_rating_quality(self, rating: float, ratings_count: int) -> str:
|
||||
"""Assess the quality of ratings."""
|
||||
if ratings_count < 100:
|
||||
return 'insufficient_data'
|
||||
elif rating >= 4.5 and ratings_count > 1000:
|
||||
return 'excellent'
|
||||
elif rating >= 4.0 and ratings_count > 500:
|
||||
return 'good'
|
||||
elif rating >= 3.5:
|
||||
return 'average'
|
||||
else:
|
||||
return 'poor'
|
||||
|
||||
def _calculate_competitive_strength(
|
||||
self,
|
||||
rating: float,
|
||||
ratings_count: int,
|
||||
description_length: int
|
||||
) -> float:
|
||||
"""
|
||||
Calculate overall competitive strength (0-100).
|
||||
|
||||
Factors:
|
||||
- Rating quality (40%)
|
||||
- Rating volume (30%)
|
||||
- Metadata quality (30%)
|
||||
"""
|
||||
# Rating quality score (0-40)
|
||||
rating_score = (rating / 5.0) * 40
|
||||
|
||||
# Rating volume score (0-30)
|
||||
volume_score = min((ratings_count / 10000) * 30, 30)
|
||||
|
||||
# Metadata quality score (0-30)
|
||||
metadata_score = min((description_length / 2000) * 30, 30)
|
||||
|
||||
total_score = rating_score + volume_score + metadata_score
|
||||
|
||||
return round(total_score, 1)
|
||||
|
||||
def _identify_differentiators(self, description: str) -> List[str]:
|
||||
"""Identify key differentiators from description."""
|
||||
differentiator_keywords = [
|
||||
'unique', 'only', 'first', 'best', 'leading', 'exclusive',
|
||||
'revolutionary', 'innovative', 'patent', 'award'
|
||||
]
|
||||
|
||||
differentiators = []
|
||||
sentences = description.split('.')
|
||||
|
||||
for sentence in sentences:
|
||||
sentence_lower = sentence.lower()
|
||||
if any(keyword in sentence_lower for keyword in differentiator_keywords):
|
||||
differentiators.append(sentence.strip())
|
||||
|
||||
return differentiators[:5]
|
||||
|
||||
def _find_common_keywords(self, all_keywords: List[str]) -> List[str]:
|
||||
"""Find keywords used by multiple competitors."""
|
||||
keyword_counts = Counter(all_keywords)
|
||||
# Return keywords used by at least 2 competitors
|
||||
common = [kw for kw, count in keyword_counts.items() if count >= 2]
|
||||
return sorted(common, key=lambda x: keyword_counts[x], reverse=True)[:20]
|
||||
|
||||
def _identify_keyword_gaps(self, analyses: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||||
"""Identify keywords used by some competitors but not others."""
|
||||
all_keywords_by_app = {}
|
||||
|
||||
for analysis in analyses:
|
||||
app_name = analysis['app_name']
|
||||
keywords = analysis['keyword_strategy']['primary_keywords']
|
||||
all_keywords_by_app[app_name] = set(keywords)
|
||||
|
||||
# Find keywords used by some but not all
|
||||
all_keywords_set = set()
|
||||
for keywords in all_keywords_by_app.values():
|
||||
all_keywords_set.update(keywords)
|
||||
|
||||
gaps = []
|
||||
for keyword in all_keywords_set:
|
||||
using_apps = [
|
||||
app for app, keywords in all_keywords_by_app.items()
|
||||
if keyword in keywords
|
||||
]
|
||||
if 1 < len(using_apps) < len(analyses):
|
||||
gaps.append({
|
||||
'keyword': keyword,
|
||||
'used_by': using_apps,
|
||||
'usage_percentage': round(len(using_apps) / len(analyses) * 100, 1)
|
||||
})
|
||||
|
||||
return sorted(gaps, key=lambda x: x['usage_percentage'], reverse=True)[:15]
|
||||
|
||||
def _analyze_rating_distribution(self, analyses: List[Dict[str, Any]]) -> Dict[str, Any]:
|
||||
"""Analyze rating distribution across competitors."""
|
||||
ratings = [a['rating_metrics']['rating'] for a in analyses]
|
||||
ratings_counts = [a['rating_metrics']['ratings_count'] for a in analyses]
|
||||
|
||||
return {
|
||||
'average_rating': round(sum(ratings) / len(ratings), 2),
|
||||
'highest_rating': max(ratings),
|
||||
'lowest_rating': min(ratings),
|
||||
'average_ratings_count': int(sum(ratings_counts) / len(ratings_counts)),
|
||||
'total_ratings_in_category': sum(ratings_counts)
|
||||
}
|
||||
|
||||
def _identify_best_practices(self, ranked_competitors: List[Dict[str, Any]]) -> List[str]:
|
||||
"""Identify best practices from top competitors."""
|
||||
if not ranked_competitors:
|
||||
return []
|
||||
|
||||
top_competitor = ranked_competitors[0]
|
||||
practices = []
|
||||
|
||||
# Title strategy
|
||||
title_analysis = top_competitor['title_analysis']
|
||||
if title_analysis['has_keywords']:
|
||||
practices.append(
|
||||
f"Title Strategy: Include primary keyword in title (e.g., '{title_analysis['title']}')"
|
||||
)
|
||||
|
||||
# Description structure
|
||||
desc_analysis = top_competitor['description_analysis']
|
||||
if desc_analysis['structure']['has_bullet_points']:
|
||||
practices.append("Description: Use bullet points to highlight key features")
|
||||
|
||||
if desc_analysis['structure']['has_sections']:
|
||||
practices.append("Description: Organize content with clear section headers")
|
||||
|
||||
# Rating strategy
|
||||
rating_quality = top_competitor['rating_metrics']['rating_quality']
|
||||
if rating_quality in ['excellent', 'good']:
|
||||
practices.append(
|
||||
f"Ratings: Maintain high rating quality ({top_competitor['rating_metrics']['rating']}★) "
|
||||
f"with significant volume ({top_competitor['rating_metrics']['ratings_count']} ratings)"
|
||||
)
|
||||
|
||||
return practices[:5]
|
||||
|
||||
def _identify_opportunities(
|
||||
self,
|
||||
analyses: List[Dict[str, Any]],
|
||||
common_keywords: List[str],
|
||||
keyword_gaps: List[Dict[str, Any]]
|
||||
) -> List[str]:
|
||||
"""Identify ASO opportunities based on competitive analysis."""
|
||||
opportunities = []
|
||||
|
||||
# Keyword opportunities from gaps
|
||||
if keyword_gaps:
|
||||
underutilized_keywords = [
|
||||
gap['keyword'] for gap in keyword_gaps
|
||||
if gap['usage_percentage'] < 50
|
||||
]
|
||||
if underutilized_keywords:
|
||||
opportunities.append(
|
||||
f"Target underutilized keywords: {', '.join(underutilized_keywords[:5])}"
|
||||
)
|
||||
|
||||
# Rating opportunity
|
||||
avg_rating = sum(a['rating_metrics']['rating'] for a in analyses) / len(analyses)
|
||||
if avg_rating < 4.5:
|
||||
opportunities.append(
|
||||
f"Category average rating is {avg_rating:.1f} - opportunity to differentiate with higher ratings"
|
||||
)
|
||||
|
||||
# Content depth opportunity
|
||||
avg_desc_length = sum(
|
||||
a['description_analysis']['length'] for a in analyses
|
||||
) / len(analyses)
|
||||
if avg_desc_length < 1500:
|
||||
opportunities.append(
|
||||
"Competitors have relatively short descriptions - opportunity to provide more comprehensive information"
|
||||
)
|
||||
|
||||
return opportunities[:5]
|
||||
|
||||
def _extract_features(self, description: str) -> List[str]:
|
||||
"""Extract feature mentions from description."""
|
||||
# Look for bullet points or numbered lists
|
||||
lines = description.split('\n')
|
||||
features = []
|
||||
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
# Check if line starts with bullet or number
|
||||
if line and (line[0] in ['•', '*', '-', '✓'] or line[0].isdigit()):
|
||||
# Clean the line
|
||||
cleaned = re.sub(r'^[•*\-✓\d.)\s]+', '', line)
|
||||
if cleaned:
|
||||
features.append(cleaned)
|
||||
|
||||
return features[:10]
|
||||
|
||||
def _assess_keyword_focus(
|
||||
self,
|
||||
title_keywords: List[str],
|
||||
description_keywords: List[str]
|
||||
) -> str:
|
||||
"""Assess keyword focus strategy."""
|
||||
overlap = set(title_keywords) & set(description_keywords)
|
||||
|
||||
if len(overlap) >= 3:
|
||||
return 'consistent_focus'
|
||||
elif len(overlap) >= 1:
|
||||
return 'moderate_focus'
|
||||
else:
|
||||
return 'broad_focus'
|
||||
|
||||
def _generate_keyword_recommendations(self, missing_keywords: set) -> List[str]:
|
||||
"""Generate recommendations for missing keywords."""
|
||||
if not missing_keywords:
|
||||
return ["Your keyword coverage is comprehensive"]
|
||||
|
||||
recommendations = []
|
||||
missing_list = list(missing_keywords)[:5]
|
||||
|
||||
recommendations.append(
|
||||
f"Consider adding these competitor keywords: {', '.join(missing_list)}"
|
||||
)
|
||||
recommendations.append(
|
||||
"Test keyword variations in subtitle/promotional text first"
|
||||
)
|
||||
recommendations.append(
|
||||
"Monitor competitor keyword changes monthly"
|
||||
)
|
||||
|
||||
return recommendations
|
||||
|
||||
def _generate_rating_improvement_actions(self, rating_gap: float) -> List[str]:
|
||||
"""Generate actions to improve ratings."""
|
||||
actions = []
|
||||
|
||||
if rating_gap > 0.5:
|
||||
actions.append("CRITICAL: Significant rating gap - prioritize user satisfaction improvements")
|
||||
actions.append("Analyze negative reviews to identify top issues")
|
||||
actions.append("Implement in-app rating prompts after positive experiences")
|
||||
actions.append("Respond to all negative reviews professionally")
|
||||
elif rating_gap > 0.2:
|
||||
actions.append("Focus on incremental improvements to close rating gap")
|
||||
actions.append("Optimize timing of rating requests")
|
||||
else:
|
||||
actions.append("Ratings are competitive - maintain quality and continue improvements")
|
||||
|
||||
return actions
|
||||
|
||||
def _generate_content_recommendations(self, desc_length_gap: int) -> List[str]:
|
||||
"""Generate content recommendations based on length gap."""
|
||||
recommendations = []
|
||||
|
||||
if desc_length_gap > 500:
|
||||
recommendations.append(
|
||||
"Expand description to match competitor detail level"
|
||||
)
|
||||
recommendations.append(
|
||||
"Add use case examples and success stories"
|
||||
)
|
||||
recommendations.append(
|
||||
"Include more feature explanations and benefits"
|
||||
)
|
||||
elif desc_length_gap < -500:
|
||||
recommendations.append(
|
||||
"Consider condensing description for better readability"
|
||||
)
|
||||
recommendations.append(
|
||||
"Focus on most important features first"
|
||||
)
|
||||
else:
|
||||
recommendations.append(
|
||||
"Description length is competitive"
|
||||
)
|
||||
|
||||
return recommendations
|
||||
|
||||
def _assess_competitive_position(
|
||||
self,
|
||||
your_analysis: Dict[str, Any],
|
||||
competitor_comparison: Dict[str, Any]
|
||||
) -> str:
|
||||
"""Assess your competitive position."""
|
||||
your_strength = your_analysis['competitive_strength']
|
||||
competitors = competitor_comparison['ranked_competitors']
|
||||
|
||||
if not competitors:
|
||||
return "No comparison data available"
|
||||
|
||||
# Find where you'd rank
|
||||
better_than_count = sum(
|
||||
1 for comp in competitors
|
||||
if your_strength > comp['competitive_strength']
|
||||
)
|
||||
|
||||
position_percentage = (better_than_count / len(competitors)) * 100
|
||||
|
||||
if position_percentage >= 75:
|
||||
return "Strong Position: Top quartile in competitive strength"
|
||||
elif position_percentage >= 50:
|
||||
return "Competitive Position: Above average, opportunities for improvement"
|
||||
elif position_percentage >= 25:
|
||||
return "Challenging Position: Below average, requires strategic improvements"
|
||||
else:
|
||||
return "Weak Position: Bottom quartile, major ASO overhaul needed"
|
||||
|
||||
|
||||
def analyze_competitor_set(
|
||||
category: str,
|
||||
competitors_data: List[Dict[str, Any]],
|
||||
platform: str = 'apple'
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Convenience function to analyze a set of competitors.
|
||||
|
||||
Args:
|
||||
category: App category
|
||||
competitors_data: List of competitor data
|
||||
platform: 'apple' or 'google'
|
||||
|
||||
Returns:
|
||||
Complete competitive analysis
|
||||
"""
|
||||
analyzer = CompetitorAnalyzer(category, platform)
|
||||
return analyzer.compare_competitors(competitors_data)
|
||||
@@ -0,0 +1,406 @@
|
||||
"""
|
||||
Keyword analysis module for App Store Optimization.
|
||||
Analyzes keyword search volume, competition, and relevance for app discovery.
|
||||
"""
|
||||
|
||||
from typing import Dict, List, Any, Optional, Tuple
|
||||
import re
|
||||
from collections import Counter
|
||||
|
||||
|
||||
class KeywordAnalyzer:
|
||||
"""Analyzes keywords for ASO effectiveness."""
|
||||
|
||||
# Competition level thresholds (based on number of competing apps)
|
||||
COMPETITION_THRESHOLDS = {
|
||||
'low': 1000,
|
||||
'medium': 5000,
|
||||
'high': 10000
|
||||
}
|
||||
|
||||
# Search volume categories (monthly searches estimate)
|
||||
VOLUME_CATEGORIES = {
|
||||
'very_low': 1000,
|
||||
'low': 5000,
|
||||
'medium': 20000,
|
||||
'high': 100000,
|
||||
'very_high': 500000
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize keyword analyzer."""
|
||||
self.analyzed_keywords = {}
|
||||
|
||||
def analyze_keyword(
|
||||
self,
|
||||
keyword: str,
|
||||
search_volume: int = 0,
|
||||
competing_apps: int = 0,
|
||||
relevance_score: float = 0.0
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Analyze a single keyword for ASO potential.
|
||||
|
||||
Args:
|
||||
keyword: The keyword to analyze
|
||||
search_volume: Estimated monthly search volume
|
||||
competing_apps: Number of apps competing for this keyword
|
||||
relevance_score: Relevance to your app (0.0-1.0)
|
||||
|
||||
Returns:
|
||||
Dictionary with keyword analysis
|
||||
"""
|
||||
competition_level = self._calculate_competition_level(competing_apps)
|
||||
volume_category = self._categorize_search_volume(search_volume)
|
||||
difficulty_score = self._calculate_keyword_difficulty(
|
||||
search_volume,
|
||||
competing_apps
|
||||
)
|
||||
|
||||
# Calculate potential score (0-100)
|
||||
potential_score = self._calculate_potential_score(
|
||||
search_volume,
|
||||
competing_apps,
|
||||
relevance_score
|
||||
)
|
||||
|
||||
analysis = {
|
||||
'keyword': keyword,
|
||||
'search_volume': search_volume,
|
||||
'volume_category': volume_category,
|
||||
'competing_apps': competing_apps,
|
||||
'competition_level': competition_level,
|
||||
'relevance_score': relevance_score,
|
||||
'difficulty_score': difficulty_score,
|
||||
'potential_score': potential_score,
|
||||
'recommendation': self._generate_recommendation(
|
||||
potential_score,
|
||||
difficulty_score,
|
||||
relevance_score
|
||||
),
|
||||
'keyword_length': len(keyword.split()),
|
||||
'is_long_tail': len(keyword.split()) >= 3
|
||||
}
|
||||
|
||||
self.analyzed_keywords[keyword] = analysis
|
||||
return analysis
|
||||
|
||||
def compare_keywords(self, keywords_data: List[Dict[str, Any]]) -> Dict[str, Any]:
|
||||
"""
|
||||
Compare multiple keywords and rank by potential.
|
||||
|
||||
Args:
|
||||
keywords_data: List of dicts with keyword, search_volume, competing_apps, relevance_score
|
||||
|
||||
Returns:
|
||||
Comparison report with ranked keywords
|
||||
"""
|
||||
analyses = []
|
||||
for kw_data in keywords_data:
|
||||
analysis = self.analyze_keyword(
|
||||
keyword=kw_data['keyword'],
|
||||
search_volume=kw_data.get('search_volume', 0),
|
||||
competing_apps=kw_data.get('competing_apps', 0),
|
||||
relevance_score=kw_data.get('relevance_score', 0.0)
|
||||
)
|
||||
analyses.append(analysis)
|
||||
|
||||
# Sort by potential score (descending)
|
||||
ranked_keywords = sorted(
|
||||
analyses,
|
||||
key=lambda x: x['potential_score'],
|
||||
reverse=True
|
||||
)
|
||||
|
||||
# Categorize keywords
|
||||
primary_keywords = [
|
||||
kw for kw in ranked_keywords
|
||||
if kw['potential_score'] >= 70 and kw['relevance_score'] >= 0.8
|
||||
]
|
||||
|
||||
secondary_keywords = [
|
||||
kw for kw in ranked_keywords
|
||||
if 50 <= kw['potential_score'] < 70 and kw['relevance_score'] >= 0.6
|
||||
]
|
||||
|
||||
long_tail_keywords = [
|
||||
kw for kw in ranked_keywords
|
||||
if kw['is_long_tail'] and kw['relevance_score'] >= 0.7
|
||||
]
|
||||
|
||||
return {
|
||||
'total_keywords_analyzed': len(analyses),
|
||||
'ranked_keywords': ranked_keywords,
|
||||
'primary_keywords': primary_keywords[:5], # Top 5
|
||||
'secondary_keywords': secondary_keywords[:10], # Top 10
|
||||
'long_tail_keywords': long_tail_keywords[:10], # Top 10
|
||||
'summary': self._generate_comparison_summary(
|
||||
primary_keywords,
|
||||
secondary_keywords,
|
||||
long_tail_keywords
|
||||
)
|
||||
}
|
||||
|
||||
def find_long_tail_opportunities(
|
||||
self,
|
||||
base_keyword: str,
|
||||
modifiers: List[str]
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Generate long-tail keyword variations.
|
||||
|
||||
Args:
|
||||
base_keyword: Core keyword (e.g., "task manager")
|
||||
modifiers: List of modifiers (e.g., ["free", "simple", "team"])
|
||||
|
||||
Returns:
|
||||
List of long-tail keyword suggestions
|
||||
"""
|
||||
long_tail_keywords = []
|
||||
|
||||
# Generate combinations
|
||||
for modifier in modifiers:
|
||||
# Modifier + base
|
||||
variation1 = f"{modifier} {base_keyword}"
|
||||
long_tail_keywords.append({
|
||||
'keyword': variation1,
|
||||
'pattern': 'modifier_base',
|
||||
'estimated_competition': 'low',
|
||||
'rationale': f"Less competitive variation of '{base_keyword}'"
|
||||
})
|
||||
|
||||
# Base + modifier
|
||||
variation2 = f"{base_keyword} {modifier}"
|
||||
long_tail_keywords.append({
|
||||
'keyword': variation2,
|
||||
'pattern': 'base_modifier',
|
||||
'estimated_competition': 'low',
|
||||
'rationale': f"Specific use-case variation of '{base_keyword}'"
|
||||
})
|
||||
|
||||
# Add question-based long-tail
|
||||
question_words = ['how', 'what', 'best', 'top']
|
||||
for q_word in question_words:
|
||||
question_keyword = f"{q_word} {base_keyword}"
|
||||
long_tail_keywords.append({
|
||||
'keyword': question_keyword,
|
||||
'pattern': 'question_based',
|
||||
'estimated_competition': 'very_low',
|
||||
'rationale': f"Informational search query"
|
||||
})
|
||||
|
||||
return long_tail_keywords
|
||||
|
||||
def extract_keywords_from_text(
|
||||
self,
|
||||
text: str,
|
||||
min_word_length: int = 3
|
||||
) -> List[Tuple[str, int]]:
|
||||
"""
|
||||
Extract potential keywords from text (descriptions, reviews).
|
||||
|
||||
Args:
|
||||
text: Text to analyze
|
||||
min_word_length: Minimum word length to consider
|
||||
|
||||
Returns:
|
||||
List of (keyword, frequency) tuples
|
||||
"""
|
||||
# Clean and normalize text
|
||||
text = text.lower()
|
||||
text = re.sub(r'[^\w\s]', ' ', text)
|
||||
|
||||
# Extract words
|
||||
words = text.split()
|
||||
|
||||
# Filter by length
|
||||
words = [w for w in words if len(w) >= min_word_length]
|
||||
|
||||
# Remove common stop words
|
||||
stop_words = {
|
||||
'the', 'and', 'for', 'with', 'this', 'that', 'from', 'have',
|
||||
'but', 'not', 'you', 'all', 'can', 'are', 'was', 'were', 'been'
|
||||
}
|
||||
words = [w for w in words if w not in stop_words]
|
||||
|
||||
# Count frequency
|
||||
word_counts = Counter(words)
|
||||
|
||||
# Extract 2-word phrases
|
||||
phrases = []
|
||||
for i in range(len(words) - 1):
|
||||
phrase = f"{words[i]} {words[i+1]}"
|
||||
phrases.append(phrase)
|
||||
|
||||
phrase_counts = Counter(phrases)
|
||||
|
||||
# Combine and sort
|
||||
all_keywords = list(word_counts.items()) + list(phrase_counts.items())
|
||||
all_keywords.sort(key=lambda x: x[1], reverse=True)
|
||||
|
||||
return all_keywords[:50] # Top 50
|
||||
|
||||
def calculate_keyword_density(
|
||||
self,
|
||||
text: str,
|
||||
target_keywords: List[str]
|
||||
) -> Dict[str, float]:
|
||||
"""
|
||||
Calculate keyword density in text.
|
||||
|
||||
Args:
|
||||
text: Text to analyze (title, description)
|
||||
target_keywords: Keywords to check density for
|
||||
|
||||
Returns:
|
||||
Dictionary of keyword: density (percentage)
|
||||
"""
|
||||
text_lower = text.lower()
|
||||
total_words = len(text_lower.split())
|
||||
|
||||
densities = {}
|
||||
for keyword in target_keywords:
|
||||
keyword_lower = keyword.lower()
|
||||
occurrences = text_lower.count(keyword_lower)
|
||||
density = (occurrences / total_words) * 100 if total_words > 0 else 0
|
||||
densities[keyword] = round(density, 2)
|
||||
|
||||
return densities
|
||||
|
||||
def _calculate_competition_level(self, competing_apps: int) -> str:
|
||||
"""Determine competition level based on number of competing apps."""
|
||||
if competing_apps < self.COMPETITION_THRESHOLDS['low']:
|
||||
return 'low'
|
||||
elif competing_apps < self.COMPETITION_THRESHOLDS['medium']:
|
||||
return 'medium'
|
||||
elif competing_apps < self.COMPETITION_THRESHOLDS['high']:
|
||||
return 'high'
|
||||
else:
|
||||
return 'very_high'
|
||||
|
||||
def _categorize_search_volume(self, search_volume: int) -> str:
|
||||
"""Categorize search volume."""
|
||||
if search_volume < self.VOLUME_CATEGORIES['very_low']:
|
||||
return 'very_low'
|
||||
elif search_volume < self.VOLUME_CATEGORIES['low']:
|
||||
return 'low'
|
||||
elif search_volume < self.VOLUME_CATEGORIES['medium']:
|
||||
return 'medium'
|
||||
elif search_volume < self.VOLUME_CATEGORIES['high']:
|
||||
return 'high'
|
||||
else:
|
||||
return 'very_high'
|
||||
|
||||
def _calculate_keyword_difficulty(
|
||||
self,
|
||||
search_volume: int,
|
||||
competing_apps: int
|
||||
) -> float:
|
||||
"""
|
||||
Calculate keyword difficulty score (0-100).
|
||||
Higher score = harder to rank.
|
||||
"""
|
||||
if competing_apps == 0:
|
||||
return 0.0
|
||||
|
||||
# Competition factor (0-1)
|
||||
competition_factor = min(competing_apps / 50000, 1.0)
|
||||
|
||||
# Volume factor (0-1) - higher volume = more difficulty
|
||||
volume_factor = min(search_volume / 1000000, 1.0)
|
||||
|
||||
# Difficulty score (weighted average)
|
||||
difficulty = (competition_factor * 0.7 + volume_factor * 0.3) * 100
|
||||
|
||||
return round(difficulty, 1)
|
||||
|
||||
def _calculate_potential_score(
|
||||
self,
|
||||
search_volume: int,
|
||||
competing_apps: int,
|
||||
relevance_score: float
|
||||
) -> float:
|
||||
"""
|
||||
Calculate overall keyword potential (0-100).
|
||||
Higher score = better opportunity.
|
||||
"""
|
||||
# Volume score (0-40 points)
|
||||
volume_score = min((search_volume / 100000) * 40, 40)
|
||||
|
||||
# Competition score (0-30 points) - inverse relationship
|
||||
if competing_apps > 0:
|
||||
competition_score = max(30 - (competing_apps / 500), 0)
|
||||
else:
|
||||
competition_score = 30
|
||||
|
||||
# Relevance score (0-30 points)
|
||||
relevance_points = relevance_score * 30
|
||||
|
||||
total_score = volume_score + competition_score + relevance_points
|
||||
|
||||
return round(min(total_score, 100), 1)
|
||||
|
||||
def _generate_recommendation(
|
||||
self,
|
||||
potential_score: float,
|
||||
difficulty_score: float,
|
||||
relevance_score: float
|
||||
) -> str:
|
||||
"""Generate actionable recommendation for keyword."""
|
||||
if relevance_score < 0.5:
|
||||
return "Low relevance - avoid targeting"
|
||||
|
||||
if potential_score >= 70:
|
||||
return "High priority - target immediately"
|
||||
elif potential_score >= 50:
|
||||
if difficulty_score < 50:
|
||||
return "Good opportunity - include in metadata"
|
||||
else:
|
||||
return "Competitive - use in description, not title"
|
||||
elif potential_score >= 30:
|
||||
return "Secondary keyword - use for long-tail variations"
|
||||
else:
|
||||
return "Low potential - deprioritize"
|
||||
|
||||
def _generate_comparison_summary(
|
||||
self,
|
||||
primary_keywords: List[Dict[str, Any]],
|
||||
secondary_keywords: List[Dict[str, Any]],
|
||||
long_tail_keywords: List[Dict[str, Any]]
|
||||
) -> str:
|
||||
"""Generate summary of keyword comparison."""
|
||||
summary_parts = []
|
||||
|
||||
summary_parts.append(
|
||||
f"Identified {len(primary_keywords)} high-priority primary keywords."
|
||||
)
|
||||
|
||||
if primary_keywords:
|
||||
top_keyword = primary_keywords[0]['keyword']
|
||||
summary_parts.append(
|
||||
f"Top recommendation: '{top_keyword}' (potential score: {primary_keywords[0]['potential_score']})."
|
||||
)
|
||||
|
||||
summary_parts.append(
|
||||
f"Found {len(secondary_keywords)} secondary keywords for description and metadata."
|
||||
)
|
||||
|
||||
summary_parts.append(
|
||||
f"Discovered {len(long_tail_keywords)} long-tail opportunities with lower competition."
|
||||
)
|
||||
|
||||
return " ".join(summary_parts)
|
||||
|
||||
|
||||
def analyze_keyword_set(keywords_data: List[Dict[str, Any]]) -> Dict[str, Any]:
|
||||
"""
|
||||
Convenience function to analyze a set of keywords.
|
||||
|
||||
Args:
|
||||
keywords_data: List of keyword data dictionaries
|
||||
|
||||
Returns:
|
||||
Complete analysis report
|
||||
"""
|
||||
analyzer = KeywordAnalyzer()
|
||||
return analyzer.compare_keywords(keywords_data)
|
||||
@@ -0,0 +1,739 @@
|
||||
"""
|
||||
Launch checklist module for App Store Optimization.
|
||||
Generates comprehensive pre-launch and update checklists.
|
||||
"""
|
||||
|
||||
from typing import Dict, List, Any, Optional
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
|
||||
class LaunchChecklistGenerator:
|
||||
"""Generates comprehensive checklists for app launches and updates."""
|
||||
|
||||
def __init__(self, platform: str = 'both'):
|
||||
"""
|
||||
Initialize checklist generator.
|
||||
|
||||
Args:
|
||||
platform: 'apple', 'google', or 'both'
|
||||
"""
|
||||
if platform not in ['apple', 'google', 'both']:
|
||||
raise ValueError("Platform must be 'apple', 'google', or 'both'")
|
||||
|
||||
self.platform = platform
|
||||
|
||||
def generate_prelaunch_checklist(
|
||||
self,
|
||||
app_info: Dict[str, Any],
|
||||
launch_date: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Generate comprehensive pre-launch checklist.
|
||||
|
||||
Args:
|
||||
app_info: App information (name, category, target_audience)
|
||||
launch_date: Target launch date (YYYY-MM-DD)
|
||||
|
||||
Returns:
|
||||
Complete pre-launch checklist
|
||||
"""
|
||||
checklist = {
|
||||
'app_info': app_info,
|
||||
'launch_date': launch_date,
|
||||
'checklists': {}
|
||||
}
|
||||
|
||||
# Generate platform-specific checklists
|
||||
if self.platform in ['apple', 'both']:
|
||||
checklist['checklists']['apple'] = self._generate_apple_checklist(app_info)
|
||||
|
||||
if self.platform in ['google', 'both']:
|
||||
checklist['checklists']['google'] = self._generate_google_checklist(app_info)
|
||||
|
||||
# Add universal checklist items
|
||||
checklist['checklists']['universal'] = self._generate_universal_checklist(app_info)
|
||||
|
||||
# Generate timeline
|
||||
if launch_date:
|
||||
checklist['timeline'] = self._generate_launch_timeline(launch_date)
|
||||
|
||||
# Calculate completion status
|
||||
checklist['summary'] = self._calculate_checklist_summary(checklist['checklists'])
|
||||
|
||||
return checklist
|
||||
|
||||
def validate_app_store_compliance(
|
||||
self,
|
||||
app_data: Dict[str, Any],
|
||||
platform: str = 'apple'
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Validate compliance with app store guidelines.
|
||||
|
||||
Args:
|
||||
app_data: App data including metadata, privacy policy, etc.
|
||||
platform: 'apple' or 'google'
|
||||
|
||||
Returns:
|
||||
Compliance validation report
|
||||
"""
|
||||
validation_results = {
|
||||
'platform': platform,
|
||||
'is_compliant': True,
|
||||
'errors': [],
|
||||
'warnings': [],
|
||||
'recommendations': []
|
||||
}
|
||||
|
||||
if platform == 'apple':
|
||||
self._validate_apple_compliance(app_data, validation_results)
|
||||
elif platform == 'google':
|
||||
self._validate_google_compliance(app_data, validation_results)
|
||||
|
||||
# Determine overall compliance
|
||||
validation_results['is_compliant'] = len(validation_results['errors']) == 0
|
||||
|
||||
return validation_results
|
||||
|
||||
def create_update_plan(
|
||||
self,
|
||||
current_version: str,
|
||||
planned_features: List[str],
|
||||
update_frequency: str = 'monthly'
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Create update cadence and feature rollout plan.
|
||||
|
||||
Args:
|
||||
current_version: Current app version
|
||||
planned_features: List of planned features
|
||||
update_frequency: 'weekly', 'biweekly', 'monthly', 'quarterly'
|
||||
|
||||
Returns:
|
||||
Update plan with cadence and feature schedule
|
||||
"""
|
||||
# Calculate next versions
|
||||
next_versions = self._calculate_next_versions(
|
||||
current_version,
|
||||
update_frequency,
|
||||
len(planned_features)
|
||||
)
|
||||
|
||||
# Distribute features across versions
|
||||
feature_schedule = self._distribute_features(
|
||||
planned_features,
|
||||
next_versions
|
||||
)
|
||||
|
||||
# Generate "What's New" templates
|
||||
whats_new_templates = [
|
||||
self._generate_whats_new_template(version_data)
|
||||
for version_data in feature_schedule
|
||||
]
|
||||
|
||||
return {
|
||||
'current_version': current_version,
|
||||
'update_frequency': update_frequency,
|
||||
'planned_updates': len(feature_schedule),
|
||||
'feature_schedule': feature_schedule,
|
||||
'whats_new_templates': whats_new_templates,
|
||||
'recommendations': self._generate_update_recommendations(update_frequency)
|
||||
}
|
||||
|
||||
def optimize_launch_timing(
|
||||
self,
|
||||
app_category: str,
|
||||
target_audience: str,
|
||||
current_date: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Recommend optimal launch timing.
|
||||
|
||||
Args:
|
||||
app_category: App category
|
||||
target_audience: Target audience description
|
||||
current_date: Current date (YYYY-MM-DD), defaults to today
|
||||
|
||||
Returns:
|
||||
Launch timing recommendations
|
||||
"""
|
||||
if not current_date:
|
||||
current_date = datetime.now().strftime('%Y-%m-%d')
|
||||
|
||||
# Analyze launch timing factors
|
||||
day_of_week_rec = self._recommend_day_of_week(app_category)
|
||||
seasonal_rec = self._recommend_seasonal_timing(app_category, current_date)
|
||||
competitive_rec = self._analyze_competitive_timing(app_category)
|
||||
|
||||
# Calculate optimal dates
|
||||
optimal_dates = self._calculate_optimal_dates(
|
||||
current_date,
|
||||
day_of_week_rec,
|
||||
seasonal_rec
|
||||
)
|
||||
|
||||
return {
|
||||
'current_date': current_date,
|
||||
'optimal_launch_dates': optimal_dates,
|
||||
'day_of_week_recommendation': day_of_week_rec,
|
||||
'seasonal_considerations': seasonal_rec,
|
||||
'competitive_timing': competitive_rec,
|
||||
'final_recommendation': self._generate_timing_recommendation(
|
||||
optimal_dates,
|
||||
seasonal_rec
|
||||
)
|
||||
}
|
||||
|
||||
def plan_seasonal_campaigns(
|
||||
self,
|
||||
app_category: str,
|
||||
current_month: int = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Identify seasonal opportunities for ASO campaigns.
|
||||
|
||||
Args:
|
||||
app_category: App category
|
||||
current_month: Current month (1-12), defaults to current
|
||||
|
||||
Returns:
|
||||
Seasonal campaign opportunities
|
||||
"""
|
||||
if not current_month:
|
||||
current_month = datetime.now().month
|
||||
|
||||
# Identify relevant seasonal events
|
||||
seasonal_opportunities = self._identify_seasonal_opportunities(
|
||||
app_category,
|
||||
current_month
|
||||
)
|
||||
|
||||
# Generate campaign ideas
|
||||
campaigns = [
|
||||
self._generate_seasonal_campaign(opportunity)
|
||||
for opportunity in seasonal_opportunities
|
||||
]
|
||||
|
||||
return {
|
||||
'current_month': current_month,
|
||||
'category': app_category,
|
||||
'seasonal_opportunities': seasonal_opportunities,
|
||||
'campaign_ideas': campaigns,
|
||||
'implementation_timeline': self._create_seasonal_timeline(campaigns)
|
||||
}
|
||||
|
||||
def _generate_apple_checklist(self, app_info: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||||
"""Generate Apple App Store specific checklist."""
|
||||
return [
|
||||
{
|
||||
'category': 'App Store Connect Setup',
|
||||
'items': [
|
||||
{'task': 'App Store Connect account created', 'status': 'pending'},
|
||||
{'task': 'App bundle ID registered', 'status': 'pending'},
|
||||
{'task': 'App Privacy declarations completed', 'status': 'pending'},
|
||||
{'task': 'Age rating questionnaire completed', 'status': 'pending'}
|
||||
]
|
||||
},
|
||||
{
|
||||
'category': 'Metadata (Apple)',
|
||||
'items': [
|
||||
{'task': 'App title (30 chars max)', 'status': 'pending'},
|
||||
{'task': 'Subtitle (30 chars max)', 'status': 'pending'},
|
||||
{'task': 'Promotional text (170 chars max)', 'status': 'pending'},
|
||||
{'task': 'Description (4000 chars max)', 'status': 'pending'},
|
||||
{'task': 'Keywords (100 chars, comma-separated)', 'status': 'pending'},
|
||||
{'task': 'Category selection (primary + secondary)', 'status': 'pending'}
|
||||
]
|
||||
},
|
||||
{
|
||||
'category': 'Visual Assets (Apple)',
|
||||
'items': [
|
||||
{'task': 'App icon (1024x1024px)', 'status': 'pending'},
|
||||
{'task': 'Screenshots (iPhone 6.7" required)', 'status': 'pending'},
|
||||
{'task': 'Screenshots (iPhone 5.5" required)', 'status': 'pending'},
|
||||
{'task': 'Screenshots (iPad Pro 12.9" if iPad app)', 'status': 'pending'},
|
||||
{'task': 'App preview video (optional but recommended)', 'status': 'pending'}
|
||||
]
|
||||
},
|
||||
{
|
||||
'category': 'Technical Requirements (Apple)',
|
||||
'items': [
|
||||
{'task': 'Build uploaded to App Store Connect', 'status': 'pending'},
|
||||
{'task': 'TestFlight testing completed', 'status': 'pending'},
|
||||
{'task': 'App tested on required iOS versions', 'status': 'pending'},
|
||||
{'task': 'Crash-free rate > 99%', 'status': 'pending'},
|
||||
{'task': 'All links in app/metadata working', 'status': 'pending'}
|
||||
]
|
||||
},
|
||||
{
|
||||
'category': 'Legal & Privacy (Apple)',
|
||||
'items': [
|
||||
{'task': 'Privacy Policy URL provided', 'status': 'pending'},
|
||||
{'task': 'Terms of Service URL (if applicable)', 'status': 'pending'},
|
||||
{'task': 'Data collection declarations accurate', 'status': 'pending'},
|
||||
{'task': 'Third-party SDKs disclosed', 'status': 'pending'}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
def _generate_google_checklist(self, app_info: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||||
"""Generate Google Play Store specific checklist."""
|
||||
return [
|
||||
{
|
||||
'category': 'Play Console Setup',
|
||||
'items': [
|
||||
{'task': 'Google Play Console account created', 'status': 'pending'},
|
||||
{'task': 'Developer profile completed', 'status': 'pending'},
|
||||
{'task': 'Payment merchant account linked (if paid app)', 'status': 'pending'},
|
||||
{'task': 'Content rating questionnaire completed', 'status': 'pending'}
|
||||
]
|
||||
},
|
||||
{
|
||||
'category': 'Metadata (Google)',
|
||||
'items': [
|
||||
{'task': 'App title (50 chars max)', 'status': 'pending'},
|
||||
{'task': 'Short description (80 chars max)', 'status': 'pending'},
|
||||
{'task': 'Full description (4000 chars max)', 'status': 'pending'},
|
||||
{'task': 'Category selection', 'status': 'pending'},
|
||||
{'task': 'Tags (up to 5)', 'status': 'pending'}
|
||||
]
|
||||
},
|
||||
{
|
||||
'category': 'Visual Assets (Google)',
|
||||
'items': [
|
||||
{'task': 'App icon (512x512px)', 'status': 'pending'},
|
||||
{'task': 'Feature graphic (1024x500px)', 'status': 'pending'},
|
||||
{'task': 'Screenshots (2-8 required, phone)', 'status': 'pending'},
|
||||
{'task': 'Screenshots (tablet, if applicable)', 'status': 'pending'},
|
||||
{'task': 'Promo video (YouTube link, optional)', 'status': 'pending'}
|
||||
]
|
||||
},
|
||||
{
|
||||
'category': 'Technical Requirements (Google)',
|
||||
'items': [
|
||||
{'task': 'APK/AAB uploaded to Play Console', 'status': 'pending'},
|
||||
{'task': 'Internal testing completed', 'status': 'pending'},
|
||||
{'task': 'App tested on required Android versions', 'status': 'pending'},
|
||||
{'task': 'Target API level meets requirements', 'status': 'pending'},
|
||||
{'task': 'All permissions justified', 'status': 'pending'}
|
||||
]
|
||||
},
|
||||
{
|
||||
'category': 'Legal & Privacy (Google)',
|
||||
'items': [
|
||||
{'task': 'Privacy Policy URL provided', 'status': 'pending'},
|
||||
{'task': 'Data safety section completed', 'status': 'pending'},
|
||||
{'task': 'Ads disclosure (if applicable)', 'status': 'pending'},
|
||||
{'task': 'In-app purchase disclosure (if applicable)', 'status': 'pending'}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
def _generate_universal_checklist(self, app_info: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||||
"""Generate universal (both platforms) checklist."""
|
||||
return [
|
||||
{
|
||||
'category': 'Pre-Launch Marketing',
|
||||
'items': [
|
||||
{'task': 'Landing page created', 'status': 'pending'},
|
||||
{'task': 'Social media accounts setup', 'status': 'pending'},
|
||||
{'task': 'Press kit prepared', 'status': 'pending'},
|
||||
{'task': 'Beta tester feedback collected', 'status': 'pending'},
|
||||
{'task': 'Launch announcement drafted', 'status': 'pending'}
|
||||
]
|
||||
},
|
||||
{
|
||||
'category': 'ASO Preparation',
|
||||
'items': [
|
||||
{'task': 'Keyword research completed', 'status': 'pending'},
|
||||
{'task': 'Competitor analysis done', 'status': 'pending'},
|
||||
{'task': 'A/B test plan created for post-launch', 'status': 'pending'},
|
||||
{'task': 'Analytics tracking configured', 'status': 'pending'}
|
||||
]
|
||||
},
|
||||
{
|
||||
'category': 'Quality Assurance',
|
||||
'items': [
|
||||
{'task': 'All core features tested', 'status': 'pending'},
|
||||
{'task': 'User flows validated', 'status': 'pending'},
|
||||
{'task': 'Performance testing completed', 'status': 'pending'},
|
||||
{'task': 'Accessibility features tested', 'status': 'pending'},
|
||||
{'task': 'Security audit completed', 'status': 'pending'}
|
||||
]
|
||||
},
|
||||
{
|
||||
'category': 'Support Infrastructure',
|
||||
'items': [
|
||||
{'task': 'Support email/system setup', 'status': 'pending'},
|
||||
{'task': 'FAQ page created', 'status': 'pending'},
|
||||
{'task': 'Documentation for users prepared', 'status': 'pending'},
|
||||
{'task': 'Team trained on handling reviews', 'status': 'pending'}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
def _generate_launch_timeline(self, launch_date: str) -> List[Dict[str, Any]]:
|
||||
"""Generate timeline with milestones leading to launch."""
|
||||
launch_dt = datetime.strptime(launch_date, '%Y-%m-%d')
|
||||
|
||||
milestones = [
|
||||
{
|
||||
'date': (launch_dt - timedelta(days=90)).strftime('%Y-%m-%d'),
|
||||
'milestone': '90 days before: Complete keyword research and competitor analysis'
|
||||
},
|
||||
{
|
||||
'date': (launch_dt - timedelta(days=60)).strftime('%Y-%m-%d'),
|
||||
'milestone': '60 days before: Finalize metadata and visual assets'
|
||||
},
|
||||
{
|
||||
'date': (launch_dt - timedelta(days=45)).strftime('%Y-%m-%d'),
|
||||
'milestone': '45 days before: Begin beta testing program'
|
||||
},
|
||||
{
|
||||
'date': (launch_dt - timedelta(days=30)).strftime('%Y-%m-%d'),
|
||||
'milestone': '30 days before: Submit app for review (Apple typically takes 1-2 days, Google instant)'
|
||||
},
|
||||
{
|
||||
'date': (launch_dt - timedelta(days=14)).strftime('%Y-%m-%d'),
|
||||
'milestone': '14 days before: Prepare launch marketing materials'
|
||||
},
|
||||
{
|
||||
'date': (launch_dt - timedelta(days=7)).strftime('%Y-%m-%d'),
|
||||
'milestone': '7 days before: Set up analytics and monitoring'
|
||||
},
|
||||
{
|
||||
'date': launch_dt.strftime('%Y-%m-%d'),
|
||||
'milestone': 'Launch Day: Release app and execute marketing plan'
|
||||
},
|
||||
{
|
||||
'date': (launch_dt + timedelta(days=7)).strftime('%Y-%m-%d'),
|
||||
'milestone': '7 days after: Monitor metrics, respond to reviews, address critical issues'
|
||||
},
|
||||
{
|
||||
'date': (launch_dt + timedelta(days=30)).strftime('%Y-%m-%d'),
|
||||
'milestone': '30 days after: Analyze launch metrics, plan first update'
|
||||
}
|
||||
]
|
||||
|
||||
return milestones
|
||||
|
||||
def _calculate_checklist_summary(self, checklists: Dict[str, List[Dict[str, Any]]]) -> Dict[str, Any]:
|
||||
"""Calculate completion summary."""
|
||||
total_items = 0
|
||||
completed_items = 0
|
||||
|
||||
for platform, categories in checklists.items():
|
||||
for category in categories:
|
||||
for item in category['items']:
|
||||
total_items += 1
|
||||
if item['status'] == 'completed':
|
||||
completed_items += 1
|
||||
|
||||
completion_percentage = (completed_items / total_items * 100) if total_items > 0 else 0
|
||||
|
||||
return {
|
||||
'total_items': total_items,
|
||||
'completed_items': completed_items,
|
||||
'pending_items': total_items - completed_items,
|
||||
'completion_percentage': round(completion_percentage, 1),
|
||||
'is_ready_to_launch': completion_percentage == 100
|
||||
}
|
||||
|
||||
def _validate_apple_compliance(
|
||||
self,
|
||||
app_data: Dict[str, Any],
|
||||
validation_results: Dict[str, Any]
|
||||
) -> None:
|
||||
"""Validate Apple App Store compliance."""
|
||||
# Check for required fields
|
||||
if not app_data.get('privacy_policy_url'):
|
||||
validation_results['errors'].append("Privacy Policy URL is required")
|
||||
|
||||
if not app_data.get('app_icon'):
|
||||
validation_results['errors'].append("App icon (1024x1024px) is required")
|
||||
|
||||
# Check metadata character limits
|
||||
title = app_data.get('title', '')
|
||||
if len(title) > 30:
|
||||
validation_results['errors'].append(f"Title exceeds 30 characters ({len(title)})")
|
||||
|
||||
# Warnings for best practices
|
||||
subtitle = app_data.get('subtitle', '')
|
||||
if not subtitle:
|
||||
validation_results['warnings'].append("Subtitle is empty - consider adding for better discoverability")
|
||||
|
||||
keywords = app_data.get('keywords', '')
|
||||
if len(keywords) < 80:
|
||||
validation_results['warnings'].append(
|
||||
f"Keywords field underutilized ({len(keywords)}/100 chars) - add more keywords"
|
||||
)
|
||||
|
||||
def _validate_google_compliance(
|
||||
self,
|
||||
app_data: Dict[str, Any],
|
||||
validation_results: Dict[str, Any]
|
||||
) -> None:
|
||||
"""Validate Google Play Store compliance."""
|
||||
# Check for required fields
|
||||
if not app_data.get('privacy_policy_url'):
|
||||
validation_results['errors'].append("Privacy Policy URL is required")
|
||||
|
||||
if not app_data.get('feature_graphic'):
|
||||
validation_results['errors'].append("Feature graphic (1024x500px) is required")
|
||||
|
||||
# Check metadata character limits
|
||||
title = app_data.get('title', '')
|
||||
if len(title) > 50:
|
||||
validation_results['errors'].append(f"Title exceeds 50 characters ({len(title)})")
|
||||
|
||||
short_desc = app_data.get('short_description', '')
|
||||
if len(short_desc) > 80:
|
||||
validation_results['errors'].append(f"Short description exceeds 80 characters ({len(short_desc)})")
|
||||
|
||||
# Warnings
|
||||
if not short_desc:
|
||||
validation_results['warnings'].append("Short description is empty")
|
||||
|
||||
def _calculate_next_versions(
|
||||
self,
|
||||
current_version: str,
|
||||
update_frequency: str,
|
||||
feature_count: int
|
||||
) -> List[str]:
|
||||
"""Calculate next version numbers."""
|
||||
# Parse current version (assume semantic versioning)
|
||||
parts = current_version.split('.')
|
||||
major, minor, patch = int(parts[0]), int(parts[1]), int(parts[2] if len(parts) > 2 else 0)
|
||||
|
||||
versions = []
|
||||
for i in range(feature_count):
|
||||
if update_frequency == 'weekly':
|
||||
patch += 1
|
||||
elif update_frequency == 'biweekly':
|
||||
patch += 1
|
||||
elif update_frequency == 'monthly':
|
||||
minor += 1
|
||||
patch = 0
|
||||
else: # quarterly
|
||||
minor += 1
|
||||
patch = 0
|
||||
|
||||
versions.append(f"{major}.{minor}.{patch}")
|
||||
|
||||
return versions
|
||||
|
||||
def _distribute_features(
|
||||
self,
|
||||
features: List[str],
|
||||
versions: List[str]
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Distribute features across versions."""
|
||||
features_per_version = max(1, len(features) // len(versions))
|
||||
|
||||
schedule = []
|
||||
for i, version in enumerate(versions):
|
||||
start_idx = i * features_per_version
|
||||
end_idx = start_idx + features_per_version if i < len(versions) - 1 else len(features)
|
||||
|
||||
schedule.append({
|
||||
'version': version,
|
||||
'features': features[start_idx:end_idx],
|
||||
'release_priority': 'high' if i == 0 else ('medium' if i < len(versions) // 2 else 'low')
|
||||
})
|
||||
|
||||
return schedule
|
||||
|
||||
def _generate_whats_new_template(self, version_data: Dict[str, Any]) -> Dict[str, str]:
|
||||
"""Generate What's New template for version."""
|
||||
features_list = '\n'.join([f"• {feature}" for feature in version_data['features']])
|
||||
|
||||
template = f"""Version {version_data['version']}
|
||||
|
||||
{features_list}
|
||||
|
||||
We're constantly improving your experience. Thanks for using [App Name]!
|
||||
|
||||
Have feedback? Contact us at support@[company].com"""
|
||||
|
||||
return {
|
||||
'version': version_data['version'],
|
||||
'template': template
|
||||
}
|
||||
|
||||
def _generate_update_recommendations(self, update_frequency: str) -> List[str]:
|
||||
"""Generate recommendations for update strategy."""
|
||||
recommendations = []
|
||||
|
||||
if update_frequency == 'weekly':
|
||||
recommendations.append("Weekly updates show active development but ensure quality doesn't suffer")
|
||||
elif update_frequency == 'monthly':
|
||||
recommendations.append("Monthly updates are optimal for most apps - balance features and stability")
|
||||
|
||||
recommendations.extend([
|
||||
"Include bug fixes in every update",
|
||||
"Update 'What's New' section with each release",
|
||||
"Respond to reviews mentioning fixed issues"
|
||||
])
|
||||
|
||||
return recommendations
|
||||
|
||||
def _recommend_day_of_week(self, app_category: str) -> Dict[str, Any]:
|
||||
"""Recommend best day of week to launch."""
|
||||
# General recommendations based on category
|
||||
if app_category.lower() in ['games', 'entertainment']:
|
||||
return {
|
||||
'recommended_day': 'Thursday',
|
||||
'rationale': 'People download entertainment apps before weekend'
|
||||
}
|
||||
elif app_category.lower() in ['productivity', 'business']:
|
||||
return {
|
||||
'recommended_day': 'Tuesday',
|
||||
'rationale': 'Business users most active mid-week'
|
||||
}
|
||||
else:
|
||||
return {
|
||||
'recommended_day': 'Wednesday',
|
||||
'rationale': 'Mid-week provides good balance and review potential'
|
||||
}
|
||||
|
||||
def _recommend_seasonal_timing(self, app_category: str, current_date: str) -> Dict[str, Any]:
|
||||
"""Recommend seasonal timing considerations."""
|
||||
current_dt = datetime.strptime(current_date, '%Y-%m-%d')
|
||||
month = current_dt.month
|
||||
|
||||
# Avoid certain periods
|
||||
avoid_periods = []
|
||||
if month == 12:
|
||||
avoid_periods.append("Late December - low user engagement during holidays")
|
||||
if month in [7, 8]:
|
||||
avoid_periods.append("Summer months - some categories see lower engagement")
|
||||
|
||||
# Recommend periods
|
||||
good_periods = []
|
||||
if month in [1, 9]:
|
||||
good_periods.append("New Year/Back-to-school - high user engagement")
|
||||
if month in [10, 11]:
|
||||
good_periods.append("Pre-holiday season - good for shopping/gift apps")
|
||||
|
||||
return {
|
||||
'current_month': month,
|
||||
'avoid_periods': avoid_periods,
|
||||
'good_periods': good_periods
|
||||
}
|
||||
|
||||
def _analyze_competitive_timing(self, app_category: str) -> Dict[str, str]:
|
||||
"""Analyze competitive timing considerations."""
|
||||
return {
|
||||
'recommendation': 'Research competitor launch schedules in your category',
|
||||
'strategy': 'Avoid launching same week as major competitor updates'
|
||||
}
|
||||
|
||||
def _calculate_optimal_dates(
|
||||
self,
|
||||
current_date: str,
|
||||
day_rec: Dict[str, Any],
|
||||
seasonal_rec: Dict[str, Any]
|
||||
) -> List[str]:
|
||||
"""Calculate optimal launch dates."""
|
||||
current_dt = datetime.strptime(current_date, '%Y-%m-%d')
|
||||
|
||||
# Find next occurrence of recommended day
|
||||
target_day = day_rec['recommended_day']
|
||||
days_map = {'Monday': 0, 'Tuesday': 1, 'Wednesday': 2, 'Thursday': 3, 'Friday': 4}
|
||||
target_day_num = days_map.get(target_day, 2)
|
||||
|
||||
days_ahead = (target_day_num - current_dt.weekday()) % 7
|
||||
if days_ahead == 0:
|
||||
days_ahead = 7
|
||||
|
||||
next_target_date = current_dt + timedelta(days=days_ahead)
|
||||
|
||||
optimal_dates = [
|
||||
next_target_date.strftime('%Y-%m-%d'),
|
||||
(next_target_date + timedelta(days=7)).strftime('%Y-%m-%d'),
|
||||
(next_target_date + timedelta(days=14)).strftime('%Y-%m-%d')
|
||||
]
|
||||
|
||||
return optimal_dates
|
||||
|
||||
def _generate_timing_recommendation(
|
||||
self,
|
||||
optimal_dates: List[str],
|
||||
seasonal_rec: Dict[str, Any]
|
||||
) -> str:
|
||||
"""Generate final timing recommendation."""
|
||||
if seasonal_rec['avoid_periods']:
|
||||
return f"Consider launching in {optimal_dates[1]} to avoid {seasonal_rec['avoid_periods'][0]}"
|
||||
elif seasonal_rec['good_periods']:
|
||||
return f"Launch on {optimal_dates[0]} to capitalize on {seasonal_rec['good_periods'][0]}"
|
||||
else:
|
||||
return f"Recommended launch date: {optimal_dates[0]}"
|
||||
|
||||
def _identify_seasonal_opportunities(
|
||||
self,
|
||||
app_category: str,
|
||||
current_month: int
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Identify seasonal opportunities for category."""
|
||||
opportunities = []
|
||||
|
||||
# Universal opportunities
|
||||
if current_month == 1:
|
||||
opportunities.append({
|
||||
'event': 'New Year Resolutions',
|
||||
'dates': 'January 1-31',
|
||||
'relevance': 'high' if app_category.lower() in ['health', 'fitness', 'productivity'] else 'medium'
|
||||
})
|
||||
|
||||
if current_month in [11, 12]:
|
||||
opportunities.append({
|
||||
'event': 'Holiday Shopping Season',
|
||||
'dates': 'November-December',
|
||||
'relevance': 'high' if app_category.lower() in ['shopping', 'gifts'] else 'low'
|
||||
})
|
||||
|
||||
# Category-specific
|
||||
if app_category.lower() == 'education' and current_month in [8, 9]:
|
||||
opportunities.append({
|
||||
'event': 'Back to School',
|
||||
'dates': 'August-September',
|
||||
'relevance': 'high'
|
||||
})
|
||||
|
||||
return opportunities
|
||||
|
||||
def _generate_seasonal_campaign(self, opportunity: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Generate campaign idea for seasonal opportunity."""
|
||||
return {
|
||||
'event': opportunity['event'],
|
||||
'campaign_idea': f"Create themed visuals and messaging for {opportunity['event']}",
|
||||
'metadata_updates': 'Update app description and screenshots with seasonal themes',
|
||||
'promotion_strategy': 'Consider limited-time features or discounts'
|
||||
}
|
||||
|
||||
def _create_seasonal_timeline(self, campaigns: List[Dict[str, Any]]) -> List[str]:
|
||||
"""Create implementation timeline for campaigns."""
|
||||
return [
|
||||
f"30 days before: Plan {campaign['event']} campaign strategy"
|
||||
for campaign in campaigns
|
||||
]
|
||||
|
||||
|
||||
def generate_launch_checklist(
|
||||
platform: str,
|
||||
app_info: Dict[str, Any],
|
||||
launch_date: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Convenience function to generate launch checklist.
|
||||
|
||||
Args:
|
||||
platform: Platform ('apple', 'google', or 'both')
|
||||
app_info: App information
|
||||
launch_date: Target launch date
|
||||
|
||||
Returns:
|
||||
Complete launch checklist
|
||||
"""
|
||||
generator = LaunchChecklistGenerator(platform)
|
||||
return generator.generate_prelaunch_checklist(app_info, launch_date)
|
||||
@@ -0,0 +1,588 @@
|
||||
"""
|
||||
Localization helper module for App Store Optimization.
|
||||
Manages multi-language ASO optimization strategies.
|
||||
"""
|
||||
|
||||
from typing import Dict, List, Any, Optional, Tuple
|
||||
|
||||
|
||||
class LocalizationHelper:
|
||||
"""Helps manage multi-language ASO optimization."""
|
||||
|
||||
# Priority markets by language (based on app store revenue and user base)
|
||||
PRIORITY_MARKETS = {
|
||||
'tier_1': [
|
||||
{'language': 'en-US', 'market': 'United States', 'revenue_share': 0.25},
|
||||
{'language': 'zh-CN', 'market': 'China', 'revenue_share': 0.20},
|
||||
{'language': 'ja-JP', 'market': 'Japan', 'revenue_share': 0.10},
|
||||
{'language': 'de-DE', 'market': 'Germany', 'revenue_share': 0.08},
|
||||
{'language': 'en-GB', 'market': 'United Kingdom', 'revenue_share': 0.06}
|
||||
],
|
||||
'tier_2': [
|
||||
{'language': 'fr-FR', 'market': 'France', 'revenue_share': 0.05},
|
||||
{'language': 'ko-KR', 'market': 'South Korea', 'revenue_share': 0.05},
|
||||
{'language': 'es-ES', 'market': 'Spain', 'revenue_share': 0.03},
|
||||
{'language': 'it-IT', 'market': 'Italy', 'revenue_share': 0.03},
|
||||
{'language': 'pt-BR', 'market': 'Brazil', 'revenue_share': 0.03}
|
||||
],
|
||||
'tier_3': [
|
||||
{'language': 'ru-RU', 'market': 'Russia', 'revenue_share': 0.02},
|
||||
{'language': 'es-MX', 'market': 'Mexico', 'revenue_share': 0.02},
|
||||
{'language': 'nl-NL', 'market': 'Netherlands', 'revenue_share': 0.02},
|
||||
{'language': 'sv-SE', 'market': 'Sweden', 'revenue_share': 0.01},
|
||||
{'language': 'pl-PL', 'market': 'Poland', 'revenue_share': 0.01}
|
||||
]
|
||||
}
|
||||
|
||||
# Character limit multipliers by language (some languages need more/less space)
|
||||
CHAR_MULTIPLIERS = {
|
||||
'en': 1.0,
|
||||
'zh': 0.6, # Chinese characters are more compact
|
||||
'ja': 0.7, # Japanese uses kanji
|
||||
'ko': 0.8, # Korean is relatively compact
|
||||
'de': 1.3, # German words are typically longer
|
||||
'fr': 1.2, # French tends to be longer
|
||||
'es': 1.1, # Spanish slightly longer
|
||||
'pt': 1.1, # Portuguese similar to Spanish
|
||||
'ru': 1.1, # Russian similar length
|
||||
'ar': 1.0, # Arabic varies
|
||||
'it': 1.1 # Italian similar to Spanish
|
||||
}
|
||||
|
||||
def __init__(self, app_category: str = 'general'):
|
||||
"""
|
||||
Initialize localization helper.
|
||||
|
||||
Args:
|
||||
app_category: App category to prioritize relevant markets
|
||||
"""
|
||||
self.app_category = app_category
|
||||
self.localization_plans = []
|
||||
|
||||
def identify_target_markets(
|
||||
self,
|
||||
current_market: str = 'en-US',
|
||||
budget_level: str = 'medium',
|
||||
target_market_count: int = 5
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Recommend priority markets for localization.
|
||||
|
||||
Args:
|
||||
current_market: Current/primary market
|
||||
budget_level: 'low', 'medium', or 'high'
|
||||
target_market_count: Number of markets to target
|
||||
|
||||
Returns:
|
||||
Prioritized market recommendations
|
||||
"""
|
||||
# Determine tier priorities based on budget
|
||||
if budget_level == 'low':
|
||||
priority_tiers = ['tier_1']
|
||||
max_markets = min(target_market_count, 3)
|
||||
elif budget_level == 'medium':
|
||||
priority_tiers = ['tier_1', 'tier_2']
|
||||
max_markets = min(target_market_count, 8)
|
||||
else: # high budget
|
||||
priority_tiers = ['tier_1', 'tier_2', 'tier_3']
|
||||
max_markets = target_market_count
|
||||
|
||||
# Collect markets from priority tiers
|
||||
recommended_markets = []
|
||||
for tier in priority_tiers:
|
||||
for market in self.PRIORITY_MARKETS[tier]:
|
||||
if market['language'] != current_market:
|
||||
recommended_markets.append({
|
||||
**market,
|
||||
'tier': tier,
|
||||
'estimated_translation_cost': self._estimate_translation_cost(
|
||||
market['language']
|
||||
)
|
||||
})
|
||||
|
||||
# Sort by revenue share and limit
|
||||
recommended_markets.sort(key=lambda x: x['revenue_share'], reverse=True)
|
||||
recommended_markets = recommended_markets[:max_markets]
|
||||
|
||||
# Calculate potential ROI
|
||||
total_potential_revenue_share = sum(m['revenue_share'] for m in recommended_markets)
|
||||
|
||||
return {
|
||||
'recommended_markets': recommended_markets,
|
||||
'total_markets': len(recommended_markets),
|
||||
'estimated_total_revenue_lift': f"{total_potential_revenue_share*100:.1f}%",
|
||||
'estimated_cost': self._estimate_total_localization_cost(recommended_markets),
|
||||
'implementation_priority': self._prioritize_implementation(recommended_markets)
|
||||
}
|
||||
|
||||
def translate_metadata(
|
||||
self,
|
||||
source_metadata: Dict[str, str],
|
||||
source_language: str,
|
||||
target_language: str,
|
||||
platform: str = 'apple'
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Generate localized metadata with character limit considerations.
|
||||
|
||||
Args:
|
||||
source_metadata: Original metadata (title, description, etc.)
|
||||
source_language: Source language code (e.g., 'en')
|
||||
target_language: Target language code (e.g., 'es')
|
||||
platform: 'apple' or 'google'
|
||||
|
||||
Returns:
|
||||
Localized metadata with character limit validation
|
||||
"""
|
||||
# Get character multiplier
|
||||
target_lang_code = target_language.split('-')[0]
|
||||
char_multiplier = self.CHAR_MULTIPLIERS.get(target_lang_code, 1.0)
|
||||
|
||||
# Platform-specific limits
|
||||
if platform == 'apple':
|
||||
limits = {'title': 30, 'subtitle': 30, 'description': 4000, 'keywords': 100}
|
||||
else:
|
||||
limits = {'title': 50, 'short_description': 80, 'description': 4000}
|
||||
|
||||
localized_metadata = {}
|
||||
warnings = []
|
||||
|
||||
for field, text in source_metadata.items():
|
||||
if field not in limits:
|
||||
continue
|
||||
|
||||
# Estimate target length
|
||||
estimated_length = int(len(text) * char_multiplier)
|
||||
limit = limits[field]
|
||||
|
||||
localized_metadata[field] = {
|
||||
'original_text': text,
|
||||
'original_length': len(text),
|
||||
'estimated_target_length': estimated_length,
|
||||
'character_limit': limit,
|
||||
'fits_within_limit': estimated_length <= limit,
|
||||
'translation_notes': self._get_translation_notes(
|
||||
field,
|
||||
target_language,
|
||||
estimated_length,
|
||||
limit
|
||||
)
|
||||
}
|
||||
|
||||
if estimated_length > limit:
|
||||
warnings.append(
|
||||
f"{field}: Estimated length ({estimated_length}) may exceed limit ({limit}) - "
|
||||
f"condensing may be required"
|
||||
)
|
||||
|
||||
return {
|
||||
'source_language': source_language,
|
||||
'target_language': target_language,
|
||||
'platform': platform,
|
||||
'localized_fields': localized_metadata,
|
||||
'character_multiplier': char_multiplier,
|
||||
'warnings': warnings,
|
||||
'recommendations': self._generate_translation_recommendations(
|
||||
target_language,
|
||||
warnings
|
||||
)
|
||||
}
|
||||
|
||||
def adapt_keywords(
|
||||
self,
|
||||
source_keywords: List[str],
|
||||
source_language: str,
|
||||
target_language: str,
|
||||
target_market: str
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Adapt keywords for target market (not just direct translation).
|
||||
|
||||
Args:
|
||||
source_keywords: Original keywords
|
||||
source_language: Source language code
|
||||
target_language: Target language code
|
||||
target_market: Target market (e.g., 'France', 'Japan')
|
||||
|
||||
Returns:
|
||||
Adapted keyword recommendations
|
||||
"""
|
||||
# Cultural adaptation considerations
|
||||
cultural_notes = self._get_cultural_keyword_considerations(target_market)
|
||||
|
||||
# Search behavior differences
|
||||
search_patterns = self._get_search_patterns(target_market)
|
||||
|
||||
adapted_keywords = []
|
||||
for keyword in source_keywords:
|
||||
adapted_keywords.append({
|
||||
'source_keyword': keyword,
|
||||
'adaptation_strategy': self._determine_adaptation_strategy(
|
||||
keyword,
|
||||
target_market
|
||||
),
|
||||
'cultural_considerations': cultural_notes.get(keyword, []),
|
||||
'priority': 'high' if keyword in source_keywords[:3] else 'medium'
|
||||
})
|
||||
|
||||
return {
|
||||
'source_language': source_language,
|
||||
'target_language': target_language,
|
||||
'target_market': target_market,
|
||||
'adapted_keywords': adapted_keywords,
|
||||
'search_behavior_notes': search_patterns,
|
||||
'recommendations': [
|
||||
'Use native speakers for keyword research',
|
||||
'Test keywords with local users before finalizing',
|
||||
'Consider local competitors\' keyword strategies',
|
||||
'Monitor search trends in target market'
|
||||
]
|
||||
}
|
||||
|
||||
def validate_translations(
|
||||
self,
|
||||
translated_metadata: Dict[str, str],
|
||||
target_language: str,
|
||||
platform: str = 'apple'
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Validate translated metadata for character limits and quality.
|
||||
|
||||
Args:
|
||||
translated_metadata: Translated text fields
|
||||
target_language: Target language code
|
||||
platform: 'apple' or 'google'
|
||||
|
||||
Returns:
|
||||
Validation report
|
||||
"""
|
||||
# Platform limits
|
||||
if platform == 'apple':
|
||||
limits = {'title': 30, 'subtitle': 30, 'description': 4000, 'keywords': 100}
|
||||
else:
|
||||
limits = {'title': 50, 'short_description': 80, 'description': 4000}
|
||||
|
||||
validation_results = {
|
||||
'is_valid': True,
|
||||
'field_validations': {},
|
||||
'errors': [],
|
||||
'warnings': []
|
||||
}
|
||||
|
||||
for field, text in translated_metadata.items():
|
||||
if field not in limits:
|
||||
continue
|
||||
|
||||
actual_length = len(text)
|
||||
limit = limits[field]
|
||||
is_within_limit = actual_length <= limit
|
||||
|
||||
validation_results['field_validations'][field] = {
|
||||
'text': text,
|
||||
'length': actual_length,
|
||||
'limit': limit,
|
||||
'is_valid': is_within_limit,
|
||||
'usage_percentage': round((actual_length / limit) * 100, 1)
|
||||
}
|
||||
|
||||
if not is_within_limit:
|
||||
validation_results['is_valid'] = False
|
||||
validation_results['errors'].append(
|
||||
f"{field} exceeds limit: {actual_length}/{limit} characters"
|
||||
)
|
||||
|
||||
# Quality checks
|
||||
quality_issues = self._check_translation_quality(
|
||||
translated_metadata,
|
||||
target_language
|
||||
)
|
||||
|
||||
validation_results['quality_checks'] = quality_issues
|
||||
|
||||
if quality_issues:
|
||||
validation_results['warnings'].extend(
|
||||
[f"Quality issue: {issue}" for issue in quality_issues]
|
||||
)
|
||||
|
||||
return validation_results
|
||||
|
||||
def calculate_localization_roi(
|
||||
self,
|
||||
target_markets: List[str],
|
||||
current_monthly_downloads: int,
|
||||
localization_cost: float,
|
||||
expected_lift_percentage: float = 0.15
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Estimate ROI of localization investment.
|
||||
|
||||
Args:
|
||||
target_markets: List of market codes
|
||||
current_monthly_downloads: Current monthly downloads
|
||||
localization_cost: Total cost to localize
|
||||
expected_lift_percentage: Expected download increase (default 15%)
|
||||
|
||||
Returns:
|
||||
ROI analysis
|
||||
"""
|
||||
# Estimate market-specific lift
|
||||
market_data = []
|
||||
total_expected_lift = 0
|
||||
|
||||
for market_code in target_markets:
|
||||
# Find market in priority lists
|
||||
market_info = None
|
||||
for tier_name, markets in self.PRIORITY_MARKETS.items():
|
||||
for m in markets:
|
||||
if m['language'] == market_code:
|
||||
market_info = m
|
||||
break
|
||||
|
||||
if not market_info:
|
||||
continue
|
||||
|
||||
# Estimate downloads from this market
|
||||
market_downloads = int(current_monthly_downloads * market_info['revenue_share'])
|
||||
expected_increase = int(market_downloads * expected_lift_percentage)
|
||||
total_expected_lift += expected_increase
|
||||
|
||||
market_data.append({
|
||||
'market': market_info['market'],
|
||||
'current_monthly_downloads': market_downloads,
|
||||
'expected_increase': expected_increase,
|
||||
'revenue_potential': market_info['revenue_share']
|
||||
})
|
||||
|
||||
# Calculate payback period (assuming $2 revenue per download)
|
||||
revenue_per_download = 2.0
|
||||
monthly_additional_revenue = total_expected_lift * revenue_per_download
|
||||
payback_months = (localization_cost / monthly_additional_revenue) if monthly_additional_revenue > 0 else float('inf')
|
||||
|
||||
return {
|
||||
'markets_analyzed': len(market_data),
|
||||
'market_breakdown': market_data,
|
||||
'total_expected_monthly_lift': total_expected_lift,
|
||||
'expected_monthly_revenue_increase': f"${monthly_additional_revenue:,.2f}",
|
||||
'localization_cost': f"${localization_cost:,.2f}",
|
||||
'payback_period_months': round(payback_months, 1) if payback_months != float('inf') else 'N/A',
|
||||
'annual_roi': f"{((monthly_additional_revenue * 12 - localization_cost) / localization_cost * 100):.1f}%" if payback_months != float('inf') else 'Negative',
|
||||
'recommendation': self._generate_roi_recommendation(payback_months)
|
||||
}
|
||||
|
||||
def _estimate_translation_cost(self, language: str) -> Dict[str, float]:
|
||||
"""Estimate translation cost for a language."""
|
||||
# Base cost per word (professional translation)
|
||||
base_cost_per_word = 0.12
|
||||
|
||||
# Language-specific multipliers
|
||||
multipliers = {
|
||||
'zh-CN': 1.5, # Chinese requires specialist
|
||||
'ja-JP': 1.5, # Japanese requires specialist
|
||||
'ko-KR': 1.3,
|
||||
'ar-SA': 1.4, # Arabic (right-to-left)
|
||||
'default': 1.0
|
||||
}
|
||||
|
||||
multiplier = multipliers.get(language, multipliers['default'])
|
||||
|
||||
# Typical word counts for app store metadata
|
||||
typical_word_counts = {
|
||||
'title': 5,
|
||||
'subtitle': 5,
|
||||
'description': 300,
|
||||
'keywords': 20,
|
||||
'screenshots': 50 # Caption text
|
||||
}
|
||||
|
||||
total_words = sum(typical_word_counts.values())
|
||||
estimated_cost = total_words * base_cost_per_word * multiplier
|
||||
|
||||
return {
|
||||
'cost_per_word': base_cost_per_word * multiplier,
|
||||
'total_words': total_words,
|
||||
'estimated_cost': round(estimated_cost, 2)
|
||||
}
|
||||
|
||||
def _estimate_total_localization_cost(self, markets: List[Dict[str, Any]]) -> str:
|
||||
"""Estimate total cost for multiple markets."""
|
||||
total = sum(m['estimated_translation_cost']['estimated_cost'] for m in markets)
|
||||
return f"${total:,.2f}"
|
||||
|
||||
def _prioritize_implementation(self, markets: List[Dict[str, Any]]) -> List[Dict[str, str]]:
|
||||
"""Create phased implementation plan."""
|
||||
phases = []
|
||||
|
||||
# Phase 1: Top revenue markets
|
||||
phase_1 = [m for m in markets[:3]]
|
||||
if phase_1:
|
||||
phases.append({
|
||||
'phase': 'Phase 1 (First 30 days)',
|
||||
'markets': ', '.join([m['market'] for m in phase_1]),
|
||||
'rationale': 'Highest revenue potential markets'
|
||||
})
|
||||
|
||||
# Phase 2: Remaining tier 1 and top tier 2
|
||||
phase_2 = [m for m in markets[3:6]]
|
||||
if phase_2:
|
||||
phases.append({
|
||||
'phase': 'Phase 2 (Days 31-60)',
|
||||
'markets': ', '.join([m['market'] for m in phase_2]),
|
||||
'rationale': 'Strong revenue markets with good ROI'
|
||||
})
|
||||
|
||||
# Phase 3: Remaining markets
|
||||
phase_3 = [m for m in markets[6:]]
|
||||
if phase_3:
|
||||
phases.append({
|
||||
'phase': 'Phase 3 (Days 61-90)',
|
||||
'markets': ', '.join([m['market'] for m in phase_3]),
|
||||
'rationale': 'Complete global coverage'
|
||||
})
|
||||
|
||||
return phases
|
||||
|
||||
def _get_translation_notes(
|
||||
self,
|
||||
field: str,
|
||||
target_language: str,
|
||||
estimated_length: int,
|
||||
limit: int
|
||||
) -> List[str]:
|
||||
"""Get translation-specific notes for field."""
|
||||
notes = []
|
||||
|
||||
if estimated_length > limit:
|
||||
notes.append(f"Condensing required - aim for {limit - 10} characters to allow buffer")
|
||||
|
||||
if field == 'title' and target_language.startswith('zh'):
|
||||
notes.append("Chinese characters convey more meaning - may need fewer characters")
|
||||
|
||||
if field == 'keywords' and target_language.startswith('de'):
|
||||
notes.append("German compound words may be longer - prioritize shorter keywords")
|
||||
|
||||
return notes
|
||||
|
||||
def _generate_translation_recommendations(
|
||||
self,
|
||||
target_language: str,
|
||||
warnings: List[str]
|
||||
) -> List[str]:
|
||||
"""Generate translation recommendations."""
|
||||
recommendations = [
|
||||
"Use professional native speakers for translation",
|
||||
"Test translations with local users before finalizing"
|
||||
]
|
||||
|
||||
if warnings:
|
||||
recommendations.append("Work with translator to condense text while preserving meaning")
|
||||
|
||||
if target_language.startswith('zh') or target_language.startswith('ja'):
|
||||
recommendations.append("Consider cultural context and local idioms")
|
||||
|
||||
return recommendations
|
||||
|
||||
def _get_cultural_keyword_considerations(self, target_market: str) -> Dict[str, List[str]]:
|
||||
"""Get cultural considerations for keywords by market."""
|
||||
# Simplified example - real implementation would be more comprehensive
|
||||
considerations = {
|
||||
'China': ['Avoid politically sensitive terms', 'Consider local alternatives to blocked services'],
|
||||
'Japan': ['Honorific language important', 'Technical terms often use katakana'],
|
||||
'Germany': ['Privacy and security terms resonate', 'Efficiency and quality valued'],
|
||||
'France': ['French language protection laws', 'Prefer French terms over English'],
|
||||
'default': ['Research local search behavior', 'Test with native speakers']
|
||||
}
|
||||
|
||||
return considerations.get(target_market, considerations['default'])
|
||||
|
||||
def _get_search_patterns(self, target_market: str) -> List[str]:
|
||||
"""Get search pattern notes for market."""
|
||||
patterns = {
|
||||
'China': ['Use both simplified characters and romanization', 'Brand names often romanized'],
|
||||
'Japan': ['Mix of kanji, hiragana, and katakana', 'English words common in tech'],
|
||||
'Germany': ['Compound words common', 'Specific technical terminology'],
|
||||
'default': ['Research local search trends', 'Monitor competitor keywords']
|
||||
}
|
||||
|
||||
return patterns.get(target_market, patterns['default'])
|
||||
|
||||
def _determine_adaptation_strategy(self, keyword: str, target_market: str) -> str:
|
||||
"""Determine how to adapt keyword for market."""
|
||||
# Simplified logic
|
||||
if target_market in ['China', 'Japan', 'Korea']:
|
||||
return 'full_localization' # Complete translation needed
|
||||
elif target_market in ['Germany', 'France', 'Spain']:
|
||||
return 'adapt_and_translate' # Some adaptation needed
|
||||
else:
|
||||
return 'direct_translation' # Direct translation usually sufficient
|
||||
|
||||
def _check_translation_quality(
|
||||
self,
|
||||
translated_metadata: Dict[str, str],
|
||||
target_language: str
|
||||
) -> List[str]:
|
||||
"""Basic quality checks for translations."""
|
||||
issues = []
|
||||
|
||||
# Check for untranslated placeholders
|
||||
for field, text in translated_metadata.items():
|
||||
if '[' in text or '{' in text or 'TODO' in text.upper():
|
||||
issues.append(f"{field} contains placeholder text")
|
||||
|
||||
# Check for excessive punctuation
|
||||
for field, text in translated_metadata.items():
|
||||
if text.count('!') > 3:
|
||||
issues.append(f"{field} has excessive exclamation marks")
|
||||
|
||||
return issues
|
||||
|
||||
def _generate_roi_recommendation(self, payback_months: float) -> str:
|
||||
"""Generate ROI recommendation."""
|
||||
if payback_months <= 3:
|
||||
return "Excellent ROI - proceed immediately"
|
||||
elif payback_months <= 6:
|
||||
return "Good ROI - recommended investment"
|
||||
elif payback_months <= 12:
|
||||
return "Moderate ROI - consider if strategic market"
|
||||
else:
|
||||
return "Low ROI - reconsider or focus on higher-priority markets first"
|
||||
|
||||
|
||||
def plan_localization_strategy(
|
||||
current_market: str,
|
||||
budget_level: str,
|
||||
monthly_downloads: int
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Convenience function to plan localization strategy.
|
||||
|
||||
Args:
|
||||
current_market: Current market code
|
||||
budget_level: Budget level
|
||||
monthly_downloads: Current monthly downloads
|
||||
|
||||
Returns:
|
||||
Complete localization plan
|
||||
"""
|
||||
helper = LocalizationHelper()
|
||||
|
||||
target_markets = helper.identify_target_markets(
|
||||
current_market=current_market,
|
||||
budget_level=budget_level
|
||||
)
|
||||
|
||||
# Extract market codes
|
||||
market_codes = [m['language'] for m in target_markets['recommended_markets']]
|
||||
|
||||
# Calculate ROI
|
||||
estimated_cost = float(target_markets['estimated_cost'].replace('$', '').replace(',', ''))
|
||||
|
||||
roi_analysis = helper.calculate_localization_roi(
|
||||
market_codes,
|
||||
monthly_downloads,
|
||||
estimated_cost
|
||||
)
|
||||
|
||||
return {
|
||||
'target_markets': target_markets,
|
||||
'roi_analysis': roi_analysis
|
||||
}
|
||||
@@ -0,0 +1,581 @@
|
||||
"""
|
||||
Metadata optimization module for App Store Optimization.
|
||||
Optimizes titles, descriptions, and keyword fields with platform-specific character limit validation.
|
||||
"""
|
||||
|
||||
from typing import Dict, List, Any, Optional, Tuple
|
||||
import re
|
||||
|
||||
|
||||
class MetadataOptimizer:
|
||||
"""Optimizes app store metadata for maximum discoverability and conversion."""
|
||||
|
||||
# Platform-specific character limits
|
||||
CHAR_LIMITS = {
|
||||
'apple': {
|
||||
'title': 30,
|
||||
'subtitle': 30,
|
||||
'promotional_text': 170,
|
||||
'description': 4000,
|
||||
'keywords': 100,
|
||||
'whats_new': 4000
|
||||
},
|
||||
'google': {
|
||||
'title': 50,
|
||||
'short_description': 80,
|
||||
'full_description': 4000
|
||||
}
|
||||
}
|
||||
|
||||
def __init__(self, platform: str = 'apple'):
|
||||
"""
|
||||
Initialize metadata optimizer.
|
||||
|
||||
Args:
|
||||
platform: 'apple' or 'google'
|
||||
"""
|
||||
if platform not in ['apple', 'google']:
|
||||
raise ValueError("Platform must be 'apple' or 'google'")
|
||||
|
||||
self.platform = platform
|
||||
self.limits = self.CHAR_LIMITS[platform]
|
||||
|
||||
def optimize_title(
|
||||
self,
|
||||
app_name: str,
|
||||
target_keywords: List[str],
|
||||
include_brand: bool = True
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Optimize app title with keyword integration.
|
||||
|
||||
Args:
|
||||
app_name: Your app's brand name
|
||||
target_keywords: List of keywords to potentially include
|
||||
include_brand: Whether to include brand name
|
||||
|
||||
Returns:
|
||||
Optimized title options with analysis
|
||||
"""
|
||||
max_length = self.limits['title']
|
||||
|
||||
title_options = []
|
||||
|
||||
# Option 1: Brand name only
|
||||
if include_brand:
|
||||
option1 = app_name[:max_length]
|
||||
title_options.append({
|
||||
'title': option1,
|
||||
'length': len(option1),
|
||||
'remaining_chars': max_length - len(option1),
|
||||
'keywords_included': [],
|
||||
'strategy': 'brand_only',
|
||||
'pros': ['Maximum brand recognition', 'Clean and simple'],
|
||||
'cons': ['No keyword targeting', 'Lower discoverability']
|
||||
})
|
||||
|
||||
# Option 2: Brand + Primary Keyword
|
||||
if target_keywords:
|
||||
primary_keyword = target_keywords[0]
|
||||
option2 = self._build_title_with_keywords(
|
||||
app_name,
|
||||
[primary_keyword],
|
||||
max_length
|
||||
)
|
||||
if option2:
|
||||
title_options.append({
|
||||
'title': option2,
|
||||
'length': len(option2),
|
||||
'remaining_chars': max_length - len(option2),
|
||||
'keywords_included': [primary_keyword],
|
||||
'strategy': 'brand_plus_primary',
|
||||
'pros': ['Targets main keyword', 'Maintains brand identity'],
|
||||
'cons': ['Limited keyword coverage']
|
||||
})
|
||||
|
||||
# Option 3: Brand + Multiple Keywords (if space allows)
|
||||
if len(target_keywords) > 1:
|
||||
option3 = self._build_title_with_keywords(
|
||||
app_name,
|
||||
target_keywords[:2],
|
||||
max_length
|
||||
)
|
||||
if option3:
|
||||
title_options.append({
|
||||
'title': option3,
|
||||
'length': len(option3),
|
||||
'remaining_chars': max_length - len(option3),
|
||||
'keywords_included': target_keywords[:2],
|
||||
'strategy': 'brand_plus_multiple',
|
||||
'pros': ['Multiple keyword targets', 'Better discoverability'],
|
||||
'cons': ['May feel cluttered', 'Less brand focus']
|
||||
})
|
||||
|
||||
# Option 4: Keyword-first approach (for new apps)
|
||||
if target_keywords and not include_brand:
|
||||
option4 = " ".join(target_keywords[:2])[:max_length]
|
||||
title_options.append({
|
||||
'title': option4,
|
||||
'length': len(option4),
|
||||
'remaining_chars': max_length - len(option4),
|
||||
'keywords_included': target_keywords[:2],
|
||||
'strategy': 'keyword_first',
|
||||
'pros': ['Maximum SEO benefit', 'Clear functionality'],
|
||||
'cons': ['No brand recognition', 'Generic appearance']
|
||||
})
|
||||
|
||||
return {
|
||||
'platform': self.platform,
|
||||
'max_length': max_length,
|
||||
'options': title_options,
|
||||
'recommendation': self._recommend_title_option(title_options)
|
||||
}
|
||||
|
||||
def optimize_description(
|
||||
self,
|
||||
app_info: Dict[str, Any],
|
||||
target_keywords: List[str],
|
||||
description_type: str = 'full'
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Optimize app description with keyword integration and conversion focus.
|
||||
|
||||
Args:
|
||||
app_info: Dict with 'name', 'key_features', 'unique_value', 'target_audience'
|
||||
target_keywords: List of keywords to integrate naturally
|
||||
description_type: 'full', 'short' (Google), 'subtitle' (Apple)
|
||||
|
||||
Returns:
|
||||
Optimized description with analysis
|
||||
"""
|
||||
if description_type == 'short' and self.platform == 'google':
|
||||
return self._optimize_short_description(app_info, target_keywords)
|
||||
elif description_type == 'subtitle' and self.platform == 'apple':
|
||||
return self._optimize_subtitle(app_info, target_keywords)
|
||||
else:
|
||||
return self._optimize_full_description(app_info, target_keywords)
|
||||
|
||||
def optimize_keyword_field(
|
||||
self,
|
||||
target_keywords: List[str],
|
||||
app_title: str = "",
|
||||
app_description: str = ""
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Optimize Apple's 100-character keyword field.
|
||||
|
||||
Rules:
|
||||
- No spaces between commas
|
||||
- No plural forms if singular exists
|
||||
- No duplicates
|
||||
- Keywords in title/subtitle are already indexed
|
||||
|
||||
Args:
|
||||
target_keywords: List of target keywords
|
||||
app_title: Current app title (to avoid duplication)
|
||||
app_description: Current description (to check coverage)
|
||||
|
||||
Returns:
|
||||
Optimized keyword field (comma-separated, no spaces)
|
||||
"""
|
||||
if self.platform != 'apple':
|
||||
return {'error': 'Keyword field optimization only applies to Apple App Store'}
|
||||
|
||||
max_length = self.limits['keywords']
|
||||
|
||||
# Extract words already in title (these don't need to be in keyword field)
|
||||
title_words = set(app_title.lower().split()) if app_title else set()
|
||||
|
||||
# Process keywords
|
||||
processed_keywords = []
|
||||
for keyword in target_keywords:
|
||||
keyword_lower = keyword.lower().strip()
|
||||
|
||||
# Skip if already in title
|
||||
if keyword_lower in title_words:
|
||||
continue
|
||||
|
||||
# Remove duplicates and process
|
||||
words = keyword_lower.split()
|
||||
for word in words:
|
||||
if word not in processed_keywords and word not in title_words:
|
||||
processed_keywords.append(word)
|
||||
|
||||
# Remove plurals if singular exists
|
||||
deduplicated = self._remove_plural_duplicates(processed_keywords)
|
||||
|
||||
# Build keyword field within 100 character limit
|
||||
keyword_field = self._build_keyword_field(deduplicated, max_length)
|
||||
|
||||
# Calculate keyword density in description
|
||||
density = self._calculate_coverage(target_keywords, app_description)
|
||||
|
||||
return {
|
||||
'keyword_field': keyword_field,
|
||||
'length': len(keyword_field),
|
||||
'remaining_chars': max_length - len(keyword_field),
|
||||
'keywords_included': keyword_field.split(','),
|
||||
'keywords_count': len(keyword_field.split(',')),
|
||||
'keywords_excluded': [kw for kw in target_keywords if kw.lower() not in keyword_field],
|
||||
'description_coverage': density,
|
||||
'optimization_tips': [
|
||||
'Keywords in title are auto-indexed - no need to repeat',
|
||||
'Use singular forms only (Apple indexes plurals automatically)',
|
||||
'No spaces between commas to maximize character usage',
|
||||
'Update keyword field with each app update to test variations'
|
||||
]
|
||||
}
|
||||
|
||||
def validate_character_limits(
|
||||
self,
|
||||
metadata: Dict[str, str]
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Validate all metadata fields against platform character limits.
|
||||
|
||||
Args:
|
||||
metadata: Dictionary of field_name: value
|
||||
|
||||
Returns:
|
||||
Validation report with errors and warnings
|
||||
"""
|
||||
validation_results = {
|
||||
'is_valid': True,
|
||||
'errors': [],
|
||||
'warnings': [],
|
||||
'field_status': {}
|
||||
}
|
||||
|
||||
for field_name, value in metadata.items():
|
||||
if field_name not in self.limits:
|
||||
validation_results['warnings'].append(
|
||||
f"Unknown field '{field_name}' for {self.platform} platform"
|
||||
)
|
||||
continue
|
||||
|
||||
max_length = self.limits[field_name]
|
||||
actual_length = len(value)
|
||||
remaining = max_length - actual_length
|
||||
|
||||
field_status = {
|
||||
'value': value,
|
||||
'length': actual_length,
|
||||
'limit': max_length,
|
||||
'remaining': remaining,
|
||||
'is_valid': actual_length <= max_length,
|
||||
'usage_percentage': round((actual_length / max_length) * 100, 1)
|
||||
}
|
||||
|
||||
validation_results['field_status'][field_name] = field_status
|
||||
|
||||
if actual_length > max_length:
|
||||
validation_results['is_valid'] = False
|
||||
validation_results['errors'].append(
|
||||
f"'{field_name}' exceeds limit: {actual_length}/{max_length} chars"
|
||||
)
|
||||
elif remaining > max_length * 0.2: # More than 20% unused
|
||||
validation_results['warnings'].append(
|
||||
f"'{field_name}' under-utilizes space: {remaining} chars remaining"
|
||||
)
|
||||
|
||||
return validation_results
|
||||
|
||||
def calculate_keyword_density(
|
||||
self,
|
||||
text: str,
|
||||
target_keywords: List[str]
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Calculate keyword density in text.
|
||||
|
||||
Args:
|
||||
text: Text to analyze
|
||||
target_keywords: Keywords to check
|
||||
|
||||
Returns:
|
||||
Density analysis
|
||||
"""
|
||||
text_lower = text.lower()
|
||||
total_words = len(text_lower.split())
|
||||
|
||||
keyword_densities = {}
|
||||
for keyword in target_keywords:
|
||||
keyword_lower = keyword.lower()
|
||||
count = text_lower.count(keyword_lower)
|
||||
density = (count / total_words * 100) if total_words > 0 else 0
|
||||
|
||||
keyword_densities[keyword] = {
|
||||
'occurrences': count,
|
||||
'density_percentage': round(density, 2),
|
||||
'status': self._assess_density(density)
|
||||
}
|
||||
|
||||
# Overall assessment
|
||||
total_keyword_occurrences = sum(kw['occurrences'] for kw in keyword_densities.values())
|
||||
overall_density = (total_keyword_occurrences / total_words * 100) if total_words > 0 else 0
|
||||
|
||||
return {
|
||||
'total_words': total_words,
|
||||
'keyword_densities': keyword_densities,
|
||||
'overall_keyword_density': round(overall_density, 2),
|
||||
'assessment': self._assess_overall_density(overall_density),
|
||||
'recommendations': self._generate_density_recommendations(keyword_densities)
|
||||
}
|
||||
|
||||
def _build_title_with_keywords(
|
||||
self,
|
||||
app_name: str,
|
||||
keywords: List[str],
|
||||
max_length: int
|
||||
) -> Optional[str]:
|
||||
"""Build title combining app name and keywords within limit."""
|
||||
separators = [' - ', ': ', ' | ']
|
||||
|
||||
for sep in separators:
|
||||
for kw in keywords:
|
||||
title = f"{app_name}{sep}{kw}"
|
||||
if len(title) <= max_length:
|
||||
return title
|
||||
|
||||
return None
|
||||
|
||||
def _optimize_short_description(
|
||||
self,
|
||||
app_info: Dict[str, Any],
|
||||
target_keywords: List[str]
|
||||
) -> Dict[str, Any]:
|
||||
"""Optimize Google Play short description (80 chars)."""
|
||||
max_length = self.limits['short_description']
|
||||
|
||||
# Focus on unique value proposition with primary keyword
|
||||
unique_value = app_info.get('unique_value', '')
|
||||
primary_keyword = target_keywords[0] if target_keywords else ''
|
||||
|
||||
# Template: [Primary Keyword] - [Unique Value]
|
||||
short_desc = f"{primary_keyword.title()} - {unique_value}"[:max_length]
|
||||
|
||||
return {
|
||||
'short_description': short_desc,
|
||||
'length': len(short_desc),
|
||||
'remaining_chars': max_length - len(short_desc),
|
||||
'keywords_included': [primary_keyword] if primary_keyword in short_desc.lower() else [],
|
||||
'strategy': 'keyword_value_proposition'
|
||||
}
|
||||
|
||||
def _optimize_subtitle(
|
||||
self,
|
||||
app_info: Dict[str, Any],
|
||||
target_keywords: List[str]
|
||||
) -> Dict[str, Any]:
|
||||
"""Optimize Apple App Store subtitle (30 chars)."""
|
||||
max_length = self.limits['subtitle']
|
||||
|
||||
# Very concise - primary keyword or key feature
|
||||
primary_keyword = target_keywords[0] if target_keywords else ''
|
||||
key_feature = app_info.get('key_features', [''])[0] if app_info.get('key_features') else ''
|
||||
|
||||
options = [
|
||||
primary_keyword[:max_length],
|
||||
key_feature[:max_length],
|
||||
f"{primary_keyword} App"[:max_length]
|
||||
]
|
||||
|
||||
return {
|
||||
'subtitle_options': [opt for opt in options if opt],
|
||||
'max_length': max_length,
|
||||
'recommendation': options[0] if options else ''
|
||||
}
|
||||
|
||||
def _optimize_full_description(
|
||||
self,
|
||||
app_info: Dict[str, Any],
|
||||
target_keywords: List[str]
|
||||
) -> Dict[str, Any]:
|
||||
"""Optimize full app description (4000 chars for both platforms)."""
|
||||
max_length = self.limits.get('description', self.limits.get('full_description', 4000))
|
||||
|
||||
# Structure: Hook → Features → Benefits → Social Proof → CTA
|
||||
sections = []
|
||||
|
||||
# Hook (with primary keyword)
|
||||
primary_keyword = target_keywords[0] if target_keywords else ''
|
||||
unique_value = app_info.get('unique_value', '')
|
||||
hook = f"{unique_value} {primary_keyword.title()} that helps you achieve more.\n\n"
|
||||
sections.append(hook)
|
||||
|
||||
# Features (with keywords naturally integrated)
|
||||
features = app_info.get('key_features', [])
|
||||
if features:
|
||||
sections.append("KEY FEATURES:\n")
|
||||
for i, feature in enumerate(features[:5], 1):
|
||||
# Integrate keywords naturally
|
||||
feature_text = f"• {feature}"
|
||||
if i <= len(target_keywords):
|
||||
keyword = target_keywords[i-1]
|
||||
if keyword.lower() not in feature.lower():
|
||||
feature_text = f"• {feature} with {keyword}"
|
||||
sections.append(f"{feature_text}\n")
|
||||
sections.append("\n")
|
||||
|
||||
# Benefits
|
||||
target_audience = app_info.get('target_audience', 'users')
|
||||
sections.append(f"PERFECT FOR:\n{target_audience}\n\n")
|
||||
|
||||
# Social proof placeholder
|
||||
sections.append("WHY USERS LOVE US:\n")
|
||||
sections.append("Join thousands of satisfied users who have transformed their workflow.\n\n")
|
||||
|
||||
# CTA
|
||||
sections.append("Download now and start experiencing the difference!")
|
||||
|
||||
# Combine and validate length
|
||||
full_description = "".join(sections)
|
||||
if len(full_description) > max_length:
|
||||
full_description = full_description[:max_length-3] + "..."
|
||||
|
||||
# Calculate keyword density
|
||||
density = self.calculate_keyword_density(full_description, target_keywords)
|
||||
|
||||
return {
|
||||
'full_description': full_description,
|
||||
'length': len(full_description),
|
||||
'remaining_chars': max_length - len(full_description),
|
||||
'keyword_analysis': density,
|
||||
'structure': {
|
||||
'has_hook': True,
|
||||
'has_features': len(features) > 0,
|
||||
'has_benefits': True,
|
||||
'has_cta': True
|
||||
}
|
||||
}
|
||||
|
||||
def _remove_plural_duplicates(self, keywords: List[str]) -> List[str]:
|
||||
"""Remove plural forms if singular exists."""
|
||||
deduplicated = []
|
||||
singular_set = set()
|
||||
|
||||
for keyword in keywords:
|
||||
if keyword.endswith('s') and len(keyword) > 1:
|
||||
singular = keyword[:-1]
|
||||
if singular not in singular_set:
|
||||
deduplicated.append(singular)
|
||||
singular_set.add(singular)
|
||||
else:
|
||||
if keyword not in singular_set:
|
||||
deduplicated.append(keyword)
|
||||
singular_set.add(keyword)
|
||||
|
||||
return deduplicated
|
||||
|
||||
def _build_keyword_field(self, keywords: List[str], max_length: int) -> str:
|
||||
"""Build comma-separated keyword field within character limit."""
|
||||
keyword_field = ""
|
||||
|
||||
for keyword in keywords:
|
||||
test_field = f"{keyword_field},{keyword}" if keyword_field else keyword
|
||||
if len(test_field) <= max_length:
|
||||
keyword_field = test_field
|
||||
else:
|
||||
break
|
||||
|
||||
return keyword_field
|
||||
|
||||
def _calculate_coverage(self, keywords: List[str], text: str) -> Dict[str, int]:
|
||||
"""Calculate how many keywords are covered in text."""
|
||||
text_lower = text.lower()
|
||||
coverage = {}
|
||||
|
||||
for keyword in keywords:
|
||||
coverage[keyword] = text_lower.count(keyword.lower())
|
||||
|
||||
return coverage
|
||||
|
||||
def _assess_density(self, density: float) -> str:
|
||||
"""Assess individual keyword density."""
|
||||
if density < 0.5:
|
||||
return "too_low"
|
||||
elif density <= 2.5:
|
||||
return "optimal"
|
||||
else:
|
||||
return "too_high"
|
||||
|
||||
def _assess_overall_density(self, density: float) -> str:
|
||||
"""Assess overall keyword density."""
|
||||
if density < 2:
|
||||
return "Under-optimized: Consider adding more keyword variations"
|
||||
elif density <= 5:
|
||||
return "Optimal: Good keyword integration without stuffing"
|
||||
elif density <= 8:
|
||||
return "High: Approaching keyword stuffing - reduce keyword usage"
|
||||
else:
|
||||
return "Too High: Keyword stuffing detected - rewrite for natural flow"
|
||||
|
||||
def _generate_density_recommendations(
|
||||
self,
|
||||
keyword_densities: Dict[str, Dict[str, Any]]
|
||||
) -> List[str]:
|
||||
"""Generate recommendations based on keyword density analysis."""
|
||||
recommendations = []
|
||||
|
||||
for keyword, data in keyword_densities.items():
|
||||
if data['status'] == 'too_low':
|
||||
recommendations.append(
|
||||
f"Increase usage of '{keyword}' - currently only {data['occurrences']} times"
|
||||
)
|
||||
elif data['status'] == 'too_high':
|
||||
recommendations.append(
|
||||
f"Reduce usage of '{keyword}' - appears {data['occurrences']} times (keyword stuffing risk)"
|
||||
)
|
||||
|
||||
if not recommendations:
|
||||
recommendations.append("Keyword density is well-balanced")
|
||||
|
||||
return recommendations
|
||||
|
||||
def _recommend_title_option(self, options: List[Dict[str, Any]]) -> str:
|
||||
"""Recommend best title option based on strategy."""
|
||||
if not options:
|
||||
return "No valid options available"
|
||||
|
||||
# Prefer brand_plus_primary for established apps
|
||||
for option in options:
|
||||
if option['strategy'] == 'brand_plus_primary':
|
||||
return f"Recommended: '{option['title']}' (Balance of brand and SEO)"
|
||||
|
||||
# Fallback to first option
|
||||
return f"Recommended: '{options[0]['title']}' ({options[0]['strategy']})"
|
||||
|
||||
|
||||
def optimize_app_metadata(
|
||||
platform: str,
|
||||
app_info: Dict[str, Any],
|
||||
target_keywords: List[str]
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Convenience function to optimize all metadata fields.
|
||||
|
||||
Args:
|
||||
platform: 'apple' or 'google'
|
||||
app_info: App information dictionary
|
||||
target_keywords: Target keywords list
|
||||
|
||||
Returns:
|
||||
Complete metadata optimization package
|
||||
"""
|
||||
optimizer = MetadataOptimizer(platform)
|
||||
|
||||
return {
|
||||
'platform': platform,
|
||||
'title': optimizer.optimize_title(
|
||||
app_info['name'],
|
||||
target_keywords
|
||||
),
|
||||
'description': optimizer.optimize_description(
|
||||
app_info,
|
||||
target_keywords,
|
||||
'full'
|
||||
),
|
||||
'keyword_field': optimizer.optimize_keyword_field(
|
||||
target_keywords
|
||||
) if platform == 'apple' else None
|
||||
}
|
||||
714
.agents/skills/app-store-optimization/scripts/review_analyzer.py
Normal file
714
.agents/skills/app-store-optimization/scripts/review_analyzer.py
Normal file
@@ -0,0 +1,714 @@
|
||||
"""
|
||||
Review analysis module for App Store Optimization.
|
||||
Analyzes user reviews for sentiment, issues, and feature requests.
|
||||
"""
|
||||
|
||||
from typing import Dict, List, Any, Optional, Tuple
|
||||
from collections import Counter
|
||||
import re
|
||||
|
||||
|
||||
class ReviewAnalyzer:
|
||||
"""Analyzes user reviews for actionable insights."""
|
||||
|
||||
# Sentiment keywords
|
||||
POSITIVE_KEYWORDS = [
|
||||
'great', 'awesome', 'excellent', 'amazing', 'love', 'best', 'perfect',
|
||||
'fantastic', 'wonderful', 'brilliant', 'outstanding', 'superb'
|
||||
]
|
||||
|
||||
NEGATIVE_KEYWORDS = [
|
||||
'bad', 'terrible', 'awful', 'horrible', 'hate', 'worst', 'useless',
|
||||
'broken', 'crash', 'bug', 'slow', 'disappointing', 'frustrating'
|
||||
]
|
||||
|
||||
# Issue indicators
|
||||
ISSUE_KEYWORDS = [
|
||||
'crash', 'bug', 'error', 'broken', 'not working', 'doesnt work',
|
||||
'freezes', 'slow', 'laggy', 'glitch', 'problem', 'issue', 'fail'
|
||||
]
|
||||
|
||||
# Feature request indicators
|
||||
FEATURE_REQUEST_KEYWORDS = [
|
||||
'wish', 'would be nice', 'should add', 'need', 'want', 'hope',
|
||||
'please add', 'missing', 'lacks', 'feature request'
|
||||
]
|
||||
|
||||
def __init__(self, app_name: str):
|
||||
"""
|
||||
Initialize review analyzer.
|
||||
|
||||
Args:
|
||||
app_name: Name of the app
|
||||
"""
|
||||
self.app_name = app_name
|
||||
self.reviews = []
|
||||
self.analysis_cache = {}
|
||||
|
||||
def analyze_sentiment(
|
||||
self,
|
||||
reviews: List[Dict[str, Any]]
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Analyze sentiment across reviews.
|
||||
|
||||
Args:
|
||||
reviews: List of review dicts with 'text', 'rating', 'date'
|
||||
|
||||
Returns:
|
||||
Sentiment analysis summary
|
||||
"""
|
||||
self.reviews = reviews
|
||||
|
||||
sentiment_counts = {
|
||||
'positive': 0,
|
||||
'neutral': 0,
|
||||
'negative': 0
|
||||
}
|
||||
|
||||
detailed_sentiments = []
|
||||
|
||||
for review in reviews:
|
||||
text = review.get('text', '').lower()
|
||||
rating = review.get('rating', 3)
|
||||
|
||||
# Calculate sentiment score
|
||||
sentiment_score = self._calculate_sentiment_score(text, rating)
|
||||
sentiment_category = self._categorize_sentiment(sentiment_score)
|
||||
|
||||
sentiment_counts[sentiment_category] += 1
|
||||
|
||||
detailed_sentiments.append({
|
||||
'review_id': review.get('id', ''),
|
||||
'rating': rating,
|
||||
'sentiment_score': sentiment_score,
|
||||
'sentiment': sentiment_category,
|
||||
'text_preview': text[:100] + '...' if len(text) > 100 else text
|
||||
})
|
||||
|
||||
# Calculate percentages
|
||||
total = len(reviews)
|
||||
sentiment_distribution = {
|
||||
'positive': round((sentiment_counts['positive'] / total) * 100, 1) if total > 0 else 0,
|
||||
'neutral': round((sentiment_counts['neutral'] / total) * 100, 1) if total > 0 else 0,
|
||||
'negative': round((sentiment_counts['negative'] / total) * 100, 1) if total > 0 else 0
|
||||
}
|
||||
|
||||
# Calculate average rating
|
||||
avg_rating = sum(r.get('rating', 0) for r in reviews) / total if total > 0 else 0
|
||||
|
||||
return {
|
||||
'total_reviews_analyzed': total,
|
||||
'average_rating': round(avg_rating, 2),
|
||||
'sentiment_distribution': sentiment_distribution,
|
||||
'sentiment_counts': sentiment_counts,
|
||||
'sentiment_trend': self._assess_sentiment_trend(sentiment_distribution),
|
||||
'detailed_sentiments': detailed_sentiments[:50] # Limit output
|
||||
}
|
||||
|
||||
def extract_common_themes(
|
||||
self,
|
||||
reviews: List[Dict[str, Any]],
|
||||
min_mentions: int = 3
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Extract frequently mentioned themes and topics.
|
||||
|
||||
Args:
|
||||
reviews: List of review dicts
|
||||
min_mentions: Minimum mentions to be considered common
|
||||
|
||||
Returns:
|
||||
Common themes analysis
|
||||
"""
|
||||
# Extract all words from reviews
|
||||
all_words = []
|
||||
all_phrases = []
|
||||
|
||||
for review in reviews:
|
||||
text = review.get('text', '').lower()
|
||||
# Clean text
|
||||
text = re.sub(r'[^\w\s]', ' ', text)
|
||||
words = text.split()
|
||||
|
||||
# Filter out common words
|
||||
stop_words = {
|
||||
'the', 'and', 'for', 'with', 'this', 'that', 'from', 'have',
|
||||
'app', 'apps', 'very', 'really', 'just', 'but', 'not', 'you'
|
||||
}
|
||||
words = [w for w in words if w not in stop_words and len(w) > 3]
|
||||
|
||||
all_words.extend(words)
|
||||
|
||||
# Extract 2-3 word phrases
|
||||
for i in range(len(words) - 1):
|
||||
phrase = f"{words[i]} {words[i+1]}"
|
||||
all_phrases.append(phrase)
|
||||
|
||||
# Count frequency
|
||||
word_freq = Counter(all_words)
|
||||
phrase_freq = Counter(all_phrases)
|
||||
|
||||
# Filter by min_mentions
|
||||
common_words = [
|
||||
{'word': word, 'mentions': count}
|
||||
for word, count in word_freq.most_common(30)
|
||||
if count >= min_mentions
|
||||
]
|
||||
|
||||
common_phrases = [
|
||||
{'phrase': phrase, 'mentions': count}
|
||||
for phrase, count in phrase_freq.most_common(20)
|
||||
if count >= min_mentions
|
||||
]
|
||||
|
||||
# Categorize themes
|
||||
themes = self._categorize_themes(common_words, common_phrases)
|
||||
|
||||
return {
|
||||
'common_words': common_words,
|
||||
'common_phrases': common_phrases,
|
||||
'identified_themes': themes,
|
||||
'insights': self._generate_theme_insights(themes)
|
||||
}
|
||||
|
||||
def identify_issues(
|
||||
self,
|
||||
reviews: List[Dict[str, Any]],
|
||||
rating_threshold: int = 3
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Identify bugs, crashes, and other issues from reviews.
|
||||
|
||||
Args:
|
||||
reviews: List of review dicts
|
||||
rating_threshold: Only analyze reviews at or below this rating
|
||||
|
||||
Returns:
|
||||
Issue identification report
|
||||
"""
|
||||
issues = []
|
||||
|
||||
for review in reviews:
|
||||
rating = review.get('rating', 5)
|
||||
if rating > rating_threshold:
|
||||
continue
|
||||
|
||||
text = review.get('text', '').lower()
|
||||
|
||||
# Check for issue keywords
|
||||
mentioned_issues = []
|
||||
for keyword in self.ISSUE_KEYWORDS:
|
||||
if keyword in text:
|
||||
mentioned_issues.append(keyword)
|
||||
|
||||
if mentioned_issues:
|
||||
issues.append({
|
||||
'review_id': review.get('id', ''),
|
||||
'rating': rating,
|
||||
'date': review.get('date', ''),
|
||||
'issue_keywords': mentioned_issues,
|
||||
'text': text[:200] + '...' if len(text) > 200 else text
|
||||
})
|
||||
|
||||
# Group by issue type
|
||||
issue_frequency = Counter()
|
||||
for issue in issues:
|
||||
for keyword in issue['issue_keywords']:
|
||||
issue_frequency[keyword] += 1
|
||||
|
||||
# Categorize issues
|
||||
categorized_issues = self._categorize_issues(issues)
|
||||
|
||||
# Calculate issue severity
|
||||
severity_scores = self._calculate_issue_severity(
|
||||
categorized_issues,
|
||||
len(reviews)
|
||||
)
|
||||
|
||||
return {
|
||||
'total_issues_found': len(issues),
|
||||
'issue_frequency': dict(issue_frequency.most_common(15)),
|
||||
'categorized_issues': categorized_issues,
|
||||
'severity_scores': severity_scores,
|
||||
'top_issues': self._rank_issues_by_severity(severity_scores),
|
||||
'recommendations': self._generate_issue_recommendations(
|
||||
categorized_issues,
|
||||
severity_scores
|
||||
)
|
||||
}
|
||||
|
||||
def find_feature_requests(
|
||||
self,
|
||||
reviews: List[Dict[str, Any]]
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Extract feature requests and desired improvements.
|
||||
|
||||
Args:
|
||||
reviews: List of review dicts
|
||||
|
||||
Returns:
|
||||
Feature request analysis
|
||||
"""
|
||||
feature_requests = []
|
||||
|
||||
for review in reviews:
|
||||
text = review.get('text', '').lower()
|
||||
rating = review.get('rating', 3)
|
||||
|
||||
# Check for feature request indicators
|
||||
is_feature_request = any(
|
||||
keyword in text
|
||||
for keyword in self.FEATURE_REQUEST_KEYWORDS
|
||||
)
|
||||
|
||||
if is_feature_request:
|
||||
# Extract the specific request
|
||||
request_text = self._extract_feature_request_text(text)
|
||||
|
||||
feature_requests.append({
|
||||
'review_id': review.get('id', ''),
|
||||
'rating': rating,
|
||||
'date': review.get('date', ''),
|
||||
'request_text': request_text,
|
||||
'full_review': text[:200] + '...' if len(text) > 200 else text
|
||||
})
|
||||
|
||||
# Cluster similar requests
|
||||
clustered_requests = self._cluster_feature_requests(feature_requests)
|
||||
|
||||
# Prioritize based on frequency and rating context
|
||||
prioritized_requests = self._prioritize_feature_requests(clustered_requests)
|
||||
|
||||
return {
|
||||
'total_feature_requests': len(feature_requests),
|
||||
'clustered_requests': clustered_requests,
|
||||
'prioritized_requests': prioritized_requests,
|
||||
'implementation_recommendations': self._generate_feature_recommendations(
|
||||
prioritized_requests
|
||||
)
|
||||
}
|
||||
|
||||
def track_sentiment_trends(
|
||||
self,
|
||||
reviews_by_period: Dict[str, List[Dict[str, Any]]]
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Track sentiment changes over time.
|
||||
|
||||
Args:
|
||||
reviews_by_period: Dict of period_name: reviews
|
||||
|
||||
Returns:
|
||||
Trend analysis
|
||||
"""
|
||||
trends = []
|
||||
|
||||
for period, reviews in reviews_by_period.items():
|
||||
sentiment = self.analyze_sentiment(reviews)
|
||||
|
||||
trends.append({
|
||||
'period': period,
|
||||
'total_reviews': len(reviews),
|
||||
'average_rating': sentiment['average_rating'],
|
||||
'positive_percentage': sentiment['sentiment_distribution']['positive'],
|
||||
'negative_percentage': sentiment['sentiment_distribution']['negative']
|
||||
})
|
||||
|
||||
# Calculate trend direction
|
||||
if len(trends) >= 2:
|
||||
first_period = trends[0]
|
||||
last_period = trends[-1]
|
||||
|
||||
rating_change = last_period['average_rating'] - first_period['average_rating']
|
||||
sentiment_change = last_period['positive_percentage'] - first_period['positive_percentage']
|
||||
|
||||
trend_direction = self._determine_trend_direction(
|
||||
rating_change,
|
||||
sentiment_change
|
||||
)
|
||||
else:
|
||||
trend_direction = 'insufficient_data'
|
||||
|
||||
return {
|
||||
'periods_analyzed': len(trends),
|
||||
'trend_data': trends,
|
||||
'trend_direction': trend_direction,
|
||||
'insights': self._generate_trend_insights(trends, trend_direction)
|
||||
}
|
||||
|
||||
def generate_response_templates(
|
||||
self,
|
||||
issue_category: str
|
||||
) -> List[Dict[str, str]]:
|
||||
"""
|
||||
Generate response templates for common review scenarios.
|
||||
|
||||
Args:
|
||||
issue_category: Category of issue ('crash', 'feature_request', 'positive', etc.)
|
||||
|
||||
Returns:
|
||||
Response templates
|
||||
"""
|
||||
templates = {
|
||||
'crash': [
|
||||
{
|
||||
'scenario': 'App crash reported',
|
||||
'template': "Thank you for bringing this to our attention. We're sorry you experienced a crash. "
|
||||
"Our team is investigating this issue. Could you please share more details about when "
|
||||
"this occurred (device model, iOS/Android version) by contacting support@[company].com? "
|
||||
"We're committed to fixing this quickly."
|
||||
},
|
||||
{
|
||||
'scenario': 'Crash already fixed',
|
||||
'template': "Thank you for your feedback. We've identified and fixed this crash issue in version [X.X]. "
|
||||
"Please update to the latest version. If the problem persists, please reach out to "
|
||||
"support@[company].com and we'll help you directly."
|
||||
}
|
||||
],
|
||||
'bug': [
|
||||
{
|
||||
'scenario': 'Bug reported',
|
||||
'template': "Thanks for reporting this bug. We take these issues seriously. Our team is looking into it "
|
||||
"and we'll have a fix in an upcoming update. We appreciate your patience and will notify you "
|
||||
"when it's resolved."
|
||||
}
|
||||
],
|
||||
'feature_request': [
|
||||
{
|
||||
'scenario': 'Feature request received',
|
||||
'template': "Thank you for this suggestion! We're always looking to improve [app_name]. We've added your "
|
||||
"request to our roadmap and will consider it for a future update. Follow us @[social] for "
|
||||
"updates on new features."
|
||||
},
|
||||
{
|
||||
'scenario': 'Feature already planned',
|
||||
'template': "Great news! This feature is already on our roadmap and we're working on it. Stay tuned for "
|
||||
"updates in the coming months. Thanks for your feedback!"
|
||||
}
|
||||
],
|
||||
'positive': [
|
||||
{
|
||||
'scenario': 'Positive review',
|
||||
'template': "Thank you so much for your kind words! We're thrilled that you're enjoying [app_name]. "
|
||||
"Reviews like yours motivate our team to keep improving. If you ever have suggestions, "
|
||||
"we'd love to hear them!"
|
||||
}
|
||||
],
|
||||
'negative_general': [
|
||||
{
|
||||
'scenario': 'General complaint',
|
||||
'template': "We're sorry to hear you're not satisfied with your experience. We'd like to make this right. "
|
||||
"Please contact us at support@[company].com so we can understand the issue better and help "
|
||||
"you directly. Thank you for giving us a chance to improve."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
return templates.get(issue_category, templates['negative_general'])
|
||||
|
||||
def _calculate_sentiment_score(self, text: str, rating: int) -> float:
|
||||
"""Calculate sentiment score (-1 to 1)."""
|
||||
# Start with rating-based score
|
||||
rating_score = (rating - 3) / 2 # Convert 1-5 to -1 to 1
|
||||
|
||||
# Adjust based on text sentiment
|
||||
positive_count = sum(1 for keyword in self.POSITIVE_KEYWORDS if keyword in text)
|
||||
negative_count = sum(1 for keyword in self.NEGATIVE_KEYWORDS if keyword in text)
|
||||
|
||||
text_score = (positive_count - negative_count) / 10 # Normalize
|
||||
|
||||
# Weighted average (60% rating, 40% text)
|
||||
final_score = (rating_score * 0.6) + (text_score * 0.4)
|
||||
|
||||
return max(min(final_score, 1.0), -1.0)
|
||||
|
||||
def _categorize_sentiment(self, score: float) -> str:
|
||||
"""Categorize sentiment score."""
|
||||
if score > 0.3:
|
||||
return 'positive'
|
||||
elif score < -0.3:
|
||||
return 'negative'
|
||||
else:
|
||||
return 'neutral'
|
||||
|
||||
def _assess_sentiment_trend(self, distribution: Dict[str, float]) -> str:
|
||||
"""Assess overall sentiment trend."""
|
||||
positive = distribution['positive']
|
||||
negative = distribution['negative']
|
||||
|
||||
if positive > 70:
|
||||
return 'very_positive'
|
||||
elif positive > 50:
|
||||
return 'positive'
|
||||
elif negative > 30:
|
||||
return 'concerning'
|
||||
elif negative > 50:
|
||||
return 'critical'
|
||||
else:
|
||||
return 'mixed'
|
||||
|
||||
def _categorize_themes(
|
||||
self,
|
||||
common_words: List[Dict[str, Any]],
|
||||
common_phrases: List[Dict[str, Any]]
|
||||
) -> Dict[str, List[str]]:
|
||||
"""Categorize themes from words and phrases."""
|
||||
themes = {
|
||||
'features': [],
|
||||
'performance': [],
|
||||
'usability': [],
|
||||
'support': [],
|
||||
'pricing': []
|
||||
}
|
||||
|
||||
# Keywords for each category
|
||||
feature_keywords = {'feature', 'functionality', 'option', 'tool'}
|
||||
performance_keywords = {'fast', 'slow', 'crash', 'lag', 'speed', 'performance'}
|
||||
usability_keywords = {'easy', 'difficult', 'intuitive', 'confusing', 'interface', 'design'}
|
||||
support_keywords = {'support', 'help', 'customer', 'service', 'response'}
|
||||
pricing_keywords = {'price', 'cost', 'expensive', 'cheap', 'subscription', 'free'}
|
||||
|
||||
for word_data in common_words:
|
||||
word = word_data['word']
|
||||
if any(kw in word for kw in feature_keywords):
|
||||
themes['features'].append(word)
|
||||
elif any(kw in word for kw in performance_keywords):
|
||||
themes['performance'].append(word)
|
||||
elif any(kw in word for kw in usability_keywords):
|
||||
themes['usability'].append(word)
|
||||
elif any(kw in word for kw in support_keywords):
|
||||
themes['support'].append(word)
|
||||
elif any(kw in word for kw in pricing_keywords):
|
||||
themes['pricing'].append(word)
|
||||
|
||||
return {k: v for k, v in themes.items() if v} # Remove empty categories
|
||||
|
||||
def _generate_theme_insights(self, themes: Dict[str, List[str]]) -> List[str]:
|
||||
"""Generate insights from themes."""
|
||||
insights = []
|
||||
|
||||
for category, keywords in themes.items():
|
||||
if keywords:
|
||||
insights.append(
|
||||
f"{category.title()}: Users frequently mention {', '.join(keywords[:3])}"
|
||||
)
|
||||
|
||||
return insights[:5]
|
||||
|
||||
def _categorize_issues(self, issues: List[Dict[str, Any]]) -> Dict[str, List[Dict[str, Any]]]:
|
||||
"""Categorize issues by type."""
|
||||
categories = {
|
||||
'crashes': [],
|
||||
'bugs': [],
|
||||
'performance': [],
|
||||
'compatibility': []
|
||||
}
|
||||
|
||||
for issue in issues:
|
||||
keywords = issue['issue_keywords']
|
||||
|
||||
if 'crash' in keywords or 'freezes' in keywords:
|
||||
categories['crashes'].append(issue)
|
||||
elif 'bug' in keywords or 'error' in keywords or 'broken' in keywords:
|
||||
categories['bugs'].append(issue)
|
||||
elif 'slow' in keywords or 'laggy' in keywords:
|
||||
categories['performance'].append(issue)
|
||||
else:
|
||||
categories['compatibility'].append(issue)
|
||||
|
||||
return {k: v for k, v in categories.items() if v}
|
||||
|
||||
def _calculate_issue_severity(
|
||||
self,
|
||||
categorized_issues: Dict[str, List[Dict[str, Any]]],
|
||||
total_reviews: int
|
||||
) -> Dict[str, Dict[str, Any]]:
|
||||
"""Calculate severity scores for each issue category."""
|
||||
severity_scores = {}
|
||||
|
||||
for category, issues in categorized_issues.items():
|
||||
count = len(issues)
|
||||
percentage = (count / total_reviews) * 100 if total_reviews > 0 else 0
|
||||
|
||||
# Calculate average rating of affected reviews
|
||||
avg_rating = sum(i['rating'] for i in issues) / count if count > 0 else 0
|
||||
|
||||
# Severity score (0-100)
|
||||
severity = min((percentage * 10) + ((5 - avg_rating) * 10), 100)
|
||||
|
||||
severity_scores[category] = {
|
||||
'count': count,
|
||||
'percentage': round(percentage, 2),
|
||||
'average_rating': round(avg_rating, 2),
|
||||
'severity_score': round(severity, 1),
|
||||
'priority': 'critical' if severity > 70 else ('high' if severity > 40 else 'medium')
|
||||
}
|
||||
|
||||
return severity_scores
|
||||
|
||||
def _rank_issues_by_severity(
|
||||
self,
|
||||
severity_scores: Dict[str, Dict[str, Any]]
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Rank issues by severity score."""
|
||||
ranked = sorted(
|
||||
[{'category': cat, **data} for cat, data in severity_scores.items()],
|
||||
key=lambda x: x['severity_score'],
|
||||
reverse=True
|
||||
)
|
||||
return ranked
|
||||
|
||||
def _generate_issue_recommendations(
|
||||
self,
|
||||
categorized_issues: Dict[str, List[Dict[str, Any]]],
|
||||
severity_scores: Dict[str, Dict[str, Any]]
|
||||
) -> List[str]:
|
||||
"""Generate recommendations for addressing issues."""
|
||||
recommendations = []
|
||||
|
||||
for category, score_data in severity_scores.items():
|
||||
if score_data['priority'] == 'critical':
|
||||
recommendations.append(
|
||||
f"URGENT: Address {category} issues immediately - affecting {score_data['percentage']}% of reviews"
|
||||
)
|
||||
elif score_data['priority'] == 'high':
|
||||
recommendations.append(
|
||||
f"HIGH PRIORITY: Focus on {category} issues in next update"
|
||||
)
|
||||
|
||||
return recommendations
|
||||
|
||||
def _extract_feature_request_text(self, text: str) -> str:
|
||||
"""Extract the specific feature request from review text."""
|
||||
# Simple extraction - find sentence with feature request keywords
|
||||
sentences = text.split('.')
|
||||
for sentence in sentences:
|
||||
if any(keyword in sentence for keyword in self.FEATURE_REQUEST_KEYWORDS):
|
||||
return sentence.strip()
|
||||
return text[:100] # Fallback
|
||||
|
||||
def _cluster_feature_requests(
|
||||
self,
|
||||
feature_requests: List[Dict[str, Any]]
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Cluster similar feature requests."""
|
||||
# Simplified clustering - group by common keywords
|
||||
clusters = {}
|
||||
|
||||
for request in feature_requests:
|
||||
text = request['request_text'].lower()
|
||||
# Extract key words
|
||||
words = [w for w in text.split() if len(w) > 4]
|
||||
|
||||
# Try to find matching cluster
|
||||
matched = False
|
||||
for cluster_key in clusters:
|
||||
if any(word in cluster_key for word in words[:3]):
|
||||
clusters[cluster_key].append(request)
|
||||
matched = True
|
||||
break
|
||||
|
||||
if not matched and words:
|
||||
cluster_key = ' '.join(words[:2])
|
||||
clusters[cluster_key] = [request]
|
||||
|
||||
return [
|
||||
{'feature_theme': theme, 'request_count': len(requests), 'examples': requests[:3]}
|
||||
for theme, requests in clusters.items()
|
||||
]
|
||||
|
||||
def _prioritize_feature_requests(
|
||||
self,
|
||||
clustered_requests: List[Dict[str, Any]]
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Prioritize feature requests by frequency."""
|
||||
return sorted(
|
||||
clustered_requests,
|
||||
key=lambda x: x['request_count'],
|
||||
reverse=True
|
||||
)[:10]
|
||||
|
||||
def _generate_feature_recommendations(
|
||||
self,
|
||||
prioritized_requests: List[Dict[str, Any]]
|
||||
) -> List[str]:
|
||||
"""Generate recommendations for feature requests."""
|
||||
recommendations = []
|
||||
|
||||
if prioritized_requests:
|
||||
top_request = prioritized_requests[0]
|
||||
recommendations.append(
|
||||
f"Most requested feature: {top_request['feature_theme']} "
|
||||
f"({top_request['request_count']} mentions) - consider for next major release"
|
||||
)
|
||||
|
||||
if len(prioritized_requests) > 1:
|
||||
recommendations.append(
|
||||
f"Also consider: {prioritized_requests[1]['feature_theme']}"
|
||||
)
|
||||
|
||||
return recommendations
|
||||
|
||||
def _determine_trend_direction(
|
||||
self,
|
||||
rating_change: float,
|
||||
sentiment_change: float
|
||||
) -> str:
|
||||
"""Determine overall trend direction."""
|
||||
if rating_change > 0.2 and sentiment_change > 5:
|
||||
return 'improving'
|
||||
elif rating_change < -0.2 and sentiment_change < -5:
|
||||
return 'declining'
|
||||
else:
|
||||
return 'stable'
|
||||
|
||||
def _generate_trend_insights(
|
||||
self,
|
||||
trends: List[Dict[str, Any]],
|
||||
trend_direction: str
|
||||
) -> List[str]:
|
||||
"""Generate insights from trend analysis."""
|
||||
insights = []
|
||||
|
||||
if trend_direction == 'improving':
|
||||
insights.append("Positive trend: User satisfaction is increasing over time")
|
||||
elif trend_direction == 'declining':
|
||||
insights.append("WARNING: User satisfaction is declining - immediate action needed")
|
||||
else:
|
||||
insights.append("Sentiment is stable - maintain current quality")
|
||||
|
||||
# Review velocity insight
|
||||
if len(trends) >= 2:
|
||||
recent_reviews = trends[-1]['total_reviews']
|
||||
previous_reviews = trends[-2]['total_reviews']
|
||||
|
||||
if recent_reviews > previous_reviews * 1.5:
|
||||
insights.append("Review volume increasing - growing user base or recent controversy")
|
||||
|
||||
return insights
|
||||
|
||||
|
||||
def analyze_reviews(
|
||||
app_name: str,
|
||||
reviews: List[Dict[str, Any]]
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Convenience function to perform comprehensive review analysis.
|
||||
|
||||
Args:
|
||||
app_name: App name
|
||||
reviews: List of review dictionaries
|
||||
|
||||
Returns:
|
||||
Complete review analysis
|
||||
"""
|
||||
analyzer = ReviewAnalyzer(app_name)
|
||||
|
||||
return {
|
||||
'sentiment_analysis': analyzer.analyze_sentiment(reviews),
|
||||
'common_themes': analyzer.extract_common_themes(reviews),
|
||||
'issues_identified': analyzer.identify_issues(reviews),
|
||||
'feature_requests': analyzer.find_feature_requests(reviews)
|
||||
}
|
||||
Reference in New Issue
Block a user