Unit 1 Mini-ProjectΒΆ

  • Class: 1:55
  • Analyst: Theo Brito Machado, Olivia Smith
  • Company: Ralph Lauren (RL)

1) Importing data from the CSVΒΆ

Here we are importing the data from the CSV we created on the first notebook, path is in the root folder named data. For code quality purpose I am also importing all the libraries and function on the first line of the notebook

# As mentioned, keeping the good practise of doing all the importations on top of the code
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from pathlib import Path
from IPython.display import display

pd.set_option("display.max_columns", 200)
pd.set_option("display.width", 180)
pd.set_option("display.float_format", lambda x: f"{x:,.2f}")

# same we will always use to find root 
def find_project_root(start: Path | None = None) -> Path:
    start_path = (start or Path.cwd()).resolve()
    for candidate in [start_path, *start_path.parents]:
        if (candidate / "DATA" / "csv_export").exists() and (candidate / "Mini-projects").exists():
            return candidate
    raise FileNotFoundError("Could not find the project root containing DATA/csv_export and Mini-projects.")

# Function for reading the csv
def load_source_csv(csv_dir: Path, filename: str) -> pd.DataFrame:
    path = csv_dir / filename
    header = pd.read_csv(path, nrows=0)
    parse_dates = [c for c in ["date", "acceptedDate", "filingDate"] if c in header.columns]
    return pd.read_csv(path, parse_dates=parse_dates)

# use the root function + actual data directions
PROJECT_ROOT = find_project_root()
CSV_DIR = PROJECT_ROOT / "DATA" / "csv_export"
OUT_DIR = PROJECT_ROOT / "Mini-projects" / "outputs"
QUARTERLY_EXPORT_DIR = CSV_DIR

# Loading csv
is_raw = load_source_csv(CSV_DIR, "RL_income_statement.csv")
bs_raw = load_source_csv(CSV_DIR, "RL_balance_sheet.csv")
cf_raw = load_source_csv(CSV_DIR, "RL_cash_flow.csv")
ratios_raw = load_source_csv(CSV_DIR, "RL_ratios.csv")

# Printing the shape information, since we have 40 quarters of data should always be (40,n)
print("Project root:", PROJECT_ROOT)
print("Loaded tables:")
print("  Income statement:", is_raw.shape)
print("  Balance sheet   :", bs_raw.shape)
print("  Cash flow       :", cf_raw.shape)
print("  Ratios          :", ratios_raw.shape)
Project root: /Users/theomachado/Library/CloudStorage/OneDrive-UniversityofFlorida/Spring 2026/FIN4453 - OFC
Loaded tables:
  Income statement: (40, 39)
  Balance sheet   : (40, 61)
  Cash flow       : (40, 47)
  Ratios          : (40, 64)

2) Build a TidyΒΆ

  • Timeline: fiscal quarters, with each Q1-Q4.
  • Units: USD MM
  • Audit checks:
    • one source row per fiscal quarter for IS, BS, CF, and ratios
    • Total Assets = Total Liabilities + Total Equity
    • Gross Profit = Revenue - COGS
  • Professor note: Dr. Mascio, as mentioned in the executive summary RL report a bit different, they have a time difference. Example: 2021Q1 is the quarter ended actually in june 2020.
#Facilitating the order of the quarters process
PERIOD_ORDER = {"Q1": 1, "Q2": 2, "Q3": 3, "Q4": 4}


def make_fiscal_label(fiscal_year: int | float, period: str) -> str:
    return f"FY{int(fiscal_year)} {period}"

#preping the statments
def _prep(df: pd.DataFrame) -> pd.DataFrame:
    out = df.copy()
    for col in ["date", "acceptedDate", "filingDate"]:
        if col in out.columns:
            out[col] = pd.to_datetime(out[col], errors="coerce")
    out["fiscalYear"] = pd.to_numeric(out["fiscalYear"], errors="coerce")
    out = out.dropna(subset=["fiscalYear", "period"]).copy()
    out["fiscalYear"] = out["fiscalYear"].astype(int)
    out["period"] = out["period"].astype(str).str.upper().str.strip()
    out = out[out["period"].isin(PERIOD_ORDER)].copy()
    out["QuarterNumber"] = out["period"].map(PERIOD_ORDER).astype(int)
    sort_cols = [c for c in ["date", "acceptedDate", "filingDate", "fiscalYear", "QuarterNumber"] if c in out.columns]
    return out.sort_values(sort_cols).reset_index(drop=True)

# For purpose of cleaning the data and avoiding mechanical mistakes we clean and remove all the duplicated rows
def _latest_unique_rows(df: pd.DataFrame, keys: list[str]) -> pd.DataFrame:
    sort_cols = [c for c in [*keys, "acceptedDate", "filingDate", "date"] if c in df.columns]
    ordered = df.sort_values(sort_cols) if sort_cols else df.copy()
    return ordered.drop_duplicates(subset=keys, keep="last").copy()


def _quarter_record_counts(df: pd.DataFrame) -> pd.Series:
    counts = df.groupby(["fiscalYear", "period"]).size()
    counts.index = [make_fiscal_label(year, period) for year, period in counts.index]
    return counts.sort_index()

# Creating a pd dataframe for each statment and making the df colum names to later bring the raw data in
def build_tidy_pack(
    is_df: pd.DataFrame,
    bs_df: pd.DataFrame,
    cf_df: pd.DataFrame,
    ratios_df: pd.DataFrame,
):
    merge_keys = ["date", "fiscalYear", "period"]

    is_p = _latest_unique_rows(_prep(is_df), merge_keys)
    bs_p = _latest_unique_rows(_prep(bs_df), merge_keys)
    cf_p = _latest_unique_rows(_prep(cf_df), merge_keys)
    r_p = _latest_unique_rows(_prep(ratios_df), merge_keys)

    income_cols = [
        "date", "fiscalYear", "period",
        "revenue", "costOfRevenue", "grossProfit", "operatingExpenses", "operatingIncome",
        "ebit", "ebitda", "depreciationAndAmortization", "interestExpense",
        "incomeBeforeTax", "incomeTaxExpense", "netIncome",
    ]
    balance_cols = [
        "date", "fiscalYear", "period",
        "cashAndCashEquivalents", "cashAndShortTermInvestments",
        "accountsReceivables", "inventory", "accountPayables", "accruedExpenses",
        "deferredRevenue", "otherCurrentAssets", "otherCurrentLiabilities",
        "totalCurrentAssets", "totalCurrentLiabilities",
        "propertyPlantEquipmentNet", "totalAssets", "totalLiabilities", "totalEquity",
        "totalStockholdersEquity", "totalDebt", "netDebt",
    ]
    cash_flow_cols = [
        "date", "fiscalYear", "period",
        "operatingCashFlow", "capitalExpenditure", "freeCashFlow", "changeInWorkingCapital",
        "deferredIncomeTax", "stockBasedCompensation", "commonDividendsPaid", "incomeTaxesPaid",
    ]
    ratio_cols = [
        "date", "fiscalYear", "period",
        "priceToEarningsRatio", "priceToSalesRatio", "priceToBookRatio", "enterpriseValueMultiple", "debtToMarketCap",
    ]

    income = is_p[[c for c in income_cols if c in is_p.columns]].rename(columns={
        "revenue": "Revenue",
        "costOfRevenue": "COGS",
        "grossProfit": "GrossProfit",
        "operatingExpenses": "OperatingExpenses",
        "operatingIncome": "OperatingIncome",
        "ebit": "EBIT",
        "ebitda": "EBITDA",
        "depreciationAndAmortization": "DA",
        "interestExpense": "InterestExpense",
        "incomeBeforeTax": "EBT",
        "incomeTaxExpense": "TaxExpense",
        "netIncome": "NetIncome",
    })

    balance = bs_p[[c for c in balance_cols if c in bs_p.columns]].rename(columns={
        "cashAndCashEquivalents": "Cash",
        "cashAndShortTermInvestments": "CashAndSTInvestments",
        "accountsReceivables": "AccountsReceivable",
        "inventory": "Inventory",
        "accountPayables": "AccountsPayable",
        "accruedExpenses": "AccruedExpenses",
        "deferredRevenue": "DeferredRevenue",
        "otherCurrentAssets": "OtherCurrentAssets",
        "otherCurrentLiabilities": "OtherCurrentLiabilities",
        "totalCurrentAssets": "TotalCurrentAssets",
        "totalCurrentLiabilities": "TotalCurrentLiabilities",
        "propertyPlantEquipmentNet": "PPENet",
        "totalAssets": "TotalAssets",
        "totalLiabilities": "TotalLiabilities",
        "totalEquity": "TotalEquity",
        "totalStockholdersEquity": "TotalStockholdersEquity",
        "totalDebt": "TotalDebt",
        "netDebt": "NetDebt",
    })

    cash_flow = cf_p[[c for c in cash_flow_cols if c in cf_p.columns]].rename(columns={
        "operatingCashFlow": "OperatingCashFlow",
        "capitalExpenditure": "CapexRaw",
        "freeCashFlow": "FreeCashFlow",
        "changeInWorkingCapital": "DeltaWC_CFSign",
        "deferredIncomeTax": "DeferredTax",
        "stockBasedCompensation": "SBC",
        "commonDividendsPaid": "DividendsRaw",
        "incomeTaxesPaid": "CashTaxesPaid",
    })

    ratios = r_p[[c for c in ratio_cols if c in r_p.columns]].rename(columns={
        "priceToEarningsRatio": "PE",
        "priceToSalesRatio": "PS",
        "priceToBookRatio": "PB",
        "enterpriseValueMultiple": "EV_EBITDA",
        "debtToMarketCap": "DebtToMarketCap",
    })

    tidy = (
        income.merge(balance, on=merge_keys, how="inner", validate="1:1")
        .merge(cash_flow, on=merge_keys, how="inner", validate="1:1")
        .merge(ratios, on=merge_keys, how="left", validate="1:1")
    )

    # we are creating cleaner time colums making sure the raw data will align perfectly
    tidy["Date"] = pd.to_datetime(tidy["date"])
    tidy["FiscalYear"] = tidy["fiscalYear"].astype(int)
    tidy["Period"] = tidy["period"].astype(str)
    tidy["QuarterNumber"] = tidy["Period"].map(PERIOD_ORDER).astype(int)
    tidy["Quarter"] = [make_fiscal_label(year, period) for year, period in zip(tidy["FiscalYear"], tidy["Period"])]

    # correct indexing the tidy df by quarters
    tidy = tidy.sort_values(["Date", "FiscalYear", "QuarterNumber"]).set_index("Quarter")
    tidy.index.name = "Quarter"
    tidy = tidy.drop(columns=[c for c in ["date", "fiscalYear", "period"] if c in tidy.columns])

    #fixing sign presentations to use easier in graphs
    if "CapexRaw" in tidy.columns:
        tidy["CapEx"] = tidy["CapexRaw"].abs()
    if "DividendsRaw" in tidy.columns:
        tidy["Dividends"] = tidy["DividendsRaw"].abs()

    if "TotalStockholdersEquity" in tidy.columns:
        tidy["EquityForBalance"] = tidy["TotalStockholdersEquity"]
    elif "TotalEquity" in tidy.columns:
        tidy["EquityForBalance"] = tidy["TotalEquity"]

    if "NetDebt" not in tidy.columns and {"TotalDebt", "Cash"}.issubset(tidy.columns):
        tidy["NetDebt"] = tidy["TotalDebt"] - tidy["Cash"]

    # This is key, we want to use MM data, however if we divide everything my 1,000,000 ratios would also be divided and become non-sense. Thefore, we are separating the ratio cols from the numeric ones and than doing the mm division.
    ratio_like_cols = {"PE", "PS", "PB", "EV_EBITDA", "DebtToMarketCap"}
    id_like_cols = {"FiscalYear", "QuarterNumber"}
    numeric_cols = tidy.select_dtypes(include="number").columns
    currency_cols = [c for c in numeric_cols if c not in ratio_like_cols | id_like_cols]
    tidy[currency_cols] = tidy[currency_cols] / 1_000_000

    ttm_flow_cols = [
        "Revenue", "COGS", "GrossProfit", "OperatingIncome", "EBIT", "EBITDA",
        "InterestExpense", "EBT", "TaxExpense", "NetIncome", "OperatingCashFlow",
        "FreeCashFlow", "CapEx", "Dividends",
    ]
    for col in ttm_flow_cols:
        if col in tidy.columns:
            tidy[f"{col}_TTM"] = tidy[col].rolling(4, min_periods=4).sum()

    for stock_col, avg_col in [
        ("TotalAssets", "AvgAssets"),
        ("EquityForBalance", "AvgEquity"),
        ("AccountsReceivable", "AvgAR"),
        ("Inventory", "AvgInventory"),
        ("AccountsPayable", "AvgAP"),
    ]:
        if stock_col in tidy.columns:
            tidy[avg_col] = (tidy[stock_col] + tidy[stock_col].shift(1)) / 2

    # To avoid mistakes and as since was required auditable dataset we are ensuring that only one colums of this source table contributes to that period. Example, only one row of the IS is going to contribute to the 2017Q1
    audit = pd.DataFrame(index=tidy.index)
    audit["IS_RecordCount"] = _quarter_record_counts(_prep(is_df)).reindex(tidy.index).fillna(0).astype(int)
    audit["BS_RecordCount"] = _quarter_record_counts(_prep(bs_df)).reindex(tidy.index).fillna(0).astype(int)
    audit["CF_RecordCount"] = _quarter_record_counts(_prep(cf_df)).reindex(tidy.index).fillna(0).astype(int)
    audit["Ratios_RecordCount"] = _quarter_record_counts(_prep(ratios_df)).reindex(tidy.index).fillna(0).astype(int)

    if {"TotalAssets", "TotalLiabilities", "EquityForBalance"}.issubset(tidy.columns):
        audit["BalanceError_USDm"] = tidy["TotalAssets"] - (tidy["TotalLiabilities"] + tidy["EquityForBalance"])
    else:
        audit["BalanceError_USDm"] = np.nan

    if {"GrossProfit", "Revenue", "COGS"}.issubset(tidy.columns):
        audit["GrossProfitError_USDm"] = tidy["GrossProfit"] - (tidy["Revenue"] - tidy["COGS"])
    else:
        audit["GrossProfitError_USDm"] = np.nan

    audit["AuditPass"] = (
        audit[["IS_RecordCount", "BS_RecordCount", "CF_RecordCount", "Ratios_RecordCount"]].eq(1).all(axis=1)
        & audit["BalanceError_USDm"].fillna(0).abs().lt(0.5)
        & audit["GrossProfitError_USDm"].fillna(0).abs().lt(0.5)
    )

    return tidy, audit
