Inspecting Cities Skylines In-Game Transfer Offers.md¶
Preface¶
I created a Mod to record all transfer reasons, incoming offers, and outgoing offers from student citizens for 1 month (in CS timeline). This notebook aims to inspect the data exported from my mod.
The mod monitors three specific functions from the source code and creates a log record whenever the functions are executed.
The functions the mod monitors are: - SimulationStep: runs every frame for each citizen - AddOutgoingOffer: runs whenever a citizen is trying to create an outgoing offer - FindVisitPlace: runs whenever a building is creating an incoming offer
Whenever a citizen wants to go to work/school, the game checks to see if they have a workplace assigned. If not, the citizen creates an outgoing offer and waits for the offer to get accepted. Once the offer is accepted, the workplace is assigned to that citizen. The citizen won't create any other outgoing offers as long as they don't want to change their workplace. The same goes for homeplaces.
Visitplaces are assigned temporarily. If a citizen wants to go shopping, they create a transfer offer. When their offer gets accepted, their visitplace is assigned. Once they finish shopping, their visitplace is removed immediately.
The school for a student is assigned as their workplace. Students only make outgoing offers when they are not assigned to any school.
You can find the source code of the mod and the logs from here:
https://github.com/developers412/Cities_Skylines/tree/CitiesOSMods/LoggingMod
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
Case #1: Static City¶
I ran the mod on a city I created before. The city had about 37K citizens. The city had elementry schools, highschools and universities before recording. I didn't add or remove anything while the mod was recording.
# the name of the log file we're going to insepct
logfile = "studentLog6.csv"
# parsing the logfile
df = pd.read_csv(logfile)
df
| CitizenID | PositionX | PositionY | PositionZ | TransferReason | BuildingName | currentLocation | m_service | m_subService | |
|---|---|---|---|---|---|---|---|---|---|
| 0 | 235533 | 682.58840 | 95.00727 | -322.80610 | NaN | Expensive Playground | Visit | Beautification | None |
| 1 | 235539 | 406.79900 | 120.05250 | -1478.79500 | NaN | H2 4x3 Shop07 | Visit | Commercial | CommercialHigh |
| 2 | 235539 | 205.11310 | 137.96360 | -1929.67300 | NaN | H3 4x4 Tenement08b | Home | Residential | ResidentialHigh |
| 3 | 235547 | -334.60580 | 112.31580 | 40.35154 | NaN | H4 4x3 Tenement10 | Home | Residential | ResidentialHigh |
| 4 | 235547 | -130.62590 | 103.17870 | 43.21632 | NaN | H4 3x4 tenement05a | Home | Residential | ResidentialHigh |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 69252 | 468783 | 59.61536 | 104.82810 | -115.53540 | NaN | H2 4x3 Tenement06 | Home | Residential | ResidentialHigh |
| 69253 | 468865 | 59.61536 | 104.82810 | -115.53540 | NaN | H2 4x3 Tenement06 | Home | Residential | ResidentialHigh |
| 69254 | 470842 | -642.51770 | 116.22030 | -253.40910 | NaN | H3 4x3 Tenement09 | Home | Residential | ResidentialHigh |
| 69255 | 476867 | -111.07570 | 107.04330 | -209.94170 | NaN | H1 1x1 Tenement | Home | Residential | ResidentialHigh |
| 69256 | 478013 | -339.05140 | 115.22240 | 72.29230 | NaN | H4 3x4 Tenement11 | Home | Residential | ResidentialHigh |
69257 rows × 9 columns
# create a boolean mask where the TransferReason isn't a NaN value
# if the TransferReason is NaN, it means we were recording from the
# simulationStep function,
mask = ~df['TransferReason'].isnull()
# count all transfer reasons recorded
df[mask]['TransferReason'].value_counts()
Single2 3469
Single2B 3413
ShoppingC 350
ShoppingG 325
Shopping 302
ShoppingE 302
ShoppingB 299
ShoppingD 297
ShoppingH 293
ShoppingF 277
EntertainmentD 148
Entertainment 133
EntertainmentC 132
EntertainmentB 130
ChildCare 126
PartnerAdult 8
Sick 7
Name: TransferReason, dtype: int64
Although we log offers only from studnet citizens, there's no studnet1, student2, or studnet3 transfer reasons recorded.
It seems that students don't create transfer offers as long as they know where their schools are.
Case #2: Delete all schools, then start recording¶
I used the same city. But I decided to delete all schools and universities before recording. I ran the simulation for a while after deleting and then added new schools and universities.
# the name of the log file we're going to insepct for case #2
logfile = "studentLog7.csv"
# parsing the logfile
df = pd.read_csv(logfile)
df
| CitizenID | PositionX | PositionY | PositionZ | TransferReason | BuildingName | currentLocation | m_service | m_subService | |
|---|---|---|---|---|---|---|---|---|---|
| 0 | 82472 | -72.59171 | 127.5228 | -1525.53400 | NaN | H3 4x3 Tenement09 | Home | Residential | ResidentialHigh |
| 1 | 82472 | -56.25000 | 0.0000 | -1518.75000 | Student2 | NaN | NaN | NaN | NaN |
| 2 | 82472 | -72.59171 | 127.5228 | -1525.53400 | NaN | H3 4x3 Tenement09 | Home | Residential | ResidentialHigh |
| 3 | 82475 | 732.47920 | 95.4660 | 399.37210 | NaN | L4 4x3 Villa11 | Home | Residential | ResidentialLow |
| 4 | 82496 | 242.22690 | 111.3989 | -585.01860 | NaN | H3 4x3 Tenement04a | Home | Residential | ResidentialHigh |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 124002 | 218915 | -34.86323 | 136.6353 | -1933.04400 | NaN | H3 4x4 Tenement05a | Home | Residential | ResidentialHigh |
| 124003 | 224008 | -384.97020 | 107.5739 | -361.80410 | NaN | H4 4x3 Tenement10 | Home | Residential | ResidentialHigh |
| 124004 | 224584 | -34.41370 | 139.9190 | -1965.04100 | NaN | H3 4x3 Tenement09 | Home | Residential | ResidentialHigh |
| 124005 | 225435 | -176.36140 | 104.7191 | -118.84950 | NaN | H3 3x2 Tenement04a | Home | Residential | ResidentialHigh |
| 124006 | 226638 | -207.06440 | 109.6235 | 74.14597 | NaN | H4 4x4 Tenement09a | Home | Residential | ResidentialHigh |
124007 rows × 9 columns
# create a boolean mask where the TransferReason isn't a NaN value
mask = ~df['TransferReason'].isnull()
# count all transfer reasons recorded
df[mask]['TransferReason'].value_counts()
Student2 12557
Single2B 5325
Single2 5290
Student1 4615
Single1 1787
Single1B 1726
Worker2 1241
Student3 1238
Shopping 495
ShoppingF 487
ShoppingD 483
ShoppingB 475
ShoppingE 470
ShoppingH 463
ShoppingG 459
Worker1 454
ShoppingC 439
EntertainmentC 206
EntertainmentB 196
EntertainmentD 193
ChildCare 191
Entertainment 191
PartnerAdult 24
PartnerYoung 18
Sick 7
Single0 7
Single0B 4
Sick2 2
Family2 1
Name: TransferReason, dtype: int64
This time, you can see we have transfer reasons of type student1, student2, and student3.
This concludes that students only make outgoing offers when they are not assigned to any school.
# get the id of the first student that created a transfer offer with 'Student1' as the reason
studentID = df[df['TransferReason']=='Student1'].iloc[0]['CitizenID']
# get all logs the mod recorded that are related to this student
df[df['CitizenID']==studentID]
| CitizenID | PositionX | PositionY | PositionZ | TransferReason | BuildingName | currentLocation | m_service | m_subService | |
|---|---|---|---|---|---|---|---|---|---|
| 59 | 83298 | 165.90810 | 108.50330 | -278.058700 | NaN | H3 4x3 Shop12 | Visit | Commercial | CommercialHigh |
| 60 | 83298 | 56.25000 | 0.00000 | -18.750000 | Student1 | NaN | NaN | NaN | NaN |
| 61 | 83298 | 56.25000 | 0.00000 | -18.750000 | Student1 | NaN | NaN | NaN | NaN |
| 62 | 83298 | 42.03121 | 96.22192 | -2.363523 | NaN | H4 4x4 Tenement09a | Home | Residential | ResidentialHigh |
| 63 | 83298 | 42.03121 | 96.22192 | -2.363523 | ShoppingH | H4 4x4 Tenement09a | Home | Residential | ResidentialHigh |
| 64 | 83298 | 42.03121 | 96.22192 | -2.363523 | NaN | H4 4x4 Tenement09a | Home | Residential | ResidentialHigh |
| 65 | 83298 | 56.25000 | 0.00000 | -18.750000 | Student1 | NaN | NaN | NaN | NaN |
| 66 | 83298 | 42.03121 | 96.22192 | -2.363523 | NaN | H4 4x4 Tenement09a | Home | Residential | ResidentialHigh |
student1_mask = df['TransferReason'] == 'Student1'
student2_mask = df['TransferReason'] == 'Student2'
student3_mask = df['TransferReason'] == 'Student3'
# no student completed elementry school during the time I recorded
len(df[student1_mask&student2_mask])
0
# no student completed highschool during the time I recorded
len(df[student2_mask&student3_mask])
0
Log results from each harmony patch¶
** this is mainly for Adham **
# inspect rows logged from HumanAI patch on 'FindVisitPlace'
humanAIMask = ~df.isnull().any(axis=1)
df[humanAIMask]
| CitizenID | PositionX | PositionY | PositionZ | TransferReason | BuildingName | currentLocation | m_service | m_subService | |
|---|---|---|---|---|---|---|---|---|---|
| 8 | 82506 | -501.40810 | 112.93140 | -331.435000 | EntertainmentB | H2 3x4 Tenement06 | Home | Residential | ResidentialHigh |
| 18 | 82613 | 903.36340 | 97.53139 | -91.682100 | EntertainmentD | L3 3x3 Semi-detachedhouse02 | Home | Residential | ResidentialLow |
| 31 | 82717 | 206.23710 | 139.81480 | -2009.665000 | ShoppingB | H3 2x2 Tenement04 | Home | Residential | ResidentialHigh |
| 46 | 83133 | 19.66403 | 110.91940 | -404.126000 | Entertainment | L4 3x2 Villa02 | Home | Residential | ResidentialLow |
| 63 | 83298 | 42.03121 | 96.22192 | -2.363523 | ShoppingH | H4 4x4 Tenement09a | Home | Residential | ResidentialHigh |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 123642 | 202524 | 83.33214 | 102.21220 | -95.200300 | ShoppingF | H1 3x2 Tenement01 | Home | Residential | ResidentialHigh |
| 123669 | 310471 | 802.46420 | 96.28125 | -313.121200 | ShoppingH | L4 3x2 Villa02b | Home | Residential | ResidentialLow |
| 123750 | 562009 | -665.39200 | 114.11470 | -333.738100 | Shopping | H2 4x3 Tenement08 | Home | Residential | ResidentialHigh |
| 123776 | 627369 | -559.15520 | 114.81070 | -492.267300 | Shopping | L3 4x3 Detached09 | Home | Residential | ResidentialLow |
| 123928 | 32212 | -559.15520 | 114.81070 | -492.267300 | ShoppingF | L3 4x3 Detached09 | Home | Residential | ResidentialLow |
4741 rows × 9 columns
# inspect rows logged from TransferManager patch on 'AddOutgoingOffer'
transferManagerMask = df['BuildingName'].isnull()
transferManagerMask &= df['currentLocation'].isnull()
transferManagerMask &= df['m_service'].isnull()
transferManagerMask &= df['m_subService'].isnull()
cols = ['CitizenID', 'PositionX', 'PositionY', 'PositionZ', 'TransferReason']
df[transferManagerMask][cols]
| CitizenID | PositionX | PositionY | PositionZ | TransferReason | |
|---|---|---|---|---|---|
| 1 | 82472 | -56.25 | 0.0 | -1518.75 | Student2 |
| 6 | 82506 | -506.25 | 0.0 | -318.75 | Student2 |
| 16 | 82613 | 918.75 | 0.0 | -93.75 | Student2 |
| 29 | 82717 | 206.25 | 0.0 | -2006.25 | Student2 |
| 45 | 83133 | 18.75 | 0.0 | -393.75 | Student2 |
| ... | ... | ... | ... | ... | ... |
| 123419 | 582261 | 468.75 | 0.0 | -206.25 | Student1 |
| 123421 | 582261 | 468.75 | 0.0 | -206.25 | Student1 |
| 123464 | 712931 | -18.75 | 0.0 | -1856.25 | Student1 |
| 123488 | 797397 | -206.25 | 0.0 | -168.75 | Student1 |
| 123489 | 797397 | -206.25 | 0.0 | -168.75 | Student1 |
34303 rows × 5 columns
# inspect rows logged from ResidentAI patch on 'SimulationStep'
cols = list(df.columns)
cols.remove('TransferReason')
ResidentAIMask = ~df[cols].isnull()
df[ResidentAIMask][cols]
| CitizenID | PositionX | PositionY | PositionZ | BuildingName | currentLocation | m_service | m_subService | |
|---|---|---|---|---|---|---|---|---|
| 0 | 82472 | -72.59171 | 127.5228 | -1525.53400 | H3 4x3 Tenement09 | Home | Residential | ResidentialHigh |
| 1 | 82472 | -56.25000 | 0.0000 | -1518.75000 | NaN | NaN | NaN | NaN |
| 2 | 82472 | -72.59171 | 127.5228 | -1525.53400 | H3 4x3 Tenement09 | Home | Residential | ResidentialHigh |
| 3 | 82475 | 732.47920 | 95.4660 | 399.37210 | L4 4x3 Villa11 | Home | Residential | ResidentialLow |
| 4 | 82496 | 242.22690 | 111.3989 | -585.01860 | H3 4x3 Tenement04a | Home | Residential | ResidentialHigh |
| ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 124002 | 218915 | -34.86323 | 136.6353 | -1933.04400 | H3 4x4 Tenement05a | Home | Residential | ResidentialHigh |
| 124003 | 224008 | -384.97020 | 107.5739 | -361.80410 | H4 4x3 Tenement10 | Home | Residential | ResidentialHigh |
| 124004 | 224584 | -34.41370 | 139.9190 | -1965.04100 | H3 4x3 Tenement09 | Home | Residential | ResidentialHigh |
| 124005 | 225435 | -176.36140 | 104.7191 | -118.84950 | H3 3x2 Tenement04a | Home | Residential | ResidentialHigh |
| 124006 | 226638 | -207.06440 | 109.6235 | 74.14597 | H4 4x4 Tenement09a | Home | Residential | ResidentialHigh |
124007 rows × 8 columns