Skip to content

Applied RL cookbook

A practical recipe for taking a real operational decision — "how much to reorder?", "what price?", "admit or shed?" — and turning it into something a decisionrl agent can solve and something you can trust. The emphasis is on the honest part: always measure against the classical baseline, and know when not to use RL.

The recipe in five steps

1. Frame the decision as an MDP

Question What it becomes
What do I observe when I decide? observation (observation_space)
What do I choose? action (Discrete for counts/choices, Box for continuous quantities)
What am I trading off? reward (revenue − costs − penalties, per step)
When does an episode end? horizon (truncation) or a terminal condition

Keep the reward the real objective (profit, cost, service level), not a shaped proxy — the whole point of applied RL is optimising the thing you actually care about. Put any signal the optimal policy needs into the observation (e.g. a recent-demand EWMA if demand drifts), otherwise the problem is needlessly partially observed.

2. Pick the baseline first (the honest part)

Before training anything, decide what "good" means. decisionrl.baselines and decisionrl.solvers ship the classics:

from decisionrl import baselines as B, solvers
from decisionrl.envs import InventoryManagement

# exact optimum where the problem is a small stationary MDP:
opt = solvers.inventory_optimal_value(InventoryManagement)          # value iteration
# strong heuristic baselines elsewhere:
_, best_bs = B.best_base_stock(InventoryManagement)                 # best order-up-to level
_, best_thr = B.best_value_threshold                                # (queues) best admission threshold

Compare against the strongest classical rule you can, not a straw man. Beating "do nothing" proves nothing to a practitioner.

3. Choose the algorithm

Situation Reach for
Discrete action, needs stability DQN (value-based)
Discrete or continuous, robust default PPO
Continuous control, sample-efficient SAC, TD3
Partial observability (history matters) RecurrentPPO
Non-stationary / drifting any of the above + a recent-signal feature in the observation

Value-based methods (DQN) are often steadier than policy-gradient on small discrete operational problems; policy-gradient shines on continuous multi-dimensional actions. (If a method collapses on some seeds, switch families and re-verify — don't ship a fragile number.)

4. Verify honestly — multiple seeds, mean ± std

One seed is a rumour. Train across a few seeds and report the spread, against the baseline from step 2. examples/verify_applied_claims.py is the template:

from decisionrl.training import evaluate_policy
vals = []
for seed in range(3):
    agent = PPO(env_fn(), seed=seed).learn(50_000)
    vals.append(evaluate_policy(agent, env_fn(), n_episodes=20, seed=100)[0])
# report mean(vals) ± std(vals) vs the baseline

Record provenance with decisionrl.tracking.run_manifest (git SHA, versions, seed, config, metrics) so the number is reproducible later.

5. Know when not to use RL

If the problem is stationary and fully observed, a formula or solver is usually better — interpretable and provably optimal. decisionrl is honest about this: on stationary inventory the learned policy only matches the DP optimum. RL earns its place when those assumptions break:

  • non-stationary dynamics (drifting demand, regime switches),
  • partial observability (you don't see the full state),
  • coupled decisions with no closed form (e.g. joint pricing + inventory),
  • dynamics you can't write as a clean LP/DP.

That's the boundary the proof table draws.

Worked example: inventory in ~20 lines

from decisionrl.algorithms import PPO
from decisionrl.envs import InventoryManagement
from decisionrl.training import evaluate_policy
from decisionrl import baselines as B, solvers

env_fn = InventoryManagement
agent = PPO(env_fn(), n_steps=1024, batch_size=64, n_epochs=10, seed=0).learn(40_000)

learned = evaluate_policy(agent, env_fn(), n_episodes=40)[0]
optimum = solvers.inventory_optimal_value(env_fn)     # exact DP optimum
print(f"PPO {learned:.1f}  vs DP optimum {optimum:.1f}")   # ~ matches: RL found the optimum

Checklist

  • [ ] Reward is the real objective, not a proxy.
  • [ ] Observation contains what the optimal policy needs (add a drift/recent signal if non-stationary).
  • [ ] A strong classical baseline is defined before training.
  • [ ] Results are reported as mean ± std over ≥3 seeds.
  • [ ] You can state why RL here (which classical assumption breaks) — or you used the solver instead.