khaled-dsml 0.1.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.
@@ -0,0 +1,17 @@
1
+ """KhaledDSML: educational statistics, ML and big data in Python."""
2
+ __version__ = "0.1.0"
3
+
4
+ from .statistics import (normality_test, shapiro_test, jarque_bera_test,
5
+ independent_t_test, paired_t_test, mann_whitney_test, wilcoxon_test,
6
+ anova_test, kruskal_test, levene_test, chi_square_test, pearson_test,
7
+ spearman_test, ks_two_sample_test, descriptive_summary)
8
+ from .quality import (quality_report, missingness_report, iqr_outliers,
9
+ zscore_outliers, fill_missing_numeric, memory_report, optimize_memory,
10
+ distribution_drift_test)
11
+ from .ml import (split_data, make_regressor, make_classifier, regression_metrics,
12
+ classification_metrics, cross_validation_summary, cluster_data, pca_summary)
13
+ from .bigdata import (generate_transactions, csv_chunk_summary, csv_chunk_missing,
14
+ csv_chunk_monthly, map_reduce_word_count, json_documents_to_table)
15
+ from .quizzes import Question, get_questions, grade_quiz
16
+ from .timeseries import lagged_features
17
+ from .sqltools import sqlite_import_frame, sqlite_group_summary
@@ -0,0 +1,39 @@
1
+ """Simple, teaching-oriented results for statistical hypothesis tests."""
2
+ from dataclasses import asdict, dataclass
3
+ import math
4
+
5
+
6
+ @dataclass(frozen=True)
7
+ class TestResult:
8
+ test: str
9
+ statistic: float
10
+ p_value: float
11
+ alpha: float
12
+ reject_null: bool
13
+ interpretation_ar: str
14
+ null_hypothesis_ar: str
15
+ sample_size: int
16
+ notes: str = ""
17
+
18
+ def to_dict(self) -> dict:
19
+ return asdict(self)
20
+
21
+
22
+ def make_result(
23
+ name: str, statistic: float, p_value: float, n: int,
24
+ alpha: float, null_ar: str, notes: str = "",
25
+ ) -> TestResult:
26
+ if not (0 < alpha < 1):
27
+ raise ValueError("alpha must be between 0 and 1 (exclusive)")
28
+ if not math.isfinite(float(p_value)) or not (0 <= float(p_value) <= 1):
29
+ raise ValueError("Test returned an invalid p-value; inspect assumptions and data")
30
+ reject = bool(p_value < alpha)
31
+ interpretation = (
32
+ f"نرفض الفرضية الصفرية عند مستوى دلالة {alpha:g}؛ "
33
+ "توجد أدلة إحصائية ضدها."
34
+ if reject else
35
+ f"لا نرفض الفرضية الصفرية عند مستوى دلالة {alpha:g}؛ "
36
+ "لا يعني ذلك إثبات صحتها."
37
+ )
38
+ return TestResult(name, float(statistic), float(p_value), float(alpha),
39
+ reject, interpretation, null_ar, int(n), notes)
khaled_dsml/bigdata.py ADDED
@@ -0,0 +1,107 @@
1
+ """Memory-aware big-data teaching tools (local, chunked, non-distributed)."""
2
+ from __future__ import annotations
3
+
4
+ from collections import Counter
5
+ from pathlib import Path
6
+ import re
7
+
8
+ import numpy as np
9
+ import pandas as pd
10
+
11
+
12
+ def generate_transactions(n_rows=10_000, seed=42, missing_rate=.02) -> pd.DataFrame:
13
+ """Reproducible synthetic Algerian transactions, amounts in Algerian dinars."""
14
+ if n_rows < 1 or not 0 <= missing_rate < 1:
15
+ raise ValueError("Require n_rows >= 1 and 0 <= missing_rate < 1")
16
+ rng = np.random.default_rng(seed)
17
+ base = pd.Timestamp("2025-01-01")
18
+ df = pd.DataFrame({
19
+ "id_client": rng.integers(1, 50_000, n_rows),
20
+ "wilaya": rng.choice(["Alger", "Oran", "Setif", "Constantine", "Annaba", "Batna"], n_rows),
21
+ "categorie": rng.choice(["Alimentation", "Electronique", "Vetements", "Transport"], n_rows),
22
+ "montant_da": np.round(rng.lognormal(7.5, .9, n_rows), 2),
23
+ "date": base + pd.to_timedelta(rng.integers(0, 365*86400, n_rows), unit="s"),
24
+ })
25
+ miss = rng.random(n_rows) < missing_rate
26
+ df.loc[miss, "montant_da"] = np.nan
27
+ return df
28
+
29
+
30
+ def csv_chunk_summary(path: str | Path, *, amount_column="montant_da",
31
+ group_by="wilaya", chunksize=100_000) -> pd.DataFrame:
32
+ """Streaming count/sum/mean by category; never loads the whole CSV."""
33
+ if chunksize < 1:
34
+ raise ValueError("chunksize must be positive")
35
+ usecols = [amount_column] if group_by is None else [group_by, amount_column]
36
+ sums: dict = {}
37
+ counts: dict = {}
38
+ for chunk in pd.read_csv(path, usecols=usecols, chunksize=chunksize):
39
+ chunk[amount_column] = pd.to_numeric(chunk[amount_column], errors="coerce")
40
+ if group_by is None:
41
+ pairs = [("ALL", (int(chunk[amount_column].count()),
42
+ float(chunk[amount_column].sum())))]
43
+ else:
44
+ group = chunk.groupby(group_by, dropna=False)[amount_column].agg(["count", "sum"])
45
+ pairs = [(k, (int(row["count"]), float(row["sum"]))) for k, row in group.iterrows()]
46
+ for key, (count, total) in pairs:
47
+ # Use a stable string key also for missing group values.
48
+ k = "<MISSING>" if pd.isna(key) else str(key)
49
+ sums[k] = sums.get(k, 0.0) + total
50
+ counts[k] = counts.get(k, 0) + count
51
+ result = pd.DataFrame({"group": list(sums),
52
+ "count": [counts[k] for k in sums],
53
+ "sum": [sums[k] for k in sums]})
54
+ if result.empty:
55
+ return pd.DataFrame(columns=["count", "sum", "mean"],
56
+ index=pd.Index([], name=group_by or "group"))
57
+ result["mean"] = result["sum"] / result["count"].replace(0, np.nan)
58
+ return result.set_index("group").rename_axis(group_by or "group").sort_values("sum", ascending=False)
59
+
60
+
61
+ def csv_chunk_missing(path: str | Path, *, chunksize=100_000) -> pd.DataFrame:
62
+ """Count missing observations over all CSV chunks."""
63
+ if chunksize < 1:
64
+ raise ValueError("chunksize must be positive")
65
+ total = 0
66
+ missing = None
67
+ for chunk in pd.read_csv(path, chunksize=chunksize):
68
+ total += len(chunk)
69
+ counts = chunk.isna().sum()
70
+ missing = counts if missing is None else missing.add(counts, fill_value=0)
71
+ if missing is None:
72
+ return pd.DataFrame(columns=["count", "percent"])
73
+ return pd.DataFrame({"count": missing.astype(int), "percent": missing / total * 100})
74
+
75
+
76
+ def csv_chunk_monthly(path: str | Path, *, date_column="date",
77
+ amount_column="montant_da", chunksize=100_000) -> pd.Series:
78
+ if chunksize < 1:
79
+ raise ValueError("chunksize must be positive")
80
+ sums = {}
81
+ for chunk in pd.read_csv(path, usecols=[date_column, amount_column], chunksize=chunksize):
82
+ dates = pd.to_datetime(chunk[date_column], errors="coerce")
83
+ amounts = pd.to_numeric(chunk[amount_column], errors="coerce")
84
+ grouped = amounts.groupby(dates.dt.to_period("M")).sum()
85
+ for period, value in grouped.items():
86
+ if pd.notna(period):
87
+ sums[period] = sums.get(period, 0.0) + float(value)
88
+ result = pd.Series(sums, dtype=float, name="total_da").sort_index()
89
+ result.index.name = "month"
90
+ return result
91
+
92
+
93
+ def map_reduce_word_count(documents, *, min_length=2) -> dict[str, int]:
94
+ """Small local MapReduce illustration, not an Apache Spark implementation."""
95
+ if min_length < 1:
96
+ raise ValueError("min_length must be >= 1")
97
+ mapped = (Counter(w.lower() for w in re.findall(r"\b\w+\b", str(doc), flags=re.UNICODE)
98
+ if len(w) >= min_length) for doc in documents)
99
+ reduced: Counter = Counter()
100
+ for part in mapped:
101
+ reduced.update(part)
102
+ return dict(reduced.most_common())
103
+
104
+
105
+ def json_documents_to_table(documents: list[dict]) -> pd.DataFrame:
106
+ """Flatten document-style JSON as an introduction to NoSQL data."""
107
+ return pd.json_normalize(documents)
khaled_dsml/cli.py ADDED
@@ -0,0 +1,36 @@
1
+ """Command line interface: demo, inspect, and quiz."""
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import json
6
+ from .bigdata import generate_transactions
7
+ from .quality import quality_report
8
+ from .quizzes import get_questions
9
+
10
+
11
+ def main(argv=None):
12
+ parser = argparse.ArgumentParser(prog="khaled-dsml", description="KhaledDSML educational toolkit")
13
+ commands = parser.add_subparsers(dest="command", required=True)
14
+ demo = commands.add_parser("demo", help="Run a synthetic dataset example")
15
+ demo.add_argument("--rows", type=int, default=1000)
16
+ inspect = commands.add_parser("inspect", help="Inspect a CSV file (loads it into RAM)")
17
+ inspect.add_argument("csv")
18
+ quiz = commands.add_parser("quiz", help="Print questions without exposing the solutions")
19
+ quiz.add_argument("--course", choices=["all", "data_science", "machine_learning", "big_data"], default="all")
20
+ quiz.add_argument("--count", type=int, default=5)
21
+ args = parser.parse_args(argv)
22
+ if args.command == "demo":
23
+ df = generate_transactions(n_rows=args.rows)
24
+ print(json.dumps(quality_report(df), ensure_ascii=False, indent=2))
25
+ elif args.command == "inspect":
26
+ import pandas as pd
27
+ print(json.dumps(quality_report(pd.read_csv(args.csv)), ensure_ascii=False, indent=2))
28
+ else:
29
+ for q in get_questions(args.course, args.count):
30
+ print(f"\n{q.id}: {q.question}")
31
+ for label, choice in zip("ABCD", q.choices):
32
+ print(f" {label}. {choice}")
33
+
34
+
35
+ if __name__ == "__main__":
36
+ main()
@@ -0,0 +1,32 @@
1
+ """Optional adapters for Dask and Spark; real engines required when used."""
2
+ from __future__ import annotations
3
+
4
+
5
+ def dask_csv_summary(path, *, group_by="wilaya", amount_column="montant_da",
6
+ blocksize="64MB"):
7
+ """Lazy distributed CSV computation; returns a small pandas result table."""
8
+ try:
9
+ import dask.dataframe as dd
10
+ except ImportError as exc:
11
+ raise ImportError('Install with: pip install "khaled-dsml[dask]"') from exc
12
+ frame = dd.read_csv(str(path), blocksize=blocksize)
13
+ result = frame.groupby(group_by)[amount_column].agg(["count", "sum", "mean"])
14
+ return result.compute().sort_values("sum", ascending=False)
15
+
16
+
17
+ def spark_group_summary(spark_df, *, group_by="wilaya", amount_column="montant_da"):
18
+ """Return aggregated *Spark DataFrame* without collecting it to driver RAM."""
19
+ try:
20
+ from pyspark.sql import functions as F
21
+ from pyspark.sql import DataFrame as SparkDataFrame
22
+ except ImportError as exc:
23
+ raise ImportError('Install with: pip install "khaled-dsml[spark]"; Java is also required') from exc
24
+ if not isinstance(spark_df, SparkDataFrame):
25
+ raise TypeError("spark_df must be a pyspark.sql.DataFrame")
26
+ if group_by not in spark_df.columns or amount_column not in spark_df.columns:
27
+ raise KeyError("Requested columns are absent")
28
+ return spark_df.groupBy(group_by).agg(
29
+ F.count(amount_column).alias("count"),
30
+ F.sum(amount_column).alias("sum"),
31
+ F.avg(amount_column).alias("mean"),
32
+ ).orderBy(F.col("sum").desc())
@@ -0,0 +1,79 @@
1
+ """Optional statsmodels diagnostics for quantitative economics."""
2
+ from __future__ import annotations
3
+
4
+ import numpy as np
5
+ import pandas as pd
6
+ from ._results import make_result, TestResult
7
+
8
+
9
+ def _require_statsmodels():
10
+ try:
11
+ import statsmodels.api as sm
12
+ except ImportError as exc:
13
+ raise ImportError('Install with: pip install "khaled-dsml[econometrics]"') from exc
14
+ return sm
15
+
16
+
17
+ def adf_test(series, alpha=0.05) -> TestResult:
18
+ sm = _require_statsmodels()
19
+ from statsmodels.tsa.stattools import adfuller
20
+ x = pd.to_numeric(pd.Series(series), errors="raise").dropna().to_numpy(dtype=float)
21
+ if len(x) < 10:
22
+ raise ValueError("Need at least 10 observations for this educational ADF check")
23
+ stat, p, *_ = adfuller(x, autolag="AIC")
24
+ return make_result("Augmented Dickey-Fuller", stat, p, len(x), alpha,
25
+ "توجد جذور وحدة (السلسلة غير مستقرة بالمعنى المختبر).",
26
+ "اختيار المكون الحتمي وطول الإبطاء يؤثر في الاستنتاج.")
27
+
28
+
29
+ def vif_table(frame: pd.DataFrame) -> pd.DataFrame:
30
+ sm = _require_statsmodels()
31
+ from statsmodels.stats.outliers_influence import variance_inflation_factor
32
+ if not isinstance(frame, pd.DataFrame) or frame.shape[1] < 2:
33
+ raise ValueError("Provide numeric DataFrame with at least 2 features")
34
+ arr = frame.to_numpy(dtype=float)
35
+ if not np.all(np.isfinite(arr)):
36
+ raise ValueError("VIF requires complete finite numeric data")
37
+ design = sm.add_constant(arr, has_constant="add")
38
+ values = [float(variance_inflation_factor(design, i + 1)) for i in range(arr.shape[1])]
39
+ return pd.DataFrame({"feature": frame.columns.tolist(), "vif": values})
40
+
41
+
42
+ def breusch_pagan_test(residuals, features, alpha=.05) -> TestResult:
43
+ sm = _require_statsmodels()
44
+ from statsmodels.stats.diagnostic import het_breuschpagan
45
+ residuals = np.asarray(residuals, dtype=float)
46
+ x = np.asarray(features, dtype=float)
47
+ if x.ndim != 2 or len(residuals) != len(x) or not np.all(np.isfinite(x)):
48
+ raise ValueError("Features must be a finite 2D matrix aligned with residuals")
49
+ if x.shape[1] < 1 or len(x) <= x.shape[1] + 1:
50
+ raise ValueError("Insufficient observations for Breusch-Pagan test")
51
+ design = sm.add_constant(x, has_constant="add")
52
+ stat, p, *_ = het_breuschpagan(residuals, design)
53
+ return make_result("Breusch-Pagan", stat, p, len(residuals), alpha,
54
+ "تباين الأخطاء ثابت.")
55
+
56
+
57
+ def ljung_box_test(series, lags=10, alpha=0.05) -> TestResult:
58
+ sm = _require_statsmodels()
59
+ from statsmodels.stats.diagnostic import acorr_ljungbox
60
+ x = pd.to_numeric(pd.Series(series), errors="raise").dropna().to_numpy(dtype=float)
61
+ if lags < 1 or len(x) <= lags:
62
+ raise ValueError("Need 1 <= lags < number of observations")
63
+ r = acorr_ljungbox(x, lags=[lags], return_df=True)
64
+ return make_result("Ljung-Box", r["lb_stat"].iloc[0], r["lb_pvalue"].iloc[0],
65
+ len(x), alpha, "لا يوجد ارتباط ذاتي حتى عدد الإبطاءات المختار.")
66
+
67
+
68
+ def cronbach_alpha(items: pd.DataFrame) -> float:
69
+ """Internal-consistency coefficient; no claim of unidimensionality."""
70
+ if not isinstance(items, pd.DataFrame) or items.shape[1] < 2:
71
+ raise ValueError("Need >=2 item columns")
72
+ x = items.astype(float).dropna().to_numpy()
73
+ if len(x) < 2:
74
+ raise ValueError("Need >=2 complete cases")
75
+ k = x.shape[1]
76
+ total = np.var(x.sum(axis=1), ddof=1)
77
+ if total <= 0:
78
+ raise ValueError("Total-score variance is zero")
79
+ return float((k / (k - 1)) * (1 - np.var(x, axis=0, ddof=1).sum() / total))
khaled_dsml/ml.py ADDED
@@ -0,0 +1,155 @@
1
+ """Leakage-aware ML workflows and metrics for classroom use."""
2
+ from __future__ import annotations
3
+
4
+ from dataclasses import dataclass
5
+ import numpy as np
6
+ import pandas as pd
7
+ from sklearn.cluster import AgglomerativeClustering, KMeans
8
+ from sklearn.compose import ColumnTransformer
9
+ from sklearn.decomposition import PCA
10
+ from sklearn.ensemble import RandomForestClassifier, RandomForestRegressor
11
+ from sklearn.impute import SimpleImputer
12
+ from sklearn.linear_model import Lasso, LinearRegression, LogisticRegression, Ridge
13
+ from sklearn.metrics import (accuracy_score, confusion_matrix, f1_score,
14
+ mean_absolute_error, mean_squared_error, precision_score, recall_score,
15
+ r2_score, silhouette_score, davies_bouldin_score)
16
+ from sklearn.model_selection import KFold, StratifiedKFold, cross_val_score, train_test_split
17
+ from sklearn.neighbors import KNeighborsClassifier
18
+ from sklearn.pipeline import Pipeline
19
+ from sklearn.preprocessing import OneHotEncoder, StandardScaler
20
+ from sklearn.tree import DecisionTreeClassifier, DecisionTreeRegressor
21
+
22
+
23
+ def split_data(X, y, *, test_size=.2, random_state=42, stratify=False):
24
+ """Always split before computing any preprocessing statistics."""
25
+ return train_test_split(X, y, test_size=test_size, random_state=random_state,
26
+ stratify=y if stratify else None)
27
+
28
+
29
+ def _preprocessor(X):
30
+ if isinstance(X, pd.DataFrame):
31
+ numeric = X.select_dtypes(include="number").columns.tolist()
32
+ categorical = [c for c in X.columns if c not in numeric]
33
+ transformers = []
34
+ if numeric:
35
+ transformers.append(("numeric", Pipeline([
36
+ ("impute", SimpleImputer(strategy="median")),
37
+ ("scale", StandardScaler()),
38
+ ]), numeric))
39
+ if categorical:
40
+ transformers.append(("categorical", Pipeline([
41
+ ("impute", SimpleImputer(strategy="most_frequent")),
42
+ ("encode", OneHotEncoder(handle_unknown="ignore")),
43
+ ]), categorical))
44
+ if not transformers:
45
+ raise ValueError("X must contain at least one feature")
46
+ return ColumnTransformer(transformers)
47
+ return Pipeline([("impute", SimpleImputer(strategy="median")),
48
+ ("scale", StandardScaler())])
49
+
50
+
51
+ def make_regressor(X, algorithm="linear", random_state=42, **kwargs) -> Pipeline:
52
+ """Unfitted pipeline. Fit on training data only."""
53
+ algorithms = {
54
+ "linear": LinearRegression,
55
+ "ridge": Ridge,
56
+ "lasso": Lasso,
57
+ "decision_tree": DecisionTreeRegressor,
58
+ "random_forest": RandomForestRegressor,
59
+ }
60
+ if algorithm not in algorithms:
61
+ raise ValueError("algorithm must be: " + ", ".join(algorithms))
62
+ cls = algorithms[algorithm]
63
+ if algorithm in ("decision_tree", "random_forest"):
64
+ kwargs.setdefault("random_state", random_state)
65
+ return Pipeline([("preprocess", _preprocessor(X)), ("model", cls(**kwargs))])
66
+
67
+
68
+ def make_classifier(X, algorithm="logistic", random_state=42, **kwargs) -> Pipeline:
69
+ """Unfitted pipeline with imputation and categoricals inside CV folds."""
70
+ algorithms = {
71
+ "logistic": LogisticRegression,
72
+ "knn": KNeighborsClassifier,
73
+ "decision_tree": DecisionTreeClassifier,
74
+ "random_forest": RandomForestClassifier,
75
+ }
76
+ if algorithm not in algorithms:
77
+ raise ValueError("algorithm must be: " + ", ".join(algorithms))
78
+ cls = algorithms[algorithm]
79
+ if algorithm in ("logistic", "decision_tree", "random_forest"):
80
+ kwargs.setdefault("random_state", random_state)
81
+ if algorithm == "logistic":
82
+ kwargs.setdefault("max_iter", 1000)
83
+ return Pipeline([("preprocess", _preprocessor(X)), ("model", cls(**kwargs))])
84
+
85
+
86
+ def regression_metrics(y_true, y_pred) -> dict:
87
+ return {"mae": float(mean_absolute_error(y_true, y_pred)),
88
+ "mse": float(mean_squared_error(y_true, y_pred)),
89
+ "rmse": float(np.sqrt(mean_squared_error(y_true, y_pred))),
90
+ "r2": float(r2_score(y_true, y_pred))}
91
+
92
+
93
+ def classification_metrics(y_true, y_pred) -> dict:
94
+ """Macro averaging treats every class equally, including minority classes."""
95
+ return {"accuracy": float(accuracy_score(y_true, y_pred)),
96
+ "precision_macro": float(precision_score(y_true, y_pred, average="macro", zero_division=0)),
97
+ "recall_macro": float(recall_score(y_true, y_pred, average="macro", zero_division=0)),
98
+ "f1_macro": float(f1_score(y_true, y_pred, average="macro", zero_division=0)),
99
+ "confusion_matrix": confusion_matrix(y_true, y_pred).tolist()}
100
+
101
+
102
+ def cross_validation_summary(estimator, X, y, *, task="regression", folds=5,
103
+ random_state=42, scoring=None) -> dict:
104
+ if folds < 2:
105
+ raise ValueError("folds must be >= 2")
106
+ if task not in ("regression", "classification"):
107
+ raise ValueError("task must be regression or classification")
108
+ cv = (KFold(n_splits=folds, shuffle=True, random_state=random_state)
109
+ if task == "regression" else
110
+ StratifiedKFold(n_splits=folds, shuffle=True, random_state=random_state))
111
+ score = scoring or ("r2" if task == "regression" else "f1_macro")
112
+ values = cross_val_score(estimator, X, y, cv=cv, scoring=score)
113
+ return {"scoring": score, "fold_scores": values.tolist(),
114
+ "mean": float(np.mean(values)), "std": float(np.std(values))}
115
+
116
+
117
+ @dataclass
118
+ class ClusterResult:
119
+ labels: np.ndarray
120
+ silhouette: float | None
121
+ davies_bouldin: float | None
122
+ model: object
123
+
124
+
125
+ def cluster_data(X, *, method="kmeans", n_clusters=3, random_state=42,
126
+ scale=True) -> ClusterResult:
127
+ x = np.asarray(X, dtype=float)
128
+ if x.ndim != 2 or len(x) < 2 or not np.all(np.isfinite(x)):
129
+ raise ValueError("Clustering requires a finite numeric 2D matrix")
130
+ if not 2 <= n_clusters <= len(x):
131
+ raise ValueError("Need 2 <= n_clusters <= number of rows")
132
+ data = StandardScaler().fit_transform(x) if scale else x
133
+ if method == "kmeans":
134
+ model = KMeans(n_clusters=n_clusters, random_state=random_state, n_init=10)
135
+ elif method == "hierarchical":
136
+ model = AgglomerativeClustering(n_clusters=n_clusters)
137
+ else:
138
+ raise ValueError("method must be kmeans or hierarchical")
139
+ labels = model.fit_predict(data)
140
+ n_unique = len(np.unique(labels))
141
+ silhouette = (float(silhouette_score(data, labels))
142
+ if 2 <= n_unique < len(x) else None)
143
+ db = float(davies_bouldin_score(data, labels)) if 2 <= n_unique < len(x) else None
144
+ return ClusterResult(labels, silhouette, db, model)
145
+
146
+
147
+ def pca_summary(X, n_components=2, *, scale=True) -> dict:
148
+ x = np.asarray(X, dtype=float)
149
+ if x.ndim != 2 or not np.all(np.isfinite(x)):
150
+ raise ValueError("PCA needs complete finite numeric 2D data")
151
+ data = StandardScaler().fit_transform(x) if scale else x
152
+ model = PCA(n_components=n_components)
153
+ components = model.fit_transform(data)
154
+ return {"scores": components, "explained_variance_ratio": model.explained_variance_ratio_.tolist(),
155
+ "components": model.components_, "model": model}
khaled_dsml/plots.py ADDED
@@ -0,0 +1,41 @@
1
+ """Optional classroom figures; matplotlib is installed through [plots]."""
2
+ from __future__ import annotations
3
+
4
+
5
+ def _plt():
6
+ try:
7
+ import matplotlib.pyplot as plt
8
+ except ImportError as exc:
9
+ raise ImportError('Install with: pip install "khaled-dsml[plots]"') from exc
10
+ return plt
11
+
12
+
13
+ def plot_missingness(df):
14
+ plt = _plt()
15
+ fig, ax = plt.subplots()
16
+ (100 * df.isna().mean()).plot.bar(ax=ax)
17
+ ax.set(ylabel="Missing values (%)", title="Missingness by column")
18
+ fig.tight_layout()
19
+ return fig, ax
20
+
21
+
22
+ def plot_monthly(monthly):
23
+ plt = _plt()
24
+ fig, ax = plt.subplots()
25
+ monthly.plot(ax=ax)
26
+ ax.set(ylabel="Total (DA)", title="Monthly transaction amounts")
27
+ fig.tight_layout()
28
+ return fig, ax
29
+
30
+
31
+ def plot_confusion(confusion):
32
+ import numpy as np
33
+ plt = _plt()
34
+ cm = np.asarray(confusion)
35
+ fig, ax = plt.subplots()
36
+ ax.imshow(cm)
37
+ for (i, j), value in np.ndenumerate(cm):
38
+ ax.text(j, i, str(value), ha="center", va="center")
39
+ ax.set(xlabel="Predicted", ylabel="Actual", title="Confusion matrix")
40
+ fig.tight_layout()
41
+ return fig, ax
khaled_dsml/quality.py ADDED
@@ -0,0 +1,109 @@
1
+ """Data inspection, cleaning helpers and simple distribution drift diagnostics."""
2
+ from __future__ import annotations
3
+
4
+ import numpy as np
5
+ import pandas as pd
6
+ from scipy import stats
7
+ from ._results import make_result, TestResult
8
+
9
+
10
+ def quality_report(df: pd.DataFrame) -> dict:
11
+ """Return shape, missing cells, duplicate rows, dtypes and memory usage."""
12
+ _require_frame(df)
13
+ return {
14
+ "rows": len(df), "columns": len(df.columns),
15
+ "missing_cells": int(df.isna().sum().sum()),
16
+ "missing_by_column": df.isna().sum().astype(int).to_dict(),
17
+ "duplicate_rows": int(df.duplicated().sum()),
18
+ "dtypes": {k: str(v) for k, v in df.dtypes.items()},
19
+ "memory_mb": round(float(df.memory_usage(deep=True).sum()) / 1e6, 4),
20
+ }
21
+
22
+
23
+ def missingness_report(df: pd.DataFrame) -> pd.DataFrame:
24
+ _require_frame(df)
25
+ result = pd.DataFrame({"count": df.isna().sum()})
26
+ result["percent"] = (100 * result["count"] / len(df)) if len(df) else 0.0
27
+ return result
28
+
29
+
30
+ def _require_frame(df):
31
+ if not isinstance(df, pd.DataFrame):
32
+ raise TypeError("Expected a pandas DataFrame")
33
+
34
+
35
+ def iqr_outliers(data: pd.DataFrame, column: str, multiplier: float = 1.5) -> pd.Series:
36
+ """Boolean mask of outliers; NaNs are False. Returns original row index."""
37
+ _require_frame(data)
38
+ if multiplier <= 0:
39
+ raise ValueError("multiplier must be positive")
40
+ x = pd.to_numeric(data[column], errors="raise")
41
+ if x.notna().sum() == 0:
42
+ raise ValueError("Column has no observed values")
43
+ q1, q3 = x.quantile([.25, .75])
44
+ spread = q3 - q1
45
+ return ((x < q1 - multiplier * spread) | (x > q3 + multiplier * spread)).fillna(False)
46
+
47
+
48
+ def zscore_outliers(data: pd.DataFrame, column: str, threshold: float = 3.0) -> pd.Series:
49
+ _require_frame(data)
50
+ if threshold <= 0:
51
+ raise ValueError("threshold must be positive")
52
+ x = pd.to_numeric(data[column], errors="raise")
53
+ std = x.std(ddof=0)
54
+ if not pd.notna(std) or std == 0:
55
+ return pd.Series(False, index=data.index)
56
+ return ((x - x.mean()).abs() / std > threshold).fillna(False)
57
+
58
+
59
+ def fill_missing_numeric(df: pd.DataFrame, strategy: str = "median") -> pd.DataFrame:
60
+ """Fill numeric missing values; return a copy. For ML use train-only imputation."""
61
+ _require_frame(df)
62
+ if strategy not in ("median", "mean", "zero"):
63
+ raise ValueError("strategy must be median, mean or zero")
64
+ out = df.copy()
65
+ for c in out.select_dtypes(include="number").columns:
66
+ if out[c].notna().sum() == 0:
67
+ continue
68
+ value = {"median": out[c].median, "mean": out[c].mean, "zero": lambda: 0}[strategy]()
69
+ out[c] = out[c].fillna(value)
70
+ return out
71
+
72
+
73
+ def memory_report(df: pd.DataFrame) -> pd.DataFrame:
74
+ _require_frame(df)
75
+ usage = df.memory_usage(deep=True, index=False)
76
+ return pd.DataFrame({"bytes": usage, "megabytes": usage / 1e6}).sort_values("bytes", ascending=False)
77
+
78
+
79
+ def optimize_memory(df: pd.DataFrame, *, category_ratio: float = .5,
80
+ allow_float_downcast: bool = False) -> pd.DataFrame:
81
+ """Downcast ints, optionally float64, and low-cardinality text columns.
82
+
83
+ Float downcasting can be lossy and is disabled by default.
84
+ Original dataframe is not changed.
85
+ """
86
+ _require_frame(df)
87
+ if not 0 < category_ratio <= 1:
88
+ raise ValueError("category_ratio must be in (0, 1]")
89
+ out = df.copy()
90
+ for c in out.columns:
91
+ s = out[c]
92
+ if pd.api.types.is_integer_dtype(s.dtype):
93
+ out[c] = pd.to_numeric(s, downcast="integer")
94
+ elif allow_float_downcast and pd.api.types.is_float_dtype(s.dtype):
95
+ out[c] = pd.to_numeric(s, downcast="float")
96
+ elif (pd.api.types.is_object_dtype(s.dtype) or pd.api.types.is_string_dtype(s.dtype)):
97
+ observed = s.dropna()
98
+ if len(observed) and observed.nunique() / len(observed) <= category_ratio:
99
+ out[c] = s.astype("category")
100
+ return out
101
+
102
+
103
+ def distribution_drift_test(reference, current, alpha=.05) -> TestResult:
104
+ from .statistics import ks_two_sample_test
105
+ result = ks_two_sample_test(reference, current, alpha=alpha)
106
+ return TestResult("Distribution drift / KS", result.statistic, result.p_value,
107
+ result.alpha, result.reject_null, result.interpretation_ar,
108
+ result.null_hypothesis_ar, result.sample_size,
109
+ "اختبار اختلاف التوزيعات وليس اختبارًا للسببية.")
khaled_dsml/quizzes.py ADDED
@@ -0,0 +1,62 @@
1
+ """Optional classroom multiple-choice quizzes with reproducible selection."""
2
+ from __future__ import annotations
3
+ from dataclasses import dataclass
4
+ from random import Random
5
+
6
+
7
+ @dataclass(frozen=True)
8
+ class Question:
9
+ id: str
10
+ course: str
11
+ question: str
12
+ choices: tuple[str, str, str, str]
13
+ answer: str
14
+ explanation_ar: str
15
+
16
+
17
+ BANK = [
18
+ Question("DS01", "data_science", "ما المقصود بوحدة المشاهدة؟", ("سطر يمثل حالة", "اسم الجدول", "نوع الرسم", "ملف البرنامج"), "A", "المشاهدة هي الحالة التي يمثلها صف الجدول."),
19
+ Question("DS02", "data_science", "أي مقياس أقل تأثرًا بالقيم المتطرفة؟", ("المتوسط", "الوسيط", "المدى", "التباين"), "B", "الوسيط مقاوم نسبيًا للقيم المتطرفة."),
20
+ Question("DS03", "data_science", "ما المشكلة الأساسية في حذف جميع القيم المفقودة؟", ("زيادة حجم البيانات", "فقد عينة واحتمال التحيز", "تحسين التمثيل دائمًا", "تقليل الدقة العددية فقط"), "B", "الحذف قد يقلص العينة ويسبب تحيزًا."),
21
+ Question("DS04", "data_science", "أي اختبار شائع لفحص اعتدالية توزيع البيانات؟", ("Shapiro-Wilk", "K-means", "F1", "MAE"), "A", "Shapiro-Wilk يختبر فرضية التوزيع الطبيعي."),
22
+ Question("DS05", "data_science", "ماذا يعني الارتباط الموجب؟", ("السببية مؤكدة", "اتجاه التغير المشترك غالبًا واحد", "عدم وجود علاقة", "تساوي المتوسطات"), "B", "الارتباط لا يثبت السببية."),
23
+ Question("DS06", "data_science", "ماذا يعني عدم رفض H0؟", ("أثبتنا صحة H0", "لا توجد أدلة كافية للرفض", "H1 صحيحة حتمًا", "حجم العينة كبير"), "B", "عدم الرفض لا يثبت الفرضية الصفرية."),
24
+ Question("DS07", "data_science", "الهدف من IQR في التنظيف؟", ("قياس ارتباط خطي", "كشف قيم بعيدة وفق الربيعات", "ترتيب الأشهر", "تقسيم التدريب"), "B", "قاعدة IQR إحدى طرق الإشارة إلى الشواذ."),
25
+ Question("DS08", "data_science", "ماذا تتطلب معالجة البيانات الأخلاقية؟", ("نشر بيانات العملاء", "حماية الخصوصية وتوثيق المصدر", "حذف مصدر البيانات", "إلغاء النسخ الاحتياطية"), "B", "إخفاء البيانات الحساسة وأخذ الموافقات من الأسس."),
26
+ Question("ML01", "machine_learning", "ما دور مجموعة الاختبار؟", ("ضبط كل المعاملات حتى أفضل نتيجة", "قياس التعميم النهائي", "تدريب النموذج", "حذف الفئات"), "B", "مجموعة الاختبار تعطي تقديرًا على بيانات غير مستخدمة بالتدريب/الضبط."),
27
+ Question("ML02", "machine_learning", "ما مثال على تسرب البيانات؟", ("التقييس قبل تقسيم كامل البيانات", "استخدام Pipeline داخل CV", "فصل البيانات أولًا", "تثبيت random_state"), "A", "التقييس قبل التقسيم ينقل معلومات الاختبار إلى التدريب."),
28
+ Question("ML03", "machine_learning", "أي مقياس يستخدم للتنبؤ العددي؟", ("Recall", "MAE", "Accuracy", "F1"), "B", "MAE متوسط الخطأ المطلق."),
29
+ Question("ML04", "machine_learning", "ما وظيفة Ridge؟", ("عقوبة L2", "تجميع غير خاضع للإشراف", "تشخيص بيانات نصية", "تحويل JSON"), "A", "Ridge يطبق تنظيم L2 في الانحدار."),
30
+ Question("ML05", "machine_learning", "أي خوارزمية تستخدم لتجميع عملاء دون تسميات؟", ("K-means", "LogisticRegression", "LinearRegression", "Welch t-test"), "A", "K-means تعلم غير خاضع للإشراف."),
31
+ Question("ML06", "machine_learning", "ما سبب استخدام F1 مع الفئات غير المتوازنة؟", ("يلغي تحيز البيانات", "يوفق بين Precision وRecall", "يقيس المبيعات", "يلغي الحاجة إلى الاختبار"), "B", "F1 متوسط توافقي للدقة الإيجابية والاسترجاع."),
32
+ Question("ML07", "machine_learning", "كيف تختبر عدة تقسيمات للتدريب والتقييم؟", ("Cross-validation", "Excel فقط", "إزالة المتغير الهدف", "تقريب الأرقام"), "A", "التحقق المتقاطع يعيد التقييم عبر طيات متعددة."),
33
+ Question("ML08", "machine_learning", "ما المقصود بـ PCA؟", ("اختبار قيمة مفقودة", "خفض الأبعاد بمركبات رئيسية", "تصنيف موجه", "توليد SQL"), "B", "PCA يستخرج مركبات تفسر تباينًا من البيانات."),
34
+ Question("BD01", "big_data", "ما المقصود بـ Volume في 5Vs؟", ("حجم البيانات", "دقتها", "مصدرها", "قيمتها الاقتصادية"), "A", "Volume يشير إلى حجم البيانات."),
35
+ Question("BD02", "big_data", "ماذا تفيد chunksize في read_csv؟", ("تحميل الملف كله دائمًا", "قراءة دفعات بدل الملف كاملًا", "إزالة الارتباط", "ترميز متغير الهدف"), "B", "chunksize يعيد أجزاء متعاقبة للمعالجة."),
36
+ Question("BD03", "big_data", "ما المقصود بـ MapReduce؟", ("رسم النقاط", "توزيع المعالجة ثم تجميع النتائج", "إعادة تسمية الأعمدة", "فحص الاعتدالية"), "B", "Map ثم Reduce من مفاهيم المعالجة الموزعة."),
37
+ Question("BD04", "big_data", "أي نوع تمثله مستندات JSON غالبًا؟", ("شبه منظمة", "صور خام", "غير قابلة للمعالجة", "بيانات مقاسة بالثواني"), "A", "JSON يستخدم بنية مفاتيح وقيم مرنة."),
38
+ Question("BD05", "big_data", "كيف نقارن استهلاك الذاكرة لـ DataFrame؟", ("memory_usage(deep=True)", "shape وحده", "head", "unique"), "A", "deep=True يحسب أيضًا الذاكرة المخزنة في كائنات النصوص."),
39
+ Question("BD06", "big_data", "ما الفرق بين SQL وNoSQL؟", ("NoSQL دائمًا أسرع", "NoSQL تدعم نماذج تخزين متعددة", "SQL لا تدعم الفهارس", "SQL تعني الصور"), "B", "NoSQL تشمل المستندات والمفاتيح/القيم وأشكالًا أخرى."),
40
+ Question("BD07", "big_data", "ما خطر استخلاص استنتاجات عامة من بيانات اصطناعية؟", ("لا توجد مخاطر", "قد لا تمثل الواقع الاقتصادي", "تزيد السرعة", "تزيد عدد الأعمدة"), "B", "بيانات المحاكاة توضح المنهج لكنها ليست دليلًا واقعيًا."),
41
+ Question("BD08", "big_data", "ما الاسم الشائع للتنبؤ الآني بمؤشر اقتصادي؟", ("Nowcasting", "OneHot", "Shapiro", "KNN"), "A", "Nowcasting تقدير للحالة الحالية قبل إصدار الإحصاءات النهائية."),
42
+ ]
43
+
44
+
45
+ def get_questions(course="all", limit=10, seed=42) -> list[Question]:
46
+ courses = ("all", "data_science", "machine_learning", "big_data")
47
+ if course not in courses:
48
+ raise ValueError("course must be: " + ", ".join(courses))
49
+ if limit < 1:
50
+ raise ValueError("limit must be positive")
51
+ questions = [q for q in BANK if course == "all" or q.course == course]
52
+ return Random(seed).sample(questions, min(limit, len(questions)))
53
+
54
+
55
+ def grade_quiz(questions: list[Question], answers: dict[str, str]) -> dict:
56
+ """Grade a list of questions. Answers map question IDs to A/B/C/D."""
57
+ results = [{"id": q.id, "correct": answers.get(q.id, "").strip().upper() == q.answer,
58
+ "answer": q.answer, "explanation_ar": q.explanation_ar} for q in questions]
59
+ count = sum(r["correct"] for r in results)
60
+ return {"correct": count, "total": len(results),
61
+ "percentage": round(count / len(results) * 100, 2) if results else 0,
62
+ "details": results}
@@ -0,0 +1,35 @@
1
+ """SQLite demonstrations for TD2: SQL versus document-style data."""
2
+ from __future__ import annotations
3
+ import re
4
+ import sqlite3
5
+ from pathlib import Path
6
+ import pandas as pd
7
+
8
+
9
+ def _safe_name(name: str) -> str:
10
+ if not isinstance(name, str) or not re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", name):
11
+ raise ValueError("SQL identifier must be ASCII letters, digits or underscores")
12
+ return '"' + name + '"'
13
+
14
+
15
+ def sqlite_import_frame(df: pd.DataFrame, database: str | Path,
16
+ table="transactions", if_exists="replace") -> int:
17
+ """Store a pandas DataFrame in a local SQLite database."""
18
+ if not isinstance(df, pd.DataFrame):
19
+ raise TypeError("df must be a pandas DataFrame")
20
+ _safe_name(table)
21
+ if if_exists not in {"replace", "append", "fail"}:
22
+ raise ValueError("if_exists must be replace, append or fail")
23
+ with sqlite3.connect(str(database)) as conn:
24
+ count = df.to_sql(table, conn, if_exists=if_exists, index=False)
25
+ return int(count)
26
+
27
+
28
+ def sqlite_group_summary(database: str | Path, *, table="transactions",
29
+ group_by="wilaya", amount_column="montant_da") -> pd.DataFrame:
30
+ """Return count/sum/mean, computed by SQLite using a safe fixed SQL pattern."""
31
+ t, g, a = _safe_name(table), _safe_name(group_by), _safe_name(amount_column)
32
+ sql = (f"SELECT {g} AS category, COUNT({a}) AS count, SUM({a}) AS total, "
33
+ f"AVG({a}) AS mean FROM {t} GROUP BY {g} ORDER BY total DESC")
34
+ with sqlite3.connect(str(database)) as conn:
35
+ return pd.read_sql_query(sql, conn)
@@ -0,0 +1,162 @@
1
+ """Introductory inferential statistics. Each test explicitly reports H0."""
2
+ from __future__ import annotations
3
+
4
+ import numpy as np
5
+ import pandas as pd
6
+ from scipy import stats
7
+ from ._results import TestResult, make_result
8
+
9
+
10
+ def _sample(x, minimum=2):
11
+ s = pd.to_numeric(pd.Series(x), errors="raise").dropna().to_numpy(dtype=float)
12
+ if s.size < minimum or not np.all(np.isfinite(s)):
13
+ raise ValueError(f"Need at least {minimum} finite numeric observations")
14
+ return s
15
+
16
+
17
+ def shapiro_test(x, alpha=0.05) -> TestResult:
18
+ a = _sample(x, 3)
19
+ if np.ptp(a) == 0:
20
+ raise ValueError("Shapiro-Wilk is undefined for constant data")
21
+ result = stats.shapiro(a)
22
+ note = "قد تكون p-value غير دقيقة للعينات أكبر من 5000." if len(a) > 5000 else ""
23
+ return make_result("Shapiro-Wilk", result.statistic, result.pvalue, len(a), alpha,
24
+ "تتبع البيانات توزيعًا طبيعيًا.", note)
25
+
26
+
27
+ def jarque_bera_test(x, alpha=0.05) -> TestResult:
28
+ a = _sample(x, 3)
29
+ if np.ptp(a) == 0:
30
+ raise ValueError("Jarque-Bera is undefined for constant data")
31
+ r = stats.jarque_bera(a)
32
+ return make_result("Jarque-Bera", r.statistic, r.pvalue, len(a), alpha,
33
+ "تتفق الالتواء والتفرطح مع التوزيع الطبيعي.")
34
+
35
+
36
+ def normality_test(x, method="shapiro", alpha=0.05) -> TestResult:
37
+ """method='shapiro' or 'jarque_bera'."""
38
+ if method == "shapiro":
39
+ return shapiro_test(x, alpha)
40
+ if method == "jarque_bera":
41
+ return jarque_bera_test(x, alpha)
42
+ raise ValueError("method must be 'shapiro' or 'jarque_bera'")
43
+
44
+
45
+ def independent_t_test(a, b, alpha=0.05, equal_var=False) -> TestResult:
46
+ x, y = _sample(a), _sample(b)
47
+ r = stats.ttest_ind(x, y, equal_var=equal_var)
48
+ name = "Student t-test" if equal_var else "Welch t-test"
49
+ return make_result(name, r.statistic, r.pvalue, len(x)+len(y), alpha,
50
+ "متوسطا المجموعتين متساويان.")
51
+
52
+
53
+ def paired_t_test(a, b, alpha=0.05) -> TestResult:
54
+ x, y = np.asarray(a, dtype=float), np.asarray(b, dtype=float)
55
+ if x.shape != y.shape:
56
+ raise ValueError("Paired samples must have the same shape")
57
+ good = np.isfinite(x) & np.isfinite(y)
58
+ x, y = x[good], y[good]
59
+ if len(x) < 2:
60
+ raise ValueError("Need at least two valid pairs")
61
+ r = stats.ttest_rel(x, y)
62
+ return make_result("Paired t-test", r.statistic, r.pvalue, len(x), alpha,
63
+ "متوسط الفروق بين القياسين يساوي صفرًا.")
64
+
65
+
66
+ def mann_whitney_test(a, b, alpha=0.05) -> TestResult:
67
+ x, y = _sample(a), _sample(b)
68
+ r = stats.mannwhitneyu(x, y, alternative="two-sided")
69
+ return make_result("Mann-Whitney U", r.statistic, r.pvalue, len(x)+len(y), alpha,
70
+ "توزيعا المجموعتين متماثلان.",
71
+ "لا يُفسَّر تلقائيًا على أنه اختبار تساوي الوسيطين.")
72
+
73
+
74
+ def wilcoxon_test(a, b, alpha=0.05) -> TestResult:
75
+ x, y = np.asarray(a, dtype=float), np.asarray(b, dtype=float)
76
+ if x.shape != y.shape:
77
+ raise ValueError("Paired samples must have the same shape")
78
+ valid = np.isfinite(x) & np.isfinite(y)
79
+ x, y = x[valid], y[valid]
80
+ if len(x) < 2 or np.allclose(x-y, 0):
81
+ raise ValueError("Need at least two pairs and a nonzero difference")
82
+ r = stats.wilcoxon(x, y)
83
+ return make_result("Wilcoxon signed-rank", r.statistic, r.pvalue, len(x), alpha,
84
+ "توزيع الفروق متناظر حول الصفر.")
85
+
86
+
87
+ def anova_test(*groups, alpha=0.05) -> TestResult:
88
+ if len(groups) < 2:
89
+ raise ValueError("Provide at least two groups")
90
+ arrays = [_sample(group) for group in groups]
91
+ r = stats.f_oneway(*arrays)
92
+ return make_result("One-way ANOVA", r.statistic, r.pvalue,
93
+ sum(map(len, arrays)), alpha, "متوسطات كل المجموعات متساوية.")
94
+
95
+
96
+ def kruskal_test(*groups, alpha=0.05) -> TestResult:
97
+ if len(groups) < 2:
98
+ raise ValueError("Provide at least two groups")
99
+ arrays = [_sample(group) for group in groups]
100
+ r = stats.kruskal(*arrays)
101
+ return make_result("Kruskal-Wallis", r.statistic, r.pvalue,
102
+ sum(map(len, arrays)), alpha, "توزيعات المجموعات متماثلة.")
103
+
104
+
105
+ def levene_test(*groups, alpha=0.05) -> TestResult:
106
+ if len(groups) < 2:
107
+ raise ValueError("Provide at least two groups")
108
+ arrays = [_sample(group) for group in groups]
109
+ r = stats.levene(*arrays, center="median")
110
+ return make_result("Brown-Forsythe (Levene)", r.statistic, r.pvalue,
111
+ sum(map(len, arrays)), alpha, "تباينات المجموعات متساوية.")
112
+
113
+
114
+ def chi_square_test(contingency_table, alpha=0.05) -> TestResult:
115
+ table = np.asarray(contingency_table, dtype=float)
116
+ if table.ndim != 2 or min(table.shape) < 2 or not np.all(np.isfinite(table)):
117
+ raise ValueError("Expected a finite 2D contingency table >= 2x2")
118
+ if np.any(table < 0) or np.any(table.sum(axis=0) == 0) or np.any(table.sum(axis=1) == 0):
119
+ raise ValueError("Table needs nonnegative counts and nonzero margins")
120
+ r = stats.chi2_contingency(table, correction=False)
121
+ notes = "راجع التكرارات المتوقعة؛ قد يلزم اختبار دقيق عند ندرتها."
122
+ return make_result("Pearson Chi-square independence", r.statistic, r.pvalue,
123
+ int(table.sum()), alpha, "المتغيران الفئويان مستقلان.", notes)
124
+
125
+
126
+ def pearson_test(a, b, alpha=0.05) -> TestResult:
127
+ x, y = _paired(a, b)
128
+ r = stats.pearsonr(x, y)
129
+ return make_result("Pearson correlation", r.statistic, r.pvalue,
130
+ len(x), alpha, "الارتباط الخطي السكاني يساوي صفرًا.")
131
+
132
+
133
+ def spearman_test(a, b, alpha=0.05) -> TestResult:
134
+ x, y = _paired(a, b)
135
+ r = stats.spearmanr(x, y)
136
+ return make_result("Spearman correlation", r.statistic, r.pvalue,
137
+ len(x), alpha, "الارتباط الرتبي السكاني يساوي صفرًا.")
138
+
139
+
140
+ def _paired(a, b):
141
+ x, y = np.asarray(a, dtype=float), np.asarray(b, dtype=float)
142
+ if x.shape != y.shape or x.ndim != 1:
143
+ raise ValueError("Need aligned one-dimensional samples")
144
+ valid = np.isfinite(x) & np.isfinite(y)
145
+ x, y = x[valid], y[valid]
146
+ if len(x) < 3 or np.std(x) == 0 or np.std(y) == 0:
147
+ raise ValueError("Need at least 3 variable paired observations")
148
+ return x, y
149
+
150
+
151
+ def ks_two_sample_test(a, b, alpha=0.05) -> TestResult:
152
+ x, y = _sample(a), _sample(b)
153
+ r = stats.ks_2samp(x, y)
154
+ return make_result("Two-sample Kolmogorov-Smirnov", r.statistic, r.pvalue,
155
+ len(x)+len(y), alpha, "المجموعتان تتبعان التوزيع نفسه.")
156
+
157
+
158
+ def descriptive_summary(data: pd.DataFrame) -> pd.DataFrame:
159
+ """Descriptive statistics for numeric columns only."""
160
+ if not isinstance(data, pd.DataFrame):
161
+ raise TypeError("data must be a pandas DataFrame")
162
+ return data.describe(include="number").T
@@ -0,0 +1,22 @@
1
+ """Time-safe lagged features for classroom forecasting exercises."""
2
+ from __future__ import annotations
3
+ import pandas as pd
4
+
5
+
6
+ def lagged_features(series, *, lags=(1, 2, 3), rolling_window=None) -> pd.DataFrame:
7
+ """Create past-only features: lag t-n and optional mean up to t-1.
8
+
9
+ Input must already be sorted in ascending time order. No random splitting
10
+ should be used to validate temporal forecasts.
11
+ """
12
+ values = pd.Series(series)
13
+ if len(lags) < 1 or any(not isinstance(lag, int) or lag < 1 for lag in lags):
14
+ raise ValueError("lags must be positive integers")
15
+ out = pd.DataFrame(index=values.index)
16
+ for lag in lags:
17
+ out[f"lag_{lag}"] = values.shift(lag)
18
+ if rolling_window is not None:
19
+ if not isinstance(rolling_window, int) or rolling_window < 2:
20
+ raise ValueError("rolling_window must be an integer >= 2")
21
+ out[f"rolling_mean_{rolling_window}"] = values.shift(1).rolling(rolling_window).mean()
22
+ return out
@@ -0,0 +1,188 @@
1
+ Metadata-Version: 2.4
2
+ Name: khaled-dsml
3
+ Version: 0.1.0
4
+ Summary: Educational data science, machine learning and big-data analysis toolkit with Arabic interpretations
5
+ Author-email: Khaled Ali <ali-khaled@univ-eloued.dz>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/noble2013/KhaledDSML
8
+ Project-URL: Repository, https://github.com/noble2013/KhaledDSML
9
+ Project-URL: Issues, https://github.com/noble2013/KhaledDSML/issues
10
+ Keywords: data-science,machine-learning,big-data,education,statistics,arabic
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Education
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Scientific/Engineering :: Information Analysis
20
+ Requires-Python: >=3.10
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Requires-Dist: numpy>=1.26
24
+ Requires-Dist: pandas>=2.1
25
+ Requires-Dist: scipy>=1.11
26
+ Requires-Dist: scikit-learn>=1.3
27
+ Provides-Extra: econometrics
28
+ Requires-Dist: statsmodels>=0.14; extra == "econometrics"
29
+ Provides-Extra: dask
30
+ Requires-Dist: dask[dataframe]>=2024.1; extra == "dask"
31
+ Provides-Extra: spark
32
+ Requires-Dist: pyspark>=3.5; extra == "spark"
33
+ Provides-Extra: plots
34
+ Requires-Dist: matplotlib>=3.8; extra == "plots"
35
+ Provides-Extra: notebooks
36
+ Requires-Dist: jupyterlab>=4; extra == "notebooks"
37
+ Requires-Dist: ipykernel>=6; extra == "notebooks"
38
+ Requires-Dist: openpyxl>=3.1; extra == "notebooks"
39
+ Provides-Extra: dev
40
+ Requires-Dist: pytest>=8; extra == "dev"
41
+ Requires-Dist: build>=1; extra == "dev"
42
+ Requires-Dist: twine>=5; extra == "dev"
43
+ Provides-Extra: all
44
+ Requires-Dist: statsmodels>=0.14; extra == "all"
45
+ Requires-Dist: matplotlib>=3.8; extra == "all"
46
+ Requires-Dist: jupyterlab>=4; extra == "all"
47
+ Requires-Dist: ipykernel>=6; extra == "all"
48
+ Requires-Dist: openpyxl>=3.1; extra == "all"
49
+ Dynamic: license-file
50
+
51
+ # KhaledDSML — علم البيانات وتعلّم الآلة وتحليل البيانات الضخمة
52
+
53
+ **الإعداد والتطوير: الأستاذ الدكتور خالد علي (Khaled Ali)**
54
+ **الجامعة:** جامعة الشهيد حمه لخضر – الوادي، الجزائر (University of El Oued, Algeria)
55
+ **الكلية:** كلية العلوم الاقتصادية والتجارية وعلوم التسيير (Faculty of Economic, Commercial and Management Sciences)
56
+ **البريد الإلكتروني المهني:** [ali-khaled@univ-eloued.dz](mailto:ali-khaled@univ-eloued.dz)
57
+ **الإصدار التجريبي: 0.1.0 — أكتوبر 2026**
58
+
59
+ حزمة Python تعليمية للمقاييس التالية:
60
+
61
+ - علم البيانات وتعلّم الآلة — السنة الثالثة ليسانس اقتصاد وتسيير المؤسسات.
62
+ - تحليل البيانات الضخمة — السنة الثانية ماستر اقتصاد كمي.
63
+
64
+ > تنبيه: تم تصميم المشروع وفق البرامج البيداغوجية التي أعدّها المؤلف؛ لا يُدَّعى تطابقه مع بطاقة وزارية رسمية دون مراجعتها. البيانات الاصطناعية للأغراض التعليمية فقط. الإحصاء الوصفي لا يثبت السببية، وعدم رفض الفرضية الصفرية لا يثبت صحتها.
65
+
66
+ ## لماذا هذه المكتبة؟
67
+
68
+ - اختبارات فرضيات شائعة مع `statistic`، و`p_value`، وفرضية صفرية، وتفسير عربي.
69
+ - فحص جودة البيانات واكتشاف القيم الشاذة وتحسين الذاكرة.
70
+ - أنابيب تدريب آمنة من تسرب البيانات، وتقييم الانحدار والتصنيف والتجميع.
71
+ - معالجة CSV بالأجزاء، وتجميع شهري وجغرافي، وتوليد مليون معاملة اصطناعية إذا طُلب ذلك.
72
+ - 24 سؤال اختيار متعدد للتقويم التكويني مع التصحيح والتفسير.
73
+ - دعم Windows وJupyterLab وGoogle Colab بعد تثبيت الاعتمادات.
74
+
75
+ ## التثبيت من المجلد المصدر — Windows PowerShell
76
+
77
+ افتح PowerShell من داخل المجلد الذي يحتوي `pyproject.toml`:
78
+
79
+ ```powershell
80
+ py -3.13 -m venv .venv
81
+ .\.venv\Scripts\python.exe -m pip install --upgrade pip
82
+ .\.venv\Scripts\python.exe -m pip install -e ".[all,dev]"
83
+ .\.venv\Scripts\python.exe -m pytest
84
+ .\.venv\Scripts\khaled-dsml.exe demo --rows 1000
85
+ .\.venv\Scripts\khaled-dsml.exe quiz --course big_data --count 5
86
+ ```
87
+
88
+ بعض بيئات Windows تستعمل `py` بدل `py -3.13` عند عدم توفر الفرع 3.13.
89
+
90
+ *لاحقًا وبعد النشر الفعلي على PyPI فقط:*
91
+
92
+ ```powershell
93
+ py -m pip install khaled-dsml
94
+ ```
95
+
96
+ ## أمثلة عملية
97
+
98
+ ### اختبار اعتدالية
99
+
100
+ ```python
101
+ from khaled_dsml import normality_test
102
+ result = normality_test([12, 13, 15, 17, 16, 14, 13, 18])
103
+ print(result.interpretation_ar)
104
+ print(result.p_value)
105
+ ```
106
+
107
+ ### كشف مشاكل البيانات
108
+
109
+ ```python
110
+ from khaled_dsml import generate_transactions, quality_report, missingness_report
111
+ transactions = generate_transactions(100_000)
112
+ print(quality_report(transactions))
113
+ print(missingness_report(transactions))
114
+ ```
115
+
116
+ ### تعلم الآلة (منع تسرب البيانات)
117
+
118
+ ```python
119
+ from sklearn.datasets import make_regression
120
+ from khaled_dsml import split_data, make_regressor, regression_metrics
121
+ X, y = make_regression(n_samples=200, n_features=3, noise=3, random_state=42)
122
+ X_train, X_test, y_train, y_test = split_data(X, y)
123
+ model = make_regressor(X_train, "ridge", alpha=1.0)
124
+ model.fit(X_train, y_train)
125
+ print(regression_metrics(y_test, model.predict(X_test)))
126
+ ```
127
+
128
+ ### مليون معاملة والتجميع بالأجزاء
129
+
130
+ ```python
131
+ from khaled_dsml import generate_transactions, csv_chunk_summary, csv_chunk_monthly
132
+ transactions = generate_transactions(1_000_000)
133
+ transactions.to_csv("transactions.csv", index=False)
134
+ print(csv_chunk_summary("transactions.csv", group_by="wilaya"))
135
+ print(csv_chunk_monthly("transactions.csv"))
136
+ ```
137
+
138
+ `read_csv(chunksize=...)` لا يحتاج تحميل الملف بكامله. **لكن** توليد مليون صف في `generate_transactions` يحتفظ بها في الذاكرة قبل الحفظ؛ اختر عدد صفوف أصغر لجهاز بذاكرة محدودة.
139
+
140
+ ### أسئلة تصحيح آلي
141
+
142
+ ```python
143
+ from khaled_dsml import get_questions, grade_quiz
144
+ questions = get_questions("machine_learning", limit=3, seed=3)
145
+ for question in questions:
146
+ print(question.id, question.question)
147
+ answers = {q.id: "A" for q in questions} # إجابات افتراضية
148
+ print(grade_quiz(questions, answers))
149
+ ```
150
+
151
+ ## واجهات المكتبة
152
+
153
+ | الوحدة | أمثلة الدوال |
154
+ |---|---|
155
+ | `statistics` | `normality_test`, `independent_t_test`, `paired_t_test`, `anova_test`, `kruskal_test`, `chi_square_test`, `pearson_test`, `spearman_test`, `levene_test`, `ks_two_sample_test` |
156
+ | `quality` | `quality_report`, `missingness_report`, `iqr_outliers`, `fill_missing_numeric`, `optimize_memory`, `distribution_drift_test` |
157
+ | `ml` | `make_regressor`, `make_classifier`, `cross_validation_summary`, `classification_metrics`, `regression_metrics`, `cluster_data`, `pca_summary` |
158
+ | `bigdata` | `generate_transactions`, `csv_chunk_summary`, `csv_chunk_missing`, `csv_chunk_monthly`, `map_reduce_word_count`, `json_documents_to_table` |
159
+ | `sqltools` | `sqlite_import_frame`, `sqlite_group_summary` (SQLite محلي لتجارب SQL) |
160
+ | `timeseries` | `lagged_features` (خصائص مؤخرة دون تسرب للمستقبل) |
161
+ | `distributed` | `dask_csv_summary`, `spark_group_summary` (تثبيت `[dask]` أو `[spark]` واستخدام محرك فعلي) |
162
+ | `econometrics` | `adf_test`, `vif_table`, `breusch_pagan_test`, `ljung_box_test`, `cronbach_alpha` (تثبيت `econometrics`) |
163
+ | `plots` | `plot_missingness`, `plot_monthly`, `plot_confusion` (تثبيت `plots`) |
164
+ | `quizzes` | `get_questions`, `grade_quiz` |
165
+
166
+ لخريطة المقياسين والتغطية التفصيلية، راجع [docs/COURSE_MAPPING_AR.md](docs/COURSE_MAPPING_AR.md). للحصول على أمثلة مكتملة راجع `examples/`.
167
+
168
+ **لإنشاء المستودع ونشر الحزمة بأمان عبر GitHub Actions وTrusted Publishing (بدون حفظ مفتاح API)، راجع [دليل النشر العربي](docs/PUBLISH_AR.md).**
169
+
170
+ ## نشر الحزمة على TestPyPI وPyPI باستخدام GitHub Actions
171
+
172
+ هذه المكتبة **غير منشورة بعد** على PyPI أو TestPyPI؛ لا تستخدم أمر `pip install khaled-dsml` من PyPI إلى حين نشرها.
173
+
174
+ 1. تابع نتائج الاختبارات والبناء في [GitHub Actions](https://github.com/noble2013/KhaledDSML/actions).
175
+ 2. أنشئ حسابًا منفصلًا على [TestPyPI](https://test.pypi.org/account/register/) وبيئة `testpypi` في `Settings → Environments` بمستودع GitHub.
176
+ 3. أضف في [TestPyPI Trusted Publishing](https://test.pypi.org/manage/account/publishing/) ناشرًا معلقًا للاسم `khaled-dsml` ومعلومات المستودع `noble2013/KhaledDSML`، وملف `testpypi.yml`، والبيئة `testpypi`.
177
+ 4. بعد نجاح البناء، نفذ `Actions → Publish to TestPyPI (manual) → Run workflow`.
178
+ 5. بعد نجاح التجربة، أنشئ البيئة `pypi` وناشرًا موثوقًا للاسم نفسه على [PyPI](https://pypi.org/manage/account/publishing/) باستخدام `release.yml` والبيئة `pypi`، ثم انشر GitHub Release يحمل الوسم `v0.1.0`.
179
+
180
+ يوصى بحماية بيئة `pypi` بمراجعة يدوية. لا تضع كلمات مرور أو رموز API داخل GitHub. اقرأ [دليل النشر العربي](docs/PUBLISH_AR.md) للمزيد.
181
+
182
+ ## Notes (English)
183
+
184
+ KhaledDSML is an **educational** Python toolkit; it wraps proven NumPy, pandas, SciPy and scikit-learn primitives rather than reinventing statistical algorithms. Optional statistics are backed by statsmodels. It is not a distributed processing engine: the MapReduce example runs locally; Optional Dask and Spark adapters are included, but need real engines and are not validated on a Spark cluster in version 0.1.0. Do not fit preprocessing on test data. Hypothesis-test interpretations should be checked against sampling design and assumptions. An MIT license covers this original wrapper code, not a claim of authorship over third-party algorithms.
185
+
186
+ ## حقوق الاستخدام
187
+
188
+ الترخيص MIT. الاسم المقترح للنشر هو `khaled-dsml` (يلزم التحقق من توفره عند رفع الحزمة). كل الإضافات يمكن مراجعتها عبر اختبارات `pytest` قبل النشر. حقوق المنهجيات والخوارزميات الأساسية محفوظة لمؤلفيها ومشروعاتها الأصلية.
@@ -0,0 +1,19 @@
1
+ khaled_dsml/__init__.py,sha256=iRLVRN4MK57sdy9y2gEktx_p-TZAUL3-BxU_lbfKGk8,1026
2
+ khaled_dsml/_results.py,sha256=ZIMEhQwUu7OxfLBp7FG9-IDinQOVoc23ys3QT8X9RnA,1357
3
+ khaled_dsml/bigdata.py,sha256=8i1wUjgtRk3vZJu7B-MuBOzMVVTG5ifRCdHSQ7Tp3TE,4982
4
+ khaled_dsml/cli.py,sha256=kXLfODk_fozZc2nSdDSusGpYHUIGZeXcxPXhSM49cNk,1557
5
+ khaled_dsml/distributed.py,sha256=3yoe84VQUOyRJYR4i0HajcOn7t9PbG3zDkLSzlQthp8,1565
6
+ khaled_dsml/econometrics.py,sha256=nMFLh2sC3K6xHbZPdSDISo1fbVSCugDlAIm3H4JqseE,3753
7
+ khaled_dsml/ml.py,sha256=_9lPEl9K37vHZWMyi64MUdEaNwxeRtpgNdbUNZuQhGE,7144
8
+ khaled_dsml/plots.py,sha256=o3Tvc-logeaZNtndQIVd8t4M84lbuH2pLSU33omexUE,1118
9
+ khaled_dsml/quality.py,sha256=iO-7UH0aPdQD0tv91HXxjquNrLYlALRt5i1t13Jho6o,4513
10
+ khaled_dsml/quizzes.py,sha256=6qUjsu7WuHHoPFSdFBcOdx_3BSRFUAM03-Q1RbmkCUM,8578
11
+ khaled_dsml/sqltools.py,sha256=DMgh7iq_Q22BIyHMi6ZPHSFjVV3H9aOXzNdS22evOQg,1580
12
+ khaled_dsml/statistics.py,sha256=jjMlLFw-br97xv762Q0gGb3LZGe4cNIVQdW1mdbtwiA,7200
13
+ khaled_dsml/timeseries.py,sha256=CNl0xmPf8hUCafXAx4qJZnT_JKraRidjdoZ57GLwnM0,988
14
+ khaled_dsml-0.1.0.dist-info/licenses/LICENSE,sha256=f7as3Fpf2T2Z9GAKUuCt7dzd2fdQCh8FldlmMGs3Hx4,1067
15
+ khaled_dsml-0.1.0.dist-info/METADATA,sha256=GnWw6sPdwAzA8qHahFv6K3IShbTe10Y8GTJbZqqDrf4,11102
16
+ khaled_dsml-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
17
+ khaled_dsml-0.1.0.dist-info/entry_points.txt,sha256=igTVGou4rpnn0Ik3oZKnB4REyvSzNh1QelHldW5Mqkk,53
18
+ khaled_dsml-0.1.0.dist-info/top_level.txt,sha256=d9Gk3AokLxRE8eFeC417BYZBC3JmSBxWF1denSkt5fA,12
19
+ khaled_dsml-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ khaled-dsml = khaled_dsml.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Khaled Ali
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ khaled_dsml