For information and reference only – not to be submitted as your own work.
This extract shows how a strong answer to the July 2026 ANL252 end-of-course assessment on the payment delay dataset can be structured: tidy pre-processing, three purposeful figures, a linear regression with an explicit equation, and a short critical extension. It follows the tasks in the live ANL252 ECA payment delay question. For the method and the marker’s viewpoint, read the companion guide to approaching the ANL252 ECA. If you want a wider, less dataset-specific example, the earlier ANL252 Python for Data Analytics sample covers general coursework.
Important: we do not have your copy of ECA_PaymentDelay.csv in this extract. Every number, coefficient and metric below is illustrative, produced on a small practice table with the same column names. Your own output will differ, so you must run the code on your file and report what you see.
What this sample covers
The assessment is worth 100 marks, all questions compulsory, with a separate word limit for each answer. Code and references sit outside those limits, but in-text citations and any appendix text count. The map below shows where each task is handled.
| Task | Marks / word limit | Where covered |
|---|---|---|
| Q1 Pre-processing methods | 30 marks / 300 words | Four methods, each with code and a reason |
| Q2 Three figures with insights | 30 marks / 450 words | Histogram, box plot, bar chart |
| Q3 Linear regression approach | 15 marks / 300 words | Feature choice, encoding, split, fitting |
| Q4 Evaluation and equation | 15 marks / 300 words | R-squared, MAE, RMSE, equation, residuals |
| Q5 Further analytics | 10 marks / 300 words | Random forest, cross-validation, importance |
Because the report must also carry a cover page, a GenAI attribution table (tool, prompts, output, how you adapted it) and possibly a viva, the last section of this page explains how to prepare those.
Model-answer extract
Q1. Pre-processing (about 300 words of explanation)
I first inspected the shape, data types and missing values, because every later decision depends on what the data actually contains. Four methods were then applied.
Duplicates and impossible values. Repeated Record_ID rows would count the same invoice twice and overstate how reliable the model is, so duplicates were dropped. Non-positive Amount values cannot be valid invoice values, so they were removed rather than repaired (McKinney, 2022).
Missing values. Missing Amount values were replaced with the median, which is not pulled upwards by the few very large invoices. Missing Region and City codes were given an explicit “Unknown” label, so that no rows are lost and the model can still learn whether missing codes behave differently (pandas development team, 2026).
Skew and extremes. Invoice amounts are normally right-skewed, so Amount was log-transformed and then capped at its 1st and 99th percentiles. This limits the leverage of a handful of extreme invoices on the fitted line.
Feature construction. Rank_of_order_by_customer and No_of_orders_by_customer are likely to be related, since a customer with many orders can reach high ranks, which could cause multicollinearity. I combined them into Order_position, the rank divided by the number of orders, which shows how far through the customer’s history an order sits.
import numpy as np
import pandas as pd
df = pd.read_csv("ECA_PaymentDelay.csv")
print(df.shape)
print(df.isna().sum())
# Method 1: remove duplicate records and impossible values
df = df.drop_duplicates(subset="Record_ID")
df = df[df["Amount"].isna() | (df["Amount"] > 0)].copy()
# Method 2: treat missing values (median for numeric, explicit label for codes)
df["Region"] = df["Region"].astype(str).replace("nan", "Unknown")
df["City"] = df["City"].astype(str).replace("nan", "Unknown")
df["Amount"] = df["Amount"].fillna(df["Amount"].median())
# Method 3: tame the skew in Amount (log transform) and cap extreme values
df["Amount_log"] = np.log1p(df["Amount"])
low, high = df["Amount_log"].quantile([0.01, 0.99])
df["Amount_log"] = df["Amount_log"].clip(low, high)
# Method 4: treat repeat-order information as a ratio of orders placed
df["Order_position"] = df["Rank_of_order_by_customer"] / df["No_of_orders_by_customer"]
print(df.shape)
Illustrative output (practice data, not the ECA file): 3010 rows before cleaning, 2999 after; 60 missing Amount values; 31 missing Region codes.
Q2. Three figures and insights (about 450 words)
The figures were chosen to answer three different questions: what does the target look like, which planned feature separates it, and does timing matter. Axis labels carry units, and each chart is discussed in the text.
Figure 1, histogram of Days_Overdue_Delay. The shape of the target decides whether ordinary least squares is a fair choice. A roughly symmetric distribution centred near a few days would suit it; a long right tail would signal that a few very late payers dominate the squared error (James et al., 2023). In the practice data the histogram is bell-shaped, so no target transformation was needed. State what yours shows, and say what you did about it.
Figure 2, box plot of delay by Payment_Term. Payment terms are set before the invoice is issued, so they are one of the few levers a finance team controls. Compare medians and spread between terms. If longer terms show a higher median but similar spread, say so, and avoid claiming causation from a plot alone.
Figure 3, mean delay by Quarter_clearing. A bar chart shows whether clearing in the final quarter, when companies close their books, is associated with more or less delay. A small difference should be described as small. Markers reward calibrated wording more than dramatic wording.
import matplotlib.pyplot as plt
# Figure 1: distribution of the target
fig, ax = plt.subplots(figsize=(6, 4))
ax.hist(df["Days_Overdue_Delay"], bins=30, edgecolor="black")
ax.set_xlabel("Days_Overdue_Delay (days)")
ax.set_ylabel("Number of invoices")
ax.set_title("Figure 1: Distribution of payment delay")
fig.tight_layout()
fig.savefig("fig1_delay_distribution.png", dpi=150)
# Figure 2: delay by payment term
terms = sorted(df["Payment_Term"].unique())
fig, ax = plt.subplots(figsize=(6, 4))
ax.boxplot([df.loc[df["Payment_Term"] == t, "Days_Overdue_Delay"] for t in terms])
ax.set_xticks(range(1, len(terms) + 1))
ax.set_xticklabels([str(t) for t in terms])
ax.set_xlabel("Payment_Term (days)")
ax.set_ylabel("Days_Overdue_Delay (days)")
ax.set_title("Figure 2: Delay by payment term")
fig.tight_layout()
fig.savefig("fig2_delay_by_term.png", dpi=150)
# Figure 3: average delay by clearing quarter
order = ["Q1", "Q2", "Q3", "Q4"]
means = df.groupby("Quarter_clearing")["Days_Overdue_Delay"].mean()
means = means.reindex([q for q in order if q in means.index])
fig, ax = plt.subplots(figsize=(6, 4))
ax.bar(means.index, means.values, edgecolor="black")
ax.set_xlabel("Quarter_clearing")
ax.set_ylabel("Mean Days_Overdue_Delay (days)")
ax.set_title("Figure 3: Mean delay by clearing quarter")
fig.tight_layout()
fig.savefig("fig3_delay_by_quarter.png", dpi=150)
Illustrative insight wording: “Mean delay differs by less than one day across quarters in the practice data, so seasonality is unlikely to be a strong driver on its own.” Replace this with the finding from your own figure.
Figures are saved as images and pasted into the Word report directly under the code that created them, with a numbered caption (Hunter, 2007).
Mid-sample note: if you would like a specialist to review your own code and word counts before submission, the options are on our page for programming assignment help in Singapore.
Hire a Professional Essay & Assignment Writer for completing your Academic Assessments
Native Singapore Writers Team
- 100% Plagiarism-Free Essay
- Highest Satisfaction Rate
- Free Revision
- On-Time Delivery
Q3. Linear regression approach (about 300 words)
Days_Overdue_Delay is the target, because it measures how late payment is in days. Record_ID was excluded as an identifier with no meaning, and Rank_of_order_by_customer was replaced by Order_position from Q1.
Numeric predictors (log Amount, customer age in months, payment term, number of orders, order position) were kept as they are, so that coefficients stay interpretable in the original units. Categorical predictors (Region, City, the two weekday columns and the quarter) were one-hot encoded, dropping one level each to avoid the dummy-variable trap (scikit-learn developers, 2026a).
The data were split 80/20 with a fixed random seed, so that the model is judged on invoices it has not seen. All transformations sit inside a single pipeline, which means the encoder learns its categories from the training rows only and cannot leak information from the test rows. Ordinary least squares was fitted with scikit-learn (Pedregosa et al., 2011; scikit-learn developers, 2026b). It minimises the sum of squared residuals, is easy to explain, and gives a baseline against which Q5 can be compared.
Assumptions worth stating briefly: a linear relationship, no severe multicollinearity, and residuals without obvious structure. Q4 checks the last of these.
from sklearn.compose import ColumnTransformer
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import train_test_split
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder
target = "Days_Overdue_Delay"
numeric = ["Amount_log", "Age_Of_Customer_Months", "Payment_Term",
"No_of_orders_by_customer", "Order_position"]
categorical = ["Region", "City", "Weekday_clearing", "Weekday_due", "Quarter_clearing"]
X = df[numeric + categorical]
y = df[target]
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42)
prep = ColumnTransformer(
[("cat", OneHotEncoder(drop="first", handle_unknown="ignore"), categorical)],
remainder="passthrough")
model = Pipeline([("prep", prep), ("ols", LinearRegression())])
model.fit(X_train, y_train)
Q4. Evaluation, equation and insights (about 300 words)
Evaluation uses held-out data and three measures. R-squared gives the share of variance explained; MAE is the average error in days and is easiest for a finance reader to interpret; RMSE penalises large misses more heavily.
import numpy as np
from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score
pred = model.predict(X_test)
print("R2 :", round(r2_score(y_test, pred), 3))
print("MAE :", round(mean_absolute_error(y_test, pred), 2))
print("RMSE:", round(np.sqrt(mean_squared_error(y_test, pred)), 2))
names = model.named_steps["prep"].get_feature_names_out()
coefs = model.named_steps["ols"].coef_
table = pd.Series(coefs, index=names).sort_values(key=abs, ascending=False)
print(table.head(8))
print("Intercept:", round(model.named_steps["ols"].intercept_, 3))
# Regression equation written out for the report (largest terms only)
terms = " ".join(f"{c:+.3f}*[{n}]" for n, c in table.head(5).items())
print(f"Days_Overdue_Delay = {model.named_steps['ols'].intercept_:.3f} {terms} + ...")
# Residual plot
fig, ax = plt.subplots(figsize=(6, 4))
ax.scatter(pred, y_test - pred, s=8, alpha=0.5)
ax.axhline(0, color="black", linewidth=1)
ax.set_xlabel("Predicted delay (days)")
ax.set_ylabel("Residual (days)")
fig.tight_layout()
fig.savefig("fig4_residuals.png", dpi=150)
Illustrative output (practice data): R-squared 0.21, MAE 5.1 days, RMSE 6.7 days. Largest coefficient: log Amount at about +3.6, with intercept about -17.3.
The fitted equation is written in the report with the real names and numbers, for example: Days_Overdue_Delay = intercept + 3.6 × Amount_log − 0.8 × Order_position + (dummy terms) + … . Each coefficient is read as the expected change in delay, in days, for a one-unit rise in that variable with the others held constant. For a dummy variable, it is the difference from the dropped reference level.
In the practice data, a model explaining roughly a fifth of the variance would be described honestly as a weak-to-moderate predictor: useful for ranking risk, poor for forecasting an individual invoice to the day. The residual plot is then checked for curvature or a funnel shape. Structure there would justify the non-linear approach in Q5.
Q5. Further analytics (about 300 words)
Two extensions are reasonable under the stated assumption that the data are a fair sample of invoices.
A non-linear model. A random forest can capture interactions, such as large invoices behaving differently for new customers, that a linear equation cannot. Five-fold cross-validation gives a fairer comparison than one split, because every row is used for testing once.
Interpretation of drivers. Permutation importance measures how much the score falls when one feature is shuffled. It is model-agnostic and works on the forest, which has no coefficients.
Further options: a classification model for “paid late versus on time” if finance staff care more about a threshold than the exact days, and clustering of customers by order history. Say which you would try first and why, and mention one limit, such as the lack of customer industry or invoice dispute data in the file.
from sklearn.ensemble import RandomForestRegressor
from sklearn.inspection import permutation_importance
from sklearn.model_selection import cross_val_score
rf = Pipeline([("prep", ColumnTransformer(
[("cat", OneHotEncoder(handle_unknown="ignore"), categorical)],
remainder="passthrough")),
("rf", RandomForestRegressor(n_estimators=200, min_samples_leaf=5,
random_state=42, n_jobs=-1))])
for label, est in [("Linear regression", model), ("Random forest", rf)]:
scores = cross_val_score(est, X, y, cv=5, scoring="r2")
print(label, "mean CV R2:", scores.mean().round(3))
rf.fit(X_train, y_train)
imp = permutation_importance(rf, X_test, y_test, n_repeats=5, random_state=42)
print(pd.Series(imp.importances_mean, index=X_test.columns).sort_values(ascending=False))
Illustrative output (practice data): mean cross-validated R-squared 0.21 for linear regression and 0.31 for the random forest, with Amount_log the dominant feature. Do not copy these; run your own.
How to adapt this to your own submission
- Run every block on your file, then replace each illustrative number with your own output.
- Keep explanations inside each word limit; count in-text citations and appendix text, not code or references.
- Place the code and its output directly under the question it answers, in a monospaced font, as the brief requires.
- Complete the GenAI attribution table honestly: tool, prompts, output, and what you changed. Be ready to explain every line in a viva.
- Check your assessment brief for the correct cover page and submission steps through Canvas.
For help structuring a report like this, see our ECA assignment help page. We provide model answers and guidance; how you use them, and your institution’s rules, remain your responsibility.
About the expert
This topic sits closest to the work of Eric Koh, whose profile on our site lists an MS in Computer Science from the National University of Singapore, with subject areas including data structures, algorithms, operating systems and computer networks. View Eric Koh’s expert profile.
References
Hunter, J. D. (2007). Matplotlib: A 2D graphics environment. Computing in Science & Engineering, 9(3), 90–95. https://doi.org/10.1109/MCSE.2007.55
James, G., Witten, D., Hastie, T., Tibshirani, R., & Taylor, J. (2023). An introduction to statistical learning: With applications in Python. Springer.
McKinney, W. (2022). Python for data analysis (3rd ed.). O’Reilly Media.
pandas development team. (2026). Working with missing data. https://pandas.pydata.org/docs/user_guide/missing_data.html
Pedregosa, F., Varoquaux, G., Gramfort, A., Michel, V., Thirion, B., Grisel, O., Blondel, M., Prettenhofer, P., Weiss, R., Dubourg, V., Vanderplas, J., Passos, A., Cournapeau, D., Brucher, M., Perrot, M., & Duchesnay, É. (2011). Scikit-learn: Machine learning in Python. Journal of Machine Learning Research, 12, 2825–2830. https://jmlr.org/papers/v12/pedregosa11a.html
scikit-learn developers. (2026a). OneHotEncoder. https://scikit-learn.org/stable/modules/generated/sklearn.preprocessing.OneHotEncoder.html
scikit-learn developers. (2026b). Linear models: Ordinary least squares. https://scikit-learn.org/stable/modules/linear_model.html