# Using what we just created and bringing in the raw data
tidy, audit = build_tidy_pack(is_raw, bs_raw, cf_raw, ratios_raw)

print("Fiscal quarters in tidy dataset:", tidy.index[0], "to", tidy.index[-1])
print("Rows x columns:", tidy.shape)
display(tidy.head(8))
display(audit.head(8))

# Wasn't required but for control purpose we are going to save a tidy CSV that can be loaded to other projects data that is already ready to go.
out_dir = OUT_DIR
out_dir.mkdir(parents=True, exist_ok=True)
tidy.to_csv(out_dir / "unit1_tidy_historical_pack_usdm.csv")
audit.to_csv(out_dir / "unit1_tidy_historical_audit.csv")
Fiscal quarters in tidy dataset: FY2016 Q4 to FY2026 Q3
Rows x columns: (40, 69)
Revenue COGS GrossProfit OperatingExpenses OperatingIncome EBIT EBITDA DA InterestExpense EBT TaxExpense NetIncome Cash CashAndSTInvestments AccountsReceivable Inventory AccountsPayable AccruedExpenses DeferredRevenue OtherCurrentAssets OtherCurrentLiabilities TotalCurrentAssets TotalCurrentLiabilities PPENet TotalAssets TotalLiabilities TotalEquity TotalStockholdersEquity TotalDebt NetDebt OperatingCashFlow CapexRaw FreeCashFlow DeltaWC_CFSign DeferredTax SBC DividendsRaw CashTaxesPaid PE PS PB EV_EBITDA DebtToMarketCap Date FiscalYear Period QuarterNumber CapEx Dividends EquityForBalance Revenue_TTM COGS_TTM GrossProfit_TTM OperatingIncome_TTM EBIT_TTM EBITDA_TTM InterestExpense_TTM EBT_TTM TaxExpense_TTM NetIncome_TTM OperatingCashFlow_TTM FreeCashFlow_TTM CapEx_TTM Dividends_TTM AvgAssets AvgEquity AvgAR AvgInventory AvgAP
Quarter
FY2016 Q4 1,871.00 857.00 1,014.00 947.00 67.00 69.00 152.00 7.00 7.00 62.00 21.00 41.00 456.30 1,085.70 516.50 1,125.00 151.00 581.20 50.10 90.20 66.20 3,053.00 1,198.00 1,583.00 6,213.00 2,469.00 3,744.00 3,744.00 999.50 543.20 155.00 -93.00 62.00 -153.00 -4.00 18.00 -42.00 0.00 49.76 4.36 2.18 57.26 0.09 2016-04-02 2016 Q4 4 93.00 42.00 3,744.00 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
FY2017 Q1 1,552.00 657.60 894.60 925.80 -31.20 -29.80 48.60 6.00 3.40 -33.00 -10.90 -22.00 457.00 1,076.00 338.00 1,242.00 192.00 591.00 48.00 97.00 134.00 3,002.00 1,296.00 1,565.00 6,118.00 2,552.00 3,566.00 3,566.00 978.00 521.00 243.00 -78.00 165.00 81.00 3.00 18.00 -41.40 0.00 -84.54 4.79 2.09 163.81 0.09 2016-07-02 2017 Q1 1 78.00 41.40 3,566.00 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN 6,165.50 3,655.00 427.25 1,183.50 171.50
FY2017 Q2 1,821.00 866.40 954.20 877.90 76.30 77.80 153.40 76.00 4.10 73.00 28.00 45.00 434.00 965.00 490.00 1,173.00 159.00 565.00 38.00 81.00 127.00 2,976.00 1,217.00 1,564.00 6,067.00 2,469.00 3,598.00 3,598.00 973.00 539.00 -11.00 -87.00 -98.00 -244.00 -12.00 14.00 -41.00 0.00 46.47 4.59 2.32 58.04 0.08 2016-10-01 2017 Q2 2 87.00 41.00 3,598.00 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN 6,092.50 3,582.00 414.00 1,207.50 175.50
FY2017 Q3 1,714.00 731.40 983.20 854.90 128.30 126.70 204.60 6.00 3.60 124.00 41.80 82.00 928.00 1,381.00 285.00 984.00 158.00 609.00 34.00 130.00 94.00 3,034.00 1,151.00 1,514.00 5,966.00 2,356.00 3,610.00 3,610.00 863.00 -65.00 618.00 -60.00 558.00 391.00 19.00 14.00 -42.00 0.00 22.75 4.35 2.07 36.15 0.08 2016-12-31 2017 Q3 3 60.00 42.00 3,610.00 6,958.00 3,112.40 3,846.00 240.40 243.70 558.60 18.10 226.00 79.90 146.00 1,005.00 687.00 318.00 166.40 6,016.50 3,604.00 387.50 1,078.50 158.50
FY2017 Q4 1,565.80 746.70 819.10 1,086.90 -267.80 -267.50 -192.00 75.50 1.40 -268.90 -64.60 -204.30 668.30 1,353.00 450.20 791.50 147.70 564.60 29.70 82.30 153.10 2,954.50 1,159.90 1,316.00 5,652.00 2,352.40 3,299.60 3,299.60 861.70 193.40 102.30 -59.00 43.30 5.60 -48.90 17.60 -40.80 0.00 -8.20 4.28 2.03 -35.91 0.09 2017-04-01 2017 Q4 4 59.00 40.80 3,299.60 6,652.80 3,002.10 3,651.10 -94.40 -92.80 214.60 12.50 -104.90 -5.70 -99.30 952.30 668.30 284.00 165.20 5,809.00 3,454.80 367.60 887.75 152.85
FY2018 Q1 1,347.10 495.90 851.20 760.90 90.30 91.80 164.70 72.90 5.00 86.80 27.30 59.50 830.40 1,570.90 279.20 859.90 160.90 587.40 30.60 90.30 134.90 3,086.70 1,216.90 1,273.30 5,814.00 2,453.90 3,360.10 3,360.10 859.20 28.80 334.20 -41.90 292.30 182.50 -14.70 21.60 -40.50 0.00 25.30 4.47 1.79 36.74 0.10 2017-07-01 2018 Q1 1 41.90 40.50 3,360.10 6,447.90 2,840.40 3,607.70 27.10 28.80 330.70 14.10 14.90 32.50 -17.80 1,043.50 795.60 247.90 164.30 5,733.00 3,329.85 364.70 825.70 154.30
FY2018 Q2 1,664.20 668.40 995.80 802.50 193.30 195.80 269.60 73.80 4.60 191.20 47.40 143.80 1,111.60 1,618.70 470.30 864.60 172.80 610.10 33.10 81.80 143.60 3,324.40 1,590.10 1,240.50 6,028.30 2,518.70 3,509.60 3,509.60 855.10 -256.50 102.80 -32.80 70.00 -136.20 -10.60 17.80 -40.60 0.00 12.54 4.33 2.06 25.80 0.08 2017-09-30 2018 Q2 2 32.80 40.60 3,509.60 6,291.10 2,642.40 3,649.30 144.10 146.80 446.90 14.60 133.10 51.90 81.00 1,157.30 963.60 193.70 163.90 5,921.15 3,434.85 374.75 862.25 166.85
FY2018 Q3 1,641.80 645.60 996.20 807.00 189.20 191.10 263.80 72.70 4.80 186.30 268.10 -81.80 1,175.70 2,038.00 295.20 825.40 184.30 659.60 33.90 73.70 130.50 3,533.20 1,710.20 1,215.90 6,199.00 2,791.50 3,407.50 3,407.50 849.00 -326.70 514.10 -48.30 465.80 479.20 17.30 16.90 -40.60 0.00 -25.89 5.16 2.49 30.87 0.07 2017-12-30 2018 Q3 3 48.30 40.60 3,407.50 6,218.90 2,556.60 3,662.30 205.00 211.20 506.10 15.80 195.40 278.20 -82.80 1,053.40 871.40 182.00 162.50 6,113.65 3,458.55 382.75 845.00 178.55
IS_RecordCount BS_RecordCount CF_RecordCount Ratios_RecordCount BalanceError_USDm GrossProfitError_USDm AuditPass
Quarter
FY2016 Q4 1 1 1 1 0.00 0.00 True
FY2017 Q1 1 1 1 1 0.00 0.20 True
FY2017 Q2 1 1 1 1 0.00 -0.40 True
FY2017 Q3 1 1 1 1 0.00 0.60 False
FY2017 Q4 1 1 1 1 0.00 0.00 True
FY2018 Q1 1 1 1 1 0.00 0.00 True
FY2018 Q2 1 1 1 1 0.00 -0.00 True
FY2018 Q3 1 1 1 1 0.00 0.00 True

