3 · Análise exploratória

Recorrência, elenco, idade dos filmes e avaliações

Exploração da base enriquecida de filmes exibidos na Sessão da Tarde.

Esta etapa investiga os padrões propostos no início do projeto. O recorte parte da base enriquecida e conserva apenas registros com título, data de lançamento e avaliação válidos; também exclui casos em que o lançamento aparece depois da exibição.

Os números abaixo descrevem os arquivos versionados neste repositório. A fonte de exibições é comunitária, e as avaliações do TMDB são um retrato do momento em que os dados foram coletados.

Preparação do recorte

Ver código
import ast
from collections import Counter

import matplotlib.pyplot as plt
import pandas as pd
import seaborn as sns
from sklearn.cluster import KMeans
from sklearn.metrics import silhouette_score
from sklearn.preprocessing import StandardScaler

sns.set_theme(style="whitegrid", context="notebook", palette="colorblind")
plt.rcParams["figure.figsize"] = (9, 5.2)
Ver código
df_raw = pd.read_csv("datasets/sdtcentury2.csv")

df = df_raw.dropna(
    subset=["title", "english_title", "release_date", "vote_average"]
).copy()

df["date"] = pd.to_datetime(df["date"], errors="coerce")
df["release_date"] = pd.to_datetime(df["release_date"], errors="coerce")
df["year"] = df["date"].dt.year
df["release_year"] = df["release_date"].dt.year
df["diff"] = df["year"] - df["release_year"]

invalid_dates = df["date"].isna() | df["release_date"].isna()
future_releases = df["diff"] < 0
df = df.loc[~invalid_dates & ~future_releases].copy()

pd.DataFrame(
    {
        "etapa": ["Base enriquecida", "Recorte analítico"],
        "registros": [len(df_raw), len(df)],
        "títulos distintos": [
            df_raw["title"].nunique(dropna=True),
            df["title"].nunique(dropna=True),
        ],
    }
)
etapa registros títulos distintos
0 Base enriquecida 8521 3205
1 Recorte analítico 7540 2866

O recorte evita tratar ausências e combinações cronologicamente impossíveis como observações válidas. Ele é usado nas seções seguintes, exceto quando a pergunta requer elenco, que tem sua própria filtragem.

