This is an automated email from the ASF dual-hosted git repository.

pitrou pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow.git


The following commit(s) were added to refs/heads/main by this push:
     new 626407c899 GH-51224: [C++] Keep the correct nulls when winsorizing a 
sliced array (#51251)
626407c899 is described below

commit 626407c899759168343d5cd86674136f05abf3b6
Author: Stefan Wang <[email protected]>
AuthorDate: Mon Sep 28 01:08:32 2026 -0700

    GH-51224: [C++] Keep the correct nulls when winsorizing a sliced array 
(#51251)
    
    ### Rationale for this change
    
    Winsorizing a sliced array can put nulls in the wrong positions. Slices 
containing only nulls or NaNs can also return values from outside the slice.
    
    Closes https://github.com/apache/arrow/issues/51224.
    
    ### What changes are included in this PR?
    
    Use the bitmap helper to match validity bits to the output values. Preserve 
the input offset when returning the original buffers.
    
    ### Are these changes tested?
    
    I ran the native C++ tests and PyArrow on macOS arm64 with Apple clang 21. 
The new regression covers null-only, NaN-only and mixed null/NaN slices, with 
chunked inputs as controls.
    
    With PyArrow loading the native library being tested, save this as 
repro-winsorize.py:
    
    ```python
    import pyarrow as pa
    import pyarrow.compute as pc
    
    parent = pa.array([1.0, 2.0, None, 4.0, None, 6.0, 7.0, 8.0])
    result = pc.winsorize(parent.slice(2, 5), lower_limit=0.0, upper_limit=1.0)
    print("mixed slice:", result.to_pylist(), flush=True)
    
    parent = pa.array([1.0, 2.0, None, None, None, 3.0])
    nulls = pc.winsorize(parent.slice(2, 3), lower_limit=0.0, upper_limit=1.0)
    print("all-null slice:", nulls.to_pylist(), flush=True)
    assert result.to_pylist() == [None, 4.0, None, 6.0, 7.0]
    assert nulls.to_pylist() == [None, None, None]
    ```
    
    <details><summary>Raw logs</summary>
    
    Before:
    
    ```console
    $ python repro-winsorize.py
    mixed slice: [0.0, 4.0, None, 6.0, None]
    all-null slice: [1.0, 2.0, None]
    AssertionError
    ```
    
    After:
    
    ```console
    $ python repro-winsorize.py
    mixed slice: [None, 4.0, None, 6.0, 7.0]
    all-null slice: [None, None, None]
    exit_code=0
    $ build/release/arrow-compute-vector-test
    [  PASSED  ] 1157 tests.
    $ build/release/arrow-compute-aggregate-test 
--gtest_filter='*Quantile*:*TDigest*'
    [  PASSED  ] 28 tests.
    ```
    
    </details>
    
    ### Are there any user-facing changes?
    
    Sliced inputs retain the correct values and null positions.
    
    ### Was AI used for this PR?
    
    In accordance to the [AI generation 
guidelines](https://arrow.apache.org/docs/dev/developers/overview.html#ai-generated-code),
 please disclose below whether and how AI was used in this PR.
    
    **PR code and description written by:**
    
    - [ ] Human
    - [x] AI
    
    **Reviewed before submission by:**
    
    - [x] Human
    - [ ] AI
    - [ ] Not reviewed
    
    * GitHub Issue: #51224
    
    Authored-by: 1fanwang <[email protected]>
    Signed-off-by: Antoine Pitrou <[email protected]>
---
 cpp/src/arrow/compute/kernels/vector_statistics.cc |  6 +++-
 .../compute/kernels/vector_statistics_test.cc      | 39 ++++++++++++++++++++++
 2 files changed, 44 insertions(+), 1 deletion(-)

diff --git a/cpp/src/arrow/compute/kernels/vector_statistics.cc 
b/cpp/src/arrow/compute/kernels/vector_statistics.cc
index 074f2ec0a7..cc2fbc4016 100644
--- a/cpp/src/arrow/compute/kernels/vector_statistics.cc
+++ b/cpp/src/arrow/compute/kernels/vector_statistics.cc
@@ -26,6 +26,7 @@
 #include "arrow/compute/function.h"
 #include "arrow/compute/kernel.h"
 #include "arrow/compute/kernels/codegen_internal.h"
+#include "arrow/compute/kernels/util_internal.h"
 #include "arrow/compute/registry.h"
 #include "arrow/compute/registry_internal.h"
 #include "arrow/result.h"
@@ -71,6 +72,7 @@ struct Winsorize {
       // Only nulls and NaNs => return input as-is
       out_data->null_count = data->null_count.load();
       out_data->length = data->length;
+      out_data->offset = data->offset;
       out_data->buffers = data->buffers;
       return Status::OK();
     }
@@ -127,7 +129,9 @@ struct Winsorize {
     DCHECK_EQ(out->buffers.size(), data.buffers.size());
     out->null_count = data.null_count.load();
     out->length = data.length;
-    out->buffers[0] = data.buffers[0];
+    out->offset = 0;
+    ARROW_ASSIGN_OR_RAISE(out->buffers[0],
+                          GetOrCopyNullBitmapBuffer(data, ctx->memory_pool()));
     ARROW_ASSIGN_OR_RAISE(out->buffers[1], ctx->Allocate(out->length * 
sizeof(CType)));
     // Avoid leaving uninitialized memory under null entries
     std::memset(out->buffers[1]->mutable_data(), 0, out->length * 
sizeof(CType));
diff --git a/cpp/src/arrow/compute/kernels/vector_statistics_test.cc 
b/cpp/src/arrow/compute/kernels/vector_statistics_test.cc
index 97715cdaed..72a765be10 100644
--- a/cpp/src/arrow/compute/kernels/vector_statistics_test.cc
+++ b/cpp/src/arrow/compute/kernels/vector_statistics_test.cc
@@ -87,6 +87,45 @@ TEST_F(TestWinsorize, FloatingPoint) {
   }
 }
 
+TEST_F(TestWinsorize, SlicedInput) {
+  options_.lower_limit = 0.0;
+  options_.upper_limit = 1.0;
+  auto parent = ArrayFromJSON(float64(), "[1.1, 2.2, null, 4.4, null, 6.6, 
7.7, 8.8]");
+  auto expected = ArrayFromJSON(float64(), "[null, 4.4, null, 6.6, 7.7]");
+  CheckWinsorize(parent->Slice(2, 5), expected);
+
+  options_.lower_limit = 0.25;
+  options_.upper_limit = 0.75;
+  auto dense = ArrayFromJSON(float64(), "[1.0, 2.0, 3.0, 44.0, 55.0, 66.0, 
77.0]");
+  CheckWinsorize(dense->Slice(1, 5),
+                 ArrayFromJSON(float64(), "[3.0, 3.0, 44.0, 55.0, 55.0]"));
+}
+
+TEST_F(TestWinsorize, SlicedChunkedInput) {
+  options_.lower_limit = 0.0;
+  options_.upper_limit = 1.0;
+  auto parent = ArrayFromJSON(float64(), "[1.1, 2.2, null, 4.4, null, 6.6, 
7.7, 8.8]");
+  auto chunked = std::make_shared<ChunkedArray>(
+      ArrayVector{parent->Slice(2, 3), parent->Slice(5, 3)});
+  auto expected = std::make_shared<ChunkedArray>(ArrayVector{
+      ArrayFromJSON(float64(), "[null, 4.4, null]"),
+      ArrayFromJSON(float64(), "[6.6, 7.7, 8.8]"),
+  });
+  CheckWinsorize(chunked, expected);
+}
+
+TEST_F(TestWinsorize, SlicedInputWithoutQuantiles) {
+  for (const auto* json_input :
+       {"[1, 2, null, null, null, 3]", "[1, 2, NaN, null, NaN, 3]",
+        "[1, 2, NaN, NaN, NaN, 3]"}) {
+    auto parent = ArrayFromJSON(float64(), json_input);
+    auto sliced = parent->Slice(2, 3);
+    CheckWinsorize(sliced, sliced);
+    auto chunked = std::make_shared<ChunkedArray>(ArrayVector{sliced});
+    CheckWinsorize(chunked, chunked);
+  }
+}
+
 TEST_F(TestWinsorize, Integral) {
   for (auto type : IntTypes()) {
     options_.lower_limit = 0.25;

Reply via email to