tablofy 1.0.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.
tablofy/__init__.py ADDED
@@ -0,0 +1,28 @@
1
+ """Tablofy — A beginner-friendly data analytics library."""
2
+
3
+ __version__ = "1.0.0"
4
+
5
+ from tablofy.core.errors import (
6
+ TablofyColumnError,
7
+ TablofyDataError,
8
+ TablofyError,
9
+ TablofyFileError,
10
+ )
11
+ from tablofy.core.frame import TablofyFrame
12
+ from tablofy.core.loader import load
13
+
14
+ ColumnNotFoundError = TablofyColumnError
15
+ EmptyTableError = TablofyDataError
16
+ FileFormatError = TablofyFileError
17
+
18
+ __all__ = [
19
+ "TablofyFrame",
20
+ "load",
21
+ "TablofyError",
22
+ "TablofyFileError",
23
+ "TablofyColumnError",
24
+ "TablofyDataError",
25
+ "ColumnNotFoundError",
26
+ "EmptyTableError",
27
+ "FileFormatError",
28
+ ]
@@ -0,0 +1 @@
1
+ """Analytics: insights, statistics, outlier detection."""
@@ -0,0 +1,130 @@
1
+ """Rule-based insight generation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ if TYPE_CHECKING:
8
+ from tablofy.core.frame import TablofyFrame
9
+
10
+
11
+ class Insights:
12
+ """Generates deterministic, rule-based observations about a dataset."""
13
+
14
+ def __init__(self, frame: TablofyFrame) -> None:
15
+ self._frame = frame
16
+ self._df = frame._df
17
+
18
+ def generate(self) -> list[str]:
19
+ """Return a list of human-readable insight strings.
20
+
21
+ Checks performed
22
+ -----------------
23
+ - Dataset dimensions
24
+ - Missing values
25
+ - Duplicate rows
26
+ - Strong correlations
27
+ - High-cardinality columns
28
+ - Low-cardinality categorical columns
29
+ - Highest/lowest numeric values
30
+ - Outlier hints
31
+ """
32
+ insights: list[str] = []
33
+ df = self._df
34
+
35
+ rows, cols = df.shape
36
+ insights.append(f"Dataset has {rows} rows and {cols} columns.")
37
+
38
+ # Missing values
39
+ missing_counts = df.isna().sum()
40
+ total_missing = int(missing_counts.sum())
41
+ if total_missing:
42
+ pct = total_missing / df.size * 100
43
+ insights.append(
44
+ f"Found {total_missing} missing cell(s) ({pct:.1f}% of data)."
45
+ )
46
+ cols_with_missing = [c for c in df.columns if missing_counts[c] > 0]
47
+ insights.append(
48
+ f"Columns with missing values: {cols_with_missing}."
49
+ )
50
+ else:
51
+ insights.append("No missing values detected.")
52
+
53
+ # Duplicate rows
54
+ dup_count = int(df.duplicated().sum())
55
+ if dup_count:
56
+ dup_pct = dup_count / rows * 100
57
+ insights.append(
58
+ f"Found {dup_count} duplicate row(s) ({dup_pct:.1f}% of rows)."
59
+ )
60
+ else:
61
+ insights.append("No duplicate rows found.")
62
+
63
+ # Strong correlations
64
+ numeric = df.select_dtypes(include="number")
65
+ if numeric.shape[1] >= 2:
66
+ corr = numeric.corr().abs()
67
+ strong: list[str] = []
68
+ for i in range(len(corr.columns)):
69
+ for j in range(i + 1, len(corr.columns)):
70
+ val = corr.iloc[i, j]
71
+ if val >= 0.7:
72
+ strong.append(
73
+ f"{corr.columns[i]} — {corr.columns[j]} "
74
+ f"(r = {corr.iloc[i, j]:.2f})"
75
+ )
76
+ if strong:
77
+ insights.append(
78
+ f"Strong correlations detected ({len(strong)}): "
79
+ f"{'; '.join(strong)}."
80
+ )
81
+ else:
82
+ insights.append(
83
+ "No strong correlations (|r| >= 0.7) found between numeric columns."
84
+ )
85
+
86
+ # High-cardinality columns
87
+ high_card = [
88
+ c for c in df.columns
89
+ if df[c].nunique() > 50 and df[c].nunique() == len(df)
90
+ ]
91
+ if high_card:
92
+ insights.append(
93
+ f"High-cardinality columns (unique ID-like): {high_card}."
94
+ )
95
+
96
+ # Low-cardinality categorical columns
97
+ obj_cols = df.select_dtypes(include=["object", "category"]).columns
98
+ low_card = [c for c in obj_cols if 2 <= df[c].nunique() <= 10]
99
+ if low_card:
100
+ insights.append(
101
+ f"Low-cardinality categorical columns ({len(low_card)}): "
102
+ f"{low_card}."
103
+ )
104
+
105
+ # Highest / lowest numeric
106
+ if numeric.shape[1] >= 1:
107
+ for col in numeric.columns:
108
+ max_val = df[col].max()
109
+ min_val = df[col].min()
110
+ insights.append(
111
+ f"{col}: min = {min_val}, max = {max_val}."
112
+ )
113
+
114
+ # Outlier hints (IQR-based)
115
+ if numeric.shape[1] >= 1:
116
+ for col in numeric.columns:
117
+ q1 = df[col].quantile(0.25)
118
+ q3 = df[col].quantile(0.75)
119
+ iqr = q3 - q1
120
+ lower = q1 - 1.5 * iqr
121
+ upper = q3 + 1.5 * iqr
122
+ outlier_count = int(((df[col] < lower) | (df[col] > upper)).sum())
123
+ if outlier_count:
124
+ pct = outlier_count / rows * 100
125
+ insights.append(
126
+ f"{col}: {outlier_count} potential outlier(s) "
127
+ f"({pct:.1f}% of rows)."
128
+ )
129
+
130
+ return insights
@@ -0,0 +1,81 @@
1
+ """Outlier detection using IQR and z-score methods."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ import pandas as pd
8
+
9
+ from tablofy.core.errors import TablofyColumnError
10
+
11
+ if TYPE_CHECKING:
12
+ from tablofy.core.frame import TablofyFrame
13
+
14
+
15
+ class OutlierDetector:
16
+ """Detects outliers in numeric columns."""
17
+
18
+ def __init__(self, frame: TablofyFrame) -> None:
19
+ self._frame = frame
20
+ self._df = frame._df
21
+
22
+ def iqr_outliers(
23
+ self, column: str, multiplier: float = 1.5
24
+ ) -> pd.DataFrame:
25
+ """Return rows where *column* values are IQR outliers.
26
+
27
+ Parameters
28
+ ----------
29
+ column : str
30
+ Numeric column to check.
31
+ multiplier : float
32
+ IQR multiplier for the fence (default 1.5).
33
+
34
+ Returns
35
+ -------
36
+ pd.DataFrame
37
+ Rows where the column value is outside the IQR fences.
38
+ Includes a ``_outlier_distance`` column.
39
+ """
40
+ if column not in self._df.columns:
41
+ raise TablofyColumnError(
42
+ f"Column {column!r} not found. "
43
+ f"Available: {list(self._df.columns)}"
44
+ )
45
+ if not pd.api.types.is_numeric_dtype(self._df[column]):
46
+ raise TablofyColumnError(
47
+ f"Column {column!r} is not numeric."
48
+ )
49
+
50
+ q1 = self._df[column].quantile(0.25)
51
+ q3 = self._df[column].quantile(0.75)
52
+ iqr = q3 - q1
53
+ lower = q1 - multiplier * iqr
54
+ upper = q3 + multiplier * iqr
55
+ mask = (self._df[column] < lower) | (self._df[column] > upper)
56
+ result = self._df[mask].copy()
57
+ result["_outlier_distance"] = (self._df.loc[mask, column] - q3).abs()
58
+ return result
59
+
60
+ def zscore_outliers(
61
+ self, column: str, threshold: float = 3.0
62
+ ) -> pd.DataFrame:
63
+ """Return rows where *column* has a z-score beyond *threshold*."""
64
+ if column not in self._df.columns:
65
+ raise TablofyColumnError(
66
+ f"Column {column!r} not found. "
67
+ f"Available: {list(self._df.columns)}"
68
+ )
69
+ if not pd.api.types.is_numeric_dtype(self._df[column]):
70
+ raise TablofyColumnError(
71
+ f"Column {column!r} is not numeric."
72
+ )
73
+
74
+ mean = self._df[column].mean()
75
+ std = self._df[column].std()
76
+ if std == 0:
77
+ return self._df.iloc[:0].copy()
78
+ zscores = (self._df[column] - mean).abs() / std
79
+ result = self._df[zscores > threshold].copy()
80
+ result["_zscore"] = zscores[zscores > threshold]
81
+ return result
@@ -0,0 +1,115 @@
1
+ """Statistical helpers for TablofyFrame."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, Any
6
+
7
+ import pandas as pd
8
+
9
+ from tablofy.core.errors import TablofyColumnError
10
+
11
+ if TYPE_CHECKING:
12
+ from tablofy.core.frame import TablofyFrame
13
+
14
+
15
+ class Stats:
16
+ """Statistical operations on a TablofyFrame.
17
+
18
+ Accessed via ``data.stats``.
19
+ """
20
+
21
+ def __init__(self, frame: TablofyFrame) -> None:
22
+ self._frame = frame
23
+ self._df = frame._df
24
+
25
+ def _check_col(self, column: str) -> None:
26
+ if column not in self._df.columns:
27
+ raise TablofyColumnError(
28
+ f"Column {column!r} not found. "
29
+ f"Available: {list(self._df.columns)}"
30
+ )
31
+
32
+ def _check_numeric(self, column: str) -> None:
33
+ self._check_col(column)
34
+ if not pd.api.types.is_numeric_dtype(self._df[column]):
35
+ raise TablofyColumnError(
36
+ f"Column {column!r} is not numeric."
37
+ )
38
+
39
+ def describe(self, **kwargs: Any) -> pd.DataFrame:
40
+ """Return descriptive statistics for all columns."""
41
+ return self._df.describe(include="all", **kwargs)
42
+
43
+ def correlation(self, **kwargs: Any) -> pd.DataFrame:
44
+ """Return the correlation matrix of numeric columns."""
45
+ return self._df.select_dtypes(include="number").corr(**kwargs)
46
+
47
+ def covariance(self, **kwargs: Any) -> pd.DataFrame:
48
+ """Return the covariance matrix of numeric columns."""
49
+ return self._df.select_dtypes(include="number").cov(**kwargs)
50
+
51
+ def outliers(
52
+ self,
53
+ column: str,
54
+ multiplier: float = 1.5,
55
+ return_indices: bool = False,
56
+ ) -> pd.DataFrame | dict:
57
+ """Detect outliers in *column* using the IQR method.
58
+
59
+ Parameters
60
+ ----------
61
+ column : str
62
+ Numeric column to check.
63
+ multiplier : float
64
+ IQR multiplier for the fence (default 1.5).
65
+ return_indices : bool
66
+ If True, return a dict with ``count`` and ``indices``.
67
+
68
+ Returns
69
+ -------
70
+ pd.DataFrame or dict
71
+ """
72
+ from tablofy.analytics.outliers import OutlierDetector
73
+
74
+ result = OutlierDetector(self._frame).iqr_outliers(column, multiplier)
75
+ if return_indices:
76
+ return {
77
+ "count": len(result),
78
+ "indices": list(result.index),
79
+ }
80
+ return result
81
+
82
+ def mean(self, column: str) -> float:
83
+ """Return the mean of *column*."""
84
+ self._check_numeric(column)
85
+ return float(self._df[column].mean())
86
+
87
+ def median(self, column: str) -> float:
88
+ """Return the median of *column*."""
89
+ self._check_numeric(column)
90
+ return float(self._df[column].median())
91
+
92
+ def std(self, column: str) -> float:
93
+ """Return the standard deviation of *column*."""
94
+ self._check_numeric(column)
95
+ return float(self._df[column].std())
96
+
97
+ def min(self, column: str) -> float:
98
+ """Return the minimum of *column*."""
99
+ self._check_numeric(column)
100
+ return float(self._df[column].min())
101
+
102
+ def max(self, column: str) -> float:
103
+ """Return the maximum of *column*."""
104
+ self._check_numeric(column)
105
+ return float(self._df[column].max())
106
+
107
+ def quantile(self, column: str, q: float) -> float:
108
+ """Return the *q*-th quantile of *column*."""
109
+ self._check_numeric(column)
110
+ return float(self._df[column].quantile(q))
111
+
112
+ def value_counts(self, column: str) -> pd.Series:
113
+ """Return frequency counts for *column*."""
114
+ self._check_col(column)
115
+ return self._df[column].value_counts()
@@ -0,0 +1 @@
1
+ """Data cleaning: cleaner and cleaning report."""
@@ -0,0 +1,132 @@
1
+ """Data cleaning operations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, Any
6
+
7
+ import pandas as pd
8
+
9
+ from tablofy.utils.formatting import Formatter
10
+
11
+ if TYPE_CHECKING:
12
+ from tablofy.core.frame import TablofyFrame
13
+
14
+
15
+ class Cleaner:
16
+ """Performs cleaning operations on a TablofyFrame in-place."""
17
+
18
+ def __init__(self, frame: TablofyFrame) -> None:
19
+ self._frame = frame
20
+ self.actions: list[dict[str, Any]] = []
21
+
22
+ def run(
23
+ self,
24
+ duplicates: bool = True,
25
+ missing: str | dict = "smart",
26
+ columns: str | bool = "snake_case",
27
+ dates: bool = True,
28
+ text: bool = True,
29
+ ) -> TablofyFrame:
30
+ """Execute the cleaning pipeline and return the frame."""
31
+ self.actions = []
32
+ df = self._frame._df
33
+
34
+ # 1. Remove duplicate rows
35
+ if duplicates:
36
+ before = len(df)
37
+ df.drop_duplicates(inplace=True)
38
+ removed = before - len(df)
39
+ if removed:
40
+ self.actions.append({
41
+ "action": "duplicates",
42
+ "message": f"Removed {removed} duplicate row(s).",
43
+ })
44
+
45
+ # 2. Remove fully empty columns
46
+ empty_cols = [col for col in df.columns if df[col].isna().all()]
47
+ if empty_cols:
48
+ df.drop(columns=empty_cols, inplace=True)
49
+ self.actions.append({
50
+ "action": "empty_columns",
51
+ "message": f"Removed {len(empty_cols)} fully empty column(s): {empty_cols}.",
52
+ })
53
+
54
+ # 3. Clean column names to snake_case
55
+ if columns == "snake_case":
56
+ renamed = {}
57
+ for col in df.columns:
58
+ clean = Formatter.to_snake_case(col)
59
+ if clean != col:
60
+ renamed[col] = clean
61
+ if renamed:
62
+ df.rename(columns=renamed, inplace=True)
63
+ self.actions.append({
64
+ "action": "column_names",
65
+ "message": f"Renamed {len(renamed)} column(s) to snake_case.",
66
+ })
67
+
68
+ # 4. Strip whitespace from text columns
69
+ if text:
70
+ stripped_cols = []
71
+ text_cols = df.select_dtypes(include=["object"]).columns
72
+ for col in text_cols:
73
+ before_strip = df[col].astype(str).str.strip()
74
+ df[col] = df[col].astype(str).str.strip()
75
+ df[col] = df[col].replace(["nan", "None", ""], None)
76
+ if not df[col].equals(before_strip):
77
+ stripped_cols.append(col)
78
+ if stripped_cols:
79
+ self.actions.append({
80
+ "action": "whitespace",
81
+ "message": f"Stripped whitespace from {len(stripped_cols)} text column(s).",
82
+ })
83
+
84
+ # 5. Smart fill missing values
85
+ if missing == "smart":
86
+ filled_cols = []
87
+ for col in df.columns:
88
+ if df[col].isna().any():
89
+ if pd.api.types.is_numeric_dtype(df[col]):
90
+ median_val = df[col].median()
91
+ if pd.notna(median_val):
92
+ df[col] = df[col].fillna(median_val)
93
+ filled_cols.append(col)
94
+ else:
95
+ mode_vals = df[col].mode()
96
+ if len(mode_vals):
97
+ fill_val = mode_vals[0] if pd.notna(mode_vals[0]) else ""
98
+ df[col] = df[col].fillna(fill_val)
99
+ filled_cols.append(col)
100
+ if filled_cols:
101
+ self.actions.append({
102
+ "action": "missing_values",
103
+ "message": f"Smart-filled {len(filled_cols)} column(s): {filled_cols}.",
104
+ })
105
+ elif isinstance(missing, dict):
106
+ df.fillna(missing, inplace=True)
107
+ self.actions.append({
108
+ "action": "missing_values",
109
+ "message": f"Filled missing values using explicit mapping: {missing}.",
110
+ })
111
+
112
+ # 6. Date parsing
113
+ if dates:
114
+ parsed_cols = []
115
+ for col in df.select_dtypes(include=["object"]).columns:
116
+ sample = df[col].dropna().head(20)
117
+ if len(sample) < 2:
118
+ continue
119
+ try:
120
+ pd.to_datetime(sample, errors="raise")
121
+ df[col] = pd.to_datetime(df[col], errors="coerce")
122
+ parsed_cols.append(col)
123
+ except (ValueError, TypeError):
124
+ pass
125
+ if parsed_cols:
126
+ self.actions.append({
127
+ "action": "date_parsing",
128
+ "message": f"Parsed {len(parsed_cols)} date column(s): {parsed_cols}.",
129
+ })
130
+
131
+ self._frame._df = df
132
+ return self._frame
@@ -0,0 +1,29 @@
1
+ """Cleaning report generator."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ if TYPE_CHECKING:
8
+ from tablofy.core.frame import TablofyFrame
9
+
10
+
11
+ class CleanReport:
12
+ """Summarises the most recent cleaning run."""
13
+
14
+ def __init__(self, frame: TablofyFrame) -> None:
15
+ self._frame = frame
16
+
17
+ def summary(self) -> dict:
18
+ """Return a dict with actions, action_count, and summary_text."""
19
+ actions = self._frame._last_clean_actions
20
+ count = len(actions)
21
+ if count:
22
+ summary_text = f"Cleaning completed: {count} action(s) performed."
23
+ else:
24
+ summary_text = "No cleaning operations recorded."
25
+ return {
26
+ "actions": actions,
27
+ "action_count": count,
28
+ "summary_text": summary_text,
29
+ }
@@ -0,0 +1 @@
1
+ """Core components: frame, loader, errors, config."""
tablofy/core/config.py ADDED
@@ -0,0 +1,11 @@
1
+ """Package-level configuration constants."""
2
+
3
+ SUPPORTED_EXTENSIONS: frozenset[str] = frozenset({
4
+ ".csv",
5
+ ".xlsx",
6
+ ".xls",
7
+ ".json",
8
+ ".parquet",
9
+ })
10
+
11
+ REPORT_EXTENSIONS: frozenset[str] = frozenset({".html", ".xlsx"})
tablofy/core/errors.py ADDED
@@ -0,0 +1,25 @@
1
+ """Custom exception classes for Tablofy."""
2
+
3
+
4
+ class TablofyError(Exception):
5
+ """Base exception for all Tablofy errors."""
6
+
7
+
8
+ class TablofyFileError(TablofyError):
9
+ """Raised when a file operation fails (not found, unsupported format)."""
10
+
11
+
12
+ class TablofyColumnError(TablofyError):
13
+ """Raised when a requested column does not exist."""
14
+
15
+
16
+ class TablofyDataError(TablofyError):
17
+ """Raised on invalid data (empty dataset, coercion failure)."""
18
+
19
+
20
+ class TablofyChartError(TablofyError):
21
+ """Raised when a chart operation is invalid."""
22
+
23
+
24
+ class TablofyParseError(TablofyError):
25
+ """Raised when parsing a user string (e.g. chart description) fails."""