Prompt:
import uuid

import pandas as pd  # pyright: ignore[reportMissingImports]

from ..checks import _check_type
from ..types import Column, PandasDataFrame

def spread(df: PandasDataFrame, column: Column, using: Column) -> PandasDataFrame:

    _check_type(column, str)
    _check_type(using, str)
    if column == using:
        raise KeyError("column and using must be unique")
    original_shape = df.shape[1]
    if original_shape == 2:
        temp = uuid.uuid4().hex
        df[temp] = df.groupby(column).cumcount()
    index = [col for col in df.columns if col not in [column, using]]
    df = pd.pivot_table(df, index=index, columns=[column], values=[using], aggfunc="first")  # type: ignore
    df.columns = [col for col in df.columns.get_level_values(1)]  # type: ignore
    df = df.reset_index().rename_axis(None, axis=0)
    if original_shape == 2:
        df = df.drop(temp, axis=1)  # type: ignore
    return df


Prompt:
from __future__ import annotations

import warnings

import pandas as pd  # pyright: ignore[reportMissingImports]

from ..checks import _check_type
from ..types import Column, Columns, LazyColumns, PandasDataFrame, PandasGroupedFrame

def _melt(
    df: PandasDataFrame,
    cols_to_keep: list[str],
    cols_to_gather: list[str],
    into: tuple[str, str],
) -> PandasDataFrame:

    df = pd.melt(
        df,
        id_vars=cols_to_keep,
        value_vars=cols_to_gather,
        var_name=into[0],
        value_name=into[1],
    )
    df = df.dropna(subset=into[1])  # type: ignore
    df = df.reset_index(drop=True)
    return df


Prompt:
from __future__ import annotations

import warnings

import pandas as pd  # pyright: ignore[reportMissingImports]

from ..checks import _check_type
from ..types import Column, Columns, LazyColumns, PandasDataFrame, PandasGroupedFrame

def _melt(
    df: PandasDataFrame,
    cols_to_keep: list[str],
    cols_to_gather: list[str],
    into: tuple[str, str],
) -> PandasDataFrame:
    df = pd.melt(
        df,
        id_vars=cols_to_keep,
        value_vars=cols_to_gather,
        var_name=into[0],
        value_name=into[1],
    )
    df = df.dropna(subset=into[1])  # type: ignore
    df = df.reset_index(drop=True)
    return df

def _grouped_melt(df: PandasGroupedFrame, into: tuple[str, str]) -> PandasDataFrame:

    cols_to_keep = df.grouper.names  # type: ignore
    cols_to_gather = [col for col in df.obj.columns if col not in cols_to_keep]  # type: ignore
    df = _melt(df.obj, cols_to_keep, cols_to_gather, into)  # type: ignore
    return df


Prompt:
from __future__ import annotations

import pandas as pd  # pyright: ignore[reportMissingImports]

from ..checks import _check_type
from ..types import Join, LazyColumns, PandasDataFrame

def join(
    lhs: PandasDataFrame,
    rhs: PandasDataFrame,
    on: LazyColumns,
    how: Join = "left",
    postfix: tuple[str, str] = ("_lhs", "_rhs"),
) -> PandasDataFrame:

    _check_type(on, {list, str})
    _check_type(how, str)
    _check_type(postfix, tuple)
    if not how in ["left", "right", "inner", "full"]:
        message = (
            "on argument is invalid, must be one of {'left', 'right', 'inner', 'full'}"
        )
        raise ValueError(message)
    how = "outer" if (how == "full") else how  # type: ignore
    df = pd.merge(lhs, rhs, on=on, how=how, suffixes=postfix)
    df = df.reset_index(drop=True)
    return df


Prompt:
from ..types import Func, PandasDataFrame

def filter(df: PandasDataFrame, func: Func) -> PandasDataFrame:

    if not callable(func):
        raise TypeError("must be Func")
    df = df.loc[func]  # type: ignore
    df = df.reset_index(drop=True)
    return df


Prompt:
from __future__ import annotations

from ..checks import _check_type
from ..types import PandasDataFrame, PandasGroupedFrame

def take(
    df: PandasDataFrame | PandasGroupedFrame, rows: int = 1, **kwargs
) -> PandasDataFrame:

    if kwargs:  # compatibility: sklearn / train_test_split
        df = df.take(rows, **kwargs)  # type: ignore
        df = df.reset_index(drop=True)
        return df
    _check_type(rows, int)
    if isinstance(df, PandasDataFrame):
        if rows > df.shape[0]:
            raise ValueError("rows argument is invalid, exceeds total size")
    if rows == 0:
        raise ValueError("rows argument is invalid, must not be 0")
    if rows <= -1:
        df = df.tail(rows * -1)
    else:
        df = df.head(rows)
    if isinstance(df, PandasGroupedFrame):
        df = df.reset_index()
    else:
        df = df.reset_index(drop=True)
    return df


Prompt:
from __future__ import annotations

from ..checks import _check_type
from ..types import LazyColumns, PandasDataFrame, PandasGroupedFrame

def group(df: PandasDataFrame, by: LazyColumns) -> PandasGroupedFrame:

    _check_type(by, {list, str})
    gdf = df.groupby(by, as_index=False, sort=False)
    return gdf


Prompt:
import pandas as pd  # pyright: ignore[reportMissingImports]

from ..checks import _check_type
from ..types import LazyColumns, PandasDataFrame

def select(df: PandasDataFrame, columns: LazyColumns) -> PandasDataFrame:

    _check_type(columns, {list, str})
    columns = [columns] if isinstance(columns, str) else columns
    if len(set(columns)) != len(columns):
        raise KeyError(f"column keys must be unique")
    bad_columns = list(set(columns) - set(df.columns))
    if bad_columns and len(bad_columns) == 1:
        raise KeyError(f"column key: {bad_columns} is invalid")
    if bad_columns and len(bad_columns) > 1:
        raise KeyError(f"column keys: {bad_columns} are invalid")
    df = df[columns]
    return df


Prompt:
from ..checks import _check_file, _check_type
from ..core import DataFrame

def save(df: DataFrame, path: str, **kwargs) -> None:
    """Save a rf.DataFrame to a csv file (opposite of `load`)

    Example:

    ```python
    rf.save(df, "example.csv")
    ```
    """

    _check_type(df, DataFrame)
    _check_type(path, str)
    _check_file(path)
    df._data.to_csv(path, index=False, **kwargs)


Prompt:
import pandas as pd  # pyright: ignore[reportMissingImports]

from redframes.types import PandasDataFrame

from ..checks import _check_columns, _check_file, _check_index, _check_type
from ..core import DataFrame, _wrap

def load(path: str, **kwargs) -> DataFrame:
    """Load a csv file into a rf.DataFrame (opposite of `save`)

    Example:

    ```python
    df = rf.load("example.csv")
    ```
    """

    _check_type(path, str)
    _check_file(path)
    data: PandasDataFrame = pd.read_csv(path, **kwargs)  # type: ignore
    _check_index(data)
    _check_columns(data)
    return _wrap(data)


Prompt:
from __future__ import annotations

from ..checks import _check_columns, _check_index, _check_type
from ..core import DataFrame
from ..types import PandasDataFrame

def unwrap(rdf: DataFrame) -> PandasDataFrame:
    """Convert a rf.DataFrame into a pd.DataFrame (opposite of `wrap`)

    Example:

    ```python
    rdf = rf.DataFrame({"foo": range(10)})
    pdf = rf.unwrap(rdf)
    ```
    """
    _check_type(rdf, DataFrame)
    return rdf._data.copy()

def wrap(pdf: PandasDataFrame) -> DataFrame:
    """Convert a pd.DataFrame into a rf.DataFrame (opposite of `unwrap`)

    Example:

    ```python
    pdf = pd.DataFrame({"foo": range(10)})
    rdf = rf.wrap(pdf)
    ```
    """

    _check_type(pdf, PandasDataFrame)
    _check_index(pdf)
    _check_columns(pdf)
    rdf = DataFrame()
    rdf._data = pdf.copy()
    return rdf


Prompt:
from __future__ import annotations

import pprint
import warnings

from .checks import _check_type
from .types import (
    Any,
    Column,
    Columns,
    DateTime,
    Direction,
    Func,
    Join,
    LazyColumns,
    NewColumn,
    NewValue,
    NumpyArray,
    NumpyType,
    OldColumn,
    OldValue,
    PandasDataFrame,
    PandasGroupedFrame,
    Value,
    Values,
)
from .verbs import (
    accumulate,
    append,
    combine,
    cross,
    dedupe,
    denix,
    drop,
    fill,
    filter,
    gather,
    group,
    join,
    mutate,
    pack,
    rank,
    rename,
    replace,
    rollup,
    sample,
    select,
    shuffle,
    sort,
    split,
    spread,
    take,
    unpack,
)

def _wrap(data: PandasDataFrame) -> DataFrame:
    """Unsafe version of redframes.io.wrap()"""

    df = DataFrame()
    df._data = data
    return df


Prompt:
from __future__ import annotations

import pprint
import warnings

from .checks import _check_type
from .types import (
    Any,
    Column,
    Columns,
    DateTime,
    Direction,
    Func,
    Join,
    LazyColumns,
    NewColumn,
    NewValue,
    NumpyArray,
    NumpyType,
    OldColumn,
    OldValue,
    PandasDataFrame,
    PandasGroupedFrame,
    Value,
    Values,
)
from .verbs import (
    accumulate,
    append,
    combine,
    cross,
    dedupe,
    denix,
    drop,
    fill,
    filter,
    gather,
    group,
    join,
    mutate,
    pack,
    rank,
    rename,
    replace,
    rollup,
    sample,
    select,
    shuffle,
    sort,
    split,
    spread,
    take,
    unpack,
)

def _wrap(data: PandasDataFrame) -> DataFrame:
    """Unsafe version of redframes.io.wrap()"""
    df = DataFrame()
    df._data = data
    return df

class _TakeMixin:
    def __init__(self, data: PandasDataFrame | PandasGroupedFrame) -> None:
        self._data = data

    def take(self, rows: int, **kwargs) -> DataFrame:
        """Take any number of rows (from the top/bottom)

        Examples:

        ```python
        df = rf.DataFrame({"foo": range(10)})
        ```
        |   foo |
        |------:|
        |     0 |
        |     1 |
        |     2 |
        |     3 |
        |     4 |
        |     5 |
        |     6 |
        |     7 |
        |     8 |
        |     9 |

        From "head":

        ```python
        df.take(1)
        ```
        |   foo |
        |------:|
        |     0 |

        From "tail":

        ```python
        df.take(-2)
        ```
        |   foo |
        |------:|
        |     8 |
        |     9 |
        """
        return _wrap(take(self._data, rows, **kwargs))

class _InterchangeMixin(_TakeMixin):
    def __init__(self, data: PandasDataFrame) -> None:
        self._data = data

    def __array__(self) -> NumpyArray:
        return self._data.__array__()

    def __dataframe__(self, nan_as_null=False, allow_copy=True) -> "PandasDataFrameXchg":  # type: ignore
        return self._data.__dataframe__(nan_as_null, allow_copy)

    def __len__(self) -> int:
        return self._data.__len__()

    @property
    def iloc(self):
        return self._data.iloc

