Pandas - forward and backwards fill by index - python

I have a dataframe that has this overall structure:
(I know. It could be better, but this is what i have to work with :)
| patient_id | inclusion_timestamp | pre_event_1 | post_event_1 | post_event_2 |
|------------|---------------------|------------------|------------------|------------------|
| 1 | NaN | 27-06-2020 12:26 | NaN | NaN |
| 1 | 28-06-2020 13:05 | NaN | NaN | NaN |
| 1 | NaN | NaN | 29-06-2020 14:00 | NaN |
| 1 | NaN | NaN | NaN | 29-06-2020 23:57 |
| 2 | NaN | 29-06-2020 10:11 | NaN | NaN |
| 2 | 29-06-2020 18:26 | NaN | NaN | NaN |
| 2 | NaN | NaN | 30-06-2020 19:36 | NaN |
| 2 | NaN | NaN | NaN | 31-06-2020 21:20 |
| 3 | NaN | 29-06-2020 06:35 | NaN | NaN |
| 3 | NaN | 29-06-2020 07:28 | NaN | NaN |
| 3 | 30-06-2020 09:06 | NaN | NaN | NaN |
| 3 | NaN | NaN | NaN | 01-07-2020 12:10 |
and so forth.
The only way i know to do calculations from the inclusion_timestamp, is to fill forward from the inclusion_timestamp. However, this will yield wrong calculations for the pre_event_1 field, as it's column typically precedes the value for calculation.
Is there any way to do forward and backwards fill but only on the same index_col(patient_id)?
This way, the resulting dataframe will look like so:
| patient_id | inclusion_timestamp | pre_event_1 | post_event_1 | post_event_2 |
|------------|---------------------|------------------|------------------|------------------|
| 1 | 28-06-2020 13:05 | 27-06-2020 12:26 | NaN | NaN |
| 1 | 28-06-2020 13:05 | NaN | NaN | NaN |
| 1 | 28-06-2020 13:05 | NaN | 29-06-2020 14:00 | NaN |
| 1 | 28-06-2020 13:05 | NaN | NaN | 29-06-2020 23:57 |
| 2 | 29-06-2020 18:26 | 29-06-2020 10:11 | NaN | NaN |
| 2 | 29-06-2020 18:26 | NaN | NaN | NaN |
| 2 | 29-06-2020 18:26 | NaN | 30-06-2020 19:36 | NaN |
| 2 | 29-06-2020 18:26 | NaN | NaN | 31-06-2020 21:20 |
| 3 | 30-06-2020 09:06 | 29-06-2020 06:35 | NaN | NaN |
| 3 | 30-06-2020 09:06 | 29-06-2020 07:28 | NaN | NaN |
| 3 | 30-06-2020 09:06 | NaN | NaN | NaN |
| 3 | 30-06-2020 09:06 | NaN | NaN | 01-07-2020 12:10 |
I think that the answer is to iterate over the index column, and then apply forward and backwards fill within each patient_id, but i can't get my code to work...

Use DataFrame.groupby on column patient_id and using apply to ffill and bfill:
df['inclusion_timestamp'] = df.groupby('patient_id')['inclusion_timestamp']\
.apply(lambda x: x.ffill().bfill())
Or another idea using DataFrame.groupby with Series.combine_first:
g = df.groupby('patient_id')['inclusion_timestamp']
df['inclusion_timestamp'] = g.ffill().combine_first(g.bfill())
Another idea using two successive Series.groupby:
df['inclusion_timestamp'] = df['inclusion_timestamp'].groupby(df['patient_id'])\
.ffill().groupby(df['patient_id']).bfill()
Result:
patient_id inclusion_timestamp pre_event_1 post_event_1 post_event_2
0 1 28-06-2020 13:05 27-06-2020 12:26 NaN NaN
1 1 28-06-2020 13:05 NaN NaN NaN
2 1 28-06-2020 13:05 NaN 29-06-2020 14:00 NaN
3 1 28-06-2020 13:05 NaN NaN 29-06-2020 23:57
4 2 29-06-2020 18:26 29-06-2020 10:11 NaN NaN
5 2 29-06-2020 18:26 NaN NaN NaN
6 2 29-06-2020 18:26 NaN 30-06-2020 19:36 NaN
7 2 29-06-2020 18:26 NaN NaN 31-06-2020 21:20
8 3 30-06-2020 09:06 29-06-2020 06:35 NaN NaN
9 3 30-06-2020 09:06 29-06-2020 07:28 NaN NaN
10 3 30-06-2020 09:06 NaN NaN NaN
11 3 30-06-2020 09:06 NaN NaN 01-07-2020 12:10
Performance (measured using timeit):
df.shape
(1200000, 5)
%%timeit -n10 #Method 1 (Best Method)
263 ms ± 1.72 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
%%timeit -n10 #Method 2
342 ms ± 1.58 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
%%timeit -n10 #Method3
297 ms ± 4.83 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

