python pandas - 航班到达和起飞时间在单独的行 - 匹配并加入同一行然后绘制甘特图

python pandas - flight arrival and departure time on separate row - to be matched and joined to same row then draw gantt chart

假设我有一个这样的数据框(笔记本文本版本如下图):

A为Arrival Flight(着陆),D为Departure flight(起飞)。 Carrier 和 FltReg 一起是一架飞机......到达和离开机场,它将再次返回同一机场......几小时或几天后。 Acft是飞机的类型。

到达和离开需要匹配,以便生成的数据框可用于计算和绘制甘特图(开始时间即到达时间和结束时间即出发时间......航班在地面上的时间.)

数据通常会持续 7 天的航班时刻表和更多航空公司。7 天大约 3000 行...来自 sql 服务器数据库

from io import StringIO
import pandas as pd

dfstr = StringIO(u"""
ID;Car;FltNo;Acft;FltReg;E_FltType;Rtg;STADDtTm;ArrDep
0;EK;376;77W;A6ECI;T/A;DXB-BKK-DXB;03/05/2017 12:50;A
1;EK;377;77W;A6ECI;T/A;DXB-BKK-DXB;03/05/2017 15:40;D
2;EK;384;380;A6EDL;T/S;DXB-BKK-HKG;02/05/2017 12:15;A
3;EK;384;380;A6EDL;T/S;DXB-BKK-HKG;02/05/2017 14:00;D
4;EK;385;380;A6EDL;T/A;HKG-BKK-DXB;02/05/2017 23:45;A
5;EK;385;380;A6EDL;T/A;HKG-BKK-DXB;03/05/2017 01:15;D
54;VZ;920;320;HSVKA;DEP ONLY;BKK-HPH;01/05/2017 11:15;D
55;VZ;921;320;HSVKA;ARR ONLY;HPH-BKK;01/05/2017 15:25;A
56;VZ;602;320;HSVKA;DEP ONLY;BKK-CNX;01/05/2017 16:35;D
57;VZ;603;320;HSVKA;ARR ONLY;CNX-BKK;01/05/2017 19:45;A
58;VZ;602;320;HSVKA;DEP ONLY;BKK-CNX;02/05/2017 11:15;D
59;VZ;603;320;HSVKA;ARR ONLY;CNX-BKK;02/05/2017 14:25;A
60;VZ;820;320;HSVKA;DEP ONLY;BKK-HKT;03/05/2017 07:05;D
61;VZ;821;320;HSVKA;ARR ONLY;HKT-BKK;03/05/2017 15:45;A
62;VZ;828;320;HSVKA;DEP ONLY;BKK-HKT;03/05/2017 18:20;D
63;VZ;829;320;HSVKA;ARR ONLY;HKT-BKK;03/05/2017 21:50;A
64;VZ;600;320;HSVKB;DEP ONLY;BKK-CNX;01/05/2017 06:10;D
65;VZ;601;320;HSVKB;ARR ONLY;CNX-BKK;01/05/2017 09:20;A
66;VZ;606;320;HSVKB;DEP ONLY;BKK-CNX;01/05/2017 09:50;D
67;VZ;607;320;HSVKB;ARR ONLY;CNX-BKK;01/05/2017 13:00;A

""")
df = pd.read_csv(dfstr, sep=";", index_col='ID')
df

问题1:如何将上面的dataframe转换成下面的。

如果 Car 和 FltReg 相同,我希望将其转换为相同的行。例如ID 0,EK376 A6ECI 5 月 3 日抵达 12:50 并作为 ID 1,EK377 A6ECI 于 5 月 3 日 15:40 离开......同样对于 ID2 和 3,ID4 和 5......这些是突出显示的 3 架不同的飞机以粗体显示。中间还有许多其他航班……然后接下来是 ID54,它是一架带有 HSKVA 飞机的 VZ 承运人……它首先起飞,所以它应该在自己的行上……然后它到达 ID55 并以 ID56 离开,以 ID57 再次到达,以 ID58 离开。

结果数据框应该是这样的:

from io import StringIO
import pandas as pd

