Skip to content
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

Adding min() and max() aggregations #5029

Merged
merged 2 commits into from
Nov 5, 2024
Merged

Adding min() and max() aggregations #5029

merged 2 commits into from
Nov 5, 2024

Conversation

brimoor
Copy link
Contributor

@brimoor brimoor commented Nov 1, 2024

Adds min() and max() aggregations.

These can be computed via bounds()[0] and bounds()[1] today, but it's nice to have dedicated methods because:

  • These are more efficient when you don't need both extrema
  • Yields more readable code when you want only a particular extremum

Summary by CodeRabbit

  • New Features

    • Introduced new aggregation methods for computing minimum (min()) and maximum (max()) values in datasets.
    • Added new public entities, Min and Max, to enhance aggregation functionalities.
    • Updated documentation to include examples for the new aggregation methods.
  • Bug Fixes

    • Improved handling of summary fields to ensure accurate updates based on dataset modifications.
  • Tests

    • Added unit tests for the new min and max functionalities to validate their performance and correctness.

@brimoor brimoor added the feature Work on a feature request label Nov 1, 2024
Copy link
Contributor

coderabbitai bot commented Nov 1, 2024

Walkthrough

The pull request introduces new aggregation methods for computing minimum and maximum values in the FiftyOne library. Specifically, it adds min() and max() methods to the SampleCollection class and corresponding Min and Max classes in the core.aggregations module. Documentation updates reflect these changes, providing examples for users. Additionally, modifications in the Dataset class enhance the handling of summary fields, and new unit tests validate the functionality of the newly introduced methods.

Changes

File Path Change Summary
fiftyone/core/collections.py Added min and max methods to SampleCollection for computing minimum and maximum values. Updated bounds, count, and count_values methods to support date fields.
fiftyone/core/aggregations.py Introduced Min and Max classes for aggregation, including methods for default results, parsing, and MongoDB pipeline generation.
fiftyone/__public__.py Added Min and Max entities to the public interface.
fiftyone/core/dataset.py Replaced _get_last_modified_at with _max method for retrieving maximum timestamps in Dataset. Updated check_summary_fields method accordingly.
tests/unittests/aggregation_tests.py Added test_min and test_max methods to validate the new aggregation functionalities.
tests/unittests/dataset_tests.py Updated test_sync_last_modified_at to use _max for last modified timestamps instead of _get_last_modified_at.

Possibly related PRs

  • Add support for summary fields #4765: This PR enhances the dataset management system, which is relevant to the new aggregation methods introduced in the main PR, as both involve improvements to data handling and analysis capabilities within the FiftyOne framework.
  • Adding builtin operators for more of the FO interface #4830: This PR introduces new operators for creating and managing summary fields, which directly relates to the new min and max aggregation methods added in the main PR, as they both enhance the aggregation functionalities available in the FiftyOne interface.

Suggested reviewers

  • ritch
  • imanjra

Poem

In the fields of data bright,
Min and Max take their flight.
With each value, they do play,
Helping us in a clever way.
So hop along, let’s celebrate,
Aggregations that are truly great! 🐰✨


Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Outside diff range and nitpick comments (5)
docs/source/user_guide/using_aggregations.rst (1)

399-418: Consider adding performance comparison note.

While the documentation is well-structured and clear, consider adding a note about the performance advantage of using max() over bounds()[1] when only the maximum value is needed. This aligns with the PR objectives of highlighting efficiency benefits.

Add a note after line 407:

 compute the maximum of the (non-``None``) values of a field in a collection:
+
+.. note::
+
+    Using ``max()`` is more efficient than ``bounds()[1]`` when you only need
+    the maximum value, as it avoids computing both bounds.
tests/unittests/aggregation_tests.py (1)

236-278: Add test cases for edge scenarios in test_min.

While the test covers the basic functionality well, consider adding the following test cases to improve robustness:

  1. Empty dataset scenario
  2. Fields containing None/null values
  3. Fields containing NaN/Inf values
  4. Invalid field paths to verify error handling

Example test cases:

def test_min(self):
    # Existing tests...
    
    # Test empty dataset
    empty_dataset = fo.Dataset()
    self.assertIsNone(empty_dataset.min("number"))
    
    # Test None values
    d = fo.Dataset()
    s = fo.Sample(filepath="image.jpeg", number=None)
    d.add_sample(s)
    self.assertIsNone(d.min("number"))
    
    # Test NaN/Inf
    d = fo.Dataset()
    s = fo.Sample(filepath="image.jpeg", number=float('inf'))
    d.add_sample(s)
    self.assertTrue(math.isinf(d.min("number")))
    
    # Test invalid field
    with self.assertRaises(ValueError):
        d.min("nonexistent_field")
fiftyone/core/aggregations.py (1)

1597-1600: Consider using ternary operators for more concise code.

