DEV Community

Symphony
Symphony

Posted on

Behind the scenes of the anime rankings: A quick look focused on studio and genre data (Spanish)

Este es mi primer análisis sobre un dataset, lo hice para reforzar mis conocimientos en manipulación de datos en pandas. Cualquier sugerencia o corrección será muy bienvenida. Espero que encuentres algo interesante en este pequeño análisis

Primero leeremos el CSV del dataset de MyAnimeList extraído el 2023

df_anime= pd.read_csv('/kaggle/input/myanimelist-dataset/anime-dataset-2023.csv')
Enter fullscreen mode Exit fullscreen mode

Procederemos con la limpieza de estos, por lo cual empezaremos en busqueda de datos nulos

df_anime.info()
df_anime.isna().any()
Enter fullscreen mode Exit fullscreen mode

Image description

Interesantemente no tenemos ningun valor nulo en el dataset, pero no es de algo el cual fiarse

df_anime.head()
Enter fullscreen mode Exit fullscreen mode

Image description

Bueno al parecer todo parece bien y correcto, ¿Cierto?. Por lo tanto que mejor manera de empezar la búsqueda de los mejores anime si no es por su rank. Así que los ordenare por su rank y dejare algo de datos interesantes a tener en cuenta, como el score, el scored by (cantidad de personas que han calificado con un score), la popularidad y etc.

anime_sorted_rank = df_anime.sort_values('Rank',ascending = True)[['Name','Score','Rank','Scored By','Popularity','Type','Genres','Synopsis']]
anime_sorted_rank.head()
Enter fullscreen mode Exit fullscreen mode

Image description

Vaya, al parecer no había nulos pero sí 'UNKNOWN' por ahí escondidos. Otra cosa a resaltar son los ranks en 0 que aparecen al parecer de la convinacion de un score y scored by en 'UNKNOWN'. Así que por ahora borraremos los valores 'UNKNOWN' de esas columnas y dejaremos solo los animes emitidos en TV (Que es por donde mas se transmiten semanalmente) y las películas que pueden ser igual de populares como los animes, aparte de que sería un 'sacrilegio' dejar de lado peliculas como Kimi no Na Wa o películas de los studios Ghibli
Porque no borramos tambien las filas que tienen los ranks en 'UNKNOWN' ya que si no tenemos su rank no nos deberia servir.. ¿Cierto?. Pues en cierta manera si estamos dandole una 'clasificación' por rank, pero esta esta relacionada a las columnas score y scored by. ¿Y porque no tomamos en cuenta el popularity, favorites ni los members para los ranks que estan en 'UNKNOWN?

