La industria de los videojuegos es una de las más grandes industrias de entretenimiento a nivel mundial. Las mayores publicaciones se enfrentan codo a codo para conseguir el mayor éxito y con esto mayores ventas.
En esta industria, como en muchas otras, los críticos juegan un rol vital a la hora de definir la recepción que tendrá un juego. Casi siempre, los críticos reciben copias de juegos antes de que estos sean lanzados al público, por lo que tienen la primera palabra a la hora de publicitar si un juego es de calidad o no.
Dado esto, se da origen a un fenómeno en el que los críticos dan muy buena crítica a un juego, tal vez motivados por dinero o por quedar bien con los publicadores para seguir recibiendo acceso exclusivo a los juegos, y luego los usuarios dan un puntaje mucho menor, dejando una sensación de engaño y desencanto. A estos juegos con una gran diferencia de puntaje los llamaremos fiascos.
Interesa entonces utilizar las herramientas que provee este curso para estudiar los distintos patrones que pueden surgir a la hora de puntuar la calidad de un juego. En específico, lograr crear un predictor para saber, ojalá con bastante seguridad, si un juego será un fiasco o no.
Por la información manejada por los miembros del equipo, y lo que se ha observado de los últimos grandes fiascos en la industria de los videojuegos, se intuye que un factor importante a la hora de determinar si un juego será un fiasco o no será el publicador del juego. Esperamos que esta variable tenga alta correlación con los fiascos.
Para explorar el fenómeno antes descrito, se utiliza el dataset extraido de https://www.kaggle.com/silver1996/videogames/data.
Este se construye de datos de Metacritic.com, el cual incluye 16719 entradas con los datos que se presentan a continuación.
import pandas as pd
import numpy as np
original_data = pd.read_csv('data/Video_Games_Sales_as_at_22_Dec_2016.csv',encoding='latin1')
print("(Filas x Columnas) = ",original_data.shape)
original_data.head()
Los gráficos presentados a continuación se contruyen con este data set, con la intención de extraer información útil de este.
Se observa que el data set incluye muchas columnas con distintas variables, y también algunas filas que le faltan valores. Para comenzar a usar este data set se debe hacer una limpieza que solo deje datos que sean útiles.
El siguiente script en R busca limpiar y ordenar el data set para ser utilizado posteriormente por los clasificadores. Se borran varias columnas que no estarían disponibles cuando sale un juego, como ventas. También se borran columnas que no nos aportarán información al explorar a futuro, como el año de lanzamiento, o el nombre del juego.
Se busca tambien limitar la cantidad de valores distintos de varias columnas, para mantener el problema con baja dimensionalidad.
########### 1 - creacion de la tabla y tipos ################ ## cambiar esta linea al path del .csv ## si se mantiene la estructura de carpetas del repositorio, basta con ## dejar el working directory en la ubicacion de este archivo library(readr) videogames <- read_csv("../data/Video_Games_Sales_as_at_22_Dec_2016.csv") # se eliminan las filas que tengan NA (quedan aprox 7000 resultados) filtered_vid = na.omit(videogames) # se arreglan los tipos: user-score a numeric, los otros char a factor filtered_vid$User_Score <- as.numeric(filtered_vid$User_Score) character_vars <- lapply(filtered_vid, class) == "character" filtered_vid[, character_vars] <- lapply(filtered_vid[, character_vars], as.factor) # se define la diferencia necesaria para considerar que un juego fue un fiasco # (al comparar user_score con critic_score) dif_for_fail = 2 # se crea y pobla la columna que define si un juego es un fiasco o no filtered_vid["Is_Fiasco"] <- NA filtered_vid$Is_Fiasco <- ((filtered_vid$Critic_Score / 10) - filtered_vid$User_Score) > dif_for_fail ########## 2 - reduccion de dimensionalidad para Machine Learning ########## ## eliminar categorias reduced <- filtered_vid reduced["User_Count"] <- NULL reduced["User_Score"] <- NULL reduced["Critic_Count"] <- NULL reduced["Other_Sales"] <- NULL reduced["EU_Sales"] <- NULL reduced["JP_Sales"] <- NULL reduced["NA_Sales"] <- NULL reduced["Year_of_Release"] <- NULL reduced["Name"] <- NULL reduced["Developer"] <- NULL reduced["Developer"] <- NULL ## tomar solo las n consolas mas populares: n = 5 popular_console <- reduced popular_console["counter"] <- 1 popular_console = aggregate(counter ~ Platform,popular_console,FUN=sum) popular_console = popular_console[order(popular_console$counter,decreasing = T),] reduced <- reduced[reduced$Platform %in% popular_console[1:n,]$Platform,] ## tomar solo los n mayores publicadores: n = 10 popular_publisher <- reduced popular_publisher["counter"] <- 1 popular_publisher = aggregate(counter ~ Publisher,popular_publisher,FUN=sum) popular_publisher = popular_publisher[order(popular_publisher$counter,decreasing = T),] reduced <- reduced[reduced$Publisher %in% popular_publisher[1:n,]$Publisher,] #### Descomentar siguiente linea para exportar el dataset write.csv(reduced, file = "../data/data_para_clasificadores.csv")
Tras esto, se tienen 8 columnas. La primera un número por cada juego. Las siguientes 6 son parámetros, y la última la clase a clasificiar del juego. La clase corresponde a si el juego es un fiasco o no.
data = pd.read_csv('data/data_para_clasificadores.csv',encoding='latin1')
data.head()
Cabe notar que las clases no están balanceadas.
print("Cantidad de Fiascos")
data['Is_Fiasco'].value_counts()
Los clasificadores no pueden trabajar con Strings directamente. Vemos que Platform, Genre, Publisher y Rating son categorías que utilizan Strings, y hay que aplicar algún tipo de transformación para poder alimentarlas al clasificador.
Para esto, se utiliza un LabelBinarizer, que permite covertir las distintas categorías de una columna en columnas independientes. El resultado final es una matriz de dimensiones: 2348 rows x 34 columns.
from sklearn import preprocessing
## Se aplica LabelBinarizer columna por columna, y finalmente se unen los resultados
## En header se van guardando los nombres de cada columna para luego agregarlas al nuevo Data Set
lb = preprocessing.LabelBinarizer()
lb.fit(data["Platform"])
platform = lb.transform(data["Platform"])
header = lb.classes_
lb.fit(data["Genre"])
genre = lb.transform(data["Genre"])
header = np.append(header,lb.classes_)
lb.fit(data["Publisher"])
publisher = lb.transform(data["Publisher"])
header = np.append(header,lb.classes_)
##sales = np.transpose(np.matrix(data["Global_Sales"].values))
##header = np.append(header,"Global_Sales")
critic_score = np.transpose(np.matrix(data["Critic_Score"].values))
header = np.append(header,"Critic_Score")
lb.fit(data["Rating"])
rating = lb.transform(data["Rating"])
header = np.append(header,lb.classes_)
fiasco = np.transpose(np.matrix(data["Is_Fiasco"].values))
header = np.append(header,"Is_Fiasco")
new_matrix = np.hstack((platform,genre,publisher,critic_score,rating,fiasco))
new_data = pd.DataFrame(new_matrix)
new_data.columns = header
## Se separa los datos de los resultados a predecir.
X = new_data[new_data.columns[:-1]]
y = new_data[new_data.columns[-1]]
Para observar la nueva data:
new_data.head()
Como predictor de fiascos, se busca tener un clasificador que tenga un porcentaje alto de predicción de fiascos. Mediante varios experimentos, se muestra a continuación como se comparan varios clasificadores ante nuestro data set.
Utilizando código del laboratorio 2.2 del curso, se comparan distintos clasificadores mediante el contraste de las métricas promedio obtenidas tras un buen número de pruebas.
import graphviz
import io
import pydotplus
import imageio
from sklearn import tree
from sklearn.tree import DecisionTreeClassifier, export_graphviz
from sklearn.model_selection import train_test_split, cross_val_score
from matplotlib import pyplot as plt
from sklearn.metrics import f1_score, recall_score, precision_score
from sklearn.dummy import DummyClassifier
from sklearn.naive_bayes import GaussianNB # naive bayes
from sklearn.neighbors import KNeighborsClassifier
from sklearn.svm import SVC # support vector machine classifier
from sklearn.svm import LinearSVC
from sklearn.ensemble import RandomForestClassifier
from sklearn.datasets import make_classification
def run_clf_with_cross_val(clf, X, y, num_tests=100, k=5):
metrics = {'f1-score': [], 'precision': [], 'recall': [], 'score': []}
for _ in range(num_tests):
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=.30, stratify=y)
clf.fit(X_train, y_train)
predictions = clf.predict(X_test)
scores = cross_val_score(clf, X, y, cv=k, scoring='f1')
metrics['f1-score'].append(f1_score(y_test,predictions))
metrics['recall'].append(recall_score(y_test,predictions))
metrics['precision'].append(precision_score(y_test,predictions))
metrics['score'].append(scores.mean())
return metrics
import warnings
warnings.filterwarnings('ignore')
def run_many_classifiers(X, y, num_test):
c0 = ("Base Dummy", DummyClassifier(strategy='stratified'))
c1 = ("Decision Tree", DecisionTreeClassifier(min_samples_split=100))
c2 = ("Gaussian Naive Bayes", GaussianNB())
c3 = ("KNN-3", KNeighborsClassifier(n_neighbors=3))
c4 = ("KNN-5", KNeighborsClassifier(n_neighbors=5))
c5 = ("Random Forest",RandomForestClassifier(max_features="auto", max_depth=15, n_estimators=40))
classifiers = [c0, c1, c2, c3, c4, c5]
print("Corriendo "+ str(num_test) + " tests por clasificador\n")
for name, clf in classifiers:
metrics = run_clf_with_cross_val(clf, X, y, num_test)
print("----------------")
print("Resultados para clasificador: ",name)
print("Precision promedio:",np.array(metrics['precision']).mean())
print("Recall promedio:",np.array(metrics['recall']).mean())
print("F1-score promedio:",np.array(metrics['f1-score']).mean())
print("Cross Validation F1-score promedio:", np.array(metrics['score']).mean())
run_many_classifiers(X, y, 20)
warnings.filterwarnings('once')
Se puede observar que los resultados obtenidos no son considerablemente buenos. Tomando en cuenta el F1-score, que describe en general la eficacia de un clasificador, los puntajes son bien bajos, aunque aún así mejores que el base dummy.
De todos los clasificadores explorados, Gaussan Naive Bayes y Random Forest son los que se ven más prometedores a predictor de fiascos.
En un intento de encontrar mejores resultados que los anteriores, se aplicarán estas estrategias sobre el dataset, buscando que los clasificadores aprendan mejor teniendo clases balanceadas. Nuevamente nos apoyamos en código trabajado en el laboratorio 2.2.
# oversampling sobre la clase True
idx = np.random.choice(new_data.loc[data.Is_Fiasco == True].index, size=1990)
data_oversampled = pd.concat([new_data, new_data.iloc[idx]])
print("Data oversampled on class 'True'")
print(data_oversampled['Is_Fiasco'].value_counts())
print()
# subsampling sobre la clase False
idx = np.random.choice(new_data.loc[new_data.Is_Fiasco == False].index, size=1990, replace=False)
data_subsampled = new_data.drop(new_data.iloc[idx].index)
print("Data subsampled on class 'False'")
print(data_subsampled['Is_Fiasco'].value_counts())
warnings.filterwarnings('ignore')
# datos "oversampleados"
X_over = data_oversampled[new_data.columns[:-1]]
y_over = data_oversampled[new_data.columns[-1]]
# datos "subsampleados"
X_subs = data_subsampled[new_data.columns[:-1]]
y_subs = data_subsampled[new_data.columns[-1]]
print("----------Prueba Oversampling------------")
run_many_classifiers(X_over, y_over, 20)
print("\n\n----------Prueba Subsampling------------")
run_many_classifiers(X_subs, y_subs, 20)
También, como experimento, se buscó hacer subsampling y oversampling al mismo tiempo para no repetir tantos datos, pero tampoco quedarnos con tan pocos. Esto se muestra a continuación.
idx = np.random.choice(new_data.loc[data.Is_Fiasco == True].index, size=71)
data_master = pd.concat([new_data, new_data.iloc[idx]])
idx = np.random.choice(new_data.loc[new_data.Is_Fiasco == False].index, size=1669, replace=False)
data_master = data_master.drop(new_data.iloc[idx].index)
print("Data subsampled on class 'False' and oversampled on class 'True'")
print(data_master['Is_Fiasco'].value_counts())
X_mast = data_master[new_data.columns[:-1]]
y_mast = data_master[new_data.columns[-1]]
run_many_classifiers(X_mast, y_mast, 10)
A continuación se grafican los resultados obtenidos para Base Dummy, Decision Tree y Random Forest. Como se han corrido varias veces los clasificadores puede que los gráficos no correspondan perfectamente a los valores, pero sí con la cercanía suficiente para ser precisos.
Haciendo un análisis de los resultados obtenidos, podemos considerar que Random Forest es clasificador que tiene mejor desempeño en cuanto a resultados, en especial al hacer oversampling.
Si bien no es siempre certero, tiene un puntaje suficiente de exactitud, lo que consideramos un logro aceptable con respecto a lo que esperabamos obtener. Si necesitaramos crear un predictor efectivo, utilizariamos ese clasificador.
En un ejercicio para explorar la importancia de las variables, y para observar la lógica de los decision trees, se grafican los árboles de decisión, donde los colores indican afinidad con una clase.
c12= DecisionTreeClassifier(min_samples_split=100)
c13= DecisionTreeClassifier(min_samples_split=100)
c14= DecisionTreeClassifier(min_samples_split=100)
c15= DecisionTreeClassifier(min_samples_split=100)
features=new_data.columns[:-1]
train, test= train_test_split(new_data,test_size=.30, stratify=y)
X_train= train[features]
y_train=train["Is_Fiasco"]
X_test=test[features]
y_test=test["Is_Fiasco"]
dt12=c12.fit(X_train,y_train)
dt13=c13.fit(X_over,y_over)
dt14=c14.fit(X_subs,y_subs)
dt15=c15.fit(X_mast,y_mast)
def show_tree(tree, features, path):
path = "images/" + path
f= io.StringIO()
export_graphviz(tree, out_file=f, feature_names=features,filled=True,rounded=True)
pydotplus.graph_from_dot_data(f.getvalue()).write_png(path)
img= imageio.imread(path)
plt.rcParams["figure.figsize"]=(20,20)
plt.imshow(img)
Árbol Normal:
show_tree(dt12, features, 'arbol_normal.png')
Árbol Oversampled:
show_tree(dt13, features, 'arbol_oversampled.png')
Árbol Subsampled:
show_tree(dt14, features, 'arbol_subsampled.png')
Árbol con Oversampling y Subsampling:
show_tree(dt15, features, 'arbol_master.png')
Aplicando Random Forest, se pueden obtener los atributos que más seguido se utilizan para discriminar la clase a clasificar. De esta forma se puede evidenciar cuales son las columnas más importantes para decidir si un juego es un fiasco o no.
import operator
clf6 = RandomForestClassifier(n_estimators= 1000,max_depth=100, random_state=0)
train, test= train_test_split(new_data,test_size=.30, stratify=y)
X_train= train[features]
y_train=train["Is_Fiasco"]
X_test=test[features]
y_test=test["Is_Fiasco"]
clf6.fit(X_train, y_train)
mi_lista_de_tuplas = []
for i in range(33):
tupla = (header[i],clf6.feature_importances_[i])
mi_lista_de_tuplas.append(tupla)
mi_lista_de_tuplas.sort(key=operator.itemgetter(1), reverse=True)
for i in range(33):
print(mi_lista_de_tuplas[i])
Vemos con esto que la variable más importante por lejos es Critic_Score, lo cual tiene mucho sentido pues con esta en parte se define la clase Is_Fiasco. Luego, dentro de las más importantes tenemos una plataforma (PC), dos publicadores (Activision y EA) y dos géneros (Action y Sports).
Como una nueva estrategia para hacer análisis de los datos,se trabajó con clustering usando K-means. Se utilizaron los atributos “Global_Sales”, “Critic_Score” y “User_Score”, que son los únicos datos numéricos que se manejaban en el data set. El siguiente script de R detalla el trabajo hecho de clustering.
########### 1 - creacion de la tabla y tipos ################ ## cambiar esta linea al path del .csv ## si se mantiene la estructura de carpetas del repositorio, basta con ## dejar el working directory en la ubicacion de este archivo library(readr) videogames <- read_csv("../data/Video_Games_Sales_as_at_22_Dec_2016.csv") # se eliminan las filas que tengan NA (quedan aprox 7000 resultados) filtered_vid = na.omit(videogames) # se arreglan los tipos: user-score a numeric, los otros char a factor filtered_vid$User_Score <- as.numeric(filtered_vid$User_Score) character_vars <- lapply(filtered_vid, class) == "character" filtered_vid[, character_vars] <- lapply(filtered_vid[, character_vars], as.factor) # se define la diferencia necesaria para considerar que un juego fue un fiasco # (al comparar user_score con critic_score) dif_for_fail = 2 #se crea y pobla la columna que define si un juego es un fiasco o no filtered_vid["Is_Fiasco"] <- NA filtered_vid$Is_Fiasco <- ((filtered_vid$Critic_Score / 10) - filtered_vid$User_Score) > dif_for_fail ########## 2 - reduccion de dimensionalidad para Machine Learning ########## ## eliminar categorias reduced <- filtered_vid reduced["User_Count"] <- NULL reduced["User_Score"] <- NULL reduced["Critic_Count"] <- NULL reduced["Other_Sales"] <- NULL reduced["EU_Sales"] <- NULL reduced["JP_Sales"] <- NULL reduced["NA_Sales"] <- NULL reduced["Year_of_Release"] <- NULL reduced["Name"] <- NULL reduced["Developer"] <- NULL reduced["Developer"] <- NULL ## tomar solo las n consolas mas populares: n = 5 popular_console <- reduced popular_console["counter"] <- 1 popular_console = aggregate(counter ~ Platform, popular_console, FUN = sum) popular_console = popular_console[order(popular_console$counter, decreasing = T), ] reduced <- reduced[reduced$Platform %in% popular_console[1:n, ]$Platform, ] ## tomar solo los n mayores publicadores: n = 10 popular_publisher <- reduced popular_publisher["counter"] <- 1 popular_publisher = aggregate(counter ~ Publisher, popular_publisher, FUN = sum) popular_publisher = popular_publisher[order(popular_publisher$counter, decreasing = T), ] reduced <- reduced[reduced$Publisher %in% popular_publisher[1:n, ]$Publisher, ] #### Descomentar siguiente linea para exportar el dataset #write.csv(reduced, file = "../data/data_para_clasificadores.csv") ################ CLUSTERING ############## datacluster <- data.frame(reduced["Global_Sales"], reduced["Critic_Score"])#se hace un vector de 2 variables para hacer un cluster plot(datacluster) #lo miro, el me mira # vemos el codo para ver cual k tomar --------------- set.seed(2) wss <- 0 clust = 15 # graficaremos hasta 15 clusters for (i in 1:clust) { wss[i] <- sum(kmeans(datacluster, centers = i)$withinss) } plot(1:clust, wss, type = "b", xlab = "Numero de clusters", ylab = "wss") # hacemos kmeans k=3 ------------------------------------ set.seed(2) km.out <- kmeans(datacluster, 3, nstart = 20) # vemos los resultados plot( datacluster, col = (km.out$cluster), main = "Resultados usando k = 3 (plot con colores)", xlab = "Global_Sales", ylab = "Critic_Scores", pch = 20, cex = 2 ) library(ggplot2) # instalar si es necesario # install.packages("GGally") library(GGally) #otra forma de visualizar datacluster$cluster <- factor(km.out$cluster) ggpairs(datacluster, aes(colour = cluster), alpha = 0.4) #m?s a?n library("cluster") # instalar si es necesario clusplot( datacluster[, c(1, 2)], km.out$cluster, color = TRUE, shade = TRUE, labels = 2, lines = 0, main = "IPCAP (k=3)" ) hc.complete <- hclust(dist(datacluster), method = "complete") hc.single <- hclust(dist(datacluster), method = "single") hc.average <- hclust(dist(datacluster), method = "average") par(mfrow = c(1, 3)) plot( hc.complete, main = "Complete", xlab = "Global_Sales", ylab = "Critic_Scores", cex = .9 ) plot( hc.single, main = "Single", xlab = "Global_Sales", ylab = "Critic_Scores", cex = .9 ) plot( hc.average, main = "Average", xlab = "Global_Sales", ylab = "Critic_Scores", cex = .9 ) library("dbscan") db <- dbscan(datacluster[, c(1, 2)], eps = 1, minPts = 20)
Primero se consideran las ventas globales y la puntuación de la crítica. Calculando el valor del parámetro WSS para distintas cantidades de clusters y utilizando el método del codo, se distinguió que 3 es un número de clusters que podría dar buenos resultados.
Con estas variables, se ve que el data set no se adapta directamente a una estrategia de clustering. Sin embargo, aún nos permite caracterizar más los datos con los que se trabajan. Se evidencia que los juegos valorados más positivamente tienden a tener mejores ventas que los que no, y en general los juegos que acumulan ventas masivas pertenecen al cluster de juegos de valoración alta por los críticos.
Luego se consideran las ventas globales y puntuación de usuarios. Usando el siguiente gráfico también se decide usar K = 3
Con este clustering ocurre algo más interesante. Vemos que independiente de la puntuación de los usuarios, se registra un cluster correspondiente a juegos con altas ventas, donde básicamente todos los juegos con más de 5 millones de ventas se encuentra en esta categoría. Este cluster de altas ventas tiene un puntaje de usuarios promedio bastante alto, aunque de todas formas se pueden identificar varios outliers que tienen un puntaje considerablemente más bajo.
Luego se consideran las puntuaciones de usuarios y críticos. Usando el siguiente gráfico también se decide usar K = 3
Este gráfico, a pesar de que es interesante ver graficadas las puntuaciones de los usuarios v/s los críticos, no entrega información que se considere relevante o nueva.
En este gráfico se consideran las tres variables antes descritas. Al igual que el gráfico anterior, no entrega información que se considere de gran relevancia.
Se aplicaron técnicas de reglas de asociación. Para esto primero se hace un preprocesamiento de los datos, en donde, por ejemplo, se definen las clases respecto a la cantidad de ventas (Muchisimas, Muchas, Bastante, Intermedio, Pocas, Muypocas).
library(readr) #cargamos readr para poder leer csv's library(arules) # cargamos arules videogames <- read_csv("../data/Video_Games_Sales_as_at_22_Dec_2016.csv") auxiliar <- videogames #usamos un auxiliar para realizar cambios sin modificar el original # se eliminan las filas que tengan NA (quedan aprox 7000 resultados) sinna = na.omit(videogames) sinnauxiliar = na.omit(videogames) #se definen los intervalos para calificar los atributos num?ricos y asignarles una etiqueta #En caso de haber un n?mero mucho m?s alto que los dem?s, se considera outlier y se le asigna #la etiqueta "much?simo" mientras que el segundo n?mero m?s alto se divide en 5 para crear #los intervalos si es que no es un outlier como el anterior #ventas en Norte Am?rica Muchisimas = 40 Muchas = 3.136+3.136+3.136+3.136+3.136 Bastante = 3.136+3.136+3.136+3.136 Intermedio = 3.136+3.136+3.136 Pocas= 3.136+3.136 Muypocas = 3.136 ventasnulas_o_sin_informacion = 0 #ventas en Europa Muchisimas2 = 28.96 Muchas2 = 2.552+2.552+2.552+2.552+2.552 Bastante2 = 2.552+2.552+2.552+2.552 Intermedio2 = 2.552+2.552+2.552 Pocas2= 2.552+2.552 Muypocas2 = 2.552 ventasnulas_o_sin_informacion2 = 0 #ventas en Jap?n Muchas3 = 1.3+1.3+1.3+1.3+1.3 Bastante3 = 1.3+1.3+1.3+1.3 Intermedio3 = 1.3+1.3+1.3 Pocas3= 1.3+1.3 Muypocas3 = 1.3 ventasnulas_o_sin_informacion3 = 0 #Otras ventas Muchas4 = 2.114+2.114+2.114+2.114+2.114 Bastante4 = 2.114+2.114+2.114+2.114 Intermedio4 = 2.114+2.114+2.114 Pocas4 = 2.114+2.114 Muypocas4 = 2.114 ventasnulas_o_sin_informacion3 = 0 #Ventas globales Muchas5 = 82.53 Bastante5 = 2+2+2+2 Intermedio5 = 2+2+2 Pocas5 = 2+2 Muypocas5 = 2 ventasnulas_o_sin_informacion4 = 0 #se crea una columna extra para ventas de norte am?rica sinna["Ventas NA"] <- NA # se asignan las etiquetas a las ventas de norte am?rica for (i in 1:6947){ if(sinna[i,6]>Muchisimas){ sinna[i,17]="muchisimas ventas en NA" } else if (sinna[i,6]>Bastante){ sinna[i,17]="muchas ventas en NA" } else if (sinna[i,6]>Intermedio){ sinna[i,17]="Bastantes ventas en NA" } else if (sinna[i,6]>Pocas){ sinna[i,17]="ventas en NA intermedias" } else if (sinna[i,6]>Muypocas){ sinna[i,17]="pocas ventas en NA" } else if (sinna[i,6]>ventasnulas_o_sin_informacion){ sinna[i,17]="muy pocas ventas en NA" } else if (sinna[i,6]==ventasnulas_o_sin_informacion){ sinna[i,17]="nulas ventas en NA o sin informacion" } } #se crea una columna extra para ventas de Europa sinna["Ventas EU"] <- NA for (i in 1:6947){ if(sinna[i,7]==Muchisimas2){ sinna[i,18]="muchisimas Ventas en EU" } else if (sinna[i,7]>Bastante2){ sinna[i,18]="muchas Ventas en EU" } else if (sinna[i,7]>Intermedio2){ sinna[i,18]="Bastantes Ventas en EU " } else if (sinna[i,7]>Pocas2){ sinna[i,18]=" Ventas en EU intermedias" } else if (sinna[i,7]>Muypocas2){ sinna[i,18]="pocas Ventas en EU" } else if (sinna[i,7]>ventasnulas_o_sin_informacion){ sinna[i,18]="muy pocas Ventas en EU" } else if (sinna[i,7]==ventasnulas_o_sin_informacion){ sinna[i,18]="Ventas en EU nulas o sin informacion" } } #se crea una columna extra para ventas de Japon sinna["Ventas JP"] <- NA for (i in 1:6947){ if (sinna[i,8]>Bastante3){ sinna[i,19]="muchas Ventas en JP" } else if (sinna[i,8]>Intermedio3){ sinna[i,19]="Bastantes Ventas en JP " } else if (sinna[i,8]>Pocas3){ sinna[i,19]="Ventas en JP intermedias" } else if (sinna[i,8]>Muypocas3){ sinna[i,19]="pocas Ventas en JP" } else if (sinna[i,8]>ventasnulas_o_sin_informacion){ sinna[i,19]="muy pocas Ventas en JP" } else if (sinna[i,8]==ventasnulas_o_sin_informacion){ sinna[i,19]="nulas Ventas en JP o sin informacion" } } #se crea una columna extra para "otras" ventas sinna["Ventas otras"] <- NA for (i in 1:6947){ if (sinna[i,9]>Bastante4){ sinna[i,20]="muchas Ventas otras" } else if (sinna[i,9]>Intermedio4){ sinna[i,20]="Bastantes Ventas otras " } else if (sinna[i,9]>Pocas4){ sinna[i,20]="Ventas otras intermedias" } else if (sinna[i,9]>Muypocas4){ sinna[i,20]="pocas Ventas otras" } else if (sinna[i,9]>ventasnulas_o_sin_informacion){ sinna[i,20]="muy pocas Ventas otras" } else if (sinna[i,9]==ventasnulas_o_sin_informacion){ sinna[i,20]="Ventas otras nulas o sin informacion" } } #se crea una columna extra para "otras" ventas sinna["Ventas globales"] <- NA for (i in 1:6947){ if (sinna[i,10]>Bastante5){ sinna[i,21]="muchas Ventas globales" } else if (sinna[i,10]>Intermedio5){ sinna[i,21]="Bastantes Ventas globales" } else if (sinna[i,10]>Pocas5){ sinna[i,21]="Ventas globales intermedias" } else if (sinna[i,10]>Muypocas5){ sinna[i,21]="pocas Ventas globales" } else if (sinna[i,10]>ventasnulas_o_sin_informacion){ sinna[i,21]="muy pocas Ventas globales" } else if (sinna[i,10]==ventasnulas_o_sin_informacion){ sinna[i,21]="Ventas globales nulas o sin informacion" } } # se crean los intervalos para las etiquetas de los puntajes en las cr?ticas de cr?ticos y usuarios y #el n?mero de cr?ticas de cada uno de estos #Puntajes cr?ticos Muchas6 = 100 Bastante6 = 80 Intermedio6 = 60 Pocas6 = 40 Muypocas6 = 20 ventasnulas_o_sin_informacion4 = 0 #Puntajes usuarios Muchas7 = 10 Bastante7 = 8 Intermedio7 = 6 Pocas7 = 4 Muypocas7 = 2 ventasnulas_o_sin_informacion7 = 0 #N? cr?ticas cr?ticos Muchas8 = 22.6+22.6+22.6+22.6+22.6 Bastante8 = 22.6+22.6+22.6+22.6 Intermedio8 = 22.6+22.6+22.6 Pocas8 = 22.6+22.6 Muypocas8 = 22.6 ventasnulas_o_sin_informacion8 = 0 #N? cr?ticas usuarios Muchas9 = 2133+2133+2133+2133+2133 Bastante9 = 2133+2133+2133+2133 Intermedio9 = 2133+2133+2133 Pocas9 = 2133+2133 Muypocas9 = 2133 ventasnulas_o_sin_informacion8 = 0 #se asignan las etiquetas #se crea una columna extra para puntajes de cr?ticos sinna["Puntajes Cr?ticos"] <- NA for (i in 1:6947){ if (sinna[i,11]>Bastante6){ sinna[i,22]="Puntajes Cr?ticos muy alto" } else if (sinna[i,11]>Intermedio6){ sinna[i,22]="Puntajes Cr?ticos alto" } else if (sinna[i,11]>Pocas6){ sinna[i,22]="Puntajes Cr?ticos intermedio" } else if (sinna[i,11]>Muypocas6){ sinna[i,22]="Puntajes Cr?ticos bajo" } else if (sinna[i,11]>ventasnulas_o_sin_informacion){ sinna[i,22]="Puntajes Cr?ticos muy bajo" } else if (sinna[i,11]==ventasnulas_o_sin_informacion){ sinna[i,22]="Puntajes Cr?ticos nulas o sin informacion" } } #se crea una columna extra para cantidad de cr?ticas de cr?ticos sinna["n? criticas criticos"] <- NA for (i in 1:6947){ if (sinna[i,12]>Bastante8){ sinna[i,23]="n? criticas criticos muy alto" } else if (sinna[i,12]>Intermedio8){ sinna[i,23]="n? criticas criticos alto" } else if (sinna[i,12]>Pocas8){ sinna[i,23]="n? criticas criticos intermedio" } else if (sinna[i,12]>Muypocas8){ sinna[i,23]="n? criticas criticos bajo" } else if (sinna[i,12]>ventasnulas_o_sin_informacion){ sinna[i,23]="n? criticas criticos muy bajo" } else if (sinna[i,12]==ventasnulas_o_sin_informacion){ sinna[i,23]="n? criticas criticos nulas o sin informacion" } } #se crea una columna extra para puntajes de usuarios sinna["Puntajes usuarios"] <- NA for (i in 1:6947){ if (sinna[i,13]>Bastante7){ sinna[i,24]="Puntajes usuarios muy alto" } else if (sinna[i,13]>Intermedio7){ sinna[i,24]="Puntajes usuarios alto" } else if (sinna[i,13]>Pocas7){ sinna[i,24]="Puntajes usuarios intermedio" } else if (sinna[i,13]>Muypocas7){ sinna[i,24]="Puntajes usuarios bajo" } else if (sinna[i,13]>ventasnulas_o_sin_informacion){ sinna[i,24]="Puntajes usuarios muy bajo" } else if (sinna[i,13]==ventasnulas_o_sin_informacion){ sinna[i,24]="Puntajes usuarios nulas o sin informacion" } } #se crea una columna extra para n?mero de cr?ticas de usuarios sinna["n? cr?ticas usuarios"] <- NA for (i in 1:6947){ if (sinna[i,14]>Bastante9){ sinna[i,25]="n? cr?ticas usuarios muy alto" } else if (sinna[i,14]>Intermedio9){ sinna[i,25]="n? cr?ticas usuarios alto" } else if (sinna[i,14]>Pocas9){ sinna[i,25]="n? cr?ticas usuarios intermedio" } else if (sinna[i,14]>Muypocas9){ sinna[i,25]="n? cr?ticas usuarios bajo" } else if (sinna[i,14]>ventasnulas_o_sin_informacion){ sinna[i,25]="n? cr?ticas usuarios muy bajo" } else if (sinna[i,14]==ventasnulas_o_sin_informacion){ sinna[i,25]="n? cr?ticas usuarios nulas o sin informacion" } } #habiendo asignado todas las etiquetas, se eliminan las columnas num?ricas que #contaminar?an la regla de asociaci?n sinna["NA_Sales"] <- NULL sinna["EU_Sales"] <- NULL sinna["JP_Sales"] <- NULL sinna["Other_Sales"] <- NULL sinna["Global_Sales"] <- NULL sinna["Critic_Score"] <- NULL sinna["Critic_Count"] <- NULL sinna["User_Score"] <- NULL sinna["User_Count"] <- NULL #se crea un dataset con todos los atributos nuevos con etiquetas write.csv(sinna, file = "../data/conventas.csv") #se crea otro dataset pero sin atributos que no aportan a las reglas de asociaci?n #se considera que no aportan pues las reglas encontradas asocian esas las etiquetas de esos atributos #sin aportar informaci?n porque o bien la asociaci?n es obvia (por ejemplo: muchas ventas en las distintas regiones del mundo #implican muchas ventas a nivel global) #el nuevo dataset tiene solo las ventas a nivel global, borrando las a nivel de regiones # tambi?n se elimina el rating por las mismas razones sinna["Rating"] <- NULL sinna["Ventas JP"] <- NULL sinna["Ventas NA"] <- NULL sinna["Ventas EU"] <- NULL sinna["Ventas otras"] <- NULL write.csv(sinna, file = "../data/soloventasglobales.csv") #se crea un dataset eliminando el atributo de ventas globales, dejando uno sin ventas sinna["Ventas globales"] <- NULL write.csv(sinna, file = "../data/sinventas.csv") #se eliminan las columnas relacionadas con el g?nero, la plataforma y el a?o de lanzamiento del videojuego sinna["Genre"] <- NULL sinna["Platform"] <- NULL sinna["Year_of_Release"] <- NULL write.csv(sinna, file = "../data/singeneroplataformaano.csv") # se eliminan el n?mero de cr?ticas sinna["n? criticas criticos"] <- NULL sinna["n? cr?ticas usuarios"] <- NULL write.csv(sinna, file = "../data/sinnumerodecriticas.csv") sinna2 <- sinna #Luego, se prueba qu? pasa si se deja solamente el atributo del desarrollador o del publicador sinna["Developer"] <- NULL write.csv(sinna, file = "../data/sindesarrollador.csv") sinna2["Publisher"] <- NULL write.csv(sinna, file = "../data/sinpublicador.csv")library(arules) #luego de importar la librer?a necesaria para el m?todo de las reglas de asociaci?n #se leen los archivos .csv creados como transacciones conventas <- read.transactions("../data/conventas.csv", sep=",") soloventasglobales<- read.transactions("../data/soloventasglobales.csv", sep=",") sinventas <- read.transactions("../data/sinventas.csv", sep=",") singeneroplataformaano <- read.transactions("../data/singeneroplataformaano.csv", sep=",") sinnumerodecriticas <- read.transactions("../data/sinnumerodecriticas.csv", sep=",") sindesarrollador <- read.transactions("../data/sindesarrollador.csv", sep=",") sinpublicador <- read.transactions("../data/sinpublicador.csv", sep=",") #se aplica la metodolog?a de las reglas de asociaci?n para cada uno de las transacciones rules <- apriori(conventas, parameter=list(support=0.01, confidence=0.5)) rules.sorted <- sort(rules, by="lift") rules.sorted.first10 <- head(rules.sorted, 10) inspect(rules.sorted.first10) rules2 <- apriori(soloventasglobales, parameter=list(support=0.01, confidence=0.5)) rules2.sorted <- sort(rules2, by="lift") rules2.sorted.first10 <- head(rules2.sorted, 10) inspect(rules2.sorted.first10) rules3 <- apriori(sinventas, parameter=list(support=0.01, confidence=0.5)) rules3.sorted <- sort(rules3, by="lift") rules3.sorted.first10 <- head(rules3.sorted, 10) inspect(rules3.sorted.first10) rules4 <- apriori(singeneroplataformaano, parameter=list(support=0.01, confidence=0.5)) rules4.sorted <- sort(rules4, by="lift") rules4.sorted.first10 <- head(rules4.sorted, 10) inspect(rules4.sorted.first10) rules5 <- apriori(sinnumerodecriticas, parameter=list(support=0.01, confidence=0.5)) rules5.sorted <- sort(rules5, by="lift") rules5.sorted.first10 <- head(rules5.sorted, 30) inspect(rules5.sorted.first10) rules6 <- apriori(sinpublicador, parameter=list(support=0.01, confidence=0.5)) rules6.sorted <- sort(rules6, by="lift") rules6.sorted.first10 <- head(rules6.sorted, 10) inspect(rules6.sorted.first10) rules7 <- apriori(sindesarrollador, parameter=list(support=0.01, confidence=0.5)) rules7.sorted <- sort(rules7, by="lift") rules7.sorted.first10 <- head(rules7.sorted, 10) inspect(rules7.sorted.first10) #dandose cuenta de que las etiquetas de los puntajes incurren en un error, dado a que #lass cr?ticas pueden ser muy altas por parte de los cr?ticos y muy altas por parte de los usuarios #pero de todas formas puede haber una diferencia de puntajes de 2 puntos lo que clasificar?a como una decepci?n #Se retoma entonces el atributo Is.fiasco que tiene valor "true" si hay una diferencia de puntaje de m?s #de dos entre el promedio de puntajes de cr?ticos y usuarios # Se toma el dataset que cuenta con el atributo "is.fiasco" data_is_fiasco <- read_csv("../data/data_para_clasificadores.csv") #se borran los atributos que no aportaron a la asociaci?n anterior data_is_fiasco["Genre"] <- NULL data_is_fiasco["Platform"] <- NULL data_is_fiasco["Global_Sales"] <- NULL data_is_fiasco["Rating"] <- NULL data_is_fiasco["Critic_Score"] <- NULL #se crea el dataset para poder ser leido como transacci?n write.csv(data_is_fiasco, file = "../data/data_para_reglas_de_asociacion.csv") #se lee el dataset data_reglas_asociacion <- read.transactions("../data/data_para_reglas_de_asociacion.csv", sep=",") #se aplica el m?todo de reglas de asociaci?n al dataset que cuenta con #el atributo is.fiasco rules9 <- apriori(data_reglas_asociacion, parameter=list(support=0.001, confidence=0.2)) rules9.sorted <- sort(rules9, by="lift") rules9.sorted.first10 <- head(rules9.sorted, 30) inspect(rules9.sorted.first10)Después de generar estos archivos .csv se aplican las reglas de asociación:
Con esto se obtienen los siguientes resultados:
Usando todos los datos se obtienen resultados que no aportan mucha información, como por ejemplo:
- {Muchas ventas Regionales} => {Muchas ventas globales}
- {Konami, muy pocas ventas en EU} => {Konami Digital Entertainment}
Considerando solo las ventas globales y no las regionales suceden cosas parecidas:
- {muy pocas Ventas globales, Ubisoft Montreal} => {Ubisoft}
Sin considerar las ventas tampoco se obtienen resultados muy significativos:
- {EA Canada, Puntajes usuarios alto} => {Electronic Arts}
Sin el género, la plataforma ni el año:
- {Konami, n° críticas usuarios muy bajo} => {Konami Digital Entertainment}
Sin el número de críticas se obtienen resultados un poco más interesantes, como por ejemplo:
- {Nintendo, Puntajes Críticos muy alto} => {Puntajes usuarios muy alto}
Lo cual a primera vista podría indicar que los juegos de Nintendo tienden a no ser un fiasco, sin embargo, esto es un error pues ambos puntajes podrían ser muy altos según los criterios definidos y aún así tener una diferencia de 2 puntos.
Al eliminar el desarrollador sucede algo parecido al caso anterior, aunque en el caso de Electronic Arts se observa que puntaje de usuarios alto suele estar relacionado con puntaje de críticos aún más alto:
- {Electronic Arts, Puntajes usuarios alto} => {Puntajes Críticos muy alto}
- {Nintendo, Puntajes Críticos muy alto} => {Puntajes usuarios muy alto}
Con el desarrollador pero sin publicador no se observan cambios significativos.
Al considerar el atributo Is_Fiasco se obtienen los resultados más interesantes, en los cuales se sugiere que el publicador puede estar muy relacionado con la clasificación del videojuego:
Esto último ayuda a validar las hipótesis iniciales, de que ciertos publicadores están más ligados a que un juego sea o no un fiasco, y consideramos que es valiosa información extraída del dataset.
Después de bastantes pruebas para encontrar un buen predictor de fiascos de videojuegos, nos encontramos con Random Forest, que tiene un puntaje f1 que sobresale por sobre el de base dummy, en especial aplicando oversampling y subsampling. Consideramos que la diferencia de puntaje obtenida lo vuelve un predictor adecuado con los parámetros manejados.
Lo aprendido durante este curso de clasificadores es un acercamiento práctico que abre las puertas a aplicar estas herramientas en próximos proyectos, y a seguir aprendiendo para refinar los resultados que se pueden obtener con más técnicas y estrategias.
El clustering aplicado en este proyecto no nos brindó mucha información nueva, pues consideramos que los datos utilizados no se adecuaban a un modelo en el que se pudieran identificar clusters naturalmente. A pesar de esto, nos sirvió para validar los datos utilizados, y para extraer información sobre su comportamiento.
Como trabajo no desarrollado, se podrían haber evaluado más métricas internas sobre clustering para validar o descartar su uso en nuestros datos. También se podrían haber buscado nuevas dimensiones sobre las cuales aplicar clustering que podrían haber aportado información.
De estas estrategia, si bien fue realizada como último intento de extraer datos, destacamos que tomando los datos correctos nos puede proporcionar valiosa información. La forma intuitiva de interpretar las reglas de asociación nos fue útil para validar nuestra hipótesis de la relación entre los publicadores de videojuegos y los fracasos.
Las herramientas que se trabajaron en el curso nos permitieron llevar a cabo un proyecto en el que logramos extraer una gran cantidad de información del data set trabajado. La evolución natural de este proyecto llevó a probar distintas formas de analizar los datos que se reflejaron en resultados diversos: por un lado un predictor relativamente eficaz que podría utilizarse a futuro, y por otro lado una caracterización de la información desde ángulos que no se manejaban anteriormente.