The if-else blocks for setting the value variable can be simplified using ternary operators:

-        if id_to_str:
-            value = {"$toString": "$" + path}
-        else:
-            value = "$" + path
+        value = {"$toString": "$" + path} if id_to_str else "$" + path

This change would make the code more concise while maintaining readability.

Also applies to: 1719-1722

🧰 Tools
🪛 Ruff

1597-1600: Use ternary operator value = {"$toString": "$" + path} if id_to_str else "$" + path instead of if-else-block

Replace if-else-block with value = {"$toString": "$" + path} if id_to_str else "$" + path

(SIM108)

fiftyone/core/collections.py (2)

8087-8089: Consider defining make as a regular function instead of a lambda.

Using a regular def instead of assigning a lambda expression to a variable can improve readability.

Would you like me to rewrite this as a regular function definition for you?

🧰 Tools
🪛 Ruff

8087-8089: Do not assign a lambda expression, use a def

Rewrite make as a def

(E731)


8171-8173: Consider defining make as a regular function instead of a lambda.

Using a regular def instead of assigning a lambda expression to a variable can improve readability.

I can rewrite this as a regular function definition if you'd like. Let me know if you want me to submit a pull request with the change.

🧰 Tools
🪛 Ruff

8171-8173: Do not assign a lambda expression, use a def

Rewrite make as a def

(E731)

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 84f7e27 and 4a09701.

📒 Files selected for processing (7)
  • docs/source/user_guide/using_aggregations.rst (1 hunks)
  • fiftyone/__public__.py (1 hunks)
  • fiftyone/core/aggregations.py (2 hunks)
  • fiftyone/core/collections.py (4 hunks)
  • fiftyone/core/dataset.py (1 hunks)
  • tests/unittests/aggregation_tests.py (1 hunks)
  • tests/unittests/dataset_tests.py (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • tests/unittests/dataset_tests.py
🧰 Additional context used
🪛 Ruff
fiftyone/__public__.py

24-24: .core.aggregations.Min imported but unused

Remove unused import

(F401)


25-25: .core.aggregations.Max imported but unused

Remove unused import

(F401)

fiftyone/core/aggregations.py

1597-1600: Use ternary operator value = {"$toString": "$" + path} if id_to_str else "$" + path instead of if-else-block

Replace if-else-block with value = {"$toString": "$" + path} if id_to_str else "$" + path

(SIM108)


1719-1722: Use ternary operator value = {"$toString": "$" + path} if id_to_str else "$" + path instead of if-else-block

Replace if-else-block with value = {"$toString": "$" + path} if id_to_str else "$" + path

(SIM108)

fiftyone/core/collections.py

8087-8089: Do not assign a lambda expression, use a def

Rewrite make as a def

(E731)


8171-8173: Do not assign a lambda expression, use a def

Rewrite make as a def

(E731)

🔇 Additional comments (6)
fiftyone/__public__.py (1)

24-25: LGTM! The new aggregation classes are properly exposed.

The addition of Min and Max to the public interface aligns with the PR objectives and follows the existing pattern of exposing aggregation classes.

Let's verify the implementation of these classes:

✅ Verification successful

The Min and Max aggregations are properly implemented and integrated

The verification confirms:

  • Both Min and Max classes are fully implemented in core/aggregations.py with proper inheritance from Aggregation
  • They include comprehensive documentation, examples, and handle edge cases like None values
  • The classes are integrated into the collections interface with corresponding min() and max() methods
  • The implementation follows MongoDB aggregation patterns using $min and $max operators
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify the implementation of Min and Max aggregation classes
# Expected: Find class definitions with proper inheritance and required methods

# Test 1: Verify class definitions
ast-grep --pattern 'class Min(Aggregation):
  $$$
'

ast-grep --pattern 'class Max(Aggregation):
  $$$
'

# Test 2: Verify integration with SampleCollection
rg -A 5 'def (min|max)\(' core/collections.py

Length of output: 15793


Script:

#!/bin/bash
# Let's verify the integration with SampleCollection by searching in the correct path
fd -t f collections.py --exec rg -A 5 'def (min|max)\(' {}

# Also verify if there are any tests for these aggregations
fd -t f test_aggregations.py --exec rg -A 5 '(test_min|test_max)' {}

Length of output: 634

🧰 Tools
🪛 Ruff

24-24: .core.aggregations.Min imported but unused

Remove unused import

(F401)


25-25: .core.aggregations.Max imported but unused

Remove unused import

(F401)

docs/source/user_guide/using_aggregations.rst (1)

379-398: Documentation for min() aggregation looks good!

The section is well-structured, follows the established documentation pattern, and provides a clear, practical example that aligns with the existing bounds() method documentation.

fiftyone/core/aggregations.py (2)

1485-1604: LGTM! Well-implemented Min aggregation class.

The implementation is clean and follows the established patterns:

  • Proper inheritance from Aggregation base class
  • Comprehensive documentation with clear examples
  • Correct handling of field type conversion and None values
  • Efficient MongoDB aggregation pipeline generation
🧰 Tools
🪛 Ruff

1597-1600: Use ternary operator value = {"$toString": "$" + path} if id_to_str else "$" + path instead of if-else-block

Replace if-else-block with value = {"$toString": "$" + path} if id_to_str else "$" + path

(SIM108)


1607-1726: LGTM! Well-implemented Max aggregation class.

The implementation is clean and follows the established patterns:

  • Proper inheritance from Aggregation base class
  • Comprehensive documentation with clear examples
  • Correct handling of field type conversion and None values
  • Efficient MongoDB aggregation pipeline generation
🧰 Tools
🪛 Ruff

1719-1722: Use ternary operator value = {"$toString": "$" + path} if id_to_str else "$" + path instead of if-else-block

Replace if-else-block with value = {"$toString": "$" + path} if id_to_str else "$" + path

(SIM108)

fiftyone/core/dataset.py (2)

2086-2087: LGTM! Verify the new method implementation.

The change to use _max() for getting the maximum value of "frames.last_modified_at" looks good and improves code clarity.

#!/bin/bash
# Verify that the _max() method exists and is properly implemented
ast-grep --pattern $'class Dataset {
  $$$
  _max($field) {
    $$$
  }
  $$$
}'

