From 86cccf2d2f9473cbeba0c7aa6f872612d47cfe33 Mon Sep 17 00:00:00 2001 From: dawid-januszkiewicz Date: Fri, 10 Mar 2023 14:15:35 +0000 Subject: [PATCH 01/11] feat(rust,python): option to right-align numeric columns --- crates/polars-core/src/config.rs | 1 + crates/polars-core/src/fmt.rs | 45 ++++++++--- py-polars/polars/config.py | 76 +++++++++++++++++- py-polars/src/functions/meta.rs | 18 +++++ py-polars/src/lib.rs | 4 + py-polars/tests/unit/test_cfg.py | 131 ++++++++++++++++++++++++++++++- 6 files changed, 262 insertions(+), 13 deletions(-) diff --git a/crates/polars-core/src/config.rs b/crates/polars-core/src/config.rs index d94113724406..765ba090ca36 100644 --- a/crates/polars-core/src/config.rs +++ b/crates/polars-core/src/config.rs @@ -3,6 +3,7 @@ pub(crate) const FMT_MAX_COLS: &str = "POLARS_FMT_MAX_COLS"; pub(crate) const FMT_MAX_ROWS: &str = "POLARS_FMT_MAX_ROWS"; pub(crate) const FMT_STR_LEN: &str = "POLARS_FMT_STR_LEN"; pub(crate) const FMT_TABLE_CELL_ALIGNMENT: &str = "POLARS_FMT_TABLE_CELL_ALIGNMENT"; +pub(crate) const FMT_TABLE_CELL_NUMERIC_ALIGNMENT: &str = "POLARS_FMT_TABLE_CELL_NUMERIC_ALIGNMENT"; pub(crate) const FMT_TABLE_DATAFRAME_SHAPE_BELOW: &str = "POLARS_FMT_TABLE_DATAFRAME_SHAPE_BELOW"; pub(crate) const FMT_TABLE_FORMATTING: &str = "POLARS_FMT_TABLE_FORMATTING"; pub(crate) const FMT_TABLE_HIDE_COLUMN_DATA_TYPES: &str = "POLARS_FMT_TABLE_HIDE_COLUMN_DATA_TYPES"; diff --git a/crates/polars-core/src/fmt.rs b/crates/polars-core/src/fmt.rs index 59c69f8cdc95..f4e9729cb430 100644 --- a/crates/polars-core/src/fmt.rs +++ b/crates/polars-core/src/fmt.rs @@ -33,6 +33,7 @@ pub enum FloatFmt { Full, } static FLOAT_FMT: AtomicU8 = AtomicU8::new(FloatFmt::Mixed as u8); +static FLOAT_PRECISION: AtomicU8 = AtomicU8::new(u8::MAX); pub fn get_float_fmt() -> FloatFmt { match FLOAT_FMT.load(Ordering::Relaxed) { @@ -42,10 +43,18 @@ pub fn get_float_fmt() -> FloatFmt { } } +pub fn get_float_precision() -> u8 { + FLOAT_PRECISION.load(Ordering::Relaxed) +} + pub fn set_float_fmt(fmt: FloatFmt) { FLOAT_FMT.store(fmt as u8, Ordering::Relaxed) } +pub fn set_float_precision(precision: u8) { + FLOAT_PRECISION.store(precision, Ordering::Relaxed) +} + macro_rules! format_array { ($f:ident, $a:expr, $dtype:expr, $name:expr, $array_type:expr) => {{ write!( @@ -655,19 +664,24 @@ impl Display for DataFrame { } // set alignment of cells, if defined - if std::env::var(FMT_TABLE_CELL_ALIGNMENT).is_ok() { - // for (column_index, column) in table.column_iter_mut().enumerate() { + if std::env::var(FMT_TABLE_CELL_ALIGNMENT).is_ok() + | std::env::var(FMT_TABLE_CELL_NUMERIC_ALIGNMENT).is_ok() + { let str_preset = std::env::var(FMT_TABLE_CELL_ALIGNMENT) .unwrap_or_else(|_| "DEFAULT".to_string()); - for column in table.column_iter_mut() { - if str_preset == "RIGHT" { - column.set_cell_alignment(CellAlignment::Right); - } else if str_preset == "LEFT" { - column.set_cell_alignment(CellAlignment::Left); - } else if str_preset == "CENTER" { - column.set_cell_alignment(CellAlignment::Center); - } else { - column.set_cell_alignment(CellAlignment::Left); + let num_preset = std::env::var(FMT_TABLE_CELL_NUMERIC_ALIGNMENT) + .unwrap_or_else(|_| str_preset.to_string()); + for (column_index, column) in table.column_iter_mut().enumerate() { + let dtype = fields[column_index].data_type(); + let mut preset = str_preset.as_str(); + if dtype.to_physical().is_numeric() { + preset = num_preset.as_str(); + } + match preset { + "RIGHT" => column.set_cell_alignment(CellAlignment::Right), + "LEFT" => column.set_cell_alignment(CellAlignment::Left), + "CENTER" => column.set_cell_alignment(CellAlignment::Center), + _ => {}, } } } @@ -709,6 +723,15 @@ const SCIENTIFIC_BOUND: f64 = 999999.0; fn fmt_float(f: &mut Formatter<'_>, width: usize, v: T) -> fmt::Result { let v: f64 = NumCast::from(v).unwrap(); + + let precision = get_float_precision(); + if precision != u8::MAX { + if format!("{v:.precision$}", precision = precision as usize).len() > 19 { + return write!(f, "{v:>width$.precision$e}", precision = precision as usize); + } + return write!(f, "{v:>width$.precision$}", precision = precision as usize); + } + if matches!(get_float_fmt(), FloatFmt::Full) { return write!(f, "{v:>width$}"); } diff --git a/py-polars/polars/config.py b/py-polars/polars/config.py index 2ca55f2bd123..c7c09df6e9c5 100644 --- a/py-polars/polars/config.py +++ b/py-polars/polars/config.py @@ -16,10 +16,18 @@ def _get_float_fmt() -> str: # pragma: no cover return "n/a" +def _get_float_precision() -> str: + return "n/a" + + # note: module not available when building docs with contextlib.suppress(ImportError): from polars.polars import get_float_fmt as _get_float_fmt # type: ignore[no-redef] + from polars.polars import ( # type: ignore[no-redef] + get_float_precision as _get_float_precision, + ) from polars.polars import set_float_fmt as _set_float_fmt + from polars.polars import set_float_precision as _set_float_precision if sys.version_info >= (3, 10): @@ -60,7 +68,9 @@ def _get_float_fmt() -> str: # pragma: no cover "POLARS_FMT_MAX_COLS", "POLARS_FMT_MAX_ROWS", "POLARS_FMT_STR_LEN", + "POLARS_FMT_NUM_LEN", "POLARS_FMT_TABLE_CELL_ALIGNMENT", + "POLARS_FMT_TABLE_CELL_NUMERIC_ALIGNMENT", "POLARS_FMT_TABLE_DATAFRAME_SHAPE_BELOW", "POLARS_FMT_TABLE_FORMATTING", "POLARS_FMT_TABLE_HIDE_COLUMN_DATA_TYPES", @@ -76,7 +86,10 @@ def _get_float_fmt() -> str: # pragma: no cover # vars that set the rust env directly should declare themselves here as the Config # method name paired with a callable that returns the current state of that value: -_POLARS_CFG_DIRECT_VARS = {"set_fmt_float": _get_float_fmt} +_POLARS_CFG_DIRECT_VARS = { + "set_fmt_float": _get_float_fmt, + "set_float_precision": _get_float_precision, +} class Config(contextlib.ContextDecorator): @@ -252,6 +265,7 @@ def restore_defaults(cls) -> type[Config]: # apply any 'direct' setting values cls.set_fmt_float() + cls.set_float_precision() return cls @classmethod @@ -427,6 +441,26 @@ def set_auto_structify(cls, active: bool | None = False) -> type[Config]: os.environ["POLARS_AUTO_STRUCTIFY"] = str(int(active)) return cls + @classmethod + def set_float_precision(cls, precision: int | None = None) -> type[Config]: + """ + Control how floating point values are displayed. + + Parameters + ---------- + precision : int + Number of decimal places to display + + """ + if precision is None: + precision = 255 + elif precision > 16 and precision != 255: + raise ValueError( + f"precision must be None, or a number between 0-16; got {precision}" + ) + _set_float_precision(precision) + return cls + @classmethod def set_fmt_float(cls, fmt: FloatFmt | None = "mixed") -> type[Config]: """ @@ -598,6 +632,46 @@ def set_tbl_cell_alignment( os.environ["POLARS_FMT_TABLE_CELL_ALIGNMENT"] = format return cls + @classmethod + def set_tbl_cell_numeric_alignment( + cls, format: Literal["LEFT", "CENTER", "RIGHT"] + ) -> type[Config]: + """ + Set table cell alignment for numeric columns. + + Parameters + ---------- + format : str + * "LEFT": left aligned + * "CENTER": center aligned + * "RIGHT": right aligned + + Examples + -------- + >>> df = pl.DataFrame( + ... {"column_abc": [11, 2, 333], "column_xyz": [True, False, True]} + ... ) + >>> pl.Config.set_tbl_cell_numeric_alignment("RIGHT") # doctest: +SKIP + # ... + # shape: (3, 2) + # ┌────────────┬────────────┐ + # │ column_abc ┆ column_xyz │ + # │ --- ┆ --- │ + # │ i64 ┆ bool │ + # ╞════════════╪════════════╡ + # │ 11 ┆ true │ + # │ 2 ┆ false │ + # │ 333 ┆ true │ + # └────────────┴────────────┘ + + Raises + ------ + KeyError: if alignment string not recognised. + + """ + os.environ["POLARS_FMT_TABLE_CELL_NUMERIC_ALIGNMENT"] = format + return cls + @classmethod def set_tbl_cols(cls, n: int | None) -> type[Config]: """ diff --git a/py-polars/src/functions/meta.rs b/py-polars/src/functions/meta.rs index 467c65ffc133..721ca777b724 100644 --- a/py-polars/src/functions/meta.rs +++ b/py-polars/src/functions/meta.rs @@ -46,3 +46,21 @@ pub fn get_float_fmt() -> PyResult { }; Ok(strfmt.to_string()) } + +#[pyfunction] +pub fn set_float_precision(precision: u8) -> PyResult<()> { + use polars_core::fmt::set_float_precision; + if precision > 16 && precision != u8::MAX { + return Err(PyValueError::new_err(format!( + "maximum supported float precision is 16, got {precision}", + ))); + } + set_float_precision(precision); + Ok(()) +} + +#[pyfunction] +pub fn get_float_precision() -> PyResult { + use polars_core::fmt::get_float_precision; + Ok(get_float_precision()) +} diff --git a/py-polars/src/lib.rs b/py-polars/src/lib.rs index 6e7e3619297a..9a0e72aff086 100644 --- a/py-polars/src/lib.rs +++ b/py-polars/src/lib.rs @@ -230,6 +230,10 @@ fn polars(py: Python, m: &PyModule) -> PyResult<()> { .unwrap(); m.add_wrapped(wrap_pyfunction!(functions::meta::get_float_fmt)) .unwrap(); + m.add_wrapped(wrap_pyfunction!(functions::meta::set_float_precision)) + .unwrap(); + m.add_wrapped(wrap_pyfunction!(functions::meta::get_float_precision)) + .unwrap(); // Functions - misc m.add_wrapped(wrap_pyfunction!(functions::misc::dtype_str_repr)) diff --git a/py-polars/tests/unit/test_cfg.py b/py-polars/tests/unit/test_cfg.py index 72a9e22d276a..17c9f4548b49 100644 --- a/py-polars/tests/unit/test_cfg.py +++ b/py-polars/tests/unit/test_cfg.py @@ -7,7 +7,9 @@ import pytest import polars as pl -from polars.config import _POLARS_CFG_ENV_VARS, _get_float_fmt +from polars.config import _POLARS_CFG_ENV_VARS, _get_float_fmt, _get_float_precision +from polars.exceptions import StringCacheMismatchError +from polars.testing import assert_frame_equal @pytest.fixture(autouse=True) @@ -509,6 +511,128 @@ def test_shape_format_for_big_numbers() -> None: ) +def test_numeric_right_alignment() -> None: + pl.Config.set_tbl_cell_numeric_alignment("RIGHT") + + df = pl.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6], "c": [7, 8, 9]}) + assert ( + str(df) == "shape: (3, 3)\n" + "┌─────┬─────┬─────┐\n" + "│ a ┆ b ┆ c │\n" + "│ --- ┆ --- ┆ --- │\n" + "│ i64 ┆ i64 ┆ i64 │\n" + "╞═════╪═════╪═════╡\n" + "│ 1 ┆ 4 ┆ 7 │\n" + "│ 2 ┆ 5 ┆ 8 │\n" + "│ 3 ┆ 6 ┆ 9 │\n" + "└─────┴─────┴─────┘" + ) + + df = pl.DataFrame( + {"a": [1.1, 2.22, 3.333], "b": [4.0, 5.0, 6.0], "c": [7.0, 8.0, 9.0]} + ) + with pl.Config(): + pl.Config.set_fmt_float("full") + assert ( + str(df) == "shape: (3, 3)\n" + "┌───────┬─────┬─────┐\n" + "│ a ┆ b ┆ c │\n" + "│ --- ┆ --- ┆ --- │\n" + "│ f64 ┆ f64 ┆ f64 │\n" + "╞═══════╪═════╪═════╡\n" + "│ 1.1 ┆ 4 ┆ 7 │\n" + "│ 2.22 ┆ 5 ┆ 8 │\n" + "│ 3.333 ┆ 6 ┆ 9 │\n" + "└───────┴─────┴─────┘" + ) + + with pl.Config(): + pl.Config.set_fmt_float("mixed") + assert ( + str(df) == "shape: (3, 3)\n" + "┌───────┬─────┬─────┐\n" + "│ a ┆ b ┆ c │\n" + "│ --- ┆ --- ┆ --- │\n" + "│ f64 ┆ f64 ┆ f64 │\n" + "╞═══════╪═════╪═════╡\n" + "│ 1.1 ┆ 4.0 ┆ 7.0 │\n" + "│ 2.22 ┆ 5.0 ┆ 8.0 │\n" + "│ 3.333 ┆ 6.0 ┆ 9.0 │\n" + "└───────┴─────┴─────┘" + ) + + df = pl.DataFrame( + {"a": [1.1, 22.2, 3.33], "b": [444, 55.5, 6.6], "c": [77.7, 8888, 9.9999]} + ) + with pl.Config(): + pl.Config.set_fmt_float("full") + pl.Config.set_float_precision(1) + assert ( + str(df) == "shape: (3, 3)\n" + "┌──────┬───────┬────────┐\n" + "│ a ┆ b ┆ c │\n" + "│ --- ┆ --- ┆ --- │\n" + "│ f64 ┆ f64 ┆ f64 │\n" + "╞══════╪═══════╪════════╡\n" + "│ 1.1 ┆ 444.0 ┆ 77.7 │\n" + "│ 22.2 ┆ 55.5 ┆ 8888.0 │\n" + "│ 3.3 ┆ 6.6 ┆ 10.0 │\n" + "└──────┴───────┴────────┘" + ) + + df = pl.DataFrame( + { + "a": [1100000000000000000.1, 22200000000000000.2, 33330000000000000.33333], + "b": [40000000000000000000.0, 5, 600000000000000000.0], + "c": [700000.0, 80000000000000000.0, 900], + } + ) + with pl.Config(): + pl.Config.set_float_precision(2) + assert ( + str(df) == "shape: (3, 3)\n" + "┌─────────┬─────────┬───────────┐\n" + "│ a ┆ b ┆ c │\n" + "│ --- ┆ --- ┆ --- │\n" + "│ f64 ┆ f64 ┆ f64 │\n" + "╞═════════╪═════════╪═══════════╡\n" + "│ 1.10e18 ┆ 4.00e19 ┆ 700000.00 │\n" + "│ 2.22e16 ┆ 5.00 ┆ 8.00e16 │\n" + "│ 3.33e16 ┆ 6.00e17 ┆ 900.00 │\n" + "└─────────┴─────────┴───────────┘" + ) + # test nonsensical float precision raises an error + with pytest.raises(ValueError): + pl.Config.set_float_precision(50) + + +def test_string_cache() -> None: + df1 = pl.DataFrame({"a": ["foo", "bar", "ham"], "b": [1, 2, 3]}) + df2 = pl.DataFrame({"a": ["foo", "spam", "eggs"], "c": [3, 2, 2]}) + + # ensure cache is off when casting to categorical; the join will fail + pl.enable_string_cache(False) + assert pl.using_string_cache() is False + + df1a = df1.with_columns(pl.col("a").cast(pl.Categorical)) + df2a = df2.with_columns(pl.col("a").cast(pl.Categorical)) + with pytest.raises(StringCacheMismatchError): + _ = df1a.join(df2a, on="a", how="inner") + + # now turn on the cache + pl.enable_string_cache(True) + assert pl.using_string_cache() is True + + df1b = df1.with_columns(pl.col("a").cast(pl.Categorical)) + df2b = df2.with_columns(pl.col("a").cast(pl.Categorical)) + out = df1b.join(df2b, on="a", how="inner") + + expected = pl.DataFrame( + {"a": ["foo"], "b": [1], "c": [3]}, schema_overrides={"a": pl.Categorical} + ) + assert_frame_equal(out, expected) + + @pytest.mark.write_disk() def test_config_load_save(tmp_path: Path) -> None: for file in ( @@ -520,6 +644,7 @@ def test_config_load_save(tmp_path: Path) -> None: pl.Config.set_tbl_cols(12) pl.Config.set_verbose(True) pl.Config.set_fmt_float("full") + pl.Config.set_float_precision(6) assert os.environ.get("POLARS_VERBOSE") == "1" if file is None: @@ -533,6 +658,8 @@ def test_config_load_save(tmp_path: Path) -> None: # ...modify the same options... pl.Config.set_tbl_cols(10) pl.Config.set_verbose(False) + pl.Config.set_fmt_float("mixed") + pl.Config.set_float_precision("2") assert os.environ.get("POLARS_VERBOSE") == "0" # ...load back from config file/string... @@ -555,6 +682,7 @@ def test_config_load_save(tmp_path: Path) -> None: assert os.environ.get("POLARS_FMT_MAX_COLS") == "12" assert os.environ.get("POLARS_VERBOSE") == "1" assert _get_float_fmt() == "full" + assert _get_float_precision() == 6 # restore all default options (unsets from env) pl.Config.restore_defaults() @@ -565,6 +693,7 @@ def test_config_load_save(tmp_path: Path) -> None: assert os.environ.get("POLARS_FMT_MAX_COLS") is None assert os.environ.get("POLARS_VERBOSE") is None assert _get_float_fmt() == "mixed" + assert _get_float_precision() == 255 # ref: #11094 with pl.Config( From 302ee682118f739663c8d805b1e465db9fbfe0ea Mon Sep 17 00:00:00 2001 From: alexander-beedie Date: Sun, 27 Aug 2023 15:11:42 +0400 Subject: [PATCH 02/11] minor typing lint --- py-polars/polars/config.py | 8 ++++---- py-polars/tests/unit/test_cfg.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/py-polars/polars/config.py b/py-polars/polars/config.py index c7c09df6e9c5..a590fe06f942 100644 --- a/py-polars/polars/config.py +++ b/py-polars/polars/config.py @@ -11,13 +11,13 @@ from polars.utils.various import normalize_filepath -# dummy func required (so docs build) +# dummy funcs required here (so that docs build) def _get_float_fmt() -> str: # pragma: no cover return "n/a" -def _get_float_precision() -> str: - return "n/a" +def _get_float_precision() -> int: + return 0 # note: module not available when building docs @@ -361,7 +361,7 @@ def state( } if not env_only: for cfg_methodname, get_value in _POLARS_CFG_DIRECT_VARS.items(): - config_state[cfg_methodname] = get_value() + config_state[cfg_methodname] = get_value() # type: ignore[assignment] return config_state diff --git a/py-polars/tests/unit/test_cfg.py b/py-polars/tests/unit/test_cfg.py index 17c9f4548b49..aaddf6ba93ac 100644 --- a/py-polars/tests/unit/test_cfg.py +++ b/py-polars/tests/unit/test_cfg.py @@ -659,7 +659,7 @@ def test_config_load_save(tmp_path: Path) -> None: pl.Config.set_tbl_cols(10) pl.Config.set_verbose(False) pl.Config.set_fmt_float("mixed") - pl.Config.set_float_precision("2") + pl.Config.set_float_precision(2) assert os.environ.get("POLARS_VERBOSE") == "0" # ...load back from config file/string... From 89b1e6444b81600318802752d3598e67aeb697c8 Mon Sep 17 00:00:00 2001 From: alexander-beedie Date: Sun, 27 Aug 2023 18:49:46 +0400 Subject: [PATCH 03/11] bonus unit test --- py-polars/tests/unit/test_cfg.py | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/py-polars/tests/unit/test_cfg.py b/py-polars/tests/unit/test_cfg.py index aaddf6ba93ac..5f339bd8ce12 100644 --- a/py-polars/tests/unit/test_cfg.py +++ b/py-polars/tests/unit/test_cfg.py @@ -546,8 +546,7 @@ def test_numeric_right_alignment() -> None: "└───────┴─────┴─────┘" ) - with pl.Config(): - pl.Config.set_fmt_float("mixed") + with pl.Config(fmt_float="mixed"): assert ( str(df) == "shape: (3, 3)\n" "┌───────┬─────┬─────┐\n" @@ -561,12 +560,23 @@ def test_numeric_right_alignment() -> None: "└───────┴─────┴─────┘" ) + with pl.Config(float_precision=6): + assert str(df) == ( + "shape: (3, 3)\n" + "┌──────────┬──────────┬──────────┐\n" + "│ a ┆ b ┆ c │\n" + "│ --- ┆ --- ┆ --- │\n" + "│ f64 ┆ f64 ┆ f64 │\n" + "╞══════════╪══════════╪══════════╡\n" + "│ 1.100000 ┆ 4.000000 ┆ 7.000000 │\n" + "│ 2.220000 ┆ 5.000000 ┆ 8.000000 │\n" + "│ 3.333000 ┆ 6.000000 ┆ 9.000000 │\n" + "└──────────┴──────────┴──────────┘" + ) df = pl.DataFrame( {"a": [1.1, 22.2, 3.33], "b": [444, 55.5, 6.6], "c": [77.7, 8888, 9.9999]} ) - with pl.Config(): - pl.Config.set_fmt_float("full") - pl.Config.set_float_precision(1) + with pl.Config(fmt_float="full", float_precision=1): assert ( str(df) == "shape: (3, 3)\n" "┌──────┬───────┬────────┐\n" @@ -587,8 +597,7 @@ def test_numeric_right_alignment() -> None: "c": [700000.0, 80000000000000000.0, 900], } ) - with pl.Config(): - pl.Config.set_float_precision(2) + with pl.Config(float_precision=2): assert ( str(df) == "shape: (3, 3)\n" "┌─────────┬─────────┬───────────┐\n" @@ -601,6 +610,7 @@ def test_numeric_right_alignment() -> None: "│ 3.33e16 ┆ 6.00e17 ┆ 900.00 │\n" "└─────────┴─────────┴───────────┘" ) + # test nonsensical float precision raises an error with pytest.raises(ValueError): pl.Config.set_float_precision(50) From 906f1fbd95d8729a401da98f80510b1800d881fa Mon Sep 17 00:00:00 2001 From: Alexander Beedie Date: Tue, 5 Sep 2023 08:26:48 +0000 Subject: [PATCH 04/11] only check `is_numeric` (not `to_physical`) --- crates/polars-core/src/fmt.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/polars-core/src/fmt.rs b/crates/polars-core/src/fmt.rs index f4e9729cb430..7e18929829ce 100644 --- a/crates/polars-core/src/fmt.rs +++ b/crates/polars-core/src/fmt.rs @@ -674,7 +674,7 @@ impl Display for DataFrame { for (column_index, column) in table.column_iter_mut().enumerate() { let dtype = fields[column_index].data_type(); let mut preset = str_preset.as_str(); - if dtype.to_physical().is_numeric() { + if dtype.is_numeric() { preset = num_preset.as_str(); } match preset { From de85417c0110b85deeec3714cd4c308c456bb515 Mon Sep 17 00:00:00 2001 From: Alexander Beedie Date: Tue, 5 Sep 2023 08:31:08 +0000 Subject: [PATCH 05/11] update docstring example --- py-polars/polars/config.py | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/py-polars/polars/config.py b/py-polars/polars/config.py index a590fe06f942..81c6659feded 100644 --- a/py-polars/polars/config.py +++ b/py-polars/polars/config.py @@ -648,21 +648,26 @@ def set_tbl_cell_numeric_alignment( Examples -------- + >>> from datetime import date >>> df = pl.DataFrame( - ... {"column_abc": [11, 2, 333], "column_xyz": [True, False, True]} + ... { + ... "abc": [11, 2, 333], + ... "mno": [date.today(), None, date.today()], + ... "xyz": [True, False, None], + ... } ... ) >>> pl.Config.set_tbl_cell_numeric_alignment("RIGHT") # doctest: +SKIP # ... - # shape: (3, 2) - # ┌────────────┬────────────┐ - # │ column_abc ┆ column_xyz │ - # │ --- ┆ --- │ - # │ i64 ┆ bool │ - # ╞════════════╪════════════╡ - # │ 11 ┆ true │ - # │ 2 ┆ false │ - # │ 333 ┆ true │ - # └────────────┴────────────┘ + # shape: (3, 3) + # ┌─────┬────────────┬───────┐ + # │ abc ┆ mno ┆ xyz │ + # │ --- ┆ --- ┆ --- │ + # │ i64 ┆ date ┆ bool │ + # ╞═════╪════════════╪═══════╡ + # │ 11 ┆ 2023-09-05 ┆ true │ + # │ 2 ┆ null ┆ false │ + # │ 333 ┆ 2023-09-05 ┆ null │ + # └─────┴────────────┴───────┘ Raises ------ From 80175a667be94e3e3fd8bc67b2586b5eb9669339 Mon Sep 17 00:00:00 2001 From: Alexander Beedie Date: Tue, 12 Sep 2023 14:04:01 +0000 Subject: [PATCH 06/11] rebase and add 'unset' logic for `Config.set_tbl_cell_numeric_alignment` --- py-polars/polars/config.py | 9 +++++++-- py-polars/tests/unit/test_cfg.py | 6 ++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/py-polars/polars/config.py b/py-polars/polars/config.py index 81c6659feded..1f7b2957a271 100644 --- a/py-polars/polars/config.py +++ b/py-polars/polars/config.py @@ -634,7 +634,7 @@ def set_tbl_cell_alignment( @classmethod def set_tbl_cell_numeric_alignment( - cls, format: Literal["LEFT", "CENTER", "RIGHT"] + cls, format: Literal["LEFT", "CENTER", "RIGHT"] | None ) -> type[Config]: """ Set table cell alignment for numeric columns. @@ -674,7 +674,12 @@ def set_tbl_cell_numeric_alignment( KeyError: if alignment string not recognised. """ - os.environ["POLARS_FMT_TABLE_CELL_NUMERIC_ALIGNMENT"] = format + if format is None: + os.environ.pop("POLARS_FMT_TABLE_CELL_NUMERIC_ALIGNMENT", None) + elif format not in {"LEFT", "CENTER", "RIGHT"}: + raise ValueError(f"invalid alignment: {format!r}") + else: + os.environ["POLARS_FMT_TABLE_CELL_NUMERIC_ALIGNMENT"] = format return cls @classmethod diff --git a/py-polars/tests/unit/test_cfg.py b/py-polars/tests/unit/test_cfg.py index 5f339bd8ce12..1cb46640a7ac 100644 --- a/py-polars/tests/unit/test_cfg.py +++ b/py-polars/tests/unit/test_cfg.py @@ -798,6 +798,12 @@ def test_set_fmt_str_lengths_invalid_length() -> None: ("POLARS_FMT_MAX_ROWS", "set_tbl_rows", 3, "3"), ("POLARS_FMT_STR_LEN", "set_fmt_str_lengths", 42, "42"), ("POLARS_FMT_TABLE_CELL_ALIGNMENT", "set_tbl_cell_alignment", "RIGHT", "RIGHT"), + ( + "POLARS_FMT_TABLE_CELL_NUMERIC_ALIGNMENT", + "set_tbl_cell_numeric_alignment", + "RIGHT", + "RIGHT", + ), ("POLARS_FMT_TABLE_HIDE_COLUMN_NAMES", "set_tbl_hide_column_names", True, "1"), ( "POLARS_FMT_TABLE_DATAFRAME_SHAPE_BELOW", From 4a1592b8a9ab6940afc16c011b9c46820fed51ba Mon Sep 17 00:00:00 2001 From: Alicja Januszkiewicz Date: Sun, 17 Sep 2023 17:08:41 +0100 Subject: [PATCH 07/11] change FLOAT_PRECISION type from AtomicU8 to RwLock> --- crates/polars-core/src/fmt.rs | 27 ++++++++++++++++----------- py-polars/polars/config.py | 8 +------- py-polars/src/functions/meta.rs | 9 ++------- py-polars/tests/unit/test_cfg.py | 6 +----- 4 files changed, 20 insertions(+), 30 deletions(-) diff --git a/crates/polars-core/src/fmt.rs b/crates/polars-core/src/fmt.rs index 7e18929829ce..2dbf7b666df6 100644 --- a/crates/polars-core/src/fmt.rs +++ b/crates/polars-core/src/fmt.rs @@ -2,6 +2,7 @@ use std::borrow::Cow; use std::fmt::{Debug, Display, Formatter}; use std::sync::atomic::{AtomicU8, Ordering}; +use std::sync::RwLock; use std::{fmt, str}; #[cfg(any( @@ -33,7 +34,7 @@ pub enum FloatFmt { Full, } static FLOAT_FMT: AtomicU8 = AtomicU8::new(FloatFmt::Mixed as u8); -static FLOAT_PRECISION: AtomicU8 = AtomicU8::new(u8::MAX); +static FLOAT_PRECISION: RwLock> = RwLock::new(None); pub fn get_float_fmt() -> FloatFmt { match FLOAT_FMT.load(Ordering::Relaxed) { @@ -43,16 +44,16 @@ pub fn get_float_fmt() -> FloatFmt { } } -pub fn get_float_precision() -> u8 { - FLOAT_PRECISION.load(Ordering::Relaxed) +pub fn get_float_precision() -> Option { + FLOAT_PRECISION.read().unwrap().clone() } pub fn set_float_fmt(fmt: FloatFmt) { FLOAT_FMT.store(fmt as u8, Ordering::Relaxed) } -pub fn set_float_precision(precision: u8) { - FLOAT_PRECISION.store(precision, Ordering::Relaxed) +pub fn set_float_precision(precision: Option) { + *FLOAT_PRECISION.write().unwrap() = precision; } macro_rules! format_array { @@ -724,12 +725,16 @@ const SCIENTIFIC_BOUND: f64 = 999999.0; fn fmt_float(f: &mut Formatter<'_>, width: usize, v: T) -> fmt::Result { let v: f64 = NumCast::from(v).unwrap(); - let precision = get_float_precision(); - if precision != u8::MAX { - if format!("{v:.precision$}", precision = precision as usize).len() > 19 { - return write!(f, "{v:>width$.precision$e}", precision = precision as usize); - } - return write!(f, "{v:>width$.precision$}", precision = precision as usize); + let float_precision = get_float_precision(); + + match float_precision { + Some(precision) => { + if format!("{v:.precision$}", precision = precision).len() > 19 { + return write!(f, "{v:>width$.precision$e}", precision = precision); + } + return write!(f, "{v:>width$.precision$}", precision = precision); + }, + _ => {}, } if matches!(get_float_fmt(), FloatFmt::Full) { diff --git a/py-polars/polars/config.py b/py-polars/polars/config.py index 1f7b2957a271..08eb2a90d1a2 100644 --- a/py-polars/polars/config.py +++ b/py-polars/polars/config.py @@ -17,7 +17,7 @@ def _get_float_fmt() -> str: # pragma: no cover def _get_float_precision() -> int: - return 0 + return -1 # note: module not available when building docs @@ -452,12 +452,6 @@ def set_float_precision(cls, precision: int | None = None) -> type[Config]: Number of decimal places to display """ - if precision is None: - precision = 255 - elif precision > 16 and precision != 255: - raise ValueError( - f"precision must be None, or a number between 0-16; got {precision}" - ) _set_float_precision(precision) return cls diff --git a/py-polars/src/functions/meta.rs b/py-polars/src/functions/meta.rs index 721ca777b724..80f69e37a05d 100644 --- a/py-polars/src/functions/meta.rs +++ b/py-polars/src/functions/meta.rs @@ -48,19 +48,14 @@ pub fn get_float_fmt() -> PyResult { } #[pyfunction] -pub fn set_float_precision(precision: u8) -> PyResult<()> { +pub fn set_float_precision(precision: Option) -> PyResult<()> { use polars_core::fmt::set_float_precision; - if precision > 16 && precision != u8::MAX { - return Err(PyValueError::new_err(format!( - "maximum supported float precision is 16, got {precision}", - ))); - } set_float_precision(precision); Ok(()) } #[pyfunction] -pub fn get_float_precision() -> PyResult { +pub fn get_float_precision() -> PyResult> { use polars_core::fmt::get_float_precision; Ok(get_float_precision()) } diff --git a/py-polars/tests/unit/test_cfg.py b/py-polars/tests/unit/test_cfg.py index 1cb46640a7ac..866c1e960eca 100644 --- a/py-polars/tests/unit/test_cfg.py +++ b/py-polars/tests/unit/test_cfg.py @@ -611,10 +611,6 @@ def test_numeric_right_alignment() -> None: "└─────────┴─────────┴───────────┘" ) - # test nonsensical float precision raises an error - with pytest.raises(ValueError): - pl.Config.set_float_precision(50) - def test_string_cache() -> None: df1 = pl.DataFrame({"a": ["foo", "bar", "ham"], "b": [1, 2, 3]}) @@ -703,7 +699,7 @@ def test_config_load_save(tmp_path: Path) -> None: assert os.environ.get("POLARS_FMT_MAX_COLS") is None assert os.environ.get("POLARS_VERBOSE") is None assert _get_float_fmt() == "mixed" - assert _get_float_precision() == 255 + assert _get_float_precision() is None # ref: #11094 with pl.Config( From 665a612485bdb4748b96d4abd5136ba093a35add Mon Sep 17 00:00:00 2001 From: Alicja Januszkiewicz Date: Sun, 17 Sep 2023 18:47:12 +0100 Subject: [PATCH 08/11] fix clippy-nightly issues and rebase --- crates/polars-core/src/fmt.rs | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/crates/polars-core/src/fmt.rs b/crates/polars-core/src/fmt.rs index 2dbf7b666df6..884e612e8b31 100644 --- a/crates/polars-core/src/fmt.rs +++ b/crates/polars-core/src/fmt.rs @@ -45,7 +45,7 @@ pub fn get_float_fmt() -> FloatFmt { } pub fn get_float_precision() -> Option { - FLOAT_PRECISION.read().unwrap().clone() + *FLOAT_PRECISION.read().unwrap() } pub fn set_float_fmt(fmt: FloatFmt) { @@ -727,14 +727,11 @@ fn fmt_float(f: &mut Formatter<'_>, width: usize, v: T) -> fmt let float_precision = get_float_precision(); - match float_precision { - Some(precision) => { - if format!("{v:.precision$}", precision = precision).len() > 19 { - return write!(f, "{v:>width$.precision$e}", precision = precision); - } - return write!(f, "{v:>width$.precision$}", precision = precision); - }, - _ => {}, + if let Some(precision) = float_precision { + if format!("{v:.precision$}", precision = precision).len() > 19 { + return write!(f, "{v:>width$.precision$e}", precision = precision); + } + return write!(f, "{v:>width$.precision$}", precision = precision); } if matches!(get_float_fmt(), FloatFmt::Full) { From ea7894928b317c6d7532ab924d1d6ea9b9276785 Mon Sep 17 00:00:00 2001 From: alexander-beedie Date: Thu, 12 Oct 2023 00:19:23 +0400 Subject: [PATCH 09/11] rebase/update --- py-polars/tests/unit/test_cfg.py | 29 ----------------------------- 1 file changed, 29 deletions(-) diff --git a/py-polars/tests/unit/test_cfg.py b/py-polars/tests/unit/test_cfg.py index 866c1e960eca..515555a5d8e6 100644 --- a/py-polars/tests/unit/test_cfg.py +++ b/py-polars/tests/unit/test_cfg.py @@ -8,8 +8,6 @@ import polars as pl from polars.config import _POLARS_CFG_ENV_VARS, _get_float_fmt, _get_float_precision -from polars.exceptions import StringCacheMismatchError -from polars.testing import assert_frame_equal @pytest.fixture(autouse=True) @@ -612,33 +610,6 @@ def test_numeric_right_alignment() -> None: ) -def test_string_cache() -> None: - df1 = pl.DataFrame({"a": ["foo", "bar", "ham"], "b": [1, 2, 3]}) - df2 = pl.DataFrame({"a": ["foo", "spam", "eggs"], "c": [3, 2, 2]}) - - # ensure cache is off when casting to categorical; the join will fail - pl.enable_string_cache(False) - assert pl.using_string_cache() is False - - df1a = df1.with_columns(pl.col("a").cast(pl.Categorical)) - df2a = df2.with_columns(pl.col("a").cast(pl.Categorical)) - with pytest.raises(StringCacheMismatchError): - _ = df1a.join(df2a, on="a", how="inner") - - # now turn on the cache - pl.enable_string_cache(True) - assert pl.using_string_cache() is True - - df1b = df1.with_columns(pl.col("a").cast(pl.Categorical)) - df2b = df2.with_columns(pl.col("a").cast(pl.Categorical)) - out = df1b.join(df2b, on="a", how="inner") - - expected = pl.DataFrame( - {"a": ["foo"], "b": [1], "c": [3]}, schema_overrides={"a": pl.Categorical} - ) - assert_frame_equal(out, expected) - - @pytest.mark.write_disk() def test_config_load_save(tmp_path: Path) -> None: for file in ( From fcc18d8e8e14b380f92af982b1b4fdaf8e8329ee Mon Sep 17 00:00:00 2001 From: alexander-beedie Date: Thu, 12 Oct 2023 00:43:47 +0400 Subject: [PATCH 10/11] add `set_float_precision` example (and notes) --- py-polars/polars/config.py | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/py-polars/polars/config.py b/py-polars/polars/config.py index 08eb2a90d1a2..72b2aef89cc6 100644 --- a/py-polars/polars/config.py +++ b/py-polars/polars/config.py @@ -444,13 +444,39 @@ def set_auto_structify(cls, active: bool | None = False) -> type[Config]: @classmethod def set_float_precision(cls, precision: int | None = None) -> type[Config]: """ - Control how floating point values are displayed. + Control the number of decimal places displayed for floating point values. Parameters ---------- precision : int Number of decimal places to display + Notes + ----- + When setting this to a larger value you should ensure that you are aware of both + the limitations of floating point representations, and of the precision of the + data that you are looking at. + + This setting only applies to Float32 and Float64 dtypes; it does not cover + Decimal dtype values (which are displayed at their native level of precision). + + Examples + -------- + >>> from math import pi, e + >>> df = pl.DataFrame({"const": ["pi", "e"], "value": [pi, e]}) + >>> with pl.Config(float_precision=15): + ... print(repr(df)) + ... + shape: (2, 2) + ┌───────┬───────────────────┐ + │ const ┆ value │ + │ --- ┆ --- │ + │ str ┆ f64 │ + ╞═══════╪═══════════════════╡ + │ pi ┆ 3.141592653589793 │ + │ e ┆ 2.718281828459045 │ + └───────┴───────────────────┘ + """ _set_float_precision(precision) return cls From 1a7a48561aae1a24de74dbd02491fb886dc98b49 Mon Sep 17 00:00:00 2001 From: alexander-beedie Date: Thu, 12 Oct 2023 00:58:17 +0400 Subject: [PATCH 11/11] minor docstring update and an extra unit test --- py-polars/polars/config.py | 3 ++- py-polars/tests/unit/test_cfg.py | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/py-polars/polars/config.py b/py-polars/polars/config.py index 72b2aef89cc6..162e56be4d05 100644 --- a/py-polars/polars/config.py +++ b/py-polars/polars/config.py @@ -449,7 +449,8 @@ def set_float_precision(cls, precision: int | None = None) -> type[Config]: Parameters ---------- precision : int - Number of decimal places to display + Number of decimal places to display; set to ``None`` to revert to the + default/standard behaviour. Notes ----- diff --git a/py-polars/tests/unit/test_cfg.py b/py-polars/tests/unit/test_cfg.py index 515555a5d8e6..54b766e3cdaf 100644 --- a/py-polars/tests/unit/test_cfg.py +++ b/py-polars/tests/unit/test_cfg.py @@ -571,6 +571,20 @@ def test_numeric_right_alignment() -> None: "│ 3.333000 ┆ 6.000000 ┆ 9.000000 │\n" "└──────────┴──────────┴──────────┘" ) + with pl.Config(float_precision=None): + assert ( + str(df) == "shape: (3, 3)\n" + "┌───────┬─────┬─────┐\n" + "│ a ┆ b ┆ c │\n" + "│ --- ┆ --- ┆ --- │\n" + "│ f64 ┆ f64 ┆ f64 │\n" + "╞═══════╪═════╪═════╡\n" + "│ 1.1 ┆ 4.0 ┆ 7.0 │\n" + "│ 2.22 ┆ 5.0 ┆ 8.0 │\n" + "│ 3.333 ┆ 6.0 ┆ 9.0 │\n" + "└───────┴─────┴─────┘" + ) + df = pl.DataFrame( {"a": [1.1, 22.2, 3.33], "b": [444, 55.5, 6.6], "c": [77.7, 8888, 9.9999]} )