Merge pull request #194 from apache/req_python
Req python
diff --git a/kll/include/kll_sketch.hpp b/kll/include/kll_sketch.hpp
index af36ceb..bbca76f 100644
--- a/kll/include/kll_sketch.hpp
+++ b/kll/include/kll_sketch.hpp
@@ -203,6 +203,12 @@
bool is_empty() const;
/**
+ * Returns configured parameter k
+ * @return parameter k
+ */
+ uint16_t get_k() const;
+
+ /**
* Returns the length of the input stream.
* @return stream length
*/
diff --git a/kll/include/kll_sketch_impl.hpp b/kll/include/kll_sketch_impl.hpp
index 90d3be7..0e0ef87 100644
--- a/kll/include/kll_sketch_impl.hpp
+++ b/kll/include/kll_sketch_impl.hpp
@@ -234,6 +234,11 @@
}
template<typename T, typename C, typename S, typename A>
+uint16_t kll_sketch<T, C, S, A>::get_k() const {
+ return k_;
+}
+
+template<typename T, typename C, typename S, typename A>
uint64_t kll_sketch<T, C, S, A>::get_n() const {
return n_;
}
diff --git a/kll/test/kll_sketch_test.cpp b/kll/test/kll_sketch_test.cpp
index 13f7673..26ee7dd 100644
--- a/kll/test/kll_sketch_test.cpp
+++ b/kll/test/kll_sketch_test.cpp
@@ -471,6 +471,9 @@
REQUIRE(sketch2.get_min_value() == n);
REQUIRE(sketch2.get_max_value() == 2.0f * n - 1);
+ REQUIRE(sketch1.get_k() == 256);
+ REQUIRE(sketch2.get_k() == 128);
+
REQUIRE(sketch1.get_normalized_rank_error(false) < sketch2.get_normalized_rank_error(false));
REQUIRE(sketch1.get_normalized_rank_error(true) < sketch2.get_normalized_rank_error(true));
diff --git a/python/CMakeLists.txt b/python/CMakeLists.txt
index e1d8eb1..0b76799 100644
--- a/python/CMakeLists.txt
+++ b/python/CMakeLists.txt
@@ -35,6 +35,7 @@
fi
theta
sampling
+ req
pybind11::module
)
@@ -57,5 +58,6 @@
src/fi_wrapper.cpp
src/theta_wrapper.cpp
src/vo_wrapper.cpp
+ src/req_wrapper.cpp
src/vector_of_kll.cpp
)
diff --git a/python/README.md b/python/README.md
index 84e5e61..5153405 100644
--- a/python/README.md
+++ b/python/README.md
@@ -1,4 +1,4 @@
-# Python Wrapper for Apache Datasketches
+# Python Wrapper for Apache DataSketches
## Installation
@@ -43,9 +43,12 @@
## Available Sketch Classes
-- KLL
+- KLL (Absolute Error Quantiles)
- `kll_ints_sketch`
- `kll_floats_sketch`
+- REQ (Relative Error Quantiles)
+ - `req_ints_sketch`
+ - `req_floats_sketch`
- Frequent Items
- `frequent_strings_sketch`
- Error types are `frequent_items_error_type.{NO_FALSE_NEGATIVES | NO_FALSE_POSITIVES}`
diff --git a/python/src/datasketches.cpp b/python/src/datasketches.cpp
index 288c145..4ef491b 100644
--- a/python/src/datasketches.cpp
+++ b/python/src/datasketches.cpp
@@ -27,6 +27,7 @@
void init_cpc(py::module& m);
void init_theta(py::module& m);
void init_vo(py::module& m);
+void init_req(py::module& m);
void init_vector_of_kll(py::module& m);
PYBIND11_MODULE(datasketches, m) {
@@ -36,5 +37,6 @@
init_cpc(m);
init_theta(m);
init_vo(m);
+ init_req(m);
init_vector_of_kll(m);
}
diff --git a/python/src/kll_wrapper.cpp b/python/src/kll_wrapper.cpp
index 8ffdc12..d356358 100644
--- a/python/src/kll_wrapper.cpp
+++ b/python/src/kll_wrapper.cpp
@@ -130,6 +130,8 @@
"Produces a string summary of the sketch")
.def("is_empty", &kll_sketch<T>::is_empty,
"Returns True if the sketch is empty, otherwise False")
+ .def("get_k", &kll_sketch<T>::get_k,
+ "Returns the configured parameter k")
.def("get_n", &kll_sketch<T>::get_n,
"Returns the length of the input stream")
.def("get_num_retained", &kll_sketch<T>::get_num_retained,
@@ -198,7 +200,7 @@
"If pmf is True, returns the 'double-sided' normalized rank error for the get_PMF() function.\n"
"Otherwise, it is the 'single-sided' normalized rank error for all the other queries.\n"
"Constants were derived as the best fit to 99 percentile empirically measured max error in thousands of trials")
- .def("serialize", &dspy::kll_sketch_serialize<T>, "Serailizes the sketch into a bytes object")
+ .def("serialize", &dspy::kll_sketch_serialize<T>, "Serializes the sketch into a bytes object")
.def_static("deserialize", &dspy::kll_sketch_deserialize<T>, "Deserializes the sketch from a bytes object")
;
}
diff --git a/python/src/req_wrapper.cpp b/python/src/req_wrapper.cpp
new file mode 100644
index 0000000..9ef0b87
--- /dev/null
+++ b/python/src/req_wrapper.cpp
@@ -0,0 +1,246 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#include "req_sketch.hpp"
+
+#include <pybind11/pybind11.h>
+#include <pybind11/stl.h>
+#include <pybind11/numpy.h>
+#include <sstream>
+#include <vector>
+
+namespace py = pybind11;
+
+namespace datasketches {
+
+namespace python {
+
+template<typename T>
+req_sketch<T> req_sketch_deserialize(py::bytes sk_bytes) {
+ std::string sk_str = sk_bytes; // implicit cast
+ return req_sketch<T>::deserialize(sk_str.c_str(), sk_str.length());
+}
+
+template<typename T>
+py::object req_sketch_serialize(const req_sketch<T>& sk) {
+ auto ser_result = sk.serialize();
+ return py::bytes((char*)ser_result.data(), ser_result.size());
+}
+
+// maybe possible to disambiguate the static vs method rank error calls, but
+// this is easier for now
+template<typename T>
+double req_sketch_generic_normalized_rank_error(uint16_t k, bool pmf) {
+ return req_sketch<T>::get_normalized_rank_error(k, pmf);
+}
+
+template<typename T>
+double req_sketch_get_rank(const req_sketch<T>& sk,
+ const T& item,
+ bool inclusive) {
+ if (inclusive)
+ return sk.template get_rank<true>(item);
+ else
+ return sk.template get_rank<false>(item);
+}
+
+template<typename T>
+T req_sketch_get_quantile(const req_sketch<T>& sk,
+ double rank,
+ bool inclusive) {
+ if (inclusive)
+ return T(sk.template get_quantile<true>(rank));
+ else
+ return T(sk.template get_quantile<false>(rank));
+}
+
+template<typename T>
+py::list req_sketch_get_quantiles(const req_sketch<T>& sk,
+ std::vector<double>& fractions,
+ bool inclusive) {
+ size_t n_quantiles = fractions.size();
+ auto result = inclusive
+ ? sk.template get_quantiles<true>(&fractions[0], n_quantiles)
+ : sk.template get_quantiles<false>(&fractions[0], n_quantiles);
+
+ // returning as std::vector<> would copy values to a list anyway
+ py::list list(n_quantiles);
+ for (size_t i = 0; i < n_quantiles; ++i) {
+ list[i] = result[i];
+ }
+
+ return list;
+}
+
+template<typename T>
+py::list req_sketch_get_pmf(const req_sketch<T>& sk,
+ std::vector<T>& split_points,
+ bool inclusive) {
+ size_t n_points = split_points.size();
+ auto result = inclusive
+ ? sk.template get_PMF<true>(&split_points[0], n_points)
+ : sk.template get_PMF<false>(&split_points[0], n_points);
+
+ py::list list(n_points + 1);
+ for (size_t i = 0; i <= n_points; ++i) {
+ list[i] = result[i];
+ }
+
+ return list;
+}
+
+template<typename T>
+py::list req_sketch_get_cdf(const req_sketch<T>& sk,
+ std::vector<T>& split_points,
+ bool inclusive) {
+ size_t n_points = split_points.size();
+ auto result = inclusive
+ ? sk.template get_CDF<true>(&split_points[0], n_points)
+ : sk.template get_CDF<false>(&split_points[0], n_points);
+
+ py::list list(n_points + 1);
+ for (size_t i = 0; i <= n_points; ++i) {
+ list[i] = result[i];
+ }
+
+ return list;
+}
+
+template<typename T>
+void req_sketch_update(req_sketch<T>& sk, py::array_t<T, py::array::c_style | py::array::forcecast> items) {
+ if (items.ndim() != 1) {
+ throw std::invalid_argument("input data must have only one dimension. Found: "
+ + std::to_string(items.ndim()));
+ }
+
+ auto data = items.template unchecked<1>();
+ for (uint32_t i = 0; i < data.size(); ++i) {
+ sk.update(data(i));
+ }
+}
+
+}
+}
+
+namespace dspy = datasketches::python;
+
+template<typename T>
+void bind_req_sketch(py::module &m, const char* name) {
+ using namespace datasketches;
+
+ py::class_<req_sketch<T>>(m, name)
+ .def(py::init<uint16_t, bool>(), py::arg("k")=12, py::arg("is_hra")=true)
+ .def(py::init<const req_sketch<T>&>())
+ .def("update", (void (req_sketch<T>::*)(const T&)) &req_sketch<T>::update, py::arg("item"),
+ "Updates the sketch with the given value")
+ .def("update", &dspy::req_sketch_update<T>, py::arg("array"),
+ "Updates the sketch with the values in the given array")
+ .def("merge", (void (req_sketch<T>::*)(const req_sketch<T>&)) &req_sketch<T>::merge, py::arg("sketch"),
+ "Merges the provided sketch into the this one")
+ .def("__str__", &req_sketch<T>::to_string, py::arg("print_levels")=false, py::arg("print_items")=false,
+ "Produces a string summary of the sketch")
+ .def("to_string", &req_sketch<T>::to_string, py::arg("print_levels")=false, py::arg("print_items")=false,
+ "Produces a string summary of the sketch")
+ .def("is_hra", &req_sketch<T>::is_HRA,
+ "Returns True if the sketch is in High Rank Accuracy mode, otherwise False")
+ .def("is_empty", &req_sketch<T>::is_empty,
+ "Returns True if the sketch is empty, otherwise False")
+ .def("get_k", &req_sketch<T>::get_k,
+ "Returns the configured parameter k")
+ .def("get_n", &req_sketch<T>::get_n,
+ "Returns the length of the input stream")
+ .def("get_num_retained", &req_sketch<T>::get_num_retained,
+ "Returns the number of retained items (samples) in the sketch")
+ .def("is_estimation_mode", &req_sketch<T>::is_estimation_mode,
+ "Returns True if the sketch is in estimation mode, otherwise False")
+ .def("get_min_value", &req_sketch<T>::get_min_value,
+ "Returns the minimum value from the stream. If empty, req_floats_sketch returns nan; req_ints_sketch throws a RuntimeError")
+ .def("get_max_value", &req_sketch<T>::get_max_value,
+ "Returns the maximum value from the stream. If empty, req_floats_sketch returns nan; req_ints_sketch throws a RuntimeError")
+ .def("get_quantile", &dspy::req_sketch_get_quantile<T>, py::arg("rank"), py::arg("inclusive")=false,
+ "Returns an approximation to the value of the data item "
+ "that would be preceded by the given fraction of a hypothetical sorted "
+ "version of the input stream so far.\n"
+ "Note that this method has a fairly large overhead (microseconds instead of nanoseconds) "
+ "so it should not be called multiple times to get different quantiles from the same "
+ "sketch. Instead use get_quantiles(), which pays the overhead only once.\n"
+ "For req_floats_sketch: if the sketch is empty this returns nan. "
+ "For req_ints_sketch: if the sketch is empty this throws a RuntimeError.")
+ .def("get_quantiles", &dspy::req_sketch_get_quantiles<T>, py::arg("ranks"), py::arg("inclusive")=false,
+ "This is a more efficient multiple-query version of get_quantile().\n"
+ "This returns an array that could have been generated by using get_quantile() for each "
+ "fractional rank separately, but would be very inefficient. "
+ "This method incurs the internal set-up overhead once and obtains multiple quantile values in "
+ "a single query. It is strongly recommend that this method be used instead of multiple calls "
+ "to get_quantile().\n"
+ "If the sketch is empty this returns an empty vector.")
+ .def("get_rank", &dspy::req_sketch_get_rank<T>, py::arg("item"), py::arg("inclusive")=false,
+ "Returns an approximation to the normalized (fractional) rank of the given value from 0 to 1, inclusive.\n"
+ "The resulting approximation has a probabilistic guarantee that can be obtained from the "
+ "get_normalized_rank_error(False) function.\n"
+ "If the sketch is empty this returns nan.")
+ .def("get_pmf", &dspy::req_sketch_get_pmf<T>, py::arg("split_points"), py::arg("inclusive")=false,
+ "Returns an approximation to the Probability Mass Function (PMF) of the input stream "
+ "given a set of split points (values).\n"
+ "The resulting approximations have a probabilistic guarantee that can be obtained from the "
+ "get_normalized_rank_error(True) function.\n"
+ "If the sketch is empty this returns an empty vector.\n"
+ "split_points is an array of m unique, monotonically increasing float values "
+ "that divide the real number line into m+1 consecutive disjoint intervals.\n"
+ "The definition of an 'interval' is inclusive of the left split point (or minimum value) and "
+ "exclusive of the right split point, with the exception that the last interval will include "
+ "the maximum value.\n"
+ "It is not necessary to include either the min or max values in these split points.")
+ .def("get_cdf", &dspy::req_sketch_get_cdf<T>, py::arg("split_points"), py::arg("inclusive")=false,
+ "Returns an approximation to the Cumulative Distribution Function (CDF), which is the "
+ "cumulative analog of the PMF, of the input stream given a set of split points (values).\n"
+ "The resulting approximations have a probabilistic guarantee that can be obtained from the "
+ "get_normalized_rank_error(True) function.\n"
+ "If the sketch is empty this returns an empty vector.\n"
+ "split_points is an array of m unique, monotonically increasing float values "
+ "that divide the real number line into m+1 consecutive disjoint intervals.\n"
+ "The definition of an 'interval' is inclusive of the left split point (or minimum value) and "
+ "exclusive of the right split point, with the exception that the last interval will include "
+ "the maximum value.\n"
+ "It is not necessary to include either the min or max values in these split points.")
+ .def("get_rank_lower_bound", &req_sketch<T>::get_rank_lower_bound, py::arg("rank"), py::arg("num_std_dev"),
+ "Returns an approximate lower bound on the given normalized rank.\n"
+ "Normalized rank must be a value between 0.0 and 1.0 (inclusive); "
+ "the number of standard deviations must be 1, 2, or 3.")
+ .def("get_rank_upper_bound", &req_sketch<T>::get_rank_upper_bound, py::arg("rank"), py::arg("num_std_dev"),
+ "Returns an approximate upper bound on the given normalized rank.\n"
+ "Normalized rank must be a value between 0.0 and 1.0 (inclusive); "
+ "the number of standard deviations must be 1, 2, or 3.")
+ .def_static("get_RSE", &req_sketch<T>::get_RSE,
+ py::arg("k"), py::arg("rank"), py::arg("is_hra"), py::arg("n"),
+ "Returns an a priori estimate of relative standard error (RSE, expressed as a number in [0,1]). "
+ "Derived from Lemma 12 in http://arxiv.org/abs/2004.01668v2, but the constant factors have been "
+ "modified based on empirical measurements, for a given value of parameter k.\n"
+ "Normalized rank must be a value between 0.0 and 1.0 (inclusive). If is_hra is True, uses high "
+ "rank accuracy mode, else low rank accuracy. N is an estimate of the total number of points "
+ "provided to the sketch.")
+ .def("serialize", &dspy::req_sketch_serialize<T>, "Serializes the sketch into a bytes object")
+ .def_static("deserialize", &dspy::req_sketch_deserialize<T>, "Deserializes the sketch from a bytes object")
+ ;
+}
+
+void init_req(py::module &m) {
+ bind_req_sketch<int>(m, "req_ints_sketch");
+ bind_req_sketch<float>(m, "req_floats_sketch");
+}
diff --git a/python/tests/kll_test.py b/python/tests/kll_test.py
index abe92ec..696260f 100644
--- a/python/tests/kll_test.py
+++ b/python/tests/kll_test.py
@@ -16,9 +16,7 @@
# under the License.
import unittest
-from datasketches import (kll_ints_sketch, kll_floats_sketch,
- vector_of_kll_ints_sketches,
- vector_of_kll_floats_sketches)
+from datasketches import kll_ints_sketch, kll_floats_sketch
import numpy as np
class KllTest(unittest.TestCase):
@@ -59,6 +57,7 @@
self.assertFalse(kll.is_empty())
self.assertTrue(kll.is_estimation_mode())
self.assertEqual(kll.get_n(), n)
+ self.assertEqual(kll.get_k(), k)
self.assertLess(kll.get_num_retained(), n)
# merging itself will double the number of items the sketch has seen
@@ -86,6 +85,7 @@
self.assertEqual(kll.get_n(), n)
self.assertFalse(kll.is_empty())
self.assertFalse(kll.is_estimation_mode()) # n < k
+ self.assertEqual(kll.get_k(), k)
pmf = kll.get_pmf([round(n/2)])
self.assertIsNotNone(pmf)
diff --git a/python/tests/req_test.py b/python/tests/req_test.py
new file mode 100644
index 0000000..1e39bb7
--- /dev/null
+++ b/python/tests/req_test.py
@@ -0,0 +1,126 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import unittest
+from datasketches import req_ints_sketch, req_floats_sketch
+import numpy as np
+
+class reqTest(unittest.TestCase):
+ def test_req_example(self):
+ k = 12
+ n = 2 ** 20
+
+ # create a sketch and inject ~1 million N(0,1) points as an array and as a single item
+ req = req_floats_sketch(k, True) # high rank accuracy
+ req.update(np.random.normal(size=n-1))
+ req.update(0.0)
+
+ # 0 should be near the median
+ self.assertAlmostEqual(0.5, req.get_rank(0.0), delta=0.03)
+
+ # the median should be near 0
+ self.assertAlmostEqual(0.0, req.get_quantile(0.5), delta=0.03)
+
+ # we also track the min/max independently from the rest of the data
+ # which lets us know the full observed data range
+ self.assertLessEqual(req.get_min_value(), req.get_quantile(0.01))
+ self.assertLessEqual(0.0, req.get_rank(req.get_min_value()))
+ self.assertGreaterEqual(req.get_max_value(), req.get_quantile(0.99))
+ self.assertGreaterEqual(1.0, req.get_rank(req.get_max_value()))
+
+ # we can also extract a list of values at a time,
+ # here the values should give us something close to [-2, -1, 0, 1, 2].
+ # then get the CDF, which will return something close to
+ # the original values used in get_quantiles()
+ # finally, can check the normalized rank error bound
+ pts = req.get_quantiles([0.0228, 0.1587, 0.5, 0.8413, 0.9772])
+ cdf = req.get_cdf(pts) # include 1.0 at end to account for all probability mass
+ self.assertEqual(len(cdf), len(pts)+1)
+
+ # For relative error quantiles, the error depends on the actual rank
+ # so we need to use that to detemrine the bounds
+ est = req.get_rank(0.999, True)
+ lb = req.get_rank_lower_bound(est, 1)
+ ub = req.get_rank_upper_bound(est, 1)
+ self.assertLessEqual(lb, est)
+ self.assertLessEqual(est, ub)
+
+ # and a few basic queries about the sketch
+ self.assertFalse(req.is_empty())
+ self.assertTrue(req.is_estimation_mode())
+ self.assertEqual(req.get_n(), n)
+ self.assertLess(req.get_num_retained(), n)
+ self.assertEqual(req.get_k(), k)
+
+ # merging itself will double the number of items the sketch has seen
+ req.merge(req)
+ self.assertEqual(req.get_n(), 2*n)
+
+ # we can then serialize and reconstruct the sketch
+ req_bytes = req.serialize()
+ new_req = req.deserialize(req_bytes)
+ self.assertEqual(req.get_num_retained(), new_req.get_num_retained())
+ self.assertEqual(req.get_min_value(), new_req.get_min_value())
+ self.assertEqual(req.get_max_value(), new_req.get_max_value())
+ self.assertEqual(req.get_quantile(0.7), new_req.get_quantile(0.7))
+ self.assertEqual(req.get_rank(0.0), new_req.get_rank(0.0))
+
+ def test_req_ints_sketch(self):
+ k = 100
+ n = 10
+ req = req_ints_sketch(k)
+ for i in range(0, n):
+ req.update(i)
+
+ self.assertEqual(req.get_min_value(), 0)
+ self.assertEqual(req.get_max_value(), n-1)
+ self.assertEqual(req.get_n(), n)
+ self.assertFalse(req.is_empty())
+ self.assertFalse(req.is_estimation_mode()) # n < k
+ self.assertEqual(req.get_k(), k)
+
+ pmf = req.get_pmf([round(n/2)])
+ self.assertIsNotNone(pmf)
+ self.assertEqual(len(pmf), 2)
+
+ cdf = req.get_cdf([round(n/2)])
+ self.assertIsNotNone(cdf)
+ self.assertEqual(len(cdf), 2)
+
+ self.assertEqual(req.get_quantile(0.5), round(n/2))
+ quants = req.get_quantiles([0.25, 0.5, 0.75])
+ self.assertIsNotNone(quants)
+ self.assertEqual(len(quants), 3)
+
+ self.assertEqual(req.get_rank(round(n/2)), 0.5)
+
+ # merge self
+ req.merge(req)
+ self.assertEqual(req.get_n(), 2 * n)
+
+ sk_bytes = req.serialize()
+ self.assertTrue(isinstance(req_ints_sketch.deserialize(sk_bytes), req_ints_sketch))
+
+ def test_req_floats_sketch(self):
+ # already tested ints and it's templatized, so just make sure it instantiates properly
+ k = 75
+ req = req_floats_sketch(k, False) # low rank accuracy
+ self.assertTrue(req.is_empty())
+ self.assertFalse(req.is_hra())
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/setup.py b/setup.py
index 83cd994..5fb3cd8 100644
--- a/setup.py
+++ b/setup.py
@@ -80,9 +80,9 @@
setup(
name='datasketches',
version='2.2.0.dev0',
- author='Apache Datasketches Developers',
+ author='Apache DataSketches Developers',
author_email='dev@datasketches.apache.org',
- description='A wrapper for the C++ Apache Datasketches library',
+ description='A wrapper for the C++ Apache DataSketches library',
license='Apache License 2.0',
url='http://datasketches.apache.org',
long_description=open('python/README.md').read(),