Sistema de Análise e Visualização de Dados de Filmes com Big Data

Introdução ao Projeto

Com a expansão contínua da internet, plataformas sociais como Weibo, Zhihu e Douban tornaram-se repositórios vastos de opiniões e avaliações dos usuários sobre produtos e serviços. Esse feedback gera um "efeito boca a boca" que influencia o comportamento de consumo. A mineração e análise dessas informações ocultas representam um valor significativo, tanto para indivíduos que buscam a melhor escolha de filmes ou livros quanto para empresas que desejam aprimorar seus produtos e serviços.

Ferramentas de web scraping permitem a coleta eficiente, organização e gerenciamento desses dados de sites sociais. Douban, por exemplo, é uma plataforma social proeminente que oferece um sistema exclusivo de avaliação, recomendação e comparação para filmes, livros e música, exercendo uma influência considerável na rede social chinesa. Este projeto explora a construção de um sistema abrangente para extração, análise e visualização de dados de filmes.

Funcionalidades do Sistema

Interface Principal

O sistema apresenta uma interface intuitiva, permitindo fácil navegação pelas diferentes funcionalidades.

Consulta de Dados de Filmes

Usuários com privilégios de administrador podem realizar consultas detalhadas sobre os dados dos filmes. As opções de filtro incluem: "Pontuação Mínima", "Pontuação Máxima", "Ano de Lançamento", "Gênero do Filme", "Região de Lançamento" e "Palavra-chave do Título".

Visualização Interativa

Para facilitar a compreensão dos dados, o sistema oferece diversas visualizações:

  • Gráfico de Treemap por Gênero: Exibe a proporção de diferentes gêneros de filmes, destacando a predominância de dramas, documentários e comédias.
  • Gráfico de Funil por Região de Lançamento: Ilustra a distribuição de filmes por região, com a China e os Estados Unidos apresentando os maiores volumes.
  • Distribuição por Ano de Lançamento e Pontuação: Gráficos dedicados à frequência de filmes lançados por ano e à distribuição das pontuações médias.
  • Linha do Tempo Dinâmica: Apresenta a evolução dos dados ao longo dos anos, com animações que mostram mudanças dinâmicas.
  • Gráfico de Pizza de Distribuição Regional: Detalha a proporção de filmes de cada região.
  • Nuvem de Palavras para Frequência: Gera nuvens de palavras baseadas nos nomes de atores, diretores, roteiristas e classificações de filmes, destacando os termos mais frequentes.

Extração de Dados

A fase de extração de dados é crucial e utiliza bibliotecas Python para coletar informações de websites.

Requests

A biblioteca requests é uma ferramenta essencial em Python para realizar requisições HTTP, fundamental para web scraping. Abaixo está um exemplo de como é utilizada para coletar URLs de filmes:

import requests
from lxml import html
import csv

def configurar_cabecalhos():
    """Define os cabeçalhos HTTP para simular um navegador."""
    return {
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
    }

def obter_links_filmes(base_url, total_paginas, passo=25):
    """
    Coleta os links das páginas de detalhes dos filmes de um site paginado.
    Args:
        base_url (str): URL base para as páginas do índice.
        total_paginas (int): Número total de itens a serem raspados.
        passo (int): Incremento para a paginação.
    Returns:
        list: Uma lista de URLs dos filmes.
    """
    links_coletados = []
    headers = configurar_cabecalhos()
    for offset in range(0, total_paginas, passo):
        url_paginada = f"{base_url}?start={offset}"
        try:
            resposta = requests.get(url_paginada, headers=headers, timeout=10)
            resposta.raise_for_status() # Lança um erro para códigos de status HTTP ruins
            arvore_html = html.fromstring(resposta.text)
            links_na_pagina = arvore_html.xpath('//div[@class="hd"]/a/@href')
            links_coletados.extend(links_na_pagina)
        except requests.exceptions.RequestException as e:
            print(f"Erro ao acessar {url_paginada}: {e}")
        except Exception as e:
            print(f"Erro de parsing ou inesperado: {e}")
    return links_coletados

def salvar_links_csv(lista_links, nome_arquivo="filmes_top_links.csv"):
    """
    Salva uma lista de links em um arquivo CSV.
    Args:
        lista_links (list): Lista de URLs para salvar.
        nome_arquivo (str): Nome do arquivo CSV de saída.
    """
    try:
        with open(nome_arquivo, "w", newline='', encoding='utf-8') as arquivo_csv:
            escritor = csv.writer(arquivo_csv)
            for link in lista_links:
                escritor.writerow([link])
        print(f"Links salvos com sucesso em {nome_arquivo}")
    except IOError as e:
        print(f"Erro ao salvar arquivo CSV: {e}")

