tigerdatalab 3.0.4__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.
Files changed (44) hide show
  1. tigerdatalab/__init__.py +17 -0
  2. tigerdatalab/analytics/__init__.py +0 -0
  3. tigerdatalab/analytics/category.py +38 -0
  4. tigerdatalab/analytics/customer.py +51 -0
  5. tigerdatalab/analytics/growth.py +86 -0
  6. tigerdatalab/analytics/kpi.py +66 -0
  7. tigerdatalab/analytics/product.py +40 -0
  8. tigerdatalab/analytics/profitability.py +24 -0
  9. tigerdatalab/analytics/trends.py +79 -0
  10. tigerdatalab/cli/__init__.py +0 -0
  11. tigerdatalab/cli/main.py +77 -0
  12. tigerdatalab/config.py +47 -0
  13. tigerdatalab/core.py +319 -0
  14. tigerdatalab/dashboard/__init__.py +0 -0
  15. tigerdatalab/dashboard/builder.py +145 -0
  16. tigerdatalab/dataops/__init__.py +0 -0
  17. tigerdatalab/dataops/asset.py +144 -0
  18. tigerdatalab/exceptions.py +71 -0
  19. tigerdatalab/insights/__init__.py +0 -0
  20. tigerdatalab/insights/engine.py +175 -0
  21. tigerdatalab/insights/questions.py +112 -0
  22. tigerdatalab/io/__init__.py +0 -0
  23. tigerdatalab/io/loaders.py +237 -0
  24. tigerdatalab/quality/__init__.py +0 -0
  25. tigerdatalab/quality/anomalies.py +45 -0
  26. tigerdatalab/quality/cleaning.py +85 -0
  27. tigerdatalab/quality/profiler.py +146 -0
  28. tigerdatalab/quality/types.py +163 -0
  29. tigerdatalab/reporting/__init__.py +0 -0
  30. tigerdatalab/reporting/_safe_io.py +63 -0
  31. tigerdatalab/reporting/exporters.py +34 -0
  32. tigerdatalab/reporting/html.py +44 -0
  33. tigerdatalab/reporting/pdf.py +168 -0
  34. tigerdatalab/reporting/terminal.py +137 -0
  35. tigerdatalab/scale/__init__.py +0 -0
  36. tigerdatalab/scale/duckdb_engine.py +96 -0
  37. tigerdatalab/visualization/__init__.py +0 -0
  38. tigerdatalab/visualization/charts.py +247 -0
  39. tigerdatalab-3.0.4.dist-info/METADATA +235 -0
  40. tigerdatalab-3.0.4.dist-info/RECORD +44 -0
  41. tigerdatalab-3.0.4.dist-info/WHEEL +5 -0
  42. tigerdatalab-3.0.4.dist-info/entry_points.txt +2 -0
  43. tigerdatalab-3.0.4.dist-info/licenses/LICENSE +21 -0
  44. tigerdatalab-3.0.4.dist-info/top_level.txt +1 -0