Related

Accessing column of a list of nested json in a python dataframe

Identifier Properties
1 [{"$id":"2","SMName":"pia.redoabs.com","Type":"sms"},{"$id":"3","Name":"_18_Lucene41_0.doc","Type":"file"}]
2 [{"$id":"2","SMName":"pred.redocad.com","Type":"sms"},{"$id":"3","Name":"_18_Nil41_0.doc","Type":"file"}]
3 [{"$id":"2","SMName":"promomaster.com","Type":"sms"},{"$id":"3","Name":"_17_Litre41_0.doc","Type":"file"}]
4 [{"$id":"2","SMName":"admaster.com","Type":"sms"},{"$id":"3","Name":"_k.pos","Type":"file"}]
5 [{"$id":"2","SMName":"plan.com.com","Type":"sms"},{"$id":"3","Name":"_3_Lucene41_0.doc","Type":"file"}]
6 [{"$id":"2","Name":"jm460","ETNDomain":"ent.ad.ent","Sid":"S-1-5-21-117609710-2111687655-839522115-432193","AadUserId":"7133971dffgh5r-b9b8-4af3-bbfd-85b1b56d1f6f","IsDomainJoined":true,"Type":"account","UserPrincipalName":"jmjklo460#ent.com"},{"$id":"3","Directory":"C:\\CR\\new_cbest_malware","Name":"ent_Survey.zip","hash":[{"$id":"4","Algorithm":"hsa1","Value":"cecce931f21697876efc80f5897a31481c396795","Type":"hash"},{"$id":"5","Algorithm":"MI5","Value":"12c216630a5f24faab06d463c9ce72a5","Type":"hash"},{"$id":"6","Algorithm":"TM345","Value":"cbb327b70a83fefeaf744458f2ed62021e529ce0ece36566761779f17d4c07a6","Type":"hash"}],"CreatedTimeUtc":"2022-08-22T17:42:02.4272869Z","Type":"file"},{"$ref":"4"},{"$ref":"5"},{"$ref":"6"},{"$id":"7","ProcessId":"54884","CommandLine":"\"7zG.exe\" a -i#7zMap23807:40278:7zEvent24942 -ad -saa -- \"C:\\CR\\CR_2\"","ElevationToken":"Default","CreationTimeUtc":"2022-10-03T17:59:35.2339055Z","ImageFile":{"$id":"8","Directory":"C:\\Program Files\\7-Zip","Name":"9zG.exe","FileHashes":[{"$id":"9","Algorithm":"HSA2","Value":"df22612647e9404a515d48ebad490349685250de","Type":"hash"},{"$id":"10","Algorithm":"MI5","Value":"04fb3ae7f05c8bc333125972ba907398","Type":"hash"},{"$id":"11","Algorithm":"hsa1","Value":"2fb898bacb587f2484c9c4aa6da2729079d93d1f923a017bb84beef87bf74fef","Type":"hash"}],"CreatedTimeUtc":"2020-09-21T16:34:33.1299959Z","Type":"file"},"ParentProcess":{"$id":"12","ProcessId":"13516","CreationTimeUtc":"2022-09-21T12:41:32.4609401Z","CreatedTimeUtc":"2022-09-21T12:41:32.4609401Z","Type":"process"},"CreatedTimeUtc":"2022-10-03T17:59:35.2339055Z","Type":"process"},{"$ref":"12"},{"$ref":"8"},{"$ref":"9"},{"$ref":"10"},{"$ref":"11"},{"$id":"13","DnsDomain":"ent.ad.ent.com","HostName":"ilch-l788441","OSFamily":"Windows","OSVersion":"20H2","Tags":[{"ProviderName":"tmdp","TagId":"VwanPov","TagName":"VwanPov","TagType":"UserDefined"},{"ProviderName":"dmpt","TagId":"Proxy Allow Personal Storage","TagName":"Proxy Allow Personal Storage","TagType":"UserDefined"},{"ProviderName":"dmpt","TagId":"Proxy Allow Webmail","TagName":"Proxy Allow Webmail","TagType":"UserDefined"},{"ProviderName":"dmpt","TagId":"proxy-allow-social-media","TagName":"proxy-allow-social-media","TagType":"UserDefined"}],"Type":"host","dmptDeviceId":"fa52ff90ab60ee6eac86ec60ed2ac748a33e29fa","FQDN":"ilch-567.ent.ad.ent.com","AadDeviceId":"e1d59b69-dd3f-4f33-96b5-db9233654c16","RiskScore":"Medium","HealthStatus":"Active","LastSeen":"2022-10-03T18:09:32.7812655","LastExternalIpAddress":"208.95.144.39","LastIpAddress":"10.14.126.52","AvStatus":"Updated","OnboardingStatus":"Onboarded","LoggedOnUsers":[{"AccountName":"jmjklo460","DomainName":"ENT"}]}]
This is a dataframe with 2 columns "Identifier" & "Properties". The "Properties" column appears as a list of json.The aim is to create different columns for "sms" , "file" ,"ETNDomain" ,"UserPrincipalName" etc.
Not all the rows have same information as it is seen above. The first 5 rows are similar while the 6th row is different
Can we make the code dynamic to be able to extract any values ?
Further I used kql to parse this data & it was relatively straightforward. But having little/no experience with json it would be great if someone can help ?
You can use something like that. However, since there are many different keys in the json data, most rows will be nan.
import numpy as np
import ast
df['Properties']=df['Properties'].astype(str)
df['Properties']=df['Properties'].apply(lambda x: ast.literal_eval(x) if x != 'nan' else np.nan)
df = df.explode('Properties') #df is your dataframe.
final = df[['Identifier']].join(pd.json_normalize(df['Properties']))
'''
| | Identifier | $id | SMName | Type | Name | ETNDomain | Sid | AadUserId | IsDomainJoined | UserPrincipalName | Directory | hash | CreatedTimeUtc | $ref | ProcessId | CommandLine | ElevationToken | CreationTimeUtc | ImageFile.$id | ImageFile.Directory | ImageFile.Name | ImageFile.FileHashes | ImageFile.CreatedTimeUtc | ImageFile.Type | ParentProcess.$id | ParentProcess.ProcessId | ParentProcess.CreationTimeUtc | ParentProcess.CreatedTimeUtc | ParentProcess.Type | DnsDomain | HostName | OSFamily | OSVersion | Tags | dmptDeviceId | FQDN | AadDeviceId | RiskScore | HealthStatus | LastSeen | LastExternalIpAddress | LastIpAddress | AvStatus | OnboardingStatus | LoggedOnUsers |
|---:|-------------:|------:|:-----------------|:-------|:-------------------|------------:|------:|------------:|-----------------:|--------------------:|------------:|-------:|-----------------:|-------:|------------:|--------------:|-----------------:|------------------:|----------------:|----------------------:|-----------------:|-----------------------:|---------------------------:|-----------------:|--------------------:|--------------------------:|--------------------------------:|-------------------------------:|---------------------:|------------:|-----------:|-----------:|------------:|-------:|---------------:|-------:|--------------:|------------:|---------------:|-----------:|------------------------:|----------------:|-----------:|-------------------:|----------------:|
| 0 | 1 | 2 | pia.redoabs.com | sms | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan |
| 0 | 1 | 2 | pia.redoabs.com | sms | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan |
| 1 | 2 | 3 | nan | file | _18_Lucene41_0.doc | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan |
| 1 | 2 | 3 | nan | file | _18_Lucene41_0.doc | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan |
| 2 | 3 | 2 | pred.redocad.com | sms | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan |
| 2 | 3 | 2 | pred.redocad.com | sms | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan |
| 3 | 4 | 3 | nan | file | _18_Nil41_0.doc | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan |
| 3 | 4 | 3 | nan | file | _18_Nil41_0.doc | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan |
| 4 | 5 | 2 | promomaster.com | sms | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan |
| 4 | 5 | 2 | promomaster.com | sms | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan |
| 5 | 6 | 3 | nan | file | _17_Litre41_0.doc | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan |
| 5 | 6 | 3 | nan | file | _17_Litre41_0.doc | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan |
| 5 | 6 | 3 | nan | file | _17_Litre41_0.doc | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan |
| 5 | 6 | 3 | nan | file | _17_Litre41_0.doc | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan |
| 5 | 6 | 3 | nan | file | _17_Litre41_0.doc | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan |
| 5 | 6 | 3 | nan | file | _17_Litre41_0.doc | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan |
| 5 | 6 | 3 | nan | file | _17_Litre41_0.doc | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan |
| 5 | 6 | 3 | nan | file | _17_Litre41_0.doc | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan |
| 5 | 6 | 3 | nan | file | _17_Litre41_0.doc | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan |
| 5 | 6 | 3 | nan | file | _17_Litre41_0.doc | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan |
| 5 | 6 | 3 | nan | file | _17_Litre41_0.doc | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan |
| 5 | 6 | 3 | nan | file | _17_Litre41_0.doc | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan | nan |
'''

