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,67 @@
|
|
|
1
|
+
"""Correlation summary module."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
|
|
7
|
+
import pandas as pd
|
|
8
|
+
import polars as pl
|
|
9
|
+
|
|
10
|
+
from featuresmith.core.dataset import Dataset
|
|
11
|
+
from featuresmith.core.profile_result import CorrelationSummary
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def compute_correlations(
|
|
15
|
+
dataset: Dataset,
|
|
16
|
+
numeric_columns: list[str],
|
|
17
|
+
max_correlation_columns: int = 100,
|
|
18
|
+
) -> CorrelationSummary:
|
|
19
|
+
"""Compute pairwise Pearson correlation matrix for numeric columns.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
dataset: The normalized dataset.
|
|
23
|
+
numeric_columns: List of numeric column names.
|
|
24
|
+
max_correlation_columns: Maximum number of numeric columns to correlate.
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
A CorrelationSummary object.
|
|
28
|
+
"""
|
|
29
|
+
if not numeric_columns:
|
|
30
|
+
return CorrelationSummary(pearson={})
|
|
31
|
+
|
|
32
|
+
# Cap columns to prevent combinatorial blowup
|
|
33
|
+
cols = numeric_columns[:max_correlation_columns]
|
|
34
|
+
df = dataset.dataframe
|
|
35
|
+
|
|
36
|
+
pearson: dict[str, dict[str, float | None]] = {c: {} for c in cols}
|
|
37
|
+
|
|
38
|
+
if dataset.backend == "pandas":
|
|
39
|
+
# Pandas
|
|
40
|
+
corr_df = df[cols].corr(method="pearson")
|
|
41
|
+
for col1 in cols:
|
|
42
|
+
for col2 in cols:
|
|
43
|
+
val = corr_df.loc[col1, col2]
|
|
44
|
+
if pd.isna(val) or math.isnan(val) or math.isinf(val):
|
|
45
|
+
pearson[col1][col2] = None
|
|
46
|
+
else:
|
|
47
|
+
pearson[col1][col2] = float(val)
|
|
48
|
+
else:
|
|
49
|
+
# Polars: compute correlations in parallel using expressions
|
|
50
|
+
exprs = []
|
|
51
|
+
for i, c1 in enumerate(cols):
|
|
52
|
+
for c2 in cols[i:]:
|
|
53
|
+
exprs.append(pl.corr(c1, c2).alias(f"{c1}__{c2}"))
|
|
54
|
+
|
|
55
|
+
if exprs:
|
|
56
|
+
res = df.select(exprs)
|
|
57
|
+
for i, c1 in enumerate(cols):
|
|
58
|
+
for c2 in cols[i:]:
|
|
59
|
+
val = res.get_column(f"{c1}__{c2}")[0]
|
|
60
|
+
if val is None or math.isnan(val) or math.isinf(val):
|
|
61
|
+
f_val = None
|
|
62
|
+
else:
|
|
63
|
+
f_val = float(val)
|
|
64
|
+
pearson[c1][c2] = f_val
|
|
65
|
+
pearson[c2][c1] = f_val
|
|
66
|
+
|
|
67
|
+
return CorrelationSummary(pearson=pearson)
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Datetime column profiling module."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import date, datetime
|
|
6
|
+
|
|
7
|
+
import pandas as pd
|
|
8
|
+
import polars as pl
|
|
9
|
+
|
|
10
|
+
from featuresmith.core.dataset import Dataset
|
|
11
|
+
from featuresmith.core.profile_result import DatetimeProfile
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def profile_datetime_column(dataset: Dataset, col_name: str) -> DatetimeProfile:
|
|
15
|
+
"""Profile a single datetime column.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
dataset: The normalized dataset.
|
|
19
|
+
col_name: Name of the column.
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
A DatetimeProfile object.
|
|
23
|
+
"""
|
|
24
|
+
df = dataset.dataframe
|
|
25
|
+
|
|
26
|
+
if dataset.backend == "pandas":
|
|
27
|
+
series = df[col_name]
|
|
28
|
+
missing_count = int(series.isna().sum())
|
|
29
|
+
|
|
30
|
+
min_val = series.min()
|
|
31
|
+
max_val = series.max()
|
|
32
|
+
|
|
33
|
+
# Check if min_val/max_val are null (NaT)
|
|
34
|
+
if pd.isna(min_val) or pd.isna(max_val):
|
|
35
|
+
return DatetimeProfile(
|
|
36
|
+
column_name=col_name,
|
|
37
|
+
minimum=None,
|
|
38
|
+
maximum=None,
|
|
39
|
+
range_days=None,
|
|
40
|
+
missing_count=missing_count,
|
|
41
|
+
earliest_record=None,
|
|
42
|
+
latest_record=None,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
# Handle conversion to standard datetime if they are Timestamp objects
|
|
46
|
+
if hasattr(min_val, "to_pydatetime"):
|
|
47
|
+
min_dt = min_val.to_pydatetime()
|
|
48
|
+
else:
|
|
49
|
+
min_dt = min_val
|
|
50
|
+
|
|
51
|
+
if hasattr(max_val, "to_pydatetime"):
|
|
52
|
+
max_dt = max_val.to_pydatetime()
|
|
53
|
+
else:
|
|
54
|
+
max_dt = max_val
|
|
55
|
+
|
|
56
|
+
# If min_dt/max_dt are date (not datetime) objects, convert them to datetimes to subtract safely
|
|
57
|
+
if isinstance(min_dt, date) and not isinstance(min_dt, datetime):
|
|
58
|
+
min_dt = datetime.combine(min_dt, datetime.min.time())
|
|
59
|
+
if isinstance(max_dt, date) and not isinstance(max_dt, datetime):
|
|
60
|
+
max_dt = datetime.combine(max_dt, datetime.min.time())
|
|
61
|
+
|
|
62
|
+
# ISO format
|
|
63
|
+
minimum = min_val.isoformat()
|
|
64
|
+
maximum = max_val.isoformat()
|
|
65
|
+
range_days = float((max_dt - min_dt).total_seconds() / 86400.0)
|
|
66
|
+
|
|
67
|
+
else:
|
|
68
|
+
# Polars
|
|
69
|
+
missing_count = int(df.select(pl.col(col_name).null_count())[0, 0])
|
|
70
|
+
min_val = df.select(pl.col(col_name).min())[0, 0]
|
|
71
|
+
max_val = df.select(pl.col(col_name).max())[0, 0]
|
|
72
|
+
|
|
73
|
+
if min_val is None or max_val is None:
|
|
74
|
+
return DatetimeProfile(
|
|
75
|
+
column_name=col_name,
|
|
76
|
+
minimum=None,
|
|
77
|
+
maximum=None,
|
|
78
|
+
range_days=None,
|
|
79
|
+
missing_count=missing_count,
|
|
80
|
+
earliest_record=None,
|
|
81
|
+
latest_record=None,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# Polars returns datetime.date or datetime.datetime objects directly
|
|
85
|
+
min_dt = min_val
|
|
86
|
+
max_dt = max_val
|
|
87
|
+
|
|
88
|
+
if isinstance(min_dt, date) and not isinstance(min_dt, datetime):
|
|
89
|
+
min_dt = datetime.combine(min_dt, datetime.min.time())
|
|
90
|
+
if isinstance(max_dt, date) and not isinstance(max_dt, datetime):
|
|
91
|
+
max_dt = datetime.combine(max_dt, datetime.min.time())
|
|
92
|
+
|
|
93
|
+
minimum = min_val.isoformat()
|
|
94
|
+
maximum = max_val.isoformat()
|
|
95
|
+
range_days = float((max_dt - min_dt).total_seconds() / 86400.0)
|
|
96
|
+
|
|
97
|
+
return DatetimeProfile(
|
|
98
|
+
column_name=col_name,
|
|
99
|
+
minimum=minimum,
|
|
100
|
+
maximum=maximum,
|
|
101
|
+
range_days=range_days,
|
|
102
|
+
missing_count=missing_count,
|
|
103
|
+
earliest_record=minimum,
|
|
104
|
+
latest_record=maximum,
|
|
105
|
+
)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Duplicate analysis module."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from featuresmith.core.dataset import Dataset
|
|
6
|
+
from featuresmith.core.profile_result import DuplicateSummary
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def analyze_duplicates(
|
|
10
|
+
dataset: Dataset, constant_columns: list[str], fully_empty_columns: list[str]
|
|
11
|
+
) -> DuplicateSummary:
|
|
12
|
+
"""Analyze duplicate rows in the dataset.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
dataset: The normalized dataset.
|
|
16
|
+
constant_columns: Pre-identified constant columns.
|
|
17
|
+
fully_empty_columns: Pre-identified fully empty columns.
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
A DuplicateSummary object.
|
|
21
|
+
"""
|
|
22
|
+
df = dataset.dataframe
|
|
23
|
+
row_count = dataset.row_count
|
|
24
|
+
|
|
25
|
+
if row_count == 0:
|
|
26
|
+
return DuplicateSummary(
|
|
27
|
+
duplicate_rows_count=0,
|
|
28
|
+
duplicate_percentage=0.0,
|
|
29
|
+
constant_columns=constant_columns,
|
|
30
|
+
fully_empty_columns=fully_empty_columns,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
if dataset.backend == "pandas":
|
|
34
|
+
duplicate_rows_count = int(df.duplicated().sum())
|
|
35
|
+
else:
|
|
36
|
+
# Polars: duplicate rows count is total rows minus unique rows
|
|
37
|
+
# This is equivalent to pandas duplicated().sum()
|
|
38
|
+
unique_rows_count = df.unique().height
|
|
39
|
+
duplicate_rows_count = row_count - unique_rows_count
|
|
40
|
+
|
|
41
|
+
duplicate_percentage = float((duplicate_rows_count / row_count) * 100.0)
|
|
42
|
+
|
|
43
|
+
return DuplicateSummary(
|
|
44
|
+
duplicate_rows_count=duplicate_rows_count,
|
|
45
|
+
duplicate_percentage=duplicate_percentage,
|
|
46
|
+
constant_columns=list(constant_columns),
|
|
47
|
+
fully_empty_columns=list(fully_empty_columns),
|
|
48
|
+
)
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Missing value analysis module."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import polars as pl
|
|
6
|
+
|
|
7
|
+
from featuresmith.core.dataset import Dataset
|
|
8
|
+
from featuresmith.core.profile_result import MissingValueSummary
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def analyze_missing_values(dataset: Dataset) -> MissingValueSummary:
|
|
12
|
+
"""Analyze missing values in the dataset.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
dataset: The normalized dataset.
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
A MissingValueSummary object.
|
|
19
|
+
"""
|
|
20
|
+
df = dataset.dataframe
|
|
21
|
+
row_count = dataset.row_count
|
|
22
|
+
col_count = dataset.column_count
|
|
23
|
+
total_cells = row_count * col_count
|
|
24
|
+
|
|
25
|
+
column_missing_counts: dict[str, int] = {}
|
|
26
|
+
column_missing_percentages: dict[str, float] = {}
|
|
27
|
+
total_missing = 0
|
|
28
|
+
|
|
29
|
+
if col_count == 0 or row_count == 0:
|
|
30
|
+
# Edge case: empty dataset
|
|
31
|
+
for col_name in dataset.schema.names:
|
|
32
|
+
column_missing_counts[col_name] = row_count
|
|
33
|
+
column_missing_percentages[col_name] = 100.0 if row_count > 0 else 0.0
|
|
34
|
+
return MissingValueSummary(
|
|
35
|
+
column_missing_counts=column_missing_counts,
|
|
36
|
+
column_missing_percentages=column_missing_percentages,
|
|
37
|
+
total_missing=row_count * col_count,
|
|
38
|
+
dataset_missing_percentage=100.0 if total_cells > 0 else 0.0,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
if dataset.backend == "pandas":
|
|
42
|
+
# Compute missing counts for all columns at once in pandas
|
|
43
|
+
missing_series = df.isna().sum()
|
|
44
|
+
for col_name in dataset.schema.names:
|
|
45
|
+
count = int(missing_series[col_name])
|
|
46
|
+
column_missing_counts[col_name] = count
|
|
47
|
+
column_missing_percentages[col_name] = float((count / row_count) * 100.0)
|
|
48
|
+
total_missing += count
|
|
49
|
+
else:
|
|
50
|
+
# Polars
|
|
51
|
+
# Compute null counts for all columns in a single select
|
|
52
|
+
null_counts = df.select(
|
|
53
|
+
[pl.col(col).null_count().alias(col) for col in dataset.schema.names]
|
|
54
|
+
)
|
|
55
|
+
for col_name in dataset.schema.names:
|
|
56
|
+
count = int(null_counts.get_column(col_name)[0])
|
|
57
|
+
column_missing_counts[col_name] = count
|
|
58
|
+
column_missing_percentages[col_name] = float((count / row_count) * 100.0)
|
|
59
|
+
total_missing += count
|
|
60
|
+
|
|
61
|
+
dataset_missing_percentage = (
|
|
62
|
+
float((total_missing / total_cells) * 100.0) if total_cells > 0 else 0.0
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
return MissingValueSummary(
|
|
66
|
+
column_missing_counts=column_missing_counts,
|
|
67
|
+
column_missing_percentages=column_missing_percentages,
|
|
68
|
+
total_missing=total_missing,
|
|
69
|
+
dataset_missing_percentage=dataset_missing_percentage,
|
|
70
|
+
)
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
"""Numeric column profiling module."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import polars as pl
|
|
9
|
+
|
|
10
|
+
from featuresmith.core.dataset import Dataset
|
|
11
|
+
from featuresmith.core.profile_result import NumericProfile
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _to_float(val: Any) -> float | None:
|
|
15
|
+
"""Safely convert a value to a Python float, returning None if NaN/Inf."""
|
|
16
|
+
if val is None:
|
|
17
|
+
return None
|
|
18
|
+
# Check for pandas/numpy nulls
|
|
19
|
+
if isinstance(val, (float, int)) and (math.isnan(val) or math.isinf(val)):
|
|
20
|
+
return None
|
|
21
|
+
try:
|
|
22
|
+
# If it's a series or list, we grab first element
|
|
23
|
+
if hasattr(val, "item"):
|
|
24
|
+
val = val.item()
|
|
25
|
+
elif hasattr(val, "iloc"):
|
|
26
|
+
val = val.iloc[0]
|
|
27
|
+
|
|
28
|
+
f = float(val)
|
|
29
|
+
if math.isnan(f) or math.isinf(f):
|
|
30
|
+
return None
|
|
31
|
+
return f
|
|
32
|
+
except (ValueError, TypeError):
|
|
33
|
+
return None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _to_int(val: Any) -> int:
|
|
37
|
+
"""Convert a value to a Python int."""
|
|
38
|
+
if val is None:
|
|
39
|
+
return 0
|
|
40
|
+
try:
|
|
41
|
+
if hasattr(val, "item"):
|
|
42
|
+
val = val.item()
|
|
43
|
+
return int(val)
|
|
44
|
+
except (ValueError, TypeError):
|
|
45
|
+
return 0
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def profile_numeric_column(dataset: Dataset, col_name: str) -> NumericProfile:
|
|
49
|
+
"""Profile a single numeric column.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
dataset: The normalized dataset.
|
|
53
|
+
col_name: Name of the column.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
A NumericProfile object.
|
|
57
|
+
"""
|
|
58
|
+
df = dataset.dataframe
|
|
59
|
+
row_count = dataset.row_count
|
|
60
|
+
|
|
61
|
+
if dataset.backend == "pandas":
|
|
62
|
+
series = df[col_name]
|
|
63
|
+
non_null_series = series.dropna()
|
|
64
|
+
count = int(non_null_series.count())
|
|
65
|
+
missing_count = int(series.isna().sum())
|
|
66
|
+
missing_percentage = (
|
|
67
|
+
float((missing_count / row_count) * 100.0) if row_count > 0 else 0.0
|
|
68
|
+
)
|
|
69
|
+
unique_count = int(non_null_series.nunique())
|
|
70
|
+
|
|
71
|
+
if count == 0:
|
|
72
|
+
return NumericProfile(
|
|
73
|
+
column_name=col_name,
|
|
74
|
+
count=0,
|
|
75
|
+
missing_count=missing_count,
|
|
76
|
+
missing_percentage=missing_percentage,
|
|
77
|
+
unique_count=0,
|
|
78
|
+
mean=None,
|
|
79
|
+
median=None,
|
|
80
|
+
mode=None,
|
|
81
|
+
minimum=None,
|
|
82
|
+
maximum=None,
|
|
83
|
+
range=None,
|
|
84
|
+
variance=None,
|
|
85
|
+
std_dev=None,
|
|
86
|
+
q1=None,
|
|
87
|
+
q2=None,
|
|
88
|
+
q3=None,
|
|
89
|
+
iqr=None,
|
|
90
|
+
sum=None,
|
|
91
|
+
zero_count=0,
|
|
92
|
+
negative_count=0,
|
|
93
|
+
positive_count=0,
|
|
94
|
+
skewness=None,
|
|
95
|
+
kurtosis=None,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
mean = _to_float(non_null_series.mean())
|
|
99
|
+
median = _to_float(non_null_series.median())
|
|
100
|
+
minimum = _to_float(non_null_series.min())
|
|
101
|
+
maximum = _to_float(non_null_series.max())
|
|
102
|
+
|
|
103
|
+
rng = None
|
|
104
|
+
if minimum is not None and maximum is not None:
|
|
105
|
+
rng = maximum - minimum
|
|
106
|
+
|
|
107
|
+
variance = _to_float(non_null_series.var(ddof=1))
|
|
108
|
+
std_dev = _to_float(non_null_series.std(ddof=1))
|
|
109
|
+
|
|
110
|
+
q1 = _to_float(non_null_series.quantile(0.25))
|
|
111
|
+
q2 = _to_float(non_null_series.quantile(0.50))
|
|
112
|
+
q3 = _to_float(non_null_series.quantile(0.75))
|
|
113
|
+
|
|
114
|
+
iqr = None
|
|
115
|
+
if q1 is not None and q3 is not None:
|
|
116
|
+
iqr = q3 - q1
|
|
117
|
+
|
|
118
|
+
sm = _to_float(non_null_series.sum())
|
|
119
|
+
zero_count = int((non_null_series == 0).sum())
|
|
120
|
+
negative_count = int((non_null_series < 0).sum())
|
|
121
|
+
positive_count = int((non_null_series > 0).sum())
|
|
122
|
+
|
|
123
|
+
# Mode
|
|
124
|
+
modes = non_null_series.mode()
|
|
125
|
+
mode = _to_float(modes.iloc[0]) if not modes.empty else None
|
|
126
|
+
|
|
127
|
+
skewness = _to_float(non_null_series.skew())
|
|
128
|
+
kurtosis = _to_float(non_null_series.kurt())
|
|
129
|
+
|
|
130
|
+
else:
|
|
131
|
+
# Polars
|
|
132
|
+
col = pl.col(col_name)
|
|
133
|
+
non_null_col = col.drop_nulls()
|
|
134
|
+
|
|
135
|
+
# Compute everything in a single select statement
|
|
136
|
+
res = df.select(
|
|
137
|
+
[
|
|
138
|
+
col.len().alias("total_len"),
|
|
139
|
+
col.null_count().alias("missing_count"),
|
|
140
|
+
non_null_col.n_unique().alias("unique_count"),
|
|
141
|
+
non_null_col.mean().alias("mean"),
|
|
142
|
+
non_null_col.median().alias("median"),
|
|
143
|
+
non_null_col.min().alias("min"),
|
|
144
|
+
non_null_col.max().alias("max"),
|
|
145
|
+
non_null_col.var(ddof=1).alias("var"),
|
|
146
|
+
non_null_col.std(ddof=1).alias("std"),
|
|
147
|
+
non_null_col.quantile(0.25).alias("q1"),
|
|
148
|
+
non_null_col.quantile(0.50).alias("q2"),
|
|
149
|
+
non_null_col.quantile(0.75).alias("q3"),
|
|
150
|
+
non_null_col.sum().alias("sum"),
|
|
151
|
+
(non_null_col == 0).sum().alias("zero_count"),
|
|
152
|
+
(non_null_col < 0).sum().alias("negative_count"),
|
|
153
|
+
(non_null_col > 0).sum().alias("positive_count"),
|
|
154
|
+
non_null_col.skew().alias("skew"),
|
|
155
|
+
non_null_col.kurtosis().alias("kurt"),
|
|
156
|
+
non_null_col.mode().sort().first().alias("mode"),
|
|
157
|
+
]
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
total_len = _to_int(res.get_column("total_len")[0])
|
|
161
|
+
missing_count = _to_int(res.get_column("missing_count")[0])
|
|
162
|
+
missing_percentage = (
|
|
163
|
+
float((missing_count / total_len) * 100.0) if total_len > 0 else 0.0
|
|
164
|
+
)
|
|
165
|
+
unique_count = _to_int(res.get_column("unique_count")[0])
|
|
166
|
+
count = total_len - missing_count
|
|
167
|
+
|
|
168
|
+
if count == 0:
|
|
169
|
+
return NumericProfile(
|
|
170
|
+
column_name=col_name,
|
|
171
|
+
count=0,
|
|
172
|
+
missing_count=missing_count,
|
|
173
|
+
missing_percentage=missing_percentage,
|
|
174
|
+
unique_count=0,
|
|
175
|
+
mean=None,
|
|
176
|
+
median=None,
|
|
177
|
+
mode=None,
|
|
178
|
+
minimum=None,
|
|
179
|
+
maximum=None,
|
|
180
|
+
range=None,
|
|
181
|
+
variance=None,
|
|
182
|
+
std_dev=None,
|
|
183
|
+
q1=None,
|
|
184
|
+
q2=None,
|
|
185
|
+
q3=None,
|
|
186
|
+
iqr=None,
|
|
187
|
+
sum=None,
|
|
188
|
+
zero_count=0,
|
|
189
|
+
negative_count=0,
|
|
190
|
+
positive_count=0,
|
|
191
|
+
skewness=None,
|
|
192
|
+
kurtosis=None,
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
mean = _to_float(res.get_column("mean")[0])
|
|
196
|
+
median = _to_float(res.get_column("median")[0])
|
|
197
|
+
minimum = _to_float(res.get_column("min")[0])
|
|
198
|
+
maximum = _to_float(res.get_column("max")[0])
|
|
199
|
+
|
|
200
|
+
rng = None
|
|
201
|
+
if minimum is not None and maximum is not None:
|
|
202
|
+
rng = maximum - minimum
|
|
203
|
+
|
|
204
|
+
variance = _to_float(res.get_column("var")[0])
|
|
205
|
+
std_dev = _to_float(res.get_column("std")[0])
|
|
206
|
+
|
|
207
|
+
q1 = _to_float(res.get_column("q1")[0])
|
|
208
|
+
q2 = _to_float(res.get_column("q2")[0])
|
|
209
|
+
q3 = _to_float(res.get_column("q3")[0])
|
|
210
|
+
|
|
211
|
+
iqr = None
|
|
212
|
+
if q1 is not None and q3 is not None:
|
|
213
|
+
iqr = q3 - q1
|
|
214
|
+
|
|
215
|
+
sm = _to_float(res.get_column("sum")[0])
|
|
216
|
+
zero_count = _to_int(res.get_column("zero_count")[0])
|
|
217
|
+
negative_count = _to_int(res.get_column("negative_count")[0])
|
|
218
|
+
positive_count = _to_int(res.get_column("positive_count")[0])
|
|
219
|
+
|
|
220
|
+
mode = _to_float(res.get_column("mode")[0])
|
|
221
|
+
skewness = _to_float(res.get_column("skew")[0])
|
|
222
|
+
kurtosis = _to_float(res.get_column("kurt")[0])
|
|
223
|
+
|
|
224
|
+
return NumericProfile(
|
|
225
|
+
column_name=col_name,
|
|
226
|
+
count=count,
|
|
227
|
+
missing_count=missing_count,
|
|
228
|
+
missing_percentage=missing_percentage,
|
|
229
|
+
unique_count=unique_count,
|
|
230
|
+
mean=mean,
|
|
231
|
+
median=median,
|
|
232
|
+
mode=mode,
|
|
233
|
+
minimum=minimum,
|
|
234
|
+
maximum=maximum,
|
|
235
|
+
range=rng,
|
|
236
|
+
variance=variance,
|
|
237
|
+
std_dev=std_dev,
|
|
238
|
+
q1=q1,
|
|
239
|
+
q2=q2,
|
|
240
|
+
q3=q3,
|
|
241
|
+
iqr=iqr,
|
|
242
|
+
sum=sm,
|
|
243
|
+
zero_count=zero_count,
|
|
244
|
+
negative_count=negative_count,
|
|
245
|
+
positive_count=positive_count,
|
|
246
|
+
skewness=skewness,
|
|
247
|
+
kurtosis=kurtosis,
|
|
248
|
+
)
|