dfstr = StringIO(u"""
IDArr;Car;FltNo;Acft;FltReg;E_FltType;Rtg;STADDtTm;ArrDep;IDDep;Car;FltNo;Acft;FltReg;E_FltType;Rtg;STADDtTm;ArrDep
0;EK;376;77W;A6ECI;T/A;DXB-BKK-DXB;03/05/2017 12:50;A;1;EK;377;77W;A6ECI;T/A;DXB-BKK-DXB;03/05/2017 15:40;D
2;EK;384;380;A6EDL;T/S;DXB-BKK-HKG;02/05/2017 12:15;A;3;EK;384;380;A6EDL;T/S;DXB-BKK-HKG;02/05/2017 14:00;D
4;EK;385;380;A6EDL;T/A;HKG-BKK-DXB;02/05/2017 23:45;A;5;EK;385;380;A6EDL;T/A;HKG-BKK-DXB;03/05/2017 01:15;D
;;;;;;;;;54;VZ;920;320;HSVKA;DEP ONLY;BKK-HPH;01/05/2017 11:15;D
55;VZ;921;320;HSVKA;ARR ONLY;HPH-BKK;01/05/2017 15:25;A;56;VZ;602;320;HSVKA;DEP ONLY;BKK-CNX;01/05/2017 16:35;D
57;VZ;603;320;HSVKA;ARR ONLY;CNX-BKK;01/05/2017 19:45;A;58;VZ;602;320;HSVKA;DEP ONLY;BKK-CNX;02/05/2017 11:15;D
59;VZ;603;320;HSVKA;ARR ONLY;CNX-BKK;02/05/2017 14:25;A;60;VZ;820;320;HSVKA;DEP ONLY;BKK-HKT;03/05/2017 07:05;D
61;VZ;821;320;HSVKA;ARR ONLY;HKT-BKK;03/05/2017 15:45;A;62;VZ;828;320;HSVKA;DEP ONLY;BKK-HKT;03/05/2017 18:20;D
63;VZ;829;320;HSVKA;ARR ONLY;HKT-BKK;03/05/2017 21:50;A;;;;;;;;;
;;;;;;;;;64;VZ;600;320;HSVKB;DEP ONLY;BKK-CNX;01/05/2017 06:10;D
65;VZ;601;320;HSVKB;ARR ONLY;CNX-BKK;01/05/2017 09:20;A;66;VZ;606;320;HSVKB;DEP ONLY;BKK-CNX;01/05/2017 09:50;D
67;VZ;607;320;HSVKB;ARR ONLY;CNX-BKK;01/05/2017 13:00;A;;;;;;;;;

""")
df2 = pd.read_csv(dfstr, sep=";")
df2

如您所见...我们可以看到 ID0 和 ID1 匹配在同一行...因此更容易看到航班在地面(即在机场)的时间...从 12:50 到 15:40(2 小时 50 分钟)...其余航班依此类推。

问题 2:使用上述结果数据框制作甘特图

生成的数据框将用于生成甘特图。

