Repository: KwstasMCPU/BambooTools (language: python)

Issue:
fixed read readme



Discussion / hints:

Fix patch:
diff --git a/.gitignore b/.gitignore
index ffe15d6..29828bb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -155,7 +155,9 @@ dmypy.json
 # Cython debug symbols
 cython_debug/
 
+# exclude specific files
 docs/
+*.ipynb
 
 # PyCharm
 #  JetBrains specific template is maintained in a separate JetBrains.gitignore that can
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..fd55ee1
--- /dev/null
+++ b/README.md
@@ -0,0 +1,57 @@
+# BambooTools
+
+BambooTools is a Python package that provides extensions of pandas functionalities.
+
+## Installation
+
+The package is in very early stage and yet not published in PyPi. 
+
+```bash
+pip install git+https://github.com/KwstasMCPU/BambooTools
+```
+
+OR simple download the project and:
+
+```bash
+pip install . 
+```
+
+# Usage
+
+```python
+from bambootools import bambootools
+import pandas as pd
+import numpy as np
+
+>>> df = pd.DataFrame({'Animal': ['Falcon', 'Falcon',
+...                              'Parrot', 'Parrot', 
+...                               'Lama', 'Falcon'],
+...                  'Max Speed': [380., 370., 24., 26., np.nan, np.nan]})
+
+# check the completeness of the dataset per column
+>>> df.bbt.completeness()
+               perc  count
+Animal     1.000000      6
+Max Speed  0.666667      4
+
+# check the completeness of the datataset per category
+>>> print(df.bbt.completeness(by=['Animal']))
+        Max Speed      
+            perc count
+Animal                
+Falcon  0.666667     2
+Lama    0.000000     0
+Parrot  1.000000     2
+```
+
+# Documentation
+
+Work in progress
+
+# Contributing
+
+Contributions are welcome! Contribution guidelines are pending.
+
+# License
+
+MIT
\ No newline at end of file
diff --git a/README.txt b/README.txt
deleted file mode 100644
index af0709b..0000000
--- a/README.txt
+++ /dev/null
@@ -1,43 +0,0 @@
-===========
-BambooTools
-===========
-
-Towel Stuff provides such and such and so and so. You might find
-it most useful for tasks involving <x> and also <y>. Typical usage
-often looks like this::
-
-    #!/usr/bin/env python
-
-    from towelstuff import location
-    from towelstuff import utils
-
-    if utils.has_towel():
-        print "Your towel is located:", location.where_is_my_towel()
-
-(Note the double-colon and 4-space indent formatting above.)
-
-Paragraphs are separated by blank lines. *Italics*, **bold**,
-and ``monospace`` look like this.
-
-
-A Section
-=========
-
-Lists look like this:
-
-* First
-
-* Second. Can be multiple lines
-  but must be indented properly.
-
-A Sub-Section
--------------
-
-Numbered lists look like you'd expect:
-
-1. hi there
-
-2. must be going
-
-Urls are http://like.this and links can be
-written `like this <http://www.example.com/foo/bar>`_.
\ No newline at end of file
diff --git a/bambootools/bambootools.py b/bambootools/bambootools.py
index 26fa2bf..27e939a 100644
--- a/bambootools/bambootools.py
+++ b/bambootools/bambootools.py
@@ -1,45 +1,137 @@
 """
-This module implements the Requests API.
-
 :copyright: (c) 2023 by Maravegias Konstantinos.
 :license: MIT, see LICENSE for more details.
 """
 import pandas as pd
-import numpy as np
+from typing import List, Tuple
 
 
 @pd.api.extensions.register_dataframe_accessor("bbt")
-class BambooToolsAccessor:
+class BambooToolsDfAccessor:
     def __init__(self, pandas_obj) -> None:
         self._validate(pandas_obj)
         self._obj = pandas_obj
-        
+
     @property
-    def pandas_obj(self):
+    def pandas_obj(self) -> pd.DataFrame:
         return self._obj
-    
-    # @pandas_obj.setter
-    # def pandas_obj(self, pandas_obj):
-    #     if not isinstance(pandas_obj, pd.DataFrame):
-    #         raise AttributeError("Must be a pandas DataFrame")
+
     @staticmethod    
