Как удалить дубликаты в фрейме данных Pandas на основе условия

#python #pandas

#python #pandas

Вопрос:

Я создаю небольшое программное обеспечение для подсчета, которое в основном подсчитывает общее количество людей, присутствующих внутри помещения. Фрейм данных, который я получаю из базы данных микроконтроллера (который позволяет людям входить и выходить), имеет человеческую ошибку, при которой иногда пользователь имеет выход перед входом. Таким образом, в фрейме данных есть экземпляры, в которых одна запись имеет несколько выходов перед другой последующей записью. Df — это что-то вроде этого:

 date     timestamp  type    cardno      status
**20201006  55737   PC010   117016056   Valid Card Exit**
20201006    55907   PC010   117016056   Valid Card Entry
20201006    60312   PC006   100024021   Valid Card Entry
20201006    61311   PC006   100024021   Valid Card Exit
20201006    61445   PC006   100024021   Valid Card Entry
20201006    61538   PC006   100024021   Valid Card Exit
20201006    61646   PC010   117016056   Valid Card Exit
20201006    61933   PC006   100024021   Valid Card Entry
20201006    61938   PC010   117016056   Valid Card Entry
20201006    62025   PC006   100024021   Valid Card Exit
20201006    62041   PC010   117016056   Valid Card Exit
20201006    62042   PC006   100024021   Valid Card Entry
20201006    62225   PC010   117016056   Valid Card Entry
20201006    62527   PC006   100024021   Valid Card Exit
20201006    63018   PC006   100024021   Valid Card Entry
20201006    64832   PC007   116057383   Valid Card Entry
20201006    64834   PC011   117016074   Valid Card Entry
**20201006  64952   PC012   116054003   Valid Card Exit**
  

Записи с ** — это, по сути, то, что сотрудник нажимает exit перед записью (по какой-либо причине), и это сбивает подсчет. Я хочу избавиться от всех таких экземпляров в фрейме данных. Мне очень трудно заниматься этим tbh. Программное обеспечение для подсчета, которое я создал до сих пор, в основном считывает базу данных firebird, а затем создает из нее разные фреймы данных, продолжает считать ее форму, а затем отображает результат в виде простого HTML на большом экране, размещенном в помещении. Фрейм данных, который я описал выше с проблемой, называется «contractorDf» в программе, которую я запускаю в производство (тестирование), как показано ниже:

 import subprocess
from datetime import datetime
from datetime import date
import pandas as pd
import re
import os
import sys
   
#------------------------------------------------------PRODUCTION-----------------------------------------#
# Generating a Temporary Date for Production Environment
tempDate = date(2020, 10, 6)
tempDate = str(tempDate)
tempDate = tempDate.replace('-', '')
#------------------------------------------------------PRODUCTION----------------------------------------#
   
################################################################################################################################
# Getting Current Day (This will be used in real environment)
currentDay = datetime.now().day

if currentDay < 10:
    currentDay = str(currentDay)
    currentDay = '0'  currentDay
else:
    currentDay = str(currentDay)


# Getting Current Year amp; Month
currentYear = datetime.now().year
currentMonth = datetime.now().month
currentYear = str(currentYear)
currentMonth = str(currentMonth)
currentYearMonth = currentYear currentMonth
currentYearMonthDay = currentYearMonth currentDay

# Getting Variable for After FROM
currentTableName = 'ST' currentYearMonth

# Getting Final Query (Commented Right now because Testing)
query = "SELECT * FROM "   currentYearMonth   " "   "WHERE TRDATE="   currentYearMonthDay   ";"
finalQuery = bytes(query, 'ascii')
#############################################################################################################################


#-------------------------------------------------------PRODUCTION------------------------------------------------------#
# Making a temporary Table Name and Query for Production Environment
tempTableName = 'ST' currentYearMonth
nonByteQuery = "SELECT * FROM "   tempTableName   " "   "WHERE TRDATE="   tempDate   ";"
tempQuery = bytes(nonByteQuery, 'ascii')
#-------------------------------------------------------PRODUCTION------------------------------------------------------#