class _CommonMixin(_TakeMixin):
    def __init__(self, data: PandasDataFrame | PandasGroupedFrame) -> None:
        self._data = data

    def accumulate(self, column: Column, into: Column) -> DataFrame:
        """Run a cumulative sum over a column

        Example:

        ```python
        df = rf.DataFrame({"foo": [1, 2, 3, 4]})
        ```
        |   foo |
        |------:|
        |     1 |
        |     2 |
        |     3 |
        |     4 |

        ```python
        df.accumulate("foo", into="cumsum")
        ```
        |   foo |   cumsum |
        |------:|---------:|
        |     1 |        1 |
        |     2 |        3 |
        |     3 |        6 |
        |     4 |       10 |
        """
        return _wrap(accumulate(self._data, column, into))

    def gather(
        self,
        columns: Columns | None = None,
        beside: LazyColumns | None = None,
        into: tuple[Column, Column] = ("variable", "value"),
    ):
        """Gather columns into rows (opposite of spread)

        Examples:

        ```python
        df = rf.DataFrame({
            "foo": [1, 2, 1, 2],
            "bar": ["A", "B", "C", "D"],
            "baz": ["!", "@", "#", "$"],
            "jaz": range(4)
        })
        ```
        |   foo | bar   | baz   |   jaz |
        |------:|:------|:------|------:|
        |     1 | A     | !     |     0 |
        |     2 | B     | @     |     1 |
        |     1 | C     | #     |     2 |
        |     2 | D     | $     |     3 |

        All columns:

        ```python
        df.gather()
        ```
        | variable   | value   |
        |:-----------|:--------|
        | foo        | 1       |
        | foo        | 2       |
        | foo        | 1       |
        | foo        | 2       |
        | bar        | A       |
        | bar        | B       |
        | bar        | C       |
        | bar        | D       |
        | baz        | !       |
        | baz        | @       |
        | baz        | #       |
        | baz        | $       |
        | jaz        | 0       |
        | jaz        | 1       |
        | jaz        | 2       |
        | jaz        | 3       |

        Multiple columns:

        ```python
        df.gather(["foo", "bar"], into=("var", "val"))
        ```
        | baz   |   jaz | var   | val   |
        |:------|------:|:------|:------|
        | !     |     0 | foo   | 1     |
        | @     |     1 | foo   | 2     |
        | #     |     2 | foo   | 1     |
        | $     |     3 | foo   | 2     |
        | !     |     0 | bar   | A     |
        | @     |     1 | bar   | B     |
        | #     |     2 | bar   | C     |
        | $     |     3 | bar   | D     |

        All columns beside:

        ```python
        df.group(["foo", "bar"]).gather(into=("variable", "value"))
        ```
        |   foo | bar   | variable   | value   |
        |------:|:------|:-----------|:--------|
        |     1 | A     | baz        | !       |
        |     2 | B     | baz        | @       |
        |     1 | C     | baz        | #       |
        |     2 | D     | baz        | $       |
        |     1 | A     | jaz        | 0       |
        |     2 | B     | jaz        | 1       |
        |     1 | C     | jaz        | 2       |
        |     2 | D     | jaz        | 3       |
        """
        return _wrap(gather(self._data, columns, beside, into))

    def pack(self, column: Column, sep: str) -> DataFrame:
        """Collate and concatenate row values for a target column (opposite of unpack)

        Examples:

        ```python
        df = rf.DataFrame({
            "foo": ["A", "A", "B", "A", "B", "C"],
            "bar": [1, 2, 3, 4, 5, 6]
        })
        ```
        | foo   |   bar |
        |:------|------:|
        | A     |     1 |
        | A     |     2 |
        | B     |     3 |
        | A     |     4 |
        | B     |     5 |
        | C     |     6 |

        Pack all rows:

        ```python
        df.pack("foo", sep="+")
        ```
        | foo         |
        |:------------|
        | A+A+B+A+B+C |

        Pack rows by Group:

        ```python
        df.group("foo").pack("bar", sep="|")
        ```
        | foo   | bar   |
        |:------|:------|
        | A     | 1|2|4 |
        | B     | 3|5   |
        | C     | 6     |
        """
        return _wrap(pack(self._data, column, sep))

    def rank(
        self,
        column: Column,
        into: Column,
        descending: bool = False,
    ) -> DataFrame:
        """Rank order values in a column

        Example:

        ```python
        df = rf.DataFrame({"foo": [2, 3, 3, 99, 1000, 1, -6, 4]})
        ```
        |   foo |
        |------:|
        |     2 |
        |     3 |
        |     3 |
        |    99 |
        |  1000 |
        |     1 |
        |    -6 |
        |     4 |

        ```python
        df.rank("foo", into="rank", descending=True)
        ```
        |   foo |   rank |
        |------:|-------:|
        |     2 |      5 |
        |     3 |      4 |
        |     3 |      4 |
        |    99 |      2 |
        |  1000 |      1 |
        |     1 |      6 |
        |    -6 |      7 |
        |     4 |      3 |
        """
        return _wrap(rank(self._data, column, into, descending))

    def rollup(self, over: dict[Column, tuple[Column, Func]]) -> DataFrame:
        """Apply summary functions and/or statistics to target columns

        Example:

        ```python
        df = rf.DataFrame({"foo": [1, 2, 3, 4, 5], "bar": [99, 100, 1, -5, 2]})
        ```
        |   foo |   bar |
        |------:|------:|
        |     1 |    99 |
        |     2 |   100 |
        |     3 |     1 |
        |     4 |    -5 |
        |     5 |     2 |

        ```python
        df.rollup({
            "fcount": ("foo", rf.stat.count),
            "fmean": ("foo", rf.stat.mean),
            "fsum": ("foo", rf.stat.sum),
            "fmax": ("foo", rf.stat.max),
            "bmedian": ("bar", rf.stat.median),
            "bmin": ("bar", rf.stat.min),
            "bstd": ("bar", rf.stat.std)
        })
        ```
        |   fcount |   fmean |   fsum |   fmax |   bmedian |   bmin |   bstd |
        |---------:|--------:|-------:|-------:|----------:|-------:|-------:|
        |        5 |       3 |     15 |      5 |         2 |     -5 |  54.93 |
        """
        return _wrap(rollup(self._data, over))

    def summarize(self, over: dict[Column, tuple[Column, Func]]) -> DataFrame:

        message = "Marked for removal, please use `rollup` instead"
        warnings.warn(message, FutureWarning)
        return self.rollup(over)


Prompt:
from __future__ import annotations

import pprint
import warnings

from .checks import _check_type
from .types import (
    Any,
    Column,
    Columns,
    DateTime,
    Direction,
    Func,
    Join,
    LazyColumns,
    NewColumn,
    NewValue,
    NumpyArray,
    NumpyType,
    OldColumn,
    OldValue,
    PandasDataFrame,
    PandasGroupedFrame,
    Value,
    Values,
)
from .verbs import (
    accumulate,
    append,
    combine,
    cross,
    dedupe,
    denix,
    drop,
    fill,
    filter,
    gather,
    group,
    join,
    mutate,
    pack,
    rank,
    rename,
    replace,
    rollup,
    sample,
    select,
    shuffle,
    sort,
    split,
    spread,
    take,
    unpack,
)

def _wrap(data: PandasDataFrame) -> DataFrame:
    """Unsafe version of redframes.io.wrap()"""
    df = DataFrame()
    df._data = data
    return df

class _TakeMixin:
    def __init__(self, data: PandasDataFrame | PandasGroupedFrame) -> None:
        self._data = data

    def take(self, rows: int, **kwargs) -> DataFrame:
        """Take any number of rows (from the top/bottom)

        Examples:

        ```python
        df = rf.DataFrame({"foo": range(10)})
        ```
        |   foo |
        |------:|
        |     0 |
        |     1 |
        |     2 |
        |     3 |
        |     4 |
        |     5 |
        |     6 |
        |     7 |
        |     8 |
        |     9 |

        From "head":

        ```python
        df.take(1)
        ```
        |   foo |
        |------:|
        |     0 |

        From "tail":

        ```python
        df.take(-2)
        ```
        |   foo |
        |------:|
        |     8 |
        |     9 |
        """
        return _wrap(take(self._data, rows, **kwargs))

class _InterchangeMixin(_TakeMixin):
    def __init__(self, data: PandasDataFrame) -> None:
        self._data = data

    def __array__(self) -> NumpyArray:
        return self._data.__array__()

    def __dataframe__(self, nan_as_null=False, allow_copy=True) -> "PandasDataFrameXchg":  # type: ignore
        return self._data.__dataframe__(nan_as_null, allow_copy)

    def __len__(self) -> int:
        return self._data.__len__()

    @property
    def iloc(self):
        return self._data.iloc

