How to Monitor Food Delivery App Ratings and Reviews Across SEA

How to Monitor Food Delivery App Ratings and Reviews Across SEA

Customer ratings and reviews on food delivery platforms are among the most influential factors in restaurant visibility and ordering decisions. In Southeast Asia, where GrabFood, Foodpanda, ShopeeFood, and GoFood collectively serve hundreds of millions of users, review data represents a goldmine of consumer sentiment, competitive intelligence, and operational insight.

This guide covers how to systematically monitor and analyze restaurant ratings and reviews across major food delivery platforms in the region.

The Value of Review Intelligence

Why Reviews Matter More in Food Delivery

Unlike traditional dining where ambiance, service, and word-of-mouth drive decisions, food delivery customers rely almost entirely on digital signals:

  • Platform algorithms use ratings to determine search rankings and visibility
  • Customers filter by rating, often ignoring restaurants below 4.0 stars
  • Review content influences ordering choices for specific menu items
  • Response patterns signal restaurant quality and customer care

Business Applications

ApplicationDescriptionWho Benefits
Reputation managementTrack your own ratings in real-timeRestaurant operators
Competitive benchmarkingCompare ratings against competitorsF&B brands
Menu optimizationIdentify items praised or criticizedKitchen managers
Quality assuranceDetect service issues earlyOperations teams
Market researchUnderstand consumer preferencesInvestors and analysts
Franchise monitoringTrack consistency across locationsMulti-unit operators

Understanding Review Systems Across Platforms

GrabFood Reviews

GrabFood uses a 5-star rating system with text reviews. Key features:

  • Reviews are tied to verified orders
  • Merchants can respond to reviews
  • Rating breakdown shows distribution across star levels
  • Platform displays overall rating prominently

Foodpanda Reviews

Foodpanda also uses 5-star ratings with optional text:

  • Includes specific feedback categories (food quality, delivery, packaging)
  • Shows “positive feedback” percentage
  • Recent reviews displayed on restaurant pages
  • Aggregate scores updated in near real-time

GoFood Reviews

GoFood’s review system within the Gojek ecosystem:

  • 5-star ratings with tags (taste, portion, packaging)
  • Photo reviews from customers
  • Merchant response capability
  • Ratings influence GoFood recommendation algorithm

ShopeeFood Reviews

ShopeeFood leverages Shopee’s review infrastructure:

  • Star ratings with text and photo reviews
  • Helpful votes on reviews
  • Integration with Shopee user profiles
  • Review analytics in merchant dashboard

Setting Up Review Monitoring

Core Monitoring Infrastructure

import requests
import time
import random
from datetime import datetime, timedelta
from dataclasses import dataclass, field
from typing import List, Optional

@dataclass
class Review:
    platform: str
    restaurant_id: str
    restaurant_name: str
    reviewer_name: str
    rating: float
    text: str
    date: datetime
    order_items: List[str] = field(default_factory=list)
    has_photo: bool = False
    merchant_reply: Optional[str] = None
    tags: List[str] = field(default_factory=list)
    helpful_count: int = 0

class ReviewMonitor:
    def __init__(self, proxy_user, proxy_pass):
        self.proxy_user = proxy_user
        self.proxy_pass = proxy_pass

    def _get_session(self, country):
        session = requests.Session()
        proxy_host = f"{country.lower()}-mobile.dataresearchtools.com"
        session.proxies = {
            "http": f"http://{self.proxy_user}:{self.proxy_pass}@{proxy_host}:8080",
            "https": f"http://{self.proxy_user}:{self.proxy_pass}@{proxy_host}:8080"
        }
        session.headers.update({
            "User-Agent": "Mozilla/5.0 (Linux; Android 14) AppleWebKit/537.36",
            "Accept": "application/json"
        })
        return session

Scraping GrabFood Reviews

def scrape_grabfood_reviews(self, restaurant_id, country="SG"):
    """Scrape reviews from GrabFood."""
    session = self._get_session(country)
    reviews = []
    offset = 0

    while True:
        response = session.get(
            f"https://food.grab.com/api/v1/restaurants/{restaurant_id}/reviews",
            params={"offset": offset, "limit": 20, "sort": "recent"}
        )

        if response.status_code != 200:
            break

        data = response.json()
        review_list = data.get("reviews", [])

        if not review_list:
            break

        for r in review_list:
            reviews.append(Review(
                platform="grabfood",
                restaurant_id=restaurant_id,
                restaurant_name=data.get("restaurant_name", ""),
                reviewer_name=r.get("reviewer", {}).get("name", "Anonymous"),
                rating=r.get("rating", 0),
                text=r.get("comment", ""),
                date=datetime.fromisoformat(r.get("created_at", "")),
                order_items=[item.get("name") for item in r.get("order_items", [])],
                has_photo=bool(r.get("photos")),
                merchant_reply=r.get("merchant_reply", {}).get("text"),
                tags=r.get("tags", [])
            ))

        offset += 20
        time.sleep(random.uniform(2, 4))

    return reviews

