92 lines
4.0 KiB
C#
92 lines
4.0 KiB
C#
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 (18–34)",
|
||
"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
|
||
};
|
||
}
|
||
}
|