import pandas as pd
import numpy as np
import seaborn as sns
Aqui eu estou apenas definindo algumas questões estáticas na apresentação dos gráficos
sns.set_style('whitegrid')
sns.set_palette('flare_r')
sns.color_palette('flare_r')
Afim de me aprofundar um pouco mais em data science, regressões e machine learning, baixei o conjunto de dados disponibilizado nesse link (por Nik Davis):
https://www.kaggle.com/nikdavis/steam-store-games
Dentro do arquivo de download, haviam 6 arquivos .csv. Dessa forma, vou primeiro dar uma olhadinha em cada 1 deles para ter mais noção do tipo e variação de dados que tenho a disposição.
df1 = pd.read_csv('Dados Brutos/steam.csv')
df1.head()
df1.shape
df1.info()
df1.describe()
df2 = pd.read_csv('Dados Brutos/steam_description_data.csv')
df2.head(10)
df2.shape
df2.describe()
df3 = pd.read_csv('Dados Brutos/steam_media_data.csv')
df3.head()
df4 = pd.read_csv('Dados Brutos/steam_requirements_data.csv')
df4.head()
df5 = pd.read_csv('Dados Brutos/steam_support_info.csv')
df5.head()
df6 = pd.read_csv('Dados Brutos/steamspy_tag_data.csv')
df6.head()
Deu pra dar uma boa olhada nas bases. Provavelmente a mais útil pro que tenho em mente será a primeira mesmo, porém, acho que existe bastante espaço pra fazer algum tipo de nuvem de palavras com a segunda, que contém descrições dos jogos, e talvez dê pra ter uma noção de qual a configuração ideal de computador pra rodar jogos da steam usando essas informações da quarta base.
É possível notar que, na primeira base, existe um campo de data. Ele está com a tipagem de 'object', porém, acredito que faça mais sentido avalia-lo como data.
df = df1.copy()
df.head()
Aparentemente, a primeira coluna possui os dados de identificação, então, provavelmente só sera utilizada para eventuais joins.
Já a segunda coluna apresenta os nomes dos jogos. Muito provavelmente só usaremos para identificação também, dessa forma...
Podemos começar a avaliar as colunas a partir da terceira, que possui data.
df['release_date'] = pd.to_datetime(df['release_date'])
df.info()
Agora que o campo 'release_date' está com a tipagem correta, vamos dar uma olhada nas datas e tentar descobrir algumas informações acerca da idade dos jogos.
df['release_date'].max()
df['release_date'].min()
df[df['release_date'] == df['release_date'].max()]
df[df['release_date'] == df['release_date'].min()]
Aqui é interessante ter uma ideia da distribuição dos jogos ao longo dos anos. Podemos supor, em um primeiro momento, que, uma vez que a Steam foi lançada por volta de 2003, e, começou a incluir jogos de plataformas como Linux e MacOS depois de 2009, provavelmente exista uma maior distribuição de jogos conforme os anos avançam.
PT-BR | EN |
df['Year'] = df['release_date'].apply(lambda x: x.year)
ax = sns.histplot(data=df, x='Year', stat='probability', discrete=True)
ax.figure.set_size_inches(15,6)
Esse dataset foi extraido durante o mês de maio de 2019, por isso, é notável essa queda ao fim do gráfico (podemos imaginar que a coluna referente ao ano de 2019 seria maior se os dados tivessem sido extraidos em dezembro). Podemos dar uma olhada na quarta coluna
df['english'].unique()
Aparentemente, só possui 2 valores, indicando se o jogo tem suporte a lingua inglesa ou não. Vale a pena dar uma olhada nos jogos que não possuem esse suporte.
df[df['english'] == 0]
df[df['english'] == 0].count()
Boa parte deles parece ter vindo de países asiáticos. Interessante. Existe a possibilidade de, devido a esses jogos serem produzidos em países asiáticos, o valor dos mesmos seja menor, uma vez que, na média, o valor das moedas asiaticas é menor que da moeda americana ou européia. Além disso, existe também a possibilidade de a ausência de suporte a lingua inglesa indicar limitações no capital investido na confecção do jogo.
Podemos dar uma olhada melhor nesse seleto grupo em outro momento, por enquanto, vamos prosseguir.
df['developer'].unique()
df['developer'].mode()
Essa moda não reflete, de fato, a moda real do conjunto de desenvolvedores, já que vários deles estão concatenados na mesma coluna.
desenv_split = df['developer'].apply(lambda x: pd.Series(x.split(';')))
desenv_split
desenv = []
for index, row in desenv_split.iterrows():
for i in desenv_split.columns:
desenv.append(row[i])
(Essa foi a solução que eu achei para listar todas as desenvolvedoras. Não foi das mais elegantes, então, se você tiver alguma ideia pra substituir essa monstruosidade, eu estou aberto a sugestões)
desenv = pd.Series(desenv)
desenv.dropna(inplace=True)
desenv.mode()
Agora temos a verdadeira moda (que, ironicamente, era a mesma de antes).
Infelizmente, esses dados são muito variados, então, acho improvável transforma-los em variáveis binárias ou ordinais. Dessa forma, existe um maior proveito em simplesmente tentar identificar algumas desenvolvedoras mais proeminentes.
desenv.value_counts().head(10)
desenv.value_counts().tail(10)
Provavelmente teremos o mesmo problema com a publisher.
df['publisher'].unique()
df['publisher'].mode()
publi_split = df['publisher'].apply(lambda x: pd.Series(x.split(';')))
publi_split
publi = []
for index, row in publi_split.iterrows():
for i in publi_split.columns:
publi.append(row[i])
publi = pd.Series(publi)
publi.dropna(inplace=True)
publi.mode()
publi.value_counts().head(10)
publi.value_counts().tail(10)
Algumas dessas informações são bem interessantes. Particularmente, eu não conhecia algumas dessas empresas.
Agora, seguiremos para avaliar as plataformas
df['platforms'].unique()
Meu palpite é de que, jogos que estão disponíveis em mais plataformas provavelmente venderiam mais, o que pode acarretar no número maior de owners.
Pensando em como tratar esse campo, talvez seja interessante criar 3 novas colunas de valor binário, pra determinar se um dado jogo está disponível para essa plataforma ou não.
df['platforms_windows'] = df['platforms'].str.contains('windows', regex=False).astype(int)
df['platforms_mac'] = df['platforms'].str.contains('mac', regex=False).astype(int)
df['platforms_linux'] = df['platforms'].str.contains('linux', regex=False).astype(int)
df.tail(10)
Essa transformação nos permite, também, ter uma noção de quanto jogos estão disponíveis em cada plataforma, simplesmente efetuando uma soma.
df['platforms_windows'].sum()
df['platforms_mac'].sum()
df['platforms_linux'].sum()
Aparentemente, existem mais jogos disponíveis para windows, e, por último, para linux. Acho que é interessante calcular a correlação entre essas 3 variáveis, pra evitar problemas de multicolinearidade futuros.
df[['platforms_windows','platforms_mac','platforms_linux']].corr()
Existe uma correlação razoável entre mac e linux. Minha hipótese é a de que, uma vez que windows possui mais entradas, provavelmente quando uma desenvolvedora decide produzir para alguma plataforma, ela opta pelo windows, e, quando quer desenvolver para mais de uma, acaba por optar por desenvolver para linux E mac, e não linux OU mac. Podemos dar uma olhada novamente na coluna platforms, pra ter uma ideia melhor.
df[df['platforms'] == 'windows;mac;linux'].shape[0]
df[df['platforms'] == 'mac;linux'].shape[0]
df[df['platforms'] == 'windows;mac'].shape[0]
df[df['platforms'] == 'windows;linux'].shape[0]
Como eu pensei. Apesar de poucos jogos serem lançados somente para mac e linux (1), boa parte deles é lançado juntamente com windows. Isso significa que, uma vez que o jogo saia para linux, é esperado também que saia para mac, e o inverso é, em menor grau, também verdade.
Agora, vamos ver a variável 'required_age'
df['required_age'].unique()
O campo 'required_age' está em formato numérico, o que pode facilitar a nossa eventual regressão.
ax = sns.histplot(data=df, x='required_age', discrete=True, stat='probability')
ax.figure.set_size_inches(15,6)
ax.set(xticks=range(0,20,2))
Parace que a grande maioria dos jogos na nossa amostra não requer idade mínima. Pode ser interessante, mais pra frente, dar uma olhada na quantidade de pessoas que detem jogos nas últimas duas faixas de idade, por curiosidade.
Agora, vamos prosseguir para a próxima variável.
df['categories'].unique()
len(df['categories'].unique().tolist())
Parece que existe uma grande quantidade de variações de categorias. Vou tentar utilizar a mesma técnica que utilizamos para separar developers e publishers, e, dependendo de como ficar, talvez seja viável transforma-las em variáveis binárias, como no caso das platforms.
categ_split = df['categories'].apply(lambda x: pd.Series(x.split(';')))
categ_split
Parece que um mesmo jogo pode ter até 18 categorias diferentes. Apesar de serem muitas, eu acredito que isso possa influenciar tanto a quantidade de pessoas que detem o jogo, como o seu preço. Acho que vou ter que criar colunas binárias pra eles então...
categ = []
for index, row in categ_split.iterrows():
for i in categ_split.columns:
categ.append(row[i])
categ = pd.Series(categ)
categ.dropna(inplace=True)
categ.unique()
len(categ.unique())
Parece que me precipitei. Essencialmente, existem 29 categorias diferentes, só nesta amostra. Talvez seja sim possível que um jogo tenha mais de 18 categorias então. POrém, é perceptível que algumas dessas categorias parecem um pouco redundantes. Talvez seja interessante calcular correlação entre elas. Também pode ser interessante calcular a quantidade de categorias que cada jogo tem, já que o simples número de categorias pode ser um indicador também.
categ.mode()
categ.value_counts().head(10)
categ.value_counts().tail(10)
categ.unique().tolist()
df_corr = pd.DataFrame()
df_corr
for i in categ.unique().tolist():
nome = 'categories_'+i
df[nome] = df['categories'].str.contains(i, regex=False).astype(int)
df_corr[nome] = df['categories'].str.contains(i, regex=False).astype(int)
df_corr.columns
df['categories_QTD'] = 1+ df['categories'].str.contains(';', regex=False).astype(int)
df_corr['categories_QTD'] = 1+ df['categories'].str.contains(';', regex=False).astype(int)
df_corr.head(10)
df_corr.corr()
Convenhamos, essa tabela de correlação ficou muito grande, meio inviável de se avaliar. Por isso, vou construir um heatmap abaixo pra ter uma noção um pouco melhor.
ax = sns.heatmap(df_corr.corr(), linewidths=.2, xticklabels=True, yticklabels=True)
ax.figure.set_size_inches(15,15)
Felizmente, a maioria das correlações não foi tão alta, porém, houveram algumas aqui e ali que incomodaram um pouco. Em especial, as colunas:
Variável A | Variável B |
---|---|
'categories_Mods' | 'categories_Mods (require HL2)' |
'categories_Shared/Split Screen' | 'categories_Local Co-op' |
'categories_Online Co-op' | 'categories_Online Multi-Player' |
'categories_Co-op' | 'categories_Local Co-op' |
'categories_Co-op' | 'categories_Online Co-op' |
Houveram mais algumas correlações, mas, acredito que com essas que elenquei, já podemos ter uma ideia do que eventualmente jogar fora e o que manter. Já adicionamos as colunas binárias ao dataframe, então, podemos prosseguir para a próxima coluna.
df.columns
df['genres'].unique()
len(df['genres'].unique())
Parece que teremos que fazer a mesma coisa que fizemos nas últimas variáveis qualitativas.
gen_split = df['genres'].apply(lambda x: pd.Series(x.split(';')))
gen_split
gen = []
for index, row in gen_split.iterrows():
for i in gen_split.columns:
gen.append(row[i])
gen = pd.Series(gen)
gen.dropna(inplace=True)
len(gen.unique())
gen.unique()
gen.mode()
gen.value_counts().head(10)
gen.value_counts().tail(10)
Nik Davis, o cara que extraiu essa base, tinha comentado que poderiam haver alguns jogos que não eram realmente jogos, e sim software disponível na steam. Me parece que esses gêneros encontrados com a função tail() podem estar relacionados a esses softwares.
gen.value_counts().tail(50)
gen.value_counts().tail(13).index.tolist()
Esses parecem ser os gêneros a serem verificados. Vou filtrar o dataframe para dar uma olhada nos titulos desses "jogos".
nomes = []
for i in gen.value_counts().tail(13).index.tolist():
nomes.extend(df[df['genres'].str.contains(i)]['name'].tolist())
set(nomes)
len(set(nomes))
Podemos ver que o número não é tão expressivo. E, de fato, varios dos "jogos" são, claramente, softwares de criação de jogos e afins. Vamos ver quais são as categorias ligadas a esses jogos.
df.columns
df[df['name'].isin(nomes)]['categories'].unique()
df[df['name'].isin(nomes)]['categories'].mode()
df[df['name'].isin(nomes)]['categories'].value_counts().head(10)
Considerando tanto as categorias como os gêneros, é possível afirmar com um pouco mais de propriedade que essas entradas são, de fato, em grande maioria, ferramentas de desenvolvimento de jogos. Como o volume também é baixo, vou exclui-las do dataframe.
df = df[~df['name'].isin(nomes)]
df.head()
Agora que limpamos a base de prováveis entradas de software, vale a pena reiniciar o processo de separação dos gêneros. Imagino que, como sobraram menos gêneros, talvez seja possível transforma-los em binário também.
df['genres'].unique()
gen_split = df['genres'].apply(lambda x: pd.Series(x.split(';')))
gen_split
gen = []
for index, row in gen_split.iterrows():
for i in gen_split.columns:
gen.append(row[i])
gen = pd.Series(gen)
gen.dropna(inplace=True)
gen.mode()
gen.unique()
len(gen.unique())
gen.value_counts().head(10)
gen.value_counts().tail(10)
df_corr = pd.DataFrame()
df_corr
for i in gen.unique().tolist():
nome = 'genres_'+i
df[nome] = df['genres'].str.contains(i, regex=False).astype(int)
df_corr[nome] = df['genres'].str.contains(i, regex=False).astype(int)
df_corr.head(10)
df_corr.corr()
ax = sns.heatmap(df_corr.corr(), linewidths=.2, xticklabels=True, yticklabels=True)
ax.figure.set_size_inches(15,15)
Nesse caso, houveram apenas 2 "grandes" correlações.
Variável A | Variável B |
---|---|
genres_Nudity | genres_Sexual Content |
genres_Violent | genres_Gore |
Provavelmente teremos que escolher 1 variável de cada linha para utilizar na eventual regressão. Vamos para a próxima variável
df['achievements'].unique()
Pelo que pode ser visto, esse campo já está em formato numérico, o que facilita muita coisa. Podemos dar uma olhada no seu histograma, apenas pra ter uma noção melhor da distribuição dos dados.
ax = sns.histplot(data=df['achievements'])
ax.figure.set_size_inches(15,6)
Parece que o histograma não foi muito útil como forma de visualização desses dados. Vamos tentar o boxplot.
ax = sns.boxplot(data=df['achievements'])
ax.figure.set_size_inches(15,6)
df['achievements'].mean()
df['achievements'].std()
df['achievements'].var()
df['achievements'].max()
df['achievements'].min()
df['achievements']
df['achievements'].dropna()
Bom, parece que esse campo é um pouco estranho mesmo. O desvio padrão é bem alto, o que indica que poucos valores de achievements se repetem entre os jogos, e que eles não estão exatamente próximos um dos outros.
Vamos pra próxima variável.
df['positive_ratings'].unique()
len(df['positive_ratings'].unique())
df['positive_ratings'].max()
df['positive_ratings'].min()
df['positive_ratings'].mean()
df['positive_ratings'].std()
df['positive_ratings'].var()
Tentei plotar os gráficos de histograma e boxplot, mas, não deu muito certo, muito tempo de processamento, preferi retirar da análise.
Esse campo diz qual a quantidade de análises positivas que um dado jogo tem. Ele tem um par, que é o campo 'negative_ratings'. Podemos verificar se existe alguma correlação forte entre eles.
df['negative_ratings'].unique()
len(df['negative_ratings'].unique())
df['negative_ratings'].max()
df['negative_ratings'].min()
df['negative_ratings'].mean()
df['negative_ratings'].std()
df['negative_ratings'].var()
Vamos a correlação
df[['negative_ratings','positive_ratings']].corr()
Como já era de se esperar, a correlação entre as avaliações realmente é consideravel, porém, é interessante notar que elas tem uma correlação positiva. Isso pode indicar que elas são, na verdade, proporcionais a quantidade de avaliações. Como elas tem uma correlação bem alta, vou criar uma terceira variável, que será a subtração dessas duas, e vou usar ela na eventual regressão.
df['delta_ratings'] = df['positive_ratings'] - df['negative_ratings']
df['delta_ratings'].unique()
len(df['delta_ratings'].unique())
df['delta_ratings'].max()
df['delta_ratings'].min()
df['delta_ratings'].mean()
df['delta_ratings'].std()
df['delta_ratings'].var()
Vou calcular também uma nota média baseada no percentual de votos positivos.
df['%_ratings'] = df['positive_ratings']/(df['positive_ratings'] + df['negative_ratings'])
df['%_ratings'].unique()
len(df['%_ratings'].unique())
df['%_ratings'].max()
df['%_ratings'].min()
df['%_ratings'].mean()
Parece que encontramos um outro ponto de atenção. É bem provável que essas entradas com nota percentual 1 e 0 sejam, na verdade, casos com poucas avaliações. Será que o número de avaliações pode ter influência no seu preço? Na quantidade, eu imagino que tenha.
df['QTD_ratings'] = (df['positive_ratings'] + df['negative_ratings'])
Como comentado acima, vamos dar uma olhada nas entradas com '%_ratings' com valor 1 e 0
rating_1 = df[df['%_ratings'] == df['%_ratings'].max()][['name','positive_ratings','negative_ratings','owners','QTD_ratings']].copy()
rating_1
rating_0 = df[df['%_ratings'] == df['%_ratings'].min()][['name','positive_ratings','negative_ratings','owners','QTD_ratings']].copy()
rating_0
df[['delta_ratings','%_ratings','QTD_ratings']].corr()
Como existe uma correlação alta entre QTD_ratings e delta_ratings, vou usar só o QTD_ratings.
rating_1['owners'].unique()
rating_0['owners'].unique()
df['owners'].unique()
Como visto acima, a maioria dessas entradas apresenta valores comparativamente baixos no campo "owners", que são as pessoas que detem esses jogos em suas bibliotecas. Podemos analisar e fazer alguns tratamentos no campo 'owners', pra, depois, ter uma ideia de qual o percentual médio de pessoas detentoras do jogo que o avaliam.
len(df['owners'].unique())
own_split = df['owners'].apply(lambda x: pd.Series(x.split('-')))
df['owners_mean'] = (own_split[0].astype(int)+own_split[1].astype(int))/2
df['owners_mean']
rating_1 = df[df['%_ratings'] == df['%_ratings'].max()][['name','positive_ratings','negative_ratings','owners','QTD_ratings','owners_mean']]
rating_0 = df[df['%_ratings'] == df['%_ratings'].min()][['name','positive_ratings','negative_ratings','owners','QTD_ratings','owners_mean']]
rating_1['owners_mean'].unique()
rating_0['owners_mean'].unique()
temp = df[['QTD_ratings','positive_ratings','owners_mean']].groupby('owners_mean').mean().round(2)
temp
temp.reset_index(inplace=True)
temp
temp['QTD_ratings'].astype(float)/temp['owners_mean'].astype(float)
(temp['QTD_ratings'].astype(float)/temp['owners_mean'].astype(float)).mean()
temp['positive_ratings'].astype(float)/temp['owners_mean'].astype(float)
Podemos perceber que o percentual de owners que avalia os jogos não é exatamente constante entre todas as médias.
len(df['owners_mean'])
df['owners_mean'].hist()
len(df[df['owners_mean'] == df['owners_mean'].min()]['owners_mean'])
df[df['owners_mean'] == df['owners_mean'].min()]['owners_mean']
Parece que a grande maioria dos jogos nesse dataset tem, em média, 10 mil owners. Acho que vale a pena separar os dados em 3 grupos então, 1 somente com jogos com mais de 10 mil owners de média, um com todos os jogos, e outro com somente os jogos com média de 10 mil owners. Pra não atrapalhar o fluxo até agora, vou deixar pra fazer essa separação depois de ter efetuado as análises nas outras variáveis.
df["average_playtime"].unique()
len(df["average_playtime"].unique())
df["average_playtime"].head()
df["average_playtime"].max()
df["average_playtime"].min()
Parece que existem jogos que foram jogados, em média, 0 horas. Acho que é uma boa ideia dar uma olhadinha neles.
df[df["average_playtime"] == df["average_playtime"].min()][['name','average_playtime','median_playtime']]
df[df["average_playtime"] == df["average_playtime"].min()][['name','average_playtime','median_playtime']].shape
A grande maioria dos jogos não possui horas significativas de jogatina média ou mediana. Provavelmente não vai ajudar muito considerar o tempo de jogatina de cada jogo. Por via das dúvidas, vou criar uma coluna que apenas computa se o average_playtime é maior que 0.
df["average_playtime_>0"] = df["average_playtime"].apply(lambda x: x>0).astype(int)
ax = sns.histplot(df["average_playtime"], stat='probability')
ax.figure.set_size_inches(15,6)
Bom, por fim, falta avaliarmos a variável 'price'. Lembrando que os preços encontrados aqui estão em libra.
df['price'].unique().round(2)
len(df['price'].unique())
df['price'].hist()
from pingouin import normality
normality(df['price'],method='normaltest')
df['log_price'] = np.log(df['price']+1)
df['log_price'].hist()
df['log_log_price'] = np.log(df['log_price']+1)
df['log_log_price'].hist()
Agora que todas as variáveis foram avaliadas, vou exportar 3 bases diferentes, baseado na média de owners, como dito anteriormente. Farei as regressões em outro arquivo.
Colunas = ['appid','name','Year','english','platforms_windows','platforms_mac','platforms_linux','genres_Action',
'genres_Free to Play', 'genres_Strategy','genres_Adventure', 'genres_Indie', 'genres_RPG',
'genres_Casual','genres_Simulation', 'genres_Racing', 'genres_Violent','genres_Massively Multiplayer',
'genres_Nudity', 'genres_Sports', 'genres_Early Access', 'genres_Gore', 'genres_Sexual Content',
'categories_Multi-player', 'categories_Online Multi-Player','categories_Local Multi-Player',
'categories_Valve Anti-Cheat enabled','categories_Single-player', 'categories_Steam Cloud',
'categories_Steam Achievements', 'categories_Steam Trading Cards','categories_Captions available',
'categories_Partial Controller Support','categories_Includes Source SDK','categories_Cross-Platform Multiplayer',
'categories_Stats','categories_Commentary available', 'categories_Includes level editor',
'categories_Steam Workshop', 'categories_In-App Purchases','categories_Co-op',
'categories_Full controller support','categories_Steam Leaderboards', 'categories_SteamVR Collectibles',
'categories_Online Co-op', 'categories_Shared/Split Screen','categories_Local Co-op', 'categories_MMO',
'categories_VR Support','categories_Mods', 'categories_Mods (require HL2)',
'categories_Steam Turn Notifications','achievements','%_ratings','QTD_ratings','owners_mean',
'average_playtime_>0','price','log_price','log_log_price']
output_owners_min = df[df['owners_mean'] == df['owners_mean'].min()][Colunas]
output_owners = df[df['owners_mean'] != df['owners_mean'].min()][Colunas]
output_geral = df[Colunas]
output_owners_min.to_csv('Dados Tratados/output_owners_min.csv', sep=';')
output_owners.to_csv('Dados Tratados/output_owners.csv', sep=';')
output_geral.to_csv('Dados Tratados/output_geral.csv', sep=';')
Dados tratados e discriminados, agora vamos para as regressões... no arquivo "Regressões.ipynb" dentro da mesma pasta, ou, acessando ao link: https://bhscjhdvds.github.io/Minor/Regress%C3%B5es.html