Repository: dpuenteramirez/Semisupervised-learning-and-instance-selection-methods (language: python)

Issue:
Improve Instance Selection Algorithms code quality and its documentation

Viability study - Project Planning Annex

Viability study - Project Planning Annex

Improve SemiSupervised Algorithms code quality and its documentation

Improve Instance Selection Algorithms code quality and its documentation

Improve Instance Selection Algorithms code quality and its documentation

Improve Instance Selection Algorithms code quality and its documentation

Improve SemiSupervised Algorithms code quality and its documentation

Improve SemiSupervised Algorithms code quality and its documentation

Improve Instance Selection Algorithms code quality and its documentation



Discussion / hints:

Fix patch:
diff --git a/docs/anexos.pdf b/docs/anexos.pdf
index 350bcc0..6e90a2b 100644
Binary files a/docs/anexos.pdf and b/docs/anexos.pdf differ
diff --git a/docs/anexos.tex b/docs/anexos.tex
index 61d635c..f9a63c3 100644
--- a/docs/anexos.tex
+++ b/docs/anexos.tex
@@ -8,9 +8,13 @@
 \usepackage{lmodern} % scalable font
 \usepackage{microtype}
 \usepackage{placeins}
+\usepackage{adjustbox}
 \usepackage{tabularx}
 \usepackage{placeins}
 
+% Moneda
+\usepackage{eurosym}
+
 \RequirePackage{booktabs}
 \RequirePackage[table]{xcolor}
 \RequirePackage{xtab}