How to generate a dataframe column to keep track of a file version

I have a dataframe that stores the dates of creation and upgrades of files. I would like to write a function in order to create a column named 'version' keeping track of the files versions.
original dataset:
| fileID | creationDate | upgradeDate |
|--------|----------------------|----------------------|
| file_1 | 02/02/2022 15:00:00 | 02/02/2022 15:00:00 |
| file_1 | 02/02/2022 15:00:00 | 02/02/2022 15:30:00 |
| file_1 | 02/02/2022 15:00:00 | 10/02/2022 10:00:00 |
| file_2 | 03/02/2022 15:00:00 | 03/02/2022 15:00:00 |
| file_3 | 04/02/2022 15:00:00 | 04/02/2022 15:00:00 |
| file_3 | 04/02/2022 15:00:00 | 05/02/2022 15:00:00 |
what I would like:
| fileID | creationDate | upgradeDate | version |
|--------|----------------------|----------------------|---------|
| file_1 | 02/02/2022 15:00:00 | 02/02/2022 15:00:00 | 1 |
| file_1 | 02/02/2022 15:00:00 | 02/02/2022 15:30:00 | 2 |
| file_1 | 02/02/2022 15:00:00 | 10/02/2022 10:00:00 | 3 |
| file_2 | 03/02/2022 15:00:00 | 03/02/2022 15:00:00 | 1 |
| file_3 | 04/02/2022 15:00:00 | 04/02/2022 15:00:00 | 1 |
| file_3 | 04/02/2022 15:00:00 | 05/02/2022 15:00:00 | 2 |
I was able to do this through a for loop :
ids = df['fileID'].unique().tolist()
for id in ids:
df.loc[df['fileID'] == id, 'version'] = range(1, len(df.loc[df['fileID'] == id]) + 1)
But I would prefer not to use a for loop and do it with a function or list comprehension. Any idea ?