Ver código
df[["date", "title", "release_date", "vote_average", "cast"]].head()
date title release_date vote_average cast
0 2023-01-18 Os Parças 2 2019-11-14 7.100 ['Tom Cavalcante', 'Whindersson Nunes', 'Tirul...
1 2023-03-14 Uma Amizade Inesperada 2020-12-17 6.977 ['Hong Chau', 'Lucas Jaye', 'Brian Dennehy']
2 2023-03-30 Motoqueiros Selvagens 1970-07-08 5.000 ['Tom Baker', 'William Smith', 'Carl Steppling']
3 2023-02-21 Até que a Sorte nos Separe 2012-10-05 6.400 ['Leandro Hassum', 'Danielle Winits', 'Kiko Ma...
4 2023-02-02 Arthur: O Milionário Irresistível 2011-04-08 5.601 ['Russell Brand', 'Helen Mirren', 'Greta Gerwig']

Quais filmes mais se repetem?

Ver código
top_titles = (
    df["title"]
    .value_counts()
    .head(10)
    .rename_axis("filme")
    .reset_index(name="exibições")
)
top_titles
filme exibições
0 De Volta À Lagoa Azul 18
1 De Repente 30 18
2 A Família Buscapé 15
3 Curtindo A Vida Adoidado 14
4 Matilda 14
5 Um Dia Especial 14
6 O Paizão 14
7 Táxi 14
8 Click 14
9 As Aparências Enganam 13
Ver código
ax = sns.barplot(
    data=top_titles.sort_values("exibições"),
    x="exibições",
    y="filme",
    color="#176b95",
)
ax.set(
    title="Títulos com mais registros de exibição",
    xlabel="Exibições no recorte",
    ylabel=None,
)
sns.despine()
plt.tight_layout()
plt.show()

“De Repente 30” e “De Volta à Lagoa Azul” ocupam o topo deste recorte, com 18 registros cada. A contagem mede ocorrências na base depois das validações descritas acima.

Quem aparece nos elencos?

Ver código
cast_df = df.dropna(subset=["cast"]).copy()
cast_df["cast"] = cast_df["cast"].apply(ast.literal_eval)

all_cast = Counter(name for cast in cast_df["cast"] for name in cast)
first_credit = Counter(cast[0] for cast in cast_df["cast"] if cast)

actor_summary = pd.DataFrame(
    {
        "todos os créditos disponíveis": dict(all_cast.most_common(10)),
        "primeiro crédito": dict(first_credit.most_common(10)),
    }
).fillna("—")
actor_summary
todos os créditos disponíveis primeiro crédito
Renato Aragão 135.0 135.0
Eddie Murphy 122.0 73.0
Dedé Santana 105.0
Adam Sandler 88.0 88.0
Whoopi Goldberg 82.0 67.0
Jackie Chan 78.0 76.0
Mussum 75.0
Anne Hathaway 74.0
Steve Guttenberg 72.0
Tom Hanks 66.0 61.0
Martin Lawrence 48.0
Arnold Schwarzenegger 48.0
Michael J. Fox 48.0
Will Smith 45.0
Ver código
top_first_credit = pd.DataFrame(
    first_credit.most_common(10),
    columns=["pessoa", "exibições"],
)

ax = sns.barplot(
    data=top_first_credit.sort_values("exibições"),
    x="exibições",
    y="pessoa",
    color="#e9ad2f",
)
ax.set(
    title="Pessoas mais recorrentes no primeiro crédito disponível",
    xlabel="Exibições no recorte",
    ylabel=None,
)
sns.despine()
plt.tight_layout()
plt.show()

Renato Aragão aparece com maior frequência tanto na soma dos créditos armazenados quanto na primeira posição dessas listas. A leitura deve considerar que o CSV guarda apenas o elenco disponível no enriquecimento, e não necessariamente os créditos completos de cada filme.

Quanto tempo separa lançamento e exibição?

Ver código
df["diff"].describe().rename(
    {
        "count": "registros",
        "mean": "média",
        "std": "desvio padrão",
        "min": "mínimo",
        "25%": "25%",
        "50%": "mediana",
        "75%": "75%",
        "max": "máximo",
    }
)
registros        7540.000000
média              11.562334
desvio padrão       9.641111
mínimo              0.000000
25%                 6.000000
mediana             9.000000
75%                14.000000
máximo             85.000000
Name: diff, dtype: float64
Ver código
fig, axes = plt.subplots(1, 2, figsize=(11, 4.5))

sns.histplot(data=df, x="diff", bins=35, color="#176b95", ax=axes[0])
axes[0].set(
    title="Intervalo entre lançamento e exibição",
    xlabel="Anos",
    ylabel="Registros",
)

yearly_gap = df.groupby("year", as_index=False)["diff"].median()
sns.lineplot(data=yearly_gap, x="year", y="diff", color="#123f68", ax=axes[1])
axes[1].set(
    title="Mediana do intervalo por ano de exibição",
    xlabel="Ano da exibição",
    ylabel="Mediana em anos",
)

sns.despine()
plt.tight_layout()
plt.show()

A mediana do recorte é de 9 anos entre lançamento e exibição. A distribuição é assimétrica e chega a intervalos muito maiores; por isso a mediana anual é mais legível do que uma média isolada.

Avaliações e idade do filme

Ver código
gap_labels = ["0–5", "6–10", "11–25", "26–50", "51–100"]
df["gap_group"] = pd.cut(
    df["diff"],
    bins=[-1, 5, 10, 25, 50, 100],
    labels=gap_labels,
)

gap_summary = (
    df.groupby("gap_group", observed=True)["vote_average"]
    .agg(registros="count", média="mean", mediana="median", mínimo="min", máximo="max")
    .round(2)
)
gap_summary
registros média mediana mínimo máximo
gap_group
0–5 1863 5.82 6.06 0.0 10.00
6–10 2593 6.00 6.10 0.0 10.00
11–25 2456 6.15 6.27 0.0 10.00
26–50 546 6.13 6.40 0.0 8.36
51–100 82 4.38 6.50 0.0 7.60
Ver código
fig, axes = plt.subplots(1, 2, figsize=(12, 4.8))

sns.histplot(data=df, x="vote_average", bins=30, color="#176b95", ax=axes[0])
axes[0].set(
    title="Distribuição das avaliações",
    xlabel="Avaliação média no TMDB",
    ylabel="Registros",
)

sns.boxplot(
    data=df,
    x="gap_group",
    y="vote_average",
    color="#dceef6",
    showfliers=False,
    ax=axes[1],
)
axes[1].set(
    title="Avaliação por intervalo até a exibição",
    xlabel="Anos entre lançamento e exibição",
    ylabel="Avaliação média no TMDB",
)

sns.despine()
plt.tight_layout()
plt.show()

Ver código
correlations = df[["diff", "year", "release_year", "vote_average"]].corr()

ax = sns.heatmap(
    correlations,
    annot=True,
    fmt=".2f",
    cmap="vlag",
    center=0,
    square=True,
)
ax.set_title("Correlação entre variáveis numéricas")
plt.tight_layout()
plt.show()

As faixas permitem comparar distribuições sem sugerir causalidade. A avaliação vem de uma fonte externa e não mede a recepção específica da exibição na televisão; a correlação apenas resume associação linear dentro deste recorte.

Uma exploração de agrupamentos

O notebook original testava k-means com idade e avaliação. Aqui as variáveis são padronizadas antes do ajuste, para que a escala em anos não domine a escala de avaliação. O exercício é descritivo: não prevê futuras exibições e não define categorias naturais de filmes.

Ver código
cluster_data = df[["diff", "vote_average"]].dropna()
scaled = StandardScaler().fit_transform(cluster_data)

cluster_diagnostics = []
for k in range(2, 7):
    model = KMeans(n_clusters=k, n_init="auto", random_state=42)
    labels = model.fit_predict(scaled)
    cluster_diagnostics.append(
        {
            "grupos": k,
            "inércia": model.inertia_,
            "silhouette": silhouette_score(scaled, labels),
        }
    )

cluster_diagnostics = pd.DataFrame(cluster_diagnostics)
cluster_diagnostics.round(3)
grupos inércia silhouette
0 2 10938.466 0.666
1 3 6746.482 0.549
2 4 4938.138 0.366
3 5 4280.751 0.320
4 6 3492.788 0.341
Ver código
fig, axes = plt.subplots(1, 2, figsize=(11, 4.4))

sns.lineplot(
    data=cluster_diagnostics,
    x="grupos",
    y="inércia",
    marker="o",
    color="#176b95",
    ax=axes[0],
)
axes[0].set(title="Inércia por número de grupos", xlabel="Grupos", ylabel="Inércia")

sns.lineplot(
    data=cluster_diagnostics,
    x="grupos",
    y="silhouette",
    marker="o",
    color="#e9ad2f",
    ax=axes[1],
)
axes[1].set(
    title="Separação média dos grupos",
    xlabel="Grupos",
    ylabel="Silhouette",
)

sns.despine()
plt.tight_layout()
plt.show()

Ver código
final_model = KMeans(n_clusters=2, n_init="auto", random_state=42)
cluster_plot = cluster_data.assign(grupo=final_model.fit_predict(scaled).astype(str))

ax = sns.scatterplot(
    data=cluster_plot,
    x="diff",
    y="vote_average",
    hue="grupo",
    alpha=0.55,
    s=32,
)
ax.set(
    title="Agrupamento exploratório: intervalo e avaliação",
    xlabel="Anos entre lançamento e exibição",
    ylabel="Avaliação média no TMDB",
)
ax.legend(title="Grupo")
sns.despine()
plt.tight_layout()
plt.show()

O que esta etapa sustenta

  • Há títulos claramente recorrentes no recorte; os dois primeiros têm 18 registros cada.
  • Renato Aragão é a pessoa mais recorrente na primeira posição do elenco disponível, com 135 ocorrências.
  • O intervalo mediano entre lançamento e exibição é de 9 anos, mas há uma cauda longa de filmes bem mais antigos.
  • As comparações de avaliação são descritivas. Elas não demonstram que a idade do filme causa avaliações maiores ou menores.
  • O agrupamento é uma exploração de estrutura, não uma previsão nem uma classificação editorial do programa.

Questões sobre gênero, público-alvo e previsão de futuras exibições permanecem como trabalho futuro no escopo original do projeto.

Voltar à visão geral do estudo Ver dados e limitações

De volta ao topo