-
Notifications
You must be signed in to change notification settings - Fork 166
Add function for computing package trend #8740
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 1 commit
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,96 @@ | ||
| // Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file | ||
| // for details. All rights reserved. Use of this source code is governed by a | ||
| // BSD-style license that can be found in the LICENSE file. | ||
|
|
||
| const minAvgDailyDownloadsThreshold = 1000; | ||
| const analysisWindowDays = 30; | ||
| const weightAge = 0.25; | ||
| const weightGrowthRate = 0.75; | ||
|
|
||
| /// Calculates the relative daily growth rate of a package's downloads. | ||
| /// | ||
| /// Given a list with total daily downloads ([totalDownloads]), where the most | ||
| /// recent day's data is at index 0, this function analyzes the downloads trend | ||
| /// over the last ([analysisWindowDays]) days to determine how fast a package is | ||
| /// growing relative to its own current download volume. | ||
| /// | ||
| /// Returns -double.maxFinite: | ||
| /// - If the [totalDownloads] list has fewer data points than | ||
| /// [analysisWindowDays]. | ||
| /// - If the average daily downloads over the [analysisWindowDays] period are | ||
| /// less than [minAvgDailyDownloadsThreshold]. | ||
| /// | ||
| /// Otherwise, it returns the calculated relative growth rate. A positive value | ||
| /// indicates an upward trend in downloads, while a negative value indicates a | ||
| /// downward trend. The magnitude represents the growth (or decline) rate | ||
| /// normalized by the average daily downloads, allowing for comparison across | ||
| /// packages of different popularity. For example, a slope of +10 downloads/day | ||
| /// is more significant for a package with 100 average daily downloads (10% | ||
| /// relative growth) than for a package with 10000 average daily downloads (0.1% | ||
| /// relative growth). | ||
| double computeRelativeGrowthRate(List<int> totalDownloads) { | ||
| if (totalDownloads.length < analysisWindowDays) { | ||
| // insufficient data points. | ||
| return -double.maxFinite; | ||
| } | ||
|
|
||
| final recentDownloads = totalDownloads.sublist(0, analysisWindowDays); | ||
|
|
||
| final averageRecentDownloads = | ||
| recentDownloads.reduce((prev, element) => prev + element) / | ||
| recentDownloads.length; | ||
|
|
||
| if (averageRecentDownloads < minAvgDailyDownloadsThreshold) { | ||
| // Package does not meet the minimum average download threshold. | ||
| return -double.maxFinite; | ||
| } | ||
| // We reverse the recentDownloads list for regression, since the first entry | ||
| // is the newest point in time. By reversing, we pass the data in | ||
| // chronological order. | ||
| final growthRate = | ||
| calculateLinearRegressionSlope(recentDownloads.reversed.toList()); | ||
|
|
||
| // Normalize slope by average downloads to represent relative growth. | ||
| // This measures how much the download count is growing relative to its | ||
| // current volume. | ||
| return growthRate / averageRecentDownloads; | ||
| } | ||
|
|
||
| /// Computes the slope of the best-fit line for a given list of data points | ||
| /// [yValues] using the method of least squares (linear regression). | ||
| /// | ||
| /// The function assumes that the [yValues] are equally spaced in time and are | ||
| /// provided in chronological order | ||
| /// | ||
| /// The slope `b` is calculated using the formula: `b = (N * sum(xy) - sum(x) * | ||
| /// sum(y)) / (N * sum(x^2) - (sum(x))^2)` where `N` is the number of data | ||
| /// points. | ||
| /// | ||
| /// Returns `0.0` if the slope cannot be determined reliably (e.g., if there are | ||
| /// fewer than 2 data points, or if the denominator in the slope formula is | ||
| /// effectively zero). | ||
| double calculateLinearRegressionSlope(List<num> yValues) { | ||
| double sumX = 0, sumY = 0, sumXY = 0, sumXX = 0; | ||
| final n = yValues.length; | ||
|
|
||
| // Slope is undefined or 0 for fewer than 2 points. | ||
| if (n < 2) { | ||
| return 0.0; | ||
| } | ||
|
|
||
| for (int x = 0; x < n; x++) { | ||
| final y = yValues[x]; | ||
| sumX += x; | ||
| sumY += y; | ||
| sumXY += x * y; | ||
| sumXX += x * x; | ||
| } | ||
|
|
||
| final double denominator = (n * sumXX - sumX * sumX); | ||
|
|
||
| // If the denominator is very close to zero, the slope is unstable/undefined. | ||
| if (denominator.abs() < 1e-9) { | ||
| return 0.0; | ||
| } | ||
| return (n * sumXY - sumX * sumY) / denominator; | ||
| } | ||
107 changes: 107 additions & 0 deletions
107
app/test/service/download_counts/package_trends_test.dart
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,107 @@ | ||
| // Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file | ||
| // for details. All rights reserved. Use of this source code is governed by a | ||
| // BSD-style license that can be found in the LICENSE file. | ||
|
|
||
| import 'package:pub_dev/service/download_counts/package_trends.dart'; | ||
| import 'package:test/test.dart'; | ||
|
|
||
| void main() { | ||
| group('calculateLinearRegressionSlope', () { | ||
| test('correctly calculates slope for chronological data', () { | ||
| expect(calculateLinearRegressionSlope([10.0, 20.0, 30.0]), 10.0); | ||
| expect(calculateLinearRegressionSlope([30.0, 20.0, 10.0]), -10.0); | ||
| expect(calculateLinearRegressionSlope([10.0, 10.0, 10.0]), 0); | ||
| }); | ||
|
|
||
| test('return 0.0 if denominator is very small', () { | ||
| expect(calculateLinearRegressionSlope([]), 0.0); | ||
| expect(calculateLinearRegressionSlope([10.0]), 0.0); | ||
| }); | ||
| }); | ||
|
|
||
| group('computeRelativeGrowthRate', () { | ||
| test('returns -double.maxFinite for insufficient data', () { | ||
| final downloads = List<int>.generate(analysisWindowDays - 1, (i) => 1500); | ||
| expect(computeRelativeGrowthRate(downloads), -double.maxFinite); | ||
| }); | ||
|
|
||
| test('returns -double.maxFinite for average downloads below threshold', () { | ||
| final downloads = List<int>.generate( | ||
| analysisWindowDays, (i) => minAvgDailyDownloadsThreshold - 1); | ||
| expect(computeRelativeGrowthRate(downloads), -double.maxFinite); | ||
| }); | ||
|
|
||
| test('returns 0.0 for stable downloads meeting threshold', () { | ||
| final downloads = List<int>.generate(analysisWindowDays, (i) => 2000); | ||
| expect(computeRelativeGrowthRate(downloads), 0.0); | ||
| }); | ||
|
|
||
| test('calculates positive relative growth rate for positive trend', () { | ||
| // Input list (newest first): [1645, 1635, ..., 1355] (30 values) | ||
| // Average = 1500 for the first 30 values. Slope: 10. | ||
| final downloads = | ||
| List<int>.generate(analysisWindowDays * 2, (i) => 1645 - (i * 10)); | ||
| final expectedRate = 10.0 / 1500.0; | ||
| expect(computeRelativeGrowthRate(downloads), expectedRate); | ||
| }); | ||
|
|
||
| test('calculates negative relative growth rate for negative trend', () { | ||
| // Input list (newest first): [1355, 1365, ..., 1645] | ||
| // Average = 1500. Slope: -10. | ||
| final downloads = | ||
| List<int>.generate(analysisWindowDays, (i) => 1355 + (i * 10)); | ||
| final expectedRate = -10.0 / 1500.0; | ||
| expect(computeRelativeGrowthRate(downloads), expectedRate); | ||
| }); | ||
|
|
||
| test( | ||
| 'calculates positive relative growth for data barely meeting threshold', | ||
| () { | ||
| // Input list (newest first): [1016, 1015, ..., 987] | ||
| // Average: 1001.5. Slope: 1. | ||
| final downloads = | ||
| List<int>.generate(analysisWindowDays, (i) => 1016 - i * 1); | ||
| final expectedRate = 1.0 / 1001.5; | ||
| expect(computeRelativeGrowthRate(downloads), closeTo(expectedRate, 1e-9)); | ||
| }); | ||
|
|
||
| test('should handle fluctuating data with a slight positive overall trend', | ||
| () { | ||
| // Newest first. Average 1135. | ||
| final downloads = <int>[ | ||
| 1300, | ||
| 1250, | ||
| 1280, | ||
| 1230, | ||
| 1260, | ||
| 1210, | ||
| 1240, | ||
| 1190, | ||
| 1220, | ||
| 1170, | ||
| 1200, | ||
| 1150, | ||
| 1180, | ||
| 1130, | ||
| 1160, | ||
| 1110, | ||
| 1140, | ||
| 1090, | ||
| 1120, | ||
| 1070, | ||
| 1100, | ||
| 1050, | ||
| 1080, | ||
| 1030, | ||
| 1060, | ||
| 1010, | ||
| 1040, | ||
| 990, | ||
| 1020, | ||
| 970 | ||
| ]; | ||
| final expectedRate = 683250.0 / 67425.0 / 1135.0; | ||
| expect(computeRelativeGrowthRate(downloads), expectedRate); | ||
| }); | ||
| }); | ||
| } |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.