sinica 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.
- seneca/__init__.py +0 -0
- seneca/core.py +264 -0
- sinica-0.1.0.dist-info/METADATA +14 -0
- sinica-0.1.0.dist-info/RECORD +6 -0
- sinica-0.1.0.dist-info/WHEEL +5 -0
- sinica-0.1.0.dist-info/top_level.txt +1 -0
seneca/__init__.py
ADDED
|
File without changes
|
seneca/core.py
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import pandas as pd
|
|
3
|
+
from scipy import stats
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ProfileReport:
|
|
7
|
+
"""
|
|
8
|
+
A tool for profiling and generating statistical summaries of pandas DataFrames.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
def __init__(self, df: pd.DataFrame):
|
|
12
|
+
if not isinstance(df, pd.DataFrame):
|
|
13
|
+
raise TypeError("Input must be a pandas DataFrame.")
|
|
14
|
+
self.df = df
|
|
15
|
+
|
|
16
|
+
def summary(self, decimals: int = 4) -> pd.DataFrame:
|
|
17
|
+
"""Generates a complete column-by-column statistical summary."""
|
|
18
|
+
_info = []
|
|
19
|
+
|
|
20
|
+
for col in self.df.columns:
|
|
21
|
+
s = self.df[col]
|
|
22
|
+
clean_s = s.dropna()
|
|
23
|
+
mode_val = clean_s.mode()
|
|
24
|
+
|
|
25
|
+
entry = {
|
|
26
|
+
"Column": str(col),
|
|
27
|
+
"dtype": str(s.dtype),
|
|
28
|
+
"missing": int(s.isnull().sum()),
|
|
29
|
+
"unique": int(s.nunique()),
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if pd.api.types.is_numeric_dtype(s):
|
|
33
|
+
entry.update(
|
|
34
|
+
{
|
|
35
|
+
"mean": round(float(clean_s.mean()), decimals)
|
|
36
|
+
if not clean_s.empty
|
|
37
|
+
else None,
|
|
38
|
+
"median": round(float(clean_s.median()), decimals)
|
|
39
|
+
if not clean_s.empty
|
|
40
|
+
else None,
|
|
41
|
+
"mode": round(float(mode_val.iloc[0]), decimals)
|
|
42
|
+
if not mode_val.empty
|
|
43
|
+
else None,
|
|
44
|
+
"min": round(float(clean_s.min()), decimals)
|
|
45
|
+
if not clean_s.empty
|
|
46
|
+
else None,
|
|
47
|
+
"max": round(float(clean_s.max()), decimals)
|
|
48
|
+
if not clean_s.empty
|
|
49
|
+
else None,
|
|
50
|
+
}
|
|
51
|
+
)
|
|
52
|
+
else:
|
|
53
|
+
entry.update(
|
|
54
|
+
{
|
|
55
|
+
"mean": "-",
|
|
56
|
+
"median": "-",
|
|
57
|
+
"mode": str(mode_val.iloc[0]) if not mode_val.empty else "-",
|
|
58
|
+
"min": str(clean_s.min()) if not clean_s.empty else "-",
|
|
59
|
+
"max": str(clean_s.max()) if not clean_s.empty else "-",
|
|
60
|
+
}
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
_info.append(entry)
|
|
64
|
+
|
|
65
|
+
return pd.DataFrame(_info).set_index("Column")
|
|
66
|
+
|
|
67
|
+
def missing_report(self) -> pd.DataFrame:
|
|
68
|
+
"""Returns a breakdown of missing values per column with percentages."""
|
|
69
|
+
total = self.df.isnull().sum()
|
|
70
|
+
percent = (total / len(self.df)) * 100
|
|
71
|
+
report = pd.DataFrame(
|
|
72
|
+
{"Missing Count": total, "Percentage (%)": round(percent, 2)}
|
|
73
|
+
)
|
|
74
|
+
return report[report["Missing Count"] > 0].sort_values(
|
|
75
|
+
by="Missing Count", ascending=False
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
def numeric_overview(self) -> pd.DataFrame:
|
|
79
|
+
"""Returns standard descriptive statistics for numeric columns only."""
|
|
80
|
+
return self.df.describe().T
|
|
81
|
+
|
|
82
|
+
def categorical_overview(self) -> pd.DataFrame:
|
|
83
|
+
"""Returns descriptive statistics for non-numeric columns only."""
|
|
84
|
+
return self.df.select_dtypes(exclude=["number"]).describe().T
|
|
85
|
+
|
|
86
|
+
def alerts(
|
|
87
|
+
self,
|
|
88
|
+
correlation_threshold: float = 0.8,
|
|
89
|
+
high_cardinality_threshold: int = 50,
|
|
90
|
+
imbalance_threshold: float = 0.8,
|
|
91
|
+
skewness_threshold: float = 1.0,
|
|
92
|
+
uniform_pvalue_threshold: float = 0.999,
|
|
93
|
+
duplicate_threshold: int = 10,
|
|
94
|
+
) -> pd.DataFrame:
|
|
95
|
+
"""Calculates dataset and column-level quality alerts and returns a standard pandas DataFrame."""
|
|
96
|
+
alerts_list: list[dict[str, str]] = []
|
|
97
|
+
n_rows = len(self.df)
|
|
98
|
+
|
|
99
|
+
# -------------------------------------------------------------------
|
|
100
|
+
# 1. Dataset-Level Alerts
|
|
101
|
+
# -------------------------------------------------------------------
|
|
102
|
+
if self.df.empty:
|
|
103
|
+
alerts_list.append(
|
|
104
|
+
{"Description": "Dataset is empty", "Alert Type": "Empty"}
|
|
105
|
+
)
|
|
106
|
+
return pd.DataFrame(alerts_list)
|
|
107
|
+
|
|
108
|
+
duplicate_count = self.df.duplicated().sum()
|
|
109
|
+
|
|
110
|
+
if duplicate_count > duplicate_threshold:
|
|
111
|
+
pct = round((duplicate_count / n_rows) * 100, 1)
|
|
112
|
+
alerts_list.append(
|
|
113
|
+
{
|
|
114
|
+
"Description": (
|
|
115
|
+
f"Dataset has {duplicate_count} ({pct}%) duplicate rows"
|
|
116
|
+
),
|
|
117
|
+
"Alert Type": "Duplicates",
|
|
118
|
+
}
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
# -------------------------------------------------------------------
|
|
122
|
+
# 2. Correlation Alert (Dataset Level)
|
|
123
|
+
# -------------------------------------------------------------------
|
|
124
|
+
numeric_df = self.df.select_dtypes(include=[np.number])
|
|
125
|
+
if numeric_df.shape[1] > 1:
|
|
126
|
+
corr_matrix = numeric_df.corr().abs()
|
|
127
|
+
for col in corr_matrix.columns:
|
|
128
|
+
high_corr_cols = corr_matrix.index[
|
|
129
|
+
(corr_matrix[col] >= correlation_threshold)
|
|
130
|
+
& (corr_matrix.index != col)
|
|
131
|
+
].tolist()
|
|
132
|
+
|
|
133
|
+
if high_corr_cols:
|
|
134
|
+
main_target = high_corr_cols[0]
|
|
135
|
+
other_count = len(high_corr_cols) - 1
|
|
136
|
+
other_text = (
|
|
137
|
+
f" and {other_count} other fields" if other_count > 0 else ""
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
alerts_list.append(
|
|
141
|
+
{
|
|
142
|
+
"Description": (
|
|
143
|
+
f"{col} is highly correlated with"
|
|
144
|
+
f" {main_target}{other_text}"
|
|
145
|
+
),
|
|
146
|
+
"Alert Type": "High correlation",
|
|
147
|
+
}
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
# -------------------------------------------------------------------
|
|
151
|
+
# 3. Column-Level Alerts
|
|
152
|
+
# -------------------------------------------------------------------
|
|
153
|
+
for col in self.df.columns:
|
|
154
|
+
s = self.df[col]
|
|
155
|
+
clean_s = s.dropna()
|
|
156
|
+
n_unique = clean_s.nunique()
|
|
157
|
+
n_missing = s.isnull().sum()
|
|
158
|
+
|
|
159
|
+
# Constant / Zeros
|
|
160
|
+
if n_unique == 1:
|
|
161
|
+
val = clean_s.iloc[0] if not clean_s.empty else ""
|
|
162
|
+
alerts_list.append(
|
|
163
|
+
{
|
|
164
|
+
"Description": f'{col} has constant value "{val}"',
|
|
165
|
+
"Alert Type": "Constant",
|
|
166
|
+
}
|
|
167
|
+
)
|
|
168
|
+
if val == 0:
|
|
169
|
+
alerts_list.append(
|
|
170
|
+
{
|
|
171
|
+
"Description": f"{col} contains only zeros",
|
|
172
|
+
"Alert Type": "Zeros",
|
|
173
|
+
}
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
# High Cardinality
|
|
177
|
+
elif n_unique > high_cardinality_threshold:
|
|
178
|
+
alerts_list.append(
|
|
179
|
+
{
|
|
180
|
+
"Description": (
|
|
181
|
+
f"{col} has a high cardinality: {n_unique} distinct values"
|
|
182
|
+
),
|
|
183
|
+
"Alert Type": "High cardinality",
|
|
184
|
+
}
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
# Imbalance
|
|
188
|
+
if not clean_s.empty and n_unique > 1:
|
|
189
|
+
top_freq = clean_s.value_counts(normalize=True).iloc[0]
|
|
190
|
+
if top_freq >= imbalance_threshold:
|
|
191
|
+
pct = round(top_freq * 100, 1)
|
|
192
|
+
alerts_list.append(
|
|
193
|
+
{
|
|
194
|
+
"Description": (
|
|
195
|
+
f"{col} is highly imbalanced ({pct}% top value)"
|
|
196
|
+
),
|
|
197
|
+
"Alert Type": "Imbalance",
|
|
198
|
+
}
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
# Missing Values
|
|
202
|
+
if n_missing > 0:
|
|
203
|
+
pct = round((n_missing / n_rows) * 100, 1)
|
|
204
|
+
alerts_list.append(
|
|
205
|
+
{
|
|
206
|
+
"Description": f"{col} has {n_missing} ({pct}%) missing values",
|
|
207
|
+
"Alert Type": "Missing",
|
|
208
|
+
}
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
# Infinite Values
|
|
212
|
+
if pd.api.types.is_numeric_dtype(s) and not pd.api.types.is_bool_dtype(s):
|
|
213
|
+
inf_count = np.isinf(s).sum()
|
|
214
|
+
if inf_count > 0:
|
|
215
|
+
alerts_list.append(
|
|
216
|
+
{
|
|
217
|
+
"Description": f"{col} has {inf_count} infinite values",
|
|
218
|
+
"Alert Type": "Infinite",
|
|
219
|
+
}
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
# Unique Values
|
|
223
|
+
if n_unique == n_rows and n_rows > 0:
|
|
224
|
+
alerts_list.append(
|
|
225
|
+
{
|
|
226
|
+
"Description": f"{col} has all unique values",
|
|
227
|
+
"Alert Type": "Unique",
|
|
228
|
+
}
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
# Skewness & Uniformity
|
|
232
|
+
if (
|
|
233
|
+
pd.api.types.is_numeric_dtype(s)
|
|
234
|
+
and not pd.api.types.is_bool_dtype(s)
|
|
235
|
+
and len(clean_s) > 3
|
|
236
|
+
):
|
|
237
|
+
col_skew = abs(clean_s.skew()) # type: ignore
|
|
238
|
+
if col_skew >= skewness_threshold: # type: ignore
|
|
239
|
+
alerts_list.append(
|
|
240
|
+
{
|
|
241
|
+
"Description": (
|
|
242
|
+
f"{col} is highly skewed ({round(col_skew, 2)})" # type: ignore
|
|
243
|
+
),
|
|
244
|
+
"Alert Type": "Skewed",
|
|
245
|
+
}
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
try:
|
|
249
|
+
counts, _ = np.histogram(clean_s, bins=10)
|
|
250
|
+
_, p_val = stats.chisquare(counts)
|
|
251
|
+
if p_val >= uniform_pvalue_threshold:
|
|
252
|
+
alerts_list.append(
|
|
253
|
+
{
|
|
254
|
+
"Description": f"{col} follows a uniform distribution",
|
|
255
|
+
"Alert Type": "Uniform",
|
|
256
|
+
}
|
|
257
|
+
)
|
|
258
|
+
except Exception: # noqa: BLE001, S110
|
|
259
|
+
pass
|
|
260
|
+
|
|
261
|
+
if not alerts_list:
|
|
262
|
+
return pd.DataFrame(columns=["Description", "Alert Type"])
|
|
263
|
+
|
|
264
|
+
return pd.DataFrame(alerts_list)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sinica
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A pandas DataFrame profiling tool for statistical summaries and quality alerts.
|
|
5
|
+
Author-email: sarvamm <srvmrtr@gmail.com>
|
|
6
|
+
Project-URL: Homepage, https://github.com/sarvamm/seneca
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Requires-Python: >=3.8
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
Requires-Dist: pandas>=1.2.0
|
|
13
|
+
Requires-Dist: numpy>=1.20.0
|
|
14
|
+
Requires-Dist: scipy>=1.6.0
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
seneca/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
seneca/core.py,sha256=tqvbdZVnnHL9F-QB3n9jldwkEmGot574AnnKaAU32ko,10377
|
|
3
|
+
sinica-0.1.0.dist-info/METADATA,sha256=shIpPQL-FMHya9Z_4tLvbYIaIouL4Mmahbq-cWUKO_k,547
|
|
4
|
+
sinica-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
5
|
+
sinica-0.1.0.dist-info/top_level.txt,sha256=oNQFO8HGTwLRkdrYvjRMdF93x4v6Yfe8iOxS3s7KZ3c,7
|
|
6
|
+
sinica-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
seneca
|