if __name__ == '__main__':
    url_base_douban = "https://movie.douban.com/top250"
    links_dos_filmes = obter_links_filmes(url_base_douban, 250)
    if links_dos_filmes:
        salvar_links_csv(links_dos_filmes)


BeautifulSoup (bs4)

BeautifulSoup (bs4) é uma biblioteca Python para parsing de documentos HTML e XML. Ela facilita a extração de dados da web ao transformar documentos complexos em uma estrutura navegável e pesquisável. A biblioteca é flexível, permitindo diferentes estratégias de parsing, e lida automaticamente com a codificação de caracteres, convertendo a entrada para Unicode e a saída para UTF-8, o que simplifica muito o processo de tratamento de texto em diversas línguas.

Integração com MySQL

Os dados coletados são persistidos em um banco de dados MySQL para armazenamento e gerenciamento. A biblioteca pymysql é utilizada para estabelecer a conexão e executar operações no banco de dados, como a inserção dos detalhes dos filmes. O exemplo a seguir demonstra a inserção de dados em uma tabela:

import pymysql

def inserir_dados_filme(detalhes_filme):
    """
    Insere detalhes de um filme no banco de dados MySQL.
    Args:
        detalhes_filme (dict): Um dicionário contendo os dados do filme.
    """
    db_config = {
        'host': '127.0.0.1',
        'port': 3306,
        'user': 'root',
        'password': 'mysecretpassword', # Alterar para a sua senha
        'database': 'db_filmes',
        'charset': 'utf8mb4'
    }
    
    conn = None
    try:
        conn = pymysql.connect(**db_config)
        cursor = conn.cursor()

        colunas = [
            'url', 'nome_filme', 'pontuacao', 'data_lancamento', 
            'generos', 'regioes', 'diretores', 'roteiristas', 
            'atores', 'comentarios'
        ]
        
        # Cria a string SQL de inserção dinamicamente
        query_sql = f"INSERT INTO tb_filmes ({', '.join(colunas)}) VALUES ({', '.join(['%s'] * len(colunas))})"
        
        valores = (
            detalhes_filme.get('url'),
            detalhes_filme.get('nome_filme'),
            detalhes_filme.get('pontuacao'),
            detalhes_filme.get('data_lancamento'),
            detalhes_filme.get('generos'),
            detalhes_filme.get('regioes'),
            detalhes_filme.get('diretores'),
            detalhes_filme.get('roteiristas'),
            detalhes_filme.get('atores'),
            detalhes_filme.get('comentarios')
        )

        cursor.execute(query_sql, valores)
        conn.commit()
        print(f"Filme '{detalhes_filme.get('nome_filme')}' inserido com sucesso.")

    except pymysql.Error as e:
        print(f"Erro ao inserir dados no banco de dados: {e}")
        if conn:
            conn.rollback() # Reverte a transação em caso de erro
    finally:
        if conn:
            conn.close() # Garante que a conexão seja fechada

# Exemplo de uso
if __name__ == '__main__':
    exemplo_filme = {
        'url': 'http://exemplo.com/filme/1',
        'nome_filme': 'A Grande Aventura',
        'pontuacao': 8.5,
        'data_lancamento': 2023,
        'generos': 'Aventura, Fantasia',
        'regioes': 'EUA',
        'diretores': 'João Silva',
        'roteiristas': 'Maria Souza',
        'atores': 'Carlos Santos, Ana Costa',
        'comentarios': 1500
    }
    # inserir_dados_filme(exemplo_filme)
    # Certifique-se de que o banco de dados 'db_filmes' e a tabela 'tb_filmes' existem
    # com as colunas correspondentes antes de executar.

Tecnologias de Visualização

Flask

Flask é um microframework web para Python, conhecido por sua simplicidade, flexibilidade e leveza. Construído sobre Werkzeug e Jinja2, ele permite o desenvolvimento rápido de aplicações web. Sua modularidade facilita a integração com diversas bibliotecas e padrões de design, como MVC (Model-View-Controller). A extensibilidade do Flask é um de seus ponttos fortes, oferecendo uma vasta gama de extensões para adicionar funcionalidades como autenticação, ORM e CORS.

Neste projeto, o Flask é usado para o backend da aplicação, lidando com requisições HTTP e interagindo com o banco de dados. Para resolver questões de Cross-Origin Resource Sharing (CORS) em requisições de frontend, a extensão Flask-CORS é utilizada, juntamente com bibliotecas de requisição como Axios no front end.

