Package: ppinot4py
What is PPINot4Py?PPINot4Py is a Python implementation ofPPINot, used to compute process performance indicators (PPIs) for event logs.A quick exampleIn the following example, we use theRoad Traffic Fine event logto show how ppinot4py can be used to compute some PPIs. There is also avideowith an extended version of this short example. The log is in XES format, so we usepm4pyto load it into a dataframe:importppinot4pyfromppinot4pyimportmodelimportpandasaspdimportpm4pyfrompm4py.objects.conversion.logimportconverteraslog_converter# Loads the event loglog=pm4py.read_xes('Road_Traffic_Fine_Management_Process.xes')# Transforms the event log into a pandas dataframedf=log_converter.apply(log,variant=log_converter.Variants.TO_DATA_FRAME)# Converts the timestamp column into a timestampdf['time:timestamp']=pd.to_datetime(df['time:timestamp'],utc=True)# Computes the time between activity Create Fine and activity Send Finetm=model.TimeMeasure('`concept:name` == "Create Fine"','`concept:name` == "Send Fine"')result=ppinot4py.measure_computer(tm,df)The value of result is:idA1134days01:00:00A100132days01:00:00A10000129days23:00:00A10001119days23:00:00A10004118days23:00:00...V999548days00:00:00V999648days00:00:00V999748days00:00:00V999848days00:00:00V999948days00:00:00Name:t,Length:150370,dtype:timedelta64[ns]We can also execute more complex metrics. For instance, we can get the percentage of cases in which the time between Create Fine and Send Fine is less than 90 days yearly grouped as follows:create_to_send_fine_90_days=model.DerivedMeasure("create_to_send_fine < days90",{"create_to_send_fine":tm,"days90":pd.Timedelta(days=90)})avg_create_to_send_fine_90_days=model.AggregatedMeasure(create_to_send_fine_90_days,'avg')ppinot4py.measure_computer(avg_create_to_send_fine_90_days,df,time_grouper=pd.Grouper(freq='1Y'))The result is:datacase_end2000-12-3100:00:00+00:000.3700002001-12-3100:00:00+00:000.4888302002-12-3100:00:00+00:000.7814792003-12-3100:00:00+00:000.5677772004-12-3100:00:00+00:000.4019802005-12-3100:00:00+00:000.0161072006-12-3100:00:00+00:000.0870012007-12-3100:00:00+00:000.0626282008-12-3100:00:00+00:000.2545782009-12-3100:00:00+00:000.1785802010-12-3100:00:00+00:000.3674122011-12-3100:00:00+00:000.3560822012-12-3100:00:00+00:000.4608122013-12-3100:00:00+00:000.829418This is just a small example of what can be done with ppinot4py. Next, you can find the details on how to use it.ConditionsMeasures need conditions to specify when to count or when to start or stop measuring time. In ppinot4py, you can specify these conditions in 2 different ways.1. Time Instant Condition:countStateCount=DataObjectState("`concept:name` == 'Close'")countConditionCount=TimeInstantCondition(countStateCount)countMeasureCount=CountMeasure(countConditionCount)or simplycountMeasureExample=CountMeasure('`concept:name` == "Close"')ATimeInstantConditionis True when the conditions changes in the event log from(!condition) -> (condition), so if our condition is "concept:name==A", and we have this secuence: A B A A A, the result will be True, False, True, False False.The expression language that can be used to specify the condition is the same that can be used inpandas DataFrame.query().It is also possible to specifyTimeInstantConditions that refers to the beginning or the end of the case:beginCaseCondition=TimeInstantCondition(Runtime.START,AppliesTo.PROCESS)endCaseCondition=TimeInstantCondition(Runtime.END,AppliesTo.PROCESS)or that refers to the beginning, end, or any other lifecycle state recorded in the log of an activity:beginActivityCreateFine=TimeInstantCondition(Runtime.START,AppliesTo.ACTIVITY,"Create Fine")endActivityCreateFine=TimeInstantCondition(Runtime.END,AppliesTo.ACTIVITY,"Create Fine")2. Series ConditionIt is also possible to directly give the program a pandas Series with the calculated Boolean values.Measure computerThe measure computer function receives parameters, 3 of them are optional:defmeasure_computer(measure,dataframe,log_configuration=LogConfiguration(id_case='case:concept:name',time_column='time:timestamp',transition_column='lifecycle:transition',activity_column='concept:name'),time_grouper=None):LogConfigurationis a specification of the names of four attributes of the log that identify the case id (id_case), the time of the event (time_column), the name of the activity (activity_column) and the lifecycle transitions of the activity (transition_column). By default, all of these attributes will have the standard names as specified by theXES standard. In case the user have custom names for these columns, they must be indicated.Time grouper is apandas Grouper objectthat indicates how to group the results of an aggregated measure based on the time each case finishes.MeasuresCount MeasureA count measure is used to count how many times a condition occurs in each case of the event log. It is composed of an unique attributewhenthat can be astringor aTimeInstantConditionand refers to the condition we want to evaluate.classCountMeasure():def__init__(self,when):self.when=whenExample: For a certain event log and the following condition:countState=DataObjectState('concept:name == "In Progress"')countCondition=TimeInstantCondition(countState)countMeasure=CountMeasure(countCondition)measure_computer(countMeasure,dataframe)We obtain:case_concept_name1-3642857687.01-46715394616.01-5035737727.01-5045385558.01-50607164628.0...1-7408659532.01-7408659692.01-7408666911.01-7408667081.01-7408668210.0Length:7554,dtype:float64Data measureA data measure obtains values from the attributes of the event log and it is composed of 3 values:data_content_selection: The attribute you want to select.precondition: Condition you want to apply to the dataset, can beTimeInstantCondition, pandasSeriesor string.first: Boolean value, if is true, it will take the first filtered value of each case. If it is false, it will take the last value.classDataMeasure():def__init__(self,data_content_selection,precondition,first):self.data_content_selection=data_content_selectionself.precondition=preconditionself.first=firstExample: For a certain event log and the following condition:countState=DataObjectState("org:group == 'V5 3rd'")precondition=TimeInstantCondition(countState)dataMeasure=DataMeasure("lifecycle:transition",precondition,True)measure_computer(dataMeasure,dataframe)We obtain the value oflifecycle:transitionfor those cases where the precondition is met:case_concept_name1-364285768AwaitingAssignment1-692918254InProgressName:lifecycle:transition,dtype:objectTime measureA time measure measure the time between two time instants. It is composed of the following attributes:from_condition: The starter condition where we want to count we will refer to it as 'A', it can be aTimeInstantCondition, a pandasSeriesor a stringto_condition: The final condition, we will refer to ir as 'B' it can be aTimeInstantCondition, a pandasSeriesor a stringtime_measure_type: Linear or Cyclic. By default, it is LinearLinear: Counts the time elapsed between the firstAand the lastBCyclic: Counts the time elapsed between all pairs ofAandBsingle_instance_agg_function: Type of aggregation we want to apply to our data, it only applies to cyclic measures. The default value isSUM. There are 5 types of operations:SUM:The sum of allAtoBpairsMIN:Minimum time value between theAtoBpairsMAX:Maximum time value between theAtoBpairsAVG:The average time between allAtoBpairsGROUPBY:Raw grouped dataframe with no operation appliedfirst_to:Only applies to linear measures and it indicates if we want to take the first occurrence of 'B' condition or the last. By default it is False.precondition:Condition applied before the calculation of A and B.business_duration:If provided, the time measure will take the business hours and holidays specified in this parameter.classTimeMeasure():def__init__(self,from_condition,to_condition,time_measure_type='Linear',single_instance_agg_function='SUM',first_to='False',precondition=None,business_duration=None):self.from_condition=from_conditionself.to_condition=to_conditionself.time_measure_type=time_measure_typeself.single_instance_agg_function=single_instance_agg_functionself.precondition=preconditionself.first_to=first_toself.business_duration=business_durationIn this Linear example, we want to calculate how much time has passed between 'In progress' and the last 'Closed':condition_A=TimeInstantCondition('`lifecycle:transition` == "In Progress"')condition_B=TimeInstantCondition('`lifecycle:transition` == "Closed"')time_measure_linear=TimeMeasure(condition_A,condition_B)measure_computer(time_measure_linear,dataframe)case_concept_name1-364285768771days08:26:331-467153946477days13:10:031-512795200401days08:29:231-537219938318days12:45:491-543979253292days14:10:21...1-7408613712days18:28:501-7408620610days01:45:071-7408620809days23:18:501-7408659533days02:17:031-7408659693days02:13:18Name:data,Length:4904,dtype:timedelta64[ns]In this cyclic example, we want to calculate the average time of all pairs 'In Progress' - 'Awaiting Assignment' along all cases:condition_A=TimeInstantCondition('`lifecycle:transition` == "In Progress"')condition_C=TimeInstantCondition('`lifecycle:transition` == "Awaiting Assignment"')time_measure_cyclic=TimeMeasure(condition_A,condition_C,'CYCLIC','AVG')measure_computer(time_measure_cyclic,dataframe)case_concept_name1-3642857680days00:12:02.2500001-46715394638days21:55:53.6666671-5035737723days21:29:361-5045385551days01:46:431-5060716466days06:51:22.583333...1-7408597810days03:17:48.3333331-7408620610days00:05:591-7408620800days00:03:421-7408659530days00:02:161-7408659690days00:01:23Name:data,Length:3669,dtype:timedelta64[ns]The business duration to compute time considering only business hours is specified using classBusinessDurationas follows:business=BusinessDuration(business_start=time(7,0,0),business_end=time(17,0,0),weekend_list=[5,6],holiday_list=pyholidays.ES(prov='AN'),unit_hour='sec')Wherebusiness_startandbusiness_endare the times for the beginning and the end of the working day,weekend_listis the specification of the days that include the weekend (from 0 to 6),holiday_listis a list of the holidays (package pyholidays can be used for that), andunit_houris the time unit in which the measure will be computed. The valid values are:day,hour,min, andsec.Aggregated measureAn aggregated measure aggregates the results obtained from any of the three previous base measures. It is composed of the following attributes:base_measure:Can be any kind of the previous measures (Time, Count or Data)single_instance_agg_function:Operation we want to apply to data of each Time aggrupationSUM:Sum of all valuesMIN:Minimum valueMAX:Maximum valueAVG:Average of all valuesGROUPBY:Raw grouped dataframe with no operation applieddata_grouper:List of measures to group by the base measure.filter_to_apply:Filter to apply to the base_measure. It is a base measure that returns a boolean valueclassAggregatedMeasure():def__init__(self,base_measure,single_instance_agg_function,data_grouper,filter_to_apply):self.base_measure=base_measureself.filter_to_apply=filter_to_applyself.single_instance_agg_function=single_instance_agg_functionself.data_grouper=data_grouperThe following example computes a linear time measure between 'In Progress' and 'Closed' and aggregates the values grouping by each 60 seconds:importpandasaspdcondition_A=TimeInstantCondition('lifecycle:transition == "In Progress"')condition_B=TimeInstantCondition('lifecycle:transition == "Closed"')time_measure=TimeMeasure(condition_A,condition_B)aggregated_measure=AggregatedMeasure(time_measure,'SUM')time_grouper_60s=pd.Grouper(freq='60s')measure_computer(aggregated_measure,dataframe,time_grouper=time_grouper_60s)time_to_calculate2012-05-0105:58:00+00:0018days05:59:562012-05-0105:59:00+00:000days00:00:002012-05-0106:00:00+00:000days00:00:002012-05-0106:01:00+00:000days00:00:002012-05-0106:02:00+00:000days00:00:00...2012-05-2223:18:00+00:000days00:00:002012-05-2223:19:00+00:00213days05:00:362012-05-2223:20:00+00:00947days00:30:032012-05-2223:21:00+00:00437days23:18:162012-05-2223:22:00+00:001233days22:43:00Freq:60S,Name:data_seconds,Length:31285,dtype:timedelta64[ns]We can also group them, for example, in intervals of 2 weeks:aggregated_measure=AggregatedMeasure(time_measure,'SUM')measure_computer(aggregated_measure,dataframe,time_grouper=pd.Grouper(freq='2W'))time_to_calculate2012-05-0600:00:00+00:006554days17:33:542012-05-2000:00:00+00:0053639days01:32:152012-06-0300:00:00+00:006794days05:15:30Freq:2W-SUN,Name:data_seconds,dtype:timedelta64[ns]Derived measureA derived measure computes a measure by applying arithmetical or boolean functions to several other measures. It is composed of 2 attributes:function_expression:Function that we want to apply to some measures. Can be arithmetical or booleanExample: (A + B) / C where A,B and C are the result of previous metricsmeasure_map:A dictionary where the key values are the name we want to assign to that measure, and the values are the measure definitions.classDerivedMeasure():def__init__(self,function_expression,measure_map):self.function_expression=function_expressionself.measure_map=measure_mapFor instance, we define 3 linear time measures, create the dictionary and then we define the functiontime_measure_A=TimeMeasure(condition_A,condition_B)time_measure_B=TimeMeasure(condition_B,condition_A)time_measure_C=TimeMeasure(condition_A,condition_C)measure_dictionary={'A':time_measure_A,'B':time_measure_B,'C':time_measure_C}derived_measure=DerivedMeasure('(A + B) / C',measure_dictionary)measure_computer(derived_measure,dataframe)case_concept_name1-3642857680days06:41:34.2852491-4671539461days04:26:22.6377171-5127952000days23:40:01.3832921-5372199380days08:40:38.5346201-5439792530days13:33:22.743243...1-7408613710days00:00:001-7408620610days00:00:17.5682451-7408620800days01:04:40.7657661-7408659530days00:32:46.3455881-7408659690days00:53:39.253012Length:4904,dtype:timedelta64[ns]One can also use derived measures to define boolean expressions. For instance, we can define a boolean derived measure that returns true when the number ofSend Fineactivities in a case is greater or equal than 1.has_send_fine=DerivedMeasure('count_send_fine >= 1',{"count_send_fine":CountMeasure(TimeInstantCondition(RuntimeState.END,AppliesTo.ACTIVITY,"Send Fine"))})measure_computer(has_send_fine,df)If the data type used in the comparison is an object, then it has to be added as a parameter in the expression and in the measure map. For instance, in the following example, we definedays90with the valuepd.Timedelta(days=90):create_fine_to_send_fine=TimeMeasure('`concept:name` == "Create Fine"','`concept:name` == "Send Fine"')create_to_send_fine_90_days=DerivedMeasure("create_to_send_fine < days90",{"create_to_send_fine":create_fine_to_send_fine,"days90":pd.Timedelta(days=90)})measure_computer(avg_create_to_send_fine_90_days,df,time_grouper=pd.Grouper(freq='1Y'))