Add "what changed this month" to Follower Study, ahead of the cumulative picture

Feedback: showing "102 followers to date" as the headline of a monthly
report answers the wrong question — a reader wants to know what happened
in the month the report covers, with the cumulative numbers as supporting
context, not the other way around. Audience Composition already did this
correctly (month-scoped "22 new followers" by design); Follower Study now
leads the same way: which new Notable Contacts joined this specific month,
and which Account Clusters grew, computed live for whatever month is
selected. The existing cumulative "bimodal seniority"/"shallow-network"
findings stay, just demoted to a clearly-labeled "full picture, to date"
section beneath.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
beantownbabeandClaude Sonnet 5 committed 2026-08-31 10:48:50 +02:00
1 parent d4fff48384
commit 5e20823947
1 file changed
+52 -12
+52 -12
View File
@@ -3974,12 +3974,19 @@ _SENIOR_TIERS = ("C-Level/Founder", "Director/Head/Principal", "Senior IC/Lead")
_JUNIOR_TIERS = ("Student/Trainee",)
def _follower_study_findings(whole, non_team, period_label):
"""Two dynamically-computed findings in the same spirit as assets/follower_study.html's
own hand-written ones ("bimodal seniority," "shallow-network audience") — but
computed live from whatever's in the Directory through the selected month, not
hardcoded to July's specific numbers, so the finding stays honest for any month
instead of silently describing a snapshot that's since moved on."""
def _follower_study_findings(whole, non_team, period_label, month_rows=None, clusters=None):
"""Two dynamically-computed cumulative findings in the same spirit as
assets/follower_study.html's own hand-written ones ("bimodal seniority,"
"shallow-network audience") — but computed live from whatever's in the
Directory through the selected month, not hardcoded to July's specific
numbers, so the finding stays honest for any month instead of silently
describing a snapshot that's since moved on.
Plus, ahead of those two, what actually happened THIS month specifically —
a monthly report showing "112 followers to date" as its headline is
answering the wrong question; the reader wants to know what changed in the
period this report covers, with the cumulative picture as supporting
context, not the other way around."""
total = whole["total"]
if not total:
return html.Div()
@@ -3987,8 +3994,31 @@ def _follower_study_findings(whole, non_team, period_label):
junior_n = sum(whole["seniority"].get(t, 0) for t in _JUNIOR_TIERS)
first_degree_n = sum(1 for d in non_team if d.get("connection") == "1st")
bullets = [
f"{senior_n} of {total} ({senior_n / total * 100:.0f}%) sit at C-Level/Founder, "
this_month_bullets = []
if month_rows:
new_notable = [d for d in month_rows if d.get("seniority") in _NOTABLE_SENIORITIES]
if new_notable:
names = ", ".join(f"{d.get('name', '')} ({d.get('company', '') or d.get('seniority', '')})"
for d in new_notable)
this_month_bullets.append(
f"{len(new_notable)} new notable contact{'s' if len(new_notable) != 1 else ''} "
f"joined this month: {names}.")
if clusters:
from collections import Counter
month_companies = Counter(d.get("company", "").strip() for d in month_rows
if d.get("company", "").strip())
grown = [f"{c} ({total_n} total, {month_companies[c]} new this month)"
for c, total_n in clusters if month_companies.get(c)]
if grown:
this_month_bullets.append("Account clusters that grew this month: "
+ "; ".join(grown) + ".")
this_month_bullets = [html.Li(html.Strong(b), style={"marginBottom": "4px"}) for b in this_month_bullets]
if not this_month_bullets:
this_month_bullets = [html.Li(html.Em("No new notable contacts or growing account "
"clusters this month."), style={"marginBottom": "4px"})]
overall_bullets = [
f"Overall, {senior_n} of {total} ({senior_n / total * 100:.0f}%) sit at C-Level/Founder, "
f"Director/Head/Principal, or Senior IC/Lead level, while {junior_n} "
f"({junior_n / total * 100:.0f}%) are students or trainees — both ends of the "
f"seniority range are represented, not just the middle.",
@@ -3997,9 +4027,18 @@ def _follower_study_findings(whole, non_team, period_label):
f"algorithmic surfacing rather than a direct personal tie.",
]
return dbc.Card([
dbc.CardHeader(f"What {period_label}'s Followers Tell Us"),
dbc.CardBody(html.Ul([html.Li(b, style={"marginBottom": "4px"}) for b in bullets],
style={"fontSize": "0.85rem", "paddingLeft": "1.1rem", "marginBottom": "0"})),
dbc.CardHeader(f"What Changed This Month vs. The Full Picture"),
dbc.CardBody([
html.Div(f"New this month ({period_label}):", style={"fontWeight": "700", "fontSize": "0.82rem",
"marginBottom": "4px"}),
html.Ul(this_month_bullets, style={"fontSize": "0.85rem", "paddingLeft": "1.1rem",
"marginBottom": "12px"}),
html.Div("The full picture, to date:", style={"fontWeight": "700", "fontSize": "0.82rem",
"color": "#6b7a8d", "marginBottom": "4px"}),
html.Ul([html.Li(b, style={"marginBottom": "4px"}) for b in overall_bullets],
style={"fontSize": "0.85rem", "paddingLeft": "1.1rem", "marginBottom": "0",
"color": "#6b7a8d"}),
]),
], className="mb-3 shadow-sm")
@@ -4022,6 +4061,7 @@ def _follower_study_insights_page(month_value="2026-07"):
if not d.get("followedSort") or d.get("followedSort") <= month_value]
non_team = [d for d in directory_as_of_month if not d.get("is_team")]
whole = follower_directory_data.composition_stats(non_team)
month_rows = [d for d in non_team if d.get("followedSort") == month_value]
from collections import Counter
companies = Counter(d.get("company", "").strip() for d in non_team if d.get("company", "").strip())
@@ -4044,7 +4084,7 @@ def _follower_study_insights_page(month_value="2026-07"):
], bordered=True, size="sm")
return dbc.Container([
_follower_study_findings(whole, non_team, period_label),
_follower_study_findings(whole, non_team, period_label, month_rows, clusters),
dbc.Card([
dbc.CardHeader(f"All {whole['total']} Followers to Date (through {period_label}), by "
f"Sector, Function, Seniority and Country"),