Pandas apply function to multiple columns, using value from another dataframe - pandas

I have a dataframe with some examples, and another dataframe representing a population. For each numeric column in the examples df, I want to calculate the Cumulative Distribution Function of those values with respect to the population df.
This relies on column-wise mean and std values from the population df - and I can't find a way properly refer to these mean and std values in my apply function.
Here is a simplified example of what I'm trying:
The examples:
df_test = pd.DataFrame([['Azriel', 45, 76], ['Moses', 23, 34]])
df_test.columns = (['Name', 'Age', 'Weight'])
Name Age Weight
0 Azriel 45 76
1 Moses 23 34
The population:
df_comp = pd.DataFrame([['Mary', 28, 66], ['Joseph', 32, 86], ['Paul', 54, 88]])
df_comp.columns = (['Name', 'Age', 'Weight'])
Name Age Weight
0 Mary 28 66
1 Joseph 32 86
2 Paul 54 88
I am trying to produce the calculation in df_dist:
df_dist = df_test.copy()
numeric_cols = df_comp.select_dtypes(include=[np.number]).columns
mu = df_comp[numeric_cols].mean()
sig = df_comp[numeric_cols].std()
df_dist[numeric_cols] = df_dist[numeric_cols].apply(lambda x: scipy.stats.norm.cdf(x, mu, sig))
The output of df_dist is:
Name Age Weight
0 Azriel 0.691462 0.996679
1 Moses 0.000001 0.000078
The expected output of df_dist (calculated manually):
Age Weight
Azriel 0.6914624613 0.371154197
Moses 0.1419883859 0.00007804441375
You can see, the value for Azriel's Age and Moses's Weight is correct, but the rest are wrong.
I think I am making a mistake trying to referring to mu and sig in the apply function, when I only want to refer to one of the values within mu and sig.
I hope that makes sense - can anyone see a solution?

If we look at mu and sig, we see they are series and have values for each numeric column:
>>> mu
Age 38.0
Weight 80.0
dtype: float64
>>> sigma
Age 14.000000
Weight 12.165525
dtype: float64
When you are applying CDF function per column, you are using the whole mu and sigma series instead of using the corresponding values specific to the column (so your suspicion is correct!).
Remedy is to use the column's name in apply and select from the mu and sigma accordingly:
df_dist[numeric_cols].apply(lambda x: scipy.stats.norm.cdf(x, mu[x.name], sig[x.name]))
x.name will be e.g. "Age" when Age column is applied upon, and so on.
This gives:
Name Age Weight
0 Azriel 0.691462 0.371154
1 Moses 0.141988 0.000078

Related

Transforming a dataframe of dict of dict specific format

