llm-pig 0.1.0__tar.gz

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.
llm_pig-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,38 @@
1
+ Metadata-Version: 2.4
2
+ Name: llm-pig
3
+ Version: 0.1.0
4
+ Summary: A Python library to visualize LLM perplexity metrics in interactive graphs
5
+ Author: Juan Morysson Viana Marciano
6
+ Author-email: juanmorysson@gmail.com
7
+ Requires-Python: >=3.10,<4.0
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.10
10
+ Classifier: Programming Language :: Python :: 3.11
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Classifier: Programming Language :: Python :: 3.13
13
+ Classifier: Programming Language :: Python :: 3.14
14
+ Requires-Dist: pandas (>=2.0.0)
15
+ Requires-Dist: plotly (>=5.0.0)
16
+ Description-Content-Type: text/markdown
17
+
18
+ # llm-pig 🐷
19
+
20
+ `llm-pig` is a comprehensive Python library designed for advanced LLM perplexity analysis, covering both token-level micro inspection and macro-level dataset evaluation. It provides clean, interactive HTML visualizations powered by Pandas and Plotly.
21
+
22
+ ---
23
+
24
+ ## Features
25
+
26
+ * **Token-Level Curve Analysis**: Interactive line charts tracking token uncertainty across a prompt or document.
27
+ * **Cascading Bar Charts**: Sorted bar representations highlighting peak perplexity tokens.
28
+ * **SHAP-Style Text Highlights**: Individual and comparative multi-model text highlights using dynamic color gradient scales.
29
+ * **Macro Statistical Distribution**: Comprehensive Boxplots and Violin Plots summarizing model performance across large test datasets (including Min, Q1, Median, Q3, and Max metrics).
30
+
31
+ ---
32
+
33
+ ## Installation
34
+
35
+ Install the package via pip:
36
+
37
+ ```bash
38
+ pip install llm-pig
@@ -0,0 +1,21 @@
1
+ # llm-pig 🐷
2
+
3
+ `llm-pig` is a comprehensive Python library designed for advanced LLM perplexity analysis, covering both token-level micro inspection and macro-level dataset evaluation. It provides clean, interactive HTML visualizations powered by Pandas and Plotly.
4
+
5
+ ---
6
+
7
+ ## Features
8
+
9
+ * **Token-Level Curve Analysis**: Interactive line charts tracking token uncertainty across a prompt or document.
10
+ * **Cascading Bar Charts**: Sorted bar representations highlighting peak perplexity tokens.
11
+ * **SHAP-Style Text Highlights**: Individual and comparative multi-model text highlights using dynamic color gradient scales.
12
+ * **Macro Statistical Distribution**: Comprehensive Boxplots and Violin Plots summarizing model performance across large test datasets (including Min, Q1, Median, Q3, and Max metrics).
13
+
14
+ ---
15
+
16
+ ## Installation
17
+
18
+ Install the package via pip:
19
+
20
+ ```bash
21
+ pip install llm-pig
@@ -0,0 +1,21 @@
1
+ [tool.poetry]
2
+ name = "llm-pig"
3
+ version = "0.1.0"
4
+ description = "A Python library to visualize LLM perplexity metrics in interactive graphs"
5
+ authors = ["Juan Morysson Viana Marciano <juanmorysson@gmail.com>"]
6
+ readme = "README.md"
7
+ packages = [{include = "llm_pig", from = "src"}]
8
+
9
+ [tool.poetry.dependencies]
10
+ python = "^3.10"
11
+ plotly = ">=5.0.0"
12
+ pandas = ">=2.0.0"
13
+
14
+ [build-system]
15
+ requires = ["poetry-core"]
16
+ build-backend = "poetry.core.masonry.api"
17
+
18
+ [dependency-groups]
19
+ dev = [
20
+ "pytest (>=9.1.1,<10.0.0)"
21
+ ]
@@ -0,0 +1,4 @@
1
+ from .parser import PerplexityParser
2
+ from .plots import PerplexityVisualizer
3
+
4
+ __all__ = ["PerplexityParser", "PerplexityVisualizer"]
@@ -0,0 +1,13 @@
1
+ import pandas as pd
2
+
3
+ class PerplexityParser:
4
+ def __init__(self, tokens: list, perplexities: list):
5
+ if len(tokens) != len(perplexities):
6
+ raise ValueError("A lista de tokens e a lista de perplexidades devem ter o mesmo tamanho.")
7
+ self.df = pd.DataFrame({
8
+ "token": tokens,
9
+ "perplexity": perplexities
10
+ })
11
+
12
+ def get_dataframe(self) -> pd.DataFrame:
13
+ return self.df
@@ -0,0 +1,558 @@
1
+ import plotly.express as px
2
+ from .parser import PerplexityParser
3
+
4
+ class PerplexityVisualizer:
5
+ def __init__(self, parser: PerplexityParser):
6
+ self.df = parser.get_dataframe()
7
+
8
+ def plot_token_curve(self, save_path: str = "perplexity_curve.html", x_axis: str = "index", tick_angle: int = 0):
9
+ """
10
+ Gera um gráfico de linha interativo da perplexidade por token.
11
+
12
+ Parâmetros:
13
+ - save_path (str): Caminho para salvar o arquivo HTML gerado.
14
+ - x_axis (str): Define o que aparece no eixo X ('index' ou 'token').
15
+ - tick_angle (int): Ângulo de rotação dos rótulos do eixo X (ex: 0 para normal, 90 para vertical).
16
+ """
17
+ if x_axis not in ["index", "token"]:
18
+ raise ValueError("O parâmetro 'x_axis' deve ser 'index' ou 'token'.")
19
+
20
+ x_data = self.df.index if x_axis == "index" else self.df["token"]
21
+
22
+ fig = px.line(
23
+ self.df,
24
+ x=x_data,
25
+ y="perplexity",
26
+ hover_data=["token"],
27
+ markers=True,
28
+ title="LLM Perplexity Curve by Token",
29
+ labels={"x": "Token Position" if x_axis == "index" else "Token", "perplexity": "Perplexity"}
30
+ )
31
+
32
+ fig.update_traces(
33
+ hovertemplate="<b>Token:</b> %{customdata[0]}<br><b>Perplexity:</b> %{y:.2f}<extra></extra>"
34
+ )
35
+
36
+ fig.update_layout(xaxis=dict(tickangle=tick_angle))
37
+
38
+ fig.write_html(save_path)
39
+ print(f"Interactive chart successfully saved to: {save_path}")
40
+ return fig
41
+
42
+ def plot_token_bars(self, save_path: str = "perplexity_bars.html", top_n: int = None, cascade: bool = True):
43
+ """
44
+ Gera um gráfico de barras horizontais com os tokens no eixo Y e valores exibidos nas barras.
45
+
46
+ Parâmetros:
47
+ - save_path (str): Caminho para salvar o arquivo HTML gerado.
48
+ - top_n (int, opcional): Limita a quantidade de tokens exibidos.
49
+ - cascade (bool): Se True (padrão), ordena do maior para o menor (maior perplexidade no topo).
50
+ """
51
+ df_plot = self.df.copy()
52
+
53
+ if cascade:
54
+ df_plot = df_plot.sort_values(by="perplexity", ascending=True)
55
+
56
+ if top_n is not None:
57
+ if cascade:
58
+ df_plot = df_plot.tail(top_n)
59
+ else:
60
+ df_plot = df_plot.head(top_n)
61
+
62
+ fig = px.bar(
63
+ df_plot,
64
+ x="perplexity",
65
+ y="token",
66
+ orientation="h",
67
+ text="perplexity", # Define a coluna que terá o valor exibido na barra
68
+ title="LLM Perplexity by Token (Bar Chart)",
69
+ labels={"perplexity": "Perplexity", "token": "Token"}
70
+ )
71
+
72
+ # Formata o texto para exibir duas casas decimais e posiciona o texto dentro/ao lado da barra
73
+ fig.update_traces(
74
+ texttemplate='%{text:.2f}',
75
+ textposition='inside',
76
+ hovertemplate="<b>Token:</b> %{y}<br><b>Perplexity:</b> %{x:.2f}<extra></extra>"
77
+ )
78
+
79
+ fig.write_html(save_path)
80
+ print(f"Interactive bar chart successfully saved to: {save_path}")
81
+ return fig
82
+
83
+ def perplexity_text(self, save_path: str = "perplexity_text.html", top_n: int = None):
84
+ """
85
+ Gera uma visualização em texto destacado (tipo SHAP) onde o degradê
86
+ e a legenda são recalculados com base apenas nos tokens destacados pelo filtro.
87
+ """
88
+ df = self.df.copy()
89
+
90
+ # Identifica os tokens que entram no top_n
91
+ if top_n is not None and top_n < len(df):
92
+ df_filtered = df.nlargest(top_n, "perplexity")
93
+ top_indices = df_filtered.index
94
+ else:
95
+ df_filtered = df
96
+ top_indices = df.index
97
+
98
+ # Recalcula min e max baseados estritamente nos itens que receberão o destaque
99
+ min_p = df_filtered["perplexity"].min()
100
+ max_p = df_filtered["perplexity"].max()
101
+
102
+ def get_color_and_text_color(val, is_highlighted):
103
+ if not is_highlighted:
104
+ return "transparent", "#333333", False
105
+
106
+ if max_p == min_p:
107
+ norm_val = 0.5
108
+ else:
109
+ # Normaliza usando o novo limite inferior do filtro
110
+ norm_val = (val - min_p) / (max_p - min_p)
111
+
112
+ r = int(255 - norm_val * (255 - 128))
113
+ g = int(192 - norm_val * 192)
114
+ b = int(203 - norm_val * (203 - 32))
115
+ alpha = 0.3 + (norm_val * 0.7)
116
+ bg_color = f"rgba({r}, {g}, {b}, {alpha})"
117
+ text_color = "#ffffff" if norm_val > 0.5 else "#333333"
118
+
119
+ return bg_color, text_color, True
120
+
121
+ spans = []
122
+ for idx, row in df.iterrows():
123
+ token = row["token"]
124
+ p_val = row["perplexity"]
125
+ is_hl = idx in top_indices
126
+
127
+ bg_color, text_color, highlighted = get_color_and_text_color(p_val, is_hl)
128
+ safe_token = token.replace("\n", "<br>")
129
+
130
+ if highlighted:
131
+ span_html = (
132
+ f'<span style="background-color: {bg_color}; color: {text_color}; '
133
+ f'padding: 3px 6px; margin: 2px; border-radius: 4px; display: inline-block; '
134
+ f'font-weight: 500;" title="Perplexity: {p_val:.2f}">{safe_token}</span>'
135
+ )
136
+ else:
137
+ span_html = (
138
+ f'<span style="background-color: transparent; color: {text_color}; '
139
+ f'padding: 3px 6px; margin: 2px; display: inline-block;" '
140
+ f'title="Perplexity: {p_val:.2f} (Not in Top {top_n})">{safe_token}</span>'
141
+ )
142
+ spans.append(span_html)
143
+
144
+ legend_start = "rgba(255, 192, 203, 0.3)"
145
+ legend_end = "rgba(128, 0, 32, 1.0)"
146
+
147
+ html_content = f"""
148
+ <!DOCTYPE html>
149
+ <html>
150
+ <head>
151
+ <meta charset="utf-8">
152
+ <title>LLM Perplexity Text Highlight</title>
153
+ <style>
154
+ body {{
155
+ font-family: Arial, sans-serif;
156
+ margin: 40px;
157
+ background-color: #f9f9f9;
158
+ color: #333;
159
+ }}
160
+ .container {{
161
+ background: #fff;
162
+ padding: 25px;
163
+ border-radius: 8px;
164
+ box-shadow: 0 2px 5px rgba(0,0,0,0.1);
165
+ line-height: 2.5;
166
+ font-size: 18px;
167
+ }}
168
+ h2 {{
169
+ margin-bottom: 20px;
170
+ }}
171
+ .legend-container {{
172
+ margin-top: 25px;
173
+ display: flex;
174
+ align-items: center;
175
+ gap: 15px;
176
+ font-size: 14px;
177
+ color: #555;
178
+ }}
179
+ .legend-bar {{
180
+ width: 200px;
181
+ height: 14px;
182
+ border-radius: 4px;
183
+ background: linear-gradient(to right, {legend_start}, {legend_end});
184
+ border: 1px solid #ddd;
185
+ }}
186
+ </style>
187
+ </head>
188
+ <body>
189
+ <h2>LLM Perplexity Text Highlight (Top {top_n if top_n else 'All'} Tokens)</h2>
190
+ <div class="container">
191
+ {"".join(spans)}
192
+
193
+ <div class="legend-container">
194
+ <span>Low ({min_p:.2f})</span>
195
+ <div class="legend-bar"></div>
196
+ <span>High ({max_p:.2f})</span>
197
+ </div>
198
+ </div>
199
+ </body>
200
+ </html>
201
+ """
202
+
203
+ with open(save_path, "w", encoding="utf-8") as f:
204
+ f.write(html_content)
205
+
206
+ print(f"Text highlight visualization with recalculated legend successfully saved to: {save_path}")
207
+
208
+ def compare_perplexity_texts(
209
+ self,
210
+ save_path: str = "perplexity_comparison.html",
211
+ tokens: list[list[str]] = None,
212
+ perplexities: list[list[float]] = None,
213
+ labels: list[str] = None,
214
+ top_n: int = None
215
+ ):
216
+ """
217
+ Gera uma visualização comparativa em texto destacado utilizando escala de cores
218
+ global e uma única legenda unificada no rodapé.
219
+ """
220
+ if not tokens or not perplexities or len(tokens) != len(perplexities):
221
+ raise ValueError("As listas 'tokens' e 'perplexities' devem ser fornecidas e ter o mesmo comprimento.")
222
+
223
+ if labels is None:
224
+ labels = [f"Model {i+1}" for i in range(len(tokens))]
225
+
226
+ if len(tokens) != len(labels):
227
+ raise ValueError("A quantidade de listas de tokens deve ser igual à quantidade de 'labels'.")
228
+
229
+ import pandas as pd
230
+
231
+ models_dfs = []
232
+ all_highlighted_perplexities = []
233
+
234
+ # Passo 1: Processa cada modelo e coleta os valores destacados globalmente
235
+ for model_tokens, model_perps, label in zip(tokens, perplexities, labels):
236
+ if len(model_tokens) != len(model_perps):
237
+ raise ValueError(f"As listas de tokens e perplexidades do modelo '{label}' devem ter o mesmo tamanho.")
238
+
239
+ df = pd.DataFrame({"token": model_tokens, "perplexity": model_perps})
240
+
241
+ if top_n is not None and top_n < len(df):
242
+ df_filtered = df.nlargest(top_n, "perplexity")
243
+ top_indices = set(df_filtered.index)
244
+ else:
245
+ top_indices = set(df.index)
246
+ df_filtered = df
247
+
248
+ models_dfs.append({"df": df, "top_indices": top_indices, "label": label})
249
+
250
+ if not df_filtered.empty:
251
+ all_highlighted_perplexities.extend(df_filtered["perplexity"].tolist())
252
+
253
+ # Passo 2: Determina o Mínimo e Máximo global entre todos os tokens destacados
254
+ if all_highlighted_perplexities:
255
+ global_min_p = min(all_highlighted_perplexities)
256
+ global_max_p = max(all_highlighted_perplexities)
257
+ else:
258
+ global_min_p = 0.0
259
+ global_max_p = 1.0
260
+
261
+ def get_color_and_text_color(val, is_highlighted):
262
+ if not is_highlighted:
263
+ return "transparent", "#333333", False
264
+
265
+ if global_max_p == global_min_p:
266
+ norm_val = 0.5
267
+ else:
268
+ norm_val = (val - global_min_p) / (global_max_p - global_min_p)
269
+ norm_val = max(0.0, min(1.0, norm_val))
270
+
271
+ r = int(255 - norm_val * (255 - 128))
272
+ g = int(192 - norm_val * 192)
273
+ b = int(203 - norm_val * (203 - 32))
274
+ alpha = 0.3 + (norm_val * 0.7)
275
+ bg_color = f"rgba({r}, {g}, {b}, {alpha})"
276
+ text_color = "#ffffff" if norm_val > 0.5 else "#333333"
277
+
278
+ return bg_color, text_color, True
279
+
280
+ sections_html = []
281
+
282
+ # Passo 3: Constrói o HTML de cada seção de texto
283
+ for item in models_dfs:
284
+ df = item["df"]
285
+ top_indices = item["top_indices"]
286
+ label = item["label"]
287
+
288
+ spans = []
289
+ for idx, row in df.iterrows():
290
+ token = row["token"]
291
+ p_val = row["perplexity"]
292
+ is_hl = idx in top_indices
293
+
294
+ bg_color, text_color, highlighted = get_color_and_text_color(p_val, is_hl)
295
+ safe_token = token.replace("\n", "<br>")
296
+
297
+ if highlighted:
298
+ span_html = (
299
+ f'<span style="background-color: {bg_color}; color: {text_color}; '
300
+ f'padding: 3px 6px; margin: 2px; border-radius: 4px; display: inline-block; '
301
+ f'font-weight: 500;" title="Perplexity: {p_val:.2f}">{safe_token}</span>'
302
+ )
303
+ else:
304
+ span_html = (
305
+ f'<span style="background-color: transparent; color: {text_color}; '
306
+ f'padding: 3px 6px; margin: 2px; display: inline-block;" '
307
+ f'title="Perplexity: {p_val:.2f} (Not in Top {top_n})">{safe_token}</span>'
308
+ )
309
+ spans.append(span_html)
310
+
311
+ section_content = f"""
312
+ <div class="model-section">
313
+ <h3>{label}</h3>
314
+ <div class="text-container">
315
+ {"".join(spans)}
316
+ </div>
317
+ </div>
318
+ """
319
+ sections_html.append(section_content)
320
+
321
+ legend_start = "rgba(255, 192, 203, 0.3)"
322
+ legend_end = "rgba(128, 0, 32, 1.0)"
323
+
324
+ html_content = f"""
325
+ <!DOCTYPE html>
326
+ <html>
327
+ <head>
328
+ <meta charset="utf-8">
329
+ <title>LLM Perplexity Comparison</title>
330
+ <style>
331
+ body {{
332
+ font-family: Arial, sans-serif;
333
+ margin: 40px;
334
+ background-color: #f9f9f9;
335
+ color: #333;
336
+ }}
337
+ .model-section {{
338
+ margin-bottom: 25px;
339
+ }}
340
+ h3 {{
341
+ margin-bottom: 8px;
342
+ color: #444;
343
+ }}
344
+ .text-container {{
345
+ background: #fff;
346
+ padding: 20px;
347
+ border-radius: 8px;
348
+ box-shadow: 0 2px 5px rgba(0,0,0,0.1);
349
+ line-height: 2.5;
350
+ font-size: 18px;
351
+ }}
352
+ h2 {{
353
+ margin-bottom: 30px;
354
+ }}
355
+ .global-legend-container {{
356
+ margin-top: 35px;
357
+ background: #fff;
358
+ padding: 20px;
359
+ border-radius: 8px;
360
+ box-shadow: 0 2px 5px rgba(0,0,0,0.1);
361
+ display: flex;
362
+ align-items: center;
363
+ justify-content: center;
364
+ gap: 20px;
365
+ font-size: 15px;
366
+ color: #555;
367
+ }}
368
+ .legend-bar {{
369
+ width: 300px;
370
+ height: 16px;
371
+ border-radius: 4px;
372
+ background: linear-gradient(to right, {legend_start}, {legend_end});
373
+ border: 1px solid #ddd;
374
+ }}
375
+ </style>
376
+ </head>
377
+ <body>
378
+ <h2>LLM Perplexity Text Comparison (Top {top_n if top_n else 'All'} Tokens)</h2>
379
+ {"".join(sections_html)}
380
+
381
+ <div class="global-legend-container">
382
+ <span>Low ({global_min_p:.2f})</span>
383
+ <div class="legend-bar"></div>
384
+ <span>High ({global_max_p:.2f})</span>
385
+ </div>
386
+ </body>
387
+ </html>
388
+ """
389
+
390
+ with open(save_path, "w", encoding="utf-8") as f:
391
+ f.write(html_content)
392
+
393
+ print(f"Global comparison visualization successfully saved to: {save_path}")
394
+
395
+ def plot_macro_comparison(
396
+ self,
397
+ macro_data: dict[str, list[float]],
398
+ save_path: str = "macro_perplexity_comparison.html",
399
+ show_values: bool = False
400
+ ):
401
+ """
402
+ Gera um gráfico de caixa (Boxplot) interativo para comparação macro,
403
+ exibindo as 5 estatísticas principais com cores alinhadas aos modelos.
404
+ """
405
+ import pandas as pd
406
+ import numpy as np
407
+ import plotly.graph_objects as go
408
+
409
+ rows = []
410
+ for model_name, values in macro_data.items():
411
+ for val in values:
412
+ rows.append({"Model": model_name, "Perplexity": val})
413
+
414
+ df_long = pd.DataFrame(rows)
415
+
416
+ fig = go.Figure()
417
+ colors = px.colors.qualitative.Plotly[:len(macro_data)]
418
+ models = list(macro_data.items())
419
+
420
+ for i, (model_name, values) in enumerate(models):
421
+ df_model = df_long[df_long["Model"] == model_name]
422
+ color = colors[i % len(colors)]
423
+
424
+ fig.add_trace(go.Box(
425
+ y=df_model["Perplexity"],
426
+ name=model_name,
427
+ boxpoints=False,
428
+ marker_color=color
429
+ ))
430
+
431
+ if show_values:
432
+ arr = np.array(values)
433
+ q1 = np.percentile(arr, 25)
434
+ median = np.median(arr)
435
+ q3 = np.percentile(arr, 75)
436
+ minimum = np.min(arr)
437
+ maximum = np.max(arr)
438
+
439
+ stat_y = [minimum, q1, median, q3, maximum]
440
+ stat_labels = [
441
+ f"Min: {minimum:.2f}",
442
+ f"Q1: {q1:.2f}",
443
+ f"Median: {median:.2f}",
444
+ f"Q3: {q3:.2f}",
445
+ f"Max: {maximum:.2f}"
446
+ ]
447
+
448
+ fig.add_trace(go.Scatter(
449
+ x=[model_name] * len(stat_y),
450
+ y=stat_y,
451
+ mode="markers+text",
452
+ text=stat_labels,
453
+ textposition="top right",
454
+ textfont=dict(size=11, color=color, family="Arial Black"),
455
+ marker=dict(color=color, size=6, symbol="circle"),
456
+ showlegend=False,
457
+ hovertemplate="Model: " + model_name + "<br>%{text}<extra></extra>"
458
+ ))
459
+
460
+ fig.update_layout(
461
+ title="Macro Comparison: Statistical Distribution Summary",
462
+ xaxis_title="Models",
463
+ yaxis_title="Perplexity",
464
+ showlegend=False,
465
+ margin=dict(r=150) # Corrigido de 'right' para 'r'
466
+ )
467
+
468
+ fig.write_html(save_path)
469
+ print(f"Macro comparison statistical chart successfully saved to: {save_path}")
470
+ return fig
471
+
472
+ def plot_macro_violin(
473
+ self,
474
+ macro_data: dict[str, list[float]],
475
+ save_path: str = "macro_violin_comparison.html",
476
+ show_values: bool = False
477
+ ):
478
+ """
479
+ Gera um Violin Plot interativo para comparação macro da distribuição de perplexidade,
480
+ exibindo opcionalmente as 5 principais estatísticas (Min, Q1, Median, Q3, Max)
481
+ com cores sincronizadas ao modelo.
482
+
483
+ Parâmetros:
484
+ - macro_data (dict): Dicionário de listas com as perplexidades dos modelos.
485
+ - save_path (str): Caminho para salvar o arquivo HTML gerado.
486
+ - show_values (bool): Se True, exibe as estatísticas numéricas ao lado direito de cada violino.
487
+ """
488
+ import pandas as pd
489
+ import numpy as np
490
+ import plotly.graph_objects as go
491
+
492
+ rows = []
493
+ for model_name, values in macro_data.items():
494
+ for val in values:
495
+ rows.append({"Model": model_name, "Perplexity": val})
496
+
497
+ df_long = pd.DataFrame(rows)
498
+
499
+ fig = go.Figure()
500
+ colors = px.colors.qualitative.Plotly[:len(macro_data)]
501
+ models = list(macro_data.items())
502
+
503
+ for i, (model_name, values) in enumerate(models):
504
+ df_model = df_long[df_long["Model"] == model_name]
505
+ color = colors[i % len(colors)]
506
+
507
+ # Adiciona o Violin Plot com caixa interna (box) embutida
508
+ fig.add_trace(go.Violin(
509
+ y=df_model["Perplexity"],
510
+ name=model_name,
511
+ box_visible=True, # Mostra o boxplot interno embutido no violino
512
+ meanline_visible=True, # Mostra a linha da média
513
+ fillcolor=color,
514
+ opacity=0.6,
515
+ line_color=color
516
+ ))
517
+
518
+ if show_values:
519
+ arr = np.array(values)
520
+ q1 = np.percentile(arr, 25)
521
+ median = np.median(arr)
522
+ q3 = np.percentile(arr, 75)
523
+ minimum = np.min(arr)
524
+ maximum = np.max(arr)
525
+
526
+ stat_y = [minimum, q1, median, q3, maximum]
527
+ stat_labels = [
528
+ f"Min: {minimum:.2f}",
529
+ f"Q1: {q1:.2f}",
530
+ f"Median: {median:.2f}",
531
+ f"Q3: {q3:.2f}",
532
+ f"Max: {maximum:.2f}"
533
+ ]
534
+
535
+ fig.add_trace(go.Scatter(
536
+ x=[model_name] * len(stat_y),
537
+ y=stat_y,
538
+ mode="markers+text",
539
+ text=stat_labels,
540
+ textposition="top right",
541
+ textfont=dict(size=11, color=color, family="Arial Black"),
542
+ marker=dict(color=color, size=6, symbol="circle"),
543
+ showlegend=False,
544
+ hovertemplate="Model: " + model_name + "<br>%{text}<extra></extra>"
545
+ ))
546
+
547
+ fig.update_layout(
548
+ title="Macro Comparison: Violin Distribution Summary",
549
+ xaxis_title="Models",
550
+ yaxis_title="Perplexity",
551
+ showlegend=False,
552
+ margin=dict(r=150) # Margem lateral para acomodar os rótulos estatísticos
553
+ )
554
+
555
+ fig.write_html(save_path)
556
+ print(f"Macro violin comparison chart successfully saved to: {save_path}")
557
+ return fig
558
+
@@ -0,0 +1,631 @@
1
+ import plotly.express as px
2
+ from .parser import PerplexityParser
3
+
4
+ class PerplexityVisualizer:
5
+ def __init__(self, parser: PerplexityParser):
6
+ self.df = parser.get_dataframe()
7
+
8
+ def plot_token_curve(self, save_path: str = "perplexity_curve.html", x_axis: str = "index", tick_angle: int = 0):
9
+ """
10
+ Generates an interactive line chart displaying token-by-token perplexity along a prompt or document,
11
+ facilitating the identification of uncertainty spikes.
12
+
13
+ Parameters:
14
+ - save_path (str, optional): File path to save the generated HTML file. Default: "perplexity_curve.html".
15
+ - x_axis (str, optional): Defines the X-axis representation ('index' or 'token'). Default: "index".
16
+ - tick_angle (int, optional): Rotation angle for X-axis labels (e.g., 0 for normal, 90 for vertical). Default: 0.
17
+
18
+ Example Usage:
19
+ --------------
20
+ visualizer.plot_token_curve(
21
+ save_path="perplexity_curve.html",
22
+ x_axis="token",
23
+ tick_angle=45
24
+ )
25
+ """
26
+ if x_axis not in ["index", "token"]:
27
+ raise ValueError("O parâmetro 'x_axis' deve ser 'index' ou 'token'.")
28
+
29
+ x_data = self.df.index if x_axis == "index" else self.df["token"]
30
+
31
+ fig = px.line(
32
+ self.df,
33
+ x=x_data,
34
+ y="perplexity",
35
+ hover_data=["token"],
36
+ markers=True,
37
+ title="LLM Perplexity Curve by Token",
38
+ labels={"x": "Token Position" if x_axis == "index" else "Token", "perplexity": "Perplexity"}
39
+ )
40
+
41
+ fig.update_traces(
42
+ hovertemplate="<b>Token:</b> %{customdata[0]}<br><b>Perplexity:</b> %{y:.2f}<extra></extra>"
43
+ )
44
+
45
+ fig.update_layout(xaxis=dict(tickangle=tick_angle))
46
+
47
+ fig.write_html(save_path)
48
+ print(f"Interactive chart successfully saved to: {save_path}")
49
+ return fig
50
+
51
+ def plot_token_bars(self, save_path: str = "perplexity_bars.html", top_n: int = None, cascade: bool = True):
52
+ """
53
+ Generates a horizontal bar chart displaying tokens on the Y-axis and perplexity values on the X-axis,
54
+ allowing clear visualization and ranking of token uncertainties.
55
+
56
+ Parameters:
57
+ - save_path (str, optional): File path to save the generated HTML file. Default: "perplexity_bars.html".
58
+ - top_n (int, optional): Limits the number of displayed tokens. Default: None (displays all tokens).
59
+ - cascade (bool, optional): If True, sorts tokens by perplexity value (highest on top). Default: True.
60
+
61
+ Example Usage:
62
+ --------------
63
+ visualizer.plot_token_bars(
64
+ save_path="perplexity_bars.html",
65
+ top_n=10,
66
+ cascade=True
67
+ )
68
+ """
69
+ df_plot = self.df.copy()
70
+
71
+ if cascade:
72
+ df_plot = df_plot.sort_values(by="perplexity", ascending=True)
73
+
74
+ if top_n is not None:
75
+ if cascade:
76
+ df_plot = df_plot.tail(top_n)
77
+ else:
78
+ df_plot = df_plot.head(top_n)
79
+
80
+ fig = px.bar(
81
+ df_plot,
82
+ x="perplexity",
83
+ y="token",
84
+ orientation="h",
85
+ text="perplexity", # Define a coluna que terá o valor exibido na barra
86
+ title="LLM Perplexity by Token (Bar Chart)",
87
+ labels={"perplexity": "Perplexity", "token": "Token"}
88
+ )
89
+
90
+ # Formata o texto para exibir duas casas decimais e posiciona o texto dentro/ao lado da barra
91
+ fig.update_traces(
92
+ texttemplate='%{text:.2f}',
93
+ textposition='inside',
94
+ hovertemplate="<b>Token:</b> %{y}<br><b>Perplexity:</b> %{x:.2f}<extra></extra>"
95
+ )
96
+
97
+ fig.write_html(save_path)
98
+ print(f"Interactive bar chart successfully saved to: {save_path}")
99
+ return fig
100
+
101
+ def perplexity_text(self, save_path: str = "perplexity_text.html", top_n: int = None):
102
+ """
103
+ Generates an individual SHAP-style highlighted text visualization where token background colors
104
+ and legend gradients are dynamically calculated based on token perplexity scores.
105
+
106
+ Parameters:
107
+ - save_path (str, optional): File path to save the generated HTML file. Default: "perplexity_text.html".
108
+ - top_n (int, optional): Limits highlighting to the top N tokens by perplexity score. Default: None (highlights all).
109
+
110
+ Example Usage:
111
+ --------------
112
+ visualizer.perplexity_text(
113
+ save_path="perplexity_text.html",
114
+ top_n=5
115
+ )
116
+ """
117
+ df = self.df.copy()
118
+
119
+ # Identifica os tokens que entram no top_n
120
+ if top_n is not None and top_n < len(df):
121
+ df_filtered = df.nlargest(top_n, "perplexity")
122
+ top_indices = df_filtered.index
123
+ else:
124
+ df_filtered = df
125
+ top_indices = df.index
126
+
127
+ # Recalcula min e max baseados estritamente nos itens que receberão o destaque
128
+ min_p = df_filtered["perplexity"].min()
129
+ max_p = df_filtered["perplexity"].max()
130
+
131
+ def get_color_and_text_color(val, is_highlighted):
132
+ if not is_highlighted:
133
+ return "transparent", "#333333", False
134
+
135
+ if max_p == min_p:
136
+ norm_val = 0.5
137
+ else:
138
+ # Normaliza usando o novo limite inferior do filtro
139
+ norm_val = (val - min_p) / (max_p - min_p)
140
+
141
+ r = int(255 - norm_val * (255 - 128))
142
+ g = int(192 - norm_val * 192)
143
+ b = int(203 - norm_val * (203 - 32))
144
+ alpha = 0.3 + (norm_val * 0.7)
145
+ bg_color = f"rgba({r}, {g}, {b}, {alpha})"
146
+ text_color = "#ffffff" if norm_val > 0.5 else "#333333"
147
+
148
+ return bg_color, text_color, True
149
+
150
+ spans = []
151
+ for idx, row in df.iterrows():
152
+ token = row["token"]
153
+ p_val = row["perplexity"]
154
+ is_hl = idx in top_indices
155
+
156
+ bg_color, text_color, highlighted = get_color_and_text_color(p_val, is_hl)
157
+ safe_token = token.replace("\n", "<br>")
158
+
159
+ if highlighted:
160
+ span_html = (
161
+ f'<span style="background-color: {bg_color}; color: {text_color}; '
162
+ f'padding: 3px 6px; margin: 2px; border-radius: 4px; display: inline-block; '
163
+ f'font-weight: 500;" title="Perplexity: {p_val:.2f}">{safe_token}</span>'
164
+ )
165
+ else:
166
+ span_html = (
167
+ f'<span style="background-color: transparent; color: {text_color}; '
168
+ f'padding: 3px 6px; margin: 2px; display: inline-block;" '
169
+ f'title="Perplexity: {p_val:.2f} (Not in Top {top_n})">{safe_token}</span>'
170
+ )
171
+ spans.append(span_html)
172
+
173
+ legend_start = "rgba(255, 192, 203, 0.3)"
174
+ legend_end = "rgba(128, 0, 32, 1.0)"
175
+
176
+ html_content = f"""
177
+ <!DOCTYPE html>
178
+ <html>
179
+ <head>
180
+ <meta charset="utf-8">
181
+ <title>LLM Perplexity Text Highlight</title>
182
+ <style>
183
+ body {{
184
+ font-family: Arial, sans-serif;
185
+ margin: 40px;
186
+ background-color: #f9f9f9;
187
+ color: #333;
188
+ }}
189
+ .container {{
190
+ background: #fff;
191
+ padding: 25px;
192
+ border-radius: 8px;
193
+ box-shadow: 0 2px 5px rgba(0,0,0,0.1);
194
+ line-height: 2.5;
195
+ font-size: 18px;
196
+ }}
197
+ h2 {{
198
+ margin-bottom: 20px;
199
+ }}
200
+ .legend-container {{
201
+ margin-top: 25px;
202
+ display: flex;
203
+ align-items: center;
204
+ gap: 15px;
205
+ font-size: 14px;
206
+ color: #555;
207
+ }}
208
+ .legend-bar {{
209
+ width: 200px;
210
+ height: 14px;
211
+ border-radius: 4px;
212
+ background: linear-gradient(to right, {legend_start}, {legend_end});
213
+ border: 1px solid #ddd;
214
+ }}
215
+ </style>
216
+ </head>
217
+ <body>
218
+ <h2>LLM Perplexity Text Highlight (Top {top_n if top_n else 'All'} Tokens)</h2>
219
+ <div class="container">
220
+ {"".join(spans)}
221
+
222
+ <div class="legend-container">
223
+ <span>Low ({min_p:.2f})</span>
224
+ <div class="legend-bar"></div>
225
+ <span>High ({max_p:.2f})</span>
226
+ </div>
227
+ </div>
228
+ </body>
229
+ </html>
230
+ """
231
+
232
+ with open(save_path, "w", encoding="utf-8") as f:
233
+ f.write(html_content)
234
+
235
+ print(f"Text highlight visualization with recalculated legend successfully saved to: {save_path}")
236
+
237
+ def compare_perplexity_texts(
238
+ self,
239
+ save_path: str = "perplexity_comparison.html",
240
+ tokens: list[list[str]] = None,
241
+ perplexities: list[list[float]] = None,
242
+ labels: list[str] = None,
243
+ top_n: int = None
244
+ ):
245
+ """
246
+ Generates a comparative multi-model SHAP-style text visualization using a unified global
247
+ color scale gradient and a single consolidated legend in the footer.
248
+
249
+ Parameters:
250
+ - save_path (str, optional): File path to save the generated HTML file. Default: "perplexity_comparison.html".
251
+ - tokens (list of list of str, required): List containing token lists for each model. Default: None.
252
+ - perplexities (list of list of float, required): List containing perplexity score lists for each model. Default: None.
253
+ - labels (list of str, optional): Model identifiers or aliases. Default: None (auto-generated).
254
+ - top_n (int, optional): Limits highlighting to the top N tokens per model. Default: None.
255
+
256
+ Example Usage:
257
+ --------------
258
+ visualizer.compare_perplexity_texts(
259
+ save_path="perplexity_comparison.html",
260
+ tokens=[["O", " modelo"], ["A", " resposta"]],
261
+ perplexities=[[1.1, 2.3], [2.0, 3.5]],
262
+ labels=["Gemini", "GPT-4"],
263
+ top_n=2
264
+ )
265
+ """
266
+ if not tokens or not perplexities or len(tokens) != len(perplexities):
267
+ raise ValueError("As listas 'tokens' e 'perplexities' devem ser fornecidas e ter o mesmo comprimento.")
268
+
269
+ if labels is None:
270
+ labels = [f"Model {i+1}" for i in range(len(tokens))]
271
+
272
+ if len(tokens) != len(labels):
273
+ raise ValueError("A quantidade de listas de tokens deve ser igual à quantidade de 'labels'.")
274
+
275
+ import pandas as pd
276
+
277
+ models_dfs = []
278
+ all_highlighted_perplexities = []
279
+
280
+ # Passo 1: Processa cada modelo e coleta os valores destacados globalmente
281
+ for model_tokens, model_perps, label in zip(tokens, perplexities, labels):
282
+ if len(model_tokens) != len(model_perps):
283
+ raise ValueError(f"As listas de tokens e perplexidades do modelo '{label}' devem ter o mesmo tamanho.")
284
+
285
+ df = pd.DataFrame({"token": model_tokens, "perplexity": model_perps})
286
+
287
+ if top_n is not None and top_n < len(df):
288
+ df_filtered = df.nlargest(top_n, "perplexity")
289
+ top_indices = set(df_filtered.index)
290
+ else:
291
+ top_indices = set(df.index)
292
+ df_filtered = df
293
+
294
+ models_dfs.append({"df": df, "top_indices": top_indices, "label": label})
295
+
296
+ if not df_filtered.empty:
297
+ all_highlighted_perplexities.extend(df_filtered["perplexity"].tolist())
298
+
299
+ # Passo 2: Determina o Mínimo e Máximo global entre todos os tokens destacados
300
+ if all_highlighted_perplexities:
301
+ global_min_p = min(all_highlighted_perplexities)
302
+ global_max_p = max(all_highlighted_perplexities)
303
+ else:
304
+ global_min_p = 0.0
305
+ global_max_p = 1.0
306
+
307
+ def get_color_and_text_color(val, is_highlighted):
308
+ if not is_highlighted:
309
+ return "transparent", "#333333", False
310
+
311
+ if global_max_p == global_min_p:
312
+ norm_val = 0.5
313
+ else:
314
+ norm_val = (val - global_min_p) / (global_max_p - global_min_p)
315
+ norm_val = max(0.0, min(1.0, norm_val))
316
+
317
+ r = int(255 - norm_val * (255 - 128))
318
+ g = int(192 - norm_val * 192)
319
+ b = int(203 - norm_val * (203 - 32))
320
+ alpha = 0.3 + (norm_val * 0.7)
321
+ bg_color = f"rgba({r}, {g}, {b}, {alpha})"
322
+ text_color = "#ffffff" if norm_val > 0.5 else "#333333"
323
+
324
+ return bg_color, text_color, True
325
+
326
+ sections_html = []
327
+
328
+ # Passo 3: Constrói o HTML de cada seção de texto
329
+ for item in models_dfs:
330
+ df = item["df"]
331
+ top_indices = item["top_indices"]
332
+ label = item["label"]
333
+
334
+ spans = []
335
+ for idx, row in df.iterrows():
336
+ token = row["token"]
337
+ p_val = row["perplexity"]
338
+ is_hl = idx in top_indices
339
+
340
+ bg_color, text_color, highlighted = get_color_and_text_color(p_val, is_hl)
341
+ safe_token = token.replace("\n", "<br>")
342
+
343
+ if highlighted:
344
+ span_html = (
345
+ f'<span style="background-color: {bg_color}; color: {text_color}; '
346
+ f'padding: 3px 6px; margin: 2px; border-radius: 4px; display: inline-block; '
347
+ f'font-weight: 500;" title="Perplexity: {p_val:.2f}">{safe_token}</span>'
348
+ )
349
+ else:
350
+ span_html = (
351
+ f'<span style="background-color: transparent; color: {text_color}; '
352
+ f'padding: 3px 6px; margin: 2px; display: inline-block;" '
353
+ f'title="Perplexity: {p_val:.2f} (Not in Top {top_n})">{safe_token}</span>'
354
+ )
355
+ spans.append(span_html)
356
+
357
+ section_content = f"""
358
+ <div class="model-section">
359
+ <h3>{label}</h3>
360
+ <div class="text-container">
361
+ {"".join(spans)}
362
+ </div>
363
+ </div>
364
+ """
365
+ sections_html.append(section_content)
366
+
367
+ legend_start = "rgba(255, 192, 203, 0.3)"
368
+ legend_end = "rgba(128, 0, 32, 1.0)"
369
+
370
+ html_content = f"""
371
+ <!DOCTYPE html>
372
+ <html>
373
+ <head>
374
+ <meta charset="utf-8">
375
+ <title>LLM Perplexity Comparison</title>
376
+ <style>
377
+ body {{
378
+ font-family: Arial, sans-serif;
379
+ margin: 40px;
380
+ background-color: #f9f9f9;
381
+ color: #333;
382
+ }}
383
+ .model-section {{
384
+ margin-bottom: 25px;
385
+ }}
386
+ h3 {{
387
+ margin-bottom: 8px;
388
+ color: #444;
389
+ }}
390
+ .text-container {{
391
+ background: #fff;
392
+ padding: 20px;
393
+ border-radius: 8px;
394
+ box-shadow: 0 2px 5px rgba(0,0,0,0.1);
395
+ line-height: 2.5;
396
+ font-size: 18px;
397
+ }}
398
+ h2 {{
399
+ margin-bottom: 30px;
400
+ }}
401
+ .global-legend-container {{
402
+ margin-top: 35px;
403
+ background: #fff;
404
+ padding: 20px;
405
+ border-radius: 8px;
406
+ box-shadow: 0 2px 5px rgba(0,0,0,0.1);
407
+ display: flex;
408
+ align-items: center;
409
+ justify-content: center;
410
+ gap: 20px;
411
+ font-size: 15px;
412
+ color: #555;
413
+ }}
414
+ .legend-bar {{
415
+ width: 300px;
416
+ height: 16px;
417
+ border-radius: 4px;
418
+ background: linear-gradient(to right, {legend_start}, {legend_end});
419
+ border: 1px solid #ddd;
420
+ }}
421
+ </style>
422
+ </head>
423
+ <body>
424
+ <h2>LLM Perplexity Text Comparison (Top {top_n if top_n else 'All'} Tokens)</h2>
425
+ {"".join(sections_html)}
426
+
427
+ <div class="global-legend-container">
428
+ <span>Low ({global_min_p:.2f})</span>
429
+ <div class="legend-bar"></div>
430
+ <span>High ({global_max_p:.2f})</span>
431
+ </div>
432
+ </body>
433
+ </html>
434
+ """
435
+
436
+ with open(save_path, "w", encoding="utf-8") as f:
437
+ f.write(html_content)
438
+
439
+ print(f"Global comparison visualization successfully saved to: {save_path}")
440
+
441
+ def plot_macro_comparison(
442
+ self,
443
+ macro_data: dict[str, list[float]],
444
+ save_path: str = "macro_perplexity_comparison.html",
445
+ show_values: bool = False
446
+ ):
447
+ """
448
+ Generates an interactive Boxplot for macro-level comparison of perplexity distributions across multiple models,
449
+ showing statistical metrics (Min, Q1, Median, Q3, Max) with model-aligned colors.
450
+
451
+ Parameters:
452
+ - macro_data (dict, required): Dictionary mapping model names to lists of perplexity floats. Default: None (positional).
453
+ - save_path (str, optional): File path to save the generated HTML file. Default: "macro_perplexity_comparison.html".
454
+ - show_values (bool, optional): If True, displays key statistical values on the right side. Default: False.
455
+
456
+ Example Usage:
457
+ --------------
458
+ macro_data = {
459
+ "Gemini": [1.25, 1.30, 1.45],
460
+ "GPT-4": [1.10, 1.15, 1.25]
461
+ }
462
+ visualizer.plot_macro_comparison(
463
+ macro_data=macro_data,
464
+ save_path="macro_boxplot.html",
465
+ show_values=True
466
+ )
467
+ """
468
+ import pandas as pd
469
+ import numpy as np
470
+ import plotly.graph_objects as go
471
+
472
+ rows = []
473
+ for model_name, values in macro_data.items():
474
+ for val in values:
475
+ rows.append({"Model": model_name, "Perplexity": val})
476
+
477
+ df_long = pd.DataFrame(rows)
478
+
479
+ fig = go.Figure()
480
+ colors = px.colors.qualitative.Plotly[:len(macro_data)]
481
+ models = list(macro_data.items())
482
+
483
+ for i, (model_name, values) in enumerate(models):
484
+ df_model = df_long[df_long["Model"] == model_name]
485
+ color = colors[i % len(colors)]
486
+
487
+ fig.add_trace(go.Box(
488
+ y=df_model["Perplexity"],
489
+ name=model_name,
490
+ boxpoints=False,
491
+ marker_color=color
492
+ ))
493
+
494
+ if show_values:
495
+ arr = np.array(values)
496
+ q1 = np.percentile(arr, 25)
497
+ median = np.median(arr)
498
+ q3 = np.percentile(arr, 75)
499
+ minimum = np.min(arr)
500
+ maximum = np.max(arr)
501
+
502
+ stat_y = [minimum, q1, median, q3, maximum]
503
+ stat_labels = [
504
+ f"Min: {minimum:.2f}",
505
+ f"Q1: {q1:.2f}",
506
+ f"Median: {median:.2f}",
507
+ f"Q3: {q3:.2f}",
508
+ f"Max: {maximum:.2f}"
509
+ ]
510
+
511
+ fig.add_trace(go.Scatter(
512
+ x=[model_name] * len(stat_y),
513
+ y=stat_y,
514
+ mode="markers+text",
515
+ text=stat_labels,
516
+ textposition="top right",
517
+ textfont=dict(size=11, color=color, family="Arial Black"),
518
+ marker=dict(color=color, size=6, symbol="circle"),
519
+ showlegend=False,
520
+ hovertemplate="Model: " + model_name + "<br>%{text}<extra></extra>"
521
+ ))
522
+
523
+ fig.update_layout(
524
+ title="Macro Comparison: Statistical Distribution Summary",
525
+ xaxis_title="Models",
526
+ yaxis_title="Perplexity",
527
+ showlegend=False,
528
+ margin=dict(r=150)
529
+ )
530
+
531
+ fig.write_html(save_path)
532
+ print(f"Macro comparison statistical chart successfully saved to: {save_path}")
533
+ return fig
534
+
535
+ def plot_macro_violin(
536
+ self,
537
+ macro_data: dict[str, list[float]],
538
+ save_path: str = "macro_violin_comparison.html",
539
+ show_values: bool = False
540
+ ):
541
+ """
542
+ Generates an interactive Violin Plot for macro-level analysis of perplexity distributions,
543
+ displaying probability density, internal box stats, and optional key statistical markers.
544
+
545
+ Parameters:
546
+ - macro_data (dict, required): Dictionary mapping model names to lists of perplexity floats. Default: None (positional).
547
+ - save_path (str, optional): File path to save the generated HTML file. Default: "macro_violin_comparison.html".
548
+ - show_values (bool, optional): If True, displays statistical values (Min, Q1, Median, Q3, Max). Default: False.
549
+
550
+ Example Usage:
551
+ --------------
552
+ macro_data = {
553
+ "Gemini": [1.25, 1.30, 1.45],
554
+ "GPT-4": [1.10, 1.15, 1.25]
555
+ }
556
+ visualizer.plot_macro_violin(
557
+ macro_data=macro_data,
558
+ save_path="macro_violin.html",
559
+ show_values=True
560
+ )
561
+ """
562
+ import pandas as pd
563
+ import numpy as np
564
+ import plotly.graph_objects as go
565
+
566
+ rows = []
567
+ for model_name, values in macro_data.items():
568
+ for val in values:
569
+ rows.append({"Model": model_name, "Perplexity": val})
570
+
571
+ df_long = pd.DataFrame(rows)
572
+
573
+ fig = go.Figure()
574
+ colors = px.colors.qualitative.Plotly[:len(macro_data)]
575
+ models = list(macro_data.items())
576
+
577
+ for i, (model_name, values) in enumerate(models):
578
+ df_model = df_long[df_long["Model"] == model_name]
579
+ color = colors[i % len(colors)]
580
+
581
+ # Adiciona o Violin Plot com caixa interna (box) embutida
582
+ fig.add_trace(go.Violin(
583
+ y=df_model["Perplexity"],
584
+ name=model_name,
585
+ box_visible=True, # Mostra o boxplot interno embutido no violino
586
+ meanline_visible=True, # Mostra a linha da média
587
+ fillcolor=color,
588
+ opacity=0.6,
589
+ line_color=color
590
+ ))
591
+
592
+ if show_values:
593
+ arr = np.array(values)
594
+ q1 = np.percentile(arr, 25)
595
+ median = np.median(arr)
596
+ q3 = np.percentile(arr, 75)
597
+ minimum = np.min(arr)
598
+ maximum = np.max(arr)
599
+
600
+ stat_y = [minimum, q1, median, q3, maximum]
601
+ stat_labels = [
602
+ f"Min: {minimum:.2f}",
603
+ f"Q1: {q1:.2f}",
604
+ f"Median: {median:.2f}",
605
+ f"Q3: {q3:.2f}",
606
+ f"Max: {maximum:.2f}"
607
+ ]
608
+
609
+ fig.add_trace(go.Scatter(
610
+ x=[model_name] * len(stat_y),
611
+ y=stat_y,
612
+ mode="markers+text",
613
+ text=stat_labels,
614
+ textposition="top right",
615
+ textfont=dict(size=11, color=color, family="Arial Black"),
616
+ marker=dict(color=color, size=6, symbol="circle"),
617
+ showlegend=False,
618
+ hovertemplate="Model: " + model_name + "<br>%{text}<extra></extra>"
619
+ ))
620
+
621
+ fig.update_layout(
622
+ title="Macro Comparison: Violin Distribution Summary",
623
+ xaxis_title="Models",
624
+ yaxis_title="Perplexity",
625
+ showlegend=False,
626
+ margin=dict(r=150) # Margem lateral para acomodar os rótulos estatísticos
627
+ )
628
+
629
+ fig.write_html(save_path)
630
+ print(f"Macro violin comparison chart successfully saved to: {save_path}")
631
+ return fig