这是示例飞机:HSKVA(VZ 航班)将有自己的行...首先是 11:15 起飞(甘特图从 10:15 绘制(起飞前 1 小时,因为没有到达) 11:15。然后在同一行绘制甘特图 15:25 到 16:35,19:45 到 11:15 第二天,14:25 到 07:05, 15:45 to 18:20, 21:50 to 22:50 (航班到达后一小时没有起飞). matplotlib的broken_barh来了记住

HSKVB 将有自己的甘特图行...等等。

每个 Carrier/Aircraft 都在自己的视觉行上注册。

问题一

对您的设置的一个快速更改是我没有将 ID 设置为 index_col,因为我想在 groupby().shift 中快速使用它的值。所以从修改后的 read_csv:

开始
df = pd.read_csv(dfstr, sep=";")
cols = df.columns.values.tolist()

解决方案的很大一部分是确保 df 按 CarFltRegSTADDtTm 排序(因为前两个是唯一标识符,最后一个是主要排序值)。

sort_cols = ['Car', 'FltReg', 'STADDtTm']
df.sort_values(by=sort_cols, inplace=True)

现在我们进入了逻辑的主要部分。我要将 df 分为到达和离开,两者的连接方式是通过 shifted ID。也就是说,对于任何 (Car, FltReg) 分区,我知道将给定的 'A' 行与紧随其后的 'D' 行配对。同样,这就是我们需要排序(和完整)数据的原因。

让我们生成转换后的 ID:

# sort_cols[:2] is `Car` and `FltReg` together
df['NextID'] = df.groupby(sort_cols[:2])['ID'].shift(1)

现在使用 'A' 过滤后的 df 和 'D' 过滤后的 df,我将把它们全外连接在一起。到达(左数据集)以原始 ID 为键,出发(右数据集)以我们刚刚制作的 NextID 为键。

df_display = df[df['ArrDep'] == 'A'] \
                 .merge(df[df['ArrDep'] == 'D'],
                       how='outer',
                       left_on='ID',
                       right_on='NextID',
                       suffixes=('1', '2'))

请注意,这些列现在将以 1(左)和 2(右)作为后缀。

此时,这个新数据框 df_display 具有它需要的所有行,但在您的最终显示中它没有很好的排序。为此,您再次需要 sort_cols 列表,但每个列的 coalesced 版本将各自的左右版本放在一起。例如,Car1Car2 必须合并在一起,以便您可以按合并版本对 所有行 进行排序。

pandas' combine_first 就像合并。

# purely for sorting the final display
for c in sort_cols: 
    df_display['sort_' + c] = df_display[c + '1'] \
                                  .combine_first(df_display[c + '2'])
    # for example, Car1 and Car2 have now been coalesced into sort_Car

df_display.sort_values(by=['sort_{}'.format(c) for c in sort_cols], inplace=True)

我们快完成了。现在 df_display 有我们不需要的无关列。我们可以 select 只有我们想要的列——基本上,原始列列表的两个副本 cols.

df_display = df_display[['{}1'.format(c) for c in cols] + ['{}2'.format(c) for c in cols]]
df_display.to_csv('output.csv', index=None)

我检查了(在 csv 导出中以便我们可以看到广泛的数据集)这与您的示例匹配。

问题二

好的,所以如果您尝试一下 https://matplotlib.org/examples/pylab_examples/broken_barh.html 中的代码,您就会看到 broken_barh 是如何运行的。这很重要,因为我们必须使数据适合这种结构才能使用它。 broken_barh 的第一个参数是要绘制的元组列表,每个元组是一个(开始时间,持续时间)。

对于 matplotlib,开始时间必须采用其特殊的日期格式。所以我们必须使用 matplotlib.dates.date2num 转换 pandas 日期时间。最后,持续时间似乎以天为单位。

因此,如果 HSVKA 到达 2017-05-01 15:25:00 并在地面上停留 70 分钟,则 broken_barh 需要绘制元组 (mdates.date2num(Timestamp('2017-05-03 15:25:00')), 70 minutes in day units or 0.04861).

所以第一步是以这种格式从问题 1 中得到 df_display。我们现在只需要关注四列'Car1', 'FltReg1', 'STADDtTm1', 'STADDtTm2'

import matplotlib.pyplot as plt
import matplotlib.dates as mdates
import seaborn # optional ... I like the look

print(df_display[['Car1', 'FltReg1', 'STADDtTm1', 'STADDtTm2']])

看起来像

   Car1 FltReg1         STADDtTm1         STADDtTm2
0    EK   A6ECI  03/05/2017 12:50  03/05/2017 15:40
1    EK   A6EDL  02/05/2017 12:15  02/05/2017 14:00
2    EK   A6EDL  02/05/2017 23:45  03/05/2017 01:15
10  NaN     NaN               NaN  01/05/2017 11:15
3    VZ   HSVKA  01/05/2017 15:25  01/05/2017 16:35
4    VZ   HSVKA  01/05/2017 19:45  02/05/2017 11:15
5    VZ   HSVKA  02/05/2017 14:25  03/05/2017 07:05
6    VZ   HSVKA  03/05/2017 15:45  03/05/2017 18:20
7    VZ   HSVKA  03/05/2017 21:50               NaN
11  NaN     NaN               NaN  01/05/2017 06:10
8    VZ   HSVKB  01/05/2017 09:20  01/05/2017 09:50
9    VZ   HSVKB  01/05/2017 13:00               NaN

到达或离开时有 NaN 秒。估算这些是相当简单的。我在您的文章中注意到,当缺少某些内容时,您希望两侧各有一个小时的缓冲时间。所以这就是所有直接的争论:

df_gantt = df_display.copy()

# Convert to pandas timestamps for date arithmetic
df_gantt['STADDtTm1'] = pd.to_datetime(df_gantt['STADDtTm1'],
                                       format='%d/%m/%Y %H:%M')
df_gantt['STADDtTm2'] = pd.to_datetime(df_gantt['STADDtTm2'],
                                       format='%d/%m/%Y %H:%M')

# Impute identifiers
df_gantt['Car'] = df_gantt['Car1'].combine_first(df_gantt['Car2'])
df_gantt['FltReg'] = df_gantt['FltReg1'].combine_first(df_gantt['FltReg2'])

# Also just gonna combine Car and FltReg
# into a single column for simplicty
df_gantt['Car_FltReg'] = df_gantt['Car'] + ': ' +  df_gantt['FltReg']

# Impute hour gaps
df_gantt['STADDtTm1'] = df_gantt['STADDtTm1'] \
                            .fillna(df_gantt['STADDtTm2'] - pd.Timedelta('1 hour'))
df_gantt['STADDtTm2'] = df_gantt['STADDtTm2'] \
                            .fillna(df_gantt['STADDtTm1'] + pd.Timedelta('1 hour'))

# Date diff in day units
df_gantt['DayDiff'] = (df_gantt['STADDtTm2'] - df_gantt['STADDtTm1']).dt.seconds \
                          / 60 / 60 / 24

# matplotlib numeric date format
df_gantt['STADDtTm1'] = df_gantt['STADDtTm1'].apply(mdates.date2num)
df_gantt['STADDtTm2'] = df_gantt['STADDtTm2'].apply(mdates.date2num)

df_gantt = df_gantt[['Car_FltReg', 'STADDtTm1', 'STADDtTm2', 'DayDiff']]
print(df_gantt)

现在看起来像

   Car_FltReg      STADDtTm1      STADDtTm2   DayDiff
0   EK: A6ECI  736452.534722  736452.652778  0.118056
1   EK: A6EDL  736451.510417  736451.583333  0.072917
2   EK: A6EDL  736451.989583  736452.052083  0.062500
10  VZ: HSVKA  736450.427083  736450.468750  0.041667
3   VZ: HSVKA  736450.642361  736450.690972  0.048611
4   VZ: HSVKA  736450.822917  736451.468750  0.645833
5   VZ: HSVKA  736451.600694  736452.295139  0.694444
6   VZ: HSVKA  736452.656250  736452.763889  0.107639
7   VZ: HSVKA  736452.909722  736452.951389  0.041667
11  VZ: HSVKB  736450.215278  736450.256944  0.041667
8   VZ: HSVKB  736450.388889  736450.409722  0.020833
9   VZ: HSVKB  736450.541667  736450.583333  0.041667

现在创建一个字典,其中每个键都是唯一的 Car_FltReg,每个值都是一个元组列表(如前所述),可以输入 broken_barh.

dict_gantt = df_gantt.groupby('Car_FltReg')['STADDtTm1', 'DayDiff'] \
                 .apply(lambda x: list(zip(x['STADDtTm1'].tolist(),
                                           x['DayDiff'].tolist()))) \
                 .to_dict()

所以dict_gantt看起来像

{'EK: A6ECI': [(736452.5347222222, 0.11805555555555557)],
 'EK: A6EDL': [(736451.5104166666, 0.07291666666666667),
               (736451.9895833334, 0.0625)],
 'VZ: HSVKA': [(736450.4270833334, 0.041666666666666664),
               (736450.6423611111, 0.04861111111111111),
               (736450.8229166666, 0.6458333333333334),
               (736451.6006944445, 0.6944444444444445),
               (736452.65625, 0.1076388888888889),
               (736452.9097222222, 0.041666666666666664)],
 'VZ: HSVKB': [(736450.2152777778, 0.041666666666666664),
               (736450.3888888889, 0.020833333333333332),
               (736450.5416666666, 0.041666666666666664)]}

非常适合 broken_barh。而现在这一切都是 matplotlib 的疯狂。在准备 broken_barh 东西的核心逻辑之后,其他一切都只是艰苦的刻度格式等。如果你在 matplotlib 中定制过一些东西,这些东西应该很熟悉——我不会解释太多。

FltReg_list = sorted(dict_gantt, reverse=True)

fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)

