Damián Árquez, Benjamín Hurtado, Matias Villegas
CC5206 - Introducción a la minería de datos
En este proyecto se utilizan dos datasets en conjunto. El primero consta de dos tablas que contienen información sobre el Google Play Store. El segundo contiene una sola tabla con información del App Store de Apple. A continuación se describe la información contenida en cada archivo/tabla:
El dataset de Google Play Store se encuentra en el siguiente link (Kaggle): Dataset Google Play Store.
El dataset de App Store se encuentra en el siguiente link (Kaggle): Dataset AppStore
Hoy en día los smartphones son uno de los dispositivos más utilizados. Debido a esto, existe un gran mercado de aplicaciones para éstos. Las aplicaciones permiten al dispositivo extender su propósito en distintas categorías (entretenimiento, ocio, educación, productividad, etc...). Dada la alta demanda de smartphones, se genera genera un interés en el desarrollo de software para celulares inteligentes por sobre otras plataformas.
Se desea encontrar una caracterización del mercado actual de aplicaciones a partir de Google Play Store, la tienda de aplicaciones por defecto en el sistema Android, a partir de la información de las aplicaciones en sí, además de sus reseñas.
Esta información se presta de utilidad al momento de diseñar aplicaciones nuevas, comprendiendo qué características pueden hacer una aplicación más exitosa que otra, entendiendo por éxito una alta cantidad de descargas y buenas reseñas, esto para construir una aplicación exitosa se necesita hacer un análisis de datos para obtener así cuál sería la categoría y tipo de aplicación que se debe crear.
El objetivo de este proyecto es caracterizar aplicaciones según los distintos atributos que poseen, buscando relaciones interesantes que puedan existir entre ellos como, por ejemplo, la relación entre si una aplicacion es paga y la evaluación promedio de sus reviews.
En esta misma linea, surgen las siguientes preguntas, que probablemente el análisis de este dataset podrá responder:
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import seaborn as sns; sns.set(style="ticks", color_codes=True)
import requests
Para mantener los datos disponibles, se subieron a Amazon S3.
Cargamos los datos de las apps.
apps = pd.read_csv('https://s3.us-east-2.amazonaws.com/cc5206-2018-2/googleplaystore.csv')
reviews = pd.read_csv('https://s3.us-east-2.amazonaws.com/cc5206-2018-2/googleplaystore_user_reviews.csv')
apps.info()
apps.head()
reviews.info()
reviews.head()
En un primer encuentro con los datos no usaremos las columnas Size, Last updated, Current ver y Android ver del dataset apps.
apps = pd.DataFrame(apps, columns=['App', 'Category', 'Rating', 'Reviews', 'Installs', 'Type', 'Price', 'Content Rating', 'Genres'])
Además, notamos que los atributos Installs y Price del dataset Apps son del tipo object y sería mejor para el análisis que fueran valores numéricos. Para esto se realizará un 'Data Cleaning'
El Data Cleaning se realizará en base a lo descrito en Kaggle..
apps.columns = apps.columns.str.replace(' ', '_')
Se procede a ver los valores únicos en la columna Installs.
apps.Installs.value_counts()
Hay que eliminar los "+" y las comas.
apps.Installs = apps.Installs.apply(lambda x: x.strip('+'))
apps.Installs = apps.Installs.apply(lambda x: x.replace(',',''))
apps.Installs = pd.to_numeric(apps.Installs)
apps.Installs.describe()
Se verá los valores únicos de Price con el fin de buscar anormalidades:
apps.Price.unique()
Se debe eliminar el signo "$" y luego convertir la columna a numeric.
apps.Price = apps.Price.apply(lambda p: p.strip('$'))
apps.Price = pd.to_numeric(apps.Price)
apps.Price.describe()
apps.Content_Rating.describe()
Con esto finaliza la limpieza de datos.
apps.Category.value_counts().plot(kind='barh', figsize=(12,8))
Se puede observar que, con una gran ventaja, las aplicaciones clasificadas como familiares son las que más se encuentran en Google Play Store que es en donde se concentran las aplicaciones orientadas para niños pequeños, seguidas por los juegos y aplicaciones con finalidades de herramienta.
¿Son las aplicaciones pagadas mejor evaluadas? Para responder esto se realiza un gráfico de frecuencia vs el rating.
freeapps = apps[apps.Type == 'Free']
paidapps = apps[apps.Type != 'Free']
g = sns.kdeplot(freeapps.Rating, color="Red", shade = True, label='Gratis')
g = sns.kdeplot(paidapps.Rating, color="Blue", shade = True, label='Pagadas')
g.set_xlabel("Rating")
g.set_ylabel("Frecuencia")
plt.title('Distribución de Rating de Apps pagadas vs gratis',size = 20)
freeapps.Rating.describe()
paidapps.Rating.describe()
Lo primero que se puede notar es que la distribución de ratings está cargada sobre el 4. Con esto notamos que la mayoría de la gente suele entregar puntuaciones muy altas, lo que puede deberse a que si un usuario disgusta de una aplicación la elimina en vez de puntuarla negativamente.
Se puede apreciar, tanto en el gráfico como en el cálculo estadístico del promedio, que efectivamente existe un ligero desplazamiento de las distribuciones que evidencia que las aplicaciones pagadas tienen, en promedio, mejor valoración que las aplicaciones gratuitas. Esto puede deberse a que como existe un pago de por medio, el desarrollador es una aplicación paga tiene mayor esmero en su desarrollo, además de que probablemente estas aplicaciones no posean publicidad. Se propone comprobar a partir del dataset de reviews que la publicidad aporta a una calificación negativa.
Se realiza un gráfico de barras de error en el cual se consideran los promedios de evaluación por cada categoría considerando su desviación estándar.
ratings_by_cat = apps.groupby('Category')['Rating'] \
.agg({'mean_rating': 'mean', 'std_rating': 'std'})
plt.errorbar(ratings_by_cat.index.tolist(), ratings_by_cat.mean_rating, \
yerr=ratings_by_cat.std_rating, fmt='o', \
color='red', ecolor='blue')
plt.xticks(rotation='vertical')
plt.show()
El resultado de este experimento muestra que la categoría con mejor evaluación es eventos seguida de educación. Para la categoría con peor calificación se tiene que es citas, lo que puede deberse a que las aplicaciones de este estilo suelen producir decepción o malas experiencias en los usuarios.
¿Cuál es la categoría con mayor cantidad de descargas? El siguiente gráfico muestra las descargas totales por cada categoría.
installs_by_cat = apps.groupby('Category')['Installs']\
.agg({'Installs': 'sum'})
plt.bar(installs_by_cat.index.tolist(), installs_by_cat.Installs)
plt.xticks(rotation='vertical')
plt.title('Cantidad de descargas por Categoria')
plt.ylabel('Cantidad de descargas')
plt.show()
Es importante destacar que este gráfico posee las descargas totales por categoría y NO un promedio. Aquí se muestra que las categorías con más descargas son comunicación y juegos. Ya vimos que la categoría familia posee la mayor cantidad de aplicaciones por sobre el resto de categorías, por lo que se nota que a pesar de la cantidad de aplicaciones presentes en esta categoría, no recibe descargas. Con respecto a nuestro análisis: familia no es una buena categoría para la cual enfocar una aplicación exitosa.
¿Existen aplicaciones con un rating muy bajo pero muchas descargas? El gráfico siguiente muestra la cantidad de reviews según la cantidad de instalaciones.
plt.plot(apps.Installs, apps.Reviews, linestyle="", marker="o")
El gráfico anterior no nos permite obtener información, debido a que en Play Store la cantidad de descargas se definen como más de 100, más de 1000, más de 1.000.000, en vez de entregar cantidades exactas. Debido al crecimiento exponencial de los intervalos no se puede concluir.
¿Se puede predecir la categoría que tendrá una aplicación en base solo a su nombre?
La idea de esto es poder crear una aplicación cuyo nombre sea consistente con su categoría tomando en cuenta el mercado de aplicaciones ya existente para los usuarios.
A simple vista no se podría predecir, debido a que para el clasificador dos nombres serán distintos y no parecidos. Para esto se utilizó una herramienta de vectorización de sklearn, con cuyos valores fuimos capaces de realizar la clasificación.
Para la clasificación se decidió utilizar un árbol de decisiones. Se optó por este clasificador debido a que entre las otras opciones se tiene KNN, el cual no se ajusta bien a las necesidades del análisis ya que no nos interesa una vecindad de nombres, y cross-validation crece demasiado rápido en costo según cantidad de datos en el dataframe.
from sklearn.tree import DecisionTreeClassifier
from sklearn.preprocessing import LabelEncoder
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score, classification_report
from sklearn.feature_extraction.text import TfidfVectorizer
# Extraemos las columnas para entrenar el clasificador
app_names = apps.App
app_categories = apps.Category
# Vectorizamos el texto de los nombres de las apps
tfidf = TfidfVectorizer(sublinear_tf=True, min_df=5, norm='l2', encoding='latin-1', ngram_range=(1, 2), stop_words=['englis', 'spanish'])
features = tfidf.fit_transform(app_names).toarray()
# Codificamos a valores numericos las categorias
le_cat = LabelEncoder()
cat_classes = le_cat.fit_transform(app_categories)
# Dividimos el dataset en test+train
N_train, N_test, cat_train, cat_test = train_test_split(features, cat_classes, test_size=.3, random_state=37, stratify=cat_classes)
clf = DecisionTreeClassifier()
clf.fit(N_train, cat_train)
cat_pred = clf.predict(N_test)
print("Accuracy en test set:", accuracy_score(cat_test, cat_pred))
print(classification_report(cat_test, cat_pred, target_names=le_cat.classes_))
Destacamos que las aplicaciónes de clima (WEATHER) poseen una precisión de 1. Probablemente debido a lo sencillo que resulta nombrarlas (time, tiempo, etc). Por otro lado, las 3 categorías que observamos tenían mayor cantidad de aplicaciones tienen una baja precisión a la hora de ser clasificadas. Esto debido a que la alta cantidad de aplicaciones produce muchos nombres distintos, haciendo dificil la tarea de clasificarlos.
Con estos resultados poseemos un accuracy de aproximadamente 47%. Con esto se propone un modelo que a partir de un nombre entregue 3 posibles categorías a la que ese nombre pertenecería, a modo de tener una aplicación con un nombre consistente con su categoría.
¿Se puede predecir el comportamiento si una aplicación de una tienda es publicado en otra? Para esto se utilizó un dataset del App Store
apple = pd.read_csv('https://s3.us-east-2.amazonaws.com/cc5206-2018-2/AppleStore.csv')
apple.head()
apple.info()
Con este dataframe tenemos columnas en común con el de Play Store, por lo que podemos identificar las aplicaciones en común para realizar clasificación. A continuación se calcula cuántas aplicaciones están presentes en ambos dataframes juzgando sólo por su nombre.
#Cantidad de aplicaciones con el mismo nombre en ambos dataframes
s1 = apps.App.str.lower().str.strip()
s2 = apple.track_name.str.lower().str.strip()
print("Hay",s1[s1.isin(s2)].size,"aplicaciones en común.")
Observamos que hay 569 aplicaciones que coinciden en ambos dataframes. Este número es demasiado pequeño para realizar una clasificación ya que corresponde a aproximadamente el 5% del tamaño de lo que deseamos clasificar, además de que en estos valores se presentan outliers, como serían las aplicaciones de whatsapp, facebook, instagram, entre otras, resultando en una cantidad útil de datos aún menor.
Debido a esto el estudio no se puede realizar, ya que los resultados no serían representativos.
Resolviendo varias dudas que generó el hito 1, se propone cambiar el estudio de Google Play Store a un análisis de tiendas de aplicaciones en general, obteniendo relaciones a partir de resultados de los distintos dataframes por separado.
Además de esto, se buscará algún método para intentar predecir el rating de una aplicación con otra herramienta, ya que en esta entrega no se pudo lograr con clasificadores.
Se propone utilizar el dataset de Reviews para buscar palabras clave que hagan que una review corresponda a un sentimiento en particular, de forma de saber qué comportamientos de aplicación producen un sentimiento negativo/positivo según los usuarios.
Luego de las presentaciones del hito 2, se recibió un feedback por escrito de algunos compañeros además de comentarios tanto por parte de otros estudiantes como del cuerpo docente.
El feedback por escrito no fue de gran ayuda, ya que la mayoría apelaba ante la presentación en sí antes que el proyecto presentado, y los pocos comentarios que iban dirigidos a la información sobre el proyecto, eran de información que sí entregamos, pero durante las presentaciones no se dieron a entender tan claramente.
Los comentarios no escritos que recibimos de nuestros compañeros y del cuerpo docente dieron a lugar a lo que se realizó en el hito 3. Durante el hito 2 se nos llamó la atención sobre el hecho de utilizar un clasificador sobre el nombre de aplicaciones, ya que "Tu app" y "TuApp" podrían ser clasificadas de manera distinta, cuando cláramente aluden a lo mismo. Esto es debido a la naturaleza de los nombres de las aplicaciones y cómo se vectoriza el texto para poder trabajar.
Además, notamos que nuestro dataset poseía poca información, por lo que, a pesar de escapar un poco de lo visto en el curso, se realizó Scraping para poder agregar la descripción de las aplicaciones a éste.
Con esto, se resolvería un poco la problemática de la poca información acerca de una misma App, además de que en la descripción, al ser texto más extenso y escrito, usualmente, de manera formal, es posible realizar una clasificación esperando un mejor resultado que con los nombres.
En vista de los gráficos del hito anterior, se nota que puede ser interesante medir la cantidad de descargas promedio de cada categoría, dado que, por ejemplo, a pesar de que la categoría Familia lidera en cuanto a cantidad de aplicaciones, pero no se destaca en el gráfico de cantidad de descargas totales.
El siguiente gráfico muestra la cantidad de descargas promedio de cada categoría.
apps_by_cat = apps.groupby('Category').count()['App']
inst_by_apps = installs_by_cat.div(apps_by_cat, axis=0)
plt.plot(inst_by_apps, ls=':', marker='o')
plt.xticks(rotation='vertical')
plt.show()
Se puede notar que esta métrica se acerca más a lo que se podría esperar, pues la categoría que lidera (por bastante) es Communication la que contiene aplicaciones como Whatsapp, Telegram, Line, etc. De hecho, ahora se puede apreciar que Family no se destaca tanto como se esperaría.
En vista de la limitada información de la que se dispone. Se utiliza un scraper en Python que permite extender el Dataset para ahora disponer de las descripciones de cada aplicación. El scraper puede encontrarse en este link
Utilizando esta nueva información, se procede a clasificar las descripciones con el fin de predecir la categoría de una App en base a ésta.
Se debe recordar que en el hito anterior se había clasificado con los nombres de las aplicaciones. Se espera que esta clasificación sea más efectiva pues las descripciones suelen presentar más información sobre el contenido de su aplicación que el nombre.
import pandas as pd
df = pd.read_csv('https://users.dcc.uchile.cl/~bhurtado/resources/googleplaystore.csv')
df.head()
Para este análisis se considerará solamente la categoría (Category) y la descripción (Description).
col = ['Category', 'Description']
df = df[col]
# Hay una app con descripción nula
df = df[pd.notnull(df['Description'])]
df.columns = col
# Representación numérica a las categorias
df['category_id'] = df['Category'].factorize()[0]
category_id_df = df[['Category', 'category_id']].drop_duplicates().sort_values('category_id')
category_to_id = dict(category_id_df.values)
id_to_category = dict(category_id_df[['category_id', 'Category']].values)
df.head()
En la siguiente figura se puede apreciar que las clases están bastante debalanceadas. Esto al momento de poner a prueba el clasificador puede significar un problema, ya que la categoría FAMILY
tiene muchos más datos con los que se puede producir un sesgo. Sin embargo, se decide seguir adelante sin hacer oversampling o subsampling, pues se considera que el dataset representa el esquema real de aplicaciones, no se debe perturbar a quitar o agregar aplicaciones.
import matplotlib.pyplot as plt
fig = plt.figure(figsize=(8,6))
df.groupby('Category').Description.count().plot.bar(ylim=0)
plt.show()
Para procesar el texto de las descripciones se utiliza la herramienta TfidfVectorizer
de python, con el fin de vectorizar el texto con la técnica Term-frecuency inverse document frecuency.
from sklearn.feature_extraction.text import TfidfVectorizer
tfidf = TfidfVectorizer(sublinear_tf=True, min_df=5, norm='l2', encoding='latin-1', ngram_range=(1, 2), stop_words='english')
features = tfidf.fit_transform(df.Description).toarray()
labels = df.category_id
features.shape
A continuación, se utilizarán 5 modelos distintos de clasificación, entre ellos, el Decision Tree dada la alta familiarización con su funcionamiento, con el fin de considerarlo como una solución base en cuanto a comparación de métricas.
Esta comparación se realizará en función del accuracy de los modelos, los cuales se podrán apreciar en el siguiente gráfico de boxplot que muestra los resultados de los 5 modelos.
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier
from sklearn.svm import LinearSVC
from sklearn.naive_bayes import MultinomialNB
from sklearn.model_selection import cross_val_score
models = [
RandomForestClassifier(n_estimators=200, max_depth=3, random_state=0),
LinearSVC(),
MultinomialNB(),
LogisticRegression(random_state=0),
DecisionTreeClassifier(),
]
CV = 5
cv_df = pd.DataFrame(index=range(CV * len(models)))
entries = []
for model in models:
model_name = model.__class__.__name__
accuracies = cross_val_score(model, features, labels, scoring='accuracy', cv=CV)
for fold_idx, accuracy in enumerate(accuracies):
entries.append((model_name, fold_idx, accuracy))
cv_df = pd.DataFrame(entries, columns=['model_name', 'fold_idx', 'accuracy'])
import seaborn as sns
sns.boxplot(x='model_name', y='accuracy', data=cv_df)
sns.stripplot(x='model_name', y='accuracy', data=cv_df,
size=8, jitter=True, edgecolor="gray", linewidth=2)
plt.show()
Definitivamente Random Forest no se ajusta bien a los datos. Además, se aprecia que LinearSVC y Logistic Regression presentan mejor accuracy's mejor que el considerado como base, Decision Tree.
Dado que LinearSVC es el mejor en cuanto a esta métrica se procede a calcular la matriz de confusión:
from sklearn.svm import LinearSVC
import seaborn as sns
model = LinearSVC()
X_train, X_test, y_train, y_test, indices_train, indices_test = train_test_split(features, labels, df.index, test_size=0.33, random_state=0)
model.fit(X_train, y_train)
y_pred = model.predict(X_test)
from sklearn.metrics import confusion_matrix
conf_mat = confusion_matrix(y_test, y_pred)
fig, ax = plt.subplots(figsize=(10,10))
sns.heatmap(conf_mat, annot=True, fmt='d',
xticklabels=category_id_df.Category.values, yticklabels=category_id_df.Category.values)
plt.ylabel('Real')
plt.xlabel('Predicción')
plt.show()
Se puede apreciar que, en efecto, la matriz de confusión tiene una diagonal bastante definida, lo que habla bien de las predicciones que realiza el modelo LinearSVC.
Algo interesante de notar es el cuadrado que se genera en el centro de la matriz en que se observa que el modelo confunde, con mayor tasa, las categorías de Juegos y Familia, lo que puede explicarse con la gran correlación que existe entre estas dos categorías, pues en FAMILY se encuentran muchos de los juegos enfoncados para niños pequeños.
A continuación se presenta, de cierta manera, otra métrica que muestra los unigramas y bigramas, utilizados por el modelo, más comunes en el análisis.
model.fit(features, labels)
N = 2
for Category, category_id in sorted(category_to_id.items()):
indices = np.argsort(model.coef_[category_id])
feature_names = np.array(tfidf.get_feature_names())[indices]
unigrams = [v for v in reversed(feature_names) if len(v.split(' ')) == 1][:N]
bigrams = [v for v in reversed(feature_names) if len(v.split(' ')) == 2][:N]
print("# '{}':".format(Category))
print(" . Top unigramas:\n . {}".format('\n . '.join(unigrams)))
print(" . Top bigramas:\n . {}".format('\n . '.join(bigrams)))
Se puede apreciar que los resultados se presentan con bastante sentido en relación a sus categorías, lo cual da aún más confibilidad sobre el modelo.
from sklearn import metrics
print(metrics.classification_report(y_test, y_pred, target_names=df['Category'].unique()))
Finalmente, con respecto a los distintos modelos de clasificación utilizados en el análisis de descripciones, claramente LinearSVC y Logistic Regression son los que mejor se adaptan a los datos siendo el primero, el que tiene mejores métricas de accuracy.
Haciendo un recuento global del proyecto, es claro para el grupo que éste no representa un éxito en lo absoluto, pues si bien se logró aplicar conocimientos adquiridos en el curso, no fue posible aplicar muchas otras de las herramientas y análisis aprendidos.
Esto se debe, mayoritariamente, a que al no conocer, en un principio, las herramientas que se aprenderían, la elección del dataset no fue óptima en función de la posiblidad de usar éstas. En muchos sentidos el dataset no aportó la información necesaria para realizar hartas ideas que surgieron, y la cantidad de columnas limitó la posibilidad de usar clasificadores más interesantes.
Definitivamente, una de los cosas que se puede mejorar en un futuro, en otros proyectos, es que se tendrá una visión mucho más realista de qué es posible y qué no, incluso al inicio de éste.