PeekPanda docs
How estimates are computed
The full derivation of the install and revenue models, every constant, every calibration anchor, and the exact conditions under which each returns nothing.
Download Markdown ↓PeekPanda publishes two estimates for an iOS app: monthly installs and monthly revenue. Neither is a measured number.
Apple does not publish download counts. Apple does not publish revenue. No public store surface exposes either. So both numbers are models built on top of things that are public, and this page shows you the whole model so you can decide how much of it you believe.
An estimate here is a modelled number derived from public observations. It is never a measured download count and never reported revenue. Every estimate is labelled as an estimate wherever it appears, and it carries the name of the method that produced it.
Two methods do all the work:
| Method | Produces | Built on |
|---|---|---|
ratings_delta_proxy | Monthly installs, lifetime installs | Change in the app's public lifetime rating count over time |
grossing_rank_power_law | Monthly revenue | The app's position on a public grossing chart |
Both live in packages/libraries/src/domain/app-catalog/store-rating-tape.contract.ts. Both names travel with the number in the API response, in the fields installMethod and revenueMethod, so a consumer can never receive an estimate without also receiving the name of how it was made.
Monthly installs: ratings_delta_proxy
The idea
Apple publishes one number that moves with usage: the lifetime rating count on the store listing. PeekPanda records that number once per app per day. The sequence of those daily records is called the app's rating tape.
If an app gained 300 ratings in 10 days, it gained 30 a day. Multiply by 30 for a month. Then multiply by the number of installs it takes to produce one rating.
The formula
textdays = (latest.observedAt − previous.observedAt) / 86,400,000
delta = max(0, latest.ratingCount − previous.ratingCount)
monthly_installs = round( (delta / days) × 30 × 50 )
| Symbol | Meaning | Value or source |
|---|---|---|
latest.ratingCount | Lifetime rating count at the most recent tape point | Observed from the store's public lookup |
previous.ratingCount | Lifetime rating count at the tape point before it | Observed |
observedAt | Timestamp of a tape point | Observed |
86,400,000 | Milliseconds in a day, converting the elapsed gap to days | n/a |
30 | Days in the reported month | n/a |
50 | Installs assumed per rating left | Constant IOS_RATING_INSTALL_MULTIPLIER = 50 |
max(0, …) | Rating counts can fall when the store prunes reviews. A fall is reported as zero growth, never as negative installs. | n/a |
round | The result is an integer | n/a |
50 is the single assumption carrying the most weight in this page. It is a fixed constant in code, not tuned per app, per category or per country. If you think the true ratio for the app you are looking at is 100 installs per rating rather than 50, the honest correction is to double the number PeekPanda shows you. The model is deliberately arranged so you can do exactly that.
Preconditions
Two tape points are required, in the same country. With one tape point, monthlyInstalls is null. Not zero. One reading is a level, and you cannot get a rate of change out of a level.
The elapsed gap must be positive. If two points carry the same timestamp, or the later point is not later, the result is null rather than a division by zero.
The tape stores one point per app per UTC day for the US storefront. So in practice a brand-new app in the catalogue shows null installs on its first day and a number from its second.
Lifetime installs
A separate, blunter number on the same multiplier:
textlifetime_installs = latest.ratingCount × 50
This is a proxy too, and a cruder one: it assumes the 50:1 ratio has held over the app's entire life, across every version and every country, which it certainly has not. Read it as an order of magnitude.
Where this model breaks down
- The 50:1 ratio is not universal. Apps that prompt aggressively for ratings earn far more ratings per install than apps that never prompt. A rating-prompt A/B test will move this estimate without a single extra install.
- Rating prompts changed over time. Apple's in-app review API changed how often apps can ask. Historical tape and current tape are not on the same footing.
- The tape is US-only, the ratings are not. The lifetime rating count on the US listing includes ratings from other storefronts. The delta therefore reflects global rating activity, while the estimate is presented against a US tape. This inflates the number for apps with large non-US audiences.
- Short windows are noisy. Two consecutive days with a review-prompt spike between them will annualise that spike into a monthly figure.
- Pruned reviews clip to zero. A genuine decline is unrepresentable, so a month in which the store removed a batch of reviews reads as a quiet month rather than a negative one.
Where to find it
estimateMonthlyInstallsFromRatingTape and lifetimeInstallsFromRatingCount in packages/libraries/src/domain/app-catalog/store-rating-tape.contract.ts. Persisted and served by apps/backend/src/modules/app-catalog/enrichment/rating-tape.repository.ts.
Monthly revenue: grossing_rank_power_law
The idea
Apple publishes an ordered grossing chart per country and per category. Position 1 out-earns position 2, which out-earns position 3, and the drop-off follows something close to a power law: steep at the top, flattening out down the tail.
If you know what the number-one app actually earns, and you assume a shape for the curve, you can read a dollar figure off any position on that chart. That is the entire model.
The formula
textmonthly_revenue = country_scale × top1_usd × rank^(−0.85)
| Symbol | Meaning | Value or source |
|---|---|---|
rank | The app's observed position on a public grossing chart | Observed. Must be 1 or greater, otherwise the result is null. |
0.85 | The power-law exponent. Larger means a steeper drop from #1. | Constant GROSSING_REVENUE_ALPHA = 0.85 |
top1_usd | Assumed monthly USD of the #1 app on that chart | Calibration anchor, see below |
country_scale | Multiplier for a non-US storefront relative to US | See table below |
The result is rounded to whole dollars when it is stored.
The calibration anchors
top1_usd is chosen by the category of the chart the rank came from.
| Chart | top1_usd | Anchor app | Constant |
|---|---|---|---|
Overall (all) | $235,000,000 | ChatGPT, while it held US overall grossing #1 | US_OVERALL_GROSSING_TOP1_USD |
Games (6014) | $61,000,000 | MONOPOLY GO!, US Games grossing #1 | US_GAMES_GROSSING_TOP1_USD |
Business (6000) | $48,000,000 | LinkedIn, US Business grossing #1 | US_BUSINESS_GROSSING_TOP1_USD |
Food & Drink (6023) | $2,000,000 | ReciMe, US Food & Drink grossing #1 | US_FOOD_DRINK_GROSSING_TOP1_USD |
| Every other category | $3,000,000 | No anchor. This is the fallback. | DEFAULT_US_CATEGORY_GROSSING_TOP1_USD |
All four anchors were read on 6 September 2026 from public AppRank iOS pages.
Two things about that, stated plainly:
AppRank is a calibration reference, not a data source. PeekPanda does not call AppRank, does not have an account there, and does not import anything from it. Four monthly-revenue figures were read off four public pages once, hard-coded as constants, and that is the whole of the relationship. If AppRank's own numbers are wrong, PeekPanda's are wrong in the same direction.
Most categories have no anchor at all. Only four charts are calibrated. Everything else falls back to a flat $3,000,000 for the #1 app. A Health & Fitness estimate and a Photo & Video estimate are both built on that same unvalidated placeholder. Treat them as far weaker than the four anchored charts.
Country scaling
| Country | Scale | Meaning |
|---|---|---|
us | 1 | The reference market |
gb | 0.12 | A UK #1 is assumed to earn 12% of a US #1 |
de | 0.1 | 10% |
fr | 0.08 | 8% |
br | 0.05 | 5% |
be | 0.02 | 2% |
Any country outside this list returns null rather than being guessed at. The scales are flat per country and are not varied by category. They are estimates of relative market size, not measurements of it.
Which rank gets used
An app can sit on more than one grossing chart on the same day: the overall chart and one or more category charts. The model picks one.
- If the app is on the overall grossing chart, that rank is used, with the $235M overall anchor.
- Otherwise the best available rank across the category charts is used, meaning the numerically lowest one, with that category's anchor.
That is preferRank in apps/backend/src/modules/app-catalog/enrichment/rating-tape.repository.ts.
Preconditions
The app must be on a grossing chart. No chart position means no revenue estimate, and the field is null. It is not zero.
This is the model's largest coverage limitation and it is worth being blunt about it: charts are a top slice of the store. The overwhelming majority of apps in the catalogue never appear on any grossing chart, so the overwhelming majority of apps have null revenue. That is not a bug in the collection. There is simply no public signal to model from.
The country must have a scale. Ranks from storefronts outside the six covered countries produce null.
Rank must be 1 or greater. A missing or nonsensical rank produces null.
Where this model breaks down
- The exponent is a single assumed shape.
0.85is one number applied to every chart in every country. Real revenue curves differ sharply between Games, where whales concentrate spend at the top, and utility categories. - Four anchors, read on one day. They were true of four apps on 6 September 2026. The #1 app on any chart changes, and so does what it earns. The constants do not move on their own.
- The uncalibrated fallback dominates by count. Most categories use $3,000,000 with no anchor behind it.
- A chart position is not revenue. Two apps at the same grossing rank in different weeks are not earning the same amount; the chart is relative, and the whole chart moves.
- Country scaling is coarse. One multiplier per country, applied identically to a game and a B2B tool.
Where to find it
estimateMonthlyRevenueFromGrossingRank in packages/libraries/src/domain/app-catalog/store-rating-tape.contract.ts.
What an estimate record actually contains
Every estimate is served as an observation with its own date and its own method labels, not as a bare number.
| Field | Meaning |
|---|---|
observedAt | The timestamp of the tape point this estimate was computed at |
country | Two-letter storefront |
ratingCount | The observed lifetime rating count at that point |
lifetimeInstalls | ratingCount × 50, or null |
monthlyInstalls | The install model, or null |
previousMonthlyInstalls | The same model one tape point earlier, so you can see the direction |
estimatedMonthlyRevenue | The revenue model, or null |
previousMonthlyRevenue | The same model at the previous chart day |
currency | Always USD |
installMethod | Always ratings_delta_proxy |
revenueMethod | Always grossing_rank_power_law |
source | Always peekpanda, marking the row as PeekPanda's own derived output |
The source: peekpanda marker matters. It separates a number PeekPanda derived from an observation PeekPanda merely recorded. Nothing in an estimate row is a store fact.
Comparisons are made against the previous observation, not against a general baseline. previousMonthlyInstalls runs the same install model over the two tape points before the latest one. previousMonthlyRevenue runs the same revenue model over the preceding chart observation. When the history is too short for either, the previous value is null and no movement is drawn.
Contract: peekPandaEstimateObservationSchema in packages/libraries/src/domain/app-catalog/store-rating-tape.contract.ts.
Null is a result
Both models return null rather than a fallback whenever their preconditions are unmet:
| Situation | Result |
|---|---|
| One tape day only | monthlyInstalls is null |
| Two tape points with no elapsed time | monthlyInstalls is null |
| App not on any grossing chart | estimatedMonthlyRevenue is null |
| Storefront outside the six covered countries | estimatedMonthlyRevenue is null |
| No rating count recorded | lifetimeInstalls is null |
A null means PeekPanda does not know. A zero would mean PeekPanda knows the answer is zero. They are never interchanged, and no fallback value is ever substituted for a missing one.
Reading these numbers well
Use them for shape and relative order, not for absolute dollars. The models are consistent, which makes app-to-app comparison inside the same category and country meaningfully informative. They are not accurate in the sense of matching a company's real books, and a category using the uncalibrated $3M fallback should not be compared against an anchored one at all.
If you disagree with a number, you can now say which step you disagree with: the 50:1 install ratio, the 0.85 exponent, one of the four anchors, the $3M fallback, or a country scale. Each is one constant in one file.