diff --git a/docs/bibliografiaAnexos.bib b/docs/bibliografiaAnexos.bib
index 7ac5606..50655f6 100644
--- a/docs/bibliografiaAnexos.bib
+++ b/docs/bibliografiaAnexos.bib
@@ -210,4 +210,22 @@ @misc{Travis-CI-LOG-UBUMLaaS
 
 @misc{PyPI, title={Python Package Index}, url={https://pypi.org/}, note={https://pypi.org/}}
 
-@misc{NearestNeighbors, title={Scikit-Learn Nearest Neighbors}, url={https://scikit-learn.org/stable/modules/generated/sklearn.neighbors.NearestNeighbors.html}, note={sklearn.neighbors.NearestNeighbors}}
\ No newline at end of file
+@misc{NearestNeighbors, title={Scikit-Learn Nearest Neighbors}, url={https://scikit-learn.org/stable/modules/generated/sklearn.neighbors.NearestNeighbors.html}, note={sklearn.neighbors.NearestNeighbors}}
+
+@misc{SalarioJunior, title={Salario medio Programador Junior España}, url={https://es.indeed.com/career/programador-junior/salaries}, note={https://es.indeed.com/career/programador-junior/salaries}}
+
+@misc{SalarioInvestigador, title={Salario medio Investigador España}, url={https://es.indeed.com/career/investigador/salaries}, note={https://es.indeed.com/career/investigador/salaries}}
+
+@misc{ss_cotizacion,
+    author = "Ministerio de Empleo y Seguridad Social",
+  title = {Bases y tipos de cotización 2022},
+  url = {https://www.seg-social.es/wps/portal/wss/internet/Trabajadores/CotizacionRecaudacionTrabajadores/36537}
+}
+
+@misc{softwareLicense, title={What is a Software License?}, url={https://snyk.io/learn/what-is-a-software-license/}, author={Synk}}
+
+@misc{JLA, title={JLA - Find and compare software licenses}, url={https://joinup.ec.europa.eu/collection/eupl/solution/joinup-licensing-assistant/jla-find-and-compare-software-licenses}, author={European Union}}
+
+@misc{CC0, title={Creative Commons - Public Domain}, url={https://wiki.creativecommons.org/wiki/CC0}, note={https://wiki.creativecommons.org/wiki/CC0}}
+
+@misc{CCBYNCND40, title={Creative Commons - Public Domain}, url={https://creativecommons.org/licenses/by-nc-nd/4.0/deed.es}, note={https://creativecommons.org/licenses/by-nc-nd/4.0/deed.es}}
\ No newline at end of file
diff --git a/docs/img/anexos/plan/Licencias.png b/docs/img/anexos/plan/Licencias.png
new file mode 100644
index 0000000..7c21a35
Binary files /dev/null and b/docs/img/anexos/plan/Licencias.png differ
diff --git a/docs/img/anexos/plan/licencia-is-ssl.png b/docs/img/anexos/plan/licencia-is-ssl.png
new file mode 100644
index 0000000..de17100
Binary files /dev/null and b/docs/img/anexos/plan/licencia-is-ssl.png differ
diff --git a/docs/img/anexos/plan/licencia-ubumlaas.png b/docs/img/anexos/plan/licencia-ubumlaas.png
new file mode 100644
index 0000000..f361946
Binary files /dev/null and b/docs/img/anexos/plan/licencia-ubumlaas.png differ
diff --git a/docs/tex/A_Plan_proyecto.tex b/docs/tex/A_Plan_proyecto.tex
index c2b61eb..aaa4d55 100644
--- a/docs/tex/A_Plan_proyecto.tex
+++ b/docs/tex/A_Plan_proyecto.tex
@@ -671,86 +671,308 @@ \subsubsection{\textit{Sprint} n: Name}
 
 \newpage
 \section{Estudio de viabilidad}
+En esta sección se va a desarrollar el estudio de la viabilidad del proyecto, permitiendo tener una visión global de los beneficios en contraposición al coste que supone el desarrollo del proyecto.
 
-\newpage
+El desarrollo de cualquier producto \textit{software} conlleva una serie de riesgos, entre los que destacan la experiencia del equipo de desarrollo, el tamaño del proyecto, el tiempo del que se dispone para conseguirlo,\dots Influyendo todos ellos en el resultado final.
 \subsection{Viabilidad económica}
+Lo primero de todo que se va a calcular es la viabilidad económica del proyecto, reportando y analizando los costes/beneficios que habría supuesto el desarrollo del proyecto en el sector empresarial, en España\footnote{Se hace referencia al lugar de desarrollo puesto que se van a hacer referencias económicas en la moneda del país, así como uso de legislación vigente, la cuál varía en función del país en el que se encuentre asentada la empresa desarrolladora.}.
+
+\subsubsection{Costes}
+La realización de un proyecto de esta envergadura lleva asociados una serie de costes fijos y variables, a continuación se desglosan agrupados en \textit{hardware}, \textit{software}, personal, otros.
+
+\textbf{Costes \textit{hardware}}
+
+Para el desarrollo del proyecto es necesario más de un equipo \textit{hardware}, lo primero de todo es un equipo de tipo portátil. Se utilizará un MacBook Pro, con un procesador Inter Core i7 de 4 núcleos a 2.3~GHz, con 16~GB de memoria RAM, atendiendo a un precio de mercado de 2249~\officialeuro, se tiene en cuenta que la vida útil del equipo se encuentra en torno a los seis años, por lo que para los cálculos se usa la vida media del inmovilizado, es decir, 3 años. 
+
+A su vez se necesita un servidor para desplegar la aplicación y trabajar sobre esta, ya que el despliegue local no se considera una forma de trabajo óptima. Es por ello que se hace uso de un equipo con un AMD FX(tm)-4130 Quad-Core a 4.5~GHz, con 64~GB de memoria RAM. El coste aproximado del equipo es de 1500~\officialeuro. La vida útil estimada es de seis ocho años, se utilizará igual que con el equipo portatil, su vida media de inmovilizado, 4 años.
+
+La amortización para cada equipo es diferente, pero el tiempo de uso es el mismo, de noviembre de dos mil veintiuno a junio de dos mil veintidós, ambos incluidos, luego en total ocho meses. Se detallan todos los costes \textit{hardware} en la Tabla~\ref{tab:costes-hardware}.
+
+\begin{table}[H]
+\centering
+\begin{tabular}{lrr}
+	\toprule
+	\textbf{Concepto} & \textbf{Coste} & \textbf{Coste Amortizado}\\
+	\midrule
+	Macbook Pro & 2.249~\officialeuro & 499,78~\officialeuro \\
+	Servidor & 1.500~\officialeuro & 250~\officialeuro \\
+	\midrule
+	\textbf{Total} & 3.049~\officialeuro & 749,78~\officialeuro \\
+	\bottomrule
+\end{tabular}
+\caption{Costes de \emph{hardware}.}\label{tab:costes-hardware}
+\end{table}
+
+\textbf{Costes \textit{software}}
+
+Para el desarrollo del proyecto y uso del \textit{software} necesario, es necesaria la adquisición de una serie de licencias, todas ellas se encuentran detalladas con su amortización en la Tabla~\ref{tab:costes-software}. Todas las licencias tienen un periodo de validez de un año.
+
+\begin{table}[H]
+\centering
+\begin{tabular}{lrr}
+	\toprule
+	\textbf{Concepto} & \textbf{Coste} & \textbf{Coste Amortizado}\\
+	\midrule
+	Codacy & 170,76~\officialeuro & 113,84~\officialeuro \\
+	FileZilla & 0~\officialeuro & 0~\officialeuro \\
+	GitKraken & 59,4~\officialeuro & 36,27~\officialeuro \\
+	Codacy & 170,76~\officialeuro & 113,84~\officialeuro \\
+	Linux Mint 20.3 & 0~\officialeuro & 0~\officialeuro \\
+	MacOs Monterey & 0~\officialeuro & 0~\officialeuro \\
+	PyCharm & 240,79~\officialeuro & 160,53~\officialeuro \\
+	Selenium & 0~\officialeuro & 0~\officialeuro \\
+	SonarCloud & 120~\officialeuro & 80~\officialeuro \\
+	\TeX Maker & 0~\officialeuro & 0~\officialeuro \\
+	Travis-CI & 719,86~\officialeuro & 479,81~\officialeuro \\
+	Visual Paradigm & 93,89~\officialeuro & 62,59~\officialeuro \\
+	Visual Studio Code & 0~\officialeuro & 0~\officialeuro \\
+	\midrule
+	\textbf{Total} & 1.399,70~\officialeuro & 933,13~\officialeuro \\
+	\bottomrule
+\end{tabular}
+\caption{Costes de \emph{software}.}\label{tab:costes-software}
+\end{table}
+
+\textbf{Coste de personal}
+El desarrollo del proyecto ha sido llevado a cabo por un desarrollador y el tutor del proyecto.
+\begin{itemize}
+\item El salario del desarrollador se calcula según~\cite{SalarioJunior}, siendo el salario medio anal del 20.123~\officialeuro brutos al año.
+\item El salario del tutor se calcula según~\cite{SalarioInvestigador}, siendo el salario medio anual de 22.784~\officialeuro brutos al año. Con una carga de trabajo de dos horas semanales.
+\end{itemize}
 
-\newpage
-\subsection{Viabilidad legal}
-
-\newpage
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+La duración total del proyecto es de 31 semanas, por lo tanto, el tutor trabajará un total de 62 horas. Frente a las XXX del desarrollador.
 
+El IRPF es del XX\% y la retribución a la Seguridad Social, calculada tal cual se plantea en~\cite{ss_cotizacion} por el Ministerio de Empleo y Seguridad Social, se contribuye con un XX\% en total. Estando dividido en:
+\begin{itemize}
+\tightlist
+\item XX\% de contingencias comunes.
+\item XX\% por desempleo de tipo general.
+\item XX\% destinado al Fondo de Garantía Salarial.
+\item XX\% de formación profesional.
+\end{itemize}
 
+\begin{table}[H]
+\centering
+\begin{tabular}{lrr}
+	\toprule
+	\textbf{Concepto} & \textbf{Desarrollador} & \textbf{Tutor}\\
+	\midrule
+	Salario total neto & XX~\officialeuro & XX~\officialeuro \\
+	Retención IRPF & XX~\officialeuro & XX~\officialeuro \\
+	Seguridad Social & XX~\officialeuro & XX~\officialeuro \\
+	\midrule
+	\textbf{Total salario bruto} & XX~\officialeuro & XX~\officialeuro \\
+	\bottomrule
+\end{tabular}
+\caption{Costes de personal.}\label{tab:costes-personal}
+\end{table}
+
+En la Tabla~\ref{tab:costes-personal} se desglosan punto por punto los costes de personal, resultando en:
+\[ XX\text{\officialeuro/desarrollador} + XX\text{\officialeuro/tutor} = XX\text{\officialeuro}\]
+
+Siendo el coste total en personal de XX~\officialeuro.
+
+\textbf{Otros costes}
+
+Costes no agrupables en los anteriores apartados pero a tener en cuenta, Tabla~\ref{tab:costes-otros}.
+
+\begin{table}[H]
+\centering
+\begin{tabular}{lr}
+	\toprule
+	\textbf{Concepto} & \textbf{Coste} \\
+	\midrule
+	Dominio despliegue & 32~\officialeuro \\
+	Logos & 45~\officialeuro \\
+	Memoria impresa & 250~\officialeuro \\
+	Alquiler oficina & 1600~\officialeuro \\
+	Internet & 136~\officialeuro \\
+	Electricidad & 140~\officialeuro \\
+	Agua & 51~\officialeuro \\
+	Calefacción & 230~\officialeuro \\
+	\midrule
+	\textbf{Total} & 2484~\officialeuro \\
+	\bottomrule
+\end{tabular}
+\caption{Otros costes.}\label{tab:costes-otros}
+\end{table}
+
+\textbf{Total de  costes}
+
+En la Tabla~\ref{tab:costes-totales} se puede ver un resumen de todos los costes comentados. Dejando el coste base total del proyecto en XX~\officialeuro.
+
+\begin{table}[H]
+\centering
+\begin{tabular}{lr}
+	\toprule
+	\textbf{Categoría} & \textbf{Coste} \\
+	\midrule
+	\textit{Hardware} & 3.049~\officialeuro \\
+	\textit{Software} & 1.399,70~\officialeuro \\
+	Personal & XX~\officialeuro \\
+	Otros & 2484~\officialeuro \\
+	\midrule
+	\textbf{Total} & 6.932,7+XX~\officialeuro \\
+	\bottomrule
+\end{tabular}
+\caption{Costes totales.}\label{tab:costes-totales}
+\end{table}
+
+
+\textbf{Beneficios}
+El proyecto está compuesto de dos partes, las cuáles se pueden comercializar de forma independiente.
+\begin{itemize}
+\item \textbf{IS-SSL.} Las bibliotecas de algoritmos son distribuidas de forma pública, gratuita y sin publicidad, con el fin de aportar un <<granito de arena>> a la investigación en aprendizaje semi-supervisado y selección de instancias.
+\item \textbf{UBUMLaaS.} En el diseño existente no está planteada su monetización. Si se quisiera obtener un rédito económico de este \texttt{MLaaS}, se podría fácilmente crear niveles de usuarios para acceso, número de experimentos máximos a lanzar. También se podría plantear como servicio de suscripción. En la Tabla~\ref{tab:opciones-beneficio} se plantean algunas posibilidades de comercialización con sus posibles respectivos costes para los usuarios, todas las licencias son anuales, en caso de un usuario quedarse sin tiempo podría comprar más horas de ejecución.
+\end{itemize}
 
+\begin{table}[H]
+\centering
+\begin{tabular}{lcrcr}
+	\toprule
+	\textbf{Tipo}          &\textbf{Experimentos} &  \textbf{Precio} & \textbf{Tiempo (h)} & EUR/h\\
+	\midrule
+	\textit{Trial}        & 5 & 0~\officialeuro & 3 & X\\
+	Estudiante   & 25 & 10~\officialeuro & 20 & X\\
+	Investigador  & 120 & 40~\officialeuro & 250 & 0.4~\officialeuro/h \\
+	Equipo  & 300 & 150~\officialeuro & 1000 & 0.4~\officialeuro/h \\
+	Empresa & Ilimitados & 2000~\officialeuro & 5000 & 0.25~\officialeuro/h \\
+	Otros & \multicolumn{3}{c}{Contactar con soporte para obtener presupuesto} \\
+	\bottomrule
+\end{tabular}
+\caption{Opciones para obtener beneficio con UBUMLaaS.}\label{tab:opciones-beneficio}
+\end{table}
+
+Teniendo en cuenta los anteriores valores, y que la aplicación no tiene <<fronteras>> ya que el inglés es el idioma oficial de la investigación y la ciencia, se pueden alcanzar usuarios de todo el mundo. En la Tabla~\ref{tab:simulacion-clientes} se presenta un ejemplo real y viable de alcanzar la cuál permitiría recuperar la inversión del proyecto en el primer año.
+
+\begin{table}[H]
+\centering
+\begin{tabular}{lcr}
+	\toprule
+	\textbf{Tipo}     & \textbf{Número de cuentas} & \textbf{Beneficios}\\
+	\midrule
+	\textit{Trial}        & Ilimitadas & 0~\officialeuro\\
+	Estudiante   & 4 & 40~\officialeuro\\
+	Investigador  & 15 & 600~\officialeuro\\
+	Equipo  & 40 & 6.000~\officialeuro\\
+	Empresa & 15 & 30.000~\officialeuro\\
+	Otros & \multicolumn{2}{c}{No computado} \\
+	\midrule
+	\textbf{Total} &&36.640~\officialeuro \\
+	\bottomrule
+\end{tabular}
+\caption{Simulación para recuperar la inversión con UBUMLaaS.}\label{tab:simulacion-clientes}
+\end{table}
+
+Debido a las funcionalidades ofrecidas y al precio competitivo de la plataforma, no sería difícil encontrar organizaciones que incorporen UBUMLaaS en su repertorio de herramientas, maximizando beneficios.
+
+\textbf{Conclusiones}
+
+Analizando los beneficios reportados en contraposción a los costes del desarrollo del proyecto, queda más que demostrada la viabilidad de desarrollo del proyecto. Se tiene en cuenta además que el proyecto una vez desarrollado no tiene necesidad de ser mantenido más allá de añadir nuevos algoritmos. Teniendo como costes fijos el mantenimiento de el(los) servidor(es) en donde esté desplegado.
 
+\subsection{Viabilidad legal}
 
+En esta sección principalmente se va a discutir el tema de licencias, ya que al ser un desarrollo \textit{software} es la temática más importante.
+
+Según~\cite{softwareLicense}, una licencia de \textit{software} es un contrato entre la entidad que ha creado y suministrado una aplicación, el código fuente subyacente o un producto relacionado con el mismo, y su usuario final.  La licencia es un documento, habitualmente de texto, diseñado para proteger la propiedad intelectual del desarrollador del \textit{software} y para limitar cualquier reclamación contra él que pueda surgir de su uso.
+
+Una licencia proporciona, además, definiciones legalmente vinculantes para la distribución y el uso del \textit{software}. Los derechos del usuario final, como la instalación,  las garantías y las responsabilidades, también se encuentran detallados en la licencia.
+
+
+\subsubsection{\textit{Software}}
+La licencia más importante es aquella bajo la cual se encuentra el \textit{software} desarrollado por el producto,  permitiendo el uso (sobretodo comercial y de distribución que se le puede dar). La principal limitación que se debe tener en cuenta es el orden jerárquico de licencias que se debe respetar. El proyecto utiliza una serie de dependencias externas, las cuáles poseen sus propias licencias, nunca pudiendo encontrarse bajo una licencia más permisiva.
+
+A continuación se exponen las licencias de las dependencias utilizadas tanto por UBUMLaaS, Tabla~\ref{tab:dependencias-ubumlaas}, como por IS-SSL, Tabla~\ref{tab:dependencias-is-ssl}. La descripción de cada una se encuentra en las Tablas~\ref{tab:bibliotecas-python-ubumlaas}~y~\ref{tab:bibliotecas-python-is-ssl}, respectivamente.
+
+\begin{table}[]
+\centering
+\begin{tabular}{lcl}
+	\toprule
+	\textbf{Dependencia} & \textbf{Versión} & \textbf{Licencia}\\
+	\midrule
+	\rowcolor[HTML]{EFEFEF} 
+	\texttt{email-validator} & 1.1.1 & CC0 1.0 Universal\\
+	\texttt{flask} & 1.1.1 & BSD 3 Clause\\ \rowcolor[HTML]{EFEFEF}
+	\texttt{flask-login} & 0.4.1 & MIT\\
+	\texttt{flask-mail} & 0.9.1 & BSD\\ \rowcolor[HTML]{EFEFEF}
+	\texttt{flask-migrate} & 2.5.2 & MIT\\
+	\texttt{flask-redis} & 0.4.0 & Blue Oak\\ \rowcolor[HTML]{EFEFEF}
+	\texttt{flask-sqlalchemy} & 2.4.0 & BSD 3 Clause\\
+	\texttt{flask-wtf} & 0.14.2 & BSD 3 Clause\\ \rowcolor[HTML]{EFEFEF}
+	\texttt{future} & 0.16.0 & MIT\\ 
+	\texttt{geopy} & 2.2.0 & MIT\\ \rowcolor[HTML]{EFEFEF}
+	\texttt{glances} & 3.2.4.2 & GNU LGPLv3\\ 
+	\texttt{imbalanced-learn} & 0.5.0 & MIT\\ \rowcolor[HTML]{EFEFEF}
+	\texttt{itsdangerous} & 1.1.0 & BSD 3 Clause\\
+	\texttt{liac-arff} & 2.2.1 & MIT \\ \rowcolor[HTML]{EFEFEF}
+	\texttt{numpy} & 1.22.3 &BSD\\
+	\texttt{pandas} & 0.25.1 & BSD 3 Clause\\ \rowcolor[HTML]{EFEFEF}
+	\texttt{psutil} & 5.9.0 & BSD\\
+	\texttt{pycountry} & 22.3.5 & GNU LGPLv2\\ \rowcolor[HTML]{EFEFEF}
+	\texttt{pytest} & 5.2.1 & MIT\\
+	\texttt{python-weka-wrapper3} & 0.1.7 & GNU GPLv3\\ \rowcolor[HTML]{EFEFEF}
+	\texttt{requests} & 2.22.0 & Apache 2.0\\
+	\texttt{rq} & 1.1.0 & BSD \\ \rowcolor[HTML]{EFEFEF}
+	\texttt{scikit-learn} & 0.24 & BSD\\ 
+	\texttt{selenium} & 3.141.0 & Apache 2.0\\ \rowcolor[HTML]{EFEFEF}
+	\texttt{urllib3} & 1.25.6 & MIT\\
+	\texttt{werkzeug} & 0.15.6 & BSD 3 Clause\\ \rowcolor[HTML]{EFEFEF}
+	\texttt{whichcraft} & 0.4.1 & BSD \\
+	\bottomrule
+\end{tabular}
+\caption{Bibliotecas utilizadas y sus versiones.}\label{tab:dependencias-ubumlaas}
+\end{table}
+
+\begin{table}[]
+\centering
+\begin{tabular}{lcl}
+	\toprule
+	\textbf{Biblioteca} & \textbf{Versión} & \textbf{Licencia}\\ \rowcolor[HTML]{EFEFEF} 
+	\midrule
+	\rowcolor[HTML]{EFEFEF} 
+	\textbf{\texttt{numpy}}    & 1.20.3 &  BSD \\ \rowcolor[HTML]{EFEFEF} 
+	\textbf{\texttt{pandas}}    & 1.3.4  & BSD 3 Clause  \\  
+	\textbf{\texttt{scikit-learn}}    & 0.24.2 & BSD  \\ \rowcolor[HTML]{EFEFEF} 
+	\textbf{\texttt{scipy}}    & 1.7.1  & BSD   \\ 
+	\bottomrule
+\end{tabular}
+\caption{Bibliotecas utilizadas y sus versiones.}\label{tab:dependencias-is-ssl}
+\end{table}
+
+Como se aprecia en las Tablas~\ref{tab:dependencias-ubumlaas}~y~\ref{tab:dependencias-ubumlaas}, cada proyecto de manera individual va a estar sujeto a diferentes restricciones en cuanto a licencias se refiere, siendo más laxo en IS-SSL y mucho más restrictivo en UBUMLaaS. Para la comparación de licencias se utiliza JLA,~\cite{JLA}, \textit{joinup Licensing Assisntant}.
+
+\imagenFlotante{../img/anexos/plan/Licencias}{Orden de permisividad/restricción de licencias.}{licencias}
+
+En la Figura~\ref{fig:licencias} se detalla una comparación de las diferentes licencias de las dependencias de cada parte del proyecto, estando más a la derecha aquella con mayor restricciones, y a la izquierda la más permisivas. El \textit{software} debe estar licenciado como mínimo, con la más restrictiva.
 
+\begin{itemize}
+\item \textbf{UBUMLaaS.} Estará licenciado bajo GNU v3.  Figura~\ref{fig:licencia-ubumlaas}. 
 
+Siendo una licencia perfecta para los objetivos de comercialización planteados. El \textit{software} se distribuye como \textit{open source} permitiendo que individuos lo usen de forma privada, pero en caso de que se quiera utilizar de cualquier manera de forma comercial deberá ser hecho \textit{open source}, permitiendo al equipo de desarrollo comprobar los cambios realizados, de forma que se puedan añadir al proyecto <<base>> u original.
 
+\item \textbf{IS-SSL.} Estará licenciado bajo BSD Cláusula 3.  Figura~\ref{fig:licencia-is-ssl}.  El objetivo de las bibliotecas que conforman IS-SSL es que puedan ser ampliadas en cualquier momento por cualquier desarrollador, ya sea mediante \textit{pull requests} al proyecto o mediante sus propias implementaciones a través de \textit{forks}.  El objetivo del proyecto no es obtener un beneficio económico mediante licencias de uso, por lo tanto, BSD 3 se ajusta a las necesidades y requerimientos del proyecto.
+\end{itemize}
 
+\imagenFlotante{../img/anexos/plan/licencia-ubumlaas}{Licencia GNU v3 para UBUMLaaS}{licencia-ubumlaas}
+\imagenFlotante{../img/anexos/plan/licencia-is-ssl}{Licencia BSD 3 Clause para IS-SSL}{licencia-is-ssl}
+
+\subsubsection{Documentación}
+La documentación del proyecto también se encuentra licenciada. En este caso no tiene sentido utilizar licencias similares a las anteriores puesto que no es un desarrollo \textit{software}, siendo más comunes y correctas licencias de tipo \textit{Creative Commons}. Se debe tener en cuenta el mismo principio anterior, todo aquel material de terceros que haya sido utilizado para la composición de la documentación, estará sujeto a licencias, no pudiendo ser distribuido con una licencia más permisiva que la que originalmente su autor lo distribuyó. 
+
+En la documentación únicamente se han utilizado imágenes de terceros (unas pocas), estado todas ellas licenciadas bajo dominio público (CC0~\cite{CC0}), permitiendo utilizar cualquier licencia superior de \textit{Creative Commons} para licenciar la documentación.
+
+Se ha decidido utilizar como licencia: \textit{Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International} (CC BY-NC-ND 4.0)~\cite{CCBYNCND40}.  En la Tabla~\ref{tab:licencia-documentacion} se resume lo que establece, así como acciones permitidas y prohibidas.
+
+\begin{table}[H]
+\centering
+\begin{tabular}{ll}
+	\toprule
+	\textbf{Libre de} & \textbf{Bajo los términos}\\
+	\midrule
+	\multirow{3}{*}{Compartir - Copiar y redistribuir}   &Atribución\\
+	    & No Comercial   \\  
+	   & Sin Derivadas  \\ 
+	\bottomrule
+\end{tabular}
+\caption{Bibliotecas utilizadas y sus versiones.}\label{tab:licencia-documentacion}
+\end{table}
diff --git a/docs/tex/D_Manual_programador.tex b/docs/tex/D_Manual_programador.tex
index 2762721..352d3ea 100644
--- a/docs/tex/D_Manual_programador.tex
+++ b/docs/tex/D_Manual_programador.tex
@@ -68,8 +68,9 @@ \subsubsection{Bibliotecas Python}
 \begin{table}[p]
 \centering
 \begin{tabular}{lcl}
-	\textbf{Biblioteca} & \textbf{Versión} & \textbf{Descripción}\\
 	\toprule
+	\textbf{Biblioteca} & \textbf{Versión} & \textbf{Descripción}\\
+	\midrule
 	\rowcolor[HTML]{EFEFEF} 
 	\texttt{email-validator} & 1.1.1 & Validar direcciones de correo electrónico.\\
 	\texttt{flask} & 1.1.1 & Web \textit{framework}.\\ \rowcolor[HTML]{EFEFEF}
@@ -100,7 +101,7 @@ \subsubsection{Bibliotecas Python}
 	\texttt{whichcraft} & 0.4.1 & Funcionalidad \textit{shutil.which}. \\
 	\bottomrule
 \end{tabular}
-\caption{Bibliotecas utilizadas y sus versiones.}\label{tab:bibliotecas-python-ubumlaas}
+\caption{Bibliotecas utilizadas por UBUMLaaS y sus versiones.}\label{tab:bibliotecas-python-ubumlaas}
 \end{table}
 
 Las versiones indicadas en la tabla~\ref{tab:bibliotecas-python-ubumlaas} son las que se han utilizado para el desarrollo del proyecto, se pueden actualizar a versiones futuras, siempre y cuando sean compatibles entre sí.
@@ -329,18 +330,19 @@ \subsubsection{Bibliotecas Python}
 \begin{table}[]
 \centering
 \begin{tabular}{lcl}
-	\textbf{Biblioteca} & \textbf{Versión} & \textbf{Descripción}\\ \rowcolor[HTML]{EFEFEF} 
 	\toprule
+	\textbf{Biblioteca} & \textbf{Versión} & \textbf{Descripción}\\ \rowcolor[HTML]{EFEFEF} 
+	\midrule
 	\rowcolor[HTML]{EFEFEF} 
 	\texttt{matplotlib}    & 3.4.3   & Representación gráfica.   \\  
 	\textbf{\texttt{numpy}}    & 1.20.3 &  Computación de \textit{arrays}.  \\ \rowcolor[HTML]{EFEFEF} 
-	\textbf{\texttt{pandas}}    & 1.3.4  & Estructuras de datos    \\  
+	\textbf{\texttt{pandas}}    & 1.3.4  & Estructuras de datos.    \\  
 	\textbf{\texttt{scikit-learn}}    & 0.24.2 & Módulos de minería de datos y ML.   \\ \rowcolor[HTML]{EFEFEF} 
 	\textbf{\texttt{scipy}}    & 1.7.1  & Módulos científicos.    \\ 
 	\texttt{yagmail}    & 0.15.277  & Cliente de GMAIL.    \\ 
 	\bottomrule
 \end{tabular}
-\caption{Bibliotecas utilizadas y sus versiones.}\label{tab:bibliotecas-python-is-ssl}
+\caption{Bibliotecas utilizadas por IS-SSL y sus versiones.}\label{tab:bibliotecas-python-is-ssl}
 \end{table}
 
 Se recomienda el uso de un entorno de desarrollo de \texttt{Conda}, se facilitan ficheros de configuración tanto para \texttt{Conda} como para instalación con \texttt{PIP}.
diff --git a/instance_selection/_CNN.py b/instance_selection/_CNN.py
index f3a5419..7f702dc 100644
--- a/instance_selection/_CNN.py
+++ b/instance_selection/_CNN.py
@@ -3,7 +3,7 @@
 # @Filename:    CNN.py
 # @Author:      Daniel Puente Ramírez
 # @Time:        19/11/21 07:13
-# @Version:     4.0
+# @Version:     5.0
 
 import numpy as np
 import pandas as pd
@@ -11,26 +11,23 @@
 from .utils import transform, delete_multiple_element
 
 
-def check_store(store, sample, store_classes):
-    euc = []
-    for s in store:
-        euc.append(np.linalg.norm(s - sample))
-    euc = np.array(euc)
-    euc_nn = np.amin(euc)
-    index_nn = np.ravel(np.where(euc == euc_nn))
-    return store_classes[index_nn[0]]
+class CNN:
 
+    """
+    Hart, P. (1968). The condensed nearest neighbor rule (corresp.). IEEE
+    transactions on information theory, 14(3), 515-516.
 
-class CNN:
+    Parameters
+    ----------
+
+    """
 
     def __init__(self):
+        """A constructor for the class."""
         self.x_attr = None
 
     def filter(self, samples, y):
         """
-        Hart, P. (1968). The condensed nearest neighbor rule (corresp.). IEEE
-            transactions on information theory, 14(3), 515-516.
-
         Implementation of The Condensed Nearest Neighbor Rule
 
         The first sample of each class is placed in *store*. Thus we only have
@@ -48,6 +45,7 @@ def filter(self, samples, y):
         Extracted from:
         The condensed nearest neighbor rule. IEEE Transactions on Information
         Theory ( Volume: 14, Issue: 3, May 1968)
+
         :param samples: DataFrame.
         :param y: DataFrame.
         :return: the input dataset with the remaining samples.
@@ -61,7 +59,7 @@ def filter(self, samples, y):
         handbag = []
 
         for sample_class, sample in zip(samples.target, samples.data):
-            nn_class = check_store(store, sample, store_classes)
+            nn_class = self._check_store(store, sample, store_classes)
 
             if nn_class == sample_class:
                 handbag.append((sample_class, sample))
@@ -75,7 +73,7 @@ def filter(self, samples, y):
             indexes = []
             for index, s2 in enumerate(handbag):
                 sample_class, sample = s2
-                nn_class = check_store(store, sample, store_classes)
+                nn_class = self._check_store(store, sample, store_classes)
                 if nn_class != sample_class:
                     store.append(sample)
                     store_classes.append(sample_class)
@@ -88,3 +86,24 @@ def filter(self, samples, y):
             int))
 
         return samples, y
+
+    @staticmethod
+    def _check_store(store, sample, store_classes):
+        """
+        > The function takes in a sample, a store of samples, and the classes of
+         the store of samples. It then calculates the Euclidean distance
+         between the sample and each sample in the store. It then returns the
+         class of the sample in the store that is closest to the sample
+
+        :param store: the list of samples that have been stored
+        :param sample: the sample we want to classify
+        :param store_classes: the classes of the samples in the store
+        :return: The class of the nearest neighbor.
+        """
+        euc = []
+        for s in store:
+            euc.append(np.linalg.norm(s - sample))
+        euc = np.array(euc)
+        euc_nn = np.amin(euc)
+        index_nn = np.ravel(np.where(euc == euc_nn))
+        return store_classes[index_nn[0]]
\ No newline at end of file
diff --git a/instance_selection/_DROP3.py b/instance_selection/_DROP3.py
index a23cb15..0c38a1f 100644
--- a/instance_selection/_DROP3.py
+++ b/instance_selection/_DROP3.py
@@ -3,7 +3,7 @@
 # @Filename:    DROP3.py
 # @Author:      Daniel Puente Ramírez
 # @Time:        31/12/21 16:00
-# @Version:     4.0
+# @Version:     5.0
 
 import copy
 from sys import maxsize
@@ -15,52 +15,41 @@
 from .utils import transform
 
 
-def with_without(x_sample, samples_info):
-    index_a = 0
-    with_ = 0
-    without = 0
-    x_associates = samples_info[x_sample][1]
-    associates_targets = [samples_info[tuple(x)][2] for x in x_associates]
-    associates_neighs = [samples_info[tuple(x)][0] for x in x_associates]
-
-    for _, a_target, a_neighs in zip(x_associates,
-                                     associates_targets,
-                                     associates_neighs):
-
-        neighs_targets = np.ravel(np.array([samples_info[tuple(x)][2] for x
-                                            in a_neighs])).astype(int)
-        neighs_targets = neighs_targets.tolist()
-        # With
-        count = np.bincount(neighs_targets[:-1])
-        max_class = np.where(count == np.amax(count))[0][0]
-        if max_class == a_target:
-            with_ += 1
-
-        # Without
-        for index_a, neigh in enumerate(a_neighs):
-            if np.array_equal(neigh, x_sample):
-                break
-        count = np.bincount(neighs_targets[:index_a] + neighs_targets[
-                                                       index_a + 1:])
-        max_class = np.where(count == np.amax(count))[0][0]
-        if max_class == a_target:
-            without += 1
+class DROP3:
 
-    return with_, without
+    """
+    Wilson, D. R., & Martinez, T. R. (2000). Reduction techniques for
+    instance-based learning algorithms. Machine learning, 38(3), 257-286.
 
+    Parameters
+    ----------
+    nearest_neighbors : int, default=3
+        Number to use as nearest neighbors when computing distances.
+
+    power_parameter : int, default=2
+        Power parameter for the Minkowski metric. When p = 1, this is
+        equivalent to using manhattan_distance (l1), and euclidean_distance (l2)
+        for p = 2. For arbitrary p, minkowski_distance (l_p) is used.
+
+    """
 
-class DROP3:
     def __init__(self, nearest_neighbors=3, power_parameter=2):
+        """
+        The function takes in two parameters, nearest_neighbors and
+        power_parameter, and assigns them to the attributes nearest_neighbors
+        and power_parameter
+
+        :param nearest_neighbors: The number of nearest neighbors to use when
+        calculating the weights, defaults to 3 (optional)
+        :param power_parameter: This is the exponent that is used to calculate
+        the weights, defaults to 2 (optional)
+        """
         self.nearest_neighbors = nearest_neighbors
         self.power_parameter = power_parameter
         self.x_attr = None
 
     def filter(self, samples, y):
         """
-        Wilson, D. R., & Martinez, T. R. (2000). Reduction techniques for
-            instance-based learning algorithms. Machine learning, 38(3),
-            257-286.
-
         Implementation of DROP3.
 
         The Decremental Reduction Optimization Procedure (DROP) algorithms base
@@ -74,46 +63,11 @@ def filter(self, samples, y):
         :param y: DataFrame.
         :return: the input dataset with the remaining samples.
         """
-        self.x_attr = samples.keys()
-        samples = transform(samples, y)
-        s = copy.deepcopy(samples)
-        initial_samples = s['data']
-        initial_targets = s['target']
-        index_a = 0
-
-        initial_samples, samples_index = np.unique(ar=initial_samples,
-                                                   return_index=True, axis=0)
-        initial_targets = initial_targets[samples_index]
+        initial_distances, initial_samples, initial_targets, knn, \
+            samples_info = self._create_variables(samples, y)
 
-        knn = NearestNeighbors(n_neighbors=self.nearest_neighbors + 2, n_jobs=1,
-                               p=self.power_parameter)
-        knn.fit(initial_samples)
-
-        # Samples_info -> dict{x_sample : list([list(k+1-NN), list(x_associates)
-        # label])}
-        samples_info = {tuple(x): [[], [], y] for x, y in zip(initial_samples,
-                                                              initial_targets)}
-
-        initial_distances = []
-
-        for x_sample, x_target in zip(initial_samples, initial_targets):
-            # Find distance to closest enemy
-            min_distance = maxsize
-            for y_sample, y_label in zip(initial_samples, initial_targets):
-                if x_target != y_label:
-                    xy_distance = np.linalg.norm(x_sample - y_sample)
-                    if xy_distance < min_distance:
-                        min_distance = xy_distance
-            initial_distances.append([x_sample, x_target, min_distance])
-
-            # Find k+1-NN
-            _, neigh_ind = knn.kneighbors([x_sample])
-            x_neighs = [initial_samples[x] for x in neigh_ind[0][1:]]
-            samples_info[tuple(x_sample)][0] = x_neighs
-
-            # Add x in the associates lists of x_neighs
-            for neigh in x_neighs[:-1]:
-                samples_info[tuple(neigh)][1].append(x_sample)
+        self._find_associates(initial_distances, initial_samples,
+                              initial_targets, knn, samples_info)
 
         initial_distances.sort(key=lambda x: x[2], reverse=True)
 
@@ -122,24 +76,18 @@ def filter(self, samples, y):
         for index_x in range(size):
             x_sample = initial_distances[index_x - removed][0]
 
-            with_, without = with_without(tuple(x_sample), samples_info)
+            with_, without = self._with_without(tuple(x_sample), samples_info)
 
             if without >= with_:
                 initial_distances = initial_distances[:index_x - removed] + \
                                     initial_distances[index_x - removed + 1:]
                 removed += 1
 
-                # For each associate of x_sample
                 for a_associate_of_x in samples_info[(tuple(x_sample))][1]:
-                    # a_associate_of_x = a_associate_of_x.tolist()
-                    # Remove x_sample from a_associate neighs
-                    a_neighs = samples_info[tuple(a_associate_of_x)][0]
-                    for index_a, neigh in enumerate(a_neighs):
-                        if np.array_equal(neigh, x_sample):
-                            break
-                    a_neighs = a_neighs[:index_a] + a_neighs[index_a + 1:]
-                    # Find a new neigh for the associate
-                    remaining_samples = [x for x, _, _ in initial_distances]
+                    a_neighs, remaining_samples = self._remove_from_neighs(
+                        a_associate_of_x, initial_distances,
+                        samples_info, x_sample)
+
                     knn = NearestNeighbors(
                         n_neighbors=self.nearest_neighbors + 2,
                         n_jobs=1, p=self.power_parameter)
@@ -148,19 +96,9 @@ def filter(self, samples, y):
                     possible_neighs = [initial_distances[x][0] for x in
                                        neigh_ind[0]]
 
-                    for pos_neigh in possible_neighs[1:]:
-                        was_in = False
-                        for old_neigh in a_neighs:
-                            if np.array_equal(old_neigh, pos_neigh):
-                                was_in = True
-                                break
-                        if not was_in:
-                            a_neighs.append(pos_neigh)
-                            break
-
-                    samples_info[tuple(a_associate_of_x)][0] = a_neighs
+                    self._find_new_neighs(a_associate_of_x, a_neighs,
+                                          possible_neighs, samples_info)
 
-                    # Add a_associate to the associates list of the new neigh
                     new_neigh = a_neighs[-1]
                     samples_info[tuple(new_neigh)][1].append(
                         a_associate_of_x)
@@ -170,3 +108,163 @@ def filter(self, samples, y):
         y = pd.DataFrame([x for _, x, _ in initial_distances])
 
         return samples, y
+
+    def _create_variables(self, samples, y):
+        """
+        > It takes in the samples and targets, and returns the initial
+        distances, samples, targets, knn, and samples_info
+
+        :param samples: the data
+        :param y: the target variable
+        :return: initial_distances, initial_samples, initial_targets, knn,
+        samples_info
+        """
+        self.x_attr = samples.keys()
+        samples = transform(samples, y)
+        s = copy.deepcopy(samples)
+        initial_samples = s['data']
+        initial_targets = s['target']
+        initial_samples, samples_index = np.unique(ar=initial_samples,
+                                                   return_index=True, axis=0)
+        initial_targets = initial_targets[samples_index]
+        knn = NearestNeighbors(n_neighbors=self.nearest_neighbors + 2, n_jobs=1,
+                               p=self.power_parameter)
+        knn.fit(initial_samples)
+        samples_info = {tuple(x): [[], [], y] for x, y in zip(initial_samples,
+                                                              initial_targets)}
+        initial_distances = []
+        return initial_distances, initial_samples, initial_targets, knn, \
+            samples_info
+
+    @staticmethod
+    def _find_new_neighs(a_associate_of_x, a_neighs, possible_neighs,
+                         samples_info):
+        """
+        > The function takes a sample, finds its neighbors, and then checks if
+        any of the neighbors are not already in the list of neighbors. If
+        they are not, then they are added to the list of neighbors
+
+        :param a_associate_of_x: the sample we are looking for neighbors for
+        :param a_neighs: the list of neighbors of a_associate_of_x
+        :param possible_neighs: a list of all the possible neighbors of a given
+        point
+        :param samples_info: a dictionary with the following structure:
+        """
+        for pos_neigh in possible_neighs[1:]:
+            was_in = False
+            for old_neigh in a_neighs:
+                if np.array_equal(old_neigh, pos_neigh):
+                    was_in = True
+                    break
+            if not was_in:
+                a_neighs.append(pos_neigh)
+                break
+        samples_info[tuple(a_associate_of_x)][0] = a_neighs
+
+    @staticmethod
+    def _remove_from_neighs(a_associate_of_x, initial_distances,
+                            samples_info, x_sample):
+        """
+        > It removes the sample `x_sample` from the list of neighbors of
+        `a_associate_of_x` and returns the updated list of neighbors of
+        `a_associate_of_x` and the updated list of remaining samples
+
+        :param a_associate_of_x: the sample that is the associate of x
+        :param initial_distances: a list of tuples of the form (sample,
+        distance, associate)
+        :param samples_info: a dictionary of the form
+        {(x,y):[neighs,distances,associate]}
+        :param x_sample: the sample we want to find the nearest neighbor for
+        :return: the new list of neighbors of a_associate_of_x, and the list of
+        remaining samples.
+        """
+        a_neighs = samples_info[tuple(a_associate_of_x)][0]
+        index_to_use = 0
+        for index_a, neigh in enumerate(a_neighs):
+            index_to_use = index_a
+            if np.array_equal(neigh, x_sample):
+                break
+
+        a_neighs = a_neighs[:index_to_use] + a_neighs[index_to_use + 1:]
+        remaining_samples = [x for x, _, _ in initial_distances]
+
+        return a_neighs, remaining_samples
+
+    @staticmethod
+    def _find_associates(initial_distances, initial_samples, initial_targets,
+                         knn, samples_info):
+        """
+        For each sample in the initial set, find the closest sample from the
+        other class and store it in the initial_distances list
+
+        :param initial_distances: a list of lists, each list containing a
+        sample, its target, and its distance to the nearest sample of a
+        different class
+        :param initial_samples: the samples that we want to find the nearest
+        neighbors for
+        :param initial_targets: the labels of the initial samples
+        :param knn: the k-nearest neighbors model
+        :param samples_info: a dictionary that stores the neighbors of each
+        sample and the samples that are neighbors of each sample
+        """
+        for x_sample, x_target in zip(initial_samples, initial_targets):
+            min_distance = maxsize
+            for y_sample, y_label in zip(initial_samples, initial_targets):
+                if x_target != y_label:
+                    xy_distance = np.linalg.norm(x_sample - y_sample)
+                    if xy_distance < min_distance:
+                        min_distance = xy_distance
+            initial_distances.append([x_sample, x_target, min_distance])
+
+            _, neigh_ind = knn.kneighbors([x_sample])
+            x_neighs = [initial_samples[x] for x in neigh_ind[0][1:]]
+            samples_info[tuple(x_sample)][0] = x_neighs
+
+            for neigh in x_neighs[:-1]:
+                samples_info[tuple(neigh)][1].append(x_sample)
+
+    @staticmethod
+    def _with_without(x_sample, samples_info):
+        """
+        For each sample in the dataset, we find its associates and then for each
+        associate, we find its neighbors. We then find the class with the most
+        number of neighbors and compare it with the class of the associate. If
+        they are the same, we increment the `with_` variable. If they are not
+        the same, we increment the `without` variable
+
+        :param x_sample: the sample we're looking at
+        :param samples_info: a dictionary of the form {(x,y):[neighbors,
+        associates, target]}
+        :return: The number of times the target class of the sample is the most
+        common class among its neighbors, with and without the sample itself.
+        """
+        index_a = 0
+        with_ = 0
+        without = 0
+        x_associates = samples_info[x_sample][1]
+        associates_targets = [samples_info[tuple(x)][2] for x in x_associates]
+        associates_neighs = [samples_info[tuple(x)][0] for x in x_associates]
+
+        for _, a_target, a_neighs in zip(x_associates,
+                                         associates_targets,
+                                         associates_neighs):
+
+            neighs_targets = np.ravel(np.array([samples_info[tuple(x)][2] for x
+                                                in a_neighs])).astype(int)
+            neighs_targets = neighs_targets.tolist()
+
+            count = np.bincount(neighs_targets[:-1])
+            max_class = np.where(count == np.amax(count))[0][0]
+            if max_class == a_target:
+                with_ += 1
+
+            for index_a, neigh in enumerate(a_neighs):
+                if np.array_equal(neigh, x_sample):
+                    break
+            count = np.bincount(neighs_targets[:index_a] + neighs_targets[
+                                                           index_a + 1:])
+            max_class = np.where(count == np.amax(count))[0][0]
+            if max_class == a_target:
+                without += 1
+
+        return with_, without
diff --git a/instance_selection/_ENN.py b/instance_selection/_ENN.py
index c21d7d2..2363ede 100644
--- a/instance_selection/_ENN.py
+++ b/instance_selection/_ENN.py
@@ -3,7 +3,7 @@
 # @Filename:    ENN.py
 # @Author:      Daniel Puente Ramírez
 # @Time:        16/11/21 17:14
-# @Version:     6.0
+# @Version:     7.0
 
 import numpy as np
 import pandas as pd
@@ -13,12 +13,53 @@
 
 
 class ENN:
+
+    """
+    Wilson, D. L. (1972). Asymptotic properties of nearest neighbor rules
+    using edited data. IEEE Transactions on Systems, Man, and
+    Cybernetics, (3), 408-421.
+
+    Parameters
+    ----------
+    nearest_neighbors : int, default=3
+        Number to use as nearest neighbors when computing distances.
+
+    power_parameter : int, default=2
+        Power parameter for the Minkowski metric. When p = 1, this is
+        equivalent to using manhattan_distance (l1), and euclidean_distance (l2)
+        for p = 2. For arbitrary p, minkowski_distance (l_p) is used.
+
+    """
+
     def __init__(self, nearest_neighbors=3, power_parameter=2):
+        """
+        The function takes in two parameters, nearest_neighbors
+        and power_parameter, and assigns them to the attributes
+        nearest_neighbors and power_parameter
+
+        :param nearest_neighbors: The number of nearest neighbors to use when
+        calculating the weights, defaults to 3 (optional)
+        :param power_parameter: This is the exponent that is used to calculate
+        the weights, defaults to 2 (optional)
+        """
         self.nearest_neighbors = nearest_neighbors
         self.power_parameter = power_parameter
         self.x_attr = None
 
-    def __neighs(self, s_samples, s_targets, index, removed):
+    def _neighs(self, s_samples, s_targets, index, removed):
+        """
+        _neighs() takes in the samples and targets, the index of the sample to
+        be removed, and the number of samples already removed. It returns the
+        sample to be removed, its target, the targets of the samples not yet
+        removed, the samples not yet removed, and the indices of the nearest
+        neighbors of the sample to be removed.
+
+        :param s_samples: the samples that are being used to train the model
+        :param s_targets: the targets of the samples
+        :param index: the index of the sample to be removed
+        :param removed: the number of samples that have been removed from the
+        dataset
+        """
         x_sample = s_samples[index - removed]
         x_target = s_targets[index - removed]
         knn = NearestNeighbors(n_jobs=-1,
@@ -34,16 +75,13 @@ def __neighs(self, s_samples, s_targets, index, removed):
 
     def filter(self, samples, y):
         """
-        Wilson, D. L. (1972). Asymptotic properties of nearest neighbor rules
-            using edited data. IEEE Transactions on Systems, Man, and
-            Cybernetics, (3), 408-421.
-
         Implementation of the Wilson Editing algorithm.
 
         For each sample locates the *k* nearest neighbors and selects the
         number of different classes there are.
         If a sample results in a wrong classification after being classified
         with k-NN, that sample is removed from the TS.
+
         :param samples: DataFrame.
         :param y: DataFrame.
         :return: the input dataset with the remaining samples.
@@ -57,7 +95,7 @@ def filter(self, samples, y):
 
         for index in range(size):
             _, x_target, targets_not_x, samples_not_x, neigh_ind = \
-                self.__neighs(s_samples, s_targets, index, removed)
+                self._neighs(s_samples, s_targets, index, removed)
             y_targets = np.ravel(
                 np.array([targets_not_x[x] for x in neigh_ind[0]])).astype(int)
             count = np.bincount(y_targets)
@@ -82,6 +120,7 @@ def filter_original_complete(self, original, original_y, complete,
         If a sample results in a wrong classification after being classified
         with k-NN, that sample is removed from the TS, only if the sample to be
         removed is not from the original dataset.
+
         :param original: DataFrame: dataset with the initial samples.
         :param original_y: DataFrame: labels.
         :param complete: DataFrame: dataset with the initial samples and the new
@@ -100,7 +139,7 @@ def filter_original_complete(self, original, original_y, complete,
 
         for index in range(size):
             x_sample, x_target, targets_not_x, samples_not_x, neigh_ind = \
-                self.__neighs(s_samples, s_targets, index, removed)
+                self._neighs(s_samples, s_targets, index, removed)
             y_targets = [targets_not_x[x] for x in neigh_ind[0]]
             count = np.bincount(np.ravel(y_targets))
             max_class = np.where(count == np.amax(count))[0][0]
diff --git a/instance_selection/_ICF.py b/instance_selection/_ICF.py
index 382c990..d9ddcba 100644
--- a/instance_selection/_ICF.py
+++ b/instance_selection/_ICF.py
@@ -3,7 +3,7 @@
 # @Filename:    ICF.py
 # @Author:      Daniel Puente Ramírez
 # @Time:        23/11/21 09:37
-# @Version:     3.0
+# @Version:     4.0
 
 from sys import maxsize
 
@@ -14,76 +14,42 @@
 from .utils import transform, delete_multiple_element
 
 
-def __coverage__(train_set):
-    """
-    Inner method that performs the coverage and reachability of T.
-    :param train_set: samples of T
-    :return: (cov, reachable): vectors. cov contains for each sample the
-    samples that it can cover. And reachable for each sample the samples
-    that are able to reach it.
+class ICF:
+
     """
-    size = len(train_set.data)
-    matrix_distances = np.zeros([size, size])
-    distances_to_enemies = []
-    sol = []
-
-    for sample in range(size):
-        distance_to_closest_enemy = maxsize
-        x_sample = train_set['data'][sample]
-        x_target = train_set['target'][sample]
-        for other_sample in range(size):
-            y_sample = train_set['data'][other_sample]
-            y_target = train_set['target'][other_sample]
-            distance = np.linalg.norm(x_sample - y_sample)
-            matrix_distances[sample][other_sample] = distance
-            # Si son enemigas, nos quedamos con la distancia
-            if x_target != y_target and \
-                    distance < distance_to_closest_enemy:
-                distance_to_closest_enemy = distance
-        distances_to_enemies.append(distance_to_closest_enemy)
-
-    for sample in range(size):
-        x_coverage = []
-        x_target = train_set['target'][sample]
-        distance_to_closest_enemy = distances_to_enemies[sample]
-        for other_sample in range(size):
-            if sample == other_sample:
-                continue
-            y_target = train_set['target'][other_sample]
-            if x_target == y_target:
-                distance_between_samples = matrix_distances[sample][
-                    other_sample]
-                if distance_between_samples < distance_to_closest_enemy:
-                    x_coverage.append(other_sample)
-        sol.append([x_coverage])
-
-    for sample in range(size):
-        reachable = []
-        x_target = train_set['target'][sample]
-        for other_sample in range(size):
-            y_target = train_set['target'][other_sample]
-            if sample != other_sample and x_target == y_target:
-                # if x_target == y_target:
-                coverage = sol[other_sample][0]
-                if sample in coverage:
-                    reachable.append(other_sample)
-        sol[sample].append(reachable)
-
-    return sol
+    Brighton, H., & Mellish, C. (2002). Advances in instance selection for
+    instance-based learning algorithms. Data mining and knowledge
+    discovery, 6(2), 153-172.
 
+    Parameters
+    ----------
+    nearest_neighbors : int, default=3
+        Number to use as nearest neighbors when computing distances.
+
+    power_parameter : int, default=2
+        Power parameter for the Minkowski metric. When p = 1, this is
+        equivalent to using manhattan_distance (l1), and euclidean_distance (l2)
+        for p = 2. For arbitrary p, minkowski_distance (l_p) is used.
+
+    """
 
-class ICF:
     def __init__(self, nearest_neighbors=3, power_parameter=2):
+        """
+        The function takes in two parameters, nearest_neighbors and
+        power_parameter, and assigns them to the attributes nearest_neighbors
+        and power_parameter.
+
+        :param nearest_neighbors: The number of nearest neighbors to use when
+        calculating the weights, defaults to 3 (optional)
+        :param power_parameter: This is the exponent that is used to calculate
+        the weights, defaults to 2 (optional)
+        """
         self.nearest_neighbors = nearest_neighbors
         self.power_parameter = power_parameter
         self.x_attr = None
 
     def filter(self, samples, y):
         """
-        Brighton, H., & Mellish, C. (2002). Advances in instance selection for
-            instance-based learning algorithms. Data mining and knowledge
-            discovery, 6(2), 153-172.
-
         Implementation of Iterative Case Filtering.
 
         ICF is based on coverage and reachable, due to this two concepts it
@@ -94,6 +60,7 @@ def filter(self, samples, y):
         closest enemy is able to provide more information, the sample with
         less coverage than reachable will be removed.
         Ending up with a dataset as generalized as ICF allows.
+
         :param samples: DataFrame.
         :param y: DataFrame.
         :return: the input dataset with the remaining samples.
@@ -108,7 +75,7 @@ def filter(self, samples, y):
             data = list(ts['data'])
             target = list(ts['target'])
 
-            cov_reach = __coverage__(ts)
+            cov_reach = self._coverage(ts)
 
             progress = False
             removable_indexes = []
@@ -128,3 +95,111 @@ def filter(self, samples, y):
         y = pd.DataFrame(ts['target'])
 
         return samples, y
+
+    @staticmethod
+    def _coverage(train_set):
+        """
+        For each sample, it finds the closest enemy and the samples that can
+        reach it.
+
+        :param train_set: the training set
+        :return: A list of lists. Each list contains two lists. The first one is
+         the list of samples that can be covered by the sample. The second
+         one is the
+        list of samples that can reach the sample.
+        """
+        size = len(train_set.data)
+        matrix_distances = np.zeros([size, size])
+        distances_to_enemies = []
+        sol = []
+
+        ICF.calculate_distances_to_enemies(distances_to_enemies,
+                                           matrix_distances,
+                                           size, train_set)
+
+        ICF.calculate_coverage(distances_to_enemies, matrix_distances, size,
+                               sol, train_set)
+
+        ICF.calculate_reachability(size, sol, train_set)
+
+        return sol
+
+    @staticmethod
+    def calculate_reachability(size, sol, train_set):
+        """
+        For each sample, find all other samples that are reachable from it
+
+        :param size: the number of samples in the training set
+        :param sol: the solution matrix
+        :param train_set: the training set
+        """
+        for sample in range(size):
+            reachable = []
+            x_target = train_set['target'][sample]
+            for other_sample in range(size):
+                y_target = train_set['target'][other_sample]
+                if sample != other_sample and x_target == y_target:
+                    coverage = sol[other_sample][0]
+                    if sample in coverage:
+                        reachable.append(other_sample)
+            sol[sample].append(reachable)
+
+    @staticmethod
+    def calculate_coverage(distances_to_enemies, matrix_distances, size, sol,
+                           train_set):
+        """
+        For each sample, we find the distance to the closest enemy. Then, we
+        find all the samples that are closer to the current sample than the
+        closest enemy
+
+        :param distances_to_enemies: a list of distances to the closest enemy
+        for each sample
+        :param matrix_distances: a matrix of distances between all samples in
+        the training set
+        :param size: the number of samples in the training set
+        :param sol: the solution matrix
+        :param train_set: the training set
+        """
+        for sample in range(size):
+            x_coverage = []
+            x_target = train_set['target'][sample]
+            distance_to_closest_enemy = distances_to_enemies[sample]
+            for other_sample in range(size):
+                if sample == other_sample:
+                    continue
+                y_target = train_set['target'][other_sample]
+                if x_target == y_target:
+                    distance_between_samples = matrix_distances[sample][
+                        other_sample]
+                    if distance_between_samples < distance_to_closest_enemy:
+                        x_coverage.append(other_sample)
+            sol.append([x_coverage])
+
+    @staticmethod
+    def calculate_distances_to_enemies(distances_to_enemies,
+                                       matrix_distances, size, train_set):
+        """
+        For each sample in the training set, calculate the distance to the
+        closest enemy and store it in the distances_to_enemies array
+
+        :param distances_to_enemies: a list of distances to the closest enemy
+        for each sample
+        :param matrix_distances: a matrix of size (size,size) where size is the
+        number of samples in the train set
+        :param size: the number of samples in the training set
+        :param train_set: the training set
+        """
+        for sample in range(size):
+            distance_to_closest_enemy = maxsize
+            x_sample = train_set['data'][sample]
+            x_target = train_set['target'][sample]
+            for other_sample in range(size):
+                y_sample = train_set['data'][other_sample]
+                y_target = train_set['target'][other_sample]
+                distance = np.linalg.norm(x_sample - y_sample)
+                matrix_distances[sample][other_sample] = distance
+
+                if x_target != y_target and \
+                        distance < distance_to_closest_enemy:
+                    distance_to_closest_enemy = distance
+            distances_to_enemies.append(distance_to_closest_enemy)
diff --git a/instance_selection/_LocalSets.py b/instance_selection/_LocalSets.py
index bc6884b..87248ee 100644
--- a/instance_selection/_LocalSets.py
+++ b/instance_selection/_LocalSets.py
@@ -3,7 +3,7 @@
 # @Filename:    _LocalSets.py
 # @Author:      Daniel Puente Ramírez
 # @Time:        18/3/22 11:14
-# @Version:     2.0
+# @Version:     3.0
 import sys
 
 import numpy as np
@@ -12,17 +12,30 @@
 
 
 class LocalSets:
+
     """
     Leyva, E., González, A., & Pérez, R. (2015). Three new instance selection
-        methods based on local sets: A comparative study with several approaches
-        from a bi-objective perspective. Pattern Recognition, 48(4), 1523-1537.
+    methods based on local sets: A comparative study with several approaches
+    from a bi-objective perspective. Pattern Recognition, 48(4), 1523-1537.
+
+    Parameters
+    ----------
+
     """
 
     def __init__(self):
+        """A constructor for the class."""
         self.local_sets = None
         self.n_id = 0
 
-    def compute_local_sets(self, instances, labels):
+    def _compute_local_sets(self, instances, labels):
+        """
+        For each sample, find the closest enemy sample and the closest friendly
+        sample
+
+        :param instances: the data
+        :param labels: the labels of the data
+        """
         self.n_id = len(instances)
         structure = dict.fromkeys(range(self.n_id))
         distances = pairwise_distances(instances)
@@ -30,12 +43,10 @@ def compute_local_sets(self, instances, labels):
         for index, (sample, label) in enumerate(zip(instances, labels)):
             closest_enemy_distance = sys.maxsize
             closest_enemy_sample = None
-            for index2, (_, label2) in enumerate(zip(instances, labels)):
-                if index == index2 or label == label2:
-                    continue
-                if distances[index][index2] < closest_enemy_distance:
-                    closest_enemy_distance = distances[index][index2]
-                    closest_enemy_sample = index2
+            closest_enemy_distance, closest_enemy_sample = \
+                self._find_enemy_distance(
+                    closest_enemy_distance, closest_enemy_sample, distances,
+                    index, instances, label, labels)
             structure[index] = [sample, [], None, closest_enemy_distance,
                                 closest_enemy_sample, label]
 
@@ -55,33 +66,84 @@ def compute_local_sets(self, instances, labels):
                                                          'enemy', 'label']) \
             .transpose()
 
-    def sort_asc_lsc(self):
+    @staticmethod
+    def _find_enemy_distance(closest_enemy_distance, closest_enemy_sample,
+                             distances, index, instances, label, labels):
+        for index2, (_, label2) in enumerate(zip(instances, labels)):
+            if index == index2 or label == label2:
+                continue
+            if distances[index][index2] < closest_enemy_distance:
+                closest_enemy_distance = distances[index][index2]
+                closest_enemy_sample = index2
+        return closest_enemy_distance, closest_enemy_sample
+
+    def _sort_asc_lsc(self):
+        """
+        The function takes a dataframe of local sets and sorts them in ascending
+        order by the LSC value
+        """
         self.local_sets = self.local_sets.sort_values(by='LSC')
 
-    def usefulness(self, e):
+    def _usefulness(self, e):
+        """
+        It returns the number of local sets that contain the element e
+
+        :param e: the element to be added to the set
+        :return: The number of times the element appears in the local sets.
+        """
         local_sets = self.local_sets
         local_sets = local_sets['index_ls'].values
         return len([x for x in local_sets if e in x])
 
-    def get_local_sets(self):
+    def _get_local_sets(self):
+        """
+        It returns the local sets of the class.
+        :return: The local_sets variable is being returned.
+        """
         return self.local_sets
 
     @staticmethod
-    def check_frame_to_numpy(y):
+    def _check_frame_to_numpy(y):
+        """
+        If the input is a pandas dataframe, convert it to a numpy array.
+        Otherwise, return the input
+
+        :param y: The target variable
+        :return: the y value as a numpy array.
+        """
         if isinstance(y, pd.DataFrame):
             return np.ravel(y.to_numpy())
         return y
 
 
 class LSSm(LocalSets):
+    """
+    Leyva, E., González, A., & Pérez, R. (2015). Three new instance selection
+    methods based on local sets: A comparative study with several approaches
+    from a bi-objective perspective. Pattern Recognition, 48(4), 1523-1537.
+
+    Parameters
+    ----------
+
+    """
+
     def __init__(self):
+        """A constructor for the class."""
         super().__init__()
 
     def filter(self, instances, labels):
-        names = instances.keys()
+        """
+        The function takes in a dataframe of instances and a dataframe of
+        labels, and returns a dataframe of instances and a dataframe of labels
+
+        :param instances: the dataframe of the instances
+        :param labels: The labels of the data
+        :return: The return is a tuple of two dataframes. The first dataframe is
+         the instances and the second dataframe is the labels.
+        """
         instances = instances.to_numpy()
         instances = [np.ravel(i) for i in instances]
-        labels = self.check_frame_to_numpy(labels)
+        labels = self._check_frame_to_numpy(labels)
         if len(instances) != len(labels):
             raise ValueError(
                 f'The dimension of the labeled data must be the same as the '
@@ -91,11 +153,11 @@ def filter(self, instances, labels):
         s_samples = []
         s_labels = []
 
-        super(LSSm, self).compute_local_sets(instances, labels)
+        super(LSSm, self)._compute_local_sets(instances, labels)
         for index in range(self.n_id):
-            usefulness = super(LSSm, self).usefulness(index)
+            usefulness = super(LSSm, self)._usefulness(index)
             try:
-                harmfulness = super(LSSm, self).get_local_sets()['enemy']
+                harmfulness = super(LSSm, self)._get_local_sets()['enemy']
                 harmfulness = harmfulness.value_counts()[index]
             except KeyError:
                 harmfulness = 0
@@ -103,33 +165,51 @@ def filter(self, instances, labels):
                 s_samples.append(instances[index])
                 s_labels.append(labels[index])
 
-        x = pd.DataFrame(s_samples, columns=names)
+        x = pd.DataFrame(s_samples)
         y = pd.DataFrame(s_labels)
         return x, y
 
 
 class LSBo(LocalSets):
+    """
+    Leyva, E., González, A., & Pérez, R. (2015). Three new instance selection
+    methods based on local sets: A comparative study with several approaches
+    from a bi-objective perspective. Pattern Recognition, 48(4), 1523-1537.
+
+    Parameters
+    ----------
+
+    """
+
     def __init__(self):
+        """A constructor for the class."""
         super(LSBo, self).__init__()
 
     def filter(self, instances, labels):
-        names = instances.keys()
+        """
+        > The function takes in a dataframe of instances and a dataframe of
+        labels, and returns a dataframe of instances and a dataframe of labels
+
+        :param instances: the dataframe of the instances
+        :param labels: The labels of the data
+        :return: The filtered data.
+        """
         if len(instances) != len(labels):
             raise ValueError(
                 f'The dimension of the labeled data must be the same as the '
                 f'number of labels given. {len(instances)} != {len(labels)}'
             )
         self.n_id = len(instances)
-        labels = self.check_frame_to_numpy(labels)
+        labels = self._check_frame_to_numpy(labels)
         lssm = LSSm()
         instances, labels = lssm.filter(instances, labels)
         instances = instances.to_numpy()
         labels = labels.to_numpy().flatten()
-        super(LSBo, self).compute_local_sets(instances, labels)
-        super(LSBo, self).sort_asc_lsc()
+        super(LSBo, self)._compute_local_sets(instances, labels)
+        super(LSBo, self)._sort_asc_lsc()
 
         s_indexes = []
-        for index, row in super(LSBo, self).get_local_sets().iterrows():
+        for index, row in super(LSBo, self)._get_local_sets().iterrows():
             is_in = False
             for ls_index in row['index_ls']:
                 if ls_index in s_indexes:
@@ -138,7 +218,7 @@ def filter(self, instances, labels):
             if not is_in:
                 s_indexes.append(index)
 
-        x = pd.DataFrame(instances[s_indexes], columns=names)
+        x = pd.DataFrame(instances[s_indexes])
         y = pd.DataFrame(labels[s_indexes])
 
         return x, y
diff --git a/instance_selection/_MSS.py b/instance_selection/_MSS.py
index b6d100a..8a43b68 100644
--- a/instance_selection/_MSS.py
+++ b/instance_selection/_MSS.py
@@ -3,7 +3,7 @@
 # @Filename:    MSS.py
 # @Author:      Daniel Puente Ramírez
 # @Time:        23/11/21 11:53
-# @Version:     2.0
+# @Version:     3.0
 
 import sys
 
@@ -13,43 +13,25 @@
 from .utils import transform
 
 
-def __enemy_distance__(dat, tar):
-    """
-    Inner method which calculates the distance between each sample and its
-    nearest enemy.
-    :param dat: T samples.
-    :param tar: T classes.
-    :return: list of triplets: [sample, class, distance], sorted by the
-    smallest distance.
-    """
-    solution = []
-    for sample, x_class in zip(dat, tar):
-        distance = sys.maxsize
-        for sample_1, x1_class in zip(dat, tar):
-            if x1_class == x_class:
-                continue
-            else:
-                euc = np.linalg.norm(sample - sample_1)
-                if euc < distance:
-                    distance = euc
-        solution.append([sample, x_class, distance])
+class MSS:
 
-    solution.sort(key=lambda x: x[2])
+    """
+    Barandela, R., Ferri, F. J., & Sánchez, J. S. (2005). Decision boundary
+    preserving prototype selection for nearest neighbor classification.
+    International Journal of Pattern Recognition and Artificial
+    Intelligence, 19(06), 787-806.
 
-    return solution
+    Parameters
+    ----------
 
+    """
 
-class MSS:
     def __init__(self):
+        """A constructor for the class."""
         self.x_attr = None
 
     def filter(self, samples, y):
         """
-        Barandela, R., Ferri, F. J., & Sánchez, J. S. (2005). Decision boundary
-            preserving prototype selection for nearest neighbor classification.
-            International Journal of Pattern Recognition and Artificial
-            Intelligence, 19(06), 787-806.
-
         Implementation of Modified Selective Subset.
 
         It starts with two empty arrays *dat* and *tar*, which will contain the
@@ -66,7 +48,7 @@ def filter(self, samples, y):
         """
         self.x_attr = samples.keys()
         samples = transform(samples, y)
-        triplets = __enemy_distance__(samples['data'], samples['target'])
+        triplets = self._enemy_distance(samples['data'], samples['target'])
         dat = []
         tar = []
         remove_indexes = []
@@ -90,3 +72,31 @@ def filter(self, samples, y):
         y = pd.DataFrame(tar)
 
         return samples, y
+
+    @staticmethod
+    def _enemy_distance(dat, tar):
+        """
+        For each sample in the dataset, find the distance to the nearest sample
+        of a different class.
+
+        :param dat: the data
+        :param tar: the target variable
+        :return: A list of lists, where each list contains a sample, its class,
+         and its distance to its nearest enemy.
+        """
+
+        solution = []
+        for sample, x_class in zip(dat, tar):
+            distance = sys.maxsize
+            for sample_1, x1_class in zip(dat, tar):
+                if x1_class == x_class:
+                    continue
+                else:
+                    euc = np.linalg.norm(sample - sample_1)
+                    if euc < distance:
+                        distance = euc
+            solution.append([sample, x_class, distance])
+
+        solution.sort(key=lambda x: x[2])
+
+        return solution
diff --git a/instance_selection/_RNN.py b/instance_selection/_RNN.py
index 1fc8052..c184844 100644
--- a/instance_selection/_RNN.py
+++ b/instance_selection/_RNN.py
@@ -3,7 +3,7 @@
 # @Filename:    RNN.py
 # @Author:      Daniel Puente Ramírez
 # @Time:        22/11/21 08:22
-# @Version:     2.0
+# @Version:     3.0
 
 from collections import deque
 
@@ -16,14 +16,21 @@
 
 class RNN:
 
+    """
+    Gates, G. (1972). The reduced nearest neighbor rule (corresp.).
+    IEEE transactions on information theory, 18(3), 431-433.
+
+    Parameters
+    ----------
+
+    """
+
     def __init__(self):
+        """A constructor for the class."""
         self.x_attr = None
 
     def filter(self, samples, y):
         """
-        Gates, G. (1972). The reduced nearest neighbor rule (corresp.).
-            IEEE transactions on information theory, 18(3), 431-433.
-
         Implementation of The Reduced Nearest Neighbor.
 
         RNN is an extension of CNN. Firstly CNN will be executed in order to
diff --git a/instance_selection/__init__.py b/instance_selection/__init__.py
index 8b3c6a4..6e194ac 100644
--- a/instance_selection/__init__.py
+++ b/instance_selection/__init__.py
@@ -1,5 +1,4 @@
-"""
-Instance Selection.
+"""Instance Selection.
 
 The package contains some of the most widely used instance selection algorithms
 in the literature.
diff --git a/instance_selection/utils/__init__.py b/instance_selection/utils/__init__.py
index 61248ec..9dc9cd7 100644
--- a/instance_selection/utils/__init__.py
+++ b/instance_selection/utils/__init__.py
@@ -1,3 +1,8 @@
+"""Instance Selection Utils.
+
+Methods that are useful for multiple algorithms implemented in the instance
+selection library.
+"""
 from ._transformer import transform, transform_original_complete, \
     delete_multiple_element
 
diff --git a/instance_selection/utils/_transformer.py b/instance_selection/utils/_transformer.py
index 13ecf9b..fc9edaa 100644
--- a/instance_selection/utils/_transformer.py
+++ b/instance_selection/utils/_transformer.py
@@ -2,16 +2,45 @@
 
 
 def transform(samples, y):
+    """
+    Transform pandas DataFrame to Sklearn Bunch.
+
+    It takes a pandas dataframe and a pandas series, converts them to numpy
+    arrays, and returns a Bunch object with the data and target attributes
+    set to the numpy arrays
+
+    :param samples: The data to be transformed
+    :param y: The target variable
+    :return: A Bunch object, which is a dictionary-like object.
+    """
     x_transformed = samples.to_numpy()
     y_transformed = y.to_numpy()
     return Bunch(data=x_transformed, target=y_transformed)
 
 
 def transform_original_complete(original, original_y, complete, complete_y):
+    """
+    It takes in two sets of data, and returns two sets of data.
+
+    :param original: the original data
+    :param original_y: the original labels
+    :param complete: the complete data set
+    :param complete_y: the labels for the complete data
+    :return: The transformed original and complete data.
+    """
     return transform(original, original_y), transform(complete, complete_y)
 
 
 def delete_multiple_element(list_object, indices, reverse=True):
+    """
+    It deletes multiple elements from a list, given a list of indices.
+
+    :param list_object: The list object you want to delete elements from
+    :param indices: a list of indices to be deleted
+    :param reverse: If True, the indices are sorted in descending order. If
+    False, the indices are sorted in ascending order, defaults to True (
+    optional)
+    """
     indices = sorted(indices, reverse=reverse)
     for idx in indices:
         if idx < len(list_object):
diff --git a/semisupervised/CoTraining.py b/semisupervised/CoTraining.py
index 06d6cb5..a277186 100644
--- a/semisupervised/CoTraining.py
+++ b/semisupervised/CoTraining.py
@@ -3,7 +3,7 @@
 # @Filename:    CoTraining.py
 # @Author:      Daniel Puente Ramírez
 # @Time:        22/12/21 09:27
-# @Version:     4.0
+# @Version:     5 .0
 
 from math import ceil, floor
 
@@ -15,15 +15,46 @@
 
 
 class CoTraining:
-    """Blum, A., & Mitchell, T. (1998, July). Combining labeled and unlabeled
-        data with co-training. In Proceedings of the eleventh annual conference
-        on Computational learning theory (pp. 92-100).
+    """
+    Blum, A., & Mitchell, T. (1998, July). Combining labeled and unlabeled
+    data with co-training. In Proceedings of the eleventh annual conference
+    on Computational learning theory (pp. 92-100).
+
+    Parameters
+    ----------
+    p : int, default=1
+        The number of positive samples.
+
+    n : int, default=3
+        The number of negative samples.
+
+    k : int, default=30
+        The number of iterations to train the classifiers.
+
+    u : int, default=75
+        The number of unlabeled samples to use in the training set
+
+    random_state : int, default=None
+        The random seed used to generate the initial population
+
+    c1 : base_estimator, default=GaussianNB
+        The first classifier to be used
+
+    c1_params : dict, default=None
+        Parameters for the first classifier
+
+    c2 : base_estimator, default=GaussianNB
+        The second classifier to be used
+
+    c2_params : dict, default=None
+        Parameters for the second classifier
+
      """
 
     def __init__(self, p=1, n=3, k=30, u=75, random_state=None,
                  c1=None, c1_params=None,
-                 c2=None, c2_params=None,
-                 ):
+                 c2=None, c2_params=None,):
+        """Co-Training."""
         self.p = p
         self.n = n
         self.k = k
@@ -46,10 +77,17 @@ def __init__(self, p=1, n=3, k=30, u=75, random_state=None,
         self.h1, self.h2 = configs
 
     def fit(self, samples, y):
-        try:
-            labeled, u, y = split(samples, y)
-        except IndexError:
-            raise ValueError('Dimensions do not match.')
+        """
+        The function takes in a set of labeled samples and unlabeled samples,
+        and then uses the labeled samples to train two classifiers, and then
+        uses the two classifiers to predict the unlabeled samples. The top n
+        samples with the highest confidence are then added to the labeled
+        samples, and the process is repeated k times
+
+        :param samples: the unlabeled data
+        :param y: the labels of the samples
+        """
+        labeled, rng, u, u_random_index, y = self._check_parameters(samples, y)
 
         le = LabelEncoder()
         le.fit(y)
@@ -58,14 +96,6 @@ def fit(self, samples, y):
 
         self.size_x1 = ceil(len(labeled[0]) / 2)
 
-        rng = np.random.default_rng()
-        try:
-            u_random_index = rng.choice(len(u), size=floor(self.u),
-                                        replace=False, shuffle=False)
-        except ValueError:
-            raise ValueError('The model was incorrectly parametrized, '
-                             'total between _p_ and _u_ is to big.')
-
         u_prime = u[u_random_index]
         u1, u2 = np.array_split(u_prime, 2, axis=1)
 
@@ -118,7 +148,38 @@ def fit(self, samples, y):
 
             u_prime = np.concatenate((u_prime, u[u_random_index]))
 
+    def _check_parameters(self, samples, y):
+        """
+        > The function checks the parameters of the model and returns the
+        labeled samples, the random number generator, the unlabeled samples,
+        the random index of the unlabeled samples, and the labels
+
+        :param samples: The samples to be labeled
+        :param y: the target variable
+        :return: the labeled, rng, u, u_random_index, y
+        """
+        try:
+            labeled, u, y = split(samples, y)
+        except IndexError:
+            raise ValueError('Dimensions do not match.')
+        rng = np.random.default_rng()
+        try:
+            u_random_index = rng.choice(len(u), size=floor(self.u),
+                                        replace=False, shuffle=False)
+        except ValueError:
+            raise ValueError('The model was incorrectly parametrized, '
+                             'total between _p_ and _u_ is to big.')
+        return labeled, rng, u, u_random_index, y
+
     def predict(self, samples):
+        """
+        If the predictions of the two classifiers are the same, return that
+        prediction. If they disagree, return the prediction of the classifier
+        with the highest probability
+
+        :param samples: the data to be predicted
+        :return: The labels of the samples.
+        """
         x1, x2 = np.array_split(samples, 2, axis=1)
         pred1, pred_proba1 = self.h1.predict(x1), self.h1.predict_proba(x1)
         pred2, pred_proba2 = self.h2.predict(x2), self.h2.predict_proba(x2)
diff --git a/semisupervised/DemocraticCoLearning.py b/semisupervised/DemocraticCoLearning.py
index 09e7a9e..76125a0 100644
--- a/semisupervised/DemocraticCoLearning.py
+++ b/semisupervised/DemocraticCoLearning.py
@@ -3,9 +3,10 @@
 # @Filename:    DemocraticCoLearning.py
 # @Author:      Daniel Puente Ramírez
 # @Time:        29/12/21 15:39
-# @Version:     4.0
+# @Version:     5.0
 
 import copy
+import warnings
 from math import sqrt
 
 import numpy as np
@@ -17,31 +18,43 @@
 from .utils import split
 
 
-def check_bounds(wi):
-    """Check upper and lower bounds. The left minimum value can be 0, and the
-    right minimum value can be 1.
-
-    :param wi: lower and upper mean confidence
-    :return: wi fixed
+class DemocraticCoLearning:
     """
-    if wi[0] < 0:
-        wi[0] = 0
-    if wi[1] > 1:
-        wi[1] = 1
-    return wi
+    Democratic Co-Learning Implementation. Based on:
+    Zhou, Y., & Goldman, S. (2004, November). Democratic co-learning.
+    In 16th IEEE International Conference on Tools with Artificial
+    Intelligence (pp. 594-602). IEEE.
 
+    Parameters
+    ----------
+    random_state : int, default=None
+        The random seed used to initialize the classifiers
+
+    c1 : base_estimator, default=MultinomialNB
+        The first classifier to be used
+
+    c1_params : dict, default=None
+        Parameters for the first classifier
+
+    c2 : base_estimator, default=KNeighborsClassifier
+        The second classifier to be used
+
+    c2_params : dict, default=None
+        Parameters for the second classifier
+
+    c3 : base_estimator, default=DecisionTreeClassifier
+        The third classifier to be used
+
+    c3_params : dict, default=None
+        Parameters for the third classifier
 
-class DemocraticCoLearning:
-    """Democratic Co-Learning Implementation. Based on:
-        Zhou, Y., & Goldman, S. (2004, November). Democratic co-learning.
-        In 16th IEEE International Conference on Tools with Artificial
-        Intelligence (pp. 594-602). IEEE.
     """
 
     def __init__(self, random_state=None,
                  c1=None, c1_params=None,
                  c2=None, c2_params=None,
                  c3=None, c3_params=None):
+        """Democratic Co-Learning."""
         self.const = 1.96  # 95%
         self.random_state = random_state if random_state is not None else \
             np.random.randint(low=0, high=10e5, size=1)[0]
@@ -69,6 +82,17 @@ def __init__(self, random_state=None,
         self.h1, self.h2, self.h3 = configs
 
     def fit(self, samples, y):
+        """
+        The function takes in a set of labeled and unlabeled data, and uses the
+        labeled data to train three classifiers. Then, it uses the three
+        classifiers to predict the labels of the unlabeled data. If the
+        prediction is correct, the data is not added to the training set. If
+        the prediction is incorrect, the data is added to the training set.
+        The process is repeated until the training set stops changing
+
+        :param samples: the training data
+        :param y: the labels of the samples
+        """
         try:
             labeled, u, y = split(samples, y)
         except IndexError:
@@ -130,7 +154,7 @@ def fit(self, samples, y):
                                             len(labeled)),
                   error + self.const * sqrt((error * (1 - error)) /
                                             len(labeled))]
-            w1 = sum(check_bounds(w1)) / 2
+            w1 = sum(self.check_bounds(w1)) / 2
 
             for index, proba in enumerate(probas):
                 c_k = new_labels[index][0]
@@ -155,7 +179,7 @@ def fit(self, samples, y):
                                             len(labeled)),
                   error + self.const * sqrt((error * (1 - error)) /
                                             len(labeled))]
-            w2 = sum(check_bounds(w2)) / 2
+            w2 = sum(self.check_bounds(w2)) / 2
 
             for index, proba in enumerate(probas):
                 c_k = new_labels[index][0]
@@ -180,7 +204,7 @@ def fit(self, samples, y):
                                             len(labeled)),
                   error + self.const * sqrt((error * (1 - error)) /
                                             len(labeled))]
-            w3 = sum(check_bounds(w3)) / 2
+            w3 = sum(self.check_bounds(w3)) / 2
 
             for index, proba in enumerate(probas):
                 c_k = new_labels[index][0]
@@ -211,7 +235,7 @@ def fit(self, samples, y):
             ci_1 = [
                 error - self.const * sqrt((error * (1 - error)) / len(pred)),
                 error + self.const * sqrt((error * (1 - error)) / len(pred))]
-            ci_1 = check_bounds(ci_1)
+            ci_1 = self.check_bounds(ci_1)
             q_1 = len(pred) * pow((1 - 2 * (e_1 / len(pred))), 2)
             e_prime_1 = (1 - (ci_1[0] * len(pred)) / len(pred)) * len(pred)
             q_prime_1 = (len(l1_data) + len(pred)) * pow(
@@ -235,7 +259,7 @@ def fit(self, samples, y):
             ci_2 = [
                 error - self.const * sqrt((error * (1 - error)) / len(pred)),
                 error + self.const * sqrt((error * (1 - error)) / len(pred))]
-            ci_2 = check_bounds(ci_2)
+            ci_2 = self.check_bounds(ci_2)
             q_2 = len(pred) * pow((1 - 2 * (e_2 / len(pred))), 2)
             e_prime_2 = (1 - (ci_2[0] * len(pred)) / len(pred)) * len(pred)
             q_prime_2 = (len(l2_data) + len(pred)) * pow(
@@ -259,7 +283,7 @@ def fit(self, samples, y):
             ci_3 = [
                 error - self.const * sqrt((error * (1 - error)) / len(pred)),
                 error + self.const * sqrt((error * (1 - error)) / len(pred))]
-            ci_3 = check_bounds(ci_3)
+            ci_3 = self.check_bounds(ci_3)
             q_3 = len(pred) * pow((1 - 2 * (e_3 / len(pred))), 2)
             e_prime_3 = (1 - (ci_3[0] * len(pred)) / len(pred)) * len(pred)
             q_prime_3 = (len(l3_data) + len(pred)) * pow(
@@ -278,19 +302,27 @@ def fit(self, samples, y):
         error = len([0 for p, tar in zip(pred, y) if p != tar]) / len(pred)
         w1 = [error - self.const * sqrt((error * (1 - error)) / len(labeled)),
               error + self.const * sqrt((error * (1 - error)) / len(labeled))]
-        self.w1 = sum(check_bounds(w1)) / 2
+        self.w1 = sum(self.check_bounds(w1)) / 2
         pred = self.h2.predict(labeled)
         error = len([0 for p, tar in zip(pred, y) if p != tar]) / len(pred)
         w2 = [error - self.const * sqrt((error * (1 - error)) / len(labeled)),
               error + self.const * sqrt((error * (1 - error)) / len(labeled))]
-        self.w2 = sum(check_bounds(w2)) / 2
+        self.w2 = sum(self.check_bounds(w2)) / 2
         pred = self.h3.predict(labeled)
         error = len([0 for p, tar in zip(pred, y) if p != tar]) / len(pred)
         w3 = [error - self.const * sqrt((error * (1 - error)) / len(labeled)),
               error + self.const * sqrt((error * (1 - error)) / len(labeled))]
-        self.w3 = sum(check_bounds(w3)) / 2
+        self.w3 = sum(self.check_bounds(w3)) / 2
 
     def predict(self, samples):
+        """
+        For each sample, we get the predictions of the three classifiers, and
+        then we count the number of times each label appears in the
+        predictions. The label that appears the most is the one we return
+
+        :param samples: the samples to be classified
+        :return: The labels of the samples.
+        """
         all_instances = samples
 
         gj = [0 for _ in range(self.n_labels)]
@@ -315,7 +347,7 @@ def predict(self, samples):
                     gj[p] += 1
                     gj_h[2][p] += 1
         except IndexError:
-            breakpoint()
+            warnings.warn("Retraining the model is advised.")
 
         confidence = [0 for _ in range(self.n_labels)]
         for index, j in enumerate(gj):
@@ -340,3 +372,18 @@ def predict(self, samples):
             labels.append(np.where(count == np.amax(count))[0][0])
 
         return np.array(labels)
+
+    @staticmethod
+    def check_bounds(wi):
+        """
+        It checks that the lower bound is not less than 0 and the upper bound
+        is not greater than 1
+
+        :param wi: lower and upper mean confidence
+        :return: the fixed wi.
+        """
+        if wi[0] < 0:
+            wi[0] = 0
+        if wi[1] > 1:
+            wi[1] = 1
+        return wi
\ No newline at end of file
diff --git a/semisupervised/DensityPeaks.py b/semisupervised/DensityPeaks.py
index b682a12..d173510 100644
--- a/semisupervised/DensityPeaks.py
+++ b/semisupervised/DensityPeaks.py
@@ -94,7 +94,8 @@ def __build_distance(self):
 
     def __auto_select_dc(self):
         """
-        Auto select the local density threshold that let average neighbor is 1-2 percent of all nodes.
+        Auto select the local density threshold that let average neighbor is 1-2
+         percent of all nodes.
 
         :return: dc that local density threshold
         """
@@ -118,7 +119,8 @@ def __auto_select_dc(self):
 
     def __select_dc(self):
         """
-        Select the local density threshold, default is the method used in paper, 'auto' is auto select.
+        Select the local density threshold, default is the method used in paper,
+        'auto' is auto select.
 
         :return: dc that local density threshold
         """
@@ -150,12 +152,13 @@ def __local_density(self):
 
     def __min_neighbor_and_distance(self):
         """
-        Compute all points' min util to the higher local density point(which is the nearest neighbor).
+        Compute all points' min util to the higher local density point(which is
+         the nearest neighbor).
 
         :return: distance vector, nearest neighbor vector
         """
         sort_rho_idx = np.argsort(-self.rho)
-        delta, nneigh = [float(self.max_dis)] * (self.n_id), [0] * self.n_id
+        delta, nneigh = [float(self.max_dis)] * self.n_id, [0] * self.n_id
         delta[sort_rho_idx[0]] = -1.
         for i in range(self.n_id):
             for j in range(0, i):
@@ -168,6 +171,18 @@ def __min_neighbor_and_distance(self):
         return np.array(delta, np.float32), np.array(nneigh, np.float32)
 
     def __structure(self):
+        """
+        The function takes the data and the nearest neighbor indices and creates
+        a dataframe with the following columns:
+
+        - sample: the data point
+        - next: the index of the nearest neighbor
+        - previous: the index of the nearest neighbor of the nearest neighbor
+        - label: the label of the data point
+
+        The function also creates a copy of the dataframe called
+        structure_stdnpf
+        """
         self.structure = dict.fromkeys(range(self.n_id))
         for index, sample in enumerate(self.data):
             self.structure[index] = [
@@ -189,16 +204,42 @@ def __structure(self):
         self.structure_stdnpf = self.structure.copy(deep=True)
 
     def __step_a(self):
+        """
+        > The function takes the labeled samples and trains the classifier on
+        them
+        :return: The samples that have been labeled.
+        """
         samples_labeled = self.structure.loc[self.structure['label'] != -1]
-        l = samples_labeled['sample'].to_list()
+        sam_lab = samples_labeled['sample'].to_list()
         y_without = samples_labeled['label'].to_list()
-        self.classifier.fit(l, y_without)
+        self.classifier.fit(sam_lab, y_without)
         return samples_labeled
 
     def __discover_structure(self):
         self._fit_without()
 
     def __nan_search(self):
+        """
+        For each point, find the set of points that are within a distance of r,
+        and the set of points that are within a distance of r+1.
+
+        The set of points that are within a distance of r+1 is a superset of the
+        set of points that are within a distance of r.
+
+        The set of points that are within a distance of r+1 is also a superset
+        of the set of points that are within a distance of r+2.
+
+        The set of points that are within a distance of r+2 is also a superset
+        of the set of points that are within a distance of r+3.
+
+        And so on.
+
+        The set of points that are within a distance of r+1 is also a superset
+        of the set of points that are within a distance of r+2.
+
+        The set of points that are within a distance of r+2 is
+        :return: nan, r
+        """
         r = 1
         nan = defaultdict(set)
         nb = dict.fromkeys(range(self.n_id), 0)
@@ -229,6 +270,24 @@ def __nan_search(self):
         return nan, r
 
     def __enane(self, fx, nan, r):
+        """
+        > The function takes in the dataframe, the list of indices of the
+        unlabeled data, the list of indices of the neighbors of the unlabeled
+        data, and the number of neighbors to use in the KNN classifier. It
+        then creates a new dataframe with the labeled data and the unlabeled
+        data, and uses the KNN classifier to predict the labels of the
+        unlabeled data. It then checks if the predicted label is the same as
+        the label of the majority of the neighbors of the unlabeled data. If
+        it is, then it adds the index of the unlabeled data to the list of
+        indices of the data to be labeled
+
+        :param fx: the indexes of the unlabeled data
+        :param nan: a list of lists, where each list contains the indices of the
+        neighbors of a sample
+        :param r: the number of neighbors to consider
+        :return: The indexes of the samples that are going to be labeled and the
+        labels that are going to be assigned to them.
+        """
         es = []
         es_pred = []
         local_structure = self.structure_stdnpf.copy(deep=True)
@@ -262,6 +321,17 @@ def __enane(self, fx, nan, r):
         return es, es_pred
 
     def __init_values(self, l, u, y):
+        """
+        It takes in the lower and upper bounds of the data, and the data itself,
+         and then calculates the distances between the data points,
+         the maximum distance, the minimum distance, the dc value, the rho
+         value, the delta value, the number of neighbors, and the structure
+         of the data
+
+        :param l: lower bound of the data
+        :param u: upper bound of the data
+        :param y: the labels of the data
+        """
         self.y = y
         self.l = l
         self.u = u
@@ -274,44 +344,30 @@ def __init_values(self, l, u, y):
         self.__structure()
 
     def _fit_without(self):
+        """
+        The function takes in a classifier, and then labels the next point,
+        and then labels the previous points, without filtering.
+        """
         if self.classifier is None:
             self.classifier = SVC()
         count = 1
         self.order = dict.fromkeys(range(self.n_id), 0)
 
-        # Step 2
-        while True:
-            # 2.a
-            samples_labeled = self.__step_a()
-
-            # 2.b
-            next_rows = samples_labeled['next'].to_numpy()
-            next_unlabeled = []
-            samples_labeled_index = samples_labeled.index.to_list()
-            for next_row in next_rows:
-                if next_row not in samples_labeled_index:
-                    next_unlabeled.append(next_row)
-                    self.order[next_row] = count
-            if len(next_unlabeled) == 0:
-                break
-            unlabeled_next_of_labeled = self.structure.loc[next_unlabeled]
-
-            lu = unlabeled_next_of_labeled['sample'].to_list()
-            y_pred = self.classifier.predict(lu)
+        count = self._label_next_point(count)
 
-            # 2.c
-            for new_label, pos in zip(y_pred, next_unlabeled):
-                self.structure.at[pos, 'label'] = new_label
+        self._label_previous_points(count)
 
-            # For STDPNF
-            count += 1
+    def _label_previous_points(self, count):
+        """
+        > The function takes the samples labeled in the previous step and finds
+         the previous samples of those samples. It then labels those samples
+         and repeats the process until there are no more samples to label
 
-        # Step 3
+        :param count: the number of the current iteration
+        """
         while True:
-            # 3.a
             samples_labeled = self.__step_a()
 
-            # 3.b
             prev_rows = samples_labeled['previous'].to_numpy()
             prev_unlabeled = []
             samples_labeled_index = samples_labeled.index.to_list()
@@ -327,13 +383,43 @@ def _fit_without(self):
             lu = unlabeled_prev_of_labeled['sample'].to_list()
             y_pred = self.classifier.predict(lu)
 
-            # 3.c
             for new_label, pos in zip(y_pred, prev_unlabeled):
                 self.structure.at[pos, 'label'] = new_label
 
-            # For STDPNF
             count += 1
 
+    def _label_next_point(self, count):
+        """
+        > The function takes the samples labeled in the previous step and finds
+         the next samples in the structure. If the next samples are not
+         labeled, it labels them and updates the order of the samples
+
+        :param count: the number of the next point to be labeled
+        :return: The number of labeled samples.
+        """
+        while True:
+            samples_labeled = self.__step_a()
+
+            next_rows = samples_labeled['next'].to_numpy()
+            next_unlabeled = []
+            samples_labeled_index = samples_labeled.index.to_list()
+            for next_row in next_rows:
+                if next_row not in samples_labeled_index:
+                    next_unlabeled.append(next_row)
+                    self.order[next_row] = count
+            if len(next_unlabeled) == 0:
+                break
+            unlabeled_next_of_labeled = self.structure.loc[next_unlabeled]
+
+            lu = unlabeled_next_of_labeled['sample'].to_list()
+            y_pred = self.classifier.predict(lu)
+
+            for new_label, pos in zip(y_pred, next_unlabeled):
+                self.structure.at[pos, 'label'] = new_label
+
+            count += 1
+        return count
+
     def _fit_stdpnf(self):
         """
         Self Training based on Density Peaks and a parameter-free noise
@@ -420,4 +506,12 @@ def fit(self, samples, y):
             self._fit_without()
 
     def predict(self, src):
+        """
+        Predict based on a trained classifier.
+
+        :param src: The source image
+        :return: The classifier is being returned.
+        """
+        if self.classifier is None:
+            raise AssertionError("The model needs to be fitted first.")
         return self.classifier.predict(src)
diff --git a/semisupervised/TriTraining.py b/semisupervised/TriTraining.py
index d29919a..51a6837 100644
--- a/semisupervised/TriTraining.py
+++ b/semisupervised/TriTraining.py
@@ -26,16 +26,41 @@ def measure_error(classifier_j, classifier_k, labeled_data):
 
 
 class TriTraining:
-    """Zhou, Z. H., & Li, M. (2005). Tri-training: Exploiting unlabeled data
-        using three classifiers. IEEE Transactions on knowledge and Data
-        Engineering, 17(11), 1529-1541.
+    """
+    Zhou, Z. H., & Li, M. (2005). Tri-training: Exploiting unlabeled data
+    using three classifiers. IEEE Transactions on knowledge and Data
+    Engineering, 17(11), 1529-1541.
+
+    Parameters
+    ----------
+    random_state : int, default=None
+        The random seed used to initialize the classifiers
+
+    c1 : base_estimator, default=KNeighborsClassifier
+        The first classifier to be used
+
+    c1_params : dict, default=None
+        Parameters for the first classifier
+
+    c2 : base_estimator, default=DecisionTreeClassifier
+        The second classifier to be used
+
+    c2_params : dict, default=None
+        Parameters for the second classifier
+
+    c3 : base_estimator, default=RandomForestClassifier
+        The third classifier to be used
+
+    c3_params : dict, default=None
+        Parameters for the third classifier
+
     """
 
     def __init__(self, random_state=None,
                  c1=None, c1_params=None,
                  c2=None, c2_params=None,
                  c3=None, c3_params=None):
-
+        """Tri-Training."""
         classifiers = [c1, c2, c3]
         classifiers_params = [c1_params, c2_params, c3_params]
         default_classifiers = [KNeighborsClassifier, DecisionTreeClassifier,
@@ -55,7 +80,7 @@ def __init__(self, random_state=None,
         self.random_state = random_state if random_state is not None else \
             np.random.randint(low=0, high=10e5, size=1)[0]
 
-    def subsample(self, l_t, s):
+    def _subsample(self, l_t, s):
         np.random.seed(self.random_state)
         rng = np.random.default_rng()
         data = np.array(l_t['data'])
@@ -119,8 +144,8 @@ def fit(self, samples, y):
                     if e_j * len(l_j['data']) < ep_j * lp_j:
                         update_j = True
                     elif lp_j > e_j / (ep_j - e_j):
-                        l_j = self.subsample(l_j, ceil(((ep_j * lp_j) / e_j)
-                                                       - 1))
+                        l_j = self._subsample(l_j, ceil(((ep_j * lp_j) / e_j)
+                                                        - 1))
                         update_j = True
 
             update_k = False
@@ -146,8 +171,8 @@ def fit(self, samples, y):
                     if e_k * len(l_k['data']) < ep_k * lp_k:
                         update_k = True
                     elif lp_k > e_k / (ep_k - e_k):
-                        l_k = self.subsample(l_k, ceil(((ep_k * lp_k) / e_k)
-                                                       - 1))
+                        l_k = self._subsample(l_k, ceil(((ep_k * lp_k) / e_k)
+                                                        - 1))
                         update_k = True
 
             update_i = False
@@ -173,8 +198,8 @@ def fit(self, samples, y):
                     if e_i * len(l_i['data']) < ep_i * lp_i:
                         update_i = True
                     elif lp_i > e_i / (ep_i - e_i):
-                        l_i = self.subsample(l_i, ceil(((ep_i * lp_i) / e_i)
-                                                       - 1))
+                        l_i = self._subsample(l_i, ceil(((ep_i * lp_i) / e_i)
+                                                        - 1))
                         update_i = True
 
             if update_j:
diff --git a/semisupervised/ensemble/_RESSEL.py b/semisupervised/ensemble/_RESSEL.py
index c3d4057..6308529 100644
--- a/semisupervised/ensemble/_RESSEL.py
+++ b/semisupervised/ensemble/_RESSEL.py
@@ -13,16 +13,54 @@ class RESSEL:
     """
     de Vries, S., & Thierens, D. (2021). A reliable ensemble based approach
     to semi-supervised learning. Knowledge-Based Systems, 215, 106738.
+
+    Parameters
+    ----------
+    n : int, default=2
+        Batch size. Number of samples to take from the labeled ones.
+
+    m : int, default=5
+        Number of iterations for the self-training method.
+
+    k : int, default=25
+        The times the base estimator is duplicated.
+
+    unlabeled_sample_frac : float, default=0.75
+        Fraction of unlabeled data sampled.
+
+    random_state : int or list, default=None
+        Controls the randomness of the estimator.
+
+    reuse_samples : boolean, default=True
+        If True a base estimator k could label the same sample in different
+        iterations. If False, the labeled samples will be removed from the
+        unlabeled samples set.
     """
 
-    def __init__(self, n=10, m=10, k=8, unlabeled_sample_frac=0.75,
-                 random_state=42, reuse_samples=True):
+    def __init__(self, n=2, m=5, k=25, unlabeled_sample_frac=0.75,
+                 random_state=None, reuse_samples=True):
+        """
+        Class constructor.
+
+        :param n: The number of classifiers in the ensemble, defaults to 2
+        (optional)
+        :param m: number of base classifiers in the ensemble, defaults to 5
+        (optional)
+        :param k: The number of samples to draw from the training set, defaults
+        to 25 (optional)
+        :param unlabeled_sample_frac: The fraction of the training data that
+        will be used as unlabeled data
+        :param random_state: The random seed to use for sampling
+        :param reuse_samples: If True, the same samples will be used for all the
+        classifiers. If False, each classifier will get a new sample, defaults
+        to True (optional)
+        """
         self.n = n
         self.m = m
         self.k = k
         self.unlabeled_sample_frac = unlabeled_sample_frac
         self.random_state = random_state
-        self.reuse_samples = reuse_samples
+        self.reuse_samples_ = reuse_samples
         self.ensemble = []
 
     def fit(self, labeled, unlabeled, base_estimator, estimator_params=None):
@@ -36,22 +74,7 @@ def fit(self, labeled, unlabeled, base_estimator, estimator_params=None):
         :return: the ensemble in case is needed.
         """
 
-        if not isinstance(labeled, pd.DataFrame):
-            raise AttributeError("Labeled samples object needs to be a "
-                                 "Pandas DataFrame. Not a ", type(labeled))
-
-        if not isinstance(unlabeled, pd.DataFrame):
-            raise AttributeError("Unlabeled samples object needs to be a "
-                                 "Pandas DataFrame. Not a ",
-                                 type(unlabeled))
-
-        if labeled.shape[1] != unlabeled.shape[1] + 1:
-            raise ValueError("Labeled samples must have one more attribute "
-                             "than the unlabeled ones.",
-                             labeled.shape[1], unlabeled.shape[1])
-
-        if base_estimator is None:
-            raise AttributeError("The base estimator can not be None.")
+        self._validate_params(base_estimator, labeled, unlabeled)
 
         if estimator_params is None:
             for _ in range(self.k):
@@ -91,12 +114,59 @@ def fit(self, labeled, unlabeled, base_estimator, estimator_params=None):
             d_class_i = [x / d_class_i.sum() for x in d_class_i]
 
             self.ensemble[i].fit(l_i.iloc[:, :-1], np.ravel(l_i.iloc[:, -1:]))
-            self.__robust_self_training(i, l_i, u_i, oob_i, d_class_i)
+            self._robust_self_training(i, l_i, u_i, oob_i, d_class_i)
 
         return self.ensemble
 
-    def __robust_self_training(self, iteration, l_i, u_i, oob_i, d_class_i):
-        """Procedure to enrich a given classifier."""
+    def _validate_params(self, base_estimator, labeled, unlabeled):
+        """
+        The function checks if the labeled and unlabeled data are pandas
+         dataframes, if the number of columns in the labeled data is one more
+         than the unlabeled data, and if the base estimator is not None
+
+        :param base_estimator: The base estimator to be used
+        :param labeled: The labeled samples
+        :param unlabeled: The unlabeled samples
+        """
+        if not isinstance(labeled, pd.DataFrame):
+            raise AttributeError("Labeled samples object needs to be a "
+                                 "Pandas DataFrame. Not a ", type(labeled))
+        if not isinstance(unlabeled, pd.DataFrame):
+            raise AttributeError("Unlabeled samples object needs to be a "
+                                 "Pandas DataFrame. Not a ",
+                                 type(unlabeled))
+        if labeled.shape[1] != unlabeled.shape[1] + 1:
+            raise ValueError("Labeled samples must have one more attribute "
+                             "than the unlabeled ones.",
+                             labeled.shape[1], unlabeled.shape[1])
+        if base_estimator is None:
+            raise AttributeError("The base estimator can not be None.")
+        if not isinstance(self.random_state, int) and \
+                not hasattr(self.random_state, '__iter__') and \
+                not isinstance(self.random_state, list):
+            raise AttributeError("The random state must be an integer, "
+                                 "iterable or list. Not "
+                                 f"{type(self.random_state)}")
+
+    def _robust_self_training(self, iteration, l_i, u_i, oob_i, d_class_i):
+        """
+        > The function takes in the current iteration, the labeled data, the
+        unlabeled data, the out-of-bag data, and the distribution of classes in
+         the unlabeled data. It then iterates through the unlabeled data and
+         selects the samples that have the highest probability of belonging
+         to the class with the highest proportion of samples in the unlabeled
+         data. It then adds these samples to the labeled data and retrains
+         the model. It then checks the performance of the model on the
+         out-of-bag data and if the performance is better than the previous
+         iteration, it keeps the model
+
+        :param iteration: the current iteration of the ensemble
+        :param l_i: the labeled data for the current classifier
+        :param u_i: the unlabeled data
+        :param oob_i: the out-of-bag samples for the current classifier
+        :param d_class_i: the proportion of samples to be selected from each
+        class
+        """
 
         y_pred = self.ensemble[iteration].predict(oob_i.iloc[:, :-1])
         best_error_i = f1_score(y_true=np.ravel(oob_i.iloc[:, -1:]),
@@ -137,16 +207,7 @@ def __robust_self_training(self, iteration, l_i, u_i, oob_i, d_class_i):
 
             l_i = pd.concat([l_i, samples_u_best], ignore_index=True, axis=0)
 
-            if not self.reuse_samples:
-                indexes = []
-                for _, sample in samples_u_best.iterrows():
-                    sample = sample.to_numpy()[:-1]
-                    for index, sample_u in u_i.iterrows():
-                        if np.array_equal(sample, sample_u.to_numpy()):
-                            indexes.append(index)
-                            break
-
-                u_i = u_i.drop(index=indexes)
+            u_i = self._reuse_samples(samples_u_best, u_i)
 
             self.ensemble[iteration].fit(l_i.iloc[:, :-1],
                                          np.ravel(l_i.iloc[:, -1:]))
@@ -161,6 +222,28 @@ def __robust_self_training(self, iteration, l_i, u_i, oob_i, d_class_i):
 
         self.ensemble[iteration] = best_c_i
 
+    def _reuse_samples(self, samples_u_best, u_i):
+        """
+        > If the user has not specified that they want to reuse samples, then we
+         will remove any samples from the new set of samples that are already in
+         the best set of samples
+
+        :param samples_u_best: the best samples from the previous iteration
+        :param u_i: The samples that are not yet labeled
+        :return: the best sample from the set of samples.
+        """
+        if not self.reuse_samples_:
+            indexes = []
+            for _, sample in samples_u_best.iterrows():
+                sample = sample.to_numpy()[:-1]
+                for index, sample_u in u_i.iterrows():
+                    if np.array_equal(sample, sample_u.to_numpy()):
+                        indexes.append(index)
+                        break
+
+            u_i = u_i.drop(index=indexes)
+        return u_i
+
     def predict(self, samples):
         """
         Predict using the trained ensemble with a majority vote.
diff --git a/semisupervised/ensemble/__init__.py b/semisupervised/ensemble/__init__.py
index 8dbd915..903f7a3 100644
--- a/semisupervised/ensemble/__init__.py
+++ b/semisupervised/ensemble/__init__.py
@@ -1,8 +1,4 @@
-#!/usr/bin/env python
-# -*- coding:utf-8 -*-
-# @Filename:    __init__.py.py
-# @Author:      Daniel Puente Ramírez
-# @Time:        27/4/22 10:41
+"""Ensemble algorithms."""
 
 from ._RESSEL import RESSEL
 
diff --git a/semisupervised/utils/__init__.py b/semisupervised/utils/__init__.py
index 49b973a..4403bf8 100644
--- a/semisupervised/utils/__init__.py
+++ b/semisupervised/utils/__init__.py
@@ -1,3 +1,4 @@
+"""Utils methods used by SemiSupervised Algorithms."""
 from ._split import split
 
 __all__ = [
diff --git a/semisupervised/utils/_split.py b/semisupervised/utils/_split.py
index 1d84b99..443c8fd 100644
--- a/semisupervised/utils/_split.py
+++ b/semisupervised/utils/_split.py
@@ -9,18 +9,15 @@
 
 
 def split(samples, y):
-    """Split X, y into Labeled, Unlabeled and the y real tags.
-
-    Arguments:
-        X {DataFrame} -- samples
-        y {DataFrame} -- labeles
-
-    Returns:
-        L {Numpy array} -- labeled samples
-        U {Numpy array} -- unlabeled samples
-        y {Numpy array} -- real labels
     """
+    It takes a dataframe of samples and a dataframe of labels, and returns a
+    tuple of three numpy arrays: the labeled samples, the unlabeled samples,
+    and the labels
 
+    :param samples: the dataframe of samples
+    :param y: the labels of the data
+    :return: L, U, y
+    """
     if isinstance(y, pd.DataFrame):
         y = y.to_numpy()
 
diff --git a/utils/__init__.py b/utils/__init__.py
index 164c2a4..b0770da 100644
--- a/utils/__init__.py
+++ b/utils/__init__.py
@@ -1,8 +1,4 @@
-#!/usr/bin/env python
-# -*- coding:utf-8 -*-
-# @Filename:    __init__.py.py
-# @Author:      Daniel Puente Ramírez
-# @Time:        22/12/21 18:05
+"""Utils ARFF"""
 
 from .arff2dataset import arff_data
 


Test patch:
diff --git a/tests/__init__.py b/tests/__init__.py
index 7ee9a42..1d62cdb 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -1,7 +1,1 @@
-#!/usr/bin/env python
-# -*- coding:utf-8 -*-
-# @Filename:    __init__.py
-# @Author:      Daniel Puente Ramírez
-# @Time:        15/4/22 16:19
-
-"""Python module for testing"""
+"""Python module for testing."""