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 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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ seneca