From dad1e0bdb2cf0bf7c3fe68d2bdf3def32490f6b5 Mon Sep 17 00:00:00 2001 From: Oscar Nydza <33619748+nipsn@users.noreply.github.com> Date: Mon, 22 Jan 2024 16:26:59 +0100 Subject: [PATCH] Added implementation of round (#21) * Added round implementation, tests and documentation * Typo in round documentation * Type checking with isinstance * fix linting issues * fix linting issues * reworked round implementation and tests * Refactored column updates and fixed typos --- docs/user-guide/advanced/Pandas_API.ipynb | 62 +++++++++++++++++++++++ src/pykx/pandas_api/pandas_meta.py | 46 +++++++++++++++++ tests/test_pandas_api.py | 35 +++++++++++++ 3 files changed, 143 insertions(+) diff --git a/docs/user-guide/advanced/Pandas_API.ipynb b/docs/user-guide/advanced/Pandas_API.ipynb index 7d6449d..a240a90 100644 --- a/docs/user-guide/advanced/Pandas_API.ipynb +++ b/docs/user-guide/advanced/Pandas_API.ipynb @@ -2472,6 +2472,68 @@ "tab.abs(numeric_only=True)" ] }, + { + "cell_type": "markdown", + "id": "0c056fd9-fe7b-43d5-b1c7-7ceec3cae5ff", + "metadata": {}, + "source": [ + "### Table.round()\n", + "\n", + "```\n", + "Table.round(self, decimals: Union[int, Dict[str, int]] = 0)\n", + "```\n", + "\n", + "Round a Table to a variable number of decimal places.\n", + "\n", + "**Parameters:**\n", + "\n", + "| Name | Type | Description | Default |\n", + "| :--------------: | :-----------------: | :------------------------------------------------------------ | :-----: |\n", + "| decimals | int or Dict | Number of decimal places to round each column to. If an int is given, round each real or float column to the same number of places. Otherwise, dict rounds to variable numbers of places. Column names should be in the keys if decimals parameter is a dict-like and the decimals to round should be the value. Any columns not included in decimals will be left as is. Elements of decimals which are not columns of the input will be ignored.| 0 |\n", + "\n", + "**Returns:**\n", + "\n", + "| Type | Description |\n", + "| :--------: | :--------------------------------------------------------------------------------------- |\n", + "| Table | A Table with the affected columns rounded to the specified number of decimal places. |" + ] + }, + { + "cell_type": "markdown", + "id": "1b629def", + "metadata": {}, + "source": [ + "If an integer is provided it rounds every float column to set decimals." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "08c182c9", + "metadata": {}, + "outputs": [], + "source": [ + "tab.round(1)" + ] + }, + { + "cell_type": "markdown", + "id": "28853fc0", + "metadata": {}, + "source": [ + "If a dict whose keys are the column names and its values are the decimals to round set column is provided, it will round them accordingly.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7640df4c", + "metadata": {}, + "outputs": [], + "source": [ + "tab.round({\"price\": 1, \"traded\": 0})" + ] + }, { "cell_type": "markdown", "id": "cbcdf84e", diff --git a/src/pykx/pandas_api/pandas_meta.py b/src/pykx/pandas_api/pandas_meta.py index f2a4128..fab85fb 100644 --- a/src/pykx/pandas_api/pandas_meta.py +++ b/src/pykx/pandas_api/pandas_meta.py @@ -1,3 +1,5 @@ +from typing import Dict, Union + from . import api_return from ..exceptions import QError @@ -104,6 +106,28 @@ def inner(*args, **kwargs): '': b'kx.List'} +# Define the mapping between the returns of kx.*Vector.t and the associated typechar +_typenum_to_typechar_mapping = {0: '', + 1: 'b', + 2: 'g', + 4: 'x', + 5: 'h', + 6: 'i', + 7: 'j', + 8: 'e', + 9: 'f', + 10: 'c', + 11: 's', + 12: 'p', + 14: 'd', + 15: 'z', + 16: 'n', + 17: 'u', + 18: 'v', + 19: 't', + 13: 'm'} + + class PandasMeta: # Dataframe properties @property @@ -265,6 +289,28 @@ def abs(self, numeric_only=False): tab = _get_numeric_only_subtable(self) return q.abs(tab) + @api_return + def round(self, decimals: Union[int, Dict[str, int]] = 0): + tab = self + if 'Keyed' in str(type(tab)): + tab = q.value(tab) + + affected_cols = _get_numeric_only_subtable(tab).columns.py() + type_dict = {col: _typenum_to_typechar_mapping[tab[col].t] for col in affected_cols} + + cast_back = q('{string[y][0]$x}') + + if isinstance(decimals, int): + dec_dict = {col: decimals for col in affected_cols} + else: + dec_dict = {col: decimals[col] for col in affected_cols} + + rounded = {col: [cast_back(round(elem, dec_dict[col]), type_dict[col]) + for elem in tab[col]] + for col in dec_dict} + + return q.qsql.update(tab, columns=rounded) + @convert_result def all(self, axis=0, bool_only=False, skipna=True): res, cols = preparse_computations(self, axis, skipna, bool_only=bool_only) diff --git a/tests/test_pandas_api.py b/tests/test_pandas_api.py index 479d428..0a1dbf8 100644 --- a/tests/test_pandas_api.py +++ b/tests/test_pandas_api.py @@ -1937,6 +1937,41 @@ def test_pandas_abs(kx, q): tab.abs() +def test_pandas_round(kx, q): + q_tab = q('([]c1:4 5 10 15 20 25h;' + 'c2:4 5 10 15 20 25i;' + 'c3:4 5 10 15 20 25j;' + 'c4:0 0.10 0.25 0.30 0.45 0.50e;' + 'c5:0 0.10 0.25 0.30 0.45 0.50f;' + 'c6:`a`b`c`d`e`f)') + p_tab = q_tab.pd() + + pd.testing.assert_frame_equal(p_tab.round(), + q_tab.round().pd()) + + pd.testing.assert_frame_equal(q_tab.round(0).pd(), + q_tab.round().pd()) + + pd.testing.assert_frame_equal(p_tab.round(2), + q_tab.round(2).pd()) + + pd.testing.assert_frame_equal(p_tab.round(-1), + q_tab.round(-1).pd()) + + dict_test = {'c1': -2, + 'c2': -1, + 'c3': -0, + 'c4': 1, + 'c5': 2, + 'c6': 3, + 'c7': 4} + + q_res = q_tab.round(dict_test) + pd.testing.assert_frame_equal(p_tab.round(dict_test), q_res.pd()) + + pd.testing.assert_frame_equal(q_tab.dtypes.pd(), q_res.dtypes.pd()) + + def test_pandas_min(q): tab = q('([] sym: 100?`foo`bar`baz`qux; price: 250.0f - 100?500.0f; ints: 100 - 100?200)') df = tab.pd()