3) Common-SizeΒΆ

Here we are going to analyse the historical data behavior. As mentioned in class, a great way to understand more of the company is visually identifying the disruptions and investigating the causes.

Also we are going to start using data as % of revenue to better visualization and control

#Defining the 2 main functions that we are going to use for conduct this analysis

def common_size(df: pd.DataFrame, base_col: str, cols: list[str]) -> pd.DataFrame:
    base = pd.to_numeric(df[base_col], errors="coerce").replace(0, np.nan)
    out = pd.DataFrame(index=df.index)
    for c in cols:
        if c in df.columns:
            out[c] = pd.to_numeric(df[c], errors="coerce") / base
    return out * 100.0


def add_trend_metrics(df: pd.DataFrame, cols: list[str]) -> pd.DataFrame:
    out = df.copy()
    for c in cols:
        series = pd.to_numeric(out[c], errors="coerce")
        out[c] = series
        out[f"{c}_QoQ_pct"] = series.pct_change() * 100
        out[f"{c}_YoY_pct"] = series.pct_change(4) * 100
        out[f"{c}_TTM"] = series.rolling(4, min_periods=4).sum()
    return out

# Settings columns with information that we are going to analyse
is_cs_cols = ["Revenue", "COGS", "GrossProfit", "OperatingExpenses", "OperatingIncome", "NetIncome"]
bs_cs_cols = ["Cash", "AccountsReceivable", "Inventory", "PPENet", "TotalDebt", "TotalLiabilities", "EquityForBalance", "TotalAssets"]

common_is = common_size(tidy, "Revenue", [c for c in is_cs_cols if c in tidy.columns])
common_bs = common_size(tidy, "TotalAssets", [c for c in bs_cs_cols if c in tidy.columns])

# Creating columns for the trending analyse
trend_cols = [c for c in ["Revenue", "GrossProfit", "OperatingIncome", "NetIncome", "OperatingCashFlow", "FreeCashFlow", "CapEx"] if c in tidy.columns]
trend = add_trend_metrics(tidy[trend_cols], trend_cols)

display(common_is.round(2))
display(common_bs.round(2))
display(trend.round(2))

fig, axes = plt.subplots(1, 2, figsize=(16, 5))

marg = pd.DataFrame(index=tidy.index)
if {"GrossProfit", "Revenue"}.issubset(tidy.columns):
    marg["GrossMargin"] = tidy["GrossProfit"] / tidy["Revenue"].replace(0, np.nan) * 100
if {"OperatingIncome", "Revenue"}.issubset(tidy.columns):
    marg["OperatingMargin"] = tidy["OperatingIncome"] / tidy["Revenue"].replace(0, np.nan) * 100
if {"NetIncome", "Revenue"}.issubset(tidy.columns):
    marg["NetMargin"] = tidy["NetIncome"] / tidy["Revenue"].replace(0, np.nan) * 100

for col in marg.columns:
    axes[0].plot(marg.index, marg[col], marker="o", linewidth=1.8, label=col)
axes[0].set_title("Quarterly Common-Size Margins (%)")
axes[0].set_xlabel("Fiscal Quarter")
axes[0].set_ylabel("Percent")
axes[0].tick_params(axis="x", rotation=45)
axes[0].legend()

#Formating for getting plot ready
plot_cols = [c for c in ["Revenue", "NetIncome"] if c in tidy.columns]
for col in plot_cols:
    axes[1].plot(tidy.index, tidy[col], marker="o", linewidth=1.8, label=col)
axes[1].set_title("Quarterly Level Trends (USD m)")
axes[1].set_xlabel("Fiscal Quarter")
axes[1].set_ylabel("USD m")
axes[1].tick_params(axis="x", rotation=45)
axes[1].legend()

