Recommend Opening Slots with Evidence

PythonBeginner
Practice Now

Introduction

A large visitor total does not by itself identify the best opening slot. A high hourly rate from one recording is also a weak basis for a decision. You will apply an explicit exhibition rule to decide which comparisons have enough recorded evidence, keep every tied leader, and communicate the limits alongside a real chart.

This lab assumes the preceding hourly-observation lab or equivalent knowledge of composite keys, missing recordings, hourly rates and coverage, plus basic Python, pandas and Matplotlib. This environment starts independently with original synthetic data and a complete observe.py; it needs no files from your previous environment. The time is an author estimate, not a measured completion time.

Apply an Explicit Evidence Rule

In this step, you will separate eligibility from ranking. In the WebIDE Tab, open the bottom terminal, or choose Terminal → New Terminal from the top-left menu. Start in the prepared workspace and run the supplied observation analysis. Its two arguments are the input directory and the output directory.

cd /home/labex/project/opening-hours
python3 observe.py data results
cat results/summary.csv

The summary retains every slot. morning totals 40 but has rate 10; afternoon and evening each have rate 12. short has rate 20 from one recorded hour. sparse has three recorded hours but coverage 0.6. quiet is genuinely observed at zero, while unlogged has an unknown rate.

The exhibition's training rule is public: a slot is eligible only with at least three observed hours and coverage at least 0.75. Equality qualifies: three observations out of four planned hours meet both boundaries. Compare eligible slots using visitors per observed hour. Retain all slots within absolute difference 1e-9 of the highest eligible rate. This tiny tolerance handles floating-point representation, not meaningful business differences. If there are no eligible slots, defer the recommendation. These are exercise decision rules, not universal statistical confidence thresholds.

Each slot also keeps a reason. Use this precedence: no_open_hours when planned hours are zero; otherwise too_few_observations when observed hours are below three; otherwise low_coverage below 0.75; otherwise eligible. This distinguishes a closed slot, an unrecorded slot and a slot with enough recordings but insufficient coverage. Never remove an ineligible slot from the comparison table.

set_index('slot') makes each stable slot name the row label. The candidate-rate subtraction followed by .abs().le(1e-9) keeps every rate within the stated tolerance, and sorted gives the decision's slot list a stable display order.

Create recommend.py. As before, > replaces the file and the quoted here-document saves all lines through PY literally. sys.argv reads the summary path and output directory. .ge() means greater than or equal to; the masks classify whole columns. None becomes JSON null when no chosen rate exists. JSON is a structured text format; json.dumps preserves the list of slot identities, so a tie is a list, not whichever row happened to appear first.

cat > recommend.py <<'PY'
import sys
import json
from pathlib import Path
import pandas as pd

source = Path(sys.argv[1])
output = Path(sys.argv[2])
summary = pd.read_csv(source).set_index('slot')
summary['eligible'] = summary['observed_hours'].ge(3) & summary['coverage'].ge(0.75)
summary['reason'] = 'eligible'
summary.loc[summary['coverage'].lt(0.75), 'reason'] = 'low_coverage'
summary.loc[summary['observed_hours'].lt(3), 'reason'] = 'too_few_observations'
summary.loc[summary['planned_hours'].eq(0), 'reason'] = 'no_open_hours'
candidates = summary.loc[summary['eligible']]
best = float(candidates['rate'].max()) if len(candidates) else None
winners = sorted(candidates.index[(candidates['rate'] - best).abs().le(1e-9)].tolist()) if best is not None else []
decision = {'status': 'compare' if winners else 'defer', 'slots': winners, 'rate': best}
output.mkdir(parents=True, exist_ok=True)
summary.to_csv(output / 'comparison.csv')
(output / 'decision.json').write_text(json.dumps(decision, indent=2) + '\n')
print(summary[['observed_hours', 'coverage', 'rate', 'eligible', 'reason']])
print(json.dumps(decision))
PY
python3 recommend.py results/summary.csv results
cat results/decision.json
{
  "status": "compare",
  "slots": [
    "afternoon",
    "evening"
  ],
  "rate": 12.0
}

Both tied slots remain. short is not a candidate despite its high rate; sparse is excluded for low coverage. quiet remains eligible with rate zero: being quiet is not invalid data. If all eligible rates were zero, every eligible slot would tie at zero and could still be compared. If every slot were ineligible, the correct result would be status: defer, an empty slots list and rate: null.

Self-check: open results/comparison.csv in the Explorer and find the reason for every slot. The program contract is python3 recommend.py SUMMARY_CSV OUTPUT_DIRECTORY. It must work with another correctly generated summary, rather than fixed slot names or a fixed winner.

Show the Evidence Beside the Recommendation

In this step, you will make totals and rates visible together without disguising missing observations as zero. Continue in the same WebIDE terminal at /home/labex/project/opening-hours.

