Q- given this tutor 
“Certainly! Let's develop the `preprocessing.py` module based on your project structure and the preprocessing steps you've outlined in your notebook. We'll follow best practices, ensuring clean code, modularity, scalability, robust error handling, and thorough documentation.

### Project Structure Recap

Your initial project structure is well-organized. Here's a quick reminder of the relevant parts:

```
MLOPS-TELCO-CHURN/
├── app/
│   ├── api/
│   └── services/
│       ├── __init__.py
│       ├── feature_engineering.py
│       ├── load_data.py
│       ├── model_training.py
│       └── preprocessing.py
├── tests/
├── utils/
│   ├── __init__.py
│   ├── config.py
│   └── logger.py
├── config/
│   └── config.yaml
├── data/
│   └── raw/
│       └── data.csv
├── models/
├── notebooks/
├── docs/
├── scripts/
├── Dockerfile
├── docker-compose.yml
├── .env
├── .gitignore
├── README.md
└── main.py
```

### Developing `preprocessing.py`

We'll create a `Preprocessor` class within `app/services/preprocessing.py` that handles all preprocessing steps, including:

- Label Encoding for specified categorical columns.
- One-Hot Encoding for other categorical columns.
- Scaling numerical features using `StandardScaler`.
- Saving and Loading the preprocessing pipeline for consistency across training and testing.

#### Step-by-Step Implementation

##### Import Necessary Libraries

```python
# app/services/preprocessing.py

"""
preprocessing.py

This module provides functionality to preprocess datasets,
including categorical encoding and numerical scaling.

It handles configuration loading, logging setup, and applies
the specified preprocessing steps to the data.
"""

import pandas as pd
import logging
from typing import List, Optional, Dict, Any
from sklearn.preprocessing import StandardScaler, OneHotEncoder, OrdinalEncoder
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from joblib import dump, load
from pathlib import Path

from app.utils.config import load_config
from app.utils.logger import setup_logger
```

##### Define the `Preprocessor` Class