class _CommonMixin(_TakeMixin):
    def __init__(self, data: PandasDataFrame | PandasGroupedFrame) -> None:
        self._data = data

    def accumulate(self, column: Column, into: Column) -> DataFrame:
        """Run a cumulative sum over a column

        Example:

        ```python
        df = rf.DataFrame({"foo": [1, 2, 3, 4]})
        ```
        |   foo |
        |------:|
        |     1 |
        |     2 |
        |     3 |
        |     4 |

        ```python
        df.accumulate("foo", into="cumsum")
        ```
        |   foo |   cumsum |
        |------:|---------:|
        |     1 |        1 |
        |     2 |        3 |
        |     3 |        6 |
        |     4 |       10 |
        """
        return _wrap(accumulate(self._data, column, into))

    def gather(
        self,
        columns: Columns | None = None,
        beside: LazyColumns | None = None,
        into: tuple[Column, Column] = ("variable", "value"),
    ):
        """Gather columns into rows (opposite of spread)

        Examples:

        ```python
        df = rf.DataFrame({
            "foo": [1, 2, 1, 2],
            "bar": ["A", "B", "C", "D"],
            "baz": ["!", "@", "#", "$"],
            "jaz": range(4)
        })
        ```
        |   foo | bar   | baz   |   jaz |
        |------:|:------|:------|------:|
        |     1 | A     | !     |     0 |
        |     2 | B     | @     |     1 |
        |     1 | C     | #     |     2 |
        |     2 | D     | $     |     3 |

        All columns:

        ```python
        df.gather()
        ```
        | variable   | value   |
        |:-----------|:--------|
        | foo        | 1       |
        | foo        | 2       |
        | foo        | 1       |
        | foo        | 2       |
        | bar        | A       |
        | bar        | B       |
        | bar        | C       |
        | bar        | D       |
        | baz        | !       |
        | baz        | @       |
        | baz        | #       |
        | baz        | $       |
        | jaz        | 0       |
        | jaz        | 1       |
        | jaz        | 2       |
        | jaz        | 3       |

        Multiple columns:

        ```python
        df.gather(["foo", "bar"], into=("var", "val"))
        ```
        | baz   |   jaz | var   | val   |
        |:------|------:|:------|:------|
        | !     |     0 | foo   | 1     |
        | @     |     1 | foo   | 2     |
        | #     |     2 | foo   | 1     |
        | $     |     3 | foo   | 2     |
        | !     |     0 | bar   | A     |
        | @     |     1 | bar   | B     |
        | #     |     2 | bar   | C     |
        | $     |     3 | bar   | D     |

        All columns beside:

        ```python
        df.group(["foo", "bar"]).gather(into=("variable", "value"))
        ```
        |   foo | bar   | variable   | value   |
        |------:|:------|:-----------|:--------|
        |     1 | A     | baz        | !       |
        |     2 | B     | baz        | @       |
        |     1 | C     | baz        | #       |
        |     2 | D     | baz        | $       |
        |     1 | A     | jaz        | 0       |
        |     2 | B     | jaz        | 1       |
        |     1 | C     | jaz        | 2       |
        |     2 | D     | jaz        | 3       |
        """
        return _wrap(gather(self._data, columns, beside, into))

    def pack(self, column: Column, sep: str) -> DataFrame:
        """Collate and concatenate row values for a target column (opposite of unpack)

        Examples:

        ```python
        df = rf.DataFrame({
            "foo": ["A", "A", "B", "A", "B", "C"],
            "bar": [1, 2, 3, 4, 5, 6]
        })
        ```
        | foo   |   bar |
        |:------|------:|
        | A     |     1 |
        | A     |     2 |
        | B     |     3 |
        | A     |     4 |
        | B     |     5 |
        | C     |     6 |

        Pack all rows:

        ```python
        df.pack("foo", sep="+")
        ```
        | foo         |
        |:------------|
        | A+A+B+A+B+C |

        Pack rows by Group:

        ```python
        df.group("foo").pack("bar", sep="|")
        ```
        | foo   | bar   |
        |:------|:------|
        | A     | 1|2|4 |
        | B     | 3|5   |
        | C     | 6     |
        """
        return _wrap(pack(self._data, column, sep))

    def rank(
        self,
        column: Column,
        into: Column,
        descending: bool = False,
    ) -> DataFrame:
        """Rank order values in a column

        Example:

        ```python
        df = rf.DataFrame({"foo": [2, 3, 3, 99, 1000, 1, -6, 4]})
        ```
        |   foo |
        |------:|
        |     2 |
        |     3 |
        |     3 |
        |    99 |
        |  1000 |
        |     1 |
        |    -6 |
        |     4 |

        ```python
        df.rank("foo", into="rank", descending=True)
        ```
        |   foo |   rank |
        |------:|-------:|
        |     2 |      5 |
        |     3 |      4 |
        |     3 |      4 |
        |    99 |      2 |
        |  1000 |      1 |
        |     1 |      6 |
        |    -6 |      7 |
        |     4 |      3 |
        """
        return _wrap(rank(self._data, column, into, descending))

    def rollup(self, over: dict[Column, tuple[Column, Func]]) -> DataFrame:
        """Apply summary functions and/or statistics to target columns

        Example:

        ```python
        df = rf.DataFrame({"foo": [1, 2, 3, 4, 5], "bar": [99, 100, 1, -5, 2]})
        ```
        |   foo |   bar |
        |------:|------:|
        |     1 |    99 |
        |     2 |   100 |
        |     3 |     1 |
        |     4 |    -5 |
        |     5 |     2 |

        ```python
        df.rollup({
            "fcount": ("foo", rf.stat.count),
            "fmean": ("foo", rf.stat.mean),
            "fsum": ("foo", rf.stat.sum),
            "fmax": ("foo", rf.stat.max),
            "bmedian": ("bar", rf.stat.median),
            "bmin": ("bar", rf.stat.min),
            "bstd": ("bar", rf.stat.std)
        })
        ```
        |   fcount |   fmean |   fsum |   fmax |   bmedian |   bmin |   bstd |
        |---------:|--------:|-------:|-------:|----------:|-------:|-------:|
        |        5 |       3 |     15 |      5 |         2 |     -5 |  54.93 |
        """
        return _wrap(rollup(self._data, over))

    def summarize(self, over: dict[Column, tuple[Column, Func]]) -> DataFrame:
        message = "Marked for removal, please use `rollup` instead"
        warnings.warn(message, FutureWarning)
        return self.rollup(over)

class GroupedFrame(_CommonMixin):
    """GroupedFrame compatible with: `accumulate`, `gather`, `pack`, `rank`, `rollup`, `take`"""

    def __repr__(self) -> str:
        return self._data.obj.__repr__()  # type: ignore

    def _repr_html_(self) -> str:
        return self._data.obj.to_html(index=True)  # type: ignore

class DataFrame(_CommonMixin, _InterchangeMixin):
    def __init__(self, data: dict[Column, Values] | None = None) -> None:
        """Initialize a DataFrame with a standard dictionary

        Example:

        ```python
        df = rf.DataFrame({"foo": [1, 2], "bar": ["A", "B"]})
        ```
        |   foo | bar   |
        |------:|:------|
        |     1 | A     |
        |     2 | B     |
        """

        _check_type(data, {dict, None})
        if not data:
            self._data = PandasDataFrame()
        if isinstance(data, dict):
            self._data = PandasDataFrame(data)


Prompt:
from __future__ import annotations

import pprint
import warnings

from .checks import _check_type
from .types import (
    Any,
    Column,
    Columns,
    DateTime,
    Direction,
    Func,
    Join,
    LazyColumns,
    NewColumn,
    NewValue,
    NumpyArray,
    NumpyType,
    OldColumn,
    OldValue,
    PandasDataFrame,
    PandasGroupedFrame,
    Value,
    Values,
)
from .verbs import (
    accumulate,
    append,
    combine,
    cross,
    dedupe,
    denix,
    drop,
    fill,
    filter,
    gather,
    group,
    join,
    mutate,
    pack,
    rank,
    rename,
    replace,
    rollup,
    sample,
    select,
    shuffle,
    sort,
    split,
    spread,
    take,
    unpack,
)

def _wrap(data: PandasDataFrame) -> DataFrame:
    """Unsafe version of redframes.io.wrap()"""
    df = DataFrame()
    df._data = data
    return df

class _TakeMixin:
    def __init__(self, data: PandasDataFrame | PandasGroupedFrame) -> None:
        self._data = data

    def take(self, rows: int, **kwargs) -> DataFrame:
        """Take any number of rows (from the top/bottom)

        Examples:

        ```python
        df = rf.DataFrame({"foo": range(10)})
        ```
        |   foo |
        |------:|
        |     0 |
        |     1 |
        |     2 |
        |     3 |
        |     4 |
        |     5 |
        |     6 |
        |     7 |
        |     8 |
        |     9 |

        From "head":

        ```python
        df.take(1)
        ```
        |   foo |
        |------:|
        |     0 |

        From "tail":

        ```python
        df.take(-2)
        ```
        |   foo |
        |------:|
        |     8 |
        |     9 |
        """
        return _wrap(take(self._data, rows, **kwargs))

class _InterchangeMixin(_TakeMixin):
    def __init__(self, data: PandasDataFrame) -> None:
        self._data = data

    def __array__(self) -> NumpyArray:
        return self._data.__array__()

    def __dataframe__(self, nan_as_null=False, allow_copy=True) -> "PandasDataFrameXchg":  # type: ignore
        return self._data.__dataframe__(nan_as_null, allow_copy)

    def __len__(self) -> int:
        return self._data.__len__()

    @property
    def iloc(self):
        return self._data.iloc

class _CommonMixin(_TakeMixin):
    def __init__(self, data: PandasDataFrame | PandasGroupedFrame) -> None:
        self._data = data

    def accumulate(self, column: Column, into: Column) -> DataFrame:
        """Run a cumulative sum over a column

        Example:

        ```python
        df = rf.DataFrame({"foo": [1, 2, 3, 4]})
        ```
        |   foo |
        |------:|
        |     1 |
        |     2 |
        |     3 |
        |     4 |

        ```python
        df.accumulate("foo", into="cumsum")
        ```
        |   foo |   cumsum |
        |------:|---------:|
        |     1 |        1 |
        |     2 |        3 |
        |     3 |        6 |
        |     4 |       10 |
        """
        return _wrap(accumulate(self._data, column, into))

    def gather(
        self,
        columns: Columns | None = None,
        beside: LazyColumns | None = None,
        into: tuple[Column, Column] = ("variable", "value"),
    ):
        """Gather columns into rows (opposite of spread)

        Examples:

        ```python
        df = rf.DataFrame({
            "foo": [1, 2, 1, 2],
            "bar": ["A", "B", "C", "D"],
            "baz": ["!", "@", "#", "$"],
            "jaz": range(4)
        })
        ```
        |   foo | bar   | baz   |   jaz |
        |------:|:------|:------|------:|
        |     1 | A     | !     |     0 |
        |     2 | B     | @     |     1 |
        |     1 | C     | #     |     2 |
        |     2 | D     | $     |     3 |

        All columns:

        ```python
        df.gather()
        ```
        | variable   | value   |
        |:-----------|:--------|
        | foo        | 1       |
        | foo        | 2       |
        | foo        | 1       |
        | foo        | 2       |
        | bar        | A       |
        | bar        | B       |
        | bar        | C       |
        | bar        | D       |
        | baz        | !       |
        | baz        | @       |
        | baz        | #       |
        | baz        | $       |
        | jaz        | 0       |
        | jaz        | 1       |
        | jaz        | 2       |
        | jaz        | 3       |

        Multiple columns:

        ```python
        df.gather(["foo", "bar"], into=("var", "val"))
        ```
        | baz   |   jaz | var   | val   |
        |:------|------:|:------|:------|
        | !     |     0 | foo   | 1     |
        | @     |     1 | foo   | 2     |
        | #     |     2 | foo   | 1     |
        | $     |     3 | foo   | 2     |
        | !     |     0 | bar   | A     |
        | @     |     1 | bar   | B     |
        | #     |     2 | bar   | C     |
        | $     |     3 | bar   | D     |

        All columns beside:

        ```python
        df.group(["foo", "bar"]).gather(into=("variable", "value"))
        ```
        |   foo | bar   | variable   | value   |
        |------:|:------|:-----------|:--------|
        |     1 | A     | baz        | !       |
        |     2 | B     | baz        | @       |
        |     1 | C     | baz        | #       |
        |     2 | D     | baz        | $       |
        |     1 | A     | jaz        | 0       |
        |     2 | B     | jaz        | 1       |
        |     1 | C     | jaz        | 2       |
        |     2 | D     | jaz        | 3       |
        """
        return _wrap(gather(self._data, columns, beside, into))

    def pack(self, column: Column, sep: str) -> DataFrame:
        """Collate and concatenate row values for a target column (opposite of unpack)

        Examples:

        ```python
        df = rf.DataFrame({
            "foo": ["A", "A", "B", "A", "B", "C"],
            "bar": [1, 2, 3, 4, 5, 6]
        })
        ```
        | foo   |   bar |
        |:------|------:|
        | A     |     1 |
        | A     |     2 |
        | B     |     3 |
        | A     |     4 |
        | B     |     5 |
        | C     |     6 |

        Pack all rows:

        ```python
        df.pack("foo", sep="+")
        ```
        | foo         |
        |:------------|
        | A+A+B+A+B+C |

        Pack rows by Group:

        ```python
        df.group("foo").pack("bar", sep="|")
        ```
        | foo   | bar   |
        |:------|:------|
        | A     | 1|2|4 |
        | B     | 3|5   |
        | C     | 6     |
        """
        return _wrap(pack(self._data, column, sep))

    def rank(
        self,
        column: Column,
        into: Column,
        descending: bool = False,
    ) -> DataFrame:
        """Rank order values in a column

        Example:

        ```python
        df = rf.DataFrame({"foo": [2, 3, 3, 99, 1000, 1, -6, 4]})
        ```
        |   foo |
        |------:|
        |     2 |
        |     3 |
        |     3 |
        |    99 |
        |  1000 |
        |     1 |
        |    -6 |
        |     4 |

        ```python
        df.rank("foo", into="rank", descending=True)
        ```
        |   foo |   rank |
        |------:|-------:|
        |     2 |      5 |
        |     3 |      4 |
        |     3 |      4 |
        |    99 |      2 |
        |  1000 |      1 |
        |     1 |      6 |
        |    -6 |      7 |
        |     4 |      3 |
        """
        return _wrap(rank(self._data, column, into, descending))

    def rollup(self, over: dict[Column, tuple[Column, Func]]) -> DataFrame:
        """Apply summary functions and/or statistics to target columns

        Example:

        ```python
        df = rf.DataFrame({"foo": [1, 2, 3, 4, 5], "bar": [99, 100, 1, -5, 2]})
        ```
        |   foo |   bar |
        |------:|------:|
        |     1 |    99 |
        |     2 |   100 |
        |     3 |     1 |
        |     4 |    -5 |
        |     5 |     2 |

        ```python
        df.rollup({
            "fcount": ("foo", rf.stat.count),
            "fmean": ("foo", rf.stat.mean),
            "fsum": ("foo", rf.stat.sum),
            "fmax": ("foo", rf.stat.max),
            "bmedian": ("bar", rf.stat.median),
            "bmin": ("bar", rf.stat.min),
            "bstd": ("bar", rf.stat.std)
        })
        ```
        |   fcount |   fmean |   fsum |   fmax |   bmedian |   bmin |   bstd |
        |---------:|--------:|-------:|-------:|----------:|-------:|-------:|
        |        5 |       3 |     15 |      5 |         2 |     -5 |  54.93 |
        """
        return _wrap(rollup(self._data, over))

    def summarize(self, over: dict[Column, tuple[Column, Func]]) -> DataFrame:
        message = "Marked for removal, please use `rollup` instead"
        warnings.warn(message, FutureWarning)
        return self.rollup(over)

