Repository: pyjanitor-devs/pyjanitor

Issue:
Formatting for chemistry tests inconsistent with other tests
Hi, 

I was adding some additional functionality to the chemistry submodule and when I was writing tests I noticed the structure of existing chemistry tests were inconsistent with the tests for the other submodules. The other submodules all get their own testing folder and have one function tested per file. The current test_chemistry.py file has tests for two functions and there is no testing folder. I propose making the structure of the tests for the chemistry submodule consistent with the other test submodules.

Thanks,
Yash Bhalla


Discussion:

Resolving patch:
diff --git a/janitor/chemistry.py b/janitor/chemistry.py
--- a/janitor/chemistry.py
+++ b/janitor/chemistry.py
@@ -16,6 +16,46 @@
     from rdkit.Chem.rdMolDescriptors import (
         GetHashedMorganFingerprint,
         GetMorganFingerprintAsBitVect,
+        CalcChi0n,
+        CalcChi0v,
+        CalcChi1n,
+        CalcChi1v,
+        CalcChi2n,
+        CalcChi2v,
+        CalcChi3n,
+        CalcChi3v,
+        CalcChi4n,
+        CalcChi4v,
+        CalcExactMolWt,
+        CalcFractionCSP3,
+        CalcHallKierAlpha,
+        CalcKappa1,
+        CalcKappa2,
+        CalcKappa3,
+        CalcLabuteASA,
+        CalcNumAliphaticCarbocycles,
+        CalcNumAliphaticHeterocycles,
+        CalcNumAliphaticRings,
+        CalcNumAmideBonds,
+        CalcNumAromaticCarbocycles,
+        CalcNumAromaticHeterocycles,
+        CalcNumAromaticRings,
+        CalcNumAtomStereoCenters,
+        CalcNumBridgeheadAtoms,
+        CalcNumHBA,
+        CalcNumHBD,
+        CalcNumHeteroatoms,
+        CalcNumHeterocycles,
+        CalcNumLipinskiHBA,
+        CalcNumLipinskiHBD,
+        CalcNumRings,
+        CalcNumSaturatedCarbocycles,
+        CalcNumSaturatedHeterocycles,
+        CalcNumSaturatedRings,
+        CalcNumSpiroAtoms,
+        CalcNumUnspecifiedAtomStereoCenters,
+        CalcTPSA,
+        GetMACCSKeysFingerprint,
     )
 except ImportError:
     import_message("chemistry", "rdkit", "conda install -c rdkit rdkit")
