Forum Discussion
Python Visual rendering issue on .Net wrapper
- 7 months ago
Hi prasaddn
Since pre-computing every permutation of filters isn't feasible for free-text comments, you need to move the Python logic "up" the stack
Solution 1: Move Logic to the .NET Wrapper (Pro-Code)
Since you are already using a .NET portal, you can handle the summarization outside of Power BI.
The Workflow:
Use the Power BI Client SDK in your .NET app to capture the state of the slicers.
Use a DAX Query via the REST API (Datasets - Execute Queries) to fetch only the filtered subset of comments.
Pass those comments to a Python Azure Function (where you can reuse your existing script).
Display the result in a standard HTML <div> or text box in your portal, positioned next to the report.
Solution 2: Power Automate + AI Builder (Low-Code)
If you want to keep the result inside the report area, you can replace the Python visual with a Power Automate visual.
The Workflow:
Add the Power Automate for Power BI visual to your report.
Pass the "Comments" field into the visual data bucket.
Create a flow that triggers on button click, sends the filtered data to Azure OpenAI or AI Builder, and returns the summary to a "Results" table.
Benefit: This avoids the sandbox limitations because the "Visual" is just a button, and the actual processing happens in the cloud.
Solution 3: The "HTML Content" Visual (Fastest Implementation)
You can use the HTML Content custom visual (available in AppSource) to display dynamic text that standard visuals can't handle.
Instead of Python, you can use a Calculation Group or a complex DAX measure to concatenate the strings (if the summarization logic is simple).
If the logic must be Python, you can use Power Query to call a web API during the refresh, though this will not be "real-time" for slicer changes.
If this architectural change helps you bypass the Python visual limitation in your portal, please mark this as an "Accepted Solution" to help others!
Best regards,
Vishwanath
- prasaddn7 months agoFrequent Visitor
Dear AshokKunwar , Firstly thank you for providing 3 different approaches. I need to also call out that I am new to Power BI.
Solution 1: Move Logic to the .NET Wrapper (Pro-Code)
I don't have Power BI Client SDK and the .NET portal is also developed by some other team, and we use them to host our dashboard. so literally, I dont have access to edit or modify anything in the portal. I am also not familiar with using Azure functions.
Solution 2: Power Automate + AI Builder (Low-Code)
Like I mentioned, do not have exposure or access to Azure or AI functions or token based model.
Solution 3: The "HTML Content" Visual (Fastest Implementation). I was thinking we may have some custom visual that could take input as set of text comments, and then return some summarize text. However, I am not sure if this can be done with calcaluation groups as it involves understanding all the comments and creating short summary.
Please advise if the attached python script here, can be converted in a DAX function or some script that works with HTML Content custom visual and in .NET environment?
apologies as it is getting a long post:
====================import matplotlib.pyplot as plt
import re
from collections import Counter
import textwrap
# ============================================================
# CONFIG
# ============================================================
EVENT_COL = "Event Name"
TEXT_COL = "Any suggestions for us to improve your experience?"
MAX_THEMES = 5 # show at most this many themes
MIN_COUNT_RAW = 4 # min frequency for unmapped raw words (very strict)
RAW_THEMES_ON = True # set False if you ONLY want curated themes from label_map
# ============================================================
# ============================================================
# STEP 1 — Extract event-name tokens so event names never become themes
# ============================================================
def tokenize_event_name(name: str):
if not isinstance(name, str):
return []
name = name.lower()
name = re.sub(r"[^a-z0-9\s]", " ", name)
return [t for t in name.split() if len(t) > 2]
event_tokens = set()
if EVENT_COL in dataset.columns:
for ev in dataset[EVENT_COL].dropna().unique():
event_tokens.update(tokenize_event_name(ev))
# ============================================================
# STEP 2 — Robust stopword + junk dictionary
# ============================================================
# Base English stopwords (NLTK-style)
base_stopwords = {
"i","me","my","myself","we","our","ours","ourselves","you","your","yours","yourself","yourselves",
"he","him","his","himself","she","her","hers","herself","it","its","itself",
"they","them","their","theirs","themselves",
"what","which","who","whom","this","that","these","those",
"am","is","are","was","were","be","been","being",
"have","has","had","having","do","does","did","doing",
"a","an","the","and","but","if","or","because","as","until","while",
"of","at","by","for","with","about","against","between","into","through","during",
"before","after","above","below","to","from","up","down","in","out","on","off","over","under",
"again","further","then","once","here","there","when","where","why","how",
"all","any","both","each","few","more","most","other","some","such",
"no","nor","not","only","own","same","so","than","too","very",
"can","will","just","don","should","now","also","please","include","better"
}
# Extra junk/structural words specific to suggestions context
extra_junk = {
# Generic praise/fillers – not themes
"good","great","nice","awesome","amazing","excellent","fantastic","superb","wonderful","best",
"well","fine","ok","okay","super","memorable","enjoyable","satisfying",
# Politeness / thanks
"thank","thanks","thankyou","thanks!","thanks.","thanks!!","thanks!!!","thanks🙏","thankyou🙏",
# Question echo / filler words
"suggestions","suggestion","improve","improved","improvement","improvements","experience",
"feedback","inputs","input","point","points","comment","comments","share","shared","sharing",
# Event-generic nouns
"event","events","session","sessions","meeting","meetings","program","programs",
"show","shows","function","functions","day","days","today","yesterday","tomorrow",
"evening","morning","afternoon",
# Generic verbs & vague stuff
"got","had","made","did","doing","done","make","makes","take","took","taken",
"see","saw","seen","want","wanted","would","could","should","hope","hoped","hoping",
"looking","look","felt","feel","feeling","enjoy","enjoyed","liked","like","love","loved",
"appreciate","appreciated","helped","helpful","managed","arranged","organized","organised",
"planning","planned","coordinate","coordinated","support","supported","supportive",
# Short conversational fillers
"yeah","yes","yep","okie","okay","hmm","hmmm","hahaha","haha","lol",
# Emojis / junk tokens
"👍","👏","😊","🙏","😀","😄","😂","😁","🙂","😉","😇","🎉","🎊","💯","❤️","♥","😁😁","😂😂",
# Known junk/structural words you do not want as themes
"camp", # often part of event name like "Blood Donation Camp"
}
# Combine base stopwords, custom junk, and event tokens
stopwords = base_stopwords | extra_junk | event_tokens
# ============================================================
# STEP 3 — Tokenizer with strong filtering
# ============================================================
def clean_tokens(text: str):
if not isinstance(text, str):
return []
text = text.lower()
text = re.sub(r"[^a-z0-9\s]", " ", text)
tokens = []
for t in text.split():
if not t:
continue
t = t.strip()
# skip pure numbers
if t.isdigit():
continue
# skip very short tokens
if len(t) <= 2:
continue
# skip stopwords and event-name pieces
if t in stopwords or t in event_tokens:
continue
tokens.append(t)
return tokens
# ============================================================
# STEP 4 — Theme dictionary (label_map) tuned for improvements
# ============================================================
label_map = {
# Crowd / queue / flow
"crowd": "crowd & queues",
"crowded": "crowd & queues",
"queue": "crowd & queues",
"queues": "crowd & queues",
"line": "crowd & queues",
"lines": "crowd & queues",
"entry": "crowd & queues",
"exit": "crowd & queues",
"flow": "crowd & queues",
# Time / schedule / duration
"timing": "time management",
"delay": "time management",
"delays": "time management",
"late": "time management",
"earlier": "time management",
"duration": "time management",
"schedule": "time management",
"reschedule": "time management",
# Venue / seating / facilities
"venue": "venue & facilities",
"hall": "venue & facilities",
"auditorium": "venue & facilities",
"location": "venue & facilities",
"space": "seating & space",
"seating": "seating & space",
"seat": "seating & space",
"seats": "seating & space",
"chairs": "seating & space",
"chair": "seating & space",
"fan": "venue & facilities",
"fans": "venue & facilities",
"ac": "venue & facilities",
"air": "venue & facilities",
"sound": "sound & technical",
"audio": "sound & technical",
"mic": "sound & technical",
"speaker": "sound & technical",
"speakers": "sound & technical",
"light": "lighting & ambience",
"lights": "lighting & ambience",
"lighting": "lighting & ambience",
# Food / snacks / water
"snack": "snacks & refreshments",
"snacks": "snacks & refreshments",
"refreshment": "snacks & refreshments",
"refreshments": "snacks & refreshments",
"food": "snacks & refreshments",
"lunch": "snacks & refreshments",
"dinner": "snacks & refreshments",
"water": "snacks & refreshments",
"bottle": "snacks & refreshments",
"bottles": "snacks & refreshments",
# Registration / process / communication
"registration": "registration & process",
"register": "registration & process",
"forms": "registration & process",
"form": "registration & process",
"process": "registration & process",
"procedure": "registration & process",
"instructions": "communication & information",
"information": "communication & information",
"info": "communication & information",
"communication": "communication & information",
"update": "communication & information",
"updates": "communication & information",
"mail": "communication & information",
"email": "communication & information",
"whatsapp": "communication & information",
"notification": "communication & information",
"notifications": "communication & information",
# Parking / transport / access
"parking": "parking & access",
"park": "parking & access",
"entrypoint": "parking & access",
"gate": "parking & access",
"traffic": "parking & access",
# Kids & family
"kids": "kids & family activities",
"children": "kids & family activities",
"family": "kids & family activities",
"families": "kids & family activities",
"booth": "kids & family activities",
"photo": "kids & family activities",
"photos": "kids & family activities",
"photobooth": "kids & family activities",
"games": "kids & family activities", # kids game stalls etc.
"game": "kids & family activities",
# Frequency / future events
"frequency": "event frequency & cadence",
"regular": "event frequency & cadence",
"regularly": "event frequency & cadence",
"often": "event frequency & cadence",
"monthly": "event frequency & cadence",
"yearly": "event frequency & cadence",
"quarterly": "event frequency & cadence",
# Content / activities / engagement
"content": "content & activities",
"activities": "content & activities",
"activity": "content & activities",
"session": "content & activities",
"sessions": "content & activities",
"variety": "content & activities",
"options": "content & activities",
"options": "content & activities",
# Safety / hygiene
"safety": "safety & hygiene",
"safe": "safety & hygiene",
"hygiene": "safety & hygiene",
"cleanliness": "safety & hygiene",
"cleaning": "safety & hygiene",
"washroom": "safety & hygiene",
"washrooms": "safety & hygiene",
"toilet": "safety & hygiene",
"toilets": "safety & hygiene",
# Staffing / volunteers
"volunteers": "staffing & volunteers",
"volunteer": "staffing & volunteers",
"staff": "staffing & volunteers",
"helpdesk": "staffing & volunteers",
}
# ============================================================
# STEP 5 — Summariser
# ============================================================
def summarize_group(texts):
tokens = []
for t in texts:
tokens.extend(clean_tokens(t))
if not tokens:
return ""
counts = Counter(tokens)
# 1) Aggregate mapped themes
theme_counts = Counter()
for word, c in counts.items():
if word in label_map:
theme_counts[label_map[word]] += c
# 2) Optional: Add very strong unmapped words as raw themes
if RAW_THEMES_ON:
for word, c in counts.items():
if word in label_map:
continue
if c < MIN_COUNT_RAW:
continue
# avoid obvious verbs/adverbs
if word.endswith(("ed", "ing", "ly", "s")):
continue
if len(word) <= 3:
continue
if word in event_tokens:
continue
theme_counts[word] += c
if not theme_counts:
return ""
top_themes = [theme for theme, _ in theme_counts.most_common(MAX_THEMES)]
if not top_themes:
return ""
if len(top_themes) == 1:
return f"Participants most frequently suggested improvements in {top_themes[0]}."
elif len(top_themes) == 2:
return (
f"Participants most frequently suggested improvements in "
f"{top_themes[0]} and {top_themes[1]}."
)
else:
return (
"Participants most frequently suggested improvements in "
+ ", ".join(top_themes[:-1])
+ f", and {top_themes[-1]}."
)
def wrap_text(text, width=80):
return "\n".join(textwrap.wrap(text, width))
# ============================================================
# APPLY TO CURRENT FILTERED DATA
# ============================================================
if TEXT_COL not in dataset.columns:
summary_text = f"Column '{TEXT_COL}' not found in current visual."
else:
texts = dataset[TEXT_COL].dropna()
summary_text = summarize_group(texts)
if not summary_text:
summary_text = "No clear recurring improvement themes found for the current selection."
wrapped = wrap_text(summary_text)
# ============================================================
# DRAW VISUAL — robust against clipping
# ============================================================
fig_w, fig_h, dpi = 20, 4, 120
fontsize = 40
# Optional: smarter wrapping based on figure + font size
def dynamic_wrap(text, fig_width_in=fig_w, dpi=dpi, font_size=fontsize, char_width_px=0.6):
px_width = fig_width_in * dpi
approx_char_px = font_size * char_width_px
width_chars = max(30, int(px_width / approx_char_px))
return "\n".join(textwrap.wrap(text, width=width_chars))
wrapped = dynamic_wrap(summary_text)
fig = plt.figure(figsize=(fig_w, fig_h), dpi=dpi)
# Place text in FIGURE coordinates to avoid axes clipping
fig.text(0.5, 0.5, wrapped,
ha="center", va="center",
fontsize=fontsize, fontweight='bold',
color='darkblue',
wrap=True,
bbox=dict(boxstyle='round,pad=0.5', facecolor='white', alpha=0.95, edgecolor='white'))
plt.tight_layout(pad=2.0)
plt.show()====================
- kushanNa7 months agoSuper User
- prasaddn7 months agoFrequent Visitor
Hi kushanNa , given the challenges I have with report embedded in .NET portal, and not being able to pre-process, the solution you provided is definitely helpful.
I need to see and keep updating the ThemeMap table. And, this will improve the summarization. Out of so many different alternate solutions I explored and seen from expertise here, at least for my situation, this is most applicable solution. It does not mean the solutions others proposed is wrong, it was just not feasible in my business scenario.