{"cells":[{"metadata":{},"cell_type":"markdown","source":"# Summary\n\nThis kernel is based on a seriously underrated kernel by BigIronSphere: [Molecular Geometry - Feature Engineering & EDA](https://www.kaggle.com/bigironsphere/molecular-geometry-feature-engineering-eda), in which he computed four extremely good features for small molecules, and he used [HOW TO: Easy Visualization of Molecule](https://www.kaggle.com/borisdee/how-to-easy-visualization-of-molecules) to show some feature examples. \n\nIn BigIronSphere's original kernel, computing the only 4 features for all train and test data takes 13 hours! I profiled his code offline and found that the culprit is `pandas`!!!! Everytime a huge dataframe is queried, it takes hundreds more time than reading an `xyz` from structures folder. I would like to demostrate how to make use of the `xyz` files provided by the competition organizers.\n\nFor example, you can speed up [the bonds calculation](https://www.kaggle.com/scaomath/parallelization-of-coulomb-yukawa-interaction) even more!\n\nLastly, multiprocessing is applied the groupby iterator to speed up the computation even more.\n\nIf you like to skip the explanation and directly the use the features, just concat the features with the train and test dataframe you are good to go."},{"metadata":{"_kg_hide-output":true,"_kg_hide-input":false,"trusted":true},"cell_type":"code","source":"!pip install ase","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"_kg_hide-input":true},"cell_type":"code","source":"import numpy as np \nimport pandas as pd \nfrom tqdm import tqdm, tqdm_notebook\nimport matplotlib.pyplot as plt\nimport math\nfrom numpy.linalg import svd, norm\nfrom multiprocessing import Pool\n\nimport warnings\nwarnings.filterwarnings('ignore')","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"_kg_hide-input":true},"cell_type":"code","source":"import ase\nfrom ase import Atoms\nimport ase.visualize\ndef view(molecule):\n    # Select a molecule\n    mol = struct[struct['molecule_name'] == molecule]\n    \n    # Get atomic coordinates\n    xcart = mol.iloc[:, 3:].values\n    \n    # Get atomic symbols\n    symbols = mol.iloc[:, 2].values\n    \n    # Display molecule\n    system = Atoms(positions=xcart, symbols=symbols)\n    print('Molecule Name: %s.' %molecule)\n    return ase.visualize.view(system, viewer=\"x3d\")","execution_count":null,"outputs":[]},{"metadata":{"_cell_guid":"79c7e3d0-c299-4dcb-8224-4455121ee9b0","_uuid":"d629ff2d2480ee46fbb7e2d37f6b5fab8052498a","trusted":true},"cell_type":"code","source":"train = pd.read_csv('../input/train.csv')\ntest = pd.read_csv('../input/test.csv')\nstruct = pd.read_csv('../input/structures.csv')","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"all_molecule_names = struct['molecule_name'].unique()","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"In the original kernel, SVD function only returns the first eigenvector, which represents the principle direction of the molecule (the direction with the most variance in terms of coordinates distribution). Here I changed it a little bit so we can have more features generated. "},{"metadata":{"trusted":true},"cell_type":"code","source":"#quick PCA via singular value decomp\ndef PCA_SVD(a, n_vec = 1):\n    a_mean = a.mean(axis=1)\n    #normalize\n    a = (a.T - a_mean.T).T\n    u, s, v = svd(a.T)\n    return v[:n_vec], s[:n_vec]\n\n#obtain plane with minimum sum distance from nuclei\ndef get_plane(a):\n    a = np.reshape(a, (a.shape[0], -1))\n    centroid = a.mean(axis=1)\n    #normalise\n    x = a - centroid[:,np.newaxis]\n    m = np.dot(x, x.T) \n    return centroid, svd(m)[0][:,-1]","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# New geometric features\n\nFor an $n\\times 3$ matrix which represents a point cloud, we can have 3 singular values by doing SVD. Denote these 3 singular values by $s_i (i=1,2,3)$ sorted from the biggest to the smallest. \n\n* `thinness_metric` is $(s_2^2+s_3^2)/s_1^2$, if the molecule is thin and long, then this number should be close to 0.\n* `flatness_metric` is now changed to $s_3^2/(s_1^2+s_2^2)$, if the molecule is flat, this number should be close to 0, and there is no $\\leq 3$ atoms molecule problem like the old one where `np.nan` has to be filled.\n* `roundness_metric` is $s_2/s_1$, if the molecule is flat or can be inscribed in a ball, this number should be close to 1.\n* `bond_angle_plane` and `bond_angle_axis` are unchanged from BigIronSphere's original kernel, which represents the angles of the bonds with the plane and principle eigenvector respectively.\n\nThe first three features are all relative, which may describe some invariant geometric features no matter a molecule is big or small."},{"metadata":{},"cell_type":"markdown","source":"# Benchmark\n\nWe benchmark the code using first 100 molecule from the structures, which contains about 70 molecules in the train."},{"metadata":{"trusted":true},"cell_type":"code","source":"N_mol = 100\nmolecule_names = all_molecule_names[:N_mol]\nsmall_idx = train.molecule_name.isin(molecule_names)\nsmall_train = train.loc[small_idx]\n\nprint(f\"There are {small_train.molecule_name.nunique()} molecules in the train to benchmark.\")","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"%%time\nsize_list = []\nflatness_list = []\nthinness_list = []\nroundness_list = []\nbond_angle_plane = []\nbond_angle_axis = []\n\nfor i in tqdm_notebook(range(len(molecule_names))):\n    \n    mol = molecule_names[i]\n    #obtain structure and bond information for each molecule\n    temp_struct = struct.loc[struct.molecule_name==mol, :]\n    bonds = train.loc[train.molecule_name == mol, :]\n    \n    #number of constituent atoms\n    size = len(temp_struct)\n    size_list.extend(np.full(len(bonds), size))\n    \n    #nuclei coords\n    coords = np.column_stack([temp_struct.x.values,\n                              temp_struct.y.values,\n                              temp_struct.z.values]).T\n    \n    #principal axis of molecular alignment\n    axis_vector_all, singular_val_all = PCA_SVD(coords, n_vec=3)\n    axis_vector = axis_vector_all[0] # major axis vector\n    \n    thinness = (singular_val_all[1]**2 + singular_val_all[2]**2)/singular_val_all[0]**2\n    thinness_list.extend(np.full(len(bonds), thinness))\n    \n    flatness = singular_val_all[2]**2/(singular_val_all[0]**2 + singular_val_all[1]**2)\n    flatness_list.extend(np.full(len(bonds), flatness))\n    \n    roundness = singular_val_all[1]/singular_val_all[0]\n    roundness_list.extend(np.full(len(bonds), roundness))\n    \n    \n    #obtain flatness metric and plane angles for binds if nuclei > 3\n    if size > 3:\n        \n        coords = coords - coords.mean()\n        #calculate best fitting 2D plane\n        ctr, norm = get_plane(coords) \n        \n        #calculate distances of each atom from plane\n        dists = np.zeros(size)\n        for j in range(0, size):\n            dists[j] = np.dot(norm, coords[:, j] - ctr) \n        \n        #obtain angle subtended by each atomic bind and plane\n        #print('There are {} bonds to calculate.'.format(len(bonds)))\n        for j in range(0, len(bonds)):\n            \n            #obtain atom index numbers for this bond\n            atom1 = bonds.atom_index_0.values[j]\n            atom2 = bonds.atom_index_1.values[j]\n            \n            #get 3D coords\n            atom1_coords = coords[:, atom1]\n            atom2_coords = coords[:, atom2]\n            \n            #bond vector\n            atom_vec = np.array([atom1_coords[0] - atom2_coords[0],\n                                 atom1_coords[1] - atom2_coords[1],\n                                 atom1_coords[2] - atom2_coords[2]])\n            \n            #angle subtended by bond vector and molecule plane\n            angle = np.dot(norm, atom_vec)/(np.linalg.norm(norm)*np.linalg.norm(atom_vec))\n            axis_angle = np.dot(axis_vector, atom_vec)/(np.linalg.norm(norm)*np.linalg.norm(atom_vec))\n            \n            #standardise to degrees <= 90\n            angle = np.arccos(angle)\n            axis_angle = np.arccos(axis_angle)\n\n            if angle > np.pi/2:\n                angle = np.pi - angle\n            \n            if axis_angle > np.pi/2:\n                axis_angle = np.pi - axis_angle\n                \n            #not needed but somewhat easier to visualise\n            angle = np.pi/2 - angle\n            axis_angle = np.pi/2 - axis_angle\n            bond_angle_plane.append(angle)\n            bond_angle_axis.append(axis_angle)\n               \n    else:\n        bond_angle_plane.extend(np.full(len(bonds), np.nan))\n        \n        for j in range(0, len(bonds)):\n            \n            #obtain atom index numbers for this bond\n            atom1 = bonds.atom_index_0.values[j]\n            atom2 = bonds.atom_index_1.values[j]\n            \n            #get 3D coords\n            atom1_coords = coords[:, atom1]\n            atom2_coords = coords[:, atom2]\n            \n            #bond vector\n            atom_vec = np.array([atom1_coords[0] - atom2_coords[0],\n                                 atom1_coords[1] - atom2_coords[1],\n                                 atom1_coords[2] - atom2_coords[2]])\n            \n            #angle subtended by bond vector and molecule principal axis\n            axis_angle = np.dot(axis_vector, atom_vec)/(np.linalg.norm(axis_vector)*np.linalg.norm(atom_vec))\n            \n            #standardise to degrees <= 90\n            axis_angle = np.arccos(axis_angle)\n                 \n            if axis_angle > np.pi/2:\n                axis_angle = np.pi - axis_angle\n                \n            #not needed but somewhat easier to visualise\n            axis_angle = np.pi/2 - axis_angle\n            bond_angle_axis.append(axis_angle)\n  ","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"## Manipulating xyz files\n\nThe code above takes about ~53 seconds to run for about 70 molecules/~1800 rows of the train...But we have 7 million rows which you guys can compute... Now let us optimize the code above by reading `xyz` files instead of reading the position from the `struct` dataframe. Except reading coordinates from the `xyz` files, the main changes I have made are:\n* the bonds features are now extracted using `groupby`, so that the for loop does not query molecule name that is not the dataframe (molecule names that are in structures may not be in the train which further makes the execution slower)."},{"metadata":{"trusted":true},"cell_type":"code","source":"folder = '../input/structures/'","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Simple example using 1 molecule `dsgdb9nsd_133885` which has 16 atoms."},{"metadata":{"trusted":true},"cell_type":"code","source":"with open(folder + \"/dsgdb9nsd_133885.xyz\") as f:\n    positions = []\n    symbols = []\n    for row, line in enumerate(f):\n        \n        print(row, line.replace('\\n', ''))\n        \n        fields = line.split(' ')\n        \n        if row < 2: \n            continue\n        \n        # Then rows of atomic positions and chemical symbols.\n        else:\n            positions.append(fields[1:4])\n            print(f\"{fields[0]} -> {symbols}\\n\")\n            symbols.append(fields[0])       \n\nprint(f\"Number of atoms: {len(symbols)}\")","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Now onto the same routine."},{"metadata":{"trusted":true},"cell_type":"code","source":"%%time\nflatness_list = []\nthinness_list = []\nroundness_list = []\nbond_angle_plane = []\nbond_angle_axis = []\n\nN = small_train['molecule_name'].nunique()\n\nwith tqdm(total=N) as pbar:\n    \n    for i, (mol, bonds) in enumerate(small_train.groupby(['molecule_name'])):\n        pbar.update(1)\n        ## instead of querying structures dataframe, we read from xyz file\n        filename = folder + mol + '.xyz'\n        positions = []\n        with open(filename) as f:\n            for row, line in enumerate(f):\n                fields = line.split(' ')\n                if row < 2:\n                    continue\n                # Then rows of atomic positions and chemical symbols.\n                else:\n                    positions.append(fields[1:4])\n\n        size = len(positions)\n        n_bonds = len(bonds)\n\n        #nuclei coords\n        coords = np.array(positions, dtype=float).T\n\n        #principal axis of molecular alignment\n        axis_vector_all, singular_val_all = PCA_SVD(coords, n_vec=3)\n        axis_vector = axis_vector_all[0] # major axis vector\n\n        thinness = (singular_val_all[1]**2 + singular_val_all[2]**2)/singular_val_all[0]**2\n        thinness_list.extend(np.full(n_bonds, thinness))\n\n        flatness = singular_val_all[2]**2/(singular_val_all[0]**2 + singular_val_all[1]**2)\n        flatness_list.extend(np.full(n_bonds, flatness))\n\n        roundness = singular_val_all[1]/singular_val_all[0]\n        roundness_list.extend(np.full(n_bonds, roundness))\n\n        \n        #obtain flatness metric and plane angles for binds if nuclei > 3\n        if size > 3:\n\n            coords = coords - coords.mean()\n            #calculate best fitting 2D plane\n            ctr, norm = get_plane(coords) \n\n            #calculate distances of each atom from plane\n            dists = np.zeros(size)\n            for j in range(0, size):\n                dists[j] = np.dot(norm, coords[:, j] - ctr) \n\n            #obtain angle subtended by each atomic bind and plane\n            #print('There are {} bonds to calculate.'.format(len(bonds)))\n            for j in range(0, n_bonds):\n\n                #obtain atom index numbers for this bond\n                atom1 = bonds.atom_index_0.values[j]\n                atom2 = bonds.atom_index_1.values[j]\n\n                #get 3D coords\n                atom1_coords = coords[:, atom1]\n                atom2_coords = coords[:, atom2]\n\n                #bond vector\n                atom_vec = np.array([atom1_coords[0] - atom2_coords[0],\n                                     atom1_coords[1] - atom2_coords[1],\n                                     atom1_coords[2] - atom2_coords[2]])\n\n                #angle subtended by bond vector and molecule plane\n                angle = np.dot(norm, atom_vec)/(np.linalg.norm(norm)*np.linalg.norm(atom_vec))\n                axis_angle = np.dot(axis_vector, atom_vec)/(np.linalg.norm(norm)*np.linalg.norm(atom_vec))\n\n                #standardise to degrees <= 90\n                angle = np.arccos(angle)\n                axis_angle = np.arccos(axis_angle)\n\n                if angle > np.pi/2:\n                    angle = np.pi - angle\n\n                if axis_angle > np.pi/2:\n                    axis_angle = np.pi - axis_angle\n\n                #not needed but somewhat easier to visualise\n                angle = np.pi/2 - angle\n                axis_angle = np.pi/2 - axis_angle\n                bond_angle_plane.append(angle)\n                bond_angle_axis.append(axis_angle)\n\n        else:\n            bond_angle_plane.extend(np.full(n_bonds, np.nan))\n\n            for j in range(0, n_bonds):\n\n                #obtain atom index numbers for this bond\n                atom1 = bonds.atom_index_0.values[j]\n                atom2 = bonds.atom_index_1.values[j]\n\n                #get 3D coords\n                atom1_coords = coords[:, atom1]\n                atom2_coords = coords[:, atom2]\n\n                #bond vector\n                atom_vec = np.array([atom1_coords[0] - atom2_coords[0],\n                                     atom1_coords[1] - atom2_coords[1],\n                                     atom1_coords[2] - atom2_coords[2]])\n\n                #angle subtended by bond vector and molecule principal axis\n                axis_angle = np.dot(axis_vector, atom_vec)/(np.linalg.norm(axis_vector)*np.linalg.norm(atom_vec))\n\n                #standardise to degrees <= 90\n                axis_angle = np.arccos(axis_angle)\n\n                if axis_angle > np.pi/2:\n                    axis_angle = np.pi - axis_angle\n\n                #not needed but somewhat easier to visualise\n                axis_angle = np.pi/2 - axis_angle\n                bond_angle_axis.append(axis_angle)   ","execution_count":null,"outputs":[]},{"metadata":{"_kg_hide-input":true,"trusted":true},"cell_type":"code","source":"def get_geometric_features(df):\n    mol = df.molecule_name.values[0]\n    bonds = df\n    filename = folder + mol + '.xyz'\n    positions = []\n    flatness_list = []\n    thinness_list = []\n    roundness_list = []\n    bond_angle_plane = []\n    bond_angle_axis = []\n    \n    with open(filename) as f:\n        for row, line in enumerate(f):\n            fields = line.split(' ')\n            if row < 2:\n                continue\n            # Then rows of atomic positions and chemical symbols.\n            else:\n                positions.append(fields[1:4])\n\n    size = len(positions)\n    n_bonds = len(df)\n\n    #nuclei coords\n    coords = np.array(positions, dtype=float).T\n\n    #principal axis of molecular alignment\n    axis_vector_all, singular_val_all = PCA_SVD(coords, n_vec=3)\n    axis_vector = axis_vector_all[0] # major axis vector\n\n    thinness = (singular_val_all[1]**2 + singular_val_all[2]**2)/singular_val_all[0]**2\n    thinness_list.extend(np.full(n_bonds, thinness))\n\n    flatness = singular_val_all[2]**2/(singular_val_all[0]**2 + singular_val_all[1]**2)\n    flatness_list.extend(np.full(n_bonds, flatness))\n\n    roundness = singular_val_all[1]/singular_val_all[0]\n    roundness_list.extend(np.full(n_bonds, roundness))\n\n\n    #obtain flatness metric and plane angles for binds if nuclei > 3\n    if size > 3:\n\n        coords = coords - coords.mean()\n        #calculate best fitting 2D plane\n        ctr, norm = get_plane(coords) \n\n        #calculate distances of each atom from plane\n        dists = np.zeros(size)\n        for j in range(0, size):\n            dists[j] = np.dot(norm, coords[:, j] - ctr) \n\n        #obtain angle subtended by each atomic bind and plane\n        #print('There are {} bonds to calculate.'.format(len(bonds)))\n        for j in range(0, n_bonds):\n\n            #obtain atom index numbers for this bond\n            atom1 = bonds.atom_index_0.values[j]\n            atom2 = bonds.atom_index_1.values[j]\n            \n            #get 3D coords\n            atom1_coords = coords[:, atom1]\n            atom2_coords = coords[:, atom2]\n\n            #bond vector\n            atom_vec = np.array([atom1_coords[0] - atom2_coords[0],\n                                 atom1_coords[1] - atom2_coords[1],\n                                 atom1_coords[2] - atom2_coords[2]])\n\n            #angle subtended by bond vector and molecule plane\n            angle = np.dot(norm, atom_vec)/(np.linalg.norm(norm)*np.linalg.norm(atom_vec))\n            axis_angle = np.dot(axis_vector, atom_vec)/(np.linalg.norm(norm)*np.linalg.norm(atom_vec))\n            \n            #standardise to degrees <= 90\n            angle = np.arccos(angle)\n            axis_angle = np.arccos(axis_angle)\n        \n            if angle > np.pi/2:\n                angle = np.pi - angle\n\n            if axis_angle > np.pi/2:\n                axis_angle = np.pi - axis_angle\n\n            #not needed but somewhat easier to visualise\n            angle = np.pi/2 - angle\n            axis_angle = np.pi/2 - axis_angle\n            bond_angle_plane.append(angle)\n            bond_angle_axis.append(axis_angle)\n\n    else:\n        bond_angle_plane.extend(np.full(n_bonds, np.nan))\n\n        for j in range(0, n_bonds):\n\n            #obtain atom index numbers for this bond\n            atom1 = bonds.atom_index_0.values[j]\n            atom2 = bonds.atom_index_1.values[j]\n\n            #get 3D coords\n            atom1_coords = coords[:, atom1]\n            atom2_coords = coords[:, atom2]\n\n            #bond vector\n            atom_vec = np.array([atom1_coords[0] - atom2_coords[0],\n                                 atom1_coords[1] - atom2_coords[1],\n                                 atom1_coords[2] - atom2_coords[2]])\n\n            #angle subtended by bond vector and molecule principal axis\n            axis_angle = np.dot(axis_vector, atom_vec)/(np.linalg.norm(axis_vector)*np.linalg.norm(atom_vec))\n\n            #standardise to degrees <= 90\n            axis_angle = np.arccos(axis_angle)\n\n            if axis_angle > np.pi/2:\n                axis_angle = np.pi - axis_angle\n\n            #not needed but somewhat easier to visualise\n            axis_angle = np.pi/2 - axis_angle\n            bond_angle_axis.append(axis_angle) \n\n    features = pd.DataFrame(index=df.index, dtype=np.float32)\n    features['flatness_metric'] = np.asarray(flatness_list)\n    features['bond_angle_plane'] = np.asarray(bond_angle_plane)\n    features['bond_angle_axis'] = np.asarray(bond_angle_axis)\n    features['thinness_metric'] = np.asarray(thinness_list)\n    features['roundness_metric'] = np.asarray(roundness_list)\n    \n    return features","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"## Multiprocessing\n\nThe same code takes about ~0.25s, which is 250 times speed up...Yet when applying to the full training dataframe, it takes about 8 minutes to generate all the features.\n\nYet here we want to utilize all 4 CPU cores, without considering the setup overhead, the speed is about triple of the original, which should take only about 4 minutes for setting up and processing for all the training data."},{"metadata":{"trusted":true},"cell_type":"code","source":"import multiprocessing as mp","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"chunk_iter = small_train.groupby(['molecule_name'])\npool = mp.Pool(4) # use 4 CPU cores\n\nfunclist = []\nfor df in tqdm_notebook(chunk_iter):\n    # process each data frame\n    f = pool.apply_async(get_geometric_features,[df[1]])\n    funclist.append(f)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"%%time\nresult = []\nfor f in tqdm_notebook(funclist):\n    result.append(f.get()) \n\n# combine chunks with transformed data into a single training set\nfeatures = pd.concat(result, ignore_index=True)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# Feature Evaluation\n\nNow we can examine some of the results. Let's start with a molecule that has a high flatness metric. Recall that the higher this metric, the *less* flat a molecule is."},{"metadata":{"trusted":true},"cell_type":"code","source":"features.shape","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"features.sort_values('flatness_metric', ascending=False).head(5)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"The least flat molecule is location 0. Let's view it... (it is a tetrahedron, so...)"},{"metadata":{"trusted":true},"cell_type":"code","source":"view(train.iloc[0]['molecule_name'])","execution_count":null,"outputs":[]},{"metadata":{"_kg_hide-input":false,"trusted":true},"cell_type":"code","source":"features.sort_values('flatness_metric', ascending=True).head(5)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"The most flat (as well as thinnest and longest) molecule is train location 292."},{"metadata":{"trusted":true},"cell_type":"code","source":"view(train.iloc[292]['molecule_name'])","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# Generating the feature files\n\nNow let us generate all the features for train and test."},{"metadata":{"trusted":true},"cell_type":"code","source":"def get_features(df):\n    chunk_iter = df.groupby(['molecule_name'])\n    pool = mp.Pool(4) # use 4 CPU cores\n\n    funclist = []\n    for df in tqdm_notebook(chunk_iter):\n        # process each data frame\n        f = pool.apply_async(get_geometric_features,[df[1]])\n        funclist.append(f)\n\n    result = []\n    for f in tqdm_notebook(funclist):\n        result.append(f.get()) \n\n    # combine chunks with transformed data into a single training set\n    features = pd.concat(result, ignore_index=True)\n    \n    return features","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"%%time\ntrain_features = get_features(train)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"train_features.to_csv('train_geometric_features.csv',index=False)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"%%time\ntest_features = get_features(test)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"test_features.to_csv('test_geometric_features.csv',index=False)","execution_count":null,"outputs":[]}],"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"name":"python","version":"3.6.4","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"}},"nbformat":4,"nbformat_minor":1}