diff --git a/demos/demo_cleaning.ipynb b/demos/demo_cleaning.ipynb
new file mode 100644
index 0000000..341156e
--- /dev/null
+++ b/demos/demo_cleaning.ipynb
@@ -0,0 +1,617 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "ec779476-1923-4840-9fbb-a33b62a325ba",
+ "metadata": {},
+ "source": [
+ "### Step 1: Load the Titanic Dataset\n",
+ "We start by loading the Titanic dataset using Seaborn. \n",
+ "This dataset contains information about passengers such as age, class, fare, and survival status. \n",
+ "It’s a great example for demonstrating data cleaning because it includes missing values, categorical variables, and numerical features.\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 9,
+ "id": "cd31f646-5065-47d2-a0e8-65b8f3a9523a",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Original shape: (891, 15)\n"
+ ]
+ },
+ {
+ "data": {
+ "text/html": [
+ "
\n",
+ "\n",
+ "
\n",
+ " \n",
+ " \n",
+ " | \n",
+ " survived | \n",
+ " pclass | \n",
+ " sex | \n",
+ " age | \n",
+ " sibsp | \n",
+ " parch | \n",
+ " fare | \n",
+ " embarked | \n",
+ " class | \n",
+ " who | \n",
+ " adult_male | \n",
+ " deck | \n",
+ " embark_town | \n",
+ " alive | \n",
+ " alone | \n",
+ "
\n",
+ " \n",
+ " \n",
+ " \n",
+ " | 0 | \n",
+ " 0 | \n",
+ " 3 | \n",
+ " male | \n",
+ " 22.0 | \n",
+ " 1 | \n",
+ " 0 | \n",
+ " 7.2500 | \n",
+ " S | \n",
+ " Third | \n",
+ " man | \n",
+ " True | \n",
+ " NaN | \n",
+ " Southampton | \n",
+ " no | \n",
+ " False | \n",
+ "
\n",
+ " \n",
+ " | 1 | \n",
+ " 1 | \n",
+ " 1 | \n",
+ " female | \n",
+ " 38.0 | \n",
+ " 1 | \n",
+ " 0 | \n",
+ " 71.2833 | \n",
+ " C | \n",
+ " First | \n",
+ " woman | \n",
+ " False | \n",
+ " C | \n",
+ " Cherbourg | \n",
+ " yes | \n",
+ " False | \n",
+ "
\n",
+ " \n",
+ " | 2 | \n",
+ " 1 | \n",
+ " 3 | \n",
+ " female | \n",
+ " 26.0 | \n",
+ " 0 | \n",
+ " 0 | \n",
+ " 7.9250 | \n",
+ " S | \n",
+ " Third | \n",
+ " woman | \n",
+ " False | \n",
+ " NaN | \n",
+ " Southampton | \n",
+ " yes | \n",
+ " True | \n",
+ "
\n",
+ " \n",
+ " | 3 | \n",
+ " 1 | \n",
+ " 1 | \n",
+ " female | \n",
+ " 35.0 | \n",
+ " 1 | \n",
+ " 0 | \n",
+ " 53.1000 | \n",
+ " S | \n",
+ " First | \n",
+ " woman | \n",
+ " False | \n",
+ " C | \n",
+ " Southampton | \n",
+ " yes | \n",
+ " False | \n",
+ "
\n",
+ " \n",
+ " | 4 | \n",
+ " 0 | \n",
+ " 3 | \n",
+ " male | \n",
+ " 35.0 | \n",
+ " 0 | \n",
+ " 0 | \n",
+ " 8.0500 | \n",
+ " S | \n",
+ " Third | \n",
+ " man | \n",
+ " True | \n",
+ " NaN | \n",
+ " Southampton | \n",
+ " no | \n",
+ " True | \n",
+ "
\n",
+ " \n",
+ "
\n",
+ "
"
+ ],
+ "text/plain": [
+ " survived pclass sex age sibsp parch fare embarked class \\\n",
+ "0 0 3 male 22.0 1 0 7.2500 S Third \n",
+ "1 1 1 female 38.0 1 0 71.2833 C First \n",
+ "2 1 3 female 26.0 0 0 7.9250 S Third \n",
+ "3 1 1 female 35.0 1 0 53.1000 S First \n",
+ "4 0 3 male 35.0 0 0 8.0500 S Third \n",
+ "\n",
+ " who adult_male deck embark_town alive alone \n",
+ "0 man True NaN Southampton no False \n",
+ "1 woman False C Cherbourg yes False \n",
+ "2 woman False NaN Southampton yes True \n",
+ "3 woman False C Southampton yes False \n",
+ "4 man True NaN Southampton no True "
+ ]
+ },
+ "execution_count": 9,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "import pandas as pd\n",
+ "import seaborn as sns\n",
+ "from dskit import cleaning\n",
+ "\n",
+ "# Load Titanic dataset\n",
+ "df = sns.load_dataset(\"titanic\")\n",
+ "print(\"Original shape:\", df.shape)\n",
+ "df.head()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "295b8b6a-adc0-41df-8959-24cbf6bffa2f",
+ "metadata": {},
+ "source": [
+ "### Step 2: Clean Column Names\n",
+ "Column names often contain spaces, uppercase letters, or special characters that make them hard to work with. \n",
+ "Using `rename_columns_auto`, we standardize them by:\n",
+ "- Converting to lowercase\n",
+ "- Replacing spaces with underscores\n",
+ "- Removing special characters \n",
+ "\n",
+ "This ensures consistency and makes coding easier."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 10,
+ "id": "53765dd8-4e15-4178-956b-51873f72bcf1",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Renamed Columns: ['survived', 'pclass', 'sex', 'age', 'sibsp', 'parch', 'fare', 'embarked', 'class', 'who', 'adult_male', 'deck', 'embark_town', 'alive', 'alone']\n"
+ ]
+ }
+ ],
+ "source": [
+ "df_cleaned = cleaning.rename_columns_auto(df)\n",
+ "print(\"Renamed Columns:\", df_cleaned.columns.tolist())"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "5f1bac70-4586-452d-bc5d-9178a58b1d9b",
+ "metadata": {},
+ "source": [
+ "### Step 3: Summarize Missing Values\n",
+ "Real-world datasets almost always have missing values. \n",
+ "The `missing_summary` function helps us quickly identify:\n",
+ "- How many missing values each column has\n",
+ "- The percentage of missing values relative to the dataset size \n",
+ "\n",
+ "This summary guides us in deciding how to handle missing data."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 11,
+ "id": "726620da-f368-4294-a848-c91b554c7879",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "\n",
+ "
\n",
+ " \n",
+ " \n",
+ " | \n",
+ " Missing Count | \n",
+ " Missing % | \n",
+ "
\n",
+ " \n",
+ " \n",
+ " \n",
+ " | deck | \n",
+ " 688 | \n",
+ " 77.216611 | \n",
+ "
\n",
+ " \n",
+ " | age | \n",
+ " 177 | \n",
+ " 19.865320 | \n",
+ "
\n",
+ " \n",
+ " | embarked | \n",
+ " 2 | \n",
+ " 0.224467 | \n",
+ "
\n",
+ " \n",
+ " | embark_town | \n",
+ " 2 | \n",
+ " 0.224467 | \n",
+ "
\n",
+ " \n",
+ "
\n",
+ "
"
+ ],
+ "text/plain": [
+ " Missing Count Missing %\n",
+ "deck 688 77.216611\n",
+ "age 177 19.865320\n",
+ "embarked 2 0.224467\n",
+ "embark_town 2 0.224467"
+ ]
+ },
+ "execution_count": 11,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "missing_summary = cleaning.missing_summary(df_cleaned)\n",
+ "missing_summary"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "f2cfd23a-a28c-4f77-9833-a0c43c9d62d4",
+ "metadata": {},
+ "source": [
+ "### Step 4: Fill Missing Values\n",
+ "Once we know where the missing values are, we need to handle them. \n",
+ "The `fill_missing` function provides multiple strategies:\n",
+ "- Mean or median for numeric columns\n",
+ "- Mode for categorical columns\n",
+ "- Forward/backward fill for sequential data\n",
+ "- Constant values if specified \n",
+ "\n",
+ "Here, we use the `auto` strategy, which intelligently chooses the best method for each column.\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 12,
+ "id": "58e8a23c-9bcb-4f79-b970-90b18f94bd14",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "survived 0\n",
+ "pclass 0\n",
+ "sex 0\n",
+ "age 0\n",
+ "sibsp 0\n",
+ "parch 0\n",
+ "fare 0\n",
+ "embarked 0\n",
+ "class 0\n",
+ "who 0\n",
+ "adult_male 0\n",
+ "deck 0\n",
+ "embark_town 0\n",
+ "alive 0\n",
+ "alone 0\n",
+ "dtype: int64"
+ ]
+ },
+ "execution_count": 12,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "df_filled = cleaning.fill_missing(df_cleaned, strategy=\"auto\")\n",
+ "df_filled.isnull().sum()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "653b6962-5d99-4513-9dee-3d41e2a34345",
+ "metadata": {},
+ "source": [
+ "### Step 5: Detect Outliers\n",
+ "Outliers are extreme values that can distort analysis and models. \n",
+ "The `outlier_summary` function detects them using:\n",
+ "- IQR method: values outside 1.5× the interquartile range\n",
+ "- Z-score method: values more than 3 standard deviations from the mean \n",
+ "\n",
+ "This helps us understand which columns contain unusual values."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 13,
+ "id": "e9ed791a-460d-4f0b-a024-4c96f6ae247d",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "parch 213\n",
+ "fare 116\n",
+ "age 66\n",
+ "sibsp 46\n",
+ "Name: Outlier Count, dtype: int64"
+ ]
+ },
+ "execution_count": 13,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "outliers = cleaning.outlier_summary(df_filled, method=\"iqr\")\n",
+ "outliers"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "64cd5f52-b387-47b5-bd3e-065e93d941c2",
+ "metadata": {},
+ "source": [
+ "### Step 6: Remove Outliers\n",
+ "After detecting outliers, we can remove them to make the dataset more robust. \n",
+ "The `remove_outliers` function filters rows that fall outside the acceptable range. \n",
+ "This step reduces noise and improves the reliability of statistical analysis and machine learning models."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 14,
+ "id": "9231793a-18a2-494a-8250-2021623c3105",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Shape after removing outliers: (561, 15)\n"
+ ]
+ }
+ ],
+ "source": [
+ "df_no_outliers = cleaning.remove_outliers(df_filled, method=\"iqr\")\n",
+ "print(\"Shape after removing outliers:\", df_no_outliers.shape)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "22ac079f-d6e9-4b03-8fc1-f51880d8707f",
+ "metadata": {},
+ "source": [
+ "### Step 6a: Visualize Outlier Removal\n",
+ "To better understand the impact of outlier removal, we can compare the distribution of the **age** column before and after cleaning. \n",
+ "Histograms are a simple way to see how extreme values affect the overall shape of the data. \n",
+ "\n",
+ "- Before removal: The distribution may show long tails or unusual spikes caused by outliers. \n",
+ "- After removal: The distribution becomes smoother and more representative of the majority of passengers. \n",
+ "\n",
+ "This visualization helps confirm that our cleaning step improves data quality without losing important information."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 15,
+ "id": "18856d9c-563d-4e4b-926d-b3524b2c1032",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "image/png": "iVBORw0KGgoAAAANSUhEUgAAA+0AAAHWCAYAAAACZWhUAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQAAXX9JREFUeJzt3Qu8jXW+x/Hf3vZ2qxBym5BUKJekknQht2hMxUw3FSWajm40EyoVXZjuJ4mpU9SJlDm6ycg1pkIoSclgdHUrYofBZq/z+v5Pzzprrb321dp7Pc/an/frtVj7Wbf/83+etf7/3/O/pYVCoZABAAAAAADfSU92AgAAAAAAQHwE7QAAAAAA+BRBOwAAAAAAPkXQDgAAAACATxG0AwAAAADgUwTtAAAAAAD4FEE7AAAAAAA+RdAOAAAAAIBPEbQDAAAAAOBTBO0oNV9//bWlpaXZpEmTSvyz9Bn6LH2m57jjjrPf/va3Vhref/999/n6PxlycnKsefPm9tBDD5XK5z366KN2/PHHW7ly5ezUU0+1sqxDhw7ulozzHtH69evnvvee7du32xFHHGEzZ84kq4AkoS4QrLrAf//3f1vTpk0tMzPTqlWrZmX9XL3//vvdNpS+tLQ0l/+eCRMmWIMGDWz//v1l4nAQtAfMs88+607atm3bJjspLh3eLSMjw6pXr25t2rSx2267zb788suE7rNfAx6/pu3VV1+17777zm6++eZcFzIib7Vq1bKOHTva3//+92J/1uzZs+3OO++09u3b28SJE+3hhx82vwiFQq7Ccd5557nKRuXKla1FixY2atQo27NnT7HfV+e3Co7Ii0J+qmB4t/T0dPe97N69uy1evNjKuho1atgNN9xgI0aMSHZSgMNCXcBfglQXKMp59NVXX7mLn40bN7bnn3/ennvuOdu7d68r/5LRKKELr3/+85+tSZMmVrFiRVe+devWzWbMmHFY7ztlyhR76qmnzG+8CwTeTRdOdCH61ltvtZ07d1pZ169fPztw4ID99a9/tTIhhEA5++yzQ8cdd1xIh27dunVJTYvS0KVLl9B///d/h15++eXQ2LFjQzfccEOoatWqoYyMjNDjjz8e9fycnJzQv//979DBgweL9DmnnHJK6Pzzzy/Sa/QZ+ix9pqdhw4ahiy66qEjvU9y0HTp0yH2+/k+GVq1ahQYOHBi1beLEie6YjRo1KnzMHn30UbcP2v7OO+8U67OGDh0aSk9PD+3fvz/kJzoHLrvsMrdv5557bujJJ58M/fWvfw1dffXVLr3NmzcPbdmypVjvPW3aNPe+CxYsyPWYzofIc6K4531xbNy40aXryiuvdMd40qRJobvuuitUrVq1UIUKFUKrVq0KlSV9+/Z13/tIX375pcujefPmJS1dwOGiLlA41AVy1wWKch6NHz8+12M//vij23bfffeFStNXX30V+s1vfhMqX7586MYbbww9//zzrg5z6qmnuvT86U9/KvZ7q24YW1ZElqmqP3mys7NdmV4alMf6fB0HlekTJkwI/eEPf3Db2rdvHyprLM55d+edd7pjF1nfT1UE7QHyr3/9y52w06dPDx1zzDGh+++/P6npUVoGDRqUa/tPP/0UateunXv83XffPezPKUrQvnv37jwfK82gPZk++eQTl/dz586NG7QvW7YsavuOHTtCmZmZoauuuqpYn3fdddeFjjjiiFCi6Id37969h/0+Dz/8cJ4F+dtvv+0C9wsvvLDEg/ZEy+8c9yoYqshE+vvf/+6233TTTaGyHrSLLthcc801SUkTcLioCxSMukDedYGinEcjR450z1GgXtJBe37H7MCBA+53u3LlyqElS5bkujBz+eWXuzRNnTq1xIP2RNuzZ0+BQXtk/ou3v0uXLg2VJRbnvFu+fHmZuRBP9/gAmTx5sh199NF20UUX2e9//3v3d17dh6655hqrUqWK6xLct29f++yzz+KOq1XXJ72Xuhipq9Hpp59ub7/99mF3QZ06darrMh85jire2KAtW7bYddddZ8cee6xVqFDB6tataxdffHG427G6AX3xxRe2cOHCcPcgb7yw191bj/3Hf/yH6+qt94l8LF73ZXXn1rhr7e/JJ59s06dPj3o8r/FKse+ZX9ryGtM+bdo0N4SgUqVKVrNmTbv66qvthx9+yNXd58gjj3TbL7nkEnf/mGOOsT/96U926NChAvP/zTfftPLly7su4YWhc0Tp0fGKHQun7mKnnHKKy6vatWvbjTfeaD///HP4OdpHdYlXV3MvD7zje/DgQXvggQdctzodW+XXXXfdlWvskTfXwHvvvefOP6XF6+qk7l+333671a9f373HCSecYH/5y19c2vLz73//242zP+mkk2z06NG5Hu/Zs6f7XsyaNcuWLFmS53ip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O3XqVOz3HT58uA0ZMiSqpb1+/frWtWtXN6FdcVtFVFHr0qWLZWZmWtCQ/uR6ctV218KugH1dvTaWk17wV3Nwy//rceIXnEPJxzFInfz3eoABAAD4OmiPdfzxx1vNmjVt/fr1LmjXWPdt27ZFPefgwYNuRvm8xsF74+R1i6VK1uFWtBLxHslE+pMjMkjX/cIE7X49zziHko9jEPz89+v3GwAAlL5ArdP+/fff2/bt261u3bru73bt2tnOnTttxYr/7048f/58y8nJsbZt2yYxpQAAAAAABLylXeupq9Xcs3HjRlu5cqUbk66bxp337t3btZprTPudd95pJ5xwgnXr1s09v1mzZm7c+4ABA2zChAmua+LNN9/sutUzczwAAAAAIOiS2tK+fPlya926tbuJxpnr/r333usmmlu1apX97ne/s5NOOsn69+9vbdq0sX/84x9RXdsnT55sTZs2dd3ltdTbOeecY88991wS9woAAAAAgBRoae/QoYOFQqE8H3/vvfcKfA+1yE+ZMiXBKQMAAAAAIPkCNaYdAAAAAICyhKAdAAAAAACfImgHAAAAAMCnCNoBAAAAAPApgnYAAAAAAHyKoB0AAAAAAJ8iaAcAAAAAwKcI2gEAAAAA8CmCdgAAAAAAfIqgHQAAAAAAnyJoBwAAAADApwjaAQAAAABIpaD9+OOPt+3bt+favnPnTvcYAAAItkSV9YsWLbKePXtavXr1LC0tzd58882ox0OhkN17771Wt25dq1SpknXu3NnWrVsX9ZwdO3ZYnz59rEqVKlatWjXr37+/7d69+zD2DgCAFA/av/76azt06FCu7fv377cffvghEekCAABJlKiyfs+ePdaqVSsbN25c3McfeeQRe/rpp23ChAm2dOlSO+KII6xbt262b9++8HMUsH/xxRc2Z84cmzFjhrsQMHDgwGLuGQAAwZJRlCe//fbb4fvvvfeeVa1aNfy3CvZ58+bZcccdl9gUAgCAUpPosr579+7uFo9a2Z966im755577OKLL3bbXn75Zatdu7Zrkb/iiitszZo1NmvWLFu2bJmdfvrp7jljx461Hj162GOPPeZa8AEASGVFCtovueQS97+6t/Xt2zfqsczMTFeIP/7444lNIQAAKDWlWdZv3LjRtmzZ4rrEe3SRoG3btrZ48WIXtOt/dYn3AnbR89PT013L/KWXXhr3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u/HGG+3uu++Oev2GDRvswgsvtN69e9uqVavcBQAF8TfffDO5DwCAD1DWA/5F93gABWrfvr1ddtlldtttt9nBgwetbt26Nm3aNOvQoYMNGzbM3n33Xfv888/Dz7/nnnvsoYcesp9//tmqVavmuttpPPxf//rX8HMUtJ9//vmutV0t9AAAIHko6wH/oqUdQL40fv3jjz+2K6+80v2dkZFhl19+uRvb7j1+xhlnRL3mzDPPjPpbLfDqUq8x8d6tW7dulpOT41rqAQBA8lDWA/6WkewEAPA3BedqXY+ceE5d4ytUqGDPPPNMocfJqdu8xrHHatCgQULTCwAAioayHvA3gnYAeVKw/vLLL9vjjz9uXbt2jXpMY9hfffVVN8595syZUY8tW7Ys6u/TTjvNvvzySzvhhBPIbQAAfISyHvA/xrQDyNObb77pusJv27bNqlatGvXY0KFDbf78+W7iGgXugwcPtv79+9vKlSvtjjvusO+//97NHq/XafK5s846y66//no3vv2II45wQfycOXMK3VoPAAASj7Ie8D/GtAPIt7tc586dcwXsopngly9fbr/88ov97W9/s+nTp1vLli1t/Pjx4dnj1YVetH3hwoX2z3/+0y371rp1a7v33ntZ6x0AgCSjrAf8j5Z2AAmnmeMnTJhg3333HbkLAEAKoqwHSg9j2gEctmeffdbNIF+jRg378MMP7dFHH2UNdgAAUghlPZA8BO0ADtu6devswQcftB07drjZ4DWmffjw4eQsAAApgrIeSB66xwMAAAAA4FNMRAcAAAAAgE8RtAMAAAAA4FME7QAAAAAA+BRBOwAAAAAAPkXQDgAAAACATxG0AwAAAADgUwTtAAAAAAD4FEE7AAAAAADmT/8L+tXPu/Ah+xAAAAAASUVORK5CYII=",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "import matplotlib.pyplot as plt\n",
+ "\n",
+ "plt.figure(figsize=(12,5))\n",
+ "\n",
+ "#Before Outlier Removal\n",
+ "plt.subplot(1,2,1)\n",
+ "df_filled['age'].hist(bins=30, color='skyblue')\n",
+ "plt.title(\"Age Distribution (Before Outlier Removal)\")\n",
+ "plt.xlabel(\"Age\")\n",
+ "plt.ylabel(\"Count\")\n",
+ "\n",
+ "plt.subplot(1,2,2)\n",
+ "df_no_outliers['age'].hist(bins=30, color='salmon')\n",
+ "plt.title(\"Age Distribution (After Outlier Removal)\")\n",
+ "plt.xlabel(\"Age\")\n",
+ "plt.ylabel(\"Count\")\n",
+ "\n",
+ "plt.show()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "7c4d4d15-815d-4ff3-89f3-48a75d34d476",
+ "metadata": {},
+ "source": [
+ "### Step 7: Clean Text Columns\n",
+ "Text data often contains inconsistencies like uppercase letters, punctuation, or extra spaces. \n",
+ "The `simple_nlp_clean` function standardizes text by:\n",
+ "- Converting to lowercase\n",
+ "- Removing punctuation\n",
+ "- Stripping extra spaces \n",
+ "\n",
+ "This makes text columns easier to analyze and prepares them for NLP tasks."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 16,
+ "id": "ed5dfee1-acc2-4c71-9383-900350ac4fa3",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "\n",
+ "
\n",
+ " \n",
+ " \n",
+ " | \n",
+ " embark_town | \n",
+ "
\n",
+ " \n",
+ " \n",
+ " \n",
+ " | 0 | \n",
+ " southampton | \n",
+ "
\n",
+ " \n",
+ " | 2 | \n",
+ " southampton | \n",
+ "
\n",
+ " \n",
+ " | 3 | \n",
+ " southampton | \n",
+ "
\n",
+ " \n",
+ " | 4 | \n",
+ " southampton | \n",
+ "
\n",
+ " \n",
+ " | 5 | \n",
+ " queenstown | \n",
+ "
\n",
+ " \n",
+ "
\n",
+ "
"
+ ],
+ "text/plain": [
+ " embark_town\n",
+ "0 southampton\n",
+ "2 southampton\n",
+ "3 southampton\n",
+ "4 southampton\n",
+ "5 queenstown"
+ ]
+ },
+ "execution_count": 16,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "df_text_cleaned = cleaning.simple_nlp_clean(df_no_outliers, text_cols=[\"embark_town\"])\n",
+ "df_text_cleaned[[\"embark_town\"]].head()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "4b45d75a-1fe5-4c77-8aac-ae07c28db56a",
+ "metadata": {},
+ "source": [
+ "### Conclusion\n",
+ "In this demo, we applied the `cleaning.py` functions to the Titanic dataset. \n",
+ "We saw how to:\n",
+ "- Standardize column names\n",
+ "- Summarize and fill missing values\n",
+ "- Detect and remove outliers\n",
+ "- Clean text columns\n",
+ "\n",
+ "Together, these steps show how `dskit.cleaning` simplifies common data preprocessing tasks, making datasets ready for analysis or modeling.\n"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3 (ipykernel)",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.13.3"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/dskit/cleaning.py b/dskit/cleaning.py
index bb28ee5..c89c786 100644
--- a/dskit/cleaning.py
+++ b/dskit/cleaning.py
@@ -4,8 +4,38 @@
def fix_dtypes(df):
"""
- Auto-detects and converts column types.
+ Auto-detects and converts column types in a DataFrame.
+
+ Parameters
+ ----------
+ df : pandas.DataFrame
+ Input DataFrame whose column types need to be inferred and converted.
+
+ Returns
+ -------
+ pandas.DataFrame
+ A copy of the DataFrame with columns converted to appropriate types:
+ - Numeric if possible
+ - Datetime if possible
+ - Category if object dtype with low cardinality
+
+ Notes
+ -----
+ - Numeric conversion is attempted first, followed by datetime conversion.
+ - Object columns with unique values less than 50% of total rows are converted to category.
+ - Category conversion is heuristic-based; users should validate conversions for critical datasets.
+
+ Example
+ -------
+ >>> import pandas as pd
+ >>> from dskit.cleaning import fix_dtypes
+ >>> df = pd.DataFrame({"A": ["1", "2", "3"], "B": ["2020-01-01", "2020-01-02", "2020-01-03"]})
+ >>> fix_dtypes(df).dtypes
+ A int64
+ B datetime64[ns]
+ dtype: object
"""
+
df = df.copy()
for col in df.columns:
# Try converting to numeric
@@ -31,8 +61,39 @@ def fix_dtypes(df):
def rename_columns_auto(df):
"""
- Cleans column names: lowercase, replace spaces with underscores, remove special chars.
+ Automatically cleans DataFrame column names.
+
+ This function:
+ - Converts column names to lowercase
+ - Replaces spaces with underscores
+ - Removes special characters
+
+ Parameters
+ ----------
+ df : pandas.DataFrame
+ Input DataFrame whose column names need cleaning.
+
+ Returns
+ -------
+ pandas.DataFrame
+ DataFrame with cleaned column names.
+
+ Raises
+ ------
+ TypeError
+ If input is not a pandas DataFrame.
+
+ Example
+ -------
+ >>> import pandas as pd
+ >>> from dskit.cleaning import rename_columns_auto
+ >>> df = pd.DataFrame(columns=["User Name", "Total$Amount"])
+ >>> rename_columns_auto(df).columns
+ Index(['user_name', 'totalamount'], dtype='object')
"""
+ if not isinstance(df, pd.DataFrame):
+ raise TypeError("Input must be a pandas DataFrame")
+
df = df.copy()
new_cols = []
for col in df.columns:
@@ -43,19 +104,82 @@ def rename_columns_auto(df):
df.columns = new_cols
return df
+
def replace_specials(df, chars_to_remove=r'[@#%$]', replacement=''):
"""
- Removes or replaces special characters from text columns.
+ Removes or replaces special characters from text columns in a DataFrame.
+
+ This function applies a regular expression replacement to all
+ object/string columns in the DataFrame.
+
+ Parameters
+ ----------
+ df : pandas.DataFrame
+ Input DataFrame containing text columns.
+ chars_to_remove : str, default=r'[@#%$]'
+ Regular expression pattern of characters to remove or replace.
+ replacement : str, default=''
+ String to replace the matched characters with.
+
+ Returns
+ -------
+ pandas.DataFrame
+ DataFrame with special characters removed or replaced in text columns.
+
+ Raises
+ ------
+ TypeError
+ If input is not a pandas DataFrame.
+
+ Example
+ -------
+ >>> import pandas as pd
+ >>> from dskit.cleaning import replace_specials
+ >>> df = pd.DataFrame({'text': ['Hello@World!', 'Price#$100']})
+ >>> replace_specials(df)
+ text
+ 0 HelloWorld
+ 1 Price100
"""
+ if not isinstance(df, pd.DataFrame):
+ raise TypeError("Input must be a pandas DataFrame")
+
df = df.copy()
for col in df.select_dtypes(include=['object', 'string']).columns:
- df[col] = df[col].astype(str).str.replace(chars_to_remove, replacement, regex=True)
+ df[col] = df[col].astype(str).str.replace(
+ chars_to_remove, replacement, regex=True
+ )
return df
+
def missing_summary(df):
"""
- Returns a summary of missing values.
+ Generates a summary of missing values in a DataFrame.
+
+ Parameters
+ ----------
+ df : pandas.DataFrame
+ Input DataFrame.
+
+ Returns
+ -------
+ pandas.DataFrame
+ DataFrame with two columns:
+ - 'Missing Count': number of missing values per column
+ - 'Missing %': percentage of missing values per column
+ Only columns with missing values are included.
+
+ Example
+ -------
+ >>> import pandas as pd
+ >>> from dskit.cleaning import missing_summary
+ >>> df = pd.DataFrame({"A": [1, None, 3], "B": [None, None, 2]})
+ >>> missing_summary(df)
+ Missing Count Missing %
+ B 2 66.666667
+ A 1 33.333333
"""
+
missing = df.isnull().sum()
missing_percent = 100 * df.isnull().sum() / len(df)
summary = pd.concat([missing, missing_percent], axis=1, keys=['Missing Count', 'Missing %'])
@@ -63,9 +187,47 @@ def missing_summary(df):
def fill_missing(df, strategy='auto', fill_value=None):
"""
- Fills missing values.
- strategy: 'auto', 'mean', 'median', 'mode', 'ffill', 'bfill', 'constant'
+ Fills missing values in a DataFrame using various strategies.
+
+ Parameters
+ ----------
+ df : pandas.DataFrame
+ Input DataFrame.
+ strategy : str, default='auto'
+ Strategy for filling missing values:
+ - 'auto': mean for numeric, mode for non-numeric
+ - 'mean': fill with column mean
+ - 'median': fill with column median
+ - 'mode': fill with column mode
+ - 'ffill': forward fill
+ - 'bfill': backward fill
+ - 'constant': fill with a specified constant value
+ fill_value : any, optional
+ Value to use when strategy='constant'.
+
+ Returns
+ -------
+ pandas.DataFrame
+ DataFrame with missing values filled.
+
+ Example
+ -------
+ >>> import pandas as pd
+ >>> from dskit.cleaning import fill_missing
+ >>> df = pd.DataFrame({"A": [1, None, 3], "B": ["x", None, "y"]})
+ >>> fill_missing(df, strategy="auto")
+ A B
+ 0 1 x
+ 1 2 x
+ 2 3 y
+
+ Notes
+ -----
+ - If strategy='constant' and fill_value=None, missing values will remain unchanged.
+ - For 'auto', numeric columns use mean, non-numeric columns use mode.
+
"""
+
df = df.copy()
for col in df.columns:
@@ -97,8 +259,45 @@ def fill_missing(df, strategy='auto', fill_value=None):
def outlier_summary(df, method='iqr', threshold=1.5):
"""
- Returns a summary of outliers.
+ Summarizes the number of outliers in numeric columns of a DataFrame.
+
+ Parameters
+ ----------
+ df : pandas.DataFrame
+ Input DataFrame.
+ method : str, default='iqr'
+ Method for detecting outliers:
+ - 'iqr': Interquartile Range method
+ - 'zscore': Z-score method
+ threshold : float, default=1.5
+ Threshold multiplier for IQR method. For z-score, cutoff is fixed at 3.
+
+ Returns
+ -------
+ pandas.Series
+ Series with:
+ - Index: column names
+ - Values: outlier counts per column
+
+ Example
+ -------
+ Standard case:
+ >>> import pandas as pd
+ >>> from dskit.cleaning import outlier_summary
+ >>> df = pd.DataFrame({"A": [1, 2, 100, 3, 4]})
+ >>> outlier_summary(df)
+ A 1
+ Name: Outlier Count, dtype: int64
+
+ Edge case (no outliers):
+ >>> import pandas as pd
+ >>> from dskit.cleaning import outlier_summary
+ >>> df = pd.DataFrame({"A": [1, 2, 3, 4]}) # no outliers
+ >>> outlier_summary(df)
+ Series([], Name: Outlier Count, dtype: int64)
+
"""
+
summary = {}
numeric_cols = df.select_dtypes(include=[np.number]).columns
@@ -125,8 +324,37 @@ def outlier_summary(df, method='iqr', threshold=1.5):
def remove_outliers(df, method='iqr', threshold=1.5):
"""
- Removes rows with outliers.
+ Removes rows containing outliers from numeric columns.
+
+ Parameters
+ ----------
+ df : pandas.DataFrame
+ Input DataFrame.
+ method : str, default='iqr'
+ Method for detecting outliers:
+ - 'iqr': Interquartile Range method
+ - 'zscore': Z-score method
+ threshold : float, default=1.5
+ Threshold multiplier for IQR method. For z-score, cutoff is fixed at 3.
+
+ Returns
+ -------
+ pandas.DataFrame
+ DataFrame with outlier rows removed.
+
+ Example
+ -------
+ >>> import pandas as pd
+ >>> from dskit.cleaning import remove_outliers
+ >>> df = pd.DataFrame({"A": [1, 2, 100, 3, 4]})
+ >>> remove_outliers(df)
+ A
+ 0 1
+ 1 2
+ 3 3
+ 4 4
"""
+
df = df.copy()
numeric_cols = df.select_dtypes(include=[np.number]).columns
@@ -150,8 +378,46 @@ def remove_outliers(df, method='iqr', threshold=1.5):
def simple_nlp_clean(df, text_cols=None):
"""
- Basic text cleaning: lowercase, remove punctuation, remove extra spaces.
+ Performs basic text cleaning on specified columns.
+
+ Operations
+ ----------
+ - Converts text to lowercase
+ - Removes punctuation
+ - Removes extra spaces
+
+ Parameters
+ ----------
+ df : pandas.DataFrame
+ Input DataFrame.
+ text_cols : list of str, optional
+ List of column names to clean. If None, all object/string columns are cleaned.
+
+ Returns
+ -------
+ pandas.DataFrame
+ DataFrame with cleaned text columns.
+
+ Example
+ -------
+ Standard case:
+ >>> import pandas as pd
+ >>> from dskit.cleaning import simple_nlp_clean
+ >>> df = pd.DataFrame({"text": ["Hello, World!!", "Python is GREAT"]})
+ >>> simple_nlp_clean(df)
+ text
+ 0 hello world
+ 1 python is great
+
+ Edge case (no outliers):
+ >>> df = pd.DataFrame({"text": [" ", None]})
+ >>> simple_nlp_clean(df)
+ text
+ 0
+ 1 none
+
"""
+
df = df.copy()
if text_cols is None:
text_cols = df.select_dtypes(include=['object', 'string']).columns
diff --git a/tests/test_cleaning.py b/tests/test_cleaning.py
new file mode 100644
index 0000000..c16c6ae
--- /dev/null
+++ b/tests/test_cleaning.py
@@ -0,0 +1,22 @@
+import pandas as pd
+import pytest
+from dskit import cleaning
+
+def test_missing_summary_returns_dataframe():
+ df = pd.DataFrame({"A": [1, None, 3], "B": [None, None, 2]})
+ result = cleaning.missing_summary(df)
+ assert isinstance(result, pd.DataFrame)
+ assert list(result.columns) == ["Missing Count", "Missing %"]
+
+def test_outlier_summary_returns_series():
+ df = pd.DataFrame({"A": [1, 2, 100, 3, 4]})
+ result = cleaning.outlier_summary(df)
+ assert isinstance(result, pd.Series)
+ assert result.name == "Outlier Count"
+
+def test_fill_missing_auto_strategy():
+ df = pd.DataFrame({"A": [1, None, 3], "B": ["x", None, "y"]})
+ result = cleaning.fill_missing(df, strategy="auto")
+ # Check that missing values are filled
+ assert result["A"].isnull().sum() == 0
+ assert result["B"].isnull().sum() == 0