Calibrate RFV frequency scoring
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 2m14s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 2m14s
This commit is contained in:
@@ -4,6 +4,7 @@ const RFM_QUERY_TIMEOUT_MS = 15000;
|
||||
const RECENT_MAX_DAYS = 60;
|
||||
const COOLING_MAX_DAYS = 180;
|
||||
const LOST_MIN_DAYS = 366;
|
||||
const FREQUENCY_MEDIUM_MAX_ORDERS = 4;
|
||||
const SIZE_SUFFIX_SQL_PATTERN = '\\s+-\\s+(?:(?:PP|P|M|G|GG|XG|XGG|EG|EGG|EXG|U|UNICO|ÚNICO|\\d{2})(?:/(?:PP|P|M|G|GG|XG|XGG|EG|EGG|EXG|U|UNICO|ÚNICO|\\d{2}))*)$';
|
||||
const PRODUCT_NAME_SQL = `
|
||||
CASE
|
||||
@@ -133,6 +134,13 @@ const getRecencyScore = (recencyDays) => {
|
||||
return 1;
|
||||
};
|
||||
|
||||
const getFrequencyScore = (frequency) => {
|
||||
const orders = Math.max(0, toNumber(frequency));
|
||||
if (orders <= 1) return 1;
|
||||
if (orders <= FREQUENCY_MEDIUM_MAX_ORDERS) return 2;
|
||||
return 3;
|
||||
};
|
||||
|
||||
const getLifecycleSegment = ({
|
||||
recencyDays,
|
||||
historicalFrequency,
|
||||
@@ -157,8 +165,11 @@ const getLifecycleSegment = ({
|
||||
return { segment: RFM_SEGMENTS_BY_KEY.get('new_customers'), valueScore: 1 };
|
||||
}
|
||||
|
||||
if (days <= RECENT_MAX_DAYS && valueScore === 1) {
|
||||
return { segment: RFM_SEGMENTS_BY_KEY.get('potential_loyalists'), valueScore: 2 };
|
||||
if (days <= RECENT_MAX_DAYS) {
|
||||
const isChampion = frequencyScore === 3 && monetaryScore === 3;
|
||||
return isChampion
|
||||
? { segment: RFM_SEGMENTS_BY_KEY.get('champions'), valueScore: 3 }
|
||||
: { segment: RFM_SEGMENTS_BY_KEY.get('potential_loyalists'), valueScore: 2 };
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -200,7 +211,6 @@ const buildRfmSegments = (clients) => {
|
||||
};
|
||||
|
||||
const buildRfmClients = (baseClients) => {
|
||||
const frequencyScoreFor = buildTertileScorer(baseClients.map(client => client.rfmFrequency ?? client.frequency), true);
|
||||
const monetaryScoreFor = buildTertileScorer(baseClients.map(client => client.rfmMonetary ?? client.monetary), true);
|
||||
|
||||
return baseClients.map(client => {
|
||||
@@ -208,7 +218,7 @@ const buildRfmClients = (baseClients) => {
|
||||
const historicalFrequency = toNumber(client.rfmFrequency ?? client.frequency);
|
||||
const historicalMonetary = toNumber(client.rfmMonetary ?? client.monetary);
|
||||
const recencyScore = getRecencyScore(recencyDays);
|
||||
const frequencyScore = client.rfmFrequencyScore ?? frequencyScoreFor(historicalFrequency);
|
||||
const frequencyScore = client.rfmFrequencyScore ?? getFrequencyScore(historicalFrequency);
|
||||
const monetaryScore = client.rfmMonetaryScore ?? monetaryScoreFor(historicalMonetary);
|
||||
const baseValueScore = Math.min(3, Math.max(1, Math.round((frequencyScore + monetaryScore) / 2)));
|
||||
const classification = getLifecycleSegment({
|
||||
@@ -462,9 +472,9 @@ const getRfmAnalytics = async (range = {}) => {
|
||||
SELECT
|
||||
customer_history.*,
|
||||
CASE
|
||||
WHEN COUNT(*) OVER () = 1 THEN 3
|
||||
WHEN MIN(frequency) OVER () = MAX(frequency) OVER () THEN 2
|
||||
ELSE LEAST(3, GREATEST(1, FLOOR(PERCENT_RANK() OVER (ORDER BY frequency) * 3)::int + 1))
|
||||
WHEN frequency <= 1 THEN 1
|
||||
WHEN frequency <= ${FREQUENCY_MEDIUM_MAX_ORDERS} THEN 2
|
||||
ELSE 3
|
||||
END as frequency_score,
|
||||
CASE
|
||||
WHEN COUNT(*) OVER () = 1 THEN 3
|
||||
@@ -552,6 +562,7 @@ module.exports = {
|
||||
buildDateFilter,
|
||||
buildRfmClients,
|
||||
buildRfmSegments,
|
||||
getFrequencyScore,
|
||||
getPreviousDate,
|
||||
getRecencyScore,
|
||||
getRfmAnalytics,
|
||||
|
||||
Reference in New Issue
Block a user