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 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
- ## Example
18
-
19
- user runs: /pandex scan sales.csv
20
- -> python scripts/profile.py sales.csv
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pandexai",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "PandexAI - AI-native data profiling CLI. Run pandex init inside your project to get started.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -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):