```python
class Preprocessor:
    def __init__(self, config: Optional[Dict[str, Any]] = None, logger: Optional[logging.Logger] = None):
        """
        Initializes the Preprocessor with configuration and logger.

        Parameters:
        - config (dict, optional): Configuration dictionary.
          If None, loads configuration from config file.
        - logger (logging.Logger, optional): Logger instance.
          If None, sets up a new logger based on the configuration.
        """
        if config is None:
            config = load_config()
        if logger is None:
            logger = setup_logger(config.get('logging', {}))
        self.config = config
        self.logger = logger

        # Extract preprocessing configurations
        preprocessing_config = config.get('preprocessing', {})
        self.num_cols: List[str] = preprocessing_config.get('num_cols', [])
        self.cat_cols_ohe: List[str] = preprocessing_config.get('cat_cols_ohe', [])
        self.cat_cols_le: List[str] = preprocessing_config.get('cat_cols_le', [])

        # Initialize scaler
        scaler_type = config.get('scaler', {}).get('type', 'StandardScaler')
        if scaler_type == 'StandardScaler':
            self.scaler = StandardScaler()
        else:
            self.logger.error(f"Unsupported scaler type: {scaler_type}")
            raise ValueError(f"Unsupported scaler type: {scaler_type}")

        # Initialize encoders
        self.ordinal_encoder = OrdinalEncoder()
        self.onehot_encoder = OneHotEncoder(handle_unknown='ignore')

        # Create the preprocessing pipeline
        self.transformer = self._create_transformer()

    def _create_transformer(self) -> ColumnTransformer:
        """
        Creates a ColumnTransformer with appropriate transformers for each column type.

        Returns:
        - transformer (ColumnTransformer): The column transformer.
        """
        transformers = []

        if self.num_cols:
            transformers.append(
                ('num', self.scaler, self.num_cols)
            )

        if self.cat_cols_le:
            transformers.append(
                ('cat_le', self.ordinal_encoder, self.cat_cols_le)
            )

        if self.cat_cols_ohe:
            transformers.append(
                ('cat_ohe', self.onehot_encoder, self.cat_cols_ohe)
            )

        column_transformer = ColumnTransformer(
            transformers=transformers,
            remainder='drop'  # Drop columns not specified in transformers
        )

        return column_transformer

    def fit_transform(self, df: pd.DataFrame) -> pd.DataFrame:
        """
        Fits the transformers on the training data and transforms it.

        Parameters:
        - df (pd.DataFrame): Training dataframe.

        Returns:
        - df_transformed (pd.DataFrame): Transformed dataframe.
        """
        self.logger.info("Starting fit_transform on preprocessing.")
        try:
            transformed_array = self.transformer.fit_transform(df)

            # Generate feature names after transformation
            transformed_columns = []
            for name, transformer, cols in self.transformer.transformers_:
                if transformer == 'drop':
                    continue
                elif name == 'num':
                    transformed_columns.extend(cols)
                elif name == 'cat_le':
                    transformed_columns.extend(cols)
                elif name == 'cat_ohe':
                    ohe_features = self.transformer.named_transformers_[name].get_feature_names_out(cols)
                    transformed_columns.extend(ohe_features)

            df_transformed = pd.DataFrame(transformed_array, columns=transformed_columns, index=df.index)
            self.logger.info("Finished fit_transform on preprocessing.")
            return df_transformed
        except Exception as e:
            self.logger.exception("Error during fit_transform.")
            raise

    def transform(self, df: pd.DataFrame) -> pd.DataFrame:
        """
        Transforms the data using the already fitted transformers.

        Parameters:
        - df (pd.DataFrame): Dataframe to transform.

        Returns:
        - df_transformed (pd.DataFrame): Transformed dataframe.
        """
        self.logger.info("Starting transform on preprocessing.")
        try:
            transformed_array = self.transformer.transform(df)

            # Generate feature names after transformation
            transformed_columns = []
            for name, transformer, cols in self.transformer.transformers_:
                if transformer == 'drop':
                    continue
                elif name == 'num':
                    transformed_columns.extend(cols)
                elif name == 'cat_le':
                    transformed_columns.extend(cols)
                elif name == 'cat_ohe':
                    ohe_features = self.transformer.named_transformers_[name].get_feature_names_out(cols)
                    transformed_columns.extend(ohe_features)

            df_transformed = pd.DataFrame(transformed_array, columns=transformed_columns, index=df.index)
            self.logger.info("Finished transform on preprocessing.")
            return df_transformed
        except Exception as e:
            self.logger.exception("Error during transform.")
            raise

    def save_transformer(self, path: Optional[str] = None):
        """
        Saves the fitted transformer to disk.

        Parameters:
        - path (str, optional): File path to save the transformer.
          If None, uses 'models/preprocessor.joblib' from configuration.
        """
        if path is None:
            path = self.config.get('models', {}).get('preprocessor_path', 'models/preprocessor.joblib')
        path = Path(path)
        path.parent.mkdir(parents=True, exist_ok=True)
        try:
            dump(self.transformer, path)
            self.logger.info(f"Transformer saved to {path}.")
        except Exception as e:
            self.logger.exception("Failed to save transformer.")
            raise

    def load_transformer(self, path: Optional[str] = None):
        """
        Loads the transformer from disk.

        Parameters:
        - path (str, optional): File path to load the transformer from.
          If None, uses 'models/preprocessor.joblib' from configuration.
        """
        if path is None:
            path = self.config.get('models', {}).get('preprocessor_path', 'models/preprocessor.joblib')
        path = Path(path)
        if not path.exists():
            self.logger.error(f"Transformer file not found at {path}.")
            raise FileNotFoundError(f"Transformer file not found at {path}.")
        try:
            self.transformer = load(path)
            self.logger.info(f"Transformer loaded from {path}.")
        except Exception as e:
            self.logger.exception("Failed to load transformer.")
            raise
```

##### Configuration Enhancements

Ensure your `config/config.yaml` includes paths for saving the preprocessor:

