Sentimental Equilibrium
Models one person–app relationship. Feeling fades unless outbound effort is supplied (dx/dt = -r x + a c). The sustaining rate solves x* = (a/r) c* and a U'(x*) = (r + ρ) D'(c*).
Live Dynamic Engagement scoring does not return that rate. score() ranks offers from the response rate: feeling is the take-up, implied effort is (r/a) x, and the rank is U(x) - D(c)/(r+ρ) mixed with a UCB bonus. RollingBehavior writes that map to arm_reward. computeEquilibrium() solves the steady state and score() does not read it.
How often to contact a person is the Workbench batch at /sentimental-equilibrium. A field map turns outbound events into effort and response events into feeling, then writes recommended_engagement_rate and rate_adjustment.
Algorithm
Config value: "approach": "behaviorAlgos", "sub_approach": "sentimentalEquilibrium"
State dynamics (engagement capital \(x\) with effort \(c\)):
\(\frac{dx}{dt} = a \cdot c - r \cdot x\)
Engagement capital accumulates with effort at efficiency \(a\) and decays at rate \(r\).
Equilibrium (steady state with \(x^* = \frac{a}{r} \cdot c^*\)):
\(a \cdot U'(x^*) = (r + \rho) \cdot D'(c^*)\)
Here \(\rho\) is the discount rate (time preference). The equilibrium \((x^*, c^*)\) is found with Newton–Raphson iteration.
Utility functions (configurable via utilityType):
sqrt: \(U(x) = \sqrt{x}\)linear: \(U(x) = x\)log: \(U(x) = \ln(1 + x)\)
Disutility of effort (configurable via disutilityType):
quadratic: \(D(c) = \frac{1}{2}(c - c_\star)^2\). Default \(c_\star = 0\), which is \(\frac{1}{2} c^2\).linear: \(D(c) = c\)
Parameters
- decayRate (\(r\)): Rate at which engagement capital depreciates. Default:
0.1. - effortEfficiency (\(a\)): How effectively effort converts into engagement capital. Default:
1.0. - discountRate (\(\rho\)): Time preference / impatience. Default:
0.05. - utilityType: Shape of \(U\):
"sqrt","linear", or"log". Default:"sqrt". - disutilityType: Shape of \(D\):
"quadratic"or"linear". Default:"quadratic". - Processing Window: Time window in milliseconds for historical data (when used with rolling updates).
- Historical Count: Max records to process per update cycle.
Cold Start
Recommendations are always returned. With no history, RollingBehavior assigns platform random scores. With history, Sentimental Equilibrium ranks each offer from its response rate.
score()does not return \(x^*\) and \(c^*\). Those values come fromcomputeEquilibrium()and are not written toarm_reward.- A live run uses the default \(r\), \(a\), and \(\rho\).
RollingBehaviordoes not copydecayRate,effortEfficiency, ordiscountRatefrom the dynamic-engagement document onto the history row. - Live dynamic ranking:
PostScoreSentimentalEquilibriumcopies options-storearm_reward. It does not recompute the equilibrium. - Workbench Process job on the feature store: use Sentimental Equilibrium PreScored (
PostScoreSentimentalEquilibriumPreScored) — a static extract ofengagementScore/sentimental_equilibrium/ … (same approach as spend personality). Do not use the dynamic post-score for that path.
When To Use
- Ranking offers by a utility of the response rate
- Prescribing how often to contact someone, via the Workbench sentimental equilibrium job
When NOT To Use
- When
arm_rewardis expected to be the engagement rate. That rate is the Workbench batch, not this rank.
Example
from prediction.apis import deployment_management as dm
from prediction.apis import online_learning_management as ol
from prediction import jwt_access
auth = jwt_access.Authenticate("http://localhost:3001/api", ecosystem_username, ecosystem_password)
deployment_id = "demo-sentimental-equilibrium"
online_learning_uuid = ol.create_online_learning(
auth,
algorithm="ecosystem_rewards",
name=deployment_id,
description="Sentimental Equilibrium configuration",
feature_store_collection="set_up_features",
feature_store_database="my_mongo_database",
options_store_database="my_mongo_database",
options_store_collection="demo-deployment_options",
randomisation_processing_count=5000,
randomisation_processing_window=604800000,
contextual_variables_offer_key="offer",
create_options_index=True,
create_covering_index=True
)
online_learning = dm.define_deployment_multi_armed_bandit(epsilon=0, dynamic_interaction_uuid=online_learning_uuid)
parameter_access = dm.define_deployment_parameter_access(
auth,
lookup_key="customer_id",
lookup_type="string",
database="my_mongo_database",
table_collection="customer_feature_store",
datasource="mongodb"
)
deployment_step = dm.create_deployment(
auth,
project_id="demo-project",
deployment_id=deployment_id,
description="Sentimental Equilibrium demo deployment",
version="001",
plugin_post_score_class="PlatformDynamicEngagement.java",
plugin_pre_score_class="PreScoreDynamic.java",
scoring_engine_path_dev="http://localhost:8091",
mongo_connect=f"mongodb://{mongo_user}:{mongo_password}@localhost:54445/?authSource=admin",
parameter_access=parameter_access,
multi_armed_bandit=online_learning
)algorithm="ecosystem_rewards" on create_online_learning is the SDK document type. Scoring uses randomisation: "approach": "behaviorAlgos", "sub_approach": "sentimentalEquilibrium".
setData can read decayRate, effortEfficiency, discountRate, utilityType, and disutilityType from the first history row. A normal Dynamic Engagement call does not put those keys there, so scoring uses the defaults. The Workbench batch stores its own model settings on the sentimental-equilibrium config.