Pandas timestamp time mean

Input
Tech ID | 4th April'22 | 3rd April'22 | 2nd April'22
123 | 2022-04-04 05:03:00 | 2022-04-03 05:08:00 | 2022-04-02 05:10:00
345 | 2022-04-04 05:37:00 | 2022-04-03 05:18:00 | 2022-04-02 05:12:00
678 | 2022-04-04 05:42:00 | 2022-04-03 05:25:00 | 2022-04-02 05:30:00
901 | 2022-04-04 05:48:00 | 2022-04-03 05:45:00 | 2022-04-02 04:08:00
367 | 2022-04-04 05:32:00 | 2022-04-03 06:08:00 | 2022-04-02 06:08:00
Output
Tech ID | 4th April'22 | 3rd April'22 | 2nd April'22 | Mean
123 | 2022-04-04 05:03:00 | 2022-04-03 05:08:00 | 2022-04-02 05:10:00 | result
345 | 2022-04-04 05:37:00 | 2022-04-03 05:18:00 | 2022-04-02 05:12:00 | result
678 | 2022-04-04 05:42:00 | 2022-04-03 05:25:00 | 2022-04-02 05:30:00 | result
901 | 2022-04-04 05:48:00 | 2022-04-03 05:45:00 | 2022-04-02 04:08:00 | result
367 | 2022-04-04 05:32:00 | 2022-04-03 06:08:00 | 2022-04-02 06:08:00 | result
Depending on the version of Pandas that you are using, you may be able to use the Dataframe.mean method to determine the mean across the rows.
You will need to set numeric_only to False per the pandas documentation:
numeric_only: bool, default None
Include only float, int, boolean columns. If None, will attempt to use everything, >then use only numeric data. Not implemented for Series.
df['mean'] = df.mean(axis=1,numeric_only=False)
This works for me using pandas 1.3.4 assuming the dtype of the 3 source columns are all datetime64[ns]