@@ -145,3 +185,135 @@ def morgan_fingerprint(
     fpdf = pd.DataFrame(np_fps)
     fpdf.index = df.index
     return fpdf
+
+
+@pf.register_dataframe_method
+def molecular_descriptors(df: pd.DataFrame, mols_col: str):
+    """"
+    Convert a column of RDKIT mol objects into a Pandas DataFrame
+    of molecular descriptors.
+
+    Returns a new dataframe without any of the original data. This is
+    intentional to leave the user only with the data requested.
+
+    The molecular descriptors are from the rdkit.Chem.rdMolDescriptors:
+        Chi0n, Chi0v, Chi1n, Chi1v, Chi2n, Chi2v, Chi3n, Chi3v,
+        Chi4n, Chi4v, ExactMolWt, FractionCSP3, HallKierAlpha, Kappa1,
+        Kappa2, Kappa3, LabuteASA, NumAliphaticCarbocycles,
+        NumAliphaticHeterocycles, NumAliphaticRings, NumAmideBonds,
+        NumAromaticCarbocycles, NumAromaticHeterocycles, NumAromaticRings,
+        NumAtomStereoCenters, NumBridgeheadAtoms, NumHBA, NumHBD,
+        NumHeteroatoms, NumHeterocycles, NumLipinskiHBA, NumLipinskiHBD,
+        NumRings, NumSaturatedCarbocycles, NumSaturatedHeterocycles,
+        NumSaturatedRings, NumSpiroAtoms, NumUnspecifiedAtomStereoCenters,
+        TPSA.
+
+     Method chaining usage:
+
+    .. code-block:: python
+
+        df = pd.DataFrame(...)
+        mol_desc = df.molecular_descriptors(mols_col='mols')
+
+    If you wish to join the molecular descriptors back into the original
+    dataframe, this can be accomplished by doing a `join`,
+    because the indices are preserved:
+
+    ..code-block:: python
+
+        joined = df.join(mol_desc)
+
+    :param df: A pandas DataFrame.
+    :mols_col: The name of the column that has the RDKIT mol objects.
+    :returns: A pandas DataFrame
+    """
+    descriptors = [
+        CalcChi0n,
+        CalcChi0v,
+        CalcChi1n,
+        CalcChi1v,
+        CalcChi2n,
+        CalcChi2v,
+        CalcChi3n,
+        CalcChi3v,
+        CalcChi4n,
+        CalcChi4v,
+        CalcExactMolWt,
+        CalcFractionCSP3,
+        CalcHallKierAlpha,
+        CalcKappa1,
+        CalcKappa2,
+        CalcKappa3,
+        CalcLabuteASA,
+        CalcNumAliphaticCarbocycles,
+        CalcNumAliphaticHeterocycles,
+        CalcNumAliphaticRings,
+        CalcNumAmideBonds,
+        CalcNumAromaticCarbocycles,
+        CalcNumAromaticHeterocycles,
+        CalcNumAromaticRings,
+        CalcNumAtomStereoCenters,
+        CalcNumBridgeheadAtoms,
+        CalcNumHBA,
+        CalcNumHBD,
+        CalcNumHeteroatoms,
+        CalcNumHeterocycles,
+        CalcNumLipinskiHBA,
+        CalcNumLipinskiHBD,
+        CalcNumRings,
+        CalcNumSaturatedCarbocycles,
+        CalcNumSaturatedHeterocycles,
+        CalcNumSaturatedRings,
+        CalcNumSpiroAtoms,
+        CalcNumUnspecifiedAtomStereoCenters,
+        CalcTPSA,
+    ]
+    descriptors = {f.__name__.strip("Calc"): f for f in descriptors}
+
+    feats = dict()
+    for name, func in descriptors.items():
+        feats[name] = [func(m) for m in df[mols_col]]
+    return pd.DataFrame(feats)
+
+
+@pf.register_dataframe_method
+def maccs_keys_fingerprint(df: pd.DataFrame, mols_col: str):
+    """
+    Convert a column of RDKIT mol objects into MACCS Keys Fingeprints.
+
+    Returns a new dataframe without any of the original data.
+    This is intentional to leave the user with the data requested.
+
+    Method chaining usage:
+
+    .. code-block:: python
+
+        df = pd.DataFrame(...)
+        maccs = df.maccs_keys_fingerprint(mols_col='mols')
+
+    If you wish to join the molecular descriptors back into the
+    original dataframe, this can be accomplished by doing a `join`,
+    because the indices are preserved:
+
+    ..code-block:: python
+
+        joined = df.join(maccs_keys_fingerprint)
+
+
+    :param df: A pandas DataFrame.
+    :mols_col: The name of the column that has the RDKIT mol objects.
+    :returns: A pandas DataFrame
+    """
+
+    maccs = [GetMACCSKeysFingerprint(m) for m in df[mols_col]]
+
+    np_maccs = []
+
+    for macc in maccs:
+        arr = np.zeros((1,))
+        DataStructs.ConvertToNumpyArray(macc, arr)
+        np_maccs.append(arr)
+    np_maccs = np.vstack(np_maccs)
+    fmaccs = pd.DataFrame(np_maccs)
+    fmaccs.index = df.index
+    return fmaccs


Test patch:
diff --git a/tests/chemistry/test_maccs_keys_fingerprint.py b/tests/chemistry/test_maccs_keys_fingerprint.py
new file mode 100644
--- /dev/null
+++ b/tests/chemistry/test_maccs_keys_fingerprint.py
@@ -0,0 +1,11 @@
+import pytest
+import janitor.chemistry  # noqa: disable=unused-import
+
+
+@pytest.mark.chemistry
+def test_maccs_keys_fingerprint(chemdf):
+    maccs_keys = chemdf.smiles2mol("smiles", "mol").maccs_keys_fingerprint(
+        "mol"
+    )
+    assert maccs_keys.shape == (10, 167)
+    assert set(maccs_keys.values.flatten().tolist()) == set([0, 1])
diff --git a/tests/chemistry/test_molecular_descriptors.py b/tests/chemistry/test_molecular_descriptors.py
new file mode 100644
--- /dev/null
+++ b/tests/chemistry/test_molecular_descriptors.py
@@ -0,0 +1,8 @@
+import pytest
+import janitor.chemistry  # noqa: disable=unused-import
+
+
+@pytest.mark.chemistry
+def test_molecular_descriptors(chemdf):
+    mol_desc = chemdf.smiles2mol("smiles", "mol").molecular_descriptors("mol")
+    assert mol_desc.shape == (10, 39)
diff --git a/tests/test_chemistry.py b/tests/chemistry/test_morgan_fingerprint.py
similarity index 56%
rename from tests/test_chemistry.py
rename to tests/chemistry/test_morgan_fingerprint.py
--- a/tests/test_chemistry.py
+++ b/tests/chemistry/test_morgan_fingerprint.py
@@ -1,21 +1,5 @@
-import os
-from pathlib import Path
-
-import pandas as pd
 import pytest
-
-import janitor.chemistry
-
-
-@pytest.mark.parametrize("progressbar", [None, "terminal"])
-@pytest.mark.chemistry
-def test_smiles2mol(chemdf, progressbar):
-    from rdkit import Chem
-
-    chemdf = chemdf.smiles2mol("smiles", "mol", progressbar)
-    assert "mol" in chemdf.columns
-    for elem in chemdf["mol"]:
-        assert isinstance(elem, Chem.rdchem.Mol)
+import janitor.chemistry  # noqa: disable=unused-import
 
 
 @pytest.mark.chemistry
diff --git a/tests/chemistry/test_smiles2mol.py b/tests/chemistry/test_smiles2mol.py
new file mode 100644
--- /dev/null
+++ b/tests/chemistry/test_smiles2mol.py
@@ -0,0 +1,12 @@
+import pytest
+
+
+@pytest.mark.parametrize("progressbar", [None, "terminal"])
+@pytest.mark.chemistry
+def test_smiles2mol(chemdf, progressbar):
+    from rdkit import Chem
+
+    chemdf = chemdf.smiles2mol("smiles", "mol", progressbar)
+    assert "mol" in chemdf.columns
+    for elem in chemdf["mol"]:
+        assert isinstance(elem, Chem.rdchem.Mol)