<SYSTEM_TASK:>
Given the following text description, write Python code to implement the functionality described below step by step
<END_TASK>
<USER_TASK:>
Description:
 Vignette
I've made a small tool while waiting for an airplane. It's a small wrapper around pandas that makes dataframe manipulation more functional. The goal was to give it a slightly more functional api for personal use. It sort of works and I would like to demonstrate the functionality in this document. It might not be a package that I'll offer a lot of support for, but it has some ideas that may be worth sharing. In this document I'll describe some functionality.
Philosophy
Data should be a noun and any manipulations on it should be described with verb. In R it is convenient to have those verbs be functions because the language allows you to write global operators that can chain functions togehter. In python it makes sense to have them wrapped with methods via an object instead.
The idea behind the tool is to have a more minimal api that accomodates 80-90% of the typical dataframe manipulations by attaching a few very useful composable verbs to a wrapped dataframer. This work is not meant to replace pandas nor is it meant as a python port of popular r-packages (though I'll gladly admit that a lot of ideas are bluntly copied from tidyr and dplyr). 
The goal of this work is to show a proof of concept to demonstrate extra composability and readability for python based data manipulation. Some performance sacrifices will ensue as a result. 
Starting Point
To start to explain what the tool does, we need a dataset to start out with.
Step1:  This is the data that we'll work with. We won't change the dataframe or it's api, rather we'll wrap it in an object that contains extra methods.
Step2:  This new object contains the original pandas dataframe as a child, this means that you can always access the original data via &lt;obj_name&gt;.df. You will always be able to work with pure pandas if need be but a few methods are added to enchance readability and composability.
The added methods are
Step3:  The column is added via a lambda function. This lambda function accepts the original dataframe. This allows you to refer to columns and allows you to apply any function you'd like. You can also create multiple columns in a single mutate statement.
Step4:  You might appreciate that this method is somewhat lazy. You can use the 1st new column that you've created when creating your second one without the need to call mutate again. 
Notice that any numpy warnings that you may cause will still appear, but they won't cause the operation to fail. 
tibble.filter
Filter creates a subset of the datastructure. Whenever the datastructure evaluates as true it will keep the row.
Step5:  Again you should notice a lazy structure. No need to call .filter multiple times as you can apply multiple filters in a single step. 
tibble.slice
Step6:  tibble.head and tibble.tail
These methods are just like their pandas equivalents. The select the top rows or the bottom rows of the datastructure.
Step7:  tibble.select
Select is merely a method to select a subset of columns.
Step8:  tibble.rename
The .rename method expects a dictionary of strings where the keys represent the new names and the values represent the old names.
Step9:  tibble.set_names
The .set_names method expects a list of strings and will reset the column names of the dataframe.
Step10:  tibble.drop
The .drop method allows you to pass column names of columns that can be dropped from the datastructure.
Step11:  tibble.group_by
Step12:  tibble.agg
With the grouping attached you can easily aggregate with the .agg method. Notice that aggregating without a group will just regard the entire datastructure as a single group.
Step13:  A few things to note. 
in the .agg method you can pass any function that accepts a dataframe
the .agg method can have multiple output columns that depend on different columns 
you can even combine multiple columns in a lambda function 
tibble.sort
Step14:  Note that grouping a datastructure has an effect on how it is sorted.
Step15:  tibble.ungroup
Step16:  tibble.sample_n
Step17:  tibble.pipe
The .pipe method is a simple wrapper around the pandas.DataFrame.pipe method. It was the method I was using most from pandas and I did not want it's functionality removed. 
It is a very useful construct that allows you to add functions can can perform complex tasks on the datastructure.
Step18:  tibble.gather
Step19:  tibble.spread
tibble.left_join
Step20:  tibble.inner_join
Step21:  Plotting
The .plot method exists in kadro and works just like pandas.
Step22:  The nice thing about plotting is that this doesn't break your flow in kadro.
<ASSISTANT_TASK:>
Python Code:
import numpy as np 
import pandas as pd 
import kadro as kd
%matplotlib inline
np.random.seed(42)
n = 20
df = pd.DataFrame({
    'a': np.random.randn(n),
    'b': np.random.randn(n),
    'c': ['foo' if x > 0.5 else 'bar' for x in np.random.rand(n)],
    'd': ['fizz' if x > 0.6 else 'bo' for x in np.random.rand(n)]
})
df = df.sort_values(['c', 'd'])
print(df)
Explanation: Vignette
I've made a small tool while waiting for an airplane. It's a small wrapper around pandas that makes dataframe manipulation more functional. The goal was to give it a slightly more functional api for personal use. It sort of works and I would like to demonstrate the functionality in this document. It might not be a package that I'll offer a lot of support for, but it has some ideas that may be worth sharing. In this document I'll describe some functionality.
Philosophy
Data should be a noun and any manipulations on it should be described with verb. In R it is convenient to have those verbs be functions because the language allows you to write global operators that can chain functions togehter. In python it makes sense to have them wrapped with methods via an object instead.
The idea behind the tool is to have a more minimal api that accomodates 80-90% of the typical dataframe manipulations by attaching a few very useful composable verbs to a wrapped dataframer. This work is not meant to replace pandas nor is it meant as a python port of popular r-packages (though I'll gladly admit that a lot of ideas are bluntly copied from tidyr and dplyr). 
The goal of this work is to show a proof of concept to demonstrate extra composability and readability for python based data manipulation. Some performance sacrifices will ensue as a result. 
Starting Point
To start to explain what the tool does, we need a dataset to start out with.
End of explanation
kf = kd.Frame(df)
kf
Explanation: This is the data that we'll work with. We won't change the dataframe or it's api, rather we'll wrap it in an object that contains extra methods.
End of explanation
kf.mutate(e = lambda _: _['a'] + _['b']*2)
Explanation: This new object contains the original pandas dataframe as a child, this means that you can always access the original data via &lt;obj_name&gt;.df. You will always be able to work with pure pandas if need be but a few methods are added to enchance readability and composability.
The added methods are: 
mutate
filter
head/tail/slice
select/rename/drop
group_by/agg/ungroup
sort
sample_n
pipe 
gather 
spread
left_join, inner_join
If you're used to dplyr or Spark then you may recognize some of these functions. We'll demonstrate how they work one by one. 
tibble.mutate
Mutate overwrites a column value or creates a new one. You specify the name of the column via the parameter name and you use a lambda function to describe the contents of the column. The input of the function will be the dataframe in the tibble object. This means that you can refer to columns but that you can also use numpy to describe changes.
End of explanation
(kf
  .mutate(e = lambda _: _['a'] + _['b']*2,
          f = lambda _: np.sqrt(_['e']),
          a = lambda _: _['a'] / 2))
Explanation: The column is added via a lambda function. This lambda function accepts the original dataframe. This allows you to refer to columns and allows you to apply any function you'd like. You can also create multiple columns in a single mutate statement.
End of explanation
(kf
  .filter(lambda _: _['a'] > 0, 
          lambda _: _['b'] > 0))
Explanation: You might appreciate that this method is somewhat lazy. You can use the 1st new column that you've created when creating your second one without the need to call mutate again. 
Notice that any numpy warnings that you may cause will still appear, but they won't cause the operation to fail. 
tibble.filter
Filter creates a subset of the datastructure. Whenever the datastructure evaluates as true it will keep the row.
End of explanation
kf.slice(2, 3, 10)
kf.slice([2, 3, 10])
Explanation: Again you should notice a lazy structure. No need to call .filter multiple times as you can apply multiple filters in a single step. 
tibble.slice
End of explanation
kf.head(5)
kf.head(5).tail(3)
Explanation: tibble.head and tibble.tail
These methods are just like their pandas equivalents. The select the top rows or the bottom rows of the datastructure.
End of explanation
kf.select('b', 'c')
kf.select(['b', 'c'])
Explanation: tibble.select
Select is merely a method to select a subset of columns.
End of explanation
kf
kf.rename({"aa":"a", "bb":"b"})
Explanation: tibble.rename
The .rename method expects a dictionary of strings where the keys represent the new names and the values represent the old names.
End of explanation
kf.set_names(["a", "b", "c", "omg_d"])
Explanation: tibble.set_names
The .set_names method expects a list of strings and will reset the column names of the dataframe.
End of explanation
kf.drop("a", "b")
kf.drop(["a", "b"])
Explanation: tibble.drop
The .drop method allows you to pass column names of columns that can be dropped from the datastructure.
End of explanation
kf.group_by("c", "d")
Explanation: tibble.group_by
End of explanation
kf.agg(m_a = lambda _: np.mean(_['a']),
       v_b = lambda _: np.var(_['b']), 
       cov_ab = lambda _: np.cov(_['a'], _['b'])[1,1])
(kf
 .group_by("c", "d")
 .agg(m_a = lambda _: np.mean(_['a']),
      v_b = lambda _: np.var(_['b']), 
      cov_ab = lambda _: np.cov(_['a'], _['b'])[1,1]))
Explanation: tibble.agg
With the grouping attached you can easily aggregate with the .agg method. Notice that aggregating without a group will just regard the entire datastructure as a single group.
End of explanation
kf.sort("a")
Explanation: A few things to note. 
in the .agg method you can pass any function that accepts a dataframe
the .agg method can have multiple output columns that depend on different columns 
you can even combine multiple columns in a lambda function 
tibble.sort
End of explanation
kf.group_by("c", "d").sort("a")
Explanation: Note that grouping a datastructure has an effect on how it is sorted.
End of explanation
kf.group_by("a")
kf.group_by("a").ungroup()
Explanation: tibble.ungroup
End of explanation
kf.sample_n(10)
kf.sample_n(1000, replace=True).sort("a").head(5)
Explanation: tibble.sample_n
End of explanation
def heavy_func(frame, colname, multy):
    frame[colname] = frame[colname] * multy
    return frame
    
(kf
 .pipe(heavy_func, colname = "b", multy = 2)
 .pipe(heavy_func, colname = "a", multy = 10))
Explanation: tibble.pipe
The .pipe method is a simple wrapper around the pandas.DataFrame.pipe method. It was the method I was using most from pandas and I did not want it's functionality removed. 
It is a very useful construct that allows you to add functions can can perform complex tasks on the datastructure.
End of explanation
kf.gather('key', 'value')
Explanation: tibble.gather
End of explanation
df_age = pd.DataFrame({
    'name': ['vincent', 'tim', 'anna'],
    'age': [28, 30, 25]
})
df_length = pd.DataFrame({
    'name': ['vincent', 'tim'],
    'length': [188, 172]
})
kd_age = kd.Frame(df_age)
kd_length = kd.Frame(df_length)
kd_age.left_join(kd_length)
Explanation: tibble.spread
tibble.left_join
End of explanation
kd_age.inner_join(kd_length)
Explanation: tibble.inner_join
End of explanation
kf.plot('a', 'b', kind = 'scatter')
Explanation: Plotting
The .plot method exists in kadro and works just like pandas.
End of explanation
(kf
   .mutate(a = lambda _: _['a'] + _['b'])
   .filter(lambda _: _['a'] < 1)
   .plot('a', 'b', kind = 'scatter', title = 'foobar'))
Explanation: The nice thing about plotting is that this doesn't break your flow in kadro.
End of explanation
<END_TASK>