featuresmith-core 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.
- featuresmith/__init__.py +7 -0
- featuresmith/api.py +180 -0
- featuresmith/connectors/__init__.py +18 -0
- featuresmith/connectors/_paths.py +25 -0
- featuresmith/connectors/base.py +54 -0
- featuresmith/connectors/csv_connector.py +44 -0
- featuresmith/connectors/dataframe_connector.py +40 -0
- featuresmith/connectors/excel_connector.py +45 -0
- featuresmith/connectors/parquet_connector.py +44 -0
- featuresmith/connectors/registry.py +55 -0
- featuresmith/core/__init__.py +50 -0
- featuresmith/core/dataset.py +107 -0
- featuresmith/core/exceptions.py +17 -0
- featuresmith/core/profile_result.py +412 -0
- featuresmith/core/rule_finding.py +44 -0
- featuresmith/core/rule_result.py +49 -0
- featuresmith/core/schema.py +34 -0
- featuresmith/profiling/__init__.py +5 -0
- featuresmith/profiling/categorical.py +108 -0
- featuresmith/profiling/correlation.py +67 -0
- featuresmith/profiling/datetime.py +105 -0
- featuresmith/profiling/duplicates.py +48 -0
- featuresmith/profiling/missing.py +70 -0
- featuresmith/profiling/numeric.py +248 -0
- featuresmith/profiling/profiler.py +209 -0
- featuresmith/profiling/quality.py +29 -0
- featuresmith/profiling/summary.py +117 -0
- featuresmith/profiling/text.py +109 -0
- featuresmith/py.typed +1 -0
- featuresmith/rules/__init__.py +27 -0
- featuresmith/rules/base.py +69 -0
- featuresmith/rules/cardinality.py +95 -0
- featuresmith/rules/constants.py +115 -0
- featuresmith/rules/correlation.py +77 -0
- featuresmith/rules/duplicates.py +76 -0
- featuresmith/rules/engine.py +156 -0
- featuresmith/rules/leakage.py +93 -0
- featuresmith/rules/missing.py +83 -0
- featuresmith/rules/outliers.py +113 -0
- featuresmith/rules/registry.py +81 -0
- featuresmith_core-0.1.0.dist-info/METADATA +50 -0
- featuresmith_core-0.1.0.dist-info/RECORD +45 -0
- featuresmith_core-0.1.0.dist-info/WHEEL +5 -0
- featuresmith_core-0.1.0.dist-info/licenses/LICENSE +187 -0
- featuresmith_core-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Typed exceptions raised by Featuresmith core components."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ConnectorError(Exception):
|
|
5
|
+
"""Raised when a data source cannot be validated or loaded."""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SourceNotFoundError(ConnectorError):
|
|
9
|
+
"""Raised when the target source path or file does not exist."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class UnsupportedFormatError(ConnectorError):
|
|
13
|
+
"""Raised when the data source has an unsupported format or invalid type."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SourceParseError(ConnectorError):
|
|
17
|
+
"""Raised when parsing or reading the data source fails."""
|
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
"""Canonical schema for profiling results."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping, Sequence
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True, slots=True)
|
|
11
|
+
class DatasetSummary:
|
|
12
|
+
"""High-level summary of the dataset.
|
|
13
|
+
|
|
14
|
+
Attributes:
|
|
15
|
+
row_count: Number of rows in the dataset.
|
|
16
|
+
column_count: Number of columns in the dataset.
|
|
17
|
+
size_in_bytes: Approximate size in bytes if known.
|
|
18
|
+
missing_percentage: Overall percentage of missing cells.
|
|
19
|
+
duplicate_percentage: Percentage of duplicate rows in the dataset.
|
|
20
|
+
num_numeric_columns: Number of numeric columns.
|
|
21
|
+
num_categorical_columns: Number of categorical columns.
|
|
22
|
+
num_datetime_columns: Number of datetime columns.
|
|
23
|
+
num_text_columns: Number of text columns.
|
|
24
|
+
num_constant_columns: Number of constant columns.
|
|
25
|
+
num_fully_empty_columns: Number of fully empty columns.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
row_count: int
|
|
29
|
+
column_count: int
|
|
30
|
+
size_in_bytes: int | None
|
|
31
|
+
missing_percentage: float
|
|
32
|
+
duplicate_percentage: float
|
|
33
|
+
num_numeric_columns: int
|
|
34
|
+
num_categorical_columns: int
|
|
35
|
+
num_datetime_columns: int
|
|
36
|
+
num_text_columns: int
|
|
37
|
+
num_constant_columns: int
|
|
38
|
+
num_fully_empty_columns: int
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True, slots=True)
|
|
42
|
+
class ColumnProfile:
|
|
43
|
+
"""General profile summary for a single column.
|
|
44
|
+
|
|
45
|
+
Attributes:
|
|
46
|
+
name: Column name.
|
|
47
|
+
dtype: Native datatype string.
|
|
48
|
+
logical_type: Inferred logical type ("numeric", "categorical", "datetime", "text").
|
|
49
|
+
missing_count: Count of missing values in the column.
|
|
50
|
+
missing_percentage: Percentage of missing values.
|
|
51
|
+
is_constant: Whether the column is constant (contains 1 or fewer unique values).
|
|
52
|
+
is_fully_empty: Whether the column is fully empty (all values are missing).
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
name: str
|
|
56
|
+
dtype: str
|
|
57
|
+
logical_type: str
|
|
58
|
+
missing_count: int
|
|
59
|
+
missing_percentage: float
|
|
60
|
+
is_constant: bool
|
|
61
|
+
is_fully_empty: bool
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass(frozen=True, slots=True)
|
|
65
|
+
class NumericProfile:
|
|
66
|
+
"""Detailed profiling statistics for a numeric column.
|
|
67
|
+
|
|
68
|
+
Attributes:
|
|
69
|
+
column_name: Name of the column.
|
|
70
|
+
count: Total non-missing count.
|
|
71
|
+
missing_count: Total missing count.
|
|
72
|
+
missing_percentage: Percentage of missing values.
|
|
73
|
+
unique_count: Number of unique non-missing values.
|
|
74
|
+
mean: Arithmetic mean.
|
|
75
|
+
median: 50th percentile.
|
|
76
|
+
mode: The most common value (first if multiple).
|
|
77
|
+
minimum: Minimum value.
|
|
78
|
+
maximum: Maximum value.
|
|
79
|
+
range: Maximum - minimum.
|
|
80
|
+
variance: Sample variance (ddof=1).
|
|
81
|
+
std_dev: Sample standard deviation (ddof=1).
|
|
82
|
+
q1: 25th percentile.
|
|
83
|
+
q2: 50th percentile (median).
|
|
84
|
+
q3: 75th percentile.
|
|
85
|
+
iqr: Interquartile range (q3 - q1).
|
|
86
|
+
sum: Sum of all values.
|
|
87
|
+
zero_count: Number of zeros.
|
|
88
|
+
negative_count: Number of negative values.
|
|
89
|
+
positive_count: Number of positive values.
|
|
90
|
+
skewness: Skewness of the distribution.
|
|
91
|
+
kurtosis: Kurtosis of the distribution.
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
column_name: str
|
|
95
|
+
count: int
|
|
96
|
+
missing_count: int
|
|
97
|
+
missing_percentage: float
|
|
98
|
+
unique_count: int
|
|
99
|
+
mean: float | None
|
|
100
|
+
median: float | None
|
|
101
|
+
mode: float | None
|
|
102
|
+
minimum: float | None
|
|
103
|
+
maximum: float | None
|
|
104
|
+
range: float | None
|
|
105
|
+
variance: float | None
|
|
106
|
+
std_dev: float | None
|
|
107
|
+
q1: float | None
|
|
108
|
+
q2: float | None
|
|
109
|
+
q3: float | None
|
|
110
|
+
iqr: float | None
|
|
111
|
+
sum: float | None
|
|
112
|
+
zero_count: int
|
|
113
|
+
negative_count: int
|
|
114
|
+
positive_count: int
|
|
115
|
+
skewness: float | None
|
|
116
|
+
kurtosis: float | None
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@dataclass(frozen=True, slots=True)
|
|
120
|
+
class CategoricalProfile:
|
|
121
|
+
"""Detailed profiling statistics for a categorical column.
|
|
122
|
+
|
|
123
|
+
Attributes:
|
|
124
|
+
column_name: Name of the column.
|
|
125
|
+
cardinality: Number of unique non-missing categories.
|
|
126
|
+
unique_count: Number of unique non-missing values.
|
|
127
|
+
missing_count: Total missing count.
|
|
128
|
+
frequency_table: Mapping of value string representation to count (capped to a maximum size, default 1000).
|
|
129
|
+
top_values: List of (value, count) pairs for top frequent items.
|
|
130
|
+
least_frequent_values: List of (value, count) pairs for least frequent items.
|
|
131
|
+
most_common_category: Name of the most frequent category.
|
|
132
|
+
entropy: Shannon entropy of categories (base 2).
|
|
133
|
+
"""
|
|
134
|
+
|
|
135
|
+
column_name: str
|
|
136
|
+
cardinality: int
|
|
137
|
+
unique_count: int
|
|
138
|
+
missing_count: int
|
|
139
|
+
frequency_table: Mapping[str, int]
|
|
140
|
+
top_values: Sequence[tuple[str, int]]
|
|
141
|
+
least_frequent_values: Sequence[tuple[str, int]]
|
|
142
|
+
most_common_category: str | None
|
|
143
|
+
entropy: float | None
|
|
144
|
+
|
|
145
|
+
def __post_init__(self) -> None:
|
|
146
|
+
"""Freeze mutable fields to improve immutability consistency."""
|
|
147
|
+
from types import MappingProxyType
|
|
148
|
+
|
|
149
|
+
object.__setattr__(
|
|
150
|
+
self, "frequency_table", MappingProxyType(dict(self.frequency_table))
|
|
151
|
+
)
|
|
152
|
+
object.__setattr__(self, "top_values", tuple(self.top_values))
|
|
153
|
+
object.__setattr__(
|
|
154
|
+
self, "least_frequent_values", tuple(self.least_frequent_values)
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@dataclass(frozen=True, slots=True)
|
|
159
|
+
class DatetimeProfile:
|
|
160
|
+
"""Detailed profiling statistics for a datetime column.
|
|
161
|
+
|
|
162
|
+
Attributes:
|
|
163
|
+
column_name: Name of the column.
|
|
164
|
+
minimum: Earliest timestamp string (ISO 8601).
|
|
165
|
+
maximum: Latest timestamp string (ISO 8601).
|
|
166
|
+
range_days: Range of timestamps in days.
|
|
167
|
+
missing_count: Number of missing values.
|
|
168
|
+
earliest_record: Same as minimum.
|
|
169
|
+
latest_record: Same as maximum.
|
|
170
|
+
"""
|
|
171
|
+
|
|
172
|
+
column_name: str
|
|
173
|
+
minimum: str | None
|
|
174
|
+
maximum: str | None
|
|
175
|
+
range_days: float | None
|
|
176
|
+
missing_count: int
|
|
177
|
+
earliest_record: str | None
|
|
178
|
+
latest_record: str | None
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
@dataclass(frozen=True, slots=True)
|
|
182
|
+
class TextProfile:
|
|
183
|
+
"""Detailed profiling statistics for a text column.
|
|
184
|
+
|
|
185
|
+
Attributes:
|
|
186
|
+
column_name: Name of the column.
|
|
187
|
+
avg_length: Average character length.
|
|
188
|
+
min_length: Minimum character length.
|
|
189
|
+
max_length: Maximum character length.
|
|
190
|
+
empty_strings: Number of empty string values ("").
|
|
191
|
+
whitespace_only: Number of strings containing only whitespace.
|
|
192
|
+
char_count: Total character count across all strings.
|
|
193
|
+
word_count: Total word count across all strings.
|
|
194
|
+
"""
|
|
195
|
+
|
|
196
|
+
column_name: str
|
|
197
|
+
avg_length: float | None
|
|
198
|
+
min_length: int | None
|
|
199
|
+
max_length: int | None
|
|
200
|
+
empty_strings: int
|
|
201
|
+
whitespace_only: int
|
|
202
|
+
char_count: int
|
|
203
|
+
word_count: int
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
@dataclass(frozen=True, slots=True)
|
|
207
|
+
class MissingValueSummary:
|
|
208
|
+
"""Summary of missingness across the dataset.
|
|
209
|
+
|
|
210
|
+
Attributes:
|
|
211
|
+
column_missing_counts: Mapping of column name to missing count.
|
|
212
|
+
column_missing_percentages: Mapping of column name to missing percentage.
|
|
213
|
+
total_missing: Total count of missing cells.
|
|
214
|
+
dataset_missing_percentage: Overall dataset-wide missing cell percentage.
|
|
215
|
+
"""
|
|
216
|
+
|
|
217
|
+
column_missing_counts: Mapping[str, int]
|
|
218
|
+
column_missing_percentages: Mapping[str, float]
|
|
219
|
+
total_missing: int
|
|
220
|
+
dataset_missing_percentage: float
|
|
221
|
+
|
|
222
|
+
def __post_init__(self) -> None:
|
|
223
|
+
"""Freeze mutable fields to improve immutability consistency."""
|
|
224
|
+
from types import MappingProxyType
|
|
225
|
+
|
|
226
|
+
object.__setattr__(
|
|
227
|
+
self,
|
|
228
|
+
"column_missing_counts",
|
|
229
|
+
MappingProxyType(dict(self.column_missing_counts)),
|
|
230
|
+
)
|
|
231
|
+
object.__setattr__(
|
|
232
|
+
self,
|
|
233
|
+
"column_missing_percentages",
|
|
234
|
+
MappingProxyType(dict(self.column_missing_percentages)),
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
@dataclass(frozen=True, slots=True)
|
|
239
|
+
class DuplicateSummary:
|
|
240
|
+
"""Summary of duplicate records and redundant columns.
|
|
241
|
+
|
|
242
|
+
Attributes:
|
|
243
|
+
duplicate_rows_count: Number of duplicate rows.
|
|
244
|
+
duplicate_percentage: Percentage of duplicate rows.
|
|
245
|
+
constant_columns: List of constant column names.
|
|
246
|
+
fully_empty_columns: List of fully empty column names.
|
|
247
|
+
"""
|
|
248
|
+
|
|
249
|
+
duplicate_rows_count: int
|
|
250
|
+
duplicate_percentage: float
|
|
251
|
+
constant_columns: Sequence[str]
|
|
252
|
+
fully_empty_columns: Sequence[str]
|
|
253
|
+
|
|
254
|
+
def __post_init__(self) -> None:
|
|
255
|
+
"""Freeze mutable fields to improve immutability consistency."""
|
|
256
|
+
object.__setattr__(self, "constant_columns", tuple(self.constant_columns))
|
|
257
|
+
object.__setattr__(self, "fully_empty_columns", tuple(self.fully_empty_columns))
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
@dataclass(frozen=True, slots=True)
|
|
261
|
+
class CorrelationSummary:
|
|
262
|
+
"""Summary of pairwise column correlations.
|
|
263
|
+
|
|
264
|
+
Attributes:
|
|
265
|
+
pearson: Mapping of colA -> colB -> Pearson correlation value.
|
|
266
|
+
spearman: Reserved for Spearman correlations.
|
|
267
|
+
kendall: Reserved for Kendall correlations.
|
|
268
|
+
"""
|
|
269
|
+
|
|
270
|
+
pearson: Mapping[str, Mapping[str, float | None]]
|
|
271
|
+
spearman: Mapping[str, Mapping[str, float | None]] = field(default_factory=dict)
|
|
272
|
+
kendall: Mapping[str, Mapping[str, float | None]] = field(default_factory=dict)
|
|
273
|
+
|
|
274
|
+
def __post_init__(self) -> None:
|
|
275
|
+
"""Freeze mutable fields to improve immutability consistency."""
|
|
276
|
+
from types import MappingProxyType
|
|
277
|
+
|
|
278
|
+
pearson_frozen = {k: MappingProxyType(dict(v)) for k, v in self.pearson.items()}
|
|
279
|
+
object.__setattr__(self, "pearson", MappingProxyType(pearson_frozen))
|
|
280
|
+
|
|
281
|
+
spearman_frozen = {
|
|
282
|
+
k: MappingProxyType(dict(v)) for k, v in self.spearman.items()
|
|
283
|
+
}
|
|
284
|
+
object.__setattr__(self, "spearman", MappingProxyType(spearman_frozen))
|
|
285
|
+
|
|
286
|
+
kendall_frozen = {k: MappingProxyType(dict(v)) for k, v in self.kendall.items()}
|
|
287
|
+
object.__setattr__(self, "kendall", MappingProxyType(kendall_frozen))
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
@dataclass(frozen=True, slots=True)
|
|
291
|
+
class DatasetMetadata:
|
|
292
|
+
"""Descriptive metadata from the data source and connector.
|
|
293
|
+
|
|
294
|
+
Attributes:
|
|
295
|
+
source: Source file path or identifier.
|
|
296
|
+
file_size: Size in bytes if applicable.
|
|
297
|
+
backend: DataFrame backend ("polars" or "pandas").
|
|
298
|
+
custom_metadata: Optional dictionary of extra connector metadata.
|
|
299
|
+
"""
|
|
300
|
+
|
|
301
|
+
source: str | None
|
|
302
|
+
file_size: int | None
|
|
303
|
+
backend: str
|
|
304
|
+
custom_metadata: Mapping[str, Any] = field(default_factory=dict)
|
|
305
|
+
|
|
306
|
+
def __post_init__(self) -> None:
|
|
307
|
+
"""Freeze mutable fields to improve immutability consistency."""
|
|
308
|
+
from types import MappingProxyType
|
|
309
|
+
|
|
310
|
+
object.__setattr__(
|
|
311
|
+
self, "custom_metadata", MappingProxyType(dict(self.custom_metadata))
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
@dataclass(frozen=True, slots=True)
|
|
316
|
+
class ExecutionMetadata:
|
|
317
|
+
"""Metadata about the profiling run itself.
|
|
318
|
+
|
|
319
|
+
Attributes:
|
|
320
|
+
start_time: ISO 8601 timestamp when profiling started.
|
|
321
|
+
duration_seconds: Duration of the profiling run.
|
|
322
|
+
featuresmith_version: Version of Featuresmith used.
|
|
323
|
+
"""
|
|
324
|
+
|
|
325
|
+
start_time: str
|
|
326
|
+
duration_seconds: float
|
|
327
|
+
featuresmith_version: str
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
@dataclass(frozen=True, slots=True)
|
|
331
|
+
class ProfileResult:
|
|
332
|
+
"""The canonical, strongly-typed output of the profiling engine.
|
|
333
|
+
|
|
334
|
+
Attributes:
|
|
335
|
+
dataset_summary: High-level dataset summary.
|
|
336
|
+
column_profiles: Individual column overview profiles.
|
|
337
|
+
numeric_profiles: Numeric profiling details.
|
|
338
|
+
categorical_profiles: Categorical profiling details.
|
|
339
|
+
datetime_profiles: Datetime profiling details.
|
|
340
|
+
text_profiles: Text profiling details.
|
|
341
|
+
missing_value_summary: Missing value summary.
|
|
342
|
+
duplicate_summary: Duplicate summary.
|
|
343
|
+
correlation_summary: Correlation matrix.
|
|
344
|
+
dataset_metadata: Source metadata.
|
|
345
|
+
execution_metadata: Run execution metadata.
|
|
346
|
+
"""
|
|
347
|
+
|
|
348
|
+
dataset_summary: DatasetSummary
|
|
349
|
+
column_profiles: Mapping[str, ColumnProfile]
|
|
350
|
+
numeric_profiles: Mapping[str, NumericProfile]
|
|
351
|
+
categorical_profiles: Mapping[str, CategoricalProfile]
|
|
352
|
+
datetime_profiles: Mapping[str, DatetimeProfile]
|
|
353
|
+
text_profiles: Mapping[str, TextProfile]
|
|
354
|
+
missing_value_summary: MissingValueSummary
|
|
355
|
+
duplicate_summary: DuplicateSummary
|
|
356
|
+
correlation_summary: CorrelationSummary
|
|
357
|
+
dataset_metadata: DatasetMetadata
|
|
358
|
+
execution_metadata: ExecutionMetadata
|
|
359
|
+
|
|
360
|
+
def __post_init__(self) -> None:
|
|
361
|
+
"""Freeze mutable fields to improve immutability consistency."""
|
|
362
|
+
from types import MappingProxyType
|
|
363
|
+
|
|
364
|
+
object.__setattr__(
|
|
365
|
+
self, "column_profiles", MappingProxyType(dict(self.column_profiles))
|
|
366
|
+
)
|
|
367
|
+
object.__setattr__(
|
|
368
|
+
self,
|
|
369
|
+
"numeric_profiles",
|
|
370
|
+
MappingProxyType(dict(self.numeric_profiles)),
|
|
371
|
+
)
|
|
372
|
+
object.__setattr__(
|
|
373
|
+
self,
|
|
374
|
+
"categorical_profiles",
|
|
375
|
+
MappingProxyType(dict(self.categorical_profiles)),
|
|
376
|
+
)
|
|
377
|
+
object.__setattr__(
|
|
378
|
+
self,
|
|
379
|
+
"datetime_profiles",
|
|
380
|
+
MappingProxyType(dict(self.datetime_profiles)),
|
|
381
|
+
)
|
|
382
|
+
object.__setattr__(
|
|
383
|
+
self, "text_profiles", MappingProxyType(dict(self.text_profiles))
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
def to_dict(self) -> dict[str, Any]:
|
|
387
|
+
"""Serialize the profile result to a dictionary of primitive values.
|
|
388
|
+
|
|
389
|
+
Returns:
|
|
390
|
+
A dictionary representation suitable for JSON serialization.
|
|
391
|
+
"""
|
|
392
|
+
from typing import cast
|
|
393
|
+
|
|
394
|
+
return cast(dict[str, Any], _asdict_custom(self))
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def _asdict_custom(obj: Any) -> Any:
|
|
398
|
+
"""Recursively convert dataclasses and mapping proxies to standard python primitives."""
|
|
399
|
+
import dataclasses
|
|
400
|
+
from types import MappingProxyType
|
|
401
|
+
|
|
402
|
+
if dataclasses.is_dataclass(obj):
|
|
403
|
+
return {
|
|
404
|
+
f.name: _asdict_custom(getattr(obj, f.name))
|
|
405
|
+
for f in dataclasses.fields(obj)
|
|
406
|
+
}
|
|
407
|
+
elif isinstance(obj, (dict, MappingProxyType)):
|
|
408
|
+
return {k: _asdict_custom(v) for k, v in obj.items()}
|
|
409
|
+
elif isinstance(obj, (list, tuple)):
|
|
410
|
+
return [_asdict_custom(v) for v in obj]
|
|
411
|
+
else:
|
|
412
|
+
return obj
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Serializable model representing a single rule engine finding."""
|
|
2
|
+
|
|
3
|
+
import uuid
|
|
4
|
+
from collections.abc import Mapping
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True, slots=True)
|
|
10
|
+
class RuleFinding:
|
|
11
|
+
"""A single finding generated by evaluating a rule against a profile.
|
|
12
|
+
|
|
13
|
+
Attributes:
|
|
14
|
+
rule_id: The stable namespaced identifier of the rule.
|
|
15
|
+
rule_name: Human-readable name of the rule.
|
|
16
|
+
category: The category of the rule ("quality", "statistical", "leakage").
|
|
17
|
+
severity: The severity of the finding ("info", "warning", "critical").
|
|
18
|
+
column_name: Name of the column this finding applies to, or None if dataset-wide.
|
|
19
|
+
title: Short title summarizing the finding.
|
|
20
|
+
description: Detailed explanation of the finding.
|
|
21
|
+
evidence: Dictionary of metrics or values that triggered the finding.
|
|
22
|
+
confidence: Confidence level in the finding (float between 0.0 and 1.0).
|
|
23
|
+
id: Unique identifier for this finding instance, defaults to a random UUID.
|
|
24
|
+
metadata: Optional dictionary of additional metadata.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
rule_id: str
|
|
28
|
+
rule_name: str
|
|
29
|
+
category: str
|
|
30
|
+
severity: str
|
|
31
|
+
column_name: str | None
|
|
32
|
+
title: str
|
|
33
|
+
description: str
|
|
34
|
+
evidence: Mapping[str, Any]
|
|
35
|
+
confidence: float = 1.0
|
|
36
|
+
id: str = field(default_factory=lambda: str(uuid.uuid4()))
|
|
37
|
+
metadata: Mapping[str, Any] = field(default_factory=dict)
|
|
38
|
+
|
|
39
|
+
def __post_init__(self) -> None:
|
|
40
|
+
"""Freeze mutable fields to improve immutability consistency."""
|
|
41
|
+
from types import MappingProxyType
|
|
42
|
+
|
|
43
|
+
object.__setattr__(self, "evidence", MappingProxyType(dict(self.evidence)))
|
|
44
|
+
object.__setattr__(self, "metadata", MappingProxyType(dict(self.metadata)))
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Serializable model representing the result of a rule engine run."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping, Sequence
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from featuresmith.core.profile_result import ProfileResult
|
|
8
|
+
from featuresmith.core.rule_finding import RuleFinding
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True, slots=True)
|
|
12
|
+
class RuleResult:
|
|
13
|
+
"""The canonical output of the Featuresmith Rule Engine.
|
|
14
|
+
|
|
15
|
+
Attributes:
|
|
16
|
+
profile: The ProfileResult used for the evaluation.
|
|
17
|
+
findings: The list of rule findings generated.
|
|
18
|
+
executed_rules: List of rule IDs that were successfully executed.
|
|
19
|
+
execution_time_ms: Total time taken to run the rules in milliseconds.
|
|
20
|
+
failed_rules: Mapping from rule ID to the exception message/traceback for any rules that failed.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
profile: ProfileResult
|
|
24
|
+
findings: Sequence[RuleFinding]
|
|
25
|
+
executed_rules: Sequence[str]
|
|
26
|
+
execution_time_ms: float
|
|
27
|
+
failed_rules: Mapping[str, str] = field(default_factory=dict)
|
|
28
|
+
|
|
29
|
+
def __post_init__(self) -> None:
|
|
30
|
+
"""Freeze mutable fields to improve immutability consistency."""
|
|
31
|
+
from types import MappingProxyType
|
|
32
|
+
|
|
33
|
+
object.__setattr__(self, "findings", tuple(self.findings))
|
|
34
|
+
object.__setattr__(self, "executed_rules", tuple(self.executed_rules))
|
|
35
|
+
object.__setattr__(
|
|
36
|
+
self, "failed_rules", MappingProxyType(dict(self.failed_rules))
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
def to_dict(self) -> dict[str, Any]:
|
|
40
|
+
"""Serialize the rule result to a dictionary of primitive values.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
A dictionary representation suitable for JSON serialization.
|
|
44
|
+
"""
|
|
45
|
+
from typing import cast
|
|
46
|
+
|
|
47
|
+
from featuresmith.core.profile_result import _asdict_custom
|
|
48
|
+
|
|
49
|
+
return cast(dict[str, Any], _asdict_custom(self))
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Typed schemas shared by core pipeline stages."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True, slots=True)
|
|
9
|
+
class ColumnSchema:
|
|
10
|
+
"""Describe one column in a normalized dataset.
|
|
11
|
+
|
|
12
|
+
Attributes:
|
|
13
|
+
name: The column name as exposed by the dataframe backend.
|
|
14
|
+
dtype: The backend's stable string representation of the column dtype.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
name: str
|
|
18
|
+
dtype: str
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True, slots=True)
|
|
22
|
+
class DatasetSchema:
|
|
23
|
+
"""Describe the columns available in a normalized dataset.
|
|
24
|
+
|
|
25
|
+
Attributes:
|
|
26
|
+
columns: Ordered column descriptors from the source dataframe.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
columns: tuple[ColumnSchema, ...]
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def names(self) -> tuple[str, ...]:
|
|
33
|
+
"""Return the ordered column names."""
|
|
34
|
+
return tuple(column.name for column in self.columns)
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Categorical column profiling module."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
|
|
7
|
+
import polars as pl
|
|
8
|
+
|
|
9
|
+
from featuresmith.core.dataset import Dataset
|
|
10
|
+
from featuresmith.core.profile_result import CategoricalProfile
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def profile_categorical_column(
|
|
14
|
+
dataset: Dataset, col_name: str, max_frequency_table_size: int = 1000
|
|
15
|
+
) -> CategoricalProfile:
|
|
16
|
+
"""Profile a single categorical column.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
dataset: The normalized dataset.
|
|
20
|
+
col_name: Name of the column.
|
|
21
|
+
max_frequency_table_size: Maximum entries to keep in frequency_table (default 1000).
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
A CategoricalProfile object.
|
|
25
|
+
"""
|
|
26
|
+
df = dataset.dataframe
|
|
27
|
+
|
|
28
|
+
# Calculate missing count first
|
|
29
|
+
if dataset.backend == "pandas":
|
|
30
|
+
series = df[col_name]
|
|
31
|
+
missing_count = int(series.isna().sum())
|
|
32
|
+
else:
|
|
33
|
+
# Polars
|
|
34
|
+
missing_count = int(df.select(pl.col(col_name).null_count())[0, 0])
|
|
35
|
+
|
|
36
|
+
# Get non-null counts as frequency table
|
|
37
|
+
frequency_table: dict[str, int] = {}
|
|
38
|
+
if dataset.backend == "pandas":
|
|
39
|
+
series = df[col_name].dropna()
|
|
40
|
+
if len(series) > 0:
|
|
41
|
+
# Group and count
|
|
42
|
+
vc = series.astype(str).value_counts(dropna=True)
|
|
43
|
+
frequency_table = {str(k): int(v) for k, v in vc.to_dict().items()}
|
|
44
|
+
else:
|
|
45
|
+
# Polars
|
|
46
|
+
non_null_df = df.select(pl.col(col_name).drop_nulls().cast(pl.String))
|
|
47
|
+
if non_null_df.height > 0:
|
|
48
|
+
# Group by and count
|
|
49
|
+
freq_df = (
|
|
50
|
+
non_null_df.group_by(col_name)
|
|
51
|
+
.len()
|
|
52
|
+
.sort(by=["len", col_name], descending=[True, False])
|
|
53
|
+
)
|
|
54
|
+
categories = freq_df.get_column(col_name).to_list()
|
|
55
|
+
counts = freq_df.get_column("len").to_list()
|
|
56
|
+
frequency_table = {
|
|
57
|
+
str(k): int(v) for k, v in zip(categories, counts, strict=True)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
cardinality = len(frequency_table)
|
|
61
|
+
unique_count = cardinality
|
|
62
|
+
|
|
63
|
+
if cardinality == 0:
|
|
64
|
+
return CategoricalProfile(
|
|
65
|
+
column_name=col_name,
|
|
66
|
+
cardinality=0,
|
|
67
|
+
unique_count=0,
|
|
68
|
+
missing_count=missing_count,
|
|
69
|
+
frequency_table={},
|
|
70
|
+
top_values=[],
|
|
71
|
+
least_frequent_values=[],
|
|
72
|
+
most_common_category=None,
|
|
73
|
+
entropy=None,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# Deterministic sorting: count descending, key/value string ascending
|
|
77
|
+
sorted_items = sorted(frequency_table.items(), key=lambda x: (-x[1], x[0]))
|
|
78
|
+
top_values = sorted_items[:10]
|
|
79
|
+
|
|
80
|
+
# Deterministic sorting for least frequent: count ascending, key/value string ascending
|
|
81
|
+
sorted_least = sorted(frequency_table.items(), key=lambda x: (x[1], x[0]))
|
|
82
|
+
least_frequent_values = sorted_least[:10]
|
|
83
|
+
|
|
84
|
+
most_common_category = top_values[0][0] if top_values else None
|
|
85
|
+
|
|
86
|
+
# Entropy (Shannon entropy with base 2)
|
|
87
|
+
total_non_null = sum(frequency_table.values())
|
|
88
|
+
entropy = 0.0
|
|
89
|
+
for count in frequency_table.values():
|
|
90
|
+
p = count / total_non_null
|
|
91
|
+
if p > 0:
|
|
92
|
+
entropy -= p * math.log2(p)
|
|
93
|
+
|
|
94
|
+
# Cap frequency table size *after* computing cardinality, top/least values, and entropy
|
|
95
|
+
capped_items = sorted_items[:max_frequency_table_size]
|
|
96
|
+
frequency_table = dict(capped_items)
|
|
97
|
+
|
|
98
|
+
return CategoricalProfile(
|
|
99
|
+
column_name=col_name,
|
|
100
|
+
cardinality=cardinality,
|
|
101
|
+
unique_count=unique_count,
|
|
102
|
+
missing_count=missing_count,
|
|
103
|
+
frequency_table=frequency_table,
|
|
104
|
+
top_values=top_values,
|
|
105
|
+
least_frequent_values=least_frequent_values,
|
|
106
|
+
most_common_category=most_common_category,
|
|
107
|
+
entropy=entropy,
|
|
108
|
+
)
|