Scraping Foodpanda Reviews

def scrape_foodpanda_reviews(self, vendor_code, country="SG"):
    """Scrape reviews from Foodpanda."""
    session = self._get_session(country)
    domain_map = {
        "SG": "www.foodpanda.sg",
        "MY": "www.foodpanda.my",
        "TH": "www.foodpanda.co.th",
        "PH": "www.foodpanda.ph"
    }
    domain = domain_map.get(country, domain_map["SG"])
    reviews = []
    offset = 0

    while True:
        response = session.get(
            f"https://{domain}/api/v5/vendors/{vendor_code}/reviews",
            params={"offset": offset, "limit": 20}
        )

        if response.status_code != 200:
            break

        data = response.json()
        review_list = data.get("data", {}).get("reviews", [])

        if not review_list:
            break

        for r in review_list:
            reviews.append(Review(
                platform="foodpanda",
                restaurant_id=vendor_code,
                restaurant_name="",
                reviewer_name=r.get("customer_name", "Anonymous"),
                rating=r.get("rating", 0),
                text=r.get("comment", ""),
                date=datetime.fromisoformat(r.get("created_at", "")),
                has_photo=bool(r.get("images")),
                merchant_reply=r.get("reply", {}).get("text") if r.get("reply") else None
            ))

        offset += 20
        time.sleep(random.uniform(2, 4))

    return reviews

Review Analysis Techniques

Sentiment Analysis

Extract sentiment from review text to understand customer feelings beyond star ratings:

from collections import Counter
import re

# SEA-specific food delivery sentiment keywords
POSITIVE_KEYWORDS = [
    "delicious", "fresh", "fast", "hot", "generous", "worth", "recommend",
    "amazing", "love", "best", "perfect", "excellent", "great", "good",
    "tasty", "nice", "awesome", "fantastic", "wonderful", "satisfied",
    "sedap", "enak", "อร่อย", "masarap"  # Malay, Indonesian, Thai, Filipino
]

NEGATIVE_KEYWORDS = [
    "cold", "late", "wrong", "missing", "soggy", "small", "expensive",
    "terrible", "worst", "disappointing", "horrible", "awful", "bad",
    "stale", "undercooked", "overcooked", "spilled", "rude", "slow",
    "waited", "never again", "refund"
]

def analyze_sentiment(review_text):
    """Simple keyword-based sentiment analysis."""
    text_lower = review_text.lower()
    words = re.findall(r'\w+', text_lower)

    positive_hits = [w for w in words if w in POSITIVE_KEYWORDS]
    negative_hits = [w for w in words if w in NEGATIVE_KEYWORDS]

    total_hits = len(positive_hits) + len(negative_hits)
    if total_hits == 0:
        return {"sentiment": "neutral", "score": 0, "keywords": []}

    score = (len(positive_hits) - len(negative_hits)) / total_hits

    return {
        "sentiment": "positive" if score > 0.2 else "negative" if score < -0.2 else "neutral",
        "score": round(score, 3),
        "positive_keywords": positive_hits,
        "negative_keywords": negative_hits
    }

Topic Extraction

Identify the most discussed topics in reviews:

def extract_review_topics(reviews):
    """Extract common topics from review text."""
    topic_patterns = {
        "food_quality": r'\b(taste|flavor|fresh|stale|quality|delicious|bland)\b',
        "portion_size": r'\b(portion|size|amount|generous|small|large|big|quantity)\b',
        "delivery_speed": r'\b(fast|slow|quick|late|early|time|wait|delivery|minutes)\b',
        "packaging": r'\b(pack|packaging|container|spill|leak|wrapped|sealed)\b',
        "value": r'\b(price|value|worth|expensive|cheap|affordable|overpriced)\b',
        "temperature": r'\b(hot|cold|warm|lukewarm|room temperature|heated)\b',
        "accuracy": r'\b(wrong|missing|correct|accurate|order|mistake|forgot)\b',
        "hygiene": r'\b(clean|dirty|hygiene|hair|foreign|object|contaminated)\b'
    }

    topic_counts = Counter()
    topic_sentiments = {}

    for review in reviews:
        text = review.text.lower()
        for topic, pattern in topic_patterns.items():
            if re.search(pattern, text):
                topic_counts[topic] += 1
                if topic not in topic_sentiments:
                    topic_sentiments[topic] = []
                topic_sentiments[topic].append(review.rating)

    results = {}
    for topic, count in topic_counts.most_common():
        ratings = topic_sentiments[topic]
        results[topic] = {
            "mention_count": count,
            "mention_rate": f"{count / len(reviews) * 100:.1f}%",
            "avg_rating_when_mentioned": round(sum(ratings) / len(ratings), 2),
            "sentiment_impact": round(
                sum(ratings) / len(ratings) - sum(r.rating for r in reviews) / len(reviews), 2
            )
        }

    return results