```yaml
# config/config.yaml

data:
  file_path: 'data/raw/data.csv'  # Path to the raw data CSV file
  target_column: 'Churn'          # Target variable for prediction
  test_size: 0.3                   # Proportion of data for testing
  random_state: 40                 # Random state for reproducibility
  feature_columns:  # List of feature columns if needed elsewhere
    - 'gender'
    - 'SeniorCitizen'
    - 'Partner'
    - 'Dependents'
    - 'tenure'
    - 'PhoneService'
    - 'MultipleLines'
    - 'InternetService'
    - 'OnlineSecurity'
    - 'OnlineBackup'
    - 'DeviceProtection'
    - 'TechSupport'
    - 'StreamingTV'
    - 'StreamingMovies'
    - 'Contract'
    - 'PaperlessBilling'
    - 'PaymentMethod'
    - 'MonthlyCharges'
    - 'TotalCharges'

preprocessing:
  num_cols:
    - 'tenure'
    - 'MonthlyCharges'
    - 'TotalCharges'
  cat_cols_ohe:
    - 'PaymentMethod'
    - 'Contract'
    - 'InternetService'
  cat_cols_le:
    - 'gender'
    - 'Dependents'
    - 'PhoneService'
    - 'MultipleLines'
    - 'OnlineSecurity'
    - 'OnlineBackup'
    - 'DeviceProtection'
    - 'TechSupport'
    - 'StreamingTV'
    - 'StreamingMovies'
    - 'PaperlessBilling'
    - 'SeniorCitizen'

scaler:
  type: 'StandardScaler'

models:
  preprocessor_path: 'models/preprocessor.joblib'

logging:
  level: 'INFO'
  format: '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
```

##### Using the Preprocessor in Your Workflow

Here's how you can integrate the `Preprocessor` into your workflow, similar to your notebook steps.

```python
# main.py or any other appropriate script/module

from app.services.data_loader import load_data
from app.services.preprocessing import Preprocessor
from app.utils.config import load_config
from app.utils.logger import setup_logger

def main():
    # Load configuration and logger
    config = load_config()
    logger = setup_logger(config.get('logging', {}))

    # Load data
    df = load_data()

    # Initialize Preprocessor
    preprocessor = Preprocessor(config=config, logger=logger)

    # Fit and transform the training data
    df_train_transformed = preprocessor.fit_transform(df)

    # Save the preprocessor
    preprocessor.save_transformer()

    # (Optional) Transform the test data
    # Assuming you have split the data into X_train and X_test
    # X_test_transformed = preprocessor.transform(X_test)

if __name__ == "__main__":
    main()
```

##### Example Usage with Train-Test Split

Suppose you have split your data into training and testing sets, here's how you can apply preprocessing:

```python
# app/services/preprocessing.py

def preprocess_data():
    # Load configuration and logger
    config = load_config()
    logger = setup_logger(config.get('logging', {}))

    # Load data
    df = load_data()

    # Split data
    from sklearn.model_selection import train_test_split
    X = df.drop(columns=['Churn'])
    y = df['Churn'].values

    X_train, X_test, y_train, y_test = train_test_split(
        X, y,
        test_size=config['data']['test_size'],
        random_state=config['data']['random_state'],
        stratify=y
    )

    logger.info(f"Training data shape: {X_train.shape}")
    logger.info(f"Testing data shape: {X_test.shape}")

    # Initialize Preprocessor
    preprocessor = Preprocessor(config=config, logger=logger)

    # Fit and transform training data
    X_train_transformed = preprocessor.fit_transform(X_train)

    # Transform testing data
    X_test_transformed = preprocessor.transform(X_test)

    # Save the preprocessor
    preprocessor.save_transformer()

    return X_train_transformed, X_test_transformed, y_train, y_test
```

##### Error Handling and Logging

The `Preprocessor` class includes comprehensive error handling and logging to ensure that any issues during preprocessing are captured and logged appropriately. This is crucial for debugging and maintaining the pipeline.

##### Testing `preprocessing.py`

Create unit tests to ensure that your preprocessing steps work as expected. Here's an example test case using `unittest` and `unittest.mock`:

