TikTok Shop 影片增長率欄位異常:123 條記錄全低於 −100%,排序後卻出現 +220,000%
123 TikTok Shop Videos, 123 Growth Figures Below 100%. Then the Same Column Returned +220,000%
作者測試 TikTok Shop Videos Scraper,發現影片清單的 revenueGrowthPercent 在 123 條記錄中全部低於 −100%。按增長率排序時,結果卻出現 +220,000%,作者指出低額前期基數會造成失真的百分比。文章建議以兩次有日期的抓取自行計算增長率,並在繪圖前檢查指標範圍。
I run a visa agency in Bali. The other half of my week goes into Apify Actors, and one of them reads TikTok Shop's shoppable videos: which clips are actually selling, with revenue, views, ad ROAS and TikTok's AI-generated flag, in 15 markets.
The output has a column called revenueGrowthPercent. It is the field a client asks for first — show me what's taking off — and it is the one field in that row I now refuse to put on a chart.
123 rows, not one of them above −117%
Buyers hit this Actor on a schedule, so I had a clean pile of real output to diff. Between 20 and 22 October 2026, seven runs fired with the same input:
{"country": "US", "period": "last30Day", "sortBy": "revenue",
"maxResultsPerSource": 20, "addAnalytics": false, "aiVideos": "all"}
Top shoppable videos in the US by revenue over the last 30 days. 123 video rows, 23 distinct videos (the US top 20 barely moves day to day — that's a separate article).
| Column | Across 123 rows |
|---|---|
revenue |
$174,133 – $499,291, median $202,464 |
views |
median 10,220,203 |
adsRoas |
2.31 – 4.42 |
isAd |
true on 123 of 123 |
revenueGrowthPercent |
−230.99 to −117.53, median −197.31 |
Read that last row again. Not one value was positive. Not one value was above −100%. A revenue decline of −117% means a video earned less than nothing, and −231% means it did that twice over. Whatever this column is, it is not "revenue fell by X%".
The values cluster hard, too: 109 of the 123 sit within 15 points of −200%. That is the shape of an arithmetic artefact, not of a market.
The sibling endpoints get it right
My Actor does not compute this number. It reads the upstream field revenue_growth_rate and rounds it to two decimals — one line of JavaScript, no arithmetic of my own:
revenueGrowthPercent: round2(num(r.revenue_growth_rate)),
The same line appears in three of my TikTok Shop Actors, pointed at three different list endpoints of the same provider. So I pulled six recent runs from each of the other two and ran the identical check:
| Endpoint | Rows | Range | Positive | Below −100% |
|---|---|---|---|---|
| Products | 120 | −70.31 … +77,603.46 | 100 | 0 |
| Creators | 120 | −31.93 … +407.80 | 96 | 0 |
| Videos | 123 | −230.99 … −117.53 | 0 | 123 |
Products and creators behave exactly as a growth rate should: mostly positive, occasionally down, never below −100% because that floor is a mathematical one. The video endpoint clears that floor on every single row.
That ruled out my own code, my rounding, and the provider as a whole. It is this one list.
Sort by the column and it flips sign entirely
If a field is simply broken, sorting by it is meaningless. sortBy: "revenueGrowth" is a documented option, so I spent $0.03 finding out. Same country, same period, ten rows:
| Rank | revenueGrowthPercent |
revenue |
views |
|---|---|---|---|
| 1 | +220,000 | $1,101 | 13,075 |
| 2 | +100,000 | $501 | 16,801 |
| 3 | +60,000 | $10,234 | 1,117,571 |
| 4 | +46,666.67 | $1,406 | 50,288 |
| 5 | +36,000 | $3,620 | 287,936 |
| 10 | +20,888.89 | $142.35 | 5,548 |
So the column is not stuck, not clamped, and not always negative. It just never produced a sane number in either direction. Those suspiciously round figures — 220000, 100000, 60000, 36000, 30000 twice — are the tell: they come from dividing this period's revenue by a previous-period baseline of a few dollars. Rank 10 earned $142.35 and the implied baseline is 68 cents.
Median revenue in that growth-sorted top ten: $852.50, against $202,464 in the revenue-sorted list. Median views 18,441 against 10.2 million. Zero of the ten appear anywhere in the 23 revenue leaders. Sorting by growth doesn't surface tomorrow's winners, it surfaces whoever sold nothing last month and three units this month.
And it isn't a proxy for "new", either: the +220,000% row at the top was published on 26 November 2025, eleven months before the run.
The Actor's own README suggests pairing a growth sort with a revenue floor, so I tested that too — minRevenue: 10000:
revenueGrowthPercent |
revenue |
adsRoas |
|---|---|---|
| +60,000 | $10,234 | 2.32 |
| +20,000 | $18,786 | 2.59 |
| +20,000 | $12,524 | 1.89 |
| +4,032.26 | $10,496 | 1.86 |
Better — these are real videos moving real money, median revenue $12,905, all ten running as ads. The ranking is now usable. The numbers are still fiction: three different revenue figures sharing an identical +20,000% is bucketing, not measurement. Use the list, drop the column.
What to compute instead
Two dated runs and your own subtraction. The revenue figure itself is solid — it's the derived field that isn't.
import requests, json, datetime as dt
TOKEN = "<APIFY_TOKEN>"
URL = ("https://api.apify.com/v2/acts/"
"lergassy~tiktok-shop-videos-scraper/run-sync-get-dataset-items")
def pull(period):
rows = requests.post(URL, params={"token": TOKEN}, json={
"country": "US", "period": period, "sortBy": "revenue",
"maxResultsPerSource": 100, "addAnalytics": False,
}).json()
return {r["videoId"]: r for r in rows if r.get("type") == "video"}
last7 = pull("last7Day")
last30 = pull("last30Day")
for vid, now in last7.items():
then = last30.get(vid)
if not then or now["revenue"] <= 0:
continue
prior23 = then["revenue"] - now["revenue"] # days 8–30
if prior23 <= 0:
continue
weekly_prior = prior23 / 23 * 7
lift = (now["revenue"] - weekly_prior) / weekly_prior * 100
print(f'{now["creatorUsername"]:<24} {lift:+8.1f}% ${now["revenue"]:,.0f}')
Three things worth copying out of that snippet. type == "video" — the last row of every run is a free notice row carrying the run's own accounting (Run summary: 10 row(s) delivered, 0 free; charged video ×10 = $0.03), and left in a dataframe it becomes a video with no revenue. videoId as the join key — the URL embeds the creator handle and handles get renamed. And guard the denominator yourself, because a near-zero baseline is exactly what produced +220,000% upstream; decide whether you want those rows or a floor, and make that decision in code where you can see it.
The Actor is TikTok Shop Videos Scraper — shoppable videos with revenue, views, ad ROAS and the AI flag, by keyword, category, product link, creator or shop, at $0.003 per row.
Two more fields to read carefully
adRevenueSharePercent stops at 80. Across the 123 rows the maximum was exactly 80, and 77 of 123 sat at 79.9 or above. I don't know whether that's a real attribution cap or a display ceiling, so I pass it through and treat anything ≥ 79.9 as "mostly paid" rather than as a measurement.
publishedAt routinely predates the window. 15 of the 23 videos in a last 30 days revenue list were published before that window opened; the oldest was from 10 January 2026. The period filters the revenue, not the upload date — so "newest sellers" is a publishedAt filter you apply, not a period you request.
When this is the wrong tool
- You want a growth metric you can trust out of the box. You want two dated pulls and your own delta.
-
You want organic creative research. All 123 top-revenue rows were
isAd: true. The top of TikTok Shop by revenue is a paid-media leaderboard; if you need organic winners, sort by views and filterisAdyourself. - You want your own shop's numbers. This is public marketplace data. TikTok's Seller Center is the system of record for your own GMV, and it will disagree with this.
-
You want unit economics per row.
unitsSold,gpmandadCpaarrive only with the analytics option, which costs an order of magnitude more per row because each one is a separate paid data call.
If you keep one thing: before a scraped field reaches a chart, check its range against the bound the metric cannot cross. Percent declines stop at −100. Shares stop at 100. Conversion rates stop at 1. My 123 rows were all plausible-looking three-digit numbers with a minus sign, and they would have sat in a dashboard for months looking like a collapsing market.
來源:DEV Community · dev.to