How to unpivot pandas dataframe to create datetime column

I have a pivoted pandas dataframe that looks like the one below.
I need to unpivot it into a dataframe indexed by datetime, and the variables (columns) reduced to only one of each.
I tried using melt but I am struggling to reshape it because of the hour row.
What would be the best option to reshape such a dataframe?
The dataframe I have
+----------+------+------+------+------+------+
| nan | var1 | var1 | var2 | var2 | var3 |
+----------+------+------+------+------+------+
| Hour | 2 | 3 | 0 | 2 | 0 |
| 1/1/2019 | 0.8 | 0.4 | 0.6 | 0.9 | 0.7 |
| 1/2/2019 | 0.2 | 0.2 | 0.7 | 0.3 | 0.1 |
| 1/3/2019 | 0.1 | 0.0 | 0.3 | 0.4 | 1.0 |
+----------+------+------+------+------+------+
The dataframe I need to get
+---------------+------+------+------+
| Datetime | var1 | var2 | var3 |
+---------------+------+------+------+
| 1/1/2019 0:00 | NaN | 0.6 | 0.7 |
| 1/1/2019 1:00 | NaN | NaN | NaN |
| 1/1/2019 2:00 | 0.8 | 0.9 | NaN |
| 1/1/2019 3:00 | 0.4 | NaN | NaN |
| 1/2/2019 0:00 | NaN | 0.7 | 0.1 |
| 1/2/2019 1:00 | NaN | NaN | NaN |
| 1/2/2019 2:00 | 0.2 | 0.3 | NaN |
| 1/2/2019 3:00 | 0.2 | NaN | NaN |
| 1/3/2019 0:00 | NaN | 0.3 | 1.0 |
| 1/3/2019 1:00 | NaN | NaN | NaN |
| 1/3/2019 2:00 | 0.1 | 0.4 | NaN |
| 1/3/2019 3:00 | 0.0 | NaN | NaN |
+---------------+------+------+------+
Here's a really shitty answer that is unidiomatic pandas, but gets the job done given the data you presented in the format you presented in. If you have massive amount of data I highly recommend you find a more optimized way.
dff = df.copy()
mn, mx = df.loc['Hour'].agg([min, max]).astype(int)
df = df.loc[df.index.repeat(mx-mn+1)]
df = df.loc[df.index != 'Hour']
df = df.assign(time=list(range(mn,mx+1))*(mx-mn))
df = df.set_index('time', append=True).iloc[:,:0]
for i,v in enumerate(dff.columns):
d = dff.iloc[:, i].to_frame()
hour = d.at['Hour', v]
for idx, row in d.iloc[1:].iterrows():
df.loc[(idx, hour), v] = row[v]
df = df.reset_index().rename(columns={0: 'date'})
df['datetime'] = df[['date', 'time']].apply(lambda x: f"{x['date']} {x['time']}:00", axis=1)
df = df.drop(columns=['date', 'time']).set_index('datetime').reset_index()
print(df)
datetime v1 v2 v3
0 1/1/2019 0:00 NaN 0.6 0.7
1 1/1/2019 1:00 NaN NaN NaN
2 1/1/2019 2:00 0.8 0.9 NaN
3 1/1/2019 3:00 0.4 NaN NaN
4 1/2/2019 0:00 NaN 0.7 0.1
5 1/2/2019 1:00 NaN NaN NaN
6 1/2/2019 2:00 0.2 0.3 NaN
7 1/2/2019 3:00 0.2 NaN NaN
8 1/3/2019 0:00 NaN 0.3 1.0
9 1/3/2019 1:00 NaN NaN NaN
10 1/3/2019 2:00 0.1 0.4 NaN
11 1/3/2019 3:00 0.0 NaN NaN