Competitive Review Benchmarking

def benchmark_reviews(target_reviews, competitor_reviews_dict):
    """Benchmark your reviews against competitors."""
    target_avg = sum(r.rating for r in target_reviews) / len(target_reviews)
    target_sentiment = [analyze_sentiment(r.text) for r in target_reviews]
    target_positive_rate = len([s for s in target_sentiment if s["sentiment"] == "positive"]) / len(target_sentiment)

    benchmarks = {
        "your_restaurant": {
            "avg_rating": round(target_avg, 2),
            "total_reviews": len(target_reviews),
            "positive_sentiment_rate": f"{target_positive_rate:.1%}",
            "response_rate": f"{len([r for r in target_reviews if r.merchant_reply]) / len(target_reviews):.1%}",
            "photo_review_rate": f"{len([r for r in target_reviews if r.has_photo]) / len(target_reviews):.1%}"
        },
        "competitors": {}
    }

    all_competitor_ratings = []
    for comp_name, comp_reviews in competitor_reviews_dict.items():
        comp_avg = sum(r.rating for r in comp_reviews) / len(comp_reviews)
        all_competitor_ratings.append(comp_avg)

        comp_sentiment = [analyze_sentiment(r.text) for r in comp_reviews]
        comp_positive = len([s for s in comp_sentiment if s["sentiment"] == "positive"]) / len(comp_sentiment)

        benchmarks["competitors"][comp_name] = {
            "avg_rating": round(comp_avg, 2),
            "total_reviews": len(comp_reviews),
            "positive_sentiment_rate": f"{comp_positive:.1%}",
            "response_rate": f"{len([r for r in comp_reviews if r.merchant_reply]) / len(comp_reviews):.1%}"
        }

    market_avg = sum(all_competitor_ratings) / len(all_competitor_ratings)
    benchmarks["market_comparison"] = {
        "market_avg_rating": round(market_avg, 2),
        "your_vs_market": f"{'+' if target_avg > market_avg else ''}{round(target_avg - market_avg, 2)}",
        "ranking": sorted(all_competitor_ratings + [target_avg], reverse=True).index(target_avg) + 1,
        "total_ranked": len(all_competitor_ratings) + 1
    }

    return benchmarks

Monitoring and Alerting

Real-Time Review Alerts

Set up alerts for critical review events:

class ReviewAlertSystem:
    def __init__(self):
        self.alert_rules = []

    def add_alert_rule(self, rule_type, threshold, notification_method="email"):
        self.alert_rules.append({
            "type": rule_type,
            "threshold": threshold,
            "method": notification_method
        })

    def process_new_reviews(self, new_reviews):
        """Check new reviews against alert rules."""
        alerts = []

        for review in new_reviews:
            # Check for low ratings
            if review.rating <= 2:
                alerts.append({
                    "severity": "high",
                    "type": "low_rating",
                    "message": f"New {review.rating}-star review on {review.platform}: "
                              f"'{review.text[:100]}...'",
                    "restaurant": review.restaurant_name,
                    "action_needed": "Respond within 2 hours"
                })

            # Check for specific complaint keywords
            sentiment = analyze_sentiment(review.text)
            if "missing" in sentiment.get("negative_keywords", []):
                alerts.append({
                    "severity": "high",
                    "type": "missing_items",
                    "message": f"Missing items complaint on {review.platform}",
                    "restaurant": review.restaurant_name,
                    "action_needed": "Investigate order fulfillment"
                })

            if "hygiene" in review.text.lower() or "hair" in review.text.lower():
                alerts.append({
                    "severity": "critical",
                    "type": "hygiene_complaint",
                    "message": f"Hygiene complaint detected on {review.platform}",
                    "restaurant": review.restaurant_name,
                    "action_needed": "Immediate investigation required"
                })

        return alerts

Rating Trend Monitoring