I have this df dataset:
df = pd.DataFrame({'train': {'auc': [0.432, 0.543, 0.523],
'logloss': [0.123, 0.234, 0.345]},
'test': {'auc': [0.456, 0.567, 0.678],
'logloss': [0.321, 0.432, 0.543]}})
Where I'm trying to transform it into this:
And also considering that:
epochs always have the same order for every cell, but instead of only 3 epochs, it could reach 1.000 or 10.000.
The column names and axis could change. For example another day the data could have f1 instead of logloss, or val instead of train. But no matter the names, in df each row will always be a metric name, and each column will always be a dataset name.
The number of columns and rows in df could change too. There are some models with 5 datasets, and 7 metrics for example (which would give a df with 5 columns and 7 rows)
The columname of the output table should be datasetname_metricname
So I'm trying to build some generic code transformation where at the same time avoiding brute force transformations. Just if it's helpful, the df source is:
df = pd.DataFrame(model_xgb.evals_result())
df.columns = ['train', 'test'] # This is the line that can change (and the metrics inside `model_xgb`)
Where model_xgb = xgboost.XGBClassifier(..), but after using model_xgb.fit(..)
Here's a generic way to get the result you've specified, irrespective of the number of epochs or the number or labels of rows and columns:
df2 = df.stack().apply(pd.Series)
df2.index = ['_'.join(reversed(x)) for x in df2.index]
df2 = df2.T.assign(epochs=range(1, len(df2.columns) + 1)).set_index('epochs').reset_index()
Output:
epochs train_auc test_auc train_logloss test_logloss
0 1 0.432 0.456 0.123 0.321
1 2 0.543 0.567 0.234 0.432
2 3 0.523 0.678 0.345 0.543
Explanation:
Use stack() to convert the input dataframe to a series (of lists) with a multiindex that matches the desired column sequence in the question
Use apply(pd.Series) to convert the series of lists to a dataframe with each list converted to a row and with column count equal to the uniform length of the list values in the input series (in other words, equal to the number of epochs)
Create the desired column labels from the latest multiindex rows transformed using join() with _ as a separator, then use T to transpose the dataframe so these index labels (which are the desired column labels) become column labels
Use assign() to add a column named epochs enumerating the epochs beginning with 1
Use set_index() followed by reset_index() to make epochs the leftmost column.
Try this:
df = pd.DataFrame({'train': {'auc': [0.432, 0.543, 0.523],
'logloss': [0.123, 0.234, 0.345]},
'test': {'auc': [0.456, 0.567, 0.678],
'logloss': [0.321, 0.432, 0.543]}})
de=df.explode(['train', 'test'])
df_out = de.set_index(de.groupby(level=0).cumcount()+1, append=True).unstack(0)
df_out.columns = df_out.columns.map('_'.join)
df_out = df_out.reset_index().rename(columns={'index':'epochs'})
print(df_out)
Output:
epochs train_auc train_logloss test_auc test_logloss
0 1 0.432 0.123 0.456 0.321
1 2 0.543 0.234 0.567 0.432
2 3 0.523 0.345 0.678 0.543

Locating column of dataframe in a dataframe

I have a dataframe in a dataframe similar to this one (my real one is much larger)
df_peter = pd.DataFrame({"height": [50,np.nan,65], "weight": [20,25,27]})
df_anna = pd.DataFrame({"height": [47,55,np.nan], "weight": [18,23,30]})
df = pd.DataFrame({"Name":["Peter", "Anna"], "Year":[2000, 2002], "Data":[df_peter, df_anna]})
df
Name Year Data
0 Peter 2000 height weight 0 50.0 20 1 NaN ...
1 Anna 2002 height weight 0 47.0 18 1 55.0 ...
my final goal is to use the .fillna(method = "ffill") function on the height column of Peter and Anna,
so i need a way to locate these two height columns
would be also practical to plot the data
using .fillna(method = "ffill") for one row is easy
df.loc[0,"Data"]["height"].fillna(method = "ffill")
But for both/all rows is not that easy beacause: df["Data"]["height"] doesn't work
actually i found a solution writing this question:
df["Data"].apply(lambda x: x["height"].fillna(method = "ffill", inplace = True))
but is there a way to do this without apply and lambda?
As an alternative, you can use a loop:
for i in range(0,len(df)):
df.loc[i,"Data"]["height"]=df.loc[i,"Data"]["height"].fillna(method = "ffill")

How to rewrite this code into an apply-lambda expression?