class GroupedFrame(_CommonMixin):
    """GroupedFrame compatible with: `accumulate`, `gather`, `pack`, `rank`, `rollup`, `take`"""

    def __repr__(self) -> str:
        return self._data.obj.__repr__()  # type: ignore

    def _repr_html_(self) -> str:
        return self._data.obj.to_html(index=True)  # type: ignore

class DataFrame(_CommonMixin, _InterchangeMixin):
    def __init__(self, data: dict[Column, Values] | None = None) -> None:
        """Initialize a DataFrame with a standard dictionary

        Example:

        ```python
        df = rf.DataFrame({"foo": [1, 2], "bar": ["A", "B"]})
        ```
        |   foo | bar   |
        |------:|:------|
        |     1 | A     |
        |     2 | B     |
        """
        _check_type(data, {dict, None})
        if not data:
            self._data = PandasDataFrame()
        if isinstance(data, dict):
            self._data = PandasDataFrame(data)

    def __eq__(self, rhs: Any) -> bool:
        """Check if two DataFrames are equal to each other

        Example:

        ```python
        adf = rf.DataFrame({"foo": [1]})
        bdf = rf.DataFrame({"bar": [1]})
        cdf = rf.DataFrame({"foo": [1]})
        print(adf == bdf)
        print(adf == cdf)
        # False
        # True
        ```
        """

        if not isinstance(rhs, DataFrame):
            return False
        return self._data.equals(rhs._data)


Prompt:
from __future__ import annotations

import pprint
import warnings

from .checks import _check_type
from .types import (
    Any,
    Column,
    Columns,
    DateTime,
    Direction,
    Func,
    Join,
    LazyColumns,
    NewColumn,
    NewValue,
    NumpyArray,
    NumpyType,
    OldColumn,
    OldValue,
    PandasDataFrame,
    PandasGroupedFrame,
    Value,
    Values,
)
from .verbs import (
    accumulate,
    append,
    combine,
    cross,
    dedupe,
    denix,
    drop,
    fill,
    filter,
    gather,
    group,
    join,
    mutate,
    pack,
    rank,
    rename,
    replace,
    rollup,
    sample,
    select,
    shuffle,
    sort,
    split,
    spread,
    take,
    unpack,
)

def _wrap(data: PandasDataFrame) -> DataFrame:
    """Unsafe version of redframes.io.wrap()"""
    df = DataFrame()
    df._data = data
    return df

class _TakeMixin:
    def __init__(self, data: PandasDataFrame | PandasGroupedFrame) -> None:
        self._data = data

    def take(self, rows: int, **kwargs) -> DataFrame:
        """Take any number of rows (from the top/bottom)

        Examples:

        ```python
        df = rf.DataFrame({"foo": range(10)})
        ```
        |   foo |
        |------:|
        |     0 |
        |     1 |
        |     2 |
        |     3 |
        |     4 |
        |     5 |
        |     6 |
        |     7 |
        |     8 |
        |     9 |

        From "head":

        ```python
        df.take(1)
        ```
        |   foo |
        |------:|
        |     0 |

        From "tail":

        ```python
        df.take(-2)
        ```
        |   foo |
        |------:|
        |     8 |
        |     9 |
        """
        return _wrap(take(self._data, rows, **kwargs))

class _InterchangeMixin(_TakeMixin):
    def __init__(self, data: PandasDataFrame) -> None:
        self._data = data

    def __array__(self) -> NumpyArray:
        return self._data.__array__()

    def __dataframe__(self, nan_as_null=False, allow_copy=True) -> "PandasDataFrameXchg":  # type: ignore
        return self._data.__dataframe__(nan_as_null, allow_copy)

    def __len__(self) -> int:
        return self._data.__len__()

    @property
    def iloc(self):
        return self._data.iloc

class _CommonMixin(_TakeMixin):
    def __init__(self, data: PandasDataFrame | PandasGroupedFrame) -> None:
        self._data = data

    def accumulate(self, column: Column, into: Column) -> DataFrame:
        """Run a cumulative sum over a column

        Example:

        ```python
        df = rf.DataFrame({"foo": [1, 2, 3, 4]})
        ```
        |   foo |
        |------:|
        |     1 |
        |     2 |
        |     3 |
        |     4 |

        ```python
        df.accumulate("foo", into="cumsum")
        ```
        |   foo |   cumsum |
        |------:|---------:|
        |     1 |        1 |
        |     2 |        3 |
        |     3 |        6 |
        |     4 |       10 |
        """
        return _wrap(accumulate(self._data, column, into))

    def gather(
        self,
        columns: Columns | None = None,
        beside: LazyColumns | None = None,
        into: tuple[Column, Column] = ("variable", "value"),
    ):
        """Gather columns into rows (opposite of spread)

        Examples:

        ```python
        df = rf.DataFrame({
            "foo": [1, 2, 1, 2],
            "bar": ["A", "B", "C", "D"],
            "baz": ["!", "@", "#", "$"],
            "jaz": range(4)
        })
        ```
        |   foo | bar   | baz   |   jaz |
        |------:|:------|:------|------:|
        |     1 | A     | !     |     0 |
        |     2 | B     | @     |     1 |
        |     1 | C     | #     |     2 |
        |     2 | D     | $     |     3 |

        All columns:

        ```python
        df.gather()
        ```
        | variable   | value   |
        |:-----------|:--------|
        | foo        | 1       |
        | foo        | 2       |
        | foo        | 1       |
        | foo        | 2       |
        | bar        | A       |
        | bar        | B       |
        | bar        | C       |
        | bar        | D       |
        | baz        | !       |
        | baz        | @       |
        | baz        | #       |
        | baz        | $       |
        | jaz        | 0       |
        | jaz        | 1       |
        | jaz        | 2       |
        | jaz        | 3       |

        Multiple columns:

        ```python
        df.gather(["foo", "bar"], into=("var", "val"))
        ```
        | baz   |   jaz | var   | val   |
        |:------|------:|:------|:------|
        | !     |     0 | foo   | 1     |
        | @     |     1 | foo   | 2     |
        | #     |     2 | foo   | 1     |
        | $     |     3 | foo   | 2     |
        | !     |     0 | bar   | A     |
        | @     |     1 | bar   | B     |
        | #     |     2 | bar   | C     |
        | $     |     3 | bar   | D     |

        All columns beside:

        ```python
        df.group(["foo", "bar"]).gather(into=("variable", "value"))
        ```
        |   foo | bar   | variable   | value   |
        |------:|:------|:-----------|:--------|
        |     1 | A     | baz        | !       |
        |     2 | B     | baz        | @       |
        |     1 | C     | baz        | #       |
        |     2 | D     | baz        | $       |
        |     1 | A     | jaz        | 0       |
        |     2 | B     | jaz        | 1       |
        |     1 | C     | jaz        | 2       |
        |     2 | D     | jaz        | 3       |
        """
        return _wrap(gather(self._data, columns, beside, into))

    def pack(self, column: Column, sep: str) -> DataFrame:
        """Collate and concatenate row values for a target column (opposite of unpack)

        Examples:

        ```python
        df = rf.DataFrame({
            "foo": ["A", "A", "B", "A", "B", "C"],
            "bar": [1, 2, 3, 4, 5, 6]
        })
        ```
        | foo   |   bar |
        |:------|------:|
        | A     |     1 |
        | A     |     2 |
        | B     |     3 |
        | A     |     4 |
        | B     |     5 |
        | C     |     6 |

        Pack all rows:

        ```python
        df.pack("foo", sep="+")
        ```
        | foo         |
        |:------------|
        | A+A+B+A+B+C |

        Pack rows by Group:

        ```python
        df.group("foo").pack("bar", sep="|")
        ```
        | foo   | bar   |
        |:------|:------|
        | A     | 1|2|4 |
        | B     | 3|5   |
        | C     | 6     |
        """
        return _wrap(pack(self._data, column, sep))

    def rank(
        self,
        column: Column,
        into: Column,
        descending: bool = False,
    ) -> DataFrame:
        """Rank order values in a column

        Example:

        ```python
        df = rf.DataFrame({"foo": [2, 3, 3, 99, 1000, 1, -6, 4]})
        ```
        |   foo |
        |------:|
        |     2 |
        |     3 |
        |     3 |
        |    99 |
        |  1000 |
        |     1 |
        |    -6 |
        |     4 |

        ```python
        df.rank("foo", into="rank", descending=True)
        ```
        |   foo |   rank |
        |------:|-------:|
        |     2 |      5 |
        |     3 |      4 |
        |     3 |      4 |
        |    99 |      2 |
        |  1000 |      1 |
        |     1 |      6 |
        |    -6 |      7 |
        |     4 |      3 |
        """
        return _wrap(rank(self._data, column, into, descending))

    def rollup(self, over: dict[Column, tuple[Column, Func]]) -> DataFrame:
        """Apply summary functions and/or statistics to target columns

        Example:

        ```python
        df = rf.DataFrame({"foo": [1, 2, 3, 4, 5], "bar": [99, 100, 1, -5, 2]})
        ```
        |   foo |   bar |
        |------:|------:|
        |     1 |    99 |
        |     2 |   100 |
        |     3 |     1 |
        |     4 |    -5 |
        |     5 |     2 |

        ```python
        df.rollup({
            "fcount": ("foo", rf.stat.count),
            "fmean": ("foo", rf.stat.mean),
            "fsum": ("foo", rf.stat.sum),
            "fmax": ("foo", rf.stat.max),
            "bmedian": ("bar", rf.stat.median),
            "bmin": ("bar", rf.stat.min),
            "bstd": ("bar", rf.stat.std)
        })
        ```
        |   fcount |   fmean |   fsum |   fmax |   bmedian |   bmin |   bstd |
        |---------:|--------:|-------:|-------:|----------:|-------:|-------:|
        |        5 |       3 |     15 |      5 |         2 |     -5 |  54.93 |
        """
        return _wrap(rollup(self._data, over))

    def summarize(self, over: dict[Column, tuple[Column, Func]]) -> DataFrame:
        message = "Marked for removal, please use `rollup` instead"
        warnings.warn(message, FutureWarning)
        return self.rollup(over)