from flask import Flask, request, render_template
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.declarative import declarative_base
from flask_paginate import Pagination, get_page_parameter
from flask_cors import CORS # Importar Flask-CORS

# Importar suas funções de manipulação de dados
# from data_processors import get_score_ranges, get_release_years, get_genres_list, get_area_list, fetch_movie_data_filtered

app = Flask(__name__)
CORS(app) # Habilitar CORS para todas as rotas
app.config['TEMPLATES_AUTO_RELOAD'] = True

# Configuração do banco de dados SQLAlchemy
app.config['SQLALCHEMY_DATABASE_URI'] = "mysql+pymysql://root:mysecretpassword@localhost:3306/db_filmes?charset=utf8mb4"
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)

# Definição do modelo de dados para a tabela de filmes
class Movie(db.Model):
    __tablename__ = 'tb_filmes'
    id = Column(Integer, primary_key=True, autoincrement=True)
    url = Column(String(255))
    nome_filme = Column(String(100))
    pontuacao = Column(String(10)) # Armazenar como string para flexibilidade
    comentarios = Column(Integer)
    data_lancamento = Column(Integer)
    generos = Column(String(50))
    regioes = Column(String(50))
    atores = Column(String(255))
    diretores = Column(String(255))
    roteiristas = Column(String(255))

@app.route('/')
def home():
    """Rota da página inicial que pode direcionar para o login."""
    return render_template('login.html')

@app.route('/dashboard')
def dashboard():
    """Rota para a página principal do dashboard após o login."""
    return render_template('index.html')

@app.route('/authenticate', methods=['POST'])
def authenticate_user():
    """Processa as credenciais de login."""
    username = request.form.get('username')
    password = request.form.get('password')

    if username == 'admin' and password == '123456': # Exemplo simples, usar autenticação real em produção
        return render_template('index.html')
    return "Credenciais inválidas", 401 # Retornar erro para falha de login

@app.route("/movies")
def movie_list():
    """Exibe uma lista paginada e filtrável de filmes."""
    page = request.args.get(get_page_parameter(), type=int, default=1)
    per_page = 15

    # Parâmetros de filtro
    min_score_param = request.args.get("min_score")
    max_score_param = request.args.get("max_score")
    release_year_param = request.args.get("release_year")
    genres_param = request.args.get("genres")
    areas_param = request.args.get("areas")
    title_keyword_param = request.args.get("title_keyword")

    query = Movie.query

    if min_score_param:
        query = query.filter(Movie.pontuacao >= float(min_score_param))
    if max_score_param:
        query = query.filter(Movie.pontuacao <= float(max_score_param))
    if release_year_param:
        query = query.filter(Movie.data_lancamento == int(release_year_param))
    if genres_param and genres_param != 'Todos':
        query = query.filter(Movie.generos.contains(genres_param))
    if areas_param and areas_param != 'Todos':
        query = query.filter(Movie.regioes.contains(areas_param))
    if title_keyword_param:
        query = query.filter(Movie.nome_filme.contains(title_keyword_param))
    
    total_movies = query.count()
    paginated_movies = query.paginate(page=page, per_page=per_page, error_out=False)

    # Dados de exemplo para filtros (em um sistema real, viriam do DB)
    available_scores = ["6.0", "7.0", "8.0", "9.0"]
    available_years = [str(y) for y in range(1990, 2025)]
    available_genres = ["Drama", "Comédia", "Ação", "Fantasia"]
    available_areas = ["China", "EUA", "França", "Brasil"]

    return render_template(
        'pages/movie_management/movie_list.html',
        movies=paginated_movies.items,
        pagination=paginated_movies,
        min_scores=available_scores,
        max_scores=available_scores,
        release_years=available_years,
        genres_options=available_genres,
        areas_options=available_areas
    )

if __name__ == '__main__':
    # Para criar as tabelas no banco de dados, descomente e execute uma vez
    # with app.app_context():
    #     db.create_all()
    app.run(debug=True)

ECharts

ECharts (Enterprise Charts), uma ferramenta de visualização de dados de código aberto da Baidu, baseia-se na leve biblioteca Canvas ZRender. Ele se destacca por sua compatibilidade com a maioria dos navegadores modernos, tornando-o adequado para aplicações em desktop e mobile. ECharts capacita desenvolvedores a integrar e visualizar dados de forma inovadora e personalizável, suportando uma ampla gama de tipos de gráficos como linhas, barras, dispersão, K-line e pizza. A sua utilização em projetos web Java ou Python é facilitada pela inclusão de bibliotecas JavaScript.

Preparação e Transformação de Dados com Pandas