def track_rating_trend(review_history, window_days=30):
    """Track rating trends over time."""
    now = datetime.utcnow()
    windows = {
        "last_7_days": now - timedelta(days=7),
        "last_30_days": now - timedelta(days=30),
        "last_90_days": now - timedelta(days=90),
        "all_time": datetime.min
    }

    trends = {}
    for period, start_date in windows.items():
        period_reviews = [r for r in review_history if r.date >= start_date]
        if period_reviews:
            trends[period] = {
                "avg_rating": round(sum(r.rating for r in period_reviews) / len(period_reviews), 2),
                "review_count": len(period_reviews),
                "five_star_pct": f"{len([r for r in period_reviews if r.rating == 5]) / len(period_reviews):.1%}",
                "one_star_pct": f"{len([r for r in period_reviews if r.rating == 1]) / len(period_reviews):.1%}"
            }

    # Calculate trend direction
    if "last_7_days" in trends and "last_30_days" in trends:
        recent = trends["last_7_days"]["avg_rating"]
        baseline = trends["last_30_days"]["avg_rating"]
        trends["trend_direction"] = "improving" if recent > baseline + 0.1 else \
                                   "declining" if recent < baseline - 0.1 else "stable"
        trends["trend_magnitude"] = round(recent - baseline, 2)

    return trends

Multi-Language Review Analysis

Southeast Asian reviews come in multiple languages. Handle this diversity:

def detect_review_language(text):
    """Simple language detection for SEA reviews."""
    # Character-based detection
    thai_chars = len(re.findall(r'[\u0E00-\u0E7F]', text))
    if thai_chars > len(text) * 0.3:
        return "th"

    malay_indicators = ["sedap", "enak", "makanan", "penghantaran", "bagus", "teruk"]
    if any(word in text.lower() for word in malay_indicators):
        return "ms"

    indonesian_indicators = ["enak", "lezat", "pengiriman", "mantap", "lumayan"]
    if any(word in text.lower() for word in indonesian_indicators):
        return "id"

    filipino_indicators = ["masarap", "sarap", "delivery", "maganda", "pangit"]
    if any(word in text.lower() for word in filipino_indicators):
        return "tl"

    return "en"

def multilingual_sentiment(review_text, language=None):
    """Analyze sentiment across SEA languages."""
    if language is None:
        language = detect_review_language(review_text)

    sentiment_lexicons = {
        "en": {"positive": POSITIVE_KEYWORDS, "negative": NEGATIVE_KEYWORDS},
        "ms": {
            "positive": ["sedap", "bagus", "terbaik", "cepat", "segar", "puas"],
            "negative": ["teruk", "lambat", "mahal", "sejuk", "kurang", "kecewa"]
        },
        "id": {
            "positive": ["enak", "mantap", "lezat", "cepat", "segar", "puas", "recommended"],
            "negative": ["jelek", "lambat", "mahal", "dingin", "kecewa", "mengecewakan"]
        },
        "th": {
            "positive": ["อร่อย", "ดี", "เร็ว", "สด", "คุ้ม", "ชอบ"],
            "negative": ["แย่", "ช้า", "แพง", "เย็น", "ผิด", "ผิดหวัง"]
        }
    }

    lexicon = sentiment_lexicons.get(language, sentiment_lexicons["en"])
    text_lower = review_text.lower()

    positive_count = sum(1 for word in lexicon["positive"] if word in text_lower)
    negative_count = sum(1 for word in lexicon["negative"] if word in text_lower)

    total = positive_count + negative_count
    if total == 0:
        return {"sentiment": "neutral", "score": 0, "language": language}

    score = (positive_count - negative_count) / total
    return {
        "sentiment": "positive" if score > 0.2 else "negative" if score < -0.2 else "neutral",
        "score": round(score, 3),
        "language": language
    }

Proxy Considerations for Review Scraping

Review scraping has specific proxy requirements:

  1. Pagination depth: Reviews require many sequential requests per restaurant, demanding stable proxy sessions
  2. Multi-platform coverage: You need proxies for each country-platform combination
  3. Rate sensitivity: Review endpoints are often more heavily rate-limited than menu endpoints
  4. Content completeness: Missing reviews due to blocked requests creates biased analysis

DataResearchTools mobile proxies are well-suited for review monitoring because they provide sticky sessions for deep pagination, country-specific targeting for accurate content access, and the mobile carrier trust level needed to avoid detection during sustained scraping operations.

Conclusion

Monitoring food delivery app ratings and reviews across Southeast Asia provides actionable intelligence for restaurants, F&B brands, and market researchers. By building systematic review collection and analysis pipelines with proper proxy infrastructure, businesses can track reputation trends, benchmark against competitors, and respond quickly to customer feedback.

Start by monitoring your own restaurant across all platforms, then expand to track key competitors. Over time, the accumulated review data becomes a strategic asset for understanding consumer preferences across Southeast Asia’s diverse food delivery markets.


Related Reading

Scroll to Top