---
# System prepended metadata

title: Method Chaining
tags: [data science, Python, Java]

---

# Method Chaining

Sometimes, data science projects involve manipulating an object through a series of events. For example, a matrix may need to be transposed first, added some shape sentitive non-linear transformations, and transposed  back.

Suppose the matrix object is assigned `m`, and the method to transpose it is `t()` and that to transform it `s()`, then the operations just stated are essentially something like this in the pseudo-code:

```{}
m = new object of class Matrix
m = m.t()
m = m.s()
m = m.t()
```

But suppose there are way more steps than 3, as shown in the example above. Maybe you are considering writing something like the following to work the same, which is what this article is about to share with you.
```{}
m = new object of class Matrix
m.t().s().t()
```

The trick is for every method you want chainable to always return the object, i.e. `this` or `self`, etc. depending on the language you are using. The following are just illustrative examples.

# An Illustrative Example in Java

Say you needed a Matrix class that can transpose, append, calculates sums, etc. You can do it this way. Notice how the methods always return `this`.

```{}
public class Mat {
	
	int dim1;
	int dim2;
	int[][] m; // matrix data;
	
	Mat(int[][] m) {
		this.m = m;
		updateDims();
	}
	
	void updateDims() {
		this.dim1 = m.length;
		this.dim2 = m[0].length;
	}
	
	static int vSum(int[] v) {
		int sum = 0;
		for(int elem: v)
			sum += elem;
		return sum;
	}
	
	// s is for sums
	Mat s() {
		return this.s(0).s(1);
	}
	
	// s is for summing over a dimension
	Mat s(int dim) { 
		
		if(dim == 0) {
			
			int[] rowSums = new int[dim1];
			for (int i = 0; i <= dim1 - 1 ; i++) {
				rowSums[i] = vSum(m[i]);
			}
			return this.a(rowSums, 1);
			
		} else if (dim == 1) {
			return this.t().s(0).t();
		} else {
			return e("Dim must be either 0 (row) or 1 (column)! Returning the original matrix.");
		}
		
	}
	
	
	Mat a(int[] v) {
		return this.a(v, 0, -1);	
	}
	
	Mat a(int[] v, int dim) {
		return this.a(v, dim, -1);	
	}
	
	Mat a(int[] v, int dim, int loc) {
		// append at loc location

		if(dim == 0) {
			
			if(loc < 0) loc += dim1 + 1; // e.g. if you type -1, you get dim1, which is the bottom most location
			
			if(loc < 0 || loc > dim1) return e("Specified location out of bound.");
			if(v.length != dim2) return e("Cannot append the vector to rows. Vector length must be the same as the number of columns.");
			
			
			
			int[][] output = new int[dim1 + 1][dim2];
			boolean newVectorAppended = false;
			
			for(int i = 0; i <= dim1 - 1; i++) {
				
				if(i == loc) {
					output[i] = v;
					newVectorAppended = true;
				}
				
				if(!newVectorAppended) {
					output[i] = m[i];
				} else {
					output[i + 1] = m[i];
				}
				
			}
			
			if(!newVectorAppended) output[dim1] = v;
			m = output;
			updateDims();
			return this;
			
		} else if (dim == 1) {
			
			if(loc < 0) loc += dim2 + 1; // e.g. if you type -1, you get dim2 - 1, which is the rightmost  location
			
			if(loc < 0 || loc > dim2) return e("Specified location out of bound.");
			if(v.length != dim1) return e("Cannot append the vector to columns. Vector length must be the same as the number of rows.");
			
			return this.t().a(v, 0, loc).t();
			
		} else {
			
			return e("Dim must be either 0 (row) or 1 (column)! Returning the original matrix.");
		}
		
	}
	
	// t is for transpose
	Mat t() {

		int[][] output = new int[dim2][dim1];
		for(int i = 0; i <= dim1 - 1; i++) {
			for(int j = 0; j <= dim2 - 1; j++) {
				output[j][i] = m[i][j];
			}
		}
		m = output;
		updateDims();
		return this;
	}
	
	// r is for reversing the matrix
	Mat r(int dim){
		// if  dim == 0; by Row; if dim == 1; by Column
		int[][] output = new int[dim1][dim2];
		
		// reverse by columns
		if (dim == 1) {
			for(int i = 0; i  <= dim1 - 1; i++) {
				for(int j = dim2 - 1; j >=0; j--) {
					output[i][dim2 - j - 1] = m[i][j]; 
				}
			}
		// reverse by columns
		} else if (dim == 0) {
			for(int i = dim1 - 1; i  >= 0; i--) {
				for(int j = 0; j <= dim2 - 1; j++ ) {
					output[dim1 - i - 1][j] = m[i][j]; 
				}
			}
			
		} else {
			return e("the dim argument must either be 1 (byCol) or 0 (byRow).");
		}		
		m = output;
		updateDims();
		return this;
	}
	
	// p is for print
	Mat p() {
				
		for(int[] v: m) {
			for(int elem: v) {
				System.out.print(elem + "\t");
			}
			System.out.println();
		}
		
		return this;
	}
	
	// e is for error
	Mat e() {
		return e("Something went wrong.");
	}
	
	Mat e(String msg) {
		System.out.println(msg +"\n Returning the original matrix.");
		updateDims();
		return this;
	}
	
	// d is for divider
	Mat d() {	
		return d(10);
	}
	
	Mat d(int num) {
		for(int i = 0; i <= num - 1 ;i++) System.out.print("-");
		System.out.print("\n");
		return this;
	}
	

}
```