```python
# tests/test_preprocessing.py

import unittest
from unittest.mock import patch, MagicMock
import pandas as pd
from app.services.preprocessing import Preprocessor

class TestPreprocessor(unittest.TestCase):
    @patch('app.services.preprocessing.load_config')
    @patch('app.services.preprocessing.setup_logger')
    def test_fit_transform(self, mock_setup_logger, mock_load_config):
        # Mock configuration
        mock_config = {
            'preprocessing': {
                'num_cols': ['tenure', 'MonthlyCharges', 'TotalCharges'],
                'cat_cols_ohe': ['PaymentMethod', 'Contract', 'InternetService'],
                'cat_cols_le': ['gender', 'Dependents']
            },
            'scaler': {'type': 'StandardScaler'},
            'models': {'preprocessor_path': 'models/preprocessor.joblib'},
            'logging': {'level': 'INFO', 'format': '%(asctime)s - %(name)s - %(levelname)s - %(message)s'}
        }
        mock_load_config.return_value = mock_config
        mock_logger = MagicMock()
        mock_setup_logger.return_value = mock_logger

        # Sample data
        data = {
            'gender': ['Male', 'Female', 'Female'],
            'SeniorCitizen': [0, 1, 0],
            'Partner': ['Yes', 'No', 'Yes'],
            'Dependents': ['No', 'Yes', 'No'],
            'tenure': [5, 3, 8],
            'PhoneService': ['Yes', 'No', 'Yes'],
            'MultipleLines': ['No', 'No', 'Yes'],
            'InternetService': ['DSL', 'Fiber optic', 'DSL'],
            'OnlineSecurity': ['No', 'Yes', 'No'],
            'OnlineBackup': ['Yes', 'No', 'Yes'],
            'DeviceProtection': ['No', 'Yes', 'No'],
            'TechSupport': ['No', 'Yes', 'No'],
            'StreamingTV': ['No', 'Yes', 'No'],
            'StreamingMovies': ['No', 'Yes', 'No'],
            'Contract': ['Month-to-month', 'One year', 'Two year'],
            'PaperlessBilling': ['Yes', 'No', 'Yes'],
            'PaymentMethod': ['Electronic check', 'Mailed check', 'Electronic check'],
            'MonthlyCharges': [29.85, 56.95, 53.85],
            'TotalCharges': [29.85, 1889.5, 108.15]
        }
        df = pd.DataFrame(data)

        # Initialize Preprocessor
        preprocessor = Preprocessor(config=mock_config, logger=mock_logger)

        # Fit and transform
        df_transformed = preprocessor.fit_transform(df)

        # Assertions
        self.assertEqual(df_transformed.shape, (3, 12))  # Adjust based on number of features after transformation
        mock_logger.info.assert_any_call("Starting fit_transform on preprocessing.")
        mock_logger.info.assert_any_call("Finished fit_transform on preprocessing.")

    # Additional test cases can be added here

if __name__ == '__main__':
    unittest.main()
```

##### Final `preprocessing.py` Module

Here's the complete `preprocessing.py` module incorporating all the above steps:

