Your “lowest-rated competitor products” list can include products with excellent customer ratings.
Picture opening that slide in front of product leadership. You selected sortBy="RATING_ASC", so the heading felt defensible. Now you're explaining why the stars disagree with it.
RATING_ASC follows Bol.com's rating score, not the visible average customer rating. With a collection cap, that difference affects which products you receive, before your analysis starts.
What does ascending rating actually select?
The tempting question is straightforward: which competing laptops attract poor customer ratings, and what complaints recur?
The input answers a narrower question: which products can the Actor retrieve through a laptop search under the platform's score ordering, subject to the collection limits?
Those populations needn't match.
This Example input makes the selection choices explicit, retrieves the dataset through the streaming interface, and checks the run status.
import os
import re
import duckdb
import pandas as pd
from apify_client import ApifyClient
run_input = {
"mode": "search",
"searchQueries": ["laptop"],
"marketplace": "nl",
"language": "nl",
"sortBy": "RATING_ASC",
"condition": "any",
"inStockOnly": False,
"includeSponsored": True,
"includeDetails": True,
"maxItems": 100,
"maxPages": 0,
}
client = ApifyClient(os.environ["APIFY_TOKEN"])
run = client.actor(
"dataspryva/bol-com-product-search-price-and-stock-extractor"
).call(run_input=run_input)
if run["status"] != "SUCCEEDED":
raise RuntimeError(f"Collection ended with status {run['status']}")
items = []
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
items.append(item)
The score formula isn't exposed here. We can't attribute the ordering to review-volume weighting, a confidence interval or any particular adjustment.
That uncertainty matters. “An internal score” doesn't establish that popular products systematically rise or fall. It establishes that sorting the exported rating column and selecting products upstream are different operations.
The cap turns an ordering mismatch into missing products
If you already had the complete target population, you could sort its visible ratings locally.
A capped collection makes that repair incomplete: local sorting can reorder retrieved products, but it can't recover products excluded during collection.
The selection path looks like this:
Keyword and shop settings
|
Platform ordering and reachable listings
|
Collection limits
|
Retrieved products
|
Local rating filter
Every filter at the bottom operates on the survivors above it. A tidy SQL query can't negotiate with a product it never received.
Search reach also matters independently of the rating sort. A keyword result list stops at 10 pages; for larger collections, the Actor automatically splits broad searches into narrower category slices.
Those slices extend reach. They don't establish complete category coverage or known inclusion probabilities.
The overall maxItems cap applies across the run, while maxPages limits each query or listing. In the input above, the latter delegates pagination to the source's limit. Neither setting certifies that the resulting file contains every eligible laptop.
Keep rating eligibility separate from customer sentiment
Before ranking anything, separate an unavailable rating from a poor rating.
Empty fields are omitted. A missing reviewCount is therefore unknown, and a missing rating mustn't become the bottom of your scale through a convenient fillna.
These fields carry different analytical weight:
| Field or setting | Role in this analysis | Boundary |
|---|---|---|
rating |
Visible-rating screening | Doesn't reproduce the upstream score |
reviewCount |
Review-volume eligibility | Doesn't identify causes of dissatisfaction |
includeDetails |
Requests richer product evidence | Enrichment doesn't broaden the selected population |
ratingDistribution |
Shows star counts when available | Doesn't explain the complaints |
recentReviews |
Provides text for qualitative inspection | Isn't the full review history |
This cell handles omitted keys, accepts decimal commas in ratings, and parses space-grouped review counts. Other punctuated count strings remain unknown rather than receiving a guessed interpretation.
def parse_measure(value, *, count=False):
if value is None or isinstance(value, bool):
return None
if isinstance(value, str):
text = re.sub(r"\s+", "", value)
pattern = r"\d+" if count else r"\d+(?:[.,]\d+)?"
if not re.fullmatch(pattern, text):
return None
value = text.replace(",", ".")
try:
number = float(value)
except (TypeError, ValueError, OverflowError):
return None
if count:
return number if number >= 0 and number.is_integer() else None
return number if 0 <= number <= 5 else None
frame = pd.DataFrame.from_records(
items, columns=["productId", "rating", "reviewCount"]
)
frame["productId"] = (
frame["productId"].astype("string").str.strip().replace("", pd.NA)
)
frame["rating"] = frame["rating"].map(parse_measure).astype("Float64")
frame["reviewCount"] = frame["reviewCount"].map(
lambda value: parse_measure(value, count=True)
).astype("Float64")
Out-of-range ratings become unusable values. Keep the original items available for inspection: a parser rejection and an omitted field both prevent a rating-based decision, but they have different causes.
Audit the denominator before naming the shortlist
A useful check asks how much of the retrieved data can support your proposed screen.
It doesn't try to reverse-engineer the platform score from export order. The Actor doesn't provide a source-rank field, so dataset row position isn't evidence of storefront position.
The following cell audits rating availability, flags repeated product identifiers, and builds a shortlist using explicit rating and review-volume thresholds.
connection = duckdb.connect()
connection.register("retrieved", frame)
rating_ceiling = 2.5
review_floor = 10
audit = connection.execute("""
SELECT
COUNT(*) AS retrieved_rows,
COUNT(*) FILTER (
WHERE productId IS NULL
) AS rows_without_product_id,
COUNT(productId) - COUNT(DISTINCT productId)
AS repeated_identified_rows,
COUNT(*) FILTER (
WHERE rating IS NULL
) AS rows_without_usable_rating,
COUNT(*) FILTER (
WHERE reviewCount IS NULL
) AS rows_without_usable_review_count,
COUNT(*) FILTER (
WHERE rating IS NOT NULL
AND reviewCount >= ?
) AS review_eligible_rows,
COUNT(*) FILTER (
WHERE rating <= ?
AND reviewCount >= ?
) AS low_rated_eligible_rows
FROM retrieved
""", [review_floor, rating_ceiling, review_floor]).df()
shortlist = connection.execute("""
SELECT productId, rating, reviewCount
FROM retrieved
WHERE productId IS NOT NULL
AND rating <= ?
AND reviewCount >= ?
ORDER BY rating ASC, reviewCount DESC
""", [rating_ceiling, review_floor]).df()
connection.close()
audit gives you the denominator checks; shortlist gives you candidates to investigate. Neither asserts that these are the category's worst-rated products.
The thresholds are triage choices, not a statistical significance test. Review volume helps distinguish sparse evidence from more extensively reviewed products, but it doesn't make reviewers representative of buyers.
Don't publish a product-level percentage while repeated identifiers remain unresolved. When combining collections, check repeated products for conflicting values before deduplicating; silently retaining whichever row arrived first can conceal a changing snapshot.
Build the category frame before screening ratings
For this question, we'd start from a category definition rather than asking a rating-related ordering to discover the population.
This Example input uses the laptop category identifier and a larger collection budget. It's an alternative collection configuration, not evidence that the category fits inside the budget.
category_run_input = {
"mode": "categoryBrowse",
"categoryUrls": ["4770"],
"marketplace": "nl",
"language": "nl",
"sortBy": "RELEVANCE",
"condition": "any",
"inStockOnly": False,
"includeSponsored": True,
"includeDetails": True,
"maxItems": 10000,
"maxPages": 0,
}
The worked example uses Bol.com Product Search, Price & Stock Extractor.
Changing the ordering alone doesn't remove selection bias. The improvement comes from defining the intended category, collecting broadly enough to assess reach, and applying the visible-rating screen afterwards.
If collection reaches its item cap, treat coverage as unresolved. Reaching fewer items also doesn't prove completeness: the accessible listing may have ended before the commercial population did.
Keep stock, condition and advertising choices aligned with your question. Excluding unavailable products could remove relevant complaint evidence; retaining every condition could mix product experiences you intended to analyse separately.
Those are hypotheses to check against your category, not reasons to prefer a particular setting automatically.
Also resist adding minRating to a search for poorly rated products. It's a lower bound, so it removes products below the supplied value.
Give the percentage its actual population
For an exploratory report, the defensible metric is the share of eligible retrieved products meeting your low-rating rule.
Its denominator excludes products without usable rating evidence and those below your chosen review-volume floor. Those exclusions belong beside the metric, because changing them changes the population being described.
Record the category or queries, shop settings, collection limits, collection timestamps, eligibility thresholds and unresolved coverage gaps.
A useful label is “Low-rated share among review-eligible products retrieved from the selected category.” “Competitor defect rate” asks the dataset to impersonate a different study.
If your manager wants a category-wide estimate, the missing ingredient is a defensible population frame or sampling design. A larger export can improve coverage without supplying either.
Complaints still aren't manufacturing defects
With details enabled, recentReviews contains up to six recent reviews. That's a short, recency-selected text sample, even when the rating summary reflects a longer review history.
Use it to propose complaint categories for inspection. Don't turn the frequency of a phrase in that sample into its prevalence among customers.
A low rating can concern delivery, a seller, expectations or the product itself. Establishing a manufacturing defect requires evidence that separates those explanations.
The uncomfortable question belongs before the slide: are you measuring dissatisfaction in a defined population, or merely describing the products your inputs allowed you to see?
Top comments (0)