@@ -0,0 +1,17 @@
1
+ """TigerDataLab — an automated Data Analytics + Data Quality + Visualization
2
+ + BI + DataOps layer on top of pandas, numpy, duckdb and plotly.
3
+
4
+ import tigerdatalab as tdl
5
+ result = tdl.analyze("sales.xlsx")
6
+ print(result.summary())
7
+ result.report("analysis")
8
+ """
9
+ from .config import __version__
10
+ from .core import AnalysisResult, analyze, open, large, profile, quality_check, clean_file
11
+ from .exceptions import TigerDataLabError, UnsupportedFileTypeError, NoTrendDataError, NoCustomerIdentifierError
12
+
13
+ __all__ = [
14
+ "__version__", "AnalysisResult", "analyze", "open", "large",
15
+ "profile", "quality_check", "clean_file",
16
+ "TigerDataLabError", "UnsupportedFileTypeError", "NoTrendDataError", "NoCustomerIdentifierError",
17
+ ]
File without changes
@@ -0,0 +1,38 @@
1
+ """Category-level analytics + Pareto data for chart engine."""
2
+ from __future__ import annotations
3
+
4
+ import pandas as pd
5
+
6
+
7
+ def analyze_categories(df: pd.DataFrame, semantics: dict) -> dict:
8
+ category_col = semantics.get("category")
9
+ if not category_col or category_col not in df.columns:
10
+ return {"available": False, "reason": "No category/segment column was detected."}
11
+
12
+ revenue_col = semantics.get("revenue")
13
+ profit_col = semantics.get("profit")
14
+
15
+ grouped = df.groupby(category_col)
16
+ result: dict = {"available": True, "category_column": category_col, "unique_categories": int(df[category_col].nunique())}
17
+
18
+ if revenue_col and revenue_col in df.columns:
19
+ rev = grouped[revenue_col].apply(lambda s: pd.to_numeric(s, errors="coerce").sum()).sort_values(ascending=False)
20
+ total = rev.sum()
21
+ result["revenue_by_category"] = [{"category": str(k), "revenue": float(v),
22
+ "share_pct": round(float(100 * v / total), 2) if total else 0}
23
+ for k, v in rev.items()]
24
+ if total:
25
+ result["top_category_revenue_share_pct"] = round(float(100 * rev.iloc[0] / total), 2)
26
+ result["top_category"] = str(rev.index[0])
27
+
28
+ if profit_col and profit_col in df.columns:
29
+ profit = grouped[profit_col].apply(lambda s: pd.to_numeric(s, errors="coerce").sum()).sort_values(ascending=False)
30
+ result["profit_by_category"] = [{"category": str(k), "profit": float(v)} for k, v in profit.items()]
31
+ if revenue_col and revenue_col in df.columns:
32
+ rev_by_cat = grouped[revenue_col].apply(lambda s: pd.to_numeric(s, errors="coerce").sum())
33
+ margin = (profit / rev_by_cat.reindex(profit.index) * 100).round(2)
34
+ margin_sorted = margin.sort_values()
35
+ result["worst_margin_category"] = str(margin_sorted.index[0]) if len(margin_sorted) else None
36
+ result["margin_by_category"] = [{"category": str(k), "margin_pct": float(v)} for k, v in margin.items() if pd.notna(v)]
37
+
38
+ return result
@@ -0,0 +1,51 @@
1
+ """Customer-level analytics. Raises NoCustomerIdentifierError when no
2
+ customer identifier column was detected, rather than inventing numbers."""
3
+ from __future__ import annotations
4
+
5
+ import pandas as pd
6
+
7
+ from ..exceptions import NoCustomerIdentifierError
8
+
9
+
10
+ def analyze_customers(df: pd.DataFrame, semantics: dict) -> dict:
11
+ customer_col = semantics.get("customer")
12
+ if not customer_col or customer_col not in df.columns:
13
+ raise NoCustomerIdentifierError()
14
+
15
+ revenue_col = semantics.get("revenue")
16
+ profit_col = semantics.get("profit")
17
+ order_col = semantics.get("order")
18
+
19
+ grouped = df.groupby(customer_col)
20
+ n_customers = int(df[customer_col].nunique())
21
+
22
+ result: dict = {"customer_column": customer_col, "unique_customers": n_customers}
23
+
24
+ if order_col and order_col in df.columns:
25
+ orders_per_customer = grouped[order_col].nunique()
26
+ result["avg_orders_per_customer"] = round(float(orders_per_customer.mean()), 2)
27
+ result["repeat_customers"] = int((orders_per_customer > 1).sum())
28
+ result["one_time_customers"] = int((orders_per_customer == 1).sum())
29
+ else:
30
+ orders_per_customer = grouped.size()
31
+ result["avg_orders_per_customer"] = round(float(orders_per_customer.mean()), 2)
32
+ result["repeat_customers"] = int((orders_per_customer > 1).sum())
33
+ result["one_time_customers"] = int((orders_per_customer == 1).sum())
34
+
35
+ if revenue_col and revenue_col in df.columns:
36
+ rev_by_customer = grouped[revenue_col].apply(lambda s: pd.to_numeric(s, errors="coerce").sum())
37
+ total_rev = rev_by_customer.sum()
38
+ result["avg_revenue_per_customer"] = round(float(rev_by_customer.mean()), 2)
39
+ top = rev_by_customer.sort_values(ascending=False).head(10)
40
+ result["top_customers"] = [{"customer": str(k), "revenue": float(v)} for k, v in top.items()]
41
+ bottom = rev_by_customer.sort_values(ascending=True).head(10)
42
+ result["bottom_customers"] = [{"customer": str(k), "revenue": float(v)} for k, v in bottom.items()]
43
+ if total_rev:
44
+ top5_share = top.head(5).sum() / total_rev * 100
45
+ result["top5_revenue_concentration_pct"] = round(float(top5_share), 2)
46
+
47
+ if profit_col and profit_col in df.columns:
48
+ profit_by_customer = grouped[profit_col].apply(lambda s: pd.to_numeric(s, errors="coerce").sum())
49
+ result["avg_profit_per_customer"] = round(float(profit_by_customer.mean()), 2)
50
+
51
+ return result
@@ -0,0 +1,86 @@
1
+ """Growth/decline detection per product and per category.
2
+
3
+ Splits the dataset's date range into an earlier half and a later half,
4
+ compares the chosen metric (revenue by default) per group, and tags each
5
+ group as growing / declining / flat. Requires a date column; degrades to
6
+ `{"available": False, ...}` when one isn't present rather than crashing.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import pandas as pd
11
+
12
+ from ..quality.types import parse_date_column
13
+
14
+ FLAT_BAND_PCT = 5.0
15
+
16
+
17
+ def _growth_by_group(df: pd.DataFrame, group_col: str, date_col: str, metric_col: str) -> dict:
18
+ dates = parse_date_column(df[date_col])
19
+ metric = pd.to_numeric(df[metric_col], errors="coerce")
20
+ work = pd.DataFrame({"group": df[group_col], "date": dates, "value": metric}).dropna()
21
+ if work.empty or work["date"].nunique() < 2:
22
+ return {"available": False, "reason": "Not enough valid date-tagged rows to compute growth."}
23
+
24
+ midpoint = work["date"].min() + (work["date"].max() - work["date"].min()) / 2
25
+ first_half = work[work["date"] <= midpoint].groupby("group")["value"].sum()
26
+ second_half = work[work["date"] > midpoint].groupby("group")["value"].sum()
27
+
28
+ all_groups = set(first_half.index) | set(second_half.index)
29
+ rows = []
30
+ for g in all_groups:
31
+ f = float(first_half.get(g, 0.0))
32
+ s = float(second_half.get(g, 0.0))
33
+ if f == 0 and s == 0:
34
+ continue
35
+ if f == 0:
36
+ change_pct = 100.0 if s > 0 else 0.0
37
+ else:
38
+ change_pct = round(100 * (s - f) / abs(f), 2)
39
+ if change_pct > FLAT_BAND_PCT:
40
+ status = "growing"
41
+ elif change_pct < -FLAT_BAND_PCT:
42
+ status = "declining"
43
+ else:
44
+ status = "flat"
45
+ rows.append({"group": str(g), "first_half": round(f, 2), "second_half": round(s, 2),
46
+ "change_pct": change_pct, "status": status})
47
+
48
+ rows.sort(key=lambda r: r["change_pct"])
49
+ growing = [r for r in rows if r["status"] == "growing"]
50
+ declining = [r for r in rows if r["status"] == "declining"]
51
+
52
+ return {
53
+ "available": True,
54
+ "group_column": group_col,
55
+ "metric_column": metric_col,
56
+ "split_date": midpoint.strftime("%Y-%m-%d"),
57
+ "growing": sorted(growing, key=lambda r: -r["change_pct"])[:10],
58
+ "declining": declining[:10],
59
+ "all": rows,
60
+ }
61
+
62
+
63
+ def analyze_growth(df: pd.DataFrame, semantics: dict) -> dict:
64
+ date_col = semantics.get("date")
65
+ metric_col = semantics.get("revenue") or semantics.get("profit") or semantics.get("quantity")
66
+ result = {"product": {"available": False}, "category": {"available": False}}
67
+
68
+ if not date_col or not metric_col:
69
+ result["product"] = {"available": False, "reason": "No date + numeric metric available for growth analysis."}
70
+ result["category"] = result["product"]
71
+ return result
72
+
73
+ product_col = semantics.get("product")
74
+ category_col = semantics.get("category")
75
+
76
+ if product_col and product_col in df.columns:
77
+ result["product"] = _growth_by_group(df, product_col, date_col, metric_col)
78
+ else:
79
+ result["product"] = {"available": False, "reason": "No product column detected."}
80
+
81
+ if category_col and category_col in df.columns:
82
+ result["category"] = _growth_by_group(df, category_col, date_col, metric_col)
83
+ else:
84
+ result["category"] = {"available": False, "reason": "No category column detected."}
85
+
86
+ return result
@@ -0,0 +1,66 @@
1
+ """Business KPI calculation from detected semantic columns."""
2
+ from __future__ import annotations
3
+
4
+ import pandas as pd
5
+
6
+
7
+ def calculate_kpis(df: pd.DataFrame, semantics: dict) -> dict:
8
+ kpis: dict = {}
9
+
10
+ def col(concept):
11
+ c = semantics.get(concept)
12
+ return pd.to_numeric(df[c], errors="coerce") if c and c in df.columns else None
13
+
14
+ revenue = col("revenue")
15
+ cost = col("cost")
16
+ profit = col("profit")
17
+ quantity = col("quantity")
18
+ discount = col("discount")
19
+
20
+ if revenue is not None:
21
+ kpis["total_revenue"] = float(revenue.sum())
22
+ kpis["average_selling_price"] = float(revenue.mean())
23
+
24
+ if cost is not None:
25
+ kpis["total_cost"] = float(cost.sum())
26
+
27
+ if profit is None and revenue is not None and cost is not None:
28
+ profit = revenue - cost
29
+ kpis["profit_derived"] = True
30
+
31
+ if profit is not None:
32
+ kpis["total_profit"] = float(profit.sum())
33
+ if revenue is not None and revenue.sum() != 0:
34
+ kpis["profit_margin_pct"] = round(100 * profit.sum() / revenue.sum(), 2)
35
+
36
+ if quantity is not None:
37
+ kpis["total_quantity"] = float(quantity.sum())
38
+ if revenue is not None and quantity.sum() != 0:
39
+ kpis["average_selling_price"] = round(float(revenue.sum() / quantity.sum()), 2)
40
+
41
+ order_col = semantics.get("order")
42
+ if order_col and order_col in df.columns:
43
+ n_orders = int(df[order_col].nunique())
44
+ kpis["orders"] = n_orders
45
+ if revenue is not None and n_orders:
46
+ kpis["average_order_value"] = round(float(revenue.sum() / n_orders), 2)
47
+ else:
48
+ kpis["orders"] = int(len(df))
49
+ if revenue is not None and len(df):
50
+ kpis["average_order_value"] = round(float(revenue.sum() / len(df)), 2)
51
+
52
+ customer_col = semantics.get("customer")
53
+ if customer_col and customer_col in df.columns:
54
+ kpis["customers"] = int(df[customer_col].nunique())
55
+
56
+ product_col = semantics.get("product")
57
+ if product_col and product_col in df.columns:
58
+ kpis["products"] = int(df[product_col].nunique())
59
+
60
+ if discount is not None:
61
+ kpis["average_discount_pct"] = round(float(discount.mean()), 2)
62
+ if revenue is not None and revenue.sum() != 0:
63
+ kpis["total_discount_value"] = round(float((revenue * discount / 100).sum()), 2) \
64
+ if discount.max() <= 1.5 * 100 and discount.max() > 1 else None
65
+
66
+ return kpis
@@ -0,0 +1,40 @@
1
+ """Product-level analytics."""
2
+ from __future__ import annotations
3
+
4
+ import pandas as pd
5
+
6
+
7
+ def analyze_products(df: pd.DataFrame, semantics: dict) -> dict:
8
+ product_col = semantics.get("product")
9
+ if not product_col or product_col not in df.columns:
10
+ return {"available": False, "reason": "No product identifier/name column was detected."}
11
+
12
+ revenue_col = semantics.get("revenue")
13
+ profit_col = semantics.get("profit")
14
+ quantity_col = semantics.get("quantity")
15
+ discount_col = semantics.get("discount")
16
+
17
+ grouped = df.groupby(product_col)
18
+ result: dict = {"available": True, "product_column": product_col, "unique_products": int(df[product_col].nunique())}
19
+
20
+ if revenue_col and revenue_col in df.columns:
21
+ rev = grouped[revenue_col].apply(lambda s: pd.to_numeric(s, errors="coerce").sum()).sort_values(ascending=False)
22
+ result["top_products_by_revenue"] = [{"product": str(k), "revenue": float(v)} for k, v in rev.head(10).items()]
23
+
24
+ if quantity_col and quantity_col in df.columns:
25
+ qty = grouped[quantity_col].apply(lambda s: pd.to_numeric(s, errors="coerce").sum()).sort_values(ascending=False)
26
+ result["top_products_by_quantity"] = [{"product": str(k), "quantity": float(v)} for k, v in qty.head(10).items()]
27
+
28
+ if profit_col and profit_col in df.columns:
29
+ profit = grouped[profit_col].apply(lambda s: pd.to_numeric(s, errors="coerce").sum()).sort_values(ascending=False)
30
+ result["top_products_by_profit"] = [{"product": str(k), "profit": float(v)} for k, v in profit.head(10).items()]
31
+ result["worst_products_by_profit"] = [{"product": str(k), "profit": float(v)} for k, v in profit.tail(10).items()]
32
+ negative = profit[profit < 0]
33
+ result["loss_making_products"] = [{"product": str(k), "profit": float(v)} for k, v in negative.items()]
34
+ result["loss_making_product_count"] = int(len(negative))
35
+
36
+ if discount_col and discount_col in df.columns:
37
+ disc = grouped[discount_col].apply(lambda s: pd.to_numeric(s, errors="coerce").mean()).sort_values(ascending=False)
38
+ result["highest_discount_products"] = [{"product": str(k), "avg_discount": float(v)} for k, v in disc.head(10).items()]
39
+
40
+ return result
@@ -0,0 +1,24 @@
1
+ """Profitability derivation: profit = revenue - cost when profit is absent."""
2
+ from __future__ import annotations
3
+
4
+ import pandas as pd
5
+
6
+
7
+ def ensure_profit_column(df: pd.DataFrame, semantics: dict) -> tuple[pd.DataFrame, dict, bool]:
8
+ """Return (df, semantics, derived) - adds a `_tdl_profit` column and
9
+ registers it under semantics['profit'] only if profit is not already
10
+ present but revenue and cost both are. Never overwrites an existing
11
+ business column."""
12
+ if semantics.get("profit"):
13
+ return df, semantics, False
14
+
15
+ revenue_col = semantics.get("revenue")
16
+ cost_col = semantics.get("cost")
17
+ if revenue_col and cost_col and revenue_col in df.columns and cost_col in df.columns:
18
+ out = df.copy()
19
+ out["_tdl_profit"] = pd.to_numeric(out[revenue_col], errors="coerce") - pd.to_numeric(out[cost_col], errors="coerce")
20
+ new_semantics = dict(semantics)
21
+ new_semantics["profit"] = "_tdl_profit"
22
+ return out, new_semantics, True
23
+
24
+ return df, semantics, False
@@ -0,0 +1,79 @@
1
+ """Time-trend analysis: monthly/daily aggregation, MoM, YoY, rolling avg."""
2
+ from __future__ import annotations
3
+
4
+ import pandas as pd
5
+
6
+ from ..exceptions import NoTrendDataError
7
+ from ..quality.types import parse_date_column, numeric_columns, detect_all_dtypes
8
+
9
+
10
+ def _pick_metric(df: pd.DataFrame, semantics: dict) -> str | None:
11
+ for concept in ("revenue", "profit", "cost", "quantity"):
12
+ col = semantics.get(concept)
13
+ if col and col in df.columns:
14
+ return col
15
+ numeric = numeric_columns(df)
16
+ return numeric[0] if numeric else None
17
+
18
+
19
+ def compute_trend(df: pd.DataFrame, semantics: dict) -> dict:
20
+ date_col = semantics.get("date")
21
+ if not date_col or date_col not in df.columns:
22
+ raise NoTrendDataError(candidates=[])
23
+
24
+ metric_col = _pick_metric(df, semantics)
25
+ if not metric_col:
26
+ raise NoTrendDataError(candidates=[date_col])
27
+
28
+ dates = parse_date_column(df[date_col])
29
+ metric = pd.to_numeric(df[metric_col], errors="coerce")
30
+ work = pd.DataFrame({"date": dates, "value": metric}).dropna()
31
+
32
+ if work.empty or work["date"].nunique() < 2:
33
+ raise NoTrendDataError(candidates=[date_col])
34
+
35
+ span_days = (work["date"].max() - work["date"].min()).days
36
+ freq = "D" if span_days <= 62 else "M"
37
+
38
+ work = work.set_index("date").sort_index()
39
+ if freq == "D":
40
+ agg = work["value"].resample("D").sum()
41
+ granularity = "daily"
42
+ else:
43
+ agg = work["value"].resample("ME").sum()
44
+ granularity = "monthly"
45
+
46
+ agg = agg[agg.index.notna()]
47
+ result_df = agg.reset_index()
48
+ result_df.columns = ["period", "value"]
49
+
50
+ growth_pct = None
51
+ if len(agg) >= 2 and agg.iloc[0] != 0:
52
+ growth_pct = round(100 * (agg.iloc[-1] - agg.iloc[0]) / abs(agg.iloc[0]), 2)
53
+
54
+ mom_pct = None
55
+ if len(agg) >= 2 and agg.iloc[-2] != 0:
56
+ mom_pct = round(100 * (agg.iloc[-1] - agg.iloc[-2]) / abs(agg.iloc[-2]), 2)
57
+
58
+ yoy_pct = None
59
+ if granularity == "monthly" and len(agg) >= 13 and agg.iloc[-13] != 0:
60
+ yoy_pct = round(100 * (agg.iloc[-1] - agg.iloc[-13]) / abs(agg.iloc[-13]), 2)
61
+
62
+ rolling = agg.rolling(window=3, min_periods=1).mean()
63
+
64
+ label = {"revenue": "Revenue", "profit": "Profit", "cost": "Cost", "quantity": "Quantity"}
65
+ metric_label = next((v for k, v in label.items() if semantics.get(k) == metric_col), metric_col)
66
+
67
+ return {
68
+ "date_column": date_col,
69
+ "metric_column": metric_col,
70
+ "metric_label": metric_label,
71
+ "granularity": granularity,
72
+ "title": f"{granularity.capitalize()} {metric_label} Trend",
73
+ "periods": [p.strftime("%Y-%m-%d") for p in result_df["period"]],
74
+ "values": [float(v) for v in result_df["value"]],
75
+ "rolling_average": [float(v) for v in rolling.values],
76
+ "growth_pct": growth_pct,
77
+ "mom_pct": mom_pct,
78
+ "yoy_pct": yoy_pct,
79
+ }
File without changes
@@ -0,0 +1,77 @@
1
+ """TigerDataLab command-line interface.
2
+
3
+ tigerdatalab analyze sales.csv
4
+ tigerdatalab dashboard sales.csv
5
+ tigerdatalab profile sales.csv
6
+ tigerdatalab quality sales.csv
7
+ tigerdatalab clean sales.csv
8
+ tigerdatalab report sales.csv
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import argparse
13
+ import sys
14
+
15
+ from .. import core
16
+ from ..exceptions import TigerDataLabError
17
+
18
+
19
+ def main(argv: list[str] | None = None) -> int:
20
+ parser = argparse.ArgumentParser(prog="tigerdatalab", description="TigerDataLab CLI")
21
+ sub = parser.add_subparsers(dest="command", required=True)
22
+
23
+ p_analyze = sub.add_parser("analyze", help="Run full analysis and print summary")
24
+ p_analyze.add_argument("path")
25
+
26
+ p_dash = sub.add_parser("dashboard", help="Generate the interactive dashboard")
27
+ p_dash.add_argument("path")
28
+ p_dash.add_argument("-o", "--output", default="analysis/dashboard.html")
29
+
30
+ p_profile = sub.add_parser("profile", help="Print a data profile")
31
+ p_profile.add_argument("path")
32
+
33
+ p_quality = sub.add_parser("quality", help="Print a data quality report")
34
+ p_quality.add_argument("path")
35
+
36
+ p_clean = sub.add_parser("clean", help="Clean and export the dataset")
37
+ p_clean.add_argument("path")
38
+ p_clean.add_argument("-o", "--output", default="cleaned_data.xlsx")
39
+
40
+ p_report = sub.add_parser("report", help="Generate the full report bundle")
41
+ p_report.add_argument("path")
42
+ p_report.add_argument("-o", "--output", default="analysis")
43
+
44
+ args = parser.parse_args(argv)
45
+
46
+ try:
47
+ if args.command == "analyze":
48
+ core.analyze(args.path, verbose=True)
49
+ elif args.command == "dashboard":
50
+ result = core.analyze(args.path, verbose=False)
51
+ out = result.dashboard(args.output)
52
+ print(f"Dashboard written to: {out}")
53
+ elif args.command == "profile":
54
+ import json
55
+ print(json.dumps(core.profile(args.path), indent=2, default=str))
56
+ elif args.command == "quality":
57
+ import json
58
+ print(json.dumps(core.quality_check(args.path), indent=2, default=str))
59
+ elif args.command == "clean":
60
+ from ..reporting.exporters import save_cleaned_excel
61
+ df = core.clean_file(args.path)
62
+ out = save_cleaned_excel(args.output, df)
63
+ print(f"Cleaned data written to: {out}")
64
+ elif args.command == "report":
65
+ result = core.analyze(args.path, verbose=True)
66
+ outputs = result.report(args.output)
67
+ print("\nGenerated files:")
68
+ for k, v in outputs.items():
69
+ print(f" {k}: {v}")
70
+ return 0
71
+ except TigerDataLabError as e:
72
+ print(f"Error: {e}", file=sys.stderr)
73
+ return 1
74
+
75
+
76
+ if __name__ == "__main__":
77
+ sys.exit(main())
tigerdatalab/config.py ADDED
@@ -0,0 +1,47 @@
1
+ """Global configuration and constants for TigerDataLab."""
2
+ from __future__ import annotations
3
+
4
+ __version__ = "3.0.4"
5
+
6
+ SUPPORTED_EXTENSIONS = [".csv", ".xlsx", ".xlsm", ".json", ".parquet", ".sql", ".db", ".sqlite", ".duckdb"]
7
+
8
+ # Row-count thresholds used to decide small / medium / large execution strategy.
9
+ SMALL_DATA_ROW_LIMIT = 100_000
10
+ MEDIUM_DATA_ROW_LIMIT = 5_000_000
11
+
12
+ # SQL keywords that TigerDataLab will never auto-execute.
13
+ DESTRUCTIVE_SQL_KEYWORDS = ["DROP", "TRUNCATE", "DELETE", "ALTER"]
14
+
15
+ # Semantic keyword dictionaries used by the column-semantics detector.
16
+ # Each business concept maps to a list of substrings matched against
17
+ # lower-cased, underscore/space-normalized column names.
18
+ SEMANTIC_KEYWORDS: dict[str, list[str]] = {
19
+ "revenue": ["revenue", "sales_amount", "sales", "amount", "gmv", "turnover",
20
+ "net_sales", "total_sales", "grand_total", "line_total",
21
+ "sale_amount", "total_amount", "order_value", "order_total",
22
+ "net_amount", "gross_sales", "sales_value"],
23
+ "profit": ["profit", "net_profit", "gross_profit", "profit_amount", "margin_amount",
24
+ "net_income", "earnings", "profit_value"],
25
+ "cost": ["cost", "cogs", "cost_price", "purchase_cost", "unit_cost",
26
+ "cost_amount", "buying_price", "purchase_price", "cogs_amount"],
27
+ "quantity": ["quantity", "qty", "units", "units_sold", "unit_count",
28
+ "qty_sold", "no_of_units", "units_purchased", "order_qty"],
29
+ "customer": ["customer_id", "customer", "buyer_id", "buyer", "client_id", "user_id",
30
+ "cust_id", "member_id", "account_id", "customer_name", "client_name"],
31
+ "product": ["product_id", "product_name", "product", "item", "sku", "item_name", "name",
32
+ "product_title", "item_code", "product_code"],
33
+ "category": ["category", "segment", "department", "product_category", "product_type",
34
+ "sub_category", "subcategory", "product_group"],
35
+ "date": ["date", "order_date", "transaction_date", "created_at", "timestamp", "order_time",
36
+ "invoice_date", "order_dt", "txn_date", "purchase_date", "created_date", "datetime"],
37
+ "discount": ["discount_pct", "discount_percent", "discount", "disc_pct", "disc_percent"],
38
+ "order": ["order_id", "transaction_id", "invoice_id", "invoice_no", "order_no",
39
+ "order_number", "txn_id", "receipt_no", "receipt_id", "bill_no", "bill_id"],
40
+ "price": ["unit_price", "selling_price", "price", "rate", "mrp", "list_price",
41
+ "retail_price", "sale_price"],
42
+ }
43
+
44
+ # Keys checked in priority order (more specific first) to avoid, e.g.,
45
+ # "cost_price" being tagged as "price" instead of "cost".
46
+ SEMANTIC_PRIORITY = ["order", "customer", "product", "category", "date",
47
+ "discount", "cost", "profit", "revenue", "quantity", "price"]