start_datetime = df_gantt['STADDtTm1'].min()
end_datetime = df_gantt['STADDtTm2'].max()

# parameters for yticks, etc.
# you might have to play around
# with the different parts to modify
n = len(FltReg_list)
bar_size = 9

for i, bar in enumerate(FltReg_list):
    ax.broken_barh(dict_gantt[bar],          # data
                   (10 * (i + 1), bar_size), # (y position, bar size)
                   alpha=0.75,
                   edgecolor='k',
                   linewidth=1.2)

# I got date formatting ideas from
# https://matplotlib.org/examples/pylab_examples/finance_demo.html
ax.set_xlim(start_datetime, end_datetime)
ax.xaxis.set_major_locator(mdates.HourLocator(byhour=range(0, 24, 6)))
ax.xaxis.set_major_formatter(mdates.DateFormatter('%m-%d %H:%M'))
ax.xaxis.set_minor_locator(mdates.HourLocator(byhour=range(0, 24, 1)))
# omitting minor labels ...

plt.grid(b=True, which='minor', color='w', linestyle='dotted')

ax.set_yticks([5 + 10 * n for n in range(1, n + 1)])
ax.set_ylim(5, 5 + 10 * (n + 1))
ax.set_yticklabels(FltReg_list)

ax.set_title('Time on Ground')
ax.set_ylabel('Carrier: Registration')

plt.setp(plt.gca().get_xticklabels(), rotation=30, horizontalalignment='right')

plt.tight_layout()
fig.savefig('gantt.png', dpi=200)

这是最终输出。