Files
AdPlatform-Server/IntelligenceApi/Engines/DemographicsAnalyzer.cs
2026-03-14 13:50:09 -07:00

92 lines
4.0 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using IntelligenceApi.Models;
namespace IntelligenceApi.Engines;
/// <summary>
/// Derives audience recommendations from raw census data for a ZCTA.
///
/// This logic was previously embedded in the Gateway's DemographicsController
/// as BuildMarketAnalysis(). It belongs here — IntelligenceApi owns all
/// recommendation and analysis logic; the Gateway is a thin proxy.
///
/// Registered as a singleton: stateless, no IO.
/// </summary>
public sealed class DemographicsAnalyzer
{
public DemographicAnalysisResponse Analyze(DemographicAnalysisRequest request)
{
var c = request.Census;
var zcta = request.Zcta;
// ── Age skew ──────────────────────────────────────────────────────────
var youngPct = c.Pct18to24 + c.Pct25to34;
var maturePct = c.Pct55to64 + c.Pct65plus;
string ageSkew;
if (youngPct > maturePct + 10) ageSkew = "young";
else if (maturePct > youngPct + 10) ageSkew = "mature";
else ageSkew = "balanced";
// ── Recommended age chips ─────────────────────────────────────────────
// Include brackets with meaningful population share.
var ageRanges = new List<string>();
if (c.Pct18to24 >= 10) ageRanges.Add("AGE_18_24");
if (c.Pct25to34 >= 12) ageRanges.Add("AGE_25_34");
if (c.Pct35to44 >= 12) ageRanges.Add("AGE_35_44");
if (c.Pct45to54 >= 12) ageRanges.Add("AGE_45_54");
if (c.Pct55to64 >= 10) ageRanges.Add("AGE_55_64");
if (c.Pct65plus >= 12) ageRanges.Add("AGE_65_UP");
// Fallback: if no bracket clears the threshold, default to prime brackets
if (ageRanges.Count == 0)
{
ageRanges.Add("AGE_25_34");
ageRanges.Add("AGE_35_44");
}
// ── Recommended income chips ──────────────────────────────────────────
var incomes = c.MedianIncome switch
{
> 100_000 => new List<string> { "TOP_10", "TOP_11_20" },
> 75_000 => new List<string> { "TOP_11_20", "TOP_21_30" },
> 50_000 => new List<string> { "TOP_21_30", "TOP_31_40" },
_ => new List<string> { "TOP_41_50", "LOWER_50" }
};
// ── Human-readable insights ───────────────────────────────────────────
var insights = new List<string>();
if (c.TotalPopulation > 0) insights.Add($"{c.TotalPopulation:N0} people");
if (c.MedianIncome > 0) insights.Add($"Median income ${c.MedianIncome:N0}");
if (c.PctBachelorPlus > 0) insights.Add($"{c.PctBachelorPlus}% college-educated");
if (c.PctOwnerOccupied > 55) insights.Add($"{c.PctOwnerOccupied}% homeowners");
else if (c.PctRenterOccupied > 55) insights.Add($"{c.PctRenterOccupied}% renters");
if (c.PctFamilyHouseholds > 60) insights.Add($"{c.PctFamilyHouseholds}% families");
else if (c.PctLivingAlone > 35) insights.Add($"{c.PctLivingAlone}% single-person households");
insights.Add(ageSkew switch
{
"young" => "Skews younger (1834)",
"mature" => "Skews older (55+)",
_ => "Balanced age distribution"
});
return new DemographicAnalysisResponse
{
Ok = true,
Zcta = zcta,
Census = c,
Recommendations = new AudienceRecommendations
{
AgeRanges = ageRanges,
Incomes = incomes,
AgeSkew = ageSkew,
MarketScope = "local" // single ZIP is always local scope
},
Insights = insights
};
}
}