Microsoft Fabric Updates Blog

Enrich Power BI reports with machine learning in Microsoft Fabric

Many organizations want to go beyond descriptive Power BI reports and start answering forward‑looking questions with machine learning—identifying emerging trends, at-risk accounts, and where to focus effort to maximize impact.

In practice, this is hard. Adding machine learning to Power BI often means moving data out of semantic models, rebuilding logic, managing separate storage and security, and assembling custom pipelines. As teams cross these boundaries, definitions drift, refreshes break, and insights rarely return to the tools business users rely on.

Microsoft Fabric takes a different approach.

Fabric unifies data engineering, data science, and business intelligence on a single, unified platform built on OneLake. Instead of breaking apart your BI stack, Fabric lets you extend it—reusing governed semantic models, training models where the data lives, and operationalize predictions in the same flow without duplicating logic or rebuilding pipelines.

This post shows an end‑to‑end pattern for enriching a Power BI report with machine learning in Fabric. We start with a governed semantic model, train a churn-prediction model, and operationalize predictions with batch and real‑time scoring. The result is predictive insight aligned with business logic, reliably refreshed, and surfaced directly in Power BI for monitoring, sharing, and action.

Scenario: Predicting bank customer churn

Across industries, teams use Power BI to understand what has already happened. Dashboards show trends, highlight performance, and keep organizations aligned around a shared view of the business.

But leaders are asking new questions—not just what happened, but what is likely next and how outcomes might change if they act. They want insights that help teams prioritize, intervene earlier, and focus effort where it matters. This is why many organizations look to enrich Power BI reports with machine learning.

This challenge is especially common in financial services.

Consider a bank that uses Power BI to track customer activity, balances, and service usage. Historical analysis shows that around 20% of customers churn, with churn tied to factors such as customer tenure, product usage, service interactions, and balance changes.

Figure 1 Bank customer churn overview in Power BI
Figure 1 Bank customer churn overview in Power BI

At this point, descriptive reporting is no longer enough. The real questions become:

  • Which customers are most likely to churn next?
  • How confident are those predictions?
  • Can churn risk update automatically as customer data changes?
  • Can accounts teams monitor high‑risk customers and act early?

Microsoft Fabric makes it possible to answer those questions with a fully integrated, end-to-end workflow.

Architecture

Figure 2 Architecture to enrich Power BI reports with ML Model

First, let’s look briefly at the architecture:

  1. Semantic model defines business logic and metrics.
  2. Semantic Link to access that model directly from notebooks.
  3. Fabric ML experiment to train and evaluate a churn prediction model.
  4. Batch scoring applies to the model at scale and saves predictions to OneLake.
  5. Real-time scoring endpoints support low-latency inference where needed.
  6. Dataflow Gen2 calls the real-time scoring endpoint to enrich data during ingestion.
  7. Power BI Report visualizing churn risk and driving action.

All components live natively in Fabric and share the same security, governance, and storage layer.

Prerequisites

Before starting, you should have:

  • A Fabric-enabled workspace.
  • A “Bank Customer Churn Analysis” semantic model published in the workspace.

You can find a sample semantic model and notebooks in this repository.

Step 1: Explore the semantic model with Semantic Link

A common challenge in analytics organizations is logic duplication. Business concepts like active customer, average monthly balance, or churned user are first defined in DAX for reporting, then redefined again in SQL or Spark for pipelines, and rebuilt once more in Python during feature engineering. Over time, those definitions drift, numbers diverge, and teams spend more time reconciling metrics than using them.

In Fabric, the Power BI semantic model becomes the contract. Instead of exporting data and reimplementing logic, Semantic Link lets you query the semantic model directly from a Fabric notebook.

Measures, filters, and calculated columns behave exactly as they do in Power BI. There is no separate authentication step, and no risk of logic drifting over time—the Fabric security context applies automatically.

With one line of Python code, you can explore all semantic models, tables, and columns.

# Semantic Link uses your Fabric identity — no separate credential handling.
import sempy.fabric as fabric
# Discover semantic models (datasets) you can access
df_datasets = fabric.list_datasets()
display(df_datasets)
# Inspect tables in a specific semantic model
dataset_name = "Bank Customer Churn Analysis"
df_tables = fabric.list_tables(dataset_name)
display(df_tables)
# Inspect columns and metadata (useful for feature planning)
df_columns = fabric.list_columns(dataset_name)
display(df_columns)
Figure 3 Semantic model discovery

Figure 3 Semantic model discovery

When you query a Power BI semantic model through Semantic Link, you work with the same logic that already powers your reports, not a parallel copy for data science. The result is a FabricDataFrame, which looks and feels like a pandas DataFrame but carries much more value. It preserves semantic context—such as measures, calculations, and lineage—directly from the semantic model into the data science environment. Because it subclasses pandas, you can use it with familiar libraries and workflows, while also calling Power BI measures directly from your notebook.

# Measures are the business logic. Keeping them centralized prevents drift. df_measures = fabric.list_measures(dataset_name) display(df_measures)
Figure 4 Inspect measure definitions
Figure 4 Inspect measure definitions
# Pull governed data from the semantic model.
# This keeps your ML features aligned with what Power BI shows.
df_raw = fabric.read_table(dataset=dataset_name, table="churn")
display(df_raw)
Figure 5 Pull Semantic model table as a SparkDataFrame
Figure 5 Pull Semantic model table as a SparkDataFrame

This lets you build features, explore data, and train models on the same business definitions your stakeholders trust in Power BI—without rewriting logic, managing credentials, or worrying about drift over time.

Once the data is available in the notebook, you can perform exploratory analysis with familiar tools such as pandas, seaborn, or matplotlib. Any additional features you derive build on centrally governed definitions instead of replacing them. That foundation makes everything that follows safe, repeatable, and trustworthy.

import seaborn as sns
sns.set_theme(style="whitegrid", palette="tab10", rc = {'figure.figsize':(9,6)})
import matplotlib.pyplot as plt
import matplotlib.ticker as mticker
from matplotlib import rc, rcParams
import numpy as np
import pandas as pd
import itertools

Drop the duplicated rows, rows with missing data and drop the columns that you do not need.

def clean_data(df):
# Drop rows with missing data across all columns
df = df.dropna()
# Drop duplicate rows in columns: 'CustomerId', 'RowNumber'
df = df.drop_duplicates(subset=['CustomerId', 'RowNumber'])
# Drop columns: 'RowNumber', 'Surname'
return df
df_clean = clean_data(df_raw.copy())
df_clean.head()
Figure 6 Cleaned training dataset
Figure 6 Cleaned training dataset

Data exploration

Display some summaries and visualizations of the cleaned data. Use this code to determine categorical, numerical, and target attributes.

# Determine the dependent (target) attribute
dependent_variable_name = "Exited"
print(dependent_variable_name)
# Determine the categorical attributes
categorical_variables = [col for col in df_clean.columns if col in "O"
or df_clean[col].nunique() <=5
and col not in "Exited"]
print(categorical_variables)
# Determine the numerical attributes
numeric_variables = [col for col in df_clean.columns if df_clean[col].dtype != "object"
and df_clean[col].nunique() >5 and col not in "CustomerId"]
print(numeric_variables)

Show the five-number summary (the minimum score, first quartile, median, third quartile, the maximum score) for the numerical attributes, using box plots.

df_num_cols = df_raw[numeric_variables]
sns.set(font_scale = 0.7)
fig, axes = plt.subplots(nrows = 2, ncols = 3, gridspec_kw = dict(hspace=0.3), figsize = (17,8))
fig.tight_layout()
for ax,col in zip(axes.flatten(), df_num_cols.columns):
sns.boxplot(x = df_num_cols[col], color='green', ax = ax)
fig.delaxes(axes[1,2])
Figure 7 Five-number summary for numeric attributes
Figure 7 Five-number summary for numeric attributes

Show the distribution of exited versus non-exited customers across the categorical attributes.

df_raw['Exited'] = df_raw['Exited'].astype(str)
attr_list = ['Geography', 'Gender', 'HasCrCard', 'IsActiveMember', 'NumOfProducts', 'Tenure']
df_raw['Exited'] = df_raw['Exited'].astype(str)
fig, axarr = plt.subplots(2, 3, figsize=(15, 4))
for ind, item in enumerate (attr_list):
print(ind, item)
sns.countplot(x = item, hue = 'Exited', data = df_raw, ax = axarr[ind%2][ind//2])
fig.subplots_adjust(hspace=0.7)
Figure 8 Distribution of exited versus non-exited customers across the categorical attributes
Figure 8 Distribution of exited versus non-exited customers across the categorical attributes

Show the frequency distribution of numerical attributes using histogram.

columns = df_num_cols.columns[: len(df_num_cols.columns)]
fig = plt.figure()
fig.set_size_inches(18, 8)
length = len(columns)
for i,j in itertools.zip_longest(columns, range(length)):
plt.subplot((length // 2), 3, j+1)
plt.subplots_adjust(wspace = 0.2, hspace = 0.5)
df_num_cols[i].hist(bins = 20, edgecolor = 'black')
plt.title(i)
plt.show()
Figure 9 Frequency distribution of numerical attributes
Figure 9 Frequency distribution of numerical attributes

The exploratory analysis highlights a few clear patterns. Most customers are based in France, while Spain—despite having fewer customers—shows the lowest churn rate compared to France and Germany. Product adoption is limited, with very few customers using more than two bank products. Customer activity stands out as a strong signal: inactive customers are significantly more likely to churn. In contrast, gender and tenure length show little influence on whether a customer decides to close their account.

Feature engineering

Now let us perform feature engineering to generate new attributes based on current attributes:

df_clean['Tenure'] = df_clean['Tenure'].astype(int)
df_clean["NewTenure"] = df_clean["Tenure"]/df_clean["Age"]
df_clean["NewCreditsScore"] = pd.qcut(df_clean['CreditScore'], 6, labels = [1, 2, 3, 4, 5, 6])
df_clean["NewAgeScore"] = pd.qcut(df_clean['Age'], 8, labels = [1, 2, 3, 4, 5, 6, 7, 8])
df_clean["NewBalanceScore"] = pd.qcut(df_clean['Balance'].rank(method="first"), 5, labels = [1, 2, 3, 4, 5])
df_clean["NewEstSalaryScore"] = pd.qcut(df_clean['EstimatedSalary'], 10, labels = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10])

Perform one-hot encoding for geography and gender columns:

import pandas as pd
def clean_data(df_clean): 
# One-hot encode columns: 'Geography', 'Gender'
df_clean = pd.get_dummies(df_clean, columns=['Geography', 'Gender'])
return df_clean
df_clean_1 = clean_data(df_clean.copy())
df_clean_1.head()

Create a delta table for the cleaned data to get ready for model training.

table_name = "df_clean"
# Create Spark DataFrame from pandas
sparkDF=spark.createDataFrame(df_clean_1)
sparkDF.write.mode("overwrite").option("overwriteSchema", "true").format("delta").save(f"Tables/{table_name}")
print(f"Spark dataframe saved to delta table: {table_name}")

Step 2: Build a machine learning model in Fabric

With governed data in hand, the focus shifts from alignment to prediction: learning which customers are most likely to leave.

Model training

To predict churn, we train a LightGBM classifier, a strong fit for large tabular datasets like customer records. LightGBM captures non‑linear patterns with minimal tuning and integrates natively with Fabric’s MLflow experience, making experiments easy to track and models easy to operationalize.

Because churn data is naturally imbalanced, we apply SMOTE during training to ensure the model learns equally from both churned and retained customers. This combination keeps the notebook focused on insight and outcomes—building a reliable model that can be registered once and reused consistently across batch and real‑time scoring—rather than on managing infrastructure or custom training code.

# Install imblearn for SMOTE using pip

%pip install imblearn

Prior to training any machine learning model, you need to load the delta table from the Lakehouse to read the cleaned data you created in the previous step.

import pandas as pd
SEED = 12345
df_clean = spark.read.format("delta").load("Tables/df_clean").toPandas()

Now let us generate experiments for tracking and set the experiment.

import mlflow
# Setup experiment name
EXPERIMENT_NAME = "bank-churn-experiment"
mlflow.set_experiment(EXPERIMENT_NAME)

Import the required libraries for model training

from sklearn.model_selection import train_test_split
from lightgbm import LGBMClassifier
from sklearn.metrics import accuracy_score, f1_score, precision_score, confusion_matrix, recall_score, roc_auc_score, classification_report

Use the train_test_split function from scikit-learn to split the data into training, validation, and test sets.

#Split the dataset to 60%, 20%, 20% for training, validation, and test datasets
y = df_clean["Exited"]
X = df_clean.drop("Exited",axis=1)
#Train-Test Separation
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.20, random_state=SEED)
#Train-Validation Separation
X_train, X_val, y_train, y_val = train_test_split(X_train, y_train, test_size=0.25, random_state=SEED)

And save the test data to a delta table

# Save the test data to a delta table
table_name = "df_test"
df_test=spark.createDataFrame(X_test)
df_test.write.mode("overwrite").option("overwriteSchema", "true").format("delta").save(f"Tables/{table_name}")
print(f"Spark test DataFrame saved to delta table: {table_name}")

The data exploration in Step 1 showed that of 10,000 customers, only 2,037 customers (around 20%) left the bank, indicating a highly imbalanced dataset. With so few examples of the minority class, a model struggles to learn the decision boundary. SMOTE is the most widely used approach for synthesizing new minority-class samples.

Apply SMOTE to the training data to synthesize new samples for the minority class.

from collections import Counter
from imblearn.over_sampling import SMOTE
sm = SMOTE(random_state=1234)
X_res, y_res = sm.fit_resample(X_train, y_train)
new_train = pd.concat([X_res, y_res], axis=1)

Train the model using LightGBM and register the trained model as a ML model artifact.

At the time of this writing, Fabric Real-Time Scoring Endpoint did not support tensor-based models, so we created a non-tensor-based output schema for the model.

import mlflow.pyfunc

import pandas as pd

import numpy as np

class Predictor(mlflow.pyfunc.PythonModel):

def __init__(self, base_model):

self.base_model = base_modeldef predict(self, context, model_input: pd.DataFrame) -> pd.DataFrame:

# prediction

pred = self.base_model.predict(model_input)

pred = np.asarray(pred, dtype=np.int64)

# probability (positive class)

proba = self.base_model.predict_proba(model_input)

proba = np.asarray(proba[:, 1], dtype=np.float64)

return pd.DataFrame({

"prediction": pred,

"probability": proba

})

import mlflow
from mlflow.models.signature import infer_signature
import numpy as np
import pandas as pd
from lightgbm import LGBMClassifier
# Define the LightGMB model
lgbm_sm_model = LGBMClassifier(
learning_rate=0.07,
max_delta_step=2,
n_estimators=100,
max_depth=10,
eval_metric="logloss",
objective='binary',
random_state=42
)
with mlflow.start_run(run_name="lgbm_sm_non_tensor") as run:
lgbm_non_tensor_sm_run_id = run.info.run_id
 
# Train on balanced data
lgbm_sm_model.fit(X_res, y_res.ravel())
# Validation predictions (convert to pandas for non-tensor signature)
y_pred = lgbm_sm_model.predict(X_val)
# Optionally include probability for positive class as a tabular output example for signature
y_proba = lgbm_sm_model.predict_proba(X_val)[:, 1]
y_proba_series = pd.Series(y_proba.astype(float), name="probability")
# Build output example for signature (TWO cols)
pred_ex = np.asarray(lgbm_sm_model.predict(X_val), dtype=np.int64)
proba_ex = np.asarray(lgbm_sm_model.predict_proba(X_val)[:, 1], dtype=np.float64)
y_example = pd.DataFrame({"prediction": pred_ex, "probability": proba_ex})
signature = infer_signature(X_val, y_example)
 
# Compute metrics
accuracy = accuracy_score(y_val, y_pred)
cr_lgbm_sm = classification_report(y_val, y_pred)
cm_lgbm_sm = confusion_matrix(y_val, y_pred)
roc_auc_lgbm_sm = roc_auc_score(y_res, lgbm_sm_model.predict_proba(X_res)[:, 1])
 
# Log metrics
mlflow.log_metric("val_accuracy", accuracy)
mlflow.log_metric("train_roc_auc", roc_auc_lgbm_sm)
# Classification report
mlflow.log_text(cr_lgbm_sm, "classification_report.txt")
 
# Confusion matrix
cm_df = pd.DataFrame( cm_lgbm_sm, index=["Actual_0", "Actual_1"], columns=["Pred_0", "Pred_1"])
mlflow.log_table(cm_df, "confusion_matrix.json")
 
# Log model
mlflow.pyfunc.log_model(
artifact_path="model",
python_model=Predictor(lgbm_sm_model),
signature=signature,
input_example=X_val.head(5),
registered_model_name="lgbm_sm_non_tensor"
)

View the machine learning experiment

The experiment runs are automatically saved in the experiment artifact that can be found from the workspace. You can select the link from the cell output and open the experiment view. Parameters, metrics, and artifacts are all captured in the experiment, making every run reproducible and auditable.

Figure 10 Fabric machine learning experiment view
Figure 10 Fabric machine learning experiment view

Validate the model performance

Once machine learning model training is complete, you can assess its performance.

Open the saved experiment, load the machine learning models, and evaluate them on the validation dataset.

import mlflow.pyfunc
# Fetch the model
load_model_lgbm1_sm = mlflow.pyfunc.load_model(f"runs:/{lgbm_non_tensor_sm_run_id}/model")
# Assess the performance of the loaded model on validation dataset
ypred_lgbm1_sm_v1 = load_model_lgbm1_sm.predict(X_val)

Next, you will develop a script to plot the confusion matrix to evaluate the accuracy of the classification using the validation dataset.

import seaborn as sns
sns.set_theme(style="whitegrid", palette="tab10", rc = {'figure.figsize':(9,6)})
import matplotlib.pyplot as plt
import matplotlib.ticker as mticker
from matplotlib import rc, rcParams
import numpy as np
import itertools
def plot_confusion_matrix(cm, classes, normalize=False, title='Confusion matrix', cmap=plt.cm.Blues):
print(cm)
plt.figure(figsize=(4,4))
plt.rcParams.update({'font.size': 10})
plt.imshow(cm, interpolation='nearest', cmap=cmap)
plt.title(title)
plt.colorbar()
tick_marks = np.arange(len(classes))
plt.xticks(tick_marks, classes, rotation=45, color="blue")
plt.yticks(tick_marks, classes, color="blue")
fmt = '.2f' if normalize else 'd'
thresh = cm.max() / 2.
for i, j in itertools.product(range(cm.shape[0]), range(cm.shape[1])):
plt.text(j, i, format(cm[i, j], fmt), horizontalalignment="center",
color="red" if cm[i, j] > thresh else "black")
plt.tight_layout()
plt.ylabel('True label')
plt.xlabel('Predicted label')

cfm = confusion_matrix(y_val, y_pred=ypred_lgbm1_sm_v1["prediction"])
plot_confusion_matrix(cfm, classes=['Non Churn','Churn'], title='LightGBM-non-tensor')
tn, fp, fn, tp = cfm.ravel()
Figure 11 Confusion matrix for the churn prediction model
Figure 11 Confusion matrix for the churn prediction model

Once performance is validated, the model is registered in the MLflow Model Registry. Moving forward, the model becomes a managed Fabric asset with a stable name and version, ready to be consumed by downstream pipelines.

Step 3: Batch scoring on active customers

Once the model is registered, the fastest way to make it usable is to score data at Spark scale and persist the results in OneLake—in our case, scoring active customers to identify who is likely to churn next.

Fabric’s PREDICT capability is built for this: you point to a named model version in the Fabric registry and run inference where the data lives. That model name and version keeps scoring repeatable, letting you rerun the job with the same model and upgrade only when ready.

Start by loading the latest snapshot of active customers from OneLake and applying the same feature-engineering logic to the active users.

# Load the active users
active_users = spark.read.format("delta").load("Tables/active")
display(active_users)

from pyspark.sql import functions as F

from pyspark.sql.window import Window

# Start with Spark DataFrame

df = active_users.drop("RowNumber", "Surname")

# Tenure as integer

df = df.withColumn("Tenure", F.col("Tenure").cast("int"))

# NewTenure = Tenure / Age

df = df.withColumn("NewTenure", F.col("Tenure") / F.col("Age"))

# CreditScore qcut into 6 bins

credit_window = Window.orderBy("CreditScore")

df = df.withColumn("CreditIdx", F.row_number().over(credit_window))

credit_count = df.count()

df = df.withColumn("NewCreditsScore", F.ceil(F.lit(6) * F.col("CreditIdx") / F.lit(credit_count)))

# Age qcut into 8 bins

age_window = Window.orderBy("Age")

df = df.withColumn("AgeIdx", F.row_number().over(age_window))

age_count = df.count()

df = df.withColumn("NewAgeScore", F.ceil(F.lit(8) * F.col("AgeIdx") / F.lit(age_count)))

# Balance qcut with rank(method="first") into 5 bins

balance_window = Window.orderBy("Balance")

df = df.withColumn("BalanceRank", F.row_number().over(balance_window))

balance_count = df.count()

df = df.withColumn("NewBalanceScore", F.ceil(F.lit(5) * F.col("BalanceRank") / F.lit(balance_count)))

# EstimatedSalary qcut into 10 bins

salary_window = Window.orderBy("EstimatedSalary")

df = df.withColumn("SalaryIdx", F.row_number().over(salary_window))

salary_count = df.count()

df = df.withColumn("NewEstSalaryScore", F.ceil(F.lit(10) * F.col("SalaryIdx") / F.lit(salary_count)))

# Cleanup temporary columns

df_clean = df.drop("CreditIdx","AgeIdx","BalanceRank","SalaryIdx")


from pyspark.sql import functions as F 

def clean_data_spark(df):

df = (

df

# One-hot Geography

.withColumn("Geography_France", F.when(F.col("Geography") == "France", 1).otherwise(0))

.withColumn("Geography_Germany", F.when(F.col("Geography") == "Germany", 1).otherwise(0))

.withColumn("Geography_Spain", F.when(F.col("Geography") == "Spain", 1).otherwise(0))

# One-hot Gender

.withColumn("Gender_Male", F.when(F.col("Gender") == "Male", 1).otherwise(0))

.withColumn("Gender_Female", F.when(F.col("Gender") == "Female", 1).otherwise(0))

# Drop original categorical columns

.drop("Geography", "Gender")

)

return df

df_clean_1 = clean_data_spark(df_clean)

display(df_clean_1)

In practice, you have a few equally supported ways to invoke PREDICT, depending on how you like to work. If you prefer a notebook‑first workflow, the Transformer API gives you a clean wrapper around your registered MLflow model. You create an MLFlowTransformer, tell it which input columns to use, and choose an output column name (for example, predictions).

from synapse.ml.predict import MLFlowTransformer

model = MLFlowTransformer(

inputCols=list(df_clean_1.columns),

outputCol='predictions',

modelName='lgbm_sm_non_tensor',

modelVersion=1

)

From there, scoring is a standard Spark transform: you call transform() and you get back a DataFrame that includes predicted labels and probabilities—ready to save as a Delta table. The payoff is simple: the scoring step feels like any other Spark pipeline step, and it scales with your data.

import pandas

from pyspark.sql.functions import col predictions = model.transform(df_clean_1)

predictions = predictions.select(

"*",

col("predictions.prediction").alias("prediction"),

col("predictions.probability").alias("probability")

).drop("predictions")


ay(predictions)

If your team prefers more declarative pipelines—or you want scoring that reads like a query—Fabric also supports calling PREDICT through Spark SQL. That is useful when you want inference to sit alongside familiar SQLstyle transformations, or when you are handing off a scoring step to engineers who live in SQL. style transformations, or

from pyspark.ml.feature import SQLTransformer

# Substitute "model_name", "model_version", and "features" below with values for your own model name, model version, and feature columns

model_name = 'lgbm_sm_non_tensor'

model_version = 1

features = df_clean_1.columns

sqlt = SQLTransformer().setStatement( f"SELECT PREDICT('{model_name}/{model_version}', {','.join(features)}) as predictions FROM __THIS__")

# Substitute "X_test" below with your own test dataset

display(sqlt.transform(df_clean_1))

And if you need maximum flexibility inside PySpark, you can convert the registered model into a PySpark UDF and call it like any other function over columns. This is handy when scoring needs to be embedded inside more customized DataFrame logic, while keeping the model reference centralized in the registry.

from pyspark.sql.functions import col, pandas_udf, udf, lit

# Substitute "model" and "features" below with values for your own model name and feature columns

my_udf = model.to_udf()

features = df_clean_1.columns

display(df_clean_1.withColumn("predictions", my_udf(*[col(f) for f in features])))

Whatever you choose, the goal is the same: write predictions back to OneLake as a durable table. At that point, churn risk stops being “ML output in a notebook” and becomes governed data you can join, track over time, and surface directly in Power BI.

# Save predictions to lakehouse to be used for generating a Power BI report

table_name = "active_predictions"

predictions.write.format('delta').mode("overwrite").save(f"Tables/{table_name}")

print(f"Spark DataFrame saved to delta table: {table_name}")

Step 4: Enable real-time scoring

Batch scoring supports many reporting scenarios, but some decisions need predictions as soon as data changes—for example, a customer updating their profile, opening a support ticket, or onboarding. Waiting for the next scheduled refresh is often too late.

Microsoft Fabric addresses this with real-time scoring endpoints. The same model used for batch scoring can be activated as an online endpoint without rewriting or redeploying it. ‑time scoring endpoints‑deploying

An endpoint loads a specific model version once and serves low latency predictions through a secure REST interface. It uses the same feature schema and versioning model as batch inference, keeping training and serving cleanly separated. You can iterate on models without disrupting live scoring and always know which version is serving predictions.‑latency predictions through a secure REST interface. It uses the same feature schema and versioning model as batch inference,

Fabric makes this simple. Endpoints are a built-in property of ML models. You can enable them directly from the Fabric UI with a low code experience, preview sample predictions immediately, and then integrate them wherever needed—inside Fabric or from external systems—through a scalable, managed API.‑in property of ML models. You can enable them directly from the Fabric UI with a low‑code experience, preview sample predictions immediately, and then integrate them wherever

Figure 12 Activate real-time scoring endpoint for a ML model version
Figure 12 Activate real-time scoring endpoint for a ML model version

Step 5: Enrich data during ingestion with Dataflow Gen2

Realtime scoring becomes most powerful when it is embedded in the data ingestion pipeline. Instead of scoring after the fact, predictions are generated as data arrives or changes.‑time scoring becomes most powerful when it is embedded

With Dataflow Gen2, you can call the real-time scoring endpoint as part of a transformation step. New or updated customer records are sent to the model, enriched with churn prediction and probability, and written back to OneLake as part of the ingestion flow.‑time scoring endpoint as part of a transformation step. New or updated customer records are sent to the model, enriched with churn prediction and probability, and written back to OneLake as part of the ingestion flow.

This keeps your data continuously ML enriched without custom orchestration or brittle glue code. Downstream consumers—reports, dashboards, and analysts—always see the latest predictions, and the scoring logic stays centralized around the managed model, not scattered across pipelines.

Below is the sample M code for calling the ML endpoint from Dataflow Gen2. Use a service principal as credentials, and ensure it has the necessary permissions on the workspace hosting your ML model endpoint. To maintain workflow reliability, turn off the auto-sleep feature for endpoints.

(input as table) as table =>

let

    // Feature engineering

features = fnFeatureEngineering(input),

    // Build JSON request body

    jsonBody =

        "{""inputs"":" &

        Text.FromBinary(

            Json.FromValue(

                List.Transform(

                    Table.ToRows(features),

                    each _

                )

            )

        ) &

        "}",

    // OAuth parameters

    TenantId     = "<Your Tenant ID>",

    ClientId     = "<Your Client ID>",

    ClientSecret = "<Your Client Secret>",

    Scope        = "https://api.fabric.microsoft.com/.default",

    TokenUrl = "https://login.microsoftonline.com/" & TenantId & "/oauth2/v2.0/token",

    FormEncode =

        (fields as record) as text =>

            Text.Combine(

                List.Transform(

                    Record.FieldNames(fields),

                    (k) =>

                        Uri.EscapeDataString(k) & "=" &

                        Uri.EscapeDataString(Text.From(Record.Field(fields, k)))

                ),

                "&"

            ),

    TokenBodyText =

        FormEncode([

            client_id     = ClientId,

            client_secret = ClientSecret,

            grant_type    = "client_credentials",

            scope         = Scope

        ]),

    TokenResponse =

        Json.Document(

            Web.Contents(

                TokenUrl,

                [

                    Headers = [#"Content-Type" = "application/x-www-form-urlencoded"],

                    Content = Text.ToBinary(TokenBodyText)

                ]

            )

        ),

    AccessToken = TokenResponse[access_token],

     // Call ML model endpoint

    ScoreUrl = "<Your ML model endpoint url>",

    response =

        Json.Document(

            Web.Contents(

                ScoreUrl,

                [

                    Headers = [

                        #"Content-Type"  = "application/json",

                        #"Authorization" = "Bearer " & AccessToken

                    ],

                    Content = Text.ToBinary(jsonBody)

                ]

            )

        ),

   //  Parse orientation=values output

    PredictionsRaw = response[predictions],

    PredictionValues = List.Transform(PredictionsRaw, each _{0}),

    ProbabilityValues = List.Transform(PredictionsRaw, each _{1}),

    //  Append columns to table

    Result =

        Table.FromColumns(

            Table.ToColumns(features) &

                { PredictionValues, ProbabilityValues },

            Table.ColumnNames(features) &

                { "prediction", "probability" }

        )

in

    Result

Step 6: Visualize and act in Power BI

Once predictions are stored in OneLake, they behave like any other curated dataset in Fabric. You bring them into the semantic model, join them with customer dimensions, and shape them using the same governance patterns you already apply to business data. You can enrich your Power BI report by adding a dedicated view that highlights customers with a high risk of churn.

Figure 13 Customers with high churn risk
Figure 13 Customers with high churn risk

From the Power BI user’s perspective, churn risk becomes a clear, refreshed metric that can be filtered, segmented, and tracked over time. Account teams can monitor high-risk customers, prioritize outreach, and act before customers leave.

Under the hood, this experience is powered by a full machine learning pipeline. On the surface, it feels like standard BI—reliable, explainable, and easy to consume.

Closing thoughts

The outcome of this pattern isn’t just a trained model—it’s machine learning insight that behaves like data.

Predictions are written to OneLake as governed; refreshable tables aligned to the Power BI semantic model and surfaced directly in reports. As data changes, insights update. As definitions evolve, logic stays consistent. For business teams, churn risk appears as a trusted metric—filterable, explainable, and ready to act on.

Microsoft Fabric makes this practical by keeping data, models, and BI on the same platform. There’s no need to export data, rebuild business logic, or maintain custom scoring pipelines. Teams can move from descriptive reporting to predictive insight without adding operational complexity.

The same approach extends naturally to forecasting, anomaly detection, and propensity modeling. The principle stays the same: start from governed semantics, train models in place, and operationalize predictions where the business already works. In Fabric, machine learning isn’t adjacent to BI—it’s embedded in the analytics experience.

Try it out

  • Explore the sample repository and try it out. If you’re already using Power BI and Fabric, you can apply this pattern directly to your own data—starting with a single semantic model, one report, and one prediction, then expand as value becomes clear.
  • Check out Fabric data science document for more information.
  • If you’d like to learn more or discuss how this fits your organization, you can book time with author.

無相關文章。