Rate of change over last n hours using pandas timeseries

I would like to add columns to a time-indexed pandas DataFrame which contain the rate of change over the last n hours for each of the existing columns. I have accomplished this with the following code, however, it is too slow for my needs (probably due to looping over every index of each column?).
Is there a (computationally) faster way to do this?
roc_hours = 12
tol = 1e-10
for c in ts.columns:
c_roc = c + ' +++ RoC ' + str(roc_hours) + 'h'
ts[c_roc] = np.nan
for i in ts.index[np.isfinite(ts[c])]:
df = ts[c][i - np.timedelta64(roc_hours, 'h'):i]
X = (df.index.values - df.index.values.min()).astype('Int64')*2.77778e-13 #hours back
Y = df.values
if Y.std() > tol and X.shape[0] > 1:
fit = np.polyfit(X,Y,1)
ts[c_roc][i] = fit[0]
else:
ts[c_roc][i] = 0
Edit input dataframe ts is irregularly sampled and can contain NaNs. First few lines of input ts:
+---------------------+-------------------+------+------+--------------------+-------------------+------------------+
| WCT | a | b | c | d | e | f |
+---------------------+-------------------+------+------+--------------------+-------------------+------------------+
| 2011-09-04 20:00:00 | | | | | | |
| 2011-09-04 21:00:00 | | | | | | |
| 2011-09-04 22:00:00 | | | | | | |
| 2011-09-04 23:00:00 | | | | | | |
| 2011-09-05 02:00:00 | 93.0 | 97.0 | 20.0 | 209.0 | 85.0 | 98.0 |
| 2011-09-05 03:00:00 | 74.14285714285714 | 97.0 | 20.0 | 194.14285714285717 | 74.42857142857143 | 98.0 |
| 2011-09-05 04:00:00 | 67.5 | 98.5 | 20.0 | 176.0 | 75.0 | 98.0 |
| 2011-09-05 05:00:00 | 72.0 | 98.5 | 20.0 | 176.0 | 75.0 | 98.0 |
| 2011-09-05 07:00:00 | 80.0 | 93.0 | 19.0 | 186.0 | 71.0 | 97.0 |
| 2011-09-05 08:00:00 | 80.0 | 93.0 | 19.0 | 186.0 | 71.0 | 97.0 |
| 2011-09-05 09:00:00 | 78.5 | 98.0 | 19.0 | 186.0 | 71.0 | 97.0 |
| 2011-09-05 10:00:00 | 73.0 | 98.0 | 19.0 | 186.0 | 71.0 | 97.0 |
| 2011-09-05 11:00:00 | 77.0 | 98.0 | 18.0 | 175.0 | 87.0 | 97.0999984741211 |
| 2011-09-05 12:00:00 | 78.0 | 98.0 | 19.0 | 163.0 | 57.0 | 98.4000015258789 |
| 2011-09-05 15:00:00 | 78.0 | 98.0 | 19.0 | 163.0 | 57.0 | 98.4000015258789 |
+---------------------+-------------------+------+------+--------------------+-------------------+------------------+
Edit 2
After profiling, the bottleneck is in the slicing step: df = ts[c][i - np.timedelta64(roc_hours, 'h'):i]. This line pulls out observations time-stamped between now-roc_hours and now. It's very handy syntax, but is taking up the bulk of the compute time.
Works on a dataset of mine, haven't checked on yours:
import pandas as pd
from numpy import polyfit
from matplotlib import style
style.use('ggplot')
# ... acquire a dataframe named *water* with a column *value*
WINDOW = 10
ax=water.value.plot()
roll = pd.rolling_mean(water.value, WINDOW)
roll.plot(ax=ax)
def lintrend(df):
df = df.tolist()
m, b = polyfit(range(len(df)), df,1)
return m
linny = pd.rolling_apply(water.value, WINDOW, lintrend)
linny.plot(ax=ax)
Casting the numpy.ndarray to list after rolling_apply cast it to numpy.ndarray seems inelegant. Suggestions?

Categories

Resources