You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@arrow.apache.org by we...@apache.org on 2018/03/29 23:38:12 UTC

[arrow] branch master updated: ARROW-2140: [Python] Improve float16 support

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

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


The following commit(s) were added to refs/heads/master by this push:
     new ba0cea3  ARROW-2140: [Python] Improve float16 support
ba0cea3 is described below

commit ba0cea32cfa5e94bf292c9ce0fbb64d663eb3628
Author: Antoine Pitrou <an...@python.org>
AuthorDate: Thu Mar 29 19:38:05 2018 -0400

    ARROW-2140: [Python] Improve float16 support
    
    Author: Antoine Pitrou <an...@python.org>
    
    Closes #1744 from pitrou/ARROW-2140-py-float16 and squashes the following commits:
    
    f6ebc83 <Antoine Pitrou> Merge branch 'master' into ARROW-2140-py-float16
    64fb518 <Antoine Pitrou> ARROW-2140:  Improve float16 support
---
 ci/msvc-build.bat                           |  1 +
 ci/travis_script_python.sh                  |  1 +
 cpp/src/arrow/python/arrow_to_pandas.cc     | 30 +++++++++++++++++++++++++++++
 cpp/src/arrow/python/arrow_to_python.cc     |  5 +----
 cpp/src/arrow/python/builtin_convert.cc     | 13 +++++++++++++
 cpp/src/arrow/python/helpers.cc             | 18 +++++++++++++++++
 cpp/src/arrow/python/helpers.h              |  8 ++++++++
 python/pyarrow/__init__.py                  |  2 +-
 python/pyarrow/array.pxi                    |  5 +++++
 python/pyarrow/includes/common.pxd          |  3 +++
 python/pyarrow/includes/libarrow.pxd        |  6 ++++++
 python/pyarrow/lib.pxd                      |  4 ++++
 python/pyarrow/scalar.pxi                   |  8 ++++++++
 python/pyarrow/tests/test_convert_pandas.py | 27 ++++++++++++++++++++++----
 python/pyarrow/tests/test_scalars.py        | 11 +++++++++++
 15 files changed, 133 insertions(+), 9 deletions(-)

diff --git a/ci/msvc-build.bat b/ci/msvc-build.bat
index a29ef0b..cec1429 100644
--- a/ci/msvc-build.bat
+++ b/ci/msvc-build.bat
@@ -68,6 +68,7 @@ if "%JOB%" == "Build_Debug" (
   exit /B 0
 )
 
+@rem Note: avoid Cython 0.28.0 due to https://github.com/cython/cython/issues/2148
 conda create -n arrow -q -y python=%PYTHON% ^
       six pytest setuptools numpy pandas ^
       cython=0.27.3 ^
diff --git a/ci/travis_script_python.sh b/ci/travis_script_python.sh
index 247d102..aa3c315 100755
--- a/ci/travis_script_python.sh
+++ b/ci/travis_script_python.sh
@@ -36,6 +36,7 @@ source activate $CONDA_ENV_DIR
 python --version
 which python
 
+# Note: avoid Cython 0.28.0 due to https://github.com/cython/cython/issues/2148
 conda install -y -q pip \
       nomkl \
       cloudpickle \
