Artigo 51 — Projeto 4: Dashboard Analítico com Dash e Banco de Dados Real
Prof. Ricardo Matos Módulo 9 · Projetos Reais e Carreira · Artigo 51 de 52
Introdução
O Projeto 4 é um dashboard analítico completo — uma aplicação web interativa que consome dados reais de um banco PostgreSQL, processa com pandas e exibe visualizações dinâmicas com Plotly Dash. O sistema monitora o desempenho acadêmico de uma escola em tempo real: médias por turma, evolução ao longo do tempo, distribuição de notas, ranking de alunos e alertas automáticos. É o tipo de ferramenta que diretores e coordenadores usam no dia a dia.
Visão Geral do Projeto
escola-dashboard/
├── app/
│ ├── __init__.py
│ ├── main.py ← entry point Dash
│ ├── config.py
│ ├── database.py ← SQLAlchemy + queries
│ ├── dados/
│ │ ├── __init__.py
│ │ ├── queries.py ← consultas SQL/ORM
│ │ └── cache.py ← Redis cache
│ ├── componentes/
│ │ ├── __init__.py
│ │ ├── cartoes.py ← KPI cards
│ │ ├── graficos.py ← Plotly figures
│ │ └── tabelas.py ← DataTables
│ ├── layout/
│ │ ├── __init__.py
│ │ ├── sidebar.py
│ │ ├── header.py
│ │ └── paginas/
│ │ ├── visao_geral.py
│ │ ├── turmas.py
│ │ ├── alunos.py
│ │ └── alertas.py
│ └── callbacks/
│ ├── __init__.py
│ ├── filtros.py
│ └── graficos.py
├── migrations/
├── scripts/
│ └── popular_banco.py ← dados de demonstração
├── tests/
├── assets/
│ └── style.css
├── Dockerfile
└── docker-compose.yml
Banco de Dados e Queries
# app/database.py
from sqlalchemy import create_engine, text
from sqlalchemy.orm import sessionmaker, DeclarativeBase
from sqlalchemy import Column, Integer, String, Float, Boolean, DateTime, ForeignKey
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from app.config import get_config
import pandas as pd
config = get_config()
engine = create_engine(config.database_url, pool_pre_ping=True, echo=False)
Session = sessionmaker(bind=engine)
class Base(DeclarativeBase):
pass
class Turma(Base):
__tablename__ = "turmas"
id = Column(Integer, primary_key=True)
nome = Column(String(50), unique=True)
turno = Column(String(20))
ano = Column(Integer)
alunos = relationship("Aluno", back_populates="turma")
class Aluno(Base):
__tablename__ = "alunos"
id = Column(Integer, primary_key=True)
nome = Column(String(100))
email = Column(String(100), unique=True)
turma_id = Column(Integer, ForeignKey("turmas.id"))
ativo = Column(Boolean, default=True)
criado_em = Column(DateTime, server_default=func.now())
turma = relationship("Turma", back_populates="alunos")
notas = relationship("Nota", back_populates="aluno")
class Disciplina(Base):
__tablename__ = "disciplinas"
id = Column(Integer, primary_key=True)
nome = Column(String(80))
notas = relationship("Nota", back_populates="disciplina")
class Nota(Base):
__tablename__ = "notas"
id = Column(Integer, primary_key=True)
aluno_id = Column(Integer, ForeignKey("alunos.id"))
disciplina_id = Column(Integer, ForeignKey("disciplinas.id"))
valor = Column(Float)
bimestre = Column(Integer)
ano = Column(Integer)
criado_em = Column(DateTime, server_default=func.now())
aluno = relationship("Aluno", back_populates="notas")
disciplina = relationship("Disciplina", back_populates="notas")
# app/dados/queries.py
import pandas as pd
from sqlalchemy import text
from app.database import engine
def media_por_turma(ano: int = 2024) -> pd.DataFrame:
sql = text("""
SELECT
t.nome AS turma,
t.turno,
COUNT(DISTINCT a.id) AS n_alunos,
ROUND(AVG(n.valor), 2) AS media,
ROUND(MIN(n.valor), 2) AS menor,
ROUND(MAX(n.valor), 2) AS maior,
ROUND(STDDEV(n.valor), 2) AS desvio,
ROUND(
SUM(CASE WHEN n.valor >= 6 THEN 1 ELSE 0 END) * 100.0
/ COUNT(n.id), 1
) AS pct_aprovados
FROM turmas t
JOIN alunos a ON a.turma_id = t.id
JOIN notas n ON n.aluno_id = a.id
WHERE n.ano = :ano AND a.ativo = true
GROUP BY t.id, t.nome, t.turno
ORDER BY media DESC
""")
return pd.read_sql(sql, engine, params={"ano": ano})
def evolucao_bimestral(turma_id: int = None, ano: int = 2024) -> pd.DataFrame:
sql = text("""
SELECT
n.bimestre,
t.nome AS turma,
d.nome AS disciplina,
ROUND(AVG(n.valor), 2) AS media
FROM notas n
JOIN alunos a ON a.id = n.aluno_id
JOIN turmas t ON t.id = a.turma_id
JOIN disciplinas d ON d.id = n.disciplina_id
WHERE n.ano = :ano
AND (:turma_id IS NULL OR t.id = :turma_id)
AND a.ativo = true
GROUP BY n.bimestre, t.nome, d.nome
ORDER BY n.bimestre, t.nome
""")
return pd.read_sql(sql, engine, params={"ano": ano, "turma_id": turma_id})
def distribuicao_notas(turma_id: int = None, ano: int = 2024) -> pd.DataFrame:
sql = text("""
SELECT
a.nome AS aluno,
t.nome AS turma,
d.nome AS disciplina,
n.valor AS nota,
n.bimestre,
CASE
WHEN n.valor >= 9 THEN 'Excelente'
WHEN n.valor >= 7 THEN 'Bom'
WHEN n.valor >= 6 THEN 'Regular'
WHEN n.valor >= 5 THEN 'Recuperação'
ELSE 'Reprovado'
END AS conceito
FROM notas n
JOIN alunos a ON a.id = n.aluno_id
JOIN turmas t ON t.id = a.turma_id
JOIN disciplinas d ON d.id = n.disciplina_id
WHERE n.ano = :ano
AND (:turma_id IS NULL OR t.id = :turma_id)
AND a.ativo = true
ORDER BY a.nome, d.nome, n.bimestre
""")
return pd.read_sql(sql, engine, params={"ano": ano, "turma_id": turma_id})
def ranking_alunos(turma_id: int = None, ano: int = 2024, limite: int = 20) -> pd.DataFrame:
sql = text("""
SELECT
a.id,
a.nome AS aluno,
t.nome AS turma,
ROUND(AVG(n.valor), 2) AS media_geral,
COUNT(DISTINCT n.disciplina_id) AS disciplinas,
SUM(CASE WHEN n.valor >= 6 THEN 1 ELSE 0 END) AS aprovacoes,
SUM(CASE WHEN n.valor < 6 THEN 1 ELSE 0 END) AS reprovacoes,
RANK() OVER (ORDER BY AVG(n.valor) DESC) AS posicao
FROM alunos a
JOIN turmas t ON t.id = a.turma_id
JOIN notas n ON n.aluno_id = a.id
WHERE n.ano = :ano
AND (:turma_id IS NULL OR t.id = :turma_id)
AND a.ativo = true
GROUP BY a.id, a.nome, t.nome
ORDER BY media_geral DESC
LIMIT :limite
""")
return pd.read_sql(sql, engine, params={
"ano": ano, "turma_id": turma_id, "limite": limite
})
def alertas_risco(ano: int = 2024) -> pd.DataFrame:
sql = text("""
WITH medias AS (
SELECT
a.id,
a.nome AS aluno,
t.nome AS turma,
t.id AS turma_id,
AVG(n.valor) AS media_geral,
MIN(n.valor) AS pior_nota,
SUM(CASE WHEN n.valor < 6 THEN 1 ELSE 0 END) AS n_reprovacoes
FROM alunos a
JOIN turmas t ON t.id = a.turma_id
JOIN notas n ON n.aluno_id = a.id
WHERE n.ano = :ano AND a.ativo = true
GROUP BY a.id, a.nome, t.nome, t.id
)
SELECT
*,
CASE
WHEN media_geral < 4 THEN 'CRÍTICO'
WHEN media_geral < 5 THEN 'ALTO'
WHEN media_geral < 6 THEN 'MODERADO'
ELSE 'BAIXO'
END AS nivel_risco
FROM medias
WHERE media_geral < 6
ORDER BY media_geral ASC
""")
return pd.read_sql(sql, engine, params={"ano": ano})
def kpis_gerais(ano: int = 2024) -> dict:
sql = text("""
SELECT
COUNT(DISTINCT a.id) AS total_alunos,
COUNT(DISTINCT t.id) AS total_turmas,
ROUND(AVG(n.valor), 2) AS media_geral,
ROUND(
SUM(CASE WHEN n.valor >= 6 THEN 1 ELSE 0 END) * 100.0
/ COUNT(n.id), 1
) AS pct_aprovados,
SUM(CASE WHEN sub.media < 6 THEN 1 ELSE 0 END) AS alunos_risco
FROM notas n
JOIN alunos a ON a.id = n.aluno_id
JOIN turmas t ON t.id = a.turma_id
LEFT JOIN (
SELECT aluno_id, AVG(valor) AS media
FROM notas WHERE ano = :ano
GROUP BY aluno_id
) sub ON sub.aluno_id = a.id
WHERE n.ano = :ano AND a.ativo = true
""")
with engine.connect() as conn:
row = conn.execute(sql, {"ano": ano}).fetchone()
return dict(row._mapping) if row else {}
Componentes Visuais
# app/componentes/graficos.py
import plotly.express as px
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import pandas as pd
CORES = {
"primaria": "#1F4E79",
"secundaria": "#2E75B6",
"sucesso": "#375623",
"aviso": "#F5A623",
"perigo": "#C00000",
"neutro": "#888888",
}
TEMPLATE = "plotly_white"
def grafico_media_turmas(df: pd.DataFrame) -> go.Figure:
if df.empty:
return go.Figure()
cores = [
CORES["sucesso"] if m >= 7
else CORES["aviso"] if m >= 6
else CORES["perigo"]
for m in df["media"]
]
fig = go.Figure()
fig.add_trace(go.Bar(
x=df["turma"], y=df["media"],
marker_color=cores,
text=df["media"].round(2),
textposition="outside",
hovertemplate=(
"<b>%{x}</b><br>"
"Média: %{y:.2f}<br>"
"Aprovados: %{customdata[0]:.1f}%<br>"
"Alunos: %{customdata[1]}<br>"
"<extra></extra>"
),
customdata=df[["pct_aprovados", "n_alunos"]].values
))
fig.add_hline(
y=6, line_dash="dash",
line_color=CORES["perigo"],
annotation_text="Mínimo (6.0)",
annotation_position="right"
)
fig.add_hline(
y=df["media"].mean(),
line_dash="dot",
line_color=CORES["neutro"],
annotation_text=f"Média geral ({df['media'].mean():.2f})",
annotation_position="left"
)
fig.update_layout(
title="Média por Turma",
yaxis=dict(range=[0, 11], title="Média"),
xaxis_title="Turma",
template=TEMPLATE,
showlegend=False,
height=380
)
return fig
def grafico_evolucao_bimestral(df: pd.DataFrame, turma: str = None) -> go.Figure:
if df.empty:
return go.Figure()
if turma:
df = df[df["turma"] == turma]
fig = px.line(
df,
x="bimestre", y="media",
color="disciplina",
markers=True,
title=f"Evolução Bimestral{f' — {turma}' if turma else ''}",
labels={"bimestre": "Bimestre", "media": "Média", "disciplina": "Disciplina"},
template=TEMPLATE,
height=380
)
fig.add_hline(y=6, line_dash="dash", line_color=CORES["perigo"], opacity=0.5)
fig.update_xaxes(tickmode="linear", tick0=1, dtick=1)
return fig
def grafico_distribuicao(df: pd.DataFrame) -> go.Figure:
if df.empty:
return go.Figure()
ordem = ["Reprovado", "Recuperação", "Regular", "Bom", "Excelente"]
contagem = df["conceito"].value_counts().reindex(ordem, fill_value=0)
cores = [
CORES["perigo"], "#FF6B35",
CORES["aviso"], "#5B9BD5", CORES["sucesso"]
]
fig = go.Figure(go.Bar(
x=contagem.index, y=contagem.values,
marker_color=cores,
text=contagem.values,
textposition="outside",
hovertemplate="<b>%{x}</b><br>Quantidade: %{y}<extra></extra>"
))
fig.update_layout(
title="Distribuição por Conceito",
xaxis_title="Conceito",
yaxis_title="Quantidade",
template=TEMPLATE,
showlegend=False,
height=350
)
return fig
def grafico_heatmap_disciplinas(df: pd.DataFrame) -> go.Figure:
if df.empty:
return go.Figure()
pivot = df.pivot_table(
values="nota",
index="disciplina",
columns="bimestre",
aggfunc="mean"
).round(2)
fig = go.Figure(go.Heatmap(
z=pivot.values,
x=[f"Bimestre {b}" for b in pivot.columns],
y=pivot.index,
colorscale=[
[0.0, CORES["perigo"]],
[0.5, CORES["aviso"]],
[0.7, "#5B9BD5"],
[1.0, CORES["sucesso"]]
],
zmid=6,
text=pivot.values,
texttemplate="%{text:.1f}",
hovertemplate=(
"<b>%{y}</b><br>"
"%{x}<br>"
"Média: %{z:.2f}<extra></extra>"
)
))
fig.update_layout(
title="Média por Disciplina e Bimestre",
template=TEMPLATE,
height=400
)
return fig
def grafico_radar_turma(df_turmas: pd.DataFrame) -> go.Figure:
"""Gráfico radar comparando turmas em múltiplos indicadores."""
if df_turmas.empty:
return go.Figure()
categorias = ["média", "% aprovados", "consistência"]
fig = go.Figure()
for _, row in df_turmas.iterrows():
consistencia = max(0, 10 - (row.get("desvio", 2) * 2))
valores = [
row["media"],
row["pct_aprovados"] / 10,
consistencia
]
fig.add_trace(go.Scatterpolar(
r=valores + [valores[0]],
theta=categorias + [categorias[0]],
fill="toself",
name=row["turma"],
opacity=0.7
))
fig.update_layout(
polar=dict(radialaxis=dict(visible=True, range=[0, 10])),
title="Comparativo de Turmas",
template=TEMPLATE,
height=380
)
return fig
# app/componentes/cartoes.py
from dash import html
import dash_bootstrap_components as dbc
def cartao_kpi(
titulo: str,
valor: str,
subtitulo: str = "",
cor: str = "primary",
icone: str = "📊"
) -> dbc.Card:
cores_map = {
"primary": "#1F4E79",
"success": "#375623",
"warning": "#F5A623",
"danger": "#C00000",
}
cor_hex = cores_map.get(cor, "#1F4E79")
return dbc.Card([
dbc.CardBody([
html.Div([
html.Span(icone, style={"fontSize": "2rem"}),
html.Div([
html.H4(valor, className="mb-0",
style={"color": cor_hex, "fontWeight": "bold"}),
html.P(titulo, className="mb-0 text-muted",
style={"fontSize": "0.85rem"}),
html.Small(subtitulo, className="text-muted")
if subtitulo else html.Span()
])
], style={"display": "flex", "gap": "1rem", "alignItems": "center"})
])
], style={
"borderLeft": f"4px solid {cor_hex}",
"borderRadius": "8px",
"boxShadow": "0 2px 8px rgba(0,0,0,0.08)"
})
Layout do Dashboard
# app/layout/paginas/visao_geral.py
from dash import html, dcc
import dash_bootstrap_components as dbc
from app.dados.queries import kpis_gerais, media_por_turma, distribuicao_notas
from app.componentes.cartoes import cartao_kpi
from app.componentes.graficos import (
grafico_media_turmas, grafico_distribuicao, grafico_radar_turma
)
def layout_visao_geral(ano: int = 2024):
kpis = kpis_gerais(ano)
turmas = media_por_turma(ano)
notas = distribuicao_notas(ano=ano)
return html.Div([
# ── Cabeçalho ──────────────────────────────
html.Div([
html.H2("📊 Visão Geral", className="mb-1"),
html.P(f"Ano letivo {ano}", className="text-muted")
], className="mb-4"),
# ── KPIs ───────────────────────────────────
dbc.Row([
dbc.Col(cartao_kpi(
"Total de Alunos",
str(kpis.get("total_alunos", 0)),
icone="👥", cor="primary"
), md=3),
dbc.Col(cartao_kpi(
"Média Geral",
f"{kpis.get('media_geral', 0):.2f}",
icone="📈",
cor="success" if kpis.get("media_geral", 0) >= 7 else "warning"
), md=3),
dbc.Col(cartao_kpi(
"Taxa de Aprovação",
f"{kpis.get('pct_aprovados', 0):.1f}%",
icone="✅",
cor="success" if kpis.get("pct_aprovados", 0) >= 80 else "warning"
), md=3),
dbc.Col(cartao_kpi(
"Alunos em Risco",
str(kpis.get("alunos_risco", 0)),
icone="⚠️", cor="danger"
), md=3),
], className="mb-4 g-3"),
# ── Gráficos ───────────────────────────────
dbc.Row([
dbc.Col(dcc.Graph(
id="grafico-turmas",
figure=grafico_media_turmas(turmas),
config={"displayModeBar": False}
), md=8),
dbc.Col(dcc.Graph(
id="grafico-radar",
figure=grafico_radar_turma(turmas),
config={"displayModeBar": False}
), md=4),
], className="mb-4"),
dbc.Row([
dbc.Col(dcc.Graph(
id="grafico-distribuicao",
figure=grafico_distribuicao(notas),
config={"displayModeBar": False}
), md=12),
]),
], className="p-4")
# app/layout/paginas/alertas.py
from dash import html, dash_table
import dash_bootstrap_components as dbc
from app.dados.queries import alertas_risco
import pandas as pd
def layout_alertas(ano: int = 2024):
df = alertas_risco(ano)
cores_risco = {
"CRÍTICO": "#C00000",
"ALTO": "#FF6B35",
"MODERADO": "#F5A623",
"BAIXO": "#5B9BD5"
}
return html.Div([
html.Div([
html.H2("⚠️ Alertas — Alunos em Risco", className="mb-1"),
html.P(f"{len(df)} aluno(s) necessitam atenção", className="text-muted")
], className="mb-4"),
# Resumo por nível
dbc.Row([
dbc.Col([
dbc.Badge(
f"🔴 Crítico: {len(df[df['nivel_risco']=='CRÍTICO'])}",
color="danger", className="me-2 p-2 fs-6"
),
dbc.Badge(
f"🟠 Alto: {len(df[df['nivel_risco']=='ALTO'])}",
color="warning", className="me-2 p-2 fs-6"
),
dbc.Badge(
f"🟡 Moderado: {len(df[df['nivel_risco']=='MODERADO'])}",
color="info", className="p-2 fs-6"
),
], className="mb-4")
]),
# Tabela interativa
dash_table.DataTable(
id="tabela-alertas",
data=df.to_dict("records"),
columns=[
{"name": "Aluno", "id": "aluno"},
{"name": "Turma", "id": "turma"},
{"name": "Média", "id": "media_geral", "type": "numeric",
"format": {"specifier": ".2f"}},
{"name": "Pior Nota", "id": "pior_nota", "type": "numeric",
"format": {"specifier": ".2f"}},
{"name": "Reprovações", "id": "n_reprovacoes", "type": "numeric"},
{"name": "Risco", "id": "nivel_risco"},
],
style_table={"overflowX": "auto"},
style_cell={
"fontFamily": "Arial, sans-serif",
"fontSize": "0.9rem",
"padding": "10px"
},
style_header={
"backgroundColor": "#1F4E79",
"color": "white",
"fontWeight": "bold"
},
style_data_conditional=[
{
"if": {"filter_query": '{nivel_risco} = "CRÍTICO"'},
"backgroundColor": "#FCE4D6",
"color": "#C00000"
},
{
"if": {"filter_query": '{nivel_risco} = "ALTO"'},
"backgroundColor": "#FFF2CC",
},
{
"if": {"row_index": "odd"},
"backgroundColor": "#F9F9F9"
}
],
sort_action="native",
filter_action="native",
page_size=15,
export_format="csv",
)
], className="p-4")
Aplicação Principal
# app/main.py
import dash
from dash import html, dcc, Input, Output, State
import dash_bootstrap_components as dbc
from app.dados.queries import media_por_turma
from app.componentes.graficos import grafico_media_turmas, grafico_evolucao_bimestral
from app.layout.paginas.visao_geral import layout_visao_geral
from app.layout.paginas.alertas import layout_alertas
app = dash.Dash(
__name__,
external_stylesheets=[
dbc.themes.BOOTSTRAP,
"https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap"
],
suppress_callback_exceptions=True,
title="Escola — Dashboard Analítico",
update_title=None
)
# ── Sidebar ──────────────────────────────────────────
sidebar = html.Div([
html.Div([
html.H4("🎓 Escola", style={"color": "white", "margin": "0"}),
html.Small("Dashboard Analítico",
style={"color": "rgba(255,255,255,0.7)"})
], style={"padding": "1.5rem 1rem", "borderBottom": "1px solid rgba(255,255,255,0.1)"}),
html.Nav([
dbc.Nav([
dbc.NavLink([
html.Span("📊", className="me-2"), "Visão Geral"
], href="/", active="exact", className="nav-item-custom"),
dbc.NavLink([
html.Span("🏫", className="me-2"), "Turmas"
], href="/turmas", active="exact", className="nav-item-custom"),
dbc.NavLink([
html.Span("👥", className="me-2"), "Alunos"
], href="/alunos", active="exact", className="nav-item-custom"),
dbc.NavLink([
html.Span("⚠️", className="me-2"), "Alertas"
], href="/alertas", active="exact", className="nav-item-custom"),
], vertical=True)
], style={"padding": "1rem 0"}),
# Filtros globais
html.Div([
html.Label("Ano Letivo", style={"color": "rgba(255,255,255,0.8)",
"fontSize": "0.8rem"}),
dcc.Dropdown(
id="filtro-ano",
options=[{"label": str(a), "value": a} for a in [2022, 2023, 2024]],
value=2024,
clearable=False,
style={"fontSize": "0.9rem"}
),
html.Label("Turma", style={"color": "rgba(255,255,255,0.8)",
"fontSize": "0.8rem", "marginTop": "1rem"}),
dcc.Dropdown(
id="filtro-turma",
options=[{"label": "Todas", "value": "todas"}],
value="todas",
clearable=False,
style={"fontSize": "0.9rem"}
)
], style={"padding": "1rem", "borderTop": "1px solid rgba(255,255,255,0.1)",
"marginTop": "auto"}),
], style={
"width": "240px",
"minHeight": "100vh",
"background": "linear-gradient(180deg, #1F4E79 0%, #2E75B6 100%)",
"position": "fixed",
"top": 0,
"left": 0,
"display": "flex",
"flexDirection": "column"
})
# ── Header ────────────────────────────────────────────
header = html.Div([
html.Div(id="titulo-pagina",
style={"fontWeight": "600", "fontSize": "1.1rem"}),
html.Div([
dbc.Badge(id="badge-ano", color="primary", className="me-2"),
html.Span(id="ultima-atualizacao",
style={"fontSize": "0.8rem", "color": "#888"})
], style={"display": "flex", "alignItems": "center"})
], style={
"background": "white",
"padding": "1rem 2rem",
"borderBottom": "1px solid #eee",
"display": "flex",
"justifyContent": "space-between",
"alignItems": "center",
"boxShadow": "0 2px 4px rgba(0,0,0,0.05)"
})
# ── Layout Principal ──────────────────────────────────
app.layout = html.Div([
dcc.Location(id="url", refresh=False),
dcc.Interval(id="intervalo-refresh", interval=5 * 60 * 1000), # 5 min
sidebar,
html.Div([
header,
html.Div(id="conteudo-pagina",
style={"background": "#F4F7FB", "minHeight": "calc(100vh - 60px)"})
], style={"marginLeft": "240px"})
])
# ── Callbacks ─────────────────────────────────────────
@app.callback(
Output("conteudo-pagina", "children"),
Output("titulo-pagina", "children"),
Output("badge-ano", "children"),
Input("url", "pathname"),
Input("filtro-ano", "value"),
Input("filtro-turma","value"),
)
def renderizar_pagina(pathname, ano, turma_id):
turma = None if turma_id == "todas" else int(turma_id) if turma_id else None
badge = f"Ano {ano}"
rotas = {
"/": ("Visão Geral", layout_visao_geral(ano)),
"/alertas": ("Alertas de Risco", layout_alertas(ano)),
}
titulo, conteudo = rotas.get(pathname, ("Página não encontrada", html.Div("404")))
return conteudo, titulo, badge
@app.callback(
Output("filtro-turma", "options"),
Input("filtro-ano", "value")
)
def atualizar_opcoes_turma(ano):
df = media_por_turma(ano)
opcoes = [{"label": "Todas", "value": "todas"}]
opcoes += [{"label": row["turma"], "value": str(i + 1)}
for i, (_, row) in enumerate(df.iterrows())]
return opcoes
@app.callback(
Output("ultima-atualizacao", "children"),
Input("intervalo-refresh", "n_intervals")
)
def atualizar_timestamp(n):
from datetime import datetime
return f"Atualizado: {datetime.now().strftime('%H:%M:%S')}"
Script de Dados de Demonstração
# scripts/popular_banco.py
import numpy as np
from sqlalchemy.orm import Session
from app.database import engine, Base, Turma, Aluno, Disciplina, Nota
from passlib.context import CryptContext
import random
pwd = CryptContext(schemes=["bcrypt"], deprecated="auto")
rng = np.random.default_rng(42)
Base.metadata.create_all(engine)
with Session(engine) as session:
# Turmas
turmas = [
Turma(nome="9A", turno="manha", ano=2024),
Turma(nome="9B", turno="tarde", ano=2024),
Turma(nome="8A", turno="manha", ano=2024),
Turma(nome="8B", turno="noite", ano=2024),
]
session.add_all(turmas)
session.flush()
# Disciplinas
disciplinas = [
Disciplina(nome="Matemática"),
Disciplina(nome="Português"),
Disciplina(nome="Ciências"),
Disciplina(nome="História"),
Disciplina(nome="Geografia"),
]
session.add_all(disciplinas)
session.flush()
# Alunos e notas
nomes = [
"Ana Silva", "Bruno Costa", "Carla Souza", "Diego Lima",
"Elena Pires", "Fábio Santos", "Gabriela Rocha", "Hugo Alves",
"Isabela Ferreira", "João Pedro", "Karen Oliveira", "Lucas Mendes",
]
for i, nome in enumerate(nomes):
turma = turmas[i % len(turmas)]
aluno = Aluno(
nome=nome,
email=f"{nome.split()[0].lower()}{i}@escola.com",
senha_hash=pwd.hash("Senha123"),
turma_id=turma.id
)
session.add(aluno)
session.flush()
# Perfil de desempenho variado
media_base = rng.normal(6.5 + (i % 4) * 0.5, 1.0)
for disc in disciplinas:
for bimestre in [1, 2, 3, 4]:
nota_val = float(np.clip(
rng.normal(media_base, 0.8), 0, 10
))
session.add(Nota(
aluno_id=aluno.id,
disciplina_id=disc.id,
valor=round(nota_val, 1),
bimestre=bimestre,
ano=2024
))
session.commit()
print("Banco populado com sucesso!")
Docker
# Dockerfile
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app/ ./app/
COPY scripts/ ./scripts/
COPY assets/ ./assets/
EXPOSE 8050
CMD ["gunicorn", "app.main:server", \
"--bind", "0.0.0.0:8050", \
"--workers", "2", "--timeout", "120"]
# docker-compose.yml
version: "3.9"
services:
dashboard:
build: .
ports:
- "8050:8050"
environment:
- DATABASE_URL=postgresql://escola:senha@db:5432/escola
depends_on:
db:
condition: service_healthy
db:
image: postgres:16-alpine
environment:
- POSTGRES_USER=escola
- POSTGRES_PASSWORD=senha
- POSTGRES_DB=escola
volumes:
- pg_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U escola"]
interval: 10s
retries: 5
volumes:
pg_data:
Resumo do Projeto
| Componente | Tecnologias |
|---|---|
| Frontend | Dash, Plotly, Bootstrap |
| Backend | Python, pandas, SQLAlchemy |
| Banco | PostgreSQL, SQL analítico |
| Visualizações | Barras, Linha, Heatmap, Radar, Pizza |
| Interatividade | Callbacks reativos, filtros, DataTable |
| Deploy | Docker, Gunicorn, docker-compose |
| Dados demo | SQLAlchemy ORM, NumPy para geração |
Referências e Leituras Complementares
- Dash — documentação oficial — https://dash.plotly.com/
- Dash Bootstrap Components — https://dash-bootstrap-components.opensource.faculty.ai/
- Plotly Python — https://plotly.com/python/
- dash_table — DataTable — https://dash.plotly.com/datatable
- Gunicorn — deploy WSGI — https://gunicorn.org/
- HOTZ, Adam; SCHEIDEGGER, Carlos. Interactive Data Visualization with Python. Packt, 2020. — visualizações interativas com Plotly e Dash.
Prof. Ricardo Matos — Dominando o Python em 1 Ano · Artigo 51 de 52 Próximo: Artigo 52 — Carreira: portfólio, GitHub, open source e o mercado Python
you asked
Sim