# --------------------------------------------------------------------------- # sales_analysis.py # # Reads one month of counter sales from sales.csv, cleans the entries the shop # typed by hand, and answers the four questions the shopkeeper actually asked: # # 1. Which days of the month are busy, and which are dead? # 2. Which part of the shop earns the money? # 3. Which items are worth keeping in stock, and which are not? # 4. Is this a shop of small bills or big ones? # 5. Which day of the week should the shop be fully stocked for? # # Each answer is printed as a table and drawn as a chart. # --------------------------------------------------------------------------- import pandas as pd import matplotlib.pyplot as plt # --- 1. Read ------------------------------------------------------------- # parse_dates turns the date column from text into real dates. Without it the # month-wise sorting later would be alphabetical, so 2026-06-10 would come # before 2026-06-2. df = pd.read_csv("sales.csv", parse_dates=["date"]) print("Rows read from the file :", len(df)) print("Columns :", list(df.columns)) print() # --- 2. Clean ------------------------------------------------------------ # The shop types this file itself, so it arrives with the mistakes anybody # makes: the same line entered twice, a price column left empty, and the # category spelt three different ways. before = len(df) df = df.drop_duplicates() # the same bill line keyed in twice print("Duplicate rows removed :", before - len(df)) # str.strip() removes the stray spaces, str.title() makes "snacks", # "SNACKS" and " Snacks " into one category. df["category"] = df["category"].str.strip().str.title() # A row with no price cannot be turned into money, so it is dropped rather # than guessed at. How many were dropped is printed, because a silent drop # hides a problem with the shop's record-keeping. missing = df["price"].isnull().sum() df = df.dropna(subset=["price"]) print("Rows with no price :", missing, "(dropped)") print("Rows left for analysis :", len(df)) print() # --- 3. Derive ----------------------------------------------------------- # What each line of a bill was actually worth. Every figure below is built # from this one column. df["amount"] = df["qty"] * df["price"] print("Total sales for the month : Rs", round(df["amount"].sum(), 2)) print("Number of bills :", df["bill_no"].nunique()) print("Average value of a bill : Rs", round(df.groupby("bill_no")["amount"].sum().mean(), 2)) print() # --- 4. Question 1: which days are busy? --------------------------------- # groupby() puts all the rows of one date together; sum() adds their amounts. daily = df.groupby("date")["amount"].sum() print("--- Busiest five days ---") print(daily.sort_values(ascending=False).head(5).round(2)) print() plt.figure(figsize=(9, 4)) plt.plot(daily.index, daily.values, marker="o", color="#e07b39") plt.title("Day-wise sales, June 2026") plt.xlabel("Date") plt.ylabel("Sales (Rs)") plt.xticks(rotation=45) plt.grid(True, linestyle="--", alpha=0.5) plt.tight_layout() plt.savefig("chart1_daily_sales.png") plt.close() # --- 5. Question 2: which part of the shop earns? ------------------------ by_cat = df.groupby("category")["amount"].sum().sort_values(ascending=False) print("--- Sales by category ---") print(by_cat.round(2)) print() # The share matters more than the rupees: it is what decides shelf space. print("Share of the month's takings (%):") print((by_cat / by_cat.sum() * 100).round(1)) print() plt.figure(figsize=(8, 4.5)) plt.bar(by_cat.index, by_cat.values, color="#4c9f70") plt.title("Sales by category, June 2026") plt.xlabel("Category") plt.ylabel("Sales (Rs)") plt.xticks(rotation=20) plt.tight_layout() plt.savefig("chart2_category.png") plt.close() # --- 6. Question 3: which items are worth stocking? ---------------------- by_item = df.groupby("item")["amount"].sum().sort_values(ascending=False) print("--- Top ten items by value ---") print(by_item.head(10).round(2)) print() print("--- Five slowest items ---") print(by_item.tail(5).round(2)) print() # barh() draws the bars sideways, so the long item names stay readable. # [::-1] flips the order, putting the biggest bar at the top. top10 = by_item.head(10)[::-1] plt.figure(figsize=(8, 5)) plt.barh(top10.index, top10.values, color="#3b7dd8") plt.title("Top ten items by value, June 2026") plt.xlabel("Sales (Rs)") plt.tight_layout() plt.savefig("chart3_top_items.png") plt.close() # --- 7. Question 4: small bills or big ones? ----------------------------- # One row per bill, not per line, so a three-item bill counts once. bills = df.groupby("bill_no")["amount"].sum() print("--- Bill values ---") print(bills.describe().round(2)) print() plt.figure(figsize=(8, 4.5)) plt.hist(bills.values, bins=10, color="#a05fc0", edgecolor="white") plt.title("How big is a bill? June 2026") plt.xlabel("Value of a bill (Rs)") plt.ylabel("Number of bills") plt.tight_layout() plt.savefig("chart4_bill_sizes.png") plt.close() # --- 8. Question 5: which day of the week is worth staffing? ------------- # .dt.day_name() reads the weekday out of a real date. This only works # because parse_dates was used at step 1 — on plain text it would fail. df["weekday"] = df["date"].dt.day_name() # Adding the takings by weekday is not enough on its own: a month does not # hold the same number of Mondays as Sundays. Dividing by how many of each # day the month actually had gives a figure that can be compared. order = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"] by_day = df.groupby("weekday")["amount"].sum().reindex(order) how_many = df.groupby("weekday")["date"].nunique().reindex(order) avg_day = (by_day / how_many).round(2) print("--- Average takings by day of the week ---") print(avg_day) print() print("Busiest day :", avg_day.idxmax(), "at Rs", avg_day.max()) print("Quietest day:", avg_day.idxmin(), "at Rs", avg_day.min()) print() plt.figure(figsize=(8, 4.5)) plt.bar(avg_day.index, avg_day.values, color="#c9772f") plt.title("Average takings by day of the week") plt.xlabel("Day") plt.ylabel("Average sales (Rs)") plt.xticks(rotation=20) plt.tight_layout() plt.savefig("chart5_weekday.png") plt.close() # --- 9. Write the summary the shopkeeper keeps --------------------------- # The charts are for the report; this file is what is actually handed over. summary = pd.DataFrame({ "sales": by_cat.round(2), "share_percent": (by_cat / by_cat.sum() * 100).round(1), }) summary.to_csv("category_summary.csv") print("Charts saved : chart1_daily_sales.png .. chart5_weekday.png") print("Summary saved: category_summary.csv")