The set up makes it easy to use the class `Mat`, this way: `m.p().d().t().r(1).p();`

// in Main.java
```{}
public class Main {

	public static void main(String[] args) {
    
    //input
	//  1	2 	3	4	5	6
	//  7	8	9	10	11	12
	//  13	14	15	16	17	18
	//  19	20	21	22	23	24
	//  25	26	27	28	29	30
    // expected output
	//  ------
	//  25	19	13	7	1
	//  26	20	14	8	2
	//  27	21	15	9	3
	//  28	22	16	10	14
	//  29	23	17	11	5
	//  30	24	18	12	6
		int[][] ias = { 
				{ 1, 2, 3, 4, 5, 6 }, 
				{ 7, 8, 9, 10, 11, 12 }, 
				{ 13, 14, 15, 16, 17, 18 },
				{ 19, 20, 21, 22, 23, 24 }, 
				{ 25, 26, 27, 28, 29, 30 } 
				};
		
		Mat m = new Mat(ias);
		m.p().d().t().r(1).p();
	}

}
```

# Broader Use

Imagine a scenario with Python where you are streamlining your data analytic process a certain way you want reproducible. Your class may (conceptually) look like this:

```{python}
import numpy as np
import pandas as pd

class YourModel:

    raw_data = None
    cleaned_data = None
    regression_output = None

    def __init__(self, file_path):
        self.load_data(). .. # run as many methods as necessary
    
    
    def load_data(self, file_path):
        # some check ups here
        assert isinstance(file_path, str)
        self.raw_data = pd.read_excel(file_path) #...
        return self
    

    def prepare(self, some_criteria):
        assert self.raw_data is not None
        cleaned_df = pd.DataFrame()
        #... some cleaning here based on criteria
        self.cleaned_data = cleaned_df        
        return self
        
    def fit_regression(self):
        assert self.cleaned_data is not None
        # something from scikitlearn maybe
        self.regression_output = Regression().fit(self.cleaned_data)
        return self
        
    ...

```
And for people who use your API, they simply write this:
```{}
mod = YourModel(necessary_arguments)
mod.prepare(args).fit_regression() ...
```
The structure can make hyperparameter tuning a lot friendlier than it usually is. Hope you enjoyed reading this article. Happy New Year = ]


###### tags: `data science` `Python` `Java`
