Repository: JanSkn/machine-learning-library

Issue:
Add train_test_split and update the examples
**Is your feature request related to a problem? Please describe.**
So far, there is no possibility to easily split the data into training and test data (and validation data)

**Describe the solution you'd like**
Add a function in `pylearn/model_utils.py` called *train_test_split* (from SCRATCH!) similar to the one from scikit-learn

Parameters: train_size: float (between 0 and 1), shuffle: bool, random_state: int

**Describe alternatives you've considered**
/

**Additional context**
once you have done this, add it to `pylearn/__init__.py` and update the examples by splitting the data, training and testing on the test data

Discussion:

Solution patch:
diff --git a/docs/conf.py b/docs/conf.py
index 51798bc..ffaa5fc 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -16,9 +16,9 @@
 import sphinx_theme_pd
 
 project = 'PyLearn'
-copyright = '2023, Jan Skowron'
+copyright = '2024, Jan Skowron'
 author = 'Jan Skowron'
-release = '1.1.0'
+release = '1.2.0'
 
 # -- General configuration ---------------------------------------------------
 # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
diff --git a/docs/custom/usage.rst b/docs/custom/usage.rst
index 7ee86c6..f3cb86c 100644
--- a/docs/custom/usage.rst
+++ b/docs/custom/usage.rst
@@ -99,6 +99,39 @@ Functions for normalization:
 |
 |
 
+Train and test Data
+------------------
+
+You can split your data into train and test and shuffle the data:
+
+.. code-block:: python
+
+    pl.train_test_split(X, y, test_size, shuffle, random_seed)
+
+|
+
+**Example**
+
+.. code-block:: python
+
+    X = np.array([[1, 2], [3, 4], [5, 6], [7, 8], [9, 10]])
+    y = np.array([0, 1, 0, 1, 0])
+
+    X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_seed=42)
+
+    print("Training Features:")
+    print(X_train)
+    print("Training Labels:")
+    print(y_train)
+
+    print("Test Features:")
+    print(X_test)
+    print("Test Labels:")
+    print(y_test)
+
+|
+|
+
 Evaluation Metrics
 ------------------
 
diff --git a/docs/custom/version.rst b/docs/custom/version.rst
index a4f5da6..862e9f4 100644
--- a/docs/custom/version.rst
+++ b/docs/custom/version.rst
@@ -11,4 +11,10 @@ Release notes
 
 - Classification: New classification algorithm Multinomial Naive Bayes (MNB).
 - Clustering: New clustering algorithm Gaussian Mixture Model (GMM).
-- New model evaluation with Mean Squared Error (MSE) and Sum of Squared Errors (SSE).
\ No newline at end of file
+- New model evaluation with Mean Squared Error (MSE) and Sum of Squared Errors (SSE).
+
+|
+
+**Version 1.2.0 (2024-11-06):**
+
+- New feature for your data: Train Test Split
\ No newline at end of file
diff --git a/examples/breast_cancer.py b/examples/breast_cancer.py
index 080c5a2..5c6600f 100644
--- a/examples/breast_cancer.py
+++ b/examples/breast_cancer.py
@@ -9,17 +9,20 @@
 
 import pandas as pd
 import numpy as np
-from pylearn import GaussianNaiveBayes, accuracy, precision, recall, f1_score
+from pylearn import GaussianNaiveBayes, accuracy, precision, recall, f1_score, train_test_split
 
 # Due to the short training duration, this example doesn't store and load the trained model and does training again every execution
 
-data = pd.read_csv("examples/data/breast_cancer_data.csv")
+data = pd.read_csv("data/breast_cancer_data.csv")
 
-x_train, x_test = data.iloc[:, :5], data.iloc[:25, :5]
-y_train, y_test = pd.DataFrame(data.iloc[:, -1]), pd.DataFrame(data.iloc[:25, -1])
+X = data.iloc[:, :5].values
+y = data.iloc[:, -1]
+
+x_train, x_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_seed=42)
+x_train, x_test, y_train, y_test = pd.DataFrame(x_train), pd.DataFrame(x_test), pd.DataFrame(y_train), pd.DataFrame(y_test)
 
 nb = GaussianNaiveBayes()