#plotting
plt.tight_layout()
plt.show()
Revenue COGS GrossProfit OperatingExpenses OperatingIncome NetIncome
Quarter
FY2016 Q4 100.00 45.80 54.20 50.61 3.58 2.19
FY2017 Q1 100.00 42.37 57.64 59.65 -2.01 -1.42
FY2017 Q2 100.00 47.58 52.40 48.21 4.19 2.47
FY2017 Q3 100.00 42.67 57.36 49.88 7.49 4.78
FY2017 Q4 100.00 47.69 52.31 69.41 -17.10 -13.05
FY2018 Q1 100.00 36.81 63.19 56.48 6.70 4.42
FY2018 Q2 100.00 40.16 59.84 48.22 11.62 8.64
FY2018 Q3 100.00 39.32 60.68 49.15 11.52 -4.98
FY2018 Q4 100.00 40.59 59.41 57.75 1.66 2.70
FY2019 Q1 100.00 35.59 64.41 55.06 9.36 7.84
FY2019 Q2 100.00 39.13 60.87 48.45 12.42 10.07
FY2019 Q3 100.00 38.61 61.39 50.16 11.23 6.95
FY2019 Q4 100.00 40.13 59.87 58.02 1.85 2.10
FY2020 Q1 100.00 35.55 64.45 54.42 10.03 8.20
FY2020 Q2 100.00 38.52 61.48 47.82 13.66 10.67
FY2020 Q3 100.00 37.79 62.21 49.39 12.82 19.08
FY2020 Q4 100.00 53.35 46.65 68.93 -22.27 -19.54
FY2021 Q1 100.00 28.47 71.53 105.99 -34.46 -26.19
FY2021 Q2 100.00 33.02 66.98 68.68 -1.70 -3.28
FY2021 Q3 100.00 35.06 64.94 53.04 11.89 8.36
FY2021 Q4 100.00 39.17 60.83 62.83 -2.00 -5.76
FY2022 Q1 100.00 29.66 70.34 54.31 16.03 11.97
FY2022 Q2 100.00 32.50 67.50 50.75 16.75 12.85
FY2022 Q3 100.00 34.00 66.00 50.07 15.92 11.99
FY2022 Q4 100.00 36.55 63.45 61.03 2.42 1.60
FY2023 Q1 100.00 32.82 67.18 55.43 11.75 8.28
FY2023 Q2 100.00 35.24 64.76 51.67 13.08 9.53
FY2023 Q3 100.00 35.02 64.98 49.59 15.40 11.82
FY2023 Q4 100.00 38.30 61.70 59.09 2.61 2.10
FY2024 Q1 100.00 31.04 68.96 57.84 11.12 8.83
FY2024 Q2 100.00 34.47 65.53 55.46 10.07 9.00
FY2024 Q3 100.00 33.51 66.49 50.07 16.43 14.30
FY2024 Q4 100.00 33.43 66.57 59.69 6.88 5.78
FY2025 Q1 100.00 29.52 70.48 56.69 13.79 11.15
FY2025 Q2 100.00 33.04 66.96 56.59 10.37 8.57
FY2025 Q3 100.00 31.60 68.40 50.22 18.18 13.87
FY2025 Q4 100.00 31.34 68.66 59.52 9.13 7.60
FY2026 Q1 100.00 27.74 72.26 56.35 15.92 12.82
FY2026 Q2 100.00 32.04 67.96 55.74 12.22 10.32
FY2026 Q3 100.00 30.10 69.90 48.99 20.91 15.03
Cash AccountsReceivable Inventory PPENet TotalDebt TotalLiabilities EquityForBalance TotalAssets
Quarter
FY2016 Q4 7.34 8.31 18.11 25.48 16.09 39.74 60.26 100.00
FY2017 Q1 7.47 5.52 20.30 25.58 15.99 41.71 58.29 100.00
FY2017 Q2 7.15 8.08 19.33 25.78 16.04 40.70 59.30 100.00
FY2017 Q3 15.55 4.78 16.49 25.38 14.47 39.49 60.51 100.00
FY2017 Q4 11.82 7.97 14.00 23.28 15.25 41.62 58.38 100.00
FY2018 Q1 14.28 4.80 14.79 21.90 14.78 42.21 57.79 100.00
FY2018 Q2 18.44 7.80 14.34 20.58 14.18 41.78 58.22 100.00
FY2018 Q3 18.97 4.76 13.32 19.61 13.70 45.03 54.97 100.00
FY2018 Q4 21.24 6.86 12.39 19.31 13.87 43.72 56.28 100.00
FY2019 Q1 8.82 4.31 14.75 18.92 13.85 43.32 56.68 100.00
FY2019 Q2 8.99 7.02 16.16 18.37 15.08 43.76 56.24 100.00
FY2019 Q3 11.17 4.99 15.01 17.72 15.21 44.95 55.05 100.00
FY2019 Q4 9.83 6.70 13.76 17.49 15.55 44.69 55.31 100.00
FY2020 Q1 8.83 3.96 13.46 32.72 36.50 58.97 41.03 100.00
FY2020 Q2 7.58 6.68 14.01 35.67 39.08 59.69 40.31 100.00
FY2020 Q3 14.50 4.69 12.15 34.94 37.88 58.14 41.86 100.00
FY2020 Q4 22.26 3.81 10.11 34.22 44.33 63.01 36.99 100.00
FY2021 Q1 31.67 1.40 9.99 31.13 51.18 66.98 33.02 100.00
FY2021 Q2 25.96 4.57 11.44 30.03 46.54 67.18 32.82 100.00
FY2021 Q3 32.08 4.57 10.60 29.69 45.37 67.06 32.94 100.00
FY2021 Q4 32.70 5.72 9.62 28.57 45.90 66.98 33.02 100.00
FY2022 Q1 32.61 4.61 10.09 27.08 44.37 65.87 34.13 100.00
FY2022 Q2 29.20 5.13 11.35 25.93 42.64 64.99 35.01 100.00
FY2022 Q3 27.99 5.05 11.42 25.78 42.27 66.53 33.47 100.00
FY2022 Q4 24.13 5.25 12.65 26.94 43.91 67.17 32.83 100.00
FY2023 Q1 20.96 5.04 16.95 28.57 40.43 65.99 34.01 100.00
FY2023 Q2 16.44 7.27 18.73 28.45 41.02 66.50 33.50 100.00
FY2023 Q3 22.25 6.02 17.59 28.70 40.09 64.95 35.05 100.00
FY2023 Q4 22.52 6.59 15.78 30.78 42.45 64.20 35.80 100.00
FY2024 Q1 23.40 5.03 17.29 29.65 41.34 64.46 35.54 100.00
FY2024 Q2 20.55 6.86 17.78 29.21 41.27 64.76 35.24 100.00
FY2024 Q3 25.75 5.77 15.06 27.85 39.35 63.28 36.72 100.00
FY2024 Q4 25.17 6.76 13.66 28.25 40.52 62.89 37.11 100.00
FY2025 Q1 23.90 5.60 15.65 27.79 40.39 64.36 35.64 100.00
FY2025 Q2 19.93 7.62 16.59 27.15 39.23 64.08 35.92 100.00
FY2025 Q3 27.40 6.15 14.10 26.12 37.90 64.14 35.86 100.00
FY2025 Q4 27.28 6.52 13.47 26.39 37.86 63.27 36.73 100.00
FY2026 Q1 26.96 5.12 15.77 27.16 56.10 67.55 32.45 100.00
FY2026 Q2 19.66 7.14 17.17 29.80 54.76 64.86 35.14 100.00
FY2026 Q3 26.03 5.90 14.71 28.30 51.37 63.03 36.97 100.00
Revenue GrossProfit OperatingIncome NetIncome OperatingCashFlow FreeCashFlow CapEx Revenue_QoQ_pct Revenue_YoY_pct Revenue_TTM GrossProfit_QoQ_pct GrossProfit_YoY_pct GrossProfit_TTM OperatingIncome_QoQ_pct OperatingIncome_YoY_pct OperatingIncome_TTM NetIncome_QoQ_pct NetIncome_YoY_pct NetIncome_TTM OperatingCashFlow_QoQ_pct OperatingCashFlow_YoY_pct OperatingCashFlow_TTM FreeCashFlow_QoQ_pct FreeCashFlow_YoY_pct FreeCashFlow_TTM CapEx_QoQ_pct CapEx_YoY_pct CapEx_TTM
Quarter
FY2016 Q4 1,871.00 1,014.00 67.00 41.00 155.00 62.00 93.00 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
FY2017 Q1 1,552.00 894.60 -31.20 -22.00 243.00 165.00 78.00 -17.05 NaN NaN -11.78 NaN NaN -146.57 NaN NaN -153.66 NaN NaN 56.77 NaN NaN 166.13 NaN NaN -16.13 NaN NaN
FY2017 Q2 1,821.00 954.20 76.30 45.00 -11.00 -98.00 87.00 17.33 NaN NaN 6.66 NaN NaN -344.55 NaN NaN -304.55 NaN NaN -104.53 NaN NaN -159.39 NaN NaN 11.54 NaN NaN
FY2017 Q3 1,714.00 983.20 128.30 82.00 618.00 558.00 60.00 -5.88 NaN 6,958.00 3.04 NaN 3,846.00 68.15 NaN 240.40 82.22 NaN 146.00 -5,718.18 NaN 1,005.00 -669.39 NaN 687.00 -31.03 NaN 318.00
FY2017 Q4 1,565.80 819.10 -267.80 -204.30 102.30 43.30 59.00 -8.65 -16.31 6,652.80 -16.69 -19.22 3,651.10 -308.73 -499.70 -94.40 -349.15 -598.29 -99.30 -83.45 -34.00 952.30 -92.24 -30.16 668.30 -1.67 -36.56 284.00
FY2018 Q1 1,347.10 851.20 90.30 59.50 334.20 292.30 41.90 -13.97 -13.20 6,447.90 3.92 -4.85 3,607.70 -133.72 -389.42 27.10 -129.12 -370.45 -17.80 226.69 37.53 1,043.50 575.06 77.15 795.60 -28.98 -46.28 247.90
FY2018 Q2 1,664.20 995.80 193.30 143.80 102.80 70.00 32.80 23.54 -8.61 6,291.10 16.99 4.36 3,649.30 114.06 153.34 144.10 141.68 219.56 81.00 -69.24 -1,034.55 1,157.30 -76.05 -171.43 963.60 -21.72 -62.30 193.70
FY2018 Q3 1,641.80 996.20 189.20 -81.80 514.10 465.80 48.30 -1.35 -4.21 6,218.90 0.04 1.32 3,662.30 -2.12 47.47 205.00 -156.88 -199.76 -82.80 400.10 -16.81 1,053.40 565.43 -16.52 871.40 47.26 -19.50 182.00
FY2018 Q4 1,529.20 908.50 25.40 41.30 24.00 -14.60 38.60 -6.86 -2.34 6,182.30 -8.80 10.91 3,751.70 -86.58 -109.48 498.20 -150.49 -120.22 162.80 -95.33 -76.54 975.10 -103.13 -133.72 813.50 -20.08 -34.58 161.60
FY2019 Q1 1,390.60 895.70 130.10 109.00 230.60 188.30 42.30 -9.06 3.23 6,225.80 -1.41 5.23 3,796.20 412.20 44.08 538.00 163.92 83.19 212.30 860.83 -31.00 871.50 -1,389.73 -35.58 709.50 9.59 0.95 162.00
FY2019 Q2 1,690.90 1,029.30 210.00 170.30 -17.60 -68.40 50.80 21.59 1.60 6,252.50 14.92 3.36 3,829.70 61.41 8.64 554.70 56.24 18.43 238.80 -107.63 -117.12 751.10 -136.33 -197.71 571.10 20.09 54.88 180.00
FY2019 Q3 1,725.80 1,059.50 193.80 120.00 470.10 414.00 56.10 2.06 5.12 6,336.50 2.93 6.35 3,893.00 -7.71 2.43 559.30 -29.54 -246.70 440.60 -2,771.02 -8.56 707.10 -705.26 -11.12 519.30 10.43 16.15 187.80
FY2019 Q4 1,505.70 901.50 27.90 31.60 100.70 52.20 48.50 -12.75 -1.54 6,313.00 -14.91 -0.77 3,886.00 -85.60 9.84 561.80 -73.67 -23.49 430.90 -78.58 319.58 783.80 -87.39 -457.53 586.10 -13.55 25.65 197.70
FY2020 Q1 1,428.80 920.80 143.30 117.10 197.40 148.00 49.40 -5.11 2.75 6,351.20 2.14 2.80 3,911.10 413.62 10.15 575.00 270.57 7.43 439.00 96.03 -14.40 750.60 183.52 -21.40 545.80 1.86 16.78 204.80
FY2020 Q2 1,706.20 1,049.00 233.10 182.10 6.70 -74.50 81.20 19.41 0.90 6,366.50 13.92 1.91 3,930.80 62.67 11.00 598.10 55.51 6.93 450.80 -96.61 -138.07 774.90 -150.34 8.92 539.70 64.37 59.84 235.20
FY2020 Q3 1,750.70 1,089.10 224.40 334.10 543.90 458.50 85.40 2.61 1.44 6,391.40 3.82 2.79 3,960.40 -3.73 15.79 628.70 83.47 178.42 664.90 8,017.91 15.70 848.70 -715.44 10.75 584.20 5.17 52.23 264.50
FY2020 Q4 1,274.10 594.40 -283.80 -249.00 6.60 -47.70 54.30 -27.22 -15.38 6,159.80 -45.42 -34.07 3,653.30 -226.47 -1,117.20 317.00 -174.53 -887.97 384.30 -98.79 -93.45 754.60 -110.40 -191.38 484.30 -36.42 11.96 270.30
FY2021 Q1 487.50 348.70 -168.00 -127.70 -70.30 -91.60 21.30 -61.74 -65.88 5,218.50 -41.34 -62.13 3,081.20 -40.80 -217.24 5.70 -48.71 -209.05 139.50 -1,165.15 -135.61 486.90 92.03 -161.89 244.70 -60.77 -56.88 242.20
FY2021 Q2 1,193.50 799.40 -20.30 -39.10 57.60 25.00 32.60 144.82 -30.05 4,705.80 129.25 -23.79 2,831.60 -87.92 -108.71 -247.70 -69.38 -121.47 -81.70 -181.93 759.70 537.80 -127.29 -133.56 344.20 53.05 -59.85 193.60
FY2021 Q3 1,432.80 930.40 170.40 119.80 347.30 320.40 26.90 20.05 -18.16 4,387.90 16.39 -14.57 2,672.90 -939.41 -24.06 -301.70 -406.39 -64.14 -296.00 502.95 -36.15 341.20 1,181.60 -30.12 206.10 -17.48 -68.50 135.10
FY2021 Q4 1,287.00 782.90 -25.70 -74.10 46.30 19.30 27.00 -10.18 1.01 4,400.80 -15.85 31.71 2,861.40 -115.08 -90.94 -43.60 -161.85 -70.24 -121.10 -86.67 601.52 380.90 -93.98 -140.46 273.10 0.37 -50.28 107.80
FY2022 Q1 1,376.30 968.10 220.60 164.70 247.60 219.40 28.20 6.94 182.32 5,289.60 23.66 177.63 3,480.80 -958.37 -231.31 345.00 -322.27 -228.97 171.30 434.77 -452.20 698.80 1,036.79 -339.52 584.10 4.44 32.39 114.70
FY2022 Q2 1,504.10 1,015.20 251.90 193.30 216.60 181.40 35.20 9.29 26.02 5,600.20 4.87 27.00 3,696.60 14.19 -1,340.89 617.20 17.36 -594.37 403.70 -12.52 276.04 857.80 -17.32 625.60 740.50 24.82 7.98 117.30
FY2022 Q3 1,815.40 1,198.10 289.10 217.70 357.50 307.30 50.20 20.70 26.70 5,982.80 18.02 28.77 3,964.30 14.77 69.66 735.90 12.62 81.72 501.60 65.05 2.94 868.00 69.40 -4.09 727.40 42.61 86.62 140.60
FY2022 Q4 1,522.70 966.10 36.80 24.40 -105.80 -159.10 53.30 -16.12 18.31 6,218.50 -19.36 23.40 4,147.50 -87.27 -243.19 798.40 -88.79 -132.93 600.10 -129.59 -328.51 715.90 -151.77 -924.35 549.00 6.18 97.41 166.90
FY2023 Q1 1,490.60 1,001.40 175.20 123.40 45.30 5.90 39.40 -2.11 8.30 6,332.80 3.65 3.44 4,180.80 376.09 -20.58 753.00 405.74 -25.08 558.80 -142.82 -81.70 513.60 -103.71 -97.31 335.50 -26.08 39.72 178.10
FY2023 Q2 1,579.90 1,023.10 206.70 150.50 -43.40 -87.90 44.50 5.99 5.04 6,408.60 2.17 0.78 4,188.70 17.98 -17.94 707.80 21.96 -22.14 516.00 -195.81 -120.04 253.60 -1,589.83 -148.46 66.20 12.94 26.42 187.40
FY2023 Q3 1,832.30 1,190.70 282.10 216.50 395.10 323.10 72.00 15.98 0.93 6,425.50 16.38 -0.62 4,181.30 36.48 -2.42 700.80 43.85 -0.55 514.80 -1,010.37 10.52 291.20 -467.58 5.14 82.00 61.80 43.43 209.20
FY2023 Q4 1,540.80 950.60 40.20 32.30 14.00 -47.60 61.60 -15.91 1.19 6,443.60 -20.16 -1.60 4,165.80 -85.75 9.24 704.20 -85.08 32.38 522.70 -96.46 -113.23 411.00 -114.73 -70.08 193.50 -14.44 15.57 217.50
FY2024 Q1 1,496.50 1,032.00 166.40 132.10 270.70 231.10 39.60 -2.88 0.40 6,449.50 8.56 3.06 4,196.40 313.93 -5.02 695.40 308.98 7.05 531.40 1,833.57 497.57 636.40 -585.50 3,816.95 418.70 -35.71 0.51 217.70
FY2024 Q2 1,633.00 1,070.10 164.50 146.90 72.90 30.10 42.80 9.12 3.36 6,502.60 3.69 4.59 4,243.40 -1.14 -20.42 653.20 11.20 -2.39 527.80 -73.07 -267.97 752.70 -86.98 -134.24 536.70 8.08 -3.82 216.00
FY2024 Q3 1,934.00 1,286.00 317.70 276.60 605.10 562.60 42.50 18.43 5.55 6,604.30 20.18 8.00 4,338.70 93.13 12.62 688.80 88.29 27.76 587.90 730.04 53.15 962.70 1,769.10 74.13 776.20 -0.70 -40.97 186.50
FY2024 Q4 1,567.90 1,043.70 107.80 90.70 121.00 81.10 39.90 -18.93 1.76 6,631.40 -18.84 9.79 4,431.80 -66.07 168.16 756.40 -67.21 180.80 646.30 -80.00 764.29 1,069.70 -85.58 -270.38 904.90 -6.12 -35.23 164.80
FY2025 Q1 1,512.20 1,065.80 208.50 168.60 277.30 243.90 33.40 -3.55 1.05 6,647.10 2.12 3.28 4,465.60 93.41 25.30 798.50 85.89 27.63 682.80 129.17 2.44 1,076.30 200.74 5.54 917.70 -16.29 -15.66 158.60
FY2025 Q2 1,726.00 1,155.70 178.90 147.90 97.20 55.50 41.70 14.14 5.70 6,740.10 8.43 8.00 4,551.20 -14.20 8.75 812.90 -12.28 0.68 683.80 -64.95 33.33 1,100.60 -77.24 84.39 943.10 24.85 -2.57 157.50
FY2025 Q3 2,143.50 1,466.10 389.70 297.40 738.40 677.20 61.20 24.19 10.83 6,949.60 26.86 14.00 4,731.30 117.83 22.66 884.90 101.08 7.52 704.60 659.67 22.03 1,233.90 1,120.18 20.37 1,057.70 46.76 44.00 176.20
FY2025 Q4 1,697.30 1,165.30 155.00 129.00 122.20 42.30 79.90 -20.82 8.25 7,079.00 -20.52 11.65 4,852.90 -60.23 43.78 932.10 -56.62 42.23 742.90 -83.45 0.99 1,235.10 -93.75 -47.84 1,018.90 30.56 100.25 216.20
FY2026 Q1 1,719.10 1,242.30 273.60 220.40 176.10 -11.20 187.30 1.28 13.68 7,285.90 6.61 16.56 5,029.40 76.52 31.22 997.20 70.85 30.72 794.70 44.11 -36.49 1,133.90 -126.48 -104.59 763.80 134.42 460.78 370.10
FY2026 Q2 2,010.70 1,366.40 245.70 207.50 53.20 -40.60 93.80 16.96 16.49 7,570.60 9.99 18.23 5,240.10 -10.20 37.34 1,064.00 -5.85 40.30 854.30 -69.79 -45.27 1,089.90 262.50 -173.15 667.70 -49.92 124.94 422.20
FY2026 Q3 2,406.00 1,681.70 503.10 361.60 779.60 704.00 75.60 19.66 12.25 7,833.10 23.08 14.71 5,455.70 104.76 29.10 1,177.40 74.27 21.59 918.50 1,365.41 5.58 1,131.10 -1,833.99 3.96 694.50 -19.40 23.53 436.60
No description has been provided for this image