class GroupedFrame(_CommonMixin):
    """GroupedFrame compatible with: `accumulate`, `gather`, `pack`, `rank`, `rollup`, `take`"""

    def __repr__(self) -> str:
        return self._data.obj.__repr__()  # type: ignore

    def _repr_html_(self) -> str:
        return self._data.obj.to_html(index=True)  # type: ignore

class DataFrame(_CommonMixin, _InterchangeMixin):
    def __init__(self, data: dict[Column, Values] | None = None) -> None:
        """Initialize a DataFrame with a standard dictionary

        Example:

        ```python
        df = rf.DataFrame({"foo": [1, 2], "bar": ["A", "B"]})
        ```
        |   foo | bar   |
        |------:|:------|
        |     1 | A     |
        |     2 | B     |
        """
        _check_type(data, {dict, None})
        if not data:
            self._data = PandasDataFrame()
        if isinstance(data, dict):
            self._data = PandasDataFrame(data)

    def __eq__(self, rhs: Any) -> bool:
        """Check if two DataFrames are equal to each other

        Example:

        ```python
        adf = rf.DataFrame({"foo": [1]})
        bdf = rf.DataFrame({"bar": [1]})
        cdf = rf.DataFrame({"foo": [1]})
        print(adf == bdf)
        print(adf == cdf)
        # False
        # True
        ```
        """
        if not isinstance(rhs, DataFrame):
            return False
        return self._data.equals(rhs._data)

    def __getitem__(self, key: Column) -> Values:
        """Retrive values (as a python list) from a specified column

        Example:

        ```python
        df = rf.DataFrame({"foo": [1, 2], "bar": ["A", "B"]})
        df["foo"]
        # [1, 2]
        ```
        """
        return list(self._data[key])

    def __repr__(self) -> str:
        return self._data.__repr__()

    def _repr_html_(self) -> str:
        return self._data.to_html(index=True)

    def __str__(self) -> str:
        """Return string constructor (for copy-and-pasting)

        Example:

        ```python
        df = rf.DataFrame({"foo": [1, 2], "bar": ["A", "B"]})
        str(df)
        # "rf.DataFrame({'foo': [1, 2], 'bar': ['A', 'B']})"
        ```
        """

        data = self._data.to_dict(orient="list")
        string = pprint.pformat(data, indent=4, sort_dicts=False, compact=True)
        if "\n" in string:
            string = " " + string[1:-1]
            string = f"rf.DataFrame({{\n{string}\n}})"
        else:
            string = f"rf.DataFrame({string})"
        return string


Prompt:
 PandasGroupedFrame,
    Value,
    Values,
)
from .verbs import (
    accumulate,
    append,
    combine,
    cross,
    dedupe,
    denix,
    drop,
    fill,
    filter,
    gather,
    group,
    join,
    mutate,
    pack,
    rank,
    rename,
    replace,
    rollup,
    sample,
    select,
    shuffle,
    sort,
    split,
    spread,
    take,
    unpack,
)

def _wrap(data: PandasDataFrame) -> DataFrame:
    """Unsafe version of redframes.io.wrap()"""
    df = DataFrame()
    df._data = data
    return df

class _TakeMixin:
    def __init__(self, data: PandasDataFrame | PandasGroupedFrame) -> None:
        self._data = data

    def take(self, rows: int, **kwargs) -> DataFrame:
        """Take any number of rows (from the top/bottom)

        Examples:

        ```python
        df = rf.DataFrame({"foo": range(10)})
        ```
        |   foo |
        |------:|
        |     0 |
        |     1 |
        |     2 |
        |     3 |
        |     4 |
        |     5 |
        |     6 |
        |     7 |
        |     8 |
        |     9 |

        From "head":

        ```python
        df.take(1)
        ```
        |   foo |
        |------:|
        |     0 |

        From "tail":

        ```python
        df.take(-2)
        ```
        |   foo |
        |------:|
        |     8 |
        |     9 |
        """
        return _wrap(take(self._data, rows, **kwargs))

class _InterchangeMixin(_TakeMixin):
    def __init__(self, data: PandasDataFrame) -> None:
        self._data = data

    def __array__(self) -> NumpyArray:
        return self._data.__array__()

    def __dataframe__(self, nan_as_null=False, allow_copy=True) -> "PandasDataFrameXchg":  # type: ignore
        return self._data.__dataframe__(nan_as_null, allow_copy)

    def __len__(self) -> int:
        return self._data.__len__()

    @property
    def iloc(self):
        return self._data.iloc

class _CommonMixin(_TakeMixin):
    def __init__(self, data: PandasDataFrame | PandasGroupedFrame) -> None:
        self._data = data

    def accumulate(self, column: Column, into: Column) -> DataFrame:
        """Run a cumulative sum over a column

        Example:

        ```python
        df = rf.DataFrame({"foo": [1, 2, 3, 4]})
        ```
        |   foo |
        |------:|
        |     1 |
        |     2 |
        |     3 |
        |     4 |

        ```python
        df.accumulate("foo", into="cumsum")
        ```
        |   foo |   cumsum |
        |------:|---------:|
        |     1 |        1 |
        |     2 |        3 |
        |     3 |        6 |
        |     4 |       10 |
        """
        return _wrap(accumulate(self._data, column, into))

    def gather(
        self,
        columns: Columns | None = None,
        beside: LazyColumns | None = None,
        into: tuple[Column, Column] = ("variable", "value"),
    ):
        """Gather columns into rows (opposite of spread)

        Examples:

        ```python
        df = rf.DataFrame({
            "foo": [1, 2, 1, 2],
            "bar": ["A", "B", "C", "D"],
            "baz": ["!", "@", "#", "$"],
            "jaz": range(4)
        })
        ```
        |   foo | bar   | baz   |   jaz |
        |------:|:------|:------|------:|
        |     1 | A     | !     |     0 |
        |     2 | B     | @     |     1 |
        |     1 | C     | #     |     2 |
        |     2 | D     | $     |     3 |

        All columns:

        ```python
        df.gather()
        ```
        | variable   | value   |
        |:-----------|:--------|
        | foo        | 1       |
        | foo        | 2       |
        | foo        | 1       |
        | foo        | 2       |
        | bar        | A       |
        | bar        | B       |
        | bar        | C       |
        | bar        | D       |
        | baz        | !       |
        | baz        | @       |
        | baz        | #       |
        | baz        | $       |
        | jaz        | 0       |
        | jaz        | 1       |
        | jaz        | 2       |
        | jaz        | 3       |

        Multiple columns:

        ```python
        df.gather(["foo", "bar"], into=("var", "val"))
        ```
        | baz   |   jaz | var   | val   |
        |:------|------:|:------|:------|
        | !     |     0 | foo   | 1     |
        | @     |     1 | foo   | 2     |
        | #     |     2 | foo   | 1     |
        | $     |     3 | foo   | 2     |
        | !     |     0 | bar   | A     |
        | @     |     1 | bar   | B     |
        | #     |     2 | bar   | C     |
        | $     |     3 | bar   | D     |

        All columns beside:

        ```python
        df.group(["foo", "bar"]).gather(into=("variable", "value"))
        ```
        |   foo | bar   | variable   | value   |
        |------:|:------|:-----------|:--------|
        |     1 | A     | baz        | !       |
        |     2 | B     | baz        | @       |
        |     1 | C     | baz        | #       |
        |     2 | D     | baz        | $       |
        |     1 | A     | jaz        | 0       |
        |     2 | B     | jaz        | 1       |
        |     1 | C     | jaz        | 2       |
        |     2 | D     | jaz        | 3       |
        """
        return _wrap(gather(self._data, columns, beside, into))

    def pack(self, column: Column, sep: str) -> DataFrame:
        """Collate and concatenate row values for a target column (opposite of unpack)

        Examples:

        ```python
        df = rf.DataFrame({
            "foo": ["A", "A", "B", "A", "B", "C"],
            "bar": [1, 2, 3, 4, 5, 6]
        })
        ```
        | foo   |   bar |
        |:------|------:|
        | A     |     1 |
        | A     |     2 |
        | B     |     3 |
        | A     |     4 |
        | B     |     5 |
        | C     |     6 |

        Pack all rows:

        ```python
        df.pack("foo", sep="+")
        ```
        | foo         |
        |:------------|
        | A+A+B+A+B+C |

        Pack rows by Group:

        ```python
        df.group("foo").pack("bar", sep="|")
        ```
        | foo   | bar   |
        |:------|:------|
        | A     | 1|2|4 |
        | B     | 3|5   |
        | C     | 6     |
        """
        return _wrap(pack(self._data, column, sep))

    def rank(
        self,
        column: Column,
        into: Column,
        descending: bool = False,
    ) -> DataFrame:
        """Rank order values in a column

        Example:

        ```python
        df = rf.DataFrame({"foo": [2, 3, 3, 99, 1000, 1, -6, 4]})
        ```
        |   foo |
        |------:|
        |     2 |
        |     3 |
        |     3 |
        |    99 |
        |  1000 |
        |     1 |
        |    -6 |
        |     4 |

        ```python
        df.rank("foo", into="rank", descending=True)
        ```
        |   foo |   rank |
        |------:|-------:|
        |     2 |      5 |
        |     3 |      4 |
        |     3 |      4 |
        |    99 |      2 |
        |  1000 |      1 |
        |     1 |      6 |
        |    -6 |      7 |
        |     4 |      3 |
        """
        return _wrap(rank(self._data, column, into, descending))

    def rollup(self, over: dict[Column, tuple[Column, Func]]) -> DataFrame:
        """Apply summary functions and/or statistics to target columns

        Example:

        ```python
        df = rf.DataFrame({"foo": [1, 2, 3, 4, 5], "bar": [99, 100, 1, -5, 2]})
        ```
        |   foo |   bar |
        |------:|------:|
        |     1 |    99 |
        |     2 |   100 |
        |     3 |     1 |
        |     4 |    -5 |
        |     5 |     2 |

        ```python
        df.rollup({
            "fcount": ("foo", rf.stat.count),
            "fmean": ("foo", rf.stat.mean),
            "fsum": ("foo", rf.stat.sum),
            "fmax": ("foo", rf.stat.max),
            "bmedian": ("bar", rf.stat.median),
            "bmin": ("bar", rf.stat.min),
            "bstd": ("bar", rf.stat.std)
        })
        ```
        |   fcount |   fmean |   fsum |   fmax |   bmedian |   bmin |   bstd |
        |---------:|--------:|-------:|-------:|----------:|-------:|-------:|
        |        5 |       3 |     15 |      5 |         2 |     -5 |  54.93 |
        """
        return _wrap(rollup(self._data, over))

    def summarize(self, over: dict[Column, tuple[Column, Func]]) -> DataFrame:
        message = "Marked for removal, please use `rollup` instead"
        warnings.warn(message, FutureWarning)
        return self.rollup(over)