```python
# app/services/preprocessing.py

"""
preprocessing.py

This module provides functionality to preprocess datasets,
including categorical encoding and numerical scaling.

It handles configuration loading, logging setup, and applies
the specified preprocessing steps to the data.
"""

import pandas as pd
import logging
from typing import List, Optional, Dict, Any
from sklearn.preprocessing import StandardScaler, OneHotEncoder, OrdinalEncoder
from sklearn.compose import ColumnTransformer
from joblib import dump, load
from pathlib import Path

from app.utils.config import load_config
from app.utils.logger import setup_logger

class Preprocessor:
    def __init__(self, config: Optional[Dict[str, Any]] = None, logger: Optional[logging.Logger] = None):
        """
        Initializes the Preprocessor with configuration and logger.

        Parameters:
        - config (dict, optional): Configuration dictionary.
          If None, loads configuration from config file.
        - logger (logging.Logger, optional): Logger instance.
          If None, sets up a new logger based on the configuration.
        """
        if config is None:
            config = load_config()
        if logger is None:
            logger = setup_logger(config.get('logging', {}))
        self.config = config
        self.logger = logger

        # Extract preprocessing configurations
        preprocessing_config = config.get('preprocessing', {})
        self.num_cols: List[str] = preprocessing_config.get('num_cols', [])
        self.cat_cols_ohe: List[str] = preprocessing_config.get('cat_cols_ohe', [])
        self.cat_cols_le: List[str] = preprocessing_config.get('cat_cols_le', [])

        # Initialize scaler
        scaler_type = config.get('scaler', {}).get('type', 'StandardScaler')
        if scaler_type == 'StandardScaler':
            self.scaler = StandardScaler()
        else:
            self.logger.error(f"Unsupported scaler type: {scaler_type}")
            raise ValueError(f"Unsupported scaler type: {scaler_type}")

        # Initialize encoders
        self.ordinal_encoder = OrdinalEncoder()
        self.onehot_encoder = OneHotEncoder(handle_unknown='ignore')

        # Create the preprocessing pipeline
        self.transformer = self._create_transformer()

    def _create_transformer(self) -> ColumnTransformer:
        """
        Creates a ColumnTransformer with appropriate transformers for each column type.

        Returns:
        - transformer (ColumnTransformer): The column transformer.
        """
        transformers = []

        if self.num_cols:
            transformers.append(
                ('num', self.scaler, self.num_cols)
            )

        if self.cat_cols_le:
            transformers.append(
                ('cat_le', self.ordinal_encoder, self.cat_cols_le)
            )

        if self.cat_cols_ohe:
            transformers.append(
                ('cat_ohe', self.onehot_encoder, self.cat_cols_ohe)
            )

        column_transformer = ColumnTransformer(
            transformers=transformers,
            remainder='drop'  # Drop columns not specified in transformers
        )

        return column_transformer

    def fit_transform(self, df: pd.DataFrame) -> pd.DataFrame:
        """
        Fits the transformers on the training data and transforms it.

        Parameters:
        - df (pd.DataFrame): Training dataframe.

        Returns:
        - df_transformed (pd.DataFrame): Transformed dataframe.
        """
        self.logger.info("Starting fit_transform on preprocessing.")
        try:
            transformed_array = self.transformer.fit_transform(df)

            # Generate feature names after transformation
            transformed_columns = []
            for name, transformer, cols in self.transformer.transformers_:
                if transformer == 'drop':
                    continue
                elif name == 'num':
                    transformed_columns.extend(cols)
                elif name == 'cat_le':
                    transformed_columns.extend(cols)
                elif name == 'cat_ohe':
                    ohe_features = self.transformer.named_transformers_[name].get_feature_names_out(cols)
                    transformed_columns.extend(ohe_features)

            df_transformed = pd.DataFrame(transformed_array, columns=transformed_columns, index=df.index)
            self.logger.info("Finished fit_transform on preprocessing.")
            return df_transformed
        except Exception as e:
            self.logger.exception("Error during fit_transform.")
            raise

    def transform(self, df: pd.DataFrame) -> pd.DataFrame:
        """
        Transforms the data using the already fitted transformers.

        Parameters:
        - df (pd.DataFrame): Dataframe to transform.

        Returns:
        - df_transformed (pd.DataFrame): Transformed dataframe.
        """
        self.logger.info("Starting transform on preprocessing.")
        try:
            transformed_array = self.transformer.transform(df)

            # Generate feature names after transformation
            transformed_columns = []
            for name, transformer, cols in self.transformer.transformers_:
                if transformer == 'drop':
                    continue
                elif name == 'num':
                    transformed_columns.extend(cols)
                elif name == 'cat_le':
                    transformed_columns.extend(cols)
                elif name == 'cat_ohe':
                    ohe_features = self.transformer.named_transformers_[name].get_feature_names_out(cols)
                    transformed_columns.extend(ohe_features)

            df_transformed = pd.DataFrame(transformed_array, columns=transformed_columns, index=df.index)
            self.logger.info("Finished transform on preprocessing.")
            return df_transformed
        except Exception as e:
            self.logger.exception("Error during transform.")
            raise

    def save_transformer(self, path: Optional[str] = None):
        """
        Saves the fitted transformer to disk.

        Parameters:
        - path (str, optional): File path to save the transformer.
          If None, uses 'models/preprocessor.joblib' from configuration.
        """
        if path is None:
            path = self.config.get('models', {}).get('preprocessor_path', 'models/preprocessor.joblib')
        path = Path(path)
        path.parent.mkdir(parents=True, exist_ok=True)
        try:
            dump(self.transformer, path)
            self.logger.info(f"Transformer saved to {path}.")
        except Exception as e:
            self.logger.exception("Failed to save transformer.")
            raise

    def load_transformer(self, path: Optional[str] = None):
        """
        Loads the transformer from disk.

        Parameters:
        - path (str, optional): File path to load the transformer from.
          If None, uses 'models/preprocessor.joblib' from configuration.
        """
        if path is None:
            path = self.config.get('models', {}).get('preprocessor_path', 'models/preprocessor.joblib')
        path = Path(path)
        if not path.exists():
            self.logger.error(f"Transformer file not found at {path}.")
            raise FileNotFoundError(f"Transformer file not found at {path}.")
        try:
            self.transformer = load(path)
            self.logger.info(f"Transformer loaded from {path}.")
        except Exception as e:
            self.logger.exception("Failed to load transformer.")
            raise
```