Pues analizando un poco la pagina de MyAnimeList nos damos cuenta que hay unas diferencias de varios cientos de miles en cuanto a miembros en diferentes puestos del ranking actual en relacion a members, favorites y populariy. Por ejemplo actualmente (Enero 2023) en el top se encuentra Sousou no Frieren en el top 1 dejando a Fullmetal Alchemist Brodtherhood en segundo lugar, la diferencia es que la popularidad de Sousou no Frieren (#414) es menor contra la popularidad que FullMetal Alchemist (#3) y Shingeki no Kyojin se lleva el primer lugar en popularidad pero sigue siendo el puesto 112 en el ranking . En cuanto a members Sousou no Frieren no llega ni al medio millon cuando Fullmetal Alchemist tiene mas de 3 millones. Por lo cual descartaremos esas 3 columnas para la estimación de nuestro ranking gracias a los 'UNKNOWN' presentes.

Por ende necesitamos tanto el Score como el Scored By

df_anime = df_anime[(df_anime['Type'] == 'TV') | (df_anime['Type'] == 'Movie') ]
df_anime = df_anime[(df_anime['Score'] != 'UNKNOWN') | (df_anime['Scored By'] != 'UNKNOWN')]
df_anime
Enter fullscreen mode Exit fullscreen mode

Image description

Listo, con nuestro dataframe algo mas limpio deberíamos ahora si ordenar con tranquilidad

anime_sorted_rank = df_anime.sort_values('Rank',ascending = True)[['Name','Score','Rank','Scored By','Popularity','Type','Genres','English name','Aired','Studios','Synopsis']]
anime_sorted_rank.head()
Enter fullscreen mode Exit fullscreen mode

Image description

Ok, se ve muy bien ¿Cierto?. Pues mire más a detalle. El rank numero 1 lo tiene Fullmetal Alchemist: Brotherhood, pero... ¿Porque del rank 1 se salta al rank 10? y luego al rank 100.

Si analizamos mas de manera detallada se comporta como un String, y es porque lo es. Al parecer no nos percatamos que en el indice las columnas que deberian ser numeros (rank, score, scored by) estan como objects. Así que convertiremos Score y Scored by a números. Rank no porque tenemos valores en 'UNKNOWN' que los calcularemos a mano.

anime_sorted_rank.loc[:,['Score','Scored By']] = anime_sorted_rank.loc[:,['Score','Scored By']].astype(float)
anime_sorted_rank
Enter fullscreen mode Exit fullscreen mode

Image description

Bueno ahora debemos enfocarnos en sacar el rank real de nuestra data incluyendo todos los 'UNKNOWN' por la cual utilizaremos la formula de Ponderación de Puntuaciones. Con esto lograremos ponderar el 'score' basándonos en la cantidad de personas que han calificado el anime ('score it by'). Esto ayudaría a equilibrar los animes con puntuaciones altas pero con pocas calificaciones, frente a aquellos con muchas calificaciones pero quizás con una puntuación ligeramente más baja.
$$ \text{Puntuación Ponderada} = \text{Score} \times \left( \frac{\text{Score it by}}{\text{Máximo de Score it by en el dataset}} \right) $$

def PP (score, scoreby, maxscoreby):
    return score * (scoreby/maxscoreby)

max_score_by = anime_sorted_rank['Scored By'].max()
anime_clean_rank = anime_sorted_rank.copy()
anime_clean_rank['Rank'] = anime_clean_rank.apply(lambda row: PP(row['Score'], row['Scored By'], max_score_by), axis=1)
anime_clean_rank = anime_clean_rank.sort_values('Rank', ascending= False).reset_index(drop = True)
anime_clean_rank['Rank Number'] = range(1, len(anime_clean_rank) + 1)
anime_clean_rank = anime_clean_rank.loc[:,['Name','Rank Number','Rank','Score','Scored By','Type','Studios','Popularity','Genres','English name','Aired','Synopsis']]
anime_clean_rank
Enter fullscreen mode Exit fullscreen mode

Image description

Ahora solo nos falta algo que dejamos por alto, las fechas, si revisamos detalladamente la columna veremos que hay diferentes tipos de fechas, así que las estandarizaremos y solo tomaremos animes de 1995 para adelante. Dado que animes anteriores a esa fecha contienen poca cantidad de Scored By

import datetime
from datetime import date

def estandarizar_fechas(fecha):

    if " to " in fecha:
        fecha = fecha.split(" to ")[0]

    formats = ("%b %d, %Y", "%b %d %Y", "%m/%d/%Y", "%Y")

    for fmt in formats:
        try:
            return datetime.datetime.strptime(fecha, fmt).date()
        except ValueError:
            pass

    return datetime.date(1900, 1, 1)

anime_clean_rank["Aired"] = anime_clean_rank["Aired"].apply(estandarizar_fechas)
anime_clean_rank = anime_clean_rank[anime_clean_rank['Aired'] > date(1900, 1, 1)]
anime_clean_rank
Enter fullscreen mode Exit fullscreen mode

Image description

Otro tema interesante que podemos hacer para limpiar mejor nuestra data es limitar las fechas de emisión de los animes para evitar animes con poco rank o scored by por ser muy antiguos y poco conocidos. Pero... ¿Desde donde deberiamos cortar?

anime_year_helper = anime_clean_rank.copy()
anime_year_helper['Aired'] = pd.to_datetime(anime_year_helper['Aired'])
anime_year_helper['Year'] = anime_year_helper['Aired'].dt.year
score_by_year = anime_year_helper.groupby('Year')['Scored By'].mean()
fig, ax = plt.subplots(figsize=(7, 5))
score_by_year.plot(kind='line', ax=ax)

ax.set_xlabel('Año')
ax.set_ylabel('Score By Promedio')
ax.set_title('Score By Promedio por Año')

plt.xticks(rotation=45)
plt.tight_layout()
plt.show()
Enter fullscreen mode Exit fullscreen mode

Image description

Como podemos ver los animes a inicios de los '80's ya empiezan a tener un buen promedio de calificaciones por persona. Así que restringiremos nuestro dataframe a partir de esa fecha

anime_clean_rank = anime_clean_rank[anime_clean_rank['Aired'] > date(1980, 1, 1)]
anime_clean_rank
Enter fullscreen mode Exit fullscreen mode

Image description

Análisis exploratorio simple

Ahora si pasaremos a la realización de una análisis simple para encontrar datos interesantes dentro de este dataset

Analisís sobre studios

Viendo nuestro dataset nos daremos cuenta que un anime puede estar creado por diversos estudios, como es el caso de Neon Genesis Evangeion que fue animado por Gainax y Tatsunoko Production, por otro lado también vemos que la mayoría de animes tiene diversos géneros. Así que para un mejor análisis desestructuraremos esas columnas.

df_anime_expanded = anime_clean_rank.copy()

genres_expanded = df_anime_expanded["Genres"].str.get_dummies(sep=", ")
df_anime_expanded = df_anime_expanded.join(genres_expanded)
df_anime_expanded = df_anime_expanded.drop("Genres", axis=1)
df_anime_expanded = df_anime_expanded.rename(columns={'Type': 'Medio Emision'})
def split_studios(x):
    return x.split(", ")
df_anime_expanded["Studios"] = df_anime_expanded["Studios"].apply(split_studios)
df_anime_expanded = df_anime_expanded.explode('Studios').reset_index(drop=True)

df_anime_expanded.head()
Enter fullscreen mode Exit fullscreen mode

Image description

Los estudios involucrados en el top 200 animes hasta el 2023 vs. Studios involucrados en los bottom 200 animes hasta el 2023
muestra_top = df_anime_expanded.head(200)
top_studios = (
    muestra_top.groupby('Studios').Name.count()
        .reset_index(name='cantidad')
        .sort_values('cantidad', ascending = False)
        .head(10)
)

muestra_bottom = (
    df_anime_expanded[df_anime_expanded['Studios'] != 'UNKNOWN']
        .sort_values('Rank Number', ascending = False)
        .head(200)
)

bottom_studios = (
    muestra_bottom.groupby('Studios').Name.count()
        .reset_index(name='cantidad')
        .sort_values('cantidad', ascending = False)
        .head(10)
)


top_studios.plot(kind = 'bar', x = 'Studios', title='Top 10 studios involucrados en la creación del top 200 anime raking')
bottom_studios.plot(kind = 'bar', x= 'Studios', title='Top 10 studios involucrados en la creación del bottom 200 anime raking')
plt.show()
Enter fullscreen mode Exit fullscreen mode

Image description

Image description

Es interesante ver el top y el bottom pero la participación en ello no hace que sean los participantes en los animes con mejores puntuaciones en general. Ya que solo nos hemos medido de los 200 en cada lado. Así que para una visión general sacaremos el promedio de todo el data set

best_studios = df_anime_expanded.groupby('Studios')['Rank'].mean().reset_index()
best_studios_df = best_studios.rename(columns={'Rank': 'Average Rank'}).sort_values('Average Rank',ascending = False).reset_index(drop= True)
best_studios_df
best_studios_df.head(20).plot(kind= 'bar', x= 'Studios', y= 'Average Rank', title='Top 20 studios con mejor promedio en animes')
Enter fullscreen mode Exit fullscreen mode

Image description

Entonces ser parte del top 200 de animes no te da el mismo ranking que el promedio en todo el dataset de animes. Esto se puede deber a la cantidad de animes con una calificación considerable fuera del top o incluso participación dentro del top 200 animes pero que no llegaron al top 10 de studios

df_long = pd.melt(df_anime_expanded, id_vars=['Name', 'Studios', 'Rank'], 
                  value_vars=['Adventure', 'Avant Garde', 'Award Winning', 'Boys Love', 'Comedy',
       'Drama', 'Ecchi', 'Erotica', 'Fantasy', 'Girls Love', 'Gourmet',
       'Hentai', 'Horror', 'Mystery', 'Romance', 'Sci-Fi',
       'Slice of Life', 'Sports', 'Supernatural', 'Suspense', 'Action',
       'Adventure', 'Avant Garde', 'Award Winning', 'Boys Love', 'Comedy',
       'Drama', 'Ecchi', 'Fantasy', 'Girls Love', 'Gourmet', 'Horror',
       'Mystery', 'Romance', 'Sci-Fi', 'Slice of Life', 'Sports',
       'Supernatural', 'Suspense'],
                  var_name='Genre', value_name='Present')

# Filtra solo las filas donde un género está presente (asumiendo que usas 1 o True para indicar presencia)
df_long = df_long[df_long['Present'] == 1]

# Agrupa por estudio y género, y cuenta las ocurrencias
genre_count_by_studio = df_long.groupby(['Studios', 'Genre']).size().reset_index(name='Count')

top_genres_by_studio = genre_count_by_studio.groupby('Studios').apply(lambda x: x.nlargest(3, 'Count')).reset_index(drop=True)

top_genres_by_studio
Enter fullscreen mode Exit fullscreen mode

Image description

top_studios_animes = best_studios_df.head(10).loc[:,'Studios'].tolist()
bottom_studios_animes = best_studios_df.sort_values('Average Rank', ascending = True).head(50).loc[:,'Studios'].tolist()

top_gen_of_top = top_genres_by_studio[top_genres_by_studio['Studios'].isin(top_studios_animes)]
top_gen_of_bottom = top_genres_by_studio[top_genres_by_studio['Studios'].isin(bottom_studios_animes)]

top_to_graph = top_gen_of_top.groupby('Genre')['Count'].count().reset_index().sort_values('Count',ascending = False)
bottom_to_graph = top_gen_of_bottom.groupby('Genre')['Count'].count().reset_index().sort_values('Count',ascending = False)
top_to_graph.plot(kind='bar',x='Genre', y= 'Count', title='Generos más animados en el top 10 de studios')
bottom_to_graph.plot(kind='bar',x='Genre', y= 'Count', title='Generos más animados en el bottom 50 de studios')
plt.show()
Enter fullscreen mode Exit fullscreen mode

Image description

Image description

Al parecer los studios en el top 10 se caracterizado por animar animes de tipo Fantasy, Drama y Action. Mientras que los animes en el bottom 50 se centraron mas en Comedy, Adventure y Action.

Para terminar, veremos la distribución cuestión de géneros en los animes

Análisis sobre géneros

df_anime_only = df_anime_expanded.drop_duplicates(subset='Name', keep='first')
df_anime_only = pd.melt(df_anime_only, id_vars=['Name', 'Studios', 'Rank'], 
                  value_vars=['Adventure', 'Avant Garde', 'Award Winning', 'Boys Love', 'Comedy',
       'Drama', 'Ecchi', 'Erotica', 'Fantasy', 'Girls Love', 'Gourmet',
       'Hentai', 'Horror', 'Mystery', 'Romance', 'Sci-Fi',
       'Slice of Life', 'Sports', 'Supernatural', 'Suspense', 'Action',
       'Adventure', 'Avant Garde', 'Award Winning', 'Boys Love', 'Comedy',
       'Drama', 'Ecchi', 'Fantasy', 'Girls Love', 'Gourmet', 'Horror',
       'Mystery', 'Romance', 'Sci-Fi', 'Slice of Life', 'Sports',
       'Supernatural', 'Suspense'],
                  var_name='Genre', value_name='Present')
df_anime_only = df_anime_only[df_anime_only['Present'] == 1]
df_anime_only = df_anime_only.groupby(['Name','Rank', 'Genre']).size().reset_index(name='Count')
df_anime_only = df_anime_only.sort_values('Rank', ascending = False).reset_index(drop = True)
df_anime_only
Enter fullscreen mode Exit fullscreen mode

Image description

df_count_gender_anime = df_anime_only.groupby('Genre')['Count'].count().reset_index(name = 'Counter')
df_count_gender_anime = df_count_gender_anime.sort_values('Counter', ascending = False)
df_count_gender_anime.plot(kind='bar', x='Genre', y = 'Counter', title= 'Distribución de generos en toda la data de animes')

Enter fullscreen mode Exit fullscreen mode

Image description

Es interesante saber que el genero comedia es el mas frecuente en los animes, sin embargo no es el genero que los estudios con mejor ranking realizan frecuentemente.

Ahora por último veremos los géneros del top 100 animes en el ranking para ver si tiene una similitud con los géneros en general
top_animes = anime_clean_rank.head(100).loc[:,'Name'].tolist()
top_animes
df_top_animes = df_anime_only[df_anime_only['Name'].isin(top_animes)].drop('Count', axis=1)
df_top_animes_genre = df_top_animes.groupby('Genre').size().reset_index(name='Count').sort_values('Count', ascending = False)
df_top_animes_genre.plot(kind = 'bar', x = 'Genre', y = 'Count', title= 'Distribución de generos en el top 200 animes')
Enter fullscreen mode Exit fullscreen mode

Image description

Tomando el grafico en cuenta vemos como si tienen cierta similitud en action, fantasy, drama y adventure. Pero tienen una diferencia considerable en cuestión de Comedy. Lo cual nos resalta que el top 200 no se centra tanto en la comedia como todo el dataset de animes.

Gracias por tomarse la molestia de leer mi análisis. Espero que le haya resultado interesante y perspicaz. Su interés y sus comentarios son muy valiosos para mí, ya que me animan a seguir explorando y compartiendo mi pasión por el anime y el análisis de datos. Si tienes alguna idea, pregunta o sugerencia, no dudes en compartirla. Estoy deseando participar en debates interesantes y aprender también de tus puntos de vista. ¡Mantengamos la conversación!

Top comments (0)