2094-2094: LGTM! Verify consistent usage.

The change to use _max() for getting the maximum value of "last_modified_at" is consistent with the previous change.

✅ Verification successful

The change to use _max() is consistent and verified

Based on the extensive search results, the usage of _max() for getting the maximum value of "last_modified_at" is consistent throughout the codebase. The method is used in multiple places for the same purpose, particularly in sync_last_modified_at() functionality. No instances of the old _get_last_modified_at() method were found, indicating a complete migration to the new approach.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Verify consistent usage of _max() vs _get_last_modified_at()
rg -A 2 "_get_last_modified_at"

Length of output: 362


Script:

#!/bin/bash
# Search for _max method usage
rg -A 2 "_max\("

# Search for last_modified_at field usage
rg -A 2 "last_modified_at"

# Search for method definitions using AST
ast-grep --pattern $'def _max($_) {
  $$$
}'

Length of output: 124009


Script:

#!/bin/bash
# Search for _max method implementation
ast-grep --pattern 'def _max(self, path):
  $$$
}'

# Search for _get_last_modified_at method
ast-grep --pattern 'def _get_last_modified_at(self, $_):
  $$$
}'

Length of output: 120

tests/unittests/aggregation_tests.py Show resolved Hide resolved
Copy link
Contributor

@swheaton swheaton left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm, just recommend your review comment be in code for future readers

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Outside diff range and nitpick comments (2)
fiftyone/core/collections.py (2)

8023-8104: Consider replacing lambda with a regular function definition

The implementation looks good, but using a lambda for the make function could be improved for better readability.

-    make = lambda field_or_expr: foa.Min(
-        field_or_expr, expr=expr, safe=safe
-    )
+    def make(field_or_expr):
+        return foa.Min(field_or_expr, expr=expr, safe=safe)
🧰 Tools
🪛 Ruff

8101-8103: Do not assign a lambda expression, use a def

Rewrite make as a def

(E731)


8107-8188: Consider replacing lambda with a regular function definition

Similar to the min() method, using a lambda for the make function could be improved for better readability.

-    make = lambda field_or_expr: foa.Max(
-        field_or_expr, expr=expr, safe=safe
-    )
+    def make(field_or_expr):
+        return foa.Max(field_or_expr, expr=expr, safe=safe)
🧰 Tools
🪛 Ruff

8185-8187: Do not assign a lambda expression, use a def

Rewrite make as a def

(E731)

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 4a09701 and f4190b9.

📒 Files selected for processing (1)
  • fiftyone/core/collections.py (4 hunks)
🧰 Additional context used
🪛 Ruff
fiftyone/core/collections.py

8101-8103: Do not assign a lambda expression, use a def

Rewrite make as a def

(E731)


8185-8187: Do not assign a lambda expression, use a def

Rewrite make as a def

(E731)

🔇 Additional comments (1)
fiftyone/core/collections.py (1)

661-676: LGTM! Well-optimized implementation with clear documentation

The _get_extremum() implementation is well thought out, with clear documentation explaining the optimization rationale. The method efficiently handles both sample and frame fields using MongoDB's $sort + $limit 1 approach which is more performant than $group _id: None when the field is indexed.

@brimoor brimoor merged commit 31f2d7b into develop Nov 5, 2024
13 checks passed
@brimoor brimoor deleted the min-max branch November 5, 2024 17:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature Work on a feature request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants