Visualisation, databases, APIs, BI integration and first models in Python
Chart data, query SQL databases from Python, read JSON APIs, hand results to Power BI, version your code with Git, and train and evaluate a first model properly.
This lesson builds on: NumPy and pandas — the analysis workflow.
- Create clear charts with matplotlib
- Query SQL databases from Python and combine SQL with pandas
- Read JSON from APIs into DataFrames
- Choose a Python ↔ Power BI integration method
- Train a model with a proper train/test split and evaluate it honestly
Visualisation with matplotlib
import pandas as pd, matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
orders = pd.read_csv("retail/orders.csv", parse_dates=["order_date"])
items = pd.read_csv("retail/order_items.csv")
lines = items.merge(orders[["order_id", "order_date", "status", "channel"]], on="order_id")
lines["rev"] = lines["quantity"] * lines["unit_price"] * (1 - lines["discount_pct"])
monthly = (lines[lines["status"] == "completed"]
.groupby(lines["order_date"].dt.to_period("M"))["rev"].sum())
fig, ax = plt.subplots(figsize=(9, 3.5))
monthly.plot(ax=ax, color="#0B1530")
ax.set_title("Orion Retail — monthly net revenue")
ax.set_ylabel("Net revenue")
ax.spines[["top", "right"]].set_visible(False)
fig.tight_layout()
fig.savefig("/tmp/monthly_revenue.png", dpi=120)
print(monthly.idxmax(), round(monthly.max()), "| lowest:", monthly.idxmin(), round(monthly.min()))
2025-12 145452 | lowest: 2023-02 47929
The chart shows a growing trend with a December peak every year and a February trough. Principles from the dashboard lesson apply: title says what it is, axes labelled, no chart junk. seaborn and plotly offer higher-level statistical and interactive charts.
Databases and SQL integration
Python talks to databases through drivers (sqlite3, psycopg for PostgreSQL, pyodbc for SQL Server) or SQLAlchemy. Let the database do heavy filtering/aggregation and pandas the rest:
import sqlite3
con = sqlite3.connect("../../data/sqlite/telecom.sqlite")
query = """
SELECT contract_type, COUNT(*) AS subscribers, ROUND(AVG(churned) * 100, 1) AS churn_pct
FROM subscribers
GROUP BY contract_type
ORDER BY churn_pct DESC
"""
df = pd.read_sql_query(query, con)
print(df)
min_score = 70
high = pd.read_sql_query("SELECT source, COUNT(*) AS n FROM leads WHERE lead_score >= ? GROUP BY source", con, params=(min_score,))
print(high.sort_values("n", ascending=False).head(3))
con.close()
contract_type subscribers churn_pct
0 monthly 2076 55.7
1 24-month 2088 37.8
2 12-month 4146 35.4
source n
2 partner_shop 1216
5 website 855
4 social_ads 795
Monthly contracts churn far more than 12- and 24-month contracts — a finding you will revisit in statistics (is the difference reliable?) and decision-making (is it causal?).
f"... WHERE source = '{user_input}'" invites SQL injection. Use parameters (? / %s / :name) as above. Store credentials in environment variables, never in notebooks committed to Git.
APIs and JSON
Many sources (CRMs, ad platforms, exchange rates) expose REST APIs returning JSON:
import json
payload = '{"campaigns": [{"id": 1, "name": "Spring", "metrics": {"clicks": 1200, "spend": 300.5}}, {"id": 2, "name": "Summer", "metrics": {"clicks": 2500, "spend": 610.0}}]}'
data = json.loads(payload)
flat = pd.json_normalize(data["campaigns"])
flat["cpc"] = flat["metrics.spend"] / flat["metrics.clicks"]
print(flat)
id name metrics.clicks metrics.spend cpc
0 1 Spring 1200 300.5 0.250417
1 2 Summer 2500 610.0 0.244000
With a real API: requests.get(url, params=..., headers={"Authorization": f"Bearer {token}"}, timeout=30), check response.status_code, handle pagination and rate limits, and log failures.
Power BI integration
| Method | How | Notes |
|---|---|---|
| Write files | Python writes CSV/Parquet to SharePoint/OneDrive; Power BI imports | Simple and robust; schedule the script |
| Write a database table | Python writes results (e.g. churn scores) to SQL; Power BI imports | Best for production; governed |
| Python data source / visual in Power BI | Script runs inside Power BI | Needs Python on the refresh machine (personal gateway); not for heavy work |
Prefer the database or file route: Power BI stays a presentation layer, and Python stays testable.
Version control with Git
Analysis code changes; Git records who changed what and why. Minimum workflow: git init, git add file.py, git commit -m "Add churn features", push to GitHub/Azure DevOps, use branches and pull requests for review. Add .gitignore for data files, credentials and .ipynb_checkpoints. Notebooks diff poorly — move stable logic into .py modules.
A first model: train, test, evaluate
Predict whether a telecom subscriber churned from signup-time attributes. The model must be evaluated on data it did not see during training.
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import roc_auc_score, accuracy_score
subs = pd.read_csv("telecom/subscribers.csv")
X = pd.get_dummies(subs[["contract_type", "payment_method", "region", "acquisition_channel"]], drop_first=True)
X["age"] = subs["age"]
y = subs["churned"]
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25, random_state=42, stratify=y)
model = LogisticRegression(max_iter=1000).fit(X_train, y_train)
p_test = model.predict_proba(X_test)[:, 1]
print("baseline accuracy (always 'no churn'):", round(1 - y_test.mean(), 3))
print("model accuracy:", round(accuracy_score(y_test, p_test >= 0.5), 3))
print("test AUC:", round(roc_auc_score(y_test, p_test), 3))
coefs = pd.Series(model.coef_[0], index=X.columns).sort_values()
print(coefs.round(2).head(3)); print(coefs.round(2).tail(3))
baseline accuracy (always 'no churn'): 0.589
model accuracy: 0.638
test AUC: 0.635
acquisition_channel_referral -0.76
acquisition_channel_social_ads -0.76
acquisition_channel_door_to_door -0.50
dtype: float64
acquisition_channel_shop 0.31
payment_method_cash_terminal 0.31
contract_type_monthly 0.82
dtype: float64
How to read this:
- Baseline first: always predicting the majority class already achieves some accuracy. A model is only useful if it beats the baseline.
- AUC (0.5 = random, 1.0 = perfect ranking) measures how well the model ranks churners above non-churners.
- Coefficients: positive = higher churn odds. Month-to-month contracts and cash-terminal payments increase churn odds; these are associations, not proof that changing contracts would reduce churn.
- Feature engineering — the features came from categorical columns turned into 0/1 indicators (
get_dummies). Usage and ticket history would add signal, but only if measured before the churn decision; using post-churn data is leakage (Module 11).
Practice
Which is the safe way to filter by a user-supplied value in SQL from Python?
A churn model has 64% accuracy on the test set. 59% of test customers did not churn. What is the right interpretation?
Which is usually the most robust way to get Python model scores into a production Power BI report?
Summary
- matplotlib for clear static charts; label and simplify.
- Query databases with parameters; aggregate in SQL, analyse in pandas.
- Flatten JSON with
json_normalize; handle API pagination, limits and auth securely. - Integrate with Power BI through governed tables or files.
- Version code with Git; evaluate models on held-out data against a baseline.