-nb.fit(x_train, y_train)  
+nb.fit(x_train, y_train)
 prediction = nb.predict(x_test)
 prediction = pd.DataFrame(prediction)
 prediction.columns = ["prediction"]
diff --git a/examples/cities.py b/examples/cities.py
index 8c0cfb6..17a9352 100644
--- a/examples/cities.py
+++ b/examples/cities.py
@@ -7,14 +7,13 @@
 parent = os.path.dirname(current)
 sys.path.append(parent)
 
-import numpy as np
 import pandas as pd
 import matplotlib.pyplot as plt
 from pylearn import KMeans, KMedoids, GaussianMixture
 
 # Due to the short training duration, this example doesn't store and load the trained model and does training again every execution
 
-cities = pd.read_csv("examples/data/city_coordinates.csv")
+cities = pd.read_csv("data/city_coordinates.csv")
 city_coordinates = cities[["longitude", "latitude"]].values
 
 plt.figure(figsize=(10, 8)) 
diff --git a/examples/fake_news.py b/examples/fake_news.py
index 46e02db..c815d92 100644
--- a/examples/fake_news.py
+++ b/examples/fake_news.py
@@ -9,12 +9,16 @@
 
 import pandas as pd
 import numpy as np
-from pylearn import MultinomialNaiveBayes, accuracy, precision, recall, f1_score
+from pylearn import MultinomialNaiveBayes, accuracy, precision, recall, f1_score, train_test_split
 
-data = pd.read_csv("examples/data/fake_news.csv")
+data = pd.read_csv("data/fake_news.csv")
 data = data[["title", "real"]]      # remove unneccessary columns
-x_train, x_test = data["title"][:4000], data["title"][4000:]
-y_train, y_test = data["real"][:4000], pd.DataFrame(data["real"][4000:])        
+
+X = data["title"]
+y = data["real"]
+
+x_train, x_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_seed=42)
+x_train, x_test, y_train, y_test = pd.Series(x_train), pd.Series(x_test), pd.Series(y_train), pd.Series(y_test)
 
 nb = MultinomialNaiveBayes()
 nb.fit(x_train, y_train)
diff --git a/examples/mnist_predict.py b/examples/mnist_predict.py
index 03b1245..eaedd34 100644
--- a/examples/mnist_predict.py
+++ b/examples/mnist_predict.py
@@ -14,7 +14,7 @@
 
 network = load("mnist.pkl")
 
-(_, _), (x_test, y_test) = load("examples/data/mnist.npy")
+(_, _), (x_test, y_test) = load("data/mnist.npy")
 
 x_test, y_test = adjust_data(x_test, y_test, testing_data_size)
 
diff --git a/examples/mnist_train.py b/examples/mnist_train.py
index 9049824..539d22c 100644
--- a/examples/mnist_train.py
+++ b/examples/mnist_train.py
@@ -12,7 +12,7 @@
 
 # load training data
 # x_train: image of 28 x 28 pixels, y_train: output of the number (0-9)
-(x_train, y_train), (_, _) = load("examples/data/mnist.npy")
+(x_train, y_train), (_, _) = load("data/mnist.npy")
 
 input_length = 784          # 28 x 28
 training_data_size = 10000
diff --git a/pylearn/__init__.py b/pylearn/__init__.py
index 91123dd..aec2774 100644
--- a/pylearn/__init__.py
+++ b/pylearn/__init__.py
@@ -5,6 +5,7 @@
                           min_max_normalization,
                           z_normalization,
                           to_one_hot,
+                          train_test_split,
                           save, 
                           load,
                           mean_squared_error,
diff --git a/pylearn/model_utils.py b/pylearn/model_utils.py
index f6da96f..92db2fa 100644
--- a/pylearn/model_utils.py
+++ b/pylearn/model_utils.py
@@ -1,5 +1,4 @@
 import numpy as np