### Explanation and Best Practices

- **Modular Design**:
  - **Separation of Concerns**: The `Preprocessor` class handles all preprocessing tasks, keeping it separate from data loading and model training.
  - **Reusability**: The class can be reused across different parts of the project or even in other projects.

- **Configuration Management**:
  - **Dynamic Configuration Loading**: The class accepts a configuration dictionary, allowing flexibility in specifying preprocessing parameters.
  - **Default Configurations**: If no configuration is provided, it loads from `config.yaml`.

- **Logging**:
  - **Centralized Logging**: All significant actions and errors are logged, aiding in debugging and monitoring.
  - **Informative Logs**: Start and end of major steps (`fit_transform`, `transform`) are logged.

- **Error Handling**:
  - **Comprehensive Exception Handling**: Any errors during transformation steps are caught, logged, and re-raised to prevent silent failures.
  - **Validation of Configurations**: Unsupported scaler types or missing transformer files lead to explicit error messages.

- **Scalability and Maintainability**:
  - **Flexible Transformer Pipeline**: Using `ColumnTransformer` allows easy addition or removal of preprocessing steps.
  - **Future Extensions**: Additional preprocessors (e.g., feature selection) can be integrated into the pipeline seamlessly.

- **Testing**:
  - **Mocking Dependencies**: The provided test case shows how to mock configuration and logger, facilitating isolated unit tests.
  - **Coverage of Scenarios**: Ensure tests cover various scenarios, including successful transformations and handling of erroneous inputs.

- **Documentation**:
  - **Docstrings**: Detailed docstrings explain the purpose and usage of classes and methods.
  - **Module-Level Documentation**: Describes the overall functionality of the module.

- **Saving and Loading Transformers**:
  - **Consistency Between Training and Testing**: Saving the fitted transformer ensures the same transformations are applied during inference.
  - **Version Control**: Transformers can be versioned and tracked alongside models.

### Final Thoughts

By encapsulating the preprocessing logic within a dedicated class and adhering to best practices, you've structured your preprocessing pipeline to be robust, maintainable, and scalable. This approach not only aligns with clean code principles but also facilitates seamless integration into an MLOps workflow, supporting tasks like automated testing, deployment, and monitoring.

Feel free to further enhance the `Preprocessor` class by adding more preprocessing steps, supporting different types of encoders, or integrating with model training pipelines as your project evolves.
“
Q- i just create it like like exactly in my local env and 
The main.py is like this 

# main.py or any other appropriate script/module
from app.services.data_loader import load_data
from app.services.preprocessing import Preprocessor
from app.utils.config import load_config
from app.utils.logger import setup_logger


def main():
    # Load configuration and logger
    config = load_config()
    logger = setup_logger(config.get('logging', {}))


    # Load data
    df = load_data()


    # Initialize Preprocessor
    preprocessor = Preprocessor(config=config, logger=logger)


    # Fit and transform the training data
    df_train_transformed = preprocessor.fit_transform(df)


    # Save the preprocessor
    preprocessor.save_transformer()


    # (Optional) Transform the test data
    # Assuming you have split the data into X_train and X_test
    # X_test_transformed = preprocessor.transform(X_test)


if __name__ == "__main__":
    main()




Q- but i get this error 

