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.
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