Antes da visualização, os dados são carregados, combinados e limpos usando a biblioteca Pandas. Isso inclui a união de diferentes conjuntos de dados, seleção de características relevantes, cálculo de novas métricas e tratamento de valores ausentes.

import pandas as pd
import json

def carregar_e_combinar_dados(caminho_creditos, caminho_filmes):
    """Carrega e combina os datasets de créditos e detalhes de filmes."""
    movie_credits = pd.read_csv(caminho_creditos)
    movie_details = pd.read_csv(caminho_filmes)

    # Renomear coluna para permitir a junção
    movie_credits.rename(columns={'movie_id': 'id'}, inplace=True)
    
    # Combinar datasets com base em 'id' e 'title'
    combined_df = pd.merge(movie_credits, movie_details, on=['id', 'title'], how='inner')
    return combined_df

def processar_dados_filmes(df):
    """Realiza a limpeza, seleção e transformação dos dados de filmes."""
    # Selecionar um subconjunto de colunas relevantes
    colunas_selecionadas = [
        'original_title', 'crew', 'release_date', 'genres', 'keywords',
        'production_companies', 'production_countries', 'revenue',
        'budget', 'runtime', 'vote_average'
    ]
    processed_df = df[colunas_selecionadas].copy()

    # Calcular o lucro
    processed_df['profit'] = processed_df['revenue'] - processed_df['budget']

    # Tratamento de valores ausentes na data de lançamento
    if processed_df['release_date'].isnull().any():
        # Exemplo: preencher uma data específica para valores ausentes
        # Em um cenário real, pode-se usar métodos mais sofisticados (média, moda, consulta externa)
        processed_df['release_date'].fillna('2014-06-01', inplace=True)
    
    # Converter 'release_date' para datetime e extrair o ano
    processed_df['release_date'] = pd.to_datetime(processed_df['release_date'])
    processed_df['release_year'] = processed_df['release_date'].dt.year

    # Processar a coluna 'genres' (de string JSON para lista de strings)
    processed_df['genres'] = processed_df['genres'].apply(json.loads)
    
    all_genres = set()
    def extract_genre_names(genre_list_json):
        """Extrai os nomes dos gêneros de uma lista de dicionários."""
        genre_names = [item['name'] for item in genre_list_json if 'name' in item]
        all_genres.update(genre_names) # Coleta todos os gêneros únicos
        return ','.join(genre_names)

    processed_df['genres_str'] = processed_df['genres'].apply(extract_genre_names)

    # Criação de colunas dummy para cada gênero
    for genre in all_genres:
        processed_df[genre] = processed_df['genres_str'].apply(lambda x: 1 if genre in x else 0)

    return processed_df, list(all_genres)

if __name__ == '__main__':
    credits_path = './tmdb_5000_credits.csv'
    movies_path = './tmdb_5000_movies.csv'

    combined_data = carregar_e_combinar_dados(credits_path, movies_path)
    final_processed_df, unique_genres = processar_dados_filmes(combined_data)

    print("Dados processados com sucesso. Exemplo das primeiras linhas:")
    print(final_processed_df[['original_title', 'release_year', 'genres_str', 'profit']].head())
    print("\nGêneros únicos encontrados:", unique_genres)

Visualização de Tendências de Gênero com Matplotlib

Para analisar como a popularidade dos gêneros de filmes muda ao longo do tempo, são gerados gráficos de linha utilizando Matplotlib. Essa visualização mostra o número de filmes de cada gênero lançado por ano, revelando tendências de crescimento e declínio.

import matplotlib.pyplot as plt
import pandas as pd

def visualizar_tendencia_generos(df_processado, generos_unicos):
    """
    Gera um gráfico de linha mostrando a tendência de gêneros de filmes ao longo dos anos.
    Args:
        df_processado (pd.DataFrame): DataFrame com os dados de filmes processados.
        generos_unicos (list): Lista de todos os gêneros únicos.
    """
    # Agrupar por ano e contar o número de filmes por gênero
    yearly_genre_counts = df_processado.groupby('release_year')[generos_unicos].sum()

    # Filtrar anos com dados significativos, se necessário
    yearly_genre_counts = yearly_genre_counts[yearly_genre_counts.index >= 1950] 

    plt.figure(figsize=(12, 8))
    # Configuração de fonte para suportar caracteres especiais, se necessário (ex: chinês)
    # plt.rcParams['font.sans-serif'] = ['DejaVu Sans'] # Ou outra fonte que suporte o idioma
    # plt.rcParams['axes.unicode_minus'] = False # Para garantir que sinais de menos sejam exibidos corretamente

    for genre_category in yearly_genre_counts.columns:
        plt.plot(yearly_genre_counts.index, yearly_genre_counts[genre_category], label=genre_category)

    plt.title('Tendência de Lançamentos de Filmes por Gênero ao Longo do Tempo')
    plt.xlabel('Ano de Lançamento')
    plt.ylabel('Número de Filmes')
    plt.legend(title='Gênero', bbox_to_anchor=(1.05, 1), loc='upper left', fontsize='small')
    plt.grid(True, linestyle='--', alpha=0.6)
    plt.tight_layout()
    plt.savefig('./tendencia_generos_filmes.png')
    plt.show()