class GroupedFrame(_CommonMixin):
    """GroupedFrame compatible with: `accumulate`, `gather`, `pack`, `rank`, `rollup`, `take`"""

    def __repr__(self) -> str:
        return self._data.obj.__repr__()  # type: ignore

    def _repr_html_(self) -> str:
        return self._data.obj.to_html(index=True)  # type: ignore

class DataFrame(_CommonMixin, _InterchangeMixin):
    def __init__(self, data: dict[Column, Values] | None = None) -> None:
        """Initialize a DataFrame with a standard dictionary

        Example:

        ```python
        df = rf.DataFrame({"foo": [1, 2], "bar": ["A", "B"]})
        ```
        |   foo | bar   |
        |------:|:------|
        |     1 | A     |
        |     2 | B     |
        """
        _check_type(data, {dict, None})
        if not data:
            self._data = PandasDataFrame()
        if isinstance(data, dict):
            self._data = PandasDataFrame(data)

    def __eq__(self, rhs: Any) -> bool:
        """Check if two DataFrames are equal to each other

        Example:

        ```python
        adf = rf.DataFrame({"foo": [1]})
        bdf = rf.DataFrame({"bar": [1]})
        cdf = rf.DataFrame({"foo": [1]})
        print(adf == bdf)
        print(adf == cdf)
        # False
        # True
        ```
        """
        if not isinstance(rhs, DataFrame):
            return False
        return self._data.equals(rhs._data)

    def __getitem__(self, key: Column) -> Values:
        """Retrive values (as a python list) from a specified column

        Example:

        ```python
        df = rf.DataFrame({"foo": [1, 2], "bar": ["A", "B"]})
        df["foo"]
        # [1, 2]
        ```
        """
        return list(self._data[key])

    def __repr__(self) -> str:
        return self._data.__repr__()

    def _repr_html_(self) -> str:
        return self._data.to_html(index=True)

    def __str__(self) -> str:
        """Return string constructor (for copy-and-pasting)

        Example:

        ```python
        df = rf.DataFrame({"foo": [1, 2], "bar": ["A", "B"]})
        str(df)
        # "rf.DataFrame({'foo': [1, 2], 'bar': ['A', 'B']})"
        ```
        """
        data = self._data.to_dict(orient="list")
        string = pprint.pformat(data, indent=4, sort_dicts=False, compact=True)
        if "\n" in string:
            string = " " + string[1:-1]
            string = f"rf.DataFrame({{\n{string}\n}})"
        else:
            string = f"rf.DataFrame({string})"
        return string

    @property
    def columns(self) -> Columns:
        """Inspect column keys (names)

        Example:

        ```python
        df = rf.DataFrame({"foo": [1, 2], "bar": ["A", "B"], "baz": [True, False]})
        df.columns
        # ['foo', 'bar', 'baz']
        ```
        """
        return list(self._data.columns)

    @property
    def dimensions(self) -> dict[str, int]:
        """Inspect DataFrame shape

        Example:

        ```python
        df = rf.DataFrame({"foo": range(10), "bar": range(10, 20)})
        df.dimensions
        # {'rows': 10, 'columns': 2}
        ```
        """
        return dict(zip(["rows", "columns"], self._data.shape))

    @property
    def empty(self) -> bool:
        """Inspect if DataFrame is "empty"

        Example:

        ```python
        df = rf.DataFrame()
        df.empty
        # True
        ```
        """
        return self._data.empty

    @property
    def memory(self) -> str:
        """Interrogate DataFrame (deep) memory usage

        Example:

        ```python
        df = rf.DataFrame({"foo": [1, 2, 3], "bar": ["A", "B", "C"]})
        df.memory
        # '326B'
        ```
        """

        size = self._data.memory_usage(deep=True).sum()
        power_labels = {40: "TB", 30: "GB", 20: "MB", 10: "KB"}
        for power, label in power_labels.items():
            if size >= (2**power):
                approx_size = size // 2**power
                return f"{approx_size} {label}"
        return f"{size} B"


Prompt:
asGroupedFrame) -> None:
        self._data = data

    def take(self, rows: int, **kwargs) -> DataFrame:
        """Take any number of rows (from the top/bottom)

        Examples:

        ```python
        df = rf.DataFrame({"foo": range(10)})
        ```
        |   foo |
        |------:|
        |     0 |
        |     1 |
        |     2 |
        |     3 |
        |     4 |
        |     5 |
        |     6 |
        |     7 |
        |     8 |
        |     9 |

        From "head":

        ```python
        df.take(1)
        ```
        |   foo |
        |------:|
        |     0 |

        From "tail":

        ```python
        df.take(-2)
        ```
        |   foo |
        |------:|
        |     8 |
        |     9 |
        """
        return _wrap(take(self._data, rows, **kwargs))

class _InterchangeMixin(_TakeMixin):
    def __init__(self, data: PandasDataFrame) -> None:
        self._data = data

    def __array__(self) -> NumpyArray:
        return self._data.__array__()

    def __dataframe__(self, nan_as_null=False, allow_copy=True) -> "PandasDataFrameXchg":  # type: ignore
        return self._data.__dataframe__(nan_as_null, allow_copy)

    def __len__(self) -> int:
        return self._data.__len__()

    @property
    def iloc(self):
        return self._data.iloc

class _CommonMixin(_TakeMixin):
    def __init__(self, data: PandasDataFrame | PandasGroupedFrame) -> None:
        self._data = data

    def accumulate(self, column: Column, into: Column) -> DataFrame:
        """Run a cumulative sum over a column

        Example:

        ```python
        df = rf.DataFrame({"foo": [1, 2, 3, 4]})
        ```
        |   foo |
        |------:|
        |     1 |
        |     2 |
        |     3 |
        |     4 |

        ```python
        df.accumulate("foo", into="cumsum")
        ```
        |   foo |   cumsum |
        |------:|---------:|
        |     1 |        1 |
        |     2 |        3 |
        |     3 |        6 |
        |     4 |       10 |
        """
        return _wrap(accumulate(self._data, column, into))

    def gather(
        self,
        columns: Columns | None = None,
        beside: LazyColumns | None = None,
        into: tuple[Column, Column] = ("variable", "value"),
    ):
        """Gather columns into rows (opposite of spread)

        Examples:

        ```python
        df = rf.DataFrame({
            "foo": [1, 2, 1, 2],
            "bar": ["A", "B", "C", "D"],
            "baz": ["!", "@", "#", "$"],
            "jaz": range(4)
        })
        ```
        |   foo | bar   | baz   |   jaz |
        |------:|:------|:------|------:|
        |     1 | A     | !     |     0 |
        |     2 | B     | @     |     1 |
        |     1 | C     | #     |     2 |
        |     2 | D     | $     |     3 |

        All columns:

        ```python
        df.gather()
        ```
        | variable   | value   |
        |:-----------|:--------|
        | foo        | 1       |
        | foo        | 2       |
        | foo        | 1       |
        | foo        | 2       |
        | bar        | A       |
        | bar        | B       |
        | bar        | C       |
        | bar        | D       |
        | baz        | !       |
        | baz        | @       |
        | baz        | #       |
        | baz        | $       |
        | jaz        | 0       |
        | jaz        | 1       |
        | jaz        | 2       |
        | jaz        | 3       |

        Multiple columns:

        ```python
        df.gather(["foo", "bar"], into=("var", "val"))
        ```
        | baz   |   jaz | var   | val   |
        |:------|------:|:------|:------|
        | !     |     0 | foo   | 1     |
        | @     |     1 | foo   | 2     |
        | #     |     2 | foo   | 1     |
        | $     |     3 | foo   | 2     |
        | !     |     0 | bar   | A     |
        | @     |     1 | bar   | B     |
        | #     |     2 | bar   | C     |
        | $     |     3 | bar   | D     |

        All columns beside:

        ```python
        df.group(["foo", "bar"]).gather(into=("variable", "value"))
        ```
        |   foo | bar   | variable   | value   |
        |------:|:------|:-----------|:--------|
        |     1 | A     | baz        | !       |
        |     2 | B     | baz        | @       |
        |     1 | C     | baz        | #       |
        |     2 | D     | baz        | $       |
        |     1 | A     | jaz        | 0       |
        |     2 | B     | jaz        | 1       |
        |     1 | C     | jaz        | 2       |
        |     2 | D     | jaz        | 3       |
        """
        return _wrap(gather(self._data, columns, beside, into))

    def pack(self, column: Column, sep: str) -> DataFrame:
        """Collate and concatenate row values for a target column (opposite of unpack)

        Examples:

        ```python
        df = rf.DataFrame({
            "foo": ["A", "A", "B", "A", "B", "C"],
            "bar": [1, 2, 3, 4, 5, 6]
        })
        ```
        | foo   |   bar |
        |:------|------:|
        | A     |     1 |
        | A     |     2 |
        | B     |     3 |
        | A     |     4 |
        | B     |     5 |
        | C     |     6 |

        Pack all rows:

        ```python
        df.pack("foo", sep="+")
        ```
        | foo         |
        |:------------|
        | A+A+B+A+B+C |

        Pack rows by Group:

        ```python
        df.group("foo").pack("bar", sep="|")
        ```
        | foo   | bar   |
        |:------|:------|
        | A     | 1|2|4 |
        | B     | 3|5   |
        | C     | 6     |
        """
        return _wrap(pack(self._data, column, sep))

    def rank(
        self,
        column: Column,
        into: Column,
        descending: bool = False,
    ) -> DataFrame:
        """Rank order values in a column

        Example:

        ```python
        df = rf.DataFrame({"foo": [2, 3, 3, 99, 1000, 1, -6, 4]})
        ```
        |   foo |
        |------:|
        |     2 |
        |     3 |
        |     3 |
        |    99 |
        |  1000 |
        |     1 |
        |    -6 |
        |     4 |

        ```python
        df.rank("foo", into="rank", descending=True)
        ```
        |   foo |   rank |
        |------:|-------:|
        |     2 |      5 |
        |     3 |      4 |
        |     3 |      4 |
        |    99 |      2 |
        |  1000 |      1 |
        |     1 |      6 |
        |    -6 |      7 |
        |     4 |      3 |
        """
        return _wrap(rank(self._data, column, into, descending))

    def rollup(self, over: dict[Column, tuple[Column, Func]]) -> DataFrame:
        """Apply summary functions and/or statistics to target columns

        Example:

        ```python
        df = rf.DataFrame({"foo": [1, 2, 3, 4, 5], "bar": [99, 100, 1, -5, 2]})
        ```
        |   foo |   bar |
        |------:|------:|
        |     1 |    99 |
        |     2 |   100 |
        |     3 |     1 |
        |     4 |    -5 |
        |     5 |     2 |

        ```python
        df.rollup({
            "fcount": ("foo", rf.stat.count),
            "fmean": ("foo", rf.stat.mean),
            "fsum": ("foo", rf.stat.sum),
            "fmax": ("foo", rf.stat.max),
            "bmedian": ("bar", rf.stat.median),
            "bmin": ("bar", rf.stat.min),
            "bstd": ("bar", rf.stat.std)
        })
        ```
        |   fcount |   fmean |   fsum |   fmax |   bmedian |   bmin |   bstd |
        |---------:|--------:|-------:|-------:|----------:|-------:|-------:|
        |        5 |       3 |     15 |      5 |         2 |     -5 |  54.93 |
        """
        return _wrap(rollup(self._data, over))

    def summarize(self, over: dict[Column, tuple[Column, Func]]) -> DataFrame:
        message = "Marked for removal, please use `rollup` instead"
        warnings.warn(message, FutureWarning)
        return self.rollup(over)

