wuenlp_tools.visualize.plot

  1from pathlib import Path
  2from typing import Type
  3
  4import numpy as np
  5import pandas as pd
  6import plotly.express as px
  7from loguru import logger
  8
  9from wuenlp.impl.UIMANLPStructs import UIMADocument, UIMAAnnotation, UIMASystemScene
 10
 11# Function to format text with newlines
 12from wuenlp_tools.utils.summarize import summarize_doc
 13
 14
 15def format_text_scenes(segment: UIMAAnnotation, line_length=20, max_lines=50, summary=True):
 16    # Split long texts into lines
 17    text = segment.additional_features["llama_summary"] if summary else segment.text
 18    split = text.split(" ")
 19    lines = [' '.join(split[i:i + line_length]) for i in range(0, len(split), line_length)]
 20
 21    if len(lines) > max_lines:
 22        lines = lines[:max_lines // 2] + [f"[{len(lines) - max_lines} more lines]"] + lines[-max_lines // 2:]
 23    return "<br>".join(lines)
 24
 25
 26def format_text_danger(segment: UIMAAnnotation, line_length=20, max_lines=75, summary=True):
 27    return f"{format_text_scenes(segment, line_length, max_lines, summary)}<br>{segment.additional_features['danger_score']}; {segment.additional_features['danger_reason']}"
 28
 29
 30def _signed_power_mean(values: list[float], power: float) -> float:
 31    if not values:
 32        return 0.0
 33    transformed = [np.sign(value) * (abs(value) ** power) for value in values]
 34    mean_transformed = sum(transformed) / len(transformed)
 35    return float(np.sign(mean_transformed) * (abs(mean_transformed) ** (1.0 / power)))
 36
 37
 38def _smooth_series(
 39    sentiments: list[float],
 40    smooth: int,
 41    method: str = "moving",
 42    signed_power: float = 2.0,
 43    bilateral_sigma_t: float = 2.5,
 44    bilateral_sigma_x: float = 0.08,
 45) -> list[float]:
 46    if smooth <= 0:
 47        return sentiments
 48    if method not in {"moving", "signed-power", "bilateral"}:
 49        raise ValueError(f"Unknown smoothing method: {method}")
 50    if method == "signed-power" and signed_power <= 0:
 51        raise ValueError("signed_power must be > 0")
 52    if method == "bilateral" and (bilateral_sigma_t <= 0 or bilateral_sigma_x <= 0):
 53        raise ValueError("bilateral sigma parameters must be > 0")
 54
 55    arr = np.asarray(sentiments, dtype=float)
 56    num_sentiments = len(arr)
 57    smoothed_sentiments = []
 58    for i in range(num_sentiments):
 59        start = max(0, i - smooth)
 60        end = min(num_sentiments, i + smooth + 1)
 61        window = arr[start:end]
 62        if method == "moving":
 63            smoothed = float(window.mean())
 64        elif method == "signed-power":
 65            smoothed = _signed_power_mean(window.tolist(), signed_power)
 66        else:
 67            # Bilateral: weight by temporal distance and value similarity to the center point.
 68            js = np.arange(start, end)
 69            dt = (i - js).astype(float)
 70            dx = (arr[i] - window).astype(float)
 71            weights = np.exp(-(dt * dt) / (2.0 * bilateral_sigma_t ** 2) - (dx * dx) / (2.0 * bilateral_sigma_x ** 2))
 72            weight_sum = float(weights.sum())
 73            smoothed = float((weights * window).sum() / weight_sum) if weight_sum > 0 else float(arr[i])
 74        smoothed_sentiments.append(smoothed)
 75    return smoothed_sentiments
 76
 77
 78def plot_sentiment_interactive(doc: UIMADocument, segment_type: Type[UIMAAnnotation] = UIMASystemScene, smooth: int = 0,
 79                               line_length=20, feature_name: str = "llama_sentiment",
 80                               summary: bool = False, call_summarizer_if_necessary: bool = False,
 81                               format_text_func=format_text_scenes, output_path: Path = None,
 82                               disable_markers: bool = False, doc_name: str = None,
 83                               segment_type_clean_name: str = None, show_title: bool = True,
 84                               y_label: str = 'Sentiment', smooth_only: bool = False,
 85                               smoothing_method: str = "moving", smoothing_signed_power: float = 2.0,
 86                               bilateral_sigma_t: float = 2.5, bilateral_sigma_x: float = 0.08):
 87    sentiments = []
 88    texts = []
 89    text_lengths = []
 90    keywords = []
 91
 92    segments = doc._get_annos_of_type(segment_type)
 93    if not segments:
 94        raise ValueError(f"No {segment_type.__name__} found in {doc.path.stem}")
 95
 96    if call_summarizer_if_necessary and not segments[0].additional_features.get('llama_summary', None):
 97        logger.info("Summarising segments")
 98        doc = summarize_doc(doc, unit_type=segment_type)
 99
100    # Extracting scenes (assuming 'document' is UIMADocument and has a method to get system scenes)
101    for segment in segments:
102        llama_sentiment = segment.additional_features.get(feature_name, None)
103        if llama_sentiment is not None:
104            sentiments.append(llama_sentiment)
105
106            formatted_text = format_text_func(segment, line_length, summary=summary)
107            texts.append(formatted_text)
108            keyword = "<br>".join(segment.additional_features.get('llama_keywords', []))
109            logger.info(f"Keywords: {keyword}")
110            keywords.append(keyword)
111            text_lengths.append(len(segment.text))  # Record the length of the original text
112
113    smoothed_sentiments = []
114    if smooth:
115        smoothed_sentiments = _smooth_series(
116            sentiments,
117            smooth=smooth,
118            method=smoothing_method,
119            signed_power=smoothing_signed_power,
120            bilateral_sigma_t=bilateral_sigma_t,
121            bilateral_sigma_x=bilateral_sigma_x,
122        )
123        if smooth_only:
124            sentiments = smoothed_sentiments
125
126    # Create a DataFrame for Plotly
127    df = pd.DataFrame({'Sentiment': sentiments, 'Scene': texts, 'Text Length': text_lengths, 'Keywords': keywords})
128    if smooth and not smooth_only:
129        df['Smoothed Sentiment'] = smoothed_sentiments
130
131    # Calculate the mean and standard deviation
132    mean_sentiment = df['Sentiment'].mean()
133    std_sentiment = df['Sentiment'].std()
134
135    # Define threshold for outliers (for example: mean ± 1.5 * std deviation)
136    threshold_upper = mean_sentiment + 1.5 * std_sentiment
137    threshold_lower = mean_sentiment - 1.5 * std_sentiment
138
139    size_scale = 1000  # Adjust this scaling factor for marker sizes
140    df['Marker Size'] = np.array(df['Text Length']) / size_scale
141
142    # Create an interactive plot
143    doc_name = doc_name or (doc.path.stem if doc.path else "Unknown")
144    segment_type_clean_name = segment_type_clean_name or segment_type.uima_type.split(".")[-1].replace("UIMA", "")
145    base_color = ['lightgray'] if smooth and not smooth_only else None
146
147    fig = px.line(
148        df,
149        x=df.index,
150        y='Sentiment',
151        hover_data=['Scene'],
152        labels={'Sentiment': y_label, 'Scene': 'Scene Text', 'index': segment_type_clean_name.title()},
153        title=f'{y_label} per {segment_type_clean_name}<br>{doc_name}' if show_title else None,
154        color_discrete_sequence=base_color
155    )
156
157    # 2. If 'smooth' is True, layer the smoothed line on top
158    if smooth and not smooth_only:
159        logger.info(
160            f"Adding smoothed line with window size {smooth}, "
161            f"method={smoothing_method}, signed_power={smoothing_signed_power}, "
162            f"bilateral_sigma_t={bilateral_sigma_t}, bilateral_sigma_x={bilateral_sigma_x}"
163        )
164        # Rename the first trace so the legend makes sense (Optional)
165        fig.data[0].name = 'Raw Sentiment'
166        fig.data[0].showlegend = True
167
168        # Add the Smoothed column
169        fig.add_scatter(
170            x=df.index,
171            y=df['Smoothed Sentiment'],
172            mode='lines',
173            name='Smoothed Sentiment',
174            line=dict(color='blue', width=3),  # Make it thicker and prominent
175            hovertext=df['Scene'],
176            hovertemplate='%{hovertext}<extra></extra>',
177        )
178
179    if len(sentiments) > 1000:
180        logger.warning("Trajectory is very long. Omitting additional annotations.")
181        fig.show()
182        write_to_path(fig, output_path)
183        return
184
185    fig.update_traces(line=dict(width=2),
186                      hovertemplate='%{hovertext}<extra></extra>',
187                      mode='lines+markers+text',
188                      marker=dict(size=df['Marker Size'], opacity=0.7),
189                      hovertext=df['Scene'],
190                      )
191
192    if not disable_markers:
193        for i in range(len(df)):
194            if df['Sentiment'].iloc[i] > threshold_upper:
195                # High outlier annotation
196                fig.add_annotation(
197                    x=df.index[i],
198                    y=df['Sentiment'].iloc[i] + 0.01,  # Adjust for space above
199                    text=df['Keywords'].iloc[i],
200                    showarrow=True,
201                    arrowhead=2,
202                    ax=0,
203                    ay=-30,  # Arrow pointing downwards
204                    textangle=0,  # Straight text for high points
205                    font=dict(size=10)
206                )
207            elif df['Sentiment'].iloc[i] < threshold_lower:
208                # Low outlier annotation
209                fig.add_annotation(
210                    x=df.index[i],
211                    y=df['Sentiment'].iloc[i] - 0.01,  # Adjust for space below
212                    text=df['Keywords'].iloc[i],
213                    showarrow=True,
214                    arrowhead=2,
215                    ax=0,
216                    ay=30,  # Arrow pointing upwards
217                    textangle=0,  # Straight text for low points
218                    font=dict(size=10)
219                )
220
221    fig.show()
222    write_to_path(fig, output_path)
223
224
225def write_to_path(fig, output_path):
226    if output_path:
227        output_path.parent.mkdir(parents=True, exist_ok=True)
228        if output_path.suffix == ".html":
229            fig.write_html(output_path)
230        else:
231            fig.write_image(str(output_path))
def format_text_scenes( segment: wuenlp.impl.uima.UIMANLPStructs.UIMAAnnotation, line_length=20, max_lines=50, summary=True):
16def format_text_scenes(segment: UIMAAnnotation, line_length=20, max_lines=50, summary=True):
17    # Split long texts into lines
18    text = segment.additional_features["llama_summary"] if summary else segment.text
19    split = text.split(" ")
20    lines = [' '.join(split[i:i + line_length]) for i in range(0, len(split), line_length)]
21
22    if len(lines) > max_lines:
23        lines = lines[:max_lines // 2] + [f"[{len(lines) - max_lines} more lines]"] + lines[-max_lines // 2:]
24    return "<br>".join(lines)
def format_text_danger( segment: wuenlp.impl.uima.UIMANLPStructs.UIMAAnnotation, line_length=20, max_lines=75, summary=True):
27def format_text_danger(segment: UIMAAnnotation, line_length=20, max_lines=75, summary=True):
28    return f"{format_text_scenes(segment, line_length, max_lines, summary)}<br>{segment.additional_features['danger_score']}; {segment.additional_features['danger_reason']}"
def plot_sentiment_interactive( doc: wuenlp.impl.uima.UIMANLPStructs.UIMADocument, segment_type: Type[wuenlp.impl.uima.UIMANLPStructs.UIMAAnnotation] = <class 'wuenlp.impl.uima.UIMANLPStructs.UIMASystemScene'>, smooth: int = 0, line_length=20, feature_name: str = 'llama_sentiment', summary: bool = False, call_summarizer_if_necessary: bool = False, format_text_func=<function format_text_scenes>, output_path: pathlib.Path = None, disable_markers: bool = False, doc_name: str = None, segment_type_clean_name: str = None, show_title: bool = True, y_label: str = 'Sentiment', smooth_only: bool = False, smoothing_method: str = 'moving', smoothing_signed_power: float = 2.0, bilateral_sigma_t: float = 2.5, bilateral_sigma_x: float = 0.08):
 79def plot_sentiment_interactive(doc: UIMADocument, segment_type: Type[UIMAAnnotation] = UIMASystemScene, smooth: int = 0,
 80                               line_length=20, feature_name: str = "llama_sentiment",
 81                               summary: bool = False, call_summarizer_if_necessary: bool = False,
 82                               format_text_func=format_text_scenes, output_path: Path = None,
 83                               disable_markers: bool = False, doc_name: str = None,
 84                               segment_type_clean_name: str = None, show_title: bool = True,
 85                               y_label: str = 'Sentiment', smooth_only: bool = False,
 86                               smoothing_method: str = "moving", smoothing_signed_power: float = 2.0,
 87                               bilateral_sigma_t: float = 2.5, bilateral_sigma_x: float = 0.08):
 88    sentiments = []
 89    texts = []
 90    text_lengths = []
 91    keywords = []
 92
 93    segments = doc._get_annos_of_type(segment_type)
 94    if not segments:
 95        raise ValueError(f"No {segment_type.__name__} found in {doc.path.stem}")
 96
 97    if call_summarizer_if_necessary and not segments[0].additional_features.get('llama_summary', None):
 98        logger.info("Summarising segments")
 99        doc = summarize_doc(doc, unit_type=segment_type)
100
101    # Extracting scenes (assuming 'document' is UIMADocument and has a method to get system scenes)
102    for segment in segments:
103        llama_sentiment = segment.additional_features.get(feature_name, None)
104        if llama_sentiment is not None:
105            sentiments.append(llama_sentiment)
106
107            formatted_text = format_text_func(segment, line_length, summary=summary)
108            texts.append(formatted_text)
109            keyword = "<br>".join(segment.additional_features.get('llama_keywords', []))
110            logger.info(f"Keywords: {keyword}")
111            keywords.append(keyword)
112            text_lengths.append(len(segment.text))  # Record the length of the original text
113
114    smoothed_sentiments = []
115    if smooth:
116        smoothed_sentiments = _smooth_series(
117            sentiments,
118            smooth=smooth,
119            method=smoothing_method,
120            signed_power=smoothing_signed_power,
121            bilateral_sigma_t=bilateral_sigma_t,
122            bilateral_sigma_x=bilateral_sigma_x,
123        )
124        if smooth_only:
125            sentiments = smoothed_sentiments
126
127    # Create a DataFrame for Plotly
128    df = pd.DataFrame({'Sentiment': sentiments, 'Scene': texts, 'Text Length': text_lengths, 'Keywords': keywords})
129    if smooth and not smooth_only:
130        df['Smoothed Sentiment'] = smoothed_sentiments
131
132    # Calculate the mean and standard deviation
133    mean_sentiment = df['Sentiment'].mean()
134    std_sentiment = df['Sentiment'].std()
135
136    # Define threshold for outliers (for example: mean ± 1.5 * std deviation)
137    threshold_upper = mean_sentiment + 1.5 * std_sentiment
138    threshold_lower = mean_sentiment - 1.5 * std_sentiment
139
140    size_scale = 1000  # Adjust this scaling factor for marker sizes
141    df['Marker Size'] = np.array(df['Text Length']) / size_scale
142
143    # Create an interactive plot
144    doc_name = doc_name or (doc.path.stem if doc.path else "Unknown")
145    segment_type_clean_name = segment_type_clean_name or segment_type.uima_type.split(".")[-1].replace("UIMA", "")
146    base_color = ['lightgray'] if smooth and not smooth_only else None
147
148    fig = px.line(
149        df,
150        x=df.index,
151        y='Sentiment',
152        hover_data=['Scene'],
153        labels={'Sentiment': y_label, 'Scene': 'Scene Text', 'index': segment_type_clean_name.title()},
154        title=f'{y_label} per {segment_type_clean_name}<br>{doc_name}' if show_title else None,
155        color_discrete_sequence=base_color
156    )
157
158    # 2. If 'smooth' is True, layer the smoothed line on top
159    if smooth and not smooth_only:
160        logger.info(
161            f"Adding smoothed line with window size {smooth}, "
162            f"method={smoothing_method}, signed_power={smoothing_signed_power}, "
163            f"bilateral_sigma_t={bilateral_sigma_t}, bilateral_sigma_x={bilateral_sigma_x}"
164        )
165        # Rename the first trace so the legend makes sense (Optional)
166        fig.data[0].name = 'Raw Sentiment'
167        fig.data[0].showlegend = True
168
169        # Add the Smoothed column
170        fig.add_scatter(
171            x=df.index,
172            y=df['Smoothed Sentiment'],
173            mode='lines',
174            name='Smoothed Sentiment',
175            line=dict(color='blue', width=3),  # Make it thicker and prominent
176            hovertext=df['Scene'],
177            hovertemplate='%{hovertext}<extra></extra>',
178        )
179
180    if len(sentiments) > 1000:
181        logger.warning("Trajectory is very long. Omitting additional annotations.")
182        fig.show()
183        write_to_path(fig, output_path)
184        return
185
186    fig.update_traces(line=dict(width=2),
187                      hovertemplate='%{hovertext}<extra></extra>',
188                      mode='lines+markers+text',
189                      marker=dict(size=df['Marker Size'], opacity=0.7),
190                      hovertext=df['Scene'],
191                      )
192
193    if not disable_markers:
194        for i in range(len(df)):
195            if df['Sentiment'].iloc[i] > threshold_upper:
196                # High outlier annotation
197                fig.add_annotation(
198                    x=df.index[i],
199                    y=df['Sentiment'].iloc[i] + 0.01,  # Adjust for space above
200                    text=df['Keywords'].iloc[i],
201                    showarrow=True,
202                    arrowhead=2,
203                    ax=0,
204                    ay=-30,  # Arrow pointing downwards
205                    textangle=0,  # Straight text for high points
206                    font=dict(size=10)
207                )
208            elif df['Sentiment'].iloc[i] < threshold_lower:
209                # Low outlier annotation
210                fig.add_annotation(
211                    x=df.index[i],
212                    y=df['Sentiment'].iloc[i] - 0.01,  # Adjust for space below
213                    text=df['Keywords'].iloc[i],
214                    showarrow=True,
215                    arrowhead=2,
216                    ax=0,
217                    ay=30,  # Arrow pointing upwards
218                    textangle=0,  # Straight text for low points
219                    font=dict(size=10)
220                )
221
222    fig.show()
223    write_to_path(fig, output_path)
def write_to_path(fig, output_path):
226def write_to_path(fig, output_path):
227    if output_path:
228        output_path.parent.mkdir(parents=True, exist_ok=True)
229        if output_path.suffix == ".html":
230            fig.write_html(output_path)
231        else:
232            fig.write_image(str(output_path))