pandexai 0.1.2 → 0.1.3
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.
- package/commands/scan.md +24 -20
- package/package.json +1 -1
- package/scripts/profile.py +83 -0
package/commands/scan.md
CHANGED
|
@@ -1,21 +1,25 @@
|
|
|
1
|
-
# scan
|
|
2
|
-
|
|
3
|
-
Profiles a CSV file: column types, null percentage, unique counts, and
|
|
4
|
-
basic stats (mean/median/min/max for numeric columns, top value counts for
|
|
5
|
-
categorical columns).
|
|
6
|
-
|
|
7
|
-
## How to run this command
|
|
8
|
-
|
|
9
|
-
1. Run: python scripts/profile.py <path-to-csv>
|
|
10
|
-
(use the project's .pandex/venv Python interpreter, not the system one)
|
|
11
|
-
2. The script prints a JSON object with the profiling results.
|
|
12
|
-
3. Do NOT recompute, estimate, or guess any of these numbers yourself.
|
|
13
|
-
Present the JSON results to the user in a clear, readable summary, and
|
|
14
|
-
flag anything that looks like a data quality issue (e.g. high null %,
|
|
15
|
-
suspicious types, low-cardinality columns that might be categorical).
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
1
|
+
# scan
|
|
2
|
+
|
|
3
|
+
Profiles a CSV file: column types, null percentage, unique counts, and
|
|
4
|
+
basic stats (mean/median/min/max for numeric columns, top value counts for
|
|
5
|
+
categorical columns).
|
|
6
|
+
|
|
7
|
+
## How to run this command
|
|
8
|
+
|
|
9
|
+
1. Run: python scripts/profile.py <path-to-csv>
|
|
10
|
+
(use the project's .pandex/venv Python interpreter, not the system one)
|
|
11
|
+
2. The script prints a JSON object with the profiling results.
|
|
12
|
+
3. Do NOT recompute, estimate, or guess any of these numbers yourself.
|
|
13
|
+
Present the JSON results to the user in a clear, readable summary, and
|
|
14
|
+
flag anything that looks like a data quality issue (e.g. high null %,
|
|
15
|
+
suspicious types, low-cardinality columns that might be categorical).
|
|
16
|
+
4. Pay special attention to duplicate_rows, duplicate_column_names,
|
|
17
|
+
duplicate_values, blank_like_count, and inconsistent_casing_example in
|
|
18
|
+
the output - these represent real data quality problems the user should
|
|
19
|
+
know about before doing any analysis.
|
|
20
|
+
|
|
21
|
+
## Example
|
|
22
|
+
|
|
23
|
+
user runs: /pandex scan sales.csv
|
|
24
|
+
-> python scripts/profile.py sales.csv
|
|
21
25
|
-> present the resulting JSON as a readable summary
|
package/package.json
CHANGED
package/scripts/profile.py
CHANGED
|
@@ -2,13 +2,30 @@ import sys
|
|
|
2
2
|
import json
|
|
3
3
|
import pandas as pd
|
|
4
4
|
|
|
5
|
+
# Values that mean "missing" even though pandas won't catch them as NaN by default
|
|
6
|
+
BLANK_LIKE_VALUES = {"", "n/a", "na", "null", "none", "-", "nan", "unknown"}
|
|
7
|
+
|
|
5
8
|
|
|
6
9
|
def profile_csv(path):
|
|
10
|
+
# First, check the raw header row for duplicate column names before pandas
|
|
11
|
+
# silently renames them (e.g. "amount" and "amount" become "amount" and "amount.1")
|
|
12
|
+
with open(path, "r", encoding="utf-8", errors="replace") as f:
|
|
13
|
+
raw_header = f.readline().strip().split(",")
|
|
14
|
+
seen = {}
|
|
15
|
+
duplicate_column_names = []
|
|
16
|
+
for name in raw_header:
|
|
17
|
+
seen[name] = seen.get(name, 0) + 1
|
|
18
|
+
if seen[name] == 2:
|
|
19
|
+
duplicate_column_names.append(name)
|
|
20
|
+
|
|
7
21
|
df = pd.read_csv(path)
|
|
22
|
+
|
|
8
23
|
result = {
|
|
9
24
|
"file": path,
|
|
10
25
|
"row_count": len(df),
|
|
11
26
|
"column_count": len(df.columns),
|
|
27
|
+
"duplicate_column_names": duplicate_column_names,
|
|
28
|
+
"duplicate_rows": _duplicate_row_summary(df),
|
|
12
29
|
"columns": {}
|
|
13
30
|
}
|
|
14
31
|
|
|
@@ -30,11 +47,77 @@ def profile_csv(path):
|
|
|
30
47
|
top_values = series.value_counts().head(5)
|
|
31
48
|
col_info["top_values"] = {str(k): int(v) for k, v in top_values.items()}
|
|
32
49
|
|
|
50
|
+
blank_like_count = _count_blank_like(series)
|
|
51
|
+
if blank_like_count > 0:
|
|
52
|
+
col_info["blank_like_count"] = blank_like_count
|
|
53
|
+
col_info["blank_like_note"] = (
|
|
54
|
+
"Values like empty strings, 'N/A', '-', or 'null' as text were found. "
|
|
55
|
+
"These are not counted in null_count above but likely represent missing data."
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
inconsistent_casing = _check_inconsistent_casing(series)
|
|
59
|
+
if inconsistent_casing:
|
|
60
|
+
col_info["inconsistent_casing_example"] = inconsistent_casing
|
|
61
|
+
|
|
62
|
+
dup_check = _duplicate_value_summary(series, col)
|
|
63
|
+
if dup_check:
|
|
64
|
+
col_info["duplicate_values"] = dup_check
|
|
65
|
+
|
|
33
66
|
result["columns"][col] = col_info
|
|
34
67
|
|
|
35
68
|
return result
|
|
36
69
|
|
|
37
70
|
|
|
71
|
+
def _duplicate_row_summary(df):
|
|
72
|
+
dup_mask = df.duplicated(keep=False)
|
|
73
|
+
dup_count = int(df.duplicated(keep="first").sum())
|
|
74
|
+
if dup_count == 0:
|
|
75
|
+
return {"count": 0}
|
|
76
|
+
examples = df[dup_mask].head(3).to_dict(orient="records")
|
|
77
|
+
return {"count": dup_count, "example_rows": examples}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _duplicate_value_summary(series, column_name, uniqueness_threshold=0.9, max_examples=3):
|
|
81
|
+
non_null = series.dropna()
|
|
82
|
+
if len(non_null) == 0:
|
|
83
|
+
return None
|
|
84
|
+
uniqueness_ratio = non_null.nunique() / len(non_null)
|
|
85
|
+
looks_like_id = "id" in column_name.lower()
|
|
86
|
+
if not looks_like_id and uniqueness_ratio < uniqueness_threshold:
|
|
87
|
+
return None
|
|
88
|
+
value_counts = non_null.value_counts()
|
|
89
|
+
duplicated_values = value_counts[value_counts > 1]
|
|
90
|
+
if len(duplicated_values) == 0:
|
|
91
|
+
return None
|
|
92
|
+
return {
|
|
93
|
+
"duplicate_value_count": int(duplicated_values.sum() - len(duplicated_values)),
|
|
94
|
+
"examples": {str(k): int(v) for k, v in duplicated_values.head(max_examples).items()},
|
|
95
|
+
"note": "This column looks like it should have unique values (an ID or similar), but repeats were found."
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _count_blank_like(series):
|
|
100
|
+
non_null = series.dropna().astype(str).str.strip().str.lower()
|
|
101
|
+
return int(non_null.isin(BLANK_LIKE_VALUES).sum())
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _check_inconsistent_casing(series):
|
|
105
|
+
non_null = series.dropna().astype(str)
|
|
106
|
+
if len(non_null) == 0:
|
|
107
|
+
return None
|
|
108
|
+
original_unique = non_null.nunique()
|
|
109
|
+
lowered_unique = non_null.str.lower().nunique()
|
|
110
|
+
if lowered_unique < original_unique:
|
|
111
|
+
lower_to_variants = {}
|
|
112
|
+
for val in non_null.unique():
|
|
113
|
+
key = val.lower()
|
|
114
|
+
lower_to_variants.setdefault(key, set()).add(val)
|
|
115
|
+
for key, variants in lower_to_variants.items():
|
|
116
|
+
if len(variants) > 1:
|
|
117
|
+
return sorted(variants)
|
|
118
|
+
return None
|
|
119
|
+
|
|
120
|
+
|
|
38
121
|
def _safe_float(value):
|
|
39
122
|
try:
|
|
40
123
|
if pd.isna(value):
|