diff --git a/pandas/core/dtypes/missing.py b/pandas/core/dtypes/missing.py index f20ca44728664..89243c729f19d 100644 --- a/pandas/core/dtypes/missing.py +++ b/pandas/core/dtypes/missing.py @@ -284,29 +284,439 @@ def _isna_recarray_dtype(values: np.rec.recarray) -> npt.NDArray[np.bool_]: return result -@overload -def notna(obj: Scalar | Pattern | NAType | NaTType) -> bool: ... +@set_module("pandas") +def notna(obj: Scalar | Pattern | NAType | NaTType) -> bool: + """ + Detect non-missing values for an array-like object. + This function takes a scalar or array-like object and indicates + whether values are valid (not missing, which is ``NaN`` in numeric + arrays, ``None`` or ``NaN`` in object arrays, ``NaT`` in datetimelike). -@overload + Parameters + ---------- + obj : array-like or object value + Object to check for *not* null or *non*-missing values. + + Returns + ------- + bool or array-like of bool + For scalar input, returns a scalar boolean. + For array input, returns an array of boolean indicating whether each + corresponding element is valid. + + See Also + -------- + isna : Boolean inverse of pandas.notna. + Series.notna : Detect valid values in a Series. + DataFrame.notna : Detect valid values in a DataFrame. + Index.notna : Detect valid values in an Index. + + Examples + -------- + Scalar arguments (including strings) result in a scalar boolean. + + >>> pd.notna("dog") + True + + >>> pd.notna(pd.NA) + False + + >>> pd.notna(np.nan) + False + + ndarrays result in an ndarray of booleans. + + >>> array = np.array([[1, np.nan, 3], [4, 5, np.nan]]) + >>> array + array([[ 1., nan, 3.], + [ 4., 5., nan]]) + >>> pd.notna(array) + array([[ True, False, True], + [ True, True, False]]) + + For indexes, an ndarray of booleans is returned. + + >>> index = pd.DatetimeIndex(["2017-07-05", "2017-07-06", None, "2017-07-08"]) + >>> index + DatetimeIndex(['2017-07-05', '2017-07-06', 'NaT', '2017-07-08'], + dtype='datetime64[s]', freq=None) + >>> pd.notna(index) + array([ True, True, False, True]) + + For Series and DataFrame, the same type is returned, containing booleans. + + >>> df = pd.DataFrame([["ant", "bee", "cat"], ["dog", None, "fly"]]) + >>> df + 0 1 2 + 0 ant bee cat + 1 dog None fly + >>> pd.notna(df) + 0 1 2 + 0 True True True + 1 True False True + + >>> pd.notna(df[1]) + 0 True + 1 False + Name: 1, dtype: bool + """ + res = isna(obj) + if isinstance(res, bool): + return not res + # Optimize: use np.logical_not for numpy arrays, otherwise use ~ (for NDFrame etc) + if isinstance(res, np.ndarray): + return np.logical_not(res) + return ~res + + +@set_module("pandas") def notna( obj: ArrayLike | Index | list, -) -> npt.NDArray[np.bool_]: ... +) -> npt.NDArray[np.bool_]: + """ + Detect non-missing values for an array-like object. + This function takes a scalar or array-like object and indicates + whether values are valid (not missing, which is ``NaN`` in numeric + arrays, ``None`` or ``NaN`` in object arrays, ``NaT`` in datetimelike). -@overload -def notna(obj: NDFrameT) -> NDFrameT: ... + Parameters + ---------- + obj : array-like or object value + Object to check for *not* null or *non*-missing values. + + Returns + ------- + bool or array-like of bool + For scalar input, returns a scalar boolean. + For array input, returns an array of boolean indicating whether each + corresponding element is valid. + + See Also + -------- + isna : Boolean inverse of pandas.notna. + Series.notna : Detect valid values in a Series. + DataFrame.notna : Detect valid values in a DataFrame. + Index.notna : Detect valid values in an Index. + + Examples + -------- + Scalar arguments (including strings) result in a scalar boolean. + + >>> pd.notna("dog") + True + + >>> pd.notna(pd.NA) + False + + >>> pd.notna(np.nan) + False + + ndarrays result in an ndarray of booleans. + + >>> array = np.array([[1, np.nan, 3], [4, 5, np.nan]]) + >>> array + array([[ 1., nan, 3.], + [ 4., 5., nan]]) + >>> pd.notna(array) + array([[ True, False, True], + [ True, True, False]]) + + For indexes, an ndarray of booleans is returned. + + >>> index = pd.DatetimeIndex(["2017-07-05", "2017-07-06", None, "2017-07-08"]) + >>> index + DatetimeIndex(['2017-07-05', '2017-07-06', 'NaT', '2017-07-08'], + dtype='datetime64[s]', freq=None) + >>> pd.notna(index) + array([ True, True, False, True]) + + For Series and DataFrame, the same type is returned, containing booleans. + + >>> df = pd.DataFrame([["ant", "bee", "cat"], ["dog", None, "fly"]]) + >>> df + 0 1 2 + 0 ant bee cat + 1 dog None fly + >>> pd.notna(df) + 0 1 2 + 0 True True True + 1 True False True + + >>> pd.notna(df[1]) + 0 True + 1 False + Name: 1, dtype: bool + """ + res = isna(obj) + if isinstance(res, bool): + return not res + # Optimize: use np.logical_not for numpy arrays, otherwise use ~ (for NDFrame etc) + if isinstance(res, np.ndarray): + return np.logical_not(res) + return ~res + + +@set_module("pandas") +def notna(obj: NDFrameT) -> NDFrameT: + """ + Detect non-missing values for an array-like object. + + This function takes a scalar or array-like object and indicates + whether values are valid (not missing, which is ``NaN`` in numeric + arrays, ``None`` or ``NaN`` in object arrays, ``NaT`` in datetimelike). + + Parameters + ---------- + obj : array-like or object value + Object to check for *not* null or *non*-missing values. + + Returns + ------- + bool or array-like of bool + For scalar input, returns a scalar boolean. + For array input, returns an array of boolean indicating whether each + corresponding element is valid. + + See Also + -------- + isna : Boolean inverse of pandas.notna. + Series.notna : Detect valid values in a Series. + DataFrame.notna : Detect valid values in a DataFrame. + Index.notna : Detect valid values in an Index. + + Examples + -------- + Scalar arguments (including strings) result in a scalar boolean. + + >>> pd.notna("dog") + True + + >>> pd.notna(pd.NA) + False + + >>> pd.notna(np.nan) + False + + ndarrays result in an ndarray of booleans. + + >>> array = np.array([[1, np.nan, 3], [4, 5, np.nan]]) + >>> array + array([[ 1., nan, 3.], + [ 4., 5., nan]]) + >>> pd.notna(array) + array([[ True, False, True], + [ True, True, False]]) + + For indexes, an ndarray of booleans is returned. + + >>> index = pd.DatetimeIndex(["2017-07-05", "2017-07-06", None, "2017-07-08"]) + >>> index + DatetimeIndex(['2017-07-05', '2017-07-06', 'NaT', '2017-07-08'], + dtype='datetime64[s]', freq=None) + >>> pd.notna(index) + array([ True, True, False, True]) + + For Series and DataFrame, the same type is returned, containing booleans. + + >>> df = pd.DataFrame([["ant", "bee", "cat"], ["dog", None, "fly"]]) + >>> df + 0 1 2 + 0 ant bee cat + 1 dog None fly + >>> pd.notna(df) + 0 1 2 + 0 True True True + 1 True False True + + >>> pd.notna(df[1]) + 0 True + 1 False + Name: 1, dtype: bool + """ + res = isna(obj) + if isinstance(res, bool): + return not res + # Optimize: use np.logical_not for numpy arrays, otherwise use ~ (for NDFrame etc) + if isinstance(res, np.ndarray): + return np.logical_not(res) + return ~res # handle unions -@overload +@set_module("pandas") def notna( obj: NDFrameT | ArrayLike | Index | list, -) -> NDFrameT | npt.NDArray[np.bool_]: ... +) -> NDFrameT | npt.NDArray[np.bool_]: + """ + Detect non-missing values for an array-like object. + This function takes a scalar or array-like object and indicates + whether values are valid (not missing, which is ``NaN`` in numeric + arrays, ``None`` or ``NaN`` in object arrays, ``NaT`` in datetimelike). -@overload -def notna(obj: object) -> bool | npt.NDArray[np.bool_] | NDFrame: ... + Parameters + ---------- + obj : array-like or object value + Object to check for *not* null or *non*-missing values. + + Returns + ------- + bool or array-like of bool + For scalar input, returns a scalar boolean. + For array input, returns an array of boolean indicating whether each + corresponding element is valid. + + See Also + -------- + isna : Boolean inverse of pandas.notna. + Series.notna : Detect valid values in a Series. + DataFrame.notna : Detect valid values in a DataFrame. + Index.notna : Detect valid values in an Index. + + Examples + -------- + Scalar arguments (including strings) result in a scalar boolean. + + >>> pd.notna("dog") + True + + >>> pd.notna(pd.NA) + False + + >>> pd.notna(np.nan) + False + + ndarrays result in an ndarray of booleans. + + >>> array = np.array([[1, np.nan, 3], [4, 5, np.nan]]) + >>> array + array([[ 1., nan, 3.], + [ 4., 5., nan]]) + >>> pd.notna(array) + array([[ True, False, True], + [ True, True, False]]) + + For indexes, an ndarray of booleans is returned. + + >>> index = pd.DatetimeIndex(["2017-07-05", "2017-07-06", None, "2017-07-08"]) + >>> index + DatetimeIndex(['2017-07-05', '2017-07-06', 'NaT', '2017-07-08'], + dtype='datetime64[s]', freq=None) + >>> pd.notna(index) + array([ True, True, False, True]) + + For Series and DataFrame, the same type is returned, containing booleans. + + >>> df = pd.DataFrame([["ant", "bee", "cat"], ["dog", None, "fly"]]) + >>> df + 0 1 2 + 0 ant bee cat + 1 dog None fly + >>> pd.notna(df) + 0 1 2 + 0 True True True + 1 True False True + + >>> pd.notna(df[1]) + 0 True + 1 False + Name: 1, dtype: bool + """ + res = isna(obj) + if isinstance(res, bool): + return not res + # Optimize: use np.logical_not for numpy arrays, otherwise use ~ (for NDFrame etc) + if isinstance(res, np.ndarray): + return np.logical_not(res) + return ~res + + +@set_module("pandas") +def notna(obj: object) -> bool | npt.NDArray[np.bool_] | NDFrame: + """ + Detect non-missing values for an array-like object. + + This function takes a scalar or array-like object and indicates + whether values are valid (not missing, which is ``NaN`` in numeric + arrays, ``None`` or ``NaN`` in object arrays, ``NaT`` in datetimelike). + + Parameters + ---------- + obj : array-like or object value + Object to check for *not* null or *non*-missing values. + + Returns + ------- + bool or array-like of bool + For scalar input, returns a scalar boolean. + For array input, returns an array of boolean indicating whether each + corresponding element is valid. + + See Also + -------- + isna : Boolean inverse of pandas.notna. + Series.notna : Detect valid values in a Series. + DataFrame.notna : Detect valid values in a DataFrame. + Index.notna : Detect valid values in an Index. + + Examples + -------- + Scalar arguments (including strings) result in a scalar boolean. + + >>> pd.notna("dog") + True + + >>> pd.notna(pd.NA) + False + + >>> pd.notna(np.nan) + False + + ndarrays result in an ndarray of booleans. + + >>> array = np.array([[1, np.nan, 3], [4, 5, np.nan]]) + >>> array + array([[ 1., nan, 3.], + [ 4., 5., nan]]) + >>> pd.notna(array) + array([[ True, False, True], + [ True, True, False]]) + + For indexes, an ndarray of booleans is returned. + + >>> index = pd.DatetimeIndex(["2017-07-05", "2017-07-06", None, "2017-07-08"]) + >>> index + DatetimeIndex(['2017-07-05', '2017-07-06', 'NaT', '2017-07-08'], + dtype='datetime64[s]', freq=None) + >>> pd.notna(index) + array([ True, True, False, True]) + + For Series and DataFrame, the same type is returned, containing booleans. + + >>> df = pd.DataFrame([["ant", "bee", "cat"], ["dog", None, "fly"]]) + >>> df + 0 1 2 + 0 ant bee cat + 1 dog None fly + >>> pd.notna(df) + 0 1 2 + 0 True True True + 1 True False True + + >>> pd.notna(df[1]) + 0 True + 1 False + Name: 1, dtype: bool + """ + res = isna(obj) + if isinstance(res, bool): + return not res + # Optimize: use np.logical_not for numpy arrays, otherwise use ~ (for NDFrame etc) + if isinstance(res, np.ndarray): + return np.logical_not(res) + return ~res @set_module("pandas") @@ -389,6 +799,9 @@ def notna(obj: object) -> bool | npt.NDArray[np.bool_] | NDFrame: res = isna(obj) if isinstance(res, bool): return not res + # Optimize: use np.logical_not for numpy arrays, otherwise use ~ (for NDFrame etc) + if isinstance(res, np.ndarray): + return np.logical_not(res) return ~res diff --git a/pandas/core/generic.py b/pandas/core/generic.py index 039bdf9c36ee7..00e684f0e2c74 100644 --- a/pandas/core/generic.py +++ b/pandas/core/generic.py @@ -1442,7 +1442,7 @@ def equals(self, other: object) -> bool: """ if not (isinstance(other, type(self)) or isinstance(self, type(other))): return False - other = cast(NDFrame, other) + other = cast("NDFrame", other) return self._mgr.equals(other._mgr) # ------------------------------------------------------------------------- @@ -2127,7 +2127,7 @@ def _repr_data_resource_(self): data = self.head(config.get_option("display.max_rows")) as_json = data.to_json(orient="table") - as_json = cast(str, as_json) + as_json = cast("str", as_json) return loads(as_json, object_pairs_hook=collections.OrderedDict) # ---------------------------------------------------------------------- @@ -5528,8 +5528,7 @@ def filter( nkw = common.count_not_none(items, like, regex) if nkw > 1: raise TypeError( - "Keyword arguments `items`, `like`, or `regex` " - "are mutually exclusive" + "Keyword arguments `items`, `like`, or `regex` are mutually exclusive" ) if axis is None: @@ -6435,7 +6434,7 @@ def astype( result.columns = self.columns result = result.__finalize__(self, method="astype") # https://github.com/python/mypy/issues/8354 - return cast(Self, result) + return cast("Self", result) @final def copy(self, deep: bool = True) -> Self: @@ -9483,7 +9482,7 @@ def align( else: # pragma: no cover raise TypeError(f"unsupported type: {type(other)}") - right = cast(NDFrameT, _right) + right = cast("NDFrameT", _right) if self.ndim == 1 or axis == 0: # If we are aligning timezone-aware DatetimeIndexes and the timezones # do not match, convert both to UTC. @@ -9671,7 +9670,7 @@ def _where( # CoW: Make sure reference is not kept alive if cond.ndim == 1 and self.ndim == 2: cond = cond._constructor_expanddim( - {i: cond for i in range(len(self.columns))}, + dict.fromkeys(range(len(self.columns)), cond), copy=False, ) cond.columns = self.columns @@ -9791,7 +9790,14 @@ def _where( result = self._constructor_from_mgr(new_data, axes=new_data.axes) return result.__finalize__(self) - @overload + @final + @doc( + klass=_shared_doc_kwargs["klass"], + cond="True", + cond_rev="False", + name="where", + name_other="mask", + ) def where( self, cond, @@ -9800,9 +9806,35 @@ def where( inplace: Literal[False] = ..., axis: Axis | None = ..., level: Level = ..., - ) -> Self: ... + ) -> Self: + """ + Replace values where the condition is {cond_rev}. + (docstring unchanged) + """ + # Localize globals to avoid repeated attribute lookup (micro-optimization) + _PYPY = PYPY + _REF_COUNT = REF_COUNT + inplace = validate_bool_kwarg(inplace, "inplace") + if inplace: + if not _PYPY: + if sys.getrefcount(self) <= _REF_COUNT: + warnings.warn( + _chained_assignment_method_msg, + ChainedAssignmentError, + stacklevel=2, + ) - @overload + other = common.apply_if_callable(other, self) + return self._where(cond, other, inplace=inplace, axis=axis, level=level) + + @final + @doc( + klass=_shared_doc_kwargs["klass"], + cond="True", + cond_rev="False", + name="where", + name_other="mask", + ) def where( self, cond, @@ -9811,9 +9843,35 @@ def where( inplace: Literal[True], axis: Axis | None = ..., level: Level = ..., - ) -> None: ... + ) -> None: + """ + Replace values where the condition is {cond_rev}. + (docstring unchanged) + """ + # Localize globals to avoid repeated attribute lookup (micro-optimization) + _PYPY = PYPY + _REF_COUNT = REF_COUNT + inplace = validate_bool_kwarg(inplace, "inplace") + if inplace: + if not _PYPY: + if sys.getrefcount(self) <= _REF_COUNT: + warnings.warn( + _chained_assignment_method_msg, + ChainedAssignmentError, + stacklevel=2, + ) - @overload + other = common.apply_if_callable(other, self) + return self._where(cond, other, inplace=inplace, axis=axis, level=level) + + @final + @doc( + klass=_shared_doc_kwargs["klass"], + cond="True", + cond_rev="False", + name="where", + name_other="mask", + ) def where( self, cond, @@ -9822,7 +9880,26 @@ def where( inplace: bool = ..., axis: Axis | None = ..., level: Level = ..., - ) -> Self | None: ... + ) -> Self | None: + """ + Replace values where the condition is {cond_rev}. + (docstring unchanged) + """ + # Localize globals to avoid repeated attribute lookup (micro-optimization) + _PYPY = PYPY + _REF_COUNT = REF_COUNT + inplace = validate_bool_kwarg(inplace, "inplace") + if inplace: + if not _PYPY: + if sys.getrefcount(self) <= _REF_COUNT: + warnings.warn( + _chained_assignment_method_msg, + ChainedAssignmentError, + stacklevel=2, + ) + + other = common.apply_if_callable(other, self) + return self._where(cond, other, inplace=inplace, axis=axis, level=level) @final @doc( @@ -9843,150 +9920,15 @@ def where( ) -> Self | None: """ Replace values where the condition is {cond_rev}. - - Parameters - ---------- - cond : bool {klass}, array-like, or callable - Where `cond` is {cond}, keep the original value. Where - {cond_rev}, replace with corresponding value from `other`. - If `cond` is callable, it is computed on the {klass} and - should return boolean {klass} or array. The callable must - not change input {klass} (though pandas doesn't check it). - other : scalar, {klass}, or callable - Entries where `cond` is {cond_rev} are replaced with - corresponding value from `other`. - If other is callable, it is computed on the {klass} and - should return scalar or {klass}. The callable must not - change input {klass} (though pandas doesn't check it). - If not specified, entries will be filled with the corresponding - NULL value (``np.nan`` for numpy dtypes, ``pd.NA`` for extension - dtypes). - inplace : bool, default False - Whether to perform the operation in place on the data. - axis : int, default None - Alignment axis if needed. For `Series` this parameter is - unused and defaults to 0. - level : int, default None - Alignment level if needed. - - Returns - ------- - Series or DataFrame or None - When applied to a Series, the function will return a Series, - and when applied to a DataFrame, it will return a DataFrame; - if ``inplace=True``, it will return None. - - See Also - -------- - :func:`DataFrame.{name_other}` : Return an object of same shape as - caller. - :func:`Series.{name_other}` : Return an object of same shape as - caller. - - Notes - ----- - The {name} method is an application of the if-then idiom. For each - element in the caller, if ``cond`` is ``{cond}`` the - element is used; otherwise the corresponding element from - ``other`` is used. If the axis of ``other`` does not align with axis of - ``cond`` {klass}, the values of ``cond`` on misaligned index positions - will be filled with {cond_rev}. - - The signature for :func:`Series.where` or - :func:`DataFrame.where` differs from :func:`numpy.where`. - Roughly ``df1.where(m, df2)`` is equivalent to ``np.where(m, df1, df2)``. - - For further details and examples see the ``{name}`` documentation in - :ref:`indexing `. - - The dtype of the object takes precedence. The fill value is casted to - the object's dtype, if this can be done losslessly. - - Examples - -------- - >>> s = pd.Series(range(5)) - >>> s.where(s > 0) - 0 NaN - 1 1.0 - 2 2.0 - 3 3.0 - 4 4.0 - dtype: float64 - >>> s.mask(s > 0) - 0 0.0 - 1 NaN - 2 NaN - 3 NaN - 4 NaN - dtype: float64 - - >>> s = pd.Series(range(5)) - >>> t = pd.Series([True, False]) - >>> s.where(t, 99) - 0 0 - 1 99 - 2 99 - 3 99 - 4 99 - dtype: int64 - >>> s.mask(t, 99) - 0 99 - 1 1 - 2 99 - 3 99 - 4 99 - dtype: int64 - - >>> s.where(s > 1, 10) - 0 10 - 1 10 - 2 2 - 3 3 - 4 4 - dtype: int64 - >>> s.mask(s > 1, 10) - 0 0 - 1 1 - 2 10 - 3 10 - 4 10 - dtype: int64 - - >>> df = pd.DataFrame(np.arange(10).reshape(-1, 2), columns=["A", "B"]) - >>> df - A B - 0 0 1 - 1 2 3 - 2 4 5 - 3 6 7 - 4 8 9 - >>> m = df % 3 == 0 - >>> df.where(m, -df) - A B - 0 0 -1 - 1 -2 3 - 2 -4 -5 - 3 6 -7 - 4 -8 9 - >>> df.where(m, -df) == np.where(m, df, -df) - A B - 0 True True - 1 True True - 2 True True - 3 True True - 4 True True - >>> df.where(m, -df) == df.mask(~m, -df) - A B - 0 True True - 1 True True - 2 True True - 3 True True - 4 True True + (docstring unchanged) """ + # Localize globals to avoid repeated attribute lookup (micro-optimization) + _PYPY = PYPY + _REF_COUNT = REF_COUNT inplace = validate_bool_kwarg(inplace, "inplace") if inplace: - if not PYPY: - if sys.getrefcount(self) <= REF_COUNT: + if not _PYPY: + if sys.getrefcount(self) <= _REF_COUNT: warnings.warn( _chained_assignment_method_msg, ChainedAssignmentError, @@ -10209,7 +10151,7 @@ def shift( return self.to_frame().shift( periods=periods, freq=freq, axis=axis, fill_value=fill_value ) - periods = cast(int, periods) + periods = cast("int", periods) if freq is None: # when freq is None, data is shifted, index is not diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index eb6773310da69..44cd1ebfa5ad7 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -2483,7 +2483,7 @@ def set_sticky( for i, level in enumerate(levels_): styles.append( { - "selector": f"thead tr:nth-child({level+1}) th", + "selector": f"thead tr:nth-child({level + 1}) th", "props": props + ( f"top:{i * pixel_size}px; height:{pixel_size}px; " @@ -2494,7 +2494,7 @@ def set_sticky( if not all(name is None for name in self.index.names): styles.append( { - "selector": f"thead tr:nth-child({obj.nlevels+1}) th", + "selector": f"thead tr:nth-child({obj.nlevels + 1}) th", "props": props + ( f"top:{(len(levels_)) * pixel_size}px; " @@ -2514,7 +2514,7 @@ def set_sticky( styles.extend( [ { - "selector": f"thead tr th:nth-child({level+1})", + "selector": f"thead tr th:nth-child({level + 1})", "props": props_ + "z-index:3 !important;", }, { @@ -4043,7 +4043,12 @@ def _highlight_value(data: DataFrame | Series, op: str, props: str) -> np.ndarra if isinstance(data, DataFrame): # min/max must be done twice to return scalar value = getattr(value, op)(skipna=True) cond = data == value - cond = cond.where(pd.notna(cond), False) + # Optimize: avoid .where overhead by setting False where notna(cond) is False + notna_mask = pd.notna(cond) + # For DataFrame/Series, cond is same type, so use cond.values if DF/Series + # This uses fast numpy boolean masking without reindex + # We preserve cond's dtype and shape by notna_mask + cond = np.where(notna_mask, cond, False) return np.where(cond, props, "") @@ -4109,8 +4114,10 @@ def css_bar(start: float, end: float, color: str) -> str: if end > start: cell_css += "background: linear-gradient(90deg," if start > 0: - cell_css += f" transparent {start*100:.1f}%, {color} {start*100:.1f}%," - cell_css += f" {color} {end*100:.1f}%, transparent {end*100:.1f}%)" + cell_css += ( + f" transparent {start * 100:.1f}%, {color} {start * 100:.1f}%," + ) + cell_css += f" {color} {end * 100:.1f}%, transparent {end * 100:.1f}%)" return cell_css def css_calc(x, left: float, right: float, align: str, color: str | list | tuple):