Análise dos personagens Marvel
Exploração de fonte de dados dos personagens nos quadrinhos Marvel
Dando sequência na pesquisa e documentação na versão 7 da biblioteca D3
A biblioteca D3.js - Data-Driven Documents é uma ferramenta para manipulação de documentos baseada em dados, com uso de HTML, CSS e SVG
Buscando no Kaggle uma base de dados interessante encontrei um pequeno resumo da apresentação dos personagens da Marvel nos quadrinhos Kaggle - Marvel Chracters
Transformação de dados
O arquivo se encontra em Comma Separated Value (CSV), D3 possui ferramentas para importação de dados neste formato, focando na camada de visualização vamos deixar o processo mais leve, estruturando os dados com o uso a agregação de dados
Para realizar a agregação de dados usamos um trecho de código de Python
# Importação das bibliotecas
import json
import pandas as pd
# Declarar função para extrair e agregar a contagem por valor único da coluna
def get_data(field):
# Agregar e contar
counted = df_characters.groupby(field).name.nunique()
# Converter para JSON
converted = json.loads(counted.to_json())
# Exportar por nome e contagem
return [{"name": k, "count": v} for k, v in converted.items()]
# Carregar os dados direto do CSV
df_characters = pd.read_csv('marvel_characters.csv')
# Gerar objeto com as informações agregadas
output = {}
output['eye'] = get_data('EYE')
output['hair'] = get_data('HAIR')
output['sex'] = get_data('SEX')
output['id'] = get_data('ID')
output['alive'] = get_data('ALIVE')
# Transformar para JSON e salvar os valores
json_object = json.dumps(output)
with open("data.json", "w") as outfile:
outfile.write(json_object)
Salvando os valores agregados no arquivo data.json
preparados para visualização
Função de geração de gráficos
Vamos gerar cinco gráficos da mesma maneira, gráficos de pizza com apresentação de proporcionalidade
Criamos uma função para realizar a operação, no trecho a abaixo cada etapa está comentada para facilitar a compreensão
function drawPie(data, selector_wrapper) {
// Ordenar os valores por valor da contagem
const sorted = data.sort((a, b) => b.count - a.count)
// Selecionar div para colocar o relatório gráfico dentro
const wrapper = d3.select(selector_wrapper)
// Altura da legenda
const legendWidth = 200,
// Margens do gráfico
margin = { top: 40, right: 20, bottom: 40, left: 20 },
// Dimensão do gráfico (largura e altura)
width =
wrapper.node().getBoundingClientRect().width -
margin.left -
margin.right -
legendWidth,
height = 450 - margin.top - margin.bottom,
// Cálculo do raio do gráfico
radius = Math.min(width, height) / 2,
// Definição do esquema de cores
color = d3.scaleOrdinal(d3.schemeTableau10),
// Raio do círculo da legenda
legendRadius = 12,
// Quantidade de elementos na legenda
legendCount = 10,
// Distribuição das legendas
legendSpace = height / legendCount,
// Soma das contagens
totalCount = sorted.reduce((p, c) => p + c.count, 0)
// Criação da caixa do gráfico
const svg = wrapper
.append("svg")
.attr("width", width + margin.left + margin.right + legendWidth)
.attr("height", height + margin.top + margin.bottom)
// Criação da caixa do gráfico
const content = svg
.append("g")
.attr(
"transform",
"translate(" +
(width / 2 + margin.left) +
"," +
(height / 2 + margin.top) +
")"
)
const pie = d3.pie().value((d) => d.count)
// Geração dos arcos do gráfico
const arc = d3.arc().innerRadius(0).outerRadius(radius)
// Generate groups
const arcs = content
.selectAll("arc")
.data(pie(sorted))
.enter()
.append("g")
.attr("class", "arc")
// Desenha caminho dos arcos
arcs
.append("path")
.attr("fill", (_, i) => color(i))
.attr("d", arc)
.attr("stroke", "var(--background)")
.style("stroke-width", 1)
// Criação e posicionamento das legendas
const legends = svg
.append("g")
.attr("class", "legends")
.attr(
"transform",
"translate(" +
(width + margin.left + margin.right) +
", " +
margin.top +
")"
)
// Geração da legenda
sorted.slice(0, legendCount).forEach(({ name, count }, index) => {
// Gerar círculo com a indicação das cores
legends
.append("circle")
.attr("cx", legendRadius)
.attr("cy", index * legendSpace + legendRadius)
.attr("r", legendRadius)
.style("fill", color(index))
.attr("stroke", "currentColor")
.attr("stroke-width", 1)
// Gerar texto da legenda
legends
.append("text")
.attr("x", legendRadius * 2 + 5)
.attr("y", index * legendSpace + legendRadius + 5)
.attr("fill", "currentColor")
.text(
name +
" (" +
count +
" - " +
Math.floor((count / totalCount) * 100) +
"%)"
)
})
}
Com os dados a agrupados e a função para geração pronta, vamos para os gráficos
Cor dos olhos
O primeiro campo escolhido foi a cor dos olhos
const data_eye = [
{ name: "Amber", count: 10 },
{ name: "Black Eyeballs", count: 3 },
{ name: "Black", count: 555 },
{ name: "Blue", count: 1962 },
{ name: "Brown", count: 1924 },
{ name: "Compound", count: 1 },
{ name: "Gold", count: 14 },
{ name: "Green", count: 613 },
{ name: "Grey", count: 95 },
{ name: "Hazel", count: 76 },
{ name: "Magenta", count: 2 },
{ name: "Multiple", count: 7 },
{ name: "No Eyes", count: 7 },
{ name: "One Eye", count: 21 },
{ name: "Orange", count: 25 },
{ name: "Pink", count: 21 },
{ name: "Purple", count: 31 },
{ name: "Red", count: 508 },
{ name: "Silver", count: 12 },
{ name: "Variable", count: 49 },
{ name: "Violet", count: 11 },
{ name: "White", count: 400 },
{ name: "Yellow Eyeballs", count: 6 },
{ name: "Yellow", count: 256 },
]
drawPie(data_eye, "#d3_wrapper_eye")
Podemos observar que alguns não possuem olhos, como outros uma cor uniforme para todo o globo ocular, como o Remy LeBeau (Gambit) com o globo todo preto
Proporção da cor dos olhos
Olhos azuis e marrons não a grande maioria, até ai sem grande surpresas
Cor dos cabelos
Sobre a cor dos cabelos, os personagens mais antigos tinham alta limitação de cores e resolução na gráfica que limitava muito as escolhas, como estamos falando da cultura ocidental cores diferentes não deveria ser comum
Nos dados organizados já coisas diferentes como Auburn que se traduz como ruivo, sendo diferente de vermelho
const data_hair = [
{ name: "Auburn", count: 78 },
{ name: "Bald", count: 838 },
{ name: "Black", count: 3755 },
{ name: "Blond", count: 1582 },
{ name: "Blue", count: 56 },
{ name: "Bronze", count: 1 },
{ name: "Brown", count: 2339 },
{ name: "Dyed", count: 1 },
{ name: "Gold", count: 8 },
{ name: "Green", count: 117 },
{ name: "Grey", count: 531 },
{ name: "Light Brown", count: 6 },
{ name: "Magenta", count: 5 },
{ name: "No", count: 1176 },
{ name: "Orange", count: 43 },
{ name: "Orange-brown", count: 3 },
{ name: "Pink", count: 31 },
{ name: "Purple", count: 47 },
{ name: "Red", count: 620 },
{ name: "Reddish Blond", count: 6 },
{ name: "Silver", count: 16 },
{ name: "Strawberry Blond", count: 47 },
{ name: "Variable", count: 32 },
{ name: "White", count: 754 },
{ name: "Yellow", count: 20 },
]
drawPie(data_hair, "#d3_wrapper_hair")
No gráfico podemos ver a proporção
Proporção da cor dos cabelos
Sem grandes surpresas que quase a metade possui cabelos entre o preto (Black) e o marrom (Brown), com 13% loiros (Blond) e 6% de carecas (No)
Gênero dos personagens
Gênero, vale destacar que é diferente de orientação, aqui vão se apresentar
const data_sex = [
{ name: "Agender", count: 45 },
{ name: "Female", count: 3837 },
{ name: "Genderfluid", count: 2 },
{ name: "Male", count: 11638 },
]
drawPie(data_sex, "#d3_wrapper_sex")
Como podemos ver tem dois personagens de gênero fluído, Xavin (da raça dos Skulls) e Loki, ambos personagens ligados a capacidade de mudar de forma, que acredito não ser coincidência
Proporção dos gêneros
Como podemos observar 74% dos personagens são masculinos, o público alvo das publicações
Para entender melhor essa distribuição geramos uma visualização com as primeiras aparições dos personagens por gênero
Visualizar geração do gráfico de contagens por gêneros e ano
Geração da fonte de dados por agrupamento com Python
# Importação das bibliotecas
import json
import pandas as pd
# Carregar os dados direto do CSV
df_characters = pd.read_csv('marvel_characters.csv')
# Filtrar somente generos binários
df_characters = df_characters[(df_characters.SEX == "Female Characters") | (df_characters.SEX == "Male Characters")]
# Definir ponto de contagem
df_characters['id'] = df_characters.index
# Contar elementos para ano e gênero
df_year_gender = df_characters.groupby(['Year', 'SEX']).id.count().rename("Count").reset_index(name='Count')
df_year_gender.Year = df_year_gender.Year.astype(int)
# Exportar por ano, genero e contagem
output = {}
output["female_list"] = [{"year": r.Year, "count": r.Count} for i, r in df_year_gender[df_year_gender.SEX == "Female Characters"].iterrows()]
output["male_list"] = [{"year": r.Year, "count": r.Count} for i, r in df_year_gender[df_year_gender.SEX == "Male Characters"].iterrows()]
# Transformar para JSON e salvar os valores
json_object = json.dumps(output)
with open("year_genre.json", "w") as outfile:
outfile.write(json_object)
Fonte de dados gerada com separação por gênero
const data_yearsex = {
female_list: [
{ year: 1939, count: 10 },
{ year: 1940, count: 33 },
{ year: 1941, count: 15 },
{ year: 1942, count: 14 },
{ year: 1943, count: 13 },
{ year: 1944, count: 12 },
{ year: 1945, count: 12 },
{ year: 1946, count: 12 },
{ year: 1947, count: 13 },
{ year: 1948, count: 14 },
{ year: 1949, count: 6 },
{ year: 1950, count: 3 },
{ year: 1951, count: 9 },
{ year: 1952, count: 5 },
{ year: 1953, count: 10 },
{ year: 1954, count: 19 },
{ year: 1955, count: 4 },
{ year: 1956, count: 1 },
{ year: 1958, count: 1 },
{ year: 1960, count: 2 },
{ year: 1961, count: 5 },
{ year: 1962, count: 13 },
{ year: 1963, count: 25 },
{ year: 1964, count: 23 },
{ year: 1965, count: 21 },
{ year: 1966, count: 19 },
{ year: 1967, count: 10 },
{ year: 1968, count: 18 },
{ year: 1969, count: 17 },
{ year: 1970, count: 17 },
{ year: 1971, count: 16 },
{ year: 1972, count: 34 },
{ year: 1973, count: 50 },
{ year: 1974, count: 50 },
{ year: 1975, count: 66 },
{ year: 1976, count: 57 },
{ year: 1977, count: 54 },
{ year: 1978, count: 32 },
{ year: 1979, count: 61 },
{ year: 1980, count: 60 },
{ year: 1981, count: 58 },
{ year: 1982, count: 52 },
{ year: 1983, count: 65 },
{ year: 1984, count: 61 },
{ year: 1985, count: 69 },
{ year: 1986, count: 66 },
{ year: 1987, count: 45 },
{ year: 1988, count: 81 },
{ year: 1989, count: 71 },
{ year: 1990, count: 92 },
{ year: 1991, count: 92 },
{ year: 1992, count: 118 },
{ year: 1993, count: 125 },
{ year: 1994, count: 117 },
{ year: 1995, count: 79 },
{ year: 1996, count: 101 },
{ year: 1997, count: 91 },
{ year: 1998, count: 76 },
{ year: 1999, count: 72 },
{ year: 2000, count: 87 },
{ year: 2001, count: 73 },
{ year: 2002, count: 85 },
{ year: 2003, count: 84 },
{ year: 2004, count: 108 },
{ year: 2005, count: 112 },
{ year: 2006, count: 105 },
{ year: 2007, count: 82 },
{ year: 2008, count: 101 },
{ year: 2009, count: 85 },
{ year: 2010, count: 110 },
{ year: 2011, count: 99 },
{ year: 2012, count: 56 },
{ year: 2013, count: 54 },
],
male_list: [
{ year: 1939, count: 56 },
{ year: 1940, count: 183 },
{ year: 1941, count: 180 },
{ year: 1942, count: 220 },
{ year: 1943, count: 178 },
{ year: 1944, count: 121 },
{ year: 1945, count: 85 },
{ year: 1946, count: 78 },
{ year: 1947, count: 58 },
{ year: 1948, count: 99 },
{ year: 1949, count: 53 },
{ year: 1950, count: 26 },
{ year: 1951, count: 43 },
{ year: 1952, count: 20 },
{ year: 1953, count: 26 },
{ year: 1954, count: 57 },
{ year: 1955, count: 38 },
{ year: 1956, count: 13 },
{ year: 1957, count: 7 },
{ year: 1958, count: 1 },
{ year: 1959, count: 4 },
{ year: 1960, count: 36 },
{ year: 1961, count: 71 },
{ year: 1962, count: 86 },
{ year: 1963, count: 149 },
{ year: 1964, count: 144 },
{ year: 1965, count: 132 },
{ year: 1966, count: 107 },
{ year: 1967, count: 102 },
{ year: 1968, count: 111 },
{ year: 1969, count: 92 },
{ year: 1970, count: 108 },
{ year: 1971, count: 93 },
{ year: 1972, count: 140 },
{ year: 1973, count: 181 },
{ year: 1974, count: 180 },
{ year: 1975, count: 209 },
{ year: 1976, count: 238 },
{ year: 1977, count: 214 },
{ year: 1978, count: 146 },
{ year: 1979, count: 149 },
{ year: 1980, count: 187 },
{ year: 1981, count: 156 },
{ year: 1982, count: 152 },
{ year: 1983, count: 122 },
{ year: 1984, count: 143 },
{ year: 1985, count: 171 },
{ year: 1986, count: 168 },
{ year: 1987, count: 129 },
{ year: 1988, count: 215 },
{ year: 1989, count: 237 },
{ year: 1990, count: 243 },
{ year: 1991, count: 232 },
{ year: 1992, count: 318 },
{ year: 1993, count: 401 },
{ year: 1994, count: 344 },
{ year: 1995, count: 210 },
{ year: 1996, count: 192 },
{ year: 1997, count: 194 },
{ year: 1998, count: 190 },
{ year: 1999, count: 147 },
{ year: 2000, count: 221 },
{ year: 2001, count: 148 },
{ year: 2002, count: 214 },
{ year: 2003, count: 170 },
{ year: 2004, count: 168 },
{ year: 2005, count: 211 },
{ year: 2006, count: 259 },
{ year: 2007, count: 209 },
{ year: 2008, count: 244 },
{ year: 2009, count: 199 },
{ year: 2010, count: 200 },
{ year: 2011, count: 235 },
{ year: 2012, count: 140 },
{ year: 2013, count: 97 },
],
}
Geração das linhas do gráfico
function drawLines(data, selector_wrapper) {
if (document.querySelector(selector_wrapper + " svg")) {
return true
}
// Selecionar div para colocar o relatório gráfico dentro
const wrapper = d3.select(selector_wrapper)
const // Margens do gráfico
margin = { top: 20, right: 20, bottom: 20, left: 40 },
// Dimensão do gráfico (largura e altura)
width =
wrapper.node().getBoundingClientRect().width - margin.left - margin.right,
height = 450 - margin.top - margin.bottom,
// Definição do esquema de cores
color = d3.scaleOrdinal(d3.schemeTableau10)
// Criação da caixa do gráfico
const svg = wrapper
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
// Criação e posicionamento do gráfico
const graphics = svg
.append("g")
.attr("class", "graphics")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
// Definição da escala das linhas
const x = d3.scaleLinear().range([0, width])
const y = d3.scaleLinear().range([height, 0])
// Escala da variação dos dados (menor e maior número)
const years = data.male_list.map(({ year }) => year),
counts = data.male_list.map(({ count }) => count)
x.domain([Math.min(...years), Math.max(...years)])
y.domain([0, Math.max(...counts)])
// Adicionar o eixo X
graphics
.append("g")
.attr("class", "domains")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x).tickFormat((d) => d))
// Adicionar o eixo Y
graphics.append("g").attr("class", "domains").call(d3.axisLeft(y))
// Gerar caminho para a linha
const genline = d3
.line()
.x((d) => x(d.year))
.y((d) => y(d.count))
// Gerar o caminho das linhas
graphics
.append("path")
.data([data.male_list])
.style("stroke", color(0))
.attr("class", "line")
.attr("d", genline)
graphics
.append("path")
.data([data.female_list])
.style("stroke", color(1))
.attr("class", "line")
.attr("d", genline)
document.querySelector(selector_wrapper + " > div").style.display = "none"
}
drawLines(data_yearsex, "#d3_wrapper_yearsex")
As primeiras aparições de mulheres sempre é uma visualização inferior a quantidade de homens
Primeiras aparições por gênero e ano
Relação da identidade
O Peter Parker tirou a máscara na frente das câmera para todo mundo saber a identidade do Homem-Aranha, antes do Mefisto apagar tudo
const data_id = [
{ name: "Known to Authorities Identity", count: 15 },
{ name: "No Dual Identity", count: 1788 },
{ name: "Public Identity", count: 4528 },
{ name: "Secret Identity", count: 6275 },
]
drawPie(data_id, "#d3_wrapper_id")
Essa história faz sentido para todos os persogens?
Proporção do tipo de identidades
Aparentemente não, nem a metade dos personagens possuem uma identidade secreta (49%)
Personagem de quadrinho não morre?
Vamos verificar a realidade dessa afirmação, podemos visualizar a relação entre heróis vivos e mortos
const data_alive = [
{ name: "Deceased Characters", count: 3765 },
{ name: "Living Characters", count: 12608 },
]
drawPie(data_alive, "#d3_wrapper_alive")
Com a geração dos gráficos podemos ver que uma parte das figuras não voltagem
Proporção de personagens vivo
Aqui podemos mostrar como é mentira que personagem de quadrinho não morre, temos 22% de mortos
Como todo mundo que fez parte dos quadrinhos é relacionado como personagem, pessoas históricas como Winston Churchill e o Adolf Hitler estão na lista, e ressuscitar essas pessoas seria muuuito estranho
Referências
Versões das bibliotecas utilizadas
d3: "7.8.4"