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/fast.py ADDED
@@ -0,0 +1,187 @@
1
+ """plotlibs.fast — vectorized speed helpers (decimation, limits, ticks)."""
2
+ from __future__ import annotations
3
+
4
+ import numpy as np
5
+
6
+ DECIMATE_THRESHOLD = 2000 # max points drawn per line; rest min-max decimated
7
+
8
+
9
+ def as_xy(x, y=None):
10
+ """Normalize plot(x) / plot(x, y) inputs to (x, y) float arrays."""
11
+ if y is None:
12
+ y = np.asarray(x, dtype=float).ravel()
13
+ x = np.arange(y.size, dtype=float)
14
+ else:
15
+ x = np.asarray(x, dtype=float).ravel()
16
+ y = np.asarray(y, dtype=float).ravel()
17
+ n = min(x.size, y.size)
18
+ x, y = x[:n], y[:n]
19
+ mask = np.isfinite(x) & np.isfinite(y)
20
+ if not bool(np.all(mask)):
21
+ x, y = x[mask], y[mask]
22
+ return x, y
23
+
24
+
25
+ def decimate(x: np.ndarray, y: np.ndarray, max_points: int = DECIMATE_THRESHOLD):
26
+ """Min-max bucket decimation preserving visual envelope. O(n), numpy only.
27
+
28
+ Splits data into `max_points//2` buckets and keeps min+max of each bucket,
29
+ so spikes don't disappear like naive every-Nth sampling.
30
+ """
31
+ n = x.size
32
+ if n <= max_points or max_points < 4:
33
+ return x, y
34
+ nb = max_points // 2
35
+ # bucket index per point
36
+ idx = np.linspace(0, n, nb + 1).astype(np.int64)
37
+ xs = np.empty(nb * 2, dtype=float)
38
+ ys = np.empty(nb * 2, dtype=float)
39
+ k = 0
40
+ for b in range(nb):
41
+ s, e = int(idx[b]), int(idx[b + 1])
42
+ if e <= s:
43
+ continue
44
+ segx, segy = x[s:e], y[s:e]
45
+ jmin = int(np.argmin(segy))
46
+ jmax = int(np.argmax(segy))
47
+ if jmin <= jmax:
48
+ xs[k], ys[k] = segx[jmin], segy[jmin]
49
+ xs[k + 1], ys[k + 1] = segx[jmax], segy[jmax]
50
+ else:
51
+ xs[k], ys[k] = segx[jmax], segy[jmax]
52
+ xs[k + 1], ys[k + 1] = segx[jmin], segy[jmin]
53
+ k += 2
54
+ # sort by x to keep polyline order (cheap: buckets already ordered)
55
+ return xs[:k], ys[:k]
56
+
57
+
58
+ def nice_limits(vmin: float, vmax: float, pad: float = 0.05):
59
+ if not np.isfinite(vmin) or not np.isfinite(vmax):
60
+ return 0.0, 1.0
61
+ if vmin == vmax:
62
+ d = abs(vmin) * 0.1 if vmin != 0 else 1.0
63
+ return vmin - d, vmax + d
64
+ span = vmax - vmin
65
+ return vmin - span * pad, vmax + span * pad
66
+
67
+
68
+ def nice_ticks(vmin: float, vmax: float, nbins: int = 5):
69
+ """Matplotlib-like MaxNLocator simplified, fully vectorized."""
70
+ if vmin == vmax or not (np.isfinite(vmin) and np.isfinite(vmax)):
71
+ return np.array([vmin])
72
+ span = vmax - vmin
73
+ raw = span / max(1, nbins)
74
+ mag = 10.0 ** np.floor(np.log10(raw))
75
+ for m in (1, 2, 2.5, 5, 10):
76
+ if m * mag >= raw:
77
+ step = m * mag
78
+ break
79
+ else:
80
+ step = 10 * mag
81
+ t0 = np.ceil(vmin / step) * step
82
+ t1 = np.floor(vmax / step) * step
83
+ if t1 < t0:
84
+ return np.array([(vmin + vmax) / 2])
85
+ n = int(round((t1 - t0) / step)) + 1
86
+ n = min(n, 20)
87
+ return np.linspace(t0, t0 + step * (n - 1), n)
88
+
89
+
90
+ def fmt_tick(v: float) -> str:
91
+ if v == 0:
92
+ return "0"
93
+ a = abs(v)
94
+ if a >= 1e6 or a < 1e-3:
95
+ return f"{v:.1e}"
96
+ if a >= 100:
97
+ return f"{v:.0f}" if v == int(v) else f"{v:.1f}"
98
+ if a >= 1:
99
+ s = f"{v:.2f}".rstrip("0").rstrip(".")
100
+ return s
101
+ s = f"{v:.3f}".rstrip("0").rstrip(".")
102
+ return s
103
+
104
+
105
+ def gaussian_kde_1d(v: np.ndarray, points: int = 200, bw: float | None = None):
106
+ """تقدير الكثافة Gaussian KDE بـ numpy فقط (Silverman bandwidth)."""
107
+ v = np.asarray(v, dtype=float).ravel()
108
+ v = v[np.isfinite(v)]
109
+ if v.size < 2:
110
+ xs = np.array([0.0, 1.0])
111
+ return xs, np.zeros(2)
112
+ if v.min() == v.max():
113
+ xs = np.linspace(v.min() - 1, v.max() + 1, points)
114
+ ys = np.exp(-0.5 * ((xs - v.min())) ** 2)
115
+ return xs, ys / ys.max()
116
+ std = v.std() or 1.0
117
+ n = v.size
118
+ bw = bw or (1.06 * std * n ** (-1 / 5)) or std * 0.3
119
+ xs = np.linspace(v.min(), v.max(), points)
120
+ # vectorized: (points, n) قد تكون كبيرة -> chunked
121
+ ys = np.zeros(points)
122
+ chunk = 64
123
+ for i in range(0, points, chunk):
124
+ seg = xs[i:i + chunk, None] - v[None, :]
125
+ ys[i:i + chunk] = np.exp(-0.5 * (seg / bw) ** 2).sum(axis=1)
126
+ ys /= (n * bw * np.sqrt(2 * np.pi))
127
+ return xs, ys
128
+
129
+
130
+ def box_stats(v: np.ndarray) -> dict:
131
+ """إحصاءات boxplot: min/q1/med/q3/max + outliers (1.5*IQR)."""
132
+ v = np.asarray(v, dtype=float).ravel()
133
+ v = v[np.isfinite(v)]
134
+ if v.size == 0:
135
+ return dict(q1=0, med=0, q3=0, lo=0, hi=0, out=np.array([]), mean=0)
136
+ q1, med, q3 = np.quantile(v, [0.25, 0.5, 0.75])
137
+ iqr = q3 - q1 or 1.0
138
+ lo = max(v.min(), q1 - 1.5 * iqr)
139
+ hi = min(v.max(), q3 + 1.5 * iqr)
140
+ out = v[(v < lo) | (v > hi)]
141
+ return dict(q1=float(q1), med=float(med), q3=float(q3),
142
+ lo=float(lo), hi=float(hi), out=out, mean=float(v.mean()))
143
+
144
+
145
+ def roc_from_scores(y_true, y_score, n_pts: int = 200):
146
+ """منحنى ROC بـ numpy فقط. يعيد (fpr, tpr, auc)."""
147
+ y_true = np.asarray(y_true).ravel()
148
+ y_score = np.asarray(y_score, dtype=float).ravel()
149
+ n = min(y_true.size, y_score.size)
150
+ y_true, y_score = y_true[:n] == 1, y_score[:n]
151
+ order = np.argsort(-y_score, kind="stable")
152
+ yt = y_true[order].astype(float)
153
+ tp = np.cumsum(yt)
154
+ fp = np.cumsum(1 - yt)
155
+ P, N = tp[-1] if len(tp) else 0, fp[-1] if len(fp) else 0
156
+ if P == 0 or N == 0:
157
+ return np.array([0, 1]), np.array([0, 1]), 0.5
158
+ tpr = np.concatenate([[0], tp / P, [1]])
159
+ fpr = np.concatenate([[0], fp / N, [1]])
160
+ # تخفيف النقاط
161
+ if len(fpr) > n_pts:
162
+ idx = np.linspace(0, len(fpr) - 1, n_pts).astype(int)
163
+ fpr, tpr = fpr[idx], tpr[idx]
164
+ auc = float(np.trapezoid(tpr, fpr))
165
+ return fpr, tpr, abs(auc)
166
+
167
+
168
+ def pr_from_scores(y_true, y_score, n_pts: int = 200):
169
+ """منحنى Precision-Recall بـ numpy فقط. يعيد (recall, precision, ap)."""
170
+ y_true = np.asarray(y_true).ravel()
171
+ y_score = np.asarray(y_score, dtype=float).ravel()
172
+ n = min(y_true.size, y_score.size)
173
+ y_true, y_score = (y_true[:n] == 1).astype(float), y_score[:n]
174
+ order = np.argsort(-y_score, kind="stable")
175
+ yt = y_true[order]
176
+ tp = np.cumsum(yt)
177
+ fp = np.cumsum(1 - yt)
178
+ P = yt.sum() or 1.0
179
+ prec = tp / np.maximum(tp + fp, 1)
180
+ rec = tp / P
181
+ prec = np.concatenate([[1], prec])
182
+ rec = np.concatenate([[0], rec])
183
+ if len(rec) > n_pts:
184
+ idx = np.linspace(0, len(rec) - 1, n_pts).astype(int)
185
+ rec, prec = rec[idx], prec[idx]
186
+ ap = float(np.trapezoid(prec, rec))
187
+ return rec, prec, abs(ap)