#!/usr/bin/env python
# coding: utf-8
# # *Proyecto Final Data Science*
# **-------------------------------**
# Dataset con datos del 2002 -> 2022
# Fuente: [Evolucion de Telefonos](https://www.kaggle.com/datasets/pranav941/evolution-of-smartphones)
# **-------------------------------**
# Estudiante: **Righes Marcos**
#
# # **Objetivos e información general del proyecto**
# El dataset utilizado toma los datos generales de telefonos desde 2002 hasta 2022
# El objetivo principal de este proyecto es poder **encontrar** caracteristicas (componentes) similares entre estos telefonos y agruparlos para **categorizar** de esta manera a las marcas y sus productos como competencias / alternativas de compra para los usuarios.
#
# Este proyecto pude ser de interés tanto a nivel usuario / consumidor como para nivel empresarial.
#
# Usuario ya que podremos ver las alternativas a modelos de ínteres personal, sabiendo que muchas veces los precios entre un producto y otro cambian por el nombre de quien lo vende, a pesar de que los componentes y calidad sean practicamente iguales.
#
# Empresarial ya que permite a las marcas saber quienes son su verdadera competencia segun los objetivos de producto que tengan, por otro lado se podra sacar conclusiones de que es lo que hace especial o popular a una marca u otra, permitiendo asi tomar conclusiones y respectivas acciones para llegar a igualar o superar a una marca objetivo.
#
# |Variables|Descripcion|Medida Expresada|
# |--|--|--|
# |Brand|Nombre de la marca del teléfono|--|
# |Model|Nombre identicatorio del teléfono|--|
# |OS|Sistema operativo de fabrica|--|
# |Release_Date|Fecha de lanzamiento al mercado|month day, year|
# |Battery|Capacidad de energía|mA|
# |Processor|Procesador Incorporado|--|
# |Memory|Capacidad de memoria RAM|GB|
# |Primary_Storage|Capacidad de Almacenamiento Integrado|GB|
# |External_Storage|Tipo de almacenamiento externo compatible|--|
# |Display_Size|Tamaño de pantalla principal|inch (pulgadas)|
# |Display_Resolution|Resolucion de la pantalla|pixels x pixels|
# |Display_Refresh_Rate|Tasa de refresco de la pantalla|Hz|
# |Primary_Camera|Calidad de la camara frontal|Mpx|
# |Front_Camera|Calidad de la cámara trasera|Mpx|
#
# Contamos para nuestras comparativas, con la participacion de 113 Marcas de telefonos, sin embargo no todas las marcas presentan todas las caracteristicas de sus celulares, por lo que las comparativas entre los mismos estarian incompletas si se usara los datos asociadas a estas marcas, por lo que realizando una limpieza solo 71 de ellas se utilizaron para el analisis
# Todo el dataset fue obtenido por una recopilacion colaborativa en Kaggle (Pagina web)
# # Librerias
# In[ ]:
#Importacion librerias
import numpy as np
import pandas as pd
import missingno as msno
import matplotlib.pyplot as plt
import plotly.express as px
import seaborn as sns
from sklearn.preprocessing import LabelEncoder
from sklearn.cluster import KMeans
from sklearn.metrics import silhouette_score
# In[ ]:
#Dataframe de Telefonos
df=pd.read_csv("https://docs.google.com/spreadsheets/d/e/2PACX-1vQhiKIWZRZIWB_D3qPJQuHurMHWDA-3mLEABP8vylmrGRrulkE40PE_BsVbESWcG1rlwCkuJMuWv14o/pub?output=csv")
# # Tratamiento de Nulos y transformacion de valores categoricos a numéricos
# In[ ]:
#Visualizacion del Dataframe inicial
df.head(5)
# In[ ]:
#Revisamos tipo de datos que tenemos y la cantidad de datos potenciales
df.info()
# In[ ]:
#Vemos la cantidad de nulos x columna
df.isnull().sum()
# In[ ]:
#Para tener una mejor visualizacion, se realiza un grafico de nulos sobre el dataset
msno.bar(df)
# In[ ]:
#Teniendo en cuenta que se busca caracterizar por los componentes físicos y por la falta de datos del mas del 40% para algunas columnas,se decide eliminar columnas que se consideran innecesarias para el trabajo
df.drop(columns=['Release_Date','Display_Refresh_Rate',],inplace=True)
# In[ ]:
#Reemplazo de valores incoherentes en la columna Front Camara
df.Front_Camera.replace(to_replace=["QCIF (176x144)","0.1 MP CIF (288x352)"], value=[0.3,0.1],inplace=True)
df.Front_Camera=df.Front_Camera.astype(float)
# In[ ]:
#Tratamiento de nulos para columnas NUMERICAS y relleno con valores medios de la tabla
for x in df.columns:
if df[f'{x}'].isnull().sum() != 0:
if df[f'{x}'].dtypes != 'O':
mean=df[f'{x}'].mean()
df[f'{x}'].fillna(mean,inplace=True)
# In[ ]:
#Los nulos pendientes se relacionan a columnas CATEGORICAS / TEXTO
df.isnull().sum()
# In[ ]:
#Debido a que los valores faltantes presentan caracteristicas importantes, se los dropea y no rellena
df.dropna(inplace=True)
df_text=pd.DataFrame.copy(df) #Se crea una copia del dataframe, pero manteniendo las columnas sin encoding para referencias futuras y comparaciones
df.drop(columns='Model')#Se elimina la columna de Modelo, la cual solo se mantuvo para realizar una copia en otro DataFrame
# In[ ]:
#Encoding de las columnas categóricas Brand-Processor-Storage-DisplayRes
encoder=LabelEncoder()
df.Brand=encoder.fit_transform(df.Brand)
df.Processor=encoder.fit_transform(df.Processor)
df.External_Storage=encoder.fit_transform(df.External_Storage)
df.Display_Resolution=encoder.fit_transform(df.Display_Resolution)
# In[ ]:
#De esta manera ahora tenemos un dataframe completamente numérico y tratado, listo para ser utilizado
df.drop(columns='OS',inplace=True)
# In[ ]:
#Dataframe con las columnas categoricas transformadas
df.head(5)
# In[ ]:
#DataFrame sin el encoding
df_text.head(5)
# #Exploracion de Datos y correlaciones
# En primera instancia buscamos caracteristicas que resalten en los datos para cada variable.
#
# In[ ]:
#Tomamos las columnas numéricas para analizar sus comportamientos
df_num=df.drop(columns=['Model','Brand','Processor','External_Storage','Display_Resolution'])
df_num.describe()
# In[ ]:
#Graficamos los histogramas para cada variable
df_num.hist(layout=[2,3],figsize=[15,5])
plt.show()
# Teniendo en cuenta que todas las variables son numéricas de tipo continua, podemos visualizar la distribucion de frecuencias por medio de intervalos en el grafico.
# Realizado los histogramas para cada columna numérica del Dataframe, podemos visualizar mejor el comportamiento de cada variable con respecto a sus datos.
# Las frecuencias mas distribuidas podemos ver que son la de batería y el tamaño display.
#
# Tambien se visualiza la aparicion de valores extremos / atipicos en los datos, seguramente pertenecientes a modelos mas alta gama dentro del set de datos.
# Se continua con un mapa de correlaciones lineales entre variables y se toman algunas variables para comparar.
# In[ ]:
#Realizamos un mapa de correlacion
#Se crea una mascara para ocultar los valores espejo del grafico y asi tener mas limpio la visualizacion
mask= np.triu(df.corr())
sns.heatmap(df.corr().round(2),annot=True,mask=mask)
plt.title('Mapa de Correlacion Lineal')
# Relaciones interesantes que obtenemos:
#
# |Componentes|Correlacion Lineal|Intensidad de Correlacion|
# |--|--|--|
# |Display Size x Battery|r = 0.84|Directa Alta|
# |Front Camera x Primary Camera | r = 0.72 |Directa Alta|
#
# Los dos valores mas alto de correlacion lineal que obtuvimos
# # Insights
#
# ## Correlaciones -- Grafico y analisis
# ## Camara Trasera X Camara Frontal
# Unas de las observaciones generales que se obtuvo, es la correlacion lineal entre la 'Calidad' / Pixeles de una camara trasera en relacion a la camara frontal, siendo esta ultima en su mayoria inferior o igual a la otra.
#
# Solo para recordar, el coeficiente de correlacion lineal de este caso es: 0.72
# In[ ]:
sns.relplot(data=df,
x='Front_Camera',
y="Primary_Camera",
kind="line"
)
sns.set_style("whitegrid")
plt.title("Camara Trasera/Camara Frontal")
plt.xlabel("Camara Frontal px")
plt.ylabel("Camara Trasera px")
plt.show()
# Teniendo una visualizacion grafica de los valores, ya podemos ver un cierto comportamiento entre las variables de analisis.
#
# Algo interesante es que por lo general la camara trasera, presenta mayor calidad que la frontal.
#
# En la mayoria del tiempo, sacamos fotos a elementos externos a nosotros, por lo que es lógico que se haga incapie en obtener una mejor resolucion en las fotografias que sean hacia nuestro entorno y en menor medida para las fotos frontales.
# ## Batería X Display
# Siguiendo la dinámica de las observaciones anteriores, una de las relaciones mas fuertes de los componentes, es la capacidad de las baterias y el tamaño del display/pantalla.
# Lo cual es bastante logico, debido a la alta demanda de consumo energetico que presenta tener constantemente prendida la pantalla y la actualizacion de los pixeles.
# In[ ]:
Bateria=df.Battery
Display=df.Display_Size
sns.regplot(x=Bateria,
y=Display,
line_kws={'color': 'black'})
plt.title('Relacion Display X Bateria')
plt.xlabel('Bateria mA')
plt.ylabel('Display px')
plt.show()
# Teniendo esta grafica, la cual presenta un diagrama de dispersion y su recta regresora, podemos concluir visualmente, que a mayor cantidad de pixeles en el display, requerira una mayor capacidad de batería.
#
# Cabe aclarar que para hacer un analisis aun mas profundo se requeriría tener en cuenta la tecnologia utilizada, pues hoy en dia se ha logrado una mayor calidad y cantidad de pixeles a menor coste energetíco que los primeros dispositivos del mercado.
#
# (Para nuestro caso, no nos interesa, pero nunca esta demás aclarar)
# ## Tops
# ### Sistemas Operativos más presentes
# Una característica importante, es el **sistema operativo** quien se encarga de administrar tanto los recursos del dispositivo como las interacciones entre los distintos componentes
# In[ ]:
#Nuestro Dataframe contiene tanto el nombre del SO como su version, a nosotros solo nos interesa solo el nombre, ademas que nos permitira clasificar mejor.
#Se crea una lista que recorre los valores y los separa por espacios, tomando solo la primera palabra (Nombre del SO)
marcas = list(df_text["OS"])
lista=[]
for i in marcas:
lista.append(str(i).split()[0])
# In[ ]:
#Se reemplaza la columna OS por la nueva lista sin las versiones
df_text.OS=lista
# In[ ]:
#Visualizacion de como quedó
df_text.head(5)
# In[ ]:
px.bar(df_text.OS.value_counts().head(5),title="Top 5 Sistemas Operativos más usados",labels={'value':'Cantidad de telefonos',
'index':'Sistemas Operativos',
'variable':''})
# |Android|Windows|iOS|BlackBerry|Firefox|
# |--|--|--|--|--|
# |2612|149|27|11|5|
# Con una simple visualizacion, podemos ver el interés de los fabricantes por utilizar Android.
# Cabe aclarar que dentro de las alternativas, Android al ser un SO abierto,permitir adecuaciones personalizadas y de la mano de una gran comunidad, es la mejor opcion para muchos desarrolladores.
# Caso diferente para los sistemas como Windows e iOS que ya dependen de una licencia para ser utilizados, ademas que suelen ser solo para uso exclusivo de sus fabricantes (iOS-Apple)
# Aclarado esto, es entendible la gran diferencia de dispositivos que cuentan con estos SO
# ### Marcas con mayor aportes
# Como casi todo en la vida, las grandes marcas suelen ser quienes producen la gran mayoria de los productos que consumimos, por lo que es importante saber quienes han sido las marcas mas 'Aportantes' en cuestiones de datos en nuestro dataset
# In[ ]:
#Agrupamos los datos por marca y los ordenamos de mayor a menor según la cantidad de datos/dispositivos que presenten en este dataset
top_10_marcas=df_text.groupby(by='Brand').size().sort_values(ascending=False).head(10)
# In[ ]:
fig, ax = plt.subplots()
barras = ax.barh(top_10_marcas.index, top_10_marcas.values)
plt.title('Marca de Telefono x Cantidad')
ax.bar_label(barras) #Muestra la cantidad acumulada de valores de la categoria
plt.show()
# # Proceso de KMeans y hallazgos de grupos
# ## Clusterizacion
# Ahora teniendo la informacion general y bien visualizada acerca de nuestro dataset, se continua con el objetivo principal de nuestro proyecto
# In[ ]:
df_num
# In[ ]:
#Utilizamos el método del codo para encontrar la cantidad de agrupaciones optimas para clasificar a nuestro dataset
inercia = []
K = range(1,10)
for clusters in K :
kmeans = KMeans(n_clusters=clusters)
kmeans.fit(df_num)
inercia.append(kmeans.inertia_)
plt.plot(K,inercia)
plt.title('Método del codo')
plt.xlabel('Valor K')
plt.ylabel('Inercia')
plt.show()
# ::En esta caso elegimos 3 agrupaciones por observacion del codo y tambien por tratar de segmentar un poco más a los datos
# In[ ]:
df_text
# In[ ]:
#Entrenamiento y asignacion de los grupos encontrados con sus respectivos valores
kmeans=KMeans(n_clusters=3,random_state=42)
kmeans.fit_transform(df_num)
df_text['Cluster']=kmeans.labels_
# In[ ]:
df_cluster=df_text.groupby(['Cluster','Brand']).mean().round(2).reset_index()
# In[ ]:
print(f'Cantidad de Marcas entrantes en cluster 0 =',len(df_cluster.where(df_cluster.Cluster==0).dropna().Brand.unique()))
print(f'Cantidad de Marcas entrantes en cluster 1 =',len(df_cluster.where(df_cluster.Cluster==1).dropna().Brand.unique()))
print(f'Cantidad de Marcas entrantes en cluster 2 =',len(df_cluster.where(df_cluster.Cluster==2).dropna().Brand.unique()))
# In[ ]:
# ## Cluster 0
# In[ ]:
df_cluster.where(df_cluster.Cluster==0).dropna().drop(columns=['Brand','Cluster']).hist(layout=[3,3],figsize=[18,10])
plt.show()
# In[ ]:
df_cluster.where(df_cluster.Cluster==0).dropna().drop(columns=['Brand','Cluster']).describe().T
# ## Cluster 1
# In[ ]:
df_cluster.where(df_cluster.Cluster==1).dropna().drop(columns=['Brand','Cluster']).hist(layout=[3,3],figsize=[18,10])
plt.show()
# In[ ]:
df_cluster.where(df_cluster.Cluster==1).dropna().drop(columns=['Brand','Cluster']).describe().T
# ## Cluster 2
# In[ ]:
df_cluster.where(df_cluster.Cluster==1).dropna().drop(columns=['Brand','Cluster']).hist(layout=[3,3],figsize=[18,10])
plt.show()
# In[ ]:
df_cluster.where(df_cluster.Cluster==2).dropna().drop(columns=['Brand','Cluster']).describe().T
# ## Conclusion del Clustering
# Con esta Clasificacion, cuando vemos los promedios de los valores, podemos ver que nos llega a clasificar en telefonos con características que podriamos decir que son
# de baja, media y alta gama, siendo casi todos los componentes de valores bastante diferente entre cada agrupacion, aunque observando bien, podemos visualizar que hay 2 componentes que presentan
# practicamente la misma característica en todos los agrupamientos.
# Estos son la **Memoria**, el tamaño de **Display**.
#
# Por lo que podemos decir que:
#
# Cluster 0 agrupa a los telefono de Gama Baja
# Cluster 1 agrupa a los telefono de Gama Alta
# Cluster 2 agrupa a los telefono de Gama Media
# # Evaluacion ML
#
# Proponiendo otra visualizacion ahora enfocada en la comparacion entre bateria y memoria, se hace una primera impresion sin realizar un tratado de la información
# In[ ]:
sns.scatterplot(x=df.Battery,y=df.Memory)
# Se procede a realizar un PCA para disminuir las columnas del df tratando de mantener la mayor informacion posible que describa al conjunto y se aplica un StandarScaler
# In[ ]:
from sklearn.decomposition import PCA
from sklearn.preprocessing import StandardScaler
# In[ ]:
df.drop(columns='Model',inplace=True)
# In[ ]:
#Se aplican PCA y la varianza ratio para ver la importancia de las columnas con respecto a los datos
for i in range(0,6):
print(i)
print(PCA(i,random_state=0).fit(df).explained_variance_ratio_.round(3))
i+=i
# Se decide tomar del 0 al 5 como columnas para representar el 0.99 de los datos del comienzo
# In[ ]:
df_standar=StandardScaler().fit_transform(df)
# In[ ]:
df_standar
# In[ ]:
standar=StandardScaler()
standar.fit_transform(df)
df_pca=PCA(n_components=5,random_state=0).fit_transform(df)
# In[ ]:
df_standar=pd.DataFrame(data=df_pca,columns=['Brand','Battery','Processor','Memory','Primary_Storage'])
# Una vez realizado el proceso de transformación y adecuacion de los datos, nuevamente mostramos los datos
# In[ ]:
sns.scatterplot(x=df.Battery,y=df.Memory)
# Sin la necesidad de realizar una clusterizacion, podemos visualizar como hay un comportamiento de densidad en la seccion inferior del gráfico, lo que podria indicar que hay un comportamiento radial o de rango aceptable para valores entre la memoria y bateria que mantienen las empresas telefónicas.
# Uno de los aspectos importantes que trae esta nueva visualizacion, es que no es tan lineal como la inicial, dando a entender que hay un mayor dinamica que del que se cree a simple vista
# En conclusion, por medio de este tratamiento alternativo de los datos, se podria visualizar los datos sin la necesidad de producir algoritmos de clustering
# # Feature Selection - Agrupacion con menos columnas
# In[ ]:
from sklearn.preprocessing import RobustScaler
# In[ ]:
df_2=df.drop(columns=['Primary_Camera','Front_Camera','External_Storage'])
df_2
# In[ ]:
df_standar=RobustScaler().fit_transform(df_2)
# In[ ]:
#Se toma 4 debido a que ya se consigue un 0.99 de recreacion de los datos
for i in range(0,6):
print(i)
print(PCA(i,random_state=0).fit(df_standar).explained_variance_ratio_.round(3).sum())
i+=i
# In[ ]:
df_standar_2=PCA(4,random_state=0).fit_transform(df_standar)
# Realizo el metodo de la silueta para encontrar la cantidad adecuada de Agrupaciones
# In[ ]:
silhouette=[]
for i in range(2,10):
kmeans=KMeans(n_clusters=i)
kmeans.fit_transform(df_standar_2)
silhouette.append(silhouette_score(df_standar_2,kmeans.labels_))
# In[ ]:
plt.plot(range(2,10),silhouette)
# igual que en el caso principal del desafio, tenemos que con 3 clusters, ya podemos separar de forma prolija al Dataframe
# In[ ]:
kmeans=KMeans(n_clusters=3)
kmeans.fit_transform(df_standar_2)
# In[ ]:
df_2['Cluster']=kmeans.labels_
df_2.Cluster.value_counts()
# Utilizando menos columnas de las iniciales, siendo los componentes de la camara y el almacenamiento externo como datos sin uso, podemos visualizar en la segmentacion, que no se ha logrado dividir de una forma mas equilibrada al Dataframe.
# In[ ]:
#Se realiza otro modo de clustering (Agrupacion jerarquica)
from scipy.cluster.hierarchy import linkage, dendrogram
from scipy.spatial.distance import pdist
link = linkage(df_standar_2, metric = 'euclidean', method = 'ward')
plt.figure(figsize = (15,6))
plt.title('Agglomerative Hierarchical Clustring Dendrogram')
plt.xlabel('Grupos')
plt.ylabel('Distance')
dendrogram(link)
plt.tight_layout()
# In[ ]:
from sklearn.cluster import AgglomerativeClustering as agc
AGC = agc(n_clusters = 3, affinity = 'euclidean', linkage = 'ward')
AGC.fit(df_standar_2)
Cluster_AGC = pd.Series(AGC.labels_)
print(pd.concat({'count' : Cluster_AGC.value_counts(),
'percent' : round(Cluster_AGC.value_counts(normalize = True)*100, 2)},
axis = 1 ))
# Ya desde un primer vistazo, tenemos que este algoritmo, encontro la misma solucion que el KMeans para agrupar los datos, al menos en cantidad para cada grupo, por lo que ambos Score de comparacion seran identicos
# In[ ]:
from sklearn.metrics import silhouette_score as sil_score
from sklearn.metrics import davies_bouldin_score
print('Silhouette Score:', '%.2f'%sil_score(df_standar_2, df_2['Cluster']))
print('Davies Bouldin Score:', '%.2f'%davies_bouldin_score(df_standar_2, df_2['Cluster']))
# In[ ]:
print('Silhouette Score:', '%.2f'%sil_score(df_standar_2, Cluster_AGC))
print('Davies Bouldin Score:', '%.2f'%davies_bouldin_score(df_standar_2, Cluster_AGC))
# Por lo que se concluye, que esta segmentancion de los datos, aunque parezca dispareja, es la mejor forma que ambos algoritmos presentan para clasificar los datos del comienzo (Recordando que para esta ocasion, se eliminaron columnas importantes para los telefonos como la Cámara)
# # DataFrame Final
# In[ ]:
#Para mayor legibilidad, se reemplaza los valores cluster con la conclusion de las características
df_text.Cluster.replace([0,1,2],['Gama Baja','Gama Alta','Gana Media'],inplace=True)
df_text
# # Conclusion Final - Cierre
# Se ha logrado realizar una clasificacion de los dispositivos en 3 grupos.
# Cabe aclarar que los resultados finales son en base a todo el DataFrame sin hacer distincion de fechas de lanzamientos y los procesadores asociados,
# ya que no se cuenta con los datos asociados.
# Por lo que a final de cuentas, esta presentacion final nos sirve para hacer una rapida comparacion entre dispositivos para la toma de decision con respecto a compra/venta o simplemente de modo informativo.
# Se pudo observar que hay ciertos componentes que respetan una cierta linealidad / correlacion entre ellas (aspecto muy importante si se busca fabricar) y otros que permanecen con valores mas constantes o valores optimos que satisfacen en general la demanda de los usuarios.
# Otra informacion obtenida y de gran importancia, es la importancia de la camara del mismo (al menos en gran medida la trasera).
# Siendo de vital importancia para decidir catalogar un dispositivo como competidor de baja, media o alta gama.