4) Ratio Matrix and Heat MapΒΆ

Areas covered included:

  1. Liquidity
  2. Profitability
  3. Efficiency (working-capital velocity)
  4. Leverage & coverage
  5. Valuation

We are using the Z-score method as mentioned in class to create the heat map

def _avg(s: pd.Series) -> pd.Series:
    return (s + s.shift(1)) / 2


# defining the main fuction to create the ratio matrix and making sure we are covering all families and have no blank datas
def build_ratio_matrix(tidy_df: pd.DataFrame) -> pd.DataFrame:
    d = tidy_df.copy()
    r = pd.DataFrame(index=d.index)

    if {"TotalCurrentAssets", "TotalCurrentLiabilities"}.issubset(d.columns):
        r["Liquidity_CurrentRatio"] = d["TotalCurrentAssets"] / d["TotalCurrentLiabilities"].replace(0, np.nan)
    if {"Cash", "AccountsReceivable", "TotalCurrentLiabilities"}.issubset(d.columns):
        r["Liquidity_QuickRatio"] = (d["Cash"] + d["AccountsReceivable"]) / d["TotalCurrentLiabilities"].replace(0, np.nan)
    if {"Cash", "TotalCurrentLiabilities"}.issubset(d.columns):
        r["Liquidity_CashRatio"] = d["Cash"] / d["TotalCurrentLiabilities"].replace(0, np.nan)

    if {"GrossProfit", "Revenue"}.issubset(d.columns):
        r["Profitability_GrossMargin"] = d["GrossProfit"] / d["Revenue"].replace(0, np.nan)
    if {"EBIT", "Revenue"}.issubset(d.columns):
        r["Profitability_EBITMargin"] = d["EBIT"] / d["Revenue"].replace(0, np.nan)
    if {"NetIncome", "Revenue"}.issubset(d.columns):
        r["Profitability_NetMargin"] = d["NetIncome"] / d["Revenue"].replace(0, np.nan)
    if {"NetIncome_TTM", "AvgAssets"}.issubset(d.columns):
        r["Profitability_ROA"] = d["NetIncome_TTM"] / d["AvgAssets"].replace(0, np.nan)
    if {"NetIncome_TTM", "AvgEquity"}.issubset(d.columns):
        r["Profitability_ROE"] = d["NetIncome_TTM"] / d["AvgEquity"].replace(0, np.nan)

    if {"AvgAR", "Revenue_TTM"}.issubset(d.columns):
        r["Efficiency_ARDays"] = d["AvgAR"] / (d["Revenue_TTM"] / 365).replace(0, np.nan)
    if {"AvgInventory", "COGS_TTM"}.issubset(d.columns):
        r["Efficiency_InventoryDays"] = d["AvgInventory"] / (d["COGS_TTM"] / 365).replace(0, np.nan)
    if {"AvgAP", "COGS_TTM"}.issubset(d.columns):
        r["Efficiency_APDays"] = d["AvgAP"] / (d["COGS_TTM"] / 365).replace(0, np.nan)
    if {"Efficiency_ARDays", "Efficiency_InventoryDays", "Efficiency_APDays"}.issubset(r.columns):
        r["Efficiency_CCC"] = r["Efficiency_ARDays"] + r["Efficiency_InventoryDays"] - r["Efficiency_APDays"]
    if {"Revenue_TTM", "AvgAssets"}.issubset(d.columns):
        r["Efficiency_AssetTurnover"] = d["Revenue_TTM"] / d["AvgAssets"].replace(0, np.nan)

    if {"TotalDebt", "EBITDA_TTM"}.issubset(d.columns):
        r["Leverage_DebtToEBITDA"] = d["TotalDebt"] / d["EBITDA_TTM"].replace(0, np.nan)
    if {"TotalDebt", "EquityForBalance"}.issubset(d.columns):
        r["Leverage_DebtToEquity"] = d["TotalDebt"] / d["EquityForBalance"].replace(0, np.nan)
    if {"TotalDebt", "TotalAssets"}.issubset(d.columns):
        r["Leverage_DebtToAssets"] = d["TotalDebt"] / d["TotalAssets"].replace(0, np.nan)
    if {"NetDebt", "EBITDA_TTM"}.issubset(d.columns):
        r["Leverage_NetDebtToEBITDA"] = d["NetDebt"] / d["EBITDA_TTM"].replace(0, np.nan)
    if {"EBIT_TTM", "InterestExpense_TTM"}.issubset(d.columns):
        r["Leverage_InterestCoverage"] = d["EBIT_TTM"] / d["InterestExpense_TTM"].replace(0, np.nan)

    for col in ["PE", "PS", "PB", "EV_EBITDA", "DebtToMarketCap"]:
        if col in d.columns:
            r[f"Valuation_{col}"] = d[col]

    return r.replace([np.inf, -np.inf], np.nan)


# Using the function in the tidy dataframe
ratio_matrix = build_ratio_matrix(tidy)
display(ratio_matrix.round(3))

# Defining the rations that lower is better.
heat = ratio_matrix.copy()
lower_is_better = [
    "Efficiency_ARDays", "Efficiency_InventoryDays", "Efficiency_APDays", "Efficiency_CCC",
    "Leverage_DebtToEBITDA", "Leverage_DebtToEquity", "Leverage_DebtToAssets", "Leverage_NetDebtToEBITDA",
    "Valuation_PE", "Valuation_PS", "Valuation_PB", "Valuation_EV_EBITDA", "Valuation_DebtToMarketCap",
]
for col in lower_is_better:
    if col in heat.columns:
        heat[col] = -heat[col]

z = (heat - heat.mean()) / heat.std(ddof=0)
z = z.replace([np.inf, -np.inf], np.nan)
z_plot = z.dropna(axis=1, how="all").T

