Case Study 27-2: Maya's Portfolio Health Check
The Setup
Maya Reyes has been running her consulting practice for three and a half years. She works with eight to twelve active clients at any given time — mostly mid-sized companies in manufacturing, logistics, and professional services who hire her for process improvement projects, analytics strategy, and the occasional executive workshop.
Business has been steady. Invoices are going out. Payments are coming in. But on a rainy Wednesday evening in late January, Maya opens her client tracking spreadsheet and feels a vague unease she cannot quite name.
She has been so busy with active engagements that she has not looked at the full portfolio view in months. When she does, she notices something: two clients she thinks of as anchors — Meridian Logistics and Carver & Associates — have not sent her a new project inquiry in almost three months. Historically, both companies were in touch with her every four to six weeks about something.
"Maybe they're just quiet," she tells herself.
Maybe. But she has read enough about customer churn to know that "going quiet" is often the first symptom, not the last one.
Adapting RFM for a Consulting Practice
Maya's situation is different from a retailer's. She does not have hundreds of customers. She has a portfolio of relationships. But the same underlying logic applies:
- Recency: When did a client last contact her about a new engagement?
- Frequency: How often do they typically start new projects with her?
- Monetary: What is their average annual spend with her practice?
The difference is that Maya can score these more subjectively — she knows her clients well enough to calibrate — and she does not need quintiles. She needs a simple framework that tells her where to direct her attention.
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
# Maya's client portfolio data
# In practice, pulled from her project tracking CSV
client_data = {
"client_name": [
"Meridian Logistics",
"Carver & Associates",
"Thornton Manufacturing",
"Bellview Healthcare",
"Summit Retail Group",
"Okafor Strategy Partners",
"Greystone Capital",
"Lakewood City Schools",
],
"last_project_inquiry_date": [
"2023-10-08", # Meridian — quiet since October
"2023-10-21", # Carver — quiet since late October
"2024-01-03", # Thornton — recent
"2024-01-11", # Bellview — very recent
"2023-12-15", # Summit — about a month ago
"2023-11-28", # Okafor — about 7 weeks ago
"2023-12-01", # Greystone — about 6 weeks ago
"2023-11-05", # Lakewood — about 10 weeks ago
],
"projects_last_12_months": [4, 3, 2, 1, 3, 2, 1, 2],
"annual_revenue_usd": [
52_000, 41_000, 28_000, 14_000,
38_000, 24_000, 17_500, 21_000,
],
"relationship_years": [2.8, 2.1, 0.9, 0.3, 1.6, 1.1, 0.7, 1.8],
}
clients = pd.DataFrame(client_data)
clients["last_project_inquiry_date"] = pd.to_datetime(
clients["last_project_inquiry_date"]
)
analysis_date = pd.Timestamp("2024-01-24")
clients["days_since_last_inquiry"] = (
analysis_date - clients["last_project_inquiry_date"]
).dt.days
print(clients[[
"client_name", "days_since_last_inquiry",
"projects_last_12_months", "annual_revenue_usd"
]])
Output:
client_name days_since_last_inquiry projects_last_12_months annual_revenue_usd
0 Meridian Logistics 108 4 52000
1 Carver & Associates 95 3 41000
2 Thornton Manufacturing 21 2 28000
3 Bellview Healthcare 13 1 14000
4 Summit Retail Group 40 3 38000
5 Okafor Strategy Partners 57 2 24000
6 Greystone Capital 54 1 17500
7 Lakewood City Schools 80 2 21000
Building the Simplified RFM Scores
Maya creates a simplified 1–3 scoring scale. With only eight clients, quintile binning is overkill and would produce meaningless results. Instead, she defines the thresholds manually based on her knowledge of her own business rhythms.
def score_recency(days):
"""
1–3 score for how recently the client engaged.
Maya's clients typically reach out every 4–8 weeks,
so 90+ days is genuinely concerning.
"""
if days <= 30:
return 3 # Active
elif days <= 60:
return 2 # Cooling
else:
return 1 # Quiet / At risk
def score_frequency(projects):
"""
1–3 score for project frequency in the last 12 months.
1–2 projects = low, 3 = medium, 4+ = high.
"""
if projects >= 4:
return 3
elif projects >= 2:
return 2
else:
return 1
def score_monetary(revenue):
"""
1–3 score for annual revenue.
< $20K = low, $20K–$35K = medium, > $35K = high.
"""
if revenue >= 35_000:
return 3
elif revenue >= 20_000:
return 2
else:
return 1
clients["r_score"] = clients["days_since_last_inquiry"].apply(score_recency)
clients["f_score"] = clients["projects_last_12_months"].apply(score_frequency)
clients["m_score"] = clients["annual_revenue_usd"].apply(score_monetary)
clients["rfm_total"] = clients["r_score"] + clients["f_score"] + clients["m_score"]
print(clients[[
"client_name", "r_score", "f_score", "m_score", "rfm_total"
]].sort_values("rfm_total", ascending=False).to_string(index=False))
Output:
client_name r_score f_score m_score rfm_total
Summit Retail Group 3 2 3 8
Thornton Manufacturing 3 2 2 7
Bellview Healthcare 3 1 1 5
Okafor Strategy Partners 2 2 2 6
Greystone Capital 2 1 1 4
Lakewood City Schools 1 2 2 5
Carver & Associates 1 3 3 7
Meridian Logistics 1 3 3 7
The Two Clients That Need Attention
The number that jumps out is Meridian Logistics and Carver & Associates. Both have R-scores of 1 (quiet for 95–108 days) but F-scores and M-scores of 3 — meaning they were high-engagement, high-value clients.
This is the exact "At Risk" pattern: historically strong, currently disengaged.
def assign_client_segment(row):
"""Segment assignment for Maya's portfolio (using 1–3 scale)."""
r, f, m = row["r_score"], row["f_score"], row["m_score"]
if r == 3 and f >= 2 and m >= 2:
return "Active & Valuable"
elif r == 3 and (f == 1 or m == 1):
return "Active & Growing"
elif r == 2 and f >= 2:
return "Stable"
elif r == 2 and f == 1:
return "Cooling"
elif r == 1 and f >= 2 and m >= 2:
return "At Risk"
elif r == 1:
return "Lapsed"
else:
return "Monitor"
clients["segment"] = clients.apply(assign_client_segment, axis=1)
# Print the full portfolio view
print("\nMaya's Client Portfolio — Health Assessment")
print("=" * 60)
for _, row in clients.sort_values("rfm_total", ascending=False).iterrows():
flag = " *** PRIORITY ***" if row["segment"] == "At Risk" else ""
print(f"\n{row['client_name']}{flag}")
print(f" Segment: {row['segment']}")
print(f" Days since inquiry: {row['days_since_last_inquiry']}")
print(f" Projects (12 mo): {row['projects_last_12_months']}")
print(f" Annual revenue: ${row['annual_revenue_usd']:,}")
print(f" RFM total: {row['rfm_total']}/9")
Visualizing the Portfolio
Maya builds a simple 2x2 scatter plot — recency on the X-axis, annual revenue on the Y-axis — to see the portfolio visually.
fig, ax = plt.subplots(figsize=(10, 7))
# Color by segment
segment_colors = {
"Active & Valuable": "#1a9850",
"Active & Growing": "#91cf60",
"Stable": "#74add1",
"Cooling": "#fee090",
"At Risk": "#d73027",
"Lapsed": "#878787",
"Monitor": "#f0e442",
}
for _, row in clients.iterrows():
color = segment_colors.get(row["segment"], "#888888")
ax.scatter(
row["days_since_last_inquiry"],
row["annual_revenue_usd"],
s=row["projects_last_12_months"] * 120, # size = project frequency
c=color,
edgecolors="#333333",
linewidths=0.8,
zorder=3,
alpha=0.85,
)
ax.annotate(
row["client_name"].split()[0], # first word of client name
(row["days_since_last_inquiry"], row["annual_revenue_usd"]),
textcoords="offset points",
xytext=(8, 4),
fontsize=9,
)
# Reference lines
ax.axvline(60, color="#aaaaaa", linestyle="--", linewidth=1.0,
alpha=0.7, label="60-day threshold")
ax.axvline(90, color="#d73027", linestyle="--", linewidth=1.0,
alpha=0.7, label="90-day alert threshold")
ax.set_xlabel("Days Since Last Project Inquiry", fontsize=12)
ax.set_ylabel("Annual Revenue ($)", fontsize=12)
ax.set_title(
"Maya Reyes Consulting — Client Portfolio Health\n"
"(Bubble size = projects in last 12 months)",
fontsize=13,
fontweight="bold",
)
ax.yaxis.set_major_formatter(
plt.FuncFormatter(lambda x, _: f"${x:,.0f}")
)
# Legend for segments
legend_patches = [
mpatches.Patch(color=c, label=seg)
for seg, c in segment_colors.items()
if seg in clients["segment"].values
]
ax.legend(handles=legend_patches, loc="upper right", fontsize=9,
title="Segment", title_fontsize=9)
ax.grid(True, alpha=0.25)
ax.spines[["top", "right"]].set_visible(False)
plt.tight_layout()
plt.savefig("maya_client_portfolio.png", dpi=150, bbox_inches="tight")
plt.show()
print("Portfolio chart saved.")
The visual makes the situation unmistakable. Meridian Logistics and Carver & Associates are sitting in the upper-right of the danger zone: high value, long silence.
Maya Reaches Out
That same Wednesday evening, Maya drafts two emails. They are not form letters. She writes one for each client, referencing the specific work they did together and raising a natural next step.
To Meridian Logistics:
"Hi David — I've been thinking about your supply chain consolidation project we wrapped up in October, and particularly that bottleneck we identified in the inbound receiving process but didn't have time to dig into. I'd love to schedule 30 minutes to catch up and hear how Q4 went for you. Any chance you're free next week?"
To Carver & Associates:
"Hi Pritha — It's been a few months since we finished the pricing strategy review, and I'm curious how the new framework has been performing. Also, I recall you mentioned last time that the operations team was dealing with some workflow challenges — if that's still on your list, I'd be glad to connect about it. Happy to do a no-obligation call just to catch up."
Both emails go out by 8 p.m. By the following morning, both clients have replied.
David at Meridian: "Perfect timing, actually — we've been meaning to loop you back in on the receiving process. Can we do Thursday?"
Pritha at Carver: "Maya! I was literally drafting an email to you yesterday and got pulled into a fire. Yes, let's talk. The workflow issue has gotten worse."
What Maya Learned
Portfolio thinking changes how you manage client relationships. When you are heads-down in active projects, it is easy to let quieter clients drift. An RFM-style framework — even a simplified, eight-client version — forces you to look at the whole portfolio at once, not just the clients who are currently emailing you.
Recency is the variable most likely to surprise you. Frequency and value are things you generally know intuitively about your best clients. But you may not realize how long it has been since you last heard from a client — especially one you assume is satisfied. The data corrects that assumption.
Small datasets still benefit from structure. Maya could have done this in her head, or on a whiteboard. But writing the code forced her to be explicit about her thresholds and made the output shareable. When she wanted to explain the health check to her accountant (who was asking about revenue risk), she could send her a one-page summary generated from the script.
Proactive outreach is not pushy — it is professional. Both of Maya's emails were warmly received precisely because they were specific and timely, not because they were aggressive. The analysis gave her the trigger to reach out; her client knowledge gave her the content to do it well.
Make this a habit, not a one-time exercise. Maya decides to run her portfolio health check on the first of every month — no more than thirty minutes of work, but a consistent discipline that prevents the "going quiet" problem from sneaking up on her again.
The Numbers After 60 Days
Two months later, Maya runs the analysis again. Both Meridian and Carver have moved out of the "At Risk" segment into "Active & Valuable." The Thursday meeting with Meridian turned into a four-month engagement. The call with Pritha at Carver became a six-week workflow redesign project starting in March.
Combined, those two re-engagements represent approximately $67,000 in revenue that Maya almost let slip away — not because she had done anything wrong, but because she had not been watching.
Now she is watching.