483 lines
18 KiB
Python
483 lines
18 KiB
Python
"""
|
|
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
|
|
)
|