diff --git a/exploratory_analysis.ipynb b/exploratory_analysis.ipynb index be3fc4634c61122daa077f062e519fc0a28c6cf5..051b4c1334d9a29fd0d23c31a06ed0f935f61efe 100644 --- a/exploratory_analysis.ipynb +++ b/exploratory_analysis.ipynb @@ -95,6 +95,15 @@ "We load below the packages required for the tutorial. Most of the clustering and embedding algorithms are contained in the scikit-learn and SciPy packages. We use Panda's dataframe for manipulating our dataset." ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "!pip install plotly" + ] + }, { "cell_type": "code", "execution_count": null, @@ -102,23 +111,30 @@ "ExecuteTime": { "end_time": "2022-01-28T12:24:04.045301Z", "start_time": "2022-01-28T12:24:03.255791Z" - } + }, + "scrolled": true }, "outputs": [], "source": [ - "from ase.io import read\n", - "import pandas as pd\n", "import numpy as np\n", - "from scipy.cluster.hierarchy import dendrogram, linkage, cut_tree\n", + "import pandas as pd\n", + "\n", + "from ase.io import read\n", + "\n", "from sklearn import preprocessing\n", "from sklearn.cluster import KMeans, DBSCAN, AgglomerativeClustering\n", "from sklearn.decomposition import PCA\n", "from sklearn.manifold import TSNE, MDS\n", + "\n", + "from scipy.cluster.hierarchy import dendrogram, linkage, cut_tree\n", + "\n", "import hdbscan\n", + "\n", "import plotly.graph_objects as go\n", + "import matplotlib.pyplot as plt\n", + "\n", "import ipywidgets as widgets\n", - "from IPython.display import display, clear_output\n", - "import matplotlib.pyplot as plt" + "from IPython.display import display, clear_output" ] }, { @@ -150,44 +166,69 @@ "outputs": [], "source": [ "# load data\n", - "RS_structures = read(\"data/exploratory_analysis/octet_binaries/RS_structures.xyz\", index=':')\n", - "ZB_structures = read(\"data/exploratory_analysis/octet_binaries/ZB_structures.xyz\", index=':')\n", + "RS_structures = read(\"data/exploratory_analysis/octet_binaries/RS_structures.xyz\", index=\":\")\n", + "ZB_structures = read(\"data/exploratory_analysis/octet_binaries/ZB_structures.xyz\", index=\":\")\n", "\n", - "def generate_table(RS_structures, ZB_structures):\n", "\n", + "def generate_table(RS_structures, ZB_structures):\n", " for RS, ZB in zip(RS_structures, ZB_structures):\n", - " energy_diff = RS.info['energy'] - ZB.info['energy']\n", - " min_struc_type = 'RS' if energy_diff < 0 else 'ZB'\n", - " struc_obj_min = RS if energy_diff < 0 else ZB\n", - "\n", - " yield [RS.info['energy'], ZB.info['energy'],\n", - " energy_diff, min_struc_type,\n", - " RS.info['Z'], ZB.info['Z'],\n", - " RS.info['period'], ZB.info['period'],\n", - " RS.info['IP'], ZB.info['IP'],\n", - " RS.info['EA'], ZB.info['EA'],\n", - " RS.info['E_HOMO'], ZB.info['E_HOMO'],\n", - " RS.info['E_LUMO'], ZB.info['E_LUMO'],\n", - " RS.info['r_s'], ZB.info['r_s'],\n", - " RS.info['r_p'], ZB.info['r_p'],\n", - " RS.info['r_d'], ZB.info['r_d']]\n", + " energy_diff = RS.info[\"energy\"] - ZB.info[\"energy\"]\n", + " min_struc_type = \"RS\" if energy_diff < 0 else \"ZB\"\n", + "\n", + " yield [\n", + " RS.info[\"energy\"],\n", + " ZB.info[\"energy\"],\n", + " energy_diff,\n", + " min_struc_type,\n", + " RS.info[\"Z\"],\n", + " ZB.info[\"Z\"],\n", + " RS.info[\"period\"],\n", + " ZB.info[\"period\"],\n", + " RS.info[\"IP\"],\n", + " ZB.info[\"IP\"],\n", + " RS.info[\"EA\"],\n", + " ZB.info[\"EA\"],\n", + " RS.info[\"E_HOMO\"],\n", + " ZB.info[\"E_HOMO\"],\n", + " RS.info[\"E_LUMO\"],\n", + " ZB.info[\"E_LUMO\"],\n", + " RS.info[\"r_s\"],\n", + " ZB.info[\"r_s\"],\n", + " RS.info[\"r_p\"],\n", + " ZB.info[\"r_p\"],\n", + " RS.info[\"r_d\"],\n", + " ZB.info[\"r_d\"],\n", + " ]\n", "\n", "\n", "df = pd.DataFrame(\n", " generate_table(RS_structures, ZB_structures),\n", - " columns=['energy_RS', 'energy_ZB',\n", - " 'energy_diff', 'min_struc_type',\n", - " 'Z(A)', 'Z(B)',\n", - " 'period(A)', 'period(B)',\n", - " 'IP(A)', 'IP(B)',\n", - " 'EA(A)', 'EA(B)',\n", - " 'E_HOMO(A)', 'E_HOMO(B)',\n", - " 'E_LUMO(A)', 'E_LUMO(B)',\n", - " 'r_s(A)', 'r_s(B)',\n", - " 'r_p(A)', 'r_p(B)',\n", - " 'r_d(A)', 'r_d(B)',],\n", - " index=list(RS.get_chemical_formula() for RS in RS_structures)\n", - ")\n" + " columns=[\n", + " \"energy_RS\",\n", + " \"energy_ZB\",\n", + " \"energy_diff\",\n", + " \"min_struc_type\",\n", + " \"Z(A)\",\n", + " \"Z(B)\",\n", + " \"period(A)\",\n", + " \"period(B)\",\n", + " \"IP(A)\",\n", + " \"IP(B)\",\n", + " \"EA(A)\",\n", + " \"EA(B)\",\n", + " \"E_HOMO(A)\",\n", + " \"E_HOMO(B)\",\n", + " \"E_LUMO(A)\",\n", + " \"E_LUMO(B)\",\n", + " \"r_s(A)\",\n", + " \"r_s(B)\",\n", + " \"r_p(A)\",\n", + " \"r_p(B)\",\n", + " \"r_d(A)\",\n", + " \"r_d(B)\",\n", + " ],\n", + " index=list(RS.get_chemical_formula() for RS in RS_structures),\n", + ")" ] }, { @@ -208,7 +249,7 @@ }, "outputs": [], "source": [ - "df['marker_symbol']= np.where(df['min_struc_type']=='RS','square-open','hexagram')" + "df[\"marker_symbol\"] = np.where(df[\"min_struc_type\"] == \"RS\", \"square-open\", \"hexagram\")" ] }, { @@ -230,8 +271,7 @@ "outputs": [], "source": [ "class Clustering:\n", - "\n", - " def __init__ (self):\n", + " def __init__(self):\n", " self.df_flag = False\n", " try:\n", " df\n", @@ -239,53 +279,61 @@ " print(\"Please define a dataframe 'df' and a features list\")\n", " self.df_flag = True\n", "\n", - " def kmeans (self, n_clusters, max_iter):\n", + " def kmeans(self, n_clusters, max_iter):\n", " if self.df_flag:\n", " return\n", - " cluster_labels = KMeans (n_clusters=n_clusters, max_iter=max_iter).fit_predict(df[features])\n", - " print(max(cluster_labels)+1,' clusters were extracted.')\n", - " df['clustering'] = 'k-means'\n", - " df['cluster_label']=cluster_labels\n", + " cluster_labels = KMeans(n_clusters=n_clusters, max_iter=max_iter).fit_predict(\n", + " df[features]\n", + " )\n", + " print(max(cluster_labels) + 1, \" clusters were extracted.\")\n", + " df[\"clustering\"] = \"k-means\"\n", + " df[\"cluster_label\"] = cluster_labels\n", "\n", - " def hierarchical (self, distance_threshold):\n", + " def hierarchical(self, distance_threshold):\n", " if self.df_flag:\n", " return\n", - " linkage_criterion = 'ward'\n", - " Z = linkage(df[features], linkage_criterion )\n", + " linkage_criterion = \"ward\"\n", + " Z = linkage(df[features], linkage_criterion)\n", " cluster_labels = cut_tree(Z, height=distance_threshold)\n", - " print(int(max(cluster_labels))+1,' clusters were extracted.')\n", - " df['clustering'] = 'Hierarchical - ' + linkage_criterion + ' criterion'\n", - " df['cluster_label']=cluster_labels\n", + " print(int(max(cluster_labels)) + 1, \" clusters were extracted.\")\n", + " df[\"clustering\"] = \"Hierarchical - \" + linkage_criterion + \" criterion\"\n", + " df[\"cluster_label\"] = cluster_labels\n", "\n", - " def dbscan (self, eps, min_samples):\n", + " def dbscan(self, eps, min_samples):\n", " if self.df_flag:\n", " return\n", - " cluster_labels = DBSCAN(eps=eps, min_samples=min_samples).fit_predict(df[features])\n", - " print(max(cluster_labels)+1,' clusters were extracted.')\n", - " df['clustering'] = 'DBSCAN'\n", - " df['cluster_label']=cluster_labels\n", + " cluster_labels = DBSCAN(eps=eps, min_samples=min_samples).fit_predict(\n", + " df[features]\n", + " )\n", + " print(max(cluster_labels) + 1, \" clusters were extracted.\")\n", + " df[\"clustering\"] = \"DBSCAN\"\n", + " df[\"cluster_label\"] = cluster_labels\n", "\n", - " def hdbscan (self, min_cluster_size, min_samples):\n", - " clusterer = hdbscan.HDBSCAN(min_cluster_size=min_cluster_size, min_samples=min_samples)\n", + " def hdbscan(self, min_cluster_size, min_samples):\n", + " clusterer = hdbscan.HDBSCAN(\n", + " min_cluster_size=min_cluster_size, min_samples=min_samples\n", + " )\n", " clusterer.fit(df[features])\n", - " cluster_labels=clusterer.labels_\n", - " print(max(cluster_labels)+1,' clusters were extracted.')\n", - " df['clustering']= 'HDBSCAN'\n", - " df['cluster_label']=cluster_labels\n", + " cluster_labels = clusterer.labels_\n", + " print(max(cluster_labels) + 1, \" clusters were extracted.\")\n", + " df[\"clustering\"] = \"HDBSCAN\"\n", + " df[\"cluster_label\"] = cluster_labels\n", "\n", - " def dpc (self, density = 0, delta = 0 ):\n", + " def dpc(self, density=0, delta=0):\n", " if self.df_flag:\n", " return\n", - " if density > 0 and delta > 0 :\n", - " clu=DPCClustering(np.ascontiguousarray(df[features].to_numpy()), autoplot=False)\n", + " if density > 0 and delta > 0:\n", + " clu = DPCClustering(\n", + " np.ascontiguousarray(df[features].to_numpy()), autoplot=False\n", + " )\n", " clu.autoplot = True\n", - " clu.assign(density,delta)\n", + " clu.assign(density, delta)\n", " cluster_labels = clu.membership\n", - " print(max(cluster_labels)+1,' clusters were extracted.')\n", - " df['clustering'] = 'DPC'\n", - " df['cluster_label']=cluster_labels\n", + " print(max(cluster_labels) + 1, \" clusters were extracted.\")\n", + " df[\"clustering\"] = \"DPC\"\n", + " df[\"cluster_label\"] = cluster_labels\n", " else:\n", - " clu=DPCClustering(np.ascontiguousarray(df[features].to_numpy()))\n" + " clu = DPCClustering(np.ascontiguousarray(df[features].to_numpy()))" ] }, { @@ -306,87 +354,96 @@ }, "outputs": [], "source": [ - "def show_embedding ():\n", + "def show_embedding():\n", + " btn_PCA = widgets.Button(description=\"PCA\")\n", + " btn_MDS = widgets.Button(description=\"MDS\")\n", + " btn_tSNE = widgets.Button(description=\"t-SNE\")\n", "\n", - " btn_PCA = widgets.Button(description='PCA')\n", - " btn_MDS = widgets.Button(description='MDS')\n", - " btn_tSNE = widgets.Button(description='t-SNE')\n", - "\n", - " def btn_eventhandler_embedding (obj):\n", - "\n", - " method = str (obj.description)\n", + " def btn_eventhandler_embedding(obj):\n", + " method = str(obj.description)\n", "\n", " try:\n", - " df['clustering'][0]\n", + " df[\"clustering\"][0]\n", " except KeyError:\n", " print(\"Please assign labels with a clustering algorithm\")\n", " return\n", "\n", - " if (method == 'PCA'):\n", + " if method == \"PCA\":\n", " transformed_data = PCA(n_components=2).fit_transform(df[features])\n", - " df['x_emb']=transformed_data[:,0]\n", - " df['y_emb']=transformed_data[:,1]\n", - " df['embedding'] = 'PCA'\n", - " elif (method == 'MDS'):\n", - " transformed_data = MDS (n_components=2).fit_transform(df[features])\n", - " df['x_emb']=transformed_data[:,0]\n", - " df['y_emb']=transformed_data[:,1]\n", - " df['embedding'] = 'MDS'\n", - " elif (method == 't-SNE'):\n", - " transformed_data = TSNE (n_components=2).fit_transform(df[features])\n", - " df['x_emb']=transformed_data[:,0]\n", - " df['y_emb']=transformed_data[:,1]\n", - " df['embedding'] = 't-SNE'\n", + " df[\"x_emb\"] = transformed_data[:, 0]\n", + " df[\"y_emb\"] = transformed_data[:, 1]\n", + " df[\"embedding\"] = \"PCA\"\n", + " elif method == \"MDS\":\n", + " transformed_data = MDS(n_components=2).fit_transform(df[features])\n", + " df[\"x_emb\"] = transformed_data[:, 0]\n", + " df[\"y_emb\"] = transformed_data[:, 1]\n", + " df[\"embedding\"] = \"MDS\"\n", + " elif method == \"t-SNE\":\n", + " transformed_data = TSNE(n_components=2).fit_transform(df[features])\n", + " df[\"x_emb\"] = transformed_data[:, 0]\n", + " df[\"y_emb\"] = transformed_data[:, 1]\n", + " df[\"embedding\"] = \"t-SNE\"\n", " plot_embedding()\n", "\n", " def plot_embedding():\n", " with fig.batch_update():\n", - "\n", - " for scatter in fig['data']:\n", + " for scatter in fig[\"data\"]:\n", " cl = scatter.meta\n", - " scatter['x']=df[df['cluster_label']==cl]['x_emb']\n", - " scatter['y']=df[df['cluster_label']==cl]['y_emb']\n", - " scatter['customdata']=np.dstack((df[df['cluster_label']==cl]['min_struc_type'].to_numpy(),\n", - " df[df['cluster_label']==cl]['cluster_label'].to_numpy(),\n", - " ))[0]\n", - " scatter['hovertemplate']=r\"<b>%{text}</b><br><br> Low energy structure: %{customdata[0]}<br>Cluster label: %{customdata[1]}<br>\"\n", - " scatter['marker'].symbol=df[df['cluster_label']==cl]['marker_symbol'].to_numpy()\n", - " scatter['text']=df[df['cluster_label']==cl].index.to_list()\n", + " scatter[\"x\"] = df[df[\"cluster_label\"] == cl][\"x_emb\"]\n", + " scatter[\"y\"] = df[df[\"cluster_label\"] == cl][\"y_emb\"]\n", + " scatter[\"customdata\"] = np.dstack(\n", + " (\n", + " df[df[\"cluster_label\"] == cl][\"min_struc_type\"].to_numpy(),\n", + " df[df[\"cluster_label\"] == cl][\"cluster_label\"].to_numpy(),\n", + " )\n", + " )[0]\n", + " scatter[\"hovertemplate\"] = (\n", + " r\"<b>%{text}</b><br><br> Low energy structure: %{customdata[0]}<br>Cluster label: %{customdata[1]}<br>\"\n", + " )\n", + " scatter[\"marker\"].symbol = df[df[\"cluster_label\"] == cl][\n", + " \"marker_symbol\"\n", + " ].to_numpy()\n", + " scatter[\"text\"] = df[df[\"cluster_label\"] == cl].index.to_list()\n", "\n", " fig.update_layout(\n", - " plot_bgcolor='rgba(229,236,246, 0.5)',\n", + " plot_bgcolor=\"rgba(229,236,246, 0.5)\",\n", " xaxis=dict(visible=True),\n", " yaxis=dict(visible=True),\n", - " legend_title_text='List of clusters',\n", - " showlegend=True,)\n", - " label_b.value = \"Embedding method used: \" + str(df['embedding'][0])\n", + " legend_title_text=\"List of clusters\",\n", + " showlegend=True,\n", + " )\n", + " label_b.value = \"Embedding method used: \" + str(df[\"embedding\"][0])\n", "\n", " btn_PCA.on_click(btn_eventhandler_embedding)\n", " btn_MDS.on_click(btn_eventhandler_embedding)\n", " btn_tSNE.on_click(btn_eventhandler_embedding)\n", - " label_t = widgets.Label(value=\"Clustering algorithm used: \" + str(df['clustering'][0]))\n", - " label_b = widgets.Label(value='Select a dimension reduction method to visualize the 2-dimensional embedding')\n", + " label_t = widgets.Label(\n", + " value=\"Clustering algorithm used: \" + str(df[\"clustering\"][0])\n", + " )\n", + " label_b = widgets.Label(\n", + " value=\"Select a dimension reduction method to visualize the 2-dimensional embedding\"\n", + " )\n", "\n", " fig = go.FigureWidget()\n", "\n", - " for cl in np.unique(df['cluster_label'].to_numpy()):\n", + " for cl in np.unique(df[\"cluster_label\"].to_numpy()):\n", " if cl == -1:\n", - " name = 'Outliers'\n", + " name = \"Outliers\"\n", " else:\n", - " name = 'Cluster ' + str(cl)\n", - " fig.add_trace(go.Scatter(\n", - " name=name,\n", - " mode='markers',\n", - " meta=cl\n", - " ))\n", + " name = \"Cluster \" + str(cl)\n", + " fig.add_trace(go.Scatter(name=name, mode=\"markers\", meta=cl))\n", "\n", - " fig.update_layout(plot_bgcolor='rgba(229,236,246, 0.5)',\n", - " width=800,\n", - " height=600,\n", - " xaxis=dict(visible=False, title='x_emb'),\n", - " yaxis=dict(visible=False, title='y_emb'))\n", + " fig.update_layout(\n", + " plot_bgcolor=\"rgba(229,236,246, 0.5)\",\n", + " width=800,\n", + " height=600,\n", + " xaxis=dict(visible=False, title=\"x_emb\"),\n", + " yaxis=dict(visible=False, title=\"y_emb\"),\n", + " )\n", "\n", - " return widgets.VBox([widgets.HBox ([btn_PCA,btn_MDS,btn_tSNE]),label_t, label_b, fig])" + " return widgets.VBox(\n", + " [widgets.HBox([btn_PCA, btn_MDS, btn_tSNE]), label_t, label_b, fig]\n", + " )" ] }, { @@ -408,22 +465,22 @@ "outputs": [], "source": [ "features = []\n", - "features.append('IP(A)')\n", - "features.append('IP(B)')\n", - "features.append('EA(A)')\n", - "features.append('EA(B)')\n", - "features.append('Z(A)')\n", - "features.append('Z(B)')\n", - "features.append('E_HOMO(A)')\n", - "features.append('E_HOMO(B)')\n", - "features.append('E_LUMO(A)')\n", - "features.append('E_LUMO(B)')\n", - "features.append('r_s(A)')\n", - "features.append('r_s(B)')\n", - "features.append('r_p(A)')\n", - "features.append('r_p(B)')\n", - "features.append('r_d(A)')\n", - "features.append('r_d(B)')" + "features.append(\"IP(A)\")\n", + "features.append(\"IP(B)\")\n", + "features.append(\"EA(A)\")\n", + "features.append(\"EA(B)\")\n", + "features.append(\"Z(A)\")\n", + "features.append(\"Z(B)\")\n", + "features.append(\"E_HOMO(A)\")\n", + "features.append(\"E_HOMO(B)\")\n", + "features.append(\"E_LUMO(A)\")\n", + "features.append(\"E_LUMO(B)\")\n", + "features.append(\"r_s(A)\")\n", + "features.append(\"r_s(B)\")\n", + "features.append(\"r_p(A)\")\n", + "features.append(\"r_p(B)\")\n", + "features.append(\"r_d(A)\")\n", + "features.append(\"r_d(B)\")" ] }, { @@ -445,7 +502,7 @@ }, "outputs": [], "source": [ - "df[features]=preprocessing.scale(df[features])" + "df[features] = preprocessing.scale(df[features])" ] }, { @@ -467,7 +524,7 @@ }, "outputs": [], "source": [ - "hist = df[features].hist( bins=10, figsize = (20,15));" + "hist = df[features].hist(bins=10, figsize=(20, 15));" ] }, { @@ -505,7 +562,7 @@ "outputs": [], "source": [ "n_clusters = 2\n", - "max_iter =100\n", + "max_iter = 100\n", "Clustering().kmeans(n_clusters, max_iter)" ] }, @@ -520,7 +577,7 @@ }, "outputs": [], "source": [ - "print(df['cluster_label'][:10])" + "print(df[\"cluster_label\"][:10])" ] }, { @@ -576,18 +633,28 @@ }, "outputs": [], "source": [ - "def composition_RS_ZB (df):\n", - " df_cm = pd.DataFrame (columns=['RS','ZB','Materials in cluster'], dtype=object)\n", + "def composition_RS_ZB(df):\n", + " df_cm = pd.DataFrame(columns=[\"RS\", \"ZB\", \"Materials in cluster\"], dtype=object)\n", "\n", - " n_clusters = df['cluster_label'].max() + 1\n", + " n_clusters = df[\"cluster_label\"].max() + 1\n", "\n", - " for i in range (n_clusters):\n", - " Tot = len(df.loc[df['cluster_label']==i])\n", - " if (Tot == 0):\n", + " for i in range(n_clusters):\n", + " Tot = len(df.loc[df[\"cluster_label\"] == i])\n", + " if Tot == 0:\n", " continue\n", - " RS = int(100*len(df.loc[(df['cluster_label']==i) & (df['min_struc_type']=='RS')])/len(df.loc[df['cluster_label']==i]))\n", - " ZB = int(100*len(df.loc[(df['cluster_label']==i) & (df['min_struc_type']=='ZB')])/len(df.loc[df['cluster_label']==i]))\n", - " df_cm = df_cm.append({'RS':RS, 'ZB':ZB, \"Materials in cluster\":Tot},ignore_index=True)\n", + " RS = int(\n", + " 100\n", + " * len(df.loc[(df[\"cluster_label\"] == i) & (df[\"min_struc_type\"] == \"RS\")])\n", + " / len(df.loc[df[\"cluster_label\"] == i])\n", + " )\n", + " ZB = int(\n", + " 100\n", + " * len(df.loc[(df[\"cluster_label\"] == i) & (df[\"min_struc_type\"] == \"ZB\")])\n", + " / len(df.loc[df[\"cluster_label\"] == i])\n", + " )\n", + " df_cm = df_cm.append(\n", + " {\"RS\": RS, \"ZB\": ZB, \"Materials in cluster\": Tot}, ignore_index=True\n", + " )\n", "\n", " return df_cm" ] @@ -644,7 +711,7 @@ }, "outputs": [], "source": [ - "distance_threshold=20\n", + "distance_threshold = 20\n", "Clustering().hierarchical(distance_threshold=distance_threshold)" ] }, @@ -698,8 +765,8 @@ }, "outputs": [], "source": [ - "Z = linkage(df[features], 'ward' )\n", - "dendrogram(Z, truncate_mode='lastp',p=11);" + "Z = linkage(df[features], \"ward\")\n", + "dendrogram(Z, truncate_mode=\"lastp\", p=11);" ] }, { @@ -738,8 +805,8 @@ "outputs": [], "source": [ "eps = 3\n", - "min_samples= 8\n", - "Clustering().dbscan(eps,min_samples)" + "min_samples = 8\n", + "Clustering().dbscan(eps, min_samples)" ] }, { @@ -951,7 +1018,7 @@ "metadata": {}, "outputs": [], "source": [ - "Clustering().dpc(2.4,3.8)" + "Clustering().dpc(2.4, 3.8)" ] }, { @@ -1000,7 +1067,7 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, @@ -1014,7 +1081,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.7.3" + "version": "3.9.13" } }, "nbformat": 4, diff --git a/requirements.in b/requirements.in index d8773816104d711df5ab42fc8a6694564f0c3b9e..370619699dfe3db4783b55beba8c9465aa533ed3 100644 --- a/requirements.in +++ b/requirements.in @@ -1,6 +1,7 @@ numpy pandas matplotlib +plotly scikit-learn hdbscan ase