Module 4 Mini-Project: Constructing the Cash Flow Statement¶
Company: Ralph lauren ticker: RL Analyst: Theo and Olivia
- Dr. Mascio please note: We left a output section for control the generated data and use in the loading for final project in section 9 and as highlighted the need to go "Above and beyond" by you and luke I added a graph to better visualize the results and delta cash in the very end of the project. Thank you.
1. Setup and data loading¶
Here we are going to load the DATA from the raw CSV and set the number to mm
from pathlib import Path
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from IPython.display import Markdown, display
pd.set_option("display.max_columns", 200)
pd.set_option("display.width", 220)
pd.set_option("display.float_format", lambda x: f"{x:,.2f}")
#Defining MM
USD_MM = 1_000_000.0
#Finding the csv in the correct path of raising error if not found
def locate_base_dir(start: Path | None = None) -> Path:
start = start or Path.cwd()
for path in [start, *start.parents]:
if (path / "DATA" / "csv_export").exists():
return path
raise FileNotFoundError("Could not locate DATA/csv_export from the current workspace.")
#Setting numbers to MM
def to_usd_mm(df: pd.DataFrame, exclude_cols: list[str] | None = None) -> pd.DataFrame:
out = df.copy()
exclude_cols = set(exclude_cols or [])
numeric_cols = [
col for col in out.select_dtypes(include=[np.number]).columns
if col not in exclude_cols
]
out = out.astype({col: "float64" for col in numeric_cols})
out.loc[:, numeric_cols] = out.loc[:, numeric_cols] / USD_MM
return out
BASE_DIR = locate_base_dir()
CSV_DIR = BASE_DIR / "DATA" / "csv_export"
#Setting the outputs direction
OUT_DIR = BASE_DIR / "Mini-projects" / "Module 4" / "outputs"
OUT_DIR.mkdir(parents=True, exist_ok=True)
#Here we are cleaning the data and preping the statments
def prep_raw_statement(path: Path) -> pd.DataFrame:
df = pd.read_csv(path, parse_dates=["date"])
df["fiscalYear"] = pd.to_numeric(df["fiscalYear"], errors="coerce").astype("Int64")
df = df.dropna(subset=["fiscalYear"]).copy()
df["fiscalYear"] = df["fiscalYear"].astype(int)
df["period"] = df["period"].astype(str).str.upper()
df["PeriodLabel"] = "FY" + df["fiscalYear"].astype(str) + "-" + df["period"]
return df.sort_values("date")
is_raw = prep_raw_statement(CSV_DIR / "RL_income_statement.csv")
bs_raw = prep_raw_statement(CSV_DIR / "RL_balance_sheet.csv")
cf_raw = prep_raw_statement(CSV_DIR / "RL_cash_flow.csv")
print("Loaded raw tables:")
print("Income statement:", is_raw.shape)
print("Balance sheet :", bs_raw.shape)
print("Cash flow :", cf_raw.shape)
#output should all be "40" for the amount of quarters we have on the csvs
Loaded raw tables: Income statement: (40, 40) Balance sheet : (40, 62) Cash flow : (40, 48)
2. Fixing timeline¶
Ensuring the timeline of all statments are correctly organized and linked so FCF does't break it
#data cleaning and aligment of the periods
common_timeline = (
is_raw[["date", "fiscalYear", "period", "PeriodLabel"]]
.merge(bs_raw[["date", "fiscalYear", "period", "PeriodLabel"]], on=["date", "fiscalYear", "period", "PeriodLabel"])
.merge(cf_raw[["date", "fiscalYear", "period", "PeriodLabel"]], on=["date", "fiscalYear", "period", "PeriodLabel"])
.sort_values("date")
.reset_index(drop=True)
)
model_timeline = common_timeline.iloc[1:].copy().reset_index(drop=True)
periods = model_timeline["PeriodLabel"].tolist()
display(model_timeline.head(10))
print("Modeled periods:", model_timeline.iloc[0]["PeriodLabel"], "to", model_timeline.iloc[-1]["PeriodLabel"])
print("Number of modeled quarters:", len(model_timeline))
# displaying first 10 to ensure correctness
| date | fiscalYear | period | PeriodLabel | |
|---|---|---|---|---|
| 0 | 2016-07-02 | 2017 | Q1 | FY2017-Q1 |
| 1 | 2016-10-01 | 2017 | Q2 | FY2017-Q2 |
| 2 | 2016-12-31 | 2017 | Q3 | FY2017-Q3 |
| 3 | 2017-04-01 | 2017 | Q4 | FY2017-Q4 |
| 4 | 2017-07-01 | 2018 | Q1 | FY2018-Q1 |
| 5 | 2017-09-30 | 2018 | Q2 | FY2018-Q2 |
| 6 | 2017-12-30 | 2018 | Q3 | FY2018-Q3 |
| 7 | 2018-03-31 | 2018 | Q4 | FY2018-Q4 |
| 8 | 2018-06-30 | 2019 | Q1 | FY2019-Q1 |
| 9 | 2018-09-29 | 2019 | Q2 | FY2019-Q2 |
Modeled periods: FY2017-Q1 to FY2026-Q3 Number of modeled quarters: 39
3. Align¶
Since FCF needs all the statments to be aligned with each other we are doing it here, making sure all index are the same across the dataframes
is_q = is_raw.set_index("PeriodLabel").loc[periods].copy()
cf_q = cf_raw.set_index("PeriodLabel").loc[periods].copy()
bs_all = bs_raw.set_index("PeriodLabel").loc[common_timeline["PeriodLabel"]].copy()
bs_q = bs_raw.set_index("PeriodLabel").loc[periods].copy()
prev_bs_q = bs_all.shift(1).loc[periods].copy()
timeline_index = model_timeline.set_index("PeriodLabel")[["date", "fiscalYear", "period"]].copy()
timeline_index.index.name = "Period"
print("Aligned shapes:")
print("is_q :", is_q.shape)
print("bs_q :", bs_q.shape)
print("prev_bs_q:", prev_bs_q.shape)
print("cf_q :", cf_q.shape)
Aligned shapes: is_q : (39, 39) bs_q : (39, 61) prev_bs_q: (39, 61) cf_q : (39, 47)
4. Quarterly base table¶
Here we linking all the 3 statments to a single dataframe so is all in a single dictonary to be referenced. Me and Olivia found easier to work in a single dataframe than linking 3 different tables for calculations
quarterly_inputs = timeline_index.copy()
quarterly_inputs["revenue"] = is_q["revenue"].values
quarterly_inputs["ebit"] = is_q["ebit"].values
quarterly_inputs["incomeBeforeTax"] = is_q["incomeBeforeTax"].values
quarterly_inputs["incomeTaxExpense"] = is_q["incomeTaxExpense"].values
quarterly_inputs["interestExpense"] = is_q["interestExpense"].values
quarterly_inputs["netIncome"] = is_q["netIncome"].values
quarterly_inputs["depreciationAndAmortization"] = cf_q["depreciationAndAmortization"].values
quarterly_inputs["deferredIncomeTax"] = cf_q["deferredIncomeTax"].values
quarterly_inputs["stockBasedCompensation"] = cf_q["stockBasedCompensation"].values
quarterly_inputs["changeInWorkingCapital"] = cf_q["changeInWorkingCapital"].values
quarterly_inputs["otherNonCashItems"] = cf_q["otherNonCashItems"].values
quarterly_inputs["capitalExpenditure"] = cf_q["capitalExpenditure"].values
quarterly_inputs["netDebtIssuance"] = cf_q["netDebtIssuance"].values
quarterly_inputs["netDividendsPaid"] = cf_q["netDividendsPaid"].values
quarterly_inputs["cash_begin_reported"] = prev_bs_q["cashAndCashEquivalents"].values
quarterly_inputs["cash_end_reported"] = bs_q["cashAndCashEquivalents"].values
quarterly_inputs["delta_cash_from_bs"] = quarterly_inputs["cash_end_reported"] - quarterly_inputs["cash_begin_reported"]
quarterly_inputs_usdm = to_usd_mm(quarterly_inputs, exclude_cols=["fiscalYear"])
display(quarterly_inputs_usdm.head(8).round(1))
#making sure the dataframe was correctly built
| date | fiscalYear | period | revenue | ebit | incomeBeforeTax | incomeTaxExpense | interestExpense | netIncome | depreciationAndAmortization | deferredIncomeTax | stockBasedCompensation | changeInWorkingCapital | otherNonCashItems | capitalExpenditure | netDebtIssuance | netDividendsPaid | cash_begin_reported | cash_end_reported | delta_cash_from_bs | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Period | ||||||||||||||||||||
| FY2017-Q1 | 2016-07-02 | 2017 | Q1 | 1,552.00 | -29.80 | -33.00 | -10.90 | 3.40 | -22.00 | 78.00 | 3.00 | 18.00 | 81.00 | 85.00 | -78.00 | -33.30 | -41.00 | 456.30 | 457.00 | 0.70 |
| FY2017-Q2 | 2016-10-01 | 2017 | Q2 | 1,821.00 | 77.80 | 73.00 | 28.00 | 4.10 | 45.00 | 76.00 | -12.00 | 14.00 | -244.00 | 110.00 | -87.00 | -1.10 | -41.00 | 457.00 | 434.00 | -23.00 |
| FY2017-Q3 | 2016-12-31 | 2017 | Q3 | 1,714.00 | 126.70 | 124.00 | 41.80 | 3.60 | 82.00 | 78.00 | 19.00 | 14.00 | 391.00 | 34.00 | -60.00 | -101.10 | -42.00 | 434.00 | 928.00 | 494.00 |
| FY2017-Q4 | 2017-04-01 | 2017 | Q4 | 1,565.80 | -267.50 | -268.90 | -64.60 | 1.40 | -204.30 | 75.50 | -48.90 | 17.60 | 5.60 | 256.80 | -59.00 | -8.40 | -40.80 | 928.00 | 668.30 | -259.70 |
| FY2018-Q1 | 2017-07-01 | 2018 | Q1 | 1,347.10 | 91.80 | 86.80 | 27.30 | 5.00 | 59.50 | 72.90 | -14.70 | 21.60 | 182.50 | 12.40 | -41.90 | -6.20 | -40.50 | 668.30 | 830.40 | 162.10 |
| FY2018-Q2 | 2017-09-30 | 2018 | Q2 | 1,664.20 | 195.80 | 191.20 | 47.40 | 4.60 | 143.80 | 73.80 | -10.60 | 17.80 | -136.20 | 14.20 | -32.80 | -8.00 | -40.60 | 830.40 | 1,111.60 | 281.20 |
| FY2018-Q3 | 2017-12-30 | 2018 | Q3 | 1,641.80 | 191.10 | 186.30 | 268.10 | 4.80 | -81.80 | 72.70 | 17.30 | 16.90 | 479.20 | 9.80 | -48.30 | -7.00 | -40.60 | 1,111.60 | 1,175.70 | 64.10 |
| FY2018-Q4 | 2018-03-31 | 2018 | Q4 | 1,529.20 | 28.70 | 24.90 | -16.40 | 3.80 | 41.30 | 75.80 | 92.10 | 18.20 | -236.50 | 33.10 | -38.60 | -7.00 | -40.70 | 1,175.70 | 1,304.60 | 128.90 |
5. Working-capital bridge¶
Here we are following the sign logic directly from the quarter-over-quarter balance sheet changes.
working_capital_bridge = timeline_index.copy()
# we create the dataframe columns
wc_cols = [
"netReceivables",
"inventory",
"prepaids",
"otherCurrentAssets",
"accountPayables",
"accruedExpenses",
"deferredRevenue",
"taxPayables",
"otherCurrentLiabilities",
]
#function to get the true delta (unsure if this is the optimal way)
for col in wc_cols:
working_capital_bridge[f"delta_{col}"] = bs_q[col].values - prev_bs_q[col].values
working_capital_bridge["wc_adjustment_from_bs"] = (
-working_capital_bridge["delta_netReceivables"]
- working_capital_bridge["delta_inventory"]
- working_capital_bridge["delta_prepaids"]
- working_capital_bridge["delta_otherCurrentAssets"]
+ working_capital_bridge["delta_accountPayables"]
+ working_capital_bridge["delta_accruedExpenses"]
+ working_capital_bridge["delta_deferredRevenue"]
+ working_capital_bridge["delta_taxPayables"]
+ working_capital_bridge["delta_otherCurrentLiabilities"]
)
working_capital_bridge["changeInWorkingCapital"] = cf_q["changeInWorkingCapital"].values
working_capital_bridge["wc_scope_gap"] = (
working_capital_bridge["changeInWorkingCapital"] - working_capital_bridge["wc_adjustment_from_bs"]
)
#Display making sure is correct and years aligned
working_capital_bridge_usdm = to_usd_mm(working_capital_bridge, exclude_cols=["fiscalYear"])
display(working_capital_bridge_usdm[["fiscalYear", "period", "wc_adjustment_from_bs", "changeInWorkingCapital", "wc_scope_gap"]].head(12).round(1))
| fiscalYear | period | wc_adjustment_from_bs | changeInWorkingCapital | wc_scope_gap | |
|---|---|---|---|---|---|
| Period | |||||
| FY2017-Q1 | 2017 | Q1 | 146.90 | 81.00 | -65.90 |
| FY2017-Q2 | 2017 | Q2 | -13.00 | -244.00 | -231.00 |
| FY2017-Q3 | 2017 | Q3 | 232.00 | 391.00 | 159.00 |
| FY2017-Q4 | 2017 | Q4 | 215.30 | 5.60 | -209.70 |
| FY2018-Q1 | 2018 | Q1 | -60.70 | 182.50 | 243.20 |
| FY2018-Q2 | 2018 | Q2 | -124.00 | -136.20 | -12.20 |
| FY2018-Q3 | 2018 | Q3 | 341.00 | 479.20 | 138.20 |
| FY2018-Q4 | 2018 | Q4 | -171.30 | -236.50 | -65.20 |
| FY2019-Q1 | 2019 | Q1 | -16.40 | 15.40 | 31.80 |
| FY2019-Q2 | 2019 | Q2 | -319.70 | -293.00 | 26.70 |
| FY2019-Q3 | 2019 | Q3 | 205.20 | 234.80 | 29.60 |
| FY2019-Q4 | 2019 | Q4 | 14.00 | -34.20 | -48.20 |
6. Cash flow statement build¶
Here we used the same logic from the two cells up. We ties everything that is from different dataframes into a single larger one and make the calculations that are needed to find the CFO, CFI and CFF
cash_flow_statement = timeline_index.copy()
#Creating a single dataframe to work
cash_flow_statement["netIncome"] = is_q["netIncome"].values
cash_flow_statement["depreciationAndAmortization"] = cf_q["depreciationAndAmortization"].values
cash_flow_statement["deferredIncomeTax"] = cf_q["deferredIncomeTax"].values
cash_flow_statement["stockBasedCompensation"] = cf_q["stockBasedCompensation"].values
cash_flow_statement["changeInWorkingCapital"] = cf_q["changeInWorkingCapital"].values
cash_flow_statement["otherNonCashItems"] = cf_q["otherNonCashItems"].values
cash_flow_statement["reclassifications"] = 0.0
#Making CFO calculation
cash_flow_statement["CFO"] = (
cash_flow_statement["netIncome"]
+ cash_flow_statement["depreciationAndAmortization"]
+ cash_flow_statement["deferredIncomeTax"]
+ cash_flow_statement["stockBasedCompensation"]
+ cash_flow_statement["changeInWorkingCapital"]
+ cash_flow_statement["otherNonCashItems"]
+ cash_flow_statement["reclassifications"]
)
#Appending what is needed for CFI and making the calculations by "hand"
cash_flow_statement["capitalExpenditure"] = cf_q["capitalExpenditure"].values
cash_flow_statement["acquisitionsNet"] = cf_q["acquisitionsNet"].values
cash_flow_statement["purchasesOfInvestments"] = cf_q["purchasesOfInvestments"].values
cash_flow_statement["salesMaturitiesOfInvestments"] = cf_q["salesMaturitiesOfInvestments"].values
cash_flow_statement["otherInvestingActivities"] = cf_q["otherInvestingActivities"].values
cash_flow_statement["CFI"] = (
cash_flow_statement["capitalExpenditure"]
+ cash_flow_statement["acquisitionsNet"]
+ cash_flow_statement["purchasesOfInvestments"]
+ cash_flow_statement["salesMaturitiesOfInvestments"]
+ cash_flow_statement["otherInvestingActivities"]
)
#Repeat for CFF
cash_flow_statement["netDebtIssuance"] = cf_q["netDebtIssuance"].values
cash_flow_statement["netCommonStockIssuance"] = cf_q["netCommonStockIssuance"].values
cash_flow_statement["netPreferredStockIssuance"] = cf_q["netPreferredStockIssuance"].values
cash_flow_statement["netDividendsPaid"] = cf_q["netDividendsPaid"].values
cash_flow_statement["otherFinancingActivities"] = cf_q["otherFinancingActivities"].values
cash_flow_statement["CFF"] = (
cash_flow_statement["netDebtIssuance"]
+ cash_flow_statement["netCommonStockIssuance"]
+ cash_flow_statement["netPreferredStockIssuance"]
+ cash_flow_statement["netDividendsPaid"]
+ cash_flow_statement["otherFinancingActivities"]
)
#Putting everything together to find the cash delta
cash_flow_statement["cash_begin"] = prev_bs_q["cashAndCashEquivalents"].values
cash_flow_statement["cash_end_reported_bs"] = bs_q["cashAndCashEquivalents"].values
cash_flow_statement["effectOfForexChangesOnCash"] = cf_q["effectOfForexChangesOnCash"].values
cash_flow_statement["cash_definition_gap"] = (
cash_flow_statement["cash_end_reported_bs"]
- cash_flow_statement["cash_begin"]
- cash_flow_statement["CFO"]
- cash_flow_statement["CFI"]
- cash_flow_statement["CFF"]
- cash_flow_statement["effectOfForexChangesOnCash"]
)
#Not forgetting the other as mentioned in chapters PDF
cash_flow_statement["other_cash"] = (
cash_flow_statement["effectOfForexChangesOnCash"]
+ cash_flow_statement["cash_definition_gap"]
)
cash_flow_statement["delta_cash"] = (
cash_flow_statement["CFO"]
+ cash_flow_statement["CFI"]
+ cash_flow_statement["CFF"]
+ cash_flow_statement["other_cash"]
)
cash_flow_statement["cash_end"] = cash_flow_statement["cash_begin"] + cash_flow_statement["delta_cash"]
cash_flow_statement_usdm = to_usd_mm(cash_flow_statement, exclude_cols=["fiscalYear"])
display(cash_flow_statement_usdm[["fiscalYear", "period", "CFO", "CFI", "CFF", "other_cash", "delta_cash", "cash_begin", "cash_end"]].head(12).round(1))
| fiscalYear | period | CFO | CFI | CFF | other_cash | delta_cash | cash_begin | cash_end | |
|---|---|---|---|---|---|---|---|---|---|
| Period | |||||||||
| FY2017-Q1 | 2017 | Q1 | 243.00 | -41.00 | -185.60 | -15.70 | 0.70 | 456.30 | 457.00 |
| FY2017-Q2 | 2017 | Q2 | -11.00 | 28.30 | -41.30 | 1.00 | -23.00 | 457.00 | 434.00 |
| FY2017-Q3 | 2017 | Q3 | 618.00 | 30.00 | -142.90 | -11.10 | 494.00 | 434.00 | 928.00 |
| FY2017-Q4 | 2017 | Q4 | 102.30 | -224.80 | -149.10 | 11.90 | -259.70 | 928.00 | 668.30 |
| FY2018-Q1 | 2018 | Q1 | 334.20 | -128.50 | -61.00 | 17.40 | 162.10 | 668.30 | 830.40 |
| FY2018-Q2 | 2018 | Q2 | 102.80 | 215.20 | -48.80 | 12.00 | 281.20 | 830.40 | 1,111.60 |
| FY2018-Q3 | 2018 | Q3 | 514.10 | -404.50 | -48.90 | 3.40 | 64.10 | 1,111.60 | 1,175.70 |
| FY2018-Q4 | 2018 | Q4 | 24.00 | 128.70 | -38.80 | 15.00 | 128.90 | 1,175.70 | 1,304.60 |
| FY2019-Q1 | 2019 | Q1 | 230.60 | -827.10 | -164.40 | -11.40 | -772.30 | 1,304.60 | 532.30 |
| FY2019-Q2 | 2019 | Q2 | -17.60 | 94.30 | -54.10 | -1.80 | 20.80 | 532.30 | 553.10 |
| FY2019-Q3 | 2019 | Q3 | 470.10 | -77.30 | -262.60 | -2.80 | 127.40 | 553.10 | 680.50 |
| FY2019-Q4 | 2019 | Q4 | 100.70 | -69.20 | -124.60 | -3.30 | -96.40 | 680.50 | 584.10 |
7. Balance-sheet shell¶
We are going to repeat the process to inlude the cash changes in the balance sheet. We create the DF make the calculations by "hand" and compute the final total values
balance_sheet_shell = timeline_index.copy()
balance_sheet_shell["cashAndCashEquivalents_model"] = cash_flow_statement["cash_end"].values
balance_sheet_shell["shortTermInvestments"] = bs_q["shortTermInvestments"].values
balance_sheet_shell["netReceivables"] = bs_q["netReceivables"].values
balance_sheet_shell["inventory"] = bs_q["inventory"].values
balance_sheet_shell["prepaids"] = bs_q["prepaids"].values
balance_sheet_shell["otherCurrentAssets"] = bs_q["otherCurrentAssets"].values
balance_sheet_shell["totalCurrentAssets_model"] = (
balance_sheet_shell["cashAndCashEquivalents_model"]
+ balance_sheet_shell["shortTermInvestments"]
+ balance_sheet_shell["netReceivables"]
+ balance_sheet_shell["inventory"]
+ balance_sheet_shell["prepaids"]
+ balance_sheet_shell["otherCurrentAssets"]
)
balance_sheet_shell["nonCurrentAssets_reported"] = bs_q["totalAssets"].values - bs_q["totalCurrentAssets"].values
balance_sheet_shell["totalAssets_model"] = (
balance_sheet_shell["totalCurrentAssets_model"]
+ balance_sheet_shell["nonCurrentAssets_reported"]
)
balance_sheet_shell["totalLiabilitiesAndTotalEquity"] = bs_q["totalLiabilitiesAndTotalEquity"].values
balance_sheet_shell["balance_error"] = (
balance_sheet_shell["totalAssets_model"]
- balance_sheet_shell["totalLiabilitiesAndTotalEquity"]
)
balance_sheet_shell_usdm = to_usd_mm(balance_sheet_shell, exclude_cols=["fiscalYear"])
display(balance_sheet_shell_usdm[["fiscalYear", "period", "cashAndCashEquivalents_model", "totalAssets_model", "totalLiabilitiesAndTotalEquity", "balance_error"]].head(12).round(1))
| fiscalYear | period | cashAndCashEquivalents_model | totalAssets_model | totalLiabilitiesAndTotalEquity | balance_error | |
|---|---|---|---|---|---|---|
| Period | ||||||
| FY2017-Q1 | 2017 | Q1 | 457.00 | 6,118.00 | 6,118.00 | 0.00 |
| FY2017-Q2 | 2017 | Q2 | 434.00 | 6,067.00 | 6,067.00 | 0.00 |
| FY2017-Q3 | 2017 | Q3 | 928.00 | 5,966.00 | 5,966.00 | 0.00 |
| FY2017-Q4 | 2017 | Q4 | 668.30 | 5,652.00 | 5,652.00 | 0.00 |
| FY2018-Q1 | 2018 | Q1 | 830.40 | 5,814.00 | 5,814.00 | 0.00 |
| FY2018-Q2 | 2018 | Q2 | 1,111.60 | 6,028.30 | 6,028.30 | 0.00 |
| FY2018-Q3 | 2018 | Q3 | 1,175.70 | 6,199.00 | 6,199.00 | 0.00 |
| FY2018-Q4 | 2018 | Q4 | 1,304.60 | 6,143.30 | 6,143.30 | 0.00 |
| FY2019-Q1 | 2019 | Q1 | 532.30 | 6,035.30 | 6,035.30 | 0.00 |
| FY2019-Q2 | 2019 | Q2 | 553.10 | 6,155.50 | 6,155.50 | 0.00 |
| FY2019-Q3 | 2019 | Q3 | 680.50 | 6,092.00 | 6,092.00 | 0.00 |
| FY2019-Q4 | 2019 | Q4 | 584.10 | 5,942.80 | 5,942.80 | 0.00 |
8. Integrity checks¶
Integrity checks on the basic level as mentioned on the chapter.
- Timeline
- roll foward error
- Plug error
- Checking if our numbers match the ones reported
integrity_report = timeline_index.copy()
integrity_report["cash_flow_identity_error"] = (
cash_flow_statement["delta_cash"]
- cash_flow_statement["CFO"]
- cash_flow_statement["CFI"]
- cash_flow_statement["CFF"]
- cash_flow_statement["other_cash"]
)
integrity_report["cash_roll_forward_error"] = (
cash_flow_statement["cash_end"]
- cash_flow_statement["cash_begin"]
- cash_flow_statement["delta_cash"]
)
integrity_report["cash_to_bs_error"] = (
cash_flow_statement["cash_end"] - bs_q["cashAndCashEquivalents"].values
)
integrity_report["balance_error"] = balance_sheet_shell["balance_error"].values
integrity_report["cfo_vs_reported"] = (
cash_flow_statement["CFO"] - cf_q["netCashProvidedByOperatingActivities"].values
)
# What I believe to be the most important part, making sure our calculations are the same values as reported in the original CF reported by RL
integrity_report["cfi_vs_reported"] = (
cash_flow_statement["CFI"] - cf_q["netCashProvidedByInvestingActivities"].values
)
integrity_report["cff_vs_reported"] = (
cash_flow_statement["CFF"] - cf_q["netCashProvidedByFinancingActivities"].values
)
integrity_report["delta_cash_vs_reported"] = (
cash_flow_statement["delta_cash"] - cf_q["netChangeInCash"].values
)
integrity_report["cash_definition_gap"] = cash_flow_statement["cash_definition_gap"].values
#Tolerance for the test checks of fail
tolerance = 2.0 * USD_MM
integrity_report["passes_all_core_checks"] = (
integrity_report[["cash_flow_identity_error", "cash_roll_forward_error", "cash_to_bs_error", "balance_error"]]
.abs()
.max(axis=1)
<= tolerance
)
integrity_report_usdm = to_usd_mm(integrity_report, exclude_cols=["fiscalYear"])
display(integrity_report_usdm[["fiscalYear", "period", "cash_to_bs_error", "balance_error", "cash_definition_gap", "passes_all_core_checks"]].tail(12).round(3))
print("All quarters pass:", bool(integrity_report["passes_all_core_checks"].all()))
| fiscalYear | period | cash_to_bs_error | balance_error | cash_definition_gap | passes_all_core_checks | |
|---|---|---|---|---|---|---|
| Period | ||||||
| FY2023-Q4 | 2023 | Q4 | 0.00 | 0.00 | -0.20 | True |
| FY2024-Q1 | 2024 | Q1 | 0.00 | 0.00 | 0.60 | True |
| FY2024-Q2 | 2024 | Q2 | 0.00 | 0.00 | 0.20 | True |
| FY2024-Q3 | 2024 | Q3 | 0.00 | 0.00 | -1.70 | True |
| FY2024-Q4 | 2024 | Q4 | 0.00 | 0.00 | 0.10 | True |
| FY2025-Q1 | 2025 | Q1 | 0.00 | 0.00 | 1.70 | True |
| FY2025-Q2 | 2025 | Q2 | 0.00 | 0.00 | -0.50 | True |
| FY2025-Q3 | 2025 | Q3 | 0.00 | 0.00 | 0.40 | True |
| FY2025-Q4 | 2025 | Q4 | 0.00 | 0.00 | -0.10 | True |
| FY2026-Q1 | 2026 | Q1 | 0.00 | 0.00 | 1.40 | True |
| FY2026-Q2 | 2026 | Q2 | 0.00 | 1.40 | 1.60 | True |
| FY2026-Q3 | 2026 | Q3 | 0.00 | 0.00 | -1.50 | True |
All quarters pass: True
pretax_income = is_q["incomeBeforeTax"].replace(0, np.nan)
raw_tax_rate = is_q["incomeTaxExpense"] / pretax_income
fallback_tax_rate = raw_tax_rate.replace([np.inf, -np.inf], np.nan).median()
if pd.isna(fallback_tax_rate):
fallback_tax_rate = 0.25
tax_rate_used = raw_tax_rate.where(
(pretax_income > 0) & raw_tax_rate.between(0.0, 0.60),
np.nan,
).fillna(fallback_tax_rate)
fcf_outputs = timeline_index.copy()
fcf_outputs["revenue"] = is_q["revenue"].values
fcf_outputs["ebit"] = is_q["ebit"].values
fcf_outputs["tax_rate_used"] = tax_rate_used.values
fcf_outputs["NOPAT"] = (is_q["ebit"] * (1.0 - tax_rate_used)).values
fcf_outputs["depreciationAndAmortization"] = cf_q["depreciationAndAmortization"].values
fcf_outputs["other_operating_adjustments"] = (
cf_q["deferredIncomeTax"]
+ cf_q["stockBasedCompensation"]
+ cf_q["otherNonCashItems"]
).values
fcf_outputs["netCapEx"] = (-cf_q["capitalExpenditure"]).values
fcf_outputs["delta_owc_use_of_cash"] = (-cf_q["changeInWorkingCapital"]).values
fcf_outputs["FCFF"] = (
fcf_outputs["NOPAT"]
+ fcf_outputs["depreciationAndAmortization"]
+ fcf_outputs["other_operating_adjustments"]
- fcf_outputs["netCapEx"]
- fcf_outputs["delta_owc_use_of_cash"]
)
fcf_outputs["netBorrowing"] = cf_q["netDebtIssuance"].values
fcf_outputs["after_tax_interest"] = (is_q["interestExpense"] * (1.0 - tax_rate_used)).values
fcf_outputs["FCFE_from_NI_bridge"] = (
is_q["netIncome"]
+ cf_q["depreciationAndAmortization"]
+ cf_q["deferredIncomeTax"]
+ cf_q["stockBasedCompensation"]
+ cf_q["otherNonCashItems"]
- (-cf_q["capitalExpenditure"])
- (-cf_q["changeInWorkingCapital"])
+ cf_q["netDebtIssuance"]
).values
fcf_outputs["FCFE_from_FCFF_bridge"] = (
fcf_outputs["FCFF"]
- fcf_outputs["after_tax_interest"]
+ fcf_outputs["netBorrowing"]
)
fcf_outputs["FCFE_bridge_gap"] = (
fcf_outputs["FCFE_from_NI_bridge"] - fcf_outputs["FCFE_from_FCFF_bridge"]
)
fcf_outputs_usdm = to_usd_mm(fcf_outputs, exclude_cols=["fiscalYear", "tax_rate_used"])
display(fcf_outputs_usdm[["fiscalYear", "period", "FCFF", "FCFE_from_NI_bridge", "FCFE_from_FCFF_bridge", "FCFE_bridge_gap"]].tail(12).round(1))
| fiscalYear | period | FCFF | FCFE_from_NI_bridge | FCFE_from_FCFF_bridge | FCFE_bridge_gap | |
|---|---|---|---|---|---|---|
| Period | ||||||
| FY2023-Q4 | 2023 | Q4 | -42.90 | -53.60 | -53.60 | 0.00 |
| FY2024-Q1 | 2024 | Q1 | 238.80 | 225.10 | 225.10 | 0.00 |
| FY2024-Q2 | 2024 | Q2 | 39.00 | 24.90 | 24.90 | 0.00 |
| FY2024-Q3 | 2024 | Q3 | 571.50 | 557.50 | 557.50 | 0.00 |
| FY2024-Q4 | 2024 | Q4 | 90.50 | 76.10 | 76.10 | 0.00 |
| FY2025-Q1 | 2025 | Q1 | 252.40 | 239.00 | 239.00 | 0.00 |
| FY2025-Q2 | 2025 | Q2 | 64.50 | 49.60 | 49.60 | -0.00 |
| FY2025-Q3 | 2025 | Q3 | 686.20 | 671.50 | 671.50 | 0.00 |
| FY2025-Q4 | 2025 | Q4 | 50.40 | 36.80 | 36.80 | -0.00 |
| FY2026-Q1 | 2026 | Q1 | -2.10 | 481.00 | 481.00 | 0.00 |
| FY2026-Q2 | 2026 | Q2 | -39.50 | -446.10 | -458.60 | 12.50 |
| FY2026-Q3 | 2026 | Q3 | 714.40 | 704.00 | 704.00 | 0.00 |
9. Export¶
CSV outputs for the results only for control purpose since was not specify under the project instructions
quarterly_inputs_usdm.to_csv(OUT_DIR / "chapter5_quarterly_inputs_usdm.csv")
working_capital_bridge_usdm.to_csv(OUT_DIR / "chapter5_quarterly_working_capital_bridge_usdm.csv")
cash_flow_statement_usdm.to_csv(OUT_DIR / "chapter5_quarterly_cash_flow_statement_usdm.csv")
balance_sheet_shell_usdm.to_csv(OUT_DIR / "chapter5_quarterly_balance_sheet_shell_usdm.csv")
integrity_report_usdm.to_csv(OUT_DIR / "chapter5_quarterly_integrity_report.csv")
fcf_outputs_usdm.to_csv(OUT_DIR / "chapter5_quarterly_fcf_outputs_usdm.csv")
latest_period = cash_flow_statement_usdm.index[-1]
latest_cf = cash_flow_statement_usdm.loc[latest_period]
latest_fcf = fcf_outputs_usdm.loc[latest_period]
(OUT_DIR / "chapter5_quarterly_review_summary.txt")
print("Outputs saved to:", OUT_DIR)
Outputs saved to: /Users/theomachado/Library/CloudStorage/OneDrive-UniversityofFlorida/Spring 2026/FIN4453 - OFC/Mini-projects/Module 4/outputs
TO GO ABOVE AND BEYOND¶
Decided to do a small graph showing the tragectory of CFO CFI and CFF
ax = cash_flow_statement_usdm[["CFO", "CFI", "CFF"]].plot(
kind="bar",
stacked=True,
figsize=(14, 6),
color=["#1b6ca8", "#f08a24", "#2f855a"],
)
cash_flow_statement_usdm["delta_cash"].plot(
ax=ax,
color="black",
marker="o",
linewidth=2,
label="delta_cash",
)
ax.set_title("Quarterly CFO / CFI / CFF and delta_cash")
ax.set_xlabel("Quarter")
ax.set_ylabel("USD mm")
ax.axhline(0, color="black", linewidth=1)
ax.tick_params(axis="x", rotation=75)
ax.legend(loc="best")
plt.tight_layout()
plt.show()