# Plotting
fig, ax = plt.subplots(figsize=(16, 7))
im = ax.imshow(z_plot.values, aspect="auto")
ax.set_yticks(range(len(z_plot.index)))
ax.set_yticklabels(z_plot.index)
ax.set_xticks(range(len(z_plot.columns)))
ax.set_xticklabels(z_plot.columns, rotation=45, ha="right")
ax.set_title("Quarterly Ratio Dashboard Heat Map (within-ratio z-scores)")
ax.set_xlabel("Fiscal Quarter")
ax.set_ylabel("Ratio")
plt.colorbar(im, ax=ax, fraction=0.046, pad=0.04)
plt.tight_layout()
plt.show()
Liquidity_CurrentRatio Liquidity_QuickRatio Liquidity_CashRatio Profitability_GrossMargin Profitability_EBITMargin Profitability_NetMargin Profitability_ROA Profitability_ROE Efficiency_ARDays Efficiency_InventoryDays Efficiency_APDays Efficiency_CCC Efficiency_AssetTurnover Leverage_DebtToEBITDA Leverage_DebtToEquity Leverage_DebtToAssets Leverage_NetDebtToEBITDA Leverage_InterestCoverage Valuation_PE Valuation_PS Valuation_PB Valuation_EV_EBITDA Valuation_DebtToMarketCap
Quarter
FY2016 Q4 2.55 0.81 0.38 0.54 0.04 0.02 NaN NaN NaN NaN NaN NaN NaN NaN 0.27 0.16 NaN NaN 49.76 4.36 2.18 57.26 0.09
FY2017 Q1 2.32 0.61 0.35 0.58 -0.02 -0.01 NaN NaN NaN NaN NaN NaN NaN NaN 0.27 0.16 NaN NaN -84.55 4.79 2.09 163.81 0.09
FY2017 Q2 2.44 0.76 0.36 0.52 0.04 0.03 NaN NaN NaN NaN NaN NaN NaN NaN 0.27 0.16 NaN NaN 46.47 4.59 2.33 58.04 0.08
FY2017 Q3 2.64 1.05 0.81 0.57 0.07 0.05 0.02 0.04 20.33 126.48 18.59 128.22 1.16 1.54 0.24 0.14 -0.12 13.46 22.75 4.35 2.07 36.15 0.08
FY2017 Q4 2.55 0.96 0.58 0.52 -0.17 -0.13 -0.02 -0.03 20.17 107.93 18.58 109.52 1.15 4.01 0.26 0.15 0.90 -7.42 -8.20 4.28 2.03 -35.91 0.09
FY2018 Q1 2.54 0.91 0.68 0.63 0.07 0.04 -0.00 -0.01 20.64 106.11 19.83 106.92 1.12 2.60 0.26 0.15 0.09 2.04 25.30 4.47 1.79 36.74 0.10
FY2018 Q2 2.09 0.99 0.70 0.60 0.12 0.09 0.01 0.02 21.74 119.10 23.05 117.80 1.06 1.91 0.24 0.14 -0.57 10.05 12.54 4.33 2.06 25.80 0.08
FY2018 Q3 2.07 0.86 0.69 0.61 0.12 -0.05 -0.01 -0.02 22.46 120.64 25.49 117.61 1.02 1.68 0.25 0.14 -0.65 13.37 -25.89 5.16 2.49 30.88 0.07
FY2018 Q4 2.24 1.09 0.82 0.59 0.02 0.03 0.03 0.05 21.15 119.14 26.27 114.02 1.00 1.06 0.25 0.14 -0.56 27.88 55.29 5.97 2.64 83.08 0.07
FY2019 Q1 2.27 0.51 0.34 0.64 0.10 0.08 0.04 0.06 19.97 124.04 27.66 116.35 1.02 0.99 0.24 0.14 0.36 31.41 23.62 7.40 3.01 51.06 0.06
FY2019 Q2 2.98 0.79 0.45 0.61 0.13 0.10 0.04 0.07 20.21 141.96 30.49 131.68 1.03 1.07 0.27 0.15 0.43 30.42 16.42 6.61 3.23 39.79 0.06
FY2019 Q3 2.90 0.77 0.53 0.61 0.12 0.07 0.07 0.13 21.21 142.59 27.72 136.07 1.03 1.05 0.28 0.15 0.28 30.49 17.01 4.73 2.43 30.39 0.08
FY2019 Q4 3.00 0.82 0.49 0.60 0.03 0.02 0.07 0.13 20.30 130.26 27.93 122.63 1.05 1.04 0.28 0.15 0.38 29.14 81.05 6.80 3.12 96.49 0.07
FY2020 Q1 2.30 0.59 0.41 0.64 0.11 0.08 0.07 0.14 19.79 135.10 41.42 113.48 0.96 3.00 0.89 0.36 2.27 30.08 18.96 6.22 2.95 50.30 0.08
FY2020 Q2 1.95 0.59 0.32 0.61 0.14 0.11 0.06 0.15 22.18 149.94 52.58 119.54 0.87 3.10 0.97 0.39 2.50 33.70 9.92 4.24 2.48 30.90 0.10
FY2020 Q3 1.94 0.81 0.61 0.62 0.13 0.19 0.09 0.22 23.77 143.92 46.39 121.31 0.87 3.01 0.91 0.38 1.86 37.25 6.56 5.01 2.81 34.71 0.08
FY2020 Q4 1.61 0.91 0.78 0.47 -0.22 -0.20 0.05 0.13 18.56 119.47 37.45 100.58 0.84 5.26 1.20 0.44 2.62 19.55 -5.03 3.93 1.86 -30.94 0.23
FY2021 Q1 2.59 1.72 1.65 0.71 -0.33 -0.26 0.02 0.05 13.49 128.88 33.39 108.99 0.69 13.33 1.55 0.51 5.08 1.31 -9.67 10.13 1.93 -64.96 0.39
FY2021 Q2 2.52 1.52 1.29 0.67 -0.01 -0.03 -0.01 -0.03 17.94 161.66 41.74 137.86 0.61 103.67 1.42 0.47 45.86 -7.23 -33.40 4.38 2.05 150.88 0.31
FY2021 Q3 2.55 1.79 1.56 0.65 0.12 0.08 -0.04 -0.11 30.26 186.54 65.92 150.88 0.55 -111.33 1.38 0.45 -32.61 -7.29 15.50 5.18 2.76 36.27 0.22
FY2021 Q4 2.65 1.91 1.63 0.61 -0.02 -0.06 -0.01 -0.05 34.22 192.65 81.91 144.96 0.55 16.36 1.39 0.46 4.71 -0.54 -30.25 6.97 3.44 245.91 0.18
FY2022 Q1 2.07 1.40 1.23 0.70 0.16 0.12 0.02 0.06 28.25 157.60 73.27 112.58 0.67 5.88 1.30 0.44 1.56 6.90 13.57 6.50 3.29 35.22 0.18
FY2022 Q2 2.05 1.24 1.06 0.68 0.17 0.13 0.05 0.14 25.63 165.97 78.78 112.83 0.69 4.04 1.22 0.43 1.27 11.85 11.15 5.73 3.01 31.59 0.19
FY2022 Q3 1.88 1.11 0.94 0.66 0.16 0.12 0.06 0.18 25.32 167.93 83.56 109.68 0.73 3.52 1.26 0.42 1.19 13.73 9.43 4.52 3.02 27.02 0.20
FY2022 Q4 1.87 1.01 0.83 0.63 0.03 0.02 0.08 0.23 23.95 168.00 81.20 110.74 0.78 3.27 1.34 0.44 1.47 14.97 81.36 5.21 3.13 91.58 0.21
FY2023 Q1 2.05 1.03 0.83 0.67 0.12 0.08 0.08 0.23 21.78 182.80 85.72 118.86 0.86 2.85 1.19 0.40 1.37 14.46 12.88 4.26 2.69 33.71 0.18
FY2023 Q2 2.01 0.93 0.65 0.65 0.13 0.10 0.07 0.22 23.92 200.56 87.15 137.33 0.94 2.93 1.22 0.41 1.76 14.82 9.59 3.65 2.56 28.26 0.20
FY2023 Q3 2.08 1.14 0.90 0.65 0.16 0.12 0.07 0.22 25.95 203.29 78.56 150.68 0.93 2.99 1.14 0.40 1.33 15.30 8.11 3.83 2.85 23.83 0.16
FY2023 Q4 2.23 1.32 1.02 0.62 0.04 0.02 0.08 0.21 24.69 185.06 67.27 142.47 0.93 3.02 1.19 0.42 1.42 18.13 59.96 5.03 3.19 80.17 0.15
FY2024 Q1 2.18 1.23 1.01 0.69 0.12 0.09 0.08 0.22 22.45 182.99 66.42 139.02 0.94 2.95 1.16 0.41 1.28 19.14 15.38 5.43 3.33 39.17 0.14
FY2024 Q2 2.11 1.14 0.86 0.66 0.11 0.09 0.08 0.22 22.65 192.51 73.39 141.77 0.96 2.97 1.17 0.41 1.49 18.03 12.96 4.66 3.21 38.50 0.15
FY2024 Q3 2.17 1.31 1.07 0.67 0.18 0.14 0.09 0.24 23.90 181.28 70.23 134.94 0.96 2.80 1.07 0.39 0.97 19.97 8.47 4.85 3.64 26.04 0.12
FY2024 Q4 2.29 1.44 1.13 0.67 0.08 0.06 0.10 0.26 23.40 162.40 61.73 124.07 0.97 2.55 1.09 0.41 0.97 19.42 33.28 7.70 4.93 73.07 0.09
FY2025 Q1 2.19 1.25 1.01 0.70 0.15 0.11 0.10 0.28 22.47 162.41 67.76 117.11 1.00 2.46 1.13 0.40 1.00 20.10 16.41 7.32 4.67 43.13 0.10
FY2025 Q2 1.72 0.90 0.65 0.67 0.12 0.09 0.10 0.28 24.09 180.67 81.17 123.60 1.00 2.40 1.09 0.39 1.18 20.01 20.80 7.13 5.04 53.31 0.09
FY2025 Q3 1.75 1.07 0.87 0.68 0.18 0.14 0.10 0.28 25.03 174.95 81.05 118.92 1.00 2.30 1.06 0.38 0.64 20.78 12.06 6.69 5.65 33.56 0.08
FY2025 Q4 1.78 1.12 0.90 0.69 0.10 0.08 0.10 0.29 23.07 159.72 75.87 106.91 1.00 2.20 1.03 0.38 0.61 22.56 26.00 7.90 5.18 62.25 0.09
FY2026 Q1 1.84 1.09 0.91 0.72 0.17 0.13 0.11 0.31 21.44 175.65 84.53 112.57 0.98 3.40 1.73 0.56 1.77 23.64 19.12 9.81 6.70 55.41 0.16
FY2026 Q2 2.04 1.07 0.79 0.68 0.12 0.10 0.11 0.34 22.21 194.48 89.41 127.28 1.00 3.04 1.56 0.55 1.95 22.39 23.49 9.70 7.55 73.32 0.14
FY2026 Q3 2.10 1.25 1.02 0.70 0.20 0.15 0.12 0.34 22.96 185.06 82.63 125.38 1.03 2.85 1.39 0.51 1.40 23.25 15.13 9.09 7.58 44.45 0.12
No description has been provided for this image

5) Red-Flag RulesΒΆ

Rules very simple using a buffer on the median to set "maximum bands" if value is above or below it it trigger the flag.