class GroupedFrame(_CommonMixin):
    """GroupedFrame compatible with: `accumulate`, `gather`, `pack`, `rank`, `rollup`, `take`"""

    def __repr__(self) -> str:
        return self._data.obj.__repr__()  # type: ignore

    def _repr_html_(self) -> str:
        return self._data.obj.to_html(index=True)  # type: ignore

class DataFrame(_CommonMixin, _InterchangeMixin):
    def __init__(self, data: dict[Column, Values] | None = None) -> None:
        """Initialize a DataFrame with a standard dictionary

        Example:

        ```python
        df = rf.DataFrame({"foo": [1, 2], "bar": ["A", "B"]})
        ```
        |   foo | bar   |
        |------:|:------|
        |     1 | A     |
        |     2 | B     |
        """
        _check_type(data, {dict, None})
        if not data:
            self._data = PandasDataFrame()
        if isinstance(data, dict):
            self._data = PandasDataFrame(data)

    def __eq__(self, rhs: Any) -> bool:
        """Check if two DataFrames are equal to each other

        Example:

        ```python
        adf = rf.DataFrame({"foo": [1]})
        bdf = rf.DataFrame({"bar": [1]})
        cdf = rf.DataFrame({"foo": [1]})
        print(adf == bdf)
        print(adf == cdf)
        # False
        # True
        ```
        """
        if not isinstance(rhs, DataFrame):
            return False
        return self._data.equals(rhs._data)

    def __getitem__(self, key: Column) -> Values:
        """Retrive values (as a python list) from a specified column

        Example:

        ```python
        df = rf.DataFrame({"foo": [1, 2], "bar": ["A", "B"]})
        df["foo"]
        # [1, 2]
        ```
        """
        return list(self._data[key])

    def __repr__(self) -> str:
        return self._data.__repr__()

    def _repr_html_(self) -> str:
        return self._data.to_html(index=True)

    def __str__(self) -> str:
        """Return string constructor (for copy-and-pasting)

        Example:

        ```python
        df = rf.DataFrame({"foo": [1, 2], "bar": ["A", "B"]})
        str(df)
        # "rf.DataFrame({'foo': [1, 2], 'bar': ['A', 'B']})"
        ```
        """
        data = self._data.to_dict(orient="list")
        string = pprint.pformat(data, indent=4, sort_dicts=False, compact=True)
        if "\n" in string:
            string = " " + string[1:-1]
            string = f"rf.DataFrame({{\n{string}\n}})"
        else:
            string = f"rf.DataFrame({string})"
        return string

    @property
    def columns(self) -> Columns:
        """Inspect column keys (names)

        Example:

        ```python
        df = rf.DataFrame({"foo": [1, 2], "bar": ["A", "B"], "baz": [True, False]})
        df.columns
        # ['foo', 'bar', 'baz']
        ```
        """
        return list(self._data.columns)

    @property
    def dimensions(self) -> dict[str, int]:
        """Inspect DataFrame shape

        Example:

        ```python
        df = rf.DataFrame({"foo": range(10), "bar": range(10, 20)})
        df.dimensions
        # {'rows': 10, 'columns': 2}
        ```
        """
        return dict(zip(["rows", "columns"], self._data.shape))

    @property
    def empty(self) -> bool:
        """Inspect if DataFrame is "empty"

        Example:

        ```python
        df = rf.DataFrame()
        df.empty
        # True
        ```
        """
        return self._data.empty

    @property
    def memory(self) -> str:
        """Interrogate DataFrame (deep) memory usage

        Example:

        ```python
        df = rf.DataFrame({"foo": [1, 2, 3], "bar": ["A", "B", "C"]})
        df.memory
        # '326B'
        ```
        """
        size = self._data.memory_usage(deep=True).sum()
        power_labels = {40: "TB", 30: "GB", 20: "MB", 10: "KB"}
        for power, label in power_labels.items():
            if size >= (2**power):
                approx_size = size // 2**power
                return f"{approx_size} {label}"
        return f"{size} B"

    @property
    def types(self) -> dict[Column, type]:
        """Inspect column types

        Example:

        ```python
        df = rf.DataFrame({"foo": [1, 2], "bar": ["A", "B"], "baz": [True, False]})
        df.types
        # {'foo': int, 'bar': object, 'baz': bool}
        ```
        """

        numpy_types = {
            NumpyType("O"): object,
            NumpyType("int64"): int,
            NumpyType("float64"): float,
            NumpyType("bool"): bool,
            NumpyType("datetime64"): DateTime,
        }
        raw_types = dict(self._data.dtypes)
        clean_types = {}
        for column in self.columns:
            current = raw_types[column]
            clean = numpy_types.get(current, current)  # type: ignore
            clean_types[column] = clean
        return clean_types


