rankora-api / app /services /ml /ml_engine.py
Shoaib898's picture
Deploy Rankora API: buy box competitors, offers persistence, scraper fixes
5aa2bd9 verified
Raw
History Blame Contribute Delete
20 kB
"""
Rankora ML Engine
4 ML Features:
1. Fake Review Detector
2. Price Prediction (Linear Regression)
3. Demand Forecasting (Seasonality)
4. Niche Scorer
"""
import math
import random
from typing import Optional
from datetime import datetime, timezone
# ═══════════════════════════════════════════════════════════════
# 1. FAKE REVIEW DETECTOR
# ═══════════════════════════════════════════════════════════════
def detect_fake_reviews(
rating: float,
review_count: int,
rating_distribution: Optional[dict] = None,
monthly_sales_estimate: int = 100,
asin: str = ""
) -> dict:
"""
Detect suspicious review patterns using rule-based ML heuristics.
Returns a suspicion score 0-100 and detailed signals.
"""
signals = []
suspicion_score = 0
# Signal 1: Perfect or near-perfect rating with many reviews
if rating >= 4.8 and review_count > 500:
suspicion_score += 20
signals.append({
"signal": "Suspiciously High Rating",
"detail": f"{rating}β˜… with {review_count:,} reviews β€” real products rarely maintain 4.8+ at scale",
"severity": "high",
"weight": 20
})
elif rating >= 4.9 and review_count > 100:
suspicion_score += 25
signals.append({
"signal": "Near-Perfect Rating",
"detail": f"{rating}β˜… is unusually high β€” may indicate review manipulation",
"severity": "high",
"weight": 25
})
# Signal 2: Review velocity vs sales ratio
# If reviews >> expected for sales level, reviews may be incentivized
expected_reviews = monthly_sales_estimate * 0.02 # ~2% of sales leave reviews
if review_count > 0 and monthly_sales_estimate > 0:
review_rate = review_count / max(monthly_sales_estimate * 6, 1) # 6 months
if review_rate > 0.15: # more than 15% of buyers reviewing is suspicious
suspicion_score += 20
signals.append({
"signal": "High Review Velocity",
"detail": f"Review rate ({review_rate:.1%}) is unusually high vs estimated sales β€” possible incentivized reviews",
"severity": "medium",
"weight": 20
})
# Signal 3: Very low review count but high BSR
# High sales rank but very few reviews = possibly review reset / new ASIN
if monthly_sales_estimate > 500 and review_count < 20:
suspicion_score += 15
signals.append({
"signal": "Sales/Review Mismatch",
"detail": f"High estimated sales ({monthly_sales_estimate:,}/mo) but only {review_count} reviews β€” possible review manipulation reset",
"severity": "medium",
"weight": 15
})
# Signal 4: Rating distribution analysis (if provided)
if rating_distribution:
five_star_pct = rating_distribution.get("5_star", 0)
one_star_pct = rating_distribution.get("1_star", 0)
if five_star_pct > 85:
suspicion_score += 20
signals.append({
"signal": "Extreme 5-Star Concentration",
"detail": f"{five_star_pct}% five-star reviews β€” legitimate products rarely exceed 80%",
"severity": "high",
"weight": 20
})
if one_star_pct < 1 and review_count > 200:
suspicion_score += 10
signals.append({
"signal": "Missing Negative Reviews",
"detail": f"Only {one_star_pct}% 1-star reviews out of {review_count:,} β€” statistically unlikely for real products",
"severity": "medium",
"weight": 10
})
# Signal 5: Round number review counts often indicate manipulation
if review_count > 100:
str_count = str(review_count)
trailing_zeros = len(str_count) - len(str_count.rstrip("0"))
if trailing_zeros >= 2:
suspicion_score += 10
signals.append({
"signal": "Suspicious Review Count",
"detail": f"Round number ({review_count:,}) with trailing zeros β€” may indicate manipulated count",
"severity": "low",
"weight": 10
})
suspicion_score = min(suspicion_score, 100)
if suspicion_score >= 60:
verdict = "HIGH RISK β€” Likely Fake Reviews"
verdict_color = "#EF4444"
recommendation = "Avoid this product β€” high probability of review manipulation"
elif suspicion_score >= 35:
verdict = "MODERATE RISK β€” Suspicious Patterns"
verdict_color = "#F59E0B"
recommendation = "Investigate further before competing in this space"
elif suspicion_score >= 15:
verdict = "LOW RISK β€” Minor Concerns"
verdict_color = "#FB923C"
recommendation = "Reviews appear mostly genuine with minor anomalies"
else:
verdict = "AUTHENTIC β€” Reviews Appear Genuine"
verdict_color = "#10B981"
recommendation = "No significant red flags detected in review patterns"
return {
"suspicion_score": suspicion_score,
"verdict": verdict,
"verdict_color": verdict_color,
"recommendation": recommendation,
"signals": signals,
"signals_count": len(signals),
"is_suspicious": suspicion_score >= 35,
"analysis_basis": {
"rating": rating,
"review_count": review_count,
"monthly_sales_estimate": monthly_sales_estimate,
}
}
# ═══════════════════════════════════════════════════════════════
# 2. PRICE PREDICTION (Linear Regression)
# ═══════════════════════════════════════════════════════════════
def predict_price(
price_history: list,
days_ahead: int = 7
) -> dict:
"""
Predict future price using linear regression on price history.
price_history: list of {"price": float, "recorded_at": str}
"""
if not price_history or len(price_history) < 3:
return {
"predicted_price": None,
"confidence": "low",
"trend": "insufficient_data",
"message": "Need at least 3 data points for prediction"
}
# Extract prices
prices = [float(p["price"]) for p in price_history if p.get("price")]
if len(prices) < 3:
return {"predicted_price": None, "confidence": "low", "trend": "insufficient_data"}
n = len(prices)
x = list(range(n))
# Linear regression: y = mx + b
sum_x = sum(x)
sum_y = sum(prices)
sum_xy = sum(x[i] * prices[i] for i in range(n))
sum_x2 = sum(xi ** 2 for xi in x)
denom = n * sum_x2 - sum_x ** 2
if denom == 0:
slope = 0
else:
slope = (n * sum_xy - sum_x * sum_y) / denom
intercept = (sum_y - slope * sum_x) / n
# Predict future price
future_x = n - 1 + days_ahead
predicted = round(intercept + slope * future_x, 2)
predicted = max(0.01, predicted) # price can't be negative
# Calculate RΒ² for confidence
y_mean = sum_y / n
ss_tot = sum((p - y_mean) ** 2 for p in prices)
ss_res = sum((prices[i] - (intercept + slope * x[i])) ** 2 for i in range(n))
r2 = 1 - (ss_res / ss_tot) if ss_tot > 0 else 0
# Determine trend
current_price = prices[-1]
price_change_pct = ((predicted - current_price) / current_price) * 100
if slope > 0.01:
trend = "rising"
trend_emoji = "πŸ“ˆ"
trend_color = "#EF4444"
elif slope < -0.01:
trend = "falling"
trend_emoji = "πŸ“‰"
trend_color = "#10B981"
else:
trend = "stable"
trend_emoji = "➑️"
trend_color = "#6B7280"
confidence = "high" if r2 > 0.7 else "medium" if r2 > 0.4 else "low"
# Price stats
min_price = min(prices)
max_price = max(prices)
avg_price = sum(prices) / len(prices)
return {
"predicted_price": predicted,
"current_price": current_price,
"price_change": round(predicted - current_price, 2),
"price_change_pct": round(price_change_pct, 1),
"trend": trend,
"trend_emoji": trend_emoji,
"trend_color": trend_color,
"confidence": confidence,
"r_squared": round(r2, 3),
"days_ahead": days_ahead,
"slope_per_day": round(slope, 4),
"price_stats": {
"min": round(min_price, 2),
"max": round(max_price, 2),
"avg": round(avg_price, 2),
"volatility": round((max_price - min_price) / avg_price * 100, 1)
},
"recommendation": (
f"Price expected to {'rise' if trend == 'rising' else 'fall' if trend == 'falling' else 'stay stable'} "
f"by {abs(price_change_pct):.1f}% over next {days_ahead} days"
)
}
# ═══════════════════════════════════════════════════════════════
# 3. DEMAND FORECASTING (Seasonality Detection)
# ═══════════════════════════════════════════════════════════════
def forecast_demand(
bsr_history: list,
category: str = "general",
current_month: Optional[int] = None
) -> dict:
"""
Forecast demand using BSR trends and seasonal patterns.
Lower BSR = higher demand.
"""
if current_month is None:
current_month = datetime.now(timezone.utc).month
# Category seasonal multipliers (month 1-12)
seasonal_patterns = {
"electronics": [0.8, 0.7, 0.8, 0.9, 0.9, 0.8, 0.9, 0.9, 1.0, 1.0, 1.3, 1.4],
"home": [0.9, 0.8, 1.0, 1.1, 1.2, 1.1, 1.0, 0.9, 0.9, 1.0, 1.1, 1.0],
"toys": [0.7, 0.6, 0.7, 0.7, 0.8, 0.8, 0.9, 0.9, 1.0, 1.1, 1.3, 1.8],
"sports": [0.9, 0.9, 1.1, 1.2, 1.3, 1.3, 1.2, 1.1, 1.0, 0.9, 0.8, 0.8],
"kitchen": [0.9, 0.9, 1.0, 1.0, 1.0, 0.9, 0.9, 0.9, 0.9, 1.0, 1.1, 1.2],
"general": [1.0, 0.9, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.1, 1.2],
}
# Match category
cat_key = "general"
category_lower = category.lower()
for key in seasonal_patterns:
if key in category_lower:
cat_key = key
break
pattern = seasonal_patterns[cat_key]
current_multiplier = pattern[current_month - 1]
next_month = (current_month % 12)
next_multiplier = pattern[next_month]
# BSR trend analysis
bsr_trend = "stable"
bsr_change_pct = 0
demand_trend = "stable"
if bsr_history and len(bsr_history) >= 3:
bsr_values = [h.get("bsr") for h in bsr_history if h.get("bsr")]
if len(bsr_values) >= 3:
old_bsr = sum(bsr_values[:3]) / 3
new_bsr = sum(bsr_values[-3:]) / 3
bsr_change_pct = ((new_bsr - old_bsr) / old_bsr) * 100
if bsr_change_pct < -10:
bsr_trend = "improving" # BSR going down = demand going up
demand_trend = "increasing"
elif bsr_change_pct > 10:
bsr_trend = "declining"
demand_trend = "decreasing"
else:
bsr_trend = "stable"
demand_trend = "stable"
# Seasonal score for each month
months = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]
monthly_forecast = []
for i, mult in enumerate(pattern):
monthly_forecast.append({
"month": months[i],
"demand_index": round(mult * 100),
"is_peak": mult >= 1.2,
"is_low": mult <= 0.8,
})
peak_months = [months[i] for i, m in enumerate(pattern) if m >= 1.2]
low_months = [months[i] for i, m in enumerate(pattern) if m <= 0.8]
if current_multiplier >= 1.2:
season_status = "PEAK SEASON"
season_color = "#10B981"
season_advice = "Great time to sell β€” high demand period"
elif current_multiplier >= 1.0:
season_status = "NORMAL SEASON"
season_color = "#3B82F6"
season_advice = "Average demand β€” maintain stock levels"
elif current_multiplier >= 0.85:
season_status = "SLOW SEASON"
season_color = "#F59E0B"
season_advice = "Reduce inventory β€” lower demand period"
else:
season_status = "OFF SEASON"
season_color = "#EF4444"
season_advice = "Avoid heavy stock β€” very low demand"
demand_change_next = round((next_multiplier - current_multiplier) / current_multiplier * 100, 1)
return {
"current_season": season_status,
"season_color": season_color,
"season_advice": season_advice,
"current_demand_index": round(current_multiplier * 100),
"next_month_demand_index": round(next_multiplier * 100),
"demand_change_next_month": demand_change_next,
"demand_trend": demand_trend,
"bsr_trend": bsr_trend,
"bsr_change_pct": round(bsr_change_pct, 1),
"category": cat_key,
"peak_months": peak_months,
"low_months": low_months,
"monthly_forecast": monthly_forecast,
"recommendation": (
f"{season_status}: {season_advice}. "
f"Next month demand expected to {'increase' if demand_change_next > 5 else 'decrease' if demand_change_next < -5 else 'stay similar'} "
f"by {abs(demand_change_next):.0f}%."
)
}
# ═══════════════════════════════════════════════════════════════
# 4. NICHE SCORER
# ═══════════════════════════════════════════════════════════════
def score_niche(
keyword: str,
products: list, # list of {bsr, price, reviews, rating}
category: str = "general"
) -> dict:
"""
Score an entire niche/keyword based on multiple product data points.
Returns opportunity score 0-100 and detailed breakdown.
"""
if not products:
return {"error": "No products provided for niche analysis"}
prices = [p.get("price", 0) for p in products if p.get("price")]
bsrs = [p.get("bsr", 0) for p in products if p.get("bsr")]
reviews = [p.get("reviews", 0) for p in products if p.get("reviews") is not None]
ratings = [p.get("rating", 0) for p in products if p.get("rating")]
def avg(lst): return sum(lst) / len(lst) if lst else 0
def median(lst):
s = sorted(lst)
n = len(s)
return (s[n//2] + s[n//2-1]) / 2 if n % 2 == 0 else s[n//2]
avg_price = avg(prices)
avg_bsr = avg(bsrs)
avg_reviews = avg(reviews)
avg_rating = avg(ratings)
med_reviews = median(reviews) if reviews else 0
scores = {}
# 1. Demand Score (based on BSR)
if avg_bsr < 1000:
scores["demand"] = 95
elif avg_bsr < 5000:
scores["demand"] = 80
elif avg_bsr < 20000:
scores["demand"] = 65
elif avg_bsr < 100000:
scores["demand"] = 45
else:
scores["demand"] = 20
# 2. Competition Score (lower reviews = easier to compete)
if avg_reviews < 50:
scores["competition"] = 90
elif avg_reviews < 200:
scores["competition"] = 75
elif avg_reviews < 500:
scores["competition"] = 55
elif avg_reviews < 2000:
scores["competition"] = 35
else:
scores["competition"] = 15
# 3. Profitability Score (based on price)
if avg_price >= 25 and avg_price <= 70:
scores["profitability"] = 85
elif avg_price >= 15 and avg_price < 25:
scores["profitability"] = 65
elif avg_price >= 70 and avg_price <= 150:
scores["profitability"] = 70
elif avg_price > 150:
scores["profitability"] = 50
else:
scores["profitability"] = 35
# 4. Quality Gap Score (lower ratings = easier to beat with better product)
if avg_rating < 3.8:
scores["quality_gap"] = 90
elif avg_rating < 4.2:
scores["quality_gap"] = 70
elif avg_rating < 4.5:
scores["quality_gap"] = 50
else:
scores["quality_gap"] = 25
# 5. Market Size Score
n = len(products)
if n >= 10:
scores["market_size"] = 80
elif n >= 5:
scores["market_size"] = 60
else:
scores["market_size"] = 40
# Weighted total
total = (
scores["demand"] * 0.30 +
scores["competition"] * 0.30 +
scores["profitability"]* 0.20 +
scores["quality_gap"] * 0.15 +
scores["market_size"] * 0.05
)
total = round(total)
if total >= 70:
verdict = "Excellent Niche"
verdict_color = "#10B981"
recommendation = "Strong opportunity β€” low competition, good demand, profitable price range"
elif total >= 55:
verdict = "Good Niche"
verdict_color = "#3B82F6"
recommendation = "Decent opportunity β€” worth pursuing with right differentiation"
elif total >= 40:
verdict = "Moderate Niche"
verdict_color = "#F59E0B"
recommendation = "Average opportunity β€” possible but competitive, needs strong USP"
else:
verdict = "Tough Niche"
verdict_color = "#EF4444"
recommendation = "Difficult market β€” high competition or low margins"
# Review gaps β€” products with < 100 reviews in a market with demand
low_review_products = [p for p in products if (p.get("reviews") or 0) < 100 and (p.get("bsr") or 999999) < 50000]
return {
"keyword": keyword,
"niche_score": total,
"verdict": verdict,
"verdict_color": verdict_color,
"recommendation": recommendation,
"scores": {
"demand": {"score": scores["demand"], "label": "Market Demand", "weight": "30%"},
"competition": {"score": scores["competition"], "label": "Competition Level", "weight": "30%"},
"profitability": {"score": scores["profitability"], "label": "Profitability", "weight": "20%"},
"quality_gap": {"score": scores["quality_gap"], "label": "Quality Gap", "weight": "15%"},
"market_size": {"score": scores["market_size"], "label": "Market Size", "weight": "5%"},
},
"market_stats": {
"products_analyzed": len(products),
"avg_price": round(avg_price, 2),
"avg_bsr": round(avg_bsr),
"avg_reviews": round(avg_reviews),
"avg_rating": round(avg_rating, 1),
"median_reviews": round(med_reviews),
},
"opportunities": {
"low_review_opportunities": len(low_review_products),
"easy_entry_products": [p.get("asin", "") for p in low_review_products[:3]],
"price_gap": round(max(prices) - min(prices), 2) if prices else 0,
}
}