edaprep 0.1.0__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.
- edaprep/__init__.py +188 -0
- edaprep/_version.py +1 -0
- edaprep/backends/__init__.py +11 -0
- edaprep/backends/base.py +167 -0
- edaprep/backends/pandas_backend.py +128 -0
- edaprep/config.py +504 -0
- edaprep/core/__init__.py +7 -0
- edaprep/core/base.py +314 -0
- edaprep/core/context.py +81 -0
- edaprep/core/journal.py +244 -0
- edaprep/core/pipeline.py +587 -0
- edaprep/eda/__init__.py +22 -0
- edaprep/eda/analyzer.py +486 -0
- edaprep/eda/categorical.py +100 -0
- edaprep/eda/correlation.py +183 -0
- edaprep/eda/numerical.py +86 -0
- edaprep/eda/outliers.py +99 -0
- edaprep/eda/target.py +214 -0
- edaprep/exceptions.py +161 -0
- edaprep/planning/__init__.py +16 -0
- edaprep/planning/decisions.py +358 -0
- edaprep/planning/planner.py +364 -0
- edaprep/planning/rules.py +819 -0
- edaprep/preprocessing/__init__.py +67 -0
- edaprep/preprocessing/casting.py +260 -0
- edaprep/preprocessing/datetime_features.py +208 -0
- edaprep/preprocessing/duplicates.py +170 -0
- edaprep/preprocessing/encoding.py +804 -0
- edaprep/preprocessing/missing.py +363 -0
- edaprep/preprocessing/outliers.py +569 -0
- edaprep/preprocessing/scaling.py +228 -0
- edaprep/preprocessing/selection.py +468 -0
- edaprep/preprocessing/text.py +124 -0
- edaprep/preprocessing/transformations.py +379 -0
- edaprep/profiling/__init__.py +19 -0
- edaprep/profiling/column_types.py +498 -0
- edaprep/profiling/profiler.py +1095 -0
- edaprep/profiling/quality.py +357 -0
- edaprep/profiling/statistics.py +516 -0
- edaprep/py.typed +0 -0
- edaprep/reporting/__init__.py +5 -0
- edaprep/reporting/html.py +173 -0
- edaprep/reporting/report.py +317 -0
- edaprep/types.py +189 -0
- edaprep/visualization/__init__.py +33 -0
- edaprep/visualization/plots.py +330 -0
- edaprep-0.1.0.dist-info/METADATA +306 -0
- edaprep-0.1.0.dist-info/RECORD +51 -0
- edaprep-0.1.0.dist-info/WHEEL +5 -0
- edaprep-0.1.0.dist-info/licenses/LICENSE +21 -0
- edaprep-0.1.0.dist-info/top_level.txt +1 -0
edaprep/__init__.py
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"""edaprep: transparent, leakage-safe EDA and ML preprocessing.
|
|
2
|
+
|
|
3
|
+
Quick start
|
|
4
|
+
-----------
|
|
5
|
+
|
|
6
|
+
Understand a dataset::
|
|
7
|
+
|
|
8
|
+
import edaprep
|
|
9
|
+
|
|
10
|
+
profile = edaprep.profile(df, target="churn")
|
|
11
|
+
print(profile.summary())
|
|
12
|
+
|
|
13
|
+
report = edaprep.EDA(df, target="churn").analyze()
|
|
14
|
+
print(report.summary())
|
|
15
|
+
|
|
16
|
+
Prepare it, automatically but not opaquely::
|
|
17
|
+
|
|
18
|
+
pipe = edaprep.AutoPipeline(target="churn", model_family="tree", random_state=42)
|
|
19
|
+
pipe.fit(train_df)
|
|
20
|
+
pipe.explain() # why every column was treated as it was
|
|
21
|
+
|
|
22
|
+
X_train = pipe.transform(train_df)
|
|
23
|
+
X_test = pipe.transform(test_df) # same fitted statistics, no leakage
|
|
24
|
+
|
|
25
|
+
print(pipe.report_.summary())
|
|
26
|
+
|
|
27
|
+
Or say exactly what should happen::
|
|
28
|
+
|
|
29
|
+
pipe = (
|
|
30
|
+
edaprep.Pipeline(target="churn")
|
|
31
|
+
.handle_missing()
|
|
32
|
+
.handle_outliers(strategy="clip")
|
|
33
|
+
.encode_categorical()
|
|
34
|
+
.scale_numeric()
|
|
35
|
+
)
|
|
36
|
+
X = pipe.fit_transform(train_df)
|
|
37
|
+
|
|
38
|
+
Design guarantees
|
|
39
|
+
-----------------
|
|
40
|
+
* Every learned statistic is fitted on the training frame only; ``transform`` is a
|
|
41
|
+
pure function of that fitted state.
|
|
42
|
+
* Every automatic decision is inspectable (``pipe.plan_``), explainable
|
|
43
|
+
(``pipe.explain()``), overridable (``config.column("age").imputation = "mean"``)
|
|
44
|
+
and reproducible (``random_state``, serialisable plan and report).
|
|
45
|
+
* Nothing is silently discarded. Dropped columns, imputed values, grouped
|
|
46
|
+
categories and clipped rows are all counted and reported.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
from ._version import __version__
|
|
50
|
+
from .config import AUTO, ColumnConfig, Config, Thresholds
|
|
51
|
+
from .core.base import Transformer
|
|
52
|
+
from .core.context import FitContext
|
|
53
|
+
from .core.pipeline import AutoPipeline, Pipeline
|
|
54
|
+
from .eda.analyzer import EDA, EDAReport
|
|
55
|
+
from .exceptions import (
|
|
56
|
+
ConfigurationError,
|
|
57
|
+
DataError,
|
|
58
|
+
EdaPrepError,
|
|
59
|
+
EmptyDataError,
|
|
60
|
+
LeakageError,
|
|
61
|
+
NotFittedError,
|
|
62
|
+
SchemaError,
|
|
63
|
+
TransformationError,
|
|
64
|
+
)
|
|
65
|
+
from .planning.decisions import Decision, Plan, PlannedStep
|
|
66
|
+
from .planning.planner import Planner
|
|
67
|
+
from .planning.rules import Rule, RuleSet, default_rules
|
|
68
|
+
from .preprocessing import (
|
|
69
|
+
CategoricalEncoder,
|
|
70
|
+
ColumnDropper,
|
|
71
|
+
ConstantFilter,
|
|
72
|
+
CorrelationFilter,
|
|
73
|
+
DataTypeInference,
|
|
74
|
+
DateTimeExpander,
|
|
75
|
+
DistributionTransformer,
|
|
76
|
+
DuplicateColumnFilter,
|
|
77
|
+
DuplicateRowHandler,
|
|
78
|
+
FrequencyEncoder,
|
|
79
|
+
MissingIndicator,
|
|
80
|
+
MissingnessFilter,
|
|
81
|
+
MissingValueHandler,
|
|
82
|
+
OneHotEncoder,
|
|
83
|
+
OrdinalEncoder,
|
|
84
|
+
OutlierHandler,
|
|
85
|
+
RareCategoryGrouper,
|
|
86
|
+
Scaler,
|
|
87
|
+
TargetEncoder,
|
|
88
|
+
TextColumnHandler,
|
|
89
|
+
VarianceFilter,
|
|
90
|
+
detect_outliers,
|
|
91
|
+
)
|
|
92
|
+
from .profiling.profiler import ColumnProfile, DatasetProfile, profile
|
|
93
|
+
from .reporting.report import Report
|
|
94
|
+
from .types import AnalysisLevel, ModelFamily, SemanticType, Severity, Stage
|
|
95
|
+
|
|
96
|
+
__all__ = [
|
|
97
|
+
"__version__",
|
|
98
|
+
# entry points
|
|
99
|
+
"profile",
|
|
100
|
+
"EDA",
|
|
101
|
+
"AutoPipeline",
|
|
102
|
+
"Pipeline",
|
|
103
|
+
# configuration
|
|
104
|
+
"Config",
|
|
105
|
+
"ColumnConfig",
|
|
106
|
+
"Thresholds",
|
|
107
|
+
"AUTO",
|
|
108
|
+
# planning
|
|
109
|
+
"Planner",
|
|
110
|
+
"Plan",
|
|
111
|
+
"PlannedStep",
|
|
112
|
+
"Decision",
|
|
113
|
+
"Rule",
|
|
114
|
+
"RuleSet",
|
|
115
|
+
"default_rules",
|
|
116
|
+
# results
|
|
117
|
+
"DatasetProfile",
|
|
118
|
+
"ColumnProfile",
|
|
119
|
+
"Report",
|
|
120
|
+
"EDAReport",
|
|
121
|
+
# types
|
|
122
|
+
"SemanticType",
|
|
123
|
+
"ModelFamily",
|
|
124
|
+
"Stage",
|
|
125
|
+
"Severity",
|
|
126
|
+
"AnalysisLevel",
|
|
127
|
+
# extension
|
|
128
|
+
"Transformer",
|
|
129
|
+
"FitContext",
|
|
130
|
+
# transformers
|
|
131
|
+
"DataTypeInference",
|
|
132
|
+
"DateTimeExpander",
|
|
133
|
+
"DuplicateRowHandler",
|
|
134
|
+
"MissingValueHandler",
|
|
135
|
+
"MissingIndicator",
|
|
136
|
+
"OutlierHandler",
|
|
137
|
+
"detect_outliers",
|
|
138
|
+
"CategoricalEncoder",
|
|
139
|
+
"OneHotEncoder",
|
|
140
|
+
"OrdinalEncoder",
|
|
141
|
+
"FrequencyEncoder",
|
|
142
|
+
"TargetEncoder",
|
|
143
|
+
"RareCategoryGrouper",
|
|
144
|
+
"Scaler",
|
|
145
|
+
"DistributionTransformer",
|
|
146
|
+
"TextColumnHandler",
|
|
147
|
+
"ColumnDropper",
|
|
148
|
+
"ConstantFilter",
|
|
149
|
+
"MissingnessFilter",
|
|
150
|
+
"DuplicateColumnFilter",
|
|
151
|
+
"CorrelationFilter",
|
|
152
|
+
"VarianceFilter",
|
|
153
|
+
# exceptions
|
|
154
|
+
"EdaPrepError",
|
|
155
|
+
"ConfigurationError",
|
|
156
|
+
"NotFittedError",
|
|
157
|
+
"SchemaError",
|
|
158
|
+
"DataError",
|
|
159
|
+
"EmptyDataError",
|
|
160
|
+
"TransformationError",
|
|
161
|
+
"LeakageError",
|
|
162
|
+
]
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def __getattr__(name: str):
|
|
166
|
+
"""Lazily expose optional subpackages.
|
|
167
|
+
|
|
168
|
+
``edaprep.visualization`` needs matplotlib, which is an optional dependency.
|
|
169
|
+
Importing it eagerly would make ``import edaprep`` fail for users who installed
|
|
170
|
+
only the core, so it is resolved on first access with a message that says what to
|
|
171
|
+
install.
|
|
172
|
+
"""
|
|
173
|
+
if name == "visualization":
|
|
174
|
+
try:
|
|
175
|
+
# importlib rather than `from . import visualization`: the latter goes
|
|
176
|
+
# through _handle_fromlist, which calls getattr on this module again and
|
|
177
|
+
# re-enters __getattr__, recursing until the stack blows instead of
|
|
178
|
+
# surfacing the ImportError.
|
|
179
|
+
import importlib
|
|
180
|
+
|
|
181
|
+
module = importlib.import_module("edaprep.visualization")
|
|
182
|
+
except ImportError as exc:
|
|
183
|
+
raise ImportError(
|
|
184
|
+
"edaprep.visualization requires matplotlib, which is an optional "
|
|
185
|
+
"dependency. Install it with: pip install 'edaprep[visualization]'"
|
|
186
|
+
) from exc
|
|
187
|
+
return module
|
|
188
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
edaprep/_version.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Execution backends.
|
|
2
|
+
|
|
3
|
+
A narrow protocol between the library and a dataframe implementation. See
|
|
4
|
+
:mod:`edaprep.backends.base` for what it covers and, more importantly, what it
|
|
5
|
+
deliberately does not.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .base import Backend, get_backend, register_backend
|
|
9
|
+
from .pandas_backend import PandasBackend
|
|
10
|
+
|
|
11
|
+
__all__ = ["Backend", "PandasBackend", "get_backend", "register_backend"]
|
edaprep/backends/base.py
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""The backend protocol.
|
|
2
|
+
|
|
3
|
+
A narrow seam between "what the library wants done to a frame" and "how a particular
|
|
4
|
+
dataframe implementation does it". ``pandas_backend.PandasBackend`` is the only
|
|
5
|
+
implementation today.
|
|
6
|
+
|
|
7
|
+
Why it is this small
|
|
8
|
+
--------------------
|
|
9
|
+
This is not a general dataframe abstraction, and it is deliberately not trying to be.
|
|
10
|
+
Wrapping every pandas call would be abstraction for its own sake: it would slow the
|
|
11
|
+
common path, obscure the code, and buy nothing until a second backend exists.
|
|
12
|
+
|
|
13
|
+
What it does cover is the handful of operations that are (a) hot enough to be worth
|
|
14
|
+
routing through a seam and (b) the ones an Arrow or Polars implementation would
|
|
15
|
+
genuinely do differently. Everything else in the library calls pandas directly.
|
|
16
|
+
|
|
17
|
+
The motivation is concrete rather than speculative: tabular ML frames routinely reach
|
|
18
|
+
500,000 x 400, and an Arrow-backed implementation is a foreseeable need for frames of
|
|
19
|
+
that shape and larger. Nothing here is used to support a second backend yet.
|
|
20
|
+
|
|
21
|
+
Before writing one
|
|
22
|
+
------------------
|
|
23
|
+
Read ``docs/performance.md`` first. The one place in this library where a hand-written
|
|
24
|
+
replacement for pandas looked obviously worthwhile turned out to be 2.1x *slower* than
|
|
25
|
+
the pandas code it replaced, and was deleted. Measure before committing to an
|
|
26
|
+
implementation.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
from abc import ABC, abstractmethod
|
|
32
|
+
from typing import Any, Dict, Iterable, List, Mapping, Optional, Sequence, Tuple
|
|
33
|
+
|
|
34
|
+
import numpy as np
|
|
35
|
+
|
|
36
|
+
__all__ = ["Backend", "get_backend", "register_backend"]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class Backend(ABC):
|
|
40
|
+
"""Operations the library routes through a backend.
|
|
41
|
+
|
|
42
|
+
Implementations receive and return their own native frame and column types; the
|
|
43
|
+
library treats them as opaque except through these methods.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
#: Short name, used by :func:`get_backend`.
|
|
47
|
+
name: str = "base"
|
|
48
|
+
|
|
49
|
+
# -- introspection ---------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
@abstractmethod
|
|
52
|
+
def is_frame(self, obj: Any) -> bool:
|
|
53
|
+
"""True when ``obj`` is a frame this backend understands."""
|
|
54
|
+
|
|
55
|
+
@abstractmethod
|
|
56
|
+
def shape(self, frame: Any) -> Tuple[int, int]:
|
|
57
|
+
"""``(n_rows, n_columns)``."""
|
|
58
|
+
|
|
59
|
+
@abstractmethod
|
|
60
|
+
def column_names(self, frame: Any) -> List[str]:
|
|
61
|
+
"""Column names, in order."""
|
|
62
|
+
|
|
63
|
+
@abstractmethod
|
|
64
|
+
def dtype_of(self, frame: Any, column: str) -> str:
|
|
65
|
+
"""A string naming the column's storage type."""
|
|
66
|
+
|
|
67
|
+
@abstractmethod
|
|
68
|
+
def memory_usage(self, frame: Any, deep: bool = True) -> Dict[str, int]:
|
|
69
|
+
"""Per-column bytes, plus a ``"total"`` key."""
|
|
70
|
+
|
|
71
|
+
# -- column access ----------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
@abstractmethod
|
|
74
|
+
def get_column(self, frame: Any, column: str) -> Any:
|
|
75
|
+
"""One column, in the backend's native representation."""
|
|
76
|
+
|
|
77
|
+
@abstractmethod
|
|
78
|
+
def to_float_array(self, frame: Any, column: str) -> np.ndarray:
|
|
79
|
+
"""One column as a float64 NumPy array, with missing values as NaN.
|
|
80
|
+
|
|
81
|
+
Every numeric kernel in the library ultimately works on NumPy, so this is the
|
|
82
|
+
single conversion point a backend must provide.
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
@abstractmethod
|
|
86
|
+
def select(self, frame: Any, columns: Sequence[str]) -> Any:
|
|
87
|
+
"""A frame restricted to ``columns``, without copying where possible."""
|
|
88
|
+
|
|
89
|
+
@abstractmethod
|
|
90
|
+
def assign(self, frame: Any, columns: Mapping[str, Any]) -> Any:
|
|
91
|
+
"""A new frame with ``columns`` replaced or appended.
|
|
92
|
+
|
|
93
|
+
Must not mutate ``frame``. Implementations should pass untouched columns
|
|
94
|
+
through by reference rather than copying the whole frame -- the copy discipline
|
|
95
|
+
described in ``docs/architecture.md`` section 6.
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
# -- aggregation -------------------------------------------------------------------
|
|
99
|
+
|
|
100
|
+
@abstractmethod
|
|
101
|
+
def null_mask(self, frame: Any, column: str) -> np.ndarray:
|
|
102
|
+
"""Boolean array: True where the value is missing."""
|
|
103
|
+
|
|
104
|
+
@abstractmethod
|
|
105
|
+
def n_unique(self, frame: Any, column: str, dropna: bool = True) -> int:
|
|
106
|
+
"""Distinct value count."""
|
|
107
|
+
|
|
108
|
+
@abstractmethod
|
|
109
|
+
def value_counts(
|
|
110
|
+
self, frame: Any, column: str, dropna: bool = True
|
|
111
|
+
) -> List[Tuple[Any, int]]:
|
|
112
|
+
"""``(value, count)`` pairs, most frequent first."""
|
|
113
|
+
|
|
114
|
+
@abstractmethod
|
|
115
|
+
def quantiles(
|
|
116
|
+
self, frame: Any, columns: Sequence[str], levels: Sequence[float]
|
|
117
|
+
) -> np.ndarray:
|
|
118
|
+
"""Quantile matrix of shape ``(len(levels), len(columns))``."""
|
|
119
|
+
|
|
120
|
+
@abstractmethod
|
|
121
|
+
def group_mean(
|
|
122
|
+
self, frame: Any, value_column: str, group_column: str
|
|
123
|
+
) -> Dict[Any, float]:
|
|
124
|
+
"""Mean of ``value_column`` per level of ``group_column``."""
|
|
125
|
+
|
|
126
|
+
@abstractmethod
|
|
127
|
+
def duplicated_rows(
|
|
128
|
+
self, frame: Any, subset: Optional[Sequence[str]] = None
|
|
129
|
+
) -> np.ndarray:
|
|
130
|
+
"""Boolean array: True where the row repeats an earlier one."""
|
|
131
|
+
|
|
132
|
+
# -- construction --------------------------------------------------------------------
|
|
133
|
+
|
|
134
|
+
@abstractmethod
|
|
135
|
+
def concat_columns(self, frames: Iterable[Any]) -> Any:
|
|
136
|
+
"""Join frames side by side. Indexes are assumed aligned."""
|
|
137
|
+
|
|
138
|
+
@abstractmethod
|
|
139
|
+
def take_rows(self, frame: Any, mask: np.ndarray) -> Any:
|
|
140
|
+
"""The rows where ``mask`` is True."""
|
|
141
|
+
|
|
142
|
+
def __repr__(self) -> str: # pragma: no cover - trivial
|
|
143
|
+
return f"{type(self).__name__}(name={self.name!r})"
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
_REGISTRY: Dict[str, Backend] = {}
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def register_backend(backend: Backend) -> Backend:
|
|
150
|
+
"""Make a backend available to :func:`get_backend`."""
|
|
151
|
+
_REGISTRY[backend.name] = backend
|
|
152
|
+
return backend
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def get_backend(name: str = "pandas") -> Backend:
|
|
156
|
+
"""Look up a registered backend by name."""
|
|
157
|
+
if name not in _REGISTRY:
|
|
158
|
+
if name == "pandas":
|
|
159
|
+
from .pandas_backend import PandasBackend
|
|
160
|
+
|
|
161
|
+
return register_backend(PandasBackend())
|
|
162
|
+
available = ", ".join(sorted(_REGISTRY)) or "pandas"
|
|
163
|
+
raise ValueError(
|
|
164
|
+
f"No backend named {name!r} is registered. Available: {available}. "
|
|
165
|
+
f"Register one with edaprep.backends.register_backend(MyBackend())."
|
|
166
|
+
)
|
|
167
|
+
return _REGISTRY[name]
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""The pandas backend: the only implementation, and the reference one.
|
|
2
|
+
|
|
3
|
+
Every method here is a thin adapter over a pandas call. That is the point: the protocol
|
|
4
|
+
exists so a *different* implementation can be written, not to add a layer over the one
|
|
5
|
+
that already works.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any, Dict, Iterable, List, Mapping, Optional, Sequence, Tuple
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
import pandas as pd
|
|
14
|
+
|
|
15
|
+
from .base import Backend
|
|
16
|
+
|
|
17
|
+
__all__ = ["PandasBackend"]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class PandasBackend(Backend):
|
|
21
|
+
"""Backend over ``pandas.DataFrame``."""
|
|
22
|
+
|
|
23
|
+
name = "pandas"
|
|
24
|
+
|
|
25
|
+
# -- introspection ---------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
def is_frame(self, obj: Any) -> bool:
|
|
28
|
+
return isinstance(obj, pd.DataFrame)
|
|
29
|
+
|
|
30
|
+
def shape(self, frame: pd.DataFrame) -> Tuple[int, int]:
|
|
31
|
+
return frame.shape
|
|
32
|
+
|
|
33
|
+
def column_names(self, frame: pd.DataFrame) -> List[str]:
|
|
34
|
+
return [str(c) for c in frame.columns]
|
|
35
|
+
|
|
36
|
+
def dtype_of(self, frame: pd.DataFrame, column: str) -> str:
|
|
37
|
+
return str(frame[column].dtype)
|
|
38
|
+
|
|
39
|
+
def memory_usage(self, frame: pd.DataFrame, deep: bool = True) -> Dict[str, int]:
|
|
40
|
+
usage = frame.memory_usage(index=True, deep=deep)
|
|
41
|
+
return {"total": int(usage.sum()), **{str(k): int(v) for k, v in usage.items()}}
|
|
42
|
+
|
|
43
|
+
# -- column access ----------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
def get_column(self, frame: pd.DataFrame, column: str) -> pd.Series:
|
|
46
|
+
return frame[column]
|
|
47
|
+
|
|
48
|
+
def to_float_array(self, frame: pd.DataFrame, column: str) -> np.ndarray:
|
|
49
|
+
series = frame[column]
|
|
50
|
+
if isinstance(series.dtype, pd.CategoricalDtype):
|
|
51
|
+
series = series.astype("float64")
|
|
52
|
+
try:
|
|
53
|
+
return series.to_numpy(dtype=np.float64, na_value=np.nan, copy=False)
|
|
54
|
+
except (TypeError, ValueError):
|
|
55
|
+
return pd.to_numeric(series, errors="coerce").to_numpy(dtype=np.float64)
|
|
56
|
+
|
|
57
|
+
def select(self, frame: pd.DataFrame, columns: Sequence[str]) -> pd.DataFrame:
|
|
58
|
+
return frame[list(columns)]
|
|
59
|
+
|
|
60
|
+
def assign(self, frame: pd.DataFrame, columns: Mapping[str, Any]) -> pd.DataFrame:
|
|
61
|
+
if not columns:
|
|
62
|
+
return frame
|
|
63
|
+
# Rebuild from the existing column blocks with only the named columns replaced,
|
|
64
|
+
# rather than `frame.copy()` then assigning: a step that touches 5 of 400
|
|
65
|
+
# columns then allocates 5 columns, not 400.
|
|
66
|
+
data: Dict[str, Any] = {}
|
|
67
|
+
for name in frame.columns:
|
|
68
|
+
key = str(name)
|
|
69
|
+
data[key] = columns.get(key, frame[name])
|
|
70
|
+
for key, value in columns.items():
|
|
71
|
+
if key not in data:
|
|
72
|
+
data[key] = value
|
|
73
|
+
return pd.DataFrame(data, index=frame.index, copy=False)
|
|
74
|
+
|
|
75
|
+
# -- aggregation -------------------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
def null_mask(self, frame: pd.DataFrame, column: str) -> np.ndarray:
|
|
78
|
+
return frame[column].isna().to_numpy()
|
|
79
|
+
|
|
80
|
+
def n_unique(self, frame: pd.DataFrame, column: str, dropna: bool = True) -> int:
|
|
81
|
+
try:
|
|
82
|
+
return int(frame[column].nunique(dropna=dropna))
|
|
83
|
+
except TypeError:
|
|
84
|
+
# Unhashable cell values (lists, dicts); the string view is slower but is
|
|
85
|
+
# the only thing that can be counted at all.
|
|
86
|
+
return int(frame[column].astype(str).nunique(dropna=dropna))
|
|
87
|
+
|
|
88
|
+
def value_counts(
|
|
89
|
+
self, frame: pd.DataFrame, column: str, dropna: bool = True
|
|
90
|
+
) -> List[Tuple[Any, int]]:
|
|
91
|
+
counts = frame[column].value_counts(dropna=dropna)
|
|
92
|
+
return [(index, int(value)) for index, value in counts.items()]
|
|
93
|
+
|
|
94
|
+
def quantiles(
|
|
95
|
+
self, frame: pd.DataFrame, columns: Sequence[str], levels: Sequence[float]
|
|
96
|
+
) -> np.ndarray:
|
|
97
|
+
return frame[list(columns)].quantile(list(levels)).to_numpy()
|
|
98
|
+
|
|
99
|
+
def group_mean(
|
|
100
|
+
self, frame: pd.DataFrame, value_column: str, group_column: str
|
|
101
|
+
) -> Dict[Any, float]:
|
|
102
|
+
grouped = frame.groupby(group_column, observed=True)[value_column].mean()
|
|
103
|
+
return {index: float(value) for index, value in grouped.items()}
|
|
104
|
+
|
|
105
|
+
def duplicated_rows(
|
|
106
|
+
self, frame: pd.DataFrame, subset: Optional[Sequence[str]] = None
|
|
107
|
+
) -> np.ndarray:
|
|
108
|
+
try:
|
|
109
|
+
return frame.duplicated(subset=list(subset) if subset else None).to_numpy()
|
|
110
|
+
except TypeError:
|
|
111
|
+
return (
|
|
112
|
+
frame.astype(str)
|
|
113
|
+
.duplicated(subset=list(subset) if subset else None)
|
|
114
|
+
.to_numpy()
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# -- construction --------------------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
def concat_columns(self, frames: Iterable[pd.DataFrame]) -> pd.DataFrame:
|
|
120
|
+
parts = list(frames)
|
|
121
|
+
if not parts:
|
|
122
|
+
return pd.DataFrame()
|
|
123
|
+
if len(parts) == 1:
|
|
124
|
+
return parts[0]
|
|
125
|
+
return pd.concat(parts, axis=1, copy=False)
|
|
126
|
+
|
|
127
|
+
def take_rows(self, frame: pd.DataFrame, mask: np.ndarray) -> pd.DataFrame:
|
|
128
|
+
return frame.loc[np.asarray(mask, dtype=bool)]
|