Prompt:
 foo   | 1     |
        | $     |     3 | foo   | 2     |
        | !     |     0 | bar   | A     |
        | @     |     1 | bar   | B     |
        | #     |     2 | bar   | C     |
        | $     |     3 | bar   | D     |

        All columns beside:

        ```python
        df.group(["foo", "bar"]).gather(into=("variable", "value"))
        ```
        |   foo | bar   | variable   | value   |
        |------:|:------|:-----------|:--------|
        |     1 | A     | baz        | !       |
        |     2 | B     | baz        | @       |
        |     1 | C     | baz        | #       |
        |     2 | D     | baz        | $       |
        |     1 | A     | jaz        | 0       |
        |     2 | B     | jaz        | 1       |
        |     1 | C     | jaz        | 2       |
        |     2 | D     | jaz        | 3       |
        """
        return _wrap(gather(self._data, columns, beside, into))

    def pack(self, column: Column, sep: str) -> DataFrame:
        """Collate and concatenate row values for a target column (opposite of unpack)

        Examples:

        ```python
        df = rf.DataFrame({
            "foo": ["A", "A", "B", "A", "B", "C"],
            "bar": [1, 2, 3, 4, 5, 6]
        })
        ```
        | foo   |   bar |
        |:------|------:|
        | A     |     1 |
        | A     |     2 |
        | B     |     3 |
        | A     |     4 |
        | B     |     5 |
        | C     |     6 |

        Pack all rows:

        ```python
        df.pack("foo", sep="+")
        ```
        | foo         |
        |:------------|
        | A+A+B+A+B+C |

        Pack rows by Group:

        ```python
        df.group("foo").pack("bar", sep="|")
        ```
        | foo   | bar   |
        |:------|:------|
        | A     | 1|2|4 |
        | B     | 3|5   |
        | C     | 6     |
        """
        return _wrap(pack(self._data, column, sep))

    def rank(
        self,
        column: Column,
        into: Column,
        descending: bool = False,
    ) -> DataFrame:
        """Rank order values in a column

        Example:

        ```python
        df = rf.DataFrame({"foo": [2, 3, 3, 99, 1000, 1, -6, 4]})
        ```
        |   foo |
        |------:|
        |     2 |
        |     3 |
        |     3 |
        |    99 |
        |  1000 |
        |     1 |
        |    -6 |
        |     4 |

        ```python
        df.rank("foo", into="rank", descending=True)
        ```
        |   foo |   rank |
        |------:|-------:|
        |     2 |      5 |
        |     3 |      4 |
        |     3 |      4 |
        |    99 |      2 |
        |  1000 |      1 |
        |     1 |      6 |
        |    -6 |      7 |
        |     4 |      3 |
        """
        return _wrap(rank(self._data, column, into, descending))

    def rollup(self, over: dict[Column, tuple[Column, Func]]) -> DataFrame:
        """Apply summary functions and/or statistics to target columns

        Example:

        ```python
        df = rf.DataFrame({"foo": [1, 2, 3, 4, 5], "bar": [99, 100, 1, -5, 2]})
        ```
        |   foo |   bar |
        |------:|------:|
        |     1 |    99 |
        |     2 |   100 |
        |     3 |     1 |
        |     4 |    -5 |
        |     5 |     2 |

        ```python
        df.rollup({
            "fcount": ("foo", rf.stat.count),
            "fmean": ("foo", rf.stat.mean),
            "fsum": ("foo", rf.stat.sum),
            "fmax": ("foo", rf.stat.max),
            "bmedian": ("bar", rf.stat.median),
            "bmin": ("bar", rf.stat.min),
            "bstd": ("bar", rf.stat.std)
        })
        ```
        |   fcount |   fmean |   fsum |   fmax |   bmedian |   bmin |   bstd |
        |---------:|--------:|-------:|-------:|----------:|-------:|-------:|
        |        5 |       3 |     15 |      5 |         2 |     -5 |  54.93 |
        """
        return _wrap(rollup(self._data, over))

    def summarize(self, over: dict[Column, tuple[Column, Func]]) -> DataFrame:
        message = "Marked for removal, please use `rollup` instead"
        warnings.warn(message, FutureWarning)
        return self.rollup(over)

class GroupedFrame(_CommonMixin):
    """GroupedFrame compatible with: `accumulate`, `gather`, `pack`, `rank`, `rollup`, `take`"""

    def __repr__(self) -> str:
        return self._data.obj.__repr__()  # type: ignore

    def _repr_html_(self) -> str:
        return self._data.obj.to_html(index=True)  # type: ignore

class DataFrame(_CommonMixin, _InterchangeMixin):
    def __init__(self, data: dict[Column, Values] | None = None) -> None:
        """Initialize a DataFrame with a standard dictionary

        Example:

        ```python
        df = rf.DataFrame({"foo": [1, 2], "bar": ["A", "B"]})
        ```
        |   foo | bar   |
        |------:|:------|
        |     1 | A     |
        |     2 | B     |
        """
        _check_type(data, {dict, None})
        if not data:
            self._data = PandasDataFrame()
        if isinstance(data, dict):
            self._data = PandasDataFrame(data)

    def __eq__(self, rhs: Any) -> bool:
        """Check if two DataFrames are equal to each other

        Example:

        ```python
        adf = rf.DataFrame({"foo": [1]})
        bdf = rf.DataFrame({"bar": [1]})
        cdf = rf.DataFrame({"foo": [1]})
        print(adf == bdf)
        print(adf == cdf)
        # False
        # True
        ```
        """
        if not isinstance(rhs, DataFrame):
            return False
        return self._data.equals(rhs._data)

    def __getitem__(self, key: Column) -> Values:
        """Retrive values (as a python list) from a specified column

        Example:

        ```python
        df = rf.DataFrame({"foo": [1, 2], "bar": ["A", "B"]})
        df["foo"]
        # [1, 2]
        ```
        """
        return list(self._data[key])

    def __repr__(self) -> str:
        return self._data.__repr__()

    def _repr_html_(self) -> str:
        return self._data.to_html(index=True)

    def __str__(self) -> str:
        """Return string constructor (for copy-and-pasting)

        Example:

        ```python
        df = rf.DataFrame({"foo": [1, 2], "bar": ["A", "B"]})
        str(df)
        # "rf.DataFrame({'foo': [1, 2], 'bar': ['A', 'B']})"
        ```
        """
        data = self._data.to_dict(orient="list")
        string = pprint.pformat(data, indent=4, sort_dicts=False, compact=True)
        if "\n" in string:
            string = " " + string[1:-1]
            string = f"rf.DataFrame({{\n{string}\n}})"
        else:
            string = f"rf.DataFrame({string})"
        return string

    @property
    def columns(self) -> Columns:
        """Inspect column keys (names)

        Example:

        ```python
        df = rf.DataFrame({"foo": [1, 2], "bar": ["A", "B"], "baz": [True, False]})
        df.columns
        # ['foo', 'bar', 'baz']
        ```
        """
        return list(self._data.columns)

    @property
    def dimensions(self) -> dict[str, int]:
        """Inspect DataFrame shape

        Example:

        ```python
        df = rf.DataFrame({"foo": range(10), "bar": range(10, 20)})
        df.dimensions
        # {'rows': 10, 'columns': 2}
        ```
        """
        return dict(zip(["rows", "columns"], self._data.shape))

    @property
    def empty(self) -> bool:
        """Inspect if DataFrame is "empty"

        Example:

        ```python
        df = rf.DataFrame()
        df.empty
        # True
        ```
        """
        return self._data.empty

    @property
    def memory(self) -> str:
        """Interrogate DataFrame (deep) memory usage

        Example:

        ```python
        df = rf.DataFrame({"foo": [1, 2, 3], "bar": ["A", "B", "C"]})
        df.memory
        # '326B'
        ```
        """
        size = self._data.memory_usage(deep=True).sum()
        power_labels = {40: "TB", 30: "GB", 20: "MB", 10: "KB"}
        for power, label in power_labels.items():
            if size >= (2**power):
                approx_size = size // 2**power
                return f"{approx_size} {label}"
        return f"{size} B"

    @property
    def types(self) -> dict[Column, type]:
        """Inspect column types

        Example:

        ```python
        df = rf.DataFrame({"foo": [1, 2], "bar": ["A", "B"], "baz": [True, False]})
        df.types
        # {'foo': int, 'bar': object, 'baz': bool}
        ```
        """
        numpy_types = {
            NumpyType("O"): object,
            NumpyType("int64"): int,
            NumpyType("float64"): float,
            NumpyType("bool"): bool,
            NumpyType("datetime64"): DateTime,
        }
        raw_types = dict(self._data.dtypes)
        clean_types = {}
        for column in self.columns:
            current = raw_types[column]
            clean = numpy_types.get(current, current)  # type: ignore
            clean_types[column] = clean
        return clean_types

    def append(self, other: DataFrame) -> DataFrame:
        """Append rows from another DataFrame

        Example:

        ```python
        df1 = rf.DataFrame({"foo": [1, 2], "bar": ["A", "B"]})
        ```
        |   foo | bar   |
        |------:|:------|
        |     1 | A     |
        |     2 | B     |

        ```python
        df2 = rf.DataFrame({"bar": ["C", "D"], "foo": [3, 4], "baz": ["$", "@"]})
        ```
        | bar   |   foo | baz   |
        |:------|------:|:------|
        | C     |     3 | $     |
        | D     |     4 | @     |

        ```python
        df1.append(df2)
        ```
        |   foo | bar   | baz   |
        |------:|:------|:------|
        |     1 | A     | nan   |
        |     2 | B     | nan   |
        |     3 | C     | $     |
        |     4 | D     | @     |
        """
        _check_type(other, DataFrame)
        return _wrap(append(self._data, other._data))

    def combine(
        self, columns: Columns, into: Column, sep: str, drop: bool = True
    ) -> DataFrame:
        """Combine multiple columns into a single column (opposite of `split`)

        Example:

        ```python
        df = rf.DataFrame({"foo": [1, 2], "bar": ["A", "B"]})
        ```
        |   foo | bar   |
        |------:|:------|
        |     1 | A     |
        |     2 | B     |

        ```python
        df.combine(["bar", "foo"], into="baz", sep="::", drop=True)
        ```
        | baz   |
        |:------|
        | A::1  |
        | B::2  |
        """
        return _wrap(combine(self._data, columns, into, sep, drop))

    def cross(
        self, rhs: DataFrame | None = None, postfix: tuple[str, str] = ("_lhs", "_rhs")
    ) -> DataFrame:
        """Cross join columns from another DataFrame

        Examples:

        ```python
        df = rf.DataFrame({"foo": ["a", "b", "c"], "bar": [1, 2, 3]})
        ```
        | foo   |   bar |
        |:------|------:|
        | a     |     1 |
        | b     |     2 |
        | c     |     3 |

        Self:

        ```python
        df.cross()
        ```

        | foo_lhs   |   bar_lhs | foo_rhs   |   bar_rhs |
        |:----------|----------:|:----------|----------:|
        | a         |         1 | a         |         1 |
        | a         |         1 | b         |         2 |
        | a         |         1 | c         |         3 |
        | b         |         2 | a         |         1 |
        | b         |         2 | b         |         2 |
        | b         |         2 | c         |         3 |
        | c         |         3 | a         |         1 |
        | c         |         3 | b         |         2 |
        | c         |         3 | c         |         3 |

        Two DataFrames:

        ```python
        dfa = rf.DataFrame({"foo": [1, 2, 3]})
        dfb = rf.DataFrame({"bar": [1, 2, 3]})
        dfa.cross(dfb, postfix=("_a", "_b"))
        ```

        |   foo |   bar |
        |------:|------:|
        |     1 |     1 |
        |     1 |     2 |
        |     1 |     3 |
        |     2 |     1 |
        |     2 |     2 |
        |     2 |     3 |
        |     3 |     1 |
        |     3 |     2 |
        |     3 |     3 |
        """

        rhs = self if (rhs == None) else rhs
        _check_type(rhs, DataFrame)
        return _wrap(cross(self._data, rhs._data, postfix))  # type: ignore


Prompt:
from __future__ import annotations

from .types import (
    Any,
    Columns,
    LazyColumns,
    PandasDataFrame,
    PandasIndex,
    PandasRangeIndex,
)

def _check_type(argument: Any, against: type | set[type | None]) -> None:

    if isinstance(against, set):
        if len(against) == 0:
            against = {against}  # type: ignore
    if not isinstance(against, set):
        against = {against}
    optional = None in against
    just_types = against.difference({None})
    checks = [isinstance(argument, t) for t in just_types]  # type: ignore
    if optional:
        checks += [argument == None]
    if not any(checks):
        str_types = " | ".join([t.__name__ for t in just_types])  # type: ignore
        if optional:
            str_types += " | None"
        raise TypeError(f"must be {str_types}")


Prompt:
from __future__ import annotations

from .types import (
    Any,
    Columns,
    LazyColumns,
    PandasDataFrame,
    PandasIndex,
    PandasRangeIndex,
)

def _check_type(argument: Any, against: type | set[type | None]) -> None:
    if isinstance(against, set):
        if len(against) == 0:
            against = {against}  # type: ignore
    if not isinstance(against, set):
        against = {against}
    optional = None in against
    just_types = against.difference({None})
    checks = [isinstance(argument, t) for t in just_types]  # type: ignore
    if optional:
        checks += [argument == None]
    if not any(checks):
        str_types = " | ".join([t.__name__ for t in just_types])  # type: ignore
        if optional:
            str_types += " | None"
        raise TypeError(f"must be {str_types}")

def _check_values(values: Any, type: type) -> None:
    if not all(isinstance(value, type) for value in values):
        raise TypeError(f"must be {type.__name__}")

def _check_keys(columns: LazyColumns | None, against: Columns | PandasIndex) -> None:

    if isinstance(columns, str):
        columns = [columns]
    columns = [] if (columns == None) else columns
    bad_keys = set(columns).difference(against)  # type: ignore
    if bad_keys:
        if len(bad_keys) == 1:
            raise KeyError(f"invalid key {bad_keys}")
        else:
            raise KeyError(f"invalid keys {bad_keys}")


Prompt:
from __future__ import annotations

from .types import (
    Any,
    Columns,
    LazyColumns,
    PandasDataFrame,
    PandasIndex,
    PandasRangeIndex,
)

def _check_type(argument: Any, against: type | set[type | None]) -> None:
    if isinstance(against, set):
        if len(against) == 0:
            against = {against}  # type: ignore
    if not isinstance(against, set):
        against = {against}
    optional = None in against
    just_types = against.difference({None})
    checks = [isinstance(argument, t) for t in just_types]  # type: ignore
    if optional:
        checks += [argument == None]
    if not any(checks):
        str_types = " | ".join([t.__name__ for t in just_types])  # type: ignore
        if optional:
            str_types += " | None"
        raise TypeError(f"must be {str_types}")

def _check_values(values: Any, type: type) -> None:
    if not all(isinstance(value, type) for value in values):
        raise TypeError(f"must be {type.__name__}")

def _check_keys(columns: LazyColumns | None, against: Columns | PandasIndex) -> None:
    if isinstance(columns, str):
        columns = [columns]
    columns = [] if (columns == None) else columns
    bad_keys = set(columns).difference(against)  # type: ignore
    if bad_keys:
        if len(bad_keys) == 1:
            raise KeyError(f"invalid key {bad_keys}")
        else:
            raise KeyError(f"invalid keys {bad_keys}")

def _check_index(df: PandasDataFrame) -> None:

    if not (df.index.name == None):
        raise IndexError("must be unnamed")
    if not isinstance(df.index, PandasRangeIndex):
        raise IndexError("must be range")
    if not (df.index.start == 0):
        raise IndexError("must start at 0")
    if not (df.index.step == 1):
        raise IndexError("must step by 1")


Prompt:
from __future__ import annotations

from .types import (
    Any,
    Columns,
    LazyColumns,
    PandasDataFrame,
    PandasIndex,
    PandasRangeIndex,
)

def _check_type(argument: Any, against: type | set[type | None]) -> None:
    if isinstance(against, set):
        if len(against) == 0:
            against = {against}  # type: ignore
    if not isinstance(against, set):
        against = {against}
    optional = None in against
    just_types = against.difference({None})
    checks = [isinstance(argument, t) for t in just_types]  # type: ignore
    if optional:
        checks += [argument == None]
    if not any(checks):
        str_types = " | ".join([t.__name__ for t in just_types])  # type: ignore
        if optional:
            str_types += " | None"
        raise TypeError(f"must be {str_types}")

def _check_values(values: Any, type: type) -> None:
    if not all(isinstance(value, type) for value in values):
        raise TypeError(f"must be {type.__name__}")

def _check_keys(columns: LazyColumns | None, against: Columns | PandasIndex) -> None:
    if isinstance(columns, str):
        columns = [columns]
    columns = [] if (columns == None) else columns
    bad_keys = set(columns).difference(against)  # type: ignore
    if bad_keys:
        if len(bad_keys) == 1:
            raise KeyError(f"invalid key {bad_keys}")
        else:
            raise KeyError(f"invalid keys {bad_keys}")

def _check_index(df: PandasDataFrame) -> None:
    if not (df.index.name == None):
        raise IndexError("must be unnamed")
    if not isinstance(df.index, PandasRangeIndex):
        raise IndexError("must be range")
    if not (df.index.start == 0):
        raise IndexError("must start at 0")
    if not (df.index.step == 1):
        raise IndexError("must step by 1")

def _check_columns(df: PandasDataFrame) -> None:

    if type(df.columns) != PandasIndex:
        raise KeyError("must be flat")
    if df.columns.has_duplicates:
        raise KeyError("must not contain duplicate keys")