dashgusbr 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
dashgusbr/__init__.py ADDED
@@ -0,0 +1,39 @@
1
+ """dashgusbr — consumo e visualização do histórico do Campeonato Brasileiro.
2
+
3
+ Camada de consumo da arquitetura serverless do projeto Infra-Brasileirao:
4
+ lê a OBT (One Big Table) publicada pelo pipeline ETL (CSV no GitHub, com
5
+ fallback para Google Sheets) e oferece análises em Pandas e gráficos Plotly.
6
+
7
+ Uso rápido::
8
+
9
+ from dashgusbr import Brasileirao
10
+
11
+ br = Brasileirao()
12
+ br.tabela(2023) # DataFrame da classificação
13
+ br.plot_confronto("Flamengo", "Palmeiras").show()
14
+
15
+ Uso avançado (camadas puras)::
16
+
17
+ from dashgusbr import analytics, data, viz
18
+
19
+ df = data.carregar_dados()
20
+ tab = analytics.classificacao(df, ano=2023)
21
+ fig = viz.classificacao(tab)
22
+ """
23
+
24
+ from . import analytics, config, data, schema, viz
25
+ from .client import Brasileirao
26
+ from .data import carregar_dados
27
+
28
+ __version__ = "0.1.0"
29
+
30
+ __all__ = [
31
+ "Brasileirao",
32
+ "carregar_dados",
33
+ "analytics",
34
+ "config",
35
+ "data",
36
+ "schema",
37
+ "viz",
38
+ "__version__",
39
+ ]
dashgusbr/_theme.py ADDED
@@ -0,0 +1,113 @@
1
+ """Tema visual da dashgusbr (template Plotly + paleta).
2
+
3
+ Paleta validada para daltonismo (deutan/protan/tritan) em modo claro:
4
+ ordem fixa dos slots categóricos — nunca ciclar nem reordenar, a ordem é o
5
+ mecanismo de segurança para visão de cores. Sequencial = um matiz (azul),
6
+ claro→escuro. Cinza neutro para categorias "sem lado" (empates).
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import plotly.graph_objects as go
12
+ import plotly.io as pio
13
+
14
+ # Slots categóricos, em ordem fixa (identidade de séries)
15
+ CORES_CATEGORICAS = [
16
+ "#2a78d6", # 1 azul
17
+ "#008300", # 2 verde
18
+ "#e87ba4", # 3 magenta
19
+ "#eda100", # 4 amarelo
20
+ "#1baf7a", # 5 aqua
21
+ "#eb6834", # 6 laranja
22
+ "#4a3aa7", # 7 violeta
23
+ "#e34948", # 8 vermelho
24
+ ]
25
+
26
+ AZUL = CORES_CATEGORICAS[0]
27
+ VERDE = CORES_CATEGORICAS[1]
28
+
29
+ # Cinza neutro para marcas sem identidade de série (ex.: empates)
30
+ CINZA_NEUTRO = "#898781"
31
+
32
+ # Rampa sequencial (magnitude): azul claro→escuro
33
+ RAMPA_SEQUENCIAL = [
34
+ "#cde2fb",
35
+ "#9ec5f4",
36
+ "#6da7ec",
37
+ "#3987e5",
38
+ "#256abf",
39
+ "#184f95",
40
+ "#0d366b",
41
+ ]
42
+
43
+ # Superfície e tinta (chrome do gráfico)
44
+ SUPERFICIE = "#fcfcfb"
45
+ TINTA = "#0b0b0b"
46
+ TINTA_SECUNDARIA = "#52514e"
47
+ TINTA_MUTED = "#898781"
48
+ GRADE = "#e1e0d9"
49
+ EIXO = "#c3c2b7"
50
+
51
+ FONTE = 'system-ui, -apple-system, "Segoe UI", sans-serif'
52
+
53
+ TEMA = "dashgusbr"
54
+
55
+
56
+ def escala_sequencial() -> list:
57
+ """Rampa sequencial no formato de colorscale do Plotly (0..1)."""
58
+ n = len(RAMPA_SEQUENCIAL) - 1
59
+ return [[i / n, cor] for i, cor in enumerate(RAMPA_SEQUENCIAL)]
60
+
61
+
62
+ def registrar_tema() -> None:
63
+ """Registra (idempotente) o template ``dashgusbr`` no Plotly.
64
+
65
+ Não altera o template default global do usuário: cada figura da
66
+ biblioteca pede ``template="dashgusbr"`` explicitamente.
67
+ """
68
+ template = go.layout.Template(
69
+ layout=go.Layout(
70
+ paper_bgcolor=SUPERFICIE,
71
+ plot_bgcolor=SUPERFICIE,
72
+ colorway=CORES_CATEGORICAS,
73
+ font=dict(family=FONTE, color=TINTA, size=13),
74
+ title=dict(font=dict(size=16, color=TINTA), x=0, xanchor="left"),
75
+ margin=dict(l=64, r=32, t=64, b=48),
76
+ xaxis=dict(
77
+ gridcolor=GRADE,
78
+ linecolor=EIXO,
79
+ zerolinecolor=EIXO,
80
+ ticks="outside",
81
+ tickcolor=EIXO,
82
+ title=dict(font=dict(color=TINTA_SECUNDARIA)),
83
+ tickfont=dict(color=TINTA_MUTED, size=12),
84
+ ),
85
+ yaxis=dict(
86
+ gridcolor=GRADE,
87
+ linecolor=EIXO,
88
+ zerolinecolor=EIXO,
89
+ ticks="outside",
90
+ tickcolor=EIXO,
91
+ title=dict(font=dict(color=TINTA_SECUNDARIA)),
92
+ tickfont=dict(color=TINTA_MUTED, size=12),
93
+ ),
94
+ legend=dict(
95
+ orientation="h",
96
+ yanchor="bottom",
97
+ y=1.02,
98
+ xanchor="left",
99
+ x=0,
100
+ font=dict(color=TINTA_SECUNDARIA, size=12),
101
+ ),
102
+ hoverlabel=dict(
103
+ bgcolor="#ffffff",
104
+ bordercolor=GRADE,
105
+ font=dict(family=FONTE, color=TINTA, size=12),
106
+ ),
107
+ hovermode="closest",
108
+ )
109
+ )
110
+ pio.templates[TEMA] = template
111
+
112
+
113
+ registrar_tema()
dashgusbr/analytics.py ADDED
@@ -0,0 +1,363 @@
1
+ """Agregações analíticas sobre a OBT do Brasileirão.
2
+
3
+ Funções puras: recebem o DataFrame no schema canônico e devolvem DataFrames
4
+ (ou dicts) prontos para consumo ou visualização. Nenhuma função acessa rede
5
+ ou estado global — a carga é responsabilidade de :mod:`dashgusbr.data`.
6
+
7
+ Nota sobre pontuação: as colunas ``pontos_mandante``/``pontos_visitante`` já
8
+ vêm calculadas pelo ETL respeitando a regra da época (vitória valia 2 pontos
9
+ até 1994 e 3 pontos a partir de 1995). Por isso as agregações SOMAM esses
10
+ pontos em vez de recalcular 3-1-0, e o aproveitamento é normalizado pelo
11
+ valor da vitória vigente em cada temporada.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import difflib
17
+ from typing import Optional
18
+
19
+ import pandas as pd
20
+
21
+ from .schema import TIPO_FASE_PONTOS_CORRIDOS
22
+
23
+ # ---------------------------------------------------------------------------
24
+ # Helpers internos
25
+ # ---------------------------------------------------------------------------
26
+
27
+
28
+ def _validar_ano(df: pd.DataFrame, ano: int) -> None:
29
+ anos = df["ano_campeonato"].dropna().unique()
30
+ if ano not in anos:
31
+ raise ValueError(
32
+ f"Temporada {ano} não encontrada. Dados disponíveis de "
33
+ f"{int(anos.min())} a {int(anos.max())}."
34
+ )
35
+
36
+
37
+ def _validar_time(df: pd.DataFrame, time: str) -> None:
38
+ times = pd.unique(pd.concat([df["mandante"], df["visitante"]]).dropna())
39
+ if time not in times:
40
+ sugestoes = difflib.get_close_matches(time, list(times), n=3)
41
+ dica = f" Você quis dizer: {', '.join(sugestoes)}?" if sugestoes else ""
42
+ raise ValueError(f"Time {time!r} não encontrado na base.{dica}")
43
+
44
+
45
+ def _pontos_corridos(df: pd.DataFrame) -> pd.DataFrame:
46
+ """Mantém apenas jogos que contam para a tabela (fase de pontos corridos)."""
47
+ return df[df["tipo_fase"] == TIPO_FASE_PONTOS_CORRIDOS]
48
+
49
+
50
+ def _com_placar(df: pd.DataFrame) -> pd.DataFrame:
51
+ return df.dropna(subset=["gols_mandante", "gols_visitante"])
52
+
53
+
54
+ def _formato_longo(df: pd.DataFrame) -> pd.DataFrame:
55
+ """Uma linha por (partida, time): a partida vista da perspectiva de cada lado."""
56
+ mandante = pd.DataFrame(
57
+ {
58
+ "id_partida": df["id_partida"],
59
+ "ano_campeonato": df["ano_campeonato"],
60
+ "data": df["data"],
61
+ "time": df["mandante"],
62
+ "adversario": df["visitante"],
63
+ "local": "mandante",
64
+ "gols_pro": df["gols_mandante"],
65
+ "gols_contra": df["gols_visitante"],
66
+ "resultado": df["resultado_mandante"],
67
+ "pontos": df["pontos_mandante"],
68
+ }
69
+ )
70
+ visitante = pd.DataFrame(
71
+ {
72
+ "id_partida": df["id_partida"],
73
+ "ano_campeonato": df["ano_campeonato"],
74
+ "data": df["data"],
75
+ "time": df["visitante"],
76
+ "adversario": df["mandante"],
77
+ "local": "visitante",
78
+ "gols_pro": df["gols_visitante"],
79
+ "gols_contra": df["gols_mandante"],
80
+ "resultado": df["resultado_visitante"],
81
+ "pontos": df["pontos_visitante"],
82
+ }
83
+ )
84
+ return pd.concat([mandante, visitante], ignore_index=True)
85
+
86
+
87
+ def _valor_vitoria(df_temporada: pd.DataFrame) -> int:
88
+ """Quantos pontos valia a vitória na temporada (2 até 1994, 3 depois)."""
89
+ pontos = pd.concat(
90
+ [df_temporada["pontos_mandante"], df_temporada["pontos_visitante"]]
91
+ ).dropna()
92
+ if pontos.empty:
93
+ return 3
94
+ return int(pontos.max())
95
+
96
+
97
+ # ---------------------------------------------------------------------------
98
+ # Classificação
99
+ # ---------------------------------------------------------------------------
100
+
101
+
102
+ def classificacao(df: pd.DataFrame, ano: int) -> pd.DataFrame:
103
+ """Tabela de classificação da fase de pontos corridos de uma temporada.
104
+
105
+ Colunas: posicao, time, pontos, jogos, vitorias, empates, derrotas,
106
+ gols_pro, gols_contra, saldo, aproveitamento (%). Desempate no critério
107
+ CBF: pontos, vitórias, saldo de gols, gols pró.
108
+ """
109
+ _validar_ano(df, ano)
110
+ temporada = _com_placar(_pontos_corridos(df[df["ano_campeonato"] == ano]))
111
+ longo = _formato_longo(temporada)
112
+
113
+ tabela = (
114
+ longo.groupby("time")
115
+ .agg(
116
+ pontos=("pontos", "sum"),
117
+ jogos=("id_partida", "count"),
118
+ vitorias=("resultado", lambda r: int((r == "V").sum())),
119
+ empates=("resultado", lambda r: int((r == "E").sum())),
120
+ derrotas=("resultado", lambda r: int((r == "D").sum())),
121
+ gols_pro=("gols_pro", "sum"),
122
+ gols_contra=("gols_contra", "sum"),
123
+ )
124
+ .reset_index()
125
+ )
126
+ tabela["saldo"] = tabela["gols_pro"] - tabela["gols_contra"]
127
+
128
+ valor_vitoria = _valor_vitoria(temporada)
129
+ tabela["aproveitamento"] = (
130
+ 100 * tabela["pontos"] / (tabela["jogos"] * valor_vitoria)
131
+ ).astype(float).round(1)
132
+
133
+ tabela = tabela.sort_values(
134
+ by=["pontos", "vitorias", "saldo", "gols_pro", "time"],
135
+ ascending=[False, False, False, False, True],
136
+ ignore_index=True,
137
+ )
138
+ tabela.insert(0, "posicao", tabela.index + 1)
139
+ return tabela
140
+
141
+
142
+ # ---------------------------------------------------------------------------
143
+ # Evolução e histórico de um time
144
+ # ---------------------------------------------------------------------------
145
+
146
+
147
+ def evolucao_pontos(df: pd.DataFrame, time: str, ano: int) -> pd.DataFrame:
148
+ """Pontuação acumulada de um time, jogo a jogo, dentro de uma temporada.
149
+
150
+ A OBT não publica número de rodada, então a linha do tempo é ordenada
151
+ pela data da partida (coluna ``jogo`` = 1º, 2º, 3º... jogo do time).
152
+ """
153
+ _validar_ano(df, ano)
154
+ _validar_time(df, time)
155
+ temporada = _com_placar(_pontos_corridos(df[df["ano_campeonato"] == ano]))
156
+ longo = _formato_longo(temporada)
157
+ jogos = (
158
+ longo[longo["time"] == time]
159
+ .sort_values(["data", "id_partida"])
160
+ .reset_index(drop=True)
161
+ )
162
+ if jogos.empty:
163
+ raise ValueError(f"{time!r} não disputou pontos corridos em {ano}.")
164
+ jogos.insert(0, "jogo", jogos.index + 1)
165
+ jogos["pontos_acumulados"] = jogos["pontos"].cumsum().astype("Int64")
166
+ return jogos[
167
+ [
168
+ "jogo",
169
+ "data",
170
+ "time",
171
+ "adversario",
172
+ "local",
173
+ "gols_pro",
174
+ "gols_contra",
175
+ "resultado",
176
+ "pontos",
177
+ "pontos_acumulados",
178
+ ]
179
+ ]
180
+
181
+
182
+ def historico_time(df: pd.DataFrame, time: str) -> pd.DataFrame:
183
+ """Desempenho de um time temporada a temporada (posição, pontos, aproveitamento).
184
+
185
+ O aproveitamento é comparável entre eras (normalizado pelo valor da
186
+ vitória de cada temporada); a posição vem da classificação completa
187
+ da fase de pontos corridos de cada ano.
188
+ """
189
+ _validar_time(df, time)
190
+ pontos_corridos = _pontos_corridos(df)
191
+ anos = sorted(
192
+ pontos_corridos[
193
+ (pontos_corridos["mandante"] == time)
194
+ | (pontos_corridos["visitante"] == time)
195
+ ]["ano_campeonato"].dropna().unique()
196
+ )
197
+ linhas = []
198
+ for ano in anos:
199
+ tabela = classificacao(df, int(ano))
200
+ linha = tabela[tabela["time"] == time]
201
+ if linha.empty:
202
+ continue
203
+ registro = linha.iloc[0].to_dict()
204
+ registro["ano_campeonato"] = int(ano)
205
+ linhas.append(registro)
206
+ if not linhas:
207
+ raise ValueError(f"{time!r} nunca disputou fases de pontos corridos na base.")
208
+ historico = pd.DataFrame(linhas)
209
+ colunas = ["ano_campeonato"] + [c for c in historico.columns if c != "ano_campeonato"]
210
+ return historico[colunas].reset_index(drop=True)
211
+
212
+
213
+ # ---------------------------------------------------------------------------
214
+ # Confronto direto
215
+ # ---------------------------------------------------------------------------
216
+
217
+
218
+ def confronto(df: pd.DataFrame, time_a: str, time_b: str) -> dict:
219
+ """Histórico do confronto direto entre dois times (todas as fases).
220
+
221
+ Retorna um dict com o resumo (jogos, vitórias de cada lado, empates,
222
+ gols) e o DataFrame ``partidas`` com todos os jogos, do mais antigo ao
223
+ mais recente.
224
+ """
225
+ _validar_time(df, time_a)
226
+ _validar_time(df, time_b)
227
+ if time_a == time_b:
228
+ raise ValueError("Informe dois times diferentes para o confronto.")
229
+
230
+ partidas = _com_placar(
231
+ df[
232
+ ((df["mandante"] == time_a) & (df["visitante"] == time_b))
233
+ | ((df["mandante"] == time_b) & (df["visitante"] == time_a))
234
+ ]
235
+ ).sort_values(["data", "id_partida"]).reset_index(drop=True)
236
+
237
+ a_mandante = partidas["mandante"] == time_a
238
+ vitorias_a = (
239
+ (a_mandante & (partidas["resultado_mandante"] == "V"))
240
+ | (~a_mandante & (partidas["resultado_visitante"] == "V"))
241
+ ).sum()
242
+ vitorias_b = (
243
+ (~a_mandante & (partidas["resultado_mandante"] == "V"))
244
+ | (a_mandante & (partidas["resultado_visitante"] == "V"))
245
+ ).sum()
246
+ empates = (partidas["resultado_mandante"] == "E").sum()
247
+ gols_a = (
248
+ partidas.loc[a_mandante, "gols_mandante"].sum()
249
+ + partidas.loc[~a_mandante, "gols_visitante"].sum()
250
+ )
251
+ gols_b = (
252
+ partidas.loc[~a_mandante, "gols_mandante"].sum()
253
+ + partidas.loc[a_mandante, "gols_visitante"].sum()
254
+ )
255
+
256
+ return {
257
+ "time_a": time_a,
258
+ "time_b": time_b,
259
+ "jogos": int(len(partidas)),
260
+ "vitorias_a": int(vitorias_a),
261
+ "empates": int(empates),
262
+ "vitorias_b": int(vitorias_b),
263
+ "gols_a": int(gols_a),
264
+ "gols_b": int(gols_b),
265
+ "partidas": partidas,
266
+ }
267
+
268
+
269
+ # ---------------------------------------------------------------------------
270
+ # Estatísticas agregadas do campeonato
271
+ # ---------------------------------------------------------------------------
272
+
273
+
274
+ def estatisticas_temporada(df: pd.DataFrame) -> pd.DataFrame:
275
+ """Indicadores por temporada: jogos, gols, média de gols e mando de campo.
276
+
277
+ ``pct_vitorias_mandante``/``pct_empates``/``pct_vitorias_visitante`` medem
278
+ o peso do fator casa ao longo da história (somam 100% por temporada).
279
+ """
280
+ jogos = _com_placar(_pontos_corridos(df)).copy()
281
+ jogos["_gols_partida"] = jogos["gols_mandante"] + jogos["gols_visitante"]
282
+ stats = (
283
+ jogos.groupby("ano_campeonato")
284
+ .agg(
285
+ jogos=("id_partida", "count"),
286
+ gols=("_gols_partida", "sum"),
287
+ pct_vitorias_mandante=(
288
+ "resultado_mandante",
289
+ lambda r: round(100 * (r == "V").mean(), 1),
290
+ ),
291
+ pct_empates=(
292
+ "resultado_mandante",
293
+ lambda r: round(100 * (r == "E").mean(), 1),
294
+ ),
295
+ pct_vitorias_visitante=(
296
+ "resultado_mandante",
297
+ lambda r: round(100 * (r == "D").mean(), 1),
298
+ ),
299
+ )
300
+ .reset_index()
301
+ )
302
+ stats["media_gols"] = (stats["gols"] / stats["jogos"]).astype(float).round(2)
303
+ return stats[
304
+ [
305
+ "ano_campeonato",
306
+ "jogos",
307
+ "gols",
308
+ "media_gols",
309
+ "pct_vitorias_mandante",
310
+ "pct_empates",
311
+ "pct_vitorias_visitante",
312
+ ]
313
+ ]
314
+
315
+
316
+ def distribuicao_placares(
317
+ df: pd.DataFrame, ano: Optional[int] = None, max_gols: int = 6
318
+ ) -> pd.DataFrame:
319
+ """Matriz de frequência de placares (gols do mandante × gols do visitante).
320
+
321
+ Placares com mais de ``max_gols`` de um dos lados são agrupados no último
322
+ bin para a matriz não explodir por causa de goleadas raras. Índice =
323
+ gols do mandante, colunas = gols do visitante, valores = nº de jogos.
324
+ """
325
+ jogos = _com_placar(df if ano is None else df[df["ano_campeonato"] == ano])
326
+ if ano is not None:
327
+ _validar_ano(df, ano)
328
+ gm = jogos["gols_mandante"].clip(upper=max_gols).astype(int)
329
+ gv = jogos["gols_visitante"].clip(upper=max_gols).astype(int)
330
+ matriz = pd.crosstab(gm, gv)
331
+ eixo = range(0, max_gols + 1)
332
+ matriz = matriz.reindex(index=eixo, columns=eixo, fill_value=0)
333
+ matriz.index.name = "gols_mandante"
334
+ matriz.columns.name = "gols_visitante"
335
+ return matriz
336
+
337
+
338
+ def maiores_goleadas(df: pd.DataFrame, n: int = 10) -> pd.DataFrame:
339
+ """As ``n`` maiores goleadas da história (maior diferença de gols)."""
340
+ jogos = _com_placar(df).copy()
341
+ jogos["diferenca"] = (jogos["gols_mandante"] - jogos["gols_visitante"]).abs()
342
+ jogos["_gols_partida"] = jogos["gols_mandante"] + jogos["gols_visitante"]
343
+ jogos["placar"] = (
344
+ jogos["gols_mandante"].astype(int).astype(str)
345
+ + " x "
346
+ + jogos["gols_visitante"].astype(int).astype(str)
347
+ )
348
+ return (
349
+ jogos.sort_values(
350
+ ["diferenca", "_gols_partida", "data"], ascending=[False, False, True]
351
+ )
352
+ .head(n)[
353
+ [
354
+ "ano_campeonato",
355
+ "data",
356
+ "mandante",
357
+ "placar",
358
+ "visitante",
359
+ "diferenca",
360
+ ]
361
+ ]
362
+ .reset_index(drop=True)
363
+ )
dashgusbr/client.py ADDED
@@ -0,0 +1,183 @@
1
+ """Fachada pública da dashgusbr: a classe :class:`Brasileirao`.
2
+
3
+ Orquestra as camadas puras (``data`` → ``analytics`` → ``viz``) com um
4
+ DataFrame cacheado por instância. Usuários avançados podem importar as
5
+ camadas diretamente (``from dashgusbr import analytics, viz``).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Iterable, Optional, Union
11
+
12
+ import pandas as pd
13
+ import plotly.graph_objects as go
14
+
15
+ from . import analytics, data, viz
16
+
17
+
18
+ class Brasileirao:
19
+ """Ponto de entrada da biblioteca.
20
+
21
+ Carrega a OBT sob demanda (no primeiro uso, não no construtor) e a mantém
22
+ em memória; todos os métodos de análise e plot operam sobre essa cópia.
23
+
24
+ Examples
25
+ --------
26
+ >>> from dashgusbr import Brasileirao
27
+ >>> br = Brasileirao()
28
+ >>> br.tabela(2023).head()
29
+ >>> br.plot_confronto("Flamengo", "Palmeiras").show()
30
+ """
31
+
32
+ def __init__(
33
+ self,
34
+ fonte: str = "auto",
35
+ github_url: Optional[str] = None,
36
+ sheets_url: Optional[str] = None,
37
+ cache: bool = True,
38
+ ) -> None:
39
+ self._fonte = fonte
40
+ self._github_url = github_url
41
+ self._sheets_url = sheets_url
42
+ self._cache = cache
43
+ self._df: Optional[pd.DataFrame] = None
44
+
45
+ # -- dados -------------------------------------------------------------
46
+
47
+ @property
48
+ def df(self) -> pd.DataFrame:
49
+ """A OBT completa (carga preguiçosa; não modifique in-place)."""
50
+ if self._df is None:
51
+ self._df = data.carregar_dados(
52
+ fonte=self._fonte,
53
+ github_url=self._github_url,
54
+ sheets_url=self._sheets_url,
55
+ cache=self._cache,
56
+ )
57
+ return self._df
58
+
59
+ def recarregar(self) -> "Brasileirao":
60
+ """Força novo download da fonte, ignorando os caches."""
61
+ self._df = data.carregar_dados(
62
+ fonte=self._fonte,
63
+ github_url=self._github_url,
64
+ sheets_url=self._sheets_url,
65
+ cache=self._cache,
66
+ forcar_download=True,
67
+ )
68
+ return self
69
+
70
+ def partidas(
71
+ self, ano: Optional[int] = None, time: Optional[str] = None
72
+ ) -> pd.DataFrame:
73
+ """Partidas da base, opcionalmente filtradas por temporada e/ou time."""
74
+ partidas = self.df
75
+ if ano is not None:
76
+ partidas = partidas[partidas["ano_campeonato"] == ano]
77
+ if time is not None:
78
+ partidas = partidas[
79
+ (partidas["mandante"] == time) | (partidas["visitante"] == time)
80
+ ]
81
+ return partidas.reset_index(drop=True).copy()
82
+
83
+ def anos(self) -> "list[int]":
84
+ """Temporadas disponíveis na base, em ordem crescente."""
85
+ return sorted(int(a) for a in self.df["ano_campeonato"].dropna().unique())
86
+
87
+ def times(self, ano: Optional[int] = None) -> "list[str]":
88
+ """Times presentes na base (ou apenas em uma temporada), em ordem alfabética."""
89
+ partidas = self.partidas(ano=ano)
90
+ return sorted(
91
+ pd.unique(pd.concat([partidas["mandante"], partidas["visitante"]]).dropna())
92
+ )
93
+
94
+ # -- classificação -----------------------------------------------------
95
+
96
+ def tabela(self, ano: int) -> pd.DataFrame:
97
+ """Classificação da fase de pontos corridos da temporada."""
98
+ return analytics.classificacao(self.df, ano)
99
+
100
+ def plot_tabela(self, ano: int) -> go.Figure:
101
+ """Gráfico de barras da classificação da temporada."""
102
+ return viz.classificacao(
103
+ self.tabela(ano), titulo=f"Brasileirão {ano} — Classificação"
104
+ )
105
+
106
+ # -- evolução e histórico ----------------------------------------------
107
+
108
+ def evolucao(self, time: str, ano: int) -> pd.DataFrame:
109
+ """Pontos acumulados do time, jogo a jogo, na temporada."""
110
+ return analytics.evolucao_pontos(self.df, time, ano)
111
+
112
+ def plot_evolucao(
113
+ self, times: Union[str, Iterable[str]], ano: int
114
+ ) -> go.Figure:
115
+ """Linha(s) de pontos acumulados de um ou mais times na temporada."""
116
+ if isinstance(times, str):
117
+ times = [times]
118
+ evolucoes = pd.concat(
119
+ [analytics.evolucao_pontos(self.df, t, ano) for t in times],
120
+ ignore_index=True,
121
+ )
122
+ return viz.evolucao(
123
+ evolucoes, titulo=f"Brasileirão {ano} — Evolução de pontos"
124
+ )
125
+
126
+ def historico(self, time: str) -> pd.DataFrame:
127
+ """Desempenho do time temporada a temporada (posição, pontos, aproveitamento)."""
128
+ return analytics.historico_time(self.df, time)
129
+
130
+ def plot_historico(
131
+ self, time: str, metrica: str = "aproveitamento"
132
+ ) -> go.Figure:
133
+ """Linha do desempenho histórico do time (aproveitamento por padrão)."""
134
+ return viz.historico(self.historico(time), metrica=metrica)
135
+
136
+ # -- confronto direto ----------------------------------------------------
137
+
138
+ def confronto(self, time_a: str, time_b: str) -> dict:
139
+ """Resumo do confronto direto (inclui o DataFrame ``partidas``)."""
140
+ return analytics.confronto(self.df, time_a, time_b)
141
+
142
+ def plot_confronto(self, time_a: str, time_b: str) -> go.Figure:
143
+ """Barras de vitórias/empates do confronto direto."""
144
+ return viz.confronto(self.confronto(time_a, time_b))
145
+
146
+ # -- estatísticas do campeonato ------------------------------------------
147
+
148
+ def estatisticas(self) -> pd.DataFrame:
149
+ """Indicadores por temporada: jogos, gols, média de gols, fator casa."""
150
+ return analytics.estatisticas_temporada(self.df)
151
+
152
+ def plot_gols_por_temporada(self) -> go.Figure:
153
+ """Linha da média de gols por jogo em cada temporada."""
154
+ return viz.gols_por_temporada(self.estatisticas())
155
+
156
+ def plot_mandante_visitante(self) -> go.Figure:
157
+ """Linhas do fator casa (% vitórias mandante/empates/visitante)."""
158
+ return viz.mandante_visitante(self.estatisticas())
159
+
160
+ def placares(self, ano: Optional[int] = None, max_gols: int = 6) -> pd.DataFrame:
161
+ """Matriz de frequência de placares (mandante × visitante)."""
162
+ return analytics.distribuicao_placares(self.df, ano=ano, max_gols=max_gols)
163
+
164
+ def plot_placares(
165
+ self, ano: Optional[int] = None, max_gols: int = 6
166
+ ) -> go.Figure:
167
+ """Heatmap da distribuição de placares."""
168
+ titulo = (
169
+ f"Brasileirão {ano} — Distribuição de placares"
170
+ if ano is not None
171
+ else "Distribuição de placares (1971–hoje)"
172
+ )
173
+ return viz.distribuicao_placares(
174
+ self.placares(ano=ano, max_gols=max_gols), titulo=titulo
175
+ )
176
+
177
+ def goleadas(self, n: int = 10) -> pd.DataFrame:
178
+ """As ``n`` maiores goleadas da história do campeonato."""
179
+ return analytics.maiores_goleadas(self.df, n=n)
180
+
181
+ def __repr__(self) -> str:
182
+ estado = "não carregado" if self._df is None else f"{len(self._df)} partidas"
183
+ return f"Brasileirao(fonte={self._fonte!r}, dados: {estado})"