# Exemplo de uso (requer 'final_processed_df' e 'unique_genres' do script anterior)
# if __name__ == '__main__':
#     # Assumindo que 'final_processed_df' e 'unique_genres' foram obtidos
#     # de carregar_e_combinar_dados e processar_dados_filmes
#     visualizar_tendencia_generos(final_processed_df, unique_genres)

Análise: Observa-se que a maioria dos gêneros de filmes exibe uma tendência de crescimento ao longo do tempo, com um aumento notável a partir de 1992. Gêneros como Drama e Comédia mostram o crescimento mais rápido, permanecendo entre os mais populares.

Visualização de Lucratividade por Gênero com Pyecharts

Para identificar os gêneros de filmes mais lucrativos, um gráfico de barras é gerado usando Pyecharts. Esta visualização permite comparar o lucro médio gerado por diferentes categorias de filmes.

from pyecharts import options as opts
from pyecharts.charts import Bar
import pandas as pd

def visualizar_lucratividade_generos(df_processado, generos_unicos):
    """
    Gera um gráfico de barras horizontais da lucratividade média por gênero.
    Args:
        df_processado (pd.DataFrame): DataFrame com os dados de filmes processados.
        generos_unicos (list): Lista de todos os gêneros únicos.
    """
    # Calcular o lucro médio por gênero
    genre_profit_data = {}
    for genre in generos_unicos:
        # Filtrar filmes que pertencem ao gênero e que têm lucro > 0 para evitar distorções
        relevant_movies = df_processado[df_processado[genre] == 1]
        if not relevant_movies.empty and relevant_movies['profit'].sum() > 0:
            genre_profit_data[genre] = relevant_movies['profit'].mean()
        else:
            genre_profit_data[genre] = 0 # Se não houver filmes ou lucro, consideramos 0
    
    # Converter para Series para facilitar a ordenação e manipulação
    profit_series = pd.Series(genre_profit_data).sort_values(ascending=False)

    # Filtrar gêneros com lucro muito baixo ou zero para uma visualização mais clara
    profit_series = profit_series[profit_series > 1000000] # Exemplo: filtrar lucros abaixo de 1 milhão

    bar_chart = (
        Bar(init_opts=opts.InitOpts(width="900px", height="600px", theme="white"))
        .add_xaxis(profit_series.index.tolist())
        .add_yaxis(
            "Lucro Médio (Milhões USD)",
            [float(f'{val / 1_000_000:.2f}') for val in profit_series.values],
            color="#5C88DA", # Um azul diferente
            label_opts=opts.LabelOpts(
                is_show=True, position="right", color="#333", formatter="{c}M"
            ),
        )
        .set_global_opts(
            title_opts=opts.TitleOpts(
                title="Lucro Médio por Gênero de Filme", pos_left="center", pos_top="3%"
            ),
            legend_opts=opts.LegendOpts(is_show=False),
            xaxis_opts=opts.AxisOpts(name="Lucro Médio (Milhões USD)"),
            yaxis_opts=opts.AxisOpts(name="Gênero do Filme"),
            datazoom_opts=[opts.DataZoomOpts(type_="inside", orient="vertical")] # Adiciona zoom
        )
        .reversal_axis() # Inverte os eixos para um gráfico de barras horizontal
    )
    bar_chart.render("lucratividade_generos_filmes.html")

# Exemplo de uso (requer 'final_processed_df' e 'unique_genres' do script de processamento)
# if __name__ == '__main__':
#     # Assumindo que 'final_processed_df' e 'unique_genres' foram obtidos
#     # de carregar_e_combinar_dados e processar_dados_filmes
#     visualizar_lucratividade_generos(final_processed_df, unique_genres)

Análise: A visualização do lucro médio por gênero revela que filmes de Animação, Aventura e Fantasia tendem a ser os mais lucrativos. Por outro lado, gêneros como Estrangeiro e Filme de TV podem apresentar maior risco financeiro, com potencial para perdas.

Tags: Python Flask pandas echarts matplotlib

Publicado em 7-30 23:28