QuantRocket logo
Disclaimer


Fundamental Factors › Lesson 11: Altman Z-Score


Analyzing the Altman Z-Score with Alphalens¶

Let's use Alphalens to explore the forward performance of stocks based on their Altman Z-Score. In previous notebooks we formed equal-sizes quantiles, but in this notebook we'll form bins corresponding to the Altman Z-Scores three "zones":

  • Distress Zone: Z-Score < 0
  • Grey Zone: 0 < Z-Score < 3
  • Safe zone: Z-Score > 3

Defining your own bin edges can be done either in the Alphalens' from_pipeline() function or in the Pipeline definition itself. First, we'll show how to do it in Alphalens, and then we'll show how to do it in Pipeline.

Defining bin edges in Alphalens¶

To define the bin edges in Alphalens, we can start by creating a simple Pipeline that contains the Altman Z-Score factor:

In [1]:
from zipline.pipeline import sharadar, Pipeline
from codeload.fundamental_factors.universe import CommonStocks, BaseUniverse

universe = BaseUniverse() 

altman = sharadar.AltmanZScore('ART', mask=universe)

pipeline = Pipeline(
    columns={
        'altman': altman,
    },
    initial_universe=CommonStocks(),
    screen=universe & altman.notnull()
)

The Alphalens from_pipeline() function contains two, mutually exclusive parameters that control how the factor will be binned: quantiles and bins.

In the previous notebooks, we specified quantiles=n to create n equal-sized bins. Under the hood, this caused Alphalens to pass the factor data to pandas' qcut() function, which calculates the bin edges needed to have the same number of items in each bin.

In this notebook, we will use the bins argument to tell Alphalens where the bin edges should be. Under the hood, this will cause Alphalens to pass the factor data to pandas' cut() function. To split the data into three bins, where the first bin contains everything below 0, the second bin goes from 0 to 3, and the third bin contains everything above 3, we can pass the following bins argument:

bins=[-float("inf"), 0, 3, float("inf")]

Since we want 3 bins, we have to pass 4 bin edges; the length of the list you pass to bins will always be the desired number of bins plus one. The range of the first bin is defined by the first two numbers (here, minus infinity and 0), the range of the second bin is defined by the second and third numbers (0 and 3), and the range of the third bin is defined by the third and fourth numbers (3 and infinity).

In [2]:
import alphalens as al

al.from_pipeline(
    pipeline,
    start_date="1998-02-01",
    end_date="2022-12-30",
    periods=[1, 5, 21],
    factor="altman",
    bins=[-float("inf"), 0, 3, float("inf")],
    segment="Y"
)
Factor Distribution
 minmaxmeanstdcountavg daily countcount %
Altman Quantile       
1-326,034.031-0.000-14.8561,408.4041,754,284279.79.4%
20.0003.0001.7350.7776,944,9241107.537.2%
33.000264,810.75014.864673.9189,951,4671586.953.4%
Returns Analysis
 1D21D5D
Ann. alpha-0.045-0.030-0.027
beta0.055-0.0230.019
Mean Relative Return Top Quantile (bps)0.107-0.0340.056
Mean Relative Return Bottom Quantile (bps)-1.354-1.365-1.476
Mean Spread (bps)1.4611.9371.838
Information Analysis
 1D21D5D
IC Mean0.0110.0180.015
IC Std.0.0650.0820.073
Risk-Adjusted IC0.1650.2160.213
t-stat(IC)13.03617.08216.832
p-value(IC)0.0000.0000.000
IC Skew-0.118-0.264-0.271
IC Kurtosis0.9760.2720.949
Turnover Analysis
 1D5D21D
Quantile 1 Mean Turnover0.0080.0370.133
Quantile 2 Mean Turnover0.0040.0190.070
Quantile 3 Mean Turnover0.0030.0130.049
1D21D5D
Mean Factor Rank Autocorrelation0.9990.9780.995
Out[2]:
1D21D5Dfactorfactor_quantile
dateasset
1998-02-02Equity(FIBBG000H83MP4 [AAI1])0.0000000.0000000.000000-1.9671961
Equity(FIBBG000B9XRY4 [AAPL])-0.0338610.2626980.0103771.9087862
Equity(FIBBG000B9Y973 [AATI1])0.0166950.1237160.0768945.8291923
Equity(FIBBG000MDCQC2 [COR])0.0310840.0022960.0641125.3161023
Equity(FIBBG000B9YDD7 [ABF])0.0123020.0685410.0544823.7592533
.....................
2022-12-30Equity(FIBBG019QV0CG8 [SNAL])-0.0328950.052632-0.0065792.2266642
Equity(FIBBG011RWR2Q4 [ACRV])-0.0400000.350000-0.03333312.9786633
Equity(FIBBG000BCVMH9 [CP])-0.0108740.0462800.0281131.6607142
Equity(FIBBG000K5M1S8 [ENB])-0.0043290.0432900.0318310.7748742
Equity(FIBBG000C32XT3 [IMO])0.0045340.128607-0.0043283.7313413

18650675 rows × 5 columns

Tear sheet commentary¶

Factor Distribution table¶

  • min/max: For each factor quantile, we can check the minimum and maximum values to make sure they are as we expected. Here, everything in quantile 1 is below 0, everything in quantile 2 is between 0 and 3, and everything in quantile 3 is above 3.
  • count %: Because we defined the bin edges instead of using equal-sized quantiles, the quantiles are different sizes. Less than 10% of stocks are in the distress zone, ~40% are in the grey zone, and ~50% are in the safe zone.

Returns Analysis¶

  • Mean Relative Return By Factor Quantile: companies in distress perform worse than companies not in distress. There is little difference between the safe zone and the grey zone.

Defining bin edges in Pipeline¶

Suppose you want to define the bin edges in Pipeline instead of in Alphalens, perhaps because you're not feeding the Pipeline output to Alphalens. A good way to do this is using the Constant factor. Here, we define a factor for the 3 Altman Z-Score zones, with 1 being the distress zone, 2 being the grey zone, and 3 being the safe zone:

In [3]:
from zipline.pipeline import Constant

altman = sharadar.AltmanZScore('ART')

altman_zone = Constant(1).where(altman<=0, Constant(2).where(altman<=3, Constant(3).where(altman > 3)))

Next Up¶

Lesson 12: Multi-Factor Scores