Skip to content

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

Back to top