nested-pandas 0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nested_pandas/__init__.py +8 -0
- nested_pandas/_version.py +16 -0
- nested_pandas/datasets/__init__.py +1 -0
- nested_pandas/datasets/generation.py +84 -0
- nested_pandas/example_benchmarks.py +14 -0
- nested_pandas/nestedframe/__init__.py +2 -0
- nested_pandas/nestedframe/core.py +489 -0
- nested_pandas/nestedframe/io.py +77 -0
- nested_pandas/nestedframe/utils.py +35 -0
- nested_pandas/py.typed +0 -0
- nested_pandas/series/__init__.py +0 -0
- nested_pandas/series/accessor.py +286 -0
- nested_pandas/series/dtype.py +225 -0
- nested_pandas/series/ext_array.py +742 -0
- nested_pandas/series/packer.py +300 -0
- nested_pandas/series/utils.py +41 -0
- nested_pandas/utils/__init__.py +1 -0
- nested_pandas/utils/utils.py +44 -0
- nested_pandas-0.1.dist-info/LICENSE +21 -0
- nested_pandas-0.1.dist-info/METADATA +85 -0
- nested_pandas-0.1.dist-info/RECORD +23 -0
- nested_pandas-0.1.dist-info/WHEEL +5 -0
- nested_pandas-0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
from .nestedframe import NestedFrame
|
|
2
|
+
from .nestedframe.io import read_parquet
|
|
3
|
+
|
|
4
|
+
# Import for registering
|
|
5
|
+
from .series.accessor import NestSeriesAccessor # noqa: F401
|
|
6
|
+
from .series.dtype import NestedDtype
|
|
7
|
+
|
|
8
|
+
__all__ = ["NestedDtype", "NestedFrame", "read_parquet"]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# file generated by setuptools_scm
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
TYPE_CHECKING = False
|
|
4
|
+
if TYPE_CHECKING:
|
|
5
|
+
from typing import Tuple, Union
|
|
6
|
+
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
7
|
+
else:
|
|
8
|
+
VERSION_TUPLE = object
|
|
9
|
+
|
|
10
|
+
version: str
|
|
11
|
+
__version__: str
|
|
12
|
+
__version_tuple__: VERSION_TUPLE
|
|
13
|
+
version_tuple: VERSION_TUPLE
|
|
14
|
+
|
|
15
|
+
__version__ = version = '0.1'
|
|
16
|
+
__version_tuple__ = version_tuple = (0, 1)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .generation import * # noqa
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
from nested_pandas import NestedFrame
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def generate_data(n_base, n_layer, seed=None) -> NestedFrame:
|
|
7
|
+
"""Generates a toy dataset.
|
|
8
|
+
|
|
9
|
+
Parameters
|
|
10
|
+
----------
|
|
11
|
+
n_base : int
|
|
12
|
+
The number of rows to generate for the base layer
|
|
13
|
+
n_layer : int, or dict
|
|
14
|
+
The number of rows per n_base row to generate for a nested layer.
|
|
15
|
+
Alternatively, a dictionary of layer label, layer_size pairs may be
|
|
16
|
+
specified to created multiple nested columns with custom sizing.
|
|
17
|
+
seed : int
|
|
18
|
+
A seed to use for random generation of data
|
|
19
|
+
|
|
20
|
+
Returns
|
|
21
|
+
-------
|
|
22
|
+
NestedFrame
|
|
23
|
+
The constructed NestedFrame.
|
|
24
|
+
|
|
25
|
+
Examples
|
|
26
|
+
--------
|
|
27
|
+
>>> nested_pandas.datasets.generate_data(10,100)
|
|
28
|
+
>>> nested_pandas.datasets.generate_data(10, {"nested_a": 100, "nested_b": 200})
|
|
29
|
+
"""
|
|
30
|
+
# use provided seed, "None" acts as if no seed is provided
|
|
31
|
+
randomstate = np.random.RandomState(seed=seed)
|
|
32
|
+
|
|
33
|
+
# Generate base data
|
|
34
|
+
base_data = {"a": randomstate.random(n_base), "b": randomstate.random(n_base) * 2}
|
|
35
|
+
base_nf = NestedFrame(data=base_data)
|
|
36
|
+
|
|
37
|
+
# In case of int, create a single nested layer called "nested"
|
|
38
|
+
if isinstance(n_layer, int):
|
|
39
|
+
n_layer = {"nested": n_layer}
|
|
40
|
+
|
|
41
|
+
# It should be a dictionary
|
|
42
|
+
if isinstance(n_layer, dict):
|
|
43
|
+
for key in n_layer:
|
|
44
|
+
layer_size = n_layer[key]
|
|
45
|
+
layer_data = {
|
|
46
|
+
"t": randomstate.random(layer_size * n_base) * 20,
|
|
47
|
+
"flux": randomstate.random(layer_size * n_base) * 100,
|
|
48
|
+
"band": randomstate.choice(["r", "g"], size=layer_size * n_base),
|
|
49
|
+
"index": np.arange(layer_size * n_base) % n_base,
|
|
50
|
+
}
|
|
51
|
+
layer_nf = NestedFrame(data=layer_data).set_index("index")
|
|
52
|
+
base_nf = base_nf.add_nested(layer_nf, key)
|
|
53
|
+
return base_nf
|
|
54
|
+
else:
|
|
55
|
+
raise TypeError("Input to n_layer is not an int or dict.")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def generate_parquet_file(n_base, n_layer, path, file_per_layer=False, seed=None):
|
|
59
|
+
"""Generates a toy dataset and outputs it to one or more parquet files.
|
|
60
|
+
|
|
61
|
+
Parameters
|
|
62
|
+
----------
|
|
63
|
+
n_base : int
|
|
64
|
+
The number of rows to generate for the base layer
|
|
65
|
+
n_layer : int, or dict
|
|
66
|
+
The number of rows per n_base row to generate for a nested layer.
|
|
67
|
+
Alternatively, a dictionary of layer label, layer_size pairs may be
|
|
68
|
+
specified to created multiple nested columns with custom sizing.
|
|
69
|
+
path : str,
|
|
70
|
+
The path to the parquet file to write to if `file_per_layer` is `False`,
|
|
71
|
+
and otherwise the path to the directory to write the parquet file for
|
|
72
|
+
each layer.
|
|
73
|
+
file_per_layer : bool, default=False
|
|
74
|
+
If True, write each layer to its own parquet file. Otherwise, write
|
|
75
|
+
the generated to a single parquet file representing a nested dataset.
|
|
76
|
+
seed : int, default=None
|
|
77
|
+
A seed to use for random generation of data
|
|
78
|
+
|
|
79
|
+
Returns
|
|
80
|
+
-------
|
|
81
|
+
None
|
|
82
|
+
"""
|
|
83
|
+
nf = generate_data(n_base, n_layer, seed)
|
|
84
|
+
nf.to_parquet(path, by_layer=file_per_layer)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""An example module containing simplistic methods under benchmarking."""
|
|
2
|
+
|
|
3
|
+
import random
|
|
4
|
+
import time
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def runtime_computation():
|
|
8
|
+
"""Runtime computation consuming between 0 and 5 seconds."""
|
|
9
|
+
time.sleep(random.uniform(0, 5))
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def memory_computation():
|
|
13
|
+
"""Memory computation for a random list up to 512 samples."""
|
|
14
|
+
return [0] * random.randint(0, 512)
|
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
# typing.Self and "|" union syntax don't exist in Python 3.9
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
import pandas as pd
|
|
8
|
+
import pyarrow as pa
|
|
9
|
+
from pandas._libs import lib
|
|
10
|
+
from pandas._typing import Any, AnyAll, Axis, IndexLabel
|
|
11
|
+
from pandas.api.extensions import no_default
|
|
12
|
+
|
|
13
|
+
from nested_pandas.series import packer
|
|
14
|
+
from nested_pandas.series.dtype import NestedDtype
|
|
15
|
+
|
|
16
|
+
from .utils import _ensure_spacing
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class NestedFrame(pd.DataFrame):
|
|
20
|
+
"""A Pandas Dataframe extension with support for nested structure.
|
|
21
|
+
|
|
22
|
+
See https://pandas.pydata.org/docs/development/extending.html#subclassing-pandas-data-structures
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
# normal properties
|
|
26
|
+
_metadata = ["added_property"]
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def _constructor(self) -> Self: # type: ignore[name-defined] # noqa: F821
|
|
30
|
+
return NestedFrame
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def _constructor_expanddim(self) -> Self: # type: ignore[name-defined] # noqa: F821
|
|
34
|
+
return NestedFrame
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def all_columns(self) -> dict:
|
|
38
|
+
"""returns a dictionary of columns for each base/nested dataframe"""
|
|
39
|
+
all_columns = {"base": self.columns}
|
|
40
|
+
for column in self.columns:
|
|
41
|
+
if isinstance(self[column].dtype, NestedDtype):
|
|
42
|
+
nest_cols = self[column].nest.fields
|
|
43
|
+
all_columns[column] = nest_cols
|
|
44
|
+
return all_columns
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def nested_columns(self) -> list:
|
|
48
|
+
"""retrieves the base column names for all nested dataframes"""
|
|
49
|
+
nest_cols = []
|
|
50
|
+
for column in self.columns:
|
|
51
|
+
if isinstance(self[column].dtype, NestedDtype):
|
|
52
|
+
nest_cols.append(column)
|
|
53
|
+
return nest_cols
|
|
54
|
+
|
|
55
|
+
def _is_known_hierarchical_column(self, colname) -> bool:
|
|
56
|
+
"""Determine whether a string is a known hierarchical column name"""
|
|
57
|
+
if "." in colname:
|
|
58
|
+
left, right = colname.split(".")
|
|
59
|
+
if left in self.nested_columns:
|
|
60
|
+
return right in self.all_columns[left]
|
|
61
|
+
return False
|
|
62
|
+
return False
|
|
63
|
+
|
|
64
|
+
def _is_known_column(self, colname) -> bool:
|
|
65
|
+
"""Determine whether a string is a known column name"""
|
|
66
|
+
return colname in self.columns or self._is_known_hierarchical_column(colname)
|
|
67
|
+
|
|
68
|
+
def add_nested(
|
|
69
|
+
self, obj, name: str, *, dtype: NestedDtype | pd.ArrowDtype | pa.DataType | None = None
|
|
70
|
+
) -> Self: # type: ignore[name-defined] # noqa: F821
|
|
71
|
+
"""Packs input object to a nested column and adds it to the NestedFrame
|
|
72
|
+
|
|
73
|
+
This method returns a new NestedFrame with the added nested column.
|
|
74
|
+
|
|
75
|
+
Parameters
|
|
76
|
+
----------
|
|
77
|
+
obj : pd.DataFrame or a sequence of items convertible to nested structures
|
|
78
|
+
The object to be packed into nested pd.Series and added to
|
|
79
|
+
the NestedFrame. If a DataFrame is passed, it must have non-unique
|
|
80
|
+
index values, which are used to pack the DataFrame. If a sequence
|
|
81
|
+
of elements is passed, it is packed into a nested pd.Series.
|
|
82
|
+
Sequence elements may be individual pd.DataFrames, dictionaries
|
|
83
|
+
(keys are nested column names, values are arrays of the same
|
|
84
|
+
length), or any other object convertible to pa.StructArray.
|
|
85
|
+
Additionally, None and pd.NA are allowed as elements to represent
|
|
86
|
+
missing values.
|
|
87
|
+
name : str
|
|
88
|
+
The name of the nested column to be added to the NestedFrame.
|
|
89
|
+
dtype : dtype or None
|
|
90
|
+
NestedDtype to use for the nested column; pd.ArrowDtype or
|
|
91
|
+
pa.DataType can also be used to specify the nested dtype. If None,
|
|
92
|
+
the dtype is inferred from the input object.
|
|
93
|
+
|
|
94
|
+
Returns
|
|
95
|
+
-------
|
|
96
|
+
NestedFrame
|
|
97
|
+
A new NestedFrame with the added nested column.
|
|
98
|
+
"""
|
|
99
|
+
# Add sources to objects
|
|
100
|
+
packed = packer.pack(obj, name=name, dtype=dtype)
|
|
101
|
+
label = packed.name
|
|
102
|
+
return self.assign(**{f"{label}": packed})
|
|
103
|
+
|
|
104
|
+
def _split_query(self, expr) -> dict:
|
|
105
|
+
"""Splits a pandas query into multiple subqueries for nested and base layers"""
|
|
106
|
+
# Ensure query has needed spacing for upcoming split
|
|
107
|
+
expr = _ensure_spacing(expr)
|
|
108
|
+
nest_exprs = {col: [] for col in self.nested_columns + ["base"]} # type: dict
|
|
109
|
+
split_expr = expr.split(" ")
|
|
110
|
+
|
|
111
|
+
i = 0
|
|
112
|
+
current_focus = "base"
|
|
113
|
+
while i < len(split_expr):
|
|
114
|
+
expr_slice = split_expr[i].strip("()")
|
|
115
|
+
# Check if it's a nested column
|
|
116
|
+
if self._is_known_hierarchical_column(expr_slice):
|
|
117
|
+
nested, colname = split_expr[i].split(".")
|
|
118
|
+
current_focus = nested.strip("()")
|
|
119
|
+
# account for parentheses
|
|
120
|
+
j = 0
|
|
121
|
+
while j < len(nested):
|
|
122
|
+
if nested[j] == "(":
|
|
123
|
+
nest_exprs[current_focus].append("(")
|
|
124
|
+
j += 1
|
|
125
|
+
nest_exprs[current_focus].append(colname)
|
|
126
|
+
# or if it's a top-level column
|
|
127
|
+
elif expr_slice in self.columns:
|
|
128
|
+
current_focus = "base"
|
|
129
|
+
nest_exprs[current_focus].append(split_expr[i])
|
|
130
|
+
else:
|
|
131
|
+
nest_exprs[current_focus].append(split_expr[i])
|
|
132
|
+
i += 1
|
|
133
|
+
return {expr: " ".join(nest_exprs[expr]) for expr in nest_exprs if len(nest_exprs[expr]) > 0}
|
|
134
|
+
|
|
135
|
+
def query(self, expr) -> Self: # type: ignore[name-defined] # noqa: F821
|
|
136
|
+
"""
|
|
137
|
+
Query the columns of a NestedFrame with a boolean expression. Specified
|
|
138
|
+
queries can target nested columns in addition to the typical column set
|
|
139
|
+
|
|
140
|
+
Parameters
|
|
141
|
+
----------
|
|
142
|
+
expr : str
|
|
143
|
+
The query string to evaluate.
|
|
144
|
+
|
|
145
|
+
Access nested columns using `nested_df.nested_col` (where
|
|
146
|
+
`nested_df` refers to a particular nested dataframe and
|
|
147
|
+
`nested_col` is a column of that nested dataframe).
|
|
148
|
+
|
|
149
|
+
You can refer to variables
|
|
150
|
+
in the environment by prefixing them with an '@' character like
|
|
151
|
+
``@a + b``.
|
|
152
|
+
|
|
153
|
+
You can refer to column names that are not valid Python variable names
|
|
154
|
+
by surrounding them in backticks. Thus, column names containing spaces
|
|
155
|
+
or punctuations (besides underscores) or starting with digits must be
|
|
156
|
+
surrounded by backticks. (For example, a column named "Area (cm^2)" would
|
|
157
|
+
be referenced as ```Area (cm^2)```). Column names which are Python keywords
|
|
158
|
+
(like "list", "for", "import", etc) cannot be used.
|
|
159
|
+
|
|
160
|
+
For example, if one of your columns is called ``a a`` and you want
|
|
161
|
+
to sum it with ``b``, your query should be ```a a` + b``.
|
|
162
|
+
|
|
163
|
+
Returns
|
|
164
|
+
-------
|
|
165
|
+
DataFrame
|
|
166
|
+
DataFrame resulting from the provided query expression.
|
|
167
|
+
|
|
168
|
+
Notes
|
|
169
|
+
-----
|
|
170
|
+
Queries that target a particular nested structure return a dataframe
|
|
171
|
+
with rows of that particular nested structure filtered. For example,
|
|
172
|
+
querying the NestedFrame "df" with nested structure "my_nested" as
|
|
173
|
+
below will return all rows of df, but with mynested filtered by the
|
|
174
|
+
condition:
|
|
175
|
+
|
|
176
|
+
>>> df.query("mynested.a > 2")
|
|
177
|
+
"""
|
|
178
|
+
|
|
179
|
+
# Rebuild queries for each specified nested/base layer
|
|
180
|
+
exprs_to_use = self._split_query(expr)
|
|
181
|
+
|
|
182
|
+
# For now (simplicity), limit query to only operating on one layer
|
|
183
|
+
if len(exprs_to_use.keys()) != 1:
|
|
184
|
+
raise ValueError("Queries cannot target multiple structs/layers, write a separate query for each")
|
|
185
|
+
|
|
186
|
+
# Send queries to layers
|
|
187
|
+
# We'll only execute 1 per the Error above, but the loop will be useful
|
|
188
|
+
# for when/if we allow multi-layer queries
|
|
189
|
+
result = self.copy()
|
|
190
|
+
for expr in exprs_to_use:
|
|
191
|
+
if expr == "base":
|
|
192
|
+
result = super().query(exprs_to_use["base"], inplace=False)
|
|
193
|
+
else:
|
|
194
|
+
# TODO: does not work with queries that empty the dataframe
|
|
195
|
+
result[expr] = result[expr].nest.query_flat(exprs_to_use[expr])
|
|
196
|
+
return result
|
|
197
|
+
|
|
198
|
+
def _resolve_dropna_target(self, on_nested, subset):
|
|
199
|
+
"""resolves the target layer for a given set of dropna kwargs"""
|
|
200
|
+
|
|
201
|
+
nested_cols = self.nested_columns
|
|
202
|
+
columns = self.columns
|
|
203
|
+
|
|
204
|
+
# first check the subset kwarg input
|
|
205
|
+
subset_target = []
|
|
206
|
+
if subset:
|
|
207
|
+
if isinstance(subset, str):
|
|
208
|
+
subset = [subset]
|
|
209
|
+
|
|
210
|
+
for col in subset:
|
|
211
|
+
col = col.split(".")[0]
|
|
212
|
+
if col in nested_cols:
|
|
213
|
+
subset_target.append(col)
|
|
214
|
+
elif col in columns:
|
|
215
|
+
subset_target.append("base")
|
|
216
|
+
else:
|
|
217
|
+
raise ValueError(f"Column name {col} not found in any base or nested columns")
|
|
218
|
+
|
|
219
|
+
# Check for 1 target
|
|
220
|
+
subset_target = np.unique(subset_target)
|
|
221
|
+
if len(subset_target) > 1: # prohibit multi-target operations
|
|
222
|
+
raise ValueError(
|
|
223
|
+
f"Targeted multiple nested structures ({subset_target}), write one command per target dataframe" # noqa
|
|
224
|
+
)
|
|
225
|
+
subset_target = str(subset_target[0])
|
|
226
|
+
|
|
227
|
+
# Next check the on_nested kwarg input
|
|
228
|
+
if on_nested and on_nested not in nested_cols:
|
|
229
|
+
raise ValueError("Provided nested layer not found in nested dataframes")
|
|
230
|
+
|
|
231
|
+
# Resolve target layer
|
|
232
|
+
target = "base"
|
|
233
|
+
if on_nested and subset_target:
|
|
234
|
+
if on_nested != subset_target:
|
|
235
|
+
raise ValueError(
|
|
236
|
+
f"Provided on_nested={on_nested}, but subset columns are from {subset_target}. Make sure these are aligned or just use subset." # noqa
|
|
237
|
+
)
|
|
238
|
+
else:
|
|
239
|
+
target = subset_target
|
|
240
|
+
elif on_nested:
|
|
241
|
+
target = str(on_nested)
|
|
242
|
+
elif subset_target:
|
|
243
|
+
target = str(subset_target)
|
|
244
|
+
return target, subset
|
|
245
|
+
|
|
246
|
+
def dropna(
|
|
247
|
+
self,
|
|
248
|
+
*,
|
|
249
|
+
axis: Axis = 0,
|
|
250
|
+
how: AnyAll | lib.NoDefault = no_default,
|
|
251
|
+
thresh: int | lib.NoDefault = no_default,
|
|
252
|
+
on_nested: bool = False,
|
|
253
|
+
subset: IndexLabel | None = None,
|
|
254
|
+
inplace: bool = False,
|
|
255
|
+
ignore_index: bool = False,
|
|
256
|
+
) -> NestedFrame | None:
|
|
257
|
+
"""
|
|
258
|
+
Remove missing values for one layer of the NestedFrame.
|
|
259
|
+
|
|
260
|
+
Parameters
|
|
261
|
+
----------
|
|
262
|
+
axis : {0 or 'index', 1 or 'columns'}, default 0
|
|
263
|
+
Determine if rows or columns which contain missing values are
|
|
264
|
+
removed.
|
|
265
|
+
|
|
266
|
+
* 0, or 'index' : Drop rows which contain missing values.
|
|
267
|
+
* 1, or 'columns' : Drop columns which contain missing value.
|
|
268
|
+
|
|
269
|
+
Only a single axis is allowed.
|
|
270
|
+
|
|
271
|
+
how : {'any', 'all'}, default 'any'
|
|
272
|
+
Determine if row or column is removed from DataFrame, when we have
|
|
273
|
+
at least one NA or all NA.
|
|
274
|
+
|
|
275
|
+
* 'any' : If any NA values are present, drop that row or column.
|
|
276
|
+
* 'all' : If all values are NA, drop that row or column.
|
|
277
|
+
thresh : int, optional
|
|
278
|
+
Require that many non-NA values. Cannot be combined with how.
|
|
279
|
+
on_nested : str or bool, optional
|
|
280
|
+
If not False, applies the call to the nested dataframe in the
|
|
281
|
+
column with label equal to the provided string. If specified,
|
|
282
|
+
the nested dataframe should align with any columns given in
|
|
283
|
+
`subset`.
|
|
284
|
+
subset : column label or sequence of labels, optional
|
|
285
|
+
Labels along other axis to consider, e.g. if you are dropping rows
|
|
286
|
+
these would be a list of columns to include.
|
|
287
|
+
|
|
288
|
+
Access nested columns using `nested_df.nested_col` (where
|
|
289
|
+
`nested_df` refers to a particular nested dataframe and
|
|
290
|
+
`nested_col` is a column of that nested dataframe).
|
|
291
|
+
inplace : bool, default False
|
|
292
|
+
Whether to modify the DataFrame rather than creating a new one.
|
|
293
|
+
ignore_index : bool, default ``False``
|
|
294
|
+
If ``True``, the resulting axis will be labeled 0, 1, …, n - 1.
|
|
295
|
+
|
|
296
|
+
.. versionadded:: 2.0.0
|
|
297
|
+
|
|
298
|
+
Returns
|
|
299
|
+
-------
|
|
300
|
+
DataFrame or None
|
|
301
|
+
DataFrame with NA entries dropped from it or None if ``inplace=True``.
|
|
302
|
+
|
|
303
|
+
Notes
|
|
304
|
+
-----
|
|
305
|
+
Operations that target a particular nested structure return a dataframe
|
|
306
|
+
with rows of that particular nested structure affected.
|
|
307
|
+
|
|
308
|
+
Values for `on_nested` and `subset` should be consistent in pointing
|
|
309
|
+
to a single layer, multi-layer operations are not supported at this
|
|
310
|
+
time.
|
|
311
|
+
"""
|
|
312
|
+
|
|
313
|
+
# determine target dataframe
|
|
314
|
+
target, subset = self._resolve_dropna_target(on_nested, subset)
|
|
315
|
+
|
|
316
|
+
if target == "base":
|
|
317
|
+
return super().dropna(
|
|
318
|
+
axis=axis, how=how, thresh=thresh, subset=subset, inplace=inplace, ignore_index=ignore_index
|
|
319
|
+
)
|
|
320
|
+
if subset is not None:
|
|
321
|
+
subset = [col.split(".")[-1] for col in subset]
|
|
322
|
+
if inplace:
|
|
323
|
+
target_flat = self[target].nest.to_flat()
|
|
324
|
+
target_flat.dropna(
|
|
325
|
+
axis=axis,
|
|
326
|
+
how=how,
|
|
327
|
+
thresh=thresh,
|
|
328
|
+
subset=subset,
|
|
329
|
+
inplace=inplace,
|
|
330
|
+
ignore_index=ignore_index,
|
|
331
|
+
)
|
|
332
|
+
self[target] = packer.pack_flat(target_flat)
|
|
333
|
+
return self
|
|
334
|
+
# Or if not inplace
|
|
335
|
+
new_df = self.copy()
|
|
336
|
+
new_df[target] = packer.pack_flat(
|
|
337
|
+
new_df[target]
|
|
338
|
+
.nest.to_flat()
|
|
339
|
+
.dropna(
|
|
340
|
+
axis=axis,
|
|
341
|
+
how=how,
|
|
342
|
+
thresh=thresh,
|
|
343
|
+
subset=subset,
|
|
344
|
+
inplace=inplace,
|
|
345
|
+
ignore_index=ignore_index,
|
|
346
|
+
)
|
|
347
|
+
)
|
|
348
|
+
return new_df
|
|
349
|
+
|
|
350
|
+
def reduce(self, func, *args, **kwargs) -> NestedFrame:
|
|
351
|
+
"""
|
|
352
|
+
Takes a function and applies it to each top-level row of the NestedFrame.
|
|
353
|
+
|
|
354
|
+
The user may specify which columns the function is applied to, with
|
|
355
|
+
columns from the 'base' layer being passsed to the function as
|
|
356
|
+
scalars and columns from the nested layers being passed as numpy arrays.
|
|
357
|
+
|
|
358
|
+
Parameters
|
|
359
|
+
----------
|
|
360
|
+
func : callable
|
|
361
|
+
Function to apply to each nested dataframe. The first arguments to `func` should be which
|
|
362
|
+
columns to apply the function to.
|
|
363
|
+
args : positional arguments
|
|
364
|
+
Positional arguments to pass to the function, the first *args should be the names of the
|
|
365
|
+
columns to apply the function to.
|
|
366
|
+
kwargs : keyword arguments, optional
|
|
367
|
+
Keyword arguments to pass to the function.
|
|
368
|
+
|
|
369
|
+
Returns
|
|
370
|
+
-------
|
|
371
|
+
`NestedFrame`
|
|
372
|
+
`NestedFrame` with the results of the function applied to the columns of the frame.
|
|
373
|
+
|
|
374
|
+
Notes
|
|
375
|
+
-----
|
|
376
|
+
The recommend return value of func should be a `pd.Series` where the indices are the names of the
|
|
377
|
+
output columns in the dataframe returned by `reduce`. Note however that in cases where func
|
|
378
|
+
returns a single value there may be a performance benefit to returning the scalar value
|
|
379
|
+
rather than a `pd.Series`.
|
|
380
|
+
|
|
381
|
+
Example User Function:
|
|
382
|
+
```
|
|
383
|
+
import pandas as pd
|
|
384
|
+
|
|
385
|
+
def my_sum(col1, col2):
|
|
386
|
+
return pd.Series(
|
|
387
|
+
[sum(col1), sum(col2)],
|
|
388
|
+
index=["sum_col1", "sum_col2"],
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
"""
|
|
394
|
+
# Parse through the initial args to determine the columns to apply the function to
|
|
395
|
+
requested_columns = []
|
|
396
|
+
for arg in args:
|
|
397
|
+
if not isinstance(arg, str) or not self._is_known_column(arg):
|
|
398
|
+
# We've reached an argument that is not a valid column, so we assume
|
|
399
|
+
# the remaining args are extra arguments to the function
|
|
400
|
+
break
|
|
401
|
+
layer = "base" if "." not in arg else arg.split(".")[0]
|
|
402
|
+
col = arg.split(".")[-1]
|
|
403
|
+
requested_columns.append((layer, col))
|
|
404
|
+
|
|
405
|
+
# We require the first *args to be the columns to apply the function to
|
|
406
|
+
if not requested_columns:
|
|
407
|
+
raise ValueError("No columns in `*args` specified to apply function to")
|
|
408
|
+
|
|
409
|
+
# The remaining args are the extra arguments to the function other than columns
|
|
410
|
+
extra_args: tuple[Any, ...] = () # empty tuple to make mypy happy
|
|
411
|
+
if len(requested_columns) < len(args):
|
|
412
|
+
extra_args = args[len(requested_columns) :]
|
|
413
|
+
|
|
414
|
+
# find targeted layers
|
|
415
|
+
layers = np.unique([col[0] for col in requested_columns])
|
|
416
|
+
|
|
417
|
+
# build a flat dataframe with array columns to apply to the function
|
|
418
|
+
apply_df = NestedFrame()
|
|
419
|
+
for layer in layers:
|
|
420
|
+
if layer == "base":
|
|
421
|
+
columns = [col[1] for col in requested_columns if col[0] == layer]
|
|
422
|
+
apply_df = apply_df.join(self[columns], how="outer")
|
|
423
|
+
else:
|
|
424
|
+
# TODO: It should be faster to pass these columns to to_lists, but its 20x slower
|
|
425
|
+
# columns = [col[1] for col in requested_columns if col[0] == layer]
|
|
426
|
+
apply_df = apply_df.join(self[layer].nest.to_lists(), how="outer")
|
|
427
|
+
|
|
428
|
+
# Translates the requested columns into the scalars or arrays we pass to func.
|
|
429
|
+
def translate_cols(frame, layer, col):
|
|
430
|
+
if layer == "base":
|
|
431
|
+
# We pass the "base" column as a scalar
|
|
432
|
+
return frame[col]
|
|
433
|
+
return np.asarray(frame[col])
|
|
434
|
+
|
|
435
|
+
# send arrays along to the apply call
|
|
436
|
+
result = apply_df.apply(
|
|
437
|
+
lambda x: func(
|
|
438
|
+
*[translate_cols(x, layer, col) for layer, col in requested_columns], *extra_args, **kwargs
|
|
439
|
+
),
|
|
440
|
+
axis=1, # to apply func on each row of our nested frame)
|
|
441
|
+
)
|
|
442
|
+
return result
|
|
443
|
+
|
|
444
|
+
def to_parquet(self, path, by_layer=False, **kwargs) -> None:
|
|
445
|
+
"""Creates parquet file(s) with the data of a NestedFrame, either
|
|
446
|
+
as a single parquet file where each nested dataset is packed into its
|
|
447
|
+
own column or as an individual parquet file for each layer.
|
|
448
|
+
|
|
449
|
+
Note that here we always opt to use the pyarrow engine for writing
|
|
450
|
+
parquet files.
|
|
451
|
+
|
|
452
|
+
Parameters
|
|
453
|
+
----------
|
|
454
|
+
path : str
|
|
455
|
+
The path to the parquet file to be written if 'by_layer' is False.
|
|
456
|
+
If 'by_layer' is True, this should be the path to an existing.
|
|
457
|
+
by_layer : bool, default False
|
|
458
|
+
If False, writes the entire NestedFrame to a single parquet file.
|
|
459
|
+
|
|
460
|
+
If True, writes each layer to a separate parquet file within the
|
|
461
|
+
directory specified by path. The filename for each outputted file will
|
|
462
|
+
be named after its layer and then the ".parquet" extension.
|
|
463
|
+
For example for the base layer this is always "base.parquet".
|
|
464
|
+
kwargs : keyword arguments, optional
|
|
465
|
+
Keyword arguments to pass to the function.
|
|
466
|
+
|
|
467
|
+
Returns
|
|
468
|
+
-------
|
|
469
|
+
None
|
|
470
|
+
"""
|
|
471
|
+
if not by_layer:
|
|
472
|
+
# We just defer to the pandas to_parquet method if we're not writing by layer
|
|
473
|
+
# or there is only one layer in the NestedFrame.
|
|
474
|
+
super().to_parquet(path, engine="pyarrow", **kwargs)
|
|
475
|
+
else:
|
|
476
|
+
# If we're writing by layer, path must be an existing directory
|
|
477
|
+
if not os.path.isdir(path):
|
|
478
|
+
raise ValueError("The provided path must be an existing directory if by_layer=True")
|
|
479
|
+
|
|
480
|
+
# Write the base layer to a parquet file
|
|
481
|
+
base_frame = self.drop(columns=self.nested_columns, inplace=False)
|
|
482
|
+
base_frame.to_parquet(os.path.join(path, "base.parquet"), by_layer=False, **kwargs)
|
|
483
|
+
|
|
484
|
+
# Write each nested layer to a parquet file
|
|
485
|
+
for layer in self.all_columns:
|
|
486
|
+
if layer != "base":
|
|
487
|
+
path_layer = os.path.join(path, f"{layer}.parquet")
|
|
488
|
+
self[layer].nest.to_flat().to_parquet(path_layer, engine="pyarrow", **kwargs)
|
|
489
|
+
return None
|