-    def _validate(obj):
-        # verify this is a DataFrame
+    def _validate(obj) -> None:
+        """Validate that the passed object is a pandas Dataframe
+
+        Args:
+            obj: Object passed
+
+        Raises:
+            AttributeError: Prompts the user that pd.DataFrame should be used
+        """
         if not isinstance(obj, pd.DataFrame):
             raise AttributeError("Must be a pandas DataFrame")
-        
-    def completeness(self):
-        
-        # if not isinstance(by, list):
-        #     raise AttributeError("Must pass a list of columns")
-
-        by = self._obj.columns.to_list()
-        counts = self._obj.groupby(by, dropna=False).apply(lambda x: x.notnull().sum()).sum()
-        perc = self._obj.groupby(by, dropna=False).apply(lambda x: x.notnull().sum()).sum() / self._obj.shape[0]
-
-        _df = pd.concat([perc, counts], axis=1).\
-            rename(columns={0:'percentage',
-                            1:'count'
-                            }
-                   )
-        return _df
\ No newline at end of file
+
+    def completeness(self, by: List[str] = None) -> pd.DataFrame:
+        """Returns the completeness table of a dataframe
+
+        Returns:
+            pd.DataFrame: The completeness table
+        """
+
+        if by is None:
+            by = self._obj.columns.to_list()
+            counts = self._obj.groupby(by, dropna=False).\
+                apply(lambda x: x.notnull().sum()).sum()
+
+            perc = self._obj.groupby(by, dropna=False).\
+                apply(lambda x: x.notnull().sum()).sum() / self._obj.shape[0]
+
+            _df = pd.concat([perc, counts], axis=1).\
+                rename(columns={0: 'perc',
+                                1: 'count'
+                                }
+                       )
+        else:
+            if not isinstance(by, List):
+                raise AttributeError("`by` arg must be a list of strings")
+
+            _df = self._obj.groupby(by, dropna=False).\
+                agg([('perc', lambda x: x.notnull().sum()/x.shape[0]),
+                     ('count', lambda x: x.notnull().sum())
+                     ]
+                    )
+
+        return _df
+
+
+@pd.api.extensions.register_series_accessor("bbt")
+class BambooToolsSeriesAccessor:
+    def __init__(self, series_obj) -> None:
+        self._validate(series_obj)
+        self._obj = series_obj
+
+    @property
+    def series_obj(self) -> pd.Series:
+        return self._obj
+
+    @staticmethod    
+    def _validate(obj) -> None:
+        """Validate that the passed object is a pandas Series
+
+        Args:
+            obj: Object passed
+
+        Raises:
+            AttributeError: Prompts the user that pd.Series should be used
+        """
+        if not isinstance(obj, pd.Series):
+            raise AttributeError("Must be a pandas Series")
+
+    def above(self, thresh: float,
+              dropna: bool = False) -> Tuple[float, float]:
+        """Calculates the number of values and their percentage which are
+        above a specific threshold.
+
+        Args:
+            thresh (float): The threshold given by the user.
+            dropna (bool, optional): If True drops the NULL records before
+                the calculation of percentage of NULL values. Hence the total
+                number of records equal to the number of non NULL values.
+                Defaults to False.
+
+        Returns:
+            Tuple[float, float]: Counts and percentage of records above the
+                given threshold.
+        """
+        if dropna:
+            values_above = self._obj > thresh
+        else:
+            values_above = self._obj.dropna() > thresh
+        count = values_above.sum()
+        perc = values_above.sum() / len(values_above)
+
+        return count, perc
+
+    def below(self, thresh: float,
+              dropna: bool = False) -> Tuple[float, float]:
+        """Calculates the number of values and their percentage which are
+        below a specific threshold.
+
+        Args:
+            thresh (float): The threshold given by the user.
+            dropna (bool, optional): If True drops the NULL records before
+                the calculation of percentage of NULL values. Hence the total
+                number of records equal to the number of non NULL values.
+                Defaults to False.
+
+        Returns:
+            Tuple[float, float]: Counts and percentage of records below the
+                given threshold.
+        """
+
+        if dropna:
+            values_above = self._obj < thresh
+        else:
+            values_above = self._obj.dropna() < thresh
+        count = values_above.sum()
+        perc = values_above.sum() / len(values_above)
+
+        return count, perc
diff --git a/bin/examples.py b/bin/examples.py
new file mode 100644
index 0000000..64fadb1
--- /dev/null
+++ b/bin/examples.py
@@ -0,0 +1,38 @@
+from bambootools import bambootools
+import pandas as pd
+import numpy as np
+
+# Set a seed for reproducibility
+np.random.seed(0)
+
+# Define the number of records
+n_records = 20
+
+# Define the categories for the 'animal' column
+animals = ['cat', 'dog', 'lama']
+
+# Generate random data for the 'animal', 'weight', and 'tail length' columns
+df = pd.DataFrame({
+    'animal': np.random.choice(animals, n_records),
+    'weight': np.random.randint(1, 100, n_records),
+    'tail length': np.random.randint(1, 100, n_records),
+    'color': np.random.choice(['black', 'white', 'brown', 'gray'], n_records),
+    'name': [f'name_{i}' for i in range(n_records)]
+})
+
+# Insert NULL values in the 'weight', 'tail length' and 'name' columns
+for col, n_nulls in zip(['weight', 'tail length', 'name'], [3, 5, 1]):
+    null_indices = np.random.choice(df.index, n_nulls, replace=False)
+    df.loc[null_indices, col] = np.nan
+ 
+# get dataset's completeness for each column
+print(df.bbt.completeness())
+
+# get dataset's completeness per group
+print(df.bbt.completeness(by=['animal']))
+
+# find how many values and their percentage which are above a threshold
+print(df['weight'].bbt.above(thresh=30))
+
+# find how many values and their percentage which are below a threshold
+print(df['weight'].bbt.below(thresh=30))
diff --git a/setup.py b/setup.py
index df30cce..6b41af5 100644
--- a/setup.py
+++ b/setup.py
@@ -1,7 +1,8 @@
 from setuptools import setup, find_packages
