| import pandas as pd |
| from typing import Tuple |
| import numpy as np |
| import plotly.graph_objects as go |
| import re |
|
|
| _NEG_COLOR = "red" |
|
|
| def format_large_number(n, decimals=2): |
| if n >= 1e12: |
| return f'{n / 1e12:.{decimals}f} T' |
| elif n >= 1e9: |
| return f'{n / 1e9:.{decimals}f} B' |
| elif n >= 1e6: |
| return f'{n / 1e6:.{decimals}f} M' |
| else: |
| return str(n) |
|
|
| def format_results(df: pd.DataFrame, rename_columns: dict) -> pd.DataFrame: |
| |
| if "ind_sust" in df.columns: |
| df["ind_sust"] = df["ind_sust"].apply(lambda x: "-" if pd.isna(x) else int(round(x * 100, 0))) |
| |
| for col in ["trailingPE", "beta"]: |
| if col in df.columns: |
| df[col] = df[col].apply(lambda x: "-" if pd.isna(x) else f"{x:.1f}") |
|
|
| |
| if "Search dist." in df.columns: |
| df["Search dist."] = df["Search dist."].apply(lambda n: "-" if pd.isna(n) else f"{n:.2f}") |
|
|
| |
| for col in ["marketCap", "totalRevenue", "ebitda"]: |
| if col in df.columns: |
| df[col] = df[col].apply(lambda n: "-" if pd.isna(n) else format_large_number(n, 1)) |
| |
| for col in ["ret_365", "revenueGrowth"]: |
| if col in df.columns: |
| df[col] = df[col].apply(lambda x: "-" if pd.isna(x) or x == 0 else f"{(x * 100):.1f}%") |
| |
| for col in ["dividendYield"]: |
| if col in df.columns: |
| df[col] = df[col].apply(lambda x: "-" if pd.isna(x) else f"{round(x, 1)}%") |
| |
| if "vol_365" in df.columns: |
| df["vol_365"] = df["vol_365"].apply(lambda x: "-" if pd.isna(x) or x == 0 else f"{x:.4f}") |
|
|
| |
| return df.rename(columns=rename_columns) |
|
|
|
|
| def random_ticker(df: pd.DataFrame) -> str: |
| return df["ticker"].sample(n=1).values[0] |
|
|
| def styler_negative_red(df: pd.DataFrame, cols: list[str] | None = None): |
| """ |
| Returns a Styler that paints negative numeric values in *cols*. |
| Columns absent in *df* are ignored. |
| """ |
| cols = [c for c in (cols or df.columns) if c in df.columns] |
|
|
| def _style(v): |
| try: |
| num = float(re.sub(r"[ %,TMB]", "", str(v))) |
| if num < 0: |
| return f"color:{_NEG_COLOR}" |
| except ValueError: |
| pass |
| return "" |
|
|
| return df.style.applymap(_style, subset=cols) |
|
|
| def get_company_info( |
| maestro: pd.DataFrame, |
| ticker: str, |
| rename_columns: dict |
| ) -> Tuple[str, str, pd.DataFrame]: |
| """ |
| Returns the company name, longBusinessSummary, and a DataFrame |
| of all other fields for the given ticker. |
| """ |
| company = maestro[maestro["ticker"] == ticker] |
| if company.empty: |
| return ticker, "No data available.", pd.DataFrame() |
|
|
| |
| name = company["security"].iloc[0] if "security" in company.columns else ticker |
| summary = company["longBusinessSummary"].iloc[0] if "longBusinessSummary" in company.columns else "" |
|
|
| |
| details = company.drop(columns=["longBusinessSummary"], errors="ignore").iloc[0] |
| df = pd.DataFrame({ |
| "Field": details.index.tolist(), |
| "Value": details.values.tolist() |
| }) |
| df["Field"] = df["Field"].map(lambda c: rename_columns.get(c, c)) |
|
|
| |
| for i, field in enumerate(df["Field"]): |
| if field.endswith("norm."): |
| value = df.iloc[i]["Value"] |
| if isinstance(value, (int, float)) and not pd.isna(value): |
| df.iloc[i, df.columns.get_loc("Value")] = round(value, 3) |
| |
| |
| numeric_fields = [] |
| numeric_values = [] |
| numeric_indices = [] |
| |
| for i, (display_field, value) in enumerate(zip(df["Field"], df["Value"])): |
| if not display_field.endswith("norm.") and isinstance(value, (int, float)) and not pd.isna(value): |
| |
| orig_field = next((k for k, v in rename_columns.items() if v == display_field), display_field) |
| numeric_fields.append(orig_field) |
| numeric_values.append(value) |
| numeric_indices.append(i) |
| |
| if numeric_fields: |
| |
| temp_df = pd.DataFrame([numeric_values], columns=numeric_fields) |
| |
| |
| formatted_df = format_results(temp_df, rename_columns) |
| |
| |
| for i, field in zip(numeric_indices, numeric_fields): |
| display_field = rename_columns.get(field, field) |
| df.iloc[i, df.columns.get_loc("Value")] = formatted_df.iloc[0][display_field] |
| |
|
|
| return name, summary, df |
|
|
|
|
| def spider_plot(df: pd.DataFrame) -> None: |
| spider_plot_cols = ['Beta norm.', 'Debt to Equity norm.', '1-year Return norm.', 'Revenue Growth norm.', 'Volatility norm.'] |
| plot_data = df[df['Field'].isin(spider_plot_cols)].set_index('Field') |
| values = plot_data.loc[spider_plot_cols, 'Value'].fillna(0.5).astype(float).tolist() |
| metrics_to_invert = ['Debt to Equity norm.', 'Beta norm.', 'Volatility norm.'] |
| values = [1 - v if col in metrics_to_invert else v for v, col in zip(values, spider_plot_cols)] |
| categories = [s.replace(' norm.', '').replace('1-year', '1yr').replace('Debt to Equity', 'D/E') for s in spider_plot_cols] |
| fig = go.Figure() |
|
|
| fig.add_trace(go.Scatterpolar( |
| r=values + [values[0]], |
| theta=categories + [categories[0]], |
| fill='toself', |
| name='Company Profile' |
| )) |
|
|
| fig.add_trace(go.Scatterpolar( |
| r=[0.5] * len(categories) + [0.5], |
| theta=categories + [categories[0]], |
| mode='lines', |
| line=dict(dash='dot', color='grey'), |
| fill='toself', |
| fillcolor='rgba(0,0,0,0)', |
| name='Median (0.5)' |
| )) |
|
|
| legend_text = ( |
| "<b>Quantile Scale: 0 to 1</b><br>" |
| "D/E, Beta, and Volatility:<br>" |
| "0 is highest, 1 is lowest<br>" |
| "Rev. growth and 1yr return:<br>" |
| "0 is lowest, 1 is highest<br>" |
| ) |
|
|
| fig.update_layout( |
| polar=dict( |
| radialaxis=dict( |
| visible=True, |
| range=[0, 1] |
| )), |
| showlegend=True, |
| title='Normalized Company Metrics', |
| annotations=[ |
| go.layout.Annotation( |
| text=legend_text, |
| align='right', |
| showarrow=False, |
| xref='paper', |
| yref='paper', |
| x=1.41, |
| y=-0.1 |
| ) |
| ], |
| margin=dict(b=120), |
| width=600, |
| height=500 |
| ) |
|
|
| fig.show() |
|
|
|
|
| |
| def get_spider_plot_fig_v0(df: pd.DataFrame): |
| spider_plot_cols = ['Beta norm.', 'Debt to Equity norm.', '1-year Return norm.', 'Revenue Growth norm.', 'Volatility norm.'] |
| plot_data = df[df['Field'].isin(spider_plot_cols)].set_index('Field') |
| values = plot_data.loc[spider_plot_cols, 'Value'].fillna(0.5).astype(float).tolist() |
| metrics_to_invert = ['Debt to Equity norm.', 'Beta norm.', 'Volatility norm.'] |
| values = [1 - v if col in metrics_to_invert else v for v, col in zip(values, spider_plot_cols)] |
| categories = [s.replace(' norm.', '').replace('1-year', '1yr').replace('Debt to Equity', 'D/E') for s in spider_plot_cols] |
| company_name = df.loc[df['Field'] == 'Name', 'Value'].values[0] |
| fig = go.Figure() |
|
|
|
|
| fig.add_trace(go.Scatterpolar( |
| r=values + [values[0]], |
| theta=categories + [categories[0]], |
| fill='toself', |
| name='Company Profile' |
| )) |
|
|
| fig.add_trace(go.Scatterpolar( |
| r=[0.5] * len(categories) + [0.5], |
| theta=categories + [categories[0]], |
| mode='lines', |
| line=dict(dash='dot', color='grey'), |
| fill='toself', |
| fillcolor='rgba(0,0,0,0)', |
| name='Median (0.5)' |
| )) |
|
|
| legend_text = ( |
| "<b>Quantile Scale: 0 to 1</b><br>" |
| "D/E, Beta, and Volatility:<br>" |
| "0 is highest, 1 is lowest<br>" |
| "Rev. growth and 1yr return:<br>" |
| "0 is lowest, 1 is highest<br>" |
| ) |
|
|
| fig.update_layout( |
| polar=dict( |
| radialaxis=dict( |
| visible=True, |
| range=[0, 1] |
| )), |
| showlegend=True, |
| title=f'{company_name} - Normalized Metrics', |
| annotations=[ |
| go.layout.Annotation( |
| text=legend_text, |
| align='right', |
| showarrow=False, |
| xref='paper', |
| yref='paper', |
| x=1.41, |
| y=-0.1 |
| ) |
| ], |
| margin=dict(b=120), |
| width=600, |
| height=500 |
| ) |
|
|
| return fig |
|
|
|
|
| def get_spider_plot_fig(df: pd.DataFrame): |
| spider_plot_cols = ['Beta norm.', 'Debt to Equity norm.', '1-year Return norm.', 'Revenue Growth norm.', 'Volatility norm.'] |
| plot_data = df[df['Field'].isin(spider_plot_cols)].set_index('Field') |
| values = plot_data.loc[spider_plot_cols, 'Value'].fillna(0.5).astype(float).tolist() |
| metrics_to_invert = ['Debt to Equity norm.', 'Beta norm.', 'Volatility norm.'] |
| values = [1 - v if col in metrics_to_invert else v for v, col in zip(values, spider_plot_cols)] |
| |
| |
| avg_strength = round(np.mean(values) * 100) |
| |
| |
| if avg_strength < 30: |
| profile_color = 'red' |
| elif avg_strength < 50: |
| profile_color = 'gold' |
| elif avg_strength < 60: |
| profile_color = 'blue' |
| else: |
| profile_color = 'green' |
| |
| categories = [s.replace(' norm.', '').replace('1-year', '1yr').replace('Debt to Equity', 'D/E') for s in spider_plot_cols] |
| company_name = df.loc[df['Field'] == 'Name', 'Value'].values[0] |
| fig = go.Figure() |
|
|
| fig.add_trace(go.Scatterpolar( |
| r=values + [values[0]], |
| theta=categories + [categories[0]], |
| fill='toself', |
| name='Company Profile', |
| line=dict(color=profile_color), |
| fillcolor=f'rgba({",".join(["255,0,0,0.2" if profile_color == "red" else "255,215,0,0.2" if profile_color == "gold" else "0,0,255,0.2" if profile_color == "blue" else "0,128,0,0.2"])})' |
| )) |
|
|
| fig.add_trace(go.Scatterpolar( |
| r=[0.5] * len(categories) + [0.5], |
| theta=categories + [categories[0]], |
| mode='lines', |
| line=dict(dash='dot', color='grey'), |
| fill='toself', |
| fillcolor='rgba(0,0,0,0)', |
| name='Median (0.5)' |
| )) |
|
|
| |
| if avg_strength < 30: |
| strength_level = "very low" |
| text_color = "red" |
| elif avg_strength < 50: |
| strength_level = "low" |
| text_color = "gold" |
| elif avg_strength < 60: |
| strength_level = "medium" |
| text_color = "blue" |
| else: |
| strength_level = "high" |
| text_color = "green" |
| |
| legend_text = ( |
| f"<b>Avg. strength: {avg_strength}</b> (<span style='color:{text_color}'>{strength_level}</span>)<br><br>" |
| "<b>Quantile Scale: 0 to 1</b><br>" |
| "D/E, Beta, and Volatility:<br>" |
| "0 is highest, 1 is lowest<br>" |
| "Rev. growth and 1yr return:<br>" |
| "0 is lowest, 1 is highest<br>" |
| ) |
|
|
| fig.update_layout( |
| polar=dict( |
| radialaxis=dict( |
| visible=True, |
| range=[0, 1] |
| )), |
| showlegend=True, |
| title=f'{company_name} - Normalized Metrics', |
| annotations=[ |
| go.layout.Annotation( |
| text=legend_text, |
| align='right', |
| showarrow=False, |
| xref='paper', |
| yref='paper', |
| x=1.41, |
| y=-0.1 |
| ) |
| ], |
| margin=dict(b=120), |
| width=600, |
| height=500 |
| ) |
|
|
| return fig |