-import pandas as pd
 import dill
 from typing import BinaryIO
 
@@ -203,6 +202,39 @@ def to_one_hot(y: np.ndarray, num_of_classes=10) -> np.ndarray:
     
     return one_hot
 
+def train_test_split(X: np.ndarray, y: np.ndarray, test_size=0.2, shuffle=True, random_seed=None) -> np.ndarray:
+    """
+    Splits data into training and test sets.
+
+    Parameters:
+        :X (numpy.ndarray): List of feature lists.
+        :y (numpy.ndarray): List of labels.
+        :test_size (float, optional): Fraction of data to use as test data, default: 0.2
+        :shuffle (bool, optional): Whether to shuffle data before splitting, default: True
+        :random_seed (int, optional): Seed for reproducibility of shuffling, default: None
+
+    Returns:
+        Tuple of numpy arrays: (X_train, X_test, y_train, y_test) where X_train and X_test are the feature arrays, and y_train and y_test are the label arrays.
+    """
+    if random_seed is not None:
+        np.random.seed(random_seed)
+
+    # zip to shuffle X and y together
+    data = list(zip(X, y))
+
+    if shuffle:
+        np.random.shuffle(data)
+
+    test_len = int(len(data) * test_size)
+
+    train_data = data[test_len:]
+    test_data = data[:test_len]
+
+    X_test, y_test = zip(*test_data)
+    X_train, y_train = zip(*train_data)
+
+    return np.array(X_train), np.array(X_test), np.array(y_train), np.array(y_test)
+
 def save(file_name: str, model: object) -> None:
     """
     Saves the trained model to the storage.
diff --git a/setup.py b/setup.py
index e5703cb..4cd6d27 100644
--- a/setup.py
+++ b/setup.py
@@ -42,7 +42,7 @@
 
 setup(
     name="pylearn-ml",
-    version="1.1.0",      
+    version="1.2.0",
     author="Jan Skowron",
     url="https://github.com/JanSkn/machine-learning-library", 
     long_description=long_description,


Test patch:
diff --git a/tests/test_model_utils.py b/tests/test_model_utils.py
index cb1e979..08651e1 100644
--- a/tests/test_model_utils.py
+++ b/tests/test_model_utils.py
@@ -1,5 +1,5 @@
 import numpy as np
-from pylearn import accuracy, precision, recall, f1_score, mean_squared_error, sum_of_squared_errors, min_max_normalization, z_normalization
+from pylearn import accuracy, precision, recall, f1_score, mean_squared_error, sum_of_squared_errors, min_max_normalization, z_normalization, train_test_split
 
 def test_accuracy():
     y_true = np.array([1, 2, 1, 2])
@@ -77,4 +77,20 @@ def test_z_normalization():
     normalized_data = z_normalization(data)
 
     expected_normalized_data = np.array([[-1, -1], [1, 1]])
-    np.testing.assert_array_almost_equal(normalized_data, expected_normalized_data, decimal=2)
\ No newline at end of file
+    np.testing.assert_array_almost_equal(normalized_data, expected_normalized_data, decimal=2)
+
+def test_train_test_split():
+    X = np.array([[1, 2], [3, 4], [5, 6], [7, 8], [9, 10]])
+    y = np.array([0, 1, 0, 1, 0])
+
+    X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.4, shuffle=False)
+
+    expected_X_train = np.array([[5, 6], [7, 8], [9, 10]])
+    expected_X_test = np.array([[1, 2], [3, 4]])
+    expected_y_train = np.array([0, 1, 0])
+    expected_y_test = np.array([0, 1])
+
+    np.testing.assert_array_equal(X_train, expected_X_train)
+    np.testing.assert_array_equal(X_test, expected_X_test)
+    np.testing.assert_array_equal(y_train, expected_y_train)
+    np.testing.assert_array_equal(y_test, expected_y_test)
\ No newline at end of file