+from pathlib import Path
 
-with open("README.txt", 'r') as f:
-    long_description = f.read()
+this_directory = Path(__file__).parent
+long_description = (this_directory / "README.md").read_text()
 
 setup(
     name='BambooTools',
@@ -9,12 +10,14 @@
     author='Konstantinos Maravegias',
     author_email='kwstas.maras@gmail.com',
     packages=find_packages(),
-    #scripts=['bin/stowe-towels.py','bin/wash-towels.py'],
-    url='http://pypi.python.org/pypi/TowelStuff/',
+    url='http://pypi.python.org/pypi/BambooTools/',
     license='MIT',
-    description='Useful towel-related stuff.',
+    description='Useful Bamboo stuff.',
     long_description=long_description,
+    long_description_content_type='text/markdown',
     install_requires=[
-        "pandas >= 1.5.3"
+        "pandas >= 1.5.3",
+        "pathlib",
     ],
+    python_requires='>=3.8'
 )
\ No newline at end of file


Test patch:
diff --git a/bin/test.ipynb b/bin/test.ipynb
deleted file mode 100644
index e728c1d..0000000
--- a/bin/test.ipynb
+++ /dev/null
@@ -1,565 +0,0 @@
-{
- "cells": [
-  {
-   "cell_type": "code",
-   "execution_count": 7,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "import pandas as pd\n",
-    "import numpy as np\n",
-    "import seaborn as sns\n",
-    "\n",
-    "df = pd.DataFrame({'weight':[1, 2, 3, 4, 5, np.nan],\n",
-    "                   'animal':['cat', 'dog', 'dog', 'dog', 'cat', 'lama']})"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": 8,
-   "metadata": {},
-   "outputs": [
-    {
-     "data": {
-      "text/html": [
-       "<div>\n",
-       "<style scoped>\n",
-       "    .dataframe tbody tr th:only-of-type {\n",
-       "        vertical-align: middle;\n",
-       "    }\n",
-       "\n",
-       "    .dataframe tbody tr th {\n",
-       "        vertical-align: top;\n",
-       "    }\n",
-       "\n",
-       "    .dataframe thead th {\n",
-       "        text-align: right;\n",
-       "    }\n",
-       "</style>\n",
-       "<table border=\"1\" class=\"dataframe\">\n",
-       "  <thead>\n",
-       "    <tr style=\"text-align: right;\">\n",
-       "      <th></th>\n",
-       "      <th>weight</th>\n",
-       "    </tr>\n",
-       "    <tr>\n",
-       "      <th>animal</th>\n",
-       "      <th></th>\n",
-       "    </tr>\n",
-       "  </thead>\n",
-       "  <tbody>\n",
-       "    <tr>\n",
-       "      <th>cat</th>\n",
-       "      <td>1.0</td>\n",
-       "    </tr>\n",
-       "    <tr>\n",
-       "      <th>dog</th>\n",
-       "      <td>1.0</td>\n",
-       "    </tr>\n",
-       "    <tr>\n",
-       "      <th>lama</th>\n",
-       "      <td>0.0</td>\n",
-       "    </tr>\n",
-       "  </tbody>\n",
-       "</table>\n",
-       "</div>"
-      ],
-      "text/plain": [
-       "        weight\n",
-       "animal        \n",
-       "cat        1.0\n",
-       "dog        1.0\n",
-       "lama       0.0"
-      ]
-     },
-     "execution_count": 8,
-     "metadata": {},
-     "output_type": "execute_result"
-    }
-   ],
-   "source": [
-    "df.groupby(['animal']).agg(lambda x: x.notnull().sum()/x.shape[0])"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": 15,
-   "metadata": {},
-   "outputs": [
-    {
-     "data": {
-      "text/html": [
-       "<div>\n",
-       "<style scoped>\n",
-       "    .dataframe tbody tr th:only-of-type {\n",
-       "        vertical-align: middle;\n",
-       "    }\n",
-       "\n",
-       "    .dataframe tbody tr th {\n",
-       "        vertical-align: top;\n",
-       "    }\n",
-       "\n",
-       "    .dataframe thead th {\n",
-       "        text-align: right;\n",
-       "    }\n",
-       "</style>\n",
-       "<table border=\"1\" class=\"dataframe\">\n",
-       "  <thead>\n",
-       "    <tr style=\"text-align: right;\">\n",
-       "      <th></th>\n",
-       "      <th>weight</th>\n",
-       "    </tr>\n",
-       "    <tr>\n",
-       "      <th>animal</th>\n",
-       "      <th></th>\n",
-       "    </tr>\n",
-       "  </thead>\n",
-       "  <tbody>\n",
-       "    <tr>\n",
-       "      <th>cat</th>\n",
-       "      <td>2</td>\n",
-       "    </tr>\n",
-       "    <tr>\n",
-       "      <th>dog</th>\n",
-       "      <td>3</td>\n",
-       "    </tr>\n",
-       "    <tr>\n",
-       "      <th>lama</th>\n",
-       "      <td>0</td>\n",
-       "    </tr>\n",
-       "  </tbody>\n",
-       "</table>\n",
-       "</div>"
-      ],
-      "text/plain": [
-       "        weight\n",
-       "animal        \n",
-       "cat          2\n",
-       "dog          3\n",
-       "lama         0"
-      ]
-     },
-     "execution_count": 15,
-     "metadata": {},
-     "output_type": "execute_result"
-    }
-   ],
-   "source": [
-    "df.groupby(['animal']).agg(lambda x: x.notnull().sum())"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": 13,
-   "metadata": {},
-   "outputs": [
-    {
-     "data": {
-      "text/plain": [
-       "False"
-      ]
-     },
-     "execution_count": 13,
-     "metadata": {},
-     "output_type": "execute_result"
-    }
-   ],
-   "source": [
-    "[] == False"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": 5,
-   "metadata": {},
-   "outputs": [
-    {
-     "data": {
-      "text/plain": [
-       "['A', 'animal']"
-      ]
-     },
-     "execution_count": 5,
-     "metadata": {},
-     "output_type": "execute_result"
-    }
-   ],
-   "source": [
-    "df.columns.to_list()"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": 17,
-   "metadata": {},
-   "outputs": [
-    {
-     "data": {
-      "text/html": [
-       "<div>\n",
-       "<style scoped>\n",
-       "    .dataframe tbody tr th:only-of-type {\n",
-       "        vertical-align: middle;\n",
-       "    }\n",
-       "\n",
-       "    .dataframe tbody tr th {\n",
-       "        vertical-align: top;\n",
-       "    }\n",
-       "\n",
-       "    .dataframe thead th {\n",
-       "        text-align: right;\n",
-       "    }\n",
-       "</style>\n",
-       "<table border=\"1\" class=\"dataframe\">\n",
-       "  <thead>\n",
-       "    <tr style=\"text-align: right;\">\n",
-       "      <th></th>\n",
-       "      <th>weight</th>\n",
-       "      <th>animal</th>\n",
-       "    </tr>\n",
-       "    <tr>\n",
-       "      <th>animal</th>\n",
-       "      <th></th>\n",
-       "      <th></th>\n",
-       "    </tr>\n",
-       "  </thead>\n",
-       "  <tbody>\n",
-       "    <tr>\n",
-       "      <th>cat</th>\n",
-       "      <td>2</td>\n",
-       "      <td>2</td>\n",
-       "    </tr>\n",
-       "    <tr>\n",
-       "      <th>dog</th>\n",
-       "      <td>3</td>\n",
-       "      <td>3</td>\n",
-       "    </tr>\n",
-       "    <tr>\n",
-       "      <th>lama</th>\n",
-       "      <td>0</td>\n",
-       "      <td>1</td>\n",
-       "    </tr>\n",
-       "  </tbody>\n",
-       "</table>\n",
-       "</div>"
-      ],
-      "text/plain": [
-       "        weight  animal\n",
-       "animal                \n",
-       "cat          2       2\n",
-       "dog          3       3\n",
-       "lama         0       1"
-      ]
-     },
-     "execution_count": 17,
-     "metadata": {},
-     "output_type": "execute_result"
-    }
-   ],
-   "source": [
-    "df.groupby(['animal'], dropna=False).apply(lambda x: x.notnull().sum())"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": 9,
-   "metadata": {},
-   "outputs": [
-    {
-     "data": {
-      "text/html": [
-       "<div>\n",
-       "<style scoped>\n",
-       "    .dataframe tbody tr th:only-of-type {\n",
-       "        vertical-align: middle;\n",
-       "    }\n",
-       "\n",
-       "    .dataframe tbody tr th {\n",
-       "        vertical-align: top;\n",
-       "    }\n",
-       "\n",
-       "    .dataframe thead th {\n",
-       "        text-align: right;\n",
-       "    }\n",
-       "</style>\n",
-       "<table border=\"1\" class=\"dataframe\">\n",
-       "  <thead>\n",
-       "    <tr style=\"text-align: right;\">\n",
-       "      <th></th>\n",
-       "      <th>0</th>\n",
-       "      <th>1</th>\n",
-       "    </tr>\n",
-       "  </thead>\n",
-       "  <tbody>\n",
-       "    <tr>\n",
-       "      <th>weight</th>\n",
-       "      <td>0.833333</td>\n",
-       "      <td>5</td>\n",
-       "    </tr>\n",
-       "    <tr>\n",
-       "      <th>animal</th>\n",
-       "      <td>1.000000</td>\n",
-       "      <td>6</td>\n",
-       "    </tr>\n",
-       "  </tbody>\n",
-       "</table>\n",
-       "</div>"
-      ],
-      "text/plain": [
-       "               0  1\n",
-       "weight  0.833333  5\n",
-       "animal  1.000000  6"
-      ]
-     },
-     "execution_count": 9,
-     "metadata": {},
-     "output_type": "execute_result"
-    }
-   ],
-   "source": [
-    "counts = df.groupby(df.columns.to_list(), dropna=False).apply(lambda x: x.notnull().sum()).sum()\n",
-    "perc = df.groupby(df.columns.to_list(), dropna=False).apply(lambda x: x.notnull().sum()).sum() / df.shape[0]\n",
-    "\n",
-    "pd.concat([perc, counts], axis=1)"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": 57,
-   "metadata": {},
-   "outputs": [
-    {
-     "data": {
-      "text/plain": [
-       "A    0.833333\n",
-       "B    1.000000\n",
-       "dtype: float64"
-      ]
-     },
-     "execution_count": 57,
-     "metadata": {},
-     "output_type": "execute_result"
-    }
-   ],
-   "source": [
-    "df.groupby(df.columns.to_list(), dropna=False).apply(lambda x: x.notnull().sum()).sum() / df.shape[0]"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": 36,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "perc = (df.notnull().sum()/ df.shape[0])\n",
-    "counts = df.notnull().sum()\n",
-    "#_.rename(columns={0:\"completeness\"})"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": 37,
-   "metadata": {},
-   "outputs": [
-    {
-     "data": {
-      "text/html": [
-       "<div>\n",
-       "<style scoped>\n",
-       "    .dataframe tbody tr th:only-of-type {\n",
-       "        vertical-align: middle;\n",
-       "    }\n",
-       "\n",
-       "    .dataframe tbody tr th {\n",
-       "        vertical-align: top;\n",
-       "    }\n",
-       "\n",
-       "    .dataframe thead th {\n",
-       "        text-align: right;\n",
-       "    }\n",
-       "</style>\n",
-       "<table border=\"1\" class=\"dataframe\">\n",
-       "  <thead>\n",
-       "    <tr style=\"text-align: right;\">\n",
-       "      <th></th>\n",
-       "      <th>0</th>\n",
-       "      <th>1</th>\n",
-       "    </tr>\n",
-       "  </thead>\n",
-       "  <tbody>\n",
-       "    <tr>\n",
-       "      <th>A</th>\n",
-       "      <td>0.833333</td>\n",
-       "      <td>5</td>\n",
-       "    </tr>\n",
-       "    <tr>\n",
-       "      <th>B</th>\n",
-       "      <td>1.000000</td>\n",
-       "      <td>6</td>\n",
-       "    </tr>\n",
-       "  </tbody>\n",
-       "</table>\n",
-       "</div>"
-      ],
-      "text/plain": [
-       "          0  1\n",
-       "A  0.833333  5\n",
-       "B  1.000000  6"
-      ]
-     },
-     "execution_count": 37,
-     "metadata": {},
-     "output_type": "execute_result"
-    }
-   ],
-   "source": [
-    "pd.concat([perc, counts], axis=1).rename"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": 11,
-   "metadata": {},
-   "outputs": [
-    {
-     "data": {
-      "text/plain": [
-       "survived         0\n",
-       "pclass           0\n",
-       "sex              0\n",
-       "age            177\n",
-       "sibsp            0\n",
-       "parch            0\n",
-       "fare             0\n",
-       "embarked         2\n",
-       "class            0\n",
-       "who              0\n",
-       "adult_male       0\n",
-       "deck           688\n",
-       "embark_town      2\n",
-       "alive            0\n",
-       "alone            0\n",
-       "dtype: int64"
-      ]
-     },
-     "execution_count": 11,
-     "metadata": {},
-     "output_type": "execute_result"
-    }
-   ],
-   "source": [
-    "sns.load_dataset('titanic').isnull().sum()"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": 5,
-   "metadata": {},
-   "outputs": [
-    {
-     "data": {
-      "text/plain": [
-       "A    0\n",
-       "B    1\n",
-       "C    2\n",
-       "D    0\n",
-       "dtype: int64"
-      ]
-     },
-     "execution_count": 5,
-     "metadata": {},
-     "output_type": "execute_result"
-    }
-   ],
-   "source": [
-    "# sum of na per column\n",
-    "df.isnull().sum()"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": 14,
-   "metadata": {},
-   "outputs": [
-    {
-     "data": {
-      "text/html": [
-       "<div>\n",
-       "<style scoped>\n",
-       "    .dataframe tbody tr th:only-of-type {\n",
-       "        vertical-align: middle;\n",
-       "    }\n",
-       "\n",
-       "    .dataframe tbody tr th {\n",
-       "        vertical-align: top;\n",
-       "    }\n",
-       "\n",
-       "    .dataframe thead th {\n",
-       "        text-align: right;\n",
-       "    }\n",
-       "</style>\n",
-       "<table border=\"1\" class=\"dataframe\">\n",
-       "  <thead>\n",
-       "    <tr style=\"text-align: right;\">\n",
-       "      <th></th>\n",
-       "      <th>C</th>\n",
-       "    </tr>\n",
-       "    <tr>\n",
-       "      <th>D</th>\n",
-       "      <th></th>\n",
-       "    </tr>\n",
-       "  </thead>\n",
-       "  <tbody>\n",
-       "    <tr>\n",
-       "      <th>cat</th>\n",
-       "      <td>1</td>\n",
-       "    </tr>\n",
-       "    <tr>\n",
-       "      <th>dog</th>\n",
-       "      <td>1</td>\n",
-       "    </tr>\n",
-       "  </tbody>\n",
-       "</table>\n",
-       "</div>"
-      ],
-      "text/plain": [
-       "     C\n",
-       "D     \n",
-       "cat  1\n",
-       "dog  1"
-      ]
-     },
-     "execution_count": 14,
-     "metadata": {},
-     "output_type": "execute_result"
-    }
-   ],
-   "source": [
-    "# % of na per column\n",
-    "df.groupby(['D']).agg({'C':lambda x: x.isnull().sum()})"
-   ]
-  }
- ],
- "metadata": {
-  "kernelspec": {
-   "display_name": "Python 3",
-   "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.8.0"
-  },
-  "orig_nbformat": 4
- },
- "nbformat": 4,
- "nbformat_minor": 2
-}
diff --git a/bin/test_bambootools.py b/bin/test_bambootools.py
deleted file mode 100644
index 34f90e5..0000000
--- a/bin/test_bambootools.py
+++ /dev/null
@@ -1,9 +0,0 @@
-from BambooTools import bambootools
-import pandas as pd
-import numpy as np
-
-df = pd.DataFrame({'weight':[1, 2, 3, 4, 5, np.nan],
-                   'animal':['cat', 'dog', 'dog', 'dog', 'cat', 'lama']})
-
-
-print(df.bbt.pandas_obj)
\ No newline at end of file
diff --git a/tests/test_bambootools.py b/tests/test_bambootools.py
new file mode 100644
index 0000000..68cc166
--- /dev/null
+++ b/tests/test_bambootools.py
@@ -0,0 +1,75 @@
+import pytest
+from bambootools import bambootools
+import pandas as pd
+import numpy as np
+
+
+@pytest.fixture
+def make_dataset():
+    # Set a seed for reproducibility
+    np.random.seed(0)
+
+    # Define the number of records
+    n_records = 20
+
+    # Define the categories for the 'animal' column
+    animals = ['cat', 'dog', 'lama']
+
+    # Generate random data for the 'animal', 'weight', n 'tail length' columns
+    df = pd.DataFrame({
+        'animal': np.random.choice(animals, n_records),
+        'weight': np.random.randint(1, 100, n_records),
+        'tail length': np.random.randint(1, 100, n_records),
+        'color': np.random.choice(['black', 'white',
+                                   'brown', 'gray'], 
+                                  n_records),
+        'name': [f'name_{i}' for i in range(n_records)]
+    })
+
+    # Insert NULL values in the 'weight', 'tail length' and 'name' columns
+    for col, n_nulls in zip(['weight', 'tail length', 'name'], [3, 5, 1]):
+        null_indices = np.random.choice(df.index, n_nulls, replace=False)
+        df.loc[null_indices, col] = np.nan
+    return df
+
+
+# tests for BambooToolsDfAccessor
+def test_init_dataframe(make_dataset):
+    assert make_dataset.equals(make_dataset.bbt.pandas_obj), (
+        "Expected equal dataframe.")
+
+
+def test_completeness(make_dataset):
+    result = make_dataset.bbt.completeness()
+    assert result.shape == (5, 2), "Wrong table dimensions."
+    assert result['count'].max() <= make_dataset.shape[0], (
+        "Max value of non missing cannot exceed total number of records.")
+    assert result['perc'].max() <= 1.0, (
+        "Max value of perc cannot exceed 1.")
+
+
+def test_completeness_per_group(make_dataset):
+    result = make_dataset.bbt.completeness(by=['animal'])
+    assert result.shape == (3, 8), "Wrong table dimensions."
+    assert result['weight']['perc'].max() <= 1.0, (
+        "Max value of perc cannot exceed 1.")
+
+
+# tests for BambooToolsSeriesAccessor
+def test_init_series(make_dataset):
+    assert make_dataset['weight'].equals(
+        make_dataset['weight'].bbt.series_obj), (
+        "Expected equal series."
+    )
+
+
+def test_above(make_dataset):
+    result = make_dataset['weight'].bbt.above(thresh=30)
+    assert isinstance(result, tuple), "Did not return as tuple."
+    assert result[0] == 10, "Value counts should be 10."
+
+
+def test_below(make_dataset):
+    result = make_dataset['weight'].bbt.below(thresh=30)
+    assert isinstance(result, tuple), "Did not return as tuple."
+    assert result[0] == 6, "Value counts should be 10."