diff --git a/cpp/src/arrow/python/arrow_to_pandas.cc b/cpp/src/arrow/python/arrow_to_pandas.cc
index 5749e4f..92461fc 100644
--- a/cpp/src/arrow/python/arrow_to_pandas.cc
+++ b/cpp/src/arrow/python/arrow_to_pandas.cc
@@ -197,6 +197,7 @@ class PandasBlock {
     INT32,
     UINT64,
     INT64,
+    HALF_FLOAT,
     FLOAT,
     DOUBLE,
     BOOL,
@@ -815,6 +816,31 @@ using Int32Block = IntBlock<Type::INT32, int32_t>;
 using UInt64Block = IntBlock<Type::UINT64, uint64_t>;
 using Int64Block = IntBlock<Type::INT64, int64_t>;
 
+class Float16Block : public PandasBlock {
+ public:
+  using PandasBlock::PandasBlock;
+  Status Allocate() override { return AllocateNDArray(NPY_FLOAT16); }
+
+  Status Write(const std::shared_ptr<Column>& col, int64_t abs_placement,
+               int64_t rel_placement) override {
+    Type::type type = col->type()->id();
+
+    if (type != Type::HALF_FLOAT) {
+      std::stringstream ss;
+      ss << "Cannot write Arrow data of type " << col->type()->ToString();
+      ss << " to a Pandas float16 block.";
+      return Status::NotImplemented(ss.str());
+    }
+
+    npy_half* out_buffer =
+        reinterpret_cast<npy_half*>(block_data_) + rel_placement * num_rows_;
+
+    ConvertNumericNullable<npy_half>(*col->data().get(), NPY_HALF_NAN, out_buffer);
+    placement_data_[rel_placement] = abs_placement;
+    return Status::OK();
+  }
+};
+
 class Float32Block : public PandasBlock {
  public:
   using PandasBlock::PandasBlock;
@@ -1225,6 +1251,7 @@ Status MakeBlock(PandasOptions options, PandasBlock::type type, int64_t num_rows
     BLOCK_CASE(INT32, Int32Block);
     BLOCK_CASE(UINT64, UInt64Block);
     BLOCK_CASE(INT64, Int64Block);
+    BLOCK_CASE(HALF_FLOAT, Float16Block);
     BLOCK_CASE(FLOAT, Float32Block);
     BLOCK_CASE(DOUBLE, Float64Block);
     BLOCK_CASE(BOOL, BoolBlock);
@@ -1269,6 +1296,9 @@ static Status GetPandasBlockType(const Column& col, const PandasOptions& options
       INTEGER_CASE(UINT64);
     case Type::INT64:
       INTEGER_CASE(INT64);
+    case Type::HALF_FLOAT:
+      *output_type = PandasBlock::HALF_FLOAT;
+      break;
     case Type::FLOAT:
       *output_type = PandasBlock::FLOAT;
       break;
diff --git a/cpp/src/arrow/python/arrow_to_python.cc b/cpp/src/arrow/python/arrow_to_python.cc
index 5515d24..3fdc5f1 100644
--- a/cpp/src/arrow/python/arrow_to_python.cc
+++ b/cpp/src/arrow/python/arrow_to_python.cc
@@ -127,11 +127,8 @@ Status GetValue(PyObject* context, const UnionArray& parent, const Array& arr,
       return CheckPyError();
     }
     case Type::HALF_FLOAT: {
-      *result = PyArrayScalar_New(Half);
+      *result = PyHalf_FromHalf(static_cast<const HalfFloatArray&>(arr).Value(index));
       RETURN_IF_PYERROR();
-
-      npy_half halffloat = static_cast<const HalfFloatArray&>(arr).Value(index);
-      PyArrayScalar_ASSIGN(*result, Half, halffloat);
       return Status::OK();
     }
     case Type::FLOAT:
diff --git a/cpp/src/arrow/python/builtin_convert.cc b/cpp/src/arrow/python/builtin_convert.cc
index 595499d..5e99992 100644
--- a/cpp/src/arrow/python/builtin_convert.cc
+++ b/cpp/src/arrow/python/builtin_convert.cc
@@ -655,6 +655,17 @@ class TimestampConverter
   TimeUnit::type unit_;
 };
 
+class Float16Converter
+    : public TypedConverterVisitor<HalfFloatBuilder, Float16Converter> {
+ public:
+  // Append a non-missing item
+  Status AppendItem(PyObject* obj) {
+    npy_half val;
+    RETURN_NOT_OK(PyFloat_AsHalf(obj, &val));
+    return typed_builder_->Append(val);
+  }
+};
+
 class Float32Converter : public TypedConverterVisitor<FloatBuilder, Float32Converter> {
  public:
   // Append a non-missing item
@@ -887,6 +898,8 @@ std::unique_ptr<SeqConverter> GetConverter(const std::shared_ptr<DataType>& type
     case Type::TIMESTAMP:
       return std::unique_ptr<SeqConverter>(
           new TimestampConverter(static_cast<const TimestampType&>(*type).unit()));
+    case Type::HALF_FLOAT:
+      return std::unique_ptr<SeqConverter>(new Float16Converter);
     case Type::FLOAT:
       return std::unique_ptr<SeqConverter>(new Float32Converter);
     case Type::DOUBLE:
diff --git a/cpp/src/arrow/python/helpers.cc b/cpp/src/arrow/python/helpers.cc
index 63fee54..4fd9ef2 100644
--- a/cpp/src/arrow/python/helpers.cc
+++ b/cpp/src/arrow/python/helpers.cc
@@ -58,6 +58,24 @@ std::shared_ptr<DataType> GetPrimitiveType(Type::type type) {
   }
 }
 
+PyObject* PyHalf_FromHalf(npy_half value) {
+  PyObject* result = PyArrayScalar_New(Half);
+  if (result != NULL) {
+    PyArrayScalar_ASSIGN(result, Half, value);
+  }
+  return result;
+}
+
+Status PyFloat_AsHalf(PyObject* obj, npy_half* out) {
+  if (PyArray_IsScalar(obj, Half)) {
+    *out = PyArrayScalar_VAL(obj, Half);
+    return Status::OK();
+  } else {
+    // XXX: cannot use npy_double_to_half() without linking with Numpy
+    return Status::TypeError("Expected np.float16 instance");
+  }
+}
+
 namespace internal {
 
 Status ImportModule(const std::string& module_name, OwnedRef* ref) {
diff --git a/cpp/src/arrow/python/helpers.h b/cpp/src/arrow/python/helpers.h
index b9f505a..e2f3b18 100644
--- a/cpp/src/arrow/python/helpers.h
+++ b/cpp/src/arrow/python/helpers.h
@@ -24,6 +24,8 @@
 #include <string>
 #include <utility>
 
+#include <numpy/halffloat.h>
+
 #include "arrow/type.h"
 #include "arrow/util/macros.h"
 #include "arrow/util/visibility.h"
@@ -41,6 +43,12 @@ class OwnedRef;
 // \return A shared pointer to DataType
 ARROW_EXPORT std::shared_ptr<DataType> GetPrimitiveType(Type::type type);
 
+// \brief Construct a np.float16 object from a npy_half value.
+ARROW_EXPORT PyObject* PyHalf_FromHalf(npy_half value);
+
+// \brief Convert a Python object to a npy_half value.
+ARROW_EXPORT Status PyFloat_AsHalf(PyObject* obj, npy_half* out);
+
 namespace internal {
 
 // \brief Import a Python module
diff --git a/python/pyarrow/__init__.py b/python/pyarrow/__init__.py
index bfd7d4d..09f907c 100644
--- a/python/pyarrow/__init__.py
+++ b/python/pyarrow/__init__.py
@@ -78,7 +78,7 @@ from pyarrow.lib import (null, bool_,
                          BooleanValue,
                          Int8Value, Int16Value, Int32Value, Int64Value,
                          UInt8Value, UInt16Value, UInt32Value, UInt64Value,
-                         FloatValue, DoubleValue, ListValue,
+                         HalfFloatValue, FloatValue, DoubleValue, ListValue,
                          BinaryValue, StringValue, FixedSizeBinaryValue,
                          DecimalValue,
                          Date32Value, Date64Value, TimestampValue)
diff --git a/python/pyarrow/array.pxi b/python/pyarrow/array.pxi
index c40d7b5..a67bd8b 100644
--- a/python/pyarrow/array.pxi
+++ b/python/pyarrow/array.pxi
@@ -735,6 +735,10 @@ cdef class Time64Array(NumericArray):
     pass
 
 
+cdef class HalfFloatArray(FloatingPointArray):
+    pass
+
+
 cdef class FloatArray(FloatingPointArray):
     pass
 
@@ -1056,6 +1060,7 @@ cdef dict _array_classes = {
     _Type_TIMESTAMP: TimestampArray,
     _Type_TIME32: Time32Array,
     _Type_TIME64: Time64Array,
+    _Type_HALF_FLOAT: HalfFloatArray,
     _Type_FLOAT: FloatArray,
     _Type_DOUBLE: DoubleArray,
     _Type_LIST: ListArray,
diff --git a/python/pyarrow/includes/common.pxd b/python/pyarrow/includes/common.pxd
index 5afa075..16eae92 100644
--- a/python/pyarrow/includes/common.pxd
+++ b/python/pyarrow/includes/common.pxd
@@ -35,6 +35,9 @@ cdef extern from "<Python.h>":
     void Py_XDECREF(PyObject* o)
     Py_ssize_t Py_REFCNT(PyObject* o)
 
+cdef extern from "numpy/halffloat.h":
+    ctypedef uint16_t npy_half
+
 cdef extern from "arrow/api.h" namespace "arrow" nogil:
     # We can later add more of the common status factory methods as needed
     cdef CStatus CStatus_OK "Status::OK"()
diff --git a/python/pyarrow/includes/libarrow.pxd b/python/pyarrow/includes/libarrow.pxd
index f103249..dbcc94c 100644
--- a/python/pyarrow/includes/libarrow.pxd
+++ b/python/pyarrow/includes/libarrow.pxd
@@ -334,6 +334,9 @@ cdef extern from "arrow/api.h" namespace "arrow" nogil:
     cdef cppclass CTimestampArray" arrow::TimestampArray"(CArray):
         int64_t Value(int i)
 
+    cdef cppclass CHalfFloatArray" arrow::HalfFloatArray"(CArray):
+        uint16_t Value(int i)
+
     cdef cppclass CFloatArray" arrow::FloatArray"(CArray):
         float Value(int i)
 
@@ -875,6 +878,9 @@ cdef extern from "arrow/compute/api.h" namespace "arrow::compute" nogil:
 cdef extern from "arrow/python/api.h" namespace "arrow::py" nogil:
     shared_ptr[CDataType] GetPrimitiveType(Type type)
     shared_ptr[CDataType] GetTimestampType(TimeUnit unit)
+
+    object PyHalf_FromHalf(npy_half value)
+
     CStatus ConvertPySequence(object obj, CMemoryPool* pool,
                               shared_ptr[CArray]* out)
     CStatus ConvertPySequence(object obj, const shared_ptr[CDataType]& type,
diff --git a/python/pyarrow/lib.pxd b/python/pyarrow/lib.pxd
index 7a1b221..4c24b48 100644
--- a/python/pyarrow/lib.pxd
+++ b/python/pyarrow/lib.pxd
@@ -231,6 +231,10 @@ cdef class UInt64Array(IntegerArray):
     pass
 
 
+cdef class HalfFloatArray(FloatingPointArray):
+    pass
+
+
 cdef class FloatArray(FloatingPointArray):
     pass
 
diff --git a/python/pyarrow/scalar.pxi b/python/pyarrow/scalar.pxi
index eb8bb25..6784e56 100644
--- a/python/pyarrow/scalar.pxi
+++ b/python/pyarrow/scalar.pxi
@@ -251,6 +251,13 @@ cdef class TimestampValue(ArrayValue):
         return converter(value, tzinfo=tzinfo)
 
 
+cdef class HalfFloatValue(ArrayValue):
+
+    def as_py(self):
+        cdef CHalfFloatArray* ap = <CHalfFloatArray*> self.sp_array.get()
+        return PyHalf_FromHalf(ap.Value(self.index))
+
+
 cdef class FloatValue(ArrayValue):
 
     def as_py(self):
@@ -397,6 +404,7 @@ cdef dict _scalar_classes = {
     _Type_TIME32: Time32Value,
     _Type_TIME64: Time64Value,
     _Type_TIMESTAMP: TimestampValue,
+    _Type_HALF_FLOAT: HalfFloatValue,
     _Type_FLOAT: FloatValue,
     _Type_DOUBLE: DoubleValue,
     _Type_LIST: ListValue,
diff --git a/python/pyarrow/tests/test_convert_pandas.py b/python/pyarrow/tests/test_convert_pandas.py
index d448de0..45ec66d 100644
--- a/python/pyarrow/tests/test_convert_pandas.py
+++ b/python/pyarrow/tests/test_convert_pandas.py
@@ -462,7 +462,9 @@ class TestConvertPrimitiveTypes(object):
     def test_float_no_nulls(self):
         data = {}
         fields = []
-        dtypes = [('f4', pa.float32()), ('f8', pa.float64())]
+        dtypes = [('f2', pa.float16()),
+                  ('f4', pa.float32()),
+                  ('f8', pa.float64())]
         num_values = 100
 
         for numpy_dtype, arrow_dtype in dtypes:
@@ -478,8 +480,10 @@ class TestConvertPrimitiveTypes(object):
         num_values = 100
 
         null_mask = np.random.randint(0, 10, size=num_values) < 3
-        dtypes = [('f4', pa.float32()), ('f8', pa.float64())]
-        names = ['f4', 'f8']
+        dtypes = [('f2', pa.float16()),
+                  ('f4', pa.float32()),
+                  ('f8', pa.float64())]
+        names = ['f2', 'f4', 'f8']
         expected_cols = []
 
         arrays = []
@@ -653,6 +657,21 @@ class TestConvertPrimitiveTypes(object):
         _check_type(pa.int32())
         _check_type(pa.float64())
 
+    def test_half_floats_from_numpy(self):
+        arr = np.array([1.5, np.nan], dtype=np.float16)
+        a = pa.array(arr, type=pa.float16())
+        x, y = a.to_pylist()
+        assert isinstance(x, np.float16)
+        assert x == 1.5
+        assert isinstance(y, np.float16)
+        assert np.isnan(y)
+
+        a = pa.array(arr, type=pa.float16(), from_pandas=True)
+        x, y = a.to_pylist()
+        assert isinstance(x, np.float16)
+        assert x == 1.5
+        assert y is None
+
 
 @pytest.mark.parametrize('dtype',
                          ['i1', 'i2', 'i4', 'i8', 'u1', 'u2', 'u4', 'u8'])
@@ -1668,7 +1687,7 @@ class TestConvertMisc(object):
         (np.uint16, pa.uint16()),
         (np.uint32, pa.uint32()),
         (np.uint64, pa.uint64()),
-        # (np.float16, pa.float16()),  # XXX unsupported
+        (np.float16, pa.float16()),
         (np.float32, pa.float32()),
         (np.float64, pa.float64()),
         # XXX unsupported
diff --git a/python/pyarrow/tests/test_scalars.py b/python/pyarrow/tests/test_scalars.py
index 23dd237..41057a0 100644
--- a/python/pyarrow/tests/test_scalars.py
+++ b/python/pyarrow/tests/test_scalars.py
@@ -18,6 +18,7 @@
 
 import pytest
 
+import numpy as np
 import pandas as pd
 
 from pyarrow.compat import unittest, u, unicode_type
@@ -76,6 +77,16 @@ class TestScalars(unittest.TestCase):
         v = arr[2]
         assert v.as_py() == 3.0
 
+    def test_half_float(self):
+        arr = pa.array([np.float16(1.5), None], type=pa.float16())
+        v = arr[0]
+        assert isinstance(v, pa.HalfFloatValue)
+        assert repr(v) == "1.5"
+        assert v.as_py() == 1.5
+        assert v == 1.5
+
+        assert arr[1] is pa.NA
+
     def test_string_unicode(self):
         arr = pa.array([u'foo', None, u'maƱana'])
 

-- 
To stop receiving notification emails like this one, please contact
wesm@apache.org.