# Defining the buffers
def build_activation_red_flags(
    ratios: pd.DataFrame,
    rolling_window: int | None = None,
    min_periods: int = 3,
    liquidity_buffer: float = 0.1,
    leverage_buffer: float = 0.15,
    coverage_buffer: float = 0.15,
    operating_buffer: float = 0.1,
    ccc_jump_threshold: float = 20.0,
    wc_buffer: float = 0.2,
) -> pd.DataFrame:
    
    r = ratios.copy()
    flags = pd.DataFrame(index=r.index)

    if rolling_window is None:
        rolling_window = 12 if len(r) >= 12 else max(3, len(r) // 2)
    mp = min(min_periods, rolling_window)

    def _get_col(candidates: list[str]) -> pd.Series | None:
        for c in candidates:
            if c in r.columns:
                return pd.to_numeric(r[c], errors="coerce")
        return None

    def _rm(s: pd.Series) -> pd.Series:
        return s.rolling(rolling_window, min_periods=mp).mean()

    def _or_reduce(conds: list[pd.Series]) -> pd.Series:
        if not conds:
            return pd.Series(False, index=r.index)
        arr = [c.fillna(False).astype(bool) for c in conds]
        return pd.Series(np.logical_or.reduce(arr), index=r.index)

    liquidity_conditions = []

    quick = _get_col(["QuickRatio", "Liquidity_QuickRatio"])
    if quick is not None:
        quick_rm = _rm(quick)
        liquidity_conditions.append(quick < quick_rm * (1 - liquidity_buffer))

    current = _get_col(["CurrentRatio", "Liquidity_CurrentRatio"])
    if current is not None:
        current_rm = _rm(current)
        liquidity_conditions.append(current < current_rm * (1 - liquidity_buffer))

    cash = _get_col(["CashRatio", "Liquidity_CashRatio"])
    if cash is not None:
        cash_rm = _rm(cash)
        liquidity_conditions.append(cash < cash_rm * (1 - liquidity_buffer))

    flags["RF1_LiquidityStress"] = _or_reduce(liquidity_conditions)

    # RF2: Leverage Risk
    leverage_conditions = []

    debt_ebitda = _get_col(["DebtEBITDA", "Leverage_DebtToEBITDA"])
    if debt_ebitda is not None:
        debt_ebitda_rm = _rm(debt_ebitda)
        leverage_conditions.append(debt_ebitda > debt_ebitda_rm * (1 + leverage_buffer))

    net_debt_ebitda = _get_col(["NetDebtEBITDA", "Leverage_NetDebtToEBITDA"])
    if net_debt_ebitda is not None:
        nde_rm = _rm(net_debt_ebitda)
        leverage_conditions.append(net_debt_ebitda > nde_rm * (1 + leverage_buffer))

    debt_to_equity = _get_col(["DebtToEquity", "Leverage_DebtToEquity"])
    if debt_to_equity is not None:
        dte_rm = _rm(debt_to_equity)
        leverage_conditions.append(debt_to_equity > dte_rm * (1 + leverage_buffer))

    debt_to_assets = _get_col(["DebtToAssets", "Leverage_DebtToAssets"])
    if debt_to_assets is not None:
        dta_rm = _rm(debt_to_assets)
        leverage_conditions.append(debt_to_assets > dta_rm * (1 + leverage_buffer))

    int_cov = _get_col(["IntCoverage", "Leverage_InterestCoverage"])
    if int_cov is not None:
        int_cov_rm = _rm(int_cov)
        leverage_conditions.append(int_cov < int_cov_rm * (1 - coverage_buffer))

    flags["RF2_LeverageRisk"] = _or_reduce(leverage_conditions)

    # RF3: Operating Deterioration
    op_conditions = []
    for candidates in [["GrossMargin", "Profitability_GrossMargin"],
                       ["EBITMargin", "Profitability_EBITMargin"],
                       ["ROA", "Profitability_ROA"]]:
        s = _get_col(candidates)
        if s is None:
            continue
        s_rm = _rm(s)
        op_conditions.append((s < s_rm * (1 - operating_buffer)) & (s.diff() < 0))

    flags["RF3_OperatingDeterioration"] = _or_reduce(op_conditions)

    # RF4: Working Capital Shock
    wc_conditions = []

    ccc = _get_col(["CCC", "Efficiency_CCC"])
    if ccc is not None:
        ccc_rm = _rm(ccc)
        wc_conditions.append((ccc.diff() > ccc_jump_threshold) | (ccc > ccc_rm * (1 + wc_buffer)))

    inv_days = _get_col(["InventoryDays", "Efficiency_InventoryDays"])
    if inv_days is not None:
        inv_rm = _rm(inv_days)
        wc_conditions.append((inv_days > inv_rm * (1 + wc_buffer)) & (inv_days.diff() > 0))

    ar_days = _get_col(["ARDays", "Efficiency_ARDays"])
    if ar_days is not None:
        ar_rm = _rm(ar_days)
        wc_conditions.append((ar_days > ar_rm * (1 + wc_buffer)) & (ar_days.diff() > 0))

    flags["RF4_WorkingCapitalShock"] = _or_reduce(wc_conditions)

    return flags


flags = build_activation_red_flags(ratio_matrix)
# Making the Redflag text and identification
driver_text = {
    "RF1_LiquidityStress": "Liquidity ratios are below buffered rolling norms, indicating tighter near-term funding flexibility.",
    "RF2_LeverageRisk": "Leverage metrics are above buffered rolling norms and/or coverage is below buffered norms.",
    "RF3_OperatingDeterioration": "Core operating metrics are below buffered rolling trend and still moving down.",
    "RF4_WorkingCapitalShock": "CCC and/or working-capital days are rising above buffered rolling baseline (cash conversion drag).",
}

# Activating the flags
activated_flags = (
    flags[flags.any(axis=1)]
    .stack()
    .reset_index()
    .rename(columns={"level_0": "Year", "level_1": "Rule", 0: "Triggered"})
)
activated_flags = activated_flags[activated_flags["Triggered"]].drop(columns="Triggered")
if len(activated_flags) > 0:
    activated_flags["Driver"] = activated_flags["Rule"].map(driver_text)

# printing the outputs
print("Flag matrix (rolling-mean + buffer rules):")
display(flags)

print()
print("Activated flags with drivers:")
if len(activated_flags) == 0:
    print("No flags triggered under current thresholds.")
else:
    display(activated_flags)
    
# Created a flag count to help us in the executive summary
flag_counts = flags.sum(axis=1).rename("RedFlagCount")
display(flag_counts.to_frame())
x = flag_counts.sum()
print(f"Total Red flags activated: {x}")
Flag matrix (rolling-mean + buffer rules):
RF1_LiquidityStress RF2_LeverageRisk RF3_OperatingDeterioration RF4_WorkingCapitalShock
Quarter
FY2016 Q4 False False False False
FY2017 Q1 False False False False
FY2017 Q2 False False False False
FY2017 Q3 False False False False
FY2017 Q4 False False True False
FY2018 Q1 False True False False
FY2018 Q2 True False False False
FY2018 Q3 True False True False
FY2018 Q4 False False True False
FY2019 Q1 True True False False
FY2019 Q2 True True False False
FY2019 Q3 False True False False
FY2019 Q4 True True True False
FY2020 Q1 True True False False
FY2020 Q2 True True False False
FY2020 Q3 True True False False
FY2020 Q4 True True True False
FY2021 Q1 False True True False
FY2021 Q2 False True True True
FY2021 Q3 False True True True
FY2021 Q4 False True True True
FY2022 Q1 True True False False
FY2022 Q2 True True False False
FY2022 Q3 True False False False
FY2022 Q4 True False True False
FY2023 Q1 True False False False
FY2023 Q2 True False False True
FY2023 Q3 True False False True
FY2023 Q4 False False True False
FY2024 Q1 False False False False
FY2024 Q2 True True False False
FY2024 Q3 False False False False
FY2024 Q4 False False True False
FY2025 Q1 False False False False
FY2025 Q2 True False False False
FY2025 Q3 True False False False
FY2025 Q4 True False True False
FY2026 Q1 False True False False
FY2026 Q2 True True False False
FY2026 Q3 False True False False
Activated flags with drivers:
Quarter Rule Driver
2 FY2017 Q4 RF3_OperatingDeterioration Core operating metrics are below buffered roll...
5 FY2018 Q1 RF2_LeverageRisk Leverage metrics are above buffered rolling no...
8 FY2018 Q2 RF1_LiquidityStress Liquidity ratios are below buffered rolling no...
12 FY2018 Q3 RF1_LiquidityStress Liquidity ratios are below buffered rolling no...
14 FY2018 Q3 RF3_OperatingDeterioration Core operating metrics are below buffered roll...
18 FY2018 Q4 RF3_OperatingDeterioration Core operating metrics are below buffered roll...
20 FY2019 Q1 RF1_LiquidityStress Liquidity ratios are below buffered rolling no...
21 FY2019 Q1 RF2_LeverageRisk Leverage metrics are above buffered rolling no...
24 FY2019 Q2 RF1_LiquidityStress Liquidity ratios are below buffered rolling no...
25 FY2019 Q2 RF2_LeverageRisk Leverage metrics are above buffered rolling no...
29 FY2019 Q3 RF2_LeverageRisk Leverage metrics are above buffered rolling no...
32 FY2019 Q4 RF1_LiquidityStress Liquidity ratios are below buffered rolling no...
33 FY2019 Q4 RF2_LeverageRisk Leverage metrics are above buffered rolling no...
34 FY2019 Q4 RF3_OperatingDeterioration Core operating metrics are below buffered roll...
36 FY2020 Q1 RF1_LiquidityStress Liquidity ratios are below buffered rolling no...
37 FY2020 Q1 RF2_LeverageRisk Leverage metrics are above buffered rolling no...
40 FY2020 Q2 RF1_LiquidityStress Liquidity ratios are below buffered rolling no...
41 FY2020 Q2 RF2_LeverageRisk Leverage metrics are above buffered rolling no...
44 FY2020 Q3 RF1_LiquidityStress Liquidity ratios are below buffered rolling no...
45 FY2020 Q3 RF2_LeverageRisk Leverage metrics are above buffered rolling no...
48 FY2020 Q4 RF1_LiquidityStress Liquidity ratios are below buffered rolling no...
49 FY2020 Q4 RF2_LeverageRisk Leverage metrics are above buffered rolling no...
50 FY2020 Q4 RF3_OperatingDeterioration Core operating metrics are below buffered roll...
53 FY2021 Q1 RF2_LeverageRisk Leverage metrics are above buffered rolling no...
54 FY2021 Q1 RF3_OperatingDeterioration Core operating metrics are below buffered roll...
57 FY2021 Q2 RF2_LeverageRisk Leverage metrics are above buffered rolling no...
58 FY2021 Q2 RF3_OperatingDeterioration Core operating metrics are below buffered roll...
59 FY2021 Q2 RF4_WorkingCapitalShock CCC and/or working-capital days are rising abo...
61 FY2021 Q3 RF2_LeverageRisk Leverage metrics are above buffered rolling no...
62 FY2021 Q3 RF3_OperatingDeterioration Core operating metrics are below buffered roll...
63 FY2021 Q3 RF4_WorkingCapitalShock CCC and/or working-capital days are rising abo...
65 FY2021 Q4 RF2_LeverageRisk Leverage metrics are above buffered rolling no...
66 FY2021 Q4 RF3_OperatingDeterioration Core operating metrics are below buffered roll...
67 FY2021 Q4 RF4_WorkingCapitalShock CCC and/or working-capital days are rising abo...
68 FY2022 Q1 RF1_LiquidityStress Liquidity ratios are below buffered rolling no...
69 FY2022 Q1 RF2_LeverageRisk Leverage metrics are above buffered rolling no...
72 FY2022 Q2 RF1_LiquidityStress Liquidity ratios are below buffered rolling no...
73 FY2022 Q2 RF2_LeverageRisk Leverage metrics are above buffered rolling no...
76 FY2022 Q3 RF1_LiquidityStress Liquidity ratios are below buffered rolling no...
80 FY2022 Q4 RF1_LiquidityStress Liquidity ratios are below buffered rolling no...
82 FY2022 Q4 RF3_OperatingDeterioration Core operating metrics are below buffered roll...
84 FY2023 Q1 RF1_LiquidityStress Liquidity ratios are below buffered rolling no...
88 FY2023 Q2 RF1_LiquidityStress Liquidity ratios are below buffered rolling no...
91 FY2023 Q2 RF4_WorkingCapitalShock CCC and/or working-capital days are rising abo...
92 FY2023 Q3 RF1_LiquidityStress Liquidity ratios are below buffered rolling no...
95 FY2023 Q3 RF4_WorkingCapitalShock CCC and/or working-capital days are rising abo...
98 FY2023 Q4 RF3_OperatingDeterioration Core operating metrics are below buffered roll...
100 FY2024 Q2 RF1_LiquidityStress Liquidity ratios are below buffered rolling no...
101 FY2024 Q2 RF2_LeverageRisk Leverage metrics are above buffered rolling no...
106 FY2024 Q4 RF3_OperatingDeterioration Core operating metrics are below buffered roll...
108 FY2025 Q2 RF1_LiquidityStress Liquidity ratios are below buffered rolling no...
112 FY2025 Q3 RF1_LiquidityStress Liquidity ratios are below buffered rolling no...
116 FY2025 Q4 RF1_LiquidityStress Liquidity ratios are below buffered rolling no...
118 FY2025 Q4 RF3_OperatingDeterioration Core operating metrics are below buffered roll...
121 FY2026 Q1 RF2_LeverageRisk Leverage metrics are above buffered rolling no...
124 FY2026 Q2 RF1_LiquidityStress Liquidity ratios are below buffered rolling no...
125 FY2026 Q2 RF2_LeverageRisk Leverage metrics are above buffered rolling no...
129 FY2026 Q3 RF2_LeverageRisk Leverage metrics are above buffered rolling no...
RedFlagCount
Quarter
FY2016 Q4 0
FY2017 Q1 0
FY2017 Q2 0
FY2017 Q3 0
FY2017 Q4 1
FY2018 Q1 1
FY2018 Q2 1
FY2018 Q3 2
FY2018 Q4 1
FY2019 Q1 2
FY2019 Q2 2
FY2019 Q3 1
FY2019 Q4 3
FY2020 Q1 2
FY2020 Q2 2
FY2020 Q3 2
FY2020 Q4 3
FY2021 Q1 2
FY2021 Q2 3
FY2021 Q3 3
FY2021 Q4 3
FY2022 Q1 2
FY2022 Q2 2
FY2022 Q3 1
FY2022 Q4 2
FY2023 Q1 1
FY2023 Q2 2
FY2023 Q3 2
FY2023 Q4 1
FY2024 Q1 0
FY2024 Q2 2
FY2024 Q3 0
FY2024 Q4 1
FY2025 Q1 0
FY2025 Q2 1
FY2025 Q3 1
FY2025 Q4 2
FY2026 Q1 1
FY2026 Q2 2
FY2026 Q3 1
Total Red flags activated: 58

Going above and beyondΒΆ

Nice visuals to wrap up our analysis

# Defining the function to plor possitive only for better analysis

def mountain_plot_positive_only(cs_df: pd.DataFrame, cols: list[str], title: str, y_label: str, percent: bool = True):
    plot_df = cs_df[cols].copy().apply(pd.to_numeric, errors="coerce")

    keep, dropped = [], []
    for c in plot_df.columns:
        s = plot_df[c].dropna()
        if s.empty:
            dropped.append(c)
            continue
        all_nonneg = (s >= 0).all()
        all_nonpos = (s <= 0).all()
        if all_nonneg or all_nonpos:
            keep.append(c)
        else:
            dropped.append(c)

    plot_df = plot_df[keep]

    for c in plot_df.columns:
        if (plot_df[c].dropna() <= 0).all():
            plot_df[c] = -plot_df[c]

    if percent:
        plot_df = plot_df * 100

    if plot_df.shape[1] == 0:
        print(f"Skipped '{title}': no single-sign columns available for stacking.")
        return

    # Plotting
    ax = plot_df.plot(kind="area", stacked=True, figsize=(13, 5), alpha=0.85)
    plt.title(title)
    plt.ylabel(y_label)
    plt.xlabel("Fiscal Quarter")
    plt.xticks(rotation=45, ha="right")
    plt.legend(loc="upper left")
    plt.tight_layout()
    plt.show()

    if dropped:
        print("Dropped (mixed-sign/empty columns):", dropped)


# Preping the first mountain graph to plot
is_cs_plot = pd.DataFrame(index=tidy.index)
for col in ["COGS", "OperatingExpenses"]:
    if col in tidy.columns:
        is_cs_plot[col] = tidy[col] / tidy["Revenue"].replace(0, np.nan)

if not is_cs_plot.empty:
    mountain_plot_positive_only(
        cs_df=is_cs_plot,
        cols=list(is_cs_plot.columns),
        title="Quarterly Income Statement Expense Intensity (Mountain)",
        y_label="% of Revenue",
        percent=True,
    )

# Preping the margin plot
margin_overlay = pd.DataFrame(index=tidy.index)
if {"GrossProfit", "Revenue"}.issubset(tidy.columns):
    margin_overlay["Gross Margin"] = tidy["GrossProfit"] / tidy["Revenue"].replace(0, np.nan)
if {"OperatingIncome", "Revenue"}.issubset(tidy.columns):
    margin_overlay["Operating Margin"] = tidy["OperatingIncome"] / tidy["Revenue"].replace(0, np.nan)
if {"NetIncome", "Revenue"}.issubset(tidy.columns):
    margin_overlay["Net Margin"] = tidy["NetIncome"] / tidy["Revenue"].replace(0, np.nan)

if not is_cs_plot.empty:
    ax = (is_cs_plot.abs() * 100).plot(kind="area", stacked=True, figsize=(13, 5), alpha=0.75)
    if not margin_overlay.empty:
        (margin_overlay * 100).plot(ax=ax, linewidth=2)
    plt.title("Quarterly Expense Mountain + Margin Trend Lines")
    plt.ylabel("% of Revenue")
    plt.xlabel("Fiscal Quarter")
    plt.xticks(rotation=45, ha="right")
    plt.legend(loc="upper left")
    plt.tight_layout()
    plt.show()

# Preping the bs mountain graph to plot
bs_cs_plot = pd.DataFrame(index=tidy.index)
for col in ["Cash", "AccountsReceivable", "Inventory", "PPENet"]:
    if col in tidy.columns:
        bs_cs_plot[col] = tidy[col] / tidy["TotalAssets"].replace(0, np.nan)

if not bs_cs_plot.empty:
    mountain_plot_positive_only(
        cs_df=bs_cs_plot,
        cols=list(bs_cs_plot.columns),
        title="Quarterly Balance Sheet Asset Mix (Mountain)",
        y_label="% of Total Assets",
        percent=True,
    )

# Preping the cf mountain graph to plot
cf_cs_plot = pd.DataFrame(index=tidy.index)
if "OperatingCashFlow" in tidy.columns:
    cf_cs_plot["OperatingCashFlow"] = tidy["OperatingCashFlow"] / tidy["Revenue"].replace(0, np.nan)
if "CapEx" in tidy.columns:
    cf_cs_plot["CapEx_Outflow"] = -tidy["CapEx"] / tidy["Revenue"].replace(0, np.nan)
if "FreeCashFlow" in tidy.columns:
    cf_cs_plot["FreeCashFlow"] = tidy["FreeCashFlow"] / tidy["Revenue"].replace(0, np.nan)

if not cf_cs_plot.empty:
    mountain_plot_positive_only(
        cs_df=cf_cs_plot,
        cols=list(cf_cs_plot.columns),
        title="Quarterly Cash-Flow Components (Mountain)",
        y_label="% of Revenue",
        percent=True,
    )
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
Dropped (mixed-sign/empty columns): ['OperatingCashFlow', 'FreeCashFlow']

Export for control purposeΒΆ

Not required in the deliverable. However we can keep track of the work and use as inputs for future projects

# Export key deliverables
(out_dir / "unit1_common_size_income_pct.csv").write_text(common_is.round(4).to_csv())
(out_dir / "unit1_common_size_balance_pct.csv").write_text(common_bs.round(4).to_csv())
(out_dir / "unit1_trend_diagnostics.csv").write_text(trend.round(4).to_csv())
(out_dir / "unit1_ratio_matrix.csv").write_text(ratio_matrix.round(6).to_csv())
(out_dir / "unit1_red_flags_matrix.csv").write_text(flags.to_csv())
(out_dir / "unit1_red_flags_activated.csv").write_text(activated_flags.to_csv(index=False))

quarterly_pack_path = QUARTERLY_EXPORT_DIR / "RL_quarterly_historical_pack_usdm.csv"
quarterly_audit_path = QUARTERLY_EXPORT_DIR / "RL_quarterly_historical_audit.csv"
tidy.to_csv(quarterly_pack_path)
audit.to_csv(quarterly_audit_path)

print("Export complete. Files saved in:", out_dir)
print("Quarterly historical pack:", quarterly_pack_path)
print("Quarterly audit table    :", quarterly_audit_path)
Export complete. Files saved in: /Users/theomachado/Library/CloudStorage/OneDrive-UniversityofFlorida/Spring 2026/FIN4453 - OFC/Mini-projects/outputs
Quarterly historical pack: /Users/theomachado/Library/CloudStorage/OneDrive-UniversityofFlorida/Spring 2026/FIN4453 - OFC/DATA/csv_export/RL_quarterly_historical_pack_usdm.csv
Quarterly audit table    : /Users/theomachado/Library/CloudStorage/OneDrive-UniversityofFlorida/Spring 2026/FIN4453 - OFC/DATA/csv_export/RL_quarterly_historical_audit.csv