The chart contract is deliberately small: two vertical bar panels, slot identities as horizontal tick labels, and vertical labels Total visitors and Visitors per recorded hour. Show observed-hour counts and coverage for every slot, including ineligible ones. A missing total or rate has no numeric bar and is labeled unknown. Colors, fonts, dimensions and slot order may differ. The chart must be deterministic: unchanged input and the current script reproduce the same pixels; PNG metadata may differ. Do not put random styles or the current time into the image.

The loop uses zip to take one axis, one column name and one unit label together. summary.iterrows() visits each slot and its row, and enumerate adds that slot's numeric bar position. The f-string format :.0% displays a fraction as a percentage with no decimal places. pd.notna decides whether coverage exists before formatting it. annotate places text at a data position; xytext=(0, 5) with textcoords='offset points' moves it five display points above that position. ha='center' centers the text, and fontsize=7 keeps the evidence labels compact. None of these display choices changes the calculation.

Append the following block once to recommend.py; >> preserves the code already there. subplots creates two axes. range(len(summary)) provides a bar position for each slot. Use the label format n=N; cov=P% for the observed-hour count and whole-percentage coverage (cov=n/a when undefined). Each label tells the reader how much evidence supports that position. The chart includes the actual rate for an ineligible slot, but its reason and the accompanying decision prevent mistaking it for a recommendation. Missing rates remain missing.

cat >> recommend.py <<'PY'

import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt

fig, axes = plt.subplots(1, 2, figsize=(14, 6))
x = list(range(len(summary)))
for ax, column, label in zip(axes, ['visitors', 'rate'], ['Total visitors', 'Visitors per recorded hour']):
    bars = ax.bar(x, summary[column], color='#26705e')
    ax.set_xticks(x, summary.index, rotation=40, ha='right')
    ax.set_ylabel(label)
    ax.set_ylim(bottom=0)
    ax.grid(axis='y', alpha=0.2)
    for n, (slot, row) in enumerate(summary.iterrows()):
        coverage = f"{row['coverage']:.0%}" if pd.notna(row['coverage']) else 'n/a'
        note = f"n={int(row['observed_hours'])}; cov={coverage}"
        value = row[column]
        if pd.isna(value):
            note = 'unknown\n' + note
        ax.annotate(note, (n, 0 if pd.isna(value) else value), xytext=(0, 5), textcoords='offset points', ha='center', fontsize=7)
fig.suptitle('Opening opportunities: totals and observed-hour rates')
fig.tight_layout(rect=(0, 0.04, 1, 0.93))
fig.savefig(output / 'comparison.png', dpi=150)
plt.close(fig)
choice = ', '.join(winners) if winners else 'none'
advice = f"Decision: {decision['status']}\nSlots: {choice}\n"
advice += 'Evidence: historical description\n'
advice += 'Effect of extending hours: not established\n'
(output / 'advice.txt').write_text(advice)
print(advice)
PY
python3 recommend.py results/summary.csv results

Open results/comparison.png from the WebIDE Explorer. Read the total and rate panels together: morning's higher total does not beat afternoon or evening's rate. short visibly has n=1, and sparse has cov=60%. quiet has a genuine zero, while unlogged and closed have unknown rates. A bar at zero must not stand in for either unknown.

cat results/advice.txt
Decision: compare
Slots: afternoon, evening
Evidence: historical description
Effect of extending hours: not established

The advice uses four public fields: Decision is compare or defer, Slots contains comma-separated stable slot IDs or none, Evidence is historical description, and Effect of extending hours is not established. Field order and slot order may vary; you may add explanation after these fields. These small structured fields keep the advice consistent with the actual decision and state its interpretation without relying on a persuasive paragraph.

The recommendation is limited to the observed historical opportunities and the explicit rule. It cannot show that changing hours will cause the same visitor rate. A useful next action is to collect more missing recordings before deciding about under-recorded slots. advice.txt communicates the result; the hourly relationships, comparison table, decision and actual plotted data provide its supporting evidence.

Keep both scripts, both original input CSVs, hours.csv, summary.csv, comparison.csv, decision.json, the actual PNG and the advice. If a filename is wrong, read the error, run ls to locate it, and rerun the correct path. If you accidentally replace instead of append, recreate step 1's script and append this block once. Rerunning either program recomputes its outputs without changing the inputs.

Self-check: the chart's values come from the current summary, every tie is present, and the limitation accompanies the result. In the challenge, another venue will change slot identities and evidence without changing these analysis rules.

Summary

You separated eligibility from ranking, retained all tied leaders, and gave every excluded slot a reason. A pair of real bar charts shows total visitors beside visitors per recorded hour, with observation counts and coverage visible. Unknown rates remain unknown, and the advice states the causal limit of descriptive historical data.

Your scripts can recompute another venue's results. A correct analysis may defer, retain several ties, or compare a genuinely quiet eligible slot; it need not produce a single exciting winner.