Split QM9 by SMILES

In this example notebook, we will discuss how to use DataSAIL to compute split for the QM9 dataset to compute more challenging splits for machine learning models. Therefore, we first import all necessary tools.

[1]:
%%capture
import deepchem as dc
from rdkit import Chem
from datasail.sail import datasail

Load the Dataset

Load the dataset from deepchem. As the deepchem dataset contains the targets of QM9 as y1 to y12 as well as weights for every task. Therefore, we rename the targets and remove the weights. Finally, we also convert all RDKit molecules to SMILES strings.

[2]:
from rdkit import rdBase
blocker = rdBase.BlockLogs()

def mol2smiles(mol):
    try:
        return Chem.MolToSmiles(Chem.rdmolops.RemoveHs(mol))
    except:
        return None

dataset = dc.molnet.load_qm9(featurizer=dc.feat.DummyFeaturizer(), splitter=None)[1][0]
df = dataset.to_dataframe()
df.rename(columns=dict([(f"y{i + 1}", task) for i, task in enumerate(dataset.tasks)] + [("X", "SMILES")]), inplace=True)
df["SMILES"] = df["SMILES"].apply(mol2smiles)
df = df.dropna(subset=["SMILES"])
df["ID"] = [f"Comp{i + 1:06d}" for i in range(len(df))]
df = df[["ID", "SMILES"] + dataset.tasks.tolist()]
df
[2]:
ID SMILES mu alpha homo lumo gap r2 zpve cv u0 u298 h298 g298
0 Comp000001 C -1.695514 -5.140947 -5.551545 1.965764 4.744480 -3.370877 -2.278929 -4.317699 6.607249 6.607212 6.607212 6.607387
1 Comp000002 N -0.560317 -5.574660 -0.343349 1.281473 1.451169 -3.414138 -2.606638 -4.354192 6.229272 6.229231 6.229231 6.229431
2 Comp000003 O -0.402845 -5.938979 -1.769924 0.997352 1.882555 -3.447759 -3.016092 -4.429086 5.761037 5.760991 5.760991 5.761238
3 Comp000004 C#C -1.695514 -4.785881 -1.439182 0.635197 1.355305 -3.257365 -2.843707 -3.815621 5.739750 5.739706 5.739706 5.739920
4 Comp000005 C#N 0.325228 -5.166392 -4.463681 0.004929 2.244040 -3.307999 -3.166654 -4.363256 5.360441 5.360385 5.360385 5.360611
... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
132475 Comp132193 C1[C@H]2[C@@H]3[C@H]2[N@H+]2[C@@H]4C[C@]12[C@H]34 -0.706407 -0.695101 -3.100863 -0.711376 0.844033 0.469551 -1.241336 -0.050394 -0.434861 -0.434856 -0.434856 -0.434869
132476 Comp132194 C1[C@H]2[C@@H]3[C@H]4[C@H]5O[C@@]13[C@@H]2[C@H]54 -0.112342 0.703189 -0.144106 -0.931470 -0.857544 0.936507 0.150709 0.962825 0.294224 0.294260 0.294260 0.294169
132477 Comp132195 C1[N@H+]2[C@@H]3[C@H]2[C@H]2[N@@H+]4C[C@]12[C@... 0.450717 0.063607 -0.984909 -1.223594 -0.727729 0.617526 -0.169841 0.431887 -0.085259 -0.085238 -0.085238 -0.085293
132478 Comp132196 C1[C@H]2[C@@H]3[C@H]2[C@H]2[N@@H+]4C[C@]12[C@H]34 0.707701 0.382820 -0.128167 -0.003074 0.061148 0.565689 -0.168043 0.597179 -0.084153 -0.084130 -0.084130 -0.084187
132479 Comp132197 C1[N@@H+]2[C@H]3[C@@H]4[C@@H]5O[C@]13[C@H]2[C@... 0.571597 -0.166550 -0.905212 -0.403245 0.053159 0.448744 -0.496793 0.128255 -0.463497 -0.463485 -0.463485 -0.463519

132197 rows × 14 columns

Run DataSAIL

Use DataSAIL to split the data into an identity-based single cold split (I1e) and a cluster-based single cold split (C1e). We define - the techniques as list: I1e abd C1e The e in the end is important to split the e-data. - the spits as list. The values will be normalized to ratios. - the names as list. Similarly to the list of split sizes, DataSAIL needs names to name the splits. - the number of runs. This will determine how many different splits to compute per technique to compute. - the solving algorithm for optimizing the final problem formulation. - the type of the dataset in the first axis. - the data as mapping from IDs to SMILES strings.

For an extensive description of the arguments please refer to the according pages of the documentation.

Given there exist a CSV or TSV file storing the data as described in the documentation, the according call to DataSAIL in the commandline would be:

$ datasail -t C1e -s 7 2 1 -n train val test -r 3 --solver SCIP --e-type M --e-data <filepath>
[3]:
%%capture
e_splits, f_splits, inter_splits = datasail(
    techniques=["C1e"],
    splits=[7, 2, 1],
    names=["train","val", "test"],
    runs=3,
    solver="SCIP",
    e_type="M",
    e_data=dict(df[["ID", "SMILES"]].values.tolist())
)

The output

Finally, we inspect the e_split object as this holds all the assignments of the datapoints to the splits, for each run and each technique. First, the overall architecture is described, lastly we look at the first five assignments of the C1 run 0.

[4]:
print(type(e_splits))
for key in e_splits.keys():
    print(f"{key} - Type: {type(e_splits[key])} - Length: {len(e_splits[key])}")
    for run in range(len(e_splits[key])):
        print(f"\tRun {run + 1} - Type: {type(e_splits[key][run])} - {len(e_splits[key][run])} assignments")
print("\n" + "\n".join(f"ID: {idx} - Split: {split}" for idx, split in list(e_splits[key][0].items())[:5]))
<class 'dict'>
C1e - Type: <class 'list'> - Length: 3
        Run 1 - Type: <class 'dict'> - 132197 assignments
        Run 2 - Type: <class 'dict'> - 132197 assignments
        Run 3 - Type: <class 'dict'> - 132197 assignments

ID: Comp000001 - Split: val
ID: Comp000002 - Split: val
ID: Comp000003 - Split: val
ID: Comp000004 - Split: train
ID: Comp000005 - Split: train