My dataframe(df) has some NaN entries in the new column, 's_score' which I can exclude by using func(x).
i.e. the execution of document_path_similarity() leads to some NaNs, preventing the execution of most_similar_docs() (if I don't use func(x) first).
D1,D2 are df.columns with string data.
df
Quality D1 D2
0 1 Ms Stewart, the chief executive... Ms Stewart, 61, its chief executive
1 1 After more than two years' det... After more than two years in
def most_similar_docs():
def func(x):
try:
return document_path_similarity(x['D1'], x['D2'])
except:
return np.nan
df['s_score'] = df.apply(func, axis=1)
Is there a way to rewrite this code as a one liner?
My attempts such as below lead to 'ValueError: ('max() arg is an empty sequence' or SyntaxError.
df['s_scores'] = df.apply(lambda x: document_path_similarity(x.D1, x.D2),axis=1)
paraphrases['s_scores'] = paraphrases.apply(lambda x: document_path_similarity(x.D1, x.D2),axis=1 if np.isnan(x))
I don't think there is anything wrong with your pandas code. What I did find is that similarity_score() is failing because it's trying to take max of an empty list. I forced the list to be non-empty by forcing in a zero score. This is first time I've looked at this library so please don't assume my patch is a good quality patch.
import io
df = pd.read_csv(io.StringIO(""" Quality D1 D2
0 1 Ms Stewart, the chief executive... Ms Stewart, 61, its chief executive
1 1 After more than two years' det... After more than two years in """), sep="\s\s+", engine="python")
def similarity_score(s1, s2):
list1 = []
for a in s1:
# patch +[0] at end so never finding max of empty list
list1.append(max([i.path_similarity(a) for i in s2 if i.path_similarity(a) is not None]+[0]))
output = sum(list1)/len(list1)
return output
df = df.assign(
s_scores=lambda x: x.apply(lambda r: document_path_similarity(r.D1, r.D2), axis=1)
)
print(df.to_string(index=False))
output
Quality D1 D2 s_scores
1 Ms Stewart, the chief executive... Ms Stewart, 61, its chief executive 0.838889
1 After more than two years' det... After more than two years in 0.912500

pandas mapping function to transform data using dictionary

please help friends.
I want to use mapping to match the age of student and identify them in category adult or child by comparing it with a dictionary 'dlist' containing age 1 to 18 as child and age 19 to 60 as adult..
# making Data Frame
age=np.random.randint(1,50,5,int)
name=['kashif', 'dawood', 'ali', 'zain', 'hamza']
df5=pd.DataFrame({'name':name,
'age':age})
# making dictionary
dlist={range(1,18):'child' , range(19,50):'adult'}
# now maping dictionary with data frame 'age' column elements to add status adult if age greater than 18 using dictionary
df5['Status']=df5.age.map(dlist)
but it returns the data frame with column name 'Status' but NAN values (instead of adult or child)
kindly ignore my English if there are mistakes. i m not a native speaker of english.
In Python 3, you are allowed to use ranges as dict-keys, but it does not work the way you seem to think. For example
print(dlist[1])
will give you a key-error, since the key 1 does not exist in dlist, however
print(dlist[range(1,18)])
will work, since you have a key that is range(1,18). This means that you can not use your dlist the way you want in the map-function
To make use of your dict, with ranges as keys, you should instead use apply
df5['Status'] = df5['age'].apply(
lambda x: next((v for k, v in dlist.items() if x in k), 'NA')
)
Where [v for k, v in dlist.items() if x in k] gives you a list of all values in your dict, if x is in k (which is a range). The next()-function gets the next value (i.e. the first value) in that list (but it also works on iterators, and thats why [] can be omitted. NA is the default value for next() if no next exists. See https://docs.python.org/3/library/functions.html#next
You should howevr note that range(1,18) does NOT include 18. So with this code, the age 18 will give you Status = 'NA'
you can achieve by using np.where
df5['status'] = np.where((df5['age']>=1) & (df5['age']<=18), 'child', 'adult')
print(df5)
name age status
kashif 15 child
dawood 11 child
ali 33 adult
zain 21 adult
hamza 31 adult
That's my personal preference when working with pandas. I always use cut() method of pandas with a list of labels and bins to create a categorical variable:
import numpy as np
import pandas as pd
# making Data Frame
np.random.seed(41)
age=np.random.randint(1,50,5,int)
name=['kashif', 'dawood', 'ali', 'zain', 'hamza']
df=pd.DataFrame({'name':name, 'age':age})
# create a bin
bins = [0, 18, 50]
# create a bin label
label_list = ['adult', 'old']
# create a new column with bin and label
df['status'] = pd.cut(df.age, bins, labels=label_list)
use np.select
#specify conditions
conditions=[(df5['age']<=18),
(df5['age']>18)& (df5['age']<=50)]
#specify column output based on conditions
choices = ['child','adult'] #you can also specify numbers as well here
#create status column based on conditions
df5["status"] = np.select(conditions, choices)

dataframe slicing with loc [duplicate]

How do I select columns a and b from df, and save them into a new dataframe df1?
index a b c
1 2 3 4
2 3 4 5
Unsuccessful attempt:
df1 = df['a':'b']
df1 = df.ix[:, 'a':'b']
The column names (which are strings) cannot be sliced in the manner you tried.
Here you have a couple of options. If you know from context which variables you want to slice out, you can just return a view of only those columns by passing a list into the __getitem__ syntax (the []'s).
df1 = df[['a', 'b']]
Alternatively, if it matters to index them numerically and not by their name (say your code should automatically do this without knowing the names of the first two columns) then you can do this instead:
df1 = df.iloc[:, 0:2] # Remember that Python does not slice inclusive of the ending index.
Additionally, you should familiarize yourself with the idea of a view into a Pandas object vs. a copy of that object. The first of the above methods will return a new copy in memory of the desired sub-object (the desired slices).
Sometimes, however, there are indexing conventions in Pandas that don't do this and instead give you a new variable that just refers to the same chunk of memory as the sub-object or slice in the original object. This will happen with the second way of indexing, so you can modify it with the .copy() method to get a regular copy. When this happens, changing what you think is the sliced object can sometimes alter the original object. Always good to be on the look out for this.
df1 = df.iloc[0, 0:2].copy() # To avoid the case where changing df1 also changes df
To use iloc, you need to know the column positions (or indices). As the column positions may change, instead of hard-coding indices, you can use iloc along with get_loc function of columns method of dataframe object to obtain column indices.
{df.columns.get_loc(c): c for idx, c in enumerate(df.columns)}
Now you can use this dictionary to access columns through names and using iloc.
As of version 0.11.0, columns can be sliced in the manner you tried using the .loc indexer:
df.loc[:, 'C':'E']
is equivalent to
df[['C', 'D', 'E']] # or df.loc[:, ['C', 'D', 'E']]
and returns columns C through E.
A demo on a randomly generated DataFrame:
import pandas as pd
import numpy as np
np.random.seed(5)
df = pd.DataFrame(np.random.randint(100, size=(100, 6)),
columns=list('ABCDEF'),
index=['R{}'.format(i) for i in range(100)])
df.head()
Out:
A B C D E F
R0 99 78 61 16 73 8
R1 62 27 30 80 7 76
R2 15 53 80 27 44 77
R3 75 65 47 30 84 86
R4 18 9 41 62 1 82
To get the columns from C to E (note that unlike integer slicing, E is included in the columns):
df.loc[:, 'C':'E']
Out:
C D E
R0 61 16 73
R1 30 80 7
R2 80 27 44
R3 47 30 84
R4 41 62 1
R5 5 58 0
...
The same works for selecting rows based on labels. Get the rows R6 to R10 from those columns:
df.loc['R6':'R10', 'C':'E']
Out:
C D E
R6 51 27 31
R7 83 19 18
R8 11 67 65
R9 78 27 29
R10 7 16 94
.loc also accepts a Boolean array so you can select the columns whose corresponding entry in the array is True. For example, df.columns.isin(list('BCD')) returns array([False, True, True, True, False, False], dtype=bool) - True if the column name is in the list ['B', 'C', 'D']; False, otherwise.
df.loc[:, df.columns.isin(list('BCD'))]
Out:
B C D
R0 78 61 16
R1 27 30 80
R2 53 80 27
R3 65 47 30
R4 9 41 62
R5 78 5 58
...
Assuming your column names (df.columns) are ['index','a','b','c'], then the data you want is in the
third and fourth columns. If you don't know their names when your script runs, you can do this
newdf = df[df.columns[2:4]] # Remember, Python is zero-offset! The "third" entry is at slot two.
As EMS points out in his answer, df.ix slices columns a bit more concisely, but the .columns slicing interface might be more natural, because it uses the vanilla one-dimensional Python list indexing/slicing syntax.
Warning: 'index' is a bad name for a DataFrame column. That same label is also used for the real df.index attribute, an Index array. So your column is returned by df['index'] and the real DataFrame index is returned by df.index. An Index is a special kind of Series optimized for lookup of its elements' values. For df.index it's for looking up rows by their label. That df.columns attribute is also a pd.Index array, for looking up columns by their labels.
In the latest version of Pandas there is an easy way to do exactly this. Column names (which are strings) can be sliced in whatever manner you like.
columns = ['b', 'c']
df1 = pd.DataFrame(df, columns=columns)
In [39]: df
Out[39]:
index a b c
0 1 2 3 4
1 2 3 4 5
In [40]: df1 = df[['b', 'c']]
In [41]: df1
Out[41]:
b c
0 3 4
1 4 5
With Pandas,
wit column names
dataframe[['column1','column2']]
to select by iloc and specific columns with index number:
dataframe.iloc[:,[1,2]]
with loc column names can be used like
dataframe.loc[:,['column1','column2']]
You can use the pandas.DataFrame.filter method to either filter or reorder columns like this:
df1 = df.filter(['a', 'b'])
This is also very useful when you are chaining methods.
You could provide a list of columns to be dropped and return back the DataFrame with only the columns needed using the drop() function on a Pandas DataFrame.
Just saying
colsToDrop = ['a']
df.drop(colsToDrop, axis=1)
would return a DataFrame with just the columns b and c.
The drop method is documented here.
I found this method to be very useful:
# iloc[row slicing, column slicing]
surveys_df.iloc [0:3, 1:4]
More details can be found here.
Starting with 0.21.0, using .loc or [] with a list with one or more missing labels is deprecated in favor of .reindex. So, the answer to your question is:
df1 = df.reindex(columns=['b','c'])
In prior versions, using .loc[list-of-labels] would work as long as at least one of the keys was found (otherwise it would raise a KeyError). This behavior is deprecated and now shows a warning message. The recommended alternative is to use .reindex().
Read more at Indexing and Selecting Data.
You can use Pandas.
I create the DataFrame:
import pandas as pd
df = pd.DataFrame([[1, 2,5], [5,4, 5], [7,7, 8], [7,6,9]],
index=['Jane', 'Peter','Alex','Ann'],
columns=['Test_1', 'Test_2', 'Test_3'])
The DataFrame:
Test_1 Test_2 Test_3
Jane 1 2 5
Peter 5 4 5
Alex 7 7 8
Ann 7 6 9
To select one or more columns by name:
df[['Test_1', 'Test_3']]
Test_1 Test_3
Jane 1 5
Peter 5 5
Alex 7 8
Ann 7 9
You can also use:
df.Test_2
And you get column Test_2:
Jane 2
Peter 4
Alex 7
Ann 6
You can also select columns and rows from these rows using .loc(). This is called "slicing". Notice that I take from column Test_1 to Test_3:
df.loc[:, 'Test_1':'Test_3']
The "Slice" is:
Test_1 Test_2 Test_3
Jane 1 2 5
Peter 5 4 5
Alex 7 7 8
Ann 7 6 9
And if you just want Peter and Ann from columns Test_1 and Test_3:
df.loc[['Peter', 'Ann'], ['Test_1', 'Test_3']]
You get:
Test_1 Test_3
Peter 5 5
Ann 7 9
If you want to get one element by row index and column name, you can do it just like df['b'][0]. It is as simple as you can imagine.
Or you can use df.ix[0,'b'] - mixed usage of index and label.
Note: Since v0.20, ix has been deprecated in favour of loc / iloc.
df[['a', 'b']] # Select all rows of 'a' and 'b'column
df.loc[0:10, ['a', 'b']] # Index 0 to 10 select column 'a' and 'b'
df.loc[0:10, 'a':'b'] # Index 0 to 10 select column 'a' to 'b'
df.iloc[0:10, 3:5] # Index 0 to 10 and column 3 to 5
df.iloc[3, 3:5] # Index 3 of column 3 to 5
Try to use pandas.DataFrame.get (see the documentation):
import pandas as pd
import numpy as np
dates = pd.date_range('20200102', periods=6)
df = pd.DataFrame(np.random.randn(6, 4), index=dates, columns=list('ABCD'))
df.get(['A', 'C'])
One different and easy approach: iterating rows
Using iterows
df1 = pd.DataFrame() # Creating an empty dataframe
for index,i in df.iterrows():
df1.loc[index, 'A'] = df.loc[index, 'A']
df1.loc[index, 'B'] = df.loc[index, 'B']
df1.head()
The different approaches discussed in the previous answers are based on the assumption that either the user knows column indices to drop or subset on, or the user wishes to subset a dataframe using a range of columns (for instance between 'C' : 'E').
pandas.DataFrame.drop() is certainly an option to subset data based on a list of columns defined by user (though you have to be cautious that you always use copy of dataframe and inplace parameters should not be set to True!!)
Another option is to use pandas.columns.difference(), which does a set difference on column names, and returns an index type of array containing desired columns. Following is the solution:
df = pd.DataFrame([[2,3,4], [3,4,5]], columns=['a','b','c'], index=[1,2])
columns_for_differencing = ['a']
df1 = df.copy()[df.columns.difference(columns_for_differencing)]
print(df1)
The output would be:
b c
1 3 4
2 4 5
You can also use df.pop():
>>> df = pd.DataFrame([('falcon', 'bird', 389.0),
... ('parrot', 'bird', 24.0),
... ('lion', 'mammal', 80.5),
... ('monkey', 'mammal', np.nan)],
... columns=('name', 'class', 'max_speed'))
>>> df
name class max_speed
0 falcon bird 389.0
1 parrot bird 24.0
2 lion mammal 80.5
3 monkey mammal
>>> df.pop('class')
0 bird
1 bird
2 mammal
3 mammal
Name: class, dtype: object
>>> df
name max_speed
0 falcon 389.0
1 parrot 24.0
2 lion 80.5
3 monkey NaN
Please use df.pop(c).
I've seen several answers on that, but one remained unclear to me. How would you select those columns of interest?
The answer to that is that if you have them gathered in a list, you can just reference the columns using the list.
Example
print(extracted_features.shape)
print(extracted_features)
(63,)
['f000004' 'f000005' 'f000006' 'f000014' 'f000039' 'f000040' 'f000043'
'f000047' 'f000048' 'f000049' 'f000050' 'f000051' 'f000052' 'f000053'
'f000054' 'f000055' 'f000056' 'f000057' 'f000058' 'f000059' 'f000060'
'f000061' 'f000062' 'f000063' 'f000064' 'f000065' 'f000066' 'f000067'
'f000068' 'f000069' 'f000070' 'f000071' 'f000072' 'f000073' 'f000074'
'f000075' 'f000076' 'f000077' 'f000078' 'f000079' 'f000080' 'f000081'
'f000082' 'f000083' 'f000084' 'f000085' 'f000086' 'f000087' 'f000088'
'f000089' 'f000090' 'f000091' 'f000092' 'f000093' 'f000094' 'f000095'
'f000096' 'f000097' 'f000098' 'f000099' 'f000100' 'f000101' 'f000103']
I have the following list/NumPy array extracted_features, specifying 63 columns. The original dataset has 103 columns, and I would like to extract exactly those, then I would use
dataset[extracted_features]
And you will end up with this
This something you would use quite often in machine learning (more specifically, in feature selection). I would like to discuss other ways too, but I think that has already been covered by other Stack Overflower users.
To exclude some columns you can drop them in the column index. For example:
A B C D
0 1 10 100 1000
1 2 20 200 2000
Select all except two:
df[df.columns.drop(['B', 'D'])]
Output:
A C
0 1 100
1 2 200
You can also use the method truncate to select middle columns:
df.truncate(before='B', after='C', axis=1)
Output:
B C
0 10 100
1 20 200
To select multiple columns, extract and view them thereafter: df is the previously named data frame. Then create a new data frame df1, and select the columns A to D which you want to extract and view.
df1 = pd.DataFrame(data_frame, columns=['Column A', 'Column B', 'Column C', 'Column D'])
df1
All required columns will show up!
def get_slize(dataframe, start_row, end_row, start_col, end_col):
assert len(dataframe) > end_row and start_row >= 0
assert len(dataframe.columns) > end_col and start_col >= 0
list_of_indexes = list(dataframe.columns)[start_col:end_col]
ans = dataframe.iloc[start_row:end_row][list_of_indexes]
return ans
Just use this function
I think this is the easiest way to reach your goal.
import pandas as pd
cols = ['a', 'b']
df1 = pd.DataFrame(df, columns=cols)
df1 = df.iloc[:, 0:2]