plotlibs 0.3.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.
- plotlib/__init__.py +38 -0
- plotlibs/__init__.py +110 -0
- plotlibs/backends/__init__.py +0 -0
- plotlibs/backends/renderer.py +520 -0
- plotlibs/colors.py +107 -0
- plotlibs/data.py +208 -0
- plotlibs/db.py +204 -0
- plotlibs/eda.py +157 -0
- plotlibs/fast.py +187 -0
- plotlibs/figure.py +617 -0
- plotlibs/ml.py +205 -0
- plotlibs/pyplot.py +283 -0
- plotlibs/style.py +96 -0
- plotlibs-0.3.0.dist-info/METADATA +228 -0
- plotlibs-0.3.0.dist-info/RECORD +18 -0
- plotlibs-0.3.0.dist-info/WHEEL +5 -0
- plotlibs-0.3.0.dist-info/licenses/LICENSE +21 -0
- plotlibs-0.3.0.dist-info/top_level.txt +2 -0
plotlibs/colors.py
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""plotlibs.colors — fast matplotlib-compatible color parsing."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
TABLEAU = {
|
|
5
|
+
"C0": (31, 119, 180),
|
|
6
|
+
"C1": (255, 127, 14),
|
|
7
|
+
"C2": (44, 160, 44),
|
|
8
|
+
"C3": (214, 39, 40),
|
|
9
|
+
"C4": (148, 103, 189),
|
|
10
|
+
"C5": (140, 86, 75),
|
|
11
|
+
"C6": (227, 119, 194),
|
|
12
|
+
"C7": (127, 127, 127),
|
|
13
|
+
"C8": (188, 189, 34),
|
|
14
|
+
"C9": (23, 190, 207),
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
NAMED = {
|
|
18
|
+
"b": (0, 0, 255), "g": (0, 128, 0), "r": (255, 0, 0),
|
|
19
|
+
"c": (0, 191, 191), "m": (191, 0, 191), "y": (191, 191, 0),
|
|
20
|
+
"k": (0, 0, 0), "w": (255, 255, 255),
|
|
21
|
+
"blue": (0, 0, 255), "green": (0, 128, 0), "red": (255, 0, 0),
|
|
22
|
+
"cyan": (0, 191, 191), "magenta": (191, 0, 191), "yellow": (255, 255, 0),
|
|
23
|
+
"black": (0, 0, 0), "white": (255, 255, 255),
|
|
24
|
+
"gray": (128, 128, 128), "grey": (128, 128, 128),
|
|
25
|
+
"orange": (255, 165, 0), "purple": (128, 0, 128),
|
|
26
|
+
"brown": (165, 42, 42), "pink": (255, 192, 203),
|
|
27
|
+
"lime": (0, 255, 0), "navy": (0, 0, 128), "teal": (0, 128, 128),
|
|
28
|
+
"lightgray": (211, 211, 211), "lightgrey": (211, 211, 211),
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
BRAND = {
|
|
32
|
+
# plotlibs visual identity v1 (see assets/logo.svg)
|
|
33
|
+
"primary": (79, 70, 229), # #4F46E5 indigo
|
|
34
|
+
"accent": (6, 182, 212), # #06B6D4 cyan
|
|
35
|
+
"ink": (15, 23, 42), # #0F172A
|
|
36
|
+
"paper": (248, 250, 252), # #F8FAFC
|
|
37
|
+
"lime": (163, 230, 53), # #A3E635
|
|
38
|
+
"amber": (250, 204, 21), # #FACC15
|
|
39
|
+
"coral": (251, 113, 133), # #FB7185
|
|
40
|
+
"violet": (167, 139, 250), # #A78BFA
|
|
41
|
+
"sky": (56, 189, 248), # #38BDF8
|
|
42
|
+
"slate": (100, 116, 139), # #64748B
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
CYCLE = [
|
|
46
|
+
BRAND["primary"],
|
|
47
|
+
BRAND["accent"],
|
|
48
|
+
BRAND["coral"],
|
|
49
|
+
(34, 197, 94),
|
|
50
|
+
BRAND["amber"],
|
|
51
|
+
BRAND["violet"],
|
|
52
|
+
BRAND["sky"],
|
|
53
|
+
BRAND["slate"],
|
|
54
|
+
(244, 114, 182),
|
|
55
|
+
(45, 212, 191),
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def to_rgb(color) -> tuple[int, int, int]:
|
|
60
|
+
"""Parse any matplotlib-like color spec to (r, g, b) 0-255."""
|
|
61
|
+
if color is None:
|
|
62
|
+
return BRAND["primary"]
|
|
63
|
+
if isinstance(color, (tuple, list)):
|
|
64
|
+
vals = list(color)
|
|
65
|
+
if len(vals) in (3, 4):
|
|
66
|
+
# float 0-1 or int 0-255 ?
|
|
67
|
+
if all(isinstance(v, float) or (isinstance(v, int) and False) for v in vals):
|
|
68
|
+
pass
|
|
69
|
+
mx = max(vals[:3])
|
|
70
|
+
if mx <= 1.0 and any(isinstance(v, float) for v in vals):
|
|
71
|
+
return tuple(int(max(0, min(1, v)) * 255) for v in vals[:3])
|
|
72
|
+
return tuple(int(max(0, min(255, v))) for v in vals[:3])
|
|
73
|
+
raise ValueError(f"Bad color tuple {color!r}")
|
|
74
|
+
if not isinstance(color, str):
|
|
75
|
+
raise ValueError(f"Bad color {color!r}")
|
|
76
|
+
c = color.strip()
|
|
77
|
+
if c in TABLEAU:
|
|
78
|
+
return TABLEAU[c]
|
|
79
|
+
if c in NAMED:
|
|
80
|
+
return NAMED[c]
|
|
81
|
+
if c.startswith("#"):
|
|
82
|
+
h = c[1:]
|
|
83
|
+
if len(h) == 3:
|
|
84
|
+
h = "".join(ch * 2 for ch in h)
|
|
85
|
+
if len(h) == 6:
|
|
86
|
+
return (int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16))
|
|
87
|
+
if len(h) == 8: # #rrggbbaa -> ignore alpha
|
|
88
|
+
return (int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16))
|
|
89
|
+
# gray string like "0.5"
|
|
90
|
+
try:
|
|
91
|
+
f = float(c)
|
|
92
|
+
v = int(max(0.0, min(1.0, f)) * 255)
|
|
93
|
+
return (v, v, v)
|
|
94
|
+
except ValueError:
|
|
95
|
+
pass
|
|
96
|
+
raise ValueError(f"Unknown color {color!r}")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def to_rgba(color, alpha: float | None = None) -> tuple[int, int, int, int]:
|
|
100
|
+
r, g, b = to_rgb(color)
|
|
101
|
+
a = 255
|
|
102
|
+
if isinstance(color, (tuple, list)) and len(color) == 4:
|
|
103
|
+
v = color[3]
|
|
104
|
+
a = int(v * 255) if isinstance(v, float) and v <= 1.0 else int(v)
|
|
105
|
+
if alpha is not None:
|
|
106
|
+
a = int(max(0.0, min(1.0, float(alpha))) * 255)
|
|
107
|
+
return (r, g, b, a)
|
plotlibs/data.py
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"""plotlibs.data — جسر البيانات: pandas / numpy / csv بدون اعتماديات إجبارية.
|
|
2
|
+
|
|
3
|
+
الهدف: محلل البيانات يكتب سطرين فقط:
|
|
4
|
+
import plotlibs as pl
|
|
5
|
+
df = pl.load_csv("sales.csv")
|
|
6
|
+
pl.quick_eda(df)
|
|
7
|
+
|
|
8
|
+
كل الدوال تعمل مع:
|
|
9
|
+
- pandas.DataFrame (إن وُجدت pandas)
|
|
10
|
+
- dict of columns
|
|
11
|
+
- numpy 2D array
|
|
12
|
+
- list of dicts (rows)
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import csv
|
|
17
|
+
import math
|
|
18
|
+
|
|
19
|
+
import numpy as np
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _has_pandas() -> bool:
|
|
23
|
+
try:
|
|
24
|
+
import pandas # noqa: F401
|
|
25
|
+
return True
|
|
26
|
+
except Exception:
|
|
27
|
+
return False
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def to_frame(data):
|
|
31
|
+
"""حوّل أي مصدر شائع إلى pandas.DataFrame إن أمكن، وإلا dict."""
|
|
32
|
+
if data is None:
|
|
33
|
+
raise ValueError("data is None")
|
|
34
|
+
if _has_pandas():
|
|
35
|
+
import pandas as pd
|
|
36
|
+
if isinstance(data, pd.DataFrame):
|
|
37
|
+
return data
|
|
38
|
+
if isinstance(data, pd.Series):
|
|
39
|
+
return data.to_frame()
|
|
40
|
+
if isinstance(data, dict):
|
|
41
|
+
return pd.DataFrame(data)
|
|
42
|
+
if isinstance(data, (list, tuple)) and data and isinstance(data[0], dict):
|
|
43
|
+
return pd.DataFrame(data)
|
|
44
|
+
arr = np.asarray(data)
|
|
45
|
+
if arr.ndim == 2:
|
|
46
|
+
return pd.DataFrame(arr, columns=[f"c{i}" for i in range(arr.shape[1])])
|
|
47
|
+
return pd.DataFrame({"value": np.asarray(data).ravel()})
|
|
48
|
+
# بدون pandas: أعد dict من الأعمدة
|
|
49
|
+
if isinstance(data, dict):
|
|
50
|
+
return {k: np.asarray(v) for k, v in data.items()}
|
|
51
|
+
arr = np.asarray(data)
|
|
52
|
+
if arr.ndim == 2:
|
|
53
|
+
return {f"c{i}": arr[:, i] for i in range(arr.shape[1])}
|
|
54
|
+
return {"value": np.asarray(arr).ravel()}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def columns_of(data) -> list[str]:
|
|
58
|
+
if _has_pandas():
|
|
59
|
+
import pandas as pd
|
|
60
|
+
if isinstance(data, pd.DataFrame):
|
|
61
|
+
return list(data.columns.astype(str))
|
|
62
|
+
if isinstance(data, dict):
|
|
63
|
+
return list(data.keys())
|
|
64
|
+
return []
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def column_values(data, col):
|
|
68
|
+
"""أعد عموداً كـ numpy array نظيف."""
|
|
69
|
+
if _has_pandas():
|
|
70
|
+
import pandas as pd
|
|
71
|
+
if isinstance(data, pd.DataFrame):
|
|
72
|
+
return pd.to_numeric(data[col], errors="coerce").to_numpy(dtype=float)
|
|
73
|
+
if isinstance(data, dict):
|
|
74
|
+
return np.asarray(data[col], dtype=float).ravel()
|
|
75
|
+
raise KeyError(f"unknown column {col!r}")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def numeric_columns(data) -> list[str]:
|
|
79
|
+
if isinstance(data, dict):
|
|
80
|
+
out = []
|
|
81
|
+
for k, v in data.items():
|
|
82
|
+
try:
|
|
83
|
+
arr = np.asarray(v)
|
|
84
|
+
if arr.dtype.kind in "iufb":
|
|
85
|
+
out.append(k)
|
|
86
|
+
continue
|
|
87
|
+
# حاول تحويل عينة
|
|
88
|
+
np.asarray(v, dtype=float)
|
|
89
|
+
out.append(k)
|
|
90
|
+
except Exception:
|
|
91
|
+
pass
|
|
92
|
+
return out
|
|
93
|
+
if _has_pandas():
|
|
94
|
+
import pandas as pd
|
|
95
|
+
if isinstance(data, pd.DataFrame):
|
|
96
|
+
return [str(c) for c in data.select_dtypes(include=[np.number]).columns]
|
|
97
|
+
return []
|
|
98
|
+
return []
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def load_csv(path, **kw):
|
|
102
|
+
"""قراءة CSV سريعة — pandas إن وُجدت وإلا csv stdlib."""
|
|
103
|
+
if _has_pandas():
|
|
104
|
+
import pandas as pd
|
|
105
|
+
return pd.read_csv(path, **kw)
|
|
106
|
+
with open(path, newline="", encoding=kw.get("encoding", "utf-8")) as f:
|
|
107
|
+
reader = csv.DictReader(f)
|
|
108
|
+
cols: dict[str, list] = {}
|
|
109
|
+
for row in reader:
|
|
110
|
+
for k, v in row.items():
|
|
111
|
+
cols.setdefault(k, []).append(v)
|
|
112
|
+
# حاول التحويل لرقمي
|
|
113
|
+
out: dict[str, np.ndarray] = {}
|
|
114
|
+
for k, vals in cols.items():
|
|
115
|
+
try:
|
|
116
|
+
out[k] = np.array(vals, dtype=float)
|
|
117
|
+
except Exception:
|
|
118
|
+
out[k] = np.array(vals)
|
|
119
|
+
return out
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def save_csv(data, path, **kw):
|
|
123
|
+
if _has_pandas():
|
|
124
|
+
import pandas as pd
|
|
125
|
+
if isinstance(data, pd.DataFrame):
|
|
126
|
+
data.to_csv(path, index=kw.get("index", False))
|
|
127
|
+
return path
|
|
128
|
+
if isinstance(data, dict):
|
|
129
|
+
keys = list(data.keys())
|
|
130
|
+
n = max(len(np.asarray(data[k]).ravel()) for k in keys)
|
|
131
|
+
with open(path, "w", newline="", encoding="utf-8") as f:
|
|
132
|
+
w = csv.writer(f)
|
|
133
|
+
w.writerow(keys)
|
|
134
|
+
for i in range(n):
|
|
135
|
+
row = []
|
|
136
|
+
for k in keys:
|
|
137
|
+
col = np.asarray(data[k]).ravel()
|
|
138
|
+
row.append(col[i] if i < len(col) else "")
|
|
139
|
+
w.writerow(row)
|
|
140
|
+
return path
|
|
141
|
+
raise ValueError("unsupported data type for save_csv")
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def describe(data) -> dict:
|
|
145
|
+
"""إحصاءات وصفية سريعة (count/mean/std/min/25/50/75/max) لكل عمود رقمي."""
|
|
146
|
+
result: dict[str, dict[str, float]] = {}
|
|
147
|
+
for col in numeric_columns(data):
|
|
148
|
+
try:
|
|
149
|
+
v = column_values(data, col)
|
|
150
|
+
except Exception:
|
|
151
|
+
continue
|
|
152
|
+
v = v[np.isfinite(v)]
|
|
153
|
+
if v.size == 0:
|
|
154
|
+
continue
|
|
155
|
+
q = np.quantile(v, [0.0, 0.25, 0.5, 0.75, 1.0])
|
|
156
|
+
result[col] = {
|
|
157
|
+
"count": float(v.size),
|
|
158
|
+
"mean": float(v.mean()),
|
|
159
|
+
"std": float(v.std()),
|
|
160
|
+
"min": float(q[0]),
|
|
161
|
+
"25%": float(q[1]),
|
|
162
|
+
"50%": float(q[2]),
|
|
163
|
+
"75%": float(q[3]),
|
|
164
|
+
"max": float(q[4]),
|
|
165
|
+
}
|
|
166
|
+
return result
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def corr_matrix(data, cols=None) -> tuple[np.ndarray, list[str]]:
|
|
170
|
+
"""مصفوفة ارتباط Pearson بـ numpy فقط."""
|
|
171
|
+
cols = cols or numeric_columns(data)
|
|
172
|
+
if len(cols) < 2:
|
|
173
|
+
raise ValueError("need >= 2 numeric columns for correlation")
|
|
174
|
+
mat = np.column_stack([column_values(data, c) for c in cols])
|
|
175
|
+
# احذف الصفوف التي فيها NaN
|
|
176
|
+
mask = np.all(np.isfinite(mat), axis=1)
|
|
177
|
+
mat = mat[mask]
|
|
178
|
+
if mat.shape[0] < 2:
|
|
179
|
+
return np.eye(len(cols)), cols
|
|
180
|
+
c = np.corrcoef(mat, rowvar=False)
|
|
181
|
+
c = np.nan_to_num(c, nan=0.0)
|
|
182
|
+
return c, cols
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def sample_rows(data, n: int = 5):
|
|
186
|
+
if _has_pandas():
|
|
187
|
+
import pandas as pd
|
|
188
|
+
if isinstance(data, pd.DataFrame):
|
|
189
|
+
return data.head(n)
|
|
190
|
+
if isinstance(data, dict):
|
|
191
|
+
keys = list(data.keys())
|
|
192
|
+
return {k: np.asarray(data[k]).ravel()[:n] for k in keys}
|
|
193
|
+
return data
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def train_test_split_simple(*arrays, test_size=0.2, seed=0):
|
|
197
|
+
"""تقسيم سريع بدون sklearn (للمبتدئين)."""
|
|
198
|
+
rng = np.random.default_rng(seed)
|
|
199
|
+
n = len(np.asarray(arrays[0]))
|
|
200
|
+
idx = np.arange(n)
|
|
201
|
+
rng.shuffle(idx)
|
|
202
|
+
k = int(math.ceil(n * test_size))
|
|
203
|
+
te, tr = idx[:k], idx[k:]
|
|
204
|
+
out = []
|
|
205
|
+
for a in arrays:
|
|
206
|
+
a = np.asarray(a)
|
|
207
|
+
out += [a[tr], a[te]]
|
|
208
|
+
return out
|
plotlibs/db.py
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""plotlibs.db — مهام قواعد البيانات في سطرين.
|
|
2
|
+
|
|
3
|
+
import plotlibs as pl
|
|
4
|
+
conn = pl.db_connect("sales.db")
|
|
5
|
+
df = pl.read_sql("SELECT * FROM sales LIMIT 1000", conn)
|
|
6
|
+
pl.quick_eda(df)
|
|
7
|
+
|
|
8
|
+
يدعم:
|
|
9
|
+
- sqlite3 من المكتبة القياسية (بدون أي تنصيب)
|
|
10
|
+
- sqlalchemy / duckdb إن وُجدت (URLs مثل sqlite:///x.db أو duckdb:///x.ddb)
|
|
11
|
+
- كتابة DataFrame إلى جدول + إنشاء جداول تجريبية + رسم مباشر من SQL
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import sqlite3
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _is_duckdb_url(url: str) -> bool:
|
|
20
|
+
return url.startswith("duckdb:")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _is_sqla_url(url: str) -> bool:
|
|
24
|
+
return "://" in url and not _is_duckdb_url(url) and url.split("://")[0] not in ("sqlite",)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def connect(url_or_path: str = ":memory:"):
|
|
28
|
+
"""اتصال ذكي: مسار sqlite أو :memory: أو URL.
|
|
29
|
+
|
|
30
|
+
يعيد كائن اتصال sqlite3، أو sqlalchemy Connection، أو duckdb connection.
|
|
31
|
+
"""
|
|
32
|
+
s = str(url_or_path)
|
|
33
|
+
if _is_duckdb_url(s):
|
|
34
|
+
try:
|
|
35
|
+
import duckdb # type: ignore
|
|
36
|
+
except ImportError as e:
|
|
37
|
+
raise ImportError("duckdb not installed: pip install duckdb") from e
|
|
38
|
+
path = s.replace("duckdb://", "").replace("duckdb:", "") or ":memory:"
|
|
39
|
+
return duckdb.connect(path if path else ":memory:")
|
|
40
|
+
if s.startswith("sqlite:///"):
|
|
41
|
+
return sqlite3.connect(s.replace("sqlite:///", ""))
|
|
42
|
+
if "://" in s:
|
|
43
|
+
# sqlalchemy generic (postgres/mysql/...)
|
|
44
|
+
try:
|
|
45
|
+
from sqlalchemy import create_engine # type: ignore
|
|
46
|
+
except ImportError as e:
|
|
47
|
+
raise ImportError("sqlalchemy not installed: pip install sqlalchemy") from e
|
|
48
|
+
eng = create_engine(s)
|
|
49
|
+
return eng.connect()
|
|
50
|
+
# مسار ملف أو :memory:
|
|
51
|
+
return sqlite3.connect(s)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
db_connect = connect
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def read_sql(query: str, conn):
|
|
58
|
+
"""نفّذ SELECT وأعد DataFrame (أو dict إن لم توجد pandas)."""
|
|
59
|
+
# duckdb
|
|
60
|
+
try:
|
|
61
|
+
import duckdb # type: ignore
|
|
62
|
+
if isinstance(conn, duckdb.DuckDBPyConnection):
|
|
63
|
+
return conn.execute(query).fetchdf()
|
|
64
|
+
except Exception:
|
|
65
|
+
pass
|
|
66
|
+
# sqlalchemy
|
|
67
|
+
try:
|
|
68
|
+
from sqlalchemy.engine import Connection as _SAConn # type: ignore
|
|
69
|
+
if isinstance(conn, _SAConn):
|
|
70
|
+
import pandas as pd
|
|
71
|
+
return pd.read_sql(query, conn)
|
|
72
|
+
except Exception:
|
|
73
|
+
pass
|
|
74
|
+
# sqlite3
|
|
75
|
+
try:
|
|
76
|
+
import pandas as pd
|
|
77
|
+
return pd.read_sql_query(query, conn)
|
|
78
|
+
except ImportError:
|
|
79
|
+
cur = conn.execute(query)
|
|
80
|
+
cols = [d[0] for d in cur.description]
|
|
81
|
+
rows = cur.fetchall()
|
|
82
|
+
import numpy as np
|
|
83
|
+
out: dict = {}
|
|
84
|
+
for i, c in enumerate(cols):
|
|
85
|
+
col = [r[i] for r in rows]
|
|
86
|
+
try:
|
|
87
|
+
out[c] = np.array(col, dtype=float)
|
|
88
|
+
except Exception:
|
|
89
|
+
out[c] = np.array(col)
|
|
90
|
+
return out
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def to_sql(data, table: str, conn, if_exists: str = "replace"):
|
|
94
|
+
"""اكتب DataFrame/dict إلى جدول SQL."""
|
|
95
|
+
try:
|
|
96
|
+
import pandas as pd
|
|
97
|
+
if isinstance(data, pd.DataFrame):
|
|
98
|
+
# sqlalchemy conn أم sqlite3؟
|
|
99
|
+
try:
|
|
100
|
+
from sqlalchemy.engine import Connection as _SAConn # type: ignore
|
|
101
|
+
if isinstance(conn, _SAConn):
|
|
102
|
+
data.to_sql(table, conn, if_exists=if_exists, index=False)
|
|
103
|
+
return table
|
|
104
|
+
except Exception:
|
|
105
|
+
pass
|
|
106
|
+
data.to_sql(table, conn, if_exists=if_exists, index=False)
|
|
107
|
+
return table
|
|
108
|
+
except ImportError:
|
|
109
|
+
pass
|
|
110
|
+
# dict fallback عبر sqlite3
|
|
111
|
+
import numpy as np
|
|
112
|
+
if isinstance(data, dict) and hasattr(conn, "execute"):
|
|
113
|
+
keys = list(data.keys())
|
|
114
|
+
n = max(len(np.asarray(data[k]).ravel()) for k in keys)
|
|
115
|
+
if if_exists == "replace":
|
|
116
|
+
try:
|
|
117
|
+
conn.execute(f'DROP TABLE IF EXISTS "{table}"')
|
|
118
|
+
except Exception:
|
|
119
|
+
pass
|
|
120
|
+
coldefs = ", ".join(f'"{k}" TEXT' for k in keys)
|
|
121
|
+
conn.execute(f'CREATE TABLE IF NOT EXISTS "{table}" ({coldefs})')
|
|
122
|
+
for i in range(n):
|
|
123
|
+
vals = []
|
|
124
|
+
for k in keys:
|
|
125
|
+
col = np.asarray(data[k]).ravel()
|
|
126
|
+
vals.append(str(col[i]) if i < len(col) else None)
|
|
127
|
+
conn.execute(
|
|
128
|
+
f'INSERT INTO "{table}" VALUES ({",".join("?" for _ in vals)})', vals
|
|
129
|
+
)
|
|
130
|
+
conn.commit()
|
|
131
|
+
return table
|
|
132
|
+
raise ValueError("unsupported to_sql input")
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def list_tables(conn) -> list[str]:
|
|
136
|
+
try:
|
|
137
|
+
import duckdb # type: ignore
|
|
138
|
+
if isinstance(conn, duckdb.DuckDBPyConnection):
|
|
139
|
+
rows = conn.execute("SHOW TABLES").fetchall()
|
|
140
|
+
return [r[0] for r in rows]
|
|
141
|
+
except Exception:
|
|
142
|
+
pass
|
|
143
|
+
try:
|
|
144
|
+
cur = conn.execute(
|
|
145
|
+
"SELECT name FROM sqlite_master WHERE type='table' ORDER BY name"
|
|
146
|
+
)
|
|
147
|
+
return [r[0] for r in cur.fetchall()]
|
|
148
|
+
except Exception:
|
|
149
|
+
pass
|
|
150
|
+
try:
|
|
151
|
+
rows = conn.execute(
|
|
152
|
+
"SELECT tablename FROM pg_tables WHERE schemaname='public'"
|
|
153
|
+
).fetchall()
|
|
154
|
+
return [r[0] for r in rows]
|
|
155
|
+
except Exception:
|
|
156
|
+
return []
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def demo_db(path: str = ":memory:"):
|
|
160
|
+
"""أنشئ قاعدة تجريبية sales(region, amount, qty) للتعليم والعروض."""
|
|
161
|
+
import numpy as np
|
|
162
|
+
conn = connect(path)
|
|
163
|
+
rng = np.random.default_rng(7)
|
|
164
|
+
regions = rng.choice(["Cairo", "Oran", "Riyadh", "Dubai"], size=300)
|
|
165
|
+
amounts = rng.gamma(2.0, 150.0, size=300).round(2)
|
|
166
|
+
qty = rng.integers(1, 20, size=300)
|
|
167
|
+
try:
|
|
168
|
+
import pandas as pd
|
|
169
|
+
df = pd.DataFrame({"region": regions, "amount": amounts, "qty": qty})
|
|
170
|
+
except ImportError:
|
|
171
|
+
df = {"region": regions, "amount": amounts, "qty": qty}
|
|
172
|
+
to_sql(df, "sales", conn, if_exists="replace")
|
|
173
|
+
return conn
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def plot_from_sql(query: str, conn, kind: str = "line", **kw):
|
|
177
|
+
"""ارسم مباشرة من استعلام SQL: SELECT x, y FROM t."""
|
|
178
|
+
from . import pyplot as plt
|
|
179
|
+
from .data import numeric_columns, column_values, columns_of
|
|
180
|
+
|
|
181
|
+
df = read_sql(query, conn)
|
|
182
|
+
cols = columns_of(df)
|
|
183
|
+
nums = numeric_columns(df)
|
|
184
|
+
if len(nums) >= 2:
|
|
185
|
+
x = column_values(df, nums[0])
|
|
186
|
+
y = column_values(df, nums[1])
|
|
187
|
+
if kind == "bar":
|
|
188
|
+
plt.bar(x, y, **kw)
|
|
189
|
+
elif kind == "scatter":
|
|
190
|
+
plt.scatter(x, y, **kw)
|
|
191
|
+
else:
|
|
192
|
+
plt.plot(x, y, **kw)
|
|
193
|
+
plt.xlabel(nums[0])
|
|
194
|
+
plt.ylabel(nums[1])
|
|
195
|
+
plt.title(query[:60])
|
|
196
|
+
elif cols:
|
|
197
|
+
# تجميع فئوي: SELECT region, SUM(amount)
|
|
198
|
+
try:
|
|
199
|
+
import pandas as _pd # noqa
|
|
200
|
+
vals = df
|
|
201
|
+
except Exception:
|
|
202
|
+
vals = df
|
|
203
|
+
_ = vals
|
|
204
|
+
return df
|
plotlibs/eda.py
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""plotlibs.eda — تحليل استكشافي بسطر واحد للمحللين و Data Science.
|
|
2
|
+
|
|
3
|
+
import plotlibs as pl
|
|
4
|
+
pl.quick_eda(df) # شكل 2x2: توزيع + box + ارتباط + قيم مفقودة
|
|
5
|
+
pl.scatter_matrix(df) # مصفوفة انتشار
|
|
6
|
+
pl.plot_missing(df) # خريطة القيم المفقودة
|
|
7
|
+
pl.plot_trend(df, "date", "sales") # خط + متوسط متحرك
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _get_pyplot():
|
|
15
|
+
from . import pyplot as plt
|
|
16
|
+
return plt
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def quick_eda(data, max_cols: int = 6, savefig=None, title: str = "Quick EDA"):
|
|
20
|
+
"""تقرير بصري سريع 2x2. يعيد Figure."""
|
|
21
|
+
from .data import numeric_columns, column_values, describe
|
|
22
|
+
plt = _get_pyplot()
|
|
23
|
+
nums = numeric_columns(data)[:max_cols]
|
|
24
|
+
if not nums:
|
|
25
|
+
raise ValueError("quick_eda needs at least 1 numeric column")
|
|
26
|
+
fig, axs = plt.subplots(2, 2, figsize=(10, 7))
|
|
27
|
+
fig.suptitle(title)
|
|
28
|
+
# 1) توزيع أول عمود
|
|
29
|
+
v0 = column_values(data, nums[0])
|
|
30
|
+
v0 = v0[np.isfinite(v0)]
|
|
31
|
+
axs[0, 0].hist(v0, bins=30)
|
|
32
|
+
axs[0, 0].kde(v0, label="KDE")
|
|
33
|
+
axs[0, 0].set_title(f"dist: {nums[0]}")
|
|
34
|
+
# 2) boxplot كل الأعمدة
|
|
35
|
+
cols = [column_values(data, c) for c in nums]
|
|
36
|
+
axs[0, 1].boxplot(cols, labels=nums)
|
|
37
|
+
axs[0, 1].set_title("boxplot")
|
|
38
|
+
# 3) ارتباط
|
|
39
|
+
try:
|
|
40
|
+
axs[1, 0].corr(data, cols=nums, annot=len(nums) <= 8)
|
|
41
|
+
axs[1, 0].set_title("correlation")
|
|
42
|
+
except Exception as e:
|
|
43
|
+
axs[1, 0].set_title(f"corr N/A: {e}")
|
|
44
|
+
# 4) نص إحصاءات وصفية
|
|
45
|
+
desc = describe(data)
|
|
46
|
+
lines = []
|
|
47
|
+
for c in nums[:4]:
|
|
48
|
+
d = desc.get(c, {})
|
|
49
|
+
if d:
|
|
50
|
+
lines.append(f"{c}: μ={d['mean']:.2f} σ={d['std']:.2f} med={d['50%']:.2f}")
|
|
51
|
+
axs[1, 1].plot([0, 1], [0, 0], label="baseline")
|
|
52
|
+
axs[1, 1].set_title("summary")
|
|
53
|
+
axs[1, 1].text(0.02, 0.9, "\n".join(lines) or "no stats", fontsize=9, color="black")
|
|
54
|
+
# حفظ اختياري
|
|
55
|
+
if savefig:
|
|
56
|
+
fig.savefig(savefig)
|
|
57
|
+
_ = desc
|
|
58
|
+
return fig
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def scatter_matrix(data, cols=None, figsize=(9, 9), **kw):
|
|
62
|
+
"""مصفوفة انتشار n×n (بديل pandas.plotting.scatter_matrix) — سريعة."""
|
|
63
|
+
from .data import numeric_columns, column_values
|
|
64
|
+
plt = _get_pyplot()
|
|
65
|
+
cols = cols or numeric_columns(data)[:5]
|
|
66
|
+
n = len(cols)
|
|
67
|
+
if n < 2:
|
|
68
|
+
raise ValueError("need >= 2 numeric columns")
|
|
69
|
+
fig, axs = plt.subplots(n, n, figsize=figsize)
|
|
70
|
+
if n == 1:
|
|
71
|
+
axs = np.array([[axs]])
|
|
72
|
+
for i, ci in enumerate(cols):
|
|
73
|
+
for j, cj in enumerate(cols):
|
|
74
|
+
ax = axs[i, j]
|
|
75
|
+
x = column_values(data, cj)
|
|
76
|
+
y = column_values(data, ci)
|
|
77
|
+
m = np.isfinite(x) & np.isfinite(y)
|
|
78
|
+
x, y = x[m], y[m]
|
|
79
|
+
if i == j:
|
|
80
|
+
ax.hist(x, bins=20)
|
|
81
|
+
else:
|
|
82
|
+
ax.scatter(x[:5000], y[:5000])
|
|
83
|
+
if j == 0:
|
|
84
|
+
ax.set_ylabel(ci)
|
|
85
|
+
if i == n - 1:
|
|
86
|
+
ax.set_xlabel(cj)
|
|
87
|
+
fig.suptitle("scatter matrix")
|
|
88
|
+
return fig
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def plot_missing(data, **kw):
|
|
92
|
+
"""شريط القيم المفقودة لكل عمود (بديل missingno)."""
|
|
93
|
+
plt = _get_pyplot()
|
|
94
|
+
try:
|
|
95
|
+
import pandas as pd
|
|
96
|
+
if isinstance(data, pd.DataFrame):
|
|
97
|
+
miss = data.isna().mean().sort_values(ascending=False)
|
|
98
|
+
cols = list(miss.index.astype(str))
|
|
99
|
+
vals = miss.to_numpy()
|
|
100
|
+
plt.bar(cols, vals)
|
|
101
|
+
plt.ylabel("missing ratio")
|
|
102
|
+
plt.title("missing values")
|
|
103
|
+
return vals
|
|
104
|
+
except ImportError:
|
|
105
|
+
pass
|
|
106
|
+
# fallback: dict
|
|
107
|
+
from .data import columns_of
|
|
108
|
+
ratios = []
|
|
109
|
+
labels = []
|
|
110
|
+
for c in columns_of(data):
|
|
111
|
+
v = np.asarray(data[c]).ravel()
|
|
112
|
+
try:
|
|
113
|
+
vf = v.astype(float)
|
|
114
|
+
r = float(np.isnan(vf).mean())
|
|
115
|
+
except Exception:
|
|
116
|
+
r = 0.0
|
|
117
|
+
labels.append(str(c))
|
|
118
|
+
ratios.append(r)
|
|
119
|
+
plt.bar(labels, np.array(ratios))
|
|
120
|
+
plt.ylabel("missing ratio")
|
|
121
|
+
plt.title("missing values")
|
|
122
|
+
return np.array(ratios)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def plot_trend(data, x_col, y_col, window: int = 7, **kw):
|
|
126
|
+
"""خط زمني + متوسط متحرك (للمحللين الماليين ومحللي المبيعات)."""
|
|
127
|
+
from .data import column_values
|
|
128
|
+
plt = _get_pyplot()
|
|
129
|
+
x = column_values(data, x_col) if x_col else None
|
|
130
|
+
y = column_values(data, y_col)
|
|
131
|
+
n = y.size
|
|
132
|
+
if x is None or x.size != n:
|
|
133
|
+
x = np.arange(n, dtype=float)
|
|
134
|
+
plt.plot(x, y, label=y_col)
|
|
135
|
+
if n >= window:
|
|
136
|
+
ma = np.convolve(y, np.ones(window) / window, mode="same")
|
|
137
|
+
plt.plot(x, ma, label=f"MA({window})")
|
|
138
|
+
plt.xlabel(x_col or "t")
|
|
139
|
+
plt.ylabel(y_col)
|
|
140
|
+
plt.title(f"trend: {y_col}")
|
|
141
|
+
plt.legend()
|
|
142
|
+
return ma if n >= window else y
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def value_counts_plot(values, top: int = 15, horizontal: bool = False, **kw):
|
|
146
|
+
"""رسم تكرار الفئات الأعلى (لمحللي البيانات الفئوية)."""
|
|
147
|
+
plt = _get_pyplot()
|
|
148
|
+
vals = np.asarray(values).ravel()
|
|
149
|
+
uniq, counts = np.unique(vals, return_counts=True)
|
|
150
|
+
order = np.argsort(-counts, kind="stable")[:top]
|
|
151
|
+
uniq, counts = uniq[order], counts[order]
|
|
152
|
+
if horizontal:
|
|
153
|
+
plt.barh(uniq, counts, **kw)
|
|
154
|
+
else:
|
|
155
|
+
plt.bar(uniq, counts, **kw)
|
|
156
|
+
plt.title("value counts")
|
|
157
|
+
return uniq, counts
|