# Generating record.csv file from command prompt (Before initiating this, C:\Program Files (x86)\FireBird\FireBird_2_1\bin should be in the environment variables)
p = subprocess.Popen('isql', shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
p.stdin.write(b'CONNECT "C:\Users\JH\OneDrive\Desktop\EntryPass\P1_Server\event\TRANS.fdb";') #The italicized b is because its a Byte size code and we can't 
p.stdin.write(b'OUTPUT "C:\Users\JH\OneDrive\Desktop\EntryPass\P1_Server\event\record.csv";')
p.stdin.write(tempQuery)
p.stdin.write(b'OUTPUT;')
p.communicate()
p.terminate()
# Terminating the Command Prompt Window



# Reading the record file that is just generated above
tempdf = pd.read_csv('C:\Users\JH\OneDrive\Desktop\EntryPass\P1_Server\event\record.csv', sep='delimeter', engine='python', header=None, skipinitialspace=True)
pd.set_option('display.max_colwidth', -1)
pd.set_option('display.max_columns', None)
pd.set_option('display.max_rows', None)
pd.set_option('display.width', 1000)

#tempdf = tempdf[0].astype(str)
columns = ["TRDATE", "TRTIME", "TRCODE", "TRDESC", "CTRLTAG", "CTRLNAME", "CTRLIP", "CARDNO", "STAFFNO", "STAFFNAME", "DEPTNAME", "JOBNAME", "SHIFTNAME", "DEVTYPE", "DEVNAME", "DEVNO", "TRID", "ISCAP", "RCGROUP", "POLLTIME", "SENDSEQ", "RECSEQ", "IOBNO", "IOBNAME", "ZONENO", "ZONENAME", "POINTNO", "POINTNAME", "ISSNAPRET", "PROTRAG"]
header = tempdf.iloc[0]
linespace = tempdf.iloc[1]
header = str(header)
header = header[5:]
header = header[:-24]
linespace = str(linespace)
linespace = linespace[7:]
linespace = linespace[:-23]

tempdf = tempdf[~tempdf[0].str.contains(header)]
tempdf = tempdf[~tempdf[0].str.contains(linespace)]
tempdf = tempdf[0].str.replace(' ', ',')
df = tempdf.str.split(",", n=400, expand=True)
df = df[[0,1,7,8,9,10,31,41,42,43,52,53,54]]
df[100] = df[7].map(str)   ' '   df[8].map(str)   ' '   df[9].map(str)   ' '   df[10].map(str)
df = df.drop([7,8,9,10], axis=1)
df[101] = df[31].map(str)   df[41].map(str)
df = df.drop([31,41], axis=1)
df[102] = df[43].map(str)   df[52].map(str)   df[53].map(str)   df[54].map(str)
df = df.drop([43,52,53,54], axis=1)

def newblock(column):
    if column[42].startswith('VIS'):
        return column[42]
    else:
        pass


df = df.assign(newblock=df.apply(newblock, axis=1))

df[42] = df[42].str.replace('VIS_dddddddddd', '')

df[105] = df[42].map(str)   df[101].map(str)
df = df.drop([42,101], axis=1)
df[106] = df[102].map(str)   df['newblock'].map(str)
df = df.drop(['newblock', 102], axis=1)
df[106] = df[106].str.replace('None', '')
df = df[[0,1,106,105,100]]
columns = ['date', 'timestamp', 'type', 'cardno', 'status']
df.columns = df.columns.map(str)
df.columns = columns
df = df.reset_index()
df = df.drop(['index'], axis=1)




#Making Visitor Counter
visitorDf = df[df['type'].str.startswith('VIS')]
#visitorDf = visitorDf[~visitorDf['status'].str.contains('Unknown')]
visitorIn1 = len(visitorDf[visitorDf['status'].str.contains('Unknown')])
VisitorIn1 = int(visitorIn1)
visitorDf = visitorDf.reset_index()
visitorDf = visitorDf.drop(('index'), axis=1)
visitorIn = len(visitorDf[visitorDf['status'].str.contains('Valid Card Entry')])
visitorOut = len(visitorDf[visitorDf['status'].str.contains('Valid Card Exit')])
visitorIn = int(visitorIn)
visitorOut = int(visitorOut)
totalVisitor = visitorIn1   visitorIn - visitorOut

#Making Contractor Counter
contractorDf = df[df['type'].str.startswith('PC')]
#contractorDf = contractorDf[~contractorDf['status'].str.contains('Unknown')]
contractorIn1 = len(contractorDf[contractorDf['status'].str.contains('Unknown')])
contractorIn1 = int(contractorIn1)
contractorDf = contractorDf.reset_index()
contractorDf = contractorDf.drop(('index'), axis=1)
contractorIn = len(contractorDf[contractorDf['status'].str.contains('Valid Card Entry')])
contractorOut = len(contractorDf[contractorDf['status'].str.contains('Valid Card Exit')])
contractorIn = int(contractorIn)
contractorOut = int(contractorOut)
totalContractor = contractorIn1   contractorIn - contractorOut


#Making Employee Counter
employeeDf = df[df['type'].str.contains('^d', regex=True)]
#employeeDf = employeeDf[~employeeDf['status'].str.contains('Unknown')]
employeeIn1 = len(employeeDf[employeeDf['status'].str.contains('Unknown')])
employeeIn1 = int(employeeIn1)
employeeDf = employeeDf.reset_index()
employeeDf = employeeDf.drop(('index'), axis=1)
employeeIn = len(employeeDf[employeeDf['status'].str.contains('Valid Card Entry')])
employeeOut = len(employeeDf[employeeDf['status'].str.contains('Valid Card Exit')])
employeeIn = int(employeeIn)
employeeOut = int(employeeOut)
totalEmployee = employeeIn1   employeeIn - employeeOut


os.remove('C:\Users\JH\OneDrive\Desktop\EntryPass\P1_Server\event\record.csv')

visitor = totalVisitor
employee = totalEmployee
contractor = totalContractor

if os.path.exists('C:\Apache24\htdocs\counter\index.html'):
    os.remove('c:\Apache24\htdocs\counter\index.html')
else:
    pass

f = open('C:\Apache24\htdocs\counter\index.html', 'w')

message = """
<html lang="en-US" class="hide-scroll">
    <head>
        <title>Emhart Counter</title>
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
        <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" integrity="sha384-JcKb8q3iqJ61gNV9KGb8thSsNjpSL0n8PARn9HuZOnIxN0hoP VmmDGMN5t9UJ0Z" crossorigin="anonymous">
        <style>
        body {{
            background-color: lightblue;
        }}

        .verticalCenter {{
            margin: 0;
            top: 100%;
            -ms-transform: translateY(25%);
            transform: translateY(25%);
        }}
        </style>
    </head>
    <body>
        <center>
            <div class=“verticalCenter">
                <h1 style=font-size:100px>VISITORS: amp;emsp;amp;emsp;amp;emsp;amp;emsp;amp;emsp;amp;emsp; {visitor}</h1><br></br><br></br>
                <h1 style=font-size:100px>EMPLOYEES: amp;emsp;amp;emsp;amp;emsp;amp;emsp;amp;emsp;amp;emsp; {employee}</h1><br></br><br></br>
                <h1 style=font-size:100px>CONTRACTORS: amp;emsp;amp;emsp;amp;emsp;amp;emsp;amp;emsp;amp;emsp; {contractor}</h1><br></br><br></br><br></br><br></br>
                <h3 style=font-size: 50px>THIS IS A TEST RUN<h3>
            </div>
        </center>
    </body>
</html>"""


new_message = message.format(visitor=visitor, employee=employee, contractor=contractor)
f.write(new_message)
f.close()


sys.exit()
  

Единственная оставшаяся проблема заключается в том, как мне избавиться от выходов для cardno / type до того, как у него появится соответствующая запись в contractorDf. Я был бы очень признателен за любую помощь по этому вопросу.

Комментарии:

1. Последний пример кода кажется избыточным.

2. Кроме того, двойные звезды, похоже, вводят в заблуждение. Я предполагаю, что они отсутствуют в исходном наборе данных.

Ответ №1:

Для вашего примера будут работать startswith и endswith . Для более сложных шаблонов регулярных выражений используйте contains.

 mask = df.date.str.startswith("**")
print(df[mask])

# or

mask = df.status.str.endswith("**")
print(df[mask])
  

Выводит:

          date timestamp   type     cardno             status
0  **20201006     55737  PC010  117016056  Valid_Card_Exit**
3  **20201006     64952  PC012  116054003  Valid_Card_Exit**
  

Настройка:

 columns = ['date','timestamp','type','cardno','status']
data = [el.split(",") for el in ['**20201006,55737,PC010,117016056,Valid_Card_Exit**',
'20201006,55907,PC010,117016056,Valid_Card_Entry',
'20201006,64834,PC011,117016074,Valid_Card_Entry',
'**20201006,64952,PC012,116054003,Valid_Card_Exit**']]
df = pd.DataFrame(data, columns=columns)
  

Ответ №2:

Трюк Cumsum

Ключом к проблеме является часто встречающийся математический трюк. Сначала мы рассматриваем entry как 1 , и exit как отмену ввода, а именно -1 . Тогда событие выхода является плохим, если оно сначала выдает отрицательную совокупную сумму ( cumsum ) до этой строки. Т.Е., Когда произошло событие выхода, его нельзя интерпретировать как правильную отмену предыдущей записи. Однако обратите внимание, что последующие отрицательные cumsum значения могут быть вызваны предыдущими неверными значениями. Поэтому мы идентифицируем ТОЛЬКО первое отрицательное значение cumsum как плохое.

Основываясь на приведенном выше наблюдении, можно рекурсивно находить первую неверную запись для каждой карты до тех пор, пока не будет получено отрицательное значение cumsum.

Код

Реализация демонстрирует, как сделать это рекурсивно. Он не совсем оптимизирован для больших наборов данных, но достоинство должно быть как-то похоже.

 # initialize
df["retain"] = True
df["delta"] = -1
df.loc[df["status"] == "Valid Card Entry", "delta"] = 1

def recurse(df):

    # sort for cumsum (bad values found were not retained)
    df_sorted = df[df["retain"]].sort_values(by=["cardno", "timestamp"]).reset_index(drop=True)

    # cumsum
    df_sorted["cumsum"] = df_sorted[["cardno", "delta"]].groupby("cardno").cumsum()

    # get the first occurrence of negative cumsum
    df_dup = df_sorted[df_sorted["cumsum"] < 0].groupby("cardno").first()

    # termination condition: no more bad values were found
    if len(df_dup) == 0:
        return

    # else, remove the bad rows
    for cardno, row in df_dup.iterrows():
        df.loc[(df["cardno"] == cardno) amp; (df["timestamp"] == row["timestamp"]), "retain"] = False

# execute    
recurse(df)

del df["delta"]  # optional cleanup
  

Вывод

Смотрите столбец «сохранить» ( False = неудачные выходы).

 df
Out[61]: 
        date  timestamp   type     cardno            status  retain
0   20201006      55737  PC010  117016056   Valid Card Exit   False
1   20201006      55907  PC010  117016056  Valid Card Entry    True
2   20201006      60312  PC006  100024021  Valid Card Entry    True
3   20201006      61311  PC006  100024021   Valid Card Exit    True
4   20201006      61445  PC006  100024021  Valid Card Entry    True
5   20201006      61538  PC006  100024021   Valid Card Exit    True
6   20201006      61646  PC010  117016056   Valid Card Exit    True
7   20201006      61933  PC006  100024021  Valid Card Entry    True
8   20201006      61938  PC010  117016056  Valid Card Entry    True
9   20201006      62025  PC006  100024021   Valid Card Exit    True
10  20201006      62041  PC010  117016056   Valid Card Exit    True
11  20201006      62042  PC006  100024021  Valid Card Entry    True
12  20201006      62225  PC010  117016056  Valid Card Entry    True
13  20201006      62527  PC006  100024021   Valid Card Exit    True
14  20201006      63018  PC006  100024021  Valid Card Entry    True
15  20201006      64832  PC007  116057383  Valid Card Entry    True
16  20201006      64834  PC011  117016074  Valid Card Entry    True
17  20201006      64952  PC012  116054003   Valid Card Exit   False
  

Для демонстрации cumsum ниже показаны s до и после очистки. Набор данных сортируется по (cardno, timestamp) , а date столбец удаляется для наглядности.

Перед

 df_sorted
Out[69]: 
    timestamp   type     cardno            status  retain  delta  cumsum
0       60312  PC006  100024021  Valid Card Entry    True      1       1
1       61311  PC006  100024021   Valid Card Exit    True     -1       0
2       61445  PC006  100024021  Valid Card Entry    True      1       1
3       61538  PC006  100024021   Valid Card Exit    True     -1       0
4       61933  PC006  100024021  Valid Card Entry    True      1       1
5       62025  PC006  100024021   Valid Card Exit    True     -1       0
6       62042  PC006  100024021  Valid Card Entry    True      1       1
7       62527  PC006  100024021   Valid Card Exit    True     -1       0
8       63018  PC006  100024021  Valid Card Entry    True      1       1
9       64952  PC012  116054003   Valid Card Exit    True     -1      -1
10      64832  PC007  116057383  Valid Card Entry    True      1       1
11      55737  PC010  117016056   Valid Card Exit    True     -1      -1
12      55907  PC010  117016056  Valid Card Entry    True      1       0
13      61646  PC010  117016056   Valid Card Exit    True     -1      -1
14      61938  PC010  117016056  Valid Card Entry    True      1       0
15      62041  PC010  117016056   Valid Card Exit    True     -1      -1
16      62225  PC010  117016056  Valid Card Entry    True      1       0
17      64834  PC011  117016074  Valid Card Entry    True      1       1
  

После

 df_sorted
Out[73]: 
    timestamp   type     cardno            status  retain  delta  cumsum
0       60312  PC006  100024021  Valid Card Entry    True      1       1
1       61311  PC006  100024021   Valid Card Exit    True     -1       0
2       61445  PC006  100024021  Valid Card Entry    True      1       1
3       61538  PC006  100024021   Valid Card Exit    True     -1       0
4       61933  PC006  100024021  Valid Card Entry    True      1       1
5       62025  PC006  100024021   Valid Card Exit    True     -1       0
6       62042  PC006  100024021  Valid Card Entry    True      1       1
7       62527  PC006  100024021   Valid Card Exit    True     -1       0
8       63018  PC006  100024021  Valid Card Entry    True      1       1
9       64832  PC007  116057383  Valid Card Entry    True      1       1
10      55907  PC010  117016056  Valid Card Entry    True      1       1
11      61646  PC010  117016056   Valid Card Exit    True     -1       0
12      61938  PC010  117016056  Valid Card Entry    True      1       1
13      62041  PC010  117016056   Valid Card Exit    True     -1       0
14      62225  PC010  117016056  Valid Card Entry    True      1       1
15      64834  PC011  117016074  Valid Card Entry    True      1       1