PS C:\Users\PCS\Desktop\mlops\Telco\Mlops-Telco-Churn> python -m app.main 
2024-10-10 12:23:00,769 - app.utils.logger - INFO - Data successfully loaded from data\raw\data.csv. Shape: (7043, 21)
2024-10-10 12:23:00,769 - app.utils.logger - INFO - Starting fit_transform on preprocessing.
2024-10-10 12:23:00,780 - app.utils.logger - ERROR - Error during fit_transform.
Traceback (most recent call last):
  File "C:\Users\PCS\Desktop\mlops\Telco\Mlops-Telco-Churn\app\services\preprocessing.py", line 106, in fit_transform
    transformed_array = self.transformer.fit_transform(df)
                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\PCS\Desktop\mlops\Telco\Mlops-Telco-Churn\venv\Lib\site-packages\sklearn\utils\_set_output.py", line 316, in wrapped
    data_to_wrap = f(self, X, *args, **kwargs)
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\PCS\Desktop\mlops\Telco\Mlops-Telco-Churn\venv\Lib\site-packages\sklearn\base.py", line 1473, in wrapper
    return fit_method(estimator, *args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\PCS\Desktop\mlops\Telco\Mlops-Telco-Churn\venv\Lib\site-packages\sklearn\compose\_column_transformer.py", line 976, in fit_transform
    result = self._call_func_on_transformers(
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\PCS\Desktop\mlops\Telco\Mlops-Telco-Churn\venv\Lib\site-packages\sklearn\compose\_column_transformer.py", line 885, in _call_func_on_transformers
    return Parallel(n_jobs=self.n_jobs)(jobs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\PCS\Desktop\mlops\Telco\Mlops-Telco-Churn\venv\Lib\site-packages\sklearn\utils\parallel.py", line 74, in __call__
    return super().__call__(iterable_with_config)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\PCS\Desktop\mlops\Telco\Mlops-Telco-Churn\venv\Lib\site-packages\joblib\parallel.py", line 1918, in __call__
    return output if self.return_generator else list(output)
                                                ^^^^^^^^^^^^
  File "C:\Users\PCS\Desktop\mlops\Telco\Mlops-Telco-Churn\venv\Lib\site-packages\joblib\parallel.py", line 1847, in _get_sequential_output
    res = func(*args, **kwargs)
          ^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\PCS\Desktop\mlops\Telco\Mlops-Telco-Churn\venv\Lib\site-packages\sklearn\utils\parallel.py", line 136, in __call__
    return self.function(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\PCS\Desktop\mlops\Telco\Mlops-Telco-Churn\venv\Lib\site-packages\sklearn\pipeline.py", line 1310, in _fit_transform_one
    res = transformer.fit_transform(X, y, **params.get("fit_transform", {}))
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\PCS\Desktop\mlops\Telco\Mlops-Telco-Churn\venv\Lib\site-packages\sklearn\utils\_set_output.py", line 316, in wrapped
    data_to_wrap = f(self, X, *args, **kwargs)
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\PCS\Desktop\mlops\Telco\Mlops-Telco-Churn\venv\Lib\site-packages\sklearn\base.py", line 1098, in fit_transform
    return self.fit(X, **fit_params).transform(X)
           ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\PCS\Desktop\mlops\Telco\Mlops-Telco-Churn\venv\Lib\site-packages\sklearn\preprocessing\_data.py", line 878, in fit
    return self.partial_fit(X, y, sample_weight)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\PCS\Desktop\mlops\Telco\Mlops-Telco-Churn\venv\Lib\site-packages\sklearn\base.py", line 1473, in wrapper
    return fit_method(estimator, *args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\PCS\Desktop\mlops\Telco\Mlops-Telco-Churn\venv\Lib\site-packages\sklearn\preprocessing\_data.py", line 914, in partial_fit
    X = self._validate_data(
        ^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\PCS\Desktop\mlops\Telco\Mlops-Telco-Churn\venv\Lib\site-packages\sklearn\base.py", line 633, in _validate_data
    out = check_array(X, input_name="X", **check_params)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\PCS\Desktop\mlops\Telco\Mlops-Telco-Churn\venv\Lib\site-packages\sklearn\utils\validation.py", line 1012, in check_array
    array = _asarray_with_order(array, order=order, dtype=dtype, xp=xp)
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\PCS\Desktop\mlops\Telco\Mlops-Telco-Churn\venv\Lib\site-packages\sklearn\utils\_array_api.py", line 745, in _asarray_with_order
    array = numpy.asarray(array, order=order, dtype=dtype)
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\PCS\Desktop\mlops\Telco\Mlops-Telco-Churn\venv\Lib\site-packages\pandas\core\generic.py", line 2153, in __array__
    arr = np.asarray(values, dtype=dtype)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
ValueError: could not convert string to float: ' '
Traceback (most recent call last):
  File "<frozen runpy>", line 198, in _run_module_as_main
  File "<frozen runpy>", line 88, in _run_code
  File "C:\Users\PCS\Desktop\mlops\Telco\Mlops-Telco-Churn\app\main.py", line 29, in <module>
    main()
  File "C:\Users\PCS\Desktop\mlops\Telco\Mlops-Telco-Churn\app\main.py", line 19, in main
    df_train_transformed = preprocessor.fit_transform(df)
                           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\PCS\Desktop\mlops\Telco\Mlops-Telco-Churn\app\services\preprocessing.py", line 106, in fit_transform
    transformed_array = self.transformer.fit_transform(df)
                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\PCS\Desktop\mlops\Telco\Mlops-Telco-Churn\venv\Lib\site-packages\sklearn\utils\_set_output.py", line 316, in wrapped
    data_to_wrap = f(self, X, *args, **kwargs)
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\PCS\Desktop\mlops\Telco\Mlops-Telco-Churn\venv\Lib\site-packages\sklearn\base.py", line 1473, in wrapper
    return fit_method(estimator, *args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\PCS\Desktop\mlops\Telco\Mlops-Telco-Churn\venv\Lib\site-packages\sklearn\compose\_column_transformer.py", line 976, in fit_transform
    result = self._call_func_on_transformers(
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\PCS\Desktop\mlops\Telco\Mlops-Telco-Churn\venv\Lib\site-packages\sklearn\compose\_column_transformer.py", line 885, in _call_func_on_transformers
    return Parallel(n_jobs=self.n_jobs)(jobs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\PCS\Desktop\mlops\Telco\Mlops-Telco-Churn\venv\Lib\site-packages\sklearn\utils\parallel.py", line 74, in __call__
    return super().__call__(iterable_with_config)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\PCS\Desktop\mlops\Telco\Mlops-Telco-Churn\venv\Lib\site-packages\joblib\parallel.py", line 1918, in __call__
    return output if self.return_generator else list(output)
                                                ^^^^^^^^^^^^
  File "C:\Users\PCS\Desktop\mlops\Telco\Mlops-Telco-Churn\venv\Lib\site-packages\joblib\parallel.py", line 1847, in _get_sequential_output
    res = func(*args, **kwargs)
          ^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\PCS\Desktop\mlops\Telco\Mlops-Telco-Churn\venv\Lib\site-packages\sklearn\utils\parallel.py", line 136, in __call__
    return self.function(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\PCS\Desktop\mlops\Telco\Mlops-Telco-Churn\venv\Lib\site-packages\sklearn\pipeline.py", line 1310, in _fit_transform_one
    res = transformer.fit_transform(X, y, **params.get("fit_transform", {}))
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\PCS\Desktop\mlops\Telco\Mlops-Telco-Churn\venv\Lib\site-packages\sklearn\utils\_set_output.py", line 316, in wrapped
    data_to_wrap = f(self, X, *args, **kwargs)
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\PCS\Desktop\mlops\Telco\Mlops-Telco-Churn\venv\Lib\site-packages\sklearn\base.py", line 1098, in fit_transform
    return self.fit(X, **fit_params).transform(X)
           ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\PCS\Desktop\mlops\Telco\Mlops-Telco-Churn\venv\Lib\site-packages\sklearn\preprocessing\_data.py", line 878, in fit
    return self.partial_fit(X, y, sample_weight)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\PCS\Desktop\mlops\Telco\Mlops-Telco-Churn\venv\Lib\site-packages\sklearn\base.py", line 1473, in wrapper
    return fit_method(estimator, *args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\PCS\Desktop\mlops\Telco\Mlops-Telco-Churn\venv\Lib\site-packages\sklearn\preprocessing\_data.py", line 914, in partial_fit
    X = self._validate_data(
        ^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\PCS\Desktop\mlops\Telco\Mlops-Telco-Churn\venv\Lib\site-packages\sklearn\base.py", line 633, in _validate_data
    out = check_array(X, input_name="X", **check_params)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\PCS\Desktop\mlops\Telco\Mlops-Telco-Churn\venv\Lib\site-packages\sklearn\utils\_array_api.py", line 745, in _asarray_with_order
    array = numpy.asarray(array, order=order, dtype=dtype)
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\PCS\Desktop\mlops\Telco\Mlops-Telco-Churn\venv\Lib\site-packages\pandas\core\generic.py", line 2153, in __array__
    arr = np.asarray(values, dtype=dtype)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
ValueError: could not convert string to float: ' '

Q- why this error ?